Skip to content

feat(onchain): recipient and distributor client-signing seam - #450

Merged
kilodesodiq-arch merged 4 commits into
ChainForgee:mainfrom
P3az3:fix/issue-428-soroban-adapter-signing
Aug 20, 2026
Merged

feat(onchain): recipient and distributor client-signing seam#450
kilodesodiq-arch merged 4 commits into
ChainForgee:mainfrom
P3az3:fix/issue-428-soroban-adapter-signing

Conversation

@P3az3

@P3az3 P3az3 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #428

SorobanAdapter now exposes a client-signing seam: buildUnsignedClaimTx / buildUnsignedCreatePackageTx return an unsigned, simulated envelope whose Soroban auth entries demand the caller's signature, and submitSignedTx verifies 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) calls claimer.require_auth() and create_package calls require_admin_or_distributor(&operator) — but the adapter signed every transaction with one SOROBAN_ADMIN_SECRET_KEY keypair. 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, claimAidPackage encoded the wrong ABI (it passed only id to 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/:

File What it contains
onchain.adapter.ts New public types (BuildUnsignedClaimTxParams/Result, BuildUnsignedCreatePackageParams/Result, SubmitSignedTxParams/Result) and the three new OnchainAdapter methods.
soroban.adapter.ts buildPreparedTransaction (build+simulate+assemble, shared with the admin path), buildUnsignedClaimTx, buildUnsignedCreatePackageTx, submitSignedTx, and the auth-entry verifier (verifySorobanAuthEntries / verifyAuthEntrySignature / authEntrySignatureList) which reconstructs the exact HashIdPreimageSorobanAuthorization preimage the SDK's authorizeEntry signs. submitContractOp was split into the shared build/send helpers; the admin-signed path is unchanged for admin-only ops (disburse, etc.). claimAidPackage now encodes claim(id, claimer).
soroban.adapter.signing.spec.ts (new) 9 tests using the real stellar-sdk crypto/XDR with only the RPC server mocked.
onchain.adapter.mock.ts Deterministic implementations of the three seam methods so mock-mode development keeps working.
onchain.adapter.mock.spec.ts 3 tests for the new mock methods.
SOROBAN_INTEGRATION.md Documents the signing model, fee-sponsorship strategy, and the exact client-side signing sequence.

Integration changes outside soroban.adapter.ts

  • onchain.adapter.ts — interface additions only (purely additive).
  • onchain.adapter.mock.ts — implements the three new interface methods (required: the class implements OnchainAdapter).
  • onchain.adapter.mock.spec.ts / SOROBAN_INTEGRATION.md — test/docs additions.

No existing runtime behavior was removed; MockOnchainAdapter and SorobanAdapter are the only interface implementors, and the useValue object-literal mocks in app/backend/test/ are structural and unaffected.

Acceptance criteria coverage

  • claimAidPackage can submit a recipient-signed claim transaction without the backend holding the recipient's secret; the resulting package transitions to Claimed. (buildUnsignedClaimTx + submitSignedTx; covered by soroban.adapter.signing.spec.ts — "submits a recipient-signed claim successfully". The on-chain Claimed transition follows from the contract once the recipient-signed auth entry is accepted.)
  • createAidPackage can submit a distributor-signed create_package for a non-admin operatorAddress. (buildUnsignedCreatePackageTx + submitSignedTx with expectedSigner = operator; covered by the "distributor-created packages" suite.)
  • Admin-only operations (disburse, revoke, refund) still sign with the admin keypair. (Admin path preserved — submitContractOp still builds, signs with getKeypair(), and submits; disburseAidPackage unchanged. revoke/refund are not exposed by the adapter surface, so there is nothing to change for them.)
  • A test proves a claim signed only by the admin keypair is rejected, and one signed by the recipient succeeds (mock the RPC/sign boundary). (soroban.adapter.signing.spec.ts — "rejects a claim whose auth entry was signed by the admin keypair, not the recipient" asserts sendTransaction is never called; "submits a recipient-signed claim successfully" asserts the confirmed hash is returned.)
  • The adapter README (SOROBAN_INTEGRATION.md) documents the signing model and the fee-sponsorship strategy. (New "Signing model" section.)

Deliberately deferred

  • REST/service wiring — exposing the seam through AidEscrowService + controller + DTOs. The repo gates CI on OpenAPI-spec drift with a regenerated frontend api.ts, so adding endpoints is a separate slice. The adapter (the interface the issue names) is complete and ready for that slice.
  • Nonce/sequence manager — the unsigned envelope pins the admin's sequence at build time; a concurrent admin-signed submission between build* and submitSignedTx can invalidate it (tx_bad_seq). Submit promptly and rebuild on failure; the README documents this. A shared sequence manager is a follow-up.
  • Smoke-harness paritytools/testnet-smoke/index.js still signs with SOROBAN_RECIPIENT_SECRET_KEY; wiring it to the two-phase seam is a follow-up.
  • Mobile/Freighter clients — the README documents the exact client signing sequence; the app/mobile / app/frontend signers 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 file
  • npx prettier --check — clean
  • npx nest build — succeeds

Env vars / Notes

No new environment variables. The signing seam uses the existing SOROBAN_ADMIN_SECRET_KEY for fee sponsorship and envelope signing; the recipient/distributor key never touches the backend. Auth-entry signatures expire per the on-chain signature_expiration_ledger returned by simulation — enforced by the network, not by the backend.

P3az3 and others added 4 commits August 20, 2026 18:36
…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.

@kilodesodiq-arch kilodesodiq-arch left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@kilodesodiq-arch
kilodesodiq-arch merged commit 2fdb5a5 into ChainForgee:main Aug 20, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SorobanAdapter signs every transaction with the admin keypair: recipient and distributor auth cannot be satisfied

2 participants