@playmos/sdk 0.3.10 → 0.3.12

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/server.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var crypto = require('crypto');
4
+ var viem = require('viem');
4
5
 
5
6
  // src/webhooks.ts
6
7
 
@@ -14,6 +15,11 @@ var PlaymosError = class extends Error {
14
15
  Object.setPrototypeOf(this, new.target.prototype);
15
16
  }
16
17
  };
18
+ var ApiError = class extends PlaymosError {
19
+ constructor(message, detail) {
20
+ super("api_error", message, detail);
21
+ }
22
+ };
17
23
 
18
24
  // src/webhooks.ts
19
25
  var WebhookSignatureError = class extends PlaymosError {
@@ -56,6 +62,117 @@ function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
56
62
  }
57
63
  return JSON.parse(body);
58
64
  }
65
+ var toBytes32 = (s) => viem.keccak256(viem.toBytes(s));
66
+
67
+ // src/hasEntered.ts
68
+ var hasEnteredAbi = [
69
+ {
70
+ type: "function",
71
+ name: "hasEntered",
72
+ stateMutability: "view",
73
+ inputs: [
74
+ { name: "roundId", type: "bytes32" },
75
+ { name: "identity", type: "bytes32" }
76
+ ],
77
+ outputs: [{ type: "bool" }]
78
+ }
79
+ ];
80
+ var HAS_ENTERED_DEFAULT_TIMEOUT_MS = 8e3;
81
+ async function hasEntered(args) {
82
+ const rpcUrl = typeof args.rpcUrl === "string" ? args.rpcUrl.trim() : "";
83
+ if (!rpcUrl) {
84
+ throw new ApiError('hasEntered requires a non-empty "rpcUrl" (server-side only \u2014 no wallet provider)');
85
+ }
86
+ if (!args.prizePool || !/^0x[0-9a-fA-F]{40}$/.test(args.prizePool)) {
87
+ throw new ApiError("hasEntered requires prizePool as a 0x-prefixed 20-byte address");
88
+ }
89
+ if (!args.roundKey?.trim() || !args.identity?.trim()) {
90
+ throw new ApiError("hasEntered requires non-empty roundKey and identity (plain strings, same as enterRound)");
91
+ }
92
+ const roundKey = args.roundKey.trim();
93
+ const identity = args.identity.trim();
94
+ if (/^0x[0-9a-fA-F]{64}$/.test(roundKey) || /^0x[0-9a-fA-F]{64}$/.test(identity)) {
95
+ throw new ApiError(
96
+ "hasEntered requires plain-string roundKey/identity (not 0x+64 hex). Pre-hashed pins diverge from enterRound and from service GET /entered. Pass the same plain pins used at enter.",
97
+ { prizePool: args.prizePool, roundKey }
98
+ );
99
+ }
100
+ const data = viem.encodeFunctionData({
101
+ abi: hasEnteredAbi,
102
+ functionName: "hasEntered",
103
+ args: [toBytes32(roundKey), toBytes32(identity)]
104
+ });
105
+ const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs > 0 ? args.timeoutMs : HAS_ENTERED_DEFAULT_TIMEOUT_MS;
106
+ const controller = new AbortController();
107
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
108
+ const onExternalAbort = () => controller.abort();
109
+ if (args.signal) {
110
+ if (args.signal.aborted) controller.abort();
111
+ else args.signal.addEventListener("abort", onExternalAbort, { once: true });
112
+ }
113
+ let res;
114
+ try {
115
+ res = await fetch(rpcUrl, {
116
+ method: "POST",
117
+ headers: { "content-type": "application/json" },
118
+ body: JSON.stringify({
119
+ jsonrpc: "2.0",
120
+ id: 1,
121
+ method: "eth_call",
122
+ params: [{ to: args.prizePool, data }, "latest"]
123
+ }),
124
+ signal: controller.signal
125
+ });
126
+ } catch (e) {
127
+ const msg = e.message ?? String(e);
128
+ const aborted = controller.signal.aborted || e.name === "AbortError" || /abort/i.test(msg);
129
+ throw new ApiError(
130
+ aborted ? `hasEntered RPC timed out after ${timeoutMs}ms (or was aborted)` : `hasEntered RPC request failed: ${msg}`,
131
+ { prizePool: args.prizePool, roundKey }
132
+ );
133
+ } finally {
134
+ clearTimeout(timer);
135
+ if (args.signal) args.signal.removeEventListener("abort", onExternalAbort);
136
+ }
137
+ if (!res.ok) {
138
+ throw new ApiError(`hasEntered RPC HTTP ${res.status}`, {
139
+ prizePool: args.prizePool,
140
+ roundKey
141
+ });
142
+ }
143
+ let json;
144
+ try {
145
+ json = await res.json();
146
+ } catch (e) {
147
+ throw new ApiError(`hasEntered RPC response not JSON: ${e.message}`);
148
+ }
149
+ if (json.error || json.result == null || json.result === "") {
150
+ throw new ApiError(
151
+ `hasEntered eth_call failed: ${json.error?.message ?? `HTTP ${res.status}`}`,
152
+ { prizePool: args.prizePool, roundKey }
153
+ );
154
+ }
155
+ if (json.result === "0x" || json.result === "0x0") {
156
+ throw new ApiError(
157
+ `hasEntered eth_call returned empty data for pool ${args.prizePool} \u2014 wrong address or no contract code`,
158
+ { prizePool: args.prizePool, roundKey }
159
+ );
160
+ }
161
+ try {
162
+ return viem.decodeFunctionResult({
163
+ abi: hasEnteredAbi,
164
+ functionName: "hasEntered",
165
+ data: json.result
166
+ });
167
+ } catch (e) {
168
+ throw new ApiError(
169
+ `hasEntered eth_call result malformed: ${e.message}`,
170
+ { prizePool: args.prizePool, roundKey }
171
+ );
172
+ }
173
+ }
59
174
 
