zcashname-sdk 0.8.4 → 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,117 +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
44
+ var ZNS_ACTIONS = ["CLAIM", "BUY", "UPDATE", "LIST", "DELIST", "RELEASE"];
45
+
46
+ // src/zns.ts
47
+ var DEFAULT_URL = "https://light.zcash.me/zns-testnet";
46
48
  var BUY_COMMISSION = 1e4;
47
49
  var LIST_COMMISSION = 1e6;
48
- function toPendingBuy(raw) {
49
- return {
50
- buyer: raw.buyer_ua,
51
- price: raw.price,
52
- claimHeight: raw.claim_height,
53
- expiresAt: raw.expires_at,
54
- txid: raw.txid
55
- };
56
- }
57
- function toListing(raw) {
58
- return {
59
- name: raw.name,
60
- price: raw.price,
61
- payTaddr: raw.pay_taddr,
62
- nonce: raw.nonce,
63
- txid: raw.txid,
64
- height: raw.height,
65
- signature: raw.signature,
66
- pubkey: raw.pubkey,
67
- pendingBuy: raw.pending_buy ? toPendingBuy(raw.pending_buy) : void 0
68
- };
69
- }
70
- function toRegistration(raw) {
71
- return {
72
- name: raw.name,
73
- address: raw.address,
74
- txid: raw.txid,
75
- height: raw.height,
76
- nonce: raw.nonce,
77
- signature: raw.signature,
78
- lastAction: raw.last_action,
79
- pubkey: raw.pubkey,
80
- listing: raw.listing ? toListing(raw.listing) : null
81
- };
82
- }
83
- function toPricing(raw) {
84
- return {
85
- nonce: raw.nonce,
86
- height: raw.height,
87
- tiers: raw.tiers
88
- };
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
+ }
61
+ };
62
+ var NAME_RE = /^[a-z0-9]{1,62}$/;
63
+ function isValidName(name) {
64
+ return NAME_RE.test(name);
89
65
  }
90
- function toStatus(raw) {
91
- return {
92
- syncedHeight: raw.synced_height,
93
- adminPubkey: raw.admin_pubkey,
94
- uivk: raw.uivk,
95
- address: raw.address,
96
- registered: raw.registered,
97
- listed: raw.listed,
98
- pricing: raw.pricing ? toPricing(raw.pricing) : null
99
- };
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;
100
77
  }
101
- function toEvent(raw) {
102
- return {
103
- id: raw.id,
104
- name: raw.name,
105
- action: raw.action,
106
- txid: raw.txid,
107
- height: raw.height,
108
- ua: raw.ua,
109
- price: raw.price,
110
- nonce: raw.nonce,
111
- signature: raw.signature,
112
- pubkey: raw.pubkey
113
- };
78
+ function isWholeNumber(value) {
79
+ return /^\d+$/.test(value) && !value.startsWith("0") || value === "0";
114
80
  }
115
- function toEventsFilter(raw) {
116
- return {
117
- name: raw.name,
118
- action: raw.action,
119
- since_height: raw.sinceHeight,
120
- limit: raw.limit,
121
- offset: raw.offset
122
- };
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
+ }
123
91
  }
124
- function toEventsResult(raw) {
125
- return {
126
- events: raw.events.map(toEvent),
127
- total: raw.total
128
- };
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);
129
99
  }
130
-
131
- // src/zns.ts
132
- var DEFAULT_URL = "https://light.zcash.me/zns-testnet";
133
- var TESTNET_UIVK = "utest1hzw7wyadutvzfgpna80yftsk5l7jeyu2p5me5quvp28tytxueta00cx4068wnlzcv7tx9n3t3gfhsy83pe4y6jrhxtzaq0hj6xtg5zrk2dn7zen3vns2a5pgs4fxdjlletmqrhfa42";
134
- var MAINNET_UIVK = "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9";
135
- var KNOWN_UIVKS = [TESTNET_UIVK, MAINNET_UIVK];
136
- var REGISTRY_ADDRESSES = {
137
- testnet: "utest1f32kn6c4zvn54xr8wfsnxmj9hzpu2mwgtxzpzwcw34906tdccdvzs0z2dx38lly7tpan77x6udt8pjczqm22ymsdhlz9j0tk5yq664nl",
138
- mainnet: "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9"
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"] }
139
107
  };
140
- var ZNS_ACTIONS = ["CLAIM", "BUY", "UPDATE", "LIST", "DELIST", "RELEASE"];
141
- var NAME_RE = /^[a-z0-9]{1,62}$/;
142
- function isWholeNumber(value) {
143
- return /^\d+$/.test(value) && !value.startsWith("0") || value === "0";
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
+ }
144
121
  }
