From a8a3a1d17ef08259c11526cf36dee10b36c5915b Mon Sep 17 00:00:00 2001 From: "moxey.eth" Date: Sun, 3 Mar 2024 13:10:38 +1100 Subject: [PATCH] wip: spec updates --- playground/constants/abi.ts | 197 ++++++++++++++++++++++++++++++ playground/src/index.tsx | 233 ++++-------------------------------- src/components/Button.tsx | 10 +- src/frog-base.tsx | 2 +- src/routes/transaction.ts | 69 ++++++++--- src/types/transaction.ts | 97 ++++++++++----- 6 files changed, 353 insertions(+), 255 deletions(-) create mode 100644 playground/constants/abi.ts diff --git a/playground/constants/abi.ts b/playground/constants/abi.ts new file mode 100644 index 00000000..3d38d424 --- /dev/null +++ b/playground/constants/abi.ts @@ -0,0 +1,197 @@ +export const wagmiExampleAbi = [ + { inputs: [], stateMutability: 'nonpayable', type: 'constructor' }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'approved', + type: 'address', + }, + { + indexed: true, + internalType: 'uint256', + name: 'tokenId', + type: 'uint256', + }, + ], + name: 'Approval', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'operator', + type: 'address', + }, + { indexed: false, internalType: 'bool', name: 'approved', type: 'bool' }, + ], + name: 'ApprovalForAll', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'from', type: 'address' }, + { indexed: true, internalType: 'address', name: 'to', type: 'address' }, + { + indexed: true, + internalType: 'uint256', + name: 'tokenId', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + { + inputs: [ + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'approve', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'owner', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'tokenId', type: 'uint256' }], + name: 'getApproved', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'operator', type: 'address' }, + ], + name: 'isApprovedForAll', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'mint', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'tokenId', type: 'uint256' }], + name: 'mint', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'tokenId', type: 'uint256' }], + name: 'ownerOf', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'safeTransferFrom', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'bytes', name: '_data', type: 'bytes' }, + ], + name: 'safeTransferFrom', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'operator', type: 'address' }, + { internalType: 'bool', name: 'approved', type: 'bool' }, + ], + name: 'setApprovalForAll', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], + name: 'supportsInterface', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'tokenId', type: 'uint256' }], + name: 'tokenURI', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'from', type: 'address' }, + { internalType: 'address', name: 'to', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'transferFrom', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const diff --git a/playground/src/index.tsx b/playground/src/index.tsx index 677fd882..f039cb1d 100644 --- a/playground/src/index.tsx +++ b/playground/src/index.tsx @@ -1,5 +1,6 @@ import { Button, Frog, TextInput } from 'frog' +import { wagmiExampleAbi } from '../constants/abi.js' import { app as routingApp } from './routing.js' import { app as todoApp } from './todos.js' @@ -232,226 +233,44 @@ app.frame('/transaction', () => { ), intents: [ - - /tx - , - + /tx, + /tx-contract - , + , ], } }) -app.experimental_transaction('/tx', (c) => { +// Raw transaction +app.transaction('/tx', (c) => { return c.res({ - description: 'Rent 1 Farcaster storage unit to FID 3621', - to: '0x00000000fcCe7f938e7aE6D3c335bD6a1a7c593D', - data: '0x783a112b0000000000000000000000000000000000000000000000000000000000000e250000000000000000000000000000000000000000000000000000000000000001', - value: '984316556204476', - chainId: '10', + chainId: 'eip155:1', + method: 'eth_sendTransaction', + params: { + to: '0xd2135CfB216b74109775236E36d4b433F1DF507B', + value: 1n, + }, + }) +}) + +// Send transaction +app.transaction('/tx-send', (c) => { + return c.send({ + chainId: 'eip155:1', + to: '0xd2135CfB216b74109775236E36d4b433F1DF507B', + value: 1n, }) }) -app.experimental_transaction('/tx-contract', (c) => { +// Contract transaction +app.transaction('/tx-contract', (c) => { return c.contract({ - abi: erc20Abi, - functionName: 'transferFrom', - args: ['0x', '0x', 1n], - description: 'foo', + chainId: 'eip155:1', + abi: wagmiExampleAbi, + functionName: 'mint', to: '0x00000000fcCe7f938e7aE6D3c335bD6a1a7c593D', - value: '984316556204476', - chainId: '10', }) }) app.route('/todos', todoApp) app.route('/routing', routingApp) - -export const erc20Abi = [ - { - type: 'event', - name: 'Approval', - inputs: [ - { - indexed: true, - name: 'owner', - type: 'address', - }, - { - indexed: true, - name: 'spender', - type: 'address', - }, - { - indexed: false, - name: 'value', - type: 'uint256', - }, - ], - }, - { - type: 'event', - name: 'Transfer', - inputs: [ - { - indexed: true, - name: 'from', - type: 'address', - }, - { - indexed: true, - name: 'to', - type: 'address', - }, - { - indexed: false, - name: 'value', - type: 'uint256', - }, - ], - }, - { - type: 'function', - name: 'allowance', - stateMutability: 'view', - inputs: [ - { - name: 'owner', - type: 'address', - }, - { - name: 'spender', - type: 'address', - }, - ], - outputs: [ - { - type: 'uint256', - }, - ], - }, - { - type: 'function', - name: 'approve', - stateMutability: 'nonpayable', - inputs: [ - { - name: 'spender', - type: 'address', - }, - { - name: 'amount', - type: 'uint256', - }, - ], - outputs: [ - { - type: 'bool', - }, - ], - }, - { - type: 'function', - name: 'balanceOf', - stateMutability: 'view', - inputs: [ - { - name: 'account', - type: 'address', - }, - ], - outputs: [ - { - type: 'uint256', - }, - ], - }, - { - type: 'function', - name: 'decimals', - stateMutability: 'view', - inputs: [], - outputs: [ - { - type: 'uint8', - }, - ], - }, - { - type: 'function', - name: 'name', - stateMutability: 'view', - inputs: [], - outputs: [ - { - type: 'string', - }, - ], - }, - { - type: 'function', - name: 'symbol', - stateMutability: 'view', - inputs: [], - outputs: [ - { - type: 'string', - }, - ], - }, - { - type: 'function', - name: 'totalSupply', - stateMutability: 'view', - inputs: [], - outputs: [ - { - type: 'uint256', - }, - ], - }, - { - type: 'function', - name: 'transfer', - stateMutability: 'nonpayable', - inputs: [ - { - name: 'recipient', - type: 'address', - }, - { - name: 'amount', - type: 'uint256', - }, - ], - outputs: [ - { - type: 'bool', - }, - ], - }, - { - type: 'function', - name: 'transferFrom', - stateMutability: 'nonpayable', - inputs: [ - { - name: 'sender', - type: 'address', - }, - { - name: 'recipient', - type: 'address', - }, - { - name: 'amount', - type: 'uint256', - }, - ], - outputs: [ - { - type: 'bool', - }, - ], - }, -] as const diff --git a/src/components/Button.tsx b/src/components/Button.tsx index c40fdc1d..a94eb230 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -119,17 +119,17 @@ export function ButtonReset({ ) } -export type experimental_ButtonTransactionProps = ButtonProps & { +export type ButtonTransactionProps = ButtonProps & { location: string } -experimental_ButtonTransaction.__type = 'button' -export function experimental_ButtonTransaction({ +ButtonTransaction.__type = 'button' +export function ButtonTransaction({ children, // @ts-ignore - private index = 1, location, -}: experimental_ButtonTransactionProps) { +}: ButtonTransactionProps) { return [ ( context: TransactionContext, ) => TransactionResponse | Promise, ) { - this.hono.get(parsePath(path), async (c) => { + this.hono.post(parsePath(path), async (c) => { const transaction = await handler({ contract(parameters) { - const { abi, chainId, description, functionName, to, value, args } = - parameters + const { abi, chainId, functionName, to, args, value } = parameters + + const abiItem = getAbiItem({ + abi: abi, + name: functionName, + args, + } as GetAbiItemParameters) + // TODO: custom error + if (!abiItem) throw new Error('could not find abi item') + + const abiErrorItems = (abi as Abi).filter( + (item) => item.type === 'error', + ) + + return this.res({ + chainId, + method: 'eth_sendTransaction', + params: { + abi: [abiItem, ...abiErrorItems], + data: encodeFunctionData({ + abi, + args, + functionName, + } as EncodeFunctionDataParameters), + to, + value, + }, + }) + }, + req: c.req, + res(parameters) { + const { chainId, method, params } = parameters + const { abi, data, to, value } = params const response: TransactionResponse = { chainId, - description, - to, - value, + method, + params: { + abi, + data, + to, + }, } - response.data = encodeFunctionData({ - abi, - args, - functionName, - } as EncodeFunctionDataParameters) + if (value) response.params.value = value.toString() return response }, - req: c.req, - res(response) { - return response + send(parameters) { + return this.res({ + chainId: parameters.chainId, + method: 'eth_sendTransaction', + params: parameters, + }) }, }) return c.json(transaction) diff --git a/src/types/transaction.ts b/src/types/transaction.ts index b51e045f..522c59a4 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -1,34 +1,82 @@ import type { Context, Env } from 'hono' -import type { Abi, ContractFunctionArgs, ContractFunctionName } from 'viem' +import type { + Abi, + ContractFunctionArgs, + ContractFunctionName, + GetValue, + Hex, +} from 'viem' import type { UnionWiden, Widen } from './utils.js' export type TransactionContext = { - /** HTTP request object. */ + /** + * Contract transaction request. + * + * This is a convenience method for "contract transaction" requests as defined in the [Transaction Spec](https://www.notion.so/warpcast/Frame-Transactions-Public-Draft-v2-9d9f9f4f527249519a41bd8d16165f73?pvs=4#1b69c268f0684c978fbdf4d331ab8869), + * with a type-safe interface to infer types based on a provided `abi`. + */ + contract: ContractTransactionResponseFn + /** + * HTTP request object. + */ req: Context['req'] - /** Transaction response that includes properties such as: data, to, value, etc */ + /** + * Raw transaction request. + * @see https://www.notion.so/warpcast/Frame-Transactions-Public-Draft-v2-9d9f9f4f527249519a41bd8d16165f73?pvs=4#1b69c268f0684c978fbdf4d331ab8869 + */ res: TransactionResponseFn - /** Contract response that includes properties such as: abi, functionName, to, value, etc */ - contract: ContractTransactionResponseFn + /** + * Send transaction request. + * + * This is a convenience method for "send transaction" requests as defined in the [Transaction Spec](https://www.notion.so/warpcast/Frame-Transactions-Public-Draft-v2-9d9f9f4f527249519a41bd8d16165f73?pvs=4#1b69c268f0684c978fbdf4d331ab8869). + */ + send: SendTransactionResponseFn } -export type TransactionResponse = { - /** Chain ID relating to the transaction. */ - chainId: string - /** Description of the transaction. */ - description: string +////////////////////////////////////////////////////// +// Raw Transaction + +export type TransactionParameters = { + /** A CAIP-2 Chain ID to identify the transaction network. */ + chainId: `eip155:${number}` +} & EthSendTransactionSchema + +export type TransactionResponse = Pick & + EthSendTransactionSchema + +export type EthSendTransactionSchema = { + /** A method ID to identify the type of transaction request. */ + method: 'eth_sendTransaction' /** Transaction calldata. */ - data?: string | undefined - /** Destination address of the transaction. */ - to: string - /** Value to send with the transaction. */ - value: string + params: EthSendTransactionParameters +} + +export type EthSendTransactionParameters = { + abi?: Abi | undefined + data?: Hex | undefined + to: Hex + value?: quantity } export type TransactionResponseFn = ( - response: TransactionResponse, + parameters: TransactionParameters, +) => TransactionResponse + +////////////////////////////////////////////////////// +// Send Transaction + +type SendTransactionParameters = { + chainId: `eip155:${number}` +} & EthSendTransactionParameters + +export type SendTransactionResponseFn = ( + parameters: SendTransactionParameters, ) => TransactionResponse -export type ContractTransactionResponse< +////////////////////////////////////////////////////// +// Contract Transaction + +export type ContractTransactionParameters< abi extends Abi | readonly unknown[] = Abi, functionName extends ContractFunctionName< abi, @@ -42,24 +90,19 @@ export type ContractTransactionResponse< /// allFunctionNames = ContractFunctionName, allArgs = ContractFunctionArgs, -> = { +> = Pick & { /** Contract ABI. */ abi: abi /** Contract function arguments. */ args?: (abi extends Abi ? UnionWiden : never) | allArgs | undefined - /** Chain ID relating to the transaction. */ - chainId: string - /** Description of the transaction. */ - description: string /** Contract function name to invoke. */ functionName: | allFunctionNames // show all options | (functionName extends allFunctionNames ? functionName : never) // infer value /** Destination address of the transaction. */ - to: string - /** Value to send with the transaction. */ - value: string -} & (readonly [] extends allArgs ? {} : { args: Widen }) + to: Hex +} & (readonly [] extends allArgs ? {} : { args: Widen }) & + GetValue export type ContractTransactionResponseFn = < const abi extends Abi | readonly unknown[], @@ -70,5 +113,5 @@ export type ContractTransactionResponseFn = < functionName >, >( - response: ContractTransactionResponse, + response: ContractTransactionParameters, ) => TransactionResponse