copay钱包(5.助记词导出导入代码阅读)
传送门
copay钱包(1.windows环境编译运行)
copay钱包(2.RestfulAPI初步分析)
copay钱包(3.转账功能报文分析)
copay钱包(4.bitcore-lib与bitcore-wallet-client类库修改)
copay钱包(5.助记词导出导入代码阅读)
导出入口点
程序中,有4个地方可以导出助记词:
- 在最开始创建钱包时(BackupRequestPage)
- 在主页查看交易明细中(WalletDetailsPage)
-
在设置页面的钱包详情中(WalletSettingsPage)
setting-backup.png - 在接收到Bitcoin时(ReceivePage)
其中,除了setting的备份外,其他的<div>都有用 *ngIf="wallet.needsBackup" 判断,只要备份过一次就不会再出现了.
调用的形式如下:
this.navCtrl.push(BackupWarningPage, { walletId: this.walletId, fromOnboarding: true });
向BackupWarningPage传递钱包的id和是否从启动页进入(仅第1种为true).
导出BackupGamePage处理流程
导出流程的核心是BackupGamePage,Backup目录的其他页面都是提示性的,
1.页面构造函数
// 从navCtrl读取walletId参数
this.walletId = this.navParams.get('walletId');
// 从navCtrl读取fromOnboarding参数
this.fromOnboarding = this.navParams.get('fromOnboarding');
// 根据walletId,获取到当前需要备份的wallet对象-实际可用直接把wallet传进来,无需在获取一次.
this.wallet = this.profileProvider.getWallet(this.walletId);
// 判断该credential是否有加密过
this.credentialsEncrypted = this.wallet.isPrivKeyEncrypted();
// 判断是否手工清除过助记词?profile编辑?
this.deleted = this.isDeletedSeed();
if (this.deleted) {
this.logger.debug('no mnemonics');
return;
}
// 调用walletProvider服务获取到助记词列表(本身profile里面就有,但是这里还是用了异步方法.)
this.walletProvider.getKeys(this.wallet).then((keys) => {
if (_.isEmpty(keys)) {
this.logger.error('Empty keys');
}
// 能获取到说明没有加密
this.credentialsEncrypted = false;
// keys包含2部分,助记词和privateKey
this.keys = keys;
// 流程控制.
this.setFlow();
}).catch((err) => {
this.logger.error('Could not get keys: ', err);
this.navCtrl.pop();
});
- 流程控制函数
// 流程控制函数
private setFlow(): void {
if (!this.keys) return;
// words为助记词
let words = this.keys.mnemonic;
// profile的助记词是用\u3000(unicode的空格)分割的.split后就变为数组了.
this.mnemonicWords = words.split(/[\u3000\s]+/);
// 打乱一下顺序.
this.shuffledMnemonicWords = this.shuffledWords(this.mnemonicWords);
// 始终为false
this.mnemonicHasPassphrase = this.wallet.mnemonicHasPassphrase();
this.useIdeograms = words.indexOf("\u3000") >= 0;
this.password = '';
this.customWords = [];
this.selectComplete = false;
this.error = false;
// 把words擦掉,避免泄露
words = _.repeat('x', 300);
// 如果是第二页,就回退(说明输错了)
if (this.currentIndex == 2) this.slidePrev();
}
- 判断助记词游戏是否输入正确
// 判断助记词游戏结果,返回值是一个promise
private confirm(): Promise<any> {
return new Promise((resolve, reject) => {
this.error = false;
// 把输入框的字配在一起.
let customWordList = _.map(this.customWords, 'word');
// 判断是否和记录的助记词一致,
if (!_.isEqual(this.mnemonicWords, customWordList)) {
// 调用reject()
return reject('Mnemonic string mismatch');
}
// 把备份信息登记到profile中
this.profileProvider.setBackupFlag(this.wallet.credentials.walletId);
return resolve();
});
}
如图这个备份信息并不是保存在credentials中,而是在profile的外面,根据backup-(钱包ID)键值保存.:
backup-profile.png
confirm()和setFlow()是由finalStep统一控制进行的,如果confirm成功,就转到BackupReadyModalPage完成备份;如果confirm不成功,就转到setFlow(),重新开始游戏.
总的来说,copay的助记词导出,还是比较方便的,有适当的提示,然后还有一个随机校验去验证是否真的抄下来了,并且能有效的提示和记录导出数据的结果,确实值得借鉴的,
导入入口点
程序中,有2个地方可以导入助记词,使用已有钱包:
- 在最开始创建钱包时(此时还是更多导入形式,如多签钱包)
- 在主页钱包列表右上角
调用的形式如下:
this.navCtrl.push(ImportWalletPage, { fromOnboarding: true });
向BackupWarningPage是否从启动页进入(仅第1种为true).
导入ImportWalletPage处理流程
实际上,有words和file两张导入方式,但是导出并没有file呢,那这个应该是导入其他设备(如TREZOR)生成的文件.如果只是copay的使用,关注word方式即可.而代码中,因为需要兼容2种方式,增加了大量的判断分支,阅读起来就比较累了.
- 从助记词导入
// 从助记词导入.
public importFromMnemonic(): void {
// 判断是否合法
if (!this.importForm.valid) {
let title = this.translate.instant('Error');
let subtitle = this.translate.instant('There is an error in the form');
this.popupProvider.ionicAlert(title, subtitle);
return;
}
let opts: any = {};
// 从页面中读取bwsURL信息,保存到opts中.
if (this.importForm.value.bwsURL)
opts.bwsurl = this.importForm.value.bwsURL;
// 从页面中读取pathData信息,livenet为m/44'/0'/0',testnet为m/44'/1'/0',并调用derivationPathHelperProvider进行解析.
let pathData: any = this.derivationPathHelperProvider.parse(this.importForm.value.derivationPath);
// 判断解析后的衍生路径,其值是必须的,如果没有值,就报错
if (!pathData) {
let title = this.translate.instant('Error');
let subtitle = this.translate.instant('Invalid derivation path');
this.popupProvider.ionicAlert(title, subtitle);
return;
}
// 从衍生路径中获取账号,网络,策略等信息
opts.account = pathData.account;
opts.networkName = pathData.networkName;
opts.derivationStrategy = pathData.derivationStrategy;
// 币种
opts.coin = this.importForm.value.coin;
// 解析输入的助记词
let words: string = this.importForm.value.words || null;
if (!words) {
let title = this.translate.instant('Error');
let subtitle = this.translate.instant('Please enter the recovery phrase');
this.popupProvider.ionicAlert(title, subtitle);
return;
// 可以直接导入私钥xprv开头(livnet),tprv开头(testnet).
} else if (words.indexOf('xprv') == 0 || words.indexOf('tprv') == 0) {
return this.importExtendedPrivateKey(words, opts);
} else {
let wordList: any[] = words.split(/[\u3000\s]+/);
// 初步判断一下是否长度符合.
if ((wordList.length % 3) != 0) {
let title = this.translate.instant('Error');
let subtitle = this.translate.instant('Wrong number of recovery words:');
this.popupProvider.ionicAlert(title, subtitle + ' ' + wordList.length);
return;
}
}
opts.passphrase = this.importForm.value.passphrase || null;
// 再次调用importMnemonic完成导入
this.importMnemonic(words, opts);
}
2.具体调入代码
// 具体调入代码
private importMnemonic(words: string, opts: any): void {
// 显示等待框
this.onGoingProcessProvider.set('importingWallet');
// 用异步任务完成
setTimeout(() => {
// 实际是调用了profileProvider的importMnemonic()进行具体导入,其内部是通过walletClient客户端与bws服务通讯完成的导入.
this.profileProvider.importMnemonic(words, opts).then((wallet: any) => {
this.onGoingProcessProvider.clear();
// 调用finish()函数
this.finish(wallet);
}).catch((err: any) => {
// 捕捉异常状况
if (err instanceof this.errors.NOT_AUTHORIZED) {
this.importErr = true;
} else {
let title = this.translate.instant('Error');
this.popupProvider.ionicAlert(title, err);
}
// 异常情况要清理等待框,
this.onGoingProcessProvider.clear();
return;
});
}, 100);
}