@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.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { InvalidAmountError, ConfigError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, ApiError, WalletConnectionError, InsufficientGasError
|
|
2
|
-
export { ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError } from './chunk-
|
|
1
|
+
import { AuthError, InvalidAmountError, ConfigError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, ApiError, WalletConnectionError, InsufficientGasError } from './chunk-VYR6BBHF.js';
|
|
2
|
+
export { ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError } from './chunk-VYR6BBHF.js';
|
|
3
3
|
import { encodeFunctionData, decodeFunctionResult, numberToHex, keccak256, toBytes } from 'viem';
|
|
4
4
|
|
|
5
5
|
// src/config.ts
|
|
@@ -15,9 +15,26 @@ var DEFAULT_API_BASE_URL = {
|
|
|
15
15
|
base: "https://api.playmos.io/v1",
|
|
16
16
|
"base-sepolia": "https://api.sandbox.playmos.io/v1"
|
|
17
17
|
};
|
|
18
|
+
function clientRuntimeSignals(g = globalThis) {
|
|
19
|
+
const any = g;
|
|
20
|
+
const signals = [];
|
|
21
|
+
if (typeof any.window !== "undefined") signals.push("window");
|
|
22
|
+
if (typeof any.document !== "undefined") signals.push("document");
|
|
23
|
+
const likelyCfWorker = typeof any.Cloudflare !== "undefined" || typeof any.WebSocketPair !== "undefined" && typeof any.window === "undefined";
|
|
24
|
+
if (typeof any.importScripts === "function" && !likelyCfWorker) {
|
|
25
|
+
signals.push("importScripts");
|
|
26
|
+
}
|
|
27
|
+
if (any.navigator?.product === "ReactNative") signals.push("ReactNative");
|
|
28
|
+
return signals;
|
|
29
|
+
}
|
|
30
|
+
function isClientRuntime(g = globalThis) {
|
|
31
|
+
return clientRuntimeSignals(g).length > 0;
|
|
32
|
+
}
|
|
18
33
|
function resolveEnv(apiKey, networkOverride, apiBaseUrlOverride) {
|
|
19
34
|
if (typeof apiKey !== "string" || apiKey.trim() === "") {
|
|
20
|
-
throw new AuthError(
|
|
35
|
+
throw new AuthError(
|
|
36
|
+
'Missing apiKey. Expected a "pk_test_", "pk_live_", "sk_test_", or "sk_live_" prefix.'
|
|
37
|
+
);
|
|
21
38
|
}
|
|
22
39
|
const isTest = apiKey.startsWith("pk_test_") || apiKey.startsWith("sk_test_");
|
|
23
40
|
const isLive = apiKey.startsWith("pk_live_") || apiKey.startsWith("sk_live_");
|
|
@@ -27,12 +44,14 @@ function resolveEnv(apiKey, networkOverride, apiBaseUrlOverride) {
|
|
|
27
44
|
{ keyPreview: apiKey.slice(0, 8) }
|
|
28
45
|
);
|
|
29
46
|
}
|
|
47
|
+
const isSecret = apiKey.startsWith("sk_");
|
|
30
48
|
const network = networkOverride ?? (isTest ? "base-sepolia" : "base");
|
|
31
49
|
return {
|
|
32
50
|
network,
|
|
33
51
|
chainId: CHAIN_ID[network],
|
|
34
52
|
apiBaseUrl: apiBaseUrlOverride ?? DEFAULT_API_BASE_URL[network],
|
|
35
|
-
isTest
|
|
53
|
+
isTest,
|
|
54
|
+
isSecret
|
|
36
55
|
};
|
|
37
56
|
}
|
|
38
57
|
|
|
@@ -340,13 +359,6 @@ var erc20Abi = [
|
|
|
340
359
|
{ name: "amount", type: "uint256" }
|
|
341
360
|
],
|
|
342
361
|
outputs: [{ type: "bool" }]
|
|
343
|
-
},
|
|
344
|
-
{
|
|
345
|
-
type: "function",
|
|
346
|
-
name: "balanceOf",
|
|
347
|
-
stateMutability: "view",
|
|
348
|
-
inputs: [{ name: "account", type: "address" }],
|
|
349
|
-
outputs: [{ type: "uint256" }]
|
|
350
362
|
}
|
|
351
363
|
];
|
|
352
364
|
var playmosPayAbi = [
|
|
@@ -363,17 +375,6 @@ var playmosPayAbi = [
|
|
|
363
375
|
}
|
|
364
376
|
];
|
|
365
377
|
var prizePoolAbi = [
|
|
366
|
-
{
|
|
367
|
-
type: "function",
|
|
368
|
-
name: "openRound",
|
|
369
|
-
stateMutability: "nonpayable",
|
|
370
|
-
inputs: [
|
|
371
|
-
{ name: "roundId", type: "bytes32" },
|
|
372
|
-
{ name: "series", type: "bytes32" },
|
|
373
|
-
{ name: "entry", type: "uint256" }
|
|
374
|
-
],
|
|
375
|
-
outputs: []
|
|
376
|
-
},
|
|
377
378
|
{
|
|
378
379
|
type: "function",
|
|
379
380
|
name: "enter",
|
|
@@ -384,50 +385,6 @@ var prizePoolAbi = [
|
|
|
384
385
|
],
|
|
385
386
|
outputs: []
|
|
386
387
|
},
|
|
387
|
-
{
|
|
388
|
-
type: "function",
|
|
389
|
-
name: "lockRound",
|
|
390
|
-
stateMutability: "nonpayable",
|
|
391
|
-
inputs: [{ name: "roundId", type: "bytes32" }],
|
|
392
|
-
outputs: []
|
|
393
|
-
},
|
|
394
|
-
{
|
|
395
|
-
type: "function",
|
|
396
|
-
name: "settle",
|
|
397
|
-
stateMutability: "nonpayable",
|
|
398
|
-
inputs: [
|
|
399
|
-
{ name: "roundId", type: "bytes32" },
|
|
400
|
-
{ name: "winners", type: "address[]" },
|
|
401
|
-
{ name: "amounts", type: "uint256[]" }
|
|
402
|
-
],
|
|
403
|
-
outputs: []
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
type: "function",
|
|
407
|
-
name: "getRound",
|
|
408
|
-
stateMutability: "view",
|
|
409
|
-
inputs: [{ name: "roundId", type: "bytes32" }],
|
|
410
|
-
outputs: [
|
|
411
|
-
{ name: "state", type: "uint8" },
|
|
412
|
-
{ name: "series", type: "bytes32" },
|
|
413
|
-
{ name: "entry", type: "uint256" },
|
|
414
|
-
{ name: "pot", type: "uint256" },
|
|
415
|
-
{ name: "entrantCount", type: "uint256" },
|
|
416
|
-
{ name: "payable_", type: "uint256" },
|
|
417
|
-
{ name: "inheritedSeed", type: "uint256" },
|
|
418
|
-
{ name: "lockedAt", type: "uint256" }
|
|
419
|
-
]
|
|
420
|
-
},
|
|
421
|
-
{
|
|
422
|
-
type: "function",
|
|
423
|
-
name: "hasEntered",
|
|
424
|
-
stateMutability: "view",
|
|
425
|
-
inputs: [
|
|
426
|
-
{ name: "roundId", type: "bytes32" },
|
|
427
|
-
{ name: "identity", type: "bytes32" }
|
|
428
|
-
],
|
|
429
|
-
outputs: [{ type: "bool" }]
|
|
430
|
-
},
|
|
431
388
|
{
|
|
432
389
|
type: "function",
|
|
433
390
|
name: "withdrawable",
|
|
@@ -551,7 +508,7 @@ function buildWithdrawCall(prizePool) {
|
|
|
551
508
|
}
|
|
552
509
|
|
|
553
510
|
// src/mock.ts
|
|
554
|
-
var MOCK_TX = "
|
|
511
|
+
var MOCK_TX = `0x${"0".repeat(56)}deadbeef`;
|
|
555
512
|
function mockIapPayment(args) {
|
|
556
513
|
const { feeMicro, netMicro } = computeIapSplit(args.amountMicro, args.feeBps);
|
|
557
514
|
return {
|
|
@@ -799,10 +756,16 @@ var Playmos = class {
|
|
|
799
756
|
}
|
|
800
757
|
};
|
|
801
758
|
this.payouts = {
|
|
802
|
-
/** Choose how the studio is paid: "usdc" (default) or "fiat" (Bridge). */
|
|
803
|
-
setMode: (mode) =>
|
|
804
|
-
|
|
805
|
-
|
|
759
|
+
/** Choose how the studio is paid: "usdc" (default) or "fiat" (Bridge). Secret key only (#264). */
|
|
760
|
+
setMode: async (mode) => {
|
|
761
|
+
this.assertSecretKey("playmos.payouts.setMode");
|
|
762
|
+
return this.http.post("/payouts/mode", { mode });
|
|
763
|
+
},
|
|
764
|
+
/** Create a Bridge KYC onboarding link (fiat payout). Secret key only (#264). */
|
|
765
|
+
createOnboardingLink: async () => {
|
|
766
|
+
this.assertSecretKey("playmos.payouts.createOnboardingLink");
|
|
767
|
+
return this.http.post("/payouts/onboarding_link", {});
|
|
768
|
+
}
|
|
806
769
|
};
|
|
807
770
|
/**
|
|
808
771
|
* `x402` — Phase 4 HTTP 402 / x402-shaped adapter (#153).
|
|
@@ -852,6 +815,10 @@ var Playmos = class {
|
|
|
852
815
|
* Settle a previously minted challenge with an `x402-payload` authorization.
|
|
853
816
|
* Defaults mode to `server-signer` when omitted (explicit on the wire — fail-closed
|
|
854
817
|
* materialize still requires mode; the route also defaults for this path).
|
|
818
|
+
*
|
|
819
|
+
* #259/#269: settle uses `idempotency-key = requirement.id` and auto-retries gateway 504.
|
|
820
|
+
* A 504 replay may return **without throwing** with `success: false`, `status: "settling"`,
|
|
821
|
+
* `txHash: null` (in-flight). Treat as non-final — re-call or verify later; do not new-key.
|
|
855
822
|
*/
|
|
856
823
|
fulfill: async (requirement, authorization) => {
|
|
857
824
|
this.assertX402Allowed();
|
|
@@ -885,10 +852,19 @@ var Playmos = class {
|
|
|
885
852
|
}
|
|
886
853
|
if (payload.scheme === void 0) payload.scheme = "exact";
|
|
887
854
|
if (payload.requirementId === void 0) payload.requirementId = requirement.id;
|
|
888
|
-
const res = await this.http.post(
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
855
|
+
const res = await this.http.post(
|
|
856
|
+
"/x402/settle",
|
|
857
|
+
{
|
|
858
|
+
requirement,
|
|
859
|
+
authorization: { kind: "x402-payload", payload }
|
|
860
|
+
},
|
|
861
|
+
// Settlement idempotency anchor = requirement.id (settlement.ts: "Settlement
|
|
862
|
+
// idempotency key + x402 challenge id"). Without this header the settle POST is
|
|
863
|
+
// excluded from the safe gateway-504 auto-retry (isRetryable), and the SDK's own
|
|
864
|
+
// 504 message ("retry with the same idempotency key (safe)") would be a lie —
|
|
865
|
+
// a 504 is exactly when the on-chain settle may have already landed (#259).
|
|
866
|
+
{ idempotencyKey: requirement.id }
|
|
867
|
+
);
|
|
892
868
|
const s = res.settlement;
|
|
893
869
|
return {
|
|
894
870
|
requirementId: s.requirementId,
|
|
@@ -931,8 +907,11 @@ var Playmos = class {
|
|
|
931
907
|
*/
|
|
932
908
|
/** Offline mock store for rounds.open/lock/settle/get when `mock: true` (3-game dogfood). */
|
|
933
909
|
this.mockRounds = /* @__PURE__ */ new Map();
|
|
910
|
+
/** Mock withdrawable credits after settle — key `${roundId}:${walletLower}` (#240 offline prize). */
|
|
911
|
+
this.mockWithdrawable = /* @__PURE__ */ new Map();
|
|
934
912
|
this.rounds = {
|
|
935
913
|
open: async (input) => {
|
|
914
|
+
this.assertSecretKey("playmos.rounds.open");
|
|
936
915
|
requireField(input?.gameId, "gameId");
|
|
937
916
|
requireField(input?.roundId, "roundId");
|
|
938
917
|
requireField(input?.entryAmount, "entryAmount");
|
|
@@ -952,34 +931,41 @@ var Playmos = class {
|
|
|
952
931
|
code: "conflict"
|
|
953
932
|
});
|
|
954
933
|
}
|
|
955
|
-
const
|
|
934
|
+
const round = {
|
|
956
935
|
roundId: input.roundId,
|
|
957
936
|
gameId: input.gameId,
|
|
958
937
|
roundKey,
|
|
938
|
+
// #351 — must persist seriesKey so activeForSeries mock is authoritative
|
|
939
|
+
seriesKey: input.seriesKey ?? input.gameId,
|
|
959
940
|
status: "open",
|
|
960
941
|
entryAmount: input.entryAmount,
|
|
961
942
|
pool: null,
|
|
962
943
|
entrants: 0,
|
|
963
944
|
payout: input.payout,
|
|
964
945
|
closeAt: input.closeAt,
|
|
965
|
-
openTxHash:
|
|
946
|
+
openTxHash: MOCK_TX,
|
|
966
947
|
prizePoolAddress: this.config.contracts?.prizePool ?? "0x0000000000000000000000000000000000000002"
|
|
967
948
|
};
|
|
968
|
-
this.mockRounds.set(input.roundId,
|
|
969
|
-
return
|
|
949
|
+
this.mockRounds.set(input.roundId, round);
|
|
950
|
+
return round;
|
|
970
951
|
}
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
952
|
+
const body = await this.http.post(
|
|
953
|
+
"/rounds",
|
|
954
|
+
{
|
|
955
|
+
gameId: input.gameId,
|
|
956
|
+
roundId: input.roundId,
|
|
957
|
+
entryAmount: input.entryAmount,
|
|
958
|
+
payout: input.payout,
|
|
959
|
+
closeAt: input.closeAt,
|
|
960
|
+
seriesKey: input.seriesKey,
|
|
961
|
+
roundKey: input.roundKey
|
|
962
|
+
},
|
|
963
|
+
{ acceptStatuses: [202] }
|
|
964
|
+
);
|
|
965
|
+
return body.round;
|
|
981
966
|
},
|
|
982
967
|
lock: async (input) => {
|
|
968
|
+
this.assertSecretKey("playmos.rounds.lock");
|
|
983
969
|
requireField(input?.roundId, "roundId");
|
|
984
970
|
if (this.config.mock) {
|
|
985
971
|
const existing = this.mockRounds.get(input.roundId);
|
|
@@ -999,7 +985,7 @@ var Playmos = class {
|
|
|
999
985
|
// Mock payable pool ≈ 60% of entry × max(1, entrants) for shape only — not money truth.
|
|
1000
986
|
pool: existing.pool ?? existing.entryAmount,
|
|
1001
987
|
entrants: existing.entrants ?? 0,
|
|
1002
|
-
lockTxHash:
|
|
988
|
+
lockTxHash: MOCK_TX
|
|
1003
989
|
};
|
|
1004
990
|
this.mockRounds.set(input.roundId, locked);
|
|
1005
991
|
return locked;
|
|
@@ -1011,6 +997,7 @@ var Playmos = class {
|
|
|
1011
997
|
return round;
|
|
1012
998
|
},
|
|
1013
999
|
settle: async (input) => {
|
|
1000
|
+
this.assertSecretKey("playmos.rounds.settle");
|
|
1014
1001
|
requireField(input?.roundId, "roundId");
|
|
1015
1002
|
if (!input?.results) throw new ConfigError("results are required (ranking or winners)");
|
|
1016
1003
|
if (this.config.mock) {
|
|
@@ -1041,7 +1028,7 @@ var Playmos = class {
|
|
|
1041
1028
|
// Winner-take-all mock shape when ranking only.
|
|
1042
1029
|
amount: i === 0 ? existing.pool ?? existing.entryAmount : "0"
|
|
1043
1030
|
}));
|
|
1044
|
-
const settleTxHash =
|
|
1031
|
+
const settleTxHash = MOCK_TX;
|
|
1045
1032
|
const poolPaid = existing.pool ?? existing.entryAmount;
|
|
1046
1033
|
const settled = {
|
|
1047
1034
|
...existing,
|
|
@@ -1050,6 +1037,19 @@ var Playmos = class {
|
|
|
1050
1037
|
pool: poolPaid
|
|
1051
1038
|
};
|
|
1052
1039
|
this.mockRounds.set(input.roundId, settled);
|
|
1040
|
+
for (const w of winners) {
|
|
1041
|
+
const wAddr = String(w.wallet).toLowerCase();
|
|
1042
|
+
let micro = 0n;
|
|
1043
|
+
try {
|
|
1044
|
+
micro = validateAmount(String(w.amount ?? "0"));
|
|
1045
|
+
} catch {
|
|
1046
|
+
micro = 0n;
|
|
1047
|
+
}
|
|
1048
|
+
if (micro > 0n) {
|
|
1049
|
+
const k = `${input.roundId}:${wAddr}`;
|
|
1050
|
+
this.mockWithdrawable.set(k, (this.mockWithdrawable.get(k) ?? 0n) + micro);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
1053
|
return {
|
|
1054
1054
|
roundId: input.roundId,
|
|
1055
1055
|
txHash: settleTxHash,
|
|
@@ -1060,10 +1060,65 @@ var Playmos = class {
|
|
|
1060
1060
|
}
|
|
1061
1061
|
const { settle } = await this.http.post(
|
|
1062
1062
|
`/rounds/${encodeURIComponent(input.roundId)}/settle`,
|
|
1063
|
-
{ gameId: input.gameId, results: input.results }
|
|
1063
|
+
{ gameId: input.gameId, results: input.results },
|
|
1064
|
+
{ acceptStatuses: [202] }
|
|
1064
1065
|
);
|
|
1065
1066
|
return settle;
|
|
1066
1067
|
},
|
|
1068
|
+
/**
|
|
1069
|
+
* Cancel an open/locked round (operator sk_ only). Refunds entrants' escrowed ~90%
|
|
1070
|
+
* and frees the series latch. May return `status: "cancelling"` (HTTP 202) until
|
|
1071
|
+
* chain Cancelled(4) reconciles — poll `rounds.get` or re-call cancel (#346).
|
|
1072
|
+
*/
|
|
1073
|
+
cancel: async (input) => {
|
|
1074
|
+
this.assertSecretKey("playmos.rounds.cancel");
|
|
1075
|
+
requireField(input?.roundId, "roundId");
|
|
1076
|
+
if (this.config.mock) {
|
|
1077
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1078
|
+
if (!existing) {
|
|
1079
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1080
|
+
}
|
|
1081
|
+
if (existing.status === "cancelled") {
|
|
1082
|
+
return {
|
|
1083
|
+
roundId: input.roundId,
|
|
1084
|
+
txHash: existing.cancelTxHash ?? MOCK_TX,
|
|
1085
|
+
status: "cancelled",
|
|
1086
|
+
round: existing
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
if (existing.status === "settled") {
|
|
1090
|
+
throw new ApiError(`round ${input.roundId} is already settled \u2014 cannot cancel`, {
|
|
1091
|
+
status: 409,
|
|
1092
|
+
code: "conflict"
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
const cancelled = {
|
|
1096
|
+
...existing,
|
|
1097
|
+
status: "cancelled",
|
|
1098
|
+
cancelTxHash: MOCK_TX,
|
|
1099
|
+
pool: null
|
|
1100
|
+
};
|
|
1101
|
+
this.mockRounds.set(input.roundId, cancelled);
|
|
1102
|
+
return {
|
|
1103
|
+
roundId: input.roundId,
|
|
1104
|
+
txHash: MOCK_TX,
|
|
1105
|
+
status: "cancelled",
|
|
1106
|
+
round: cancelled
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
const body = await this.http.post(
|
|
1110
|
+
`/rounds/${encodeURIComponent(input.roundId)}/cancel`,
|
|
1111
|
+
{ gameId: input.gameId },
|
|
1112
|
+
{ acceptStatuses: [202] }
|
|
1113
|
+
);
|
|
1114
|
+
const status = body.status === "cancelling" || body.round?.status === "cancelling" ? "cancelling" : "cancelled";
|
|
1115
|
+
return {
|
|
1116
|
+
roundId: input.roundId,
|
|
1117
|
+
txHash: body.txHash ?? body.round?.cancelTxHash ?? null,
|
|
1118
|
+
status,
|
|
1119
|
+
round: body.round
|
|
1120
|
+
};
|
|
1121
|
+
},
|
|
1067
1122
|
get: async (input) => {
|
|
1068
1123
|
requireField(input?.roundId, "roundId");
|
|
1069
1124
|
if (this.config.mock) {
|
|
@@ -1078,6 +1133,58 @@ var Playmos = class {
|
|
|
1078
1133
|
);
|
|
1079
1134
|
return round;
|
|
1080
1135
|
},
|
|
1136
|
+
/**
|
|
1137
|
+
* Authoritative series latch (#351) — `null` means **proven free**.
|
|
1138
|
+
* `GET /v1/series/:seriesKey/active?gameId=…` — not a candidate probe.
|
|
1139
|
+
*/
|
|
1140
|
+
activeForSeries: async (input) => {
|
|
1141
|
+
requireField(input?.seriesKey, "seriesKey");
|
|
1142
|
+
requireField(input?.gameId, "gameId");
|
|
1143
|
+
if (this.config.mock) {
|
|
1144
|
+
for (const r of this.mockRounds.values()) {
|
|
1145
|
+
if (r.gameId !== input.gameId) continue;
|
|
1146
|
+
const sk = r.seriesKey ?? r.gameId;
|
|
1147
|
+
if (sk !== input.seriesKey) continue;
|
|
1148
|
+
if (r.status === "open" || r.status === "locked" || r.status === "opening" || r.status === "cancelling") {
|
|
1149
|
+
return {
|
|
1150
|
+
roundId: r.roundId,
|
|
1151
|
+
roundKey: r.roundKey,
|
|
1152
|
+
roundIdHash: "0x" + "m".repeat(64),
|
|
1153
|
+
status: r.status,
|
|
1154
|
+
entrants: r.entrants,
|
|
1155
|
+
pool: r.pool,
|
|
1156
|
+
openTxHash: r.openTxHash ?? null,
|
|
1157
|
+
cancelTxHash: r.cancelTxHash ?? null,
|
|
1158
|
+
prizePoolAddress: r.prizePoolAddress ?? "0x0000000000000000000000000000000000000002"
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
const body = await this.http.get(
|
|
1165
|
+
`/series/${encodeURIComponent(input.seriesKey)}/active?gameId=${encodeURIComponent(input.gameId)}`
|
|
1166
|
+
);
|
|
1167
|
+
if (body == null || typeof body !== "object") {
|
|
1168
|
+
throw new ApiError("activeForSeries: malformed response (expected object body)", {
|
|
1169
|
+
code: "invalid_response",
|
|
1170
|
+
status: 502
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
if (!("active" in body)) {
|
|
1174
|
+
throw new ApiError("activeForSeries: malformed response (missing active field)", {
|
|
1175
|
+
code: "invalid_response",
|
|
1176
|
+
status: 502
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
if (body.active === null) return null;
|
|
1180
|
+
if (body.active == null || typeof body.active !== "object") {
|
|
1181
|
+
throw new ApiError("activeForSeries: malformed response (active must be object or null)", {
|
|
1182
|
+
code: "invalid_response",
|
|
1183
|
+
status: 502
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
return body.active;
|
|
1187
|
+
},
|
|
1081
1188
|
/**
|
|
1082
1189
|
* Read a wallet's **claimable** prize balance for a round (issue #41).
|
|
1083
1190
|
* Prefers the service chain read (`GET /v1/rounds/:id/prize?wallet=`); falls
|
|
@@ -1088,6 +1195,21 @@ var Playmos = class {
|
|
|
1088
1195
|
prize: async (input) => {
|
|
1089
1196
|
requireField(input?.roundId, "roundId");
|
|
1090
1197
|
const wallet = requireAddressField(input?.wallet, "wallet");
|
|
1198
|
+
if (this.config.mock) {
|
|
1199
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1200
|
+
if (!existing) {
|
|
1201
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1202
|
+
}
|
|
1203
|
+
const claimableMicro2 = this.mockWithdrawable.get(`${input.roundId}:${wallet.toLowerCase()}`) ?? 0n;
|
|
1204
|
+
const prizePoolAddress2 = input.prizePoolAddress ?? existing.prizePoolAddress ?? "0x0000000000000000000000000000000000000002";
|
|
1205
|
+
return {
|
|
1206
|
+
roundId: input.roundId,
|
|
1207
|
+
prizePoolAddress: prizePoolAddress2,
|
|
1208
|
+
wallet,
|
|
1209
|
+
claimable: formatMicroToUsd(claimableMicro2),
|
|
1210
|
+
claimableMicro: claimableMicro2.toString()
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1091
1213
|
try {
|
|
1092
1214
|
const res = await this.http.get(
|
|
1093
1215
|
`/rounds/${encodeURIComponent(input.roundId)}/prize?wallet=${encodeURIComponent(wallet)}`
|
|
@@ -1114,11 +1236,24 @@ var Playmos = class {
|
|
|
1114
1236
|
*/
|
|
1115
1237
|
withdraw: async (input) => {
|
|
1116
1238
|
if (this.config.mock) {
|
|
1239
|
+
let amountMicro2 = 0n;
|
|
1240
|
+
let prizePool2 = input.prizePoolAddress ?? "0x0000000000000000000000000000000000000001";
|
|
1241
|
+
if (input.roundId) {
|
|
1242
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
1243
|
+
if (existing?.prizePoolAddress) prizePool2 = existing.prizePoolAddress;
|
|
1244
|
+
for (const [k, v] of this.mockWithdrawable) {
|
|
1245
|
+
if (k.startsWith(`${input.roundId}:`) && v > 0n) {
|
|
1246
|
+
amountMicro2 = v;
|
|
1247
|
+
this.mockWithdrawable.set(k, 0n);
|
|
1248
|
+
break;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1117
1252
|
return {
|
|
1118
|
-
prizePoolAddress:
|
|
1119
|
-
amount:
|
|
1120
|
-
amountMicro:
|
|
1121
|
-
txHash:
|
|
1253
|
+
prizePoolAddress: prizePool2,
|
|
1254
|
+
amount: formatMicroToUsd(amountMicro2),
|
|
1255
|
+
amountMicro: amountMicro2.toString(),
|
|
1256
|
+
txHash: MOCK_TX,
|
|
1122
1257
|
status: "confirmed"
|
|
1123
1258
|
};
|
|
1124
1259
|
}
|
|
@@ -1193,23 +1328,27 @@ var Playmos = class {
|
|
|
1193
1328
|
this.agents = {
|
|
1194
1329
|
/** Assign (or return) the wallet for a game NPC id. Idempotent — safe to call wherever your NPCs spawn. */
|
|
1195
1330
|
createWallet: async (input) => {
|
|
1331
|
+
this.assertSecretKey("playmos.agents");
|
|
1196
1332
|
requireField(input?.agentId, "agentId");
|
|
1197
1333
|
const { agent } = await this.http.post("/agents/wallets", { agentId: input.agentId });
|
|
1198
1334
|
return agent;
|
|
1199
1335
|
},
|
|
1200
1336
|
/** Resolve one NPC's wallet by your id. */
|
|
1201
1337
|
wallet: async (agentId) => {
|
|
1338
|
+
this.assertSecretKey("playmos.agents");
|
|
1202
1339
|
requireField(agentId, "agentId");
|
|
1203
1340
|
const { agent } = await this.http.get(`/agents/wallets/${encodeURIComponent(agentId)}`);
|
|
1204
1341
|
return agent;
|
|
1205
1342
|
},
|
|
1206
1343
|
/** List the NPC wallets you've assigned in this studio. */
|
|
1207
1344
|
list: async () => {
|
|
1345
|
+
this.assertSecretKey("playmos.agents");
|
|
1208
1346
|
const { agents } = await this.http.get("/agents/wallets");
|
|
1209
1347
|
return agents;
|
|
1210
1348
|
},
|
|
1211
1349
|
/** Sandbox faucet: fund an NPC with USDC from your treasury (per-NPC lifetime cap). */
|
|
1212
|
-
fund: (input) => {
|
|
1350
|
+
fund: async (input) => {
|
|
1351
|
+
this.assertSecretKey("playmos.agents.fund");
|
|
1213
1352
|
requireField(input?.agentId, "agentId");
|
|
1214
1353
|
validateAmount(input.amount);
|
|
1215
1354
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
@@ -1220,10 +1359,17 @@ var Playmos = class {
|
|
|
1220
1359
|
);
|
|
1221
1360
|
},
|
|
1222
1361
|
/** NPC→NPC (or →player) USDC transfer, by id. A thin alias of `playmos.transfer` (which is canonical). */
|
|
1223
|
-
pay: (input) => {
|
|
1362
|
+
pay: async (input) => {
|
|
1363
|
+
this.assertSecretKey("playmos.agents.pay");
|
|
1224
1364
|
requireField(input?.from, "from");
|
|
1225
1365
|
requireField(input?.to, "to");
|
|
1226
|
-
return this.transfer({
|
|
1366
|
+
return this.transfer({
|
|
1367
|
+
from: input.from,
|
|
1368
|
+
to: input.to,
|
|
1369
|
+
amount: input.amount,
|
|
1370
|
+
feeBps: input.feeBps,
|
|
1371
|
+
feeSink: input.feeSink
|
|
1372
|
+
});
|
|
1227
1373
|
}
|
|
1228
1374
|
};
|
|
1229
1375
|
/**
|
|
@@ -1407,6 +1553,15 @@ var Playmos = class {
|
|
|
1407
1553
|
};
|
|
1408
1554
|
this.config = config;
|
|
1409
1555
|
this.env = resolveEnv(config.apiKey, config.network, config.apiBaseUrl);
|
|
1556
|
+
if (this.env.isSecret) {
|
|
1557
|
+
const signals = clientRuntimeSignals();
|
|
1558
|
+
if (signals.length > 0) {
|
|
1559
|
+
throw new AuthError(
|
|
1560
|
+
"Secret (sk_) key used in a client runtime \u2014 sk_ keys are server-only; use a pk_ key client-side.",
|
|
1561
|
+
{ isSecret: true, clientRuntime: true, signals }
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1410
1565
|
this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
|
|
1411
1566
|
}
|
|
1412
1567
|
/** Connect the player's wallet and return their address. */
|
|
@@ -1434,9 +1589,10 @@ var Playmos = class {
|
|
|
1434
1589
|
roundId: req.roundKey,
|
|
1435
1590
|
roundKey: req.roundKey,
|
|
1436
1591
|
identity: req.identity,
|
|
1437
|
-
|
|
1438
|
-
|
|
1592
|
+
playerId: req.wallet,
|
|
1593
|
+
amount: formatMicroToUsd(req.entryUnits)
|
|
1439
1594
|
});
|
|
1595
|
+
this.rememberMock(entry);
|
|
1440
1596
|
const raw = String(entry.status ?? "pending").toLowerCase();
|
|
1441
1597
|
const status = raw === "confirmed" ? "confirmed" : raw === "failed" ? "failed" : "pending";
|
|
1442
1598
|
const onchain = !entry.mock;
|
|
@@ -1476,14 +1632,39 @@ var Playmos = class {
|
|
|
1476
1632
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1477
1633
|
const res = await this.http.post(
|
|
1478
1634
|
"/payments",
|
|
1479
|
-
{
|
|
1480
|
-
|
|
1635
|
+
{
|
|
1636
|
+
kind: "iap",
|
|
1637
|
+
amount: input.amount,
|
|
1638
|
+
sku: input.sku,
|
|
1639
|
+
playerId: input.playerId,
|
|
1640
|
+
gameId: input.gameId,
|
|
1641
|
+
studio: input.studio,
|
|
1642
|
+
metadata,
|
|
1643
|
+
settle: "server",
|
|
1644
|
+
idempotencyKey
|
|
1645
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1646
|
+
},
|
|
1647
|
+
{ idempotencyKey, acceptStatuses: [201, 202] }
|
|
1481
1648
|
);
|
|
1482
|
-
|
|
1649
|
+
let payment = this.mapServerPayment(res.payment, "iap", amountMicro);
|
|
1650
|
+
if (payment.status === "pending" || payment.status === "created") {
|
|
1651
|
+
payment = await this.waitForServerSettle(payment.id, "iap", amountMicro);
|
|
1652
|
+
}
|
|
1653
|
+
return payment;
|
|
1483
1654
|
}
|
|
1484
1655
|
const intent = await this.http.post(
|
|
1485
1656
|
"/payments",
|
|
1486
|
-
{
|
|
1657
|
+
{
|
|
1658
|
+
kind: "iap",
|
|
1659
|
+
amount: input.amount,
|
|
1660
|
+
sku: input.sku,
|
|
1661
|
+
playerId: input.playerId,
|
|
1662
|
+
gameId: input.gameId,
|
|
1663
|
+
studio: input.studio,
|
|
1664
|
+
metadata,
|
|
1665
|
+
idempotencyKey
|
|
1666
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1667
|
+
},
|
|
1487
1668
|
{ idempotencyKey }
|
|
1488
1669
|
);
|
|
1489
1670
|
const provider = resolveProvider(this.config.wallet);
|
|
@@ -1532,23 +1713,48 @@ var Playmos = class {
|
|
|
1532
1713
|
);
|
|
1533
1714
|
}
|
|
1534
1715
|
if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
|
|
1716
|
+
const pinnedRoundKey = input.roundKey ?? input.roundId;
|
|
1535
1717
|
const res = await this.http.post(
|
|
1536
1718
|
"/payments",
|
|
1537
|
-
{
|
|
1538
|
-
|
|
1719
|
+
{
|
|
1720
|
+
kind: "entry",
|
|
1721
|
+
amount: input.amount,
|
|
1722
|
+
gameId: input.gameId,
|
|
1723
|
+
roundId: pinnedRoundKey,
|
|
1724
|
+
identity: input.identity,
|
|
1725
|
+
playerId: input.playerId,
|
|
1726
|
+
metadata,
|
|
1727
|
+
settle: "server",
|
|
1728
|
+
idempotencyKey
|
|
1729
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1730
|
+
},
|
|
1731
|
+
{ idempotencyKey, acceptStatuses: [201, 202] }
|
|
1539
1732
|
);
|
|
1540
|
-
|
|
1733
|
+
let payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
|
|
1734
|
+
if (payment2.status === "pending" || payment2.status === "created") {
|
|
1735
|
+
payment2 = await this.waitForServerSettle(payment2.id, "entry", amountMicro);
|
|
1736
|
+
}
|
|
1541
1737
|
if (input.identity) payment2.identity = input.identity;
|
|
1542
1738
|
const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
1543
1739
|
if (pool) payment2.prizePoolAddress = pool.toLowerCase();
|
|
1544
|
-
payment2.roundKey =
|
|
1740
|
+
payment2.roundKey = input.roundKey ?? res.clientParams?.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
|
|
1545
1741
|
return payment2;
|
|
1546
1742
|
}
|
|
1547
1743
|
const intent = await this.http.post(
|
|
1548
1744
|
"/payments",
|
|
1549
1745
|
// When the game supplies its own roundKey, that IS the round the service
|
|
1550
1746
|
// stores + verifies against (so on-chain enter and server hasEntered align).
|
|
1551
|
-
{
|
|
1747
|
+
{
|
|
1748
|
+
kind: "entry",
|
|
1749
|
+
amount: input.amount,
|
|
1750
|
+
gameId: input.gameId,
|
|
1751
|
+
roundId: input.roundKey ?? input.roundId,
|
|
1752
|
+
identity: input.identity,
|
|
1753
|
+
playerId: input.playerId,
|
|
1754
|
+
metadata,
|
|
1755
|
+
idempotencyKey
|
|
1756
|
+
/* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
|
|
1757
|
+
},
|
|
1552
1758
|
{ idempotencyKey }
|
|
1553
1759
|
);
|
|
1554
1760
|
const provider = resolveProvider(this.config.wallet);
|
|
@@ -1591,6 +1797,7 @@ var Playmos = class {
|
|
|
1591
1797
|
* NPC `from` requires `sk_test_` and settles gaslessly (NPC signs; service relays).
|
|
1592
1798
|
*/
|
|
1593
1799
|
async transfer(input, opts) {
|
|
1800
|
+
this.assertSecretKey("playmos.transfer");
|
|
1594
1801
|
const amountMicro = validateAmount(input.amount);
|
|
1595
1802
|
const from = input.from === void 0 ? void 0 : partyRef(input.from, "from");
|
|
1596
1803
|
const to = partyRef(input.to, "to");
|
|
@@ -1660,17 +1867,56 @@ var Playmos = class {
|
|
|
1660
1867
|
}
|
|
1661
1868
|
// ---- internals ---------------------------------------------------------
|
|
1662
1869
|
/**
|
|
1663
|
-
*
|
|
1664
|
-
*
|
|
1870
|
+
* Secret (sk_) surfaces — agents / transfer / x402. Capability tracks key type
|
|
1871
|
+
* in code, not README prose (#260). pk_ keys are client-safe and refuse these.
|
|
1872
|
+
*/
|
|
1873
|
+
assertSecretKey(surface) {
|
|
1874
|
+
if (!this.env.isSecret) {
|
|
1875
|
+
throw new AuthError(
|
|
1876
|
+
`${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.`,
|
|
1877
|
+
{ surface, isSecret: false }
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* x402 V1 is Base Sepolia + secret test keys only (ADR D5 + #260). Fail closed
|
|
1883
|
+
* before any network call so mainnet / live / publishable keys never hit the adapter.
|
|
1884
|
+
* Does not change fulfill/settle body shape (A owns #261 / #259 money path).
|
|
1665
1885
|
*/
|
|
1666
1886
|
assertX402Allowed() {
|
|
1887
|
+
this.assertSecretKey("playmos.x402");
|
|
1667
1888
|
if (!this.env.isTest || this.env.network !== "base-sepolia") {
|
|
1668
1889
|
throw new ConfigError(
|
|
1669
|
-
'playmos.x402 is Base Sepolia /
|
|
1670
|
-
{ network: this.env.network, isTest: this.env.isTest }
|
|
1890
|
+
'playmos.x402 is Base Sepolia / sk_test_ only in V1 (ADR D5). Use apiKey sk_test_\u2026 and network "base-sepolia".',
|
|
1891
|
+
{ network: this.env.network, isTest: this.env.isTest, isSecret: this.env.isSecret }
|
|
1671
1892
|
);
|
|
1672
1893
|
}
|
|
1673
1894
|
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Poll GET /payments/:id after server-settle until terminal status.
|
|
1897
|
+
* - Entry (#221): service may return 202 pending (openRound+enter > DO edge).
|
|
1898
|
+
* - IAP: service often returns 201 + txHash while status is still "created"
|
|
1899
|
+
* until chain verify catches PaymentSettled (RPC lag) — poll so pay() matches
|
|
1900
|
+
* the documented "confirmed" sandbox promise (TTFSC-T0 / cold-run).
|
|
1901
|
+
*/
|
|
1902
|
+
async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4) {
|
|
1903
|
+
const start = Date.now();
|
|
1904
|
+
let delayMs = 400;
|
|
1905
|
+
while (Date.now() - start < timeoutMs) {
|
|
1906
|
+
const raw = await this.http.get(
|
|
1907
|
+
`/payments/${encodeURIComponent(paymentId)}`
|
|
1908
|
+
);
|
|
1909
|
+
if (raw.status === "confirmed" || raw.status === "failed") {
|
|
1910
|
+
return this.mapServerPayment(raw, kind, amountMicro);
|
|
1911
|
+
}
|
|
1912
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
1913
|
+
delayMs = Math.min(Math.floor(delayMs * 1.4), 2e3);
|
|
1914
|
+
}
|
|
1915
|
+
throw new ApiError(
|
|
1916
|
+
`Timed out after ${timeoutMs}ms waiting for server-settle of ${paymentId}. Re-check with playmos.verify("${paymentId}").`,
|
|
1917
|
+
{ paymentId, timeout: true, asyncSettle: true }
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1674
1920
|
/**
|
|
1675
1921
|
* Map a server-settled payment (from the `settle: "server"` response) into the
|
|
1676
1922
|
* SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
|
|
@@ -2042,6 +2288,4 @@ function isX402PayloadAuthorization(auth) {
|
|
|
2042
2288
|
return auth.kind === "x402-payload";
|
|
2043
2289
|
}
|
|
2044
2290
|
|
|
2045
|
-
export { CHAIN_ID, DEFAULT_API_BASE_URL, MICRO_PER_USDC, PayoutError, Playmos, USDC_ADDRESS, USDC_DECIMALS, computeIapSplit, computePayout, computePoolSplit, createPaymentRequirement, createX402Challenge, decodePaymentHeader, encodePaymentHeader, formatMicroToUsd, isWalletSignatureAuthorization, isX402PayloadAuthorization, networkToCaip2, parsePaymentRequirement, parseUsdToMicro, prefixedId, previewEscrowFee, previewIapSplit, previewMarketplaceSplit, previewPoolSplit, previewTransferSplit, serializePaymentRequirement, toX402PaymentRequired, ulid, validateX402ChallengeInput };
|
|
2046
|
-
//# sourceMappingURL=index.js.map
|
|
2047
|
-
//# sourceMappingURL=index.js.map
|
|
2291
|
+
export { CHAIN_ID, DEFAULT_API_BASE_URL, MICRO_PER_USDC, PayoutError, Playmos, USDC_ADDRESS, USDC_DECIMALS, clientRuntimeSignals, computeIapSplit, computePayout, computePoolSplit, createPaymentRequirement, createX402Challenge, decodePaymentHeader, encodePaymentHeader, formatMicroToUsd, isClientRuntime, isWalletSignatureAuthorization, isX402PayloadAuthorization, networkToCaip2, parsePaymentRequirement, parseUsdToMicro, prefixedId, previewEscrowFee, previewIapSplit, previewMarketplaceSplit, previewPoolSplit, previewTransferSplit, resolveEnv, serializePaymentRequirement, toX402PaymentRequired, ulid, validateX402ChallengeInput };
|