@playmos/sdk 0.3.10 → 0.3.11

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.js CHANGED
@@ -1,6 +1,6 @@
1
- import { AuthError, InvalidAmountError, ConfigError, MissingFieldError, NothingToWithdrawError, WalletTimeoutError, PaymentFailedError, ApiError, WalletConnectionError, InsufficientGasError, AlreadyEnteredError } from './chunk-UZECDT6F.js';
2
- export { AlreadyEnteredError, ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError, WalletTimeoutError } from './chunk-UZECDT6F.js';
3
- import { encodeFunctionData, decodeFunctionResult, numberToHex, keccak256, toBytes } from 'viem';
1
+ import { AuthError, InvalidAmountError, ConfigError, MissingFieldError, NothingToWithdrawError, assertEnoughGas, buildWithdrawCall, sendCalls, waitForCalls, WalletTimeoutError, PaymentFailedError, ApiError, buildIapCalls, buildEntryCalls, prizePoolAbi, WalletConnectionError, AlreadyEnteredError } from './chunk-35WJANW4.js';
2
+ export { AlreadyEnteredError, ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError, WalletTimeoutError } from './chunk-35WJANW4.js';
3
+ import { encodeFunctionData, decodeFunctionResult } from 'viem';
4
4
 
5
5
  // src/config.ts
