Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 268 additions & 1 deletion indexer/src/decode.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,39 @@
import { describe, expect, it } from "vitest";
import { StrKey, xdr } from "@stellar/stellar-sdk";
import { extractEvents, extractInvocationReturnValue, scValToDisplay } from "./decode.js";
import {
decodeContractCreation,
decodeInvocation,
extractEvents,
extractInvocationReturnValue,
extractStorageChanges,
scValToDisplay,
} from "./decode.js";
import createContractV2Fixture from "./fixtures/create-contract-v2.json" with { type: "json" };
import createContractFixture from "./fixtures/create-contract.json" with { type: "json" };
import failedInvokeFixture from "./fixtures/failed-invoke.json" with { type: "json" };
import multiOperationFixture from "./fixtures/multi-operation.json" with { type: "json" };
import successfulInvokeFixture from "./fixtures/successful-invoke.json" with { type: "json" };

interface Fixture {
description: string;
hash: string;
ledger: number;
status: "SUCCESS" | "FAILED";
envelopeXdr: string;
resultMetaXdr: string;
}

/**
* Testnet envelopes and metas captured from Soroban RPC (`getTransaction`) on
* 2026-10-22; see src/fixtures/README.md for provenance.
*/
function loadFixture(fixture: Fixture) {
return {
...fixture,
envelope: xdr.TransactionEnvelope.fromXDR(fixture.envelopeXdr, "base64"),
meta: xdr.TransactionMeta.fromXDR(fixture.resultMetaXdr, "base64"),
};
}

