@playmos/sdk 0.3.9 → 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/CHANGELOG.md +189 -0
- package/README.md +3 -0
- package/dist/chunk-35WJANW4.js +255 -0
- package/dist/{errors-BMlWHsMb.d.cts → errors-Dpeesyop.d.cts} +68 -5
- package/dist/{errors-BMlWHsMb.d.ts → errors-Dpeesyop.d.ts} +68 -5
- package/dist/index.cjs +172 -29
- package/dist/index.d.cts +47 -8
- package/dist/index.d.ts +47 -8
- package/dist/index.js +175 -200
- package/dist/server.cjs +117 -0
- package/dist/server.d.cts +34 -2
- package/dist/server.d.ts +34 -2
- package/dist/server.js +110 -2
- package/package.json +4 -8
- package/dist/chunk-UZECDT6F.js +0 -83
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 RoundCancelInput, 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, d as RoundGetResult, A as ActiveSeriesRound, e as PrizeBalance, f as WithdrawResult, g as AgentWallet, h as AgentFundResult, T as TransferResult, E as EscrowHoldInput, i as EscrowHoldResult, j as EscrowResolveResult, M as MarketplaceListInput, L as Listing, k as MarketplaceSaleResult, l as MarketplaceGetResult, m as PayInput, n as Payment, o as EnterRoundInput, p as TransferReconcile, q as WaitOptions, r as TransferInput, s as TransferConfirmOptions, V as VerifyResult, t as PayoutRule } from './errors-Dpeesyop.js';
|
|
2
|
+
export { u as ActiveForSeriesResult, v as AgentEconomyConfig, w as AlreadyEnteredError, x as ApiError, y as AuthError, z as ConfigError, B as ContractConfig, D as Eip1193Provider, G as GasConfig, F as GasMode, I as InsufficientGasError, H as InvalidAmountError, J as ListingStatus, K as MarketplaceItem, O as MarketplaceSale, Q as MissingFieldError, U as NothingToWithdrawError, X as PaymentFailedError, Y as PaymentStatus, Z as PlaymosError, _ as PlaymosErrorCode, $ as RetryOptions, a0 as RoundGetVia, a1 as RoundStatus, a2 as WalletConfig, a3 as WalletConnectionError, a4 as WalletConnector, a5 as WalletTimeoutError, a6 as WebhookEventType } from './errors-Dpeesyop.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Environment resolution + the canonical address book.
|
|
@@ -518,9 +518,22 @@ declare class Playmos {
|
|
|
518
518
|
* if still pending — never invents terminal success).
|
|
519
519
|
*/
|
|
520
520
|
cancel: (input: RoundCancelInput) => Promise<CancelRoundResult>;
|
|
521
|
+
/**
|
|
522
|
+
* Round state only (stable). Prefer {@link getWithMeta} when you need service
|
|
523
|
+
* read-path honesty (`via` / `chainReadFailed` — #497 / #501).
|
|
524
|
+
*/
|
|
521
525
|
get: (input: {
|
|
522
526
|
roundId: string;
|
|
523
527
|
}) => Promise<RoundState>;
|
|
528
|
+
/**
|
|
529
|
+
* Round state + transport meta from GET /v1/rounds/:id (#501).
|
|
530
|
+
* Does **not** attach `via` onto RoundState (Claude rejected option B — hub would
|
|
531
|
+
* silently pick it up and leak into settle/cancel paths that call get()).
|
|
532
|
+
* Under `mock: true`, meta fields are **omitted** (no eth_call; do not invent "cache").
|
|
533
|
+
*/
|
|
534
|
+
getWithMeta: (input: {
|
|
535
|
+
roundId: string;
|
|
536
|
+
}) => Promise<RoundGetResult>;
|
|
524
537
|
/**
|
|
525
538
|
* Authoritative series latch (#351) — `null` means **proven free**.
|
|
526
539
|
* `GET /v1/series/:seriesKey/active?gameId=…` — not a candidate probe.
|
|
@@ -550,6 +563,11 @@ declare class Playmos {
|
|
|
550
563
|
withdraw: (input: {
|
|
551
564
|
prizePoolAddress?: `0x${string}`;
|
|
552
565
|
roundId?: string;
|
|
566
|
+
/**
|
|
567
|
+
* Winner wallet (0x…). **Required under mock** (#495 Claude) — live path uses the
|
|
568
|
+
* connected provider. Without this, mock paid the first non-zero credit to anyone.
|
|
569
|
+
*/
|
|
570
|
+
wallet?: string;
|
|
553
571
|
/** Pre-check claimable; default true. Set false only if you already called `prize()`. */
|
|
554
572
|
checkBalance?: boolean;
|
|
555
573
|
}) => Promise<WithdrawResult>;
|
|
@@ -637,8 +655,16 @@ declare class Playmos {
|
|
|
637
655
|
/**
|
|
638
656
|
* Offline mock payments for this client instance — so `verify(id)` after
|
|
639
657
|
* `mock: true` pay/enterRound does not hit the live API (#204).
|
|
658
|
+
* Keyed by payment id for verify.
|
|
640
659
|
*/
|
|
641
660
|
private readonly mockPayments;
|
|
661
|
+
/**
|
|
662
|
+
* sdk#513 / residual B1–B2 — held idempotencyKey offline (live contract rehearsal).
|
|
663
|
+
* Keyed like service: `${gameId}:${key}` (IAP without gameId uses empty gameId).
|
|
664
|
+
* Stores terms so same key + different money throws (live IdempotencyConflict), not silent replay.
|
|
665
|
+
* Auto-minted keys still unique per call (not stored for cross-call replay).
|
|
666
|
+
*/
|
|
667
|
+
private readonly mockByIdempotencyKey;
|
|
642
668
|
constructor(config: PlaymosConfig);
|
|
643
669
|
/**
|
|
644
670
|
* Wallet timeout policy (#462 / PR #463 residual).
|
|
@@ -677,7 +703,10 @@ declare class Playmos {
|
|
|
677
703
|
identity?: string;
|
|
678
704
|
}>;
|
|
679
705
|
};
|
|
706
|
+
private mockIdemScopeKey;
|
|
680
707
|
private rememberMock;
|
|
708
|
+
/** Live-shaped conflict when held mock key is reused with different money terms (sdk#513 residual B1). */
|
|
709
|
+
private mockIdemReplayOrThrow;
|
|
681
710
|
/** External-studio IAP (1%). Returns a real confirmed Payment with a txHash. */
|
|
682
711
|
pay(input: PayInput): Promise<Payment>;
|
|
683
712
|
/** Playmos-owned skill-game entry (10%, 60/30/10 prize pool). Closes #343. */
|
|
@@ -741,12 +770,22 @@ declare class Playmos {
|
|
|
741
770
|
* the documented "confirmed" sandbox promise (TTFSC-T0 / cold-run).
|
|
742
771
|
*/
|
|
743
772
|
private waitForServerSettle;
|
|
773
|
+
/**
|
|
774
|
+
* sdk#509 — recover a payment after a lost response by the idempotency key you held.
|
|
775
|
+
* Requires secret sk_test_ (same as list). Returns null if not found.
|
|
776
|
+
*/
|
|
777
|
+
findPaymentByIdempotencyKey(idempotencyKey: string, opts?: {
|
|
778
|
+
gameId?: string;
|
|
779
|
+
}): Promise<Payment | null>;
|
|
744
780
|
/**
|
|
745
781
|
* Map a server-settled payment (from the `settle: "server"` response) into the
|
|
746
782
|
* SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
|
|
747
|
-
* pass its authoritative `status`/`txHash`/amounts straight through
|
|
748
|
-
*
|
|
749
|
-
*
|
|
783
|
+
* pass its authoritative `status`/`txHash`/amounts straight through.
|
|
784
|
+
*
|
|
785
|
+
* For entry, `Payment.split` is a **first-party 60/30/10 projection** from the
|
|
786
|
+
* request amount using SDK BPS constants (same integer math as mock) — not a
|
|
787
|
+
* chain read of pool constructor bps or post-lock payable pot (sdk#514 residual N1).
|
|
788
|
+
* Prefer `chainAmount` / on-chain `getRound` for money truth.
|
|
750
789
|
*/
|
|
751
790
|
private mapServerPayment;
|
|
752
791
|
/** Prefer the service's authoritative micro-USDC amount; fall back to the
|
|
@@ -820,8 +859,8 @@ declare function previewPoolSplit(amount: string): {
|
|
|
820
859
|
*/
|
|
821
860
|
|
|
822
861
|
declare class PayoutError extends Error {
|
|
823
|
-
code:
|
|
824
|
-
constructor(message: string);
|
|
862
|
+
code: string;
|
|
863
|
+
constructor(message: string, code?: string);
|
|
825
864
|
}
|
|
826
865
|
/**
|
|
827
866
|
* Apply the studio's payout rule to a payable pool and ranking (best-first).
|
|
@@ -890,4 +929,4 @@ declare function ulid(seedTime?: number): string;
|
|
|
890
929
|
/** `${prefix}_${ulid()}` — e.g. `pay_01J…`, `entry_01J…`, `idem_01J…`. */
|
|
891
930
|
declare function prefixedId(prefix: string): string;
|
|
892
931
|
|
|
893
|
-
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 };
|
|
932
|
+
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, RoundGetResult, 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,6 +1,6 @@
|
|
|
1
|
-
import { AuthError, InvalidAmountError, ConfigError, MissingFieldError, NothingToWithdrawError, WalletTimeoutError, PaymentFailedError, ApiError,
|
|
2
|
-
export { AlreadyEnteredError, ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError, WalletTimeoutError } from './chunk-
|
|
3
|
-
import { encodeFunctionData, decodeFunctionResult
|
|
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
|
|
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
|
-
|
|
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) {
|
|
@@ -1530,19 +1363,34 @@ var Playmos = class {
|
|
|
1530
1363
|
}
|
|
1531
1364
|
return { ...result, txHash: result.txHash ?? firstTx ?? null };
|
|
1532
1365
|
},
|
|
1366
|
+
/**
|
|
1367
|
+
* Round state only (stable). Prefer {@link getWithMeta} when you need service
|
|
1368
|
+
* read-path honesty (`via` / `chainReadFailed` — #497 / #501).
|
|
1369
|
+
*/
|
|
1533
1370
|
get: async (input) => {
|
|
1371
|
+
const meta = await this.rounds.getWithMeta(input);
|
|
1372
|
+
return meta.round;
|
|
1373
|
+
},
|
|
1374
|
+
/**
|
|
1375
|
+
* Round state + transport meta from GET /v1/rounds/:id (#501).
|
|
1376
|
+
* Does **not** attach `via` onto RoundState (Claude rejected option B — hub would
|
|
1377
|
+
* silently pick it up and leak into settle/cancel paths that call get()).
|
|
1378
|
+
* Under `mock: true`, meta fields are **omitted** (no eth_call; do not invent "cache").
|
|
1379
|
+
*/
|
|
1380
|
+
getWithMeta: async (input) => {
|
|
1534
1381
|
requireField(input?.roundId, "roundId");
|
|
1535
1382
|
if (this.config.mock) {
|
|
1536
1383
|
const existing = this.mockRounds.get(input.roundId);
|
|
1537
1384
|
if (!existing) {
|
|
1538
1385
|
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
1539
1386
|
}
|
|
1540
|
-
return existing;
|
|
1387
|
+
return { round: existing };
|
|
1541
1388
|
}
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
);
|
|
1545
|
-
|
|
1389
|
+
const body = await this.http.get(`/rounds/${encodeURIComponent(input.roundId)}`);
|
|
1390
|
+
const out = { round: body.round };
|
|
1391
|
+
if (body.via != null) out.via = body.via;
|
|
1392
|
+
if (body.chainReadFailed === true) out.chainReadFailed = true;
|
|
1393
|
+
return out;
|
|
1546
1394
|
},
|
|
1547
1395
|
/**
|
|
1548
1396
|
* Authoritative series latch (#351) — `null` means **proven free**.
|
|
@@ -1647,19 +1495,26 @@ var Playmos = class {
|
|
|
1647
1495
|
*/
|
|
1648
1496
|
withdraw: async (input) => {
|
|
1649
1497
|
if (this.config.mock) {
|
|
1650
|
-
let amountMicro2 = 0n;
|
|
1651
1498
|
let prizePool2 = input.prizePoolAddress ?? "0x0000000000000000000000000000000000000001";
|
|
1652
1499
|
if (input.roundId) {
|
|
1653
1500
|
const existing = this.mockRounds.get(input.roundId);
|
|
1654
1501
|
if (existing?.prizePoolAddress) prizePool2 = existing.prizePoolAddress;
|
|
1655
|
-
for (const [k, v] of this.mockWithdrawable) {
|
|
1656
|
-
if (k.startsWith(`${input.roundId}:`) && v > 0n) {
|
|
1657
|
-
amountMicro2 = v;
|
|
1658
|
-
this.mockWithdrawable.set(k, 0n);
|
|
1659
|
-
break;
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
1502
|
}
|
|
1503
|
+
if (!input.wallet || !ADDRESS_RE3.test(input.wallet)) {
|
|
1504
|
+
throw new ConfigError(
|
|
1505
|
+
"mock rounds.withdraw requires wallet (0x\u2026) \u2014 live uses the connected provider; without wallet mock would pay the first non-zero credit to the wrong player (#495)"
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
const wallet = input.wallet.toLowerCase();
|
|
1509
|
+
if (!input.roundId) {
|
|
1510
|
+
throw new ConfigError("mock rounds.withdraw requires roundId to locate claimable credits");
|
|
1511
|
+
}
|
|
1512
|
+
const key = `${input.roundId}:${wallet}`;
|
|
1513
|
+
const amountMicro2 = this.mockWithdrawable.get(key) ?? 0n;
|
|
1514
|
+
if (amountMicro2 === 0n) {
|
|
1515
|
+
throw new NothingToWithdrawError({ prizePoolAddress: prizePool2, wallet });
|
|
1516
|
+
}
|
|
1517
|
+
this.mockWithdrawable.set(key, 0n);
|
|
1663
1518
|
return {
|
|
1664
1519
|
prizePoolAddress: prizePool2,
|
|
1665
1520
|
amount: formatMicroToUsd(amountMicro2),
|
|
@@ -1931,8 +1786,16 @@ var Playmos = class {
|
|
|
1931
1786
|
/**
|
|
1932
1787
|
* Offline mock payments for this client instance — so `verify(id)` after
|
|
1933
1788
|
* `mock: true` pay/enterRound does not hit the live API (#204).
|
|
1789
|
+
* Keyed by payment id for verify.
|
|
1934
1790
|
*/
|
|
1935
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();
|
|
1936
1799
|
/**
|
|
1937
1800
|
* `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
|
|
1938
1801
|
* `get` reconciles once against chain truth; `wait` polls it to a terminal state
|
|
@@ -2117,18 +1980,62 @@ var Playmos = class {
|
|
|
2117
1980
|
}
|
|
2118
1981
|
};
|
|
2119
1982
|
}
|
|
2120
|
-
|
|
2121
|
-
|
|
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
|
+
}
|
|
2122
1996
|
return payment;
|
|
2123
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
|
+
}
|
|
2124
2017
|
/** External-studio IAP (1%). Returns a real confirmed Payment with a txHash. */
|
|
2125
2018
|
async pay(input) {
|
|
2126
2019
|
const amountMicro = validateAmount(input.amount);
|
|
2127
2020
|
requireField(input.sku, "sku");
|
|
2128
2021
|
requireField(input.playerId, "playerId");
|
|
2129
2022
|
const metadata = validateMetadata(input.metadata);
|
|
2130
|
-
const
|
|
2023
|
+
const heldIdem = typeof input.idempotencyKey === "string" && input.idempotencyKey.trim() ? input.idempotencyKey.trim() : void 0;
|
|
2024
|
+
const idempotencyKey = heldIdem ?? prefixedId("idem");
|
|
2131
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
|
+
}
|
|
2132
2039
|
return this.rememberMock(
|
|
2133
2040
|
mockIapPayment({
|
|
2134
2041
|
amountMicro,
|
|
@@ -2137,7 +2044,8 @@ var Playmos = class {
|
|
|
2137
2044
|
playerId: input.playerId,
|
|
2138
2045
|
chain: this.env.network,
|
|
2139
2046
|
metadata
|
|
2140
|
-
})
|
|
2047
|
+
}),
|
|
2048
|
+
heldIdem ? { key: heldIdem, terms } : void 0
|
|
2141
2049
|
);
|
|
2142
2050
|
}
|
|
2143
2051
|
if (this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
|
|
@@ -2159,7 +2067,13 @@ var Playmos = class {
|
|
|
2159
2067
|
);
|
|
2160
2068
|
let payment = this.mapServerPayment(res.payment, "iap", amountMicro);
|
|
2161
2069
|
if (payment.status === "pending" || payment.status === "created") {
|
|
2162
|
-
payment = await this.waitForServerSettle(
|
|
2070
|
+
payment = await this.waitForServerSettle(
|
|
2071
|
+
payment.id,
|
|
2072
|
+
"iap",
|
|
2073
|
+
amountMicro,
|
|
2074
|
+
input.settleTimeoutMs ?? 6e4,
|
|
2075
|
+
input.signal
|
|
2076
|
+
);
|
|
2163
2077
|
}
|
|
2164
2078
|
return payment;
|
|
2165
2079
|
}
|
|
@@ -2204,8 +2118,22 @@ var Playmos = class {
|
|
|
2204
2118
|
requireField(input.roundId, "roundId");
|
|
2205
2119
|
requireField(input.playerId, "playerId");
|
|
2206
2120
|
const metadata = validateMetadata(input.metadata);
|
|
2207
|
-
const
|
|
2121
|
+
const heldIdem = typeof input.idempotencyKey === "string" && input.idempotencyKey.trim() ? input.idempotencyKey.trim() : void 0;
|
|
2122
|
+
const idempotencyKey = heldIdem ?? prefixedId("idem");
|
|
2208
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
|
+
}
|
|
2209
2137
|
this.mockAccumulateEntry(input.roundId, amountMicro);
|
|
2210
2138
|
return this.rememberMock(
|
|
2211
2139
|
mockEntryPayment({
|
|
@@ -2221,7 +2149,8 @@ var Playmos = class {
|
|
|
2221
2149
|
// B2 pins — must survive mock path for hub hasEntered parity (#204).
|
|
2222
2150
|
roundKey: input.roundKey,
|
|
2223
2151
|
identity: input.identity
|
|
2224
|
-
})
|
|
2152
|
+
}),
|
|
2153
|
+
heldIdem ? { key: heldIdem, terms } : void 0
|
|
2225
2154
|
);
|
|
2226
2155
|
}
|
|
2227
2156
|
const serverSettleNoWallet = this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy());
|
|
@@ -2254,7 +2183,13 @@ var Playmos = class {
|
|
|
2254
2183
|
);
|
|
2255
2184
|
let payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
|
|
2256
2185
|
if (payment2.status === "pending" || payment2.status === "created") {
|
|
2257
|
-
payment2 = await this.waitForServerSettle(
|
|
2186
|
+
payment2 = await this.waitForServerSettle(
|
|
2187
|
+
payment2.id,
|
|
2188
|
+
"entry",
|
|
2189
|
+
amountMicro,
|
|
2190
|
+
input.settleTimeoutMs ?? 6e4,
|
|
2191
|
+
input.signal
|
|
2192
|
+
);
|
|
2258
2193
|
}
|
|
2259
2194
|
if (input.identity) payment2.identity = input.identity;
|
|
2260
2195
|
const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
@@ -2437,10 +2372,16 @@ var Playmos = class {
|
|
|
2437
2372
|
* until chain verify catches PaymentSettled (RPC lag) — poll so pay() matches
|
|
2438
2373
|
* the documented "confirmed" sandbox promise (TTFSC-T0 / cold-run).
|
|
2439
2374
|
*/
|
|
2440
|
-
async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4) {
|
|
2375
|
+
async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4, signal) {
|
|
2441
2376
|
const start = Date.now();
|
|
2442
2377
|
let delayMs = 400;
|
|
2443
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
|
+
}
|
|
2444
2385
|
const raw = await this.http.get(
|
|
2445
2386
|
`/payments/${encodeURIComponent(paymentId)}`
|
|
2446
2387
|
);
|
|
@@ -2455,12 +2396,46 @@ var Playmos = class {
|
|
|
2455
2396
|
{ paymentId, timeout: true, asyncSettle: true }
|
|
2456
2397
|
);
|
|
2457
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
|
+
}
|
|
2458
2430
|
/**
|
|
2459
2431
|
* Map a server-settled payment (from the `settle: "server"` response) into the
|
|
2460
2432
|
* SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
|
|
2461
|
-
* pass its authoritative `status`/`txHash`/amounts straight through
|
|
2462
|
-
*
|
|
2463
|
-
*
|
|
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.
|
|
2464
2439
|
*/
|
|
2465
2440
|
mapServerPayment(p, kind, amountMicro) {
|
|
2466
2441
|
const payment = {
|