@playmos/sdk 0.1.6 → 0.3.0
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 +146 -0
- package/dist/errors-BVESr920.d.cts +552 -0
- package/dist/errors-BVESr920.d.ts +552 -0
- package/dist/index.cjs +682 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +335 -17
- package/dist/index.d.ts +335 -17
- package/dist/index.js +674 -19
- package/dist/index.js.map +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/package.json +3 -3
- package/dist/errors-B-85VYMv.d.cts +0 -224
- package/dist/errors-B-85VYMv.d.ts +0 -224
package/dist/index.cjs
CHANGED
|
@@ -222,8 +222,37 @@ function validateMetadata(metadata) {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// src/http.ts
|
|
225
|
-
function
|
|
225
|
+
function resolveRetry(retry) {
|
|
226
|
+
const off = { maxRetries: 0, baseDelayMs: 500, maxDelayMs: 2e4 };
|
|
227
|
+
if (retry === false) return off;
|
|
228
|
+
const r = retry === true || retry === void 0 ? {} : retry;
|
|
229
|
+
return {
|
|
230
|
+
maxRetries: r.maxRetries ?? 2,
|
|
231
|
+
baseDelayMs: r.baseDelayMs ?? 500,
|
|
232
|
+
maxDelayMs: r.maxDelayMs ?? 2e4
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
236
|
+
function backoffMs(res, attempt, cfg) {
|
|
237
|
+
const header = res.headers.get("retry-after");
|
|
238
|
+
if (header) {
|
|
239
|
+
const secs = Number(header);
|
|
240
|
+
let ms;
|
|
241
|
+
if (Number.isFinite(secs)) {
|
|
242
|
+
ms = secs * 1e3;
|
|
243
|
+
} else {
|
|
244
|
+
const at = Date.parse(header);
|
|
245
|
+
ms = Number.isNaN(at) ? NaN : at - Date.now();
|
|
246
|
+
}
|
|
247
|
+
if (Number.isFinite(ms) && ms >= 0) return Math.min(ms, cfg.maxDelayMs);
|
|
248
|
+
}
|
|
249
|
+
const expo = cfg.baseDelayMs * 2 ** attempt;
|
|
250
|
+
const jitter = Math.random() * cfg.baseDelayMs;
|
|
251
|
+
return Math.min(expo + jitter, cfg.maxDelayMs);
|
|
252
|
+
}
|
|
253
|
+
function createHttpClient(baseUrl, apiKey, retry) {
|
|
226
254
|
const base = baseUrl.replace(/\/+$/, "");
|
|
255
|
+
const cfg = resolveRetry(retry);
|
|
227
256
|
async function send(url, init) {
|
|
228
257
|
try {
|
|
229
258
|
return await fetch(url, init);
|
|
@@ -235,6 +264,14 @@ function createHttpClient(baseUrl, apiKey) {
|
|
|
235
264
|
);
|
|
236
265
|
}
|
|
237
266
|
}
|
|
267
|
+
async function sendWithRetry(url, init) {
|
|
268
|
+
let res = await send(url, init);
|
|
269
|
+
for (let attempt = 0; res.status === 429 && attempt < cfg.maxRetries; attempt++) {
|
|
270
|
+
await sleep(backoffMs(res, attempt, cfg));
|
|
271
|
+
res = await send(url, init);
|
|
272
|
+
}
|
|
273
|
+
return res;
|
|
274
|
+
}
|
|
238
275
|
async function handle(res) {
|
|
239
276
|
let text;
|
|
240
277
|
try {
|
|
@@ -267,11 +304,11 @@ function createHttpClient(baseUrl, apiKey) {
|
|
|
267
304
|
authorization: `Bearer ${apiKey}`
|
|
268
305
|
};
|
|
269
306
|
if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
|
|
270
|
-
const res = await
|
|
307
|
+
const res = await sendWithRetry(`${base}${path}`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
271
308
|
return handle(res);
|
|
272
309
|
},
|
|
273
310
|
async get(path) {
|
|
274
|
-
const res = await
|
|
311
|
+
const res = await sendWithRetry(`${base}${path}`, {
|
|
275
312
|
method: "GET",
|
|
276
313
|
headers: { authorization: `Bearer ${apiKey}` }
|
|
277
314
|
});
|
|
@@ -360,6 +397,17 @@ var playmosPayAbi = [
|
|
|
360
397
|
}
|
|
361
398
|
];
|
|
362
399
|
var prizePoolAbi = [
|
|
400
|
+
{
|
|
401
|
+
type: "function",
|
|
402
|
+
name: "openRound",
|
|
403
|
+
stateMutability: "nonpayable",
|
|
404
|
+
inputs: [
|
|
405
|
+
{ name: "roundId", type: "bytes32" },
|
|
406
|
+
{ name: "series", type: "bytes32" },
|
|
407
|
+
{ name: "entry", type: "uint256" }
|
|
408
|
+
],
|
|
409
|
+
outputs: []
|
|
410
|
+
},
|
|
363
411
|
{
|
|
364
412
|
type: "function",
|
|
365
413
|
name: "enter",
|
|
@@ -370,6 +418,40 @@ var prizePoolAbi = [
|
|
|
370
418
|
],
|
|
371
419
|
outputs: []
|
|
372
420
|
},
|
|
421
|
+
{
|
|
422
|
+
type: "function",
|
|
423
|
+
name: "lockRound",
|
|
424
|
+
stateMutability: "nonpayable",
|
|
425
|
+
inputs: [{ name: "roundId", type: "bytes32" }],
|
|
426
|
+
outputs: []
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
type: "function",
|
|
430
|
+
name: "settle",
|
|
431
|
+
stateMutability: "nonpayable",
|
|
432
|
+
inputs: [
|
|
433
|
+
{ name: "roundId", type: "bytes32" },
|
|
434
|
+
{ name: "winners", type: "address[]" },
|
|
435
|
+
{ name: "amounts", type: "uint256[]" }
|
|
436
|
+
],
|
|
437
|
+
outputs: []
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
type: "function",
|
|
441
|
+
name: "getRound",
|
|
442
|
+
stateMutability: "view",
|
|
443
|
+
inputs: [{ name: "roundId", type: "bytes32" }],
|
|
444
|
+
outputs: [
|
|
445
|
+
{ name: "state", type: "uint8" },
|
|
446
|
+
{ name: "series", type: "bytes32" },
|
|
447
|
+
{ name: "entry", type: "uint256" },
|
|
448
|
+
{ name: "pot", type: "uint256" },
|
|
449
|
+
{ name: "entrantCount", type: "uint256" },
|
|
450
|
+
{ name: "payable_", type: "uint256" },
|
|
451
|
+
{ name: "inheritedSeed", type: "uint256" },
|
|
452
|
+
{ name: "lockedAt", type: "uint256" }
|
|
453
|
+
]
|
|
454
|
+
},
|
|
373
455
|
{
|
|
374
456
|
type: "function",
|
|
375
457
|
name: "hasEntered",
|
|
@@ -379,6 +461,13 @@ var prizePoolAbi = [
|
|
|
379
461
|
{ name: "identity", type: "bytes32" }
|
|
380
462
|
],
|
|
381
463
|
outputs: [{ type: "bool" }]
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
type: "function",
|
|
467
|
+
name: "withdraw",
|
|
468
|
+
stateMutability: "nonpayable",
|
|
469
|
+
inputs: [],
|
|
470
|
+
outputs: [{ name: "amount", type: "uint256" }]
|
|
382
471
|
}
|
|
383
472
|
];
|
|
384
473
|
async function sendCalls(provider, from, chainId, calls, paymasterUrl) {
|
|
@@ -514,17 +603,42 @@ function mockEntryPayment(args) {
|
|
|
514
603
|
}
|
|
515
604
|
|
|
516
605
|
// src/client.ts
|
|
606
|
+
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
607
|
+
function requireAddressField(value, field) {
|
|
608
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
609
|
+
throw new MissingFieldError(field);
|
|
610
|
+
}
|
|
611
|
+
if (!ADDRESS_RE.test(value)) {
|
|
612
|
+
throw new ConfigError(
|
|
613
|
+
`${field} must be a 0x-prefixed 20-byte wallet address (Phase 1a transfers settle server-held wallets), got: ${JSON.stringify(value)}`,
|
|
614
|
+
{ field, value }
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
return value.toLowerCase();
|
|
618
|
+
}
|
|
619
|
+
function partyRef(value, field) {
|
|
620
|
+
if (typeof value !== "string" || value.trim() === "") throw new MissingFieldError(field);
|
|
621
|
+
if (value.startsWith("0x")) return requireAddressField(value, field);
|
|
622
|
+
return value;
|
|
623
|
+
}
|
|
517
624
|
var IAP_FEE_BPS = 100;
|
|
518
625
|
var POOL_BPS = 6e3;
|
|
519
626
|
var SEED_BPS = 3e3;
|
|
520
627
|
var RAKE_BPS = 1e3;
|
|
521
628
|
var Playmos = class {
|
|
522
629
|
constructor(config) {
|
|
630
|
+
/**
|
|
631
|
+
* Webhook helpers. Signature verification is **server-only** (Node `crypto`) and
|
|
632
|
+
* lives on `@playmos/sdk/server` so browser bundlers never see `node:crypto` (#9).
|
|
633
|
+
*
|
|
634
|
+
* import { verifyWebhook } from "@playmos/sdk/server";
|
|
635
|
+
* const event = verifyWebhook(rawBody, req.headers["x-playmos-signature"], secret);
|
|
636
|
+
*/
|
|
523
637
|
this.webhooks = {
|
|
524
638
|
/**
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
639
|
+
* @deprecated Use `import { verifyWebhook } from "@playmos/sdk/server"` instead.
|
|
640
|
+
* Throws if called — kept as a discoverable pointer so call sites fail loudly
|
|
641
|
+
* with a fix instruction rather than a silent missing method.
|
|
528
642
|
*/
|
|
529
643
|
verify: (_rawBody, _signatureHeader, _secret) => {
|
|
530
644
|
throw new ConfigError(
|
|
@@ -539,23 +653,278 @@ var Playmos = class {
|
|
|
539
653
|
/** Create a Bridge KYC onboarding link (fiat payout). */
|
|
540
654
|
createOnboardingLink: () => this.http.post("/payouts/onboarding_link", {})
|
|
541
655
|
};
|
|
656
|
+
/**
|
|
657
|
+
* Skill/contest **round lifecycle** (issue #13) — closes the enterRound loop.
|
|
658
|
+
*
|
|
659
|
+
* Scores never leave the studio. Flow:
|
|
660
|
+
* rounds.open → players enterRound → studio scores → rounds.lock →
|
|
661
|
+
* rounds.settle({ ranking }) → contract pays winners.
|
|
662
|
+
*
|
|
663
|
+
* Operator txs are signed by the Playmos service (OPERATOR_ROLE key).
|
|
664
|
+
*/
|
|
665
|
+
this.rounds = {
|
|
666
|
+
open: async (input) => {
|
|
667
|
+
requireField(input?.gameId, "gameId");
|
|
668
|
+
requireField(input?.roundId, "roundId");
|
|
669
|
+
requireField(input?.entryAmount, "entryAmount");
|
|
670
|
+
if (!input?.payout || typeof input.payout !== "object") {
|
|
671
|
+
throw new ConfigError("payout rule is required (winner-take-all | top-n | custom)");
|
|
672
|
+
}
|
|
673
|
+
validateAmount(input.entryAmount);
|
|
674
|
+
const { round } = await this.http.post("/rounds", {
|
|
675
|
+
gameId: input.gameId,
|
|
676
|
+
roundId: input.roundId,
|
|
677
|
+
entryAmount: input.entryAmount,
|
|
678
|
+
payout: input.payout,
|
|
679
|
+
closeAt: input.closeAt,
|
|
680
|
+
seriesKey: input.seriesKey,
|
|
681
|
+
roundKey: input.roundKey
|
|
682
|
+
});
|
|
683
|
+
return round;
|
|
684
|
+
},
|
|
685
|
+
lock: async (input) => {
|
|
686
|
+
requireField(input?.roundId, "roundId");
|
|
687
|
+
const { round } = await this.http.post(
|
|
688
|
+
`/rounds/${encodeURIComponent(input.roundId)}/lock`,
|
|
689
|
+
{ gameId: input.gameId }
|
|
690
|
+
);
|
|
691
|
+
return round;
|
|
692
|
+
},
|
|
693
|
+
settle: async (input) => {
|
|
694
|
+
requireField(input?.roundId, "roundId");
|
|
695
|
+
if (!input?.results) throw new ConfigError("results are required (ranking or winners)");
|
|
696
|
+
const { settle } = await this.http.post(
|
|
697
|
+
`/rounds/${encodeURIComponent(input.roundId)}/settle`,
|
|
698
|
+
{ gameId: input.gameId, results: input.results }
|
|
699
|
+
);
|
|
700
|
+
return settle;
|
|
701
|
+
},
|
|
702
|
+
get: async (input) => {
|
|
703
|
+
requireField(input?.roundId, "roundId");
|
|
704
|
+
const { round } = await this.http.get(
|
|
705
|
+
`/rounds/${encodeURIComponent(input.roundId)}`
|
|
706
|
+
);
|
|
707
|
+
return round;
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
/**
|
|
711
|
+
* `agents` — assign wallets to the NPCs YOUR game already owns, so they can transact USDC in your economy.
|
|
712
|
+
* The game creates the NPCs; the SDK only creates the WALLET for a game-supplied id. Engine-agnostic
|
|
713
|
+
* (Unity/Unreal/Godot/web all call the same REST). Requires a secret test key (`sk_test_`) on the sandbox.
|
|
714
|
+
*/
|
|
542
715
|
this.agents = {
|
|
543
|
-
/** Assign
|
|
544
|
-
createWallet: (input) => {
|
|
716
|
+
/** Assign (or return) the wallet for a game NPC id. Idempotent — safe to call wherever your NPCs spawn. */
|
|
717
|
+
createWallet: async (input) => {
|
|
718
|
+
requireField(input?.agentId, "agentId");
|
|
719
|
+
const { agent } = await this.http.post("/agents/wallets", { agentId: input.agentId });
|
|
720
|
+
return agent;
|
|
721
|
+
},
|
|
722
|
+
/** Resolve one NPC's wallet by your id. */
|
|
723
|
+
wallet: async (agentId) => {
|
|
724
|
+
requireField(agentId, "agentId");
|
|
725
|
+
const { agent } = await this.http.get(`/agents/wallets/${encodeURIComponent(agentId)}`);
|
|
726
|
+
return agent;
|
|
727
|
+
},
|
|
728
|
+
/** List the NPC wallets you've assigned in this studio. */
|
|
729
|
+
list: async () => {
|
|
730
|
+
const { agents } = await this.http.get("/agents/wallets");
|
|
731
|
+
return agents;
|
|
732
|
+
},
|
|
733
|
+
/** Sandbox faucet: fund an NPC with USDC from your treasury (per-NPC lifetime cap). */
|
|
734
|
+
fund: (input) => {
|
|
545
735
|
requireField(input?.agentId, "agentId");
|
|
546
|
-
|
|
736
|
+
validateAmount(input.amount);
|
|
737
|
+
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
738
|
+
return this.http.post(
|
|
739
|
+
`/agents/wallets/${encodeURIComponent(input.agentId)}/fund`,
|
|
740
|
+
{ amount: input.amount, idempotencyKey },
|
|
741
|
+
{ idempotencyKey }
|
|
742
|
+
);
|
|
547
743
|
},
|
|
548
|
-
/**
|
|
744
|
+
/** NPC→NPC (or →player) USDC transfer, by id. A thin alias of `playmos.transfer` (which is canonical). */
|
|
549
745
|
pay: (input) => {
|
|
550
746
|
requireField(input?.from, "from");
|
|
551
747
|
requireField(input?.to, "to");
|
|
552
|
-
|
|
553
|
-
|
|
748
|
+
return this.transfer({ from: input.from, to: input.to, amount: input.amount, feeBps: input.feeBps, feeSink: input.feeSink });
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
/**
|
|
752
|
+
* `escrow` — the fair-exchange primitive (Phase 2): lock the payer's USDC on-chain the instant a deal
|
|
753
|
+
* opens (`hold`), then move it exactly once — `release` (→ payee, fee skimmed) XOR `refund` (→ payer,
|
|
754
|
+
* 100%). The contract holds the funds trustlessly; YOUR game decides the rule (who resolves, and when).
|
|
755
|
+
* A `deadline` auto-refund guarantees funds never get stuck. Pass the `hold` result's `id` to the rest.
|
|
756
|
+
*/
|
|
757
|
+
this.escrow = {
|
|
758
|
+
/** Open a deal: lock `amount` of the payer's USDC into the on-chain escrow. Retries are idempotent.
|
|
759
|
+
* `async` so client-side validation surfaces as a rejected promise, not a synchronous throw. */
|
|
760
|
+
hold: async (input) => {
|
|
761
|
+
const amountMicro = validateAmount(input.amount);
|
|
762
|
+
const payer = input.payer === void 0 ? void 0 : requireAddressField(input.payer, "payer");
|
|
763
|
+
const payee = requireAddressField(input.payee, "payee");
|
|
764
|
+
const feeBps = input.feeBps ?? 0;
|
|
765
|
+
if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 1e4) {
|
|
766
|
+
throw new ConfigError(
|
|
767
|
+
`feeBps must be an integer in [0, 10000] (fee is taken at release only; refunds are 100%), got: ${JSON.stringify(input.feeBps)}`,
|
|
768
|
+
{ feeBps: input.feeBps }
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
const feeSink = input.feeSink === void 0 ? void 0 : requireAddressField(input.feeSink, "feeSink");
|
|
772
|
+
const resolver = input.resolver === void 0 ? void 0 : requireAddressField(input.resolver, "resolver");
|
|
773
|
+
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
774
|
+
const feeMicro = amountMicro * BigInt(feeBps) / 10000n;
|
|
775
|
+
const { escrow } = await this.http.post(
|
|
776
|
+
"/escrows",
|
|
777
|
+
{
|
|
778
|
+
payer,
|
|
779
|
+
payee,
|
|
780
|
+
amount: input.amount,
|
|
781
|
+
feeBps,
|
|
782
|
+
feeSink,
|
|
783
|
+
resolver,
|
|
784
|
+
expiresIn: input.expiresIn,
|
|
785
|
+
deadline: input.deadline,
|
|
786
|
+
memo: input.memo,
|
|
787
|
+
idempotencyKey
|
|
788
|
+
},
|
|
789
|
+
{ idempotencyKey }
|
|
790
|
+
);
|
|
791
|
+
return {
|
|
792
|
+
...escrow,
|
|
793
|
+
payee: escrow.payee ?? payee,
|
|
794
|
+
amount: escrow.amount ?? formatMicroToUsd(amountMicro),
|
|
795
|
+
feeBps: escrow.feeBps ?? feeBps,
|
|
796
|
+
feeSink: escrow.feeSink ?? feeSink ?? null,
|
|
797
|
+
resolver: escrow.resolver ?? resolver ?? null,
|
|
798
|
+
feeAtRelease: escrow.feeAtRelease ?? formatMicroToUsd(feeMicro),
|
|
799
|
+
netAtRelease: escrow.netAtRelease ?? formatMicroToUsd(amountMicro - feeMicro),
|
|
800
|
+
idempotentReplay: Boolean(escrow.idempotentReplay)
|
|
801
|
+
};
|
|
802
|
+
},
|
|
803
|
+
/** Release a held deal to the payee (fee skimmed). `escrowId` is the `hold` result's `id`. */
|
|
804
|
+
release: async (input) => {
|
|
805
|
+
requireField(input?.escrowId, "escrowId");
|
|
806
|
+
const { escrow } = await this.http.post(`/escrows/${encodeURIComponent(input.escrowId)}/release`, {});
|
|
807
|
+
return escrow;
|
|
808
|
+
},
|
|
809
|
+
/** Refund a held deal to the payer (100%, untaxed). `escrowId` is the `hold` result's `id`. */
|
|
810
|
+
refund: async (input) => {
|
|
811
|
+
requireField(input?.escrowId, "escrowId");
|
|
812
|
+
const { escrow } = await this.http.post(`/escrows/${encodeURIComponent(input.escrowId)}/refund`, {});
|
|
813
|
+
return escrow;
|
|
814
|
+
},
|
|
815
|
+
/** Verify a deal's on-chain state (reconciles a `settling` hold wedged by a crash). */
|
|
816
|
+
get: async (escrowId) => {
|
|
817
|
+
requireField(escrowId, "escrowId");
|
|
818
|
+
const { escrow } = await this.http.get(`/escrows/${encodeURIComponent(escrowId)}`);
|
|
819
|
+
return escrow;
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
/**
|
|
823
|
+
* `marketplace` — list an item, and the seller is paid ONLY when it's bought (Phase 3a, off-chain items).
|
|
824
|
+
* Built on `escrow`: `buy` locks the buyer's USDC on-chain, `confirm` (after your game server delivers the
|
|
825
|
+
* item) pays the seller, and a no-delivery/timeout `refund`s the buyer. `deliver: true` on `buy` collapses
|
|
826
|
+
* lock+pay into one call when your server delivers synchronously. On-chain items are Phase 3b.
|
|
827
|
+
*/
|
|
828
|
+
this.marketplace = {
|
|
829
|
+
/** List an off-chain item for sale. No money moves. Idempotent on `idempotencyKey`. */
|
|
830
|
+
list: async (input) => {
|
|
831
|
+
if (!input?.item || input.item.kind !== "offchain") {
|
|
832
|
+
throw new ConfigError('marketplace.list requires item = { kind: "offchain", sku: "<your-item-id>" } (on-chain items are Phase 3b)', { item: input?.item });
|
|
833
|
+
}
|
|
834
|
+
validateAmount(input.price);
|
|
835
|
+
const seller = requireAddressField(input.seller, "seller");
|
|
836
|
+
const feeBps = input.feeBps;
|
|
837
|
+
if (feeBps !== void 0 && (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 1e4)) {
|
|
838
|
+
throw new ConfigError(`feeBps must be an integer in [0, 10000], got: ${JSON.stringify(feeBps)}`, { feeBps });
|
|
839
|
+
}
|
|
840
|
+
const feeSink = input.feeSink === void 0 ? void 0 : requireAddressField(input.feeSink, "feeSink");
|
|
841
|
+
const { listing } = await this.http.post("/listings", {
|
|
842
|
+
seller,
|
|
843
|
+
item: input.item,
|
|
844
|
+
price: input.price,
|
|
845
|
+
feeBps,
|
|
846
|
+
feeSink,
|
|
847
|
+
deliveryWindow: input.deliveryWindow,
|
|
848
|
+
expiresIn: input.expiresIn,
|
|
849
|
+
gameId: input.gameId,
|
|
850
|
+
idempotencyKey: input.idempotencyKey
|
|
851
|
+
});
|
|
852
|
+
return listing;
|
|
853
|
+
},
|
|
854
|
+
/** Buy a listing: lock the buyer's USDC in escrow. Pass `deliver: true` to also pay the seller in one call. */
|
|
855
|
+
buy: async (input) => {
|
|
856
|
+
requireField(input?.listingId, "listingId");
|
|
857
|
+
const buyer = input.buyer === void 0 ? void 0 : requireAddressField(input.buyer, "buyer");
|
|
858
|
+
return this.http.post(`/listings/${encodeURIComponent(input.listingId)}/buy`, { buyer, deliver: input.deliver === true });
|
|
859
|
+
},
|
|
860
|
+
/** Confirm delivery → the seller is paid (release), fee skimmed. */
|
|
861
|
+
confirm: async (input) => {
|
|
862
|
+
requireField(input?.listingId, "listingId");
|
|
863
|
+
return this.http.post(`/listings/${encodeURIComponent(input.listingId)}/confirm`, {});
|
|
864
|
+
},
|
|
865
|
+
/** Refund the buyer 100% (seller couldn't deliver / dispute / timeout). */
|
|
866
|
+
refund: async (input) => {
|
|
867
|
+
requireField(input?.listingId, "listingId");
|
|
868
|
+
return this.http.post(`/listings/${encodeURIComponent(input.listingId)}/refund`, {});
|
|
869
|
+
},
|
|
870
|
+
/** Delist an unsold listing (pure DB, no tx). */
|
|
871
|
+
cancel: async (input) => {
|
|
872
|
+
requireField(input?.listingId, "listingId");
|
|
873
|
+
const { listing } = await this.http.post(`/listings/${encodeURIComponent(input.listingId)}/cancel`, {});
|
|
874
|
+
return listing;
|
|
875
|
+
},
|
|
876
|
+
/** Fetch a listing + its on-chain-verified sale status. */
|
|
877
|
+
get: async (listingId) => {
|
|
878
|
+
requireField(listingId, "listingId");
|
|
879
|
+
return this.http.get(`/listings/${encodeURIComponent(listingId)}`);
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
/**
|
|
883
|
+
* `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
|
|
884
|
+
* `get` reconciles once against chain truth; `wait` polls it to a terminal state
|
|
885
|
+
* for you (no hand-rolled loop). Both cover the gasless agent path.
|
|
886
|
+
*/
|
|
887
|
+
this.transfers = {
|
|
888
|
+
/** One-shot reconcile of a transfer against chain truth. */
|
|
889
|
+
get: async (transferId) => {
|
|
890
|
+
requireField(transferId, "transferId");
|
|
891
|
+
const res = await this.http.get(
|
|
892
|
+
`/transfers/${encodeURIComponent(transferId)}`
|
|
893
|
+
);
|
|
894
|
+
return res.transfer;
|
|
895
|
+
},
|
|
896
|
+
/**
|
|
897
|
+
* Block until a transfer reaches a terminal state — `settled` or `failed` —
|
|
898
|
+
* instead of hand-rolling a poll loop (#47). Polls `transfers.get(id)` every
|
|
899
|
+
* `intervalMs` (default 1000) until terminal, then RESOLVES with the final
|
|
900
|
+
* reconcile. Throws a typed `ApiError` (`detail.timeout`) if neither
|
|
901
|
+
* `timeoutMs` (default 30000) nor `maxAttempts` (default 40) is reached first.
|
|
902
|
+
*
|
|
903
|
+
* A `failed` transfer is a legitimate outcome, so it RESOLVES (status
|
|
904
|
+
* "failed") — inspect `result.status`; it does not throw.
|
|
905
|
+
*/
|
|
906
|
+
wait: async (transferId, opts) => {
|
|
907
|
+
requireField(transferId, "transferId");
|
|
908
|
+
const intervalMs = opts?.intervalMs ?? 1e3;
|
|
909
|
+
const timeoutMs = opts?.timeoutMs ?? 3e4;
|
|
910
|
+
const maxAttempts = opts?.maxAttempts ?? 40;
|
|
911
|
+
const deadline = Date.now() + timeoutMs;
|
|
912
|
+
let last;
|
|
913
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
914
|
+
last = await this.transfers.get(transferId);
|
|
915
|
+
if (last.status === "settled" || last.status === "failed") return last;
|
|
916
|
+
if (Date.now() + intervalMs > deadline) break;
|
|
917
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
918
|
+
}
|
|
919
|
+
throw new ApiError(
|
|
920
|
+
`Timed out waiting for transfer ${transferId} to settle (last status: ${last?.status ?? "unknown"}). It may still settle \u2014 re-check with playmos.transfers.get("${transferId}").`,
|
|
921
|
+
{ transferId, timeout: true, lastStatus: last?.status ?? "unknown" }
|
|
922
|
+
);
|
|
554
923
|
}
|
|
555
924
|
};
|
|
556
925
|
this.config = config;
|
|
557
926
|
this.env = resolveEnv(config.apiKey, config.network, config.apiBaseUrl);
|
|
558
|
-
this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey);
|
|
927
|
+
this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
|
|
559
928
|
}
|
|
560
929
|
/** Connect the player's wallet and return their address. */
|
|
561
930
|
async connect() {
|
|
@@ -627,9 +996,11 @@ var Playmos = class {
|
|
|
627
996
|
const playmosPay = intent.clientParams?.contractAddress ?? this.config.contracts?.playmosPay;
|
|
628
997
|
const studio = intent.clientParams?.studio ?? input.studio;
|
|
629
998
|
if (!playmosPay) throw new ConfigError("No PlaymosPay contract address (service intent + config.contracts both empty).");
|
|
630
|
-
if (!studio) throw new ConfigError("No studio payout address for this payment.");
|
|
999
|
+
if (!studio) throw new ConfigError("No studio payout address for this payment (service must return clientParams.studio).");
|
|
1000
|
+
if (!intent.clientParams?.studio && input.studio) ;
|
|
631
1001
|
const amountUnits = this.resolveUnits(intent, amountMicro);
|
|
632
|
-
const
|
|
1002
|
+
const paymentId = intent.clientParams?.paymentId ?? intent.payment.id;
|
|
1003
|
+
const calls = buildIapCalls({ usdc, playmosPay, paymentId, studio, amountUnits });
|
|
633
1004
|
const { id: callsId } = await sendCalls(provider, from, this.env.chainId, calls, this.sponsorUrl(intent));
|
|
634
1005
|
const { txHash } = await waitForCalls(provider, callsId);
|
|
635
1006
|
return this.settle(intent.payment.id, txHash);
|
|
@@ -681,8 +1052,9 @@ var Playmos = class {
|
|
|
681
1052
|
const usdc = intent.clientParams?.usdc ?? this.config.contracts?.usdc ?? USDC_ADDRESS[this.env.network];
|
|
682
1053
|
const prizePool = intent.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
683
1054
|
if (!prizePool) throw new ConfigError("No PrizePool contract address for this game (service intent + config.contracts both empty).");
|
|
684
|
-
const roundKey =
|
|
685
|
-
|
|
1055
|
+
const roundKey = intent.clientParams?.roundKey ?? intent.clientParams?.roundId ?? input.roundKey ?? intent.payment.roundId ?? input.roundId;
|
|
1056
|
+
if (!roundKey) throw new ConfigError("No roundKey/roundId for enterRound (service clientParams missing).");
|
|
1057
|
+
const identity = intent.clientParams?.identity ?? input.identity ?? intent.payment.identity ?? `${from}#${idempotencyKey}`;
|
|
686
1058
|
const amountUnits = this.resolveUnits(intent, amountMicro);
|
|
687
1059
|
const calls = buildEntryCalls({ usdc, prizePool, roundKey, identity, amountUnits });
|
|
688
1060
|
const { id: callsId } = await sendCalls(provider, from, this.env.chainId, calls, this.sponsorUrl(intent));
|
|
@@ -691,6 +1063,66 @@ var Playmos = class {
|
|
|
691
1063
|
payment.identity = identity;
|
|
692
1064
|
return payment;
|
|
693
1065
|
}
|
|
1066
|
+
/**
|
|
1067
|
+
* `transfer` — the value-movement base primitive (Phase 1a): move USDC from one wallet to another,
|
|
1068
|
+
* with a per-call fee. The GAME LOGIC is the authority — you already decided the move is valid — so
|
|
1069
|
+
* this is a direct, unconditional push (use `escrow`/`marketplace` when a trust boundary needs fair
|
|
1070
|
+
* exchange). The service settles it through the protocol-agnostic settlement core (idempotent,
|
|
1071
|
+
* reserve-before-broadcast) and the on-chain PlaymosTransfer / PlaymosTransferAuth contracts.
|
|
1072
|
+
*
|
|
1073
|
+
* Fee is per-call: `feeBps` 0–10000 (+ `feeSink`). `feeBps: 0` is an untaxed reward/faucet transfer.
|
|
1074
|
+
* Retries are safe: pass the same `idempotencyKey` and a re-call NEVER broadcasts a second tx —
|
|
1075
|
+
* it returns the cached result (`idempotentReplay: true`).
|
|
1076
|
+
*
|
|
1077
|
+
* Confirmation: treat `status === "settled" && txHash` as final. If `settling`, either call
|
|
1078
|
+
* `playmos.transfers.wait(id)`, or pass `{ confirm: true }` here to block until terminal in one
|
|
1079
|
+
* call (#47). BaseScan: `https://sepolia.basescan.org/tx/<txHash>`.
|
|
1080
|
+
*
|
|
1081
|
+
* NPC `from` requires `sk_test_` and settles gaslessly (NPC signs; service relays).
|
|
1082
|
+
*/
|
|
1083
|
+
async transfer(input, opts) {
|
|
1084
|
+
const amountMicro = validateAmount(input.amount);
|
|
1085
|
+
const from = input.from === void 0 ? void 0 : partyRef(input.from, "from");
|
|
1086
|
+
const to = partyRef(input.to, "to");
|
|
1087
|
+
const feeBps = input.feeBps ?? 0;
|
|
1088
|
+
if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 1e4) {
|
|
1089
|
+
throw new ConfigError(
|
|
1090
|
+
`feeBps must be an integer in [0, 10000] (0 = untaxed reward/faucet; 500 = 5%; 10000 = 100%), got: ${JSON.stringify(input.feeBps)}`,
|
|
1091
|
+
{ feeBps: input.feeBps }
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
const feeSink = input.feeSink === void 0 ? void 0 : requireAddressField(input.feeSink, "feeSink");
|
|
1095
|
+
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
1096
|
+
const res = await this.http.post(
|
|
1097
|
+
"/transfers",
|
|
1098
|
+
{ from, to, amount: input.amount, memo: input.memo, feeBps, feeSink, idempotencyKey },
|
|
1099
|
+
{ idempotencyKey }
|
|
1100
|
+
);
|
|
1101
|
+
const t = res.transfer;
|
|
1102
|
+
const feeMicro = amountMicro * BigInt(feeBps) / 10000n;
|
|
1103
|
+
const out = {
|
|
1104
|
+
id: t.id,
|
|
1105
|
+
status: t.status,
|
|
1106
|
+
txHash: t.txHash,
|
|
1107
|
+
from: t.from,
|
|
1108
|
+
// the service's resolved payer address (omitted when it's the redacted treasury signer)
|
|
1109
|
+
to: t.to,
|
|
1110
|
+
// the service's resolved payee address
|
|
1111
|
+
amount: t.amount ?? formatMicroToUsd(amountMicro),
|
|
1112
|
+
fee: t.fee ?? formatMicroToUsd(feeMicro),
|
|
1113
|
+
net: t.net ?? formatMicroToUsd(amountMicro - feeMicro),
|
|
1114
|
+
feeBps: t.feeBps ?? feeBps,
|
|
1115
|
+
feeSink: t.feeSink ?? feeSink ?? null,
|
|
1116
|
+
memo: t.memo ?? input.memo ?? null,
|
|
1117
|
+
idempotentReplay: Boolean(t.idempotentReplay)
|
|
1118
|
+
};
|
|
1119
|
+
if (opts?.confirm && out.status === "settling") {
|
|
1120
|
+
const final = await this.transfers.wait(out.id, opts);
|
|
1121
|
+
out.status = final.status;
|
|
1122
|
+
out.txHash = final.txHash ?? out.txHash;
|
|
1123
|
+
}
|
|
1124
|
+
return out;
|
|
1125
|
+
}
|
|
694
1126
|
/** Verify a payment by the service's on-chain read (spec §6.1). Idempotent. */
|
|
695
1127
|
async verify(paymentId) {
|
|
696
1128
|
requireField(paymentId, "paymentId");
|
|
@@ -782,6 +1214,30 @@ function previewIapSplit(amount) {
|
|
|
782
1214
|
const { feeMicro, netMicro } = computeIapSplit(micro, IAP_FEE_BPS);
|
|
783
1215
|
return { amount: formatMicroToUsd(micro), fee: formatMicroToUsd(feeMicro), net: formatMicroToUsd(netMicro) };
|
|
784
1216
|
}
|
|
1217
|
+
function previewTransferSplit(amount, feeBps) {
|
|
1218
|
+
const micro = validateAmount(amount);
|
|
1219
|
+
if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 1e4) {
|
|
1220
|
+
throw new ConfigError(`feeBps must be an integer in [0, 10000], got: ${JSON.stringify(feeBps)}`, { feeBps });
|
|
1221
|
+
}
|
|
1222
|
+
const feeMicro = micro * BigInt(feeBps) / 10000n;
|
|
1223
|
+
return { amount: formatMicroToUsd(micro), fee: formatMicroToUsd(feeMicro), net: formatMicroToUsd(micro - feeMicro), feeBps };
|
|
1224
|
+
}
|
|
1225
|
+
function previewEscrowFee(amount, feeBps) {
|
|
1226
|
+
const micro = validateAmount(amount);
|
|
1227
|
+
if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 1e4) {
|
|
1228
|
+
throw new ConfigError(`feeBps must be an integer in [0, 10000], got: ${JSON.stringify(feeBps)}`, { feeBps });
|
|
1229
|
+
}
|
|
1230
|
+
const feeMicro = micro * BigInt(feeBps) / 10000n;
|
|
1231
|
+
return { amount: formatMicroToUsd(micro), fee: formatMicroToUsd(feeMicro), net: formatMicroToUsd(micro - feeMicro), feeBps };
|
|
1232
|
+
}
|
|
1233
|
+
function previewMarketplaceSplit(price, feeBps) {
|
|
1234
|
+
const micro = validateAmount(price);
|
|
1235
|
+
if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 1e4) {
|
|
1236
|
+
throw new ConfigError(`feeBps must be an integer in [0, 10000], got: ${JSON.stringify(feeBps)}`, { feeBps });
|
|
1237
|
+
}
|
|
1238
|
+
const feeMicro = micro * BigInt(feeBps) / 10000n;
|
|
1239
|
+
return { price: formatMicroToUsd(micro), fee: formatMicroToUsd(feeMicro), net: formatMicroToUsd(micro - feeMicro), feeBps };
|
|
1240
|
+
}
|
|
785
1241
|
function previewPoolSplit(amount) {
|
|
786
1242
|
const micro = validateAmount(amount);
|
|
787
1243
|
const { poolMicro, seedMicro, rakeMicro } = computePoolSplit(micro, POOL_BPS, SEED_BPS, RAKE_BPS);
|
|
@@ -793,6 +1249,205 @@ function previewPoolSplit(amount) {
|
|
|
793
1249
|
};
|
|
794
1250
|
}
|
|
795
1251
|
|
|
1252
|
+
// src/payout.ts
|
|
1253
|
+
var PayoutError = class extends Error {
|
|
1254
|
+
constructor(message) {
|
|
1255
|
+
super(message);
|
|
1256
|
+
this.code = "payout_invalid";
|
|
1257
|
+
this.name = "PayoutError";
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
|
|
1261
|
+
var BPS = 10000n;
|
|
1262
|
+
function requireAddress(w, i) {
|
|
1263
|
+
if (!ADDRESS_RE2.test(w)) {
|
|
1264
|
+
throw new PayoutError(`ranking[${i}] must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(w)}`);
|
|
1265
|
+
}
|
|
1266
|
+
return w.toLowerCase();
|
|
1267
|
+
}
|
|
1268
|
+
function parseUsdToMicroLoose(amount) {
|
|
1269
|
+
if (typeof amount !== "string" || !/^\d+(\.\d{1,6})?$/.test(amount.trim())) {
|
|
1270
|
+
throw new PayoutError(`invalid USD amount: ${JSON.stringify(amount)}`);
|
|
1271
|
+
}
|
|
1272
|
+
const [wholeRaw, frac = ""] = amount.trim().split(".");
|
|
1273
|
+
const whole = wholeRaw ?? "0";
|
|
1274
|
+
const fracPadded = (frac + "000000").slice(0, 6);
|
|
1275
|
+
const micro = BigInt(whole) * 1000000n + BigInt(fracPadded);
|
|
1276
|
+
if (micro <= 0n) throw new PayoutError(`amount must be > 0, got: ${JSON.stringify(amount)}`);
|
|
1277
|
+
return micro;
|
|
1278
|
+
}
|
|
1279
|
+
function computePayout(pool, ranking, rule) {
|
|
1280
|
+
if (typeof pool !== "bigint" || pool <= 0n) {
|
|
1281
|
+
throw new PayoutError(`pool must be a positive bigint (micro-USDC), got: ${String(pool)}`);
|
|
1282
|
+
}
|
|
1283
|
+
if (!Array.isArray(ranking) || ranking.length === 0) {
|
|
1284
|
+
throw new PayoutError("ranking must be a non-empty array of wallets (best-first)");
|
|
1285
|
+
}
|
|
1286
|
+
const wallets = ranking.map((w, i) => requireAddress(w, i));
|
|
1287
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1288
|
+
for (const w of wallets) {
|
|
1289
|
+
if (seen.has(w)) throw new PayoutError(`duplicate wallet in ranking: ${w}`);
|
|
1290
|
+
seen.add(w);
|
|
1291
|
+
}
|
|
1292
|
+
if (rule.kind === "winner-take-all") {
|
|
1293
|
+
return [{ wallet: wallets[0], amount: pool }];
|
|
1294
|
+
}
|
|
1295
|
+
if (rule.kind === "top-n") {
|
|
1296
|
+
const splits = rule.splitsBps;
|
|
1297
|
+
if (!Array.isArray(splits) || splits.length === 0) {
|
|
1298
|
+
throw new PayoutError("top-n splitsBps must be a non-empty array");
|
|
1299
|
+
}
|
|
1300
|
+
if (splits.length > wallets.length) {
|
|
1301
|
+
throw new PayoutError(
|
|
1302
|
+
`top-n needs ${splits.length} ranked wallets but ranking only has ${wallets.length}`
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
let sumBps = 0;
|
|
1306
|
+
for (const b of splits) {
|
|
1307
|
+
if (!Number.isInteger(b) || b <= 0 || b > 1e4) {
|
|
1308
|
+
throw new PayoutError(`each splitsBps entry must be an integer in (0, 10000], got: ${b}`);
|
|
1309
|
+
}
|
|
1310
|
+
sumBps += b;
|
|
1311
|
+
}
|
|
1312
|
+
if (sumBps !== 1e4) {
|
|
1313
|
+
throw new PayoutError(`splitsBps must sum to 10000, got ${sumBps}`);
|
|
1314
|
+
}
|
|
1315
|
+
const out = [];
|
|
1316
|
+
let allocated = 0n;
|
|
1317
|
+
for (let i = 0; i < splits.length; i++) {
|
|
1318
|
+
const amt = pool * BigInt(splits[i]) / BPS;
|
|
1319
|
+
out.push({ wallet: wallets[i], amount: amt });
|
|
1320
|
+
allocated += amt;
|
|
1321
|
+
}
|
|
1322
|
+
const remainder = pool - allocated;
|
|
1323
|
+
if (remainder < 0n) throw new PayoutError("internal: allocated more than pool");
|
|
1324
|
+
out[0] = { wallet: out[0].wallet, amount: out[0].amount + remainder };
|
|
1325
|
+
const filtered = out.filter((x) => x.amount > 0n);
|
|
1326
|
+
if (filtered.length === 0) throw new PayoutError("payout produced no positive amounts");
|
|
1327
|
+
const total = filtered.reduce((s, x) => s + x.amount, 0n);
|
|
1328
|
+
if (total !== pool) throw new PayoutError(`internal: sum ${total} != pool ${pool}`);
|
|
1329
|
+
return filtered;
|
|
1330
|
+
}
|
|
1331
|
+
if (rule.kind === "custom") {
|
|
1332
|
+
const amounts = rule.amounts;
|
|
1333
|
+
if (!Array.isArray(amounts) || amounts.length === 0) {
|
|
1334
|
+
throw new PayoutError("custom amounts must be a non-empty array of USD strings");
|
|
1335
|
+
}
|
|
1336
|
+
if (amounts.length > wallets.length) {
|
|
1337
|
+
throw new PayoutError(
|
|
1338
|
+
`custom amounts has ${amounts.length} entries but ranking only has ${wallets.length}`
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
const out = [];
|
|
1342
|
+
let total = 0n;
|
|
1343
|
+
for (let i = 0; i < amounts.length; i++) {
|
|
1344
|
+
const amt = parseUsdToMicroLoose(amounts[i]);
|
|
1345
|
+
out.push({ wallet: wallets[i], amount: amt });
|
|
1346
|
+
total += amt;
|
|
1347
|
+
}
|
|
1348
|
+
if (total !== pool) {
|
|
1349
|
+
throw new PayoutError(
|
|
1350
|
+
`custom amounts sum to ${total} micro-USDC but pool is ${pool} \u2014 must match exactly`
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
return out;
|
|
1354
|
+
}
|
|
1355
|
+
throw new PayoutError(`unknown payout rule kind: ${JSON.stringify(rule.kind)}`);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// src/settlement.ts
|
|
1359
|
+
var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
|
|
1360
|
+
var DEFAULT_TTL_MS = 15 * 60 * 1e3;
|
|
1361
|
+
function requireAddress2(value, field) {
|
|
1362
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
1363
|
+
throw new MissingFieldError(field);
|
|
1364
|
+
}
|
|
1365
|
+
if (!ADDRESS_RE3.test(value)) {
|
|
1366
|
+
throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
|
|
1367
|
+
field,
|
|
1368
|
+
value
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
return value;
|
|
1372
|
+
}
|
|
1373
|
+
function requireNetwork(value) {
|
|
1374
|
+
if (value === "base" || value === "base-sepolia") return value;
|
|
1375
|
+
throw new ConfigError(`Unknown network: ${JSON.stringify(value)}. Expected one of ${Object.keys(CHAIN_ID).join(", ")}.`, {
|
|
1376
|
+
field: "network",
|
|
1377
|
+
value
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
function createPaymentRequirement(input) {
|
|
1381
|
+
const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
1382
|
+
const payTo = requireAddress2(input.payTo, "payTo");
|
|
1383
|
+
const network = requireNetwork(input.network);
|
|
1384
|
+
const asset = input.asset ?? "USDC";
|
|
1385
|
+
if (asset !== "USDC") {
|
|
1386
|
+
throw new ConfigError(`Unsupported asset: ${JSON.stringify(asset)}. Only "USDC" is supported.`, { asset });
|
|
1387
|
+
}
|
|
1388
|
+
parseUsdToMicro(input.amount);
|
|
1389
|
+
const expiresAt = input.expiresAt ?? new Date(now.getTime() + (input.expiresInMs ?? DEFAULT_TTL_MS)).toISOString();
|
|
1390
|
+
const req = {
|
|
1391
|
+
id: input.id ?? prefixedId("preq"),
|
|
1392
|
+
payTo,
|
|
1393
|
+
amount: input.amount,
|
|
1394
|
+
asset,
|
|
1395
|
+
network,
|
|
1396
|
+
expiresAt
|
|
1397
|
+
};
|
|
1398
|
+
if (input.terms !== void 0) req.terms = input.terms;
|
|
1399
|
+
return req;
|
|
1400
|
+
}
|
|
1401
|
+
function serializePaymentRequirement(req) {
|
|
1402
|
+
const body = {
|
|
1403
|
+
id: req.id,
|
|
1404
|
+
payTo: req.payTo,
|
|
1405
|
+
amount: req.amount,
|
|
1406
|
+
asset: req.asset,
|
|
1407
|
+
network: req.network,
|
|
1408
|
+
expiresAt: req.expiresAt
|
|
1409
|
+
};
|
|
1410
|
+
if (req.terms !== void 0) body.terms = req.terms;
|
|
1411
|
+
return body;
|
|
1412
|
+
}
|
|
1413
|
+
function parsePaymentRequirement(input) {
|
|
1414
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
1415
|
+
throw new ConfigError("PaymentRequirement must be a JSON object.", { input });
|
|
1416
|
+
}
|
|
1417
|
+
const o = input;
|
|
1418
|
+
const id = o.id;
|
|
1419
|
+
if (typeof id !== "string" || id.trim() === "") throw new MissingFieldError("id");
|
|
1420
|
+
if (o.asset !== "USDC") {
|
|
1421
|
+
throw new ConfigError(`PaymentRequirement.asset must be "USDC", got: ${JSON.stringify(o.asset)}.`, {
|
|
1422
|
+
asset: o.asset
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
const payTo = requireAddress2(o.payTo, "payTo");
|
|
1426
|
+
const network = requireNetwork(o.network);
|
|
1427
|
+
if (typeof o.amount !== "string") throw new InvalidAmountError(o.amount);
|
|
1428
|
+
parseUsdToMicro(o.amount);
|
|
1429
|
+
if (typeof o.expiresAt !== "string" || o.expiresAt.trim() === "") throw new MissingFieldError("expiresAt");
|
|
1430
|
+
if (o.terms !== void 0 && typeof o.terms !== "string") {
|
|
1431
|
+
throw new ConfigError("PaymentRequirement.terms must be a string when present.", { terms: o.terms });
|
|
1432
|
+
}
|
|
1433
|
+
const req = {
|
|
1434
|
+
id,
|
|
1435
|
+
payTo,
|
|
1436
|
+
amount: o.amount,
|
|
1437
|
+
asset: "USDC",
|
|
1438
|
+
network,
|
|
1439
|
+
expiresAt: o.expiresAt
|
|
1440
|
+
};
|
|
1441
|
+
if (o.terms !== void 0) req.terms = o.terms;
|
|
1442
|
+
return req;
|
|
1443
|
+
}
|
|
1444
|
+
function isWalletSignatureAuthorization(auth) {
|
|
1445
|
+
return auth.kind === "wallet-signature";
|
|
1446
|
+
}
|
|
1447
|
+
function isX402PayloadAuthorization(auth) {
|
|
1448
|
+
return auth.kind === "x402-payload";
|
|
1449
|
+
}
|
|
1450
|
+
|
|
796
1451
|
exports.ApiError = ApiError;
|
|
797
1452
|
exports.AuthError = AuthError;
|
|
798
1453
|
exports.CHAIN_ID = CHAIN_ID;
|
|
@@ -803,18 +1458,28 @@ exports.InvalidAmountError = InvalidAmountError;
|
|
|
803
1458
|
exports.MICRO_PER_USDC = MICRO_PER_USDC;
|
|
804
1459
|
exports.MissingFieldError = MissingFieldError;
|
|
805
1460
|
exports.PaymentFailedError = PaymentFailedError;
|
|
1461
|
+
exports.PayoutError = PayoutError;
|
|
806
1462
|
exports.Playmos = Playmos;
|
|
807
1463
|
exports.PlaymosError = PlaymosError;
|
|
808
1464
|
exports.USDC_ADDRESS = USDC_ADDRESS;
|
|
809
1465
|
exports.USDC_DECIMALS = USDC_DECIMALS;
|
|
810
1466
|
exports.WalletConnectionError = WalletConnectionError;
|
|
811
1467
|
exports.computeIapSplit = computeIapSplit;
|
|
1468
|
+
exports.computePayout = computePayout;
|
|
812
1469
|
exports.computePoolSplit = computePoolSplit;
|
|
1470
|
+
exports.createPaymentRequirement = createPaymentRequirement;
|
|
813
1471
|
exports.formatMicroToUsd = formatMicroToUsd;
|
|
1472
|
+
exports.isWalletSignatureAuthorization = isWalletSignatureAuthorization;
|
|
1473
|
+
exports.isX402PayloadAuthorization = isX402PayloadAuthorization;
|
|
1474
|
+
exports.parsePaymentRequirement = parsePaymentRequirement;
|
|
814
1475
|
exports.parseUsdToMicro = parseUsdToMicro;
|
|
815
1476
|
exports.prefixedId = prefixedId;
|
|
1477
|
+
exports.previewEscrowFee = previewEscrowFee;
|
|
816
1478
|
exports.previewIapSplit = previewIapSplit;
|
|
1479
|
+
exports.previewMarketplaceSplit = previewMarketplaceSplit;
|
|
817
1480
|
exports.previewPoolSplit = previewPoolSplit;
|
|
1481
|
+
exports.previewTransferSplit = previewTransferSplit;
|
|
1482
|
+
exports.serializePaymentRequirement = serializePaymentRequirement;
|
|
818
1483
|
exports.ulid = ulid;
|
|
819
1484
|
//# sourceMappingURL=index.cjs.map
|
|
820
1485
|
//# sourceMappingURL=index.cjs.map
|