@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.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { InvalidAmountError, ConfigError,
|
|
2
|
-
export { ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, PaymentFailedError, PlaymosError, WalletConnectionError } from './chunk-
|
|
3
|
-
import { encodeFunctionData, numberToHex, keccak256, toBytes } from 'viem';
|
|
1
|
+
import { InvalidAmountError, ConfigError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, ApiError, WalletConnectionError, InsufficientGasError, AuthError } from './chunk-TMBBEGIF.js';
|
|
2
|
+
export { ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError } from './chunk-TMBBEGIF.js';
|
|
3
|
+
import { encodeFunctionData, decodeFunctionResult, numberToHex, keccak256, toBytes } from 'viem';
|
|
4
4
|
|
|
5
5
|
// src/config.ts
|
|
6
6
|
var CHAIN_ID = {
|
|
@@ -205,15 +205,36 @@ function createHttpClient(baseUrl, apiKey, retry) {
|
|
|
205
205
|
);
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
|
+
function hasIdempotencyKey(init) {
|
|
209
|
+
const h = init.headers;
|
|
210
|
+
if (!h) return false;
|
|
211
|
+
if (h instanceof Headers) {
|
|
212
|
+
return Boolean(h.get("idempotency-key") || h.get("Idempotency-Key"));
|
|
213
|
+
}
|
|
214
|
+
if (Array.isArray(h)) {
|
|
215
|
+
return h.some(([k]) => String(k).toLowerCase() === "idempotency-key");
|
|
216
|
+
}
|
|
217
|
+
const rec = h;
|
|
218
|
+
return Object.keys(rec).some((k) => k.toLowerCase() === "idempotency-key" && !!rec[k]);
|
|
219
|
+
}
|
|
220
|
+
function isRetryable(res, init) {
|
|
221
|
+
if (res.status === 429) return true;
|
|
222
|
+
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
|
223
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
224
|
+
if (method === "GET" || method === "HEAD") return true;
|
|
225
|
+
return hasIdempotencyKey(init);
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
208
229
|
async function sendWithRetry(url, init) {
|
|
209
230
|
let res = await send(url, init);
|
|
210
|
-
for (let attempt = 0;
|
|
231
|
+
for (let attempt = 0; attempt < cfg.maxRetries && isRetryable(res, init); attempt++) {
|
|
211
232
|
await sleep(backoffMs(res, attempt, cfg));
|
|
212
233
|
res = await send(url, init);
|
|
213
234
|
}
|
|
214
235
|
return res;
|
|
215
236
|
}
|
|
216
|
-
async function handle(res) {
|
|
237
|
+
async function handle(res, acceptStatuses) {
|
|
217
238
|
let text;
|
|
218
239
|
try {
|
|
219
240
|
text = await res.text();
|
|
@@ -228,9 +249,13 @@ function createHttpClient(baseUrl, apiKey, retry) {
|
|
|
228
249
|
try {
|
|
229
250
|
json = text ? JSON.parse(text) : {};
|
|
230
251
|
} catch {
|
|
231
|
-
|
|
252
|
+
const gateway = res.status === 502 || res.status === 503 || res.status === 504;
|
|
253
|
+
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}`;
|
|
254
|
+
throw new ApiError(msg, { status: res.status, body: text.slice(0, 500), gateway });
|
|
255
|
+
}
|
|
256
|
+
if (res.ok || acceptStatuses !== void 0 && acceptStatuses.includes(res.status)) {
|
|
257
|
+
return json;
|
|
232
258
|
}
|
|
233
|
-
if (res.ok) return json;
|
|
234
259
|
const errBody = json;
|
|
235
260
|
const message = errBody?.error?.message ?? `Request failed with ${res.status}`;
|
|
236
261
|
if (res.status === 401 || res.status === 403 || errBody?.error?.code === "auth") {
|
|
@@ -246,7 +271,7 @@ function createHttpClient(baseUrl, apiKey, retry) {
|
|
|
246
271
|
};
|
|
247
272
|
if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
|
|
248
273
|
const res = await sendWithRetry(`${base}${path}`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
249
|
-
return handle(res);
|
|
274
|
+
return handle(res, opts?.acceptStatuses);
|
|
250
275
|
},
|
|
251
276
|
async get(path) {
|
|
252
277
|
const res = await sendWithRetry(`${base}${path}`, {
|
|
@@ -403,6 +428,13 @@ var prizePoolAbi = [
|
|
|
403
428
|
],
|
|
404
429
|
outputs: [{ type: "bool" }]
|
|
405
430
|
},
|
|
431
|
+
{
|
|
432
|
+
type: "function",
|
|
433
|
+
name: "withdrawable",
|
|
434
|
+
stateMutability: "view",
|
|
435
|
+
inputs: [{ name: "account", type: "address" }],
|
|
436
|
+
outputs: [{ type: "uint256" }]
|
|
437
|
+
},
|
|
406
438
|
{
|
|
407
439
|
type: "function",
|
|
408
440
|
name: "withdraw",
|
|
@@ -455,7 +487,23 @@ async function waitForCalls(provider, id, timeoutMs = 6e4) {
|
|
|
455
487
|
function encodeApprove(spender, amountUnits) {
|
|
456
488
|
return encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [spender, amountUnits] });
|
|
457
489
|
}
|
|
458
|
-
|
|
490
|
+
var GAS_FLOOR_WEI_DEFAULT = 200000000000000n;
|
|
491
|
+
var GAS_FLOOR_WEI_BASE_SEPOLIA = 50000000000000n;
|
|
492
|
+
var CHAIN_ID_BASE_SEPOLIA = 84532;
|
|
493
|
+
function gasFloorWei(chainId) {
|
|
494
|
+
return chainId === CHAIN_ID_BASE_SEPOLIA ? GAS_FLOOR_WEI_BASE_SEPOLIA : GAS_FLOOR_WEI_DEFAULT;
|
|
495
|
+
}
|
|
496
|
+
async function assertEnoughGas(provider, from, minWei) {
|
|
497
|
+
let floor = minWei;
|
|
498
|
+
if (floor === void 0) {
|
|
499
|
+
let chainId = 0;
|
|
500
|
+
try {
|
|
501
|
+
const hex = await provider.request({ method: "eth_chainId", params: [] });
|
|
502
|
+
chainId = Number(BigInt(hex));
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
floor = gasFloorWei(chainId);
|
|
506
|
+
}
|
|
459
507
|
let balance;
|
|
460
508
|
try {
|
|
461
509
|
const hex = await provider.request({ method: "eth_getBalance", params: [from, "latest"] });
|
|
@@ -463,8 +511,11 @@ async function assertEnoughGas(provider, from, minWei = 200000000000000n) {
|
|
|
463
511
|
} catch {
|
|
464
512
|
return;
|
|
465
513
|
}
|
|
466
|
-
if (balance <
|
|
467
|
-
throw new InsufficientGasError({
|
|
514
|
+
if (balance < floor) {
|
|
515
|
+
throw new InsufficientGasError({
|
|
516
|
+
balanceWei: balance.toString(),
|
|
517
|
+
minWei: floor.toString()
|
|
518
|
+
});
|
|
468
519
|
}
|
|
469
520
|
}
|
|
470
521
|
var toBytes32 = (s) => keccak256(toBytes(s));
|
|
@@ -490,6 +541,14 @@ function buildEntryCalls(args) {
|
|
|
490
541
|
{ to: args.prizePool, data: enterData }
|
|
491
542
|
];
|
|
492
543
|
}
|
|
544
|
+
function buildWithdrawCall(prizePool) {
|
|
545
|
+
const data = encodeFunctionData({
|
|
546
|
+
abi: prizePoolAbi,
|
|
547
|
+
functionName: "withdraw",
|
|
548
|
+
args: []
|
|
549
|
+
});
|
|
550
|
+
return { to: prizePool, data };
|
|
551
|
+
}
|
|
493
552
|
|
|
494
553
|
// src/mock.ts
|
|
495
554
|
var MOCK_TX = "0x000000000000000000000000000000000000000000000000000000000000mock";
|
|
@@ -518,6 +577,7 @@ function mockEntryPayment(args) {
|
|
|
518
577
|
args.seedBps,
|
|
519
578
|
args.rakeBps
|
|
520
579
|
);
|
|
580
|
+
const roundKey = args.roundKey && args.roundKey.trim() !== "" ? args.roundKey : args.roundId;
|
|
521
581
|
return {
|
|
522
582
|
id: prefixedId("entry"),
|
|
523
583
|
status: "confirmed",
|
|
@@ -534,6 +594,10 @@ function mockEntryPayment(args) {
|
|
|
534
594
|
},
|
|
535
595
|
gameId: args.gameId,
|
|
536
596
|
roundId: args.roundId,
|
|
597
|
+
roundKey,
|
|
598
|
+
identity: args.identity,
|
|
599
|
+
// Deterministic mock pool address so claim-flow unit tests can assert shape (#41).
|
|
600
|
+
prizePoolAddress: "0x0000000000000000000000000000000000000001",
|
|
537
601
|
playerId: args.playerId,
|
|
538
602
|
txHash: MOCK_TX,
|
|
539
603
|
chain: args.chain,
|
|
@@ -542,14 +606,160 @@ function mockEntryPayment(args) {
|
|
|
542
606
|
mock: true
|
|
543
607
|
};
|
|
544
608
|
}
|
|
609
|
+
function mockVerifyResult(payment) {
|
|
610
|
+
return { ...payment, mock: true };
|
|
611
|
+
}
|
|
545
612
|
|
|
546
|
-
// src/
|
|
613
|
+
// src/x402.ts
|
|
547
614
|
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
548
|
-
function
|
|
615
|
+
function requireAddress(value, field) {
|
|
549
616
|
if (typeof value !== "string" || value.trim() === "") {
|
|
550
617
|
throw new MissingFieldError(field);
|
|
551
618
|
}
|
|
552
619
|
if (!ADDRESS_RE.test(value)) {
|
|
620
|
+
throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
|
|
621
|
+
field,
|
|
622
|
+
value
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return value.toLowerCase();
|
|
626
|
+
}
|
|
627
|
+
function networkToCaip2(network) {
|
|
628
|
+
if (network === "base") return "eip155:8453";
|
|
629
|
+
if (network === "base-sepolia") return "eip155:84532";
|
|
630
|
+
throw new ConfigError(`Unknown network for x402: ${JSON.stringify(network)}`, { network });
|
|
631
|
+
}
|
|
632
|
+
function toX402PaymentRequired(req, extras) {
|
|
633
|
+
const amountMicro = parseUsdToMicro(req.amount);
|
|
634
|
+
const out = {
|
|
635
|
+
scheme: "exact",
|
|
636
|
+
network: networkToCaip2(req.network),
|
|
637
|
+
maxAmountRequired: amountMicro.toString(),
|
|
638
|
+
// Service V1 emits the asset symbol; address is implied by network + config.
|
|
639
|
+
asset: "USDC",
|
|
640
|
+
payTo: req.payTo,
|
|
641
|
+
playmosRequirementId: req.id,
|
|
642
|
+
amount: req.amount,
|
|
643
|
+
playmosNetwork: req.network,
|
|
644
|
+
expiresAt: req.expiresAt,
|
|
645
|
+
terms: req.terms ?? null,
|
|
646
|
+
feeBps: extras?.feeBps ?? null,
|
|
647
|
+
feeSink: extras?.feeSink ?? null
|
|
648
|
+
};
|
|
649
|
+
return out;
|
|
650
|
+
}
|
|
651
|
+
function encodePaymentHeader(obj) {
|
|
652
|
+
const json = JSON.stringify(obj);
|
|
653
|
+
if (typeof Buffer !== "undefined") {
|
|
654
|
+
return Buffer.from(json, "utf8").toString("base64");
|
|
655
|
+
}
|
|
656
|
+
const bytes = new TextEncoder().encode(json);
|
|
657
|
+
let bin = "";
|
|
658
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
659
|
+
return btoa(bin);
|
|
660
|
+
}
|
|
661
|
+
function decodePaymentHeader(header) {
|
|
662
|
+
if (typeof header !== "string" || header.trim() === "") {
|
|
663
|
+
throw new ConfigError("payment header must be a non-empty base64 JSON string");
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
let json;
|
|
667
|
+
if (typeof Buffer !== "undefined") {
|
|
668
|
+
json = Buffer.from(header, "base64").toString("utf8");
|
|
669
|
+
} else {
|
|
670
|
+
const bin = atob(header);
|
|
671
|
+
const bytes = new Uint8Array(bin.length);
|
|
672
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
673
|
+
json = new TextDecoder().decode(bytes);
|
|
674
|
+
}
|
|
675
|
+
return JSON.parse(json);
|
|
676
|
+
} catch (err) {
|
|
677
|
+
throw new ConfigError("invalid base64 JSON payment header", {
|
|
678
|
+
cause: err instanceof Error ? err.message : String(err)
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function createX402Challenge(requirement, extras) {
|
|
683
|
+
if (!requirement || typeof requirement !== "object") {
|
|
684
|
+
throw new ConfigError("createX402Challenge requires a PaymentRequirement");
|
|
685
|
+
}
|
|
686
|
+
if (typeof requirement.id !== "string" || !requirement.id.startsWith("preq_")) {
|
|
687
|
+
throw new ConfigError('PaymentRequirement.id must be a "preq_\u2026" string', { id: requirement.id });
|
|
688
|
+
}
|
|
689
|
+
requireAddress(requirement.payTo, "payTo");
|
|
690
|
+
parseUsdToMicro(requirement.amount);
|
|
691
|
+
const feeBps = extras?.feeBps ?? 0;
|
|
692
|
+
if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 1e4) {
|
|
693
|
+
throw new ConfigError(
|
|
694
|
+
`feeBps must be an integer in [0, 10000], got: ${JSON.stringify(extras?.feeBps)}`,
|
|
695
|
+
{ feeBps: extras?.feeBps }
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
const feeSink = extras?.feeSink ?? null;
|
|
699
|
+
if (feeBps > 0 && feeSink) requireAddress(feeSink, "feeSink");
|
|
700
|
+
const paymentRequired = toX402PaymentRequired(requirement, { feeBps, feeSink });
|
|
701
|
+
return {
|
|
702
|
+
status: 402,
|
|
703
|
+
headers: {
|
|
704
|
+
"PAYMENT-REQUIRED": encodePaymentHeader(paymentRequired),
|
|
705
|
+
"Content-Type": "application/json"
|
|
706
|
+
},
|
|
707
|
+
body: {
|
|
708
|
+
error: { code: "payment_required", message: "Payment Required" },
|
|
709
|
+
requirement,
|
|
710
|
+
paymentRequired,
|
|
711
|
+
feeBps,
|
|
712
|
+
feeSink
|
|
713
|
+
},
|
|
714
|
+
paymentRequired
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function validateX402ChallengeInput(input) {
|
|
718
|
+
if (!input || typeof input !== "object") {
|
|
719
|
+
throw new ConfigError("x402 challenge input is required");
|
|
720
|
+
}
|
|
721
|
+
if ("url" in input && input.url !== void 0) {
|
|
722
|
+
throw new ConfigError(
|
|
723
|
+
"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.",
|
|
724
|
+
{ field: "url" }
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
const payTo = requireAddress(input.payTo, "payTo");
|
|
728
|
+
parseUsdToMicro(input.amount);
|
|
729
|
+
const intent = input.intent ?? "transfer";
|
|
730
|
+
if (intent !== "transfer" && intent !== "marketplace.buy") {
|
|
731
|
+
throw new ConfigError(
|
|
732
|
+
`x402 intent must be "transfer" or "marketplace.buy", got: ${JSON.stringify(input.intent)}`,
|
|
733
|
+
{ intent: input.intent }
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
const feeBps = input.feeBps ?? 0;
|
|
737
|
+
if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 1e4) {
|
|
738
|
+
throw new ConfigError(
|
|
739
|
+
`feeBps must be an integer in [0, 10000], got: ${JSON.stringify(input.feeBps)}`,
|
|
740
|
+
{ feeBps: input.feeBps }
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
const feeSink = input.feeSink === void 0 ? void 0 : requireAddress(input.feeSink, "feeSink");
|
|
744
|
+
return {
|
|
745
|
+
payTo,
|
|
746
|
+
amount: input.amount,
|
|
747
|
+
intent,
|
|
748
|
+
feeBps,
|
|
749
|
+
...feeSink !== void 0 ? { feeSink } : {},
|
|
750
|
+
...input.terms !== void 0 ? { terms: input.terms } : {},
|
|
751
|
+
...input.expiresInMs !== void 0 ? { expiresInMs: input.expiresInMs } : {},
|
|
752
|
+
...input.id !== void 0 ? { id: input.id } : {}
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/client.ts
|
|
757
|
+
var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
|
|
758
|
+
function requireAddressField(value, field) {
|
|
759
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
760
|
+
throw new MissingFieldError(field);
|
|
761
|
+
}
|
|
762
|
+
if (!ADDRESS_RE2.test(value)) {
|
|
553
763
|
throw new ConfigError(
|
|
554
764
|
`${field} must be a 0x-prefixed 20-byte wallet address (Phase 1a transfers settle server-held wallets), got: ${JSON.stringify(value)}`,
|
|
555
765
|
{ field, value }
|
|
@@ -594,6 +804,122 @@ var Playmos = class {
|
|
|
594
804
|
/** Create a Bridge KYC onboarding link (fiat payout). */
|
|
595
805
|
createOnboardingLink: () => this.http.post("/payouts/onboarding_link", {})
|
|
596
806
|
};
|
|
807
|
+
/**
|
|
808
|
+
* `x402` — Phase 4 HTTP 402 / x402-shaped adapter (#153).
|
|
809
|
+
*
|
|
810
|
+
* V1 surface (ADR):
|
|
811
|
+
* - `challenge` → `POST /v1/x402/challenges` (returns 402; treated as success)
|
|
812
|
+
* - `fulfill` → `POST /v1/x402/settle`
|
|
813
|
+
* - `pay` → challenge then fulfill (senior DX ≤15 lines)
|
|
814
|
+
*
|
|
815
|
+
* **Not** resource URLs (`pay({ url })` is V1.1+). **Not** stock third-party
|
|
816
|
+
* x402 client interop (X3b deferred until #161). Requires `sk_test_` +
|
|
817
|
+
* Base Sepolia; flag `PLAYMOS_X402_ENABLED` on the service.
|
|
818
|
+
*
|
|
819
|
+
* ```ts
|
|
820
|
+
* const playmos = new Playmos({ apiKey: process.env.PLAYMOS_SK!, network: "base-sepolia" });
|
|
821
|
+
* const result = await playmos.x402.pay({
|
|
822
|
+
* payTo: "0x…",
|
|
823
|
+
* amount: "0.50",
|
|
824
|
+
* feeBps: 0,
|
|
825
|
+
* });
|
|
826
|
+
* // result.txHash → BaseScan
|
|
827
|
+
* ```
|
|
828
|
+
*/
|
|
829
|
+
this.x402 = {
|
|
830
|
+
/**
|
|
831
|
+
* Mint a server-authoritative PaymentRequirement (HTTP 402).
|
|
832
|
+
* Fee terms are stored server-side — settle rejects payload disagreement.
|
|
833
|
+
*/
|
|
834
|
+
challenge: async (input) => {
|
|
835
|
+
this.assertX402Allowed();
|
|
836
|
+
const body = validateX402ChallengeInput(input);
|
|
837
|
+
if (body.intent === "marketplace.buy") {
|
|
838
|
+
throw new ConfigError(
|
|
839
|
+
'x402 challenge intent "marketplace.buy" is not supported yet (V1 service: "transfer" only; marketplace.buy follows).',
|
|
840
|
+
{ intent: body.intent }
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
const res = await this.http.post("/x402/challenges", body, { acceptStatuses: [402] });
|
|
844
|
+
return {
|
|
845
|
+
requirement: res.requirement,
|
|
846
|
+
paymentRequired: res.paymentRequired,
|
|
847
|
+
feeBps: res.feeBps ?? body.feeBps,
|
|
848
|
+
feeSink: res.feeSink ?? body.feeSink ?? null
|
|
849
|
+
};
|
|
850
|
+
},
|
|
851
|
+
/**
|
|
852
|
+
* Settle a previously minted challenge with an `x402-payload` authorization.
|
|
853
|
+
* Defaults mode to `server-signer` when omitted (explicit on the wire — fail-closed
|
|
854
|
+
* materialize still requires mode; the route also defaults for this path).
|
|
855
|
+
*/
|
|
856
|
+
fulfill: async (requirement, authorization) => {
|
|
857
|
+
this.assertX402Allowed();
|
|
858
|
+
if (!requirement || typeof requirement !== "object") {
|
|
859
|
+
throw new ConfigError("fulfill requires a PaymentRequirement from challenge()");
|
|
860
|
+
}
|
|
861
|
+
if (typeof requirement.id !== "string" || requirement.id.trim() === "") {
|
|
862
|
+
throw new MissingFieldError("requirement.id");
|
|
863
|
+
}
|
|
864
|
+
let payload;
|
|
865
|
+
if (authorization && typeof authorization === "object" && "kind" in authorization) {
|
|
866
|
+
const a = authorization;
|
|
867
|
+
if (a.kind !== "x402-payload") {
|
|
868
|
+
throw new ConfigError('authorization.kind must be "x402-payload"', { kind: a.kind });
|
|
869
|
+
}
|
|
870
|
+
payload = { ...a.payload ?? {} };
|
|
871
|
+
} else {
|
|
872
|
+
payload = { ...authorization ?? {} };
|
|
873
|
+
}
|
|
874
|
+
delete payload.feeBps;
|
|
875
|
+
delete payload.feeSink;
|
|
876
|
+
if (payload.mode === void 0 || typeof payload.mode === "string" && payload.mode.trim() === "") {
|
|
877
|
+
const hasNestedAuth = payload.authorization !== void 0 || payload.signature !== void 0 || payload.paymentPayload !== void 0;
|
|
878
|
+
if (hasNestedAuth) {
|
|
879
|
+
throw new ConfigError(
|
|
880
|
+
"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)",
|
|
881
|
+
{ field: "mode" }
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
payload.mode = "server-signer";
|
|
885
|
+
}
|
|
886
|
+
if (payload.scheme === void 0) payload.scheme = "exact";
|
|
887
|
+
if (payload.requirementId === void 0) payload.requirementId = requirement.id;
|
|
888
|
+
const res = await this.http.post("/x402/settle", {
|
|
889
|
+
requirement,
|
|
890
|
+
authorization: { kind: "x402-payload", payload }
|
|
891
|
+
});
|
|
892
|
+
const s = res.settlement;
|
|
893
|
+
return {
|
|
894
|
+
requirementId: s.requirementId,
|
|
895
|
+
status: s.status,
|
|
896
|
+
txHash: s.txHash ?? null,
|
|
897
|
+
idempotentReplay: Boolean(s.idempotentReplay),
|
|
898
|
+
verifiedVia: s.verifiedVia,
|
|
899
|
+
feeBps: s.feeBps,
|
|
900
|
+
fee: s.fee,
|
|
901
|
+
net: s.net,
|
|
902
|
+
requirement: res.requirement,
|
|
903
|
+
success: s.success
|
|
904
|
+
};
|
|
905
|
+
},
|
|
906
|
+
/**
|
|
907
|
+
* Senior DX: mint challenge + settle in one call.
|
|
908
|
+
* Wraps challenges + settle only — **not** `pay({ url })` (ADR).
|
|
909
|
+
*/
|
|
910
|
+
pay: async (input) => {
|
|
911
|
+
this.assertX402Allowed();
|
|
912
|
+
validateX402ChallengeInput(input);
|
|
913
|
+
const challenge = await this.x402.challenge(input);
|
|
914
|
+
const auth = {
|
|
915
|
+
mode: input.mode ?? "server-signer"
|
|
916
|
+
};
|
|
917
|
+
if (input.from !== void 0) auth.from = input.from;
|
|
918
|
+
if (input.authorization !== void 0) auth.authorization = input.authorization;
|
|
919
|
+
if (input.txHash !== void 0) auth.txHash = input.txHash;
|
|
920
|
+
return this.x402.fulfill(challenge.requirement, auth);
|
|
921
|
+
}
|
|
922
|
+
};
|
|
597
923
|
/**
|
|
598
924
|
* Skill/contest **round lifecycle** (issue #13) — closes the enterRound loop.
|
|
599
925
|
*
|
|
@@ -646,6 +972,112 @@ var Playmos = class {
|
|
|
646
972
|
`/rounds/${encodeURIComponent(input.roundId)}`
|
|
647
973
|
);
|
|
648
974
|
return round;
|
|
975
|
+
},
|
|
976
|
+
/**
|
|
977
|
+
* Read a wallet's **claimable** prize balance for a round (issue #41).
|
|
978
|
+
* Prefers the service chain read (`GET /v1/rounds/:id/prize?wallet=`); falls
|
|
979
|
+
* back to a direct eth_call when `prizePoolAddress` is supplied and no wallet
|
|
980
|
+
* connector is needed for the read path… actually uses service first, then
|
|
981
|
+
* on-chain via the player's provider if the service is unavailable.
|
|
982
|
+
*/
|
|
983
|
+
prize: async (input) => {
|
|
984
|
+
requireField(input?.roundId, "roundId");
|
|
985
|
+
const wallet = requireAddressField(input?.wallet, "wallet");
|
|
986
|
+
try {
|
|
987
|
+
const res = await this.http.get(
|
|
988
|
+
`/rounds/${encodeURIComponent(input.roundId)}/prize?wallet=${encodeURIComponent(wallet)}`
|
|
989
|
+
);
|
|
990
|
+
return res.prize;
|
|
991
|
+
} catch (e) {
|
|
992
|
+
if (!input.prizePoolAddress && !(e instanceof ApiError)) throw e;
|
|
993
|
+
if (!input.prizePoolAddress) throw e;
|
|
994
|
+
}
|
|
995
|
+
const prizePoolAddress = requireAddressField(input.prizePoolAddress, "prizePoolAddress");
|
|
996
|
+
const claimableMicro = await this.readWithdrawable(prizePoolAddress, wallet);
|
|
997
|
+
return {
|
|
998
|
+
roundId: input.roundId,
|
|
999
|
+
prizePoolAddress,
|
|
1000
|
+
wallet,
|
|
1001
|
+
claimable: formatMicroToUsd(claimableMicro),
|
|
1002
|
+
claimableMicro: claimableMicro.toString()
|
|
1003
|
+
};
|
|
1004
|
+
},
|
|
1005
|
+
/**
|
|
1006
|
+
* Winner **claim** — call PrizePool.withdraw() from the player's wallet (issue #41).
|
|
1007
|
+
* Pull-payment: credits from settle live in `withdrawable[msg.sender]`. No approve needed.
|
|
1008
|
+
* Pass either `prizePoolAddress` (from enterRound / rounds.get) or `roundId` (service resolves).
|
|
1009
|
+
*/
|
|
1010
|
+
withdraw: async (input) => {
|
|
1011
|
+
if (this.config.mock) {
|
|
1012
|
+
return {
|
|
1013
|
+
prizePoolAddress: input.prizePoolAddress ?? "0x0000000000000000000000000000000000000001",
|
|
1014
|
+
amount: "0.00",
|
|
1015
|
+
amountMicro: "0",
|
|
1016
|
+
txHash: "0x000000000000000000000000000000000000000000000000000000000000mock",
|
|
1017
|
+
status: "confirmed"
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
let prizePool = input.prizePoolAddress ? requireAddressField(input.prizePoolAddress, "prizePoolAddress") : void 0;
|
|
1021
|
+
if (!prizePool) {
|
|
1022
|
+
requireField(input?.roundId, "roundId");
|
|
1023
|
+
const round = await this.rounds.get({ roundId: input.roundId });
|
|
1024
|
+
if (!round.prizePoolAddress) {
|
|
1025
|
+
throw new ConfigError(
|
|
1026
|
+
"rounds.get did not return prizePoolAddress \u2014 pass prizePoolAddress from enterRound or configure contracts.prizePool"
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
prizePool = round.prizePoolAddress;
|
|
1030
|
+
}
|
|
1031
|
+
const provider = resolveProvider(this.config.wallet);
|
|
1032
|
+
if (this.config.gas?.mode === "player") {
|
|
1033
|
+
const from0 = await getAccount(provider);
|
|
1034
|
+
await assertEnoughGas(provider, from0);
|
|
1035
|
+
}
|
|
1036
|
+
const from = await getAccount(provider);
|
|
1037
|
+
let amountMicro = 0n;
|
|
1038
|
+
if (input.checkBalance !== false) {
|
|
1039
|
+
amountMicro = await this.readWithdrawable(prizePool, from);
|
|
1040
|
+
if (amountMicro === 0n) {
|
|
1041
|
+
throw new NothingToWithdrawError({ prizePoolAddress: prizePool, wallet: from });
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
const call = buildWithdrawCall(prizePool);
|
|
1045
|
+
const paymasterUrl = this.config.gas?.mode === "sponsored" ? this.config.gas.paymasterUrl : void 0;
|
|
1046
|
+
let txHash;
|
|
1047
|
+
let status = "pending";
|
|
1048
|
+
try {
|
|
1049
|
+
const { id: callsId } = await sendCalls(
|
|
1050
|
+
provider,
|
|
1051
|
+
from,
|
|
1052
|
+
this.env.chainId,
|
|
1053
|
+
[call],
|
|
1054
|
+
paymasterUrl
|
|
1055
|
+
);
|
|
1056
|
+
const waited = await waitForCalls(provider, callsId);
|
|
1057
|
+
txHash = waited.txHash;
|
|
1058
|
+
status = waited.status === "CONFIRMED" ? "confirmed" : waited.status === "FAILED" ? "failed" : "pending";
|
|
1059
|
+
} catch (e) {
|
|
1060
|
+
const msg = e?.message ?? String(e);
|
|
1061
|
+
if (/NothingToWithdraw|nothing to withdraw/i.test(msg)) {
|
|
1062
|
+
throw new NothingToWithdrawError({ prizePoolAddress: prizePool, wallet: from, cause: msg });
|
|
1063
|
+
}
|
|
1064
|
+
if (e instanceof NothingToWithdrawError || e instanceof PaymentFailedError) throw e;
|
|
1065
|
+
throw new PaymentFailedError("PrizePool.withdraw() failed.", { cause: msg });
|
|
1066
|
+
}
|
|
1067
|
+
if (status === "failed") {
|
|
1068
|
+
throw new PaymentFailedError("PrizePool.withdraw() did not confirm on-chain.", {
|
|
1069
|
+
prizePoolAddress: prizePool,
|
|
1070
|
+
txHash
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
if (amountMicro === 0n && input.checkBalance === false) ;
|
|
1074
|
+
return {
|
|
1075
|
+
prizePoolAddress: prizePool,
|
|
1076
|
+
amount: formatMicroToUsd(amountMicro),
|
|
1077
|
+
amountMicro: amountMicro.toString(),
|
|
1078
|
+
txHash,
|
|
1079
|
+
status
|
|
1080
|
+
};
|
|
649
1081
|
}
|
|
650
1082
|
};
|
|
651
1083
|
/**
|
|
@@ -820,6 +1252,11 @@ var Playmos = class {
|
|
|
820
1252
|
return this.http.get(`/listings/${encodeURIComponent(listingId)}`);
|
|
821
1253
|
}
|
|
822
1254
|
};
|
|
1255
|
+
/**
|
|
1256
|
+
* Offline mock payments for this client instance — so `verify(id)` after
|
|
1257
|
+
* `mock: true` pay/enterRound does not hit the live API (#204).
|
|
1258
|
+
*/
|
|
1259
|
+
this.mockPayments = /* @__PURE__ */ new Map();
|
|
823
1260
|
/**
|
|
824
1261
|
* `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
|
|
825
1262
|
* `get` reconciles once against chain truth; `wait` polls it to a terminal state
|
|
@@ -869,6 +1306,9 @@ var Playmos = class {
|
|
|
869
1306
|
}
|
|
870
1307
|
/** Connect the player's wallet and return their address. */
|
|
871
1308
|
async connect() {
|
|
1309
|
+
if (this.config.mock) {
|
|
1310
|
+
return "0x0000000000000000000000000000000000000001";
|
|
1311
|
+
}
|
|
872
1312
|
return getAccount(resolveProvider(this.config.wallet));
|
|
873
1313
|
}
|
|
874
1314
|
/**
|
|
@@ -885,18 +1325,29 @@ var Playmos = class {
|
|
|
885
1325
|
payEntry: async (req) => {
|
|
886
1326
|
const entry = await this.enterRound({
|
|
887
1327
|
gameId: cfg.gameId,
|
|
1328
|
+
// Studio roundId for bookkeeping; pin roundKey separately for hasEntered.
|
|
888
1329
|
roundId: req.roundKey,
|
|
889
|
-
// the round the server verifies against
|
|
890
1330
|
roundKey: req.roundKey,
|
|
891
1331
|
identity: req.identity,
|
|
892
1332
|
amount: formatMicroToUsd(req.entryUnits),
|
|
893
1333
|
playerId: req.wallet
|
|
894
1334
|
});
|
|
895
1335
|
const status = entry.status === "confirmed" ? "CONFIRMED" : entry.status === "failed" ? "FAILED" : "PENDING";
|
|
896
|
-
|
|
1336
|
+
const onchain = !entry.mock;
|
|
1337
|
+
return {
|
|
1338
|
+
paymentId: entry.id,
|
|
1339
|
+
status,
|
|
1340
|
+
txHash: entry.txHash,
|
|
1341
|
+
onchain,
|
|
1342
|
+
identity: entry.identity ?? req.identity
|
|
1343
|
+
};
|
|
897
1344
|
}
|
|
898
1345
|
};
|
|
899
1346
|
}
|
|
1347
|
+
rememberMock(payment) {
|
|
1348
|
+
if (payment.mock) this.mockPayments.set(payment.id, payment);
|
|
1349
|
+
return payment;
|
|
1350
|
+
}
|
|
900
1351
|
/** External-studio IAP (1%). Returns a real confirmed Payment with a txHash. */
|
|
901
1352
|
async pay(input) {
|
|
902
1353
|
const amountMicro = validateAmount(input.amount);
|
|
@@ -905,14 +1356,16 @@ var Playmos = class {
|
|
|
905
1356
|
const metadata = validateMetadata(input.metadata);
|
|
906
1357
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
907
1358
|
if (this.config.mock) {
|
|
908
|
-
return
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1359
|
+
return this.rememberMock(
|
|
1360
|
+
mockIapPayment({
|
|
1361
|
+
amountMicro,
|
|
1362
|
+
feeBps: IAP_FEE_BPS,
|
|
1363
|
+
sku: input.sku,
|
|
1364
|
+
playerId: input.playerId,
|
|
1365
|
+
chain: this.env.network,
|
|
1366
|
+
metadata
|
|
1367
|
+
})
|
|
1368
|
+
);
|
|
916
1369
|
}
|
|
917
1370
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
918
1371
|
const res = await this.http.post(
|
|
@@ -955,17 +1408,22 @@ var Playmos = class {
|
|
|
955
1408
|
const metadata = validateMetadata(input.metadata);
|
|
956
1409
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
957
1410
|
if (this.config.mock) {
|
|
958
|
-
return
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1411
|
+
return this.rememberMock(
|
|
1412
|
+
mockEntryPayment({
|
|
1413
|
+
amountMicro,
|
|
1414
|
+
poolBps: POOL_BPS,
|
|
1415
|
+
seedBps: SEED_BPS,
|
|
1416
|
+
rakeBps: RAKE_BPS,
|
|
1417
|
+
gameId: input.gameId,
|
|
1418
|
+
roundId: input.roundId,
|
|
1419
|
+
playerId: input.playerId,
|
|
1420
|
+
chain: this.env.network,
|
|
1421
|
+
metadata,
|
|
1422
|
+
// B2 pins — must survive mock path for hub hasEntered parity (#204).
|
|
1423
|
+
roundKey: input.roundKey,
|
|
1424
|
+
identity: input.identity
|
|
1425
|
+
})
|
|
1426
|
+
);
|
|
969
1427
|
}
|
|
970
1428
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
971
1429
|
const res = await this.http.post(
|
|
@@ -975,6 +1433,9 @@ var Playmos = class {
|
|
|
975
1433
|
);
|
|
976
1434
|
const payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
|
|
977
1435
|
if (input.identity) payment2.identity = input.identity;
|
|
1436
|
+
const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
1437
|
+
if (pool) payment2.prizePoolAddress = pool.toLowerCase();
|
|
1438
|
+
payment2.roundKey = res.clientParams?.roundKey ?? input.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
|
|
978
1439
|
return payment2;
|
|
979
1440
|
}
|
|
980
1441
|
const intent = await this.http.post(
|
|
@@ -1002,6 +1463,8 @@ var Playmos = class {
|
|
|
1002
1463
|
const { txHash } = await waitForCalls(provider, callsId);
|
|
1003
1464
|
const payment = await this.settle(intent.payment.id, txHash);
|
|
1004
1465
|
payment.identity = identity;
|
|
1466
|
+
payment.prizePoolAddress = prizePool.toLowerCase();
|
|
1467
|
+
payment.roundKey = roundKey;
|
|
1005
1468
|
return payment;
|
|
1006
1469
|
}
|
|
1007
1470
|
/**
|
|
@@ -1067,9 +1530,41 @@ var Playmos = class {
|
|
|
1067
1530
|
/** Verify a payment by the service's on-chain read (spec §6.1). Idempotent. */
|
|
1068
1531
|
async verify(paymentId) {
|
|
1069
1532
|
requireField(paymentId, "paymentId");
|
|
1533
|
+
if (this.config.mock) {
|
|
1534
|
+
const cached = this.mockPayments.get(paymentId);
|
|
1535
|
+
if (!cached) {
|
|
1536
|
+
throw new ApiError(`payment not found: ${paymentId}`, { status: 404, code: "not_found" });
|
|
1537
|
+
}
|
|
1538
|
+
const m = mockVerifyResult(cached);
|
|
1539
|
+
return {
|
|
1540
|
+
id: m.id,
|
|
1541
|
+
status: m.status,
|
|
1542
|
+
amount: m.amount,
|
|
1543
|
+
fee: m.fee,
|
|
1544
|
+
net: m.net,
|
|
1545
|
+
txHash: m.txHash,
|
|
1546
|
+
playerId: m.playerId ?? "",
|
|
1547
|
+
sku: m.sku,
|
|
1548
|
+
roundId: m.roundId,
|
|
1549
|
+
chain: m.chain,
|
|
1550
|
+
verifiedVia: "cache"
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1070
1553
|
return this.http.get(`/payments/${encodeURIComponent(paymentId)}`);
|
|
1071
1554
|
}
|
|
1072
1555
|
// ---- internals ---------------------------------------------------------
|
|
1556
|
+
/**
|
|
1557
|
+
* x402 V1 is Base Sepolia + test keys only (ADR D5). Fail closed before any
|
|
1558
|
+
* network call so mainnet / live keys never silently hit the adapter.
|
|
1559
|
+
*/
|
|
1560
|
+
assertX402Allowed() {
|
|
1561
|
+
if (!this.env.isTest || this.env.network !== "base-sepolia") {
|
|
1562
|
+
throw new ConfigError(
|
|
1563
|
+
'playmos.x402 is Base Sepolia / test-key only in V1 (ADR D5). Use apiKey sk_test_\u2026 and network "base-sepolia".',
|
|
1564
|
+
{ network: this.env.network, isTest: this.env.isTest }
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1073
1568
|
/**
|
|
1074
1569
|
* Map a server-settled payment (from the `settle: "server"` response) into the
|
|
1075
1570
|
* SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
|
|
@@ -1111,6 +1606,58 @@ var Playmos = class {
|
|
|
1111
1606
|
const fromServer = intent.clientParams?.amountUnits ?? intent.clientParams?.amountMicro;
|
|
1112
1607
|
return fromServer ? BigInt(fromServer) : local;
|
|
1113
1608
|
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Read PrizePool.withdrawable[wallet] via eth_call (issue #41).
|
|
1611
|
+
* Uses the wallet provider when present; otherwise a public Base Sepolia RPC in test.
|
|
1612
|
+
*/
|
|
1613
|
+
async readWithdrawable(prizePool, wallet) {
|
|
1614
|
+
const data = encodeFunctionData({
|
|
1615
|
+
abi: prizePoolAbi,
|
|
1616
|
+
functionName: "withdrawable",
|
|
1617
|
+
args: [wallet]
|
|
1618
|
+
});
|
|
1619
|
+
let raw;
|
|
1620
|
+
try {
|
|
1621
|
+
if (await walletAvailable(this.config.wallet)) {
|
|
1622
|
+
const provider = resolveProvider(this.config.wallet);
|
|
1623
|
+
raw = await provider.request({
|
|
1624
|
+
method: "eth_call",
|
|
1625
|
+
params: [{ to: prizePool, data }, "latest"]
|
|
1626
|
+
});
|
|
1627
|
+
} else {
|
|
1628
|
+
const rpc = this.env.network === "base" ? "https://mainnet.base.org" : "https://sepolia.base.org";
|
|
1629
|
+
const res = await fetch(rpc, {
|
|
1630
|
+
method: "POST",
|
|
1631
|
+
headers: { "content-type": "application/json" },
|
|
1632
|
+
body: JSON.stringify({
|
|
1633
|
+
jsonrpc: "2.0",
|
|
1634
|
+
id: 1,
|
|
1635
|
+
method: "eth_call",
|
|
1636
|
+
params: [{ to: prizePool, data }, "latest"]
|
|
1637
|
+
})
|
|
1638
|
+
});
|
|
1639
|
+
const json = await res.json();
|
|
1640
|
+
if (!json.result) {
|
|
1641
|
+
throw new ApiError(
|
|
1642
|
+
`eth_call withdrawable failed: ${json.error?.message ?? "no result"}`,
|
|
1643
|
+
{ prizePool, wallet }
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
raw = json.result;
|
|
1647
|
+
}
|
|
1648
|
+
} catch (e) {
|
|
1649
|
+
if (e instanceof ApiError) throw e;
|
|
1650
|
+
throw new ApiError(`Could not read withdrawable balance: ${e.message}`, {
|
|
1651
|
+
prizePool,
|
|
1652
|
+
wallet
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
return decodeFunctionResult({
|
|
1656
|
+
abi: prizePoolAbi,
|
|
1657
|
+
functionName: "withdrawable",
|
|
1658
|
+
data: raw
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1114
1661
|
sponsorUrl(intent) {
|
|
1115
1662
|
if (this.config.gas?.mode === "player") return void 0;
|
|
1116
1663
|
return this.config.gas?.paymasterUrl ?? intent.clientParams?.paymasterUrl;
|
|
@@ -1198,10 +1745,10 @@ var PayoutError = class extends Error {
|
|
|
1198
1745
|
this.name = "PayoutError";
|
|
1199
1746
|
}
|
|
1200
1747
|
};
|
|
1201
|
-
var
|
|
1748
|
+
var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
|
|
1202
1749
|
var BPS = 10000n;
|
|
1203
|
-
function
|
|
1204
|
-
if (!
|
|
1750
|
+
function requireAddress2(w, i) {
|
|
1751
|
+
if (!ADDRESS_RE3.test(w)) {
|
|
1205
1752
|
throw new PayoutError(`ranking[${i}] must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(w)}`);
|
|
1206
1753
|
}
|
|
1207
1754
|
return w.toLowerCase();
|
|
@@ -1224,7 +1771,7 @@ function computePayout(pool, ranking, rule) {
|
|
|
1224
1771
|
if (!Array.isArray(ranking) || ranking.length === 0) {
|
|
1225
1772
|
throw new PayoutError("ranking must be a non-empty array of wallets (best-first)");
|
|
1226
1773
|
}
|
|
1227
|
-
const wallets = ranking.map((w, i) =>
|
|
1774
|
+
const wallets = ranking.map((w, i) => requireAddress2(w, i));
|
|
1228
1775
|
const seen = /* @__PURE__ */ new Set();
|
|
1229
1776
|
for (const w of wallets) {
|
|
1230
1777
|
if (seen.has(w)) throw new PayoutError(`duplicate wallet in ranking: ${w}`);
|
|
@@ -1297,13 +1844,13 @@ function computePayout(pool, ranking, rule) {
|
|
|
1297
1844
|
}
|
|
1298
1845
|
|
|
1299
1846
|
// src/settlement.ts
|
|
1300
|
-
var
|
|
1847
|
+
var ADDRESS_RE4 = /^0x[0-9a-fA-F]{40}$/;
|
|
1301
1848
|
var DEFAULT_TTL_MS = 15 * 60 * 1e3;
|
|
1302
|
-
function
|
|
1849
|
+
function requireAddress3(value, field) {
|
|
1303
1850
|
if (typeof value !== "string" || value.trim() === "") {
|
|
1304
1851
|
throw new MissingFieldError(field);
|
|
1305
1852
|
}
|
|
1306
|
-
if (!
|
|
1853
|
+
if (!ADDRESS_RE4.test(value)) {
|
|
1307
1854
|
throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
|
|
1308
1855
|
field,
|
|
1309
1856
|
value
|
|
@@ -1320,7 +1867,7 @@ function requireNetwork(value) {
|
|
|
1320
1867
|
}
|
|
1321
1868
|
function createPaymentRequirement(input) {
|
|
1322
1869
|
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
1323
|
-
const payTo =
|
|
1870
|
+
const payTo = requireAddress3(input.payTo, "payTo");
|
|
1324
1871
|
const network = requireNetwork(input.network);
|
|
1325
1872
|
const asset = input.asset ?? "USDC";
|
|
1326
1873
|
if (asset !== "USDC") {
|
|
@@ -1363,7 +1910,7 @@ function parsePaymentRequirement(input) {
|
|
|
1363
1910
|
asset: o.asset
|
|
1364
1911
|
});
|
|
1365
1912
|
}
|
|
1366
|
-
const payTo =
|
|
1913
|
+
const payTo = requireAddress3(o.payTo, "payTo");
|
|
1367
1914
|
const network = requireNetwork(o.network);
|
|
1368
1915
|
if (typeof o.amount !== "string") throw new InvalidAmountError(o.amount);
|
|
1369
1916
|
parseUsdToMicro(o.amount);
|
|
@@ -1389,6 +1936,6 @@ function isX402PayloadAuthorization(auth) {
|
|
|
1389
1936
|
return auth.kind === "x402-payload";
|
|
1390
1937
|
}
|
|
1391
1938
|
|
|
1392
|
-
export { CHAIN_ID, DEFAULT_API_BASE_URL, MICRO_PER_USDC, PayoutError, Playmos, USDC_ADDRESS, USDC_DECIMALS, computeIapSplit, computePayout, computePoolSplit, createPaymentRequirement, formatMicroToUsd, isWalletSignatureAuthorization, isX402PayloadAuthorization, parsePaymentRequirement, parseUsdToMicro, prefixedId, previewEscrowFee, previewIapSplit, previewMarketplaceSplit, previewPoolSplit, previewTransferSplit, serializePaymentRequirement, ulid };
|
|
1939
|
+
export { CHAIN_ID, DEFAULT_API_BASE_URL, MICRO_PER_USDC, PayoutError, Playmos, USDC_ADDRESS, USDC_DECIMALS, computeIapSplit, computePayout, computePoolSplit, createPaymentRequirement, createX402Challenge, decodePaymentHeader, encodePaymentHeader, formatMicroToUsd, isWalletSignatureAuthorization, isX402PayloadAuthorization, networkToCaip2, parsePaymentRequirement, parseUsdToMicro, prefixedId, previewEscrowFee, previewIapSplit, previewMarketplaceSplit, previewPoolSplit, previewTransferSplit, serializePaymentRequirement, toX402PaymentRequired, ulid, validateX402ChallengeInput };
|
|
1393
1940
|
//# sourceMappingURL=index.js.map
|
|
1394
1941
|
//# sourceMappingURL=index.js.map
|