@playmos/sdk 0.3.0 → 0.3.2
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 +33 -20
- package/dist/{chunk-B7SHFZYY.js → chunk-TMBBEGIF.js} +12 -3
- package/dist/chunk-TMBBEGIF.js.map +1 -0
- package/dist/{errors-BVESr920.d.cts → errors-DMtMpbR6.d.cts} +48 -4
- package/dist/{errors-BVESr920.d.ts → errors-DMtMpbR6.d.ts} +48 -4
- package/dist/index.cjs +605 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +410 -131
- package/dist/index.d.ts +410 -131
- package/dist/index.js +591 -44
- package/dist/index.js.map +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-B7SHFZYY.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
var viem = require('viem');
|
|
4
4
|
|
|
5
|
+
// src/client.ts
|
|
6
|
+
|
|
5
7
|
// src/errors.ts
|
|
6
8
|
var PlaymosError = class extends Error {
|
|
7
9
|
constructor(code, message, detail) {
|
|
@@ -60,6 +62,15 @@ var ConfigError = class extends PlaymosError {
|
|
|
60
62
|
super("config", message, detail);
|
|
61
63
|
}
|
|
62
64
|
};
|
|
65
|
+
var NothingToWithdrawError = class extends PlaymosError {
|
|
66
|
+
constructor(detail) {
|
|
67
|
+
super(
|
|
68
|
+
"nothing_to_withdraw",
|
|
69
|
+
"Nothing to withdraw \u2014 this wallet has no credited prize balance on this PrizePool (round not settled for them, or already claimed).",
|
|
70
|
+
detail
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
63
74
|
|
|
64
75
|
// src/config.ts
|
|
65
76
|
var CHAIN_ID = {
|
|
@@ -264,15 +275,36 @@ function createHttpClient(baseUrl, apiKey, retry) {
|
|
|
264
275
|
);
|
|
265
276
|
}
|
|
266
277
|
}
|
|
278
|
+
function hasIdempotencyKey(init) {
|
|
279
|
+
const h = init.headers;
|
|
280
|
+
if (!h) return false;
|
|
281
|
+
if (h instanceof Headers) {
|
|
282
|
+
return Boolean(h.get("idempotency-key") || h.get("Idempotency-Key"));
|
|
283
|
+
}
|
|
284
|
+
if (Array.isArray(h)) {
|
|
285
|
+
return h.some(([k]) => String(k).toLowerCase() === "idempotency-key");
|
|
286
|
+
}
|
|
287
|
+
const rec = h;
|
|
288
|
+
return Object.keys(rec).some((k) => k.toLowerCase() === "idempotency-key" && !!rec[k]);
|
|
289
|
+
}
|
|
290
|
+
function isRetryable(res, init) {
|
|
291
|
+
if (res.status === 429) return true;
|
|
292
|
+
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
|
293
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
294
|
+
if (method === "GET" || method === "HEAD") return true;
|
|
295
|
+
return hasIdempotencyKey(init);
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
267
299
|
async function sendWithRetry(url, init) {
|
|
268
300
|
let res = await send(url, init);
|
|
269
|
-
for (let attempt = 0;
|
|
301
|
+
for (let attempt = 0; attempt < cfg.maxRetries && isRetryable(res, init); attempt++) {
|
|
270
302
|
await sleep(backoffMs(res, attempt, cfg));
|
|
271
303
|
res = await send(url, init);
|
|
272
304
|
}
|
|
273
305
|
return res;
|
|
274
306
|
}
|
|
275
|
-
async function handle(res) {
|
|
307
|
+
async function handle(res, acceptStatuses) {
|
|
276
308
|
let text;
|
|
277
309
|
try {
|
|
278
310
|
text = await res.text();
|
|
@@ -287,9 +319,13 @@ function createHttpClient(baseUrl, apiKey, retry) {
|
|
|
287
319
|
try {
|
|
288
320
|
json = text ? JSON.parse(text) : {};
|
|
289
321
|
} catch {
|
|
290
|
-
|
|
322
|
+
const gateway = res.status === 502 || res.status === 503 || res.status === 504;
|
|
323
|
+
const msg = gateway ? `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).` : `Non-JSON response (${res.status}) from ${res.url}`;
|
|
324
|
+
throw new ApiError(msg, { status: res.status, body: text.slice(0, 500), gateway });
|
|
325
|
+
}
|
|
326
|
+
if (res.ok || acceptStatuses !== void 0 && acceptStatuses.includes(res.status)) {
|
|
327
|
+
return json;
|
|
291
328
|
}
|
|
292
|
-
if (res.ok) return json;
|
|
293
329
|
const errBody = json;
|
|
294
330
|
const message = errBody?.error?.message ?? `Request failed with ${res.status}`;
|
|
295
331
|
if (res.status === 401 || res.status === 403 || errBody?.error?.code === "auth") {
|
|
@@ -305,7 +341,7 @@ function createHttpClient(baseUrl, apiKey, retry) {
|
|
|
305
341
|
};
|
|
306
342
|
if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
|
|
307
343
|
const res = await sendWithRetry(`${base}${path}`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
308
|
-
return handle(res);
|
|
344
|
+
return handle(res, opts?.acceptStatuses);
|
|
309
345
|
},
|
|
310
346
|
async get(path) {
|
|
311
347
|
const res = await sendWithRetry(`${base}${path}`, {
|
|
@@ -462,6 +498,13 @@ var prizePoolAbi = [
|
|
|
462
498
|
],
|
|
463
499
|
outputs: [{ type: "bool" }]
|
|
464
500
|
},
|
|
501
|
+
{
|
|
502
|
+
type: "function",
|
|
503
|
+
name: "withdrawable",
|
|
504
|
+
stateMutability: "view",
|
|
505
|
+
inputs: [{ name: "account", type: "address" }],
|
|
506
|
+
outputs: [{ type: "uint256" }]
|
|
507
|
+
},
|
|
465
508
|
{
|
|
466
509
|
type: "function",
|
|
467
510
|
name: "withdraw",
|
|
@@ -514,7 +557,23 @@ async function waitForCalls(provider, id, timeoutMs = 6e4) {
|
|
|
514
557
|
function encodeApprove(spender, amountUnits) {
|
|
515
558
|
return viem.encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [spender, amountUnits] });
|
|
516
559
|
}
|
|
517
|
-
|
|
560
|
+
var GAS_FLOOR_WEI_DEFAULT = 200000000000000n;
|
|
561
|
+
var GAS_FLOOR_WEI_BASE_SEPOLIA = 50000000000000n;
|
|
562
|
+
var CHAIN_ID_BASE_SEPOLIA = 84532;
|
|
563
|
+
function gasFloorWei(chainId) {
|
|
564
|
+
return chainId === CHAIN_ID_BASE_SEPOLIA ? GAS_FLOOR_WEI_BASE_SEPOLIA : GAS_FLOOR_WEI_DEFAULT;
|
|
565
|
+
}
|
|
566
|
+
async function assertEnoughGas(provider, from, minWei) {
|
|
567
|
+
let floor = minWei;
|
|
568
|
+
if (floor === void 0) {
|
|
569
|
+
let chainId = 0;
|
|
570
|
+
try {
|
|
571
|
+
const hex = await provider.request({ method: "eth_chainId", params: [] });
|
|
572
|
+
chainId = Number(BigInt(hex));
|
|
573
|
+
} catch {
|
|
574
|
+
}
|
|
575
|
+
floor = gasFloorWei(chainId);
|
|
576
|
+
}
|
|
518
577
|
let balance;
|
|
519
578
|
try {
|
|
520
579
|
const hex = await provider.request({ method: "eth_getBalance", params: [from, "latest"] });
|
|
@@ -522,8 +581,11 @@ async function assertEnoughGas(provider, from, minWei = 200000000000000n) {
|
|
|
522
581
|
} catch {
|
|
523
582
|
return;
|
|
524
583
|
}
|
|
525
|
-
if (balance <
|
|
526
|
-
throw new InsufficientGasError({
|
|
584
|
+
if (balance < floor) {
|
|
585
|
+
throw new InsufficientGasError({
|
|
586
|
+
balanceWei: balance.toString(),
|
|
587
|
+
minWei: floor.toString()
|
|
588
|
+
});
|
|
527
589
|
}
|
|
528
590
|
}
|
|
529
591
|
var toBytes32 = (s) => viem.keccak256(viem.toBytes(s));
|
|
@@ -549,6 +611,14 @@ function buildEntryCalls(args) {
|
|
|
549
611
|
{ to: args.prizePool, data: enterData }
|
|
550
612
|
];
|
|
551
613
|
}
|
|
614
|
+
function buildWithdrawCall(prizePool) {
|
|
615
|
+
const data = viem.encodeFunctionData({
|
|
616
|
+
abi: prizePoolAbi,
|
|
617
|
+
functionName: "withdraw",
|
|
618
|
+
args: []
|
|
619
|
+
});
|
|
620
|
+
return { to: prizePool, data };
|
|
621
|
+
}
|
|
552
622
|
|
|
553
623
|
// src/mock.ts
|
|
554
624
|
var MOCK_TX = "0x000000000000000000000000000000000000000000000000000000000000mock";
|
|
@@ -577,6 +647,7 @@ function mockEntryPayment(args) {
|
|
|
577
647
|
args.seedBps,
|
|
578
648
|
args.rakeBps
|
|
579
649
|
);
|
|
650
|
+
const roundKey = args.roundKey && args.roundKey.trim() !== "" ? args.roundKey : args.roundId;
|
|
580
651
|
return {
|
|
581
652
|
id: prefixedId("entry"),
|
|
582
653
|
status: "confirmed",
|
|
@@ -593,6 +664,10 @@ function mockEntryPayment(args) {
|
|
|
593
664
|
},
|
|
594
665
|
gameId: args.gameId,
|
|
595
666
|
roundId: args.roundId,
|
|
667
|
+
roundKey,
|
|
668
|
+
identity: args.identity,
|
|
669
|
+
// Deterministic mock pool address so claim-flow unit tests can assert shape (#41).
|
|
670
|
+
prizePoolAddress: "0x0000000000000000000000000000000000000001",
|
|
596
671
|
playerId: args.playerId,
|
|
597
672
|
txHash: MOCK_TX,
|
|
598
673
|
chain: args.chain,
|
|
@@ -601,14 +676,160 @@ function mockEntryPayment(args) {
|
|
|
601
676
|
mock: true
|
|
602
677
|
};
|
|
603
678
|
}
|
|
679
|
+
function mockVerifyResult(payment) {
|
|
680
|
+
return { ...payment, mock: true };
|
|
681
|
+
}
|
|
604
682
|
|
|
605
|
-
// src/
|
|
683
|
+
// src/x402.ts
|
|
606
684
|
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
607
|
-
function
|
|
685
|
+
function requireAddress(value, field) {
|
|
608
686
|
if (typeof value !== "string" || value.trim() === "") {
|
|
609
687
|
throw new MissingFieldError(field);
|
|
610
688
|
}
|
|
611
689
|
if (!ADDRESS_RE.test(value)) {
|
|
690
|
+
throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
|
|
691
|
+
field,
|
|
692
|
+
value
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
return value.toLowerCase();
|
|
696
|
+
}
|
|
697
|
+
function networkToCaip2(network) {
|
|
698
|
+
if (network === "base") return "eip155:8453";
|
|
699
|
+
if (network === "base-sepolia") return "eip155:84532";
|
|
700
|
+
throw new ConfigError(`Unknown network for x402: ${JSON.stringify(network)}`, { network });
|
|
701
|
+
}
|
|
702
|
+
function toX402PaymentRequired(req, extras) {
|
|
703
|
+
const amountMicro = parseUsdToMicro(req.amount);
|
|
704
|
+
const out = {
|
|
705
|
+
scheme: "exact",
|
|
706
|
+
network: networkToCaip2(req.network),
|
|
707
|
+
maxAmountRequired: amountMicro.toString(),
|
|
708
|
+
// Service V1 emits the asset symbol; address is implied by network + config.
|
|
709
|
+
asset: "USDC",
|
|
710
|
+
payTo: req.payTo,
|
|
711
|
+
playmosRequirementId: req.id,
|
|
712
|
+
amount: req.amount,
|
|
713
|
+
playmosNetwork: req.network,
|
|
714
|
+
expiresAt: req.expiresAt,
|
|
715
|
+
terms: req.terms ?? null,
|
|
716
|
+
feeBps: extras?.feeBps ?? null,
|
|
717
|
+
feeSink: extras?.feeSink ?? null
|
|
718
|
+
};
|
|
719
|
+
return out;
|
|
720
|
+
}
|
|
721
|
+
function encodePaymentHeader(obj) {
|
|
722
|
+
const json = JSON.stringify(obj);
|
|
723
|
+
if (typeof Buffer !== "undefined") {
|
|
724
|
+
return Buffer.from(json, "utf8").toString("base64");
|
|
725
|
+
}
|
|
726
|
+
const bytes = new TextEncoder().encode(json);
|
|
727
|
+
let bin = "";
|
|
728
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
729
|
+
return btoa(bin);
|
|
730
|
+
}
|
|
731
|
+
function decodePaymentHeader(header) {
|
|
732
|
+
if (typeof header !== "string" || header.trim() === "") {
|
|
733
|
+
throw new ConfigError("payment header must be a non-empty base64 JSON string");
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
let json;
|
|
737
|
+
if (typeof Buffer !== "undefined") {
|
|
738
|
+
json = Buffer.from(header, "base64").toString("utf8");
|
|
739
|
+
} else {
|
|
740
|
+
const bin = atob(header);
|
|
741
|
+
const bytes = new Uint8Array(bin.length);
|
|
742
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
743
|
+
json = new TextDecoder().decode(bytes);
|
|
744
|
+
}
|
|
745
|
+
return JSON.parse(json);
|
|
746
|
+
} catch (err) {
|
|
747
|
+
throw new ConfigError("invalid base64 JSON payment header", {
|
|
748
|
+
cause: err instanceof Error ? err.message : String(err)
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function createX402Challenge(requirement, extras) {
|
|
753
|
+
if (!requirement || typeof requirement !== "object") {
|
|
754
|
+
throw new ConfigError("createX402Challenge requires a PaymentRequirement");
|
|
755
|
+
}
|
|
756
|
+
if (typeof requirement.id !== "string" || !requirement.id.startsWith("preq_")) {
|
|
757
|
+
throw new ConfigError('PaymentRequirement.id must be a "preq_\u2026" string', { id: requirement.id });
|
|
758
|
+
}
|
|
759
|
+
requireAddress(requirement.payTo, "payTo");
|
|
760
|
+
parseUsdToMicro(requirement.amount);
|
|
761
|
+
const feeBps = extras?.feeBps ?? 0;
|
|
762
|
+
if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 1e4) {
|
|
763
|
+
throw new ConfigError(
|
|
764
|
+
`feeBps must be an integer in [0, 10000], got: ${JSON.stringify(extras?.feeBps)}`,
|
|
765
|
+
{ feeBps: extras?.feeBps }
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
const feeSink = extras?.feeSink ?? null;
|
|
769
|
+
if (feeBps > 0 && feeSink) requireAddress(feeSink, "feeSink");
|
|
770
|
+
const paymentRequired = toX402PaymentRequired(requirement, { feeBps, feeSink });
|
|
771
|
+
return {
|
|
772
|
+
status: 402,
|
|
773
|
+
headers: {
|
|
774
|
+
"PAYMENT-REQUIRED": encodePaymentHeader(paymentRequired),
|
|
775
|
+
"Content-Type": "application/json"
|
|
776
|
+
},
|
|
777
|
+
body: {
|
|
778
|
+
error: { code: "payment_required", message: "Payment Required" },
|
|
779
|
+
requirement,
|
|
780
|
+
paymentRequired,
|
|
781
|
+
feeBps,
|
|
782
|
+
feeSink
|
|
783
|
+
},
|
|
784
|
+
paymentRequired
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function validateX402ChallengeInput(input) {
|
|
788
|
+
if (!input || typeof input !== "object") {
|
|
789
|
+
throw new ConfigError("x402 challenge input is required");
|
|
790
|
+
}
|
|
791
|
+
if ("url" in input && input.url !== void 0) {
|
|
792
|
+
throw new ConfigError(
|
|
793
|
+
"playmos.x402 does not accept { url } in V1 \u2014 use pay({ payTo, amount }) wrapping challenges+settle (ADR: resource URLs deferred V1.1+). See docs/design/PHASE-4-X402-ADR.md.",
|
|
794
|
+
{ field: "url" }
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const payTo = requireAddress(input.payTo, "payTo");
|
|
798
|
+
parseUsdToMicro(input.amount);
|
|
799
|
+
const intent = input.intent ?? "transfer";
|
|
800
|
+
if (intent !== "transfer" && intent !== "marketplace.buy") {
|
|
801
|
+
throw new ConfigError(
|
|
802
|
+
`x402 intent must be "transfer" or "marketplace.buy", got: ${JSON.stringify(input.intent)}`,
|
|
803
|
+
{ intent: input.intent }
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
const feeBps = input.feeBps ?? 0;
|
|
807
|
+
if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 1e4) {
|
|
808
|
+
throw new ConfigError(
|
|
809
|
+
`feeBps must be an integer in [0, 10000], got: ${JSON.stringify(input.feeBps)}`,
|
|
810
|
+
{ feeBps: input.feeBps }
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
const feeSink = input.feeSink === void 0 ? void 0 : requireAddress(input.feeSink, "feeSink");
|
|
814
|
+
return {
|
|
815
|
+
payTo,
|
|
816
|
+
amount: input.amount,
|
|
817
|
+
intent,
|
|
818
|
+
feeBps,
|
|
819
|
+
...feeSink !== void 0 ? { feeSink } : {},
|
|
820
|
+
...input.terms !== void 0 ? { terms: input.terms } : {},
|
|
821
|
+
...input.expiresInMs !== void 0 ? { expiresInMs: input.expiresInMs } : {},
|
|
822
|
+
...input.id !== void 0 ? { id: input.id } : {}
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/client.ts
|
|
827
|
+
var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
|
|
828
|
+
function requireAddressField(value, field) {
|
|
829
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
830
|
+
throw new MissingFieldError(field);
|
|
831
|
+
}
|
|
832
|
+
if (!ADDRESS_RE2.test(value)) {
|
|
612
833
|
throw new ConfigError(
|
|
613
834
|
`${field} must be a 0x-prefixed 20-byte wallet address (Phase 1a transfers settle server-held wallets), got: ${JSON.stringify(value)}`,
|
|
614
835
|
{ field, value }
|
|
@@ -653,6 +874,122 @@ var Playmos = class {
|
|
|
653
874
|
/** Create a Bridge KYC onboarding link (fiat payout). */
|
|
654
875
|
createOnboardingLink: () => this.http.post("/payouts/onboarding_link", {})
|
|
655
876
|
};
|
|
877
|
+
/**
|
|
878
|
+
* `x402` — Phase 4 HTTP 402 / x402-shaped adapter (#153).
|
|
879
|
+
*
|
|
880
|
+
* V1 surface (ADR):
|
|
881
|
+
* - `challenge` → `POST /v1/x402/challenges` (returns 402; treated as success)
|
|
882
|
+
* - `fulfill` → `POST /v1/x402/settle`
|
|
883
|
+
* - `pay` → challenge then fulfill (senior DX ≤15 lines)
|
|
884
|
+
*
|
|
885
|
+
* **Not** resource URLs (`pay({ url })` is V1.1+). **Not** stock third-party
|
|
886
|
+
* x402 client interop (X3b deferred until #161). Requires `sk_test_` +
|
|
887
|
+
* Base Sepolia; flag `PLAYMOS_X402_ENABLED` on the service.
|
|
888
|
+
*
|
|
889
|
+
* ```ts
|
|
890
|
+
* const playmos = new Playmos({ apiKey: process.env.PLAYMOS_SK!, network: "base-sepolia" });
|
|
891
|
+
* const result = await playmos.x402.pay({
|
|
892
|
+
* payTo: "0x…",
|
|
893
|
+
* amount: "0.50",
|
|
894
|
+
* feeBps: 0,
|
|
895
|
+
* });
|
|
896
|
+
* // result.txHash → BaseScan
|
|
897
|
+
* ```
|
|
898
|
+
*/
|
|
899
|
+
this.x402 = {
|
|
900
|
+
/**
|
|
901
|
+
* Mint a server-authoritative PaymentRequirement (HTTP 402).
|
|
902
|
+
* Fee terms are stored server-side — settle rejects payload disagreement.
|
|
903
|
+
*/
|
|
904
|
+
challenge: async (input) => {
|
|
905
|
+
this.assertX402Allowed();
|
|
906
|
+
const body = validateX402ChallengeInput(input);
|
|
907
|
+
if (body.intent === "marketplace.buy") {
|
|
908
|
+
throw new ConfigError(
|
|
909
|
+
'x402 challenge intent "marketplace.buy" is not supported yet (V1 service: "transfer" only; marketplace.buy follows).',
|
|
910
|
+
{ intent: body.intent }
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
const res = await this.http.post("/x402/challenges", body, { acceptStatuses: [402] });
|
|
914
|
+
return {
|
|
915
|
+
requirement: res.requirement,
|
|
916
|
+
paymentRequired: res.paymentRequired,
|
|
917
|
+
feeBps: res.feeBps ?? body.feeBps,
|
|
918
|
+
feeSink: res.feeSink ?? body.feeSink ?? null
|
|
919
|
+
};
|
|
920
|
+
},
|
|
921
|
+
/**
|
|
922
|
+
* Settle a previously minted challenge with an `x402-payload` authorization.
|
|
923
|
+
* Defaults mode to `server-signer` when omitted (explicit on the wire — fail-closed
|
|
924
|
+
* materialize still requires mode; the route also defaults for this path).
|
|
925
|
+
*/
|
|
926
|
+
fulfill: async (requirement, authorization) => {
|
|
927
|
+
this.assertX402Allowed();
|
|
928
|
+
if (!requirement || typeof requirement !== "object") {
|
|
929
|
+
throw new ConfigError("fulfill requires a PaymentRequirement from challenge()");
|
|
930
|
+
}
|
|
931
|
+
if (typeof requirement.id !== "string" || requirement.id.trim() === "") {
|
|
932
|
+
throw new MissingFieldError("requirement.id");
|
|
933
|
+
}
|
|
934
|
+
let payload;
|
|
935
|
+
if (authorization && typeof authorization === "object" && "kind" in authorization) {
|
|
936
|
+
const a = authorization;
|
|
937
|
+
if (a.kind !== "x402-payload") {
|
|
938
|
+
throw new ConfigError('authorization.kind must be "x402-payload"', { kind: a.kind });
|
|
939
|
+
}
|
|
940
|
+
payload = { ...a.payload ?? {} };
|
|
941
|
+
} else {
|
|
942
|
+
payload = { ...authorization ?? {} };
|
|
943
|
+
}
|
|
944
|
+
delete payload.feeBps;
|
|
945
|
+
delete payload.feeSink;
|
|
946
|
+
if (payload.mode === void 0 || typeof payload.mode === "string" && payload.mode.trim() === "") {
|
|
947
|
+
const hasNestedAuth = payload.authorization !== void 0 || payload.signature !== void 0 || payload.paymentPayload !== void 0;
|
|
948
|
+
if (hasNestedAuth) {
|
|
949
|
+
throw new ConfigError(
|
|
950
|
+
"fulfill: payload.mode is required when authorization/signature is present \u2014 stock-shaped payloads are not auto-mapped to server-signer (V1: no interop; set mode explicitly or see #161)",
|
|
951
|
+
{ field: "mode" }
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
payload.mode = "server-signer";
|
|
955
|
+
}
|
|
956
|
+
if (payload.scheme === void 0) payload.scheme = "exact";
|
|
957
|
+
if (payload.requirementId === void 0) payload.requirementId = requirement.id;
|
|
958
|
+
const res = await this.http.post("/x402/settle", {
|
|
959
|
+
requirement,
|
|
960
|
+
authorization: { kind: "x402-payload", payload }
|
|
961
|
+
});
|
|
962
|
+
const s = res.settlement;
|
|
963
|
+
return {
|
|
964
|
+
requirementId: s.requirementId,
|
|
965
|
+
status: s.status,
|
|
966
|
+
txHash: s.txHash ?? null,
|
|
967
|
+
idempotentReplay: Boolean(s.idempotentReplay),
|
|
968
|
+
verifiedVia: s.verifiedVia,
|
|
969
|
+
feeBps: s.feeBps,
|
|
970
|
+
fee: s.fee,
|
|
971
|
+
net: s.net,
|
|
972
|
+
requirement: res.requirement,
|
|
973
|
+
success: s.success
|
|
974
|
+
};
|
|
975
|
+
},
|
|
976
|
+
/**
|
|
977
|
+
* Senior DX: mint challenge + settle in one call.
|
|
978
|
+
* Wraps challenges + settle only — **not** `pay({ url })` (ADR).
|
|
979
|
+
*/
|
|
980
|
+
pay: async (input) => {
|
|
981
|
+
this.assertX402Allowed();
|
|
982
|
+
validateX402ChallengeInput(input);
|
|
983
|
+
const challenge = await this.x402.challenge(input);
|
|
984
|
+
const auth = {
|
|
985
|
+
mode: input.mode ?? "server-signer"
|
|
986
|
+
};
|
|
987
|
+
if (input.from !== void 0) auth.from = input.from;
|
|
988
|
+
if (input.authorization !== void 0) auth.authorization = input.authorization;
|
|
989
|
+
if (input.txHash !== void 0) auth.txHash = input.txHash;
|
|
990
|
+
return this.x402.fulfill(challenge.requirement, auth);
|
|
991
|
+
}
|
|
992
|
+
};
|
|
656
993
|
/**
|
|
657
994
|
* Skill/contest **round lifecycle** (issue #13) — closes the enterRound loop.
|
|
658
995
|
*
|
|
@@ -705,6 +1042,112 @@ var Playmos = class {
|
|
|
705
1042
|
`/rounds/${encodeURIComponent(input.roundId)}`
|
|
706
1043
|
);
|
|
707
1044
|
return round;
|
|
1045
|
+
},
|
|
1046
|
+
/**
|
|
1047
|
+
* Read a wallet's **claimable** prize balance for a round (issue #41).
|
|
1048
|
+
* Prefers the service chain read (`GET /v1/rounds/:id/prize?wallet=`); falls
|
|
1049
|
+
* back to a direct eth_call when `prizePoolAddress` is supplied and no wallet
|
|
1050
|
+
* connector is needed for the read path… actually uses service first, then
|
|
1051
|
+
* on-chain via the player's provider if the service is unavailable.
|
|
1052
|
+
*/
|
|
1053
|
+
prize: async (input) => {
|
|
1054
|
+
requireField(input?.roundId, "roundId");
|
|
1055
|
+
const wallet = requireAddressField(input?.wallet, "wallet");
|
|
1056
|
+
try {
|
|
1057
|
+
const res = await this.http.get(
|
|
1058
|
+
`/rounds/${encodeURIComponent(input.roundId)}/prize?wallet=${encodeURIComponent(wallet)}`
|
|
1059
|
+
);
|
|
1060
|
+
return res.prize;
|
|
1061
|
+
} catch (e) {
|
|
1062
|
+
if (!input.prizePoolAddress && !(e instanceof ApiError)) throw e;
|
|
1063
|
+
if (!input.prizePoolAddress) throw e;
|
|
1064
|
+
}
|
|
1065
|
+
const prizePoolAddress = requireAddressField(input.prizePoolAddress, "prizePoolAddress");
|
|
1066
|
+
const claimableMicro = await this.readWithdrawable(prizePoolAddress, wallet);
|
|
1067
|
+
return {
|
|
1068
|
+
roundId: input.roundId,
|
|
1069
|
+
prizePoolAddress,
|
|
1070
|
+
wallet,
|
|
1071
|
+
claimable: formatMicroToUsd(claimableMicro),
|
|
1072
|
+
claimableMicro: claimableMicro.toString()
|
|
1073
|
+
};
|
|
1074
|
+
},
|
|
1075
|
+
/**
|
|
1076
|
+
* Winner **claim** — call PrizePool.withdraw() from the player's wallet (issue #41).
|
|
1077
|
+
* Pull-payment: credits from settle live in `withdrawable[msg.sender]`. No approve needed.
|
|
1078
|
+
* Pass either `prizePoolAddress` (from enterRound / rounds.get) or `roundId` (service resolves).
|
|
1079
|
+
*/
|
|
1080
|
+
withdraw: async (input) => {
|
|
1081
|
+
if (this.config.mock) {
|
|
1082
|
+
return {
|
|
1083
|
+
prizePoolAddress: input.prizePoolAddress ?? "0x0000000000000000000000000000000000000001",
|
|
1084
|
+
amount: "0.00",
|
|
1085
|
+
amountMicro: "0",
|
|
1086
|
+
txHash: "0x000000000000000000000000000000000000000000000000000000000000mock",
|
|
1087
|
+
status: "confirmed"
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
let prizePool = input.prizePoolAddress ? requireAddressField(input.prizePoolAddress, "prizePoolAddress") : void 0;
|
|
1091
|
+
if (!prizePool) {
|
|
1092
|
+
requireField(input?.roundId, "roundId");
|
|
1093
|
+
const round = await this.rounds.get({ roundId: input.roundId });
|
|
1094
|
+
if (!round.prizePoolAddress) {
|
|
1095
|
+
throw new ConfigError(
|
|
1096
|
+
"rounds.get did not return prizePoolAddress \u2014 pass prizePoolAddress from enterRound or configure contracts.prizePool"
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
prizePool = round.prizePoolAddress;
|
|
1100
|
+
}
|
|
1101
|
+
const provider = resolveProvider(this.config.wallet);
|
|
1102
|
+
if (this.config.gas?.mode === "player") {
|
|
1103
|
+
const from0 = await getAccount(provider);
|
|
1104
|
+
await assertEnoughGas(provider, from0);
|
|
1105
|
+
}
|
|
1106
|
+
const from = await getAccount(provider);
|
|
1107
|
+
let amountMicro = 0n;
|
|
1108
|
+
if (input.checkBalance !== false) {
|
|
1109
|
+
amountMicro = await this.readWithdrawable(prizePool, from);
|
|
1110
|
+
if (amountMicro === 0n) {
|
|
1111
|
+
throw new NothingToWithdrawError({ prizePoolAddress: prizePool, wallet: from });
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
const call = buildWithdrawCall(prizePool);
|
|
1115
|
+
const paymasterUrl = this.config.gas?.mode === "sponsored" ? this.config.gas.paymasterUrl : void 0;
|
|
1116
|
+
let txHash;
|
|
1117
|
+
let status = "pending";
|
|
1118
|
+
try {
|
|
1119
|
+
const { id: callsId } = await sendCalls(
|
|
1120
|
+
provider,
|
|
1121
|
+
from,
|
|
1122
|
+
this.env.chainId,
|
|
1123
|
+
[call],
|
|
1124
|
+
paymasterUrl
|
|
1125
|
+
);
|
|
1126
|
+
const waited = await waitForCalls(provider, callsId);
|
|
1127
|
+
txHash = waited.txHash;
|
|
1128
|
+
status = waited.status === "CONFIRMED" ? "confirmed" : waited.status === "FAILED" ? "failed" : "pending";
|
|
1129
|
+
} catch (e) {
|
|
1130
|
+
const msg = e?.message ?? String(e);
|
|
1131
|
+
if (/NothingToWithdraw|nothing to withdraw/i.test(msg)) {
|
|
1132
|
+
throw new NothingToWithdrawError({ prizePoolAddress: prizePool, wallet: from, cause: msg });
|
|
1133
|
+
}
|
|
1134
|
+
if (e instanceof NothingToWithdrawError || e instanceof PaymentFailedError) throw e;
|
|
1135
|
+
throw new PaymentFailedError("PrizePool.withdraw() failed.", { cause: msg });
|
|
1136
|
+
}
|
|
1137
|
+
if (status === "failed") {
|
|
1138
|
+
throw new PaymentFailedError("PrizePool.withdraw() did not confirm on-chain.", {
|
|
1139
|
+
prizePoolAddress: prizePool,
|
|
1140
|
+
txHash
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
if (amountMicro === 0n && input.checkBalance === false) ;
|
|
1144
|
+
return {
|
|
1145
|
+
prizePoolAddress: prizePool,
|
|
1146
|
+
amount: formatMicroToUsd(amountMicro),
|
|
1147
|
+
amountMicro: amountMicro.toString(),
|
|
1148
|
+
txHash,
|
|
1149
|
+
status
|
|
1150
|
+
};
|
|
708
1151
|
}
|
|
709
1152
|
};
|
|
710
1153
|
/**
|
|
@@ -879,6 +1322,11 @@ var Playmos = class {
|
|
|
879
1322
|
return this.http.get(`/listings/${encodeURIComponent(listingId)}`);
|
|
880
1323
|
}
|
|
881
1324
|
};
|
|
1325
|
+
/**
|
|
1326
|
+
* Offline mock payments for this client instance — so `verify(id)` after
|
|
1327
|
+
* `mock: true` pay/enterRound does not hit the live API (#204).
|
|
1328
|
+
*/
|
|
1329
|
+
this.mockPayments = /* @__PURE__ */ new Map();
|
|
882
1330
|
/**
|
|
883
1331
|
* `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
|
|
884
1332
|
* `get` reconciles once against chain truth; `wait` polls it to a terminal state
|
|
@@ -928,6 +1376,9 @@ var Playmos = class {
|
|
|
928
1376
|
}
|
|
929
1377
|
/** Connect the player's wallet and return their address. */
|
|
930
1378
|
async connect() {
|
|
1379
|
+
if (this.config.mock) {
|
|
1380
|
+
return "0x0000000000000000000000000000000000000001";
|
|
1381
|
+
}
|
|
931
1382
|
return getAccount(resolveProvider(this.config.wallet));
|
|
932
1383
|
}
|
|
933
1384
|
/**
|
|
@@ -944,18 +1395,29 @@ var Playmos = class {
|
|
|
944
1395
|
payEntry: async (req) => {
|
|
945
1396
|
const entry = await this.enterRound({
|
|
946
1397
|
gameId: cfg.gameId,
|
|
1398
|
+
// Studio roundId for bookkeeping; pin roundKey separately for hasEntered.
|
|
947
1399
|
roundId: req.roundKey,
|
|
948
|
-
// the round the server verifies against
|
|
949
1400
|
roundKey: req.roundKey,
|
|
950
1401
|
identity: req.identity,
|
|
951
1402
|
amount: formatMicroToUsd(req.entryUnits),
|
|
952
1403
|
playerId: req.wallet
|
|
953
1404
|
});
|
|
954
1405
|
const status = entry.status === "confirmed" ? "CONFIRMED" : entry.status === "failed" ? "FAILED" : "PENDING";
|
|
955
|
-
|
|
1406
|
+
const onchain = !entry.mock;
|
|
1407
|
+
return {
|
|
1408
|
+
paymentId: entry.id,
|
|
1409
|
+
status,
|
|
1410
|
+
txHash: entry.txHash,
|
|
1411
|
+
onchain,
|
|
1412
|
+
identity: entry.identity ?? req.identity
|
|
1413
|
+
};
|
|
956
1414
|
}
|
|
957
1415
|
};
|
|
958
1416
|
}
|
|
1417
|
+
rememberMock(payment) {
|
|
1418
|
+
if (payment.mock) this.mockPayments.set(payment.id, payment);
|
|
1419
|
+
return payment;
|
|
1420
|
+
}
|
|
959
1421
|
/** External-studio IAP (1%). Returns a real confirmed Payment with a txHash. */
|
|
960
1422
|
async pay(input) {
|
|
961
1423
|
const amountMicro = validateAmount(input.amount);
|
|
@@ -964,14 +1426,16 @@ var Playmos = class {
|
|
|
964
1426
|
const metadata = validateMetadata(input.metadata);
|
|
965
1427
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
966
1428
|
if (this.config.mock) {
|
|
967
|
-
return
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1429
|
+
return this.rememberMock(
|
|
1430
|
+
mockIapPayment({
|
|
1431
|
+
amountMicro,
|
|
1432
|
+
feeBps: IAP_FEE_BPS,
|
|
1433
|
+
sku: input.sku,
|
|
1434
|
+
playerId: input.playerId,
|
|
1435
|
+
chain: this.env.network,
|
|
1436
|
+
metadata
|
|
1437
|
+
})
|
|
1438
|
+
);
|
|
975
1439
|
}
|
|
976
1440
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
977
1441
|
const res = await this.http.post(
|
|
@@ -1014,17 +1478,22 @@ var Playmos = class {
|
|
|
1014
1478
|
const metadata = validateMetadata(input.metadata);
|
|
1015
1479
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
1016
1480
|
if (this.config.mock) {
|
|
1017
|
-
return
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1481
|
+
return this.rememberMock(
|
|
1482
|
+
mockEntryPayment({
|
|
1483
|
+
amountMicro,
|
|
1484
|
+
poolBps: POOL_BPS,
|
|
1485
|
+
seedBps: SEED_BPS,
|
|
1486
|
+
rakeBps: RAKE_BPS,
|
|
1487
|
+
gameId: input.gameId,
|
|
1488
|
+
roundId: input.roundId,
|
|
1489
|
+
playerId: input.playerId,
|
|
1490
|
+
chain: this.env.network,
|
|
1491
|
+
metadata,
|
|
1492
|
+
// B2 pins — must survive mock path for hub hasEntered parity (#204).
|
|
1493
|
+
roundKey: input.roundKey,
|
|
1494
|
+
identity: input.identity
|
|
1495
|
+
})
|
|
1496
|
+
);
|
|
1028
1497
|
}
|
|
1029
1498
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1030
1499
|
const res = await this.http.post(
|
|
@@ -1034,6 +1503,9 @@ var Playmos = class {
|
|
|
1034
1503
|
);
|
|
1035
1504
|
const payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
|
|
1036
1505
|
if (input.identity) payment2.identity = input.identity;
|
|
1506
|
+
const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
1507
|
+
if (pool) payment2.prizePoolAddress = pool.toLowerCase();
|
|
1508
|
+
payment2.roundKey = res.clientParams?.roundKey ?? input.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
|
|
1037
1509
|
return payment2;
|
|
1038
1510
|
}
|
|
1039
1511
|
const intent = await this.http.post(
|
|
@@ -1061,6 +1533,8 @@ var Playmos = class {
|
|
|
1061
1533
|
const { txHash } = await waitForCalls(provider, callsId);
|
|
1062
1534
|
const payment = await this.settle(intent.payment.id, txHash);
|
|
1063
1535
|
payment.identity = identity;
|
|
1536
|
+
payment.prizePoolAddress = prizePool.toLowerCase();
|
|
1537
|
+
payment.roundKey = roundKey;
|
|
1064
1538
|
return payment;
|
|
1065
1539
|
}
|
|
1066
1540
|
/**
|
|
@@ -1126,9 +1600,41 @@ var Playmos = class {
|
|
|
1126
1600
|
/** Verify a payment by the service's on-chain read (spec §6.1). Idempotent. */
|
|
1127
1601
|
async verify(paymentId) {
|
|
1128
1602
|
requireField(paymentId, "paymentId");
|
|
1603
|
+
if (this.config.mock) {
|
|
1604
|
+
const cached = this.mockPayments.get(paymentId);
|
|
1605
|
+
if (!cached) {
|
|
1606
|
+
throw new ApiError(`payment not found: ${paymentId}`, { status: 404, code: "not_found" });
|
|
1607
|
+
}
|
|
1608
|
+
const m = mockVerifyResult(cached);
|
|
1609
|
+
return {
|
|
1610
|
+
id: m.id,
|
|
1611
|
+
status: m.status,
|
|
1612
|
+
amount: m.amount,
|
|
1613
|
+
fee: m.fee,
|
|
1614
|
+
net: m.net,
|
|
1615
|
+
txHash: m.txHash,
|
|
1616
|
+
playerId: m.playerId ?? "",
|
|
1617
|
+
sku: m.sku,
|
|
1618
|
+
roundId: m.roundId,
|
|
1619
|
+
chain: m.chain,
|
|
1620
|
+
verifiedVia: "cache"
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1129
1623
|
return this.http.get(`/payments/${encodeURIComponent(paymentId)}`);
|
|
1130
1624
|
}
|
|
1131
1625
|
// ---- internals ---------------------------------------------------------
|
|
1626
|
+
/**
|
|
1627
|
+
* x402 V1 is Base Sepolia + test keys only (ADR D5). Fail closed before any
|
|
1628
|
+
* network call so mainnet / live keys never silently hit the adapter.
|
|
1629
|
+
*/
|
|
1630
|
+
assertX402Allowed() {
|
|
1631
|
+
if (!this.env.isTest || this.env.network !== "base-sepolia") {
|
|
1632
|
+
throw new ConfigError(
|
|
1633
|
+
'playmos.x402 is Base Sepolia / test-key only in V1 (ADR D5). Use apiKey sk_test_\u2026 and network "base-sepolia".',
|
|
1634
|
+
{ network: this.env.network, isTest: this.env.isTest }
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1132
1638
|
/**
|
|
1133
1639
|
* Map a server-settled payment (from the `settle: "server"` response) into the
|
|
1134
1640
|
* SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
|
|
@@ -1170,6 +1676,58 @@ var Playmos = class {
|
|
|
1170
1676
|
const fromServer = intent.clientParams?.amountUnits ?? intent.clientParams?.amountMicro;
|
|
1171
1677
|
return fromServer ? BigInt(fromServer) : local;
|
|
1172
1678
|
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Read PrizePool.withdrawable[wallet] via eth_call (issue #41).
|
|
1681
|
+
* Uses the wallet provider when present; otherwise a public Base Sepolia RPC in test.
|
|
1682
|
+
*/
|
|
1683
|
+
async readWithdrawable(prizePool, wallet) {
|
|
1684
|
+
const data = viem.encodeFunctionData({
|
|
1685
|
+
abi: prizePoolAbi,
|
|
1686
|
+
functionName: "withdrawable",
|
|
1687
|
+
args: [wallet]
|
|
1688
|
+
});
|
|
1689
|
+
let raw;
|
|
1690
|
+
try {
|
|
1691
|
+
if (await walletAvailable(this.config.wallet)) {
|
|
1692
|
+
const provider = resolveProvider(this.config.wallet);
|
|
1693
|
+
raw = await provider.request({
|
|
1694
|
+
method: "eth_call",
|
|
1695
|
+
params: [{ to: prizePool, data }, "latest"]
|
|
1696
|
+
});
|
|
1697
|
+
} else {
|
|
1698
|
+
const rpc = this.env.network === "base" ? "https://mainnet.base.org" : "https://sepolia.base.org";
|
|
1699
|
+
const res = await fetch(rpc, {
|
|
1700
|
+
method: "POST",
|
|
1701
|
+
headers: { "content-type": "application/json" },
|
|
1702
|
+
body: JSON.stringify({
|
|
1703
|
+
jsonrpc: "2.0",
|
|
1704
|
+
id: 1,
|
|
1705
|
+
method: "eth_call",
|
|
1706
|
+
params: [{ to: prizePool, data }, "latest"]
|
|
1707
|
+
})
|
|
1708
|
+
});
|
|
1709
|
+
const json = await res.json();
|
|
1710
|
+
if (!json.result) {
|
|
1711
|
+
throw new ApiError(
|
|
1712
|
+
`eth_call withdrawable failed: ${json.error?.message ?? "no result"}`,
|
|
1713
|
+
{ prizePool, wallet }
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
raw = json.result;
|
|
1717
|
+
}
|
|
1718
|
+
} catch (e) {
|
|
1719
|
+
if (e instanceof ApiError) throw e;
|
|
1720
|
+
throw new ApiError(`Could not read withdrawable balance: ${e.message}`, {
|
|
1721
|
+
prizePool,
|
|
1722
|
+
wallet
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
return viem.decodeFunctionResult({
|
|
1726
|
+
abi: prizePoolAbi,
|
|
1727
|
+
functionName: "withdrawable",
|
|
1728
|
+
data: raw
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1173
1731
|
sponsorUrl(intent) {
|
|
1174
1732
|
if (this.config.gas?.mode === "player") return void 0;
|
|
1175
1733
|
return this.config.gas?.paymasterUrl ?? intent.clientParams?.paymasterUrl;
|
|
@@ -1257,10 +1815,10 @@ var PayoutError = class extends Error {
|
|
|
1257
1815
|
this.name = "PayoutError";
|
|
1258
1816
|
}
|
|
1259
1817
|
};
|
|
1260
|
-
var
|
|
1818
|
+
var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
|
|
1261
1819
|
var BPS = 10000n;
|
|
1262
|
-
function
|
|
1263
|
-
if (!
|
|
1820
|
+
function requireAddress2(w, i) {
|
|
1821
|
+
if (!ADDRESS_RE3.test(w)) {
|
|
1264
1822
|
throw new PayoutError(`ranking[${i}] must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(w)}`);
|
|
1265
1823
|
}
|
|
1266
1824
|
return w.toLowerCase();
|
|
@@ -1283,7 +1841,7 @@ function computePayout(pool, ranking, rule) {
|
|
|
1283
1841
|
if (!Array.isArray(ranking) || ranking.length === 0) {
|
|
1284
1842
|
throw new PayoutError("ranking must be a non-empty array of wallets (best-first)");
|
|
1285
1843
|
}
|
|
1286
|
-
const wallets = ranking.map((w, i) =>
|
|
1844
|
+
const wallets = ranking.map((w, i) => requireAddress2(w, i));
|
|
1287
1845
|
const seen = /* @__PURE__ */ new Set();
|
|
1288
1846
|
for (const w of wallets) {
|
|
1289
1847
|
if (seen.has(w)) throw new PayoutError(`duplicate wallet in ranking: ${w}`);
|
|
@@ -1356,13 +1914,13 @@ function computePayout(pool, ranking, rule) {
|
|
|
1356
1914
|
}
|
|
1357
1915
|
|
|
1358
1916
|
// src/settlement.ts
|
|
1359
|
-
var
|
|
1917
|
+
var ADDRESS_RE4 = /^0x[0-9a-fA-F]{40}$/;
|
|
1360
1918
|
var DEFAULT_TTL_MS = 15 * 60 * 1e3;
|
|
1361
|
-
function
|
|
1919
|
+
function requireAddress3(value, field) {
|
|
1362
1920
|
if (typeof value !== "string" || value.trim() === "") {
|
|
1363
1921
|
throw new MissingFieldError(field);
|
|
1364
1922
|
}
|
|
1365
|
-
if (!
|
|
1923
|
+
if (!ADDRESS_RE4.test(value)) {
|
|
1366
1924
|
throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
|
|
1367
1925
|
field,
|
|
1368
1926
|
value
|
|
@@ -1379,7 +1937,7 @@ function requireNetwork(value) {
|
|
|
1379
1937
|
}
|
|
1380
1938
|
function createPaymentRequirement(input) {
|
|
1381
1939
|
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
1382
|
-
const payTo =
|
|
1940
|
+
const payTo = requireAddress3(input.payTo, "payTo");
|
|
1383
1941
|
const network = requireNetwork(input.network);
|
|
1384
1942
|
const asset = input.asset ?? "USDC";
|
|
1385
1943
|
if (asset !== "USDC") {
|
|
@@ -1422,7 +1980,7 @@ function parsePaymentRequirement(input) {
|
|
|
1422
1980
|
asset: o.asset
|
|
1423
1981
|
});
|
|
1424
1982
|
}
|
|
1425
|
-
const payTo =
|
|
1983
|
+
const payTo = requireAddress3(o.payTo, "payTo");
|
|
1426
1984
|
const network = requireNetwork(o.network);
|
|
1427
1985
|
if (typeof o.amount !== "string") throw new InvalidAmountError(o.amount);
|
|
1428
1986
|
parseUsdToMicro(o.amount);
|
|
@@ -1457,6 +2015,7 @@ exports.InsufficientGasError = InsufficientGasError;
|
|
|
1457
2015
|
exports.InvalidAmountError = InvalidAmountError;
|
|
1458
2016
|
exports.MICRO_PER_USDC = MICRO_PER_USDC;
|
|
1459
2017
|
exports.MissingFieldError = MissingFieldError;
|
|
2018
|
+
exports.NothingToWithdrawError = NothingToWithdrawError;
|
|
1460
2019
|
exports.PaymentFailedError = PaymentFailedError;
|
|
1461
2020
|
exports.PayoutError = PayoutError;
|
|
1462
2021
|
exports.Playmos = Playmos;
|
|
@@ -1468,9 +2027,13 @@ exports.computeIapSplit = computeIapSplit;
|
|
|
1468
2027
|
exports.computePayout = computePayout;
|
|
1469
2028
|
exports.computePoolSplit = computePoolSplit;
|
|
1470
2029
|
exports.createPaymentRequirement = createPaymentRequirement;
|
|
2030
|
+
exports.createX402Challenge = createX402Challenge;
|
|
2031
|
+
exports.decodePaymentHeader = decodePaymentHeader;
|
|
2032
|
+
exports.encodePaymentHeader = encodePaymentHeader;
|
|
1471
2033
|
exports.formatMicroToUsd = formatMicroToUsd;
|
|
1472
2034
|
exports.isWalletSignatureAuthorization = isWalletSignatureAuthorization;
|
|
1473
2035
|
exports.isX402PayloadAuthorization = isX402PayloadAuthorization;
|
|
2036
|
+
exports.networkToCaip2 = networkToCaip2;
|
|
1474
2037
|
exports.parsePaymentRequirement = parsePaymentRequirement;
|
|
1475
2038
|
exports.parseUsdToMicro = parseUsdToMicro;
|
|
1476
2039
|
exports.prefixedId = prefixedId;
|
|
@@ -1480,6 +2043,8 @@ exports.previewMarketplaceSplit = previewMarketplaceSplit;
|
|
|
1480
2043
|
exports.previewPoolSplit = previewPoolSplit;
|
|
1481
2044
|
exports.previewTransferSplit = previewTransferSplit;
|
|
1482
2045
|
exports.serializePaymentRequirement = serializePaymentRequirement;
|
|
2046
|
+
exports.toX402PaymentRequired = toX402PaymentRequired;
|
|
1483
2047
|
exports.ulid = ulid;
|
|
2048
|
+
exports.validateX402ChallengeInput = validateX402ChallengeInput;
|
|
1484
2049
|
//# sourceMappingURL=index.cjs.map
|
|
1485
2050
|
//# sourceMappingURL=index.cjs.map
|