Skip to content

Add airdrop payout commands (pull, review, pay, mark-paid) - #167

Open
n13 wants to merge 1 commit into
mainfrom
n13/airdrop-payout
Open

n13 wants to merge 1 commit into
mainfrom
n13/airdrop-payout

Conversation

@n13

@n13 n13 commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Operator side of the airdrop, as four separate quantus airdrop commands so every step can be checked by hand. A manifest file carries the state between them: pulled → approved → paying → paid → marked.

Step Command What it does
Pull airdrop pull [--out f] [--limit N] GET /unpaid, keeps status: recorded rows only, aggregates rewards by claim_account (one transfer per destination), writes the manifest. Refuses to run while another manifest in the directory is not yet marked.
Review airdrop review --manifest f [--approve] Prints the transfers, re-fetches /unpaid and fails on any reward that was paid, re-claimed or changed since the pull, encodes the batch to report its size against the 8 KiB cold-wallet payload cap. --approve stamps a SHA-256 of the transfers.
Pay airdrop pay --manifest f --from wallet [--recover] Requires the approval stamp, re-checks the server once more, then submits one utility.batch_all through the existing submit path (cold wallets go over QR) and waits for finalization. Records signer, nonce, anchor block, then tx and block hash.
Mark paid airdrop mark-paid --manifest f [--admin-token-file p] POST /mark-paid per reward with the bearer token from the file or AIRDROP_ADMIN_TOKEN. Idempotent and re-runnable; 409 already marked paid counts as done.

Atomicity and double-pay guards

  • The on-chain payment is a single batch_all: every transfer lands or none does, and a revert surfaces as a command failure through the existing ExtrinsicFailed check.
  • Pay writes paying (with signer, nonce and anchor block) before submitting. If the process dies, the manifest cannot be paid again; pay --recover scans the finalized blocks from the anchor to the mortality horizon for an extrinsic with the same signer and identical call bytes, then either records it as paid, resets to approved if it reverted, or resets to approved once the transaction can no longer be included.
  • Mark-paid persists progress after every address, so a crash mid-way resumes where it stopped.

Also in this PR

  • DEFAULT_SERVER for all airdrop commands is now https://airdrop-claim.quantus.com (what the mobile app uses) instead of localhost.
  • /snapshot and /unpaid share one JSON GET helper.
  • QuantusClient::get_block_hash replaces the inline chain_getBlockHash calls in block.rs and collect_rewards_lib.rs.
  • README section for operators.

Test plan

  • SKIP_CIRCUIT_BUILD=1 cargo test --lib: 371 passed (7 new unit tests: aggregation, drift detection, approval hash, manifest lock, mark-paid outcome classification, cold payload budget).
  • SKIP_CIRCUIT_BUILD=1 cargo clippy --all-targets --locked -- -D warnings, cargo +nightly fmt.
  • Read-only against production: airdrop pull wrote a manifest with 150 transfers covering 286 rewards (5577.96 QUAN); a second pull in the same directory is refused; airdrop review against wss://a1-planck.quantus.cat passes the drift check and reports a 6277-byte batch that fits a cold-wallet payload.
  • pay and mark-paid end to end against the dev claim server (data/dev_dummy.csv) and a dev node, hot wallet and developer cold-sign-sim.
  • pay --recover after killing pay between signing and finalization.

Not in this PR

Server side: a batch mark-paid endpoint that takes the address list plus tx hash in one SQLite transaction (and a tx_hash column), so step 4 becomes a single call. The per-address loop here works with the server as it is today.

Operator side of the airdrop. `pull` turns the claim server's unpaid recorded
claims into a manifest (one transfer per payout account), `review` re-checks
it against the server and the chain and approves it, `pay` submits it as a
single utility.batch_all from a hot or cold wallet and waits for finalization,
and `mark-paid` marks every reward in it paid on the server.

The manifest carries the state between steps. A new pull is refused while
another manifest in the directory is not yet marked, pay re-verifies the
server right before signing and checks the approval hash, and `pay --recover`
resolves an interrupted payment by scanning the finalized blocks the batch
could have landed in for the exact signer and call bytes.

Also default the claim server to https://airdrop-claim.quantus.com, share one
JSON GET between /snapshot and /unpaid, and add QuantusClient::get_block_hash
in place of the inline chain_getBlockHash calls.
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.

1 participant