@playmos/sdk 0.3.5 → 0.3.7
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/README.md +39 -15
- package/dist/index.cjs +74 -6
- package/dist/index.js +74 -6
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -5,20 +5,6 @@
|
|
|
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
7
|
|
|
8
|
-
## Local `file:` / monorepo install
|
|
9
|
-
|
|
10
|
-
`@playmos/sdk` ships **built `dist/`**. `prepare` / `prepack` / `prepublishOnly` run
|
|
11
|
-
`scripts/ensure-build.cjs`, which rebuilds `dist/` via local `tsup` or (on a cold
|
|
12
|
-
`file:` install with no `sdk/node_modules`) `npx --yes tsup@8.3.0`:
|
|
13
|
-
|
|
14
|
-
- `npm install` inside `sdk/`
|
|
15
|
-
- `npm install` of a **`file:`** / git dependency (dogfood monorepos)
|
|
16
|
-
- `npm publish` / pack
|
|
17
|
-
|
|
18
|
-
So wiping `dist/` then `npm i file:…/sdk` still yields a current build (#211).
|
|
19
|
-
CI also fails if committed `dist/` drifts from source.
|
|
20
|
-
|
|
21
|
-
|
|
22
8
|
## Install
|
|
23
9
|
|
|
24
10
|
```bash
|
|
@@ -205,6 +191,26 @@ The full 5-NPC walkthrough — create → fund → P2P / shop / bounty, each con
|
|
|
205
191
|
PLAYMOS_SK_TEST=sk_test_… PLAYMOS_FEE_SINK=0x… node docs/examples/playmos-town-sdk.mjs
|
|
206
192
|
```
|
|
207
193
|
|
|
194
|
+
## Local `file:` / monorepo install
|
|
195
|
+
|
|
196
|
+
`@playmos/sdk` ships **built `dist/`**. Lifecycle hooks `prepare` / `prepack` / `prepublishOnly`
|
|
197
|
+
run `scripts/ensure-build.cjs` (rebuild via local `tsup` or, on a cold `file:` install with no
|
|
198
|
+
`sdk/node_modules`, `npx --yes tsup@8.3.0`) — **when lifecycle scripts are enabled**.
|
|
199
|
+
|
|
200
|
+
**Repo hardening (sdk#431 / #434):** root + `sdk/` + `service/` + `contracts/` set
|
|
201
|
+
`ignore-scripts=true` in `.npmrc`. That blocks dependency install scripts (supply-chain defense)
|
|
202
|
+
and also suppresses those prepare/prepack/prepublishOnly hooks.
|
|
203
|
+
|
|
204
|
+
| Action | What to do |
|
|
205
|
+
|--------|------------|
|
|
206
|
+
| Day-to-day install in this monorepo | `npm ci` / `npm i` as usual — scripts ignored by design |
|
|
207
|
+
| Rebuild dist after source edits | **`npm run build`** in `sdk/` (user scripts still run) |
|
|
208
|
+
| **Manual** `npm publish` from a laptop | **`cd sdk && npm run build && npm publish`** — do not rely on prepublishOnly |
|
|
209
|
+
| CI publish | Already runs `npm run build` explicitly before pack/publish |
|
|
210
|
+
|
|
211
|
+
So wiping `dist/` then `npm i file:…/sdk` may **not** auto-rebuild under `ignore-scripts`;
|
|
212
|
+
run `npm run build` in `sdk/` first, or use CI. CI also fails if committed `dist/` drifts from source.
|
|
213
|
+
|
|
208
214
|
## Mock mode — offline, deterministic
|
|
209
215
|
|
|
210
216
|
For CI and wiring checks, `mock: true` returns instant, deterministic results with **no network and no chain**. Results carry `mock: true` and use the real status union, so your handling code sees the exact production shape.
|
|
@@ -215,9 +221,23 @@ const payment = await playmos.pay({
|
|
|
215
221
|
gameId: "game_sandbox_iap", sku: "gems_100", amount: "0.99", playerId: "player_abc",
|
|
216
222
|
});
|
|
217
223
|
// instant — payment.status === "confirmed", payment.mock === true
|
|
224
|
+
|
|
225
|
+
// Operator lifecycle offline too — no sk_ required when mock:true (#435)
|
|
226
|
+
await playmos.rounds.open({
|
|
227
|
+
gameId: "game_sandbox_skill",
|
|
228
|
+
roundId: "r1",
|
|
229
|
+
entryAmount: "0.25",
|
|
230
|
+
payout: { kind: "winner-take-all" },
|
|
231
|
+
});
|
|
232
|
+
await playmos.rounds.lock({ roundId: "r1" });
|
|
233
|
+
await playmos.rounds.settle({
|
|
234
|
+
roundId: "r1",
|
|
235
|
+
results: { ranking: ["0x0000000000000000000000000000000000000001"] },
|
|
236
|
+
});
|
|
218
237
|
```
|
|
219
238
|
|
|
220
239
|
Mock mode is not test mode: mock is fabricated and offline; the sandbox is real and settles on Base Sepolia.
|
|
240
|
+
**Live** `rounds.open` / `lock` / `settle` still need a provisioned secret key on the server.
|
|
221
241
|
|
|
222
242
|
## Production — real player wallets
|
|
223
243
|
|
|
@@ -257,4 +277,8 @@ The escrow/marketplace resolve endpoints (release/refund/confirm) return a deter
|
|
|
257
277
|
|
|
258
278
|
## Docs
|
|
259
279
|
|
|
260
|
-
Full
|
|
280
|
+
Full developer docs (install, IAP `pay()`, skill `enterRound()`, agents, webhooks, REST) — **public, no login**:
|
|
281
|
+
|
|
282
|
+
**https://playmos-docs-public.vercel.app/docs**
|
|
283
|
+
|
|
284
|
+
That is the third-party / stranger surface while we stress-test. Production **`playmos.io/docs`** comes after Founder promote (gate still holds for production only).
|
package/dist/index.cjs
CHANGED
|
@@ -315,12 +315,66 @@ function createHttpClient(baseUrl, apiKey, retry) {
|
|
|
315
315
|
}
|
|
316
316
|
return false;
|
|
317
317
|
}
|
|
318
|
+
async function probeSignerBelowFloor() {
|
|
319
|
+
try {
|
|
320
|
+
const res = await send(`${base}/health`, { method: "GET" });
|
|
321
|
+
if (!res.ok) return { below: false };
|
|
322
|
+
const body = await res.json();
|
|
323
|
+
const bal = body?.capabilities?.payments?.signerBalance;
|
|
324
|
+
if (!bal || bal.ok !== false) return { below: false };
|
|
325
|
+
const reason = bal.reason ?? "";
|
|
326
|
+
const fundedFloorMiss = reason === "low_usdc" || reason === "low_eth" || reason === "low_usdc_and_eth";
|
|
327
|
+
if (!fundedFloorMiss) return { below: false, reason: reason || void 0 };
|
|
328
|
+
return { below: true, reason };
|
|
329
|
+
} catch {
|
|
330
|
+
return { below: false };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
318
333
|
async function sendWithRetry(url, init) {
|
|
319
334
|
let res = await send(url, init);
|
|
335
|
+
let signerBelowFloor = false;
|
|
336
|
+
let signerBelowReason;
|
|
337
|
+
let probed = false;
|
|
338
|
+
const maybeProbe = async () => {
|
|
339
|
+
if (probed || signerBelowFloor) return;
|
|
340
|
+
if (res.status !== 502 && res.status !== 503 && res.status !== 504) return;
|
|
341
|
+
const ct = (res.headers.get("content-type") || "").toLowerCase();
|
|
342
|
+
if (ct.includes("application/json")) return;
|
|
343
|
+
probed = true;
|
|
344
|
+
const probe = await probeSignerBelowFloor();
|
|
345
|
+
signerBelowFloor = probe.below;
|
|
346
|
+
signerBelowReason = probe.reason;
|
|
347
|
+
};
|
|
348
|
+
const maybePeekJsonSignerLow = async () => {
|
|
349
|
+
if (signerBelowFloor) return;
|
|
350
|
+
if (res.status !== 502 && res.status !== 503 && res.status !== 504) return;
|
|
351
|
+
const ct = (res.headers.get("content-type") || "").toLowerCase();
|
|
352
|
+
if (!ct.includes("application/json")) return;
|
|
353
|
+
try {
|
|
354
|
+
const peek = JSON.parse(await res.clone().text());
|
|
355
|
+
const code = peek?.error?.code;
|
|
356
|
+
const reason = peek?.error?.reason;
|
|
357
|
+
if (code === "signer_low_balance" || reason === "low_usdc" || reason === "low_eth" || reason === "low_usdc_and_eth") {
|
|
358
|
+
signerBelowFloor = true;
|
|
359
|
+
signerBelowReason = reason ?? code;
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
const classifySignerBelow = async () => {
|
|
365
|
+
await maybeProbe();
|
|
366
|
+
await maybePeekJsonSignerLow();
|
|
367
|
+
};
|
|
368
|
+
await classifySignerBelow();
|
|
320
369
|
for (let attempt = 0; attempt < cfg.maxRetries && isRetryable(res, init); attempt++) {
|
|
370
|
+
if (signerBelowFloor) break;
|
|
321
371
|
await sleep(backoffMs(res, attempt, cfg));
|
|
322
372
|
res = await send(url, init);
|
|
373
|
+
await classifySignerBelow();
|
|
323
374
|
}
|
|
375
|
+
const flagged = res;
|
|
376
|
+
flagged.__playmosSignerBelowFloor = signerBelowFloor;
|
|
377
|
+
if (signerBelowReason) flagged.__playmosSignerBelowReason = signerBelowReason;
|
|
324
378
|
return res;
|
|
325
379
|
}
|
|
326
380
|
async function handle(res, acceptStatuses) {
|
|
@@ -339,8 +393,22 @@ function createHttpClient(baseUrl, apiKey, retry) {
|
|
|
339
393
|
json = text ? JSON.parse(text) : {};
|
|
340
394
|
} catch {
|
|
341
395
|
const gateway = res.status === 502 || res.status === 503 || res.status === 504;
|
|
342
|
-
const
|
|
343
|
-
|
|
396
|
+
const below = res.__playmosSignerBelowFloor === true;
|
|
397
|
+
let msg;
|
|
398
|
+
if (gateway && below) {
|
|
399
|
+
const why = res.__playmosSignerBelowReason ?? "low_balance";
|
|
400
|
+
msg = `Sandbox signer below funding floor (${why}) (${res.status} from ${res.url}) \u2014 top up Sepolia USDC (see GET /health capabilities.payments.signerBalance). Do not retry; retry cannot succeed until funded. Cross-ref: issue #205 recurrence \xB7 hub funding #15.`;
|
|
401
|
+
} else if (gateway) {
|
|
402
|
+
msg = `Sandbox gateway timeout (${res.status}) from ${res.url} \u2014 the request likely exceeded the platform limit while settling on-chain. Retry with the same idempotency key (safe); prefer a dedicated Base Sepolia RPC on the service (BB-GATEB-002).`;
|
|
403
|
+
} else {
|
|
404
|
+
msg = `Non-JSON response (${res.status}) from ${res.url}`;
|
|
405
|
+
}
|
|
406
|
+
throw new ApiError(msg, {
|
|
407
|
+
status: res.status,
|
|
408
|
+
body: text.slice(0, 500),
|
|
409
|
+
gateway,
|
|
410
|
+
signerBelowFloor: below || void 0
|
|
411
|
+
});
|
|
344
412
|
}
|
|
345
413
|
if (res.ok || acceptStatuses !== void 0 && acceptStatuses.includes(res.status)) {
|
|
346
414
|
return json;
|
|
@@ -981,7 +1049,7 @@ var Playmos = class {
|
|
|
981
1049
|
this.mockWithdrawable = /* @__PURE__ */ new Map();
|
|
982
1050
|
this.rounds = {
|
|
983
1051
|
open: async (input) => {
|
|
984
|
-
this.assertSecretKey("playmos.rounds.open");
|
|
1052
|
+
if (!this.config.mock) this.assertSecretKey("playmos.rounds.open");
|
|
985
1053
|
requireField(input?.gameId, "gameId");
|
|
986
1054
|
requireField(input?.roundId, "roundId");
|
|
987
1055
|
requireField(input?.entryAmount, "entryAmount");
|
|
@@ -1035,7 +1103,7 @@ var Playmos = class {
|
|
|
1035
1103
|
return body.round;
|
|
1036
1104
|
},
|
|
1037
1105
|
lock: async (input) => {
|
|
1038
|
-
this.assertSecretKey("playmos.rounds.lock");
|
|
1106
|
+
if (!this.config.mock) this.assertSecretKey("playmos.rounds.lock");
|
|
1039
1107
|
requireField(input?.roundId, "roundId");
|
|
1040
1108
|
if (this.config.mock) {
|
|
1041
1109
|
const existing = this.mockRounds.get(input.roundId);
|
|
@@ -1067,7 +1135,7 @@ var Playmos = class {
|
|
|
1067
1135
|
return round;
|
|
1068
1136
|
},
|
|
1069
1137
|
settle: async (input) => {
|
|
1070
|
-
this.assertSecretKey("playmos.rounds.settle");
|
|
1138
|
+
if (!this.config.mock) this.assertSecretKey("playmos.rounds.settle");
|
|
1071
1139
|
requireField(input?.roundId, "roundId");
|
|
1072
1140
|
if (!input?.results) throw new ConfigError("results are required (ranking or winners)");
|
|
1073
1141
|
if (this.config.mock) {
|
|
@@ -1141,7 +1209,7 @@ var Playmos = class {
|
|
|
1141
1209
|
* chain Cancelled(4) reconciles — poll `rounds.get` or re-call cancel (#346).
|
|
1142
1210
|
*/
|
|
1143
1211
|
cancel: async (input) => {
|
|
1144
|
-
this.assertSecretKey("playmos.rounds.cancel");
|
|
1212
|
+
if (!this.config.mock) this.assertSecretKey("playmos.rounds.cancel");
|
|
1145
1213
|
requireField(input?.roundId, "roundId");
|
|
1146
1214
|
if (this.config.mock) {
|
|
1147
1215
|
const existing = this.mockRounds.get(input.roundId);
|
package/dist/index.js
CHANGED
|
@@ -245,12 +245,66 @@ function createHttpClient(baseUrl, apiKey, retry) {
|
|
|
245
245
|
}
|
|
246
246
|
return false;
|
|
247
247
|
}
|
|
248
|
+
async function probeSignerBelowFloor() {
|
|
249
|
+
try {
|
|
250
|
+
const res = await send(`${base}/health`, { method: "GET" });
|
|
251
|
+
if (!res.ok) return { below: false };
|
|
252
|
+
const body = await res.json();
|
|
253
|
+
const bal = body?.capabilities?.payments?.signerBalance;
|
|
254
|
+
if (!bal || bal.ok !== false) return { below: false };
|
|
255
|
+
const reason = bal.reason ?? "";
|
|
256
|
+
const fundedFloorMiss = reason === "low_usdc" || reason === "low_eth" || reason === "low_usdc_and_eth";
|
|
257
|
+
if (!fundedFloorMiss) return { below: false, reason: reason || void 0 };
|
|
258
|
+
return { below: true, reason };
|
|
259
|
+
} catch {
|
|
260
|
+
return { below: false };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
248
263
|
async function sendWithRetry(url, init) {
|
|
249
264
|
let res = await send(url, init);
|
|
265
|
+
let signerBelowFloor = false;
|
|
266
|
+
let signerBelowReason;
|
|
267
|
+
let probed = false;
|
|
268
|
+
const maybeProbe = async () => {
|
|
269
|
+
if (probed || signerBelowFloor) return;
|
|
270
|
+
if (res.status !== 502 && res.status !== 503 && res.status !== 504) return;
|
|
271
|
+
const ct = (res.headers.get("content-type") || "").toLowerCase();
|
|
272
|
+
if (ct.includes("application/json")) return;
|
|
273
|
+
probed = true;
|
|
274
|
+
const probe = await probeSignerBelowFloor();
|
|
275
|
+
signerBelowFloor = probe.below;
|
|
276
|
+
signerBelowReason = probe.reason;
|
|
277
|
+
};
|
|
278
|
+
const maybePeekJsonSignerLow = async () => {
|
|
279
|
+
if (signerBelowFloor) return;
|
|
280
|
+
if (res.status !== 502 && res.status !== 503 && res.status !== 504) return;
|
|
281
|
+
const ct = (res.headers.get("content-type") || "").toLowerCase();
|
|
282
|
+
if (!ct.includes("application/json")) return;
|
|
283
|
+
try {
|
|
284
|
+
const peek = JSON.parse(await res.clone().text());
|
|
285
|
+
const code = peek?.error?.code;
|
|
286
|
+
const reason = peek?.error?.reason;
|
|
287
|
+
if (code === "signer_low_balance" || reason === "low_usdc" || reason === "low_eth" || reason === "low_usdc_and_eth") {
|
|
288
|
+
signerBelowFloor = true;
|
|
289
|
+
signerBelowReason = reason ?? code;
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const classifySignerBelow = async () => {
|
|
295
|
+
await maybeProbe();
|
|
296
|
+
await maybePeekJsonSignerLow();
|
|
297
|
+
};
|
|
298
|
+
await classifySignerBelow();
|
|
250
299
|
for (let attempt = 0; attempt < cfg.maxRetries && isRetryable(res, init); attempt++) {
|
|
300
|
+
if (signerBelowFloor) break;
|
|
251
301
|
await sleep(backoffMs(res, attempt, cfg));
|
|
252
302
|
res = await send(url, init);
|
|
303
|
+
await classifySignerBelow();
|
|
253
304
|
}
|
|
305
|
+
const flagged = res;
|
|
306
|
+
flagged.__playmosSignerBelowFloor = signerBelowFloor;
|
|
307
|
+
if (signerBelowReason) flagged.__playmosSignerBelowReason = signerBelowReason;
|
|
254
308
|
return res;
|
|
255
309
|
}
|
|
256
310
|
async function handle(res, acceptStatuses) {
|
|
@@ -269,8 +323,22 @@ function createHttpClient(baseUrl, apiKey, retry) {
|
|
|
269
323
|
json = text ? JSON.parse(text) : {};
|
|
270
324
|
} catch {
|
|
271
325
|
const gateway = res.status === 502 || res.status === 503 || res.status === 504;
|
|
272
|
-
const
|
|
273
|
-
|
|
326
|
+
const below = res.__playmosSignerBelowFloor === true;
|
|
327
|
+
let msg;
|
|
328
|
+
if (gateway && below) {
|
|
329
|
+
const why = res.__playmosSignerBelowReason ?? "low_balance";
|
|
330
|
+
msg = `Sandbox signer below funding floor (${why}) (${res.status} from ${res.url}) \u2014 top up Sepolia USDC (see GET /health capabilities.payments.signerBalance). Do not retry; retry cannot succeed until funded. Cross-ref: issue #205 recurrence \xB7 hub funding #15.`;
|
|
331
|
+
} else if (gateway) {
|
|
332
|
+
msg = `Sandbox gateway timeout (${res.status}) from ${res.url} \u2014 the request likely exceeded the platform limit while settling on-chain. Retry with the same idempotency key (safe); prefer a dedicated Base Sepolia RPC on the service (BB-GATEB-002).`;
|
|
333
|
+
} else {
|
|
334
|
+
msg = `Non-JSON response (${res.status}) from ${res.url}`;
|
|
335
|
+
}
|
|
336
|
+
throw new ApiError(msg, {
|
|
337
|
+
status: res.status,
|
|
338
|
+
body: text.slice(0, 500),
|
|
339
|
+
gateway,
|
|
340
|
+
signerBelowFloor: below || void 0
|
|
341
|
+
});
|
|
274
342
|
}
|
|
275
343
|
if (res.ok || acceptStatuses !== void 0 && acceptStatuses.includes(res.status)) {
|
|
276
344
|
return json;
|
|
@@ -911,7 +979,7 @@ var Playmos = class {
|
|
|
911
979
|
this.mockWithdrawable = /* @__PURE__ */ new Map();
|
|
912
980
|
this.rounds = {
|
|
913
981
|
open: async (input) => {
|
|
914
|
-
this.assertSecretKey("playmos.rounds.open");
|
|
982
|
+
if (!this.config.mock) this.assertSecretKey("playmos.rounds.open");
|
|
915
983
|
requireField(input?.gameId, "gameId");
|
|
916
984
|
requireField(input?.roundId, "roundId");
|
|
917
985
|
requireField(input?.entryAmount, "entryAmount");
|
|
@@ -965,7 +1033,7 @@ var Playmos = class {
|
|
|
965
1033
|
return body.round;
|
|
966
1034
|
},
|
|
967
1035
|
lock: async (input) => {
|
|
968
|
-
this.assertSecretKey("playmos.rounds.lock");
|
|
1036
|
+
if (!this.config.mock) this.assertSecretKey("playmos.rounds.lock");
|
|
969
1037
|
requireField(input?.roundId, "roundId");
|
|
970
1038
|
if (this.config.mock) {
|
|
971
1039
|
const existing = this.mockRounds.get(input.roundId);
|
|
@@ -997,7 +1065,7 @@ var Playmos = class {
|
|
|
997
1065
|
return round;
|
|
998
1066
|
},
|
|
999
1067
|
settle: async (input) => {
|
|
1000
|
-
this.assertSecretKey("playmos.rounds.settle");
|
|
1068
|
+
if (!this.config.mock) this.assertSecretKey("playmos.rounds.settle");
|
|
1001
1069
|
requireField(input?.roundId, "roundId");
|
|
1002
1070
|
if (!input?.results) throw new ConfigError("results are required (ranking or winners)");
|
|
1003
1071
|
if (this.config.mock) {
|
|
@@ -1071,7 +1139,7 @@ var Playmos = class {
|
|
|
1071
1139
|
* chain Cancelled(4) reconciles — poll `rounds.get` or re-call cancel (#346).
|
|
1072
1140
|
*/
|
|
1073
1141
|
cancel: async (input) => {
|
|
1074
|
-
this.assertSecretKey("playmos.rounds.cancel");
|
|
1142
|
+
if (!this.config.mock) this.assertSecretKey("playmos.rounds.cancel");
|
|
1075
1143
|
requireField(input?.roundId, "roundId");
|
|
1076
1144
|
if (this.config.mock) {
|
|
1077
1145
|
const existing = this.mockRounds.get(input.roundId);
|
package/package.json
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playmos/sdk",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Playmos SDK
|
|
3
|
+
"version": "0.3.7",
|
|
4
|
+
"description": "Playmos SDK — stablecoin payments for games on Base. One SDK for IAP (1%), skill-game prize-pool entries (10%, 60/30/10), and agent economies. USD in, USDC on-chain, no crypto UX for players.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/playmos-labs/playmos-sdk.git",
|
|
10
|
+
"directory": "sdk"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
7
15
|
"type": "module",
|
|
8
16
|
"sideEffects": false,
|
|
9
17
|
"main": "./dist/index.cjs",
|