import {
AppRequest,
CHAIN,
CONNECT_EVENT_ERROR_CODES,
ConnectEvent,
ConnectEventSuccess,
ConnectManifest,
ConnectRequest,
DeviceInfo,
RpcMethod,
SendTransactionRpcRequest,
SendTransactionRpcResponseError,
SendTransactionRpcResponseSuccess,
TonAddressItem,
TonProofItem,
WalletEvent,
WalletResponse,
} from '@tonconnect/protocol';
export type TonConnectCallback = (event: WalletEvent | DisconnectEvent) => void;
// https://github.com/ton-connect/sdk/blob/main/packages/sdk/src/provider/injected/models/injected-wallet-api.ts
export interface TonConnectBridge {
deviceInfo: DeviceInfo; // see Requests/Responses spec
walletInfo?: WalletInfo;
protocolVersion: number; // max supported Ton Connect version (e.g. 2)
isWalletBrowser: boolean; // if the page is opened into wallet's browser
connect(
protocolVersion: number,
message: ConnectRequest,
): Promise<ConnectEvent>;
restoreConnection(): Promise<ConnectEvent>;
send<T extends RpcMethod>(message: AppRequest<T>): Promise<WalletResponse<T>>;
listen(callback: TonConnectCallback): () => void;
}
export interface DisconnectEvent {
event: 'disconnect';
id: number | string;
payload: Record<string, never>;
}
export interface WalletInfo {
name: string;
image: string; // <png image url>
tondns?: string;
about_url: string;
}
// Instance of this class should be injected in window.[custodian].tonconnect property.
export class JsBridge implements TonConnectBridge {
deviceInfo: DeviceInfo = {
platform: 'browser',
appName: '[custodian]', // Must match your manifest app_name
appVersion: '1.0.0', // Your wallet version
maxProtocolVersion: 2, // TON Connect protocol version, currently 2
features: [
'SendTransaction', // Keep 'SendTransaction' as string for backward compatibility
{ // And pass the object of 'SendTransaction' feature
name: 'SendTransaction',
maxMessages: 4,
extraCurrencySupported: false
}
]
};
isWalletBrowser: boolean = true;
protocolVersion: number = 2;
walletInfo: WalletInfo = {
name: 'walletName',
about_url: 'about.com',
image: 'image.png',
};
// Refer to https://github.com/ton-blockchain/ton-connect/blob/main/requests-responses.md#initiating-connection documentation for more details.
async connect(protocolVersion: number, request: ConnectRequest): Promise<ConnectEvent> {
if (protocolVersion > this.protocolVersion) {
throw new Error('Invalid TON Connect URL');
}
// Check if the ton_addr is requested in the connection request, if not, throw an error
const tonAddrItemRequest: TonAddressItem | null = request.items.find(p => p.name === 'ton_addr') ?? null;
if (!tonAddrItemRequest) {
throw new Error("`ton_addr` item is required in the connection request");
}
// Check if the ton_proof is requested in the connection request, optional
const tonProofItemRequest: TonProofItem | null = request.items.find(p => p.name === 'ton_proof') ?? null;
// Load app manifest
const manifestUrl: string = request.manifestUrl; // app manifest url
const manifest: ConnectManifest = await fetch(manifestUrl).then(res => res.json());
if (!manifest) {
throw new Error("Failed to load app manifest");
}
// 2. Show connection approval dialog to the user
const userApproved = await confirm(`Allow ${request.manifestUrl} to connect to your wallet?`);
if (!userApproved) {
// User rejected the connection
throw new Error('User rejected connection'); //
}
// 3. Get the user's wallet data from custodian API
const walletAddress = '0:9C60B85...57805AC'; // Replace with actual address from custodian API
const walletPublicKey = 'ADA60BC...1B56B86'; // Replace with actual wallet's public key from custodian API
const walletStateInit = 'te6cckEBBAEA...PsAlxCarA=='; // Replace with actual wallet's state init from custodian API
// 4. Create the connect event
return {
event: 'connect',
id: 0, // The id field is 0 for connect events
payload: {
items: [
{
name: 'ton_addr',
address: walletAddress,
network: CHAIN.MAINNET,
publicKey: walletPublicKey,
walletStateInit: walletStateInit
}
// If ton_proof was requested in the connection request, include it here:
// Note: how to get the proof is described in separate section
// {
// name: 'ton_proof',
// proof: {
// // Signed proof data
// }
// }
],
device: this.deviceInfo
}
};
}
private listeners: TonConnectCallback[] = [];
listen(callback: TonConnectCallback): () => void {
this.listeners.push(callback);
return () => {
this.listeners = this.listeners.filter(listener => listener !== callback);
}
}
// Function to disconnect from a dApp
// Refer to the https://github.com/ton-blockchain/ton-connect/blob/main/requests-responses.md#wallet-events documentation for more details.
async disconnectFromDApp() {
// Create a disconnect event
// The id field should be incremented for each sent message
const disconnectEvent = {
event: 'disconnect',
id: nextEventId++,
payload: {
reason: 'user_disconnected'
}
} as const;
this.listeners.map(listener => listener(disconnectEvent));
}
async restoreConnection(): Promise<ConnectEvent> {
// 1. Get the user's wallet data from custodian API
const walletAddress = '0:9C60B85...57805AC'; // Replace with actual address from custodian API
const walletPublicKey = 'ADA60BC...1B56B86'; // Replace with actual wallet's public key from custodian API
const walletStateInit = 'te6cckEBBAEA...PsAlxCarA=='; // Replace with actual wallet's state init from custodian API
// 2. Create the connect event
return {
event: 'connect',
id: 0, // The id field is 0 for connect events
payload: {
items: [
{
name: 'ton_addr',
address: walletAddress,
network: CHAIN.MAINNET,
publicKey: walletPublicKey,
walletStateInit: walletStateInit
}
// If ton_proof was requested in the connection request, include it here:
// Note: how to get the proof is described in separate section
// {
// name: 'ton_proof',
// proof: {
// // Signed proof data
// }
// }
],
device: this.deviceInfo
}
};
}
// Handle messages from dApps
// Refer to the https://github.com/ton-blockchain/ton-connect/blob/main/requests-responses.md#sign-and-send-transaction documentation for more details.
// Parameters:
// - request: The request from the dApp
send<T extends RpcMethod>(request: AppRequest<T>): Promise<WalletResponse<T>> {
console.log(`Received message:`, request);
// Check the message type
if (request.method === 'sendTransaction') {
// Handle transaction request
await handleTransactionRequest(request);
} else if (request.method === 'disconnect') {
// Handle disconnect request
await handleDisconnectRequest(request);
} else {
console.warn(`Unknown message method: ${request.method}`);
}
}
}
// Handle transaction request
// Parameters:
// - request: The transaction request object from the dApp
async function handleTransactionRequest(request: SendTransactionRpcRequest) {
// Extract transaction details
const {id, params} = request;
let [{network, from, valid_until, messages}] = JSON.parse(params[0]);
// The wallet should check all the parameters of the request, if any of the checks fail, it should send an error response back to the dApp
// Check if the selected network is valid
if (network !== CHAIN.MAINNET) {
return {
id: request.id,
error: {code: 1, message: 'Invalid network ID'},
} satisfies SendTransactionRpcResponseError;
}
// Check if the selected wallet address is valid
if (!Address.parse(from).equals(Address.parse(sessionData.walletAddress))) {
return {
id: request.id,
error: {
code: 1,
message: 'Invalid wallet address'
},
} satisfies SendTransactionRpcResponseError;
}
// Set limit for valid_until
const limit = 60 * 5; // 5 minutes
const now = Math.round(Date.now() / 1000);
valid_until = Math.min(valid_until ?? Number.MAX_SAFE_INTEGER, now + limit);
// Check if the transaction is still valid
if (valid_until < now) {
return {
id: request.id,
error: {
code: 1,
message: 'Transaction expired'
},
} satisfies SendTransactionRpcResponseError;
}
// Check if the messages are valid
for (const message of messages) {
if (!message.to || !Address.isFriendly(message.to)) {
return {
id: request.id,
error: {
code: 1,
message: 'Address is not friendly'
},
} satisfies SendTransactionRpcResponseError;
}
// Check if the value is a string of digits
if (!(typeof message.value === 'string' && /^[0-9]+$/.test(message.value))) {
return {
id: request.id,
error: {
code: 1,
message: 'Value is not a string of digits'
},
} satisfies SendTransactionRpcResponseError;
}
// Check if the payload is valid boc
if (message.payload) {
try {
const payload = Cell.fromBoc(message.payload)[0];
} catch (e) {
return {
id: request.id,
error: {
code: 1,
message: 'Payload is not valid boc'
},
} satisfies SendTransactionRpcResponseError;
}
}
// Check if the stateInit is valid boc
if (message.stateInit) {
try {
const stateInit = Cell.fromBoc(message.stateInit)[0];
} catch (e) {
return {
id: request.id,
error: {
code: 1,
message: 'StateInit is not valid boc'
},
} satisfies SendTransactionRpcResponseError;
}
}
}
if (messages.length === 0) {
return {
id: request.id,
error: {
code: 1,
message: 'No messages'
},
} satisfies SendTransactionRpcResponseError;
}
// Show transaction approval UI to the user
const userApproved = await confirm(`Approve transaction from ${dAppName}?`);
// User rejected the transaction - send error response
if (!userApproved) {
return {
id: request.id,
error: {
code: 300,
message: 'Transaction rejected by user'
},
} satisfies SendTransactionRpcResponseError;
}
if (messages.length > 4) {
return {
id: request.id,
error: {
code: 1,
message: 'Too many messages'
},
} satisfies SendTransactionRpcResponseError;
}
// User approved the transaction - sign it using custodian API, send signed boc to the blockchain and send success response
try {
// Sign the transaction (implementation would depend on custodian API)
const signedBoc = await signTransactionWithMpcApi(sessionData.walletAddress, messages);
// Send the signed transaction to the blockchain and wait for the result
const isSuccess = await sendTransactionToBlockchain(signedBoc);
if (!isSuccess) {
throw new Error('Transaction send failed');
}
return {
id: request.id,
result: signedBoc,
} satisfies SendTransactionRpcResponseSuccess;
} catch (error) {
return {
id: request.id,
error: {
code: 100,
message: 'Transaction signing failed'
},
} satisfies SendTransactionRpcResponseError;
}
}