Problem
The aid_escrow contract ships a complete delegate/recovery module, app/onchain/contracts/aid_escrow/src/delegate.rs, that is never compiled into the contract and therefore is absent from the ABI. src/lib.rs contains no mod delegate; declaration and the #[contractimpl] impl AidEscrow block exposes no set_delegate, get_delegate, or delegate-aware claim path. Because Rust only compiles modules that are declared, delegate.rs is dead code: cargo test in app/onchain never exercises it and the generated AidEscrowClient exposes none of its functions.
The module is also not merely unwired — it cannot be wired in as-is. It references a type that does not exist:
// app/onchain/contracts/aid_escrow/src/delegate.rs
let package: crate::AidPackage = env.storage() // crate::AidPackage does not exist
.persistent()
.get(&package_key)
.unwrap();
The actual package type in src/lib.rs is Package (with id, claim_starts_at fields that AidPackage omits), so compiling delegate.rs would fail immediately.
Consequence: the delegate/recovery capability the module documents — "Either the primary recipient OR the delegate may authorise a claim" — does not exist on-chain. The only claim paths are claim(id) (requires package.recipient.require_auth()) and claim_with_proof(id, claimant, proof) (Merkle membership). A recipient who loses their key, or a field operator claiming on a recipient's behalf, has no supported path; the "recovery" story is a comment, not a feature.
Root cause
The module was written against a hypothetical AidPackage shape and never reconciled with the real Package struct or the contract's claim flow, then orphaned rather than removed or completed.
Why this is architecturally hard
- It is a contract-state and auth-boundary change, not a cleanup. Delegates change who can move escrowed funds, so the feature must thread through
finalize_claim (which currently does payout_recipient = package.recipient) and must be reconciled with claim_with_proof's Merkle path. A naive "declare mod delegate;" will not even compile because of the AidPackage mismatch.
- Two storage layouts exist.
delegate.rs uses KEY_DELEGATES/KEY_DELEGATE_HISTORY/KEY_DELEGATE_EXPIRY as Map<u64, Address> entries in persistent storage, while package state lives under (Symbol "pkg", u64). The design must decide whether delegates are part of the Package record or a parallel map, and how clear_delegate/cleanup_expired_delegates interact with the existing get_recipient_package_count/list_recipient_packages full scans.
- Migration. The contract is already deployed on testnet (
CDSBJ27PKTNFTRW6OKPCVXDRUSSRUIQUG6DW5PUTKLDXTDT23NQIS6JG). Adding an entrypoint is a new wasm + migrate()/redeploy decision under VERSIONING.md, not a source-only edit.
- Backend/tool coordination. If delegates are to be usable end-to-end,
app/backend/src/onchain/onchain.adapter.ts (the OnchainAdapter interface) and tools/testnet-smoke/index.js must expose the new call(s); otherwise the contract change is untestable from the rest of the platform.
Proposed design
Decide first whether to complete or remove. The codebase already documents the intended semantics in delegate.rs, so completing is plausible: add a DelegateRecord-compatible field to Package (or keep the parallel map), add admin entrypoints set_delegate(package_id, delegate, expires_at) and view get_delegate(package_id), and make claim accept the delegate when is_authorised_claimer(...) is true (delegate not expired, package still Created). A markdown table of the new ABI surface:
| Entrypoint |
Auth |
Notes |
set_delegate(package_id, delegate, expires_at) |
admin |
reject if package Claimed |
get_delegate(package_id) |
none |
return None if expired |
claim (extended) |
recipient OR delegate |
delegate must not be expired |
Downstream impact
Any new/changed entrypoint alters the Soroban ABI consumed by app/backend/src/onchain/soroban.adapter.ts (contract.call(method, ...)) and the deployment runbook docs/testnet-deploy-runbook.md. Removing delegate.rs instead has no ABI impact but closes the recovery path permanently; state that choice in the PR.
Acceptance criteria
Contract
Tests
Documentation
Out of scope
Merkle-allowlist amount binding, recipient query pagination, and delegate UI in the mobile app are separate issues.
Getting started
Files: app/onchain/contracts/aid_escrow/src/lib.rs, app/onchain/contracts/aid_escrow/src/delegate.rs, app/onchain/contracts/aid_escrow/tests/.
cd app/onchain
make test # cargo test -- --nocapture
make clippy # cargo clippy -- -D warnings
Good first files to read: src/lib.rs (the claim/finalize_claim flow and Package struct), then src/delegate.rs to see the intended semantics you must reconcile against it.
Problem
The
aid_escrowcontract ships a complete delegate/recovery module,app/onchain/contracts/aid_escrow/src/delegate.rs, that is never compiled into the contract and therefore is absent from the ABI.src/lib.rscontains nomod delegate;declaration and the#[contractimpl] impl AidEscrowblock exposes noset_delegate,get_delegate, or delegate-aware claim path. Because Rust only compiles modules that are declared,delegate.rsis dead code:cargo testinapp/onchainnever exercises it and the generatedAidEscrowClientexposes none of its functions.The module is also not merely unwired — it cannot be wired in as-is. It references a type that does not exist:
The actual package type in
src/lib.rsisPackage(withid,claim_starts_atfields thatAidPackageomits), so compilingdelegate.rswould fail immediately.Consequence: the delegate/recovery capability the module documents — "Either the primary recipient OR the delegate may authorise a claim" — does not exist on-chain. The only claim paths are
claim(id)(requirespackage.recipient.require_auth()) andclaim_with_proof(id, claimant, proof)(Merkle membership). A recipient who loses their key, or a field operator claiming on a recipient's behalf, has no supported path; the "recovery" story is a comment, not a feature.Root cause
The module was written against a hypothetical
AidPackageshape and never reconciled with the realPackagestruct or the contract's claim flow, then orphaned rather than removed or completed.Why this is architecturally hard
finalize_claim(which currently doespayout_recipient=package.recipient) and must be reconciled withclaim_with_proof's Merkle path. A naive "declaremod delegate;" will not even compile because of theAidPackagemismatch.delegate.rsusesKEY_DELEGATES/KEY_DELEGATE_HISTORY/KEY_DELEGATE_EXPIRYasMap<u64, Address>entries in persistent storage, while package state lives under(Symbol "pkg", u64). The design must decide whether delegates are part of thePackagerecord or a parallel map, and howclear_delegate/cleanup_expired_delegatesinteract with the existingget_recipient_package_count/list_recipient_packagesfull scans.CDSBJ27PKTNFTRW6OKPCVXDRUSSRUIQUG6DW5PUTKLDXTDT23NQIS6JG). Adding an entrypoint is a new wasm +migrate()/redeploy decision underVERSIONING.md, not a source-only edit.app/backend/src/onchain/onchain.adapter.ts(theOnchainAdapterinterface) andtools/testnet-smoke/index.jsmust expose the new call(s); otherwise the contract change is untestable from the rest of the platform.Proposed design
Decide first whether to complete or remove. The codebase already documents the intended semantics in
delegate.rs, so completing is plausible: add aDelegateRecord-compatible field toPackage(or keep the parallel map), add admin entrypointsset_delegate(package_id, delegate, expires_at)and viewget_delegate(package_id), and makeclaimaccept the delegate whenis_authorised_claimer(...)is true (delegate not expired, package stillCreated). A markdown table of the new ABI surface:set_delegate(package_id, delegate, expires_at)Claimedget_delegate(package_id)Noneif expiredclaim(extended)Downstream impact
Any new/changed entrypoint alters the Soroban ABI consumed by
app/backend/src/onchain/soroban.adapter.ts(contract.call(method, ...)) and the deployment runbookdocs/testnet-deploy-runbook.md. Removingdelegate.rsinstead has no ABI impact but closes the recovery path permanently; state that choice in the PR.Acceptance criteria
Contract
cargo testinapp/onchaincompilesdelegate.rs(or the file is deleted) — no module references a nonexistent type.Createdpackage, and the package transitions toClaimedexactly once.Claimedpackage, or after the delegate's ownexpires_at.set_delegateis rejected forClaimedpackages and when the delegate equals the recipient.Tests
app/onchain/contracts/aid_escrow/tests/cover: delegate claims, delegate expiry, delegate-after-claim rejection, and history/audit records.Documentation
app/onchain/README.mdmethod reference lists the new entrypoint(s) and its auth column, and the deployed testnet contract note reflects any redeploy.Out of scope
Merkle-allowlist amount binding, recipient query pagination, and delegate UI in the mobile app are separate issues.
Getting started
Files:
app/onchain/contracts/aid_escrow/src/lib.rs,app/onchain/contracts/aid_escrow/src/delegate.rs,app/onchain/contracts/aid_escrow/tests/.Good first files to read:
src/lib.rs(theclaim/finalize_claimflow andPackagestruct), thensrc/delegate.rsto see the intended semantics you must reconcile against it.