@pushchain/core 6.0.25 → 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 (32) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/package.json +1 -1
  3. package/src/lib/constants/chain.js +1 -1
  4. package/src/lib/constants/index.d.ts +5 -3
  5. package/src/lib/constants/index.js +1 -0
  6. package/src/lib/constants/read-state.d.ts +6 -2
  7. package/src/lib/constants/read-state.js +7 -3
  8. package/src/lib/index.d.ts +1 -1
  9. package/src/lib/index.js +3 -2
  10. package/src/lib/progress-hook/progress-hook.js +21 -0
  11. package/src/lib/progress-hook/progress-hook.types.d.ts +4 -1
  12. package/src/lib/progress-hook/progress-hook.types.js +3 -0
  13. package/src/lib/push-chain/push-chain.d.ts +2 -1
  14. package/src/lib/push-chain/push-chain.js +4 -3
  15. package/src/lib/push-client/push-client.d.ts +7 -0
  16. package/src/lib/push-client/push-client.js +41 -15
  17. package/src/lib/read-state/envelopes/evm.js +2 -3
  18. package/src/lib/read-state/envelopes/svm.js +11 -1
  19. package/src/lib/read-state/read-executor.js +4 -0
  20. package/src/lib/read-state/read-params.d.ts +68 -23
  21. package/src/lib/read-state/read-params.js +57 -13
  22. package/src/lib/read-state/read-state.types.d.ts +41 -0
  23. package/src/lib/read-state/read-state.types.js +23 -1
  24. package/src/lib/read-state/read-tracker.d.ts +6 -1
  25. package/src/lib/read-state/read-tracker.js +81 -15
  26. package/src/lib/read-state/registry.js +2 -3
  27. package/src/lib/read-state/result-decoder.js +6 -0
  28. package/src/lib/read-state/spec-builder.js +1 -1
  29. package/src/lib/read-state/svm-account.d.ts +20 -0
  30. package/src/lib/read-state/svm-account.js +114 -0
  31. package/src/lib/read-state/svm-pda.d.ts +13 -0
  32. package/src/lib/read-state/svm-pda.js +147 -0
@@ -3,13 +3,17 @@
3
3
  *
4
4
  * read(user, { chain }) native balance (ETH / lamports)
5
5
  * read(user, { chain, token }) token balance (ERC-20 / SPL via ATA)
6
- * read(contract, { chain, abi, functionName, args }) typed call — abi encodes AND decodes
6
+ * read(contract, { chain, abi, functionName, args }) typed call — abi encodes AND decodes (any mutability; eth_call simulation)
7
+ * read(account, { chain, idl, functionName? }) Anchor account, layout picked by discriminator
8
+ * read(program, { chain, idl, functionName, args }) Anchor PDA — args are its IDL-declared seeds
7
9
  * read(contract, { chain, storageSlot }) storage word
8
10
  * read(url, { chain: CHAIN.WEB2, web2 }) web2
9
11
  *
10
12
  * Query keys are mutually exclusive; the namespace decides which are legal.
11
13
  */
12
- import type { Abi, Address, Hex, ContractFunctionReturnType, ContractFunctionName, ContractFunctionArgs, ExtractAbiFunctionForArgs } from 'viem';
14
+ import type { Abi, AbiStateMutability, Address, Hex, ContractFunctionReturnType, ContractFunctionName, ContractFunctionArgs, ExtractAbiFunctionForArgs } from 'viem';
15
+ import type { Idl, IdlAccounts } from '@coral-xyz/anchor';
16
+ import { PublicKey } from '@solana/web3.js';
13
17
  import { CHAIN, PUSH_NETWORK } from '../constants/enums';
14
18
  import type { BuildReadSpecParams, ReadChain, ReadDestination, ReadLifecycleOptions, ReadQuery, ReadResultShape, Web2Extract } from './read-state.types';
15
19
  import type { ReadCallback } from './read-state.types';
