@playmos/sdk 0.3.10 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +197 -0
- package/README.md +6 -2
- package/dist/chunk-2CW4U5YB.js +445 -0
- package/dist/{errors-Chizbb96.d.cts → errors-CYKgt2VU.d.cts} +112 -6
- package/dist/{errors-Chizbb96.d.ts → errors-CYKgt2VU.d.ts} +112 -6
- package/dist/index.cjs +1412 -24
- package/dist/index.d.cts +688 -21
- package/dist/index.d.ts +688 -21
- package/dist/index.js +1202 -189
- package/dist/server.cjs +117 -0
- package/dist/server.d.cts +26 -2
- package/dist/server.d.ts +26 -2
- package/dist/server.js +110 -2
- package/package.json +7 -11
- package/dist/chunk-UZECDT6F.js +0 -83
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AuthError, InvalidAmountError, ConfigError, MissingFieldError, NothingToWithdrawError, WalletTimeoutError,
|
|
2
|
-
export { AlreadyEnteredError, ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError, WalletTimeoutError } from './chunk-
|
|
3
|
-
import { encodeFunctionData,
|
|
1
|
+
import { AuthError, InvalidAmountError, epochPrizePoolEnterAbi, seriesToBytes32, identityToBytes32, encodeApprove, epochPrizePoolRefundAbi, ConfigError, MissingFieldError, buildEpochExecuteSettlementCall, PaymentFailedError, epochPrizePoolViewAbi, ApiError, assertEnoughGas, sendCalls, waitForCalls, NothingToWithdrawError, buildWithdrawCall, WalletTimeoutError, buildIapCalls, buildEntryCalls, prizePoolAbi, WalletConnectionError, AlreadyEnteredError } from './chunk-2CW4U5YB.js';
|
|
2
|
+
export { AlreadyEnteredError, ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError, WalletTimeoutError, buildEpochExecuteSettlementCall, identityToBytes32, seriesToBytes32 } from './chunk-2CW4U5YB.js';
|
|
3
|
+
import { encodeFunctionData, encodeAbiParameters, 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) {
|
|
@@ -580,173 +581,36 @@ async function getAccount(provider) {
|
|
|
580
581
|
});
|
|
581
582
|
}
|
|
582
583
|
}
|
|
583
|
-
|
|
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) {
|
|
584
|
+
function buildEpochEntryCalls(args) {
|
|
733
585
|
const enterData = encodeFunctionData({
|
|
734
|
-
abi:
|
|
586
|
+
abi: epochPrizePoolEnterAbi,
|
|
735
587
|
functionName: "enter",
|
|
736
|
-
args: [
|
|
588
|
+
args: [seriesToBytes32(args.series), identityToBytes32(args.identity)]
|
|
737
589
|
});
|
|
738
590
|
return [
|
|
739
|
-
{ to: args.usdc, data: encodeApprove(args.
|
|
740
|
-
{ to: args.
|
|
591
|
+
{ to: args.usdc, data: encodeApprove(args.epochPrizePool, args.entryMicro) },
|
|
592
|
+
{ to: args.epochPrizePool, data: enterData }
|
|
741
593
|
];
|
|
742
594
|
}
|
|
743
|
-
function
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
595
|
+
function buildEpochClaimRefundCall(args) {
|
|
596
|
+
return {
|
|
597
|
+
to: args.epochPrizePool,
|
|
598
|
+
data: encodeFunctionData({
|
|
599
|
+
abi: epochPrizePoolRefundAbi,
|
|
600
|
+
functionName: "claimRefund",
|
|
601
|
+
args: [seriesToBytes32(args.series), args.epochId, args.payer]
|
|
602
|
+
})
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function buildEpochWithdrawCall(epochPrizePool) {
|
|
606
|
+
return {
|
|
607
|
+
to: epochPrizePool,
|
|
608
|
+
data: encodeFunctionData({
|
|
609
|
+
abi: epochPrizePoolRefundAbi,
|
|
610
|
+
functionName: "withdraw",
|
|
611
|
+
args: []
|
|
612
|
+
})
|
|
613
|
+
};
|
|
750
614
|
}
|
|
751
615
|
|
|
752
616
|
// src/mock.ts
|
|
@@ -809,13 +673,893 @@ function mockVerifyResult(payment) {
|
|
|
809
673
|
return { ...payment, mock: true };
|
|
810
674
|
}
|
|
811
675
|
|
|
812
|
-
// src/
|
|
676
|
+
// src/epochs.ts
|
|
677
|
+
var PLAYMOS_FEE_SINK_DEFAULT = "0x1b8031e20ed96131a849a52290b4d640f286998d";
|
|
678
|
+
var EPOCH_POOL_CONSTRUCTOR_TYPES = [
|
|
679
|
+
{ type: "address" },
|
|
680
|
+
{ type: "address" },
|
|
681
|
+
{ type: "address" },
|
|
682
|
+
{ type: "address" },
|
|
683
|
+
{ type: "uint256" }
|
|
684
|
+
];
|
|
685
|
+
function encodeEpochPoolConstructorArgs(args) {
|
|
686
|
+
return encodeAbiParameters(EPOCH_POOL_CONSTRUCTOR_TYPES, [
|
|
687
|
+
args.token,
|
|
688
|
+
args.admin,
|
|
689
|
+
args.operator,
|
|
690
|
+
args.feeSink,
|
|
691
|
+
args.refundTimeout
|
|
692
|
+
]);
|
|
693
|
+
}
|
|
694
|
+
function derivedEpochId(genesis, epochDuration, at) {
|
|
695
|
+
if (epochDuration === 0n) {
|
|
696
|
+
throw new ConfigError("epochDuration must be > 0");
|
|
697
|
+
}
|
|
698
|
+
if (at < genesis) {
|
|
699
|
+
throw new ConfigError("epoch has not started (at < genesis)");
|
|
700
|
+
}
|
|
701
|
+
return (at - genesis) / epochDuration;
|
|
702
|
+
}
|
|
703
|
+
function epochPayableMicro(incomingSeed, pool) {
|
|
704
|
+
return incomingSeed + pool;
|
|
705
|
+
}
|
|
706
|
+
function mockPodiumPayable(winners) {
|
|
707
|
+
if (winners.length === 0) return void 0;
|
|
708
|
+
let total = 0n;
|
|
709
|
+
for (const w of winners) {
|
|
710
|
+
if (typeof w === "string") return void 0;
|
|
711
|
+
if (w.amountMicro != null && w.amountMicro !== "") {
|
|
712
|
+
try {
|
|
713
|
+
total += BigInt(w.amountMicro);
|
|
714
|
+
} catch {
|
|
715
|
+
return void 0;
|
|
716
|
+
}
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (w.amount) {
|
|
720
|
+
try {
|
|
721
|
+
total += parseUsdToMicro(w.amount);
|
|
722
|
+
} catch {
|
|
723
|
+
return void 0;
|
|
724
|
+
}
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
return void 0;
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
payable: formatMicroToUsd(total),
|
|
731
|
+
payableMicro: total.toString()
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function mockWinnerMicro(w) {
|
|
735
|
+
if (typeof w === "string") return void 0;
|
|
736
|
+
if (w.amountMicro != null && w.amountMicro !== "" && /^\d+$/.test(w.amountMicro)) {
|
|
737
|
+
return BigInt(w.amountMicro);
|
|
738
|
+
}
|
|
739
|
+
if (w.amount) {
|
|
740
|
+
try {
|
|
741
|
+
return parseUsdToMicro(w.amount);
|
|
742
|
+
} catch {
|
|
743
|
+
return void 0;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return void 0;
|
|
747
|
+
}
|
|
748
|
+
function mockWinnerAmount(w) {
|
|
749
|
+
const micro = mockWinnerMicro(w);
|
|
750
|
+
if (micro === void 0) return {};
|
|
751
|
+
return { amount: formatMicroToUsd(micro), amountMicro: micro.toString() };
|
|
752
|
+
}
|
|
753
|
+
var TERMINALS = ["none", "settled", "refunded", "rolled"];
|
|
754
|
+
function terminalFromChain(raw) {
|
|
755
|
+
return TERMINALS[raw] ?? "none";
|
|
756
|
+
}
|
|
757
|
+
function requireSeries(series) {
|
|
758
|
+
if (typeof series !== "string" || series.trim() === "") {
|
|
759
|
+
throw new MissingFieldError("series");
|
|
760
|
+
}
|
|
761
|
+
return series.trim();
|
|
762
|
+
}
|
|
763
|
+
function requireIdentity(identity) {
|
|
764
|
+
if (typeof identity !== "string" || identity.trim() === "") {
|
|
765
|
+
throw new MissingFieldError("identity");
|
|
766
|
+
}
|
|
767
|
+
return identity.trim();
|
|
768
|
+
}
|
|
769
|
+
function assertPinParity(label, local, fromService) {
|
|
770
|
+
if (!fromService) {
|
|
771
|
+
throw new ConfigError(
|
|
772
|
+
`epochs.enter: the service did not return ${label}Bytes32 \u2014 cannot prove the on-chain id matches before paying.`,
|
|
773
|
+
{ field: `${label}Bytes32` }
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
if (local.toLowerCase() !== fromService.toLowerCase()) {
|
|
777
|
+
throw new ConfigError(
|
|
778
|
+
`epochs.enter: ${label} derives differently in the SDK and the service (sdk=${local}, service=${fromService}). Refusing to enter \u2014 a paid entry under a mismatched id is never scored. See sdk#633.`,
|
|
779
|
+
{ field: `${label}Bytes32` }
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function epochPathId(epochId) {
|
|
784
|
+
if (epochId === void 0 || epochId === null || epochId === "") return "current";
|
|
785
|
+
const s = String(epochId).trim();
|
|
786
|
+
if (s === "current") return "current";
|
|
787
|
+
if (!/^\d+$/.test(s)) {
|
|
788
|
+
throw new ConfigError(`epochId must be a non-negative integer, got: ${JSON.stringify(epochId)}`);
|
|
789
|
+
}
|
|
790
|
+
return s;
|
|
791
|
+
}
|
|
792
|
+
function qs(params) {
|
|
793
|
+
const u = new URLSearchParams();
|
|
794
|
+
for (const [k, v] of Object.entries(params)) {
|
|
795
|
+
if (v) u.set(k, v);
|
|
796
|
+
}
|
|
797
|
+
const s = u.toString();
|
|
798
|
+
return s ? `?${s}` : "";
|
|
799
|
+
}
|
|
800
|
+
function pictureFromMicros(args) {
|
|
801
|
+
const payableMicro = epochPayableMicro(args.incomingSeedMicro, args.poolMicro);
|
|
802
|
+
return {
|
|
803
|
+
series: args.series,
|
|
804
|
+
epochId: args.epochId,
|
|
805
|
+
pool: formatMicroToUsd(args.poolMicro),
|
|
806
|
+
incomingSeed: formatMicroToUsd(args.incomingSeedMicro),
|
|
807
|
+
outgoingSeed: formatMicroToUsd(args.outgoingSeedMicro),
|
|
808
|
+
payable: formatMicroToUsd(payableMicro),
|
|
809
|
+
poolMicro: args.poolMicro.toString(),
|
|
810
|
+
incomingSeedMicro: args.incomingSeedMicro.toString(),
|
|
811
|
+
outgoingSeedMicro: args.outgoingSeedMicro.toString(),
|
|
812
|
+
payableMicro: payableMicro.toString(),
|
|
813
|
+
via: args.via,
|
|
814
|
+
epochPrizePool: args.epochPrizePool,
|
|
815
|
+
...args.mock ? { mock: true } : {}
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
function toBig(value, fallback) {
|
|
819
|
+
if (value === void 0) return fallback;
|
|
820
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) return BigInt(Math.trunc(value));
|
|
821
|
+
if (typeof value === "string" && /^\d+$/.test(value.trim())) return BigInt(value.trim());
|
|
822
|
+
throw new ConfigError(`expected a non-negative integer, got: ${JSON.stringify(value)}`);
|
|
823
|
+
}
|
|
813
824
|
var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
|
|
825
|
+
function requireAddr(raw, field) {
|
|
826
|
+
if (typeof raw !== "string" || !ADDRESS_RE2.test(raw.trim())) {
|
|
827
|
+
throw new MissingFieldError(field);
|
|
828
|
+
}
|
|
829
|
+
return raw.trim().toLowerCase();
|
|
830
|
+
}
|
|
831
|
+
function requireAddrKeepCase(raw, field) {
|
|
832
|
+
if (typeof raw !== "string" || !ADDRESS_RE2.test(raw.trim())) {
|
|
833
|
+
throw new MissingFieldError(field);
|
|
834
|
+
}
|
|
835
|
+
return raw.trim();
|
|
836
|
+
}
|
|
837
|
+
function requireEpochPool(input, cfg) {
|
|
838
|
+
const raw = input?.epochPrizePool ?? cfg.contracts?.epochPrizePool;
|
|
839
|
+
if (!raw) {
|
|
840
|
+
throw new ConfigError(
|
|
841
|
+
"epochs refund rail needs contracts.epochPrizePool (or pass epochPrizePool) \u2014 this is EpochPrizePool, not PrizePool",
|
|
842
|
+
{ field: "epochPrizePool" }
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
return requireAddr(raw, "epochPrizePool");
|
|
846
|
+
}
|
|
847
|
+
function requirePastEpochId(epochId) {
|
|
848
|
+
const id = epochPathId(epochId);
|
|
849
|
+
if (id === "current") {
|
|
850
|
+
throw new ConfigError("epochs refund rail requires a past epochId \u2014 Current is still taking entries");
|
|
851
|
+
}
|
|
852
|
+
return id;
|
|
853
|
+
}
|
|
854
|
+
function walletStatus(status) {
|
|
855
|
+
if (status === "CONFIRMED") return "confirmed";
|
|
856
|
+
if (status === "FAILED") return "failed";
|
|
857
|
+
return "pending";
|
|
858
|
+
}
|
|
859
|
+
function sameAddr(a, b) {
|
|
860
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
861
|
+
}
|
|
862
|
+
function prepareStudioPoolLocal(input, playmosFeeSink, token, playmosAddresses = []) {
|
|
863
|
+
const studio = requireAddr(input.studioWallet, "studioWallet");
|
|
864
|
+
const sink = playmosFeeSink.toLowerCase();
|
|
865
|
+
const forbidden = [sink, ...playmosAddresses.map((a) => a.toLowerCase())];
|
|
866
|
+
if (forbidden.some((a) => sameAddr(a, studio))) {
|
|
867
|
+
throw new ConfigError("studioWallet must not be a Playmos address (Playmos never holds studio keys)");
|
|
868
|
+
}
|
|
869
|
+
if (input.feeSink && !sameAddr(input.feeSink, sink)) {
|
|
870
|
+
throw new ConfigError("feeSink must equal the Playmos sink; refused before broadcast");
|
|
871
|
+
}
|
|
872
|
+
if (input.admin && !sameAddr(input.admin, studio)) {
|
|
873
|
+
throw new ConfigError("admin must be the studio wallet from block one");
|
|
874
|
+
}
|
|
875
|
+
if (input.operator && !sameAddr(input.operator, studio)) {
|
|
876
|
+
throw new ConfigError("operator must be the studio wallet from block one");
|
|
877
|
+
}
|
|
878
|
+
if (input.admin && forbidden.some((a) => sameAddr(a, input.admin))) {
|
|
879
|
+
throw new ConfigError("Playmos must not be ADMIN_ROLE");
|
|
880
|
+
}
|
|
881
|
+
if (input.operator && forbidden.some((a) => sameAddr(a, input.operator))) {
|
|
882
|
+
throw new ConfigError("Playmos must not be OPERATOR_ROLE");
|
|
883
|
+
}
|
|
884
|
+
const refundTimeout = toBig(input.refundTimeout, 604800n);
|
|
885
|
+
if (refundTimeout < 3600n || refundTimeout > 7776000n) {
|
|
886
|
+
throw new ConfigError("refundTimeout must be between 3600 (1h) and 7776000 (90d) seconds");
|
|
887
|
+
}
|
|
888
|
+
return {
|
|
889
|
+
token: token.toLowerCase(),
|
|
890
|
+
admin: studio,
|
|
891
|
+
operator: studio,
|
|
892
|
+
feeSink: sink,
|
|
893
|
+
refundTimeout: refundTimeout.toString(),
|
|
894
|
+
constructorArgs: encodeEpochPoolConstructorArgs({
|
|
895
|
+
token: token.toLowerCase(),
|
|
896
|
+
admin: studio,
|
|
897
|
+
operator: studio,
|
|
898
|
+
feeSink: sink,
|
|
899
|
+
refundTimeout
|
|
900
|
+
}),
|
|
901
|
+
constructor: "EpochPrizePool",
|
|
902
|
+
studioHoldsAdmin: true,
|
|
903
|
+
studioHoldsOperator: true,
|
|
904
|
+
playmosIsAdmin: false,
|
|
905
|
+
playmosIsOperator: false,
|
|
906
|
+
via: "mock",
|
|
907
|
+
mock: true
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
function epochPoolProofMessage(terms) {
|
|
911
|
+
return [
|
|
912
|
+
"Playmos EpochPrizePool registration",
|
|
913
|
+
`studio: ${terms.studioId}`,
|
|
914
|
+
`pool: ${terms.poolAddress.toLowerCase()}`,
|
|
915
|
+
`wallet: ${terms.studioWallet.toLowerCase()}`,
|
|
916
|
+
`chainId: ${terms.chainId}`
|
|
917
|
+
].join("\n");
|
|
918
|
+
}
|
|
919
|
+
async function readWithdrawableMicro(deps, pool) {
|
|
920
|
+
const read = deps.readView;
|
|
921
|
+
const account = deps.walletAddress;
|
|
922
|
+
if (!read || !account) return null;
|
|
923
|
+
try {
|
|
924
|
+
const raw = await read(
|
|
925
|
+
pool,
|
|
926
|
+
encodeFunctionData({
|
|
927
|
+
abi: epochPrizePoolViewAbi,
|
|
928
|
+
functionName: "withdrawable",
|
|
929
|
+
args: [await account()]
|
|
930
|
+
})
|
|
931
|
+
);
|
|
932
|
+
return decodeFunctionResult({
|
|
933
|
+
abi: epochPrizePoolViewAbi,
|
|
934
|
+
functionName: "withdrawable",
|
|
935
|
+
data: raw
|
|
936
|
+
});
|
|
937
|
+
} catch {
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
function settlementTypedData(input) {
|
|
942
|
+
const pool = requireAddr(input.pool, "pool");
|
|
943
|
+
const series = requireSeries(input.series);
|
|
944
|
+
const epochId = requirePastEpochId(input.epochId);
|
|
945
|
+
if (!Array.isArray(input.winners) || !Array.isArray(input.amounts)) {
|
|
946
|
+
throw new MissingFieldError("winners");
|
|
947
|
+
}
|
|
948
|
+
if (input.winners.length !== input.amounts.length) {
|
|
949
|
+
throw new ConfigError("winners and amounts must be the same length");
|
|
950
|
+
}
|
|
951
|
+
return {
|
|
952
|
+
domain: {
|
|
953
|
+
name: "EpochPrizePool",
|
|
954
|
+
version: "1",
|
|
955
|
+
chainId: input.chainId,
|
|
956
|
+
verifyingContract: pool
|
|
957
|
+
},
|
|
958
|
+
types: {
|
|
959
|
+
Settlement: [
|
|
960
|
+
{ name: "series", type: "bytes32" },
|
|
961
|
+
{ name: "epochId", type: "uint256" },
|
|
962
|
+
{ name: "winners", type: "address[]" },
|
|
963
|
+
{ name: "amounts", type: "uint256[]" }
|
|
964
|
+
]
|
|
965
|
+
},
|
|
966
|
+
primaryType: "Settlement",
|
|
967
|
+
message: {
|
|
968
|
+
series: seriesToBytes32(series),
|
|
969
|
+
epochId: BigInt(epochId),
|
|
970
|
+
winners: input.winners.map((w, i) => requireAddrKeepCase(w, `winners[${i}]`)),
|
|
971
|
+
amounts: input.amounts.map((a, i) => parseMicroString(a, `amounts[${i}]`))
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
function parseMicroString(raw, field) {
|
|
976
|
+
if (typeof raw !== "string" || !/^(0|[1-9]\d*)$/.test(raw)) {
|
|
977
|
+
throw new ConfigError(`${field} must be an integer micro-USDC string`, { field });
|
|
978
|
+
}
|
|
979
|
+
return BigInt(raw);
|
|
980
|
+
}
|
|
981
|
+
async function readEpochTerminal(deps, pool, series, epochId) {
|
|
982
|
+
const read = deps.readView;
|
|
983
|
+
if (!read) return null;
|
|
984
|
+
try {
|
|
985
|
+
const raw = await read(
|
|
986
|
+
pool,
|
|
987
|
+
encodeFunctionData({
|
|
988
|
+
abi: epochPrizePoolViewAbi,
|
|
989
|
+
functionName: "getEpoch",
|
|
990
|
+
args: [seriesToBytes32(series), BigInt(epochId)]
|
|
991
|
+
})
|
|
992
|
+
);
|
|
993
|
+
const decoded = decodeFunctionResult({
|
|
994
|
+
abi: epochPrizePoolViewAbi,
|
|
995
|
+
functionName: "getEpoch",
|
|
996
|
+
data: raw
|
|
997
|
+
});
|
|
998
|
+
const termRaw = decoded && typeof decoded === "object" && "terminal" in decoded ? Number(decoded.terminal) : Number(decoded[4]);
|
|
999
|
+
return terminalFromChain(termRaw);
|
|
1000
|
+
} catch {
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
function createEpochsApi(deps) {
|
|
1005
|
+
return {
|
|
1006
|
+
async currentId(input) {
|
|
1007
|
+
const series = requireSeries(input?.series);
|
|
1008
|
+
const cfg = deps.config();
|
|
1009
|
+
if (cfg.mock) {
|
|
1010
|
+
const genesis = toBig(input.genesis, 0n);
|
|
1011
|
+
const duration = toBig(input.epochDuration, 1n);
|
|
1012
|
+
const at = toBig(input.at, BigInt(Math.floor(Date.now() / 1e3)));
|
|
1013
|
+
const epochId = derivedEpochId(genesis, duration, at);
|
|
1014
|
+
return {
|
|
1015
|
+
series,
|
|
1016
|
+
epochId: epochId.toString(),
|
|
1017
|
+
via: "mock",
|
|
1018
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
|
|
1019
|
+
mock: true
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
const path = `/epochs/current${qs({
|
|
1023
|
+
series,
|
|
1024
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
|
|
1025
|
+
})}`;
|
|
1026
|
+
return deps.http().get(path);
|
|
1027
|
+
},
|
|
1028
|
+
async prize(input) {
|
|
1029
|
+
const series = requireSeries(input?.series);
|
|
1030
|
+
const cfg = deps.config();
|
|
1031
|
+
const epochId = epochPathId(input?.epochId);
|
|
1032
|
+
if (cfg.mock) {
|
|
1033
|
+
return pictureFromMicros({
|
|
1034
|
+
series,
|
|
1035
|
+
epochId: epochId === "current" ? "0" : epochId,
|
|
1036
|
+
poolMicro: 0n,
|
|
1037
|
+
incomingSeedMicro: 0n,
|
|
1038
|
+
outgoingSeedMicro: 0n,
|
|
1039
|
+
via: "mock",
|
|
1040
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
|
|
1041
|
+
mock: true
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
const path = `/epochs/${encodeURIComponent(epochId)}/prize${qs({
|
|
1045
|
+
series,
|
|
1046
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
|
|
1047
|
+
})}`;
|
|
1048
|
+
const res = await deps.http().get(path);
|
|
1049
|
+
return res.prize;
|
|
1050
|
+
},
|
|
1051
|
+
async get(input) {
|
|
1052
|
+
const series = requireSeries(input?.series);
|
|
1053
|
+
const cfg = deps.config();
|
|
1054
|
+
const epochId = epochPathId(input?.epochId);
|
|
1055
|
+
if (cfg.mock) {
|
|
1056
|
+
const prize = pictureFromMicros({
|
|
1057
|
+
series,
|
|
1058
|
+
epochId: epochId === "current" ? "0" : epochId,
|
|
1059
|
+
poolMicro: 0n,
|
|
1060
|
+
incomingSeedMicro: 0n,
|
|
1061
|
+
outgoingSeedMicro: 0n,
|
|
1062
|
+
via: "mock",
|
|
1063
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
|
|
1064
|
+
mock: true
|
|
1065
|
+
});
|
|
1066
|
+
return {
|
|
1067
|
+
...prize,
|
|
1068
|
+
entryCount: "0",
|
|
1069
|
+
terminal: "none"
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
const path = `/epochs/${encodeURIComponent(epochId)}${qs({
|
|
1073
|
+
series,
|
|
1074
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
|
|
1075
|
+
})}`;
|
|
1076
|
+
return deps.http().get(path);
|
|
1077
|
+
},
|
|
1078
|
+
async getSeries(input) {
|
|
1079
|
+
const series = requireSeries(input?.series);
|
|
1080
|
+
const cfg = deps.config();
|
|
1081
|
+
if (cfg.mock) {
|
|
1082
|
+
return {
|
|
1083
|
+
series,
|
|
1084
|
+
created: true,
|
|
1085
|
+
genesis: "0",
|
|
1086
|
+
epochDuration: "1",
|
|
1087
|
+
entry: "0",
|
|
1088
|
+
feeBps: 1e3,
|
|
1089
|
+
poolBps: 6e3,
|
|
1090
|
+
seedBps: 3e3,
|
|
1091
|
+
via: "mock",
|
|
1092
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
|
|
1093
|
+
mock: true
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
const path = `/epochs/series${qs({
|
|
1097
|
+
series,
|
|
1098
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
|
|
1099
|
+
})}`;
|
|
1100
|
+
return deps.http().get(path);
|
|
1101
|
+
},
|
|
1102
|
+
async preparePool(input) {
|
|
1103
|
+
const studioWallet = requireAddr(input?.studioWallet, "studioWallet");
|
|
1104
|
+
const cfg = deps.config();
|
|
1105
|
+
const sink = (input.playmosFeeSink ?? PLAYMOS_FEE_SINK_DEFAULT).toLowerCase();
|
|
1106
|
+
const token = (input.token ?? cfg.contracts?.usdc ?? "0x036cbd53842c5426634e7929541ec2318f3dcf7e").toLowerCase();
|
|
1107
|
+
if (cfg.mock) {
|
|
1108
|
+
return prepareStudioPoolLocal({ ...input, studioWallet }, sink, token);
|
|
1109
|
+
}
|
|
1110
|
+
return deps.http().post("/epochs/pools/prepare", {
|
|
1111
|
+
studioWallet,
|
|
1112
|
+
feeSink: input.feeSink,
|
|
1113
|
+
admin: input.admin,
|
|
1114
|
+
operator: input.operator,
|
|
1115
|
+
refundTimeout: input.refundTimeout
|
|
1116
|
+
});
|
|
1117
|
+
},
|
|
1118
|
+
async registerPool(input) {
|
|
1119
|
+
const studioWallet = requireAddr(input?.studioWallet, "studioWallet");
|
|
1120
|
+
const poolAddress = requireAddr(input?.poolAddress, "poolAddress");
|
|
1121
|
+
const cfg = deps.config();
|
|
1122
|
+
if (cfg.mock) {
|
|
1123
|
+
const sink = (input.feeSink ?? PLAYMOS_FEE_SINK_DEFAULT).toLowerCase();
|
|
1124
|
+
const token = (cfg.contracts?.usdc ?? "0x036cbd53842c5426634e7929541ec2318f3dcf7e").toLowerCase();
|
|
1125
|
+
const prepared = prepareStudioPoolLocal({ studioWallet, feeSink: input.feeSink, admin: input.admin, operator: input.operator }, sink, token);
|
|
1126
|
+
return {
|
|
1127
|
+
poolAddress,
|
|
1128
|
+
studioWallet: prepared.admin,
|
|
1129
|
+
feeSink: prepared.feeSink,
|
|
1130
|
+
admin: prepared.admin,
|
|
1131
|
+
operator: prepared.operator,
|
|
1132
|
+
studioHoldsAdmin: true,
|
|
1133
|
+
studioHoldsOperator: true,
|
|
1134
|
+
playmosIsAdmin: false,
|
|
1135
|
+
playmosIsOperator: false,
|
|
1136
|
+
ownership: input.walletProof ? "confirmed" : "pending",
|
|
1137
|
+
owner: null,
|
|
1138
|
+
via: "mock",
|
|
1139
|
+
mock: true
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
return deps.http().post("/epochs/pools", {
|
|
1143
|
+
studioWallet,
|
|
1144
|
+
poolAddress,
|
|
1145
|
+
feeSink: input.feeSink,
|
|
1146
|
+
admin: input.admin,
|
|
1147
|
+
operator: input.operator,
|
|
1148
|
+
txHash: input.txHash,
|
|
1149
|
+
walletProof: input.walletProof
|
|
1150
|
+
});
|
|
1151
|
+
},
|
|
1152
|
+
async createPool(input) {
|
|
1153
|
+
const prepared = await this.preparePool(input);
|
|
1154
|
+
const cfg = deps.config();
|
|
1155
|
+
if (cfg.mock) {
|
|
1156
|
+
const poolAddress = input.poolAddress ? requireAddr(input.poolAddress, "poolAddress") : `0x${prepared.constructorArgs.slice(2, 42).padEnd(40, "0")}`;
|
|
1157
|
+
return this.registerPool({
|
|
1158
|
+
studioWallet: prepared.admin,
|
|
1159
|
+
poolAddress,
|
|
1160
|
+
feeSink: prepared.feeSink,
|
|
1161
|
+
walletProof: input.walletProof
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
if (!input.poolAddress) {
|
|
1165
|
+
throw new ConfigError(
|
|
1166
|
+
"createPool on the live path needs poolAddress after the studio wallet submits the constructor \u2014 Playmos does not broadcast"
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
return this.registerPool({
|
|
1170
|
+
studioWallet: prepared.admin,
|
|
1171
|
+
poolAddress: input.poolAddress,
|
|
1172
|
+
feeSink: prepared.feeSink,
|
|
1173
|
+
txHash: input.txHash,
|
|
1174
|
+
walletProof: input.walletProof
|
|
1175
|
+
});
|
|
1176
|
+
},
|
|
1177
|
+
async settle(input) {
|
|
1178
|
+
const series = requireSeries(input?.series);
|
|
1179
|
+
if (!Array.isArray(input?.winners)) {
|
|
1180
|
+
throw new MissingFieldError("winners");
|
|
1181
|
+
}
|
|
1182
|
+
const epochId = epochPathId(input?.epochId);
|
|
1183
|
+
if (epochId === "current") {
|
|
1184
|
+
throw new ConfigError("epochs.settle requires a past epochId \u2014 Current is still taking entries");
|
|
1185
|
+
}
|
|
1186
|
+
const cfg = deps.config();
|
|
1187
|
+
if (!cfg.mock) {
|
|
1188
|
+
if (deps.assertSecretKey) deps.assertSecretKey("playmos.epochs.settle");
|
|
1189
|
+
else if (!String(cfg.apiKey ?? "").startsWith("sk_")) {
|
|
1190
|
+
throw new AuthError(
|
|
1191
|
+
"playmos.epochs.settle requires a secret (sk_) key \u2014 use sk_test_\u2026 / sk_live_\u2026 on the server. Publishable (pk_) keys cannot settle.",
|
|
1192
|
+
{ surface: "playmos.epochs.settle", isSecret: false }
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (cfg.mock) {
|
|
1197
|
+
const empty = input.winners.length === 0;
|
|
1198
|
+
const podiumPayable = mockPodiumPayable(input.winners);
|
|
1199
|
+
return {
|
|
1200
|
+
series,
|
|
1201
|
+
epochId,
|
|
1202
|
+
status: empty ? "rolled" : "settled",
|
|
1203
|
+
txHash: "0x0000000000000000000000000000000000000000000000000000000000000001",
|
|
1204
|
+
winners: input.winners.map((w) => {
|
|
1205
|
+
const wallet = (typeof w === "string" ? w : w.wallet).toLowerCase();
|
|
1206
|
+
return { wallet, ...mockWinnerAmount(w) };
|
|
1207
|
+
}),
|
|
1208
|
+
...podiumPayable ?? {},
|
|
1209
|
+
terminal: empty ? "rolled" : "settled",
|
|
1210
|
+
via: "mock",
|
|
1211
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
|
|
1212
|
+
mock: true
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
const body = await deps.http().post(
|
|
1216
|
+
`/epochs/${encodeURIComponent(epochId)}/settle`,
|
|
1217
|
+
{
|
|
1218
|
+
series,
|
|
1219
|
+
winners: input.winners,
|
|
1220
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
|
|
1221
|
+
},
|
|
1222
|
+
{ acceptStatuses: [200, 202] }
|
|
1223
|
+
);
|
|
1224
|
+
return body.settle;
|
|
1225
|
+
},
|
|
1226
|
+
async enter(input) {
|
|
1227
|
+
const series = requireSeries(input?.series);
|
|
1228
|
+
const identity = requireIdentity(input?.identity);
|
|
1229
|
+
const cfg = deps.config();
|
|
1230
|
+
const seriesBytes32 = seriesToBytes32(series);
|
|
1231
|
+
const identityBytes32 = identityToBytes32(identity);
|
|
1232
|
+
if (cfg.mock) {
|
|
1233
|
+
const genesis = toBig(input.genesis, 0n);
|
|
1234
|
+
const duration = toBig(input.epochDuration, 1n);
|
|
1235
|
+
const at = toBig(input.at, BigInt(Math.floor(Date.now() / 1e3)));
|
|
1236
|
+
return {
|
|
1237
|
+
series,
|
|
1238
|
+
identity,
|
|
1239
|
+
epochId: derivedEpochId(genesis, duration, at).toString(),
|
|
1240
|
+
epochIdSource: "mock",
|
|
1241
|
+
seriesBytes32,
|
|
1242
|
+
identityBytes32,
|
|
1243
|
+
entry: formatMicroToUsd(C1_ENTER_SPLIT_MICRO.entry),
|
|
1244
|
+
entryMicro: C1_ENTER_SPLIT_MICRO.entry.toString(),
|
|
1245
|
+
status: "confirmed",
|
|
1246
|
+
txHash: MOCK_TX,
|
|
1247
|
+
identityEntryCount: "1",
|
|
1248
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
|
|
1249
|
+
usdc: cfg.contracts?.usdc ?? null,
|
|
1250
|
+
via: "mock",
|
|
1251
|
+
mock: true
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
const epochPrizePool = input.epochPrizePool ?? cfg.contracts?.epochPrizePool;
|
|
1255
|
+
const plan = await deps.http().post("/epochs/enter", {
|
|
1256
|
+
series,
|
|
1257
|
+
identity,
|
|
1258
|
+
...epochPrizePool ? { epochPrizePool } : {}
|
|
1259
|
+
});
|
|
1260
|
+
assertPinParity("series", seriesBytes32, plan.clientParams?.seriesBytes32);
|
|
1261
|
+
assertPinParity("identity", identityBytes32, plan.clientParams?.identityBytes32);
|
|
1262
|
+
const send = deps.sendEntry;
|
|
1263
|
+
if (!send) {
|
|
1264
|
+
throw new ConfigError(
|
|
1265
|
+
"epochs.enter needs a player wallet \u2014 pass `wallet` to the Playmos client. Entry money goes straight to the contract; Playmos never holds it. Building the calls yourself? Import `buildEpochEntryCalls`.",
|
|
1266
|
+
{ field: "wallet" }
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
const entryMicro = BigInt(plan.clientParams.entryMicro);
|
|
1270
|
+
const calls = buildEpochEntryCalls({
|
|
1271
|
+
usdc: plan.clientParams.usdc,
|
|
1272
|
+
epochPrizePool: plan.clientParams.epochPrizePool,
|
|
1273
|
+
series,
|
|
1274
|
+
identity,
|
|
1275
|
+
entryMicro
|
|
1276
|
+
});
|
|
1277
|
+
const sent = await send(calls, { paymasterUrl: plan.clientParams.paymasterUrl });
|
|
1278
|
+
const base = {
|
|
1279
|
+
series,
|
|
1280
|
+
identity,
|
|
1281
|
+
epochId: plan.entry.epochId,
|
|
1282
|
+
epochIdSource: "chain-clock",
|
|
1283
|
+
seriesBytes32,
|
|
1284
|
+
identityBytes32,
|
|
1285
|
+
entry: plan.entry.entry,
|
|
1286
|
+
entryMicro: plan.entry.entryMicro,
|
|
1287
|
+
status: sent.status === "FAILED" ? "failed" : "pending",
|
|
1288
|
+
txHash: sent.txHash,
|
|
1289
|
+
epochPrizePool: plan.clientParams.epochPrizePool,
|
|
1290
|
+
usdc: plan.clientParams.usdc,
|
|
1291
|
+
via: "service"
|
|
1292
|
+
};
|
|
1293
|
+
if (!sent.txHash) return base;
|
|
1294
|
+
try {
|
|
1295
|
+
const confirmed = await deps.http().post("/epochs/enter/confirm", {
|
|
1296
|
+
series,
|
|
1297
|
+
identity,
|
|
1298
|
+
txHash: sent.txHash,
|
|
1299
|
+
...epochPrizePool ? { epochPrizePool } : {}
|
|
1300
|
+
});
|
|
1301
|
+
return {
|
|
1302
|
+
...base,
|
|
1303
|
+
epochId: confirmed.entry.epochId,
|
|
1304
|
+
epochIdSource: confirmed.entry.epochIdSource,
|
|
1305
|
+
status: confirmed.entry.status,
|
|
1306
|
+
identityEntryCount: confirmed.entry.identityEntryCount
|
|
1307
|
+
};
|
|
1308
|
+
} catch {
|
|
1309
|
+
return base;
|
|
1310
|
+
}
|
|
1311
|
+
},
|
|
1312
|
+
async claimableRefund(input) {
|
|
1313
|
+
const series = requireSeries(input?.series);
|
|
1314
|
+
const payer = requireAddr(input?.payer, "payer");
|
|
1315
|
+
const epochId = requirePastEpochId(input?.epochId);
|
|
1316
|
+
const cfg = deps.config();
|
|
1317
|
+
const seriesBytes32 = seriesToBytes32(series);
|
|
1318
|
+
const epochPrizePool = input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null;
|
|
1319
|
+
if (cfg.mock) {
|
|
1320
|
+
return {
|
|
1321
|
+
series,
|
|
1322
|
+
epochId,
|
|
1323
|
+
payer,
|
|
1324
|
+
claimable: "0.00",
|
|
1325
|
+
claimableMicro: "0",
|
|
1326
|
+
seriesBytes32,
|
|
1327
|
+
epochPrizePool,
|
|
1328
|
+
via: "mock",
|
|
1329
|
+
mock: true
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
const pool = requireEpochPool(input, cfg);
|
|
1333
|
+
const data = encodeFunctionData({
|
|
1334
|
+
abi: epochPrizePoolViewAbi,
|
|
1335
|
+
functionName: "claimableRefund",
|
|
1336
|
+
args: [seriesBytes32, BigInt(epochId), payer]
|
|
1337
|
+
});
|
|
1338
|
+
const read = deps.readView;
|
|
1339
|
+
if (!read) {
|
|
1340
|
+
throw new ConfigError(
|
|
1341
|
+
"epochs.claimableRefund needs a wallet or RPC to read EpochPrizePool \u2014 pass `wallet` to the Playmos client",
|
|
1342
|
+
{ field: "wallet" }
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
const raw = await read(pool, data);
|
|
1346
|
+
const amount = decodeFunctionResult({
|
|
1347
|
+
abi: epochPrizePoolViewAbi,
|
|
1348
|
+
functionName: "claimableRefund",
|
|
1349
|
+
data: raw
|
|
1350
|
+
});
|
|
1351
|
+
return {
|
|
1352
|
+
series,
|
|
1353
|
+
epochId,
|
|
1354
|
+
payer,
|
|
1355
|
+
claimable: formatMicroToUsd(amount),
|
|
1356
|
+
claimableMicro: amount.toString(),
|
|
1357
|
+
seriesBytes32,
|
|
1358
|
+
epochPrizePool: pool,
|
|
1359
|
+
via: "chain"
|
|
1360
|
+
};
|
|
1361
|
+
},
|
|
1362
|
+
async claimRefund(input) {
|
|
1363
|
+
const series = requireSeries(input?.series);
|
|
1364
|
+
const payer = requireAddr(input?.payer, "payer");
|
|
1365
|
+
const epochId = requirePastEpochId(input?.epochId);
|
|
1366
|
+
const cfg = deps.config();
|
|
1367
|
+
const pool = requireEpochPool(input, cfg);
|
|
1368
|
+
if (cfg.mock) {
|
|
1369
|
+
return {
|
|
1370
|
+
series,
|
|
1371
|
+
epochId,
|
|
1372
|
+
payer,
|
|
1373
|
+
txHash: MOCK_TX,
|
|
1374
|
+
status: "confirmed",
|
|
1375
|
+
epochPrizePool: pool,
|
|
1376
|
+
via: "mock",
|
|
1377
|
+
mock: true
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
const send = deps.sendEntry;
|
|
1381
|
+
if (!send) {
|
|
1382
|
+
throw new ConfigError(
|
|
1383
|
+
"epochs.claimRefund needs a player wallet \u2014 pass `wallet` to the Playmos client. The credit lands on `payer`; the connected wallet only signs.",
|
|
1384
|
+
{ field: "wallet" }
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
const call = buildEpochClaimRefundCall({
|
|
1388
|
+
epochPrizePool: pool,
|
|
1389
|
+
series,
|
|
1390
|
+
epochId: BigInt(epochId),
|
|
1391
|
+
payer
|
|
1392
|
+
});
|
|
1393
|
+
const sent = await send([call], {});
|
|
1394
|
+
return {
|
|
1395
|
+
series,
|
|
1396
|
+
epochId,
|
|
1397
|
+
payer,
|
|
1398
|
+
txHash: sent.txHash,
|
|
1399
|
+
status: walletStatus(sent.status),
|
|
1400
|
+
epochPrizePool: pool,
|
|
1401
|
+
via: "chain"
|
|
1402
|
+
};
|
|
1403
|
+
},
|
|
1404
|
+
async withdraw(input = {}) {
|
|
1405
|
+
const cfg = deps.config();
|
|
1406
|
+
const pool = requireEpochPool(input, cfg);
|
|
1407
|
+
if (cfg.mock) {
|
|
1408
|
+
return {
|
|
1409
|
+
epochPrizePool: pool,
|
|
1410
|
+
txHash: MOCK_TX,
|
|
1411
|
+
status: "confirmed",
|
|
1412
|
+
via: "mock",
|
|
1413
|
+
mock: true
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
const send = deps.sendEntry;
|
|
1417
|
+
if (!send) {
|
|
1418
|
+
throw new ConfigError(
|
|
1419
|
+
"epochs.withdraw needs the payer wallet \u2014 pass `wallet` to the Playmos client. USDC is pulled to msg.sender on EpochPrizePool, not PrizePool.",
|
|
1420
|
+
{ field: "wallet" }
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
const creditedMicro = await readWithdrawableMicro(deps, pool);
|
|
1424
|
+
const call = buildEpochWithdrawCall(pool);
|
|
1425
|
+
const sent = await send([call], {});
|
|
1426
|
+
return {
|
|
1427
|
+
epochPrizePool: pool,
|
|
1428
|
+
...creditedMicro === null ? {} : {
|
|
1429
|
+
amount: formatMicroToUsd(creditedMicro),
|
|
1430
|
+
amountMicro: creditedMicro.toString()
|
|
1431
|
+
},
|
|
1432
|
+
txHash: sent.txHash,
|
|
1433
|
+
status: walletStatus(sent.status),
|
|
1434
|
+
via: "chain"
|
|
1435
|
+
};
|
|
1436
|
+
},
|
|
1437
|
+
async getAttestation(input) {
|
|
1438
|
+
const series = requireSeries(input?.series);
|
|
1439
|
+
const epochId = requirePastEpochId(input?.epochId);
|
|
1440
|
+
const cfg = deps.config();
|
|
1441
|
+
if (cfg.mock) {
|
|
1442
|
+
const pool = input.epochPrizePool ?? cfg.contracts?.epochPrizePool;
|
|
1443
|
+
if (!pool) {
|
|
1444
|
+
throw new ConfigError("epochs.getAttestation needs contracts.epochPrizePool (or pass epochPrizePool)", {
|
|
1445
|
+
field: "epochPrizePool"
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
return {
|
|
1449
|
+
series,
|
|
1450
|
+
epochId,
|
|
1451
|
+
winners: input.winners ?? [],
|
|
1452
|
+
amountsMicro: input.amountsMicro ?? [],
|
|
1453
|
+
signature: input.signature ?? `0x${"00".repeat(65)}`,
|
|
1454
|
+
epochPrizePool: requireAddr(pool, "epochPrizePool"),
|
|
1455
|
+
via: "mock",
|
|
1456
|
+
mock: true
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
const path = `/epochs/${encodeURIComponent(epochId)}/attestation${qs({
|
|
1460
|
+
series,
|
|
1461
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
|
|
1462
|
+
})}`;
|
|
1463
|
+
const res = await deps.http().get(path);
|
|
1464
|
+
const inner = res.attestation ?? res;
|
|
1465
|
+
if (!inner.series || !inner.winners || !inner.amountsMicro || !inner.signature || !inner.epochPrizePool) {
|
|
1466
|
+
throw new ConfigError("attestation response missing posted fields \u2014 refusing to invent a podium");
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
series: inner.series,
|
|
1470
|
+
epochId: String(inner.epochId ?? epochId),
|
|
1471
|
+
winners: inner.winners,
|
|
1472
|
+
amountsMicro: inner.amountsMicro,
|
|
1473
|
+
signature: inner.signature,
|
|
1474
|
+
epochPrizePool: inner.epochPrizePool,
|
|
1475
|
+
...res.terminal !== void 0 ? { terminal: res.terminal } : {},
|
|
1476
|
+
...res.dueAt !== void 0 ? { dueAt: res.dueAt } : {},
|
|
1477
|
+
via: res.via ?? "service"
|
|
1478
|
+
};
|
|
1479
|
+
},
|
|
1480
|
+
async executeSettlement(input) {
|
|
1481
|
+
const series = requireSeries(input?.series);
|
|
1482
|
+
const epochId = requirePastEpochId(input?.epochId);
|
|
1483
|
+
if (!Array.isArray(input?.winners)) throw new MissingFieldError("winners");
|
|
1484
|
+
if (!Array.isArray(input?.amounts)) throw new MissingFieldError("amounts");
|
|
1485
|
+
if (typeof input?.signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(input.signature)) {
|
|
1486
|
+
throw new MissingFieldError("signature");
|
|
1487
|
+
}
|
|
1488
|
+
const winners = input.winners.map((w, i) => requireAddrKeepCase(w, `winners[${i}]`));
|
|
1489
|
+
const amountsMicro = input.amounts.map((a, i) => {
|
|
1490
|
+
parseMicroString(a, `amounts[${i}]`);
|
|
1491
|
+
return a;
|
|
1492
|
+
});
|
|
1493
|
+
const amounts = amountsMicro.map((a, i) => parseMicroString(a, `amounts[${i}]`));
|
|
1494
|
+
const cfg = deps.config();
|
|
1495
|
+
const pool = requireEpochPool(input, cfg);
|
|
1496
|
+
if (cfg.mock) {
|
|
1497
|
+
return {
|
|
1498
|
+
series,
|
|
1499
|
+
epochId,
|
|
1500
|
+
winners,
|
|
1501
|
+
amountsMicro,
|
|
1502
|
+
signature: input.signature,
|
|
1503
|
+
txHash: MOCK_TX,
|
|
1504
|
+
status: "confirmed",
|
|
1505
|
+
epochPrizePool: pool,
|
|
1506
|
+
via: "mock",
|
|
1507
|
+
mock: true
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
const send = deps.sendEntry;
|
|
1511
|
+
if (!send) {
|
|
1512
|
+
throw new ConfigError(
|
|
1513
|
+
"epochs.executeSettlement needs a wallet \u2014 pass `wallet` to the Playmos client. The SDK relays the signed podium; it does not choose winners.",
|
|
1514
|
+
{ field: "wallet" }
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
const call = buildEpochExecuteSettlementCall({
|
|
1518
|
+
epochPrizePool: pool,
|
|
1519
|
+
series,
|
|
1520
|
+
epochId: BigInt(epochId),
|
|
1521
|
+
winners,
|
|
1522
|
+
amounts,
|
|
1523
|
+
signature: input.signature
|
|
1524
|
+
});
|
|
1525
|
+
let sent;
|
|
1526
|
+
try {
|
|
1527
|
+
sent = await send([call], {});
|
|
1528
|
+
} catch (e) {
|
|
1529
|
+
const msg = e?.message ?? String(e);
|
|
1530
|
+
throw new PaymentFailedError("EpochPrizePool.executeSignedSettlement failed.", { cause: msg });
|
|
1531
|
+
}
|
|
1532
|
+
const terminal = await readEpochTerminal(deps, pool, series, epochId);
|
|
1533
|
+
return {
|
|
1534
|
+
series,
|
|
1535
|
+
epochId,
|
|
1536
|
+
winners,
|
|
1537
|
+
amountsMicro,
|
|
1538
|
+
signature: input.signature,
|
|
1539
|
+
txHash: sent.txHash,
|
|
1540
|
+
status: walletStatus(sent.status),
|
|
1541
|
+
...terminal === null ? {} : { terminal, settled: terminal === "settled" },
|
|
1542
|
+
epochPrizePool: pool,
|
|
1543
|
+
via: "chain"
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
var C1_ENTER_SPLIT_MICRO = {
|
|
1549
|
+
entry: 1000000n,
|
|
1550
|
+
fee: 100000n,
|
|
1551
|
+
pool: 600000n,
|
|
1552
|
+
outgoingSeed: 300000n,
|
|
1553
|
+
incomingSeed: 0n
|
|
1554
|
+
};
|
|
1555
|
+
|
|
1556
|
+
// src/x402.ts
|
|
1557
|
+
var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
|
|
814
1558
|
function requireAddress2(value, field) {
|
|
815
1559
|
if (typeof value !== "string" || value.trim() === "") {
|
|
816
1560
|
throw new MissingFieldError(field);
|
|
817
1561
|
}
|
|
818
|
-
if (!
|
|
1562
|
+
if (!ADDRESS_RE3.test(value)) {
|
|
819
1563
|
throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
|
|
820
1564
|
field,
|
|
821
1565
|
value
|
|
@@ -953,7 +1697,7 @@ function validateX402ChallengeInput(input) {
|
|
|
953
1697
|
}
|
|
954
1698
|
|
|
955
1699
|
// src/client.ts
|
|
956
|
-
var
|
|
1700
|
+
var ADDRESS_RE4 = /^0x[0-9a-fA-F]{40}$/;
|
|
957
1701
|
function mapAlreadyEntered(e) {
|
|
958
1702
|
const msg = e instanceof Error ? e.message : typeof e === "object" && e && "message" in e ? String(e.message) : String(e);
|
|
959
1703
|
const detail = e instanceof PaymentFailedError || e instanceof ApiError ? e.detail : void 0;
|
|
@@ -1001,7 +1745,7 @@ function requireAddressField(value, field) {
|
|
|
1001
1745
|
if (typeof value !== "string" || value.trim() === "") {
|
|
1002
1746
|
throw new MissingFieldError(field);
|
|
1003
1747
|
}
|
|
1004
|
-
if (!
|
|
1748
|
+
if (!ADDRESS_RE4.test(value)) {
|
|
1005
1749
|
throw new ConfigError(
|
|
1006
1750
|
`${field} must be a 0x-prefixed 20-byte wallet address (Phase 1a transfers settle server-held wallets), got: ${JSON.stringify(value)}`,
|
|
1007
1751
|
{ field, value }
|
|
@@ -1040,6 +1784,56 @@ var Playmos = class {
|
|
|
1040
1784
|
);
|
|
1041
1785
|
}
|
|
1042
1786
|
};
|
|
1787
|
+
/**
|
|
1788
|
+
* Rolling epochs — `currentId` / `prize` / `get` / `getSeries` (sdk#629),
|
|
1789
|
+
* `enter` (sdk#635 / S2), operator `settle` (sdk#639 / S3), the C3
|
|
1790
|
+
* refund pull (sdk#634), and E1b `getAttestation` / `executeSettlement`
|
|
1791
|
+
* (wallet-direct via the same `walletProvider()` funnel as `rounds.withdraw`).
|
|
1792
|
+
* Reads/settle go through the service. Refund claim, withdraw, and signed
|
|
1793
|
+
* execute are wallet-direct on EpochPrizePool — not PrizePool.
|
|
1794
|
+
* Nothing here opens a round.
|
|
1795
|
+
*/
|
|
1796
|
+
this.epochs = createEpochsApi({
|
|
1797
|
+
http: () => this.http,
|
|
1798
|
+
config: () => this.config,
|
|
1799
|
+
assertSecretKey: (surface) => this.assertSecretKey(surface),
|
|
1800
|
+
sendEntry: async (calls, opts) => {
|
|
1801
|
+
const provider = this.walletProvider();
|
|
1802
|
+
if (this.config.gas?.mode === "player") {
|
|
1803
|
+
await assertEnoughGas(provider, await getAccount(provider));
|
|
1804
|
+
}
|
|
1805
|
+
const from = await getAccount(provider);
|
|
1806
|
+
const paymasterUrl = this.config.gas?.mode === "player" ? void 0 : this.config.gas?.paymasterUrl ?? opts.paymasterUrl;
|
|
1807
|
+
const { id } = await sendCalls(provider, from, this.env.chainId, calls, paymasterUrl);
|
|
1808
|
+
return waitForCalls(provider, id);
|
|
1809
|
+
},
|
|
1810
|
+
walletAddress: async () => getAccount(this.walletProvider()),
|
|
1811
|
+
readView: async (to, data) => {
|
|
1812
|
+
if (await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
|
|
1813
|
+
const provider = this.walletProvider();
|
|
1814
|
+
return await provider.request({
|
|
1815
|
+
method: "eth_call",
|
|
1816
|
+
params: [{ to, data }, "latest"]
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
const rpc = this.env.network === "base" ? "https://mainnet.base.org" : "https://sepolia.base.org";
|
|
1820
|
+
const res = await fetch(rpc, {
|
|
1821
|
+
method: "POST",
|
|
1822
|
+
headers: { "content-type": "application/json" },
|
|
1823
|
+
body: JSON.stringify({
|
|
1824
|
+
jsonrpc: "2.0",
|
|
1825
|
+
id: 1,
|
|
1826
|
+
method: "eth_call",
|
|
1827
|
+
params: [{ to, data }, "latest"]
|
|
1828
|
+
})
|
|
1829
|
+
});
|
|
1830
|
+
const json = await res.json();
|
|
1831
|
+
if (!json.result) {
|
|
1832
|
+
throw new ApiError(`eth_call failed: ${json.error?.message ?? "no result"}`, { to });
|
|
1833
|
+
}
|
|
1834
|
+
return json.result;
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1043
1837
|
this.payouts = {
|
|
1044
1838
|
/** Choose how the studio is paid: "usdc" (default) or "fiat" (Bridge). Secret key only (#264). */
|
|
1045
1839
|
setMode: async (mode) => {
|
|
@@ -1205,6 +1999,8 @@ var Playmos = class {
|
|
|
1205
1999
|
this.mockPotMicro = /* @__PURE__ */ new Map();
|
|
1206
2000
|
/** Persisted settle winners for re-settle replay (#490 C2) — never re-invent pool-to-everyone. */
|
|
1207
2001
|
this.mockSettleWinners = /* @__PURE__ */ new Map();
|
|
2002
|
+
/** L34 notices after mock settle — funded only with a push txHash. */
|
|
2003
|
+
this.mockPayoutNotices = /* @__PURE__ */ new Map();
|
|
1208
2004
|
this.rounds = {
|
|
1209
2005
|
open: async (input) => {
|
|
1210
2006
|
if (!this.config.mock) this.assertSecretKey("playmos.rounds.open");
|
|
@@ -1370,6 +2166,16 @@ var Playmos = class {
|
|
|
1370
2166
|
};
|
|
1371
2167
|
this.mockRounds.set(input.roundId, settled);
|
|
1372
2168
|
this.mockSettleWinners.set(input.roundId, winners);
|
|
2169
|
+
this.mockPayoutNotices.set(
|
|
2170
|
+
input.roundId,
|
|
2171
|
+
rows.filter((w) => w.micro > 0n).map((w) => ({
|
|
2172
|
+
roundId: input.roundId,
|
|
2173
|
+
wallet: w.wallet.toLowerCase(),
|
|
2174
|
+
amountMicro: w.micro.toString(),
|
|
2175
|
+
status: "claimable",
|
|
2176
|
+
reason: "prizepool_pull_credit"
|
|
2177
|
+
}))
|
|
2178
|
+
);
|
|
1373
2179
|
for (const w of rows) {
|
|
1374
2180
|
if (w.micro <= 0n) continue;
|
|
1375
2181
|
const k = `${input.roundId}:${w.wallet.toLowerCase()}`;
|
|
@@ -1611,6 +2417,93 @@ var Playmos = class {
|
|
|
1611
2417
|
}
|
|
1612
2418
|
return body.active;
|
|
1613
2419
|
},
|
|
2420
|
+
/**
|
|
2421
|
+
* L34 / sdk#610 — notifications for a settled round.
|
|
2422
|
+
* Each win is `funded` (push txHash) or `claimable` (withdraw fallback).
|
|
2423
|
+
* Never returns `funded` without a transfer txHash.
|
|
2424
|
+
*/
|
|
2425
|
+
payoutNotices: async (input) => {
|
|
2426
|
+
requireField(input?.roundId, "roundId");
|
|
2427
|
+
if (this.config.mock) {
|
|
2428
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
2429
|
+
if (!existing) {
|
|
2430
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
2431
|
+
}
|
|
2432
|
+
let notices = this.mockPayoutNotices.get(input.roundId) ?? [];
|
|
2433
|
+
if (input.wallet) {
|
|
2434
|
+
if (!ADDRESS_RE4.test(input.wallet)) {
|
|
2435
|
+
throw new ConfigError("wallet must be a 0x-prefixed 20-byte address");
|
|
2436
|
+
}
|
|
2437
|
+
const want = input.wallet.toLowerCase();
|
|
2438
|
+
notices = notices.filter((n) => n.wallet === want);
|
|
2439
|
+
}
|
|
2440
|
+
for (const n of notices) {
|
|
2441
|
+
if (n.status === "funded" && !n.txHash) {
|
|
2442
|
+
throw new ApiError("cannot label funded without a transfer txHash", {
|
|
2443
|
+
status: 500,
|
|
2444
|
+
code: "payout_notice_invalid"
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
return { roundId: input.roundId, status: existing.status, notices };
|
|
2449
|
+
}
|
|
2450
|
+
const q = input.wallet ? `?wallet=${encodeURIComponent(input.wallet)}` : "";
|
|
2451
|
+
return this.http.get(
|
|
2452
|
+
`/rounds/${encodeURIComponent(input.roundId)}/payout-notices${q}`
|
|
2453
|
+
);
|
|
2454
|
+
},
|
|
2455
|
+
/**
|
|
2456
|
+
* Operator push-attempt after settle (L34). Mock: mark a winner funded only
|
|
2457
|
+
* when a real-shaped txHash is supplied; otherwise that row stays claimable.
|
|
2458
|
+
*/
|
|
2459
|
+
pushWinnings: async (input) => {
|
|
2460
|
+
requireField(input?.roundId, "roundId");
|
|
2461
|
+
if (!this.config.mock) this.assertSecretKey("playmos.rounds.pushWinnings");
|
|
2462
|
+
if (this.config.mock) {
|
|
2463
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
2464
|
+
if (!existing) {
|
|
2465
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
2466
|
+
}
|
|
2467
|
+
if (existing.status !== "settled") {
|
|
2468
|
+
throw new ApiError(`round ${input.roundId} is not settled \u2014 push after the clock`, {
|
|
2469
|
+
status: 409,
|
|
2470
|
+
code: "conflict"
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
const prior = this.mockPayoutNotices.get(input.roundId) ?? [];
|
|
2474
|
+
const byWallet = new Map(
|
|
2475
|
+
(input.pushes ?? []).map((p) => [p.wallet.toLowerCase(), p])
|
|
2476
|
+
);
|
|
2477
|
+
const TX_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
2478
|
+
const ZERO = "0x" + "00".repeat(32);
|
|
2479
|
+
const notices = prior.map((n) => {
|
|
2480
|
+
const p = byWallet.get(n.wallet);
|
|
2481
|
+
if (p && !p.failed && p.txHash && TX_RE.test(p.txHash) && p.txHash.toLowerCase() !== ZERO) {
|
|
2482
|
+
return {
|
|
2483
|
+
...n,
|
|
2484
|
+
status: "funded",
|
|
2485
|
+
txHash: p.txHash.toLowerCase(),
|
|
2486
|
+
reason: void 0
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
if (p && (p.failed || !p.txHash || !TX_RE.test(p.txHash) || p.txHash.toLowerCase() === ZERO)) {
|
|
2490
|
+
return {
|
|
2491
|
+
...n,
|
|
2492
|
+
status: "claimable",
|
|
2493
|
+
txHash: void 0,
|
|
2494
|
+
reason: p.failed ? "push_failed" : "push_missing_txHash"
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
return n;
|
|
2498
|
+
});
|
|
2499
|
+
this.mockPayoutNotices.set(input.roundId, notices);
|
|
2500
|
+
return { roundId: input.roundId, status: existing.status, notices };
|
|
2501
|
+
}
|
|
2502
|
+
return this.http.post(
|
|
2503
|
+
`/rounds/${encodeURIComponent(input.roundId)}/push-winnings`,
|
|
2504
|
+
{}
|
|
2505
|
+
);
|
|
2506
|
+
},
|
|
1614
2507
|
/**
|
|
1615
2508
|
* Read a wallet's **claimable** prize balance for a round (issue #41).
|
|
1616
2509
|
* Prefers the service chain read (`GET /v1/rounds/:id/prize?wallet=`); falls
|
|
@@ -1667,7 +2560,7 @@ var Playmos = class {
|
|
|
1667
2560
|
const existing = this.mockRounds.get(input.roundId);
|
|
1668
2561
|
if (existing?.prizePoolAddress) prizePool2 = existing.prizePoolAddress;
|
|
1669
2562
|
}
|
|
1670
|
-
if (!input.wallet || !
|
|
2563
|
+
if (!input.wallet || !ADDRESS_RE4.test(input.wallet)) {
|
|
1671
2564
|
throw new ConfigError(
|
|
1672
2565
|
"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)"
|
|
1673
2566
|
);
|
|
@@ -1953,8 +2846,16 @@ var Playmos = class {
|
|
|
1953
2846
|
/**
|
|
1954
2847
|
* Offline mock payments for this client instance — so `verify(id)` after
|
|
1955
2848
|
* `mock: true` pay/enterRound does not hit the live API (#204).
|
|
2849
|
+
* Keyed by payment id for verify.
|
|
1956
2850
|
*/
|
|
1957
2851
|
this.mockPayments = /* @__PURE__ */ new Map();
|
|
2852
|
+
/**
|
|
2853
|
+
* sdk#513 / residual B1–B2 — held idempotencyKey offline (live contract rehearsal).
|
|
2854
|
+
* Keyed like service: `${gameId}:${key}` (IAP without gameId uses empty gameId).
|
|
2855
|
+
* Stores terms so same key + different money throws (live IdempotencyConflict), not silent replay.
|
|
2856
|
+
* Auto-minted keys still unique per call (not stored for cross-call replay).
|
|
2857
|
+
*/
|
|
2858
|
+
this.mockByIdempotencyKey = /* @__PURE__ */ new Map();
|
|
1958
2859
|
/**
|
|
1959
2860
|
* `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
|
|
1960
2861
|
* `get` reconciles once against chain truth; `wait` polls it to a terminal state
|
|
@@ -2139,18 +3040,62 @@ var Playmos = class {
|
|
|
2139
3040
|
}
|
|
2140
3041
|
};
|
|
2141
3042
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
3043
|
+
mockIdemScopeKey(gameId, heldKey) {
|
|
3044
|
+
return `${gameId ?? ""}:${heldKey}`;
|
|
3045
|
+
}
|
|
3046
|
+
rememberMock(payment, held) {
|
|
3047
|
+
if (payment.mock) {
|
|
3048
|
+
this.mockPayments.set(payment.id, payment);
|
|
3049
|
+
if (held) {
|
|
3050
|
+
this.mockByIdempotencyKey.set(this.mockIdemScopeKey(held.terms.gameId, held.key), {
|
|
3051
|
+
payment,
|
|
3052
|
+
terms: held.terms
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
2144
3056
|
return payment;
|
|
2145
3057
|
}
|
|
3058
|
+
/** Live-shaped conflict when held mock key is reused with different money terms (sdk#513 residual B1). */
|
|
3059
|
+
mockIdemReplayOrThrow(heldKey, terms) {
|
|
3060
|
+
const prior = this.mockByIdempotencyKey.get(this.mockIdemScopeKey(terms.gameId, heldKey));
|
|
3061
|
+
if (!prior) return null;
|
|
3062
|
+
const t = prior.terms;
|
|
3063
|
+
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;
|
|
3064
|
+
if (!same) {
|
|
3065
|
+
throw new ApiError(
|
|
3066
|
+
`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).`,
|
|
3067
|
+
{
|
|
3068
|
+
code: "idempotency_conflict",
|
|
3069
|
+
idempotencyKey: heldKey,
|
|
3070
|
+
priorKind: t.kind,
|
|
3071
|
+
priorAmount: t.amount
|
|
3072
|
+
}
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
3075
|
+
return prior.payment;
|
|
3076
|
+
}
|
|
2146
3077
|
/** External-studio IAP (1%). Returns a real confirmed Payment with a txHash. */
|
|
2147
3078
|
async pay(input) {
|
|
2148
3079
|
const amountMicro = validateAmount(input.amount);
|
|
2149
3080
|
requireField(input.sku, "sku");
|
|
2150
3081
|
requireField(input.playerId, "playerId");
|
|
2151
3082
|
const metadata = validateMetadata(input.metadata);
|
|
2152
|
-
const
|
|
3083
|
+
const heldIdem = typeof input.idempotencyKey === "string" && input.idempotencyKey.trim() ? input.idempotencyKey.trim() : void 0;
|
|
3084
|
+
const idempotencyKey = heldIdem ?? prefixedId("idem");
|
|
2153
3085
|
if (this.config.mock) {
|
|
3086
|
+
const terms = {
|
|
3087
|
+
kind: "iap",
|
|
3088
|
+
amount: formatMicroToUsd(amountMicro),
|
|
3089
|
+
gameId: input.gameId ?? "",
|
|
3090
|
+
playerId: input.playerId,
|
|
3091
|
+
roundId: "",
|
|
3092
|
+
identity: "",
|
|
3093
|
+
sku: input.sku.trim()
|
|
3094
|
+
};
|
|
3095
|
+
if (heldIdem) {
|
|
3096
|
+
const replay = this.mockIdemReplayOrThrow(heldIdem, terms);
|
|
3097
|
+
if (replay) return replay;
|
|
3098
|
+
}
|
|
2154
3099
|
return this.rememberMock(
|
|
2155
3100
|
mockIapPayment({
|
|
2156
3101
|
amountMicro,
|
|
@@ -2159,7 +3104,8 @@ var Playmos = class {
|
|
|
2159
3104
|
playerId: input.playerId,
|
|
2160
3105
|
chain: this.env.network,
|
|
2161
3106
|
metadata
|
|
2162
|
-
})
|
|
3107
|
+
}),
|
|
3108
|
+
heldIdem ? { key: heldIdem, terms } : void 0
|
|
2163
3109
|
);
|
|
2164
3110
|
}
|
|
2165
3111
|
if (this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
|
|
@@ -2181,7 +3127,13 @@ var Playmos = class {
|
|
|
2181
3127
|
);
|
|
2182
3128
|
let payment = this.mapServerPayment(res.payment, "iap", amountMicro);
|
|
2183
3129
|
if (payment.status === "pending" || payment.status === "created") {
|
|
2184
|
-
payment = await this.waitForServerSettle(
|
|
3130
|
+
payment = await this.waitForServerSettle(
|
|
3131
|
+
payment.id,
|
|
3132
|
+
"iap",
|
|
3133
|
+
amountMicro,
|
|
3134
|
+
input.settleTimeoutMs ?? 6e4,
|
|
3135
|
+
input.signal
|
|
3136
|
+
);
|
|
2185
3137
|
}
|
|
2186
3138
|
return payment;
|
|
2187
3139
|
}
|
|
@@ -2219,15 +3171,29 @@ var Playmos = class {
|
|
|
2219
3171
|
const { txHash } = await waitForCalls(provider, callsId);
|
|
2220
3172
|
return this.settle(intent.payment.id, txHash);
|
|
2221
3173
|
}
|
|
2222
|
-
/**
|
|
3174
|
+
/** Skill-game prize-pool entry. Live sandbox / Playmos Lab uses 60/30/10. Studio contest take is 1% (not a live separate pool yet). Closes #343. */
|
|
2223
3175
|
async enterRound(input) {
|
|
2224
3176
|
const amountMicro = validateAmount(input.amount);
|
|
2225
3177
|
requireField(input.gameId, "gameId");
|
|
2226
3178
|
requireField(input.roundId, "roundId");
|
|
2227
3179
|
requireField(input.playerId, "playerId");
|
|
2228
3180
|
const metadata = validateMetadata(input.metadata);
|
|
2229
|
-
const
|
|
3181
|
+
const heldIdem = typeof input.idempotencyKey === "string" && input.idempotencyKey.trim() ? input.idempotencyKey.trim() : void 0;
|
|
3182
|
+
const idempotencyKey = heldIdem ?? prefixedId("idem");
|
|
2230
3183
|
if (this.config.mock) {
|
|
3184
|
+
const terms = {
|
|
3185
|
+
kind: "entry",
|
|
3186
|
+
amount: formatMicroToUsd(amountMicro),
|
|
3187
|
+
gameId: input.gameId,
|
|
3188
|
+
playerId: input.playerId,
|
|
3189
|
+
roundId: input.roundId,
|
|
3190
|
+
identity: typeof input.identity === "string" ? input.identity.trim() : "",
|
|
3191
|
+
sku: ""
|
|
3192
|
+
};
|
|
3193
|
+
if (heldIdem) {
|
|
3194
|
+
const replay = this.mockIdemReplayOrThrow(heldIdem, terms);
|
|
3195
|
+
if (replay) return replay;
|
|
3196
|
+
}
|
|
2231
3197
|
this.mockAccumulateEntry(input.roundId, amountMicro);
|
|
2232
3198
|
return this.rememberMock(
|
|
2233
3199
|
mockEntryPayment({
|
|
@@ -2243,7 +3209,8 @@ var Playmos = class {
|
|
|
2243
3209
|
// B2 pins — must survive mock path for hub hasEntered parity (#204).
|
|
2244
3210
|
roundKey: input.roundKey,
|
|
2245
3211
|
identity: input.identity
|
|
2246
|
-
})
|
|
3212
|
+
}),
|
|
3213
|
+
heldIdem ? { key: heldIdem, terms } : void 0
|
|
2247
3214
|
);
|
|
2248
3215
|
}
|
|
2249
3216
|
const serverSettleNoWallet = this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy());
|
|
@@ -2276,7 +3243,13 @@ var Playmos = class {
|
|
|
2276
3243
|
);
|
|
2277
3244
|
let payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
|
|
2278
3245
|
if (payment2.status === "pending" || payment2.status === "created") {
|
|
2279
|
-
payment2 = await this.waitForServerSettle(
|
|
3246
|
+
payment2 = await this.waitForServerSettle(
|
|
3247
|
+
payment2.id,
|
|
3248
|
+
"entry",
|
|
3249
|
+
amountMicro,
|
|
3250
|
+
input.settleTimeoutMs ?? 6e4,
|
|
3251
|
+
input.signal
|
|
3252
|
+
);
|
|
2280
3253
|
}
|
|
2281
3254
|
if (input.identity) payment2.identity = input.identity;
|
|
2282
3255
|
const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
@@ -2459,10 +3432,16 @@ var Playmos = class {
|
|
|
2459
3432
|
* until chain verify catches PaymentSettled (RPC lag) — poll so pay() matches
|
|
2460
3433
|
* the documented "confirmed" sandbox promise (TTFSC-T0 / cold-run).
|
|
2461
3434
|
*/
|
|
2462
|
-
async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4) {
|
|
3435
|
+
async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4, signal) {
|
|
2463
3436
|
const start = Date.now();
|
|
2464
3437
|
let delayMs = 400;
|
|
2465
3438
|
while (Date.now() - start < timeoutMs) {
|
|
3439
|
+
if (signal?.aborted) {
|
|
3440
|
+
throw new ApiError(
|
|
3441
|
+
`Aborted waiting for server-settle of ${paymentId}. Re-check with playmos.verify("${paymentId}").`,
|
|
3442
|
+
{ paymentId, aborted: true, asyncSettle: true }
|
|
3443
|
+
);
|
|
3444
|
+
}
|
|
2466
3445
|
const raw = await this.http.get(
|
|
2467
3446
|
`/payments/${encodeURIComponent(paymentId)}`
|
|
2468
3447
|
);
|
|
@@ -2477,12 +3456,46 @@ var Playmos = class {
|
|
|
2477
3456
|
{ paymentId, timeout: true, asyncSettle: true }
|
|
2478
3457
|
);
|
|
2479
3458
|
}
|
|
3459
|
+
/**
|
|
3460
|
+
* sdk#509 — recover a payment after a lost response by the idempotency key you held.
|
|
3461
|
+
* Requires secret sk_test_ (same as list). Returns null if not found.
|
|
3462
|
+
*/
|
|
3463
|
+
async findPaymentByIdempotencyKey(idempotencyKey, opts) {
|
|
3464
|
+
requireField(idempotencyKey, "idempotencyKey");
|
|
3465
|
+
if (this.config.mock) {
|
|
3466
|
+
return null;
|
|
3467
|
+
}
|
|
3468
|
+
const q = new URLSearchParams({ idempotencyKey });
|
|
3469
|
+
if (opts?.gameId) q.set("gameId", opts.gameId);
|
|
3470
|
+
try {
|
|
3471
|
+
const res = await this.http.get(
|
|
3472
|
+
`/payments?${q.toString()}`
|
|
3473
|
+
);
|
|
3474
|
+
const row = res.data?.[0];
|
|
3475
|
+
if (!row || typeof row.id !== "string") return null;
|
|
3476
|
+
const kind = row.kind === "entry" ? "entry" : "iap";
|
|
3477
|
+
const amountMicro = validateAmount(String(row.amount ?? "0.01"));
|
|
3478
|
+
return this.mapServerPayment(
|
|
3479
|
+
row,
|
|
3480
|
+
kind,
|
|
3481
|
+
amountMicro
|
|
3482
|
+
);
|
|
3483
|
+
} catch (e) {
|
|
3484
|
+
if (e instanceof ApiError && (e.detail?.status === 404 || e.detail?.code === "not_found" || /not found/i.test(e.message))) {
|
|
3485
|
+
return null;
|
|
3486
|
+
}
|
|
3487
|
+
throw e;
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
2480
3490
|
/**
|
|
2481
3491
|
* Map a server-settled payment (from the `settle: "server"` response) into the
|
|
2482
3492
|
* SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
|
|
2483
|
-
* pass its authoritative `status`/`txHash`/amounts straight through
|
|
2484
|
-
*
|
|
2485
|
-
*
|
|
3493
|
+
* pass its authoritative `status`/`txHash`/amounts straight through.
|
|
3494
|
+
*
|
|
3495
|
+
* For entry, `Payment.split` is a **first-party 60/30/10 projection** from the
|
|
3496
|
+
* request amount using SDK BPS constants (same integer math as mock) — not a
|
|
3497
|
+
* chain read of pool constructor bps or post-lock payable pot (sdk#514 residual N1).
|
|
3498
|
+
* Prefer `chainAmount` / on-chain `getRound` for money truth.
|
|
2486
3499
|
*/
|
|
2487
3500
|
mapServerPayment(p, kind, amountMicro) {
|
|
2488
3501
|
const payment = {
|
|
@@ -2650,13 +3663,13 @@ function previewPoolSplit(amount) {
|
|
|
2650
3663
|
}
|
|
2651
3664
|
|
|
2652
3665
|
// src/settlement.ts
|
|
2653
|
-
var
|
|
3666
|
+
var ADDRESS_RE5 = /^0x[0-9a-fA-F]{40}$/;
|
|
2654
3667
|
var DEFAULT_TTL_MS = 15 * 60 * 1e3;
|
|
2655
3668
|
function requireAddress3(value, field) {
|
|
2656
3669
|
if (typeof value !== "string" || value.trim() === "") {
|
|
2657
3670
|
throw new MissingFieldError(field);
|
|
2658
3671
|
}
|
|
2659
|
-
if (!
|
|
3672
|
+
if (!ADDRESS_RE5.test(value)) {
|
|
2660
3673
|
throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
|
|
2661
3674
|
field,
|
|
2662
3675
|
value
|
|
@@ -2742,4 +3755,4 @@ function isX402PayloadAuthorization(auth) {
|
|
|
2742
3755
|
return auth.kind === "x402-payload";
|
|
2743
3756
|
}
|
|
2744
3757
|
|
|
2745
|
-
export { CHAIN_ID, DEFAULT_API_BASE_URL, MICRO_PER_USDC, PayoutError, Playmos, USDC_ADDRESS, USDC_DECIMALS, 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 };
|
|
3758
|
+
export { C1_ENTER_SPLIT_MICRO, CHAIN_ID, DEFAULT_API_BASE_URL, MICRO_PER_USDC, PLAYMOS_FEE_SINK_DEFAULT, PayoutError, Playmos, USDC_ADDRESS, USDC_DECIMALS, buildEpochClaimRefundCall, buildEpochEntryCalls, buildEpochWithdrawCall, clientRuntimeSignals, computeIapSplit, computePayout, computePoolSplit, createEpochsApi, createPaymentRequirement, createX402Challenge, decodePaymentHeader, derivedEpochId, encodeEpochPoolConstructorArgs, encodePaymentHeader, epochPayableMicro, epochPoolProofMessage, formatMicroToUsd, isClientRuntime, isWalletSignatureAuthorization, isX402PayloadAuthorization, networkToCaip2, parsePaymentRequirement, parseUsdToMicro, prefixedId, prepareStudioPoolLocal, previewEscrowFee, previewIapSplit, previewMarketplaceSplit, previewPoolSplit, previewTransferSplit, resolveEnv, serializePaymentRequirement, settlementTypedData, toX402PaymentRequired, ulid, validateX402ChallengeInput };
|