Repository: alchemyplatform/aa-sdk
Pull request #1609: fix: parallel eth_calls, make maxTokenAmount requried
Author: login: andysim3d
Created: 2025-05-09T00:42:56Z | Merged: 2025-05-13T17:49:21Z

# Pull Request Checklist

- [ ] Did you add new tests and confirm existing tests pass? (`yarn test`)
- [ ] Did you update relevant docs? (docs are found in the `site` folder, and guidelines for updating/adding docs can be found in the [contribution guide](https://github.com/alchemyplatform/aa-sdk/blob/main/CONTRIBUTING.md))
- [ ] Do your commits follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard?
- [ ] Does your PR title also follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard?
- [ ] If you have a breaking change, is it [correctly reflected in your commit message](https://www.conventionalcommits.org/en/v1.0.0/#examples)? (e.g. `feat!: breaking change`)
- [ ] Did you run lint (`yarn lint:check`) and fix any issues? (`yarn lint:write`)
- [ ] Did you follow the [contribution guidelines](https://github.com/alchemyplatform/aa-sdk/blob/main/CONTRIBUTING.md)?

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the handling of the `maxTokenAmount` property in the `policyToken` object and improving the retrieval of token information via contract calls, ensuring that values are properly assigned and utilized.

### Detailed summary
- Changed `maxTokenAmount` in `policyToken` from optional to required.
- Refactored code to directly assign `maxTokenAmount` without conditional spreading.
- Added new function `balanceOf` and `allowance` in the contract ABI.
- Introduced `Promise.all` for concurrent fetching of `decimals`, `nonces`, and `paymasterData`.
- Improved error handling for missing contract call responses.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->


Diff:
diff --git a/account-kit/infra/src/client/smartAccountClient.ts b/account-kit/infra/src/client/smartAccountClient.ts
index b5f634fc8a..234bfb9c4e 100644
--- a/account-kit/infra/src/client/smartAccountClient.ts
+++ b/account-kit/infra/src/client/smartAccountClient.ts
@@ -50,8 +50,8 @@ export type AlchemySmartAccountClientConfig<
   policyId?: string | string[];
   policyToken?: {
     address: Address;
+    maxTokenAmount: bigint;
     approvalMode?: "NONE" | "PERMIT";
-    maxTokenAmount?: bigint;
     erc20Name?: string;
     version?: string;
   };
diff --git a/account-kit/infra/src/gas-manager.ts b/account-kit/infra/src/gas-manager.ts
index eccdc69eba..7924c65a01 100644
--- a/account-kit/infra/src/gas-manager.ts
+++ b/account-kit/infra/src/gas-manager.ts
@@ -86,6 +86,8 @@ export const PermitTypes = {
 export const EIP712NoncesAbi = [
   "function nonces(address owner) external view returns (uint)",
   "function decimals() public view returns (uint8)",
+  "function balanceOf(address owner) external view returns (uint256)",
+  "function allowance(address owner, address spender) external view returns (uint256)",
 ] as const;
 
 export type PermitMessage = {
diff --git a/account-kit/infra/src/middleware/gasManager.ts b/account-kit/infra/src/middleware/gasManager.ts
index 9a41d598b6..b2c18b25d6 100644
--- a/account-kit/infra/src/middleware/gasManager.ts
+++ b/account-kit/infra/src/middleware/gasManager.ts
@@ -31,7 +31,6 @@ import {
   encodeAbiParameters,
   encodeFunctionData,
   parseAbi,
-  maxUint256,
   sliceHex,
 } from "viem";
 import type { AlchemySmartAccountClient } from "../client/smartAccountClient.js";
@@ -91,9 +90,7 @@ export function alchemyGasManagerMiddleware(
       const userOp = await deepHexlify(await resolveProperties(uo));
       context.erc20Context = {
         tokenAddress: policyToken.address,
-        ...(policyToken.maxTokenAmount
-          ? { maxTokenAmount: policyToken.maxTokenAmount }
-          : {}),
+        maxTokenAmount: policyToken.maxTokenAmount,
       };
 
       if (policyToken.approvalMode === "PERMIT") {
@@ -134,7 +131,7 @@ interface AlchemyGasAndPaymasterAndDataMiddlewareParams {
 
 export type PolicyToken = {
   address: Address;
-  maxTokenAmount?: bigint;
+  maxTokenAmount: bigint;
   approvalMode?: "NONE" | "PERMIT";
   erc20Name?: string;
   version?: string;
@@ -279,9 +276,7 @@ export function alchemyGasAndPaymasterAndDataMiddleware(
       if (policyToken !== undefined) {
         erc20Context = {
           tokenAddress: policyToken.address,
-          ...(policyToken.maxTokenAmount
-            ? { maxTokenAmount: policyToken.maxTokenAmount }
-            : {}),
+          maxTokenAmount: policyToken.maxTokenAmount,
         };
         if (policyToken.approvalMode === "PERMIT") {
           erc20Context.permit = await generateSignedPermit(
@@ -393,7 +388,7 @@ const generateSignedPermit = async <
   policyId: string | string[],
   policyToken: {
     address: Address;
-    maxTokenAmount?: bigint;
+    maxTokenAmount: bigint;
     approvalMode?: "NONE" | "PERMIT";
     erc20Name?: string;
     version?: string;
@@ -406,21 +401,25 @@ const generateSignedPermit = async <
     throw new Error("erc20Name or version is missing");
   }
 
-  let maxAmountToken = maxUint256;
+  let decimalsFuture = client.call({
+    to: policyToken.address,
+    data: encodeFunctionData({
+      abi: parseAbi(EIP712NoncesAbi),
+      functionName: "decimals",
+      args: [],
+    }),
+  });
 
-  if (policyToken.maxTokenAmount) {
-    let { data } = await client.call({
-      to: policyToken.address,
-      data: encodeFunctionData({
-        abi: parseAbi(EIP712NoncesAbi),
-        functionName: "decimals",
-        args: [],
-      }),
-    });
-    const decimals = 10n ** (data ? BigInt(data) : 18n);
-    maxAmountToken = policyToken.maxTokenAmount * decimals;
-  }
-  const paymasterData = await (client as Erc7677Client).request({
+  let nonceFuture = client.call({
+    to: policyToken.address,
+    data: encodeFunctionData({
+      abi: parseAbi(EIP712NoncesAbi),
+      functionName: "nonces",
+      args: [account.address],
+    }),
+  });
+
+  let paymasterDataFuture = (client as Erc7677Client).request({
     method: "pm_getPaymasterStubData",
     params: [
       userOp,
@@ -432,6 +431,23 @@ const generateSignedPermit = async <
     ],
   });
 
+  const [decimalsResponse, nonceResponse, paymasterData] = await Promise.all([
+    decimalsFuture,
+    nonceFuture,
+    paymasterDataFuture,
+  ]);
+  if (!decimalsResponse.data) {
+    throw new Error("No decimals returned from erc20 contract call");
+  }
+  if (!nonceResponse.data) {
+    throw new Error("No nonces returned from erc20 contract call");
+  }
+
+  const decimals =
+    10n ** (decimalsResponse.data ? BigInt(decimalsResponse.data) : 18n);
+  const maxAmountToken = policyToken.maxTokenAmount * decimals;
+  const nonce = BigInt(nonceResponse.data);
+
   const paymasterAddress = paymasterData.paymaster
     ? paymasterData.paymaster
     : paymasterData.paymasterAndData
@@ -441,18 +457,8 @@ const generateSignedPermit = async <
   if (paymasterAddress === undefined || paymasterAddress === "0x") {
     throw new Error("no paymaster contract address available");
   }
-  const deadline = maxUint256;
-  const { data } = await client.call({
-    to: policyToken.address,
-    data: encodeFunctionData({
-      abi: parseAbi(EIP712NoncesAbi),
-      functionName: "nonces",
-      args: [account.address],
-    }),
-  });
-  if (!data) {
-    throw new Error("No nonces returned from erc20 contract call");
-  }
+
+  const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 10);
 
   const typedPermitData = {
     types: PermitTypes,
@@ -467,7 +473,7 @@ const generateSignedPermit = async <
       owner: account.address,
       spender: paymasterAddress,
       value: maxAmountToken,
-      nonce: BigInt(data),
+      nonce: nonce,
       deadline,
     } satisfies PermitMessage,
   } as const;


Reviews:
user: login: Blu-J
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T16:46:44Z
user: login: Blu-J
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T16:50:04Z
user: login: Blu-J
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T16:50:53Z
user: login: Blu-J
body: It looks mostly good to me. 
Low risk of error, params for client calls ✔️ same
Rest is type changes and moving around. Getting rid of if because of conditional params.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T16:56:16Z
user: login: Blu-J
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T17:28:42Z
user: login: dphilipson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T20:25:20Z
user: login: dphilipson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T20:26:38Z
user: login: dphilipson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T20:27:07Z
user: login: andysim3d
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T13:32:28Z
user: login: andysim3d
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T13:34:17Z
user: login: Blu-J
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T17:39:21Z

Review comments:
diff_hunk: @@ -406,21 +402,25 @@ const generateSignedPermit = async <
     throw new Error("erc20Name or version is missing");
   }
 
-  let maxAmountToken = maxUint256;
+  let decimals_future = client.call({
+    to: policyToken.address,
+    data: encodeFunctionData({
+      abi: parseAbi(EIP712NoncesAbi),
+      functionName: "decimals",
+      args: [],
+    }),
+  });
 
-  if (policyToken.maxTokenAmount) {
-    let { data } = await client.call({
-      to: policyToken.address,
-      data: encodeFunctionData({
-        abi: parseAbi(EIP712NoncesAbi),
-        functionName: "decimals",
-        args: [],
-      }),
-    });
-    const decimals = 10n ** (data ? BigInt(data) : 18n);
-    maxAmountToken = policyToken.maxTokenAmount * decimals;
-  }
-  const paymasterData = await (client as Erc7677Client).request({
+  let nonce_future = client.call({
+    to: policyToken.address,
+    data: encodeFunctionData({
+      abi: parseAbi(EIP712NoncesAbi),
+      functionName: "nonces",
+      args: [account.address],
+    }),
+  });
+
+  let paymasterData_future = (client as Erc7677Client).request({
path: account-kit/infra/src/middleware/gasManager.ts
user: login: Blu-J
body: ❔ (no-changes) I see that it used to be casted before, do we know why it needs the casting?
created_at: 2025-05-12T16:46:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -432,6 +432,23 @@ const generateSignedPermit = async <
     ],
   });
 
+  const [decimals_response, nonce_response, paymasterData] = await Promise.all([
path: account-kit/infra/src/middleware/gasManager.ts
user: login: Blu-J
body: ❔ (noChange) So why choose this style.
Since the futures are already in flight, is there a performance reason for this. It was good to not await the values until later, that makes the parallel work. 

Wouldn't 
```ts
const decimals_response = await decimals_future;
const none_response = await nonce_future;
const paymasterData_future = await paymasterData_future;
```
Also why are these snake_cased and camelCased?
created_at: 2025-05-12T16:50:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -441,18 +458,8 @@ const generateSignedPermit = async <
   if (paymasterAddress === undefined || paymasterAddress === "0x") {
     throw new Error("no paymaster contract address available");
   }
+  // TODO: set a shorter deadline
path: account-kit/infra/src/middleware/gasManager.ts
user: login: Blu-J
body: 💡 Are we going to make a task for this, or did you mean to do the todo?
created_at: 2025-05-12T16:50:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -432,6 +432,23 @@ const generateSignedPermit = async <
     ],
   });
 
+  const [decimals_response, nonce_response, paymasterData] = await Promise.all([
path: account-kit/infra/src/middleware/gasManager.ts
user: login: dphilipson
body: An advantage of awaiting in parallel is that if any of the promises fail, then we can fail immediately. In your snippet, if `paymasterData_future` fails, then this function would still wait for `decimals_response` and `none_response` before terminating.

Agreed about the naming, we should use camelCase in TypeScript.
created_at: 2025-05-12T20:25:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -432,6 +432,23 @@ const generateSignedPermit = async <
     ],
   });
 
+  const [decimals_response, nonce_response, paymasterData] = await Promise.all([
+    decimals_future,
+    nonce_future,
+    paymasterData_future,
+  ]);
+  if (!decimals_response.data) {
+    throw new Error("No decimals returned from erc20 contract call");
+  }
+  if (!nonce_response.data) {
+    throw new Error("No nonces returned from erc20 contract call");
+  }
+
+  const decimals =
+    10n ** (decimals_response.data ? BigInt(decimals_response.data) : 18n);
path: account-kit/infra/src/middleware/gasManager.ts
user: login: dphilipson
body: `decimals_response.data` is always present because of the `if (!decimals_response.data)` check a few lines up.
created_at: 2025-05-12T20:26:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -467,7 +474,7 @@ const generateSignedPermit = async <
       owner: account.address,
       spender: paymasterAddress,
       value: maxAmountToken,
-      nonce: BigInt(data),
+      nonce: nonce,
path: account-kit/infra/src/middleware/gasManager.ts
user: login: dphilipson
body: (Nit) Can write more simply:
```suggestion
      nonce,
```
created_at: 2025-05-12T20:27:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -406,21 +402,25 @@ const generateSignedPermit = async <
     throw new Error("erc20Name or version is missing");
   }
 
-  let maxAmountToken = maxUint256;
+  let decimals_future = client.call({
+    to: policyToken.address,
+    data: encodeFunctionData({
+      abi: parseAbi(EIP712NoncesAbi),
+      functionName: "decimals",
+      args: [],
+    }),
+  });
 
-  if (policyToken.maxTokenAmount) {
-    let { data } = await client.call({
-      to: policyToken.address,
-      data: encodeFunctionData({
-        abi: parseAbi(EIP712NoncesAbi),
-        functionName: "decimals",
-        args: [],
-      }),
-    });
-    const decimals = 10n ** (data ? BigInt(data) : 18n);
-    maxAmountToken = policyToken.maxTokenAmount * decimals;
-  }
-  const paymasterData = await (client as Erc7677Client).request({
+  let nonce_future = client.call({
+    to: policyToken.address,
+    data: encodeFunctionData({
+      abi: parseAbi(EIP712NoncesAbi),
+      functionName: "nonces",
+      args: [account.address],
+    }),
+  });
+
+  let paymasterData_future = (client as Erc7677Client).request({
path: account-kit/infra/src/middleware/gasManager.ts
user: login: andysim3d
body: well it is because pm_getPaymasterData is only declared at Erc7677Client. https://github.com/alchemyplatform/aa-sdk/blob/82ce8ca483693137937eec1abc99583f23537839/aa-sdk/core/src/middleware/erc7677middleware.ts#L25
created_at: 2025-05-13T13:32:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -441,18 +458,8 @@ const generateSignedPermit = async <
   if (paymasterAddress === undefined || paymasterAddress === "0x") {
     throw new Error("no paymaster contract address available");
   }
+  // TODO: set a shorter deadline
path: account-kit/infra/src/middleware/gasManager.ts
user: login: andysim3d
body: we have an asana ticket for that. 
created_at: 2025-05-13T13:34:17Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-09T00:43:01Z
body: [vc]: #nKNy4rMZiUjZWG7EbRDI35lSKQS/Jj1cxbMLeV0QxH0=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJhYS1zZGstc2l0ZSIsInJvb3REaXJlY3RvcnkiOiJzaXRlIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2FsY2hlbXktZG90LWNvbS9hYS1zZGstc2l0ZS9FZUxHSHRTdE1OU2ZvZFdaeGFjdUtXWUpjUEtyIiwicHJldmlld1VybCI6ImFhLXNkay1zaXRlLWdpdC1hbmR5LWVyYzIwLWZvbGxvd3VwLWFsY2hlbXktZG90LWNvbS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImFhLXNkay1zaXRlLWdpdC1hbmR5LWVyYzIwLWZvbGxvd3VwLWFsY2hlbXktZG90LWNvbS52ZXJjZWwuYXBwIn19LHsibmFtZSI6ImFhLXNkay11aS1kZW1vIiwicm9vdERpcmVjdG9yeSI6ImV4YW1wbGVzL3VpLWRlbW8iLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vYWxjaGVteS1kb3QtY29tL2FhLXNkay11aS1kZW1vLzhyTTRtV0tqWmJBS1lraDJqMzREMVl4WHBkU1AiLCJwcmV2aWV3VXJsIjoiYWEtc2RrLXVpLWRlbW8tZ2l0LWFuZHktZXJjMjAtZm9sbG93dXAtYWxjaGVteS1kb3QtY29tLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiYWEtc2RrLXVpLWRlbW8tZ2l0LWFuZHktZXJjMjAtZm9sbG93dXAtYWxjaGVteS1kb3QtY29tLnZlcmNlbC5hcHAifX1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **aa-sdk-site** | ✅ Ready ([Inspect](https://vercel.com/alchemy-dot-com/aa-sdk-site/EeLGHtStMNSfodWZxacuKWYJcPKr)) | [Visit Preview](https://aa-sdk-site-git-andy-erc20-followup-alchemy-dot-com.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/aa-sdk-site-git-andy-erc20-followup-alchemy-dot-com.vercel.app?via=pr-comment-feedback-link) | May 13, 2025 5:23pm |
| **aa-sdk-ui-demo** | ✅ Ready ([Inspect](https://vercel.com/alchemy-dot-com/aa-sdk-ui-demo/8rM4mWKjZbAKYkh2j34D1YxXpdSP)) | [Visit Preview](https://aa-sdk-ui-demo-git-andy-erc20-followup-alchemy-dot-com.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/aa-sdk-ui-demo-git-andy-erc20-followup-alchemy-dot-com.vercel.app?via=pr-comment-feedback-link) | May 13, 2025 5:23pm |



author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-09T00:43:28Z
body: ## 🌿 Documentation Preview

| Name | Status | Preview | Updated (UTC) |
| :--- | :------ | :------ | :------ |
| **Alchemy Docs** | ✅ Ready | [🔗 Visit Preview](https://alchemy-preview-e0ed3ca4-66d4-4da5-907d-c6f14ac16b1e.docs.buildwithfern.com/docs) | May 13, 2025, 5:16 PM |

>
author_association: CONTRIBUTOR