Repository navigation
SorobanAdapter signs every transaction with the admin keypair: recipient and distributor auth cannot be satisfied #428
Description
Problem
SorobanAdapter signs every submitted transaction with a single admin keypair, which the aid_escrow contract's require_auth() checks will reject for the two operations that must be signed by someone else. In soroban.adapter.ts:
private getKeypair(): Keypair {
if (!this.keypair) this.keypair = Keypair.fromSecret(this.adminSecretKey);
return this.keypair;
}
// ...
preparedTx.sign(kp); // ← always the SOROBAN_ADMIN_SECRET_KEY keypairThe contract requires the recipient to authorise claims and the operator to authorise package creation:
// app/onchain/contracts/aid_escrow/src/lib.rs
package.recipient.require_auth(); // in claim()
Self::require_admin_or_distributor(&env, &operator)?; // operator.require_auth() in create_package()Consequence: the backend can only successfully submit operations where the admin is the required signer (disburse, revoke, refund, fund, config). claimAidPackage submits claim signed by the admin, which fails recipient.require_auth() for any real recipient; createAidPackage with a distributor operatorAddress fails require_admin_or_distributor because the admin keypair is not that distributor. There is no method to accept or inject a recipient/distributor-signed transaction (no XDR submission path, no fee-bump/sponsorship option), so the backend cannot orchestrate recipient claims or distributor-created packages for end users who hold their own keys. By contrast, tools/testnet-smoke/index.js only completes the claim flow by holding the recipient's secret itself (SOROBAN_RECIPIENT_SECRET_KEY), confirming the gap in the production adapter.
Root cause
The adapter assumes a single server-side key for all flows, but the contract's auth model is per-caller: recipient claims and distributor operations require the caller's signature, which the backend does not possess.
Why this is architecturally hard
- This is a signing-orchestration design, not a key swap. The backend must either (a) accept an already-built, user-signed transaction XDR and submit/sponsor it, or (b) return an unsigned/stub transaction for the client (mobile WalletConnect / Freighter) to sign. Each choice changes the
OnchainAdapterinterface, which today returns{ transactionHash, status }from a server-side submit. - It crosses the trust boundary. Holding recipient keys server-side would defeat the wallet model; the fix must define a sponsorship/fee-bump strategy (who pays fees) and a nonce/sequence management story for client-built transactions.
- The interface is the contract.
OnchainAdapter.claimAidPackage/createAidPackagesignatures would need a new "unsigned XDR" or "submit signed XDR" mode, rippling intoOnchainService,AidEscrowService, the BullMQOnchainProcessor, and everyONCHAIN_ADAPTER_TOKENmock inapp/backend/test/. - Parity with the smoke harness.
tools/testnet-smoke/index.jsalready models the correct recipient-signed claim; the production path must converge with it or the two will keep diverging.
Proposed design
Extend the adapter with a client-signing seam: e.g. buildUnsignedClaimTx(packageId, recipient) -> xdr and submitSignedTx(xdr), or a claimAidPackage overload that accepts a recipient-signed envelope. Sponsor fees via the admin account (fee bump) rather than requiring the recipient to hold XLM. Document the sequence for mobile WalletConnect and Freighter callers. Keep the existing admin-signed methods for admin-only operations.
Downstream impact
OnchainAdapter (app/backend/src/onchain/onchain.adapter.ts) is consumed by OnchainService, AidEscrowService, OnchainProcessor, ClaimsService, and every test mock under app/backend/test/. Any interface change must update those mocks. The mobile app (app/mobile/src/services/walletConnect.ts) and frontend (app/frontend/src/lib/walletStore.ts) are the eventual signers.
Acceptance criteria
Service
-
claimAidPackagecan submit a recipient-signedclaimtransaction without the backend holding the recipient's secret; the resulting package transitions toClaimed. -
createAidPackagecan submit a distributor-signedcreate_packagefor a non-adminoperatorAddress. - Admin-only operations (
disburse,revoke,refund) still sign with the admin keypair.
Tests
- 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).
Documentation
- The adapter README (
SOROBAN_INTEGRATION.md) documents the signing model and the fee-sponsorship strategy.
Out of scope
Deleting the broken SorobanOnchainAdapter stub and ledger reconciliation are separate issues.
Getting started
Files: app/backend/src/onchain/soroban.adapter.ts, app/backend/src/onchain/onchain.adapter.ts, app/backend/src/onchain/onchain.service.ts, tools/testnet-smoke/index.js.
cd app/backend
npm testGood first files to read: soroban.adapter.ts submitContractOp/claimAidPackage, then tools/testnet-smoke/index.js for the recipient-keypair claim pattern to generalise.
Activity
P3az3 commentedon Aug 19, 2026
Please assign me this issue. I will do it well and professionally please. Thanks
snowrugar-beep commentedon Aug 19, 2026
Hello maintainer, please assign me this issue, I have professional experience please.
grantfox-oss commentedon Aug 20, 2026
🦊 GrantFox — @P3az3 has been assigned to this issue as part of the Third Campaign campaign!
Next steps:
Closes #428)Good luck! Track your progress on GrantFox.
grantfox-oss commentedon Aug 20, 2026
🎉 This issue has been marked as completed on GrantFox as part of the Third Campaign campaign!
@P3az3's PR #450 was approved and merged by @kilodesodiq-arch.
🏆 @P3az3: You earned 35 FoxPoints for this contribution! Your current tier: Explorer (484 total points). Track your full progress on GrantFox.
👏 Great work, @P3az3! Keep contributing to ChainForgee.