6
6
  var CHAIN_ID = {
@@ -183,10 +183,10 @@ function validateMetadata(metadata) {
183
183
 
184
184
  // src/payout.ts
185
185
  var PayoutError = class extends Error {
186
- constructor(message) {
186
+ constructor(message, code = "payout_invalid") {
187
187
  super(message);
188
- this.code = "payout_invalid";
189
188
  this.name = "PayoutError";
189
+ this.code = code;
190
190
  }
191
191
  };
192
192
  var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
@@ -231,7 +231,8 @@ function computePayout(pool, ranking, rule) {
231
231
  }
232
232
  if (splits.length > wallets.length) {
233
233
  throw new PayoutError(
234
- `top-n needs ${splits.length} ranked wallets but ranking only has ${wallets.length}`
234
+ `ranking_too_short: a top-n rule cannot be settled from a ranking shorter than splitsBps.length (need ${splits.length}, got ${wallets.length}) \u2014 open with winner-take-all, or settle with explicit winners[{wallet,amount}]`,
235
+ "ranking_too_short"
235
236
  );
236
237
  }
237
238
  let sumBps = 0;
@@ -544,7 +545,7 @@ function resolveRawProvider(wallet) {
544
545
  }
545
546
  if (injected) return injected;
546
547
  throw new WalletConnectionError(
547
- "base-account connector needs a provider. In the Base App it is injected automatically; elsewhere, create one with @base-org/account and pass it as wallet.provider."
548
+ 'base-account connector needs a provider \u2014 this is not a built-in Base Account/passkey flow in @playmos/sdk. In the Base App a provider is often injected; elsewhere install @base-org/account, construct a provider, and pass it as wallet.provider (or use connector: "injected" in a wallet browser).'
548
549
  );
549
550
  }
550
551
  function resolveProvider(wallet, opts) {
@@ -581,174 +582,6 @@ async function getAccount(provider) {
581
582
  }
582
583
  }
583
584
 
584
- // src/chain/abis.ts
585
- var erc20Abi = [
586
- {
587
- type: "function",
588
- name: "approve",
589
- stateMutability: "nonpayable",
590
- inputs: [
591
- { name: "spender", type: "address" },
592
- { name: "amount", type: "uint256" }
593
- ],
594
- outputs: [{ type: "bool" }]
595
- }
596
- ];
597
- var playmosPayAbi = [
598
- {
599
- type: "function",
600
- name: "pay",
601
- stateMutability: "nonpayable",
602
- inputs: [
603
- { name: "paymentId", type: "bytes32" },
604
- { name: "studio", type: "address" },
605
- { name: "amount", type: "uint256" }
606
- ],
607
- outputs: []
608
- }
609
- ];
610
- var prizePoolAbi = [
611
- {
612
- type: "function",
613
- name: "enter",
614
- stateMutability: "nonpayable",
615
- inputs: [
616
- { name: "roundId", type: "bytes32" },
617
- { name: "identity", type: "bytes32" }
618
- ],
619
- outputs: []
620
- },
621
- {
622
- type: "function",
623
- name: "withdrawable",
624
- stateMutability: "view",
625
- inputs: [{ name: "account", type: "address" }],
626
- outputs: [{ type: "uint256" }]
627
- },
628
- {
629
- type: "function",
630
- name: "withdraw",
631
- stateMutability: "nonpayable",
632
- inputs: [],
633
- outputs: [{ name: "amount", type: "uint256" }]
634
- }
635
- ];
636
- async function sendCalls(provider, from, chainId, calls, paymasterUrl) {
637
- const params = {
638
- version: "2.0.0",
639
- from,
640
- chainId: numberToHex(chainId),
641
- atomicRequired: true,
642
- calls
643
- };
644
- if (paymasterUrl) {
645
- params.capabilities = { paymasterService: { url: paymasterUrl } };
646
- }
647
- let result;
648
- try {
649
- result = await provider.request({ method: "wallet_sendCalls", params: [params] });
650
- } catch (e) {
651
- if (e instanceof WalletTimeoutError) throw e;
652
- const msg = e?.message ?? String(e);
653
- if (/reject|denied|cancel|closed/i.test(msg)) {
654
- throw new WalletConnectionError("The player cancelled or closed the payment sheet.", { cause: msg });
655
- }
656
- throw new PaymentFailedError("wallet_sendCalls failed.", { cause: msg });
657
- }
658
- const id = typeof result === "string" ? result : result?.id ?? "";
659
- if (!id) throw new PaymentFailedError("Wallet returned no calls id.");
660
- return { id };
661
- }
662
- async function waitForCalls(provider, id, timeoutMs = 6e4) {
663
- if (!id) return { status: "FAILED" };
664
- const deadline = Date.now() + timeoutMs;
665
- while (Date.now() < deadline) {
666
- try {
667
- const res = await provider.request({
668
- method: "wallet_getCallsStatus",
669
- params: [id]
670
- });
671
- const s = String(res?.status ?? "").toUpperCase();
672
- const txHash = res?.receipts?.[0]?.transactionHash;
673
- if (s === "200" || s === "CONFIRMED" || s === "SUCCESS") return { status: "CONFIRMED", txHash };
674
- if (s === "400" || s === "500" || s === "FAILED" || s === "REVERTED") return { status: "FAILED", txHash };
675
- } catch (e) {
676
- if (e instanceof WalletTimeoutError) {
677
- await new Promise((r) => setTimeout(r, 900));
678
- continue;
679
- }
680
- throw e;
681
- }
682
- await new Promise((r) => setTimeout(r, 900));
683
- }
684
- return { status: "PENDING" };
685
- }
686
- function encodeApprove(spender, amountUnits) {
687
- return encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [spender, amountUnits] });
688
- }
689
- var GAS_FLOOR_WEI_DEFAULT = 200000000000000n;
690
- var GAS_FLOOR_WEI_BASE_SEPOLIA = 50000000000000n;
691
- var CHAIN_ID_BASE_SEPOLIA = 84532;
692
- function gasFloorWei(chainId) {
693
- return chainId === CHAIN_ID_BASE_SEPOLIA ? GAS_FLOOR_WEI_BASE_SEPOLIA : GAS_FLOOR_WEI_DEFAULT;
694
- }
695
- async function assertEnoughGas(provider, from, minWei) {
696
- let floor = minWei;
697
- if (floor === void 0) {
698
- let chainId = 0;
699
- try {
700
- const hex = await provider.request({ method: "eth_chainId", params: [] });
701
- chainId = Number(BigInt(hex));
702
- } catch {
703
- }
704
- floor = gasFloorWei(chainId);
705
- }
706
- let balance;
707
- try {
708
- const hex = await provider.request({ method: "eth_getBalance", params: [from, "latest"] });
709
- balance = BigInt(hex);
710
- } catch {
711
- return;
712
- }
713
- if (balance < floor) {
714
- throw new InsufficientGasError({
715
- balanceWei: balance.toString(),
716
- minWei: floor.toString()
717
- });
718
- }
719
- }
720
- var toBytes32 = (s) => keccak256(toBytes(s));
721
- function buildIapCalls(args) {
722
- const payData = encodeFunctionData({
723
- abi: playmosPayAbi,
724
- functionName: "pay",
725
- args: [toBytes32(args.paymentId), args.studio, args.amountUnits]
726
- });
727
- return [
728
- { to: args.usdc, data: encodeApprove(args.playmosPay, args.amountUnits) },
729
- { to: args.playmosPay, data: payData }
730
- ];
731
- }
732
- function buildEntryCalls(args) {
733
- const enterData = encodeFunctionData({
734
- abi: prizePoolAbi,
735
- functionName: "enter",
736
- args: [toBytes32(args.roundKey), toBytes32(args.identity)]
737
- });
738
- return [
739
- { to: args.usdc, data: encodeApprove(args.prizePool, args.amountUnits) },
740
- { to: args.prizePool, data: enterData }
741
- ];
742
- }
743
- function buildWithdrawCall(prizePool) {
744
- const data = encodeFunctionData({
745
- abi: prizePoolAbi,
746
- functionName: "withdraw",
747
- args: []
748
- });
749
- return { to: prizePool, data };
750
- }
751
-
752
585
  // src/mock.ts
