mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-22 23:08:07 +01:00
ADD: support multiple accounts from Keystone hw wallet (closes #5281)
This commit is contained in:
parent
eac4c1c174
commit
c23dbfec5d
4 changed files with 129 additions and 32 deletions
|
@ -228,41 +228,58 @@ class BlueURDecoder extends URDecoder {
|
|||
if (decoded.type === 'crypto-account') {
|
||||
const cryptoAccount = CryptoAccount.fromCBOR(decoded.cbor);
|
||||
|
||||
// now, crafting zpub out of data we have
|
||||
const hdKey = cryptoAccount.outputDescriptors[0].getCryptoKey();
|
||||
const derivationPath = 'm/' + hdKey.getOrigin().getPath();
|
||||
const script = cryptoAccount.outputDescriptors[0].getScriptExpressions()[0].getExpression();
|
||||
const isMultisig =
|
||||
script === ScriptExpressions.WITNESS_SCRIPT_HASH.getExpression() ||
|
||||
// fallback to paths (unreliable).
|
||||
// dont know how to add ms p2sh (legacy) or p2sh-p2wsh (wrapped segwit) atm
|
||||
derivationPath === MultisigHDWallet.PATH_LEGACY ||
|
||||
derivationPath === MultisigHDWallet.PATH_WRAPPED_SEGWIT ||
|
||||
derivationPath === MultisigHDWallet.PATH_NATIVE_SEGWIT;
|
||||
const version = Buffer.from(isMultisig ? '02aa7ed3' : '04b24746', 'hex');
|
||||
const parentFingerprint = hdKey.getParentFingerprint();
|
||||
const depth = hdKey.getOrigin().getDepth();
|
||||
const depthBuf = Buffer.alloc(1);
|
||||
depthBuf.writeUInt8(depth);
|
||||
const components = hdKey.getOrigin().getComponents();
|
||||
const lastComponents = components[components.length - 1];
|
||||
const index = lastComponents.isHardened() ? lastComponents.getIndex() + 0x80000000 : lastComponents.getIndex();
|
||||
const indexBuf = Buffer.alloc(4);
|
||||
indexBuf.writeUInt32BE(index);
|
||||
const chainCode = hdKey.getChainCode();
|
||||
const key = hdKey.getKey();
|
||||
const data = Buffer.concat([version, depthBuf, parentFingerprint, indexBuf, chainCode, key]);
|
||||
const results = [];
|
||||
for (const outputDescriptor of cryptoAccount.outputDescriptors) {
|
||||
// now, crafting zpub out of data we have
|
||||
const hdKey = outputDescriptor.getCryptoKey();
|
||||
const derivationPath = 'm/' + hdKey.getOrigin().getPath();
|
||||
const script = cryptoAccount.outputDescriptors[0].getScriptExpressions()[0].getExpression();
|
||||
const isMultisig =
|
||||
script === ScriptExpressions.WITNESS_SCRIPT_HASH.getExpression() ||
|
||||
// fallback to paths (unreliable).
|
||||
// dont know how to add ms p2sh (legacy) or p2sh-p2wsh (wrapped segwit) atm
|
||||
derivationPath === MultisigHDWallet.PATH_LEGACY ||
|
||||
derivationPath === MultisigHDWallet.PATH_WRAPPED_SEGWIT ||
|
||||
derivationPath === MultisigHDWallet.PATH_NATIVE_SEGWIT;
|
||||
const version = Buffer.from(isMultisig ? '02aa7ed3' : '04b24746', 'hex');
|
||||
const parentFingerprint = hdKey.getParentFingerprint();
|
||||
const depth = hdKey.getOrigin().getDepth();
|
||||
const depthBuf = Buffer.alloc(1);
|
||||
depthBuf.writeUInt8(depth);
|
||||
const components = hdKey.getOrigin().getComponents();
|
||||
const lastComponents = components[components.length - 1];
|
||||
const index = lastComponents.isHardened() ? lastComponents.getIndex() + 0x80000000 : lastComponents.getIndex();
|
||||
const indexBuf = Buffer.alloc(4);
|
||||
indexBuf.writeUInt32BE(index);
|
||||
const chainCode = hdKey.getChainCode();
|
||||
const key = hdKey.getKey();
|
||||
const data = Buffer.concat([version, depthBuf, parentFingerprint, indexBuf, chainCode, key]);
|
||||
|
||||
const zpub = b58.encode(data);
|
||||
const zpub = b58.encode(data);
|
||||
|
||||
const result = {};
|
||||
result.ExtPubKey = zpub;
|
||||
result.MasterFingerprint = cryptoAccount.getMasterFingerprint().toString('hex').toUpperCase();
|
||||
result.AccountKeyPath = derivationPath;
|
||||
const result = {};
|
||||
result.ExtPubKey = zpub;
|
||||
result.MasterFingerprint = cryptoAccount.getMasterFingerprint().toString('hex').toUpperCase();
|
||||
result.AccountKeyPath = derivationPath;
|
||||
|
||||
const str = JSON.stringify(result);
|
||||
return str;
|
||||
// return Buffer.from(str, 'ascii').toString('hex'); // we are expected to return hex-encoded string
|
||||
if (derivationPath.startsWith("m/49'/0'/")) {
|
||||
// converting to ypub
|
||||
let data = b58.decode(result.ExtPubKey);
|
||||
data = data.slice(4);
|
||||
result.ExtPubKey = b58.encode(Buffer.concat([Buffer.from('049d7cb2', 'hex'), data]));
|
||||
}
|
||||
|
||||
if (derivationPath.startsWith("m/44'/0'/")) {
|
||||
// converting to xpub
|
||||
let data = b58.decode(result.ExtPubKey);
|
||||
data = data.slice(4);
|
||||
result.ExtPubKey = b58.encode(Buffer.concat([Buffer.from('0488b21e', 'hex'), data]));
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
return JSON.stringify(results);
|
||||
}
|
||||
|
||||
throw new Error('unsupported data format');
|
||||
|
|
|
@ -81,6 +81,7 @@ const startImport = (importTextOrig, askPassphrase = false, searchAccounts = fal
|
|||
// 6. check if its address (watch-only wallet)
|
||||
// 7. check if its private key (segwit address P2SH) TODO
|
||||
// 7. check if its private key (legacy address) TODO
|
||||
// 8. check if its a json array from BC-UR with multiple accounts
|
||||
let text = importTextOrig.trim();
|
||||
let password;
|
||||
|
||||
|
@ -383,6 +384,22 @@ const startImport = (importTextOrig, askPassphrase = false, searchAccounts = fal
|
|||
yield { wallet: s3 };
|
||||
}
|
||||
}
|
||||
|
||||
// is it BC-UR payload with multiple accounts?
|
||||
yield { progress: 'BC-UR' };
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
if (Array.isArray(json)) {
|
||||
for (const account of json) {
|
||||
if (account.ExtPubKey && account.MasterFingerprint && account.AccountKeyPath) {
|
||||
const wallet = new WatchOnlyWallet();
|
||||
wallet.setSecret(JSON.stringify(account));
|
||||
wallet.init();
|
||||
yield { wallet };
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// POEHALI
|
||||
|
|
|
@ -397,6 +397,41 @@ describe('import procedure', () => {
|
|||
await promise;
|
||||
assert.strictEqual(store.state.wallets[0].type, WatchOnlyWallet.type);
|
||||
assert.strictEqual(store.state.wallets[0].getDerivationPath(), "m/84'/0'/0'");
|
||||
assert.strictEqual(store.state.wallets[0].getMasterFingerprintHex(), '7d2f0272');
|
||||
});
|
||||
|
||||
it('can import watch-only Cobo vault export', async () => {
|
||||
const store = createStore();
|
||||
const { promise } = startImport(
|
||||
`[{"ExtPubKey":"zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs","MasterFingerprint":"73C5DA0A","AccountKeyPath":"m/84'/0'/0'"},{"ExtPubKey":"ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP","MasterFingerprint":"73C5DA0A","AccountKeyPath":"m/49'/0'/0'"},{"ExtPubKey":"xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj","MasterFingerprint":"73C5DA0A","AccountKeyPath":"m/44'/0'/0'"}]`,
|
||||
false,
|
||||
false,
|
||||
...store.callbacks,
|
||||
);
|
||||
await promise;
|
||||
assert.strictEqual(store.state.wallets[0].type, WatchOnlyWallet.type);
|
||||
assert.strictEqual(store.state.wallets[0].getDerivationPath(), "m/84'/0'/0'");
|
||||
assert.strictEqual(store.state.wallets[0].getMasterFingerprintHex(), '73c5da0a');
|
||||
assert.strictEqual(
|
||||
store.state.wallets[0].getSecret(),
|
||||
'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs',
|
||||
);
|
||||
|
||||
assert.strictEqual(store.state.wallets[1].type, WatchOnlyWallet.type);
|
||||
assert.strictEqual(store.state.wallets[1].getDerivationPath(), "m/49'/0'/0'");
|
||||
assert.strictEqual(store.state.wallets[1].getMasterFingerprintHex(), '73c5da0a');
|
||||
assert.strictEqual(
|
||||
store.state.wallets[1].getSecret(),
|
||||
'ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP',
|
||||
);
|
||||
|
||||
assert.strictEqual(store.state.wallets[2].type, WatchOnlyWallet.type);
|
||||
assert.strictEqual(store.state.wallets[2].getDerivationPath(), "m/44'/0'/0'");
|
||||
assert.strictEqual(store.state.wallets[2].getMasterFingerprintHex(), '73c5da0a');
|
||||
assert.strictEqual(
|
||||
store.state.wallets[2].getSecret(),
|
||||
'xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj',
|
||||
);
|
||||
});
|
||||
|
||||
it('can import watch-only Keystone vault export', async () => {
|
||||
|
|
|
@ -577,6 +577,34 @@ describe('BC-UR', () => {
|
|||
assert.ok(str.includes('Keystone Multisig setup file'));
|
||||
});
|
||||
|
||||
it('v2: can decodeUR() into accounts', () => {
|
||||
const decoder = new BlueURDecoder();
|
||||
decoder.receivePart(
|
||||
'UR:CRYPTO-ACCOUNT/OEADCYJKSKTNBKAOLSTAADMWTAADDLONAXHDCLAOJOKNIDZCPSSAJTPTRPFRCECFKKAMYKJTVTCSBTBDTKCFIYVYOETNEEYKWFNBNYNDAAHDCXGEGUNBPYCLRHUOMDLNNSGLMOOYHSCFGLAXRTWSFHYKADGESWMOWKEOSSKOGHMHZTAMTAADDYOTADLNCSGHYKAEYKAEYKAOCYJKSKTNBKAXAXATTAADDYOYADLRAEWKLAWKAYCYKBWFDNUYTAADMHTAADMWTAADDLONAXHDCLAOWNWFFLLDCWCXYLHFMNPLFMSOLNNERSRPKGSGRPWFHDEYJLBEWPSSCNHFRYGOMUNTAAHDCXJTPKVLIHPLBABKBKPYLREYHHZEKETSJZFRMHMHECYALDVDTEWNROFLPTNBKKKBSBAMTAADDYOTADLNCSEHYKAEYKAEYKAOCYJKSKTNBKAXAXATTAADDYOYADLRAEWKLAWKAYCYFSAHZMKPTAADMUTAADDLONAXHDCLAXKTGSMEBSTKATZSMTLOJTOSMWWTTLSGWENYZEDYQZGRLSYLVOBWRKMOMUBAKIWKRYAAHDCXFSOXRFCFBKDSLABYCAEHZSURUOMHHEDRLBJZVDKEJLBENLCFBYJLDAFSFXFYGMCFAMTAADDYOTADLNCSDWYKAEYKAEYKAOCYJKSKTNBKAXAXATTAADDYOYADLRAEWKLAWKAYCYBZHPSGHKSOPAJSLN',
|
||||
);
|
||||
let data = '';
|
||||
if (decoder.isComplete()) {
|
||||
data = decoder.toString();
|
||||
}
|
||||
|
||||
const json = JSON.parse(data);
|
||||
|
||||
assert.ok(Array.isArray(json));
|
||||
assert.strictEqual(json.length, 3);
|
||||
|
||||
assert.ok(json[0].ExtPubKey.startsWith('zpub'));
|
||||
assert.ok(json[0].AccountKeyPath.startsWith('m/84'));
|
||||
assert.ok(json[0].MasterFingerprint === '73C5DA0A');
|
||||
|
||||
assert.ok(json[1].ExtPubKey.startsWith('ypub'));
|
||||
assert.ok(json[1].AccountKeyPath.startsWith('m/49'));
|
||||
assert.ok(json[1].MasterFingerprint === '73C5DA0A');
|
||||
|
||||
assert.ok(json[2].ExtPubKey.startsWith('xpub'));
|
||||
assert.ok(json[2].AccountKeyPath.startsWith('m/44'));
|
||||
assert.ok(json[2].MasterFingerprint === '73C5DA0A');
|
||||
});
|
||||
|
||||
it('v1: decodeUR() works', async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // sleep
|
||||
// sleep is needed because in test envirnment setUseURv1() and init function have a race condition
|
||||
|
|
Loading…
Add table
Reference in a new issue