mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 18:00:17 +01:00
REF: coldcard integration
This commit is contained in:
parent
ff522218f1
commit
2138493bf1
@ -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]);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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' }}>
|
||||
|
@ -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' },
|
||||
],
|
||||
|
@ -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',
|
||||
},
|
||||
|
@ -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');
|
||||
|
Loading…
Reference in New Issue
Block a user