Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions app/vibenet/demos/account/components/TransactionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ function payerRejectionMessage(rejected: PayerRejectedData): string {
: 'The payer’s simulation says this transaction would revert.';
case 'GAS_TOO_LOW':
return 'The payer needs a higher gas limit for these calls. Retry to re-sign with more gas.';
case 'COST_EXCEEDS_LIMIT':
return 'This transaction costs more gas than the payer covers per transaction. Retry when gas is cheaper, or pay with ETH.';
default:
return rejected.reason ?? `Payer rejected the transaction (${rejected.code}).`;
}
Expand Down Expand Up @@ -264,15 +266,24 @@ export function TransactionModal({ onClose, preset }: TransactionModalProps) {
try {
const payerClient = createPayerClient({ url: PAYER_URL });
const rpcCalls = buildCalls(calls, acct.address).map((c) => ({ to: c.to, value: toHex(c.value), data: c.data }));
// ERC-8168: the intent's `gasLimit` excludes phase 0; the payer adds `paymentGas`.
const callGas = estimateTxGas({
calls: calls.length,
valueCalls: valueBearingCallCount(calls, acct.address),
nonceless,
payer: true,
tokenPayment: false,
});
const context = { flow: 'transact' };
const terms = await payerClient.getTerms({
chainId: toHex(chain.id || 84538453),
from: acct.address,
calls: rpcCalls,
gasLimit: toHex(BigInt(gasEstimate || 200_000)),
context: { flow: 'transact' },
gasLimit: toHex(BigInt(callGas || 200_000)),
context,
});

let selToken: Address | undefined;
let selToken: Address | 'native' | undefined;
if (mode === 'usdv') {
const tokenOffer = terms.options.find(isTokenOffer);
selToken = tokenOffer?.tokens?.[0]?.token;
Expand All @@ -281,11 +292,18 @@ export function TransactionModal({ onClose, preset }: TransactionModalProps) {
const declinedFree = mode === 'free' ? terms.options.find(isDeclinedOffer) : undefined;
const { option, tokenChoice } = selectPaymentOption(terms, selToken ? { token: selToken } : {});

if (option.kind === 'token' && tokenChoice?.token === 'native')
throw new Error('This payer only offers native-token payment, which this dialog does not build.');
// A payer that co-signs without submitting authorizes before the user signs.
const authorize = option.methods?.includes('payer_signTransaction')
? async (unsigned: Hex) => (await payerClient.signTransaction({ transaction: unsigned, context })).payerAuth
: undefined;
let payer: ComposePayer;
if (option.kind === 'token' && tokenChoice) {
const quoted = BigInt(tokenChoice.paymentAmount);
if (option.kind === 'token' && tokenChoice && tokenChoice.token !== 'native') {
const quoted = tokenChoice.paymentAmount ? BigInt(tokenChoice.paymentAmount) : 0n;
payer = {
address: option.payer,
authorize,
tokenPayment: {
token: tokenChoice.token,
to: tokenChoice.feeRecipient ?? option.payer,
Expand All @@ -294,7 +312,7 @@ export function TransactionModal({ onClose, preset }: TransactionModalProps) {
},
};
} else {
payer = { address: option.payer };
payer = { address: option.payer, authorize };
}

// A payer requires an expiry; stay inside its `maxExpiry` (the engine
Expand Down Expand Up @@ -326,15 +344,13 @@ export function TransactionModal({ onClose, preset }: TransactionModalProps) {

let txHash: Hex;
let pending = false;
let finalTx = serialized;
if (option.methods?.includes('payer_signTransaction')) {
const cosigned = await payerClient.signTransaction({ signedTransaction: serialized, context: { flow: 'transact' } });
finalTx = cosigned.signedTransaction;
({ txHash, pending } = await broadcastOrPending(finalTx));
if (authorize) {
// Already carries the payer's `payerAuth`.
({ txHash, pending } = await broadcastOrPending(serialized));
} else {
setSubmitStatus('submitting');
const anchor = latestAnchor();
const sent = await payerClient.sendTransaction({ signedTransaction: serialized, context: { flow: 'transact' } });
const sent = await payerClient.sendTransaction({ transaction: serialized, context });
setSubmitStatus('confirming');
txHash = sent.transactionHash;
try {
Expand All @@ -344,7 +360,7 @@ export function TransactionModal({ onClose, preset }: TransactionModalProps) {
pending = true;
}
}
recordResult(acct, finalTx, txHash, pending, activeSigner, gasNote);
recordResult(acct, serialized, txHash, pending, activeSigner, gasNote);
} catch (err) {
const rejected = parsePayerError(err);
if (rejected) {
Expand Down
9 changes: 9 additions & 0 deletions app/vibenet/demos/account/useAccountEngine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
k1Authenticator,
parseReceiptFields,
privateKeyToAccount,
serializeTransaction,
type Signer,
toEoaAccount,
toHex,
Expand Down Expand Up @@ -166,6 +167,12 @@ export type ComposePayer = {
* `payerAuth` for a hosted payer service to co-sign out of band.
*/
localSigner?: Signer;
/**
* Gets `payerAuth` from a hosted payer (`payer_signTransaction`) for the
* transaction serialized without `sender_auth`, before the sender signs, so
* a payer rejection never costs the user a signature.
*/
authorize?: (unsigned: Hex) => Promise<Hex>;
};

export type ComposeOptions = {
Expand Down Expand Up @@ -575,6 +582,8 @@ function useAccountEngineCore() {
...(metadata ? { metadata } : {}),
...(payer ? { payer: payer.address } : {}),
};
if (payer?.authorize)
transaction.payerAuth = await payer.authorize(serializeTransaction({ ...transaction, senderAuth: '0x' }));
const serialized = await account.signTransaction(
transaction,
payer?.localSigner ? { payer: { account: payer.localSigner, address: payer.address } } : undefined,
Expand Down
3 changes: 3 additions & 0 deletions vendor/aa/entry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,19 @@ export {
parseReceiptFields,
prepareTransactionRequest,
sendTransaction,
serializeTransaction,
toEoaAccount,
txValueCost,
} from '../../../viem/src/eip8130/index.ts'

// ERC-8168 (payer / sponsorship)
export {
createPayerClient,
encodePayment,
encodeTokenTransfer,
isDeclinedOffer,
isTokenOffer,
parsePayerError,
requiredPaymentAmount,
selectPaymentOption,
} from '../../../viem/src/eip8168/index.ts'
74 changes: 49 additions & 25 deletions vendor/aa/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ export type SendTransactionParameters = Omit<PrepareTransactionRequestParameters
/** Prepares, signs (sender and, when set, payer) and submits an AA transaction. */
export function sendTransaction(client: Client, parameters: SendTransactionParameters): Promise<Hex>

/** RLP-serializes an EIP-8130 transaction; an empty `senderAuth` gives the bytes a payer can authorize before the sender signs. */
export function serializeTransaction(transaction: TransactionSerializable8130): TransactionSerialized8130

export type ReceiptFields = {
payer?: Address | undefined
phaseStatuses?: readonly Hex[] | undefined
Expand Down Expand Up @@ -254,13 +257,15 @@ export type BalanceLimit = {
decimals?: number | undefined
}

export type PayerProvider = { name: string; icon?: string | undefined }

export type PayerBalance = {
kind: 'sponsorship' | 'credit'
limits: readonly BalanceLimit[]
validFor?: number | undefined
payer?: Address | undefined
endpoint?: string | undefined
name?: string | undefined
provider?: PayerProvider | undefined
}

export type PayerGasEstimate = { gasLimit: Hex; maxFeePerGas: Hex; maxPriorityFeePerGas: Hex }
Expand All @@ -272,13 +277,16 @@ export type PayerConditions = {
maxCost?: Hex | undefined
}

export type PayerProvider = { name: string; icon?: string | undefined }
export type PayerCostDiagnostic = { estimated: Hex; limit: Hex }

export type BaseOffer = {
/** 20-byte payer address, or `0x00` for an open payer. */
payer: Address
endpoint?: string | undefined
/** Optional methods beyond `payer_getTerms` / `payer_sendTransaction` (e.g. `payer_signTransaction`). */
methods?: readonly string[] | undefined
/** Co-sign methods for this offer; absent means `["payer_sendTransaction"]`. */
methods?: readonly ('payer_sendTransaction' | 'payer_signTransaction')[] | undefined
/** Preauthorized `payer_auth`, when the payer signed ahead of time. */
payerAuth?: Hex | undefined
ttl: number
conditions?: PayerConditions | undefined
provider?: PayerProvider | undefined
Expand All @@ -294,38 +302,44 @@ export type SponsoredOfferDeclined = {
code: string
reason?: string | undefined
balance?: PayerBalance | undefined
gas?: { estimatedCost: Hex; maxCost: Hex } | undefined
/** Only for `COST_EXCEEDS_LIMIT`. */
cost?: PayerCostDiagnostic | undefined
provider?: PayerProvider | undefined
}

export type SponsoredOffer = SponsoredOfferSelectable | SponsoredOfferDeclined

export type TokenChoice = {
token: Address
/** ERC-20 address, or `"native"` for a value-transfer payment. */
token: Address | 'native'
symbol: string
decimals: number
/** Phase-0 amount, quoted against the terms' `gasEstimate`. */
paymentAmount: Hex
/** The required amount at the terms' `gasEstimate`; present exactly when `gasEstimate` is. */
paymentAmount?: Hex | undefined
feeRecipient?: Address | undefined
rate: { numerator: Hex; denominator: Hex }
/** WAD: token atomic units per 1e18 wei. Phase 0 covers `ceil(max_cost × rate / 1e18)`. */
rate: Hex
paymentGas?: Hex | undefined
/** One whole token's fiat price, as a WAD. */
fiatRate?: Hex | undefined
refund?: { window: number } | undefined
}

export type TokenPaymentOffer = BaseOffer & {
export type TokenOffer = BaseOffer & {
kind: 'token'
tokens: readonly TokenChoice[]
paymentMode?: 'transfer' | 'any' | undefined
}

export type PaymentOption = SponsoredOffer | TokenPaymentOffer
export type PaymentOption = SponsoredOffer | TokenOffer

export type GetTermsParameters = {
chainId: Hex
from: Address
calls: readonly PayerRpcCall[]
/** Gas for the user calls only; excludes phase 0. */
gasLimit?: Hex | undefined
preferredTokens?: readonly Address[] | undefined
preferredTokens?: readonly (Address | 'native')[] | undefined
fiatCurrency?: string | undefined
context?: Record<string, unknown> | undefined
}
Expand All @@ -336,25 +350,25 @@ export type GetTermsReturnType = {
fiatCurrency?: string | undefined
}

export type TokenCharged = { token: Address; amount: Hex }

export type PayerSendTransactionParameters = {
signedTransaction: Hex
/** The sender-signed transaction, `payer_auth` empty. */
transaction: Hex
context?: Record<string, unknown> | undefined
}
export type PayerSendTransactionReturnType = { transactionHash: Hex; tokenCharged?: TokenCharged | undefined }
export type PayerSendTransactionReturnType = { transactionHash: Hex }
export type PayerSignTransactionParameters = {
signedTransaction: Hex
/** The built transaction, with or without `sender_auth`. */
transaction: Hex
context?: Record<string, unknown> | undefined
}
export type PayerSignTransactionReturnType = { signedTransaction: Hex; tokenCharged?: TokenCharged | undefined }
export type PayerSignTransactionReturnType = { payerAuth: Hex }

export type GetSponsorshipBalanceParameters = {
from: Address
chainId?: Hex | undefined
payer?: Address | undefined
endpoint?: string | undefined
kind?: readonly ('sponsorship' | 'credit')[] | undefined
kinds?: readonly ('sponsorship' | 'credit')[] | undefined
context?: Record<string, unknown> | undefined
}
export type GetSponsorshipBalanceReturnType = { balances: readonly PayerBalance[]; ttl: number }
Expand All @@ -370,25 +384,29 @@ export type PayerClient = {
export function createPayerClient(parameters: { url?: string | undefined; transport?: any }): PayerClient

export type SelectPaymentOptionReturnType = {
option: SponsoredOfferSelectable | TokenPaymentOffer
option: SponsoredOfferSelectable | TokenOffer
tokenChoice?: TokenChoice | undefined
}
/** Picks one selectable offer: the matching token offer when `token` is set, else sponsorship first. */
export function selectPaymentOption(
terms: GetTermsReturnType,
parameters?: { token?: Address | undefined },
parameters?: { token?: Address | 'native' | undefined },
): SelectPaymentOptionReturnType
export function isTokenOffer(option: PaymentOption): option is TokenPaymentOffer
export function isTokenOffer(option: PaymentOption): option is TokenOffer
export function isDeclinedOffer(option: PaymentOption): option is SponsoredOfferDeclined

/** Builds the phase-0 `IERC20.transfer(to, amount)` call. */
export function encodeTokenTransfer(parameters: { token: Address; to: Address; amount: bigint }): AaCall
/** Phase-0 payment: an ERC-20 transfer, or a value transfer for `"native"`. */
export function encodePayment(parameters: { token: Address | 'native'; to: Address; amount: bigint }): AaCall
/** `ceil(maxCost × rate / 1e18)`. */
export function requiredPaymentAmount(parameters: { maxCost: bigint; rate: bigint }): bigint

export type PayerRequote = {
token: Address
token: Address | 'native'
paymentAmount: Hex
feeRecipient?: Address | undefined
rate?: { numerator: Hex; denominator: Hex } | undefined
rate?: Hex | undefined
ttl?: number | undefined
}

Expand All @@ -397,11 +415,17 @@ export type PayerRejectedData = {
code: string
reason?: string | undefined
balance?: PayerBalance | undefined
gas?: { estimatedCost: Hex; maxCost: Hex } | undefined
/** `COST_EXCEEDS_LIMIT`: the payer's estimate and per-tx ceiling. */
cost?: PayerCostDiagnostic | undefined
/** `GAS_TOO_LOW`: the smallest gas limit the calls need. */
minGasLimit?: Hex | undefined
/** `PAYMENT_INSUFFICIENT`: the corrected phase-0 transfer. */
requote?: PayerRequote | undefined
/** `EXECUTION_REVERTED`: which phase reverted, and why. */
revert?: { phase: number; reason?: string | undefined; data?: Hex | undefined } | undefined
/** `SENDER_BALANCE_INSUFFICIENT`. */
shortfall?: { token: Address | 'native'; required: Hex; available: Hex } | undefined
options?: readonly PaymentOption[] | undefined
}

/** Extracts {@link PayerRejectedData} from a thrown payer error; `undefined` for other errors. */
Expand Down
13 changes: 13 additions & 0 deletions vendor/aa/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18776,6 +18776,16 @@ function createPayerClient(parameters) {
init_abis();
init_base();
init_encodeFunctionData();
function requiredPaymentAmount(parameters) {
const { maxCost, rate } = parameters;
return (maxCost * rate + 10n ** 18n - 1n) / 10n ** 18n;
}
function encodePayment(parameters) {
const { token, to, amount } = parameters;
if (token === "native")
return { to, value: amount, data: "0x" };
return encodeTokenTransfer({ token, to, amount });
}
function encodeTokenTransfer(parameters) {
return {
to: parameters.token,
Expand Down Expand Up @@ -18847,8 +18857,10 @@ export {
txValueCost,
toHex,
toEoaAccount,
serializeTransaction4 as serializeTransaction,
sendTransaction,
selectPaymentOption,
requiredPaymentAmount,
privateKeyToAccount,
prepareTransactionRequest2 as prepareTransactionRequest,
parseUnits2 as parseUnits,
Expand All @@ -18868,6 +18880,7 @@ export {
generatePrivateKey,
estimateGas2 as estimateGas,
encodeTokenTransfer,
encodePayment,
encodeFunctionData,
encodeAbiParameters,
decodeAbiParameters,
Expand Down
Loading