@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/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { N as Network, P as PlaymosConfig, W as WebhookEvent, R as RoundOpenInput, a as RoundState, b as RoundSettleInput, S as SettleRoundResult, A as AgentWallet, c as AgentFundResult, T as TransferResult, E as EscrowHoldInput, d as EscrowHoldResult, e as EscrowResolveResult, M as MarketplaceListInput, L as Listing, f as MarketplaceSaleResult, g as MarketplaceGetResult, h as PayInput, i as Payment, j as EnterRoundInput, k as TransferInput, V as VerifyResult } from './errors-CjL85YKR.cjs';
2
- export { l as AgentEconomyConfig, m as ApiError, n as AuthError, C as ConfigError, o as ContractConfig, p as Eip1193Provider, G as GasConfig, q as GasMode, I as InsufficientGasError, r as InvalidAmountError, s as ListingStatus, t as MarketplaceItem, u as MarketplaceSale, v as MissingFieldError, w as PaymentFailedError, x as PaymentStatus, y as PayoutRule, z as PlaymosError, B as PlaymosErrorCode, D as RoundStatus, F as WalletConfig, H as WalletConnectionError, J as WalletConnector, K as WebhookEventType } from './errors-CjL85YKR.cjs';
1
+ import { N as Network, P as PlaymosConfig, W as WebhookEvent, R as RoundOpenInput, a as RoundState, b as RoundSettleInput, S as SettleRoundResult, A as AgentWallet, c as AgentFundResult, T as TransferResult, E as EscrowHoldInput, d as EscrowHoldResult, e as EscrowResolveResult, M as MarketplaceListInput, L as Listing, f as MarketplaceSaleResult, g as MarketplaceGetResult, h as PayInput, i as Payment, j as EnterRoundInput, k as TransferReconcile, l as WaitOptions, m as TransferInput, n as TransferConfirmOptions, V as VerifyResult } from './errors-BVESr920.cjs';
2
+ export { o as AgentEconomyConfig, p as ApiError, q as AuthError, C as ConfigError, r as ContractConfig, s as Eip1193Provider, G as GasConfig, t as GasMode, I as InsufficientGasError, u as InvalidAmountError, v as ListingStatus, w as MarketplaceItem, x as MarketplaceSale, y as MissingFieldError, z as PaymentFailedError, B as PaymentStatus, D as PayoutRule, F as PlaymosError, H as PlaymosErrorCode, J as RetryOptions, K as RoundStatus, O as WalletConfig, Q as WalletConnectionError, U as WalletConnector, X as WebhookEventType } from './errors-BVESr920.cjs';
3
3
 
4
4
  /**
5
5
  * Environment resolution + the canonical address book.
@@ -99,6 +99,7 @@ declare class Playmos {
99
99
  fund: (input: {
100
100
  agentId: string;
101
101
  amount: string;
102
+ idempotencyKey?: string;
102
103
  }) => Promise<AgentFundResult>;
103
104
  /** NPC→NPC (or →player) USDC transfer, by id. A thin alias of `playmos.transfer` (which is canonical). */
