@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.cjs
CHANGED
|
@@ -85,9 +85,26 @@ var DEFAULT_API_BASE_URL = {
|
|
|
85
85
|
base: "https://api.playmos.io/v1",
|
|
86
86
|
"base-sepolia": "https://api.sandbox.playmos.io/v1"
|
|
87
87
|
};
|
|
88
|
+
function clientRuntimeSignals(g = globalThis) {
|
|
89
|
+
const any = g;
|
|
90
|
+
const signals = [];
|
|
91
|
+
if (typeof any.window !== "undefined") signals.push("window");
|
|
92
|
+
if (typeof any.document !== "undefined") signals.push("document");
|
|
93
|
+
const likelyCfWorker = typeof any.Cloudflare !== "undefined" || typeof any.WebSocketPair !== "undefined" && typeof any.window === "undefined";
|
|
94
|
+
if (typeof any.importScripts === "function" && !likelyCfWorker) {
|
|
95
|
+
signals.push("importScripts");
|
|
96
|
+
}
|
|
97
|
+
if (any.navigator?.product === "ReactNative") signals.push("ReactNative");
|
|
98
|
+
return signals;
|
|
99
|
+
}
|
|
100
|
+
function isClientRuntime(g = globalThis) {
|
|
101
|
+
return clientRuntimeSignals(g).length > 0;
|
|
102
|
+
}
|
|
88
103
|
function resolveEnv(apiKey, networkOverride, apiBaseUrlOverride) {
|
|
89
104
|
if (typeof apiKey !== "string" || apiKey.trim() === "") {
|
|
90
|
-
throw new AuthError(
|
|
105
|
+
throw new AuthError(
|
|
106
|
+
'Missing apiKey. Expected a "pk_test_", "pk_live_", "sk_test_", or "sk_live_" prefix.'
|
|
107
|
+
);
|
|
91
108
|
}
|
|
92
109
|
const isTest = apiKey.startsWith("pk_test_") || apiKey.startsWith("sk_test_");
|
|
93
110
|
const isLive = apiKey.startsWith("pk_live_") || apiKey.startsWith("sk_live_");
|
|
@@ -97,12 +114,14 @@ function resolveEnv(apiKey, networkOverride, apiBaseUrlOverride) {
|
|
|
97
114
|
{ keyPreview: apiKey.slice(0, 8) }
|
|
98
115
|
);
|
|
99
116
|
}
|
|
117
|
+
const isSecret = apiKey.startsWith("sk_");
|
|
100
118
|
const network = networkOverride ?? (isTest ? "base-sepolia" : "base");
|
|
101
119
|
return {
|
|
102
120
|
network,
|
|
103
121
|
chainId: CHAIN_ID[network],
|
|
104
122
|
apiBaseUrl: apiBaseUrlOverride ?? DEFAULT_API_BASE_URL[network],
|
|
105
|
-
isTest
|
|
123
|
+
isTest,
|
|
124
|
+
isSecret
|
|
106
125
|
};
|
|
107
126
|
}
|
|
108
127
|
|
|
@@ -410,13 +429,6 @@ var erc20Abi = [
|
|
|
410
429
|
{ name: "amount", type: "uint256" }
|
|
411
430
|
],
|
|
412
431
|
outputs: [{ type: "bool" }]
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
type: "function",
|
|
416
|
-
name: "balanceOf",
|
|
417
|
-
stateMutability: "view",
|
|
418
|
-
inputs: [{ name: "account", type: "address" }],
|
|
419
|
-
outputs: [{ type: "uint256" }]
|
|
420
432
|
}
|
|
421
433
|
];
|
|
422
434
|
var playmosPayAbi = [
|
|
@@ -433,17 +445,6 @@ var playmosPayAbi = [
|
|
|
433
445
|
}
|
|
434
446
|
];
|
|
435
447
|
var prizePoolAbi = [
|
|
436
|
-
{
|
|
437
|
-
type: "function",
|
|
438
|
-
name: "openRound",
|
|
439
|
-
stateMutability: "nonpayable",
|
|
440
|
-
inputs: [
|
|
441
|
-
{ name: "roundId", type: "bytes32" },
|
|
442
|
-
{ name: "series", type: "bytes32" },
|
|
443
|
-
{ name: "entry", type: "uint256" }
|
|
444
|
-
],
|
|
445
|
-
outputs: []
|
|
446
|
-
},
|
|
447
448
|
{
|
|
448
449
|
type: "function",
|
|
449
450
|
name: "enter",
|
|
@@ -454,50 +455,6 @@ var prizePoolAbi = [
|
|
|
454
455
|
],
|
|
455
456
|
outputs: []
|
|
456
457
|
},
|
|
457
|
-
{
|
|
458
|
-
type: "function",
|
|
459
|
-
name: "lockRound",
|
|
460
|
-
stateMutability: "nonpayable",
|
|
461
|
-
inputs: [{ name: "roundId", type: "bytes32" }],
|
|
462
|
-
outputs: []
|
|
463
|
-
},
|
|
464
|
-
{
|
|
465
|
-
type: "function",
|
|
466
|
-
name: "settle",
|
|
467
|
-
stateMutability: "nonpayable",
|
|
468
|
-
inputs: [
|
|
469
|
-
{ name: "roundId", type: "bytes32" },
|
|
470
|
-
{ name: "winners", type: "address[]" },
|
|
471
|
-
{ name: "amounts", type: "uint256[]" }
|
|
472
|
-
],
|
|
473
|
-
outputs: []
|
|
474
|
-
},
|
|
475
|
-
{
|
|
476
|
-
type: "function",
|
|
477
|
-
name: "getRound",
|
|
478
|
-
stateMutability: "view",
|
|
479
|
-
inputs: [{ name: "roundId", type: "bytes32" }],
|
|
480
|
-
outputs: [
|
|
481
|
-
{ name: "state", type: "uint8" },
|
|
482
|
-
{ name: "series", type: "bytes32" },
|
|
483
|
-
{ name: "entry", type: "uint256" },
|
|
484
|
-
{ name: "pot", type: "uint256" },
|
|
485
|
-
{ name: "entrantCount", type: "uint256" },
|
|
486
|
-
{ name: "payable_", type: "uint256" },
|
|
487
|
-
{ name: "inheritedSeed", type: "uint256" },
|
|
488
|
-
{ name: "lockedAt", type: "uint256" }
|
|
489
|
-
]
|
|
490
|
-
},
|
|
491
|
-
{
|
|
492
|
-
type: "function",
|
|
493
|
-
name: "hasEntered",
|
|
494
|
-
stateMutability: "view",
|
|
495
|
-
inputs: [
|
|
496
|
-
{ name: "roundId", type: "bytes32" },
|
|
497
|
-
{ name: "identity", type: "bytes32" }
|
|
498
|
-
],
|
|
499
|
-
outputs: [{ type: "bool" }]
|
|
500
|
-
},
|
|
501
458
|
{
|
|
502
459
|
type: "function",
|
|
503
460
|
name: "withdrawable",
|
|
@@ -621,7 +578,7 @@ function buildWithdrawCall(prizePool) {
|
|
|
621
578
|
}
|
|
622
579
|
|
|
623
580
|
// src/mock.ts
|
|
624
|
-
var MOCK_TX = "
|
|
581
|
+
var MOCK_TX = `0x${"0".repeat(56)}deadbeef`;
|
|
625
582
|
function mockIapPayment(args) {
|
|
626
583
|
const { feeMicro, netMicro } = computeIapSplit(args.amountMicro, args.feeBps);
|
|
627
584
|
return {
|
|
@@ -869,10 +826,16 @@ var Playmos = class {
|
|
|
869
826
|
}
|
|
870
827
|
};
|
|
871
828
|
this.payouts = {
|
|
872
|
-
/** Choose how the studio is paid: "usdc" (default) or "fiat" (Bridge). */
|
|
873
|
-
setMode: (mode) =>
|
|
874
|
-
|
|
875
|
-
|
|
829
|
+
/** Choose how the studio is paid: "usdc" (default) or "fiat" (Bridge). Secret key only (#264). */
|
|
830
|
+
setMode: async (mode) => {
|
|
831
|
+
this.assertSecretKey("playmos.payouts.setMode");
|
|
832
|
+
return this.http.post("/payouts/mode", { mode });
|
|
833
|
+
},
|
|
834
|
+
/** Create a Bridge KYC onboarding link (fiat payout). Secret key only (#264). */
|
|
835
|
+
createOnboardingLink: async () => {
|
|
836
|
+
this.assertSecretKey("playmos.payouts.createOnboardingLink");
|
|
837
|
+
return this.http.post("/payouts/onboarding_link", {});
|
|
838
|
+
}
|
|
876
839
|
};
|
|
877
840
|
/**
|
|
878
841
|
* `x402` — Phase 4 HTTP 402 / x402-shaped adapter (#153).
|
|
@@ -922,6 +885,10 @@ var Playmos = class {
|
|
|
922
885
|
* Settle a previously minted challenge with an `x402-payload` authorization.
|
|
923
886
|
* Defaults mode to `server-signer` when omitted (explicit on the wire — fail-closed
|
|
924
887
|
* materialize still requires mode; the route also defaults for this path).
|
|
888
|
+
*
|
|
889
|
+
* #259/#269: settle uses `idempotency-key = requirement.id` and auto-retries gateway 504.
|
|
890
|
+
* A 504 replay may return **without throwing** with `success: false`, `status: "settling"`,
|
|
891
|
+
* `txHash: null` (in-flight). Treat as non-final — re-call or verify later; do not new-key.
|
|
925
892
|
*/
|
|
926
893
|
fulfill: async (requirement, authorization) => {
|
|
927
894
|
this.assertX402Allowed();
|
|
@@ -955,10 +922,19 @@ var Playmos = class {
|
|
|
955
922
|
}
|
|
956
923
|
if (payload.scheme === void 0) payload.scheme = "exact";
|
|
957
924
|
if (payload.requirementId === void 0) payload.requirementId = requirement.id;
|
|
958
|
-
const res = await this.http.post(
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
925
|
+
const res = await this.http.post(
|
|
926
|
+
"/x402/settle",
|
|
927
|
+
{
|
|
928
|
+
requirement,
|
|
929
|
+
authorization: { kind: "x402-payload", payload }
|
|
930
|
+
},
|
|
931
|
+
// Settlement idempotency anchor = requirement.id (settlement.ts: "Settlement
|
|
932
|
+
// idempotency key + x402 challenge id"). Without this header the settle POST is
|
|
933
|
+
// excluded from the safe gateway-504 auto-retry (isRetryable), and the SDK's own
|
|
934
|
+
// 504 message ("retry with the same idempotency key (safe)") would be a lie —
|
|
935
|
+
// a 504 is exactly when the on-chain settle may have already landed (#259).
|
|
936
|
+
{ idempotencyKey: requirement.id }
|
|
937
|
+
);
|
|
962
938
|
const s = res.settlement;
|
|
963
939
|
return {
|
|
964
940
|
requirementId: s.requirementId,
|
|
@@ -999,8 +975,13 @@ var Playmos = class {
|
|
|
999
975
|
*
|
|
1000
976
|
* Operator txs are signed by the Playmos service (OPERATOR_ROLE key).
|
|
1001
977
|
*/
|
|
978
|
+
/** Offline mock store for rounds.open/lock/settle/get when `mock: true` (3-game dogfood). */
|
|
979
|
+
this.mockRounds = /* @__PURE__ */ new Map();
|
|
980
|
+
/** Mock withdrawable credits after settle — key `${roundId}:${walletLower}` (#240 offline prize). */
|
|
981
|
+
this.mockWithdrawable = /* @__PURE__ */ new Map();
|
|
1002
982
|
this.rounds = {
|
|
1003
983
|
open: async (input) => {
|
|
984
|
+
this.assertSecretKey("playmos.rounds.open");
|
|
1004
985
|
requireField(input?.gameId, "gameId");
|
|
1005
986
|
requireField(input?.roundId, "roundId");
|
|
1006
987
|
requireField(input?.entryAmount, "entryAmount");
|
|
@@ -1008,19 +989,75 @@ var Playmos = class {
|
|
|
1008
989
|
throw new ConfigError("payout rule is required (winner-take-all | top-n | custom)");
|
|
1009
990
|
}
|
|
1010
991
|
validateAmount(input.entryAmount);
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
992
|
+
if (this.config.mock) {
|
|
993
|
+
const roundKey = input.roundKey ?? input.roundId;
|
|
994
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
995
|
+
if (existing?.status === "open" || existing?.status === "locked") {
|
|
996
|
+
return existing;
|
|
997
|
+
}
|
|
998
|
+
if (existing?.status === "settled") {
|
|
999
|
+
throw new ApiError(`round ${input.roundId} already settled`, {
|
|
1000
|
+
status: 409,
|
|
1001
|
+
code: "conflict"
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
const round = {
|
|
1005
|
+
roundId: input.roundId,
|
|
1006
|
+
gameId: input.gameId,
|
|
1007
|
+
roundKey,
|
|
1008
|
+
status: "open",
|
|
1009
|
+
entryAmount: input.entryAmount,
|
|
1010
|
+
pool: null,
|
|
1011
|
+
entrants: 0,
|
|
1012
|
+
payout: input.payout,
|
|
1013
|
+
closeAt: input.closeAt,
|
|
1014
|
+
openTxHash: MOCK_TX,
|
|
1015
|
+
prizePoolAddress: this.config.contracts?.prizePool ?? "0x0000000000000000000000000000000000000002"
|
|
1016
|
+
};
|
|
1017
|
+
this.mockRounds.set(input.roundId, round);
|
|
1018
|
+
return round;
|
|
1019
|
+
}
|
|
1020
|
+
const body = await this.http.post(
|
|
1021
|
+
"/rounds",
|
|
1022
|
+
{
|
|
1023
|
+
gameId: input.gameId,
|
|
1024
|
+
roundId: input.roundId,
|
|
1025
|
+
entryAmount: input.entryAmount,
|
|
1026
|
+
payout: input.payout,
|
|
1027
|
+
closeAt: input.closeAt,
|
|
1028
|
+
seriesKey: input.seriesKey,
|
|
1029
|
+
roundKey: input.roundKey
|
|
1030
|
+
},
|
|
1031
|
+
{ acceptStatuses: [202] }
|
|
1032
|
+
);
|
|
1033
|
+
return body.round;
|
|
1021
1034
|
},
|
|
1022
1035
|
lock: async (input) => {
|
|
1036
|
+
this.assertSecretKey("playmos.rounds.lock");
|
|
1023
1037
|
requireField(input?.roundId, "roundId");
|
|
1038
|
+
if (this.config.mock) {
|
|
1039
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1040
|
+
if (!existing) {
|
|
1041
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1042
|
+
}
|
|
1043
|
+
if (existing.status === "locked" || existing.status === "settled") return existing;
|
|
1044
|
+
if (existing.status !== "open") {
|
|
1045
|
+
throw new ApiError(`round ${input.roundId} is not open (status=${existing.status})`, {
|
|
1046
|
+
status: 409,
|
|
1047
|
+
code: "conflict"
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
const locked = {
|
|
1051
|
+
...existing,
|
|
1052
|
+
status: "locked",
|
|
1053
|
+
// Mock payable pool ≈ 60% of entry × max(1, entrants) for shape only — not money truth.
|
|
1054
|
+
pool: existing.pool ?? existing.entryAmount,
|
|
1055
|
+
entrants: existing.entrants ?? 0,
|
|
1056
|
+
lockTxHash: MOCK_TX
|
|
1057
|
+
};
|
|
1058
|
+
this.mockRounds.set(input.roundId, locked);
|
|
1059
|
+
return locked;
|
|
1060
|
+
}
|
|
1024
1061
|
const { round } = await this.http.post(
|
|
1025
1062
|
`/rounds/${encodeURIComponent(input.roundId)}/lock`,
|
|
1026
1063
|
{ gameId: input.gameId }
|
|
@@ -1028,16 +1065,137 @@ var Playmos = class {
|
|
|
1028
1065
|
return round;
|
|
1029
1066
|
},
|
|
1030
1067
|
settle: async (input) => {
|
|
1068
|
+
this.assertSecretKey("playmos.rounds.settle");
|
|
1031
1069
|
requireField(input?.roundId, "roundId");
|
|
1032
1070
|
if (!input?.results) throw new ConfigError("results are required (ranking or winners)");
|
|
1071
|
+
if (this.config.mock) {
|
|
1072
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1073
|
+
if (!existing) {
|
|
1074
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1075
|
+
}
|
|
1076
|
+
if (existing.status === "settled" && existing.settleTxHash) {
|
|
1077
|
+
return {
|
|
1078
|
+
roundId: input.roundId,
|
|
1079
|
+
txHash: existing.settleTxHash,
|
|
1080
|
+
poolPaid: existing.pool ?? "0",
|
|
1081
|
+
winners: "winners" in input.results ? input.results.winners : input.results.ranking.map((wallet) => ({
|
|
1082
|
+
wallet,
|
|
1083
|
+
amount: existing.pool ?? existing.entryAmount
|
|
1084
|
+
})),
|
|
1085
|
+
status: "settled"
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
if (existing.status !== "locked" && existing.status !== "open") {
|
|
1089
|
+
throw new ApiError(`round ${input.roundId} cannot settle (status=${existing.status})`, {
|
|
1090
|
+
status: 409,
|
|
1091
|
+
code: "conflict"
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
const winners = "winners" in input.results ? input.results.winners : input.results.ranking.map((wallet, i) => ({
|
|
1095
|
+
wallet,
|
|
1096
|
+
// Winner-take-all mock shape when ranking only.
|
|
1097
|
+
amount: i === 0 ? existing.pool ?? existing.entryAmount : "0"
|
|
1098
|
+
}));
|
|
1099
|
+
const settleTxHash = MOCK_TX;
|
|
1100
|
+
const poolPaid = existing.pool ?? existing.entryAmount;
|
|
1101
|
+
const settled = {
|
|
1102
|
+
...existing,
|
|
1103
|
+
status: "settled",
|
|
1104
|
+
settleTxHash,
|
|
1105
|
+
pool: poolPaid
|
|
1106
|
+
};
|
|
1107
|
+
this.mockRounds.set(input.roundId, settled);
|
|
1108
|
+
for (const w of winners) {
|
|
1109
|
+
const wAddr = String(w.wallet).toLowerCase();
|
|
1110
|
+
let micro = 0n;
|
|
1111
|
+
try {
|
|
1112
|
+
micro = validateAmount(String(w.amount ?? "0"));
|
|
1113
|
+
} catch {
|
|
1114
|
+
micro = 0n;
|
|
1115
|
+
}
|
|
1116
|
+
if (micro > 0n) {
|
|
1117
|
+
const k = `${input.roundId}:${wAddr}`;
|
|
1118
|
+
this.mockWithdrawable.set(k, (this.mockWithdrawable.get(k) ?? 0n) + micro);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
roundId: input.roundId,
|
|
1123
|
+
txHash: settleTxHash,
|
|
1124
|
+
poolPaid,
|
|
1125
|
+
winners,
|
|
1126
|
+
status: "settled"
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1033
1129
|
const { settle } = await this.http.post(
|
|
1034
1130
|
`/rounds/${encodeURIComponent(input.roundId)}/settle`,
|
|
1035
|
-
{ gameId: input.gameId, results: input.results }
|
|
1131
|
+
{ gameId: input.gameId, results: input.results },
|
|
1132
|
+
{ acceptStatuses: [202] }
|
|
1036
1133
|
);
|
|
1037
1134
|
return settle;
|
|
1038
1135
|
},
|
|
1136
|
+
/**
|
|
1137
|
+
* Cancel an open/locked round (operator sk_ only). Refunds entrants' escrowed ~90%
|
|
1138
|
+
* and frees the series latch. May return `status: "cancelling"` (HTTP 202) until
|
|
1139
|
+
* chain Cancelled(4) reconciles — poll `rounds.get` or re-call cancel (#346).
|
|
1140
|
+
*/
|
|
1141
|
+
cancel: async (input) => {
|
|
1142
|
+
this.assertSecretKey("playmos.rounds.cancel");
|
|
1143
|
+
requireField(input?.roundId, "roundId");
|
|
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
|
+
if (existing.status === "cancelled") {
|
|
1150
|
+
return {
|
|
1151
|
+
roundId: input.roundId,
|
|
1152
|
+
txHash: existing.cancelTxHash ?? MOCK_TX,
|
|
1153
|
+
status: "cancelled",
|
|
1154
|
+
round: existing
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
if (existing.status === "settled") {
|
|
1158
|
+
throw new ApiError(`round ${input.roundId} is already settled \u2014 cannot cancel`, {
|
|
1159
|
+
status: 409,
|
|
1160
|
+
code: "conflict"
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
const cancelled = {
|
|
1164
|
+
...existing,
|
|
1165
|
+
status: "cancelled",
|
|
1166
|
+
cancelTxHash: MOCK_TX,
|
|
1167
|
+
pool: null
|
|
1168
|
+
};
|
|
1169
|
+
this.mockRounds.set(input.roundId, cancelled);
|
|
1170
|
+
return {
|
|
1171
|
+
roundId: input.roundId,
|
|
1172
|
+
txHash: MOCK_TX,
|
|
1173
|
+
status: "cancelled",
|
|
1174
|
+
round: cancelled
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
const body = await this.http.post(
|
|
1178
|
+
`/rounds/${encodeURIComponent(input.roundId)}/cancel`,
|
|
1179
|
+
{ gameId: input.gameId },
|
|
1180
|
+
{ acceptStatuses: [202] }
|
|
1181
|
+
);
|
|
1182
|
+
const status = body.status === "cancelling" || body.round?.status === "cancelling" ? "cancelling" : "cancelled";
|
|
1183
|
+
return {
|
|
1184
|
+
roundId: input.roundId,
|
|
1185
|
+
txHash: body.txHash ?? body.round?.cancelTxHash ?? null,
|
|
1186
|
+
status,
|
|
1187
|
+
round: body.round
|
|
1188
|
+
};
|
|
1189
|
+
},
|
|
1039
1190
|
get: async (input) => {
|
|
1040
1191
|
requireField(input?.roundId, "roundId");
|
|
1192
|
+
if (this.config.mock) {
|
|
1193
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1194
|
+
if (!existing) {
|
|
1195
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1196
|
+
}
|
|
1197
|
+
return existing;
|
|
1198
|
+
}
|
|
1041
1199
|
const { round } = await this.http.get(
|
|
1042
1200
|
`/rounds/${encodeURIComponent(input.roundId)}`
|
|
1043
1201
|
);
|
|
@@ -1053,6 +1211,21 @@ var Playmos = class {
|
|
|
1053
1211
|
prize: async (input) => {
|
|
1054
1212
|
requireField(input?.roundId, "roundId");
|
|
1055
1213
|
const wallet = requireAddressField(input?.wallet, "wallet");
|
|
1214
|
+
if (this.config.mock) {
|
|
1215
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1216
|
+
if (!existing) {
|
|
1217
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1218
|
+
}
|
|
1219
|
+
const claimableMicro2 = this.mockWithdrawable.get(`${input.roundId}:${wallet.toLowerCase()}`) ?? 0n;
|
|
1220
|
+
const prizePoolAddress2 = input.prizePoolAddress ?? existing.prizePoolAddress ?? "0x0000000000000000000000000000000000000002";
|
|
1221
|
+
return {
|
|
1222
|
+
roundId: input.roundId,
|
|
1223
|
+
prizePoolAddress: prizePoolAddress2,
|
|
1224
|
+
wallet,
|
|
1225
|
+
claimable: formatMicroToUsd(claimableMicro2),
|
|
1226
|
+
claimableMicro: claimableMicro2.toString()
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1056
1229
|
try {
|
|
1057
1230
|
const res = await this.http.get(
|
|
1058
1231
|
`/rounds/${encodeURIComponent(input.roundId)}/prize?wallet=${encodeURIComponent(wallet)}`
|
|
@@ -1079,11 +1252,24 @@ var Playmos = class {
|
|
|
1079
1252
|
*/
|
|
1080
1253
|
withdraw: async (input) => {
|
|
1081
1254
|
if (this.config.mock) {
|
|
1255
|
+
let amountMicro2 = 0n;
|
|
1256
|
+
let prizePool2 = input.prizePoolAddress ?? "0x0000000000000000000000000000000000000001";
|
|
1257
|
+
if (input.roundId) {
|
|
1258
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1259
|
+
if (existing?.prizePoolAddress) prizePool2 = existing.prizePoolAddress;
|
|
1260
|
+
for (const [k, v] of this.mockWithdrawable) {
|
|
1261
|
+
if (k.startsWith(`${input.roundId}:`) && v > 0n) {
|
|
1262
|
+
amountMicro2 = v;
|
|
1263
|
+
this.mockWithdrawable.set(k, 0n);
|
|
1264
|
+
break;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1082
1268
|
return {
|
|
1083
|
-
prizePoolAddress:
|
|
1084
|
-
amount:
|
|
1085
|
-
amountMicro:
|
|
1086
|
-
txHash:
|
|
1269
|
+
prizePoolAddress: prizePool2,
|
|
1270
|
+
amount: formatMicroToUsd(amountMicro2),
|
|
1271
|
+
amountMicro: amountMicro2.toString(),
|
|
1272
|
+
txHash: MOCK_TX,
|
|
1087
1273
|
status: "confirmed"
|
|
1088
1274
|
};
|
|
1089
1275
|
}
|
|
@@ -1158,23 +1344,27 @@ var Playmos = class {
|
|
|
1158
1344
|
this.agents = {
|
|
1159
1345
|
/** Assign (or return) the wallet for a game NPC id. Idempotent — safe to call wherever your NPCs spawn. */
|
|
1160
1346
|
createWallet: async (input) => {
|
|
1347
|
+
this.assertSecretKey("playmos.agents");
|
|
1161
1348
|
requireField(input?.agentId, "agentId");
|
|
1162
1349
|
const { agent } = await this.http.post("/agents/wallets", { agentId: input.agentId });
|
|
1163
1350
|
return agent;
|
|
1164
1351
|
},
|
|
1165
1352
|
/** Resolve one NPC's wallet by your id. */
|
|
1166
1353
|
wallet: async (agentId) => {
|
|
1354
|
+
this.assertSecretKey("playmos.agents");
|
|
1167
1355
|
requireField(agentId, "agentId");
|
|
1168
1356
|
const { agent } = await this.http.get(`/agents/wallets/${encodeURIComponent(agentId)}`);
|
|
1169
1357
|
return agent;
|
|
1170
1358
|
},
|
|
1171
1359
|
/** List the NPC wallets you've assigned in this studio. */
|
|
1172
1360
|
list: async () => {
|
|
1361
|
+
this.assertSecretKey("playmos.agents");
|
|
1173
1362
|
const { agents } = await this.http.get("/agents/wallets");
|
|
1174
1363
|
return agents;
|
|
1175
1364
|
},
|
|
1176
1365
|
/** Sandbox faucet: fund an NPC with USDC from your treasury (per-NPC lifetime cap). */
|
|
1177
|
-
fund: (input) => {
|
|
1366
|
+
fund: async (input) => {
|
|
1367
|
+
this.assertSecretKey("playmos.agents.fund");
|
|
1178
1368
|
requireField(input?.agentId, "agentId");
|
|
1179
1369
|
validateAmount(input.amount);
|
|
1180
1370
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
@@ -1185,10 +1375,17 @@ var Playmos = class {
|
|
|
1185
1375
|
);
|
|
1186
1376
|
},
|
|
1187
1377
|
/** NPC→NPC (or →player) USDC transfer, by id. A thin alias of `playmos.transfer` (which is canonical). */
|
|
1188
|
-
pay: (input) => {
|
|
1378
|
+
pay: async (input) => {
|
|
1379
|
+
this.assertSecretKey("playmos.agents.pay");
|
|
1189
1380
|
requireField(input?.from, "from");
|
|
1190
1381
|
requireField(input?.to, "to");
|
|
1191
|
-
return this.transfer({
|
|
1382
|
+
return this.transfer({
|
|
1383
|
+
from: input.from,
|
|
1384
|
+
to: input.to,
|
|
1385
|
+
amount: input.amount,
|
|
1386
|
+
feeBps: input.feeBps,
|
|
1387
|
+
feeSink: input.feeSink
|
|
1388
|
+
});
|
|
1192
1389
|
}
|
|
1193
1390
|
};
|
|
1194
1391
|
/**
|
|
@@ -1372,6 +1569,15 @@ var Playmos = class {
|
|
|
1372
1569
|
};
|
|
1373
1570
|
this.config = config;
|
|
1374
1571
|
this.env = resolveEnv(config.apiKey, config.network, config.apiBaseUrl);
|
|
1572
|
+
if (this.env.isSecret) {
|
|
1573
|
+
const signals = clientRuntimeSignals();
|
|
1574
|
+
if (signals.length > 0) {
|
|
1575
|
+
throw new AuthError(
|
|
1576
|
+
"Secret (sk_) key used in a client runtime \u2014 sk_ keys are server-only; use a pk_ key client-side.",
|
|
1577
|
+
{ isSecret: true, clientRuntime: true, signals }
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1375
1581
|
this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
|
|
1376
1582
|
}
|
|
1377
1583
|
/** Connect the player's wallet and return their address. */
|
|
@@ -1399,15 +1605,17 @@ var Playmos = class {
|
|
|
1399
1605
|
roundId: req.roundKey,
|
|
1400
1606
|
roundKey: req.roundKey,
|
|
1401
1607
|
identity: req.identity,
|
|
1402
|
-
|
|
1403
|
-
|
|
1608
|
+
playerId: req.wallet,
|
|
1609
|
+
amount: formatMicroToUsd(req.entryUnits)
|
|
1404
1610
|
});
|
|
1405
|
-
|
|
1611
|
+
this.rememberMock(entry);
|
|
1612
|
+
const raw = String(entry.status ?? "pending").toLowerCase();
|
|
1613
|
+
const status = raw === "confirmed" ? "confirmed" : raw === "failed" ? "failed" : "pending";
|
|
1406
1614
|
const onchain = !entry.mock;
|
|
1407
1615
|
return {
|
|
1408
1616
|
paymentId: entry.id,
|
|
1409
1617
|
status,
|
|
1410
|
-
txHash: entry.txHash,
|
|
1618
|
+
...entry.txHash ? { txHash: entry.txHash } : {},
|
|
1411
1619
|
onchain,
|
|
1412
1620
|
identity: entry.identity ?? req.identity
|
|
1413
1621
|
};
|
|
@@ -1440,14 +1648,39 @@ var Playmos = class {
|
|
|
1440
1648
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1441
1649
|
const res = await this.http.post(
|
|
1442
1650
|
"/payments",
|
|
1443
|
-
{
|
|
1444
|
-
|
|
1651
|
+
{
|
|
1652
|
+
kind: "iap",
|
|
1653
|
+
amount: input.amount,
|
|
1654
|
+
sku: input.sku,
|
|
1655
|
+
playerId: input.playerId,
|
|
1656
|
+
gameId: input.gameId,
|
|
1657
|
+
studio: input.studio,
|
|
1658
|
+
metadata,
|
|
1659
|
+
settle: "server",
|
|
1660
|
+
idempotencyKey
|
|
1661
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1662
|
+
},
|
|
1663
|
+
{ idempotencyKey, acceptStatuses: [201, 202] }
|
|
1445
1664
|
);
|
|
1446
|
-
|
|
1665
|
+
let payment = this.mapServerPayment(res.payment, "iap", amountMicro);
|
|
1666
|
+
if (payment.status === "pending" || payment.status === "created") {
|
|
1667
|
+
payment = await this.waitForServerSettle(payment.id, "iap", amountMicro);
|
|
1668
|
+
}
|
|
1669
|
+
return payment;
|
|
1447
1670
|
}
|
|
1448
1671
|
const intent = await this.http.post(
|
|
1449
1672
|
"/payments",
|
|
1450
|
-
{
|
|
1673
|
+
{
|
|
1674
|
+
kind: "iap",
|
|
1675
|
+
amount: input.amount,
|
|
1676
|
+
sku: input.sku,
|
|
1677
|
+
playerId: input.playerId,
|
|
1678
|
+
gameId: input.gameId,
|
|
1679
|
+
studio: input.studio,
|
|
1680
|
+
metadata,
|
|
1681
|
+
idempotencyKey
|
|
1682
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1683
|
+
},
|
|
1451
1684
|
{ idempotencyKey }
|
|
1452
1685
|
);
|
|
1453
1686
|
const provider = resolveProvider(this.config.wallet);
|
|
@@ -1496,23 +1729,48 @@ var Playmos = class {
|
|
|
1496
1729
|
);
|
|
1497
1730
|
}
|
|
1498
1731
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1732
|
+
const pinnedRoundKey = input.roundKey ?? input.roundId;
|
|
1499
1733
|
const res = await this.http.post(
|
|
1500
1734
|
"/payments",
|
|
1501
|
-
{
|
|
1502
|
-
|
|
1735
|
+
{
|
|
1736
|
+
kind: "entry",
|
|
1737
|
+
amount: input.amount,
|
|
1738
|
+
gameId: input.gameId,
|
|
1739
|
+
roundId: pinnedRoundKey,
|
|
1740
|
+
identity: input.identity,
|
|
1741
|
+
playerId: input.playerId,
|
|
1742
|
+
metadata,
|
|
1743
|
+
settle: "server",
|
|
1744
|
+
idempotencyKey
|
|
1745
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1746
|
+
},
|
|
1747
|
+
{ idempotencyKey, acceptStatuses: [201, 202] }
|
|
1503
1748
|
);
|
|
1504
|
-
|
|
1749
|
+
let payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
|
|
1750
|
+
if (payment2.status === "pending" || payment2.status === "created") {
|
|
1751
|
+
payment2 = await this.waitForServerSettle(payment2.id, "entry", amountMicro);
|
|
1752
|
+
}
|
|
1505
1753
|
if (input.identity) payment2.identity = input.identity;
|
|
1506
1754
|
const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
1507
1755
|
if (pool) payment2.prizePoolAddress = pool.toLowerCase();
|
|
1508
|
-
payment2.roundKey =
|
|
1756
|
+
payment2.roundKey = input.roundKey ?? res.clientParams?.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
|
|
1509
1757
|
return payment2;
|
|
1510
1758
|
}
|
|
1511
1759
|
const intent = await this.http.post(
|
|
1512
1760
|
"/payments",
|
|
1513
1761
|
// When the game supplies its own roundKey, that IS the round the service
|
|
1514
1762
|
// stores + verifies against (so on-chain enter and server hasEntered align).
|
|
1515
|
-
{
|
|
1763
|
+
{
|
|
1764
|
+
kind: "entry",
|
|
1765
|
+
amount: input.amount,
|
|
1766
|
+
gameId: input.gameId,
|
|
1767
|
+
roundId: input.roundKey ?? input.roundId,
|
|
1768
|
+
identity: input.identity,
|
|
1769
|
+
playerId: input.playerId,
|
|
1770
|
+
metadata,
|
|
1771
|
+
idempotencyKey
|
|
1772
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1773
|
+
},
|
|
1516
1774
|
{ idempotencyKey }
|
|
1517
1775
|
);
|
|
1518
1776
|
const provider = resolveProvider(this.config.wallet);
|
|
@@ -1555,6 +1813,7 @@ var Playmos = class {
|
|
|
1555
1813
|
* NPC `from` requires `sk_test_` and settles gaslessly (NPC signs; service relays).
|
|
1556
1814
|
*/
|
|
1557
1815
|
async transfer(input, opts) {
|
|
1816
|
+
this.assertSecretKey("playmos.transfer");
|
|
1558
1817
|
const amountMicro = validateAmount(input.amount);
|
|
1559
1818
|
const from = input.from === void 0 ? void 0 : partyRef(input.from, "from");
|
|
1560
1819
|
const to = partyRef(input.to, "to");
|
|
@@ -1624,16 +1883,55 @@ var Playmos = class {
|
|
|
1624
1883
|
}
|
|
1625
1884
|
// ---- internals ---------------------------------------------------------
|
|
1626
1885
|
/**
|
|
1627
|
-
*
|
|
1628
|
-
*
|
|
1886
|
+
* Secret (sk_) surfaces — agents / transfer / x402. Capability tracks key type
|
|
1887
|
+
* in code, not README prose (#260). pk_ keys are client-safe and refuse these.
|
|
1888
|
+
*/
|
|
1889
|
+
assertSecretKey(surface) {
|
|
1890
|
+
if (!this.env.isSecret) {
|
|
1891
|
+
throw new AuthError(
|
|
1892
|
+
`${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.`,
|
|
1893
|
+
{ surface, isSecret: false }
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* x402 V1 is Base Sepolia + secret test keys only (ADR D5 + #260). Fail closed
|
|
1899
|
+
* before any network call so mainnet / live / publishable keys never hit the adapter.
|
|
1900
|
+
* Does not change fulfill/settle body shape (A owns #261 / #259 money path).
|
|
1629
1901
|
*/
|
|
1630
1902
|
assertX402Allowed() {
|
|
1903
|
+
this.assertSecretKey("playmos.x402");
|
|
1631
1904
|
if (!this.env.isTest || this.env.network !== "base-sepolia") {
|
|
1632
1905
|
throw new ConfigError(
|
|
1633
|
-
'playmos.x402 is Base Sepolia /
|
|
1634
|
-
{ network: this.env.network, isTest: this.env.isTest }
|
|
1906
|
+
'playmos.x402 is Base Sepolia / sk_test_ only in V1 (ADR D5). Use apiKey sk_test_\u2026 and network "base-sepolia".',
|
|
1907
|
+
{ network: this.env.network, isTest: this.env.isTest, isSecret: this.env.isSecret }
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Poll GET /payments/:id after server-settle until terminal status.
|
|
1913
|
+
* - Entry (#221): service may return 202 pending (openRound+enter > DO edge).
|
|
1914
|
+
* - IAP: service often returns 201 + txHash while status is still "created"
|
|
1915
|
+
* until chain verify catches PaymentSettled (RPC lag) — poll so pay() matches
|
|
1916
|
+
* the documented "confirmed" sandbox promise (TTFSC-T0 / cold-run).
|
|
1917
|
+
*/
|
|
1918
|
+
async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4) {
|
|
1919
|
+
const start = Date.now();
|
|
1920
|
+
let delayMs = 400;
|
|
1921
|
+
while (Date.now() - start < timeoutMs) {
|
|
1922
|
+
const raw = await this.http.get(
|
|
1923
|
+
`/payments/${encodeURIComponent(paymentId)}`
|
|
1635
1924
|
);
|
|
1925
|
+
if (raw.status === "confirmed" || raw.status === "failed") {
|
|
1926
|
+
return this.mapServerPayment(raw, kind, amountMicro);
|
|
1927
|
+
}
|
|
1928
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
1929
|
+
delayMs = Math.min(Math.floor(delayMs * 1.4), 2e3);
|
|
1636
1930
|
}
|
|
1931
|
+
throw new ApiError(
|
|
1932
|
+
`Timed out after ${timeoutMs}ms waiting for server-settle of ${paymentId}. Re-check with playmos.verify("${paymentId}").`,
|
|
1933
|
+
{ paymentId, timeout: true, asyncSettle: true }
|
|
1934
|
+
);
|
|
1637
1935
|
}
|
|
1638
1936
|
/**
|
|
1639
1937
|
* Map a server-settled payment (from the `settle: "server"` response) into the
|
|
@@ -2023,6 +2321,7 @@ exports.PlaymosError = PlaymosError;
|
|
|
2023
2321
|
exports.USDC_ADDRESS = USDC_ADDRESS;
|
|
2024
2322
|
exports.USDC_DECIMALS = USDC_DECIMALS;
|
|
2025
2323
|
exports.WalletConnectionError = WalletConnectionError;
|
|
2324
|
+
exports.clientRuntimeSignals = clientRuntimeSignals;
|
|
2026
2325
|
exports.computeIapSplit = computeIapSplit;
|
|
2027
2326
|
exports.computePayout = computePayout;
|
|
2028
2327
|
exports.computePoolSplit = computePoolSplit;
|
|
@@ -2031,6 +2330,7 @@ exports.createX402Challenge = createX402Challenge;
|
|
|
2031
2330
|
exports.decodePaymentHeader = decodePaymentHeader;
|
|
2032
2331
|
exports.encodePaymentHeader = encodePaymentHeader;
|
|
2033
2332
|
exports.formatMicroToUsd = formatMicroToUsd;
|
|
2333
|
+
exports.isClientRuntime = isClientRuntime;
|
|
2034
2334
|
exports.isWalletSignatureAuthorization = isWalletSignatureAuthorization;
|
|
2035
2335
|
exports.isX402PayloadAuthorization = isX402PayloadAuthorization;
|
|
2036
2336
|
exports.networkToCaip2 = networkToCaip2;
|
|
@@ -2042,9 +2342,8 @@ exports.previewIapSplit = previewIapSplit;
|
|
|
2042
2342
|
exports.previewMarketplaceSplit = previewMarketplaceSplit;
|
|
2043
2343
|
exports.previewPoolSplit = previewPoolSplit;
|
|
2044
2344
|
exports.previewTransferSplit = previewTransferSplit;
|
|
2345
|
+
exports.resolveEnv = resolveEnv;
|
|
2045
2346
|
exports.serializePaymentRequirement = serializePaymentRequirement;
|
|
2046
2347
|
exports.toX402PaymentRequired = toX402PaymentRequired;
|
|
2047
2348
|
exports.ulid = ulid;
|
|
2048
2349
|
exports.validateX402ChallengeInput = validateX402ChallengeInput;
|
|
2049
|
-
//# sourceMappingURL=index.cjs.map
|
|
2050
|
-
//# sourceMappingURL=index.cjs.map
|