From 9a0c3079379cf1840c414e79a377d3f0f20ec93a Mon Sep 17 00:00:00 2001 From: Prithvish Baidya Date: Tue, 10 Jun 2025 04:58:41 +0530 Subject: [PATCH 1/2] transfer route fixes + experimental mine worker polling interval env vars --- src/server/routes/backend-wallet/transfer.ts | 73 ++++++++++++-------- src/shared/utils/env.ts | 16 +++++ src/worker/tasks/mine-transaction-worker.ts | 12 +++- 3 files changed, 72 insertions(+), 29 deletions(-) diff --git a/src/server/routes/backend-wallet/transfer.ts b/src/server/routes/backend-wallet/transfer.ts index 2c3c7d765..2610f4480 100644 --- a/src/server/routes/backend-wallet/transfer.ts +++ b/src/server/routes/backend-wallet/transfer.ts @@ -11,9 +11,13 @@ import { import { transfer as transferERC20 } from "thirdweb/extensions/erc20"; import { isContractDeployed, resolvePromisedValue } from "thirdweb/utils"; import { getChain } from "../../../shared/utils/chain"; -import { normalizeAddress } from "../../../shared/utils/primitive-types"; +import { + getChecksumAddress, + normalizeAddress, +} from "../../../shared/utils/primitive-types"; import { thirdwebClient } from "../../../shared/utils/sdk"; import { insertTransaction } from "../../../shared/utils/transaction/insert-transaction"; +import { queueTransaction } from "../../../shared/utils/transaction/queue-transation"; import type { InsertedTransaction } from "../../../shared/utils/transaction/types"; import { createCustomError } from "../../middleware/error"; import { AddressSchema } from "../../schemas/address"; @@ -25,7 +29,7 @@ import { } from "../../schemas/shared-api-schemas"; import { txOverridesWithValueSchema } from "../../schemas/tx-overrides"; import { - walletHeaderSchema, + walletWithAAHeaderSchema, walletWithAddressParamSchema, } from "../../schemas/wallet"; import { getChainIdFromChain } from "../../utils/chain"; @@ -70,7 +74,7 @@ export async function transfer(fastify: FastifyInstance) { operationId: "transfer", params: requestSchema, body: requestBodySchema, - headers: walletHeaderSchema, + headers: walletWithAAHeaderSchema, querystring: requestQuerystringSchema, response: { ...standardResponseSchema, @@ -88,31 +92,50 @@ export async function transfer(fastify: FastifyInstance) { const { "x-backend-wallet-address": walletAddress, "x-idempotency-key": idempotencyKey, + "x-account-address": accountAddress, + "x-account-factory-address": accountFactoryAddress, + "x-account-salt": accountSalt, "x-transaction-mode": transactionMode, - } = request.headers as Static; + } = request.headers as Static; const { simulateTx: shouldSimulate } = request.query; // Resolve inputs. const currencyAddress = normalizeAddress(_currencyAddress); const chainId = await getChainIdFromChain(chain); - let insertedTransaction: InsertedTransaction; + let queueId: string; if ( currencyAddress === ZERO_ADDRESS || currencyAddress === NATIVE_TOKEN_ADDRESS ) { - insertedTransaction = { - isUserOp: false, + // Native token transfer - use insertTransaction directly + const insertedTransaction: InsertedTransaction = { chainId, from: walletAddress as Address, to: to as Address, data: "0x", value: toWei(amount), - extension: "none", - functionName: "transfer", transactionMode, ...parseTransactionOverrides(txOverrides), + ...(accountAddress + ? { + isUserOp: true, + accountAddress: getChecksumAddress(accountAddress), + signerAddress: getChecksumAddress(walletAddress), + target: getChecksumAddress(to), + accountFactoryAddress: getChecksumAddress( + accountFactoryAddress, + ), + accountSalt, + } + : { isUserOp: false }), }; + + queueId = await insertTransaction({ + insertedTransaction, + idempotencyKey, + shouldSimulate, + }); } else { const contract = getContract({ client: thirdwebClient, @@ -131,31 +154,25 @@ export async function transfer(fastify: FastifyInstance) { ); } + // ERC20 token transfer - use queueTransaction with PreparedTransaction const transaction = transferERC20({ contract, to, amount }); - insertedTransaction = { - isUserOp: false, - chainId, - from: walletAddress as Address, - to: (await resolvePromisedValue(transaction.to)) as - | Address - | undefined, - data: await resolvePromisedValue(transaction.data), - value: 0n, - extension: "erc20", + queueId = await queueTransaction({ + transaction, + fromAddress: getChecksumAddress(walletAddress), + toAddress: getChecksumAddress(to), + accountAddress: getChecksumAddress(accountAddress), + accountFactoryAddress: getChecksumAddress(accountFactoryAddress), + accountSalt, + txOverrides, + idempotencyKey, + shouldSimulate, functionName: "transfer", - functionArgs: [to, amount, currencyAddress], + extension: "erc20", transactionMode, - ...parseTransactionOverrides(txOverrides), - }; + }); } - const queueId = await insertTransaction({ - insertedTransaction, - idempotencyKey, - shouldSimulate, - }); - reply.status(StatusCodes.OK).send({ result: { queueId, diff --git a/src/shared/utils/env.ts b/src/shared/utils/env.ts index a81a7c9a5..4d2f57346 100644 --- a/src/shared/utils/env.ts +++ b/src/shared/utils/env.ts @@ -103,6 +103,16 @@ export const env = createEnv({ .default(30 * 60), // Sets the max gas price for a transaction attempt. Most RPCs reject transactions above a certain gas price. Default: 10^18 wei. EXPERIMENTAL__MAX_GAS_PRICE_WEI: z.coerce.number().default(10 ** 18), + EXPERIMENTAL__MINE_WORKER_BASE_POLL_INTERVAL_SECONDS: z.coerce + .number() + .default(2), + EXPERIMENTAL__MINE_WORKER_MAX_POLL_INTERVAL_SECONDS: z.coerce + .number() + .default(20), + EXPERIMENTAL__MINE_WORKER_POLL_INTERVAL_SCALING_FACTOR: z.coerce + .number() + .gt(0.0, "scaling factor must be greater than 0") + .default(1.0), }, clientPrefix: "NEVER_USED", client: {}, @@ -151,6 +161,12 @@ export const env = createEnv({ CUSTOM_HMAC_AUTH_CLIENT_ID: process.env.CUSTOM_HMAC_AUTH_CLIENT_ID, CUSTOM_HMAC_AUTH_CLIENT_SECRET: process.env.CUSTOM_HMAC_AUTH_CLIENT_SECRET, ACCOUNT_CACHE_SIZE: process.env.ACCOUNT_CAHCE_SIZE, + EXPERIMENTAL__MINE_WORKER_BASE_POLL_INTERVAL_SECONDS: + process.env.EXPERIMENTAL__MINE_WORKER_BASE_POLL_INTERVAL_SECONDS, + EXPERIMENTAL__MINE_WORKER_MAX_POLL_INTERVAL_SECONDS: + process.env.EXPERIMENTAL__MINE_WORKER_MAX_POLL_INTERVAL_SECONDS, + EXPERIMENTAL__MINE_WORKER_POLL_INTERVAL_SCALING_FACTOR: + process.env.EXPERIMENTAL__MINE_WORKER_POLL_INTERVAL_SCALING_FACTOR, }, onValidationError: (error: ZodError) => { console.error( diff --git a/src/worker/tasks/mine-transaction-worker.ts b/src/worker/tasks/mine-transaction-worker.ts index 69d8b0184..7954b2633 100644 --- a/src/worker/tasks/mine-transaction-worker.ts +++ b/src/worker/tasks/mine-transaction-worker.ts @@ -309,6 +309,13 @@ const _notifyIfLowBalance = async (transaction: MinedTransaction) => { } }; +const SCALING_FACTOR = + env.EXPERIMENTAL__MINE_WORKER_POLL_INTERVAL_SCALING_FACTOR; +const MAX_POLL_INTERVAL_MS = + env.EXPERIMENTAL__MINE_WORKER_MAX_POLL_INTERVAL_SECONDS * 1000; +const BASE_POLL_INTERVAL_MS = + env.EXPERIMENTAL__MINE_WORKER_BASE_POLL_INTERVAL_SECONDS * 1000; + // Must be explicitly called for the worker to run on this host. export const initMineTransactionWorker = () => { const _worker = new Worker(MineTransactionQueue.q.name, handler, { @@ -317,7 +324,10 @@ export const initMineTransactionWorker = () => { settings: { backoffStrategy: (attemptsMade: number) => { // Retries at 2s, 4s, 6s, ..., 18s, 20s, 20s, 20s, ... - return Math.min(attemptsMade * 2_000, 20_000); + return Math.min( + attemptsMade * BASE_POLL_INTERVAL_MS * SCALING_FACTOR, // 2_000 default * 1.0 = 2_000 + MAX_POLL_INTERVAL_MS, // 20_000 default + ); }, }, }); From 63b48ef5dedc9ba66c8da62c1ee966b779706010 Mon Sep 17 00:00:00 2001 From: Prithvish Baidya Date: Tue, 10 Jun 2025 05:03:53 +0530 Subject: [PATCH 2/2] lint --- src/server/routes/backend-wallet/transfer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/routes/backend-wallet/transfer.ts b/src/server/routes/backend-wallet/transfer.ts index 2610f4480..a3b0b0f5b 100644 --- a/src/server/routes/backend-wallet/transfer.ts +++ b/src/server/routes/backend-wallet/transfer.ts @@ -9,7 +9,7 @@ import { type Address, } from "thirdweb"; import { transfer as transferERC20 } from "thirdweb/extensions/erc20"; -import { isContractDeployed, resolvePromisedValue } from "thirdweb/utils"; +import { isContractDeployed } from "thirdweb/utils"; import { getChain } from "../../../shared/utils/chain"; import { getChecksumAddress,