753
586
  var MOCK_TX = `0x${"0".repeat(56)}deadbeef`;
754
587
  function mockIapPayment(args) {
@@ -1953,8 +1786,16 @@ var Playmos = class {
1953
1786
  /**
1954
1787
  * Offline mock payments for this client instance — so `verify(id)` after
1955
1788
  * `mock: true` pay/enterRound does not hit the live API (#204).
1789
+ * Keyed by payment id for verify.
1956
1790
  */
1957
1791
  this.mockPayments = /* @__PURE__ */ new Map();
1792
+ /**
1793
+ * sdk#513 / residual B1–B2 — held idempotencyKey offline (live contract rehearsal).
1794
+ * Keyed like service: `${gameId}:${key}` (IAP without gameId uses empty gameId).
1795
+ * Stores terms so same key + different money throws (live IdempotencyConflict), not silent replay.
1796
+ * Auto-minted keys still unique per call (not stored for cross-call replay).
1797
+ */
1798
+ this.mockByIdempotencyKey = /* @__PURE__ */ new Map();
1958
1799
  /**
1959
1800
  * `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
1960
1801
  * `get` reconciles once against chain truth; `wait` polls it to a terminal state
@@ -2139,18 +1980,62 @@ var Playmos = class {
2139
1980
  }
2140
1981
  };
2141
1982
  }
2142
- rememberMock(payment) {
2143
- if (payment.mock) this.mockPayments.set(payment.id, payment);
1983
+ mockIdemScopeKey(gameId, heldKey) {
1984
+ return `${gameId ?? ""}:${heldKey}`;
1985
+ }
1986
+ rememberMock(payment, held) {
1987
+ if (payment.mock) {
1988
+ this.mockPayments.set(payment.id, payment);
1989
+ if (held) {
1990
+ this.mockByIdempotencyKey.set(this.mockIdemScopeKey(held.terms.gameId, held.key), {
1991
+ payment,
1992
+ terms: held.terms
1993
+ });
1994
+ }
1995
+ }
2144
1996
  return payment;
2145
1997
  }
1998
+ /** Live-shaped conflict when held mock key is reused with different money terms (sdk#513 residual B1). */
1999
+ mockIdemReplayOrThrow(heldKey, terms) {
2000
+ const prior = this.mockByIdempotencyKey.get(this.mockIdemScopeKey(terms.gameId, heldKey));
2001
+ if (!prior) return null;
2002
+ const t = prior.terms;
2003
+ const same = t.kind === terms.kind && t.amount === terms.amount && t.gameId === terms.gameId && t.playerId === terms.playerId && t.roundId === terms.roundId && t.identity === terms.identity && t.sku === terms.sku;
2004
+ if (!same) {
2005
+ throw new ApiError(
2006
+ `idempotencyKey was already used for a payment with DIFFERENT terms (kind/amount/gameId/playerId/roundId/identity/sku); use a fresh key (mock:true mirrors live conflict \u2014 sdk#513).`,
2007
+ {
2008
+ code: "idempotency_conflict",
2009
+ idempotencyKey: heldKey,
2010
+ priorKind: t.kind,
2011
+ priorAmount: t.amount
2012
+ }
2013
+ );
2014
+ }
2015
+ return prior.payment;
2016
+ }
2146
2017
  /** External-studio IAP (1%). Returns a real confirmed Payment with a txHash. */
2147
2018
  async pay(input) {
2148
2019
  const amountMicro = validateAmount(input.amount);
2149
2020
  requireField(input.sku, "sku");
2150
2021
  requireField(input.playerId, "playerId");
2151
2022
  const metadata = validateMetadata(input.metadata);
2152
- const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
2023
+ const heldIdem = typeof input.idempotencyKey === "string" && input.idempotencyKey.trim() ? input.idempotencyKey.trim() : void 0;
2024
+ const idempotencyKey = heldIdem ?? prefixedId("idem");
2153
2025
  if (this.config.mock) {
2026
+ const terms = {
2027
+ kind: "iap",
2028
+ amount: formatMicroToUsd(amountMicro),
2029
+ gameId: input.gameId ?? "",
2030
+ playerId: input.playerId,
2031
+ roundId: "",
2032
+ identity: "",
2033
+ sku: input.sku.trim()
2034
+ };
2035
+ if (heldIdem) {
2036
+ const replay = this.mockIdemReplayOrThrow(heldIdem, terms);
2037
+ if (replay) return replay;
2038
+ }
2154
2039
  return this.rememberMock(
2155
2040
  mockIapPayment({
2156
2041
  amountMicro,
@@ -2159,7 +2044,8 @@ var Playmos = class {
2159
2044
  playerId: input.playerId,
2160
2045
  chain: this.env.network,
2161
2046
  metadata
2162
- })
2047
+ }),
2048
+ heldIdem ? { key: heldIdem, terms } : void 0
2163
2049
  );
2164
2050
  }
2165
2051
  if (this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
@@ -2181,7 +2067,13 @@ var Playmos = class {
2181
2067
  );
2182
2068
  let payment = this.mapServerPayment(res.payment, "iap", amountMicro);
2183
2069
  if (payment.status === "pending" || payment.status === "created") {
2184
- payment = await this.waitForServerSettle(payment.id, "iap", amountMicro);
2070
+ payment = await this.waitForServerSettle(
2071
+ payment.id,
2072
+ "iap",
2073
+ amountMicro,
2074
+ input.settleTimeoutMs ?? 6e4,
2075
+ input.signal
2076
+ );
2185
2077
  }
2186
2078
  return payment;
2187
2079
  }
@@ -2226,8 +2118,22 @@ var Playmos = class {
2226
2118
  requireField(input.roundId, "roundId");
2227
2119
  requireField(input.playerId, "playerId");
2228
2120
  const metadata = validateMetadata(input.metadata);
2229
- const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
2121
+ const heldIdem = typeof input.idempotencyKey === "string" && input.idempotencyKey.trim() ? input.idempotencyKey.trim() : void 0;
2122
+ const idempotencyKey = heldIdem ?? prefixedId("idem");
2230
2123
  if (this.config.mock) {
2124
+ const terms = {
2125
+ kind: "entry",
2126
+ amount: formatMicroToUsd(amountMicro),
2127
+ gameId: input.gameId,
2128
+ playerId: input.playerId,
2129
+ roundId: input.roundId,
2130
+ identity: typeof input.identity === "string" ? input.identity.trim() : "",
2131
+ sku: ""
2132
+ };
2133
+ if (heldIdem) {
2134
+ const replay = this.mockIdemReplayOrThrow(heldIdem, terms);
2135
+ if (replay) return replay;
2136
+ }
2231
2137
  this.mockAccumulateEntry(input.roundId, amountMicro);
2232
2138
  return this.rememberMock(
2233
2139
  mockEntryPayment({
@@ -2243,7 +2149,8 @@ var Playmos = class {
2243
2149
  // B2 pins — must survive mock path for hub hasEntered parity (#204).
2244
2150
  roundKey: input.roundKey,
2245
2151
  identity: input.identity
2246
- })
2152
+ }),
2153
+ heldIdem ? { key: heldIdem, terms } : void 0
2247
2154
  );
2248
2155
  }
2249
2156
  const serverSettleNoWallet = this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy());
@@ -2276,7 +2183,13 @@ var Playmos = class {
2276
2183
  );
2277
2184
  let payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
2278
2185
  if (payment2.status === "pending" || payment2.status === "created") {
2279
- payment2 = await this.waitForServerSettle(payment2.id, "entry", amountMicro);
2186
+ payment2 = await this.waitForServerSettle(
2187
+ payment2.id,
2188
+ "entry",
2189
+ amountMicro,
2190
+ input.settleTimeoutMs ?? 6e4,
2191
+ input.signal
2192
+ );
2280
2193
  }
2281
2194
  if (input.identity) payment2.identity = input.identity;
2282
2195
  const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
@@ -2459,10 +2372,16 @@ var Playmos = class {
2459
2372
  * until chain verify catches PaymentSettled (RPC lag) — poll so pay() matches
2460
2373
  * the documented "confirmed" sandbox promise (TTFSC-T0 / cold-run).
2461
2374
  */
2462
- async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4) {
2375
+ async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4, signal) {
2463
2376
  const start = Date.now();
2464
2377
  let delayMs = 400;
2465
2378
  while (Date.now() - start < timeoutMs) {
2379
+ if (signal?.aborted) {
2380
+ throw new ApiError(
2381
+ `Aborted waiting for server-settle of ${paymentId}. Re-check with playmos.verify("${paymentId}").`,
2382
+ { paymentId, aborted: true, asyncSettle: true }
2383
+ );
2384
+ }
2466
2385
  const raw = await this.http.get(
2467
2386
  `/payments/${encodeURIComponent(paymentId)}`
2468
2387
  );
@@ -2477,12 +2396,46 @@ var Playmos = class {
2477
2396
  { paymentId, timeout: true, asyncSettle: true }
2478
2397
  );
2479
2398
  }
2399
+ /**
2400
+ * sdk#509 — recover a payment after a lost response by the idempotency key you held.
2401
+ * Requires secret sk_test_ (same as list). Returns null if not found.
2402
+ */
2403
+ async findPaymentByIdempotencyKey(idempotencyKey, opts) {
2404
+ requireField(idempotencyKey, "idempotencyKey");
2405
+ if (this.config.mock) {
2406
+ return null;
2407
+ }
2408
+ const q = new URLSearchParams({ idempotencyKey });
2409
+ if (opts?.gameId) q.set("gameId", opts.gameId);
2410
+ try {
2411
+ const res = await this.http.get(
2412
+ `/payments?${q.toString()}`
2413
+ );
2414
+ const row = res.data?.[0];
2415
+ if (!row || typeof row.id !== "string") return null;
2416
+ const kind = row.kind === "entry" ? "entry" : "iap";
2417
+ const amountMicro = validateAmount(String(row.amount ?? "0.01"));
2418
+ return this.mapServerPayment(
2419
+ row,
2420
+ kind,
2421
+ amountMicro
2422
+ );
2423
+ } catch (e) {
2424
+ if (e instanceof ApiError && (e.detail?.status === 404 || e.detail?.code === "not_found" || /not found/i.test(e.message))) {
2425
+ return null;
2426
+ }
2427
+ throw e;
2428
+ }
2429
+ }
2480
2430
  /**
2481
2431
  * Map a server-settled payment (from the `settle: "server"` response) into the
2482
2432
  * SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
2483
- * pass its authoritative `status`/`txHash`/amounts straight through and, for an
2484
- * entry, derive the 60/30/10 USD `split` locally (exactly as the mock + the
2485
- * contract compute it) from the parsed amount.
2433
+ * pass its authoritative `status`/`txHash`/amounts straight through.
2434
+ *
2435
+ * For entry, `Payment.split` is a **first-party 60/30/10 projection** from the
2436
+ * request amount using SDK BPS constants (same integer math as mock) — not a
2437
+ * chain read of pool constructor bps or post-lock payable pot (sdk#514 residual N1).
2438
+ * Prefer `chainAmount` / on-chain `getRound` for money truth.
2486
2439
  */
2487
2440
  mapServerPayment(p, kind, amountMicro) {
2488
2441
  const payment = {
package/dist/server.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var crypto = require('crypto');
4
+ var viem = require('viem');
4
5
 
5
6
  // src/webhooks.ts
6
7
 
@@ -14,6 +15,11 @@ var PlaymosError = class extends Error {
14
15
  Object.setPrototypeOf(this, new.target.prototype);
15
16
  }
16
17
  };
18
+ var ApiError = class extends PlaymosError {
19
+ constructor(message, detail) {
20
+ super("api_error", message, detail);
21
+ }
22
+ };
17
23
 
18
24
  // src/webhooks.ts
19
25
  var WebhookSignatureError = class extends PlaymosError {
@@ -56,6 +62,117 @@ function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
56
62
  }
57
63
  return JSON.parse(body);
58
64
  }
65
+ var toBytes32 = (s) => viem.keccak256(viem.toBytes(s));
66
+
67
+ // src/hasEntered.ts
68
+ var hasEnteredAbi = [
69
+ {
70
+ type: "function",
71
+ name: "hasEntered",
72
+ stateMutability: "view",
73
+ inputs: [
74
+ { name: "roundId", type: "bytes32" },
75
+ { name: "identity", type: "bytes32" }
76
+ ],
77
+ outputs: [{ type: "bool" }]
78
+ }
79
+ ];
80
+ var HAS_ENTERED_DEFAULT_TIMEOUT_MS = 8e3;
81
+ async function hasEntered(args) {
82
+ const rpcUrl = typeof args.rpcUrl === "string" ? args.rpcUrl.trim() : "";
83
+ if (!rpcUrl) {
84
+ throw new ApiError('hasEntered requires a non-empty "rpcUrl" (server-side only \u2014 no wallet provider)');
85
+ }
86
+ if (!args.prizePool || !/^0x[0-9a-fA-F]{40}$/.test(args.prizePool)) {
87
+ throw new ApiError("hasEntered requires prizePool as a 0x-prefixed 20-byte address");
88
+ }
89
+ if (!args.roundKey?.trim() || !args.identity?.trim()) {
90
+ throw new ApiError("hasEntered requires non-empty roundKey and identity (plain strings, same as enterRound)");
91
+ }
92
+ const roundKey = args.roundKey.trim();
93
+ const identity = args.identity.trim();
94
+ if (/^0x[0-9a-fA-F]{64}$/.test(roundKey) || /^0x[0-9a-fA-F]{64}$/.test(identity)) {
95
+ throw new ApiError(
96
+ "hasEntered requires plain-string roundKey/identity (not 0x+64 hex). Pre-hashed pins diverge from enterRound and from service GET /entered. Pass the same plain pins used at enter.",
97
+ { prizePool: args.prizePool, roundKey }
98
+ );
99
+ }
100
+ const data = viem.encodeFunctionData({
101
+ abi: hasEnteredAbi,
102
+ functionName: "hasEntered",
103
+ args: [toBytes32(roundKey), toBytes32(identity)]
104
+ });
105
+ const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs > 0 ? args.timeoutMs : HAS_ENTERED_DEFAULT_TIMEOUT_MS;
106
+ const controller = new AbortController();
107
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
108
+ const onExternalAbort = () => controller.abort();
109
+ if (args.signal) {
110
+ if (args.signal.aborted) controller.abort();
111
+ else args.signal.addEventListener("abort", onExternalAbort, { once: true });
112
+ }
113
+ let res;
114
+ try {
115
+ res = await fetch(rpcUrl, {
116
+ method: "POST",
117
+ headers: { "content-type": "application/json" },
118
+ body: JSON.stringify({
119
+ jsonrpc: "2.0",
120
+ id: 1,
121
+ method: "eth_call",
122
+ params: [{ to: args.prizePool, data }, "latest"]
123
+ }),
124
+ signal: controller.signal
125
+ });
126
+ } catch (e) {
127
+ const msg = e.message ?? String(e);
128
+ const aborted = controller.signal.aborted || e.name === "AbortError" || /abort/i.test(msg);
129
+ throw new ApiError(
130
+ aborted ? `hasEntered RPC timed out after ${timeoutMs}ms (or was aborted)` : `hasEntered RPC request failed: ${msg}`,
131
+ { prizePool: args.prizePool, roundKey }
132
+ );
133
+ } finally {
134
+ clearTimeout(timer);
135
+ if (args.signal) args.signal.removeEventListener("abort", onExternalAbort);
136
+ }
137
+ if (!res.ok) {
138
+ throw new ApiError(`hasEntered RPC HTTP ${res.status}`, {
139
+ prizePool: args.prizePool,
140
+ roundKey
141
+ });
142
+ }
143
+ let json;
144
+ try {
145
+ json = await res.json();
146
+ } catch (e) {
147
+ throw new ApiError(`hasEntered RPC response not JSON: ${e.message}`);
148
+ }
149
+ if (json.error || json.result == null || json.result === "") {
150
+ throw new ApiError(
151
+ `hasEntered eth_call failed: ${json.error?.message ?? `HTTP ${res.status}`}`,
152
+ { prizePool: args.prizePool, roundKey }
153
+ );
154
+ }
155
+ if (json.result === "0x" || json.result === "0x0") {
156
+ throw new ApiError(
157
+ `hasEntered eth_call returned empty data for pool ${args.prizePool} \u2014 wrong address or no contract code`,
158
+ { prizePool: args.prizePool, roundKey }
159
+ );
160
+ }
161
+ try {
162
+ return viem.decodeFunctionResult({
163
+ abi: hasEnteredAbi,
164
+ functionName: "hasEntered",
165
+ data: json.result
166
+ });
167
+ } catch (e) {
168
+ throw new ApiError(
169
+ `hasEntered eth_call result malformed: ${e.message}`,
170
+ { prizePool: args.prizePool, roundKey }
171
+ );
172
+ }
173
+ }
59
174
 
60
175
  exports.WebhookSignatureError = WebhookSignatureError;
176
+ exports.enterPathToBytes32 = toBytes32;
177
+ exports.hasEntered = hasEntered;
61
178
  exports.verifyWebhook = verifyWebhook;
package/dist/server.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Z as PlaymosError, W as WebhookEvent } from './errors-Chizbb96.cjs';
1
+ import { Z as PlaymosError, W as WebhookEvent } from './errors-Dpeesyop.cjs';
2
2
 
3
3
  /**
4
4
  * Webhook signature verification (spec §6.2) — server-side only.
@@ -23,4 +23,36 @@ declare function verifyWebhook(rawBody: string | Buffer, signatureHeader: string
23
23
  toleranceSeconds?: number;
24
24
  }): WebhookEvent;
25
25
 
26
- export { WebhookSignatureError, verifyWebhook };
26
+ /**
27
+ * Builders for the two settling calls. Both mirror the proven entry batch:
28
+ * [approve(spender, amount), settle(...)] sent atomically via EIP-5792.
29
+ */
30
+
31
+ /** The server derives these identically (keccak256 of UTF-8 bytes) so the
32
+ * off-chain record and the on-chain call line up. */
33
+ declare const toBytes32: (s: string) => `0x${string}`;
34
+
35
+ interface HasEnteredArgs {
36
+ prizePool: `0x${string}`;
37
+ /** Same plain string pin as enterRound roundKey / body roundId. */
38
+ roundKey: string;
39
+ /** Same plain string pin as enterRound identity. */
40
+ identity: string;
41
+ /** Required JSON-RPC URL for eth_call (server-owned). */
42
+ rpcUrl: string;
43
+ /**
44
+ * Optional deadline for the RPC fetch (ms). Default 8000.
45
+ * On timeout throws ApiError (never returns false).
46
+ */
47
+ timeoutMs?: number;
48
+ /** Optional AbortSignal; combined with timeoutMs when both set. */
49
+ signal?: AbortSignal;
50
+ }
51
+ /**
52
+ * On-chain hasEntered for admit-to-session from a game backend.
53
+ * @returns true only when the chain read succeeds and returns true.
54
+ * @throws ApiError when eth_call fails, times out, or response is malformed.
55
+ */
56
+ declare function hasEntered(args: HasEnteredArgs): Promise<boolean>;
57
+
58
+ export { type HasEnteredArgs, WebhookSignatureError, toBytes32 as enterPathToBytes32, hasEntered, verifyWebhook };
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Z as PlaymosError, W as WebhookEvent } from './errors-Chizbb96.js';
1
+ import { Z as PlaymosError, W as WebhookEvent } from './errors-Dpeesyop.js';
2
2
 