145
- function mk(level, action, canonical, message) {
146
- return { valid: level === "valid", action, canonicalAction: canonical, message, level };
122
+ function buildValidationResult(level, action, message) {
123
+ return { valid: level === "valid", action, message, level };
147
124
  }
148
125
  var ZNS = class {
149
126
  /**
@@ -155,8 +132,20 @@ var ZNS = class {
155
132
  constructor(options) {
156
133
  this.rpcId = 0;
157
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;
158
147
  this.network = options?.network ?? "testnet";
159
- this.url = options?.url ?? DEFAULT_URL;
148
+ this.url = options?.url ?? NETWORKS[this.network].url;
160
149
  }
161
150
  /**
162
151
  * Verifies that the connected server is a known ZNS instance.
@@ -164,7 +153,7 @@ var ZNS = class {
164
153
  */
165
154
  async verify() {
166
155
  const status = await this.status();
167
- if (!KNOWN_UIVKS.includes(status.uivk)) {
156
+ if (status.uivk !== NETWORKS[this.network].uivk) {
168
157
  throw new Error(
169
158
  `UIVK mismatch: indexer returned "${status.uivk.slice(0, 20)}..." which is not a known ZNS instance`
170
159
  );
@@ -177,21 +166,17 @@ var ZNS = class {
177
166
  }
178
167
  /** Get the registry address for the current network. */
179
168
  get registryAddress() {
180
- const addr = REGISTRY_ADDRESSES[this.network];
181
- if (!addr) {
182
- throw new Error(`Unknown network: ${this.network}`);
183
- }
184
- return addr;
169
+ return NETWORKS[this.network].registryAddress;
185
170
  }
186
171
  /** Fetch current server status including pricing and configuration. */
187
172
  async status() {
188
173
  const raw = await this.rpc("status");
189
- return toStatus(raw);
174
+ return normalizeApiResponse(raw);
190
175
  }
191
176
  /** Resolve a ZNS name to its registration. Returns null if not registered. */
192
177
  async resolveName(name) {
193
178
  const raw = await this.rpc("resolve", { query: name });
194
- return raw ? toRegistration(raw) : null;
179
+ return raw ? normalizeApiResponse(raw) : null;
195
180
  }
196
181
  /** Resolve a Zcash Unified Address to all names pointing to it. Returns empty array if none.
197
182
  * Supports pagination with limit (default 50, max 500) and offset (default 0). */
@@ -201,7 +186,7 @@ var ZNS = class {
201
186
  limit,
202
187
  offset
203
188
  });
204
- return raw.map(toRegistration);
189
+ return raw.map((r) => normalizeApiResponse(r));
205
190
  }
206
191
  /** List all registered names. Useful for explorers or browsers.
207
192
  * Supports pagination with limit (default 50, max 500) and offset (default 0). */
@@ -211,62 +196,31 @@ var ZNS = class {
211
196
  limit,
212
197
  offset
213
198
  });
214
- return raw.map(toRegistration);
199
+ return raw.map((r) => normalizeApiResponse(r));
215
200
  }
216
201
  /** Check if a name is available for registration.
217
202
  * Returns false immediately for invalid names without hitting the server. */
218
203
  async isAvailable(name) {
219
- if (!this.isValidName(name)) return false;
204
+ if (!isValidName(name)) return false;
220
205
  const result = await this.resolveName(name);
221
206
  return result === null;
222
207
  }
223
- /** Validate a Zcash Unified Address format.
224
- * Accepts both mainnet ('u') and testnet ('utest') prefixes.
225
- * Performs basic format validation but NOT full bech32m checksum verification.
226
- * Returns true if the address looks like a unified address, false otherwise. */
227
- isValidUnifiedAddress(address) {
228
- if (!address) return false;
229
- if (address.startsWith("utest1")) return true;
230
- if (address.startsWith("u1")) return true;
231
- try {
232
- const decoded = import_bech32.bech32m.decode(address);
233
- return decoded.prefix === "u" || decoded.prefix === "utest";
234
- } catch {
235
- return false;
236
- }
237
- }
238
208
  async listings(limit, offset) {
239
- const result = await this.rpc("listings", {
209
+ const raw = await this.rpc("listings", {
240
210
  limit,
241
211
  offset
242
212
  });
243
213
  return {
244
- listings: result.listings.map((l) => ({
245
- name: l.name,
246
- price: l.price,
247
- payTaddr: l.pay_taddr,
248
- nonce: l.nonce,
249
- txid: l.txid,
250
- height: l.height,
251
- signature: l.signature,
252
- pubkey: l.pubkey,
253
- pendingBuy: l.pending_buy ? {
254
- buyer: l.pending_buy.buyer_ua,
255
- price: l.pending_buy.price,
256
- claimHeight: l.pending_buy.claim_height,
257
- expiresAt: l.pending_buy.expires_at,
258
- txid: l.pending_buy.txid
259
- } : void 0
260
- })),
261
- total: result.total
214
+ listings: raw.listings.map((l) => normalizeApiResponse(l)),
215
+ total: raw.total
262
216
  };
263
217
  }
264
218
  async events(filter) {
265
219
  const raw = await this.rpc(
266
220
  "events",
267
- toEventsFilter(filter ?? {})
221
+ normalizeApiResponse(filter ?? {})
268
222
  );
269
- return toEventsResult(raw);
223
+ return normalizeApiResponse(raw);
270
224
  }
271
225
  /**
272
226
  * Verify a listing's signature.
@@ -292,10 +246,6 @@ var ZNS = class {
292
246
  if (!payload) return false;
293
247
  return this.verifyEd25519(payload, reg.signature, pubkey);
294
248
  }
295
- /** Check if a name is valid format (lowercase alphanumeric, 1-62 chars). */
296
- isValidName(name) {
297
- return NAME_RE.test(name);
298
- }
299
249
  /**
300
250
  * Validate a signing payload string against the ZNS memo format spec.
301
251
  *
@@ -322,7 +272,6 @@ var ZNS = class {
322
272
  return {
323
273
  valid: false,
324
274
  action: "",
325
- canonicalAction: null,
326
275
  message: "Empty payload.",
327
276
  level: "invalid"
328
277
  };
@@ -332,83 +281,29 @@ var ZNS = class {
332
281
  return {
333
282
  valid: false,
334
283
  action: raw.toUpperCase(),
335
- canonicalAction: null,
336
284
  message: "Missing colon separator. Expected format: ACTION:field1:field2:...",
337
285
  level: "invalid"
338
286
  };
339
287
  }
340
- const actionUpper = raw.slice(0, colonIdx).toUpperCase();
341
- const actionLower = raw.slice(0, colonIdx).toLowerCase();
288
+ const action = raw.slice(0, colonIdx).toUpperCase();
342
289
  const rest = raw.slice(colonIdx + 1);
343
290
  const parts = rest.split(":");
344
- switch (actionLower) {
345
- case "claim": {
346
- if (parts.length !== 2)
347
- return mk("invalid", actionUpper, "claim", `Expected CLAIM:<name>:<ua>.`);
348
- if (!NAME_RE.test(parts[0]))
349
- return mk("invalid", actionUpper, "claim", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
350
- if (!this.isValidUnifiedAddress(parts[1]))
351
- return mk("invalid", actionUpper, "claim", `Invalid unified address: "${parts[1]}".`);
352
- return mk("valid", actionUpper, "claim", "Valid CLAIM payload.");
353
- }
354
- case "buy": {
355
- if (parts.length !== 2)
356
- return mk("invalid", actionUpper, "buy", `Expected BUY:<name>:<buyer_ua>.`);
357
- if (!NAME_RE.test(parts[0]))
358
- return mk("invalid", actionUpper, "buy", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
359
- if (!this.isValidUnifiedAddress(parts[1]))
360
- return mk("invalid", actionUpper, "buy", `Invalid unified address: "${parts[1]}".`);
361
- return mk("valid", actionUpper, "buy", "Valid BUY payload.");
362
- }
363
- case "update": {
364
- if (parts.length !== 3)
365
- return mk("invalid", actionUpper, "update", `Expected UPDATE:<name>:<ua>:<nonce>.`);
366
- if (!NAME_RE.test(parts[0]))
367
- return mk("invalid", actionUpper, "update", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
368
- if (!this.isValidUnifiedAddress(parts[1]))
369
- return mk("invalid", actionUpper, "update", `Invalid unified address: "${parts[1]}".`);
370
- if (!isWholeNumber(parts[2]))
371
- return mk("invalid", actionUpper, "update", `Nonce must be a whole number.`);
372
- return mk("valid", actionUpper, "update", "Valid UPDATE payload.");
373
- }
374
- case "list": {
375
- if (parts.length !== 4)
376
- return mk("invalid", actionUpper, "list", `Expected LIST:<name>:<price_zats>:<pay_taddr>:<nonce>.`);
377
- if (!NAME_RE.test(parts[0]))
378
- return mk("invalid", actionUpper, "list", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
379
- if (!isWholeNumber(parts[1]) || Number(parts[1]) <= 0)
380
- return mk("invalid", actionUpper, "list", `Price must be a positive whole number in zats.`);
381
- if (!isWholeNumber(parts[3]))
382
- return mk("invalid", actionUpper, "list", `Nonce must be a whole number.`);
383
- return mk("valid", actionUpper, "list", "Valid LIST payload.");
384
- }
385
- case "delist": {
386
- if (parts.length !== 2)
387
- return mk("invalid", actionUpper, "delist", `Expected DELIST:<name>:<nonce>.`);
388
- if (!NAME_RE.test(parts[0]))
389
- return mk("invalid", actionUpper, "delist", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
390
- if (!isWholeNumber(parts[1]))
391
- return mk("invalid", actionUpper, "delist", `Nonce must be a whole number.`);
392
- return mk("valid", actionUpper, "delist", "Valid DELIST payload.");
393
- }
394
- case "release": {
395
- if (parts.length !== 2)
396
- return mk("invalid", actionUpper, "release", `Expected RELEASE:<name>:<nonce>.`);
397
- if (!NAME_RE.test(parts[0]))
398
- return mk("invalid", actionUpper, "release", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
399
- if (!isWholeNumber(parts[1]))
400
- return mk("invalid", actionUpper, "release", `Nonce must be a whole number.`);
401
- return mk("valid", actionUpper, "release", "Valid RELEASE payload.");
402
- }
403
- default:
404
- return {
405
- valid: false,
406
- action: actionUpper,
407
- canonicalAction: null,
408
- message: `Unrecognized action "${actionUpper}". Valid actions: ${ZNS_ACTIONS.join(", ")}.`,
409
- level: "unrecognized"
410
- };
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);
411
305
  }
306
+ return buildValidationResult("valid", action, `Valid ${action} payload.`);
412
307
  }
413
308
  /**
414
309
  * Get the claim cost in zatoshis for a name of given length.
@@ -451,7 +346,7 @@ var ZNS = class {
451
346
  */
452
347
  prepareClaim(name, address, cost) {
453
348
  this.requireValidName(name);
454
- if (!this.isValidUnifiedAddress(address)) {
349
+ if (!isValidUnifiedAddress(address)) {
455
350
  throw new Error(`Invalid Zcash Unified Address: ${address}`);
456
351
  }
457
352
  return {
@@ -494,7 +389,7 @@ var ZNS = class {
494
389
  }
495
390
  prepareUpdate(name, newAddress, nonce) {
496
391
  this.requireValidName(name);
497
- if (!this.isValidUnifiedAddress(newAddress)) {
392
+ if (!isValidUnifiedAddress(newAddress)) {
498
393
  throw new Error(`Invalid Zcash Unified Address: ${newAddress}`);
499
394
  }
500
395
  return {
@@ -576,7 +471,7 @@ var ZNS = class {
576
471
  }
577
472
  }
578
473
  requireValidName(name) {
579
- if (!NAME_RE.test(name)) throw new Error(`Invalid ZNS name: ${name}`);
474
+ if (!isValidName(name)) throw new Error(`Invalid ZNS name: ${name}`);
580
475
  }
581
476
  /** Build a ZIP-321 URI. Amount is in zatoshis and will be converted to ZEC for the URI. */
582
477
  buildZcashUri(address, amountZats, memo) {
@@ -642,8 +537,6 @@ var ZNS = class {
642
537
  BUY_COMMISSION,
643
538
  DEFAULT_URL,
644
539
  LIST_COMMISSION,
645
- MAINNET_UIVK,
646
- TESTNET_UIVK,
647
- ZNS,
648
- ZNS_ACTIONS
540
+ NETWORKS,
541
+ ZNS
649
542
  });
package/dist/zns.d.cts CHANGED
@@ -1,18 +1,21 @@
1
1
  /**
2
- * Internal types that mirror the Rust indexer API exactly (snake_case).
3
- * These are used for raw data passthrough and debugging.
4
- * Public SDK consumers should use the camelCase types exported below.
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.
5
7
  */
6
8
  type Zats = number;
7
- /** Commission sent with a BUY claim memo (0.0001 ZEC = 10,000 zats). */
8
- declare const BUY_COMMISSION: Zats;
9
- /** Listing commission sent with a LIST memo (0.01 ZEC = 1,000,000 zats).
10
- * Mirrors the indexer's formula: min_tier × 1000. */
11
- declare const LIST_COMMISSION: Zats;
12
- /** Actions that can be the 'last action' on a Registration (ownership-changing actions) */
13
- type LastAction = "CLAIM" | "BUY" | "UPDATE" | "DELIST" | "RELEASE";
14
- /** All actions that can appear in the Event log (includes non-ownership actions like LIST) */
15
- type EventAction = "CLAIM" | "LIST" | "DELIST" | "RELEASE" | "UPDATE" | "BUY" | "SETPRICE";
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";
16
19
  /** Pending purchase for a listed name. */
17
20
  interface PendingBuy {
18
21
  buyer: string;
@@ -152,8 +155,6 @@ interface PayloadValidationResult {
152
155
  readonly valid: boolean;
153
156
  /** Parsed action name (uppercase), e.g. "CLAIM", "LIST" */
154
157
  readonly action: string;
155
- /** Canonical action used internally (lowercase), e.g. "claim", "list" */
156
- readonly canonicalAction: string | null;
157
158
  /** Human-readable validation message */
158
159
  readonly message: string;
159
160
  /** Validation level: valid | invalid | unrecognized */
@@ -161,12 +162,28 @@ interface PayloadValidationResult {
161
162
  }
162
163
 
163
164
  declare const DEFAULT_URL = "https://light.zcash.me/zns-testnet";
164
- declare const TESTNET_UIVK = "utest1hzw7wyadutvzfgpna80yftsk5l7jeyu2p5me5quvp28tytxueta00cx4068wnlzcv7tx9n3t3gfhsy83pe4y6jrhxtzaq0hj6xtg5zrk2dn7zen3vns2a5pgs4fxdjlletmqrhfa42";
165
- declare const MAINNET_UIVK = "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9";
166
- /** Actions accepted by {@link validatePayload}. Exposed for consumers who need
167
- * to build action selectors or dynamic validation. */
168
- declare const ZNS_ACTIONS: readonly ["CLAIM", "BUY", "UPDATE", "LIST", "DELIST", "RELEASE"];
169
- 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;
170
187
  declare class ZNS {
171
188
  private url;
172
189
  private network;
@@ -207,8 +224,15 @@ declare class ZNS {
207
224
  /** Validate a Zcash Unified Address format.
208
225
  * Accepts both mainnet ('u') and testnet ('utest') prefixes.
209
226
  * Performs basic format validation but NOT full bech32m checksum verification.
210
- * Returns true if the address looks like a unified address, false otherwise. */
211
- 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;
212
236
  listings(limit?: number, offset?: number): Promise<{
213
237
  listings: Listing[];
214
238
  total: number;
@@ -228,8 +252,6 @@ declare class ZNS {
228
252
  * @returns true if the signature is valid
229
253
  */
230
254
  verifyRegistration(reg: Registration, adminPubkey: string): Promise<boolean>;
231
- /** Check if a name is valid format (lowercase alphanumeric, 1-62 chars). */
232
- isValidName(name: string): boolean;
233
255
  /**
234
256
  * Validate a signing payload string against the ZNS memo format spec.
235
257
  *
@@ -296,4 +318,4 @@ declare class ZNS {
296
318
  private rpc;
297
319
  }
298
320
 
299
- 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,18 +1,21 @@
1
1
  /**
2
- * Internal types that mirror the Rust indexer API exactly (snake_case).
3
- * These are used for raw data passthrough and debugging.
4
- * Public SDK consumers should use the camelCase types exported below.
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.
5
7
  */
6
8
  type Zats = number;
7
- /** Commission sent with a BUY claim memo (0.0001 ZEC = 10,000 zats). */
8
- declare const BUY_COMMISSION: Zats;
9
- /** Listing commission sent with a LIST memo (0.01 ZEC = 1,000,000 zats).
10
- * Mirrors the indexer's formula: min_tier × 1000. */
11
- declare const LIST_COMMISSION: Zats;
12
- /** Actions that can be the 'last action' on a Registration (ownership-changing actions) */
13
- type LastAction = "CLAIM" | "BUY" | "UPDATE" | "DELIST" | "RELEASE";
14
- /** All actions that can appear in the Event log (includes non-ownership actions like LIST) */
15
- type EventAction = "CLAIM" | "LIST" | "DELIST" | "RELEASE" | "UPDATE" | "BUY" | "SETPRICE";
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";
16
19
  /** Pending purchase for a listed name. */
17
20
  interface PendingBuy {
18
21
  buyer: string;
@@ -152,8 +155,6 @@ interface PayloadValidationResult {
152
155
  readonly valid: boolean;
153
156
  /** Parsed action name (uppercase), e.g. "CLAIM", "LIST" */
154
157
  readonly action: string;
155
- /** Canonical action used internally (lowercase), e.g. "claim", "list" */
156
- readonly canonicalAction: string | null;
157
158
  /** Human-readable validation message */
158
159
  readonly message: string;
159
160
  /** Validation level: valid | invalid | unrecognized */
@@ -161,12 +162,28 @@ interface PayloadValidationResult {
161
162
  }
162
163
 
163
164
  declare const DEFAULT_URL = "https://light.zcash.me/zns-testnet";
164
- declare const TESTNET_UIVK = "utest1hzw7wyadutvzfgpna80yftsk5l7jeyu2p5me5quvp28tytxueta00cx4068wnlzcv7tx9n3t3gfhsy83pe4y6jrhxtzaq0hj6xtg5zrk2dn7zen3vns2a5pgs4fxdjlletmqrhfa42";
165
- declare const MAINNET_UIVK = "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9";
166
- /** Actions accepted by {@link validatePayload}. Exposed for consumers who need
167
- * to build action selectors or dynamic validation. */
168
- declare const ZNS_ACTIONS: readonly ["CLAIM", "BUY", "UPDATE", "LIST", "DELIST", "RELEASE"];
169
- 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;
170
187
  declare class ZNS {
171
188
  private url;
172
189
  private network;
@@ -207,8 +224,15 @@ declare class ZNS {
207
224
  /** Validate a Zcash Unified Address format.
208
225
  * Accepts both mainnet ('u') and testnet ('utest') prefixes.
209
226
  * Performs basic format validation but NOT full bech32m checksum verification.
210
- * Returns true if the address looks like a unified address, false otherwise. */
211
- 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;
212
236
  listings(limit?: number, offset?: number): Promise<{
213
237
  listings: Listing[];
214
238
  total: number;
@@ -228,8 +252,6 @@ declare class ZNS {
228
252
  * @returns true if the signature is valid
229
253
  */
230
254
  verifyRegistration(reg: Registration, adminPubkey: string): Promise<boolean>;
231
- /** Check if a name is valid format (lowercase alphanumeric, 1-62 chars). */
232
- isValidName(name: string): boolean;
233
255
  /**
234
256
  * Validate a signing payload string against the ZNS memo format spec.
235
257
  *
@@ -296,4 +318,4 @@ declare class ZNS {
296
318
  private rpc;
297
319
  }
298
320
 
299
- 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,107 +3,86 @@ import * as ed25519 from "@noble/ed25519";
3
3
  import { bech32m } from "bech32";
4
4
 
5
5
  // src/types.ts
6
+ var ZNS_ACTIONS = ["CLAIM", "BUY", "UPDATE", "LIST", "DELIST", "RELEASE"];
7
+
8
+ // src/zns.ts
9
+ var DEFAULT_URL = "https://light.zcash.me/zns-testnet";
6
10
  var BUY_COMMISSION = 1e4;
7
11
  var LIST_COMMISSION = 1e6;
8
- function toPendingBuy(raw) {
9
- return {
10
- buyer: raw.buyer_ua,
11
- price: raw.price,
12
- claimHeight: raw.claim_height,
13
- expiresAt: raw.expires_at,
14
- txid: raw.txid
15
- };
16
- }
17
- function toListing(raw) {
18
- return {
19
- name: raw.name,
20
- price: raw.price,
21
- payTaddr: raw.pay_taddr,
22
- nonce: raw.nonce,
23
- txid: raw.txid,
24
- height: raw.height,
25
- signature: raw.signature,
26
- pubkey: raw.pubkey,
27
- pendingBuy: raw.pending_buy ? toPendingBuy(raw.pending_buy) : void 0
28
- };
29
- }
30
- function toRegistration(raw) {
31
- return {
32
- name: raw.name,
33
- address: raw.address,
34
- txid: raw.txid,
35
- height: raw.height,
36
- nonce: raw.nonce,
37
- signature: raw.signature,
38
- lastAction: raw.last_action,
39
- pubkey: raw.pubkey,
40
- listing: raw.listing ? toListing(raw.listing) : null
41
- };
42
- }
43
- function toPricing(raw) {
44
- return {
45
- nonce: raw.nonce,
46
- height: raw.height,
47
- tiers: raw.tiers
48
- };
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
+ }
23
+ };
24
+ var NAME_RE = /^[a-z0-9]{1,62}$/;
25
+ function isValidName(name) {
26
+ return NAME_RE.test(name);
49
27
  }
50
- function toStatus(raw) {
51
- return {
52
- syncedHeight: raw.synced_height,
53
- adminPubkey: raw.admin_pubkey,
54
- uivk: raw.uivk,
55
- address: raw.address,
56
- registered: raw.registered,
57
- listed: raw.listed,
58
- pricing: raw.pricing ? toPricing(raw.pricing) : null
59
- };
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;
60
39
  }
61
- function toEvent(raw) {
62
- return {
63
- id: raw.id,
64
- name: raw.name,
65
- action: raw.action,
66
- txid: raw.txid,
67
- height: raw.height,
68
- ua: raw.ua,
69
- price: raw.price,
70
- nonce: raw.nonce,
71
- signature: raw.signature,
72
- pubkey: raw.pubkey
73
- };
40
+ function isWholeNumber(value) {
41
+ return /^\d+$/.test(value) && !value.startsWith("0") || value === "0";
74
42
  }
75
- function toEventsFilter(raw) {
76
- return {
77
- name: raw.name,
78
- action: raw.action,
79
- since_height: raw.sinceHeight,
80
- limit: raw.limit,
81
- offset: raw.offset
82
- };
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
+ }
83
53
  }
84
- function toEventsResult(raw) {
85
- return {
86
- events: raw.events.map(toEvent),
87
- total: raw.total
88
- };
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);
89
61
  }
90
-
91
- // src/zns.ts
92
- var DEFAULT_URL = "https://light.zcash.me/zns-testnet";
93
- var TESTNET_UIVK = "utest1hzw7wyadutvzfgpna80yftsk5l7jeyu2p5me5quvp28tytxueta00cx4068wnlzcv7tx9n3t3gfhsy83pe4y6jrhxtzaq0hj6xtg5zrk2dn7zen3vns2a5pgs4fxdjlletmqrhfa42";
94
- var MAINNET_UIVK = "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9";
95
- var KNOWN_UIVKS = [TESTNET_UIVK, MAINNET_UIVK];
96
- var REGISTRY_ADDRESSES = {
97
- testnet: "utest1f32kn6c4zvn54xr8wfsnxmj9hzpu2mwgtxzpzwcw34906tdccdvzs0z2dx38lly7tpan77x6udt8pjczqm22ymsdhlz9j0tk5yq664nl",
98
- mainnet: "u1k0evt0ahj5qdt6y9ftsxndl8lrkm4ff6rp00u04cjpmqj6hxl9t8hfsxftmn3ht34e03lljh89czn2h8qn67rwrs8x0hm3lsxsucp9q9"
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"] }
99
69
  };
100
- var ZNS_ACTIONS = ["CLAIM", "BUY", "UPDATE", "LIST", "DELIST", "RELEASE"];
101
- var NAME_RE = /^[a-z0-9]{1,62}$/;
102
- function isWholeNumber(value) {
103
- return /^\d+$/.test(value) && !value.startsWith("0") || value === "0";
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
+ }
104
83
  }
105
- function mk(level, action, canonical, message) {
106
- return { valid: level === "valid", action, canonicalAction: canonical, message, level };
84
+ function buildValidationResult(level, action, message) {
85
+ return { valid: level === "valid", action, message, level };
107
86
  }
108
87
  var ZNS = class {
109
88
  /**
@@ -115,8 +94,20 @@ var ZNS = class {
115
94
  constructor(options) {
116
95
  this.rpcId = 0;
117
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;
118
109
  this.network = options?.network ?? "testnet";
119
- this.url = options?.url ?? DEFAULT_URL;
110
+ this.url = options?.url ?? NETWORKS[this.network].url;
120
111
  }
121
112
  /**
122
113
  * Verifies that the connected server is a known ZNS instance.
@@ -124,7 +115,7 @@ var ZNS = class {
124
115
  */
125
116
  async verify() {
126
117
  const status = await this.status();
127
- if (!KNOWN_UIVKS.includes(status.uivk)) {
118
+ if (status.uivk !== NETWORKS[this.network].uivk) {
128
119
  throw new Error(
129
120
  `UIVK mismatch: indexer returned "${status.uivk.slice(0, 20)}..." which is not a known ZNS instance`
130
121
  );
@@ -137,21 +128,17 @@ var ZNS = class {
137
128
  }
138
129
  /** Get the registry address for the current network. */
139
130
  get registryAddress() {
140
- const addr = REGISTRY_ADDRESSES[this.network];
141
- if (!addr) {
142
- throw new Error(`Unknown network: ${this.network}`);
143
- }
144
- return addr;
131
+ return NETWORKS[this.network].registryAddress;
145
132
  }
146
133
  /** Fetch current server status including pricing and configuration. */
147
134
  async status() {
148
135
  const raw = await this.rpc("status");
149
- return toStatus(raw);
136
+ return normalizeApiResponse(raw);
150
137
  }
151
138
  /** Resolve a ZNS name to its registration. Returns null if not registered. */
152
139
  async resolveName(name) {
153
140
  const raw = await this.rpc("resolve", { query: name });
154
- return raw ? toRegistration(raw) : null;
141
+ return raw ? normalizeApiResponse(raw) : null;
155
142
  }
156
143
  /** Resolve a Zcash Unified Address to all names pointing to it. Returns empty array if none.
157
144
  * Supports pagination with limit (default 50, max 500) and offset (default 0). */
@@ -161,7 +148,7 @@ var ZNS = class {
161
148
  limit,
162
149
  offset
163
150
  });
164
- return raw.map(toRegistration);
151
+ return raw.map((r) => normalizeApiResponse(r));
165
152
  }
166
153
  /** List all registered names. Useful for explorers or browsers.
167
154
  * Supports pagination with limit (default 50, max 500) and offset (default 0). */
@@ -171,62 +158,31 @@ var ZNS = class {
171
158
  limit,
172
159
  offset
173
160
  });
174
- return raw.map(toRegistration);
161
+ return raw.map((r) => normalizeApiResponse(r));
175
162
  }
176
163
  /** Check if a name is available for registration.
177
164
  * Returns false immediately for invalid names without hitting the server. */
178
165
  async isAvailable(name) {
179
- if (!this.isValidName(name)) return false;
166
+ if (!isValidName(name)) return false;
180
167
  const result = await this.resolveName(name);
181
168
  return result === null;
182
169
  }
183
- /** Validate a Zcash Unified Address format.
184
- * Accepts both mainnet ('u') and testnet ('utest') prefixes.
185
- * Performs basic format validation but NOT full bech32m checksum verification.
186
- * Returns true if the address looks like a unified address, false otherwise. */
187
- isValidUnifiedAddress(address) {
188
- if (!address) return false;
189
- if (address.startsWith("utest1")) return true;
190
- if (address.startsWith("u1")) return true;
191
- try {
192
- const decoded = bech32m.decode(address);
193
- return decoded.prefix === "u" || decoded.prefix === "utest";
194
- } catch {
195
- return false;
196
- }
197
- }
198
170
  async listings(limit, offset) {
199
- const result = await this.rpc("listings", {
171
+ const raw = await this.rpc("listings", {
200
172
  limit,
201
173
  offset
202
174
  });
203
175
  return {
204
- listings: result.listings.map((l) => ({
205
- name: l.name,
206
- price: l.price,
207
- payTaddr: l.pay_taddr,
208
- nonce: l.nonce,
209
- txid: l.txid,
210
- height: l.height,
211
- signature: l.signature,
212
- pubkey: l.pubkey,
213
- pendingBuy: l.pending_buy ? {
214
- buyer: l.pending_buy.buyer_ua,
215
- price: l.pending_buy.price,
216
- claimHeight: l.pending_buy.claim_height,
217
- expiresAt: l.pending_buy.expires_at,
218
- txid: l.pending_buy.txid
219
- } : void 0
220
- })),
221
- total: result.total
176
+ listings: raw.listings.map((l) => normalizeApiResponse(l)),
177
+ total: raw.total
222
178
  };
223
179
  }
224
180
  async events(filter) {
225
181
  const raw = await this.rpc(
226
182
  "events",
227
- toEventsFilter(filter ?? {})
183
+ normalizeApiResponse(filter ?? {})
228
184
  );
229
- return toEventsResult(raw);
185
+ return normalizeApiResponse(raw);
230
186
  }
231
187
  /**
232
188
  * Verify a listing's signature.
@@ -252,10 +208,6 @@ var ZNS = class {
252
208
  if (!payload) return false;
253
209
  return this.verifyEd25519(payload, reg.signature, pubkey);
254
210
  }
255
- /** Check if a name is valid format (lowercase alphanumeric, 1-62 chars). */
256
- isValidName(name) {
257
- return NAME_RE.test(name);
258
- }
259
211
  /**
260
212
  * Validate a signing payload string against the ZNS memo format spec.
261
213
  *
@@ -282,7 +234,6 @@ var ZNS = class {
282
234
  return {
283
235
  valid: false,
284
236
  action: "",
285
- canonicalAction: null,
286
237
  message: "Empty payload.",
287
238
  level: "invalid"
288
239
  };
@@ -292,83 +243,29 @@ var ZNS = class {
292
243
  return {
293
244
  valid: false,
294
245
  action: raw.toUpperCase(),
295
- canonicalAction: null,
296
246
  message: "Missing colon separator. Expected format: ACTION:field1:field2:...",
297
247
  level: "invalid"
298
248
  };
299
249
  }
300
- const actionUpper = raw.slice(0, colonIdx).toUpperCase();
301
- const actionLower = raw.slice(0, colonIdx).toLowerCase();
250
+ const action = raw.slice(0, colonIdx).toUpperCase();
302
251
  const rest = raw.slice(colonIdx + 1);
303
252
  const parts = rest.split(":");
304
- switch (actionLower) {
305
- case "claim": {
306
- if (parts.length !== 2)
307
- return mk("invalid", actionUpper, "claim", `Expected CLAIM:<name>:<ua>.`);
308
- if (!NAME_RE.test(parts[0]))
309
- return mk("invalid", actionUpper, "claim", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
310
- if (!this.isValidUnifiedAddress(parts[1]))
311
- return mk("invalid", actionUpper, "claim", `Invalid unified address: "${parts[1]}".`);
312
- return mk("valid", actionUpper, "claim", "Valid CLAIM payload.");
313
- }
314
- case "buy": {
315
- if (parts.length !== 2)
316
- return mk("invalid", actionUpper, "buy", `Expected BUY:<name>:<buyer_ua>.`);
317
- if (!NAME_RE.test(parts[0]))
318
- return mk("invalid", actionUpper, "buy", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
319
- if (!this.isValidUnifiedAddress(parts[1]))
320
- return mk("invalid", actionUpper, "buy", `Invalid unified address: "${parts[1]}".`);
321
- return mk("valid", actionUpper, "buy", "Valid BUY payload.");
322
- }
323
- case "update": {
324
- if (parts.length !== 3)
325
- return mk("invalid", actionUpper, "update", `Expected UPDATE:<name>:<ua>:<nonce>.`);
326
- if (!NAME_RE.test(parts[0]))
327
- return mk("invalid", actionUpper, "update", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
328
- if (!this.isValidUnifiedAddress(parts[1]))
329
- return mk("invalid", actionUpper, "update", `Invalid unified address: "${parts[1]}".`);
330
- if (!isWholeNumber(parts[2]))
331
- return mk("invalid", actionUpper, "update", `Nonce must be a whole number.`);
332
- return mk("valid", actionUpper, "update", "Valid UPDATE payload.");
333
- }
334
- case "list": {
335
- if (parts.length !== 4)
336
- return mk("invalid", actionUpper, "list", `Expected LIST:<name>:<price_zats>:<pay_taddr>:<nonce>.`);
337
- if (!NAME_RE.test(parts[0]))
338
- return mk("invalid", actionUpper, "list", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
339
- if (!isWholeNumber(parts[1]) || Number(parts[1]) <= 0)
340
- return mk("invalid", actionUpper, "list", `Price must be a positive whole number in zats.`);
341
- if (!isWholeNumber(parts[3]))
342
- return mk("invalid", actionUpper, "list", `Nonce must be a whole number.`);
343
- return mk("valid", actionUpper, "list", "Valid LIST payload.");
344
- }
345
- case "delist": {
346
- if (parts.length !== 2)
347
- return mk("invalid", actionUpper, "delist", `Expected DELIST:<name>:<nonce>.`);
348
- if (!NAME_RE.test(parts[0]))
349
- return mk("invalid", actionUpper, "delist", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
350
- if (!isWholeNumber(parts[1]))
351
- return mk("invalid", actionUpper, "delist", `Nonce must be a whole number.`);
352
- return mk("valid", actionUpper, "delist", "Valid DELIST payload.");
353
- }
354
- case "release": {
355
- if (parts.length !== 2)
356
- return mk("invalid", actionUpper, "release", `Expected RELEASE:<name>:<nonce>.`);
357
- if (!NAME_RE.test(parts[0]))
358
- return mk("invalid", actionUpper, "release", `Invalid name. Use lowercase a-z and 0-9, 1 to 62 chars.`);
359
- if (!isWholeNumber(parts[1]))
360
- return mk("invalid", actionUpper, "release", `Nonce must be a whole number.`);
361
- return mk("valid", actionUpper, "release", "Valid RELEASE payload.");
362
- }
363
- default:
364
- return {
365
- valid: false,
366
- action: actionUpper,
367
- canonicalAction: null,
368
- message: `Unrecognized action "${actionUpper}". Valid actions: ${ZNS_ACTIONS.join(", ")}.`,
369
- level: "unrecognized"
370
- };
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);
371
267
  }
268
+ return buildValidationResult("valid", action, `Valid ${action} payload.`);
372
269
  }
373
270
  /**
374
271
  * Get the claim cost in zatoshis for a name of given length.
@@ -411,7 +308,7 @@ var ZNS = class {
411
308
  */
412
309
  prepareClaim(name, address, cost) {
413
310
  this.requireValidName(name);
414
- if (!this.isValidUnifiedAddress(address)) {
311
+ if (!isValidUnifiedAddress(address)) {
415
312
  throw new Error(`Invalid Zcash Unified Address: ${address}`);
416
313
  }
417
314
  return {
@@ -454,7 +351,7 @@ var ZNS = class {
454
351
  }
455
352
  prepareUpdate(name, newAddress, nonce) {
456
353
  this.requireValidName(name);
457
- if (!this.isValidUnifiedAddress(newAddress)) {
354
+ if (!isValidUnifiedAddress(newAddress)) {
458
355
  throw new Error(`Invalid Zcash Unified Address: ${newAddress}`);
459
356
  }
460
357
  return {
@@ -536,7 +433,7 @@ var ZNS = class {
536
433
  }
537
434
  }
538
435
  requireValidName(name) {
539
- if (!NAME_RE.test(name)) throw new Error(`Invalid ZNS name: ${name}`);
436
+ if (!isValidName(name)) throw new Error(`Invalid ZNS name: ${name}`);
540
437
  }
541
438
  /** Build a ZIP-321 URI. Amount is in zatoshis and will be converted to ZEC for the URI. */
542
439
  buildZcashUri(address, amountZats, memo) {
@@ -601,8 +498,6 @@ export {
601
498
  BUY_COMMISSION,
602
499
  DEFAULT_URL,
603
500
  LIST_COMMISSION,
604
- MAINNET_UIVK,
605
- TESTNET_UIVK,
606
- ZNS,
607
- ZNS_ACTIONS
501
+ NETWORKS,
502
+ ZNS
608
503
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zcashname-sdk",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "type": "module",
5
5
  "description": "TypeScript SDK for the Zcash Name System (ZNS).",
6
6
  "main": "dist/zns.cjs",