Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/**
* 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 {
ApiClient,
Base64String,
UserFriendlyAddress,
RawStackItem,
GetMethodResult,
NFTsRequest,
NFTsResponse,
UserNFTsRequest,
TokenAmount,
TransactionsResponse,
JettonsResponse,
FullAccountState,
ToncenterEmulationResult,
ToncenterResponseJettonMasters,
ToncenterTracesResponse,
TransactionsByAddressRequest,
GetTransactionByHashRequest,
GetPendingTransactionsRequest,
GetTraceRequest,
GetPendingTraceRequest,
GetJettonsByOwnerRequest,
GetJettonsByAddressRequest,
GetEventsRequest,
GetEventsResponse,
Network,
} from '@ton/walletkit';

import { log, 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;
};

type AndroidWindow = Window & {
WalletKitNative?: AndroidAPIClientBridge;
};

/**
* Android native API client adapter.
* Uses Android's JavascriptInterface methods for API calls.
* Similar to SwiftAPIClientAdapter for iOS.
*/
export class AndroidAPIClientAdapter implements ApiClient {
private androidBridge: AndroidAPIClientBridge;
private network: Network;

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;
}

/**
* 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<string> {
log('[AndroidAPIClientAdapter] sendBoc:', boc.substring(0, 50) + '...');
try {
const networkJson = JSON.stringify(this.network);
const result = this.androidBridge.apiSendBoc(networkJson, boc);
log('[AndroidAPIClientAdapter] sendBoc result:', result);
return result;
} catch (err) {
error('[AndroidAPIClientAdapter] sendBoc failed:', err);
throw err;
}
}

async runGetMethod(
address: UserFriendlyAddress,
method: string,
stack?: RawStackItem[],
seqno?: number,
): Promise<GetMethodResult> {
log('[AndroidAPIClientAdapter] runGetMethod:', address, method);
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;
log('[AndroidAPIClientAdapter] runGetMethod result:', result);
return result;
} catch (err) {
error('[AndroidAPIClientAdapter] runGetMethod failed:', err);
throw err;
}
}

// Methods not implemented - will throw if called
// These are optional for mobile usage

async nftItemsByAddress(_request: NFTsRequest): Promise<NFTsResponse> {
throw new Error('nftItemsByAddress is not implemented yet');
}

async nftItemsByOwner(_request: UserNFTsRequest): Promise<NFTsResponse> {
throw new Error('nftItemsByOwner is not implemented yet');
}

async fetchEmulation(_messageBoc: Base64String, _ignoreSignature?: boolean): Promise<ToncenterEmulationResult> {
throw new Error('fetchEmulation is not implemented yet');
}

async getAccountState(_address: UserFriendlyAddress, _seqno?: number): Promise<FullAccountState> {
throw new Error('getAccountState is not implemented yet');
}

async getBalance(address: UserFriendlyAddress, seqno?: number): Promise<TokenAmount> {
log('[AndroidAPIClientAdapter] getBalance:', address);
try {
const networkJson = JSON.stringify(this.network);
const seqnoArg = seqno ?? -1; // Use -1 to represent null
const result = this.androidBridge.apiGetBalance(networkJson, address, seqnoArg);
log('[AndroidAPIClientAdapter] getBalance result:', result);
return result;
} catch (err) {
error('[AndroidAPIClientAdapter] getBalance failed:', err);
throw err;
}
}

async getAccountTransactions(_request: TransactionsByAddressRequest): Promise<TransactionsResponse> {
throw new Error('getAccountTransactions is not implemented yet');
}

async getTransactionsByHash(_request: GetTransactionByHashRequest): Promise<TransactionsResponse> {
throw new Error('getTransactionsByHash is not implemented yet');
}

async getPendingTransactions(_request: GetPendingTransactionsRequest): Promise<TransactionsResponse> {
throw new Error('getPendingTransactions is not implemented yet');
}

async getTrace(_request: GetTraceRequest): Promise<ToncenterTracesResponse> {
throw new Error('getTrace is not implemented yet');
}

async getPendingTrace(_request: GetPendingTraceRequest): Promise<ToncenterTracesResponse> {
throw new Error('getPendingTrace is not implemented yet');
}

async resolveDnsWallet(_domain: string): Promise<string | null> {
throw new Error('resolveDnsWallet is not implemented yet');
}

async backResolveDnsWallet(_address: UserFriendlyAddress): Promise<string | null> {
throw new Error('backResolveDnsWallet is not implemented yet');
}

async jettonsByAddress(_request: GetJettonsByAddressRequest): Promise<ToncenterResponseJettonMasters> {
throw new Error('jettonsByAddress is not implemented yet');
}

async jettonsByOwnerAddress(_request: GetJettonsByOwnerRequest): Promise<JettonsResponse> {
throw new Error('jettonsByOwnerAddress is not implemented yet');
}

async getEvents(_request: GetEventsRequest): Promise<GetEventsResponse> {
throw new Error('getEvents is not implemented yet');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const eventListeners = {
onConnectListener: null as BridgeEventListener,
onTransactionListener: null as BridgeEventListener,
onSignDataListener: null as BridgeEventListener,
onSignMessageListener: null as BridgeEventListener,
onDisconnectListener: null as BridgeEventListener,
onErrorListener: null as BridgeEventListener,
};
2 changes: 2 additions & 0 deletions packages/walletkit-android-bridge/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ const apiImpl: WalletKitBridgeApi = {
rejectTransactionRequest: requests.rejectTransactionRequest,
approveSignDataRequest: requests.approveSignDataRequest,
rejectSignDataRequest: requests.rejectSignDataRequest,
approveSignMessageRequest: requests.approveSignMessageRequest,
rejectSignMessageRequest: requests.rejectSignMessageRequest,

// TonConnect & sessions
handleTonConnectUrl: tonconnect.handleTonConnectUrl,
Expand Down
16 changes: 16 additions & 0 deletions packages/walletkit-android-bridge/src/api/initialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ export function setEventsListeners(args?: SetEventsListenersArgs): { ok: true }

kit.onSignDataRequest(eventListeners.onSignDataListener);

// Register signMessage listener for gasless transactions
if (eventListeners.onSignMessageListener) {
kit.removeSignMessageRequestCallback?.();
}

eventListeners.onSignMessageListener = (event: unknown) => {
callback('signMessage', event);
};

kit.onSignMessageRequest?.(eventListeners.onSignMessageListener);

if (eventListeners.onDisconnectListener) {
kit.removeDisconnectCallback();
}
Expand Down Expand Up @@ -120,6 +131,11 @@ export function removeEventListeners(): { ok: true } {
eventListeners.onSignDataListener = null;
}

if (eventListeners.onSignMessageListener) {
kit.removeSignMessageRequestCallback?.();
eventListeners.onSignMessageListener = null;
}

if (eventListeners.onDisconnectListener) {
kit.removeDisconnectCallback();
eventListeners.onDisconnectListener = null;
Expand Down
33 changes: 33 additions & 0 deletions packages/walletkit-android-bridge/src/api/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import type {
RejectTransactionRequestArgs,
ApproveSignDataRequestArgs,
RejectSignDataRequestArgs,
ApproveSignMessageRequestArgs,
RejectSignMessageRequestArgs,
} from '../types';
import { callBridge } from '../utils/bridgeWrapper';
import { log } from '../utils/logger';
Expand Down Expand Up @@ -108,3 +110,34 @@ export async function rejectSignDataRequest(args: RejectSignDataRequestArgs) {
return result ?? { success: true };
});
}

/**
* Approves a signMessage request (for gasless transactions).
* Returns a signed internal message BOC that can be sent to a gasless provider.
*/
export async function approveSignMessageRequest(args: ApproveSignMessageRequestArgs) {
return callBridge('approveSignMessageRequest', async (kit) => {
// Enrich event with walletId (same pattern as approveTransactionRequest)
if (args.walletId) {
args.event.walletId = args.walletId;
}

return await kit.approveSignMessageRequest(args.event);
});
}

