Skip to content
Merged
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
33 changes: 17 additions & 16 deletions packages/walletkit-android-bridge/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
*
*/

/**
* Aggregates all domain-specific bridge APIs into a single export.
*/
import type { WalletKitBridgeApi } from '../types';
import * as initialization from './initialization';
import * as cryptography from './cryptography';
Expand All @@ -20,30 +17,27 @@ import * as nft from './nft';
import * as jettons from './jettons';
import * as staking from './staking';
import * as browser from './browser';
import * as streaming from './streaming';
import * as swap from './swap';
import { eventListeners } from './eventListeners';

export { eventListeners };

export const api: WalletKitBridgeApi = {
// Initialization
export const api = {
init: initialization.init,
setEventsListeners: initialization.setEventsListeners,
removeEventListeners: initialization.removeEventListeners,

// Cryptography
mnemonicToKeyPair: cryptography.mnemonicToKeyPair,
sign: cryptography.sign,
createTonMnemonic: cryptography.createTonMnemonic,

// 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,
Expand All @@ -52,48 +46,56 @@ export const api: WalletKitBridgeApi = {
removeWallet: wallets.removeWallet,
getBalance: wallets.getBalance,

// Transactions
getRecentTransactions: transactions.getRecentTransactions,
createTransferTonTransaction: transactions.createTransferTonTransaction,
createTransferMultiTonTransaction: transactions.createTransferMultiTonTransaction,
getTransactionPreview: transactions.getTransactionPreview,
handleNewTransaction: transactions.handleNewTransaction,
sendTransaction: transactions.sendTransaction,

// Requests
approveConnectRequest: requests.approveConnectRequest,
rejectConnectRequest: requests.rejectConnectRequest,
approveTransactionRequest: requests.approveTransactionRequest,
rejectTransactionRequest: requests.rejectTransactionRequest,
approveSignDataRequest: requests.approveSignDataRequest,
rejectSignDataRequest: requests.rejectSignDataRequest,

// TonConnect & sessions
handleTonConnectUrl: tonconnect.handleTonConnectUrl,
connectionEventFromUrl: tonconnect.connectionEventFromUrl,
listSessions: tonconnect.listSessions,
disconnectSession: tonconnect.disconnectSession,
processInternalBrowserRequest: tonconnect.processInternalBrowserRequest,

// NFTs
getNfts: nft.getNfts,
getNft: nft.getNft,
createTransferNftTransaction: nft.createTransferNftTransaction,
createTransferNftRawTransaction: nft.createTransferNftRawTransaction,

// Jettons
getJettons: jettons.getJettons,
createTransferJettonTransaction: jettons.createTransferJettonTransaction,
getJettonBalance: jettons.getJettonBalance,
getJettonWalletAddress: jettons.getJettonWalletAddress,

// Browser events
emitBrowserPageStarted: browser.emitBrowserPageStarted,
emitBrowserPageFinished: browser.emitBrowserPageFinished,
emitBrowserError: browser.emitBrowserError,
emitBrowserBridgeRequest: browser.emitBrowserBridgeRequest,

// Staking
createTonCenterStreamingProvider: streaming.createTonCenterStreamingProvider,
createTonApiStreamingProvider: streaming.createTonApiStreamingProvider,
registerStreamingProvider: streaming.registerStreamingProvider,
streamingHasProvider: streaming.streamingHasProvider,
streamingWatch: streaming.streamingWatch,
streamingUnwatch: streaming.streamingUnwatch,
streamingConnect: streaming.streamingConnect,
streamingDisconnect: streaming.streamingDisconnect,
streamingWatchConnectionChange: streaming.streamingWatchConnectionChange,
streamingWatchBalance: streaming.streamingWatchBalance,
streamingWatchTransactions: streaming.streamingWatchTransactions,
streamingWatchJettons: streaming.streamingWatchJettons,
registerKotlinStreamingProvider: streaming.registerKotlinStreamingProvider,
kotlinProviderDispatch: streaming.kotlinProviderDispatch,

createTonStakersStakingProvider: staking.createTonStakersStakingProvider,
registerStakingProvider: staking.registerStakingProvider,
setDefaultStakingProvider: staking.setDefaultStakingProvider,
Expand All @@ -103,7 +105,6 @@ export const api: WalletKitBridgeApi = {
getStakingProviderInfo: staking.getStakingProviderInfo,
getSupportedUnstakeModes: staking.getSupportedUnstakeModes,

// Swap
createOmnistonSwapProvider: swap.createOmnistonSwapProvider,
createDeDustSwapProvider: swap.createDeDustSwapProvider,
registerSwapProvider: swap.registerSwapProvider,
Expand Down
252 changes: 252 additions & 0 deletions packages/walletkit-android-bridge/src/api/streaming.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/**
* 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 {
BalanceUpdate,
JettonUpdate,
Network,
StreamingProvider,
StreamingUpdate,
StreamingWatchType,
TonApiStreamingProviderConfig,
TonCenterStreamingProviderConfig,
TransactionsUpdate,
} from '@ton/walletkit';
import { TonApiStreamingProvider, TonCenterStreamingProvider } from '@ton/walletkit';
import { v7 as uuidv7 } from 'uuid';

import { emit } from '../transport/messaging';
import { bridgeRequest } from '../transport/nativeBridge';
import { getKit } from '../utils/bridge';
import { get, release, retain, retainWithId } from '../utils/registry';

const kotlinSubCallbacks = new Map<string, (update: unknown) => void>();
const kotlinProviderSubs = new Map<string, Set<string>>();

type InternalStreamingManager = {
providers: Map<string, StreamingProvider>;
providerConnectionUnsubs: Map<string, () => void>;
};

function trackKotlinSub(providerId: string, subId: string): void {
let subs = kotlinProviderSubs.get(providerId);
if (!subs) {
subs = new Set<string>();
kotlinProviderSubs.set(providerId, subs);
}
subs.add(subId);
}

function forgetKotlinSub(providerId: string, subId: string): void {
const subs = kotlinProviderSubs.get(providerId);
if (!subs) return;
subs.delete(subId);
if (subs.size === 0) {
kotlinProviderSubs.delete(providerId);
}
}

function cleanupReplacedKotlinProvider(
instance: { streaming: unknown },
nextProviderId: string,
network: { chainId: string },
): void {
const manager = instance.streaming as InternalStreamingManager;
const networkId = String(network.chainId);
const previousProvider = manager.providers.get(networkId);
if (!(previousProvider instanceof ProxyStreamingProvider)) {
return;
}

manager.providerConnectionUnsubs.get(networkId)?.();
manager.providerConnectionUnsubs.delete(networkId);
manager.providers.delete(networkId);
previousProvider.dispose();
if (previousProvider.providerId !== nextProviderId) {
void bridgeRequest('kotlinProviderRelease', { providerId: previousProvider.providerId });
}
release(previousProvider.providerId);
}

class ProxyStreamingProvider implements StreamingProvider {
readonly type = 'streaming' as const;
readonly network: Network;

constructor(
readonly providerId: string,
network: Network,
) {
this.network = network;
}

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 });
return () => {
kotlinSubCallbacks.delete(subId);
forgetKotlinSub(this.providerId, subId);
void bridgeRequest('kotlinProviderUnwatch', { subId });
};
}

watchBalance(address: string, onChange: (update: BalanceUpdate) => void): () => void {
return this.watch('balance', address, onChange as (update: unknown) => void);
}

watchTransactions(address: string, onChange: (update: TransactionsUpdate) => void): () => void {
return this.watch('transactions', address, onChange as (update: unknown) => void);
}

watchJettons(address: string, onChange: (update: JettonUpdate) => void): () => void {
return this.watch('jettons', address, onChange as (update: unknown) => void);
}

onConnectionChange(callback: (connected: boolean) => void): () => void {
return this.watch('connectionChange', null, callback as (update: unknown) => void);
}

connect(): void {
void bridgeRequest('kotlinProviderConnect', { providerId: this.providerId });
}

disconnect(): void {
void bridgeRequest('kotlinProviderDisconnect', { providerId: this.providerId });
}

dispose(): void {
const subs = kotlinProviderSubs.get(this.providerId);
if (!subs) return;
for (const subId of subs) {
kotlinSubCallbacks.delete(subId);
void bridgeRequest('kotlinProviderUnwatch', { subId });
}
kotlinProviderSubs.delete(this.providerId);
}
}

export async function createTonCenterStreamingProvider(args: { config: TonCenterStreamingProviderConfig }) {
const instance = await getKit();
const provider = new TonCenterStreamingProvider(instance.createFactoryContext(), args.config);
return { providerId: retain('streamingProvider', provider) };
}

export async function createTonApiStreamingProvider(args: { config: TonApiStreamingProviderConfig }) {
const instance = await getKit();
const provider = new TonApiStreamingProvider(instance.createFactoryContext(), args.config);
return { providerId: retain('streamingProvider', provider) };
}

export async function registerStreamingProvider(args: { providerId: string }) {
const instance = await getKit();
const provider = get<StreamingProvider>(args.providerId);
if (!provider) throw new Error(`Streaming provider not found: ${args.providerId}`);
instance.streaming.registerProvider(() => provider);
}

export async function streamingHasProvider(args: { network: { chainId: string } }) {
const instance = await getKit();
return { hasProvider: instance.streaming.hasProvider(args.network) };
}

export async function streamingWatch(args: {
network: { chainId: string };
address: string;
types: StreamingWatchType[];
}) {
const instance = await getKit();
let subscriptionId: string;
const unwatch = instance.streaming.watch(
args.network,
args.address,
args.types as Exclude<StreamingWatchType, 'trace'>[],
(_type: StreamingWatchType, update: StreamingUpdate) => {
emit('streamingUpdate', { subscriptionId, update });
},
);
subscriptionId = retain('streamingSub', unwatch);
return { subscriptionId };
}

export async function streamingUnwatch(args: { subscriptionId: string }) {
const unwatch = get<() => void>(args.subscriptionId);
if (unwatch) {
unwatch();
release(args.subscriptionId);
}
}

export async function streamingConnect() {
const instance = await getKit();
instance.streaming.connect();
}

export async function streamingDisconnect() {
const instance = await getKit();
instance.streaming.disconnect();
}

export async function streamingWatchConnectionChange(args: { network: { chainId: string } }) {
const instance = await getKit();
let subscriptionId: string;
const unwatch = instance.streaming.onConnectionChange(args.network, (connected: boolean) => {
emit('streamingConnectionChange', { subscriptionId, connected });
});
subscriptionId = retain('streamingSub', unwatch);
return { subscriptionId };
}

export async function streamingWatchBalance(args: { network: { chainId: string }; address: string }) {
const instance = await getKit();
let subscriptionId: string;
const unwatch = instance.streaming.watchBalance(args.network, args.address, (update) => {
emit('streamingBalanceUpdate', { subscriptionId, update });
});
subscriptionId = retain('streamingSub', unwatch);
return { subscriptionId };
}

export async function streamingWatchTransactions(args: { network: { chainId: string }; address: string }) {
const instance = await getKit();
let subscriptionId: string;
const unwatch = instance.streaming.watchTransactions(args.network, args.address, (update) => {
emit('streamingTransactionsUpdate', { subscriptionId, update });
});
subscriptionId = retain('streamingSub', unwatch);
return { subscriptionId };
}

export async function streamingWatchJettons(args: { network: { chainId: string }; address: string }) {
const instance = await getKit();
let subscriptionId: string;
const unwatch = instance.streaming.watchJettons(args.network, args.address, (update) => {
emit('streamingJettonsUpdate', { subscriptionId, update });
});
subscriptionId = retain('streamingSub', unwatch);
return { subscriptionId };
}

export async function registerKotlinStreamingProvider(args: { providerId: string; network: { chainId: string } }) {
const instance = await getKit();
cleanupReplacedKotlinProvider(instance, args.providerId, args.network);
const provider = new ProxyStreamingProvider(args.providerId, args.network as unknown as Network);
retainWithId(args.providerId, provider);
instance.streaming.registerProvider(() => provider);
}

export async function kotlinProviderDispatch(args: { subId: string; updateJson: string }) {
const callback = kotlinSubCallbacks.get(args.subId);
if (callback) {
try {
callback(JSON.parse(args.updateJson));
} catch {
// Ignore malformed update payloads
}
}
}
4 changes: 2 additions & 2 deletions packages/walletkit-android-bridge/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ declare global {
}
}

setBridgeApi(api as WalletKitBridgeApi);
setBridgeApi(api as unknown as WalletKitBridgeApi);
registerNativeCallHandler();
registerNativeResponseHandler();

window.walletkitBridge = api;
window.walletkitBridge = api as unknown as WalletKitBridgeApi;
Loading
Loading