Skip to content

Testnet mining rewards claim flow - #656

Merged
n13 merged 11 commits into
mainfrom
n13/mining-rewards-claim
Sep 17, 2026
Merged

n13 merged 11 commits into
mainfrom
n13/mining-rewards-claim

Conversation

@n13

@n13 n13 commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

What

The testnet mining rewards claim flow, built to the Figma "Airdrop Flow" designs (section 546:5277).

  1. Settings → Airdrop (right after Wallet) → entry page with the hero copy → Check eligibility.
  2. Wallet list when there is more than one software wallet: initials badge, name, account count.
  3. Wallet rewards screen: "CHECKING N ADDRESSES" with a chain counter while the four chains resolve, then "TOTAL REWARDS" with per-chain payout and blocks. Tapping a chain opens a bottom sheet with blocks, reward and every owned address with its payout (address formats changed across testnets, so one wallet often owns several rows per chain). A wallet with no rows anywhere gets the "No mining on this wallet" page. A wallet that already submitted sees "CLAIMED {date}", where it is paying to, and the next payout day instead of a new check.
  4. Pay rewards to: total, "Enter the address we should send the rewards to", every transparent and encrypted account grouped by wallet, active account preselected; Use Another Address opens a typed-address page with inline validation ("This isn't a valid QTC address…").
  5. Confirm Destination: claiming amount, destination with checkphrase, TO / AMOUNT / PAYOUT (Monday). Submit Claim proves and posts; a failure shows the "Couldn't submit right now" page with Try Again (already-recorded addresses are idempotent on the server). Success (every address answered 200 or 409) shows the submitted page and stores the claim record for that wallet.

Every state was walked on the iOS simulator against the Figma frames using the debug outcome picker on the Mining Rewards entry page (AppConstants.debugMiningRewards, debug builds only): mixed / some / below / none / claimed / fail.

Server client (SDK)

