@playmos/sdk 0.3.2 → 0.3.4
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 +15 -0
- package/dist/{chunk-TMBBEGIF.js → chunk-VYR6BBHF.js} +0 -2
- package/dist/{errors-DMtMpbR6.d.cts → errors-B_AqyZQE.d.cts} +42 -12
- package/dist/{errors-DMtMpbR6.d.ts → errors-B_AqyZQE.d.ts} +42 -12
- package/dist/index.cjs +409 -110
- package/dist/index.d.cts +64 -17
- package/dist/index.d.ts +64 -17
- package/dist/index.js +409 -113
- package/dist/server.cjs +0 -2
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -3
- package/package.json +10 -5
- package/dist/chunk-TMBBEGIF.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/server.cjs.map +0 -1
- package/dist/server.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { InvalidAmountError, ConfigError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, ApiError, WalletConnectionError, InsufficientGasError
|
|
2
|
-
export { ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError } from './chunk-
|
|
1
|
+
import { AuthError, InvalidAmountError, ConfigError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, ApiError, WalletConnectionError, InsufficientGasError } from './chunk-VYR6BBHF.js';
|
|
2
|
+
export { ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError } from './chunk-VYR6BBHF.js';
|
|
3
3
|
import { encodeFunctionData, decodeFunctionResult, numberToHex, keccak256, toBytes } from 'viem';
|
|
4
4
|
|
|
5
5
|
// src/config.ts
|
|
@@ -15,9 +15,26 @@ var DEFAULT_API_BASE_URL = {
|
|
|
15
15
|
base: "https://api.playmos.io/v1",
|
|
16
16
|
"base-sepolia": "https://api.sandbox.playmos.io/v1"
|
|
17
17
|
};
|
|
18
|
+
function clientRuntimeSignals(g = globalThis) {
|
|
19
|
+
const any = g;
|
|
20
|
+
const signals = [];
|
|
21
|
+
if (typeof any.window !== "undefined") signals.push("window");
|
|
22
|
+
if (typeof any.document !== "undefined") signals.push("document");
|
|
23
|
+
const likelyCfWorker = typeof any.Cloudflare !== "undefined" || typeof any.WebSocketPair !== "undefined" && typeof any.window === "undefined";
|
|
24
|
+
if (typeof any.importScripts === "function" && !likelyCfWorker) {
|
|
25
|
+
signals.push("importScripts");
|
|
26
|
+
}
|
|
27
|
+
if (any.navigator?.product === "ReactNative") signals.push("ReactNative");
|
|
28
|
+
return signals;
|
|
29
|
+
}
|
|
30
|
+
function isClientRuntime(g = globalThis) {
|
|
31
|
+
return clientRuntimeSignals(g).length > 0;
|
|
32
|
+
}
|
|
18
33
|
function resolveEnv(apiKey, networkOverride, apiBaseUrlOverride) {
|
|
19
34
|
if (typeof apiKey !== "string" || apiKey.trim() === "") {
|
|
20
|
-
throw new AuthError(
|
|
35
|
+
throw new AuthError(
|
|
36
|
+
'Missing apiKey. Expected a "pk_test_", "pk_live_", "sk_test_", or "sk_live_" prefix.'
|
|
37
|
+
);
|
|
21
38
|
}
|
|
22
39
|
const isTest = apiKey.startsWith("pk_test_") || apiKey.startsWith("sk_test_");
|
|
23
40
|
const isLive = apiKey.startsWith("pk_live_") || apiKey.startsWith("sk_live_");
|
|
@@ -27,12 +44,14 @@ function resolveEnv(apiKey, networkOverride, apiBaseUrlOverride) {
|
|
|
27
44
|
{ keyPreview: apiKey.slice(0, 8) }
|
|
28
45
|
);
|
|
29
46
|
}
|
|
47
|
+
const isSecret = apiKey.startsWith("sk_");
|
|
30
48
|
const network = networkOverride ?? (isTest ? "base-sepolia" : "base");
|
|
31
49
|
return {
|
|
32
50
|
network,
|
|
33
51
|
chainId: CHAIN_ID[network],
|
|
34
52
|
apiBaseUrl: apiBaseUrlOverride ?? DEFAULT_API_BASE_URL[network],
|
|
35
|
-
isTest
|
|
53
|
+
isTest,
|
|
54
|
+
isSecret
|
|
36
55
|
};
|
|
37
56
|
}
|
|
38
57
|
|
|
@@ -340,13 +359,6 @@ var erc20Abi = [
|
|
|
340
359
|
{ name: "amount", type: "uint256" }
|
|
341
360
|
],
|
|
342
361
|
outputs: [{ type: "bool" }]
|
|
343
|
-
},
|
|
344
|
-
{
|
|
345
|
-
type: "function",
|
|
346
|
-
name: "balanceOf",
|
|
347
|
-
stateMutability: "view",
|
|
348
|
-
inputs: [{ name: "account", type: "address" }],
|
|
349
|
-
outputs: [{ type: "uint256" }]
|
|
350
362
|
}
|
|
351
363
|
];
|
|
352
364
|
var playmosPayAbi = [
|
|
@@ -363,17 +375,6 @@ var playmosPayAbi = [
|
|
|
363
375
|
}
|
|
364
376
|
];
|
|
365
377
|
var prizePoolAbi = [
|
|
366
|
-
{
|
|
367
|
-
type: "function",
|
|
368
|
-
name: "openRound",
|
|
369
|
-
stateMutability: "nonpayable",
|
|
370
|
-
inputs: [
|
|
371
|
-
{ name: "roundId", type: "bytes32" },
|
|
372
|
-
{ name: "series", type: "bytes32" },
|
|
373
|
-
{ name: "entry", type: "uint256" }
|
|
374
|
-
],
|
|
375
|
-
outputs: []
|
|
376
|
-
},
|
|
377
378
|
{
|
|
378
379
|
type: "function",
|
|
379
380
|
name: "enter",
|
|
@@ -384,50 +385,6 @@ var prizePoolAbi = [
|
|
|
384
385
|
],
|
|
385
386
|
outputs: []
|
|
386
387
|
},
|
|
387
|
-
{
|
|
388
|
-
type: "function",
|
|
389
|
-
name: "lockRound",
|
|
390
|
-
stateMutability: "nonpayable",
|
|
391
|
-
inputs: [{ name: "roundId", type: "bytes32" }],
|
|
392
|
-
outputs: []
|
|
393
|
-
},
|
|
394
|
-
{
|
|
395
|
-
type: "function",
|
|
396
|
-
name: "settle",
|
|
397
|
-
stateMutability: "nonpayable",
|
|
398
|
-
inputs: [
|
|
399
|
-
{ name: "roundId", type: "bytes32" },
|
|
400
|
-
{ name: "winners", type: "address[]" },
|
|
401
|
-
{ name: "amounts", type: "uint256[]" }
|
|
402
|
-
],
|
|
403
|
-
outputs: []
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
type: "function",
|
|
407
|
-
name: "getRound",
|
|
408
|
-
stateMutability: "view",
|
|
409
|
-
inputs: [{ name: "roundId", type: "bytes32" }],
|
|
410
|
-
outputs: [
|
|
411
|
-
{ name: "state", type: "uint8" },
|
|
412
|
-
{ name: "series", type: "bytes32" },
|
|
413
|
-
{ name: "entry", type: "uint256" },
|
|
414
|
-
{ name: "pot", type: "uint256" },
|
|
415
|
-
{ name: "entrantCount", type: "uint256" },
|
|
416
|
-
{ name: "payable_", type: "uint256" },
|
|
417
|
-
{ name: "inheritedSeed", type: "uint256" },
|
|
418
|
-
{ name: "lockedAt", type: "uint256" }
|
|
419
|
-
]
|
|
420
|
-
},
|
|
421
|
-
{
|
|
422
|
-
type: "function",
|
|
423
|
-
name: "hasEntered",
|
|
424
|
-
stateMutability: "view",
|
|
425
|
-
inputs: [
|
|
426
|
-
{ name: "roundId", type: "bytes32" },
|
|
427
|
-
{ name: "identity", type: "bytes32" }
|
|
428
|
-
],
|
|
429
|
-
outputs: [{ type: "bool" }]
|
|
430
|
-
},
|
|
431
388
|
{
|
|
432
389
|
type: "function",
|
|
433
390
|
name: "withdrawable",
|
|
@@ -551,7 +508,7 @@ function buildWithdrawCall(prizePool) {
|
|
|
551
508
|
}
|
|
552
509
|
|
|
553
510
|
// src/mock.ts
|
|
554
|
-
var MOCK_TX = "
|
|
511
|
+
var MOCK_TX = `0x${"0".repeat(56)}deadbeef`;
|
|
555
512
|
function mockIapPayment(args) {
|
|
556
513
|
const { feeMicro, netMicro } = computeIapSplit(args.amountMicro, args.feeBps);
|
|
557
514
|
return {
|
|
@@ -799,10 +756,16 @@ var Playmos = class {
|
|
|
799
756
|
}
|
|
800
757
|
};
|
|
801
758
|
this.payouts = {
|
|
802
|
-
/** Choose how the studio is paid: "usdc" (default) or "fiat" (Bridge). */
|
|
803
|
-
setMode: (mode) =>
|
|
804
|
-
|
|
805
|
-
|
|
759
|
+
/** Choose how the studio is paid: "usdc" (default) or "fiat" (Bridge). Secret key only (#264). */
|
|
760
|
+
setMode: async (mode) => {
|
|
761
|
+
this.assertSecretKey("playmos.payouts.setMode");
|
|
762
|
+
return this.http.post("/payouts/mode", { mode });
|
|
763
|
+
},
|
|
764
|
+
/** Create a Bridge KYC onboarding link (fiat payout). Secret key only (#264). */
|
|
765
|
+
createOnboardingLink: async () => {
|
|
766
|
+
this.assertSecretKey("playmos.payouts.createOnboardingLink");
|
|
767
|
+
return this.http.post("/payouts/onboarding_link", {});
|
|
768
|
+
}
|
|
806
769
|
};
|
|
807
770
|
/**
|
|
808
771
|
* `x402` — Phase 4 HTTP 402 / x402-shaped adapter (#153).
|
|
@@ -852,6 +815,10 @@ var Playmos = class {
|
|
|
852
815
|
* Settle a previously minted challenge with an `x402-payload` authorization.
|
|
853
816
|
* Defaults mode to `server-signer` when omitted (explicit on the wire — fail-closed
|
|
854
817
|
* materialize still requires mode; the route also defaults for this path).
|
|
818
|
+
*
|
|
819
|
+
* #259/#269: settle uses `idempotency-key = requirement.id` and auto-retries gateway 504.
|
|
820
|
+
* A 504 replay may return **without throwing** with `success: false`, `status: "settling"`,
|
|
821
|
+
* `txHash: null` (in-flight). Treat as non-final — re-call or verify later; do not new-key.
|
|
855
822
|
*/
|
|
856
823
|
fulfill: async (requirement, authorization) => {
|
|
857
824
|
this.assertX402Allowed();
|
|
@@ -885,10 +852,19 @@ var Playmos = class {
|
|
|
885
852
|
}
|
|
886
853
|
if (payload.scheme === void 0) payload.scheme = "exact";
|
|
887
854
|
if (payload.requirementId === void 0) payload.requirementId = requirement.id;
|
|
888
|
-
const res = await this.http.post(
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
855
|
+
const res = await this.http.post(
|
|
856
|
+
"/x402/settle",
|
|
857
|
+
{
|
|
858
|
+
requirement,
|
|
859
|
+
authorization: { kind: "x402-payload", payload }
|
|
860
|
+
},
|
|
861
|
+
// Settlement idempotency anchor = requirement.id (settlement.ts: "Settlement
|
|
862
|
+
// idempotency key + x402 challenge id"). Without this header the settle POST is
|
|
863
|
+
// excluded from the safe gateway-504 auto-retry (isRetryable), and the SDK's own
|
|
864
|
+
// 504 message ("retry with the same idempotency key (safe)") would be a lie —
|
|
865
|
+
// a 504 is exactly when the on-chain settle may have already landed (#259).
|
|
866
|
+
{ idempotencyKey: requirement.id }
|
|
867
|
+
);
|
|
892
868
|
const s = res.settlement;
|
|
893
869
|
return {
|
|
894
870
|
requirementId: s.requirementId,
|
|
@@ -929,8 +905,13 @@ var Playmos = class {
|
|
|
929
905
|
*
|
|
930
906
|
* Operator txs are signed by the Playmos service (OPERATOR_ROLE key).
|
|
931
907
|
*/
|
|
908
|
+
/** Offline mock store for rounds.open/lock/settle/get when `mock: true` (3-game dogfood). */
|
|
909
|
+
this.mockRounds = /* @__PURE__ */ new Map();
|
|
910
|
+
/** Mock withdrawable credits after settle — key `${roundId}:${walletLower}` (#240 offline prize). */
|
|
911
|
+
this.mockWithdrawable = /* @__PURE__ */ new Map();
|
|
932
912
|
this.rounds = {
|
|
933
913
|
open: async (input) => {
|
|
914
|
+
this.assertSecretKey("playmos.rounds.open");
|
|
934
915
|
requireField(input?.gameId, "gameId");
|
|
935
916
|
requireField(input?.roundId, "roundId");
|
|
936
917
|
requireField(input?.entryAmount, "entryAmount");
|
|
@@ -938,19 +919,75 @@ var Playmos = class {
|
|
|
938
919
|
throw new ConfigError("payout rule is required (winner-take-all | top-n | custom)");
|
|
939
920
|
}
|
|
940
921
|
validateAmount(input.entryAmount);
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
922
|
+
if (this.config.mock) {
|
|
923
|
+
const roundKey = input.roundKey ?? input.roundId;
|
|
924
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
925
|
+
if (existing?.status === "open" || existing?.status === "locked") {
|
|
926
|
+
return existing;
|
|
927
|
+
}
|
|
928
|
+
if (existing?.status === "settled") {
|
|
929
|
+
throw new ApiError(`round ${input.roundId} already settled`, {
|
|
930
|
+
status: 409,
|
|
931
|
+
code: "conflict"
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
const round = {
|
|
935
|
+
roundId: input.roundId,
|
|
936
|
+
gameId: input.gameId,
|
|
937
|
+
roundKey,
|
|
938
|
+
status: "open",
|
|
939
|
+
entryAmount: input.entryAmount,
|
|
940
|
+
pool: null,
|
|
941
|
+
entrants: 0,
|
|
942
|
+
payout: input.payout,
|
|
943
|
+
closeAt: input.closeAt,
|
|
944
|
+
openTxHash: MOCK_TX,
|
|
945
|
+
prizePoolAddress: this.config.contracts?.prizePool ?? "0x0000000000000000000000000000000000000002"
|
|
946
|
+
};
|
|
947
|
+
this.mockRounds.set(input.roundId, round);
|
|
948
|
+
return round;
|
|
949
|
+
}
|
|
950
|
+
const body = await this.http.post(
|
|
951
|
+
"/rounds",
|
|
952
|
+
{
|
|
953
|
+
gameId: input.gameId,
|
|
954
|
+
roundId: input.roundId,
|
|
955
|
+
entryAmount: input.entryAmount,
|
|
956
|
+
payout: input.payout,
|
|
957
|
+
closeAt: input.closeAt,
|
|
958
|
+
seriesKey: input.seriesKey,
|
|
959
|
+
roundKey: input.roundKey
|
|
960
|
+
},
|
|
961
|
+
{ acceptStatuses: [202] }
|
|
962
|
+
);
|
|
963
|
+
return body.round;
|
|
951
964
|
},
|
|
952
965
|
lock: async (input) => {
|
|
966
|
+
this.assertSecretKey("playmos.rounds.lock");
|
|
953
967
|
requireField(input?.roundId, "roundId");
|
|
968
|
+
if (this.config.mock) {
|
|
969
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
970
|
+
if (!existing) {
|
|
971
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
972
|
+
}
|
|
973
|
+
if (existing.status === "locked" || existing.status === "settled") return existing;
|
|
974
|
+
if (existing.status !== "open") {
|
|
975
|
+
throw new ApiError(`round ${input.roundId} is not open (status=${existing.status})`, {
|
|
976
|
+
status: 409,
|
|
977
|
+
code: "conflict"
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
const locked = {
|
|
981
|
+
...existing,
|
|
982
|
+
status: "locked",
|
|
983
|
+
// Mock payable pool ≈ 60% of entry × max(1, entrants) for shape only — not money truth.
|
|
984
|
+
pool: existing.pool ?? existing.entryAmount,
|
|
985
|
+
entrants: existing.entrants ?? 0,
|
|
986
|
+
lockTxHash: MOCK_TX
|
|
987
|
+
};
|
|
988
|
+
this.mockRounds.set(input.roundId, locked);
|
|
989
|
+
return locked;
|
|
990
|
+
}
|
|
954
991
|
const { round } = await this.http.post(
|
|
955
992
|
`/rounds/${encodeURIComponent(input.roundId)}/lock`,
|
|
956
993
|
{ gameId: input.gameId }
|
|
@@ -958,16 +995,137 @@ var Playmos = class {
|
|
|
958
995
|
return round;
|
|
959
996
|
},
|
|
960
997
|
settle: async (input) => {
|
|
998
|
+
this.assertSecretKey("playmos.rounds.settle");
|
|
961
999
|
requireField(input?.roundId, "roundId");
|
|
962
1000
|
if (!input?.results) throw new ConfigError("results are required (ranking or winners)");
|
|
1001
|
+
if (this.config.mock) {
|
|
1002
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1003
|
+
if (!existing) {
|
|
1004
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1005
|
+
}
|
|
1006
|
+
if (existing.status === "settled" && existing.settleTxHash) {
|
|
1007
|
+
return {
|
|
1008
|
+
roundId: input.roundId,
|
|
1009
|
+
txHash: existing.settleTxHash,
|
|
1010
|
+
poolPaid: existing.pool ?? "0",
|
|
1011
|
+
winners: "winners" in input.results ? input.results.winners : input.results.ranking.map((wallet) => ({
|
|
1012
|
+
wallet,
|
|
1013
|
+
amount: existing.pool ?? existing.entryAmount
|
|
1014
|
+
})),
|
|
1015
|
+
status: "settled"
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
if (existing.status !== "locked" && existing.status !== "open") {
|
|
1019
|
+
throw new ApiError(`round ${input.roundId} cannot settle (status=${existing.status})`, {
|
|
1020
|
+
status: 409,
|
|
1021
|
+
code: "conflict"
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
const winners = "winners" in input.results ? input.results.winners : input.results.ranking.map((wallet, i) => ({
|
|
1025
|
+
wallet,
|
|
1026
|
+
// Winner-take-all mock shape when ranking only.
|
|
1027
|
+
amount: i === 0 ? existing.pool ?? existing.entryAmount : "0"
|
|
1028
|
+
}));
|
|
1029
|
+
const settleTxHash = MOCK_TX;
|
|
1030
|
+
const poolPaid = existing.pool ?? existing.entryAmount;
|
|
1031
|
+
const settled = {
|
|
1032
|
+
...existing,
|
|
1033
|
+
status: "settled",
|
|
1034
|
+
settleTxHash,
|
|
1035
|
+
pool: poolPaid
|
|
1036
|
+
};
|
|
1037
|
+
this.mockRounds.set(input.roundId, settled);
|
|
1038
|
+
for (const w of winners) {
|
|
1039
|
+
const wAddr = String(w.wallet).toLowerCase();
|
|
1040
|
+
let micro = 0n;
|
|
1041
|
+
try {
|
|
1042
|
+
micro = validateAmount(String(w.amount ?? "0"));
|
|
1043
|
+
} catch {
|
|
1044
|
+
micro = 0n;
|
|
1045
|
+
}
|
|
1046
|
+
if (micro > 0n) {
|
|
1047
|
+
const k = `${input.roundId}:${wAddr}`;
|
|
1048
|
+
this.mockWithdrawable.set(k, (this.mockWithdrawable.get(k) ?? 0n) + micro);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
roundId: input.roundId,
|
|
1053
|
+
txHash: settleTxHash,
|
|
1054
|
+
poolPaid,
|
|
1055
|
+
winners,
|
|
1056
|
+
status: "settled"
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
963
1059
|
const { settle } = await this.http.post(
|
|
964
1060
|
`/rounds/${encodeURIComponent(input.roundId)}/settle`,
|
|
965
|
-
{ gameId: input.gameId, results: input.results }
|
|
1061
|
+
{ gameId: input.gameId, results: input.results },
|
|
1062
|
+
{ acceptStatuses: [202] }
|
|
966
1063
|
);
|
|
967
1064
|
return settle;
|
|
968
1065
|
},
|
|
1066
|
+
/**
|
|
1067
|
+
* Cancel an open/locked round (operator sk_ only). Refunds entrants' escrowed ~90%
|
|
1068
|
+
* and frees the series latch. May return `status: "cancelling"` (HTTP 202) until
|
|
1069
|
+
* chain Cancelled(4) reconciles — poll `rounds.get` or re-call cancel (#346).
|
|
1070
|
+
*/
|
|
1071
|
+
cancel: async (input) => {
|
|
1072
|
+
this.assertSecretKey("playmos.rounds.cancel");
|
|
1073
|
+
requireField(input?.roundId, "roundId");
|
|
1074
|
+
if (this.config.mock) {
|
|
1075
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1076
|
+
if (!existing) {
|
|
1077
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1078
|
+
}
|
|
1079
|
+
if (existing.status === "cancelled") {
|
|
1080
|
+
return {
|
|
1081
|
+
roundId: input.roundId,
|
|
1082
|
+
txHash: existing.cancelTxHash ?? MOCK_TX,
|
|
1083
|
+
status: "cancelled",
|
|
1084
|
+
round: existing
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
if (existing.status === "settled") {
|
|
1088
|
+
throw new ApiError(`round ${input.roundId} is already settled \u2014 cannot cancel`, {
|
|
1089
|
+
status: 409,
|
|
1090
|
+
code: "conflict"
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
const cancelled = {
|
|
1094
|
+
...existing,
|
|
1095
|
+
status: "cancelled",
|
|
1096
|
+
cancelTxHash: MOCK_TX,
|
|
1097
|
+
pool: null
|
|
1098
|
+
};
|
|
1099
|
+
this.mockRounds.set(input.roundId, cancelled);
|
|
1100
|
+
return {
|
|
1101
|
+
roundId: input.roundId,
|
|
1102
|
+
txHash: MOCK_TX,
|
|
1103
|
+
status: "cancelled",
|
|
1104
|
+
round: cancelled
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
const body = await this.http.post(
|
|
1108
|
+
`/rounds/${encodeURIComponent(input.roundId)}/cancel`,
|
|
1109
|
+
{ gameId: input.gameId },
|
|
1110
|
+
{ acceptStatuses: [202] }
|
|
1111
|
+
);
|
|
1112
|
+
const status = body.status === "cancelling" || body.round?.status === "cancelling" ? "cancelling" : "cancelled";
|
|
1113
|
+
return {
|
|
1114
|
+
roundId: input.roundId,
|
|
1115
|
+
txHash: body.txHash ?? body.round?.cancelTxHash ?? null,
|
|
1116
|
+
status,
|
|
1117
|
+
round: body.round
|
|
1118
|
+
};
|
|
1119
|
+
},
|
|
969
1120
|
get: async (input) => {
|
|
970
1121
|
requireField(input?.roundId, "roundId");
|
|
1122
|
+
if (this.config.mock) {
|
|
1123
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1124
|
+
if (!existing) {
|
|
1125
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1126
|
+
}
|
|
1127
|
+
return existing;
|
|
1128
|
+
}
|
|
971
1129
|
const { round } = await this.http.get(
|
|
972
1130
|
`/rounds/${encodeURIComponent(input.roundId)}`
|
|
973
1131
|
);
|
|
@@ -983,6 +1141,21 @@ var Playmos = class {
|
|
|
983
1141
|
prize: async (input) => {
|
|
984
1142
|
requireField(input?.roundId, "roundId");
|
|
985
1143
|
const wallet = requireAddressField(input?.wallet, "wallet");
|
|
1144
|
+
if (this.config.mock) {
|
|
1145
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1146
|
+
if (!existing) {
|
|
1147
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1148
|
+
}
|
|
1149
|
+
const claimableMicro2 = this.mockWithdrawable.get(`${input.roundId}:${wallet.toLowerCase()}`) ?? 0n;
|
|
1150
|
+
const prizePoolAddress2 = input.prizePoolAddress ?? existing.prizePoolAddress ?? "0x0000000000000000000000000000000000000002";
|
|
1151
|
+
return {
|
|
1152
|
+
roundId: input.roundId,
|
|
1153
|
+
prizePoolAddress: prizePoolAddress2,
|
|
1154
|
+
wallet,
|
|
1155
|
+
claimable: formatMicroToUsd(claimableMicro2),
|
|
1156
|
+
claimableMicro: claimableMicro2.toString()
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
986
1159
|
try {
|
|
987
1160
|
const res = await this.http.get(
|
|
988
1161
|
`/rounds/${encodeURIComponent(input.roundId)}/prize?wallet=${encodeURIComponent(wallet)}`
|
|
@@ -1009,11 +1182,24 @@ var Playmos = class {
|
|
|
1009
1182
|
*/
|
|
1010
1183
|
withdraw: async (input) => {
|
|
1011
1184
|
if (this.config.mock) {
|
|
1185
|
+
let amountMicro2 = 0n;
|
|
1186
|
+
let prizePool2 = input.prizePoolAddress ?? "0x0000000000000000000000000000000000000001";
|
|
1187
|
+
if (input.roundId) {
|
|
1188
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1189
|
+
if (existing?.prizePoolAddress) prizePool2 = existing.prizePoolAddress;
|
|
1190
|
+
for (const [k, v] of this.mockWithdrawable) {
|
|
1191
|
+
if (k.startsWith(`${input.roundId}:`) && v > 0n) {
|
|
1192
|
+
amountMicro2 = v;
|
|
1193
|
+
this.mockWithdrawable.set(k, 0n);
|
|
1194
|
+
break;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1012
1198
|
return {
|
|
1013
|
-
prizePoolAddress:
|
|
1014
|
-
amount:
|
|
1015
|
-
amountMicro:
|
|
1016
|
-
txHash:
|
|
1199
|
+
prizePoolAddress: prizePool2,
|
|
1200
|
+
amount: formatMicroToUsd(amountMicro2),
|
|
1201
|
+
amountMicro: amountMicro2.toString(),
|
|
1202
|
+
txHash: MOCK_TX,
|
|
1017
1203
|
status: "confirmed"
|
|
1018
1204
|
};
|
|
1019
1205
|
}
|
|
@@ -1088,23 +1274,27 @@ var Playmos = class {
|
|
|
1088
1274
|
this.agents = {
|
|
1089
1275
|
/** Assign (or return) the wallet for a game NPC id. Idempotent — safe to call wherever your NPCs spawn. */
|
|
1090
1276
|
createWallet: async (input) => {
|
|
1277
|
+
this.assertSecretKey("playmos.agents");
|
|
1091
1278
|
requireField(input?.agentId, "agentId");
|
|
1092
1279
|
const { agent } = await this.http.post("/agents/wallets", { agentId: input.agentId });
|
|
1093
1280
|
return agent;
|
|
1094
1281
|
},
|
|
1095
1282
|
/** Resolve one NPC's wallet by your id. */
|
|
1096
1283
|
wallet: async (agentId) => {
|
|
1284
|
+
this.assertSecretKey("playmos.agents");
|
|
1097
1285
|
requireField(agentId, "agentId");
|
|
1098
1286
|
const { agent } = await this.http.get(`/agents/wallets/${encodeURIComponent(agentId)}`);
|
|
1099
1287
|
return agent;
|
|
1100
1288
|
},
|
|
1101
1289
|
/** List the NPC wallets you've assigned in this studio. */
|
|
1102
1290
|
list: async () => {
|
|
1291
|
+
this.assertSecretKey("playmos.agents");
|
|
1103
1292
|
const { agents } = await this.http.get("/agents/wallets");
|
|
1104
1293
|
return agents;
|
|
1105
1294
|
},
|
|
1106
1295
|
/** Sandbox faucet: fund an NPC with USDC from your treasury (per-NPC lifetime cap). */
|
|
1107
|
-
fund: (input) => {
|
|
1296
|
+
fund: async (input) => {
|
|
1297
|
+
this.assertSecretKey("playmos.agents.fund");
|
|
1108
1298
|
requireField(input?.agentId, "agentId");
|
|
1109
1299
|
validateAmount(input.amount);
|
|
1110
1300
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
@@ -1115,10 +1305,17 @@ var Playmos = class {
|
|
|
1115
1305
|
);
|
|
1116
1306
|
},
|
|
1117
1307
|
/** NPC→NPC (or →player) USDC transfer, by id. A thin alias of `playmos.transfer` (which is canonical). */
|
|
1118
|
-
pay: (input) => {
|
|
1308
|
+
pay: async (input) => {
|
|
1309
|
+
this.assertSecretKey("playmos.agents.pay");
|
|
1119
1310
|
requireField(input?.from, "from");
|
|
1120
1311
|
requireField(input?.to, "to");
|
|
1121
|
-
return this.transfer({
|
|
1312
|
+
return this.transfer({
|
|
1313
|
+
from: input.from,
|
|
1314
|
+
to: input.to,
|
|
1315
|
+
amount: input.amount,
|
|
1316
|
+
feeBps: input.feeBps,
|
|
1317
|
+
feeSink: input.feeSink
|
|
1318
|
+
});
|
|
1122
1319
|
}
|
|
1123
1320
|
};
|
|
1124
1321
|
/**
|
|
@@ -1302,6 +1499,15 @@ var Playmos = class {
|
|
|
1302
1499
|
};
|
|
1303
1500
|
this.config = config;
|
|
1304
1501
|
this.env = resolveEnv(config.apiKey, config.network, config.apiBaseUrl);
|
|
1502
|
+
if (this.env.isSecret) {
|
|
1503
|
+
const signals = clientRuntimeSignals();
|
|
1504
|
+
if (signals.length > 0) {
|
|
1505
|
+
throw new AuthError(
|
|
1506
|
+
"Secret (sk_) key used in a client runtime \u2014 sk_ keys are server-only; use a pk_ key client-side.",
|
|
1507
|
+
{ isSecret: true, clientRuntime: true, signals }
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1305
1511
|
this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
|
|
1306
1512
|
}
|
|
1307
1513
|
/** Connect the player's wallet and return their address. */
|
|
@@ -1329,15 +1535,17 @@ var Playmos = class {
|
|
|
1329
1535
|
roundId: req.roundKey,
|
|
1330
1536
|
roundKey: req.roundKey,
|
|
1331
1537
|
identity: req.identity,
|
|
1332
|
-
|
|
1333
|
-
|
|
1538
|
+
playerId: req.wallet,
|
|
1539
|
+
amount: formatMicroToUsd(req.entryUnits)
|
|
1334
1540
|
});
|
|
1335
|
-
|
|
1541
|
+
this.rememberMock(entry);
|
|
1542
|
+
const raw = String(entry.status ?? "pending").toLowerCase();
|
|
1543
|
+
const status = raw === "confirmed" ? "confirmed" : raw === "failed" ? "failed" : "pending";
|
|
1336
1544
|
const onchain = !entry.mock;
|
|
1337
1545
|
return {
|
|
1338
1546
|
paymentId: entry.id,
|
|
1339
1547
|
status,
|
|
1340
|
-
txHash: entry.txHash,
|
|
1548
|
+
...entry.txHash ? { txHash: entry.txHash } : {},
|
|
1341
1549
|
onchain,
|
|
1342
1550
|
identity: entry.identity ?? req.identity
|
|
1343
1551
|
};
|
|
@@ -1370,14 +1578,39 @@ var Playmos = class {
|
|
|
1370
1578
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1371
1579
|
const res = await this.http.post(
|
|
1372
1580
|
"/payments",
|
|
1373
|
-
{
|
|
1374
|
-
|
|
1581
|
+
{
|
|
1582
|
+
kind: "iap",
|
|
1583
|
+
amount: input.amount,
|
|
1584
|
+
sku: input.sku,
|
|
1585
|
+
playerId: input.playerId,
|
|
1586
|
+
gameId: input.gameId,
|
|
1587
|
+
studio: input.studio,
|
|
1588
|
+
metadata,
|
|
1589
|
+
settle: "server",
|
|
1590
|
+
idempotencyKey
|
|
1591
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1592
|
+
},
|
|
1593
|
+
{ idempotencyKey, acceptStatuses: [201, 202] }
|
|
1375
1594
|
);
|
|
1376
|
-
|
|
1595
|
+
let payment = this.mapServerPayment(res.payment, "iap", amountMicro);
|
|
1596
|
+
if (payment.status === "pending" || payment.status === "created") {
|
|
1597
|
+
payment = await this.waitForServerSettle(payment.id, "iap", amountMicro);
|
|
1598
|
+
}
|
|
1599
|
+
return payment;
|
|
1377
1600
|
}
|
|
1378
1601
|
const intent = await this.http.post(
|
|
1379
1602
|
"/payments",
|
|
1380
|
-
{
|
|
1603
|
+
{
|
|
1604
|
+
kind: "iap",
|
|
1605
|
+
amount: input.amount,
|
|
1606
|
+
sku: input.sku,
|
|
1607
|
+
playerId: input.playerId,
|
|
1608
|
+
gameId: input.gameId,
|
|
1609
|
+
studio: input.studio,
|
|
1610
|
+
metadata,
|
|
1611
|
+
idempotencyKey
|
|
1612
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1613
|
+
},
|
|
1381
1614
|
{ idempotencyKey }
|
|
1382
1615
|
);
|
|
1383
1616
|
const provider = resolveProvider(this.config.wallet);
|
|
@@ -1426,23 +1659,48 @@ var Playmos = class {
|
|
|
1426
1659
|
);
|
|
1427
1660
|
}
|
|
1428
1661
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1662
|
+
const pinnedRoundKey = input.roundKey ?? input.roundId;
|
|
1429
1663
|
const res = await this.http.post(
|
|
1430
1664
|
"/payments",
|
|
1431
|
-
{
|
|
1432
|
-
|
|
1665
|
+
{
|
|
1666
|
+
kind: "entry",
|
|
1667
|
+
amount: input.amount,
|
|
1668
|
+
gameId: input.gameId,
|
|
1669
|
+
roundId: pinnedRoundKey,
|
|
1670
|
+
identity: input.identity,
|
|
1671
|
+
playerId: input.playerId,
|
|
1672
|
+
metadata,
|
|
1673
|
+
settle: "server",
|
|
1674
|
+
idempotencyKey
|
|
1675
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1676
|
+
},
|
|
1677
|
+
{ idempotencyKey, acceptStatuses: [201, 202] }
|
|
1433
1678
|
);
|
|
1434
|
-
|
|
1679
|
+
let payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
|
|
1680
|
+
if (payment2.status === "pending" || payment2.status === "created") {
|
|
1681
|
+
payment2 = await this.waitForServerSettle(payment2.id, "entry", amountMicro);
|
|
1682
|
+
}
|
|
1435
1683
|
if (input.identity) payment2.identity = input.identity;
|
|
1436
1684
|
const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
1437
1685
|
if (pool) payment2.prizePoolAddress = pool.toLowerCase();
|
|
1438
|
-
payment2.roundKey =
|
|
1686
|
+
payment2.roundKey = input.roundKey ?? res.clientParams?.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
|
|
1439
1687
|
return payment2;
|
|
1440
1688
|
}
|
|
1441
1689
|
const intent = await this.http.post(
|
|
1442
1690
|
"/payments",
|
|
1443
1691
|
// When the game supplies its own roundKey, that IS the round the service
|
|
1444
1692
|
// stores + verifies against (so on-chain enter and server hasEntered align).
|
|
1445
|
-
{
|
|
1693
|
+
{
|
|
1694
|
+
kind: "entry",
|
|
1695
|
+
amount: input.amount,
|
|
1696
|
+
gameId: input.gameId,
|
|
1697
|
+
roundId: input.roundKey ?? input.roundId,
|
|
1698
|
+
identity: input.identity,
|
|
1699
|
+
playerId: input.playerId,
|
|
1700
|
+
metadata,
|
|
1701
|
+
idempotencyKey
|
|
1702
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1703
|
+
},
|
|
1446
1704
|
{ idempotencyKey }
|
|
1447
1705
|
);
|
|
1448
1706
|
const provider = resolveProvider(this.config.wallet);
|
|
@@ -1485,6 +1743,7 @@ var Playmos = class {
|
|
|
1485
1743
|
* NPC `from` requires `sk_test_` and settles gaslessly (NPC signs; service relays).
|
|
1486
1744
|
*/
|
|
1487
1745
|
async transfer(input, opts) {
|
|
1746
|
+
this.assertSecretKey("playmos.transfer");
|
|
1488
1747
|
const amountMicro = validateAmount(input.amount);
|
|
1489
1748
|
const from = input.from === void 0 ? void 0 : partyRef(input.from, "from");
|
|
1490
1749
|
const to = partyRef(input.to, "to");
|
|
@@ -1554,16 +1813,55 @@ var Playmos = class {
|
|
|
1554
1813
|
}
|
|
1555
1814
|
// ---- internals ---------------------------------------------------------
|
|
1556
1815
|
/**
|
|
1557
|
-
*
|
|
1558
|
-
*
|
|
1816
|
+
* Secret (sk_) surfaces — agents / transfer / x402. Capability tracks key type
|
|
1817
|
+
* in code, not README prose (#260). pk_ keys are client-safe and refuse these.
|
|
1818
|
+
*/
|
|
1819
|
+
assertSecretKey(surface) {
|
|
1820
|
+
if (!this.env.isSecret) {
|
|
1821
|
+
throw new AuthError(
|
|
1822
|
+
`${surface} requires a secret (sk_) key \u2014 use sk_test_\u2026 / sk_live_\u2026 on the server. Publishable (pk_) keys are client-safe and cannot call this surface.`,
|
|
1823
|
+
{ surface, isSecret: false }
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* x402 V1 is Base Sepolia + secret test keys only (ADR D5 + #260). Fail closed
|
|
1829
|
+
* before any network call so mainnet / live / publishable keys never hit the adapter.
|
|
1830
|
+
* Does not change fulfill/settle body shape (A owns #261 / #259 money path).
|
|
1559
1831
|
*/
|
|
1560
1832
|
assertX402Allowed() {
|
|
1833
|
+
this.assertSecretKey("playmos.x402");
|
|
1561
1834
|
if (!this.env.isTest || this.env.network !== "base-sepolia") {
|
|
1562
1835
|
throw new ConfigError(
|
|
1563
|
-
'playmos.x402 is Base Sepolia /
|
|
1564
|
-
{ network: this.env.network, isTest: this.env.isTest }
|
|
1836
|
+
'playmos.x402 is Base Sepolia / sk_test_ only in V1 (ADR D5). Use apiKey sk_test_\u2026 and network "base-sepolia".',
|
|
1837
|
+
{ network: this.env.network, isTest: this.env.isTest, isSecret: this.env.isSecret }
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Poll GET /payments/:id after server-settle until terminal status.
|
|
1843
|
+
* - Entry (#221): service may return 202 pending (openRound+enter > DO edge).
|
|
1844
|
+
* - IAP: service often returns 201 + txHash while status is still "created"
|
|
1845
|
+
* until chain verify catches PaymentSettled (RPC lag) — poll so pay() matches
|
|
1846
|
+
* the documented "confirmed" sandbox promise (TTFSC-T0 / cold-run).
|
|
1847
|
+
*/
|
|
1848
|
+
async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4) {
|
|
1849
|
+
const start = Date.now();
|
|
1850
|
+
let delayMs = 400;
|
|
1851
|
+
while (Date.now() - start < timeoutMs) {
|
|
1852
|
+
const raw = await this.http.get(
|
|
1853
|
+
`/payments/${encodeURIComponent(paymentId)}`
|
|
1565
1854
|
);
|
|
1855
|
+
if (raw.status === "confirmed" || raw.status === "failed") {
|
|
1856
|
+
return this.mapServerPayment(raw, kind, amountMicro);
|
|
1857
|
+
}
|
|
1858
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
1859
|
+
delayMs = Math.min(Math.floor(delayMs * 1.4), 2e3);
|
|
1566
1860
|
}
|
|
1861
|
+
throw new ApiError(
|
|
1862
|
+
`Timed out after ${timeoutMs}ms waiting for server-settle of ${paymentId}. Re-check with playmos.verify("${paymentId}").`,
|
|
1863
|
+
{ paymentId, timeout: true, asyncSettle: true }
|
|
1864
|
+
);
|
|
1567
1865
|
}
|
|
1568
1866
|
/**
|
|
1569
1867
|
* Map a server-settled payment (from the `settle: "server"` response) into the
|
|
@@ -1936,6 +2234,4 @@ function isX402PayloadAuthorization(auth) {
|
|
|
1936
2234
|
return auth.kind === "x402-payload";
|
|
1937
2235
|
}
|
|
1938
2236
|
|
|
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 };
|
|
1940
|
-
//# sourceMappingURL=index.js.map
|
|
1941
|
-
//# sourceMappingURL=index.js.map
|
|
2237
|
+
export { CHAIN_ID, DEFAULT_API_BASE_URL, MICRO_PER_USDC, PayoutError, Playmos, USDC_ADDRESS, USDC_DECIMALS, clientRuntimeSignals, computeIapSplit, computePayout, computePoolSplit, createPaymentRequirement, createX402Challenge, decodePaymentHeader, encodePaymentHeader, formatMicroToUsd, isClientRuntime, isWalletSignatureAuthorization, isX402PayloadAuthorization, networkToCaip2, parsePaymentRequirement, parseUsdToMicro, prefixedId, previewEscrowFee, previewIapSplit, previewMarketplaceSplit, previewPoolSplit, previewTransferSplit, resolveEnv, serializePaymentRequirement, toX402PaymentRequired, ulid, validateX402ChallengeInput };
|