diff --git a/README.md b/README.md index 79b2126..7bfb82e 100644 --- a/README.md +++ b/README.md @@ -500,3 +500,7 @@ We welcome contributions to GenLayerJS SDK! Whether it's new features, improved ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +### v0.6 consumer migration + +See [the v2 migration guide](docs/consumer-migration-v06.md) for the staking rewards rename, producer ABI changes, execution generation, and package selection. diff --git a/docs/api-references/types.Interface.ValidatorInfo.md b/docs/api-references/types.Interface.ValidatorInfo.md index 4182a13..672008b 100644 --- a/docs/api-references/types.Interface.ValidatorInfo.md +++ b/docs/api-references/types.Interface.ValidatorInfo.md @@ -68,9 +68,9 @@ Defined in: [types/staking.ts:64](https://github.com/genlayerlabs/genlayer-js/bl *** -### live +### hasUnclaimedRewards -> **live**: `boolean` +> **hasUnclaimedRewards**: `boolean` Defined in: [types/staking.ts:60](https://github.com/genlayerlabs/genlayer-js/blob/eaba6adec6803bdd0b4968e3f0763cf22107acd1/src/types/staking.ts#L60) diff --git a/docs/api-references/types.Interface.ValidatorView.md b/docs/api-references/types.Interface.ValidatorView.md index ff2d379..fa96f6d 100644 --- a/docs/api-references/types.Interface.ValidatorView.md +++ b/docs/api-references/types.Interface.ValidatorView.md @@ -44,9 +44,9 @@ Defined in: [types/staking.ts:19](https://github.com/genlayerlabs/genlayer-js/bl *** -### live +### hasUnclaimedRewards -> **live**: `boolean` +> **hasUnclaimedRewards**: `boolean` Defined in: [types/staking.ts:30](https://github.com/genlayerlabs/genlayer-js/blob/eaba6adec6803bdd0b4968e3f0763cf22107acd1/src/types/staking.ts#L30) diff --git a/docs/consumer-migration-v06.md b/docs/consumer-migration-v06.md new file mode 100644 index 0000000..a24986f --- /dev/null +++ b/docs/consumer-migration-v06.md @@ -0,0 +1,66 @@ +# v0.6 consumer migration + +This v2 SDK update targets the JM/Claudio/Kiril consensus train, audited at +`genlayerlabs/genlayer-consensus@c4749a42095bcfb9de69bef4f4fd5e9a6a2f86f2` +(PR #1634, above #1628). The fixture in +`tests/fixtures/consensus-consumer-abi.json` contains unmodified ABI entries +from that revision's `abis/IGenLayerStaking.json` `abis/ConsensusData.json`, and `abis/Messages.json`. +This is source compatibility evidence, not a deployment or full E2E result. + +## Staking + +Replace `ValidatorView.live` and `ValidatorInfo.live` with +`hasUnclaimedRewards`. There is no `live` alias. The flag reports rewards still +to be consumed at priming; it does not establish eligibility, readiness or ban +state. Continue to use `banned`, `needsPriming`, and selectable-validator APIs +for their independently defined purposes. Both boolean states are tested. + +Claim `quantity`/`offset` and Commit `outstanding` now decode as `uint256`. +Their JavaScript type remains `bigint`. The producer-fixture regression uses +values above the old `uint120` boundary. + +The shipped staking ABI subset also follows the producer's `delegatorClaim` +return, payable two-argument `inflationInit`, transaction-ID inputs on +idleness bans/quarantine, and `ValidatorBannedIdleness` event. Obsolete staking +selectors/events absent from this producer are removed. No administrative +wrapper or new governance journey is introduced. + +## Lifecycle + +`advanced.getTransactionLifecycle()` exposes optional `executionGeneration` +as an exact decimal string, independent of the retained decision's generation. +Older Studio RPCs without this field leave it absent; no generation is guessed. +A generation change invalidates cached attempt-specific receipts. Do not use +`decisionId` as a substitute for the execution generation. + +An accepted, undetermined or timeout decision can still be appealed or +finalized. `waitUntil: "decided"` is appropriate for an early result; use +`waitUntil: "finalized"` to wait for stored parent finalization. Neither parent +finalization nor `getTriggeredTransactionIds()` proves all internal delivery, +refunds or payouts complete. Child lookup reads parent-filtered +`InternalMessageEffectCommitted` events from the registered Messages contract, +including deferred delivery after the decision and earlier generations. It +returns historical canonical child IDs, not an active-generation inventory. +Queries scan all history in 10,000-block pages; supply `fromBlock` at deployment +to reduce RPC work. A later lower bound intentionally excludes older children. +RPC failures propagate instead of returning a silently incomplete list. This +requires the audited producer event and does not infer IDs for older deployments. + +## Package delivery and remaining qualification + +Use an exact commit of PR #220 for pre-release consumers and regenerate their +lockfiles; v0/v1 semver ranges do not select this v2 train. Studio frontend and +explorer must select the same audited SDK build. Publishing a tagged npm +release remains a separate release action after qualification. + +Select dev-env #145 (it includes #147 and merged #148), retaining Python #116 +and E2E #781 in the consumer closure. Later qualification must compose the +implemented Node stack #1953–#1956 and its Consensus #1634 dependency, verify +one source per repository, and record the actual resolved manifest. Do not +add a reverse SDK-to-Node dependency that creates a cycle. + +Outstanding integration work: full deployment/reset provenance, integrated +child-delivery journey validation, authoritative delivery-completion +API, generation-aware UI receipt invalidation, and deploy/write/read/appeal/ +recompute/governance journey coverage. Native ABI tests do not qualify those +journeys or the producer's native-GEN reserve allocation. diff --git a/src/abi/consensusTrain.ts b/src/abi/consensusTrain.ts index 38b6a95..b1fec81 100644 --- a/src/abi/consensusTrain.ts +++ b/src/abi/consensusTrain.ts @@ -7,7 +7,7 @@ const parseTrainAbi = parseAbi as (signatures: readonly string[]) => Abi; /** Minimal train ABIs used by the high-level transaction reader. */ export const CONSENSUS_DATA_TRAIN_ABI = parseTrainAbi([ "function addressManager() view returns (address)", - "function getTransactionLifecycle(bytes32 _txId, uint256 _timestamp) view returns ((uint8 storedStatus, (bytes32 txId, uint8 storedStatus, uint8 projectedStatus, uint8 action, uint8 result, bytes32 resultHash, uint8 source, uint256 sourceRound, uint256 sourceGeneration, bytes32 sourceRoundContextHash, bytes32 roundPlanHash, uint256 resultRound, uint256 resultGeneration, uint256 basisDecisionId, uint8 context, bytes32 attemptId, uint256 boundaryAt, uint256 evaluatedAt, uint256 snapshotBlock, uint256 decisionWindow, uint256 appealDeadline, bool materializesDecision, bool actionOutcomeDeterministic, bool nonCurrentEvaluation) resolution, (bool exists, uint256 decisionId, uint256 basisDecisionId, uint8 context, uint8 source, bytes32 sourceAttemptId, uint8 sourceStatus, uint8 status, uint256 sourceRound, uint256 sourceGeneration, bytes32 sourceRoundContextHash, bytes32 roundPlanHash, uint256 resultRound, uint256 resultGeneration, uint8 result, bytes32 resultHash, uint256 effectiveAt, uint256 materializedAt, uint256 appealDeadline) latestDecision, bool decisionActive) lifecycle)", + "function getTransactionLifecycle(bytes32 _txId, uint256 _timestamp) view returns ((uint8 storedStatus, (bytes32 txId, uint8 storedStatus, uint8 projectedStatus, uint8 action, uint8 result, bytes32 resultHash, uint8 source, uint256 sourceRound, uint256 sourceGeneration, bytes32 sourceRoundContextHash, bytes32 roundPlanHash, uint256 resultRound, uint256 resultGeneration, uint256 basisDecisionId, uint8 context, bytes32 attemptId, uint256 boundaryAt, uint256 evaluatedAt, uint256 snapshotBlock, uint256 decisionWindow, uint256 appealDeadline, bool materializesDecision, bool actionOutcomeDeterministic, bool nonCurrentEvaluation) resolution, (bool exists, uint256 decisionId, uint256 basisDecisionId, uint8 context, uint8 source, bytes32 sourceAttemptId, uint8 sourceStatus, uint8 status, uint256 sourceRound, uint256 sourceGeneration, bytes32 sourceRoundContextHash, bytes32 roundPlanHash, uint256 resultRound, uint256 resultGeneration, uint8 result, bytes32 resultHash, uint256 effectiveAt, uint256 materializedAt, uint256 appealDeadline) latestDecision, bool decisionActive, uint256 executionGeneration) lifecycle)", "function estimateLatestAppealCharge(bytes32 _txId) view returns (uint256 decisionId, uint256 bond, uint256 funding, uint256 appealDeadline)", ]); diff --git a/src/abi/staking.ts b/src/abi/staking.ts index 56bc51f..92e7b09 100644 --- a/src/abi/staking.ts +++ b/src/abi/staking.ts @@ -133,16 +133,17 @@ export const VALIDATOR_WALLET_ABI = [ }, ] as const; +// Source: consensus c4749a42095bcfb9de69bef4f4fd5e9a6a2f86f2, abis/IGenLayerStaking.json. const STAKING_CLAIM_COMPONENTS = [ - {name: "quantity", type: "uint120"}, - {name: "offset", type: "uint120"}, + {name: "quantity", type: "uint256"}, + {name: "offset", type: "uint256"}, {name: "commit", type: "uint256"}, ] as const; const STAKING_COMMIT_COMPONENTS = [ {name: "input", type: "uint256"}, {name: "output", type: "uint256"}, - {name: "outstanding", type: "uint120"}, + {name: "outstanding", type: "uint256"}, {name: "epoch", type: "uint64"}, {name: "linkToNextCommit", type: "uint56"}, {name: "priced", type: "bool"}, @@ -205,19 +206,10 @@ export const STAKING_ABI = [ type: "event", inputs: [], }, - { - name: "BurnFailed", - type: "event", - inputs: [ - {name: "amount", type: "uint256", indexed: false}, - ], - }, { name: "BurnToL1", type: "event", - inputs: [ - {name: "amount", type: "uint256", indexed: false}, - ], + inputs: [{name: "amount", type: "uint256", indexed: false}], }, { name: "DelegatorClaim", @@ -248,30 +240,12 @@ export const STAKING_ABI = [ { name: "EpochAdvance", type: "event", - inputs: [ - {name: "epoch", type: "uint256", indexed: false}, - ], + inputs: [{name: "epoch", type: "uint256", indexed: false}], }, { name: "EpochFinalize", type: "event", - inputs: [ - {name: "epoch", type: "uint256", indexed: false}, - ], - }, - { - name: "EpochHasPendingTribunals", - type: "event", - inputs: [ - {name: "epoch", type: "uint256", indexed: false}, - ], - }, - { - name: "EpochZeroEnded", - type: "event", - inputs: [ - {name: "timestamp", type: "uint256", indexed: false}, - ], + inputs: [{name: "epoch", type: "uint256", indexed: false}], }, { name: "FeesReceived", @@ -284,9 +258,7 @@ export const STAKING_ABI = [ { name: "InflationInitiated", type: "event", - inputs: [ - {name: "timestamp", type: "uint256", indexed: false}, - ], + inputs: [{name: "timestamp", type: "uint256", indexed: false}], }, { name: "InflationReceived", @@ -308,72 +280,42 @@ export const STAKING_ABI = [ { name: "SetDeepthought", type: "event", - inputs: [ - {name: "deepthought", type: "address", indexed: false}, - ], + inputs: [{name: "deepthought", type: "address", indexed: false}], }, { name: "SetDelegatorMinimumStake", type: "event", - inputs: [ - {name: "delegatorMinStake", type: "uint256", indexed: false}, - ], + inputs: [{name: "delegatorMinStake", type: "uint256", indexed: false}], }, { name: "SetEpochMinDuration", type: "event", - inputs: [ - {name: "epochMinDuration", type: "uint256", indexed: false}, - ], + inputs: [{name: "epochMinDuration", type: "uint256", indexed: false}], }, { name: "SetEpochMinDurationThreshold", type: "event", - inputs: [ - {name: "epochMinDurationThreshold", type: "uint256", indexed: false}, - ], + inputs: [{name: "epochMinDurationThreshold", type: "uint256", indexed: false}], }, { name: "SetEpochZeroMinDuration", type: "event", - inputs: [ - {name: "epochZeroMinDuration", type: "uint256", indexed: false}, - ], - }, - { - name: "SetGen", - type: "event", - inputs: [ - {name: "gen", type: "address", indexed: false}, - ], + inputs: [{name: "epochZeroMinDuration", type: "uint256", indexed: false}], }, { name: "SetMaxValidators", type: "event", - inputs: [ - {name: "maxValidators", type: "uint256", indexed: false}, - ], + inputs: [{name: "maxValidators", type: "uint256", indexed: false}], }, { name: "SetReductionFactor", type: "event", - inputs: [ - {name: "reductionFactor", type: "uint256", indexed: false}, - ], - }, - { - name: "SetStakingInvariant", - type: "event", - inputs: [ - {name: "stakingInvariant", type: "address", indexed: false}, - ], + inputs: [{name: "reductionFactor", type: "uint256", indexed: false}], }, { name: "SetTransactionFeesManager", type: "event", - inputs: [ - {name: "transactionFeesManager", type: "address", indexed: false}, - ], + inputs: [{name: "transactionFeesManager", type: "address", indexed: false}], }, { name: "SetUnbondingPeriods", @@ -386,9 +328,7 @@ export const STAKING_ABI = [ { name: "SetValidatorMinimumStake", type: "event", - inputs: [ - {name: "validatorMinStake", type: "uint256", indexed: false}, - ], + inputs: [{name: "validatorMinStake", type: "uint256", indexed: false}], }, { name: "SetValidatorWeightParams", @@ -401,25 +341,18 @@ export const STAKING_ABI = [ { name: "ValidatorBanRemoved", type: "event", - inputs: [ - {name: "validator", type: "address", indexed: false}, - ], + inputs: [{name: "validator", type: "address", indexed: false}], }, { - name: "ValidatorBannedDeterministic", - type: "event", + anonymous: false, inputs: [ - {name: "validator", type: "address", indexed: false}, + {indexed: false, name: "validator", type: "address"}, + {indexed: false, name: "txId", type: "bytes32"}, + {indexed: false, name: "bannedAt", type: "uint256"}, + {indexed: false, name: "bannedUntil", type: "uint256"}, ], - }, - { name: "ValidatorBannedIdleness", type: "event", - inputs: [ - {name: "validator", type: "address", indexed: false}, - {name: "bannedAt", type: "uint256", indexed: false}, - {name: "bannedUntil", type: "uint256", indexed: false}, - ], }, { name: "ValidatorClaim", @@ -469,16 +402,7 @@ export const STAKING_ABI = [ { name: "ValidatorQuarantineRemoved", type: "event", - inputs: [ - {name: "validator", type: "address", indexed: false}, - ], - }, - { - name: "ValidatorQuarantineRepealed", - type: "event", - inputs: [ - {name: "validator", type: "address", indexed: false}, - ], + inputs: [{name: "validator", type: "address", indexed: false}], }, { name: "ValidatorQuarantined", @@ -498,13 +422,6 @@ export const STAKING_ABI = [ {name: "epoch", type: "uint256", indexed: false}, ], }, - { - name: "ValidatorsRegistered", - type: "event", - inputs: [ - {name: "count", type: "uint256", indexed: false}, - ], - }, // Functions { @@ -521,20 +438,6 @@ export const STAKING_ABI = [ inputs: [], outputs: [{name: "", type: "uint256"}], }, - { - name: "activeWeights", - type: "function", - stateMutability: "view", - inputs: [], - outputs: [{name: "", type: "uint256[]"}], - }, - { - name: "adminRegisterValidators", - type: "function", - stateMutability: "nonpayable", - inputs: [{name: "validatorAddresses", type: "address[]"}], - outputs: [], - }, { name: "burning", type: "function", @@ -557,14 +460,14 @@ export const STAKING_ABI = [ outputs: [{name: "", type: "uint256"}], }, { - name: "delegatorClaim", - type: "function", - stateMutability: "nonpayable", inputs: [ {name: "_delegator", type: "address"}, {name: "_validator", type: "address"}, ], - outputs: [], + name: "delegatorClaim", + outputs: [{name: "", type: "uint256"}], + stateMutability: "nonpayable", + type: "function", }, { name: "delegatorDeposit", @@ -944,26 +847,28 @@ export const STAKING_ABI = [ outputs: [{name: "", type: "address[]"}], }, { - name: "idlenessBan", - type: "function", - stateMutability: "nonpayable", inputs: [ {name: "_validator", type: "address"}, {name: "_at", type: "uint256"}, {name: "_until", type: "uint256"}, + {name: "_txId", type: "bytes32"}, ], + name: "idlenessBan", outputs: [], + stateMutability: "nonpayable", + type: "function", }, { - name: "idlenessBanBatch", - type: "function", - stateMutability: "nonpayable", inputs: [ {name: "_validators", type: "address[]"}, {name: "_quarantinedAt", type: "uint256"}, {name: "_quarantinedUntil", type: "uint256"}, + {name: "_txId", type: "bytes32"}, ], + name: "idlenessBanBatch", outputs: [], + stateMutability: "nonpayable", + type: "function", }, { name: "inflationEpoch", @@ -973,11 +878,14 @@ export const STAKING_ABI = [ outputs: [{name: "", type: "uint256"}], }, { + inputs: [ + {name: "_epochTo", type: "uint256"}, + {name: "_inauguralTimestamp", type: "uint256"}, + ], name: "inflationInit", - type: "function", - stateMutability: "nonpayable", - inputs: [{name: "_inflationOnset", type: "uint256"}], outputs: [], + stateMutability: "payable", + type: "function", }, { name: "inflationReceive", @@ -1052,13 +960,6 @@ export const STAKING_ABI = [ inputs: [{name: "_finalizationPhase", type: "address"}], outputs: [], }, - { - name: "setGen", - type: "function", - stateMutability: "nonpayable", - inputs: [{name: "_gen", type: "address"}], - outputs: [], - }, { name: "setIdlenessPhase", type: "function", @@ -1087,13 +988,6 @@ export const STAKING_ABI = [ inputs: [{name: "_revealingPhase", type: "address"}], outputs: [], }, - { - name: "setStakingInvariant", - type: "function", - stateMutability: "nonpayable", - inputs: [{name: "_stakingInvariant", type: "address"}], - outputs: [], - }, { name: "setTransactionFeesManager", type: "function", @@ -1111,13 +1005,6 @@ export const STAKING_ABI = [ ], outputs: [], }, - { - name: "setValidatorMinimumStake", - type: "function", - stateMutability: "nonpayable", - inputs: [{name: "_validatorMinStake", type: "uint256"}], - outputs: [], - }, { name: "setValidatorWeightParams", type: "function", @@ -1148,13 +1035,6 @@ export const STAKING_ABI = [ ], outputs: [{name: "", type: "uint256"}], }, - { - name: "validatorBanDeterministic", - type: "function", - stateMutability: "nonpayable", - inputs: [{name: "_validator", type: "address"}], - outputs: [], - }, { name: "validatorClaim", type: "function", @@ -1241,14 +1121,15 @@ export const STAKING_ABI = [ outputs: [], }, { - name: "validatorQuarantine", - type: "function", - stateMutability: "nonpayable", inputs: [ {name: "_validator", type: "address"}, {name: "_at", type: "uint256"}, + {name: "_txId", type: "bytes32"}, ], + name: "validatorQuarantine", outputs: [], + stateMutability: "nonpayable", + type: "function", }, { name: "validatorQuarantineCount", @@ -1257,13 +1138,6 @@ export const STAKING_ABI = [ inputs: [], outputs: [{name: "", type: "uint256"}], }, - { - name: "validatorQuarantineRepeal", - type: "function", - stateMutability: "nonpayable", - inputs: [{name: "_validator", type: "address"}], - outputs: [], - }, { name: "validatorSelection", type: "function", @@ -1311,7 +1185,7 @@ export const STAKING_ABI = [ name: "", type: "tuple", components: [ - {name: "eBanned", type: "uint256"}, + {name: "eBanned", type: "uint256"}, {name: "ePrimed", type: "uint256"}, {name: "vStake", type: "uint256"}, {name: "vShares", type: "uint256"}, @@ -1319,7 +1193,7 @@ export const STAKING_ABI = [ {name: "dShares", type: "uint256"}, {name: "vDeposit", type: "uint256"}, {name: "vWithdrawal", type: "uint256"}, - {name: "live", type: "bool"}, + {name: "hasUnclaimedRewards", type: "bool"}, ], }, ], @@ -1334,7 +1208,7 @@ export const STAKING_ABI = [ name: "", type: "tuple", components: [ - {name: "eBanned", type: "uint256"}, + {name: "eBanned", type: "uint256"}, {name: "ePrimed", type: "uint256"}, {name: "vStake", type: "uint256"}, {name: "vShares", type: "uint256"}, @@ -1342,7 +1216,7 @@ export const STAKING_ABI = [ {name: "dShares", type: "uint256"}, {name: "vDeposit", type: "uint256"}, {name: "vWithdrawal", type: "uint256"}, - {name: "live", type: "bool"}, + {name: "hasUnclaimedRewards", type: "bool"}, ], }, ], @@ -1357,7 +1231,7 @@ export const STAKING_ABI = [ name: "", type: "tuple", components: [ - {name: "eBanned", type: "uint256"}, + {name: "eBanned", type: "uint256"}, {name: "ePrimed", type: "uint256"}, {name: "vStake", type: "uint256"}, {name: "vShares", type: "uint256"}, @@ -1365,7 +1239,7 @@ export const STAKING_ABI = [ {name: "dShares", type: "uint256"}, {name: "vDeposit", type: "uint256"}, {name: "vWithdrawal", type: "uint256"}, - {name: "live", type: "bool"}, + {name: "hasUnclaimedRewards", type: "bool"}, ], }, ], @@ -1424,13 +1298,6 @@ export const STAKING_ABI = [ inputs: [], outputs: [{name: "", type: "uint256"}], }, - { - name: "validatorsCount", - type: "function", - stateMutability: "view", - inputs: [], - outputs: [{name: "", type: "uint256"}], - }, { name: "validatorsJoinedCount", type: "function", diff --git a/src/staking/actions.ts b/src/staking/actions.ts index 337cb82..f525e17 100644 --- a/src/staking/actions.ts +++ b/src/staking/actions.ts @@ -658,7 +658,7 @@ export const stakingActions = ( vWithdrawal: formatStakingAmount(view.vWithdrawal), vWithdrawalRaw: view.vWithdrawal, ePrimed: view.ePrimed, - live: view.live, + hasUnclaimedRewards: view.hasUnclaimedRewards, banned, bannedEpoch: banned ? view.eBanned : undefined, needsPriming, diff --git a/src/transactions/actions.ts b/src/transactions/actions.ts index f978891..53291f5 100644 --- a/src/transactions/actions.ts +++ b/src/transactions/actions.ts @@ -20,7 +20,7 @@ import { import {transactionsConfig} from "../config/transactions"; import {sleep} from "../utils/async"; import {GenLayerChain} from "@/types"; -import {Abi, PublicClient, Address, keccak256, concat, stringToBytes, toBytes, zeroAddress} from "viem"; +import {Abi, PublicClient, Address, keccak256, concat, stringToBytes, toBytes, zeroAddress, parseAbiItem} from "viem"; import {decodeLocalnetTransaction, decodeTransaction, simplifyTransactionReceipt} from "./decoders"; import {isMethodNotFoundError, readStudioLifecycleFallback} from "./lifecycleFallback"; import { @@ -41,6 +41,7 @@ type RawProtocolLifecycle = { decisionId: unknown; decisionActive: unknown; evaluatedAt: unknown; + executionGeneration?: unknown; }; const protocolInteger = (value: unknown, label: string): number => { @@ -76,6 +77,14 @@ const protocolDecisionId = (value: unknown, active: boolean): string | null => { return BigInt(decimal).toString(); }; +const protocolGeneration = (value: unknown): string => { + if ((typeof value !== "bigint" && typeof value !== "string" && typeof value !== "number") || + (typeof value === "number" && !Number.isSafeInteger(value)) || !/^\d+$/.test(String(value))) { + throw new Error(`Invalid protocol lifecycle executionGeneration: ${String(value)}`); + } + return BigInt(value).toString(); +}; + const normalizeProtocolLifecycle = (raw: RawProtocolLifecycle): TransactionProtocolLifecycle => { const storedStatusCode = protocolInteger(raw.storedStatusCode, "storedStatusCode"); const projectedStatusCode = protocolInteger(raw.projectedStatusCode, "projectedStatusCode"); @@ -109,6 +118,9 @@ const normalizeProtocolLifecycle = (raw: RawProtocolLifecycle): TransactionProto decisionId: protocolDecisionId(raw.decisionId, raw.decisionActive), decisionActive: raw.decisionActive, evaluatedAt: protocolInteger(raw.evaluatedAt, "evaluatedAt"), + ...(raw.executionGeneration === undefined ? {} : { + executionGeneration: protocolGeneration(raw.executionGeneration), + }), }; }; @@ -369,6 +381,7 @@ export const transactionActions = (client: GenLayerClient, public decisionId: lifecycle.latestDecision.decisionId, decisionActive: lifecycle.decisionActive, evaluatedAt: lifecycle.resolution.evaluatedAt, + executionGeneration: lifecycle.executionGeneration, }); }, }, @@ -513,58 +526,56 @@ export const transactionActions = (client: GenLayerClient, public } as unknown as GenLayerRawTransaction; return decodeTransaction(transaction); }, - /** Returns transaction IDs of child transactions created from emitted messages. */ - getTriggeredTransactionIds: async ({hash}: {hash: TransactionHash}): Promise => { + /** + * Returns canonical child IDs committed for this parent, including deferred delivery. + * This is historical creation evidence, not child completion or current-generation status. + * Pass the deployment block as fromBlock to avoid scanning earlier chain history. + */ + getTriggeredTransactionIds: async ({hash, fromBlock = 0n}: { + hash: TransactionHash; + fromBlock?: bigint; + }): Promise => { if (client.chain.isStudio) { const tx = await client.getTransaction({hash}); return ((tx as any).triggered_transactions ?? []) as TransactionHash[]; } - - const tx = await transactionActions(client, publicClient).getTransaction({hash}); - const proposalBlock = BigInt(tx.readStateBlockRange?.proposalBlock ?? "0"); - if (proposalBlock === BigInt(0)) return []; - - const scanRange = BigInt(10_000); - const latestBlock = await publicClient.getBlockNumber(); - const toBlock = proposalBlock + scanRange < latestBlock ? proposalBlock + scanRange : latestBlock; - - const consensusAddress = client.chain.consensusMainContract?.address as Address; - const internalMessageProcessedTopic = keccak256(stringToBytes("InternalMessageProcessed(bytes32,address,address)")); - const transactionAcceptedTopic = keccak256(stringToBytes("TransactionAccepted(bytes32)")); - const transactionFinalizedTopic = keccak256(stringToBytes("TransactionFinalized(bytes32)")); - - // InternalMessageProcessed indexes the child transaction ID, not its - // parent. Find the EVM transactions that decided the parent first, then - // inspect their receipts for the child-message events emitted alongside - // that decision. The event is emitted by MessagePayments, not - // ConsensusMain, so the decision receipt is the authoritative boundary - // and the event emitter address must not be restricted here. - const decisionLogs = await publicClient.getLogs({ - address: consensusAddress, - event: undefined, - fromBlock: proposalBlock, - toBlock, - topics: [[transactionAcceptedTopic, transactionFinalizedTopic], hash], - } as any); - - const decisionTransactionHashes = [ - ...new Set(decisionLogs.map(log => log.transactionHash).filter(Boolean)), - ]; - const receipts = await Promise.all( - decisionTransactionHashes.map(transactionHash => - publicClient.getTransactionReceipt({hash: transactionHash!}), - ), - ); - return [ - ...new Set( - receipts.flatMap(receipt => - receipt.logs - .filter(log => log.topics[0] === internalMessageProcessedTopic) - .map(log => log.topics[1] as TransactionHash) - .filter(Boolean), - ), - ), - ]; + if (fromBlock < 0n) throw new Error("fromBlock must be nonnegative"); + const consensusDataAddress = client.chain.consensusDataContract?.address as Address; + if (!consensusDataAddress || consensusDataAddress === zeroAddress) { + throw new Error("ConsensusData contract is not configured for this chain"); + } + const blockNumber = await publicClient.getBlockNumber(); + const addressManager = resolvedAddress("AddressManager", await publicClient.readContract({ + address: consensusDataAddress, + abi: CONSENSUS_DATA_TRAIN_ABI, + functionName: "addressManager", + blockNumber, + }) as Address); + const messages = resolvedAddress("Messages", await publicClient.readContract({ + address: addressManager, + abi: ADDRESS_MANAGER_TRAIN_ABI, + functionName: "getAddress", + args: ["Messages"], + blockNumber, + }) as Address); + const children = new Set(); + // Bound each RPC range, not the total history: terminal delivery may occur + // long after the parent's decision, and re-reveals can move proposalBlock. + for (let start = fromBlock; start <= blockNumber; start += 10_000n) { + const end = start + 9_999n; + const logs = await publicClient.getLogs({ + address: messages, + event: parseAbiItem("event InternalMessageEffectCommitted(bytes32 indexed txId, bytes32 indexed logicalOccurrence, bytes32 indexed generationEffectId, bytes32 childTxId)"), + args: {txId: hash}, + fromBlock: start, + toBlock: end < blockNumber ? end : blockNumber, + strict: true, + }); + for (const log of logs) { + if (log.args.childTxId) children.add(log.args.childTxId as TransactionHash); + } + } + return [...children]; }, /** Fetches the full execution trace including return data, stdout, stderr, and GenVM logs. */ debugTraceTransaction: async ({hash, round = 0}: {hash: TransactionHash; round?: number}): Promise => { diff --git a/src/types/staking.ts b/src/types/staking.ts index 0a32106..e358843 100644 --- a/src/types/staking.ts +++ b/src/types/staking.ts @@ -25,7 +25,7 @@ export interface ValidatorView { dShares: bigint; vDeposit: bigint; vWithdrawal: bigint; - live: boolean; + hasUnclaimedRewards: boolean; } export interface ValidatorIdentity { @@ -55,7 +55,7 @@ export interface ValidatorInfo { vWithdrawal: string; vWithdrawalRaw: bigint; ePrimed: bigint; - live: boolean; + hasUnclaimedRewards: boolean; banned: boolean; bannedEpoch?: bigint; needsPriming: boolean; diff --git a/src/types/transactions.ts b/src/types/transactions.ts index e33e32c..de4f7e1 100644 --- a/src/types/transactions.ts +++ b/src/types/transactions.ts @@ -293,6 +293,8 @@ export type TransactionProtocolLifecycle = { resolutionSourceCode: number; decisionId: string | null; decisionActive: boolean; + /** Current producer generation as an exact decimal string; absent on older Studio APIs. */ + executionGeneration?: string; evaluatedAt: number; }; diff --git a/tests/consumer-producer-abi.test.ts b/tests/consumer-producer-abi.test.ts new file mode 100644 index 0000000..86a3b16 --- /dev/null +++ b/tests/consumer-producer-abi.test.ts @@ -0,0 +1,47 @@ +import {describe, expect, it} from "vitest"; +import {decodeFunctionResult, encodeAbiParameters, type AbiParameter} from "viem"; +import producer from "./fixtures/consensus-consumer-abi.json"; +import {STAKING_ABI} from "../src/abi/staking"; +import {CONSENSUS_DATA_TRAIN_ABI} from "../src/abi/consensusTrain"; + +const layout = (parameters: readonly any[]): unknown => parameters.map(p => ({ + type: p.type, + ...(p.components ? {components: p.components.map((c: any) => ({name: c.name, layout: layout([c])}))} : {}), +})); +const stakingRead = (name: string) => (STAKING_ABI as readonly any[]).find( + item => item.name === name && item.stateMutability === "view", +)!; + +describe(`consumer ABI at consensus ${producer.revision}`, () => { + it("matches the producer validator views and all claim/commit readers", () => { + for (const name of ["validatorView", "validatorViewPrimed", "validatorViewPrePrimed"]) { + expect(layout(stakingRead(name).outputs)).toEqual(layout(producer.validatorView.outputs)); + } + const [claim, commit] = producer.delegatorDeposit.outputs; + for (const name of ["delegatorDeposit", "delegatorWithdrawal"]) { + expect(layout(stakingRead(name).outputs)).toEqual(layout([claim, commit])); + } + for (const name of ["validatorDeposit", "validatorWithdrawal", "delegatorDepositByEpoch", + "delegatorWithdrawalByEpoch", "validatorDepositByEpoch", "validatorWithdrawalByEpoch"]) { + expect(layout([stakingRead(name).outputs.at(-1)])).toEqual(layout([commit])); + } + }); + + it("decodes producer claim and commit amounts above uint120 without truncating", () => { + const amount = (1n << 200n) + 7n; + const encoded = encodeAbiParameters(producer.delegatorDeposit.outputs as AbiParameter[], [ + {quantity: amount, offset: amount - 1n, commit: 3n}, + {input: amount, output: amount, outstanding: amount, epoch: 4n, + linkToNextCommit: 0n, priced: true, fragmented: false}, + ]); + const [claim, commit] = decodeFunctionResult({abi: STAKING_ABI, functionName: "delegatorDeposit", data: encoded}); + expect(claim.quantity).toBe(amount); + expect(claim.offset).toBe(amount - 1n); + expect(commit.outstanding).toBe(amount); + }); + + it("matches the producer lifecycle including its independent execution generation", () => { + const fn = CONSENSUS_DATA_TRAIN_ABI.find(item => item.type === "function" && item.name === "getTransactionLifecycle") as any; + expect(layout(fn.outputs)).toEqual(layout(producer.getTransactionLifecycle.outputs)); + }); +}); diff --git a/tests/fixtures/consensus-consumer-abi.json b/tests/fixtures/consensus-consumer-abi.json new file mode 100644 index 0000000..775195e --- /dev/null +++ b/tests/fixtures/consensus-consumer-abi.json @@ -0,0 +1,459 @@ +{ + "repository": "genlayerlabs/genlayer-consensus", + "revision": "c4749a42095bcfb9de69bef4f4fd5e9a6a2f86f2", + "validatorView": { + "inputs": [ + { + "internalType": "address", + "name": "_validator", + "type": "address" + } + ], + "name": "validatorView", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "eBanned", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ePrimed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "vStake", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "vShares", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "dStake", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "dShares", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "vDeposit", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "vWithdrawal", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "hasUnclaimedRewards", + "type": "bool" + } + ], + "internalType": "struct IGenLayerStaking.ValidatorView", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + "delegatorDeposit": { + "inputs": [ + { + "internalType": "address", + "name": "_delegator", + "type": "address" + }, + { + "internalType": "address", + "name": "_validator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_index", + "type": "uint256" + } + ], + "name": "delegatorDeposit", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "quantity", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "offset", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "commit", + "type": "uint256" + } + ], + "internalType": "struct IGenLayerStaking.Claim", + "name": "claim_", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "input", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "output", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outstanding", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "epoch", + "type": "uint64" + }, + { + "internalType": "uint56", + "name": "linkToNextCommit", + "type": "uint56" + }, + { + "internalType": "bool", + "name": "priced", + "type": "bool" + }, + { + "internalType": "bool", + "name": "fragmented", + "type": "bool" + } + ], + "internalType": "struct IGenLayerStaking.Commit", + "name": "commit_", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + "getTransactionLifecycle": { + "inputs": [ + { + "internalType": "bytes32", + "name": "_txId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "_timestamp", + "type": "uint256" + } + ], + "name": "getTransactionLifecycle", + "outputs": [ + { + "components": [ + { + "internalType": "enum ITransactions.TransactionStatus", + "name": "storedStatus", + "type": "uint8" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "txId", + "type": "bytes32" + }, + { + "internalType": "enum ITransactions.TransactionStatus", + "name": "storedStatus", + "type": "uint8" + }, + { + "internalType": "enum ITransactions.TransactionStatus", + "name": "projectedStatus", + "type": "uint8" + }, + { + "internalType": "enum IIdlenessPhase.ResolutionAction", + "name": "action", + "type": "uint8" + }, + { + "internalType": "enum ITransactions.ResultType", + "name": "result", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "resultHash", + "type": "bytes32" + }, + { + "internalType": "enum ITransactionManager.ResolutionSource", + "name": "source", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "sourceRound", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sourceGeneration", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "sourceRoundContextHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "roundPlanHash", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "resultRound", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "resultGeneration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "basisDecisionId", + "type": "uint256" + }, + { + "internalType": "enum ITransactionManager.ResolutionContext", + "name": "context", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "attemptId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "boundaryAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "evaluatedAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "snapshotBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "decisionWindow", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "appealDeadline", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "materializesDecision", + "type": "bool" + }, + { + "internalType": "bool", + "name": "actionOutcomeDeterministic", + "type": "bool" + }, + { + "internalType": "bool", + "name": "nonCurrentEvaluation", + "type": "bool" + } + ], + "internalType": "struct IIdlenessPhase.ResolutionPlan", + "name": "resolution", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bool", + "name": "exists", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "decisionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "basisDecisionId", + "type": "uint256" + }, + { + "internalType": "enum ITransactionManager.ResolutionContext", + "name": "context", + "type": "uint8" + }, + { + "internalType": "enum ITransactionManager.ResolutionSource", + "name": "source", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "sourceAttemptId", + "type": "bytes32" + }, + { + "internalType": "enum ITransactions.TransactionStatus", + "name": "sourceStatus", + "type": "uint8" + }, + { + "internalType": "enum ITransactions.TransactionStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "sourceRound", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sourceGeneration", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "sourceRoundContextHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "roundPlanHash", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "resultRound", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "resultGeneration", + "type": "uint256" + }, + { + "internalType": "enum ITransactions.ResultType", + "name": "result", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "resultHash", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "effectiveAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "materializedAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "appealDeadline", + "type": "uint256" + } + ], + "internalType": "struct ITransactionManager.DecisionRecord", + "name": "latestDecision", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "decisionActive", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "executionGeneration", + "type": "uint256" + } + ], + "internalType": "struct ConsensusData.TransactionLifecycle", + "name": "lifecycle", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + "internalMessageEffectCommitted": { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "txId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "logicalOccurrence", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "generationEffectId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "childTxId", + "type": "bytes32" + } + ], + "name": "InternalMessageEffectCommitted", + "type": "event" + } +} diff --git a/tests/smoke.test.ts b/tests/smoke.test.ts index 0ed9207..f152ecc 100644 --- a/tests/smoke.test.ts +++ b/tests/smoke.test.ts @@ -102,7 +102,7 @@ describe(`Testnet ${name} - Staking (read-only)`, () => { expect(info.owner).toMatch(/^0x[0-9a-fA-F]{40}$/); expect(info.operator).toMatch(/^0x[0-9a-fA-F]{40}$/); expect(info.vStakeRaw).toBeTypeOf("bigint"); - expect(typeof info.live).toBe("boolean"); + expect(typeof info.hasUnclaimedRewards).toBe("boolean"); expect(typeof info.banned).toBe("boolean"); expect(typeof info.needsPriming).toBe("boolean"); expect(Array.isArray(info.pendingDeposits)).toBe(true); diff --git a/tests/staking-actions.test.ts b/tests/staking-actions.test.ts index 0ad8136..02836c7 100644 --- a/tests/staking-actions.test.ts +++ b/tests/staking-actions.test.ts @@ -260,7 +260,7 @@ describe("stakingActions provider lane (Address-only)", () => { }); describe("stakingActions validator reads", () => { - it("uses the authoritative ban predicate instead of a stale nonzero ban epoch", async () => { + it.each([true, false])("keeps rewards=%s separate from the authoritative ban predicate", async hasUnclaimedRewards => { const readContract = vi.fn().mockImplementation(async ({functionName}: any) => { if (functionName === "isValidator") return true; if (functionName === "validatorView") { @@ -273,7 +273,7 @@ describe("stakingActions validator reads", () => { dShares: 0n, vDeposit: 0n, vWithdrawal: 0n, - live: true, + hasUnclaimedRewards, }; } if (functionName === "owner") return ACCOUNT_ADDRESS; @@ -290,6 +290,8 @@ describe("stakingActions validator reads", () => { const info = await actions.getValidatorInfo(VALIDATOR_WALLET_ADDRESS); + expect(info.hasUnclaimedRewards).toBe(hasUnclaimedRewards); + expect(info).not.toHaveProperty("live"); expect(info.banned).toBe(false); expect(info.bannedEpoch).toBeUndefined(); expect(readContract).toHaveBeenCalledWith(expect.objectContaining({ diff --git a/tests/staking-commit-layout.test.ts b/tests/staking-commit-layout.test.ts index fc72fa5..71091ae 100644 --- a/tests/staking-commit-layout.test.ts +++ b/tests/staking-commit-layout.test.ts @@ -4,14 +4,14 @@ import {STAKING_ABI} from "../src/abi/staking"; const STAKE = 100000000000000000n; const CLAIM_COMPONENTS = [ - {name: "quantity", type: "uint120"}, - {name: "offset", type: "uint120"}, + {name: "quantity", type: "uint256"}, + {name: "offset", type: "uint256"}, {name: "commit", type: "uint256"}, ]; const COMMIT_COMPONENTS = [ {name: "input", type: "uint256"}, {name: "output", type: "uint256"}, - {name: "outstanding", type: "uint120"}, + {name: "outstanding", type: "uint256"}, {name: "epoch", type: "uint64"}, {name: "linkToNextCommit", type: "uint56"}, {name: "priced", type: "bool"}, @@ -26,7 +26,7 @@ const VALIDATOR_VIEW_COMPONENTS = [ {name: "dShares", type: "uint256"}, {name: "vDeposit", type: "uint256"}, {name: "vWithdrawal", type: "uint256"}, - {name: "live", type: "bool"}, + {name: "hasUnclaimedRewards", type: "bool"}, ]; const view = (name: string) => @@ -94,7 +94,7 @@ describe("staking ValidatorView train layout", () => { } }); - it("decodes raw train validator bytes without removed tree links", () => { + it.each([true, false])("decodes raw train validator bytes with rewards=%s", hasUnclaimedRewards => { const output = view("validatorView").outputs[0]; const encoded = encodeAbiParameters([output], [{ eBanned: 1n, @@ -105,7 +105,7 @@ describe("staking ValidatorView train layout", () => { dShares: 6n, vDeposit: 7n, vWithdrawal: 8n, - live: true, + hasUnclaimedRewards, }] as any); const decoded = decodeFunctionResult({ @@ -119,8 +119,9 @@ describe("staking ValidatorView train layout", () => { ePrimed: 2n, vStake: 3n, dStake: 5n, - live: true, + hasUnclaimedRewards, }); + expect(decoded).not.toHaveProperty("live"); expect(decoded).not.toHaveProperty("left"); expect(decoded).not.toHaveProperty("right"); expect(decoded).not.toHaveProperty("parent"); diff --git a/tests/transactions.test.ts b/tests/transactions.test.ts index 548c57e..d8ce14f 100644 --- a/tests/transactions.test.ts +++ b/tests/transactions.test.ts @@ -22,16 +22,16 @@ import { import { receiptActions, transactionActions, isSuccessful } from "../src/transactions/actions"; import { decodeTransaction, simplifyTransactionReceipt } from "../src/transactions/decoders"; import { localnet } from "../src/chains/localnet"; -import type { GenLayerRawTransaction } from "../src/types/transactions"; +import type { GenLayerRawTransaction, TransactionHash } from "../src/types/transactions"; import { decodeFunctionResult, encodeFunctionResult, - keccak256, MethodNotFoundRpcError, - stringToBytes, } from "viem"; import {CONSENSUS_DATA_BIG_ROUNDS_TRAIN_ABI, CONSENSUS_DATA_TRAIN_ABI} from "../src/abi/consensusTrain"; +import producer from "./fixtures/consensus-consumer-abi.json"; + const mockFetch = vi.fn(); vi.stubGlobal("fetch", mockFetch); @@ -164,7 +164,7 @@ describe("transaction enum maps", () => { const encoded = encodeFunctionResult({ abi: CONSENSUS_DATA_TRAIN_ABI, functionName: "getTransactionLifecycle", - result: [5, resolution, latestDecision, true] as any, + result: [5, resolution, latestDecision, true, 9n] as any, }); const decoded = decodeFunctionResult({ @@ -177,6 +177,7 @@ describe("transaction enum maps", () => { expect(decoded.resolution.projectedStatus).toBe(6); expect(decoded.resolution.action).toBe(6); expect(decoded.decisionActive).toBe(true); + expect(decoded.executionGeneration).toBe(9n); }); }); @@ -680,52 +681,49 @@ const trainReadContract = ({ }); describe("getTriggeredTransactionIds", () => { - it("finds child transaction IDs in the parent decision receipt", async () => { - const parentHash = ("0x" + "11".repeat(32)) as any; - const childHash = ("0x" + "22".repeat(32)) as any; - const decisionHash = ("0x" + "33".repeat(32)) as any; - const consensusAddress = "0x0000000000000000000000000000000000000010"; - const messagePaymentsAddress = "0x0000000000000000000000000000000000000020"; - const internalMessageTopic = keccak256( - stringToBytes("InternalMessageProcessed(bytes32,address,address)"), - ); - const readContract = trainReadContract({ - light: makeLightTx({ - readStateBlockRange: {activationBlock: 0n, processingBlock: 0n, proposalBlock: 100n}, - }), - }); - const getLogs = vi.fn().mockResolvedValue([{transactionHash: decisionHash}]); - const getTransactionReceipt = vi.fn().mockResolvedValue({ - logs: [ - { - address: messagePaymentsAddress, - topics: [internalMessageTopic, childHash], - }, - ], - }); - const publicClient = { - readContract, - getBlock: vi.fn().mockResolvedValue({number: 150n, timestamp: 1000n}), - getBlockNumber: vi.fn().mockResolvedValue(200n), - getLogs, - getTransactionReceipt, - } as any; - const client = { - chain: { - isStudio: false, - consensusDataContract: {address: consensusAddress, abi: []}, - consensusMainContract: {address: consensusAddress, abi: []}, - }, - } as any; + const parentHash = `0x${"11".repeat(32)}` as TransactionHash; + const childHash = `0x${"22".repeat(32)}` as const; + const laterChild = `0x${"33".repeat(32)}` as const; + const messages = "0x0000000000000000000000000000000000000020"; + const client = {chain: {isStudio: false, consensusDataContract: { + address: "0x0000000000000000000000000000000000000010", + }}} as any; + + it("finds canonical children across deferred delivery and re-reveals without truncating history", async () => { + const getLogs = vi.fn() + .mockResolvedValueOnce([{args: {childTxId: childHash}}]) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([{args: {childTxId: childHash}}, {args: {childTxId: laterChild}}]); + const readContract = vi.fn().mockResolvedValueOnce(client.chain.consensusDataContract.address) + .mockResolvedValueOnce(messages); + const publicClient = {readContract, getBlockNumber: vi.fn().mockResolvedValue(25_000n), getLogs} as any; + const result = await transactionActions(client, publicClient).getTriggeredTransactionIds({hash: parentHash}); + expect(result).toEqual([childHash, laterChild]); + expect(getLogs.mock.calls.map(([args]) => [args.fromBlock, args.toBlock])).toEqual([ + [0n, 9_999n], [10_000n, 19_999n], [20_000n, 25_000n], + ]); + for (const [args] of getLogs.mock.calls) { + expect(args).toMatchObject({address: messages, args: {txId: parentHash}, strict: true}); + expect(args.event.name).toBe(producer.internalMessageEffectCommitted.name); + const eventLayout = (inputs: readonly any[]) => inputs.map(({name, type, indexed}) => ({name, type, indexed: !!indexed})); + expect(eventLayout(args.event.inputs)).toEqual(eventLayout(producer.internalMessageEffectCommitted.inputs)); + } + expect(readContract.mock.calls[1][0]).toMatchObject({args: ["Messages"], blockNumber: 25_000n}); + }); - const result = await transactionActions(client, publicClient).getTriggeredTransactionIds({ - hash: parentHash, - }); + it("supports a deployment block lower bound and propagates RPC failures", async () => { + const getLogs = vi.fn().mockRejectedValue(new Error("RPC unavailable")); + const publicClient = {readContract: vi.fn().mockResolvedValue(messages), + getBlockNumber: vi.fn().mockResolvedValue(25_000n), getLogs} as any; + await expect(transactionActions(client, publicClient).getTriggeredTransactionIds({ + hash: parentHash, fromBlock: 24_000n, + })).rejects.toThrow("RPC unavailable"); + expect(getLogs.mock.calls[0][0]).toMatchObject({fromBlock: 24_000n, toBlock: 25_000n}); + }); - expect(result).toEqual([childHash]); - expect(getLogs.mock.calls[0][0].topics[1]).toBe(parentHash); - expect(Array.isArray(getLogs.mock.calls[0][0].topics[0])).toBe(true); - expect(getTransactionReceipt).toHaveBeenCalledWith({hash: decisionHash}); + it("preserves Studio's canonical triggered_transactions response", async () => { + const studio = {chain: {isStudio: true}, getTransaction: vi.fn().mockResolvedValue({triggered_transactions: [childHash]})} as any; + expect(await transactionActions(studio, {} as any).getTriggeredTransactionIds({hash: parentHash})).toEqual([childHash]); }); }); @@ -795,6 +793,7 @@ describe("getTransaction train lifecycle", () => { storedStatus: 5, resolution: {projectedStatus: 6, action: 6, source: 11, evaluatedAt: 500n}, latestDecision: {decisionId: 7n}, + executionGeneration: 9007199254740993n, decisionActive: true, }), }); @@ -827,6 +826,7 @@ describe("getTransaction train lifecycle", () => { resolutionSource: "SelectionDepleted", resolutionSourceCode: 11, decisionId: "7", + executionGeneration: "9007199254740993", decisionActive: true, evaluatedAt: 500, });