import { ChainsService } from "../chains"; import { simpleFetch } from "@keplr-wallet/simple-fetch"; import { Notification } from "../tx/types"; import { EthTxReceipt } from "@keplr-wallet/types"; import { retry } from "@keplr-wallet/common"; export class BackgroundTxEthereumService { constructor( protected readonly chainsService: ChainsService, protected readonly notification: Notification ) {} async init(): Promise { // noop } async sendEthereumTx( origin: string, chainId: string, tx: Uint8Array, options: { silent?: boolean; skipTracingTxResult?: boolean; onFulfill?: (txReceipt: EthTxReceipt) => void; } ): Promise { if (!options.silent) { this.notification.create({ iconRelativeUrl: "assets/logo-256.png", title: "Tx is pending...", message: "Wait a second", }); } try { const evmInfo = this.chainsService.getEVMInfoOrThrow(chainId); const sendRawTransactionResponse = await simpleFetch<{ result?: string; error?: Error; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion }>(evmInfo.rpc, "", { method: "POST", headers: { "content-type": "application/json", "request-source": origin, }, body: JSON.stringify({ jsonrpc: "2.0", method: "eth_sendRawTransaction", params: [`0x${Buffer.from(tx).toString("hex")}`], id: 1, }), }); const txHash = sendRawTransactionResponse.data.result; if (sendRawTransactionResponse.data.error || !txHash) { throw ( sendRawTransactionResponse.data.error ?? new Error("No tx hash responded") ); } if (options.skipTracingTxResult) { return txHash; } retry( () => { return new Promise(async (resolve, reject) => { const txReceiptResponse = await simpleFetch<{ result: EthTxReceipt | null; error?: Error; }>(evmInfo.rpc, { method: "POST", headers: { "content-type": "application/json", "request-source": origin, }, body: JSON.stringify({ jsonrpc: "2.0", method: "eth_getTransactionReceipt", params: [txHash], id: 1, }), }); if (txReceiptResponse.data.error) { console.error(txReceiptResponse.data.error); resolve(); } const txReceipt = txReceiptResponse.data.result; if (txReceipt) { options?.onFulfill?.(txReceipt); BackgroundTxEthereumService.processTxResultNotification( this.notification ); resolve(); } reject(new Error("Failed to get transaction receipt")); }); }, { maxRetries: 50, waitMsAfterError: 500, maxWaitMsAfterError: 15000, } ); return txHash; } catch (e) { console.error(e); if (!options.silent) { BackgroundTxEthereumService.processTxErrorNotification( this.notification, e ); } throw e; } } async getEthereumTxReceipt( origin: string, chainId: string, txHash: string ): Promise { const modularChainInfo = this.chainsService.getModularChainInfoOrThrow(chainId); if ( modularChainInfo.type !== "evm" && modularChainInfo.type !== "ethermint" ) { return null; } const evmInfo = modularChainInfo.evm; return await retry( () => { return new Promise(async (resolve, reject) => { const txReceiptResponse = await simpleFetch<{ result: EthTxReceipt | null; error?: Error; }>(evmInfo.rpc, { method: "POST", headers: { "content-type": "application/json", "request-source": origin, }, body: JSON.stringify({ jsonrpc: "2.0", method: "eth_getTransactionReceipt", params: [txHash], id: 1, }), }); if (txReceiptResponse.data.error) { console.error(txReceiptResponse.data.error); resolve(null); } const txReceipt = txReceiptResponse.data.result; if (txReceipt) { resolve(txReceipt); } reject(new Error("No tx receipt responded")); }); }, { maxRetries: 50, waitMsAfterError: 500, maxWaitMsAfterError: 15000, } ); } private static processTxResultNotification(notification: Notification): void { try { notification.create({ iconRelativeUrl: "assets/logo-256.png", title: "Tx succeeds", // TODO: Let users know the tx id? message: "Congratulations!", }); } catch (e) { BackgroundTxEthereumService.processTxErrorNotification(notification, e); } } private static processTxErrorNotification( notification: Notification, e: Error ): void { const message = e.message; notification.create({ iconRelativeUrl: "assets/logo-256.png", title: "Tx failed", message, }); } }