@playmos/sdk 0.2.0 → 0.3.0
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/{errors-CjL85YKR.d.cts → errors-BVESr920.d.cts} +54 -4
- package/dist/{errors-CjL85YKR.d.ts → errors-BVESr920.d.ts} +54 -4
- package/dist/index.cjs +95 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -16
- package/dist/index.d.ts +24 -16
- package/dist/index.js +96 -20
- package/dist/index.js.map +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,8 +62,8 @@ The base value-movement primitive: move USDC from one wallet to another with a c
|
|
|
62
62
|
|
|
63
63
|
```ts
|
|
64
64
|
// NPC→NPC with a 5% fee (Playmos Town P2P). Requires sk_test_ + agents.createWallet first.
|
|
65
|
-
// Full walkthrough:
|
|
66
|
-
// Confirm: status === "settled" && txHash, or
|
|
65
|
+
// Full agent/Town walkthrough: see "Agent economies — the Playmos Town golden path" below.
|
|
66
|
+
// Confirm: status === "settled" && txHash, or await playmos.transfers.wait(id) if settling.
|
|
67
67
|
const t = await playmos.transfer({
|
|
68
68
|
// `from` is OPTIONAL in the sandbox — omit it and the service uses its server-held signer wallet
|
|
69
69
|
// (Phase 1a's only payer), so you don't need to know that address to run this example.
|
|
@@ -109,6 +109,65 @@ Fields match the table above (`from` optional → defaults to the signer; `feeBp
|
|
|
109
109
|
|
|
110
110
|
> Sandbox only (Phase 1a): `transfer` settles server-held NPC/agent wallets on Base Sepolia. The player-signed (EIP-3009) non-custodial path is Phase 1b.
|
|
111
111
|
|
|
112
|
+
### Agent economies — the Playmos Town golden path
|
|
113
|
+
|
|
114
|
+
The NPCs in your game can hold and move USDC. This is the loop the [Playmos Town example](https://github.com/playmos-labs/playmos-sdk/blob/main/docs/examples/playmos-town-sdk.mjs) runs end to end: give each NPC a wallet, fund it, then move value with a per-call fee — P2P (5%), a monster bounty (0%), a shop sale (100%).
|
|
115
|
+
|
|
116
|
+
**Agents need a SECRET test key — `sk_test_`, not `pk_test_`.** Assigning NPC wallets and transferring *from* an NPC are privileged, server-side actions, so they require an `sk_test_` key kept on **your backend** — not the public `pk_test_playmos_sandbox`. `pk_test_` runs the no-wallet `pay()` / `enterRound()` demos above; `sk_test_` unlocks `agents.*` and NPC-funded `transfer`.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { Playmos } from "@playmos/sdk";
|
|
120
|
+
|
|
121
|
+
// Server-side only — an sk_test_ secret key, never shipped to the browser.
|
|
122
|
+
const playmos = new Playmos({ apiKey: process.env.PLAYMOS_SK_TEST });
|
|
123
|
+
const feeSink = process.env.PLAYMOS_FEE_SINK; // 0x treasury / shop wallet
|
|
124
|
+
|
|
125
|
+
// 1) Assign a wallet to an NPC your game already owns (idempotent — safe to re-call).
|
|
126
|
+
const miner = await playmos.agents.createWallet({ agentId: "npc_pico_miner" });
|
|
127
|
+
miner.address; // 0x… — the NPC's USDC wallet on Base Sepolia
|
|
128
|
+
|
|
129
|
+
// 2) Fund it from your treasury (sandbox faucet, per-NPC lifetime cap).
|
|
130
|
+
const funded = await playmos.agents.fund({ agentId: "npc_pico_miner", amount: "0.20" });
|
|
131
|
+
funded.funding.status; // "settled" | "settling"
|
|
132
|
+
|
|
133
|
+
// 3a) P2P trade — 5% fee to the treasury (feeBps: 500). NPC `from` ⇒ gasless (NPC signs, Playmos relays + pays gas).
|
|
134
|
+
const p2p = await playmos.transfer({
|
|
135
|
+
from: "npc_pico_miner",
|
|
136
|
+
to: "npc_bruna_blacksmith",
|
|
137
|
+
amount: "0.10",
|
|
138
|
+
feeBps: 500, // 5% P2P
|
|
139
|
+
feeSink, // required when feeBps > 0
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Confirm with the new ergonomics (#54): wait for a terminal state — no hand-rolled poll loop.
|
|
143
|
+
const settled = await playmos.transfers.wait(p2p.id);
|
|
144
|
+
settled.status; // "settled" — txHash on sepolia.basescan.org
|
|
145
|
+
|
|
146
|
+
// 3b) Monster-bounty payout — 0% untaxed reward from the treasury signer (omit `from`, no feeSink).
|
|
147
|
+
await playmos.transfer({ to: "npc_wynn_ranger", amount: "0.05", feeBps: 0 });
|
|
148
|
+
|
|
149
|
+
// 3c) First-party shop sale — 100% to the shop (feeBps: 10000).
|
|
150
|
+
await playmos.transfer({ from: "npc_kane_knight", to: feeSink, amount: "0.05", feeBps: 10_000, feeSink });
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Prefer one call? Confirm inline.** Pass `{ confirm: true }` and `transfer` blocks until the transfer is terminal before it resolves — identical to calling `transfers.wait(id)` yourself (#54):
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const t = await playmos.transfer(
|
|
157
|
+
{ from: "npc_pico_miner", to: "npc_bruna_blacksmith", amount: "0.10", feeBps: 500, feeSink },
|
|
158
|
+
{ confirm: true },
|
|
159
|
+
);
|
|
160
|
+
t.status; // "settled" (or "failed") — already terminal, no follow-up call
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The sandbox throttles (5 req/min per key), so back-to-back legs can `429`. **Retries are automatic** — the SDK backs off and retries a `429` up to twice by default, and every write carries an idempotency key so a retry never double-broadcasts (tune with `new Playmos({ apiKey, retry: { maxRetries } })`, or `retry: false` to opt out).
|
|
164
|
+
|
|
165
|
+
The full 5-NPC walkthrough — create → fund → P2P / shop / bounty, each confirmed on-chain — is [`docs/examples/playmos-town-sdk.mjs`](https://github.com/playmos-labs/playmos-sdk/blob/main/docs/examples/playmos-town-sdk.mjs):
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
PLAYMOS_SK_TEST=sk_test_… PLAYMOS_FEE_SINK=0x… node docs/examples/playmos-town-sdk.mjs
|
|
169
|
+
```
|
|
170
|
+
|
|
112
171
|
### Verify before you grant
|
|
113
172
|
|
|
114
173
|
Grant items on a verified confirmation — never on the client `pay()` return alone.
|
|
@@ -65,6 +65,54 @@ interface PlaymosConfig {
|
|
|
65
65
|
* even mock results use the REAL status union (never a synthetic "mocked").
|
|
66
66
|
*/
|
|
67
67
|
mock?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Automatic backoff/retry on HTTP 429 (throttling). Default ON (up to 2 retries,
|
|
70
|
+
* honoring a `Retry-After` header). Retries are safe — reads are idempotent and
|
|
71
|
+
* writes carry an idempotency key, so a retry never double-broadcasts.
|
|
72
|
+
* retry: false → opt out entirely
|
|
73
|
+
* retry: { maxRetries } → tune attempts / backoff (0 also opts out)
|
|
74
|
+
*/
|
|
75
|
+
retry?: boolean | RetryOptions;
|
|
76
|
+
}
|
|
77
|
+
/** Tuning for the automatic 429 retry (all optional; sane defaults). */
|
|
78
|
+
interface RetryOptions {
|
|
79
|
+
/** Max automatic retries on HTTP 429. `0` disables retrying entirely. Default 2. */
|
|
80
|
+
maxRetries?: number;
|
|
81
|
+
/** Base backoff in ms; the wait grows exponentially per attempt (2^n). Default 500. */
|
|
82
|
+
baseDelayMs?: number;
|
|
83
|
+
/** Ceiling for any single backoff wait, including a server `Retry-After`. Default 20000. */
|
|
84
|
+
maxDelayMs?: number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The on-chain-reconciled view of a `transfer`, as `playmos.transfers.get(id)` /
|
|
88
|
+
* `playmos.transfers.wait(id)` resolve it. `status` is the settlement lifecycle:
|
|
89
|
+
* "settled" (confirmed) · "settling" (reserved, confirmation pending) · "failed"
|
|
90
|
+
* (mined revert) · "pending" · "unknown". Terminal states are `settled`/`failed`.
|
|
91
|
+
*/
|
|
92
|
+
interface TransferReconcile {
|
|
93
|
+
id: string;
|
|
94
|
+
status: string;
|
|
95
|
+
txHash: string | null;
|
|
96
|
+
to: string;
|
|
97
|
+
amount: string;
|
|
98
|
+
verifiedVia: "chain" | "cache" | "degraded";
|
|
99
|
+
}
|
|
100
|
+
/** Options for `playmos.transfers.wait(id, opts?)` — how long / how often to poll. */
|
|
101
|
+
interface WaitOptions {
|
|
102
|
+
/** Poll interval in ms between `transfers.get` calls. Default 1000. */
|
|
103
|
+
intervalMs?: number;
|
|
104
|
+
/** Max total time to wait before throwing a timeout. Default 30000. */
|
|
105
|
+
timeoutMs?: number;
|
|
106
|
+
/** Belt-and-suspenders cap on poll attempts (alongside `timeoutMs`). Default 40. */
|
|
107
|
+
maxAttempts?: number;
|
|
108
|
+
}
|
|
109
|
+
/** Options for the confirm-inline form of `transfer(input, opts?)`. */
|
|
110
|
+
interface TransferConfirmOptions extends WaitOptions {
|
|
111
|
+
/**
|
|
112
|
+
* When true and the POST comes back `settling`, block until the transfer reaches
|
|
113
|
+
* a terminal state (settled/failed) before resolving — no hand-rolled poll loop.
|
|
114
|
+
*/
|
|
115
|
+
confirm?: boolean;
|
|
68
116
|
}
|
|
69
117
|
interface PayInput {
|
|
70
118
|
/** USD decimal string ("4.99"). Rejected: ≤ 0, non-numeric, > 2 dp. */
|
|
@@ -150,6 +198,7 @@ interface AgentWallet {
|
|
|
150
198
|
interface AgentFundResult {
|
|
151
199
|
agent: AgentWallet;
|
|
152
200
|
funding: {
|
|
201
|
+
id: string;
|
|
153
202
|
amount: string;
|
|
154
203
|
status: string;
|
|
155
204
|
txHash: string | null;
|
|
@@ -420,9 +469,10 @@ interface RoundState {
|
|
|
420
469
|
roundKey: string;
|
|
421
470
|
status: RoundStatus;
|
|
422
471
|
entryAmount: string;
|
|
423
|
-
/** Payable pool in USD
|
|
424
|
-
pool: string;
|
|
425
|
-
|
|
472
|
+
/** Payable pool in USD from chain after reconcile; **`null` until reconciled** (never a fabricated "0.00"). */
|
|
473
|
+
pool: string | null;
|
|
474
|
+
/** Entrant count from chain after reconcile; **`null` until reconciled**. */
|
|
475
|
+
entrants: number | null;
|
|
426
476
|
payout: PayoutRule;
|
|
427
477
|
closeAt?: string;
|
|
428
478
|
openTxHash?: `0x${string}`;
|
|
@@ -499,4 +549,4 @@ declare class ConfigError extends PlaymosError {
|
|
|
499
549
|
constructor(message: string, detail?: Record<string, unknown>);
|
|
500
550
|
}
|
|
501
551
|
|
|
502
|
-
export { type AgentWallet as A, type
|
|
552
|
+
export { type AgentWallet as A, type PaymentStatus as B, ConfigError as C, type PayoutRule as D, type EscrowHoldInput as E, PlaymosError as F, type GasConfig as G, type PlaymosErrorCode as H, InsufficientGasError as I, type RetryOptions as J, type RoundStatus as K, type Listing as L, type MarketplaceListInput as M, type Network as N, type WalletConfig as O, type PlaymosConfig as P, WalletConnectionError as Q, type RoundOpenInput as R, type SettleRoundResult as S, type TransferResult as T, type WalletConnector as U, type VerifyResult as V, type WebhookEvent as W, type WebhookEventType as X, type RoundState as a, type RoundSettleInput as b, type AgentFundResult as c, type EscrowHoldResult as d, type EscrowResolveResult as e, type MarketplaceSaleResult as f, type MarketplaceGetResult as g, type PayInput as h, type Payment as i, type EnterRoundInput as j, type TransferReconcile as k, type WaitOptions as l, type TransferInput as m, type TransferConfirmOptions as n, type AgentEconomyConfig as o, ApiError as p, AuthError as q, type ContractConfig as r, type Eip1193Provider as s, type GasMode as t, InvalidAmountError as u, type ListingStatus as v, type MarketplaceItem as w, type MarketplaceSale as x, MissingFieldError as y, PaymentFailedError as z };
|
|
@@ -65,6 +65,54 @@ interface PlaymosConfig {
|
|
|
65
65
|
* even mock results use the REAL status union (never a synthetic "mocked").
|
|
66
66
|
*/
|
|
67
67
|
mock?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Automatic backoff/retry on HTTP 429 (throttling). Default ON (up to 2 retries,
|
|
70
|
+
* honoring a `Retry-After` header). Retries are safe — reads are idempotent and
|
|
71
|
+
* writes carry an idempotency key, so a retry never double-broadcasts.
|
|
72
|
+
* retry: false → opt out entirely
|
|
73
|
+
* retry: { maxRetries } → tune attempts / backoff (0 also opts out)
|
|
74
|
+
*/
|
|
75
|
+
retry?: boolean | RetryOptions;
|
|
76
|
+
}
|
|
77
|
+
/** Tuning for the automatic 429 retry (all optional; sane defaults). */
|
|
78
|
+
interface RetryOptions {
|
|
79
|
+
/** Max automatic retries on HTTP 429. `0` disables retrying entirely. Default 2. */
|
|
80
|
+
maxRetries?: number;
|
|
81
|
+
/** Base backoff in ms; the wait grows exponentially per attempt (2^n). Default 500. */
|
|
82
|
+
baseDelayMs?: number;
|
|
83
|
+
/** Ceiling for any single backoff wait, including a server `Retry-After`. Default 20000. */
|
|
84
|
+
maxDelayMs?: number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The on-chain-reconciled view of a `transfer`, as `playmos.transfers.get(id)` /
|
|
88
|
+
* `playmos.transfers.wait(id)` resolve it. `status` is the settlement lifecycle:
|
|
89
|
+
* "settled" (confirmed) · "settling" (reserved, confirmation pending) · "failed"
|
|
90
|
+
* (mined revert) · "pending" · "unknown". Terminal states are `settled`/`failed`.
|
|
91
|
+
*/
|
|
92
|
+
interface TransferReconcile {
|
|
93
|
+
id: string;
|
|
94
|
+
status: string;
|
|
95
|
+
txHash: string | null;
|
|
96
|
+
to: string;
|
|
97
|
+
amount: string;
|
|
98
|
+
verifiedVia: "chain" | "cache" | "degraded";
|
|
99
|
+
}
|
|
100
|
+
/** Options for `playmos.transfers.wait(id, opts?)` — how long / how often to poll. */
|
|
101
|
+
interface WaitOptions {
|
|
102
|
+
/** Poll interval in ms between `transfers.get` calls. Default 1000. */
|
|
103
|
+
intervalMs?: number;
|
|
104
|
+
/** Max total time to wait before throwing a timeout. Default 30000. */
|
|
105
|
+
timeoutMs?: number;
|
|
106
|
+
/** Belt-and-suspenders cap on poll attempts (alongside `timeoutMs`). Default 40. */
|
|
107
|
+
maxAttempts?: number;
|
|
108
|
+
}
|
|
109
|
+
/** Options for the confirm-inline form of `transfer(input, opts?)`. */
|
|
110
|
+
interface TransferConfirmOptions extends WaitOptions {
|
|
111
|
+
/**
|
|
112
|
+
* When true and the POST comes back `settling`, block until the transfer reaches
|
|
113
|
+
* a terminal state (settled/failed) before resolving — no hand-rolled poll loop.
|
|
114
|
+
*/
|
|
115
|
+
confirm?: boolean;
|
|
68
116
|
}
|
|
69
117
|
interface PayInput {
|
|
70
118
|
/** USD decimal string ("4.99"). Rejected: ≤ 0, non-numeric, > 2 dp. */
|
|
@@ -150,6 +198,7 @@ interface AgentWallet {
|
|
|
150
198
|
interface AgentFundResult {
|
|
151
199
|
agent: AgentWallet;
|
|
152
200
|
funding: {
|
|
201
|
+
id: string;
|
|
153
202
|
amount: string;
|
|
154
203
|
status: string;
|
|
155
204
|
txHash: string | null;
|
|
@@ -420,9 +469,10 @@ interface RoundState {
|
|
|
420
469
|
roundKey: string;
|
|
421
470
|
status: RoundStatus;
|
|
422
471
|
entryAmount: string;
|
|
423
|
-
/** Payable pool in USD
|
|
424
|
-
pool: string;
|
|
425
|
-
|
|
472
|
+
/** Payable pool in USD from chain after reconcile; **`null` until reconciled** (never a fabricated "0.00"). */
|
|
473
|
+
pool: string | null;
|
|
474
|
+
/** Entrant count from chain after reconcile; **`null` until reconciled**. */
|
|
475
|
+
entrants: number | null;
|
|
426
476
|
payout: PayoutRule;
|
|
427
477
|
closeAt?: string;
|
|
428
478
|
openTxHash?: `0x${string}`;
|
|
@@ -499,4 +549,4 @@ declare class ConfigError extends PlaymosError {
|
|
|
499
549
|
constructor(message: string, detail?: Record<string, unknown>);
|
|
500
550
|
}
|
|
501
551
|
|
|
502
|
-
export { type AgentWallet as A, type
|
|
552
|
+
export { type AgentWallet as A, type PaymentStatus as B, ConfigError as C, type PayoutRule as D, type EscrowHoldInput as E, PlaymosError as F, type GasConfig as G, type PlaymosErrorCode as H, InsufficientGasError as I, type RetryOptions as J, type RoundStatus as K, type Listing as L, type MarketplaceListInput as M, type Network as N, type WalletConfig as O, type PlaymosConfig as P, WalletConnectionError as Q, type RoundOpenInput as R, type SettleRoundResult as S, type TransferResult as T, type WalletConnector as U, type VerifyResult as V, type WebhookEvent as W, type WebhookEventType as X, type RoundState as a, type RoundSettleInput as b, type AgentFundResult as c, type EscrowHoldResult as d, type EscrowResolveResult as e, type MarketplaceSaleResult as f, type MarketplaceGetResult as g, type PayInput as h, type Payment as i, type EnterRoundInput as j, type TransferReconcile as k, type WaitOptions as l, type TransferInput as m, type TransferConfirmOptions as n, type AgentEconomyConfig as o, ApiError as p, AuthError as q, type ContractConfig as r, type Eip1193Provider as s, type GasMode as t, InvalidAmountError as u, type ListingStatus as v, type MarketplaceItem as w, type MarketplaceSale as x, MissingFieldError as y, PaymentFailedError as z };
|
package/dist/index.cjs
CHANGED
|
@@ -222,8 +222,37 @@ function validateMetadata(metadata) {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// src/http.ts
|
|
225
|
-
function
|
|
225
|
+
function resolveRetry(retry) {
|
|
226
|
+
const off = { maxRetries: 0, baseDelayMs: 500, maxDelayMs: 2e4 };
|
|
227
|
+
if (retry === false) return off;
|
|
228
|
+
const r = retry === true || retry === void 0 ? {} : retry;
|
|
229
|
+
return {
|
|
230
|
+
maxRetries: r.maxRetries ?? 2,
|
|
231
|
+
baseDelayMs: r.baseDelayMs ?? 500,
|
|
232
|
+
maxDelayMs: r.maxDelayMs ?? 2e4
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
236
|
+
function backoffMs(res, attempt, cfg) {
|
|
237
|
+
const header = res.headers.get("retry-after");
|
|
238
|
+
if (header) {
|
|
239
|
+
const secs = Number(header);
|
|
240
|
+
let ms;
|
|
241
|
+
if (Number.isFinite(secs)) {
|
|
242
|
+
ms = secs * 1e3;
|
|
243
|
+
} else {
|
|
244
|
+
const at = Date.parse(header);
|
|
245
|
+
ms = Number.isNaN(at) ? NaN : at - Date.now();
|
|
246
|
+
}
|
|
247
|
+
if (Number.isFinite(ms) && ms >= 0) return Math.min(ms, cfg.maxDelayMs);
|
|
248
|
+
}
|
|
249
|
+
const expo = cfg.baseDelayMs * 2 ** attempt;
|
|
250
|
+
const jitter = Math.random() * cfg.baseDelayMs;
|
|
251
|
+
return Math.min(expo + jitter, cfg.maxDelayMs);
|
|
252
|
+
}
|
|
253
|
+
function createHttpClient(baseUrl, apiKey, retry) {
|
|
226
254
|
const base = baseUrl.replace(/\/+$/, "");
|
|
255
|
+
const cfg = resolveRetry(retry);
|
|
227
256
|
async function send(url, init) {
|
|
228
257
|
try {
|
|
229
258
|
return await fetch(url, init);
|
|
@@ -235,6 +264,14 @@ function createHttpClient(baseUrl, apiKey) {
|
|
|
235
264
|
);
|
|
236
265
|
}
|
|
237
266
|
}
|
|
267
|
+
async function sendWithRetry(url, init) {
|
|
268
|
+
let res = await send(url, init);
|
|
269
|
+
for (let attempt = 0; res.status === 429 && attempt < cfg.maxRetries; attempt++) {
|
|
270
|
+
await sleep(backoffMs(res, attempt, cfg));
|
|
271
|
+
res = await send(url, init);
|
|
272
|
+
}
|
|
273
|
+
return res;
|
|
274
|
+
}
|
|
238
275
|
async function handle(res) {
|
|
239
276
|
let text;
|
|
240
277
|
try {
|
|
@@ -267,11 +304,11 @@ function createHttpClient(baseUrl, apiKey) {
|
|
|
267
304
|
authorization: `Bearer ${apiKey}`
|
|
268
305
|
};
|
|
269
306
|
if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
|
|
270
|
-
const res = await
|
|
307
|
+
const res = await sendWithRetry(`${base}${path}`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
271
308
|
return handle(res);
|
|
272
309
|
},
|
|
273
310
|
async get(path) {
|
|
274
|
-
const res = await
|
|
311
|
+
const res = await sendWithRetry(`${base}${path}`, {
|
|
275
312
|
method: "GET",
|
|
276
313
|
headers: { authorization: `Bearer ${apiKey}` }
|
|
277
314
|
});
|
|
@@ -697,7 +734,12 @@ var Playmos = class {
|
|
|
697
734
|
fund: (input) => {
|
|
698
735
|
requireField(input?.agentId, "agentId");
|
|
699
736
|
validateAmount(input.amount);
|
|
700
|
-
|
|
737
|
+
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
738
|
+
return this.http.post(
|
|
739
|
+
`/agents/wallets/${encodeURIComponent(input.agentId)}/fund`,
|
|
740
|
+
{ amount: input.amount, idempotencyKey },
|
|
741
|
+
{ idempotencyKey }
|
|
742
|
+
);
|
|
701
743
|
},
|
|
702
744
|
/** NPC→NPC (or →player) USDC transfer, by id. A thin alias of `playmos.transfer` (which is canonical). */
|
|
703
745
|
pay: (input) => {
|
|
@@ -726,7 +768,7 @@ var Playmos = class {
|
|
|
726
768
|
{ feeBps: input.feeBps }
|
|
727
769
|
);
|
|
728
770
|
}
|
|
729
|
-
const feeSink =
|
|
771
|
+
const feeSink = input.feeSink === void 0 ? void 0 : requireAddressField(input.feeSink, "feeSink");
|
|
730
772
|
const resolver = input.resolver === void 0 ? void 0 : requireAddressField(input.resolver, "resolver");
|
|
731
773
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
732
774
|
const feeMicro = amountMicro * BigInt(feeBps) / 10000n;
|
|
@@ -838,19 +880,51 @@ var Playmos = class {
|
|
|
838
880
|
}
|
|
839
881
|
};
|
|
840
882
|
/**
|
|
841
|
-
* `transfers` — read-back / confirmation for a prior `transfer()` (
|
|
842
|
-
*
|
|
883
|
+
* `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
|
|
884
|
+
* `get` reconciles once against chain truth; `wait` polls it to a terminal state
|
|
885
|
+
* for you (no hand-rolled loop). Both cover the gasless agent path.
|
|
843
886
|
*/
|
|
844
887
|
this.transfers = {
|
|
888
|
+
/** One-shot reconcile of a transfer against chain truth. */
|
|
845
889
|
get: async (transferId) => {
|
|
846
890
|
requireField(transferId, "transferId");
|
|
847
|
-
const res = await this.http.get(
|
|
891
|
+
const res = await this.http.get(
|
|
892
|
+
`/transfers/${encodeURIComponent(transferId)}`
|
|
893
|
+
);
|
|
848
894
|
return res.transfer;
|
|
895
|
+
},
|
|
896
|
+
/**
|
|
897
|
+
* Block until a transfer reaches a terminal state — `settled` or `failed` —
|
|
898
|
+
* instead of hand-rolling a poll loop (#47). Polls `transfers.get(id)` every
|
|
899
|
+
* `intervalMs` (default 1000) until terminal, then RESOLVES with the final
|
|
900
|
+
* reconcile. Throws a typed `ApiError` (`detail.timeout`) if neither
|
|
901
|
+
* `timeoutMs` (default 30000) nor `maxAttempts` (default 40) is reached first.
|
|
902
|
+
*
|
|
903
|
+
* A `failed` transfer is a legitimate outcome, so it RESOLVES (status
|
|
904
|
+
* "failed") — inspect `result.status`; it does not throw.
|
|
905
|
+
*/
|
|
906
|
+
wait: async (transferId, opts) => {
|
|
907
|
+
requireField(transferId, "transferId");
|
|
908
|
+
const intervalMs = opts?.intervalMs ?? 1e3;
|
|
909
|
+
const timeoutMs = opts?.timeoutMs ?? 3e4;
|
|
910
|
+
const maxAttempts = opts?.maxAttempts ?? 40;
|
|
911
|
+
const deadline = Date.now() + timeoutMs;
|
|
912
|
+
let last;
|
|
913
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
914
|
+
last = await this.transfers.get(transferId);
|
|
915
|
+
if (last.status === "settled" || last.status === "failed") return last;
|
|
916
|
+
if (Date.now() + intervalMs > deadline) break;
|
|
917
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
918
|
+
}
|
|
919
|
+
throw new ApiError(
|
|
920
|
+
`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}").`,
|
|
921
|
+
{ transferId, timeout: true, lastStatus: last?.status ?? "unknown" }
|
|
922
|
+
);
|
|
849
923
|
}
|
|
850
924
|
};
|
|
851
925
|
this.config = config;
|
|
852
926
|
this.env = resolveEnv(config.apiKey, config.network, config.apiBaseUrl);
|
|
853
|
-
this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey);
|
|
927
|
+
this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
|
|
854
928
|
}
|
|
855
929
|
/** Connect the player's wallet and return their address. */
|
|
856
930
|
async connect() {
|
|
@@ -1000,12 +1074,13 @@ var Playmos = class {
|
|
|
1000
1074
|
* Retries are safe: pass the same `idempotencyKey` and a re-call NEVER broadcasts a second tx —
|
|
1001
1075
|
* it returns the cached result (`idempotentReplay: true`).
|
|
1002
1076
|
*
|
|
1003
|
-
* Confirmation: treat `status === "settled" && txHash` as final. If `settling`, call
|
|
1004
|
-
* `playmos.transfers.
|
|
1077
|
+
* Confirmation: treat `status === "settled" && txHash` as final. If `settling`, either call
|
|
1078
|
+
* `playmos.transfers.wait(id)`, or pass `{ confirm: true }` here to block until terminal in one
|
|
1079
|
+
* call (#47). BaseScan: `https://sepolia.basescan.org/tx/<txHash>`.
|
|
1005
1080
|
*
|
|
1006
1081
|
* NPC `from` requires `sk_test_` and settles gaslessly (NPC signs; service relays).
|
|
1007
1082
|
*/
|
|
1008
|
-
async transfer(input) {
|
|
1083
|
+
async transfer(input, opts) {
|
|
1009
1084
|
const amountMicro = validateAmount(input.amount);
|
|
1010
1085
|
const from = input.from === void 0 ? void 0 : partyRef(input.from, "from");
|
|
1011
1086
|
const to = partyRef(input.to, "to");
|
|
@@ -1016,12 +1091,7 @@ var Playmos = class {
|
|
|
1016
1091
|
{ feeBps: input.feeBps }
|
|
1017
1092
|
);
|
|
1018
1093
|
}
|
|
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
|
-
}
|
|
1094
|
+
const feeSink = input.feeSink === void 0 ? void 0 : requireAddressField(input.feeSink, "feeSink");
|
|
1025
1095
|
const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
|
|
1026
1096
|
const res = await this.http.post(
|
|
1027
1097
|
"/transfers",
|
|
@@ -1030,7 +1100,7 @@ var Playmos = class {
|
|
|
1030
1100
|
);
|
|
1031
1101
|
const t = res.transfer;
|
|
1032
1102
|
const feeMicro = amountMicro * BigInt(feeBps) / 10000n;
|
|
1033
|
-
|
|
1103
|
+
const out = {
|
|
1034
1104
|
id: t.id,
|
|
1035
1105
|
status: t.status,
|
|
1036
1106
|
txHash: t.txHash,
|
|
@@ -1046,6 +1116,12 @@ var Playmos = class {
|
|
|
1046
1116
|
memo: t.memo ?? input.memo ?? null,
|
|
1047
1117
|
idempotentReplay: Boolean(t.idempotentReplay)
|
|
1048
1118
|
};
|
|
1119
|
+
if (opts?.confirm && out.status === "settling") {
|
|
1120
|
+
const final = await this.transfers.wait(out.id, opts);
|
|
1121
|
+
out.status = final.status;
|
|
1122
|
+
out.txHash = final.txHash ?? out.txHash;
|
|
1123
|
+
}
|
|
1124
|
+
return out;
|
|
1049
1125
|
}
|
|
1050
1126
|
/** Verify a payment by the service's on-chain read (spec §6.1). Idempotent. */
|
|
1051
1127
|
async verify(paymentId) {
|