@playmos/sdk 0.3.11 → 0.3.13
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 +14 -0
- package/LICENSE +21 -0
- package/README.md +31 -12
- package/dist/{chunk-35WJANW4.js → chunk-TZFGZNXV.js} +192 -2
- package/dist/{errors-Dpeesyop.d.cts → errors-CYKgt2VU.d.cts} +64 -8
- package/dist/{errors-Dpeesyop.d.ts → errors-CYKgt2VU.d.ts} +64 -8
- package/dist/index.cjs +1285 -8
- package/dist/index.d.cts +674 -16
- package/dist/index.d.ts +674 -16
- package/dist/index.js +1081 -12
- package/dist/server.d.cts +3 -11
- package/dist/server.d.ts +3 -11
- package/dist/server.js +2 -2
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AuthError, InvalidAmountError, ConfigError, MissingFieldError,
|
|
2
|
-
export { AlreadyEnteredError, ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError, WalletTimeoutError } from './chunk-
|
|
3
|
-
import { encodeFunctionData, decodeFunctionResult } from 'viem';
|
|
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-TZFGZNXV.js';
|
|
2
|
+
export { AlreadyEnteredError, ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError, WalletTimeoutError, buildEpochExecuteSettlementCall, identityToBytes32, seriesToBytes32 } from './chunk-TZFGZNXV.js';
|
|
3
|
+
import { encodeFunctionData, encodeAbiParameters, decodeFunctionResult } from 'viem';
|
|
4
4
|
|
|
5
5
|
// src/config.ts
|
|
6
6
|
var CHAIN_ID = {
|
|
@@ -581,6 +581,37 @@ async function getAccount(provider) {
|
|
|
581
581
|
});
|
|
582
582
|
}
|
|
583
583
|
}
|
|
584
|
+
function buildEpochEntryCalls(args) {
|
|
585
|
+
const enterData = encodeFunctionData({
|
|
586
|
+
abi: epochPrizePoolEnterAbi,
|
|
587
|
+
functionName: "enter",
|
|
588
|
+
args: [seriesToBytes32(args.series), identityToBytes32(args.identity)]
|
|
589
|
+
});
|
|
590
|
+
return [
|
|
591
|
+
{ to: args.usdc, data: encodeApprove(args.epochPrizePool, args.entryMicro) },
|
|
592
|
+
{ to: args.epochPrizePool, data: enterData }
|
|
593
|
+
];
|
|
594
|
+
}
|
|
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
|
+
};
|
|
614
|
+
}
|
|
584
615
|
|
|
585
616
|
// src/mock.ts
|
|
586
617
|
var MOCK_TX = `0x${"0".repeat(56)}deadbeef`;
|
|
@@ -642,13 +673,902 @@ function mockVerifyResult(payment) {
|
|
|
642
673
|
return { ...payment, mock: true };
|
|
643
674
|
}
|
|
644
675
|
|
|
645
|
-
// src/
|
|
676
|
+
// src/epochs.ts
|
|
677
|
+
var PLAYMOS_FEE_SINK_DEFAULT = "0xd84c190085aa59c48a9b478ea333d50b8df4ad42";
|
|
678
|
+
var RETIRED_PLAYMOS_FEE_SINK = "0x1b8031e20ed96131a849a52290b4d640f286998d";
|
|
679
|
+
var EPOCH_POOL_CONSTRUCTOR_TYPES = [
|
|
680
|
+
{ type: "address" },
|
|
681
|
+
{ type: "address" },
|
|
682
|
+
{ type: "address" },
|
|
683
|
+
{ type: "address" },
|
|
684
|
+
{ type: "uint256" }
|
|
685
|
+
];
|
|
686
|
+
function encodeEpochPoolConstructorArgs(args) {
|
|
687
|
+
return encodeAbiParameters(EPOCH_POOL_CONSTRUCTOR_TYPES, [
|
|
688
|
+
args.token,
|
|
689
|
+
args.admin,
|
|
690
|
+
args.operator,
|
|
691
|
+
args.feeSink,
|
|
692
|
+
args.refundTimeout
|
|
693
|
+
]);
|
|
694
|
+
}
|
|
695
|
+
function derivedEpochId(genesis, epochDuration, at) {
|
|
696
|
+
if (epochDuration === 0n) {
|
|
697
|
+
throw new ConfigError("epochDuration must be > 0");
|
|
698
|
+
}
|
|
699
|
+
if (at < genesis) {
|
|
700
|
+
throw new ConfigError("epoch has not started (at < genesis)");
|
|
701
|
+
}
|
|
702
|
+
return (at - genesis) / epochDuration;
|
|
703
|
+
}
|
|
704
|
+
function epochPayableMicro(incomingSeed, pool) {
|
|
705
|
+
return incomingSeed + pool;
|
|
706
|
+
}
|
|
707
|
+
function mockPodiumPayable(winners) {
|
|
708
|
+
if (winners.length === 0) return void 0;
|
|
709
|
+
let total = 0n;
|
|
710
|
+
for (const w of winners) {
|
|
711
|
+
if (typeof w === "string") return void 0;
|
|
712
|
+
if (w.amountMicro != null && w.amountMicro !== "") {
|
|
713
|
+
try {
|
|
714
|
+
total += BigInt(w.amountMicro);
|
|
715
|
+
} catch {
|
|
716
|
+
return void 0;
|
|
717
|
+
}
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (w.amount) {
|
|
721
|
+
try {
|
|
722
|
+
total += parseUsdToMicro(w.amount);
|
|
723
|
+
} catch {
|
|
724
|
+
return void 0;
|
|
725
|
+
}
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
return void 0;
|
|
729
|
+
}
|
|
730
|
+
return {
|
|
731
|
+
payable: formatMicroToUsd(total),
|
|
732
|
+
payableMicro: total.toString()
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function mockWinnerMicro(w) {
|
|
736
|
+
if (typeof w === "string") return void 0;
|
|
737
|
+
if (w.amountMicro != null && w.amountMicro !== "" && /^\d+$/.test(w.amountMicro)) {
|
|
738
|
+
return BigInt(w.amountMicro);
|
|
739
|
+
}
|
|
740
|
+
if (w.amount) {
|
|
741
|
+
try {
|
|
742
|
+
return parseUsdToMicro(w.amount);
|
|
743
|
+
} catch {
|
|
744
|
+
return void 0;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return void 0;
|
|
748
|
+
}
|
|
749
|
+
function mockWinnerAmount(w) {
|
|
750
|
+
const micro = mockWinnerMicro(w);
|
|
751
|
+
if (micro === void 0) return {};
|
|
752
|
+
return { amount: formatMicroToUsd(micro), amountMicro: micro.toString() };
|
|
753
|
+
}
|
|
754
|
+
var TERMINALS = ["none", "settled", "refunded", "rolled"];
|
|
755
|
+
function terminalFromChain(raw) {
|
|
756
|
+
return TERMINALS[raw] ?? "none";
|
|
757
|
+
}
|
|
758
|
+
function requireSeries(series) {
|
|
759
|
+
if (typeof series !== "string" || series.trim() === "") {
|
|
760
|
+
throw new MissingFieldError("series");
|
|
761
|
+
}
|
|
762
|
+
return series.trim();
|
|
763
|
+
}
|
|
764
|
+
function requireIdentity(identity) {
|
|
765
|
+
if (typeof identity !== "string" || identity.trim() === "") {
|
|
766
|
+
throw new MissingFieldError("identity");
|
|
767
|
+
}
|
|
768
|
+
return identity.trim();
|
|
769
|
+
}
|
|
770
|
+
function assertPinParity(label, local, fromService) {
|
|
771
|
+
if (!fromService) {
|
|
772
|
+
throw new ConfigError(
|
|
773
|
+
`epochs.enter: the service did not return ${label}Bytes32 \u2014 cannot prove the on-chain id matches before paying.`,
|
|
774
|
+
{ field: `${label}Bytes32` }
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
if (local.toLowerCase() !== fromService.toLowerCase()) {
|
|
778
|
+
throw new ConfigError(
|
|
779
|
+
`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.`,
|
|
780
|
+
{ field: `${label}Bytes32` }
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function epochPathId(epochId) {
|
|
785
|
+
if (epochId === void 0 || epochId === null || epochId === "") return "current";
|
|
786
|
+
const s = String(epochId).trim();
|
|
787
|
+
if (s === "current") return "current";
|
|
788
|
+
if (!/^\d+$/.test(s)) {
|
|
789
|
+
throw new ConfigError(`epochId must be a non-negative integer, got: ${JSON.stringify(epochId)}`);
|
|
790
|
+
}
|
|
791
|
+
return s;
|
|
792
|
+
}
|
|
793
|
+
function qs(params) {
|
|
794
|
+
const u = new URLSearchParams();
|
|
795
|
+
for (const [k, v] of Object.entries(params)) {
|
|
796
|
+
if (v) u.set(k, v);
|
|
797
|
+
}
|
|
798
|
+
const s = u.toString();
|
|
799
|
+
return s ? `?${s}` : "";
|
|
800
|
+
}
|
|
801
|
+
function pictureFromMicros(args) {
|
|
802
|
+
const payableMicro = epochPayableMicro(args.incomingSeedMicro, args.poolMicro);
|
|
803
|
+
return {
|
|
804
|
+
series: args.series,
|
|
805
|
+
epochId: args.epochId,
|
|
806
|
+
pool: formatMicroToUsd(args.poolMicro),
|
|
807
|
+
incomingSeed: formatMicroToUsd(args.incomingSeedMicro),
|
|
808
|
+
outgoingSeed: formatMicroToUsd(args.outgoingSeedMicro),
|
|
809
|
+
payable: formatMicroToUsd(payableMicro),
|
|
810
|
+
poolMicro: args.poolMicro.toString(),
|
|
811
|
+
incomingSeedMicro: args.incomingSeedMicro.toString(),
|
|
812
|
+
outgoingSeedMicro: args.outgoingSeedMicro.toString(),
|
|
813
|
+
payableMicro: payableMicro.toString(),
|
|
814
|
+
via: args.via,
|
|
815
|
+
epochPrizePool: args.epochPrizePool,
|
|
816
|
+
...args.mock ? { mock: true } : {}
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function toBig(value, fallback) {
|
|
820
|
+
if (value === void 0) return fallback;
|
|
821
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) return BigInt(Math.trunc(value));
|
|
822
|
+
if (typeof value === "string" && /^\d+$/.test(value.trim())) return BigInt(value.trim());
|
|
823
|
+
throw new ConfigError(`expected a non-negative integer, got: ${JSON.stringify(value)}`);
|
|
824
|
+
}
|
|
646
825
|
var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
|
|
826
|
+
function requireAddr(raw, field) {
|
|
827
|
+
if (typeof raw !== "string" || !ADDRESS_RE2.test(raw.trim())) {
|
|
828
|
+
throw new MissingFieldError(field);
|
|
829
|
+
}
|
|
830
|
+
return raw.trim().toLowerCase();
|
|
831
|
+
}
|
|
832
|
+
function requireAddrKeepCase(raw, field) {
|
|
833
|
+
if (typeof raw !== "string" || !ADDRESS_RE2.test(raw.trim())) {
|
|
834
|
+
throw new MissingFieldError(field);
|
|
835
|
+
}
|
|
836
|
+
return raw.trim();
|
|
837
|
+
}
|
|
838
|
+
function requireEpochPool(input, cfg) {
|
|
839
|
+
const raw = input?.epochPrizePool ?? cfg.contracts?.epochPrizePool;
|
|
840
|
+
if (!raw) {
|
|
841
|
+
throw new ConfigError(
|
|
842
|
+
"epochs refund rail needs contracts.epochPrizePool (or pass epochPrizePool) \u2014 this is EpochPrizePool, not PrizePool",
|
|
843
|
+
{ field: "epochPrizePool" }
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
return requireAddr(raw, "epochPrizePool");
|
|
847
|
+
}
|
|
848
|
+
function requirePastEpochId(epochId) {
|
|
849
|
+
const id = epochPathId(epochId);
|
|
850
|
+
if (id === "current") {
|
|
851
|
+
throw new ConfigError("epochs refund rail requires a past epochId \u2014 Current is still taking entries");
|
|
852
|
+
}
|
|
853
|
+
return id;
|
|
854
|
+
}
|
|
855
|
+
function walletStatus(status) {
|
|
856
|
+
if (status === "CONFIRMED") return "confirmed";
|
|
857
|
+
if (status === "FAILED") return "failed";
|
|
858
|
+
return "pending";
|
|
859
|
+
}
|
|
860
|
+
function sameAddr(a, b) {
|
|
861
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
862
|
+
}
|
|
863
|
+
function prepareStudioPoolLocal(input, playmosFeeSink, token, playmosAddresses = []) {
|
|
864
|
+
const studio = requireAddr(input.studioWallet, "studioWallet");
|
|
865
|
+
const locked = PLAYMOS_FEE_SINK_DEFAULT;
|
|
866
|
+
if (playmosFeeSink && !sameAddr(playmosFeeSink, locked)) {
|
|
867
|
+
throw new ConfigError("feeSink must equal the Playmos Sepolia treasury; refused before broadcast");
|
|
868
|
+
}
|
|
869
|
+
const sink = locked;
|
|
870
|
+
const forbidden = [sink, ...playmosAddresses.map((a) => a.toLowerCase())];
|
|
871
|
+
if (forbidden.some((a) => sameAddr(a, studio))) {
|
|
872
|
+
throw new ConfigError("studioWallet must not be a Playmos address (Playmos never holds studio keys)");
|
|
873
|
+
}
|
|
874
|
+
if (input.feeSink && !sameAddr(input.feeSink, sink)) {
|
|
875
|
+
throw new ConfigError("feeSink must equal the Playmos Sepolia treasury; refused before broadcast");
|
|
876
|
+
}
|
|
877
|
+
if (input.admin && !sameAddr(input.admin, studio)) {
|
|
878
|
+
throw new ConfigError("admin must be the studio wallet from block one");
|
|
879
|
+
}
|
|
880
|
+
if (input.operator && !sameAddr(input.operator, studio)) {
|
|
881
|
+
throw new ConfigError("operator must be the studio wallet from block one");
|
|
882
|
+
}
|
|
883
|
+
if (input.admin && forbidden.some((a) => sameAddr(a, input.admin))) {
|
|
884
|
+
throw new ConfigError("Playmos must not be ADMIN_ROLE");
|
|
885
|
+
}
|
|
886
|
+
if (input.operator && forbidden.some((a) => sameAddr(a, input.operator))) {
|
|
887
|
+
throw new ConfigError("Playmos must not be OPERATOR_ROLE");
|
|
888
|
+
}
|
|
889
|
+
const refundTimeout = toBig(input.refundTimeout, 604800n);
|
|
890
|
+
if (refundTimeout < 3600n || refundTimeout > 7776000n) {
|
|
891
|
+
throw new ConfigError("refundTimeout must be between 3600 (1h) and 7776000 (90d) seconds");
|
|
892
|
+
}
|
|
893
|
+
return {
|
|
894
|
+
token: token.toLowerCase(),
|
|
895
|
+
admin: studio,
|
|
896
|
+
operator: studio,
|
|
897
|
+
feeSink: sink,
|
|
898
|
+
refundTimeout: refundTimeout.toString(),
|
|
899
|
+
constructorArgs: encodeEpochPoolConstructorArgs({
|
|
900
|
+
token: token.toLowerCase(),
|
|
901
|
+
admin: studio,
|
|
902
|
+
operator: studio,
|
|
903
|
+
feeSink: sink,
|
|
904
|
+
refundTimeout
|
|
905
|
+
}),
|
|
906
|
+
broadcast: false,
|
|
907
|
+
constructor: "EpochPrizePool",
|
|
908
|
+
studioHoldsAdmin: true,
|
|
909
|
+
studioHoldsOperator: true,
|
|
910
|
+
playmosIsAdmin: false,
|
|
911
|
+
playmosIsOperator: false,
|
|
912
|
+
via: "mock",
|
|
913
|
+
mock: true
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
function epochPoolProofMessage(terms) {
|
|
917
|
+
return [
|
|
918
|
+
"Playmos EpochPrizePool registration",
|
|
919
|
+
`studio: ${terms.studioId}`,
|
|
920
|
+
`pool: ${terms.poolAddress.toLowerCase()}`,
|
|
921
|
+
`wallet: ${terms.studioWallet.toLowerCase()}`,
|
|
922
|
+
`chainId: ${terms.chainId}`
|
|
923
|
+
].join("\n");
|
|
924
|
+
}
|
|
925
|
+
async function readWithdrawableMicro(deps, pool) {
|
|
926
|
+
const read = deps.readView;
|
|
927
|
+
const account = deps.walletAddress;
|
|
928
|
+
if (!read || !account) return null;
|
|
929
|
+
try {
|
|
930
|
+
const raw = await read(
|
|
931
|
+
pool,
|
|
932
|
+
encodeFunctionData({
|
|
933
|
+
abi: epochPrizePoolViewAbi,
|
|
934
|
+
functionName: "withdrawable",
|
|
935
|
+
args: [await account()]
|
|
936
|
+
})
|
|
937
|
+
);
|
|
938
|
+
return decodeFunctionResult({
|
|
939
|
+
abi: epochPrizePoolViewAbi,
|
|
940
|
+
functionName: "withdrawable",
|
|
941
|
+
data: raw
|
|
942
|
+
});
|
|
943
|
+
} catch {
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function settlementTypedData(input) {
|
|
948
|
+
const pool = requireAddr(input.pool, "pool");
|
|
949
|
+
const series = requireSeries(input.series);
|
|
950
|
+
const epochId = requirePastEpochId(input.epochId);
|
|
951
|
+
if (!Array.isArray(input.winners) || !Array.isArray(input.amounts)) {
|
|
952
|
+
throw new MissingFieldError("winners");
|
|
953
|
+
}
|
|
954
|
+
if (input.winners.length !== input.amounts.length) {
|
|
955
|
+
throw new ConfigError("winners and amounts must be the same length");
|
|
956
|
+
}
|
|
957
|
+
return {
|
|
958
|
+
domain: {
|
|
959
|
+
name: "EpochPrizePool",
|
|
960
|
+
version: "1",
|
|
961
|
+
chainId: input.chainId,
|
|
962
|
+
verifyingContract: pool
|
|
963
|
+
},
|
|
964
|
+
types: {
|
|
965
|
+
Settlement: [
|
|
966
|
+
{ name: "series", type: "bytes32" },
|
|
967
|
+
{ name: "epochId", type: "uint256" },
|
|
968
|
+
{ name: "winners", type: "address[]" },
|
|
969
|
+
{ name: "amounts", type: "uint256[]" }
|
|
970
|
+
]
|
|
971
|
+
},
|
|
972
|
+
primaryType: "Settlement",
|
|
973
|
+
message: {
|
|
974
|
+
series: seriesToBytes32(series),
|
|
975
|
+
epochId: BigInt(epochId),
|
|
976
|
+
winners: input.winners.map((w, i) => requireAddrKeepCase(w, `winners[${i}]`)),
|
|
977
|
+
amounts: input.amounts.map((a, i) => parseMicroString(a, `amounts[${i}]`))
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
function parseMicroString(raw, field) {
|
|
982
|
+
if (typeof raw !== "string" || !/^(0|[1-9]\d*)$/.test(raw)) {
|
|
983
|
+
throw new ConfigError(`${field} must be an integer micro-USDC string`, { field });
|
|
984
|
+
}
|
|
985
|
+
return BigInt(raw);
|
|
986
|
+
}
|
|
987
|
+
async function readEpochTerminal(deps, pool, series, epochId) {
|
|
988
|
+
const read = deps.readView;
|
|
989
|
+
if (!read) return null;
|
|
990
|
+
try {
|
|
991
|
+
const raw = await read(
|
|
992
|
+
pool,
|
|
993
|
+
encodeFunctionData({
|
|
994
|
+
abi: epochPrizePoolViewAbi,
|
|
995
|
+
functionName: "getEpoch",
|
|
996
|
+
args: [seriesToBytes32(series), BigInt(epochId)]
|
|
997
|
+
})
|
|
998
|
+
);
|
|
999
|
+
const decoded = decodeFunctionResult({
|
|
1000
|
+
abi: epochPrizePoolViewAbi,
|
|
1001
|
+
functionName: "getEpoch",
|
|
1002
|
+
data: raw
|
|
1003
|
+
});
|
|
1004
|
+
const termRaw = decoded && typeof decoded === "object" && "terminal" in decoded ? Number(decoded.terminal) : Number(decoded[4]);
|
|
1005
|
+
return terminalFromChain(termRaw);
|
|
1006
|
+
} catch {
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function createEpochsApi(deps) {
|
|
1011
|
+
return {
|
|
1012
|
+
async currentId(input) {
|
|
1013
|
+
const series = requireSeries(input?.series);
|
|
1014
|
+
const cfg = deps.config();
|
|
1015
|
+
if (cfg.mock) {
|
|
1016
|
+
const genesis = toBig(input.genesis, 0n);
|
|
1017
|
+
const duration = toBig(input.epochDuration, 1n);
|
|
1018
|
+
const at = toBig(input.at, BigInt(Math.floor(Date.now() / 1e3)));
|
|
1019
|
+
const epochId = derivedEpochId(genesis, duration, at);
|
|
1020
|
+
return {
|
|
1021
|
+
series,
|
|
1022
|
+
epochId: epochId.toString(),
|
|
1023
|
+
via: "mock",
|
|
1024
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
|
|
1025
|
+
mock: true
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
const path = `/epochs/current${qs({
|
|
1029
|
+
series,
|
|
1030
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
|
|
1031
|
+
})}`;
|
|
1032
|
+
return deps.http().get(path);
|
|
1033
|
+
},
|
|
1034
|
+
async prize(input) {
|
|
1035
|
+
const series = requireSeries(input?.series);
|
|
1036
|
+
const cfg = deps.config();
|
|
1037
|
+
const epochId = epochPathId(input?.epochId);
|
|
1038
|
+
if (cfg.mock) {
|
|
1039
|
+
return pictureFromMicros({
|
|
1040
|
+
series,
|
|
1041
|
+
epochId: epochId === "current" ? "0" : epochId,
|
|
1042
|
+
poolMicro: 0n,
|
|
1043
|
+
incomingSeedMicro: 0n,
|
|
1044
|
+
outgoingSeedMicro: 0n,
|
|
1045
|
+
via: "mock",
|
|
1046
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
|
|
1047
|
+
mock: true
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
const path = `/epochs/${encodeURIComponent(epochId)}/prize${qs({
|
|
1051
|
+
series,
|
|
1052
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
|
|
1053
|
+
})}`;
|
|
1054
|
+
const res = await deps.http().get(path);
|
|
1055
|
+
return res.prize;
|
|
1056
|
+
},
|
|
1057
|
+
async get(input) {
|
|
1058
|
+
const series = requireSeries(input?.series);
|
|
1059
|
+
const cfg = deps.config();
|
|
1060
|
+
const epochId = epochPathId(input?.epochId);
|
|
1061
|
+
if (cfg.mock) {
|
|
1062
|
+
const prize = pictureFromMicros({
|
|
1063
|
+
series,
|
|
1064
|
+
epochId: epochId === "current" ? "0" : epochId,
|
|
1065
|
+
poolMicro: 0n,
|
|
1066
|
+
incomingSeedMicro: 0n,
|
|
1067
|
+
outgoingSeedMicro: 0n,
|
|
1068
|
+
via: "mock",
|
|
1069
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
|
|
1070
|
+
mock: true
|
|
1071
|
+
});
|
|
1072
|
+
return {
|
|
1073
|
+
...prize,
|
|
1074
|
+
entryCount: "0",
|
|
1075
|
+
terminal: "none"
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
const path = `/epochs/${encodeURIComponent(epochId)}${qs({
|
|
1079
|
+
series,
|
|
1080
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
|
|
1081
|
+
})}`;
|
|
1082
|
+
return deps.http().get(path);
|
|
1083
|
+
},
|
|
1084
|
+
async getSeries(input) {
|
|
1085
|
+
const series = requireSeries(input?.series);
|
|
1086
|
+
const cfg = deps.config();
|
|
1087
|
+
if (cfg.mock) {
|
|
1088
|
+
return {
|
|
1089
|
+
series,
|
|
1090
|
+
created: true,
|
|
1091
|
+
genesis: "0",
|
|
1092
|
+
epochDuration: "1",
|
|
1093
|
+
entry: "0",
|
|
1094
|
+
feeBps: 1e3,
|
|
1095
|
+
poolBps: 6e3,
|
|
1096
|
+
seedBps: 3e3,
|
|
1097
|
+
via: "mock",
|
|
1098
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
|
|
1099
|
+
mock: true
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
const path = `/epochs/series${qs({
|
|
1103
|
+
series,
|
|
1104
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
|
|
1105
|
+
})}`;
|
|
1106
|
+
return deps.http().get(path);
|
|
1107
|
+
},
|
|
1108
|
+
async preparePool(input) {
|
|
1109
|
+
const studioWallet = requireAddr(input?.studioWallet, "studioWallet");
|
|
1110
|
+
const cfg = deps.config();
|
|
1111
|
+
const sink = PLAYMOS_FEE_SINK_DEFAULT;
|
|
1112
|
+
const token = (input.token ?? cfg.contracts?.usdc ?? "0x036cbd53842c5426634e7929541ec2318f3dcf7e").toLowerCase();
|
|
1113
|
+
if (input.playmosFeeSink && !sameAddr(input.playmosFeeSink, sink)) {
|
|
1114
|
+
throw new ConfigError("feeSink must equal the Playmos Sepolia treasury; refused before broadcast");
|
|
1115
|
+
}
|
|
1116
|
+
if (cfg.mock) {
|
|
1117
|
+
return prepareStudioPoolLocal({ ...input, studioWallet }, sink, token);
|
|
1118
|
+
}
|
|
1119
|
+
return deps.http().post("/epochs/pools/prepare", {
|
|
1120
|
+
studioWallet,
|
|
1121
|
+
feeSink: input.feeSink,
|
|
1122
|
+
admin: input.admin,
|
|
1123
|
+
operator: input.operator,
|
|
1124
|
+
refundTimeout: input.refundTimeout
|
|
1125
|
+
});
|
|
1126
|
+
},
|
|
1127
|
+
async registerPool(input) {
|
|
1128
|
+
const studioWallet = requireAddr(input?.studioWallet, "studioWallet");
|
|
1129
|
+
const poolAddress = requireAddr(input?.poolAddress, "poolAddress");
|
|
1130
|
+
const cfg = deps.config();
|
|
1131
|
+
if (cfg.mock) {
|
|
1132
|
+
const sink = (input.feeSink ?? PLAYMOS_FEE_SINK_DEFAULT).toLowerCase();
|
|
1133
|
+
const token = (cfg.contracts?.usdc ?? "0x036cbd53842c5426634e7929541ec2318f3dcf7e").toLowerCase();
|
|
1134
|
+
const prepared = prepareStudioPoolLocal({ studioWallet, feeSink: input.feeSink, admin: input.admin, operator: input.operator }, sink, token);
|
|
1135
|
+
return {
|
|
1136
|
+
poolAddress,
|
|
1137
|
+
studioWallet: prepared.admin,
|
|
1138
|
+
feeSink: prepared.feeSink,
|
|
1139
|
+
admin: prepared.admin,
|
|
1140
|
+
operator: prepared.operator,
|
|
1141
|
+
studioHoldsAdmin: true,
|
|
1142
|
+
studioHoldsOperator: true,
|
|
1143
|
+
playmosIsAdmin: false,
|
|
1144
|
+
playmosIsOperator: false,
|
|
1145
|
+
ownership: input.walletProof ? "confirmed" : "pending",
|
|
1146
|
+
owner: null,
|
|
1147
|
+
via: "mock",
|
|
1148
|
+
mock: true
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
return deps.http().post("/epochs/pools", {
|
|
1152
|
+
studioWallet,
|
|
1153
|
+
poolAddress,
|
|
1154
|
+
feeSink: input.feeSink,
|
|
1155
|
+
admin: input.admin,
|
|
1156
|
+
operator: input.operator,
|
|
1157
|
+
txHash: input.txHash,
|
|
1158
|
+
walletProof: input.walletProof
|
|
1159
|
+
});
|
|
1160
|
+
},
|
|
1161
|
+
async createPool(input) {
|
|
1162
|
+
const prepared = await this.preparePool(input);
|
|
1163
|
+
const cfg = deps.config();
|
|
1164
|
+
if (cfg.mock) {
|
|
1165
|
+
const poolAddress = input.poolAddress ? requireAddr(input.poolAddress, "poolAddress") : `0x${prepared.constructorArgs.slice(2, 42).padEnd(40, "0")}`;
|
|
1166
|
+
return this.registerPool({
|
|
1167
|
+
studioWallet: prepared.admin,
|
|
1168
|
+
poolAddress,
|
|
1169
|
+
feeSink: prepared.feeSink,
|
|
1170
|
+
walletProof: input.walletProof
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
if (!input.poolAddress) {
|
|
1174
|
+
throw new ConfigError(
|
|
1175
|
+
"createPool on the live path needs poolAddress after the studio wallet submits the constructor \u2014 Playmos does not broadcast"
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
return this.registerPool({
|
|
1179
|
+
studioWallet: prepared.admin,
|
|
1180
|
+
poolAddress: input.poolAddress,
|
|
1181
|
+
feeSink: prepared.feeSink,
|
|
1182
|
+
txHash: input.txHash,
|
|
1183
|
+
walletProof: input.walletProof
|
|
1184
|
+
});
|
|
1185
|
+
},
|
|
1186
|
+
async settle(input) {
|
|
1187
|
+
const series = requireSeries(input?.series);
|
|
1188
|
+
if (!Array.isArray(input?.winners)) {
|
|
1189
|
+
throw new MissingFieldError("winners");
|
|
1190
|
+
}
|
|
1191
|
+
const epochId = epochPathId(input?.epochId);
|
|
1192
|
+
if (epochId === "current") {
|
|
1193
|
+
throw new ConfigError("epochs.settle requires a past epochId \u2014 Current is still taking entries");
|
|
1194
|
+
}
|
|
1195
|
+
const cfg = deps.config();
|
|
1196
|
+
if (!cfg.mock) {
|
|
1197
|
+
if (deps.assertSecretKey) deps.assertSecretKey("playmos.epochs.settle");
|
|
1198
|
+
else if (!String(cfg.apiKey ?? "").startsWith("sk_")) {
|
|
1199
|
+
throw new AuthError(
|
|
1200
|
+
"playmos.epochs.settle requires a secret (sk_) key \u2014 use sk_test_\u2026 / sk_live_\u2026 on the server. Publishable (pk_) keys cannot settle.",
|
|
1201
|
+
{ surface: "playmos.epochs.settle", isSecret: false }
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
if (cfg.mock) {
|
|
1206
|
+
const empty = input.winners.length === 0;
|
|
1207
|
+
const podiumPayable = mockPodiumPayable(input.winners);
|
|
1208
|
+
return {
|
|
1209
|
+
series,
|
|
1210
|
+
epochId,
|
|
1211
|
+
status: empty ? "rolled" : "settled",
|
|
1212
|
+
txHash: "0x0000000000000000000000000000000000000000000000000000000000000001",
|
|
1213
|
+
winners: input.winners.map((w) => {
|
|
1214
|
+
const wallet = (typeof w === "string" ? w : w.wallet).toLowerCase();
|
|
1215
|
+
return { wallet, ...mockWinnerAmount(w) };
|
|
1216
|
+
}),
|
|
1217
|
+
...podiumPayable ?? {},
|
|
1218
|
+
terminal: empty ? "rolled" : "settled",
|
|
1219
|
+
via: "mock",
|
|
1220
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
|
|
1221
|
+
mock: true
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
const body = await deps.http().post(
|
|
1225
|
+
`/epochs/${encodeURIComponent(epochId)}/settle`,
|
|
1226
|
+
{
|
|
1227
|
+
series,
|
|
1228
|
+
winners: input.winners,
|
|
1229
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
|
|
1230
|
+
},
|
|
1231
|
+
{ acceptStatuses: [200, 202] }
|
|
1232
|
+
);
|
|
1233
|
+
return body.settle;
|
|
1234
|
+
},
|
|
1235
|
+
async enter(input) {
|
|
1236
|
+
const series = requireSeries(input?.series);
|
|
1237
|
+
const identity = requireIdentity(input?.identity);
|
|
1238
|
+
const cfg = deps.config();
|
|
1239
|
+
const seriesBytes32 = seriesToBytes32(series);
|
|
1240
|
+
const identityBytes32 = identityToBytes32(identity);
|
|
1241
|
+
if (cfg.mock) {
|
|
1242
|
+
const genesis = toBig(input.genesis, 0n);
|
|
1243
|
+
const duration = toBig(input.epochDuration, 1n);
|
|
1244
|
+
const at = toBig(input.at, BigInt(Math.floor(Date.now() / 1e3)));
|
|
1245
|
+
return {
|
|
1246
|
+
series,
|
|
1247
|
+
identity,
|
|
1248
|
+
epochId: derivedEpochId(genesis, duration, at).toString(),
|
|
1249
|
+
epochIdSource: "mock",
|
|
1250
|
+
seriesBytes32,
|
|
1251
|
+
identityBytes32,
|
|
1252
|
+
entry: formatMicroToUsd(C1_ENTER_SPLIT_MICRO.entry),
|
|
1253
|
+
entryMicro: C1_ENTER_SPLIT_MICRO.entry.toString(),
|
|
1254
|
+
status: "confirmed",
|
|
1255
|
+
txHash: MOCK_TX,
|
|
1256
|
+
identityEntryCount: "1",
|
|
1257
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null,
|
|
1258
|
+
usdc: cfg.contracts?.usdc ?? null,
|
|
1259
|
+
via: "mock",
|
|
1260
|
+
mock: true
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
const epochPrizePool = input.epochPrizePool ?? cfg.contracts?.epochPrizePool;
|
|
1264
|
+
const plan = await deps.http().post("/epochs/enter", {
|
|
1265
|
+
series,
|
|
1266
|
+
identity,
|
|
1267
|
+
...epochPrizePool ? { epochPrizePool } : {}
|
|
1268
|
+
});
|
|
1269
|
+
assertPinParity("series", seriesBytes32, plan.clientParams?.seriesBytes32);
|
|
1270
|
+
assertPinParity("identity", identityBytes32, plan.clientParams?.identityBytes32);
|
|
1271
|
+
const send = deps.sendEntry;
|
|
1272
|
+
if (!send) {
|
|
1273
|
+
throw new ConfigError(
|
|
1274
|
+
"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`.",
|
|
1275
|
+
{ field: "wallet" }
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
const entryMicro = BigInt(plan.clientParams.entryMicro);
|
|
1279
|
+
const calls = buildEpochEntryCalls({
|
|
1280
|
+
usdc: plan.clientParams.usdc,
|
|
1281
|
+
epochPrizePool: plan.clientParams.epochPrizePool,
|
|
1282
|
+
series,
|
|
1283
|
+
identity,
|
|
1284
|
+
entryMicro
|
|
1285
|
+
});
|
|
1286
|
+
const sent = await send(calls, { paymasterUrl: plan.clientParams.paymasterUrl });
|
|
1287
|
+
const base = {
|
|
1288
|
+
series,
|
|
1289
|
+
identity,
|
|
1290
|
+
epochId: plan.entry.epochId,
|
|
1291
|
+
epochIdSource: "chain-clock",
|
|
1292
|
+
seriesBytes32,
|
|
1293
|
+
identityBytes32,
|
|
1294
|
+
entry: plan.entry.entry,
|
|
1295
|
+
entryMicro: plan.entry.entryMicro,
|
|
1296
|
+
status: sent.status === "FAILED" ? "failed" : "pending",
|
|
1297
|
+
txHash: sent.txHash,
|
|
1298
|
+
epochPrizePool: plan.clientParams.epochPrizePool,
|
|
1299
|
+
usdc: plan.clientParams.usdc,
|
|
1300
|
+
via: "service"
|
|
1301
|
+
};
|
|
1302
|
+
if (!sent.txHash) return base;
|
|
1303
|
+
try {
|
|
1304
|
+
const confirmed = await deps.http().post("/epochs/enter/confirm", {
|
|
1305
|
+
series,
|
|
1306
|
+
identity,
|
|
1307
|
+
txHash: sent.txHash,
|
|
1308
|
+
...epochPrizePool ? { epochPrizePool } : {}
|
|
1309
|
+
});
|
|
1310
|
+
return {
|
|
1311
|
+
...base,
|
|
1312
|
+
epochId: confirmed.entry.epochId,
|
|
1313
|
+
epochIdSource: confirmed.entry.epochIdSource,
|
|
1314
|
+
status: confirmed.entry.status,
|
|
1315
|
+
identityEntryCount: confirmed.entry.identityEntryCount
|
|
1316
|
+
};
|
|
1317
|
+
} catch {
|
|
1318
|
+
return base;
|
|
1319
|
+
}
|
|
1320
|
+
},
|
|
1321
|
+
async claimableRefund(input) {
|
|
1322
|
+
const series = requireSeries(input?.series);
|
|
1323
|
+
const payer = requireAddr(input?.payer, "payer");
|
|
1324
|
+
const epochId = requirePastEpochId(input?.epochId);
|
|
1325
|
+
const cfg = deps.config();
|
|
1326
|
+
const seriesBytes32 = seriesToBytes32(series);
|
|
1327
|
+
const epochPrizePool = input.epochPrizePool ?? cfg.contracts?.epochPrizePool ?? null;
|
|
1328
|
+
if (cfg.mock) {
|
|
1329
|
+
return {
|
|
1330
|
+
series,
|
|
1331
|
+
epochId,
|
|
1332
|
+
payer,
|
|
1333
|
+
claimable: "0.00",
|
|
1334
|
+
claimableMicro: "0",
|
|
1335
|
+
seriesBytes32,
|
|
1336
|
+
epochPrizePool,
|
|
1337
|
+
via: "mock",
|
|
1338
|
+
mock: true
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
const pool = requireEpochPool(input, cfg);
|
|
1342
|
+
const data = encodeFunctionData({
|
|
1343
|
+
abi: epochPrizePoolViewAbi,
|
|
1344
|
+
functionName: "claimableRefund",
|
|
1345
|
+
args: [seriesBytes32, BigInt(epochId), payer]
|
|
1346
|
+
});
|
|
1347
|
+
const read = deps.readView;
|
|
1348
|
+
if (!read) {
|
|
1349
|
+
throw new ConfigError(
|
|
1350
|
+
"epochs.claimableRefund needs a wallet or RPC to read EpochPrizePool \u2014 pass `wallet` to the Playmos client",
|
|
1351
|
+
{ field: "wallet" }
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
const raw = await read(pool, data);
|
|
1355
|
+
const amount = decodeFunctionResult({
|
|
1356
|
+
abi: epochPrizePoolViewAbi,
|
|
1357
|
+
functionName: "claimableRefund",
|
|
1358
|
+
data: raw
|
|
1359
|
+
});
|
|
1360
|
+
return {
|
|
1361
|
+
series,
|
|
1362
|
+
epochId,
|
|
1363
|
+
payer,
|
|
1364
|
+
claimable: formatMicroToUsd(amount),
|
|
1365
|
+
claimableMicro: amount.toString(),
|
|
1366
|
+
seriesBytes32,
|
|
1367
|
+
epochPrizePool: pool,
|
|
1368
|
+
via: "chain"
|
|
1369
|
+
};
|
|
1370
|
+
},
|
|
1371
|
+
async claimRefund(input) {
|
|
1372
|
+
const series = requireSeries(input?.series);
|
|
1373
|
+
const payer = requireAddr(input?.payer, "payer");
|
|
1374
|
+
const epochId = requirePastEpochId(input?.epochId);
|
|
1375
|
+
const cfg = deps.config();
|
|
1376
|
+
const pool = requireEpochPool(input, cfg);
|
|
1377
|
+
if (cfg.mock) {
|
|
1378
|
+
return {
|
|
1379
|
+
series,
|
|
1380
|
+
epochId,
|
|
1381
|
+
payer,
|
|
1382
|
+
txHash: MOCK_TX,
|
|
1383
|
+
status: "confirmed",
|
|
1384
|
+
epochPrizePool: pool,
|
|
1385
|
+
via: "mock",
|
|
1386
|
+
mock: true
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
const send = deps.sendEntry;
|
|
1390
|
+
if (!send) {
|
|
1391
|
+
throw new ConfigError(
|
|
1392
|
+
"epochs.claimRefund needs a player wallet \u2014 pass `wallet` to the Playmos client. The credit lands on `payer`; the connected wallet only signs.",
|
|
1393
|
+
{ field: "wallet" }
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
const call = buildEpochClaimRefundCall({
|
|
1397
|
+
epochPrizePool: pool,
|
|
1398
|
+
series,
|
|
1399
|
+
epochId: BigInt(epochId),
|
|
1400
|
+
payer
|
|
1401
|
+
});
|
|
1402
|
+
const sent = await send([call], {});
|
|
1403
|
+
return {
|
|
1404
|
+
series,
|
|
1405
|
+
epochId,
|
|
1406
|
+
payer,
|
|
1407
|
+
txHash: sent.txHash,
|
|
1408
|
+
status: walletStatus(sent.status),
|
|
1409
|
+
epochPrizePool: pool,
|
|
1410
|
+
via: "chain"
|
|
1411
|
+
};
|
|
1412
|
+
},
|
|
1413
|
+
async withdraw(input = {}) {
|
|
1414
|
+
const cfg = deps.config();
|
|
1415
|
+
const pool = requireEpochPool(input, cfg);
|
|
1416
|
+
if (cfg.mock) {
|
|
1417
|
+
return {
|
|
1418
|
+
epochPrizePool: pool,
|
|
1419
|
+
txHash: MOCK_TX,
|
|
1420
|
+
status: "confirmed",
|
|
1421
|
+
via: "mock",
|
|
1422
|
+
mock: true
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
const send = deps.sendEntry;
|
|
1426
|
+
if (!send) {
|
|
1427
|
+
throw new ConfigError(
|
|
1428
|
+
"epochs.withdraw needs the payer wallet \u2014 pass `wallet` to the Playmos client. USDC is pulled to msg.sender on EpochPrizePool, not PrizePool.",
|
|
1429
|
+
{ field: "wallet" }
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
const creditedMicro = await readWithdrawableMicro(deps, pool);
|
|
1433
|
+
const call = buildEpochWithdrawCall(pool);
|
|
1434
|
+
const sent = await send([call], {});
|
|
1435
|
+
return {
|
|
1436
|
+
epochPrizePool: pool,
|
|
1437
|
+
...creditedMicro === null ? {} : {
|
|
1438
|
+
amount: formatMicroToUsd(creditedMicro),
|
|
1439
|
+
amountMicro: creditedMicro.toString()
|
|
1440
|
+
},
|
|
1441
|
+
txHash: sent.txHash,
|
|
1442
|
+
status: walletStatus(sent.status),
|
|
1443
|
+
via: "chain"
|
|
1444
|
+
};
|
|
1445
|
+
},
|
|
1446
|
+
async getAttestation(input) {
|
|
1447
|
+
const series = requireSeries(input?.series);
|
|
1448
|
+
const epochId = requirePastEpochId(input?.epochId);
|
|
1449
|
+
const cfg = deps.config();
|
|
1450
|
+
if (cfg.mock) {
|
|
1451
|
+
const pool = input.epochPrizePool ?? cfg.contracts?.epochPrizePool;
|
|
1452
|
+
if (!pool) {
|
|
1453
|
+
throw new ConfigError("epochs.getAttestation needs contracts.epochPrizePool (or pass epochPrizePool)", {
|
|
1454
|
+
field: "epochPrizePool"
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
return {
|
|
1458
|
+
series,
|
|
1459
|
+
epochId,
|
|
1460
|
+
winners: input.winners ?? [],
|
|
1461
|
+
amountsMicro: input.amountsMicro ?? [],
|
|
1462
|
+
signature: input.signature ?? `0x${"00".repeat(65)}`,
|
|
1463
|
+
epochPrizePool: requireAddr(pool, "epochPrizePool"),
|
|
1464
|
+
via: "mock",
|
|
1465
|
+
mock: true
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
const path = `/epochs/${encodeURIComponent(epochId)}/attestation${qs({
|
|
1469
|
+
series,
|
|
1470
|
+
epochPrizePool: input.epochPrizePool ?? cfg.contracts?.epochPrizePool
|
|
1471
|
+
})}`;
|
|
1472
|
+
const res = await deps.http().get(path);
|
|
1473
|
+
const inner = res.attestation ?? res;
|
|
1474
|
+
if (!inner.series || !inner.winners || !inner.amountsMicro || !inner.signature || !inner.epochPrizePool) {
|
|
1475
|
+
throw new ConfigError("attestation response missing posted fields \u2014 refusing to invent a podium");
|
|
1476
|
+
}
|
|
1477
|
+
return {
|
|
1478
|
+
series: inner.series,
|
|
1479
|
+
epochId: String(inner.epochId ?? epochId),
|
|
1480
|
+
winners: inner.winners,
|
|
1481
|
+
amountsMicro: inner.amountsMicro,
|
|
1482
|
+
signature: inner.signature,
|
|
1483
|
+
epochPrizePool: inner.epochPrizePool,
|
|
1484
|
+
...res.terminal !== void 0 ? { terminal: res.terminal } : {},
|
|
1485
|
+
...res.dueAt !== void 0 ? { dueAt: res.dueAt } : {},
|
|
1486
|
+
via: res.via ?? "service"
|
|
1487
|
+
};
|
|
1488
|
+
},
|
|
1489
|
+
async executeSettlement(input) {
|
|
1490
|
+
const series = requireSeries(input?.series);
|
|
1491
|
+
const epochId = requirePastEpochId(input?.epochId);
|
|
1492
|
+
if (!Array.isArray(input?.winners)) throw new MissingFieldError("winners");
|
|
1493
|
+
if (!Array.isArray(input?.amounts)) throw new MissingFieldError("amounts");
|
|
1494
|
+
if (typeof input?.signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(input.signature)) {
|
|
1495
|
+
throw new MissingFieldError("signature");
|
|
1496
|
+
}
|
|
1497
|
+
const winners = input.winners.map((w, i) => requireAddrKeepCase(w, `winners[${i}]`));
|
|
1498
|
+
const amountsMicro = input.amounts.map((a, i) => {
|
|
1499
|
+
parseMicroString(a, `amounts[${i}]`);
|
|
1500
|
+
return a;
|
|
1501
|
+
});
|
|
1502
|
+
const amounts = amountsMicro.map((a, i) => parseMicroString(a, `amounts[${i}]`));
|
|
1503
|
+
const cfg = deps.config();
|
|
1504
|
+
const pool = requireEpochPool(input, cfg);
|
|
1505
|
+
if (cfg.mock) {
|
|
1506
|
+
return {
|
|
1507
|
+
series,
|
|
1508
|
+
epochId,
|
|
1509
|
+
winners,
|
|
1510
|
+
amountsMicro,
|
|
1511
|
+
signature: input.signature,
|
|
1512
|
+
txHash: MOCK_TX,
|
|
1513
|
+
status: "confirmed",
|
|
1514
|
+
epochPrizePool: pool,
|
|
1515
|
+
via: "mock",
|
|
1516
|
+
mock: true
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
const send = deps.sendEntry;
|
|
1520
|
+
if (!send) {
|
|
1521
|
+
throw new ConfigError(
|
|
1522
|
+
"epochs.executeSettlement needs a wallet \u2014 pass `wallet` to the Playmos client. The SDK relays the signed podium; it does not choose winners.",
|
|
1523
|
+
{ field: "wallet" }
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
const call = buildEpochExecuteSettlementCall({
|
|
1527
|
+
epochPrizePool: pool,
|
|
1528
|
+
series,
|
|
1529
|
+
epochId: BigInt(epochId),
|
|
1530
|
+
winners,
|
|
1531
|
+
amounts,
|
|
1532
|
+
signature: input.signature
|
|
1533
|
+
});
|
|
1534
|
+
let sent;
|
|
1535
|
+
try {
|
|
1536
|
+
sent = await send([call], {});
|
|
1537
|
+
} catch (e) {
|
|
1538
|
+
const msg = e?.message ?? String(e);
|
|
1539
|
+
throw new PaymentFailedError("EpochPrizePool.executeSignedSettlement failed.", { cause: msg });
|
|
1540
|
+
}
|
|
1541
|
+
const terminal = await readEpochTerminal(deps, pool, series, epochId);
|
|
1542
|
+
return {
|
|
1543
|
+
series,
|
|
1544
|
+
epochId,
|
|
1545
|
+
winners,
|
|
1546
|
+
amountsMicro,
|
|
1547
|
+
signature: input.signature,
|
|
1548
|
+
txHash: sent.txHash,
|
|
1549
|
+
status: walletStatus(sent.status),
|
|
1550
|
+
...terminal === null ? {} : { terminal, settled: terminal === "settled" },
|
|
1551
|
+
epochPrizePool: pool,
|
|
1552
|
+
via: "chain"
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
var C1_ENTER_SPLIT_MICRO = {
|
|
1558
|
+
entry: 1000000n,
|
|
1559
|
+
fee: 100000n,
|
|
1560
|
+
pool: 600000n,
|
|
1561
|
+
outgoingSeed: 300000n,
|
|
1562
|
+
incomingSeed: 0n
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
// src/x402.ts
|
|
1566
|
+
var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
|
|
647
1567
|
function requireAddress2(value, field) {
|
|
648
1568
|
if (typeof value !== "string" || value.trim() === "") {
|
|
649
1569
|
throw new MissingFieldError(field);
|
|
650
1570
|
}
|
|
651
|
-
if (!
|
|
1571
|
+
if (!ADDRESS_RE3.test(value)) {
|
|
652
1572
|
throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
|
|
653
1573
|
field,
|
|
654
1574
|
value
|
|
@@ -786,7 +1706,7 @@ function validateX402ChallengeInput(input) {
|
|
|
786
1706
|
}
|
|
787
1707
|
|
|
788
1708
|
// src/client.ts
|
|
789
|
-
var
|
|
1709
|
+
var ADDRESS_RE4 = /^0x[0-9a-fA-F]{40}$/;
|
|
790
1710
|
function mapAlreadyEntered(e) {
|
|
791
1711
|
const msg = e instanceof Error ? e.message : typeof e === "object" && e && "message" in e ? String(e.message) : String(e);
|
|
792
1712
|
const detail = e instanceof PaymentFailedError || e instanceof ApiError ? e.detail : void 0;
|
|
@@ -834,7 +1754,7 @@ function requireAddressField(value, field) {
|
|
|
834
1754
|
if (typeof value !== "string" || value.trim() === "") {
|
|
835
1755
|
throw new MissingFieldError(field);
|
|
836
1756
|
}
|
|
837
|
-
if (!
|
|
1757
|
+
if (!ADDRESS_RE4.test(value)) {
|
|
838
1758
|
throw new ConfigError(
|
|
839
1759
|
`${field} must be a 0x-prefixed 20-byte wallet address (Phase 1a transfers settle server-held wallets), got: ${JSON.stringify(value)}`,
|
|
840
1760
|
{ field, value }
|
|
@@ -873,6 +1793,56 @@ var Playmos = class {
|
|
|
873
1793
|
);
|
|
874
1794
|
}
|
|
875
1795
|
};
|
|
1796
|
+
/**
|
|
1797
|
+
* Rolling epochs — `currentId` / `prize` / `get` / `getSeries` (sdk#629),
|
|
1798
|
+
* `enter` (sdk#635 / S2), operator `settle` (sdk#639 / S3), the C3
|
|
1799
|
+
* refund pull (sdk#634), and E1b `getAttestation` / `executeSettlement`
|
|
1800
|
+
* (wallet-direct via the same `walletProvider()` funnel as `rounds.withdraw`).
|
|
1801
|
+
* Reads/settle go through the service. Refund claim, withdraw, and signed
|
|
1802
|
+
* execute are wallet-direct on EpochPrizePool — not PrizePool.
|
|
1803
|
+
* Nothing here opens a round.
|
|
1804
|
+
*/
|
|
1805
|
+
this.epochs = createEpochsApi({
|
|
1806
|
+
http: () => this.http,
|
|
1807
|
+
config: () => this.config,
|
|
1808
|
+
assertSecretKey: (surface) => this.assertSecretKey(surface),
|
|
1809
|
+
sendEntry: async (calls, opts) => {
|
|
1810
|
+
const provider = this.walletProvider();
|
|
1811
|
+
if (this.config.gas?.mode === "player") {
|
|
1812
|
+
await assertEnoughGas(provider, await getAccount(provider));
|
|
1813
|
+
}
|
|
1814
|
+
const from = await getAccount(provider);
|
|
1815
|
+
const paymasterUrl = this.config.gas?.mode === "player" ? void 0 : this.config.gas?.paymasterUrl ?? opts.paymasterUrl;
|
|
1816
|
+
const { id } = await sendCalls(provider, from, this.env.chainId, calls, paymasterUrl);
|
|
1817
|
+
return waitForCalls(provider, id);
|
|
1818
|
+
},
|
|
1819
|
+
walletAddress: async () => getAccount(this.walletProvider()),
|
|
1820
|
+
readView: async (to, data) => {
|
|
1821
|
+
if (await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
|
|
1822
|
+
const provider = this.walletProvider();
|
|
1823
|
+
return await provider.request({
|
|
1824
|
+
method: "eth_call",
|
|
1825
|
+
params: [{ to, data }, "latest"]
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
const rpc = this.env.network === "base" ? "https://mainnet.base.org" : "https://sepolia.base.org";
|
|
1829
|
+
const res = await fetch(rpc, {
|
|
1830
|
+
method: "POST",
|
|
1831
|
+
headers: { "content-type": "application/json" },
|
|
1832
|
+
body: JSON.stringify({
|
|
1833
|
+
jsonrpc: "2.0",
|
|
1834
|
+
id: 1,
|
|
1835
|
+
method: "eth_call",
|
|
1836
|
+
params: [{ to, data }, "latest"]
|
|
1837
|
+
})
|
|
1838
|
+
});
|
|
1839
|
+
const json = await res.json();
|
|
1840
|
+
if (!json.result) {
|
|
1841
|
+
throw new ApiError(`eth_call failed: ${json.error?.message ?? "no result"}`, { to });
|
|
1842
|
+
}
|
|
1843
|
+
return json.result;
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
876
1846
|
this.payouts = {
|
|
877
1847
|
/** Choose how the studio is paid: "usdc" (default) or "fiat" (Bridge). Secret key only (#264). */
|
|
878
1848
|
setMode: async (mode) => {
|
|
@@ -1038,6 +2008,8 @@ var Playmos = class {
|
|
|
1038
2008
|
this.mockPotMicro = /* @__PURE__ */ new Map();
|
|
1039
2009
|
/** Persisted settle winners for re-settle replay (#490 C2) — never re-invent pool-to-everyone. */
|
|
1040
2010
|
this.mockSettleWinners = /* @__PURE__ */ new Map();
|
|
2011
|
+
/** L34 notices after mock settle — funded only with a push txHash. */
|
|
2012
|
+
this.mockPayoutNotices = /* @__PURE__ */ new Map();
|
|
1041
2013
|
this.rounds = {
|
|
1042
2014
|
open: async (input) => {
|
|
1043
2015
|
if (!this.config.mock) this.assertSecretKey("playmos.rounds.open");
|
|
@@ -1203,6 +2175,16 @@ var Playmos = class {
|
|
|
1203
2175
|
};
|
|
1204
2176
|
this.mockRounds.set(input.roundId, settled);
|
|
1205
2177
|
this.mockSettleWinners.set(input.roundId, winners);
|
|
2178
|
+
this.mockPayoutNotices.set(
|
|
2179
|
+
input.roundId,
|
|
2180
|
+
rows.filter((w) => w.micro > 0n).map((w) => ({
|
|
2181
|
+
roundId: input.roundId,
|
|
2182
|
+
wallet: w.wallet.toLowerCase(),
|
|
2183
|
+
amountMicro: w.micro.toString(),
|
|
2184
|
+
status: "claimable",
|
|
2185
|
+
reason: "prizepool_pull_credit"
|
|
2186
|
+
}))
|
|
2187
|
+
);
|
|
1206
2188
|
for (const w of rows) {
|
|
1207
2189
|
if (w.micro <= 0n) continue;
|
|
1208
2190
|
const k = `${input.roundId}:${w.wallet.toLowerCase()}`;
|
|
@@ -1444,6 +2426,93 @@ var Playmos = class {
|
|
|
1444
2426
|
}
|
|
1445
2427
|
return body.active;
|
|
1446
2428
|
},
|
|
2429
|
+
/**
|
|
2430
|
+
* L34 / sdk#610 — notifications for a settled round.
|
|
2431
|
+
* Each win is `funded` (push txHash) or `claimable` (withdraw fallback).
|
|
2432
|
+
* Never returns `funded` without a transfer txHash.
|
|
2433
|
+
*/
|
|
2434
|
+
payoutNotices: async (input) => {
|
|
2435
|
+
requireField(input?.roundId, "roundId");
|
|
2436
|
+
if (this.config.mock) {
|
|
2437
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
2438
|
+
if (!existing) {
|
|
2439
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
2440
|
+
}
|
|
2441
|
+
let notices = this.mockPayoutNotices.get(input.roundId) ?? [];
|
|
2442
|
+
if (input.wallet) {
|
|
2443
|
+
if (!ADDRESS_RE4.test(input.wallet)) {
|
|
2444
|
+
throw new ConfigError("wallet must be a 0x-prefixed 20-byte address");
|
|
2445
|
+
}
|
|
2446
|
+
const want = input.wallet.toLowerCase();
|
|
2447
|
+
notices = notices.filter((n) => n.wallet === want);
|
|
2448
|
+
}
|
|
2449
|
+
for (const n of notices) {
|
|
2450
|
+
if (n.status === "funded" && !n.txHash) {
|
|
2451
|
+
throw new ApiError("cannot label funded without a transfer txHash", {
|
|
2452
|
+
status: 500,
|
|
2453
|
+
code: "payout_notice_invalid"
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
return { roundId: input.roundId, status: existing.status, notices };
|
|
2458
|
+
}
|
|
2459
|
+
const q = input.wallet ? `?wallet=${encodeURIComponent(input.wallet)}` : "";
|
|
2460
|
+
return this.http.get(
|
|
2461
|
+
`/rounds/${encodeURIComponent(input.roundId)}/payout-notices${q}`
|
|
2462
|
+
);
|
|
2463
|
+
},
|
|
2464
|
+
/**
|
|
2465
|
+
* Operator push-attempt after settle (L34). Mock: mark a winner funded only
|
|
2466
|
+
* when a real-shaped txHash is supplied; otherwise that row stays claimable.
|
|
2467
|
+
*/
|
|
2468
|
+
pushWinnings: async (input) => {
|
|
2469
|
+
requireField(input?.roundId, "roundId");
|
|
2470
|
+
if (!this.config.mock) this.assertSecretKey("playmos.rounds.pushWinnings");
|
|
2471
|
+
if (this.config.mock) {
|
|
2472
|
+
const existing = this.mockRounds.get(input.roundId);
|
|
2473
|
+
if (!existing) {
|
|
2474
|
+
throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
|
|
2475
|
+
}
|
|
2476
|
+
if (existing.status !== "settled") {
|
|
2477
|
+
throw new ApiError(`round ${input.roundId} is not settled \u2014 push after the clock`, {
|
|
2478
|
+
status: 409,
|
|
2479
|
+
code: "conflict"
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
const prior = this.mockPayoutNotices.get(input.roundId) ?? [];
|
|
2483
|
+
const byWallet = new Map(
|
|
2484
|
+
(input.pushes ?? []).map((p) => [p.wallet.toLowerCase(), p])
|
|
2485
|
+
);
|
|
2486
|
+
const TX_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
2487
|
+
const ZERO = "0x" + "00".repeat(32);
|
|
2488
|
+
const notices = prior.map((n) => {
|
|
2489
|
+
const p = byWallet.get(n.wallet);
|
|
2490
|
+
if (p && !p.failed && p.txHash && TX_RE.test(p.txHash) && p.txHash.toLowerCase() !== ZERO) {
|
|
2491
|
+
return {
|
|
2492
|
+
...n,
|
|
2493
|
+
status: "funded",
|
|
2494
|
+
txHash: p.txHash.toLowerCase(),
|
|
2495
|
+
reason: void 0
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
if (p && (p.failed || !p.txHash || !TX_RE.test(p.txHash) || p.txHash.toLowerCase() === ZERO)) {
|
|
2499
|
+
return {
|
|
2500
|
+
...n,
|
|
2501
|
+
status: "claimable",
|
|
2502
|
+
txHash: void 0,
|
|
2503
|
+
reason: p.failed ? "push_failed" : "push_missing_txHash"
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
return n;
|
|
2507
|
+
});
|
|
2508
|
+
this.mockPayoutNotices.set(input.roundId, notices);
|
|
2509
|
+
return { roundId: input.roundId, status: existing.status, notices };
|
|
2510
|
+
}
|
|
2511
|
+
return this.http.post(
|
|
2512
|
+
`/rounds/${encodeURIComponent(input.roundId)}/push-winnings`,
|
|
2513
|
+
{}
|
|
2514
|
+
);
|
|
2515
|
+
},
|
|
1447
2516
|
/**
|
|
1448
2517
|
* Read a wallet's **claimable** prize balance for a round (issue #41).
|
|
1449
2518
|
* Prefers the service chain read (`GET /v1/rounds/:id/prize?wallet=`); falls
|
|
@@ -1500,7 +2569,7 @@ var Playmos = class {
|
|
|
1500
2569
|
const existing = this.mockRounds.get(input.roundId);
|
|
1501
2570
|
if (existing?.prizePoolAddress) prizePool2 = existing.prizePoolAddress;
|
|
1502
2571
|
}
|
|
1503
|
-
if (!input.wallet || !
|
|
2572
|
+
if (!input.wallet || !ADDRESS_RE4.test(input.wallet)) {
|
|
1504
2573
|
throw new ConfigError(
|
|
1505
2574
|
"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
2575
|
);
|
|
@@ -2111,7 +3180,7 @@ var Playmos = class {
|
|
|
2111
3180
|
const { txHash } = await waitForCalls(provider, callsId);
|
|
2112
3181
|
return this.settle(intent.payment.id, txHash);
|
|
2113
3182
|
}
|
|
2114
|
-
/**
|
|
3183
|
+
/** 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. */
|
|
2115
3184
|
async enterRound(input) {
|
|
2116
3185
|
const amountMicro = validateAmount(input.amount);
|
|
2117
3186
|
requireField(input.gameId, "gameId");
|
|
@@ -2603,13 +3672,13 @@ function previewPoolSplit(amount) {
|
|
|
2603
3672
|
}
|
|
2604
3673
|
|
|
2605
3674
|
// src/settlement.ts
|
|
2606
|
-
var
|
|
3675
|
+
var ADDRESS_RE5 = /^0x[0-9a-fA-F]{40}$/;
|
|
2607
3676
|
var DEFAULT_TTL_MS = 15 * 60 * 1e3;
|
|
2608
3677
|
function requireAddress3(value, field) {
|
|
2609
3678
|
if (typeof value !== "string" || value.trim() === "") {
|
|
2610
3679
|
throw new MissingFieldError(field);
|
|
2611
3680
|
}
|
|
2612
|
-
if (!
|
|
3681
|
+
if (!ADDRESS_RE5.test(value)) {
|
|
2613
3682
|
throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
|
|
2614
3683
|
field,
|
|
2615
3684
|
value
|
|
@@ -2695,4 +3764,4 @@ function isX402PayloadAuthorization(auth) {
|
|
|
2695
3764
|
return auth.kind === "x402-payload";
|
|
2696
3765
|
}
|
|
2697
3766
|
|
|
2698
|
-
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 };
|
|
3767
|
+
export { C1_ENTER_SPLIT_MICRO, CHAIN_ID, DEFAULT_API_BASE_URL, MICRO_PER_USDC, PLAYMOS_FEE_SINK_DEFAULT, PayoutError, Playmos, RETIRED_PLAYMOS_FEE_SINK, 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 };
|