60
175
  exports.WebhookSignatureError = WebhookSignatureError;
176
+ exports.enterPathToBytes32 = toBytes32;
177
+ exports.hasEntered = hasEntered;
61
178
  exports.verifyWebhook = verifyWebhook;
package/dist/server.d.cts CHANGED
@@ -1,4 +1,5 @@
1
- import { Z as PlaymosError, W as WebhookEvent } from './errors-Chizbb96.cjs';
1
+ import { a2 as PlaymosError, W as WebhookEvent } from './errors-CYKgt2VU.cjs';
2
+ export { ad as enterPathToBytes32 } from './errors-CYKgt2VU.cjs';
2
3
 
3
4
  /**
4
5
  * Webhook signature verification (spec §6.2) — server-side only.
@@ -23,4 +24,27 @@ declare function verifyWebhook(rawBody: string | Buffer, signatureHeader: string
23
24
  toleranceSeconds?: number;
24
25
  }): WebhookEvent;
25
26
 
26
- export { WebhookSignatureError, verifyWebhook };
27
+ interface HasEnteredArgs {
28
+ prizePool: `0x${string}`;
29
+ /** Same plain string pin as enterRound roundKey / body roundId. */
30
+ roundKey: string;
31
+ /** Same plain string pin as enterRound identity. */
32
+ identity: string;
33
+ /** Required JSON-RPC URL for eth_call (server-owned). */
34
+ rpcUrl: string;
35
+ /**
36
+ * Optional deadline for the RPC fetch (ms). Default 8000.
37
+ * On timeout throws ApiError (never returns false).
38
+ */
39
+ timeoutMs?: number;
40
+ /** Optional AbortSignal; combined with timeoutMs when both set. */
41
+ signal?: AbortSignal;
42
+ }
43
+ /**
44
+ * On-chain hasEntered for admit-to-session from a game backend.
45
+ * @returns true only when the chain read succeeds and returns true.
46
+ * @throws ApiError when eth_call fails, times out, or response is malformed.
47
+ */
48
+ declare function hasEntered(args: HasEnteredArgs): Promise<boolean>;
49
+
50
+ export { type HasEnteredArgs, WebhookSignatureError, hasEntered, verifyWebhook };
package/dist/server.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { Z as PlaymosError, W as WebhookEvent } from './errors-Chizbb96.js';
1
+ import { a2 as PlaymosError, W as WebhookEvent } from './errors-CYKgt2VU.js';
2
+ export { ad as enterPathToBytes32 } from './errors-CYKgt2VU.js';
2
3
 
