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 +119 -226
- package/dist/zns.d.cts +47 -25
- package/dist/zns.d.ts +47 -25
- package/dist/zns.js +117 -222
- package/package.json +1 -1
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
146
|
-
return { valid: level === "valid", action,
|
|
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 ??
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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(
|
|
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(
|
|
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 (!
|
|
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
|
|
209
|
+
const raw = await this.rpc("listings", {
|
|
240
210
|
limit,
|
|
241
211
|
offset
|
|
242
212
|
});
|
|
243
213
|
return {
|
|
244
|
-
listings:
|
|
245
|
-
|
|
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
|
-
|
|
221
|
+
normalizeApiResponse(filter ?? {})
|
|
268
222
|
);
|
|
269
|
-
return
|
|
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
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
ZNS,
|
|
648
|
-
ZNS_ACTIONS
|
|
540
|
+
NETWORKS,
|
|
541
|
+
ZNS
|
|
649
542
|
});
|
package/dist/zns.d.cts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
/**
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
165
|
-
declare const
|
|
166
|
-
/**
|
|
167
|
-
*
|
|
168
|
-
declare const
|
|
169
|
-
|
|
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
|
-
|
|
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,
|
|
321
|
+
export { BUY_COMMISSION, DEFAULT_URL, LIST_COMMISSION, NETWORKS, ZNS };
|
package/dist/zns.d.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
/**
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
165
|
-
declare const
|
|
166
|
-
/**
|
|
167
|
-
*
|
|
168
|
-
declare const
|
|
169
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
106
|
-
return { valid: level === "valid", action,
|
|
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 ??
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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(
|
|
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(
|
|
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 (!
|
|
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
|
|
171
|
+
const raw = await this.rpc("listings", {
|
|
200
172
|
limit,
|
|
201
173
|
offset
|
|
202
174
|
});
|
|
203
175
|
return {
|
|
204
|
-
listings:
|
|
205
|
-
|
|
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
|
-
|
|
183
|
+
normalizeApiResponse(filter ?? {})
|
|
228
184
|
);
|
|
229
|
-
return
|
|
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
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
ZNS,
|
|
607
|
-
ZNS_ACTIONS
|
|
501
|
+
NETWORKS,
|
|
502
|
+
ZNS
|
|
608
503
|
};
|