feat(onchain/aid_escrow): wire delegate module for recovery claims - #443
Merged
kilodesodiq-arch merged 1 commit intoAug 20, 2026
Merged
Conversation
…audit trail (ChainForgee#422) Add an admin-managed delegate (recovery) address per package. An unexpired delegate may authorise a claim on the recipient's behalf, with funds always paid out to the recipient. - `claim` now takes a `claimer` address and requires its auth; the recipient remains authorised. - New entrypoints: set_delegate, get_delegate, get_delegate_info, get_delegate_history, cleanup_expired_delegates. - Delegate state lives in parallel maps keyed by package id, so the persisted Package shape is unchanged and no migration is needed. - Assignments are rejected for claimed packages and when the delegate equals the recipient; every change is appended to an audit trail. - Updated claim scripts and README for the new claim signature. - Regenerated test snapshots to match the new claim ABI.
kilodesodiq-arch
approved these changes
Aug 20, 2026
kilodesodiq-arch
left a comment
Contributor
There was a problem hiding this comment.
Great Job Sir/Ma
6 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 #422
src/delegate.rsreferenced a nonexistentcrate::AidPackagetype and was never compiled into the contract —lib.rshad nomod delegate;, so the entire delegate/recovery module was dead code, absent from the ABI, and never exercised bycargo test. This PR reconciles the module against the realPackage/PackageStatustypes, wires it intoAidEscrow, and threads delegate authorisation through the claim path. The key design decision is that delegate state lives in parallel maps keyed by package id (dlgts,dlgexp,dlgh), not inside thePackagerecord, so the persistedPackageshape is unchanged and no storage migration is required.Why
Before this change the only claim paths were
claim(id)(requiringpackage.recipient.require_auth()) andclaim_with_proof(Merkle membership). A recipient who lost their key — or a field operator claiming on a recipient's behalf — had no supported path; the "recovery" story was a doc comment, not a feature. A naivemod delegate;would not even compile, because the module referencedcrate::AidPackage, which does not exist (lib.rsdefinesPackagewithidandclaim_starts_at). This PR completes the module against the real types and the realfinalize_claimflow rather than deleting it, preserving the recovery path the module already documented.What was built
app/onchain/contracts/aid_escrow/src/:delegate.rscrate::Package. Persistent keys:dlgts(Map<u64, Address>),dlgexp(Map<u64, u64>),dlgh(Vec).DelegateHistory.new_delegateis nowOption<Address>so a cleared delegate is auditable. Functions:is_authorised_claimer,set_delegate_with_expiry,get_delegate,get_delegate_info,get_delegate_history,clear_delegate,cleanup_expired_delegates. 10 in-file unit tests.lib.rsmod delegate;, exposes the five entrypoints (set_delegate,get_delegate,get_delegate_info,get_delegate_history,cleanup_expired_delegates), changesclaim(env, id)→claim(env, id, claimer), andfinalize_claimnow takes anactorand clears the delegate after a successful claim.app/onchain/contracts/aid_escrow/tests/:delegate_tests.rsset_delegaterejections, and history/audit records.tests/*.rsclaim(&id)→claim(&id, &recipient)for the new signature.Integration changes outside the module
src/lib.rs—claimABI changed (claim(id)→claim(id, claimer));finalize_claimgained anactorparameter and callsdelegate::clear_delegate. Unavoidable: delegate authorisation must thread through the claim entrypoint.scripts/claim.sh,scripts/testnet-invoke.sh— claim invocations now pass--claimer.app/onchain/README.md,contracts/aid_escrow/README.md— method reference and redeploy note.test_snapshots/— tracked snapshots regenerated for the new claim ABI; new snapshots added for the delegate tests.Acceptance criteria coverage
cargo testinapp/onchaincompilesdelegate.rs(or the file is deleted) — no module references a nonexistent type. (mod delegate;inlib.rs;delegate.rsusescrate::Package; compiles and its 10 unit tests run.)A registered, unexpired delegate can authorise a claim on a
Createdpackage, and the package transitions toClaimedexactly once. (delegate_tests.rs::registered_unexpired_delegate_can_claim_on_behalf_of_recipient,package_transitions_to_claimed_exactly_once)A delegate cannot claim an expired package, a
Claimedpackage, or after the delegate's ownexpires_at. (delegate_cannot_claim_an_expired_package,delegate_cannot_claim_after_recipient_already_claimed,delegate_cannot_claim_after_own_expiry)set_delegateis rejected forClaimedpackages and when the delegate equals the recipient. (set_delegate_is_rejected_for_claimed_packages,set_delegate_is_rejected_when_delegate_equals_recipient)New tests in
app/onchain/contracts/aid_escrow/tests/cover: delegate claims, delegate expiry, delegate-after-claim rejection, and history/audit records. (tests/delegate_tests.rs— 14 tests; audit records asserted inset_delegate_replaces_previous_and_audits_both_assignmentsandregistered_unexpired_delegate_can_claim_on_behalf_of_recipient)app/onchain/README.mdmethod reference lists the new entrypoint(s) and its auth column, and the deployed testnet contract note reflects any redeploy. (method table listsclaim(id, claimer),set_delegate,get_delegate,get_delegate_info,get_delegate_history,cleanup_expired_delegateswith auth; redeploy note added.)Deliberately deferred
app/backend/src/onchain/onchain.adapter.tsandtools/testnet-smoke/index.jsas downstream impact. Those are separate platform layers and will be a follow-up PR; the contract change is independently invokable viascripts/testnet-invoke.sh/soroban contract invoke.CDSBJ27…) does not yet expose the new entrypoints.Test plan
cargo test(inapp/onchain) — 181/181 passing (14 new integration tests intests/delegate_tests.rs; 10 unit tests insrc/delegate.rs, previously uncompiled)cargo fmt --all -- --check— cleancargo clippy --all-targets -- -D warnings— no warningscargo build --release --target wasm32-unknown-unknown— succeedsEnv vars / Notes
No new env vars or config keys. The
claimABI change is breaking:claim(id)→claim(id, claimer). Funds always pay out topackage.recipient, even when a delegate authorises the claim. Delegate state uses parallel maps, so existingPackagerecords remain readable with no migration. A redeploy (new wasm) is required before the new entrypoints are live on testnet.