@playmos/sdk 0.3.1 → 0.3.3
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/index.cjs +521 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +383 -130
- package/dist/index.d.ts +383 -130
- package/dist/index.js +517 -45
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InvalidAmountError, ConfigError, NothingToWithdrawError, PaymentFailedError, ApiError,
|
|
1
|
+
import { InvalidAmountError, ConfigError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, ApiError, WalletConnectionError, InsufficientGasError, AuthError } from './chunk-TMBBEGIF.js';
|
|
2
2
|
export { ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError } from './chunk-TMBBEGIF.js';
|
|
3
3
|
import { encodeFunctionData, decodeFunctionResult, numberToHex, keccak256, toBytes } from 'viem';
|
|
4
4
|
|
|
@@ -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}`, {
|
|
@@ -462,7 +487,23 @@ async function waitForCalls(provider, id, timeoutMs = 6e4) {
|
|
|
462
487
|
function encodeApprove(spender, amountUnits) {
|
|
463
488
|
return encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [spender, amountUnits] });
|
|
464
489
|
}
|
|
465
|
-
|
|
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
|
+
}
|
|
466
507
|
let balance;
|
|
467
508
|
try {
|
|
468
509
|
const hex = await provider.request({ method: "eth_getBalance", params: [from, "latest"] });
|
|
@@ -470,8 +511,11 @@ async function assertEnoughGas(provider, from, minWei = 200000000000000n) {
|
|
|
470
511
|
} catch {
|
|
471
512
|
return;
|
|
472
513
|
}
|
|
473
|
-
if (balance <
|
|
474
|
-
throw new InsufficientGasError({
|
|
514
|
+
if (balance < floor) {
|
|
515
|
+
throw new InsufficientGasError({
|
|
516
|
+
balanceWei: balance.toString(),
|
|
517
|
+
minWei: floor.toString()
|
|
518
|
+
});
|
|
475
519
|
}
|
|
476
520
|
}
|
|
477
521
|
var toBytes32 = (s) => keccak256(toBytes(s));
|
|
@@ -533,6 +577,7 @@ function mockEntryPayment(args) {
|
|
|
533
577
|
args.seedBps,
|
|
534
578
|
args.rakeBps
|
|
535
579
|
);
|
|
580
|
+
const roundKey = args.roundKey && args.roundKey.trim() !== "" ? args.roundKey : args.roundId;
|
|
536
581
|
return {
|
|
537
582
|
id: prefixedId("entry"),
|
|
538
583
|
status: "confirmed",
|
|
@@ -549,7 +594,8 @@ function mockEntryPayment(args) {
|
|
|
549
594
|
},
|
|
550
595
|
gameId: args.gameId,
|
|
551
596
|
roundId: args.roundId,
|
|
552
|
-
roundKey
|
|
597
|
+
roundKey,
|
|
598
|
+
identity: args.identity,
|
|
553
599
|
// Deterministic mock pool address so claim-flow unit tests can assert shape (#41).
|
|
554
600
|
prizePoolAddress: "0x0000000000000000000000000000000000000001",
|
|
555
601
|
playerId: args.playerId,
|
|
@@ -560,14 +606,160 @@ function mockEntryPayment(args) {
|
|
|
560
606
|
mock: true
|
|
561
607
|
};
|
|
562
608
|
}
|
|
609
|
+
function mockVerifyResult(payment) {
|
|
610
|
+
return { ...payment, mock: true };
|
|
611
|
+
}
|
|
563
612
|
|
|
564
|
-
// src/
|
|
613
|
+
// src/x402.ts
|
|
565
614
|
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
566
|
-
function
|
|
615
|
+
function requireAddress(value, field) {
|
|
567
616
|
if (typeof value !== "string" || value.trim() === "") {
|
|
568
617
|
throw new MissingFieldError(field);
|
|
569
618
|
}
|
|
570
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)) {
|
|
571
763
|
throw new ConfigError(
|
|
572
764
|
`${field} must be a 0x-prefixed 20-byte wallet address (Phase 1a transfers settle server-held wallets), got: ${JSON.stringify(value)}`,
|
|
573
765
|
{ field, value }
|
|
@@ -612,6 +804,122 @@ var Playmos = class {
|
|
|
612
804
|
/** Create a Bridge KYC onboarding link (fiat payout). */
|
|
613
805
|
createOnboardingLink: () => this.http.post("/payouts/onboarding_link", {})
|
|
614
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
|
+
};
|
|
615
923
|
/**
|
|
616
924
|
* Skill/contest **round lifecycle** (issue #13) — closes the enterRound loop.
|
|
617
925
|
*
|
|
@@ -621,6 +929,8 @@ var Playmos = class {
|
|
|
621
929
|
*
|
|
622
930
|
* Operator txs are signed by the Playmos service (OPERATOR_ROLE key).
|
|
623
931
|
*/
|
|
932
|
+
/** Offline mock store for rounds.open/lock/settle/get when `mock: true` (3-game dogfood). */
|
|
933
|
+
this.mockRounds = /* @__PURE__ */ new Map();
|
|
624
934
|
this.rounds = {
|
|
625
935
|
open: async (input) => {
|
|
626
936
|
requireField(input?.gameId, "gameId");
|
|
@@ -630,6 +940,34 @@ var Playmos = class {
|
|
|
630
940
|
throw new ConfigError("payout rule is required (winner-take-all | top-n | custom)");
|
|
631
941
|
}
|
|
632
942
|
validateAmount(input.entryAmount);
|
|
943
|
+
if (this.config.mock) {
|
|
944
|
+
const roundKey = input.roundKey ?? input.roundId;
|
|
945
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
946
|
+
if (existing?.status === "open" || existing?.status === "locked") {
|
|
947
|
+
return existing;
|
|
948
|
+
}
|
|
949
|
+
if (existing?.status === "settled") {
|
|
950
|
+
throw new ApiError(`round ${input.roundId} already settled`, {
|
|
951
|
+
status: 409,
|
|
952
|
+
code: "conflict"
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
const round2 = {
|
|
956
|
+
roundId: input.roundId,
|
|
957
|
+
gameId: input.gameId,
|
|
958
|
+
roundKey,
|
|
959
|
+
status: "open",
|
|
960
|
+
entryAmount: input.entryAmount,
|
|
961
|
+
pool: null,
|
|
962
|
+
entrants: 0,
|
|
963
|
+
payout: input.payout,
|
|
964
|
+
closeAt: input.closeAt,
|
|
965
|
+
openTxHash: "0x000000000000000000000000000000000000000000000000000000000000mock",
|
|
966
|
+
prizePoolAddress: this.config.contracts?.prizePool ?? "0x0000000000000000000000000000000000000002"
|
|
967
|
+
};
|
|
968
|
+
this.mockRounds.set(input.roundId, round2);
|
|
969
|
+
return round2;
|
|
970
|
+
}
|
|
633
971
|
const { round } = await this.http.post("/rounds", {
|
|
634
972
|
gameId: input.gameId,
|
|
635
973
|
roundId: input.roundId,
|
|
@@ -643,6 +981,29 @@ var Playmos = class {
|
|
|
643
981
|
},
|
|
644
982
|
lock: async (input) => {
|
|
645
983
|
requireField(input?.roundId, "roundId");
|
|
984
|
+
if (this.config.mock) {
|
|
985
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
986
|
+
if (!existing) {
|
|
987
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
988
|
+
}
|
|
989
|
+
if (existing.status === "locked" || existing.status === "settled") return existing;
|
|
990
|
+
if (existing.status !== "open") {
|
|
991
|
+
throw new ApiError(`round ${input.roundId} is not open (status=${existing.status})`, {
|
|
992
|
+
status: 409,
|
|
993
|
+
code: "conflict"
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
const locked = {
|
|
997
|
+
...existing,
|
|
998
|
+
status: "locked",
|
|
999
|
+
// Mock payable pool ≈ 60% of entry × max(1, entrants) for shape only — not money truth.
|
|
1000
|
+
pool: existing.pool ?? existing.entryAmount,
|
|
1001
|
+
entrants: existing.entrants ?? 0,
|
|
1002
|
+
lockTxHash: "0x000000000000000000000000000000000000000000000000000000000000loc1"
|
|
1003
|
+
};
|
|
1004
|
+
this.mockRounds.set(input.roundId, locked);
|
|
1005
|
+
return locked;
|
|
1006
|
+
}
|
|
646
1007
|
const { round } = await this.http.post(
|
|
647
1008
|
`/rounds/${encodeURIComponent(input.roundId)}/lock`,
|
|
648
1009
|
{ gameId: input.gameId }
|
|
@@ -652,6 +1013,51 @@ var Playmos = class {
|
|
|
652
1013
|
settle: async (input) => {
|
|
653
1014
|
requireField(input?.roundId, "roundId");
|
|
654
1015
|
if (!input?.results) throw new ConfigError("results are required (ranking or winners)");
|
|
1016
|
+
if (this.config.mock) {
|
|
1017
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1018
|
+
if (!existing) {
|
|
1019
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1020
|
+
}
|
|
1021
|
+
if (existing.status === "settled" && existing.settleTxHash) {
|
|
1022
|
+
return {
|
|
1023
|
+
roundId: input.roundId,
|
|
1024
|
+
txHash: existing.settleTxHash,
|
|
1025
|
+
poolPaid: existing.pool ?? "0",
|
|
1026
|
+
winners: "winners" in input.results ? input.results.winners : input.results.ranking.map((wallet) => ({
|
|
1027
|
+
wallet,
|
|
1028
|
+
amount: existing.pool ?? existing.entryAmount
|
|
1029
|
+
})),
|
|
1030
|
+
status: "settled"
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
if (existing.status !== "locked" && existing.status !== "open") {
|
|
1034
|
+
throw new ApiError(`round ${input.roundId} cannot settle (status=${existing.status})`, {
|
|
1035
|
+
status: 409,
|
|
1036
|
+
code: "conflict"
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
const winners = "winners" in input.results ? input.results.winners : input.results.ranking.map((wallet, i) => ({
|
|
1040
|
+
wallet,
|
|
1041
|
+
// Winner-take-all mock shape when ranking only.
|
|
1042
|
+
amount: i === 0 ? existing.pool ?? existing.entryAmount : "0"
|
|
1043
|
+
}));
|
|
1044
|
+
const settleTxHash = "0x000000000000000000000000000000000000000000000000000000000000set1";
|
|
1045
|
+
const poolPaid = existing.pool ?? existing.entryAmount;
|
|
1046
|
+
const settled = {
|
|
1047
|
+
...existing,
|
|
1048
|
+
status: "settled",
|
|
1049
|
+
settleTxHash,
|
|
1050
|
+
pool: poolPaid
|
|
1051
|
+
};
|
|
1052
|
+
this.mockRounds.set(input.roundId, settled);
|
|
1053
|
+
return {
|
|
1054
|
+
roundId: input.roundId,
|
|
1055
|
+
txHash: settleTxHash,
|
|
1056
|
+
poolPaid,
|
|
1057
|
+
winners,
|
|
1058
|
+
status: "settled"
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
655
1061
|
const { settle } = await this.http.post(
|
|
656
1062
|
`/rounds/${encodeURIComponent(input.roundId)}/settle`,
|
|
657
1063
|
{ gameId: input.gameId, results: input.results }
|
|
@@ -660,6 +1066,13 @@ var Playmos = class {
|
|
|
660
1066
|
},
|
|
661
1067
|
get: async (input) => {
|
|
662
1068
|
requireField(input?.roundId, "roundId");
|
|
1069
|
+
if (this.config.mock) {
|
|
1070
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1071
|
+
if (!existing) {
|
|
1072
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1073
|
+
}
|
|
1074
|
+
return existing;
|
|
1075
|
+
}
|
|
663
1076
|
const { round } = await this.http.get(
|
|
664
1077
|
`/rounds/${encodeURIComponent(input.roundId)}`
|
|
665
1078
|
);
|
|
@@ -944,6 +1357,11 @@ var Playmos = class {
|
|
|
944
1357
|
return this.http.get(`/listings/${encodeURIComponent(listingId)}`);
|
|
945
1358
|
}
|
|
946
1359
|
};
|
|
1360
|
+
/**
|
|
1361
|
+
* Offline mock payments for this client instance — so `verify(id)` after
|
|
1362
|
+
* `mock: true` pay/enterRound does not hit the live API (#204).
|
|
1363
|
+
*/
|
|
1364
|
+
this.mockPayments = /* @__PURE__ */ new Map();
|
|
947
1365
|
/**
|
|
948
1366
|
* `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
|
|
949
1367
|
* `get` reconciles once against chain truth; `wait` polls it to a terminal state
|
|
@@ -993,6 +1411,9 @@ var Playmos = class {
|
|
|
993
1411
|
}
|
|
994
1412
|
/** Connect the player's wallet and return their address. */
|
|
995
1413
|
async connect() {
|
|
1414
|
+
if (this.config.mock) {
|
|
1415
|
+
return "0x0000000000000000000000000000000000000001";
|
|
1416
|
+
}
|
|
996
1417
|
return getAccount(resolveProvider(this.config.wallet));
|
|
997
1418
|
}
|
|
998
1419
|
/**
|
|
@@ -1009,18 +1430,30 @@ var Playmos = class {
|
|
|
1009
1430
|
payEntry: async (req) => {
|
|
1010
1431
|
const entry = await this.enterRound({
|
|
1011
1432
|
gameId: cfg.gameId,
|
|
1433
|
+
// Studio roundId for bookkeeping; pin roundKey separately for hasEntered.
|
|
1012
1434
|
roundId: req.roundKey,
|
|
1013
|
-
// the round the server verifies against
|
|
1014
1435
|
roundKey: req.roundKey,
|
|
1015
1436
|
identity: req.identity,
|
|
1016
1437
|
amount: formatMicroToUsd(req.entryUnits),
|
|
1017
1438
|
playerId: req.wallet
|
|
1018
1439
|
});
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1440
|
+
const raw = String(entry.status ?? "pending").toLowerCase();
|
|
1441
|
+
const status = raw === "confirmed" ? "confirmed" : raw === "failed" ? "failed" : "pending";
|
|
1442
|
+
const onchain = !entry.mock;
|
|
1443
|
+
return {
|
|
1444
|
+
paymentId: entry.id,
|
|
1445
|
+
status,
|
|
1446
|
+
...entry.txHash ? { txHash: entry.txHash } : {},
|
|
1447
|
+
onchain,
|
|
1448
|
+
identity: entry.identity ?? req.identity
|
|
1449
|
+
};
|
|
1021
1450
|
}
|
|
1022
1451
|
};
|
|
1023
1452
|
}
|
|
1453
|
+
rememberMock(payment) {
|
|
1454
|
+
if (payment.mock) this.mockPayments.set(payment.id, payment);
|
|
1455
|
+
return payment;
|
|
1456
|
+
}
|
|
1024
1457
|
/** External-studio IAP (1%). Returns a real confirmed Payment with a txHash. */
|
|
1025
1458
|
async pay(input) {
|
|
1026
1459
|
const amountMicro = validateAmount(input.amount);
|
|
@@ -1029,14 +1462,16 @@ var Playmos = class {
|
|
|
1029
1462
|
const metadata = validateMetadata(input.metadata);
|
|
1030
1463
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
1031
1464
|
if (this.config.mock) {
|
|
1032
|
-
return
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1465
|
+
return this.rememberMock(
|
|
1466
|
+
mockIapPayment({
|
|
1467
|
+
amountMicro,
|
|
1468
|
+
feeBps: IAP_FEE_BPS,
|
|
1469
|
+
sku: input.sku,
|
|
1470
|
+
playerId: input.playerId,
|
|
1471
|
+
chain: this.env.network,
|
|
1472
|
+
metadata
|
|
1473
|
+
})
|
|
1474
|
+
);
|
|
1040
1475
|
}
|
|
1041
1476
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1042
1477
|
const res = await this.http.post(
|
|
@@ -1079,17 +1514,22 @@ var Playmos = class {
|
|
|
1079
1514
|
const metadata = validateMetadata(input.metadata);
|
|
1080
1515
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
1081
1516
|
if (this.config.mock) {
|
|
1082
|
-
return
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1517
|
+
return this.rememberMock(
|
|
1518
|
+
mockEntryPayment({
|
|
1519
|
+
amountMicro,
|
|
1520
|
+
poolBps: POOL_BPS,
|
|
1521
|
+
seedBps: SEED_BPS,
|
|
1522
|
+
rakeBps: RAKE_BPS,
|
|
1523
|
+
gameId: input.gameId,
|
|
1524
|
+
roundId: input.roundId,
|
|
1525
|
+
playerId: input.playerId,
|
|
1526
|
+
chain: this.env.network,
|
|
1527
|
+
metadata,
|
|
1528
|
+
// B2 pins — must survive mock path for hub hasEntered parity (#204).
|
|
1529
|
+
roundKey: input.roundKey,
|
|
1530
|
+
identity: input.identity
|
|
1531
|
+
})
|
|
1532
|
+
);
|
|
1093
1533
|
}
|
|
1094
1534
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1095
1535
|
const res = await this.http.post(
|
|
@@ -1101,7 +1541,7 @@ var Playmos = class {
|
|
|
1101
1541
|
if (input.identity) payment2.identity = input.identity;
|
|
1102
1542
|
const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
1103
1543
|
if (pool) payment2.prizePoolAddress = pool.toLowerCase();
|
|
1104
|
-
payment2.roundKey = res.clientParams?.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
|
|
1544
|
+
payment2.roundKey = res.clientParams?.roundKey ?? input.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
|
|
1105
1545
|
return payment2;
|
|
1106
1546
|
}
|
|
1107
1547
|
const intent = await this.http.post(
|
|
@@ -1196,9 +1636,41 @@ var Playmos = class {
|
|
|
1196
1636
|
/** Verify a payment by the service's on-chain read (spec §6.1). Idempotent. */
|
|
1197
1637
|
async verify(paymentId) {
|
|
1198
1638
|
requireField(paymentId, "paymentId");
|
|
1639
|
+
if (this.config.mock) {
|
|
1640
|
+
const cached = this.mockPayments.get(paymentId);
|
|
1641
|
+
if (!cached) {
|
|
1642
|
+
throw new ApiError(`payment not found: ${paymentId}`, { status: 404, code: "not_found" });
|
|
1643
|
+
}
|
|
1644
|
+
const m = mockVerifyResult(cached);
|
|
1645
|
+
return {
|
|
1646
|
+
id: m.id,
|
|
1647
|
+
status: m.status,
|
|
1648
|
+
amount: m.amount,
|
|
1649
|
+
fee: m.fee,
|
|
1650
|
+
net: m.net,
|
|
1651
|
+
txHash: m.txHash,
|
|
1652
|
+
playerId: m.playerId ?? "",
|
|
1653
|
+
sku: m.sku,
|
|
1654
|
+
roundId: m.roundId,
|
|
1655
|
+
chain: m.chain,
|
|
1656
|
+
verifiedVia: "cache"
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1199
1659
|
return this.http.get(`/payments/${encodeURIComponent(paymentId)}`);
|
|
1200
1660
|
}
|
|
1201
1661
|
// ---- internals ---------------------------------------------------------
|
|
1662
|
+
/**
|
|
1663
|
+
* x402 V1 is Base Sepolia + test keys only (ADR D5). Fail closed before any
|
|
1664
|
+
* network call so mainnet / live keys never silently hit the adapter.
|
|
1665
|
+
*/
|
|
1666
|
+
assertX402Allowed() {
|
|
1667
|
+
if (!this.env.isTest || this.env.network !== "base-sepolia") {
|
|
1668
|
+
throw new ConfigError(
|
|
1669
|
+
'playmos.x402 is Base Sepolia / test-key only in V1 (ADR D5). Use apiKey sk_test_\u2026 and network "base-sepolia".',
|
|
1670
|
+
{ network: this.env.network, isTest: this.env.isTest }
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1202
1674
|
/**
|
|
1203
1675
|
* Map a server-settled payment (from the `settle: "server"` response) into the
|
|
1204
1676
|
* SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
|
|
@@ -1379,10 +1851,10 @@ var PayoutError = class extends Error {
|
|
|
1379
1851
|
this.name = "PayoutError";
|
|
1380
1852
|
}
|
|
1381
1853
|
};
|
|
1382
|
-
var
|
|
1854
|
+
var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
|
|
1383
1855
|
var BPS = 10000n;
|
|
1384
|
-
function
|
|
1385
|
-
if (!
|
|
1856
|
+
function requireAddress2(w, i) {
|
|
1857
|
+
if (!ADDRESS_RE3.test(w)) {
|
|
1386
1858
|
throw new PayoutError(`ranking[${i}] must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(w)}`);
|
|
1387
1859
|
}
|
|
1388
1860
|
return w.toLowerCase();
|
|
@@ -1405,7 +1877,7 @@ function computePayout(pool, ranking, rule) {
|
|
|
1405
1877
|
if (!Array.isArray(ranking) || ranking.length === 0) {
|
|
1406
1878
|
throw new PayoutError("ranking must be a non-empty array of wallets (best-first)");
|
|
1407
1879
|
}
|
|
1408
|
-
const wallets = ranking.map((w, i) =>
|
|
1880
|
+
const wallets = ranking.map((w, i) => requireAddress2(w, i));
|
|
1409
1881
|
const seen = /* @__PURE__ */ new Set();
|
|
1410
1882
|
for (const w of wallets) {
|
|
1411
1883
|
if (seen.has(w)) throw new PayoutError(`duplicate wallet in ranking: ${w}`);
|
|
@@ -1478,13 +1950,13 @@ function computePayout(pool, ranking, rule) {
|
|
|
1478
1950
|
}
|
|
1479
1951
|
|
|
1480
1952
|
// src/settlement.ts
|
|
1481
|
-
var
|
|
1953
|
+
var ADDRESS_RE4 = /^0x[0-9a-fA-F]{40}$/;
|
|
1482
1954
|
var DEFAULT_TTL_MS = 15 * 60 * 1e3;
|
|
1483
|
-
function
|
|
1955
|
+
function requireAddress3(value, field) {
|
|
1484
1956
|
if (typeof value !== "string" || value.trim() === "") {
|
|
1485
1957
|
throw new MissingFieldError(field);
|
|
1486
1958
|
}
|
|
1487
|
-
if (!
|
|
1959
|
+
if (!ADDRESS_RE4.test(value)) {
|
|
1488
1960
|
throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
|
|
1489
1961
|
field,
|
|
1490
1962
|
value
|
|
@@ -1501,7 +1973,7 @@ function requireNetwork(value) {
|
|
|
1501
1973
|
}
|
|
1502
1974
|
function createPaymentRequirement(input) {
|
|
1503
1975
|
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
1504
|
-
const payTo =
|
|
1976
|
+
const payTo = requireAddress3(input.payTo, "payTo");
|
|
1505
1977
|
const network = requireNetwork(input.network);
|
|
1506
1978
|
const asset = input.asset ?? "USDC";
|
|
1507
1979
|
if (asset !== "USDC") {
|
|
@@ -1544,7 +2016,7 @@ function parsePaymentRequirement(input) {
|
|
|
1544
2016
|
asset: o.asset
|
|
1545
2017
|
});
|
|
1546
2018
|
}
|
|
1547
|
-
const payTo =
|
|
2019
|
+
const payTo = requireAddress3(o.payTo, "payTo");
|
|
1548
2020
|
const network = requireNetwork(o.network);
|
|
1549
2021
|
if (typeof o.amount !== "string") throw new InvalidAmountError(o.amount);
|
|
1550
2022
|
parseUsdToMicro(o.amount);
|
|
@@ -1570,6 +2042,6 @@ function isX402PayloadAuthorization(auth) {
|
|
|
1570
2042
|
return auth.kind === "x402-payload";
|
|
1571
2043
|
}
|
|
1572
2044
|
|
|
1573
|
-
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 };
|
|
2045
|
+
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 };
|
|
1574
2046
|
//# sourceMappingURL=index.js.map
|
|
1575
2047
|
//# sourceMappingURL=index.js.map
|