@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.
- package/CLI.md +1300 -0
- package/README.md +40 -31
- package/TAG.md +589 -0
- package/dist/abi/RootResolver2ABI.d.ts +54 -0
- package/dist/abi/RootResolver2ABI.d.ts.map +1 -0
- package/dist/abi/RootResolver2ABI.js +240 -0
- package/dist/abi/RootResolver2ABI.js.map +1 -0
- package/dist/business/index.d.ts +302 -0
- package/dist/business/index.d.ts.map +1 -0
- package/dist/business/index.js +1211 -0
- package/dist/business/index.js.map +1 -0
- package/dist/business/tag-context.d.ts +219 -0
- package/dist/business/tag-context.d.ts.map +1 -0
- package/dist/business/tag-context.js +560 -0
- package/dist/business/tag-context.js.map +1 -0
- package/dist/cli.js +2102 -39
- package/dist/cli.js.map +1 -1
- package/dist/debug.d.ts.map +1 -1
- package/dist/debug.js +14 -2
- package/dist/debug.js.map +1 -1
- package/dist/index.d.ts +51 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +241 -12
- package/dist/index.js.map +1 -1
- package/dist/utils/crypto-utils.d.ts +130 -0
- package/dist/utils/crypto-utils.d.ts.map +1 -0
- package/dist/utils/crypto-utils.js +402 -0
- package/dist/utils/crypto-utils.js.map +1 -0
- package/dist/utils/error-parser.d.ts +35 -0
- package/dist/utils/error-parser.d.ts.map +1 -0
- package/dist/utils/error-parser.js +202 -0
- package/dist/utils/error-parser.js.map +1 -0
- package/dist/utils/olares-id.d.ts +36 -0
- package/dist/utils/olares-id.d.ts.map +1 -0
- package/dist/utils/olares-id.js +52 -0
- package/dist/utils/olares-id.js.map +1 -0
- package/dist/utils/tag-abi-codec.d.ts +69 -0
- package/dist/utils/tag-abi-codec.d.ts.map +1 -0
- package/dist/utils/tag-abi-codec.js +144 -0
- package/dist/utils/tag-abi-codec.js.map +1 -0
- package/dist/utils/tag-type-builder.d.ts +158 -0
- package/dist/utils/tag-type-builder.d.ts.map +1 -0
- package/dist/utils/tag-type-builder.js +410 -0
- package/dist/utils/tag-type-builder.js.map +1 -0
- package/examples/crypto-utilities.ts +140 -0
- package/examples/domain-context.ts +80 -0
- package/examples/generate-mnemonic.ts +149 -0
- package/examples/index.ts +1 -1
- package/examples/ip.ts +171 -0
- package/examples/legacy.ts +10 -10
- package/examples/list-wallets.ts +81 -0
- package/examples/olares-id-format.ts +197 -0
- package/examples/quasar-demo/.eslintrc.js +23 -0
- package/examples/quasar-demo/.quasar/app.js +43 -0
- package/examples/quasar-demo/.quasar/client-entry.js +38 -0
- package/examples/quasar-demo/.quasar/client-prefetch.js +130 -0
- package/examples/quasar-demo/.quasar/quasar-user-options.js +16 -0
- package/examples/quasar-demo/README.md +49 -0
- package/examples/quasar-demo/index.html +11 -0
- package/examples/quasar-demo/package-lock.json +6407 -0
- package/examples/quasar-demo/package.json +36 -0
- package/examples/quasar-demo/quasar.config.js +73 -0
- package/examples/quasar-demo/src/App.vue +13 -0
- package/examples/quasar-demo/src/css/app.scss +1 -0
- package/examples/quasar-demo/src/layouts/MainLayout.vue +21 -0
- package/examples/quasar-demo/src/pages/IndexPage.vue +905 -0
- package/examples/quasar-demo/src/router/index.ts +25 -0
- package/examples/quasar-demo/src/router/routes.ts +11 -0
- package/examples/quasar-demo/tsconfig.json +28 -0
- package/examples/register-subdomain.ts +152 -0
- package/examples/rsa-keypair.ts +148 -0
- package/examples/tag-builder.ts +235 -0
- package/examples/tag-management.ts +534 -0
- package/examples/tag-nested-tuple.ts +190 -0
- package/examples/tag-simple.ts +149 -0
- package/examples/tag-tagger.ts +217 -0
- package/examples/test-nested-tuple-conversion.ts +143 -0
- package/examples/test-type-bytes-parser.ts +70 -0
- package/examples/transfer-domain.ts +197 -0
- package/examples/wallet-management.ts +196 -0
- package/package.json +24 -15
- package/src/abi/RootResolver2ABI.ts +237 -0
- package/src/business/index.ts +1492 -0
- package/src/business/tag-context.ts +747 -0
- package/src/cli.ts +2772 -39
- package/src/debug.ts +17 -2
- package/src/index.ts +313 -17
- package/src/utils/crypto-utils.ts +459 -0
- package/src/utils/error-parser.ts +225 -0
- package/src/utils/olares-id.ts +49 -0
- package/src/utils/tag-abi-codec.ts +158 -0
- package/src/utils/tag-type-builder.ts +469 -0
- 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
|
+
}
|