@@ -32,11 +36,23 @@ interface ReadOptionsInput {
32
36
  chain: ReadChain;
33
37
  /** Token balance of `subject`: ERC-20 `balanceOf` on EVM, the SPL ATA on SVM. */
34
38
  token?: string;
35
- /** Solana token balance only. Defaults to the original SPL Token program. */
36
- tokenProgram?: 'spl-token' | 'token-2022';
39
+ /**
40
+ * Anchor IDL — the Solana counterpart of `abi`. The subject account is decoded
41
+ * with the layout whose discriminator matches its data. Reads never execute an
42
+ * instruction.
43
+ */
44
+ idl?: Idl;
37
45
  /** Typed contract call — encodes the call and decodes the result. */
38
46
  abi?: Abi;
47
+ /**
48
+ * With `abi`: the function to call (required). With `idl`: the account layout
49
+ * (optional — inferred from the discriminator; snake_case or camelCase).
50
+ */
39
51
  functionName?: string;
52
+ /**
53
+ * With `abi`: positional call args. With `idl` and the program id as subject:
54
+ * the PDA's non-constant seeds, in IDL order.
55
+ */
40
56
  args?: readonly unknown[];
41
57
  /** EVM storage word. */
42
58
  storageSlot?: Hex | bigint;
@@ -60,6 +76,8 @@ interface ReadOptionsInput {
60
76
  type NoQuery = {
61
77
  token?: never;
62
78
  tokenProgram?: never;
79
+ idl?: never;
80
+ accountName?: never;
63
81
  abi?: never;
64
82
  functionName?: never;
65
83
  args?: never;
@@ -79,9 +97,14 @@ export type ReadQueryOptions = ({
79
97
  token: string;
80
98
  }>) | ({
81
99
  chain: SvmChain;
82
- } & Query<'token' | 'tokenProgram', {
100
+ } & Query<'token', {
83
101
  token: string;
84
- tokenProgram?: 'spl-token' | 'token-2022';
102
+ }>) | ({
103
+ chain: SvmChain;
104
+ } & Query<'idl' | 'functionName' | 'args', {
105
+ idl: Idl;
106
+ functionName?: string;
107
+ args?: readonly unknown[];
85
108
  }>) | ({
86
109
  chain: EvmChain;
87
110
  } & Query<'abi' | 'functionName' | 'args', {
@@ -100,7 +123,7 @@ export type ReadQueryOptions = ({
100
123
  export type ReadCallbackOptions = {
101
124
  callback?: ({
102
125
  target: Address;
103
- gasLimit: bigint;
126
+ gasLimit?: bigint;
104
127
  } & Pick<ReadCallback, 'abi' | 'functionName' | 'args'>) | {
105
128
  target?: never;
106
129
  gasLimit?: bigint;
@@ -128,7 +151,7 @@ export type ReadPrepareOptions = SharedPrepareOptions & ((Extract<ReadQueryOptio
128
151
  export type ReadExecuteOptions = Pick<ReadOptionsInput, 'advanced' | 'waitForCompletion' | 'progressHook'>;
129
152
  type WithExecutionOptions<T> = T extends unknown ? Omit<T, 'advanced' | 'waitForCompletion' | 'progressHook'> & ReadExecuteOptions : never;
130
153
  export type ReadOptions = WithExecutionOptions<ReadPrepareOptions> & {
131
- callback?: (Required<Pick<ReadCallback, 'target' | 'gasLimit' | 'abi' | 'functionName'>> & Pick<ReadCallback, 'args'>) | {
154
+ callback?: (Required<Pick<ReadCallback, 'target' | 'abi' | 'functionName'>> & Pick<ReadCallback, 'gasLimit' | 'args'>) | {
132
155
  target?: never;
133
156
  gasLimit?: bigint;
134
157
  abi?: never;
@@ -148,11 +171,11 @@ type Web2Values = {
148
171
  string: string;
149
172
  };
150
173
  type Web2Value<E extends Web2Extract> = Web2Values[E['valueType']];
151
- type ReadFunctionName<A extends Abi, F extends string> = F & ContractFunctionName<A, 'view' | 'pure'>;
174
+ type ReadFunctionName<A extends Abi, F extends string> = F & ContractFunctionName<A, AbiStateMutability>;
152
175
  type ReadFunctionArgs<O, A extends Abi, F extends string> = O extends {
153
- args: infer Args extends ContractFunctionArgs<A, 'view' | 'pure', ReadFunctionName<A, F>>;
154
- } ? Args : readonly [] extends ContractFunctionArgs<A, 'view' | 'pure', ReadFunctionName<A, F>> ? readonly [] : ContractFunctionArgs<A, 'view' | 'pure', ReadFunctionName<A, F>>;
155
- type ReadCallValue<A extends Abi, F extends string, Args extends ContractFunctionArgs<A, 'view' | 'pure', ReadFunctionName<A, F>>> = ExtractAbiFunctionForArgs<A, 'view' | 'pure', ReadFunctionName<A, F>, Args> extends infer Selected ? Selected extends Abi[number] & {
176
+ args: infer Args extends ContractFunctionArgs<A, AbiStateMutability, ReadFunctionName<A, F>>;
177
+ } ? Args : readonly [] extends ContractFunctionArgs<A, AbiStateMutability, ReadFunctionName<A, F>> ? readonly [] : ContractFunctionArgs<A, AbiStateMutability, ReadFunctionName<A, F>>;
178
+ type ReadCallValue<A extends Abi, F extends string, Args extends ContractFunctionArgs<A, AbiStateMutability, ReadFunctionName<A, F>>> = ExtractAbiFunctionForArgs<A, AbiStateMutability, ReadFunctionName<A, F>, Args> extends infer Selected ? Selected extends Abi[number] & {
156
179
  type: 'function';
157
180
  outputs: readonly unknown[];
158
181
  } ? ContractFunctionReturnType<readonly [Selected]> : never : never;
@@ -160,7 +183,7 @@ type ReadCallValue<A extends Abi, F extends string, Args extends ContractFunctio
160
183
  export type ReadValue<O> = O extends {
161
184
  abi: infer A extends Abi;
162
185
  functionName: infer F extends string;
163
- } ? number extends A['length'] ? readonly unknown[] : ReadCallValue<A, F, ReadFunctionArgs<O, A, F>> : O extends {
186
+ } ? number extends A['length'] ? unknown : ReadCallValue<A, F, ReadFunctionArgs<O, A, F>> : O extends {
164
187
  web2: {
165
188
  extract: infer E extends readonly Web2Extract[];
166
189
  };
@@ -168,7 +191,19 @@ export type ReadValue<O> = O extends {
168
191
  readonly [K in keyof E]: Web2Value<E[K]>;
169
192
  } : O extends {
170
193
  storageSlot: unknown;
171
- } ? Hex : bigint;
194
+ } ? Hex : O extends {
195
+ idl: infer I extends Idl;
196
+ } ? IdlReadValue<I, O extends {
197
+ functionName: infer N extends string;
198
+ } ? N : undefined> : bigint;
199
+ /** snake_case / camelCase / PascalCase spellings compare equal (mirrors `idlNameKey`). */
200
+ type IdlNameKey<S extends string> = Lowercase<S extends `${infer H}_${infer T}` ? `${H}${IdlNameKey<T>}` : S>;
201
+ type IdlAccountName<I extends Idl> = Extract<keyof IdlAccounts<I>, string>;
202
+ type MatchAccount<I extends Idl, N extends string> = {
203
+ [K in IdlAccountName<I>]: IdlNameKey<K> extends IdlNameKey<N> ? K : never;
204
+ }[IdlAccountName<I>];
205
+ /** A literal IDL types the account; a dynamic one (names typed `string`) yields `unknown`. */
206
+ type IdlReadValue<I extends Idl, N extends string | undefined> = string extends IdlAccountName<I> ? unknown : N extends string ? IdlAccounts<I>[MatchAccount<I, N>] : IdlAccounts<I>[IdlAccountName<I>];
172
207
  /** Just `balanceOf` — it also types the decoded result as uint256. */
173
208
  export declare const ERC20_BALANCE_OF_ABI: readonly [{
174
209
  readonly type: "function";
@@ -185,21 +220,31 @@ export declare const ERC20_BALANCE_OF_ABI: readonly [{
185
220
  }];
186
221
  export declare function toReadDestination(chain: ReadChain): ReadDestination;
187
222
  /** Build the internal query from the public grammar. Pure. */
188
- export declare function toReadQuery(subject: string, o: ReadOptionsInput): ReadQuery;
223
+ export declare function toReadQuery(subject: string, o: ReadOptionsInput, resolvedTokenProgram?: PublicKey): ReadQuery;
189
224
  /** Public options → the spec builder's params. Pure; throws `InvalidReadQueryError` on a malformed request. */
190
- export declare function toBuildReadSpecParams(subject: string, o: ReadOptionsInput, network?: PUSH_NETWORK): BuildReadSpecParams;
191
- /** Offline logical query key for the registry's latestResult lookup. */
225
+ export declare function toBuildReadSpecParams(subject: string, o: ReadOptionsInput, network?: PUSH_NETWORK, resolvedTokenProgram?: PublicKey): BuildReadSpecParams;
226
+ /** Resolve Solana mint ownership before the otherwise-pure options conversion. */
227
+ export declare function resolveReadSpecParams(subject: string, o: ReadOptionsInput, network: PUSH_NETWORK, rpcUrls: Partial<Record<CHAIN, string[]>>): Promise<BuildReadSpecParams>;
228
+ /** Internal offline key helper. Solana token callers must use PreparedRead.queryKey after mint resolution. */
192
229
  export declare function getReadQueryKey<const O extends ReadQueryOptions>(subject: string, options: O & ValidateReadCall<O>): Hex;
193
230
  export declare function toLifecycleOptions(o?: ReadTrackOptions): ReadLifecycleOptions;
194
- /** Restrict known ABIs to read functions and their declared argument tuple. */
231
+ /**
232
+ * Restrict known ABIs to declared functions and their argument tuple. Every mutability
233
+ * is allowed: the read is an eth_call simulation, so state-changing functions only return data.
234
+ */
195
235
  export type ValidateReadCall<O> = O extends {
196
236
  abi: infer A extends Abi;
197
237
  functionName: infer F extends string;
198
238
  } ? {
199
- functionName: ContractFunctionName<A, 'view' | 'pure'>;
200
- } & (readonly [] extends ContractFunctionArgs<A, 'view' | 'pure', F & ContractFunctionName<A, 'view' | 'pure'>> ? {
201
- args?: ContractFunctionArgs<A, 'view' | 'pure', F & ContractFunctionName<A, 'view' | 'pure'>>;
239
+ functionName: ContractFunctionName<A, AbiStateMutability>;
240
+ } & (readonly [] extends ContractFunctionArgs<A, AbiStateMutability, F & ContractFunctionName<A, AbiStateMutability>> ? {
241
+ args?: ContractFunctionArgs<A, AbiStateMutability, F & ContractFunctionName<A, AbiStateMutability>>;
202
242
  } : {
203
- args: ContractFunctionArgs<A, 'view' | 'pure', F & ContractFunctionName<A, 'view' | 'pure'>>;
204
- }) : unknown;
243
+ args: ContractFunctionArgs<A, AbiStateMutability, F & ContractFunctionName<A, AbiStateMutability>>;
244
+ }) : O extends {
245
+ idl: infer I extends Idl;
246
+ functionName: infer N extends string;
247
+ } ? string extends IdlAccountName<I> ? unknown : [MatchAccount<I, N>] extends [never] ? {
248
+ functionName: IdlAccountName<I>;
249
+ } : unknown : unknown;
205
250
  export {};
@@ -4,9 +4,13 @@ exports.ERC20_BALANCE_OF_ABI = void 0;
4
4
  exports.toReadDestination = toReadDestination;
5
5
  exports.toReadQuery = toReadQuery;
6
6
  exports.toBuildReadSpecParams = toBuildReadSpecParams;
7
+ exports.resolveReadSpecParams = resolveReadSpecParams;
7
8
  exports.getReadQueryKey = getReadQueryKey;
8
9
  exports.toLifecycleOptions = toLifecycleOptions;
9
10
  const viem_1 = require("viem");
11
+ const web3_js_1 = require("@solana/web3.js");
12
+ const svm_account_1 = require("./svm-account");
13
+ const svm_pda_1 = require("./svm-pda");
10
14
  const enums_1 = require("../constants/enums");
11
15
  const registry_1 = require("./registry");
12
16
  const read_state_1 = require("../constants/read-state");
@@ -23,14 +27,18 @@ exports.ERC20_BALANCE_OF_ABI = [
23
27
  outputs: [{ name: '', type: 'uint256' }],
24
28
  },
25
29
  ];
26
- const QUERY_KEYS = ['token', 'abi', 'storageSlot', 'web2'];
30
+ const QUERY_KEYS = ['token', 'abi', 'idl', 'storageSlot', 'web2'];
27
31
  function queryKind(o) {
32
+ if ('tokenProgram' in o)
33
+ throw new errors_1.InvalidReadQueryError('tokenProgram is detected from the mint; remove options.tokenProgram');
34
+ if ('accountName' in o)
35
+ throw new errors_1.InvalidReadQueryError('accountName was removed; pass the account layout as functionName (optional — inferred from the discriminator)');
28
36
  const present = QUERY_KEYS.filter((k) => o[k] !== undefined);
29
37
  if (present.length > 1) {
30
38
  throw new errors_1.InvalidReadQueryError(`query keys are mutually exclusive, got ${present.join(' + ')}`);
31
39
  }
32
- if ((o.functionName !== undefined || o.args !== undefined) && o.abi === undefined) {
33
- throw new errors_1.InvalidReadQueryError('functionName and args need abi');
40
+ if ((o.functionName !== undefined || o.args !== undefined) && o.abi === undefined && o.idl === undefined) {
41
+ throw new errors_1.InvalidReadQueryError('functionName and args need abi or idl');
34
42
  }
35
43
  return present[0] ?? 'native';
36
44
  }
@@ -38,12 +46,9 @@ function toReadDestination(chain) {
38
46
  return chain === enums_1.CHAIN.WEB2 ? read_state_1.WEB2_DESTINATION : { chain };
39
47
  }
40
48
  /** Build the internal query from the public grammar. Pure. */
41
- function toReadQuery(subject, o) {
49
+ function toReadQuery(subject, o, resolvedTokenProgram) {
42
50
  const dest = (0, destination_1.resolveDestination)(toReadDestination(o.chain));
43
51
  const kind = queryKind(o);
44
- if (o.tokenProgram !== undefined && (dest.namespace !== read_state_1.READ_NAMESPACE.SVM || kind !== 'token' || !['spl-token', 'token-2022'].includes(o.tokenProgram))) {
45
- throw new errors_1.InvalidReadQueryError('tokenProgram requires a Solana token balance query and must be spl-token or token-2022');
46
- }
47
52
  if (dest.namespace === read_state_1.READ_NAMESPACE.WEB2) {
48
53
  if (kind !== 'web2' || !o.web2)
49
54
  throw new errors_1.InvalidReadQueryError('a web2 read needs the `web2` option and no other query key');
@@ -75,24 +80,52 @@ function toReadQuery(subject, o) {
75
80
  throw new errors_1.InvalidReadQueryError(`unsupported query for eip155: ${kind}`);
76
81
  }
77
82
  }
78
- // SVM: the account rides in ReadSpec.account.owner; the ATA is derived here, no network.
83
+ // SVM: token-program ownership has already been resolved by the async public path.
79
84
  switch (kind) {
80
85
  case 'native':
81
86
  return { type: 'lamportBalance', account: subject };
82
87
  case 'token':
83
- return { type: 'splTokenAccount', account: (0, svm_1.deriveAssociatedTokenAddress)(subject, o.token, o.tokenProgram === 'token-2022' ? svm_1.TOKEN_2022_PROGRAM_ID : svm_1.TOKEN_PROGRAM_ID).toBase58() };
88
+ if (!resolvedTokenProgram)
89
+ throw new errors_1.InvalidReadQueryError('Solana token reads need mint-owner resolution; use universal.prepareRead or universal.read');
90
+ return { type: 'splTokenAccount', account: (0, svm_1.deriveAssociatedTokenAddress)(subject, o.token, resolvedTokenProgram).toBase58() };
91
+ case 'idl':
92
+ return toIdlAccountQuery(subject, o);
84
93
  default:
85
- throw new errors_1.InvalidReadQueryError(`unsupported query for solana: ${kind} (program reads land with idl support)`);
94
+ throw new errors_1.InvalidReadQueryError(`unsupported query for solana: ${kind}; use idl for account-state decoding`);
95
+ }
96
+ }
97
+ /**
98
+ * `idl` → a raw account read that decodes with the IDL. The subject is the account,
99
+ * or the program id when `args` are the seeds of the `functionName` PDA.
100
+ */
101
+ function toIdlAccountQuery(subject, o) {
102
+ const idl = o.idl;
103
+ (0, svm_account_1.validateIdl)(idl);
104
+ const accountName = o.functionName === undefined ? undefined : (0, svm_account_1.resolveAccountName)(idl, o.functionName);
105
+ if ((0, svm_pda_1.isProgramSubject)(idl, subject)) {
106
+ if (!o.functionName)
107
+ throw new errors_1.InvalidReadQueryError('the subject is the program id: pass functionName (the account layout) and args (its PDA seeds) to derive the account');
108
+ const pda = (0, svm_pda_1.deriveIdlPda)(idl, o.functionName, o.args ?? []);
109
+ return { type: 'rawAccountData', account: pda.toBase58(), idl, accountName };
110
+ }
111
+ if (o.args !== undefined)
112
+ throw new errors_1.InvalidReadQueryError('with idl, args are PDA seeds and need the program id as the subject');
113
+ try {
114
+ new web3_js_1.PublicKey(subject);
86
115
  }
116
+ catch {
117
+ throw new errors_1.InvalidReadQueryError(`Solana subject must be a base58 pubkey, got ${subject}`);
118
+ }
119
+ return { type: 'rawAccountData', account: subject, idl, accountName };
87
120
  }
88
121
  /** Public options → the spec builder's params. Pure; throws `InvalidReadQueryError` on a malformed request. */
89
- function toBuildReadSpecParams(subject, o, network = enums_1.PUSH_NETWORK.TESTNET_DONUT) {
122
+ function toBuildReadSpecParams(subject, o, network = enums_1.PUSH_NETWORK.TESTNET_DONUT, resolvedTokenProgram) {
90
123
  const destination = toReadDestination(o.chain);
91
124
  const namespace = (0, destination_1.resolveDestination)(destination).namespace;
92
125
  if (namespace !== read_state_1.READ_NAMESPACE.EVM && (o.blockNumber !== undefined || o.minConfirmations !== undefined)) {
93
126
  throw new errors_1.InvalidReadQueryError(`${namespace} reads do not expose blockNumber or minConfirmations; the SDK selects the read reference internally`);
94
127
  }
95
- const query = toReadQuery(subject, o);
128
+ const query = toReadQuery(subject, o, resolvedTokenProgram);
96
129
  const callback = (0, registry_1.resolveReadCallback)(o.callback, network, (0, registry_1.computeReadQueryKey)(destination, query));
97
130
  return {
98
131
  callback,
@@ -106,7 +139,18 @@ function toBuildReadSpecParams(subject, o, network = enums_1.PUSH_NETWORK.TESTNE
106
139
  maxFee: o.maxFee,
107
140
  };
108
141
  }
109
- /** Offline logical query key for the registry's latestResult lookup. */
142
+ /** Resolve Solana mint ownership before the otherwise-pure options conversion. */
143
+ async function resolveReadSpecParams(subject, o, network, rpcUrls) {
144
+ const kind = queryKind(o);
145
+ const namespace = (0, destination_1.resolveDestination)(toReadDestination(o.chain)).namespace;
146
+ if (namespace === read_state_1.READ_NAMESPACE.SVM && kind === 'token') {
147
+ new web3_js_1.PublicKey(subject);
148
+ const program = await (0, svm_account_1.detectTokenProgram)(o.token, o.chain, rpcUrls[o.chain]);
149
+ return toBuildReadSpecParams(subject, o, network, program);
150
+ }
151
+ return toBuildReadSpecParams(subject, o, network);
152
+ }
153
+ /** Internal offline key helper. Solana token callers must use PreparedRead.queryKey after mint resolution. */
110
154
  function getReadQueryKey(subject, options) {
111
155
  return (0, registry_1.computeReadQueryKey)(toReadDestination(options.chain), toReadQuery(subject, options));
112
156
  }
@@ -1,4 +1,5 @@
1
1
  import type { Abi, Address, Hex } from 'viem';
2
+ import type { Idl } from '@coral-xyz/anchor';
2
3
  import type { CHAIN } from '../constants/enums';
3
4
  import type { ReadNamespace } from '../constants/read-state';
4
5
  /**
@@ -48,6 +49,8 @@ export type SvmReadQuery = {
48
49
  } | {
49
50
  type: 'rawAccountData';
50
51
  account: string | Uint8Array;
52
+ idl?: Idl; /** Omitted → layout inferred from the discriminator. */
53
+ accountName?: string;
51
54
  };
52
55
  export type Web2ValueType = 'uint256' | 'int256' | 'bool' | 'string' | 'bytes';
53
56
  export interface Web2Extract {
@@ -71,6 +74,10 @@ export interface Web2ReadQuery {
71
74
  }
72
75
  export type ReadQuery = EvmReadQuery | SvmReadQuery | Web2ReadQuery;
73
76
  export type ReadResultShape = {
77
+ kind: 'svmAccount';
78
+ idl: Idl; /** Omitted → layout inferred from the discriminator. */
79
+ accountName?: string;
80
+ } | {
74
81
  kind: 'uint256';
75
82
  } | {
76
83
  kind: 'bytes32';
@@ -98,6 +105,10 @@ export interface EncodedReadQuery {
98
105
  warnings: string[];
99
106
  }
100
107
  export type DecodedReadResult = {
108
+ kind: 'svmAccount';
109
+ value: unknown; /** The IDL layout the data decoded as. */
110
+ accountName: string;
111
+ } | {
101
112
  kind: 'uint256';
102
113
  value: bigint;
103
114
  } | {
@@ -314,11 +325,20 @@ export interface ReadFees {
314
325
  }
315
326
  export interface UniversalReadResponse<T = unknown> {
316
327
  requestId: Hex;
328
+ /** `requestId` as the `uint256` contracts key results by (e.g. registry `resultByRequestId`). */
329
+ requestIdUint: bigint;
317
330
  /** Push tx that carried the request */
318
331
  txHash: Hex;
319
332
  destination: ResolvedDestination;
320
333
  /** Required read destination, including `CHAIN.WEB2`. */
321
334
  chain: ReadChain;
335
+ /**
336
+ * The one field that answers "did it work": `SUCCESS` means the source read
337
+ * succeeded, your callback ran, and `value` is set (when a shape is known).
338
+ * Derived by the SDK from `status`, `raw.status`, `callbackDelivered` and
339
+ * `decodeError`, which stay available for debugging.
340
+ */
341
+ outcome: READ_OUTCOME;
322
342
  status: UNIVERSAL_READ_STATUS;
323
343
  isTerminal: boolean;
324
344
  /**
@@ -405,6 +425,27 @@ export declare enum READ_ERROR_CODE {
405
425
  INVALID_RESULT = 5,
406
426
  REJECTED = 6
407
427
  }
428
+ /**
429
+ * SDK-derived single outcome. FULFILLED on the node only means the fulfil tx
430
+ * landed; this separates the ways a FULFILLED read can still not have worked.
431
+ */
432
+ export declare enum READ_OUTCOME {
433
+ /** Not terminal yet. */
434
+ PENDING = "PENDING",
435
+ /** Source SUCCESS, callback delivered, result decoded (or no shape to decode with). */
436
+ SUCCESS = "SUCCESS",
437
+ /** FULFILLED, but the destination returned an error (`raw.errorCode`). */
438
+ SOURCE_ERROR = "SOURCE_ERROR",
439
+ /** FULFILLED, but the receiver callback reverted or ran out of gas (`callbackFailReason`). */
440
+ CALLBACK_FAILED = "CALLBACK_FAILED",
441
+ /** Delivered, but `resultData` did not match the expected shape (`decodeError`). */
442
+ DECODE_FAILED = "DECODE_FAILED",
443
+ EXPIRED = "EXPIRED",
444
+ FAILED = "FAILED",
445
+ ABORTED = "ABORTED",
446
+ /** FULFILLED, but the fulfil receipt was unavailable, so delivery cannot be confirmed. */
447
+ UNKNOWN = "UNKNOWN"
448
+ }
408
449
  /** On-chain lifecycle (`RequestStatus` in ReadTypes.sol). */
409
450
  export declare enum CONTRACT_REQUEST_STATUS {
410
451
  NONE = 0,
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TERMINAL_READ_STATUSES = exports.CONTRACT_REQUEST_STATUS = exports.READ_ERROR_CODE = exports.READ_STATUS = exports.UNIVERSAL_READ_STATUS = void 0;
3
+ exports.TERMINAL_READ_STATUSES = exports.CONTRACT_REQUEST_STATUS = exports.READ_OUTCOME = exports.READ_ERROR_CODE = exports.READ_STATUS = exports.UNIVERSAL_READ_STATUS = void 0;
4
4
  // ---------------------------------------------------------------------------
5
5
  // Status enums (mirror ucallback.v1 protos; re-exported for the public surface)
6
6
  // ---------------------------------------------------------------------------
@@ -32,6 +32,28 @@ var READ_ERROR_CODE;
32
32
  READ_ERROR_CODE[READ_ERROR_CODE["INVALID_RESULT"] = 5] = "INVALID_RESULT";
33
33
  READ_ERROR_CODE[READ_ERROR_CODE["REJECTED"] = 6] = "REJECTED";
34
34
  })(READ_ERROR_CODE || (exports.READ_ERROR_CODE = READ_ERROR_CODE = {}));
35
+ /**
36
+ * SDK-derived single outcome. FULFILLED on the node only means the fulfil tx
37
+ * landed; this separates the ways a FULFILLED read can still not have worked.
38
+ */
39
+ var READ_OUTCOME;
40
+ (function (READ_OUTCOME) {
41
+ /** Not terminal yet. */
42
+ READ_OUTCOME["PENDING"] = "PENDING";
43
+ /** Source SUCCESS, callback delivered, result decoded (or no shape to decode with). */
44
+ READ_OUTCOME["SUCCESS"] = "SUCCESS";
45
+ /** FULFILLED, but the destination returned an error (`raw.errorCode`). */
46
+ READ_OUTCOME["SOURCE_ERROR"] = "SOURCE_ERROR";
47
+ /** FULFILLED, but the receiver callback reverted or ran out of gas (`callbackFailReason`). */
48
+ READ_OUTCOME["CALLBACK_FAILED"] = "CALLBACK_FAILED";
49
+ /** Delivered, but `resultData` did not match the expected shape (`decodeError`). */
50
+ READ_OUTCOME["DECODE_FAILED"] = "DECODE_FAILED";
51
+ READ_OUTCOME["EXPIRED"] = "EXPIRED";
52
+ READ_OUTCOME["FAILED"] = "FAILED";
53
+ READ_OUTCOME["ABORTED"] = "ABORTED";
54
+ /** FULFILLED, but the fulfil receipt was unavailable, so delivery cannot be confirmed. */
55
+ READ_OUTCOME["UNKNOWN"] = "UNKNOWN";
56
+ })(READ_OUTCOME || (exports.READ_OUTCOME = READ_OUTCOME = {}));
35
57
  /** On-chain lifecycle (`RequestStatus` in ReadTypes.sol). */
36
58
  var CONTRACT_REQUEST_STATUS;
37
59
  (function (CONTRACT_REQUEST_STATUS) {
@@ -18,7 +18,7 @@
18
18
  import { type Hex } from 'viem';
19
19
  import { PUSH_NETWORK } from '../constants/enums';
20
20
  import type { PushClient } from '../push-client/push-client';
21
- import { type ReadLifecycleOptions, type ReadResultShape, type UniversalReadResponse } from './read-state.types';
21
+ import { READ_OUTCOME, READ_STATUS, UNIVERSAL_READ_STATUS, type ReadLifecycleOptions, type ReadResultShape, type UniversalReadResponse } from './read-state.types';
22
22
  export type ReadHookEmitter = (hookId: string, ...args: unknown[]) => void;
23
23
  export interface TrackReadDeps {
24
24
  pushClient: Pick<PushClient, 'getUniversalRead' | 'getReadsByTx' | 'getTransactionReceiptWithArchiveFallback' | 'getBlockResultEvents'> & {
@@ -38,6 +38,11 @@ export declare function trackRead(deps: TrackReadDeps, ref: {
38
38
  }, opts?: ReadLifecycleOptions): Promise<UniversalReadResponse>;
39
39
  /** Normalise a bigint / hex requestId to the node's lowercase 32-byte key. */
40
40
  export declare function toRequestIdHex(id: Hex | bigint): Hex;
41
+ /**
42
+ * One answer to "did it work". Order matters: a source error is delivered to the
43
+ * callback as an ERROR result, so it is reported before any callback outcome.
44
+ */
45
+ export declare function deriveReadOutcome(status: UNIVERSAL_READ_STATUS, rawStatus: READ_STATUS | undefined, callbackDelivered: boolean | undefined, decodeError: string | undefined): READ_OUTCOME;
41
46
  /** Best-effort shape from the on-chain envelope. Contract calls carry no ABI → raw. */
42
47
  export declare function inferResultShape(namespace: string, query: Hex): ReadResultShape;
43
48
  export declare function statusName(status: number): string;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.trackRead = trackRead;
4
4
  exports.toRequestIdHex = toRequestIdHex;
5
+ exports.deriveReadOutcome = deriveReadOutcome;
5
6
  exports.inferResultShape = inferResultShape;
6
7
  exports.statusName = statusName;
7
8
  exports.defaultWaitTimeoutMs = defaultWaitTimeoutMs;
@@ -61,16 +62,23 @@ async function fetchRecords(deps, ref) {
61
62
  /** A request is indexed when its block is processed — briefly retry a miss. */
62
63
  async function lookupWithRetry(deps, ref, opts) {
63
64
  const now = deps.now ?? Date.now;
65
+ const emit = deps.emit ?? (() => undefined);
64
66
  const interval = pollInterval(opts);
65
67
  const start = now();
68
+ const hookRef = 'txHash' in ref ? { txHash: ref.txHash } : { requestId: toRequestIdHex(ref.requestId) };
69
+ emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_104_03, hookRef);
66
70
  for (;;) {
67
71
  const records = await fetchRecords(deps, ref);
68
- if (records.length > 0)
72
+ if (records.length > 0) {
73
+ for (const r of records)
74
+ emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_104_04, toRequestIdHex(r.id), statusName(r.status));
69
75
  return records;
76
+ }
70
77
  if (now() - start + interval > read_state_1.READ_TRACK_LOOKUP_TIMEOUT_MS)
71
78
  break;
72
79
  await sleep(interval);
73
80
  }
81
+ emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_104_05, hookRef, now() - start);
74
82
  const label = 'txHash' in ref ? ref.txHash : toRequestIdHex(ref.requestId);
75
83
  throw new errors_1.ReadNotFoundError(label, 'txHash' in ref ? { txHash: ref.txHash } : { requestId: label });
76
84
  }
@@ -144,6 +152,8 @@ async function buildResponse(deps, record, opts) {
144
152
  }
145
153
  const txHash = req.requestedTxHash.toLowerCase();
146
154
  const response = {
155
+ requestIdUint: BigInt(requestId),
156
+ outcome: deriveReadOutcome(status, raw?.status, callbackDelivered, decodeError),
147
157
  requestId,
148
158
  txHash,
149
159
  destination,
@@ -174,23 +184,51 @@ async function buildResponse(deps, record, opts) {
174
184
  };
175
185
  return response;
176
186
  }
187
+ /**
188
+ * One answer to "did it work". Order matters: a source error is delivered to the
189
+ * callback as an ERROR result, so it is reported before any callback outcome.
190
+ */
191
+ function deriveReadOutcome(status, rawStatus, callbackDelivered, decodeError) {
192
+ switch (status) {
193
+ case read_state_types_1.UNIVERSAL_READ_STATUS.EXPIRED: return read_state_types_1.READ_OUTCOME.EXPIRED;
194
+ case read_state_types_1.UNIVERSAL_READ_STATUS.FAILED: return read_state_types_1.READ_OUTCOME.FAILED;
195
+ case read_state_types_1.UNIVERSAL_READ_STATUS.ABORTED: return read_state_types_1.READ_OUTCOME.ABORTED;
196
+ case read_state_types_1.UNIVERSAL_READ_STATUS.FULFILLED:
197
+ if (rawStatus !== undefined && rawStatus !== read_state_types_1.READ_STATUS.SUCCESS)
198
+ return read_state_types_1.READ_OUTCOME.SOURCE_ERROR;
199
+ if (callbackDelivered === false)
200
+ return read_state_types_1.READ_OUTCOME.CALLBACK_FAILED;
201
+ if (callbackDelivered !== true)
202
+ return read_state_types_1.READ_OUTCOME.UNKNOWN;
203
+ if (decodeError !== undefined)
204
+ return read_state_types_1.READ_OUTCOME.DECODE_FAILED;
205
+ return rawStatus === read_state_types_1.READ_STATUS.SUCCESS ? read_state_types_1.READ_OUTCOME.SUCCESS : read_state_types_1.READ_OUTCOME.UNKNOWN;
206
+ default:
207
+ return read_state_types_1.READ_OUTCOME.PENDING;
208
+ }
209
+ }
177
210
  /** Parse every pc_tx receipt (fulfil, settle) into one merged outcome. Missing receipts are skipped. */
178
211
  async function collectOutcome(deps, record, requestId) {
179
212
  const uc = chain_1.UNIVERSAL_CALLBACK_ADDRESSES[deps.pushNetwork];
180
213
  const merged = {};
181
- for (const t of record.pcTx) {
214
+ // Fetch in parallel, merge in pc_tx order (settle facts overwrite fulfil facts as before).
215
+ const outcomes = await Promise.all(record.pcTx.map(async (t) => {
182
216
  if (!t.txHash)
183
- continue;
217
+ return undefined;
184
218
  try {
185
219
  const receipt = await deps.pushClient.getTransactionReceiptWithArchiveFallback(t.txHash);
186
- const o = (0, read_events_1.parseFulfilOutcome)(receipt, uc, requestId);
187
- for (const [k, v] of Object.entries(o)) {
188
- if (v !== undefined)
189
- merged[k] = v;
190
- }
220
+ return (0, read_events_1.parseFulfilOutcome)(receipt, uc, requestId);
191
221
  }
192
222
  catch {
193
- // pruned or not yet available — leave those fields undefined rather than guess
223
+ return undefined; // pruned or not yet available — leave those fields undefined rather than guess
224
+ }
225
+ }));
226
+ for (const o of outcomes) {
227
+ if (!o)
228
+ continue;
229
+ for (const [k, v] of Object.entries(o)) {
230
+ if (v !== undefined)
231
+ merged[k] = v;
194
232
  }
195
233
  }
196
234
  return merged;
@@ -277,7 +315,7 @@ function chainFromCaip2(caip2) {
277
315
  }
278
316
  function explorerTxUrl(network, txHash) {
279
317
  const chain = network === enums_1.PUSH_NETWORK.MAINNET ? enums_1.CHAIN.PUSH_MAINNET : network === enums_1.PUSH_NETWORK.LOCALNET ? enums_1.CHAIN.PUSH_LOCALNET : enums_1.CHAIN.PUSH_TESTNET_DONUT;
280
- const base = chain_1.PUSH_CHAIN_INFO[chain].explorerUrl ?? 'https://explorer.donut.push.org';
318
+ const base = chain_1.PUSH_CHAIN_INFO[chain].explorerUrl ?? 'https://donut.push.network';
281
319
  return `${base}/tx/${txHash}`;
282
320
  }
283
321
  // ---------------------------------------------------------------------------
@@ -312,6 +350,8 @@ async function waitForRead(deps, initial, opts) {
312
350
  const interval = pollInterval(opts);
313
351
  const start = now();
314
352
  let timeoutMs = opts.timeoutMs ?? read_state_1.READ_TRACK_MAX_TIMEOUT_MS;
353
+ // Push head at a known time; later heights are estimated from the block time (no extra RPC per poll).
354
+ let head;
315
355
  // Resolve a fresh head for each wait/resume, only when a default is needed.
316
356
  // Bound this RPC too: a stalled height lookup must not hang the promise forever.
317
357
  if (opts.timeoutMs === undefined && !initial.isTerminal) {
@@ -327,6 +367,7 @@ async function waitForRead(deps, initial, opts) {
327
367
  }, read_state_1.READ_TRACK_MAX_TIMEOUT_MS);
328
368
  }),
329
369
  ]);
370
+ head = { height, at: now() };
330
371
  timeoutMs = Math.min(read_state_1.READ_TRACK_MAX_TIMEOUT_MS, now() - start + defaultWaitTimeoutMs(height, initial.request.spec.expiryPushChainHeight));
331
372
  }
332
373
  finally {
@@ -334,6 +375,23 @@ async function waitForRead(deps, initial, opts) {
334
375
  clearTimeout(timer);
335
376
  }
336
377
  }
378
+ else if (!initial.isTerminal) {
379
+ // Explicit timeout: the head only feeds the expiry warning, so never block on it.
380
+ deps.pushClient.publicClient.getBlockNumber({ cacheTime: 0 })
381
+ .then((height) => { head = { height, at: now() }; })
382
+ .catch(() => undefined);
383
+ }
384
+ let expiryWarned = false;
385
+ const warnNearExpiry = (r) => {
386
+ if (expiryWarned || !head)
387
+ return;
388
+ const estimated = head.height + BigInt(Math.floor((now() - head.at) / read_state_1.PUSH_BLOCK_TIME_MS));
389
+ const left = r.request.spec.expiryPushChainHeight - estimated;
390
+ if (left > read_state_1.READ_EXPIRY_WARNING_BLOCKS)
391
+ return;
392
+ expiryWarned = true;
393
+ emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_105_04, r.requestId, left > 0n ? left : 0n);
394
+ };
337
395
  emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_104_02, initial.txHash, initial.requestId, initial.request.logIndex);
338
396
  // Every snapshot taken from here on is built with the wait() options, so a
339
397
  // resultShape given to wait() decodes even a record that is already terminal.
@@ -388,6 +446,7 @@ async function waitForRead(deps, initial, opts) {
388
446
  else if (current.status === read_state_types_1.UNIVERSAL_READ_STATUS.VOTING)
389
447
  emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_105_02, current.requestId);
390
448
  }
449
+ warnNearExpiry(current);
391
450
  const elapsed = now() - start;
392
451
  if (elapsed >= timeoutMs)
393
452
  timeout();
@@ -403,13 +462,20 @@ function emitTerminal(emit, r) {
403
462
  emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_106_03, r.requestId, r.callbackFailReason);
404
463
  if (r.fees.burned !== undefined)
405
464
  emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_106_04, r.requestId, r.fees.burned, r.fees.refunded ?? 0n);
406
- if (r.fees.refundFailed === true)
407
- emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_106_06, r.requestId, r.fees.refunded, r.request.refundTo);
408
- else if (r.fees.refunded !== undefined)
409
- emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_106_05, r.requestId, r.fees.refunded, r.request.refundTo);
465
+ }
466
+ // Refund facts exist for FULFILLED/FAILED (settle logs) and EXPIRED (EndBlock logs).
467
+ if (r.fees.refundFailed === true)
468
+ emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_106_06, r.requestId, r.fees.refunded, r.request.refundTo);
469
+ else if (r.fees.refunded !== undefined)
470
+ emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_106_05, r.requestId, r.fees.refunded, r.request.refundTo);
471
+ // 199-01 means it worked. A FULFILLED read that did not (source error, callback
472
+ // failure, undecodable, unconfirmed delivery) ends on 199-02 named by its outcome.
473
+ if (r.outcome === read_state_types_1.READ_OUTCOME.SUCCESS) {
410
474
  emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_199_01, r.requestId, r.value, r.raw?.resultData ?? '0x', r.callbackDelivered);
411
475
  return;
412
476
  }
413
- emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_199_02, r.requestId, statusName(r.status), r.raw?.errorCode, r.errorMsg, r.fees.refunded);
477
+ const ended = r.status === read_state_types_1.UNIVERSAL_READ_STATUS.FULFILLED ? r.outcome : statusName(r.status);
478
+ const detail = r.errorMsg || r.decodeError || '';
479
+ emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_199_02, r.requestId, ended, r.raw?.errorCode, detail, r.fees.refunded);
414
480
  }
415
481
  //# sourceMappingURL=read-tracker.js.map
@@ -28,9 +28,8 @@ function resolveReadCallback(callback, network, queryKey) {
28
28
  if (callback && 'request' in callback)
29
29
  throw new errors_1.InvalidReadQueryError('callback.request was removed; use callback.abi, functionName and args');
30
30
  if (callback?.target !== undefined) {
31
- if (callback.gasLimit === undefined)
32
- throw new errors_1.InvalidReadQueryError('callback.gasLimit is required for a custom target');
33
- return { ...callback, gasLimit: callback.gasLimit };
31
+ // Same default as the registry: unused callback budget is refunded.
32
+ return { ...callback, gasLimit: callback.gasLimit ?? read_state_1.REGISTRY_CALLBACK_GAS };
34
33
  }
35
34
  if (callback?.abi !== undefined || callback?.functionName !== undefined || callback?.args !== undefined) {
36
35
  throw new errors_1.InvalidReadQueryError('callback abi, functionName and args require callback.target');