@pushchain/core 6.0.24 → 6.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/package.json +1 -1
  3. package/src/lib/constants/abi/index.d.ts +1 -0
  4. package/src/lib/constants/abi/index.js +4 -1
  5. package/src/lib/constants/abi/prc20.evm.d.ts +32 -0
  6. package/src/lib/constants/abi/prc20.evm.js +20 -0
  7. package/src/lib/constants/abi/uea-factory.d.ts +31 -0
  8. package/src/lib/constants/abi/uea-factory.js +19 -0
  9. package/src/lib/constants/abi/universalCallback.evm.d.ts +561 -0
  10. package/src/lib/constants/abi/universalCallback.evm.js +247 -0
  11. package/src/lib/constants/abi/universalGatewayV0.json +1 -1
  12. package/src/lib/constants/abi/universalReadRegistry.evm.d.ts +247 -0
  13. package/src/lib/constants/abi/universalReadRegistry.evm.js +38 -0
  14. package/src/lib/constants/chain.d.ts +12 -0
  15. package/src/lib/constants/chain.js +24 -2
  16. package/src/lib/constants/enums.d.ts +7 -0
  17. package/src/lib/constants/enums.js +11 -0
  18. package/src/lib/constants/index.d.ts +54 -0
  19. package/src/lib/constants/index.js +38 -8
  20. package/src/lib/constants/read-state.d.ts +57 -0
  21. package/src/lib/constants/read-state.js +63 -0
  22. package/src/lib/generated/ucallback/v1/index.d.ts +2 -0
  23. package/src/lib/generated/ucallback/v1/index.js +6 -0
  24. package/src/lib/generated/ucallback/v1/query.d.ts +47 -0
  25. package/src/lib/generated/ucallback/v1/query.js +131 -0
  26. package/src/lib/generated/ucallback/v1/types.d.ts +99 -0
  27. package/src/lib/generated/ucallback/v1/types.js +366 -0
  28. package/src/lib/generated/uexecutor/v1/query.d.ts +29 -0
  29. package/src/lib/generated/uexecutor/v1/query.js +98 -1
  30. package/src/lib/index.d.ts +9 -0
  31. package/src/lib/index.js +38 -1
  32. package/src/lib/orchestrator/internals/cascade.js +5 -2
  33. package/src/lib/orchestrator/internals/errors.d.ts +6 -0
  34. package/src/lib/orchestrator/internals/errors.js +13 -1
  35. package/src/lib/orchestrator/internals/index.d.ts +1 -0
  36. package/src/lib/orchestrator/internals/index.js +6 -1
  37. package/src/lib/orchestrator/internals/push-chain-tx.js +81 -61
  38. package/src/lib/orchestrator/internals/read-state.d.ts +24 -0
  39. package/src/lib/orchestrator/internals/read-state.js +47 -0
  40. package/src/lib/orchestrator/internals/route-handlers.js +6 -1
  41. package/src/lib/orchestrator/internals/svm-helpers.js +24 -0
  42. package/src/lib/orchestrator/orchestrator.d.ts +26 -0
  43. package/src/lib/orchestrator/orchestrator.js +22 -0
  44. package/src/lib/orchestrator/orchestrator.types.d.ts +2 -0
  45. package/src/lib/orchestrator/route-detector.js +4 -0
  46. package/src/lib/progress-hook/progress-hook.js +292 -0
  47. package/src/lib/progress-hook/progress-hook.types.d.ts +35 -0
  48. package/src/lib/progress-hook/progress-hook.types.js +37 -0
  49. package/src/lib/push-chain/push-chain.d.ts +71 -0
  50. package/src/lib/push-chain/push-chain.js +28 -0
  51. package/src/lib/push-client/push-client.d.ts +42 -2
  52. package/src/lib/push-client/push-client.js +152 -13
  53. package/src/lib/read-state/budget.d.ts +16 -0
  54. package/src/lib/read-state/budget.js +32 -0
  55. package/src/lib/read-state/destination.d.ts +12 -0
  56. package/src/lib/read-state/destination.js +51 -0
  57. package/src/lib/read-state/envelopes/evm.d.ts +24 -0
  58. package/src/lib/read-state/envelopes/evm.js +147 -0
  59. package/src/lib/read-state/envelopes/index.d.ts +12 -0
  60. package/src/lib/read-state/envelopes/index.js +45 -0
  61. package/src/lib/read-state/envelopes/svm.d.ts +32 -0
  62. package/src/lib/read-state/envelopes/svm.js +117 -0
  63. package/src/lib/read-state/envelopes/web2.d.ts +29 -0
  64. package/src/lib/read-state/envelopes/web2.js +149 -0
  65. package/src/lib/read-state/errors.d.ts +70 -0
  66. package/src/lib/read-state/errors.js +116 -0
  67. package/src/lib/read-state/index.d.ts +13 -0
  68. package/src/lib/read-state/index.js +38 -0
  69. package/src/lib/read-state/preflight.d.ts +21 -0
  70. package/src/lib/read-state/preflight.js +70 -0
  71. package/src/lib/read-state/read-events.d.ts +27 -0
  72. package/src/lib/read-state/read-events.js +135 -0
  73. package/src/lib/read-state/read-executor.d.ts +18 -0
  74. package/src/lib/read-state/read-executor.js +216 -0
  75. package/src/lib/read-state/read-params.d.ts +250 -0
  76. package/src/lib/read-state/read-params.js +164 -0
  77. package/src/lib/read-state/read-state.types.d.ts +458 -0
  78. package/src/lib/read-state/read-state.types.js +72 -0
  79. package/src/lib/read-state/read-tracker.d.ts +55 -0
  80. package/src/lib/read-state/read-tracker.js +481 -0
  81. package/src/lib/read-state/registry.d.ts +21 -0
  82. package/src/lib/read-state/registry.js +53 -0
  83. package/src/lib/read-state/result-decoder.d.ts +16 -0
  84. package/src/lib/read-state/result-decoder.js +79 -0
  85. package/src/lib/read-state/spec-builder.d.ts +47 -0
  86. package/src/lib/read-state/spec-builder.js +251 -0
  87. package/src/lib/read-state/svm-account.d.ts +20 -0
  88. package/src/lib/read-state/svm-account.js +114 -0
  89. package/src/lib/read-state/svm-pda.d.ts +13 -0
  90. package/src/lib/read-state/svm-pda.js +147 -0
  91. package/src/lib/read-state/validate.d.ts +22 -0
  92. package/src/lib/read-state/validate.js +80 -0
  93. package/src/lib/universal-tx-detector/child-inbounds.d.ts +3 -1
  94. package/src/lib/universal-tx-detector/child-inbounds.js +4 -5
  95. package/src/lib/universal-tx-detector/detector-svm.js +29 -20
  96. package/src/lib/universal-tx-detector/svm-events.d.ts +17 -5
  97. package/src/lib/universal-tx-detector/svm-events.js +81 -6
  98. package/src/lib/vm-client/evm-client.js +27 -10
