@playmos/sdk 0.3.11 → 0.3.12

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/dist/index.cjs CHANGED
@@ -802,6 +802,182 @@ async function assertEnoughGas(provider, from, minWei) {
802
802
  });
803
803
  }
804
804
  }
805
+ function seriesToBytes32(series) {
806
+ const s = series.trim();
807
+ if (/^0x[0-9a-fA-F]{64}$/.test(s)) return s.toLowerCase();
808
+ return viem.keccak256(viem.toHex(s));
809
+ }
810
+ var identityToBytes32 = seriesToBytes32;
811
+
812
+ // src/chain/epochPrizePoolAbi.ts
813
+ var epochPrizePoolViewAbi = [
814
+ {
815
+ type: "function",
816
+ name: "currentEpochId",
817
+ stateMutability: "view",
818
+ inputs: [{ name: "series", type: "bytes32" }],
819
+ outputs: [{ type: "uint256" }]
820
+ },
821
+ {
822
+ type: "function",
823
+ name: "epochIdAt",
824
+ stateMutability: "view",
825
+ inputs: [
826
+ { name: "series", type: "bytes32" },
827
+ { name: "timestamp", type: "uint256" }
828
+ ],
829
+ outputs: [{ type: "uint256" }]
830
+ },
831
+ {
832
+ type: "function",
833
+ name: "epochPool",
834
+ stateMutability: "view",
835
+ inputs: [
836
+ { name: "series", type: "bytes32" },
837
+ { name: "epochId", type: "uint256" }
838
+ ],
839
+ outputs: [{ type: "uint256" }]
840
+ },
841
+ {
842
+ type: "function",
843
+ name: "epochOutgoingSeed",
844
+ stateMutability: "view",
845
+ inputs: [
846
+ { name: "series", type: "bytes32" },
847
+ { name: "epochId", type: "uint256" }
848
+ ],
849
+ outputs: [{ type: "uint256" }]
850
+ },
851
+ {
852
+ type: "function",
853
+ name: "epochIncomingSeed",
854
+ stateMutability: "view",
855
+ inputs: [
856
+ { name: "series", type: "bytes32" },
857
+ { name: "epochId", type: "uint256" }
858
+ ],
859
+ outputs: [{ type: "uint256" }]
860
+ },
861
+ {
862
+ type: "function",
863
+ name: "epochEntryCount",
864
+ stateMutability: "view",
865
+ inputs: [
866
+ { name: "series", type: "bytes32" },
867
+ { name: "epochId", type: "uint256" }
868
+ ],
869
+ outputs: [{ type: "uint256" }]
870
+ },
871
+ {
872
+ type: "function",
873
+ name: "getSeries",
874
+ stateMutability: "view",
875
+ inputs: [{ name: "series", type: "bytes32" }],
876
+ outputs: [
877
+ { name: "created", type: "bool" },
878
+ { name: "genesis", type: "uint256" },
879
+ { name: "epochDuration", type: "uint256" },
880
+ { name: "entry", type: "uint256" },
881
+ { name: "feeBps", type: "uint16" },
882
+ { name: "poolBps", type: "uint16" },
883
+ { name: "seedBps", type: "uint16" }
884
+ ]
885
+ },
886
+ {
887
+ type: "function",
888
+ name: "getEpoch",
889
+ stateMutability: "view",
890
+ inputs: [
891
+ { name: "series", type: "bytes32" },
892
+ { name: "epochId", type: "uint256" }
893
+ ],
894
+ outputs: [
895
+ { name: "pool", type: "uint256" },
896
+ { name: "outgoingSeed", type: "uint256" },
897
+ { name: "incomingSeed", type: "uint256" },
898
+ { name: "entryCount_", type: "uint256" },
899
+ { name: "terminal", type: "uint8" }
900
+ ]
901
+ },
902
+ {
903
+ type: "function",
904
+ name: "entryCount",
905
+ stateMutability: "view",
906
+ inputs: [
907
+ { name: "series", type: "bytes32" },
908
+ { name: "epochId", type: "uint256" },
909
+ { name: "identity", type: "bytes32" }
910
+ ],
911
+ outputs: [{ type: "uint256" }]
912
+ },
913
+ {
914
+ type: "function",
915
+ name: "claimableRefund",
916
+ stateMutability: "view",
917
+ inputs: [
918
+ { name: "series", type: "bytes32" },
919
+ { name: "epochId", type: "uint256" },
920
+ { name: "payer", type: "address" }
921
+ ],
922
+ outputs: [{ type: "uint256" }]
923
+ },
924
+ {
925
+ type: "function",
926
+ name: "withdrawable",
927
+ stateMutability: "view",
928
+ inputs: [{ name: "account", type: "address" }],
929
+ outputs: [{ type: "uint256" }]
930
+ }
931
+ ];
932
+ var epochPrizePoolEnterAbi = [
933
+ {
934
+ type: "function",
935
+ name: "enter",
936
+ stateMutability: "nonpayable",
937
+ inputs: [
938
+ { name: "series", type: "bytes32" },
939
+ { name: "identity", type: "bytes32" }
940
+ ],
941
+ outputs: []
942
+ }
943
+ ];
944
+ var epochPrizePoolRefundAbi = [
945
+ {
946
+ type: "function",
947
+ name: "claimRefund",
948
+ stateMutability: "nonpayable",
949
+ inputs: [
950
+ { name: "series", type: "bytes32" },
951
+ { name: "epochId", type: "uint256" },
952
+ { name: "payer", type: "address" }
953
+ ],
954
+ outputs: [{ name: "amount", type: "uint256" }]
955
+ },
956
+ {
957
+ type: "function",
958
+ name: "withdraw",
959
+ stateMutability: "nonpayable",
960
+ inputs: [],
961
+ outputs: []
962
+ }
963
+ ];
964
+ var epochPrizePoolExecuteSettlementAbi = [
965
+ {
966
+ type: "function",
967
+ name: "executeSignedSettlement",
968
+ stateMutability: "nonpayable",
969
+ inputs: [
970
+ { name: "series", type: "bytes32" },
971
+ { name: "epochId", type: "uint256" },
972
+ { name: "winners", type: "address[]" },
973
+ { name: "amounts", type: "uint256[]" },
974
+ { name: "signature", type: "bytes" }
975
+ ],
976
+ outputs: []
977
+ }
978
+ ];
979
+
980
+ // src/chain/calls.ts
805
981
  var toBytes32 = (s) => viem.keccak256(viem.toBytes(s));
