From 0a3e08c2b8d306143eade5a706043ae544f44f02 Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Mon, 8 Jun 2026 19:47:52 +0300 Subject: [PATCH 1/2] chore(android-bridge): sync adapter from main (temporary base for gasless) --- .../src/adapters/AndroidAPIClientAdapter.ts | 208 +++++++----------- .../walletkit-android-bridge/src/api/index.ts | 21 ++ .../src/api/staking.ts | 28 ++- .../walletkit-android-bridge/src/api/swap.ts | 14 ++ .../src/api/walletClient.ts | 122 ++++++++++ .../src/api/wallets.ts | 4 + .../src/core/initialization.ts | 12 +- .../src/transport/nativeBridge.ts | 48 +++- .../walletkit-android-bridge/src/types/api.ts | 32 +++ 9 files changed, 343 insertions(+), 146 deletions(-) create mode 100644 packages/walletkit-android-bridge/src/api/walletClient.ts diff --git a/packages/walletkit-android-bridge/src/adapters/AndroidAPIClientAdapter.ts b/packages/walletkit-android-bridge/src/adapters/AndroidAPIClientAdapter.ts index ddfb9d7ca..b976f8787 100644 --- a/packages/walletkit-android-bridge/src/adapters/AndroidAPIClientAdapter.ts +++ b/packages/walletkit-android-bridge/src/adapters/AndroidAPIClientAdapter.ts @@ -7,111 +7,62 @@ */ import type { + AccountState, + AccountStates, ApiClient, Base64String, - UserFriendlyAddress, - RawStackItem, + EmulationResult, + GetEventsRequest, + GetEventsResponse, + GetJettonsByAddressRequest, + GetJettonsByOwnerRequest, GetMethodResult, + GetPendingTraceRequest, + GetPendingTransactionsRequest, + GetTraceRequest, + GetTransactionByHashRequest, + JettonsResponse, + MasterchainInfo, + Network, NFTsRequest, NFTsResponse, - UserNFTsRequest, + RawStackItem, TokenAmount, - TransactionsResponse, - JettonsResponse, - AccountState, - AccountStates, - EmulationResult, ToncenterResponseJettonMasters, ToncenterTracesResponse, TransactionsByAddressRequest, - GetTransactionByHashRequest, - GetPendingTransactionsRequest, - GetTraceRequest, - GetPendingTraceRequest, - GetJettonsByOwnerRequest, - GetJettonsByAddressRequest, - GetEventsRequest, - GetEventsResponse, - MasterchainInfo, - Network, + TransactionsResponse, + UserFriendlyAddress, + UserNFTsRequest, } from '@ton/walletkit'; -import { error } from '../utils/logger'; - -type AndroidAPIClientBridge = { - apiGetNetworks: () => string; - apiSendBoc: (networkJson: string, boc: string) => string; - apiRunGetMethod: ( - networkJson: string, - address: string, - method: string, - stackJson: string | null, - seqno: number, - ) => string; - apiGetBalance: (networkJson: string, address: string, seqno: number) => string; - apiGetMasterchainInfo: (networkJson: string) => string; -}; - -type AndroidWindow = Window & { - WalletKitNative?: AndroidAPIClientBridge; -}; +import { bridgeRequestSync, bridgeRequestSyncTyped } from '../transport/nativeBridge'; /** - * Android native API client adapter. - * Uses Android's JavascriptInterface methods for API calls. - * Similar to SwiftAPIClientAdapter for iOS. + * Android native API client adapter — TS counterpart to the Kotlin `TONAPIClient`. + * + * Every method dispatches through the single sync bridge entry point + * (`window.WalletKitNative.adapterCallSync`) under the `api.*` namespace. The adapter + * only constructs the params object and picks whether the response is a raw string or + * a JSON-encoded value — JSON marshalling and error wrapping live in [bridgeRequestSync]. + * + * Methods iOS's `SwiftAPIClientAdapter` throws on (the ones the Swift host doesn't + * delegate either) are thrown here too — keeps the two adapters at parity until the + * host implements them. Bridge-availability checks belong to the bootstrap layer, not here. */ export class AndroidAPIClientAdapter implements ApiClient { - private androidBridge: AndroidAPIClientBridge; - private network: Network; + private readonly chainId: string; - constructor(network: Network) { - const androidWindow = window as AndroidWindow; - if (!androidWindow.WalletKitNative) { - throw new Error('WalletKitNative bridge not available'); - } - this.androidBridge = androidWindow.WalletKitNative; - this.network = network; + constructor(private readonly network: Network) { + this.chainId = network.chainId; } getNetwork(): Network { return this.network; } - /** - * Check if native API clients are available. - */ - static isAvailable(): boolean { - const androidWindow = window as AndroidWindow; - return typeof androidWindow.WalletKitNative?.apiGetNetworks === 'function'; - } - - /** - * Get all networks that have native API clients configured. - */ - static getAvailableNetworks(): Network[] { - const androidWindow = window as AndroidWindow; - if (!androidWindow.WalletKitNative?.apiGetNetworks) { - return []; - } - try { - const networksJson = androidWindow.WalletKitNative.apiGetNetworks(); - return JSON.parse(networksJson) as Network[]; - } catch (err) { - error('[AndroidAPIClientAdapter] Failed to get available networks:', err); - return []; - } - } - async sendBoc(boc: Base64String): Promise { - try { - const networkJson = JSON.stringify(this.network); - const result = this.androidBridge.apiSendBoc(networkJson, boc); - return result; - } catch (err) { - error('[AndroidAPIClientAdapter] sendBoc failed:', err); - throw err; - } + return bridgeRequestSync('api.sendBoc', { chainId: this.chainId, boc }); } async runGetMethod( @@ -120,52 +71,62 @@ export class AndroidAPIClientAdapter implements ApiClient { stack?: RawStackItem[], seqno?: number, ): Promise { - try { - const networkJson = JSON.stringify(this.network); - const stackJson = stack ? JSON.stringify(stack) : null; - const seqnoArg = seqno ?? -1; // Use -1 to represent null - const resultJson = this.androidBridge.apiRunGetMethod(networkJson, address, method, stackJson, seqnoArg); - const result = JSON.parse(resultJson) as GetMethodResult; - return result; - } catch (err) { - error('[AndroidAPIClientAdapter] runGetMethod failed:', err); - throw err; - } + return bridgeRequestSyncTyped('api.runGetMethod', { + chainId: this.chainId, + address, + method, + stack, + seqno, + }); } - // Methods not implemented - will throw if called - // These are optional for mobile usage + async getMasterchainInfo(): Promise { + return bridgeRequestSyncTyped('api.getMasterchainInfo', { chainId: this.chainId }); + } - async nftItemsByAddress(_request: NFTsRequest): Promise { - throw new Error('nftItemsByAddress is not implemented yet'); + async nftItemsByAddress(request: NFTsRequest): Promise { + return bridgeRequestSyncTyped('api.nftItemsByAddress', { chainId: this.chainId, request }); } - async nftItemsByOwner(_request: UserNFTsRequest): Promise { - throw new Error('nftItemsByOwner is not implemented yet'); + async nftItemsByOwner(request: UserNFTsRequest): Promise { + return bridgeRequestSyncTyped('api.nftItemsByOwner', { chainId: this.chainId, request }); } - async fetchEmulation(_messageBoc: Base64String, _ignoreSignature?: boolean): Promise { - throw new Error('fetchEmulation is not implemented yet'); + async fetchEmulation(messageBoc: Base64String, ignoreSignature?: boolean): Promise { + return bridgeRequestSyncTyped('api.fetchEmulation', { + chainId: this.chainId, + messageBoc, + ignoreSignature, + }); } - async getAccountState(_address: UserFriendlyAddress, _seqno?: number): Promise { - throw new Error('getAccountState is not implemented yet'); + async getAccountState(address: UserFriendlyAddress, seqno?: number): Promise { + return bridgeRequestSyncTyped('api.getAccountState', { + chainId: this.chainId, + address, + seqno, + }); } - async getAccountStates(_addresses: UserFriendlyAddress[]): Promise { - throw new Error('getAccountStates is not implemented yet'); + async getAccountStates(addresses: UserFriendlyAddress[]): Promise { + return bridgeRequestSyncTyped('api.getAccountStates', { + chainId: this.chainId, + addresses, + }); } async getBalance(address: UserFriendlyAddress, seqno?: number): Promise { - try { - const networkJson = JSON.stringify(this.network); - const seqnoArg = seqno ?? -1; // Use -1 to represent null - const result = this.androidBridge.apiGetBalance(networkJson, address, seqnoArg); - return result; - } catch (err) { - error('[AndroidAPIClientAdapter] getBalance failed:', err); - throw err; - } + return bridgeRequestSync('api.getBalance', { chainId: this.chainId, address, seqno }) as TokenAmount; + } + + async resolveDnsWallet(domain: string): Promise { + const raw = bridgeRequestSync('api.resolveDnsWallet', { chainId: this.chainId, domain }); + return raw || undefined; + } + + async backResolveDnsWallet(address: UserFriendlyAddress): Promise { + const raw = bridgeRequestSync('api.backResolveDnsWallet', { chainId: this.chainId, address }); + return raw || undefined; } async getAccountTransactions(_request: TransactionsByAddressRequest): Promise { @@ -188,14 +149,6 @@ export class AndroidAPIClientAdapter implements ApiClient { throw new Error('getPendingTrace is not implemented yet'); } - async resolveDnsWallet(_domain: string): Promise { - throw new Error('resolveDnsWallet is not implemented yet'); - } - - async backResolveDnsWallet(_address: UserFriendlyAddress): Promise { - throw new Error('backResolveDnsWallet is not implemented yet'); - } - async jettonsByAddress(_request: GetJettonsByAddressRequest): Promise { throw new Error('jettonsByAddress is not implemented yet'); } @@ -207,15 +160,4 @@ export class AndroidAPIClientAdapter implements ApiClient { async getEvents(_request: GetEventsRequest): Promise { throw new Error('getEvents is not implemented yet'); } - - async getMasterchainInfo(): Promise { - try { - const networkJson = JSON.stringify(this.network); - const resultJson = this.androidBridge.apiGetMasterchainInfo(networkJson); - return JSON.parse(resultJson) as MasterchainInfo; - } catch (err) { - error('[AndroidAPIClientAdapter] getMasterchainInfo failed:', err); - throw err; - } - } } diff --git a/packages/walletkit-android-bridge/src/api/index.ts b/packages/walletkit-android-bridge/src/api/index.ts index a0fccea40..b60ed5d40 100644 --- a/packages/walletkit-android-bridge/src/api/index.ts +++ b/packages/walletkit-android-bridge/src/api/index.ts @@ -19,6 +19,7 @@ import * as staking from './staking'; import * as browser from './browser'; import * as streaming from './streaming'; import * as swap from './swap'; +import * as walletClient from './walletClient'; import { eventListeners } from './eventListeners'; export { eventListeners }; @@ -43,6 +44,7 @@ export const api = { getWallets: wallets.getWallets, getWallet: wallets.getWalletById, getWalletAddress: wallets.getWalletAddress, + getWalletNetwork: wallets.getWalletNetwork, removeWallet: wallets.removeWallet, getBalance: wallets.getBalance, @@ -100,21 +102,40 @@ export const api = { createTonStakersStakingProvider: staking.createTonStakersStakingProvider, registerStakingProvider: staking.registerStakingProvider, + removeStakingProvider: staking.removeStakingProvider, setDefaultStakingProvider: staking.setDefaultStakingProvider, + getRegisteredStakingProviders: staking.getRegisteredStakingProviders, + hasStakingProvider: staking.hasStakingProvider, getStakingQuote: staking.getStakingQuote, buildStakeTransaction: staking.buildStakeTransaction, getStakedBalance: staking.getStakedBalance, getStakingProviderInfo: staking.getStakingProviderInfo, getStakingProviderMetadata: staking.getStakingProviderMetadata, + getStakingProviderSupportedNetworks: staking.getStakingProviderSupportedNetworks, registerKotlinStakingProvider: staking.registerKotlinStakingProvider, createOmnistonSwapProvider: swap.createOmnistonSwapProvider, createDeDustSwapProvider: swap.createDeDustSwapProvider, registerSwapProvider: swap.registerSwapProvider, + removeSwapProvider: swap.removeSwapProvider, setDefaultSwapProvider: swap.setDefaultSwapProvider, getRegisteredSwapProviders: swap.getRegisteredSwapProviders, + getSwapProviderMetadata: swap.getSwapProviderMetadata, + getSwapProviderSupportedNetworks: swap.getSwapProviderSupportedNetworks, hasSwapProvider: swap.hasSwapProvider, getSwapQuote: swap.getSwapQuote, buildSwapTransaction: swap.buildSwapTransaction, registerKotlinSwapProvider: swap.registerKotlinSwapProvider, + + walletClientSendBoc: walletClient.walletClientSendBoc, + walletClientRunGetMethod: walletClient.walletClientRunGetMethod, + walletClientGetBalance: walletClient.walletClientGetBalance, + walletClientGetMasterchainInfo: walletClient.walletClientGetMasterchainInfo, + walletClientNftItemsByAddress: walletClient.walletClientNftItemsByAddress, + walletClientNftItemsByOwner: walletClient.walletClientNftItemsByOwner, + walletClientFetchEmulation: walletClient.walletClientFetchEmulation, + walletClientAccountState: walletClient.walletClientAccountState, + walletClientAccountStates: walletClient.walletClientAccountStates, + walletClientResolveDnsWallet: walletClient.walletClientResolveDnsWallet, + walletClientBackResolveDnsWallet: walletClient.walletClientBackResolveDnsWallet, } as unknown as WalletKitBridgeApi; diff --git a/packages/walletkit-android-bridge/src/api/staking.ts b/packages/walletkit-android-bridge/src/api/staking.ts index a01bb362f..e2a042e50 100644 --- a/packages/walletkit-android-bridge/src/api/staking.ts +++ b/packages/walletkit-android-bridge/src/api/staking.ts @@ -27,8 +27,7 @@ import { get, release, retainWithId } from '../utils/registry'; /** * JS-side proxy that implements [StakingProviderInterface] by forwarding every call to a - * Kotlin-implemented `ITONStakingProvider` via reverse-RPC. Mirrors the streaming - * `ProxyStreamingProvider` pattern. + * Kotlin-implemented `ITONStakingProvider` via reverse-RPC. * * `getStakingProviderMetadata` and `getSupportedNetworks` are synchronous per the interface * contract, so both values are passed in at registration and cached on this instance. @@ -101,11 +100,27 @@ export async function registerStakingProvider(args: { providerId: string }) { instance.staking.registerProvider(provider); } +export async function removeStakingProvider(args: { providerId: string }): Promise { + const instance = await getKit(); + instance.staking.removeProvider(instance.staking.getProvider(args.providerId)); +} + export async function setDefaultStakingProvider(args: { providerId: string }) { const instance = await getKit(); instance.staking.setDefaultProvider(args.providerId); } +export async function getRegisteredStakingProviders(): Promise<{ providerIds: string[] }> { + const instance = await getKit(); + const providerIds = instance.staking.getProviders().map((provider) => provider.providerId); + return { providerIds }; +} + +export async function hasStakingProvider(args: { providerId: string }): Promise<{ result: boolean }> { + const instance = await getKit(); + return { result: instance.staking.hasProvider(args.providerId) }; +} + export async function getStakingQuote(args: StakingQuoteParams & { providerId?: string }) { const { providerId, ...params } = args; const instance = await getKit(); @@ -137,6 +152,14 @@ export async function getStakingProviderMetadata(args: { network?: { chainId: st return instance.staking.getStakingProviderMetadata(args.network, args.providerId); } +export async function getStakingProviderSupportedNetworks(args: { + providerId: string; +}): Promise<{ networks: Network[] }> { + const instance = await getKit(); + const networks = instance.staking.getProvider(args.providerId).getSupportedNetworks(); + return { networks }; +} + /** * Tell the JS staking manager that a Kotlin-implemented provider is available. * A [ProxyStakingProvider] is created and registered; all subsequent staking operations on it @@ -152,7 +175,6 @@ export async function registerKotlinStakingProvider(args: { supportedNetworks: Network[]; }) { const instance = await getKit(); - // Replace any previous proxy with the same id const previous = get(args.providerId); if (previous instanceof ProxyStakingProvider) { release(args.providerId); diff --git a/packages/walletkit-android-bridge/src/api/swap.ts b/packages/walletkit-android-bridge/src/api/swap.ts index 6951ca2af..5ad6b30c6 100644 --- a/packages/walletkit-android-bridge/src/api/swap.ts +++ b/packages/walletkit-android-bridge/src/api/swap.ts @@ -93,6 +93,11 @@ export async function registerSwapProvider(args: { providerId: string }): Promis (await getSwap()).registerProvider(get(args.providerId) as SwapProviderInterface); } +export async function removeSwapProvider(args: { providerId: string }): Promise { + const swap = await getSwap(); + swap.removeProvider(swap.getProvider(args.providerId)); +} + export async function setDefaultSwapProvider(args: { providerId: string }): Promise { (await getSwap()).setDefaultProvider(args.providerId); } @@ -102,6 +107,15 @@ export async function getRegisteredSwapProviders(): Promise<{ providerIds: strin return { providerIds }; } +export async function getSwapProviderMetadata(args: { providerId: string }): Promise { + return (await getSwap()).getProvider(args.providerId).getMetadata(); +} + +export async function getSwapProviderSupportedNetworks(args: { providerId: string }): Promise<{ networks: Network[] }> { + const networks = (await getSwap()).getProvider(args.providerId).getSupportedNetworks(); + return { networks }; +} + export async function hasSwapProvider(args: { providerId: string }): Promise<{ result: boolean }> { const result = (await getSwap()).hasProvider(args.providerId); return { result }; diff --git a/packages/walletkit-android-bridge/src/api/walletClient.ts b/packages/walletkit-android-bridge/src/api/walletClient.ts new file mode 100644 index 000000000..2497d6924 --- /dev/null +++ b/packages/walletkit-android-bridge/src/api/walletClient.ts @@ -0,0 +1,122 @@ +/** + * 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 type { + AccountState, + AccountStates, + Base64String, + EmulationResult, + GetMethodResult, + MasterchainInfo, + NFTsRequest, + NFTsResponse, + RawStackItem, + TokenAmount, + UserFriendlyAddress, + UserNFTsRequest, +} from '@ton/walletkit'; + +import { getWallet } from '../utils/bridge'; + +// Handlers for the Kotlin `BridgedJSAPIClient` proxy. Each Android-side call to +// `wallet.client.(...)` round-trips through `BridgeRpcClient.walletClient*` +// to one of these handlers, which dispatches against the same wallet's JS-side +// `wallet.client` (an `ApiClient`) and returns the result. Mirrors the iOS +// `JSTONAPIClient` adapter. + +export async function walletClientSendBoc(args: { walletId: string; boc: string }): Promise<{ result: string }> { + const w = await getWallet(args.walletId); + const result = await w.client.sendBoc(args.boc as Base64String); + return { result }; +} + +export async function walletClientRunGetMethod(args: { + walletId: string; + address: string; + method: string; + stack?: RawStackItem[]; + seqno?: number; +}): Promise { + const w = await getWallet(args.walletId); + return w.client.runGetMethod(args.address as UserFriendlyAddress, args.method, args.stack, args.seqno); +} + +export async function walletClientGetBalance(args: { + walletId: string; + address: string; + seqno?: number; +}): Promise<{ result: TokenAmount }> { + const w = await getWallet(args.walletId); + const result = await w.client.getBalance(args.address as UserFriendlyAddress, args.seqno); + return { result }; +} + +export async function walletClientGetMasterchainInfo(args: { walletId: string }): Promise { + const w = await getWallet(args.walletId); + return w.client.getMasterchainInfo(); +} + +export async function walletClientNftItemsByAddress(args: { + walletId: string; + request: NFTsRequest; +}): Promise { + const w = await getWallet(args.walletId); + return w.client.nftItemsByAddress(args.request); +} + +export async function walletClientNftItemsByOwner(args: { + walletId: string; + request: UserNFTsRequest; +}): Promise { + const w = await getWallet(args.walletId); + return w.client.nftItemsByOwner(args.request); +} + +export async function walletClientFetchEmulation(args: { + walletId: string; + messageBoc: string; + ignoreSignature?: boolean; +}): Promise { + const w = await getWallet(args.walletId); + return w.client.fetchEmulation(args.messageBoc as Base64String, args.ignoreSignature); +} + +export async function walletClientAccountState(args: { + walletId: string; + address: string; + seqno?: number; +}): Promise { + const w = await getWallet(args.walletId); + return w.client.getAccountState(args.address as UserFriendlyAddress, args.seqno); +} + +export async function walletClientAccountStates(args: { + walletId: string; + addresses: string[]; +}): Promise { + const w = await getWallet(args.walletId); + return w.client.getAccountStates(args.addresses as UserFriendlyAddress[]); +} + +export async function walletClientResolveDnsWallet(args: { + walletId: string; + domain: string; +}): Promise<{ result: string | null }> { + const w = await getWallet(args.walletId); + const result = await w.client.resolveDnsWallet(args.domain); + return { result: result ?? null }; +} + +export async function walletClientBackResolveDnsWallet(args: { + walletId: string; + address: string; +}): Promise<{ result: string | null }> { + const w = await getWallet(args.walletId); + const result = await w.client.backResolveDnsWallet(args.address as UserFriendlyAddress); + return { result: result ?? null }; +} diff --git a/packages/walletkit-android-bridge/src/api/wallets.ts b/packages/walletkit-android-bridge/src/api/wallets.ts index 8cf3e95b4..8f1b55f94 100644 --- a/packages/walletkit-android-bridge/src/api/wallets.ts +++ b/packages/walletkit-android-bridge/src/api/wallets.ts @@ -128,6 +128,10 @@ export async function getWalletAddress(args: { walletId: string }) { return wallet(args.walletId, 'getAddress'); } +export async function getWalletNetwork(args: { walletId: string }) { + return wallet(args.walletId, 'getNetwork'); +} + export async function removeWallet(args: { walletId: string }) { return kit('removeWallet', args.walletId); } diff --git a/packages/walletkit-android-bridge/src/core/initialization.ts b/packages/walletkit-android-bridge/src/core/initialization.ts index 5ccd8dcaa..f49fce364 100644 --- a/packages/walletkit-android-bridge/src/core/initialization.ts +++ b/packages/walletkit-android-bridge/src/core/initialization.ts @@ -9,7 +9,7 @@ /** * WalletKit initialization helpers used by the bridge entry point. */ -import type { BridgeResponse, BridgeEvent } from '@ton/walletkit'; +import type { BridgeResponse, BridgeEvent, Network } from '@ton/walletkit'; import { TONCONNECT_BRIDGE_EVENT, ApiClientTonApi, ApiClientToncenter } from '@ton/walletkit'; import { TONCONNECT_BRIDGE_RESPONSE } from '@ton/walletkit/bridge'; @@ -29,7 +29,7 @@ import { AndroidTONConnectSessionsManager, } from '../adapters/AndroidTONConnectSessionsManager'; import { AndroidAPIClientAdapter } from '../adapters/AndroidAPIClientAdapter'; -import { unwrapRef } from '../transport/nativeBridge'; +import { bridgeRequestSyncTyped, isBridgeAvailable, unwrapRef } from '../transport/nativeBridge'; interface InitTonWalletKitDeps { emit: (type: WalletKitBridgeEvent['type'], data?: WalletKitBridgeEvent['data']) => void; @@ -84,11 +84,9 @@ export async function initTonWalletKit( } } - // Check if native API clients are available and use them if so - if (AndroidAPIClientAdapter.isAvailable()) { - const availableNetworks = AndroidAPIClientAdapter.getAvailableNetworks(); - - for (const nativeNetwork of availableNetworks) { + // Use the native API clients when the host exposes them. + if (isBridgeAvailable()) { + for (const nativeNetwork of bridgeRequestSyncTyped('api.getNetworks', {})) { networksConfig[nativeNetwork.chainId] = { apiClient: new AndroidAPIClientAdapter(nativeNetwork), }; diff --git a/packages/walletkit-android-bridge/src/transport/nativeBridge.ts b/packages/walletkit-android-bridge/src/transport/nativeBridge.ts index 13ea49b43..43bba2fed 100644 --- a/packages/walletkit-android-bridge/src/transport/nativeBridge.ts +++ b/packages/walletkit-android-bridge/src/transport/nativeBridge.ts @@ -15,13 +15,55 @@ import { sendToNative } from './port'; const pendingRequests = new Map void; reject: (e: Error) => void }>(); -// Sync host call via @JavascriptInterface — WebMessagePort is async and can't satisfy sync getters. +export type BridgeFailureKind = 'bridge_unavailable' | 'native_threw' | 'decode_failed'; + +/** Structured failure for every bridge call. Distinguishes wire-level vs. host vs. decode. */ +export class BridgeError extends Error { + constructor( + public readonly kind: BridgeFailureKind, + public readonly method: string, + options?: { cause?: unknown; raw?: string }, + ) { + super(`[bridge:${kind}] ${method}${options?.raw ? ` raw=${truncate(options.raw)}` : ''}`); + this.name = 'BridgeError'; + if (options?.cause !== undefined) (this as { cause?: unknown }).cause = options.cause; + } +} + +function truncate(s: string, max = 200): string { + return s.length <= max ? s : `${s.slice(0, max)}…(${s.length} chars)`; +} + +/** Sync host call via @JavascriptInterface — returns the raw string the host produced. */ 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'); + throw new BridgeError('bridge_unavailable', method); } - return native.adapterCallSync(method, JSON.stringify(params)); + try { + return native.adapterCallSync(method, JSON.stringify(params, bigIntReplacer)); + } catch (cause) { + throw new BridgeError('native_threw', method, { cause }); + } +} + +/** Sync host call with JSON-parsed return. Optional [decode] runs after parse. */ +export function bridgeRequestSyncTyped( + method: string, + params: Record, + decode?: (parsed: unknown) => T, +): T { + const raw = bridgeRequestSync(method, params); + try { + const parsed = JSON.parse(raw); + return decode ? decode(parsed) : (parsed as T); + } catch (cause) { + throw new BridgeError('decode_failed', method, { cause, raw }); + } +} + +export function isBridgeAvailable(): boolean { + return typeof window.WalletKitNative?.adapterCallSync === 'function'; } export function bridgeRequest(method: string, params: Record): Promise { diff --git a/packages/walletkit-android-bridge/src/types/api.ts b/packages/walletkit-android-bridge/src/types/api.ts index dac436143..de58b0c78 100644 --- a/packages/walletkit-android-bridge/src/types/api.ts +++ b/packages/walletkit-android-bridge/src/types/api.ts @@ -531,4 +531,36 @@ export interface WalletKitBridgeApi { getSwapQuote(args: GetSwapQuoteArgs): PromiseOrValue; buildSwapTransaction(args: BuildSwapTransactionArgs): PromiseOrValue; registerKotlinSwapProvider(args: RegisterKotlinSwapProviderArgs): PromiseOrValue; + + // Per-wallet ApiClient proxy: Android `BridgedJSAPIClient` round-trips + // `wallet.client.` through these so the underlying JS `ApiClient` + // (built-in or user-supplied) handles the call. Mirrors iOS JSTONAPIClient. + walletClientSendBoc(args: { walletId: string; boc: string }): PromiseOrValue<{ result: string }>; + walletClientRunGetMethod(args: { + walletId: string; + address: string; + method: string; + stack?: unknown[]; + seqno?: number; + }): PromiseOrValue; + walletClientGetBalance(args: { + walletId: string; + address: string; + seqno?: number; + }): PromiseOrValue<{ result: string }>; + walletClientGetMasterchainInfo(args: { walletId: string }): PromiseOrValue; + walletClientNftItemsByAddress(args: { walletId: string; request: unknown }): PromiseOrValue; + walletClientNftItemsByOwner(args: { walletId: string; request: unknown }): PromiseOrValue; + walletClientFetchEmulation(args: { + walletId: string; + messageBoc: string; + ignoreSignature?: boolean; + }): PromiseOrValue; + walletClientAccountState(args: { walletId: string; address: string; seqno?: number }): PromiseOrValue; + walletClientAccountStates(args: { walletId: string; addresses: string[] }): PromiseOrValue; + walletClientResolveDnsWallet(args: { walletId: string; domain: string }): PromiseOrValue<{ result: string | null }>; + walletClientBackResolveDnsWallet(args: { + walletId: string; + address: string; + }): PromiseOrValue<{ result: string | null }>; } From 49938b923cc5156f96cc7d2fd0011452c194bb0a Mon Sep 17 00:00:00 2001 From: Dmitrii Nikulin Date: Mon, 8 Jun 2026 19:48:03 +0300 Subject: [PATCH 2/2] feat(gasless): add Android bridge gasless API + wallet signMessage/publicKey --- .../src/api/gasless.ts | 80 +++++++++++++++++++ .../walletkit-android-bridge/src/api/index.ts | 13 +++ .../src/api/wallets.ts | 8 ++ .../walletkit-android-bridge/src/types/api.ts | 47 +++++++++++ 4 files changed, 148 insertions(+) create mode 100644 packages/walletkit-android-bridge/src/api/gasless.ts diff --git a/packages/walletkit-android-bridge/src/api/gasless.ts b/packages/walletkit-android-bridge/src/api/gasless.ts new file mode 100644 index 000000000..7eab43887 --- /dev/null +++ b/packages/walletkit-android-bridge/src/api/gasless.ts @@ -0,0 +1,80 @@ +/** + * 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 type { + GaslessAPI, + GaslessConfig, + GaslessProviderInterface, + GaslessProviderMetadata, + GaslessQuote, + GaslessQuoteParams, + GaslessSendParams, + GaslessSendResponse, + Network, +} from '@ton/walletkit'; +import { TonApiGaslessProvider } from '@ton/walletkit/gasless/tonapi'; +import type { TonApiGaslessProviderConfig } from '@ton/walletkit/gasless/tonapi'; + +import { getKit } from '../utils/bridge'; +import { get, retainWithId } from '../utils/registry'; + +async function getGasless(): Promise { + const instance = await getKit(); + if (!instance.gasless) throw new Error('Gasless is not configured'); + return instance.gasless; +} + +export async function createTonApiGaslessProvider(args?: { + config?: TonApiGaslessProviderConfig; +}): Promise<{ providerId: string }> { + const instance = await getKit(); + const provider = TonApiGaslessProvider.createFromContext(instance.createFactoryContext(), args?.config ?? {}); + // Retain under the provider's own id so it matches what GaslessManager indexes on registerProvider. + retainWithId(provider.providerId, provider); + return { providerId: provider.providerId }; +} + +export async function registerGaslessProvider(args: { providerId: string }): Promise { + (await getGasless()).registerProvider(get(args.providerId) as GaslessProviderInterface); +} + +export async function setDefaultGaslessProvider(args: { providerId: string }): Promise { + (await getGasless()).setDefaultProvider(args.providerId); +} + +export async function getRegisteredGaslessProviders(): Promise<{ providerIds: string[] }> { + const providerIds = (await getGasless()).getProviders().map((provider) => provider.providerId); + return { providerIds }; +} + +export async function hasGaslessProvider(args: { providerId: string }): Promise<{ result: boolean }> { + const result = (await getGasless()).hasProvider(args.providerId); + return { result }; +} + +export async function getGaslessMetadata(args: { providerId?: string }): Promise { + return (await getGasless()).getMetadata(args.providerId); +} + +export async function getGaslessConfig(args: { network?: Network; providerId?: string }): Promise { + return (await getGasless()).getConfig(args.network, args.providerId); +} + +export async function getGaslessQuote(args: { + params: GaslessQuoteParams; + providerId?: string; +}): Promise { + return (await getGasless()).getQuote(args.params, args.providerId); +} + +export async function gaslessSendTransaction(args: { + params: GaslessSendParams; + providerId?: string; +}): Promise { + return (await getGasless()).sendTransaction(args.params, args.providerId); +} diff --git a/packages/walletkit-android-bridge/src/api/index.ts b/packages/walletkit-android-bridge/src/api/index.ts index b60ed5d40..4c7a308e0 100644 --- a/packages/walletkit-android-bridge/src/api/index.ts +++ b/packages/walletkit-android-bridge/src/api/index.ts @@ -19,6 +19,7 @@ import * as staking from './staking'; import * as browser from './browser'; import * as streaming from './streaming'; import * as swap from './swap'; +import * as gasless from './gasless'; import * as walletClient from './walletClient'; import { eventListeners } from './eventListeners'; @@ -45,6 +46,8 @@ export const api = { getWallet: wallets.getWalletById, getWalletAddress: wallets.getWalletAddress, getWalletNetwork: wallets.getWalletNetwork, + getWalletPublicKey: wallets.getWalletPublicKey, + getSignedSignMessage: wallets.getSignedSignMessage, removeWallet: wallets.removeWallet, getBalance: wallets.getBalance, @@ -127,6 +130,16 @@ export const api = { buildSwapTransaction: swap.buildSwapTransaction, registerKotlinSwapProvider: swap.registerKotlinSwapProvider, + createTonApiGaslessProvider: gasless.createTonApiGaslessProvider, + registerGaslessProvider: gasless.registerGaslessProvider, + setDefaultGaslessProvider: gasless.setDefaultGaslessProvider, + getRegisteredGaslessProviders: gasless.getRegisteredGaslessProviders, + hasGaslessProvider: gasless.hasGaslessProvider, + getGaslessMetadata: gasless.getGaslessMetadata, + getGaslessConfig: gasless.getGaslessConfig, + getGaslessQuote: gasless.getGaslessQuote, + gaslessSendTransaction: gasless.gaslessSendTransaction, + walletClientSendBoc: walletClient.walletClientSendBoc, walletClientRunGetMethod: walletClient.walletClientRunGetMethod, walletClientGetBalance: walletClient.walletClientGetBalance, diff --git a/packages/walletkit-android-bridge/src/api/wallets.ts b/packages/walletkit-android-bridge/src/api/wallets.ts index 8f1b55f94..e1f6f1911 100644 --- a/packages/walletkit-android-bridge/src/api/wallets.ts +++ b/packages/walletkit-android-bridge/src/api/wallets.ts @@ -132,6 +132,14 @@ export async function getWalletNetwork(args: { walletId: string }) { return wallet(args.walletId, 'getNetwork'); } +export async function getWalletPublicKey(args: { walletId: string }) { + return wallet(args.walletId, 'getPublicKey'); +} + +export async function getSignedSignMessage(args: { walletId: string; request: TransactionRequest }) { + return wallet(args.walletId, 'getSignedSignMessage', args.request); +} + export async function removeWallet(args: { walletId: string }) { return kit('removeWallet', args.walletId); } diff --git a/packages/walletkit-android-bridge/src/types/api.ts b/packages/walletkit-android-bridge/src/types/api.ts index de58b0c78..27d5a9dc3 100644 --- a/packages/walletkit-android-bridge/src/types/api.ts +++ b/packages/walletkit-android-bridge/src/types/api.ts @@ -33,6 +33,7 @@ import type { } from '@ton/walletkit'; import type { DeDustSwapProviderConfig } from '@ton/walletkit/swap/dedust'; import type { OmnistonSwapProviderConfig } from '@ton/walletkit/swap/omniston'; +import type { TonApiGaslessProviderConfig } from '@ton/walletkit/gasless/tonapi'; import type { TONBase64, TONHex, TONUserFriendlyAddress } from './brands'; import type { WalletKitBridgeEventCallback } from './events'; @@ -429,6 +430,41 @@ export interface BuildSwapTransactionArgs { params: Record; } +export interface CreateTonApiGaslessProviderArgs { + config?: TonApiGaslessProviderConfig; +} + +export interface RegisterGaslessProviderArgs { + providerId: string; +} + +export interface SetDefaultGaslessProviderArgs { + providerId: string; +} + +export interface HasGaslessProviderArgs { + providerId: string; +} + +export interface GetGaslessMetadataArgs { + providerId?: string; +} + +export interface GetGaslessConfigArgs { + network?: Record; + providerId?: string; +} + +export interface GetGaslessQuoteArgs { + params: Record; + providerId?: string; +} + +export interface GaslessSendTransactionArgs { + params: Record; + providerId?: string; +} + export interface WalletKitBridgeApi { init(config?: WalletKitBridgeInitConfig): PromiseOrValue<{ ok: true }>; setEventsListeners(args?: SetEventsListenersArgs): PromiseOrValue<{ ok: true }>; @@ -454,6 +490,8 @@ export interface WalletKitBridgeApi { getWallets(): PromiseOrValue<{ walletId: string | undefined; wallet: Wallet }[]>; getWallet(args: { walletId: string }): PromiseOrValue<{ walletId: string | undefined; wallet: Wallet } | null>; getWalletAddress(args: { walletId: string }): PromiseOrValue; + getWalletPublicKey(args: { walletId: string }): PromiseOrValue; + getSignedSignMessage(args: { walletId: string; request: Record }): PromiseOrValue; removeWallet(args: RemoveWalletArgs): PromiseOrValue; getBalance(args: GetBalanceArgs): PromiseOrValue; getRecentTransactions(args: GetRecentTransactionsArgs): PromiseOrValue; @@ -531,6 +569,15 @@ export interface WalletKitBridgeApi { getSwapQuote(args: GetSwapQuoteArgs): PromiseOrValue; buildSwapTransaction(args: BuildSwapTransactionArgs): PromiseOrValue; registerKotlinSwapProvider(args: RegisterKotlinSwapProviderArgs): PromiseOrValue; + createTonApiGaslessProvider(args?: CreateTonApiGaslessProviderArgs): PromiseOrValue<{ providerId: string }>; + registerGaslessProvider(args: RegisterGaslessProviderArgs): PromiseOrValue; + setDefaultGaslessProvider(args: SetDefaultGaslessProviderArgs): PromiseOrValue; + getRegisteredGaslessProviders(): PromiseOrValue<{ providerIds: string[] }>; + hasGaslessProvider(args: HasGaslessProviderArgs): PromiseOrValue<{ result: boolean }>; + getGaslessMetadata(args: GetGaslessMetadataArgs): PromiseOrValue; + getGaslessConfig(args: GetGaslessConfigArgs): PromiseOrValue; + getGaslessQuote(args: GetGaslessQuoteArgs): PromiseOrValue; + gaslessSendTransaction(args: GaslessSendTransactionArgs): PromiseOrValue; // Per-wallet ApiClient proxy: Android `BridgedJSAPIClient` round-trips // `wallet.client.` through these so the underlying JS `ApiClient`