104
105
  pay: (input: {
@@ -194,18 +195,24 @@ declare class Playmos {
194
195
  /** Playmos-owned skill-game entry (10%, 60/30/10 prize pool). Closes #343. */
195
196
  enterRound(input: EnterRoundInput): Promise<Payment>;
196
197
  /**
197
- * `transfers` — read-back / confirmation for a prior `transfer()` (issue #27).
198
- * Poll when POST returned `status: "settling"`; reconciles against chain (incl. gasless agent path).
198
+ * `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
199
+ * `get` reconciles once against chain truth; `wait` polls it to a terminal state
200
+ * for you (no hand-rolled loop). Both cover the gasless agent path.
199
201
  */
200
202
  readonly transfers: {
201
- get: (transferId: string) => Promise<{
202
- id: string;
203
- status: string;
204
- txHash: string | null;
205
- to: string;
206
- amount: string;
207
- verifiedVia: "chain" | "cache" | "degraded";
208
- }>;
203
+ /** One-shot reconcile of a transfer against chain truth. */
204
+ get: (transferId: string) => Promise<TransferReconcile>;
205
+ /**
206
+ * Block until a transfer reaches a terminal state — `settled` or `failed` —
207
+ * instead of hand-rolling a poll loop (#47). Polls `transfers.get(id)` every
208
+ * `intervalMs` (default 1000) until terminal, then RESOLVES with the final
209
+ * reconcile. Throws a typed `ApiError` (`detail.timeout`) if neither
210
+ * `timeoutMs` (default 30000) nor `maxAttempts` (default 40) is reached first.
211
+ *
212
+ * A `failed` transfer is a legitimate outcome, so it RESOLVES (status
213
+ * "failed") — inspect `result.status`; it does not throw.
214
+ */
215
+ wait: (transferId: string, opts?: WaitOptions) => Promise<TransferReconcile>;
209
216
  };
210
217
  /**
211
218
  * `transfer` — the value-movement base primitive (Phase 1a): move USDC from one wallet to another,
@@ -218,12 +225,13 @@ declare class Playmos {
218
225
  * Retries are safe: pass the same `idempotencyKey` and a re-call NEVER broadcasts a second tx —
219
226
  * it returns the cached result (`idempotentReplay: true`).
220
227
  *
221
- * Confirmation: treat `status === "settled" && txHash` as final. If `settling`, call
222
- * `playmos.transfers.get(id)` until settled/failed. BaseScan: `https://sepolia.basescan.org/tx/<txHash>`.
228
+ * Confirmation: treat `status === "settled" && txHash` as final. If `settling`, either call
229
+ * `playmos.transfers.wait(id)`, or pass `{ confirm: true }` here to block until terminal in one
230
+ * call (#47). BaseScan: `https://sepolia.basescan.org/tx/<txHash>`.
223
231
  *
224
232
  * NPC `from` requires `sk_test_` and settles gaslessly (NPC signs; service relays).
225
233
  */
226
- transfer(input: TransferInput): Promise<TransferResult>;
234
+ transfer(input: TransferInput, opts?: TransferConfirmOptions): Promise<TransferResult>;
227
235
  /** Verify a payment by the service's on-chain read (spec §6.1). Idempotent. */
228
236
  verify(paymentId: string): Promise<VerifyResult>;
229
237
  /**
@@ -506,4 +514,4 @@ declare function isWalletSignatureAuthorization(auth: Authorization): auth is Wa
506
514
  /** Narrow to the declared-but-unimplemented x402 variant. */
507
515
  declare function isX402PayloadAuthorization(auth: Authorization): auth is X402PayloadAuthorization;
508
516
 
509
- export { AgentFundResult, AgentWallet, type Authorization, CHAIN_ID, type CreatePaymentRequirementInput, DEFAULT_API_BASE_URL, EnterRoundInput, EscrowHoldInput, EscrowHoldResult, EscrowResolveResult, Listing, MICRO_PER_USDC, MarketplaceGetResult, MarketplaceListInput, MarketplaceSaleResult, Network, PayInput, Payment, type PaymentRequirement, PayoutError, type PayoutRule as PayoutRuleCompute, Playmos, PlaymosConfig, RoundOpenInput, RoundSettleInput, RoundState, SettleRoundResult, type SettlementAsset, TransferInput, TransferResult, USDC_ADDRESS, USDC_DECIMALS, VerifyResult, type WalletSignatureAuthorization, WebhookEvent, type X402PayloadAuthorization, computeIapSplit, computePayout, computePoolSplit, createPaymentRequirement, formatMicroToUsd, isWalletSignatureAuthorization, isX402PayloadAuthorization, parsePaymentRequirement, parseUsdToMicro, prefixedId, previewEscrowFee, previewIapSplit, previewMarketplaceSplit, previewPoolSplit, previewTransferSplit, serializePaymentRequirement, ulid };
517
+ export { AgentFundResult, AgentWallet, type Authorization, CHAIN_ID, type CreatePaymentRequirementInput, DEFAULT_API_BASE_URL, EnterRoundInput, EscrowHoldInput, EscrowHoldResult, EscrowResolveResult, Listing, MICRO_PER_USDC, MarketplaceGetResult, MarketplaceListInput, MarketplaceSaleResult, Network, PayInput, Payment, type PaymentRequirement, PayoutError, type PayoutRule as PayoutRuleCompute, Playmos, PlaymosConfig, RoundOpenInput, RoundSettleInput, RoundState, SettleRoundResult, type SettlementAsset, TransferConfirmOptions, TransferInput, TransferReconcile, TransferResult, USDC_ADDRESS, USDC_DECIMALS, VerifyResult, WaitOptions, type WalletSignatureAuthorization, WebhookEvent, type X402PayloadAuthorization, computeIapSplit, computePayout, computePoolSplit, createPaymentRequirement, formatMicroToUsd, isWalletSignatureAuthorization, isX402PayloadAuthorization, parsePaymentRequirement, parseUsdToMicro, prefixedId, previewEscrowFee, previewIapSplit, previewMarketplaceSplit, previewPoolSplit, previewTransferSplit, serializePaymentRequirement, ulid };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { N as Network, P as PlaymosConfig, W as WebhookEvent, R as RoundOpenInput, a as RoundState, b as RoundSettleInput, S as SettleRoundResult, A as AgentWallet, c as AgentFundResult, T as TransferResult, E as EscrowHoldInput, d as EscrowHoldResult, e as EscrowResolveResult, M as MarketplaceListInput, L as Listing, f as MarketplaceSaleResult, g as MarketplaceGetResult, h as PayInput, i as Payment, j as EnterRoundInput, k as TransferInput, V as VerifyResult } from './errors-CjL85YKR.js';
2
- export { l as AgentEconomyConfig, m as ApiError, n as AuthError, C as ConfigError, o as ContractConfig, p as Eip1193Provider, G as GasConfig, q as GasMode, I as InsufficientGasError, r as InvalidAmountError, s as ListingStatus, t as MarketplaceItem, u as MarketplaceSale, v as MissingFieldError, w as PaymentFailedError, x as PaymentStatus, y as PayoutRule, z as PlaymosError, B as PlaymosErrorCode, D as RoundStatus, F as WalletConfig, H as WalletConnectionError, J as WalletConnector, K as WebhookEventType } from './errors-CjL85YKR.js';
1
+ import { N as Network, P as PlaymosConfig, W as WebhookEvent, R as RoundOpenInput, a as RoundState, b as RoundSettleInput, S as SettleRoundResult, A as AgentWallet, c as AgentFundResult, T as TransferResult, E as EscrowHoldInput, d as EscrowHoldResult, e as EscrowResolveResult, M as MarketplaceListInput, L as Listing, f as MarketplaceSaleResult, g as MarketplaceGetResult, h as PayInput, i as Payment, j as EnterRoundInput, k as TransferReconcile, l as WaitOptions, m as TransferInput, n as TransferConfirmOptions, V as VerifyResult } from './errors-BVESr920.js';
2
+ export { o as AgentEconomyConfig, p as ApiError, q as AuthError, C as ConfigError, r as ContractConfig, s as Eip1193Provider, G as GasConfig, t as GasMode, I as InsufficientGasError, u as InvalidAmountError, v as ListingStatus, w as MarketplaceItem, x as MarketplaceSale, y as MissingFieldError, z as PaymentFailedError, B as PaymentStatus, D as PayoutRule, F as PlaymosError, H as PlaymosErrorCode, J as RetryOptions, K as RoundStatus, O as WalletConfig, Q as WalletConnectionError, U as WalletConnector, X as WebhookEventType } from './errors-BVESr920.js';
3
3
 
4
4
  /**
5
5
  * Environment resolution + the canonical address book.
@@ -99,6 +99,7 @@ declare class Playmos {
99
99
  fund: (input: {
100
100
  agentId: string;
101
101
  amount: string;
102
+ idempotencyKey?: string;
102
103
  }) => Promise<AgentFundResult>;
103
104
  /** NPC→NPC (or →player) USDC transfer, by id. A thin alias of `playmos.transfer` (which is canonical). */
104
105
  pay: (input: {
@@ -194,18 +195,24 @@ declare class Playmos {
194
195
  /** Playmos-owned skill-game entry (10%, 60/30/10 prize pool). Closes #343. */
195
196
  enterRound(input: EnterRoundInput): Promise<Payment>;
196
197
  /**
197
- * `transfers` — read-back / confirmation for a prior `transfer()` (issue #27).
198
- * Poll when POST returned `status: "settling"`; reconciles against chain (incl. gasless agent path).
198
+ * `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
199
+ * `get` reconciles once against chain truth; `wait` polls it to a terminal state
200
+ * for you (no hand-rolled loop). Both cover the gasless agent path.
199
201
  */
200
202
  readonly transfers: {
201
- get: (transferId: string) => Promise<{
202
- id: string;
203
- status: string;
204
- txHash: string | null;
205
- to: string;
206
- amount: string;
207
- verifiedVia: "chain" | "cache" | "degraded";
208
- }>;
203
+ /** One-shot reconcile of a transfer against chain truth. */
204
+ get: (transferId: string) => Promise<TransferReconcile>;
205
+ /**
206
+ * Block until a transfer reaches a terminal state — `settled` or `failed` —
207
+ * instead of hand-rolling a poll loop (#47). Polls `transfers.get(id)` every
208
+ * `intervalMs` (default 1000) until terminal, then RESOLVES with the final
209
+ * reconcile. Throws a typed `ApiError` (`detail.timeout`) if neither
210
+ * `timeoutMs` (default 30000) nor `maxAttempts` (default 40) is reached first.
211
+ *
212
+ * A `failed` transfer is a legitimate outcome, so it RESOLVES (status
213
+ * "failed") — inspect `result.status`; it does not throw.
214
+ */
215
+ wait: (transferId: string, opts?: WaitOptions) => Promise<TransferReconcile>;
209
216
  };
210
217
  /**
211
218
  * `transfer` — the value-movement base primitive (Phase 1a): move USDC from one wallet to another,
@@ -218,12 +225,13 @@ declare class Playmos {
218
225
  * Retries are safe: pass the same `idempotencyKey` and a re-call NEVER broadcasts a second tx —
219
226
  * it returns the cached result (`idempotentReplay: true`).
220
227
  *
221
- * Confirmation: treat `status === "settled" && txHash` as final. If `settling`, call
222
- * `playmos.transfers.get(id)` until settled/failed. BaseScan: `https://sepolia.basescan.org/tx/<txHash>`.
228
+ * Confirmation: treat `status === "settled" && txHash` as final. If `settling`, either call
229
+ * `playmos.transfers.wait(id)`, or pass `{ confirm: true }` here to block until terminal in one
230
+ * call (#47). BaseScan: `https://sepolia.basescan.org/tx/<txHash>`.
223
231
  *
224
232
  * NPC `from` requires `sk_test_` and settles gaslessly (NPC signs; service relays).
225
233
  */
226
- transfer(input: TransferInput): Promise<TransferResult>;
234
+ transfer(input: TransferInput, opts?: TransferConfirmOptions): Promise<TransferResult>;
227
235
  /** Verify a payment by the service's on-chain read (spec §6.1). Idempotent. */
228
236
  verify(paymentId: string): Promise<VerifyResult>;
229
237
  /**
@@ -506,4 +514,4 @@ declare function isWalletSignatureAuthorization(auth: Authorization): auth is Wa
506
514
  /** Narrow to the declared-but-unimplemented x402 variant. */
507
515
  declare function isX402PayloadAuthorization(auth: Authorization): auth is X402PayloadAuthorization;
508
516
 
509
- export { AgentFundResult, AgentWallet, type Authorization, CHAIN_ID, type CreatePaymentRequirementInput, DEFAULT_API_BASE_URL, EnterRoundInput, EscrowHoldInput, EscrowHoldResult, EscrowResolveResult, Listing, MICRO_PER_USDC, MarketplaceGetResult, MarketplaceListInput, MarketplaceSaleResult, Network, PayInput, Payment, type PaymentRequirement, PayoutError, type PayoutRule as PayoutRuleCompute, Playmos, PlaymosConfig, RoundOpenInput, RoundSettleInput, RoundState, SettleRoundResult, type SettlementAsset, TransferInput, TransferResult, USDC_ADDRESS, USDC_DECIMALS, VerifyResult, type WalletSignatureAuthorization, WebhookEvent, type X402PayloadAuthorization, computeIapSplit, computePayout, computePoolSplit, createPaymentRequirement, formatMicroToUsd, isWalletSignatureAuthorization, isX402PayloadAuthorization, parsePaymentRequirement, parseUsdToMicro, prefixedId, previewEscrowFee, previewIapSplit, previewMarketplaceSplit, previewPoolSplit, previewTransferSplit, serializePaymentRequirement, ulid };
517
+ export { AgentFundResult, AgentWallet, type Authorization, CHAIN_ID, type CreatePaymentRequirementInput, DEFAULT_API_BASE_URL, EnterRoundInput, EscrowHoldInput, EscrowHoldResult, EscrowResolveResult, Listing, MICRO_PER_USDC, MarketplaceGetResult, MarketplaceListInput, MarketplaceSaleResult, Network, PayInput, Payment, type PaymentRequirement, PayoutError, type PayoutRule as PayoutRuleCompute, Playmos, PlaymosConfig, RoundOpenInput, RoundSettleInput, RoundState, SettleRoundResult, type SettlementAsset, TransferConfirmOptions, TransferInput, TransferReconcile, TransferResult, USDC_ADDRESS, USDC_DECIMALS, VerifyResult, WaitOptions, type WalletSignatureAuthorization, WebhookEvent, type X402PayloadAuthorization, computeIapSplit, computePayout, computePoolSplit, createPaymentRequirement, formatMicroToUsd, isWalletSignatureAuthorization, isX402PayloadAuthorization, parsePaymentRequirement, parseUsdToMicro, prefixedId, previewEscrowFee, previewIapSplit, previewMarketplaceSplit, previewPoolSplit, previewTransferSplit, serializePaymentRequirement, ulid };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { InvalidAmountError, ConfigError, MissingFieldError, AuthError, WalletConnectionError, InsufficientGasError, PaymentFailedError, ApiError } from './chunk-B7SHFZYY.js';
1
+ import { InvalidAmountError, ConfigError, ApiError, MissingFieldError, AuthError, WalletConnectionError, InsufficientGasError, PaymentFailedError } from './chunk-B7SHFZYY.js';
2
2
  export { ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, PaymentFailedError, PlaymosError, WalletConnectionError } from './chunk-B7SHFZYY.js';
3
3
  import { encodeFunctionData, numberToHex, keccak256, toBytes } from 'viem';
4
4
 
@@ -163,8 +163,37 @@ function validateMetadata(metadata) {
163
163
  }
164
164
 
165
165
  // src/http.ts
166
- function createHttpClient(baseUrl, apiKey) {
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 send(`${base}${path}`, { method: "POST", headers, body: JSON.stringify(body) });
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 send(`${base}${path}`, {
252
+ const res = await sendWithRetry(`${base}${path}`, {
216
253
  method: "GET",
217
254
  headers: { authorization: `Bearer ${apiKey}` }
218
255
  });
@@ -638,7 +675,12 @@ var Playmos = class {
638
675
  fund: (input) => {
639
676
  requireField(input?.agentId, "agentId");
640
677
  validateAmount(input.amount);
641
- return this.http.post(`/agents/wallets/${encodeURIComponent(input.agentId)}/fund`, { amount: input.amount });
678
+ const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
679
+ return this.http.post(
680
+ `/agents/wallets/${encodeURIComponent(input.agentId)}/fund`,
681
+ { amount: input.amount, idempotencyKey },
682
+ { idempotencyKey }
683
+ );
642
684
  },
643
685
  /** NPC→NPC (or →player) USDC transfer, by id. A thin alias of `playmos.transfer` (which is canonical). */
644
686
  pay: (input) => {
@@ -667,7 +709,7 @@ var Playmos = class {
667
709
  { feeBps: input.feeBps }
668
710
  );
669
711
  }
670
- const feeSink = feeBps > 0 ? requireAddressField(input.feeSink, "feeSink") : input.feeSink === void 0 ? void 0 : requireAddressField(input.feeSink, "feeSink");
712
+ const feeSink = input.feeSink === void 0 ? void 0 : requireAddressField(input.feeSink, "feeSink");
671
713
  const resolver = input.resolver === void 0 ? void 0 : requireAddressField(input.resolver, "resolver");
672
714
  const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
673
715
  const feeMicro = amountMicro * BigInt(feeBps) / 10000n;
@@ -779,19 +821,51 @@ var Playmos = class {
779
821
  }
780
822
  };
781
823
  /**
782
- * `transfers` — read-back / confirmation for a prior `transfer()` (issue #27).
783
- * Poll when POST returned `status: "settling"`; reconciles against chain (incl. gasless agent path).
824
+ * `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
825
+ * `get` reconciles once against chain truth; `wait` polls it to a terminal state
826
+ * for you (no hand-rolled loop). Both cover the gasless agent path.
784
827
  */
785
828
  this.transfers = {
829
+ /** One-shot reconcile of a transfer against chain truth. */
786
830
  get: async (transferId) => {
787
831
  requireField(transferId, "transferId");
788
- const res = await this.http.get(`/transfers/${encodeURIComponent(transferId)}`);
832
+ const res = await this.http.get(
833
+ `/transfers/${encodeURIComponent(transferId)}`
834
+ );
789
835
  return res.transfer;
836
+ },
837
+ /**
838
+ * Block until a transfer reaches a terminal state — `settled` or `failed` —
839
+ * instead of hand-rolling a poll loop (#47). Polls `transfers.get(id)` every
840
+ * `intervalMs` (default 1000) until terminal, then RESOLVES with the final
841
+ * reconcile. Throws a typed `ApiError` (`detail.timeout`) if neither
842
+ * `timeoutMs` (default 30000) nor `maxAttempts` (default 40) is reached first.
843
+ *
844
+ * A `failed` transfer is a legitimate outcome, so it RESOLVES (status
845
+ * "failed") — inspect `result.status`; it does not throw.
846
+ */
847
+ wait: async (transferId, opts) => {
848
+ requireField(transferId, "transferId");
849
+ const intervalMs = opts?.intervalMs ?? 1e3;
850
+ const timeoutMs = opts?.timeoutMs ?? 3e4;
851
+ const maxAttempts = opts?.maxAttempts ?? 40;
852
+ const deadline = Date.now() + timeoutMs;
853
+ let last;
854
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
855
+ last = await this.transfers.get(transferId);
856
+ if (last.status === "settled" || last.status === "failed") return last;
857
+ if (Date.now() + intervalMs > deadline) break;
858
+ await new Promise((r) => setTimeout(r, intervalMs));
859
+ }
860
+ throw new ApiError(
861
+ `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}").`,
862
+ { transferId, timeout: true, lastStatus: last?.status ?? "unknown" }
863
+ );
790
864
  }
791
865
  };
792
866
  this.config = config;
793
867
  this.env = resolveEnv(config.apiKey, config.network, config.apiBaseUrl);
794
- this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey);
868
+ this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
795
869
  }
796
870
  /** Connect the player's wallet and return their address. */
797
871
  async connect() {
@@ -941,12 +1015,13 @@ var Playmos = class {
941
1015
  * Retries are safe: pass the same `idempotencyKey` and a re-call NEVER broadcasts a second tx —
942
1016
  * it returns the cached result (`idempotentReplay: true`).
943
1017
  *
944
- * Confirmation: treat `status === "settled" && txHash` as final. If `settling`, call
945
- * `playmos.transfers.get(id)` until settled/failed. BaseScan: `https://sepolia.basescan.org/tx/<txHash>`.
1018
+ * Confirmation: treat `status === "settled" && txHash` as final. If `settling`, either call
1019
+ * `playmos.transfers.wait(id)`, or pass `{ confirm: true }` here to block until terminal in one
1020
+ * call (#47). BaseScan: `https://sepolia.basescan.org/tx/<txHash>`.
946
1021
  *
947
1022
  * NPC `from` requires `sk_test_` and settles gaslessly (NPC signs; service relays).
948
1023
  */
949
- async transfer(input) {
1024
+ async transfer(input, opts) {
950
1025
  const amountMicro = validateAmount(input.amount);
951
1026
  const from = input.from === void 0 ? void 0 : partyRef(input.from, "from");
952
1027
  const to = partyRef(input.to, "to");
@@ -957,12 +1032,7 @@ var Playmos = class {
957
1032
  { feeBps: input.feeBps }
958
1033
  );
959
1034
  }
960
- let feeSink;
961
- if (feeBps > 0) {
962
- feeSink = requireAddressField(input.feeSink, "feeSink");
963
- } else if (input.feeSink !== void 0) {
964
- feeSink = requireAddressField(input.feeSink, "feeSink");
965
- }
1035
+ const feeSink = input.feeSink === void 0 ? void 0 : requireAddressField(input.feeSink, "feeSink");
966
1036
  const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
967
1037
  const res = await this.http.post(
968
1038
  "/transfers",
@@ -971,7 +1041,7 @@ var Playmos = class {
971
1041
  );
972
1042
  const t = res.transfer;
973
1043
  const feeMicro = amountMicro * BigInt(feeBps) / 10000n;
974
- return {
1044
+ const out = {
975
1045
  id: t.id,
976
1046
  status: t.status,
977
1047
  txHash: t.txHash,
@@ -987,6 +1057,12 @@ var Playmos = class {
987
1057
  memo: t.memo ?? input.memo ?? null,
988
1058
  idempotentReplay: Boolean(t.idempotentReplay)
989
1059
  };
1060
+ if (opts?.confirm && out.status === "settling") {
1061
+ const final = await this.transfers.wait(out.id, opts);
1062
+ out.status = final.status;
1063
+ out.txHash = final.txHash ?? out.txHash;
1064
+ }
1065
+ return out;
990
1066
  }
991
1067
  /** Verify a payment by the service's on-chain read (spec §6.1). Idempotent. */
992
1068
  async verify(paymentId) {