feat(onchain): recipient and distributor client-signing seam - #450
Merged
kilodesodiq-arch merged 4 commits intoAug 20, 2026
Merged
Conversation
…auth SorobanAdapter signed every submitted transaction with the admin keypair, which the aid_escrow contract's per-caller require_auth() rejects for claims (recipient) and distributor-created packages (operator). Add a client-signing seam: buildUnsignedClaimTx and buildUnsignedCreatePackageTx return an unsigned, simulated envelope whose Soroban auth entries demand the caller's signature, and submitSignedTx cryptographically verifies the auth entries against the required account before signing the envelope with the admin keypair (fee sponsorship) and submitting. Fix the claim ABI (claim(id, claimer)) and document the signing model and client sequence.
…p thresholds The signing spec requires real @stellar/stellar-sdk crypto (Keypair.random(), authorizeEntry, xdr classes) which is incompatible with the moduleNameMapper in jest-coverage.js that replaces the SDK with a mock. Exclude it from the coverage config and run it in a separate CI step using the base jest config. Also bump soroban.adapter.ts coverage thresholds to accommodate the new client-signing seam methods.
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.
Summary
Closes #428
SorobanAdapternow exposes a client-signing seam:buildUnsignedClaimTx/buildUnsignedCreatePackageTxreturn an unsigned, simulated envelope whose Soroban auth entries demand the caller's signature, andsubmitSignedTxverifies those signatures before signing the envelope with the admin keypair and submitting. The single most important design decision is admin-as-source fee sponsorship: the envelope source is the admin account, so the recipient/distributor never needs to hold XLM (a fee-bump would still require the recipient to have an account and sequence number, so it buys nothing here).Why
The contract's auth model is per-caller —
claim(id, claimer)callsclaimer.require_auth()andcreate_packagecallsrequire_admin_or_distributor(&operator)— but the adapter signed every transaction with oneSOROBAN_ADMIN_SECRET_KEYkeypair. A real recipient's claim or a distributor's package creation was therefore always rejected on-chain, and there was no XDR submission path to accept a user-signed transaction. Additionally,claimAidPackageencoded the wrong ABI (it passed onlyidto a(id, claimer)entrypoint). The smoke harness only "worked" by holding the recipient's secret itself, confirming the production gap.What was built
app/backend/src/onchain/:onchain.adapter.tsBuildUnsignedClaimTxParams/Result,BuildUnsignedCreatePackageParams/Result,SubmitSignedTxParams/Result) and the three newOnchainAdaptermethods.soroban.adapter.tsbuildPreparedTransaction(build+simulate+assemble, shared with the admin path),buildUnsignedClaimTx,buildUnsignedCreatePackageTx,submitSignedTx, and the auth-entry verifier (verifySorobanAuthEntries/verifyAuthEntrySignature/authEntrySignatureList) which reconstructs the exactHashIdPreimageSorobanAuthorizationpreimage the SDK'sauthorizeEntrysigns.submitContractOpwas split into the shared build/send helpers; the admin-signed path is unchanged for admin-only ops (disburse, etc.).claimAidPackagenow encodesclaim(id, claimer).soroban.adapter.signing.spec.ts(new)onchain.adapter.mock.tsonchain.adapter.mock.spec.tsSOROBAN_INTEGRATION.mdIntegration changes outside
soroban.adapter.tsonchain.adapter.ts— interface additions only (purely additive).onchain.adapter.mock.ts— implements the three new interface methods (required: the classimplements OnchainAdapter).onchain.adapter.mock.spec.ts/SOROBAN_INTEGRATION.md— test/docs additions.No existing runtime behavior was removed;
MockOnchainAdapterandSorobanAdapterare the only interface implementors, and theuseValueobject-literal mocks inapp/backend/test/are structural and unaffected.Acceptance criteria coverage
claimAidPackagecan submit a recipient-signedclaimtransaction without the backend holding the recipient's secret; the resulting package transitions toClaimed. (buildUnsignedClaimTx+submitSignedTx; covered bysoroban.adapter.signing.spec.ts— "submits a recipient-signed claim successfully". The on-chainClaimedtransition follows from the contract once the recipient-signed auth entry is accepted.)createAidPackagecan submit a distributor-signedcreate_packagefor a non-adminoperatorAddress. (buildUnsignedCreatePackageTx+submitSignedTxwithexpectedSigner= operator; covered by the "distributor-created packages" suite.)disburse,revoke,refund) still sign with the admin keypair. (Admin path preserved —submitContractOpstill builds, signs withgetKeypair(), and submits;disburseAidPackageunchanged.revoke/refundare not exposed by the adapter surface, so there is nothing to change for them.)soroban.adapter.signing.spec.ts— "rejects a claim whose auth entry was signed by the admin keypair, not the recipient" assertssendTransactionis never called; "submits a recipient-signed claim successfully" asserts the confirmed hash is returned.)SOROBAN_INTEGRATION.md) documents the signing model and the fee-sponsorship strategy. (New "Signing model" section.)Deliberately deferred
AidEscrowService+ controller + DTOs. The repo gates CI on OpenAPI-spec drift with a regenerated frontendapi.ts, so adding endpoints is a separate slice. The adapter (the interface the issue names) is complete and ready for that slice.build*andsubmitSignedTxcan invalidate it (tx_bad_seq). Submit promptly and rebuild on failure; the README documents this. A shared sequence manager is a follow-up.tools/testnet-smoke/index.jsstill signs withSOROBAN_RECIPIENT_SECRET_KEY; wiring it to the two-phase seam is a follow-up.app/mobile/app/frontendsigners are the eventual consumers.Test plan
npx jest— 569/569 passing (12 new tests for this feature: 9 signing-model + 3 mock-adapter)npx tsc --noEmit— no new type errors versus base (3 pre-existing errors in unrelated spec files)npx eslint(changed files) — 0 errors; 2 warnings, both pre-existing on the base filenpx prettier --check— cleannpx nest build— succeedsEnv vars / Notes
No new environment variables. The signing seam uses the existing
SOROBAN_ADMIN_SECRET_KEYfor fee sponsorship and envelope signing; the recipient/distributor key never touches the backend. Auth-entry signatures expire per the on-chainsignature_expiration_ledgerreturned by simulation — enforced by the network, not by the backend.