3
4
  /**
4
5
  * Webhook signature verification (spec §6.2) — server-side only.
@@ -23,4 +24,27 @@ declare function verifyWebhook(rawBody: string | Buffer, signatureHeader: string
23
24
  toleranceSeconds?: number;
24
25
  }): WebhookEvent;
25
26
 
26
- export { WebhookSignatureError, verifyWebhook };
27
+ interface HasEnteredArgs {
28
+ prizePool: `0x${string}`;
29
+ /** Same plain string pin as enterRound roundKey / body roundId. */
30
+ roundKey: string;
31
+ /** Same plain string pin as enterRound identity. */
32
+ identity: string;
33
+ /** Required JSON-RPC URL for eth_call (server-owned). */
34
+ rpcUrl: string;
35
+ /**
36
+ * Optional deadline for the RPC fetch (ms). Default 8000.
37
+ * On timeout throws ApiError (never returns false).
38
+ */
39
+ timeoutMs?: number;
40
+ /** Optional AbortSignal; combined with timeoutMs when both set. */
41
+ signal?: AbortSignal;
42
+ }
43
+ /**
44
+ * On-chain hasEntered for admit-to-session from a game backend.
45
+ * @returns true only when the chain read succeeds and returns true.
46
+ * @throws ApiError when eth_call fails, times out, or response is malformed.
47
+ */
48
+ declare function hasEntered(args: HasEnteredArgs): Promise<boolean>;
49
+
50
+ export { type HasEnteredArgs, WebhookSignatureError, hasEntered, verifyWebhook };
package/dist/server.js CHANGED
@@ -1,5 +1,7 @@
1
- import { PlaymosError } from './chunk-UZECDT6F.js';
1
+ import { PlaymosError, ApiError, toBytes32 } from './chunk-2CW4U5YB.js';
2
+ export { toBytes32 as enterPathToBytes32 } from './chunk-2CW4U5YB.js';
2
3
  import { timingSafeEqual, createHmac } from 'crypto';
4
+ import { encodeFunctionData, decodeFunctionResult } from 'viem';
3
5
 
4
6
  var WebhookSignatureError = class extends PlaymosError {
5
7
  constructor(message) {
@@ -41,5 +43,111 @@ function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
41
43
  }
42
44
  return JSON.parse(body);
43
45
  }
