Skip to content

Commit 80300b6

Browse files
Expose x-account-salt header to handle multiple smart wallets for single admin wallet (#715)
to test: ```bash cd test/e2e bun test userop --timeout 12000 ``` <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on adding support for `accountSalt` in various transaction-related functionalities, enhancing the ability to predict smart account addresses, and updating the `thirdweb` dependency. ### Detailed summary - Updated `thirdweb` version from `5.61.2` to `5.61.3`. - Added `accountSalt` property in several TypeScript files and schemas. - Modified functions to handle `accountSalt` in transaction processing. - Updated tests to include scenarios involving `accountSalt`. - Adjusted import statements for consistency. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 17245f7 commit 80300b6

File tree

16 files changed

+148
-59
lines changed

16 files changed

+148
-59
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
"prom-client": "^15.1.3",
7575
"prool": "^0.0.16",
7676
"superjson": "^2.2.1",
77-
"thirdweb": "5.61.2",
77+
"thirdweb": "5.61.3",
7878
"uuid": "^9.0.1",
7979
"winston": "^3.14.1",
8080
"zod": "^3.23.8"

src/server/routes/contract/extensions/accountFactory/read/predictAccountAddress.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { Static, Type } from "@sinclair/typebox";
2-
import { FastifyInstance } from "fastify";
1+
import { type Static, Type } from "@sinclair/typebox";
2+
import type { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
4-
import { getContract } from "../../../../../../utils/cache/getContract";
4+
import { predictAddress } from "thirdweb/wallets/smart";
5+
import { getContractV5 } from "../../../../../../utils/cache/getContractv5";
56
import { AddressSchema } from "../../../../../schemas/address";
67
import {
78
contractParamSchema,
@@ -22,7 +23,8 @@ const QuerySchema = Type.Object({
2223
},
2324
extraData: Type.Optional(
2425
Type.String({
25-
description: "Extra data to add to use in predicting the account address",
26+
description:
27+
"Extra data (account salt) to add to use in predicting the account address",
2628
}),
2729
),
2830
});
@@ -51,16 +53,16 @@ export const predictAccountAddress = async (fastify: FastifyInstance) => {
5153
const { chain, contractAddress } = request.params;
5254
const { adminAddress, extraData } = request.query;
5355
const chainId = await getChainIdFromChain(chain);
54-
55-
const contract = await getContract({
56+
const factoryContract = await getContractV5({
5657
chainId,
5758
contractAddress,
5859
});
59-
const accountAddress =
60-
await contract.accountFactory.predictAccountAddress(
61-
adminAddress,
62-
extraData,
63-
);
60+
61+
const accountAddress = await predictAddress({
62+
factoryContract,
63+
adminAddress,
64+
accountSalt: extraData,
65+
});
6466

6567
reply.status(StatusCodes.OK).send({
6668
result: accountAddress,

src/server/routes/contract/extensions/accountFactory/write/createAccount.ts

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { Static, Type } from "@sinclair/typebox";
2-
import { FastifyInstance } from "fastify";
1+
import { type Static, Type } from "@sinclair/typebox";
2+
import type { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
4-
import { Address } from "thirdweb";
5-
import { queueTx } from "../../../../../../db/transactions/queueTx";
6-
import { getContract } from "../../../../../../utils/cache/getContract";
4+
import { createAccount as factoryCreateAccount } from "thirdweb/extensions/erc4337";
5+
import { isHex, stringToHex } from "thirdweb/utils";
6+
import { predictAddress } from "thirdweb/wallets/smart";
7+
import { getContractV5 } from "../../../../../../utils/cache/getContractv5";
78
import { redis } from "../../../../../../utils/redis/redis";
9+
import { queueTransaction } from "../../../../../../utils/transaction/queueTransation";
810
import { AddressSchema } from "../../../../../schemas/address";
911
import { prebuiltDeployResponseSchema } from "../../../../../schemas/prebuilts";
1012
import {
@@ -13,7 +15,11 @@ import {
1315
standardResponseSchema,
1416
} from "../../../../../schemas/sharedApiSchemas";
1517
import { txOverridesWithValueSchema } from "../../../../../schemas/txOverrides";
16-
import { walletWithAAHeaderSchema } from "../../../../../schemas/wallet";
18+
import {
19+
maybeAddress,
20+
requiredAddress,
21+
walletWithAAHeaderSchema,
22+
} from "../../../../../schemas/wallet";
1723
import { getChainIdFromChain } from "../../../../../utils/chain";
1824

1925
const requestBodySchema = Type.Object({
@@ -63,37 +69,53 @@ export const createAccount = async (fastify: FastifyInstance) => {
6369
const { simulateTx } = request.query;
6470
const { adminAddress, extraData, txOverrides } = request.body;
6571
const {
66-
"x-backend-wallet-address": walletAddress,
72+
"x-backend-wallet-address": fromAddress,
6773
"x-account-address": accountAddress,
74+
"x-account-factory-address": accountFactoryAddress,
75+
"x-account-salt": accountSalt,
6876
"x-idempotency-key": idempotencyKey,
6977
} = request.headers as Static<typeof walletWithAAHeaderSchema>;
7078
const chainId = await getChainIdFromChain(chain);
7179

72-
const contract = await getContract({
80+
const factoryContract = await getContractV5({
7381
chainId,
74-
walletAddress,
7582
contractAddress,
76-
accountAddress,
7783
});
78-
const tx = await contract.accountFactory.createAccount.prepare(
84+
85+
const deployedAddress = await predictAddress({
86+
factoryContract,
7987
adminAddress,
80-
extraData,
81-
);
82-
const deployedAddress =
83-
(await contract.accountFactory.predictAccountAddress(
84-
adminAddress,
85-
extraData,
86-
)) as Address;
88+
accountSalt: extraData,
89+
});
8790

88-
const queueId = await queueTx({
89-
tx,
90-
chainId,
91-
simulateTx,
92-
extension: "account-factory",
93-
deployedContractAddress: deployedAddress,
94-
deployedContractType: "account",
95-
idempotencyKey,
91+
// if extraData is not a hex string, convert it to a hex string
92+
// this is the same transformation that is done in the SDK
93+
// for predictAddress and createAndSignUserOp
94+
// but needed here because we're calling the raw autogenerated abi function
95+
const accountSaltHex =
96+
extraData && isHex(extraData)
97+
? extraData
98+
: stringToHex(extraData ?? "");
99+
100+
const transaction = factoryCreateAccount({
101+
contract: factoryContract,
102+
admin: adminAddress,
103+
data: accountSaltHex,
104+
});
105+
106+
const queueId = await queueTransaction({
107+
transaction,
108+
fromAddress: requiredAddress(fromAddress, "x-backend-wallet-address"),
109+
toAddress: maybeAddress(contractAddress, "to"),
110+
accountAddress: maybeAddress(accountAddress, "x-account-address"),
111+
accountFactoryAddress: maybeAddress(
112+
accountFactoryAddress,
113+
"x-account-factory-address",
114+
),
115+
accountSalt,
96116
txOverrides,
117+
idempotencyKey,
118+
shouldSimulate: simulateTx,
97119
});
98120

99121
// Note: This is a temporary solution to cache the deployed address's factory for 7 days.

src/server/routes/contract/extensions/erc1155/write/claimTo.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Type, type Static } from "@sinclair/typebox";
1+
import { type Static, Type } from "@sinclair/typebox";
22
import type { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
44
import type { Address } from "thirdweb";
@@ -76,6 +76,7 @@ export async function erc1155claimTo(fastify: FastifyInstance) {
7676
"x-account-address": accountAddress,
7777
"x-idempotency-key": idempotencyKey,
7878
"x-account-factory-address": accountFactoryAddress,
79+
"x-account-salt": accountSalt,
7980
} = request.headers as Static<typeof walletWithAAHeaderSchema>;
8081

8182
const chainId = await getChainIdFromChain(chain);
@@ -101,6 +102,7 @@ export async function erc1155claimTo(fastify: FastifyInstance) {
101102
accountFactoryAddress,
102103
"x-account-factory-address",
103104
),
105+
accountSalt,
104106
txOverrides,
105107
idempotencyKey,
106108
shouldSimulate: simulateTx,

src/server/routes/contract/extensions/erc20/write/claimTo.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Type, type Static } from "@sinclair/typebox";
1+
import { type Static, Type } from "@sinclair/typebox";
22
import type { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
44
import type { Address } from "thirdweb";
@@ -71,6 +71,7 @@ export async function erc20claimTo(fastify: FastifyInstance) {
7171
"x-account-address": accountAddress,
7272
"x-idempotency-key": idempotencyKey,
7373
"x-account-factory-address": accountFactoryAddress,
74+
"x-account-salt": accountSalt,
7475
} = request.headers as Static<typeof walletWithAAHeaderSchema>;
7576

7677
const chainId = await getChainIdFromChain(chain);
@@ -94,6 +95,7 @@ export async function erc20claimTo(fastify: FastifyInstance) {
9495
accountFactoryAddress,
9596
"x-account-factory-address",
9697
),
98+
accountSalt,
9799
txOverrides,
98100
idempotencyKey,
99101
shouldSimulate: simulateTx,

src/server/routes/contract/extensions/erc721/write/claimTo.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Type, type Static } from "@sinclair/typebox";
1+
import { type Static, Type } from "@sinclair/typebox";
22
import type { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
44
import type { Address } from "thirdweb";
@@ -72,6 +72,7 @@ export async function erc721claimTo(fastify: FastifyInstance) {
7272
"x-account-address": accountAddress,
7373
"x-idempotency-key": idempotencyKey,
7474
"x-account-factory-address": accountFactoryAddress,
75+
"x-account-salt": accountSalt,
7576
} = request.headers as Static<typeof walletWithAAHeaderSchema>;
7677

7778
const chainId = await getChainIdFromChain(chain);
@@ -95,6 +96,7 @@ export async function erc721claimTo(fastify: FastifyInstance) {
9596
accountFactoryAddress,
9697
"x-account-factory-address",
9798
),
99+
accountSalt,
98100
txOverrides,
99101
idempotencyKey,
100102
shouldSimulate: simulateTx,

src/server/routes/contract/write/write.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Type, type Static } from "@sinclair/typebox";
1+
import { type Static, Type } from "@sinclair/typebox";
22
import type { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
44
import { prepareContractCall, resolveMethod } from "thirdweb";
@@ -67,6 +67,7 @@ export async function writeToContract(fastify: FastifyInstance) {
6767
"x-account-address": accountAddress,
6868
"x-idempotency-key": idempotencyKey,
6969
"x-account-factory-address": accountFactoryAddress,
70+
"x-account-salt": accountSalt,
7071
} = request.headers as Static<typeof walletWithAAHeaderSchema>;
7172

7273
const chainId = await getChainIdFromChain(chain);
@@ -107,6 +108,7 @@ export async function writeToContract(fastify: FastifyInstance) {
107108
accountFactoryAddress,
108109
"x-account-factory-address",
109110
),
111+
accountSalt,
110112
txOverrides,
111113
idempotencyKey,
112114
shouldSimulate: simulateTx,

src/server/schemas/transaction/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Type, type Static } from "@sinclair/typebox";
1+
import { type Static, Type } from "@sinclair/typebox";
22
import type { Hex } from "thirdweb";
33
import { stringify } from "thirdweb/utils";
44
import type { AnyTransaction } from "../../../utils/transaction/types";
@@ -178,6 +178,8 @@ export const TransactionSchema = Type.Object({
178178
]),
179179
signerAddress: Type.Union([AddressSchema, Type.Null()]),
180180
accountAddress: Type.Union([AddressSchema, Type.Null()]),
181+
accountSalt: Type.Union([Type.String(), Type.Null()]),
182+
accountFactoryAddress: Type.Union([AddressSchema, Type.Null()]),
181183
target: Type.Union([AddressSchema, Type.Null()]),
182184
sender: Type.Union([AddressSchema, Type.Null()]),
183185
initCode: Type.Union([Type.String(), Type.Null()]),
@@ -333,6 +335,8 @@ export const toTransactionSchema = (
333335
// User Operation
334336
signerAddress: transaction.from,
335337
accountAddress: transaction.accountAddress ?? null,
338+
accountSalt: transaction.accountSalt ?? null,
339+
accountFactoryAddress: transaction.accountFactoryAddress ?? null,
336340
target: transaction.target ?? null,
337341
sender: transaction.sender ?? null,
338342
initCode: null,

src/server/schemas/wallet/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Type } from "@sinclair/typebox";
2-
import { getAddress, type Address } from "thirdweb";
2+
import { type Address, getAddress } from "thirdweb";
33
import { env } from "../../../utils/env";
44
import { badAddressError } from "../../middleware/error";
55
import { AddressSchema } from "../address";
@@ -27,6 +27,12 @@ export const walletWithAAHeaderSchema = Type.Object({
2727
description:
2828
"Smart account factory address. If omitted, engine will try to resolve it from the chain.",
2929
}),
30+
"x-account-salt": Type.Optional(
31+
Type.String({
32+
description:
33+
"Smart account salt as string or hex. This is used to predict the smart account address. Useful when creating multiple accounts with the same admin and only needed when deploying the account as part of a userop.",
34+
}),
35+
),
3036
});
3137

3238
/**

src/utils/transaction/insertTransaction.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const insertTransaction = async (
4747
to: getChecksumAddress(insertedTransaction.to),
4848
signerAddress: getChecksumAddress(insertedTransaction.signerAddress),
4949
accountAddress: getChecksumAddress(insertedTransaction.accountAddress),
50+
accountSalt: insertedTransaction.accountSalt,
5051
target: getChecksumAddress(insertedTransaction.target),
5152
sender: getChecksumAddress(insertedTransaction.sender),
5253
value: insertedTransaction.value ?? 0n,

src/utils/transaction/queueTransation.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type QueuedTransactionParams = {
1919
toAddress: Address | undefined;
2020
accountAddress: Address | undefined;
2121
accountFactoryAddress: Address | undefined;
22+
accountSalt: string | undefined;
2223
txOverrides?: Static<
2324
typeof txOverridesWithValueSchema.properties.txOverrides
2425
>;
@@ -33,6 +34,7 @@ export async function queueTransaction(args: QueuedTransactionParams) {
3334
toAddress,
3435
accountAddress,
3536
accountFactoryAddress,
37+
accountSalt,
3638
txOverrides,
3739
idempotencyKey,
3840
shouldSimulate,
@@ -64,6 +66,7 @@ export async function queueTransaction(args: QueuedTransactionParams) {
6466
signerAddress: fromAddress,
6567
target: toAddress,
6668
accountFactoryAddress,
69+
accountSalt,
6770
}
6871
: { isUserOp: false }),
6972
};

src/utils/transaction/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export type InsertedTransaction = {
4141
// User Operation
4242
signerAddress?: Address;
4343
accountAddress?: Address;
44+
accountSalt?: string;
4445
accountFactoryAddress?: Address;
4546
target?: Address;
4647
sender?: Address;

src/utils/transaction/webhook.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { WebhooksEventTypes } from "../../schema/webhooks";
22
import { SendWebhookQueue } from "../../worker/queues/sendWebhookQueue";
3-
import { AnyTransaction } from "./types";
3+
import type { AnyTransaction } from "./types";
44

55
export const enqueueTransactionWebhook = async (
66
transaction: AnyTransaction,
@@ -10,12 +10,12 @@ export const enqueueTransactionWebhook = async (
1010
status === "sent"
1111
? WebhooksEventTypes.SENT_TX
1212
: status === "mined"
13-
? WebhooksEventTypes.MINED_TX
14-
: status === "cancelled"
15-
? WebhooksEventTypes.CANCELLED_TX
16-
: status === "errored"
17-
? WebhooksEventTypes.ERRORED_TX
18-
: null;
13+
? WebhooksEventTypes.MINED_TX
14+
: status === "cancelled"
15+
? WebhooksEventTypes.CANCELLED_TX
16+
: status === "errored"
17+
? WebhooksEventTypes.ERRORED_TX
18+
: null;
1919
if (type) {
2020
await SendWebhookQueue.enqueueWebhook({ type, queueId });
2121
}

0 commit comments

Comments
 (0)