@playmos/sdk 0.3.8 → 0.3.9
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/dist/index.cjs +246 -139
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +246 -139
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -265,6 +265,112 @@ function validateMetadata(metadata) {
|
|
|
265
265
|
return out;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
// src/payout.ts
|
|
269
|
+
var PayoutError = class extends Error {
|
|
270
|
+
constructor(message) {
|
|
271
|
+
super(message);
|
|
272
|
+
this.code = "payout_invalid";
|
|
273
|
+
this.name = "PayoutError";
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
277
|
+
var BPS = 10000n;
|
|
278
|
+
function requireAddress(w, i) {
|
|
279
|
+
if (!ADDRESS_RE.test(w)) {
|
|
280
|
+
throw new PayoutError(`ranking[${i}] must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(w)}`);
|
|
281
|
+
}
|
|
282
|
+
return w.toLowerCase();
|
|
283
|
+
}
|
|
284
|
+
function parseUsdToMicroLoose(amount) {
|
|
285
|
+
if (typeof amount !== "string" || !/^\d+(\.\d{1,6})?$/.test(amount.trim())) {
|
|
286
|
+
throw new PayoutError(`invalid USD amount: ${JSON.stringify(amount)}`);
|
|
287
|
+
}
|
|
288
|
+
const [wholeRaw, frac = ""] = amount.trim().split(".");
|
|
289
|
+
const whole = wholeRaw ?? "0";
|
|
290
|
+
const fracPadded = (frac + "000000").slice(0, 6);
|
|
291
|
+
const micro = BigInt(whole) * 1000000n + BigInt(fracPadded);
|
|
292
|
+
if (micro <= 0n) throw new PayoutError(`amount must be > 0, got: ${JSON.stringify(amount)}`);
|
|
293
|
+
return micro;
|
|
294
|
+
}
|
|
295
|
+
function computePayout(pool, ranking, rule) {
|
|
296
|
+
if (typeof pool !== "bigint" || pool <= 0n) {
|
|
297
|
+
throw new PayoutError(`pool must be a positive bigint (micro-USDC), got: ${String(pool)}`);
|
|
298
|
+
}
|
|
299
|
+
if (!Array.isArray(ranking) || ranking.length === 0) {
|
|
300
|
+
throw new PayoutError("ranking must be a non-empty array of wallets (best-first)");
|
|
301
|
+
}
|
|
302
|
+
const wallets = ranking.map((w, i) => requireAddress(w, i));
|
|
303
|
+
const seen = /* @__PURE__ */ new Set();
|
|
304
|
+
for (const w of wallets) {
|
|
305
|
+
if (seen.has(w)) throw new PayoutError(`duplicate wallet in ranking: ${w}`);
|
|
306
|
+
seen.add(w);
|
|
307
|
+
}
|
|
308
|
+
if (rule.kind === "winner-take-all") {
|
|
309
|
+
return [{ wallet: wallets[0], amount: pool }];
|
|
310
|
+
}
|
|
311
|
+
if (rule.kind === "top-n") {
|
|
312
|
+
const splits = rule.splitsBps;
|
|
313
|
+
if (!Array.isArray(splits) || splits.length === 0) {
|
|
314
|
+
throw new PayoutError("top-n splitsBps must be a non-empty array");
|
|
315
|
+
}
|
|
316
|
+
if (splits.length > wallets.length) {
|
|
317
|
+
throw new PayoutError(
|
|
318
|
+
`top-n needs ${splits.length} ranked wallets but ranking only has ${wallets.length}`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
let sumBps = 0;
|
|
322
|
+
for (const b of splits) {
|
|
323
|
+
if (!Number.isInteger(b) || b <= 0 || b > 1e4) {
|
|
324
|
+
throw new PayoutError(`each splitsBps entry must be an integer in (0, 10000], got: ${b}`);
|
|
325
|
+
}
|
|
326
|
+
sumBps += b;
|
|
327
|
+
}
|
|
328
|
+
if (sumBps !== 1e4) {
|
|
329
|
+
throw new PayoutError(`splitsBps must sum to 10000, got ${sumBps}`);
|
|
330
|
+
}
|
|
331
|
+
const out = [];
|
|
332
|
+
let allocated = 0n;
|
|
333
|
+
for (let i = 0; i < splits.length; i++) {
|
|
334
|
+
const amt = pool * BigInt(splits[i]) / BPS;
|
|
335
|
+
out.push({ wallet: wallets[i], amount: amt });
|
|
336
|
+
allocated += amt;
|
|
337
|
+
}
|
|
338
|
+
const remainder = pool - allocated;
|
|
339
|
+
if (remainder < 0n) throw new PayoutError("internal: allocated more than pool");
|
|
340
|
+
out[0] = { wallet: out[0].wallet, amount: out[0].amount + remainder };
|
|
341
|
+
const filtered = out.filter((x) => x.amount > 0n);
|
|
342
|
+
if (filtered.length === 0) throw new PayoutError("payout produced no positive amounts");
|
|
343
|
+
const total = filtered.reduce((s, x) => s + x.amount, 0n);
|
|
344
|
+
if (total !== pool) throw new PayoutError(`internal: sum ${total} != pool ${pool}`);
|
|
345
|
+
return filtered;
|
|
346
|
+
}
|
|
347
|
+
if (rule.kind === "custom") {
|
|
348
|
+
const amounts = rule.amounts;
|
|
349
|
+
if (!Array.isArray(amounts) || amounts.length === 0) {
|
|
350
|
+
throw new PayoutError("custom amounts must be a non-empty array of USD strings");
|
|
351
|
+
}
|
|
352
|
+
if (amounts.length > wallets.length) {
|
|
353
|
+
throw new PayoutError(
|
|
354
|
+
`custom amounts has ${amounts.length} entries but ranking only has ${wallets.length}`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const out = [];
|
|
358
|
+
let total = 0n;
|
|
359
|
+
for (let i = 0; i < amounts.length; i++) {
|
|
360
|
+
const amt = parseUsdToMicroLoose(amounts[i]);
|
|
361
|
+
out.push({ wallet: wallets[i], amount: amt });
|
|
362
|
+
total += amt;
|
|
363
|
+
}
|
|
364
|
+
if (total !== pool) {
|
|
365
|
+
throw new PayoutError(
|
|
366
|
+
`custom amounts sum to ${total} micro-USDC but pool is ${pool} \u2014 must match exactly`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
throw new PayoutError(`unknown payout rule kind: ${JSON.stringify(rule.kind)}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
268
374
|
// src/http.ts
|
|
269
375
|
function resolveRetry(retry) {
|
|
270
376
|
const off = { maxRetries: 0, baseDelayMs: 500, maxDelayMs: 2e4 };
|
|
@@ -788,12 +894,12 @@ function mockVerifyResult(payment) {
|
|
|
788
894
|
}
|
|
789
895
|
|
|
790
896
|
// src/x402.ts
|
|
791
|
-
var
|
|
792
|
-
function
|
|
897
|
+
var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
|
|
898
|
+
function requireAddress2(value, field) {
|
|
793
899
|
if (typeof value !== "string" || value.trim() === "") {
|
|
794
900
|
throw new MissingFieldError(field);
|
|
795
901
|
}
|
|
796
|
-
if (!
|
|
902
|
+
if (!ADDRESS_RE2.test(value)) {
|
|
797
903
|
throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
|
|
798
904
|
field,
|
|
799
905
|
value
|
|
@@ -863,7 +969,7 @@ function createX402Challenge(requirement, extras) {
|
|
|
863
969
|
if (typeof requirement.id !== "string" || !requirement.id.startsWith("preq_")) {
|
|
864
970
|
throw new ConfigError('PaymentRequirement.id must be a "preq_\u2026" string', { id: requirement.id });
|
|
865
971
|
}
|
|
866
|
-
|
|
972
|
+
requireAddress2(requirement.payTo, "payTo");
|
|
867
973
|
parseUsdToMicro(requirement.amount);
|
|
868
974
|
const feeBps = extras?.feeBps ?? 0;
|
|
869
975
|
if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 1e4) {
|
|
@@ -873,7 +979,7 @@ function createX402Challenge(requirement, extras) {
|
|
|
873
979
|
);
|
|
874
980
|
}
|
|
875
981
|
const feeSink = extras?.feeSink ?? null;
|
|
876
|
-
if (feeBps > 0 && feeSink)
|
|
982
|
+
if (feeBps > 0 && feeSink) requireAddress2(feeSink, "feeSink");
|
|
877
983
|
const paymentRequired = toX402PaymentRequired(requirement, { feeBps, feeSink });
|
|
878
984
|
return {
|
|
879
985
|
status: 402,
|
|
@@ -901,7 +1007,7 @@ function validateX402ChallengeInput(input) {
|
|
|
901
1007
|
{ field: "url" }
|
|
902
1008
|
);
|
|
903
1009
|
}
|
|
904
|
-
const payTo =
|
|
1010
|
+
const payTo = requireAddress2(input.payTo, "payTo");
|
|
905
1011
|
parseUsdToMicro(input.amount);
|
|
906
1012
|
const intent = input.intent ?? "transfer";
|
|
907
1013
|
if (intent !== "transfer" && intent !== "marketplace.buy") {
|
|
@@ -917,7 +1023,7 @@ function validateX402ChallengeInput(input) {
|
|
|
917
1023
|
{ feeBps: input.feeBps }
|
|
918
1024
|
);
|
|
919
1025
|
}
|
|
920
|
-
const feeSink = input.feeSink === void 0 ? void 0 :
|
|
1026
|
+
const feeSink = input.feeSink === void 0 ? void 0 : requireAddress2(input.feeSink, "feeSink");
|
|
921
1027
|
return {
|
|
922
1028
|
payTo,
|
|
923
1029
|
amount: input.amount,
|
|
@@ -931,7 +1037,7 @@ function validateX402ChallengeInput(input) {
|
|
|
931
1037
|
}
|
|
932
1038
|
|
|
933
1039
|
// src/client.ts
|
|
934
|
-
var
|
|
1040
|
+
var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
|
|
935
1041
|
function mapAlreadyEntered(e) {
|
|
936
1042
|
const msg = e instanceof Error ? e.message : typeof e === "object" && e && "message" in e ? String(e.message) : String(e);
|
|
937
1043
|
const detail = e instanceof PaymentFailedError || e instanceof ApiError ? e.detail : void 0;
|
|
@@ -979,7 +1085,7 @@ function requireAddressField(value, field) {
|
|
|
979
1085
|
if (typeof value !== "string" || value.trim() === "") {
|
|
980
1086
|
throw new MissingFieldError(field);
|
|
981
1087
|
}
|
|
982
|
-
if (!
|
|
1088
|
+
if (!ADDRESS_RE3.test(value)) {
|
|
983
1089
|
throw new ConfigError(
|
|
984
1090
|
`${field} must be a 0x-prefixed 20-byte wallet address (Phase 1a transfers settle server-held wallets), got: ${JSON.stringify(value)}`,
|
|
985
1091
|
{ field, value }
|
|
@@ -1167,11 +1273,22 @@ var Playmos = class {
|
|
|
1167
1273
|
* rounds.settle({ ranking }) → contract pays winners.
|
|
1168
1274
|
*
|
|
1169
1275
|
* Operator txs are signed by the Playmos service (OPERATOR_ROLE key).
|
|
1276
|
+
*
|
|
1277
|
+
* Offline `mock: true`: settle applies the stored payout rule and the 60/30/10
|
|
1278
|
+
* pool leg across entries (#490). No series seed bank — mock pool is a floor,
|
|
1279
|
+
* not a forecast of a live series with inherited seed.
|
|
1170
1280
|
*/
|
|
1171
1281
|
/** Offline mock store for rounds.open/lock/settle/get when `mock: true` (3-game dogfood). */
|
|
1172
1282
|
this.mockRounds = /* @__PURE__ */ new Map();
|
|
1173
1283
|
/** Mock withdrawable credits after settle — key `${roundId}:${walletLower}` (#240 offline prize). */
|
|
1174
1284
|
this.mockWithdrawable = /* @__PURE__ */ new Map();
|
|
1285
|
+
/**
|
|
1286
|
+
* Mock pot after per-entry rake (PrizePool escrowed sum) — keyed by roundId (#490).
|
|
1287
|
+
* Payable pool at lock = pot × POOL_BPS / (POOL_BPS + SEED_BPS); inherited seed = 0 offline.
|
|
1288
|
+
*/
|
|
1289
|
+
this.mockPotMicro = /* @__PURE__ */ new Map();
|
|
1290
|
+
/** Persisted settle winners for re-settle replay (#490 C2) — never re-invent pool-to-everyone. */
|
|
1291
|
+
this.mockSettleWinners = /* @__PURE__ */ new Map();
|
|
1175
1292
|
this.rounds = {
|
|
1176
1293
|
open: async (input) => {
|
|
1177
1294
|
if (!this.config.mock) this.assertSecretKey("playmos.rounds.open");
|
|
@@ -1210,6 +1327,8 @@ var Playmos = class {
|
|
|
1210
1327
|
prizePoolAddress: this.config.contracts?.prizePool ?? "0x0000000000000000000000000000000000000002"
|
|
1211
1328
|
};
|
|
1212
1329
|
this.mockRounds.set(input.roundId, round);
|
|
1330
|
+
this.mockPotMicro.set(input.roundId, 0n);
|
|
1331
|
+
this.mockSettleWinners.delete(input.roundId);
|
|
1213
1332
|
return round;
|
|
1214
1333
|
}
|
|
1215
1334
|
const body = await this.http.post(
|
|
@@ -1242,11 +1361,12 @@ var Playmos = class {
|
|
|
1242
1361
|
code: "conflict"
|
|
1243
1362
|
});
|
|
1244
1363
|
}
|
|
1364
|
+
const payableMicro = this.mockPayablePoolMicro(existing);
|
|
1365
|
+
const poolUsd = payableMicro > 0n ? formatMicroToUsd(payableMicro) : existing.pool ?? existing.entryAmount;
|
|
1245
1366
|
const locked = {
|
|
1246
1367
|
...existing,
|
|
1247
1368
|
status: "locked",
|
|
1248
|
-
|
|
1249
|
-
pool: existing.pool ?? existing.entryAmount,
|
|
1369
|
+
pool: poolUsd,
|
|
1250
1370
|
entrants: existing.entrants ?? 0,
|
|
1251
1371
|
lockTxHash: MOCK_TX
|
|
1252
1372
|
};
|
|
@@ -1269,14 +1389,15 @@ var Playmos = class {
|
|
|
1269
1389
|
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1270
1390
|
}
|
|
1271
1391
|
if (existing.status === "settled" && existing.settleTxHash) {
|
|
1392
|
+
const stored = this.mockSettleWinners.get(input.roundId);
|
|
1272
1393
|
return {
|
|
1273
1394
|
roundId: input.roundId,
|
|
1274
1395
|
txHash: existing.settleTxHash,
|
|
1275
1396
|
poolPaid: existing.pool ?? "0",
|
|
1276
|
-
winners: "winners" in input.results ? input.results.winners : input.results.ranking.map((wallet) => ({
|
|
1397
|
+
winners: stored ?? ("winners" in input.results ? input.results.winners : input.results.ranking.map((wallet) => ({
|
|
1277
1398
|
wallet,
|
|
1278
|
-
amount:
|
|
1279
|
-
})),
|
|
1399
|
+
amount: "0"
|
|
1400
|
+
}))),
|
|
1280
1401
|
status: "settled"
|
|
1281
1402
|
};
|
|
1282
1403
|
}
|
|
@@ -1286,13 +1407,45 @@ var Playmos = class {
|
|
|
1286
1407
|
code: "conflict"
|
|
1287
1408
|
});
|
|
1288
1409
|
}
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1410
|
+
const payableMicro = existing.pool != null ? this.parseUsdToMicroLoose(existing.pool) : this.mockPayablePoolMicro(existing);
|
|
1411
|
+
const poolPaid = payableMicro > 0n ? formatMicroToUsd(payableMicro) : existing.pool ?? existing.entryAmount;
|
|
1412
|
+
let rows;
|
|
1413
|
+
if ("winners" in input.results) {
|
|
1414
|
+
rows = input.results.winners.map((w) => {
|
|
1415
|
+
let micro;
|
|
1416
|
+
try {
|
|
1417
|
+
micro = this.parseUsdToMicroLoose(String(w.amount ?? "0"));
|
|
1418
|
+
} catch (e) {
|
|
1419
|
+
this.mockPayoutApiError(e);
|
|
1420
|
+
}
|
|
1421
|
+
return {
|
|
1422
|
+
wallet: w.wallet,
|
|
1423
|
+
amount: formatMicroToUsd(micro),
|
|
1424
|
+
micro
|
|
1425
|
+
};
|
|
1426
|
+
});
|
|
1427
|
+
} else {
|
|
1428
|
+
if (payableMicro <= 0n) {
|
|
1429
|
+
rows = [];
|
|
1430
|
+
} else {
|
|
1431
|
+
try {
|
|
1432
|
+
const computed = computePayout(
|
|
1433
|
+
payableMicro,
|
|
1434
|
+
input.results.ranking,
|
|
1435
|
+
existing.payout
|
|
1436
|
+
);
|
|
1437
|
+
rows = computed.map((c) => ({
|
|
1438
|
+
wallet: c.wallet,
|
|
1439
|
+
amount: formatMicroToUsd(c.amount),
|
|
1440
|
+
micro: c.amount
|
|
1441
|
+
}));
|
|
1442
|
+
} catch (e) {
|
|
1443
|
+
this.mockPayoutApiError(e);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
const winners = rows.map(({ wallet, amount }) => ({ wallet, amount }));
|
|
1294
1448
|
const settleTxHash = MOCK_TX;
|
|
1295
|
-
const poolPaid = existing.pool ?? existing.entryAmount;
|
|
1296
1449
|
const settled = {
|
|
1297
1450
|
...existing,
|
|
1298
1451
|
status: "settled",
|
|
@@ -1300,18 +1453,11 @@ var Playmos = class {
|
|
|
1300
1453
|
pool: poolPaid
|
|
1301
1454
|
};
|
|
1302
1455
|
this.mockRounds.set(input.roundId, settled);
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
} catch {
|
|
1309
|
-
micro = 0n;
|
|
1310
|
-
}
|
|
1311
|
-
if (micro > 0n) {
|
|
1312
|
-
const k = `${input.roundId}:${wAddr}`;
|
|
1313
|
-
this.mockWithdrawable.set(k, (this.mockWithdrawable.get(k) ?? 0n) + micro);
|
|
1314
|
-
}
|
|
1456
|
+
this.mockSettleWinners.set(input.roundId, winners);
|
|
1457
|
+
for (const w of rows) {
|
|
1458
|
+
if (w.micro <= 0n) continue;
|
|
1459
|
+
const k = `${input.roundId}:${w.wallet.toLowerCase()}`;
|
|
1460
|
+
this.mockWithdrawable.set(k, (this.mockWithdrawable.get(k) ?? 0n) + w.micro);
|
|
1315
1461
|
}
|
|
1316
1462
|
return {
|
|
1317
1463
|
roundId: input.roundId,
|
|
@@ -1403,9 +1549,12 @@ var Playmos = class {
|
|
|
1403
1549
|
...existing,
|
|
1404
1550
|
status: "cancelled",
|
|
1405
1551
|
cancelTxHash: MOCK_TX,
|
|
1406
|
-
pool: null
|
|
1552
|
+
pool: null,
|
|
1553
|
+
entrants: 0
|
|
1407
1554
|
};
|
|
1408
1555
|
this.mockRounds.set(input.roundId, cancelled);
|
|
1556
|
+
this.mockPotMicro.set(input.roundId, 0n);
|
|
1557
|
+
this.mockSettleWinners.delete(input.roundId);
|
|
1409
1558
|
return {
|
|
1410
1559
|
roundId: input.roundId,
|
|
1411
1560
|
txHash: MOCK_TX,
|
|
@@ -1930,6 +2079,69 @@ var Playmos = class {
|
|
|
1930
2079
|
}
|
|
1931
2080
|
}
|
|
1932
2081
|
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Resolve mock round by roundId **or** roundKey (kit `entryProvider` keys by roundKey, #490 C3).
|
|
2084
|
+
*/
|
|
2085
|
+
findMockRound(idOrKey) {
|
|
2086
|
+
const direct = this.mockRounds.get(idOrKey);
|
|
2087
|
+
if (direct) return direct;
|
|
2088
|
+
for (const r of this.mockRounds.values()) {
|
|
2089
|
+
if (r.roundId === idOrKey || r.roundKey === idOrKey) return r;
|
|
2090
|
+
}
|
|
2091
|
+
return void 0;
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* 6dp USD → micro for mock winner amounts (#490 C1). Must not use 2dp `validateAmount`
|
|
2095
|
+
* (would zero 0.135 from top-n 30% of $0.45).
|
|
2096
|
+
*/
|
|
2097
|
+
parseUsdToMicroLoose(amount) {
|
|
2098
|
+
if (typeof amount !== "string" || !/^\d+(\.\d{1,6})?$/.test(amount.trim())) {
|
|
2099
|
+
throw new PayoutError(`invalid USD amount: ${JSON.stringify(amount)}`);
|
|
2100
|
+
}
|
|
2101
|
+
const [wholeRaw, frac = ""] = amount.trim().split(".");
|
|
2102
|
+
const whole = wholeRaw ?? "0";
|
|
2103
|
+
const fracPadded = (frac + "000000").slice(0, 6);
|
|
2104
|
+
const micro = BigInt(whole) * 1000000n + BigInt(fracPadded);
|
|
2105
|
+
if (micro < 0n) throw new PayoutError(`amount must be >= 0, got: ${JSON.stringify(amount)}`);
|
|
2106
|
+
return micro;
|
|
2107
|
+
}
|
|
2108
|
+
/** Live-shaped 400 for mock payout validation (C5) — never leak raw PayoutError. */
|
|
2109
|
+
mockPayoutApiError(e) {
|
|
2110
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2111
|
+
throw new ApiError(msg, { status: 400, code: "payout_invalid" });
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Accumulate one mock entry into the pot (chain parity: rake off top, then lock split).
|
|
2115
|
+
* No-op when no matching open/locked round — never 404 (C4).
|
|
2116
|
+
*/
|
|
2117
|
+
mockAccumulateEntry(idOrKey, amountMicro) {
|
|
2118
|
+
const round = this.findMockRound(idOrKey);
|
|
2119
|
+
if (!round) return;
|
|
2120
|
+
if (round.status !== "open" && round.status !== "locked") return;
|
|
2121
|
+
const rake = amountMicro * BigInt(RAKE_BPS) / 10000n;
|
|
2122
|
+
const escrowed = amountMicro - rake;
|
|
2123
|
+
const pot = (this.mockPotMicro.get(round.roundId) ?? 0n) + escrowed;
|
|
2124
|
+
this.mockPotMicro.set(round.roundId, pot);
|
|
2125
|
+
const entrants = (round.entrants ?? 0) + 1;
|
|
2126
|
+
const updated = { ...round, entrants };
|
|
2127
|
+
this.mockRounds.set(round.roundId, updated);
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Payable pool micro for mock lock/settle.
|
|
2131
|
+
* With recorded entries: pot × POOL/(POOL+SEED) (seed bank = 0 offline).
|
|
2132
|
+
* Zero entries: fall back to entryAmount micro (C4 — preserves existing suite).
|
|
2133
|
+
*/
|
|
2134
|
+
mockPayablePoolMicro(round) {
|
|
2135
|
+
const pot = this.mockPotMicro.get(round.roundId) ?? 0n;
|
|
2136
|
+
if (pot > 0n) {
|
|
2137
|
+
return pot * BigInt(POOL_BPS) / BigInt(POOL_BPS + SEED_BPS);
|
|
2138
|
+
}
|
|
2139
|
+
try {
|
|
2140
|
+
return this.parseUsdToMicroLoose(round.entryAmount);
|
|
2141
|
+
} catch {
|
|
2142
|
+
return 0n;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
1933
2145
|
/**
|
|
1934
2146
|
* Wallet timeout policy (#462 / PR #463 residual).
|
|
1935
2147
|
* - connectTimeoutMs → eth_requestAccounts only
|
|
@@ -2078,6 +2290,7 @@ var Playmos = class {
|
|
|
2078
2290
|
const metadata = validateMetadata(input.metadata);
|
|
2079
2291
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
2080
2292
|
if (this.config.mock) {
|
|
2293
|
+
this.mockAccumulateEntry(input.roundId, amountMicro);
|
|
2081
2294
|
return this.rememberMock(
|
|
2082
2295
|
mockEntryPayment({
|
|
2083
2296
|
amountMicro,
|
|
@@ -2498,112 +2711,6 @@ function previewPoolSplit(amount) {
|
|
|
2498
2711
|
};
|
|
2499
2712
|
}
|
|
2500
2713
|
|
|
2501
|
-
// src/payout.ts
|
|
2502
|
-
var PayoutError = class extends Error {
|
|
2503
|
-
constructor(message) {
|
|
2504
|
-
super(message);
|
|
2505
|
-
this.code = "payout_invalid";
|
|
2506
|
-
this.name = "PayoutError";
|
|
2507
|
-
}
|
|
2508
|
-
};
|
|
2509
|
-
var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
|
|
2510
|
-
var BPS = 10000n;
|
|
2511
|
-
function requireAddress2(w, i) {
|
|
2512
|
-
if (!ADDRESS_RE3.test(w)) {
|
|
2513
|
-
throw new PayoutError(`ranking[${i}] must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(w)}`);
|
|
2514
|
-
}
|
|
2515
|
-
return w.toLowerCase();
|
|
2516
|
-
}
|
|
2517
|
-
function parseUsdToMicroLoose(amount) {
|
|
2518
|
-
if (typeof amount !== "string" || !/^\d+(\.\d{1,6})?$/.test(amount.trim())) {
|
|
2519
|
-
throw new PayoutError(`invalid USD amount: ${JSON.stringify(amount)}`);
|
|
2520
|
-
}
|
|
2521
|
-
const [wholeRaw, frac = ""] = amount.trim().split(".");
|
|
2522
|
-
const whole = wholeRaw ?? "0";
|
|
2523
|
-
const fracPadded = (frac + "000000").slice(0, 6);
|
|
2524
|
-
const micro = BigInt(whole) * 1000000n + BigInt(fracPadded);
|
|
2525
|
-
if (micro <= 0n) throw new PayoutError(`amount must be > 0, got: ${JSON.stringify(amount)}`);
|
|
2526
|
-
return micro;
|
|
2527
|
-
}
|
|
2528
|
-
function computePayout(pool, ranking, rule) {
|
|
2529
|
-
if (typeof pool !== "bigint" || pool <= 0n) {
|
|
2530
|
-
throw new PayoutError(`pool must be a positive bigint (micro-USDC), got: ${String(pool)}`);
|
|
2531
|
-
}
|
|
2532
|
-
if (!Array.isArray(ranking) || ranking.length === 0) {
|
|
2533
|
-
throw new PayoutError("ranking must be a non-empty array of wallets (best-first)");
|
|
2534
|
-
}
|
|
2535
|
-
const wallets = ranking.map((w, i) => requireAddress2(w, i));
|
|
2536
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2537
|
-
for (const w of wallets) {
|
|
2538
|
-
if (seen.has(w)) throw new PayoutError(`duplicate wallet in ranking: ${w}`);
|
|
2539
|
-
seen.add(w);
|
|
2540
|
-
}
|
|
2541
|
-
if (rule.kind === "winner-take-all") {
|
|
2542
|
-
return [{ wallet: wallets[0], amount: pool }];
|
|
2543
|
-
}
|
|
2544
|
-
if (rule.kind === "top-n") {
|
|
2545
|
-
const splits = rule.splitsBps;
|
|
2546
|
-
if (!Array.isArray(splits) || splits.length === 0) {
|
|
2547
|
-
throw new PayoutError("top-n splitsBps must be a non-empty array");
|
|
2548
|
-
}
|
|
2549
|
-
if (splits.length > wallets.length) {
|
|
2550
|
-
throw new PayoutError(
|
|
2551
|
-
`top-n needs ${splits.length} ranked wallets but ranking only has ${wallets.length}`
|
|
2552
|
-
);
|
|
2553
|
-
}
|
|
2554
|
-
let sumBps = 0;
|
|
2555
|
-
for (const b of splits) {
|
|
2556
|
-
if (!Number.isInteger(b) || b <= 0 || b > 1e4) {
|
|
2557
|
-
throw new PayoutError(`each splitsBps entry must be an integer in (0, 10000], got: ${b}`);
|
|
2558
|
-
}
|
|
2559
|
-
sumBps += b;
|
|
2560
|
-
}
|
|
2561
|
-
if (sumBps !== 1e4) {
|
|
2562
|
-
throw new PayoutError(`splitsBps must sum to 10000, got ${sumBps}`);
|
|
2563
|
-
}
|
|
2564
|
-
const out = [];
|
|
2565
|
-
let allocated = 0n;
|
|
2566
|
-
for (let i = 0; i < splits.length; i++) {
|
|
2567
|
-
const amt = pool * BigInt(splits[i]) / BPS;
|
|
2568
|
-
out.push({ wallet: wallets[i], amount: amt });
|
|
2569
|
-
allocated += amt;
|
|
2570
|
-
}
|
|
2571
|
-
const remainder = pool - allocated;
|
|
2572
|
-
if (remainder < 0n) throw new PayoutError("internal: allocated more than pool");
|
|
2573
|
-
out[0] = { wallet: out[0].wallet, amount: out[0].amount + remainder };
|
|
2574
|
-
const filtered = out.filter((x) => x.amount > 0n);
|
|
2575
|
-
if (filtered.length === 0) throw new PayoutError("payout produced no positive amounts");
|
|
2576
|
-
const total = filtered.reduce((s, x) => s + x.amount, 0n);
|
|
2577
|
-
if (total !== pool) throw new PayoutError(`internal: sum ${total} != pool ${pool}`);
|
|
2578
|
-
return filtered;
|
|
2579
|
-
}
|
|
2580
|
-
if (rule.kind === "custom") {
|
|
2581
|
-
const amounts = rule.amounts;
|
|
2582
|
-
if (!Array.isArray(amounts) || amounts.length === 0) {
|
|
2583
|
-
throw new PayoutError("custom amounts must be a non-empty array of USD strings");
|
|
2584
|
-
}
|
|
2585
|
-
if (amounts.length > wallets.length) {
|
|
2586
|
-
throw new PayoutError(
|
|
2587
|
-
`custom amounts has ${amounts.length} entries but ranking only has ${wallets.length}`
|
|
2588
|
-
);
|
|
2589
|
-
}
|
|
2590
|
-
const out = [];
|
|
2591
|
-
let total = 0n;
|
|
2592
|
-
for (let i = 0; i < amounts.length; i++) {
|
|
2593
|
-
const amt = parseUsdToMicroLoose(amounts[i]);
|
|
2594
|
-
out.push({ wallet: wallets[i], amount: amt });
|
|
2595
|
-
total += amt;
|
|
2596
|
-
}
|
|
2597
|
-
if (total !== pool) {
|
|
2598
|
-
throw new PayoutError(
|
|
2599
|
-
`custom amounts sum to ${total} micro-USDC but pool is ${pool} \u2014 must match exactly`
|
|
2600
|
-
);
|
|
2601
|
-
}
|
|
2602
|
-
return out;
|
|
2603
|
-
}
|
|
2604
|
-
throw new PayoutError(`unknown payout rule kind: ${JSON.stringify(rule.kind)}`);
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
2714
|
// src/settlement.ts
|
|
2608
2715
|
var ADDRESS_RE4 = /^0x[0-9a-fA-F]{40}$/;
|
|
2609
2716
|
var DEFAULT_TTL_MS = 15 * 60 * 1e3;
|
package/dist/index.d.cts
CHANGED
|
@@ -463,11 +463,44 @@ declare class Playmos {
|
|
|
463
463
|
* rounds.settle({ ranking }) → contract pays winners.
|
|
464
464
|
*
|
|
465
465
|
* Operator txs are signed by the Playmos service (OPERATOR_ROLE key).
|
|
466
|
+
*
|
|
467
|
+
* Offline `mock: true`: settle applies the stored payout rule and the 60/30/10
|
|
468
|
+
* pool leg across entries (#490). No series seed bank — mock pool is a floor,
|
|
469
|
+
* not a forecast of a live series with inherited seed.
|
|
466
470
|
*/
|
|
467
471
|
/** Offline mock store for rounds.open/lock/settle/get when `mock: true` (3-game dogfood). */
|
|
468
472
|
private readonly mockRounds;
|
|
469
473
|
/** Mock withdrawable credits after settle — key `${roundId}:${walletLower}` (#240 offline prize). */
|
|
470
474
|
private readonly mockWithdrawable;
|
|
475
|
+
/**
|
|
476
|
+
* Mock pot after per-entry rake (PrizePool escrowed sum) — keyed by roundId (#490).
|
|
477
|
+
* Payable pool at lock = pot × POOL_BPS / (POOL_BPS + SEED_BPS); inherited seed = 0 offline.
|
|
478
|
+
*/
|
|
479
|
+
private readonly mockPotMicro;
|
|
480
|
+
/** Persisted settle winners for re-settle replay (#490 C2) — never re-invent pool-to-everyone. */
|
|
481
|
+
private readonly mockSettleWinners;
|
|
482
|
+
/**
|
|
483
|
+
* Resolve mock round by roundId **or** roundKey (kit `entryProvider` keys by roundKey, #490 C3).
|
|
484
|
+
*/
|
|
485
|
+
private findMockRound;
|
|
486
|
+
/**
|
|
487
|
+
* 6dp USD → micro for mock winner amounts (#490 C1). Must not use 2dp `validateAmount`
|
|
488
|
+
* (would zero 0.135 from top-n 30% of $0.45).
|
|
489
|
+
*/
|
|
490
|
+
private parseUsdToMicroLoose;
|
|
491
|
+
/** Live-shaped 400 for mock payout validation (C5) — never leak raw PayoutError. */
|
|
492
|
+
private mockPayoutApiError;
|
|
493
|
+
/**
|
|
494
|
+
* Accumulate one mock entry into the pot (chain parity: rake off top, then lock split).
|
|
495
|
+
* No-op when no matching open/locked round — never 404 (C4).
|
|
496
|
+
*/
|
|
497
|
+
private mockAccumulateEntry;
|
|
498
|
+
/**
|
|
499
|
+
* Payable pool micro for mock lock/settle.
|
|
500
|
+
* With recorded entries: pot × POOL/(POOL+SEED) (seed bank = 0 offline).
|
|
501
|
+
* Zero entries: fall back to entryAmount micro (C4 — preserves existing suite).
|
|
502
|
+
*/
|
|
503
|
+
private mockPayablePoolMicro;
|
|
471
504
|
readonly rounds: {
|
|
472
505
|
open: (input: RoundOpenInput) => Promise<RoundState>;
|
|
473
506
|
lock: (input: {
|
package/dist/index.d.ts
CHANGED
|
@@ -463,11 +463,44 @@ declare class Playmos {
|
|
|
463
463
|
* rounds.settle({ ranking }) → contract pays winners.
|
|
464
464
|
*
|
|
465
465
|
* Operator txs are signed by the Playmos service (OPERATOR_ROLE key).
|
|
466
|
+
*
|
|
467
|
+
* Offline `mock: true`: settle applies the stored payout rule and the 60/30/10
|
|
468
|
+
* pool leg across entries (#490). No series seed bank — mock pool is a floor,
|
|
469
|
+
* not a forecast of a live series with inherited seed.
|
|
466
470
|
*/
|
|
467
471
|
/** Offline mock store for rounds.open/lock/settle/get when `mock: true` (3-game dogfood). */
|
|
468
472
|
private readonly mockRounds;
|
|
469
473
|
/** Mock withdrawable credits after settle — key `${roundId}:${walletLower}` (#240 offline prize). */
|
|
470
474
|
private readonly mockWithdrawable;
|
|
475
|
+
/**
|
|
476
|
+
* Mock pot after per-entry rake (PrizePool escrowed sum) — keyed by roundId (#490).
|
|
477
|
+
* Payable pool at lock = pot × POOL_BPS / (POOL_BPS + SEED_BPS); inherited seed = 0 offline.
|
|
478
|
+
*/
|
|
479
|
+
private readonly mockPotMicro;
|
|
480
|
+
/** Persisted settle winners for re-settle replay (#490 C2) — never re-invent pool-to-everyone. */
|
|
481
|
+
private readonly mockSettleWinners;
|
|
482
|
+
/**
|
|
483
|
+
* Resolve mock round by roundId **or** roundKey (kit `entryProvider` keys by roundKey, #490 C3).
|
|
484
|
+
*/
|
|
485
|
+
private findMockRound;
|
|
486
|
+
/**
|
|
487
|
+
* 6dp USD → micro for mock winner amounts (#490 C1). Must not use 2dp `validateAmount`
|
|
488
|
+
* (would zero 0.135 from top-n 30% of $0.45).
|
|
489
|
+
*/
|
|
490
|
+
private parseUsdToMicroLoose;
|
|
491
|
+
/** Live-shaped 400 for mock payout validation (C5) — never leak raw PayoutError. */
|
|
492
|
+
private mockPayoutApiError;
|
|
493
|
+
/**
|
|
494
|
+
* Accumulate one mock entry into the pot (chain parity: rake off top, then lock split).
|
|
495
|
+
* No-op when no matching open/locked round — never 404 (C4).
|
|
496
|
+
*/
|
|
497
|
+
private mockAccumulateEntry;
|
|
498
|
+
/**
|
|
499
|
+
* Payable pool micro for mock lock/settle.
|
|
500
|
+
* With recorded entries: pot × POOL/(POOL+SEED) (seed bank = 0 offline).
|
|
501
|
+
* Zero entries: fall back to entryAmount micro (C4 — preserves existing suite).
|
|
502
|
+
*/
|
|
503
|
+
private mockPayablePoolMicro;
|
|
471
504
|
readonly rounds: {
|
|
472
505
|
open: (input: RoundOpenInput) => Promise<RoundState>;
|
|
473
506
|
lock: (input: {
|
package/dist/index.js
CHANGED
|
@@ -181,6 +181,112 @@ function validateMetadata(metadata) {
|
|
|
181
181
|
return out;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// src/payout.ts
|
|
185
|
+
var PayoutError = class extends Error {
|
|
186
|
+
constructor(message) {
|
|
187
|
+
super(message);
|
|
188
|
+
this.code = "payout_invalid";
|
|
189
|
+
this.name = "PayoutError";
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
193
|
+
var BPS = 10000n;
|
|
194
|
+
function requireAddress(w, i) {
|
|
195
|
+
if (!ADDRESS_RE.test(w)) {
|
|
196
|
+
throw new PayoutError(`ranking[${i}] must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(w)}`);
|
|
197
|
+
}
|
|
198
|
+
return w.toLowerCase();
|
|
199
|
+
}
|
|
200
|
+
function parseUsdToMicroLoose(amount) {
|
|
201
|
+
if (typeof amount !== "string" || !/^\d+(\.\d{1,6})?$/.test(amount.trim())) {
|
|
202
|
+
throw new PayoutError(`invalid USD amount: ${JSON.stringify(amount)}`);
|
|
203
|
+
}
|
|
204
|
+
const [wholeRaw, frac = ""] = amount.trim().split(".");
|
|
205
|
+
const whole = wholeRaw ?? "0";
|
|
206
|
+
const fracPadded = (frac + "000000").slice(0, 6);
|
|
207
|
+
const micro = BigInt(whole) * 1000000n + BigInt(fracPadded);
|
|
208
|
+
if (micro <= 0n) throw new PayoutError(`amount must be > 0, got: ${JSON.stringify(amount)}`);
|
|
209
|
+
return micro;
|
|
210
|
+
}
|
|
211
|
+
function computePayout(pool, ranking, rule) {
|
|
212
|
+
if (typeof pool !== "bigint" || pool <= 0n) {
|
|
213
|
+
throw new PayoutError(`pool must be a positive bigint (micro-USDC), got: ${String(pool)}`);
|
|
214
|
+
}
|
|
215
|
+
if (!Array.isArray(ranking) || ranking.length === 0) {
|
|
216
|
+
throw new PayoutError("ranking must be a non-empty array of wallets (best-first)");
|
|
217
|
+
}
|
|
218
|
+
const wallets = ranking.map((w, i) => requireAddress(w, i));
|
|
219
|
+
const seen = /* @__PURE__ */ new Set();
|
|
220
|
+
for (const w of wallets) {
|
|
221
|
+
if (seen.has(w)) throw new PayoutError(`duplicate wallet in ranking: ${w}`);
|
|
222
|
+
seen.add(w);
|
|
223
|
+
}
|
|
224
|
+
if (rule.kind === "winner-take-all") {
|
|
225
|
+
return [{ wallet: wallets[0], amount: pool }];
|
|
226
|
+
}
|
|
227
|
+
if (rule.kind === "top-n") {
|
|
228
|
+
const splits = rule.splitsBps;
|
|
229
|
+
if (!Array.isArray(splits) || splits.length === 0) {
|
|
230
|
+
throw new PayoutError("top-n splitsBps must be a non-empty array");
|
|
231
|
+
}
|
|
232
|
+
if (splits.length > wallets.length) {
|
|
233
|
+
throw new PayoutError(
|
|
234
|
+
`top-n needs ${splits.length} ranked wallets but ranking only has ${wallets.length}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
let sumBps = 0;
|
|
238
|
+
for (const b of splits) {
|
|
239
|
+
if (!Number.isInteger(b) || b <= 0 || b > 1e4) {
|
|
240
|
+
throw new PayoutError(`each splitsBps entry must be an integer in (0, 10000], got: ${b}`);
|
|
241
|
+
}
|
|
242
|
+
sumBps += b;
|
|
243
|
+
}
|
|
244
|
+
if (sumBps !== 1e4) {
|
|
245
|
+
throw new PayoutError(`splitsBps must sum to 10000, got ${sumBps}`);
|
|
246
|
+
}
|
|
247
|
+
const out = [];
|
|
248
|
+
let allocated = 0n;
|
|
249
|
+
for (let i = 0; i < splits.length; i++) {
|
|
250
|
+
const amt = pool * BigInt(splits[i]) / BPS;
|
|
251
|
+
out.push({ wallet: wallets[i], amount: amt });
|
|
252
|
+
allocated += amt;
|
|
253
|
+
}
|
|
254
|
+
const remainder = pool - allocated;
|
|
255
|
+
if (remainder < 0n) throw new PayoutError("internal: allocated more than pool");
|
|
256
|
+
out[0] = { wallet: out[0].wallet, amount: out[0].amount + remainder };
|
|
257
|
+
const filtered = out.filter((x) => x.amount > 0n);
|
|
258
|
+
if (filtered.length === 0) throw new PayoutError("payout produced no positive amounts");
|
|
259
|
+
const total = filtered.reduce((s, x) => s + x.amount, 0n);
|
|
260
|
+
if (total !== pool) throw new PayoutError(`internal: sum ${total} != pool ${pool}`);
|
|
261
|
+
return filtered;
|
|
262
|
+
}
|
|
263
|
+
if (rule.kind === "custom") {
|
|
264
|
+
const amounts = rule.amounts;
|
|
265
|
+
if (!Array.isArray(amounts) || amounts.length === 0) {
|
|
266
|
+
throw new PayoutError("custom amounts must be a non-empty array of USD strings");
|
|
267
|
+
}
|
|
268
|
+
if (amounts.length > wallets.length) {
|
|
269
|
+
throw new PayoutError(
|
|
270
|
+
`custom amounts has ${amounts.length} entries but ranking only has ${wallets.length}`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const out = [];
|
|
274
|
+
let total = 0n;
|
|
275
|
+
for (let i = 0; i < amounts.length; i++) {
|
|
276
|
+
const amt = parseUsdToMicroLoose(amounts[i]);
|
|
277
|
+
out.push({ wallet: wallets[i], amount: amt });
|
|
278
|
+
total += amt;
|
|
279
|
+
}
|
|
280
|
+
if (total !== pool) {
|
|
281
|
+
throw new PayoutError(
|
|
282
|
+
`custom amounts sum to ${total} micro-USDC but pool is ${pool} \u2014 must match exactly`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
throw new PayoutError(`unknown payout rule kind: ${JSON.stringify(rule.kind)}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
184
290
|
// src/http.ts
|
|
185
291
|
function resolveRetry(retry) {
|
|
186
292
|
const off = { maxRetries: 0, baseDelayMs: 500, maxDelayMs: 2e4 };
|
|
@@ -704,12 +810,12 @@ function mockVerifyResult(payment) {
|
|
|
704
810
|
}
|
|
705
811
|
|
|
706
812
|
// src/x402.ts
|
|
707
|
-
var
|
|
708
|
-
function
|
|
813
|
+
var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
|
|
814
|
+
function requireAddress2(value, field) {
|
|
709
815
|
if (typeof value !== "string" || value.trim() === "") {
|
|
710
816
|
throw new MissingFieldError(field);
|
|
711
817
|
}
|
|
712
|
-
if (!
|
|
818
|
+
if (!ADDRESS_RE2.test(value)) {
|
|
713
819
|
throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
|
|
714
820
|
field,
|
|
715
821
|
value
|
|
@@ -779,7 +885,7 @@ function createX402Challenge(requirement, extras) {
|
|
|
779
885
|
if (typeof requirement.id !== "string" || !requirement.id.startsWith("preq_")) {
|
|
780
886
|
throw new ConfigError('PaymentRequirement.id must be a "preq_\u2026" string', { id: requirement.id });
|
|
781
887
|
}
|
|
782
|
-
|
|
888
|
+
requireAddress2(requirement.payTo, "payTo");
|
|
783
889
|
parseUsdToMicro(requirement.amount);
|
|
784
890
|
const feeBps = extras?.feeBps ?? 0;
|
|
785
891
|
if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 1e4) {
|
|
@@ -789,7 +895,7 @@ function createX402Challenge(requirement, extras) {
|
|
|
789
895
|
);
|
|
790
896
|
}
|
|
791
897
|
const feeSink = extras?.feeSink ?? null;
|
|
792
|
-
if (feeBps > 0 && feeSink)
|
|
898
|
+
if (feeBps > 0 && feeSink) requireAddress2(feeSink, "feeSink");
|
|
793
899
|
const paymentRequired = toX402PaymentRequired(requirement, { feeBps, feeSink });
|
|
794
900
|
return {
|
|
795
901
|
status: 402,
|
|
@@ -817,7 +923,7 @@ function validateX402ChallengeInput(input) {
|
|
|
817
923
|
{ field: "url" }
|
|
818
924
|
);
|
|
819
925
|
}
|
|
820
|
-
const payTo =
|
|
926
|
+
const payTo = requireAddress2(input.payTo, "payTo");
|
|
821
927
|
parseUsdToMicro(input.amount);
|
|
822
928
|
const intent = input.intent ?? "transfer";
|
|
823
929
|
if (intent !== "transfer" && intent !== "marketplace.buy") {
|
|
@@ -833,7 +939,7 @@ function validateX402ChallengeInput(input) {
|
|
|
833
939
|
{ feeBps: input.feeBps }
|
|
834
940
|
);
|
|
835
941
|
}
|
|
836
|
-
const feeSink = input.feeSink === void 0 ? void 0 :
|
|
942
|
+
const feeSink = input.feeSink === void 0 ? void 0 : requireAddress2(input.feeSink, "feeSink");
|
|
837
943
|
return {
|
|
838
944
|
payTo,
|
|
839
945
|
amount: input.amount,
|
|
@@ -847,7 +953,7 @@ function validateX402ChallengeInput(input) {
|
|
|
847
953
|
}
|
|
848
954
|
|
|
849
955
|
// src/client.ts
|
|
850
|
-
var
|
|
956
|
+
var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
|
|
851
957
|
function mapAlreadyEntered(e) {
|
|
852
958
|
const msg = e instanceof Error ? e.message : typeof e === "object" && e && "message" in e ? String(e.message) : String(e);
|
|
853
959
|
const detail = e instanceof PaymentFailedError || e instanceof ApiError ? e.detail : void 0;
|
|
@@ -895,7 +1001,7 @@ function requireAddressField(value, field) {
|
|
|
895
1001
|
if (typeof value !== "string" || value.trim() === "") {
|
|
896
1002
|
throw new MissingFieldError(field);
|
|
897
1003
|
}
|
|
898
|
-
if (!
|
|
1004
|
+
if (!ADDRESS_RE3.test(value)) {
|
|
899
1005
|
throw new ConfigError(
|
|
900
1006
|
`${field} must be a 0x-prefixed 20-byte wallet address (Phase 1a transfers settle server-held wallets), got: ${JSON.stringify(value)}`,
|
|
901
1007
|
{ field, value }
|
|
@@ -1083,11 +1189,22 @@ var Playmos = class {
|
|
|
1083
1189
|
* rounds.settle({ ranking }) → contract pays winners.
|
|
1084
1190
|
*
|
|
1085
1191
|
* Operator txs are signed by the Playmos service (OPERATOR_ROLE key).
|
|
1192
|
+
*
|
|
1193
|
+
* Offline `mock: true`: settle applies the stored payout rule and the 60/30/10
|
|
1194
|
+
* pool leg across entries (#490). No series seed bank — mock pool is a floor,
|
|
1195
|
+
* not a forecast of a live series with inherited seed.
|
|
1086
1196
|
*/
|
|
1087
1197
|
/** Offline mock store for rounds.open/lock/settle/get when `mock: true` (3-game dogfood). */
|
|
1088
1198
|
this.mockRounds = /* @__PURE__ */ new Map();
|
|
1089
1199
|
/** Mock withdrawable credits after settle — key `${roundId}:${walletLower}` (#240 offline prize). */
|
|
1090
1200
|
this.mockWithdrawable = /* @__PURE__ */ new Map();
|
|
1201
|
+
/**
|
|
1202
|
+
* Mock pot after per-entry rake (PrizePool escrowed sum) — keyed by roundId (#490).
|
|
1203
|
+
* Payable pool at lock = pot × POOL_BPS / (POOL_BPS + SEED_BPS); inherited seed = 0 offline.
|
|
1204
|
+
*/
|
|
1205
|
+
this.mockPotMicro = /* @__PURE__ */ new Map();
|
|
1206
|
+
/** Persisted settle winners for re-settle replay (#490 C2) — never re-invent pool-to-everyone. */
|
|
1207
|
+
this.mockSettleWinners = /* @__PURE__ */ new Map();
|
|
1091
1208
|
this.rounds = {
|
|
1092
1209
|
open: async (input) => {
|
|
1093
1210
|
if (!this.config.mock) this.assertSecretKey("playmos.rounds.open");
|
|
@@ -1126,6 +1243,8 @@ var Playmos = class {
|
|
|
1126
1243
|
prizePoolAddress: this.config.contracts?.prizePool ?? "0x0000000000000000000000000000000000000002"
|
|
1127
1244
|
};
|
|
1128
1245
|
this.mockRounds.set(input.roundId, round);
|
|
1246
|
+
this.mockPotMicro.set(input.roundId, 0n);
|
|
1247
|
+
this.mockSettleWinners.delete(input.roundId);
|
|
1129
1248
|
return round;
|
|
1130
1249
|
}
|
|
1131
1250
|
const body = await this.http.post(
|
|
@@ -1158,11 +1277,12 @@ var Playmos = class {
|
|
|
1158
1277
|
code: "conflict"
|
|
1159
1278
|
});
|
|
1160
1279
|
}
|
|
1280
|
+
const payableMicro = this.mockPayablePoolMicro(existing);
|
|
1281
|
+
const poolUsd = payableMicro > 0n ? formatMicroToUsd(payableMicro) : existing.pool ?? existing.entryAmount;
|
|
1161
1282
|
const locked = {
|
|
1162
1283
|
...existing,
|
|
1163
1284
|
status: "locked",
|
|
1164
|
-
|
|
1165
|
-
pool: existing.pool ?? existing.entryAmount,
|
|
1285
|
+
pool: poolUsd,
|
|
1166
1286
|
entrants: existing.entrants ?? 0,
|
|
1167
1287
|
lockTxHash: MOCK_TX
|
|
1168
1288
|
};
|
|
@@ -1185,14 +1305,15 @@ var Playmos = class {
|
|
|
1185
1305
|
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1186
1306
|
}
|
|
1187
1307
|
if (existing.status === "settled" && existing.settleTxHash) {
|
|
1308
|
+
const stored = this.mockSettleWinners.get(input.roundId);
|
|
1188
1309
|
return {
|
|
1189
1310
|
roundId: input.roundId,
|
|
1190
1311
|
txHash: existing.settleTxHash,
|
|
1191
1312
|
poolPaid: existing.pool ?? "0",
|
|
1192
|
-
winners: "winners" in input.results ? input.results.winners : input.results.ranking.map((wallet) => ({
|
|
1313
|
+
winners: stored ?? ("winners" in input.results ? input.results.winners : input.results.ranking.map((wallet) => ({
|
|
1193
1314
|
wallet,
|
|
1194
|
-
amount:
|
|
1195
|
-
})),
|
|
1315
|
+
amount: "0"
|
|
1316
|
+
}))),
|
|
1196
1317
|
status: "settled"
|
|
1197
1318
|
};
|
|
1198
1319
|
}
|
|
@@ -1202,13 +1323,45 @@ var Playmos = class {
|
|
|
1202
1323
|
code: "conflict"
|
|
1203
1324
|
});
|
|
1204
1325
|
}
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1326
|
+
const payableMicro = existing.pool != null ? this.parseUsdToMicroLoose(existing.pool) : this.mockPayablePoolMicro(existing);
|
|
1327
|
+
const poolPaid = payableMicro > 0n ? formatMicroToUsd(payableMicro) : existing.pool ?? existing.entryAmount;
|
|
1328
|
+
let rows;
|
|
1329
|
+
if ("winners" in input.results) {
|
|
1330
|
+
rows = input.results.winners.map((w) => {
|
|
1331
|
+
let micro;
|
|
1332
|
+
try {
|
|
1333
|
+
micro = this.parseUsdToMicroLoose(String(w.amount ?? "0"));
|
|
1334
|
+
} catch (e) {
|
|
1335
|
+
this.mockPayoutApiError(e);
|
|
1336
|
+
}
|
|
1337
|
+
return {
|
|
1338
|
+
wallet: w.wallet,
|
|
1339
|
+
amount: formatMicroToUsd(micro),
|
|
1340
|
+
micro
|
|
1341
|
+
};
|
|
1342
|
+
});
|
|
1343
|
+
} else {
|
|
1344
|
+
if (payableMicro <= 0n) {
|
|
1345
|
+
rows = [];
|
|
1346
|
+
} else {
|
|
1347
|
+
try {
|
|
1348
|
+
const computed = computePayout(
|
|
1349
|
+
payableMicro,
|
|
1350
|
+
input.results.ranking,
|
|
1351
|
+
existing.payout
|
|
1352
|
+
);
|
|
1353
|
+
rows = computed.map((c) => ({
|
|
1354
|
+
wallet: c.wallet,
|
|
1355
|
+
amount: formatMicroToUsd(c.amount),
|
|
1356
|
+
micro: c.amount
|
|
1357
|
+
}));
|
|
1358
|
+
} catch (e) {
|
|
1359
|
+
this.mockPayoutApiError(e);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
const winners = rows.map(({ wallet, amount }) => ({ wallet, amount }));
|
|
1210
1364
|
const settleTxHash = MOCK_TX;
|
|
1211
|
-
const poolPaid = existing.pool ?? existing.entryAmount;
|
|
1212
1365
|
const settled = {
|
|
1213
1366
|
...existing,
|
|
1214
1367
|
status: "settled",
|
|
@@ -1216,18 +1369,11 @@ var Playmos = class {
|
|
|
1216
1369
|
pool: poolPaid
|
|
1217
1370
|
};
|
|
1218
1371
|
this.mockRounds.set(input.roundId, settled);
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
} catch {
|
|
1225
|
-
micro = 0n;
|
|
1226
|
-
}
|
|
1227
|
-
if (micro > 0n) {
|
|
1228
|
-
const k = `${input.roundId}:${wAddr}`;
|
|
1229
|
-
this.mockWithdrawable.set(k, (this.mockWithdrawable.get(k) ?? 0n) + micro);
|
|
1230
|
-
}
|
|
1372
|
+
this.mockSettleWinners.set(input.roundId, winners);
|
|
1373
|
+
for (const w of rows) {
|
|
1374
|
+
if (w.micro <= 0n) continue;
|
|
1375
|
+
const k = `${input.roundId}:${w.wallet.toLowerCase()}`;
|
|
1376
|
+
this.mockWithdrawable.set(k, (this.mockWithdrawable.get(k) ?? 0n) + w.micro);
|
|
1231
1377
|
}
|
|
1232
1378
|
return {
|
|
1233
1379
|
roundId: input.roundId,
|
|
@@ -1319,9 +1465,12 @@ var Playmos = class {
|
|
|
1319
1465
|
...existing,
|
|
1320
1466
|
status: "cancelled",
|
|
1321
1467
|
cancelTxHash: MOCK_TX,
|
|
1322
|
-
pool: null
|
|
1468
|
+
pool: null,
|
|
1469
|
+
entrants: 0
|
|
1323
1470
|
};
|
|
1324
1471
|
this.mockRounds.set(input.roundId, cancelled);
|
|
1472
|
+
this.mockPotMicro.set(input.roundId, 0n);
|
|
1473
|
+
this.mockSettleWinners.delete(input.roundId);
|
|
1325
1474
|
return {
|
|
1326
1475
|
roundId: input.roundId,
|
|
1327
1476
|
txHash: MOCK_TX,
|
|
@@ -1846,6 +1995,69 @@ var Playmos = class {
|
|
|
1846
1995
|
}
|
|
1847
1996
|
}
|
|
1848
1997
|
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Resolve mock round by roundId **or** roundKey (kit `entryProvider` keys by roundKey, #490 C3).
|
|
2000
|
+
*/
|
|
2001
|
+
findMockRound(idOrKey) {
|
|
2002
|
+
const direct = this.mockRounds.get(idOrKey);
|
|
2003
|
+
if (direct) return direct;
|
|
2004
|
+
for (const r of this.mockRounds.values()) {
|
|
2005
|
+
if (r.roundId === idOrKey || r.roundKey === idOrKey) return r;
|
|
2006
|
+
}
|
|
2007
|
+
return void 0;
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* 6dp USD → micro for mock winner amounts (#490 C1). Must not use 2dp `validateAmount`
|
|
2011
|
+
* (would zero 0.135 from top-n 30% of $0.45).
|
|
2012
|
+
*/
|
|
2013
|
+
parseUsdToMicroLoose(amount) {
|
|
2014
|
+
if (typeof amount !== "string" || !/^\d+(\.\d{1,6})?$/.test(amount.trim())) {
|
|
2015
|
+
throw new PayoutError(`invalid USD amount: ${JSON.stringify(amount)}`);
|
|
2016
|
+
}
|
|
2017
|
+
const [wholeRaw, frac = ""] = amount.trim().split(".");
|
|
2018
|
+
const whole = wholeRaw ?? "0";
|
|
2019
|
+
const fracPadded = (frac + "000000").slice(0, 6);
|
|
2020
|
+
const micro = BigInt(whole) * 1000000n + BigInt(fracPadded);
|
|
2021
|
+
if (micro < 0n) throw new PayoutError(`amount must be >= 0, got: ${JSON.stringify(amount)}`);
|
|
2022
|
+
return micro;
|
|
2023
|
+
}
|
|
2024
|
+
/** Live-shaped 400 for mock payout validation (C5) — never leak raw PayoutError. */
|
|
2025
|
+
mockPayoutApiError(e) {
|
|
2026
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2027
|
+
throw new ApiError(msg, { status: 400, code: "payout_invalid" });
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Accumulate one mock entry into the pot (chain parity: rake off top, then lock split).
|
|
2031
|
+
* No-op when no matching open/locked round — never 404 (C4).
|
|
2032
|
+
*/
|
|
2033
|
+
mockAccumulateEntry(idOrKey, amountMicro) {
|
|
2034
|
+
const round = this.findMockRound(idOrKey);
|
|
2035
|
+
if (!round) return;
|
|
2036
|
+
if (round.status !== "open" && round.status !== "locked") return;
|
|
2037
|
+
const rake = amountMicro * BigInt(RAKE_BPS) / 10000n;
|
|
2038
|
+
const escrowed = amountMicro - rake;
|
|
2039
|
+
const pot = (this.mockPotMicro.get(round.roundId) ?? 0n) + escrowed;
|
|
2040
|
+
this.mockPotMicro.set(round.roundId, pot);
|
|
2041
|
+
const entrants = (round.entrants ?? 0) + 1;
|
|
2042
|
+
const updated = { ...round, entrants };
|
|
2043
|
+
this.mockRounds.set(round.roundId, updated);
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Payable pool micro for mock lock/settle.
|
|
2047
|
+
* With recorded entries: pot × POOL/(POOL+SEED) (seed bank = 0 offline).
|
|
2048
|
+
* Zero entries: fall back to entryAmount micro (C4 — preserves existing suite).
|
|
2049
|
+
*/
|
|
2050
|
+
mockPayablePoolMicro(round) {
|
|
2051
|
+
const pot = this.mockPotMicro.get(round.roundId) ?? 0n;
|
|
2052
|
+
if (pot > 0n) {
|
|
2053
|
+
return pot * BigInt(POOL_BPS) / BigInt(POOL_BPS + SEED_BPS);
|
|
2054
|
+
}
|
|
2055
|
+
try {
|
|
2056
|
+
return this.parseUsdToMicroLoose(round.entryAmount);
|
|
2057
|
+
} catch {
|
|
2058
|
+
return 0n;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
1849
2061
|
/**
|
|
1850
2062
|
* Wallet timeout policy (#462 / PR #463 residual).
|
|
1851
2063
|
* - connectTimeoutMs → eth_requestAccounts only
|
|
@@ -1994,6 +2206,7 @@ var Playmos = class {
|
|
|
1994
2206
|
const metadata = validateMetadata(input.metadata);
|
|
1995
2207
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
1996
2208
|
if (this.config.mock) {
|
|
2209
|
+
this.mockAccumulateEntry(input.roundId, amountMicro);
|
|
1997
2210
|
return this.rememberMock(
|
|
1998
2211
|
mockEntryPayment({
|
|
1999
2212
|
amountMicro,
|
|
@@ -2414,112 +2627,6 @@ function previewPoolSplit(amount) {
|
|
|
2414
2627
|
};
|
|
2415
2628
|
}
|
|
2416
2629
|
|
|
2417
|
-
// src/payout.ts
|
|
2418
|
-
var PayoutError = class extends Error {
|
|
2419
|
-
constructor(message) {
|
|
2420
|
-
super(message);
|
|
2421
|
-
this.code = "payout_invalid";
|
|
2422
|
-
this.name = "PayoutError";
|
|
2423
|
-
}
|
|
2424
|
-
};
|
|
2425
|
-
var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
|
|
2426
|
-
var BPS = 10000n;
|
|
2427
|
-
function requireAddress2(w, i) {
|
|
2428
|
-
if (!ADDRESS_RE3.test(w)) {
|
|
2429
|
-
throw new PayoutError(`ranking[${i}] must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(w)}`);
|
|
2430
|
-
}
|
|
2431
|
-
return w.toLowerCase();
|
|
2432
|
-
}
|
|
2433
|
-
function parseUsdToMicroLoose(amount) {
|
|
2434
|
-
if (typeof amount !== "string" || !/^\d+(\.\d{1,6})?$/.test(amount.trim())) {
|
|
2435
|
-
throw new PayoutError(`invalid USD amount: ${JSON.stringify(amount)}`);
|
|
2436
|
-
}
|
|
2437
|
-
const [wholeRaw, frac = ""] = amount.trim().split(".");
|
|
2438
|
-
const whole = wholeRaw ?? "0";
|
|
2439
|
-
const fracPadded = (frac + "000000").slice(0, 6);
|
|
2440
|
-
const micro = BigInt(whole) * 1000000n + BigInt(fracPadded);
|
|
2441
|
-
if (micro <= 0n) throw new PayoutError(`amount must be > 0, got: ${JSON.stringify(amount)}`);
|
|
2442
|
-
return micro;
|
|
2443
|
-
}
|
|
2444
|
-
function computePayout(pool, ranking, rule) {
|
|
2445
|
-
if (typeof pool !== "bigint" || pool <= 0n) {
|
|
2446
|
-
throw new PayoutError(`pool must be a positive bigint (micro-USDC), got: ${String(pool)}`);
|
|
2447
|
-
}
|
|
2448
|
-
if (!Array.isArray(ranking) || ranking.length === 0) {
|
|
2449
|
-
throw new PayoutError("ranking must be a non-empty array of wallets (best-first)");
|
|
2450
|
-
}
|
|
2451
|
-
const wallets = ranking.map((w, i) => requireAddress2(w, i));
|
|
2452
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2453
|
-
for (const w of wallets) {
|
|
2454
|
-
if (seen.has(w)) throw new PayoutError(`duplicate wallet in ranking: ${w}`);
|
|
2455
|
-
seen.add(w);
|
|
2456
|
-
}
|
|
2457
|
-
if (rule.kind === "winner-take-all") {
|
|
2458
|
-
return [{ wallet: wallets[0], amount: pool }];
|
|
2459
|
-
}
|
|
2460
|
-
if (rule.kind === "top-n") {
|
|
2461
|
-
const splits = rule.splitsBps;
|
|
2462
|
-
if (!Array.isArray(splits) || splits.length === 0) {
|
|
2463
|
-
throw new PayoutError("top-n splitsBps must be a non-empty array");
|
|
2464
|
-
}
|
|
2465
|
-
if (splits.length > wallets.length) {
|
|
2466
|
-
throw new PayoutError(
|
|
2467
|
-
`top-n needs ${splits.length} ranked wallets but ranking only has ${wallets.length}`
|
|
2468
|
-
);
|
|
2469
|
-
}
|
|
2470
|
-
let sumBps = 0;
|
|
2471
|
-
for (const b of splits) {
|
|
2472
|
-
if (!Number.isInteger(b) || b <= 0 || b > 1e4) {
|
|
2473
|
-
throw new PayoutError(`each splitsBps entry must be an integer in (0, 10000], got: ${b}`);
|
|
2474
|
-
}
|
|
2475
|
-
sumBps += b;
|
|
2476
|
-
}
|
|
2477
|
-
if (sumBps !== 1e4) {
|
|
2478
|
-
throw new PayoutError(`splitsBps must sum to 10000, got ${sumBps}`);
|
|
2479
|
-
}
|
|
2480
|
-
const out = [];
|
|
2481
|
-
let allocated = 0n;
|
|
2482
|
-
for (let i = 0; i < splits.length; i++) {
|
|
2483
|
-
const amt = pool * BigInt(splits[i]) / BPS;
|
|
2484
|
-
out.push({ wallet: wallets[i], amount: amt });
|
|
2485
|
-
allocated += amt;
|
|
2486
|
-
}
|
|
2487
|
-
const remainder = pool - allocated;
|
|
2488
|
-
if (remainder < 0n) throw new PayoutError("internal: allocated more than pool");
|
|
2489
|
-
out[0] = { wallet: out[0].wallet, amount: out[0].amount + remainder };
|
|
2490
|
-
const filtered = out.filter((x) => x.amount > 0n);
|
|
2491
|
-
if (filtered.length === 0) throw new PayoutError("payout produced no positive amounts");
|
|
2492
|
-
const total = filtered.reduce((s, x) => s + x.amount, 0n);
|
|
2493
|
-
if (total !== pool) throw new PayoutError(`internal: sum ${total} != pool ${pool}`);
|
|
2494
|
-
return filtered;
|
|
2495
|
-
}
|
|
2496
|
-
if (rule.kind === "custom") {
|
|
2497
|
-
const amounts = rule.amounts;
|
|
2498
|
-
if (!Array.isArray(amounts) || amounts.length === 0) {
|
|
2499
|
-
throw new PayoutError("custom amounts must be a non-empty array of USD strings");
|
|
2500
|
-
}
|
|
2501
|
-
if (amounts.length > wallets.length) {
|
|
2502
|
-
throw new PayoutError(
|
|
2503
|
-
`custom amounts has ${amounts.length} entries but ranking only has ${wallets.length}`
|
|
2504
|
-
);
|
|
2505
|
-
}
|
|
2506
|
-
const out = [];
|
|
2507
|
-
let total = 0n;
|
|
2508
|
-
for (let i = 0; i < amounts.length; i++) {
|
|
2509
|
-
const amt = parseUsdToMicroLoose(amounts[i]);
|
|
2510
|
-
out.push({ wallet: wallets[i], amount: amt });
|
|
2511
|
-
total += amt;
|
|
2512
|
-
}
|
|
2513
|
-
if (total !== pool) {
|
|
2514
|
-
throw new PayoutError(
|
|
2515
|
-
`custom amounts sum to ${total} micro-USDC but pool is ${pool} \u2014 must match exactly`
|
|
2516
|
-
);
|
|
2517
|
-
}
|
|
2518
|
-
return out;
|
|
2519
|
-
}
|
|
2520
|
-
throw new PayoutError(`unknown payout rule kind: ${JSON.stringify(rule.kind)}`);
|
|
2521
|
-
}
|
|
2522
|
-
|
|
2523
2630
|
// src/settlement.ts
|
|
2524
2631
|
var ADDRESS_RE4 = /^0x[0-9a-fA-F]{40}$/;
|
|
2525
2632
|
var DEFAULT_TTL_MS = 15 * 60 * 1e3;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playmos/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
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,
|