Skip to content

feat(user_worker): payments — on-chain top-up verification + subscription passthrough (UNIT 4)#3

Merged
kingwingfly-claude merged 1 commit into
devfrom
feat/user-system-u4-payments
Jul 5, 2026
Merged

feat(user_worker): payments — on-chain top-up verification + subscription passthrough (UNIT 4)#3
kingwingfly-claude merged 1 commit into
devfrom
feat/user-system-u4-payments

Conversation

@kingwingfly

Copy link
Copy Markdown
Owner

Summary

Implements user_worker/src/payments.rs (UNIT 4 of the SIWE user system): crypto top-up with on-chain verification via hand-rolled JSON-RPC, payment history, and subscription passthrough to the UserAccount DO. Handler names/extractor signatures match the frozen scaffold routes; no other files touched.

topup (POST /user/api/topup)

  • Session required (401), tx hash normalized + validated against ^0x[0-9a-f]{64}$ (400).
  • D1 replay lock: INSERT INTO payments keyed on tx_hash PK; constraint conflict -> 409, other D1 errors -> 500.
  • eth_getTransactionByHash: recipient must be the deposit address, value >= MIN_TOPUP_WEI (tx-intrinsic failures mark the row rejected + 400), and sender must equal the session address (403).
  • eth_getTransactionReceipt: reverted tx -> rejected + 400; unmined (null receipt / no block) -> 425 with the pending row released.
  • eth_blockNumber: head - block + 1 >= MIN_CONFIRMATIONS, else 425 + lock release so resubmission works.
  • Credit through do_credit with idempotency key tx:{hash} (retries can never double-credit), then finalize the row to credited and return TopupResp.

Other handlers

  • GET /user/api/payments: newest 50 PaymentRows for the session address.
  • POST /user/api/subscribe / unsubscribe: passthrough of the DO's status + body; on 200 mirrors the display-only users.plan column.

Deliberate hardenings beyond the letter of the spec (flagged by code review)

  1. Sender check (from == session address): without it, anyone watching the chain could steal a deposit by submitting its hash first. Mismatch releases the lock (403) so the rightful sender can still cers to top up from their SIWE login wallet (noexchange withdrawals) — easy to drop if that's not wanted.*
  2. tx not found and unmined receipts release the replay lock instead of writing a rejected row — otherwise a valid hash submitted during RPC propagation lag / before mining would be permanently burned (later retries would 409 forever). Status codes stay per spec (400 "tx not found"; unmined joins the retryable 425 path as "0 confirmations").
  3. rpc() treats non-2xx HTTP as an error so a rate-limited endpoint can't masquerade as a valid null ("tx not found") result.
  4. wei_to_points saturates instead of wrapping, so an exotic chain's value can never become a negative (debiting) credit.

ERC-20 remains out of scope; verify_native_tx is isolated (tx-intrinsic checks returning Result<wei, reason>) so an ERC-20 log-decoding variant can slot in beside it.

Testing

  • cargo check -p user_worker and cargo test -p user_worker pass (6 tests: hex quantity parsing, wei->points incl. saturation, tx-hash validation, canned RPC tx/receipt deserialization + verification vectors).
  • Real RPC verification happens at deploy time per the unit's e2e recipe (no local wrangler).

🤖 Generated with Claude Code

…ubscription passthrough)

UNIT 4: replaces the payments.rs stub.

- Hand-rolled Ethereum JSON-RPC over worker::Fetch (no alloy/ethers):
  eth_getTransactionByHash / eth_getTransactionReceipt / eth_blockNumber,
  with HTTP-status and JSON-RPC-error handling.
- topup: tx-hash validation, D1 replay lock (PK conflict -> 409), native-ETH
  verification (recipient == deposit address, value >= MIN_TOPUP_WEI,
  sender == session address), receipt status 0x1, MIN_CONFIRMATIONS gate
  (425 + lock release when unmined/underconfirmed), idempotent DO credit
  ("tx:{hash}"), row finalized to 'credited'.
- Row lifecycle: 'rejected' only for tx-intrinsic failures; transient or
  submitter-relative failures release the pending row so resubmission works.
- list_payments: newest-50 history for the session address.
- subscribe/unsubscribe: DO passthrough with users.plan display mirror on 200.
- Host unit tests: hex parsing, wei->points (incl. saturation), tx-hash
  validation, canned RPC payload deserialization + verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 4, 2026 15:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements UNIT 4 payments functionality in user_worker: on-chain native-ETH top-up verification via JSON-RPC, payment history listing, and subscription subscribe/unsubscribe passthrough to the UserAccount Durable Object.

Changes:

  • Adds a full POST /user/api/topup flow with D1 replay locking, JSON-RPC transaction/receipt verification, confirmation checks, and DO crediting with an idempotency key.
  • Implements GET /user/api/payments to return the newest 50 payment rows for the logged-in address.
  • Implements POST /user/api/subscribe and POST /user/api/unsubscribe as DO passthroughs and mirrors users.plan on success.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

&[
tx_hash.as_str().into(),
addr.as_str().into(),
(session::now_secs() as f64).into(),
"UPDATE payments SET amount_wei=?, points=?, status='credited' WHERE tx_hash=?",
&[
wei.to_string().into(),
(points as f64).into(),
Comment on lines +315 to +333
let block = match receipt {
// reverted on-chain: permanently invalid for any submitter
Some(RpcReceipt { status: Some(s), .. }) if s != "0x1" => {
return reject(&st.env, &tx_hash, "tx failed on-chain").await;
}
Some(RpcReceipt {
status: Some(_),
block_number: Some(b),
}) => match hex_to_u64(&b) {
Some(b) => b,
None => {
let e = worker::Error::RustError(format!("bad receipt blockNumber: {b}"));
return rpc_unavailable(&st.env, &tx_hash, "eth_getTransactionReceipt", e).await;
}
},
// no receipt / no status / no block: not mined => 0 confirmations,
// just the strongest form of "not enough confirmations yet".
_ => return too_early(&st.env, &tx_hash).await,
};
@kingwingfly-claude kingwingfly-claude merged commit eb9adbe into dev Jul 5, 2026
1 check passed
@kingwingfly-claude kingwingfly-claude deleted the feat/user-system-u4-payments branch July 5, 2026 05:36
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.

3 participants