zcashname-sdk 0.8.3 → 0.8.5

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/zns.cjs CHANGED
@@ -33,35 +33,94 @@ __export(zns_exports, {
33
33
  BUY_COMMISSION: () => BUY_COMMISSION,
34
34
  DEFAULT_URL: () => DEFAULT_URL,
35
35
  LIST_COMMISSION: () => LIST_COMMISSION,
36
- MAINNET_UIVK: () => MAINNET_UIVK,
37
- TESTNET_UIVK: () => TESTNET_UIVK,
38
- ZNS: () => ZNS,
39
- ZNS_ACTIONS: () => ZNS_ACTIONS
36
+ NETWORKS: () => NETWORKS,
37
+ ZNS: () => ZNS
40
38
  });
41
39
  module.exports = __toCommonJS(zns_exports);
42
40
  var ed25519 = __toESM(require("@noble/ed25519"), 1);
43
41
  var import_bech32 = require("bech32");
44
42
 
45
43
  // src/types.ts
46
- var BUY_COMMISSION = 1e4;
47
- var LIST_COMMISSION = 1e6;
44
+ var ZNS_ACTIONS = ["CLAIM", "BUY", "UPDATE", "LIST", "DELIST", "RELEASE"];
48
45
 
49
46
  // src/zns.ts
50
47
  var DEFAULT_URL = "https://light.zcash.me/zns-testnet";
