@playmos/sdk 0.3.3 → 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 +307 -114
- package/dist/index.d.cts +60 -16
- package/dist/index.d.ts +60 -16
- package/dist/index.js +307 -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 +7 -4
- 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,7 +1001,7 @@ 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,
|
|
@@ -1032,24 +1011,29 @@ var Playmos = class {
|
|
|
1032
1011
|
entrants: 0,
|
|
1033
1012
|
payout: input.payout,
|
|
1034
1013
|
closeAt: input.closeAt,
|
|
1035
|
-
openTxHash:
|
|
1014
|
+
openTxHash: MOCK_TX,
|
|
1036
1015
|
prizePoolAddress: this.config.contracts?.prizePool ?? "0x0000000000000000000000000000000000000002"
|
|
1037
1016
|
};
|
|
1038
|
-
this.mockRounds.set(input.roundId,
|
|
1039
|
-
return
|
|
1017
|
+
this.mockRounds.set(input.roundId, round);
|
|
1018
|
+
return round;
|
|
1040
1019
|
}
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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;
|
|
1051
1034
|
},
|
|
1052
1035
|
lock: async (input) => {
|
|
1036
|
+
this.assertSecretKey("playmos.rounds.lock");
|
|
1053
1037
|
requireField(input?.roundId, "roundId");
|
|
1054
1038
|
if (this.config.mock) {
|
|
1055
1039
|
const existing = this.mockRounds.get(input.roundId);
|
|
@@ -1069,7 +1053,7 @@ var Playmos = class {
|
|
|
1069
1053
|
// Mock payable pool ≈ 60% of entry × max(1, entrants) for shape only — not money truth.
|
|
1070
1054
|
pool: existing.pool ?? existing.entryAmount,
|
|
1071
1055
|
entrants: existing.entrants ?? 0,
|
|
1072
|
-
lockTxHash:
|
|
1056
|
+
lockTxHash: MOCK_TX
|
|
1073
1057
|
};
|
|
1074
1058
|
this.mockRounds.set(input.roundId, locked);
|
|
1075
1059
|
return locked;
|
|
@@ -1081,6 +1065,7 @@ var Playmos = class {
|
|
|
1081
1065
|
return round;
|
|
1082
1066
|
},
|
|
1083
1067
|
settle: async (input) => {
|
|
1068
|
+
this.assertSecretKey("playmos.rounds.settle");
|
|
1084
1069
|
requireField(input?.roundId, "roundId");
|
|
1085
1070
|
if (!input?.results) throw new ConfigError("results are required (ranking or winners)");
|
|
1086
1071
|
if (this.config.mock) {
|
|
@@ -1111,7 +1096,7 @@ var Playmos = class {
|
|
|
1111
1096
|
// Winner-take-all mock shape when ranking only.
|
|
1112
1097
|
amount: i === 0 ? existing.pool ?? existing.entryAmount : "0"
|
|
1113
1098
|
}));
|
|
1114
|
-
const settleTxHash =
|
|
1099
|
+
const settleTxHash = MOCK_TX;
|
|
1115
1100
|
const poolPaid = existing.pool ?? existing.entryAmount;
|
|
1116
1101
|
const settled = {
|
|
1117
1102
|
...existing,
|
|
@@ -1120,6 +1105,19 @@ var Playmos = class {
|
|
|
1120
1105
|
pool: poolPaid
|
|
1121
1106
|
};
|
|
1122
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
|
+
}
|
|
1123
1121
|
return {
|
|
1124
1122
|
roundId: input.roundId,
|
|
1125
1123
|
txHash: settleTxHash,
|
|
@@ -1130,10 +1128,65 @@ var Playmos = class {
|
|
|
1130
1128
|
}
|
|
1131
1129
|
const { settle } = await this.http.post(
|
|
1132
1130
|
`/rounds/${encodeURIComponent(input.roundId)}/settle`,
|
|
1133
|
-
{ gameId: input.gameId, results: input.results }
|
|
1131
|
+
{ gameId: input.gameId, results: input.results },
|
|
1132
|
+
{ acceptStatuses: [202] }
|
|
1134
1133
|
);
|
|
1135
1134
|
return settle;
|
|
1136
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
|
+
},
|
|
1137
1190
|
get: async (input) => {
|
|
1138
1191
|
requireField(input?.roundId, "roundId");
|
|
1139
1192
|
if (this.config.mock) {
|
|
@@ -1158,6 +1211,21 @@ var Playmos = class {
|
|
|
1158
1211
|
prize: async (input) => {
|
|
1159
1212
|
requireField(input?.roundId, "roundId");
|
|
1160
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
|
+
}
|
|
1161
1229
|
try {
|
|
1162
1230
|
const res = await this.http.get(
|
|
1163
1231
|
`/rounds/${encodeURIComponent(input.roundId)}/prize?wallet=${encodeURIComponent(wallet)}`
|
|
@@ -1184,11 +1252,24 @@ var Playmos = class {
|
|
|
1184
1252
|
*/
|
|
1185
1253
|
withdraw: async (input) => {
|
|
1186
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
|
+
}
|
|
1187
1268
|
return {
|
|
1188
|
-
prizePoolAddress:
|
|
1189
|
-
amount:
|
|
1190
|
-
amountMicro:
|
|
1191
|
-
txHash:
|
|
1269
|
+
prizePoolAddress: prizePool2,
|
|
1270
|
+
amount: formatMicroToUsd(amountMicro2),
|
|
1271
|
+
amountMicro: amountMicro2.toString(),
|
|
1272
|
+
txHash: MOCK_TX,
|
|
1192
1273
|
status: "confirmed"
|
|
1193
1274
|
};
|
|
1194
1275
|
}
|
|
@@ -1263,23 +1344,27 @@ var Playmos = class {
|
|
|
1263
1344
|
this.agents = {
|
|
1264
1345
|
/** Assign (or return) the wallet for a game NPC id. Idempotent — safe to call wherever your NPCs spawn. */
|
|
1265
1346
|
createWallet: async (input) => {
|
|
1347
|
+
this.assertSecretKey("playmos.agents");
|
|
1266
1348
|
requireField(input?.agentId, "agentId");
|
|
1267
1349
|
const { agent } = await this.http.post("/agents/wallets", { agentId: input.agentId });
|
|
1268
1350
|
return agent;
|
|
1269
1351
|
},
|
|
1270
1352
|
/** Resolve one NPC's wallet by your id. */
|
|
1271
1353
|
wallet: async (agentId) => {
|
|
1354
|
+
this.assertSecretKey("playmos.agents");
|
|
1272
1355
|
requireField(agentId, "agentId");
|
|
1273
1356
|
const { agent } = await this.http.get(`/agents/wallets/${encodeURIComponent(agentId)}`);
|
|
1274
1357
|
return agent;
|
|
1275
1358
|
},
|
|
1276
1359
|
/** List the NPC wallets you've assigned in this studio. */
|
|
1277
1360
|
list: async () => {
|
|
1361
|
+
this.assertSecretKey("playmos.agents");
|
|
1278
1362
|
const { agents } = await this.http.get("/agents/wallets");
|
|
1279
1363
|
return agents;
|
|
1280
1364
|
},
|
|
1281
1365
|
/** Sandbox faucet: fund an NPC with USDC from your treasury (per-NPC lifetime cap). */
|
|
1282
|
-
fund: (input) => {
|
|
1366
|
+
fund: async (input) => {
|
|
1367
|
+
this.assertSecretKey("playmos.agents.fund");
|
|
1283
1368
|
requireField(input?.agentId, "agentId");
|
|
1284
1369
|
validateAmount(input.amount);
|
|
1285
1370
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
@@ -1290,10 +1375,17 @@ var Playmos = class {
|
|
|
1290
1375
|
);
|
|
1291
1376
|
},
|
|
1292
1377
|
/** NPC→NPC (or →player) USDC transfer, by id. A thin alias of `playmos.transfer` (which is canonical). */
|
|
1293
|
-
pay: (input) => {
|
|
1378
|
+
pay: async (input) => {
|
|
1379
|
+
this.assertSecretKey("playmos.agents.pay");
|
|
1294
1380
|
requireField(input?.from, "from");
|
|
1295
1381
|
requireField(input?.to, "to");
|
|
1296
|
-
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
|
+
});
|
|
1297
1389
|
}
|
|
1298
1390
|
};
|
|
1299
1391
|
/**
|
|
@@ -1477,6 +1569,15 @@ var Playmos = class {
|
|
|
1477
1569
|
};
|
|
1478
1570
|
this.config = config;
|
|
1479
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
|
+
}
|
|
1480
1581
|
this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
|
|
1481
1582
|
}
|
|
1482
1583
|
/** Connect the player's wallet and return their address. */
|
|
@@ -1504,9 +1605,10 @@ var Playmos = class {
|
|
|
1504
1605
|
roundId: req.roundKey,
|
|
1505
1606
|
roundKey: req.roundKey,
|
|
1506
1607
|
identity: req.identity,
|
|
1507
|
-
|
|
1508
|
-
|
|
1608
|
+
playerId: req.wallet,
|
|
1609
|
+
amount: formatMicroToUsd(req.entryUnits)
|
|
1509
1610
|
});
|
|
1611
|
+
this.rememberMock(entry);
|
|
1510
1612
|
const raw = String(entry.status ?? "pending").toLowerCase();
|
|
1511
1613
|
const status = raw === "confirmed" ? "confirmed" : raw === "failed" ? "failed" : "pending";
|
|
1512
1614
|
const onchain = !entry.mock;
|
|
@@ -1546,14 +1648,39 @@ var Playmos = class {
|
|
|
1546
1648
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1547
1649
|
const res = await this.http.post(
|
|
1548
1650
|
"/payments",
|
|
1549
|
-
{
|
|
1550
|
-
|
|
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] }
|
|
1551
1664
|
);
|
|
1552
|
-
|
|
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;
|
|
1553
1670
|
}
|
|
1554
1671
|
const intent = await this.http.post(
|
|
1555
1672
|
"/payments",
|
|
1556
|
-
{
|
|
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
|
+
},
|
|
1557
1684
|
{ idempotencyKey }
|
|
1558
1685
|
);
|
|
1559
1686
|
const provider = resolveProvider(this.config.wallet);
|
|
@@ -1602,23 +1729,48 @@ var Playmos = class {
|
|
|
1602
1729
|
);
|
|
1603
1730
|
}
|
|
1604
1731
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1732
|
+
const pinnedRoundKey = input.roundKey ?? input.roundId;
|
|
1605
1733
|
const res = await this.http.post(
|
|
1606
1734
|
"/payments",
|
|
1607
|
-
{
|
|
1608
|
-
|
|
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] }
|
|
1609
1748
|
);
|
|
1610
|
-
|
|
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
|
+
}
|
|
1611
1753
|
if (input.identity) payment2.identity = input.identity;
|
|
1612
1754
|
const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
1613
1755
|
if (pool) payment2.prizePoolAddress = pool.toLowerCase();
|
|
1614
|
-
payment2.roundKey =
|
|
1756
|
+
payment2.roundKey = input.roundKey ?? res.clientParams?.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
|
|
1615
1757
|
return payment2;
|
|
1616
1758
|
}
|
|
1617
1759
|
const intent = await this.http.post(
|
|
1618
1760
|
"/payments",
|
|
1619
1761
|
// When the game supplies its own roundKey, that IS the round the service
|
|
1620
1762
|
// stores + verifies against (so on-chain enter and server hasEntered align).
|
|
1621
|
-
{
|
|
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
|
+
},
|
|
1622
1774
|
{ idempotencyKey }
|
|
1623
1775
|
);
|
|
1624
1776
|
const provider = resolveProvider(this.config.wallet);
|
|
@@ -1661,6 +1813,7 @@ var Playmos = class {
|
|
|
1661
1813
|
* NPC `from` requires `sk_test_` and settles gaslessly (NPC signs; service relays).
|
|
1662
1814
|
*/
|
|
1663
1815
|
async transfer(input, opts) {
|
|
1816
|
+
this.assertSecretKey("playmos.transfer");
|
|
1664
1817
|
const amountMicro = validateAmount(input.amount);
|
|
1665
1818
|
const from = input.from === void 0 ? void 0 : partyRef(input.from, "from");
|
|
1666
1819
|
const to = partyRef(input.to, "to");
|
|
@@ -1730,16 +1883,55 @@ var Playmos = class {
|
|
|
1730
1883
|
}
|
|
1731
1884
|
// ---- internals ---------------------------------------------------------
|
|
1732
1885
|
/**
|
|
1733
|
-
*
|
|
1734
|
-
*
|
|
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).
|
|
1735
1901
|
*/
|
|
1736
1902
|
assertX402Allowed() {
|
|
1903
|
+
this.assertSecretKey("playmos.x402");
|
|
1737
1904
|
if (!this.env.isTest || this.env.network !== "base-sepolia") {
|
|
1738
1905
|
throw new ConfigError(
|
|
1739
|
-
'playmos.x402 is Base Sepolia /
|
|
1740
|
-
{ 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)}`
|
|
1741
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);
|
|
1742
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
|
+
);
|
|
1743
1935
|
}
|
|
1744
1936
|
/**
|
|
1745
1937
|
* Map a server-settled payment (from the `settle: "server"` response) into the
|
|
@@ -2129,6 +2321,7 @@ exports.PlaymosError = PlaymosError;
|
|
|
2129
2321
|
exports.USDC_ADDRESS = USDC_ADDRESS;
|
|
2130
2322
|
exports.USDC_DECIMALS = USDC_DECIMALS;
|
|
2131
2323
|
exports.WalletConnectionError = WalletConnectionError;
|
|
2324
|
+
exports.clientRuntimeSignals = clientRuntimeSignals;
|
|
2132
2325
|
exports.computeIapSplit = computeIapSplit;
|
|
2133
2326
|
exports.computePayout = computePayout;
|
|
2134
2327
|
exports.computePoolSplit = computePoolSplit;
|
|
@@ -2137,6 +2330,7 @@ exports.createX402Challenge = createX402Challenge;
|
|
|
2137
2330
|
exports.decodePaymentHeader = decodePaymentHeader;
|
|
2138
2331
|
exports.encodePaymentHeader = encodePaymentHeader;
|
|
2139
2332
|
exports.formatMicroToUsd = formatMicroToUsd;
|
|
2333
|
+
exports.isClientRuntime = isClientRuntime;
|
|
2140
2334
|
exports.isWalletSignatureAuthorization = isWalletSignatureAuthorization;
|
|
2141
2335
|
exports.isX402PayloadAuthorization = isX402PayloadAuthorization;
|
|
2142
2336
|
exports.networkToCaip2 = networkToCaip2;
|
|
@@ -2148,9 +2342,8 @@ exports.previewIapSplit = previewIapSplit;
|
|
|
2148
2342
|
exports.previewMarketplaceSplit = previewMarketplaceSplit;
|
|
2149
2343
|
exports.previewPoolSplit = previewPoolSplit;
|
|
2150
2344
|
exports.previewTransferSplit = previewTransferSplit;
|
|
2345
|
+
exports.resolveEnv = resolveEnv;
|
|
2151
2346
|
exports.serializePaymentRequirement = serializePaymentRequirement;
|
|
2152
2347
|
exports.toX402PaymentRequired = toX402PaymentRequired;
|
|
2153
2348
|
exports.ulid = ulid;
|
|
2154
2349
|
exports.validateX402ChallengeInput = validateX402ChallengeInput;
|
|
2155
|
-
//# sourceMappingURL=index.cjs.map
|
|
2156
|
-
//# sourceMappingURL=index.cjs.map
|