@playmos/sdk 0.3.3 → 0.3.5
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-CdVzHuhY.d.cts} +69 -12
- package/dist/{errors-DMtMpbR6.d.ts → errors-CdVzHuhY.d.ts} +69 -12
- package/dist/index.cjs +361 -114
- package/dist/index.d.cts +68 -16
- package/dist/index.d.ts +68 -16
- package/dist/index.js +361 -117
- 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 +6 -3
- 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,
|
|
@@ -1001,8 +977,11 @@ var Playmos = class {
|
|
|
1001
977
|
*/
|
|
1002
978
|
/** Offline mock store for rounds.open/lock/settle/get when `mock: true` (3-game dogfood). */
|
|
1003
979
|
this.mockRounds = /* @__PURE__ */ new Map();
|
|
980
|
+
/** Mock withdrawable credits after settle — key `${roundId}:${walletLower}` (#240 offline prize). */
|
|
981
|
+
this.mockWithdrawable = /* @__PURE__ */ new Map();
|
|
1004
982
|
this.rounds = {
|
|
1005
983
|
open: async (input) => {
|
|
984
|
+
this.assertSecretKey("playmos.rounds.open");
|
|
1006
985
|
requireField(input?.gameId, "gameId");
|
|
1007
986
|
requireField(input?.roundId, "roundId");
|
|
1008
987
|
requireField(input?.entryAmount, "entryAmount");
|
|
@@ -1022,34 +1001,41 @@ var Playmos = class {
|
|
|
1022
1001
|
code: "conflict"
|
|
1023
1002
|
});
|
|
1024
1003
|
}
|
|
1025
|
-
const
|
|
1004
|
+
const round = {
|
|
1026
1005
|
roundId: input.roundId,
|
|
1027
1006
|
gameId: input.gameId,
|
|
1028
1007
|
roundKey,
|
|
1008
|
+
// #351 — must persist seriesKey so activeForSeries mock is authoritative
|
|
1009
|
+
seriesKey: input.seriesKey ?? input.gameId,
|
|
1029
1010
|
status: "open",
|
|
1030
1011
|
entryAmount: input.entryAmount,
|
|
1031
1012
|
pool: null,
|
|
1032
1013
|
entrants: 0,
|
|
1033
1014
|
payout: input.payout,
|
|
1034
1015
|
closeAt: input.closeAt,
|
|
1035
|
-
openTxHash:
|
|
1016
|
+
openTxHash: MOCK_TX,
|
|
1036
1017
|
prizePoolAddress: this.config.contracts?.prizePool ?? "0x0000000000000000000000000000000000000002"
|
|
1037
1018
|
};
|
|
1038
|
-
this.mockRounds.set(input.roundId,
|
|
1039
|
-
return
|
|
1019
|
+
this.mockRounds.set(input.roundId, round);
|
|
1020
|
+
return round;
|
|
1040
1021
|
}
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1022
|
+
const body = await this.http.post(
|
|
1023
|
+
"/rounds",
|
|
1024
|
+
{
|
|
1025
|
+
gameId: input.gameId,
|
|
1026
|
+
roundId: input.roundId,
|
|
1027
|
+
entryAmount: input.entryAmount,
|
|
1028
|
+
payout: input.payout,
|
|
1029
|
+
closeAt: input.closeAt,
|
|
1030
|
+
seriesKey: input.seriesKey,
|
|
1031
|
+
roundKey: input.roundKey
|
|
1032
|
+
},
|
|
1033
|
+
{ acceptStatuses: [202] }
|
|
1034
|
+
);
|
|
1035
|
+
return body.round;
|
|
1051
1036
|
},
|
|
1052
1037
|
lock: async (input) => {
|
|
1038
|
+
this.assertSecretKey("playmos.rounds.lock");
|
|
1053
1039
|
requireField(input?.roundId, "roundId");
|
|
1054
1040
|
if (this.config.mock) {
|
|
1055
1041
|
const existing = this.mockRounds.get(input.roundId);
|
|
@@ -1069,7 +1055,7 @@ var Playmos = class {
|
|
|
1069
1055
|
// Mock payable pool ≈ 60% of entry × max(1, entrants) for shape only — not money truth.
|
|
1070
1056
|
pool: existing.pool ?? existing.entryAmount,
|
|
1071
1057
|
entrants: existing.entrants ?? 0,
|
|
1072
|
-
lockTxHash:
|
|
1058
|
+
lockTxHash: MOCK_TX
|
|
1073
1059
|
};
|
|
1074
1060
|
this.mockRounds.set(input.roundId, locked);
|
|
1075
1061
|
return locked;
|
|
@@ -1081,6 +1067,7 @@ var Playmos = class {
|
|
|
1081
1067
|
return round;
|
|
1082
1068
|
},
|
|
1083
1069
|
settle: async (input) => {
|
|
1070
|
+
this.assertSecretKey("playmos.rounds.settle");
|
|
1084
1071
|
requireField(input?.roundId, "roundId");
|
|
1085
1072
|
if (!input?.results) throw new ConfigError("results are required (ranking or winners)");
|
|
1086
1073
|
if (this.config.mock) {
|
|
@@ -1111,7 +1098,7 @@ var Playmos = class {
|
|
|
1111
1098
|
// Winner-take-all mock shape when ranking only.
|
|
1112
1099
|
amount: i === 0 ? existing.pool ?? existing.entryAmount : "0"
|
|
1113
1100
|
}));
|
|
1114
|
-
const settleTxHash =
|
|
1101
|
+
const settleTxHash = MOCK_TX;
|
|
1115
1102
|
const poolPaid = existing.pool ?? existing.entryAmount;
|
|
1116
1103
|
const settled = {
|
|
1117
1104
|
...existing,
|
|
@@ -1120,6 +1107,19 @@ var Playmos = class {
|
|
|
1120
1107
|
pool: poolPaid
|
|
1121
1108
|
};
|
|
1122
1109
|
this.mockRounds.set(input.roundId, settled);
|
|
1110
|
+
for (const w of winners) {
|
|
1111
|
+
const wAddr = String(w.wallet).toLowerCase();
|
|
1112
|
+
let micro = 0n;
|
|
1113
|
+
try {
|
|
1114
|
+
micro = validateAmount(String(w.amount ?? "0"));
|
|
1115
|
+
} catch {
|
|
1116
|
+
micro = 0n;
|
|
1117
|
+
}
|
|
1118
|
+
if (micro > 0n) {
|
|
1119
|
+
const k = `${input.roundId}:${wAddr}`;
|
|
1120
|
+
this.mockWithdrawable.set(k, (this.mockWithdrawable.get(k) ?? 0n) + micro);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
1123
|
return {
|
|
1124
1124
|
roundId: input.roundId,
|
|
1125
1125
|
txHash: settleTxHash,
|
|
@@ -1130,10 +1130,65 @@ var Playmos = class {
|
|
|
1130
1130
|
}
|
|
1131
1131
|
const { settle } = await this.http.post(
|
|
1132
1132
|
`/rounds/${encodeURIComponent(input.roundId)}/settle`,
|
|
1133
|
-
{ gameId: input.gameId, results: input.results }
|
|
1133
|
+
{ gameId: input.gameId, results: input.results },
|
|
1134
|
+
{ acceptStatuses: [202] }
|
|
1134
1135
|
);
|
|
1135
1136
|
return settle;
|
|
1136
1137
|
},
|
|
1138
|
+
/**
|
|
1139
|
+
* Cancel an open/locked round (operator sk_ only). Refunds entrants' escrowed ~90%
|
|
1140
|
+
* and frees the series latch. May return `status: "cancelling"` (HTTP 202) until
|
|
1141
|
+
* chain Cancelled(4) reconciles — poll `rounds.get` or re-call cancel (#346).
|
|
1142
|
+
*/
|
|
1143
|
+
cancel: async (input) => {
|
|
1144
|
+
this.assertSecretKey("playmos.rounds.cancel");
|
|
1145
|
+
requireField(input?.roundId, "roundId");
|
|
1146
|
+
if (this.config.mock) {
|
|
1147
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1148
|
+
if (!existing) {
|
|
1149
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1150
|
+
}
|
|
1151
|
+
if (existing.status === "cancelled") {
|
|
1152
|
+
return {
|
|
1153
|
+
roundId: input.roundId,
|
|
1154
|
+
txHash: existing.cancelTxHash ?? MOCK_TX,
|
|
1155
|
+
status: "cancelled",
|
|
1156
|
+
round: existing
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
if (existing.status === "settled") {
|
|
1160
|
+
throw new ApiError(`round ${input.roundId} is already settled \u2014 cannot cancel`, {
|
|
1161
|
+
status: 409,
|
|
1162
|
+
code: "conflict"
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
const cancelled = {
|
|
1166
|
+
...existing,
|
|
1167
|
+
status: "cancelled",
|
|
1168
|
+
cancelTxHash: MOCK_TX,
|
|
1169
|
+
pool: null
|
|
1170
|
+
};
|
|
1171
|
+
this.mockRounds.set(input.roundId, cancelled);
|
|
1172
|
+
return {
|
|
1173
|
+
roundId: input.roundId,
|
|
1174
|
+
txHash: MOCK_TX,
|
|
1175
|
+
status: "cancelled",
|
|
1176
|
+
round: cancelled
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
const body = await this.http.post(
|
|
1180
|
+
`/rounds/${encodeURIComponent(input.roundId)}/cancel`,
|
|
1181
|
+
{ gameId: input.gameId },
|
|
1182
|
+
{ acceptStatuses: [202] }
|
|
1183
|
+
);
|
|
1184
|
+
const status = body.status === "cancelling" || body.round?.status === "cancelling" ? "cancelling" : "cancelled";
|
|
1185
|
+
return {
|
|
1186
|
+
roundId: input.roundId,
|
|
1187
|
+
txHash: body.txHash ?? body.round?.cancelTxHash ?? null,
|
|
1188
|
+
status,
|
|
1189
|
+
round: body.round
|
|
1190
|
+
};
|
|
1191
|
+
},
|
|
1137
1192
|
get: async (input) => {
|
|
1138
1193
|
requireField(input?.roundId, "roundId");
|
|
1139
1194
|
if (this.config.mock) {
|
|
@@ -1148,6 +1203,58 @@ var Playmos = class {
|
|
|
1148
1203
|
);
|
|
1149
1204
|
return round;
|
|
1150
1205
|
},
|
|
1206
|
+
/**
|
|
1207
|
+
* Authoritative series latch (#351) — `null` means **proven free**.
|
|
1208
|
+
* `GET /v1/series/:seriesKey/active?gameId=…` — not a candidate probe.
|
|
1209
|
+
*/
|
|
1210
|
+
activeForSeries: async (input) => {
|
|
1211
|
+
requireField(input?.seriesKey, "seriesKey");
|
|
1212
|
+
requireField(input?.gameId, "gameId");
|
|
1213
|
+
if (this.config.mock) {
|
|
1214
|
+
for (const r of this.mockRounds.values()) {
|
|
1215
|
+
if (r.gameId !== input.gameId) continue;
|
|
1216
|
+
const sk = r.seriesKey ?? r.gameId;
|
|
1217
|
+
if (sk !== input.seriesKey) continue;
|
|
1218
|
+
if (r.status === "open" || r.status === "locked" || r.status === "opening" || r.status === "cancelling") {
|
|
1219
|
+
return {
|
|
1220
|
+
roundId: r.roundId,
|
|
1221
|
+
roundKey: r.roundKey,
|
|
1222
|
+
roundIdHash: "0x" + "m".repeat(64),
|
|
1223
|
+
status: r.status,
|
|
1224
|
+
entrants: r.entrants,
|
|
1225
|
+
pool: r.pool,
|
|
1226
|
+
openTxHash: r.openTxHash ?? null,
|
|
1227
|
+
cancelTxHash: r.cancelTxHash ?? null,
|
|
1228
|
+
prizePoolAddress: r.prizePoolAddress ?? "0x0000000000000000000000000000000000000002"
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
const body = await this.http.get(
|
|
1235
|
+
`/series/${encodeURIComponent(input.seriesKey)}/active?gameId=${encodeURIComponent(input.gameId)}`
|
|
1236
|
+
);
|
|
1237
|
+
if (body == null || typeof body !== "object") {
|
|
1238
|
+
throw new ApiError("activeForSeries: malformed response (expected object body)", {
|
|
1239
|
+
code: "invalid_response",
|
|
1240
|
+
status: 502
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
if (!("active" in body)) {
|
|
1244
|
+
throw new ApiError("activeForSeries: malformed response (missing active field)", {
|
|
1245
|
+
code: "invalid_response",
|
|
1246
|
+
status: 502
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
if (body.active === null) return null;
|
|
1250
|
+
if (body.active == null || typeof body.active !== "object") {
|
|
1251
|
+
throw new ApiError("activeForSeries: malformed response (active must be object or null)", {
|
|
1252
|
+
code: "invalid_response",
|
|
1253
|
+
status: 502
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
return body.active;
|
|
1257
|
+
},
|
|
1151
1258
|
/**
|
|
1152
1259
|
* Read a wallet's **claimable** prize balance for a round (issue #41).
|
|
1153
1260
|
* Prefers the service chain read (`GET /v1/rounds/:id/prize?wallet=`); falls
|
|
@@ -1158,6 +1265,21 @@ var Playmos = class {
|
|
|
1158
1265
|
prize: async (input) => {
|
|
1159
1266
|
requireField(input?.roundId, "roundId");
|
|
1160
1267
|
const wallet = requireAddressField(input?.wallet, "wallet");
|
|
1268
|
+
if (this.config.mock) {
|
|
1269
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1270
|
+
if (!existing) {
|
|
1271
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1272
|
+
}
|
|
1273
|
+
const claimableMicro2 = this.mockWithdrawable.get(`${input.roundId}:${wallet.toLowerCase()}`) ?? 0n;
|
|
1274
|
+
const prizePoolAddress2 = input.prizePoolAddress ?? existing.prizePoolAddress ?? "0x0000000000000000000000000000000000000002";
|
|
1275
|
+
return {
|
|
1276
|
+
roundId: input.roundId,
|
|
1277
|
+
prizePoolAddress: prizePoolAddress2,
|
|
1278
|
+
wallet,
|
|
1279
|
+
claimable: formatMicroToUsd(claimableMicro2),
|
|
1280
|
+
claimableMicro: claimableMicro2.toString()
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1161
1283
|
try {
|
|
1162
1284
|
const res = await this.http.get(
|
|
1163
1285
|
`/rounds/${encodeURIComponent(input.roundId)}/prize?wallet=${encodeURIComponent(wallet)}`
|
|
@@ -1184,11 +1306,24 @@ var Playmos = class {
|
|
|
1184
1306
|
*/
|
|
1185
1307
|
withdraw: async (input) => {
|
|
1186
1308
|
if (this.config.mock) {
|
|
1309
|
+
let amountMicro2 = 0n;
|
|
1310
|
+
let prizePool2 = input.prizePoolAddress ?? "0x0000000000000000000000000000000000000001";
|
|
1311
|
+
if (input.roundId) {
|
|
1312
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1313
|
+
if (existing?.prizePoolAddress) prizePool2 = existing.prizePoolAddress;
|
|
1314
|
+
for (const [k, v] of this.mockWithdrawable) {
|
|
1315
|
+
if (k.startsWith(`${input.roundId}:`) && v > 0n) {
|
|
1316
|
+
amountMicro2 = v;
|
|
1317
|
+
this.mockWithdrawable.set(k, 0n);
|
|
1318
|
+
break;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1187
1322
|
return {
|
|
1188
|
-
prizePoolAddress:
|
|
1189
|
-
amount:
|
|
1190
|
-
amountMicro:
|
|
1191
|
-
txHash:
|
|
1323
|
+
prizePoolAddress: prizePool2,
|
|
1324
|
+
amount: formatMicroToUsd(amountMicro2),
|
|
1325
|
+
amountMicro: amountMicro2.toString(),
|
|
1326
|
+
txHash: MOCK_TX,
|
|
1192
1327
|
status: "confirmed"
|
|
1193
1328
|
};
|
|
1194
1329
|
}
|
|
@@ -1263,23 +1398,27 @@ var Playmos = class {
|
|
|
1263
1398
|
this.agents = {
|
|
1264
1399
|
/** Assign (or return) the wallet for a game NPC id. Idempotent — safe to call wherever your NPCs spawn. */
|
|
1265
1400
|
createWallet: async (input) => {
|
|
1401
|
+
this.assertSecretKey("playmos.agents");
|
|
1266
1402
|
requireField(input?.agentId, "agentId");
|
|
1267
1403
|
const { agent } = await this.http.post("/agents/wallets", { agentId: input.agentId });
|
|
1268
1404
|
return agent;
|
|
1269
1405
|
},
|
|
1270
1406
|
/** Resolve one NPC's wallet by your id. */
|
|
1271
1407
|
wallet: async (agentId) => {
|
|
1408
|
+
this.assertSecretKey("playmos.agents");
|
|
1272
1409
|
requireField(agentId, "agentId");
|
|
1273
1410
|
const { agent } = await this.http.get(`/agents/wallets/${encodeURIComponent(agentId)}`);
|
|
1274
1411
|
return agent;
|
|
1275
1412
|
},
|
|
1276
1413
|
/** List the NPC wallets you've assigned in this studio. */
|
|
1277
1414
|
list: async () => {
|
|
1415
|
+
this.assertSecretKey("playmos.agents");
|
|
1278
1416
|
const { agents } = await this.http.get("/agents/wallets");
|
|
1279
1417
|
return agents;
|
|
1280
1418
|
},
|
|
1281
1419
|
/** Sandbox faucet: fund an NPC with USDC from your treasury (per-NPC lifetime cap). */
|
|
1282
|
-
fund: (input) => {
|
|
1420
|
+
fund: async (input) => {
|
|
1421
|
+
this.assertSecretKey("playmos.agents.fund");
|
|
1283
1422
|
requireField(input?.agentId, "agentId");
|
|
1284
1423
|
validateAmount(input.amount);
|
|
1285
1424
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
@@ -1290,10 +1429,17 @@ var Playmos = class {
|
|
|
1290
1429
|
);
|
|
1291
1430
|
},
|
|
1292
1431
|
/** NPC→NPC (or →player) USDC transfer, by id. A thin alias of `playmos.transfer` (which is canonical). */
|
|
1293
|
-
pay: (input) => {
|
|
1432
|
+
pay: async (input) => {
|
|
1433
|
+
this.assertSecretKey("playmos.agents.pay");
|
|
1294
1434
|
requireField(input?.from, "from");
|
|
1295
1435
|
requireField(input?.to, "to");
|
|
1296
|
-
return this.transfer({
|
|
1436
|
+
return this.transfer({
|
|
1437
|
+
from: input.from,
|
|
1438
|
+
to: input.to,
|
|
1439
|
+
amount: input.amount,
|
|
1440
|
+
feeBps: input.feeBps,
|
|
1441
|
+
feeSink: input.feeSink
|
|
1442
|
+
});
|
|
1297
1443
|
}
|
|
1298
1444
|
};
|
|
1299
1445
|
/**
|
|
@@ -1477,6 +1623,15 @@ var Playmos = class {
|
|
|
1477
1623
|
};
|
|
1478
1624
|
this.config = config;
|
|
1479
1625
|
this.env = resolveEnv(config.apiKey, config.network, config.apiBaseUrl);
|
|
1626
|
+
if (this.env.isSecret) {
|
|
1627
|
+
const signals = clientRuntimeSignals();
|
|
1628
|
+
if (signals.length > 0) {
|
|
1629
|
+
throw new AuthError(
|
|
1630
|
+
"Secret (sk_) key used in a client runtime \u2014 sk_ keys are server-only; use a pk_ key client-side.",
|
|
1631
|
+
{ isSecret: true, clientRuntime: true, signals }
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1480
1635
|
this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
|
|
1481
1636
|
}
|
|
1482
1637
|
/** Connect the player's wallet and return their address. */
|
|
@@ -1504,9 +1659,10 @@ var Playmos = class {
|
|
|
1504
1659
|
roundId: req.roundKey,
|
|
1505
1660
|
roundKey: req.roundKey,
|
|
1506
1661
|
identity: req.identity,
|
|
1507
|
-
|
|
1508
|
-
|
|
1662
|
+
playerId: req.wallet,
|
|
1663
|
+
amount: formatMicroToUsd(req.entryUnits)
|
|
1509
1664
|
});
|
|
1665
|
+
this.rememberMock(entry);
|
|
1510
1666
|
const raw = String(entry.status ?? "pending").toLowerCase();
|
|
1511
1667
|
const status = raw === "confirmed" ? "confirmed" : raw === "failed" ? "failed" : "pending";
|
|
1512
1668
|
const onchain = !entry.mock;
|
|
@@ -1546,14 +1702,39 @@ var Playmos = class {
|
|
|
1546
1702
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1547
1703
|
const res = await this.http.post(
|
|
1548
1704
|
"/payments",
|
|
1549
|
-
{
|
|
1550
|
-
|
|
1705
|
+
{
|
|
1706
|
+
kind: "iap",
|
|
1707
|
+
amount: input.amount,
|
|
1708
|
+
sku: input.sku,
|
|
1709
|
+
playerId: input.playerId,
|
|
1710
|
+
gameId: input.gameId,
|
|
1711
|
+
studio: input.studio,
|
|
1712
|
+
metadata,
|
|
1713
|
+
settle: "server",
|
|
1714
|
+
idempotencyKey
|
|
1715
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1716
|
+
},
|
|
1717
|
+
{ idempotencyKey, acceptStatuses: [201, 202] }
|
|
1551
1718
|
);
|
|
1552
|
-
|
|
1719
|
+
let payment = this.mapServerPayment(res.payment, "iap", amountMicro);
|
|
1720
|
+
if (payment.status === "pending" || payment.status === "created") {
|
|
1721
|
+
payment = await this.waitForServerSettle(payment.id, "iap", amountMicro);
|
|
1722
|
+
}
|
|
1723
|
+
return payment;
|
|
1553
1724
|
}
|
|
1554
1725
|
const intent = await this.http.post(
|
|
1555
1726
|
"/payments",
|
|
1556
|
-
{
|
|
1727
|
+
{
|
|
1728
|
+
kind: "iap",
|
|
1729
|
+
amount: input.amount,
|
|
1730
|
+
sku: input.sku,
|
|
1731
|
+
playerId: input.playerId,
|
|
1732
|
+
gameId: input.gameId,
|
|
1733
|
+
studio: input.studio,
|
|
1734
|
+
metadata,
|
|
1735
|
+
idempotencyKey
|
|
1736
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1737
|
+
},
|
|
1557
1738
|
{ idempotencyKey }
|
|
1558
1739
|
);
|
|
1559
1740
|
const provider = resolveProvider(this.config.wallet);
|
|
@@ -1602,23 +1783,48 @@ var Playmos = class {
|
|
|
1602
1783
|
);
|
|
1603
1784
|
}
|
|
1604
1785
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1786
|
+
const pinnedRoundKey = input.roundKey ?? input.roundId;
|
|
1605
1787
|
const res = await this.http.post(
|
|
1606
1788
|
"/payments",
|
|
1607
|
-
{
|
|
1608
|
-
|
|
1789
|
+
{
|
|
1790
|
+
kind: "entry",
|
|
1791
|
+
amount: input.amount,
|
|
1792
|
+
gameId: input.gameId,
|
|
1793
|
+
roundId: pinnedRoundKey,
|
|
1794
|
+
identity: input.identity,
|
|
1795
|
+
playerId: input.playerId,
|
|
1796
|
+
metadata,
|
|
1797
|
+
settle: "server",
|
|
1798
|
+
idempotencyKey
|
|
1799
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1800
|
+
},
|
|
1801
|
+
{ idempotencyKey, acceptStatuses: [201, 202] }
|
|
1609
1802
|
);
|
|
1610
|
-
|
|
1803
|
+
let payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
|
|
1804
|
+
if (payment2.status === "pending" || payment2.status === "created") {
|
|
1805
|
+
payment2 = await this.waitForServerSettle(payment2.id, "entry", amountMicro);
|
|
1806
|
+
}
|
|
1611
1807
|
if (input.identity) payment2.identity = input.identity;
|
|
1612
1808
|
const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
1613
1809
|
if (pool) payment2.prizePoolAddress = pool.toLowerCase();
|
|
1614
|
-
payment2.roundKey =
|
|
1810
|
+
payment2.roundKey = input.roundKey ?? res.clientParams?.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
|
|
1615
1811
|
return payment2;
|
|
1616
1812
|
}
|
|
1617
1813
|
const intent = await this.http.post(
|
|
1618
1814
|
"/payments",
|
|
1619
1815
|
// When the game supplies its own roundKey, that IS the round the service
|
|
1620
1816
|
// stores + verifies against (so on-chain enter and server hasEntered align).
|
|
1621
|
-
{
|
|
1817
|
+
{
|
|
1818
|
+
kind: "entry",
|
|
1819
|
+
amount: input.amount,
|
|
1820
|
+
gameId: input.gameId,
|
|
1821
|
+
roundId: input.roundKey ?? input.roundId,
|
|
1822
|
+
identity: input.identity,
|
|
1823
|
+
playerId: input.playerId,
|
|
1824
|
+
metadata,
|
|
1825
|
+
idempotencyKey
|
|
1826
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1827
|
+
},
|
|
1622
1828
|
{ idempotencyKey }
|
|
1623
1829
|
);
|
|
1624
1830
|
const provider = resolveProvider(this.config.wallet);
|
|
@@ -1661,6 +1867,7 @@ var Playmos = class {
|
|
|
1661
1867
|
* NPC `from` requires `sk_test_` and settles gaslessly (NPC signs; service relays).
|
|
1662
1868
|
*/
|
|
1663
1869
|
async transfer(input, opts) {
|
|
1870
|
+
this.assertSecretKey("playmos.transfer");
|
|
1664
1871
|
const amountMicro = validateAmount(input.amount);
|
|
1665
1872
|
const from = input.from === void 0 ? void 0 : partyRef(input.from, "from");
|
|
1666
1873
|
const to = partyRef(input.to, "to");
|
|
@@ -1730,17 +1937,56 @@ var Playmos = class {
|
|
|
1730
1937
|
}
|
|
1731
1938
|
// ---- internals ---------------------------------------------------------
|
|
1732
1939
|
/**
|
|
1733
|
-
*
|
|
1734
|
-
*
|
|
1940
|
+
* Secret (sk_) surfaces — agents / transfer / x402. Capability tracks key type
|
|
1941
|
+
* in code, not README prose (#260). pk_ keys are client-safe and refuse these.
|
|
1942
|
+
*/
|
|
1943
|
+
assertSecretKey(surface) {
|
|
1944
|
+
if (!this.env.isSecret) {
|
|
1945
|
+
throw new AuthError(
|
|
1946
|
+
`${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.`,
|
|
1947
|
+
{ surface, isSecret: false }
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* x402 V1 is Base Sepolia + secret test keys only (ADR D5 + #260). Fail closed
|
|
1953
|
+
* before any network call so mainnet / live / publishable keys never hit the adapter.
|
|
1954
|
+
* Does not change fulfill/settle body shape (A owns #261 / #259 money path).
|
|
1735
1955
|
*/
|
|
1736
1956
|
assertX402Allowed() {
|
|
1957
|
+
this.assertSecretKey("playmos.x402");
|
|
1737
1958
|
if (!this.env.isTest || this.env.network !== "base-sepolia") {
|
|
1738
1959
|
throw new ConfigError(
|
|
1739
|
-
'playmos.x402 is Base Sepolia /
|
|
1740
|
-
{ network: this.env.network, isTest: this.env.isTest }
|
|
1960
|
+
'playmos.x402 is Base Sepolia / sk_test_ only in V1 (ADR D5). Use apiKey sk_test_\u2026 and network "base-sepolia".',
|
|
1961
|
+
{ network: this.env.network, isTest: this.env.isTest, isSecret: this.env.isSecret }
|
|
1741
1962
|
);
|
|
1742
1963
|
}
|
|
1743
1964
|
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Poll GET /payments/:id after server-settle until terminal status.
|
|
1967
|
+
* - Entry (#221): service may return 202 pending (openRound+enter > DO edge).
|
|
1968
|
+
* - IAP: service often returns 201 + txHash while status is still "created"
|
|
1969
|
+
* until chain verify catches PaymentSettled (RPC lag) — poll so pay() matches
|
|
1970
|
+
* the documented "confirmed" sandbox promise (TTFSC-T0 / cold-run).
|
|
1971
|
+
*/
|
|
1972
|
+
async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4) {
|
|
1973
|
+
const start = Date.now();
|
|
1974
|
+
let delayMs = 400;
|
|
1975
|
+
while (Date.now() - start < timeoutMs) {
|
|
1976
|
+
const raw = await this.http.get(
|
|
1977
|
+
`/payments/${encodeURIComponent(paymentId)}`
|
|
1978
|
+
);
|
|
1979
|
+
if (raw.status === "confirmed" || raw.status === "failed") {
|
|
1980
|
+
return this.mapServerPayment(raw, kind, amountMicro);
|
|
1981
|
+
}
|
|
1982
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
1983
|
+
delayMs = Math.min(Math.floor(delayMs * 1.4), 2e3);
|
|
1984
|
+
}
|
|
1985
|
+
throw new ApiError(
|
|
1986
|
+
`Timed out after ${timeoutMs}ms waiting for server-settle of ${paymentId}. Re-check with playmos.verify("${paymentId}").`,
|
|
1987
|
+
{ paymentId, timeout: true, asyncSettle: true }
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1744
1990
|
/**
|
|
1745
1991
|
* Map a server-settled payment (from the `settle: "server"` response) into the
|
|
1746
1992
|
* SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
|
|
@@ -2129,6 +2375,7 @@ exports.PlaymosError = PlaymosError;
|
|
|
2129
2375
|
exports.USDC_ADDRESS = USDC_ADDRESS;
|
|
2130
2376
|
exports.USDC_DECIMALS = USDC_DECIMALS;
|
|
2131
2377
|
exports.WalletConnectionError = WalletConnectionError;
|
|
2378
|
+
exports.clientRuntimeSignals = clientRuntimeSignals;
|
|
2132
2379
|
exports.computeIapSplit = computeIapSplit;
|
|
2133
2380
|
exports.computePayout = computePayout;
|
|
2134
2381
|
exports.computePoolSplit = computePoolSplit;
|
|
@@ -2137,6 +2384,7 @@ exports.createX402Challenge = createX402Challenge;
|
|
|
2137
2384
|
exports.decodePaymentHeader = decodePaymentHeader;
|
|
2138
2385
|
exports.encodePaymentHeader = encodePaymentHeader;
|
|
2139
2386
|
exports.formatMicroToUsd = formatMicroToUsd;
|
|
2387
|
+
exports.isClientRuntime = isClientRuntime;
|
|
2140
2388
|
exports.isWalletSignatureAuthorization = isWalletSignatureAuthorization;
|
|
2141
2389
|
exports.isX402PayloadAuthorization = isX402PayloadAuthorization;
|
|
2142
2390
|
exports.networkToCaip2 = networkToCaip2;
|
|
@@ -2148,9 +2396,8 @@ exports.previewIapSplit = previewIapSplit;
|
|
|
2148
2396
|
exports.previewMarketplaceSplit = previewMarketplaceSplit;
|
|
2149
2397
|
exports.previewPoolSplit = previewPoolSplit;
|
|
2150
2398
|
exports.previewTransferSplit = previewTransferSplit;
|
|
2399
|
+
exports.resolveEnv = resolveEnv;
|
|
2151
2400
|
exports.serializePaymentRequirement = serializePaymentRequirement;
|
|
2152
2401
|
exports.toX402PaymentRequired = toX402PaymentRequired;
|
|
2153
2402
|
exports.ulid = ulid;
|
|
2154
2403
|
exports.validateX402ChallengeInput = validateX402ChallengeInput;
|
|
2155
|
-
//# sourceMappingURL=index.cjs.map
|
|
2156
|
-
//# sourceMappingURL=index.cjs.map
|