/**
* Rejects a signMessage request.
*/
export async function rejectSignMessageRequest(args: RejectSignMessageRequestArgs) {
return callBridge('rejectSignMessageRequest', async (kit) => {
// If errorCode is provided, pass it as an error object; otherwise just pass the reason string
const reason =
args.errorCode !== undefined
? { code: args.errorCode, message: args.reason || 'SignMessage rejected' }
: args.reason;

const result = await kit.rejectSignMessageRequest(args.event, reason);
return result ?? { success: true };
});
}
17 changes: 16 additions & 1 deletion packages/walletkit-android-bridge/src/core/initialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
hasAndroidSessionManager,
AndroidTONConnectSessionsManager,
} from '../adapters/AndroidTONConnectSessionsManager';
import { AndroidAPIClientAdapter } from '../adapters/AndroidAPIClientAdapter';

export interface InitTonWalletKitDeps {
emit: (type: WalletKitBridgeEvent['type'], data?: WalletKitBridgeEvent['data']) => void;
Expand Down Expand Up @@ -75,10 +76,24 @@ export async function initTonWalletKit(

// Build networks config - the new SDK requires networks as an object keyed by chain ID
const apiClientConfig = clientEndpoint ? { url: clientEndpoint } : undefined;
const networksConfig: Record<string, { apiClient?: { url: string } }> = {
const networksConfig: Record<string, { apiClient?: { url: string } | AndroidAPIClientAdapter }> = {
[network]: { apiClient: apiClientConfig },
};

// Check if native API clients are available and use them if so
if (AndroidAPIClientAdapter.isAvailable()) {
log('[walletkitBridge] Native API clients available, checking for configured networks');
const availableNetworks = AndroidAPIClientAdapter.getAvailableNetworks();
log('[walletkitBridge] Available native API networks:', JSON.stringify(availableNetworks));

for (const nativeNetwork of availableNetworks) {
log('[walletkitBridge] Using native API client for network:', nativeNetwork.chainId);
networksConfig[nativeNetwork.chainId] = {
apiClient: new AndroidAPIClientAdapter(nativeNetwork),
};
}
}

const kitOptions: Record<string, unknown> = {
network,
networks: networksConfig,
Expand Down
20 changes: 20 additions & 0 deletions packages/walletkit-android-bridge/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,23 @@ export interface RejectSignDataRequestArgs {
errorCode?: number;
}

/**
* Args for approving a signMessage request (gasless transactions).
*/
export interface ApproveSignMessageRequestArgs {
event: TonConnectRequestEvent;
walletId?: string;
}

/**
* Args for rejecting a signMessage request.
*/
export interface RejectSignMessageRequestArgs {
event: TonConnectRequestEvent;
reason?: string;
errorCode?: number;
}

export interface DisconnectSessionArgs {
sessionId?: string;
}
Expand Down Expand Up @@ -272,6 +289,9 @@ export interface WalletKitBridgeApi {
rejectTransactionRequest(args: RejectTransactionRequestArgs): PromiseOrValue<object>;
approveSignDataRequest(args: ApproveSignDataRequestArgs): PromiseOrValue<unknown>;
rejectSignDataRequest(args: RejectSignDataRequestArgs): PromiseOrValue<object>;
// SignMessage methods for gasless transactions
approveSignMessageRequest(args: ApproveSignMessageRequestArgs): PromiseOrValue<unknown>;
rejectSignMessageRequest(args: RejectSignMessageRequestArgs): PromiseOrValue<object>;
listSessions(): PromiseOrValue<unknown>;
disconnectSession(args?: DisconnectSessionArgs): PromiseOrValue<unknown>;
getNfts(args: GetNftsArgs): PromiseOrValue<unknown>;
Expand Down
1 change: 1 addition & 0 deletions packages/walletkit-android-bridge/src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type WalletKitBridgeEventType =
| 'connectRequest'
| 'transactionRequest'
| 'signDataRequest'
| 'signMessage'
| 'disconnect'
| 'requestError'
| 'browserPageStarted'
Expand Down
5 changes: 5 additions & 0 deletions packages/walletkit-android-bridge/src/types/walletkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,9 @@ export interface WalletKitInstance {
rejectTransactionRequest(event: unknown, reason?: string | { code: number; message: string }): Promise<unknown>;
approveSignDataRequest(event: unknown): Promise<unknown>;
rejectSignDataRequest(event: unknown, reason?: string | { code: number; message: string }): Promise<unknown>;
// SignMessage methods for gasless transactions
onSignMessageRequest?(callback: (event: unknown) => void): void;
removeSignMessageRequestCallback?(): void;
approveSignMessageRequest(event: unknown): Promise<unknown>;
rejectSignMessageRequest(event: unknown, reason?: string | { code: number; message: string }): Promise<unknown>;
}
Loading
Loading