3
3
  /**
4
4
  * Webhook signature verification (spec §6.2) — server-side only.
@@ -23,4 +23,36 @@ declare function verifyWebhook(rawBody: string | Buffer, signatureHeader: string
23
23
  toleranceSeconds?: number;
24
24
  }): WebhookEvent;
25
25
 
26
- export { WebhookSignatureError, verifyWebhook };
26
+ /**
27
+ * Builders for the two settling calls. Both mirror the proven entry batch:
28
+ * [approve(spender, amount), settle(...)] sent atomically via EIP-5792.
29
+ */
30
+
31
+ /** The server derives these identically (keccak256 of UTF-8 bytes) so the
32
+ * off-chain record and the on-chain call line up. */
33
+ declare const toBytes32: (s: string) => `0x${string}`;
34
+
35
+ interface HasEnteredArgs {
36
+ prizePool: `0x${string}`;
37
+ /** Same plain string pin as enterRound roundKey / body roundId. */
38
+ roundKey: string;
39
+ /** Same plain string pin as enterRound identity. */
40
+ identity: string;
41
+ /** Required JSON-RPC URL for eth_call (server-owned). */
42
+ rpcUrl: string;
43
+ /**
44
+ * Optional deadline for the RPC fetch (ms). Default 8000.
45
+ * On timeout throws ApiError (never returns false).
46
+ */
47
+ timeoutMs?: number;
48
+ /** Optional AbortSignal; combined with timeoutMs when both set. */
49
+ signal?: AbortSignal;
50
+ }
51
+ /**
52
+ * On-chain hasEntered for admit-to-session from a game backend.
53
+ * @returns true only when the chain read succeeds and returns true.
54
+ * @throws ApiError when eth_call fails, times out, or response is malformed.
55
+ */
56
+ declare function hasEntered(args: HasEnteredArgs): Promise<boolean>;
57
+
58
+ export { type HasEnteredArgs, WebhookSignatureError, toBytes32 as enterPathToBytes32, hasEntered, verifyWebhook };