feat(sandbox): first real E2E payment — 16 bugs fixed across 7 services (STA-243)#248
Conversation
WalkthroughAdds sandbox E2E infrastructure and Makefile targets, seeds FX liquidity corridors, standardizes sandbox DB credentials, tightens adapter numeric/serialization behavior, refactors off-ramp payout derivation to currency-based resolvers, and updates tests and env/template for sandbox runtimes. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant PaymentOrch as Payment-Orchestrator
participant Compliance
participant FX
participant OnRamp
participant Custody
participant OffRamp
participant Kafka
participant DB
participant ExternalAPIs as SandboxProviders
Client->>PaymentOrch: initiate payment (S1)
PaymentOrch->>Compliance: submit for KYC (S2)
Compliance-->>PaymentOrch: approval/rejection (S2 result)
PaymentOrch->>FX: request quote (S6)
FX-->>PaymentOrch: quote
PaymentOrch->>OnRamp: initiate fiat collection (S3)
OnRamp->>ExternalAPIs: call provider sandbox (Stripe/Modulr) (webhooks expected)
OnRamp-->>PaymentOrch: collection success
PaymentOrch->>Custody: create blockchain transfer (S4)
Custody->>ExternalAPIs: submit to testnet (Alchemy)
Custody-->>PaymentOrch: tx confirmed
PaymentOrch->>OffRamp: request redemption & payout (S5, S7)
OffRamp->>ExternalAPIs: call Circle / Modulr sandbox
ExternalAPIs-->>OffRamp: webhook/callbacks
PaymentOrch->>Kafka: emit events
PaymentOrch->>DB: persist ledger entries
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/application/controller/GlobalExceptionHandler.java`:
- Line 65: The current log call in GlobalExceptionHandler.handleCustodySigning()
logs the full stack trace (log.error("Custody signing error: {}",
ex.getMessage(), ex)); update it to avoid printing the exception object/stack to
logs—log only the exception class and a safe message (e.g., include
ex.getClass().getSimpleName() and ex.getMessage() or just ex.getMessage()) so no
stack or sensitive crypto details are emitted; remove the throwable parameter
from the log call and ensure no other code in handleCustodySigning() logs the
exception object.
In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/provider/dev/DevCustodyAdapter.java`:
- Around line 105-108: DevCustodyAdapter currently truncates fractional minor
units by using setScale(0, RoundingMode.DOWN) when computing amountMinorUnits,
which diverges from FireblocksCustodyAdapter and can silently drop precision;
change the logic in DevCustodyAdapter around amountMinorUnits/request.amount()
to first validate the BigDecimal scale does not exceed USDC_DECIMALS (e.g., if
request.amount().scale() > USDC_DECIMALS throw an IllegalArgumentException with
a clear message), and then convert using movePointRight(USDC_DECIMALS) followed
by toBigIntegerExact() (or equivalent exact conversion) so any unexpected extra
precision fails fast instead of being truncated.
In `@docker-compose.sandbox.yml`:
- Line 139: Change the APP_EXTERNAL_API_LOGGING_ENABLED environment variable
default from "true" to "false" in docker-compose.sandbox.yml to prevent external
API payloads (PII/secrets) from being logged by default; locate every occurrence
of the APP_EXTERNAL_API_LOGGING_ENABLED key (there are multiple entries in this
file) and update their values to "false", keeping the variable name intact so it
can still be enabled manually for targeted debugging.
- Around line 224-225: Remove the hard-coded Stripe webhook secret from
docker-compose.sandbox.yml (the APP_PSP_STRIPE_WEBHOOK_WEBHOOK_SECRET entry),
rotate the leaked secret in Stripe immediately, and place the new secret into
.env.sandbox (or your secrets manager) instead; update
docker-compose.sandbox.yml to read APP_PSP_STRIPE_WEBHOOK_WEBHOOK_SECRET from
the environment (no literal value), commit the config change, and ensure you do
not re-commit the rotated secret or any .env file containing live credentials.
- Around line 363-367: Replace the fragile startup condition that uses
condition: service_started for the services compliance-travel-rule and
fx-liquidity-engine with a readiness-based check (use condition:
service_healthy) and ensure each target service defines a proper healthcheck; in
other words, update the depends_on/condition entries referencing
compliance-travel-rule and fx-liquidity-engine to service_healthy and add/verify
corresponding healthcheck configurations for those services so the orchestrator
waits for actual health readiness instead of process start.
- Around line 260-261: Remove the hardcoded private key value for
APP_CUSTODY_DEV_EVM_PRIVATE_KEY from docker-compose.sandbox.yml, replace it with
an environment variable reference (so the compose file reads the variable rather
than embedding the key), move the actual secret into .env.sandbox (create/update
.env.sandbox with APP_CUSTODY_DEV_EVM_PRIVATE_KEY=<new_key>), ensure
.env.sandbox is added to .gitignore and not committed, rotate the compromised
key by generating a new sandbox wallet and funding it, and treat the old key as
compromised/invalidated in any relevant configs or key stores.
In
`@fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapter.java`:
- Around line 67-72: In CircleRedemptionAdapter where the CirclePayoutRequest is
constructed (the block using request.payoutId() and request.amount()), add an
explicit precondition check that request.amount() is non-null and greater than
zero before normalization/scale; if the check fails, throw a clear
IllegalArgumentException (or a domain-specific exception) with a descriptive
message (e.g., "amount must be provided and > 0") so you avoid NPEs and invalid
payout requests when building CirclePayoutRequest.CircleAmount.
In
`@fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapterTest.java`:
- Around line 53-55: Update the test in CircleRedemptionAdapterTest to exercise
the normalization/rounding path by adding a case where RedemptionRequest
(constructed in aRedemptionRequest()) uses a value with more than 2 decimal
places (e.g., new BigDecimal("10000.009")); ensure the adapter
serializes/forwards the amount rounded/truncated to "10000.00" by asserting the
outbound JSON contains the string "10000.00" (or equivalent field value) so the
>2-decimal normalization is actually validated.
In
`@fx-liquidity-engine/fx-liquidity-engine/src/main/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJob.java`:
- Around line 23-27: SUPPORTED_CORRIDORS is a hard-coded list in RateRefreshJob
that duplicates corridor data kept in DB migrations; replace this static list
with a single authoritative source by injecting a domain port or repository
(e.g., CorridorRepository or CorridorPort) into RateRefreshJob and use its
method (e.g., findAllSupportedCorridors or loadRefreshCorridors) to fetch
Corridor instances at runtime; remove the SUPPORTED_CORRIDORS constant, update
the RateRefreshJob constructor to accept the repository, call the repository
method where the job currently iterates SUPPORTED_CORRIDORS, and add appropriate
null/empty handling and unit/integration test updates to reflect the new
dependency.
In
`@fx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V6__seed_usd_gbp_corridor.sql`:
- Around line 9-13: Add a second INSERT row to the V6 migration that seeds the
reverse corridor (from_currency='GBP', to_currency='USD') so directional pool
lookups succeed when RateRefreshJob triggers both directions; mirror the
existing VALUES(...) entry (use gen_random_uuid() and the same liquidity
bounds/amounts appropriate for the reverse corridor) and keep the ON CONFLICT
(from_currency, to_currency) DO NOTHING clause so it is idempotent.
In `@Makefile`:
- Around line 241-243: The sandbox-status Makefile target is using the wrong
compose variable ($(COMPOSE)) and thus inspects the dev compose file; update the
sandbox-status recipe to use $(SANDBOX_COMPOSE) instead of $(COMPOSE) so it runs
the sandbox stack (keep the same ps flags and fallback behavior). Specifically
edit the sandbox-status target to call @$(SANDBOX_COMPOSE) ps --format "table
{{.Name}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || $(SANDBOX_COMPOSE) ps,
preserving the echo "" line.
In
`@payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.java`:
- Line 47: The code builds recipientAccountHash by concatenating "sha256:" with
request.recipientId(), but that's not a cryptographic hash; in
OffRampActivityImpl replace that concatenation with an actual SHA-256 digest of
the account details (e.g., request.recipientId() or the full account string you
intend to anonymize), using java.security.MessageDigest to compute the hash
bytes and encode them as hex (or base64) and then prefix with "sha256:". Ensure
you update the variable/field named recipientAccountHash and any tests to expect
the real hashed value rather than the plain concatenation.
- Around line 80-98: The current resolver helpers (resolvePaymentRail,
resolveBankAccount, resolveBankCode, resolveAccountType, resolveCountry)
silently default all non-"GBP" currencies to SEPA/DE values, which is unsafe;
update each resolver to validate targetCurrency explicitly and throw an
IllegalArgumentException for unsupported currencies (or at minimum log a
warning) instead of falling back to DE values—locate and modify the five static
methods named above to check for "GBP" and explicit supported currencies and
raise IllegalArgumentException with a clear message like "Unsupported
targetCurrency: <value>" when not supported.
- Around line 84-90: The code currently hardcodes bank routing in
OffRampActivityImpl via resolveBankAccount and resolveBankCode, which ignores
the OffRampRequest.recipientId; update the flow to obtain real recipient banking
details instead of using currency-only defaults: either extend OffRampRequest to
include recipient account and sort code/iban and use those fields in
OffRampActivityImpl when building payouts, or (preferred) inject and call a
RecipientRepository/RecipientService from OffRampActivityImpl to lookup banking
details by recipientId and use those returned values in place of
resolveBankAccount/resolveBankCode; remove or deprecate the hardcoded
resolveBankAccount/resolveBankCode usage and add null/validation checks and
error handling when recipient banking info is missing.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: e29416bb-b045-41fc-bbbf-629b88ec3f1c
📒 Files selected for processing (23)
Makefileapi-gateway-iam/api-gateway-iam/src/main/resources/application-sandbox.ymlblockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/application/controller/GlobalExceptionHandler.javablockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/provider/dev/DevCustodyAdapter.javablockchain-custody/blockchain-custody/src/main/resources/application-sandbox.ymlcompliance-travel-rule/compliance-travel-rule/src/main/resources/application-sandbox.ymldocker-compose.dev.ymldocker-compose.sandbox.ymlfiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapter.javafiat-off-ramp/fiat-off-ramp/src/main/resources/application-sandbox.ymlfiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapterTest.javafiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.javafiat-on-ramp/fiat-on-ramp/src/main/resources/application-sandbox.ymlfiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.javafx-liquidity-engine/fx-liquidity-engine/src/main/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJob.javafx-liquidity-engine/fx-liquidity-engine/src/main/resources/application-sandbox.ymlfx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V6__seed_usd_gbp_corridor.sqlledger-accounting/ledger-accounting/src/main/resources/application-sandbox.ymlmerchant-iam/merchant-iam/src/main/resources/application-sandbox.ymlmerchant-onboarding/merchant-onboarding/src/main/java/com/stablecoin/payments/merchant/onboarding/application/config/FallbackAdaptersConfig.javamerchant-onboarding/merchant-onboarding/src/main/resources/application-sandbox.ymlpayment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.javapayment-orchestrator/payment-orchestrator/src/main/resources/application-sandbox.yml
💤 Files with no reviewable changes (2)
- fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java
- fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java
| VALUES (gen_random_uuid(), 'USD', 'GBP', | ||
| 1000000.00000000, 0.00000000, | ||
| 100000.00000000, 5000000.00000000, | ||
| now(), 0) | ||
| ON CONFLICT (from_currency, to_currency) DO NOTHING; |
There was a problem hiding this comment.
Seed the reverse GBP->USD pool as well.
Line 9 seeds only USD->GBP, but RateRefreshJob enables both directions (Line 26 and Line 27 in RateRefreshJob.java). Directional pool lookups can therefore fail for GBP->USD even though refresh attempts it.
🩹 Proposed migration update
INSERT INTO liquidity_pools (pool_id, from_currency, to_currency,
available_balance, reserved_balance,
minimum_threshold, maximum_capacity,
updated_at, version)
-VALUES (gen_random_uuid(), 'USD', 'GBP',
- 1000000.00000000, 0.00000000,
- 100000.00000000, 5000000.00000000,
- now(), 0)
+VALUES (gen_random_uuid(), 'USD', 'GBP',
+ 1000000.00000000, 0.00000000,
+ 100000.00000000, 5000000.00000000,
+ now(), 0),
+ (gen_random_uuid(), 'GBP', 'USD',
+ 1000000.00000000, 0.00000000,
+ 100000.00000000, 5000000.00000000,
+ now(), 0)
ON CONFLICT (from_currency, to_currency) DO NOTHING;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@fx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V6__seed_usd_gbp_corridor.sql`
around lines 9 - 13, Add a second INSERT row to the V6 migration that seeds the
reverse corridor (from_currency='GBP', to_currency='USD') so directional pool
lookups succeed when RateRefreshJob triggers both directions; mirror the
existing VALUES(...) entry (use gen_random_uuid() and the same liquidity
bounds/amounts appropriate for the reverse corridor) and keep the ON CONFLICT
(from_currency, to_currency) DO NOTHING clause so it is idempotent.
Sandbox E2E: $5 USD → £3.73 GBP through all 7 services with real APIs (Persona, Frankfurter, Stripe, Alchemy, Circle, Modulr). 16 bugs found/fixed during E2E + PR review remediation: - Externalize secrets to .env.sandbox (Stripe webhook, EVM key) - Default external API logging to false (PII protection) - Remove crypto stack trace from CustodySigningException handler - DevCustodyAdapter: fail-fast on sub-minor-unit precision - CircleRedemptionAdapter: validate amount is non-null and positive - OffRampActivityImpl: explicit currency switch (no silent SEPA fallback) - OffRampActivityImpl: real SHA-256 hash for recipientAccountHash - RateRefreshJob: load corridors from LiquidityPoolRepository (no hardcoded list) - V7 migration: seed reverse GBP→USD liquidity pool - Orchestrator depends_on: service_healthy for compliance + fx - Makefile: fix sandbox-status to use SANDBOX_COMPOSE - Circle rounding edge-case test (>2 decimals) Closes #243 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2b4ed1d to
0531f42
Compare
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.env.sandbox.template:
- Around line 21-24: The env var name in the sandbox template doesn't match the
YAML binding: update the variable in .env.sandbox.template from
CUSTODY_DEV_EVM_PRIVATE_KEY to DEV_CUSTODY_EVM_PRIVATE_KEY so it matches the
binding referenced in blockchain-custody/application.yml
(${DEV_CUSTODY_EVM_PRIVATE_KEY:}); ensure the value is a hex private key (no 0x
prefix) and keep the same comment context so the custody adapter (sandbox
signing) receives the expected key.
In `@docker-compose.sandbox.yml`:
- Around line 19-20: The published ports in docker-compose.sandbox.yml are
currently exposed on all interfaces (e.g., the mapping "5432:5432"); update
these mappings to bind to loopback by prefixing the host side with 127.0.0.1
(e.g., "127.0.0.1:5432:5432") for sandbox runs where APP_SECURITY_ENABLED is
false, and apply the same change to all other published-port entries referenced
in the comment (the mappings at the other locations). Ensure the change is
applied consistently to the port strings used by the affected services so ports
are only reachable from localhost when APP_SECURITY_ENABLED is not enabled.
- Line 39: Replace all Docker image entries that use mutable tags (e.g.,
timescale/timescaledb:latest-pg17 and the other image lines flagged) with pinned
immutable references — either explicit version tags (for example
timescale/timescaledb:<exact-version>) or content-addressable digests (e.g.,
timescale/timescaledb@sha256:<digest>). Update each image line in the compose
file to use the chosen immutable tag/digest so sandbox E2E runs are
deterministic; ensure you pin every occurrence of the mutable images reported
(all image entries currently using :latest or similar mutable tags).
In
`@fx-liquidity-engine/fx-liquidity-engine/src/main/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJob.java`:
- Around line 29-31: In RateRefreshJob.refreshRates, avoid letting a bad
liquidity pool abort the full corridor materialization: replace the direct
stream map that calls new Corridor(pool.fromCurrency(), pool.toCurrency()) with
defensive filtering/validation and per-pool error handling—e.g., filter out
pools with blank currencies or identical from/to, and wrap construction in a
try/catch to log and skip malformed rows returned by
liquidityPoolRepository.findAll(); ensure the resulting corridors list only
contains successfully constructed Corridor instances so the refresh can proceed
even if some rows are invalid.
In `@Makefile`:
- Around line 7-9: The .PHONY list is missing several sandbox targets so make
might skip their recipes when files with those names exist; update the .PHONY
declaration to include the sandbox targets: add sandbox-build, sandbox-destroy,
sandbox-logs, and the pattern sandbox-test-% (in addition to existing
sandbox-run-%) so the targets defined later (sandbox-build, sandbox-destroy,
sandbox-logs, sandbox-test-%) are always executed as phony targets.
- Around line 186-190: Update the sandbox-env-check Makefile target to validate
all required sandbox env vars by adding the same pattern used for
STRIPE_TEST_SECRET_KEY and ALCHEMY_API_KEY for each missing secret/ID: e.g., add
checks for CIRCLE_DESTINATION, MODULR_CLIENT_ID, MODULR_CLIENT_SECRET,
PERSONA_API_KEY, STRIPE_WEBHOOK_SECRET, CUSTODY_PRIVATE_KEY (or whatever exact
names are used in the codebase) using ". ./.env.sandbox && test -n
\"$$VAR_NAME\" || (echo \"ERROR: VAR_NAME not set in .env.sandbox\" && exit 1)";
keep the same overall structure and final success echo in the sandbox-env-check
target.
In
`@merchant-onboarding/merchant-onboarding/src/main/java/com/stablecoin/payments/merchant/onboarding/application/config/FallbackAdaptersConfig.java`:
- Around line 33-53: The real adapters (OnfidoKybAdapter and
CompaniesHouseAdapter) are being registered alongside fallback beans because
they lack the fallback toggle; add a guard identical to
TemporalOnboardingWorkflowAdapter by annotating the configurations/beans for
OnfidoKybAdapter and CompaniesHouseAdapter with `@ConditionalOnProperty`(name =
"app.fallback-adapters.enabled", havingValue = "false", matchIfMissing = true)
so that when app.fallback-adapters.enabled=true the real adapters are not
created and the mocks in FallbackAdaptersConfig are used exclusively.
In
`@payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.java`:
- Around line 93-107: The hardcoded sandbox bank details in resolveBankAccount
and resolveBankCode (values like "12345678", "000000", "DE89370400440532013000")
must be documented or externalized; either add an explicit comment above both
methods stating these are Modulr sandbox test fixtures and not production data,
or refactor these methods to read mappings from configuration (e.g., a
properties/Env/config client) keyed by currency and throw the same
IllegalArgumentException for unknown currencies; update any callers of
resolveBankAccount/resolveBankCode to use the new config-backed lookup and add a
small unit test or fixture showing the sandbox config values are used in tests.
- Around line 85-123: The five resolver methods (resolvePaymentRail,
resolveBankAccount, resolveBankCode, resolveAccountType, resolveCountry) all use
the same currency switch logic; consolidate by introducing a single immutable
currency config mapping (e.g., a Map<String, CurrencyConfig> or a CurrencyConfig
record) keyed by currency code, populate it with entries for "GBP" and "EUR",
and replace the individual switch-based methods with lookups that retrieve the
CurrencyConfig for the given targetCurrency and return the appropriate field
(throwing the same IllegalArgumentException if the key is missing); update
callers to use the new lookup methods or accessors so behavior remains identical
but maintenance is centralized.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 7f332daf-17a7-4cf9-99ab-b543f69ae86b
📒 Files selected for processing (26)
.env.sandbox.templateMakefileapi-gateway-iam/api-gateway-iam/src/main/resources/application-sandbox.ymlblockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/application/controller/GlobalExceptionHandler.javablockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/provider/dev/DevCustodyAdapter.javablockchain-custody/blockchain-custody/src/main/resources/application-sandbox.ymlcompliance-travel-rule/compliance-travel-rule/src/main/resources/application-sandbox.ymldocker-compose.dev.ymldocker-compose.sandbox.ymlfiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapter.javafiat-off-ramp/fiat-off-ramp/src/main/resources/application-sandbox.ymlfiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapterTest.javafiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.javafiat-on-ramp/fiat-on-ramp/src/main/resources/application-sandbox.ymlfiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.javafx-liquidity-engine/fx-liquidity-engine/src/main/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJob.javafx-liquidity-engine/fx-liquidity-engine/src/main/resources/application-sandbox.ymlfx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V6__seed_usd_gbp_corridor.sqlfx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V7__seed_gbp_usd_corridor.sqlfx-liquidity-engine/fx-liquidity-engine/src/test/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJobTest.javaledger-accounting/ledger-accounting/src/main/resources/application-sandbox.ymlmerchant-iam/merchant-iam/src/main/resources/application-sandbox.ymlmerchant-onboarding/merchant-onboarding/src/main/java/com/stablecoin/payments/merchant/onboarding/application/config/FallbackAdaptersConfig.javamerchant-onboarding/merchant-onboarding/src/main/resources/application-sandbox.ymlpayment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.javapayment-orchestrator/payment-orchestrator/src/main/resources/application-sandbox.yml
💤 Files with no reviewable changes (2)
- fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java
- fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java
| # --- S4 Blockchain & Custody (Dev EVM Wallet) --- | ||
| # Generate a new Sepolia testnet wallet private key (no 0x prefix) | ||
| # Fund it via https://www.alchemy.com/faucets/base-sepolia | ||
| CUSTODY_DEV_EVM_PRIVATE_KEY=your_evm_private_key_hex_here |
There was a problem hiding this comment.
Environment variable mismatch — custody private key won't bind.
CUSTODY_DEV_EVM_PRIVATE_KEY is defined here, but blockchain-custody/application.yml:125 binds to ${DEV_CUSTODY_EVM_PRIVATE_KEY:}. The custody adapter will receive an empty key and fail during sandbox signing.
Align the naming — recommend using the existing DEV_CUSTODY_EVM_PRIVATE_KEY in the template to match the YAML binding.
Proposed fix
# --- S4 Blockchain & Custody (Dev EVM Wallet) ---
# Generate a new Sepolia testnet wallet private key (no 0x prefix)
# Fund it via https://www.alchemy.com/faucets/base-sepolia
-CUSTODY_DEV_EVM_PRIVATE_KEY=your_evm_private_key_hex_here
+DEV_CUSTODY_EVM_PRIVATE_KEY=your_evm_private_key_hex_here📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # --- S4 Blockchain & Custody (Dev EVM Wallet) --- | |
| # Generate a new Sepolia testnet wallet private key (no 0x prefix) | |
| # Fund it via https://www.alchemy.com/faucets/base-sepolia | |
| CUSTODY_DEV_EVM_PRIVATE_KEY=your_evm_private_key_hex_here | |
| # --- S4 Blockchain & Custody (Dev EVM Wallet) --- | |
| # Generate a new Sepolia testnet wallet private key (no 0x prefix) | |
| # Fund it via https://www.alchemy.com/faucets/base-sepolia | |
| DEV_CUSTODY_EVM_PRIVATE_KEY=your_evm_private_key_hex_here |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.env.sandbox.template around lines 21 - 24, The env var name in the sandbox
template doesn't match the YAML binding: update the variable in
.env.sandbox.template from CUSTODY_DEV_EVM_PRIVATE_KEY to
DEV_CUSTODY_EVM_PRIVATE_KEY so it matches the binding referenced in
blockchain-custody/application.yml (${DEV_CUSTODY_EVM_PRIVATE_KEY:}); ensure the
value is a hex private key (no 0x prefix) and keep the same comment context so
the custody adapter (sandbox signing) receives the expected key.
| ports: | ||
| - "5432:5432" |
There was a problem hiding this comment.
Restrict published ports to localhost when security is disabled.
At Line 20 and other published-port lines, services are exposed on all interfaces while APP_SECURITY_ENABLED is false. For sandbox runs with real provider credentials/data, bind ports to loopback (127.0.0.1) to reduce unintended network exposure.
Proposed hardening pattern
- - "8085:8085"
+ - "127.0.0.1:8085:8085"Apply the same pattern to all published service/data-store ports unless remote access is explicitly required.
Also applies to: 42-43, 61-62, 85-86, 100-101, 127-128, 167-168, 204-205, 242-243, 282-283, 323-324, 355-356
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docker-compose.sandbox.yml` around lines 19 - 20, The published ports in
docker-compose.sandbox.yml are currently exposed on all interfaces (e.g., the
mapping "5432:5432"); update these mappings to bind to loopback by prefixing the
host side with 127.0.0.1 (e.g., "127.0.0.1:5432:5432") for sandbox runs where
APP_SECURITY_ENABLED is false, and apply the same change to all other
published-port entries referenced in the comment (the mappings at the other
locations). Ensure the change is applied consistently to the port strings used
by the affected services so ports are only reachable from localhost when
APP_SECURITY_ENABLED is not enabled.
| retries: 10 | ||
|
|
||
| timescaledb: | ||
| image: timescale/timescaledb:latest-pg17 |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify mutable tags in sandbox compose
rg -n '^\s*image:\s+\S+:latest(\S*)?$' docker-compose.sandbox.ymlRepository: Puneethkumarck/stablebridge-platform
Length of output: 543
Pin Docker images to immutable versions/digests for deterministic E2E sandbox runs.
Mutable tags (:latest, :latest-pg17) introduce non-determinism. Silently changing behavior between runs breaks test reliability and makes regression detection impossible.
Affected lines: 39, 97, 124, 164, 201, 239, 279, 320, 352
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docker-compose.sandbox.yml` at line 39, Replace all Docker image entries that
use mutable tags (e.g., timescale/timescaledb:latest-pg17 and the other image
lines flagged) with pinned immutable references — either explicit version tags
(for example timescale/timescaledb:<exact-version>) or content-addressable
digests (e.g., timescale/timescaledb@sha256:<digest>). Update each image line in
the compose file to use the chosen immutable tag/digest so sandbox E2E runs are
deterministic; ensure you pin every occurrence of the mutable images reported
(all image entries currently using :latest or similar mutable tags).
| var corridors = liquidityPoolRepository.findAll().stream() | ||
| .map(pool -> new Corridor(pool.fromCurrency(), pool.toCurrency())) | ||
| .toList(); |
There was a problem hiding this comment.
Harden corridor materialization to prevent full refresh abort on bad pool rows.
If one liquidity_pools record is invalid (blank currency or same from/to), new Corridor(...) throws during stream mapping and exits refreshRates() before any valid corridor is processed.
🔧 Proposed defensive fix
+import com.stablecoin.payments.fx.domain.model.LiquidityPool;
+import java.util.Optional;
@@
- var corridors = liquidityPoolRepository.findAll().stream()
- .map(pool -> new Corridor(pool.fromCurrency(), pool.toCurrency()))
- .toList();
+ var corridors = liquidityPoolRepository.findAll().stream()
+ .map(this::toCorridorSafely)
+ .flatMap(Optional::stream)
+ .distinct()
+ .toList();
@@
public void refreshRates() {
@@
log.debug("Rate refresh complete: {}/{} corridors updated", refreshed, corridors.size());
}
+
+ private Optional<Corridor> toCorridorSafely(LiquidityPool pool) {
+ try {
+ return Optional.of(new Corridor(pool.fromCurrency(), pool.toCurrency()));
+ } catch (IllegalArgumentException ex) {
+ log.warn("Skipping invalid corridor {}->{}: {}", pool.fromCurrency(), pool.toCurrency(), ex.getMessage());
+ return Optional.empty();
+ }
+ }
}As per coding guidelines, "Rate caching must handle stale data correctly".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@fx-liquidity-engine/fx-liquidity-engine/src/main/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJob.java`
around lines 29 - 31, In RateRefreshJob.refreshRates, avoid letting a bad
liquidity pool abort the full corridor materialization: replace the direct
stream map that calls new Corridor(pool.fromCurrency(), pool.toCurrency()) with
defensive filtering/validation and per-pool error handling—e.g., filter out
pools with blank currencies or identical from/to, and wrap construction in a
try/catch to log and skip malformed rows returned by
liquidityPoolRepository.findAll(); ensure the resulting corridors list only
contains successfully constructed Corridor instances so the refresh can proceed
even if some rows are invalid.
| e2e-up e2e-down e2e-destroy e2e-status e2e-build e2e-test e2e \ | ||
| sandbox-up sandbox-down sandbox-status sandbox-run-% sandbox-test \ | ||
| sandbox-tunnel sandbox-env-check |
There was a problem hiding this comment.
Add missing sandbox targets to .PHONY.
sandbox-build, sandbox-destroy, sandbox-logs, and sandbox-test-% are defined later but not declared phony. If matching files exist, make may skip those recipes.
Proposed fix
.PHONY: help build clean test test-unit test-integration test-business \
format lint check \
infra-up infra-down infra-destroy infra-status infra-logs infra-logs-% \
run-% db-reset db-psql topics \
deps outdated \
assemble sonar fresh ci \
e2e-up e2e-down e2e-destroy e2e-status e2e-build e2e-test e2e \
- sandbox-up sandbox-down sandbox-status sandbox-run-% sandbox-test \
- sandbox-tunnel sandbox-env-check
+ sandbox-up sandbox-down sandbox-status sandbox-run-% sandbox-test sandbox-test-% \
+ sandbox-build sandbox-destroy sandbox-logs sandbox-tunnel sandbox-env-check📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| e2e-up e2e-down e2e-destroy e2e-status e2e-build e2e-test e2e \ | |
| sandbox-up sandbox-down sandbox-status sandbox-run-% sandbox-test \ | |
| sandbox-tunnel sandbox-env-check | |
| e2e-up e2e-down e2e-destroy e2e-status e2e-build e2e-test e2e \ | |
| sandbox-up sandbox-down sandbox-status sandbox-run-% sandbox-test sandbox-test-% \ | |
| sandbox-build sandbox-destroy sandbox-logs sandbox-tunnel sandbox-env-check |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Makefile` around lines 7 - 9, The .PHONY list is missing several sandbox
targets so make might skip their recipes when files with those names exist;
update the .PHONY declaration to include the sandbox targets: add sandbox-build,
sandbox-destroy, sandbox-logs, and the pattern sandbox-test-% (in addition to
existing sandbox-run-%) so the targets defined later (sandbox-build,
sandbox-destroy, sandbox-logs, sandbox-test-%) are always executed as phony
targets.
| sandbox-env-check: ## Verify .env.sandbox exists and key vars are set | ||
| @test -f .env.sandbox || (echo "ERROR: .env.sandbox not found. Run: cp .env.sandbox.template .env.sandbox" && exit 1) | ||
| @. ./.env.sandbox && test -n "$$STRIPE_TEST_SECRET_KEY" || (echo "ERROR: STRIPE_TEST_SECRET_KEY not set in .env.sandbox" && exit 1) | ||
| @. ./.env.sandbox && test -n "$$ALCHEMY_API_KEY" || (echo "ERROR: ALCHEMY_API_KEY not set in .env.sandbox" && exit 1) | ||
| @echo "✓ .env.sandbox loaded — keys present" |
There was a problem hiding this comment.
Expand sandbox-env-check to validate all required sandbox secrets/IDs.
Current checks only cover Stripe API key and Alchemy key. The compose stack and sandbox tests also depend on additional required vars (e.g., Circle destination, Modulr credentials/secrets, Persona keys, Stripe webhook secret, custody private key). Missing values will fail late at runtime.
Proposed fix
sandbox-env-check: ## Verify .env.sandbox exists and key vars are set
`@test` -f .env.sandbox || (echo "ERROR: .env.sandbox not found. Run: cp .env.sandbox.template .env.sandbox" && exit 1)
- @. ./.env.sandbox && test -n "$$STRIPE_TEST_SECRET_KEY" || (echo "ERROR: STRIPE_TEST_SECRET_KEY not set in .env.sandbox" && exit 1)
- @. ./.env.sandbox && test -n "$$ALCHEMY_API_KEY" || (echo "ERROR: ALCHEMY_API_KEY not set in .env.sandbox" && exit 1)
+ `@set` -a; . ./.env.sandbox; set +a; \
+ required="STRIPE_TEST_SECRET_KEY STRIPE_SANDBOX_WEBHOOK_SECRET ALCHEMY_API_KEY CUSTODY_DEV_EVM_PRIVATE_KEY PERSONA_SANDBOX_API_KEY PERSONA_INQUIRY_TEMPLATE_ID CIRCLE_SANDBOX_API_KEY CIRCLE_SANDBOX_DESTINATION_ID MODULR_SANDBOX_API_KEY MODULR_SANDBOX_SOURCE_ACCOUNT_ID MODULR_SANDBOX_API_SECRET"; \
+ for k in $$required; do eval "v=\$${$$k}"; [ -n "$$v" ] || { echo "ERROR: $$k not set in .env.sandbox"; exit 1; }; done
`@echo` "✓ .env.sandbox loaded — keys present"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Makefile` around lines 186 - 190, Update the sandbox-env-check Makefile
target to validate all required sandbox env vars by adding the same pattern used
for STRIPE_TEST_SECRET_KEY and ALCHEMY_API_KEY for each missing secret/ID: e.g.,
add checks for CIRCLE_DESTINATION, MODULR_CLIENT_ID, MODULR_CLIENT_SECRET,
PERSONA_API_KEY, STRIPE_WEBHOOK_SECRET, CUSTODY_PRIVATE_KEY (or whatever exact
names are used in the codebase) using ". ./.env.sandbox && test -n
\"$$VAR_NAME\" || (echo \"ERROR: VAR_NAME not set in .env.sandbox\" && exit 1)";
keep the same overall structure and final success echo in the sandbox-env-check
target.
| @ConditionalOnMissingBean | ||
| KybProvider mockKybProvider() { | ||
| return new MockKybAdapter(); | ||
| } | ||
|
|
||
| @Bean | ||
| @ConditionalOnMissingBean | ||
| CompanyRegistryProvider mockCompanyRegistryProvider() { | ||
| return new MockCompanyRegistryAdapter(); | ||
| } | ||
|
|
||
| @Bean | ||
| @ConditionalOnMissingBean | ||
| DocumentStore mockDocumentStore() { | ||
| return new MockDocumentStoreAdapter(); | ||
| } | ||
|
|
||
| @Bean | ||
| @ConditionalOnMissingBean | ||
| OnboardingWorkflowPort mockOnboardingWorkflow() { | ||
| return new MockOnboardingWorkflowAdapter(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify conditional annotations and potential overlap between fallback and real adapters.
files=$(fd 'FallbackAdaptersConfig\.java|OnfidoKybAdapter\.java|CompaniesHouseAdapter\.java|TemporalOnboardingWorkflowAdapter\.java')
for f in $files; do
echo "===== $f ====="
rg -n -C2 '@ConditionalOnProperty|@ConditionalOnMissingBean|class .*Adapter|@Bean' "$f"
echo
done
# Expected verification outcome:
# - FallbackAdaptersConfig beans are guarded by `@ConditionalOnMissingBean`.
# - OnfidoKybAdapter / CompaniesHouseAdapter are not additionally gated by app.fallback-adapters.enabled=false.
# If both are true, fallback mode can still select real adapters for those ports.Repository: Puneethkumarck/stablebridge-platform
Length of output: 8532
Real adapters activate concurrently with fallback beans when provider properties are set, defeating fallback-mode intent.
When app.fallback-adapters.enabled=true and app.kyb.provider=onfido (or app.company-registry.provider=companies-house), both OnfidoKybAdapter and the mock fallback beans enter the context. Since @ConditionalOnMissingBean finds the real adapter already registered, the mock is never created—you silently route to production services in sandbox/test.
TemporalOnboardingWorkflowAdapter correctly uses @ConditionalOnProperty(name = "app.fallback-adapters.enabled", havingValue = "false", matchIfMissing = true) to enforce mutual exclusion. Apply the same pattern: gate OnfidoKybAdapter and CompaniesHouseAdapter with app.fallback-adapters.enabled=false to prevent this collision.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@merchant-onboarding/merchant-onboarding/src/main/java/com/stablecoin/payments/merchant/onboarding/application/config/FallbackAdaptersConfig.java`
around lines 33 - 53, The real adapters (OnfidoKybAdapter and
CompaniesHouseAdapter) are being registered alongside fallback beans because
they lack the fallback toggle; add a guard identical to
TemporalOnboardingWorkflowAdapter by annotating the configurations/beans for
OnfidoKybAdapter and CompaniesHouseAdapter with `@ConditionalOnProperty`(name =
"app.fallback-adapters.enabled", havingValue = "false", matchIfMissing = true)
so that when app.fallback-adapters.enabled=true the real adapters are not
created and the mocks in FallbackAdaptersConfig are used exclusively.
| private static String resolvePaymentRail(String targetCurrency) { | ||
| return switch (targetCurrency) { | ||
| case "GBP" -> "FASTER_PAYMENTS"; | ||
| case "EUR" -> "SEPA"; | ||
| default -> throw new IllegalArgumentException("Unsupported payout currency: " + targetCurrency); | ||
| }; | ||
| } | ||
|
|
||
| private static String resolveBankAccount(String targetCurrency) { | ||
| return switch (targetCurrency) { | ||
| case "GBP" -> "12345678"; | ||
| case "EUR" -> "DE89370400440532013000"; | ||
| default -> throw new IllegalArgumentException("Unsupported payout currency: " + targetCurrency); | ||
| }; | ||
| } | ||
|
|
||
| private static String resolveBankCode(String targetCurrency) { | ||
| return switch (targetCurrency) { | ||
| case "GBP" -> "000000"; | ||
| case "EUR" -> "COBADEFFXXX"; | ||
| default -> throw new IllegalArgumentException("Unsupported payout currency: " + targetCurrency); | ||
| }; | ||
| } | ||
|
|
||
| private static String resolveAccountType(String targetCurrency) { | ||
| return switch (targetCurrency) { | ||
| case "GBP" -> "SORT_CODE"; | ||
| case "EUR" -> "IBAN"; | ||
| default -> throw new IllegalArgumentException("Unsupported payout currency: " + targetCurrency); | ||
| }; | ||
| } | ||
|
|
||
| private static String resolveCountry(String targetCurrency) { | ||
| return switch (targetCurrency) { | ||
| case "GBP" -> "GB"; | ||
| case "EUR" -> "DE"; | ||
| default -> throw new IllegalArgumentException("Unsupported payout currency: " + targetCurrency); | ||
| }; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Explicit currency validation is correct; consider consolidating the repeated switch logic.
The IllegalArgumentException for unsupported currencies addresses the prior silent-fallback concern. However, five nearly identical switch expressions introduce maintenance overhead.
♻️ Optional: Consolidate into a single currency config record
+ private record CurrencyConfig(String rail, String account, String code, String accountType, String country) {}
+
+ private static final Map<String, CurrencyConfig> CURRENCY_CONFIGS = Map.of(
+ "GBP", new CurrencyConfig("FASTER_PAYMENTS", "12345678", "000000", "SORT_CODE", "GB"),
+ "EUR", new CurrencyConfig("SEPA", "DE89370400440532013000", "COBADEFFXXX", "IBAN", "DE")
+ );
+
+ private static CurrencyConfig requireConfig(String currency) {
+ var cfg = CURRENCY_CONFIGS.get(currency);
+ if (cfg == null) throw new IllegalArgumentException("Unsupported payout currency: " + currency);
+ return cfg;
+ }Then replace each resolver with a single map lookup.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.java`
around lines 85 - 123, The five resolver methods (resolvePaymentRail,
resolveBankAccount, resolveBankCode, resolveAccountType, resolveCountry) all use
the same currency switch logic; consolidate by introducing a single immutable
currency config mapping (e.g., a Map<String, CurrencyConfig> or a CurrencyConfig
record) keyed by currency code, populate it with entries for "GBP" and "EUR",
and replace the individual switch-based methods with lookups that retrieve the
CurrencyConfig for the given targetCurrency and return the appropriate field
(throwing the same IllegalArgumentException if the key is missing); update
callers to use the new lookup methods or accessors so behavior remains identical
but maintenance is centralized.
| private static String resolveBankAccount(String targetCurrency) { | ||
| return switch (targetCurrency) { | ||
| case "GBP" -> "12345678"; | ||
| case "EUR" -> "DE89370400440532013000"; | ||
| default -> throw new IllegalArgumentException("Unsupported payout currency: " + targetCurrency); | ||
| }; | ||
| } | ||
|
|
||
| private static String resolveBankCode(String targetCurrency) { | ||
| return switch (targetCurrency) { | ||
| case "GBP" -> "000000"; | ||
| case "EUR" -> "COBADEFFXXX"; | ||
| default -> throw new IllegalArgumentException("Unsupported payout currency: " + targetCurrency); | ||
| }; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Hardcoded bank details are sandbox test fixtures — document or extract.
These values (12345678, 000000, DE89370400440532013000) are Modulr sandbox test accounts per PR objectives. Add a comment clarifying this, or extract to configuration for easier discovery and future corridor expansion.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.java`
around lines 93 - 107, The hardcoded sandbox bank details in resolveBankAccount
and resolveBankCode (values like "12345678", "000000", "DE89370400440532013000")
must be documented or externalized; either add an explicit comment above both
methods stating these are Modulr sandbox test fixtures and not production data,
or refactor these methods to read mappings from configuration (e.g., a
properties/Env/config client) keyed by currency and throw the same
IllegalArgumentException for unknown currencies; update any callers of
resolveBankAccount/resolveBankCode to use the new config-backed lookup and add a
small unit test or fixture showing the sandbox config values are used in tests.
|
This pull request has been inactive for 14 days and has been marked as stale. Please rebase, resolve any conflicts, and update the PR if it is still intended to be merged. It will be automatically closed in 7 days if there is no further activity. |
Summary
First successful end-to-end stablecoin sandwich payment through all 7 services with real sandbox APIs.
Payment: $5.00 USD → £3.73 GBP (US → GB corridor)
Payment ID:
9c6f2646-cbfc-4dcf-8be7-a76e38c58c55Blockchain TX: 0x99950cbe... on Base Sepolia
Real APIs Used
16 Bugs Found and Fixed
confirm=trueremoval (ACH needs customer confirmation)SEPA_CREDIT_TRANSFERscheme nameNew Files
docker-compose.sandbox.yml— full 12-container E2E stackV6__seed_usd_gbp_corridor.sql— USD→GBP liquidity poolTest plan
Closes #243
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Infrastructure