/ src / frontend / utils / transaction.ts
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  };