@beclab/olaresid 0.1.1 → 0.1.3

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