UNPKG

accounts

Version:

Tempo Accounts SDK

187 lines (164 loc) 7.04 kB
import { RpcResponse, Signature } from 'ox' import { TxEnvelopeTempo } from 'ox/tempo' import type { Address, Client } from 'viem' import type { LocalAccount } from 'viem/accounts' import { Transaction } from 'viem/tempo' import * as Utils from './utils.js' /** Returns sponsor metadata for `eth_fillTransaction` responses. */ export function getSponsor(options: getSponsor.Options): getSponsor.ReturnType { const { account, name, url } = options return { address: account.address, ...(name ? { name } : {}), ...(url ? { url } : {}), } } export declare namespace getSponsor { type Options = { /** Account used for sponsorship. */ account: LocalAccount /** Optional display name. */ name?: string | undefined /** Optional display URL. */ url?: string | undefined } type ReturnType = { /** Sponsor address. */ address: Address /** Sponsor display name. */ name?: string | undefined /** Sponsor display URL. */ url?: string | undefined } } /** Returns whether the fee payer approves a filled transaction. */ export async function shouldSponsor(options: shouldSponsor.Options) { const { sender, transaction, validate } = options if (!validate) return true return await validate({ ...transaction, from: sender, } as Transaction.TransactionRequest) } export declare namespace shouldSponsor { type Options = { /** Sender address from the original request. */ sender?: Address | undefined /** Filled transaction to validate. */ transaction: Record<string, unknown> /** Optional sponsorship approval callback. */ validate?: ((request: Transaction.TransactionRequest) => boolean | Promise<boolean>) | undefined } } /** Returns whether a raw Tempo transaction is explicitly requesting sponsorship. */ export function requestsRawSponsorship(serialized: `0x${string}`) { if (!Utils.isSerializedTempoTransaction(serialized)) return false const transaction = Transaction.deserialize(serialized) return 'feePayerSignature' in transaction && transaction.feePayerSignature === null } /** Returns `true` when a fill request already has the fields needed for sponsorship signing. */ export function isPreparedTransaction(value: Record<string, unknown>) { return ( typeof value.from === 'string' && typeof Utils.resolveChainId(value.chainId) === 'number' && typeof value.gas !== 'undefined' && typeof value.nonce !== 'undefined' && (typeof value.maxFeePerGas !== 'undefined' || typeof value.gasPrice !== 'undefined') ) } /** Signs a filled transaction as the fee payer. */ export async function sign(options: sign.Options) { const { account, transaction, sender } = options const from = (transaction.from as Address | undefined) ?? sender const { signature: _, ...withoutSenderSig } = transaction const prepared = { ...withoutSenderSig, from } if (!prepared.from) throw new RpcResponse.InvalidParamsError({ message: 'Transaction sender must be provided before fee payer signing.', }) if (!account.sign) throw new Error('Fee payer account cannot sign transactions.') const feePayerSignature = Signature.from( await account.sign({ hash: TxEnvelopeTempo.getFeePayerSignPayload(TxEnvelopeTempo.from(prepared as never), { sender: prepared.from, }), }), ) return { ...prepared, feePayerSignature } } export declare namespace sign { type Options = { /** Account used as the fee payer. */ account: LocalAccount /** Filled transaction to sign. */ transaction: Record<string, unknown> /** Sender address from the original request. */ sender?: Address | undefined } } /** Handles `eth_signRawTransaction` and broadcast methods for sponsored Tempo transactions. */ export async function handleRawTransaction(options: handleRawTransaction.Options) { const { account, feeToken: sponsorFeeToken, getClient, method, request, validate } = options const serialized = request.params?.[0] as `0x76${string}` | undefined if (!Utils.isSerializedTempoTransaction(serialized)) throw new RpcResponse.InvalidParamsError({ message: 'Only Tempo (0x76/0x78) transactions are supported.', }) const transaction = Transaction.deserialize(serialized) // Prefer sender recovered from raw envelope; multisig finalize path supplies fallback sender. const sender = transaction.from ?? options.sender // Sponsorship only applies after sender has signed original transaction. if (!transaction.signature || !sender) throw new RpcResponse.InvalidParamsError({ message: 'Transaction must be signed by the sender before fee payer signing.', }) if (!account.sign) throw new Error('Fee payer account cannot sign transactions.') const client = getClient(transaction.chainId) const feeToken_chain = (client.chain as { feeToken?: Address | undefined } | undefined)?.feeToken const feeToken = (transaction.feeToken as Address | null | undefined) ?? sponsorFeeToken ?? (await options.getFeeToken?.(transaction.chainId)) ?? feeToken_chain const transaction_sponsored = feeToken ? { ...transaction, feeToken } : transaction if (validate && !(await validate(transaction_sponsored as Transaction.TransactionRequest))) throw new RpcResponse.InvalidParamsError({ message: 'Sponsorship rejected.', }) const envelope = TxEnvelopeTempo.from(transaction_sponsored as never) const feePayerSignature = Signature.from( await account.sign({ hash: TxEnvelopeTempo.getFeePayerSignPayload(envelope, { sender }), }), ) const serializedTransaction = TxEnvelopeTempo.serialize(envelope, { feePayerSignature, signature: transaction.signature, }) // Raw-sign requests stop after fee-payer signature is added; send methods broadcast it. if (method === 'eth_signRawTransaction') return serializedTransaction return await client.request({ method: method as never, params: [serializedTransaction], }) } export declare namespace handleRawTransaction { type Options = { /** Account used as the fee payer. */ account: LocalAccount /** Optional token the fee payer prefers for sponsored raw transactions. */ feeToken?: Address | undefined /** Optional fee-token resolver used when the raw envelope omits `feeToken`. */ getFeeToken?: ((chainId: number) => Promise<Address | undefined>) | undefined /** Client resolver keyed by transaction `chainId`. */ getClient: (chainId?: number | undefined) => Client /** Raw transaction method to handle. */ method: 'eth_signRawTransaction' | 'eth_sendRawTransaction' | 'eth_sendRawTransactionSync' /** Incoming JSON-RPC request. */ request: { params?: readonly unknown[] | undefined } /** Sender address to use if it cannot be recovered from the raw envelope. */ sender?: Address | undefined /** Optional sponsorship approval callback. */ validate?: ((request: Transaction.TransactionRequest) => boolean | Promise<boolean>) | undefined } }