@playmos/sdk 0.3.10 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,189 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@playmos/sdk`. This project adheres to [Semantic Versioning](https://semver.org/).
4
+
5
+ ## Unreleased
6
+
7
+ ## [0.3.11] — 2026-08-09
8
+
9
+ ### Added
10
+ - **`hasEntered` on `@playmos/sdk/server`** (sdk#510 / PR #541): server-side PrizePool admit read — required `rpcUrl`, plain-string pins, throws on RPC fail/timeout (never `false` on outage). Default `timeoutMs` 8s + optional `AbortSignal`.
11
+ - Service **`GET /v1/rounds/:id/entered`** (sk_ only): store tenancy + pure-chain query params for server-settle studios.
12
+
13
+ ### Notes
14
+ - Third parties need this tag on npm for Option A; Option B needs service deploy of main after #541.
15
+ - Related carries: timeout REVERT-RED (#542), residual money-path #538 C1–C4 (service).
16
+
17
+
18
+ ### Fixed (service; next tag)
19
+ - **#530** — `rounds.open` refuses IAP catalog ids / playmosPay games (no PrizePool calldata at PlaymosPay).
20
+ - **#533** — self-serve global settle cap releases on settle failure (no permanent shared-ceiling burn).
21
+ - **#532** — mutation-proof coverage for #528 R2 open gate, #529 burn payout, #509 idempotency lookup.
22
+ - **#528 / #529 / #508 / #509** — shipped on main via PR #531 (open-mined fence, IAP payout, settle wait, idem recovery).
23
+
24
+ ### Docs
25
+ - **#511** — `rounds.cancel` is real (docs no longer claim “no wrapper in V1”).
26
+ - **#516** — CHANGELOG in npm tarball; 0.3.8 identity breaking called out below.
27
+
28
+ ## [0.3.10] — 2026-08-06
29
+
30
+ ### Added
31
+ - **`rounds.getWithMeta`** (#501): returns `{ round, via?, chainReadFailed? }` for service read-path honesty after #499. `rounds.get` still returns `RoundState` only (no transport meta on the record — hub-safe). Mock omits `via`.
32
+
33
+ ### Fixed
34
+ - **Mock `rounds.withdraw`** (#495): requires `wallet`; debits only that wallet’s credit; zero balance → `NothingToWithdrawError` (was paying first non-zero credit to any caller).
35
+
36
+ ### Ops / service
37
+ - **Mainnet refuse non-dedicated RPC + Ankr free-path honesty** (#505 / #502 residual).
38
+ - **`/health` RPC redaction host-only** + **`chain.rpcKind`** allowlist classification (#502 partial on this tag). Warn on public RPC for Sepolia; hard-fail public RPC on mainnet chainId (further #505 refuse-unknown if merged before tag).
39
+
40
+ ## [0.3.9] — 2026-08-06
41
+
42
+ ### Fixed
43
+ - **Mock `rounds.settle` payout economics** (#490 / #493): offline settle applies the stored payout rule (`computePayout`) and accumulates the PrizePool pot (rake off top, then `POOL/(POOL+SEED)` at lock) instead of winner-takes-all on `entryAmount`. Credits `rounds.prize` from integer micro (not 2dp re-parse). Re-settle replays stored winners (claimable unchanged). Ranking validation matches live (400 `payout_invalid`). **Behaviour change:** mock settle now rejects non-address / short rankings that previously returned fake numbers.
44
+
45
+ ### Docs
46
+ - Mock pool is a **floor** (no series seed bank offline) — not a live-series forecast (#490 C6).
47
+ - Public operator note: `pk_` on `/v1/rounds*` → clean **403** (not an opaque 504) (#496 / #500).
48
+ - Public docs auto-deploy pipeline unblocked (#498).
49
+
50
+ ## [0.3.8] — 2026-08-05
51
+
52
+ ### Breaking
53
+ - **`identity` required on the player-wallet (client-signed) path** (#466 / #374). `enterRound` with a wallet no longer invents `wallet#idempotencyKey`. Missing `identity` → typed `ConfigError`. Pass the same identity your server will use for `hasEntered` / hub `/entry/confirm`. No-wallet server-settle smoke may still omit it (service derives). Studios on `^0.3.x` that auto-bump must pin identity before upgrading past 0.3.7.
54
+
55
+ ### Packaging
56
+ - **homepage / bugs** point at public docs (`https://playmos-docs-public.vercel.app/docs`) so npm sidebar links resolve for outsiders (#481 / #488).
57
+
58
+ ## [0.3.7] — 2026-08-04
59
+
60
+ ### Fixed
61
+ - **Mock operator lifecycle with public key** (#435 / #456): `rounds.open` / `lock` / `settle` / `cancel` work offline when `mock: true` with a `pk_` key. Live operator path still requires a provisioned `sk_`.
62
+
63
+ ### Docs
64
+ - **Issa residual honesty** (#435 / #457): skill entry ids are `entry_…` (not `pay_…`); Base Stack uses `basestack` / `game_sandbox_skill` (not `game_sandbox_base_stack`); shared sandbox pool callout.
65
+
66
+ ## [0.3.6] — 2026-07-25
67
+
68
+ **Packaging only. No API change, no runtime change, no behaviour change** — code under `sdk/src`
69
+ is byte-identical to `0.3.5`. Upgrading gains you nothing functionally, and nothing breaks.
70
+
71
+ This release exists to prove the release path itself. It is the **first `@playmos/sdk` version
72
+ published by CI from a version tag** rather than by a person running `npm publish` by hand.
73
+
74
+ ### Packaging
75
+ - **`repository`** — required by npm Trusted Publishing (OIDC), and it fails at publish time
76
+ rather than at setup time when absent.
77
+ - **`publishConfig: { access: "public", provenance: true }`** — a scoped package defaults to
78
+ **restricted**; a silently-private publish of a package studios must install is a failure that
79
+ looks like success. Making it explicit means `--access` can never be omitted.
80
+
81
+ ### Release path (context, not a code change)
82
+ `.github/workflows/publish-npm.yml` now publishes on a `v*` tag, behind two guards that run
83
+ **before** any credential is touched: the tag version must equal `sdk/package.json`, and the
84
+ tagged commit must be an ancestor of `origin/main`. The workflow then **reads the version back
85
+ off the registry** — the publish command's own exit code is not treated as evidence.
86
+
87
+ Why it was built: three npm tokens expired silently, each taking publishing down with it, and an
88
+ expired token returns **E404, not 401**, so the 2026-07-18 outage was misdiagnosed as a missing
89
+ package. `publish-pypi.yml` and `publish-nuget.yml` already existed; npm — the actual product —
90
+ had no publish workflow at all.
91
+
92
+ ### Supersedes
93
+ - The **"npm publish is Boz-run"** note in `0.3.5` below is **no longer accurate.** Publishing is
94
+ automated from a version tag whose commit is on `main` and passed CI. *The machine may release;
95
+ it may not release unreviewed, untagged, or off-main work.*
96
+
97
+ ### Known, recorded, not fixed here
98
+ - **The SDK's test suite cannot run standalone** — three suites import across into `service/src`,
99
+ so a clean checkout of `sdk/` alone cannot resolve them. This means **the published package has
100
+ never been validated in isolation from the monorepo**, which is the only way a studio ever
101
+ installs it. Carried to the Frozen Plan.
102
+
103
+ ## [0.3.5] — 2026-07-18
104
+
105
+ **Authoritative series latch** (issue **#351**). Unblocks hub PR #57 probe-subsystem deletion.
106
+
107
+ ### Added
108
+ - **`rounds.activeForSeries({ seriesKey, gameId })`** → `ActiveSeriesRound | null`.
109
+ `null` is a **proof of free** (service `GET /v1/series/:seriesKey/active`), not a failed probe.
110
+ - Types: `ActiveSeriesRound`, `ActiveForSeriesResult`; optional `seriesKey` on `RoundState`.
111
+
112
+ ### Fixed
113
+ - **Mock `rounds.open` persists `seriesKey`** (was discarded). Without this, mock
114
+ `activeForSeries` would always return free — the money-strand class.
115
+
116
+ ### Service (same release train)
117
+ - `GET /v1/series/:seriesKey/active?gameId=` — chain `activeRound` + studio-scoped hash reverse.
118
+ - `activeRound` on PrizePool ABI; `readActiveRound` / `readPrizeRoundByHash`.
119
+ - `serializeRound` emits `seriesKey`; `/health` capabilities include `series.active`.
120
+ - `listRoundsForStudio` on store for tenancy-safe reverse lookup.
121
+
122
+ ### Note
123
+ - **npm publish is Boz-run.** Hub pins `^0.3.4` until dual-path ships, then bumps to `^0.3.5`.
124
+ - Hub probe/memory **deletion is a separate PR** after live verify.
125
+ - **Hub dual-path capability detection:** read `ApiError.detail.status` / `detail.code` (not
126
+ top-level `.status`/`.code`). Prefer `/health` `capabilities.rounds.capabilities` includes
127
+ `"series.active"` over inferring from 404 shape.
128
+
129
+ ## [0.3.3] — 2026-07-15
130
+
131
+ 3-game dogfood convergence (Path B / kit). **Testnet only.**
132
+
133
+ ### Fixed
134
+ - **`exports["./package.json"]`** — resolvable from npm (tools that import package metadata no longer fail on 0.3.2). **#215**
135
+ - **`entryProvider().payEntry` status** — always **lowercase** (`confirmed` | `failed` | `pending`), matching `Payment.status` (was `CONFIRMED` / kit-style). **#217**
136
+ - **`entryProvider` + offline `verify`** — payEntry path keeps the mock store (`rememberMock`) so `verify(payEntry.paymentId)` is offline like `enterRound` (**#219** completes **#207** for the kit path). Same `Playmos` instance required (store is per client).
137
+
138
+ ### Added
139
+ - **Mock `rounds.open` / `lock` / `settle` / `get`** when `mock: true` — offline Path B operator lifecycle for unit tests and dogfood without sandbox. **#216**
140
+
141
+ ### Note
142
+ - Kits that compare `status === "CONFIRMED"` should use case-insensitive checks.
143
+ - No #161 interop claim language.
144
+
145
+ ## [0.3.2] — 2026-07-15
146
+
147
+ Path B mock / dogfood seam (issues **#204** / **#207**). **Testnet only.**
148
+
149
+ ### Fixed
150
+ - **`mock: true` `connect()`** works offline (no Base App / base-account provider required).
151
+ - **`enterRound` mock honors `roundKey` + `identity` pins**.
152
+ - **`verify(id)` offline for mock payments**.
153
+ - **`entryProvider` mock receipts set `onchain: false`**.
154
+
155
+ ### Docs / publish
156
+ - CI: `sdk/dist` must match a fresh `npm run build` (#211).
157
+
158
+ ## [0.3.1] — 2026-07-11
159
+
160
+ Winner claim ergonomics (issue #41). **Proven on Base Sepolia** (issue #62 — `service/scripts/prove-withdraw-claim.ts`).
161
+
162
+ ### Added
163
+ - **`enterRound` returns `prizePoolAddress` + `roundKey`** on the entry payment — keep them for later claim.
164
+ - **`rounds.prize({ roundId, wallet })`** — read claimable pull-payment balance (+ PrizePool address).
165
+ - **`rounds.withdraw({ prizePoolAddress | roundId })`** — player-signed `PrizePool.withdraw()`; pre-check claimable; typed **`NothingToWithdrawError`**.
166
+ - Service: **`GET /v1/rounds/:id/prize?wallet=`**, `RoundState.prizePoolAddress` on open/lock/get.
167
+ - Docs: claim flow marked **proven** after live settle→prize→withdraw→balance proof.
168
+
169
+ ## [0.3.0] — 2026-07-11
170
+
171
+ Agent-economy DX hardening from the Playmos Town live stress-test (Base Sepolia).
172
+
173
+ ### Added
174
+ - **`transfers.wait(id, opts?)`** and **`transfer(input, { confirm: true })`** — first-class settlement confirmation, so you no longer hand-roll a poll loop. `wait` resolves on a terminal state (`settled`/`failed` — a `failed` transfer *resolves*, inspect `status`; it does not throw) and throws a typed `ApiError` (`detail.timeout`) past `timeoutMs` (default 30s) / `maxAttempts` (40). (#47)
175
+ - **Automatic `429` retry** — the client retries throttled requests with exponential backoff + jitter, honoring `Retry-After` (default ≤ 2 retries). Opt out with `new Playmos({ apiKey, retry: false })`; tune with `retry: { maxRetries, baseDelayMs, maxDelayMs }`. Safe: reads are idempotent and writes carry an idempotency key. (#50)
176
+ - **`agents.fund` now returns `funding.id`** and auto-sends an `idempotencyKey` (body + header) like `transfer`, so a retry re-hits the same requirement (`preq_<key>`) — no double-fund. The id is pollable for recovery after a 502/timeout. (#48, #54)
177
+
178
+ ### Changed
179
+ - **`fee` / `net` strings now carry full micro precision** — a sub-cent fee shows its real value (e.g. `"0.005"`, never `"0.00"`), and **`fee + net === amount`** holds exactly. (#45)
180
+ - **`feeSink` is now optional on the sandbox** for `transfer` and `escrow.hold` (previously the client hard-rejected an omitted sink when `feeBps > 0`). The sandbox service supplies a default sink (advertised on `/health`), so agent scripts move value without wiring a treasury; on **live** an omitted sink still 400s at the service (server authority). A supplied sink is always shape-validated client-side. Now consistent with `marketplace.list`. (#49, #58)
181
+
182
+ ### Fixed
183
+ - **`RoundState.pool` / `RoundState.entrants` are now `string | null` / `number | null`** — matching the wire, which returns `null` until a round is reconciled against chain (the types previously claimed non-null). (#42)
184
+
185
+ ### Docs
186
+ - README now includes the **Playmos Town agent golden path** (`createWallet` → `fund` → `transfer` → confirm) and the `sk_test_` vs `pk_test_` distinction. (#38)
187
+
188
+ ## [0.2.0]
189
+ - Value-movement primitives: `transfer`, `escrow`, `marketplace`, agent wallets (`agents.*`), and contest-settle round lifecycle (`rounds.*`).
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  Stablecoin payments for games on Base. One SDK for in-app purchases (1%), skill-game prize-pool entries (10%, 60/30/10), and **in-game economies** (player · NPC · agent commerce via `transfer()`). USD in, USDC on-chain — no crypto UX for your players.
6
6
 
7
+ **Changelog:** see [`CHANGELOG.md`](./CHANGELOG.md) (shipped in the npm tarball from the next publish). **Breaking in 0.3.8:** player-wallet `enterRound` requires `identity` — no silent invent.
7
8
 
8
9
  ## Install
9
10
 
@@ -246,6 +247,8 @@ Outside the sandbox (a `pk_live` key), or whenever you want the player to sign t
246
247
  ```ts
247
248
  const playmos = new Playmos({
248
249
  apiKey: "pk_live_…",
250
+ // base-account (default) is NOT a built-in Base Account package — pass wallet.provider
251
+ // from @base-org/account when not in Base App / no injected ethereum (sdk#512)
249
252
  wallet: { connector: "base-account" }, // or "injected" in a wallet browser
250
253
  });
251
254
  ```
@@ -0,0 +1,255 @@
1
+ import { numberToHex, keccak256, toBytes, encodeFunctionData } from 'viem';
2
+
3
+ // src/errors.ts
4
+ var PlaymosError = class extends Error {
5
+ constructor(code, message, detail) {
6
+ super(message);
7
+ this.name = new.target.name;
8
+ this.code = code;
9
+ this.detail = detail;
10
+ Object.setPrototypeOf(this, new.target.prototype);
11
+ }
12
+ };
13
+ var InvalidAmountError = class extends PlaymosError {
14
+ constructor(amount) {
15
+ super(
16
+ "invalid_amount",
17
+ `Invalid amount: ${JSON.stringify(amount)}. Provide a positive USD decimal string with at most 2 decimals, e.g. "4.99".`,
18
+ { amount }
19
+ );
20
+ }
21
+ };
22
+ var MissingFieldError = class extends PlaymosError {
23
+ constructor(field) {
24
+ super("missing_field", `Missing required field: "${field}".`, { field });
25
+ }
26
+ };
27
+ var InsufficientGasError = class extends PlaymosError {
28
+ constructor(detail) {
29
+ super(
30
+ "insufficient_gas",
31
+ `The player's wallet has too little ETH to pay gas. Ask them to add a little ETH, or switch to gas.mode: "sponsored".`,
32
+ detail
33
+ );
34
+ }
35
+ };
36
+ var WalletConnectionError = class extends PlaymosError {
37
+ constructor(message = "Could not connect the player's wallet.", detail) {
38
+ super("wallet_connection", message, detail);
39
+ }
40
+ };
41
+ var WalletTimeoutError = class extends PlaymosError {
42
+ constructor(message = "The wallet did not respond in time. Ask the player to approve the prompt, or retry.", detail) {
43
+ super("wallet_timeout", message, detail);
44
+ }
45
+ };
46
+ var PaymentFailedError = class extends PlaymosError {
47
+ constructor(message = "The on-chain payment did not complete.", detail) {
48
+ super("payment_failed", message, detail);
49
+ }
50
+ };
51
+ var AlreadyEnteredError = class extends PlaymosError {
52
+ constructor(detail) {
53
+ super(
54
+ "already_entered",
55
+ "This identity already entered this round on-chain. For pay-per-play, pass a unique identity per attempt (not just the wallet address).",
56
+ detail
57
+ );
58
+ }
59
+ };
60
+ var AuthError = class extends PlaymosError {
61
+ constructor(message = "Invalid or missing API key.", detail) {
62
+ super("auth", message, detail);
63
+ }
64
+ };
65
+ var ApiError = class extends PlaymosError {
66
+ constructor(message, detail) {
67
+ super("api_error", message, detail);
68
+ }
69
+ };
70
+ var ConfigError = class extends PlaymosError {
71
+ constructor(message, detail) {
72
+ super("config", message, detail);
73
+ }
74
+ };
75
+ var NothingToWithdrawError = class extends PlaymosError {
76
+ constructor(detail) {
77
+ super(
78
+ "nothing_to_withdraw",
79
+ "Nothing to withdraw \u2014 this wallet has no credited prize balance on this PrizePool (round not settled for them, or already claimed).",
80
+ detail
81
+ );
82
+ }
83
+ };
84
+
85
+ // src/chain/abis.ts
86
+ var erc20Abi = [
87
+ {
88
+ type: "function",
89
+ name: "approve",
90
+ stateMutability: "nonpayable",
91
+ inputs: [
92
+ { name: "spender", type: "address" },
93
+ { name: "amount", type: "uint256" }
94
+ ],
95
+ outputs: [{ type: "bool" }]
96
+ }
97
+ ];
98
+ var playmosPayAbi = [
99
+ {
100
+ type: "function",
101
+ name: "pay",
102
+ stateMutability: "nonpayable",
103
+ inputs: [
104
+ { name: "paymentId", type: "bytes32" },
105
+ { name: "studio", type: "address" },
106
+ { name: "amount", type: "uint256" }
107
+ ],
108
+ outputs: []
109
+ }
110
+ ];
111
+ var prizePoolAbi = [
112
+ {
113
+ type: "function",
114
+ name: "enter",
115
+ stateMutability: "nonpayable",
116
+ inputs: [
117
+ { name: "roundId", type: "bytes32" },
118
+ { name: "identity", type: "bytes32" }
119
+ ],
120
+ outputs: []
121
+ },
122
+ {
123
+ type: "function",
124
+ name: "withdrawable",
125
+ stateMutability: "view",
126
+ inputs: [{ name: "account", type: "address" }],
127
+ outputs: [{ type: "uint256" }]
128
+ },
129
+ {
130
+ type: "function",
131
+ name: "withdraw",
132
+ stateMutability: "nonpayable",
133
+ inputs: [],
134
+ outputs: [{ name: "amount", type: "uint256" }]
135
+ }
136
+ ];
137
+ async function sendCalls(provider, from, chainId, calls, paymasterUrl) {
138
+ const params = {
139
+ version: "2.0.0",
140
+ from,
141
+ chainId: numberToHex(chainId),
142
+ atomicRequired: true,
143
+ calls
144
+ };
145
+ if (paymasterUrl) {
146
+ params.capabilities = { paymasterService: { url: paymasterUrl } };
147
+ }
148
+ let result;
149
+ try {
150
+ result = await provider.request({ method: "wallet_sendCalls", params: [params] });
151
+ } catch (e) {
152
+ if (e instanceof WalletTimeoutError) throw e;
153
+ const msg = e?.message ?? String(e);
154
+ if (/reject|denied|cancel|closed/i.test(msg)) {
155
+ throw new WalletConnectionError("The player cancelled or closed the payment sheet.", { cause: msg });
156
+ }
157
+ throw new PaymentFailedError("wallet_sendCalls failed.", { cause: msg });
158
+ }
159
+ const id = typeof result === "string" ? result : result?.id ?? "";
160
+ if (!id) throw new PaymentFailedError("Wallet returned no calls id.");
161
+ return { id };
162
+ }
163
+ async function waitForCalls(provider, id, timeoutMs = 6e4) {
164
+ if (!id) return { status: "FAILED" };
165
+ const deadline = Date.now() + timeoutMs;
166
+ while (Date.now() < deadline) {
167
+ try {
168
+ const res = await provider.request({
169
+ method: "wallet_getCallsStatus",
170
+ params: [id]
171
+ });
172
+ const s = String(res?.status ?? "").toUpperCase();
173
+ const txHash = res?.receipts?.[0]?.transactionHash;
174
+ if (s === "200" || s === "CONFIRMED" || s === "SUCCESS") return { status: "CONFIRMED", txHash };
175
+ if (s === "400" || s === "500" || s === "FAILED" || s === "REVERTED") return { status: "FAILED", txHash };
176
+ } catch (e) {
177
+ if (e instanceof WalletTimeoutError) {
178
+ await new Promise((r) => setTimeout(r, 900));
179
+ continue;
180
+ }
181
+ throw e;
182
+ }
183
+ await new Promise((r) => setTimeout(r, 900));
184
+ }
185
+ return { status: "PENDING" };
186
+ }
187
+ function encodeApprove(spender, amountUnits) {
188
+ return encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [spender, amountUnits] });
189
+ }
190
+ var GAS_FLOOR_WEI_DEFAULT = 200000000000000n;
191
+ var GAS_FLOOR_WEI_BASE_SEPOLIA = 50000000000000n;
192
+ var CHAIN_ID_BASE_SEPOLIA = 84532;
193
+ function gasFloorWei(chainId) {
194
+ return chainId === CHAIN_ID_BASE_SEPOLIA ? GAS_FLOOR_WEI_BASE_SEPOLIA : GAS_FLOOR_WEI_DEFAULT;
195
+ }
196
+ async function assertEnoughGas(provider, from, minWei) {
197
+ let floor = minWei;
198
+ if (floor === void 0) {
199
+ let chainId = 0;
200
+ try {
201
+ const hex = await provider.request({ method: "eth_chainId", params: [] });
202
+ chainId = Number(BigInt(hex));
203
+ } catch {
204
+ }
205
+ floor = gasFloorWei(chainId);
206
+ }
207
+ let balance;
208
+ try {
209
+ const hex = await provider.request({ method: "eth_getBalance", params: [from, "latest"] });
210
+ balance = BigInt(hex);
211
+ } catch {
212
+ return;
213
+ }
214
+ if (balance < floor) {
215
+ throw new InsufficientGasError({
216
+ balanceWei: balance.toString(),
217
+ minWei: floor.toString()
218
+ });
219
+ }
220
+ }
221
+
222
+ // src/chain/calls.ts
223
+ var toBytes32 = (s) => keccak256(toBytes(s));
224
+ function buildIapCalls(args) {
225
+ const payData = encodeFunctionData({
226
+ abi: playmosPayAbi,
227
+ functionName: "pay",
228
+ args: [toBytes32(args.paymentId), args.studio, args.amountUnits]
229
+ });
230
+ return [
231
+ { to: args.usdc, data: encodeApprove(args.playmosPay, args.amountUnits) },
232
+ { to: args.playmosPay, data: payData }
233
+ ];
234
+ }
235
+ function buildEntryCalls(args) {
236
+ const enterData = encodeFunctionData({
237
+ abi: prizePoolAbi,
238
+ functionName: "enter",
239
+ args: [toBytes32(args.roundKey), toBytes32(args.identity)]
240
+ });
241
+ return [
242
+ { to: args.usdc, data: encodeApprove(args.prizePool, args.amountUnits) },
243
+ { to: args.prizePool, data: enterData }
244
+ ];
245
+ }
246
+ function buildWithdrawCall(prizePool) {
247
+ const data = encodeFunctionData({
248
+ abi: prizePoolAbi,
249
+ functionName: "withdraw",
250
+ args: []
251
+ });
252
+ return { to: prizePool, data };
253
+ }
254
+
255
+ export { AlreadyEnteredError, ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError, WalletTimeoutError, assertEnoughGas, buildEntryCalls, buildIapCalls, buildWithdrawCall, prizePoolAbi, sendCalls, toBytes32, waitForCalls };
@@ -24,11 +24,19 @@ interface GasConfig {
24
24
  paymasterUrl?: string;
25
25
  }
26
26
  interface WalletConfig {
27
+ /**
28
+ * Which connector name to use when {@link WalletConfig.provider} is omitted.
29
+ * - `"injected"` — requires `globalThis.ethereum` (wallet browser / extension).
30
+ * - `"base-account"` (default) — **not** a built-in Base Account package inside
31
+ * `@playmos/sdk`. Resolution: use injected ethereum if present; otherwise you
32
+ * must install `@base-org/account` yourself, construct a provider, and pass
33
+ * it as `wallet.provider` (sdk#512). No hard dependency on `@base-org/account`.
34
+ */
27
35
  connector?: WalletConnector;
28
36
  /**
29
- * Pre-built EIP-1193 provider. If omitted, `injected` uses globalThis.ethereum
30
- * and `base-account` expects a provider supplied by the Base Account SDK host.
31
- * A raw provider is still **timeout-wrapped** on resolve (#462) — not a no-op passthrough.
37
+ * Pre-built EIP-1193 provider (host-owned). Always preferred when set.
38
+ * For Base Account / passkey hosts: construct with `@base-org/account` (or the
39
+ * Base App injection) and pass it here. Still **timeout-wrapped** on resolve (#462).
32
40
  */
33
41
  provider?: Eip1193Provider;
34
42
  }
@@ -129,6 +137,10 @@ interface PayInput {
129
137
  sku: string;
130
138
  /** Your opaque user id. */
131
139
  playerId: string;
140
+ /** sdk#508 — max wait for server-settle poll (ms). Default 60_000. */
141
+ settleTimeoutMs?: number;
142
+ /** sdk#508 — abort server-settle polling. */
143
+ signal?: AbortSignal;
132
144
  /**
133
145
  * The game this IAP belongs to. Optional: when your API key maps to exactly
134
146
  * one game the service resolves it for you (the quickstart). Supply it
@@ -150,6 +162,13 @@ interface EnterRoundInput {
150
162
  amount: string;
151
163
  playerId: string;
152
164
  idempotencyKey?: string;
165
+ /**
166
+ * sdk#508 — max wait for server-settle poll (ms). Default 60_000.
167
+ * Only applies to no-wallet sandbox server-settle path.
168
+ */
169
+ settleTimeoutMs?: number;
170
+ /** sdk#508 — abort server-settle polling. */
171
+ signal?: AbortSignal;
153
172
  metadata?: Record<string, string>;
154
173
  /**
155
174
  * Exact on-chain round key for Path B / entryProvider (#201 / #204 / #210).
@@ -387,10 +406,31 @@ interface Payment {
387
406
  status: PaymentStatus;
388
407
  /** "iap" (1% external) or "entry" (60/30/10 prize-pool). */
389
408
  kind: "iap" | "entry";
409
+ /**
410
+ * Requested amount (USD string). For prize-pool **server-settle**, this may
411
+ * differ from what moved on chain — see {@link Payment.chainAmount} (sdk#507).
412
+ */
390
413
  amount: string;
391
414
  fee: string;
392
415
  net: string;
393
- /** Present for prize-pool entries: the 60/30/10 breakdown in USD. */
416
+ /**
417
+ * On-chain entry amount (USD) when the service observed it (server-settle entry).
418
+ * PrizePool charges the **round open price**, not necessarily {@link Payment.amount}.
419
+ * Reconcile economics against this field (or a chain `getRound` read), not `amount` alone.
420
+ */
421
+ chainAmount?: string;
422
+ /** On-chain entry amount in micro-USDC (6 dp integer string), when known. */
423
+ chainAmountMicro?: string;
424
+ /**
425
+ * First-party **60/30/10 projection** (pool/seed/rake USD strings) for prize-pool
426
+ * entries — bookkeeping preview, **not** a chain-derived settlement observation
427
+ * (sdk#514). Numbers come from SDK constants (`POOL_BPS`/`SEED_BPS`/`RAKE_BPS`),
428
+ * not from reading the deployed pool’s constructor bps. On-chain, entry only
429
+ * takes the fee off the top; pool/seed split at **lock**; payable pot also
430
+ * includes inherited seedBank — so `split.pool` is **not** “what this round pays.”
431
+ * Reconcile money with {@link Payment.chainAmount} / pool `getRound`.
432
+ * Field name kept for compatibility (no rename in V1).
433
+ */
394
434
  split?: {
395
435
  pool: string;
396
436
  seed: string;
@@ -427,9 +467,17 @@ interface Payment {
427
467
  interface VerifyResult {
428
468
  id: string;
429
469
  status: PaymentStatus;
470
+ /** Requested amount (USD). May differ from {@link VerifyResult.chainAmount} on server-settle entry. */
430
471
  amount: string;
431
472
  fee: string;
432
473
  net: string;
474
+ /**
475
+ * On-chain settled entry amount (USD) when known — distinct from {@link VerifyResult.amount}
476
+ * (sdk#507 / Vault Pop DX-05b). Prefer this for pot/split economics on shared rounds.
477
+ */
478
+ chainAmount?: string;
479
+ /** On-chain settled entry amount in micro-USDC, when known. */
480
+ chainAmountMicro?: string;
433
481
  txHash?: `0x${string}`;
434
482
  playerId: string;
435
483
  sku?: string;
@@ -440,6 +488,8 @@ interface VerifyResult {
440
488
  * service can never confirm on-chain. */
441
489
  verifiedVia?: "chain" | "cache" | "degraded";
442
490
  chainReads?: "enabled" | "degraded";
491
+ /** Echo of on-chain identity when the service returns it (server-settle). */
492
+ identity?: string;
443
493
  }
444
494
  type WebhookEventType = "payment.confirmed" | "payment.failed" | "payout.settled" | "refund.processed";
445
495
  interface WebhookEvent {
@@ -24,11 +24,19 @@ interface GasConfig {
24
24
  paymasterUrl?: string;
25
25
  }
26
26
  interface WalletConfig {
27
+ /**
28
+ * Which connector name to use when {@link WalletConfig.provider} is omitted.
29
+ * - `"injected"` — requires `globalThis.ethereum` (wallet browser / extension).
30
+ * - `"base-account"` (default) — **not** a built-in Base Account package inside
31
+ * `@playmos/sdk`. Resolution: use injected ethereum if present; otherwise you
32
+ * must install `@base-org/account` yourself, construct a provider, and pass
33
+ * it as `wallet.provider` (sdk#512). No hard dependency on `@base-org/account`.
34
+ */
27
35
  connector?: WalletConnector;
28
36
  /**
29
- * Pre-built EIP-1193 provider. If omitted, `injected` uses globalThis.ethereum
30
- * and `base-account` expects a provider supplied by the Base Account SDK host.
31
- * A raw provider is still **timeout-wrapped** on resolve (#462) — not a no-op passthrough.
37
+ * Pre-built EIP-1193 provider (host-owned). Always preferred when set.
38
+ * For Base Account / passkey hosts: construct with `@base-org/account` (or the
39
+ * Base App injection) and pass it here. Still **timeout-wrapped** on resolve (#462).
32
40
  */
33
41
  provider?: Eip1193Provider;
34
42
  }
@@ -129,6 +137,10 @@ interface PayInput {
129
137
  sku: string;
130
138
  /** Your opaque user id. */
131
139
  playerId: string;
140
+ /** sdk#508 — max wait for server-settle poll (ms). Default 60_000. */
141
+ settleTimeoutMs?: number;
142
+ /** sdk#508 — abort server-settle polling. */
143
+ signal?: AbortSignal;
132
144
  /**
133
145
  * The game this IAP belongs to. Optional: when your API key maps to exactly
134
146
  * one game the service resolves it for you (the quickstart). Supply it
@@ -150,6 +162,13 @@ interface EnterRoundInput {
150
162
  amount: string;
151
163
  playerId: string;
152
164
  idempotencyKey?: string;
165
+ /**
166
+ * sdk#508 — max wait for server-settle poll (ms). Default 60_000.
167
+ * Only applies to no-wallet sandbox server-settle path.
168
+ */
169
+ settleTimeoutMs?: number;
170
+ /** sdk#508 — abort server-settle polling. */
171
+ signal?: AbortSignal;
153
172
  metadata?: Record<string, string>;
154
173
  /**
155
174
  * Exact on-chain round key for Path B / entryProvider (#201 / #204 / #210).
@@ -387,10 +406,31 @@ interface Payment {
387
406
  status: PaymentStatus;
388
407
  /** "iap" (1% external) or "entry" (60/30/10 prize-pool). */
389
408
  kind: "iap" | "entry";
409
+ /**
410
+ * Requested amount (USD string). For prize-pool **server-settle**, this may
411
+ * differ from what moved on chain — see {@link Payment.chainAmount} (sdk#507).
412
+ */
390
413
  amount: string;
391
414
  fee: string;
392
415
  net: string;
393
- /** Present for prize-pool entries: the 60/30/10 breakdown in USD. */
416
+ /**
417
+ * On-chain entry amount (USD) when the service observed it (server-settle entry).
418
+ * PrizePool charges the **round open price**, not necessarily {@link Payment.amount}.
419
+ * Reconcile economics against this field (or a chain `getRound` read), not `amount` alone.
420
+ */
421
+ chainAmount?: string;
422
+ /** On-chain entry amount in micro-USDC (6 dp integer string), when known. */
423
+ chainAmountMicro?: string;
424
+ /**
425
+ * First-party **60/30/10 projection** (pool/seed/rake USD strings) for prize-pool
426
+ * entries — bookkeeping preview, **not** a chain-derived settlement observation
427
+ * (sdk#514). Numbers come from SDK constants (`POOL_BPS`/`SEED_BPS`/`RAKE_BPS`),
428
+ * not from reading the deployed pool’s constructor bps. On-chain, entry only
429
+ * takes the fee off the top; pool/seed split at **lock**; payable pot also
430
+ * includes inherited seedBank — so `split.pool` is **not** “what this round pays.”
431
+ * Reconcile money with {@link Payment.chainAmount} / pool `getRound`.
432
+ * Field name kept for compatibility (no rename in V1).
433
+ */
394
434
  split?: {
395
435
  pool: string;
396
436
  seed: string;
@@ -427,9 +467,17 @@ interface Payment {
427
467
  interface VerifyResult {
428
468
  id: string;
429
469
  status: PaymentStatus;
470
+ /** Requested amount (USD). May differ from {@link VerifyResult.chainAmount} on server-settle entry. */
430
471
  amount: string;
431
472
  fee: string;
432
473
  net: string;
474
+ /**
475
+ * On-chain settled entry amount (USD) when known — distinct from {@link VerifyResult.amount}
476
+ * (sdk#507 / Vault Pop DX-05b). Prefer this for pot/split economics on shared rounds.
477
+ */
478
+ chainAmount?: string;
479
+ /** On-chain settled entry amount in micro-USDC, when known. */
480
+ chainAmountMicro?: string;
433
481
  txHash?: `0x${string}`;
434
482
  playerId: string;
435
483
  sku?: string;
@@ -440,6 +488,8 @@ interface VerifyResult {
440
488
  * service can never confirm on-chain. */
441
489
  verifiedVia?: "chain" | "cache" | "degraded";
442
490
  chainReads?: "enabled" | "degraded";
491
+ /** Echo of on-chain identity when the service returns it (server-settle). */
492
+ identity?: string;
443
493
  }
444
494
  type WebhookEventType = "payment.confirmed" | "payment.failed" | "payout.settled" | "refund.processed";
445
495
  interface WebhookEvent {