Repository: recallnet/js-recall
Pull request #203: feat: fix gas estimation & query limit issues
Author: login: dtbuchholz
Created: 2025-04-11T17:43:22Z | Merged: 2025-04-15T22:36:59Z

## Summary

closes https://github.com/recallnet/js-recall/issues/202 https://github.com/recallnet/recall-alpha-agent/issues/7. 

## Details

for the issue where we're burning through RECALL tokens, all we need to do is manually make the `getGasPrice` call before every write operation, and then pass it as an option to the method call:
```ts
const gasPrice = await this.client.publicClient.getGasPrice()

const hash = await this.client.walletClient?.sendTransaction({
  account: this.client.walletClient.account,
  chain: this.client.walletClient.chain,
  to: recipient,
  value: amount,
  gasPrice,
});
```

for the query objects issue, we do the following:
1. set a MAX_QUERY_LIMIT to 50 objects because if you try to make a query larger than that, there's a high likelihood it'll fail (out of gas issues due to onchain processing)
2. we call the bucket manager's query object method one time per 50 objects. e.g., if you want to query for 100 objects in a bucket, we'll automatically make 2 requests in 50 object increments.

note: when we move from wrappers to the facades, we should revisit what the effective query limit is since it'll be much cheaper.

## Testing

### Gas issues

The observed effective gas rate per tx dropped from `25000` to `7`. This more closely aligns with what we see in the rust clients where the gas price is `8`.
```
// before changes, you'd use up a minimum of 0.0001 RECALL, as high as 0.6
Balance before 3.016125347138601077
Gas used 158711044n
Gas price 250011n // sometimes as high as 3750220406n
ETH used 0.000039679506821484 // sometimes as high as 0.6 RECALL
Balance after 3.016085667574953021

// after, you use a negligible amount
Balance before 0.328464769145263858
Gas used 176823074n
Gas price 7n
ETH used 0.000000001237761518
Balance after 0.328464767861086285
```

### Query objects

- The existing `sdk` tests pass. 
- It takes too long to wait to add >50 objects, and then query them. We _can_ add this as a test, though.
- Fixes my trading eval scaper, which is where we uncovered this bug.
- Also, I manually ran some experiments. For a bucket with 82 objects:
   1. query for <=50 objects (default)
   2. query for 80 objects (i.e., 2 "leftover")
   4. query for 100 objects (all of them)
