REF: coldcard integration

This commit is contained in:
Overtorment 2020-02-24 21:45:14 +00:00
parent ff522218f1
commit 2138493bf1
8 changed files with 119 additions and 41 deletions

View File

@ -636,6 +636,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
async fetchUtxo() {
// considering only confirmed balance
// also, fetching utxo of addresses that only have some balance
let addressess = [];
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
@ -718,6 +719,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
* @param changeAddress {String} Excessive coins will go back to that address
* @param sequence {Number} Used in RBF
* @param skipSigning {boolean} Whether we should skip signing, use returned `psbt` in that case
* @param masterFingerprint {number} Decimal number of wallet's master fingerprint
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number, psbt: Psbt}}
*/
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
@ -759,7 +761,9 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
let pubkey = this._getPubkeyByAddress(input.address);
let masterFingerprintBuffer;
if (masterFingerprint) {
const hexBuffer = Buffer.from(Number(masterFingerprint).toString(16), 'hex');
let masterFingerprintHex = Number(masterFingerprint).toString(16);
if (masterFingerprintHex.length < 8) masterFingerprintHex = '0' + masterFingerprintHex; // conversion without explicit zero might result in lost byte
const hexBuffer = Buffer.from(masterFingerprintHex, 'hex');
masterFingerprintBuffer = Buffer.from(reverse(hexBuffer));
} else {
masterFingerprintBuffer = Buffer.from([0x00, 0x00, 0x00, 0x00]);
@ -799,7 +803,9 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
let masterFingerprintBuffer;
if (masterFingerprint) {
const hexBuffer = Buffer.from(Number(masterFingerprint).toString(16), 'hex');
let masterFingerprintHex = Number(masterFingerprint).toString(16);
if (masterFingerprintHex.length < 8) masterFingerprintHex = '0' + masterFingerprintHex; // conversion without explicit zero might result in lost byte
const hexBuffer = Buffer.from(masterFingerprintHex, 'hex');
masterFingerprintBuffer = Buffer.from(reverse(hexBuffer));
} else {
masterFingerprintBuffer = Buffer.from([0x00, 0x00, 0x00, 0x00]);

View File

@ -128,6 +128,19 @@ export class AbstractWallet {
setSecret(newSecret) {
this.secret = newSecret.trim();
try {
const parsedSecret = JSON.parse(this.secret);
if (parsedSecret && parsedSecret.keystore && parsedSecret.keystore.xpub) {
let masterFingerprint = false;
if (parsedSecret.keystore.ckcc_xfp) {
// It is a ColdCard Hardware Wallet
masterFingerprint = Number(parsedSecret.keystore.ckcc_xfp);
}
this.secret = parsedSecret.keystore.xpub;
this.masterFingerprint = masterFingerprint;
}
} catch (_) {}
return this;
}

View File

@ -44,22 +44,8 @@ export class WatchOnlyWallet extends LegacyWallet {
try {
bitcoin.address.toOutputScript(this.getAddress());
return true;
} catch (_e) {
try {
const parsedSecret = JSON.parse(this.secret);
if (parsedSecret.keystore.xpub) {
let masterFingerprint = false;
if (parsedSecret.keystore.ckcc_xfp) {
// It is a ColdCard Hardware Wallet
masterFingerprint = Number(parsedSecret.keystore.ckcc_xfp);
}
this.setSecret(parsedSecret.keystore.xpub);
this.masterFingerprint = masterFingerprint;
}
return true;
} catch (_e) {
return false;
}
} catch (_) {
return false;
}
}
@ -161,9 +147,30 @@ export class WatchOnlyWallet extends LegacyWallet {
*/
createTransaction(utxos, targets, feeRate, changeAddress, sequence) {
if (this._hdWalletInstance instanceof HDSegwitBech32Wallet) {
return this._hdWalletInstance.createTransaction(utxos, targets, feeRate, changeAddress, sequence, true, this.masterFingerprint);
return this._hdWalletInstance.createTransaction(utxos, targets, feeRate, changeAddress, sequence, true, this.getMasterFingerprint());
} else {
throw new Error('Not a zpub watch-only wallet, cant create PSBT (or just not initialized)');
}
}
getMasterFingerprint() {
return this.masterFingerprint;
}
getMasterFingerprintHex() {
let masterFingerprintHex = Number(this.masterFingerprint).toString(16);
if (masterFingerprintHex.length < 8) masterFingerprintHex = '0' + masterFingerprintHex; // conversion without explicit zero might result in lost byte
// poor man's little-endian conversion:
// ¯\_(ツ)_/¯
return (
masterFingerprintHex[6] +
masterFingerprintHex[7] +
masterFingerprintHex[4] +
masterFingerprintHex[5] +
masterFingerprintHex[2] +
masterFingerprintHex[3] +
masterFingerprintHex[0] +
masterFingerprintHex[1]
);
}
}

View File

@ -712,8 +712,9 @@ export default class SendDetails extends Component {
importTransaction = async () => {
try {
const res = await DocumentPicker.pick({ type: Platform.OS === 'ios'
? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn'] : [DocumentPicker.types.allFiles] });
const res = await DocumentPicker.pick({
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn'] : [DocumentPicker.types.allFiles],
});
if (DeeplinkSchemaMatch.isPossiblyPSBTFile(res.uri)) {
const file = await RNFS.readFile(res.uri, 'ascii');
const bufferDecoded = Buffer.from(file, 'ascii').toString('base64');

View File

@ -47,11 +47,20 @@ export default class PsbtWithHardwareWallet extends Component {
cameraRef = null;
onBarCodeRead = ret => {
if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
this.setState({ txhex: ret.data });
return;
}
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.pausePreview();
this.setState({ renderScanner: false }, () => {
console.log(ret.data);
try {
let Tx = this.state.fromWallet.combinePsbt(this.state.psbt, ret.data);
let Tx = this.state.fromWallet.combinePsbt(
this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64(),
ret.data,
);
this.setState({ txhex: Tx.toHex() });
} catch (Err) {
alert(Err);
@ -262,7 +271,9 @@ export default class PsbtWithHardwareWallet extends Component {
openSignedTransaction = async () => {
try {
const res = await DocumentPicker.pick({ type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallt.psbt.txn'] : [DocumentPicker.types.allFiles] });
const res = await DocumentPicker.pick({
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallt.psbt.txn'] : [DocumentPicker.types.allFiles],
});
const file = await RNFS.readFile(res.uri);
if (file) {
this.setState({ isSecondPSBTAlreadyBase64: true }, () => this.onBarCodeRead({ data: file }));
@ -332,7 +343,7 @@ export default class PsbtWithHardwareWallet extends Component {
color: BlueApp.settings.buttonTextColor,
}}
onPress={this.exportPSBT}
title={'Export'}
title={'Export to file'}
/>
<BlueSpacing20 />
<View style={{ justifyContent: 'center', alignItems: 'center' }}>

View File

@ -51,7 +51,6 @@ export default class WalletDetails extends Component {
super(props);
const wallet = props.navigation.getParam('wallet');
console.warn(wallet.masterFingerprint)
const isLoading = true;
this.state = {
isLoading,
@ -271,20 +270,20 @@ export default class WalletDetails extends Component {
return;
}
}
if (this.state.wallet.getBalance() > 0 && this.state.wallet.allowSend()) {
this.presentWalletHasBalanceAlert();
} else {
this.props.navigation.setParams({ isLoading: true });
this.setState({ isLoading: true }, async () => {
BlueApp.deleteWallet(this.state.wallet);
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
await BlueApp.saveToDisk();
EV(EV.enum.TRANSACTIONS_COUNT_CHANGED);
EV(EV.enum.WALLETS_COUNT_CHANGED);
this.props.navigation.navigate('Wallets');
});
}
}
if (this.state.wallet.getBalance() > 0 && this.state.wallet.allowSend()) {
this.presentWalletHasBalanceAlert();
} else {
this.props.navigation.setParams({ isLoading: true });
this.setState({ isLoading: true }, async () => {
BlueApp.deleteWallet(this.state.wallet);
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
await BlueApp.saveToDisk();
EV(EV.enum.TRANSACTIONS_COUNT_CHANGED);
EV(EV.enum.WALLETS_COUNT_CHANGED);
this.props.navigation.navigate('Wallets');
});
}
},
},
{ text: loc.wallets.details.no_cancel, onPress: () => {}, style: 'cancel' },
],

View File

@ -603,11 +603,11 @@ export default class WalletTransactions extends Component {
text: loc._.ok,
onPress: () => {
const wallet = this.state.wallet;
wallet.use_with_hardware_wallet = true
wallet.use_with_hardware_wallet = true;
this.setState({ wallet }, async () => {
await BlueApp.saveToDisk();
this.navigateToSendScreen();
})
});
},
style: 'default',
},

View File

@ -111,6 +111,47 @@ describe('Watch only wallet', () => {
);
});
it('can import coldcard/electrum compatible JSON skeleton wallet, and create a tx with master fingerprint', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 120 * 1000;
const skeleton =
'{"keystore": {"ckcc_xpub": "xpub661MyMwAqRbcGmUDQVKxmhEESB5xTk8hbsdTSV3Pmhm3HE9Fj3s45R9Y8LwyaQWjXXPytZjuhTKSyCBPeNrB1VVWQq1HCvjbEZ27k44oNmg", "xpub": "zpub6rFDtF1nuXZ9PUL4XzKURh3vJBW6Kj6TUrYL4qPtFNtDXtcTVfiqjQDyrZNwjwzt5HS14qdqo3Co2282Lv3Re6Y5wFZxAVuMEpeygnnDwfx", "label": "Coldcard Import 168DD603", "ckcc_xfp": 64392470, "type": "hardware", "hw_type": "coldcard", "derivation": "m/84\'/0\'/0\'"}, "wallet_type": "standard", "use_encryption": false, "seed_version": 17}';
let w = new WatchOnlyWallet();
w.setSecret(skeleton);
w.init();
assert.ok(w.valid());
assert.strictEqual(
w.getSecret(),
'zpub6rFDtF1nuXZ9PUL4XzKURh3vJBW6Kj6TUrYL4qPtFNtDXtcTVfiqjQDyrZNwjwzt5HS14qdqo3Co2282Lv3Re6Y5wFZxAVuMEpeygnnDwfx',
);
assert.strictEqual(w.getMasterFingerprint(), 64392470);
assert.strictEqual(w.getMasterFingerprintHex(), '168dd603');
const utxos = [
{
height: 618811,
value: 66600,
address: 'bc1qzqjwye4musmz56cg44ttnchj49zueh9yr0qsxt',
txId: '5df595dc09ee7a5c245b34ea519288137ffee731629c4ff322a6de4f72c06222',
vout: 0,
txid: '5df595dc09ee7a5c245b34ea519288137ffee731629c4ff322a6de4f72c06222',
amount: 66600,
wif: false,
confirmations: 1,
},
];
let { psbt } = await w.createTransaction(
utxos,
[{ address: 'bc1qdamevhw3zwm0ajsmyh39x8ygf0jr0syadmzepn', value: 5000 }],
22,
'bc1qtutssamysdkgd87df0afjct0mztx56qpze7wqe',
);
assert.strictEqual(
psbt.toBase64(),
'cHNidP8BAHECAAAAASJiwHJP3qYi80+cYjHn/n8TiJJR6jRbJFx67gnclfVdAAAAAAAAAACAAogTAAAAAAAAFgAUb3eWXdETtv7KGyXiUxyIS+Q3wJ1K3QAAAAAAABYAFF8XCHdkg2yGn81L+plhb9iWamgBAAAAAAABAR8oBAEAAAAAABYAFBAk4ma75DYqawitVrni8qlFzNykIgYDNK9TxoCjQ8P0+qI2Hu4hrnXnJuYAC3h2puZbgRORp+sYFo3WA1QAAIAAAACAAAAAgAAAAAAAAAAAAAAiAgL1DWeV+AfIP5RRB5zHv5vuXsIt8+rF9rrsji3FhQlhzBgWjdYDVAAAgAAAAIAAAACAAQAAAAAAAAAA',
);
});
it('can combine signed PSBT and prepare it for broadcast', async () => {
let w = new WatchOnlyWallet();
w.setSecret('zpub6rjLjQVqVnj7crz9E4QWj4WgczmEseJq22u2B6k2HZr6NE2PQx3ZYg8BnbjN9kCfHymSeMd2EpwpM5iiz5Nrb3TzvddxW2RMcE3VXdVaXHk');