import type { ConnectRequest, SignTransactionRequest, WebApp } from "./types"; import type { InjectionConfig } from "./types"; import { WalletStore } from "./store"; import type { Injected, InjectedAccount, InjectedExtension, InjectedMetadataKnown, InjectedWindowProvider } from "@polkadot/extension-inject/types"; import { encodeRequest } from "./encryption"; import { SignerPayloadRaw, SignerResult } from "@polkadot/types/types"; import { generateRequestId, parseSignedTxFromStartParam, parseWalletsFromStartParam } from "./helpers"; import { mockAccountsSubscribe } from "./constants"; 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 }: Required, ) { this.varanWalletUrl = isDev ? 'https://t.me/devReptileBot?startapp=' : 'https://t.me/varanwalletbot?startapp='; this.store = new WalletStore(); this.callbackUrl = callbackUrl; this.connectTimeout = connectTimeout; void this._startParamsCheck(); } get version(): string { return '1.0.0'; } private get signer(): InjectedExtension['signer'] { return { signRaw: (raw) => this.signRawTransaction(raw), }; } 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 get extensionAccounts(): InjectedExtension['accounts'] { return { get: () => this.accounts, subscribe: mockAccountsSubscribe }; } private get extensionMetadata(): InjectedExtension['metadata'] { const metadata = this.store.get('injectedMetadataKnown'); const setMetadata = async (metadata: InjectedMetadataKnown) => { await this.store.set('injectedMetadataKnown', metadata); return true; }; if (!metadata) return { get: () => Promise.resolve([]), provide: setMetadata, }; return { get: () => Promise.resolve([metadata]), provide: setMetadata, }; } 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 { const accounts = await this.accounts; if (!accounts.length) { 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: this.extensionAccounts, signer: this.signer, metadata: this.extensionMetadata, }); } async enable(): Promise { const accounts = await this.accounts; if (!accounts.length) { throw new Error('Please connect first'); } return { accounts: this.extensionAccounts, signer: this.signer, metadata: this.extensionMetadata, }; } }