@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.cjs
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
var viem = require('viem');
|
|
4
4
|
|
|
5
|
+
// src/client.ts
|
|
6
|
+
|
|
5
7
|
// src/errors.ts
|
|
6
8
|
var PlaymosError = class extends Error {
|
|
7
9
|
constructor(code, message, detail) {
|
|
@@ -60,6 +62,15 @@ var ConfigError = class extends PlaymosError {
|
|
|
60
62
|
super("config", message, detail);
|
|
61
63
|
}
|
|
62
64
|
};
|
|
65
|
+
var NothingToWithdrawError = class extends PlaymosError {
|
|
66
|
+
constructor(detail) {
|
|
67
|
+
super(
|
|
68
|
+
"nothing_to_withdraw",
|
|
69
|
+
"Nothing to withdraw \u2014 this wallet has no credited prize balance on this PrizePool (round not settled for them, or already claimed).",
|
|
70
|
+
detail
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
63
74
|
|
|
64
75
|
// src/config.ts
|
|
65
76
|
var CHAIN_ID = {
|
|
@@ -222,8 +233,37 @@ function validateMetadata(metadata) {
|
|
|
222
233
|
}
|
|
223
234
|
|
|
224
235
|
// src/http.ts
|
|
225
|
-
function
|
|
236
|
+
function resolveRetry(retry) {
|
|
237
|
+
const off = { maxRetries: 0, baseDelayMs: 500, maxDelayMs: 2e4 };
|
|
238
|
+
if (retry === false) return off;
|
|
239
|
+
const r = retry === true || retry === void 0 ? {} : retry;
|
|
240
|
+
return {
|
|
241
|
+
maxRetries: r.maxRetries ?? 2,
|
|
242
|
+
baseDelayMs: r.baseDelayMs ?? 500,
|
|
243
|
+
maxDelayMs: r.maxDelayMs ?? 2e4
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
247
|
+
function backoffMs(res, attempt, cfg) {
|
|
248
|
+
const header = res.headers.get("retry-after");
|
|
249
|
+
if (header) {
|
|
250
|
+
const secs = Number(header);
|
|
251
|
+
let ms;
|
|
252
|
+
if (Number.isFinite(secs)) {
|
|
253
|
+
ms = secs * 1e3;
|
|
254
|
+
} else {
|
|
255
|
+
const at = Date.parse(header);
|
|
256
|
+
ms = Number.isNaN(at) ? NaN : at - Date.now();
|
|
257
|
+
}
|
|
258
|
+
if (Number.isFinite(ms) && ms >= 0) return Math.min(ms, cfg.maxDelayMs);
|
|
259
|
+
}
|
|
260
|
+
const expo = cfg.baseDelayMs * 2 ** attempt;
|
|
261
|
+
const jitter = Math.random() * cfg.baseDelayMs;
|
|
262
|
+
return Math.min(expo + jitter, cfg.maxDelayMs);
|
|
263
|
+
}
|
|
264
|
+
function createHttpClient(baseUrl, apiKey, retry) {
|
|
226
265
|
const base = baseUrl.replace(/\/+$/, "");
|
|
266
|
+
const cfg = resolveRetry(retry);
|
|
227
267
|
async function send(url, init) {
|
|
228
268
|
try {
|
|
229
269
|
return await fetch(url, init);
|
|
@@ -235,6 +275,14 @@ function createHttpClient(baseUrl, apiKey) {
|
|
|
235
275
|
);
|
|
236
276
|
}
|
|
237
277
|
}
|
|
278
|
+
async function sendWithRetry(url, init) {
|
|
279
|
+
let res = await send(url, init);
|
|
280
|
+
for (let attempt = 0; res.status === 429 && attempt < cfg.maxRetries; attempt++) {
|
|
281
|
+
await sleep(backoffMs(res, attempt, cfg));
|
|
282
|
+
res = await send(url, init);
|
|
283
|
+
}
|
|
284
|
+
return res;
|
|
285
|
+
}
|
|
238
286
|
async function handle(res) {
|
|
239
287
|
let text;
|
|
240
288
|
try {
|
|
@@ -267,11 +315,11 @@ function createHttpClient(baseUrl, apiKey) {
|
|
|
267
315
|
authorization: `Bearer ${apiKey}`
|
|
268
316
|
};
|
|
269
317
|
if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
|
|
270
|
-
const res = await
|
|
318
|
+
const res = await sendWithRetry(`${base}${path}`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
271
319
|
return handle(res);
|
|
272
320
|
},
|
|
273
321
|
async get(path) {
|
|
274
|
-
const res = await
|
|
322
|
+
const res = await sendWithRetry(`${base}${path}`, {
|
|
275
323
|
method: "GET",
|
|
276
324
|
headers: { authorization: `Bearer ${apiKey}` }
|
|
277
325
|
});
|
|
@@ -425,6 +473,13 @@ var prizePoolAbi = [
|
|
|
425
473
|
],
|
|
426
474
|
outputs: [{ type: "bool" }]
|
|
427
475
|
},
|
|
476
|
+
{
|
|
477
|
+
type: "function",
|
|
478
|
+
name: "withdrawable",
|
|
479
|
+
stateMutability: "view",
|
|
480
|
+
inputs: [{ name: "account", type: "address" }],
|
|
481
|
+
outputs: [{ type: "uint256" }]
|
|
482
|
+
},
|
|
428
483
|
{
|
|
429
484
|
type: "function",
|
|
430
485
|
name: "withdraw",
|
|
@@ -512,6 +567,14 @@ function buildEntryCalls(args) {
|
|
|
512
567
|
{ to: args.prizePool, data: enterData }
|
|
513
568
|
];
|
|
514
569
|
}
|
|
570
|
+
function buildWithdrawCall(prizePool) {
|
|
571
|
+
const data = viem.encodeFunctionData({
|
|
572
|
+
abi: prizePoolAbi,
|
|
573
|
+
functionName: "withdraw",
|
|
574
|
+
args: []
|
|
575
|
+
});
|
|
576
|
+
return { to: prizePool, data };
|
|
577
|
+
}
|
|
515
578
|
|
|
516
579
|
// src/mock.ts
|
|
517
580
|
var MOCK_TX = "0x000000000000000000000000000000000000000000000000000000000000mock";
|
|
@@ -556,6 +619,9 @@ function mockEntryPayment(args) {
|
|
|
556
619
|
},
|
|
557
620
|
gameId: args.gameId,
|
|
558
621
|
roundId: args.roundId,
|
|
622
|
+
roundKey: args.roundId,
|
|
623
|
+
// Deterministic mock pool address so claim-flow unit tests can assert shape (#41).
|
|
624
|
+
prizePoolAddress: "0x0000000000000000000000000000000000000001",
|
|
559
625
|
playerId: args.playerId,
|
|
560
626
|
txHash: MOCK_TX,
|
|
561
627
|
chain: args.chain,
|
|
@@ -668,6 +734,112 @@ var Playmos = class {
|
|
|
668
734
|
`/rounds/${encodeURIComponent(input.roundId)}`
|
|
669
735
|
);
|
|
670
736
|
return round;
|
|
737
|
+
},
|
|
738
|
+
/**
|
|
739
|
+
* Read a wallet's **claimable** prize balance for a round (issue #41).
|
|
740
|
+
* Prefers the service chain read (`GET /v1/rounds/:id/prize?wallet=`); falls
|
|
741
|
+
* back to a direct eth_call when `prizePoolAddress` is supplied and no wallet
|
|
742
|
+
* connector is needed for the read path… actually uses service first, then
|
|
743
|
+
* on-chain via the player's provider if the service is unavailable.
|
|
744
|
+
*/
|
|
745
|
+
prize: async (input) => {
|
|
746
|
+
requireField(input?.roundId, "roundId");
|
|
747
|
+
const wallet = requireAddressField(input?.wallet, "wallet");
|
|
748
|
+
try {
|
|
749
|
+
const res = await this.http.get(
|
|
750
|
+
`/rounds/${encodeURIComponent(input.roundId)}/prize?wallet=${encodeURIComponent(wallet)}`
|
|
751
|
+
);
|
|
752
|
+
return res.prize;
|
|
753
|
+
} catch (e) {
|
|
754
|
+
if (!input.prizePoolAddress && !(e instanceof ApiError)) throw e;
|
|
755
|
+
if (!input.prizePoolAddress) throw e;
|
|
756
|
+
}
|
|
757
|
+
const prizePoolAddress = requireAddressField(input.prizePoolAddress, "prizePoolAddress");
|
|
758
|
+
const claimableMicro = await this.readWithdrawable(prizePoolAddress, wallet);
|
|
759
|
+
return {
|
|
760
|
+
roundId: input.roundId,
|
|
761
|
+
prizePoolAddress,
|
|
762
|
+
wallet,
|
|
763
|
+
claimable: formatMicroToUsd(claimableMicro),
|
|
764
|
+
claimableMicro: claimableMicro.toString()
|
|
765
|
+
};
|
|
766
|
+
},
|
|
767
|
+
/**
|
|
768
|
+
* Winner **claim** — call PrizePool.withdraw() from the player's wallet (issue #41).
|
|
769
|
+
* Pull-payment: credits from settle live in `withdrawable[msg.sender]`. No approve needed.
|
|
770
|
+
* Pass either `prizePoolAddress` (from enterRound / rounds.get) or `roundId` (service resolves).
|
|
771
|
+
*/
|
|
772
|
+
withdraw: async (input) => {
|
|
773
|
+
if (this.config.mock) {
|
|
774
|
+
return {
|
|
775
|
+
prizePoolAddress: input.prizePoolAddress ?? "0x0000000000000000000000000000000000000001",
|
|
776
|
+
amount: "0.00",
|
|
777
|
+
amountMicro: "0",
|
|
778
|
+
txHash: "0x000000000000000000000000000000000000000000000000000000000000mock",
|
|
779
|
+
status: "confirmed"
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
let prizePool = input.prizePoolAddress ? requireAddressField(input.prizePoolAddress, "prizePoolAddress") : void 0;
|
|
783
|
+
if (!prizePool) {
|
|
784
|
+
requireField(input?.roundId, "roundId");
|
|
785
|
+
const round = await this.rounds.get({ roundId: input.roundId });
|
|
786
|
+
if (!round.prizePoolAddress) {
|
|
787
|
+
throw new ConfigError(
|
|
788
|
+
"rounds.get did not return prizePoolAddress \u2014 pass prizePoolAddress from enterRound or configure contracts.prizePool"
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
prizePool = round.prizePoolAddress;
|
|
792
|
+
}
|
|
793
|
+
const provider = resolveProvider(this.config.wallet);
|
|
794
|
+
if (this.config.gas?.mode === "player") {
|
|
795
|
+
const from0 = await getAccount(provider);
|
|
796
|
+
await assertEnoughGas(provider, from0);
|
|
797
|
+
}
|
|
798
|
+
const from = await getAccount(provider);
|
|
799
|
+
let amountMicro = 0n;
|
|
800
|
+
if (input.checkBalance !== false) {
|
|
801
|
+
amountMicro = await this.readWithdrawable(prizePool, from);
|
|
802
|
+
if (amountMicro === 0n) {
|
|
803
|
+
throw new NothingToWithdrawError({ prizePoolAddress: prizePool, wallet: from });
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
const call = buildWithdrawCall(prizePool);
|
|
807
|
+
const paymasterUrl = this.config.gas?.mode === "sponsored" ? this.config.gas.paymasterUrl : void 0;
|
|
808
|
+
let txHash;
|
|
809
|
+
let status = "pending";
|
|
810
|
+
try {
|
|
811
|
+
const { id: callsId } = await sendCalls(
|
|
812
|
+
provider,
|
|
813
|
+
from,
|
|
814
|
+
this.env.chainId,
|
|
815
|
+
[call],
|
|
816
|
+
paymasterUrl
|
|
817
|
+
);
|
|
818
|
+
const waited = await waitForCalls(provider, callsId);
|
|
819
|
+
txHash = waited.txHash;
|
|
820
|
+
status = waited.status === "CONFIRMED" ? "confirmed" : waited.status === "FAILED" ? "failed" : "pending";
|
|
821
|
+
} catch (e) {
|
|
822
|
+
const msg = e?.message ?? String(e);
|
|
823
|
+
if (/NothingToWithdraw|nothing to withdraw/i.test(msg)) {
|
|
824
|
+
throw new NothingToWithdrawError({ prizePoolAddress: prizePool, wallet: from, cause: msg });
|
|
825
|
+
}
|
|
826
|
+
if (e instanceof NothingToWithdrawError || e instanceof PaymentFailedError) throw e;
|
|
827
|
+
throw new PaymentFailedError("PrizePool.withdraw() failed.", { cause: msg });
|
|
828
|
+
}
|
|
829
|
+
if (status === "failed") {
|
|
830
|
+
throw new PaymentFailedError("PrizePool.withdraw() did not confirm on-chain.", {
|
|
831
|
+
prizePoolAddress: prizePool,
|
|
832
|
+
txHash
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
if (amountMicro === 0n && input.checkBalance === false) ;
|
|
836
|
+
return {
|
|
837
|
+
prizePoolAddress: prizePool,
|
|
838
|
+
amount: formatMicroToUsd(amountMicro),
|
|
839
|
+
amountMicro: amountMicro.toString(),
|
|
840
|
+
txHash,
|
|
841
|
+
status
|
|
842
|
+
};
|
|
671
843
|
}
|
|
672
844
|
};
|
|
673
845
|
/**
|
|
@@ -697,7 +869,12 @@ var Playmos = class {
|
|
|
697
869
|
fund: (input) => {
|
|
698
870
|
requireField(input?.agentId, "agentId");
|
|
699
871
|
validateAmount(input.amount);
|
|
700
|
-
|
|
872
|
+
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
873
|
+
return this.http.post(
|
|
874
|
+
`/agents/wallets/${encodeURIComponent(input.agentId)}/fund`,
|
|
875
|
+
{ amount: input.amount, idempotencyKey },
|
|
876
|
+
{ idempotencyKey }
|
|
877
|
+
);
|
|
701
878
|
},
|
|
702
879
|
/** NPC→NPC (or →player) USDC transfer, by id. A thin alias of `playmos.transfer` (which is canonical). */
|
|
703
880
|
pay: (input) => {
|
|
@@ -726,7 +903,7 @@ var Playmos = class {
|
|
|
726
903
|
{ feeBps: input.feeBps }
|
|
727
904
|
);
|
|
728
905
|
}
|
|
729
|
-
const feeSink =
|
|
906
|
+
const feeSink = input.feeSink === void 0 ? void 0 : requireAddressField(input.feeSink, "feeSink");
|
|
730
907
|
const resolver = input.resolver === void 0 ? void 0 : requireAddressField(input.resolver, "resolver");
|
|
731
908
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
732
909
|
const feeMicro = amountMicro * BigInt(feeBps) / 10000n;
|
|
@@ -838,19 +1015,51 @@ var Playmos = class {
|
|
|
838
1015
|
}
|
|
839
1016
|
};
|
|
840
1017
|
/**
|
|
841
|
-
* `transfers` — read-back / confirmation for a prior `transfer()` (
|
|
842
|
-
*
|
|
1018
|
+
* `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
|
|
1019
|
+
* `get` reconciles once against chain truth; `wait` polls it to a terminal state
|
|
1020
|
+
* for you (no hand-rolled loop). Both cover the gasless agent path.
|
|
843
1021
|
*/
|
|
844
1022
|
this.transfers = {
|
|
1023
|
+
/** One-shot reconcile of a transfer against chain truth. */
|
|
845
1024
|
get: async (transferId) => {
|
|
846
1025
|
requireField(transferId, "transferId");
|
|
847
|
-
const res = await this.http.get(
|
|
1026
|
+
const res = await this.http.get(
|
|
1027
|
+
`/transfers/${encodeURIComponent(transferId)}`
|
|
1028
|
+
);
|
|
848
1029
|
return res.transfer;
|
|
1030
|
+
},
|
|
1031
|
+
/**
|
|
1032
|
+
* Block until a transfer reaches a terminal state — `settled` or `failed` —
|
|
1033
|
+
* instead of hand-rolling a poll loop (#47). Polls `transfers.get(id)` every
|
|
1034
|
+
* `intervalMs` (default 1000) until terminal, then RESOLVES with the final
|
|
1035
|
+
* reconcile. Throws a typed `ApiError` (`detail.timeout`) if neither
|
|
1036
|
+
* `timeoutMs` (default 30000) nor `maxAttempts` (default 40) is reached first.
|
|
1037
|
+
*
|
|
1038
|
+
* A `failed` transfer is a legitimate outcome, so it RESOLVES (status
|
|
1039
|
+
* "failed") — inspect `result.status`; it does not throw.
|
|
1040
|
+
*/
|
|
1041
|
+
wait: async (transferId, opts) => {
|
|
1042
|
+
requireField(transferId, "transferId");
|
|
1043
|
+
const intervalMs = opts?.intervalMs ?? 1e3;
|
|
1044
|
+
const timeoutMs = opts?.timeoutMs ?? 3e4;
|
|
1045
|
+
const maxAttempts = opts?.maxAttempts ?? 40;
|
|
1046
|
+
const deadline = Date.now() + timeoutMs;
|
|
1047
|
+
let last;
|
|
1048
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1049
|
+
last = await this.transfers.get(transferId);
|
|
1050
|
+
if (last.status === "settled" || last.status === "failed") return last;
|
|
1051
|
+
if (Date.now() + intervalMs > deadline) break;
|
|
1052
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1053
|
+
}
|
|
1054
|
+
throw new ApiError(
|
|
1055
|
+
`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}").`,
|
|
1056
|
+
{ transferId, timeout: true, lastStatus: last?.status ?? "unknown" }
|
|
1057
|
+
);
|
|
849
1058
|
}
|
|
850
1059
|
};
|
|
851
1060
|
this.config = config;
|
|
852
1061
|
this.env = resolveEnv(config.apiKey, config.network, config.apiBaseUrl);
|
|
853
|
-
this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey);
|
|
1062
|
+
this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
|
|
854
1063
|
}
|
|
855
1064
|
/** Connect the player's wallet and return their address. */
|
|
856
1065
|
async connect() {
|
|
@@ -960,6 +1169,9 @@ var Playmos = class {
|
|
|
960
1169
|
);
|
|
961
1170
|
const payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
|
|
962
1171
|
if (input.identity) payment2.identity = input.identity;
|
|
1172
|
+
const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
|
|
1173
|
+
if (pool) payment2.prizePoolAddress = pool.toLowerCase();
|
|
1174
|
+
payment2.roundKey = res.clientParams?.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
|
|
963
1175
|
return payment2;
|
|
964
1176
|
}
|
|
965
1177
|
const intent = await this.http.post(
|
|
@@ -987,6 +1199,8 @@ var Playmos = class {
|
|
|
987
1199
|
const { txHash } = await waitForCalls(provider, callsId);
|
|
988
1200
|
const payment = await this.settle(intent.payment.id, txHash);
|
|
989
1201
|
payment.identity = identity;
|
|
1202
|
+
payment.prizePoolAddress = prizePool.toLowerCase();
|
|
1203
|
+
payment.roundKey = roundKey;
|
|
990
1204
|
return payment;
|
|
991
1205
|
}
|
|
992
1206
|
/**
|
|
@@ -1000,12 +1214,13 @@ var Playmos = class {
|
|
|
1000
1214
|
* Retries are safe: pass the same `idempotencyKey` and a re-call NEVER broadcasts a second tx —
|
|
1001
1215
|
* it returns the cached result (`idempotentReplay: true`).
|
|
1002
1216
|
*
|
|
1003
|
-
* Confirmation: treat `status === "settled" && txHash` as final. If `settling`, call
|
|
1004
|
-
* `playmos.transfers.
|
|
1217
|
+
* Confirmation: treat `status === "settled" && txHash` as final. If `settling`, either call
|
|
1218
|
+
* `playmos.transfers.wait(id)`, or pass `{ confirm: true }` here to block until terminal in one
|
|
1219
|
+
* call (#47). BaseScan: `https://sepolia.basescan.org/tx/<txHash>`.
|
|
1005
1220
|
*
|
|
1006
1221
|
* NPC `from` requires `sk_test_` and settles gaslessly (NPC signs; service relays).
|
|
1007
1222
|
*/
|
|
1008
|
-
async transfer(input) {
|
|
1223
|
+
async transfer(input, opts) {
|
|
1009
1224
|
const amountMicro = validateAmount(input.amount);
|
|
1010
1225
|
const from = input.from === void 0 ? void 0 : partyRef(input.from, "from");
|
|
1011
1226
|
const to = partyRef(input.to, "to");
|
|
@@ -1016,12 +1231,7 @@ var Playmos = class {
|
|
|
1016
1231
|
{ feeBps: input.feeBps }
|
|
1017
1232
|
);
|
|
1018
1233
|
}
|
|
1019
|
-
|
|
1020
|
-
if (feeBps > 0) {
|
|
1021
|
-
feeSink = requireAddressField(input.feeSink, "feeSink");
|
|
1022
|
-
} else if (input.feeSink !== void 0) {
|
|
1023
|
-
feeSink = requireAddressField(input.feeSink, "feeSink");
|
|
1024
|
-
}
|
|
1234
|
+
const feeSink = input.feeSink === void 0 ? void 0 : requireAddressField(input.feeSink, "feeSink");
|
|
1025
1235
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
1026
1236
|
const res = await this.http.post(
|
|
1027
1237
|
"/transfers",
|
|
@@ -1030,7 +1240,7 @@ var Playmos = class {
|
|
|
1030
1240
|
);
|
|
1031
1241
|
const t = res.transfer;
|
|
1032
1242
|
const feeMicro = amountMicro * BigInt(feeBps) / 10000n;
|
|
1033
|
-
|
|
1243
|
+
const out = {
|
|
1034
1244
|
id: t.id,
|
|
1035
1245
|
status: t.status,
|
|
1036
1246
|
txHash: t.txHash,
|
|
@@ -1046,6 +1256,12 @@ var Playmos = class {
|
|
|
1046
1256
|
memo: t.memo ?? input.memo ?? null,
|
|
1047
1257
|
idempotentReplay: Boolean(t.idempotentReplay)
|
|
1048
1258
|
};
|
|
1259
|
+
if (opts?.confirm && out.status === "settling") {
|
|
1260
|
+
const final = await this.transfers.wait(out.id, opts);
|
|
1261
|
+
out.status = final.status;
|
|
1262
|
+
out.txHash = final.txHash ?? out.txHash;
|
|
1263
|
+
}
|
|
1264
|
+
return out;
|
|
1049
1265
|
}
|
|
1050
1266
|
/** Verify a payment by the service's on-chain read (spec §6.1). Idempotent. */
|
|
1051
1267
|
async verify(paymentId) {
|
|
@@ -1094,6 +1310,58 @@ var Playmos = class {
|
|
|
1094
1310
|
const fromServer = intent.clientParams?.amountUnits ?? intent.clientParams?.amountMicro;
|
|
1095
1311
|
return fromServer ? BigInt(fromServer) : local;
|
|
1096
1312
|
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Read PrizePool.withdrawable[wallet] via eth_call (issue #41).
|
|
1315
|
+
* Uses the wallet provider when present; otherwise a public Base Sepolia RPC in test.
|
|
1316
|
+
*/
|
|
1317
|
+
async readWithdrawable(prizePool, wallet) {
|
|
1318
|
+
const data = viem.encodeFunctionData({
|
|
1319
|
+
abi: prizePoolAbi,
|
|
1320
|
+
functionName: "withdrawable",
|
|
1321
|
+
args: [wallet]
|
|
1322
|
+
});
|
|
1323
|
+
let raw;
|
|
1324
|
+
try {
|
|
1325
|
+
if (await walletAvailable(this.config.wallet)) {
|
|
1326
|
+
const provider = resolveProvider(this.config.wallet);
|
|
1327
|
+
raw = await provider.request({
|
|
1328
|
+
method: "eth_call",
|
|
1329
|
+
params: [{ to: prizePool, data }, "latest"]
|
|
1330
|
+
});
|
|
1331
|
+
} else {
|
|
1332
|
+
const rpc = this.env.network === "base" ? "https://mainnet.base.org" : "https://sepolia.base.org";
|
|
1333
|
+
const res = await fetch(rpc, {
|
|
1334
|
+
method: "POST",
|
|
1335
|
+
headers: { "content-type": "application/json" },
|
|
1336
|
+
body: JSON.stringify({
|
|
1337
|
+
jsonrpc: "2.0",
|
|
1338
|
+
id: 1,
|
|
1339
|
+
method: "eth_call",
|
|
1340
|
+
params: [{ to: prizePool, data }, "latest"]
|
|
1341
|
+
})
|
|
1342
|
+
});
|
|
1343
|
+
const json = await res.json();
|
|
1344
|
+
if (!json.result) {
|
|
1345
|
+
throw new ApiError(
|
|
1346
|
+
`eth_call withdrawable failed: ${json.error?.message ?? "no result"}`,
|
|
1347
|
+
{ prizePool, wallet }
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
raw = json.result;
|
|
1351
|
+
}
|
|
1352
|
+
} catch (e) {
|
|
1353
|
+
if (e instanceof ApiError) throw e;
|
|
1354
|
+
throw new ApiError(`Could not read withdrawable balance: ${e.message}`, {
|
|
1355
|
+
prizePool,
|
|
1356
|
+
wallet
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
return viem.decodeFunctionResult({
|
|
1360
|
+
abi: prizePoolAbi,
|
|
1361
|
+
functionName: "withdrawable",
|
|
1362
|
+
data: raw
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1097
1365
|
sponsorUrl(intent) {
|
|
1098
1366
|
if (this.config.gas?.mode === "player") return void 0;
|
|
1099
1367
|
return this.config.gas?.paymasterUrl ?? intent.clientParams?.paymasterUrl;
|
|
@@ -1381,6 +1649,7 @@ exports.InsufficientGasError = InsufficientGasError;
|
|
|
1381
1649
|
exports.InvalidAmountError = InvalidAmountError;
|
|
1382
1650
|
exports.MICRO_PER_USDC = MICRO_PER_USDC;
|
|
1383
1651
|
exports.MissingFieldError = MissingFieldError;
|
|
1652
|
+
exports.NothingToWithdrawError = NothingToWithdrawError;
|
|
1384
1653
|
exports.PaymentFailedError = PaymentFailedError;
|
|
1385
1654
|
exports.PayoutError = PayoutError;
|
|
1386
1655
|
exports.Playmos = Playmos;
|