Conversation
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>
n13
left a comment
There was a problem hiding this comment.
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:
-
[P1] Preserve ambiguous submission outcomes instead of telling the operator to re-run (
src/cli/airdrop.rs:695).submit_prebuilt_batch_transfer_callcan 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. -
[P1] A
409frommark-paidis 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 receives409; 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. -
[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 onlywait_for_transaction, whose success stage isInBestBlock. A normal reorg can remove that transfer after/mark-paidpermanently removes the claim from/unpaid, leaving the claimant unpaid. This command should require finality before marking, or persist and reconcile reorg-aware transaction state. -
[P2] Validate the planned chunks, not the complete transfer list (
src/cli/airdrop.rs:632). The command computesper_batchand advertises support for splitting arbitrarily large payout sets, but it passes all transfers tovalidate_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 -- --checkandtaplo 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>
|
898ef59 adds the follow-ups:
New tests cover the argv rejection and the still-listed filter. |
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Operator command that pays out recorded (claimed but unpaid) rewards and marks them paid on the claim server.
Flow
GET /unpaidfrom the claim server; keeps rows withstatus: recorded(zero-amount rows are skipped, a recorded row without a claim account is an error).--yesskips, default answer is No).utility.batchextrinsics sized by the chain's safebatched_calls_limit(--batch-sizecan 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.POST /mark-paid(Bearer admin token; 409 already-marked is treated as success).Safety ordering
--admin-token-file, chmod 600, orQUANTUS_AIRDROP_ADMIN_TOKEN) is required before anything is paid, so a completed payout can always be marked.mark-paidfails after payment the affected addresses are printed loudly and the command exits non-zero (re-running before marking would double-pay).--dry-runprints the full plan (rows, batches, raw amounts) without touching the chain or the server.Tests cover the
/unpaidwire-format parsing, recorded-row filtering, and the hundredths→raw-unit conversion (including sub-2-decimal and overflow errors). README documents the command../clippy.shand the full airdrop suite (29) pass.Made with Cursor