Skip to content

Commit

Permalink
Expose x-account-salt header to handle multiple smart wallets for s…
Browse files Browse the repository at this point in the history
…ingle 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 -->
  • Loading branch information
joaquim-verges authored Oct 11, 2024
1 parent 17245f7 commit 80300b6
Show file tree
Hide file tree
Showing 16 changed files with 148 additions and 59 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"prom-client": "^15.1.3",
"prool": "^0.0.16",
"superjson": "^2.2.1",
"thirdweb": "5.61.2",
"thirdweb": "5.61.3",
"uuid": "^9.0.1",
"winston": "^3.14.1",
"zod": "^3.23.8"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Static, Type } from "@sinclair/typebox";
import { FastifyInstance } from "fastify";
import { type Static, Type } from "@sinclair/typebox";
import type { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { getContract } from "../../../../../../utils/cache/getContract";
import { predictAddress } from "thirdweb/wallets/smart";
import { getContractV5 } from "../../../../../../utils/cache/getContractv5";
import { AddressSchema } from "../../../../../schemas/address";
import {
contractParamSchema,
Expand All @@ -22,7 +23,8 @@ const QuerySchema = Type.Object({
},
extraData: Type.Optional(
Type.String({
description: "Extra data to add to use in predicting the account address",
description:
"Extra data (account salt) to add to use in predicting the account address",
}),
),
});
Expand Down Expand Up @@ -51,16 +53,16 @@ export const predictAccountAddress = async (fastify: FastifyInstance) => {
const { chain, contractAddress } = request.params;
const { adminAddress, extraData } = request.query;
const chainId = await getChainIdFromChain(chain);

const contract = await getContract({
const factoryContract = await getContractV5({
chainId,
contractAddress,
});
const accountAddress =
await contract.accountFactory.predictAccountAddress(
adminAddress,
extraData,
);

const accountAddress = await predictAddress({
factoryContract,
adminAddress,
accountSalt: extraData,
});

reply.status(StatusCodes.OK).send({
result: accountAddress,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Static, Type } from "@sinclair/typebox";
import { FastifyInstance } from "fastify";
import { type Static, Type } from "@sinclair/typebox";
import type { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { Address } from "thirdweb";
import { queueTx } from "../../../../../../db/transactions/queueTx";
import { getContract } from "../../../../../../utils/cache/getContract";
import { createAccount as factoryCreateAccount } from "thirdweb/extensions/erc4337";
import { isHex, stringToHex } from "thirdweb/utils";
import { predictAddress } from "thirdweb/wallets/smart";
import { getContractV5 } from "../../../../../../utils/cache/getContractv5";
import { redis } from "../../../../../../utils/redis/redis";
import { queueTransaction } from "../../../../../../utils/transaction/queueTransation";
import { AddressSchema } from "../../../../../schemas/address";
import { prebuiltDeployResponseSchema } from "../../../../../schemas/prebuilts";
import {
Expand All @@ -13,7 +15,11 @@ import {
standardResponseSchema,
} from "../../../../../schemas/sharedApiSchemas";
import { txOverridesWithValueSchema } from "../../../../../schemas/txOverrides";
import { walletWithAAHeaderSchema } from "../../../../../schemas/wallet";
import {
maybeAddress,
requiredAddress,
walletWithAAHeaderSchema,
} from "../../../../../schemas/wallet";
import { getChainIdFromChain } from "../../../../../utils/chain";

const requestBodySchema = Type.Object({
Expand Down Expand Up @@ -63,37 +69,53 @@ export const createAccount = async (fastify: FastifyInstance) => {
const { simulateTx } = request.query;
const { adminAddress, extraData, txOverrides } = request.body;
const {
"x-backend-wallet-address": walletAddress,
"x-backend-wallet-address": fromAddress,
"x-account-address": accountAddress,
"x-account-factory-address": accountFactoryAddress,
"x-account-salt": accountSalt,
"x-idempotency-key": idempotencyKey,
} = request.headers as Static<typeof walletWithAAHeaderSchema>;
const chainId = await getChainIdFromChain(chain);

const contract = await getContract({
const factoryContract = await getContractV5({
chainId,
walletAddress,
contractAddress,
accountAddress,
});
const tx = await contract.accountFactory.createAccount.prepare(

const deployedAddress = await predictAddress({
factoryContract,
adminAddress,
extraData,
);
const deployedAddress =
(await contract.accountFactory.predictAccountAddress(
adminAddress,
extraData,
)) as Address;
accountSalt: extraData,
});

const queueId = await queueTx({
tx,
chainId,
simulateTx,
extension: "account-factory",
deployedContractAddress: deployedAddress,
deployedContractType: "account",
idempotencyKey,
// if extraData is not a hex string, convert it to a hex string
// this is the same transformation that is done in the SDK
// for predictAddress and createAndSignUserOp
// but needed here because we're calling the raw autogenerated abi function
const accountSaltHex =
extraData && isHex(extraData)
? extraData
: stringToHex(extraData ?? "");

const transaction = factoryCreateAccount({
contract: factoryContract,
admin: adminAddress,
data: accountSaltHex,
});

const queueId = await queueTransaction({
transaction,
fromAddress: requiredAddress(fromAddress, "x-backend-wallet-address"),
toAddress: maybeAddress(contractAddress, "to"),
accountAddress: maybeAddress(accountAddress, "x-account-address"),
accountFactoryAddress: maybeAddress(
accountFactoryAddress,
"x-account-factory-address",
),
accountSalt,
txOverrides,
idempotencyKey,
shouldSimulate: simulateTx,
});

// Note: This is a temporary solution to cache the deployed address's factory for 7 days.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Type, type Static } from "@sinclair/typebox";
import { type Static, Type } from "@sinclair/typebox";
import type { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import type { Address } from "thirdweb";
Expand Down Expand Up @@ -76,6 +76,7 @@ export async function erc1155claimTo(fastify: FastifyInstance) {
"x-account-address": accountAddress,
"x-idempotency-key": idempotencyKey,
"x-account-factory-address": accountFactoryAddress,
"x-account-salt": accountSalt,
} = request.headers as Static<typeof walletWithAAHeaderSchema>;

const chainId = await getChainIdFromChain(chain);
Expand All @@ -101,6 +102,7 @@ export async function erc1155claimTo(fastify: FastifyInstance) {
accountFactoryAddress,
"x-account-factory-address",
),
accountSalt,
txOverrides,
idempotencyKey,
shouldSimulate: simulateTx,
Expand Down
4 changes: 3 additions & 1 deletion src/server/routes/contract/extensions/erc20/write/claimTo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Type, type Static } from "@sinclair/typebox";
import { type Static, Type } from "@sinclair/typebox";
import type { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import type { Address } from "thirdweb";
Expand Down Expand Up @@ -71,6 +71,7 @@ export async function erc20claimTo(fastify: FastifyInstance) {
"x-account-address": accountAddress,
"x-idempotency-key": idempotencyKey,
"x-account-factory-address": accountFactoryAddress,
"x-account-salt": accountSalt,
} = request.headers as Static<typeof walletWithAAHeaderSchema>;

const chainId = await getChainIdFromChain(chain);
Expand All @@ -94,6 +95,7 @@ export async function erc20claimTo(fastify: FastifyInstance) {
accountFactoryAddress,
"x-account-factory-address",
),
accountSalt,
txOverrides,
idempotencyKey,
shouldSimulate: simulateTx,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Type, type Static } from "@sinclair/typebox";
import { type Static, Type } from "@sinclair/typebox";
import type { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import type { Address } from "thirdweb";
Expand Down Expand Up @@ -72,6 +72,7 @@ export async function erc721claimTo(fastify: FastifyInstance) {
"x-account-address": accountAddress,
"x-idempotency-key": idempotencyKey,
"x-account-factory-address": accountFactoryAddress,
"x-account-salt": accountSalt,
} = request.headers as Static<typeof walletWithAAHeaderSchema>;

const chainId = await getChainIdFromChain(chain);
Expand All @@ -95,6 +96,7 @@ export async function erc721claimTo(fastify: FastifyInstance) {
accountFactoryAddress,
"x-account-factory-address",
),
accountSalt,
txOverrides,
idempotencyKey,
shouldSimulate: simulateTx,
Expand Down
4 changes: 3 additions & 1 deletion src/server/routes/contract/write/write.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Type, type Static } from "@sinclair/typebox";
import { type Static, Type } from "@sinclair/typebox";
import type { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { prepareContractCall, resolveMethod } from "thirdweb";
Expand Down Expand Up @@ -67,6 +67,7 @@ export async function writeToContract(fastify: FastifyInstance) {
"x-account-address": accountAddress,
"x-idempotency-key": idempotencyKey,
"x-account-factory-address": accountFactoryAddress,
"x-account-salt": accountSalt,
} = request.headers as Static<typeof walletWithAAHeaderSchema>;

const chainId = await getChainIdFromChain(chain);
Expand Down Expand Up @@ -107,6 +108,7 @@ export async function writeToContract(fastify: FastifyInstance) {
accountFactoryAddress,
"x-account-factory-address",
),
accountSalt,
txOverrides,
idempotencyKey,
shouldSimulate: simulateTx,
Expand Down
6 changes: 5 additions & 1 deletion src/server/schemas/transaction/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Type, type Static } from "@sinclair/typebox";
import { type Static, Type } from "@sinclair/typebox";
import type { Hex } from "thirdweb";
import { stringify } from "thirdweb/utils";
import type { AnyTransaction } from "../../../utils/transaction/types";
Expand Down Expand Up @@ -178,6 +178,8 @@ export const TransactionSchema = Type.Object({
]),
signerAddress: Type.Union([AddressSchema, Type.Null()]),
accountAddress: Type.Union([AddressSchema, Type.Null()]),
accountSalt: Type.Union([Type.String(), Type.Null()]),
accountFactoryAddress: Type.Union([AddressSchema, Type.Null()]),
target: Type.Union([AddressSchema, Type.Null()]),
sender: Type.Union([AddressSchema, Type.Null()]),
initCode: Type.Union([Type.String(), Type.Null()]),
Expand Down Expand Up @@ -333,6 +335,8 @@ export const toTransactionSchema = (
// User Operation
signerAddress: transaction.from,
accountAddress: transaction.accountAddress ?? null,
accountSalt: transaction.accountSalt ?? null,
accountFactoryAddress: transaction.accountFactoryAddress ?? null,
target: transaction.target ?? null,
sender: transaction.sender ?? null,
initCode: null,
Expand Down
8 changes: 7 additions & 1 deletion src/server/schemas/wallet/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Type } from "@sinclair/typebox";
import { getAddress, type Address } from "thirdweb";
import { type Address, getAddress } from "thirdweb";
import { env } from "../../../utils/env";
import { badAddressError } from "../../middleware/error";
import { AddressSchema } from "../address";
Expand Down Expand Up @@ -27,6 +27,12 @@ export const walletWithAAHeaderSchema = Type.Object({
description:
"Smart account factory address. If omitted, engine will try to resolve it from the chain.",
}),
"x-account-salt": Type.Optional(
Type.String({
description:
"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.",
}),
),
});

/**
Expand Down
1 change: 1 addition & 0 deletions src/utils/transaction/insertTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const insertTransaction = async (
to: getChecksumAddress(insertedTransaction.to),
signerAddress: getChecksumAddress(insertedTransaction.signerAddress),
accountAddress: getChecksumAddress(insertedTransaction.accountAddress),
accountSalt: insertedTransaction.accountSalt,
target: getChecksumAddress(insertedTransaction.target),
sender: getChecksumAddress(insertedTransaction.sender),
value: insertedTransaction.value ?? 0n,
Expand Down
3 changes: 3 additions & 0 deletions src/utils/transaction/queueTransation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type QueuedTransactionParams = {
toAddress: Address | undefined;
accountAddress: Address | undefined;
accountFactoryAddress: Address | undefined;
accountSalt: string | undefined;
txOverrides?: Static<
typeof txOverridesWithValueSchema.properties.txOverrides
>;
Expand All @@ -33,6 +34,7 @@ export async function queueTransaction(args: QueuedTransactionParams) {
toAddress,
accountAddress,
accountFactoryAddress,
accountSalt,
txOverrides,
idempotencyKey,
shouldSimulate,
Expand Down Expand Up @@ -64,6 +66,7 @@ export async function queueTransaction(args: QueuedTransactionParams) {
signerAddress: fromAddress,
target: toAddress,
accountFactoryAddress,
accountSalt,
}
: { isUserOp: false }),
};
Expand Down
1 change: 1 addition & 0 deletions src/utils/transaction/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type InsertedTransaction = {
// User Operation
signerAddress?: Address;
accountAddress?: Address;
accountSalt?: string;
accountFactoryAddress?: Address;
target?: Address;
sender?: Address;
Expand Down
14 changes: 7 additions & 7 deletions src/utils/transaction/webhook.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { WebhooksEventTypes } from "../../schema/webhooks";
import { SendWebhookQueue } from "../../worker/queues/sendWebhookQueue";
import { AnyTransaction } from "./types";
import type { AnyTransaction } from "./types";

export const enqueueTransactionWebhook = async (
transaction: AnyTransaction,
Expand All @@ -10,12 +10,12 @@ export const enqueueTransactionWebhook = async (
status === "sent"
? WebhooksEventTypes.SENT_TX
: status === "mined"
? WebhooksEventTypes.MINED_TX
: status === "cancelled"
? WebhooksEventTypes.CANCELLED_TX
: status === "errored"
? WebhooksEventTypes.ERRORED_TX
: null;
? WebhooksEventTypes.MINED_TX
: status === "cancelled"
? WebhooksEventTypes.CANCELLED_TX
: status === "errored"
? WebhooksEventTypes.ERRORED_TX
: null;
if (type) {
await SendWebhookQueue.enqueueWebhook({ type, queueId });
}
Expand Down
Loading

0 comments on commit 80300b6

Please sign in to comment.