806
982
  function buildIapCalls(args) {
807
983
  const payData = viem.encodeFunctionData({
@@ -833,6 +1009,53 @@ function buildWithdrawCall(prizePool) {
833
1009
  });
834
1010
  return { to: prizePool, data };
835
1011
  }
1012
+ function buildEpochExecuteSettlementCall(args) {
1013
+ return {
1014
+ to: args.epochPrizePool,
1015
+ data: viem.encodeFunctionData({
1016
+ abi: epochPrizePoolExecuteSettlementAbi,
1017
+ functionName: "executeSignedSettlement",
1018
+ args: [
1019
+ seriesToBytes32(args.series),
1020
+ args.epochId,
1021
+ args.winners.map((w) => viem.getAddress(w.toLowerCase())),
1022
+ args.amounts,
1023
+ args.signature
1024
+ ]
1025
+ })
1026
+ };
1027
+ }
1028
+ function buildEpochEntryCalls(args) {
1029
+ const enterData = viem.encodeFunctionData({
1030
+ abi: epochPrizePoolEnterAbi,
1031
+ functionName: "enter",
1032
+ args: [seriesToBytes32(args.series), identityToBytes32(args.identity)]
1033
+ });
1034
+ return [
1035
+ { to: args.usdc, data: encodeApprove(args.epochPrizePool, args.entryMicro) },
1036
+ { to: args.epochPrizePool, data: enterData }
1037
+ ];
1038
+ }
1039
+ function buildEpochClaimRefundCall(args) {
1040
+ return {
1041
+ to: args.epochPrizePool,
1042
+ data: viem.encodeFunctionData({
1043
+ abi: epochPrizePoolRefundAbi,
1044
+ functionName: "claimRefund",
1045
+ args: [seriesToBytes32(args.series), args.epochId, args.payer]
1046
+ })
1047
+ };
1048
+ }
1049
+ function buildEpochWithdrawCall(epochPrizePool) {
1050
+ return {
1051
+ to: epochPrizePool,
1052
+ data: viem.encodeFunctionData({
1053
+ abi: epochPrizePoolRefundAbi,
1054
+ functionName: "withdraw",
1055
+ args: []
1056
+ })
1057
+ };
1058
+ }
836
1059
 
837
1060
  // src/mock.ts
838
1061
  var MOCK_TX = `0x${"0".repeat(56)}deadbeef`;
@@ -894,13 +1117,893 @@ function mockVerifyResult(payment) {
894
1117
  return { ...payment, mock: true };
895
1118
  }
896
1119
 
897
- // src/x402.ts
1120
+ // src/epochs.ts
1121
+ var PLAYMOS_FEE_SINK_DEFAULT = "0x1b8031e20ed96131a849a52290b4d640f286998d";
1122
+ var EPOCH_POOL_CONSTRUCTOR_TYPES = [
1123
+ { type: "address" },
1124
+ { type: "address" },
1125
+ { type: "address" },
1126
+ { type: "address" },
1127
+ { type: "uint256" }
1128
+ ];
1129
+ function encodeEpochPoolConstructorArgs(args) {
1130
+ return viem.encodeAbiParameters(EPOCH_POOL_CONSTRUCTOR_TYPES, [
1131
+ args.token,
1132
+ args.admin,
1133
+ args.operator,
1134
+ args.feeSink,
1135
+ args.refundTimeout
1136
+ ]);
1137
+ }
1138
+ function derivedEpochId(genesis, epochDuration, at) {
1139
+ if (epochDuration === 0n) {
1140
+ throw new ConfigError("epochDuration must be > 0");
1141
+ }
1142
+ if (at < genesis) {
1143
+ throw new ConfigError("epoch has not started (at < genesis)");
1144
+ }
1145
+ return (at - genesis) / epochDuration;
1146
+ }
1147
+ function epochPayableMicro(incomingSeed, pool) {
1148
+ return incomingSeed + pool;
1149
+ }
1150
+ function mockPodiumPayable(winners) {
1151
+ if (winners.length === 0) return void 0;
1152
+ let total = 0n;
1153
+ for (const w of winners) {
1154
+ if (typeof w === "string") return void 0;
1155
+ if (w.amountMicro != null && w.amountMicro !== "") {
1156
+ try {
1157
+ total += BigInt(w.amountMicro);
1158
+ } catch {
1159
+ return void 0;
1160
+ }
1161
+ continue;
1162
+ }
1163
+ if (w.amount) {
1164
+ try {
1165
+ total += parseUsdToMicro(w.amount);
1166
+ } catch {
1167
+ return void 0;
1168
+ }
1169
+ continue;
1170
+ }
1171
+ return void 0;
1172
+ }
1173
+ return {
1174
+ payable: formatMicroToUsd(total),
1175
+ payableMicro: total.toString()
1176
+ };
1177
+ }
1178
+ function mockWinnerMicro(w) {
1179
+ if (typeof w === "string") return void 0;
1180
+ if (w.amountMicro != null && w.amountMicro !== "" && /^\d+$/.test(w.amountMicro)) {
1181
+ return BigInt(w.amountMicro);
1182
+ }
1183
+ if (w.amount) {
1184
+ try {
1185
+ return parseUsdToMicro(w.amount);
1186
+ } catch {
1187
+ return void 0;
1188
+ }
1189
+ }
1190
+ return void 0;
1191
+ }
1192
+ function mockWinnerAmount(w) {
1193
+ const micro = mockWinnerMicro(w);
1194
+ if (micro === void 0) return {};
1195
+ return { amount: formatMicroToUsd(micro), amountMicro: micro.toString() };
1196
+ }
1197
+ var TERMINALS = ["none", "settled", "refunded", "rolled"];
1198
+ function terminalFromChain(raw) {
1199
+ return TERMINALS[raw] ?? "none";
1200
+ }
1201
+ function requireSeries(series) {
1202
+ if (typeof series !== "string" || series.trim() === "") {
1203
+ throw new MissingFieldError("series");
1204
+ }
1205
+ return series.trim();
1206
+ }
1207
+ function requireIdentity(identity) {
1208
+ if (typeof identity !== "string" || identity.trim() === "") {
1209
+ throw new MissingFieldError("identity");
1210
+ }
1211
+ return identity.trim();
1212
+ }
1213
+ function assertPinParity(label, local, fromService) {
1214
+ if (!fromService) {
1215
+ throw new ConfigError(
1216
+ `epochs.enter: the service did not return ${label}Bytes32 \u2014 cannot prove the on-chain id matches before paying.`,
1217
+ { field: `${label}Bytes32` }
1218
+ );
1219
+ }
1220
+ if (local.toLowerCase() !== fromService.toLowerCase()) {
1221
+ throw new ConfigError(
1222
+ `epochs.enter: ${label} derives differently in the SDK and the service (sdk=${local}, service=${fromService}). Refusing to enter \u2014 a paid entry under a mismatched id is never scored. See sdk#633.`,
1223
+ { field: `${label}Bytes32` }
1224
+ );
1225
+ }
1226
+ }
1227
+ function epochPathId(epochId) {
1228
+ if (epochId === void 0 || epochId === null || epochId === "") return "current";
1229
+ const s = String(epochId).trim();
1230
+ if (s === "current") return "current";
1231
+ if (!/^\d+$/.test(s)) {
1232
+ throw new ConfigError(`epochId must be a non-negative integer, got: ${JSON.stringify(epochId)}`);
1233
+ }
1234
+ return s;
1235
+ }
1236
+ function qs(params) {
1237
+ const u = new URLSearchParams();
1238
+ for (const [k, v] of Object.entries(params)) {
1239
+ if (v) u.set(k, v);
1240
+ }
1241
+ const s = u.toString();
1242
+ return s ? `?${s}` : "";
1243
+ }
1244
+ function pictureFromMicros(args) {
1245
+ const payableMicro = epochPayableMicro(args.incomingSeedMicro, args.poolMicro);
1246
+ return {
1247
+ series: args.series,
1248
+ epochId: args.epochId,
1249
+ pool: formatMicroToUsd(args.poolMicro),
1250
+ incomingSeed: formatMicroToUsd(args.incomingSeedMicro),
1251
+ outgoingSeed: formatMicroToUsd(args.outgoingSeedMicro),
1252
+ payable: formatMicroToUsd(payableMicro),
1253
+ poolMicro: args.poolMicro.toString(),
1254
+ incomingSeedMicro: args.incomingSeedMicro.toString(),
1255
+ outgoingSeedMicro: args.outgoingSeedMicro.toString(),
1256
+ payableMicro: payableMicro.toString(),
1257
+ via: args.via,
1258
+ epochPrizePool: args.epochPrizePool,
1259
+ ...args.mock ? { mock: true } : {}
1260
+ };
1261
+ }
1262
+ function toBig(value, fallback) {
1263
+ if (value === void 0) return fallback;
1264
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) return BigInt(Math.trunc(value));
1265
+ if (typeof value === "string" && /^\d+$/.test(value.trim())) return BigInt(value.trim());
1266
+ throw new ConfigError(`expected a non-negative integer, got: ${JSON.stringify(value)}`);
1267
+ }
898
1268
  var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
