@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/README.md +3 -3
- package/dist/{chunk-VYR6BBHF.js → chunk-UZECDT6F.js} +15 -1
- package/dist/{errors-CdVzHuhY.d.cts → errors-BMlWHsMb.d.cts} +72 -7
- package/dist/{errors-CdVzHuhY.d.ts → errors-BMlWHsMb.d.ts} +72 -7
- package/dist/index.cjs +317 -44
- package/dist/index.d.cts +18 -9
- package/dist/index.d.ts +18 -9
- package/dist/index.js +303 -46
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/package.json +5 -1
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,
|
|
2
|
-
export {
|
|
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
|
|
481
|
-
*
|
|
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-
|
|
2
|
-
export { ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError } from './chunk-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
|
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
|
-
|
|
1132
|
-
{ acceptStatuses: [202] }
|
|
1246
|
+
body,
|
|
1247
|
+
{ acceptStatuses: [200, 202], idempotencyKey }
|
|
1133
1248
|
);
|
|
1134
|
-
|
|
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
|
|
1139
|
-
*
|
|
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
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
)
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playmos/sdk",
|
|
3
|
-
"version": "0.3.
|
|
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",
|