@playmos/sdk 0.3.7 → 0.3.8
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 +3 -3
- package/dist/{chunk-VYR6BBHF.js → chunk-UZECDT6F.js} +15 -1
- package/dist/{errors-CdVzHuhY.d.cts → errors-BMlWHsMb.d.cts} +72 -7
- package/dist/{errors-CdVzHuhY.d.ts → errors-BMlWHsMb.d.ts} +72 -7
- package/dist/index.cjs +317 -44
- package/dist/index.d.cts +18 -9
- package/dist/index.d.ts +18 -9
- package/dist/index.js +303 -46
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/package.json +5 -1
package/dist/index.cjs
CHANGED
|
@@ -42,11 +42,25 @@ var WalletConnectionError = class extends PlaymosError {
|
|
|
42
42
|
super("wallet_connection", message, detail);
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
|
+
var WalletTimeoutError = class extends PlaymosError {
|
|
46
|
+
constructor(message = "The wallet did not respond in time. Ask the player to approve the prompt, or retry.", detail) {
|
|
47
|
+
super("wallet_timeout", message, detail);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
45
50
|
var PaymentFailedError = class extends PlaymosError {
|
|
46
51
|
constructor(message = "The on-chain payment did not complete.", detail) {
|
|
47
52
|
super("payment_failed", message, detail);
|
|
48
53
|
}
|
|
49
54
|
};
|
|
55
|
+
var AlreadyEnteredError = class extends PlaymosError {
|
|
56
|
+
constructor(detail) {
|
|
57
|
+
super(
|
|
58
|
+
"already_entered",
|
|
59
|
+
"This identity already entered this round on-chain. For pay-per-play, pass a unique identity per attempt (not just the wallet address).",
|
|
60
|
+
detail
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
50
64
|
var AuthError = class extends PlaymosError {
|
|
51
65
|
constructor(message = "Invalid or missing API key.", detail) {
|
|
52
66
|
super("auth", message, detail);
|
|
@@ -399,7 +413,7 @@ function createHttpClient(baseUrl, apiKey, retry) {
|
|
|
399
413
|
const why = res.__playmosSignerBelowReason ?? "low_balance";
|
|
400
414
|
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
415
|
} else if (gateway) {
|
|
402
|
-
msg = `Sandbox gateway timeout (${res.status}) from ${res.url} \u2014 the request likely exceeded the platform limit while
|
|
416
|
+
msg = `Sandbox gateway timeout (${res.status}) from ${res.url} \u2014 the request likely exceeded the platform edge limit while on-chain work ran. Retry with the same idempotency key after checking chain/service status (safe if settle already landed). Do not assume missing RPC \u2014 sandbox usually has one; prefer shorter async settle paths (BB-GATEB-002 / #422).`;
|
|
403
417
|
} else {
|
|
404
418
|
msg = `Non-JSON response (${res.status}) from ${res.url}`;
|
|
405
419
|
}
|
|
@@ -441,7 +455,60 @@ function createHttpClient(baseUrl, apiKey, retry) {
|
|
|
441
455
|
}
|
|
442
456
|
|
|
443
457
|
// src/wallet.ts
|
|
444
|
-
|
|
458
|
+
var DEFAULT_WALLET_REQUEST_TIMEOUT_MS = 2e4;
|
|
459
|
+
var MONEY_MOVING_METHODS = /* @__PURE__ */ new Set([
|
|
460
|
+
"wallet_sendCalls",
|
|
461
|
+
"eth_sendTransaction",
|
|
462
|
+
"eth_sendRawTransaction"
|
|
463
|
+
]);
|
|
464
|
+
var CONNECT_METHODS = /* @__PURE__ */ new Set(["eth_requestAccounts"]);
|
|
465
|
+
var TIMED_FLAG = "__playmosTimedRequest";
|
|
466
|
+
function resolvePolicy(opts) {
|
|
467
|
+
const requestTimeoutMs = typeof opts?.requestTimeoutMs === "number" && opts.requestTimeoutMs > 0 ? opts.requestTimeoutMs : DEFAULT_WALLET_REQUEST_TIMEOUT_MS;
|
|
468
|
+
const connectTimeoutMs = typeof opts?.connectTimeoutMs === "number" && opts.connectTimeoutMs > 0 ? opts.connectTimeoutMs : requestTimeoutMs;
|
|
469
|
+
const sendCallsTimeoutMs = typeof opts?.sendCallsTimeoutMs === "number" && opts.sendCallsTimeoutMs >= 0 ? opts.sendCallsTimeoutMs : 0;
|
|
470
|
+
return { requestTimeoutMs, connectTimeoutMs, sendCallsTimeoutMs };
|
|
471
|
+
}
|
|
472
|
+
function timeoutMsForMethod(method, opts) {
|
|
473
|
+
const p = resolvePolicy(opts);
|
|
474
|
+
if (MONEY_MOVING_METHODS.has(method)) return p.sendCallsTimeoutMs;
|
|
475
|
+
if (CONNECT_METHODS.has(method)) return p.connectTimeoutMs;
|
|
476
|
+
return p.requestTimeoutMs;
|
|
477
|
+
}
|
|
478
|
+
async function timedRequest(provider, args, timeoutMs = DEFAULT_WALLET_REQUEST_TIMEOUT_MS) {
|
|
479
|
+
if (timeoutMs <= 0) {
|
|
480
|
+
return provider.request(args);
|
|
481
|
+
}
|
|
482
|
+
let timer;
|
|
483
|
+
try {
|
|
484
|
+
return await Promise.race([
|
|
485
|
+
provider.request(args),
|
|
486
|
+
new Promise((_, reject) => {
|
|
487
|
+
timer = setTimeout(() => {
|
|
488
|
+
reject(
|
|
489
|
+
new WalletTimeoutError(
|
|
490
|
+
`Wallet request "${args.method}" timed out after ${timeoutMs}ms.`,
|
|
491
|
+
{ method: args.method, timeoutMs }
|
|
492
|
+
)
|
|
493
|
+
);
|
|
494
|
+
}, timeoutMs);
|
|
495
|
+
})
|
|
496
|
+
]);
|
|
497
|
+
} finally {
|
|
498
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function withTimeoutProvider(provider, opts) {
|
|
502
|
+
const flagged = provider;
|
|
503
|
+
if (flagged[TIMED_FLAG]) return provider;
|
|
504
|
+
const policy = typeof opts === "number" ? { requestTimeoutMs: opts, connectTimeoutMs: opts } : opts ?? {};
|
|
505
|
+
const wrapped = {
|
|
506
|
+
request: (args) => timedRequest(provider, args, timeoutMsForMethod(args.method, policy))
|
|
507
|
+
};
|
|
508
|
+
Object.defineProperty(wrapped, TIMED_FLAG, { value: true, enumerable: false });
|
|
509
|
+
return wrapped;
|
|
510
|
+
}
|
|
511
|
+
function resolveRawProvider(wallet) {
|
|
445
512
|
if (wallet?.provider) return wallet.provider;
|
|
446
513
|
const injected = globalThis.ethereum;
|
|
447
514
|
const connector = wallet?.connector ?? "base-account";
|
|
@@ -458,18 +525,23 @@ function resolveProvider(wallet) {
|
|
|
458
525
|
"base-account connector needs a provider. In the Base App it is injected automatically; elsewhere, create one with @base-org/account and pass it as wallet.provider."
|
|
459
526
|
);
|
|
460
527
|
}
|
|
461
|
-
|
|
528
|
+
function resolveProvider(wallet, opts) {
|
|
529
|
+
const raw = resolveRawProvider(wallet);
|
|
530
|
+
return withTimeoutProvider(raw, opts);
|
|
531
|
+
}
|
|
532
|
+
async function walletAvailable(wallet, opts) {
|
|
462
533
|
if (wallet?.provider) return true;
|
|
463
534
|
let provider;
|
|
464
535
|
try {
|
|
465
|
-
provider = resolveProvider(wallet);
|
|
536
|
+
provider = resolveProvider(wallet, opts);
|
|
466
537
|
} catch {
|
|
467
538
|
return false;
|
|
468
539
|
}
|
|
469
540
|
try {
|
|
470
541
|
await getAccount(provider);
|
|
471
542
|
return true;
|
|
472
|
-
} catch {
|
|
543
|
+
} catch (e) {
|
|
544
|
+
if (e instanceof WalletTimeoutError) throw e;
|
|
473
545
|
return false;
|
|
474
546
|
}
|
|
475
547
|
}
|
|
@@ -480,6 +552,7 @@ async function getAccount(provider) {
|
|
|
480
552
|
if (!addr) throw new Error("no account");
|
|
481
553
|
return addr;
|
|
482
554
|
} catch (e) {
|
|
555
|
+
if (e instanceof WalletTimeoutError) throw e;
|
|
483
556
|
throw new WalletConnectionError("Could not read the player's wallet account.", {
|
|
484
557
|
cause: e?.message
|
|
485
558
|
});
|
|
@@ -553,6 +626,7 @@ async function sendCalls(provider, from, chainId, calls, paymasterUrl) {
|
|
|
553
626
|
try {
|
|
554
627
|
result = await provider.request({ method: "wallet_sendCalls", params: [params] });
|
|
555
628
|
} catch (e) {
|
|
629
|
+
if (e instanceof WalletTimeoutError) throw e;
|
|
556
630
|
const msg = e?.message ?? String(e);
|
|
557
631
|
if (/reject|denied|cancel|closed/i.test(msg)) {
|
|
558
632
|
throw new WalletConnectionError("The player cancelled or closed the payment sheet.", { cause: msg });
|
|
@@ -567,14 +641,22 @@ async function waitForCalls(provider, id, timeoutMs = 6e4) {
|
|
|
567
641
|
if (!id) return { status: "FAILED" };
|
|
568
642
|
const deadline = Date.now() + timeoutMs;
|
|
569
643
|
while (Date.now() < deadline) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
644
|
+
try {
|
|
645
|
+
const res = await provider.request({
|
|
646
|
+
method: "wallet_getCallsStatus",
|
|
647
|
+
params: [id]
|
|
648
|
+
});
|
|
649
|
+
const s = String(res?.status ?? "").toUpperCase();
|
|
650
|
+
const txHash = res?.receipts?.[0]?.transactionHash;
|
|
651
|
+
if (s === "200" || s === "CONFIRMED" || s === "SUCCESS") return { status: "CONFIRMED", txHash };
|
|
652
|
+
if (s === "400" || s === "500" || s === "FAILED" || s === "REVERTED") return { status: "FAILED", txHash };
|
|
653
|
+
} catch (e) {
|
|
654
|
+
if (e instanceof WalletTimeoutError) {
|
|
655
|
+
await new Promise((r) => setTimeout(r, 900));
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
throw e;
|
|
659
|
+
}
|
|
578
660
|
await new Promise((r) => setTimeout(r, 900));
|
|
579
661
|
}
|
|
580
662
|
return { status: "PENDING" };
|
|
@@ -850,6 +932,49 @@ function validateX402ChallengeInput(input) {
|
|
|
850
932
|
|
|
851
933
|
// src/client.ts
|
|
852
934
|
var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
|
|
935
|
+
function mapAlreadyEntered(e) {
|
|
936
|
+
const msg = e instanceof Error ? e.message : typeof e === "object" && e && "message" in e ? String(e.message) : String(e);
|
|
937
|
+
const detail = e instanceof PaymentFailedError || e instanceof ApiError ? e.detail : void 0;
|
|
938
|
+
const blob = `${msg} ${JSON.stringify(detail ?? {})}`;
|
|
939
|
+
if (/AlreadyEntered|already entered|already_entered/i.test(blob)) {
|
|
940
|
+
throw new AlreadyEnteredError({ cause: msg, ...detail ?? {} });
|
|
941
|
+
}
|
|
942
|
+
throw e;
|
|
943
|
+
}
|
|
944
|
+
var SETTLE_RECONCILE_TIMEOUT_MS = 2e4;
|
|
945
|
+
var SETTLE_RECONCILE_INTERVAL_MS = 1e3;
|
|
946
|
+
function isSoftRoundPollError(e, kind) {
|
|
947
|
+
if (e instanceof AuthError || e instanceof ConfigError) return false;
|
|
948
|
+
if (e instanceof ApiError) {
|
|
949
|
+
const status = typeof e.detail?.status === "number" ? e.detail.status : void 0;
|
|
950
|
+
const msg = e.message;
|
|
951
|
+
if (status === 409 && /already in progress|in progress — retry/i.test(msg)) {
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
if (kind === "settle") {
|
|
955
|
+
if (/cannot settle|not Locked on-chain|is already settled|nothing to settle/i.test(msg)) {
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
if (/cannot cancel|is already settled|is Settled on-chain|already settled — cannot cancel/i.test(
|
|
960
|
+
msg
|
|
961
|
+
)) {
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (status === 502 || status === 503 || status === 504 || status === 429 || e.detail?.gateway === true) {
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
968
|
+
if (status !== void 0 && status >= 400 && status < 500) return false;
|
|
969
|
+
if (status !== void 0 && status >= 500) return true;
|
|
970
|
+
}
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
function isSoftSettlePollError(e) {
|
|
974
|
+
return isSoftRoundPollError(e, "settle");
|
|
975
|
+
}
|
|
976
|
+
var CANCEL_RECONCILE_TIMEOUT_MS = SETTLE_RECONCILE_TIMEOUT_MS;
|
|
977
|
+
var CANCEL_RECONCILE_INTERVAL_MS = SETTLE_RECONCILE_INTERVAL_MS;
|
|
853
978
|
function requireAddressField(value, field) {
|
|
854
979
|
if (typeof value !== "string" || value.trim() === "") {
|
|
855
980
|
throw new MissingFieldError(field);
|
|
@@ -1196,17 +1321,61 @@ var Playmos = class {
|
|
|
1196
1321
|
status: "settled"
|
|
1197
1322
|
};
|
|
1198
1323
|
}
|
|
1199
|
-
const
|
|
1324
|
+
const timeoutMs = input.timeoutMs ?? SETTLE_RECONCILE_TIMEOUT_MS;
|
|
1325
|
+
const intervalMs = input.intervalMs ?? SETTLE_RECONCILE_INTERVAL_MS;
|
|
1326
|
+
const body = { gameId: input.gameId, results: input.results };
|
|
1327
|
+
const idempotencyKey = `settle:${input.roundId}`;
|
|
1328
|
+
const postSettle = () => this.http.post(
|
|
1200
1329
|
`/rounds/${encodeURIComponent(input.roundId)}/settle`,
|
|
1201
|
-
|
|
1202
|
-
{ acceptStatuses: [202] }
|
|
1330
|
+
body,
|
|
1331
|
+
{ acceptStatuses: [200, 202], idempotencyKey }
|
|
1203
1332
|
);
|
|
1204
|
-
|
|
1333
|
+
let { settle } = await postSettle();
|
|
1334
|
+
if (settle.status === "settled") return settle;
|
|
1335
|
+
const firstWinners = settle.winners;
|
|
1336
|
+
const firstPoolPaid = settle.poolPaid ?? null;
|
|
1337
|
+
const withFirstExtras = (s) => ({
|
|
1338
|
+
...s,
|
|
1339
|
+
winners: s.winners ?? firstWinners,
|
|
1340
|
+
poolPaid: s.poolPaid ?? firstPoolPaid
|
|
1341
|
+
});
|
|
1342
|
+
const deadline = Date.now() + timeoutMs;
|
|
1343
|
+
while (Date.now() < deadline && settle.status === "settling") {
|
|
1344
|
+
const wait = Math.min(intervalMs, Math.max(0, deadline - Date.now()));
|
|
1345
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
1346
|
+
if (Date.now() >= deadline) break;
|
|
1347
|
+
try {
|
|
1348
|
+
const again = await postSettle();
|
|
1349
|
+
settle = withFirstExtras(again.settle);
|
|
1350
|
+
if (settle.status === "settled") return settle;
|
|
1351
|
+
} catch (e) {
|
|
1352
|
+
if (!isSoftSettlePollError(e)) throw e;
|
|
1353
|
+
}
|
|
1354
|
+
try {
|
|
1355
|
+
const round = await this.rounds.get({ roundId: input.roundId });
|
|
1356
|
+
if (round.status === "settled") {
|
|
1357
|
+
return {
|
|
1358
|
+
roundId: input.roundId,
|
|
1359
|
+
txHash: round.settleTxHash ?? settle.txHash ?? null,
|
|
1360
|
+
poolPaid: round.pool ?? settle.poolPaid ?? firstPoolPaid,
|
|
1361
|
+
winners: settle.winners ?? firstWinners,
|
|
1362
|
+
status: "settled"
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
} catch (e) {
|
|
1366
|
+
if (!isSoftSettlePollError(e)) throw e;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return withFirstExtras(settle);
|
|
1205
1370
|
},
|
|
1206
1371
|
/**
|
|
1207
1372
|
* Cancel an open/locked round (operator sk_ only). Refunds entrants' escrowed ~90%
|
|
1208
|
-
* and frees the series latch
|
|
1209
|
-
*
|
|
1373
|
+
* and frees the series latch (#346 / #376).
|
|
1374
|
+
*
|
|
1375
|
+
* **Default is submit-only:** may return `status: "cancelling"` after broadcast —
|
|
1376
|
+
* that is **not** success. Only `status: "cancelled"` means the chain is terminal.
|
|
1377
|
+
* Pass `{ confirm: true }` to poll until cancelled or timeout (honest `cancelling`
|
|
1378
|
+
* if still pending — never invents terminal success).
|
|
1210
1379
|
*/
|
|
1211
1380
|
cancel: async (input) => {
|
|
1212
1381
|
if (!this.config.mock) this.assertSecretKey("playmos.rounds.cancel");
|
|
@@ -1244,18 +1413,57 @@ var Playmos = class {
|
|
|
1244
1413
|
round: cancelled
|
|
1245
1414
|
};
|
|
1246
1415
|
}
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
)
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1416
|
+
const bodyPayload = { gameId: input.gameId };
|
|
1417
|
+
const postCancel = () => this.http.post(`/rounds/${encodeURIComponent(input.roundId)}/cancel`, bodyPayload, {
|
|
1418
|
+
acceptStatuses: [200, 202]
|
|
1419
|
+
});
|
|
1420
|
+
const toResult = (body, priorTx) => {
|
|
1421
|
+
const explicitCancelled = body.status === "cancelled" || body.round?.status === "cancelled";
|
|
1422
|
+
const explicitCancelling = body.status === "cancelling" || body.round?.status === "cancelling";
|
|
1423
|
+
const status = explicitCancelled && !explicitCancelling ? "cancelled" : "cancelling";
|
|
1424
|
+
const tx = body.txHash ?? body.round?.cancelTxHash ?? priorTx ?? null;
|
|
1425
|
+
return {
|
|
1426
|
+
roundId: input.roundId,
|
|
1427
|
+
txHash: tx,
|
|
1428
|
+
status,
|
|
1429
|
+
round: body.round
|
|
1430
|
+
};
|
|
1258
1431
|
};
|
|
1432
|
+
let result = toResult(await postCancel());
|
|
1433
|
+
if (result.status === "cancelled") return result;
|
|
1434
|
+
if (!input.confirm) return result;
|
|
1435
|
+
const timeoutMs = Math.min(
|
|
1436
|
+
Math.max(1, input.timeoutMs ?? CANCEL_RECONCILE_TIMEOUT_MS),
|
|
1437
|
+
CANCEL_RECONCILE_TIMEOUT_MS
|
|
1438
|
+
);
|
|
1439
|
+
const intervalMs = Math.max(1, input.intervalMs ?? CANCEL_RECONCILE_INTERVAL_MS);
|
|
1440
|
+
const deadline = Date.now() + timeoutMs;
|
|
1441
|
+
const firstTx = result.txHash;
|
|
1442
|
+
while (Date.now() < deadline && result.status === "cancelling") {
|
|
1443
|
+
const wait = Math.min(intervalMs, Math.max(0, deadline - Date.now()));
|
|
1444
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
1445
|
+
if (Date.now() >= deadline) break;
|
|
1446
|
+
try {
|
|
1447
|
+
result = toResult(await postCancel(), firstTx);
|
|
1448
|
+
if (result.status === "cancelled") return result;
|
|
1449
|
+
} catch (e) {
|
|
1450
|
+
if (!isSoftRoundPollError(e, "cancel")) throw e;
|
|
1451
|
+
}
|
|
1452
|
+
try {
|
|
1453
|
+
const round = await this.rounds.get({ roundId: input.roundId });
|
|
1454
|
+
if (round.status === "cancelled") {
|
|
1455
|
+
return {
|
|
1456
|
+
roundId: input.roundId,
|
|
1457
|
+
txHash: round.cancelTxHash ?? result.txHash ?? firstTx ?? null,
|
|
1458
|
+
status: "cancelled",
|
|
1459
|
+
round
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
} catch (e) {
|
|
1463
|
+
if (!isSoftRoundPollError(e, "cancel")) throw e;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
return { ...result, txHash: result.txHash ?? firstTx ?? null };
|
|
1259
1467
|
},
|
|
1260
1468
|
get: async (input) => {
|
|
1261
1469
|
requireField(input?.roundId, "roundId");
|
|
@@ -1406,7 +1614,7 @@ var Playmos = class {
|
|
|
1406
1614
|
}
|
|
1407
1615
|
prizePool = round.prizePoolAddress;
|
|
1408
1616
|
}
|
|
1409
|
-
const provider =
|
|
1617
|
+
const provider = this.walletProvider();
|
|
1410
1618
|
if (this.config.gas?.mode === "player") {
|
|
1411
1619
|
const from0 = await getAccount(provider);
|
|
1412
1620
|
await assertEnoughGas(provider, from0);
|
|
@@ -1423,18 +1631,31 @@ var Playmos = class {
|
|
|
1423
1631
|
const paymasterUrl = this.config.gas?.mode === "sponsored" ? this.config.gas.paymasterUrl : void 0;
|
|
1424
1632
|
let txHash;
|
|
1425
1633
|
let status = "pending";
|
|
1634
|
+
let callsId;
|
|
1426
1635
|
try {
|
|
1427
|
-
const
|
|
1636
|
+
const sent = await sendCalls(
|
|
1428
1637
|
provider,
|
|
1429
1638
|
from,
|
|
1430
1639
|
this.env.chainId,
|
|
1431
1640
|
[call],
|
|
1432
1641
|
paymasterUrl
|
|
1433
1642
|
);
|
|
1643
|
+
callsId = sent.id;
|
|
1434
1644
|
const waited = await waitForCalls(provider, callsId);
|
|
1435
1645
|
txHash = waited.txHash;
|
|
1436
1646
|
status = waited.status === "CONFIRMED" ? "confirmed" : waited.status === "FAILED" ? "failed" : "pending";
|
|
1437
1647
|
} catch (e) {
|
|
1648
|
+
if (e instanceof WalletTimeoutError && callsId) {
|
|
1649
|
+
return {
|
|
1650
|
+
prizePoolAddress: prizePool,
|
|
1651
|
+
amount: formatMicroToUsd(amountMicro),
|
|
1652
|
+
amountMicro: amountMicro.toString(),
|
|
1653
|
+
txHash,
|
|
1654
|
+
status: "pending",
|
|
1655
|
+
callsId
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
if (e instanceof WalletTimeoutError) throw e;
|
|
1438
1659
|
const msg = e?.message ?? String(e);
|
|
1439
1660
|
if (/NothingToWithdraw|nothing to withdraw/i.test(msg)) {
|
|
1440
1661
|
throw new NothingToWithdrawError({ prizePoolAddress: prizePool, wallet: from, cause: msg });
|
|
@@ -1454,7 +1675,8 @@ var Playmos = class {
|
|
|
1454
1675
|
amount: formatMicroToUsd(amountMicro),
|
|
1455
1676
|
amountMicro: amountMicro.toString(),
|
|
1456
1677
|
txHash,
|
|
1457
|
-
status
|
|
1678
|
+
status,
|
|
1679
|
+
...status === "pending" && callsId ? { callsId } : {}
|
|
1458
1680
|
};
|
|
1459
1681
|
}
|
|
1460
1682
|
};
|
|
@@ -1701,13 +1923,36 @@ var Playmos = class {
|
|
|
1701
1923
|
}
|
|
1702
1924
|
}
|
|
1703
1925
|
this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
|
|
1926
|
+
if (!config.mock) {
|
|
1927
|
+
try {
|
|
1928
|
+
resolveProvider(config.wallet, this.walletTimeoutPolicy());
|
|
1929
|
+
} catch {
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Wallet timeout policy (#462 / PR #463 residual).
|
|
1935
|
+
* - connectTimeoutMs → eth_requestAccounts only
|
|
1936
|
+
* - machine RPCs → 20s default
|
|
1937
|
+
* - wallet_sendCalls → unbounded (0) — never short-time a payment sheet
|
|
1938
|
+
*/
|
|
1939
|
+
walletTimeoutPolicy() {
|
|
1940
|
+
const connect = typeof this.config.connectTimeoutMs === "number" && this.config.connectTimeoutMs > 0 ? this.config.connectTimeoutMs : 2e4;
|
|
1941
|
+
return {
|
|
1942
|
+
connectTimeoutMs: connect,
|
|
1943
|
+
requestTimeoutMs: 2e4,
|
|
1944
|
+
sendCallsTimeoutMs: 0
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
walletProvider() {
|
|
1948
|
+
return resolveProvider(this.config.wallet, this.walletTimeoutPolicy());
|
|
1704
1949
|
}
|
|
1705
1950
|
/** Connect the player's wallet and return their address. */
|
|
1706
1951
|
async connect() {
|
|
1707
1952
|
if (this.config.mock) {
|
|
1708
1953
|
return "0x0000000000000000000000000000000000000001";
|
|
1709
1954
|
}
|
|
1710
|
-
return getAccount(
|
|
1955
|
+
return getAccount(this.walletProvider());
|
|
1711
1956
|
}
|
|
1712
1957
|
/**
|
|
1713
1958
|
* A DROP-IN payment provider for game kits that expect `{ connect, payEntry }`
|
|
@@ -1767,7 +2012,7 @@ var Playmos = class {
|
|
|
1767
2012
|
})
|
|
1768
2013
|
);
|
|
1769
2014
|
}
|
|
1770
|
-
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
2015
|
+
if (this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
|
|
1771
2016
|
const res = await this.http.post(
|
|
1772
2017
|
"/payments",
|
|
1773
2018
|
{
|
|
@@ -1805,7 +2050,7 @@ var Playmos = class {
|
|
|
1805
2050
|
},
|
|
1806
2051
|
{ idempotencyKey }
|
|
1807
2052
|
);
|
|
1808
|
-
const provider =
|
|
2053
|
+
const provider = this.walletProvider();
|
|
1809
2054
|
if (this.config.gas?.mode === "player") {
|
|
1810
2055
|
const from0 = await getAccount(provider);
|
|
1811
2056
|
await assertEnoughGas(provider, from0);
|
|
@@ -1850,7 +2095,17 @@ var Playmos = class {
|
|
|
1850
2095
|
})
|
|
1851
2096
|
);
|
|
1852
2097
|
}
|
|
1853
|
-
|
|
2098
|
+
const serverSettleNoWallet = this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy());
|
|
2099
|
+
if (!serverSettleNoWallet) {
|
|
2100
|
+
const pinned = typeof input.identity === "string" ? input.identity.trim() : "";
|
|
2101
|
+
if (!pinned) {
|
|
2102
|
+
throw new ConfigError(
|
|
2103
|
+
'enterRound: "identity" is required when using a player wallet. Pass the same identity your game server will use for hasEntered / entry confirm. One entry per identity \u2014 use a unique value per paid attempt (pay-per-play). Silent invent caused paid-but-not-admitted failures (#374 / #466). No-wallet sandbox smoke may omit identity (server-settle derives one).',
|
|
2104
|
+
{ field: "identity" }
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
if (serverSettleNoWallet) {
|
|
1854
2109
|
const pinnedRoundKey = input.roundKey ?? input.roundId;
|
|
1855
2110
|
const res = await this.http.post(
|
|
1856
2111
|
"/payments",
|
|
@@ -1895,7 +2150,7 @@ var Playmos = class {
|
|
|
1895
2150
|
},
|
|
1896
2151
|
{ idempotencyKey }
|
|
1897
2152
|
);
|
|
1898
|
-
const provider =
|
|
2153
|
+
const provider = this.walletProvider();
|
|
1899
2154
|
if (this.config.gas?.mode === "player") {
|
|
1900
2155
|
const from0 = await getAccount(provider);
|
|
1901
2156
|
await assertEnoughGas(provider, from0);
|
|
@@ -1906,12 +2161,28 @@ var Playmos = class {
|
|
|
1906
2161
|
if (!prizePool) throw new ConfigError("No PrizePool contract address for this game (service intent + config.contracts both empty).");
|
|
1907
2162
|
const roundKey = intent.clientParams?.roundKey ?? intent.clientParams?.roundId ?? input.roundKey ?? intent.payment.roundId ?? input.roundId;
|
|
1908
2163
|
if (!roundKey) throw new ConfigError("No roundKey/roundId for enterRound (service clientParams missing).");
|
|
1909
|
-
const identity = intent.clientParams?.identity ?? input.identity ?? intent.payment.identity
|
|
2164
|
+
const identity = intent.clientParams?.identity ?? input.identity ?? intent.payment.identity;
|
|
2165
|
+
if (!identity || !String(identity).trim()) {
|
|
2166
|
+
throw new ConfigError(
|
|
2167
|
+
"enterRound: missing on-chain identity after create-intent (pass input.identity).",
|
|
2168
|
+
{ field: "identity" }
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
1910
2171
|
const amountUnits = this.resolveUnits(intent, amountMicro);
|
|
1911
2172
|
const calls = buildEntryCalls({ usdc, prizePool, roundKey, identity, amountUnits });
|
|
1912
|
-
|
|
2173
|
+
let callsId;
|
|
2174
|
+
try {
|
|
2175
|
+
({ id: callsId } = await sendCalls(provider, from, this.env.chainId, calls, this.sponsorUrl(intent)));
|
|
2176
|
+
} catch (e) {
|
|
2177
|
+
throw mapAlreadyEntered(e);
|
|
2178
|
+
}
|
|
1913
2179
|
const { txHash } = await waitForCalls(provider, callsId);
|
|
1914
|
-
|
|
2180
|
+
let payment;
|
|
2181
|
+
try {
|
|
2182
|
+
payment = await this.settle(intent.payment.id, txHash);
|
|
2183
|
+
} catch (e) {
|
|
2184
|
+
throw mapAlreadyEntered(e);
|
|
2185
|
+
}
|
|
1915
2186
|
payment.identity = identity;
|
|
1916
2187
|
payment.prizePoolAddress = prizePool.toLowerCase();
|
|
1917
2188
|
payment.roundKey = roundKey;
|
|
@@ -2108,8 +2379,8 @@ var Playmos = class {
|
|
|
2108
2379
|
});
|
|
2109
2380
|
let raw;
|
|
2110
2381
|
try {
|
|
2111
|
-
if (await walletAvailable(this.config.wallet)) {
|
|
2112
|
-
const provider =
|
|
2382
|
+
if (await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
|
|
2383
|
+
const provider = this.walletProvider();
|
|
2113
2384
|
raw = await provider.request({
|
|
2114
2385
|
method: "eth_call",
|
|
2115
2386
|
params: [{ to: prizePool, data }, "latest"]
|
|
@@ -2136,7 +2407,7 @@ var Playmos = class {
|
|
|
2136
2407
|
raw = json.result;
|
|
2137
2408
|
}
|
|
2138
2409
|
} catch (e) {
|
|
2139
|
-
if (e instanceof ApiError) throw e;
|
|
2410
|
+
if (e instanceof ApiError || e instanceof WalletTimeoutError) throw e;
|
|
2140
2411
|
throw new ApiError(`Could not read withdrawable balance: ${e.message}`, {
|
|
2141
2412
|
prizePool,
|
|
2142
2413
|
wallet
|
|
@@ -2426,6 +2697,7 @@ function isX402PayloadAuthorization(auth) {
|
|
|
2426
2697
|
return auth.kind === "x402-payload";
|
|
2427
2698
|
}
|
|
2428
2699
|
|
|
2700
|
+
exports.AlreadyEnteredError = AlreadyEnteredError;
|
|
2429
2701
|
exports.ApiError = ApiError;
|
|
2430
2702
|
exports.AuthError = AuthError;
|
|
2431
2703
|
exports.CHAIN_ID = CHAIN_ID;
|
|
@@ -2443,6 +2715,7 @@ exports.PlaymosError = PlaymosError;
|
|
|
2443
2715
|
exports.USDC_ADDRESS = USDC_ADDRESS;
|
|
2444
2716
|
exports.USDC_DECIMALS = USDC_DECIMALS;
|
|
2445
2717
|
exports.WalletConnectionError = WalletConnectionError;
|
|
2718
|
+
exports.WalletTimeoutError = WalletTimeoutError;
|
|
2446
2719
|
exports.clientRuntimeSignals = clientRuntimeSignals;
|
|
2447
2720
|
exports.computeIapSplit = computeIapSplit;
|
|
2448
2721
|
exports.computePayout = computePayout;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { N as Network, P as PlaymosConfig, W as WebhookEvent, R as RoundOpenInput, a as RoundState, b as RoundSettleInput, S as SettleRoundResult, C as CancelRoundResult, A as ActiveSeriesRound,
|
|
2
|
-
export {
|
|
1
|
+
import { N as Network, P as PlaymosConfig, W as WebhookEvent, R as RoundOpenInput, a as RoundState, b as RoundSettleInput, S as SettleRoundResult, c as RoundCancelInput, C as CancelRoundResult, A as ActiveSeriesRound, d as PrizeBalance, e as WithdrawResult, f as AgentWallet, g as AgentFundResult, T as TransferResult, E as EscrowHoldInput, h as EscrowHoldResult, i as EscrowResolveResult, M as MarketplaceListInput, L as Listing, j as MarketplaceSaleResult, k as MarketplaceGetResult, l as PayInput, m as Payment, n as EnterRoundInput, o as TransferReconcile, p as WaitOptions, q as TransferInput, r as TransferConfirmOptions, V as VerifyResult, s as PayoutRule } from './errors-BMlWHsMb.cjs';
|
|
2
|
+
export { t as ActiveForSeriesResult, u as AgentEconomyConfig, v as AlreadyEnteredError, w as ApiError, x as AuthError, y as ConfigError, z as ContractConfig, B as Eip1193Provider, G as GasConfig, D as GasMode, I as InsufficientGasError, F as InvalidAmountError, H as ListingStatus, J as MarketplaceItem, K as MarketplaceSale, O as MissingFieldError, Q as NothingToWithdrawError, U as PaymentFailedError, X as PaymentStatus, Y as PlaymosError, Z as PlaymosErrorCode, _ as RetryOptions, $ as RoundStatus, a0 as WalletConfig, a1 as WalletConnectionError, a2 as WalletConnector, a3 as WalletTimeoutError, a4 as WebhookEventType } from './errors-BMlWHsMb.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Environment resolution + the canonical address book.
|
|
@@ -477,13 +477,14 @@ declare class Playmos {
|
|
|
477
477
|
settle: (input: RoundSettleInput) => Promise<SettleRoundResult>;
|
|
478
478
|
/**
|
|
479
479
|
* Cancel an open/locked round (operator sk_ only). Refunds entrants' escrowed ~90%
|
|
480
|
-
* and frees the series latch
|
|
481
|
-
*
|
|
480
|
+
* and frees the series latch (#346 / #376).
|
|
481
|
+
*
|
|
482
|
+
* **Default is submit-only:** may return `status: "cancelling"` after broadcast —
|
|
483
|
+
* that is **not** success. Only `status: "cancelled"` means the chain is terminal.
|
|
484
|
+
* Pass `{ confirm: true }` to poll until cancelled or timeout (honest `cancelling`
|
|
485
|
+
* if still pending — never invents terminal success).
|
|
482
486
|
*/
|
|
483
|
-
cancel: (input:
|
|
484
|
-
roundId: string;
|
|
485
|
-
gameId?: string;
|
|
486
|
-
}) => Promise<CancelRoundResult>;
|
|
487
|
+
cancel: (input: RoundCancelInput) => Promise<CancelRoundResult>;
|
|
487
488
|
get: (input: {
|
|
488
489
|
roundId: string;
|
|
489
490
|
}) => Promise<RoundState>;
|
|
@@ -606,6 +607,14 @@ declare class Playmos {
|
|
|
606
607
|
*/
|
|
607
608
|
private readonly mockPayments;
|
|
608
609
|
constructor(config: PlaymosConfig);
|
|
610
|
+
/**
|
|
611
|
+
* Wallet timeout policy (#462 / PR #463 residual).
|
|
612
|
+
* - connectTimeoutMs → eth_requestAccounts only
|
|
613
|
+
* - machine RPCs → 20s default
|
|
614
|
+
* - wallet_sendCalls → unbounded (0) — never short-time a payment sheet
|
|
615
|
+
*/
|
|
616
|
+
private walletTimeoutPolicy;
|
|
617
|
+
private walletProvider;
|
|
609
618
|
/** Connect the player's wallet and return their address. */
|
|
610
619
|
connect(): Promise<`0x${string}`>;
|
|
611
620
|
/**
|
|
@@ -848,4 +857,4 @@ declare function ulid(seedTime?: number): string;
|
|
|
848
857
|
/** `${prefix}_${ulid()}` — e.g. `pay_01J…`, `entry_01J…`, `idem_01J…`. */
|
|
849
858
|
declare function prefixedId(prefix: string): string;
|
|
850
859
|
|
|
851
|
-
export { ActiveSeriesRound, AgentFundResult, AgentWallet, type Authorization, CHAIN_ID, CancelRoundResult, type CreatePaymentRequirementInput, DEFAULT_API_BASE_URL, EnterRoundInput, EscrowHoldInput, EscrowHoldResult, EscrowResolveResult, Listing, MICRO_PER_USDC, MarketplaceGetResult, MarketplaceListInput, MarketplaceSaleResult, Network, PayInput, Payment, type PaymentRequirement, PayoutError, PayoutRule, PayoutRule as PayoutRuleCompute, Playmos, PlaymosConfig, PrizeBalance, RoundOpenInput, RoundSettleInput, RoundState, SettleRoundResult, type SettlementAsset, TransferConfirmOptions, TransferInput, TransferReconcile, TransferResult, USDC_ADDRESS, USDC_DECIMALS, VerifyResult, WaitOptions, type WalletSignatureAuthorization, WebhookEvent, WithdrawResult, type X402ChallengeInput, type X402ChallengeResult, type X402FulfillAuthorization, type X402PayInput, type X402PayloadAuthorization, type X402PayloadMode, type X402PaymentRequired, type X402SettleResult, clientRuntimeSignals, computeIapSplit, computePayout, computePoolSplit, createPaymentRequirement, createX402Challenge, decodePaymentHeader, encodePaymentHeader, formatMicroToUsd, isClientRuntime, isWalletSignatureAuthorization, isX402PayloadAuthorization, networkToCaip2, parsePaymentRequirement, parseUsdToMicro, prefixedId, previewEscrowFee, previewIapSplit, previewMarketplaceSplit, previewPoolSplit, previewTransferSplit, resolveEnv, serializePaymentRequirement, toX402PaymentRequired, ulid, validateX402ChallengeInput };
|
|
860
|
+
export { ActiveSeriesRound, AgentFundResult, AgentWallet, type Authorization, CHAIN_ID, CancelRoundResult, type CreatePaymentRequirementInput, DEFAULT_API_BASE_URL, EnterRoundInput, EscrowHoldInput, EscrowHoldResult, EscrowResolveResult, Listing, MICRO_PER_USDC, MarketplaceGetResult, MarketplaceListInput, MarketplaceSaleResult, Network, PayInput, Payment, type PaymentRequirement, PayoutError, PayoutRule, PayoutRule as PayoutRuleCompute, Playmos, PlaymosConfig, PrizeBalance, RoundCancelInput, RoundOpenInput, RoundSettleInput, RoundState, SettleRoundResult, type SettlementAsset, TransferConfirmOptions, TransferInput, TransferReconcile, TransferResult, USDC_ADDRESS, USDC_DECIMALS, VerifyResult, WaitOptions, type WalletSignatureAuthorization, WebhookEvent, WithdrawResult, type X402ChallengeInput, type X402ChallengeResult, type X402FulfillAuthorization, type X402PayInput, type X402PayloadAuthorization, type X402PayloadMode, type X402PaymentRequired, type X402SettleResult, clientRuntimeSignals, computeIapSplit, computePayout, computePoolSplit, createPaymentRequirement, createX402Challenge, decodePaymentHeader, encodePaymentHeader, formatMicroToUsd, isClientRuntime, isWalletSignatureAuthorization, isX402PayloadAuthorization, networkToCaip2, parsePaymentRequirement, parseUsdToMicro, prefixedId, previewEscrowFee, previewIapSplit, previewMarketplaceSplit, previewPoolSplit, previewTransferSplit, resolveEnv, serializePaymentRequirement, toX402PaymentRequired, ulid, validateX402ChallengeInput };
|