51
- var TESTNET_UIVK = "uivktest1hzw7wyadutvzfgpna80yftsk5l7jeyu2p5me5quvp28tytxueta00cx4068wnlzcv7tx9n3t3gfhsy83pe4y6jrhxtzaq0hj6xtg5zrk2dn7zen3vns2a5pgs4fxdjlletmqrhfa42";
52
- var MAINNET_UIVK = "uivk1gl26qy0xjja7lqhyg3pf0x4j4j66kqwewrjkdcg28eqq4wgtzjmujpee7x9cs2ec9xhnlgrm8ptlw8z80j2aryw8nqtssser2ys778a0s00uvgkdjnfr58sndhfvc3f4zqjs6ywva6";
53
- var KNOWN_UIVKS = [TESTNET_UIVK, MAINNET_UIVK];
54
- var REGISTRY_ADDRESSES = {
55
- testnet: "utest1f32kn6c4zvn54xr8wfsnxmj9hzpu2mwgtxzpzwcw34906tdccdvzs0z2dx38lly7tpan77x6udt8pjczqm22ymsdhlz9j0tk5yq664nl",
56
- mainnet: "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9"
48
+ var BUY_COMMISSION = 1e4;
49
+ var LIST_COMMISSION = 1e6;
50
+ var NETWORKS = {
51
+ testnet: {
52
+ url: "https://light.zcash.me/zns-testnet",
53
+ registryAddress: "utest1f32kn6c4zvn54xr8wfsnxmj9hzpu2mwgtxzpzwcw34906tdccdvzs0z2dx38lly7tpan77x6udt8pjczqm22ymsdhlz9j0tk5yq664nl",
54
+ uivk: "utest1hzw7wyadutvzfgpna80yftsk5l7jeyu2p5me5quvp28tytxueta00cx4068wnlzcv7tx9n3t3gfhsy83pe4y6jrhxtzaq0hj6xtg5zrk2dn7zen3vns2a5pgs4fxdjlletmqrhfa42"
55
+ },
56
+ mainnet: {
57
+ url: "https://light.zcash.me/zns",
58
+ registryAddress: "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9",
59
+ uivk: "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9"
60
+ }
57
61
  };
58
- var ZNS_ACTIONS = ["CLAIM", "BUY", "UPDATE", "LIST", "DELIST", "RELEASE"];
59
62
  var NAME_RE = /^[a-z0-9]{1,62}$/;
63
+ function isValidName(name) {
64
+ return NAME_RE.test(name);
65
+ }
66
+ function normalizeApiResponse(obj) {
67
+ if (Array.isArray(obj)) return obj.map((item) => normalizeApiResponse(item));
68
+ if (obj && typeof obj === "object") {
69
+ return Object.fromEntries(
70
+ Object.entries(obj).map(([k, v]) => [
71
+ k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
72
+ normalizeApiResponse(v)
73
+ ])
74
+ );
75
+ }
76
+ return obj;
77
+ }
60
78
  function isWholeNumber(value) {
61
79
  return /^\d+$/.test(value) && !value.startsWith("0") || value === "0";
62
80
  }
63
- function mk(level, action, canonical, message) {
64
- return { valid: level === "valid", action, canonicalAction: canonical, message, level };
81
+ function isValidUnifiedAddress(address) {
82
+ if (!address) return false;
83
+ if (address.startsWith("utest1")) return true;
84
+ if (address.startsWith("u1")) return true;
85
+ try {
86
+ const decoded = import_bech32.bech32m.decode(address);
87
+ return decoded.prefix === "u" || decoded.prefix === "utest";
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+ function isValidTransparentAddress(address) {
93
+ if (!address) return false;
94
+ const validPrefixes = ["t1", "t3", "tm", "tn"];
95
+ if (!validPrefixes.some((p) => address.startsWith(p))) return false;
96
+ if (address.length < 26 || address.length > 35) return false;
97
+ const base58Regex = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/;
98
+ return base58Regex.test(address);
99
+ }
100
+ var PAYLOAD_RULES = {
101
+ CLAIM: { format: "CLAIM:<name>:<ua>", checks: ["name", "ua"] },
102
+ BUY: { format: "BUY:<name>:<ua>", checks: ["name", "ua"] },
103
+ UPDATE: { format: "UPDATE:<name>:<ua>:<nonce>", checks: ["name", "ua", "nonce"] },
104
+ LIST: { format: "LIST:<name>:<price>:<pay_taddr>:<nonce>", checks: ["name", "price", "pay_taddr", "nonce"] },
105
+ DELIST: { format: "DELIST:<name>:<nonce>", checks: ["name", "nonce"] },
106
+ RELEASE: { format: "RELEASE:<name>:<nonce>", checks: ["name", "nonce"] }
107
+ };
108
+ function validateField(value, type) {
109
+ switch (type) {
110
+ case "name":
111
+ return NAME_RE.test(value) ? null : "Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.";
112
+ case "ua":
113
+ return isValidUnifiedAddress(value) ? null : `Invalid unified address: "${value}".`;
114
+ case "price":
115
+ return isWholeNumber(value) && Number(value) > 0 ? null : "Price must be a positive whole number in zats.";
116
+ case "nonce":
117
+ return isWholeNumber(value) ? null : "Nonce must be a whole number.";
118
+ case "pay_taddr":
119
+ return isValidTransparentAddress(value) ? null : `Invalid transparent address: "${value}".`;
120
+ }
121
+ }
122
+ function buildValidationResult(level, action, message) {
123
+ return { valid: level === "valid", action, message, level };
65
124
  }
66
125
  var ZNS = class {
67
126
  /**
@@ -73,16 +132,28 @@ var ZNS = class {
73
132
  constructor(options) {
74
133
  this.rpcId = 0;
75
134
  this._verified = false;
135
+ /** Validate a Zcash Unified Address format.
136
+ * Accepts both mainnet ('u') and testnet ('utest') prefixes.
137
+ * Performs basic format validation but NOT full bech32m checksum verification.
138
+ * Returns true if the address looks like a unified address, false otherwise.
139
+ *
140
+ * @todo(F4Jumble) Upgrade to full ZIP-316 decoding with F4Jumble to:
141
+ * - Parse actual typecodes from address items
142
+ * - Validate F4Jumble checksum (not just bech32m)
143
+ * - Optionally enforce: address must contain at least one Orchard receiver (typecode 0x03)
144
+ * Requires @noble/hashes (blake2b) implementation of F4Jumble inverse. */
145
+ this.isValidUnifiedAddress = isValidUnifiedAddress;
146
+ this.isValidTransparentAddress = isValidTransparentAddress;
76
147
  this.network = options?.network ?? "testnet";
77
- this.url = options?.url ?? DEFAULT_URL;
148
+ this.url = options?.url ?? NETWORKS[this.network].url;
78
149
  }
79
150
  /**
80
151
  * Verifies that the connected server is a known ZNS instance.
81
152
  * @throws Error if the server's UIVK is not recognized
82
153
  */
83
154
  async verify() {
84
- const status = await this.rpc("status");
85
- if (!KNOWN_UIVKS.includes(status.uivk)) {
155
+ const status = await this.status();
156
+ if (status.uivk !== NETWORKS[this.network].uivk) {
86
157
  throw new Error(
87
158
  `UIVK mismatch: indexer returned "${status.uivk.slice(0, 20)}..." which is not a known ZNS instance`
88
159
  );
@@ -95,72 +166,61 @@ var ZNS = class {
95
166
  }
96
167
  /** Get the registry address for the current network. */
97
168
  get registryAddress() {
98
- const addr = REGISTRY_ADDRESSES[this.network];
99
- if (!addr) {
100
- throw new Error(`Unknown network: ${this.network}`);
101
- }
102
- return addr;
169
+ return NETWORKS[this.network].registryAddress;
103
170
  }
104
171
  /** Fetch current server status including pricing and configuration. */
105
172
  async status() {
106
- return this.rpc("status");
173
+ const raw = await this.rpc("status");
174
+ return normalizeApiResponse(raw);
107
175
  }
108
176
  /** Resolve a ZNS name to its registration. Returns null if not registered. */
109
177
  async resolveName(name) {
110
- return this.rpc("resolve", { query: name });
178
+ const raw = await this.rpc("resolve", { query: name });
179
+ return raw ? normalizeApiResponse(raw) : null;
111
180
  }
112
181
  /** Resolve a Zcash Unified Address to all names pointing to it. Returns empty array if none.
113
182
  * Supports pagination with limit (default 50, max 500) and offset (default 0). */
114
183
  async resolveAddress(address, limit, offset) {
115
- return this.rpc("resolve", {
184
+ const raw = await this.rpc("resolve", {
116
185
  query: address,
117
186
  limit,
118
187
  offset
119
188
  });
189
+ return raw.map((r) => normalizeApiResponse(r));
120
190
  }
121
191
  /** List all registered names. Useful for explorers or browsers.
122
192
  * Supports pagination with limit (default 50, max 500) and offset (default 0). */
123
193
  async listAllRegistrations(limit, offset) {
124
- return this.rpc("resolve", {
194
+ const raw = await this.rpc("resolve", {
125
195
  query: "",
126
196
  limit,
127
197
  offset
128
198
  });
199
+ return raw.map((r) => normalizeApiResponse(r));
129
200
  }
130
201
  /** Check if a name is available for registration.
131
202
  * Returns false immediately for invalid names without hitting the server. */
132
203
  async isAvailable(name) {
133
- if (!this.isValidName(name)) return false;
204
+ if (!isValidName(name)) return false;
134
205
  const result = await this.resolveName(name);
135
206
  return result === null;
136
207
  }
137
- /** Validate a Zcash Unified Address format.
138
- * Accepts both mainnet ('u') and testnet ('utest') prefixes.
139
- * Performs basic format validation but NOT full bech32m checksum verification.
140
- * Returns true if the address looks like a unified address, false otherwise. */
141
- isValidUnifiedAddress(address) {
142
- if (!address) return false;
143
- if (address.startsWith("utest1")) return true;
144
- if (address.startsWith("u1")) return true;
145
- try {
146
- const decoded = import_bech32.bech32m.decode(address);
147
- return decoded.prefix === "u" || decoded.prefix === "utest";
148
- } catch {
149
- return false;
150
- }
151
- }
152
208
  async listings(limit, offset) {
153
- const result = await this.rpc("listings", {
209
+ const raw = await this.rpc("listings", {
154
210
  limit,
155
211
  offset
156
212
  });
157
- return result;
213
+ return {
214
+ listings: raw.listings.map((l) => normalizeApiResponse(l)),
215
+ total: raw.total
216
+ };
158
217
  }
159
218
  async events(filter) {
160
- return this.rpc(
219
+ const raw = await this.rpc(
161
220
  "events",
162
- filter ?? {}
221
+ normalizeApiResponse(filter ?? {})
163
222
  );
223
+ return normalizeApiResponse(raw);
164
224
  }
165
225
  /**
166
226
  * Verify a listing's signature.
@@ -170,7 +230,7 @@ var ZNS = class {
170
230
  */
171
231
  async verifyListing(listing, adminPubkey) {
172
232
  const pubkey = listing.pubkey ?? adminPubkey;
173
- const payload = `LIST:${listing.name}:${listing.price}:${listing.pay_taddr}:${listing.nonce}`;
233
+ const payload = `LIST:${listing.name}:${listing.price}:${listing.payTaddr}:${listing.nonce}`;
174
234
  return this.verifyEd25519(payload, listing.signature, pubkey);
175
235
  }
176
236
  /**
@@ -186,10 +246,6 @@ var ZNS = class {
186
246
  if (!payload) return false;
187
247
  return this.verifyEd25519(payload, reg.signature, pubkey);
188
248
  }
189
- /** Check if a name is valid format (lowercase alphanumeric, 1-62 chars). */
190
- isValidName(name) {
191
- return NAME_RE.test(name);
192
- }
193
249
  /**
194
250
  * Validate a signing payload string against the ZNS memo format spec.
195
251
  *
@@ -216,7 +272,6 @@ var ZNS = class {
216
272
  return {
217
273
  valid: false,
218
274
  action: "",
219
- canonicalAction: null,
220
275
  message: "Empty payload.",
221
276
  level: "invalid"
222
277
  };
@@ -226,83 +281,29 @@ var ZNS = class {
226
281
  return {
227
282
  valid: false,
228
283
  action: raw.toUpperCase(),
229
- canonicalAction: null,
230
284
  message: "Missing colon separator. Expected format: ACTION:field1:field2:...",
231
285
  level: "invalid"
232
286
  };
233
287
  }
234
- const actionUpper = raw.slice(0, colonIdx).toUpperCase();
235
- const actionLower = raw.slice(0, colonIdx).toLowerCase();
288
+ const action = raw.slice(0, colonIdx).toUpperCase();
236
289
  const rest = raw.slice(colonIdx + 1);
237
290
  const parts = rest.split(":");
238
- switch (actionLower) {
239
- case "claim": {
240
- if (parts.length !== 2)
241
- return mk("invalid", actionUpper, "claim", `Expected CLAIM:<name>:<ua>.`);
242
- if (!NAME_RE.test(parts[0]))
243
- return mk("invalid", actionUpper, "claim", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
244
- if (!this.isValidUnifiedAddress(parts[1]))
245
- return mk("invalid", actionUpper, "claim", `Invalid unified address: "${parts[1]}".`);
246
- return mk("valid", actionUpper, "claim", "Valid CLAIM payload.");
247
- }
248
- case "buy": {
249
- if (parts.length !== 2)
250
- return mk("invalid", actionUpper, "buy", `Expected BUY:<name>:<buyer_ua>.`);
251
- if (!NAME_RE.test(parts[0]))
252
- return mk("invalid", actionUpper, "buy", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
253
- if (!this.isValidUnifiedAddress(parts[1]))
254
- return mk("invalid", actionUpper, "buy", `Invalid unified address: "${parts[1]}".`);
255
- return mk("valid", actionUpper, "buy", "Valid BUY payload.");
256
- }
257
- case "update": {
258
- if (parts.length !== 3)
259
- return mk("invalid", actionUpper, "update", `Expected UPDATE:<name>:<ua>:<nonce>.`);
260
- if (!NAME_RE.test(parts[0]))
261
- return mk("invalid", actionUpper, "update", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
262
- if (!this.isValidUnifiedAddress(parts[1]))
263
- return mk("invalid", actionUpper, "update", `Invalid unified address: "${parts[1]}".`);
264
- if (!isWholeNumber(parts[2]))
265
- return mk("invalid", actionUpper, "update", `Nonce must be a whole number.`);
266
- return mk("valid", actionUpper, "update", "Valid UPDATE payload.");
267
- }
268
- case "list": {
269
- if (parts.length !== 4)
270
- return mk("invalid", actionUpper, "list", `Expected LIST:<name>:<price_zats>:<pay_taddr>:<nonce>.`);
271
- if (!NAME_RE.test(parts[0]))
272
- return mk("invalid", actionUpper, "list", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
273
- if (!isWholeNumber(parts[1]) || Number(parts[1]) <= 0)
274
- return mk("invalid", actionUpper, "list", `Price must be a positive whole number in zats.`);
275
- if (!isWholeNumber(parts[3]))
276
- return mk("invalid", actionUpper, "list", `Nonce must be a whole number.`);
277
- return mk("valid", actionUpper, "list", "Valid LIST payload.");
278
- }
279
- case "delist": {
280
- if (parts.length !== 2)
281
- return mk("invalid", actionUpper, "delist", `Expected DELIST:<name>:<nonce>.`);
282
- if (!NAME_RE.test(parts[0]))
283
- return mk("invalid", actionUpper, "delist", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
284
- if (!isWholeNumber(parts[1]))
285
- return mk("invalid", actionUpper, "delist", `Nonce must be a whole number.`);
286
- return mk("valid", actionUpper, "delist", "Valid DELIST payload.");
287
- }
288
- case "release": {
289
- if (parts.length !== 2)
290
- return mk("invalid", actionUpper, "release", `Expected RELEASE:<name>:<nonce>.`);
291
- if (!NAME_RE.test(parts[0]))
292
- return mk("invalid", actionUpper, "release", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
293
- if (!isWholeNumber(parts[1]))
294
- return mk("invalid", actionUpper, "release", `Nonce must be a whole number.`);
295
- return mk("valid", actionUpper, "release", "Valid RELEASE payload.");
296
- }
297
- default:
298
- return {
299
- valid: false,
300
- action: actionUpper,
301
- canonicalAction: null,
302
- message: `Unrecognized action "${actionUpper}". Valid actions: ${ZNS_ACTIONS.join(", ")}.`,
303
- level: "unrecognized"
304
- };
291
+ if (!ZNS_ACTIONS.includes(action)) {
292
+ return {
293
+ valid: false,
294
+ action,
295
+ message: `Unrecognized action "${action}". Valid actions: ${ZNS_ACTIONS.join(", ")}.`,
296
+ level: "unrecognized"
297
+ };
298
+ }
299
+ const rule = PAYLOAD_RULES[action];
300
+ if (parts.length !== rule.checks.length)
301
+ return buildValidationResult("invalid", action, `Expected ${rule.format}.`);
302
+ for (let i = 0; i < rule.checks.length; i++) {
303
+ const err = validateField(parts[i], rule.checks[i]);
304
+ if (err) return buildValidationResult("invalid", action, err);
305
305
  }
306
+ return buildValidationResult("valid", action, `Valid ${action} payload.`);
306
307
  }
307
308
  /**
308
309
  * Get the claim cost in zatoshis for a name of given length.
@@ -345,7 +346,7 @@ var ZNS = class {
345
346
  */
346
347
  prepareClaim(name, address, cost) {
347
348
  this.requireValidName(name);
348
- if (!this.isValidUnifiedAddress(address)) {
349
+ if (!isValidUnifiedAddress(address)) {
349
350
  throw new Error(`Invalid Zcash Unified Address: ${address}`);
350
351
  }
351
352
  return {
@@ -360,16 +361,16 @@ var ZNS = class {
360
361
  }
361
362
  };
362
363
  }
363
- prepareList(name, price, pay_taddr, nonce) {
364
+ prepareList(name, price, payTaddr, nonce) {
364
365
  this.requireValidName(name);
365
366
  return {
366
367
  name,
367
368
  price,
368
- pay_taddr,
369
+ payTaddr,
369
370
  nonce,
370
- payload: `LIST:${name}:${price}:${pay_taddr}:${nonce}`,
371
+ payload: `LIST:${name}:${price}:${payTaddr}:${nonce}`,
371
372
  complete: (signature, userPubkey) => {
372
- const memo = userPubkey ? `ZNS:LIST:${name}:${price}:${pay_taddr}:${nonce}:${signature}:${userPubkey}` : `ZNS:LIST:${name}:${price}:${pay_taddr}:${nonce}:${signature}`;
373
+ const memo = userPubkey ? `ZNS:LIST:${name}:${price}:${payTaddr}:${nonce}:${signature}:${userPubkey}` : `ZNS:LIST:${name}:${price}:${payTaddr}:${nonce}:${signature}`;
373
374
  return { memo, uri: this.buildZcashUri(this.registryAddress, LIST_COMMISSION, memo) };
374
375
  }
375
376
  };
@@ -388,7 +389,7 @@ var ZNS = class {
388
389
  }
389
390
  prepareUpdate(name, newAddress, nonce) {
390
391
  this.requireValidName(name);
391
- if (!this.isValidUnifiedAddress(newAddress)) {
392
+ if (!isValidUnifiedAddress(newAddress)) {
392
393
  throw new Error(`Invalid Zcash Unified Address: ${newAddress}`);
393
394
  }
394
395
  return {
@@ -443,7 +444,7 @@ var ZNS = class {
443
444
  }
444
445
  // ── Private helpers ────────────────────────────────────────────────────────
445
446
  registrationPayload(reg) {
446
- switch (reg.last_action) {
447
+ switch (reg.lastAction) {
447
448
  case "CLAIM":
448
449
  return `CLAIM:${reg.name}:${reg.address}`;
449
450
  case "BUY":
@@ -470,7 +471,7 @@ var ZNS = class {
470
471
  }
471
472
  }
472
473
  requireValidName(name) {
473
- if (!NAME_RE.test(name)) throw new Error(`Invalid ZNS name: ${name}`);
474
+ if (!isValidName(name)) throw new Error(`Invalid ZNS name: ${name}`);
474
475
  }
475
476
  /** Build a ZIP-321 URI. Amount is in zatoshis and will be converted to ZEC for the URI. */
476
477
  buildZcashUri(address, amountZats, memo) {
@@ -536,8 +537,6 @@ var ZNS = class {
536
537
  BUY_COMMISSION,
537
538
  DEFAULT_URL,
538
539
  LIST_COMMISSION,
539
- MAINNET_UIVK,
540
- TESTNET_UIVK,
541
- ZNS,
542
- ZNS_ACTIONS
540
+ NETWORKS,
541
+ ZNS
543
542
  });
package/dist/zns.d.cts CHANGED
@@ -1,59 +1,70 @@
1
- /** Amount in zatoshis (1 ZEC = 100,000,000 zats).
2
- * All monetary values in ZNS are denominated in zats.
3
- * Note: JavaScript number precision degrades above 2^53 (~9e15 zats, or ~90M ZEC) */
1
+ /**
2
+ * ZNS TypeScript SDK types.
3
+ *
4
+ * API responses are converted from snake_case (Rust convention) to camelCase
5
+ * (TypeScript convention) using the snakeToCamel utility. Types below are the
6
+ * user-facing shape — Raw* types and manual converters have been removed.
7
+ */
4
8
  type Zats = number;
5
- /** Commission sent with a BUY claim memo (0.0001 ZEC = 10,000 zats). */
6
- declare const BUY_COMMISSION: Zats;
7
- /** Listing commission sent with a LIST memo (0.01 ZEC = 1,000,000 zats).
8
- * Mirrors the indexer's formula: min_tier × 1000. */
9
- declare const LIST_COMMISSION: Zats;
10
- interface Registration {
11
- name: string;
12
- address: string;
9
+ /** Target network for ZNS operations. */
10
+ type Network = "testnet" | "mainnet";
11
+ /** All user-signable ZNS actions. Single source of truth ZnsAction is derived from this. */
12
+ declare const ZNS_ACTIONS: readonly ["CLAIM", "BUY", "UPDATE", "LIST", "DELIST", "RELEASE"];
13
+ /** Union of all user-signable action names. Derived from ZNS_ACTIONS. */
14
+ type ZnsAction = (typeof ZNS_ACTIONS)[number];
15
+ /** Ownership-changing actions — can appear as Registration.lastAction. */
16
+ type LastAction = Exclude<ZnsAction, "LIST">;
17
+ /** All actions that appear in the event log — user actions plus admin SETPRICE. */
18
+ type EventAction = ZnsAction | "SETPRICE";
19
+ /** Pending purchase for a listed name. */
20
+ interface PendingBuy {
21
+ buyer: string;
22
+ price: Zats;
23
+ claimHeight: number;
24
+ expiresAt: number;
13
25
  txid: string;
14
- height: number;
15
- nonce: number;
16
- signature: string | null;
17
- last_action: LastAction;
18
- pubkey: string | null;
19
- listing: Listing | null;
20
26
  }
21
- /** Actions that can be the 'last action' on a Registration (ownership-changing actions) */
22
- type LastAction = "CLAIM" | "BUY" | "UPDATE" | "DELIST" | "RELEASE";
23
- /** All actions that can appear in the Event log (includes non-ownership actions like LIST) */
24
- type EventAction = "CLAIM" | "LIST" | "DELIST" | "RELEASE" | "UPDATE" | "BUY" | "SETPRICE";
27
+ /** A name listing in the marketplace. */
25
28
  interface Listing {
26
29
  name: string;
27
30
  price: Zats;
28
- pay_taddr: string;
31
+ payTaddr: string;
29
32
  nonce: number;
30
33
  txid: string;
31
34
  height: number;
32
35
  signature: string;
33
36
  pubkey: string | null;
34
- pending_buy: PendingBuy | null;
37
+ pendingBuy: PendingBuy | undefined;
35
38
  }
36
- interface PendingBuy {
37
- buyer_ua: string;
38
- price: Zats;
39
- claim_height: number;
40
- expires_at: number;
39
+ /** A registered ZNS name. */
40
+ interface Registration {
41
+ name: string;
42
+ address: string;
41
43
  txid: string;
44
+ height: number;
45
+ nonce: number;
46
+ signature: string | null;
47
+ lastAction: LastAction;
48
+ pubkey: string | null;
49
+ listing: Listing | null;
42
50
  }
51
+ /** Pricing tiers for name registration. */
43
52
  interface Pricing {
44
53
  nonce: number;
45
54
  height: number;
46
55
  tiers: Zats[];
47
56
  }
57
+ /** Current indexer status including pricing configuration. */
48
58
  interface Status {
49
- synced_height: number;
50
- admin_pubkey: string;
59
+ syncedHeight: number;
60
+ adminPubkey: string;
51
61
  uivk: string;
52
62
  address: string;
53
63
  registered: number;
54
64
  listed: number;
55
65
  pricing: Pricing | null;
56
66
  }
67
+ /** An event in the ZNS event log. */
57
68
  interface Event {
58
69
  id: number;
59
70
  name: string;
@@ -66,13 +77,15 @@ interface Event {
66
77
  signature: string | null;
67
78
  pubkey: string | null;
68
79
  }
80
+ /** Filter options for querying events. */
69
81
  interface EventsFilter {
70
82
  name?: string;
71
83
  action?: EventAction;
72
- since_height?: number;
84
+ sinceHeight?: number;
73
85
  limit?: number;
74
86
  offset?: number;
75
87
  }
88
+ /** Paginated events result. */
76
89
  interface EventsResult {
77
90
  events: Event[];
78
91
  total: number;
@@ -103,7 +116,7 @@ interface PreparedClaim extends PreparedAction {
103
116
  interface PreparedList extends PreparedAction {
104
117
  readonly name: string;
105
118
  readonly price: Zats;
106
- readonly pay_taddr: string;
119
+ readonly payTaddr: string;
107
120
  readonly nonce: number;
108
121
  }
109
122
  /** Prepared DELIST action */
@@ -142,8 +155,6 @@ interface PayloadValidationResult {
142
155
  readonly valid: boolean;
143
156
  /** Parsed action name (uppercase), e.g. "CLAIM", "LIST" */
144
157
  readonly action: string;
145
- /** Canonical action used internally (lowercase), e.g. "claim", "list" */
146
- readonly canonicalAction: string | null;
147
158
  /** Human-readable validation message */
148
159
  readonly message: string;
149
160
  /** Validation level: valid | invalid | unrecognized */
@@ -151,12 +162,28 @@ interface PayloadValidationResult {
151
162
  }
152
163
 
153
164
  declare const DEFAULT_URL = "https://light.zcash.me/zns-testnet";
154
- declare const TESTNET_UIVK = "uivktest1hzw7wyadutvzfgpna80yftsk5l7jeyu2p5me5quvp28tytxueta00cx4068wnlzcv7tx9n3t3gfhsy83pe4y6jrhxtzaq0hj6xtg5zrk2dn7zen3vns2a5pgs4fxdjlletmqrhfa42";
155
- declare const MAINNET_UIVK = "uivk1gl26qy0xjja7lqhyg3pf0x4j4j66kqwewrjkdcg28eqq4wgtzjmujpee7x9cs2ec9xhnlgrm8ptlw8z80j2aryw8nqtssser2ys778a0s00uvgkdjnfr58sndhfvc3f4zqjs6ywva6";
156
- /** Actions accepted by {@link validatePayload}. Exposed for consumers who need
157
- * to build action selectors or dynamic validation. */
158
- declare const ZNS_ACTIONS: readonly ["CLAIM", "BUY", "UPDATE", "LIST", "DELIST", "RELEASE"];
159
- type Network = "testnet" | "mainnet";
165
+ /** Commission sent with a BUY claim memo (0.0001 ZEC = 10,000 zats). */
166
+ declare const BUY_COMMISSION: Zats;
167
+ /** Listing commission sent with a LIST memo (0.01 ZEC = 1,000,000 zats).
168
+ * Mirrors the indexer's formula: min_tier × 1000. */
169
+ declare const LIST_COMMISSION: Zats;
170
+ /** Network-specific configuration for ZNS. */
171
+ declare const NETWORKS: {
172
+ readonly testnet: {
173
+ readonly url: "https://light.zcash.me/zns-testnet";
174
+ readonly registryAddress: "utest1f32kn6c4zvn54xr8wfsnxmj9hzpu2mwgtxzpzwcw34906tdccdvzs0z2dx38lly7tpan77x6udt8pjczqm22ymsdhlz9j0tk5yq664nl";
175
+ readonly uivk: "utest1hzw7wyadutvzfgpna80yftsk5l7jeyu2p5me5quvp28tytxueta00cx4068wnlzcv7tx9n3t3gfhsy83pe4y6jrhxtzaq0hj6xtg5zrk2dn7zen3vns2a5pgs4fxdjlletmqrhfa42";
176
+ };
177
+ readonly mainnet: {
178
+ readonly url: "https://light.zcash.me/zns";
179
+ readonly registryAddress: "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9";
180
+ readonly uivk: "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9";
181
+ };
182
+ };
183
+ /** Validates a Zcash unified (u-) address. */
184
+ declare function isValidUnifiedAddress(address: string): boolean;
185
+ /** Validates a Zcash transparent (t-) address. */
186
+ declare function isValidTransparentAddress(address: string): boolean;
160
187
  declare class ZNS {
161
188
  private url;
162
189
  private network;
@@ -197,8 +224,15 @@ declare class ZNS {
197
224
  /** Validate a Zcash Unified Address format.
198
225
  * Accepts both mainnet ('u') and testnet ('utest') prefixes.
199
226
  * Performs basic format validation but NOT full bech32m checksum verification.
200
- * Returns true if the address looks like a unified address, false otherwise. */
201
- isValidUnifiedAddress(address: string): boolean;
227
+ * Returns true if the address looks like a unified address, false otherwise.
228
+ *
229
+ * @todo(F4Jumble) Upgrade to full ZIP-316 decoding with F4Jumble to:
230
+ * - Parse actual typecodes from address items
231
+ * - Validate F4Jumble checksum (not just bech32m)
232
+ * - Optionally enforce: address must contain at least one Orchard receiver (typecode 0x03)
233
+ * Requires @noble/hashes (blake2b) implementation of F4Jumble inverse. */
234
+ isValidUnifiedAddress: typeof isValidUnifiedAddress;
235
+ isValidTransparentAddress: typeof isValidTransparentAddress;
202
236
  listings(limit?: number, offset?: number): Promise<{
203
237
  listings: Listing[];
204
238
  total: number;
@@ -218,8 +252,6 @@ declare class ZNS {
218
252
  * @returns true if the signature is valid
219
253
  */
220
254
  verifyRegistration(reg: Registration, adminPubkey: string): Promise<boolean>;
221
- /** Check if a name is valid format (lowercase alphanumeric, 1-62 chars). */
222
- isValidName(name: string): boolean;
223
255
  /**
224
256
  * Validate a signing payload string against the ZNS memo format spec.
225
257
  *
@@ -269,7 +301,7 @@ declare class ZNS {
269
301
  * @returns Prepared claim ready for signature completion
270
302
  */
271
303
  prepareClaim(name: string, address: string, cost: Zats): PreparedClaim;
272
- prepareList(name: string, price: Zats, pay_taddr: string, nonce: number): PreparedList;
304
+ prepareList(name: string, price: Zats, payTaddr: string, nonce: number): PreparedList;
273
305
  prepareDelist(name: string, nonce: number): PreparedDelist;
274
306
  prepareUpdate(name: string, newAddress: string, nonce: number): PreparedUpdate;
275
307
  prepareBuy(name: string, buyerAddress: string, price: Zats): PreparedBuy;
@@ -286,4 +318,4 @@ declare class ZNS {
286
318
  private rpc;
287
319
  }
288
320
 
289
- export { BUY_COMMISSION, type CompletedAction, DEFAULT_URL, type Event, type EventAction, type EventsFilter, type EventsResult, LIST_COMMISSION, type LastAction, type Listing, MAINNET_UIVK, type Network, type PayloadValidationLevel, type PayloadValidationResult, type PendingBuy, type PreparedBuy, type PreparedClaim, type PreparedDelist, type PreparedList, type PreparedRelease, type PreparedSetPrice, type PreparedUpdate, type Pricing, type Registration, type Status, TESTNET_UIVK, ZNS, ZNS_ACTIONS, type Zats };
321
+ export { BUY_COMMISSION, DEFAULT_URL, LIST_COMMISSION, NETWORKS, ZNS };
package/dist/zns.d.ts CHANGED
@@ -1,59 +1,70 @@
1
- /** Amount in zatoshis (1 ZEC = 100,000,000 zats).
2
- * All monetary values in ZNS are denominated in zats.
3
- * Note: JavaScript number precision degrades above 2^53 (~9e15 zats, or ~90M ZEC) */
1
+ /**
2
+ * ZNS TypeScript SDK types.
3
+ *
4
+ * API responses are converted from snake_case (Rust convention) to camelCase
5
+ * (TypeScript convention) using the snakeToCamel utility. Types below are the
6
+ * user-facing shape — Raw* types and manual converters have been removed.
7
+ */
4
8
  type Zats = number;
5
- /** Commission sent with a BUY claim memo (0.0001 ZEC = 10,000 zats). */
6
- declare const BUY_COMMISSION: Zats;
7
- /** Listing commission sent with a LIST memo (0.01 ZEC = 1,000,000 zats).
8
- * Mirrors the indexer's formula: min_tier × 1000. */
9
- declare const LIST_COMMISSION: Zats;
10
- interface Registration {
11
- name: string;
12
- address: string;
9
+ /** Target network for ZNS operations. */
10
+ type Network = "testnet" | "mainnet";
11
+ /** All user-signable ZNS actions. Single source of truth ZnsAction is derived from this. */
12
+ declare const ZNS_ACTIONS: readonly ["CLAIM", "BUY", "UPDATE", "LIST", "DELIST", "RELEASE"];
13
+ /** Union of all user-signable action names. Derived from ZNS_ACTIONS. */
14
+ type ZnsAction = (typeof ZNS_ACTIONS)[number];
15
+ /** Ownership-changing actions — can appear as Registration.lastAction. */
16
+ type LastAction = Exclude<ZnsAction, "LIST">;
17
+ /** All actions that appear in the event log — user actions plus admin SETPRICE. */
18
+ type EventAction = ZnsAction | "SETPRICE";
19
+ /** Pending purchase for a listed name. */
20
+ interface PendingBuy {
21
+ buyer: string;
22
+ price: Zats;
23
+ claimHeight: number;
24
+ expiresAt: number;
13
25
  txid: string;
14
- height: number;
15
- nonce: number;
16
- signature: string | null;
17
- last_action: LastAction;
18
- pubkey: string | null;
19
- listing: Listing | null;
20
26
  }
21
- /** Actions that can be the 'last action' on a Registration (ownership-changing actions) */
22
- type LastAction = "CLAIM" | "BUY" | "UPDATE" | "DELIST" | "RELEASE";
23
- /** All actions that can appear in the Event log (includes non-ownership actions like LIST) */
24
- type EventAction = "CLAIM" | "LIST" | "DELIST" | "RELEASE" | "UPDATE" | "BUY" | "SETPRICE";
27
+ /** A name listing in the marketplace. */
25
28
  interface Listing {
26
29
  name: string;
27
30
  price: Zats;
28
- pay_taddr: string;
31
+ payTaddr: string;
29
32
  nonce: number;
30
33
  txid: string;
31
34
  height: number;
32
35
  signature: string;
33
36
  pubkey: string | null;
34
- pending_buy: PendingBuy | null;
37
+ pendingBuy: PendingBuy | undefined;
35
38
  }
36
- interface PendingBuy {
37
- buyer_ua: string;
38
- price: Zats;
39
- claim_height: number;
40
- expires_at: number;
39
+ /** A registered ZNS name. */
40
+ interface Registration {
41
+ name: string;
42
+ address: string;
41
43
  txid: string;
44
+ height: number;
45
+ nonce: number;
46
+ signature: string | null;
47
+ lastAction: LastAction;
48
+ pubkey: string | null;
49
+ listing: Listing | null;
42
50
  }
51
+ /** Pricing tiers for name registration. */
43
52
  interface Pricing {
44
53
  nonce: number;
45
54
  height: number;
46
55
  tiers: Zats[];
47
56
  }
57
+ /** Current indexer status including pricing configuration. */
48
58
  interface Status {
49
- synced_height: number;
50
- admin_pubkey: string;
59
+ syncedHeight: number;
60
+ adminPubkey: string;
51
61
  uivk: string;
52
62
  address: string;
53
63
  registered: number;
54
64
  listed: number;
55
65
  pricing: Pricing | null;
56
66
  }
67
+ /** An event in the ZNS event log. */
57
68
  interface Event {
58
69
  id: number;
59
70
  name: string;
@@ -66,13 +77,15 @@ interface Event {
66
77
  signature: string | null;
67
78
  pubkey: string | null;
68
79
  }
80
+ /** Filter options for querying events. */
69
81
  interface EventsFilter {
70
82
  name?: string;
71
83
  action?: EventAction;
72
- since_height?: number;
84
+ sinceHeight?: number;
73
85
  limit?: number;
74
86
  offset?: number;
75
87
  }
88
+ /** Paginated events result. */
76
89
  interface EventsResult {
77
90
  events: Event[];
78
91
  total: number;
@@ -103,7 +116,7 @@ interface PreparedClaim extends PreparedAction {
103
116
  interface PreparedList extends PreparedAction {
104
117
  readonly name: string;
105
118
  readonly price: Zats;
106
- readonly pay_taddr: string;
119
+ readonly payTaddr: string;
107
120
  readonly nonce: number;
108
121
  }
109
122
  /** Prepared DELIST action */
@@ -142,8 +155,6 @@ interface PayloadValidationResult {
142
155
  readonly valid: boolean;
143
156
  /** Parsed action name (uppercase), e.g. "CLAIM", "LIST" */
144
157
  readonly action: string;
145
- /** Canonical action used internally (lowercase), e.g. "claim", "list" */
146
- readonly canonicalAction: string | null;
147
158
  /** Human-readable validation message */
148
159
  readonly message: string;
149
160
  /** Validation level: valid | invalid | unrecognized */
@@ -151,12 +162,28 @@ interface PayloadValidationResult {
151
162
  }
152
163
 
153
164
  declare const DEFAULT_URL = "https://light.zcash.me/zns-testnet";
154
- declare const TESTNET_UIVK = "uivktest1hzw7wyadutvzfgpna80yftsk5l7jeyu2p5me5quvp28tytxueta00cx4068wnlzcv7tx9n3t3gfhsy83pe4y6jrhxtzaq0hj6xtg5zrk2dn7zen3vns2a5pgs4fxdjlletmqrhfa42";
155
- declare const MAINNET_UIVK = "uivk1gl26qy0xjja7lqhyg3pf0x4j4j66kqwewrjkdcg28eqq4wgtzjmujpee7x9cs2ec9xhnlgrm8ptlw8z80j2aryw8nqtssser2ys778a0s00uvgkdjnfr58sndhfvc3f4zqjs6ywva6";
156
- /** Actions accepted by {@link validatePayload}. Exposed for consumers who need
157
- * to build action selectors or dynamic validation. */
158
- declare const ZNS_ACTIONS: readonly ["CLAIM", "BUY", "UPDATE", "LIST", "DELIST", "RELEASE"];
159
- type Network = "testnet" | "mainnet";
165
+ /** Commission sent with a BUY claim memo (0.0001 ZEC = 10,000 zats). */
166
+ declare const BUY_COMMISSION: Zats;
167
+ /** Listing commission sent with a LIST memo (0.01 ZEC = 1,000,000 zats).
168
+ * Mirrors the indexer's formula: min_tier × 1000. */
169
+ declare const LIST_COMMISSION: Zats;
170
+ /** Network-specific configuration for ZNS. */
171
+ declare const NETWORKS: {
172
+ readonly testnet: {
173
+ readonly url: "https://light.zcash.me/zns-testnet";
174
+ readonly registryAddress: "utest1f32kn6c4zvn54xr8wfsnxmj9hzpu2mwgtxzpzwcw34906tdccdvzs0z2dx38lly7tpan77x6udt8pjczqm22ymsdhlz9j0tk5yq664nl";
175
+ readonly uivk: "utest1hzw7wyadutvzfgpna80yftsk5l7jeyu2p5me5quvp28tytxueta00cx4068wnlzcv7tx9n3t3gfhsy83pe4y6jrhxtzaq0hj6xtg5zrk2dn7zen3vns2a5pgs4fxdjlletmqrhfa42";
176
+ };
177
+ readonly mainnet: {
178
+ readonly url: "https://light.zcash.me/zns";
179
+ readonly registryAddress: "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9";
180
+ readonly uivk: "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9";
181
+ };
182
+ };
183
+ /** Validates a Zcash unified (u-) address. */
184
+ declare function isValidUnifiedAddress(address: string): boolean;
185
+ /** Validates a Zcash transparent (t-) address. */
186
+ declare function isValidTransparentAddress(address: string): boolean;
160
187
  declare class ZNS {
161
188
  private url;
162
189
  private network;
@@ -197,8 +224,15 @@ declare class ZNS {
197
224
  /** Validate a Zcash Unified Address format.
198
225
  * Accepts both mainnet ('u') and testnet ('utest') prefixes.
199
226
  * Performs basic format validation but NOT full bech32m checksum verification.
200
- * Returns true if the address looks like a unified address, false otherwise. */
201
- isValidUnifiedAddress(address: string): boolean;
227
+ * Returns true if the address looks like a unified address, false otherwise.
228
+ *
229
+ * @todo(F4Jumble) Upgrade to full ZIP-316 decoding with F4Jumble to:
230
+ * - Parse actual typecodes from address items
231
+ * - Validate F4Jumble checksum (not just bech32m)
232
+ * - Optionally enforce: address must contain at least one Orchard receiver (typecode 0x03)
233
+ * Requires @noble/hashes (blake2b) implementation of F4Jumble inverse. */
234
+ isValidUnifiedAddress: typeof isValidUnifiedAddress;
235
+ isValidTransparentAddress: typeof isValidTransparentAddress;
202
236
  listings(limit?: number, offset?: number): Promise<{
203
237
  listings: Listing[];
204
238
  total: number;
@@ -218,8 +252,6 @@ declare class ZNS {
218
252
  * @returns true if the signature is valid
219
253
  */
220
254
  verifyRegistration(reg: Registration, adminPubkey: string): Promise<boolean>;
221
- /** Check if a name is valid format (lowercase alphanumeric, 1-62 chars). */
222
- isValidName(name: string): boolean;
223
255
  /**
224
256
  * Validate a signing payload string against the ZNS memo format spec.
225
257
  *
@@ -269,7 +301,7 @@ declare class ZNS {
269
301
  * @returns Prepared claim ready for signature completion
270
302
  */
271
303
  prepareClaim(name: string, address: string, cost: Zats): PreparedClaim;
272
- prepareList(name: string, price: Zats, pay_taddr: string, nonce: number): PreparedList;
304
+ prepareList(name: string, price: Zats, payTaddr: string, nonce: number): PreparedList;
273
305
  prepareDelist(name: string, nonce: number): PreparedDelist;
274
306
  prepareUpdate(name: string, newAddress: string, nonce: number): PreparedUpdate;
275
307
  prepareBuy(name: string, buyerAddress: string, price: Zats): PreparedBuy;
@@ -286,4 +318,4 @@ declare class ZNS {
286
318
  private rpc;
287
319
  }
288
320
 
289
- export { BUY_COMMISSION, type CompletedAction, DEFAULT_URL, type Event, type EventAction, type EventsFilter, type EventsResult, LIST_COMMISSION, type LastAction, type Listing, MAINNET_UIVK, type Network, type PayloadValidationLevel, type PayloadValidationResult, type PendingBuy, type PreparedBuy, type PreparedClaim, type PreparedDelist, type PreparedList, type PreparedRelease, type PreparedSetPrice, type PreparedUpdate, type Pricing, type Registration, type Status, TESTNET_UIVK, ZNS, ZNS_ACTIONS, type Zats };
321
+ export { BUY_COMMISSION, DEFAULT_URL, LIST_COMMISSION, NETWORKS, ZNS };
package/dist/zns.js CHANGED
@@ -3,25 +3,86 @@ import * as ed25519 from "@noble/ed25519";
3
3
  import { bech32m } from "bech32";
4
4
 
5
5
  // src/types.ts
6
- var BUY_COMMISSION = 1e4;
7
- var LIST_COMMISSION = 1e6;
6
+ var ZNS_ACTIONS = ["CLAIM", "BUY", "UPDATE", "LIST", "DELIST", "RELEASE"];
8
7
 
9
8
  // src/zns.ts
10
9
  var DEFAULT_URL = "https://light.zcash.me/zns-testnet";
11
- var TESTNET_UIVK = "uivktest1hzw7wyadutvzfgpna80yftsk5l7jeyu2p5me5quvp28tytxueta00cx4068wnlzcv7tx9n3t3gfhsy83pe4y6jrhxtzaq0hj6xtg5zrk2dn7zen3vns2a5pgs4fxdjlletmqrhfa42";
12
- var MAINNET_UIVK = "uivk1gl26qy0xjja7lqhyg3pf0x4j4j66kqwewrjkdcg28eqq4wgtzjmujpee7x9cs2ec9xhnlgrm8ptlw8z80j2aryw8nqtssser2ys778a0s00uvgkdjnfr58sndhfvc3f4zqjs6ywva6";
13
- var KNOWN_UIVKS = [TESTNET_UIVK, MAINNET_UIVK];
14
- var REGISTRY_ADDRESSES = {
15
- testnet: "utest1f32kn6c4zvn54xr8wfsnxmj9hzpu2mwgtxzpzwcw34906tdccdvzs0z2dx38lly7tpan77x6udt8pjczqm22ymsdhlz9j0tk5yq664nl",
16
- mainnet: "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9"
10
+ var BUY_COMMISSION = 1e4;
11
+ var LIST_COMMISSION = 1e6;
12
+ var NETWORKS = {
13
+ testnet: {
14
+ url: "https://light.zcash.me/zns-testnet",
15
+ registryAddress: "utest1f32kn6c4zvn54xr8wfsnxmj9hzpu2mwgtxzpzwcw34906tdccdvzs0z2dx38lly7tpan77x6udt8pjczqm22ymsdhlz9j0tk5yq664nl",
16
+ uivk: "utest1hzw7wyadutvzfgpna80yftsk5l7jeyu2p5me5quvp28tytxueta00cx4068wnlzcv7tx9n3t3gfhsy83pe4y6jrhxtzaq0hj6xtg5zrk2dn7zen3vns2a5pgs4fxdjlletmqrhfa42"
17
+ },
18
+ mainnet: {
19
+ url: "https://light.zcash.me/zns",
20
+ registryAddress: "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9",
21
+ uivk: "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9"
22
+ }
17
23
  };
18
- var ZNS_ACTIONS = ["CLAIM", "BUY", "UPDATE", "LIST", "DELIST", "RELEASE"];
19
24
  var NAME_RE = /^[a-z0-9]{1,62}$/;
25
+ function isValidName(name) {
26
+ return NAME_RE.test(name);
27
+ }
28
+ function normalizeApiResponse(obj) {
29
+ if (Array.isArray(obj)) return obj.map((item) => normalizeApiResponse(item));
30
+ if (obj && typeof obj === "object") {
31
+ return Object.fromEntries(
32
+ Object.entries(obj).map(([k, v]) => [
33
+ k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()),
34
+ normalizeApiResponse(v)
35
+ ])
36
+ );
37
+ }
38
+ return obj;
39
+ }
20
40
  function isWholeNumber(value) {
21
41
  return /^\d+$/.test(value) && !value.startsWith("0") || value === "0";
22
42
  }
23
- function mk(level, action, canonical, message) {
24
- return { valid: level === "valid", action, canonicalAction: canonical, message, level };
43
+ function isValidUnifiedAddress(address) {
44
+ if (!address) return false;
45
+ if (address.startsWith("utest1")) return true;
46
+ if (address.startsWith("u1")) return true;
47
+ try {
48
+ const decoded = bech32m.decode(address);
49
+ return decoded.prefix === "u" || decoded.prefix === "utest";
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+ function isValidTransparentAddress(address) {
55
+ if (!address) return false;
56
+ const validPrefixes = ["t1", "t3", "tm", "tn"];
57
+ if (!validPrefixes.some((p) => address.startsWith(p))) return false;
58
+ if (address.length < 26 || address.length > 35) return false;
59
+ const base58Regex = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/;
60
+ return base58Regex.test(address);
61
+ }
62
+ var PAYLOAD_RULES = {
63
+ CLAIM: { format: "CLAIM:<name>:<ua>", checks: ["name", "ua"] },
64
+ BUY: { format: "BUY:<name>:<ua>", checks: ["name", "ua"] },
65
+ UPDATE: { format: "UPDATE:<name>:<ua>:<nonce>", checks: ["name", "ua", "nonce"] },
66
+ LIST: { format: "LIST:<name>:<price>:<pay_taddr>:<nonce>", checks: ["name", "price", "pay_taddr", "nonce"] },
67
+ DELIST: { format: "DELIST:<name>:<nonce>", checks: ["name", "nonce"] },
68
+ RELEASE: { format: "RELEASE:<name>:<nonce>", checks: ["name", "nonce"] }
69
+ };
70
+ function validateField(value, type) {
71
+ switch (type) {
72
+ case "name":
73
+ return NAME_RE.test(value) ? null : "Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.";
74
+ case "ua":
75
+ return isValidUnifiedAddress(value) ? null : `Invalid unified address: "${value}".`;
76
+ case "price":
77
+ return isWholeNumber(value) && Number(value) > 0 ? null : "Price must be a positive whole number in zats.";
78
+ case "nonce":
79
+ return isWholeNumber(value) ? null : "Nonce must be a whole number.";
80
+ case "pay_taddr":
81
+ return isValidTransparentAddress(value) ? null : `Invalid transparent address: "${value}".`;
82
+ }
83
+ }
84
+ function buildValidationResult(level, action, message) {
85
+ return { valid: level === "valid", action, message, level };
25
86
  }
26
87
  var ZNS = class {
27
88
  /**
@@ -33,16 +94,28 @@ var ZNS = class {
33
94
  constructor(options) {
34
95
  this.rpcId = 0;
35
96
  this._verified = false;
97
+ /** Validate a Zcash Unified Address format.
98
+ * Accepts both mainnet ('u') and testnet ('utest') prefixes.
99
+ * Performs basic format validation but NOT full bech32m checksum verification.
100
+ * Returns true if the address looks like a unified address, false otherwise.
101
+ *
102
+ * @todo(F4Jumble) Upgrade to full ZIP-316 decoding with F4Jumble to:
103
+ * - Parse actual typecodes from address items
104
+ * - Validate F4Jumble checksum (not just bech32m)
105
+ * - Optionally enforce: address must contain at least one Orchard receiver (typecode 0x03)
106
+ * Requires @noble/hashes (blake2b) implementation of F4Jumble inverse. */
107
+ this.isValidUnifiedAddress = isValidUnifiedAddress;
108
+ this.isValidTransparentAddress = isValidTransparentAddress;
36
109
  this.network = options?.network ?? "testnet";
37
- this.url = options?.url ?? DEFAULT_URL;
110
+ this.url = options?.url ?? NETWORKS[this.network].url;
38
111
  }
39
112
  /**
40
113
  * Verifies that the connected server is a known ZNS instance.
41
114
  * @throws Error if the server's UIVK is not recognized
42
115
  */
43
116
  async verify() {
44
- const status = await this.rpc("status");
45
- if (!KNOWN_UIVKS.includes(status.uivk)) {
117
+ const status = await this.status();
118
+ if (status.uivk !== NETWORKS[this.network].uivk) {
46
119
  throw new Error(
47
120
  `UIVK mismatch: indexer returned "${status.uivk.slice(0, 20)}..." which is not a known ZNS instance`
48
121
  );
@@ -55,72 +128,61 @@ var ZNS = class {
55
128
  }
56
129
  /** Get the registry address for the current network. */
57
130
  get registryAddress() {
58
- const addr = REGISTRY_ADDRESSES[this.network];
59
- if (!addr) {
60
- throw new Error(`Unknown network: ${this.network}`);
61
- }
62
- return addr;
131
+ return NETWORKS[this.network].registryAddress;
63
132
  }
64
133
  /** Fetch current server status including pricing and configuration. */
65
134
  async status() {
66
- return this.rpc("status");
135
+ const raw = await this.rpc("status");
136
+ return normalizeApiResponse(raw);
67
137
  }
68
138
  /** Resolve a ZNS name to its registration. Returns null if not registered. */
69
139
  async resolveName(name) {
70
- return this.rpc("resolve", { query: name });
140
+ const raw = await this.rpc("resolve", { query: name });
141
+ return raw ? normalizeApiResponse(raw) : null;
71
142
  }
72
143
  /** Resolve a Zcash Unified Address to all names pointing to it. Returns empty array if none.
73
144
  * Supports pagination with limit (default 50, max 500) and offset (default 0). */
74
145
  async resolveAddress(address, limit, offset) {
75
- return this.rpc("resolve", {
146
+ const raw = await this.rpc("resolve", {
76
147
  query: address,
77
148
  limit,
78
149
  offset
79
150
  });
151
+ return raw.map((r) => normalizeApiResponse(r));
80
152
  }
81
153
  /** List all registered names. Useful for explorers or browsers.
82
154
  * Supports pagination with limit (default 50, max 500) and offset (default 0). */
83
155
  async listAllRegistrations(limit, offset) {
84
- return this.rpc("resolve", {
156
+ const raw = await this.rpc("resolve", {
85
157
  query: "",
86
158
  limit,
87
159
  offset
88
160
  });
161
+ return raw.map((r) => normalizeApiResponse(r));
89
162
  }
90
163
  /** Check if a name is available for registration.
91
164
  * Returns false immediately for invalid names without hitting the server. */
92
165
  async isAvailable(name) {
93
- if (!this.isValidName(name)) return false;
166
+ if (!isValidName(name)) return false;
94
167
  const result = await this.resolveName(name);
95
168
  return result === null;
96
169
  }
97
- /** Validate a Zcash Unified Address format.
98
- * Accepts both mainnet ('u') and testnet ('utest') prefixes.
99
- * Performs basic format validation but NOT full bech32m checksum verification.
100
- * Returns true if the address looks like a unified address, false otherwise. */
101
- isValidUnifiedAddress(address) {
102
- if (!address) return false;
103
- if (address.startsWith("utest1")) return true;
104
- if (address.startsWith("u1")) return true;
105
- try {
106
- const decoded = bech32m.decode(address);
107
- return decoded.prefix === "u" || decoded.prefix === "utest";
108
- } catch {
109
- return false;
110
- }
111
- }
112
170
  async listings(limit, offset) {
113
- const result = await this.rpc("listings", {
171
+ const raw = await this.rpc("listings", {
114
172
  limit,
115
173
  offset
116
174
  });
117
- return result;
175
+ return {
176
+ listings: raw.listings.map((l) => normalizeApiResponse(l)),
177
+ total: raw.total
178
+ };
118
179
  }
119
180
  async events(filter) {
120
- return this.rpc(
181
+ const raw = await this.rpc(
121
182
  "events",
122
- filter ?? {}
183
+ normalizeApiResponse(filter ?? {})
123
184
  );
185
+ return normalizeApiResponse(raw);
124
186
  }
125
187
  /**
126
188
  * Verify a listing's signature.
@@ -130,7 +192,7 @@ var ZNS = class {
130
192
  */
131
193
  async verifyListing(listing, adminPubkey) {
132
194
  const pubkey = listing.pubkey ?? adminPubkey;
133
- const payload = `LIST:${listing.name}:${listing.price}:${listing.pay_taddr}:${listing.nonce}`;
195
+ const payload = `LIST:${listing.name}:${listing.price}:${listing.payTaddr}:${listing.nonce}`;
134
196
  return this.verifyEd25519(payload, listing.signature, pubkey);
135
197
  }
136
198
  /**
@@ -146,10 +208,6 @@ var ZNS = class {
146
208
  if (!payload) return false;
147
209
  return this.verifyEd25519(payload, reg.signature, pubkey);
148
210
  }
149
- /** Check if a name is valid format (lowercase alphanumeric, 1-62 chars). */
150
- isValidName(name) {
151
- return NAME_RE.test(name);
152
- }
153
211
  /**
154
212
  * Validate a signing payload string against the ZNS memo format spec.
155
213
  *
@@ -176,7 +234,6 @@ var ZNS = class {
176
234
  return {
177
235
  valid: false,
178
236
  action: "",
179
- canonicalAction: null,
180
237
  message: "Empty payload.",
181
238
  level: "invalid"
182
239
  };
@@ -186,83 +243,29 @@ var ZNS = class {
186
243
  return {
187
244
  valid: false,
188
245
  action: raw.toUpperCase(),
189
- canonicalAction: null,
190
246
  message: "Missing colon separator. Expected format: ACTION:field1:field2:...",
191
247
  level: "invalid"
192
248
  };
193
249
  }
194
- const actionUpper = raw.slice(0, colonIdx).toUpperCase();
195
- const actionLower = raw.slice(0, colonIdx).toLowerCase();
250
+ const action = raw.slice(0, colonIdx).toUpperCase();
196
251
  const rest = raw.slice(colonIdx + 1);
197
252
  const parts = rest.split(":");
198
- switch (actionLower) {
199
- case "claim": {
200
- if (parts.length !== 2)
201
- return mk("invalid", actionUpper, "claim", `Expected CLAIM:<name>:<ua>.`);
202
- if (!NAME_RE.test(parts[0]))
203
- return mk("invalid", actionUpper, "claim", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
204
- if (!this.isValidUnifiedAddress(parts[1]))
205
- return mk("invalid", actionUpper, "claim", `Invalid unified address: "${parts[1]}".`);
206
- return mk("valid", actionUpper, "claim", "Valid CLAIM payload.");
207
- }
208
- case "buy": {
209
- if (parts.length !== 2)
210
- return mk("invalid", actionUpper, "buy", `Expected BUY:<name>:<buyer_ua>.`);
211
- if (!NAME_RE.test(parts[0]))
212
- return mk("invalid", actionUpper, "buy", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
213
- if (!this.isValidUnifiedAddress(parts[1]))
214
- return mk("invalid", actionUpper, "buy", `Invalid unified address: "${parts[1]}".`);
215
- return mk("valid", actionUpper, "buy", "Valid BUY payload.");
216
- }
217
- case "update": {
218
- if (parts.length !== 3)
219
- return mk("invalid", actionUpper, "update", `Expected UPDATE:<name>:<ua>:<nonce>.`);
220
- if (!NAME_RE.test(parts[0]))
221
- return mk("invalid", actionUpper, "update", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
222
- if (!this.isValidUnifiedAddress(parts[1]))
223
- return mk("invalid", actionUpper, "update", `Invalid unified address: "${parts[1]}".`);
224
- if (!isWholeNumber(parts[2]))
225
- return mk("invalid", actionUpper, "update", `Nonce must be a whole number.`);
226
- return mk("valid", actionUpper, "update", "Valid UPDATE payload.");
227
- }
228
- case "list": {
229
- if (parts.length !== 4)
230
- return mk("invalid", actionUpper, "list", `Expected LIST:<name>:<price_zats>:<pay_taddr>:<nonce>.`);
231
- if (!NAME_RE.test(parts[0]))
232
- return mk("invalid", actionUpper, "list", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
233
- if (!isWholeNumber(parts[1]) || Number(parts[1]) <= 0)
234
- return mk("invalid", actionUpper, "list", `Price must be a positive whole number in zats.`);
235
- if (!isWholeNumber(parts[3]))
236
- return mk("invalid", actionUpper, "list", `Nonce must be a whole number.`);
237
- return mk("valid", actionUpper, "list", "Valid LIST payload.");
238
- }
239
- case "delist": {
240
- if (parts.length !== 2)
241
- return mk("invalid", actionUpper, "delist", `Expected DELIST:<name>:<nonce>.`);
242
- if (!NAME_RE.test(parts[0]))
243
- return mk("invalid", actionUpper, "delist", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
244
- if (!isWholeNumber(parts[1]))
245
- return mk("invalid", actionUpper, "delist", `Nonce must be a whole number.`);
246
- return mk("valid", actionUpper, "delist", "Valid DELIST payload.");
247
- }
248
- case "release": {
249
- if (parts.length !== 2)
250
- return mk("invalid", actionUpper, "release", `Expected RELEASE:<name>:<nonce>.`);
251
- if (!NAME_RE.test(parts[0]))
252
- return mk("invalid", actionUpper, "release", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
253
- if (!isWholeNumber(parts[1]))
254
- return mk("invalid", actionUpper, "release", `Nonce must be a whole number.`);
255
- return mk("valid", actionUpper, "release", "Valid RELEASE payload.");
256
- }
257
- default:
258
- return {
259
- valid: false,
260
- action: actionUpper,
261
- canonicalAction: null,
262
- message: `Unrecognized action "${actionUpper}". Valid actions: ${ZNS_ACTIONS.join(", ")}.`,
263
- level: "unrecognized"
264
- };
253
+ if (!ZNS_ACTIONS.includes(action)) {
254
+ return {
255
+ valid: false,
256
+ action,
257
+ message: `Unrecognized action "${action}". Valid actions: ${ZNS_ACTIONS.join(", ")}.`,
258
+ level: "unrecognized"
259
+ };
260
+ }
261
+ const rule = PAYLOAD_RULES[action];
262
+ if (parts.length !== rule.checks.length)
263
+ return buildValidationResult("invalid", action, `Expected ${rule.format}.`);
264
+ for (let i = 0; i < rule.checks.length; i++) {
265
+ const err = validateField(parts[i], rule.checks[i]);
266
+ if (err) return buildValidationResult("invalid", action, err);
265
267
  }
268
+ return buildValidationResult("valid", action, `Valid ${action} payload.`);
266
269
  }
267
270
  /**
268
271
  * Get the claim cost in zatoshis for a name of given length.
@@ -305,7 +308,7 @@ var ZNS = class {
305
308
  */
306
309
  prepareClaim(name, address, cost) {
307
310
  this.requireValidName(name);
308
- if (!this.isValidUnifiedAddress(address)) {
311
+ if (!isValidUnifiedAddress(address)) {
309
312
  throw new Error(`Invalid Zcash Unified Address: ${address}`);
310
313
  }
311
314
  return {
@@ -320,16 +323,16 @@ var ZNS = class {
320
323
  }
321
324
  };
322
325
  }
323
- prepareList(name, price, pay_taddr, nonce) {
326
+ prepareList(name, price, payTaddr, nonce) {
324
327
  this.requireValidName(name);
325
328
  return {
326
329
  name,
327
330
  price,
328
- pay_taddr,
331
+ payTaddr,
329
332
  nonce,
330
- payload: `LIST:${name}:${price}:${pay_taddr}:${nonce}`,
333
+ payload: `LIST:${name}:${price}:${payTaddr}:${nonce}`,
331
334
  complete: (signature, userPubkey) => {
332
- const memo = userPubkey ? `ZNS:LIST:${name}:${price}:${pay_taddr}:${nonce}:${signature}:${userPubkey}` : `ZNS:LIST:${name}:${price}:${pay_taddr}:${nonce}:${signature}`;
335
+ const memo = userPubkey ? `ZNS:LIST:${name}:${price}:${payTaddr}:${nonce}:${signature}:${userPubkey}` : `ZNS:LIST:${name}:${price}:${payTaddr}:${nonce}:${signature}`;
333
336
  return { memo, uri: this.buildZcashUri(this.registryAddress, LIST_COMMISSION, memo) };
334
337
  }
335
338
  };
@@ -348,7 +351,7 @@ var ZNS = class {
348
351
  }
349
352
  prepareUpdate(name, newAddress, nonce) {
350
353
  this.requireValidName(name);
351
- if (!this.isValidUnifiedAddress(newAddress)) {
354
+ if (!isValidUnifiedAddress(newAddress)) {
352
355
  throw new Error(`Invalid Zcash Unified Address: ${newAddress}`);
353
356
  }
354
357
  return {
@@ -403,7 +406,7 @@ var ZNS = class {
403
406
  }
404
407
  // ── Private helpers ────────────────────────────────────────────────────────
405
408
  registrationPayload(reg) {
406
- switch (reg.last_action) {
409
+ switch (reg.lastAction) {
407
410
  case "CLAIM":
408
411
  return `CLAIM:${reg.name}:${reg.address}`;
409
412
  case "BUY":
@@ -430,7 +433,7 @@ var ZNS = class {
430
433
  }
431
434
  }
432
435
  requireValidName(name) {
433
- if (!NAME_RE.test(name)) throw new Error(`Invalid ZNS name: ${name}`);
436
+ if (!isValidName(name)) throw new Error(`Invalid ZNS name: ${name}`);
434
437
  }
435
438
  /** Build a ZIP-321 URI. Amount is in zatoshis and will be converted to ZEC for the URI. */
436
439
  buildZcashUri(address, amountZats, memo) {
@@ -495,8 +498,6 @@ export {
495
498
  BUY_COMMISSION,
496
499
  DEFAULT_URL,
497
500
  LIST_COMMISSION,
498
- MAINNET_UIVK,
499
- TESTNET_UIVK,
500
- ZNS,
501
- ZNS_ACTIONS
501
+ NETWORKS,
502
+ ZNS
502
503
  };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "zcashname-sdk",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "type": "module",
5
- "description": "TypeScript SDK for the Zcash Name System (ZNS)",
5
+ "description": "TypeScript SDK for the Zcash Name System (ZNS).",
6
6
  "main": "dist/zns.cjs",
7
7
  "module": "dist/zns.js",
8
8
  "types": "dist/zns.d.ts",