46
+ var hasEnteredAbi = [
47
+ {
48
+ type: "function",
49
+ name: "hasEntered",
50
+ stateMutability: "view",
51
+ inputs: [
52
+ { name: "roundId", type: "bytes32" },
53
+ { name: "identity", type: "bytes32" }
54
+ ],
55
+ outputs: [{ type: "bool" }]
56
+ }
57
+ ];
58
+ var HAS_ENTERED_DEFAULT_TIMEOUT_MS = 8e3;
59
+ async function hasEntered(args) {
60
+ const rpcUrl = typeof args.rpcUrl === "string" ? args.rpcUrl.trim() : "";
61
+ if (!rpcUrl) {
62
+ throw new ApiError('hasEntered requires a non-empty "rpcUrl" (server-side only \u2014 no wallet provider)');
63
+ }
64
+ if (!args.prizePool || !/^0x[0-9a-fA-F]{40}$/.test(args.prizePool)) {
65
+ throw new ApiError("hasEntered requires prizePool as a 0x-prefixed 20-byte address");
66
+ }
67
+ if (!args.roundKey?.trim() || !args.identity?.trim()) {
68
+ throw new ApiError("hasEntered requires non-empty roundKey and identity (plain strings, same as enterRound)");
69
+ }
70
+ const roundKey = args.roundKey.trim();
71
+ const identity = args.identity.trim();
72
+ if (/^0x[0-9a-fA-F]{64}$/.test(roundKey) || /^0x[0-9a-fA-F]{64}$/.test(identity)) {
73
+ throw new ApiError(
74
+ "hasEntered requires plain-string roundKey/identity (not 0x+64 hex). Pre-hashed pins diverge from enterRound and from service GET /entered. Pass the same plain pins used at enter.",
75
+ { prizePool: args.prizePool, roundKey }
76
+ );
77
+ }
78
+ const data = encodeFunctionData({
79
+ abi: hasEnteredAbi,
80
+ functionName: "hasEntered",
81
+ args: [toBytes32(roundKey), toBytes32(identity)]
82
+ });
83
+ const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs > 0 ? args.timeoutMs : HAS_ENTERED_DEFAULT_TIMEOUT_MS;
84
+ const controller = new AbortController();
85
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
86
+ const onExternalAbort = () => controller.abort();
87
+ if (args.signal) {
88
+ if (args.signal.aborted) controller.abort();
89
+ else args.signal.addEventListener("abort", onExternalAbort, { once: true });
90
+ }
91
+ let res;
92
+ try {
93
+ res = await fetch(rpcUrl, {
94
+ method: "POST",
95
+ headers: { "content-type": "application/json" },
96
+ body: JSON.stringify({
97
+ jsonrpc: "2.0",
98
+ id: 1,
99
+ method: "eth_call",
100
+ params: [{ to: args.prizePool, data }, "latest"]
101
+ }),
102
+ signal: controller.signal
103
+ });
104
+ } catch (e) {
105
+ const msg = e.message ?? String(e);
106
+ const aborted = controller.signal.aborted || e.name === "AbortError" || /abort/i.test(msg);
107
+ throw new ApiError(
108
+ aborted ? `hasEntered RPC timed out after ${timeoutMs}ms (or was aborted)` : `hasEntered RPC request failed: ${msg}`,
109
+ { prizePool: args.prizePool, roundKey }
110
+ );
111
+ } finally {
112
+ clearTimeout(timer);
113
+ if (args.signal) args.signal.removeEventListener("abort", onExternalAbort);
114
+ }
115
+ if (!res.ok) {
116
+ throw new ApiError(`hasEntered RPC HTTP ${res.status}`, {
117
+ prizePool: args.prizePool,
118
+ roundKey
119
+ });
120
+ }
121
+ let json;
122
+ try {
123
+ json = await res.json();
124
+ } catch (e) {
125
+ throw new ApiError(`hasEntered RPC response not JSON: ${e.message}`);
126
+ }
127
+ if (json.error || json.result == null || json.result === "") {
128
+ throw new ApiError(
129
+ `hasEntered eth_call failed: ${json.error?.message ?? `HTTP ${res.status}`}`,
130
+ { prizePool: args.prizePool, roundKey }
131
+ );
132
+ }
133
+ if (json.result === "0x" || json.result === "0x0") {
134
+ throw new ApiError(
135
+ `hasEntered eth_call returned empty data for pool ${args.prizePool} \u2014 wrong address or no contract code`,
136
+ { prizePool: args.prizePool, roundKey }
137
+ );
138
+ }
139
+ try {
140
+ return decodeFunctionResult({
141
+ abi: hasEnteredAbi,
142
+ functionName: "hasEntered",
143
+ data: json.result
144
+ });
145
+ } catch (e) {
146
+ throw new ApiError(
147
+ `hasEntered eth_call result malformed: ${e.message}`,
148
+ { prizePool: args.prizePool, roundKey }
149
+ );
150
+ }
151
+ }
44
152
 
