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