transaction.ts
1 import { ethers } from 'ethers'; 2 import { SDKProvider } from '@metamask/sdk'; 3 import { TransferAction, ActionParams } from './types'; 4 5 export const isActionInitiated = (action: ActionParams) => { 6 return !(Object.keys(action).length === 0); 7 }; 8 9 export const buildAction = (action: ActionParams, account: string, gasPrice: string) => { 10 const transactionType = action.type.toLowerCase(); 11 12 let tx: TransferAction; 13 14 switch (transactionType) { 15 case 'transfer': 16 tx = buildTransferTransaction(action, account, gasPrice); 17 break; 18 default: 19 throw Error(`Transaction of type ${transactionType} is not yet supported`); 20 } 21 22 // returned wrapped call with method for metamask with transaction params 23 return { 24 method: 'eth_sendTransaction', 25 params: [tx], 26 }; 27 }; 28 29 function extractEthereumAddress(text: string): string | null { 30 const regex = /0x[a-fA-F0-9]{40}/; 31 const match = text.match(regex); 32 return match ? match[0] : null; 33 } 34 35 const buildTransferTransaction = ( 36 action: ActionParams, 37 account: string, 38 gasPrice: any, 39 ): TransferAction => { 40 return { 41 from: account, 42 to: action.targetAddress, 43 gas: '0x76c0', //for more complex tasks estimate this from metamast 44 gasPrice: gasPrice, 45 value: '0x' + ethers.parseEther(action.ethAmount).toString(16), 46 data: '0x000000', 47 }; 48 }; 49 50 //TODO: take chain ID to get arb balance or w/e chain 51 const formatWalletBalance = (balanceWeiHex: string) => { 52 const balanceBigInt = BigInt(balanceWeiHex); 53 const balance = ethers.formatUnits(balanceBigInt, 'ether'); 54 55 return `${parseFloat(balance).toFixed(2)} ETH`; 56 }; 57 58 export const handleBalanceRequest = async ( 59 provider: SDKProvider | undefined, 60 account: string | undefined, 61 ) => { 62 const blockNumber = await provider?.request({ 63 method: 'eth_blockNumber', 64 params: [], 65 }); 66 67 const balanceWeiHex = await provider?.request({ 68 method: 'eth_getBalance', 69 params: [account, blockNumber], 70 }); 71 72 if (typeof balanceWeiHex === 'string') { 73 return `${formatWalletBalance(balanceWeiHex)}`; 74 } else { 75 console.error('Failed to retrieve a valid balance.'); 76 77 throw Error('Invalid Balance Received from MetaMask.'); 78 } 79 }; 80 81 const estimateGasWithOverHead = (estimatedGasMaybe: string) => { 82 const estimatedGas = parseInt(estimatedGasMaybe, 16); 83 const gasLimitWithOverhead = Math.ceil(estimatedGas * 2.5); 84 85 return `0x${gasLimitWithOverhead.toString(16)}`; 86 }; 87 88 export const handleTransactionRequest = async ( 89 provider: SDKProvider | undefined, 90 transaction: ActionParams, 91 account: string, 92 question: string, 93 ) => { 94 const addressInQuestion = extractEthereumAddress(question); 95 if (addressInQuestion?.toLowerCase() !== transaction.targetAddress.toLowerCase()) { 96 console.error( 97 `${addressInQuestion} !== ${transaction.targetAddress} target address did not match address in question`, 98 ); 99 throw new Error('Error, target address did not match address in question'); 100 } 101 102 const gasPrice = await provider?.request({ 103 method: 'eth_gasPrice', 104 params: [], 105 }); 106 107 // Sanity Check 108 if (typeof gasPrice !== 'string') { 109 console.error('Failed to retrieve a valid gasPrice'); 110 111 throw new Error('Invalid gasPrice received'); 112 } 113 114 const builtTx = buildAction(transaction, account, gasPrice); 115 116 const estimatedGas = await provider?.request({ 117 method: 'eth_estimateGas', 118 params: [builtTx], 119 }); 120 121 //Sanity Check 122 if (typeof estimatedGas !== 'string') { 123 console.error('Failed to estimate Gas with metamask'); 124 125 throw new Error('Invalid gasPrice received'); 126 } 127 128 const gasLimitWithOverhead = estimateGasWithOverHead(estimatedGas); 129 builtTx.params[0].gas = gasLimitWithOverhead; // Update the transaction with the new gas limit in hex 130 131 return builtTx; 132 };