1269
+ function requireAddr(raw, field) {
1270
+ if (typeof raw !== "string" || !ADDRESS_RE2.test(raw.trim())) {
1271
+ throw new MissingFieldError(field);
1272
+ }
1273
+ return raw.trim().toLowerCase();
1274
+ }
1275
+ function requireAddrKeepCase(raw, field) {
1276
+ if (typeof raw !== "string" || !ADDRESS_RE2.test(raw.trim())) {
1277
+ throw new MissingFieldError(field);
1278
+ }
1279
+ return raw.trim();
1280
+ }
1281
+ function requireEpochPool(input, cfg) {
1282
+ const raw = input?.epochPrizePool ?? cfg.contracts?.epochPrizePool;
1283
+ if (!raw) {
1284
+ throw new ConfigError(
1285
+ "epochs refund rail needs contracts.epochPrizePool (or pass epochPrizePool) \u2014 this is EpochPrizePool, not PrizePool",
1286
+ { field: "epochPrizePool" }
1287
+ );
1288
+ }
1289
+ return requireAddr(raw, "epochPrizePool");
1290
+ }
1291
+ function requirePastEpochId(epochId) {
1292
+ const id = epochPathId(epochId);
1293
+ if (id === "current") {
1294
+ throw new ConfigError("epochs refund rail requires a past epochId \u2014 Current is still taking entries");
1295
+ }
1296
+ return id;
1297
+ }
1298
+ function walletStatus(status) {
1299
+ if (status === "CONFIRMED") return "confirmed";
1300
+ if (status === "FAILED") return "failed";
1301
+ return "pending";
1302
+ }
1303
+ function sameAddr(a, b) {
1304
+ return a.toLowerCase() === b.toLowerCase();
1305
+ }
1306
+ function prepareStudioPoolLocal(input, playmosFeeSink, token, playmosAddresses = []) {
1307
+ const studio = requireAddr(input.studioWallet, "studioWallet");
1308
+ const sink = playmosFeeSink.toLowerCase();
1309
+ const forbidden = [sink, ...playmosAddresses.map((a) => a.toLowerCase())];
1310
+ if (forbidden.some((a) => sameAddr(a, studio))) {
1311
+ throw new ConfigError("studioWallet must not be a Playmos address (Playmos never holds studio keys)");
1312
+ }
1313
+ if (input.feeSink && !sameAddr(input.feeSink, sink)) {
1314
+ throw new ConfigError("feeSink must equal the Playmos sink; refused before broadcast");
1315
+ }
1316
+ if (input.admin && !sameAddr(input.admin, studio)) {
1317
+ throw new ConfigError("admin must be the studio wallet from block one");
1318
+ }
1319
+ if (input.operator && !sameAddr(input.operator, studio)) {
1320
+ throw new ConfigError("operator must be the studio wallet from block one");
1321
+ }
1322
+ if (input.admin && forbidden.some((a) => sameAddr(a, input.admin))) {
1323
+ throw new ConfigError("Playmos must not be ADMIN_ROLE");
1324
+ }
1325
+ if (input.operator && forbidden.some((a) => sameAddr(a, input.operator))) {
1326
+ throw new ConfigError("Playmos must not be OPERATOR_ROLE");
1327
+ }
1328
+ const refundTimeout = toBig(input.refundTimeout, 604800n);
1329
+ if (refundTimeout < 3600n || refundTimeout > 7776000n) {
1330
+ throw new ConfigError("refundTimeout must be between 3600 (1h) and 7776000 (90d) seconds");
1331
+ }
1332
+ return {
1333
+ token: token.toLowerCase(),
1334
+ admin: studio,
1335
+ operator: studio,
1336
+ feeSink: sink,
1337
+ refundTimeout: refundTimeout.toString(),
1338
+ constructorArgs: encodeEpochPoolConstructorArgs({
1339
+ token: token.toLowerCase(),
1340
+ admin: studio,
1341
+ operator: studio,
1342
+ feeSink: sink,
1343
+ refundTimeout
1344
+ }),
1345
+ constructor: "EpochPrizePool",
1346
+ studioHoldsAdmin: true,
1347
+ studioHoldsOperator: true,
1348
+ playmosIsAdmin: false,
1349
+ playmosIsOperator: false,
1350
+ via: "mock",
1351
+ mock: true
1352
+ };
1353
+ }
1354
+ function epochPoolProofMessage(terms) {
1355
+ return [
1356
+ "Playmos EpochPrizePool registration",
1357
+ `studio: ${terms.studioId}`,
1358
+ `pool: ${terms.poolAddress.toLowerCase()}`,
1359
+ `wallet: ${terms.studioWallet.toLowerCase()}`,
1360
+ `chainId: ${terms.chainId}`
1361
+ ].join("\n");
1362
+ }
1363
+ async function readWithdrawableMicro(deps, pool) {
1364
+ const read = deps.readView;
1365
+ const account = deps.walletAddress;
1366
+ if (!read || !account) return null;
1367
+ try {
1368
+ const raw = await read(
1369
+ pool,
1370
+ viem.encodeFunctionData({
1371
+ abi: epochPrizePoolViewAbi,
1372
+ functionName: "withdrawable",
1373
+ args: [await account()]
1374
+ })
1375
+ );
1376
+ return viem.decodeFunctionResult({
1377
+ abi: epochPrizePoolViewAbi,
1378
+ functionName: "withdrawable",
1379
+ data: raw
1380
+ });
1381
+ } catch {
1382
+ return null;
1383
+ }
1384
+ }
1385
+ function settlementTypedData(input) {
1386
+ const pool = requireAddr(input.pool, "pool");
1387
+ const series = requireSeries(input.series);
1388
+ const epochId = requirePastEpochId(input.epochId);
1389
+ if (!Array.isArray(input.winners) || !Array.isArray(input.amounts)) {
1390
+ throw new MissingFieldError("winners");
1391
+ }
1392
+ if (input.winners.length !== input.amounts.length) {
1393
+ throw new ConfigError("winners and amounts must be the same length");
1394
+ }
1395
+ return {
1396
+ domain: {
1397
+ name: "EpochPrizePool",
1398
+ version: "1",
1399
+ chainId: input.chainId,
1400
+ verifyingContract: pool
1401
+ },
1402
+ types: {
1403
+ Settlement: [
1404
+ { name: "series", type: "bytes32" },
1405
+ { name: "epochId", type: "uint256" },
1406
+ { name: "winners", type: "address[]" },
1407
+ { name: "amounts", type: "uint256[]" }
1408
+ ]
1409
+ },
1410
+ primaryType: "Settlement",
1411
+ message: {
1412
+ series: seriesToBytes32(series),
1413
+ epochId: BigInt(epochId),
1414
+ winners: input.winners.map((w, i) => requireAddrKeepCase(w, `winners[${i}]`)),
1415
+ amounts: input.amounts.map((a, i) => parseMicroString(a, `amounts[${i}]`))
1416
+ }
1417
+ };
1418
+ }
1419
+ function parseMicroString(raw, field) {
1420
+ if (typeof raw !== "string" || !/^(0|[1-9]\d*)$/.test(raw)) {
1421
+ throw new ConfigError(`${field} must be an integer micro-USDC string`, { field });
1422
+ }
1423
+ return BigInt(raw);
1424
+ }
1425
+ async function readEpochTerminal(deps, pool, series, epochId) {
1426
+ const read = deps.readView;
1427
+ if (!read) return null;
1428
+ try {
1429
+ const raw = await read(
1430
+ pool,
1431
+ viem.encodeFunctionData({
1432
+ abi: epochPrizePoolViewAbi,
1433
+ functionName: "getEpoch",
1434
+ args: [seriesToBytes32(series), BigInt(epochId)]
1435
+ })
1436
+ );
1437
+ const decoded = viem.decodeFunctionResult({
1438
+ abi: epochPrizePoolViewAbi,
1439
+ functionName: "getEpoch",
1440
+ data: raw
1441
+ });
1442
+ const termRaw = decoded && typeof decoded === "object" && "terminal" in decoded ? Number(decoded.terminal) : Number(decoded[4]);
1443
+ return terminalFromChain(termRaw);
1444
+ } catch {
1445
+ return null;
1446
+ }
1447
+ }
1448
+ function createEpochsApi(deps) {
1449
+ return {
1450
+ async currentId(input) {
1451
+ const series = requireSeries(input?.series);
1452
+ const cfg = deps.config();
1453
+ if (cfg.mock) {
1454
+ const genesis = toBig(input.genesis, 0n);
1455
+ const duration = toBig(input.epochDuration, 1n);
1456
+ const at = toBig(input.at, BigInt(Math.floor(Date.now() / 1e3)));
1457
+ const epochId = derivedEpochId(genesis, duration, at);
1458
+ return {
1459
+ series,
1460
+ epochId: epochId.toString(),
1461
+ via: "mock",
1462
+ epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
1463
+ mock: true
1464
+ };
1465
+ }
1466
+ const path = `/epochs/current${qs({
1467
+ series,
1468
+ epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
1469
+ })}`;
1470
+ return deps.http().get(path);
1471
+ },
1472
+ async prize(input) {
1473
+ const series = requireSeries(input?.series);
1474
+ const cfg = deps.config();
1475
+ const epochId = epochPathId(input?.epochId);
1476
+ if (cfg.mock) {
1477
+ return pictureFromMicros({
1478
+ series,
1479
+ epochId: epochId === "current" ? "0" : epochId,
1480
+ poolMicro: 0n,
1481
+ incomingSeedMicro: 0n,
1482
+ outgoingSeedMicro: 0n,
1483
+ via: "mock",
1484
+ epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
1485
+ mock: true
1486
+ });
1487
+ }
1488
+ const path = `/epochs/${encodeURIComponent(epochId)}/prize${qs({
1489
+ series,
1490
+ epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
1491
+ })}`;
1492
+ const res = await deps.http().get(path);
1493
+ return res.prize;
1494
+ },
1495
+ async get(input) {
1496
+ const series = requireSeries(input?.series);
1497
+ const cfg = deps.config();
1498
+ const epochId = epochPathId(input?.epochId);
1499
+ if (cfg.mock) {
1500
+ const prize = pictureFromMicros({
1501
+ series,
1502
+ epochId: epochId === "current" ? "0" : epochId,
1503
+ poolMicro: 0n,
1504
+ incomingSeedMicro: 0n,
1505
+ outgoingSeedMicro: 0n,
1506
+ via: "mock",
1507
+ epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
1508
+ mock: true
1509
+ });
1510
+ return {
1511
+ ...prize,
1512
+ entryCount: "0",
1513
+ terminal: "none"
1514
+ };
1515
+ }
1516
+ const path = `/epochs/${encodeURIComponent(epochId)}${qs({
1517
+ series,
1518
+ epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
1519
+ })}`;
1520
+ return deps.http().get(path);
1521
+ },
1522
+ async getSeries(input) {
1523
+ const series = requireSeries(input?.series);
1524
+ const cfg = deps.config();
1525
+ if (cfg.mock) {
1526
+ return {
1527
+ series,
1528
+ created: true,
1529
+ genesis: "0",
1530
+ epochDuration: "1",
1531
+ entry: "0",
1532
+ feeBps: 1e3,
1533
+ poolBps: 6e3,
1534
+ seedBps: 3e3,
1535
+ via: "mock",
1536
+ epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
1537
+ mock: true
1538
+ };
1539
+ }
1540
+ const path = `/epochs/series${qs({
1541
+ series,
1542
+ epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
1543
+ })}`;
1544
+ return deps.http().get(path);
1545
+ },
1546
+ async preparePool(input) {
1547
+ const studioWallet = requireAddr(input?.studioWallet, "studioWallet");
1548
+ const cfg = deps.config();
1549
+ const sink = (input.playmosFeeSink ?? PLAYMOS_FEE_SINK_DEFAULT).toLowerCase();
1550
+ const token = (input.token ?? cfg.contracts?.usdc ?? "0x036cbd53842c5426634e7929541ec2318f3dcf7e").toLowerCase();
1551
+ if (cfg.mock) {
1552
+ return prepareStudioPoolLocal({ ...input, studioWallet }, sink, token);
1553
+ }
1554
+ return deps.http().post("/epochs/pools/prepare", {
1555
+ studioWallet,
1556
+ feeSink: input.feeSink,
1557
+ admin: input.admin,
1558
+ operator: input.operator,
1559
+ refundTimeout: input.refundTimeout
1560
+ });
1561
+ },
1562
+ async registerPool(input) {
1563
+ const studioWallet = requireAddr(input?.studioWallet, "studioWallet");
1564
+ const poolAddress = requireAddr(input?.poolAddress, "poolAddress");
1565
+ const cfg = deps.config();
1566
+ if (cfg.mock) {
1567
+ const sink = (input.feeSink ?? PLAYMOS_FEE_SINK_DEFAULT).toLowerCase();
1568
+ const token = (cfg.contracts?.usdc ?? "0x036cbd53842c5426634e7929541ec2318f3dcf7e").toLowerCase();
1569
+ const prepared = prepareStudioPoolLocal({ studioWallet, feeSink: input.feeSink, admin: input.admin, operator: input.operator }, sink, token);
1570
+ return {
1571
+ poolAddress,
1572
+ studioWallet: prepared.admin,
1573
+ feeSink: prepared.feeSink,
1574
+ admin: prepared.admin,
1575
+ operator: prepared.operator,
1576
+ studioHoldsAdmin: true,
1577
+ studioHoldsOperator: true,
1578
+ playmosIsAdmin: false,
1579
+ playmosIsOperator: false,
1580
+ ownership: input.walletProof ? "confirmed" : "pending",
1581
+ owner: null,
1582
+ via: "mock",
1583
+ mock: true
1584
+ };
1585
+ }
1586
+ return deps.http().post("/epochs/pools", {
1587
+ studioWallet,
1588
+ poolAddress,
1589
+ feeSink: input.feeSink,
1590
+ admin: input.admin,
1591
+ operator: input.operator,
1592
+ txHash: input.txHash,
1593
+ walletProof: input.walletProof
1594
+ });
1595
+ },
1596
+ async createPool(input) {
1597
+ const prepared = await this.preparePool(input);
1598
+ const cfg = deps.config();
1599
+ if (cfg.mock) {
1600
+ const poolAddress = input.poolAddress ? requireAddr(input.poolAddress, "poolAddress") : `0x${prepared.constructorArgs.slice(2, 42).padEnd(40, "0")}`;
1601
+ return this.registerPool({
1602
+ studioWallet: prepared.admin,
1603
+ poolAddress,
1604
+ feeSink: prepared.feeSink,
1605
+ walletProof: input.walletProof
1606
+ });
1607
+ }
1608
+ if (!input.poolAddress) {
1609
+ throw new ConfigError(
1610
+ "createPool on the live path needs poolAddress after the studio wallet submits the constructor \u2014 Playmos does not broadcast"
1611
+ );
1612
+ }
1613
+ return this.registerPool({
1614
+ studioWallet: prepared.admin,
1615
+ poolAddress: input.poolAddress,
1616
+ feeSink: prepared.feeSink,
1617
+ txHash: input.txHash,
1618
+ walletProof: input.walletProof
1619
+ });
1620
+ },
1621
+ async settle(input) {
1622
+ const series = requireSeries(input?.series);
1623
+ if (!Array.isArray(input?.winners)) {
1624
+ throw new MissingFieldError("winners");
1625
+ }
1626
+ const epochId = epochPathId(input?.epochId);
1627
+ if (epochId === "current") {
1628
+ throw new ConfigError("epochs.settle requires a past epochId \u2014 Current is still taking entries");
1629
+ }
1630
+ const cfg = deps.config();
1631
+ if (!cfg.mock) {
1632
+ if (deps.assertSecretKey) deps.assertSecretKey("playmos.epochs.settle");
1633
+ else if (!String(cfg.apiKey ?? "").startsWith("sk_")) {
1634
+ throw new AuthError(
1635
+ "playmos.epochs.settle requires a secret (sk_) key \u2014 use sk_test_\u2026 / sk_live_\u2026 on the server. Publishable (pk_) keys cannot settle.",
1636
+ { surface: "playmos.epochs.settle", isSecret: false }
1637
+ );
1638
+ }
1639
+ }
1640
+ if (cfg.mock) {
1641
+ const empty = input.winners.length === 0;
1642
+ const podiumPayable = mockPodiumPayable(input.winners);
1643
+ return {
1644
+ series,
1645
+ epochId,
1646
+ status: empty ? "rolled" : "settled",
1647
+ txHash: "0x0000000000000000000000000000000000000000000000000000000000000001",
1648
+ winners: input.winners.map((w) => {
1649
+ const wallet = (typeof w === "string" ? w : w.wallet).toLowerCase();
1650
+ return { wallet, ...mockWinnerAmount(w) };
1651
+ }),
1652
+ ...podiumPayable ?? {},
1653
+ terminal: empty ? "rolled" : "settled",
1654
+ via: "mock",
1655
+ epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
1656
+ mock: true
1657
+ };
1658
+ }
1659
+ const body = await deps.http().post(
1660
+ `/epochs/${encodeURIComponent(epochId)}/settle`,
1661
+ {
1662
+ series,
1663
+ winners: input.winners,
1664
+ epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
1665
+ },
1666
+ { acceptStatuses: [200, 202] }
1667
+ );
1668
+ return body.settle;
1669
+ },
1670
+ async enter(input) {
1671
+ const series = requireSeries(input?.series);
1672
+ const identity = requireIdentity(input?.identity);
1673
+ const cfg = deps.config();
1674
+ const seriesBytes32 = seriesToBytes32(series);
1675
+ const identityBytes32 = identityToBytes32(identity);
1676
+ if (cfg.mock) {
1677
+ const genesis = toBig(input.genesis, 0n);
1678
+ const duration = toBig(input.epochDuration, 1n);
1679
+ const at = toBig(input.at, BigInt(Math.floor(Date.now() / 1e3)));
1680
+ return {
1681
+ series,
1682
+ identity,
1683
+ epochId: derivedEpochId(genesis, duration, at).toString(),
1684
+ epochIdSource: "mock",
1685
+ seriesBytes32,
1686
+ identityBytes32,
1687
+ entry: formatMicroToUsd(C1_ENTER_SPLIT_MICRO.entry),
1688
+ entryMicro: C1_ENTER_SPLIT_MICRO.entry.toString(),
1689
+ status: "confirmed",
1690
+ txHash: MOCK_TX,
1691
+ identityEntryCount: "1",
1692
+ epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
1693
+ usdc: cfg.contracts?.usdc ?? null,
1694
+ via: "mock",
1695
+ mock: true
1696
+ };
1697
+ }
1698
+ const epochPrizePool = input.epochPrizePool ?? cfg.contracts?.epochPrizePool;
1699
+ const plan = await deps.http().post("/epochs/enter", {
1700
+ series,
1701
+ identity,
1702
+ ...epochPrizePool ? { epochPrizePool } : {}
1703
+ });
1704
+ assertPinParity("series", seriesBytes32, plan.clientParams?.seriesBytes32);
1705
+ assertPinParity("identity", identityBytes32, plan.clientParams?.identityBytes32);
1706
+ const send = deps.sendEntry;
1707
+ if (!send) {
1708
+ throw new ConfigError(
1709
+ "epochs.enter needs a player wallet \u2014 pass `wallet` to the Playmos client. Entry money goes straight to the contract; Playmos never holds it. Building the calls yourself? Import `buildEpochEntryCalls`.",
1710
+ { field: "wallet" }
1711
+ );
1712
+ }
1713
+ const entryMicro = BigInt(plan.clientParams.entryMicro);
1714
+ const calls = buildEpochEntryCalls({
1715
+ usdc: plan.clientParams.usdc,
1716
+ epochPrizePool: plan.clientParams.epochPrizePool,
1717
+ series,
1718
+ identity,
1719
+ entryMicro
1720
+ });
1721
+ const sent = await send(calls, { paymasterUrl: plan.clientParams.paymasterUrl });
1722
+ const base = {
1723
+ series,
1724
+ identity,
1725
+ epochId: plan.entry.epochId,
1726
+ epochIdSource: "chain-clock",
1727
+ seriesBytes32,
1728
+ identityBytes32,
1729
+ entry: plan.entry.entry,
1730
+ entryMicro: plan.entry.entryMicro,
1731
+ status: sent.status === "FAILED" ? "failed" : "pending",
1732
+ txHash: sent.txHash,
1733
+ epochPrizePool: plan.clientParams.epochPrizePool,
1734
+ usdc: plan.clientParams.usdc,
1735
+ via: "service"
1736
+ };
1737
+ if (!sent.txHash) return base;
1738
+ try {
1739
+ const confirmed = await deps.http().post("/epochs/enter/confirm", {
1740
+ series,
1741
+ identity,
1742
+ txHash: sent.txHash,
1743
+ ...epochPrizePool ? { epochPrizePool } : {}
1744
+ });
1745
+ return {
1746
+ ...base,
1747
+ epochId: confirmed.entry.epochId,
1748
+ epochIdSource: confirmed.entry.epochIdSource,
1749
+ status: confirmed.entry.status,
1750
+ identityEntryCount: confirmed.entry.identityEntryCount
1751
+ };
1752
+ } catch {
1753
+ return base;
1754
+ }
1755
+ },
1756
+ async claimableRefund(input) {
1757
+ const series = requireSeries(input?.series);
1758
+ const payer = requireAddr(input?.payer, "payer");
1759
+ const epochId = requirePastEpochId(input?.epochId);
1760
+ const cfg = deps.config();
1761
+ const seriesBytes32 = seriesToBytes32(series);
1762
+ const epochPrizePool = input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null;
1763
+ if (cfg.mock) {
1764
+ return {
1765
+ series,
1766
+ epochId,
1767
+ payer,
1768
+ claimable: "0.00",
1769
+ claimableMicro: "0",
1770
+ seriesBytes32,
1771
+ epochPrizePool,
1772
+ via: "mock",
1773
+ mock: true
1774
+ };
1775
+ }
1776
+ const pool = requireEpochPool(input, cfg);
1777
+ const data = viem.encodeFunctionData({
1778
+ abi: epochPrizePoolViewAbi,
1779
+ functionName: "claimableRefund",
1780
+ args: [seriesBytes32, BigInt(epochId), payer]
1781
+ });
1782
+ const read = deps.readView;
1783
+ if (!read) {
1784
+ throw new ConfigError(
1785
+ "epochs.claimableRefund needs a wallet or RPC to read EpochPrizePool \u2014 pass `wallet` to the Playmos client",
1786
+ { field: "wallet" }
1787
+ );
1788
+ }
1789
+ const raw = await read(pool, data);
1790
+ const amount = viem.decodeFunctionResult({
1791
+ abi: epochPrizePoolViewAbi,
1792
+ functionName: "claimableRefund",
1793
+ data: raw
1794
+ });
1795
+ return {
1796
+ series,
1797
+ epochId,
1798
+ payer,
1799
+ claimable: formatMicroToUsd(amount),
1800
+ claimableMicro: amount.toString(),
1801
+ seriesBytes32,
1802
+ epochPrizePool: pool,
1803
+ via: "chain"
1804
+ };
1805
+ },
1806
+ async claimRefund(input) {
1807
+ const series = requireSeries(input?.series);
1808
+ const payer = requireAddr(input?.payer, "payer");
1809
+ const epochId = requirePastEpochId(input?.epochId);
1810
+ const cfg = deps.config();
1811
+ const pool = requireEpochPool(input, cfg);
1812
+ if (cfg.mock) {
1813
+ return {
1814
+ series,
1815
+ epochId,
1816
+ payer,
1817
+ txHash: MOCK_TX,
1818
+ status: "confirmed",
1819
+ epochPrizePool: pool,
1820
+ via: "mock",
1821
+ mock: true
1822
+ };
1823
+ }
1824
+ const send = deps.sendEntry;
1825
+ if (!send) {
1826
+ throw new ConfigError(
1827
+ "epochs.claimRefund needs a player wallet \u2014 pass `wallet` to the Playmos client. The credit lands on `payer`; the connected wallet only signs.",
1828
+ { field: "wallet" }
1829
+ );
1830
+ }
1831
+ const call = buildEpochClaimRefundCall({
1832
+ epochPrizePool: pool,
1833
+ series,
1834
+ epochId: BigInt(epochId),
1835
+ payer
1836
+ });
1837
+ const sent = await send([call], {});
1838
+ return {
1839
+ series,
1840
+ epochId,
1841
+ payer,
1842
+ txHash: sent.txHash,
1843
+ status: walletStatus(sent.status),
1844
+ epochPrizePool: pool,
1845
+ via: "chain"
1846
+ };
1847
+ },
1848
+ async withdraw(input = {}) {
1849
+ const cfg = deps.config();
1850
+ const pool = requireEpochPool(input, cfg);
1851
+ if (cfg.mock) {
1852
+ return {
1853
+ epochPrizePool: pool,
1854
+ txHash: MOCK_TX,
1855
+ status: "confirmed",
1856
+ via: "mock",
1857
+ mock: true
1858
+ };
1859
+ }
1860
+ const send = deps.sendEntry;
1861
+ if (!send) {
1862
+ throw new ConfigError(
1863
+ "epochs.withdraw needs the payer wallet \u2014 pass `wallet` to the Playmos client. USDC is pulled to msg.sender on EpochPrizePool, not PrizePool.",
1864
+ { field: "wallet" }
1865
+ );
1866
+ }
1867
+ const creditedMicro = await readWithdrawableMicro(deps, pool);
1868
+ const call = buildEpochWithdrawCall(pool);
1869
+ const sent = await send([call], {});
1870
+ return {
1871
+ epochPrizePool: pool,
1872
+ ...creditedMicro === null ? {} : {
1873
+ amount: formatMicroToUsd(creditedMicro),
1874
+ amountMicro: creditedMicro.toString()
1875
+ },
1876
+ txHash: sent.txHash,
1877
+ status: walletStatus(sent.status),
1878
+ via: "chain"
1879
+ };
1880
+ },
1881
+ async getAttestation(input) {
1882
+ const series = requireSeries(input?.series);
1883
+ const epochId = requirePastEpochId(input?.epochId);
1884
+ const cfg = deps.config();
1885
+ if (cfg.mock) {
1886
+ const pool = input.epochPrizePool ?? cfg.contracts?.epochPrizePool;
1887
+ if (!pool) {
1888
+ throw new ConfigError("epochs.getAttestation needs contracts.epochPrizePool (or pass epochPrizePool)", {
1889
+ field: "epochPrizePool"
1890
+ });
1891
+ }
1892
+ return {
1893
+ series,
1894
+ epochId,
1895
+ winners: input.winners ?? [],
1896
+ amountsMicro: input.amountsMicro ?? [],
1897
+ signature: input.signature ?? `0x${"00".repeat(65)}`,
1898
+ epochPrizePool: requireAddr(pool, "epochPrizePool"),
1899
+ via: "mock",
1900
+ mock: true
1901
+ };
1902
+ }
1903
+ const path = `/epochs/${encodeURIComponent(epochId)}/attestation${qs({
1904
+ series,
1905
+ epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
1906
+ })}`;
1907
+ const res = await deps.http().get(path);
1908
+ const inner = res.attestation ?? res;
1909
+ if (!inner.series || !inner.winners || !inner.amountsMicro || !inner.signature || !inner.epochPrizePool) {
1910
+ throw new ConfigError("attestation response missing posted fields \u2014 refusing to invent a podium");
1911
+ }
1912
+ return {
1913
+ series: inner.series,
1914
+ epochId: String(inner.epochId ?? epochId),
1915
+ winners: inner.winners,
1916
+ amountsMicro: inner.amountsMicro,
1917
+ signature: inner.signature,
1918
+ epochPrizePool: inner.epochPrizePool,
1919
+ ...res.terminal !== void 0 ? { terminal: res.terminal } : {},
1920
+ ...res.dueAt !== void 0 ? { dueAt: res.dueAt } : {},
1921
+ via: res.via ?? "service"
1922
+ };
1923
+ },
1924
+ async executeSettlement(input) {
1925
+ const series = requireSeries(input?.series);
1926
+ const epochId = requirePastEpochId(input?.epochId);
1927
+ if (!Array.isArray(input?.winners)) throw new MissingFieldError("winners");
1928
+ if (!Array.isArray(input?.amounts)) throw new MissingFieldError("amounts");
1929
+ if (typeof input?.signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(input.signature)) {
1930
+ throw new MissingFieldError("signature");
1931
+ }
1932
+ const winners = input.winners.map((w, i) => requireAddrKeepCase(w, `winners[${i}]`));
1933
+ const amountsMicro = input.amounts.map((a, i) => {
1934
+ parseMicroString(a, `amounts[${i}]`);
1935
+ return a;
1936
+ });
1937
+ const amounts = amountsMicro.map((a, i) => parseMicroString(a, `amounts[${i}]`));
1938
+ const cfg = deps.config();
1939
+ const pool = requireEpochPool(input, cfg);
1940
+ if (cfg.mock) {
1941
+ return {
1942
+ series,
1943
+ epochId,
1944
+ winners,
1945
+ amountsMicro,
1946
+ signature: input.signature,
1947
+ txHash: MOCK_TX,
1948
+ status: "confirmed",
1949
+ epochPrizePool: pool,
1950
+ via: "mock",
1951
+ mock: true
1952
+ };
1953
+ }
1954
+ const send = deps.sendEntry;
1955
+ if (!send) {
1956
+ throw new ConfigError(
1957
+ "epochs.executeSettlement needs a wallet \u2014 pass `wallet` to the Playmos client. The SDK relays the signed podium; it does not choose winners.",
1958
+ { field: "wallet" }
1959
+ );
1960
+ }
1961
+ const call = buildEpochExecuteSettlementCall({
1962
+ epochPrizePool: pool,
1963
+ series,
1964
+ epochId: BigInt(epochId),
1965
+ winners,
1966
+ amounts,
1967
+ signature: input.signature
1968
+ });
1969
+ let sent;
1970
+ try {
1971
+ sent = await send([call], {});
1972
+ } catch (e) {
1973
+ const msg = e?.message ?? String(e);
1974
+ throw new PaymentFailedError("EpochPrizePool.executeSignedSettlement failed.", { cause: msg });
1975
+ }
1976
+ const terminal = await readEpochTerminal(deps, pool, series, epochId);
1977
+ return {
1978
+ series,
1979
+ epochId,
1980
+ winners,
1981
+ amountsMicro,
1982
+ signature: input.signature,
1983
+ txHash: sent.txHash,
1984
+ status: walletStatus(sent.status),
1985
+ ...terminal === null ? {} : { terminal, settled: terminal === "settled" },
1986
+ epochPrizePool: pool,
1987
+ via: "chain"
1988
+ };
1989
+ }
1990
+ };
1991
+ }
1992
+ var C1_ENTER_SPLIT_MICRO = {
1993
+ entry: 1000000n,
1994
+ fee: 100000n,
1995
+ pool: 600000n,
1996
+ outgoingSeed: 300000n,
1997
+ incomingSeed: 0n
1998
+ };
1999
+
2000
+ // src/x402.ts
2001
+ var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
899
2002
  function requireAddress2(value, field) {
900
2003
  if (typeof value !== "string" || value.trim() === "") {
901
2004
  throw new MissingFieldError(field);
902
2005
  }
903
- if (!ADDRESS_RE2.test(value)) {
2006
+ if (!ADDRESS_RE3.test(value)) {
904
2007
  throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
905
2008
  field,
906
2009
  value
@@ -1038,7 +2141,7 @@ function validateX402ChallengeInput(input) {
1038
2141
  }
1039
2142
 
1040
2143
  // src/client.ts
1041
- var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
2144
+ var ADDRESS_RE4 = /^0x[0-9a-fA-F]{40}$/;
1042
2145
  function mapAlreadyEntered(e) {
1043
2146
  const msg = e instanceof Error ? e.message : typeof e === "object" && e && "message" in e ? String(e.message) : String(e);
1044
2147
  const detail = e instanceof PaymentFailedError || e instanceof ApiError ? e.detail : void 0;
@@ -1086,7 +2189,7 @@ function requireAddressField(value, field) {
1086
2189
  if (typeof value !== "string" || value.trim() === "") {
1087
2190
  throw new MissingFieldError(field);
1088
2191
  }
1089
- if (!ADDRESS_RE3.test(value)) {
2192
+ if (!ADDRESS_RE4.test(value)) {
1090
2193
  throw new ConfigError(
1091
2194
  `${field} must be a 0x-prefixed 20-byte wallet address (Phase 1a transfers settle server-held wallets), got: ${JSON.stringify(value)}`,
1092
2195
  { field, value }
@@ -1125,6 +2228,56 @@ var Playmos = class {
1125
2228
  );
1126
2229
  }
1127
2230
  };
2231
+ /**
2232
+ * Rolling epochs — `currentId` / `prize` / `get` / `getSeries` (sdk#629),
2233
+ * `enter` (sdk#635 / S2), operator `settle` (sdk#639 / S3), the C3
2234
+ * refund pull (sdk#634), and E1b `getAttestation` / `executeSettlement`
2235
+ * (wallet-direct via the same `walletProvider()` funnel as `rounds.withdraw`).
2236
+ * Reads/settle go through the service. Refund claim, withdraw, and signed
2237
+ * execute are wallet-direct on EpochPrizePool — not PrizePool.
2238
+ * Nothing here opens a round.
2239
+ */
2240
+ this.epochs = createEpochsApi({
2241
+ http: () => this.http,
2242
+ config: () => this.config,
2243
+ assertSecretKey: (surface) => this.assertSecretKey(surface),
2244
+ sendEntry: async (calls, opts) => {
2245
+ const provider = this.walletProvider();
2246
+ if (this.config.gas?.mode === "player") {
2247
+ await assertEnoughGas(provider, await getAccount(provider));
2248
+ }
2249
+ const from = await getAccount(provider);
2250
+ const paymasterUrl = this.config.gas?.mode === "player" ? void 0 : this.config.gas?.paymasterUrl ?? opts.paymasterUrl;
2251
+ const { id } = await sendCalls(provider, from, this.env.chainId, calls, paymasterUrl);
2252
+ return waitForCalls(provider, id);
2253
+ },
2254
+ walletAddress: async () => getAccount(this.walletProvider()),
2255
+ readView: async (to, data) => {
2256
+ if (await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
2257
+ const provider = this.walletProvider();
2258
+ return await provider.request({
2259
+ method: "eth_call",
2260
+ params: [{ to, data }, "latest"]
2261
+ });
2262
+ }
2263
+ const rpc = this.env.network === "base" ? "https://mainnet.base.org" : "https://sepolia.base.org";
2264
+ const res = await fetch(rpc, {
2265
+ method: "POST",
2266
+ headers: { "content-type": "application/json" },
2267
+ body: JSON.stringify({
2268
+ jsonrpc: "2.0",
2269
+ id: 1,
2270
+ method: "eth_call",
2271
+ params: [{ to, data }, "latest"]
2272
+ })
2273
+ });
2274
+ const json = await res.json();
2275
+ if (!json.result) {
2276
+ throw new ApiError(`eth_call failed: ${json.error?.message ?? "no result"}`, { to });
2277
+ }
2278
+ return json.result;
2279
+ }
2280
+ });
1128
2281
  this.payouts = {
1129
2282
  /** Choose how the studio is paid: "usdc" (default) or "fiat" (Bridge). Secret key only (#264). */
1130
2283
  setMode: async (mode) => {
@@ -1290,6 +2443,8 @@ var Playmos = class {
1290
2443
  this.mockPotMicro = /* @__PURE__ */ new Map();
1291
2444
  /** Persisted settle winners for re-settle replay (#490 C2) — never re-invent pool-to-everyone. */
1292
2445
  this.mockSettleWinners = /* @__PURE__ */ new Map();
2446
+ /** L34 notices after mock settle — funded only with a push txHash. */
2447
+ this.mockPayoutNotices = /* @__PURE__ */ new Map();
1293
2448
  this.rounds = {
1294
2449
  open: async (input) => {
1295
2450
  if (!this.config.mock) this.assertSecretKey("playmos.rounds.open");
@@ -1455,6 +2610,16 @@ var Playmos = class {
1455
2610
  };
1456
2611
  this.mockRounds.set(input.roundId, settled);
1457
2612
  this.mockSettleWinners.set(input.roundId, winners);
2613
+ this.mockPayoutNotices.set(
2614
+ input.roundId,
2615
+ rows.filter((w) => w.micro > 0n).map((w) => ({
2616
+ roundId: input.roundId,
2617
+ wallet: w.wallet.toLowerCase(),
2618
+ amountMicro: w.micro.toString(),
2619
+ status: "claimable",
2620
+ reason: "prizepool_pull_credit"
2621
+ }))
2622
+ );
1458
2623
  for (const w of rows) {
1459
2624
  if (w.micro <= 0n) continue;
1460
2625
  const k = `${input.roundId}:${w.wallet.toLowerCase()}`;
@@ -1696,6 +2861,93 @@ var Playmos = class {
1696
2861
  }
1697
2862
  return body.active;
1698
2863
  },
2864
+ /**
2865
+ * L34 / sdk#610 — notifications for a settled round.
2866
+ * Each win is `funded` (push txHash) or `claimable` (withdraw fallback).
2867
+ * Never returns `funded` without a transfer txHash.
2868
+ */
2869
+ payoutNotices: async (input) => {
2870
+ requireField(input?.roundId, "roundId");
2871
+ if (this.config.mock) {
2872
+ const existing = this.mockRounds.get(input.roundId);
2873
+ if (!existing) {
2874
+ throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
2875
+ }
2876
+ let notices = this.mockPayoutNotices.get(input.roundId) ?? [];
2877
+ if (input.wallet) {
2878
+ if (!ADDRESS_RE4.test(input.wallet)) {
2879
+ throw new ConfigError("wallet must be a 0x-prefixed 20-byte address");
2880
+ }
2881
+ const want = input.wallet.toLowerCase();
2882
+ notices = notices.filter((n) => n.wallet === want);
2883
+ }
2884
+ for (const n of notices) {
2885
+ if (n.status === "funded" && !n.txHash) {
2886
+ throw new ApiError("cannot label funded without a transfer txHash", {
2887
+ status: 500,
2888
+ code: "payout_notice_invalid"
2889
+ });
2890
+ }
2891
+ }
2892
+ return { roundId: input.roundId, status: existing.status, notices };
2893
+ }
2894
+ const q = input.wallet ? `?wallet=${encodeURIComponent(input.wallet)}` : "";
2895
+ return this.http.get(
2896
+ `/rounds/${encodeURIComponent(input.roundId)}/payout-notices${q}`
2897
+ );
2898
+ },
2899
+ /**
2900
+ * Operator push-attempt after settle (L34). Mock: mark a winner funded only
2901
+ * when a real-shaped txHash is supplied; otherwise that row stays claimable.
2902
+ */
2903
+ pushWinnings: async (input) => {
2904
+ requireField(input?.roundId, "roundId");
2905
+ if (!this.config.mock) this.assertSecretKey("playmos.rounds.pushWinnings");
2906
+ if (this.config.mock) {
2907
+ const existing = this.mockRounds.get(input.roundId);
2908
+ if (!existing) {
2909
+ throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
2910
+ }
2911
+ if (existing.status !== "settled") {
2912
+ throw new ApiError(`round ${input.roundId} is not settled \u2014 push after the clock`, {
2913
+ status: 409,
2914
+ code: "conflict"
2915
+ });
2916
+ }
2917
+ const prior = this.mockPayoutNotices.get(input.roundId) ?? [];
2918
+ const byWallet = new Map(
2919
+ (input.pushes ?? []).map((p) => [p.wallet.toLowerCase(), p])
2920
+ );
2921
+ const TX_RE = /^0x[0-9a-fA-F]{64}$/;
2922
+ const ZERO = "0x" + "00".repeat(32);
2923
+ const notices = prior.map((n) => {
2924
+ const p = byWallet.get(n.wallet);
2925
+ if (p && !p.failed && p.txHash && TX_RE.test(p.txHash) && p.txHash.toLowerCase() !== ZERO) {
2926
+ return {
2927
+ ...n,
2928
+ status: "funded",
2929
+ txHash: p.txHash.toLowerCase(),
2930
+ reason: void 0
2931
+ };
2932
+ }
2933
+ if (p && (p.failed || !p.txHash || !TX_RE.test(p.txHash) || p.txHash.toLowerCase() === ZERO)) {
2934
+ return {
2935
+ ...n,
2936
+ status: "claimable",
2937
+ txHash: void 0,
2938
+ reason: p.failed ? "push_failed" : "push_missing_txHash"
2939
+ };
2940
+ }
2941
+ return n;
2942
+ });
2943
+ this.mockPayoutNotices.set(input.roundId, notices);
2944
+ return { roundId: input.roundId, status: existing.status, notices };
2945
+ }
2946
+ return this.http.post(
2947
+ `/rounds/${encodeURIComponent(input.roundId)}/push-winnings`,
2948
+ {}
2949
+ );
2950
+ },
1699
2951
  /**
1700
2952
  * Read a wallet's **claimable** prize balance for a round (issue #41).
1701
2953
  * Prefers the service chain read (`GET /v1/rounds/:id/prize?wallet=`); falls
@@ -1752,7 +3004,7 @@ var Playmos = class {
1752
3004
  const existing = this.mockRounds.get(input.roundId);
1753
3005
  if (existing?.prizePoolAddress) prizePool2 = existing.prizePoolAddress;
1754
3006
  }
1755
- if (!input.wallet || !ADDRESS_RE3.test(input.wallet)) {
3007
+ if (!input.wallet || !ADDRESS_RE4.test(input.wallet)) {
1756
3008
  throw new ConfigError(
1757
3009
  "mock rounds.withdraw requires wallet (0x\u2026) \u2014 live uses the connected provider; without wallet mock would pay the first non-zero credit to the wrong player (#495)"
1758
3010
  );
@@ -2363,7 +3615,7 @@ var Playmos = class {
2363
3615
  const { txHash } = await waitForCalls(provider, callsId);
2364
3616
  return this.settle(intent.payment.id, txHash);
2365
3617
  }
2366
- /** Playmos-owned skill-game entry (10%, 60/30/10 prize pool). Closes #343. */
3618
+ /** Skill-game prize-pool entry. Live sandbox / Playmos Lab uses 60/30/10. Studio contest take is 1% (not a live separate pool yet). Closes #343. */
2367
3619
  async enterRound(input) {
2368
3620
  const amountMicro = validateAmount(input.amount);
2369
3621
  requireField(input.gameId, "gameId");
@@ -2855,13 +4107,13 @@ function previewPoolSplit(amount) {
2855
4107
  }
2856
4108
 
2857
4109
  // src/settlement.ts
2858
- var ADDRESS_RE4 = /^0x[0-9a-fA-F]{40}$/;
4110
+ var ADDRESS_RE5 = /^0x[0-9a-fA-F]{40}$/;
2859
4111
  var DEFAULT_TTL_MS = 15 * 60 * 1e3;
2860
4112
  function requireAddress3(value, field) {
2861
4113
  if (typeof value !== "string" || value.trim() === "") {
2862
4114
  throw new MissingFieldError(field);
2863
4115
  }
2864
- if (!ADDRESS_RE4.test(value)) {
4116
+ if (!ADDRESS_RE5.test(value)) {
2865
4117
  throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
2866
4118
  field,
2867
4119
  value
@@ -2950,6 +4202,7 @@ function isX402PayloadAuthorization(auth) {
2950
4202
  exports.AlreadyEnteredError = AlreadyEnteredError;
2951
4203
  exports.ApiError = ApiError;
2952
4204
  exports.AuthError = AuthError;
4205
+ exports.C1_ENTER_SPLIT_MICRO = C1_ENTER_SPLIT_MICRO;
2953
4206
  exports.CHAIN_ID = CHAIN_ID;
2954
4207
  exports.ConfigError = ConfigError;
2955
4208
  exports.DEFAULT_API_BASE_URL = DEFAULT_API_BASE_URL;
@@ -2958,6 +4211,7 @@ exports.InvalidAmountError = InvalidAmountError;
2958
4211
  exports.MICRO_PER_USDC = MICRO_PER_USDC;
2959
4212
  exports.MissingFieldError = MissingFieldError;
2960
4213
  exports.NothingToWithdrawError = NothingToWithdrawError;
4214
+ exports.PLAYMOS_FEE_SINK_DEFAULT = PLAYMOS_FEE_SINK_DEFAULT;
2961
4215
  exports.PaymentFailedError = PaymentFailedError;
2962
4216
  exports.PayoutError = PayoutError;
2963
4217
  exports.Playmos = Playmos;
@@ -2966,15 +4220,25 @@ exports.USDC_ADDRESS = USDC_ADDRESS;
2966
4220
  exports.USDC_DECIMALS = USDC_DECIMALS;
2967
4221
  exports.WalletConnectionError = WalletConnectionError;
2968
4222
  exports.WalletTimeoutError = WalletTimeoutError;
4223
+ exports.buildEpochClaimRefundCall = buildEpochClaimRefundCall;
4224
+ exports.buildEpochEntryCalls = buildEpochEntryCalls;
4225
+ exports.buildEpochExecuteSettlementCall = buildEpochExecuteSettlementCall;
4226
+ exports.buildEpochWithdrawCall = buildEpochWithdrawCall;
2969
4227
  exports.clientRuntimeSignals = clientRuntimeSignals;
2970
4228
  exports.computeIapSplit = computeIapSplit;
2971
4229
  exports.computePayout = computePayout;
2972
4230
  exports.computePoolSplit = computePoolSplit;
4231
+ exports.createEpochsApi = createEpochsApi;
2973
4232
  exports.createPaymentRequirement = createPaymentRequirement;
2974
4233
  exports.createX402Challenge = createX402Challenge;
2975
4234
  exports.decodePaymentHeader = decodePaymentHeader;
4235
+ exports.derivedEpochId = derivedEpochId;
4236
+ exports.encodeEpochPoolConstructorArgs = encodeEpochPoolConstructorArgs;
2976
4237
  exports.encodePaymentHeader = encodePaymentHeader;
4238
+ exports.epochPayableMicro = epochPayableMicro;
4239
+ exports.epochPoolProofMessage = epochPoolProofMessage;
2977
4240
  exports.formatMicroToUsd = formatMicroToUsd;
4241
+ exports.identityToBytes32 = identityToBytes32;
2978
4242
  exports.isClientRuntime = isClientRuntime;
2979
4243
  exports.isWalletSignatureAuthorization = isWalletSignatureAuthorization;
2980
4244
  exports.isX402PayloadAuthorization = isX402PayloadAuthorization;
@@ -2982,6 +4246,7 @@ exports.networkToCaip2 = networkToCaip2;
2982
4246
  exports.parsePaymentRequirement = parsePaymentRequirement;
2983
4247
  exports.parseUsdToMicro = parseUsdToMicro;
2984
4248
  exports.prefixedId = prefixedId;
4249
+ exports.prepareStudioPoolLocal = prepareStudioPoolLocal;
2985
4250
  exports.previewEscrowFee = previewEscrowFee;
2986
4251
  exports.previewIapSplit = previewIapSplit;
2987
4252
  exports.previewMarketplaceSplit = previewMarketplaceSplit;
@@ -2989,6 +4254,8 @@ exports.previewPoolSplit = previewPoolSplit;
2989
4254
  exports.previewTransferSplit = previewTransferSplit;
2990
4255
  exports.resolveEnv = resolveEnv;
2991
4256
  exports.serializePaymentRequirement = serializePaymentRequirement;
4257
+ exports.seriesToBytes32 = seriesToBytes32;
4258
+ exports.settlementTypedData = settlementTypedData;
2992
4259
  exports.toX402PaymentRequired = toX402PaymentRequired;
2993
4260
  exports.ulid = ulid;
2994
4261
  exports.validateX402ChallengeInput = validateX402ChallengeInput;