@beclab/olaresid 0.1.1 → 0.1.2

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.
Files changed (87) hide show
  1. package/CLI.md +1300 -0
  2. package/README.md +37 -31
  3. package/TAG.md +589 -0
  4. package/dist/abi/RootResolver2ABI.d.ts +54 -0
  5. package/dist/abi/RootResolver2ABI.d.ts.map +1 -0
  6. package/dist/abi/RootResolver2ABI.js +240 -0
  7. package/dist/abi/RootResolver2ABI.js.map +1 -0
  8. package/dist/business/index.d.ts +302 -0
  9. package/dist/business/index.d.ts.map +1 -0
  10. package/dist/business/index.js +1209 -0
  11. package/dist/business/index.js.map +1 -0
  12. package/dist/business/tag-context.d.ts +219 -0
  13. package/dist/business/tag-context.d.ts.map +1 -0
  14. package/dist/business/tag-context.js +537 -0
  15. package/dist/business/tag-context.js.map +1 -0
  16. package/dist/cli.js +2085 -39
  17. package/dist/cli.js.map +1 -1
  18. package/dist/debug.d.ts.map +1 -1
  19. package/dist/debug.js +14 -2
  20. package/dist/debug.js.map +1 -1
  21. package/dist/index.d.ts +50 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +229 -9
  24. package/dist/index.js.map +1 -1
  25. package/dist/utils/crypto-utils.d.ts +130 -0
  26. package/dist/utils/crypto-utils.d.ts.map +1 -0
  27. package/dist/utils/crypto-utils.js +402 -0
  28. package/dist/utils/crypto-utils.js.map +1 -0
  29. package/dist/utils/error-parser.d.ts +35 -0
  30. package/dist/utils/error-parser.d.ts.map +1 -0
  31. package/dist/utils/error-parser.js +202 -0
  32. package/dist/utils/error-parser.js.map +1 -0
  33. package/dist/utils/tag-abi-codec.d.ts +69 -0
  34. package/dist/utils/tag-abi-codec.d.ts.map +1 -0
  35. package/dist/utils/tag-abi-codec.js +144 -0
  36. package/dist/utils/tag-abi-codec.js.map +1 -0
  37. package/dist/utils/tag-type-builder.d.ts +158 -0
  38. package/dist/utils/tag-type-builder.d.ts.map +1 -0
  39. package/dist/utils/tag-type-builder.js +410 -0
  40. package/dist/utils/tag-type-builder.js.map +1 -0
  41. package/examples/crypto-utilities.ts +140 -0
  42. package/examples/domain-context.ts +80 -0
  43. package/examples/generate-mnemonic.ts +149 -0
  44. package/examples/index.ts +1 -1
  45. package/examples/ip.ts +171 -0
  46. package/examples/legacy.ts +10 -10
  47. package/examples/list-wallets.ts +81 -0
  48. package/examples/quasar-demo/.eslintrc.js +23 -0
  49. package/examples/quasar-demo/.quasar/app.js +43 -0
  50. package/examples/quasar-demo/.quasar/client-entry.js +38 -0
  51. package/examples/quasar-demo/.quasar/client-prefetch.js +130 -0
  52. package/examples/quasar-demo/.quasar/quasar-user-options.js +16 -0
  53. package/examples/quasar-demo/README.md +49 -0
  54. package/examples/quasar-demo/index.html +11 -0
  55. package/examples/quasar-demo/package-lock.json +6407 -0
  56. package/examples/quasar-demo/package.json +36 -0
  57. package/examples/quasar-demo/quasar.config.js +73 -0
  58. package/examples/quasar-demo/src/App.vue +13 -0
  59. package/examples/quasar-demo/src/css/app.scss +1 -0
  60. package/examples/quasar-demo/src/layouts/MainLayout.vue +21 -0
  61. package/examples/quasar-demo/src/pages/IndexPage.vue +905 -0
  62. package/examples/quasar-demo/src/router/index.ts +25 -0
  63. package/examples/quasar-demo/src/router/routes.ts +11 -0
  64. package/examples/quasar-demo/tsconfig.json +28 -0
  65. package/examples/register-subdomain.ts +152 -0
  66. package/examples/rsa-keypair.ts +148 -0
  67. package/examples/tag-builder.ts +235 -0
  68. package/examples/tag-management.ts +534 -0
  69. package/examples/tag-nested-tuple.ts +190 -0
  70. package/examples/tag-simple.ts +149 -0
  71. package/examples/tag-tagger.ts +217 -0
  72. package/examples/test-nested-tuple-conversion.ts +143 -0
  73. package/examples/test-type-bytes-parser.ts +70 -0
  74. package/examples/transfer-domain.ts +197 -0
  75. package/examples/wallet-management.ts +196 -0
  76. package/package.json +24 -15
  77. package/src/abi/RootResolver2ABI.ts +237 -0
  78. package/src/business/index.ts +1490 -0
  79. package/src/business/tag-context.ts +713 -0
  80. package/src/cli.ts +2755 -39
  81. package/src/debug.ts +17 -2
  82. package/src/index.ts +300 -14
  83. package/src/utils/crypto-utils.ts +459 -0
  84. package/src/utils/error-parser.ts +225 -0
  85. package/src/utils/tag-abi-codec.ts +158 -0
  86. package/src/utils/tag-type-builder.ts +469 -0
  87. package/tsconfig.json +1 -1
