Skip to content
This repository was archived by the owner on Apr 18, 2026. It is now read-only.
Draft
5 changes: 5 additions & 0 deletions .changeset/perfect-tools-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@abstract-foundation/agw-client': minor
---

feat: add optimistic transaction parameters to sign transaction for session to reduce rpc calls
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ root = true
end_of_line = lf
insert_final_newline = true

[*.{js,json,yml}]
[*.{js,json,yml,ts}]
charset = utf-8
indent_style = space
indent_size = 2
8 changes: 8 additions & 0 deletions packages/agw-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
"import": "./dist/esm/exports/actions.js",
"require": "./dist/cjs/exports/actions.js"
},
"./experimental": {
"types": "./dist/types/exports/experimental.d.ts",
"import": "./dist/esm/exports/experimental.js",
"require": "./dist/cjs/exports/experimental.js"
},
"./constants": {
"types": "./dist/types/exports/constants.d.ts",
"import": "./dist/esm/exports/constants.js",
Expand All @@ -53,6 +58,9 @@
"constants": [
"./dist/types/exports/constants.d.ts"
],
"experimental": [
"./dist/types/exports/experimental.d.ts"
],
"sessions": [
"./dist/types/exports/sessions.d.ts"
]
Expand Down
26 changes: 20 additions & 6 deletions packages/agw-client/src/actions/prepareTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
type GetTransactionRequestKzgParameter,
type IsNever,
keccak256,
maxUint256,
type NonceManager,
type Prettify,
type PublicClient,
Expand Down Expand Up @@ -62,6 +63,7 @@ import {
import { InsufficientBalanceError } from '../errors/insufficientBalance.js';
import { AccountFactoryAbi } from '../exports/constants.js';
import type { Call } from '../types/call.js';
import type { OptimisticTransactionParameters } from '../types/optimisticTransaction.js';
import { isSmartAccountDeployed, transformHexValues } from '../utils.js';
import { getInitializerCalldata } from '../utils.js';

Expand Down Expand Up @@ -141,6 +143,8 @@ export type PrepareTransactionRequestRequest<
* Whether the transaction is the first transaction of the account.
*/
isInitialTransaction?: boolean;

optimistic?: OptimisticTransactionParameters;
};

export type PrepareTransactionRequestParameters<
Expand Down Expand Up @@ -308,10 +312,12 @@ export async function prepareTransactionRequest<
parameters: parameterNames = defaultParameters,
} = args;

const isDeployed = await isSmartAccountDeployed(
publicClient,
client.account.address,
);
const isDeployed =
args.optimistic?.isDeployed !== undefined
? args.optimistic.isDeployed
: args.optimistic !== undefined
? true
: await isSmartAccountDeployed(publicClient, client.account.address);

