@playmos/sdk 0.3.1 → 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/index.cjs +414 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +379 -129
- package/dist/index.d.ts +379 -129
- package/dist/index.js +410 -44
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
*
|
|
@@ -944,6 +1252,11 @@ var Playmos = class {
|
|
|
944
1252
|
return this.http.get(`/listings/${encodeURIComponent(listingId)}`);
|
|
945
1253
|
}
|
|
946
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();
|
|
947
1260
|
/**
|
|
948
1261
|
* `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
|
|
949
1262
|
* `get` reconciles once against chain truth; `wait` polls it to a terminal state
|
|
@@ -993,6 +1306,9 @@ var Playmos = class {
|
|
|
993
1306
|
}
|
|
994
1307
|
/** Connect the player's wallet and return their address. */
|
|
995
1308
|
async connect() {
|
|
1309
|
+
if (this.config.mock) {
|
|
1310
|
+
return "0x0000000000000000000000000000000000000001";
|
|
1311
|
+
}
|
|
996
1312
|
return getAccount(resolveProvider(this.config.wallet));
|
|
997
1313
|
}
|
|
998
1314
|
/**
|
|
@@ -1009,18 +1325,29 @@ var Playmos = class {
|
|
|
1009
1325
|
payEntry: async (req) => {
|
|
1010
1326
|
const entry = await this.enterRound({
|
|
1011
1327
|
gameId: cfg.gameId,
|
|
1328
|
+
// Studio roundId for bookkeeping; pin roundKey separately for hasEntered.
|
|
1012
1329
|
roundId: req.roundKey,
|
|
1013
|
-
// the round the server verifies against
|
|
1014
1330
|
roundKey: req.roundKey,
|
|
1015
1331
|
identity: req.identity,
|
|
1016
1332
|
amount: formatMicroToUsd(req.entryUnits),
|
|
1017
1333
|
playerId: req.wallet
|
|
1018
1334
|
});
|
|
1019
1335
|
const status = entry.status === "confirmed" ? "CONFIRMED" : entry.status === "failed" ? "FAILED" : "PENDING";
|
|
1020
|
-
|
|
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
|
+
};
|
|
1021
1344
|
}
|
|
1022
1345
|
};
|
|
1023
1346
|
}
|
|
1347
|
+
rememberMock(payment) {
|
|
1348
|
+
if (payment.mock) this.mockPayments.set(payment.id, payment);
|
|
1349
|
+
return payment;
|
|
1350
|
+
}
|
|
1024
1351
|
/** External-studio IAP (1%). Returns a real confirmed Payment with a txHash. */
|
|
1025
1352
|
async pay(input) {
|
|
1026
1353
|
const amountMicro = validateAmount(input.amount);
|
|
@@ -1029,14 +1356,16 @@ var Playmos = class {
|
|
|
1029
1356
|
const metadata = validateMetadata(input.metadata);
|
|
1030
1357
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
1031
1358
|
if (this.config.mock) {
|
|
1032
|
-
return
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
+
);
|
|
1040
1369
|
}
|
|
1041
1370
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1042
1371
|
const res = await this.http.post(
|
|
@@ -1079,17 +1408,22 @@ var Playmos = class {
|
|
|
1079
1408
|
const metadata = validateMetadata(input.metadata);
|
|
1080
1409
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
1081
1410
|
if (this.config.mock) {
|
|
1082
|
-
return
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
+
);
|
|
1093
1427
|
}
|
|
1094
1428
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1095
1429
|
const res = await this.http.post(
|
|
@@ -1101,7 +1435,7 @@ var Playmos = class {
|
|
|
1101
1435
|
if (input.identity) payment2.identity = input.identity;
|
|
1102
1436
|
const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
1103
1437
|
if (pool) payment2.prizePoolAddress = pool.toLowerCase();
|
|
1104
|
-
payment2.roundKey = res.clientParams?.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
|
|
1438
|
+
payment2.roundKey = res.clientParams?.roundKey ?? input.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
|
|
1105
1439
|
return payment2;
|
|
1106
1440
|
}
|
|
1107
1441
|
const intent = await this.http.post(
|
|
@@ -1196,9 +1530,41 @@ var Playmos = class {
|
|
|
1196
1530
|
/** Verify a payment by the service's on-chain read (spec §6.1). Idempotent. */
|
|
1197
1531
|
async verify(paymentId) {
|
|
1198
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
|
+
}
|
|
1199
1553
|
return this.http.get(`/payments/${encodeURIComponent(paymentId)}`);
|
|
1200
1554
|
}
|
|
1201
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
|
+
}
|
|
1202
1568
|
/**
|
|
1203
1569
|
* Map a server-settled payment (from the `settle: "server"` response) into the
|
|
1204
1570
|
* SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
|
|
@@ -1379,10 +1745,10 @@ var PayoutError = class extends Error {
|
|
|
1379
1745
|
this.name = "PayoutError";
|
|
1380
1746
|
}
|
|
1381
1747
|
};
|
|
1382
|
-
var
|
|
1748
|
+
var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
|
|
1383
1749
|
var BPS = 10000n;
|
|
1384
|
-
function
|
|
1385
|
-
if (!
|
|
1750
|
+
function requireAddress2(w, i) {
|
|
1751
|
+
if (!ADDRESS_RE3.test(w)) {
|
|
1386
1752
|
throw new PayoutError(`ranking[${i}] must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(w)}`);
|
|
1387
1753
|
}
|
|
1388
1754
|
return w.toLowerCase();
|
|
@@ -1405,7 +1771,7 @@ function computePayout(pool, ranking, rule) {
|
|
|
1405
1771
|
if (!Array.isArray(ranking) || ranking.length === 0) {
|
|
1406
1772
|
throw new PayoutError("ranking must be a non-empty array of wallets (best-first)");
|
|
1407
1773
|
}
|
|
1408
|
-
const wallets = ranking.map((w, i) =>
|
|
1774
|
+
const wallets = ranking.map((w, i) => requireAddress2(w, i));
|
|
1409
1775
|
const seen = /* @__PURE__ */ new Set();
|
|
1410
1776
|
for (const w of wallets) {
|
|
1411
1777
|
if (seen.has(w)) throw new PayoutError(`duplicate wallet in ranking: ${w}`);
|
|
@@ -1478,13 +1844,13 @@ function computePayout(pool, ranking, rule) {
|
|
|
1478
1844
|
}
|
|
1479
1845
|
|
|
1480
1846
|
// src/settlement.ts
|
|
1481
|
-
var
|
|
1847
|
+
var ADDRESS_RE4 = /^0x[0-9a-fA-F]{40}$/;
|
|
1482
1848
|
var DEFAULT_TTL_MS = 15 * 60 * 1e3;
|
|
1483
|
-
function
|
|
1849
|
+
function requireAddress3(value, field) {
|
|
1484
1850
|
if (typeof value !== "string" || value.trim() === "") {
|
|
1485
1851
|
throw new MissingFieldError(field);
|
|
1486
1852
|
}
|
|
1487
|
-
if (!
|
|
1853
|
+
if (!ADDRESS_RE4.test(value)) {
|
|
1488
1854
|
throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
|
|
1489
1855
|
field,
|
|
1490
1856
|
value
|
|
@@ -1501,7 +1867,7 @@ function requireNetwork(value) {
|
|
|
1501
1867
|
}
|
|
1502
1868
|
function createPaymentRequirement(input) {
|
|
1503
1869
|
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
1504
|
-
const payTo =
|
|
1870
|
+
const payTo = requireAddress3(input.payTo, "payTo");
|
|
1505
1871
|
const network = requireNetwork(input.network);
|
|
1506
1872
|
const asset = input.asset ?? "USDC";
|
|
1507
1873
|
if (asset !== "USDC") {
|
|
@@ -1544,7 +1910,7 @@ function parsePaymentRequirement(input) {
|
|
|
1544
1910
|
asset: o.asset
|
|
1545
1911
|
});
|
|
1546
1912
|
}
|
|
1547
|
-
const payTo =
|
|
1913
|
+
const payTo = requireAddress3(o.payTo, "payTo");
|
|
1548
1914
|
const network = requireNetwork(o.network);
|
|
1549
1915
|
if (typeof o.amount !== "string") throw new InvalidAmountError(o.amount);
|
|
1550
1916
|
parseUsdToMicro(o.amount);
|
|
@@ -1570,6 +1936,6 @@ function isX402PayloadAuthorization(auth) {
|
|
|
1570
1936
|
return auth.kind === "x402-payload";
|
|
1571
1937
|
}
|
|
1572
1938
|
|
|
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 };
|
|
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 };
|
|
1574
1940
|
//# sourceMappingURL=index.js.map
|
|
1575
1941
|
//# sourceMappingURL=index.js.map
|