@pushchain/core 6.0.24 → 6.0.25

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 (94) hide show
  1. package/CHANGELOG.md +84 -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 +23 -1
  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 +52 -0
  19. package/src/lib/constants/index.js +37 -8
  20. package/src/lib/constants/read-state.d.ts +53 -0
  21. package/src/lib/constants/read-state.js +59 -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 +37 -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 +271 -0
  47. package/src/lib/progress-hook/progress-hook.types.d.ts +32 -0
  48. package/src/lib/progress-hook/progress-hook.types.js +34 -0
  49. package/src/lib/push-chain/push-chain.d.ts +70 -0
  50. package/src/lib/push-chain/push-chain.js +27 -0
  51. package/src/lib/push-client/push-client.d.ts +35 -2
  52. package/src/lib/push-client/push-client.js +117 -4
  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 +148 -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 +107 -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 +212 -0
  75. package/src/lib/read-state/read-params.d.ts +205 -0
  76. package/src/lib/read-state/read-params.js +120 -0
  77. package/src/lib/read-state/read-state.types.d.ts +417 -0
  78. package/src/lib/read-state/read-state.types.js +50 -0
  79. package/src/lib/read-state/read-tracker.d.ts +50 -0
  80. package/src/lib/read-state/read-tracker.js +415 -0
  81. package/src/lib/read-state/registry.d.ts +21 -0
  82. package/src/lib/read-state/registry.js +54 -0
  83. package/src/lib/read-state/result-decoder.d.ts +16 -0
  84. package/src/lib/read-state/result-decoder.js +73 -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/validate.d.ts +22 -0
  88. package/src/lib/read-state/validate.js +80 -0
  89. package/src/lib/universal-tx-detector/child-inbounds.d.ts +3 -1
  90. package/src/lib/universal-tx-detector/child-inbounds.js +4 -5
  91. package/src/lib/universal-tx-detector/detector-svm.js +29 -20
  92. package/src/lib/universal-tx-detector/svm-events.d.ts +17 -5
  93. package/src/lib/universal-tx-detector/svm-events.js +81 -6
  94. package/src/lib/vm-client/evm-client.js +27 -10
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.encodeSpec = encodeSpec;
4
+ exports.buildReadSpecFromPreflight = buildReadSpecFromPreflight;
5
+ exports.prepareRead = prepareRead;
6
+ exports.toCallData = toCallData;
7
+ exports.simulateRead = simulateRead;
8
+ exports.revalidateRead = revalidateRead;
9
+ const viem_1 = require("viem");
10
+ const universalCallback_evm_1 = require("../constants/abi/universalCallback.evm");
11
+ const uea_factory_1 = require("../constants/abi/uea-factory");
12
+ const chain_1 = require("../constants/chain");
13
+ const read_state_1 = require("../constants/read-state");
14
+ const budget_1 = require("./budget");
15
+ const registry_1 = require("./registry");
16
+ const destination_1 = require("./destination");
17
+ const envelopes_1 = require("./envelopes");
18
+ const errors_1 = require("./errors");
19
+ const preflight_1 = require("./preflight");
20
+ const validate_1 = require("./validate");
21
+ const progress_hook_types_1 = require("../progress-hook/progress-hook.types");
22
+ /** The ReadSpec tuple as the contract declares it — for abi.encode(spec). */
23
+ const READ_SPEC_ABI_PARAM = (0, viem_1.getAbiItem)({ abi: universalCallback_evm_1.UNIVERSAL_CALLBACK_EVM, name: 'requestExternalReadSelf' }).inputs[0];
24
+ function toTuple(spec) {
25
+ return [
26
+ [spec.account.chainNamespace, spec.account.chainId, spec.account.owner],
27
+ spec.query,
28
+ spec.minConfirmations,
29
+ spec.blockNumber,
30
+ spec.expiryPushChainHeight,
31
+ spec.maxFee,
32
+ spec.revertRecipient,
33
+ ];
34
+ }
35
+ function encodeSpec(spec) {
36
+ return (0, viem_1.encodeAbiParameters)([READ_SPEC_ABI_PARAM], [spec]);
37
+ }
38
+ /**
39
+ * Assemble a `PreparedRead` from an already-fetched preflight. Pure — no network.
40
+ * Use directly when preparing several reads from one preflight.
41
+ */
42
+ function buildReadSpecFromPreflight(preflight, params, defaults = {}) {
43
+ const dest = preflight.destination;
44
+ if ((0, destination_1.resolveDestination)(params.destination).caip2 !== dest.caip2) {
45
+ throw new errors_1.InvalidReadQueryError('preflight destination does not match the requested destination');
46
+ }
47
+ const isWeb2 = dest.namespace === read_state_1.READ_NAMESPACE.WEB2;
48
+ const minConfirmations = params.minConfirmations ?? read_state_1.MIN_CONFIRMATIONS_FLOOR;
49
+ // --- pin ---
50
+ let blockNumber;
51
+ if (isWeb2) {
52
+ if (params.blockNumber !== undefined && params.blockNumber !== 0n) {
53
+ throw new errors_1.InvalidReadQueryError('web2 is heightless: blockNumber must be 0 (or omitted)');
54
+ }
55
+ blockNumber = 0n;
56
+ }
57
+ else if (params.blockNumber !== undefined) {
58
+ blockNumber = params.blockNumber;
59
+ }
60
+ else {
61
+ // Back off by minConfirmations: validators hold until latest ≥ blockNumber + minConfirmations,
62
+ // and the oracle height IS roughly latest — pinning at the head would always wait.
63
+ const backed = preflight.observedChainHeight - BigInt(minConfirmations);
64
+ blockNumber = backed >= 1n ? backed : 1n;
65
+ }
66
+ // --- envelope ---
67
+ const encodedQuery = (0, envelopes_1.encodeReadQuery)(params.destination, params.query, {
68
+ blockNumber,
69
+ minSlot: dest.namespace === read_state_1.READ_NAMESPACE.SVM ? blockNumber : undefined,
70
+ });
71
+ // --- money ---
72
+ const callbackBudget = params.callbackBudget ?? (0, budget_1.sizeCallbackBudget)(params.callbackGasLimit, preflight.pushGasPrice, params.budgetBuffer);
73
+ const value = preflight.protocolFee + callbackBudget;
74
+ const bps = BigInt(params.maxFeeBufferBps ?? 0);
75
+ const maxFee = params.maxFee ?? value + (value * bps) / 10000n;
76
+ // --- identities ---
77
+ const refundTo = params.refundTo ?? defaults.refundTo;
78
+ if (!refundTo || !(0, viem_1.isAddress)(refundTo)) {
79
+ throw new errors_1.InvalidReadSpecError(['ZERO_REVERT_RECIPIENT'], {
80
+ destination: dest.caip2,
81
+ hint: 'Pass refundTo (where unspent budget is pushed). In the SDK it defaults to your Push-side account.',
82
+ });
83
+ }
84
+ let owner;
85
+ if (dest.namespace === read_state_1.READ_NAMESPACE.SVM) {
86
+ if (!encodedQuery.ownerBytes)
87
+ throw new errors_1.InvalidReadQueryError('SVM query did not yield the 32-byte owner');
88
+ if (params.owner !== undefined)
89
+ throw new errors_1.InvalidReadQueryError('SVM reads take the account from the query, not `owner`');
90
+ owner = encodedQuery.ownerBytes;
91
+ }
92
+ else {
93
+ owner = params.owner ?? refundTo.toLowerCase(); // validators ignore it for EVM; any non-empty bytes
94
+ }
95
+ const expiryPushChainHeight = params.expiryPushChainHeight ?? preflight.pushBlockNumber + (params.expiryBlocks ?? read_state_1.DEFAULT_EXPIRY_BLOCKS);
96
+ const spec = {
97
+ account: { chainNamespace: dest.chainNamespace, chainId: dest.chainId, owner },
98
+ query: encodedQuery.encoded,
99
+ minConfirmations,
100
+ blockNumber,
101
+ expiryPushChainHeight,
102
+ maxFee,
103
+ revertRecipient: refundTo,
104
+ };
105
+ (0, validate_1.assertValidReadSpec)({ spec, value, callbackGasLimit: params.callbackGasLimit, preflight });
106
+ return {
107
+ callback: params.callback,
108
+ queryKey: (0, registry_1.computeReadQueryKey)(params.destination, params.query),
109
+ chain: dest.caip2,
110
+ spec,
111
+ specTuple: toTuple(spec),
112
+ encodedSpec: encodeSpec(spec),
113
+ value,
114
+ protocolFee: preflight.protocolFee,
115
+ callbackBudget,
116
+ fees: { protocolFee: preflight.protocolFee, callbackBudget, total: value },
117
+ callbackGasLimit: params.callbackGasLimit,
118
+ encodedQuery,
119
+ resultShape: encodedQuery.resultShape,
120
+ preflight,
121
+ warnings: [...encodedQuery.warnings],
122
+ };
123
+ }
124
+ /**
125
+ * Preflight + build. Adds the `refundTo` advisory: a contract recipient that is not a
126
+ * UEA must have a payable receive() or the refund is forfeited to the admin rescue
127
+ * pool (verified live). UEAs (UEA_EVM / UEA_SVM) always have one.
128
+ */
129
+ async function prepareRead(deps, params) {
130
+ const emit = deps.emit ?? (() => undefined);
131
+ const dest = (0, destination_1.resolveDestination)(params.destination);
132
+ emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_101, dest.caip2, dest.namespace, params.query.type);
133
+ emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_102_01, dest.caip2);
134
+ let preflight;
135
+ try {
136
+ preflight = await (0, preflight_1.preflightRead)(deps, params.destination);
137
+ }
138
+ catch (error) {
139
+ if (error instanceof errors_1.ReadHeightUnavailableError)
140
+ emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_102_03, dest.caip2);
141
+ throw error;
142
+ }
143
+ const prepared = buildReadSpecFromPreflight(preflight, params, { refundTo: deps.defaultRefundTo });
144
+ emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_102_02, prepared.protocolFee, prepared.callbackBudget, prepared.value, prepared.spec.blockNumber, prepared.spec.expiryPushChainHeight);
145
+ if (params.query.type === 'http') {
146
+ const matched = Object.keys(params.query.headers ?? {}).filter((name) => /auth|key|token|secret|bearer/i.test(name));
147
+ if (matched.length)
148
+ emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_103_03, matched);
149
+ }
150
+ const refundTo = prepared.spec.revertRecipient;
151
+ const code = await deps.pushClient.publicClient.getCode({ address: refundTo });
152
+ // EIP-7702 delegation designator (0xef0100 + delegate): an EOA that batches via the SDK's
153
+ // executor. It receives value like an EOA — refunds to such accounts have landed live.
154
+ const isDelegatedEoa = !!code && code.toLowerCase().startsWith('0xef0100');
155
+ if (code && code !== '0x' && !isDelegatedEoa) {
156
+ const [, isUEA] = await deps.pushClient.readContract({
157
+ address: chain_1.UEA_FACTORY[deps.pushNetwork],
158
+ abi: uea_factory_1.UEA_FACTORY_ABI,
159
+ functionName: 'getOriginForUEA',
160
+ args: [refundTo],
161
+ });
162
+ if (!isUEA) {
163
+ emit(progress_hook_types_1.PROGRESS_HOOK.READ_TX_102_05, refundTo);
164
+ prepared.warnings.push(`refundTo ${refundTo} is a contract that is not a UEA — it must have a payable receive() or the unspent callback budget is forfeited`);
165
+ }
166
+ }
167
+ return prepared;
168
+ }
169
+ /**
170
+ * Splice the prepared spec into the app's own entrypoint.
171
+ * Default argument order is `(spec, callbackGasLimit)`; pass `args` to remap.
172
+ */
173
+ function toCallData(prepared, target) {
174
+ // Positional tuple: viem accepts it whether the app ABI names the struct components or not.
175
+ const args = target.args ? target.args(prepared.specTuple, prepared.callbackGasLimit) : [prepared.specTuple, prepared.callbackGasLimit];
176
+ return {
177
+ data: (0, viem_1.encodeFunctionData)({ abi: target.abi, functionName: target.functionName, args: args }),
178
+ value: prepared.value,
179
+ };
180
+ }
181
+ /**
182
+ * eth_call `requestExternalReadSelf` with `from` = the app contract, so validation
183
+ * reverts surface before broadcast, decoded into the contract's custom errors.
184
+ * Only meaningful for the app-contract persona: `callbackTarget` becomes `msg.sender`.
185
+ */
186
+ async function simulateRead(deps, prepared, opts) {
187
+ const age = Date.now() - prepared.preflight.fetchedAt;
188
+ if (age > (opts.staleAfterMs ?? 60_000)) {
189
+ prepared.warnings.push(`preflight is ${Math.round(age / 1000)} s old — pushBlockNumber moves every block; rebuild before sending`);
190
+ }
191
+ try {
192
+ await deps.pushClient.publicClient.simulateContract({
193
+ address: prepared.preflight.universalCallback,
194
+ abi: universalCallback_evm_1.UNIVERSAL_CALLBACK_EVM,
195
+ functionName: 'requestExternalReadSelf',
196
+ args: [prepared.spec, opts.callbackSelector, prepared.callbackGasLimit],
197
+ account: opts.appContract,
198
+ value: prepared.value,
199
+ // In the real flow the app contract forwards the msg.value it just received from the
200
+ // user, so for the simulation it must "hold" it. Without this the node answers
201
+ // "insufficient balance for transfer" — and maps it to JSON-RPC -32602, which viem
202
+ // renders as the misleading "Missing or invalid parameters". Verified on Donut.
203
+ stateOverride: [{ address: opts.appContract, balance: prepared.value * 2n + 10n ** 18n }],
204
+ });
205
+ return { ok: true };
206
+ }
207
+ catch (err) {
208
+ if (err instanceof viem_1.BaseError) {
209
+ const reverted = err.walk((e) => e instanceof viem_1.ContractFunctionRevertedError);
210
+ if (reverted instanceof viem_1.ContractFunctionRevertedError && reverted.data) {
211
+ return { ok: false, error: reverted.shortMessage, errorName: reverted.data.errorName, args: reverted.data.args };
212
+ }
213
+ // Surface the node's own reason when viem's summary is generic.
214
+ const details = err.details || err.cause?.details;
215
+ return { ok: false, error: details ? `${err.shortMessage} (${details})` : err.shortMessage };
216
+ }
217
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
218
+ }
219
+ }
220
+ /** Fresh validation only: never rebuild or mutate the prepared request. */
221
+ async function revalidateRead(deps, prepared) {
222
+ const [preflight, block] = await Promise.all([
223
+ (0, preflight_1.preflightRead)(deps, {
224
+ chainNamespace: prepared.spec.account.chainNamespace,
225
+ chainId: prepared.spec.account.chainId,
226
+ }),
227
+ deps.pushClient.publicClient.getBlock({ blockTag: 'latest' }),
228
+ ]);
229
+ // Mirrors node keeper.CallbackCost / CanAffordCallback (x/ucallback/keeper/evm.go).
230
+ // The module prices declared gas at base fee, without tips or the SDK sizing buffer.
231
+ const baseFee = block.baseFeePerGas;
232
+ if (baseFee === null || baseFee === undefined) {
233
+ throw new errors_1.ReadStateError('READ_CALLBACK_BASE_FEE_UNAVAILABLE', 'cannot validate callback affordability without the current Push base fee');
234
+ }
235
+ try {
236
+ (0, validate_1.assertValidReadSpec)({ spec: prepared.spec, value: prepared.value, callbackGasLimit: prepared.callbackGasLimit, preflight });
237
+ if (prepared.value - preflight.protocolFee < prepared.callbackGasLimit * baseFee) {
238
+ throw new errors_1.InvalidReadSpecError(['INSUFFICIENT_CALLBACK_BUDGET']);
239
+ }
240
+ }
241
+ catch (error) {
242
+ if (error instanceof errors_1.InvalidReadSpecError) {
243
+ throw new errors_1.InvalidReadSpecError([...error.violations], {
244
+ destination: preflight.destination.caip2,
245
+ hint: 'Prepared read is no longer valid. Call prepareRead again and review its pin, expiry and payment before executing.',
246
+ });
247
+ }
248
+ throw error;
249
+ }
250
+ }
251
+ //# sourceMappingURL=spec-builder.js.map
@@ -0,0 +1,22 @@
1
+ import { type ReadSpecViolation } from './errors';
2
+ import type { ReadPreflight, ReadSpec } from './read-state.types';
3
+ export interface ValidateReadSpecInput {
4
+ spec: ReadSpec;
5
+ /** msg.value the request will carry. */
6
+ value: bigint;
7
+ callbackGasLimit: bigint;
8
+ /** When present, the height / expiry / fee rules that need chain state are checked too. */
9
+ preflight?: ReadPreflight;
10
+ }
11
+ /**
12
+ * Client-side mirror of every revert in `UniversalCallback.requestExternalReadSelf`
13
+ * (feat-read-state f8d1a0c), plus two SDK invariants, so a developer gets a typed
14
+ * violation list instead of an opaque revert. Line numbers refer to the contract.
15
+ */
16
+ export declare function validateReadSpec(input: ValidateReadSpecInput): {
17
+ ok: true;
18
+ } | {
19
+ ok: false;
20
+ violations: ReadSpecViolation[];
21
+ };
22
+ export declare function assertValidReadSpec(input: ValidateReadSpecInput): void;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateReadSpec = validateReadSpec;
4
+ exports.assertValidReadSpec = assertValidReadSpec;
5
+ const viem_1 = require("viem");
6
+ const read_state_1 = require("../constants/read-state");
7
+ const errors_1 = require("./errors");
8
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
9
+ const UINT16_MAX = 65_535;
10
+ const UINT64_MAX = (1n << 64n) - 1n;
11
+ /**
12
+ * Client-side mirror of every revert in `UniversalCallback.requestExternalReadSelf`
13
+ * (feat-read-state f8d1a0c), plus two SDK invariants, so a developer gets a typed
14
+ * violation list instead of an opaque revert. Line numbers refer to the contract.
15
+ */
16
+ function validateReadSpec(input) {
17
+ const { spec, value, callbackGasLimit, preflight } = input;
18
+ const v = [];
19
+ // :98-103 InvalidAccountId — every account field non-empty
20
+ const ownerLen = isHexLike(spec.account.owner) ? (0, viem_1.size)(spec.account.owner) : 0;
21
+ if (!spec.account.chainNamespace || !spec.account.chainId || ownerLen === 0)
22
+ v.push('INVALID_ACCOUNT_ID');
23
+ // :104-106 EmptyQuery
24
+ if (!isHexLike(spec.query) || (0, viem_1.size)(spec.query) === 0)
25
+ v.push('EMPTY_QUERY');
26
+ // :107-109 InvalidMinConfirmations (uint16)
27
+ if (!Number.isInteger(spec.minConfirmations) || spec.minConfirmations < read_state_1.MIN_CONFIRMATIONS_FLOOR || spec.minConfirmations > UINT16_MAX) {
28
+ v.push('INVALID_MIN_CONFIRMATIONS');
29
+ }
30
+ // :113-134 InvalidBlockNumber — heightless: must be 0; else 1..oracleHeight
31
+ if (spec.blockNumber < 0n || spec.blockNumber > UINT64_MAX) {
32
+ v.push('INVALID_BLOCK_NUMBER');
33
+ }
34
+ else if (preflight) {
35
+ const h = preflight.observedChainHeight;
36
+ const bad = h === 0n ? spec.blockNumber !== 0n : spec.blockNumber === 0n || spec.blockNumber > h;
37
+ if (bad)
38
+ v.push('INVALID_BLOCK_NUMBER');
39
+ }
40
+ // :119-121 InvalidExpiryHeight — strictly in the future
41
+ if (spec.expiryPushChainHeight < 0n || spec.expiryPushChainHeight > UINT64_MAX) {
42
+ v.push('INVALID_EXPIRY_HEIGHT');
43
+ }
44
+ else if (preflight && spec.expiryPushChainHeight <= preflight.pushBlockNumber) {
45
+ v.push('INVALID_EXPIRY_HEIGHT');
46
+ }
47
+ // :122-124 ZeroRevertRecipient
48
+ if (!(0, viem_1.isAddress)(spec.revertRecipient) || spec.revertRecipient.toLowerCase() === ZERO_ADDRESS)
49
+ v.push('ZERO_REVERT_RECIPIENT');
50
+ // :125-130 ZeroCallbackGasLimit / CallbackGasLimitExceeded
51
+ if (callbackGasLimit <= 0n)
52
+ v.push('ZERO_CALLBACK_GAS_LIMIT');
53
+ else if (callbackGasLimit > read_state_1.MAX_CALLBACK_GAS_LIMIT)
54
+ v.push('CALLBACK_GAS_LIMIT_EXCEEDED');
55
+ // :132-139 InsufficientFee / ExcessiveFee
56
+ if (preflight && value < preflight.protocolFee)
57
+ v.push('INSUFFICIENT_FEE');
58
+ if (value > spec.maxFee)
59
+ v.push('EXCESSIVE_FEE');
60
+ // SDK invariant (not a contract revert): a zero budget is never fulfilled by the node.
61
+ if (preflight && value - preflight.protocolFee <= 0n)
62
+ v.push('ZERO_CALLBACK_BUDGET');
63
+ // SDM invariant from svm/read_executor.go:29-32 — validators hard-reject any other length.
64
+ if (spec.account.chainNamespace === read_state_1.READ_NAMESPACE.SVM && ownerLen !== 32)
65
+ v.push('SVM_OWNER_NOT_32_BYTES');
66
+ return v.length === 0 ? { ok: true } : { ok: false, violations: v };
67
+ }
68
+ function assertValidReadSpec(input) {
69
+ const r = validateReadSpec(input);
70
+ if (!r.ok) {
71
+ throw new errors_1.InvalidReadSpecError(r.violations, {
72
+ destination: `${input.spec.account.chainNamespace}:${input.spec.account.chainId}`,
73
+ hint: 'See ReadSpecViolation names — each mirrors a UniversalCallback revert.',
74
+ });
75
+ }
76
+ }
77
+ function isHexLike(x) {
78
+ return typeof x === 'string' && /^0x[0-9a-fA-F]*$/.test(x);
79
+ }
80
+ //# sourceMappingURL=validate.js.map
@@ -7,7 +7,9 @@
7
7
  * See push-chain/x/uexecutor/types/keys.go:49-53 (GetInboundUniversalTxKey).
