@playmos/sdk 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -2
- package/dist/{chunk-B7SHFZYY.js → chunk-TMBBEGIF.js} +12 -3
- package/dist/chunk-TMBBEGIF.js.map +1 -0
- package/dist/{errors-CjL85YKR.d.cts → errors-DMtMpbR6.d.cts} +99 -5
- package/dist/{errors-CjL85YKR.d.ts → errors-DMtMpbR6.d.ts} +99 -5
- package/dist/index.cjs +288 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -16
- package/dist/index.d.ts +53 -16
- package/dist/index.js +279 -22
- package/dist/index.js.map +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-B7SHFZYY.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { InvalidAmountError, ConfigError,
|
|
2
|
-
export { ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, PaymentFailedError, PlaymosError, WalletConnectionError } from './chunk-
|
|
3
|
-
import { encodeFunctionData, numberToHex, keccak256, toBytes } from 'viem';
|
|
1
|
+
import { InvalidAmountError, ConfigError, NothingToWithdrawError, PaymentFailedError, ApiError, MissingFieldError, WalletConnectionError, InsufficientGasError, AuthError } from './chunk-TMBBEGIF.js';
|
|
2
|
+
export { ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError } from './chunk-TMBBEGIF.js';
|
|
3
|
+
import { encodeFunctionData, decodeFunctionResult, numberToHex, keccak256, toBytes } from 'viem';
|
|
4
4
|
|
|
5
5
|
// src/config.ts
|
|
6
6
|
var CHAIN_ID = {
|
|
@@ -163,8 +163,37 @@ function validateMetadata(metadata) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// src/http.ts
|
|
166
|
-
function
|
|
166
|
+
function resolveRetry(retry) {
|
|
167
|
+
const off = { maxRetries: 0, baseDelayMs: 500, maxDelayMs: 2e4 };
|
|
168
|
+
if (retry === false) return off;
|
|
169
|
+
const r = retry === true || retry === void 0 ? {} : retry;
|
|
170
|
+
return {
|
|
171
|
+
maxRetries: r.maxRetries ?? 2,
|
|
172
|
+
baseDelayMs: r.baseDelayMs ?? 500,
|
|
173
|
+
maxDelayMs: r.maxDelayMs ?? 2e4
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
177
|
+
function backoffMs(res, attempt, cfg) {
|
|
178
|
+
const header = res.headers.get("retry-after");
|
|
179
|
+
if (header) {
|
|
180
|
+
const secs = Number(header);
|
|
181
|
+
let ms;
|
|
182
|
+
if (Number.isFinite(secs)) {
|
|
183
|
+
ms = secs * 1e3;
|
|
184
|
+
} else {
|
|
185
|
+
const at = Date.parse(header);
|
|
186
|
+
ms = Number.isNaN(at) ? NaN : at - Date.now();
|
|
187
|
+
}
|
|
188
|
+
if (Number.isFinite(ms) && ms >= 0) return Math.min(ms, cfg.maxDelayMs);
|
|
189
|
+
}
|
|
190
|
+
const expo = cfg.baseDelayMs * 2 ** attempt;
|
|
191
|
+
const jitter = Math.random() * cfg.baseDelayMs;
|
|
192
|
+
return Math.min(expo + jitter, cfg.maxDelayMs);
|
|
193
|
+
}
|
|
194
|
+
function createHttpClient(baseUrl, apiKey, retry) {
|
|
167
195
|
const base = baseUrl.replace(/\/+$/, "");
|
|
196
|
+
const cfg = resolveRetry(retry);
|
|
168
197
|
async function send(url, init) {
|
|
169
198
|
try {
|
|
170
199
|
return await fetch(url, init);
|
|
@@ -176,6 +205,14 @@ function createHttpClient(baseUrl, apiKey) {
|
|
|
176
205
|
);
|
|
177
206
|
}
|
|
178
207
|
}
|
|
208
|
+
async function sendWithRetry(url, init) {
|
|
209
|
+
let res = await send(url, init);
|
|
210
|
+
for (let attempt = 0; res.status === 429 && attempt < cfg.maxRetries; attempt++) {
|
|
211
|
+
await sleep(backoffMs(res, attempt, cfg));
|
|
212
|
+
res = await send(url, init);
|
|
213
|
+
}
|
|
214
|
+
return res;
|
|
215
|
+
}
|
|
179
216
|
async function handle(res) {
|
|
180
217
|
let text;
|
|
181
218
|
try {
|
|
@@ -208,11 +245,11 @@ function createHttpClient(baseUrl, apiKey) {
|
|
|
208
245
|
authorization: `Bearer ${apiKey}`
|
|
209
246
|
};
|
|
210
247
|
if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
|
|
211
|
-
const res = await
|
|
248
|
+
const res = await sendWithRetry(`${base}${path}`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
212
249
|
return handle(res);
|
|
213
250
|
},
|
|
214
251
|
async get(path) {
|
|
215
|
-
const res = await
|
|
252
|
+
const res = await sendWithRetry(`${base}${path}`, {
|
|
216
253
|
method: "GET",
|
|
217
254
|
headers: { authorization: `Bearer ${apiKey}` }
|
|
218
255
|
});
|
|
@@ -366,6 +403,13 @@ var prizePoolAbi = [
|
|
|
366
403
|
],
|
|
367
404
|
outputs: [{ type: "bool" }]
|
|
368
405
|
},
|
|
406
|
+
{
|
|
407
|
+
type: "function",
|
|
408
|
+
name: "withdrawable",
|
|
409
|
+
stateMutability: "view",
|
|
410
|
+
inputs: [{ name: "account", type: "address" }],
|
|
411
|
+
outputs: [{ type: "uint256" }]
|
|
412
|
+
},
|
|
369
413
|
{
|
|
370
414
|
type: "function",
|
|
371
415
|
name: "withdraw",
|
|
@@ -453,6 +497,14 @@ function buildEntryCalls(args) {
|
|
|
453
497
|
{ to: args.prizePool, data: enterData }
|
|
454
498
|
];
|
|
455
499
|
}
|
|
500
|
+
function buildWithdrawCall(prizePool) {
|
|
501
|
+
const data = encodeFunctionData({
|
|
502
|
+
abi: prizePoolAbi,
|
|
503
|
+
functionName: "withdraw",
|
|
504
|
+
args: []
|
|
505
|
+
});
|
|
506
|
+
return { to: prizePool, data };
|
|
507
|
+
}
|
|
456
508
|
|
|
457
509
|
// src/mock.ts
|
|
458
510
|
var MOCK_TX = "0x000000000000000000000000000000000000000000000000000000000000mock";
|
|
@@ -497,6 +549,9 @@ function mockEntryPayment(args) {
|
|
|
497
549
|
},
|
|
498
550
|
gameId: args.gameId,
|
|
499
551
|
roundId: args.roundId,
|
|
552
|
+
roundKey: args.roundId,
|
|
553
|
+
// Deterministic mock pool address so claim-flow unit tests can assert shape (#41).
|
|
554
|
+
prizePoolAddress: "0x0000000000000000000000000000000000000001",
|
|
500
555
|
playerId: args.playerId,
|
|
501
556
|
txHash: MOCK_TX,
|
|
502
557
|
chain: args.chain,
|
|
@@ -609,6 +664,112 @@ var Playmos = class {
|
|
|
609
664
|
`/rounds/${encodeURIComponent(input.roundId)}`
|
|
610
665
|
);
|
|
611
666
|
return round;
|
|
667
|
+
},
|
|
668
|
+
/**
|
|
669
|
+
* Read a wallet's **claimable** prize balance for a round (issue #41).
|
|
670
|
+
* Prefers the service chain read (`GET /v1/rounds/:id/prize?wallet=`); falls
|
|
671
|
+
* back to a direct eth_call when `prizePoolAddress` is supplied and no wallet
|
|
672
|
+
* connector is needed for the read path… actually uses service first, then
|
|
673
|
+
* on-chain via the player's provider if the service is unavailable.
|
|
674
|
+
*/
|
|
675
|
+
prize: async (input) => {
|
|
676
|
+
requireField(input?.roundId, "roundId");
|
|
677
|
+
const wallet = requireAddressField(input?.wallet, "wallet");
|
|
678
|
+
try {
|
|
679
|
+
const res = await this.http.get(
|
|
680
|
+
`/rounds/${encodeURIComponent(input.roundId)}/prize?wallet=${encodeURIComponent(wallet)}`
|
|
681
|
+
);
|
|
682
|
+
return res.prize;
|
|
683
|
+
} catch (e) {
|
|
684
|
+
if (!input.prizePoolAddress && !(e instanceof ApiError)) throw e;
|
|
685
|
+
if (!input.prizePoolAddress) throw e;
|
|
686
|
+
}
|
|
687
|
+
const prizePoolAddress = requireAddressField(input.prizePoolAddress, "prizePoolAddress");
|
|
688
|
+
const claimableMicro = await this.readWithdrawable(prizePoolAddress, wallet);
|
|
689
|
+
return {
|
|
690
|
+
roundId: input.roundId,
|
|
691
|
+
prizePoolAddress,
|
|
692
|
+
wallet,
|
|
693
|
+
claimable: formatMicroToUsd(claimableMicro),
|
|
694
|
+
claimableMicro: claimableMicro.toString()
|
|
695
|
+
};
|
|
696
|
+
},
|
|
697
|
+
/**
|
|
698
|
+
* Winner **claim** — call PrizePool.withdraw() from the player's wallet (issue #41).
|
|
699
|
+
* Pull-payment: credits from settle live in `withdrawable[msg.sender]`. No approve needed.
|
|
700
|
+
* Pass either `prizePoolAddress` (from enterRound / rounds.get) or `roundId` (service resolves).
|
|
701
|
+
*/
|
|
702
|
+
withdraw: async (input) => {
|
|
703
|
+
if (this.config.mock) {
|
|
704
|
+
return {
|
|
705
|
+
prizePoolAddress: input.prizePoolAddress ?? "0x0000000000000000000000000000000000000001",
|
|
706
|
+
amount: "0.00",
|
|
707
|
+
amountMicro: "0",
|
|
708
|
+
txHash: "0x000000000000000000000000000000000000000000000000000000000000mock",
|
|
709
|
+
status: "confirmed"
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
let prizePool = input.prizePoolAddress ? requireAddressField(input.prizePoolAddress, "prizePoolAddress") : void 0;
|
|
713
|
+
if (!prizePool) {
|
|
714
|
+
requireField(input?.roundId, "roundId");
|
|
715
|
+
const round = await this.rounds.get({ roundId: input.roundId });
|
|
716
|
+
if (!round.prizePoolAddress) {
|
|
717
|
+
throw new ConfigError(
|
|
718
|
+
"rounds.get did not return prizePoolAddress \u2014 pass prizePoolAddress from enterRound or configure contracts.prizePool"
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
prizePool = round.prizePoolAddress;
|
|
722
|
+
}
|
|
723
|
+
const provider = resolveProvider(this.config.wallet);
|
|
724
|
+
if (this.config.gas?.mode === "player") {
|
|
725
|
+
const from0 = await getAccount(provider);
|
|
726
|
+
await assertEnoughGas(provider, from0);
|
|
727
|
+
}
|
|
728
|
+
const from = await getAccount(provider);
|
|
729
|
+
let amountMicro = 0n;
|
|
730
|
+
if (input.checkBalance !== false) {
|
|
731
|
+
amountMicro = await this.readWithdrawable(prizePool, from);
|
|
732
|
+
if (amountMicro === 0n) {
|
|
733
|
+
throw new NothingToWithdrawError({ prizePoolAddress: prizePool, wallet: from });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const call = buildWithdrawCall(prizePool);
|
|
737
|
+
const paymasterUrl = this.config.gas?.mode === "sponsored" ? this.config.gas.paymasterUrl : void 0;
|
|
738
|
+
let txHash;
|
|
739
|
+
let status = "pending";
|
|
740
|
+
try {
|
|
741
|
+
const { id: callsId } = await sendCalls(
|
|
742
|
+
provider,
|
|
743
|
+
from,
|
|
744
|
+
this.env.chainId,
|
|
745
|
+
[call],
|
|
746
|
+
paymasterUrl
|
|
747
|
+
);
|
|
748
|
+
const waited = await waitForCalls(provider, callsId);
|
|
749
|
+
txHash = waited.txHash;
|
|
750
|
+
status = waited.status === "CONFIRMED" ? "confirmed" : waited.status === "FAILED" ? "failed" : "pending";
|
|
751
|
+
} catch (e) {
|
|
752
|
+
const msg = e?.message ?? String(e);
|
|
753
|
+
if (/NothingToWithdraw|nothing to withdraw/i.test(msg)) {
|
|
754
|
+
throw new NothingToWithdrawError({ prizePoolAddress: prizePool, wallet: from, cause: msg });
|
|
755
|
+
}
|
|
756
|
+
if (e instanceof NothingToWithdrawError || e instanceof PaymentFailedError) throw e;
|
|
757
|
+
throw new PaymentFailedError("PrizePool.withdraw() failed.", { cause: msg });
|
|
758
|
+
}
|
|
759
|
+
if (status === "failed") {
|
|
760
|
+
throw new PaymentFailedError("PrizePool.withdraw() did not confirm on-chain.", {
|
|
761
|
+
prizePoolAddress: prizePool,
|
|
762
|
+
txHash
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
if (amountMicro === 0n && input.checkBalance === false) ;
|
|
766
|
+
return {
|
|
767
|
+
prizePoolAddress: prizePool,
|
|
768
|
+
amount: formatMicroToUsd(amountMicro),
|
|
769
|
+
amountMicro: amountMicro.toString(),
|
|
770
|
+
txHash,
|
|
771
|
+
status
|
|
772
|
+
};
|
|
612
773
|
}
|
|
613
774
|
};
|
|
614
775
|
/**
|
|
@@ -638,7 +799,12 @@ var Playmos = class {
|
|
|
638
799
|
fund: (input) => {
|
|
639
800
|
requireField(input?.agentId, "agentId");
|
|
640
801
|
validateAmount(input.amount);
|
|
641
|
-
|
|
802
|
+
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
803
|
+
return this.http.post(
|
|
804
|
+
`/agents/wallets/${encodeURIComponent(input.agentId)}/fund`,
|
|
805
|
+
{ amount: input.amount, idempotencyKey },
|
|
806
|
+
{ idempotencyKey }
|
|
807
|
+
);
|
|
642
808
|
},
|
|
643
809
|
/** NPC→NPC (or →player) USDC transfer, by id. A thin alias of `playmos.transfer` (which is canonical). */
|
|
644
810
|
pay: (input) => {
|
|
@@ -667,7 +833,7 @@ var Playmos = class {
|
|
|
667
833
|
{ feeBps: input.feeBps }
|
|
668
834
|
);
|
|
669
835
|
}
|
|
670
|
-
const feeSink =
|
|
836
|
+
const feeSink = input.feeSink === void 0 ? void 0 : requireAddressField(input.feeSink, "feeSink");
|
|
671
837
|
const resolver = input.resolver === void 0 ? void 0 : requireAddressField(input.resolver, "resolver");
|
|
672
838
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
673
839
|
const feeMicro = amountMicro * BigInt(feeBps) / 10000n;
|
|
@@ -779,19 +945,51 @@ var Playmos = class {
|
|
|
779
945
|
}
|
|
780
946
|
};
|
|
781
947
|
/**
|
|
782
|
-
* `transfers` — read-back / confirmation for a prior `transfer()` (
|
|
783
|
-
*
|
|
948
|
+
* `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
|
|
949
|
+
* `get` reconciles once against chain truth; `wait` polls it to a terminal state
|
|
950
|
+
* for you (no hand-rolled loop). Both cover the gasless agent path.
|
|
784
951
|
*/
|
|
785
952
|
this.transfers = {
|
|
953
|
+
/** One-shot reconcile of a transfer against chain truth. */
|
|
786
954
|
get: async (transferId) => {
|
|
787
955
|
requireField(transferId, "transferId");
|
|
788
|
-
const res = await this.http.get(
|
|
956
|
+
const res = await this.http.get(
|
|
957
|
+
`/transfers/${encodeURIComponent(transferId)}`
|
|
958
|
+
);
|
|
789
959
|
return res.transfer;
|
|
960
|
+
},
|
|
961
|
+
/**
|
|
962
|
+
* Block until a transfer reaches a terminal state — `settled` or `failed` —
|
|
963
|
+
* instead of hand-rolling a poll loop (#47). Polls `transfers.get(id)` every
|
|
964
|
+
* `intervalMs` (default 1000) until terminal, then RESOLVES with the final
|
|
965
|
+
* reconcile. Throws a typed `ApiError` (`detail.timeout`) if neither
|
|
966
|
+
* `timeoutMs` (default 30000) nor `maxAttempts` (default 40) is reached first.
|
|
967
|
+
*
|
|
968
|
+
* A `failed` transfer is a legitimate outcome, so it RESOLVES (status
|
|
969
|
+
* "failed") — inspect `result.status`; it does not throw.
|
|
970
|
+
*/
|
|
971
|
+
wait: async (transferId, opts) => {
|
|
972
|
+
requireField(transferId, "transferId");
|
|
973
|
+
const intervalMs = opts?.intervalMs ?? 1e3;
|
|
974
|
+
const timeoutMs = opts?.timeoutMs ?? 3e4;
|
|
975
|
+
const maxAttempts = opts?.maxAttempts ?? 40;
|
|
976
|
+
const deadline = Date.now() + timeoutMs;
|
|
977
|
+
let last;
|
|
978
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
979
|
+
last = await this.transfers.get(transferId);
|
|
980
|
+
if (last.status === "settled" || last.status === "failed") return last;
|
|
981
|
+
if (Date.now() + intervalMs > deadline) break;
|
|
982
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
983
|
+
}
|
|
984
|
+
throw new ApiError(
|
|
985
|
+
`Timed out waiting for transfer ${transferId} to settle (last status: ${last?.status ?? "unknown"}). It may still settle \u2014 re-check with playmos.transfers.get("${transferId}").`,
|
|
986
|
+
{ transferId, timeout: true, lastStatus: last?.status ?? "unknown" }
|
|
987
|
+
);
|
|
790
988
|
}
|
|
791
989
|
};
|
|
792
990
|
this.config = config;
|
|
793
991
|
this.env = resolveEnv(config.apiKey, config.network, config.apiBaseUrl);
|
|
794
|
-
this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey);
|
|
992
|
+
this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
|
|
795
993
|
}
|
|
796
994
|
/** Connect the player's wallet and return their address. */
|
|
797
995
|
async connect() {
|
|
@@ -901,6 +1099,9 @@ var Playmos = class {
|
|
|
901
1099
|
);
|
|
902
1100
|
const payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
|
|
903
1101
|
if (input.identity) payment2.identity = input.identity;
|
|
1102
|
+
const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
1103
|
+
if (pool) payment2.prizePoolAddress = pool.toLowerCase();
|
|
1104
|
+
payment2.roundKey = res.clientParams?.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
|
|
904
1105
|
return payment2;
|
|
905
1106
|
}
|
|
906
1107
|
const intent = await this.http.post(
|
|
@@ -928,6 +1129,8 @@ var Playmos = class {
|
|
|
928
1129
|
const { txHash } = await waitForCalls(provider, callsId);
|
|
929
1130
|
const payment = await this.settle(intent.payment.id, txHash);
|
|
930
1131
|
payment.identity = identity;
|
|
1132
|
+
payment.prizePoolAddress = prizePool.toLowerCase();
|
|
1133
|
+
payment.roundKey = roundKey;
|
|
931
1134
|
return payment;
|
|
932
1135
|
}
|
|
933
1136
|
/**
|
|
@@ -941,12 +1144,13 @@ var Playmos = class {
|
|
|
941
1144
|
* Retries are safe: pass the same `idempotencyKey` and a re-call NEVER broadcasts a second tx —
|
|
942
1145
|
* it returns the cached result (`idempotentReplay: true`).
|
|
943
1146
|
*
|
|
944
|
-
* Confirmation: treat `status === "settled" && txHash` as final. If `settling`, call
|
|
945
|
-
* `playmos.transfers.
|
|
1147
|
+
* Confirmation: treat `status === "settled" && txHash` as final. If `settling`, either call
|
|
1148
|
+
* `playmos.transfers.wait(id)`, or pass `{ confirm: true }` here to block until terminal in one
|
|
1149
|
+
* call (#47). BaseScan: `https://sepolia.basescan.org/tx/<txHash>`.
|
|
946
1150
|
*
|
|
947
1151
|
* NPC `from` requires `sk_test_` and settles gaslessly (NPC signs; service relays).
|
|
948
1152
|
*/
|
|
949
|
-
async transfer(input) {
|
|
1153
|
+
async transfer(input, opts) {
|
|
950
1154
|
const amountMicro = validateAmount(input.amount);
|
|
951
1155
|
const from = input.from === void 0 ? void 0 : partyRef(input.from, "from");
|
|
952
1156
|
const to = partyRef(input.to, "to");
|
|
@@ -957,12 +1161,7 @@ var Playmos = class {
|
|
|
957
1161
|
{ feeBps: input.feeBps }
|
|
958
1162
|
);
|
|
959
1163
|
}
|
|
960
|
-
|
|
961
|
-
if (feeBps > 0) {
|
|
962
|
-
feeSink = requireAddressField(input.feeSink, "feeSink");
|
|
963
|
-
} else if (input.feeSink !== void 0) {
|
|
964
|
-
feeSink = requireAddressField(input.feeSink, "feeSink");
|
|
965
|
-
}
|
|
1164
|
+
const feeSink = input.feeSink === void 0 ? void 0 : requireAddressField(input.feeSink, "feeSink");
|
|
966
1165
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
967
1166
|
const res = await this.http.post(
|
|
968
1167
|
"/transfers",
|
|
@@ -971,7 +1170,7 @@ var Playmos = class {
|
|
|
971
1170
|
);
|
|
972
1171
|
const t = res.transfer;
|
|
973
1172
|
const feeMicro = amountMicro * BigInt(feeBps) / 10000n;
|
|
974
|
-
|
|
1173
|
+
const out = {
|
|
975
1174
|
id: t.id,
|
|
976
1175
|
status: t.status,
|
|
977
1176
|
txHash: t.txHash,
|
|
@@ -987,6 +1186,12 @@ var Playmos = class {
|
|
|
987
1186
|
memo: t.memo ?? input.memo ?? null,
|
|
988
1187
|
idempotentReplay: Boolean(t.idempotentReplay)
|
|
989
1188
|
};
|
|
1189
|
+
if (opts?.confirm && out.status === "settling") {
|
|
1190
|
+
const final = await this.transfers.wait(out.id, opts);
|
|
1191
|
+
out.status = final.status;
|
|
1192
|
+
out.txHash = final.txHash ?? out.txHash;
|
|
1193
|
+
}
|
|
1194
|
+
return out;
|
|
990
1195
|
}
|
|
991
1196
|
/** Verify a payment by the service's on-chain read (spec §6.1). Idempotent. */
|
|
992
1197
|
async verify(paymentId) {
|
|
@@ -1035,6 +1240,58 @@ var Playmos = class {
|
|
|
1035
1240
|
const fromServer = intent.clientParams?.amountUnits ?? intent.clientParams?.amountMicro;
|
|
1036
1241
|
return fromServer ? BigInt(fromServer) : local;
|
|
1037
1242
|
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Read PrizePool.withdrawable[wallet] via eth_call (issue #41).
|
|
1245
|
+
* Uses the wallet provider when present; otherwise a public Base Sepolia RPC in test.
|
|
1246
|
+
*/
|
|
1247
|
+
async readWithdrawable(prizePool, wallet) {
|
|
1248
|
+
const data = encodeFunctionData({
|
|
1249
|
+
abi: prizePoolAbi,
|
|
1250
|
+
functionName: "withdrawable",
|
|
1251
|
+
args: [wallet]
|
|
1252
|
+
});
|
|
1253
|
+
let raw;
|
|
1254
|
+
try {
|
|
1255
|
+
if (await walletAvailable(this.config.wallet)) {
|
|
1256
|
+
const provider = resolveProvider(this.config.wallet);
|
|
1257
|
+
raw = await provider.request({
|
|
1258
|
+
method: "eth_call",
|
|
1259
|
+
params: [{ to: prizePool, data }, "latest"]
|
|
1260
|
+
});
|
|
1261
|
+
} else {
|
|
1262
|
+
const rpc = this.env.network === "base" ? "https://mainnet.base.org" : "https://sepolia.base.org";
|
|
1263
|
+
const res = await fetch(rpc, {
|
|
1264
|
+
method: "POST",
|
|
1265
|
+
headers: { "content-type": "application/json" },
|
|
1266
|
+
body: JSON.stringify({
|
|
1267
|
+
jsonrpc: "2.0",
|
|
1268
|
+
id: 1,
|
|
1269
|
+
method: "eth_call",
|
|
1270
|
+
params: [{ to: prizePool, data }, "latest"]
|
|
1271
|
+
})
|
|
1272
|
+
});
|
|
1273
|
+
const json = await res.json();
|
|
1274
|
+
if (!json.result) {
|
|
1275
|
+
throw new ApiError(
|
|
1276
|
+
`eth_call withdrawable failed: ${json.error?.message ?? "no result"}`,
|
|
1277
|
+
{ prizePool, wallet }
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
raw = json.result;
|
|
1281
|
+
}
|
|
1282
|
+
} catch (e) {
|
|
1283
|
+
if (e instanceof ApiError) throw e;
|
|
1284
|
+
throw new ApiError(`Could not read withdrawable balance: ${e.message}`, {
|
|
1285
|
+
prizePool,
|
|
1286
|
+
wallet
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
return decodeFunctionResult({
|
|
1290
|
+
abi: prizePoolAbi,
|
|
1291
|
+
functionName: "withdrawable",
|
|
1292
|
+
data: raw
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1038
1295
|
sponsorUrl(intent) {
|
|
1039
1296
|
if (this.config.gas?.mode === "player") return void 0;
|
|
1040
1297
|
return this.config.gas?.paymasterUrl ?? intent.clientParams?.paymasterUrl;
|