diff --git a/packages/walletkit-android-bridge/src/api/streaming.ts b/packages/walletkit-android-bridge/src/api/streaming.ts index aaffaa77e..896d6f40f 100644 --- a/packages/walletkit-android-bridge/src/api/streaming.ts +++ b/packages/walletkit-android-bridge/src/api/streaming.ts @@ -33,19 +33,19 @@ type InternalStreamingManager = { providerConnectionUnsubs: Map void>; }; -function trackKotlinSub(providerId: string, subId: string): void { +function trackKotlinSub(providerId: string, subscriptionId: string): void { let subs = kotlinProviderSubs.get(providerId); if (!subs) { subs = new Set(); kotlinProviderSubs.set(providerId, subs); } - subs.add(subId); + subs.add(subscriptionId); } -function forgetKotlinSub(providerId: string, subId: string): void { +function forgetKotlinSub(providerId: string, subscriptionId: string): void { const subs = kotlinProviderSubs.get(providerId); if (!subs) return; - subs.delete(subId); + subs.delete(subscriptionId); if (subs.size === 0) { kotlinProviderSubs.delete(providerId); } @@ -85,14 +85,14 @@ class ProxyStreamingProvider implements StreamingProvider { } private watch(type: string, address: string | null, onChange: (update: unknown) => void): () => void { - const subId = uuidv7(); - kotlinSubCallbacks.set(subId, onChange); - trackKotlinSub(this.providerId, subId); - void bridgeRequest('kotlinProviderWatch', { providerId: this.providerId, subId, type, address }); + const subscriptionId = uuidv7(); + kotlinSubCallbacks.set(subscriptionId, onChange); + trackKotlinSub(this.providerId, subscriptionId); + void bridgeRequest('kotlinProviderWatch', { providerId: this.providerId, subscriptionId, type, address }); return () => { - kotlinSubCallbacks.delete(subId); - forgetKotlinSub(this.providerId, subId); - void bridgeRequest('kotlinProviderUnwatch', { subId }); + kotlinSubCallbacks.delete(subscriptionId); + forgetKotlinSub(this.providerId, subscriptionId); + void bridgeRequest('kotlinProviderUnwatch', { subscriptionId }); }; } @@ -123,23 +123,23 @@ class ProxyStreamingProvider implements StreamingProvider { dispose(): void { const subs = kotlinProviderSubs.get(this.providerId); if (!subs) return; - for (const subId of subs) { - kotlinSubCallbacks.delete(subId); - void bridgeRequest('kotlinProviderUnwatch', { subId }); + for (const subscriptionId of subs) { + kotlinSubCallbacks.delete(subscriptionId); + void bridgeRequest('kotlinProviderUnwatch', { subscriptionId }); } kotlinProviderSubs.delete(this.providerId); } } -export async function createTonCenterStreamingProvider(args: { config: TonCenterStreamingProviderConfig }) { +export async function createTonCenterStreamingProvider(config: TonCenterStreamingProviderConfig) { const instance = await getKit(); - const provider = new TonCenterStreamingProvider(instance.createFactoryContext(), args.config); + const provider = new TonCenterStreamingProvider(instance.createFactoryContext(), config); return { providerId: retain('streamingProvider', provider) }; } -export async function createTonApiStreamingProvider(args: { config: TonApiStreamingProviderConfig }) { +export async function createTonApiStreamingProvider(config: TonApiStreamingProviderConfig) { const instance = await getKit(); - const provider = new TonApiStreamingProvider(instance.createFactoryContext(), args.config); + const provider = new TonApiStreamingProvider(instance.createFactoryContext(), config); return { providerId: retain('streamingProvider', provider) }; } @@ -240,8 +240,8 @@ export async function registerKotlinStreamingProvider(args: { providerId: string instance.streaming.registerProvider(() => provider); } -export async function kotlinProviderDispatch(args: { subId: string; updateJson: string }) { - const callback = kotlinSubCallbacks.get(args.subId); +export async function kotlinProviderDispatch(args: { subscriptionId: string; updateJson: string }) { + const callback = kotlinSubCallbacks.get(args.subscriptionId); if (callback) { try { callback(JSON.parse(args.updateJson)); diff --git a/packages/walletkit-android-bridge/src/bridge.ts b/packages/walletkit-android-bridge/src/bridge.ts index 69c123a9b..41827f217 100644 --- a/packages/walletkit-android-bridge/src/bridge.ts +++ b/packages/walletkit-android-bridge/src/bridge.ts @@ -6,10 +6,12 @@ * */ -import type { WalletKitBridgeApi } from './types'; +import type { WalletKitApiMethod, WalletKitBridgeApi } from './types'; import { api } from './api'; -import { setBridgeApi, registerNativeCallHandler } from './transport/messaging'; -import { registerNativeResponseHandler } from './transport/nativeBridge'; +import { handleNativeCall, setBridgeApi } from './transport/messaging'; +import { handleNativeResponse } from './transport/nativeBridge'; +import { installPortHandshake, setInboundCallback } from './transport/port'; +import { warn, error } from './utils/logger'; declare global { interface Window { @@ -17,8 +19,47 @@ declare global { } } +interface IncomingCallEnvelope { + kind: 'call'; + id: string; + method: WalletKitApiMethod; + params?: unknown; +} + +interface IncomingResponseEnvelope { + kind: 'response'; + id: string; + result?: unknown; + error?: { message?: string }; +} + +type IncomingEnvelope = IncomingCallEnvelope | IncomingResponseEnvelope; + setBridgeApi(api as unknown as WalletKitBridgeApi); -registerNativeCallHandler(); -registerNativeResponseHandler(); + +// Synchronous: must be in place before native onPageFinished posts the port. +installPortHandshake(); + +setInboundCallback((json) => { + let envelope: IncomingEnvelope; + try { + envelope = JSON.parse(json) as IncomingEnvelope; + } catch (err) { + error('[walletkitBridge] Failed to parse inbound port message', err, json); + return; + } + switch (envelope.kind) { + case 'call': + handleNativeCall(envelope.id, envelope.method, envelope.params); + break; + case 'response': + handleNativeResponse(envelope.id, envelope.result, envelope.error); + break; + default: { + const exhaustive: never = envelope; + warn('[walletkitBridge] Unknown inbound envelope kind', exhaustive); + } + } +}); window.walletkitBridge = api as unknown as WalletKitBridgeApi; diff --git a/packages/walletkit-android-bridge/src/globals.d.ts b/packages/walletkit-android-bridge/src/globals.d.ts index 413ba0510..adf290697 100644 --- a/packages/walletkit-android-bridge/src/globals.d.ts +++ b/packages/walletkit-android-bridge/src/globals.d.ts @@ -7,13 +7,15 @@ */ // Re-export bridge types for backwards compatibility -import type { AndroidBridgeType, WalletKitNativeBridgeType, WalletKitBridgeApi, WalletKitApiMethod } from './types'; +import type { AndroidBridgeType, WalletKitNativeBridgeType, WalletKitBridgeApi } from './types'; declare global { interface Window { walletkitBridge?: WalletKitBridgeApi; - __walletkitCall?: (id: string, method: WalletKitApiMethod, paramsJson?: string | null) => void; - __walletkitResponse?: (id: string, resultJson?: string | null, errorJson?: string | null) => void; + // WalletKitNative still hosts the synchronous host calls (storageGet/Set, sessionCreate, + // apiSendBoc/RunGetMethod/GetBalance, …). The bidirectional bridge messaging that used + // to live on `__walletkitCall` / `__walletkitResponse` now flows through a + // WebMessagePort handed off from Kotlin during page load. WalletKitNative?: WalletKitNativeBridgeType; AndroidBridge?: AndroidBridgeType; } diff --git a/packages/walletkit-android-bridge/src/index.ts b/packages/walletkit-android-bridge/src/index.ts index dca1c4b30..bb7d3f883 100644 --- a/packages/walletkit-android-bridge/src/index.ts +++ b/packages/walletkit-android-bridge/src/index.ts @@ -9,7 +9,8 @@ /** * Entry point for Android WalletKit bridge. * This file ensures native polyfills are installed before the bridge code executes. - * The bridge bundle does not export anything - all communication happens via window.__walletkitCall. + * The bridge bundle does not export anything — all communication happens through the + * WebMessagePort handed off from the native side (see `transport/port.ts`). */ import './polyfills/setupNativeBridge'; import './bridge'; diff --git a/packages/walletkit-android-bridge/src/transport/messaging.ts b/packages/walletkit-android-bridge/src/transport/messaging.ts index d81c6ffc9..b556b3804 100644 --- a/packages/walletkit-android-bridge/src/transport/messaging.ts +++ b/packages/walletkit-android-bridge/src/transport/messaging.ts @@ -58,17 +58,6 @@ export async function handleCall(id: string, method: WalletKitApiMethod, params? } } -export function registerNativeCallHandler(): void { - window.__walletkitCall = (id, method, paramsJson) => { - let params: unknown = undefined; - if (paramsJson && paramsJson !== 'null') { - try { - params = JSON.parse(paramsJson); - } catch { - respond(id, undefined, { message: 'Invalid params JSON' }); - return; - } - } - void handleCall(id, method, params); - }; +export function handleNativeCall(id: string, method: WalletKitApiMethod, params: unknown): void { + void handleCall(id, method, params); } diff --git a/packages/walletkit-android-bridge/src/transport/nativeBridge.ts b/packages/walletkit-android-bridge/src/transport/nativeBridge.ts index 624f982f1..1268938d9 100644 --- a/packages/walletkit-android-bridge/src/transport/nativeBridge.ts +++ b/packages/walletkit-android-bridge/src/transport/nativeBridge.ts @@ -10,17 +10,12 @@ import { v7 as uuidv7 } from 'uuid'; import type { BridgePayload } from '../types'; import { bigIntReplacer } from '../utils/serialization'; -import { warn, error, info } from '../utils/logger'; - -// Reverse-RPC: JS sends {kind:'request', id, method, params} via postMessage. -// Kotlin responds via window.__walletkitResponse(id, resultJson, errorJson). +import { warn, error } from '../utils/logger'; +import { sendToNative } from './port'; const pendingRequests = new Map void; reject: (e: Error) => void }>(); -/** - * Synchronous bridge call via @JavascriptInterface (WalletKitNative.adapterCallSync). - * Used for sync WalletAdapter getters that cannot be async. - */ +// Sync host call via @JavascriptInterface — WebMessagePort is async and can't satisfy sync getters. export function bridgeRequestSync(method: string, params: Record): string { const native = window.WalletKitNative; if (!native || typeof native.adapterCallSync !== 'function') { @@ -29,9 +24,6 @@ export function bridgeRequestSync(method: string, params: Record): Promise { const id = uuidv7(); return new Promise((resolve, reject) => { @@ -40,76 +32,33 @@ export function bridgeRequest(method: string, params: Record): }); } -export function registerNativeResponseHandler(): void { - window.__walletkitResponse = (id: string, resultJson?: string | null, errorJson?: string | null) => { - const entry = pendingRequests.get(id); - if (!entry) { - warn('[walletkitBridge] __walletkitResponse: no pending request for id', id); - return; - } - pendingRequests.delete(id); - - if (errorJson) { - try { - const err = JSON.parse(errorJson); - entry.reject(new Error(err.message ?? 'Native request failed')); - } catch { - entry.reject(new Error(errorJson)); - } - return; - } - - if (resultJson) { - try { - entry.resolve(JSON.parse(resultJson)); - } catch { - // If it's not JSON, return the raw string - entry.resolve(resultJson); - } - } else { - entry.resolve(undefined); - } - }; - info('[walletkitBridge] __walletkitResponse handler registered'); -} - -/** - * Resolves WalletKit's native bridge implementation exposed on the global scope. - */ -export function resolveNativeBridge(scope: typeof globalThis) { - const candidate = (scope as typeof globalThis & { WalletKitNative?: { postMessage?: (json: string) => void } }) - .WalletKitNative; - if (candidate && typeof candidate.postMessage === 'function') { - return candidate.postMessage.bind(candidate); +export function handleNativeResponse(id: string, resultJson: unknown, errorJson: unknown): void { + const entry = pendingRequests.get(id); + if (!entry) { + warn('[walletkitBridge] handleNativeResponse: no pending request for id', id); + return; } - const windowRef = typeof scope.window === 'object' && scope.window ? scope.window : undefined; - const windowCandidate = windowRef?.WalletKitNative; - if (windowCandidate && typeof windowCandidate.postMessage === 'function') { - return windowCandidate.postMessage.bind(windowCandidate); + pendingRequests.delete(id); + + if (errorJson) { + const err = errorJson as { message?: string }; + entry.reject(new Error(err.message ?? 'Native request failed')); + return; } - return null; -} -/** - * Resolves the Android bridge exposed by the host WebView. - */ -export function resolveAndroidBridge(scope: typeof globalThis) { - const candidate = (scope as typeof globalThis & { AndroidBridge?: { postMessage?: (json: string) => void } }) - .AndroidBridge; - if (candidate && typeof candidate.postMessage === 'function') { - return candidate.postMessage.bind(candidate); + if (resultJson === null || resultJson === undefined) { + entry.resolve(undefined); + return; } - const windowRef = typeof scope.window === 'object' && scope.window ? scope.window : undefined; - const windowCandidate = windowRef?.AndroidBridge; - if (windowCandidate && typeof windowCandidate.postMessage === 'function') { - return windowCandidate.postMessage.bind(windowCandidate); + + if (typeof resultJson === 'string') { + entry.resolve(JSON.parse(resultJson)); + return; } - return null; + + entry.resolve(resultJson); } -/** - * Sends a payload to the native bridge, falling back to debug logging when unavailable. - */ export function postToNative(payload: BridgePayload): void { if (payload === null || (typeof payload !== 'object' && typeof payload !== 'function')) { const diagnostic = { @@ -121,18 +70,5 @@ export function postToNative(payload: BridgePayload): void { throw new Error('Invalid payload - must be an object'); } const json = JSON.stringify(payload, bigIntReplacer); - const nativePostMessage = resolveNativeBridge(window); - if (nativePostMessage) { - nativePostMessage(json); - return; - } - const androidPostMessage = resolveAndroidBridge(window); - if (androidPostMessage) { - androidPostMessage(json); - return; - } - if (payload.kind === 'event') { - throw new Error('Native bridge not available - cannot deliver event'); - } - warn('[walletkitBridge] postToNative: no native handler', payload); + sendToNative(json); } diff --git a/packages/walletkit-android-bridge/src/transport/port.ts b/packages/walletkit-android-bridge/src/transport/port.ts new file mode 100644 index 000000000..2ce7bc847 --- /dev/null +++ b/packages/walletkit-android-bridge/src/transport/port.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { warn, error } from '../utils/logger'; + +const HANDSHAKE_TAG = '__walletkit_bridge_init'; + +let port: MessagePort | null = null; +let inboundCallback: ((json: string) => void) | null = null; +const pendingOutbound: string[] = []; + +function flushPending(p: MessagePort): void { + while (pendingOutbound.length > 0) { + const next = pendingOutbound.shift() as string; + p.postMessage(next); + } +} + +export function sendToNative(json: string): void { + if (port) { + port.postMessage(json); + return; + } + pendingOutbound.push(json); +} + +export function setInboundCallback(callback: (json: string) => void): void { + inboundCallback = callback; +} + +// Must run synchronously during bundle parse — Kotlin posts the port from +// WebViewClient.onPageFinished and the listener has to be in place by then. +export function installPortHandshake(): void { + window.addEventListener('message', (event) => { + if (event.data !== HANDSHAKE_TAG) { + warn('[walletkitBridge] Ignoring window message — not the handshake tag', event.data); + return; + } + const incoming = event.ports?.[0]; + if (!incoming) { + error('[walletkitBridge] Handshake message had no port'); + return; + } + if (port) { + warn('[walletkitBridge] Bridge port already initialised — ignoring duplicate handshake'); + return; + } + incoming.onmessage = (e) => { + const data = typeof e.data === 'string' ? e.data : JSON.stringify(e.data); + const cb = inboundCallback; + if (!cb) { + warn('[walletkitBridge] Inbound port message arrived before callback was installed'); + return; + } + cb(data); + }; + incoming.start(); + port = incoming; + flushPending(port); + }); +} diff --git a/packages/walletkit-android-bridge/src/types/api.ts b/packages/walletkit-android-bridge/src/types/api.ts index 87874b061..928531048 100644 --- a/packages/walletkit-android-bridge/src/types/api.ts +++ b/packages/walletkit-android-bridge/src/types/api.ts @@ -31,12 +31,14 @@ import type { import type { DeDustSwapProviderConfig } from '@ton/walletkit/swap/dedust'; import type { OmnistonSwapProviderConfig } from '@ton/walletkit/swap/omniston'; +import type { TONBase64, TONHex, TONUserFriendlyAddress } from './brands'; +import type { WalletKitBridgeEventCallback } from './events'; +import type { WalletKitBridgeInitConfig } from './walletkit'; + /** * TonConnect event payload types that can be returned from processInternalBrowserRequest. */ export type TonConnectEventPayload = ConnectEvent | ConnectEventError | WalletResponse | DisconnectEvent; -import type { WalletKitBridgeEventCallback } from './events'; -import type { WalletKitBridgeInitConfig } from './walletkit'; export type PromiseOrValue = T | Promise; @@ -64,12 +66,12 @@ export interface CreateSignerFromMnemonicArgs { } export interface CreateSignerFromPrivateKeyArgs { - secretKey: string; + secretKey: TONHex; } export interface CreateSignerFromCustomArgs { signerId: string; - publicKey: string; + publicKey: TONHex; } export interface CreateWalletAdapterArgs { @@ -104,19 +106,19 @@ export interface GetRecentTransactionsArgs { export interface CreateTransferTonTransactionArgs { walletId: string; - recipientAddress: string; + recipientAddress: TONUserFriendlyAddress; transferAmount: string; comment?: string; - body?: string; - stateInit?: string; + body?: TONBase64; + stateInit?: TONBase64; } export interface MultiTransferMessage { - recipientAddress: string; + recipientAddress: TONUserFriendlyAddress; transferAmount: string; comment?: string; - body?: string; - stateInit?: string; + body?: TONBase64; + stateInit?: TONBase64; } export interface CreateTransferMultiTonTransactionArgs { @@ -144,7 +146,7 @@ export interface ApproveConnectRequestArgs { event: TonConnectRequestEvent; response?: { proof: { - signature: string; + signature: TONBase64; timestamp: number; domain: { lengthBytes: number; @@ -164,7 +166,7 @@ export interface RejectConnectRequestArgs { export interface ApproveTransactionRequestArgs { event: TonConnectRequestEvent; response?: { - signedBoc: string; + signedBoc: TONBase64; }; } @@ -176,7 +178,7 @@ export interface RejectTransactionRequestArgs { export interface ApproveSignDataRequestArgs { event: TonConnectRequestEvent; response?: { - signature: string; + signature: TONBase64; timestamp: number; domain: string; }; @@ -264,14 +266,6 @@ export interface EmitBrowserBridgeRequestArgs { request: string; } -export interface CreateTonCenterStreamingProviderArgs { - config: TonCenterStreamingProviderConfig; -} - -export interface CreateTonApiStreamingProviderArgs { - config: TonApiStreamingProviderConfig; -} - export interface RegisterStreamingProviderArgs { providerId: string; } @@ -282,7 +276,7 @@ export interface StreamingHasProviderArgs { export interface StreamingWatchArgs { network: { chainId: string }; - address: string; + address: TONUserFriendlyAddress; types: StreamingWatchType[]; } @@ -296,7 +290,7 @@ export interface StreamingWatchConnectionChangeArgs { export interface StreamingWatchAddressArgs { network: { chainId: string }; - address: string; + address: TONUserFriendlyAddress; } export interface RegisterKotlinStreamingProviderArgs { @@ -341,7 +335,7 @@ export interface SetDefaultStakingProviderArgs { export interface GetStakingQuoteArgs { direction: 'stake' | 'unstake'; amount: string; - userAddress?: string; + userAddress?: TONUserFriendlyAddress; network?: { chainId: string }; unstakeMode?: string; providerOptions?: unknown; @@ -350,7 +344,7 @@ export interface GetStakingQuoteArgs { export interface BuildStakeTransactionArgs { quote: StakingQuoteResponse; - userAddress: string; + userAddress: TONUserFriendlyAddress; providerOptions?: unknown; providerId?: string; } @@ -369,7 +363,7 @@ export interface StakingQuoteResponse { } export interface GetStakedBalanceArgs { - userAddress: string; + userAddress: TONUserFriendlyAddress; network?: { chainId: string }; providerId?: string; } @@ -421,22 +415,26 @@ export interface WalletKitBridgeApi { setEventsListeners(args?: SetEventsListenersArgs): PromiseOrValue<{ ok: true }>; removeEventListeners(): PromiseOrValue<{ ok: true }>; mnemonicToKeyPair(args: MnemonicToKeyPairArgs): PromiseOrValue<{ publicKey: Uint8Array; secretKey: Uint8Array }>; - sign(args: SignArgs): PromiseOrValue; + sign(args: SignArgs): PromiseOrValue; createTonMnemonic(args?: CreateTonMnemonicArgs): PromiseOrValue; createSignerFromMnemonic( args: CreateSignerFromMnemonicArgs, - ): PromiseOrValue<{ signerId: string; publicKey: string }>; + ): PromiseOrValue<{ signerId: string; publicKey: TONHex }>; createSignerFromPrivateKey( args: CreateSignerFromPrivateKeyArgs, - ): PromiseOrValue<{ signerId: string; publicKey: string }>; - createSignerFromCustom(args: CreateSignerFromCustomArgs): PromiseOrValue<{ signerId: string; publicKey: string }>; - createV5R1WalletAdapter(args: CreateWalletAdapterArgs): PromiseOrValue<{ adapterId: string; address: string }>; - createV4R2WalletAdapter(args: CreateWalletAdapterArgs): PromiseOrValue<{ adapterId: string; address: string }>; + ): PromiseOrValue<{ signerId: string; publicKey: TONHex }>; + createSignerFromCustom(args: CreateSignerFromCustomArgs): PromiseOrValue<{ signerId: string; publicKey: TONHex }>; + createV5R1WalletAdapter( + args: CreateWalletAdapterArgs, + ): PromiseOrValue<{ adapterId: string; address: TONUserFriendlyAddress }>; + createV4R2WalletAdapter( + args: CreateWalletAdapterArgs, + ): PromiseOrValue<{ adapterId: string; address: TONUserFriendlyAddress }>; addWallet(args: AddWalletArgs): PromiseOrValue<{ walletId: string | undefined; wallet: Wallet } | null>; releaseRef(args: ReleaseRefArgs): PromiseOrValue<{ ok: boolean }>; getWallets(): PromiseOrValue<{ walletId: string | undefined; wallet: Wallet }[]>; getWallet(args: { walletId: string }): PromiseOrValue<{ walletId: string | undefined; wallet: Wallet } | null>; - getWalletAddress(args: { walletId: string }): PromiseOrValue; + getWalletAddress(args: { walletId: string }): PromiseOrValue; removeWallet(args: RemoveWalletArgs): PromiseOrValue; getBalance(args: GetBalanceArgs): PromiseOrValue; getRecentTransactions(args: GetRecentTransactionsArgs): PromiseOrValue; @@ -449,9 +447,11 @@ export interface WalletKitBridgeApi { sendTransaction(args: TransactionContentArgs): PromiseOrValue; approveConnectRequest(args: ApproveConnectRequestArgs): PromiseOrValue; rejectConnectRequest(args: RejectConnectRequestArgs): PromiseOrValue<{ success: boolean }>; - approveTransactionRequest(args: ApproveTransactionRequestArgs): PromiseOrValue<{ signedBoc: string }>; + approveTransactionRequest(args: ApproveTransactionRequestArgs): PromiseOrValue<{ signedBoc: TONBase64 }>; rejectTransactionRequest(args: RejectTransactionRequestArgs): PromiseOrValue<{ success: boolean }>; - approveSignDataRequest(args: ApproveSignDataRequestArgs): PromiseOrValue<{ signature: string; timestamp: number }>; + approveSignDataRequest( + args: ApproveSignDataRequestArgs, + ): PromiseOrValue<{ signature: TONBase64; timestamp: number }>; rejectSignDataRequest(args: RejectSignDataRequestArgs): PromiseOrValue<{ success: boolean }>; listSessions(): PromiseOrValue<{ items: TONConnectSession[] }>; disconnectSession(args?: DisconnectSessionArgs): PromiseOrValue<{ ok: boolean }>; @@ -462,16 +462,14 @@ export interface WalletKitBridgeApi { getJettons(args: GetJettonsArgs): PromiseOrValue; createTransferJettonTransaction(args: CreateTransferJettonTransactionArgs): PromiseOrValue; getJettonBalance(args: GetJettonBalanceArgs): PromiseOrValue; - getJettonWalletAddress(args: GetJettonWalletAddressArgs): PromiseOrValue; + getJettonWalletAddress(args: GetJettonWalletAddressArgs): PromiseOrValue; processInternalBrowserRequest(args: ProcessInternalBrowserRequestArgs): PromiseOrValue; emitBrowserPageStarted(args: EmitBrowserPageArgs): PromiseOrValue<{ success: boolean }>; emitBrowserPageFinished(args: EmitBrowserPageArgs): PromiseOrValue<{ success: boolean }>; emitBrowserError(args: EmitBrowserErrorArgs): PromiseOrValue<{ success: boolean }>; emitBrowserBridgeRequest(args: EmitBrowserBridgeRequestArgs): PromiseOrValue<{ success: boolean }>; - createTonCenterStreamingProvider( - args: CreateTonCenterStreamingProviderArgs, - ): PromiseOrValue<{ providerId: string }>; - createTonApiStreamingProvider(args: CreateTonApiStreamingProviderArgs): PromiseOrValue<{ providerId: string }>; + createTonCenterStreamingProvider(config: TonCenterStreamingProviderConfig): PromiseOrValue<{ providerId: string }>; + createTonApiStreamingProvider(config: TonApiStreamingProviderConfig): PromiseOrValue<{ providerId: string }>; registerStreamingProvider(args: RegisterStreamingProviderArgs): PromiseOrValue; streamingHasProvider(args: StreamingHasProviderArgs): PromiseOrValue<{ hasProvider: boolean }>; streamingWatch(args: StreamingWatchArgs): PromiseOrValue<{ subscriptionId: string }>; diff --git a/packages/walletkit-android-bridge/src/types/brands.ts b/packages/walletkit-android-bridge/src/types/brands.ts new file mode 100644 index 000000000..2a06ebef4 --- /dev/null +++ b/packages/walletkit-android-bridge/src/types/brands.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +declare const tonHexBrand: unique symbol; +declare const tonBase64Brand: unique symbol; +declare const tonUserFriendlyAddressBrand: unique symbol; +declare const tonRawAddressBrand: unique symbol; + +export type TONHex = string & { readonly [tonHexBrand]: true }; +export type TONBase64 = string & { readonly [tonBase64Brand]: true }; +export type TONUserFriendlyAddress = string & { readonly [tonUserFriendlyAddressBrand]: true }; +export type TONRawAddress = string & { readonly [tonRawAddressBrand]: true }; + +const HEX_PATTERN = /^(?:0x|0X)?[0-9a-fA-F]+$/; +const BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/; +const BASE64URL_PATTERN = /^[A-Za-z0-9_-]*={0,2}$/; +const RAW_ADDRESS_PATTERN = /^-?\d+:[0-9a-fA-F]{64}$/; + +export function asTONHex(value: string): TONHex { + if (!HEX_PATTERN.test(value) || value.replace(/^0x/i, '').length % 2 !== 0) { + throw new TypeError(`Not a valid hex string: ${value}`); + } + return value as TONHex; +} + +export function asTONBase64(value: string): TONBase64 { + if (!BASE64_PATTERN.test(value) && !BASE64URL_PATTERN.test(value)) { + throw new TypeError(`Not a valid base64 string: ${value}`); + } + return value as TONBase64; +} + +export function asTONUserFriendlyAddress(value: string): TONUserFriendlyAddress { + if (value.length !== 48 || !BASE64URL_PATTERN.test(value)) { + throw new TypeError(`Not a valid user-friendly TON address: ${value}`); + } + return value as TONUserFriendlyAddress; +} + +export function asTONRawAddress(value: string): TONRawAddress { + if (!RAW_ADDRESS_PATTERN.test(value)) { + throw new TypeError(`Not a valid raw TON address: ${value}`); + } + return value as TONRawAddress; +} + +export function unbrand(value: TONHex | TONBase64 | TONUserFriendlyAddress | TONRawAddress): string { + return value; +} diff --git a/packages/walletkit-android-bridge/src/types/index.ts b/packages/walletkit-android-bridge/src/types/index.ts index 3e63ab152..257c518d5 100644 --- a/packages/walletkit-android-bridge/src/types/index.ts +++ b/packages/walletkit-android-bridge/src/types/index.ts @@ -7,6 +7,7 @@ */ export * from './api'; +export * from './brands'; export * from './bridge'; export * from './events'; export * from './walletkit';