UNPKG

@tronweb3/tronwallet-adapter-imtoken

Version:

Wallet adapter for imToken Wallet Android and IOS app.

320 lines (298 loc) 13.3 kB
import { Adapter, AdapterState, isInBrowser, WalletReadyState, WalletSignMessageError, WalletNotFoundError, WalletDisconnectedError, WalletSignTransactionError, WalletGetNetworkError, } from '@tronweb3/tronwallet-abstract-adapter'; import { getNetworkInfoByTronWeb } from '@tronweb3/tronwallet-adapter-tronlink'; import type { TronLinkWallet } from '@tronweb3/tronwallet-adapter-tronlink'; import type { Transaction, SignedTransaction, AdapterName, BaseAdapterConfig, Network, } from '@tronweb3/tronwallet-abstract-adapter'; import { openImTokenApp, supportImToken } from './utils.js'; export interface ImTokenAdapterConfig extends BaseAdapterConfig { /** * Timeout in millisecond for checking if imToken Wallet is supported. * Default is 2 * 1000ms */ checkTimeout?: number; /** * Set if open imToken Wallet app using DeepLink. * Default is true. */ openAppWithDeeplink?: boolean; } export const ImTokenWalletAdapterName = 'imToken Wallet' as AdapterName<'imToken Wallet'>; export class ImTokenAdapter extends Adapter { name = ImTokenWalletAdapterName; url = 'https://token.im/'; // prettier-ignore icon = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEzIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMyA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF84OTNfMjU5OSkiPgo8bWFzayBpZD0ibWFzazBfODkzXzI1OTkiIHN0eWxlPSJtYXNrLXR5cGU6bHVtaW5hbmNlIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSIwIiB5PSIwIiB3aWR0aD0iNTEzIiBoZWlnaHQ9IjUxMiI+CjxwYXRoIGQ9Ik01MTIuMzE5IDBIMC4zMTkzMzZWNTEySDUxMi4zMTlWMFoiIGZpbGw9IndoaXRlIi8+CjwvbWFzaz4KPGcgbWFzaz0idXJsKCNtYXNrMF84OTNfMjU5OSkiPgo8cGF0aCBkPSJNMzk3Ljc0NiAwSDExNS43NDZDNTIuMjMzMyAwIDAuNzQ2MDk0IDUxLjQ4NzMgMC43NDYwOTQgMTE1VjM5N0MwLjc0NjA5NCA0NjAuNTEzIDUyLjIzMzMgNTEyIDExNS43NDYgNTEySDM5Ny43NDZDNDYxLjI1OSA1MTIgNTEyLjc0NiA0NjAuNTEzIDUxMi43NDYgMzk3VjExNUM1MTIuNzQ2IDUxLjQ4NzMgNDYxLjI1OSAwIDM5Ny43NDYgMFoiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl84OTNfMjU5OSkiLz4KPHBhdGggZD0iTTQxNy40MzYgMTU4LjI5MUM0MjguMDg0IDMwMi41MDcgMzM1LjM4MiAzNzAuNjcgMjUyLjI3NyAzNzcuOTM5QzE3NS4wMTQgMzg0LjY5NiAxMDIuMjg3IDMzNy4yMjEgOTUuOTA2NyAyNjQuMjc5QzkwLjY0MzYgMjA0LjAxNyAxMjcuODg5IDE3OC4zNjEgMTU3LjE1MiAxNzUuODA0QzE4Ny4yNDkgMTczLjE2NSAyMTIuNTQyIDE5My45MjEgMjE0LjczNiAyMTkuMDUyQzIxNi44NDkgMjQzLjIxMyAyMDEuNzczIDI1NC4yMTEgMTkxLjI4OCAyNTUuMTI2QzE4Mi45OTYgMjU1Ljg1MyAxNzIuNTY0IDI1MC44MTkgMTcxLjYyMiAyNDAuMDFDMTcwLjgxNCAyMzAuNzIyIDE3NC4zNDEgMjI5LjQ1NyAxNzMuNDc5IDIxOS41OUMxNzEuOTQ1IDIwMi4wMjQgMTU2LjYyNyAxOTkuOTc4IDE0OC4yNDEgMjAwLjcwNUMxMzguMDkyIDIwMS41OTQgMTE5LjY3OCAyMTMuNDM5IDEyMi4yNjIgMjQyLjk0NEMxMjQuODYgMjcyLjcwNSAxNTMuMzk2IDI5Ni4yMjEgMTkwLjgwMyAyOTIuOTVDMjMxLjE3MSAyODkuNDIzIDI1OS4yNzYgMjU3Ljk5MyAyNjEuMzkgMjEzLjkxQzI2MS4zNyAyMTEuNTc1IDI2MS44NjIgMjA5LjI2NCAyNjIuODMgMjA3LjEzOUwyNjIuODQzIDIwNy4wODZDMjYzLjI3OCAyMDYuMTYyIDI2My43ODcgMjA1LjI3NSAyNjQuMzY0IDIwNC40MzRDMjY1LjIyNiAyMDMuMTQyIDI2Ni4zMyAyMDEuNzE1IDI2Ny43NTYgMjAwLjE1M0MyNjcuNzcgMjAwLjExMyAyNjcuNzcgMjAwLjExMyAyNjcuNzk3IDIwMC4xMTNDMjY4LjgzMyAxOTguOTQyIDI3MC4wODUgMTk3LjY3NyAyNzEuNDk4IDE5Ni4zMTdDMjg5LjEzMiAxNzkuNjggMzUyLjYzOCAxNDAuNDQzIDQxMi42OTggMTUyLjg2N0M0MTMuOTY4IDE1My4xMzkgNDE1LjExNSAxNTMuODE0IDQxNS45NjkgMTU0Ljc5MkM0MTYuODIzIDE1NS43NyA0MTcuMzM3IDE1Ni45OTcgNDE3LjQzNiAxNTguMjkxWiIgZmlsbD0id2hpdGUiLz4KPC9nPgo8L2c+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfODkzXzI1OTkiIHgxPSI1MTIuNDQiIHkxPSI4Ni41MTkiIHgyPSIxOC4wMjE5IiB5Mj0iMjc5LjUwMiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjMENDNUZGIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzAwN0ZGRiIvPgo8L2xpbmVhckdyYWRpZW50Pgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzg5M18yNTk5Ij4KPHJlY3Qgd2lkdGg9IjUxMyIgaGVpZ2h0PSI1MTIiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg=='; config: Required<ImTokenAdapterConfig>; private _readyState: WalletReadyState = WalletReadyState.Loading; private _state: AdapterState = AdapterState.Loading; private _connecting: boolean; private _wallet: TronLinkWallet | null; private _address: string | null; constructor(config: ImTokenAdapterConfig = {}) { super(); const { checkTimeout = 2 * 1000, openUrlWhenWalletNotFound = true, openAppWithDeeplink = true } = config; if (typeof checkTimeout !== 'number') { throw new Error('[ImTokenAdapter] config.checkTimeout should be a number'); } this.config = { checkTimeout, openUrlWhenWalletNotFound, openAppWithDeeplink, }; this._connecting = false; this._wallet = null; this._address = null; if (!isInBrowser()) { this._readyState = WalletReadyState.NotFound; this.setState(AdapterState.NotFound); return; } if (supportImToken()) { 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<Network> { 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<void> { 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(); } const wallet = this._wallet as TronLinkWallet; const address = wallet.tronWeb.defaultAddress?.base58 || ''; this.setAddress(address); this.setState(AdapterState.Connected); this.emit('connect', this.address || ''); } catch (error: any) { this.emit('error', error); throw error; } finally { this._connecting = false; } } async disconnect(): Promise<void> { if (this.state !== AdapterState.Connected) { return; } this.setAddress(null); this.setState(AdapterState.Disconnect); this.emit('disconnect'); } async signTransaction(transaction: Transaction, privateKey?: string): Promise<SignedTransaction> { try { const wallet = await this.checkAndGetWallet(); try { return await wallet.tronWeb.trx.sign(transaction, privateKey); } catch (error: any) { if (error instanceof Error) { throw new WalletSignTransactionError(error.message, error); } else { throw new WalletSignTransactionError(error, new Error(error)); } } } catch (error: any) { this.emit('error', error); throw error; } } async multiSign( transaction: Transaction, privateKey?: string | false, permissionId?: number ): Promise<SignedTransaction> { try { const wallet = await this.checkAndGetWallet(); try { return await wallet.tronWeb.trx.multiSign(transaction, privateKey, permissionId); } catch (error: any) { if (error instanceof Error) { throw new WalletSignTransactionError(error.message, error); } else { throw new WalletSignTransactionError(error, new Error(error)); } } } catch (error: any) { this.emit('error', error); throw error; } } async signMessage(message: string, privateKey?: string): Promise<string> { try { const wallet = await this.checkAndGetWallet(); try { return await wallet.tronWeb.trx.signMessageV2(message, privateKey); } catch (error: any) { if (error instanceof Error) { throw new WalletSignMessageError(error.message, error); } else { throw new WalletSignMessageError(error, new Error(error)); } } } catch (error: any) { this.emit('error', error); throw error; } } 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 TronLinkWallet; } private checkReadyInterval: ReturnType<typeof setInterval> | null = null; private checkForWalletReady() { if (this.checkReadyInterval) { return; } let times = 0; const maxTimes = Math.floor(this.config.checkTimeout / 200); const check = async () => { if (this._wallet && this._wallet.ready) { this.checkReadyInterval && clearInterval(this.checkReadyInterval); this.checkReadyInterval = null; await this._updateWallet(); 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<boolean> | null = null; /** * check if wallet exists by interval, the promise only resolve when wallet detected or timeout * @returns if wallet exists */ private _checkWallet(): Promise<boolean> { if (this.readyState === WalletReadyState.Found) { return Promise.resolve(true); } if (this._checkPromise) { return this._checkPromise; } const interval = 100; const maxTimes = Math.floor(this.config.checkTimeout / interval); let times = 0, timer: ReturnType<typeof setInterval>; this._checkPromise = new Promise((resolve) => { const check = () => { times++; const isSupport = supportImToken(); 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 checkIfOpenApp() { if (this.config.openAppWithDeeplink === false) { return; } if (openImTokenApp()) { throw new WalletNotFoundError(); } } private _updateWallet = async () => { let state = this.state; let address = this.address; if (supportImToken()) { this._wallet = { ready: window.tronWeb?.ready || false, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion tronWeb: window.tronWeb!, request: () => Promise.resolve(null), }; address = this._wallet.tronWeb.defaultAddress?.base58 || null; state = this._wallet.ready ? AdapterState.Connected : AdapterState.Disconnect; if (!this._wallet.ready) { 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); } } }