@@ -5,12 +5,15 @@ import { TxBody, TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx';
5
5
  import { DeliverTxResponse } from '@cosmjs/stargate';
6
6
  import { QueryGetUniversalTxResponse } from '../generated/uexecutor/v1/query';
7
7
  import { QueryGetUniversalTxResponseV2 } from '../generated/uexecutor/v2/query';
8
+ import { QueryReadsByTxResponse, QueryUniversalReadResponse } from '../generated/ucallback/v1';
8
9
  import { EvmClient } from '../vm-client/evm-client';
9
10
  import { PushClientOptions } from './push-client.types';
10
11
  import { TxResponse } from '../vm-client/vm-client.types';
11
12
  export declare class PushClient extends EvmClient {
12
- /** Gas limit for Cosmos transactions on Push Chain */
13
- private static readonly COSMOS_GAS_LIMIT;
13
+ /** Conservative SDK ceiling and fallback for gasless Cosmos transactions. */
14
+ private static readonly DEFAULT_COSMOS_GAS_LIMIT;
15
+ private static readonly GASLESS_GAS_LIMIT_CACHE_TTL_MS;
16
+ private static readonly MSG_EXECUTE_PAYLOAD_TYPE_URL;
14
17
  pushChainInfo: {
15
18
  chainId: string;
16
19
  vm: import("../constants/enums").VM;
@@ -42,6 +45,7 @@ export declare class PushClient extends EvmClient {
42
45
  private readonly ephemeralKey;
43
46
  private readonly ephemeralAccount;
44
47
  private currentRpcIndex;
48
+ private gaslessGasLimitCache?;
45
49
  /**
46
50
  * Archive (full-history) endpoints. History-sensitive reads fall back from
47
51
  * the prune RPC to these on a pruned-history miss. Resolved from the chain's
@@ -50,6 +54,13 @@ export declare class PushClient extends EvmClient {
50
54
  */
51
55
  private readonly archiveTendermintRpc;
52
56
  private readonly archivePublicClient?;
57
+ /**
58
+ * Prune endpoints with transport retries off, raced against the archive. A
59
+ * receipt miss on the prune node answers -32002 "Requested resource not
60
+ * available" after ~8 s (measured on Donut 2026-09-23), and the shared
61
+ * client's `http({ retryCount: 5 })` would retry that on top.
62
+ */
63
+ private readonly pruneProbeClient?;
53
64
  constructor(clientOptions: PushClientOptions);
54
65
  /**
55
66
  * Executes an operation with automatic fallback to next RPC endpoint on failure.
@@ -93,11 +104,40 @@ export declare class PushClient extends EvmClient {
93
104
  * In prod, signer should be passed in instead.
94
105
  */
95
106
  signCosmosTx(txBody: TxBody): Promise<TxRaw>;
107
+ private resolveCosmosGasLimit;
96
108
  broadcastCosmosTx(txRaw: TxRaw): Promise<DeliverTxResponse>;
97
109
  /**
98
110
  * Queries Push Chain's uexecutor gRPC service for a UniversalTx by its ID.
99
111
  */
100
112
  getUniversalTxById(id: string): Promise<QueryGetUniversalTxResponse>;
113
+ /**
114
+ * Queries x/ucallback for one cross-chain read by its requestId (0x-prefixed uint256 hex).
115
+ *
116
+ * Current-state query — an absent `read` is a real "does not exist", so there is
117
+ * deliberately no archive fallback (see getUniversalTxByIdV2 for the rationale).
118
+ */
119
+ getUniversalRead(requestId: string): Promise<QueryUniversalReadResponse>;
120
+ /**
121
+ * Cosmos `block_results` for one height, raw over the Tendermint HTTP RPC. Push Chain
122
+ * runs CometBFT 0.38 and emits `finalize_block_events`, which the cosmjs 0.33 decoders
123
+ * still drop (they only read `end_block_events`) — hence the direct call. This is the
124
+ * only place the EndBlocker's work is visible: read-state expiry runs there and leaves
125
+ * no EVM-indexed tx or receipt. Attribute values are plain strings.
126
+ */
127
+ getBlockResultEvents(height: number): Promise<readonly {
128
+ type: string;
129
+ attributes: readonly {
130
+ key: string;
131
+ value: string;
132
+ }[];
133
+ }[]>;
134
+ /**
135
+ * Queries x/ucallback for every read a Push transaction requested. One tx can emit
136
+ * several ReadRequested logs; this reassembles that batch. The node keys on the exact
137
+ * lowercase 0x hash, which is what the SDK's own tx responses carry — verified for
138
+ * both EOA and UEA-originated requests.
139
+ */
140
+ getReadsByTx(txHash: string): Promise<QueryReadsByTxResponse>;
101
141
  /**
102
142
  * Queries Push Chain's uexecutor v2 gRPC service for a UniversalTx by its ID.
103
143
  * V2 returns expanded OutboundTx fields and repeated outbound_tx array.
@@ -8,6 +8,7 @@ const proto_signing_1 = require("@cosmjs/proto-signing");
8
8
  const stargate_1 = require("@cosmjs/stargate");
9
9
  const query_1 = require("../generated/uexecutor/v1/query");
10
10
  const query_2 = require("../generated/uexecutor/v2/query");
11
+ const v1_1 = require("../generated/ucallback/v1");
11
12
  const crypto_1 = require("@cosmjs/crypto");
12
13
  const tendermint_rpc_1 = require("@cosmjs/tendermint-rpc");
13
14
  const auth_1 = require("cosmjs-types/cosmos/auth/v1beta1/auth");
@@ -24,13 +25,21 @@ function pushNetworkToChain(network) {
24
25
  return enums_1.CHAIN.PUSH_TESTNET_DONUT;
25
26
  return enums_1.CHAIN.PUSH_LOCALNET;
26
27
  }
28
+ /** ABCI code 22 / gRPC NotFound from x/ucallback — "no read request with id …: key not found". */
29
+ function isUcallbackNotFound(err) {
30
+ const msg = err instanceof Error ? err.message : String(err);
31
+ return /\(22\)|code = NotFound|key not found/i.test(msg);
32
+ }
27
33
  class PushClient extends evm_client_1.EvmClient {
28
- /** Gas limit for Cosmos transactions on Push Chain */
29
- static COSMOS_GAS_LIMIT = 100000000000;
34
+ /** Conservative SDK ceiling and fallback for gasless Cosmos transactions. */
35
+ static DEFAULT_COSMOS_GAS_LIMIT = 100_000_000;
36
+ static GASLESS_GAS_LIMIT_CACHE_TTL_MS = 60_000;
37
+ static MSG_EXECUTE_PAYLOAD_TYPE_URL = '/uexecutor.v1.MsgExecutePayload';
30
38
  pushChainInfo;
31
39
  ephemeralKey;
32
40
  ephemeralAccount;
33
41
  currentRpcIndex = 0;
42
+ gaslessGasLimitCache;
34
43
  /**
35
44
  * Archive (full-history) endpoints. History-sensitive reads fall back from
36
45
  * the prune RPC to these on a pruned-history miss. Resolved from the chain's
@@ -39,6 +48,13 @@ class PushClient extends evm_client_1.EvmClient {
39
48
  */
40
49
  archiveTendermintRpc;
41
50
  archivePublicClient;
51
+ /**
52
+ * Prune endpoints with transport retries off, raced against the archive. A
53
+ * receipt miss on the prune node answers -32002 "Requested resource not
54
+ * available" after ~8 s (measured on Donut 2026-09-23), and the shared
55
+ * client's `http({ retryCount: 5 })` would retry that on top.
56
+ */
57
+ pruneProbeClient;
42
58
  constructor(clientOptions) {
43
59
  const pushChainKey = pushNetworkToChain(clientOptions.network);
44
60
  const chain = clientOptions.chain ?? (0, chain_1.getPushViemChain)(pushChainKey);
@@ -57,6 +73,10 @@ class PushClient extends evm_client_1.EvmClient {
57
73
  chain,
58
74
  transport: (0, viem_1.fallback)(archiveEvmUrls.map((url) => (0, viem_1.http)(url, { retryCount: 5, retryDelay: 500 }))),
59
75
  });
76
+ this.pruneProbeClient = (0, viem_1.createPublicClient)({
77
+ chain,
78
+ transport: (0, viem_1.fallback)(clientOptions.rpcUrls.map((url) => (0, viem_1.http)(url, { retryCount: 0 }))),
79
+ });
60
80
  }
61
81
  this.ephemeralKey = (0, accounts_1.generatePrivateKey)();
62
82
  this.ephemeralAccount = (0, accounts_1.privateKeyToAccount)(this.ephemeralKey);
@@ -215,6 +235,7 @@ class PushClient extends evm_client_1.EvmClient {
215
235
  const status = await tmClient.status();
216
236
  const chainId = status.nodeInfo.network;
217
237
  const queryClient = stargate_1.QueryClient.withExtensions(tmClient, stargate_1.setupAuthExtension);
238
+ const cosmosGasLimit = await this.resolveCosmosGasLimit(txBody, queryClient, chainId);
218
239
  let baseAccount = null;
219
240
  try {
220
241
  const accountResp = await queryClient.auth.account(sender);
@@ -245,7 +266,7 @@ class PushClient extends evm_client_1.EvmClient {
245
266
  pubkey: pubkeyEncoded,
246
267
  sequence: baseAccount ? Number(baseAccount.sequence) : 0,
247
268
  },
248
- ], [], PushClient.COSMOS_GAS_LIMIT, undefined, undefined);
269
+ ], [], cosmosGasLimit, undefined, undefined);
249
270
  const txBodyBytes = tx_2.TxBody.encode(txBody).finish();
250
271
  const signDoc = (0, proto_signing_1.makeSignDoc)(txBodyBytes, authInfoBytes, chainId, baseAccount ? Number(baseAccount.accountNumber) : 0);
251
272
  const digest = (0, viem_1.keccak256)(tx_2.SignDoc.encode(signDoc).finish());
@@ -257,6 +278,42 @@ class PushClient extends evm_client_1.EvmClient {
257
278
  });
258
279
  }, 'signCosmosTx');