45
- export { WebhookSignatureError, verifyWebhook };
153
+ export { WebhookSignatureError, hasEntered, verifyWebhook };
package/package.json CHANGED
@@ -1,17 +1,12 @@
1
1
  {
2
2
  "name": "@playmos/sdk",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Playmos SDK — stablecoin payments for games on Base. One SDK for IAP (1%), skill-game prize-pool entries (10%, 60/30/10), and agent economies. USD in, USDC on-chain, no crypto UX for players.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
7
- "homepage": "https://playmos-docs-public.vercel.app/docs",
7
+ "homepage": "https://playmos-docs-public.vercel.app/",
8
8
  "bugs": {
9
- "url": "https://playmos-docs-public.vercel.app/docs"
10
- },
11
- "repository": {
12
- "type": "git",
13
- "url": "git+https://github.com/playmos-labs/playmos-sdk.git",
14
- "directory": "sdk"
9
+ "url": "https://playmos-docs-public.vercel.app/"
15
10
  },
16
11
  "publishConfig": {
17
12
  "access": "public"
@@ -38,6 +33,7 @@
38
33
  "dist",
39
34
  "!dist/**/*.map",
40
35
  "README.md",
36
+ "CHANGELOG.md",
41
37
  "package.json"
42
38
  ],
43
39
  "scripts": {
@@ -50,13 +46,13 @@
50
46
  "test:watch": "vitest"
51
47
  },
52
48
  "dependencies": {
53
- "viem": "^2.21.0"
49
+ "viem": "^2.55.11"
54
50
  },
55
51
  "devDependencies": {
56
- "@types/node": "^22.0.0",
52
+ "@types/node": "^26.1.2",
57
53
  "tsup": "^8.3.0",
58
54
  "typescript": "^5.6.0",
59
- "vitest": "^2.1.0"
55
+ "vitest": "^3.2.6"
60
56
  },
61
57
  "engines": {
62
58
  "node": ">=18 <25"
@@ -1,83 +0,0 @@
1
- // src/errors.ts
2
- var PlaymosError = class extends Error {
3
- constructor(code, message, detail) {
4
- super(message);
5
- this.name = new.target.name;
6
- this.code = code;
7
- this.detail = detail;
8
- Object.setPrototypeOf(this, new.target.prototype);
9
- }
10
- };
11
- var InvalidAmountError = class extends PlaymosError {
12
- constructor(amount) {
13
- super(
14
- "invalid_amount",
15
- `Invalid amount: ${JSON.stringify(amount)}. Provide a positive USD decimal string with at most 2 decimals, e.g. "4.99".`,
16
- { amount }
17
- );
18
- }
19
- };
20
- var MissingFieldError = class extends PlaymosError {
21
- constructor(field) {
22
- super("missing_field", `Missing required field: "${field}".`, { field });
23
- }
24
- };
25
- var InsufficientGasError = class extends PlaymosError {
26
- constructor(detail) {
27
- super(
28
- "insufficient_gas",
29
- `The player's wallet has too little ETH to pay gas. Ask them to add a little ETH, or switch to gas.mode: "sponsored".`,
30
- detail
31
- );
32
- }
33
- };
34
- var WalletConnectionError = class extends PlaymosError {
35
- constructor(message = "Could not connect the player's wallet.", detail) {
36
- super("wallet_connection", message, detail);
37
- }
38
- };
39
- var WalletTimeoutError = class extends PlaymosError {
40
- constructor(message = "The wallet did not respond in time. Ask the player to approve the prompt, or retry.", detail) {
41
- super("wallet_timeout", message, detail);
42
- }
43
- };
44
- var PaymentFailedError = class extends PlaymosError {
45
- constructor(message = "The on-chain payment did not complete.", detail) {
46
- super("payment_failed", message, detail);
47
- }
48
- };
49
- var AlreadyEnteredError = class extends PlaymosError {
50
- constructor(detail) {
51
- super(
52
- "already_entered",
53
- "This identity already entered this round on-chain. For pay-per-play, pass a unique identity per attempt (not just the wallet address).",
54
- detail
55
- );
56
- }
57
- };
58
- var AuthError = class extends PlaymosError {
59
- constructor(message = "Invalid or missing API key.", detail) {
60
- super("auth", message, detail);
61
- }
62
- };
63
- var ApiError = class extends PlaymosError {
64
- constructor(message, detail) {
65
- super("api_error", message, detail);
66
- }
67
- };
68
- var ConfigError = class extends PlaymosError {
69
- constructor(message, detail) {
70
- super("config", message, detail);
71
- }
72
- };
73
- var NothingToWithdrawError = class extends PlaymosError {
74
- constructor(detail) {
75
- super(
76
- "nothing_to_withdraw",
77
- "Nothing to withdraw \u2014 this wallet has no credited prize balance on this PrizePool (round not settled for them, or already claimed).",
78
- detail
79
- );
80
- }
81
- };
82
-
83
- export { AlreadyEnteredError, ApiError, AuthError, ConfigError, InsufficientGasError, InvalidAmountError, MissingFieldError, NothingToWithdrawError, PaymentFailedError, PlaymosError, WalletConnectionError, WalletTimeoutError };