import { Adapter, AdapterState, isInBrowser, WalletReadyState, WalletSignMessageError, WalletNotFoundError, WalletDisconnectedError, WalletSignTransactionError, WalletGetNetworkError, WalletConnectionError, isInMobileBrowser, WalletError, } from '@tronweb3/tronwallet-abstract-adapter'; import type { Transaction, SignedTransaction, AdapterName, BaseAdapterConfig, Network, TronWeb, } from '@tronweb3/tronwallet-abstract-adapter'; import { getNetworkInfoByTronWeb } from '@tronweb3/tronwallet-adapter-tronlink'; import type { Tron, TronAccountsChangedCallback } from '@tronweb3/tronwallet-adapter-tronlink'; import { openTokenPocket, supportTokenPocket } from './utils.js'; import type { TIP6963AnnounceProviderEvent } from '@tronweb3/tronwallet-abstract-adapter'; import { TIP6963AnnounceProviderEventName, TIP6963RequestProviderEventName, } from '@tronweb3/tronwallet-abstract-adapter'; export interface TokenPocketAdapterConfig extends BaseAdapterConfig { /** * Timeout in millisecond for checking if is in TokenPocket App. * Default is 2 * 1000ms */ checkTimeout?: number; /** * Set if open TokenPocket app using DeepLink. * Default is true. */ openAppWithDeeplink?: boolean; } export const TokenPocketAdapterName = 'TokenPocket' as AdapterName<'TokenPocket'>; export interface TokenPocketWallet { ready: boolean; tronWeb: TronWeb; tron: Tron; } declare global { interface Window { tokenpocket?: TokenPocketWallet; } } export class TokenPocketAdapter extends Adapter { name = TokenPocketAdapterName; url = 'https://tokenpocket.pro/'; icon = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEyMCIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGc+CjxwYXRoIGQ9Ik0xMDQxLjUyIDBILTI3VjEwMjRIMTA0MS41MlYwWiIgZmlsbD0iIzI5ODBGRSIvPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfNDA4XzIyNSkiPgo8cGF0aCBkPSJNNDA2Ljc5NiA0MzguNjQzSDQwNi45MjdDNDA2Ljc5NiA0MzcuODU3IDQwNi43OTYgNDM2Ljk0IDQwNi43OTYgNDM2LjE1NFY0MzguNjQzWiIgZmlsbD0iIzI5QUVGRiIvPgo8cGF0aCBkPSJNNjY3LjYwMiA0NjMuNTMzSDUyMy4yNDlWNzI0LjA3NkM1MjMuMjQ5IDczNi4zODkgNTMzLjIwNCA3NDYuMzQ1IDU0NS41MTcgNzQ2LjM0NUg2NDUuMzMzQzY1Ny42NDcgNzQ2LjM0NSA2NjcuNjAyIDczNi4zODkgNjY3LjYwMiA3MjQuMDc2VjQ2My41MzNaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNDUzLjU2MyAyNzdINDQ4LjcxNkgxOTAuMjY5QzE3Ny45NTUgMjc3IDE2OCAyODYuOTU1IDE2OCAyOTkuMjY5VjM4OS42NTNDMTY4IDQwMS45NjcgMTc3Ljk1NSA0MTEuOTIyIDE5MC4yNjkgNDExLjkyMkgyNTAuOTE4SDI3NS4wMjFWNDM4LjY0NFY3MjQuNzMxQzI3NS4wMjEgNzM3LjA0NSAyODQuOTc2IDc0NyAyOTcuMjg5IDc0N0gzOTIuMTI4QzQwNC40NDEgNzQ3IDQxNC4zOTYgNzM3LjA0NSA0MTQuMzk2IDcyNC43MzFWNDM4LjY0NFY0MzYuMTU2VjQxMS45MjJINDM4LjQ5OUg0NDguMzIzSDQ1My4xN0M0OTAuMzcyIDQxMS45MjIgNTIwLjYzMSAzODEuNjYzIDUyMC42MzEgMzQ0LjQ2MUM1MjEuMDI0IDMwNy4yNTkgNDkwLjc2NSAyNzcgNDUzLjU2MyAyNzdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNjY3LjczNSA0NjMuNTMzVjY0NS4zNUM2NzIuNzEzIDY0Ni41MjkgNjc3LjgyMSA2NDcuNDQ2IDY4My4wNjEgNjQ4LjIzMkM2OTAuMzk3IDY0OS4yOCA2OTcuOTk0IDY0OS45MzUgNzA1LjU5MiA2NTAuMDY2QzcwNS45ODUgNjUwLjA2NiA3MDYuMzc4IDY1MC4wNjYgNzA2LjkwMiA2NTAuMDY2VjUwNS40NUM2ODUuMDI2IDUwNC4wMDkgNjY3LjczNSA0ODUuODAxIDY2Ny43MzUgNDYzLjUzM1oiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl80MDhfMjI1KSIvPgo8cGF0aCBkPSJNNzA5Ljc4MSAyNzdDNjA2LjgyMiAyNzcgNTIzLjI0OSAzNjAuNTczIDUyMy4yNDkgNDYzLjUzM0M1MjMuMjQ5IDU1Mi4wODQgNTg0Ljk0NiA2MjYuMjI1IDY2Ny43MzMgNjQ1LjM1VjQ2My41MzNDNjY3LjczMyA0NDAuMzQ3IDY4Ni41OTYgNDIxLjQ4NCA3MDkuNzgxIDQyMS40ODRDNzMyLjk2NyA0MjEuNDg0IDc1MS44MyA0NDAuMzQ3IDc1MS44MyA0NjMuNTMzQzc1MS44MyA0ODMuMDUxIDczOC42IDQ5OS40MjUgNzIwLjUyMyA1MDQuMTRDNzE3LjExNyA1MDUuMDU3IDcxMy40NDkgNTA1LjU4MSA3MDkuNzgxIDUwNS41ODFWNjUwLjA2NkM3MTMuNDQ5IDY1MC4wNjYgNzE2Ljk4NiA2NDkuOTM1IDcyMC41MjMgNjQ5LjgwNEM4MTguNTA1IDY0NC4xNzEgODk2LjMxNCA1NjIuOTU2IDg5Ni4zMTQgNDYzLjUzM0M4OTYuNDQ1IDM2MC41NzMgODEyLjg3MiAyNzcgNzA5Ljc4MSAyNzdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNzA5Ljc4IDY1MC4wNjZWNTA1LjU4MUM3MDguNzMzIDUwNS41ODEgNzA3LjgxNiA1MDUuNTgxIDcwNi43NjggNTA1LjQ1VjY1MC4wNjZDNzA3LjgxNiA2NTAuMDY2IDcwOC44NjQgNjUwLjA2NiA3MDkuNzggNjUwLjA2NloiIGZpbGw9IndoaXRlIi8+CjwvZz4KPC9nPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzQwOF8yMjUiIHgxPSI3MDkuODQ0IiB5MT0iNTU2LjgyNyIgeDI9IjY2Ny43NTMiIHkyPSI1NTYuODI3IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IndoaXRlIi8+CjxzdG9wIG9mZnNldD0iMC45NjY3IiBzdG9wLWNvbG9yPSJ3aGl0ZSIgc3RvcC1vcGFjaXR5PSIwLjMyMzMiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSJ3aGl0ZSIgc3RvcC1vcGFjaXR5PSIwLjMiLz4KPC9saW5lYXJHcmFkaWVudD4KPGNsaXBQYXRoIGlkPSJjbGlwMF80MDhfMjI1Ij4KPHJlY3Qgd2lkdGg9IjcyOC40NDgiIGhlaWdodD0iNDcwIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTY4IDI3NykiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K'; config: Required; private _readyState: WalletReadyState = isInBrowser() ? WalletReadyState.Loading : WalletReadyState.NotFound; private _state: AdapterState = AdapterState.Loading; private _connecting: boolean; private _wallet: TokenPocketWallet | null; private _address: string | null; constructor(config: TokenPocketAdapterConfig = {}) { super(); const { checkTimeout = 2 * 1000, openUrlWhenWalletNotFound = true, openAppWithDeeplink = true } = config; if (typeof checkTimeout !== 'number') { throw new Error('[TokenPocketAdapter] config.checkTimeout should be a number'); } this.config = { checkTimeout, openAppWithDeeplink, openUrlWhenWalletNotFound, }; this._connecting = false; this._wallet = null; this._address = null; if (isInMobileBrowser() && supportTokenPocket()) { this._readyState = WalletReadyState.Found; this._updateWallet(); } else { this._checkWallet().then(() => { if (this.connected) { this.emit('connect', this.address || ''); } }); } } get address() { return this._address; } get state() { return this._state; } get readyState() { return this._readyState; } get connecting() { return this._connecting; } /** * Get network information. * @returns {Network} Current network information. */ async network(): Promise { try { await this._checkWallet(); if (this.state !== AdapterState.Connected) throw new WalletDisconnectedError(); const wallet = this._wallet; if (!wallet || !wallet.tronWeb) throw new WalletDisconnectedError(); try { return await getNetworkInfoByTronWeb(wallet.tronWeb); } catch (e: any) { throw new WalletGetNetworkError(e?.message, e); } } catch (e: any) { this.emit('error', e); throw e; } } async connect(): Promise { try { this.checkIfOpenApp(); if (this.connected || this.connecting) return; await this._checkWallet(); if (this.readyState === WalletReadyState.NotFound) { if (this.config.openUrlWhenWalletNotFound !== false && isInBrowser()) { window.open(this.url, '_blank'); } throw new WalletNotFoundError(); } if (!this._wallet) return; this._connecting = true; const wallet = this._wallet as TokenPocketWallet; try { const res = await wallet.tron.request({ method: 'eth_requestAccounts' }); if (!res?.[0]) { throw new WalletConnectionError('Request connect error.'); } const address = res[0]; this.setAddress(address); this.setState(AdapterState.Connected); this.emit('connect', this.address || ''); } catch (e: any) { if (e instanceof WalletError) { throw e; } else { throw new WalletConnectionError(e?.message, e); } } } catch (error: any) { this.emit('error', error); throw error; } finally { this._connecting = false; } } async disconnect(): Promise { if (this.state !== AdapterState.Connected) { return; } this.setAddress(null); this.setState(AdapterState.Disconnect); this.emit('disconnect'); } async signTransaction(transaction: Transaction): Promise { try { const wallet = await this.checkAndGetWallet(); try { return await wallet.tronWeb.trx.sign(transaction); } catch (error: any) { if (error instanceof Error || (typeof error === 'object' && error.message)) { throw new WalletSignTransactionError(error.message, error); } else if (typeof error === 'string') { throw new WalletSignTransactionError(error, new Error(error)); } else { throw new WalletSignTransactionError('Unknown error', error); } } } catch (error: any) { this.emit('error', error); throw error; } } async multiSign(transaction: Transaction, options: { permissionId?: number } = {}): Promise { try { const wallet = await this.checkAndGetWallet(); try { return await wallet.tronWeb.trx.multiSign(transaction, undefined, options.permissionId); } catch (error: any) { if (error instanceof Error || (typeof error === 'object' && error.message)) { throw new WalletSignTransactionError(error.message, error); } else if (typeof error === 'string') { throw new WalletSignTransactionError(error, new Error(error)); } else { throw new WalletSignTransactionError('Unknown error', error); } } } catch (error: any) { this.emit('error', error); throw error; } } async signMessage(message: string): Promise { try { const wallet = await this.checkAndGetWallet(); try { return await wallet.tronWeb.trx.signMessageV2(message); } catch (error: any) { if (error instanceof Error || (typeof error === 'object' && error.message)) { throw new WalletSignMessageError(error.message, error); } else if (typeof error === 'string') { throw new WalletSignMessageError(error, new Error(error)); } else { throw new WalletSignMessageError('Unknown error', error); } } } catch (error: any) { this.emit('error', error); throw error; } } private onAccountsChanged: TronAccountsChangedCallback = (accounts) => { const preAddr = this.address || ''; const curAddr = accounts?.[0] || ''; if (!curAddr) { this.setAddress(null); this.setState(AdapterState.Disconnect); } else { const address = curAddr as string; this.setAddress(address); this.setState(AdapterState.Connected); } this.emit('accountsChanged', this.address || '', preAddr); if (!preAddr && this.address) { this.emit('connect', this.address); } else if (preAddr && !this.address) { this.emit('disconnect'); } }; private listenTronEvent() { if (isInMobileBrowser()) { return; } this.stopListenTronEvent(); const wallet = this._wallet; if (!wallet || !wallet.tron) return; wallet.tron.on('accountsChanged', this.onAccountsChanged); } private stopListenTronEvent() { if (isInMobileBrowser()) { return; } const wallet = this._wallet; if (!wallet || !wallet.tron) return; wallet.tron.removeListener('accountsChanged', this.onAccountsChanged); } private async checkAndGetWallet() { this.checkIfOpenApp(); await this._checkWallet(); if (!this.connected) throw new WalletDisconnectedError(); const wallet = this._wallet; if (!wallet || !wallet.tronWeb) throw new WalletDisconnectedError(); return wallet as TokenPocketWallet; } private checkIfOpenApp() { if (this.config.openAppWithDeeplink === false) { return; } if (openTokenPocket()) { throw new WalletNotFoundError(); } } private checkReadyInterval: ReturnType | null = null; private checkForWalletReady() { if (this.checkReadyInterval) { return; } let times = 0; const maxTimes = Math.floor(this.config.checkTimeout / 200); const check = () => { if (isInMobileBrowser() && window.tronWeb?.ready) { this.checkReadyInterval && clearInterval(this.checkReadyInterval); this.checkReadyInterval = null; this._updateWallet(); this.emit('connect', this.address || ''); } else if (this._wallet?.tronWeb?.defaultAddress?.base58) { this.checkReadyInterval && clearInterval(this.checkReadyInterval); this.checkReadyInterval = null; this._wallet.ready = true; const address = this._wallet.tronWeb.defaultAddress.base58; const state = address ? AdapterState.Connected : AdapterState.Disconnect; this.setAddress(address); this.setState(state); this.emit('connect', this.address || ''); } else if (times > maxTimes) { this.checkReadyInterval && clearInterval(this.checkReadyInterval); this.checkReadyInterval = null; } else { times++; } }; this.checkReadyInterval = setInterval(check, 200); } private _checkPromise: Promise | null = null; /** * check if wallet exists by interval, the promise only resolve when wallet detected or timeout * @returns if wallet exists */ private _checkWallet(): Promise { if (this.readyState === WalletReadyState.Found) { return Promise.resolve(true); } if (this._checkPromise) { return this._checkPromise; } if (isInBrowser() && !isInMobileBrowser()) { this._checkPromise = new Promise((resolve) => { const timer = setTimeout(() => { window.removeEventListener(TIP6963AnnounceProviderEventName, handler); this._updateWallet(); if (supportTokenPocket()) { this._readyState = WalletReadyState.Found; resolve(true); } else { this._readyState = WalletReadyState.NotFound; resolve(false); } this.emit('readyStateChanged', this._readyState); }, this.config.checkTimeout); const handler = (event: TIP6963AnnounceProviderEvent) => { const { info, provider } = event.detail; if (info.name === 'TokenPocket') { this._wallet = { ready: !!provider.tronWeb?.defaultAddress?.base58, tron: provider as unknown as Tron, tronWeb: provider.tronWeb, }; this.listenTronEvent(); this._readyState = WalletReadyState.Found; const address = this._wallet.tronWeb.defaultAddress?.base58 || null; const state = address ? AdapterState.Connected : AdapterState.Disconnect; this.setState(state); this.setAddress(address); this.emit('readyStateChanged', this.readyState); window.removeEventListener(TIP6963AnnounceProviderEventName, handler); clearTimeout(timer); resolve(true); if (!this.connected) { this.checkForWalletReady(); } } }; window.addEventListener(TIP6963AnnounceProviderEventName, handler); window.dispatchEvent(new Event(TIP6963RequestProviderEventName)); }); return this._checkPromise; } // Support TIP-6963 with wallet extension const interval = 100; const maxTimes = Math.floor(this.config.checkTimeout / interval); let times = 0, timer: ReturnType; this._checkPromise = new Promise((resolve) => { const check = () => { times++; const isSupport = supportTokenPocket(); if (isSupport || times > maxTimes) { timer && clearInterval(timer); this._readyState = isSupport ? WalletReadyState.Found : WalletReadyState.NotFound; this._updateWallet(); this.emit('readyStateChanged', this.readyState); resolve(isSupport); } }; timer = setInterval(check, interval); check(); }); return this._checkPromise; } private _updateWallet = () => { let state = this.state; let address = this.address; if (supportTokenPocket()) { const tron = window.tokenpocket?.tron as Tron; this._wallet = isInMobileBrowser() ? ({ tron, ready: window.tronWeb?.ready, tronWeb: window.tokenpocket?.tronWeb, } as TokenPocketWallet) : { tron, ready: !!(tron?.tronWeb as TronWeb).defaultAddress?.base58 || false, tronWeb: tron?.tronWeb as TronWeb, }; address = this._wallet.tronWeb.defaultAddress?.base58 || null; state = address ? AdapterState.Connected : AdapterState.Disconnect; if (!address) { this.checkForWalletReady(); } } else { this._wallet = null; address = null; state = AdapterState.NotFound; } this.setAddress(address); this.setState(state); }; private setAddress(address: string | null) { this._address = address; } private setState(state: AdapterState) { const preState = this.state; if (state !== preState) { this._state = state; this.emit('stateChanged', state); } } }