import type { ConnectRequest, SignTransactionRequest, WebApp } from "./types"; import type { InjectionConfig } from "./types"; import { WalletStore } from "./store"; import type { InjectedAccount, InjectedExtension, InjectedWindowProvider } from "@polkadot/extension-inject/types"; import { encodeRequest } from "./encryption"; import { SignerPayloadRaw, SignerResult } from "@polkadot/types/types"; import { generateRequestId, parseSignedTxFromStartParam, parseWalletsFromStartParam } from "./helpers"; export class Wallet implements InjectedWindowProvider { private readonly varanWalletUrl!: string; private readonly store: WalletStore; private readonly name = 'Varan Wallet'; private readonly callbackUrl: string; private readonly connectTimeout: number; constructor( private readonly webApp: WebApp, { callbackUrl, isDev, connectTimeout, storage }: Required, ) { this.varanWalletUrl = isDev ? 'https://t.me/devReptileBot?startapp=' : 'https://t.me/varanwalletbot?startapp='; this.store = new WalletStore(storage); this.callbackUrl = callbackUrl; this.connectTimeout = connectTimeout; void this._startParamsCheck(); } get version(): string { return '1.0.0'; } private async _startParamsCheck(): Promise { let actions = 0; const newWalletAddresses = parseWalletsFromStartParam(this.webApp); const currentAccounts = await this.accounts; if (newWalletAddresses) { const allWallets = Array.from( new Set( currentAccounts .map(a => a.address) .concat(newWalletAddresses) ) ) this.store.set('accounts', allWallets); actions += 1; } const tx = parseSignedTxFromStartParam(this.webApp); if (tx) { const currentTransactions = this.store.get('transactions'); const pendingTransactionIndex = currentTransactions.findIndex(t => t.id === tx.id && t.status === 'signing'); if (pendingTransactionIndex === -1) return; currentTransactions[pendingTransactionIndex] = { ...currentTransactions[pendingTransactionIndex], status: tx.success ? 'success' : 'failed', signedTx: tx.signedTx ? tx.signedTx : '', }; this.store.set('transactions', currentTransactions); actions += 1; } if (actions) { this.webApp.close(); } } private formatAccount(account: string): InjectedAccount { return { address: account, name: account.slice(0, 6) + '...' + account.slice(-4), }; } get accounts(): Promise { const accounts = this.store.get('accounts'); return Promise.resolve(accounts.map(this.formatAccount)); } private async signRawTransaction(raw: SignerPayloadRaw): Promise { const accounts = await this.accounts; if (!accounts.length) throw new Error('No accounts found'); const address = accounts.find(a => a.address === raw.address); if (!address) throw new Error('Provided account was not found'); const request: SignTransactionRequest = { method: 'signTransaction', tx: raw.data, wallet: raw.address, callback: this.callbackUrl, id: generateRequestId(), }; const encodedRequest = encodeRequest(request); const botUrl = `${this.varanWalletUrl}${encodedRequest}`; this.store.set('transactions', [{ id: request.id, signedTx: '', status: 'signing', }]); this.webApp.openTelegramLink(botUrl); return new Promise((resolve) => { const id = this.store.subscribe('transactions', (transactions) => { const pendingTransaction = transactions.find(t => t.id === request.id && t.status === 'signing'); if (pendingTransaction) { this.store.unsubscribe('transactions', id); resolve({ id: pendingTransaction.id, signature: pendingTransaction.signedTx, } as SignerResult); } }); }); } private async _connect(): Promise { const request: ConnectRequest = { method: 'connect', callback: this.callbackUrl, }; const encodedRequest = encodeRequest(request); const botUrl = `${this.varanWalletUrl}${encodedRequest}`; await this.webApp.openTelegramLink(botUrl); return new Promise((resolve) => { const id = this.store.subscribe('accounts', (accounts) => { if (accounts.length) { this.store.unsubscribe('accounts', id); resolve(); } }); }); } async connect(): Promise { await Promise.race([ this._connect(), new Promise((_, reject) => { setTimeout(() => { reject(new Error('Connect timeout')); }, this.connectTimeout); }) ]); return Promise.resolve({ name: this.name, version: this.version, accounts: { get: () => this.accounts, subscribe: () => { return () => {}; }, }, signer: { signRaw: (raw) => this.signRawTransaction(raw), }, }); } }