diff --git a/packages/walletkit-android-bridge/src/api/cryptography.ts b/packages/walletkit-android-bridge/src/api/cryptography.ts index 57a767b0c..816202508 100644 --- a/packages/walletkit-android-bridge/src/api/cryptography.ts +++ b/packages/walletkit-android-bridge/src/api/cryptography.ts @@ -7,17 +7,11 @@ */ /** - * Cryptographic helpers backed by WalletKit and custom signer coordination. + * Cryptographic helpers. */ -import type { Hex } from '@ton/walletkit'; import { CreateTonMnemonic, MnemonicToKeyPair, DefaultSignature } from '../core/moduleLoader'; -export async function signWithCustomSigner(signerId: string, bytes: Uint8Array): Promise { - const result = await window.WalletKitNative?.signWithCustomSigner?.(signerId, Array.from(bytes)); - return result as Hex; -} - export async function mnemonicToKeyPair(args: { mnemonic: string[]; mnemonicType?: string }) { if (!MnemonicToKeyPair) { throw new Error('MnemonicToKeyPair module not loaded'); diff --git a/packages/walletkit-android-bridge/src/api/eventListeners.ts b/packages/walletkit-android-bridge/src/api/eventListeners.ts index ebef79dee..08414c752 100644 --- a/packages/walletkit-android-bridge/src/api/eventListeners.ts +++ b/packages/walletkit-android-bridge/src/api/eventListeners.ts @@ -14,24 +14,11 @@ import type { SignDataRequestEvent, } from '@ton/walletkit'; -/** - * Shared event listener references used to manage WalletKit callbacks. - */ -export type ConnectEventListener = ((event: ConnectionRequestEvent) => void) | null; -export type TransactionEventListener = ((event: SendTransactionRequestEvent) => void) | null; -export type SignDataEventListener = ((event: SignDataRequestEvent) => void) | null; -export type DisconnectEventListener = ((event: DisconnectionEvent) => void) | null; -export type ErrorEventListener = ((event: RequestErrorEvent) => void) | null; - -/** - * Union type for all bridge event listeners. - */ -export type BridgeEventListener = - | ConnectEventListener - | TransactionEventListener - | SignDataEventListener - | DisconnectEventListener - | ErrorEventListener; +type ConnectEventListener = ((event: ConnectionRequestEvent) => void) | null; +type TransactionEventListener = ((event: SendTransactionRequestEvent) => void) | null; +type SignDataEventListener = ((event: SignDataRequestEvent) => void) | null; +type DisconnectEventListener = ((event: DisconnectionEvent) => void) | null; +type ErrorEventListener = ((event: RequestErrorEvent) => void) | null; export const eventListeners = { onConnectListener: null as ConnectEventListener, diff --git a/packages/walletkit-android-bridge/src/api/index.ts b/packages/walletkit-android-bridge/src/api/index.ts index 28629bf0f..ad0f16fe5 100644 --- a/packages/walletkit-android-bridge/src/api/index.ts +++ b/packages/walletkit-android-bridge/src/api/index.ts @@ -34,11 +34,16 @@ export const api: WalletKitBridgeApi = { sign: cryptography.sign, createTonMnemonic: cryptography.createTonMnemonic, - // Wallets - createSigner: wallets.createSigner, - createAdapter: wallets.createAdapter, - getAdapterAddress: wallets.getAdapterAddress, + // Wallets — 3-step factory + createSignerFromMnemonic: wallets.createSignerFromMnemonic, + createSignerFromPrivateKey: wallets.createSignerFromPrivateKey, + createSignerFromCustom: wallets.createSignerFromCustom, + createV5R1WalletAdapter: wallets.createV5R1WalletAdapter, + createV4R2WalletAdapter: wallets.createV4R2WalletAdapter, + + // Wallets — unified addWallet (registry path + proxy adapter path) addWallet: wallets.addWallet, + releaseRef: wallets.releaseRef, getWallets: wallets.getWallets, getWallet: wallets.getWalletById, getWalletAddress: wallets.getWalletAddress, @@ -85,5 +90,3 @@ export const api: WalletKitBridgeApi = { emitBrowserError: browser.emitBrowserError, emitBrowserBridgeRequest: browser.emitBrowserBridgeRequest, } as unknown as WalletKitBridgeApi; - -export type { BridgeEventListener } from './eventListeners'; diff --git a/packages/walletkit-android-bridge/src/api/jettons.ts b/packages/walletkit-android-bridge/src/api/jettons.ts index 17bb430f0..d2db7679c 100644 --- a/packages/walletkit-android-bridge/src/api/jettons.ts +++ b/packages/walletkit-android-bridge/src/api/jettons.ts @@ -6,12 +6,6 @@ * */ -/** - * jettons.ts – Jetton operations - * - * Minimal bridge for jetton operations. - */ - import { walletCall } from '../utils/bridge'; export const getJettons = (args: { walletId: string }) => walletCall('getJettons', args); diff --git a/packages/walletkit-android-bridge/src/api/nft.ts b/packages/walletkit-android-bridge/src/api/nft.ts index b504973ce..a0f8b7a9c 100644 --- a/packages/walletkit-android-bridge/src/api/nft.ts +++ b/packages/walletkit-android-bridge/src/api/nft.ts @@ -6,12 +6,6 @@ * */ -/** - * nft.ts – NFT operations - * - * Minimal bridge for NFT operations. - */ - import { walletCall } from '../utils/bridge'; export const getNfts = (args: { walletId: string }) => walletCall('getNfts', args); diff --git a/packages/walletkit-android-bridge/src/api/transactions.ts b/packages/walletkit-android-bridge/src/api/transactions.ts index 13286bd08..ae94c2e46 100644 --- a/packages/walletkit-android-bridge/src/api/transactions.ts +++ b/packages/walletkit-android-bridge/src/api/transactions.ts @@ -6,12 +6,6 @@ * */ -/** - * transactions.ts – TON transaction operations - * - * Minimal bridge - just forwards calls to WalletKit. - */ - import type { TransactionRequest } from '@ton/walletkit'; import { walletCall, clientCall, getKit, getWallet } from '../utils/bridge'; diff --git a/packages/walletkit-android-bridge/src/api/wallets.ts b/packages/walletkit-android-bridge/src/api/wallets.ts index e9f32c221..27fe372f0 100644 --- a/packages/walletkit-android-bridge/src/api/wallets.ts +++ b/packages/walletkit-android-bridge/src/api/wallets.ts @@ -6,20 +6,86 @@ * */ -/** - * wallets.ts – Wallet management operations - * - * Pure pass-through bridge - returns raw JS objects/proxies. - * Kotlin is responsible for adapting to whatever JS returns. - */ - -import type { Hex, Network, WalletAdapter } from '@ton/walletkit'; +import type { Hex, Network, WalletAdapter, ApiClient, Base64String, UserFriendlyAddress } from '@ton/walletkit'; +import type { WalletId } from '@ton/walletkit'; +import type { TransactionRequest } from '@ton/walletkit'; +import type { PreparedSignData } from '@ton/walletkit'; +import type { ProofMessage } from '@ton/walletkit'; import { Signer, WalletV4R2Adapter, WalletV5R1Adapter } from '../core/moduleLoader'; import { kit, wallet, getKit } from '../utils/bridge'; -import { signWithCustomSigner } from './cryptography'; +import { retain, retainWithId, get, release } from '../utils/registry'; +import { bridgeRequest, bridgeRequestSync } from '../transport/nativeBridge'; + +// Wraps a Kotlin-side TONWalletAdapter. Sync getters call Kotlin synchronously +// via @JavascriptInterface; signing/stateInit delegate via async reverse-RPC. +class ProxyWalletAdapter implements WalletAdapter { + constructor( + private readonly adapterId: string, + private readonly apiClientProvider: (network: Network) => ApiClient, + ) {} + + getPublicKey(): Hex { + return bridgeRequestSync('getPublicKey', { adapterId: this.adapterId }) as Hex; + } -type SignerInstance = { sign: (bytes: Iterable) => Promise; publicKey: Hex }; + getNetwork(): Network { + const raw = bridgeRequestSync('getNetwork', { adapterId: this.adapterId }); + const parsed = JSON.parse(raw); + return parsed as Network; + } + + getClient(): ApiClient { + return this.apiClientProvider(this.getNetwork()); + } + + getAddress(): UserFriendlyAddress { + return bridgeRequestSync('getAddress', { adapterId: this.adapterId }) as UserFriendlyAddress; + } + + getWalletId(): WalletId { + return bridgeRequestSync('getWalletId', { adapterId: this.adapterId }) as WalletId; + } + + async getStateInit(): Promise { + const result = await bridgeRequest('adapterGetStateInit', { adapterId: this.adapterId }); + if (!result) throw new Error('adapterGetStateInit: no result from native'); + return result as Base64String; + } + + async getSignedSendTransaction( + input: TransactionRequest, + options?: { fakeSignature: boolean }, + ): Promise { + const result = await bridgeRequest('adapterSignTransaction', { + adapterId: this.adapterId, + input: JSON.stringify(input), + fakeSignature: options?.fakeSignature ?? false, + }); + if (!result) throw new Error('adapterSignTransaction: no result from native'); + return result as Base64String; + } + + async getSignedSignData(input: PreparedSignData, options?: { fakeSignature: boolean }): Promise { + const result = await bridgeRequest('adapterSignData', { + adapterId: this.adapterId, + input: JSON.stringify(input), + fakeSignature: options?.fakeSignature ?? false, + }); + if (!result) throw new Error('adapterSignData: no result from native'); + return result as Hex; + } + + async getSignedTonProof(input: ProofMessage, options?: { fakeSignature: boolean }): Promise { + const result = await bridgeRequest('adapterSignTonProof', { + adapterId: this.adapterId, + input: JSON.stringify(input), + fakeSignature: options?.fakeSignature ?? false, + }); + if (!result) throw new Error('adapterSignTonProof: no result from native'); + return result as Hex; + } +} /** * Lists all wallets. @@ -29,9 +95,6 @@ export async function getWallets() { return wallets.map((w) => ({ walletId: w.getWalletId?.(), wallet: w })); } -/** - * Get a single wallet by walletId. - */ export async function getWalletById(args: { walletId: string }) { const w = await kit('getWallet', args.walletId); if (!w) return null; @@ -50,103 +113,106 @@ export async function getBalance(args: { walletId: string }) { return wallet(args.walletId, 'getBalance'); } -const signerStore = new Map(); -const adapterStore = new Map(); +export async function createSignerFromMnemonic(args: { mnemonic: string[]; mnemonicType?: string }) { + if (!Signer) throw new Error('Signer module not loaded'); + const signer = await Signer.fromMnemonic(args.mnemonic, { type: args.mnemonicType ?? 'ton' }); + const signerId = retain('signer', signer); + return { signerId, publicKey: signer.publicKey }; +} -type CreateAdapterArgs = { - signerId: string; - isCustom?: boolean; - publicKey?: string; - walletVersion?: string; - network: string; - workchain: number; - walletId?: string; -}; - -type CreateSignerArgs = { - mnemonic?: string[]; - secretKey?: string; - mnemonicType?: string; -}; - -type AddWalletArgs = { - adapterId: string; -}; - -async function getSigner(args: CreateAdapterArgs): Promise { - if (args.isCustom && args.publicKey) { - return { - sign: async (bytes: Iterable): Promise => { - return await signWithCustomSigner(args.signerId, Uint8Array.from(bytes)); - }, - publicKey: args.publicKey as Hex, - }; - } +export async function createSignerFromPrivateKey(args: { secretKey: string }) { + if (!Signer) throw new Error('Signer module not loaded'); + const signer = await Signer.fromPrivateKey(args.secretKey); + const signerId = retain('signer', signer); + return { signerId, publicKey: signer.publicKey }; +} - const storedSigner = signerStore.get(args.signerId); - if (!storedSigner) { - throw new Error(`Signer not found: ${args.signerId}`); - } - return storedSigner; +export async function createSignerFromCustom(args: { signerId: string; publicKey: string }) { + const { signerId, publicKey } = args; + const proxySigner = { + publicKey: publicKey as Hex, + sign: async (bytes: Iterable): Promise => { + const result = await bridgeRequest('signWithCustomSigner', { + signerId, + data: Array.from(bytes), + }); + if (!result) throw new Error('signWithCustomSigner: no result from native'); + return result as Hex; + }, + }; + retainWithId(signerId, proxySigner); + return { signerId, publicKey }; } -export async function createSigner(args: CreateSignerArgs) { - if (!Signer) { - throw new Error('Signer module not loaded'); - } - if (!args.mnemonic?.length && !args.secretKey) { - throw new Error('Either mnemonic or secretKey is required'); - } - const signer = - args.mnemonic && args.mnemonic.length > 0 - ? ((await Signer.fromMnemonic(args.mnemonic, { type: args.mnemonicType || 'ton' })) as SignerInstance) - : ((await Signer.fromPrivateKey(args.secretKey as string)) as SignerInstance); +export async function createV5R1WalletAdapter(args: { + signerId: string; + network: { chainId: string }; + workchain?: number; + walletId?: number; +}) { + const instance = await getKit(); + const signer = get<{ publicKey: Hex; sign: (data: Iterable) => Promise }>(args.signerId); + if (!signer) throw new Error(`Signer not found in registry: ${args.signerId}`); - const tempId = `signer_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; - signerStore.set(tempId, signer); + const network = args.network as unknown as Network; + if (!WalletV5R1Adapter) throw new Error('WalletV5R1Adapter module not loaded'); + const adapter = await WalletV5R1Adapter.create(signer, { + client: instance.getApiClient(network), + network, + workchain: args.workchain ?? 0, + walletId: args.walletId, + }); - return { _tempId: tempId, signer }; + const adapterId = retain('adapter', adapter); + return { adapterId, address: adapter.getAddress() }; } -export async function createAdapter(args: CreateAdapterArgs) { +export async function createV4R2WalletAdapter(args: { + signerId: string; + network: { chainId: string }; + workchain?: number; + walletId?: number; +}) { const instance = await getKit(); - const signer = await getSigner(args); - const AdapterClass = args.walletVersion === 'v5r1' ? WalletV5R1Adapter : WalletV4R2Adapter; - if (!AdapterClass) { - throw new Error(`WalletAdapter module not loaded`); - } + const signer = get<{ publicKey: Hex; sign: (data: Iterable) => Promise }>(args.signerId); + if (!signer) throw new Error(`Signer not found in registry: ${args.signerId}`); + const network = args.network as unknown as Network; - const adapter = await AdapterClass.create(signer, { + if (!WalletV4R2Adapter) throw new Error('WalletV4R2Adapter module not loaded'); + const adapter = await WalletV4R2Adapter.create(signer, { client: instance.getApiClient(network), network, - workchain: args.workchain, + workchain: args.workchain ?? 0, walletId: args.walletId, }); - const tempId = `adapter_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; - adapterStore.set(tempId, adapter); - - return { _tempId: tempId, adapter }; -} - -export async function getAdapterAddress(args: { adapterId: string }) { - const adapter = adapterStore.get(args.adapterId) as WalletAdapter | undefined; - if (!adapter) { - throw new Error(`Adapter not found: ${args.adapterId}`); - } - return adapter.getAddress(); + const adapterId = retain('adapter', adapter); + return { adapterId, address: adapter.getAddress() }; } -export async function addWallet(args: AddWalletArgs) { +export async function addWallet(args: { adapterId: string }) { const instance = await getKit(); - const adapter = adapterStore.get(args.adapterId); - if (!adapter) { - throw new Error(`Adapter not found: ${args.adapterId}`); + + // Check if adapter exists in JS registry (BridgeWalletAdapter / JS-created adapter path) + const existingAdapter = get(args.adapterId); + if (existingAdapter) { + const w = await instance.addWallet(existingAdapter as Parameters[0]); + if (!w) return null; + return { walletId: w.getWalletId?.(), wallet: w }; } - const w = await instance.addWallet(adapter as Parameters[0]); - adapterStore.delete(args.adapterId); + // Kotlin-side adapter — create proxy that calls Kotlin synchronously for getters + const proxyAdapter = new ProxyWalletAdapter(args.adapterId, (network) => instance.getApiClient(network)); + const w = await instance.addWallet(proxyAdapter as Parameters[0]); if (!w) return null; return { walletId: w.getWalletId?.(), wallet: w }; } + +/** + * Releases a JS-side registry object (signer or adapter created by createV5R1/createV4R2). + */ +export function releaseRef(args: { id: string }) { + release(args.id); + return { ok: true }; +} diff --git a/packages/walletkit-android-bridge/src/bridge.ts b/packages/walletkit-android-bridge/src/bridge.ts index c16698614..b139db6aa 100644 --- a/packages/walletkit-android-bridge/src/bridge.ts +++ b/packages/walletkit-android-bridge/src/bridge.ts @@ -9,6 +9,7 @@ import type { WalletKitBridgeApi } from './types'; import { api } from './api'; import { setBridgeApi, registerNativeCallHandler } from './transport/messaging'; +import { registerNativeResponseHandler } from './transport/nativeBridge'; declare global { interface Window { @@ -18,8 +19,6 @@ declare global { setBridgeApi(api as WalletKitBridgeApi); registerNativeCallHandler(); +registerNativeResponseHandler(); window.walletkitBridge = api; - -export { api }; -export type { WalletKitBridgeApi } from './types'; diff --git a/packages/walletkit-android-bridge/src/core/initialization.ts b/packages/walletkit-android-bridge/src/core/initialization.ts index 020b831d8..3c9e14665 100644 --- a/packages/walletkit-android-bridge/src/core/initialization.ts +++ b/packages/walletkit-android-bridge/src/core/initialization.ts @@ -30,21 +30,12 @@ import { } from '../adapters/AndroidTONConnectSessionsManager'; import { AndroidAPIClientAdapter } from '../adapters/AndroidAPIClientAdapter'; -export interface InitTonWalletKitDeps { +interface InitTonWalletKitDeps { emit: (type: WalletKitBridgeEvent['type'], data?: WalletKitBridgeEvent['data']) => void; postToNative: (payload: BridgePayload) => void; AndroidStorageAdapter: new () => unknown; } -type NativeStorageBridge = { - storageGet: (key: string) => string | null; - storageSet: (key: string, value: string) => void; -}; - -type _AndroidBridgeWindow = Window & { - WalletKitNative?: NativeStorageBridge; -}; - /** * Initializes WalletKit with Android-specific configuration and wiring. * diff --git a/packages/walletkit-android-bridge/src/globals.d.ts b/packages/walletkit-android-bridge/src/globals.d.ts index 6a52f3d87..413ba0510 100644 --- a/packages/walletkit-android-bridge/src/globals.d.ts +++ b/packages/walletkit-android-bridge/src/globals.d.ts @@ -7,19 +7,13 @@ */ // Re-export bridge types for backwards compatibility -import type { - WalletKitBridgeEvent as _WalletKitBridgeEvent, - WalletKitBridgeInitConfig as _WalletKitBridgeInitConfig, - AndroidBridgeType, - WalletKitNativeBridgeType, - WalletKitBridgeApi, - WalletKitApiMethod, -} from './types'; +import type { AndroidBridgeType, WalletKitNativeBridgeType, WalletKitBridgeApi, WalletKitApiMethod } 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?: WalletKitNativeBridgeType; AndroidBridge?: AndroidBridgeType; } diff --git a/packages/walletkit-android-bridge/src/polyfills/setupNativeBridge.ts b/packages/walletkit-android-bridge/src/polyfills/setupNativeBridge.ts index d4cf87be3..6e5066604 100644 --- a/packages/walletkit-android-bridge/src/polyfills/setupNativeBridge.ts +++ b/packages/walletkit-android-bridge/src/polyfills/setupNativeBridge.ts @@ -33,14 +33,8 @@ function ensureBuffer(scope: GlobalWithBridge) { } } -/** - * Sets up the native storage bridge that connects JavaScript to Android's secure storage. - * Creates window.WalletKitNativeStorage that delegates to WalletKitNative. - * - * Note: Modern Android WebView (API 24+) already supports all standard Web APIs - * (fetch, TextEncoder, URL, etc.), so no polyfills are needed. - */ -export function setupNativeBridge() { +// Sets up native storage bridge: window.WalletKitNativeStorage delegates to WalletKitNative. +function setupNativeBridge() { const scope = window as GlobalWithBridge; ensureBuffer(scope); diff --git a/packages/walletkit-android-bridge/src/transport/messaging.ts b/packages/walletkit-android-bridge/src/transport/messaging.ts index ab037dad8..dde24f9ba 100644 --- a/packages/walletkit-android-bridge/src/transport/messaging.ts +++ b/packages/walletkit-android-bridge/src/transport/messaging.ts @@ -6,9 +6,6 @@ * */ -/** - * Messaging helpers that mediate between the native bridge and WalletKit APIs. - */ import type { WalletKitBridgeEvent, WalletKitBridgeApi, WalletKitApiMethod, CallContext } from '../types'; import { postToNative } from './nativeBridge'; import { emitCallDiagnostic } from './diagnostics'; @@ -16,33 +13,15 @@ import { error } from '../utils/logger'; let apiRef: WalletKitBridgeApi | undefined; -/** - * Emits a bridge event to the native layer. - * - * @param type - Event type identifier. - * @param data - Optional event payload. - */ export function emit(type: WalletKitBridgeEvent['type'], data?: WalletKitBridgeEvent['data']): void { const event: WalletKitBridgeEvent = { type, data }; postToNative({ kind: 'event', event }); } -/** - * Sends a response payload (or error) back to the native layer. - * - * @param id - Native call identifier. - * @param result - Optional result payload. - * @param error - Optional error to report. - */ export function respond(id: string, result?: unknown, error?: { message: string }): void { postToNative({ kind: 'response', id, result, error }); } -/** - * Registers the active API implementation that will service native calls. - * - * @param api - WalletKit bridge API surface. - */ export function setBridgeApi(api: WalletKitBridgeApi): void { apiRef = api; } @@ -65,13 +44,6 @@ async function invokeApiMethod( return value; } -/** - * Handles a native call by invoking the corresponding WalletKit bridge method. - * - * @param id - Native call identifier. - * @param method - API method name. - * @param params - Optional serialized parameters. - */ export async function handleCall(id: string, method: WalletKitApiMethod, params?: unknown): Promise { if (!apiRef) { throw new Error('Bridge API not registered'); @@ -90,9 +62,6 @@ export async function handleCall(id: string, method: WalletKitApiMethod, params? } } -/** - * Registers the global handler that native code invokes to call into the bridge. - */ export function registerNativeCallHandler(): void { window.__walletkitCall = (id, method, paramsJson) => { let params: unknown = undefined; diff --git a/packages/walletkit-android-bridge/src/transport/nativeBridge.ts b/packages/walletkit-android-bridge/src/transport/nativeBridge.ts index 14a976e03..624f982f1 100644 --- a/packages/walletkit-android-bridge/src/transport/nativeBridge.ts +++ b/packages/walletkit-android-bridge/src/transport/nativeBridge.ts @@ -6,12 +6,72 @@ * */ -/** - * Native bridge helpers for posting messages to the Android host. - */ +import { v7 as uuidv7 } from 'uuid'; + import type { BridgePayload } from '../types'; import { bigIntReplacer } from '../utils/serialization'; -import { warn, error } from '../utils/logger'; +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). + +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. + */ +export function bridgeRequestSync(method: string, params: Record): string { + const native = window.WalletKitNative; + if (!native || typeof native.adapterCallSync !== 'function') { + throw new Error('WalletKitNative.adapterCallSync not available'); + } + return native.adapterCallSync(method, JSON.stringify(params)); +} + +/** + * Send a request to Kotlin via postMessage and wait for a response. + */ +export function bridgeRequest(method: string, params: Record): Promise { + const id = uuidv7(); + return new Promise((resolve, reject) => { + pendingRequests.set(id, { resolve, reject }); + postToNative({ kind: 'request', id, method, params }); + }); +} + +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. diff --git a/packages/walletkit-android-bridge/src/types/api.ts b/packages/walletkit-android-bridge/src/types/api.ts index 0c4233808..a76086ccc 100644 --- a/packages/walletkit-android-bridge/src/types/api.ts +++ b/packages/walletkit-android-bridge/src/types/api.ts @@ -22,9 +22,7 @@ import type { TransactionEmulatedPreview, TransactionRequest, Wallet, - WalletAdapter, WalletResponse, - WalletSigner, } from '@ton/walletkit'; /** @@ -54,26 +52,35 @@ export interface CreateTonMnemonicArgs { count?: number; } -export interface CreateSignerArgs { - mnemonic?: string[]; - secretKey?: string; - mnemonicType?: 'ton' | 'bip39'; +export interface CreateSignerFromMnemonicArgs { + mnemonic: string[]; + mnemonicType?: string; +} + +export interface CreateSignerFromPrivateKeyArgs { + secretKey: string; } -export interface CreateAdapterArgs { +export interface CreateSignerFromCustomArgs { signerId: string; - walletVersion: 'v4r2' | 'v5r1'; - network: { chainId: string }; // Required - Kotlin must specify the network + publicKey: string; +} + +export interface CreateWalletAdapterArgs { + signerId: string; + network: { chainId: string }; workchain?: number; walletId?: number; - publicKey?: string; - isCustom?: boolean; } export interface AddWalletArgs { adapterId: string; } +export interface ReleaseRefArgs { + id: string; +} + export interface RemoveWalletArgs { walletId: string; } @@ -253,50 +260,35 @@ export interface HandleTonConnectUrlArgs { url: string; } -export interface WalletDescriptor { - address: string; - publicKey: string; - version: string; - index: number; - network: string; -} - export interface WalletKitBridgeApi { init(config?: WalletKitBridgeInitConfig): PromiseOrValue<{ ok: true }>; setEventsListeners(args?: SetEventsListenersArgs): PromiseOrValue<{ ok: true }>; removeEventListeners(): PromiseOrValue<{ ok: true }>; - // Returns raw keyPair with Uint8Array - Kotlin handles conversion mnemonicToKeyPair(args: MnemonicToKeyPairArgs): PromiseOrValue<{ publicKey: Uint8Array; secretKey: Uint8Array }>; - // Returns signature string directly sign(args: SignArgs): PromiseOrValue; - // Returns mnemonic words array directly createTonMnemonic(args?: CreateTonMnemonicArgs): PromiseOrValue; - // Returns temp ID and signer - Kotlin extracts signerId and publicKey - createSigner(args: CreateSignerArgs): PromiseOrValue<{ _tempId: string; signer: WalletSigner }>; - // Returns temp ID and adapter - Kotlin extracts adapterId and address - createAdapter(args: CreateAdapterArgs): PromiseOrValue<{ _tempId: string; adapter: WalletAdapter }>; - // Returns address string directly - getAdapterAddress(args: { adapterId: string }): PromiseOrValue; - // Returns walletId with wallet object, or null + createSignerFromMnemonic( + args: CreateSignerFromMnemonicArgs, + ): PromiseOrValue<{ signerId: string; publicKey: string }>; + 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 }>; addWallet(args: AddWalletArgs): PromiseOrValue<{ walletId: string | undefined; wallet: Wallet } | null>; - // Returns array of walletId with wallet objects + releaseRef(args: ReleaseRefArgs): PromiseOrValue<{ ok: boolean }>; getWallets(): PromiseOrValue<{ walletId: string | undefined; wallet: Wallet }[]>; - // Takes walletId, returns walletId with wallet object or null getWallet(args: { walletId: string }): PromiseOrValue<{ walletId: string | undefined; wallet: Wallet } | null>; - // Returns address string or null directly getWalletAddress(args: { walletId: string }): PromiseOrValue; - // Returns void removeWallet(args: RemoveWalletArgs): PromiseOrValue; - // Returns balance as string or undefined getBalance(args: GetBalanceArgs): PromiseOrValue; - // Returns transactions array directly getRecentTransactions(args: GetRecentTransactionsArgs): PromiseOrValue; handleTonConnectUrl(args: HandleTonConnectUrlArgs): PromiseOrValue; createTransferTonTransaction(args: CreateTransferTonTransactionArgs): PromiseOrValue; createTransferMultiTonTransaction(args: CreateTransferMultiTonTransactionArgs): PromiseOrValue; getTransactionPreview(args: TransactionContentArgs): PromiseOrValue; handleNewTransaction(args: TransactionContentArgs): PromiseOrValue<{ success: boolean }>; - // Returns result from wallet.sendTransaction sendTransaction(args: TransactionContentArgs): PromiseOrValue; approveConnectRequest(args: ApproveConnectRequestArgs): PromiseOrValue; rejectConnectRequest(args: RejectConnectRequestArgs): PromiseOrValue<{ success: boolean }>; diff --git a/packages/walletkit-android-bridge/src/types/bridge.ts b/packages/walletkit-android-bridge/src/types/bridge.ts index 34d4b532e..ccb52e797 100644 --- a/packages/walletkit-android-bridge/src/types/bridge.ts +++ b/packages/walletkit-android-bridge/src/types/bridge.ts @@ -39,7 +39,8 @@ export type BridgePayload = timestamp: number; message?: string; } - | { kind: 'jsBridgeEvent'; sessionId: string; event: JsBridgeTransportMessage }; + | { kind: 'jsBridgeEvent'; sessionId: string; event: JsBridgeTransportMessage } + | { kind: 'request'; id: string; method: string; params: Record }; export interface CallContext { id: string; diff --git a/packages/walletkit-android-bridge/src/types/events.ts b/packages/walletkit-android-bridge/src/types/events.ts index 9d2424018..d8af5d678 100644 --- a/packages/walletkit-android-bridge/src/types/events.ts +++ b/packages/walletkit-android-bridge/src/types/events.ts @@ -27,8 +27,6 @@ export interface WalletKitBridgeEvent { data?: T; } -export type WalletKitBridgeEventHandler = (event: WalletKitBridgeEvent) => void; - export type WalletKitBridgeEventCallback = ( type: WalletKitBridgeEventType, event: WalletKitBridgeEvent['data'], diff --git a/packages/walletkit-android-bridge/src/types/walletkit.ts b/packages/walletkit-android-bridge/src/types/walletkit.ts index ca4f3e63f..9266e24aa 100644 --- a/packages/walletkit-android-bridge/src/types/walletkit.ts +++ b/packages/walletkit-android-bridge/src/types/walletkit.ts @@ -59,7 +59,7 @@ export interface AndroidBridgeType { export interface WalletKitNativeBridgeType { postMessage(json: string): void; - signWithCustomSigner?(signerId: string, bytes: number[]): Promise; + adapterCallSync(method: string, paramsJson: string): string; } export type WalletKitAdapter = WalletAdapter; diff --git a/packages/walletkit-android-bridge/src/utils/bridge.ts b/packages/walletkit-android-bridge/src/utils/bridge.ts index 7cb99cdee..bdb6857e8 100644 --- a/packages/walletkit-android-bridge/src/utils/bridge.ts +++ b/packages/walletkit-android-bridge/src/utils/bridge.ts @@ -7,10 +7,7 @@ */ /** - * bridge.ts - Minimal unified bridge for all operations - * - * Single entry point for calling WalletKit, Wallet, and Client methods. - * Handles initialization, wallet lookup, and error handling in one place. + * Unified bridge for calling WalletKit, Wallet, and Client methods. */ import type { Wallet } from '@ton/walletkit'; @@ -18,12 +15,6 @@ import type { Wallet } from '@ton/walletkit'; import type { WalletKitInstance } from '../core/state'; import { walletKit } from '../core/state'; -// Re-export for external use -export type { WalletKitInstance }; - -/** - * Ensures WalletKit is initialized and ready. - */ async function ensureReady(): Promise { if (!walletKit) { throw new Error('WalletKit not initialized'); @@ -32,9 +23,6 @@ async function ensureReady(): Promise { return walletKit; } -/** - * Gets a wallet by ID, throwing if not found. - */ function getWalletOrThrow(kit: WalletKitInstance, walletId: string): Wallet { const wallet = kit.getWallet(walletId); if (!wallet) { @@ -43,14 +31,6 @@ function getWalletOrThrow(kit: WalletKitInstance, walletId: string): Wallet { return wallet; } -/** - * Calls a method on WalletKit instance. - * - * @example - * await kit('removeWallet', walletId); - * await kit('handleTonConnectUrl', url); - * await kit('listSessions'); - */ export async function kit(method: M, ...args: unknown[]): Promise { const instance = await ensureReady(); const fn = instance[method]; @@ -60,14 +40,6 @@ export async function kit(method: M, ...args: return (fn as (...a: unknown[]) => unknown).apply(instance, args); } -/** - * Calls a method on a Wallet instance. - * - * @example - * await wallet(walletId, 'getBalance'); - * await wallet(walletId, 'getJettons', { pagination }); - * await wallet(walletId, 'createTransferTonTransaction', request); - */ export async function wallet( walletId: string, method: M, @@ -82,26 +54,15 @@ export async function wallet( return (fn as (...a: unknown[]) => unknown).apply(w, args); } -/** - * Get the raw WalletKit instance (for complex operations). - * Use sparingly - prefer kit(), wallet() for type safety. - */ export async function getKit(): Promise { return ensureReady(); } -/** - * Get a wallet instance (for complex operations). - * Use sparingly - prefer wallet() for type safety. - */ export async function getWallet(walletId: string): Promise { const instance = await ensureReady(); return getWalletOrThrow(instance, walletId); } -/** - * Calls a method on a Wallet instance. Extracts walletId from args.walletId. - */ export async function walletCall( method: string, args: { walletId: string; [k: string]: unknown }, @@ -115,9 +76,6 @@ export async function walletCall( return (fn as (args?: unknown) => Promise).call(w, args); } -/** - * Calls a method on a Wallet's ApiClient. Extracts walletId from args.walletId. - */ export async function clientCall( method: string, args: { walletId: string; [k: string]: unknown }, diff --git a/packages/walletkit-android-bridge/src/utils/internalBrowserResolvers.ts b/packages/walletkit-android-bridge/src/utils/internalBrowserResolvers.ts index cc66f63b7..48ba4cdc6 100644 --- a/packages/walletkit-android-bridge/src/utils/internalBrowserResolvers.ts +++ b/packages/walletkit-android-bridge/src/utils/internalBrowserResolvers.ts @@ -8,12 +8,12 @@ import type { JsBridgeTransportMessage } from '../types/bridge'; -export type InternalBrowserResponseResolver = { +type InternalBrowserResponseResolver = { resolve: (response: JsBridgeTransportMessage) => void; reject: (error: Error) => void; }; -export type InternalBrowserResolverRegistry = Map; +type InternalBrowserResolverRegistry = Map; type InternalBrowserGlobal = typeof globalThis & { __internalBrowserResponseResolvers?: InternalBrowserResolverRegistry; @@ -31,7 +31,3 @@ export function ensureInternalBrowserResolverMap(): InternalBrowserResolverRegis } return internalBrowserGlobal.__internalBrowserResponseResolvers; } - -export function deleteInternalBrowserResolver(messageId: string): void { - internalBrowserGlobal.__internalBrowserResponseResolvers?.delete(messageId); -} diff --git a/packages/walletkit-android-bridge/src/utils/logger.ts b/packages/walletkit-android-bridge/src/utils/logger.ts index e0ce44af1..17b6c3d54 100644 --- a/packages/walletkit-android-bridge/src/utils/logger.ts +++ b/packages/walletkit-android-bridge/src/utils/logger.ts @@ -7,17 +7,10 @@ */ /** - * Flexible logging utility for the Android WalletKit bridge. - * - * Logging can be controlled by setting the log level from the Android SDK: - * window.__WALLETKIT_LOG_LEVEL__ = 'DEBUG'; // OFF, ERROR, WARN, INFO, DEBUG - * - * Default is 'OFF' in production for performance. + * Logging utility controlled via window.__WALLETKIT_LOG_LEVEL__. + * Defaults to OFF in production. */ -/** - * Log levels in order of verbosity - */ export enum LogLevel { OFF = 0, ERROR = 1, @@ -47,40 +40,24 @@ function getCurrentLogLevel(): LogLevel { return LogLevel[levelStr] ?? LogLevel.OFF; } -/** - * Debug logger - logs detailed debugging information - * Only logs when level is DEBUG or higher - */ export const log = (...args: unknown[]): void => { if (getCurrentLogLevel() >= LogLevel.DEBUG) { consoleRef?.log?.('[WalletKit]', ...args); } }; -/** - * Info logger - logs informational messages - * Only logs when level is INFO or higher - */ export const info = (...args: unknown[]): void => { if (getCurrentLogLevel() >= LogLevel.INFO) { consoleRef?.log?.('[WalletKit]', ...args); } }; -/** - * Warning logger - logs warnings - * Only logs when level is WARN or higher - */ export const warn = (...args: unknown[]): void => { if (getCurrentLogLevel() >= LogLevel.WARN) { consoleRef?.warn?.('[WalletKit]', ...args); } }; -/** - * Error logger - logs errors - * Only logs when level is ERROR or higher - */ export const error = (...args: unknown[]): void => { if (getCurrentLogLevel() >= LogLevel.ERROR) { consoleRef?.error?.('[WalletKit]', ...args); diff --git a/packages/walletkit-android-bridge/src/utils/registry.ts b/packages/walletkit-android-bridge/src/utils/registry.ts new file mode 100644 index 000000000..fff823f0e --- /dev/null +++ b/packages/walletkit-android-bridge/src/utils/registry.ts @@ -0,0 +1,33 @@ +/** + * 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. + * + */ + +/** + * Holds live JS objects by string ID so Kotlin can reference them + * across WebView bridge calls (evaluateJavascript only returns strings). + */ + +const store = new Map(); +let nextId = 1; + +export function retain(prefix: string, obj: unknown): string { + const id = `${prefix}_${nextId++}`; + store.set(id, obj); + return id; +} + +export function retainWithId(id: string, obj: unknown): void { + store.set(id, obj); +} + +export function get(id: string): T | undefined { + return store.get(id) as T | undefined; +} + +export function release(id: string): boolean { + return store.delete(id); +}