describe("scValToDisplay", () => {
it("decodes a symbol to a plain string", () => {
Expand Down Expand Up @@ -145,3 +178,237 @@ describe("extractEvents", () => {
expect(extractEvents(meta, txHash)).toEqual(expected);
});
});

// ---------------------------------------------------------------------------
// Fixture-based tests: real testnet envelopes and metas.
// ---------------------------------------------------------------------------

const successfulInvoke = loadFixture(successfulInvokeFixture as Fixture);
const failedInvoke = loadFixture(failedInvokeFixture as Fixture);
const createContract = loadFixture(createContractFixture as Fixture);
const createContractV2 = loadFixture(createContractV2Fixture as Fixture);
const multiOperation = loadFixture(multiOperationFixture as Fixture);

describe("scValToDisplay (testnet fixtures)", () => {
it("renders the Soroban return value of a real invocation", () => {
const returnValue = successfulInvoke.meta.v4().sorobanMeta()!.returnValue()!;
expect(scValToDisplay(returnValue)).toBe('["1493531","1472284"]');
});
});

describe("decodeInvocation (testnet fixtures)", () => {
it("decodes the contract, function and args of a successful invoke", () => {
expect(decodeInvocation(successfulInvoke.envelope)).toEqual({
contractId: "CBXZBQ6NW3TGPGLTR4JCG2VPVWFLCCHKDW75NJIXYXV7CJLNUO64VTFC",
functionName: "deposit_liquidity_and_collateral",
args: [
"1",
"GB6YNCUDIPIFCXHXUTE3F54GFYGFMHUK6ZR5LLXD36TZTYC2DK2WYQ3U",
"2",
"1493531",
'{"id":0,"seed1":null,"seed2":null,"tag":0}',
],
});
});

it("still decodes the invocation of a failed transaction", () => {
expect(failedInvoke.status).toBe("FAILED");
const decoded = decodeInvocation(failedInvoke.envelope);
expect(decoded?.contractId).toBe("CD4KFB23CQLJ47RIPESVBTQ444R75M6QVEZULWXHHFYYZT7SS2VGXOPW");
expect(decoded?.functionName).toBe("write_prices");
expect(decoded?.args).toEqual([
"GBV54BUAQBFAS6WIRKGURXXCMXWALH4OYBCYE75VBHROJLEKJ4LOZTFB",
'["BTC"]',
expect.stringContaining("425443"),
]);
});

it("returns null for a multi-operation transaction with no host-function op", () => {
expect(decodeInvocation(multiOperation.envelope)).toBeNull();
});

it("returns null for contract-creation host functions", () => {
expect(decodeInvocation(createContract.envelope)).toBeNull();
expect(decodeInvocation(createContractV2.envelope)).toBeNull();
});
});

describe("extractInvocationReturnValue (testnet fixtures)", () => {
it("decodes the return value of a successful invocation", () => {
expect(extractInvocationReturnValue(successfulInvoke.meta)).toBe('["1493531","1472284"]');
});

it("returns null when the transaction failed", () => {
expect(extractInvocationReturnValue(failedInvoke.meta)).toBeNull();
});

it("returns the new contract address for a creation", () => {
expect(extractInvocationReturnValue(createContract.meta)).toBe(
"CCJKICDXTNUHZBFYYLMGSDAW6FD23DS5UBWYMBC2ECYFSX3EGSSW6G5B"
);
});
});

describe("decodeContractCreation (testnet fixtures)", () => {
it("decodes a createContract host function", () => {
expect(decodeContractCreation(createContract.envelope, createContract.meta, "GCREATOR")).toEqual({
contractId: "CCJKICDXTNUHZBFYYLMGSDAW6FD23DS5UBWYMBC2ECYFSX3EGSSW6G5B",
creator: "GCREATOR",
});
});

it("decodes a createContractV2 host function", () => {
expect(decodeContractCreation(createContractV2.envelope, createContractV2.meta, "GCREATOR")).toEqual({
contractId: "CBUZIAY2CBUCCRRRJBUANPTOQEF42YETHVG44N34NZ7XIUQ6L7DBV56D",
creator: "GCREATOR",
});
});

it("returns null for an ordinary invocation and for a failed transaction", () => {
expect(decodeContractCreation(successfulInvoke.envelope, successfulInvoke.meta, "GCREATOR")).toBeNull();
expect(decodeContractCreation(failedInvoke.envelope, failedInvoke.meta, "GCREATOR")).toBeNull();
});
});

describe("extractStorageChanges (testnet fixtures)", () => {
it("extracts every entry change of a successful invocation", () => {
const changes = extractStorageChanges(successfulInvoke.meta);
expect(changes).toHaveLength(16);
expect(changes.map((change) => change.changeType)).toEqual([
"state",
"state",
"state",
"state",
"updated",
"updated",
"updated",
"updated",
"state",
"updated",
"state",
"updated",
"state",
"updated",
"state",
"updated",
]);
});

it("expands instance entries into one row per storage key plus the wasm hash", () => {
const instanceChanges = extractStorageChanges(successfulInvoke.meta).filter(
(change) => change.durability === "instance"
);
expect(instanceChanges).toHaveLength(8);
expect(instanceChanges.every((change) => change.contractId === "CDH4BTGP7GRDFO6Y24AP4RP34NMC3V2RXTTUA4ABCFRMFEYWR6AG6GK6")).toBe(
true
);
expect(instanceChanges[0]).toEqual({
contractId: "CDH4BTGP7GRDFO6Y24AP4RP34NMC3V2RXTTUA4ABCFRMFEYWR6AG6GK6",
durability: "instance",
keyDisplay: "__wasm_hash__",
valueDisplay: "a5f6b06ca8fb10a7f3d12ba5fae3361f04a242054757080cf14c375b88e1c898",
changeType: "state",
});
});

it("decodes persistent contract data keys and values", () => {
const changes = extractStorageChanges(successfulInvoke.meta);
expect(changes[9]).toMatchObject({
contractId: "CBXZBQ6NW3TGPGLTR4JCG2VPVWFLCCHKDW75NJIXYXV7CJLNUO64VTFC",
durability: "persistent",
keyDisplay: '["Reserve",1,2]',
valueDisplay: expect.stringContaining('"available":"9462586167919"'),
changeType: "updated",
});
expect(changes[6]).toMatchObject({
contractId: "CDH4BTGP7GRDFO6Y24AP4RP34NMC3V2RXTTUA4ABCFRMFEYWR6AG6GK6",
durability: "instance",
keyDisplay: '["Metadata"]',
changeType: "updated",
});
});

it("collapses a created contract's instance storage into its created rows", () => {
expect(extractStorageChanges(createContract.meta)).toEqual([
{
contractId: "CCJKICDXTNUHZBFYYLMGSDAW6FD23DS5UBWYMBC2ECYFSX3EGSSW6G5B",
durability: "instance",
keyDisplay: "METADATA",
valueDisplay:
'{"decimal":7,"name":"KEUR:GDA44W6ZQ7TI65ZZ5E3TOHBEQXSKIBTNFFY5N4XVLQBA3AUQW2B3GLUU","symbol":"KEUR"}',
changeType: "created",
},
{
contractId: "CCJKICDXTNUHZBFYYLMGSDAW6FD23DS5UBWYMBC2ECYFSX3EGSSW6G5B",
durability: "instance",
keyDisplay: '["Admin"]',
valueDisplay: "GDA44W6ZQ7TI65ZZ5E3TOHBEQXSKIBTNFFY5N4XVLQBA3AUQW2B3GLUU",
changeType: "created",
},
{
contractId: "CCJKICDXTNUHZBFYYLMGSDAW6FD23DS5UBWYMBC2ECYFSX3EGSSW6G5B",
durability: "instance",
keyDisplay: '["AssetInfo"]',
valueDisplay:
'["AlphaNum4",{"asset_code":"KEUR","issuer":"c1ce5bd987e68f7739e937371c2485e4a4066d2971d6f2f55c020d8290b683b3"}]',
changeType: "created",
},
]);
});

it("records only the wasm hash when a V2 creation has no instance storage", () => {
expect(extractStorageChanges(createContractV2.meta)).toEqual([
{
contractId: "CBUZIAY2CBUCCRRRJBUANPTOQEF42YETHVG44N34NZ7XIUQ6L7DBV56D",
durability: "instance",
keyDisplay: "__wasm_hash__",
valueDisplay: "c1a650506f7c20c8f4d16aae73f894f302cd011d7ef33adef572f20b34f7653e",
changeType: "created",
},
]);
});

it("yields no contract-data changes for a failed transaction", () => {
expect(extractStorageChanges(failedInvoke.meta)).toEqual([]);
});

it("ignores non-contract-data ledger changes across multiple operations", () => {
// The three operations only touch account/sponsorship ledger entries.
expect(extractStorageChanges(multiOperation.meta)).toEqual([]);
});
});

describe("extractEvents (testnet fixtures)", () => {
it("extracts contract events with topics and values from a successful invocation", () => {
const events = extractEvents(successfulInvoke.meta, successfulInvoke.hash);
expect(events).toHaveLength(4);
expect(events.map((event) => event.eventId)).toEqual([
`${successfulInvoke.hash}-0-0`,
`${successfulInvoke.hash}-0-1`,
`${successfulInvoke.hash}-0-2`,
`${successfulInvoke.hash}-0-3`,
]);
expect(events[0]).toEqual({
eventId: `${successfulInvoke.hash}-0-0`,
contractId: "CBVJ6GYJJ55ALTHN32OJEFGUZWWTMDFGIBCM42SPYQ4GWLKX2XARAP2N",
type: "contract",
topic: [
"transfer",
"GB6YNCUDIPIFCXHXUTE3F54GFYGFMHUK6ZR5LLXD36TZTYC2DK2WYQ3U",
"CBXZBQ6NW3TGPGLTR4JCG2VPVWFLCCHKDW75NJIXYXV7CJLNUO64VTFC",
"SOL:GAGVZMODPYFTULIVSY3RGFADIC5LF4LLSN37N4MFWGNZ57TYBXL2LQQX",
],
value: "1493531",
});
expect(events[1]?.topic[0]).toBe("mint");
expect(events[2]?.value).toBe('["1493531","1472284"]');
});

it("extracts no events from a failed transaction", () => {
expect(extractEvents(failedInvoke.meta, failedInvoke.hash)).toEqual([]);
});

it("extracts no events from a multi-operation transaction without Soroban events", () => {
expect(extractEvents(multiOperation.meta, multiOperation.hash)).toEqual([]);
});
});
19 changes: 19 additions & 0 deletions indexer/src/fixtures/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Testnet transaction fixtures

Envelopes and transaction metas captured from Stellar testnet on 2026-10-22 via
Soroban RPC `getTransaction` (`https://soroban-testnet.stellar.org`). Each file
holds the base64 `envelopeXdr` and `resultMetaXdr` exactly as the RPC returned
them, plus the transaction hash, ledger and final status for reference:

`https://horizon-testnet.stellar.org/transactions/<hash>`

| File | What it covers |
| ------------------------ | ------------------------------------------------------------------------------ |
| `successful-invoke.json` | Successful `invokeHostFunction` (SAC deposit into a lending pool): return value, 16 storage changes including expanded instance entries, and 4 contract events. |
| `failed-invoke.json` | Failed `invokeHostFunction` (oracle `write_prices` rejected): invocation is still decodable, but there is no return value, no storage change and no event. |
| `create-contract.json` | Successful `createContract` host function whose instance has storage entries. |
| `create-contract-v2.json`| Successful `createContractV2` host function whose instance only has a wasm hash. |
| `multi-operation.json` | Successful three-operation (sponsorship) transaction: exercises decoders over multiple operations, none of which is a host function. |

These files are decoded by `src/decode.test.ts`; refresh them only if the
decoders' expected output intentionally changes.
8 changes: 8 additions & 0 deletions indexer/src/fixtures/create-contract-v2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"description": "Successful createContractV2 host function",
"hash": "6df181248ff9cf3f830a9f3e7e71d0032b428b014fe90fa36fe5396152220126",
"ledger": 5327582,
"status": "SUCCESS",
"envelopeXdr": "AAAAAgAAAACZ3D1SSP9k5nKIH73DFtzCNti/aFWvZ9p47K72I9YAwgABwEsAUUrdAAAAAQAAAAEAAAAAAAAAAAAAAABq2dkhAAAAAAAAAAEAAAAAAAAAGAAAAAMAAAAAAAAAAAAAAACZ3D1SSP9k5nKIH73DFtzCNti/aFWvZ9p47K72I9YAwhn/Z/umEKLlZzr2hCwOFyoGub/k/r54t9FRT/ldBHEZAAAAAMGmUFBvfCDI9NFqrnP4lPMCzQEdfvM63vVy8gs092U+AAAAAAAAAAEAAAAAAAAAAgAAAAAAAAAAAAAAAJncPVJI/2TmcogfvcMW3MI22L9oVa9n2njsrvYj1gDCGf9n+6YQouVnOvaELA4XKga5v+T+vni30VFP+V0EcRkAAAAAwaZQUG98IMj00Wquc/iU8wLNAR1+8zre9XLyCzT3ZT4AAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAfBplBQb3wgyPTRaq5z+JTzAs0BHX7zOt71cvILNPdlPgAAAAEAAAAGAAAAAWmUAxoQaCFGMUhoBr5ugQvNYJM9Tc43fG5/dFIeX8YaAAAAFAAAAAEABQWdAAAAAAAAAGgAAAAAAAG/5wAAAAEj1gDCAAAAQPKcuMWqoDZPAnN1WIpnFvNp/Quub5rIABW3SlqXSnSsEvtxT948A4D5c27acFn+egPtEwE1aimG9ZNFNz6X8gc=",
"resultMetaXdr": "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"
}
Loading