From abf7d5abd08da94e4855ff0cdafba2a4c713ebf1 Mon Sep 17 00:00:00 2001 From: Hugo Dias Date: Mon, 30 Dec 2024 14:54:22 +0000 Subject: [PATCH] feat: add filforwarder transaction insights --- packages/snap/snap.manifest.json | 6 +- packages/snap/src/components/error.tsx | 35 ++++--- packages/snap/src/components/insights.tsx | 53 ++++++++++ packages/snap/src/index.tsx | 8 +- packages/snap/src/svg/error.svg | 1 + packages/snap/src/transaction-insight.ts | 119 ---------------------- packages/snap/src/transaction-insight.tsx | 115 +++++++++++++++++++++ 7 files changed, 198 insertions(+), 139 deletions(-) create mode 100644 packages/snap/src/components/insights.tsx create mode 100644 packages/snap/src/svg/error.svg delete mode 100644 packages/snap/src/transaction-insight.ts create mode 100644 packages/snap/src/transaction-insight.tsx diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index 1961aede..76633187 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/filecoin-project/filsnap.git" }, "source": { - "shasum": "wf1YV/Pkw0eO4py8+gkxF+6l1CtEzMW9BjJUS1K137s=", + "shasum": "zM6X5ZlF8x7w0kFoPr6nY85isY21RDnHvjjPVNBy5so=", "location": { "npm": { "filePath": "dist/snap.js", @@ -25,6 +25,9 @@ "endowment:page-home": {}, "endowment:lifecycle-hooks": {}, "endowment:network-access": {}, + "endowment:transaction-insight": { + "allowTransactionOrigin": true + }, "endowment:rpc": { "dapps": true }, @@ -39,5 +42,6 @@ ], "snap_manageState": {} }, + "platformVersion": "6.14.0", "manifestVersion": "0.1" } diff --git a/packages/snap/src/components/error.tsx b/packages/snap/src/components/error.tsx index 3ef4c383..ca5c2067 100644 --- a/packages/snap/src/components/error.tsx +++ b/packages/snap/src/components/error.tsx @@ -1,21 +1,28 @@ -import { - Box, - Heading, - Row, - type SnapComponent, - Text, -} from '@metamask/snaps-sdk/jsx' - +import { Box, Image, type SnapComponent, Text } from '@metamask/snaps-sdk/jsx' +import iconError from '../svg/error.svg' type ErrorBoxProps = { - error: string + name: string + message?: string } -export const ErrorBox: SnapComponent = ({ error }) => { +export const ErrorBox: SnapComponent = ({ name, message }) => { + if (message != null) { + return ( + + + + {name} + + {message} + + ) + } + return ( - Error - - {error} - + + + {name} + ) } diff --git a/packages/snap/src/components/insights.tsx b/packages/snap/src/components/insights.tsx new file mode 100644 index 00000000..2bb02b8b --- /dev/null +++ b/packages/snap/src/components/insights.tsx @@ -0,0 +1,53 @@ +import { + Bold, + Box, + Copyable, + Divider, + Heading, + Icon, + Link, + type SnapComponent, + Text, +} from '@metamask/snaps-sdk/jsx' +import type { SnapConfig } from '../types' +import { formatFIL } from '../utils' + +type ErrorBoxProps = { + config: SnapConfig + address: string + amount: string +} +export const Insights: SnapComponent = ({ + amount, + config, + address, +}) => { + return ( + + FilForwarder Transaction + + The FilForwarder smart contract enables FEVM users to send their FIL + safely and securely to other addresses in the Filecoin ecosystem. Review + the source{' '} + and onchain{' '} + + deployment + {' '} + for more information. + + + + + Recipient + + + + + + + Amount + + {formatFIL(amount, config)} + + ) +} diff --git a/packages/snap/src/index.tsx b/packages/snap/src/index.tsx index 3cdac596..23880165 100644 --- a/packages/snap/src/index.tsx +++ b/packages/snap/src/index.tsx @@ -41,7 +41,7 @@ export type { } from './types' // Disable transaction insight for now -// export { onTransaction } from './transaction-insight' +export { onTransaction } from './transaction-insight' export const onRpcRequest: OnRpcRequestHandler = async ({ origin, @@ -164,7 +164,7 @@ export const onHomePage: OnHomePageHandler = async () => { if (config === undefined) { return { - content: , + content: , } } @@ -183,9 +183,7 @@ export const onHomePage: OnHomePageHandler = async () => { const balance = await rpc.balance(account.address.toString()) if (balance.error != null) { return { - content: ( - - ), + content: , } } return { diff --git a/packages/snap/src/svg/error.svg b/packages/snap/src/svg/error.svg new file mode 100644 index 00000000..1fe5ff25 --- /dev/null +++ b/packages/snap/src/svg/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/snap/src/transaction-insight.ts b/packages/snap/src/transaction-insight.ts deleted file mode 100644 index 758f0c62..00000000 --- a/packages/snap/src/transaction-insight.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { - OnTransactionHandler, - OnTransactionResponse, -} from '@metamask/snaps-sdk' -import { heading, panel, text } from '@metamask/snaps-sdk' -import * as Address from 'iso-filecoin/address' -import { Token } from 'iso-filecoin/token' -import { type Hex, fromHex } from 'viem' -import { decodeFunctionData } from 'viem' -import { filForwarderMetadata } from './filforwarder' - -/** - * Invalid transfer message - * - * @param message - The message to display - */ -function invalidTransferMessage(message: string): OnTransactionResponse { - return { - content: panel([heading('Invalid FIL Transfer'), text(message)]), - } -} - -/** - * Human readable network - * - * @param chainId - The chain ID - */ -function humanReadableNetwork(chainId: string): string { - if (chainId === filForwarderMetadata.chainIds.filecoinMainnet) { - return 'Filecoin Mainnet' - } - if (chainId === filForwarderMetadata.chainIds.filecoinCalibrationTestnet) { - return 'Filecoin Calibration Testnet' - } - throw new Error(`Unknown chain ID: ${chainId}`) -} - -/** - * Chain matches - * - * @param chainId - The chain ID - */ -function chainMatches(chainId: string): boolean { - return Object.values(filForwarderMetadata.chainIds).includes(chainId) -} - -/** - * Contract address matches - * - * @param transactionTo - The transaction to address - */ -function contractAddressMatches(transactionTo: string | undefined): boolean { - return ( - transactionTo?.toLowerCase() === - filForwarderMetadata.contractAddress.toLowerCase() - ) -} - -// Note: currently MetaMask shows the transaction insight tab by default even if we don't display any information -// in it. This is a bug that the MetaMask team is addressing in this PR: -// https://github.com/MetaMask/metamask-extension/pull/20267 -export const onTransaction: OnTransactionHandler = async ({ - transaction, - chainId, -}): Promise => { - if ( - !chainMatches(chainId) || - !contractAddressMatches(transaction.to as string | undefined) - ) { - // Don't show any insights if the transaction is not a FIL transfer. - return null - } - - let transferAmount: Token - try { - transferAmount = new Token(fromHex(transaction.value as Hex, 'bigint')) - } catch (error) { - console.error(error) - return invalidTransferMessage( - 'Transfer amount is missing from the transaction.' - ) - } - - let recipient: Address.IAddress - try { - const callData = decodeFunctionData({ - abi: filForwarderMetadata.abi, - data: transaction.data as Hex, - }) - if (callData.functionName !== 'forward') { - return invalidTransferMessage('Transaction tries to call wrong method.') - } - if (callData.args === undefined || callData.args.length !== 1) { - return invalidTransferMessage('Missing recipient in transaction.') - } - - const isMainNet = chainId === filForwarderMetadata.chainIds.filecoinMainnet - recipient = Address.fromContractDestination( - callData.args[0] as Hex, - isMainNet ? 'mainnet' : 'testnet' - ) - } catch (error) { - console.error(error) - return invalidTransferMessage('Transaction recipient is invalid.') - } - - return { - content: panel([ - heading('Transfer FIL'), - text( - `You are transferring ${transferAmount - .toFIL() - .toString()} FIL to ${recipient.toString()} on ${humanReadableNetwork( - chainId - )}` - ), - ]), - } -} diff --git a/packages/snap/src/transaction-insight.tsx b/packages/snap/src/transaction-insight.tsx new file mode 100644 index 00000000..7e4bdaea --- /dev/null +++ b/packages/snap/src/transaction-insight.tsx @@ -0,0 +1,115 @@ +import type { + OnTransactionHandler, + OnTransactionResponse, +} from '@metamask/snaps-sdk' +import * as Address from 'iso-filecoin/address' +import { Token } from 'iso-filecoin/token' +import { type Hex, fromHex } from 'viem' +import { decodeFunctionData } from 'viem' +import { ErrorBox } from './components/error' +import { Insights } from './components/insights' +import { filForwarderMetadata } from './filforwarder' +import { State } from './state' + +/** + * Chain matches + * + * @param chainId - The chain ID + */ +function chainMatches(chainId: string): boolean { + return Object.values(filForwarderMetadata.chainIds).includes(chainId) +} + +/** + * Contract address matches + * + * @param transactionTo - The transaction to address + */ +function contractAddressMatches(transactionTo: string | undefined): boolean { + return ( + transactionTo?.toLowerCase() === + filForwarderMetadata.contractAddress.toLowerCase() + ) +} + +export const onTransaction: OnTransactionHandler = async ({ + transaction, + chainId, + transactionOrigin, +}): Promise => { + if (!transactionOrigin) { + return { + content: ( + + ), + } + } + const state = new State(snap) + const config = await state.get(transactionOrigin) + if (!config) { + return { + content: , + } + } + + // Don't show any insights if the transaction is not a FIL transfer. + if ( + !chainMatches(chainId) || + !contractAddressMatches(transaction.to as string | undefined) + ) { + return null + } + + try { + if (!transaction.value) { + return { + content: ( + + ), + } + } + + const callData = decodeFunctionData({ + abi: filForwarderMetadata.abi, + data: transaction.data as Hex, + }) + + if (callData.functionName !== 'forward') { + return { + content: , + } + } + if (callData.args === undefined || callData.args.length !== 1) { + return { + content: , + } + } + + const isMainNet = chainId === filForwarderMetadata.chainIds.filecoinMainnet + const transferAmount = Token.fromFIL( + fromHex(transaction.value as Hex, 'bigint') + ) + const recipient = Address.fromContractDestination( + callData.args[0] as Hex, + isMainNet ? 'mainnet' : 'testnet' + ) + return { + content: ( + + ), + } + } catch (error) { + const err = error as Error + console.error(error) + return { + content: , + } + } +}