Skip to content
Draft
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
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