@playmos/sdk 0.3.10 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +189 -0
- package/README.md +3 -0
- package/dist/chunk-35WJANW4.js +255 -0
- package/dist/{errors-Chizbb96.d.cts → errors-Dpeesyop.d.cts} +54 -4
- package/dist/{errors-Chizbb96.d.ts → errors-Dpeesyop.d.ts} +54 -4
- package/dist/index.cjs +137 -16
- package/dist/index.d.cts +28 -7
- package/dist/index.d.ts +28 -7
- package/dist/index.js +140 -187
- package/dist/server.cjs +117 -0
- package/dist/server.d.cts +34 -2
- package/dist/server.d.ts +34 -2
- package/dist/server.js +110 -2
- package/package.json +4 -8
- package/dist/chunk-UZECDT6F.js +0 -83
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AuthError, InvalidAmountError, ConfigError, MissingFieldError, NothingToWithdrawError, WalletTimeoutError, PaymentFailedError, ApiError,
|
|
2
|
-
export { AlreadyEnteredError, ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError, WalletTimeoutError } from './chunk-
|
|
3
|
-
import { encodeFunctionData, decodeFunctionResult
|
|
1
|
+
import { AuthError, InvalidAmountError, ConfigError, MissingFieldError, NothingToWithdrawError, assertEnoughGas, buildWithdrawCall, sendCalls, waitForCalls, WalletTimeoutError, PaymentFailedError, ApiError, buildIapCalls, buildEntryCalls, prizePoolAbi, WalletConnectionError, AlreadyEnteredError } from './chunk-35WJANW4.js';
|
|
2
|
+
export { AlreadyEnteredError, ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError, WalletTimeoutError } from './chunk-35WJANW4.js';
|
|
3
|
+
import { encodeFunctionData, decodeFunctionResult } from 'viem';
|
|
4
4
|
|
|
5
5
|
// src/config.ts
|
|
6
6
|
var CHAIN_ID = {
|
|
@@ -183,10 +183,10 @@ function validateMetadata(metadata) {
|
|
|
183
183
|
|
|
184
184
|
// src/payout.ts
|
|
185
185
|
var PayoutError = class extends Error {
|
|
186
|
-
constructor(message) {
|
|
186
|
+
constructor(message, code = "payout_invalid") {
|
|
187
187
|
super(message);
|
|
188
|
-
this.code = "payout_invalid";
|
|
189
188
|
this.name = "PayoutError";
|
|
189
|
+
this.code = code;
|
|
190
190
|
}
|
|
191
191
|
};
|
|
192
192
|
var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
@@ -231,7 +231,8 @@ function computePayout(pool, ranking, rule) {
|
|
|
231
231
|
}
|
|
232
232
|
if (splits.length > wallets.length) {
|
|
233
233
|
throw new PayoutError(
|
|
234
|
-
`top-n
|
|
234
|
+
`ranking_too_short: a top-n rule cannot be settled from a ranking shorter than splitsBps.length (need ${splits.length}, got ${wallets.length}) \u2014 open with winner-take-all, or settle with explicit winners[{wallet,amount}]`,
|
|
235
|
+
"ranking_too_short"
|
|
235
236
|
);
|
|
236
237
|
}
|
|
237
238
|
let sumBps = 0;
|
|
@@ -544,7 +545,7 @@ function resolveRawProvider(wallet) {
|
|
|
544
545
|
}
|
|
545
546
|
if (injected) return injected;
|
|
546
547
|
throw new WalletConnectionError(
|
|
547
|
-
|
|
548
|
+
'base-account connector needs a provider \u2014 this is not a built-in Base Account/passkey flow in @playmos/sdk. In the Base App a provider is often injected; elsewhere install @base-org/account, construct a provider, and pass it as wallet.provider (or use connector: "injected" in a wallet browser).'
|
|
548
549
|
);
|
|
549
550
|
}
|
|
550
551
|
function resolveProvider(wallet, opts) {
|
|
@@ -581,174 +582,6 @@ async function getAccount(provider) {
|
|
|
581
582
|
}
|
|
582
583
|
}
|
|
583
584
|
|
|
584
|
-
// src/chain/abis.ts
|
|
585
|
-
var erc20Abi = [
|
|
586
|
-
{
|
|
587
|
-
type: "function",
|
|
588
|
-
name: "approve",
|
|
589
|
-
stateMutability: "nonpayable",
|
|
590
|
-
inputs: [
|
|
591
|
-
{ name: "spender", type: "address" },
|
|
592
|
-
{ name: "amount", type: "uint256" }
|
|
593
|
-
],
|
|
594
|
-
outputs: [{ type: "bool" }]
|
|
595
|
-
}
|
|
596
|
-
];
|
|
597
|
-
var playmosPayAbi = [
|
|
598
|
-
{
|
|
599
|
-
type: "function",
|
|
600
|
-
name: "pay",
|
|
601
|
-
stateMutability: "nonpayable",
|
|
602
|
-
inputs: [
|
|
603
|
-
{ name: "paymentId", type: "bytes32" },
|
|
604
|
-
{ name: "studio", type: "address" },
|
|
605
|
-
{ name: "amount", type: "uint256" }
|
|
606
|
-
],
|
|
607
|
-
outputs: []
|
|
608
|
-
}
|
|
609
|
-
];
|
|
610
|
-
var prizePoolAbi = [
|
|
611
|
-
{
|
|
612
|
-
type: "function",
|
|
613
|
-
name: "enter",
|
|
614
|
-
stateMutability: "nonpayable",
|
|
615
|
-
inputs: [
|
|
616
|
-
{ name: "roundId", type: "bytes32" },
|
|
617
|
-
{ name: "identity", type: "bytes32" }
|
|
618
|
-
],
|
|
619
|
-
outputs: []
|
|
620
|
-
},
|
|
621
|
-
{
|
|
622
|
-
type: "function",
|
|
623
|
-
name: "withdrawable",
|
|
624
|
-
stateMutability: "view",
|
|
625
|
-
inputs: [{ name: "account", type: "address" }],
|
|
626
|
-
outputs: [{ type: "uint256" }]
|
|
627
|
-
},
|
|
628
|
-
{
|
|
629
|
-
type: "function",
|
|
630
|
-
name: "withdraw",
|
|
631
|
-
stateMutability: "nonpayable",
|
|
632
|
-
inputs: [],
|
|
633
|
-
outputs: [{ name: "amount", type: "uint256" }]
|
|
634
|
-
}
|
|
635
|
-
];
|
|
636
|
-
async function sendCalls(provider, from, chainId, calls, paymasterUrl) {
|
|
637
|
-
const params = {
|
|
638
|
-
version: "2.0.0",
|
|
639
|
-
from,
|
|
640
|
-
chainId: numberToHex(chainId),
|
|
641
|
-
atomicRequired: true,
|
|
642
|
-
calls
|
|
643
|
-
};
|
|
644
|
-
if (paymasterUrl) {
|
|
645
|
-
params.capabilities = { paymasterService: { url: paymasterUrl } };
|
|
646
|
-
}
|
|
647
|
-
let result;
|
|
648
|
-
try {
|
|
649
|
-
result = await provider.request({ method: "wallet_sendCalls", params: [params] });
|
|
650
|
-
} catch (e) {
|
|
651
|
-
if (e instanceof WalletTimeoutError) throw e;
|
|
652
|
-
const msg = e?.message ?? String(e);
|
|
653
|
-
if (/reject|denied|cancel|closed/i.test(msg)) {
|
|
654
|
-
throw new WalletConnectionError("The player cancelled or closed the payment sheet.", { cause: msg });
|
|
655
|
-
}
|
|
656
|
-
throw new PaymentFailedError("wallet_sendCalls failed.", { cause: msg });
|
|
657
|
-
}
|
|
658
|
-
const id = typeof result === "string" ? result : result?.id ?? "";
|
|
659
|
-
if (!id) throw new PaymentFailedError("Wallet returned no calls id.");
|
|
660
|
-
return { id };
|
|
661
|
-
}
|
|
662
|
-
async function waitForCalls(provider, id, timeoutMs = 6e4) {
|
|
663
|
-
if (!id) return { status: "FAILED" };
|
|
664
|
-
const deadline = Date.now() + timeoutMs;
|
|
665
|
-
while (Date.now() < deadline) {
|
|
666
|
-
try {
|
|
667
|
-
const res = await provider.request({
|
|
668
|
-
method: "wallet_getCallsStatus",
|
|
669
|
-
params: [id]
|
|
670
|
-
});
|
|
671
|
-
const s = String(res?.status ?? "").toUpperCase();
|
|
672
|
-
const txHash = res?.receipts?.[0]?.transactionHash;
|
|
673
|
-
if (s === "200" || s === "CONFIRMED" || s === "SUCCESS") return { status: "CONFIRMED", txHash };
|
|
674
|
-
if (s === "400" || s === "500" || s === "FAILED" || s === "REVERTED") return { status: "FAILED", txHash };
|
|
675
|
-
} catch (e) {
|
|
676
|
-
if (e instanceof WalletTimeoutError) {
|
|
677
|
-
await new Promise((r) => setTimeout(r, 900));
|
|
678
|
-
continue;
|
|
679
|
-
}
|
|
680
|
-
throw e;
|
|
681
|
-
}
|
|
682
|
-
await new Promise((r) => setTimeout(r, 900));
|
|
683
|
-
}
|
|
684
|
-
return { status: "PENDING" };
|
|
685
|
-
}
|
|
686
|
-
function encodeApprove(spender, amountUnits) {
|
|
687
|
-
return encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [spender, amountUnits] });
|
|
688
|
-
}
|
|
689
|
-
var GAS_FLOOR_WEI_DEFAULT = 200000000000000n;
|
|
690
|
-
var GAS_FLOOR_WEI_BASE_SEPOLIA = 50000000000000n;
|
|
691
|
-
var CHAIN_ID_BASE_SEPOLIA = 84532;
|
|
692
|
-
function gasFloorWei(chainId) {
|
|
693
|
-
return chainId === CHAIN_ID_BASE_SEPOLIA ? GAS_FLOOR_WEI_BASE_SEPOLIA : GAS_FLOOR_WEI_DEFAULT;
|
|
694
|
-
}
|
|
695
|
-
async function assertEnoughGas(provider, from, minWei) {
|
|
696
|
-
let floor = minWei;
|
|
697
|
-
if (floor === void 0) {
|
|
698
|
-
let chainId = 0;
|
|
699
|
-
try {
|
|
700
|
-
const hex = await provider.request({ method: "eth_chainId", params: [] });
|
|
701
|
-
chainId = Number(BigInt(hex));
|
|
702
|
-
} catch {
|
|
703
|
-
}
|
|
704
|
-
floor = gasFloorWei(chainId);
|
|
705
|
-
}
|
|
706
|
-
let balance;
|
|
707
|
-
try {
|
|
708
|
-
const hex = await provider.request({ method: "eth_getBalance", params: [from, "latest"] });
|
|
709
|
-
balance = BigInt(hex);
|
|
710
|
-
} catch {
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
if (balance < floor) {
|
|
714
|
-
throw new InsufficientGasError({
|
|
715
|
-
balanceWei: balance.toString(),
|
|
716
|
-
minWei: floor.toString()
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
var toBytes32 = (s) => keccak256(toBytes(s));
|
|
721
|
-
function buildIapCalls(args) {
|
|
722
|
-
const payData = encodeFunctionData({
|
|
723
|
-
abi: playmosPayAbi,
|
|
724
|
-
functionName: "pay",
|
|
725
|
-
args: [toBytes32(args.paymentId), args.studio, args.amountUnits]
|
|
726
|
-
});
|
|
727
|
-
return [
|
|
728
|
-
{ to: args.usdc, data: encodeApprove(args.playmosPay, args.amountUnits) },
|
|
729
|
-
{ to: args.playmosPay, data: payData }
|
|
730
|
-
];
|
|
731
|
-
}
|
|
732
|
-
function buildEntryCalls(args) {
|
|
733
|
-
const enterData = encodeFunctionData({
|
|
734
|
-
abi: prizePoolAbi,
|
|
735
|
-
functionName: "enter",
|
|
736
|
-
args: [toBytes32(args.roundKey), toBytes32(args.identity)]
|
|
737
|
-
});
|
|
738
|
-
return [
|
|
739
|
-
{ to: args.usdc, data: encodeApprove(args.prizePool, args.amountUnits) },
|
|
740
|
-
{ to: args.prizePool, data: enterData }
|
|
741
|
-
];
|
|
742
|
-
}
|
|
743
|
-
function buildWithdrawCall(prizePool) {
|
|
744
|
-
const data = encodeFunctionData({
|
|
745
|
-
abi: prizePoolAbi,
|
|
746
|
-
functionName: "withdraw",
|
|
747
|
-
args: []
|
|
748
|
-
});
|
|
749
|
-
return { to: prizePool, data };
|
|
750
|
-
}
|
|
751
|
-
|
|
752
585
|
// src/mock.ts
|
|
753
586
|
var MOCK_TX = `0x${"0".repeat(56)}deadbeef`;
|
|
754
587
|
function mockIapPayment(args) {
|
|
@@ -1953,8 +1786,16 @@ var Playmos = class {
|
|
|
1953
1786
|
/**
|
|
1954
1787
|
* Offline mock payments for this client instance — so `verify(id)` after
|
|
1955
1788
|
* `mock: true` pay/enterRound does not hit the live API (#204).
|
|
1789
|
+
* Keyed by payment id for verify.
|
|
1956
1790
|
*/
|
|
1957
1791
|
this.mockPayments = /* @__PURE__ */ new Map();
|
|
1792
|
+
/**
|
|
1793
|
+
* sdk#513 / residual B1–B2 — held idempotencyKey offline (live contract rehearsal).
|
|
1794
|
+
* Keyed like service: `${gameId}:${key}` (IAP without gameId uses empty gameId).
|
|
1795
|
+
* Stores terms so same key + different money throws (live IdempotencyConflict), not silent replay.
|
|
1796
|
+
* Auto-minted keys still unique per call (not stored for cross-call replay).
|
|
1797
|
+
*/
|
|
1798
|
+
this.mockByIdempotencyKey = /* @__PURE__ */ new Map();
|
|
1958
1799
|
/**
|
|
1959
1800
|
* `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
|
|
1960
1801
|
* `get` reconciles once against chain truth; `wait` polls it to a terminal state
|
|
@@ -2139,18 +1980,62 @@ var Playmos = class {
|
|
|
2139
1980
|
}
|
|
2140
1981
|
};
|
|
2141
1982
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
1983
|
+
mockIdemScopeKey(gameId, heldKey) {
|
|
1984
|
+
return `${gameId ?? ""}:${heldKey}`;
|
|
1985
|
+
}
|
|
1986
|
+
rememberMock(payment, held) {
|
|
1987
|
+
if (payment.mock) {
|
|
1988
|
+
this.mockPayments.set(payment.id, payment);
|
|
1989
|
+
if (held) {
|
|
1990
|
+
this.mockByIdempotencyKey.set(this.mockIdemScopeKey(held.terms.gameId, held.key), {
|
|
1991
|
+
payment,
|
|
1992
|
+
terms: held.terms
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
2144
1996
|
return payment;
|
|
2145
1997
|
}
|
|
1998
|
+
/** Live-shaped conflict when held mock key is reused with different money terms (sdk#513 residual B1). */
|
|
1999
|
+
mockIdemReplayOrThrow(heldKey, terms) {
|
|
2000
|
+
const prior = this.mockByIdempotencyKey.get(this.mockIdemScopeKey(terms.gameId, heldKey));
|
|
2001
|
+
if (!prior) return null;
|
|
2002
|
+
const t = prior.terms;
|
|
2003
|
+
const same = t.kind === terms.kind && t.amount === terms.amount && t.gameId === terms.gameId && t.playerId === terms.playerId && t.roundId === terms.roundId && t.identity === terms.identity && t.sku === terms.sku;
|
|
2004
|
+
if (!same) {
|
|
2005
|
+
throw new ApiError(
|
|
2006
|
+
`idempotencyKey was already used for a payment with DIFFERENT terms (kind/amount/gameId/playerId/roundId/identity/sku); use a fresh key (mock:true mirrors live conflict \u2014 sdk#513).`,
|
|
2007
|
+
{
|
|
2008
|
+
code: "idempotency_conflict",
|
|
2009
|
+
idempotencyKey: heldKey,
|
|
2010
|
+
priorKind: t.kind,
|
|
2011
|
+
priorAmount: t.amount
|
|
2012
|
+
}
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
return prior.payment;
|
|
2016
|
+
}
|
|
2146
2017
|
/** External-studio IAP (1%). Returns a real confirmed Payment with a txHash. */
|
|
2147
2018
|
async pay(input) {
|
|
2148
2019
|
const amountMicro = validateAmount(input.amount);
|
|
2149
2020
|
requireField(input.sku, "sku");
|
|
2150
2021
|
requireField(input.playerId, "playerId");
|
|
2151
2022
|
const metadata = validateMetadata(input.metadata);
|
|
2152
|
-
const
|
|
2023
|
+
const heldIdem = typeof input.idempotencyKey === "string" && input.idempotencyKey.trim() ? input.idempotencyKey.trim() : void 0;
|
|
2024
|
+
const idempotencyKey = heldIdem ?? prefixedId("idem");
|
|
2153
2025
|
if (this.config.mock) {
|
|
2026
|
+
const terms = {
|
|
2027
|
+
kind: "iap",
|
|
2028
|
+
amount: formatMicroToUsd(amountMicro),
|
|
2029
|
+
gameId: input.gameId ?? "",
|
|
2030
|
+
playerId: input.playerId,
|
|
2031
|
+
roundId: "",
|
|
2032
|
+
identity: "",
|
|
2033
|
+
sku: input.sku.trim()
|
|
2034
|
+
};
|
|
2035
|
+
if (heldIdem) {
|
|
2036
|
+
const replay = this.mockIdemReplayOrThrow(heldIdem, terms);
|
|
2037
|
+
if (replay) return replay;
|
|
2038
|
+
}
|
|
2154
2039
|
return this.rememberMock(
|
|
2155
2040
|
mockIapPayment({
|
|
2156
2041
|
amountMicro,
|
|
@@ -2159,7 +2044,8 @@ var Playmos = class {
|
|
|
2159
2044
|
playerId: input.playerId,
|
|
2160
2045
|
chain: this.env.network,
|
|
2161
2046
|
metadata
|
|
2162
|
-
})
|
|
2047
|
+
}),
|
|
2048
|
+
heldIdem ? { key: heldIdem, terms } : void 0
|
|
2163
2049
|
);
|
|
2164
2050
|
}
|
|
2165
2051
|
if (this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
|
|
@@ -2181,7 +2067,13 @@ var Playmos = class {
|
|
|
2181
2067
|
);
|
|
2182
2068
|
let payment = this.mapServerPayment(res.payment, "iap", amountMicro);
|
|
2183
2069
|
if (payment.status === "pending" || payment.status === "created") {
|
|
2184
|
-
payment = await this.waitForServerSettle(
|
|
2070
|
+
payment = await this.waitForServerSettle(
|
|
2071
|
+
payment.id,
|
|
2072
|
+
"iap",
|
|
2073
|
+
amountMicro,
|
|
2074
|
+
input.settleTimeoutMs ?? 6e4,
|
|
2075
|
+
input.signal
|
|
2076
|
+
);
|
|
2185
2077
|
}
|
|
2186
2078
|
return payment;
|
|
2187
2079
|
}
|
|
@@ -2226,8 +2118,22 @@ var Playmos = class {
|
|
|
2226
2118
|
requireField(input.roundId, "roundId");
|
|
2227
2119
|
requireField(input.playerId, "playerId");
|
|
2228
2120
|
const metadata = validateMetadata(input.metadata);
|
|
2229
|
-
const
|
|
2121
|
+
const heldIdem = typeof input.idempotencyKey === "string" && input.idempotencyKey.trim() ? input.idempotencyKey.trim() : void 0;
|
|
2122
|
+
const idempotencyKey = heldIdem ?? prefixedId("idem");
|
|
2230
2123
|
if (this.config.mock) {
|
|
2124
|
+
const terms = {
|
|
2125
|
+
kind: "entry",
|
|
2126
|
+
amount: formatMicroToUsd(amountMicro),
|
|
2127
|
+
gameId: input.gameId,
|
|
2128
|
+
playerId: input.playerId,
|
|
2129
|
+
roundId: input.roundId,
|
|
2130
|
+
identity: typeof input.identity === "string" ? input.identity.trim() : "",
|
|
2131
|
+
sku: ""
|
|
2132
|
+
};
|
|
2133
|
+
if (heldIdem) {
|
|
2134
|
+
const replay = this.mockIdemReplayOrThrow(heldIdem, terms);
|
|
2135
|
+
if (replay) return replay;
|
|
2136
|
+
}
|
|
2231
2137
|
this.mockAccumulateEntry(input.roundId, amountMicro);
|
|
2232
2138
|
return this.rememberMock(
|
|
2233
2139
|
mockEntryPayment({
|
|
@@ -2243,7 +2149,8 @@ var Playmos = class {
|
|
|
2243
2149
|
// B2 pins — must survive mock path for hub hasEntered parity (#204).
|
|
2244
2150
|
roundKey: input.roundKey,
|
|
2245
2151
|
identity: input.identity
|
|
2246
|
-
})
|
|
2152
|
+
}),
|
|
2153
|
+
heldIdem ? { key: heldIdem, terms } : void 0
|
|
2247
2154
|
);
|
|
2248
2155
|
}
|
|
2249
2156
|
const serverSettleNoWallet = this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy());
|
|
@@ -2276,7 +2183,13 @@ var Playmos = class {
|
|
|
2276
2183
|
);
|
|
2277
2184
|
let payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
|
|
2278
2185
|
if (payment2.status === "pending" || payment2.status === "created") {
|
|
2279
|
-
payment2 = await this.waitForServerSettle(
|
|
2186
|
+
payment2 = await this.waitForServerSettle(
|
|
2187
|
+
payment2.id,
|
|
2188
|
+
"entry",
|
|
2189
|
+
amountMicro,
|
|
2190
|
+
input.settleTimeoutMs ?? 6e4,
|
|
2191
|
+
input.signal
|
|
2192
|
+
);
|
|
2280
2193
|
}
|
|
2281
2194
|
if (input.identity) payment2.identity = input.identity;
|
|
2282
2195
|
const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
@@ -2459,10 +2372,16 @@ var Playmos = class {
|
|
|
2459
2372
|
* until chain verify catches PaymentSettled (RPC lag) — poll so pay() matches
|
|
2460
2373
|
* the documented "confirmed" sandbox promise (TTFSC-T0 / cold-run).
|
|
2461
2374
|
*/
|
|
2462
|
-
async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4) {
|
|
2375
|
+
async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4, signal) {
|
|
2463
2376
|
const start = Date.now();
|
|
2464
2377
|
let delayMs = 400;
|
|
2465
2378
|
while (Date.now() - start < timeoutMs) {
|
|
2379
|
+
if (signal?.aborted) {
|
|
2380
|
+
throw new ApiError(
|
|
2381
|
+
`Aborted waiting for server-settle of ${paymentId}. Re-check with playmos.verify("${paymentId}").`,
|
|
2382
|
+
{ paymentId, aborted: true, asyncSettle: true }
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2466
2385
|
const raw = await this.http.get(
|
|
2467
2386
|
`/payments/${encodeURIComponent(paymentId)}`
|
|
2468
2387
|
);
|
|
@@ -2477,12 +2396,46 @@ var Playmos = class {
|
|
|
2477
2396
|
{ paymentId, timeout: true, asyncSettle: true }
|
|
2478
2397
|
);
|
|
2479
2398
|
}
|
|
2399
|
+
/**
|
|
2400
|
+
* sdk#509 — recover a payment after a lost response by the idempotency key you held.
|
|
2401
|
+
* Requires secret sk_test_ (same as list). Returns null if not found.
|
|
2402
|
+
*/
|
|
2403
|
+
async findPaymentByIdempotencyKey(idempotencyKey, opts) {
|
|
2404
|
+
requireField(idempotencyKey, "idempotencyKey");
|
|
2405
|
+
if (this.config.mock) {
|
|
2406
|
+
return null;
|
|
2407
|
+
}
|
|
2408
|
+
const q = new URLSearchParams({ idempotencyKey });
|
|
2409
|
+
if (opts?.gameId) q.set("gameId", opts.gameId);
|
|
2410
|
+
try {
|
|
2411
|
+
const res = await this.http.get(
|
|
2412
|
+
`/payments?${q.toString()}`
|
|
2413
|
+
);
|
|
2414
|
+
const row = res.data?.[0];
|
|
2415
|
+
if (!row || typeof row.id !== "string") return null;
|
|
2416
|
+
const kind = row.kind === "entry" ? "entry" : "iap";
|
|
2417
|
+
const amountMicro = validateAmount(String(row.amount ?? "0.01"));
|
|
2418
|
+
return this.mapServerPayment(
|
|
2419
|
+
row,
|
|
2420
|
+
kind,
|
|
2421
|
+
amountMicro
|
|
2422
|
+
);
|
|
2423
|
+
} catch (e) {
|
|
2424
|
+
if (e instanceof ApiError && (e.detail?.status === 404 || e.detail?.code === "not_found" || /not found/i.test(e.message))) {
|
|
2425
|
+
return null;
|
|
2426
|
+
}
|
|
2427
|
+
throw e;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2480
2430
|
/**
|
|
2481
2431
|
* Map a server-settled payment (from the `settle: "server"` response) into the
|
|
2482
2432
|
* SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
|
|
2483
|
-
* pass its authoritative `status`/`txHash`/amounts straight through
|
|
2484
|
-
*
|
|
2485
|
-
*
|
|
2433
|
+
* pass its authoritative `status`/`txHash`/amounts straight through.
|
|
2434
|
+
*
|
|
2435
|
+
* For entry, `Payment.split` is a **first-party 60/30/10 projection** from the
|
|
2436
|
+
* request amount using SDK BPS constants (same integer math as mock) — not a
|
|
2437
|
+
* chain read of pool constructor bps or post-lock payable pot (sdk#514 residual N1).
|
|
2438
|
+
* Prefer `chainAmount` / on-chain `getRound` for money truth.
|
|
2486
2439
|
*/
|
|
2487
2440
|
mapServerPayment(p, kind, amountMicro) {
|
|
2488
2441
|
const payment = {
|
package/dist/server.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var crypto = require('crypto');
|
|
4
|
+
var viem = require('viem');
|
|
4
5
|
|
|
5
6
|
// src/webhooks.ts
|
|
6
7
|
|
|
@@ -14,6 +15,11 @@ var PlaymosError = class extends Error {
|
|
|
14
15
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
15
16
|
}
|
|
16
17
|
};
|
|
18
|
+
var ApiError = class extends PlaymosError {
|
|
19
|
+
constructor(message, detail) {
|
|
20
|
+
super("api_error", message, detail);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
17
23
|
|
|
18
24
|
// src/webhooks.ts
|
|
19
25
|
var WebhookSignatureError = class extends PlaymosError {
|
|
@@ -56,6 +62,117 @@ function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
|
|
|
56
62
|
}
|
|
57
63
|
return JSON.parse(body);
|
|
58
64
|
}
|
|
65
|
+
var toBytes32 = (s) => viem.keccak256(viem.toBytes(s));
|
|
66
|
+
|
|
67
|
+
// src/hasEntered.ts
|
|
68
|
+
var hasEnteredAbi = [
|
|
69
|
+
{
|
|
70
|
+
type: "function",
|
|
71
|
+
name: "hasEntered",
|
|
72
|
+
stateMutability: "view",
|
|
73
|
+
inputs: [
|
|
74
|
+
{ name: "roundId", type: "bytes32" },
|
|
75
|
+
{ name: "identity", type: "bytes32" }
|
|
76
|
+
],
|
|
77
|
+
outputs: [{ type: "bool" }]
|
|
78
|
+
}
|
|
79
|
+
];
|
|
80
|
+
var HAS_ENTERED_DEFAULT_TIMEOUT_MS = 8e3;
|
|
81
|
+
async function hasEntered(args) {
|
|
82
|
+
const rpcUrl = typeof args.rpcUrl === "string" ? args.rpcUrl.trim() : "";
|
|
83
|
+
if (!rpcUrl) {
|
|
84
|
+
throw new ApiError('hasEntered requires a non-empty "rpcUrl" (server-side only \u2014 no wallet provider)');
|
|
85
|
+
}
|
|
86
|
+
if (!args.prizePool || !/^0x[0-9a-fA-F]{40}$/.test(args.prizePool)) {
|
|
87
|
+
throw new ApiError("hasEntered requires prizePool as a 0x-prefixed 20-byte address");
|
|
88
|
+
}
|
|
89
|
+
if (!args.roundKey?.trim() || !args.identity?.trim()) {
|
|
90
|
+
throw new ApiError("hasEntered requires non-empty roundKey and identity (plain strings, same as enterRound)");
|
|
91
|
+
}
|
|
92
|
+
const roundKey = args.roundKey.trim();
|
|
93
|
+
const identity = args.identity.trim();
|
|
94
|
+
if (/^0x[0-9a-fA-F]{64}$/.test(roundKey) || /^0x[0-9a-fA-F]{64}$/.test(identity)) {
|
|
95
|
+
throw new ApiError(
|
|
96
|
+
"hasEntered requires plain-string roundKey/identity (not 0x+64 hex). Pre-hashed pins diverge from enterRound and from service GET /entered. Pass the same plain pins used at enter.",
|
|
97
|
+
{ prizePool: args.prizePool, roundKey }
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const data = viem.encodeFunctionData({
|
|
101
|
+
abi: hasEnteredAbi,
|
|
102
|
+
functionName: "hasEntered",
|
|
103
|
+
args: [toBytes32(roundKey), toBytes32(identity)]
|
|
104
|
+
});
|
|
105
|
+
const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs > 0 ? args.timeoutMs : HAS_ENTERED_DEFAULT_TIMEOUT_MS;
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
108
|
+
const onExternalAbort = () => controller.abort();
|
|
109
|
+
if (args.signal) {
|
|
110
|
+
if (args.signal.aborted) controller.abort();
|
|
111
|
+
else args.signal.addEventListener("abort", onExternalAbort, { once: true });
|
|
112
|
+
}
|
|
113
|
+
let res;
|
|
114
|
+
try {
|
|
115
|
+
res = await fetch(rpcUrl, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: { "content-type": "application/json" },
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
jsonrpc: "2.0",
|
|
120
|
+
id: 1,
|
|
121
|
+
method: "eth_call",
|
|
122
|
+
params: [{ to: args.prizePool, data }, "latest"]
|
|
123
|
+
}),
|
|
124
|
+
signal: controller.signal
|
|
125
|
+
});
|
|
126
|
+
} catch (e) {
|
|
127
|
+
const msg = e.message ?? String(e);
|
|
128
|
+
const aborted = controller.signal.aborted || e.name === "AbortError" || /abort/i.test(msg);
|
|
129
|
+
throw new ApiError(
|
|
130
|
+
aborted ? `hasEntered RPC timed out after ${timeoutMs}ms (or was aborted)` : `hasEntered RPC request failed: ${msg}`,
|
|
131
|
+
{ prizePool: args.prizePool, roundKey }
|
|
132
|
+
);
|
|
133
|
+
} finally {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
if (args.signal) args.signal.removeEventListener("abort", onExternalAbort);
|
|
136
|
+
}
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
throw new ApiError(`hasEntered RPC HTTP ${res.status}`, {
|
|
139
|
+
prizePool: args.prizePool,
|
|
140
|
+
roundKey
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
let json;
|
|
144
|
+
try {
|
|
145
|
+
json = await res.json();
|
|
146
|
+
} catch (e) {
|
|
147
|
+
throw new ApiError(`hasEntered RPC response not JSON: ${e.message}`);
|
|
148
|
+
}
|
|
149
|
+
if (json.error || json.result == null || json.result === "") {
|
|
150
|
+
throw new ApiError(
|
|
151
|
+
`hasEntered eth_call failed: ${json.error?.message ?? `HTTP ${res.status}`}`,
|
|
152
|
+
{ prizePool: args.prizePool, roundKey }
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (json.result === "0x" || json.result === "0x0") {
|
|
156
|
+
throw new ApiError(
|
|
157
|
+
`hasEntered eth_call returned empty data for pool ${args.prizePool} \u2014 wrong address or no contract code`,
|
|
158
|
+
{ prizePool: args.prizePool, roundKey }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
return viem.decodeFunctionResult({
|
|
163
|
+
abi: hasEnteredAbi,
|
|
164
|
+
functionName: "hasEntered",
|
|
165
|
+
data: json.result
|
|
166
|
+
});
|
|
167
|
+
} catch (e) {
|
|
168
|
+
throw new ApiError(
|
|
169
|
+
`hasEntered eth_call result malformed: ${e.message}`,
|
|
170
|
+
{ prizePool: args.prizePool, roundKey }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
59
174
|
|
|
60
175
|
exports.WebhookSignatureError = WebhookSignatureError;
|
|
176
|
+
exports.enterPathToBytes32 = toBytes32;
|
|
177
|
+
exports.hasEntered = hasEntered;
|
|
61
178
|
exports.verifyWebhook = verifyWebhook;
|
package/dist/server.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Z as PlaymosError, W as WebhookEvent } from './errors-
|
|
1
|
+
import { Z as PlaymosError, W as WebhookEvent } from './errors-Dpeesyop.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Webhook signature verification (spec §6.2) — server-side only.
|
|
@@ -23,4 +23,36 @@ declare function verifyWebhook(rawBody: string | Buffer, signatureHeader: string
|
|
|
23
23
|
toleranceSeconds?: number;
|
|
24
24
|
}): WebhookEvent;
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Builders for the two settling calls. Both mirror the proven entry batch:
|
|
28
|
+
* [approve(spender, amount), settle(...)] sent atomically via EIP-5792.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** The server derives these identically (keccak256 of UTF-8 bytes) so the
|
|
32
|
+
* off-chain record and the on-chain call line up. */
|
|
33
|
+
declare const toBytes32: (s: string) => `0x${string}`;
|
|
34
|
+
|
|
35
|
+
interface HasEnteredArgs {
|
|
36
|
+
prizePool: `0x${string}`;
|
|
37
|
+
/** Same plain string pin as enterRound roundKey / body roundId. */
|
|
38
|
+
roundKey: string;
|
|
39
|
+
/** Same plain string pin as enterRound identity. */
|
|
40
|
+
identity: string;
|
|
41
|
+
/** Required JSON-RPC URL for eth_call (server-owned). */
|
|
42
|
+
rpcUrl: string;
|
|
43
|
+
/**
|
|
44
|
+
* Optional deadline for the RPC fetch (ms). Default 8000.
|
|
45
|
+
* On timeout throws ApiError (never returns false).
|
|
46
|
+
*/
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
/** Optional AbortSignal; combined with timeoutMs when both set. */
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* On-chain hasEntered for admit-to-session from a game backend.
|
|
53
|
+
* @returns true only when the chain read succeeds and returns true.
|
|
54
|
+
* @throws ApiError when eth_call fails, times out, or response is malformed.
|
|
55
|
+
*/
|
|
56
|
+
declare function hasEntered(args: HasEnteredArgs): Promise<boolean>;
|
|
57
|
+
|
|
58
|
+
export { type HasEnteredArgs, WebhookSignatureError, toBytes32 as enterPathToBytes32, hasEntered, verifyWebhook };
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Z as PlaymosError, W as WebhookEvent } from './errors-
|
|
1
|
+
import { Z as PlaymosError, W as WebhookEvent } from './errors-Dpeesyop.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Webhook signature verification (spec §6.2) — server-side only.
|
|
@@ -23,4 +23,36 @@ declare function verifyWebhook(rawBody: string | Buffer, signatureHeader: string
|
|
|
23
23
|
toleranceSeconds?: number;
|
|
24
24
|
}): WebhookEvent;
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Builders for the two settling calls. Both mirror the proven entry batch:
|
|
28
|
+
* [approve(spender, amount), settle(...)] sent atomically via EIP-5792.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** The server derives these identically (keccak256 of UTF-8 bytes) so the
|
|
32
|
+
* off-chain record and the on-chain call line up. */
|
|
33
|
+
declare const toBytes32: (s: string) => `0x${string}`;
|
|
34
|
+
|
|
35
|
+
interface HasEnteredArgs {
|
|
36
|
+
prizePool: `0x${string}`;
|
|
37
|
+
/** Same plain string pin as enterRound roundKey / body roundId. */
|
|
38
|
+
roundKey: string;
|
|
39
|
+
/** Same plain string pin as enterRound identity. */
|
|
40
|
+
identity: string;
|
|
41
|
+
/** Required JSON-RPC URL for eth_call (server-owned). */
|
|
42
|
+
rpcUrl: string;
|
|
43
|
+
/**
|
|
44
|
+
* Optional deadline for the RPC fetch (ms). Default 8000.
|
|
45
|
+
* On timeout throws ApiError (never returns false).
|
|
46
|
+
*/
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
/** Optional AbortSignal; combined with timeoutMs when both set. */
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* On-chain hasEntered for admit-to-session from a game backend.
|
|
53
|
+
* @returns true only when the chain read succeeds and returns true.
|
|
54
|
+
* @throws ApiError when eth_call fails, times out, or response is malformed.
|
|
55
|
+
*/
|
|
56
|
+
declare function hasEntered(args: HasEnteredArgs): Promise<boolean>;
|
|
57
|
+
|
|
58
|
+
export { type HasEnteredArgs, WebhookSignatureError, toBytes32 as enterPathToBytes32, hasEntered, verifyWebhook };
|