Skip to content

Add airdrop pay command - #168

Open
illuzen wants to merge 5 commits into
mainfrom
illuzen/airdrop-pay
Open

illuzen wants to merge 5 commits into
mainfrom
illuzen/airdrop-pay

Conversation

@illuzen

@illuzen illuzen commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Operator command that pays out recorded (claimed but unpaid) rewards and marks them paid on the claim server.

Flow

  1. GET /unpaid from the claim server; keeps rows with status: recorded (zero-amount rows are skipped, a recorded row without a claim account is an error).
  2. Presents every pending payout for review — rewarded address → payout account, amount, scheme, verification time — plus the total and the count of still-unclaimed snapshot rows, then asks for confirmation (--yes skips, default answer is No).
  3. Chunks the transfers into utility.batch extrinsics sized by the chain's safe batched_calls_limit (--batch-size can lower it; higher values are capped). Balance is checked up front against the full total plus tips with a fee estimate on the first batch.
  4. Submits each batch through the shared submit stage, so cold wallets work unchanged — one QR roundtrip per batch extrinsic. Every batch is waited on until it is in a block.
  5. After a batch lands, each of its claims is marked paid via POST /mark-paid (Bearer admin token; 409 already-marked is treated as success).

Safety ordering

  • The admin token (--admin-token-file, chmod 600, or QUANTUS_AIRDROP_ADMIN_TOKEN) is required before anything is paid, so a completed payout can always be marked.
  • A claim is only marked after its transfer is in a block; if mark-paid fails after payment the affected addresses are printed loudly and the command exits non-zero (re-running before marking would double-pay).
  • If a batch fails, earlier batches are already marked, so re-running continues where it left off; the error message says exactly what state things are in.
  • --dry-run prints the full plan (rows, batches, raw amounts) without touching the chain or the server.

Tests cover the /unpaid wire-format parsing, recorded-row filtering, and the hundredths→raw-unit conversion (including sub-2-decimal and overflow errors). README documents the command. ./clippy.sh and the full airdrop suite (29) pass.

Made with Cursor

Fetches the claim server's /unpaid list, presents recorded (claimed but
unpaid) payouts for review with totals and verification times, batches the
transfers into utility.batch extrinsics sized by the chain's safe limit
(overridable with --batch-size), and marks each claim paid once its batch
is in a block.

The admin token (file or QUANTUS_AIRDROP_ADMIN_TOKEN) is loaded before any
payment so completed payouts can always be marked; batches are waited on
before mark-paid; mark-paid failures after payment are reported loudly and
fail the command. Cold wallets work through the shared submit stage (one QR
roundtrip per batch). --dry-run prints the plan without submitting.

Co-authored-by: Cursor <cursoragent@cursor.com>
@illuzen illuzen added the bot-review Request automated review from review-bot label Sep 22, 2026

@n13 n13 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.

Reviewer model: GPT 5.6 Sol

REQUEST_CHANGES: the payout state transition is unsafe across ambiguous, concurrent, and non-final transaction outcomes, so the current retry behavior can double-pay claims or mark unpaid transfers as paid.

Blocking findings:

  1. [P1] Preserve ambiguous submission outcomes instead of telling the operator to re-run (src/cli/airdrop.rs:695). submit_prebuilt_batch_transfer_call can return an error after the node accepted the transaction—for example, when the status stream times out or ends before inclusion is observed. The shared watcher explicitly says that such a transaction may still execute, but this wrapper unconditionally changes that into “nothing from this batch was paid — re-run to continue.” Because these rows have not been marked paid, following that instruction can submit every transfer again. Carry the submitted hash/outcome through this path and require on-chain reconciliation before retrying; only claim that nothing was paid for a provable rejection or dispatch failure. Please cover a post-submission timeout/stream-loss case.

  2. [P1] A 409 from mark-paid is not safe evidence under concurrent operators (src/cli/airdrop.rs:523). Two runs can fetch the same recorded rows, both submit successful transfers, then have one mark the row while the other receives 409; the second run currently treats that as success and reports no warning even though it made the duplicate payment. The rows need a server-side reservation/idempotency protocol before signing/submission (and the completion should be tied to that reservation/transaction), rather than interpreting “already marked” as success. Please add a concurrency regression test.

  3. [P1] Do not irreversibly mark claims after only best-block inclusion (src/cli/airdrop.rs:670). Unless the user separately supplied --finalized-tx, this forces only wait_for_transaction, whose success stage is InBestBlock. A normal reorg can remove that transfer after /mark-paid permanently removes the claim from /unpaid, leaving the claimant unpaid. This command should require finality before marking, or persist and reconcile reorg-aware transaction state.

  4. [P2] Validate the planned chunks, not the complete transfer list (src/cli/airdrop.rs:632). The command computes per_batch and advertises support for splitting arbitrarily large payout sets, but it passes all transfers to validate_batch_transfer_request, which rejects any list larger than the runtime's single-batch maximum before the chunk loop starts. Validate each chunk (or split address validation from the per-extrinsic count check) so the chunking path is reachable above one maximum-sized batch.

Validation performed at 7637180f126878c65a6bc7f02c7afb458b7bbbc4:

  • git diff --check — passed.
  • cargo +nightly-2026-08-31 fmt --all -- --check and taplo format --check --config taplo.toml — passed.
  • SKIP_CIRCUIT_BUILD=1 cargo test --locked cli::airdrop::tests -- --nocapture — passed (28 library + 28 binary test instances).
  • SKIP_CIRCUIT_BUILD=1 cargo clippy --all-targets --locked -- -D warnings — passed.
  • All GitHub checks are successful on this head.

The existing tests cover parsing/filtering/conversion, but not the blocking payout-state transitions above.

After all batches land and claims are marked, pay re-fetches /unpaid and
fails loudly if any paid address is still listed (it would be paid again on
the next run). A hidden --admin-token arg is rejected with guidance toward
--admin-token-file / QUANTUS_AIRDROP_ADMIN_TOKEN, matching the password
convention.

Co-authored-by: Cursor <cursoragent@cursor.com>
@illuzen

illuzen commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

898ef59 adds the follow-ups:

  • Post-payout server verification: after every batch is in a block and its claims are marked, pay re-fetches GET /unpaid and verifies none of the paid addresses are still listed (any status). Anything still present is printed loudly and the command exits non-zero, since a re-run would pay it again.
  • Bearer token as an arg: --admin-token exists as a hidden arg but is rejected with guidance toward --admin-token-file (chmod 600) or QUANTUS_AIRDROP_ADMIN_TOKEN, matching the --password convention — argv values leak into shell history and process listings. Mark-paid itself was already in: the token is loaded before any payment goes out, and each claim is marked via POST /mark-paid (Bearer) only after its batch lands.

New tests cover the argv rejection and the still-listed filter. ./clippy.sh and the airdrop suite (31) pass.

@n13 n13 removed the bot-review Request automated review from review-bot label Sep 22, 2026
illuzen and others added 3 commits September 22, 2026 18:16
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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.

2 participants