Skip to content

Commit

Permalink
latency improvements for submitting user operations (#2697)
Browse files Browse the repository at this point in the history
  • Loading branch information
alvrs authored Apr 19, 2024
1 parent b657fd8 commit f2f48ad
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 543 deletions.
5 changes: 3 additions & 2 deletions packages/account-kit/cli/utils/altoV1.localhost.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"rpcUrl": "http://127.0.0.1:8545",
"minBalance": "0",
"utilityPrivateKey": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
"signerPrivateKeys": "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
"signerPrivateKeys": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80,0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d,0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a,0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6,0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a,0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba,0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e,0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356,0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97,0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6",
"rpcMaxBlockRange": 10000,
"port": 4337
"port": 4337,
"maxBundleWait": 0
}
4 changes: 4 additions & 0 deletions packages/account-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"test:ci": "tsc --noEmit && vitest --run"
},
"dependencies": {
"@account-abstraction/contracts": "0.7.0",
"@latticexyz/common": "workspace:*",
"@latticexyz/config": "workspace:*",
"@latticexyz/protocol-parser": "workspace:*",
Expand All @@ -56,6 +57,7 @@
"abitype": "1.0.0",
"debug": "^4.3.4",
"execa": "^7.0.0",
"p-retry": "^5.1.2",
"permissionless": "^0.1.17",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand All @@ -68,6 +70,8 @@
"zustand": "^4.5.2"
},
"devDependencies": {
"@latticexyz/gas-tank": "workspace:*",
"@pimlicolabs/alto": "git+https://github.com/latticexyz/alto.git#3192a9a",
"@types/debug": "^4.1.7",
"@types/react": "18.2.22",
"@types/react-dom": "18.2.7",
Expand Down
160 changes: 89 additions & 71 deletions packages/account-kit/src/actions/sendTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { AccountOrClientNotFoundError } from "permissionless";
import { Middleware, sendUserOperation } from "permissionless/actions/smartAccount";
import { EntryPoint } from "permissionless/types/entrypoint";
import { AccountOrClientNotFoundError, UserOperation } from "permissionless";
import { Middleware, prepareUserOperationRequest } from "permissionless/actions/smartAccount";
import { EntryPoint, GetEntryPointVersion } from "permissionless/types/entrypoint";
import { SmartAccount } from "permissionless/accounts";
import type { Chain, Client, Hash, SendTransactionParameters, Transport } from "viem";
import type {
Chain,
Client,
EstimateFeesPerGasReturnType,
FeeValuesEIP1559,
Hash,
SendTransactionParameters,
Transport,
} from "viem";
import { Prettify } from "viem/chains";
import { getAction, parseAccount } from "viem/utils";
import { waitForUserOperationTransactionHash } from "./waitForUserOperationTransactionHash";
import { getFeeRef } from "@latticexyz/common";
import pRetry from "p-retry";
import { sendUserOperation as sendUserOperationBundler } from "permissionless/actions";

export type SendTransactionWithPaymasterParameters<
entryPoint extends EntryPoint,
Expand All @@ -14,51 +25,13 @@ export type SendTransactionWithPaymasterParameters<
TChainOverride extends Chain | undefined = Chain | undefined,
> = SendTransactionParameters<TChain, TAccount, TChainOverride> & Middleware<entryPoint>;

export type WriteContractExtraOptions = {
estimateFeesPerGas?: () => Promise<EstimateFeesPerGasReturnType>;
};

/**
* Creates, signs, and sends a new transaction to the network.
* This function also allows you to sponsor this transaction if sender is a smartAccount
*
* - Docs: https://viem.sh/docs/actions/wallet/sendTransaction.html
* - Examples: https://stackblitz.com/github/wagmi-dev/viem/tree/main/examples/transactions/sending-transactions
* - JSON-RPC Methods:
* - JSON-RPC Accounts: [`eth_sendTransaction`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction)
* - Local Accounts: [`eth_sendRawTransaction`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendrawtransaction)
*
* @param client - Client to use
* @param parameters - {@link SendTransactionParameters}
* @returns The [Transaction](https://viem.sh/docs/glossary/terms.html#transaction) hash.
*
* @example
* import { createWalletClient, custom } from 'viem'
* import { mainnet } from 'viem/chains'
* import { sendTransaction } from 'viem/wallet'
*
* const client = createWalletClient({
* chain: mainnet,
* transport: custom(window.ethereum),
* })
* const hash = await sendTransaction(client, {
* account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
* to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
* value: 1000000000000000000n,
* })
*
* @example
* // Account Hoisting
* import { createWalletClient, http } from 'viem'
* import { privateKeyToAccount } from 'viem/accounts'
* import { mainnet } from 'viem/chains'
* import { sendTransaction } from 'viem/wallet'
*
* const client = createWalletClient({
* account: privateKeyToAccount('0x…'),
* chain: mainnet,
* transport: http(),
* })
* const hash = await sendTransaction(client, {
* to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
* value: 1000000000000000000n,
* })
* Override sendTransaction with smart account handling.
* Note: no guaranteed order since a unique `key` is used for every smart account nonce.
*/
export async function sendTransaction<
TChain extends Chain | undefined,
Expand All @@ -68,6 +41,7 @@ export async function sendTransaction<
>(
client: Client<Transport, TChain, TAccount>,
args: Prettify<SendTransactionWithPaymasterParameters<entryPoint, TChain, TAccount, TChainOverride>>,
opts: WriteContractExtraOptions = {},
): Promise<Hash> {
const {
account: account_ = client.account,
Expand All @@ -94,35 +68,79 @@ export async function sendTransaction<
throw new Error("RPC account type not supported");
}

// Refetch the current fees independently of sending user ops to reduce latency
const feeRef = await getFeeRef({
client,
refreshInterval: 10000,
args: { chain: client.chain },
estimateFeesPerGas: opts.estimateFeesPerGas,
});

const callData = await account.encodeCallData({
to,
value: value || BigInt(0),
data: data || "0x",
});

const userOpHash = await getAction(
client,
sendUserOperation<entryPoint>,
"sendUserOperation",
)({
userOperation: {
sender: account.address,
maxFeePerGas: maxFeePerGas || BigInt(0),
maxPriorityFeePerGas: maxPriorityFeePerGas || BigInt(0),
callData: callData,
nonce: nonce ? BigInt(nonce) : undefined,
},
account: account,
middleware,
});
return pRetry(
async () => {
const { account: account_ = client.account } = args;
if (!account_) throw new AccountOrClientNotFoundError();

const userOperationTransactionHash = await getAction(
client,
waitForUserOperationTransactionHash,
"waitForUserOperationTransactionHash",
)({
hash: userOpHash,
});
const account = parseAccount(account_) as SmartAccount<entryPoint>;

const userOperation = await getAction(
client,
prepareUserOperationRequest<entryPoint>,
"prepareUserOperationRequest",
)({
userOperation: {
sender: account.address,
maxFeePerGas: maxFeePerGas ?? (feeRef.fees as FeeValuesEIP1559).maxFeePerGas ?? BigInt(0),
maxPriorityFeePerGas:
maxPriorityFeePerGas ?? (feeRef.fees as FeeValuesEIP1559).maxPriorityFeePerGas ?? BigInt(0),
callData: callData,
nonce: nonce ? BigInt(nonce) : undefined,
},
account: account,
middleware,
});

// We use Date.now() as `key` for the smart account nonce.
// Using only `key` allows us to send multiple user operations to the bundler and be included in the same block,
// but there is no guaranteed ordering of the user operations.
userOperation.nonce = nonce ? BigInt(nonce) : BigInt(Date.now()) << 64n;

userOperation.signature = await account.signUserOperation(
userOperation as UserOperation<GetEntryPointVersion<entryPoint>>,
);

return userOperationTransactionHash.transactionHash;
const userOpHash = await getAction(
client,
sendUserOperationBundler,
"sendUserOperationBundler",
)({
userOperation: userOperation as UserOperation<GetEntryPointVersion<entryPoint>>,
entryPoint: account.entryPoint,
});

const userOperationTransactionHash = await getAction(
client,
waitForUserOperationTransactionHash,
"waitForUserOperationTransactionHash",
)({
hash: userOpHash,
timeout: 10_000,
});

return userOperationTransactionHash.transactionHash;
},
{
retries: 3,
onFailedAttempt: async (error) => {
// TODO: any case in which we should retry?
throw error;
},
},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ export const waitForUserOperationTransactionHash = async <
): Promise<WaitForUserOperationTransactionHashReturnType> => {
const observerId = stringify(["waitForUserOperationReceipt", bundlerClient.uid, hash]);

let userOperationTransactionHash: WaitForUserOperationTransactionHashReturnType;

const getUserOperationStatusPromise = new Promise((resolve, reject) => {
const unobserve = observe(observerId, { resolve, reject }, async (emit) => {
let timeoutTimer: ReturnType<typeof setTimeout>;
Expand All @@ -79,12 +77,13 @@ export const waitForUserOperationTransactionHash = async <
)({ hash });

if (_userOperationStatus.transactionHash !== null) {
userOperationTransactionHash = { transactionHash: _userOperationStatus.transactionHash };
done(() => emit.resolve({ transactionHash: _userOperationStatus.transactionHash }));
return;
}

if (userOperationTransactionHash) {
done(() => emit.resolve(userOperationTransactionHash));
return;
// The only valid state in which the status doesn not include a tx hash is "not_submitted" or "not_found"
if (_userOperationStatus.status !== "not_submitted" && _userOperationStatus.status !== "not_found") {
done(() => emit.reject("Unexpected transaction status: " + _userOperationStatus.status));
}
} catch (err) {
done(() => emit.reject(err));
Expand Down
10 changes: 9 additions & 1 deletion packages/account-kit/src/steps/finalizing/FinalizingStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useAppAccountClient } from "../../useAppAccountClient";
import { useOnboardingSteps } from "../../useOnboardingSteps";
import { useConfig } from "../../AccountKitProvider";
import CallWithSignatureAbi from "@latticexyz/world-modules/out/IUnstable_CallWithSignatureSystem.sol/IUnstable_CallWithSignatureSystem.abi.json";
import { getAction } from "viem/utils";

export function FinalizingStep() {
const queryClient = useQueryClient();
Expand All @@ -32,11 +33,18 @@ export function FinalizingStep() {
if (!appAccountClient) throw new Error("App account client not ready.");
if (!registerDelegationSignature) throw new Error("No delegation signature.");

// TODO: should this use `callWithSignature`?
console.log("calling registerDelegation");
return await writeContract(appAccountClient, {
return await getAction(
appAccountClient,
writeContract,
"writeContract",
)({
address: worldAddress,
abi: CallWithSignatureAbi,
functionName: "callWithSignature",
account: appAccountClient.account,
chain,
args: [
userAccountClient.account.address,
resourceToHex({ type: "system", namespace: "", name: "Registration" }),
Expand Down
56 changes: 27 additions & 29 deletions packages/account-kit/src/useAppAccountClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,24 @@ export function useAppAccountClient(): UseQueryResult<AppAccountClient> {
pollingInterval: defaultPollingInterval,
}).extend(() => publicActions(publicClient));

const baseMiddleware = {
gasPrice: async () => (await pimlicoBundlerClient.getUserOperationGasPrice()).fast, // use pimlico bundler to get gas prices
} satisfies Middleware;
const gasEstimationStateOverrides = gasTank
? {
// Pimlico's gas estimation runs with high gas limits, which can make the estimation fail if
// the cost would exceed the user's balance.
// We override the user's balance in the paymaster contract and the deposit balance of the
// paymaster in the entry point contract to make the gas estimation succeed.
[gasTank.address]: {
stateDiff: {
[getUserBalanceSlot(userAddress)]: toHex(maxUint256),
},
},
[entryPointAddress]: {
stateDiff: {
[getEntryPointDepositSlot(gasTank.address)]: toHex(maxUint256),
},
},
}
: undefined;

const gasTankMiddleware = gasTank
? ({
Expand All @@ -101,22 +116,7 @@ export function useAppAccountClient(): UseQueryResult<AppAccountClient> {
paymasterData: "0x",
},
},
{
// Pimlico's gas estimation runs with high gas limits, which can make the estimation fail if
// the cost would exceed the user's balance.
// We override the user's balance in the paymaster contract and the deposit balance of the
// paymaster in the entry point contract to make the gas estimation succeed.
[gasTank.address]: {
stateDiff: {
[getUserBalanceSlot(userAddress)]: toHex(maxUint256),
},
},
[entryPointAddress]: {
stateDiff: {
[getEntryPointDepositSlot(gasTank.address)]: toHex(maxUint256),
},
},
},
gasEstimationStateOverrides,
);

return {
Expand All @@ -128,10 +128,7 @@ export function useAppAccountClient(): UseQueryResult<AppAccountClient> {
} satisfies Middleware)
: null;

const middleware = {
...baseMiddleware,
...gasTankMiddleware,
};
const middleware = { ...gasTankMiddleware };

const appAccountClient = createClient({
key: "Account",
Expand All @@ -140,11 +137,11 @@ export function useAppAccountClient(): UseQueryResult<AppAccountClient> {
chain: publicClient.chain,
account: appAccount,
pollingInterval: defaultPollingInterval,
transport: transportObserver("bundler transport", erc4337Config.transport),
transport: transportObserver("app smart account client", erc4337Config.transport),
})
.extend(() => publicActions(publicClient))
.extend((client) => ({
sendTransaction: (args) => {
console.log("bundler hijacked send transaction");
return sendTransaction<Chain, SmartAccount<ENTRYPOINT_ADDRESS_V07_TYPE>, ENTRYPOINT_ADDRESS_V07_TYPE>(
client,
{
Expand All @@ -155,20 +152,21 @@ export function useAppAccountClient(): UseQueryResult<AppAccountClient> {
Chain,
SmartAccount<ENTRYPOINT_ADDRESS_V07_TYPE>
>,
{
estimateFeesPerGas: async () => (await pimlicoBundlerClient.getUserOperationGasPrice()).fast,
},
);
},
}))
.extend(smartAccountActions({ middleware }))
// .extend(transactionQueue({ publicClient }))
.extend(
callFrom({
worldAddress,
delegatorAddress: userAddress,
publicClient,
}),
)
// .extend(writeObserver({ onWrite: (write) => write$.next(write) }))
.extend(() => publicActions(publicClient));
);
// .extend(writeObserver({ onWrite: (write) => write$.next(write) }))

return appAccountClient;
},
Expand Down
1 change: 1 addition & 0 deletions packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"execa": "^7.0.0",
"p-queue": "^7.4.1",
"p-retry": "^5.1.2",
"permissionless": "^0.1.17",
"prettier": "3.2.5",
"prettier-plugin-solidity": "1.3.1",
"viem": "2.9.20"
Expand Down
Loading

0 comments on commit f2f48ad

Please sign in to comment.