@@ -0,0 +1,1490 @@
1
+ import { DIDConsole } from '..';
2
+ import { ethers } from 'ethers';
3
+ import { parseContractError } from '../utils/error-parser';
4
+ import { TagContext } from './tag-context';
5
+ import { TagTypeBuilder } from '../utils/tag-type-builder';
6
+
7
+ export interface TransactionResult<T = any> {
8
+ // Basic transaction information
9
+ success: boolean;
10
+ transactionHash: string;
11
+ gasUsed?: bigint;
12
+ blockNumber?: number;
13
+
14
+ // Possible business data
15
+ data?: T;
16
+
17
+ error?: string;
18
+ }
19
+
20
+ export interface DomainMetaInfo {
21
+ id: string;
22
+ name: string;
23
+ did: string;
24
+ note: string;
25
+ allowSubdomain: boolean;
26
+ }
27
+
28
+ export interface DomainInfo {
29
+ meta: DomainMetaInfo;
30
+ owner: string;
31
+ tags: TagInfo[];
32
+ subdomains?: string[];
33
+ }
34
+
35
+ export interface TagInfo {
36
+ name: string;
37
+ type: string;
38
+ value: any;
39
+ }
40
+
41
+ // Re-export from crypto-utils
42
+ export {
43
+ createRsaKeyPair,
44
+ generateMnemonic,
45
+ getEthereumAddressFromMnemonic,
46
+ getEVMPrivateKeyFromMnemonic,
47
+ getDIDFromMnemonic,
48
+ generateDIDKeyData,
49
+ deriveDIDFromMnemonic
50
+ } from '../utils/crypto-utils';
51
+ export type { RSAPublicKeyData, DIDKeyData } from '../utils/crypto-utils';
52
+
53
+ // Re-export Tag-related classes
54
+ export { TagContext } from './tag-context';
55
+ export { TagTypeBuilder } from '../utils/tag-type-builder';
56
+
57
+ /**
58
+ * Domain registration type
59
+ */
60
+ export enum UserType {
61
+ IndividualOrganizationalUser = 'Individual:OrganizationalUser',
62
+ IndividualTerminusUser = 'Individual:TerminusUser',
63
+ Organization = 'Organization',
64
+ Entity = 'Entity'
65
+ }
66
+
67
+ export class DomainContext {
68
+ private domainName: string;
69
+ private console: DIDConsole;
70
+ private owner?: string;
71
+ private tokenId?: string;
72
+
73
+ constructor(domainName: string, console: DIDConsole) {
74
+ this.domainName = domainName;
75
+ this.console = console;
76
+ }
77
+
78
+ /**
79
+ * Calculate the tokenId for the domain name
80
+ * This is a pure function that hashes the domain name using keccak256
81
+ * The result is cached after the first call
82
+ * @returns The tokenId as a decimal string
83
+ */
84
+ getTokenId(): string {
85
+ if (!this.tokenId) {
86
+ const hash = ethers.keccak256(ethers.toUtf8Bytes(this.domainName));
87
+ // Convert hex string to decimal string (BigInt format)
88
+ this.tokenId = BigInt(hash).toString();
89
+ }
90
+ return this.tokenId;
91
+ }
92
+
93
+ /**
94
+ * Get only the basic information of the domain
95
+ * Uses parallel RPC calls to get metadata and latest DID for optimal performance
96
+ * TokenId is calculated locally using keccak256
97
+ * @throws Error if domain is not registered or RPC call fails
98
+ */
99
+ async getMetaInfo(): Promise<DomainMetaInfo> {
100
+ const didContract = this.console.getContractDID();
101
+ const resolverContract = this.console.getContractRootResolver();
102
+
103
+ // Calculate tokenId locally (no RPC call needed)
104
+ const tokenId = this.getTokenId();
105
+
106
+ // Parallel execution: Get metadata and latestDID simultaneously
107
+ const [metadata, latestDID] = await Promise.all([
108
+ didContract.getMetadata(tokenId),
109
+ // Use catch to handle cases where latestDID is not set
110
+ resolverContract.getLatestDID(this.domainName).catch(() => '')
111
+ ]);
112
+
113
+ // Use latestDID if it has a value, otherwise fallback to metadata.did
114
+ const finalDid =
115
+ latestDID && latestDID.trim() !== '' ? latestDID : metadata.did;
116
+
117
+ return {
118
+ id: tokenId,
119
+ name: metadata.domain,
120
+ did: finalDid,
121
+ note: metadata.notes,
122
+ allowSubdomain: metadata.allowSubdomain
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Get the owner address of the domain
128
+ * Uses cached value if available, otherwise fetches from chain
129
+ * @throws Error if domain is not registered or RPC call fails
130
+ */
131
+ async getOwner(): Promise<string> {
132
+ // Return cached owner if available
133
+ if (this.owner) {
134
+ return this.owner;
135
+ }
136
+
137
+ const contract = this.console.getContractDID();
138
+
139
+ // Calculate tokenId locally (no RPC call needed)
140
+ const tokenId = this.getTokenId();
141
+
142
+ // Single RPC call to get owner
143
+ const owner = await contract.ownerOf(tokenId);
144
+
145
+ // Cache the owner for later use
146
+ this.owner = owner;
147
+ return owner;
148
+ }
149
+
150
+ /**
151
+ * Check if the connected signer is the owner of the domain
152
+ * @throws Error if no signer is connected or RPC call fails
153
+ */
154
+ async isOwner(): Promise<boolean> {
155
+ const signer = this.console.getSigner();
156
+ const signerAddress = await signer.getAddress();
157
+ const owner = await this.getOwner();
158
+
159
+ return signerAddress.toLowerCase() === owner.toLowerCase();
160
+ }
161
+
162
+ /**
163
+ * Register a subdomain using mnemonic-derived keys
164
+ *
165
+ * @param subdomain Subdomain label only (e.g., "child" for "child.parent.com")
166
+ * @param mnemonic BIP39 mnemonic phrase (12 words by default) to derive owner and DID
167
+ * @returns Transaction result with success status and transaction hash
168
+ * @throws Error if no signer is connected, parent domain doesn't exist, or transaction fails
169
+ *
170
+ * The subdomain's metadata:
171
+ * - owner: derived from mnemonic using Trust Wallet Core (same as TermiPass)
172
+ * - DID: derived from mnemonic using Ed25519 (same as TermiPass)
173
+ * - note: inherits from parent domain
174
+ * - allowSubdomain: inherits from parent domain
175
+ *
176
+ * @example
177
+ * ```typescript
178
+ * // For parent domain "parent.com", register subdomain "child"
179
+ * const parentDomain = olaresId.domain('parent.com');
180
+ * const mnemonic = generateMnemonic(12);
181
+ *
182
+ * const result = await parentDomain.registerSubdomain('child', mnemonic);
183
+ * // This will register "child.parent.com"
184
+ *
185
+ * if (result.success) {
186
+ * console.log('Full domain:', result.data.fullDomainName); // "child.parent.com"
187
+ * console.log('Owner:', result.data.owner);
188
+ * console.log('DID:', result.data.did);
189
+ * }
190
+ * ```
191
+ */
192
+ async registerSubdomain(
193
+ subdomain: string,
194
+ mnemonic: string
195
+ ): Promise<TransactionResult> {
196
+ try {
197
+ // 1. Get parent domain metadata to inherit note and allowSubdomain
198
+ const parentMetadata = await this.getMetaInfo();
199
+
200
+ // 2. Construct full domain name: subdomain.parentDomain
201
+ const fullDomainName = `${subdomain}.${parentMetadata.name}`;
202
+
203
+ // 3. Derive owner and DID from mnemonic using Trust Wallet Core
204
+ const { deriveDIDFromMnemonic } = await import(
205
+ '../utils/crypto-utils'
206
+ );
207
+ const { owner, did } = await deriveDIDFromMnemonic(mnemonic);
208
+
209
+ // 4. Prepare metadata for subdomain (inherit from parent)
210
+ const subdomainNote = parentMetadata.note as UserType;
211
+ const subdomainAllowSubdomain = parentMetadata.allowSubdomain;
212
+
213
+ // 5. Call register function on the contract
214
+ const contract = this.console.getSignerContractDID();
215
+
216
+ const metadata = {
217
+ domain: fullDomainName,
218
+ did: did,
219
+ notes: subdomainNote,
220
+ allowSubdomain: subdomainAllowSubdomain
221
+ };
222
+
223
+ const tx = await contract.register(owner, metadata);
224
+ const receipt = await tx.wait();
225
+
226
+ return {
227
+ success: true,
228
+ transactionHash: receipt.hash,
229
+ gasUsed: receipt.gasUsed,
230
+ blockNumber: receipt.blockNumber,
231
+ data: {
232
+ owner,
233
+ did,
234
+ subdomain,
235
+ fullDomainName
236
+ }
237
+ };
238
+ } catch (error: any) {
239
+ const parsedError = parseContractError(error);
240
+
241
+ // Always throw network errors
242
+ if (parsedError.isNetworkError) {
243
+ throw new Error(`Network error: ${parsedError.message}`);
244
+ }
245
+
246
+ return {
247
+ success: false,
248
+ transactionHash: '',
249
+ error: parsedError.message
250
+ };
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Transfer domain ownership to a new owner derived from mnemonic
256
+ *
257
+ * @param mnemonic BIP39 mnemonic phrase to derive new owner and DID
258
+ * @returns Transaction result with success status
259
+ *
260
+ * This function performs two operations:
261
+ * 1. Transfer NFT ownership via DID contract's transferFrom
262
+ * 2. Update DID via RootResolver's setLatestDID
263
+ *
264
+ * @example
265
+ * ```typescript
266
+ * const mnemonic = generateMnemonic(12);
267
+ * const result = await domain.transfer(mnemonic);
268
+ * if (result.success) {
269
+ * console.log('Domain transferred!');
270
+ * console.log('New Owner:', result.data.newOwner);
271
+ * console.log('New DID:', result.data.newDid);
272
+ * }
273
+ * ```
274
+ */
275
+ async transfer(mnemonic: string): Promise<TransactionResult> {
276
+ try {
277
+ // 1. Get current domain info
278
+ const currentOwner = await this.getOwner();
279
+ const tokenId = this.getTokenId();
280
+
281
+ // 2. Derive new owner and DID from mnemonic
282
+ const { deriveDIDFromMnemonic } = await import(
283
+ '../utils/crypto-utils'
284
+ );
285
+ const { owner: newOwner, did: newDid } =
286
+ await deriveDIDFromMnemonic(mnemonic);
287
+
288
+ // 3. Update DID FIRST (while current owner still has permission)
289
+ // This must be done before transferFrom, because after transfer,
290
+ // the current owner will lose permission to call setLatestDID
291
+ const resolverContract =
292
+ this.console.getSignerContractRootResolver();
293
+ const setDidTx = await resolverContract.setLatestDID(
294
+ this.domainName,
295
+ newDid
296
+ );
297
+ const setDidReceipt = await setDidTx.wait();
298
+
299
+ // 4. Transfer NFT ownership (DID contract)
300
+ // After this, the new owner will have control
301
+ const didContract = this.console.getSignerContractDID();
302
+ const transferTx = await didContract.transferFrom(
303
+ currentOwner,
304
+ newOwner,
305
+ tokenId
306
+ );
307
+ const transferReceipt = await transferTx.wait();
308
+
309
+ // Calculate total gas used
310
+ const totalGasUsed =
311
+ setDidReceipt.gasUsed + transferReceipt.gasUsed;
312
+
313
+ return {
314
+ success: true,
315
+ transactionHash: transferReceipt.hash,
316
+ gasUsed: totalGasUsed,
317
+ blockNumber: transferReceipt.blockNumber,
318
+ data: {
319
+ newOwner,
320
+ newDid,
321
+ tokenId,
322
+ setDidTxHash: setDidReceipt.hash,
323
+ transferTxHash: transferReceipt.hash
324
+ }
325
+ };
326
+ } catch (error: any) {
327
+ const parsedError = parseContractError(error);
328
+
329
+ // Always throw network errors
330
+ if (parsedError.isNetworkError) {
331
+ throw new Error(`Network error: ${parsedError.message}`);
332
+ }
333
+
334
+ return {
335
+ success: false,
336
+ transactionHash: '',
337
+ error: parsedError.message
338
+ };
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Set the RSA public key for your own domain
344
+ * Users can use the helper function createRsaKeyPair to generate a new RSA key pair
345
+ * The pubKey parameter should be in RSA PKCS8 ASN.1 format
346
+ * @param rsaPublicKey - The RSA public key in PEM format or DER hex format (with or without '0x' prefix)
347
+ * @throws Error if no signer is connected or transaction fails
348
+ */
349
+ async setRSAPublicKey(rsaPublicKey: string): Promise<TransactionResult> {
350
+ const contract = this.console.getSignerContractRootResolver();
351
+
352
+ try {
353
+ let pubKeyBytes: string;
354
+
355
+ // Check if input is PEM format (contains BEGIN/END markers)
356
+ if (rsaPublicKey.includes('BEGIN')) {
357
+ // Convert PEM to DER hex format
358
+ pubKeyBytes = pemToDer(rsaPublicKey);
359
+ } else {
360
+ // Assume it's already in hex format, ensure it has '0x' prefix
361
+ pubKeyBytes = rsaPublicKey.startsWith('0x')
362
+ ? rsaPublicKey
363
+ : '0x' + rsaPublicKey;
364
+ }
365
+
366
+ const tx = await contract.setRsaPubKey(
367
+ this.domainName,
368
+ pubKeyBytes
369
+ );
370
+ const receipt = await tx.wait();
371
+
372
+ return {
373
+ success: true,
374
+ transactionHash: receipt.hash,
375
+ gasUsed: receipt.gasUsed,
376
+ blockNumber: receipt.blockNumber
377
+ };
378
+ } catch (error: any) {
379
+ const parsedError = parseContractError(error);
380
+
381
+ // Always throw network errors
382
+ if (parsedError.isNetworkError) {
383
+ throw new Error(`Network error: ${parsedError.message}`);
384
+ }
385
+
386
+ return {
387
+ success: false,
388
+ transactionHash: '',
389
+ error: parsedError.message
390
+ };
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Remove the RSA public key for your own domain
396
+ * This is done by calling setRsaPubKey with empty bytes
397
+ * @throws Error if no signer is connected or transaction fails
398
+ */
399
+ async removeRSAPublicKey(): Promise<TransactionResult> {
400
+ const contract = this.console.getSignerContractRootResolver();
401
+
402
+ try {
403
+ // Pass empty bytes to remove the RSA public key
404
+ const tx = await contract.setRsaPubKey(this.domainName, '0x');
405
+ const receipt = await tx.wait();
406
+
407
+ return {
408
+ success: true,
409
+ transactionHash: receipt.hash,
410
+ gasUsed: receipt.gasUsed,
411
+ blockNumber: receipt.blockNumber
412
+ };
413
+ } catch (error: any) {
414
+ const parsedError = parseContractError(error);
415
+
416
+ // Always throw network errors
417
+ if (parsedError.isNetworkError) {
418
+ throw new Error(`Network error: ${parsedError.message}`);
419
+ }
420
+
421
+ return {
422
+ success: false,
423
+ transactionHash: '',
424
+ error: parsedError.message
425
+ };
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Get the RSA public key for the domain
431
+ * @returns The RSA public key in PEM PKCS#8 format, or null if not set
432
+ * @throws Error if network error or unexpected contract error occurs
433
+ */
434
+ async getRSAPublicKey(): Promise<string | null> {
435
+ const contract = this.console.getContractRootResolver();
436
+
437
+ try {
438
+ const pubKey = await contract.getRsaPubKey(this.domainName);
439
+ // If pubKey is empty bytes, return null
440
+ if (pubKey === '0x' || pubKey.length === 2) {
441
+ return null;
442
+ }
443
+ // Convert DER hex format to PEM PKCS#8 format
444
+ return derToPem(pubKey);
445
+ } catch (error: any) {
446
+ const parsedError = parseContractError(error);
447
+
448
+ // Always throw network errors
449
+ if (parsedError.isNetworkError) {
450
+ throw new Error(`Network error: ${parsedError.message}`);
451
+ }
452
+
453
+ // If tag doesn't exist, return null (this is expected)
454
+ if (
455
+ parsedError.errorName === 'RootTagNoExists' ||
456
+ parsedError.errorName === 'TagNotExist'
457
+ ) {
458
+ return null;
459
+ }
460
+
461
+ // For other contract errors, throw with friendly message
462
+ throw new Error(parsedError.message);
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Set the DNS A record for your own domain, currently the contract only supports IPv4 address format
468
+ * The parameter is an IPv4 address, in xxx.xxx.xxx.xxx format
469
+ * @param aRecord - IPv4 address string (e.g., "192.168.1.1")
470
+ * @throws Error if no signer is connected or transaction fails
471
+ */
472
+ async setIP(aRecord: string): Promise<TransactionResult> {
473
+ const contract = this.console.getSignerContractRootResolver();
474
+
475
+ try {
476
+ // Convert IPv4 string to bytes4 format
477
+ const ipv4Bytes = ipv4ToBytes4(aRecord);
478
+
479
+ const tx = await contract.setDnsARecord(this.domainName, ipv4Bytes);
480
+ const receipt = await tx.wait();
481
+
482
+ return {
483
+ success: true,
484
+ transactionHash: receipt.hash,
485
+ gasUsed: receipt.gasUsed,
486
+ blockNumber: receipt.blockNumber
487
+ };
488
+ } catch (error: any) {
489
+ const parsedError = parseContractError(error);
490
+
491
+ // Always throw network errors
492
+ if (parsedError.isNetworkError) {
493
+ throw new Error(`Network error: ${parsedError.message}`);
494
+ }
495
+
496
+ return {
497
+ success: false,
498
+ transactionHash: '',
499
+ error: parsedError.message
500
+ };
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Remove the DNS A record for your own domain
506
+ * This is done by calling setDnsARecord with bytes4(0)
507
+ * @throws Error if no signer is connected or transaction fails
508
+ */
509
+ async removeIP(): Promise<TransactionResult> {
510
+ const contract = this.console.getSignerContractRootResolver();
511
+
512
+ try {
513
+ // Pass bytes4(0) to remove the DNS A record
514
+ const tx = await contract.setDnsARecord(
515
+ this.domainName,
516
+ '0x00000000'
517
+ );
518
+ const receipt = await tx.wait();
519
+
520
+ return {
521
+ success: true,
522
+ transactionHash: receipt.hash,
523
+ gasUsed: receipt.gasUsed,
524
+ blockNumber: receipt.blockNumber
525
+ };
526
+ } catch (error: any) {
527
+ const parsedError = parseContractError(error);
528
+
529
+ // Always throw network errors
530
+ if (parsedError.isNetworkError) {
531
+ throw new Error(`Network error: ${parsedError.message}`);
532
+ }
533
+
534
+ return {
535
+ success: false,
536
+ transactionHash: '',
537
+ error: parsedError.message
538
+ };
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Get the DNS A record for your own domain
544
+ * @returns The IPv4 address as a string (e.g., "192.168.1.1"), or null if not set
545
+ * @throws Error if network error or unexpected contract error occurs
546
+ */
547
+ async getIP(): Promise<string | null> {
548
+ const contract = this.console.getContractRootResolver();
549
+
550
+ try {
551
+ const ipv4Bytes = await contract.getDnsARecord(this.domainName);
552
+ // If ipv4Bytes is 0x00000000, return null
553
+ if (ipv4Bytes === '0x00000000') {
554
+ return null;
555
+ }
556
+ // Convert bytes4 to IPv4 string
557
+ return bytes4ToIpv4(ipv4Bytes);
558
+ } catch (error: any) {
559
+ const parsedError = parseContractError(error);
560
+
561
+ // Always throw network errors
562
+ if (parsedError.isNetworkError) {
563
+ throw new Error(`Network error: ${parsedError.message}`);
564
+ }
565
+
566
+ // If tag doesn't exist, return null (this is expected)
567
+ if (
568
+ parsedError.errorName === 'RootTagNoExists' ||
569
+ parsedError.errorName === 'TagNotExist'
570
+ ) {
571
+ return null;
572
+ }
573
+
574
+ // For other contract errors, throw with friendly message
575
+ throw new Error(parsedError.message);
576
+ }
577
+ }
578
+
579
+ /**
580
+ * Add EVM wallet address to the domain's authenticated address tag
581
+ * @param evmPrivateKey - EVM private key (hex string with 0x prefix)
582
+ * @returns Transaction result with success status and transaction hash
583
+ */
584
+ async addEVMWallet(evmPrivateKey: string): Promise<TransactionResult> {
585
+ try {
586
+ // Get wallet address from private key
587
+ const wallet = new ethers.Wallet(evmPrivateKey);
588
+ const evmAddress = wallet.address;
589
+
590
+ // Get current timestamp
591
+ const signAt = Math.floor(Date.now() / 1000) - 30 * 60; // 30 minutes ago
592
+
593
+ // Prepare the request value
594
+ const value = {
595
+ addr: evmAddress,
596
+ domain: this.domainName,
597
+ signAt: signAt,
598
+ action: 0 // Action.Add
599
+ };
600
+
601
+ // Get domain owner's signer
602
+ const domainOwner = this.console.getSigner();
603
+ if (!domainOwner) {
604
+ throw new Error(
605
+ 'Signer not set. Please call setSigner() first.'
606
+ );
607
+ }
608
+
609
+ // Get RootTagger2 contract
610
+ const rootTagger = this.console.getSignerContractRootResolver2();
611
+ const chainId = (await domainOwner.provider?.getNetwork())?.chainId;
612
+
613
+ // Define EIP-712 domain
614
+ const domain = {
615
+ name: 'Terminus DID Root Tagger',
616
+ version: '1',
617
+ chainId: chainId,
618
+ verifyingContract: await rootTagger.getAddress()
619
+ };
620
+
621
+ // Define EIP-712 types
622
+ const types = {
623
+ EVMAuthAddressReq: [
624
+ { name: 'addr', type: 'address' },
625
+ { name: 'domain', type: 'string' },
626
+ { name: 'signAt', type: 'uint256' },
627
+ { name: 'action', type: 'uint8' }
628
+ ]
629
+ };
630
+
631
+ // Sign by domain owner
632
+ const sigFromDomainOwner = await domainOwner.signTypedData(
633
+ domain,
634
+ types,
635
+ value
636
+ );
637
+
638
+ // Sign by the EVM wallet
639
+ const sigFromAuthAddr = await wallet.signTypedData(
640
+ domain,
641
+ types,
642
+ value
643
+ );
644
+
645
+ // Call contract
646
+ const tx = await rootTagger.updateEVMWallet(
647
+ value,
648
+ sigFromDomainOwner,
649
+ sigFromAuthAddr
650
+ );
651
+
652
+ const receipt = await tx.wait();
653
+
654
+ return {
655
+ success: true,
656
+ transactionHash: receipt.hash,
657
+ gasUsed: receipt.gasUsed,
658
+ blockNumber: receipt.blockNumber,
659
+ data: { address: evmAddress }
660
+ };
661
+ } catch (error: any) {
662
+ const errorInfo = parseContractError(error);
663
+
664
+ // Always throw network errors
665
+ if (errorInfo.isNetworkError) {
666
+ throw new Error(`Network error: ${errorInfo.message}`);
667
+ }
668
+
669
+ return {
670
+ success: false,
671
+ transactionHash: '',
672
+ error: errorInfo.message
673
+ };
674
+ }
675
+ }
676
+
677
+ /**
678
+ * Remove EVM wallet address from the domain's authenticated address tag
679
+ * @param evmPrivateKey - EVM private key (hex string with 0x prefix)
680
+ * @returns Transaction result with success status and transaction hash
681
+ */
682
+ async removeEVMWallet(evmPrivateKey: string): Promise<TransactionResult> {
683
+ try {
684
+ // Get wallet address from private key
685
+ const wallet = new ethers.Wallet(evmPrivateKey);
686
+ const evmAddress = wallet.address;
687
+
688
+ // Get current timestamp
689
+ const signAt = Math.floor(Date.now() / 1000) - 30 * 60; // 30 minutes ago
690
+
691
+ // Prepare the request value
692
+ const value = {
693
+ addr: evmAddress,
694
+ domain: this.domainName,
695
+ signAt: signAt,
696
+ action: 1 // Action.Remove
697
+ };
698
+
699
+ // Get domain owner's signer
700
+ const domainOwner = this.console.getSigner();
701
+ if (!domainOwner) {
702
+ throw new Error(
703
+ 'Signer not set. Please call setSigner() first.'
704
+ );
705
+ }
706
+
707
+ // Get RootTagger2 contract
708
+ const rootTagger = this.console.getSignerContractRootResolver2();
709
+ const chainId = (await domainOwner.provider?.getNetwork())?.chainId;
710
+
711
+ // Define EIP-712 domain
712
+ const domain = {
713
+ name: 'Terminus DID Root Tagger',
714
+ version: '1',
715
+ chainId: chainId,
716
+ verifyingContract: await rootTagger.getAddress()
717
+ };
718
+
719
+ // Define EIP-712 types
720
+ const types = {
721
+ EVMAuthAddressReq: [
722
+ { name: 'addr', type: 'address' },
723
+ { name: 'domain', type: 'string' },
724
+ { name: 'signAt', type: 'uint256' },
725
+ { name: 'action', type: 'uint8' }
726
+ ]
727
+ };
728
+
729
+ // Sign by domain owner
730
+ const sigFromDomainOwner = await domainOwner.signTypedData(
731
+ domain,
732
+ types,
733
+ value
734
+ );
735
+
736
+ // For remove action, signature from address is not required (pass empty bytes)
737
+ const sigFromAuthAddr = '0x';
738
+
739
+ // Call contract
740
+ const tx = await rootTagger.updateEVMWallet(
741
+ value,
742
+ sigFromDomainOwner,
743
+ sigFromAuthAddr
744
+ );
745
+
746
+ const receipt = await tx.wait();
747
+
748
+ return {
749
+ success: true,
750
+ transactionHash: receipt.hash,
751
+ gasUsed: receipt.gasUsed,
752
+ blockNumber: receipt.blockNumber,
753
+ data: { address: evmAddress }
754
+ };
755
+ } catch (error: any) {
756
+ const errorInfo = parseContractError(error);
757
+
758
+ // Always throw network errors
759
+ if (errorInfo.isNetworkError) {
760
+ throw new Error(`Network error: ${errorInfo.message}`);
761
+ }
762
+
763
+ return {
764
+ success: false,
765
+ transactionHash: '',
766
+ error: errorInfo.message
767
+ };
768
+ }
769
+ }
770
+
771
+ /**
772
+ * Get all EVM wallet addresses for the domain
773
+ * Combines addresses from both RootTagger2 (new) and RootResolver (legacy) for backward compatibility
774
+ * @returns Array of unique EVM wallet addresses
775
+ */
776
+ async getEVMWallets(): Promise<string[]> {
777
+ const addresses: string[] = [];
778
+
779
+ // Get addresses from RootTagger2 (new method)
780
+ try {
781
+ const rootTagger2 = this.console.getContractRootResolver2();
782
+ const result = await rootTagger2.getEVMWallets(this.domainName);
783
+
784
+ // Extract addresses from the result
785
+ // Result is array of { algorithm, addr }
786
+ const tagger2Addresses = result.map((item: any) =>
787
+ item.addr.toLowerCase()
788
+ );
789
+ addresses.push(...tagger2Addresses);
790
+ } catch (error: any) {
791
+ const errorInfo = parseContractError(error);
792
+
793
+ // Only skip if it's a contract error (tag doesn't exist, etc.)
794
+ // Re-throw network errors
795
+ if (errorInfo.isNetworkError) {
796
+ throw new Error(
797
+ `Network error fetching from RootTagger2: ${errorInfo.message}`
798
+ );
799
+ }
800
+ // Contract errors are silently handled (tag doesn't exist is normal)
801
+ }
802
+
803
+ // Get addresses from RootResolver (legacy method for backward compatibility)
804
+ try {
805
+ const rootResolver = this.console.getContractRootResolver();
806
+ const result = await rootResolver.getAuthenticationAddresses(
807
+ this.domainName
808
+ );
809
+
810
+ // Extract addresses from the result
811
+ // Result is array of { algorithm, addr }
812
+ const legacyAddresses = result.map((item: any) =>
813
+ item.addr.toLowerCase()
814
+ );
815
+ addresses.push(...legacyAddresses);
816
+ } catch (error: any) {
817
+ const errorInfo = parseContractError(error);
818
+
819
+ // Only skip if it's a contract error
820
+ // Re-throw network errors
821
+ if (errorInfo.isNetworkError) {
822
+ throw new Error(
823
+ `Network error fetching from RootResolver (legacy): ${errorInfo.message}`
824
+ );
825
+ }
826
+ // Contract errors are silently handled
827
+ }
828
+
829
+ // Remove duplicates and return
830
+ return [...new Set(addresses)];
831
+ }
832
+
833
+ /**
834
+ * Add Solana wallet address to the domain's authenticated address tag
835
+ * @param solanaPrivateKey - Solana private key (base64 or base58 encoded string)
836
+ * @returns Transaction result with success status and transaction hash
837
+ */
838
+ async addSolanaWallet(
839
+ solanaPrivateKey: string
840
+ ): Promise<TransactionResult> {
841
+ try {
842
+ // Import Solana libraries dynamically
843
+ const { Keypair } = await import('@solana/web3.js');
844
+ const nacl = await import('tweetnacl');
845
+ const { decodeUTF8 } = await import('tweetnacl-util');
846
+
847
+ // Parse Solana private key (support both base58 and base64)
848
+ let solanaWallet: any;
849
+ try {
850
+ // Try base58 first (most common format for Phantom)
851
+ const bs58 = await import('bs58');
852
+ const secretKey = bs58.default.decode(solanaPrivateKey);
853
+ solanaWallet = Keypair.fromSecretKey(secretKey);
854
+ } catch {
855
+ // Try base64
856
+ try {
857
+ const secretKey = Buffer.from(solanaPrivateKey, 'base64');
858
+ solanaWallet = Keypair.fromSecretKey(secretKey);
859
+ } catch {
860
+ // Try as JSON array
861
+ const secretKey = JSON.parse(solanaPrivateKey);
862
+ solanaWallet = Keypair.fromSecretKey(
863
+ Uint8Array.from(secretKey)
864
+ );
865
+ }
866
+ }
867
+
868
+ // Get Solana address as bytes32
869
+ const solanaAddressBytes =
870
+ '0x' + solanaWallet.publicKey.toBuffer().toString('hex');
871
+
872
+ // Get current timestamp
873
+ const signAt = Math.floor(Date.now() / 1000) - 30 * 60; // 30 minutes ago
874
+
875
+ // Prepare the request value
876
+ const value = {
877
+ addr: solanaAddressBytes,
878
+ domain: this.domainName,
879
+ signAt: signAt,
880
+ action: 0 // Action.Add
881
+ };
882
+
883
+ // Get domain owner's signer
884
+ const domainOwner = this.console.getSigner();
885
+ if (!domainOwner) {
886
+ throw new Error(
887
+ 'Signer not set. Please call setSigner() first.'
888
+ );
889
+ }
890
+
891
+ // Get RootTagger2 contract
892
+ const rootTagger = this.console.getSignerContractRootResolver2();
893
+ const chainId = (await domainOwner.provider?.getNetwork())?.chainId;
894
+
895
+ // Define EIP-712 domain
896
+ const domain = {
897
+ name: 'Terminus DID Root Tagger',
898
+ version: '1',
899
+ chainId: chainId,
900
+ verifyingContract: await rootTagger.getAddress()
901
+ };
902
+
903
+ // Define EIP-712 types for Solana
904
+ const types = {
905
+ SolanaAuthAddressReq: [
906
+ { name: 'addr', type: 'bytes32' },
907
+ { name: 'domain', type: 'string' },
908
+ { name: 'signAt', type: 'uint256' },
909
+ { name: 'action', type: 'uint8' }
910
+ ]
911
+ };
912
+
913
+ // Sign by domain owner
914
+ const sigFromDomainOwner = await domainOwner.signTypedData(
915
+ domain,
916
+ types,
917
+ value
918
+ );
919
+
920
+ // Sign by the Solana wallet (different signature method)
921
+ const solanaMsg =
922
+ 'prove ownership of Solana wallet ' +
923
+ solanaWallet.publicKey.toBase58() +
924
+ ' for Terminus DID ' +
925
+ this.domainName;
926
+ const sigFromAuthAddr = nacl.default.sign.detached(
927
+ decodeUTF8(solanaMsg),
928
+ solanaWallet.secretKey
929
+ );
930
+ const sigFromAuthAddrHex =
931
+ '0x' + Buffer.from(sigFromAuthAddr).toString('hex');
932
+
933
+ // Call contract
934
+ const tx = await rootTagger.updateSolanaWallet(
935
+ value,
936
+ sigFromDomainOwner,
937
+ sigFromAuthAddrHex
938
+ );
939
+
940
+ const receipt = await tx.wait();
941
+
942
+ return {
943
+ success: true,
944
+ transactionHash: receipt.hash,
945
+ gasUsed: receipt.gasUsed,
946
+ blockNumber: receipt.blockNumber,
947
+ data: {
948
+ address: solanaWallet.publicKey.toBase58(),
949
+ addressHex: solanaAddressBytes
950
+ }
951
+ };
952
+ } catch (error: any) {
953
+ const errorInfo = parseContractError(error);
954
+
955
+ // Always throw network errors
956
+ if (errorInfo.isNetworkError) {
957
+ throw new Error(`Network error: ${errorInfo.message}`);
958
+ }
959
+
960
+ return {
961
+ success: false,
962
+ transactionHash: '',
963
+ error: errorInfo.message
964
+ };
965
+ }
966
+ }
967
+
968
+ /**
969
+ * Remove Solana wallet address from the domain's authenticated address tag
970
+ * @param solanaPrivateKey - Solana private key (base64 or base58 encoded string)
971
+ * @returns Transaction result with success status and transaction hash
972
+ */
973
+ async removeSolanaWallet(
974
+ solanaPrivateKey: string
975
+ ): Promise<TransactionResult> {
976
+ try {
977
+ // Import Solana libraries dynamically
978
+ const { Keypair } = await import('@solana/web3.js');
979
+
980
+ // Parse Solana private key
981
+ let solanaWallet: any;
982
+ try {
983
+ // Try base58 first
984
+ const bs58 = await import('bs58');
985
+ const secretKey = bs58.default.decode(solanaPrivateKey);
986
+ solanaWallet = Keypair.fromSecretKey(secretKey);
987
+ } catch {
988
+ // Try base64
989
+ try {
990
+ const secretKey = Buffer.from(solanaPrivateKey, 'base64');
991
+ solanaWallet = Keypair.fromSecretKey(secretKey);
992
+ } catch {
993
+ // Try as JSON array
994
+ const secretKey = JSON.parse(solanaPrivateKey);
995
+ solanaWallet = Keypair.fromSecretKey(
996
+ Uint8Array.from(secretKey)
997
+ );
998
+ }
999
+ }
1000
+
1001
+ // Get Solana address as bytes32
1002
+ const solanaAddressBytes =
1003
+ '0x' + solanaWallet.publicKey.toBuffer().toString('hex');
1004
+
1005
+ // Get current timestamp
1006
+ const signAt = Math.floor(Date.now() / 1000) - 30 * 60; // 30 minutes ago
1007
+
1008
+ // Prepare the request value
1009
+ const value = {
1010
+ addr: solanaAddressBytes,
1011
+ domain: this.domainName,
1012
+ signAt: signAt,
1013
+ action: 1 // Action.Remove
1014
+ };
1015
+
1016
+ // Get domain owner's signer
1017
+ const domainOwner = this.console.getSigner();
1018
+ if (!domainOwner) {
1019
+ throw new Error(
1020
+ 'Signer not set. Please call setSigner() first.'
1021
+ );
1022
+ }
1023
+
1024
+ // Get RootTagger2 contract
1025
+ const rootTagger = this.console.getSignerContractRootResolver2();
1026
+ const chainId = (await domainOwner.provider?.getNetwork())?.chainId;
1027
+
1028
+ // Define EIP-712 domain
1029
+ const domain = {
1030
+ name: 'Terminus DID Root Tagger',
1031
+ version: '1',
1032
+ chainId: chainId,
1033
+ verifyingContract: await rootTagger.getAddress()
1034
+ };
1035
+
1036
+ // Define EIP-712 types for Solana
1037
+ const types = {
1038
+ SolanaAuthAddressReq: [
1039
+ { name: 'addr', type: 'bytes32' },
1040
+ { name: 'domain', type: 'string' },
1041
+ { name: 'signAt', type: 'uint256' },
1042
+ { name: 'action', type: 'uint8' }
1043
+ ]
1044
+ };
1045
+
1046
+ // Sign by domain owner
1047
+ const sigFromDomainOwner = await domainOwner.signTypedData(
1048
+ domain,
1049
+ types,
1050
+ value
1051
+ );
1052
+
1053
+ // For remove action, signature from address is not required (pass empty bytes)
1054
+ const sigFromAuthAddr = '0x';
1055
+
1056
+ // Call contract
1057
+ const tx = await rootTagger.updateSolanaWallet(
1058
+ value,
1059
+ sigFromDomainOwner,
1060
+ sigFromAuthAddr
1061
+ );
1062
+
1063
+ const receipt = await tx.wait();
1064
+
1065
+ return {
1066
+ success: true,
1067
+ transactionHash: receipt.hash,
1068
+ gasUsed: receipt.gasUsed,
1069
+ blockNumber: receipt.blockNumber,
1070
+ data: {
1071
+ address: solanaWallet.publicKey.toBase58(),
1072
+ addressHex: solanaAddressBytes
1073
+ }
1074
+ };
1075
+ } catch (error: any) {
1076
+ const errorInfo = parseContractError(error);
1077
+
1078
+ // Always throw network errors
1079
+ if (errorInfo.isNetworkError) {
1080
+ throw new Error(`Network error: ${errorInfo.message}`);
1081
+ }
1082
+
1083
+ return {
1084
+ success: false,
1085
+ transactionHash: '',
1086
+ error: errorInfo.message
1087
+ };
1088
+ }
1089
+ }
1090
+
1091
+ /**
1092
+ * Get all Solana wallet addresses for the domain
1093
+ * @returns Array of Solana wallet addresses (in base58 format)
1094
+ */
1095
+ async getSolanaWallets(): Promise<string[]> {
1096
+ try {
1097
+ const { PublicKey } = await import('@solana/web3.js');
1098
+ const rootTagger = this.console.getContractRootResolver2();
1099
+ const result = await rootTagger.getSolanaWallets(this.domainName);
1100
+
1101
+ // Extract addresses from the result and convert to base58
1102
+ // Result is array of { algorithm, addr } where addr is bytes32
1103
+ return result.map((item: any) => {
1104
+ // Remove 0x prefix and convert hex to buffer
1105
+ const hexStr = item.addr.startsWith('0x')
1106
+ ? item.addr.slice(2)
1107
+ : item.addr;
1108
+ const buffer = Buffer.from(hexStr, 'hex');
1109
+ // Convert to Solana public key and then to base58
1110
+ return new PublicKey(buffer).toBase58();
1111
+ });
1112
+ } catch (error: any) {
1113
+ const errorInfo = parseContractError(error);
1114
+
1115
+ // Only skip if it's a contract error (tag doesn't exist is normal)
1116
+ // Re-throw network errors
1117
+ if (errorInfo.isNetworkError) {
1118
+ throw new Error(
1119
+ `Network error fetching Solana wallets: ${errorInfo.message}`
1120
+ );
1121
+ }
1122
+
1123
+ // For contract errors, return empty array (tag doesn't exist is normal)
1124
+ return [];
1125
+ }
1126
+ }
1127
+
1128
+ // ========================================
1129
+ // Tag Management
1130
+ // ========================================
1131
+
1132
+ /**
1133
+ * Create a Tag context for advanced Tag operations
1134
+ * @param fromDomain The domain that defines the Tag (defaults to current domain)
1135
+ * @returns TagContext instance
1136
+ *
1137
+ * @example
1138
+ * // Use current domain as the Tag definer
1139
+ * const tagCtx = domain.tag();
1140
+ *
1141
+ * // Use a specific domain as the Tag definer (cross-domain operations)
1142
+ * const tagCtx = domain.tag('parent.com');
1143
+ */
1144
+ tag(fromDomain: string = this.domainName): TagContext {
1145
+ return new TagContext(this.console, fromDomain);
1146
+ }
1147
+
1148
+ // ========================================
1149
+ // Simplified Tag Operations (High-Level API)
1150
+ // ========================================
1151
+
1152
+ /**
1153
+ * Set the tagger (manager) for a Tag
1154
+ * Only the domain owner can set the tagger
1155
+ *
1156
+ * @param tagName Tag name
1157
+ * @param taggerAddress Address of the tagger (manager)
1158
+ * @returns Transaction result
1159
+ *
1160
+ * @example
1161
+ * // Set a specific address as the tagger
1162
+ * await domain.setTagger('email', '0x1234...');
1163
+ *
1164
+ * // Set zero address to allow anyone to manage
1165
+ * await domain.setTagger('email', '0x0000000000000000000000000000000000000000');
1166
+ */
1167
+ async setTagger(
1168
+ tagName: string,
1169
+ taggerAddress: string
1170
+ ): Promise<TransactionResult> {
1171
+ return await this.tag().setTagger(tagName, taggerAddress);
1172
+ }
1173
+
1174
+ /**
1175
+ * Get the tagger (manager) address for a Tag
1176
+ *
1177
+ * @param tagName Tag name
1178
+ * @returns Tagger address (returns zero address if no tagger is set)
1179
+ *
1180
+ * @example
1181
+ * const tagger = await domain.getTagger('email');
1182
+ * console.log('Tagger address:', tagger);
1183
+ */
1184
+ async getTagger(tagName: string): Promise<string> {
1185
+ return await this.tag().getTagger(tagName);
1186
+ }
1187
+
1188
+ /**
1189
+ * Define a simple Tag type
1190
+ * Only supports common simple types; use tag() for complex types
1191
+ *
1192
+ * @param tagName Tag name
1193
+ * @param type Supported simple type
1194
+ * @returns Transaction result
1195
+ *
1196
+ * @example
1197
+ * await domain.defineSimpleTag('email', 'string');
1198
+ * await domain.defineSimpleTag('age', 'uint8');
1199
+ * await domain.defineSimpleTag('verified', 'bool');
1200
+ * await domain.defineSimpleTag('wallets', 'address[]');
1201
+ */
1202
+ async defineSimpleTag(
1203
+ tagName: string,
1204
+ type:
1205
+ | 'string'
1206
+ | 'address'
1207
+ | 'bool'
1208
+ | 'uint8'
1209
+ | 'uint16'
1210
+ | 'uint32'
1211
+ | 'uint64'
1212
+ | 'uint128'
1213
+ | 'uint256'
1214
+ | 'int8'
1215
+ | 'int16'
1216
+ | 'int32'
1217
+ | 'int64'
1218
+ | 'int128'
1219
+ | 'int256'
1220
+ | 'bytes'
1221
+ | 'bytes32'
1222
+ | 'string[]'
1223
+ | 'address[]'
1224
+ | 'uint256[]'
1225
+ ): Promise<TransactionResult> {
1226
+ const tagCtx = this.tag();
1227
+
1228
+ // Type mapping
1229
+ let tagType;
1230
+ switch (type) {
1231
+ case 'string':
1232
+ tagType = TagTypeBuilder.string();
1233
+ break;
1234
+ case 'address':
1235
+ tagType = TagTypeBuilder.address();
1236
+ break;
1237
+ case 'bool':
1238
+ tagType = TagTypeBuilder.bool();
1239
+ break;
1240
+ case 'uint8':
1241
+ tagType = TagTypeBuilder.uint8();
1242
+ break;
1243
+ case 'uint16':
1244
+ tagType = TagTypeBuilder.uint16();
1245
+ break;
1246
+ case 'uint32':
1247
+ tagType = TagTypeBuilder.uint32();
1248
+ break;
1249
+ case 'uint64':
1250
+ tagType = TagTypeBuilder.uint64();
1251
+ break;
1252
+ case 'uint128':
1253
+ tagType = TagTypeBuilder.uint128();
1254
+ break;
1255
+ case 'uint256':
1256
+ tagType = TagTypeBuilder.uint256();
1257
+ break;
1258
+ case 'int8':
1259
+ tagType = TagTypeBuilder.int8();
1260
+ break;
1261
+ case 'int16':
1262
+ tagType = TagTypeBuilder.int16();
1263
+ break;
1264
+ case 'int32':
1265
+ tagType = TagTypeBuilder.int32();
1266
+ break;
1267
+ case 'int64':
1268
+ tagType = TagTypeBuilder.int64();
1269
+ break;
1270
+ case 'int128':
1271
+ tagType = TagTypeBuilder.int128();
1272
+ break;
1273
+ case 'int256':
1274
+ tagType = TagTypeBuilder.int256();
1275
+ break;
1276
+ case 'bytes':
1277
+ tagType = TagTypeBuilder.bytes();
1278
+ break;
1279
+ case 'bytes32':
1280
+ tagType = TagTypeBuilder.bytes32();
1281
+ break;
1282
+ case 'string[]':
1283
+ tagType = TagTypeBuilder.stringArray();
1284
+ break;
1285
+ case 'address[]':
1286
+ tagType = TagTypeBuilder.addressArray();
1287
+ break;
1288
+ case 'uint256[]':
1289
+ tagType = TagTypeBuilder.uint256Array();
1290
+ break;
1291
+ default:
1292
+ throw new Error(
1293
+ `Unsupported simple type: ${type}. Use tag() for complex types.`
1294
+ );
1295
+ }
1296
+
1297
+ return await tagCtx.defineTag(tagName, tagType);
1298
+ }
1299
+
1300
+ /**
1301
+ * Set a Tag value for the current domain
1302
+ * from = current domain, to = current domain
1303
+ *
1304
+ * @param tagName Tag name
1305
+ * @param value Tag value
1306
+ * @returns Transaction result
1307
+ *
1308
+ * @example
1309
+ * await domain.setTag('email', 'user@example.com');
1310
+ * await domain.setTag('links', ['https://...', 'https://...']);
1311
+ */
1312
+ async setTag(tagName: string, value: any): Promise<TransactionResult> {
1313
+ return await this.tag().setTag(this.domainName, tagName, value);
1314
+ }
1315
+
1316
+ /**
1317
+ * Get a Tag value for the current domain
1318
+ * from = current domain, to = current domain
1319
+ *
1320
+ * @param tagName Tag name
1321
+ * @returns Tag value, or null if not found
1322
+ */
1323
+ async getTag(tagName: string): Promise<any | null> {
1324
+ return await this.tag().getTag(this.domainName, tagName);
1325
+ }
1326
+
1327
+ /**
1328
+ * Remove a Tag from the current domain
1329
+ * from = current domain, to = current domain
1330
+ *
1331
+ * @param tagName Tag name
1332
+ * @returns Transaction result
1333
+ */
1334
+ async removeTag(tagName: string): Promise<TransactionResult> {
1335
+ return await this.tag().removeTag(this.domainName, tagName);
1336
+ }
1337
+
1338
+ /**
1339
+ * Get all Tags for the current domain
1340
+ * @returns Array of Tags with name and value
1341
+ */
1342
+ async getAllTags(): Promise<Array<{ name: string; value: any }>> {
1343
+ return await this.tag().getAllTags(this.domainName);
1344
+ }
1345
+
1346
+ /**
1347
+ * Get all Tag type names defined by the current domain
1348
+ * @returns Array of Tag names
1349
+ */
1350
+ async getDefinedTags(): Promise<string[]> {
1351
+ return await this.tag().getDefinedTagNames();
1352
+ }
1353
+ }
1354
+
1355
+ /*
1356
+ * Helper function to convert IPv4 string to bytes4 format
1357
+ * @param ipv4 - IPv4 address string (e.g., "192.168.1.1")
1358
+ * @returns bytes4 format as hex string with '0x' prefix
1359
+ */
1360
+ export function ipv4ToBytes4(ipv4: string): string {
1361
+ // Validate IPv4 format
1362
+ const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
1363
+ const match = ipv4.match(ipv4Regex);
1364
+
1365
+ if (!match) {
1366
+ throw new Error('Invalid IPv4 address format');
1367
+ }
1368
+
1369
+ // Parse and validate each octet (0-255)
1370
+ const octets = match.slice(1, 5).map((octet) => {
1371
+ const num = parseInt(octet, 10);
1372
+ if (num < 0 || num > 255) {
1373
+ throw new Error(
1374
+ `Invalid IPv4 octet: ${octet}. Must be between 0 and 255`
1375
+ );
1376
+ }
1377
+ return num;
1378
+ });
1379
+
1380
+ // Convert to hex bytes4 format
1381
+ const hexString = octets
1382
+ .map((octet) => octet.toString(16).padStart(2, '0'))
1383
+ .join('');
1384
+
1385
+ return '0x' + hexString;
1386
+ }
1387
+
1388
+ /*
1389
+ * Helper function to convert bytes4 format to IPv4 string
1390
+ * @param bytes4Hex - bytes4 format as hex string (with or without '0x' prefix)
1391
+ * @returns IPv4 address string (e.g., "192.168.1.1")
1392
+ */
1393
+ export function bytes4ToIpv4(bytes4Hex: string): string {
1394
+ // Remove '0x' prefix if present
1395
+ const hexString = bytes4Hex.startsWith('0x')
1396
+ ? bytes4Hex.slice(2)
1397
+ : bytes4Hex;
1398
+
1399
+ // Validate hex string length (should be 8 characters for 4 bytes)
1400
+ if (hexString.length !== 8) {
1401
+ throw new Error(
1402
+ `Invalid bytes4 format: expected 8 hex characters, got ${hexString.length}`
1403
+ );
1404
+ }
1405
+
1406
+ // Convert each pair of hex characters to decimal
1407
+ const octets = [];
1408
+ for (let i = 0; i < 8; i += 2) {
1409
+ const octet = parseInt(hexString.slice(i, i + 2), 16);
1410
+ octets.push(octet);
1411
+ }
1412
+
1413
+ return octets.join('.');
1414
+ }
1415
+
1416
+ /*
1417
+ * Helper function to convert PEM format to DER hex format
1418
+ * @param pem - PEM formatted string (with BEGIN/END markers)
1419
+ * @returns DER format as hex string with '0x' prefix
1420
+ */
1421
+ export function pemToDer(pem: string): string {
1422
+ // Remove PEM headers, footers, and whitespace
1423
+ const base64 = pem
1424
+ .replace(/-----BEGIN.*?-----/g, '')
1425
+ .replace(/-----END.*?-----/g, '')
1426
+ .replace(/\s/g, '');
1427
+
1428
+ // Convert base64 to hex
1429
+ let hexString: string;
1430
+
1431
+ // Check if running in Node.js or browser
1432
+ if (typeof Buffer !== 'undefined') {
1433
+ // Node.js environment
1434
+ const derBuffer = Buffer.from(base64, 'base64');
1435
+ hexString = derBuffer.toString('hex');
1436
+ } else {
1437
+ // Browser environment
1438
+ const binaryString = atob(base64);
1439
+ hexString = Array.from(binaryString, (char) =>
1440
+ char.charCodeAt(0).toString(16).padStart(2, '0')
1441
+ ).join('');
1442
+ }
1443
+
1444
+ // Convert to hex string with 0x prefix
1445
+ return '0x' + hexString;
1446
+ }
1447
+
1448
+ /*
1449
+ * Helper function to convert DER hex format to PEM PKCS#8 format
1450
+ * @param derHex - DER format as hex string (with or without '0x' prefix)
1451
+ * @returns PEM formatted public key string
1452
+ */
1453
+ export function derToPem(derHex: string): string {
1454
+ // Remove '0x' prefix if present
1455
+ const hexString = derHex.startsWith('0x') ? derHex.slice(2) : derHex;
1456
+
1457
+ // Convert hex to base64
1458
+ let base64: string;
1459
+
1460
+ // Check if running in Node.js or browser
1461
+ if (typeof Buffer !== 'undefined') {
1462
+ // Node.js environment
1463
+ const derBuffer = Buffer.from(hexString, 'hex');
1464
+ base64 = derBuffer.toString('base64');
1465
+ } else {
1466
+ // Browser environment
1467
+ // Convert hex string to byte array
1468
+ const bytes = new Uint8Array(hexString.length / 2);
1469
+ for (let i = 0; i < hexString.length; i += 2) {
1470
+ bytes[i / 2] = parseInt(hexString.substr(i, 2), 16);
1471
+ }
1472
+ // Convert byte array to binary string
1473
+ const binaryString = String.fromCharCode(...bytes);
1474
+ // Convert to base64
1475
+ base64 = btoa(binaryString);
1476
+ }
1477
+
1478
+ // Split base64 into 64-character lines for PEM format
1479
+ const lines: string[] = [];
1480
+ for (let i = 0; i < base64.length; i += 64) {
1481
+ lines.push(base64.slice(i, i + 64));
1482
+ }
1483
+
1484
+ // Construct PEM format with headers and footers
1485
+ return (
1486
+ '-----BEGIN PUBLIC KEY-----\n' +
1487
+ lines.join('\n') +
1488
+ '\n-----END PUBLIC KEY-----'
1489
+ );
1490
+ }