@playmos/sdk 0.1.5 → 0.2.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 +87 -0
- package/dist/chunk-B7SHFZYY.js +62 -0
- package/dist/chunk-B7SHFZYY.js.map +1 -0
- package/dist/errors-CjL85YKR.d.cts +502 -0
- package/dist/errors-CjL85YKR.d.ts +502 -0
- package/dist/index.cjs +610 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +321 -250
- package/dist/index.d.ts +321 -250
- package/dist/index.js +603 -113
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +63 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +26 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.js +47 -0
- package/dist/server.js.map +1 -0
- package/package.json +8 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,178 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* The old vendored surface advertised four wallet connectors that silently
|
|
5
|
-
* returned {connected:false} and config fields the code never read. Here the
|
|
6
|
-
* types expose exactly what V1 does: two connectors, the fields `pay()` /
|
|
7
|
-
* `enterRound()` actually use, and no more.
|
|
8
|
-
*/
|
|
9
|
-
type Network = "base" | "base-sepolia";
|
|
10
|
-
/** Only the connectors that are actually implemented. */
|
|
11
|
-
type WalletConnector = "base-account" | "injected";
|
|
12
|
-
type GasMode = "sponsored" | "player";
|
|
13
|
-
type PaymentStatus = "created" | "pending" | "confirmed" | "failed";
|
|
14
|
-
/** Minimal EIP-1193 provider shape (what the wallet must expose). */
|
|
15
|
-
interface Eip1193Provider {
|
|
16
|
-
request(args: {
|
|
17
|
-
method: string;
|
|
18
|
-
params?: unknown[] | object;
|
|
19
|
-
}): Promise<unknown>;
|
|
20
|
-
}
|
|
21
|
-
interface GasConfig {
|
|
22
|
-
mode?: GasMode;
|
|
23
|
-
/** CDP paymaster URL — required only for `mode: "sponsored"`. */
|
|
24
|
-
paymasterUrl?: string;
|
|
25
|
-
}
|
|
26
|
-
interface WalletConfig {
|
|
27
|
-
connector?: WalletConnector;
|
|
28
|
-
/**
|
|
29
|
-
* Pre-built EIP-1193 provider. If omitted, `injected` uses globalThis.ethereum
|
|
30
|
-
* and `base-account` expects a provider supplied by the Base Account SDK host.
|
|
31
|
-
*/
|
|
32
|
-
provider?: Eip1193Provider;
|
|
33
|
-
}
|
|
34
|
-
/** Per-game contract addresses. On testnet these come from the service intent,
|
|
35
|
-
* but a studio may pin them explicitly (matches the dogfood adapter). */
|
|
36
|
-
interface ContractConfig {
|
|
37
|
-
usdc?: `0x${string}`;
|
|
38
|
-
playmosPay?: `0x${string}`;
|
|
39
|
-
prizePool?: `0x${string}`;
|
|
40
|
-
}
|
|
41
|
-
interface AgentEconomyConfig {
|
|
42
|
-
enabled?: boolean;
|
|
43
|
-
/** Tax skimmed to Playmos on each agent↔agent transfer. 100 = 1% (default). */
|
|
44
|
-
taxBps?: number;
|
|
45
|
-
}
|
|
46
|
-
interface PlaymosConfig {
|
|
47
|
-
/** pk_test_* (sandbox / Base Sepolia) or pk_live_* (production / Base mainnet).
|
|
48
|
-
* The key prefix selects the environment. */
|
|
49
|
-
apiKey: string;
|
|
50
|
-
/** Override the network derived from the key. Explicit value wins. */
|
|
51
|
-
network?: Network;
|
|
52
|
-
wallet?: WalletConfig;
|
|
53
|
-
gas?: GasConfig;
|
|
54
|
-
agentEconomy?: AgentEconomyConfig;
|
|
55
|
-
contracts?: ContractConfig;
|
|
56
|
-
/** Escape hatch; defaults to the right env's base URL. */
|
|
57
|
-
apiBaseUrl?: string;
|
|
58
|
-
/**
|
|
59
|
-
* Explicit, clearly-labeled offline unit-test helper — instant, deterministic,
|
|
60
|
-
* NO network, NO chain. Never the default, never conflated with sandbox. Note:
|
|
61
|
-
* even mock results use the REAL status union (never a synthetic "mocked").
|
|
62
|
-
*/
|
|
63
|
-
mock?: boolean;
|
|
64
|
-
}
|
|
65
|
-
interface PayInput {
|
|
66
|
-
/** USD decimal string ("4.99"). Rejected: ≤ 0, non-numeric, > 2 dp. */
|
|
67
|
-
amount: string;
|
|
68
|
-
/** Your product id, echoed on the receipt + webhook. */
|
|
69
|
-
sku: string;
|
|
70
|
-
/** Your opaque user id. */
|
|
71
|
-
playerId: string;
|
|
72
|
-
/**
|
|
73
|
-
* The game this IAP belongs to. Optional: when your API key maps to exactly
|
|
74
|
-
* one game the service resolves it for you (the quickstart). Supply it
|
|
75
|
-
* explicitly when your key spans multiple games.
|
|
76
|
-
*/
|
|
77
|
-
gameId?: string;
|
|
78
|
-
/** The studio wallet that receives the 99%. Falls back to the service default. */
|
|
79
|
-
studio?: `0x${string}`;
|
|
80
|
-
/** Supply your own to make retries safe; omit and the SDK generates a ULID. */
|
|
81
|
-
idempotencyKey?: string;
|
|
82
|
-
metadata?: Record<string, string>;
|
|
83
|
-
}
|
|
84
|
-
interface EnterRoundInput {
|
|
85
|
-
/** The game — selects its prize-pool contract. */
|
|
86
|
-
gameId: string;
|
|
87
|
-
/** The round being entered. */
|
|
88
|
-
roundId: string;
|
|
89
|
-
/** USD entry — grows this round's pool. */
|
|
90
|
-
amount: string;
|
|
91
|
-
playerId: string;
|
|
92
|
-
idempotencyKey?: string;
|
|
93
|
-
metadata?: Record<string, string>;
|
|
94
|
-
/**
|
|
95
|
-
* Advanced (games that manage their own on-chain rounds, e.g. the Playmos
|
|
96
|
-
* game-hub kit): the EXACT on-chain round key to enter — overrides the default
|
|
97
|
-
* `${gameId}:${roundId}` derivation. Set it to the value your game's server
|
|
98
|
-
* verifies `hasEntered` against (e.g. "bjtest:T1").
|
|
99
|
-
*/
|
|
100
|
-
roundKey?: string;
|
|
101
|
-
/**
|
|
102
|
-
* Advanced: the EXACT on-chain identity for this paid attempt (e.g.
|
|
103
|
-
* "0xWallet#nonce"). Overrides the server-generated identity so the on-chain
|
|
104
|
-
* entry and your server's `hasEntered` check line up. One entry per identity.
|
|
105
|
-
*/
|
|
106
|
-
identity?: string;
|
|
107
|
-
}
|
|
108
|
-
interface Payment {
|
|
109
|
-
/** `pay_…` (IAP) or `entry_…` (prize-pool) — ULID, server-issued, unique. */
|
|
110
|
-
id: string;
|
|
111
|
-
status: PaymentStatus;
|
|
112
|
-
/** "iap" (1% external) or "entry" (60/30/10 prize-pool). */
|
|
113
|
-
kind: "iap" | "entry";
|
|
114
|
-
amount: string;
|
|
115
|
-
fee: string;
|
|
116
|
-
net: string;
|
|
117
|
-
/** Present for prize-pool entries: the 60/30/10 breakdown in USD. */
|
|
118
|
-
split?: {
|
|
119
|
-
pool: string;
|
|
120
|
-
seed: string;
|
|
121
|
-
rake: string;
|
|
122
|
-
};
|
|
123
|
-
sku?: string;
|
|
124
|
-
roundId?: string;
|
|
125
|
-
gameId?: string;
|
|
126
|
-
playerId: string;
|
|
127
|
-
txHash?: `0x${string}`;
|
|
128
|
-
chain: Network;
|
|
129
|
-
createdAt: string;
|
|
130
|
-
metadata?: Record<string, string>;
|
|
131
|
-
/**
|
|
132
|
-
* The on-chain identity used for a prize-pool entry (echo of
|
|
133
|
-
* `EnterRoundInput.identity` or the SDK-derived value) — reconcile against your
|
|
134
|
-
* server's `hasEntered`.
|
|
135
|
-
*/
|
|
136
|
-
identity?: string;
|
|
137
|
-
/** True only when produced by the labeled `mock: true` helper. */
|
|
138
|
-
mock?: boolean;
|
|
139
|
-
}
|
|
140
|
-
/** What `verify()` resolves to (server reads the chain). */
|
|
141
|
-
interface VerifyResult {
|
|
142
|
-
id: string;
|
|
143
|
-
status: PaymentStatus;
|
|
144
|
-
amount: string;
|
|
145
|
-
fee: string;
|
|
146
|
-
net: string;
|
|
147
|
-
txHash?: `0x${string}`;
|
|
148
|
-
playerId: string;
|
|
149
|
-
sku?: string;
|
|
150
|
-
roundId?: string;
|
|
151
|
-
chain: Network;
|
|
152
|
-
/** How the service derived this status: an on-chain read, the honest cache, or
|
|
153
|
-
* degraded (chain reads not configured). Lets the SDK stop polling when the
|
|
154
|
-
* service can never confirm on-chain. */
|
|
155
|
-
verifiedVia?: "chain" | "cache" | "degraded";
|
|
156
|
-
chainReads?: "enabled" | "degraded";
|
|
157
|
-
}
|
|
158
|
-
type WebhookEventType = "payment.confirmed" | "payment.failed" | "payout.settled" | "refund.processed";
|
|
159
|
-
interface WebhookEvent {
|
|
160
|
-
id: string;
|
|
161
|
-
type: WebhookEventType;
|
|
162
|
-
createdAt: string;
|
|
163
|
-
data: {
|
|
164
|
-
id: string;
|
|
165
|
-
status: PaymentStatus;
|
|
166
|
-
amount: string;
|
|
167
|
-
fee: string;
|
|
168
|
-
net: string;
|
|
169
|
-
playerId: string;
|
|
170
|
-
sku?: string;
|
|
171
|
-
txHash?: `0x${string}`;
|
|
172
|
-
chain: Network;
|
|
173
|
-
metadata?: Record<string, string>;
|
|
174
|
-
};
|
|
175
|
-
}
|
|
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';
|
|
176
3
|
|
|
177
4
|
/**
|
|
178
5
|
* Environment resolution + the canonical address book.
|
|
@@ -209,9 +36,20 @@ declare class Playmos {
|
|
|
209
36
|
readonly config: PlaymosConfig;
|
|
210
37
|
readonly env: ResolvedEnv;
|
|
211
38
|
private readonly http;
|
|
39
|
+
/**
|
|
40
|
+
* Webhook helpers. Signature verification is **server-only** (Node `crypto`) and
|
|
41
|
+
* lives on `@playmos/sdk/server` so browser bundlers never see `node:crypto` (#9).
|
|
42
|
+
*
|
|
43
|
+
* import { verifyWebhook } from "@playmos/sdk/server";
|
|
44
|
+
* const event = verifyWebhook(rawBody, req.headers["x-playmos-signature"], secret);
|
|
45
|
+
*/
|
|
212
46
|
readonly webhooks: {
|
|
213
|
-
/**
|
|
214
|
-
|
|
47
|
+
/**
|
|
48
|
+
* @deprecated Use `import { verifyWebhook } from "@playmos/sdk/server"` instead.
|
|
49
|
+
* Throws if called — kept as a discoverable pointer so call sites fail loudly
|
|
50
|
+
* with a fix instruction rather than a silent missing method.
|
|
51
|
+
*/
|
|
52
|
+
verify: (_rawBody: string | Buffer, _signatureHeader: string | string[] | undefined, _secret: string) => WebhookEvent;
|
|
215
53
|
};
|
|
216
54
|
readonly payouts: {
|
|
217
55
|
/** Choose how the studio is paid: "usdc" (default) or "fiat" (Bridge). */
|
|
@@ -223,25 +61,104 @@ declare class Playmos {
|
|
|
223
61
|
url: string;
|
|
224
62
|
}>;
|
|
225
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* Skill/contest **round lifecycle** (issue #13) — closes the enterRound loop.
|
|
66
|
+
*
|
|
67
|
+
* Scores never leave the studio. Flow:
|
|
68
|
+
* rounds.open → players enterRound → studio scores → rounds.lock →
|
|
69
|
+
* rounds.settle({ ranking }) → contract pays winners.
|
|
70
|
+
*
|
|
71
|
+
* Operator txs are signed by the Playmos service (OPERATOR_ROLE key).
|
|
72
|
+
*/
|
|
73
|
+
readonly rounds: {
|
|
74
|
+
open: (input: RoundOpenInput) => Promise<RoundState>;
|
|
75
|
+
lock: (input: {
|
|
76
|
+
roundId: string;
|
|
77
|
+
gameId?: string;
|
|
78
|
+
}) => Promise<RoundState>;
|
|
79
|
+
settle: (input: RoundSettleInput) => Promise<SettleRoundResult>;
|
|
80
|
+
get: (input: {
|
|
81
|
+
roundId: string;
|
|
82
|
+
}) => Promise<RoundState>;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* `agents` — assign wallets to the NPCs YOUR game already owns, so they can transact USDC in your economy.
|
|
86
|
+
* The game creates the NPCs; the SDK only creates the WALLET for a game-supplied id. Engine-agnostic
|
|
87
|
+
* (Unity/Unreal/Godot/web all call the same REST). Requires a secret test key (`sk_test_`) on the sandbox.
|
|
88
|
+
*/
|
|
226
89
|
readonly agents: {
|
|
227
|
-
/** Assign
|
|
90
|
+
/** Assign (or return) the wallet for a game NPC id. Idempotent — safe to call wherever your NPCs spawn. */
|
|
228
91
|
createWallet: (input: {
|
|
229
92
|
agentId: string;
|
|
230
|
-
}) => Promise<
|
|
93
|
+
}) => Promise<AgentWallet>;
|
|
94
|
+
/** Resolve one NPC's wallet by your id. */
|
|
95
|
+
wallet: (agentId: string) => Promise<AgentWallet>;
|
|
96
|
+
/** List the NPC wallets you've assigned in this studio. */
|
|
97
|
+
list: () => Promise<AgentWallet[]>;
|
|
98
|
+
/** Sandbox faucet: fund an NPC with USDC from your treasury (per-NPC lifetime cap). */
|
|
99
|
+
fund: (input: {
|
|
231
100
|
agentId: string;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
/** Agent↔agent USDC transfer; the configured taxBps is skimmed to Playmos. */
|
|
101
|
+
amount: string;
|
|
102
|
+
}) => Promise<AgentFundResult>;
|
|
103
|
+
/** NPC→NPC (or →player) USDC transfer, by id. A thin alias of `playmos.transfer` (which is canonical). */
|
|
236
104
|
pay: (input: {
|
|
237
105
|
from: string;
|
|
238
106
|
to: string;
|
|
239
107
|
amount: string;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
108
|
+
feeBps?: number;
|
|
109
|
+
feeSink?: `0x${string}`;
|
|
110
|
+
}) => Promise<TransferResult>;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* `escrow` — the fair-exchange primitive (Phase 2): lock the payer's USDC on-chain the instant a deal
|
|
114
|
+
* opens (`hold`), then move it exactly once — `release` (→ payee, fee skimmed) XOR `refund` (→ payer,
|
|
115
|
+
* 100%). The contract holds the funds trustlessly; YOUR game decides the rule (who resolves, and when).
|
|
116
|
+
* A `deadline` auto-refund guarantees funds never get stuck. Pass the `hold` result's `id` to the rest.
|
|
117
|
+
*/
|
|
118
|
+
readonly escrow: {
|
|
119
|
+
/** Open a deal: lock `amount` of the payer's USDC into the on-chain escrow. Retries are idempotent.
|
|
120
|
+
* `async` so client-side validation surfaces as a rejected promise, not a synchronous throw. */
|
|
121
|
+
hold: (input: EscrowHoldInput) => Promise<EscrowHoldResult>;
|
|
122
|
+
/** Release a held deal to the payee (fee skimmed). `escrowId` is the `hold` result's `id`. */
|
|
123
|
+
release: (input: {
|
|
124
|
+
escrowId: string;
|
|
125
|
+
}) => Promise<EscrowResolveResult>;
|
|
126
|
+
/** Refund a held deal to the payer (100%, untaxed). `escrowId` is the `hold` result's `id`. */
|
|
127
|
+
refund: (input: {
|
|
128
|
+
escrowId: string;
|
|
129
|
+
}) => Promise<EscrowResolveResult>;
|
|
130
|
+
/** Verify a deal's on-chain state (reconciles a `settling` hold wedged by a crash). */
|
|
131
|
+
get: (escrowId: string) => Promise<EscrowResolveResult>;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* `marketplace` — list an item, and the seller is paid ONLY when it's bought (Phase 3a, off-chain items).
|
|
135
|
+
* Built on `escrow`: `buy` locks the buyer's USDC on-chain, `confirm` (after your game server delivers the
|
|
136
|
+
* item) pays the seller, and a no-delivery/timeout `refund`s the buyer. `deliver: true` on `buy` collapses
|
|
137
|
+
* lock+pay into one call when your server delivers synchronously. On-chain items are Phase 3b.
|
|
138
|
+
*/
|
|
139
|
+
readonly marketplace: {
|
|
140
|
+
/** List an off-chain item for sale. No money moves. Idempotent on `idempotencyKey`. */
|
|
141
|
+
list: (input: MarketplaceListInput) => Promise<Listing>;
|
|
142
|
+
/** Buy a listing: lock the buyer's USDC in escrow. Pass `deliver: true` to also pay the seller in one call. */
|
|
143
|
+
buy: (input: {
|
|
144
|
+
listingId: string;
|
|
145
|
+
buyer?: `0x${string}`;
|
|
146
|
+
deliver?: boolean;
|
|
147
|
+
}) => Promise<MarketplaceSaleResult>;
|
|
148
|
+
/** Confirm delivery → the seller is paid (release), fee skimmed. */
|
|
149
|
+
confirm: (input: {
|
|
150
|
+
listingId: string;
|
|
151
|
+
}) => Promise<MarketplaceSaleResult>;
|
|
152
|
+
/** Refund the buyer 100% (seller couldn't deliver / dispute / timeout). */
|
|
153
|
+
refund: (input: {
|
|
154
|
+
listingId: string;
|
|
155
|
+
}) => Promise<MarketplaceSaleResult>;
|
|
156
|
+
/** Delist an unsold listing (pure DB, no tx). */
|
|
157
|
+
cancel: (input: {
|
|
158
|
+
listingId: string;
|
|
159
|
+
}) => Promise<Listing>;
|
|
160
|
+
/** Fetch a listing + its on-chain-verified sale status. */
|
|
161
|
+
get: (listingId: string) => Promise<MarketplaceGetResult>;
|
|
245
162
|
};
|
|
246
163
|
constructor(config: PlaymosConfig);
|
|
247
164
|
/** Connect the player's wallet and return their address. */
|
|
@@ -276,6 +193,37 @@ declare class Playmos {
|
|
|
276
193
|
pay(input: PayInput): Promise<Payment>;
|
|
277
194
|
/** Playmos-owned skill-game entry (10%, 60/30/10 prize pool). Closes #343. */
|
|
278
195
|
enterRound(input: EnterRoundInput): Promise<Payment>;
|
|
196
|
+
/**
|
|
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).
|
|
199
|
+
*/
|
|
200
|
+
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
|
+
}>;
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* `transfer` — the value-movement base primitive (Phase 1a): move USDC from one wallet to another,
|
|
212
|
+
* with a per-call fee. The GAME LOGIC is the authority — you already decided the move is valid — so
|
|
213
|
+
* this is a direct, unconditional push (use `escrow`/`marketplace` when a trust boundary needs fair
|
|
214
|
+
* exchange). The service settles it through the protocol-agnostic settlement core (idempotent,
|
|
215
|
+
* reserve-before-broadcast) and the on-chain PlaymosTransfer / PlaymosTransferAuth contracts.
|
|
216
|
+
*
|
|
217
|
+
* Fee is per-call: `feeBps` 0–10000 (+ `feeSink`). `feeBps: 0` is an untaxed reward/faucet transfer.
|
|
218
|
+
* Retries are safe: pass the same `idempotencyKey` and a re-call NEVER broadcasts a second tx —
|
|
219
|
+
* it returns the cached result (`idempotentReplay: true`).
|
|
220
|
+
*
|
|
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>`.
|
|
223
|
+
*
|
|
224
|
+
* NPC `from` requires `sk_test_` and settles gaslessly (NPC signs; service relays).
|
|
225
|
+
*/
|
|
226
|
+
transfer(input: TransferInput): Promise<TransferResult>;
|
|
279
227
|
/** Verify a payment by the service's on-chain read (spec §6.1). Idempotent. */
|
|
280
228
|
verify(paymentId: string): Promise<VerifyResult>;
|
|
281
229
|
/**
|
|
@@ -302,6 +250,37 @@ declare function previewIapSplit(amount: string): {
|
|
|
302
250
|
fee: string;
|
|
303
251
|
net: string;
|
|
304
252
|
};
|
|
253
|
+
/**
|
|
254
|
+
* Local preview of a `transfer` fee split without any network — handy for UIs and quotes. Mirrors the
|
|
255
|
+
* on-chain floor math: `fee = floor(amount * feeBps / 10000)`, `net = amount − fee` (dust → payee).
|
|
256
|
+
*/
|
|
257
|
+
declare function previewTransferSplit(amount: string, feeBps: number): {
|
|
258
|
+
amount: string;
|
|
259
|
+
fee: string;
|
|
260
|
+
net: string;
|
|
261
|
+
feeBps: number;
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* Local preview of what an escrow `release` would pay out, without any network. Mirrors the on-chain
|
|
265
|
+
* floor math: `fee = floor(amount * feeBps / 10000)`, `net = amount − fee` (dust → payee). A `refund`
|
|
266
|
+
* returns the full `amount` with no fee, so it needs no preview.
|
|
267
|
+
*/
|
|
268
|
+
declare function previewEscrowFee(amount: string, feeBps: number): {
|
|
269
|
+
amount: string;
|
|
270
|
+
fee: string;
|
|
271
|
+
net: string;
|
|
272
|
+
feeBps: number;
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* Local preview of a marketplace sale's payout, without any network. Mirrors the on-chain floor math
|
|
276
|
+
* (`fee = floor(price * feeBps / 10000)`, `net = price − fee`, dust → seller). A no-sale/refund pays 0.
|
|
277
|
+
*/
|
|
278
|
+
declare function previewMarketplaceSplit(price: string, feeBps: number): {
|
|
279
|
+
price: string;
|
|
280
|
+
fee: string;
|
|
281
|
+
net: string;
|
|
282
|
+
feeBps: number;
|
|
283
|
+
};
|
|
305
284
|
/** Local preview of the 60/30/10 entry split without any network. */
|
|
306
285
|
declare function previewPoolSplit(amount: string): {
|
|
307
286
|
amount: string;
|
|
@@ -311,74 +290,38 @@ declare function previewPoolSplit(amount: string): {
|
|
|
311
290
|
};
|
|
312
291
|
|
|
313
292
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* Every error carries a stable machine-readable `code` and is thrown at the
|
|
317
|
-
* EARLIEST possible layer: input errors fire client-side before any network or
|
|
318
|
-
* chain call, so a studio never pays gas to discover a typo.
|
|
319
|
-
*/
|
|
320
|
-
type PlaymosErrorCode = "invalid_amount" | "missing_field" | "insufficient_gas" | "wallet_connection" | "payment_failed" | "auth" | "api_error" | "config";
|
|
321
|
-
declare class PlaymosError extends Error {
|
|
322
|
-
readonly code: PlaymosErrorCode;
|
|
323
|
-
/** Optional machine context (e.g. the offending field, the http status). */
|
|
324
|
-
readonly detail?: Record<string, unknown>;
|
|
325
|
-
constructor(code: PlaymosErrorCode, message: string, detail?: Record<string, unknown>);
|
|
326
|
-
}
|
|
327
|
-
/** amount ≤ 0, non-numeric, empty, or more than 2 decimal places. */
|
|
328
|
-
declare class InvalidAmountError extends PlaymosError {
|
|
329
|
-
constructor(amount: unknown);
|
|
330
|
-
}
|
|
331
|
-
/** A required field (sku, playerId, gameId, roundId, agentId…) was empty. */
|
|
332
|
-
declare class MissingFieldError extends PlaymosError {
|
|
333
|
-
constructor(field: string);
|
|
334
|
-
}
|
|
335
|
-
/** gas mode "player" and the player's ETH is too low to cover gas (pre-check, §7). */
|
|
336
|
-
declare class InsufficientGasError extends PlaymosError {
|
|
337
|
-
constructor(detail?: Record<string, unknown>);
|
|
338
|
-
}
|
|
339
|
-
/** The player closed or failed the wallet sheet, or no provider is available. */
|
|
340
|
-
declare class WalletConnectionError extends PlaymosError {
|
|
341
|
-
constructor(message?: string, detail?: Record<string, unknown>);
|
|
342
|
-
}
|
|
343
|
-
/** The on-chain settlement reverted, was cancelled, or timed out. */
|
|
344
|
-
declare class PaymentFailedError extends PlaymosError {
|
|
345
|
-
constructor(message?: string, detail?: Record<string, unknown>);
|
|
346
|
-
}
|
|
347
|
-
/** Bad, missing, or wrong-environment API key (e.g. a pk_test_ key on a live route). */
|
|
348
|
-
declare class AuthError extends PlaymosError {
|
|
349
|
-
constructor(message?: string, detail?: Record<string, unknown>);
|
|
350
|
-
}
|
|
351
|
-
/** The Playmos service returned a non-2xx we don't have a more specific error for. */
|
|
352
|
-
declare class ApiError extends PlaymosError {
|
|
353
|
-
constructor(message: string, detail?: Record<string, unknown>);
|
|
354
|
-
}
|
|
355
|
-
/** SDK misconfiguration (e.g. a missing contract address for on-chain mode). */
|
|
356
|
-
declare class ConfigError extends PlaymosError {
|
|
357
|
-
constructor(message: string, detail?: Record<string, unknown>);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Webhook signature verification (spec §6.2) — server-side only.
|
|
293
|
+
* payout.ts — pure payout math for skill/contest settlement (issue #13).
|
|
362
294
|
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
295
|
+
* Scores NEVER enter this module. The studio ranks wallets (best-first) from its
|
|
296
|
+
* own leaderboard; we only turn that ranking + a payout rule into integer USDC
|
|
297
|
+
* base-unit amounts that sum EXACTLY to the payable pool.
|
|
366
298
|
*
|
|
367
|
-
*
|
|
368
|
-
* rejects stale timestamps to blunt replay. Throws on any mismatch.
|
|
299
|
+
* No network, no chain — fully unit-testable.
|
|
369
300
|
*/
|
|
370
|
-
|
|
371
|
-
|
|
301
|
+
type PayoutRule = {
|
|
302
|
+
kind: "winner-take-all";
|
|
303
|
+
} | {
|
|
304
|
+
kind: "top-n";
|
|
305
|
+
splitsBps: number[];
|
|
306
|
+
} | {
|
|
307
|
+
kind: "custom";
|
|
308
|
+
amounts: string[];
|
|
309
|
+
};
|
|
310
|
+
declare class PayoutError extends Error {
|
|
311
|
+
code: "payout_invalid";
|
|
372
312
|
constructor(message: string);
|
|
373
313
|
}
|
|
374
314
|
/**
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
315
|
+
* Apply the studio's payout rule to a payable pool and ranking (best-first).
|
|
316
|
+
*
|
|
317
|
+
* - Integer math only (bigint micro-USDC).
|
|
318
|
+
* - Sum of amounts == pool exactly; any remainder from floor division goes to rank 1.
|
|
319
|
+
* - Rejects empty ranking, bad splits, more winners than ranking, zero pool.
|
|
378
320
|
*/
|
|
379
|
-
declare function
|
|
380
|
-
|
|
381
|
-
|
|
321
|
+
declare function computePayout(pool: bigint, ranking: `0x${string}`[], rule: PayoutRule): {
|
|
322
|
+
wallet: `0x${string}`;
|
|
323
|
+
amount: bigint;
|
|
324
|
+
}[];
|
|
382
325
|
|
|
383
326
|
/**
|
|
384
327
|
* Money math — exact, integer-only, in USDC micro-units (6 decimals).
|
|
@@ -435,4 +378,132 @@ declare function ulid(seedTime?: number): string;
|
|
|
435
378
|
/** `${prefix}_${ulid()}` — e.g. `pay_01J…`, `entry_01J…`, `idem_01J…`. */
|
|
436
379
|
declare function prefixedId(prefix: string): string;
|
|
437
380
|
|
|
438
|
-
|
|
381
|
+
/**
|
|
382
|
+
* settlement.ts — the x402-ready settlement CORE contract (Phase 0).
|
|
383
|
+
*
|
|
384
|
+
* Two shared shapes every value-movement primitive (`transfer`, `escrow`,
|
|
385
|
+
* `marketplace`) AND the future x402 adapter build on. The whole point of
|
|
386
|
+
* naming them now is that x402 later becomes a thin HTTP-402 adapter with zero
|
|
387
|
+
* rework — see `docs/design/transfer-escrow-marketplace-spec.md` §"Architecture:
|
|
388
|
+
* x402-ready by design".
|
|
389
|
+
*
|
|
390
|
+
* PaymentRequirement — WHAT must be paid. A plain, JSON-safe object. A direct
|
|
391
|
+
* SDK call creates one and satisfies it immediately; an x402 "402 Payment
|
|
392
|
+
* Required" challenge is literally this object serialized onto the wire. So
|
|
393
|
+
* it is designed to round-trip to/from an HTTP 402 body with zero loss —
|
|
394
|
+
* `parsePaymentRequirement(serializePaymentRequirement(req))` is `req`.
|
|
395
|
+
*
|
|
396
|
+
* Authorization — HOW a settlement is authorized. A discriminated union so the
|
|
397
|
+
* settlement core never bakes in "direct SDK signature": today a
|
|
398
|
+
* `wallet-signature` (implemented), tomorrow an `x402-payload` (declared,
|
|
399
|
+
* not settled yet). This union is the seam that keeps x402 out of the core.
|
|
400
|
+
*
|
|
401
|
+
* Nothing here touches the network or the chain — these are the shared data
|
|
402
|
+
* shapes the SDK produces and the service's protocol-agnostic settlement core
|
|
403
|
+
* consumes.
|
|
404
|
+
*/
|
|
405
|
+
|
|
406
|
+
/** The only settlement asset in V1. Named so an x402 challenge carries it verbatim. */
|
|
407
|
+
type SettlementAsset = "USDC";
|
|
408
|
+
/**
|
|
409
|
+
* The shared payment contract — WHAT must be paid, independent of HOW it was
|
|
410
|
+
* requested (direct SDK call today, x402 challenge tomorrow).
|
|
411
|
+
*
|
|
412
|
+
* Every field is a JSON primitive so the object is byte-stable across an HTTP
|
|
413
|
+
* 402 body: mint it once, and both the direct path and the x402 path settle the
|
|
414
|
+
* exact same requirement, idempotent on `id`.
|
|
415
|
+
*/
|
|
416
|
+
interface PaymentRequirement {
|
|
417
|
+
/** The anchor id (`preq_<ulid>`). Settlement idempotency key + x402 challenge id. */
|
|
418
|
+
id: string;
|
|
419
|
+
/** The recipient wallet (the payee). Checksummed or lowercase 0x-address. */
|
|
420
|
+
payTo: `0x${string}`;
|
|
421
|
+
/** USD decimal string ("5.00") — same money convention as `pay()`/`enterRound()`. */
|
|
422
|
+
amount: string;
|
|
423
|
+
/** Only USDC in V1. */
|
|
424
|
+
asset: SettlementAsset;
|
|
425
|
+
/** Which chain settles this requirement. */
|
|
426
|
+
network: Network;
|
|
427
|
+
/** Optional free-form terms/memo, echoed on the receipt and in the 402 body. */
|
|
428
|
+
terms?: string;
|
|
429
|
+
/** ISO-8601 expiry. A challenge/authorization presented after this is rejected. */
|
|
430
|
+
expiresAt: string;
|
|
431
|
+
}
|
|
432
|
+
interface CreatePaymentRequirementInput {
|
|
433
|
+
/** Recipient wallet. */
|
|
434
|
+
payTo: `0x${string}`;
|
|
435
|
+
/** USD decimal string ("5.00"). */
|
|
436
|
+
amount: string;
|
|
437
|
+
/** Settlement chain. */
|
|
438
|
+
network: Network;
|
|
439
|
+
/** Defaults to "USDC". */
|
|
440
|
+
asset?: SettlementAsset;
|
|
441
|
+
/** Optional terms/memo. */
|
|
442
|
+
terms?: string;
|
|
443
|
+
/** Absolute ISO expiry. Overrides `expiresInMs`. Defaults to now + 15 min. */
|
|
444
|
+
expiresAt?: string;
|
|
445
|
+
/** Relative TTL from now, in ms. Ignored if `expiresAt` is set. */
|
|
446
|
+
expiresInMs?: number;
|
|
447
|
+
/** Supply for a deterministic id (e.g. to reuse an upstream id); else a `preq_<ulid>` is minted. */
|
|
448
|
+
id?: string;
|
|
449
|
+
/** Injectable clock for deterministic tests. */
|
|
450
|
+
now?: () => Date;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Build a validated {@link PaymentRequirement}. This is the single producer the
|
|
454
|
+
* direct primitives (`transfer`/`escrow`/`marketplace`) and — later — the x402
|
|
455
|
+
* adapter both call, so every requirement on the wire is shaped identically.
|
|
456
|
+
* Pure: no network, no chain, no id collisions (monotonic ULID).
|
|
457
|
+
*/
|
|
458
|
+
declare function createPaymentRequirement(input: CreatePaymentRequirementInput): PaymentRequirement;
|
|
459
|
+
/**
|
|
460
|
+
* Serialize a requirement to a plain, JSON-safe object — the exact body an x402
|
|
461
|
+
* "402 Payment Required" response carries. Undefined optionals are omitted so
|
|
462
|
+
* the shape is stable across `JSON.stringify` → `JSON.parse` → re-parse.
|
|
463
|
+
*/
|
|
464
|
+
declare function serializePaymentRequirement(req: PaymentRequirement): Record<string, unknown>;
|
|
465
|
+
/**
|
|
466
|
+
* Parse + validate an untrusted object (an HTTP 402 body, a queue message, a
|
|
467
|
+
* direct call) back into a {@link PaymentRequirement}. This is the exact decoder
|
|
468
|
+
* the x402 adapter reuses — it must reject anything malformed with a typed error.
|
|
469
|
+
*/
|
|
470
|
+
declare function parsePaymentRequirement(input: unknown): PaymentRequirement;
|
|
471
|
+
/**
|
|
472
|
+
* A settlement authorized by a wallet — the ONLY variant implemented in Phase 0.
|
|
473
|
+
* Covers both signer models the primitives support:
|
|
474
|
+
* - server-held NPC/agent wallets (the service signs) and
|
|
475
|
+
* - player Base Accounts (the client signs) —
|
|
476
|
+
* carrying either the settlement `txHash` the client already broadcast, or a
|
|
477
|
+
* `signature` + `payload` (e.g. EIP-3009 / EIP-5792 batch params) the service
|
|
478
|
+
* submits. Phase 0 only READS `txHash`; the signed-submit path lands in Phase 1.
|
|
479
|
+
*/
|
|
480
|
+
interface WalletSignatureAuthorization {
|
|
481
|
+
kind: "wallet-signature";
|
|
482
|
+
/** The wallet that authorized the move (payer / signer). */
|
|
483
|
+
from: `0x${string}`;
|
|
484
|
+
/** A settlement tx the client already broadcast (client-signed path). */
|
|
485
|
+
txHash?: `0x${string}`;
|
|
486
|
+
/** An off-chain signature the service submits on the payer's behalf. */
|
|
487
|
+
signature?: `0x${string}`;
|
|
488
|
+
/** Opaque protocol-specific authorization data (EIP-5792 calls, EIP-3009, …). */
|
|
489
|
+
payload?: Record<string, unknown>;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* A settlement authorized by an x402 payment payload (the `X-PAYMENT` header).
|
|
493
|
+
* DECLARED for the Phase 4 HTTP-402 adapter so the core's type surface is final
|
|
494
|
+
* now — but NOT settled in Phase 0: `settle()` throws a clear "not implemented
|
|
495
|
+
* yet" for this variant. This is the seam that keeps x402 out of the core.
|
|
496
|
+
*/
|
|
497
|
+
interface X402PayloadAuthorization {
|
|
498
|
+
kind: "x402-payload";
|
|
499
|
+
/** The raw, opaque x402 payment payload decoded from the request. */
|
|
500
|
+
payload: Record<string, unknown>;
|
|
501
|
+
}
|
|
502
|
+
/** How a settlement is authorized — protocol-agnostic by construction. */
|
|
503
|
+
type Authorization = WalletSignatureAuthorization | X402PayloadAuthorization;
|
|
504
|
+
/** Narrow to the implemented wallet-signature variant. */
|
|
505
|
+
declare function isWalletSignatureAuthorization(auth: Authorization): auth is WalletSignatureAuthorization;
|
|
506
|
+
/** Narrow to the declared-but-unimplemented x402 variant. */
|
|
507
|
+
declare function isX402PayloadAuthorization(auth: Authorization): auth is X402PayloadAuthorization;
|
|
508
|
+
|
|
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 };
|