- The main difference in the scenarios above is that (1) and (2) will show a value like `nextKey: 'competition/v0/cot/4-uw99f00yqca'`, which is collected during the batch processing—and (3) does not (since there's nothing left).


Diff:
diff --git a/.changeset/loud-plums-share.md b/.changeset/loud-plums-share.md
new file mode 100644
index 000000000..35d38fc2f
--- /dev/null
+++ b/.changeset/loud-plums-share.md
@@ -0,0 +1,6 @@
+---
+"@recallnet/network-constants": patch
+"@recallnet/sdk": patch
+---
+
+fix gas estimation and query limit errors
diff --git a/packages/network-constants/src/index.ts b/packages/network-constants/src/index.ts
index e1116907c..5d4669592 100644
--- a/packages/network-constants/src/index.ts
+++ b/packages/network-constants/src/index.ts
@@ -162,6 +162,11 @@ export const MIN_TTL = 3600n; // one hour
  */
 export const MAX_OBJECT_SIZE = 5_000_000_000; // 5GB
 
+/**
+ * Maximum number of objects that can be returned in a single query (to avoid hitting gas limits).
+ */
+export const MAX_QUERY_LIMIT = 50;
+
 /**
  * Gateway Manager Facet contract address for the Testnet parent chain.
  */
diff --git a/packages/sdk/src/entities/account.ts b/packages/sdk/src/entities/account.ts
index 497e038c6..79e53c0eb 100644
--- a/packages/sdk/src/entities/account.ts
+++ b/packages/sdk/src/entities/account.ts
@@ -153,6 +153,7 @@ export class AccountManager {
       throw new Error("Wallet client is not initialized for approving");
     }
     const args = [spender, amount] as ApproveParams;
+    const gasPrice = await this.client.publicClient.getGasPrice();
     const supplySource = this.getSupplySource(
       this.client.walletClient.chain,
       contractAddress,
@@ -161,6 +162,7 @@ export class AccountManager {
       args,
       {
         account: this.client.walletClient.account,
+        gasPrice,
       },
     );
     // TODO: calling `supplySource.write.approve(...)` doesn't work, for some reason
@@ -261,11 +263,13 @@ export class AccountManager {
     if (!this.client.walletClient?.account) {
       throw new Error("Wallet client is not initialized for transfers");
     }
+    const gasPrice = await this.client.publicClient.getGasPrice();
     const hash = await this.client.walletClient?.sendTransaction({
       account: this.client.walletClient.account,
       chain: this.client.walletClient.chain,
       to: recipient,
       value: amount,
+      gasPrice,
     });
     const tx = await this.client.publicClient.waitForTransactionReceipt({
       hash,
diff --git a/packages/sdk/src/entities/blob.ts b/packages/sdk/src/entities/blob.ts
index 5e3a8be42..090cb8fa7 100644
--- a/packages/sdk/src/entities/blob.ts
+++ b/packages/sdk/src/entities/blob.ts
@@ -181,10 +181,12 @@ export class BlobManager {
     }
     try {
       const args = [addParams] satisfies AddBlobFullParams;
+      const gasPrice = await this.client.publicClient.getGasPrice();
       const { request } = await this.contract.simulate.addBlob<Chain, Account>(
         args,
         {
           account: this.client.walletClient.account,
+          gasPrice,
         },
       );
       // TODO: calling `this.contract.write.addBlob(...)` doesn't work, for some reason
@@ -253,11 +255,13 @@ export class BlobManager {
         subscriptionId,
         from,
       ] satisfies DeleteBlobParams;
+      const gasPrice = await this.client.publicClient.getGasPrice();
       const { request } = await this.contract.simulate.deleteBlob<
         Chain,
         Account
       >(args, {
         account: this.client.walletClient.account,
+        gasPrice,
       });
       // TODO: calling `this.contract.write.deleteBlob(...)` doesn't work, for some reason
       const hash = await this.client.walletClient.writeContract(request);
@@ -331,11 +335,13 @@ export class BlobManager {
         );
       }
       const params = [oldHash, addParams] satisfies OverwriteBlobParams;
+      const gasPrice = await this.client.publicClient.getGasPrice();
       const { request } = await this.contract.simulate.overwriteBlob<
         Chain,
         Account
       >(params, {
         account: this.client.walletClient.account,
+        gasPrice,
       });
       // TODO: calling `this.contract.write.overwriteBlob(...)` doesn't work, for some reason
       const hash = await this.client.walletClient.writeContract(request);
diff --git a/packages/sdk/src/entities/bucket.ts b/packages/sdk/src/entities/bucket.ts
index 7b3582f38..eeb952de3 100644
--- a/packages/sdk/src/entities/bucket.ts
+++ b/packages/sdk/src/entities/bucket.ts
@@ -18,7 +18,11 @@ import {
   bucketManagerAddress,
   iMachineFacadeAbi,
 } from "@recallnet/contracts";
-import { MAX_OBJECT_SIZE, MIN_TTL } from "@recallnet/network-constants";
+import {
+  MAX_OBJECT_SIZE,
+  MAX_QUERY_LIMIT,
+  MIN_TTL,
+} from "@recallnet/network-constants";
 
 import { RecallClient } from "../client.js";
 import {
@@ -28,6 +32,7 @@ import {
   CreateBucketError,
   InvalidValue,
   ObjectNotFound,
+  OutOfGasError,
   UnhandledBucketError,
   isActorNotFoundError,
 } from "../errors.js";
@@ -96,14 +101,32 @@ export type QueryResultRaw = ContractFunctionReturnType<
   ]
 >;
 
+// Object value in query response (converted `metadata` to normal javascript object)
+export type QueryObjectValue = Omit<
+  QueryResultRaw["objects"][number]["state"],
+  "metadata"
+> & {
+  metadata: Record<string, unknown>;
+};
+
+// Object in query response
+export type QueryResultObject = {
+  key: QueryResultRaw["objects"][number]["key"];
+  state: QueryObjectValue;
+};
+
 // Converts metadata Solidity struct to normal javascript object
 export type QueryResult = Omit<QueryResultRaw, "objects"> & {
-  objects: {
-    key: QueryResultRaw["objects"][number]["key"];
-    state: Omit<QueryResultRaw["objects"][number]["state"], "metadata"> & {
-      metadata: Record<string, unknown>;
-    };
-  }[];
+  objects: QueryResultObject[];
+};
+
+// Options for query()
+export type QueryOptions = {
+  prefix?: string;
+  delimiter?: string;
+  startKey?: string;
+  limit?: number;
+  blockNumber?: bigint;
 };
 
 // Note: this event stems from the underlying machine facade contract; creating a bucket
@@ -232,11 +255,13 @@ export class BucketManager {
         owner ?? this.client.walletClient.account.address,
         metadata ? convertMetadataToAbiParams(metadata) : [],
       ] satisfies CreateBucketParams;
+      const gasPrice = await this.client.publicClient.getGasPrice();
       const { request } = await this.contract.simulate.createBucket<
         Chain,
         Account
       >(args, {
         account: this.client.walletClient.account,
+        gasPrice,
       });
       const hash = await this.contract.write.createBucket(request);
       const tx = await this.client.publicClient.waitForTransactionReceipt({
@@ -300,18 +325,22 @@ export class BucketManager {
   }
 
   // Add an object to a bucket inner
-  // TODO: should this be private and used internally by `add`
-  async addInner(bucket: Address, addParams: AddObjectParams): Promise<Result> {
+  private async executeAdd(
+    bucket: Address,
+    addParams: AddObjectParams,
+  ): Promise<Result> {
     if (!this.client.walletClient?.account) {
       throw new Error("Wallet client is not initialized for adding an object");
     }
     try {
       const args = [bucket, addParams] satisfies AddObjectFullParams;
+      const gasPrice = await this.client.publicClient.getGasPrice();
       const { request } = await this.contract.simulate.addObject<
         Chain,
         Account
       >(args, {
         account: this.client.walletClient.account,
+        gasPrice,
       });
       // TODO: calling `this.contract.write.addObject(...)` doesn't work, for some reason
       const hash = await this.client.walletClient.writeContract(request);
@@ -377,7 +406,7 @@ export class BucketManager {
       overwrite: options?.overwrite ?? false,
       from,
     } as AddObjectParams;
-    return await this.addInner(bucket, addParams);
+    return await this.executeAdd(bucket, addParams);
   }
 
   // Delete an object from a bucket
@@ -388,11 +417,13 @@ export class BucketManager {
     try {
       const from = this.client.walletClient.account.address;
       const args = [bucket, key, from] satisfies DeleteObjectParams;
+      const gasPrice = await this.client.publicClient.getGasPrice();
       const { request } = await this.contract.simulate.deleteObject<
         Chain,
         Account
       >(args, {
         account: this.client.walletClient.account,
+        gasPrice,
       });
       // TODO: calling `this.contract.write.deleteObject(...)` doesn't work, for some reason
       const hash = await this.client.walletClient.writeContract(request);
@@ -530,13 +561,64 @@ export class BucketManager {
   // Query objects in a bucket
   async query(
     bucket: Address,
-    options?: {
-      prefix?: string;
-      delimiter?: string;
-      startKey?: string;
-      limit?: number;
-      blockNumber?: bigint;
-    },
+    options?: QueryOptions,
+  ): Promise<Result<QueryResult>> {
+    const requestedLimit = options?.limit ?? MAX_QUERY_LIMIT;
+
+    // If within MAX_QUERY_LIMIT, use a single query
+    if (requestedLimit <= MAX_QUERY_LIMIT) {
+      return this.executeQuery(bucket, options);
+    }
+
+    // Otherwise, paginate and collect results. Note: this is needed to avoid
+    // hitting gas limits and failing to retrieve all objects.
+    try {
+      const allObjects: Array<QueryResultObject> = [];
+      let currentKey = options?.startKey;
+      while (true) {
+        const remainingLimit = requestedLimit - allObjects.length;
+        const batchLimit = Math.min(MAX_QUERY_LIMIT, remainingLimit);
+
+        const batchResult = await this.executeQuery(bucket, {
+          ...options,
+          startKey: currentKey,
+          limit: batchLimit,
+        });
+        allObjects.push(...batchResult.result.objects);
+
+        // If no `nextKey` or we've reached the limit, we've collected all objects
+        if (
+          !batchResult.result.nextKey ||
+          allObjects.length >= requestedLimit
+        ) {
+          return {
+            result: {
+              objects: allObjects,
+              commonPrefixes: batchResult.result.commonPrefixes,
+              nextKey: batchResult.result.nextKey,
+            },
+          };
+        }
+
+        // Update `startKey` for next iteration
+        currentKey = batchResult.result.nextKey;
+      }
+    } catch (error: unknown) {
+      if (
+        error instanceof BucketNotFound ||
+        error instanceof OutOfGasError ||
+        error instanceof UnhandledBucketError
+      ) {
+        throw error;
+      }
+      throw new UnhandledBucketError(`Failed to query bucket: ${error}`);
+    }
+  }
+
+  // Helper method for single query execution
+  private async executeQuery(
+    bucket: Address,
+    options?: QueryOptions,
   ): Promise<Result<QueryResult>> {
     try {
       const args = [
@@ -544,7 +626,7 @@ export class BucketManager {
         options?.prefix ?? "",
         options?.delimiter ?? "/",
         options?.startKey ?? "",
-        BigInt(options?.limit ?? 100),
+        BigInt(options?.limit ?? MAX_QUERY_LIMIT),
       ] satisfies QueryObjectsParams;
       const { objects, commonPrefixes, nextKey } =
         (await this.contract.read.queryObjects(args, {
@@ -566,10 +648,18 @@ export class BucketManager {
       return { result };
     } catch (error: unknown) {
       if (error instanceof ContractFunctionExecutionError) {
-        // TODO: We're optimistically assuming an error means the bucket doesn't exist
-        // 00: t0134 (method 3844450837) -- contract reverted (33)
-        // 01: t0134 (method 6) -- contract reverted (33)
-        throw new BucketNotFound(bucket);
+        if (error.message.includes("contract reverted")) {
+          const isOutOfGasError =
+            error.message.includes("wasm `unreachable` instruction executed") ||
+            error.message.includes("out of gas");
+          if (isOutOfGasError) {
+            throw new OutOfGasError(error.message);
+          }
+          // We're optimistically assuming this error means the bucket doesn't exist:
+          // 00: t0134 (method 3844450837) -- contract reverted (33)
+          // 01: t0134 (method 6) -- contract reverted (33)
+          throw new BucketNotFound(bucket);
+        }
       }
       throw new UnhandledBucketError(`Failed to query bucket: ${error}`);
     }
diff --git a/packages/sdk/src/entities/credit.ts b/packages/sdk/src/entities/credit.ts
index 692a0fe9f..b5e7245e9 100644
--- a/packages/sdk/src/entities/credit.ts
+++ b/packages/sdk/src/entities/credit.ts
@@ -149,11 +149,13 @@ export class CreditManager {
         gasFeeLimit,
         ttl,
       ] satisfies ApproveCreditParams;
+      const gasPrice = await this.client.publicClient.getGasPrice();
       const { request } = await this.contract.simulate.approveCredit<
         Chain,
         Account
       >(args, {
         account: this.client.walletClient.account,
+        gasPrice,
       });
       const hash = await this.client.walletClient.writeContract(request);
       const tx = await this.client.publicClient.waitForTransactionReceipt({
@@ -190,12 +192,14 @@ export class CreditManager {
     try {
       const toAddress = to || this.client.walletClient.account.address;
       const args = [toAddress] satisfies BuyCreditParams;
+      const gasPrice = await this.client.publicClient.getGasPrice();
       const { request } = await this.contract.simulate.buyCredit<
         Chain,
         Account
       >(args, {
         value: amount,
         account: this.client.walletClient.account,
+        gasPrice,
       });
       const hash = await this.contract.write.buyCredit(request);
       const tx = await this.client.publicClient.waitForTransactionReceipt({
@@ -233,11 +237,13 @@ export class CreditManager {
         to,
         requiredCaller,
       ] satisfies RevokeCreditParams;
+      const gasPrice = await this.client.publicClient.getGasPrice();
       const { request } = await this.contract.simulate.revokeCredit<
         Chain,
         Account
       >(args, {
         account: this.client.walletClient.account,
+        gasPrice,
       });
       // TODO: calling `this.contract.write.revokeCredit(...)` doesn't work, for some reason
       const hash = await this.client.walletClient.writeContract(request);
@@ -271,11 +277,13 @@ export class CreditManager {
     const fromAddress = from || this.client.walletClient.account.address;
     try {
       const args = [fromAddress, sponsor] satisfies SetAccountSponsorParams;
+      const gasPrice = await this.client.publicClient.getGasPrice();
       const { request } = await this.contract.simulate.setAccountSponsor<
         Chain,
         Account
       >(args, {
         account: this.client.walletClient.account,
+        gasPrice,
       });
       // TODO: calling `this.contract.write.setAccountSponsor(...)` doesn't work, for some reason
       const hash = await this.client.walletClient.writeContract(request);
diff --git a/packages/sdk/src/errors.ts b/packages/sdk/src/errors.ts
index 1549e1e7e..f5a8fa918 100644
--- a/packages/sdk/src/errors.ts
+++ b/packages/sdk/src/errors.ts
@@ -9,6 +9,13 @@ export class BucketNotFound extends Error {
   }
 }
 
+export class OutOfGasError extends Error {
+  constructor(message: string) {
+    super(`Out of gas: ${message}`);
+    this.name = "OutOfGas";
+  }
+}
+
 export class CreateBucketError extends Error {
   constructor(message: string) {
     super(`Failed to create bucket: ${message}`);
diff --git a/packages/sdk/src/ipc/gateway.ts b/packages/sdk/src/ipc/gateway.ts
index b580c27ee..9d73ea3bc 100644
--- a/packages/sdk/src/ipc/gateway.ts
+++ b/packages/sdk/src/ipc/gateway.ts
@@ -103,12 +103,14 @@ export class GatewayManager {
     try {
       const recipientAddress = recipient || walletClient.account.address;
       const args = fundParamsToTyped(recipientAddress, forSubnet, amount);
+      const gasPrice = await publicClient.getGasPrice();
       const { request } = await this.getContract(
         publicClient,
         walletClient,
         contractAddress,
       ).simulate.fundWithToken<Chain, Account>(args, {
         account: walletClient.account,
+        gasPrice,
       });
       // TODO: calling `this.getContract(client, contractAddress).write.fundWithToken(...)` doesn't work, for some reason
       const hash = await walletClient.writeContract(request);
@@ -138,6 +140,7 @@ export class GatewayManager {
     try {
       const address = recipient || walletClient.account.address;
       const args = releaseParamsToTyped(address);
+      const gasPrice = await publicClient.getGasPrice();
       const { request } = await this.getContract(
         publicClient,
         walletClient,
@@ -145,6 +148,7 @@ export class GatewayManager {
       ).simulate.release<Chain, Account>(args, {
         account: walletClient.account,
         value: amount,
+        gasPrice,
       });
       // TODO: calling `this.getContract(client, contractAddress).write.release(...)` doesn't work, for some reason
       const hash = await walletClient.writeContract(request);


Reviews:
user: login: dtbuchholz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T17:56:39Z
user: login: dtbuchholz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T17:59:00Z
user: login: asutula
body: With the caveat that I'm not super familiar with the gas issues within the sdk, looks good to me. Not sure if changes are needed in the portal since I've never experienced gas troubles there.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-15T21:36:10Z

Review comments:
diff_hunk: @@ -566,10 +648,18 @@ export class BucketManager {
       return { result };
     } catch (error: unknown) {
       if (error instanceof ContractFunctionExecutionError) {
-        // TODO: We're optimistically assuming an error means the bucket doesn't exist
-        // 00: t0134 (method 3844450837) -- contract reverted (33)
-        // 01: t0134 (method 6) -- contract reverted (33)
-        throw new BucketNotFound(bucket);
+        if (error.message.includes("contract reverted")) {
+          const isOutOfGasError =
+            error.message.includes("wasm `unreachable` instruction executed") ||
path: packages/sdk/src/entities/bucket.ts
user: login: dtbuchholz
body: meta observation: the fvm/wasm errors can be a bit too ambiguous. we'll see something like this if you query a bucket and it doesnt exist on the network, which is non-descriptive:
```
00: t0134 (method 3844450837) -- contract reverted (33)
01: t0134 (method 6) -- contract reverted (33)
```
but an out of gas issue will have an extra line that at least helps a bit:
```
02: t0134 (method 6) -- out of gas (7)
```
i.e., the js client has to make some assumption to help handle these errors nicely for the dev. 
created_at: 2025-04-11T17:56:39Z
author_association: COLLABORATOR
diff_hunk: @@ -530,21 +561,72 @@ export class BucketManager {
   // Query objects in a bucket
   async query(
     bucket: Address,
-    options?: {
-      prefix?: string;
-      delimiter?: string;
-      startKey?: string;
-      limit?: number;
-      blockNumber?: bigint;
-    },
+    options?: QueryOptions,
+  ): Promise<Result<QueryResult>> {
+    const requestedLimit = options?.limit ?? MAX_QUERY_LIMIT;
+
+    // If within MAX_QUERY_LIMIT, use a single query
+    if (requestedLimit <= MAX_QUERY_LIMIT) {
+      return this.executeQuery(bucket, options);
+    }
+
+    // Otherwise, paginate and collect results. Note: this is needed to avoid
+    // hitting gas limits and failing to retrieve all objects.
+    try {
+      const allObjects: Array<QueryResultObject> = [];
path: packages/sdk/src/entities/bucket.ts
user: login: dtbuchholz
body: for reference, here's where we batch `query` calls automatically. since the query limit is so low (50 objects), i figured it makes sense to do this automatically vs adding some flag or another method for batching multiple queries.
created_at: 2025-04-11T17:59:00Z
author_association: COLLABORATOR
diff_hunk: @@ -162,6 +162,11 @@ export const MIN_TTL = 3600n; // one hour
  */
 export const MAX_OBJECT_SIZE = 5_000_000_000; // 5GB
 
+/**
+ * Maximum number of objects that can be returned in a single query (to avoid hitting gas limits).
+ */
+export const MAX_QUERY_LIMIT = 50;
+
path: packages/network-constants/src/index.ts
user: login: asutula
body: We can incorporate this into our query react hook.
created_at: 2025-04-15T21:31:32Z
author_association: MEMBER

PR comments:
user: login: vercel[bot]
created_at: 2025-04-11T17:43:26Z
body: [vc]: #Xdy9IN7X2e7b4EUQXjqZFJfEja6HscAGn5TaWc5etg0=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJwb3J0YWwiLCJyb290RGlyZWN0b3J5IjoiYXBwcy9wb3J0YWwiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoicG9ydGFsLWdpdC1kdGItZml4LWdhcy1yZWNhbGxuZXQudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcmVjYWxsbmV0L3BvcnRhbC9EVWlEc2RGUGppZExaemtjaXE3UHhYbTdLRXFpIiwicHJldmlld1VybCI6InBvcnRhbC1naXQtZHRiLWZpeC1nYXMtcmVjYWxsbmV0LnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQifSx7Im5hbWUiOiJmYXVjZXQiLCJyb290RGlyZWN0b3J5IjoiYXBwcy9mYXVjZXQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiZmF1Y2V0LWdpdC1kdGItZml4LWdhcy1yZWNhbGxuZXQudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcmVjYWxsbmV0L2ZhdWNldC9DajhhWkQ2cnFmYzdLSnA5eTdrQ2hiZW4yQWVRIiwicHJldmlld1VybCI6ImZhdWNldC1naXQtZHRiLWZpeC1nYXMtcmVjYWxsbmV0LnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **faucet** | ✅ Ready ([Inspect](https://vercel.com/recallnet/faucet/Cj8aZD6rqfc7KJp9y7kChben2AeQ)) | [Visit Preview](https://faucet-git-dtb-fix-gas-recallnet.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/faucet-git-dtb-fix-gas-recallnet.vercel.app?via=pr-comment-feedback-link) | Apr 11, 2025 6:12pm |
| **portal** | ✅ Ready ([Inspect](https://vercel.com/recallnet/portal/DUiDsdFPjidLZzkciq7PxXm7KEqi)) | [Visit Preview](https://portal-git-dtb-fix-gas-recallnet.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/portal-git-dtb-fix-gas-recallnet.vercel.app?via=pr-comment-feedback-link) | Apr 11, 2025 6:12pm |



author_association: CONTRIBUTOR
user: login: dtbuchholz
created_at: 2025-04-11T17:45:43Z
body: @asutula i didnt look at the Portal/frontend code, but it might need to do something similar as what i did for the sdk:
- paginate objects (it looks like you _might_ do this already) when you query objects in a bucket
- explicitly set the gas price when you execute txs

also cc @mzkrasner this should fix the RECALL token issues we see
author_association: COLLABORATOR
user: login: asutula
created_at: 2025-04-15T21:24:53Z
body: > i didnt look at the Portal/frontend code, but it might need to do something similar as what i did for the sdk:

Hey @dtbuchholz have you ever seen gas problems with the portal? I haven't, but maybe haven't been pushing it in the same ways you have. 

The portal does paginate objects. It does not explicitly set gas price when sending txns.
author_association: MEMBER
user: login: dtbuchholz
created_at: 2025-04-15T22:31:13Z
body: > > i didnt look at the Portal/frontend code, but it might need to do something similar as what i did for the sdk:
> 
> Hey @dtbuchholz have you ever seen gas problems with the portal? I haven't, but maybe haven't been pushing it in the same ways you have.
> 
> The portal does paginate objects. It does not explicitly set gas price when sending txns.

oddly, no. i've only seen it with the programmatic tooling. maybe it happens when an account quickly adds objects to a bucket one after another...which isnt really possible to do fast enough in the frontend?

also, somewhat related, check out [this issue](https://github.com/recallnet/rust-recall/issues/264) re: how query results are returned to a user. TL;DR—queried objects are returned based on the HAMT's representation (e.g., not lexicographical / chronological). to improve the DX, _i think_ we'll have to alter the SDK to query _all_ objects + sort/filter so that data returned to users isn't confusing. that'll reuse some of this PRs logic, too.
author_association: COLLABORATOR