@playmos/sdk 0.3.7 → 0.3.8

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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { N as Network, P as PlaymosConfig, W as WebhookEvent, R as RoundOpenInput, a as RoundState, b as RoundSettleInput, S as SettleRoundResult, C as CancelRoundResult, A as ActiveSeriesRound, c as PrizeBalance, d as WithdrawResult, e as AgentWallet, f as AgentFundResult, T as TransferResult, E as EscrowHoldInput, g as EscrowHoldResult, h as EscrowResolveResult, M as MarketplaceListInput, L as Listing, i as MarketplaceSaleResult, j as MarketplaceGetResult, k as PayInput, l as Payment, m as EnterRoundInput, n as TransferReconcile, o as WaitOptions, p as TransferInput, q as TransferConfirmOptions, V as VerifyResult, r as PayoutRule } from './errors-CdVzHuhY.js';
2
- export { s as ActiveForSeriesResult, t as AgentEconomyConfig, u as ApiError, v as AuthError, w as ConfigError, x as ContractConfig, y as Eip1193Provider, G as GasConfig, z as GasMode, I as InsufficientGasError, B as InvalidAmountError, D as ListingStatus, F as MarketplaceItem, H as MarketplaceSale, J as MissingFieldError, K as NothingToWithdrawError, O as PaymentFailedError, Q as PaymentStatus, U as PlaymosError, X as PlaymosErrorCode, Y as RetryOptions, Z as RoundStatus, _ as WalletConfig, $ as WalletConnectionError, a0 as WalletConnector, a1 as WebhookEventType } from './errors-CdVzHuhY.js';
1
+ import { N as Network, P as PlaymosConfig, W as WebhookEvent, R as RoundOpenInput, a as RoundState, b as RoundSettleInput, S as SettleRoundResult, c as RoundCancelInput, C as CancelRoundResult, A as ActiveSeriesRound, d as PrizeBalance, e as WithdrawResult, f as AgentWallet, g as AgentFundResult, T as TransferResult, E as EscrowHoldInput, h as EscrowHoldResult, i as EscrowResolveResult, M as MarketplaceListInput, L as Listing, j as MarketplaceSaleResult, k as MarketplaceGetResult, l as PayInput, m as Payment, n as EnterRoundInput, o as TransferReconcile, p as WaitOptions, q as TransferInput, r as TransferConfirmOptions, V as VerifyResult, s as PayoutRule } from './errors-BMlWHsMb.js';
2
+ export { t as ActiveForSeriesResult, u as AgentEconomyConfig, v as AlreadyEnteredError, w as ApiError, x as AuthError, y as ConfigError, z as ContractConfig, B as Eip1193Provider, G as GasConfig, D as GasMode, I as InsufficientGasError, F as InvalidAmountError, H as ListingStatus, J as MarketplaceItem, K as MarketplaceSale, O as MissingFieldError, Q as NothingToWithdrawError, U as PaymentFailedError, X as PaymentStatus, Y as PlaymosError, Z as PlaymosErrorCode, _ as RetryOptions, $ as RoundStatus, a0 as WalletConfig, a1 as WalletConnectionError, a2 as WalletConnector, a3 as WalletTimeoutError, a4 as WebhookEventType } from './errors-BMlWHsMb.js';
3
3
 
4
4
  /**
5
5
  * Environment resolution + the canonical address book.
@@ -477,13 +477,14 @@ declare class Playmos {
477
477
  settle: (input: RoundSettleInput) => Promise<SettleRoundResult>;
478
478
  /**
479
479
  * Cancel an open/locked round (operator sk_ only). Refunds entrants' escrowed ~90%
480
- * and frees the series latch. May return `status: "cancelling"` (HTTP 202) until
481
- * chain Cancelled(4) reconciles — poll `rounds.get` or re-call cancel (#346).
480
+ * and frees the series latch (#346 / #376).
481
+ *
482
+ * **Default is submit-only:** may return `status: "cancelling"` after broadcast —
483
+ * that is **not** success. Only `status: "cancelled"` means the chain is terminal.
484
+ * Pass `{ confirm: true }` to poll until cancelled or timeout (honest `cancelling`
485
+ * if still pending — never invents terminal success).
482
486
  */
483
- cancel: (input: {
484
- roundId: string;
485
- gameId?: string;
486
- }) => Promise<CancelRoundResult>;
487
+ cancel: (input: RoundCancelInput) => Promise<CancelRoundResult>;
487
488
  get: (input: {
488
489
  roundId: string;
489
490
  }) => Promise<RoundState>;
