Skip to content

feat: Soroban signer, sequence management, submit retry, and on-chain intent creation - #148

Open
chideraisiguzor wants to merge 4 commits into
stellar-vortex-protocol:mainfrom
chideraisiguzor:feature/onchain-intent-create
Open

feat: Soroban signer, sequence management, submit retry, and on-chain intent creation#148
chideraisiguzor wants to merge 4 commits into
stellar-vortex-protocol:mainfrom
chideraisiguzor:feature/onchain-intent-create

Conversation

@chideraisiguzor

Copy link
Copy Markdown

Summary

Four related pieces of the on-chain settlement path, landed as one stack since each builds directly on the last:

  • Signer service (src/soroban/signer.service.ts, src/config/env.validation.ts) — the backend hot-wallet key is env-injected via SOROBAN_SIGNER_SECRET_KEY, never logged (custom toString/toJSON/inspect redaction), and required by env.validation.ts when NODE_ENV=production so boot fails fast instead of failing at first submit. Optional elsewhere so dev/test don't need a real key.
  • Sequence number management (src/soroban/signer.service.ts) — replaces the naive per-request getAccount() pattern with an in-process, lock-serialized sequence counter (SignerService.withNextSequence), so concurrent submissions from the signing account can't race on the same sequence number. Cache resets on failure so it re-syncs from the network instead of drifting.
  • Retry/backoff for submission (src/soroban/stellar-tx.service.ts) — new StellarTxService builds, signs, and submits Soroban contract-invocation transactions with capped exponential backoff. Never resends blindly: before every retry it checks the transaction's status by hash first (idempotency guard against double submission), and a confirmed on-chain FAILED is treated as terminal rather than retried.
  • On-chain intent creation (src/intents/intents.service.ts, src/intents/intents.controller.ts) — IntentsService.create() now calls the settlement contract via StellarTxService.invokeContract() behind a rollout flag (ONCHAIN_INTENTS_ENABLED, default off) that falls back to the existing pure in-memory behavior. IntentsController.create()'s response shape is unchanged. When the flag is on, a missing SETTLEMENT_CONTRACT_ID or a failed submission surfaces as a 503 and the intent is not added to the local store.

Recommended merge/review order (each is its own logical commit on this branch): #21#30#29#22.

Design notes / open dependencies

  • Custody model: env-injected secret over KMS, since there's no KMS integration or cloud provider config anywhere in this repo. Issue Write an ADR for the on-chain settlement architecture #19 (the ADR for this) is still open and assigned to someone else — happy to revisit if the ADR lands with a different decision.
  • Settlement contract call: the create_intent method name and argument encoding in IntentsService.buildCreateIntentArgs() are a best-effort, clearly-commented placeholder — there's no contract ABI or typed bindings in this repo yet (issue Add typed contract client bindings for the settlement contract #32, also open/unassigned). This is expected to change once real bindings land; it's isolated to one private method.

Test plan

Ran on the final branch (includes all four features):

$ npm run lint          # clean
$ npm run typecheck     # clean
$ npm test               # Test Suites: 5 passed, 5 total / Tests: 41 passed, 41 total
$ npm run test:e2e       # Test Suites: 5 passed, 5 total / Tests: 23 passed, 23 total

Also manually smoke-tested against a live-booted server:

  • Default config (flag off): POST /api/v1/intents behaves exactly as before → 201.
  • ONCHAIN_INTENTS_ENABLED=true with a settlement contract ID but no reachable RPC/contract: fails cleanly with 503 {"error":"Failed to register intent with the settlement contract"} — no crash, no phantom in-memory intent left behind.

New unit coverage: signer.service.spec.ts (11 tests, including concurrency/race tests for sequence management), stellar-tx.service.spec.ts (8 tests, including the idempotency-before-resend and terminal-vs-retryable-failure behavior), and additions to intents.service.spec.ts (4 new tests for the on-chain path).

Closes #21
Closes #30
Closes #29
Closes #22

Backend hot-wallet key is env-injected via SOROBAN_SIGNER_SECRET_KEY,
never logged (custom toString/toJSON/inspect redact it), and required
by env.validation.ts when NODE_ENV=production so boot fails fast
instead of at first submit. Optional elsewhere so dev/test don't need
a real key.

Closes stellar-vortex-protocol#21
Replaces the naive per-request getAccount() pattern with an
in-process, lock-serialized sequence counter on SignerService.
withNextSequence() fetches the account's sequence from the network
once, caches it, and hands out a fresh incremented value to each
caller in strict call order — so concurrent submissions from the
backend's signing account can't race on the same sequence number.
The cache is dropped on failure so the next call re-syncs from the
network instead of drifting.

Closes stellar-vortex-protocol#30
Adds StellarTxService for building, signing, and submitting Soroban
contract-invocation transactions on the signer's account. Submission
retries with exponential backoff, capped at maxAttempts, but never
resends blindly: before every retry it first checks the transaction's
status by hash (the signed envelope is fixed for the call) so a lost
response or a poll timeout can't cause a double submission. A
confirmed on-chain FAILED status is treated as terminal and is not
retried. Exhausted retries and terminal failures both surface as
SorobanSubmissionError with a clear message.

Also extends SorobanService with prepareTransaction/sendTransaction/
getTransactionStatus, the write-path RPC calls it was missing.

Closes stellar-vortex-protocol#29
IntentsService.create() now calls the settlement contract through
StellarTxService.invokeContract() when ONCHAIN_INTENTS_ENABLED is on,
gated behind a rollout flag (default off) that falls back to the
existing pure in-memory behavior. IntentsController.create()'s
response shape is unchanged.

When the flag is on, a missing SETTLEMENT_CONTRACT_ID or a failed
on-chain submission surfaces as a 503 and the intent is not added to
the local store -- no phantom off-chain-only intents. Argument
encoding for the contract call is provisional pending the on-chain
settlement ADR and typed contract bindings, both still open.

Closes stellar-vortex-protocol#22
@drips-wave

drips-wave Bot commented Jul 26, 2026

Copy link
Copy Markdown

@chideraisiguzor Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant