@@ -42,6 +42,8 @@ import {
42
42
pubKeyfromPrivKey ,
43
43
createStacksPrivateKey ,
44
44
AnchorMode ,
45
+ signWithKey ,
46
+ compressPublicKey ,
45
47
} from '@stacks/transactions' ;
46
48
import { buildPreorderNameTx , buildRegisterNameTx } from '@stacks/bns' ;
47
49
import { StacksMainnet , StacksTestnet } from '@stacks/network' ;
@@ -109,10 +111,17 @@ import {
109
111
ClarityFunctionArg ,
110
112
generateExplorerTxPageUrl ,
111
113
isTestnetAddress ,
114
+ subdomainOpToZFPieces ,
115
+ SubdomainOp ,
112
116
} from './utils' ;
113
117
114
118
import { handleAuth , handleSignIn } from './auth' ;
115
- import { generateNewAccount , generateWallet , getAppPrivateKey } from '@stacks/wallet-sdk' ;
119
+ import {
120
+ generateNewAccount ,
121
+ generateWallet ,
122
+ getAppPrivateKey ,
123
+ restoreWalletAccounts ,
124
+ } from '@stacks/wallet-sdk' ;
116
125
import { getMaxIDSearchIndex , setMaxIDSearchIndex , getPrivateKeyAddress } from './common' ;
117
126
// global CLI options
118
127
let txOnly = false ;
@@ -317,6 +326,163 @@ async function getStacksWalletKey(network: CLINetworkAdapter, args: string[]): P
317
326
return JSONStringify ( keyInfo ) ;
318
327
}
319
328
329
+ /**
330
+ * Enable users to transfer subdomains to wallet-key addresses that correspond to all data-key addresses
331
+ * Reference: https://github.com/hirosystems/stacks.js/issues/1209
332
+ * args:
333
+ * @mnemonic (string) the 12-word phrase to retrieve the privateKey & address
334
+ */
335
+ async function migrateSubdomains ( network : CLINetworkAdapter , args : string [ ] ) : Promise < string > {
336
+ // Decrypt the mnemonic input given by user
337
+ const mnemonic : string = await getBackupPhrase ( args [ 0 ] ) ; // args[0] is the cli argument for mnemonic
338
+ // Get baseWallet from mnemonic given by user
339
+ const baseWallet = await generateWallet ( { secretKey : mnemonic , password : '' } ) ;
340
+ // Find network type to find user accounts
341
+ const _network = network . isMainnet ( ) ? new StacksMainnet ( ) : new StacksTestnet ( ) ;
342
+ // fetch list of accounts stored in a json file on Gaia
343
+ const wallet = await restoreWalletAccounts ( {
344
+ wallet : baseWallet ,
345
+ gaiaHubUrl : 'https://hub.blockstack.org' ,
346
+ network : _network ,
347
+ } ) ;
348
+ // Inform user about retrieved accounts
349
+ console . log ( `Accounts found: ${ wallet . accounts . length } ` ) ;
350
+ // Use this status instance and return at the end of command completion
351
+ const status = { found : 0 , migrated : 0 , collisions : 0 , pending : 0 } ;
352
+ // Loop over accounts and check if there are any subdomain linked with data key address
353
+ for ( let i = 0 ; i < wallet . accounts . length ; i ++ ) {
354
+ const account = wallet . accounts [ i ] ;
355
+ console . log ( 'Index: ' , i , 'Account: ' , account ) ;
356
+ // Get data key address from data private key
357
+ const dataKeyAddress = getAddressFromPrivateKey (
358
+ account . dataPrivateKey ,
359
+ network . isMainnet ( ) ? TransactionVersion . Mainnet : TransactionVersion . Testnet
360
+ ) ;
361
+ // Get wallet key address from stx private key
362
+ const walletKeyAddress = getAddressFromPrivateKey (
363
+ account . stxPrivateKey ,
364
+ network . isMainnet ( ) ? TransactionVersion . Mainnet : TransactionVersion . Testnet
365
+ ) ;
366
+ // Find subdomains at dataKeyAddress for the account in iteration
367
+ console . log ( 'Finding subdomains at data key address: ' , dataKeyAddress ) ;
368
+ const namesResponse = await fetch (
369
+ `${ _network . coreApiUrl } /v1/addresses/stacks/${ dataKeyAddress } `
370
+ ) ;
371
+ const namesJson = await namesResponse . json ( ) ;
372
+
373
+ const regExp = / ( \. .* ) { 2 , } / ; // regex to verify two dots in subdomain
374
+
375
+ if ( ( namesJson . names . length || 0 ) > 0 ) {
376
+ // Filter only subdomains
377
+ const subDomains = namesJson . names . filter ( ( val : string ) => regExp . test ( val ) ) ;
378
+
379
+ if ( subDomains . length === 0 ) console . log ( 'No subdomains found at: ' , dataKeyAddress ) ;
380
+
381
+ for ( const subDomain of subDomains ) {
382
+ status . found ++ ;
383
+ // Alerts the user to any subdomains that can't be migrated to these wallet-key-derived addresses
384
+ // Given collision with existing usernames owned by them
385
+ const namesResponse = await fetch (
386
+ `${ _network . coreApiUrl } /v1/addresses/stacks/${ walletKeyAddress } `
387
+ ) ;
388
+ const existingNames = await namesResponse . json ( ) ;
389
+ if ( existingNames . names . indexOf ( subDomain ) >= 0 ) {
390
+ status . collisions ++ ;
391
+ console . log ( `collision: ${ subDomain } already exists in wallet key address.` ) ;
392
+ continue ;
393
+ }
394
+ // validate user owns the subdomain
395
+ const nameInfo = await fetch ( `${ _network . coreApiUrl } /v1/names/${ subDomain } ` ) ;
396
+ const nameInfoJson = await nameInfo . json ( ) ;
397
+ console . log ( 'subdomain info: ' , nameInfoJson ) ;
398
+ if ( nameInfoJson . address !== dataKeyAddress ) {
399
+ console . log ( `Error: Only owner of ${ subDomain } can execute this migration` ) ;
400
+ continue ; // Skip and move to next subdomain in iteration
401
+ }
402
+ // Remove . char in prompt name to avoid nested prompt response
403
+ const promptName = subDomain . replaceAll ( '.' , '_' ) ;
404
+ // Prompt user if he want the subdomain migration
405
+ const confirm : { [ promptName : string ] : string } = await prompt ( [
406
+ {
407
+ name : promptName ,
408
+ message : `Do you want to migrate the domain: ${ subDomain } ` ,
409
+ type : 'confirm' ,
410
+ } ,
411
+ ] ) ;
412
+ // If no, then move to next account
413
+ if ( ! confirm [ promptName ] ) continue ;
414
+
415
+ // Generate Subdomain Operation payload starting with signature
416
+
417
+ const subDomainOp : SubdomainOp = {
418
+ subdomainName : subDomain , // Subdomain name
419
+ owner : walletKeyAddress , // New owner address / wallet key address
420
+ zonefile : nameInfoJson . zonefile , // Old copy of zonefile at data key address
421
+ sequenceNumber : 1 ,
422
+ // It should be (old sequence number + 1) but cannot find old sequence number so assuming 1. Api should calc it again.
423
+ } ;
424
+
425
+ const subdomainPieces = subdomainOpToZFPieces ( subDomainOp ) ;
426
+
427
+ const textToSign = subdomainPieces . txt . join ( ',' ) ;
428
+
429
+ // Generate signature: https://docs.stacks.co/build-apps/references/bns#subdomain-lifecycle
430
+ /**
431
+ * *********************** IMPORTANT **********************************************
432
+ * If the subdomain owner wants to change the address of their subdomain, *
433
+ * they need to sign a subdomain-transfer operation and *
434
+ * give it to the on-chain name owner who created the subdomain. *
435
+ * They then package it into a zone file and broadcast it. *
436
+ * *********************** IMPORTANT **********************************************
437
+ * subdomain operation will only be accepted if it has a later "sequence=" number,*
438
+ * and a valid signature in "sig=" over the transaction body .The "sig=" field *
439
+ * includes both the public key and signature, and the public key must hash to *
440
+ * the previous subdomain operation's "addr=" field *
441
+ * ********************************************************************************
442
+ */
443
+
444
+ const hash = crypto . createHash ( 'sha256' ) . update ( textToSign ) . digest ( 'hex' ) ;
445
+ const sig = signWithKey ( createStacksPrivateKey ( account . dataPrivateKey ) , hash ) ;
446
+ // https://docs.stacks.co/build-apps/references/bns#subdomain-lifecycle
447
+ subDomainOp . signature = [
448
+ sig . data ,
449
+ compressPublicKey (
450
+ publicKeyToString ( pubKeyfromPrivKey ( account . dataPrivateKey ) )
451
+ ) . data . toString ( 'hex' ) ,
452
+ ] . join ( ',' ) ;
453
+ console . log ( 'SubDomain Operation payload' , subDomainOp ) ;
454
+ // Todo: // This is a supposed migration api request
455
+ const options = {
456
+ method : 'POST' ,
457
+ headers : { 'Content-Type' : 'application/json' } ,
458
+ body : JSON . stringify ( subDomainOp ) ,
459
+ } ;
460
+
461
+ const migrationURL = 'https://registrar.stacks.co/migrate-subdomain' ; // Todo: // Required API call
462
+
463
+ const response = await fetch ( migrationURL , options ) ;
464
+
465
+ if ( response . ok ) {
466
+ const migrationStatus = await response . json ( ) ;
467
+ status . migrated ++ ;
468
+ console . log ( migrationStatus ) ;
469
+ console . log (
470
+ 'Transaction will take some time to complete. Check transaction explorer url to verify the status of migration'
471
+ ) ;
472
+ // Continue to next subDomain or address in loop
473
+ } else {
474
+ status . pending ++ ;
475
+ console . log ( response ) ;
476
+ throw Error ( `Failed to migrate: ${ response . statusText } ` ) ;
477
+ }
478
+ }
479
+ } else {
480
+ console . log ( 'No subdomains found at: ' , dataKeyAddress ) ;
481
+ }
482
+ }
483
+ return JSONStringify ( status ) ;
484
+ }
485
+
320
486
/*
321
487
* Make a private key and output it
322
488
* args:
@@ -1836,6 +2002,7 @@ const COMMANDS: Record<string, CommandFunction> = {
1836
2002
tx_preorder : preorder ,
1837
2003
send_tokens : sendTokens ,
1838
2004
stack : stack ,
2005
+ migrate_subdomains : migrateSubdomains ,
1839
2006
stacking_status : stackingStatus ,
1840
2007
faucet : faucetCall ,
1841
2008
} ;
@@ -2013,5 +2180,6 @@ export const testables =
2013
2180
getStacksWalletKey,
2014
2181
register,
2015
2182
preorder,
2183
+ migrateSubdomains,
2016
2184
}
2017
2185
: undefined ;
0 commit comments