@@ -606,6 +607,14 @@ declare class Playmos {
606
607
  */
607
608
  private readonly mockPayments;
608
609
  constructor(config: PlaymosConfig);
610
+ /**
611
+ * Wallet timeout policy (#462 / PR #463 residual).
612
+ * - connectTimeoutMs → eth_requestAccounts only
613
+ * - machine RPCs → 20s default
614
+ * - wallet_sendCalls → unbounded (0) — never short-time a payment sheet
615
+ */
616
+ private walletTimeoutPolicy;
617
+ private walletProvider;
609
618
  /** Connect the player's wallet and return their address. */
610
619
  connect(): Promise<`0x${string}`>;
611
620
  /**
@@ -848,4 +857,4 @@ declare function ulid(seedTime?: number): string;
848
857
  /** `${prefix}_${ulid()}` — e.g. `pay_01J…`, `entry_01J…`, `idem_01J…`. */
849
858
  declare function prefixedId(prefix: string): string;
850
859
 
851
- export { ActiveSeriesRound, AgentFundResult, AgentWallet, type Authorization, CHAIN_ID, CancelRoundResult, type CreatePaymentRequirementInput, DEFAULT_API_BASE_URL, EnterRoundInput, EscrowHoldInput, EscrowHoldResult, EscrowResolveResult, Listing, MICRO_PER_USDC, MarketplaceGetResult, MarketplaceListInput, MarketplaceSaleResult, Network, PayInput, Payment, type PaymentRequirement, PayoutError, PayoutRule, PayoutRule as PayoutRuleCompute, Playmos, PlaymosConfig, PrizeBalance, RoundOpenInput, RoundSettleInput, RoundState, SettleRoundResult, type SettlementAsset, TransferConfirmOptions, TransferInput, TransferReconcile, TransferResult, USDC_ADDRESS, USDC_DECIMALS, VerifyResult, WaitOptions, type WalletSignatureAuthorization, WebhookEvent, WithdrawResult, type X402ChallengeInput, type X402ChallengeResult, type X402FulfillAuthorization, type X402PayInput, type X402PayloadAuthorization, type X402PayloadMode, type X402PaymentRequired, type X402SettleResult, 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 };
860
+ export { ActiveSeriesRound, AgentFundResult, AgentWallet, type Authorization, CHAIN_ID, CancelRoundResult, type CreatePaymentRequirementInput, DEFAULT_API_BASE_URL, EnterRoundInput, EscrowHoldInput, EscrowHoldResult, EscrowResolveResult, Listing, MICRO_PER_USDC, MarketplaceGetResult, MarketplaceListInput, MarketplaceSaleResult, Network, PayInput, Payment, type PaymentRequirement, PayoutError, PayoutRule, PayoutRule as PayoutRuleCompute, Playmos, PlaymosConfig, PrizeBalance, RoundCancelInput, RoundOpenInput, RoundSettleInput, RoundState, SettleRoundResult, type SettlementAsset, TransferConfirmOptions, TransferInput, TransferReconcile, TransferResult, USDC_ADDRESS, USDC_DECIMALS, VerifyResult, WaitOptions, type WalletSignatureAuthorization, WebhookEvent, WithdrawResult, type X402ChallengeInput, type X402ChallengeResult, type X402FulfillAuthorization, type X402PayInput, type X402PayloadAuthorization, type X402PayloadMode, type X402PaymentRequired, type X402SettleResult, 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 };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
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';
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
3
  import { encodeFunctionData, decodeFunctionResult, numberToHex, keccak256, toBytes } from 'viem';
4
4
 
5
5
  // src/config.ts
@@ -329,7 +329,7 @@ function createHttpClient(baseUrl, apiKey, retry) {
329
329
  const why = res.__playmosSignerBelowReason ?? "low_balance";
330
330
  msg = `Sandbox signer below funding floor (${why}) (${res.status} from ${res.url}) \u2014 top up Sepolia USDC (see GET /health capabilities.payments.signerBalance). Do not retry; retry cannot succeed until funded. Cross-ref: issue #205 recurrence \xB7 hub funding #15.`;
331
331
  } else if (gateway) {
332
- msg = `Sandbox gateway timeout (${res.status}) from ${res.url} \u2014 the request likely exceeded the platform limit while settling on-chain. Retry with the same idempotency key (safe); prefer a dedicated Base Sepolia RPC on the service (BB-GATEB-002).`;
332
+ msg = `Sandbox gateway timeout (${res.status}) from ${res.url} \u2014 the request likely exceeded the platform edge limit while on-chain work ran. Retry with the same idempotency key after checking chain/service status (safe if settle already landed). Do not assume missing RPC \u2014 sandbox usually has one; prefer shorter async settle paths (BB-GATEB-002 / #422).`;
333
333
  } else {
334
334
  msg = `Non-JSON response (${res.status}) from ${res.url}`;
335
335
  }
@@ -371,7 +371,60 @@ function createHttpClient(baseUrl, apiKey, retry) {
371
371
  }
372
372
 
373
373
  // src/wallet.ts
374
- function resolveProvider(wallet) {
374
+ var DEFAULT_WALLET_REQUEST_TIMEOUT_MS = 2e4;
375
+ var MONEY_MOVING_METHODS = /* @__PURE__ */ new Set([
376
+ "wallet_sendCalls",
377
+ "eth_sendTransaction",
378
+ "eth_sendRawTransaction"
379
+ ]);
380
+ var CONNECT_METHODS = /* @__PURE__ */ new Set(["eth_requestAccounts"]);
381
+ var TIMED_FLAG = "__playmosTimedRequest";
382
+ function resolvePolicy(opts) {
383
+ const requestTimeoutMs = typeof opts?.requestTimeoutMs === "number" && opts.requestTimeoutMs > 0 ? opts.requestTimeoutMs : DEFAULT_WALLET_REQUEST_TIMEOUT_MS;
384
+ const connectTimeoutMs = typeof opts?.connectTimeoutMs === "number" && opts.connectTimeoutMs > 0 ? opts.connectTimeoutMs : requestTimeoutMs;
385
+ const sendCallsTimeoutMs = typeof opts?.sendCallsTimeoutMs === "number" && opts.sendCallsTimeoutMs >= 0 ? opts.sendCallsTimeoutMs : 0;
386
+ return { requestTimeoutMs, connectTimeoutMs, sendCallsTimeoutMs };
387
+ }
388
+ function timeoutMsForMethod(method, opts) {
389
+ const p = resolvePolicy(opts);
390
+ if (MONEY_MOVING_METHODS.has(method)) return p.sendCallsTimeoutMs;
391
+ if (CONNECT_METHODS.has(method)) return p.connectTimeoutMs;
392
+ return p.requestTimeoutMs;
393
+ }
394
+ async function timedRequest(provider, args, timeoutMs = DEFAULT_WALLET_REQUEST_TIMEOUT_MS) {
395
+ if (timeoutMs <= 0) {
396
+ return provider.request(args);
397
+ }
398
+ let timer;
399
+ try {
400
+ return await Promise.race([
401
+ provider.request(args),
402
+ new Promise((_, reject) => {
403
+ timer = setTimeout(() => {
404
+ reject(
405
+ new WalletTimeoutError(
406
+ `Wallet request "${args.method}" timed out after ${timeoutMs}ms.`,
407
+ { method: args.method, timeoutMs }
408
+ )
409
+ );
410
+ }, timeoutMs);
411
+ })
412
+ ]);
413
+ } finally {
414
+ if (timer !== void 0) clearTimeout(timer);
415
+ }
416
+ }
417
+ function withTimeoutProvider(provider, opts) {
418
+ const flagged = provider;
419
+ if (flagged[TIMED_FLAG]) return provider;
420
+ const policy = typeof opts === "number" ? { requestTimeoutMs: opts, connectTimeoutMs: opts } : opts ?? {};
421
+ const wrapped = {
422
+ request: (args) => timedRequest(provider, args, timeoutMsForMethod(args.method, policy))
423
+ };
424
+ Object.defineProperty(wrapped, TIMED_FLAG, { value: true, enumerable: false });
425
+ return wrapped;
426
+ }
427
+ function resolveRawProvider(wallet) {
375
428
  if (wallet?.provider) return wallet.provider;
376
429
  const injected = globalThis.ethereum;
377
430
  const connector = wallet?.connector ?? "base-account";
@@ -388,18 +441,23 @@ function resolveProvider(wallet) {
388
441
  "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."
389
442
  );
390
443
  }
391
- async function walletAvailable(wallet) {
444
+ function resolveProvider(wallet, opts) {
445
+ const raw = resolveRawProvider(wallet);
446
+ return withTimeoutProvider(raw, opts);
447
+ }
448
+ async function walletAvailable(wallet, opts) {
392
449
  if (wallet?.provider) return true;
393
450
  let provider;
394
451
  try {
395
- provider = resolveProvider(wallet);
452
+ provider = resolveProvider(wallet, opts);
396
453
  } catch {
397
454
  return false;
398
455
  }
399
456
  try {
400
457
  await getAccount(provider);
401
458
  return true;
402
- } catch {
459
+ } catch (e) {
460
+ if (e instanceof WalletTimeoutError) throw e;
403
461
  return false;
404
462
  }
405
463
  }
@@ -410,6 +468,7 @@ async function getAccount(provider) {
410
468
  if (!addr) throw new Error("no account");
411
469
  return addr;
412
470
  } catch (e) {
471
+ if (e instanceof WalletTimeoutError) throw e;
413
472
  throw new WalletConnectionError("Could not read the player's wallet account.", {
414
473
  cause: e?.message
415
474
  });
@@ -483,6 +542,7 @@ async function sendCalls(provider, from, chainId, calls, paymasterUrl) {
483
542
  try {
484
543
  result = await provider.request({ method: "wallet_sendCalls", params: [params] });
485
544
  } catch (e) {
545
+ if (e instanceof WalletTimeoutError) throw e;
486
546
  const msg = e?.message ?? String(e);
487
547
  if (/reject|denied|cancel|closed/i.test(msg)) {
488
548
  throw new WalletConnectionError("The player cancelled or closed the payment sheet.", { cause: msg });
@@ -497,14 +557,22 @@ async function waitForCalls(provider, id, timeoutMs = 6e4) {
497
557
  if (!id) return { status: "FAILED" };
498
558
  const deadline = Date.now() + timeoutMs;
499
559
  while (Date.now() < deadline) {
500
- const res = await provider.request({
501
- method: "wallet_getCallsStatus",
502
- params: [id]
503
- });
504
- const s = String(res?.status ?? "").toUpperCase();
505
- const txHash = res?.receipts?.[0]?.transactionHash;
506
- if (s === "200" || s === "CONFIRMED" || s === "SUCCESS") return { status: "CONFIRMED", txHash };
507
- if (s === "400" || s === "500" || s === "FAILED" || s === "REVERTED") return { status: "FAILED", txHash };
560
+ try {
561
+ const res = await provider.request({
562
+ method: "wallet_getCallsStatus",
563
+ params: [id]
564
+ });
565
+ const s = String(res?.status ?? "").toUpperCase();
566
+ const txHash = res?.receipts?.[0]?.transactionHash;
567
+ if (s === "200" || s === "CONFIRMED" || s === "SUCCESS") return { status: "CONFIRMED", txHash };
568
+ if (s === "400" || s === "500" || s === "FAILED" || s === "REVERTED") return { status: "FAILED", txHash };
569
+ } catch (e) {
570
+ if (e instanceof WalletTimeoutError) {
571
+ await new Promise((r) => setTimeout(r, 900));
572
+ continue;
573
+ }
574
+ throw e;
575
+ }
508
576
  await new Promise((r) => setTimeout(r, 900));
509
577
  }
510
578
  return { status: "PENDING" };
@@ -780,6 +848,49 @@ function validateX402ChallengeInput(input) {
780
848
 
781
849
  // src/client.ts
782
850
  var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
851
+ function mapAlreadyEntered(e) {
852
+ const msg = e instanceof Error ? e.message : typeof e === "object" && e && "message" in e ? String(e.message) : String(e);
853
+ const detail = e instanceof PaymentFailedError || e instanceof ApiError ? e.detail : void 0;
854
+ const blob = `${msg} ${JSON.stringify(detail ?? {})}`;
855
+ if (/AlreadyEntered|already entered|already_entered/i.test(blob)) {
856
+ throw new AlreadyEnteredError({ cause: msg, ...detail ?? {} });
857
+ }
858
+ throw e;
859
+ }
860
+ var SETTLE_RECONCILE_TIMEOUT_MS = 2e4;
861
+ var SETTLE_RECONCILE_INTERVAL_MS = 1e3;
862
+ function isSoftRoundPollError(e, kind) {
863
+ if (e instanceof AuthError || e instanceof ConfigError) return false;
864
+ if (e instanceof ApiError) {
865
+ const status = typeof e.detail?.status === "number" ? e.detail.status : void 0;
866
+ const msg = e.message;
867
+ if (status === 409 && /already in progress|in progress — retry/i.test(msg)) {
868
+ return true;
869
+ }
870
+ if (kind === "settle") {
871
+ if (/cannot settle|not Locked on-chain|is already settled|nothing to settle/i.test(msg)) {
872
+ return false;
873
+ }
874
+ } else {
875
+ if (/cannot cancel|is already settled|is Settled on-chain|already settled — cannot cancel/i.test(
876
+ msg
877
+ )) {
878
+ return false;
879
+ }
880
+ }
881
+ if (status === 502 || status === 503 || status === 504 || status === 429 || e.detail?.gateway === true) {
882
+ return true;
883
+ }
884
+ if (status !== void 0 && status >= 400 && status < 500) return false;
885
+ if (status !== void 0 && status >= 500) return true;
886
+ }
887
+ return true;
888
+ }
889
+ function isSoftSettlePollError(e) {
890
+ return isSoftRoundPollError(e, "settle");
891
+ }
892
+ var CANCEL_RECONCILE_TIMEOUT_MS = SETTLE_RECONCILE_TIMEOUT_MS;
893
+ var CANCEL_RECONCILE_INTERVAL_MS = SETTLE_RECONCILE_INTERVAL_MS;
783
894
  function requireAddressField(value, field) {
784
895
  if (typeof value !== "string" || value.trim() === "") {
785
896
  throw new MissingFieldError(field);
@@ -1126,17 +1237,61 @@ var Playmos = class {
1126
1237
  status: "settled"
1127
1238
  };
1128
1239
  }
1129
- const { settle } = await this.http.post(
1240
+ const timeoutMs = input.timeoutMs ?? SETTLE_RECONCILE_TIMEOUT_MS;
1241
+ const intervalMs = input.intervalMs ?? SETTLE_RECONCILE_INTERVAL_MS;
1242
+ const body = { gameId: input.gameId, results: input.results };
1243
+ const idempotencyKey = `settle:${input.roundId}`;
1244
+ const postSettle = () => this.http.post(
1130
1245
  `/rounds/${encodeURIComponent(input.roundId)}/settle`,
1131
- { gameId: input.gameId, results: input.results },
1132
- { acceptStatuses: [202] }
1246
+ body,
1247
+ { acceptStatuses: [200, 202], idempotencyKey }
1133
1248
  );
1134
- return settle;
1249
+ let { settle } = await postSettle();
1250
+ if (settle.status === "settled") return settle;
1251
+ const firstWinners = settle.winners;
1252
+ const firstPoolPaid = settle.poolPaid ?? null;
1253
+ const withFirstExtras = (s) => ({
1254
+ ...s,
1255
+ winners: s.winners ?? firstWinners,
1256
+ poolPaid: s.poolPaid ?? firstPoolPaid
1257
+ });
1258
+ const deadline = Date.now() + timeoutMs;
1259
+ while (Date.now() < deadline && settle.status === "settling") {
1260
+ const wait = Math.min(intervalMs, Math.max(0, deadline - Date.now()));
1261
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
1262
+ if (Date.now() >= deadline) break;
1263
+ try {
1264
+ const again = await postSettle();
1265
+ settle = withFirstExtras(again.settle);
1266
+ if (settle.status === "settled") return settle;
1267
+ } catch (e) {
1268
+ if (!isSoftSettlePollError(e)) throw e;
1269
+ }
1270
+ try {
1271
+ const round = await this.rounds.get({ roundId: input.roundId });
1272
+ if (round.status === "settled") {
1273
+ return {
1274
+ roundId: input.roundId,
1275
+ txHash: round.settleTxHash ?? settle.txHash ?? null,
1276
+ poolPaid: round.pool ?? settle.poolPaid ?? firstPoolPaid,
1277
+ winners: settle.winners ?? firstWinners,
1278
+ status: "settled"
1279
+ };
1280
+ }
1281
+ } catch (e) {
1282
+ if (!isSoftSettlePollError(e)) throw e;
1283
+ }
1284
+ }
1285
+ return withFirstExtras(settle);
1135
1286
  },
1136
1287
  /**
1137
1288
  * Cancel an open/locked round (operator sk_ only). Refunds entrants' escrowed ~90%
1138
- * and frees the series latch. May return `status: "cancelling"` (HTTP 202) until
1139
- * chain Cancelled(4) reconciles — poll `rounds.get` or re-call cancel (#346).
1289
+ * and frees the series latch (#346 / #376).
1290
+ *
1291
+ * **Default is submit-only:** may return `status: "cancelling"` after broadcast —
1292
+ * that is **not** success. Only `status: "cancelled"` means the chain is terminal.
1293
+ * Pass `{ confirm: true }` to poll until cancelled or timeout (honest `cancelling`
1294
+ * if still pending — never invents terminal success).
1140
1295
  */
1141
1296
  cancel: async (input) => {
1142
1297
  if (!this.config.mock) this.assertSecretKey("playmos.rounds.cancel");
@@ -1174,18 +1329,57 @@ var Playmos = class {
1174
1329
  round: cancelled
1175
1330
  };
1176
1331
  }
1177
- const body = await this.http.post(
1178
- `/rounds/${encodeURIComponent(input.roundId)}/cancel`,
1179
- { gameId: input.gameId },
1180
- { acceptStatuses: [202] }
1181
- );
1182
- const status = body.status === "cancelling" || body.round?.status === "cancelling" ? "cancelling" : "cancelled";
1183
- return {
1184
- roundId: input.roundId,
1185
- txHash: body.txHash ?? body.round?.cancelTxHash ?? null,
1186
- status,
1187
- round: body.round
1332
+ const bodyPayload = { gameId: input.gameId };
1333
+ const postCancel = () => this.http.post(`/rounds/${encodeURIComponent(input.roundId)}/cancel`, bodyPayload, {
1334
+ acceptStatuses: [200, 202]
1335
+ });
1336
+ const toResult = (body, priorTx) => {
1337
+ const explicitCancelled = body.status === "cancelled" || body.round?.status === "cancelled";
1338
+ const explicitCancelling = body.status === "cancelling" || body.round?.status === "cancelling";
1339
+ const status = explicitCancelled && !explicitCancelling ? "cancelled" : "cancelling";
1340
+ const tx = body.txHash ?? body.round?.cancelTxHash ?? priorTx ?? null;
1341
+ return {
1342
+ roundId: input.roundId,
1343
+ txHash: tx,
1344
+ status,
1345
+ round: body.round
1346
+ };
1188
1347
  };
1348
+ let result = toResult(await postCancel());
1349
+ if (result.status === "cancelled") return result;
1350
+ if (!input.confirm) return result;
1351
+ const timeoutMs = Math.min(
1352
+ Math.max(1, input.timeoutMs ?? CANCEL_RECONCILE_TIMEOUT_MS),
1353
+ CANCEL_RECONCILE_TIMEOUT_MS
1354
+ );
1355
+ const intervalMs = Math.max(1, input.intervalMs ?? CANCEL_RECONCILE_INTERVAL_MS);
1356
+ const deadline = Date.now() + timeoutMs;
1357
+ const firstTx = result.txHash;
1358
+ while (Date.now() < deadline && result.status === "cancelling") {
1359
+ const wait = Math.min(intervalMs, Math.max(0, deadline - Date.now()));
1360
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
1361
+ if (Date.now() >= deadline) break;
1362
+ try {
1363
+ result = toResult(await postCancel(), firstTx);
1364
+ if (result.status === "cancelled") return result;
1365
+ } catch (e) {
1366
+ if (!isSoftRoundPollError(e, "cancel")) throw e;
1367
+ }
1368
+ try {
1369
+ const round = await this.rounds.get({ roundId: input.roundId });
1370
+ if (round.status === "cancelled") {
1371
+ return {
1372
+ roundId: input.roundId,
1373
+ txHash: round.cancelTxHash ?? result.txHash ?? firstTx ?? null,
1374
+ status: "cancelled",
1375
+ round
1376
+ };
1377
+ }
1378
+ } catch (e) {
1379
+ if (!isSoftRoundPollError(e, "cancel")) throw e;
1380
+ }
1381
+ }
1382
+ return { ...result, txHash: result.txHash ?? firstTx ?? null };
1189
1383
  },
1190
1384
  get: async (input) => {
1191
1385
  requireField(input?.roundId, "roundId");
@@ -1336,7 +1530,7 @@ var Playmos = class {
1336
1530
  }
1337
1531
  prizePool = round.prizePoolAddress;
1338
1532
  }
1339
- const provider = resolveProvider(this.config.wallet);
1533
+ const provider = this.walletProvider();
1340
1534
  if (this.config.gas?.mode === "player") {
1341
1535
  const from0 = await getAccount(provider);
1342
1536
  await assertEnoughGas(provider, from0);
@@ -1353,18 +1547,31 @@ var Playmos = class {
1353
1547
  const paymasterUrl = this.config.gas?.mode === "sponsored" ? this.config.gas.paymasterUrl : void 0;
1354
1548
  let txHash;
1355
1549
  let status = "pending";
1550
+ let callsId;
1356
1551
  try {
1357
- const { id: callsId } = await sendCalls(
1552
+ const sent = await sendCalls(
1358
1553
  provider,
1359
1554
  from,
1360
1555
  this.env.chainId,
1361
1556
  [call],
1362
1557
  paymasterUrl
1363
1558
  );
1559
+ callsId = sent.id;
1364
1560
  const waited = await waitForCalls(provider, callsId);
1365
1561
  txHash = waited.txHash;
1366
1562
  status = waited.status === "CONFIRMED" ? "confirmed" : waited.status === "FAILED" ? "failed" : "pending";
1367
1563
  } catch (e) {
1564
+ if (e instanceof WalletTimeoutError && callsId) {
1565
+ return {
1566
+ prizePoolAddress: prizePool,
1567
+ amount: formatMicroToUsd(amountMicro),
1568
+ amountMicro: amountMicro.toString(),
1569
+ txHash,
1570
+ status: "pending",
1571
+ callsId
1572
+ };
1573
+ }
1574
+ if (e instanceof WalletTimeoutError) throw e;
1368
1575
  const msg = e?.message ?? String(e);
1369
1576
  if (/NothingToWithdraw|nothing to withdraw/i.test(msg)) {
1370
1577
  throw new NothingToWithdrawError({ prizePoolAddress: prizePool, wallet: from, cause: msg });
@@ -1384,7 +1591,8 @@ var Playmos = class {
1384
1591
  amount: formatMicroToUsd(amountMicro),
1385
1592
  amountMicro: amountMicro.toString(),
1386
1593
  txHash,
1387
- status
1594
+ status,
1595
+ ...status === "pending" && callsId ? { callsId } : {}
1388
1596
  };
1389
1597
  }
1390
1598
  };
@@ -1631,13 +1839,36 @@ var Playmos = class {
1631
1839
  }
1632
1840
  }
1633
1841
  this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
1842
+ if (!config.mock) {
1843
+ try {
1844
+ resolveProvider(config.wallet, this.walletTimeoutPolicy());
1845
+ } catch {
1846
+ }
1847
+ }
1848
+ }
1849
+ /**
1850
+ * Wallet timeout policy (#462 / PR #463 residual).
1851
+ * - connectTimeoutMs → eth_requestAccounts only
1852
+ * - machine RPCs → 20s default
1853
+ * - wallet_sendCalls → unbounded (0) — never short-time a payment sheet
1854
+ */
1855
+ walletTimeoutPolicy() {
1856
+ const connect = typeof this.config.connectTimeoutMs === "number" && this.config.connectTimeoutMs > 0 ? this.config.connectTimeoutMs : 2e4;
1857
+ return {
1858
+ connectTimeoutMs: connect,
1859
+ requestTimeoutMs: 2e4,
1860
+ sendCallsTimeoutMs: 0
1861
+ };
1862
+ }
1863
+ walletProvider() {
1864
+ return resolveProvider(this.config.wallet, this.walletTimeoutPolicy());
1634
1865
  }
1635
1866
  /** Connect the player's wallet and return their address. */
1636
1867
  async connect() {
1637
1868
  if (this.config.mock) {
1638
1869
  return "0x0000000000000000000000000000000000000001";
1639
1870
  }
1640
- return getAccount(resolveProvider(this.config.wallet));
1871
+ return getAccount(this.walletProvider());
1641
1872
  }
1642
1873
  /**
1643
1874
  * A DROP-IN payment provider for game kits that expect `{ connect, payEntry }`
@@ -1697,7 +1928,7 @@ var Playmos = class {
1697
1928
  })
1698
1929
  );
1699
1930
  }
1700
- if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
1931
+ if (this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
1701
1932
  const res = await this.http.post(
1702
1933
  "/payments",
1703
1934
  {
@@ -1735,7 +1966,7 @@ var Playmos = class {
1735
1966
  },
1736
1967
  { idempotencyKey }
1737
1968
  );
1738
- const provider = resolveProvider(this.config.wallet);
1969
+ const provider = this.walletProvider();
1739
1970
  if (this.config.gas?.mode === "player") {
1740
1971
  const from0 = await getAccount(provider);
1741
1972
  await assertEnoughGas(provider, from0);
@@ -1780,7 +2011,17 @@ var Playmos = class {
1780
2011
  })
1781
2012
  );
1782
2013
  }
1783
- if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
2014
+ const serverSettleNoWallet = this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy());
2015
+ if (!serverSettleNoWallet) {
2016
+ const pinned = typeof input.identity === "string" ? input.identity.trim() : "";
2017
+ if (!pinned) {
2018
+ throw new ConfigError(
2019
+ 'enterRound: "identity" is required when using a player wallet. Pass the same identity your game server will use for hasEntered / entry confirm. One entry per identity \u2014 use a unique value per paid attempt (pay-per-play). Silent invent caused paid-but-not-admitted failures (#374 / #466). No-wallet sandbox smoke may omit identity (server-settle derives one).',
2020
+ { field: "identity" }
2021
+ );
2022
+ }
2023
+ }
2024
+ if (serverSettleNoWallet) {
1784
2025
  const pinnedRoundKey = input.roundKey ?? input.roundId;
1785
2026
  const res = await this.http.post(
1786
2027
  "/payments",
@@ -1825,7 +2066,7 @@ var Playmos = class {
1825
2066
  },
1826
2067
  { idempotencyKey }
1827
2068
  );
1828
- const provider = resolveProvider(this.config.wallet);
2069
+ const provider = this.walletProvider();
1829
2070
  if (this.config.gas?.mode === "player") {
1830
2071
  const from0 = await getAccount(provider);
1831
2072
  await assertEnoughGas(provider, from0);
@@ -1836,12 +2077,28 @@ var Playmos = class {
1836
2077
  if (!prizePool) throw new ConfigError("No PrizePool contract address for this game (service intent + config.contracts both empty).");
1837
2078
  const roundKey = intent.clientParams?.roundKey ?? intent.clientParams?.roundId ?? input.roundKey ?? intent.payment.roundId ?? input.roundId;
1838
2079
  if (!roundKey) throw new ConfigError("No roundKey/roundId for enterRound (service clientParams missing).");
1839
- const identity = intent.clientParams?.identity ?? input.identity ?? intent.payment.identity ?? `${from}#${idempotencyKey}`;
2080
+ const identity = intent.clientParams?.identity ?? input.identity ?? intent.payment.identity;
2081
+ if (!identity || !String(identity).trim()) {
2082
+ throw new ConfigError(
2083
+ "enterRound: missing on-chain identity after create-intent (pass input.identity).",
2084
+ { field: "identity" }
2085
+ );
2086
+ }
1840
2087
  const amountUnits = this.resolveUnits(intent, amountMicro);
1841
2088
  const calls = buildEntryCalls({ usdc, prizePool, roundKey, identity, amountUnits });
1842
- const { id: callsId } = await sendCalls(provider, from, this.env.chainId, calls, this.sponsorUrl(intent));
2089
+ let callsId;
2090
+ try {
2091
+ ({ id: callsId } = await sendCalls(provider, from, this.env.chainId, calls, this.sponsorUrl(intent)));
2092
+ } catch (e) {
2093
+ throw mapAlreadyEntered(e);
2094
+ }
1843
2095
  const { txHash } = await waitForCalls(provider, callsId);
1844
- const payment = await this.settle(intent.payment.id, txHash);
2096
+ let payment;
2097
+ try {
2098
+ payment = await this.settle(intent.payment.id, txHash);
2099
+ } catch (e) {
2100
+ throw mapAlreadyEntered(e);
2101
+ }
1845
2102
  payment.identity = identity;
1846
2103
  payment.prizePoolAddress = prizePool.toLowerCase();
1847
2104
  payment.roundKey = roundKey;
@@ -2038,8 +2295,8 @@ var Playmos = class {
2038
2295
  });
2039
2296
  let raw;
2040
2297
  try {
2041
- if (await walletAvailable(this.config.wallet)) {
2042
- const provider = resolveProvider(this.config.wallet);
2298
+ if (await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
2299
+ const provider = this.walletProvider();
2043
2300
  raw = await provider.request({
2044
2301
  method: "eth_call",
2045
2302
  params: [{ to: prizePool, data }, "latest"]
@@ -2066,7 +2323,7 @@ var Playmos = class {
2066
2323
  raw = json.result;
2067
2324
  }
2068
2325
  } catch (e) {
2069
- if (e instanceof ApiError) throw e;
2326
+ if (e instanceof ApiError || e instanceof WalletTimeoutError) throw e;
2070
2327
  throw new ApiError(`Could not read withdrawable balance: ${e.message}`, {
2071
2328
  prizePool,
2072
2329
  wallet
package/dist/server.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { U as PlaymosError, W as WebhookEvent } from './errors-CdVzHuhY.cjs';
1
+ import { Y as PlaymosError, W as WebhookEvent } from './errors-BMlWHsMb.cjs';
2
2
 
3
3
  /**
4
4
  * Webhook signature verification (spec §6.2) — server-side only.
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { U as PlaymosError, W as WebhookEvent } from './errors-CdVzHuhY.js';
1
+ import { Y as PlaymosError, W as WebhookEvent } from './errors-BMlWHsMb.js';
2
2
 
3
3
  /**
4
4
  * Webhook signature verification (spec §6.2) — server-side only.
package/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { PlaymosError } from './chunk-VYR6BBHF.js';
1
+ import { PlaymosError } from './chunk-UZECDT6F.js';
2
2
  import { timingSafeEqual, createHmac } from 'crypto';
3
3
 
4
4
  var WebhookSignatureError = class extends PlaymosError {
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@playmos/sdk",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Playmos SDK — stablecoin payments for games on Base. One SDK for IAP (1%), skill-game prize-pool entries (10%, 60/30/10), and agent economies. USD in, USDC on-chain, no crypto UX for players.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
7
+ "homepage": "https://playmos-docs-public.vercel.app/docs",
8
+ "bugs": {
9
+ "url": "https://playmos-docs-public.vercel.app/docs"
10
+ },
7
11
  "repository": {
8
12
  "type": "git",
9
13
  "url": "git+https://github.com/playmos-labs/playmos-sdk.git",