fix: real Stellar submission for sendPayment (closes #63) - #135
Merged
ndii-dev merged 4 commits intoJul 21, 2026
Merged
Conversation
app/api/wallet/send/route.ts fabricated `hash: 0x${Math.random()...}` and
always returned status:'completed' without ever touching the Stellar
network. wallet.service.ts then persisted every attempt as 'completed'
regardless of what the route reported, compounding the problem.
- New lib/services/stellar-payment.service.ts builds, signs, and submits
a real payment via @stellar/stellar-sdk (same approach as
Orbit-Wal/backend's StellarService), using a server-only
STELLAR_SOURCE_SECRET_KEY. Missing config fails loudly
(503 ERR_PAYMENT_NOT_CONFIGURED) instead of fabricating success.
- Real Horizon outcomes map to completed/failed/pending: a definitive
rejection (result_codes present) is failed; a response-less network
error is pending, since the signed envelope may still land in a ledger
— TransactionSyncService's existing settlement poller reconciles it.
- The transaction hash is the SDK's own pre-submission signature hash
(64 lowercase hex), present even on failure/timeout, so every attempt
is independently verifiable on Horizon/stellar.expert.
- Destination validation upgraded from a regex to real StrKey checksum
validation; asset and memo length are now validated against the
supported-asset list and Stellar's 28-byte MEMO_TEXT limit.
- wallet.service.ts now persists result.status instead of a hardcoded
'completed'; send-form.tsx distinguishes a pending broadcast from a
confirmed send instead of always saying "Transaction Successful".
- Drive-by: soroban.service.ts hardcoded the Futurenet passphrase for
"testnet" — same network-passphrase logic this issue touches, fixed
to use the SDK's own Networks constants.
Verified against real Stellar testnet with a funded throwaway keypair
(see docs/issue-63.md and verify-send.ts) — completed, op_underfunded,
and op_no_destination cases all independently re-verified against
Horizon by hash.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018d6o25SNNbEXPScxNun19p
Pure copy-paste duplication (identical declaration repeated verbatim) — unrelated to Issue Orbit-Wal#63, but it broke Jest's module parse for every test that imports Alert transitively (including tests/component/send-form.test.tsx, which this PR already modifies), so fixing it here unblocks running and showing the new pending-state test for real instead of asserting it only against isolated files. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018d6o25SNNbEXPScxNun19p
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018d6o25SNNbEXPScxNun19p
Closed
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #63.
Root cause (not just "fixed the bug")
app/api/wallet/send/route.tsvalidated request shape and then returned a fabricated result:No transaction was ever built, signed, or submitted, and
0x-prefixed hex isn't even a valid Stellar tx hash shape.wallet.service.ts::sendPaymentcompounded this by persisting every attempt asstatus: 'completed'regardless of what the route returned — so fixing the route alone would not have fixed local transaction history.Full root-cause writeup, the backend-vs-direct-SDK design decision and why, and everything below in more detail: docs/issue-63.md.
Design decision
Chose direct
@stellar/stellar-sdkusage inside this app's own route over proxying toOrbit-Wal/backend's/api/v1/wallet/send: that endpoint needs a rawsourceSecretKeyin the request body, and Globe Wallet has no secret-key storage anywhere in its account model — proxying would add a second network hop and a second service to configure without removing the actual hard part (key custody).@stellar/stellar-sdkis already a direct dependency here and already used server-side (transaction-sync.service.ts,soroban.service.ts), so this builds on existing, proven integration instead of adding new cross-service surface area.Real submission needs a signing key this app doesn't have anywhere (the mock accounts are placeholder public keys with no secret, by design — this is a demo/mock wallet). Rather than inventing fake custody, I added a server-only
STELLAR_SOURCE_SECRET_KEYenv var (optional — unset by default, a fresh checkout still boots). If it's unset, the route fails loud with503 ERR_PAYMENT_NOT_CONFIGUREDinstead of fabricating anything (same pattern as the existingSorobanService.requireConfig()). Real per-account custody is explicitly called out as follow-up work, not something this issue tries to solve.Definition of done — addressed explicitly
lib/services/stellar-payment.service.tsbuilds aTransactionBuilderpayment operation, signs with the configuredKeypair, and callsserver.submitTransaction()— same shape asOrbit-Wal/backend'sStellarService.sendPayment.statusreflects actual ledger state:successfulfrom Horizon drivescompleted/failed. A definitive Horizon rejection (result codes present, e.g.op_underfunded) →failed. A response-less network error (timeout — we never got Horizon's answer) →pending, since the envelope may still land in a ledger; the already-existingTransactionSyncService.syncFromNetwork()settlement poller reconciles it later.transaction.hash().toString('hex')— the SDK's own pre-submission signature hash — present even on failure/timeout, so every attempt is independently checkable on Horizon.Evidence the code actually runs
Real testnet run (not mocked) —
verify-send.tsRan the actual route handler against Stellar testnet with a friendbot-funded throwaway keypair (
GCTQDBNWV4PFO35SFBTB5ULEQQGOELIEYJHIWLGK6XTVZKGYGVLOTFPY):Independently re-verified every hash directly against Horizon (
GET /transactions/:hash, not this app's own code):successfulledgerd6effc88...9bf0b56(completed)true3726945e6e7c350...12f2ca24(op_underfunded)false372694655e33f77...53d78b60(op_no_destination)false3727025All three are publicly checkable, e.g. https://stellar.expert/explorer/testnet/tx/d6effc88280522fec9579d24da3e513b0f6dd9b4166ecdde2cf821a3f9bf0b56 — note that even the two "failed" transactions were included in a real ledger with a real hash (Stellar charges the fee and includes
tx_failedtransactions on-chain), which is exactly the "real, verifiable hash even on failure" requirement.Automated test suite
Coverage on touched files, well above
jest.config.js's configured thresholds:app/api/wallet/send/route.ts81% stmts/87.5% branches (req. 75%),lib/services/wallet.service.ts90%/90% (req. 85%),lib/services/stellar-payment.service.ts96%/77% (no threshold configured, included for completeness).Pre-existing, unrelated breakage (verified via
git stashon this exact branch, confirmed identical before and after this change — not something this PR causes or needs to fix): 13 test suites fail onmainfor unrelated reasons (@simplewebauthn/servernever added topackage.json,hooks/use-toast.tshas a syntax error, several analytics/chart API tests expect response shapes their routes don't return, etc.).next buildalso currently fails onmainindependent of this change, for the same@simplewebauthn/server/use-toast.tsreasons.Adjacent behavior re-verified
wallet.service.tsstatus-persistence bug: was hardcodingstatus: 'completed'on every save regardless of the route's actual response. Fixed to useresult.status; covered by 2 new tests.useWalletSend/SendFormUI:successnow means "not a definitive failure" (coverscompletedandpending), so a pending broadcast doesn't show as a false "Payment failed." The success card now distinguishes "Send Complete" from "Send Submitted — awaiting network confirmation" via the existingTransactionStatusBadgecomponent./^G[A-Z0-9]{55}$/i— accepts non-checksummed strings) to realStrKey.isValidEd25519PublicKeychecksum validation. Several test fixture addresses (GDXSPAYWALLET7QK3...,GC3G2N7N5LRYX6L5...) were never valid Stellar keys, just regex-shaped — replaced with real checksummed keys where the new validation now correctly rejects them.assetand never checked memo length. Both now validated pre-flight againstSUPPORTED_STELLAR_ASSETSand Stellar's 28-byteMEMO_TEXTlimit (ERR_UNSUPPORTED_ASSET,ERR_MEMO_TOO_LONG).soroban.service.tshardcoded the Futurenet passphrase forNEXT_PUBLIC_STELLAR_NETWORK=testnet— a real, separate bug in the same network-passphrase logic this issue touches. Fixed to use the SDK's ownNetworks.TESTNET.components/ui/alert.tsxhadAlertTitledeclared twice (verbatim copy-paste duplicate) — broke Jest's parse for every test importingAlerttransitively, includingsend-form.test.tsxwhich this PR extends. Removed the duplicate; unblocked 4 previously-broken suites as a side effect (noted in docs/issue-63.md).Files changed
lib/services/stellar-payment.service.ts(new) — real build/sign/submit + status mappingapp/api/wallet/send/route.ts— delegates to the above; real StrKey/asset/memo validation; new error codeslib/services/wallet.service.ts— status-persistence fixlib/errors.ts—ERR_UNSUPPORTED_ASSET,ERR_MEMO_TOO_LONG,ERR_ACCOUNT_KEY_MISMATCH,ERR_PAYMENT_NOT_CONFIGUREDlib/env.mjs,.env.example— newSTELLAR_SOURCE_SECRET_KEY(optional, server-only)components/app/send-form.tsx— pending vs completed UI distinctioncomponents/ui/alert.tsx— drive-by duplicate-declaration fixlib/services/soroban.service.ts— drive-by Futurenet/testnet passphrase fixnext.config.mjs,jest.config.js—uint8array-extrastranspile/transform allowlist (needed once the route imports the SDK directly)docs/issue-63.md(new),docs/issue-28.md,README.md— design rationale + env var docstests/unit/services/stellar-payment.service.test.ts(new),tests/integration/wallet-send-unconfigured.test.ts(new), plus updates totests/integration/api-wallet.test.ts,tests/integration/fixture-integration.test.ts,tests/unit/services/wallet.service.test.ts,tests/unit/hooks/useWalletSend.test.ts,tests/component/send-form.test.tsxverify-send.ts(new, repo root) — manual real-testnet verification script, mirrors the existingverify.ts/test-err.tsprecedentCo-Authored-By: Claude Sonnet 5 noreply@anthropic.com