傳送門
copay錢包(1.windows環(huán)境編譯運(yùn)行)
copay錢包(2.RestfulAPI初步分析)
copay錢包(3.轉(zhuǎn)賬功能報(bào)文分析)
copay錢包(4.bitcore-lib與bitcore-wallet-client類庫(kù)修改)
copay錢包(5.助記詞導(dǎo)出導(dǎo)入代碼閱讀)
導(dǎo)出入口點(diǎn)
程序中,有4個(gè)地方可以導(dǎo)出助記詞:
- 在最開始創(chuàng)建錢包時(shí)(BackupRequestPage)
- 在主頁(yè)查看交易明細(xì)中(WalletDetailsPage)
-
在設(shè)置頁(yè)面的錢包詳情中(WalletSettingsPage)
- 在接收到Bitcoin時(shí)(ReceivePage)
其中,除了setting的備份外,其他的<div>都有用 *ngIf="wallet.needsBackup" 判斷,只要備份過一次就不會(huì)再出現(xiàn)了.
調(diào)用的形式如下:
this.navCtrl.push(BackupWarningPage, { walletId: this.walletId, fromOnboarding: true });
向BackupWarningPage傳遞錢包的id和是否從啟動(dòng)頁(yè)進(jìn)入(僅第1種為true).
導(dǎo)出BackupGamePage處理流程
導(dǎo)出流程的核心是BackupGamePage,Backup目錄的其他頁(yè)面都是提示性的,
1.頁(yè)面構(gòu)造函數(shù)
// 從navCtrl讀取walletId參數(shù)
this.walletId = this.navParams.get('walletId');
// 從navCtrl讀取fromOnboarding參數(shù)
this.fromOnboarding = this.navParams.get('fromOnboarding');
// 根據(jù)walletId,獲取到當(dāng)前需要備份的wallet對(duì)象-實(shí)際可用直接把wallet傳進(jìn)來,無需在獲取一次.
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;
}
// 調(diào)用walletProvider服務(wù)獲取到助記詞列表(本身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();
});
- 流程控制函數(shù)
// 流程控制函數(shù)
private setFlow(): void {
if (!this.keys) return;
// words為助記詞
let words = this.keys.mnemonic;
// profile的助記詞是用\u3000(unicode的空格)分割的.split后就變?yōu)閿?shù)組了.
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);
// 如果是第二頁(yè),就回退(說明輸錯(cuò)了)
if (this.currentIndex == 2) this.slidePrev();
}
- 判斷助記詞游戲是否輸入正確
// 判斷助記詞游戲結(jié)果,返回值是一個(gè)promise
private confirm(): Promise<any> {
return new Promise((resolve, reject) => {
this.error = false;
// 把輸入框的字配在一起.
let customWordList = _.map(this.customWords, 'word');
// 判斷是否和記錄的助記詞一致,
if (!_.isEqual(this.mnemonicWords, customWordList)) {
// 調(diào)用reject()
return reject('Mnemonic string mismatch');
}
// 把備份信息登記到profile中
this.profileProvider.setBackupFlag(this.wallet.credentials.walletId);
return resolve();
});
}
如圖這個(gè)備份信息并不是保存在credentials中,而是在profile的外面,根據(jù)backup-(錢包ID)鍵值保存.:
confirm()和setFlow()是由finalStep統(tǒng)一控制進(jìn)行的,如果confirm成功,就轉(zhuǎn)到BackupReadyModalPage完成備份;如果confirm不成功,就轉(zhuǎn)到setFlow(),重新開始游戲.
總的來說,copay的助記詞導(dǎo)出,還是比較方便的,有適當(dāng)?shù)奶崾?然后還有一個(gè)隨機(jī)校驗(yàn)去驗(yàn)證是否真的抄下來了,并且能有效的提示和記錄導(dǎo)出數(shù)據(jù)的結(jié)果,確實(shí)值得借鑒的,
導(dǎo)入入口點(diǎn)
程序中,有2個(gè)地方可以導(dǎo)入助記詞,使用已有錢包:
- 在最開始創(chuàng)建錢包時(shí)(此時(shí)還是更多導(dǎo)入形式,如多簽錢包)
- 在主頁(yè)錢包列表右上角
調(diào)用的形式如下:
this.navCtrl.push(ImportWalletPage, { fromOnboarding: true });
向BackupWarningPage是否從啟動(dòng)頁(yè)進(jìn)入(僅第1種為true).
導(dǎo)入ImportWalletPage處理流程
實(shí)際上,有words和file兩張導(dǎo)入方式,但是導(dǎo)出并沒有file呢,那這個(gè)應(yīng)該是導(dǎo)入其他設(shè)備(如TREZOR)生成的文件.如果只是copay的使用,關(guān)注word方式即可.而代碼中,因?yàn)樾枰嫒?種方式,增加了大量的判斷分支,閱讀起來就比較累了.
- 從助記詞導(dǎo)入
// 從助記詞導(dǎo)入.
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 = {};
// 從頁(yè)面中讀取bwsURL信息,保存到opts中.
if (this.importForm.value.bwsURL)
opts.bwsurl = this.importForm.value.bwsURL;
// 從頁(yè)面中讀取pathData信息,livenet為m/44'/0'/0',testnet為m/44'/1'/0',并調(diào)用derivationPathHelperProvider進(jìn)行解析.
let pathData: any = this.derivationPathHelperProvider.parse(this.importForm.value.derivationPath);
// 判斷解析后的衍生路徑,其值是必須的,如果沒有值,就報(bào)錯(cuò)
if (!pathData) {
let title = this.translate.instant('Error');
let subtitle = this.translate.instant('Invalid derivation path');
this.popupProvider.ionicAlert(title, subtitle);
return;
}
// 從衍生路徑中獲取賬號(hào),網(wǎng)絡(luò),策略等信息
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;
// 可以直接導(dǎo)入私鑰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]+/);
// 初步判斷一下是否長(zhǎng)度符合.
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;
// 再次調(diào)用importMnemonic完成導(dǎo)入
this.importMnemonic(words, opts);
}
2.具體調(diào)入代碼
// 具體調(diào)入代碼
private importMnemonic(words: string, opts: any): void {
// 顯示等待框
this.onGoingProcessProvider.set('importingWallet');
// 用異步任務(wù)完成
setTimeout(() => {
// 實(shí)際是調(diào)用了profileProvider的importMnemonic()進(jìn)行具體導(dǎo)入,其內(nèi)部是通過walletClient客戶端與bws服務(wù)通訊完成的導(dǎo)入.
this.profileProvider.importMnemonic(words, opts).then((wallet: any) => {
this.onGoingProcessProvider.clear();
// 調(diào)用finish()函數(shù)
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);
}