8
8
  *
9
9
  * For SVM sources, `<txHash>` is the 0x-prefixed hex encoding of the 64-byte
10
- * Solana signature and `<logIndex>` is the event's index in `meta.logMessages`.
10
+ * Solana signature and `<logIndex>` is the gateway-event ordinal. Current
11
+ * gateways derive it from authenticated `emit_cpi` inner instructions;
12
+ * legacy log-emitting gateways used the `meta.logMessages` index.
11
13
  *
12
14
  * For Push-initiated outbounds (pcTx entries) the formula drops the log index:
13
15
  * utxId = sha256(`<pcChainCaip>:<pcTxHash>`)
@@ -14,7 +14,9 @@ exports.resolveChildInboundsFromLogs = resolveChildInboundsFromLogs;
14
14
  * See push-chain/x/uexecutor/types/keys.go:49-53 (GetInboundUniversalTxKey).
15
15
  *
16
16
  * For SVM sources, `<txHash>` is the 0x-prefixed hex encoding of the 64-byte
17
- * Solana signature and `<logIndex>` is the event's index in `meta.logMessages`.
17
+ * Solana signature and `<logIndex>` is the gateway-event ordinal. Current
18
+ * gateways derive it from authenticated `emit_cpi` inner instructions;
19
+ * legacy log-emitting gateways used the `meta.logMessages` index.
18
20
  *
