@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 +189 -0
- package/README.md +3 -0
- package/dist/chunk-35WJANW4.js +255 -0
- package/dist/{errors-Chizbb96.d.cts → errors-Dpeesyop.d.cts} +54 -4
- package/dist/{errors-Chizbb96.d.ts → errors-Dpeesyop.d.ts} +54 -4
- package/dist/index.cjs +137 -16
- package/dist/index.d.cts +28 -7
- package/dist/index.d.ts +28 -7
- package/dist/index.js +140 -187
- package/dist/server.cjs +117 -0
- package/dist/server.d.cts +34 -2
- package/dist/server.d.ts +34 -2
- package/dist/server.js +110 -2
- package/package.json +4 -8
- package/dist/chunk-UZECDT6F.js +0 -83
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.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
-
/**
|
|
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.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
-
/**
|
|
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 {
|