mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-03 12:06:21 +01:00
ADD: multisig technical release
This commit is contained in:
parent
8ace25d140
commit
5c512833d9
36 changed files with 3595 additions and 87 deletions
|
@ -27,7 +27,7 @@ import {
|
|||
import Clipboard from '@react-native-community/clipboard';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import ActionSheet from './screen/ActionSheet';
|
||||
import { LightningCustodianWallet, PlaceholderWallet } from './class';
|
||||
import { LightningCustodianWallet, MultisigHDWallet, PlaceholderWallet } from './class';
|
||||
import Carousel from 'react-native-snap-carousel';
|
||||
import { BitcoinUnit } from './models/bitcoinUnits';
|
||||
import * as NavigationService from './NavigationService';
|
||||
|
@ -339,9 +339,16 @@ export class BlueWalletNavigationHeader extends Component {
|
|||
style={{ padding: 15, minHeight: 140, justifyContent: 'center' }}
|
||||
>
|
||||
<Image
|
||||
source={
|
||||
(LightningCustodianWallet.type === this.state.wallet.type && require('./img/lnd-shape.png')) || require('./img/btc-shape.png')
|
||||
source={(() => {
|
||||
switch (this.state.wallet.type) {
|
||||
case LightningCustodianWallet.type:
|
||||
return require('./img/lnd-shape.png');
|
||||
case MultisigHDWallet.type:
|
||||
return require('./img/vault-shape.png');
|
||||
default:
|
||||
return require('./img/btc-shape.png');
|
||||
}
|
||||
})()}
|
||||
style={{
|
||||
width: 99,
|
||||
height: 94,
|
||||
|
@ -1985,7 +1992,16 @@ const WalletCarouselItem = ({ item, index, onPress, handleLongPress, isSelectedW
|
|||
}}
|
||||
>
|
||||
<Image
|
||||
source={(LightningCustodianWallet.type === item.type && require('./img/lnd-shape.png')) || require('./img/btc-shape.png')}
|
||||
source={(() => {
|
||||
switch (item.type) {
|
||||
case LightningCustodianWallet.type:
|
||||
return require('./img/lnd-shape.png');
|
||||
case MultisigHDWallet.type:
|
||||
return require('./img/vault-shape.png');
|
||||
default:
|
||||
return require('./img/btc-shape.png');
|
||||
}
|
||||
})()}
|
||||
style={{
|
||||
width: 99,
|
||||
height: 94,
|
||||
|
|
|
@ -27,6 +27,7 @@ import PleaseBackupLNDHub from './screen/wallets/pleaseBackupLNDHub';
|
|||
import ImportWallet from './screen/wallets/import';
|
||||
import WalletDetails from './screen/wallets/details';
|
||||
import WalletExport from './screen/wallets/export';
|
||||
import ExportMultisigCoordinationSetup from './screen/wallets/exportMultisigCoordinationSetup';
|
||||
import WalletXpub from './screen/wallets/xpub';
|
||||
import BuyBitcoin from './screen/wallets/buyBitcoin';
|
||||
import HodlHodl from './screen/wallets/hodlHodl';
|
||||
|
@ -53,6 +54,7 @@ import ScanQRCode from './screen/send/ScanQRCode';
|
|||
import SendCreate from './screen/send/create';
|
||||
import Confirm from './screen/send/confirm';
|
||||
import PsbtWithHardwareWallet from './screen/send/psbtWithHardwareWallet';
|
||||
import PsbtMultisig from './screen/send/psbtMultisig';
|
||||
import Success from './screen/send/success';
|
||||
import Broadcast from './screen/send/broadcast';
|
||||
|
||||
|
@ -175,6 +177,7 @@ const SendDetailsRoot = () => (
|
|||
component={PsbtWithHardwareWallet}
|
||||
options={PsbtWithHardwareWallet.navigationOptions}
|
||||
/>
|
||||
<SendDetailsStack.Screen name="PsbtMultisig" component={PsbtMultisig} options={PsbtMultisig.navigationOptions} />
|
||||
<SendDetailsStack.Screen
|
||||
name="CreateTransaction"
|
||||
component={SendCreate}
|
||||
|
@ -312,6 +315,11 @@ const Navigation = () => (
|
|||
|
||||
{/* screens */}
|
||||
<RootStack.Screen name="WalletExport" component={WalletExport} options={WalletExport.navigationOptions} />
|
||||
<RootStack.Screen
|
||||
name="ExportMultisigCoordinationSetup"
|
||||
component={ExportMultisigCoordinationSetup}
|
||||
options={ExportMultisigCoordinationSetup.navigationOptions}
|
||||
/>
|
||||
<RootStack.Screen name="WalletXpub" component={WalletXpub} options={WalletXpub.navigationOptions} />
|
||||
<RootStack.Screen name="BuyBitcoin" component={BuyBitcoin} options={BuyBitcoin.navigationOptions} />
|
||||
<RootStack.Screen name="Marketplace" component={Marketplace} options={Marketplace.navigationOptions} />
|
||||
|
|
79
blue_modules/fs.js
Normal file
79
blue_modules/fs.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
/* global alert */
|
||||
import { PermissionsAndroid, Platform } from 'react-native';
|
||||
import RNFS from 'react-native-fs';
|
||||
import Share from 'react-native-share';
|
||||
import loc from '../loc';
|
||||
import { getSystemName } from 'react-native-device-info';
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
|
||||
const isDesktop = getSystemName() === 'Mac OS X';
|
||||
|
||||
const writeFileAndExport = async function (filename, contents) {
|
||||
if (Platform.OS === 'ios') {
|
||||
const filePath = RNFS.TemporaryDirectoryPath + `/${filename}`;
|
||||
await RNFS.writeFile(filePath, contents);
|
||||
Share.open({
|
||||
url: 'file://' + filePath,
|
||||
saveToFiles: isDesktop,
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
// alert(error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
RNFS.unlink(filePath);
|
||||
});
|
||||
} else if (Platform.OS === 'android') {
|
||||
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
|
||||
title: loc.send.permission_storage_title,
|
||||
message: loc.send.permission_storage_message,
|
||||
buttonNeutral: loc.send.permission_storage_later,
|
||||
buttonNegative: loc._.cancel,
|
||||
buttonPositive: loc._.ok,
|
||||
});
|
||||
|
||||
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
|
||||
console.log('Storage Permission: Granted');
|
||||
const filePath = RNFS.DownloadDirectoryPath + `/${filename}`;
|
||||
await RNFS.writeFile(filePath, contents);
|
||||
alert(loc.formatString(loc._.file_saved, { filePath: filename }));
|
||||
} else {
|
||||
console.log('Storage Permission: Denied');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens & reads *.psbt files, and returns base64 psbt. FALSE if something went wrong (wont throw).
|
||||
*
|
||||
* @returns {Promise<string|boolean>} Base64 PSBT
|
||||
*/
|
||||
const openSignedTransaction = async function () {
|
||||
try {
|
||||
const res = await DocumentPicker.pick({
|
||||
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallt.psbt.txn'] : [DocumentPicker.types.allFiles],
|
||||
});
|
||||
const base64 = await RNFS.readFile(res.uri, 'base64');
|
||||
|
||||
const stringData = Buffer.from(base64, 'base64').toString(); // decode from base64
|
||||
if (stringData.startsWith('psbt')) {
|
||||
// file was binary, but outer code expects base64 psbt, so we return base64 we got from rn-fs;
|
||||
// most likely produced by Electrum-desktop
|
||||
return base64;
|
||||
} else {
|
||||
// file was a text file, having base64 psbt in there. so we basically have double base64encoded string
|
||||
// thats why we are returning string that was decoded once;
|
||||
// most likely produced by Coldcard
|
||||
return stringData;
|
||||
}
|
||||
} catch (err) {
|
||||
if (!DocumentPicker.isCancel(err)) {
|
||||
alert(loc.send.details_no_signed_tx);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
module.exports.writeFileAndExport = writeFileAndExport;
|
||||
module.exports.openSignedTransaction = openSignedTransaction;
|
|
@ -14,6 +14,7 @@ import {
|
|||
LightningCustodianWallet,
|
||||
HDLegacyElectrumSeedP2PKHWallet,
|
||||
HDSegwitElectrumSeedP2WPKHWallet,
|
||||
MultisigHDWallet,
|
||||
} from './';
|
||||
import DeviceQuickActions from './quick-actions';
|
||||
import { AbstractHDElectrumWallet } from './wallets/abstract-hd-electrum-wallet';
|
||||
|
@ -297,6 +298,9 @@ export class AppStorage {
|
|||
case HDSegwitElectrumSeedP2WPKHWallet.type:
|
||||
unserializedWallet = HDSegwitElectrumSeedP2WPKHWallet.fromJson(key);
|
||||
break;
|
||||
case MultisigHDWallet.type:
|
||||
unserializedWallet = MultisigHDWallet.fromJson(key);
|
||||
break;
|
||||
case LightningCustodianWallet.type: {
|
||||
/** @type {LightningCustodianWallet} */
|
||||
unserializedWallet = LightningCustodianWallet.fromJson(key);
|
||||
|
@ -433,7 +437,7 @@ export class AppStorage {
|
|||
const realm = await this.getRealm();
|
||||
for (const key of this.wallets) {
|
||||
if (typeof key === 'boolean' || key.type === PlaceholderWallet.type) continue;
|
||||
if (key.prepareForSerialization) key.prepareForSerialization();
|
||||
key.prepareForSerialization();
|
||||
const keyCloned = Object.assign({}, key); // stripped-down version of a wallet to save to secure keystore
|
||||
if (key._hdWalletInstance) keyCloned._hdWalletInstance = Object.assign({}, key._hdWalletInstance);
|
||||
this.offloadWalletToRealm(realm, key);
|
||||
|
|
|
@ -14,3 +14,4 @@ export * from './hd-segwit-bech32-transaction';
|
|||
export * from './wallets/placeholder-wallet';
|
||||
export * from './wallets/hd-legacy-electrum-seed-p2pkh-wallet';
|
||||
export * from './wallets/hd-segwit-electrum-seed-p2wpkh-wallet';
|
||||
export * from './wallets/multisig-hd-wallet';
|
||||
|
|
|
@ -10,6 +10,7 @@ import { SegwitBech32Wallet } from './wallets/segwit-bech32-wallet';
|
|||
import { HDLegacyElectrumSeedP2PKHWallet } from './wallets/hd-legacy-electrum-seed-p2pkh-wallet';
|
||||
import { HDSegwitElectrumSeedP2WPKHWallet } from './wallets/hd-segwit-electrum-seed-p2wpkh-wallet';
|
||||
import { BlueCurrentTheme } from '../components/themes';
|
||||
import { MultisigHDWallet } from './wallets/multisig-hd-wallet';
|
||||
|
||||
export default class WalletGradient {
|
||||
static hdSegwitP2SHWallet = ['#65ceef', '#68bbe1'];
|
||||
|
@ -19,6 +20,7 @@ export default class WalletGradient {
|
|||
static legacyWallet = ['#40fad1', '#15be98'];
|
||||
static hdLegacyP2PKHWallet = ['#e36dfa', '#bd10e0'];
|
||||
static hdLegacyBreadWallet = ['#fe6381', '#f99c42'];
|
||||
static multisigHdWallet = ['#1ce6eb', '#296fc5', '#3500A2'];
|
||||
static defaultGradients = ['#c65afb', '#9053fe'];
|
||||
static lightningCustodianWallet = ['#f1be07', '#f79056'];
|
||||
static createWallet = BlueCurrentTheme.colors.lightButton;
|
||||
|
@ -55,6 +57,9 @@ export default class WalletGradient {
|
|||
case SegwitBech32Wallet.type:
|
||||
gradient = WalletGradient.segwitBech32Wallet;
|
||||
break;
|
||||
case MultisigHDWallet.type:
|
||||
gradient = WalletGradient.multisigHdWallet;
|
||||
break;
|
||||
default:
|
||||
gradient = WalletGradient.defaultGradients;
|
||||
break;
|
||||
|
@ -88,6 +93,9 @@ export default class WalletGradient {
|
|||
case SegwitBech32Wallet.type:
|
||||
gradient = WalletGradient.segwitBech32Wallet;
|
||||
break;
|
||||
case MultisigHDWallet.type:
|
||||
gradient = WalletGradient.multisigHdWallet;
|
||||
break;
|
||||
case LightningCustodianWallet.type:
|
||||
gradient = WalletGradient.lightningCustodianWallet;
|
||||
break;
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
SegwitBech32Wallet,
|
||||
HDLegacyElectrumSeedP2PKHWallet,
|
||||
HDSegwitElectrumSeedP2WPKHWallet,
|
||||
MultisigHDWallet,
|
||||
} from '.';
|
||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||
import loc from '../loc';
|
||||
|
@ -34,7 +35,9 @@ export default class WalletImport {
|
|||
*/
|
||||
static async _saveWallet(w, additionalProperties) {
|
||||
try {
|
||||
const wallet = BlueApp.getWallets().some(wallet => wallet.getSecret() === w.secret && wallet.type !== PlaceholderWallet.type);
|
||||
const wallet = BlueApp.getWallets().some(
|
||||
wallet => (wallet.getSecret() === w.secret || wallet.getID() === w.getID()) && wallet.type !== PlaceholderWallet.type,
|
||||
);
|
||||
if (wallet) {
|
||||
alert('This wallet has been previously imported.');
|
||||
WalletImport.removePlaceholderWallet();
|
||||
|
@ -97,6 +100,7 @@ export default class WalletImport {
|
|||
const placeholderWallet = WalletImport.addPlaceholderWallet(importText);
|
||||
// Plan:
|
||||
// -2. check if BIP38 encrypted
|
||||
// -1a. check if multisig
|
||||
// -1. check lightning custodian
|
||||
// 0. check if its HDSegwitBech32Wallet (BIP84)
|
||||
// 1. check if its HDSegwitP2SHWallet (BIP49)
|
||||
|
@ -125,6 +129,18 @@ export default class WalletImport {
|
|||
}
|
||||
}
|
||||
|
||||
// is it multisig?
|
||||
try {
|
||||
const ms = new MultisigHDWallet();
|
||||
ms.setSecret(importText);
|
||||
if (ms.getN() > 0 && ms.getM() > 0) {
|
||||
await ms.fetchBalance();
|
||||
return WalletImport._saveWallet(ms);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
// is it lightning custodian?
|
||||
if (importText.indexOf('blitzhub://') !== -1 || importText.indexOf('lndhub://') !== -1) {
|
||||
const lnd = new LightningCustodianWallet();
|
||||
|
|
|
@ -264,4 +264,6 @@ export class AbstractWallet {
|
|||
|
||||
return b58.encode(data);
|
||||
}
|
||||
|
||||
prepareForSerialization() {}
|
||||
}
|
||||
|
|
801
class/wallets/multisig-hd-wallet.js
Normal file
801
class/wallets/multisig-hd-wallet.js
Normal file
|
@ -0,0 +1,801 @@
|
|||
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
|
||||
import bip39 from 'bip39';
|
||||
import b58 from 'bs58check';
|
||||
import { decodeUR } from 'bc-ur';
|
||||
const BlueElectrum = require('../../blue_modules/BlueElectrum');
|
||||
const coinSelectAccumulative = require('coinselect/accumulative');
|
||||
const coinSelectSplit = require('coinselect/split');
|
||||
const HDNode = require('bip32');
|
||||
const bitcoin = require('bitcoinjs-lib');
|
||||
const createHash = require('create-hash');
|
||||
const reverse = require('buffer-reverse');
|
||||
|
||||
export class MultisigHDWallet extends AbstractHDElectrumWallet {
|
||||
static type = 'HDmultisig';
|
||||
static typeReadable = 'Multisig Vault';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._m = 0; // minimum required signatures so spend (m out of n)
|
||||
this._cosigners = []; // array of xpubs or mnemonic seeds
|
||||
this._cosignersFingerprints = []; // array of according fingerprints (if any provided)
|
||||
this._cosignersCustomPaths = []; // array of according paths (if any provided)
|
||||
this._derivationPath = '';
|
||||
this._isNativeSegwit = false;
|
||||
this._isWrappedSegwit = false;
|
||||
this._isLegacy = false;
|
||||
this.gap_limit = 10;
|
||||
}
|
||||
|
||||
isLegacy() {
|
||||
return this._isLegacy;
|
||||
}
|
||||
|
||||
isNativeSegwit() {
|
||||
return this._isNativeSegwit;
|
||||
}
|
||||
|
||||
isWrappedSegwit() {
|
||||
return this._isWrappedSegwit;
|
||||
}
|
||||
|
||||
setWrappedSegwit() {
|
||||
this._isWrappedSegwit = true;
|
||||
}
|
||||
|
||||
setNativeSegwit() {
|
||||
this._isNativeSegwit = true;
|
||||
}
|
||||
|
||||
setLegacy() {
|
||||
this._isLegacy = true;
|
||||
}
|
||||
|
||||
setM(m) {
|
||||
this._m = m;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number} How many minumim signatures required to authorize a spend
|
||||
*/
|
||||
getM() {
|
||||
return this._m;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number} Total count of cosigners
|
||||
*/
|
||||
getN() {
|
||||
return this._cosigners.length;
|
||||
}
|
||||
|
||||
setDerivationPath(path) {
|
||||
this._derivationPath = path;
|
||||
switch (this._derivationPath) {
|
||||
case "m/48'/0'/0'/2'":
|
||||
this._isNativeSegwit = true;
|
||||
break;
|
||||
case "m/48'/0'/0'/1'":
|
||||
this._isWrappedSegwit = true;
|
||||
break;
|
||||
case "m/45'":
|
||||
this._isLegacy = true;
|
||||
break;
|
||||
case "m/44'":
|
||||
this._isLegacy = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getDerivationPath() {
|
||||
return this._derivationPath;
|
||||
}
|
||||
|
||||
getCustomDerivationPathForCosigner(index) {
|
||||
if (index === 0) throw new Error('cosigners indexation starts from 1');
|
||||
return this._cosignersCustomPaths[index - 1] || this.getDerivationPath();
|
||||
}
|
||||
|
||||
getCosigner(index) {
|
||||
if (index === 0) throw new Error('cosigners indexation starts from 1');
|
||||
return this._cosigners[index - 1];
|
||||
}
|
||||
|
||||
getFingerprint(index) {
|
||||
if (index === 0) throw new Error('cosigners fingerprints indexation starts from 1');
|
||||
return this._cosignersFingerprints[index - 1];
|
||||
}
|
||||
|
||||
getCosignerForFingerprint(fp) {
|
||||
const index = this._cosignersFingerprints.indexOf(fp);
|
||||
return this._cosigners[index];
|
||||
}
|
||||
|
||||
static isXpubValid(key) {
|
||||
let xpub;
|
||||
|
||||
try {
|
||||
xpub = super._zpubToXpub(key);
|
||||
HDNode.fromBase58(xpub);
|
||||
return true;
|
||||
} catch (_) {}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param key {string} Either xpub or mnemonic phrase
|
||||
* @param fingerprint {string} Fingerprint for cosigner that is added as xpub
|
||||
* @param path {string} Custom path (if any) for cosigner that is added as mnemonics
|
||||
*/
|
||||
addCosigner(key, fingerprint, path) {
|
||||
if (MultisigHDWallet.isXpubString(key) && !fingerprint) {
|
||||
throw new Error('fingerprint is required when adding cosigner as xpub (watch-only)');
|
||||
}
|
||||
|
||||
if (path && !this.constructor.isPathValid(path)) {
|
||||
throw new Error('path is not valid');
|
||||
}
|
||||
|
||||
if (!MultisigHDWallet.isXpubString(key)) {
|
||||
// mnemonics. lets derive fingerprint
|
||||
if (!bip39.validateMnemonic(key)) throw new Error('Not a valid mnemonic phrase');
|
||||
fingerprint = MultisigHDWallet.seedToFingerprint(key);
|
||||
} else {
|
||||
if (!MultisigHDWallet.isXpubValid(key)) throw new Error('Not a valid xpub: ' + key);
|
||||
}
|
||||
|
||||
const index = this._cosigners.length;
|
||||
this._cosigners[index] = key;
|
||||
if (fingerprint) this._cosignersFingerprints[index] = fingerprint.toUpperCase();
|
||||
if (path) this._cosignersCustomPaths[index] = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stored cosigner can be EITHER xpub (or Zpub or smth), OR mnemonic phrase. This method converts it to xpub
|
||||
*
|
||||
* @param cosigner {string} Zpub (or similar) or mnemonic seed
|
||||
* @returns {string} xpub
|
||||
* @private
|
||||
*/
|
||||
_getXpubFromCosigner(cosigner) {
|
||||
let xpub = cosigner;
|
||||
if (!MultisigHDWallet.isXpubString(cosigner)) {
|
||||
const index = this._cosigners.indexOf(cosigner);
|
||||
xpub = MultisigHDWallet.seedToXpub(cosigner, this._cosignersCustomPaths[index] || this._derivationPath);
|
||||
}
|
||||
return this.constructor._zpubToXpub(xpub);
|
||||
}
|
||||
|
||||
_getExternalAddressByIndex(index) {
|
||||
if (!this._m) throw new Error('m is not set');
|
||||
index = +index;
|
||||
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
|
||||
|
||||
const address = this._getAddressFromNode(0, index);
|
||||
this.external_addresses_cache[index] = address;
|
||||
return address;
|
||||
}
|
||||
|
||||
_getAddressFromNode(nodeIndex, index) {
|
||||
const pubkeys = [];
|
||||
let cosignerIndex = 0;
|
||||
for (const cosigner of this._cosigners) {
|
||||
this._nodes = this._nodes || [];
|
||||
this._nodes[nodeIndex] = this._nodes[nodeIndex] || [];
|
||||
let _node;
|
||||
|
||||
if (!this._nodes[nodeIndex][cosignerIndex]) {
|
||||
const xpub = this._getXpubFromCosigner(cosigner);
|
||||
const hdNode = HDNode.fromBase58(xpub);
|
||||
_node = hdNode.derive(nodeIndex);
|
||||
} else {
|
||||
_node = this._nodes[nodeIndex][cosignerIndex];
|
||||
}
|
||||
|
||||
pubkeys.push(_node.derive(index).publicKey);
|
||||
cosignerIndex++;
|
||||
}
|
||||
|
||||
if (this.isWrappedSegwit()) {
|
||||
const { address } = bitcoin.payments.p2sh({
|
||||
redeem: bitcoin.payments.p2wsh({
|
||||
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
|
||||
}),
|
||||
});
|
||||
|
||||
return address;
|
||||
} else if (this.isNativeSegwit()) {
|
||||
const { address } = bitcoin.payments.p2wsh({
|
||||
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
|
||||
});
|
||||
|
||||
return address;
|
||||
} else if (this.isLegacy()) {
|
||||
const { address } = bitcoin.payments.p2sh({
|
||||
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
|
||||
});
|
||||
|
||||
return address;
|
||||
} else {
|
||||
throw new Error('Dont know how to make address');
|
||||
}
|
||||
}
|
||||
|
||||
_getInternalAddressByIndex(index) {
|
||||
if (!this._m) throw new Error('m is not set');
|
||||
index = +index;
|
||||
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
|
||||
|
||||
const address = this._getAddressFromNode(1, index);
|
||||
this.internal_addresses_cache[index] = address;
|
||||
return address;
|
||||
}
|
||||
|
||||
static seedToXpub(mnemonic, path) {
|
||||
const seed = bip39.mnemonicToSeed(mnemonic);
|
||||
const root = bitcoin.bip32.fromSeed(seed);
|
||||
|
||||
const child = root.derivePath(path).neutered();
|
||||
this._xpub = child.toBase58();
|
||||
|
||||
return this._xpub;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mnemonic {string} Mnemonic seed phrase
|
||||
* @returns {string} Hex string of fingerprint derived from mnemonics. Always has lenght of 8 chars and correct leading zeroes
|
||||
*/
|
||||
static seedToFingerprint(mnemonic) {
|
||||
const seed = bip39.mnemonicToSeed(mnemonic);
|
||||
const root = bitcoin.bip32.fromSeed(seed);
|
||||
let hex = root.fingerprint.toString('hex');
|
||||
while (hex.length < 8) hex = '0' + hex; // leading zeroes
|
||||
return hex.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns xpub with correct prefix accodting to this objects set derivation path, for example 'Zpub' (with
|
||||
* capital Z) for bech32 multisig
|
||||
* @see https://github.com/satoshilabs/slips/blob/master/slip-0132.md
|
||||
*
|
||||
* @param xpub {string} Any kind of xpub, including zpub etc since we are only swapping the prefix bytes
|
||||
* @returns {string}
|
||||
*/
|
||||
convertXpubToMultisignatureXpub(xpub) {
|
||||
let data = b58.decode(xpub);
|
||||
data = data.slice(4);
|
||||
if (this.isNativeSegwit()) {
|
||||
return b58.encode(Buffer.concat([Buffer.from('02aa7ed3', 'hex'), data]));
|
||||
} else if (this.isWrappedSegwit()) {
|
||||
return b58.encode(Buffer.concat([Buffer.from('0295b43f', 'hex'), data]));
|
||||
}
|
||||
|
||||
return xpub;
|
||||
}
|
||||
|
||||
static isXpubString(xpub) {
|
||||
return ['xpub', 'ypub', 'zpub', 'Ypub', 'Zpub'].includes(xpub.substring(0, 4));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts fingerprint that is stored as a deciman number to hex string (all caps)
|
||||
*
|
||||
* @param xfp {number} For example 64392470
|
||||
* @returns {string} For example 168DD603
|
||||
*/
|
||||
static ckccXfp2fingerprint(xfp) {
|
||||
let masterFingerprintHex = Number(xfp).toString(16);
|
||||
while (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]
|
||||
).toUpperCase();
|
||||
}
|
||||
|
||||
getXpub() {
|
||||
return this.getSecret(true);
|
||||
}
|
||||
|
||||
getSecret(coordinationSetup = false) {
|
||||
let ret = '# BlueWallet Multisig setup file\n';
|
||||
if (coordinationSetup) ret += '# this file contains only public keys and is safe to\n# distribute among cosigners\n';
|
||||
if (!coordinationSetup) ret += '# this file may contain private information\n';
|
||||
ret += '#\n';
|
||||
ret += 'Name: ' + this.getLabel() + '\n';
|
||||
ret += 'Policy: ' + this.getM() + ' of ' + this.getN() + '\n';
|
||||
|
||||
let hasCustomPaths = 0;
|
||||
for (let index = 0; index < this.getN(); index++) {
|
||||
if (this._cosignersCustomPaths[index]) hasCustomPaths++;
|
||||
}
|
||||
|
||||
let printedGlobalDerivation = false;
|
||||
if (hasCustomPaths !== this.getN()) {
|
||||
printedGlobalDerivation = true;
|
||||
ret += 'Derivation: ' + this.getDerivationPath() + '\n';
|
||||
}
|
||||
|
||||
if (this.isNativeSegwit()) {
|
||||
ret += 'Format: P2WSH\n';
|
||||
} else if (this.isWrappedSegwit()) {
|
||||
ret += 'Format: P2WSH-P2SH\n';
|
||||
} else if (this.isLegacy()) {
|
||||
ret += 'Format: P2SH\n';
|
||||
} else {
|
||||
ret += 'Format: unknown\n';
|
||||
}
|
||||
ret += '\n';
|
||||
|
||||
for (let index = 0; index < this.getN(); index++) {
|
||||
if (
|
||||
this._cosignersCustomPaths[index] &&
|
||||
((printedGlobalDerivation && this._cosignersCustomPaths[index] !== this.getDerivationPath()) || !printedGlobalDerivation)
|
||||
) {
|
||||
ret += '# derivation: ' + this._cosignersCustomPaths[index] + '\n';
|
||||
// if we printed global derivation and this cosigned _has_ derivation and its different from global - we print it ;
|
||||
// or we print it if cosigner _has_ some derivation set and we did not print global
|
||||
}
|
||||
if (this.constructor.isXpubString(this._cosigners[index])) {
|
||||
ret += this._cosignersFingerprints[index] + ': ' + this._cosigners[index] + '\n';
|
||||
} else {
|
||||
if (coordinationSetup) {
|
||||
const xpub = this.convertXpubToMultisignatureXpub(
|
||||
MultisigHDWallet.seedToXpub(this._cosigners[index], this._cosignersCustomPaths[index] || this._derivationPath),
|
||||
);
|
||||
const fingerprint = MultisigHDWallet.seedToFingerprint(this._cosigners[index]);
|
||||
ret += fingerprint + ': ' + xpub + '\n';
|
||||
} else {
|
||||
ret += 'seed: ' + this._cosigners[index] + '\n';
|
||||
ret += '# warning! sensitive information, do not disclose ^^^ \n';
|
||||
}
|
||||
}
|
||||
|
||||
ret += '\n';
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
setSecret(secret) {
|
||||
if (secret.toUpperCase().startsWith('UR:BYTES')) {
|
||||
const decoded = decodeUR([secret]);
|
||||
const b = Buffer.from(decoded, 'hex');
|
||||
secret = b.toString();
|
||||
}
|
||||
|
||||
// is it Coldcard json file?
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(secret);
|
||||
} catch (_) {}
|
||||
if (json && json.xfp && json.p2wsh_deriv && json.p2wsh) {
|
||||
this.addCosigner(json.p2wsh, json.xfp); // technically we dont need deriv (json.p2wsh_deriv), since cosigner is already an xpub
|
||||
return;
|
||||
}
|
||||
|
||||
// is it electrum json?
|
||||
if (json && json.wallet_type) {
|
||||
const mofn = json.wallet_type.split('of');
|
||||
this.setM(parseInt(mofn[0].trim()));
|
||||
const n = parseInt(mofn[1].trim());
|
||||
for (let c = 1; c <= n; c++) {
|
||||
const cosignerData = json['x' + c + '/'];
|
||||
if (cosignerData) {
|
||||
const fingerprint = cosignerData.ckcc_xfp
|
||||
? MultisigHDWallet.ckccXfp2fingerprint(cosignerData.ckcc_xfp)
|
||||
: cosignerData.root_fingerprint?.toUpperCase();
|
||||
if (cosignerData.seed) {
|
||||
// TODO: support electrum's bip32
|
||||
}
|
||||
this.addCosigner(cosignerData.xpub, fingerprint, cosignerData.derivation);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.getCosigner(1).startsWith('Zpub')) this.setNativeSegwit();
|
||||
if (this.getCosigner(1).startsWith('Ypub')) this.setWrappedSegwit();
|
||||
if (this.getCosigner(1).startsWith('xpub')) this.setLegacy();
|
||||
}
|
||||
|
||||
// coldcard & cobo txt format:
|
||||
let customPathForCurrentCosigner = false;
|
||||
for (const line of secret.split('\n')) {
|
||||
const [key, value] = line.split(':');
|
||||
|
||||
switch (key) {
|
||||
case 'Name':
|
||||
this.setLabel(value.trim());
|
||||
break;
|
||||
|
||||
case 'Policy':
|
||||
this.setM(parseInt(value.trim().split('of')[0].trim()));
|
||||
break;
|
||||
|
||||
case 'Derivation':
|
||||
this.setDerivationPath(value.trim());
|
||||
break;
|
||||
|
||||
case 'Format':
|
||||
switch (value.trim()) {
|
||||
case 'P2WSH':
|
||||
this.setNativeSegwit();
|
||||
break;
|
||||
case 'P2WSH-P2SH':
|
||||
this.setWrappedSegwit();
|
||||
break;
|
||||
case 'P2SH':
|
||||
this.setLegacy();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (key && value && MultisigHDWallet.isXpubString(value.trim())) {
|
||||
this.addCosigner(value.trim(), key, customPathForCurrentCosigner);
|
||||
} else if (key.replace('#', '').trim() === 'derivation') {
|
||||
customPathForCurrentCosigner = value.trim();
|
||||
} else if (key === 'seed') {
|
||||
this.addCosigner(value.trim(), false, customPathForCurrentCosigner);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.getLabel()) this.setLabel('Multisig vault');
|
||||
}
|
||||
|
||||
_getDerivationPathByAddressWithCustomPath(address, customPathPrefix) {
|
||||
const path = customPathPrefix || this._derivationPath;
|
||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||
if (this._getExternalAddressByIndex(c) === address) return path + '/0/' + c;
|
||||
}
|
||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||
if (this._getInternalAddressByIndex(c) === address) return path + '/1/' + c;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_getWifForAddress(address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_getPubkeyByAddress(address) {
|
||||
throw new Error('Not applicable in multisig');
|
||||
}
|
||||
|
||||
_getDerivationPathByAddress(address) {
|
||||
throw new Error('Not applicable in multisig');
|
||||
}
|
||||
|
||||
_addPsbtInput(psbt, input, sequence, masterFingerprintBuffer) {
|
||||
const bip32Derivation = []; // array per each pubkey thats gona be used
|
||||
const pubkeys = [];
|
||||
for (let c = 0; c < this._cosigners.length; c++) {
|
||||
const cosigner = this._cosigners[c];
|
||||
const path = this._getDerivationPathByAddressWithCustomPath(input.address, this._cosignersCustomPaths[c] || this._derivationPath);
|
||||
// ^^ path resembles _custom path_, if provided by user during setup, otherwise default path for wallet type gona be used
|
||||
const masterFingerprint = Buffer.from(this._cosignersFingerprints[c], 'hex');
|
||||
|
||||
const xpub = this._getXpubFromCosigner(cosigner);
|
||||
const hdNode0 = HDNode.fromBase58(xpub);
|
||||
const splt = path.split('/');
|
||||
const internal = +splt[splt.length - 2];
|
||||
const index = +splt[splt.length - 1];
|
||||
const _node0 = hdNode0.derive(internal);
|
||||
const pubkey = _node0.derive(index).publicKey;
|
||||
pubkeys.push(pubkey);
|
||||
|
||||
bip32Derivation.push({
|
||||
masterFingerprint,
|
||||
path,
|
||||
pubkey,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.isNativeSegwit()) {
|
||||
const p2wsh = bitcoin.payments.p2wsh({
|
||||
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
|
||||
});
|
||||
const witnessScript = p2wsh.redeem.output;
|
||||
|
||||
psbt.addInput({
|
||||
hash: input.txId,
|
||||
index: input.vout,
|
||||
sequence,
|
||||
bip32Derivation,
|
||||
witnessUtxo: {
|
||||
script: p2wsh.output,
|
||||
value: input.value,
|
||||
},
|
||||
witnessScript,
|
||||
// hw wallets now require passing the whole previous tx as Buffer, as if it was non-segwit input, to mitigate
|
||||
// some hw wallets attack vector
|
||||
nonWitnessUtxo: Buffer.from(input.txhex, 'hex'),
|
||||
});
|
||||
} else if (this.isWrappedSegwit()) {
|
||||
const p2shP2wsh = bitcoin.payments.p2sh({
|
||||
redeem: bitcoin.payments.p2wsh({
|
||||
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
|
||||
}),
|
||||
});
|
||||
const witnessScript = p2shP2wsh.redeem.redeem.output;
|
||||
const redeemScript = p2shP2wsh.redeem.output;
|
||||
|
||||
psbt.addInput({
|
||||
hash: input.txId,
|
||||
index: input.vout,
|
||||
sequence,
|
||||
bip32Derivation,
|
||||
witnessUtxo: {
|
||||
script: p2shP2wsh.output,
|
||||
value: input.value,
|
||||
},
|
||||
witnessScript,
|
||||
redeemScript,
|
||||
// hw wallets now require passing the whole previous tx as Buffer, as if it was non-segwit input, to mitigate
|
||||
// some hw wallets attack vector
|
||||
nonWitnessUtxo: Buffer.from(input.txhex, 'hex'),
|
||||
});
|
||||
} else if (this.isLegacy()) {
|
||||
const p2sh = bitcoin.payments.p2sh({
|
||||
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
|
||||
});
|
||||
const redeemScript = p2sh.redeem.output;
|
||||
psbt.addInput({
|
||||
hash: input.txId,
|
||||
index: input.vout,
|
||||
sequence,
|
||||
bip32Derivation,
|
||||
redeemScript,
|
||||
nonWitnessUtxo: Buffer.from(input.txhex, 'hex'),
|
||||
});
|
||||
} else {
|
||||
throw new Error('Dont know how to add input');
|
||||
}
|
||||
|
||||
return psbt;
|
||||
}
|
||||
|
||||
_getOutputDataForChange(outputData) {
|
||||
const bip32Derivation = []; // array per each pubkey thats gona be used
|
||||
const pubkeys = [];
|
||||
for (let c = 0; c < this._cosigners.length; c++) {
|
||||
const cosigner = this._cosigners[c];
|
||||
const path = this._getDerivationPathByAddressWithCustomPath(
|
||||
outputData.address,
|
||||
this._cosignersCustomPaths[c] || this._derivationPath,
|
||||
);
|
||||
// ^^ path resembles _custom path_, if provided by user during setup, otherwise default path for wallet type gona be used
|
||||
const masterFingerprint = Buffer.from(this._cosignersFingerprints[c], 'hex');
|
||||
|
||||
const xpub = this._getXpubFromCosigner(cosigner);
|
||||
const hdNode0 = HDNode.fromBase58(xpub);
|
||||
const splt = path.split('/');
|
||||
const internal = +splt[splt.length - 2];
|
||||
const index = +splt[splt.length - 1];
|
||||
const _node0 = hdNode0.derive(internal);
|
||||
const pubkey = _node0.derive(index).publicKey;
|
||||
pubkeys.push(pubkey);
|
||||
|
||||
bip32Derivation.push({
|
||||
masterFingerprint,
|
||||
path,
|
||||
pubkey,
|
||||
});
|
||||
}
|
||||
|
||||
outputData.bip32Derivation = bip32Derivation;
|
||||
|
||||
if (this.isLegacy()) {
|
||||
const p2sh = bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) });
|
||||
outputData.redeemScript = p2sh.output;
|
||||
} else if (this.isWrappedSegwit()) {
|
||||
const p2shP2wsh = bitcoin.payments.p2sh({
|
||||
redeem: bitcoin.payments.p2wsh({
|
||||
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
|
||||
}),
|
||||
});
|
||||
outputData.witnessScript = p2shP2wsh.redeem.redeem.output;
|
||||
outputData.redeemScript = p2shP2wsh.redeem.output;
|
||||
} else if (this.isNativeSegwit()) {
|
||||
// not needed by coldcard, apparently..?
|
||||
const p2wsh = bitcoin.payments.p2wsh({
|
||||
redeem: bitcoin.payments.p2ms({ m: this._m, pubkeys: MultisigHDWallet.sortBuffers(pubkeys) }),
|
||||
});
|
||||
outputData.witnessScript = p2wsh.redeem.output;
|
||||
} else {
|
||||
throw new Error('dont know how to add change output');
|
||||
}
|
||||
|
||||
return outputData;
|
||||
}
|
||||
|
||||
howManySignaturesCanWeMake() {
|
||||
let howManyPrivKeysWeGot = 0;
|
||||
for (const cosigner of this._cosigners) {
|
||||
if (!MultisigHDWallet.isXpubString(cosigner)) howManyPrivKeysWeGot++;
|
||||
}
|
||||
|
||||
return howManyPrivKeysWeGot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
createTransaction(utxos, targets, feeRate, changeAddress, sequence, skipSigning = false, masterFingerprint) {
|
||||
if (targets.length === 0) throw new Error('No destination provided');
|
||||
if (this.howManySignaturesCanWeMake() === 0) skipSigning = true;
|
||||
|
||||
if (!changeAddress) throw new Error('No change address provided');
|
||||
sequence = sequence || AbstractHDElectrumWallet.defaultRBFSequence;
|
||||
|
||||
let algo = coinSelectAccumulative;
|
||||
if (targets.length === 1 && targets[0] && !targets[0].value) {
|
||||
// we want to send MAX
|
||||
algo = coinSelectSplit;
|
||||
}
|
||||
|
||||
const { inputs, outputs, fee } = algo(utxos, targets, feeRate);
|
||||
|
||||
// .inputs and .outputs will be undefined if no solution was found
|
||||
if (!inputs || !outputs) {
|
||||
throw new Error('Not enough balance. Try sending smaller amount');
|
||||
}
|
||||
|
||||
let psbt = new bitcoin.Psbt();
|
||||
|
||||
let c = 0;
|
||||
inputs.forEach(input => {
|
||||
c++;
|
||||
psbt = this._addPsbtInput(psbt, input, sequence);
|
||||
});
|
||||
|
||||
outputs.forEach(output => {
|
||||
// if output has no address - this is change output
|
||||
let change = false;
|
||||
if (!output.address) {
|
||||
change = true;
|
||||
output.address = changeAddress;
|
||||
}
|
||||
|
||||
let outputData = {
|
||||
address: output.address,
|
||||
value: output.value,
|
||||
};
|
||||
|
||||
if (change) {
|
||||
outputData = this._getOutputDataForChange(outputData);
|
||||
}
|
||||
|
||||
psbt.addOutput(outputData);
|
||||
});
|
||||
|
||||
if (!skipSigning) {
|
||||
for (let cc = 0; cc < c; cc++) {
|
||||
for (const cosigner of this._cosigners) {
|
||||
if (!MultisigHDWallet.isXpubString(cosigner)) {
|
||||
// ok this is a mnemonic, lets try to sign
|
||||
const seed = bip39.mnemonicToSeed(cosigner);
|
||||
const hdRoot = bitcoin.bip32.fromSeed(seed);
|
||||
psbt.signInputHD(cc, hdRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tx;
|
||||
if (!skipSigning && this.howManySignaturesCanWeMake() >= this.getM()) {
|
||||
tx = psbt.finalizeAllInputs().extractTransaction();
|
||||
}
|
||||
return { tx, inputs, outputs, fee, psbt };
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://github.com/bitcoin/bips/blob/master/bip-0067.mediawiki
|
||||
*
|
||||
* @param bufArr {Array.<Buffer>}
|
||||
* @returns {Array.<Buffer>}
|
||||
*/
|
||||
static sortBuffers(bufArr) {
|
||||
return bufArr.sort(Buffer.compare);
|
||||
}
|
||||
|
||||
prepareForSerialization() {
|
||||
// deleting structures that cant be serialized
|
||||
delete this._nodes;
|
||||
}
|
||||
|
||||
static isPathValid(path) {
|
||||
const root = bitcoin.bip32.fromSeed(Buffer.alloc(32));
|
||||
try {
|
||||
root.derivePath(path);
|
||||
return true;
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
allowSend() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async fetchUtxo() {
|
||||
await super.fetchUtxo();
|
||||
// now we need to fetch txhash for each input as required by PSBT
|
||||
const txhexes = await BlueElectrum.multiGetTransactionByTxid(
|
||||
this.getUtxo().map(x => x.txid),
|
||||
50,
|
||||
false,
|
||||
);
|
||||
|
||||
const newUtxos = [];
|
||||
for (const u of this.getUtxo()) {
|
||||
if (txhexes[u.txid]) u.txhex = txhexes[u.txid];
|
||||
newUtxos.push(u);
|
||||
}
|
||||
|
||||
return newUtxos;
|
||||
}
|
||||
|
||||
getID() {
|
||||
const string2hash = [...this._cosigners].sort().join(',') + ';' + [...this._cosignersFingerprints].sort().join(',');
|
||||
return createHash('sha256').update(string2hash).digest().toString('hex');
|
||||
}
|
||||
|
||||
calculateFeeFromPsbt(psbt) {
|
||||
let goesIn = 0;
|
||||
const cacheUtxoAmounts = {};
|
||||
for (const inp of psbt.data.inputs) {
|
||||
if (inp.witnessUtxo && inp.witnessUtxo.value) {
|
||||
// segwit input
|
||||
goesIn += inp.witnessUtxo.value;
|
||||
} else if (inp.nonWitnessUtxo) {
|
||||
// non-segwit input
|
||||
// lets parse this transaction and cache how much each input was worth
|
||||
const inputTx = bitcoin.Transaction.fromHex(inp.nonWitnessUtxo);
|
||||
let index = 0;
|
||||
for (const out of inputTx.outs) {
|
||||
cacheUtxoAmounts[inputTx.getId() + ':' + index] = out.value;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (goesIn === 0) {
|
||||
// means we failed to get amounts that go in previously, so lets use utxo amounts cache we've build
|
||||
// from non-segwit inputs
|
||||
for (const inp of psbt.txInputs) {
|
||||
const cacheKey = reverse(inp.hash).toString('hex') + ':' + inp.index;
|
||||
if (cacheUtxoAmounts[cacheKey]) goesIn += cacheUtxoAmounts[cacheKey];
|
||||
}
|
||||
}
|
||||
|
||||
let goesOut = 0;
|
||||
for (const output of psbt.txOutputs) {
|
||||
goesOut += output.value;
|
||||
}
|
||||
|
||||
return goesIn - goesOut;
|
||||
}
|
||||
|
||||
calculateHowManySignaturesWeHaveFromPsbt(psbt) {
|
||||
let sigsHave = 0;
|
||||
for (const inp of psbt.data.inputs) {
|
||||
sigsHave = Math.max(sigsHave, inp.partialSig?.length || 0);
|
||||
if (inp.finalScriptSig || inp.finalScriptWitness) sigsHave = this.getM(); // hacky, but it means we have enough
|
||||
// He who knows that enough is enough will always have enough. Lao Tzu
|
||||
}
|
||||
|
||||
return sigsHave;
|
||||
}
|
||||
}
|
180
components/DynamicQRCode.js
Normal file
180
components/DynamicQRCode.js
Normal file
|
@ -0,0 +1,180 @@
|
|||
/* eslint react/prop-types: "off", react-native/no-inline-styles: "off" */
|
||||
import React, { Component } from 'react';
|
||||
import { Text } from 'react-native-elements';
|
||||
import { Dimensions, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { encodeUR } from 'bc-ur/dist';
|
||||
import QRCode from 'react-native-qrcode-svg';
|
||||
import { BlueCurrentTheme } from '../components/themes';
|
||||
import { BlueSpacing20 } from '../BlueComponents';
|
||||
import loc from '../loc';
|
||||
|
||||
const { height, width } = Dimensions.get('window');
|
||||
|
||||
export class DynamicQRCode extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
const qrCodeHeight = height > width ? width - 40 : width / 3;
|
||||
const qrCodeMaxHeight = 370;
|
||||
this.state = {
|
||||
index: 0,
|
||||
total: 0,
|
||||
qrCodeHeight: Math.min(qrCodeHeight, qrCodeMaxHeight),
|
||||
intervalHandler: null,
|
||||
};
|
||||
}
|
||||
|
||||
fragments = [];
|
||||
|
||||
componentDidMount() {
|
||||
const { value, capacity = 800, hideControls = true } = this.props;
|
||||
this.fragments = encodeUR(value, capacity);
|
||||
this.setState(
|
||||
{
|
||||
total: this.fragments.length,
|
||||
hideControls,
|
||||
},
|
||||
() => {
|
||||
this.startAutoMove();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
moveToNextFragment = () => {
|
||||
const { index, total } = this.state;
|
||||
if (index === total - 1) {
|
||||
this.setState({
|
||||
index: 0,
|
||||
});
|
||||
} else {
|
||||
this.setState(state => ({
|
||||
index: state.index + 1,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
startAutoMove = () => {
|
||||
if (!this.state.intervalHandler)
|
||||
this.setState(() => ({
|
||||
intervalHandler: setInterval(this.moveToNextFragment, 500),
|
||||
}));
|
||||
};
|
||||
|
||||
stopAutoMove = () => {
|
||||
clearInterval(this.state.intervalHandler);
|
||||
this.setState(() => ({
|
||||
intervalHandler: null,
|
||||
}));
|
||||
};
|
||||
|
||||
moveToPreviousFragment = () => {
|
||||
const { index, total } = this.state;
|
||||
if (index > 0) {
|
||||
this.setState(state => ({
|
||||
index: state.index - 1,
|
||||
}));
|
||||
} else {
|
||||
this.setState(state => ({
|
||||
index: total - 1,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const currentFragment = this.fragments[this.state.index];
|
||||
|
||||
if (!currentFragment) {
|
||||
return (
|
||||
<View>
|
||||
<Text>{loc.send.dynamic_init}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={animatedQRCodeStyle.container}>
|
||||
<TouchableOpacity
|
||||
style={animatedQRCodeStyle.qrcodeContainer}
|
||||
onPress={() => {
|
||||
this.setState(prevState => ({ hideControls: !prevState.hideControls }));
|
||||
}}
|
||||
>
|
||||
<QRCode
|
||||
value={currentFragment.toUpperCase()}
|
||||
size={this.state.qrCodeHeight}
|
||||
color="#000000"
|
||||
logoBackgroundColor={BlueCurrentTheme.colors.brandingColor}
|
||||
backgroundColor="#FFFFFF"
|
||||
ecl="L"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{!this.state.hideControls && (
|
||||
<View style={animatedQRCodeStyle.container}>
|
||||
<BlueSpacing20 />
|
||||
<View>
|
||||
<Text style={animatedQRCodeStyle.text}>
|
||||
{loc.formatString(loc._.of, { number: this.state.index + 1, total: this.state.total })}
|
||||
</Text>
|
||||
</View>
|
||||
<BlueSpacing20 />
|
||||
<View style={animatedQRCodeStyle.controller}>
|
||||
<TouchableOpacity
|
||||
style={[animatedQRCodeStyle.button, { width: '25%', alignItems: 'flex-start' }]}
|
||||
onPress={this.moveToPreviousFragment}
|
||||
>
|
||||
<Text style={animatedQRCodeStyle.text}>{loc.send.dynamic_prev}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[animatedQRCodeStyle.button, { width: '50%' }]}
|
||||
onPress={this.state.intervalHandler ? this.stopAutoMove : this.startAutoMove}
|
||||
>
|
||||
<Text style={animatedQRCodeStyle.text}>{this.state.intervalHandler ? loc.send.dynamic_stop : loc.send.dynamic_start}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[animatedQRCodeStyle.button, { width: '25%', alignItems: 'flex-end' }]}
|
||||
onPress={this.moveToNextFragment}
|
||||
>
|
||||
<Text style={animatedQRCodeStyle.text}>{loc.send.dynamic_next}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const animatedQRCodeStyle = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
qrcodeContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderWidth: 6,
|
||||
borderRadius: 8,
|
||||
borderColor: '#FFFFFF',
|
||||
margin: 6,
|
||||
},
|
||||
controller: {
|
||||
width: '90%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
borderRadius: 25,
|
||||
height: 45,
|
||||
paddingHorizontal: 18,
|
||||
},
|
||||
button: {
|
||||
alignItems: 'center',
|
||||
height: 45,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
text: {
|
||||
fontSize: 14,
|
||||
color: BlueCurrentTheme.colors.foregroundColor,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
38
components/SquareButton.js
Normal file
38
components/SquareButton.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/* eslint react/prop-types: "off", react-native/no-inline-styles: "off" */
|
||||
import React from 'react';
|
||||
import { TouchableOpacity, View, Text } from 'react-native';
|
||||
import { Icon } from 'react-native-elements';
|
||||
import { useTheme } from '@react-navigation/native';
|
||||
|
||||
export const SquareButton = props => {
|
||||
const { colors } = useTheme();
|
||||
let backgroundColor = props.backgroundColor ? props.backgroundColor : colors.buttonBlueBackgroundColor;
|
||||
let fontColor = colors.buttonTextColor;
|
||||
if (props.disabled === true) {
|
||||
backgroundColor = colors.buttonDisabledBackgroundColor;
|
||||
fontColor = colors.buttonDisabledTextColor;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
flex: 1,
|
||||
borderWidth: 0.7,
|
||||
borderColor: 'transparent',
|
||||
backgroundColor: backgroundColor,
|
||||
minHeight: 50,
|
||||
height: 50,
|
||||
maxHeight: 50,
|
||||
borderRadius: 10,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }}>
|
||||
{props.icon && <Icon name={props.icon.name} type={props.icon.type} color={props.icon.color} />}
|
||||
{props.title && <Text style={{ marginHorizontal: 8, fontSize: 16, color: fontColor }}>{props.title}</Text>}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
|
@ -54,6 +54,8 @@ export const BlueDefaultTheme = {
|
|||
mainColor: '#CFDCF6',
|
||||
success: '#ccddf9',
|
||||
successCheck: '#0f5cc0',
|
||||
msSuccessBG: '#37c0a1',
|
||||
msSuccessCheck: '#ffffff',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -96,6 +98,8 @@ export const BlueDarkTheme = {
|
|||
buttonBlueBackgroundColor: '#202020',
|
||||
scanLabel: 'rgba(255,255,255,.2)',
|
||||
labelText: '#ffffff',
|
||||
msSuccessBG: '#8EFFE5',
|
||||
msSuccessCheck: '#000000',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
BIN
img/vault-shape.png
Normal file
BIN
img/vault-shape.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.6 KiB |
|
@ -939,43 +939,12 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-BlueWallet/Pods-BlueWallet-resources.sh",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Fontisto.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Foundation.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Octicons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf",
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-BlueWallet/Pods-BlueWallet-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Feather.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Fontisto.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Foundation.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Ionicons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialCommunityIcons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialIcons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Octicons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SimpleLineIcons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Zocial.ttf",
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-BlueWallet/Pods-BlueWallet-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
|
|
|
@ -36,6 +36,35 @@
|
|||
<string>io.bluewallet.psbt.txn</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>TXT</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>io.bluewallet.txt</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>JSON</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>io.bluewallet.json</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict/>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
|
@ -241,6 +270,44 @@
|
|||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.plain-text</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Text File</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>io.bluewallet.txt</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>txt</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.json</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>JSON File</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>io.bluewallet.json</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>json</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -724,4 +724,4 @@ SPEC CHECKSUMS:
|
|||
|
||||
PODFILE CHECKSUM: e9c5efd531ca5ac67a4b743a179eeefb322cf387
|
||||
|
||||
COCOAPODS: 1.10.0.beta.2
|
||||
COCOAPODS: 1.9.3
|
||||
|
|
21
loc/en.json
21
loc/en.json
|
@ -8,7 +8,9 @@
|
|||
"of": "{number} of {total}",
|
||||
"ok": "OK",
|
||||
"storage_is_encrypted": "Your storage is encrypted. Password is required to decrypt it",
|
||||
"yes": "Yes"
|
||||
"yes": "Yes",
|
||||
"invalid_animated_qr_code_fragment" : "Invalid animated QRCode fragment, please try again",
|
||||
"file_saved": "File ({filePath}) has been saved in your Downloads folder ."
|
||||
},
|
||||
"azteco": {
|
||||
"codeIs": "Your voucher code is",
|
||||
|
@ -176,7 +178,7 @@
|
|||
"details_total_exceeds_balance": "The sending amount exceeds the available balance.",
|
||||
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
|
||||
"details_wallet_selection": "Wallet Selection",
|
||||
"dynamic_init": "Initialing",
|
||||
"dynamic_init": "Initializing",
|
||||
"dynamic_next": "Next",
|
||||
"dynamic_prev": "Previous",
|
||||
"dynamic_start": "Start",
|
||||
|
@ -209,7 +211,8 @@
|
|||
"qr_error_no_qrcode": "The selected image does not contain a QR Code.",
|
||||
"qr_error_no_wallet": "The selected file does not contain a wallet that can be imported.",
|
||||
"success_done": "Done",
|
||||
"txSaved": "The transaction file ({filePath}) has been saved in your Downloads folder ."
|
||||
"txSaved": "The transaction file ({filePath}) has been saved in your Downloads folder .",
|
||||
"problem_with_psbt": "Problem with PSBT"
|
||||
},
|
||||
"settings": {
|
||||
"about": "About",
|
||||
|
@ -371,5 +374,17 @@
|
|||
"select_wallet": "Select Wallet",
|
||||
"xpub_copiedToClipboard": "Copied to clipboard.",
|
||||
"xpub_title": "wallet XPUB"
|
||||
},
|
||||
"multisig": {
|
||||
"provide_signature": "Provide signature",
|
||||
"vault_key": "Vault key {number}",
|
||||
"fee": "Fee: {number}",
|
||||
"fee_btc": "{number} BTC",
|
||||
"confirm": "Confirm",
|
||||
"header": "Send",
|
||||
"share": "Share",
|
||||
"how_many_signatures_can_bluewallet_make": "how many signatures can bluewallet make",
|
||||
"scan_or_import_file": "Scan or import file",
|
||||
"export_coordination_setup": "export coordination setup"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -176,7 +176,7 @@
|
|||
"details_total_exceeds_balance": "Jumlah yang dikirim melebihi saldo.",
|
||||
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
|
||||
"details_wallet_selection": "Wallet Selection",
|
||||
"dynamic_init": "Initialing",
|
||||
"dynamic_init": "Initializing",
|
||||
"dynamic_next": "Next",
|
||||
"dynamic_prev": "Previous",
|
||||
"dynamic_start": "Start",
|
||||
|
|
|
@ -176,7 +176,7 @@
|
|||
"details_total_exceeds_balance": "L'importo da inviare eccede i fondi disponibili.",
|
||||
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
|
||||
"details_wallet_selection": "Wallet Selection",
|
||||
"dynamic_init": "Initialing",
|
||||
"dynamic_init": "Initializing",
|
||||
"dynamic_next": "Next",
|
||||
"dynamic_prev": "Previous",
|
||||
"dynamic_start": "Start",
|
||||
|
|
|
@ -176,7 +176,7 @@
|
|||
"details_total_exceeds_balance": "Het verzendingsbedrag overschrijdt het beschikbare saldo.",
|
||||
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
|
||||
"details_wallet_selection": "Wallet Selection",
|
||||
"dynamic_init": "Initialing",
|
||||
"dynamic_init": "Initializing",
|
||||
"dynamic_next": "Volgende",
|
||||
"dynamic_prev": "Vorige",
|
||||
"dynamic_start": "Start",
|
||||
|
|
|
@ -176,7 +176,7 @@
|
|||
"details_total_exceeds_balance": "Čiastka, ktorú chcete poslať, presahuje dostupný zostatok.",
|
||||
"details_wallet_before_tx": "Pred vytvorením transakcie potrebujete najprv pridať Bitcoinovú peňaženku.",
|
||||
"details_wallet_selection": "Výber peňaženky",
|
||||
"dynamic_init": "Initialing",
|
||||
"dynamic_init": "Initializing",
|
||||
"dynamic_next": "Next",
|
||||
"dynamic_prev": "Previous",
|
||||
"dynamic_start": "Start",
|
||||
|
|
|
@ -176,7 +176,7 @@
|
|||
"details_total_exceeds_balance": "Beloppet överstiger plånbokens tillgängliga belopp",
|
||||
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
|
||||
"details_wallet_selection": "Wallet Selection",
|
||||
"dynamic_init": "Initialing",
|
||||
"dynamic_init": "Initializing",
|
||||
"dynamic_next": "Nästa",
|
||||
"dynamic_prev": "Föregående",
|
||||
"dynamic_start": "Starta",
|
||||
|
|
|
@ -176,7 +176,7 @@
|
|||
"details_total_exceeds_balance": "Gönderme miktarı mevcut bakiyeyi aşıyor.",
|
||||
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
|
||||
"details_wallet_selection": "Wallet Selection",
|
||||
"dynamic_init": "Initialing",
|
||||
"dynamic_init": "Initializing",
|
||||
"dynamic_next": "Next",
|
||||
"dynamic_prev": "Previous",
|
||||
"dynamic_start": "Start",
|
||||
|
|
|
@ -176,7 +176,7 @@
|
|||
"details_total_exceeds_balance": "余额不足",
|
||||
"details_wallet_before_tx": "Before creating a transaction, you must first add a Bitcoin wallet.",
|
||||
"details_wallet_selection": "Wallet Selection",
|
||||
"dynamic_init": "Initialing",
|
||||
"dynamic_init": "Initializing",
|
||||
"dynamic_next": "Next",
|
||||
"dynamic_prev": "Previous",
|
||||
"dynamic_start": "Start",
|
||||
|
|
15
package-lock.json
generated
15
package-lock.json
generated
|
@ -6674,15 +6674,13 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
|
||||
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
|
||||
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-extglob": "^1.0.0"
|
||||
}
|
||||
|
@ -9444,7 +9442,6 @@
|
|||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
|
||||
"integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-glob": "^2.0.0"
|
||||
},
|
||||
|
@ -9453,15 +9450,13 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
|
||||
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
|
||||
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-extglob": "^1.0.0"
|
||||
}
|
||||
|
@ -13952,8 +13947,7 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
|
||||
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "2.0.1",
|
||||
|
@ -15502,8 +15496,7 @@
|
|||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
"e2e:debug": "(test -f android/app/build/outputs/apk/debug/app-debug.apk && test -f android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk) || npm run e2e:debug-build; npm run e2e:debug-test",
|
||||
"e2e:release-build": "npx detox build -c android.emu.release",
|
||||
"e2e:release-test": "detox test -c android.emu.release --record-videos all --take-screenshots all --headless",
|
||||
"lint": "eslint *.js screen/**/*.js blue_modules/*.js class/**/*.js models/ loc/ tests/**/*.js",
|
||||
"lint": "eslint *.js screen/**/*.js blue_modules/*.js class/**/*.js models/ loc/ tests/**/*.js components/**/*.js",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"lint:quickfix": "git status --porcelain | grep -v '\\.json' | grep '\\.js' --color=never | awk '{print $2}' | xargs eslint --fix; exit 0",
|
||||
"unit": "jest tests/unit/*"
|
||||
|
|
|
@ -133,25 +133,21 @@ const ScanQRCode = () => {
|
|||
|
||||
const showFilePicker = async () => {
|
||||
try {
|
||||
const res = await DocumentPicker.pick({
|
||||
type:
|
||||
Platform.OS === 'ios'
|
||||
? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', DocumentPicker.types.plainText, 'public.json']
|
||||
: [DocumentPicker.types.allFiles],
|
||||
});
|
||||
setIsLoading(true);
|
||||
const res = await DocumentPicker.pick();
|
||||
const file = await RNFS.readFile(res.uri);
|
||||
const fileParsed = JSON.parse(file);
|
||||
if (fileParsed.keystore.xpub) {
|
||||
let masterFingerprint;
|
||||
if (fileParsed.keystore.ckcc_xfp) {
|
||||
masterFingerprint = Number(fileParsed.keystore.ckcc_xfp);
|
||||
}
|
||||
onBarCodeRead({ data: fileParsed.keystore.xpub, additionalProperties: { masterFingerprint, label: fileParsed.keystore.label } });
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
onBarCodeRead({ data: file });
|
||||
} catch (err) {
|
||||
if (!DocumentPicker.isCancel(err)) {
|
||||
alert(loc.send.qr_error_no_wallet);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const showImagePicker = () => {
|
||||
|
|
|
@ -38,7 +38,7 @@ import * as bitcoin from 'bitcoinjs-lib';
|
|||
|
||||
import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees';
|
||||
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||
import { AppStorage, HDSegwitBech32Wallet, LightningCustodianWallet, WatchOnlyWallet } from '../../class';
|
||||
import { AppStorage, HDSegwitBech32Wallet, LightningCustodianWallet, MultisigHDWallet, WatchOnlyWallet } from '../../class';
|
||||
import { BitcoinTransaction } from '../../models/bitcoinTransactionInfo';
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
|
||||
|
@ -47,6 +47,7 @@ import { BlueCurrentTheme } from '../../components/themes';
|
|||
const currency = require('../../blue_modules/currency');
|
||||
const BlueApp: AppStorage = require('../../BlueApp');
|
||||
const prompt = require('../../blue_modules/prompt');
|
||||
const fs = require('../../blue_modules/fs');
|
||||
|
||||
const btcAddressRx = /^[a-zA-Z0-9]{26,35}$/;
|
||||
|
||||
|
@ -588,6 +589,16 @@ export default class SendDetails extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
if (wallet.type === MultisigHDWallet.type) {
|
||||
this.props.navigation.navigate('PsbtMultisig', {
|
||||
memo: this.state.memo,
|
||||
psbtBase64: psbt.toBase64(),
|
||||
walletId: wallet.getID(),
|
||||
});
|
||||
this.setState({ isLoading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
BlueApp.tx_metadata = BlueApp.tx_metadata || {};
|
||||
BlueApp.tx_metadata[tx.getId()] = {
|
||||
txhex: tx.toHex(),
|
||||
|
@ -772,7 +783,10 @@ export default class SendDetails extends Component {
|
|||
|
||||
try {
|
||||
const res = await DocumentPicker.pick({
|
||||
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn'] : [DocumentPicker.types.allFiles],
|
||||
type:
|
||||
Platform.OS === 'ios'
|
||||
? ['io.bluewallet.psbt', 'io.bluewallet.psbt.txn', DocumentPicker.types.plainText, 'public.json']
|
||||
: [DocumentPicker.types.allFiles],
|
||||
});
|
||||
|
||||
if (DeeplinkSchemaMatch.isPossiblySignedPSBTFile(res.uri)) {
|
||||
|
@ -818,6 +832,21 @@ export default class SendDetails extends Component {
|
|||
}
|
||||
};
|
||||
|
||||
importTransactionMultisig = async () => {
|
||||
try {
|
||||
const base64 = await fs.openSignedTransaction();
|
||||
const psbt = bitcoin.Psbt.fromBase64(base64); // if it doesnt throw - all good, its valid
|
||||
this.props.navigation.navigate('PsbtMultisig', {
|
||||
memo: this.state.memo,
|
||||
psbtBase64: psbt.toBase64(),
|
||||
walletId: this.state.fromWallet.getID(),
|
||||
});
|
||||
} catch (error) {
|
||||
alert(loc.send.problem_with_psbt + ': ' + error.message);
|
||||
}
|
||||
this.setState({ isLoading: false, isAdvancedTransactionOptionsVisible: false });
|
||||
};
|
||||
|
||||
handleAddRecipient = () => {
|
||||
const { addresses } = this.state;
|
||||
addresses.push(new BitcoinTransaction());
|
||||
|
@ -893,6 +922,14 @@ export default class SendDetails extends Component {
|
|||
onPress={this.importTransaction}
|
||||
/>
|
||||
)}
|
||||
{this.state.fromWallet.type === MultisigHDWallet.type && (
|
||||
<BlueListItem
|
||||
title={loc.send.details_adv_import}
|
||||
hideChevron
|
||||
component={TouchableOpacity}
|
||||
onPress={this.importTransactionMultisig}
|
||||
/>
|
||||
)}
|
||||
{this.state.fromWallet.allowBatchSend() && (
|
||||
<>
|
||||
<BlueListItem
|
||||
|
|
501
screen/send/psbtMultisig.js
Normal file
501
screen/send/psbtMultisig.js
Normal file
|
@ -0,0 +1,501 @@
|
|||
/* global alert */
|
||||
import React, { useState } from 'react';
|
||||
import { FlatList, Platform, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { BlueButton, BlueButtonLink, BlueCard, BlueNavigationStyle, BlueSpacing20, BlueText, SafeBlueArea } from '../../BlueComponents';
|
||||
import { DynamicQRCode } from '../../components/DynamicQRCode';
|
||||
import { SquareButton } from '../../components/SquareButton';
|
||||
import { getSystemName } from 'react-native-device-info';
|
||||
import { decodeUR, extractSingleWorkload } from 'bc-ur/dist';
|
||||
import loc from '../../loc';
|
||||
import { Icon } from 'react-native-elements';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import ScanQRCode from './ScanQRCode';
|
||||
import { useNavigation, useRoute, useTheme } from '@react-navigation/native';
|
||||
import { BitcoinUnit } from '../../models/bitcoinUnits';
|
||||
|
||||
const BlueApp = require('../../BlueApp');
|
||||
const bitcoin = require('bitcoinjs-lib');
|
||||
const currency = require('../../blue_modules/currency');
|
||||
const fs = require('../../blue_modules/fs');
|
||||
const LocalQRCode = require('@remobile/react-native-qrcode-local-image');
|
||||
const isDesktop = getSystemName() === 'Mac OS X';
|
||||
const BigNumber = require('bignumber.js');
|
||||
|
||||
const shortenAddress = addr => {
|
||||
return addr.substr(0, Math.floor(addr.length / 2) - 1) + '\n' + addr.substr(Math.floor(addr.length / 2) - 1, addr.length);
|
||||
};
|
||||
|
||||
const PsbtMultisig = () => {
|
||||
const navigation = useNavigation();
|
||||
const route = useRoute();
|
||||
const { colors } = useTheme();
|
||||
const [flatListHeight, setFlatListHeight] = useState(0);
|
||||
|
||||
const walletId = route.params.walletId;
|
||||
const psbtBase64 = route.params.psbtBase64;
|
||||
const memo = route.params.memo;
|
||||
|
||||
const [psbt, setPsbt] = useState(bitcoin.Psbt.fromBase64(psbtBase64));
|
||||
const [animatedQRCodeData, setAnimatedQRCodeData] = useState({});
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const stylesHook = StyleSheet.create({
|
||||
root: {
|
||||
backgroundColor: colors.elevated,
|
||||
},
|
||||
textBtc: {
|
||||
color: colors.buttonAlternativeTextColor,
|
||||
},
|
||||
textDestinationFirstFour: {
|
||||
color: colors.buttonAlternativeTextColor,
|
||||
},
|
||||
textBtcUnitValue: {
|
||||
color: colors.buttonAlternativeTextColor,
|
||||
},
|
||||
textDestination: {
|
||||
color: colors.foregroundColor,
|
||||
},
|
||||
modalContentShort: {
|
||||
backgroundColor: colors.elevated,
|
||||
},
|
||||
textFiat: {
|
||||
color: colors.alternativeTextColor,
|
||||
},
|
||||
provideSignatureButton: {
|
||||
backgroundColor: colors.buttonDisabledBackgroundColor,
|
||||
},
|
||||
exportButton: {
|
||||
backgroundColor: colors.buttonDisabledBackgroundColor,
|
||||
},
|
||||
provideSignatureButtonText: {
|
||||
color: colors.buttonTextColor,
|
||||
},
|
||||
vaultKeyCircle: {
|
||||
backgroundColor: colors.buttonDisabledBackgroundColor,
|
||||
},
|
||||
vaultKeyText: {
|
||||
color: colors.alternativeTextColor,
|
||||
},
|
||||
feeFiatText: {
|
||||
color: colors.alternativeTextColor,
|
||||
},
|
||||
vaultKeyCircleSuccess: {
|
||||
backgroundColor: colors.msSuccessBG,
|
||||
},
|
||||
vaultKeyTextSigned: {
|
||||
color: colors.msSuccessBG,
|
||||
},
|
||||
});
|
||||
/** @type MultisigHDWallet */
|
||||
const wallet = BlueApp.getWallets().find(w => w.getID() === walletId);
|
||||
let destination = [];
|
||||
let totalSat = 0;
|
||||
const targets = [];
|
||||
for (const output of psbt.txOutputs) {
|
||||
if (output.address && !wallet.weOwnAddress(output.address)) {
|
||||
totalSat += output.value;
|
||||
destination.push(output.address);
|
||||
targets.push({ address: output.address, value: output.value });
|
||||
}
|
||||
}
|
||||
destination = shortenAddress(destination.join(', '));
|
||||
const totalBtc = new BigNumber(totalSat).dividedBy(100000000).toNumber();
|
||||
const totalFiat = currency.satoshiToLocalCurrency(totalSat);
|
||||
const fileName = `${Date.now()}.psbt`;
|
||||
|
||||
const howManySignaturesWeHave = () => {
|
||||
return wallet.calculateHowManySignaturesWeHaveFromPsbt(psbt);
|
||||
};
|
||||
|
||||
const getFee = () => {
|
||||
return wallet.calculateFeeFromPsbt(psbt);
|
||||
};
|
||||
|
||||
const _renderItem = el => {
|
||||
if (el.index >= howManySignaturesWeHave()) return _renderItemUnsigned(el);
|
||||
else return _renderItemSigned(el);
|
||||
};
|
||||
|
||||
const _renderItemUnsigned = el => {
|
||||
const renderProvideSignature = el.index === howManySignaturesWeHave();
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.itemUnsignedWrapper}>
|
||||
<View style={[styles.vaultKeyCircle, stylesHook.vaultKeyCircle]}>
|
||||
<Text style={[styles.vaultKeyText, stylesHook.vaultKeyText]}>{el.index + 1}</Text>
|
||||
</View>
|
||||
<View style={styles.vaultKeyTextWrapper}>
|
||||
<Text style={[styles.vaultKeyText, stylesHook.vaultKeyText]}>
|
||||
{loc.formatString(loc.multisig.vault_key, { number: el.index + 1 })}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{renderProvideSignature && (
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
style={[styles.provideSignatureButton, stylesHook.provideSignatureButton]}
|
||||
onPress={() => {
|
||||
setIsModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<Text style={[styles.provideSignatureButtonText, stylesHook.provideSignatureButtonText]}>
|
||||
{loc.multisig.provide_signature}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const _renderItemSigned = el => {
|
||||
return (
|
||||
<View style={styles.flexDirectionRow}>
|
||||
<View style={[styles.vaultKeyCircleSuccess, stylesHook.vaultKeyCircleSuccess]}>
|
||||
<Icon size={24} name="check" type="ionicons" color={colors.msSuccessCheck} />
|
||||
</View>
|
||||
<View style={styles.vaultKeyTextSignedWrapper}>
|
||||
<Text style={[styles.vaultKeyTextSigned, stylesHook.vaultKeyTextSigned]}>
|
||||
{loc.formatString(loc.multisig.vault_key, { number: el.index + 1 })}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const _onReadUniformResource = ur => {
|
||||
try {
|
||||
const [index, total] = extractSingleWorkload(ur);
|
||||
animatedQRCodeData[index + 'of' + total] = ur;
|
||||
if (Object.values(animatedQRCodeData).length === total) {
|
||||
const payload = decodeUR(Object.values(animatedQRCodeData));
|
||||
const psbtB64 = Buffer.from(payload, 'hex').toString('base64');
|
||||
_combinePSBT(psbtB64);
|
||||
} else {
|
||||
setAnimatedQRCodeData(animatedQRCodeData);
|
||||
}
|
||||
} catch (Err) {
|
||||
alert(loc._.invalid_animated_qr_code_fragment);
|
||||
}
|
||||
};
|
||||
|
||||
const _combinePSBT = receivedPSBTBase64 => {
|
||||
const receivedPSBT = bitcoin.Psbt.fromBase64(receivedPSBTBase64);
|
||||
try {
|
||||
const newPsbt = psbt.combine(receivedPSBT);
|
||||
navigation.dangerouslyGetParent().pop();
|
||||
setPsbt(newPsbt);
|
||||
setIsModalVisible(false);
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
}
|
||||
};
|
||||
|
||||
const onBarScanned = ret => {
|
||||
if (!ret.data) ret = { data: ret };
|
||||
if (ret.data.toUpperCase().startsWith('UR')) {
|
||||
return _onReadUniformResource(ret.data);
|
||||
} else if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
|
||||
// this looks like NOT base64, so maybe its transaction's hex
|
||||
// we dont support it in this flow
|
||||
} else {
|
||||
// psbt base64?
|
||||
_combinePSBT(ret.data);
|
||||
}
|
||||
};
|
||||
|
||||
const onConfirm = () => {
|
||||
try {
|
||||
psbt.finalizeAllInputs();
|
||||
} catch (_) {} // ignore if it is already finalized
|
||||
|
||||
try {
|
||||
const tx = psbt.extractTransaction().toHex();
|
||||
const satoshiPerByte = Math.round(getFee() / (tx.length / 2));
|
||||
navigation.navigate('Confirm', {
|
||||
fee: new BigNumber(getFee()).dividedBy(100000000).toNumber(),
|
||||
memo: memo,
|
||||
fromWallet: wallet,
|
||||
tx,
|
||||
recipients: targets,
|
||||
satoshiPerByte,
|
||||
});
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
}
|
||||
};
|
||||
|
||||
const openScanner = () => {
|
||||
if (isDesktop) {
|
||||
ImagePicker.launchCamera(
|
||||
{
|
||||
title: null,
|
||||
mediaType: 'photo',
|
||||
takePhotoButtonTitle: null,
|
||||
},
|
||||
response => {
|
||||
if (response.uri) {
|
||||
const uri = Platform.OS === 'ios' ? response.uri.toString().replace('file://', '') : response.path.toString();
|
||||
LocalQRCode.decode(uri, (error, result) => {
|
||||
if (!error) {
|
||||
onBarScanned(result);
|
||||
} else {
|
||||
alert(loc.send.qr_error_no_qrcode);
|
||||
}
|
||||
});
|
||||
} else if (response.error) {
|
||||
ScanQRCode.presentCameraNotAuthorizedAlert(response.error);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
navigation.navigate('ScanQRCodeRoot', {
|
||||
screen: 'ScanQRCode',
|
||||
params: {
|
||||
onBarScanned: onBarScanned,
|
||||
showFileImportButton: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const exportPSBT = async () => {
|
||||
await fs.writeFileAndExport(fileName, psbt.toBase64());
|
||||
};
|
||||
|
||||
const isConfirmEnabled = () => {
|
||||
return howManySignaturesWeHave() >= wallet.getM();
|
||||
};
|
||||
|
||||
const renderDynamicQrCode = () => {
|
||||
return (
|
||||
<SafeBlueArea style={[styles.root, stylesHook.root]}>
|
||||
<ScrollView centerContent contentContainerStyle={styles.scrollViewContent}>
|
||||
<View style={[styles.modalContentShort, stylesHook.modalContentShort]}>
|
||||
<DynamicQRCode value={psbt.toHex()} capacity={666} />
|
||||
<BlueSpacing20 />
|
||||
<SquareButton
|
||||
style={[styles.exportButton, stylesHook.exportButton]}
|
||||
onPress={openScanner}
|
||||
title={loc.multisig.scan_or_import_file}
|
||||
/>
|
||||
<BlueSpacing20 />
|
||||
<SquareButton style={[styles.exportButton, stylesHook.exportButton]} onPress={exportPSBT} title={loc.multisig.share} />
|
||||
<BlueSpacing20 />
|
||||
<BlueButtonLink title={loc._.cancel} onPress={() => setIsModalVisible(false)} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeBlueArea>
|
||||
);
|
||||
};
|
||||
|
||||
const destinationAddress = () => {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let destinationAddressView = [];
|
||||
const destinations = Object.entries(destination.split(','));
|
||||
for (const [index, address] of destinations) {
|
||||
if (index > 1) {
|
||||
destinationAddressView.push(
|
||||
<View style={styles.destionationTextContainer} key={`end-${index}`}>
|
||||
<Text style={[styles.textDestinationFirstFour, stylesHook.textFiat]}>and {destinations.length - 2} more...</Text>
|
||||
</View>,
|
||||
);
|
||||
break;
|
||||
} else {
|
||||
const currentAddress = address.replace(/\s/g, '');
|
||||
const firstFour = currentAddress.substring(0, 5);
|
||||
const lastFour = currentAddress.substring(currentAddress.length - 5, currentAddress.length);
|
||||
const middle = currentAddress.split(firstFour)[1].split(lastFour)[0];
|
||||
destinationAddressView.push(
|
||||
<View style={styles.destionationTextContainer} key={`${currentAddress}-${index}`}>
|
||||
<Text style={[styles.textDestinationFirstFour, stylesHook.textBtc]}>{firstFour}</Text>
|
||||
<View style={styles.textDestinationSpacingRight} />
|
||||
<Text style={[styles.textDestinationFirstFour, stylesHook.textFiat]}>{middle}</Text>
|
||||
<View style={styles.textDestinationSpacingLeft} />
|
||||
<Text style={[styles.textDestinationFirstFour, stylesHook.textBtc]}>{lastFour}</Text>
|
||||
</View>,
|
||||
);
|
||||
}
|
||||
}
|
||||
return destinationAddressView;
|
||||
};
|
||||
|
||||
const header = (
|
||||
<View style={stylesHook.root}>
|
||||
<View style={styles.containerText}>
|
||||
<BlueText style={[styles.textBtc, stylesHook.textBtc]}>{totalBtc}</BlueText>
|
||||
<View style={styles.textBtcUnit}>
|
||||
<BlueText style={[styles.textBtcUnitValue, stylesHook.textBtcUnitValue]}> {BitcoinUnit.BTC}</BlueText>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.containerText}>
|
||||
<BlueText style={[styles.textFiat, stylesHook.textFiat]}>{totalFiat}</BlueText>
|
||||
</View>
|
||||
<View>{destinationAddress()}</View>
|
||||
</View>
|
||||
);
|
||||
const footer = (
|
||||
<View style={styles.bottomWrapper}>
|
||||
<View style={styles.bottomFeesWrapper}>
|
||||
<BlueText style={[styles.feeFiatText, stylesHook.feeFiatText]}>
|
||||
{loc.formatString(loc.multisig.fee, { number: currency.satoshiToLocalCurrency(getFee()) })} -{' '}
|
||||
</BlueText>
|
||||
<BlueText>{loc.formatString(loc.multisig.fee_btc, { number: currency.satoshiToBTC(getFee()) })}</BlueText>
|
||||
</View>
|
||||
<BlueButton disabled={!isConfirmEnabled()} title={loc.multisig.confirm} onPress={onConfirm} />
|
||||
</View>
|
||||
);
|
||||
|
||||
if (isModalVisible) return renderDynamicQrCode();
|
||||
|
||||
const onLayout = e => {
|
||||
setFlatListHeight(e.nativeEvent.layout.height);
|
||||
};
|
||||
|
||||
const data = new Array(wallet.getM());
|
||||
return (
|
||||
<SafeBlueArea style={[styles.root, stylesHook.root]}>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.mstopcontainer}>
|
||||
<View style={styles.mscontainer}>
|
||||
<View style={[styles.msleft, { height: flatListHeight - 200 }]} />
|
||||
</View>
|
||||
<View style={styles.msright}>
|
||||
<BlueCard>
|
||||
<FlatList
|
||||
data={data}
|
||||
onLayout={onLayout}
|
||||
renderItem={_renderItem}
|
||||
keyExtractor={(_item, index) => `${index}`}
|
||||
ListHeaderComponent={header}
|
||||
scrollEnabled={false}
|
||||
/>
|
||||
</BlueCard>
|
||||
</View>
|
||||
</View>
|
||||
{footer}
|
||||
</View>
|
||||
</SafeBlueArea>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
flex: 1,
|
||||
},
|
||||
mstopcontainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
mscontainer: {
|
||||
flex: 10,
|
||||
},
|
||||
msleft: {
|
||||
width: 1,
|
||||
borderStyle: 'dashed',
|
||||
borderWidth: 0.8,
|
||||
borderColor: '#c4c4c4',
|
||||
marginLeft: 40,
|
||||
marginTop: 185,
|
||||
},
|
||||
msright: {
|
||||
flex: 90,
|
||||
marginLeft: '-11%',
|
||||
},
|
||||
scrollViewContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'column',
|
||||
paddingTop: 24,
|
||||
flex: 1,
|
||||
},
|
||||
containerText: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
destionationTextContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 4,
|
||||
},
|
||||
textFiat: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
marginBottom: 30,
|
||||
},
|
||||
textBtc: {
|
||||
fontWeight: 'bold',
|
||||
fontSize: 30,
|
||||
},
|
||||
textDestinationFirstFour: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
textDestination: {
|
||||
paddingTop: 10,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
bottomModal: {
|
||||
justifyContent: 'flex-end',
|
||||
margin: 0,
|
||||
},
|
||||
modalContentShort: {
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
},
|
||||
copyToClipboard: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
exportButton: {
|
||||
height: 48,
|
||||
borderRadius: 8,
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
provideSignatureButton: {
|
||||
marginTop: 24,
|
||||
marginLeft: 40,
|
||||
height: 48,
|
||||
borderRadius: 8,
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 8,
|
||||
},
|
||||
provideSignatureButtonText: { fontWeight: '600', fontSize: 15 },
|
||||
vaultKeyText: { fontSize: 18, fontWeight: 'bold' },
|
||||
vaultKeyTextWrapper: { justifyContent: 'center', alignItems: 'center', paddingLeft: 16 },
|
||||
vaultKeyCircle: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 25,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
vaultKeyCircleSuccess: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 25,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
itemUnsignedWrapper: { flexDirection: 'row', paddingTop: 16 },
|
||||
textDestinationSpacingRight: { marginRight: 4 },
|
||||
textDestinationSpacingLeft: { marginLeft: 4 },
|
||||
vaultKeyTextSigned: { fontSize: 18, fontWeight: 'bold' },
|
||||
vaultKeyTextSignedWrapper: { justifyContent: 'center', alignItems: 'center', paddingLeft: 16 },
|
||||
flexDirectionRow: { flexDirection: 'row', paddingVertical: 12 },
|
||||
textBtcUnit: { justifyContent: 'flex-end', bottom: 8 },
|
||||
bottomFeesWrapper: { flexDirection: 'row', paddingBottom: 20 },
|
||||
bottomWrapper: { justifyContent: 'center', alignItems: 'center', paddingVertical: 20 },
|
||||
});
|
||||
|
||||
PsbtMultisig.navigationOptions = () => ({
|
||||
...BlueNavigationStyle(null, false),
|
||||
title: loc.multisig.header,
|
||||
});
|
||||
|
||||
export default PsbtMultisig;
|
|
@ -23,7 +23,7 @@ import { HDLegacyP2PKHWallet } from '../../class/wallets/hd-legacy-p2pkh-wallet'
|
|||
import { HDSegwitP2SHWallet } from '../../class/wallets/hd-segwit-p2sh-wallet';
|
||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||
import Biometric from '../../class/biometrics';
|
||||
import { HDSegwitBech32Wallet, SegwitP2SHWallet, LegacyWallet, SegwitBech32Wallet, WatchOnlyWallet } from '../../class';
|
||||
import { HDSegwitBech32Wallet, SegwitP2SHWallet, LegacyWallet, SegwitBech32Wallet, WatchOnlyWallet, MultisigHDWallet } from '../../class';
|
||||
import { ScrollView } from 'react-native-gesture-handler';
|
||||
import loc from '../../loc';
|
||||
import { useTheme, useRoute, useNavigation } from '@react-navigation/native';
|
||||
|
@ -182,6 +182,11 @@ const WalletDetails = () => {
|
|||
wallet,
|
||||
});
|
||||
};
|
||||
const navigateToMultisigCoordinationSetup = () => {
|
||||
navigate('ExportMultisigCoordinationSetup', {
|
||||
walletId: wallet.getID(),
|
||||
});
|
||||
};
|
||||
const navigateToXPub = () =>
|
||||
navigate('WalletXpub', {
|
||||
secret: wallet.getSecret(),
|
||||
|
@ -360,6 +365,35 @@ const WalletDetails = () => {
|
|||
<BlueSpacing20 />
|
||||
<Text style={[styles.textLabel1, stylesHook.textLabel1]}>{loc.wallets.details_type.toLowerCase()}</Text>
|
||||
<Text style={[styles.textValue, stylesHook.textValue]}>{wallet.typeReadable}</Text>
|
||||
|
||||
{wallet.type === MultisigHDWallet.type && (
|
||||
<>
|
||||
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>multisig</Text>
|
||||
<BlueText>
|
||||
{wallet.getM()} of {wallet.getN()}{' '}
|
||||
{wallet.isNativeSegwit()
|
||||
? 'native segwit (p2wsh)'
|
||||
: wallet.isWrappedSegwit()
|
||||
? 'wrapped segwit (p2sh-p2wsh)'
|
||||
: 'legacy (p2sh)'}
|
||||
</BlueText>
|
||||
</>
|
||||
)}
|
||||
|
||||
{wallet.type === MultisigHDWallet.type && wallet.howManySignaturesCanWeMake() > 0 && (
|
||||
<>
|
||||
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>{loc.multisig.how_many_signatures_can_bluewallet_make}</Text>
|
||||
<BlueText>{wallet.howManySignaturesCanWeMake()}</BlueText>
|
||||
</>
|
||||
)}
|
||||
|
||||
{wallet.type === MultisigHDWallet.type && !!wallet.getDerivationPath() && (
|
||||
<>
|
||||
<Text style={[styles.textLabel2, stylesHook.textLabel2]}>derivation path</Text>
|
||||
<BlueText>{wallet.getDerivationPath()}</BlueText>
|
||||
</>
|
||||
)}
|
||||
|
||||
{wallet.type === LightningCustodianWallet.type && (
|
||||
<>
|
||||
<Text style={[styles.textLabel1, stylesHook.textLabel1]}>{loc.wallets.details_connected_to.toLowerCase()}</Text>
|
||||
|
@ -400,6 +434,15 @@ const WalletDetails = () => {
|
|||
|
||||
<BlueSpacing20 />
|
||||
|
||||
{wallet.type === MultisigHDWallet.type && (
|
||||
<>
|
||||
<SecondButton
|
||||
onPress={navigateToMultisigCoordinationSetup}
|
||||
title={loc.multisig.export_coordination_setup.replace(/^\w/, c => c.toUpperCase())}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(wallet.type === HDLegacyBreadwalletWallet.type ||
|
||||
wallet.type === HDLegacyP2PKHWallet.type ||
|
||||
wallet.type === HDSegwitBech32Wallet.type ||
|
||||
|
|
128
screen/wallets/exportMultisigCoordinationSetup.js
Normal file
128
screen/wallets/exportMultisigCoordinationSetup.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { ActivityIndicator, InteractionManager, ScrollView, StatusBar, StyleSheet, useWindowDimensions, View } from 'react-native';
|
||||
import QRCode from 'react-native-qrcode-svg';
|
||||
import { BlueNavigationStyle, BlueSpacing20, BlueText, SafeBlueArea } from '../../BlueComponents';
|
||||
import Privacy from '../../Privacy';
|
||||
import Biometric from '../../class/biometrics';
|
||||
import loc from '../../loc';
|
||||
import { encodeUR } from '../../blue_modules/bc-ur/dist';
|
||||
import { useFocusEffect, useNavigation, useRoute, useTheme } from '@react-navigation/native';
|
||||
import { SquareButton } from '../../components/SquareButton';
|
||||
|
||||
const BlueApp = require('../../BlueApp');
|
||||
const fs = require('../../blue_modules/fs');
|
||||
|
||||
const ExportMultisigCoordinationSetup = () => {
|
||||
const walletId = useRoute().params.walletId;
|
||||
const wallet = BlueApp.getWallets().find(w => w.getID() === walletId);
|
||||
const qrCodeContents = encodeUR(Buffer.from(wallet.getXpub(), 'ascii').toString('hex'), 77777)[0];
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { goBack } = useNavigation();
|
||||
const { colors } = useTheme();
|
||||
const { width, height } = useWindowDimensions();
|
||||
const stylesHook = {
|
||||
...styles,
|
||||
loading: {
|
||||
...styles.loading,
|
||||
backgroundColor: colors.elevated,
|
||||
},
|
||||
root: {
|
||||
...styles.root,
|
||||
backgroundColor: colors.elevated,
|
||||
},
|
||||
type: { ...styles.type, color: colors.foregroundColor },
|
||||
secret: { ...styles.secret, color: colors.foregroundColor },
|
||||
};
|
||||
|
||||
const exportTxtFile = async () => {
|
||||
await fs.writeFileAndExport(wallet.getLabel() + '.txt', wallet.getXpub());
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
Privacy.enableBlur();
|
||||
const task = InteractionManager.runAfterInteractions(async () => {
|
||||
if (wallet) {
|
||||
const isBiometricsEnabled = await Biometric.isBiometricUseCapableAndEnabled();
|
||||
|
||||
if (isBiometricsEnabled) {
|
||||
if (!(await Biometric.unlockWithBiometrics())) {
|
||||
return goBack();
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
task.cancel();
|
||||
Privacy.disableBlur();
|
||||
};
|
||||
}, [goBack, wallet]),
|
||||
);
|
||||
|
||||
return isLoading ? (
|
||||
<View style={stylesHook.loading}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
) : (
|
||||
<SafeBlueArea style={stylesHook.root}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<ScrollView contentContainerStyle={styles.scrollViewContent}>
|
||||
<View>
|
||||
<BlueText style={stylesHook.type}>{wallet.getLabel()}</BlueText>
|
||||
</View>
|
||||
<BlueSpacing20 />
|
||||
<View style={styles.activeQrcode}>
|
||||
<QRCode
|
||||
value={qrCodeContents}
|
||||
size={height > width ? width - 40 : width / 2}
|
||||
logoSize={70}
|
||||
color="#000000"
|
||||
logoBackgroundColor={colors.brandingColor}
|
||||
backgroundColor="#FFFFFF"
|
||||
ecl="H"
|
||||
/>
|
||||
<BlueSpacing20 />
|
||||
<SquareButton backgroundColor="#EEF0F4" onPress={exportTxtFile} title={loc.multisig.share} />
|
||||
</View>
|
||||
<BlueSpacing20 />
|
||||
<BlueText style={stylesHook.secret}>{wallet.getXpub()}</BlueText>
|
||||
</ScrollView>
|
||||
</SafeBlueArea>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loading: {
|
||||
flex: 1,
|
||||
paddingTop: 20,
|
||||
},
|
||||
root: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollViewContent: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexGrow: 1,
|
||||
},
|
||||
activeQrcode: { borderWidth: 6, borderRadius: 8, borderColor: '#FFFFFF' },
|
||||
type: {
|
||||
fontSize: 17,
|
||||
fontWeight: '700',
|
||||
},
|
||||
secret: {
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
},
|
||||
});
|
||||
|
||||
ExportMultisigCoordinationSetup.navigationOptions = ({ navigation }) => ({
|
||||
...BlueNavigationStyle(navigation, true),
|
||||
title: loc.multisig.export_coordination_setup,
|
||||
headerLeft: null,
|
||||
});
|
||||
|
||||
export default ExportMultisigCoordinationSetup;
|
|
@ -3,7 +3,7 @@ import { View, ActivityIndicator, Image, Text, StyleSheet, StatusBar, ScrollView
|
|||
import { BlueNavigationStyle } from '../../BlueComponents';
|
||||
import SortableList from 'react-native-sortable-list';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import { PlaceholderWallet, LightningCustodianWallet } from '../../class';
|
||||
import { PlaceholderWallet, LightningCustodianWallet, MultisigHDWallet } from '../../class';
|
||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||
import WalletGradient from '../../class/wallet-gradient';
|
||||
import loc, { formatBalance, transactionTimeToReadable } from '../../loc';
|
||||
|
@ -123,9 +123,16 @@ const ReorderWallets = () => {
|
|||
<View shadowOpacity={40 / 100} shadowOffset={{ width: 0, height: 0 }} shadowRadius={5} style={styles.itemRoot}>
|
||||
<LinearGradient shadowColor="#000000" colors={WalletGradient.gradientsFor(item.type)} style={styles.gradient}>
|
||||
<Image
|
||||
source={
|
||||
(LightningCustodianWallet.type === item.type && require('../../img/lnd-shape.png')) || require('../../img/btc-shape.png')
|
||||
source={(() => {
|
||||
switch (item.type) {
|
||||
case LightningCustodianWallet.type:
|
||||
return require('../../img/lnd-shape.png');
|
||||
case MultisigHDWallet.type:
|
||||
return require('../../img/vault-shape.png');
|
||||
default:
|
||||
return require('../../img/btc-shape.png');
|
||||
}
|
||||
})()}
|
||||
style={styles.image}
|
||||
/>
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
|||
import WalletGradient from '../../class/wallet-gradient';
|
||||
import { useRoute, useTheme } from '@react-navigation/native';
|
||||
import loc, { formatBalance, transactionTimeToReadable } from '../../loc';
|
||||
import { MultisigHDWallet } from '../../class';
|
||||
/** @type {AppStorage} */
|
||||
const BlueApp = require('../../BlueApp');
|
||||
|
||||
|
@ -104,9 +105,16 @@ const SelectWallet = ({ navigation }) => {
|
|||
<View shadowOpacity={40 / 100} shadowOffset={{ width: 0, height: 0 }} shadowRadius={5} style={styles.itemRoot}>
|
||||
<LinearGradient shadowColor="#000000" colors={WalletGradient.gradientsFor(item.type)} style={styles.gradient}>
|
||||
<Image
|
||||
source={
|
||||
(LightningCustodianWallet.type === item.type && require('../../img/lnd-shape.png')) || require('../../img/btc-shape.png')
|
||||
source={(() => {
|
||||
switch (item.type) {
|
||||
case LightningCustodianWallet.type:
|
||||
return require('../../img/lnd-shape.png');
|
||||
case MultisigHDWallet.type:
|
||||
return require('../../img/vault-shape.png');
|
||||
default:
|
||||
return require('../../img/btc-shape.png');
|
||||
}
|
||||
})()}
|
||||
style={styles.image}
|
||||
/>
|
||||
|
||||
|
|
51
tests/integration/multisig-hd-wallet.test.js
Normal file
51
tests/integration/multisig-hd-wallet.test.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
/* global it, describe, jasmine, afterAll, beforeAll */
|
||||
import assert from 'assert';
|
||||
import { MultisigHDWallet } from '../../class/';
|
||||
const BlueElectrum = require('../../blue_modules/BlueElectrum'); // so it connects ASAP
|
||||
global.net = require('net'); // needed by Electrum client. For RN it is proviced in shim.js
|
||||
global.tls = require('tls'); // needed by Electrum client. For RN it is proviced in shim.js
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 300 * 1000;
|
||||
|
||||
afterAll(() => {
|
||||
// after all tests we close socket so the test suite can actually terminate
|
||||
BlueElectrum.forceDisconnect();
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
|
||||
// while app starts up, but for tests we need to wait for it
|
||||
try {
|
||||
await BlueElectrum.waitTillConnected();
|
||||
} catch (Err) {
|
||||
console.log('failed to connect to Electrum:', Err);
|
||||
process.exit(2);
|
||||
}
|
||||
});
|
||||
|
||||
describe('multisig-hd-wallet', () => {
|
||||
it('can fetch balance & transactions', async () => {
|
||||
const path = "m/48'/0'/0'/2'";
|
||||
const fp1 = 'D37EAD88';
|
||||
const fp2 = '168DD603';
|
||||
const Zpub1 = 'Zpub74ijpfhERJNjhCKXRspTdLJV5eoEmSRZdHqDvp9kVtdVEyiXk7pXxRbfZzQvsDFpfDHEHVtVpx4Dz9DGUWGn2Xk5zG5u45QTMsYS2vjohNQ';
|
||||
const Zpub2 = 'Zpub75mAE8EjyxSzoyPmGnd5E6MyD7ALGNndruWv52xpzimZQKukwvEfXTHqmH8nbbc6ccP5t2aM3mws3pKYSnKpKMMytdbNEZFUxKzztYFM8Pn';
|
||||
|
||||
const w = new MultisigHDWallet();
|
||||
w.addCosigner(Zpub1, fp1);
|
||||
w.addCosigner(Zpub2, fp2);
|
||||
w.setDerivationPath(path);
|
||||
w.setM(2);
|
||||
|
||||
assert.strictEqual(w.getM(), 2);
|
||||
assert.strictEqual(w.getN(), 2);
|
||||
assert.strictEqual(w.getDerivationPath(), path);
|
||||
assert.strictEqual(w.getCosigner(1), Zpub1);
|
||||
assert.strictEqual(w.getCosigner(2), Zpub2);
|
||||
assert.strictEqual(w.getCosignerForFingerprint(fp1), Zpub1);
|
||||
assert.strictEqual(w.getCosignerForFingerprint(fp2), Zpub2);
|
||||
|
||||
await w.fetchBalance();
|
||||
await w.fetchTransactions();
|
||||
assert.strictEqual(w.getTransactions().length, 3);
|
||||
});
|
||||
});
|
237
tests/unit/fixtures/electrum-multisig-wallet-with-seed.json
Normal file
237
tests/unit/fixtures/electrum-multisig-wallet-with-seed.json
Normal file
|
@ -0,0 +1,237 @@
|
|||
{
|
||||
"addr_history": {
|
||||
"bc1q0eklp8y3psgyvg5rkny6rvf5hjq9utut4gzdefk5rx993595zv5qhl7a7m": [],
|
||||
"bc1q2mkhkvx9l7aqksvyf0dwd2x4yn8qx2w3sythjltdkjw70r8hsves2evfg6": [],
|
||||
"bc1q2u6t2ckd3rzyq9g27t46eqv4nmdjxhkpgrj5affqule7w7tj0suqxely03": [],
|
||||
"bc1q3p5krekxvrma9v7klsc4vs7gj6jw3lpepkx2vzq8ccdlevz7xaksplvmdw": [],
|
||||
"bc1q43kuf0kfkjjqngery4qcgxzy6md4c808py6l64y5jvmaqgesmrtsur44nh": [],
|
||||
"bc1q5k3v77hc4mtns8ufapeuvwwwurgywyvkjqs99xkdakvvwwqcwk0sxxhxvm": [],
|
||||
"bc1q76vzr936jr9ej958eqpn8hhqcxv2k38jrwvhw2e6vqm8nnx4lcpst9pzua": [],
|
||||
"bc1q7d3xeglhhgvhk0tkrx5xczvs3ceezvcws5yv2l2ngj3fwwhs8xpq5p675c": [],
|
||||
"bc1q96pjhzas8da92w55t9e30u7f0k3n23v5er9zc8k8ctv958j4pptq8u6vxf": [],
|
||||
"bc1q9x28wdnpdw0hv8dg0gg7w0h8x0p8m4dhp7htvr976za0nvkvw7lqsghqr6": [],
|
||||
"bc1qa554n2j8wxcgpfk620ele5xaxllap5r5f5x6csndsrjpnr48ztdscvueyj": [],
|
||||
"bc1qae520282au3dl6echfu5cqvec3plvn9fus5dv5dhxhexaezhvmcs9jwjxp": [],
|
||||
"bc1qcsu2zsvdcumnazzq8quhlw0kcu52kkt4c0xuwpkn4a2cr9m5rmaqxdyt4q": [],
|
||||
"bc1qe0cu3fh5hsr9t43n6qcj2nsmwf63tr24ahx0tk7evttklymz89qqk2nawa": [],
|
||||
"bc1qg77km63d8la99gp3u3sgnvc4va7nmgptt6kztxz9v729tnv57maspsdvu8": [],
|
||||
"bc1qgy840r9ce3z2896w5xfr6jauja3f43aqcajq3d3uxj2np2am3djsmsvc2e": [],
|
||||
"bc1qhge6xjqcp5paz6hd9d55720qczjugk3kthe0a03epwm6g0p2e7cqftyp6j": [],
|
||||
"bc1qj6dsj73033pjxf08m4tccddxerxeydjckeasl8d2z2r93lagvcvqzqslrr": [],
|
||||
"bc1qjx4nka2nsgm2k8uvxa9g5f3xz09cfkxh38ufhqa5p9yxpel3h9wqgkk9s5": [],
|
||||
"bc1qkzg22vej70cqnrlsxcee9nfnstcr70jalvsmjn0c8rjf0klwyydsk8nggs": [
|
||||
[
|
||||
"76bfabeba1fa447bb0ffa497f80585511d238b9c6bd0c3e04381b0152624ef09",
|
||||
646410
|
||||
],
|
||||
[
|
||||
"cb7fd9c2c937be547aa721fb7ffa0f5a28022d137a3912c9652d1d77fbd382ae",
|
||||
647181
|
||||
]
|
||||
],
|
||||
"bc1ql0zp6lvke3uang8lfs58tl5h62708petl04mtfknv3fjq7qc2e5q0kz7x2": [],
|
||||
"bc1qmu4kefaqtp6kw5mfp2rpn03x4768cnt9ctgfmyz27rqq66atg5ks6vdhss": [],
|
||||
"bc1qqj0zx85x3d2frn4nmdn32fgskq5c2qkvk9sukxp3xsdzuf234mds85w068": [],
|
||||
"bc1qs97hede4c5fwyzrq0neve8keg6eetn9l4xzanz4h7dss98v7pltqmqwvyd": [],
|
||||
"bc1qvkm6lkw7erx07g5gnysygecusj7dc8d4vmm6gj5aeetmlg852mmqr5wyy2": [],
|
||||
"bc1qvkvut2lumw8dvslewt02qq62787eynrjmjdut56uplclqc9qr3pq20j05t": [],
|
||||
"bc1qwf50xs9pqlrtesmu29d9a87d2vvpuec6l6qkaezty409ry88ahqsygzxe0": [],
|
||||
"bc1qwpxkr4ac7fyp6y8uegfpqa6phyqex3vdf5mwwrfayrp8889adpgszge8m5": [],
|
||||
"bc1qwvtu5amzl466ww3cq28svp2wqxhgvhp0gfk06dl0k73w9cwpv33splnrwx": [],
|
||||
"bc1qx7ep575xfcrxdmpyxeupv9vq8gm52gd8wt0fkk0q7w6n6y89ruuq98crh8": [],
|
||||
"bc1qzj8v6kxd9k829aqg9t7a5gcm9yj0tnqjlrhuden9zcafk50pqm3qrapvse": []
|
||||
},
|
||||
"addresses": {
|
||||
"change": [
|
||||
"bc1qqj0zx85x3d2frn4nmdn32fgskq5c2qkvk9sukxp3xsdzuf234mds85w068",
|
||||
"bc1qwpxkr4ac7fyp6y8uegfpqa6phyqex3vdf5mwwrfayrp8889adpgszge8m5",
|
||||
"bc1qj6dsj73033pjxf08m4tccddxerxeydjckeasl8d2z2r93lagvcvqzqslrr",
|
||||
"bc1qg77km63d8la99gp3u3sgnvc4va7nmgptt6kztxz9v729tnv57maspsdvu8",
|
||||
"bc1ql0zp6lvke3uang8lfs58tl5h62708petl04mtfknv3fjq7qc2e5q0kz7x2",
|
||||
"bc1qwf50xs9pqlrtesmu29d9a87d2vvpuec6l6qkaezty409ry88ahqsygzxe0",
|
||||
"bc1q7d3xeglhhgvhk0tkrx5xczvs3ceezvcws5yv2l2ngj3fwwhs8xpq5p675c",
|
||||
"bc1q43kuf0kfkjjqngery4qcgxzy6md4c808py6l64y5jvmaqgesmrtsur44nh",
|
||||
"bc1q96pjhzas8da92w55t9e30u7f0k3n23v5er9zc8k8ctv958j4pptq8u6vxf",
|
||||
"bc1qx7ep575xfcrxdmpyxeupv9vq8gm52gd8wt0fkk0q7w6n6y89ruuq98crh8"
|
||||
],
|
||||
"receiving": [
|
||||
"bc1qkzg22vej70cqnrlsxcee9nfnstcr70jalvsmjn0c8rjf0klwyydsk8nggs",
|
||||
"bc1q2mkhkvx9l7aqksvyf0dwd2x4yn8qx2w3sythjltdkjw70r8hsves2evfg6",
|
||||
"bc1q9x28wdnpdw0hv8dg0gg7w0h8x0p8m4dhp7htvr976za0nvkvw7lqsghqr6",
|
||||
"bc1qa554n2j8wxcgpfk620ele5xaxllap5r5f5x6csndsrjpnr48ztdscvueyj",
|
||||
"bc1qjx4nka2nsgm2k8uvxa9g5f3xz09cfkxh38ufhqa5p9yxpel3h9wqgkk9s5",
|
||||
"bc1qwvtu5amzl466ww3cq28svp2wqxhgvhp0gfk06dl0k73w9cwpv33splnrwx",
|
||||
"bc1qvkvut2lumw8dvslewt02qq62787eynrjmjdut56uplclqc9qr3pq20j05t",
|
||||
"bc1qhge6xjqcp5paz6hd9d55720qczjugk3kthe0a03epwm6g0p2e7cqftyp6j",
|
||||
"bc1qae520282au3dl6echfu5cqvec3plvn9fus5dv5dhxhexaezhvmcs9jwjxp",
|
||||
"bc1qmu4kefaqtp6kw5mfp2rpn03x4768cnt9ctgfmyz27rqq66atg5ks6vdhss",
|
||||
"bc1q2u6t2ckd3rzyq9g27t46eqv4nmdjxhkpgrj5affqule7w7tj0suqxely03",
|
||||
"bc1qcsu2zsvdcumnazzq8quhlw0kcu52kkt4c0xuwpkn4a2cr9m5rmaqxdyt4q",
|
||||
"bc1qe0cu3fh5hsr9t43n6qcj2nsmwf63tr24ahx0tk7evttklymz89qqk2nawa",
|
||||
"bc1qs97hede4c5fwyzrq0neve8keg6eetn9l4xzanz4h7dss98v7pltqmqwvyd",
|
||||
"bc1q5k3v77hc4mtns8ufapeuvwwwurgywyvkjqs99xkdakvvwwqcwk0sxxhxvm",
|
||||
"bc1q0eklp8y3psgyvg5rkny6rvf5hjq9utut4gzdefk5rx993595zv5qhl7a7m",
|
||||
"bc1q3p5krekxvrma9v7klsc4vs7gj6jw3lpepkx2vzq8ccdlevz7xaksplvmdw",
|
||||
"bc1qvkm6lkw7erx07g5gnysygecusj7dc8d4vmm6gj5aeetmlg852mmqr5wyy2",
|
||||
"bc1qgy840r9ce3z2896w5xfr6jauja3f43aqcajq3d3uxj2np2am3djsmsvc2e",
|
||||
"bc1qzj8v6kxd9k829aqg9t7a5gcm9yj0tnqjlrhuden9zcafk50pqm3qrapvse",
|
||||
"bc1q76vzr936jr9ej958eqpn8hhqcxv2k38jrwvhw2e6vqm8nnx4lcpst9pzua"
|
||||
]
|
||||
},
|
||||
"channel_backups": {},
|
||||
"fiat_value": {},
|
||||
"invoices": {
|
||||
"7bf2aec9c12aa8b0a2363f283d46769c": {
|
||||
"amount_sat": "!",
|
||||
"bip70": null,
|
||||
"exp": 0,
|
||||
"id": "7bf2aec9c12aa8b0a2363f283d46769c",
|
||||
"message": "let's see",
|
||||
"outputs": [
|
||||
[
|
||||
0,
|
||||
"bc1qhhvjcww8q904upz96nnery7nz5qd404w2prsmf",
|
||||
"!"
|
||||
]
|
||||
],
|
||||
"requestor": null,
|
||||
"time": 1598963621,
|
||||
"type": 0
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"76bfabeba1fa447bb0ffa497f80585511d238b9c6bd0c3e04381b0152624ef09": "test",
|
||||
"bc1qkzg22vej70cqnrlsxcee9nfnstcr70jalvsmjn0c8rjf0klwyydsk8nggs": "test",
|
||||
"cb7fd9c2c937be547aa721fb7ffa0f5a28022d137a3912c9652d1d77fbd382ae": "let's see"
|
||||
},
|
||||
"payment_requests": {
|
||||
"bc1qkzg22vej70cqnrlsxcee9nfnstcr70jalvsmjn0c8rjf0klwyydsk8nggs": {
|
||||
"amount_sat": 0,
|
||||
"bip70": null,
|
||||
"exp": 86400,
|
||||
"id": "44291cd0de",
|
||||
"message": "test",
|
||||
"outputs": [
|
||||
[
|
||||
0,
|
||||
"bc1qkzg22vej70cqnrlsxcee9nfnstcr70jalvsmjn0c8rjf0klwyydsk8nggs",
|
||||
0
|
||||
]
|
||||
],
|
||||
"requestor": null,
|
||||
"time": 1598962791,
|
||||
"type": 0
|
||||
}
|
||||
},
|
||||
"prevouts_by_scripthash": {
|
||||
"8aa1e2df0a1439e2bf1e3523284072c06b05ddd2f3742473009cb86edd5884e8": [
|
||||
[
|
||||
"cb7fd9c2c937be547aa721fb7ffa0f5a28022d137a3912c9652d1d77fbd382ae:0",
|
||||
19854
|
||||
]
|
||||
],
|
||||
"ea0ecf0f9c05b2c445966000703249c434b39fd858bb05dacfd8b18294cb4154": [
|
||||
[
|
||||
"76bfabeba1fa447bb0ffa497f80585511d238b9c6bd0c3e04381b0152624ef09:0",
|
||||
20000
|
||||
]
|
||||
],
|
||||
"eb2505bd6502a55e9acba99bda3795b34ffe564cb37de89b2cb077e85b6ef03b": [
|
||||
[
|
||||
"76bfabeba1fa447bb0ffa497f80585511d238b9c6bd0c3e04381b0152624ef09:1",
|
||||
130625
|
||||
]
|
||||
]
|
||||
},
|
||||
"qt-console-history": [],
|
||||
"seed_version": 32,
|
||||
"spent_outpoints": {
|
||||
"76bfabeba1fa447bb0ffa497f80585511d238b9c6bd0c3e04381b0152624ef09": {
|
||||
"0": "cb7fd9c2c937be547aa721fb7ffa0f5a28022d137a3912c9652d1d77fbd382ae"
|
||||
},
|
||||
"e8030ec801e82cadee08c959b246b72199f2112e4b5fc95ce37abdb4ad591901": {
|
||||
"1": "76bfabeba1fa447bb0ffa497f80585511d238b9c6bd0c3e04381b0152624ef09"
|
||||
}
|
||||
},
|
||||
"stored_height": 649897,
|
||||
"transactions": {
|
||||
"76bfabeba1fa447bb0ffa497f80585511d238b9c6bd0c3e04381b0152624ef09": "02000000000101011959adb4bd7ae35cc95f4b2e11f29921b746b259c908eead2ce801c80e03e801000000000000008002204e000000000000220020b090a53332f3f0098ff0363392cd3382f03f3e5dfb21b94df838e497dbee211b41fe0100000000001600140bf2e2fddd5d11bbf533d02348dae0b5abddccf60248304502210089c592f2660b2b3cd60cf32fbb1ef2d8088273d54b79fd4ced3da8308bafa6b102205d6622c92dd39f2704d465cb4f62c6348f1daed0dff3f2037596c257ee628dd6012102cd5a0cf85fc0f1632cf08f280fec194f39048618402d74344b543a4cc29c098300000000",
|
||||
"cb7fd9c2c937be547aa721fb7ffa0f5a28022d137a3912c9652d1d77fbd382ae": "0200000000010109ef242615b08143e0c3d06b9c8b231d518505f897a4ffb07b44faa1ebabbf760000000000fdffffff018e4d000000000000160014bdd92c39c7015f5e0445d4e79193d31500dabeae04004730440220614f412bb6d3a16b37fc8bda799ed624be0249c78b5cd8e489abc14ee137e20202201df43d9fc4ee974525a89a92fea3b9469a640981bedebf646f5fde9faf35aa300147304402205ba807eb6d3b7b9ad11c9d6d29aa21b12e2dda0d3ceecc51324c202715c2c10602207fd510556a9ee9650b69becfedd07e891f2add4c9d1a9f564b02d6aff383aa9701695221027b67bf915fa780268554d293aaf38219ebca75109a0292276ac7f249d8c020ce210396e5d921929dc655febfd2d378233347e9e38be88f6f18d82a36f1587d4afaf62103be43383e4661dfba8046f63d8b73a31dcef69c85dd44336d2a3053d064d02d5953ae88dc0900"
|
||||
},
|
||||
"tx_fees": {
|
||||
"76bfabeba1fa447bb0ffa497f80585511d238b9c6bd0c3e04381b0152624ef09": [
|
||||
null,
|
||||
false,
|
||||
1
|
||||
],
|
||||
"cb7fd9c2c937be547aa721fb7ffa0f5a28022d137a3912c9652d1d77fbd382ae": [
|
||||
146,
|
||||
true,
|
||||
1
|
||||
]
|
||||
},
|
||||
"txi": {
|
||||
"cb7fd9c2c937be547aa721fb7ffa0f5a28022d137a3912c9652d1d77fbd382ae": {
|
||||
"bc1qkzg22vej70cqnrlsxcee9nfnstcr70jalvsmjn0c8rjf0klwyydsk8nggs": {
|
||||
"76bfabeba1fa447bb0ffa497f80585511d238b9c6bd0c3e04381b0152624ef09:0": 20000
|
||||
}
|
||||
}
|
||||
},
|
||||
"txo": {
|
||||
"76bfabeba1fa447bb0ffa497f80585511d238b9c6bd0c3e04381b0152624ef09": {
|
||||
"bc1qkzg22vej70cqnrlsxcee9nfnstcr70jalvsmjn0c8rjf0klwyydsk8nggs": {
|
||||
"0": [
|
||||
20000,
|
||||
false
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"use_encryption": false,
|
||||
"verified_tx3": {
|
||||
"76bfabeba1fa447bb0ffa497f80585511d238b9c6bd0c3e04381b0152624ef09": [
|
||||
646410,
|
||||
1599033977,
|
||||
1607,
|
||||
"000000000000000000066e6b1be6edf8b65fb1ecda09d4fd5f0387098db26d81"
|
||||
],
|
||||
"cb7fd9c2c937be547aa721fb7ffa0f5a28022d137a3912c9652d1d77fbd382ae": [
|
||||
647181,
|
||||
1599501121,
|
||||
463,
|
||||
"0000000000000000000d126b807c92de495668dd4924de6917b58b6d7b67da62"
|
||||
]
|
||||
},
|
||||
"wallet_type": "2of3",
|
||||
"winpos-qt": [
|
||||
200,
|
||||
175,
|
||||
890,
|
||||
463
|
||||
],
|
||||
"x1/": {
|
||||
"derivation": "m/1'",
|
||||
"pw_hash_version": 1,
|
||||
"root_fingerprint": "8aaa5d05",
|
||||
"seed": "during pride layer jelly admit army want melody check witness favorite prosper",
|
||||
"type": "bip32",
|
||||
"xprv": "ZprvAkUsoZMLiqxrhaM8VpmVJ6QhjH4dZnYpTNNHGMZ3VoE6vRv7xfDeMEiKAeH1eUcN3CFUP87CgM1anM2UytMkykUMtVmXkkohRsiVGth1VMG",
|
||||
"xpub": "Zpub6yUED4tEZDX9v4RbbrJVfEMSHJu7yFGfpbHt4jxf48m5oEFGWCXtu32o1wQkEbCCrHJfRbc8GeoBwpRowcvTMHruNcsbm97QD4uUzaXrtNK"
|
||||
},
|
||||
"x2/": {
|
||||
"derivation": "m/1'",
|
||||
"pw_hash_version": 1,
|
||||
"root_fingerprint": "ef748d2c",
|
||||
"type": "bip32",
|
||||
"xprv": null,
|
||||
"xpub": "Zpub6zDCLaNWD5uppmN4gsUCGpVYpxMJMRLEx2MXeV4Qsj7VdzgTLL4VNhevYtjd8FjmMz2j9dw5oiamUF25hsxAYuFSSq2zoVHrv6MWXfjjHXq"
|
||||
},
|
||||
"x3/": {
|
||||
"derivation": "m/1'",
|
||||
"pw_hash_version": 1,
|
||||
"root_fingerprint": "fdb6c4d8",
|
||||
"type": "bip32",
|
||||
"xprv": null,
|
||||
"xpub": "Zpub6zKGu3ZgmL6WQ2kyVjSNtn8D3hKC26WBQW8tuyyRxkKrdAEQ6VCdqoFf68bVvQ289ZBTJVkUKaggwtBQFZBBMmozTPMVeBHGyHPXfvG9KkR"
|
||||
}
|
||||
}
|
1299
tests/unit/multisig-hd-wallet.test.js
Normal file
1299
tests/unit/multisig-hd-wallet.test.js
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue