diff --git a/route/chain_dependency_test.go b/route/chain_dependency_test.go new file mode 100644 index 0000000..832be87 --- /dev/null +++ b/route/chain_dependency_test.go @@ -0,0 +1,428 @@ +package route + +import ( + "context" + "encoding/json" + "path/filepath" + "strings" + "testing" + + "github.com/shopspring/decimal" + + "github.com/Wayfare-labs/wayfare/asset" + "github.com/Wayfare-labs/wayfare/dex" + "github.com/Wayfare-labs/wayfare/refrate" + "github.com/Wayfare-labs/wayfare/snapshot" +) + +// The dependency-chain tests replay scenario fixtures through +// snapshot.Replayer, like every other recorded test in this package. The +// cases they pin — a dependency that is itself derivative, a cycle, a +// dependency with no market at the tested size — do not occur on the +// recorded mainnet set, so each fixture under testdata/chain-snapshots is +// a scenario captured once from a synthetic upstream through the standard +// snapshot.Recorder (see the note in each manifest). They live in their own +// directory rather than testdata/snapshots so corridor tools that walk that +// directory (cmd/hop-analysis, the recorded-integrity tests) keep seeing +// exactly the three mainnet corridors. Replaying them keeps the same +// guarantee as the real recordings: a request the fixture does not know +// about, such as a regression that adds an extra Horizon call, fails loudly +// with snapshot.ErrNotRecorded instead of passing silently. + +// chainSnap loads the scenario fixture for one dependency-chain case. +func chainSnap(t *testing.T, prefix string) *snapshot.Manifest { + t.Helper() + matches, err := filepath.Glob(filepath.Join("..", "testdata", "chain-snapshots", prefix+"-*")) + if err != nil || len(matches) == 0 { + t.Fatalf("no chain fixture matching %q under testdata/chain-snapshots", prefix) + } + m, err := snapshot.Load(matches[0]) + if err != nil { + t.Fatalf("loading chain fixture %s: %v", matches[0], err) + } + return m +} + +// chainEngine builds an engine answering only from a chain scenario fixture, +// with the reference mid pinned so the assertions are about the chain rather +// than about whatever the rate provider says today. +func chainEngine(m *snapshot.Manifest, pair, mid string) *Engine { + return &Engine{ + DEX: &dex.Client{ + HorizonURL: "https://horizon.stellar.org", + HTTPClient: m.HTTPClient(), + }, + RefRate: refrate.NewStatic(map[string]decimal.Decimal{ + pair: decimal.RequireFromString(mid), + }), + } +} + +// TestChainMeasuredDirect verifies that when a derivative corridor's +// dependency is measured, the chain carries the measured integrity. +// USDC→GHSC depends on NGNC; USDC→NGNC has an XLM path (bridge asset), +// so NGNC is DIRECT. The chain should be depth 1 with NGNC measured as DIRECT. +func TestChainMeasuredDirect(t *testing.T) { + e := chainEngine(chainSnap(t, "usdc-ghsc-chain-direct-ngnc"), "USD/GHS", "11.7625") + res, err := e.Quote(context.Background(), ghsRequest("100")) + if err != nil { + t.Fatalf("Quote: %v", err) + } + + if res.Integrity != IntegrityDerivative { + t.Fatalf("Integrity = %s, want DERIVATIVE", res.Integrity) + } + + if len(res.Chain) != 1 || res.Chain[0].Asset.Code != "NGNC" { + t.Fatalf("Chain = %v, want exactly NGNC", res.Chain) + } + node := res.Chain[0] + if !node.Measured { + t.Error("NGNC should be measured") + } + if node.Integrity != IntegrityDirect { + t.Errorf("NGNC integrity = %s, want DIRECT", node.Integrity) + } + if len(node.Dependencies) != 0 { + t.Errorf("NGNC should have no sub-dependencies, got %v", node.Dependencies) + } + + // The warning should use the measured variant. + warnings := strings.Join(res.Quotes[0].Warnings, " ") + if !strings.Contains(warnings, "DIRECT, independent market exists") { + t.Errorf("expected measured warning with market status, got: %v", + res.Quotes[0].Warnings) + } +} + +// TestChainDepthTwo verifies recursive chain measurement through two levels. +// USDC→GHSC depends on KESC, KESC depends on NGNC, NGNC is DIRECT (reached +// via XLM, a bridge asset). +func TestChainDepthTwo(t *testing.T) { + e := chainEngine(chainSnap(t, "usdc-ghsc-chain-depth-two"), "USD/GHS", "11.7625") + res, err := e.Quote(context.Background(), ghsRequest("100")) + if err != nil { + t.Fatalf("Quote: %v", err) + } + + if res.Integrity != IntegrityDerivative { + t.Fatalf("Integrity = %s, want DERIVATIVE", res.Integrity) + } + + // Chain: GHSC depends on KESC (depth 2), KESC depends on NGNC (depth 1), + // NGNC is DIRECT (depth 0). + if len(res.Chain) != 1 || res.Chain[0].Asset.Code != "KESC" { + t.Fatalf("Chain top level = %v, want KESC", res.Chain) + } + kescNode := res.Chain[0] + if !kescNode.Measured { + t.Error("KESC should be measured") + } + if kescNode.Integrity != IntegrityDerivative { + t.Errorf("KESC integrity = %s, want DERIVATIVE", kescNode.Integrity) + } + if len(kescNode.Dependencies) != 1 || kescNode.Dependencies[0].Asset.Code != "NGNC" { + t.Fatalf("KESC dependencies = %v, want NGNC", kescNode.Dependencies) + } + ngncNode := kescNode.Dependencies[0] + if !ngncNode.Measured { + t.Error("NGNC should be measured") + } + if ngncNode.Integrity != IntegrityDirect { + t.Errorf("NGNC integrity = %s, want DIRECT", ngncNode.Integrity) + } + + // All measured. + if !allMeasured(res.Chain) { + t.Error("all nodes should be measured in this chain") + } +} + +// TestChainCycleTerminates verifies that a circular dependency does not +// cause infinite recursion. When USDC→NGNC routes through GHSC while GHSC is +// already on the path (it is the destination), the second encounter is +// detected as a cycle and reported as unmeasured. +func TestChainCycleTerminates(t *testing.T) { + e := chainEngine(chainSnap(t, "usdc-ghsc-chain-cycle"), "USD/GHS", "11.7625") + res, err := e.Quote(context.Background(), ghsRequest("100")) + if err != nil { + t.Fatalf("Quote: %v", err) + } + + if res.Integrity != IntegrityDerivative { + t.Fatalf("Integrity = %s, want DERIVATIVE", res.Integrity) + } + + // The chain should have NGNC as the top-level dependency. + if len(res.Chain) != 1 || res.Chain[0].Asset.Code != "NGNC" { + t.Fatalf("Chain top level = %v, want NGNC", res.Chain) + } + ngncNode := res.Chain[0] + if !ngncNode.Measured { + t.Error("NGNC should be measured (first encounter)") + } + if ngncNode.Integrity != IntegrityDerivative { + t.Errorf("NGNC integrity = %s, want DERIVATIVE", ngncNode.Integrity) + } + + // NGNC depends on GHSC, but GHSC is already visited (it's the + // destination), so it should be reported as unmeasured with cycle reason. + if len(ngncNode.Dependencies) != 1 || ngncNode.Dependencies[0].Asset.Code != "GHSC" { + t.Fatalf("NGNC dependencies = %v, want GHSC", ngncNode.Dependencies) + } + ghscNode := ngncNode.Dependencies[0] + if ghscNode.Measured { + t.Error("GHSC should NOT be measured (cycle detected)") + } + if ghscNode.Reason != "cycle detected" { + t.Errorf("GHSC reason = %q, want 'cycle detected'", ghscNode.Reason) + } +} + +// TestChainDependencyHasNoMarket verifies that a dependency whose own +// market is NO-MARKET is reported honestly in the chain. +func TestChainDependencyHasNoMarket(t *testing.T) { + e := chainEngine(chainSnap(t, "usdc-ghsc-chain-no-market"), "USD/GHS", "11.7625") + res, err := e.Quote(context.Background(), ghsRequest("100")) + if err != nil { + t.Fatalf("Quote: %v", err) + } + + if res.Integrity != IntegrityDerivative { + t.Fatalf("Integrity = %s, want DERIVATIVE", res.Integrity) + } + + if len(res.Chain) != 1 || res.Chain[0].Asset.Code != "KESC" { + t.Fatalf("Chain = %v, want KESC", res.Chain) + } + kescNode := res.Chain[0] + if !kescNode.Measured { + t.Error("KESC should be measured") + } + if kescNode.Integrity != IntegrityNoMarket { + t.Errorf("KESC integrity = %s, want NO-MARKET", kescNode.Integrity) + } + + // NO-MARKET is still a measurement, so the whole chain is measured. + if !allMeasured(res.Chain) { + t.Error("all nodes should be measured (NO-MARKET is still a measurement)") + } +} + +// TestChainSharedDependencyIsNotACycle pins the visited-set rule: GHSC +// depends on KESC and NGNC, and KESC also depends on NGNC. NGNC is a +// sibling-shared sub-dependency — it appears on two branches of the tree — +// which is not a cycle, and both top-level dependencies must come back +// measured rather than the second one being mislabelled "cycle detected". +func TestChainSharedDependencyIsNotACycle(t *testing.T) { + e := chainEngine(chainSnap(t, "usdc-ghsc-chain-shared"), "USD/GHS", "11.7625") + res, err := e.Quote(context.Background(), ghsRequest("100")) + if err != nil { + t.Fatalf("Quote: %v", err) + } + + if res.Integrity != IntegrityDerivative { + t.Fatalf("Integrity = %s, want DERIVATIVE", res.Integrity) + } + + // Both dependencies must be measured, in code order. + if len(res.Chain) != 2 || res.Chain[0].Asset.Code != "KESC" || res.Chain[1].Asset.Code != "NGNC" { + t.Fatalf("Chain top level = %v, want [KESC NGNC]", res.Chain) + } + for _, n := range res.Chain { + if !n.Measured { + t.Errorf("%s should be measured, not %q", n.Asset.Code, n.Reason) + } + } + + // KESC is derivative and its own dependency (NGNC) is measured. + kescNode := res.Chain[0] + if kescNode.Integrity != IntegrityDerivative { + t.Errorf("KESC integrity = %s, want DERIVATIVE", kescNode.Integrity) + } + if len(kescNode.Dependencies) != 1 || kescNode.Dependencies[0].Asset.Code != "NGNC" { + t.Fatalf("KESC dependencies = %v, want NGNC", kescNode.Dependencies) + } + if !kescNode.Dependencies[0].Measured { + t.Error("NGNC under KESC should be measured") + } + + // NGNC appears again as a sibling top-level dependency, measured. + ngncNode := res.Chain[1] + if ngncNode.Integrity != IntegrityDirect { + t.Errorf("NGNC integrity = %s, want DIRECT", ngncNode.Integrity) + } + + if !allMeasured(res.Chain) { + t.Error("every node should be measured; none of the shared dependencies is a cycle") + } +} + +// TestChainWireShape verifies the JSON wire shape of the dependency chain. +func TestChainWireShape(t *testing.T) { + e := chainEngine(chainSnap(t, "usdc-ghsc-chain-direct-ngnc"), "USD/GHS", "11.7625") + res, err := e.Quote(context.Background(), ghsRequest("100")) + if err != nil { + t.Fatalf("Quote: %v", err) + } + + chain := ToDependencyChainJSON(res.Chain) + if chain == nil { + t.Fatal("chain should not be nil for a derivative corridor") + } + if chain.Depth != 1 { + t.Errorf("depth = %d, want 1", chain.Depth) + } + if len(chain.DependsOn) != 1 { + t.Fatalf("depends_on = %d nodes, want 1", len(chain.DependsOn)) + } + node := chain.DependsOn[0] + if node.Code != "NGNC" { + t.Errorf("code = %s, want NGNC", node.Code) + } + if !node.Measured { + t.Error("measured should be true") + } + if node.Integrity != "DIRECT" { + t.Errorf("integrity = %s, want DIRECT", node.Integrity) + } + if node.Peg != "NGN" { + t.Errorf("peg = %s, want NGN", node.Peg) + } + if len(node.Dependencies) != 0 { + t.Errorf("sub-dependencies = %d, want 0", len(node.Dependencies)) + } +} + +// TestChainBackwardCompatible pins the compatibility contract on the +// serialized document rather than on Go struct fields: a consumer that only +// knows the flat depends_on array must still find it in the same JSON as a +// chain-aware one finds dependency_chain. +func TestChainBackwardCompatible(t *testing.T) { + e := chainEngine(chainSnap(t, "usdc-ghsc-chain-direct-ngnc"), "USD/GHS", "11.7625") + lr, err := e.Ladder(context.Background(), LadderRequest{ + SendAsset: asset.USDC(), + ReceiveAsset: asset.GHSC(), + Sizes: []decimal.Decimal{decimal.NewFromInt(100)}, + ReferenceBase: "USD", + ReferenceQuote: "GHS", + }) + if err != nil { + t.Fatalf("Ladder: %v", err) + } + + raw, err := json.Marshal(ToCorridorJSON(lr, "USD/GHS")) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + var doc map[string]json.RawMessage + if err := json.Unmarshal(raw, &doc); err != nil { + t.Fatalf("unmarshal: %v", err) + } + + // The flat depends_on array is what a pre-chain consumer reads. + var flat []struct { + Code string `json:"code"` + } + if err := json.Unmarshal(doc["depends_on"], &flat); err != nil { + t.Fatalf("depends_on: %v", err) + } + if len(flat) != 1 || flat[0].Code != "NGNC" { + t.Errorf("depends_on = %v, want exactly NGNC", flat) + } + + // The chain is additive: dependency_chain must be present alongside it. + chainRaw, ok := doc["dependency_chain"] + if !ok { + t.Fatal("serialized corridor is missing dependency_chain") + } + var chain struct { + Depth int `json:"depth"` + DependsOn []struct { + Code string `json:"code"` + Measured bool `json:"measured"` + } `json:"depends_on"` + } + if err := json.Unmarshal(chainRaw, &chain); err != nil { + t.Fatalf("dependency_chain: %v", err) + } + if chain.Depth != 1 { + t.Errorf("dependency_chain depth = %d, want 1", chain.Depth) + } + if len(chain.DependsOn) != 1 || chain.DependsOn[0].Code != "NGNC" || + !chain.DependsOn[0].Measured { + t.Errorf("dependency_chain depends_on = %+v, want NGNC measured", + chain.DependsOn) + } +} + +// TestLadderChainKeepsMeasuredOverUnmeasured pins the aggregation rule for +// chains across rungs: the union across the ladder may not let one rung's +// unmeasured placeholder erase another rung's measurement of the same +// dependency. +func TestLadderChainKeepsMeasuredOverUnmeasured(t *testing.T) { + measured := []DependencyNode{ + {Asset: asset.NGNC(), Measured: true, Integrity: IntegrityDirect}, + } + unmeasured := []DependencyNode{ + {Asset: asset.NGNC(), Measured: false, Reason: "Horizon error: timeout"}, + } + + mk := func(first, second []DependencyNode) *LadderResult { + return &LadderResult{ + Request: LadderRequest{ + SendAsset: asset.USDC(), + ReceiveAsset: asset.GHSC(), + ReferenceBase: "USD", + ReferenceQuote: "GHS", + }, + Rungs: []Rung{ + {SendAmount: decimal.NewFromInt(1), Result: &Result{ + Integrity: IntegrityDerivative, + DependsOn: []asset.Asset{asset.NGNC()}, + Chain: first, + }}, + {SendAmount: decimal.NewFromInt(10), Result: &Result{ + Integrity: IntegrityDerivative, + DependsOn: []asset.Asset{asset.NGNC()}, + Chain: second, + }}, + }, + } + } + + // Unmeasured rung first, measured rung second: the measurement wins. + l := mk(unmeasured, measured) + l.summarise() + if len(l.Chain) != 1 || !l.Chain[0].Measured { + t.Fatalf("Chain = %v, want the measured NGNC to survive aggregation", l.Chain) + } + if got := l.Chain[0].Integrity; got != IntegrityDirect { + t.Errorf("NGNC integrity = %s, want DIRECT", got) + } + + // Measured rung first, unmeasured second: the measurement is kept. + l = mk(measured, unmeasured) + l.summarise() + if len(l.Chain) != 1 || !l.Chain[0].Measured { + t.Fatalf("Chain = %v, want the measured NGNC to keep its place", l.Chain) + } +} + +// TestDirectCorridorHasNoChain verifies that a direct corridor does not +// produce a dependency chain. +func TestDirectCorridorHasNoChain(t *testing.T) { + e := chainEngine(chainSnap(t, "usdc-ngnc-chain-direct"), "USD/NGN", "1500") + res, err := e.Quote(context.Background(), ngnRequest("100")) + if err != nil { + t.Fatalf("Quote: %v", err) + } + + if res.Integrity != IntegrityDirect { + t.Errorf("Integrity = %s, want DIRECT", res.Integrity) + } + if res.Chain != nil { + t.Errorf("Chain = %v, want nil for direct corridor", res.Chain) + } +} diff --git a/route/ladder.go b/route/ladder.go index 82af705..7baaea7 100644 --- a/route/ladder.go +++ b/route/ladder.go @@ -91,6 +91,14 @@ type LadderResult struct { Integrity Integrity DependsOn []asset.Asset + // Chain is the full dependency tree when the corridor is derivative. + // It is the union across all rungs: if any rung discovered additional + // dependencies, they appear here, and when the same dependency was + // measured on some rungs but unmeasured on others the measured node + // wins — one rung's failed request never erases another's finding. + // Nil when the corridor is not derivative. + Chain []DependencyNode + ReferenceMid decimal.Decimal ReferenceSource string @@ -330,6 +338,7 @@ func (l *LadderResult) summarise() { anyDirect bool allNoMarket = true deps = map[string]asset.Asset{} + chainMap = map[string]DependencyNode{} firstErr error ) @@ -363,6 +372,18 @@ func (l *LadderResult) summarise() { for _, d := range r.Result.DependsOn { deps[d.Code+":"+d.Issuer] = d } + for _, c := range r.Result.Chain { + // A rung that failed to measure a dependency reports it + // unmeasured. That must never overwrite a real measurement + // from another rung: the union across rungs keeps the + // strongest evidence for each dependency, so a measured + // node only ever replaces an unmeasured placeholder. + key := c.Asset.Code + ":" + c.Asset.Issuer + existing, ok := chainMap[key] + if !ok || (c.Measured && !existing.Measured) { + chainMap[key] = c + } + } case IntegrityNoMarket: // leaves allNoMarket intact default: @@ -410,6 +431,13 @@ func (l *LadderResult) summarise() { sort.Slice(l.DependsOn, func(i, j int) bool { return l.DependsOn[i].Code < l.DependsOn[j].Code }) + l.Chain = make([]DependencyNode, 0, len(chainMap)) + for _, c := range chainMap { + l.Chain = append(l.Chain, c) + } + sort.Slice(l.Chain, func(i, j int) bool { + return l.Chain[i].Asset.Code < l.Chain[j].Asset.Code + }) default: l.Integrity = IntegrityUnknown } @@ -447,11 +475,19 @@ func (l *LadderResult) finding(anyPriced bool, firstErr error) string { var prefix string if l.Integrity == IntegrityDerivative { - prefix = fmt.Sprintf( - "Derivative corridor: every path from %s to %s routes through %s, so "+ - "%s has no independent market and these figures compound %s's cost "+ - "with its own. ", - send, recv, describeAssets(l.DependsOn), recv, describeAssets(l.DependsOn)) + if allMeasured(l.Chain) { + prefix = fmt.Sprintf( + "Derivative corridor: every path from %s to %s routes through %s, so "+ + "%s has no independent market and these figures inherit %s's "+ + "liquidity and failure modes. ", + send, recv, describeAssets(l.DependsOn), recv, describeAssets(l.DependsOn)) + } else { + prefix = fmt.Sprintf( + "Derivative corridor: every path from %s to %s routes through %s, so "+ + "%s has no independent market. Their own integrity was not fully "+ + "measured, so these figures may compound an unmeasured loss. ", + send, recv, describeAssets(l.DependsOn), recv) + } } var body string diff --git a/route/route.go b/route/route.go index 55fca5f..0c5b68a 100644 --- a/route/route.go +++ b/route/route.go @@ -172,6 +172,29 @@ func (i Integrity) Priceable() bool { return i == IntegrityDirect || i == IntegrityDerivative } +// maxDependencyDepth bounds recursive dependency measurement. The Stellar +// protocol caps path length at 5 intermediate hops (XDR Asset path<5>), +// so the real depth can never exceed that. The cap exists to prevent a +// corrupted or expanded registry from turning one corridor request into +// unbounded Horizon fan-out. +const maxDependencyDepth = 5 + +// DependencyNode is one link in a dependency chain. It is a tree, not a +// flat list: a dependency can itself depend on other fiat tokens. +// +// Measured reports whether the node's Integrity was determined by an actual +// Horizon query. When Measured is false, Integrity is the zero value +// (IntegrityUnknown) and Reason explains why measurement did not happen. +// A consumer must never present an unmeasured node as though its integrity +// were a finding. +type DependencyNode struct { + Asset asset.Asset + Integrity Integrity + Measured bool + Reason string + Dependencies []DependencyNode +} + // Kind identifies how a route delivers value. type Kind string @@ -307,6 +330,12 @@ type Result struct { // is IntegrityDerivative. Empty otherwise. DependsOn []asset.Asset + // Chain is the full dependency tree when Integrity is + // IntegrityDerivative. Each node carries the dependency's own measured + // integrity (or an explicit "not measured" state). Nil when Integrity + // is not DERIVATIVE. + Chain []DependencyNode + // Notes record corridor-level facts that no single quote captures. Notes []string } @@ -379,6 +408,7 @@ func (e *Engine) Quote(ctx context.Context, req Request) (*Result, error) { default: res.Integrity = d.integrity res.DependsOn = d.dependsOn + res.Chain = d.chain res.Notes = append(res.Notes, unknownHopNote(d.unknownHops)...) if d.quote != nil { res.Quotes = append(res.Quotes, *d.quote) @@ -393,12 +423,18 @@ func (e *Engine) Quote(ctx context.Context, req Request) (*Result, error) { "This is the absence of a price, not a bad one.", req.SendAsset.Code, req.ReceiveAsset.Code)) case IntegrityDerivative: - res.Notes = append(res.Notes, fmt.Sprintf( - "Derivative corridor: every available path to %s routes through %s. "+ - "There is no independent market, so this corridor carries %s's "+ - "liquidity and failure modes in addition to its own.", - req.ReceiveAsset.Code, describeAssets(res.DependsOn), - describeAssets(res.DependsOn))) + if allMeasured(res.Chain) { + res.Notes = append(res.Notes, fmt.Sprintf( + "Derivative corridor: every available path to %s routes through %s. "+ + "There is no independent market, so this corridor carries their "+ + "liquidity and failure modes in addition to its own.", + req.ReceiveAsset.Code, describeAssets(res.DependsOn))) + } else { + res.Notes = append(res.Notes, fmt.Sprintf( + "Derivative corridor: every available path to %s routes through %s, "+ + "but their own integrity was not fully measured.", + req.ReceiveAsset.Code, describeAssets(res.DependsOn))) + } } sort.SliceStable(res.Quotes, func(i, j int) bool { @@ -450,6 +486,7 @@ func (e *Engine) unscored(ctx context.Context, req Request, ref refrate.Rate) (* if d, err := e.quoteDEX(ctx, req, ref); err == nil { res.Integrity = d.integrity res.DependsOn = d.dependsOn + res.Chain = d.chain res.Notes = append(res.Notes, unknownHopNote(d.unknownHops)...) if d.quote != nil { // Carried for its route description and receive amount; @@ -481,6 +518,7 @@ type dexResult struct { quote *Quote integrity Integrity dependsOn []asset.Asset + chain []DependencyNode // unknownHops are the intermediate assets pathfinding routed through // that are neither native XLM nor registered in the asset registry. They @@ -524,6 +562,37 @@ func unknownHopNote(unknown []asset.Asset) []string { strings.Join(names, ", "))} } +// describeChainStatus renders the measured integrity of each dependency +// for a human-readable warning. +func describeChainStatus(nodes []DependencyNode) string { + parts := make([]string, len(nodes)) + for i, n := range nodes { + switch { + case !n.Measured: + parts[i] = fmt.Sprintf("%s (not measured: %s)", n.Asset.Code, n.Reason) + case n.Integrity == IntegrityDirect: + parts[i] = fmt.Sprintf("%s (DIRECT, independent market exists)", n.Asset.Code) + case n.Integrity == IntegrityDerivative: + parts[i] = fmt.Sprintf("%s (DERIVATIVE, depends on %s)", + n.Asset.Code, describeAssets(fiatAssets(n.Dependencies))) + case n.Integrity == IntegrityNoMarket: + parts[i] = fmt.Sprintf("%s (NO-MARKET)", n.Asset.Code) + default: + parts[i] = fmt.Sprintf("%s (UNKNOWN)", n.Asset.Code) + } + } + return strings.Join(parts, ", ") +} + +// fiatAssets extracts the asset from each DependencyNode. +func fiatAssets(nodes []DependencyNode) []asset.Asset { + out := make([]asset.Asset, len(nodes)) + for i, n := range nodes { + out[i] = n.Asset + } + return out +} + // classify determines corridor integrity from the complete set of paths. // // The claim "reachable only through another fiat token" is about every path, @@ -588,6 +657,124 @@ func classify(paths []dex.Path, dest asset.Asset) (Integrity, []asset.Asset, []a return IntegrityDerivative, deps, unknownList } +// measureChain recursively determines the integrity of each fiat dependency. +// +// For every dependency returned by classify, it queries Horizon for paths +// from sendAsset to that dependency, classifies those paths, and recurses +// into any newly discovered fiat intermediaries — building a tree whose +// depth reflects how many layers of fiat-to-fiat routing exist. +// +// ancestors names the assets already on the path from the corridor's +// destination down to the node currently being expanded. Each dependency +// branch works from its own copy, so a dependency shared between siblings is +// measured once per branch rather than mislabelled as a cycle — only a node +// already on the current root-to-leaf path is a true cycle, and the second +// encounter of one reports the link as unmeasured. The depth cap +// (maxDependencyDepth) prevents unbounded fan-out from a corrupted registry. +// +// Each Horizon call is one StrictSendPaths round trip. Per node the cost is +// bounded by len(deps) × maxDependencyDepth calls, and sharing a dependency +// between branches re-measures it rather than looping; with the current +// registry (4 fiat tokens) and protocol cap (5 hops) a corridor request +// stays within a handful of extra calls. +func (e *Engine) measureChain( + ctx context.Context, + sendAsset asset.Asset, + sendAmount decimal.Decimal, + deps []asset.Asset, + ancestors map[string]bool, + depth int, +) []DependencyNode { + if depth >= maxDependencyDepth { + nodes := make([]DependencyNode, len(deps)) + for i, d := range deps { + nodes[i] = DependencyNode{ + Asset: d, + Reason: "depth cap exceeded", + Measured: false, + } + } + return nodes + } + + nodes := make([]DependencyNode, 0, len(deps)) + for _, dep := range deps { + key := dep.Code + ":" + dep.Issuer + + // Each branch copies the ancestor path before adding itself, so + // siblings never see each other's progress. The cycle check is + // therefore "is this dependency already on the path from the + // destination to here", which is the only true cycle. + path := make(map[string]bool, len(ancestors)+1) + for k := range ancestors { + path[k] = true + } + if path[key] { + nodes = append(nodes, DependencyNode{ + Asset: dep, + Reason: "cycle detected", + Measured: false, + }) + continue + } + path[key] = true + + depPaths, err := e.DEX.StrictSendPaths(ctx, sendAsset, sendAmount, dep) + if err != nil { + nodes = append(nodes, DependencyNode{ + Asset: dep, + Reason: fmt.Sprintf("Horizon error: %v", err), + Measured: false, + }) + continue + } + + depIntegrity, depFiatHops, _ := classify(depPaths, dep) + node := DependencyNode{ + Asset: dep, + Integrity: depIntegrity, + Measured: true, + } + + if depIntegrity == IntegrityDerivative && len(depFiatHops) > 0 { + node.Dependencies = e.measureChain( + ctx, sendAsset, sendAmount, depFiatHops, path, depth+1) + } + + nodes = append(nodes, node) + } + + sort.Slice(nodes, func(i, j int) bool { + return nodes[i].Asset.Code < nodes[j].Asset.Code + }) + return nodes +} + +// chainDepth returns the maximum depth of a dependency tree. +func chainDepth(nodes []DependencyNode) int { + max := 0 + for _, n := range nodes { + d := chainDepth(n.Dependencies) + if d+1 > max { + max = d + 1 + } + } + return max +} + +// allMeasured reports whether every node in the tree was actually measured. +func allMeasured(nodes []DependencyNode) bool { + for _, n := range nodes { + if !n.Measured { + return false + } + if !allMeasured(n.Dependencies) { + return false + } + } + return true +} + // quoteDEX prices the on-chain leg via Horizon pathfinding, and classifies // the corridor's structure from the same set of paths. func (e *Engine) quoteDEX(ctx context.Context, req Request, ref refrate.Rate) (*dexResult, error) { @@ -601,6 +788,14 @@ func (e *Engine) quoteDEX(ctx context.Context, req Request, ref refrate.Rate) (* return &dexResult{integrity: integrity}, nil } + var chain []DependencyNode + if integrity == IntegrityDerivative { + visited := map[string]bool{ + req.ReceiveAsset.Code + ":" + req.ReceiveAsset.Issuer: true, + } + chain = e.measureChain(ctx, req.SendAsset, req.SendAmount, dependsOn, visited, 0) + } + // Horizon generally returns its best path first, but that is not a // documented guarantee, so the maximum is selected explicitly. best := paths[0] @@ -640,10 +835,18 @@ func (e *Engine) quoteDEX(ctx context.Context, req Request, ref refrate.Rate) (* // quote as well as the result means a caller rendering a single route // cannot present the number without the dependency attached to it. if integrity == IntegrityDerivative { - q.Warnings = append(q.Warnings, fmt.Sprintf( - "derivative corridor: every path routes through %s, so this rate "+ - "compounds %s's loss with this corridor's own", - describeAssets(dependsOn), describeAssets(dependsOn))) + if allMeasured(chain) { + q.Warnings = append(q.Warnings, fmt.Sprintf( + "derivative corridor: every path routes through %s (measured: %s), "+ + "so this corridor inherits their liquidity and failure modes", + describeAssets(dependsOn), describeChainStatus(chain))) + } else { + q.Warnings = append(q.Warnings, fmt.Sprintf( + "derivative corridor: every path routes through %s, but their "+ + "own integrity was not fully measured — this rate may compound "+ + "an unmeasured loss", + describeAssets(dependsOn))) + } } probe := e.ProbeAmount @@ -659,5 +862,5 @@ func (e *Engine) quoteDEX(ctx context.Context, req Request, ref refrate.Rate) (* } } } - return &dexResult{quote: q, integrity: integrity, dependsOn: dependsOn, unknownHops: unknownHops}, nil + return &dexResult{quote: q, integrity: integrity, dependsOn: dependsOn, chain: chain, unknownHops: unknownHops}, nil } diff --git a/route/route_test.go b/route/route_test.go index bc0e849..34560bf 100644 --- a/route/route_test.go +++ b/route/route_test.go @@ -763,3 +763,70 @@ func TestUnknownOnlyPathIsTheDocumentedFalseNegative(t *testing.T) { t.Errorf("expected the note to surface BLND, got notes: %v", res.Notes) } } + +// TestAllMeasuredAndChainDepth are unit tests for the helper functions. +func TestAllMeasuredAndChainDepth(t *testing.T) { + t.Run("all measured", func(t *testing.T) { + nodes := []DependencyNode{ + {Asset: asset.NGNC(), Measured: true, Integrity: IntegrityDirect}, + } + if !allMeasured(nodes) { + t.Error("expected all measured") + } + if chainDepth(nodes) != 1 { + t.Errorf("depth = %d, want 1", chainDepth(nodes)) + } + }) + + t.Run("unmeasured node", func(t *testing.T) { + nodes := []DependencyNode{ + {Asset: asset.NGNC(), Measured: false, Reason: "cycle detected"}, + } + if allMeasured(nodes) { + t.Error("should not be all measured") + } + }) + + t.Run("nested depth", func(t *testing.T) { + nodes := []DependencyNode{ + { + Asset: asset.GHSC(), + Measured: true, + Integrity: IntegrityDerivative, + Dependencies: []DependencyNode{ + {Asset: asset.NGNC(), Measured: true, Integrity: IntegrityDirect}, + }, + }, + } + if !allMeasured(nodes) { + t.Error("expected all measured") + } + if chainDepth(nodes) != 2 { + t.Errorf("depth = %d, want 2", chainDepth(nodes)) + } + }) + + t.Run("empty", func(t *testing.T) { + if !allMeasured(nil) { + t.Error("nil should be all measured") + } + if chainDepth(nil) != 0 { + t.Errorf("depth = %d, want 0", chainDepth(nil)) + } + }) +} + +// TestDescribeChainStatus verifies the human-readable chain status rendering. +func TestDescribeChainStatus(t *testing.T) { + nodes := []DependencyNode{ + {Asset: asset.NGNC(), Measured: true, Integrity: IntegrityDirect}, + {Asset: asset.KESC(), Measured: false, Reason: "Horizon error: timeout"}, + } + got := describeChainStatus(nodes) + if !strings.Contains(got, "NGNC (DIRECT, independent market exists)") { + t.Errorf("expected NGNC DIRECT status, got: %s", got) + } + if !strings.Contains(got, "KESC (not measured: Horizon error: timeout)") { + t.Errorf("expected KESC unmeasured status, got: %s", got) + } +} diff --git a/route/wire.go b/route/wire.go index f8bf44d..d38021c 100644 --- a/route/wire.go +++ b/route/wire.go @@ -80,6 +80,12 @@ type CorridorJSON struct { Integrity string `json:"integrity"` DependsOn []AssetJSON `json:"depends_on"` + // DependencyChain is the full tree of dependencies when the corridor + // is derivative. Nil when Integrity is not DERIVATIVE. Each node + // carries the dependency's measured integrity or an explicit "not + // measured" state with a reason. + DependencyChain *DependencyChainJSON `json:"dependency_chain,omitempty"` + ReferenceMid string `json:"reference_mid"` ReferenceSource string `json:"reference_source"` ReferencePair string `json:"reference_pair"` @@ -184,6 +190,32 @@ type AssetJSON struct { Asset string `json:"asset,omitempty"` } +// DependencyChainJSON is the wire representation of a dependency tree. +// It is present only when the corridor is derivative (integrity = "DERIVATIVE"). +// Nil on the wire means the corridor is direct or has no market — never an +// empty object. +type DependencyChainJSON struct { + Depth int `json:"depth"` + DependsOn []DependencyNodeJSON `json:"depends_on"` +} + +// DependencyNodeJSON is one link in a dependency chain on the wire. +// +// Measured is always present. When false, Integrity carries no meaningful +// value (it will be "UNKNOWN") and Reason explains why: cycle detected, +// depth cap exceeded, Horizon error, or context cancelled. A consumer +// rendering this node must not present it as though its integrity were a +// finding — an unmeasured link has no finding to present. +type DependencyNodeJSON struct { + Code string `json:"code"` + Issuer string `json:"issuer,omitempty"` + Peg string `json:"peg,omitempty"` + Integrity string `json:"integrity"` + Measured bool `json:"measured"` + Reason string `json:"reason,omitempty"` + Dependencies []DependencyNodeJSON `json:"dependencies,omitempty"` +} + func ToAssetJSON(a asset.Asset) AssetJSON { j := AssetJSON{Code: a.Code, Issuer: a.Issuer} if a.Identifiable() { @@ -195,6 +227,46 @@ func ToAssetJSON(a asset.Asset) AssetJSON { return j } +// ToDependencyChainJSON renders a dependency tree for the wire. +// +// Nil input produces nil output (omitted from JSON via omitempty). An empty +// slice produces a chain with depth 0 and an empty depends_on array, which +// is structurally valid but semantically should not occur — a derivative +// corridor always has at least one dependency. +func ToDependencyChainJSON(nodes []DependencyNode) *DependencyChainJSON { + if len(nodes) == 0 { + return nil + } + out := &DependencyChainJSON{ + Depth: chainDepth(nodes), + DependsOn: make([]DependencyNodeJSON, 0, len(nodes)), + } + for _, n := range nodes { + out.DependsOn = append(out.DependsOn, toDependencyNodeJSON(n)) + } + return out +} + +func toDependencyNodeJSON(n DependencyNode) DependencyNodeJSON { + j := DependencyNodeJSON{ + Code: n.Asset.Code, + Issuer: n.Asset.Issuer, + Integrity: n.Integrity.String(), + Measured: n.Measured, + Reason: n.Reason, + } + if peg, ok := asset.FiatPeg(n.Asset); ok { + j.Peg = peg + } + if len(n.Dependencies) > 0 { + j.Dependencies = make([]DependencyNodeJSON, 0, len(n.Dependencies)) + for _, d := range n.Dependencies { + j.Dependencies = append(j.Dependencies, toDependencyNodeJSON(d)) + } + } + return j +} + func ToQuoteJSON(q *Quote) *QuoteJSON { if q == nil { return nil @@ -292,6 +364,7 @@ func ToCorridorJSON(l *LadderResult, pair string) CorridorJSON { for _, d := range l.DependsOn { out.DependsOn = append(out.DependsOn, ToAssetJSON(d)) } + out.DependencyChain = ToDependencyChainJSON(l.Chain) for _, r := range l.Rungs { rj := RungJSON{ diff --git a/runstore/convert.go b/runstore/convert.go index ae74bf6..0f4e10d 100644 --- a/runstore/convert.go +++ b/runstore/convert.go @@ -68,6 +68,9 @@ func FromCorridorJSON(c route.CorridorJSON) *Record { for _, d := range c.DependsOn { r.DependsOn = append(r.DependsOn, d.Code) } + if c.DependencyChain != nil { + r.DependencyChain = c.DependencyChain + } for _, rung := range c.Rungs { out := Rung{ diff --git a/runstore/runstore.go b/runstore/runstore.go index fc9deed..e422539 100644 --- a/runstore/runstore.go +++ b/runstore/runstore.go @@ -38,6 +38,7 @@ import ( "time" "github.com/Wayfare-labs/wayfare/checks" + "github.com/Wayfare-labs/wayfare/route" ) // Version is the record schema version. @@ -160,6 +161,13 @@ type Record struct { Checks []checks.CheckJSON `json:"checks,omitempty"` Metrics []checks.MetricJSON `json:"metrics,omitempty"` + // DependencyChain is the full dependency tree, stored word-for-word + // from the wire so the stale path can serve it back identically. + // Absent (nil) for corridors that are not derivative, and absent for + // records written before this field existed — the stale path treats + // nil as "chain not available", which is honest rather than fabricated. + DependencyChain *route.DependencyChainJSON `json:"dependency_chain,omitempty"` + PrevHash string `json:"prev_hash"` Hash string `json:"hash"` } diff --git a/server/api.go b/server/api.go index d9c51c6..1b1ee2f 100644 --- a/server/api.go +++ b/server/api.go @@ -454,6 +454,9 @@ func staleJSON(rec *runstore.Record, pair string, now time.Time) route.CorridorJ for _, code := range rec.DependsOn { out.DependsOn = append(out.DependsOn, route.AssetJSON{Code: code}) } + if rec.DependencyChain != nil { + out.DependencyChain = rec.DependencyChain + } for _, r := range rec.Rungs { rj := route.RungJSON{ SendAmount: r.SendAmount, diff --git a/server/index.html b/server/index.html index 7ebb14c..21cc23c 100644 --- a/server/index.html +++ b/server/index.html @@ -173,6 +173,41 @@ font-size: .85rem; color: var(--muted); border-left: 3px solid var(--border); padding-left: .7rem; } + @media (max-width: 640px) { + html, body { overflow-x: hidden; } + .wrap { padding: 1.5rem 1rem 3rem; max-width: 100%; } + .controls select { flex: 1 1 0; min-height: 44px; font-size: 1rem; } + .controls button { flex: 1 1 calc(50% - .3rem); min-height: 44px; font-size: 1rem; } + .panel { padding: .9rem 1rem; } + .scroll table { border: 0; } + .scroll table thead { + clip: rect(0 0 0 0); height: 1px; margin: -1px; + overflow: hidden; position: absolute; width: 1px; + } + .scroll table tr { + display: block; margin-bottom: .75rem; + border: 1px solid var(--border); border-radius: 8px; + padding: .6rem .7rem; background: var(--panel); + } + .scroll table td { + display: flex; justify-content: space-between; align-items: baseline; + padding: .3rem .15rem; border-bottom: 1px solid var(--grid); + text-align: right; font-size: .85rem; + } + .scroll table td:last-child { border-bottom: 0; } + .scroll table td::before { + content: attr(data-label); font-weight: 550; text-align: left; + color: var(--muted); font-size: .72rem; text-transform: uppercase; + letter-spacing: .04em; flex-shrink: 0; margin-right: .75rem; + } + .scroll table td:first-child { + display: block; text-align: left; font-weight: 600; font-size: .95rem; + border-bottom: 1px solid var(--grid); padding-bottom: .45rem; margin-bottom: .1rem; + } + .legend-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .finding-row { flex-wrap: wrap; gap: .4rem; } + .f-state { min-width: auto; } + } @@ -370,33 +405,33 @@

Wayfare

: r.integrity === 'NO-MARKET' ? 'no path exists at this size' : 'not priced'; if (scored) { - return `${esc(r.send_amount)} - ${why} - ${esc(r.integrity)}`; + return `${esc(r.send_amount)} + ${why} + ${esc(r.integrity)}`; } - return `${esc(r.send_amount)} - ${why} - ${esc(r.integrity)}`; + return `${esc(r.send_amount)} + ${why} + ${esc(r.integrity)}`; } const q = r.quote; if (scored) { return ` - ${esc(r.send_amount)} - ${esc(q.receive_amount)} - ${esc(q.effective_rate)} - ${formatPct(q.loss_pct)}% - ${esc(q.verdict)} - ${esc(q.description)} + ${esc(r.send_amount)} + ${esc(q.receive_amount)} + ${esc(q.effective_rate)} + ${formatPct(q.loss_pct)}% + ${esc(q.verdict)} + ${esc(q.description)} `; } // When not scored, show structural facts only: send, receive, rate, path. // Loss and verdict are omitted — they are derived from a benchmark the // engine refused to stand behind. return ` - ${esc(r.send_amount)} - ${esc(q.receive_amount)} - ${esc(q.effective_rate)} - ${esc(q.description)} + ${esc(r.send_amount)} + ${esc(q.receive_amount)} + ${esc(q.effective_rate)} + ${esc(q.description)} `; }).join(''); @@ -835,12 +870,12 @@

Stored runs

const div = ref.divergence_pct ? ` div ${esc(ref.divergence_pct)}%` : ''; return ` - ${esc(r.recorded_at)} - ${r.seq} - ${esc(r.integrity)}${via} - ${esc(r.floor_loss_pct)}% - ${esc(r.worst_loss_pct)}% - ${esc(src)}${div} + ${esc(r.recorded_at)} + ${r.seq} + ${esc(r.integrity)}${via} + ${esc(r.floor_loss_pct)}% + ${esc(r.worst_loss_pct)}% + ${esc(src)}${div} `; }).join(''); diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-cycle-20260902T123516Z/manifest.json b/testdata/chain-snapshots/usdc-ghsc-chain-cycle-20260902T123516Z/manifest.json new file mode 100644 index 0000000..2a3c2f8 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-cycle-20260902T123516Z/manifest.json @@ -0,0 +1,66 @@ +{ + "format": "wayfare.snapshot", + "version": 1, + "recorded_at": "2026-09-02T12:35:16Z", + "corridor": { + "send": { + "code": "USDC", + "issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" + }, + "receive": { + "code": "GHSC", + "issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" + }, + "reference_pair": "USD/GHS" + }, + "sizes": [ + "100" + ], + "sources": { + "horizon": { + "provider": "synthetic", + "base_url": "https://horizon.stellar.org" + }, + "reference": { + "base_url": "static" + } + }, + "notes": [ + "scenario fixture: captured from a synthetic upstream for the dependency-chain cases that are not present on the recorded mainnet set; see route/chain_dependency_test.go" + ], + "interactions": [ + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/001-paths-strict-send-100.json", + "body_sha256": "sha256:e9d6aff68a73de0081c653812eab6962f6195212b263f0a8069586b9be5df85a" + }, + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=NGNC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=NGNC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/002-paths-strict-send-100.json", + "body_sha256": "sha256:864c1e7a0e342a5942f099a3f1a7c0713712b0c202b0e1498779e48de46a6a41" + }, + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=10\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=10\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/003-paths-strict-send-10.json", + "body_sha256": "sha256:e9d6aff68a73de0081c653812eab6962f6195212b263f0a8069586b9be5df85a" + } + ] +} diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-cycle-20260902T123516Z/responses/001-paths-strict-send-100.json b/testdata/chain-snapshots/usdc-ghsc-chain-cycle-20260902T123516Z/responses/001-paths-strict-send-100.json new file mode 100644 index 0000000..afc01f8 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-cycle-20260902T123516Z/responses/001-paths-strict-send-100.json @@ -0,0 +1,31 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "GHSC", + "destination_amount": "155.5600000", + "path": [ + { "asset_type": "native" }, + { "asset_type": "credit_alphanum4", "asset_code": "NGNC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + }, + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "GHSC", + "destination_amount": "150.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "NGNC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-cycle-20260902T123516Z/responses/002-paths-strict-send-100.json b/testdata/chain-snapshots/usdc-ghsc-chain-cycle-20260902T123516Z/responses/002-paths-strict-send-100.json new file mode 100644 index 0000000..f24e393 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-cycle-20260902T123516Z/responses/002-paths-strict-send-100.json @@ -0,0 +1,18 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "NGNC", + "destination_amount": "100.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "GHSC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-cycle-20260902T123516Z/responses/003-paths-strict-send-10.json b/testdata/chain-snapshots/usdc-ghsc-chain-cycle-20260902T123516Z/responses/003-paths-strict-send-10.json new file mode 100644 index 0000000..afc01f8 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-cycle-20260902T123516Z/responses/003-paths-strict-send-10.json @@ -0,0 +1,31 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "GHSC", + "destination_amount": "155.5600000", + "path": [ + { "asset_type": "native" }, + { "asset_type": "credit_alphanum4", "asset_code": "NGNC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + }, + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "GHSC", + "destination_amount": "150.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "NGNC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/manifest.json b/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/manifest.json new file mode 100644 index 0000000..a9da5b0 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/manifest.json @@ -0,0 +1,77 @@ +{ + "format": "wayfare.snapshot", + "version": 1, + "recorded_at": "2026-09-02T12:35:16Z", + "corridor": { + "send": { + "code": "USDC", + "issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" + }, + "receive": { + "code": "GHSC", + "issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" + }, + "reference_pair": "USD/GHS" + }, + "sizes": [ + "100" + ], + "sources": { + "horizon": { + "provider": "synthetic", + "base_url": "https://horizon.stellar.org" + }, + "reference": { + "base_url": "static" + } + }, + "notes": [ + "scenario fixture: captured from a synthetic upstream for the dependency-chain cases that are not present on the recorded mainnet set; see route/chain_dependency_test.go" + ], + "interactions": [ + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/001-paths-strict-send-100.json", + "body_sha256": "sha256:b4a7e5eeafaf8102970383555e59252891d9fb5efe639a2123863f191bcb407f" + }, + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=KESC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=KESC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/002-paths-strict-send-100.json", + "body_sha256": "sha256:2a4336bba6b72283995b99ad2fbf15da43d668cc5b79abeb8518a22b9b8f353a" + }, + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=NGNC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=NGNC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/003-paths-strict-send-100.json", + "body_sha256": "sha256:12170ad5459b6c1d59ff4c63883df804caaa8291b0abf2a592f1336fb5b28311" + }, + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=10\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=10\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/004-paths-strict-send-10.json", + "body_sha256": "sha256:b4a7e5eeafaf8102970383555e59252891d9fb5efe639a2123863f191bcb407f" + } + ] +} diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/responses/001-paths-strict-send-100.json b/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/responses/001-paths-strict-send-100.json new file mode 100644 index 0000000..b87dd72 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/responses/001-paths-strict-send-100.json @@ -0,0 +1,18 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "GHSC", + "destination_amount": "100.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "KESC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/responses/002-paths-strict-send-100.json b/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/responses/002-paths-strict-send-100.json new file mode 100644 index 0000000..aa6ab84 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/responses/002-paths-strict-send-100.json @@ -0,0 +1,18 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "KESC", + "destination_amount": "100.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "NGNC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/responses/003-paths-strict-send-100.json b/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/responses/003-paths-strict-send-100.json new file mode 100644 index 0000000..57ccf15 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/responses/003-paths-strict-send-100.json @@ -0,0 +1,24 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "NGNC", + "destination_amount": "65100.1379550", + "path": [ { "asset_type": "native" } ] + }, + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "NGNC", + "destination_amount": "21785.7821141", + "path": [] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/responses/004-paths-strict-send-10.json b/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/responses/004-paths-strict-send-10.json new file mode 100644 index 0000000..b87dd72 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-depth-two-20260902T123516Z/responses/004-paths-strict-send-10.json @@ -0,0 +1,18 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "GHSC", + "destination_amount": "100.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "KESC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-direct-ngnc-20260902T123516Z/manifest.json b/testdata/chain-snapshots/usdc-ghsc-chain-direct-ngnc-20260902T123516Z/manifest.json new file mode 100644 index 0000000..25d5b02 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-direct-ngnc-20260902T123516Z/manifest.json @@ -0,0 +1,66 @@ +{ + "format": "wayfare.snapshot", + "version": 1, + "recorded_at": "2026-09-02T12:35:16Z", + "corridor": { + "send": { + "code": "USDC", + "issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" + }, + "receive": { + "code": "GHSC", + "issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" + }, + "reference_pair": "USD/GHS" + }, + "sizes": [ + "100" + ], + "sources": { + "horizon": { + "provider": "synthetic", + "base_url": "https://horizon.stellar.org" + }, + "reference": { + "base_url": "static" + } + }, + "notes": [ + "scenario fixture: captured from a synthetic upstream for the dependency-chain cases that are not present on the recorded mainnet set; see route/chain_dependency_test.go" + ], + "interactions": [ + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/001-paths-strict-send-100.json", + "body_sha256": "sha256:e9d6aff68a73de0081c653812eab6962f6195212b263f0a8069586b9be5df85a" + }, + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=NGNC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=NGNC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/002-paths-strict-send-100.json", + "body_sha256": "sha256:12170ad5459b6c1d59ff4c63883df804caaa8291b0abf2a592f1336fb5b28311" + }, + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=10\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=10\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/003-paths-strict-send-10.json", + "body_sha256": "sha256:e9d6aff68a73de0081c653812eab6962f6195212b263f0a8069586b9be5df85a" + } + ] +} diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-direct-ngnc-20260902T123516Z/responses/001-paths-strict-send-100.json b/testdata/chain-snapshots/usdc-ghsc-chain-direct-ngnc-20260902T123516Z/responses/001-paths-strict-send-100.json new file mode 100644 index 0000000..afc01f8 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-direct-ngnc-20260902T123516Z/responses/001-paths-strict-send-100.json @@ -0,0 +1,31 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "GHSC", + "destination_amount": "155.5600000", + "path": [ + { "asset_type": "native" }, + { "asset_type": "credit_alphanum4", "asset_code": "NGNC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + }, + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "GHSC", + "destination_amount": "150.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "NGNC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-direct-ngnc-20260902T123516Z/responses/002-paths-strict-send-100.json b/testdata/chain-snapshots/usdc-ghsc-chain-direct-ngnc-20260902T123516Z/responses/002-paths-strict-send-100.json new file mode 100644 index 0000000..57ccf15 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-direct-ngnc-20260902T123516Z/responses/002-paths-strict-send-100.json @@ -0,0 +1,24 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "NGNC", + "destination_amount": "65100.1379550", + "path": [ { "asset_type": "native" } ] + }, + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "NGNC", + "destination_amount": "21785.7821141", + "path": [] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-direct-ngnc-20260902T123516Z/responses/003-paths-strict-send-10.json b/testdata/chain-snapshots/usdc-ghsc-chain-direct-ngnc-20260902T123516Z/responses/003-paths-strict-send-10.json new file mode 100644 index 0000000..afc01f8 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-direct-ngnc-20260902T123516Z/responses/003-paths-strict-send-10.json @@ -0,0 +1,31 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "GHSC", + "destination_amount": "155.5600000", + "path": [ + { "asset_type": "native" }, + { "asset_type": "credit_alphanum4", "asset_code": "NGNC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + }, + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "GHSC", + "destination_amount": "150.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "NGNC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-no-market-20260902T123516Z/manifest.json b/testdata/chain-snapshots/usdc-ghsc-chain-no-market-20260902T123516Z/manifest.json new file mode 100644 index 0000000..c1a661a --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-no-market-20260902T123516Z/manifest.json @@ -0,0 +1,66 @@ +{ + "format": "wayfare.snapshot", + "version": 1, + "recorded_at": "2026-09-02T12:35:16Z", + "corridor": { + "send": { + "code": "USDC", + "issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" + }, + "receive": { + "code": "GHSC", + "issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" + }, + "reference_pair": "USD/GHS" + }, + "sizes": [ + "100" + ], + "sources": { + "horizon": { + "provider": "synthetic", + "base_url": "https://horizon.stellar.org" + }, + "reference": { + "base_url": "static" + } + }, + "notes": [ + "scenario fixture: captured from a synthetic upstream for the dependency-chain cases that are not present on the recorded mainnet set; see route/chain_dependency_test.go" + ], + "interactions": [ + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/001-paths-strict-send-100.json", + "body_sha256": "sha256:b4a7e5eeafaf8102970383555e59252891d9fb5efe639a2123863f191bcb407f" + }, + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=KESC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=KESC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/002-paths-strict-send-100.json", + "body_sha256": "sha256:cb8391a385f66a1a9eecc2ab0110eccf561aa565c48f8c709b897fc1e4fe230d" + }, + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=10\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=10\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/003-paths-strict-send-10.json", + "body_sha256": "sha256:b4a7e5eeafaf8102970383555e59252891d9fb5efe639a2123863f191bcb407f" + } + ] +} diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-no-market-20260902T123516Z/responses/001-paths-strict-send-100.json b/testdata/chain-snapshots/usdc-ghsc-chain-no-market-20260902T123516Z/responses/001-paths-strict-send-100.json new file mode 100644 index 0000000..b87dd72 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-no-market-20260902T123516Z/responses/001-paths-strict-send-100.json @@ -0,0 +1,18 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "GHSC", + "destination_amount": "100.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "KESC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-no-market-20260902T123516Z/responses/002-paths-strict-send-100.json b/testdata/chain-snapshots/usdc-ghsc-chain-no-market-20260902T123516Z/responses/002-paths-strict-send-100.json new file mode 100644 index 0000000..b0f2e8b --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-no-market-20260902T123516Z/responses/002-paths-strict-send-100.json @@ -0,0 +1 @@ +{"_embedded":{"records":[]}} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-no-market-20260902T123516Z/responses/003-paths-strict-send-10.json b/testdata/chain-snapshots/usdc-ghsc-chain-no-market-20260902T123516Z/responses/003-paths-strict-send-10.json new file mode 100644 index 0000000..b87dd72 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-no-market-20260902T123516Z/responses/003-paths-strict-send-10.json @@ -0,0 +1,18 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "GHSC", + "destination_amount": "100.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "KESC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/manifest.json b/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/manifest.json new file mode 100644 index 0000000..da9a1c0 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/manifest.json @@ -0,0 +1,77 @@ +{ + "format": "wayfare.snapshot", + "version": 1, + "recorded_at": "2026-09-02T12:38:45Z", + "corridor": { + "send": { + "code": "USDC", + "issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" + }, + "receive": { + "code": "GHSC", + "issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" + }, + "reference_pair": "USD/GHS" + }, + "sizes": [ + "100" + ], + "sources": { + "horizon": { + "provider": "synthetic", + "base_url": "https://horizon.stellar.org" + }, + "reference": { + "base_url": "static" + } + }, + "notes": [ + "scenario fixture: captured from a synthetic upstream for the dependency-chain cases that are not present on the recorded mainnet set; see route/chain_dependency_test.go" + ], + "interactions": [ + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:38:45Z", + "body_file": "responses/001-paths-strict-send-100.json", + "body_sha256": "sha256:280a05e5442b2d3931527154e89fe18f32482353bea32ceda6dd2a761d535fde" + }, + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=KESC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=KESC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:38:45Z", + "body_file": "responses/002-paths-strict-send-100.json", + "body_sha256": "sha256:2a4336bba6b72283995b99ad2fbf15da43d668cc5b79abeb8518a22b9b8f353a" + }, + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=NGNC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=NGNC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:38:45Z", + "body_file": "responses/003-paths-strict-send-100.json", + "body_sha256": "sha256:12170ad5459b6c1d59ff4c63883df804caaa8291b0abf2a592f1336fb5b28311" + }, + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=10\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=GHSC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=10\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:38:45Z", + "body_file": "responses/004-paths-strict-send-10.json", + "body_sha256": "sha256:280a05e5442b2d3931527154e89fe18f32482353bea32ceda6dd2a761d535fde" + } + ] +} diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/responses/001-paths-strict-send-100.json b/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/responses/001-paths-strict-send-100.json new file mode 100644 index 0000000..7ad637a --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/responses/001-paths-strict-send-100.json @@ -0,0 +1,26 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", "destination_asset_code": "GHSC", + "destination_amount": "100.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "KESC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + }, + { + "source_asset_type": "credit_alphanum4", "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", "destination_asset_code": "GHSC", + "destination_amount": "90.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "NGNC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/responses/002-paths-strict-send-100.json b/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/responses/002-paths-strict-send-100.json new file mode 100644 index 0000000..aa6ab84 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/responses/002-paths-strict-send-100.json @@ -0,0 +1,18 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "KESC", + "destination_amount": "100.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "NGNC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/responses/003-paths-strict-send-100.json b/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/responses/003-paths-strict-send-100.json new file mode 100644 index 0000000..57ccf15 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/responses/003-paths-strict-send-100.json @@ -0,0 +1,24 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "NGNC", + "destination_amount": "65100.1379550", + "path": [ { "asset_type": "native" } ] + }, + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "NGNC", + "destination_amount": "21785.7821141", + "path": [] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/responses/004-paths-strict-send-10.json b/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/responses/004-paths-strict-send-10.json new file mode 100644 index 0000000..7ad637a --- /dev/null +++ b/testdata/chain-snapshots/usdc-ghsc-chain-shared-20260902T123845Z/responses/004-paths-strict-send-10.json @@ -0,0 +1,26 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", "destination_asset_code": "GHSC", + "destination_amount": "100.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "KESC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + }, + { + "source_asset_type": "credit_alphanum4", "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", "destination_asset_code": "GHSC", + "destination_amount": "90.0000000", + "path": [ + { "asset_type": "credit_alphanum4", "asset_code": "NGNC", + "asset_issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" } + ] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ngnc-chain-direct-20260902T123516Z/manifest.json b/testdata/chain-snapshots/usdc-ngnc-chain-direct-20260902T123516Z/manifest.json new file mode 100644 index 0000000..490a996 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ngnc-chain-direct-20260902T123516Z/manifest.json @@ -0,0 +1,55 @@ +{ + "format": "wayfare.snapshot", + "version": 1, + "recorded_at": "2026-09-02T12:35:16Z", + "corridor": { + "send": { + "code": "USDC", + "issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" + }, + "receive": { + "code": "NGNC", + "issuer": "GASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6" + }, + "reference_pair": "USD/NGN" + }, + "sizes": [ + "100" + ], + "sources": { + "horizon": { + "provider": "synthetic", + "base_url": "https://horizon.stellar.org" + }, + "reference": { + "base_url": "static" + } + }, + "notes": [ + "scenario fixture: captured from a synthetic upstream for the dependency-chain cases that are not present on the recorded mainnet set; see route/chain_dependency_test.go" + ], + "interactions": [ + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=NGNC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=NGNC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=100\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/001-paths-strict-send-100.json", + "body_sha256": "sha256:12170ad5459b6c1d59ff4c63883df804caaa8291b0abf2a592f1336fb5b28311" + }, + { + "kind": "horizon", + "method": "GET", + "key": "GET /paths/strict-send?destination_assets=NGNC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=10\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "url": "https://horizon.stellar.org/paths/strict-send?destination_assets=NGNC%3AGASBV6W7GGED66MXEVC7YZHTWWYMSVYEY35USF2HJZBLABLYIFQGXZY6\u0026source_amount=10\u0026source_asset_code=USDC\u0026source_asset_issuer=GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN\u0026source_asset_type=credit_alphanum4", + "status": 200, + "content_type": "application/json", + "recorded_at": "2026-09-02T12:35:16Z", + "body_file": "responses/002-paths-strict-send-10.json", + "body_sha256": "sha256:12170ad5459b6c1d59ff4c63883df804caaa8291b0abf2a592f1336fb5b28311" + } + ] +} diff --git a/testdata/chain-snapshots/usdc-ngnc-chain-direct-20260902T123516Z/responses/001-paths-strict-send-100.json b/testdata/chain-snapshots/usdc-ngnc-chain-direct-20260902T123516Z/responses/001-paths-strict-send-100.json new file mode 100644 index 0000000..57ccf15 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ngnc-chain-direct-20260902T123516Z/responses/001-paths-strict-send-100.json @@ -0,0 +1,24 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "NGNC", + "destination_amount": "65100.1379550", + "path": [ { "asset_type": "native" } ] + }, + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "NGNC", + "destination_amount": "21785.7821141", + "path": [] + } + ] + } +} \ No newline at end of file diff --git a/testdata/chain-snapshots/usdc-ngnc-chain-direct-20260902T123516Z/responses/002-paths-strict-send-10.json b/testdata/chain-snapshots/usdc-ngnc-chain-direct-20260902T123516Z/responses/002-paths-strict-send-10.json new file mode 100644 index 0000000..57ccf15 --- /dev/null +++ b/testdata/chain-snapshots/usdc-ngnc-chain-direct-20260902T123516Z/responses/002-paths-strict-send-10.json @@ -0,0 +1,24 @@ +{ + "_embedded": { + "records": [ + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "NGNC", + "destination_amount": "65100.1379550", + "path": [ { "asset_type": "native" } ] + }, + { + "source_asset_type": "credit_alphanum4", + "source_asset_code": "USDC", + "source_amount": "100.0000000", + "destination_asset_type": "credit_alphanum4", + "destination_asset_code": "NGNC", + "destination_amount": "21785.7821141", + "path": [] + } + ] + } +} \ No newline at end of file