From b38e85a0adbb0469537642d04d9f5399f6b3ee38 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Thu, 24 Jul 2025 19:19:55 +1200 Subject: [PATCH] [SDK] Optimize 4337 signature performance --- .changeset/thirty-eels-decide.md | 5 + .../src/pay/buyWithCrypto/getQuote.ts | 16 +- .../src/pay/buyWithCrypto/getTransfer.ts | 8 +- .../thirdweb/src/pay/buyWithFiat/getQuote.ts | 6 +- .../src/react/core/hooks/usePaymentMethods.ts | 4 +- .../thirdweb/src/wallets/smart/lib/signing.ts | 155 ++++-------------- .../smart/smart-wallet-integration.test.ts | 77 +-------- 7 files changed, 57 insertions(+), 214 deletions(-) create mode 100644 .changeset/thirty-eels-decide.md diff --git a/.changeset/thirty-eels-decide.md b/.changeset/thirty-eels-decide.md new file mode 100644 index 00000000000..75d956f529a --- /dev/null +++ b/.changeset/thirty-eels-decide.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Optimize 4337 signature performance diff --git a/packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts b/packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts index f12041a6263..f8448a6cfbd 100644 --- a/packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts +++ b/packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts @@ -300,14 +300,14 @@ export async function getBuyWithCryptoQuote( Number( Value.format(quote.originAmount, firstStep.originToken.decimals), ) * - (firstStep.originToken.prices["USD"] || 0) * + (firstStep.originToken.prices.USD || 0) * 100, amountWei: quote.originAmount.toString(), token: { chainId: firstStep.originToken.chainId, decimals: firstStep.originToken.decimals, name: firstStep.originToken.name, - priceUSDCents: (firstStep.originToken.prices["USD"] || 0) * 100, + priceUSDCents: (firstStep.originToken.prices.USD || 0) * 100, symbol: firstStep.originToken.symbol, tokenAddress: firstStep.originToken.address, }, @@ -323,7 +323,7 @@ export async function getBuyWithCryptoQuote( chainId: firstStep.originToken.chainId, decimals: firstStep.originToken.decimals, name: firstStep.originToken.name, - priceUSDCents: (firstStep.originToken.prices["USD"] || 0) * 100, + priceUSDCents: (firstStep.originToken.prices.USD || 0) * 100, symbol: firstStep.originToken.symbol, tokenAddress: firstStep.originToken.address, }, @@ -337,7 +337,7 @@ export async function getBuyWithCryptoQuote( Number( Value.format(quote.originAmount, firstStep.originToken.decimals), ) * - (firstStep.originToken.prices["USD"] || 0) * + (firstStep.originToken.prices.USD || 0) * 100, gasCostUSDCents: 0, slippageBPS: 0, @@ -348,7 +348,7 @@ export async function getBuyWithCryptoQuote( firstStep.destinationToken.decimals, ), ) * - (firstStep.destinationToken.prices["USD"] || 0) * + (firstStep.destinationToken.prices.USD || 0) * 100, toAmountUSDCents: Number( @@ -357,7 +357,7 @@ export async function getBuyWithCryptoQuote( firstStep.destinationToken.decimals, ), ) * - (firstStep.destinationToken.prices["USD"] || 0) * + (firstStep.destinationToken.prices.USD || 0) * 100, }, fromAddress: quote.intent.sender, @@ -372,7 +372,7 @@ export async function getBuyWithCryptoQuote( chainId: firstStep.originToken.chainId, decimals: firstStep.originToken.decimals, name: firstStep.originToken.name, - priceUSDCents: (firstStep.originToken.prices["USD"] || 0) * 100, + priceUSDCents: (firstStep.originToken.prices.USD || 0) * 100, symbol: firstStep.originToken.symbol, tokenAddress: firstStep.originToken.address, }, @@ -395,7 +395,7 @@ export async function getBuyWithCryptoQuote( chainId: firstStep.destinationToken.chainId, decimals: firstStep.destinationToken.decimals, name: firstStep.destinationToken.name, - priceUSDCents: (firstStep.destinationToken.prices["USD"] || 0) * 100, + priceUSDCents: (firstStep.destinationToken.prices.USD || 0) * 100, symbol: firstStep.destinationToken.symbol, tokenAddress: firstStep.destinationToken.address, }, diff --git a/packages/thirdweb/src/pay/buyWithCrypto/getTransfer.ts b/packages/thirdweb/src/pay/buyWithCrypto/getTransfer.ts index 3728cb9f357..3a893889bd7 100644 --- a/packages/thirdweb/src/pay/buyWithCrypto/getTransfer.ts +++ b/packages/thirdweb/src/pay/buyWithCrypto/getTransfer.ts @@ -198,14 +198,14 @@ export async function getBuyWithCryptoTransfer( Number( Value.format(quote.originAmount, firstStep.originToken.decimals), ) * - (firstStep.originToken.prices["USD"] || 0) * + (firstStep.originToken.prices.USD || 0) * 100, amountWei: quote.originAmount.toString(), token: { chainId: firstStep.originToken.chainId, decimals: firstStep.originToken.decimals, name: firstStep.originToken.name, - priceUSDCents: (firstStep.originToken.prices["USD"] || 0) * 100, + priceUSDCents: (firstStep.originToken.prices.USD || 0) * 100, symbol: firstStep.originToken.symbol, tokenAddress: firstStep.originToken.address, }, @@ -226,7 +226,7 @@ export async function getBuyWithCryptoTransfer( firstStep.originToken.decimals, ), ) * - (firstStep.originToken.prices["USD"] || 0) * + (firstStep.originToken.prices.USD || 0) * 100 : 0, amountWei: @@ -237,7 +237,7 @@ export async function getBuyWithCryptoTransfer( chainId: firstStep.originToken.chainId, decimals: firstStep.originToken.decimals, name: firstStep.originToken.name, - priceUSDCents: (firstStep.originToken.prices["USD"] || 0) * 100, + priceUSDCents: (firstStep.originToken.prices.USD || 0) * 100, symbol: firstStep.originToken.symbol, tokenAddress: firstStep.originToken.address, }, diff --git a/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts b/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts index aa07953c97b..a7ad3b11a0c 100644 --- a/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts +++ b/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts @@ -372,7 +372,7 @@ export async function getBuyWithFiatQuote( chainId: token.chainId, decimals: token.decimals, name: token.name, - priceUSDCents: Math.round((token.prices["USD"] || 0) * 100), + priceUSDCents: Math.round((token.prices.USD || 0) * 100), symbol: token.symbol, tokenAddress: token.address, }); @@ -408,7 +408,7 @@ export async function getBuyWithFiatQuote( const onRampTokenObject = { amount: onRampTokenAmount, amountUSDCents: Math.round( - Number(onRampTokenAmount) * (onRampTokenRaw.prices["USD"] || 0) * 100, + Number(onRampTokenAmount) * (onRampTokenRaw.prices.USD || 0) * 100, ), amountWei: onRampTokenAmountWei.toString(), token: tokenToPayTokenInfo(onRampTokenRaw), @@ -434,7 +434,7 @@ export async function getBuyWithFiatQuote( routingTokenObject = { amount: routingAmount, amountUSDCents: Math.round( - Number(routingAmount) * (routingTokenRaw.prices["USD"] || 0) * 100, + Number(routingAmount) * (routingTokenRaw.prices.USD || 0) * 100, ), amountWei: routingAmountWei.toString(), token: tokenToPayTokenInfo(routingTokenRaw), diff --git a/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts b/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts index 97986e4b577..323adf45f6b 100644 --- a/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts +++ b/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts @@ -240,10 +240,10 @@ export function usePaymentMethods(options: { validOwnedTokens.sort((a, b) => { const aDollarBalance = Number.parseFloat(toTokens(a.balance, a.originToken.decimals)) * - (a.originToken.prices["USD"] || 0); + (a.originToken.prices.USD || 0); const bDollarBalance = Number.parseFloat(toTokens(b.balance, b.originToken.decimals)) * - (b.originToken.prices["USD"] || 0); + (b.originToken.prices.USD || 0); return bDollarBalance - aDollarBalance; }); diff --git a/packages/thirdweb/src/wallets/smart/lib/signing.ts b/packages/thirdweb/src/wallets/smart/lib/signing.ts index 888dda585b5..592d3625b6f 100644 --- a/packages/thirdweb/src/wallets/smart/lib/signing.ts +++ b/packages/thirdweb/src/wallets/smart/lib/signing.ts @@ -1,20 +1,12 @@ import type * as ox__TypedData from "ox/TypedData"; import { serializeErc6492Signature } from "../../../auth/serialize-erc6492-signature.js"; -import { - verifyEip1271Signature, - verifyHash, -} from "../../../auth/verify-hash.js"; +import { verifyEip1271Signature } from "../../../auth/verify-hash.js"; import type { Chain } from "../../../chains/types.js"; import type { ThirdwebClient } from "../../../client/client.js"; -import { - getContract, - type ThirdwebContract, -} from "../../../contract/contract.js"; +import type { ThirdwebContract } from "../../../contract/contract.js"; import { encode } from "../../../transaction/actions/encode.js"; -import { readContract } from "../../../transaction/read-contract.js"; import { encodeAbiParameters } from "../../../utils/abi/encodeAbiParameters.js"; import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js"; -import type { Hex } from "../../../utils/encoding/hex.js"; import { hashMessage } from "../../../utils/hashing/hashMessage.js"; import { hashTypedData } from "../../../utils/hashing/hashTypedData.js"; import type { SignableMessage } from "../../../utils/types.js"; @@ -40,33 +32,24 @@ export async function smartAccountSignMessage({ message: SignableMessage; }) { const originalMsgHash = hashMessage(message); - const is712Factory = await checkFor712Factory({ - accountContract, - factoryContract, - originalMsgHash, - }); let sig: `0x${string}`; - if (is712Factory) { - const wrappedMessageHash = encodeAbiParameters( - [{ type: "bytes32" }], - [originalMsgHash], - ); + const wrappedMessageHash = encodeAbiParameters( + [{ type: "bytes32" }], + [originalMsgHash], + ); - sig = await options.personalAccount.signTypedData({ - domain: { - chainId: options.chain.id, - name: "Account", - verifyingContract: accountContract.address, - version: "1", - }, - message: { message: wrappedMessageHash }, - primaryType: "AccountMessage", - types: { AccountMessage: [{ name: "message", type: "bytes" }] }, - }); - } else { - sig = await options.personalAccount.signMessage({ message }); - } + sig = await options.personalAccount.signTypedData({ + domain: { + chainId: options.chain.id, + name: "Account", + verifyingContract: accountContract.address, + version: "1", + }, + message: { message: wrappedMessageHash }, + primaryType: "AccountMessage", + types: { AccountMessage: [{ name: "message", type: "bytes" }] }, + }); const isDeployed = await isContractDeployed(accountContract); if (isDeployed) { @@ -96,19 +79,7 @@ export async function smartAccountSignMessage({ signature: sig, }); - // check if the signature is valid - const isValid = await verifyHash({ - address: accountContract.address, - chain: accountContract.chain, - client: accountContract.client, - hash: originalMsgHash, - signature: erc6492Sig, - }); - - if (isValid) { - return erc6492Sig; - } - throw new Error("Unable to verify ERC-6492 signature after signing."); + return erc6492Sig; } } @@ -138,33 +109,23 @@ export async function smartAccountSignTypedData< } const originalMsgHash = hashTypedData(typedData); - // check if the account contract supports EIP721 domain separator based signing - const is712Factory = await checkFor712Factory({ - accountContract, - factoryContract, - originalMsgHash, - }); let sig: `0x${string}`; - if (is712Factory) { - const wrappedMessageHash = encodeAbiParameters( - [{ type: "bytes32" }], - [originalMsgHash], - ); - sig = await options.personalAccount.signTypedData({ - domain: { - chainId: options.chain.id, - name: "Account", - verifyingContract: accountContract.address, - version: "1", - }, - message: { message: wrappedMessageHash }, - primaryType: "AccountMessage", - types: { AccountMessage: [{ name: "message", type: "bytes" }] }, - }); - } else { - sig = await options.personalAccount.signTypedData(typedData); - } + const wrappedMessageHash = encodeAbiParameters( + [{ type: "bytes32" }], + [originalMsgHash], + ); + sig = await options.personalAccount.signTypedData({ + domain: { + chainId: options.chain.id, + name: "Account", + verifyingContract: accountContract.address, + version: "1", + }, + message: { message: wrappedMessageHash }, + primaryType: "AccountMessage", + types: { AccountMessage: [{ name: "message", type: "bytes" }] }, + }); const isDeployed = await isContractDeployed(accountContract); if (isDeployed) { @@ -194,21 +155,7 @@ export async function smartAccountSignTypedData< signature: sig, }); - // check if the signature is valid - const isValid = await verifyHash({ - address: accountContract.address, - chain: accountContract.chain, - client: accountContract.client, - hash: originalMsgHash, - signature: erc6492Sig, - }); - - if (isValid) { - return erc6492Sig; - } - throw new Error( - "Unable to verify signature on smart account, please make sure the admin wallet has permissions and the signature is valid.", - ); + return erc6492Sig; } } @@ -233,40 +180,6 @@ export async function confirmContractDeployment(args: { } } -async function checkFor712Factory({ - factoryContract, - accountContract, - originalMsgHash, -}: { - factoryContract: ThirdwebContract; - accountContract: ThirdwebContract; - originalMsgHash: Hex; -}) { - try { - const implementationAccount = await readContract({ - contract: factoryContract, - method: "function accountImplementation() public view returns (address)", - }); - // check if the account contract supports EIP721 domain separator or modular based signing - const is712Factory = await readContract({ - contract: getContract({ - address: implementationAccount, - chain: accountContract.chain, - client: accountContract.client, - }), - method: - "function getMessageHash(bytes32 _hash) public view returns (bytes32)", - params: [originalMsgHash], - }) - .then((res) => res !== "0x") - .catch(() => false); - - return is712Factory; - } catch { - return false; - } -} - /** * Deployes a smart account via a dummy transaction. If the account is already deployed, this will do nothing. * diff --git a/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts b/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts index 7f92405b4ef..b37c2680303 100644 --- a/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts +++ b/packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts @@ -47,7 +47,6 @@ const contract = getContract({ chain, client, }); -const factoryAddress = "0x564cf6453a1b0FF8DB603E92EA4BbD410dea45F3"; // pre 712 describe.runIf(process.env.TW_SECRET_KEY).sequential( "SmartWallet core tests", @@ -301,79 +300,6 @@ describe.runIf(process.env.TW_SECRET_KEY).sequential( expect(logs.some((l) => l.args.isAdmin)).toBe(true); }); - it("can use a different factory without replay protection", async () => { - const wallet = smartWallet({ - chain, - factoryAddress: factoryAddress, - gasless: true, - }); - - // should not be able to switch chains before connecting - await expect( - wallet.switchChain(baseSepolia), - ).rejects.toMatchInlineSnapshot( - "[Error: Cannot switch chain without a previous connection]", - ); - - const newAccount = await wallet.connect({ client, personalAccount }); - const message = "hello world"; - const signature = await newAccount.signMessage({ message }); - const isValidV1 = await verifySignature({ - address: newAccount.address, - chain, - client, - message, - signature, - }); - expect(isValidV1).toEqual(true); - - // sign typed data - const signatureTyped = await newAccount.signTypedData({ - ...typedData.basic, - primaryType: "Mail", - }); - const isValidV2 = await verifyTypedData({ - address: newAccount.address, - chain, - client, - signature: signatureTyped, - ...typedData.basic, - }); - expect(isValidV2).toEqual(true); - - // add admin pre-deployment - const newAdmin = await generateAccount({ client }); - const receipt = await sendAndConfirmTransaction({ - account: newAccount, - transaction: addAdmin({ - account: newAccount, - adminAddress: newAdmin.address, - contract: getContract({ - address: newAccount.address, - chain, - client, - }), - }), - }); - const logs = parseEventLogs({ - events: [adminUpdatedEvent()], - logs: receipt.logs, - }); - expect(logs.map((l) => l.args.signer)).toContain(newAdmin.address); - expect(logs.map((l) => l.args.isAdmin)).toContain(true); - - // should not be able to switch chains since factory not deployed elsewhere - await expect( - wallet.switchChain(baseSepolia), - ).rejects.toMatchInlineSnapshot( - "[Error: Factory contract not deployed on chain: 84532]", - ); - - // check can disconnnect - await wallet.disconnect(); - expect(wallet.getAccount()).toBeUndefined(); - }); - it("can switch chains", async () => { await wallet.switchChain(baseSepolia); expect(wallet.getChain()?.id).toEqual(baseSepolia.id); @@ -424,7 +350,7 @@ describe.runIf(process.env.TW_SECRET_KEY).sequential( tokenId: 0n, }), }), - sleep(1000).then(() => + sleep(1500).then(() => sendAndConfirmTransaction({ account: newSmartAccount, transaction: claimTo({ @@ -453,7 +379,6 @@ describe.runIf(process.env.TW_SECRET_KEY).sequential( it("can use a different paymaster", async () => { const wallet = smartWallet({ chain, - factoryAddress: factoryAddress, gasless: true, overrides: { paymaster: async () => {