19
21
  * For Push-initiated outbounds (pcTx entries) the formula drops the log index:
20
22
  * utxId = sha256(`<pcChainCaip>:<pcTxHash>`)
@@ -44,10 +46,7 @@ const OUTBOUND_STATUS_NAMES = {
44
46
  2: 'OBSERVED',
45
47
  3: 'REVERTED',
46
48
  };
47
- const INBOUND_EVENT_NAMES = new Set([
48
- 'UniversalTx',
49
- 'RevertUniversalTx',
50
- ]);
49
+ const INBOUND_EVENT_NAMES = new Set(['UniversalTx', 'RevertUniversalTx']);
51
50
  const HEX_64_BYTE_RE = /^(?:0x)?[0-9a-fA-F]{128}$/;
52
51
  function normalizeInboundTxHashForKey(sourceChainCaip, externalTxHash) {
53
52
  if (!sourceChainCaip.startsWith('solana:')) {
@@ -6,18 +6,17 @@ const tslib_1 = require("tslib");
6
6
  /**
7
7
  * detectUniversalTxSvm — SVM counterpart to the EVM detector.
8
8
  *
9
- * Fetches the tx via Solana RPC, walks `meta.logMessages`, decodes each
10
- * `Program data:` entry with Anchor's BorshEventCoder against the gateway
11
- * IDL, normalizes field names to match classify.ts's EVM-derived shape,
12
- * and hands the result to the same classifier used by the EVM branch.
9
+ * Fetches the tx via Solana RPC and decodes gateway events with Anchor's
10
+ * BorshEventCoder. Current gateways use authenticated `emit_cpi!` inner
11
+ * instructions; legacy `Program data:` log events remain as a fallback.
13
12
  *
14
13
  * Behavior deliberately mirrors detector.ts: same UniversalTxDetection
15
14
  * return, same universalTxId derivation strategy (primary-log fallback
16
15
  * via deriveChildUniversalTxId on source-chain inbounds).
17
16
  *
18
17
  * The Push keeper stores SVM inbound ids using the 0x-prefixed hex encoding
19
- * of the 64-byte Solana signature and the index of the Anchor event inside
20
- * `meta.logMessages`: `${caip}:${hexSig}:${logMessageIndex}`.
18
+ * of the 64-byte Solana signature and the gateway-event ordinal:
19
+ * `${caip}:${hexSig}:${eventIndex}`.
21
20
  */
22
21
  const anchor_1 = require("@coral-xyz/anchor");
23
22
  const web3_js_1 = require("@solana/web3.js");
@@ -86,17 +85,22 @@ async function detectUniversalTxSvm(signature, chain, opts = {}) {
86
85
  }
87
86
  const eventCoder = new anchor_1.BorshEventCoder(universalGatewayV0_json_1.default);
88
87
  const matchingLogs = [];
89
- // Walk logMessages for `Program data:` entries. Each matched event keeps
90
- // the actual Solana log-message index because that is the value validators
91
- // submit as the inbound log_index and the keeper uses for the UTX key.
92
- for (let logIndex = 0; logIndex < logMessages.length; logIndex++) {
93
- const rawLog = logMessages[logIndex];
94
- if (typeof rawLog !== 'string')
95
- continue;
96
- const prefix = 'Program data: ';
97
- if (!rawLog.startsWith(prefix))
98
- continue;
99
- const base64Data = rawLog.slice(prefix.length).trim();
88
+ const cpiPayloads = (0, svm_events_1.getSvmEventCpiPayloads)(tx, emitterBase58);
89
+ const eventPayloads = cpiPayloads.length > 0
90
+ ? cpiPayloads.map(({ base64Data, eventIndex }) => ({
91
+ base64Data,
92
+ logIndex: eventIndex,
93
+ }))
94
+ : logMessages.flatMap((rawLog, logIndex) => {
95
+ const prefix = 'Program data: ';
96
+ return typeof rawLog === 'string' && rawLog.startsWith(prefix)
97
+ ? [{ base64Data: rawLog.slice(prefix.length).trim(), logIndex }]
98
+ : [];
99
+ });
100
+ if (cpiPayloads.length > 0) {
101
+ notes.push('svm: decoded gateway events from emit_cpi inner instructions');
102
+ }
103
+ for (const { base64Data, logIndex } of eventPayloads) {
100
104
  // Fast discriminator check — skip non-universal events without running
101
105
  // the full Borsh decode.
102
106
  let bytes;
@@ -141,7 +145,7 @@ async function detectUniversalTxSvm(signature, chain, opts = {}) {
141
145
  notes.push(...classified.notes);
142
146
  const decoded = { ...classified.decoded };
143
147
  // Inbound fallback for universalTxId: same formula the EVM branch uses,
144
- // with the canonical 0x-hex Solana signature and log-message index.
148
+ // with the canonical 0x-hex Solana signature and gateway-event index.
145
149
  if (!decoded.universalTxId) {
146
150
  const caip = chain;
147
151
  if (INBOUND_KINDS.has(classified.kind) &&
@@ -245,7 +249,9 @@ function coerceArgValue(srcKey, renamedKey, value) {
245
249
  return (0, viem_1.bytesToHex)(new Uint8Array(value));
246
250
  }
247
251
  // BN from anchor — has toString().
248
- if (value && typeof value === 'object' && typeof value.toString === 'function') {
252
+ if (value &&
253
+ typeof value === 'object' &&
254
+ typeof value.toString === 'function') {
249
255
  const proto = Object.getPrototypeOf(value);
250
256
  const ctorName = proto?.constructor?.name;
251
257
  if (ctorName === 'BN') {
@@ -259,7 +265,10 @@ function coerceArgValue(srcKey, renamedKey, value) {
259
265
  }
260
266
  // Anchor enum → { [variantName]: {} }. tx_type must be surfaced as a
261
267
  // numeric index to match classify.ts's setTxType expectations.
262
- if (renamedKey === 'txType' && value && typeof value === 'object' && !Array.isArray(value)) {
268
+ if (renamedKey === 'txType' &&
269
+ value &&
270
+ typeof value === 'object' &&
271
+ !Array.isArray(value)) {
263
272
  return extractTxTypeIndex(value);
264
273
  }
265
274
  return value;
@@ -1,12 +1,24 @@
1
1
  /**
2
2
  * SVM-side universal-tx event metadata.
3
3
  *
4
- * Anchor programs emit events as base64-encoded `Program data:` log lines
5
- * prefixed with an 8-byte discriminator. The map below covers the four SVM
6
- * gateway events that have EVM parity (source: universalGatewayV0.json
7
- * events section). The discriminator bytes come from the IDL and are
8
- * converted to lowercase hex for matching.
4
+ * Current Anchor programs emit events through `emit_cpi!`: an inner
5
+ * instruction whose data is EVENT_IX_TAG || event discriminator || Borsh
6
+ * body. Older deployments emitted the discriminator + body as a base64
7
+ * `Program data:` log line. Helpers in this module support both transports.
9
8
  */
9
+ export declare const SVM_EVENT_IX_TAG: Uint8Array;
10
+ export interface SvmGatewayEventPayload {
11
+ /** Base64 discriminator + Borsh body accepted by BorshEventCoder. */
12
+ base64Data: string;
13
+ /** Gateway-event ordinal used by the Push keeper as log_index. */
14
+ eventIndex: number;
15
+ }
16
+ /**
17
+ * Extract authenticated gateway events from Anchor `emit_cpi!` inner
18
+ * instructions. Only self-CPIs whose program id equals `gatewayAddress` are
19
+ * accepted; matching a discriminator alone is not proof of the emitter.
20
+ */
21
+ export declare function getSvmEventCpiPayloads(txResp: any, gatewayAddress: string): SvmGatewayEventPayload[];
10
22
  /**
11
23
  * Mapping: 8-byte discriminator (lowercase hex, no 0x) → Anchor event name.
12
24
  * Only events with EVM classifier parity are listed; the keeper-agnostic
@@ -2,15 +2,90 @@
2
2
  /**
3
3
  * SVM-side universal-tx event metadata.
4
4
  *
5
- * Anchor programs emit events as base64-encoded `Program data:` log lines
6
- * prefixed with an 8-byte discriminator. The map below covers the four SVM
7
- * gateway events that have EVM parity (source: universalGatewayV0.json
8
- * events section). The discriminator bytes come from the IDL and are
9
- * converted to lowercase hex for matching.
5
+ * Current Anchor programs emit events through `emit_cpi!`: an inner
6
+ * instruction whose data is EVENT_IX_TAG || event discriminator || Borsh
7
+ * body. Older deployments emitted the discriminator + body as a base64
8
+ * `Program data:` log line. Helpers in this module support both transports.
10
9
  */
11
10
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.SVM_EVENT_FIELD_ALIASES = exports.SVM_FIELD_RENAMES = exports.SVM_EVENT_DISCRIMINATORS = void 0;
11
+ exports.SVM_EVENT_FIELD_ALIASES = exports.SVM_FIELD_RENAMES = exports.SVM_EVENT_DISCRIMINATORS = exports.SVM_EVENT_IX_TAG = void 0;
12
+ exports.getSvmEventCpiPayloads = getSvmEventCpiPayloads;
13
13
  exports.discriminatorHex = discriminatorHex;
14
+ const web3_js_1 = require("@solana/web3.js");
15
+ const bs58_1 = require("../internal/bs58");
16
+ exports.SVM_EVENT_IX_TAG = Uint8Array.from([
17
+ 0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d,
18
+ ]);
19
+ /**
20
+ * Extract authenticated gateway events from Anchor `emit_cpi!` inner
21
+ * instructions. Only self-CPIs whose program id equals `gatewayAddress` are
22
+ * accepted; matching a discriminator alone is not proof of the emitter.
23
+ */
24
+ function getSvmEventCpiPayloads(txResp, gatewayAddress) {
25
+ const groups = txResp?.meta?.innerInstructions;
26
+ if (!Array.isArray(groups) || groups.length === 0)
27
+ return [];
28
+ let gateway;
29
+ try {
30
+ gateway = new web3_js_1.PublicKey(gatewayAddress);
31
+ }
32
+ catch {
33
+ return [];
34
+ }
35
+ const message = txResp?.transaction?.message;
36
+ const loadedAddresses = txResp?.meta?.loadedAddresses;
37
+ const accountKeyAt = (index) => {
38
+ try {
39
+ if (typeof message?.getAccountKeys === 'function') {
40
+ return message
41
+ .getAccountKeys({
42
+ accountKeysFromLookups: loadedAddresses,
43
+ })
44
+ .get(index);
45
+ }
46
+ const keys = [
47
+ ...(message?.accountKeys ?? message?.staticAccountKeys ?? []),
48
+ ...(loadedAddresses?.writable ?? []),
49
+ ...(loadedAddresses?.readonly ?? loadedAddresses?.readOnly ?? []),
50
+ ];
51
+ const key = keys[index];
52
+ return key instanceof web3_js_1.PublicKey
53
+ ? key
54
+ : key
55
+ ? new web3_js_1.PublicKey(key)
56
+ : undefined;
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ };
62
+ const payloads = [];
63
+ for (const group of groups) {
64
+ for (const instruction of group?.instructions ?? []) {
65
+ const programId = accountKeyAt(Number(instruction?.programIdIndex));
66
+ if (!programId?.equals(gateway) ||
67
+ typeof instruction?.data !== 'string') {
68
+ continue;
69
+ }
70
+ let raw;
71
+ try {
72
+ raw = bs58_1.bs58.decode(instruction.data);
73
+ }
74
+ catch {
75
+ continue;
76
+ }
77
+ if (raw.length < exports.SVM_EVENT_IX_TAG.length + 8 ||
78
+ !exports.SVM_EVENT_IX_TAG.every((byte, i) => raw[i] === byte)) {
79
+ continue;
80
+ }
81
+ payloads.push({
82
+ base64Data: Buffer.from(raw.subarray(exports.SVM_EVENT_IX_TAG.length)).toString('base64'),
83
+ eventIndex: payloads.length,
84
+ });
85
+ }
86
+ }
87
+ return payloads;
88
+ }
14
89
  const toHex = (bytes) => bytes.map((b) => b.toString(16).padStart(2, '0')).join('');
15
90
  /**
16
91
  * Mapping: 8-byte discriminator (lowercase hex, no 0x) → Anchor event name.
@@ -202,18 +202,35 @@ class EvmClient {
202
202
  * console.log(`ETH transfer sent: ${txHash}`);
203
203
  */
204
204
  async sendTransaction({ to, data, value = BigInt(0), signer, nonce: explicitNonce, gas: explicitGas, }) {
205
+ const resolveGas = async () => {
206
+ if (explicitGas)
207
+ return explicitGas;
208
+ const estimate = () => this.publicClient.estimateGas({
209
+ account: signer.account.address,
210
+ to,
211
+ data,
212
+ value,
213
+ });
214
+ if (data !== '0x')
215
+ return estimate();
216
+ // A plain native transfer to a code-free EOA always costs 21k. Empty
217
+ // calldata is not sufficient to identify that case, though: contracts
218
+ // and EIP-7702 delegated accounts still execute code and need estimation.
219
+ // If the code lookup is unavailable, prefer estimation over submitting
220
+ // a transaction that can deterministically run out of gas.
221
+ try {
222
+ const targetCode = await this.publicClient.getCode({ address: to });
223
+ if (!targetCode || targetCode === '0x')
224
+ return BigInt(21000);
225
+ }
226
+ catch {
227
+ // Fall through to estimateGas.
228
+ }
229
+ return estimate();
230
+ };
205
231
  // Estimate gas and fees first (can run in parallel)
206
232
  const [gas, feePerGas, chainId] = await Promise.all([
207
- explicitGas
208
- ? Promise.resolve(explicitGas)
209
- : data === '0x'
210
- ? Promise.resolve(BigInt(21000))
211
- : this.publicClient.estimateGas({
212
- account: signer.account.address,
213
- to,
214
- data,
215
- value,
216
- }),
233
+ resolveGas(),
217
234
  this.publicClient.estimateFeesPerGas(),
218
235
  this.publicClient.getChainId(),
219
236
  ]);