259
280
  }
281
+ async resolveCosmosGasLimit(txBody, queryClient, chainId) {
282
+ const isGaslessExecutePayload = txBody.messages.length > 0 &&
283
+ txBody.messages.every(({ typeUrl }) => typeUrl === PushClient.MSG_EXECUTE_PAYLOAD_TYPE_URL);
284
+ if (!isGaslessExecutePayload) {
285
+ return PushClient.DEFAULT_COSMOS_GAS_LIMIT;
286
+ }
287
+ const cached = this.gaslessGasLimitCache;
288
+ if (cached?.chainId === chainId && cached.expiresAt > Date.now()) {
289
+ return cached.gasLimit;
290
+ }
291
+ try {
292
+ const rpc = (0, stargate_1.createProtobufRpcClient)(queryClient);
293
+ const request = query_1.QueryParamsRequest.fromPartial({});
294
+ const responseBytes = await rpc.request('uexecutor.v1.Query', 'Params', query_1.QueryParamsRequest.encode(request).finish());
295
+ const response = query_1.QueryParamsResponse.decode(responseBytes);
296
+ const onChainMaximum = response.params?.maxGaslessTxGas ?? BigInt(0);
297
+ if (onChainMaximum > BigInt(0)) {
298
+ const selected = onChainMaximum < BigInt(PushClient.DEFAULT_COSMOS_GAS_LIMIT)
299
+ ? onChainMaximum
300
+ : BigInt(PushClient.DEFAULT_COSMOS_GAS_LIMIT);
301
+ const gasLimit = Number(selected);
302
+ if (Number.isSafeInteger(gasLimit) && gasLimit > 0) {
303
+ this.gaslessGasLimitCache = {
304
+ chainId,
305
+ gasLimit,
306
+ expiresAt: Date.now() + PushClient.GASLESS_GAS_LIMIT_CACHE_TTL_MS,
307
+ };
308
+ return gasLimit;
309
+ }
310
+ }
311
+ }
312
+ catch {
313
+ // Older/unavailable query endpoints fall back to the current safe cap.
314
+ }
315
+ return PushClient.DEFAULT_COSMOS_GAS_LIMIT;
316
+ }
260
317
  async broadcastCosmosTx(txRaw) {
261
318
  return this.executeWithRpcFallback(async (rpcUrl) => {
262
319
  const client = await stargate_1.StargateClient.connect(rpcUrl);
@@ -276,6 +333,73 @@ class PushClient extends evm_client_1.EvmClient {
276
333
  return query_1.QueryGetUniversalTxResponse.decode(responseBytes);
277
334
  }, 'getUniversalTxById');
278
335
  }
336
+ /**
337
+ * Queries x/ucallback for one cross-chain read by its requestId (0x-prefixed uint256 hex).
338
+ *
339
+ * Current-state query — an absent `read` is a real "does not exist", so there is
340
+ * deliberately no archive fallback (see getUniversalTxByIdV2 for the rationale).
341
+ */
342
+ async getUniversalRead(requestId) {
343
+ const id = requestId.toLowerCase();
344
+ return this.executeWithRpcFallback(async (rpcUrl) => {
345
+ const tmClient = await tendermint_rpc_1.Tendermint34Client.connect(rpcUrl);
346
+ const queryClient = new stargate_1.QueryClient(tmClient);
347
+ const rpc = (0, stargate_1.createProtobufRpcClient)(queryClient);
348
+ const request = v1_1.QueryUniversalReadRequest.fromPartial({ requestId: id });
349
+ try {
350
+ const responseBytes = await rpc.request(v1_1.UCALLBACK_QUERY_SERVICE, 'UniversalRead', v1_1.QueryUniversalReadRequest.encode(request).finish());
351
+ return v1_1.QueryUniversalReadResponse.decode(responseBytes);
352
+ }
353
+ catch (err) {
354
+ // The node answers an unknown id with ABCI code 22 (sdk NotFound), which cosmjs
355
+ // throws. That is a deterministic miss, not a transport failure — classify it
356
+ // here so the RPC fallback does not retry it on every endpoint.
357
+ if (isUcallbackNotFound(err))
358
+ return v1_1.QueryUniversalReadResponse.fromPartial({});
359
+ throw err;
360
+ }
361
+ }, 'getUniversalRead');
362
+ }
363
+ /**
364
+ * Cosmos `block_results` for one height, raw over the Tendermint HTTP RPC. Push Chain
365
+ * runs CometBFT 0.38 and emits `finalize_block_events`, which the cosmjs 0.33 decoders
366
+ * still drop (they only read `end_block_events`) — hence the direct call. This is the
367
+ * only place the EndBlocker's work is visible: read-state expiry runs there and leaves
368
+ * no EVM-indexed tx or receipt. Attribute values are plain strings.
369
+ */
370
+ async getBlockResultEvents(height) {
371
+ // Prune nodes drop old heights ("height N is not available"); the archive keeps them.
372
+ return this.executeWithArchiveFallback(async (rpcUrl) => {
373
+ const url = `${rpcUrl.replace(/\/+$/, '')}/block_results?height=${height}`;
374
+ const res = await fetch(url);
375
+ // CometBFT answers a pruned height with HTTP 500 plus a JSON-RPC error. Surface that
376
+ // error so it reads as a definite miss (→ archive), not a transient HTTP failure to retry.
377
+ const body = (await res.json().catch(() => undefined));
378
+ if (body?.error)
379
+ throw new Error(`block_results ${height}: ${body.error.data ?? body.error.message ?? 'error'}`);
380
+ if (!res.ok || !body)
381
+ throw new Error(`HTTP request failed: ${res.status} ${url}`);
382
+ const events = body.result?.finalize_block_events ?? body.result?.end_block_events ?? [];
383
+ return events;
384
+ }, 'getBlockResultEvents', () => false);
385
+ }
386
+ /**
387
+ * Queries x/ucallback for every read a Push transaction requested. One tx can emit
388
+ * several ReadRequested logs; this reassembles that batch. The node keys on the exact
389
+ * lowercase 0x hash, which is what the SDK's own tx responses carry — verified for
390
+ * both EOA and UEA-originated requests.
391
+ */
392
+ async getReadsByTx(txHash) {
393
+ const hash = txHash.toLowerCase();
394
+ return this.executeWithRpcFallback(async (rpcUrl) => {
395
+ const tmClient = await tendermint_rpc_1.Tendermint34Client.connect(rpcUrl);
396
+ const queryClient = new stargate_1.QueryClient(tmClient);
397
+ const rpc = (0, stargate_1.createProtobufRpcClient)(queryClient);
398
+ const request = v1_1.QueryReadsByTxRequest.fromPartial({ txHash: hash });
399
+ const responseBytes = await rpc.request(v1_1.UCALLBACK_QUERY_SERVICE, 'ReadsByTx', v1_1.QueryReadsByTxRequest.encode(request).finish());
400
+ return v1_1.QueryReadsByTxResponse.decode(responseBytes);
401
+ }, 'getReadsByTx');
402
+ }
279
403
  /**
280
404
  * Queries Push Chain's uexecutor v2 gRPC service for a UniversalTx by its ID.
281
405
  * V2 returns expanded OutboundTx fields and repeated outbound_tx array.
@@ -356,7 +480,10 @@ class PushClient extends evm_client_1.EvmClient {
356
480
  throw error;
357
481
  const archiveClient = this.archivePublicClient;
358
482
  const tx = await archiveClient.getTransaction({ hash: txHash });
359
- const wait = async (confirmations = 1) => archiveClient.waitForTransactionReceipt({ hash: txHash, confirmations });
483
+ const wait = async (confirmations = 1) => archiveClient.waitForTransactionReceipt({
484
+ hash: txHash,
485
+ confirmations,
486
+ });
360
487
  return { ...tx, wait };
361
488
  }
362
489
  }
@@ -366,17 +493,29 @@ class PushClient extends evm_client_1.EvmClient {
366
493
  * Prune first; on not-found, retry the archive EVM client if configured.
367
494
  */
368
495
  async getTransactionReceiptWithArchiveFallback(txHash) {
369
- try {
370
- return await this.publicClient.getTransactionReceipt({ hash: txHash });
371
- }
372
- catch (error) {
373
- if (!this.hasArchiveEvm)
374
- throw error;
375
- return this.archivePublicClient.getTransactionReceipt({
376
- hash: txHash,
377
- });
496
+ if (!this.hasArchiveEvm || !this.pruneProbeClient) {
497
+ return this.publicClient.getTransactionReceipt({ hash: txHash });
378
498
  }
499
+ // Race prune and archive; the first receipt wins. A pruned receipt costs one
500
+ // archive round trip, a fresh one the prune round trip (if the archive lags).
501
+ const prune = this.pruneProbeClient.getTransactionReceipt({ hash: txHash });
502
+ const archive = this.archivePublicClient.getTransactionReceipt({
503
+ hash: txHash,
504
+ });
505
+ return firstFulfilled([archive, prune]);
379
506
  }
380
507
  }
381
508
  exports.PushClient = PushClient;
509
+ /** First promise to resolve; if all reject, the last rejection. */
510
+ function firstFulfilled(promises) {
511
+ return new Promise((resolve, reject) => {
512
+ let pending = promises.length;
513
+ for (const p of promises) {
514
+ p.then(resolve, (error) => {
515
+ if (--pending === 0)
516
+ reject(error);
517
+ });
518
+ }
519
+ });
520
+ }
382
521
  //# sourceMappingURL=push-client.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Gas price assumed when the chain reports 0 (dev chains). Donut charges 1 gwei per
3
+ * gas on module fulfil txs (verified: 118,289 gas → 118,289 × 10⁹ wei burned).
4
+ */
5
+ export declare const MIN_CALLBACK_BUDGET_GAS_PRICE = 1000000000n;
6
+ /**
7
+ * Size the callback budget escrowed on top of the protocol fee.
8
+ *
9
+ * msg.value = protocolFee + callbackBudget
10
+ *
11
+ * The node refuses to fulfil a read whose budget cannot cover its callback
12
+ * ("callback budget too small") and lets it expire with the fee already spent, so a
13
+ * zero or thin budget is a guaranteed loss. This errs high by `buffer`; whatever the
14
+ * callback does not burn is pushed back to `refundTo` at settlement.
15
+ */
16
+ export declare function sizeCallbackBudget(callbackGasLimit: bigint, pushGasPrice: bigint, buffer?: number): bigint;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MIN_CALLBACK_BUDGET_GAS_PRICE = void 0;
4
+ exports.sizeCallbackBudget = sizeCallbackBudget;
5
+ const read_state_1 = require("../constants/read-state");
6
+ const errors_1 = require("./errors");
7
+ /**
8
+ * Gas price assumed when the chain reports 0 (dev chains). Donut charges 1 gwei per
9
+ * gas on module fulfil txs (verified: 118,289 gas → 118,289 × 10⁹ wei burned).
10
+ */
11
+ exports.MIN_CALLBACK_BUDGET_GAS_PRICE = 1000000000n;
12
+ /**
13
+ * Size the callback budget escrowed on top of the protocol fee.
14
+ *
15
+ * msg.value = protocolFee + callbackBudget
16
+ *
17
+ * The node refuses to fulfil a read whose budget cannot cover its callback
18
+ * ("callback budget too small") and lets it expire with the fee already spent, so a
19
+ * zero or thin budget is a guaranteed loss. This errs high by `buffer`; whatever the
20
+ * callback does not burn is pushed back to `refundTo` at settlement.
21
+ */
22
+ function sizeCallbackBudget(callbackGasLimit, pushGasPrice, buffer = read_state_1.CALLBACK_BUDGET_BUFFER) {
23
+ if (callbackGasLimit <= 0n)
24
+ throw new errors_1.InvalidReadQueryError('callbackGasLimit must be > 0');
25
+ if (!Number.isInteger(buffer) || buffer < 1)
26
+ throw new errors_1.InvalidReadQueryError('budget buffer must be an integer ≥ 1');
27
+ const price = pushGasPrice > 0n ? pushGasPrice : exports.MIN_CALLBACK_BUDGET_GAS_PRICE;
28
+ const budget = callbackGasLimit * price * BigInt(buffer);
29
+ // cannot be zero by construction, but the invariant is worth stating
30
+ return budget > 0n ? budget : 1n;
31
+ }
32
+ //# sourceMappingURL=budget.js.map
@@ -0,0 +1,12 @@
1
+ import type { ReadDestination, ResolvedDestination } from './read-state.types';
2
+ /**
3
+ * Split a destination into the pieces the contract, the oracle and the node use.
4
+ *
5
+ * - `ReadSpec.account.chainNamespace` / `chainId` are the two halves.
6
+ * - `UniversalCore.chainHeightByChainNamespace` is keyed by the JOINED `caip2`.
7
+ * - The node routes on `chainNamespace + ":" + chainId`.
8
+ *
9
+ * Passing the joined form as the namespace would produce `eip155:1:1` on the node,
10
+ * so it is rejected here.
11
+ */
12
+ export declare function resolveDestination(dest: ReadDestination): ResolvedDestination;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveDestination = resolveDestination;
4
+ const read_state_1 = require("../constants/read-state");
5
+ const errors_1 = require("./errors");
6
+ const KNOWN = new Set(Object.values(read_state_1.READ_NAMESPACE));
7
+ /**
8
+ * Split a destination into the pieces the contract, the oracle and the node use.
9
+ *
10
+ * - `ReadSpec.account.chainNamespace` / `chainId` are the two halves.
11
+ * - `UniversalCore.chainHeightByChainNamespace` is keyed by the JOINED `caip2`.
12
+ * - The node routes on `chainNamespace + ":" + chainId`.
13
+ *
14
+ * Passing the joined form as the namespace would produce `eip155:1:1` on the node,
15
+ * so it is rejected here.
16
+ */
17
+ function resolveDestination(dest) {
18
+ let chainNamespace;
19
+ let chainId;
20
+ if ('chain' in dest) {
21
+ const idx = dest.chain.indexOf(':');
22
+ if (idx <= 0) {
23
+ throw new errors_1.UnsupportedReadDestinationError(`chain is not CAIP-2: ${dest.chain}`);
24
+ }
25
+ chainNamespace = dest.chain.slice(0, idx);
26
+ chainId = dest.chain.slice(idx + 1);
27
+ }
28
+ else {
29
+ chainNamespace = dest.chainNamespace;
30
+ chainId = dest.chainId;
31
+ }
32
+ if (!chainNamespace || !chainId) {
33
+ throw new errors_1.UnsupportedReadDestinationError('destination needs both chainNamespace and chainId');
34
+ }
35
+ if (chainNamespace.includes(':')) {
36
+ throw new errors_1.UnsupportedReadDestinationError(`chainNamespace must be the bare namespace, not CAIP-2: ${chainNamespace}`, { hint: 'Pass { chainNamespace: "eip155", chainId: "1" } or { chain: CHAIN.X }.' });
37
+ }
38
+ if (!KNOWN.has(chainNamespace)) {
39
+ throw new errors_1.UnsupportedReadDestinationError(`unsupported read namespace: ${chainNamespace}`, {
40
+ destination: `${chainNamespace}:${chainId}`,
41
+ hint: `Supported: ${[...KNOWN].join(', ')}`,
42
+ });
43
+ }
44
+ return {
45
+ chainNamespace,
46
+ chainId,
47
+ caip2: `${chainNamespace}:${chainId}`,
48
+ namespace: chainNamespace,
49
+ };
50
+ }
51
+ //# sourceMappingURL=destination.js.map
@@ -0,0 +1,24 @@
1
+ import { type Hex } from 'viem';
2
+ import type { EncodedReadQuery, EvmReadQuery } from '../read-state.types';
3
+ /** `universalClient/externalchains/evm/read_envelope.go` — evmQueryType. */
4
+ export declare const EVM_QUERY_TYPE: {
5
+ readonly ACCOUNT_BALANCE: 0;
6
+ readonly CONTRACT_CALL: 1;
7
+ readonly STORAGE_SLOT: 2;
8
+ };
9
+ /** Normalise a storage slot (hex or bigint) to bytes32. */
10
+ export declare function toStorageSlot(slot: Hex | bigint): Hex;
11
+ /**
12
+ * Encode an `eip155` read query. Pure — `blockNumber` is the pinned destination
13
+ * height and is also mirrored into `ReadSpec.blockNumber` by the spec builder.
14
+ */
15
+ export declare function encodeEvmQueryEnvelope(query: EvmReadQuery, options: {
16
+ blockNumber: bigint;
17
+ }): EncodedReadQuery;
18
+ /** Decode an envelope with the exact tuple shape the validator uses. */
19
+ export declare function decodeEvmQueryEnvelope(encoded: Hex): {
20
+ queryType: number;
21
+ refType: number;
22
+ blockNumber: bigint;
23
+ payload: Hex;
24
+ };
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EVM_QUERY_TYPE = void 0;
4
+ exports.toStorageSlot = toStorageSlot;
5
+ exports.encodeEvmQueryEnvelope = encodeEvmQueryEnvelope;
6
+ exports.decodeEvmQueryEnvelope = decodeEvmQueryEnvelope;
7
+ const viem_1 = require("viem");
8
+ const read_state_1 = require("../../constants/read-state");
9
+ const errors_1 = require("../errors");
10
+ /** `universalClient/externalchains/evm/read_envelope.go` — evmQueryType. */
11
+ exports.EVM_QUERY_TYPE = {
12
+ ACCOUNT_BALANCE: 0,
13
+ CONTRACT_CALL: 1,
14
+ STORAGE_SLOT: 2,
15
+ };
16
+ /** Only AT_NUMBER exists in v1. */
17
+ const EVM_BLOCK_REF_AT_NUMBER = 0;
18
+ /**
19
+ * The validator unpacks `abi.Arguments{ ONE tuple }`. The envelope MUST therefore be
20
+ * `abi.encode(struct)` — a single dynamic tuple with a 0x20 head offset — never the
21
+ * fields as separate parameters. The three-parameter layout parses only by coincidence
22
+ * when queryType == 0 and cost the first live read (2026-09-09).
23
+ */
24
+ const EVM_ENVELOPE_ABI = [
25
+ {
26
+ type: 'tuple',
27
+ components: [
28
+ { name: 'queryType', type: 'uint8' },
29
+ {
30
+ name: 'blockRef',
31
+ type: 'tuple',
32
+ components: [
33
+ { name: 'refType', type: 'uint8' },
34
+ { name: 'blockNumber', type: 'uint64' },
35
+ ],
36
+ },
37
+ { name: 'payload', type: 'bytes' },
38
+ ],
39
+ },
40
+ ];
41
+ const UINT64_MAX = (1n << 64n) - 1n;
42
+ function assertAddress(value, what) {
43
+ if (!(0, viem_1.isAddress)(value))
44
+ throw new errors_1.InvalidReadQueryError(`${what} is not an EVM address: ${value}`);
45
+ }
46
+ function assertBlockNumber(blockNumber) {
47
+ if (blockNumber < 0n || blockNumber > UINT64_MAX) {
48
+ throw new errors_1.InvalidReadQueryError(`blockNumber out of uint64 range: ${blockNumber}`);
49
+ }
50
+ }
51
+ /** Normalise a storage slot (hex or bigint) to bytes32. */
52
+ function toStorageSlot(slot) {
53
+ if (typeof slot === 'bigint') {
54
+ if (slot < 0n || slot >= 1n << 256n)
55
+ throw new errors_1.InvalidReadQueryError(`storage slot out of range: ${slot}`);
56
+ return (0, viem_1.toHex)(slot, { size: 32 });
57
+ }
58
+ if (!(0, viem_1.isHex)(slot))
59
+ throw new errors_1.InvalidReadQueryError(`storage slot is not hex: ${slot}`);
60
+ if ((0, viem_1.size)(slot) > 32)
61
+ throw new errors_1.InvalidReadQueryError(`storage slot longer than 32 bytes: ${slot}`);
62
+ return (0, viem_1.pad)(slot, { size: 32 });
63
+ }
64
+ function encodeEnvelope(queryType, blockNumber, payload) {
65
+ return (0, viem_1.encodeAbiParameters)(EVM_ENVELOPE_ABI, [
66
+ { queryType, blockRef: { refType: EVM_BLOCK_REF_AT_NUMBER, blockNumber }, payload },
67
+ ]);
68
+ }
69
+ /**
70
+ * Encode an `eip155` read query. Pure — `blockNumber` is the pinned destination
71
+ * height and is also mirrored into `ReadSpec.blockNumber` by the spec builder.
72
+ */
73
+ function encodeEvmQueryEnvelope(query, options) {
74
+ const { blockNumber } = options;
75
+ assertBlockNumber(blockNumber);
76
+ assertAddress(query.target, 'target');
77
+ let queryType;
78
+ let payload;
79
+ let resultShape;
80
+ switch (query.type) {
81
+ case 'accountBalance': {
82
+ queryType = exports.EVM_QUERY_TYPE.ACCOUNT_BALANCE;
83
+ // abi.encode(address) — 32 bytes. The raw 20-byte address is rejected by validators.
84
+ payload = (0, viem_1.encodeAbiParameters)([{ type: 'address' }], [query.target]);
85
+ resultShape = { kind: 'uint256' };
86
+ break;
87
+ }
88
+ case 'contractCall': {
89
+ queryType = exports.EVM_QUERY_TYPE.CONTRACT_CALL;
90
+ let callData;
91
+ if ('callData' in query) {
92
+ if (!(0, viem_1.isHex)(query.callData) || (0, viem_1.size)(query.callData) < 4) {
93
+ throw new errors_1.InvalidReadQueryError('callData must be hex with at least a 4-byte selector');
94
+ }
95
+ callData = query.callData;
96
+ resultShape = { kind: 'raw' };
97
+ }
98
+ else {
99
+ const item = (0, viem_1.getAbiItem)({
100
+ abi: query.abi,
101
+ name: query.functionName,
102
+ args: query.args,
103
+ });
104
+ if (!item || item.type !== 'function') {
105
+ throw new errors_1.InvalidReadQueryError(`function not found in abi: ${query.functionName}`);
106
+ }
107
+ // Any mutability: validators run the call as an eth_call simulation at the
108
+ // pinned block, so nonpayable/payable functions return data and never execute.
109
+ callData = (0, viem_1.encodeFunctionData)({
110
+ abi: query.abi,
111
+ functionName: query.functionName,
112
+ args: query.args,
113
+ });
114
+ resultShape = { kind: 'evmCall', abi: [item], functionName: query.functionName };
115
+ }
116
+ payload = (0, viem_1.encodeAbiParameters)([{ type: 'address' }, { type: 'bytes' }], [query.target, callData]);
117
+ break;
118
+ }
119
+ case 'storageSlot': {
120
+ queryType = exports.EVM_QUERY_TYPE.STORAGE_SLOT;
121
+ payload = (0, viem_1.encodeAbiParameters)([{ type: 'address' }, { type: 'bytes32' }], [query.target, toStorageSlot(query.slot)]);
122
+ resultShape = { kind: 'bytes32' };
123
+ break;
124
+ }
125
+ default:
126
+ throw new errors_1.InvalidReadQueryError(`unknown EVM query type: ${query.type}`);
127
+ }
128
+ return {
129
+ namespace: read_state_1.READ_NAMESPACE.EVM,
130
+ queryType,
131
+ encoded: encodeEnvelope(queryType, blockNumber, payload),
132
+ blockRef: blockNumber,
133
+ resultShape,
134
+ warnings: [],
135
+ };
136
+ }
137
+ /** Decode an envelope with the exact tuple shape the validator uses. */
138
+ function decodeEvmQueryEnvelope(encoded) {
139
+ const [env] = (0, viem_1.decodeAbiParameters)(EVM_ENVELOPE_ABI, encoded);
140
+ return {
141
+ queryType: env.queryType,
142
+ refType: env.blockRef.refType,
143
+ blockNumber: env.blockRef.blockNumber,
144
+ payload: env.payload,
145
+ };
146
+ }
147
+ //# sourceMappingURL=evm.js.map
@@ -0,0 +1,12 @@
1
+ import type { EncodedReadQuery, ReadDestination, ReadQuery } from '../read-state.types';
2
+ export * from './evm';
3
+ export * from './svm';
4
+ export * from './web2';
5
+ /**
6
+ * Encode any read query for its destination. Rejects a query kind that does not
7
+ * belong to the destination's namespace (e.g. a `storageSlot` read against Solana).
8
+ */
9
+ export declare function encodeReadQuery(destination: ReadDestination, query: ReadQuery, options?: {
10
+ blockNumber?: bigint;
11
+ minSlot?: bigint;
12
+ }): EncodedReadQuery;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.encodeReadQuery = encodeReadQuery;
4
+ const tslib_1 = require("tslib");
5
+ const read_state_1 = require("../../constants/read-state");
6
+ const destination_1 = require("../destination");
7
+ const errors_1 = require("../errors");
8
+ const evm_1 = require("./evm");
9
+ const svm_1 = require("./svm");
10
+ const web2_1 = require("./web2");
11
+ tslib_1.__exportStar(require("./evm"), exports);
12
+ tslib_1.__exportStar(require("./svm"), exports);
13
+ tslib_1.__exportStar(require("./web2"), exports);
14
+ const EVM_TYPES = new Set(['accountBalance', 'contractCall', 'storageSlot']);
15
+ const SVM_TYPES = new Set(['lamportBalance', 'splTokenAccount', 'rawAccountData']);
16
+ /**
17
+ * Encode any read query for its destination. Rejects a query kind that does not
18
+ * belong to the destination's namespace (e.g. a `storageSlot` read against Solana).
19
+ */
20
+ function encodeReadQuery(destination, query, options = {}) {
21
+ const dest = (0, destination_1.resolveDestination)(destination);
22
+ switch (dest.namespace) {
23
+ case read_state_1.READ_NAMESPACE.EVM: {
24
+ if (!EVM_TYPES.has(query.type))
25
+ throw new errors_1.InvalidReadQueryError(`query type ${query.type} is not valid for ${dest.caip2}`);
26
+ if (options.blockNumber === undefined) {
27
+ throw new errors_1.InvalidReadQueryError('EVM reads need options.blockNumber (the pinned destination height)');
28
+ }
29
+ return (0, evm_1.encodeEvmQueryEnvelope)(query, { blockNumber: options.blockNumber });
30
+ }
31
+ case read_state_1.READ_NAMESPACE.SVM: {
32
+ if (!SVM_TYPES.has(query.type))
33
+ throw new errors_1.InvalidReadQueryError(`query type ${query.type} is not valid for ${dest.caip2}`);
34
+ return (0, svm_1.encodeSvmQueryEnvelope)(query, { minSlot: options.minSlot });
35
+ }
36
+ case read_state_1.READ_NAMESPACE.WEB2: {
37
+ if (query.type !== 'http')
38
+ throw new errors_1.InvalidReadQueryError(`query type ${query.type} is not valid for web2`);
39
+ return (0, web2_1.encodeWeb2QueryEnvelope)(query);
40
+ }
41
+ default:
42
+ throw new errors_1.UnsupportedReadDestinationError(`unsupported namespace: ${dest.namespace}`);
43
+ }
44
+ }
45
+ //# sourceMappingURL=index.js.map