Repository navigation
fix(claims): user-signed claim_winnings prepare/submit flow - #993
Open
solaawojobi00-bit wants to merge 2 commits into
Open
solaawojobi00-bit wants to merge 2 commits into
solaawojobi00-bit wants to merge 2 commits into
Conversation
Replace the server-submitted claim with a prepare/sign/submit flow that matches claim_winnings(user, market_id, claim_nonce). The server never signs; a signed XDR is accepted only if its inner tx hash equals the prepared hash.
|
@solaawojobi00-bit Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
…-prepare-submit # Conflicts: # openapi.yaml # src/openapi/registry.ts # src/services/claimService.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(claims): user-signed
claim_winningsprepare/submit flowProblem
POST /api/predictions/claimcould not work against the deployed contract. The backend tried to sign and submit the call itself, andclaim_winningscallsuser.require_auth(), so only the user's wallet can authorize it.main"claim"; the contract exportsclaim_winningsmarket_idsent asString(must beSymbol);claim_noncemissinguser.require_auth()getAccount(PREDICTIFY_CONTRACT_ID)loads a contract id as a classic account and throws on every callclaimTxHashread → submit → write with no lock; two requests both passclaim_tx_failedclaims, so finality checks never ranSolution
Two-step flow; the server never signs.
POST /api/predictions/claim/prepare{ marketId }validates the market and winning prediction, resolvesclaim_nonce(injectableClaimNonceSource; default simulatesget_claim_nonce, else0only when the user has no paid/settled claim, elseclaim_nonce_unavailable), builds and simulatesclaim_winnings(user, Symbol(marketId), u64 nonce)with the user as source, stores aclaimsrow (prepared) and returns{ xdr, networkPassphrase, expiresAt, claimId }.POST /api/predictions/claim{ claimId, signedXdr }verifies the envelope, moves the rowprepared → submittingwith a conditionalUPDATE, callssendTransaction, and recordspaid(+settlement_tx) orfailed.State machine:
prepared → submitting → paid | failed,prepared → expired; the settle-confirmer finalizespaidrows (settled/rejected).Why hash equality is the authority. A wallet signature does not change a transaction's hash. The prepared tx hash is stored at prepare time and a signed envelope is accepted only if its inner tx hash is identical, so it is byte-for-byte the transaction we simulated (source, contract, function, args, fee, footprint, time bounds). The field-by-field checks (source, contract, function name, each arg, signed) run first only to return a precise
claim_xdr_mismatch; they do not replace the hash check.Changes
src/services/claimChain.ts(new):SorobanClaimChainbuilds/simulates/assembles the unsigned tx,verifySigneddoes the exact-match checks,sendrelays.SimulatedClaimNonceSourceis the default nonce source. No secret key is read anywhere.src/services/claimService.ts:claimWinnings/SorobanClaimTxBuilderremoved; replaced byprepareClaim/submitClaimwith an injectableClaimRepo/ClaimChain. Existing market/prediction checks are reused.src/services/claimRepo.ts(new): Drizzle implementation;transition()isUPDATE … WHERE id AND status = from, which is what keeps concurrent submits to a single RPC call.src/services/claimErrors.ts(new):ClaimErrorwith fixed messages; the raw error lives indetailand goes only to logs (with the request correlation id).src/routes/predictions.ts:/claim/prepareand the new/claimbody.marketIdmust be a Soroban Symbol (≤32 chars,[A-Za-z0-9_]);signedXdrmust be base64, ≤16 KB.src/db/schema.ts+drizzle/0027_claims_prepare_submit.sql+ journal: newclaimscolumns (claim_nonce,expected_tx_hash,prepared_xdr,expires_at,error_code) and partial unique indexclaims_one_active_per_user_market_idxon(user_id, market_id) WHERE status IN ('prepared','submitting','paid').src/metrics/registry.ts:claims_prepared_total,claims_submitted_total{outcome}.src/openapi/registry.ts,openapi.yaml,scripts/check-openapi.ts: both operations, schemas and error codes; new route added to the expected-routes list.src/workers/settleConfirmer.ts:DrizzleSettleConfirmerRepoexported (one word) so a test can run its real query.docs/predictions-api.md: wallet-signing sequence, states, idempotency, error codes.Regression Tests
claim_winnings(user, Symbol, 0), user as sourceprepareClaim › 1:2a,2bpaid+settlement_txsubmitClaim › 3:,3b400 claim_xdr_mismatch, RPC never called4:group (9 cases)5:expiresAt→409 claim_expired, no RPC6:ERROR→ stable code, no SDK text in body, present in log7a,7bclaimId, one row8,8bpaidclaim →200, samesettlement_tx9:Keypairnot importable from claim modules10:(static)paidrow11:Regression proof (row 1). Against the original
claimService.ts, with a fake RPC asserting the call, the old builder invokesclaimwith[scvString, scvAddress](and loads the contract id as the account); the test expectingclaim_winnings(scvAddress, scvSymbol, scvU64)fails. It passes after this change.Testing
Read-only simulation of the production
SorobanClaimChainagainst testnet contractCCWRJXIUBSMFQ4AF2RXXH3AFBW6MEZG4GK4IYES7C4Z3X4PR6K7VAFHCwith a throwaway friendbot-funded wallet (no winnings):The contract executed
claim_winningswith our arguments and answered with a business error (#104), not a function-name or argument-type error.Notes for Reviewers
0026).drizzle-kit generatecurrently does nothing onmainbecausedrizzle/meta/0002_snapshot.jsonand0003_snapshot.jsonare malformed, sodb:generate/db:check-driftcannot produce or detect a migration. I did not touch those snapshots. CI'sdb:migrateapplies the SQL; I could not run it locally (no Postgres).ClaimRepoport; unit tests use an in-memory repo with the same atomic-transition semantics. The real partial unique index and conditionalUPDATEare exercised only by CI/production.paid(a paid claim awaiting settlement must block a second one). "Previous claim" for the nonce fallback means apaid/settledclaim, so afailedorexpiredattempt does not block a retry. An RPC exception orTRY_AGAIN_LATERreturns the claim toprepared(re-sending the same signed tx is safe) with503 claim_rpc_unavailable; this code is an addition to the issue's list.claims.amountis populated from the prediction's stake as a placeholder; the real payout is decided on-chain.tscandnpm testare not clean onmainlocally in my environment (no Postgres/Redis); I fixed only what this change touches.npm run lintreports one error insrc/lib/http.ts, which this PR does not touch.Closes #977