AirdropClaimService in quantus_sdk owns the wire contract with airdrop-claim: one request per claimable matched address, duplicates collapsed, Dilithium claims signed from the mnemonic right before each request (10-minute expiry inside the server's 15-minute window), wormhole claims carrying the in-process ownership proof, and the server's {"error"} text surfaced on rejection. The server keeps one row per address and answers a repeat with 409, so 200 and 409 both count as submitted; every address is tried on every attempt, and a failure reports how many got through. The confirm page then says "Some claims have been submitted, please try again" (or "Couldn't submit right now" when none did), and the submitted page opens with "All claims have been submitted." Verified end to end by test/services/airdrop_claim_server_test.dart against a locally built airdrop-check (skipped unless AIRDROP_CHECK_BIN is set).

Data

  • One table per chain under mobile-app/assets/testnet_data/, loaded on demand. Only address,blocks,reward ships: tool/trim_miner_stats.dart rewrites the raw "Miner Stats" exports down to those three columns. Drop in fresh exports, run dart run tool/trim_miner_stats.dart from mobile-app, commit.
  • The reader (shared/utils/miner_stats_csv.dart) accepts raw and trimmed forms, finding the address, blocks and mainnet-reward columns by keyword.
  • A share that is blank or rounds to 0.00 is floored to 0.1 QTC as a thank-you, once per testnet. The server snapshot carries the same floor (airdrop-claim PR Refactor1 #3, merged), since it rejects zero-reward rows.
  • Matching runs locally in Rust across every historical key era. An address from an era the server does not accept yet (early-Planck rate-4 wormhole) shows "Claim not available yet" in the sheet and is left out of the totals and the claim.

Groundwork

  • AirdropClaimRecord stored per wallet through SettingsService (cleared with the wallet, invalidated on logout).
  • BottomSheetContainer gained a trailing slot for the sheet's close button.
  • NumberFormattingService.formatInteger / formatHundredths, DatetimeFormattingService.formatDayMonth.
  • SelectWalletScreen takes a destination and the one-or-many wallet navigation lives in pushForSoftwareWallet; wallet settings uses the same helper.
  • The claim endpoint is https://airdrop-claim.quantus.com.

Tests

  • SDK: claim client body shapes and error handling, the end-to-end server test, claim record round-trip, date helper.
  • App: CSV parsing and trimming; service rows, sums, unclaimable eras and record writing; wallet screen states (counter, results, sheet, no mining, row retry, already claimed); pay-to (sections, preselection, Continue, Use Another Address); typed address (invalid / valid); confirm (submit, failed page + Try Again); number formatters.

n13 added 6 commits September 15, 2026 18:13
Settings gets its Mining Rewards entry back. Check eligibility picks a
software wallet (directly, or through the wallet list when there are
several), then shows per testnet how many blocks that wallet mined and
what it pays out. Claim lets the user pay the reward to any of their
transparent or encrypted accounts or a typed address, proves ownership
of each matched miner address with the SDK airdrop API, and posts the
claims to the airdrop-claim server.

Payouts come from the bundled "Miner Stats" CSV exports, one per chain,
loaded on demand. The exports name their columns differently, so the
parser finds address, blocks and mainnet reward by keyword. A share that
rounds to nothing is floored to 0.1 as a thank-you.

The wallet picker and the one-or-many wallet navigation move out of
wallet settings into shared helpers so both flows use them.
The raw exports carry testnet payouts, pool shares and cumulative
columns the wallet never reads. tool/trim_miner_stats.dart rewrites the
files in assets/testnet_data down to `address,blocks,reward`; the CSV
reader moves into shared/utils so the tool and the service parse both
the raw exports and the trimmed form with one implementation.
Address formats changed several times across the testnets, so one
wallet often owns more than one row of a chain's table. Each chain now
keeps its owned rows and lists them, shortened, with their payout under
the chain line. A row whose key era the claim server does not accept
yet is labelled instead of priced, and stays out of the chain total and
the claim gate so the flow never offers a reward it cannot collect.
…p-claim

AirdropClaimService owns the POST /claim contract: one request per
claimable matched address, Dilithium claims signed from the mnemonic
right before they are sent so the expiry stays inside the server's
window, wormhole ownership proofs built in-process, and the server's
{"error"} body surfaced on rejection. The wallet's mining rewards
service now only resolves the recovery phrase and delegates.

A server test spawns a locally built airdrop-check with a snapshot
holding one Dilithium and one wormhole address of a test mnemonic, runs
the real matcher and provers, and checks both claims are recorded for
the payout address and refused a second time. It is skipped unless
AIRDROP_CHECK_BIN points at the binary.
The server keeps one row per address and answers a repeat with 409.
A submission that failed halfway used to hit that on the addresses it
had already recorded and stop before reaching the missing ones. A 409
now counts as done and the loop carries on; the server test retries
the full set and checks nothing changes.
Settings gets the Airdrop row with its icon, right after Wallet. The
entry page carries the hero copy; the wallet list shows initials and
account counts. One wallet screen now covers the check (chain counter,
rows filling in), the results (total, per-chain payout, blocks), the
per-chain bottom sheet listing every owned address with its payout,
the no-mining page, and the already-claimed page fed by a claim record
stored per wallet. Claiming goes pay-to (own accounts, active one
preselected, or another typed address with inline validation), then a
confirm page that owns submission and shows the failed page with Try
Again when the server cannot be reached, then the submitted page.

Groundwork: AirdropClaimRecord stored through SettingsService and
cleared with the wallet, a trailing slot on BottomSheetContainer for
the sheet's close button, integer and hundredths formatters, a day and
month date helper, and a debug outcome picker on the entry page so every
state can be walked through without a mining wallet.
@n13 n13 added the bot-review Request automated review from review-bot label Sep 17, 2026

@n13 n13 left a comment

Copy link
Copy Markdown
Collaborator Author

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

Verdict (advisory): Request changes

Blocking findings:

  1. [P1] Gate the flow until the configured claim service is actually reachable. quantus_sdk/lib/src/constants/app_constants.dart:22 points production submissions at https://airdrop.quantus.com, while mobile-app/lib/v2/screens/settings/settings_screen.dart:79-95 exposes the entry unconditionally. At review time both authoritative quantus.com nameservers return no A/AAAA record for that host, and an HTTPS /snapshot probe fails at DNS resolution. As written, every real Submit Claim attempt fails. Deploy and smoke-test the endpoint before exposing the row, or protect the feature with a remotely controlled availability gate.

  2. [P1] Make the bundled entitlement amounts match the server's authoritative snapshot. mobile-app/lib/shared/utils/miner_stats_csv.dart:42 floors every zero/missing per-testnet reward to 0.10, and the committed CSVs already contain that transformed output. Comparing this head's four assets with Quantus-Network/airdrop-claim main at 0f54db79df2e8645c4a8ed175fa969574f49de27 gives 10,039.20 QTC in the app versus 10,000.09 QTC on the server. In particular, 385 addresses receive 38.60 QTC in the app but zero on the server, and six funded addresses are overstated by another 0.51 QTC. The current server rejects zero-reward rows, and AirdropClaimService stops the batch on that response, so affected wallets can be shown a nonexistent entitlement and may not finish otherwise-valid claims. Coordinate the floor/aggregation change with the server before launch (or consume its /snapshot as the source of truth), and add an address-to-amount/hash consistency check against the deployed snapshot.

  3. [P1] Do not accept an unqualified 409 as successful idempotency. quantus_sdk/lib/src/services/airdrop_claim_service.dart:56 treats every Conflict as done, but the current server's 409 only says address already claimed; it neither returns nor checks the original claim_account. After a partial batch failure, the UI lets the user back out and choose another destination. A retry then leaves earlier rows bound to the old destination, records later rows for the new one, and mobile-app/lib/services/mining_rewards_service.dart:57-64 stores and displays the entire total as if it were going to the new destination. Lost local preferences/reinstallation creates the same false-success path. Make retries idempotent only when the recorded destination matches (for example, have the server return/validate the existing record), or persist and enforce one destination for the incomplete batch.

Validation on base 180d772a5c45d027462251bfa97076b4e8916e4c, head b9be6f589bc5b15f46fa4b577fa4860721a60d35:

  • git diff --check passed.
  • Workspace formatting passed (749 files, 0 changes).
  • 49 focused mobile mining-rewards tests passed.
  • 8 focused SDK claim/record/export tests passed.
  • Local analysis was capped at 10 seconds as required: cold-wallet, miner, and SDK completed cleanly before the mobile package started; GitHub's full Analyze check passed on this exact head.
  • GitHub dependency cooldown passed.

@n13 n13 removed the bot-review Request automated review from review-bot label Sep 17, 2026
n13 added 3 commits September 17, 2026 10:42
…s done

The server keeps one row per address and answers a repeat with a bare
409, so a retry after a partial failure could bind later addresses to a
new payout address while earlier ones stayed with the old one. The
client now reads GET /unpaid on a 409 and treats the address as done
only when the recorded payout address is the one being submitted;
otherwise the batch stops with AirdropClaimTaken naming the recorded
address. Failures carry how far the batch got, so the confirm page
locks the payout address once anything is recorded and, for a taken
address, offers to finish the claim to the recorded one.

The claim endpoint is the deployed airdrop-claim.quantus.com.
@n13 n13 added the bot-review Request automated review from review-bot label Sep 17, 2026

@n13 n13 left a comment

Copy link
Copy Markdown
Collaborator Author

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

Verdict (advisory): Request changes

The previously reported endpoint-availability blocker is resolved: airdrop-claim.quantus.com resolves and both /snapshot and /unpaid returned HTTP 200 during this review. Three blocking correctness issues remain.

  1. [P1] Make the bundled entitlements match the authoritative server snapshot. mobile-app/lib/shared/utils/miner_stats_csv.dart:42 floors every zero or missing per-testnet reward to 0.10, and the committed CSVs already contain that transformed output. This head's four assets aggregate to 10,039.20 QTC across 775 addresses, while the live /snapshot (SHA-256 12190d5f...e7672, matching Quantus-Network/airdrop-claim main at 0f54db79) contains 10,000.09 QTC across 390 addresses. The app gives 385 server-absent addresses 38.60 QTC in total (283 of them are non-Planck/Dilithium rows that become claimable when matched), and overstates six server-funded Dilithium addresses by another 0.51 QTC. Affected wallets are shown rewards the server rejects with 404, so a batch can never complete; the six shared addresses are also displayed and persisted at the wrong total. Coordinate the floor/aggregation change with the server before launch, or derive eligibility and amounts from /snapshot, and add an exact address-to-amount/hash consistency check.

  2. [P1] Do not treat a paid claim whose destination cannot be recovered as a successful retry. On a 409, quantus_sdk/lib/src/services/airdrop_claim_service.dart:87-92 accepts the conflict whenever recordedTo[match.address] is null. But _recordedDestinations explicitly reads /unpaid, whose contract omits claims after they are paid (:103-111); the new unit test at quantus_sdk/test/services/airdrop_claim_service_test.dart:108-112 codifies that missing row as success. After preferences are lost or the app is reinstalled, a user can pick any new destination, receive 409 for an already-paid address, and the app then stores and displays that new destination and the full amount as if it had just submitted there (mobile-app/lib/services/mining_rewards_service.dart:57-64). A 409 must count as done only when the server returns the original destination and it exactly matches, or the server needs a status/409 response that includes paid records so the UI can report the actual completed claim.

  3. [P1] Check the whole batch's existing destination before writing any new address. The loop posts addresses before it learns about a later conflict (quantus_sdk/lib/src/services/airdrop_claim_service.dart:79-99). With qza unclaimed and qzb already recorded to B, submitting the batch to A first records qza -> A, then discovers qzb -> B—the exact ordering exercised by airdrop_claim_service_test.dart:114-133. The UI's “Continue with that address” retry (mobile-app/lib/v2/screens/mining_rewards/mining_rewards_confirm_screen.dart:75-79) then immediately conflicts on qza -> A, so the batch is irreversibly split and cannot converge on either destination. Preflight all matched addresses and require zero or one existing payout destination before the first POST, or make the server accept the batch atomically.

Validation on base 180d772a5c45d027462251bfa97076b4e8916e4c, head 2de9de1eb6ff007f33852f3a01bd32c836f0ba4c:

  • git diff --check passed; dry-run formatting passed for all 42 changed Dart files.
  • 51 focused mobile mining-rewards/formatting tests passed.
  • 10 focused SDK claim/record/export tests passed; the optional local airdrop-check integration test was skipped because AIRDROP_CHECK_BIN was not configured.
  • Local analysis was capped at 10 seconds as required: cold-wallet, miner, and SDK completed cleanly before mobile analysis began; GitHub's full Analyze check passed on this exact head.
  • The live /snapshot and /unpaid probes passed, and the live snapshot source hash matches current airdrop-claim main. GitHub dependency cooldown checks passed.

@n13 n13 removed the bot-review Request automated review from review-bot label Sep 17, 2026
The claim server keeps one row per address and answers a repeat with
409, and nothing else produces one, so a 409 simply means that address
is submitted. The client no longer consults /unpaid or stops at the
first rejection: every address is tried on every attempt, 200 and 409
both count, and a failure carries how many got through. The confirm
page shows "Some claims have been submitted, please try again" when it
was partial, the submitted page opens with "All claims have been
submitted.", and Change Details stays available throughout.
@n13 n13 added the bot-review Request automated review from review-bot label Sep 17, 2026

@n13 n13 left a comment

Copy link
Copy Markdown
Collaborator Author

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

Verdict (advisory): Request changes

The entitlement source has been aligned on airdrop-claim/main, and the endpoint/CI issues from the earlier heads are resolved. Two blocking correctness issues remain.

  1. [P1] Do not report an unqualified 409 as a claim to the currently selected destination. quantus_sdk/lib/src/services/airdrop_claim_service.dart:64-80 treats every Conflict as submitted, while mobile-app/lib/v2/screens/mining_rewards/mining_rewards_confirm_screen.dart:173-180 deliberately leaves Change Details available after a partial submission and mobile-app/lib/services/mining_rewards_service.dart:57-64 persists the newly selected destination for the full total. The current server contract returns only {"error":"address already claimed"} on 409; it does not return or validate the recorded payout address. Therefore, if address A was recorded to X before address B failed, changing details to Y makes A's retry return 409, records B to Y, and then shows/stores the entire batch as “Paying to Y” even though it is split between X and Y. Reinstalling or losing preferences produces the same false-success result for a completed batch. The integration test at quantus_sdk/test/services/airdrop_claim_server_test.dart:77-82 explicitly demonstrates that submitting to another address returns success to the client while the server rows remain unchanged. Make the server's idempotent response expose/validate the original destination (including paid rows) and require an exact match before success, or provide an atomic batch/status contract; until then the client must not turn an unqualified 409 into a claim record for the chosen address.

  2. [P1] Keep the flow gated until the matching reward snapshot is live. The four bundled assets aggregate to 775 addresses and 10,039.20 QTC and now match Quantus-Network/airdrop-claim main at 01a7630e exactly. However, https://airdrop-claim.quantus.com/snapshot still serves source hash 12190d5f...e7672: 390 addresses and 10,000.09 QTC. There are 385 app-only addresses worth 38.60 QTC, plus six shared addresses overstated by another 0.51 QTC. Because quantus_sdk/lib/src/constants/app_constants.dart:21-22 points submissions at that live service and Settings exposes the flow unconditionally, affected users see entitlements that the server rejects and cannot complete the batch. Deploy and smoke-test the 775-address snapshot before this client can ship, or gate the entry on an exact snapshot version/hash.

Validation on base 180d772a5c45d027462251bfa97076b4e8916e4c, head 2b1cd8c53216430254318677af187e7b877510fc:

  • git diff --check passed.
  • Workspace dry-run formatting passed (749 Dart files, 0 changes).
  • 50 focused mobile mining-rewards/formatting tests passed.
  • 8 focused SDK claim/record/export tests passed. The optional local airdrop-check integration test was not run because AIRDROP_CHECK_BIN was not configured; its current server-contract assertions were inspected directly.
  • Local analysis was capped at 10 seconds as required: cold-wallet, miner, and SDK completed cleanly before mobile analysis began; GitHub's full Analyze check passed on this exact head.
  • Live snapshot and current airdrop-claim/main were rechecked immediately before posting; GitHub Analyze and dependency-cooldown checks passed.

@n13 n13 removed the bot-review Request automated review from review-bot label Sep 17, 2026
@n13

n13 commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Re the last review round, checked against the live server at airdrop-claim.quantus.com on 2b1cd8c:

  1. Entitlements now match the server exactly. The live /snapshot (sha 9ab25d79dd35) has 775 rows, 10,039.20 QTC, minimum 0.10, no unknown kinds. Summing the four bundled tables per address gives the same 775 addresses and the same amount for every one of them: 0 mismatches, 0 server-only rows. The 0.10-per-testnet floor is applied on both sides (airdrop-claim PR Refactor1 #3, merged and deployed).

  2. & 3. 409 handling is deliberately simple. The server keeps one row per address and answers a repeat with 409, and nothing else produces a 409, so 200 and 409 both count as submitted. Every address is tried on every attempt, a rejection does not stop the rest, and the user sees either "All claims have been submitted." or "Some claims have been submitted, please try again" with Try Again. There is no /unpaid lookup and no destination lock.

The one scenario left intentionally unresolved is a user who submits part of a batch, changes the payout address, and resubmits: the addresses recorded first stay bound to the first address, and the app cannot tell, since the server does not return the recorded payout address. We accept that.

Verified end to end against a local airdrop-check built from airdrop-claim main (01a7630): match, sign, prove, record, resubmit unchanged.

@n13
n13 merged commit 024f87e into main Sep 17, 2026
5 checks passed
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