if (!isDeployed) {
const initialCall = {
Expand Down Expand Up @@ -350,10 +356,18 @@ export async function prepareTransactionRequest<

// Prepare all async operations that can run in parallel
const asyncOperations = [];
let userBalance: bigint | undefined;
let userBalance: bigint | undefined =
args.optimistic?.balance !== undefined
? args.optimistic.balance
: args.optimistic !== undefined
? maxUint256
: undefined;

// Get balance if the transaction is not sponsored or has a value
if (!isSponsored || (request.value !== undefined && request.value > 0n)) {
if (
userBalance === undefined &&
(!isSponsored || (request.value !== undefined && request.value > 0n))
) {
asyncOperations.push(
getBalance(publicClient, {
address: initiatorAccount.address,
Expand Down
25 changes: 7 additions & 18 deletions packages/agw-client/src/actions/sendTransactionInternal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import {
type Transport,
type WalletClient,
} from 'viem';
import { getChainId, sendRawTransaction } from 'viem/actions';
import { sendRawTransaction } from 'viem/actions';
import {
assertCurrentChain,
getAction,
getTransactionError,
type GetTransactionErrorParameters,
Expand All @@ -28,6 +27,7 @@ import { INSUFFICIENT_BALANCE_SELECTOR } from '../constants.js';
import { AccountNotFoundError } from '../errors/account.js';
import { InsufficientBalanceError } from '../errors/insufficientBalance.js';
import type { CustomPaymasterHandler } from '../types/customPaymaster.js';
import type { OptimisticTransactionParameters } from '../types/optimisticTransaction.js';
import { prepareTransactionRequest } from './prepareTransaction.js';
import { signTransaction } from './signTransaction.js';

Expand All @@ -49,6 +49,7 @@ export async function sendTransactionInternal<
validator: Address,
validationHookData: Record<string, Hex> = {},
customPaymasterHandler: CustomPaymasterHandler | undefined = undefined,
optimisticData: OptimisticTransactionParameters | undefined = undefined,
): Promise<SendEip712TransactionReturnType> {
const { chain = client.chain } = parameters;

Expand All @@ -69,33 +70,21 @@ export async function sendTransactionInternal<
{
...parameters,
parameters: ['gas', 'nonce', 'fees'],
isSponsored:
customPaymasterHandler !== undefined ||
(parameters as any).paymaster !== undefined,
nonceManager: account.nonceManager,
optimistic: optimisticData,
} as any,
);

let chainId: number | undefined;
if (chain !== null) {
chainId = await getAction(signerClient, getChainId, 'getChainId')({});
assertCurrentChain({
currentChainId: chainId,
chain,
});
}

const serializedTransaction = await signTransaction(
client,
signerClient,
publicClient,
{
...request,
chainId,
} as any,
request as any,
validator,
validationHookData,
customPaymasterHandler,
false,
optimisticData,
);
return await getAction(
client,
Expand Down
30 changes: 19 additions & 11 deletions packages/agw-client/src/actions/signTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import { AccountNotFoundError } from '../errors/account.js';
import { assertSessionKeyPolicies } from '../sessionValidator.js';
import type { CustomPaymasterHandler } from '../types/customPaymaster.js';
import type { OptimisticTransactionParameters } from '../types/optimisticTransaction.js';
import { VALID_CHAINS } from '../utils.js';
import { transformHexValues } from '../utils.js';
import { signPrivyTransaction } from './sendPrivyTransaction.js';
Expand All @@ -45,6 +46,7 @@ export async function signTransaction<
validationHookData: Record<string, Hex> = {},
customPaymasterHandler: CustomPaymasterHandler | undefined = undefined,
isPrivyCrossApp = false,
optimisticData: OptimisticTransactionParameters | undefined = undefined,
): Promise<SignEip712TransactionReturnType> {
const chain = client.chain;

Expand All @@ -63,6 +65,7 @@ export async function signTransaction<
validator,
validationHookData,
customPaymasterHandler,
optimisticData,
);

return chain.serializers.transaction(
Expand All @@ -87,6 +90,7 @@ export async function signEip712TransactionInternal<
validator: Address,
validationHookData: Record<string, Hex> = {},
customPaymasterHandler: CustomPaymasterHandler | undefined = undefined,
optimisticData: OptimisticTransactionParameters | undefined = undefined,
): Promise<{
transaction: UnionRequiredBy<TransactionRequestEIP712, 'from'> & {
chainId: number;
Expand Down Expand Up @@ -134,7 +138,9 @@ export async function signEip712TransactionInternal<
if (!chain?.custom?.getEip712Domain)
throw new BaseError('`getEip712Domain` not found on chain.');

const chainId = await getAction(client, getChainId, 'getChainId')({});
const chainId = optimisticData
? chain.id
: await getAction(client, getChainId, 'getChainId')({});
if (chain !== null)
assertCurrentChain({
currentChainId: chainId,
Expand Down Expand Up @@ -177,16 +183,18 @@ export async function signEip712TransactionInternal<
} else {
const hookData: Hex[] = [];
if (!useSignerAddress) {
const validationHooks = await getAction(
client,
readContract,
'readContract',
)({
address: client.account.address,
abi: AGWAccountAbi,
functionName: 'listHooks',
args: [true],
});
const validationHooks =
optimisticData?.validationHooks ??
(await getAction(
client,
readContract,
'readContract',
)({
address: client.account.address,
abi: AGWAccountAbi,
functionName: 'listHooks',
args: [true],
}));
for (const hook of validationHooks) {
hookData.push(validationHookData[hook] ?? '0x');
}
Expand Down
20 changes: 9 additions & 11 deletions packages/agw-client/src/actions/signTransactionForSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
type SessionConfig,
} from '../sessions.js';
import type { CustomPaymasterHandler } from '../types/customPaymaster.js';
import { isSmartAccountDeployed } from '../utils.js';
import type { OptimisticTransactionParameters } from '../types/optimisticTransaction.js';
import { signTransaction } from './signTransaction.js';

export interface SendTransactionForSessionParameters<
Expand Down Expand Up @@ -51,18 +51,12 @@ export async function signTransactionForSession<
client: Client<Transport, ChainEIP712, Account>,
signerClient: WalletClient<Transport, ChainEIP712, Account>,
publicClient: PublicClient<Transport, ChainEIP712>,
parameters: SignEip712TransactionParameters<chain, account, chainOverride>,
parameters: SignEip712TransactionParameters<chain, account, chainOverride> & {
optimistic?: OptimisticTransactionParameters;
},
session: SessionConfig,
customPaymasterHandler: CustomPaymasterHandler | undefined = undefined,
): Promise<SignTransactionReturnType> {
const isDeployed = await isSmartAccountDeployed(
publicClient,
client.account.address,
);
if (!isDeployed) {
throw new BaseError('Smart account not deployed');
}

const selector: Hex | undefined = parameters.data
? `0x${parameters.data.slice(2, 10)}`
: undefined;
Expand All @@ -71,11 +65,13 @@ export async function signTransactionForSession<
throw new BaseError('Transaction to field is not specified');
}

const { optimistic, ...rest } = parameters;

return await signTransaction(
client,
signerClient,
publicClient,
parameters,
rest as SignEip712TransactionParameters<chain, account, chainOverride>,
SESSION_KEY_VALIDATOR_ADDRESS,
{
[SESSION_KEY_VALIDATOR_ADDRESS]: encodeSessionWithPeriodIds(
Expand All @@ -89,5 +85,7 @@ export async function signTransactionForSession<
),
},
customPaymasterHandler,
false,
optimistic,
);
}
4 changes: 4 additions & 0 deletions packages/agw-client/src/exports/experimental.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
DefaultOptimisticTransactionParameters,
type OptimisticTransactionParameters,
} from '../types/optimisticTransaction.js';
56 changes: 56 additions & 0 deletions packages/agw-client/src/types/optimisticTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
type Account,
type FormattedTransactionRequest,
type GetChainParameter,
type Hex,
maxUint256,
type UnionOmit,
} from 'viem';
import type { GetAccountParameter } from 'viem/_types/types/account.js';
import type { ChainEIP712 } from 'viem/chains';

import { SESSION_KEY_VALIDATOR_ADDRESS } from '../constants.js';

/**
* Parameters for configuring an optimistic transaction (experimental)
* @property {bigint} balance - The current balance of the wallet used for the transaction
* @property {boolean} isDeployed - Deployment status of the account
* @property {Hex[]} validationHooks - Array of validation hooks installed in the wallet
*/
export interface OptimisticTransactionParameters {
/** The current balance of the wallet used for the transaction */
balance: bigint;
/** Deployment status of the account */
isDeployed: boolean;
/** Array of validation hooks installed in the wallet */
validationHooks: Hex[];
}

/**
* Default parameters for an optimistic transaction.
* - balance: uint256 max
* - isDeployed: true
* - validationHooks: array with single item of session key validator
*/
export const DefaultOptimisticTransactionParameters: OptimisticTransactionParameters =
{
balance: maxUint256,
isDeployed: true,
validationHooks: [SESSION_KEY_VALIDATOR_ADDRESS],
};

export type SignOptimisticEip712TransactionParameters<
chain extends ChainEIP712 | undefined = ChainEIP712 | undefined,
account extends Account | undefined = Account | undefined,
chainOverride extends ChainEIP712 | undefined = ChainEIP712 | undefined,
> = UnionOmit<
FormattedTransactionRequest<
chainOverride extends ChainEIP712 ? chainOverride : chain
>,
'from'
> &
GetAccountParameter<account> &
GetChainParameter<chain, chainOverride> & {
/** @experimental This parameter is experimental and subject to change */
optimistic?: OptimisticTransactionParameters;
};
9 changes: 7 additions & 2 deletions packages/agw-client/src/walletActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import { EOA_VALIDATOR_ADDRESS } from './constants.js';
import { type SessionClient, toSessionClient } from './sessionClient.js';
import type { SessionConfig, SessionStatus } from './sessions.js';
import type { CustomPaymasterHandler } from './types/customPaymaster.js';
import type { SignOptimisticEip712TransactionParameters } from './types/optimisticTransaction.js';
import type { SendTransactionBatchParameters } from './types/sendTransactionBatch.js';
import type { SignTransactionBatchParameters } from './types/signTransactionBatch.js';

Expand Down Expand Up @@ -154,7 +155,11 @@ export type SessionClientActions<
>,
) => Promise<SendTransactionReturnType>;
signTransaction: (
args: SignEip712TransactionParameters<chain, account, chainOverride>,
args: SignOptimisticEip712TransactionParameters<
chain,
account,
chainOverride
>,
) => Promise<SignTransactionReturnType>;
writeContract: WalletActions<chain, account>['writeContract'];
signTypedData: WalletActions<chain, account>['signTypedData'];
Expand Down Expand Up @@ -210,7 +215,7 @@ export function sessionWalletActions(
client,
signerClient,
publicClient,
args,
args as any,
session,
paymasterHandler,
),
Expand Down
Loading
Loading