mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-01-18 21:35:21 +01:00
ADD: support for URv2 QR codes
This commit is contained in:
parent
7b3e6b5d95
commit
7526c82669
@ -34,7 +34,7 @@ import WalletGradient from './class/wallet-gradient';
|
||||
import { BlurView } from '@react-native-community/blur';
|
||||
import NetworkTransactionFees, { NetworkTransactionFee, NetworkTransactionFeeType } from './models/networkTransactionFees';
|
||||
import Biometric from './class/biometrics';
|
||||
import { encodeUR } from 'bc-ur/dist';
|
||||
import { encodeUR } from './blue_modules/ur';
|
||||
import QRCode from 'react-native-qrcode-svg';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useNavigation, useTheme } from '@react-navigation/native';
|
||||
|
228
blue_modules/ur/index.js
Normal file
228
blue_modules/ur/index.js
Normal file
@ -0,0 +1,228 @@
|
||||
import { URDecoder } from '@ngraveio/bc-ur';
|
||||
import b58 from 'bs58check';
|
||||
import {
|
||||
CryptoHDKey,
|
||||
CryptoKeypath,
|
||||
CryptoOutput,
|
||||
PathComponent,
|
||||
ScriptExpressions,
|
||||
CryptoPSBT,
|
||||
CryptoAccount,
|
||||
Bytes,
|
||||
} from '@keystonehq/bc-ur-registry';
|
||||
import { decodeUR as origDecodeUr, encodeUR as origEncodeUR, extractSingleWorkload as origExtractSingleWorkload } from '../bc-ur/dist';
|
||||
import { MultisigCosigner, MultisigHDWallet } from '../../class';
|
||||
import { Psbt } from 'bitcoinjs-lib';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
const USE_UR_V1 = 'USE_UR_V1';
|
||||
|
||||
let useURv1 = false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
useURv1 = !!(await AsyncStorage.getItem(USE_UR_V1));
|
||||
} catch (_) {}
|
||||
})();
|
||||
|
||||
async function isURv1Enabled() {
|
||||
try {
|
||||
return !!(await AsyncStorage.getItem(USE_UR_V1));
|
||||
} catch (_) {}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function setUseURv1() {
|
||||
useURv1 = true;
|
||||
return AsyncStorage.setItem(USE_UR_V1, '1');
|
||||
}
|
||||
|
||||
async function clearUseURv1() {
|
||||
useURv1 = false;
|
||||
return AsyncStorage.removeItem(USE_UR_V1);
|
||||
}
|
||||
|
||||
function encodeUR(arg1, arg2) {
|
||||
return useURv1 ? encodeURv1(arg1, arg2) : encodeURv2(arg1, arg2);
|
||||
}
|
||||
|
||||
function encodeURv1(arg1, arg2) {
|
||||
// first, lets check that its not a cosigner's json, which we do NOT encode at all:
|
||||
try {
|
||||
const json = JSON.parse(arg1);
|
||||
if (json && json.xpub && json.path && json.xfp) return [arg1];
|
||||
} catch (_) {}
|
||||
|
||||
return origEncodeUR(arg1, arg2);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param str {string} For PSBT, or coordination setup (translates to `bytes`) it expects hex string. For ms cosigner it expects plain json string
|
||||
* @param len {number} lenght of each fragment
|
||||
* @return {string[]} txt fragments ready to be displayed in dynamic QR
|
||||
*/
|
||||
function encodeURv2(str, len) {
|
||||
// now, lets do some intelligent guessing what we've got here, psbt hex, or json with a multisig cosigner..?
|
||||
|
||||
try {
|
||||
const cosigner = new MultisigCosigner(str);
|
||||
|
||||
if (cosigner.isValid()) {
|
||||
let scriptExpressions = false;
|
||||
|
||||
if (cosigner.isNativeSegwit()) {
|
||||
scriptExpressions = [ScriptExpressions.WITNESS_SCRIPT_HASH];
|
||||
} else if (cosigner.isWrappedSegwit()) {
|
||||
scriptExpressions = [ScriptExpressions.SCRIPT_HASH, ScriptExpressions.WITNESS_SCRIPT_HASH];
|
||||
} else if (cosigner.isLegacy()) {
|
||||
scriptExpressions = [ScriptExpressions.SCRIPT_HASH];
|
||||
} else {
|
||||
return ['unsupported multisig type'];
|
||||
}
|
||||
|
||||
const cryptoKeyPathComponents = [];
|
||||
for (const component of cosigner.getPath().split('/')) {
|
||||
if (component === 'm') continue;
|
||||
const index = parseInt(component);
|
||||
const hardened = component.endsWith('h') || component.endsWith("'");
|
||||
cryptoKeyPathComponents.push(new PathComponent({ index, hardened }));
|
||||
}
|
||||
|
||||
const cryptoAccount = new CryptoAccount(Buffer.from(cosigner.getFp(), 'hex'), [
|
||||
new CryptoOutput(
|
||||
scriptExpressions,
|
||||
new CryptoHDKey({
|
||||
isMaster: false,
|
||||
key: Buffer.from(cosigner.getKeyHex(), 'hex'),
|
||||
chainCode: Buffer.from(cosigner.getChainCodeHex(), 'hex'),
|
||||
origin: new CryptoKeypath(
|
||||
cryptoKeyPathComponents,
|
||||
Buffer.from(cosigner.getFp(), 'hex'),
|
||||
cosigner.getDepthNumber(),
|
||||
),
|
||||
parentFingerprint: Buffer.from(cosigner.getParentFingerprintHex(), 'hex'),
|
||||
}),
|
||||
),
|
||||
]);
|
||||
const ur = cryptoAccount.toUREncoder(2000).nextPart();
|
||||
return [ur];
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// not account. lets try psbt
|
||||
|
||||
try {
|
||||
Psbt.fromHex(str); // will throw if not PSBT hex
|
||||
const data = Buffer.from(str, 'hex');
|
||||
const cryptoPSBT = new CryptoPSBT(data);
|
||||
const encoder = cryptoPSBT.toUREncoder(len);
|
||||
|
||||
const ret = [];
|
||||
for (let c = 1; c <= encoder.fragmentsLength; c++) {
|
||||
const ur = encoder.nextPart();
|
||||
ret.push(ur);
|
||||
}
|
||||
|
||||
return ret;
|
||||
} catch (_) {}
|
||||
|
||||
// fail. fallback to bytes
|
||||
|
||||
const bytes = new Bytes(Buffer.from(str, 'hex'));
|
||||
const encoder = bytes.toUREncoder(len);
|
||||
|
||||
const ret = [];
|
||||
for (let c = 1; c <= encoder.fragmentsLength; c++) {
|
||||
const ur = encoder.nextPart();
|
||||
ret.push(ur);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function extractSingleWorkload(arg) {
|
||||
return origExtractSingleWorkload(arg);
|
||||
}
|
||||
|
||||
function decodeUR(arg) {
|
||||
try {
|
||||
return origDecodeUr(arg);
|
||||
} catch (_) {}
|
||||
|
||||
const decoder = new URDecoder();
|
||||
|
||||
for (const part of arg) {
|
||||
decoder.receivePart(part);
|
||||
}
|
||||
|
||||
if (!decoder.isSuccess()) {
|
||||
throw new Error(decoder.resultError());
|
||||
}
|
||||
|
||||
const decoded = decoder.resultUR();
|
||||
|
||||
if (decoded.type === 'crypto-psbt') {
|
||||
const cryptoPsbt = CryptoPSBT.fromCBOR(decoded.cbor);
|
||||
return cryptoPsbt.getPSBT().toString('hex');
|
||||
}
|
||||
|
||||
if (decoded.type === 'bytes') {
|
||||
const b = Bytes.fromCBOR(decoded.cbor);
|
||||
return b.getData();
|
||||
}
|
||||
|
||||
const cryptoAccount = CryptoAccount.fromCBOR(decoded.cbor);
|
||||
|
||||
// now, crafting zpub out of data we have
|
||||
const hdKey = cryptoAccount.outputDescriptors[0].getCryptoKey();
|
||||
const derivationPath = 'm/' + hdKey.getOrigin().getPath();
|
||||
const script = cryptoAccount.outputDescriptors[0].getScriptExpressions()[0].getExpression();
|
||||
const isMultisig =
|
||||
script === ScriptExpressions.WITNESS_SCRIPT_HASH.getExpression() ||
|
||||
// fallback to paths (unreliable).
|
||||
// dont know how to add ms p2sh (legacy) or p2sh-p2wsh (wrapped segwit) atm
|
||||
derivationPath === MultisigHDWallet.PATH_LEGACY ||
|
||||
derivationPath === MultisigHDWallet.PATH_WRAPPED_SEGWIT ||
|
||||
derivationPath === MultisigHDWallet.PATH_NATIVE_SEGWIT;
|
||||
const version = Buffer.from(isMultisig ? '02aa7ed3' : '04b24746', 'hex');
|
||||
const parentFingerprint = hdKey.getParentFingerprint();
|
||||
const depth = hdKey.getOrigin().getDepth();
|
||||
const depthBuf = Buffer.alloc(1);
|
||||
depthBuf.writeUInt8(depth);
|
||||
const components = hdKey.getOrigin().getComponents();
|
||||
const lastComponents = components[components.length - 1];
|
||||
const index = lastComponents.isHardened() ? lastComponents.getIndex() + 0x80000000 : lastComponents.getIndex();
|
||||
const indexBuf = Buffer.alloc(4);
|
||||
indexBuf.writeUInt32BE(index);
|
||||
const chainCode = hdKey.getChainCode();
|
||||
const key = hdKey.getKey();
|
||||
const data = Buffer.concat([version, depthBuf, parentFingerprint, indexBuf, chainCode, key]);
|
||||
|
||||
const zpub = b58.encode(data);
|
||||
|
||||
const result = {};
|
||||
result.ExtPubKey = zpub;
|
||||
result.MasterFingerprint = cryptoAccount.getMasterFingerprint().toString('hex').toUpperCase();
|
||||
result.AccountKeyPath = derivationPath;
|
||||
|
||||
const str = JSON.stringify(result);
|
||||
return Buffer.from(str, 'ascii').toString('hex'); // we are expected to return hex-encoded string
|
||||
}
|
||||
|
||||
class BlueURDecoder extends URDecoder {
|
||||
toString() {
|
||||
const decoded = this.resultUR();
|
||||
|
||||
if (decoded.type === 'crypto-psbt') {
|
||||
const cryptoPsbt = CryptoPSBT.fromCBOR(decoded.cbor);
|
||||
return cryptoPsbt.getPSBT().toString('base64');
|
||||
} else if (decoded.type === 'bytes') {
|
||||
const bytes = Bytes.fromCBOR(decoded.cbor);
|
||||
return Buffer.from(bytes.getData(), 'hex').toString('ascii');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { decodeUR, encodeUR, extractSingleWorkload, BlueURDecoder, isURv1Enabled, setUseURv1, clearUseURv1 };
|
@ -69,6 +69,21 @@ export class MultisigCosigner {
|
||||
this._valid = false;
|
||||
}
|
||||
|
||||
// is it cobo crypto-account URv2 ?
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (json && json.ExtPubKey && json.MasterFingerprint && json.AccountKeyPath) {
|
||||
this._fp = json.MasterFingerprint;
|
||||
this._xpub = json.ExtPubKey;
|
||||
this._path = json.AccountKeyPath;
|
||||
this._cosigners = [true];
|
||||
this._valid = true;
|
||||
return;
|
||||
}
|
||||
} catch (_) {
|
||||
this._valid = false;
|
||||
}
|
||||
|
||||
// is it coldcard json?
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
@ -149,4 +164,48 @@ export class MultisigCosigner {
|
||||
getAllCosigners() {
|
||||
return this._cosigners;
|
||||
}
|
||||
|
||||
isNativeSegwit() {
|
||||
return this.getXpub().startsWith('Zpub');
|
||||
}
|
||||
|
||||
isWrappedSegwit() {
|
||||
return this.getXpub().startsWith('Ypub');
|
||||
}
|
||||
|
||||
isLegacy() {
|
||||
return this.getXpub().startsWith('xpub');
|
||||
}
|
||||
|
||||
getChainCodeHex() {
|
||||
let data = b58.decode(this.getXpub());
|
||||
data = data.slice(4);
|
||||
data = data.slice(1);
|
||||
data = data.slice(4);
|
||||
data = data.slice(4, 36);
|
||||
return data.toString('hex');
|
||||
}
|
||||
|
||||
getKeyHex() {
|
||||
let data = b58.decode(this.getXpub());
|
||||
data = data.slice(4);
|
||||
data = data.slice(1);
|
||||
data = data.slice(4);
|
||||
data = data.slice(36);
|
||||
return data.toString('hex');
|
||||
}
|
||||
|
||||
getParentFingerprintHex() {
|
||||
let data = b58.decode(this.getXpub());
|
||||
data = data.slice(4);
|
||||
data = data.slice(1);
|
||||
data = data.slice(0, 4);
|
||||
return data.toString('hex');
|
||||
}
|
||||
|
||||
getDepthNumber() {
|
||||
let data = b58.decode(this.getXpub());
|
||||
data = data.slice(4, 5);
|
||||
return data.readInt8();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
|
||||
import * as bip39 from 'bip39';
|
||||
import b58 from 'bs58check';
|
||||
import { decodeUR } from 'bc-ur';
|
||||
import { decodeUR } from '../../blue_modules/ur';
|
||||
const BlueElectrum = require('../../blue_modules/BlueElectrum');
|
||||
const HDNode = require('bip32');
|
||||
const bitcoin = require('bitcoinjs-lib');
|
||||
|
@ -2,7 +2,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Text } from 'react-native-elements';
|
||||
import { Dimensions, LayoutAnimation, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { encodeUR } from 'bc-ur/dist';
|
||||
import { encodeUR } from '../blue_modules/ur';
|
||||
import QRCode from 'react-native-qrcode-svg';
|
||||
import { BlueCurrentTheme } from '../components/themes';
|
||||
import { BlueSpacing20 } from '../BlueComponents';
|
||||
|
68
package-lock.json
generated
68
package-lock.json
generated
@ -23,6 +23,11 @@
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@apocentre/alias-sampling": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@apocentre/alias-sampling/-/alias-sampling-0.5.3.tgz",
|
||||
"integrity": "sha512-7UDWIIF9hIeJqfKXkNIzkVandlwLf1FWTSdrb9iXvOP8oF544JRXQjCbiTmCv2c9n44n/FIWtehhBfNuAx2CZA=="
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
|
||||
@ -2919,6 +2924,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@keystonehq/bc-ur-registry": {
|
||||
"version": "git+https://github.com/BlueWallet/ur-registry.git#8e80466920d0cde4c06b112ad126c311bcdd3384",
|
||||
"from": "git+https://github.com/BlueWallet/ur-registry.git",
|
||||
"requires": {
|
||||
"@ngraveio/bc-ur": "git+https://github.com/BlueWallet/bc-ur.git",
|
||||
"tslib": "~2.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
|
||||
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@ngraveio/bc-ur": {
|
||||
"version": "git+https://github.com/BlueWallet/bc-ur.git#5514e3ddb7e7dafb123471818d0e516048d8a90e",
|
||||
"from": "git+https://github.com/BlueWallet/bc-ur.git",
|
||||
"requires": {
|
||||
"@apocentre/alias-sampling": "^0.5.3",
|
||||
"assert": "^2.0.0",
|
||||
"bignumber.js": "^9.0.1",
|
||||
"cbor-sync": "^1.0.4",
|
||||
"crc": "^3.8.0",
|
||||
"jsbi": "^3.1.5",
|
||||
"sha.js": "^2.4.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsbi": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.5.tgz",
|
||||
"integrity": "sha512-w2BY0VOYC1ahe+w6Qhl4SFoPvPsZ9NPHY4bwass+LCgU7RK3PBoVQlQ3G1s7vI8W3CYyJiEXcbKF7FIM/L8q3Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@react-native-async-storage/async-storage": {
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.15.5.tgz",
|
||||
@ -5407,10 +5447,6 @@
|
||||
"version": "file:blue_modules/bc-bech32",
|
||||
"integrity": "sha512-lwAn5R4LUhcnyrZgNx3YdDPr5+nseM4kARANcv8i0YOMtnPJRTF7B7TZzS3DYgC6tff/aR2W/3jGoY/SJMs6MA=="
|
||||
},
|
||||
"bc-ur": {
|
||||
"version": "file:blue_modules/bc-ur",
|
||||
"integrity": "sha512-k5jZLNgiCMQH5d/4lwsa6DJjH12vzdTEr9qVH1y9UPzJW32Ga1u8iC0KDAqtYnkvh8NR4DW8Fco6D2hphHZLzg=="
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
@ -6006,6 +6042,11 @@
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
|
||||
},
|
||||
"cbor-sync": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cbor-sync/-/cbor-sync-1.0.4.tgz",
|
||||
"integrity": "sha512-GWlXN4wiz0vdWWXBU71Dvc1q3aBo0HytqwAZnXF1wOwjqNnDWA1vZ1gDMFLlqohak31VQzmhiYfiCX5QSSfagA=="
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
@ -6592,6 +6633,25 @@
|
||||
"parse-json": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"crc": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
|
||||
"integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
|
||||
"requires": {
|
||||
"buffer": "^5.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"requires": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"create-ecdh": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
|
||||
|
@ -77,6 +77,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/preset-env": "7.12.1",
|
||||
"@keystonehq/bc-ur-registry": "https://github.com/BlueWallet/ur-registry",
|
||||
"@ngraveio/bc-ur": "https://github.com/BlueWallet/bc-ur",
|
||||
"@react-native-async-storage/async-storage": "1.15.5",
|
||||
"@react-native-clipboard/clipboard": "1.7.0",
|
||||
"@react-native-community/blur": "3.6.0",
|
||||
@ -92,7 +94,6 @@
|
||||
"assert": "2.0.0",
|
||||
"base-x": "3.0.8",
|
||||
"bc-bech32": "file:blue_modules/bc-bech32",
|
||||
"bc-ur": "file:blue_modules/bc-ur",
|
||||
"bech32": "2.0.0",
|
||||
"bignumber.js": "9.0.1",
|
||||
"bip21": "2.0.3",
|
||||
|
@ -4,7 +4,7 @@ import { Image, View, TouchableOpacity, StatusBar, Platform, StyleSheet, TextInp
|
||||
import { RNCamera } from 'react-native-camera';
|
||||
import { Icon } from 'react-native-elements';
|
||||
import { launchImageLibrary } from 'react-native-image-picker';
|
||||
import { decodeUR, extractSingleWorkload } from 'bc-ur';
|
||||
import { decodeUR, extractSingleWorkload, BlueURDecoder } from '../../blue_modules/ur';
|
||||
import { useNavigation, useRoute, useIsFocused, useTheme } from '@react-navigation/native';
|
||||
import loc from '../../loc';
|
||||
import { BlueLoading, BlueText, BlueButton, BlueSpacing40 } from '../../BlueComponents';
|
||||
@ -16,6 +16,7 @@ const createHash = require('create-hash');
|
||||
const fs = require('../../blue_modules/fs');
|
||||
const Base43 = require('../../blue_modules/base43');
|
||||
const bitcoin = require('bitcoinjs-lib');
|
||||
let decoder = false;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
@ -112,6 +113,41 @@ const ScanQRCode = () => {
|
||||
return createHash('sha256').update(s).digest().toString('hex');
|
||||
};
|
||||
|
||||
const _onReadUniformResourceV2 = part => {
|
||||
if (!decoder) decoder = new BlueURDecoder();
|
||||
try {
|
||||
decoder.receivePart(part);
|
||||
if (decoder.isComplete()) {
|
||||
const data = decoder.toString();
|
||||
decoder = false; // nullify for future use (?)
|
||||
if (launchedBy) {
|
||||
navigation.navigate(launchedBy);
|
||||
}
|
||||
onBarScanned({ data });
|
||||
} else {
|
||||
setUrTotal(100);
|
||||
setUrHave(Math.floor(decoder.estimatedPercentComplete() * 100));
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
setIsLoading(true);
|
||||
Alert.alert(loc.send.scan_error, loc._.invalid_animated_qr_code_fragment, [
|
||||
{
|
||||
text: loc._.ok,
|
||||
onPress: () => {
|
||||
setIsLoading(false);
|
||||
},
|
||||
style: 'default',
|
||||
},
|
||||
{ cancelabe: false },
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @deprecated remove when we get rid of URv1 support
|
||||
*/
|
||||
const _onReadUniformResource = ur => {
|
||||
try {
|
||||
const [index, total] = extractSingleWorkload(ur);
|
||||
@ -160,6 +196,17 @@ const ScanQRCode = () => {
|
||||
}
|
||||
scannedCache[h] = +new Date();
|
||||
|
||||
if (ret.data.toUpperCase().startsWith('UR:CRYPTO-PSBT')) {
|
||||
return _onReadUniformResourceV2(ret.data);
|
||||
}
|
||||
|
||||
if (ret.data.toUpperCase().startsWith('UR:BYTES')) {
|
||||
const splitted = ret.data.split('/');
|
||||
if (splitted.length === 3 && splitted[1].includes('-')) {
|
||||
return _onReadUniformResourceV2(ret.data);
|
||||
}
|
||||
}
|
||||
|
||||
if (ret.data.toUpperCase().startsWith('UR')) {
|
||||
return _onReadUniformResource(ret.data);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { BlueLoading, BlueText, BlueSpacing20, BlueListItem, BlueCard } from '..
|
||||
import { useNavigation, useTheme } from '@react-navigation/native';
|
||||
import loc from '../../loc';
|
||||
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
||||
import { isURv1Enabled, clearUseURv1, setUseURv1 } from '../../blue_modules/ur';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
@ -19,16 +20,22 @@ const GeneralSettings = () => {
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isAdancedModeSwitchEnabled, setIsAdancedModeSwitchEnabled] = useState(false);
|
||||
const [isURv1SwitchEnabled, setIsURv1SwitchEnabled] = useState(false);
|
||||
const { navigate } = useNavigation();
|
||||
const { colors } = useTheme();
|
||||
const onAdvancedModeSwitch = async value => {
|
||||
await setIsAdancedModeEnabled(value);
|
||||
setIsAdancedModeSwitchEnabled(value);
|
||||
};
|
||||
const onLegacyURv1Switch = async value => {
|
||||
setIsURv1SwitchEnabled(value);
|
||||
return value ? setUseURv1() : clearUseURv1();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setIsAdancedModeSwitchEnabled(await isAdancedModeEnabled());
|
||||
setIsURv1SwitchEnabled(await isURv1Enabled());
|
||||
setIsLoading(false);
|
||||
})();
|
||||
});
|
||||
@ -85,6 +92,12 @@ const GeneralSettings = () => {
|
||||
<BlueText>{loc.settings.general_adv_mode_e}</BlueText>
|
||||
</BlueCard>
|
||||
<BlueSpacing20 />
|
||||
<BlueListItem
|
||||
Component={TouchableWithoutFeedback}
|
||||
title="Legacy URv1 QR"
|
||||
switch={{ onValueChange: onLegacyURv1Switch, value: isURv1SwitchEnabled }}
|
||||
/>
|
||||
<BlueSpacing20 />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
@ -41,6 +41,7 @@ import MultipleStepsListItem, {
|
||||
MultipleStepsListItemDashType,
|
||||
} from '../../components/MultipleStepsListItem';
|
||||
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
||||
import { encodeUR } from '../../blue_modules/ur';
|
||||
|
||||
const prompt = require('../../blue_modules/prompt');
|
||||
const A = require('../../blue_modules/analytics');
|
||||
@ -60,7 +61,8 @@ const WalletsAddMultisigStep2 = () => {
|
||||
const [isMnemonicsModalVisible, setIsMnemonicsModalVisible] = useState(false);
|
||||
const [isProvideMnemonicsModalVisible, setIsProvideMnemonicsModalVisible] = useState(false);
|
||||
const [isRenderCosignersXpubModalVisible, setIsRenderCosignersXpubModalVisible] = useState(false);
|
||||
const [cosignerXpub, setCosignerXpub] = useState(''); // string displayed in renderCosignersXpubModal()
|
||||
const [cosignerXpub, setCosignerXpub] = useState(''); // string used in exportCosigner()
|
||||
const [cosignerXpubURv2, setCosignerXpubURv2] = useState(''); // string displayed in renderCosignersXpubModal()
|
||||
const [cosignerXpubFilename, setCosignerXpubFilename] = useState('bw-cosigner.json');
|
||||
const [vaultKeyData, setVaultKeyData] = useState({ keyIndex: 1, xpub: '', seed: '', isLoading: false }); // string rendered in modal
|
||||
const [importText, setImportText] = useState('');
|
||||
@ -250,6 +252,7 @@ const WalletsAddMultisigStep2 = () => {
|
||||
const viewKey = cosigner => {
|
||||
if (MultisigHDWallet.isXpubValid(cosigner[0])) {
|
||||
setCosignerXpub(MultisigCosigner.exportToJson(cosigner[1], cosigner[0], cosigner[2]));
|
||||
setCosignerXpubURv2(encodeUR(MultisigCosigner.exportToJson(cosigner[1], cosigner[0], cosigner[2]))[0]);
|
||||
setCosignerXpubFilename('bw-cosigner-' + cosigner[1] + '.json');
|
||||
setIsRenderCosignersXpubModalVisible(true);
|
||||
} else {
|
||||
@ -258,6 +261,7 @@ const WalletsAddMultisigStep2 = () => {
|
||||
const xpub = getXpubCacheForMnemonics(cosigner[0]);
|
||||
const fp = getFpCacheForMnemonics(cosigner[0]);
|
||||
setCosignerXpub(MultisigCosigner.exportToJson(fp, xpub, path));
|
||||
setCosignerXpubURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
|
||||
setCosignerXpubFilename('bw-cosigner-' + fp + '.json');
|
||||
setIsRenderCosignersXpubModalVisible(true);
|
||||
}
|
||||
@ -618,7 +622,7 @@ const WalletsAddMultisigStep2 = () => {
|
||||
<BlueSpacing20 />
|
||||
<View style={styles.qrCodeContainer}>
|
||||
<QRCode
|
||||
value={cosignerXpub}
|
||||
value={cosignerXpubURv2}
|
||||
size={260}
|
||||
color="#000000"
|
||||
logoBackgroundColor={colors.brandingColor}
|
||||
|
@ -42,6 +42,7 @@ import Biometric from '../../class/biometrics';
|
||||
import QRCode from 'react-native-qrcode-svg';
|
||||
import { SquareButton } from '../../components/SquareButton';
|
||||
import { isMacCatalina } from '../../blue_modules/environment';
|
||||
import { encodeUR } from '../../blue_modules/ur';
|
||||
const fs = require('../../blue_modules/fs');
|
||||
|
||||
const ViewEditMultisigCosigners = () => {
|
||||
@ -62,7 +63,8 @@ const ViewEditMultisigCosigners = () => {
|
||||
const [isMnemonicsModalVisible, setIsMnemonicsModalVisible] = useState(false);
|
||||
const [isShareModalVisible, setIsShareModalVisible] = useState(false);
|
||||
const [importText, setImportText] = useState('');
|
||||
const [exportString, setExportString] = useState('{}');
|
||||
const [exportString, setExportString] = useState('{}'); // used in exportCosigner()
|
||||
const [exportStringURv2, setExportStringURv2] = useState(''); // used in QR
|
||||
const [exportFilename, setExportFilename] = useState('bw-cosigner.json');
|
||||
const [vaultKeyData, setVaultKeyData] = useState({ keyIndex: 1, xpub: '', seed: '', path: '', fp: '', isLoading: false }); // string rendered in modal
|
||||
const data = useRef();
|
||||
@ -330,6 +332,7 @@ const ViewEditMultisigCosigners = () => {
|
||||
isLoading: false,
|
||||
});
|
||||
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
|
||||
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
|
||||
setExportFilename('bw-cosigner-' + fp + '.json');
|
||||
setIsMnemonicsModalVisible(true);
|
||||
},
|
||||
@ -377,6 +380,7 @@ const ViewEditMultisigCosigners = () => {
|
||||
const path = wallet.getCustomDerivationPathForCosigner(keyIndex);
|
||||
const xpub = wallet.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(seed, path));
|
||||
setExportString(MultisigCosigner.exportToJson(fp, xpub, path));
|
||||
setExportStringURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]);
|
||||
setExportFilename('bw-cosigner-' + fp + '.json');
|
||||
},
|
||||
}}
|
||||
@ -544,7 +548,7 @@ const ViewEditMultisigCosigners = () => {
|
||||
<Text style={[styles.headerText, stylesHook.textDestination]}>{loc.multisig.this_is_cosigners_xpub}</Text>
|
||||
<View style={styles.qrCodeContainer}>
|
||||
<QRCode
|
||||
value={exportString}
|
||||
value={exportStringURv2}
|
||||
size={260}
|
||||
color="#000000"
|
||||
logoBackgroundColor={colors.brandingColor}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import assert from 'assert';
|
||||
import { MultisigHDWallet } from '../../class/';
|
||||
import { decodeUR } from 'bc-ur/dist';
|
||||
import { decodeUR, encodeUR } from '../../blue_modules/ur';
|
||||
import { MultisigCosigner } from '../../class/multisig-cosigner';
|
||||
const bitcoin = require('bitcoinjs-lib');
|
||||
const Base43 = require('../../blue_modules/base43');
|
||||
@ -1790,6 +1790,23 @@ describe('multisig-cosigner', () => {
|
||||
assert.strictEqual(cosigner.howManyCosignersWeHave(), 1);
|
||||
});
|
||||
|
||||
it('can parse cobo URv2 account', () => {
|
||||
let decoded = decodeUR([
|
||||
'UR:CRYPTO-ACCOUNT/OEADCYADWMTNKIAOLYTAADMETAADDLOLAOWKAXHDCLAXHPDIHNWKMYCFHNROCARHSESKLDPDSWOTMWGTJNGWIYHYYNWSOLENTSUEMKTOTAVDAAHDCXBDHHBZSBURBSMKZOECOEHHJPHTSRVACMBGHEROMYCKHHNBHFHGNBGMNYAESPNDFHAHTAADEHOEADADAOAEAMTAADDYOTADLOCSDYYKAEYKAEYKAOYKAOCYADWMTNKIAXAAAYCYLOWLDITOJTCNDTAY',
|
||||
]);
|
||||
decoded = Buffer.from(decoded, 'hex').toString('ascii');
|
||||
|
||||
const cosigner = new MultisigCosigner(decoded);
|
||||
assert.ok(cosigner.isValid());
|
||||
assert.strictEqual(
|
||||
cosigner.getXpub(),
|
||||
'Zpub756tPxxwHiYkYiT12G2WUD2cpAHyVWhjvKPbXoY5jDZSyo71yG5C14LCuwhycTTAzgTUcQfddR8FFTQ1bSWR6kzmNbMEaVzUrj4Lhxbonjo',
|
||||
);
|
||||
assert.strictEqual(cosigner.getPath(), "m/48'/0'/0'/2'");
|
||||
assert.strictEqual(cosigner.howManyCosignersWeHave(), 1);
|
||||
assert.strictEqual(cosigner.getFp(), '01EBDA7D');
|
||||
});
|
||||
|
||||
it('can parse plain Zpub', () => {
|
||||
const cosigner = new MultisigCosigner(Zpub1);
|
||||
assert.ok(cosigner.isValid());
|
||||
@ -1890,4 +1907,66 @@ describe('multisig-cosigner', () => {
|
||||
assert.strictEqual(w._getExternalAddressByIndex(0), 'bc1qtysquqsjqjfqvhd6l2h470hdgwhcahs4nq2ca49cyxftwjnjt9ssh8emel');
|
||||
}
|
||||
});
|
||||
|
||||
it('can export to json', () => {
|
||||
const result = MultisigCosigner.exportToJson(fp1cobo, Zpub1, "m/48'/0'/0'/2'");
|
||||
assert.strictEqual(
|
||||
result,
|
||||
'{"xfp":"D37EAD88","xpub":"Zpub74ijpfhERJNjhCKXRspTdLJV5eoEmSRZdHqDvp9kVtdVEyiXk7pXxRbfZzQvsDFpfDHEHVtVpx4Dz9DGUWGn2Xk5zG5u45QTMsYS2vjohNQ","path":"m/48\'/0\'/0\'/2\'"}',
|
||||
);
|
||||
|
||||
const cosigner = new MultisigCosigner(MultisigCosigner.exportToJson(fp1cobo, Zpub1, "m/48'/0'/0'/2'"));
|
||||
assert.strictEqual(cosigner.getFp(), 'D37EAD88');
|
||||
assert.strictEqual(
|
||||
cosigner.getXpub(),
|
||||
'Zpub74ijpfhERJNjhCKXRspTdLJV5eoEmSRZdHqDvp9kVtdVEyiXk7pXxRbfZzQvsDFpfDHEHVtVpx4Dz9DGUWGn2Xk5zG5u45QTMsYS2vjohNQ',
|
||||
);
|
||||
assert.strictEqual(cosigner.getPath(), "m/48'/0'/0'/2'");
|
||||
assert.strictEqual(cosigner.isValid(), true);
|
||||
assert.strictEqual(cosigner.isNativeSegwit(), true);
|
||||
assert.strictEqual(cosigner.isWrappedSegwit(), false);
|
||||
assert.strictEqual(cosigner.isLegacy(), false);
|
||||
|
||||
// using bad xpub just to check chaincode & keyhex
|
||||
const c2 = new MultisigCosigner(
|
||||
MultisigCosigner.exportToJson(
|
||||
fp1cobo,
|
||||
'zpub6qT7amLcp2exr4mU4AhXZMjD9CFkopECVhUxc9LHW8pNsJG2B9ogs5sFbGZpxEeT5TBjLmc7EFYgZA9EeWEM1xkJMFLefzZc8eigRFhKB8Q',
|
||||
"m/48'/0'/0'/2'",
|
||||
),
|
||||
);
|
||||
assert.strictEqual(c2.getChainCodeHex(), '906730ab8a03fb4baa32a912134f2c5bfe6ae70e0264e4e9fe4b0abe3a560692');
|
||||
assert.strictEqual(c2.getKeyHex(), '0395b643f45bd89fede6f4f6416b288e73005419b48cdcd88465913bd31b4be5ea');
|
||||
assert.strictEqual(c2.getParentFingerprintHex(), '125688b1');
|
||||
assert.strictEqual(c2.getDepthNumber(), 3);
|
||||
});
|
||||
|
||||
it('can export cosigner to URv2', () => {
|
||||
let result = encodeUR(MultisigCosigner.exportToJson(fp1cobo, Zpub1, "m/48'/0'/0'/2'"));
|
||||
assert.deepStrictEqual(result, [
|
||||
'ur:crypto-account/oeadcytekbpmloaolytaadmetaaddloxaxhdclaofejnolgudllagdgodyweehzsmeyasnswrpdalnwzfenbmewlrtplsklbjkvdloweaahdcxltjzjpctayfsimuogtpypffrnlisflswwzntbecabtbdwdbstojnfrahdamnpfcyamtaaddyotadlocsdyykaeykaeykaoykaocytekbpmloaxaaaycyghykhpcmkgnstevs',
|
||||
]);
|
||||
|
||||
result = encodeUR(
|
||||
MultisigCosigner.exportToJson(
|
||||
'42A2460E',
|
||||
'Ypub6m2WhkZvujztfZVYWEB4Hfcq3mKfeZYMfZj2wfvgNmTDjcCncU9ua6VSxXno7FeF8P2kqp1S7N8UoYapR8YKnMLNq8bEDDd2PU6q7QCHoEb',
|
||||
"m/48'/0'/0'/1'",
|
||||
),
|
||||
);
|
||||
assert.deepStrictEqual(result, [
|
||||
'ur:crypto-account/oeadcyfwoefgbaaolytaadmhtaadmetaaddloxaxhdclaxsblplucfptgtwywzcmnshtotqzleihrnndtoeodrfdpfoyeyqzsbenrplbhtdymuaahdcxlgbdsrcxatcmdpuokpwzvymttatphtlftplsvlgmeeflpdtanlromhfgvekbbznyamtaaddyotadlocsdyykaeykaeykadykaocyfwoefgbaaxaaaycywsutbeuyyndacaeh',
|
||||
]);
|
||||
|
||||
result = encodeUR(
|
||||
MultisigCosigner.exportToJson(
|
||||
'ED5C5B8A',
|
||||
'xpub69dgpFkP9mFYhaAWt6svmwd1BYsuGiyyNs8sJW1GwCn8GSK69mrCmNG6ZLcrPGvBSiJzfjXD66ntgJxdqQbhMk4j273VQYHEMc5knoqFGvt',
|
||||
"m/45'",
|
||||
),
|
||||
);
|
||||
assert.deepStrictEqual(result, [
|
||||
'ur:crypto-account/oeadcywehhhpleaolytaadmhtaaddloxaxhdclaoamutctbahthnislelbwemnkeoefnhddienfetbpygrpaqdkemyrywyldaspyjkdtaahdcxhyneskwdhlehlfbwrpdnjlgsgakplkjtknvyttsgolnnlbwlcagoolcpfgsglkinamtaaddyotadlfcsdpykaocywehhhpleaxadaycywehhhplekpdwveih',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { WatchOnlyWallet } from '../../class';
|
||||
import { decodeUR } from 'bc-ur/dist';
|
||||
import { decodeUR, encodeUR, setUseURv1, clearUseURv1, extractSingleWorkload, BlueURDecoder } from '../../blue_modules/ur';
|
||||
import { Psbt } from 'bitcoinjs-lib';
|
||||
const assert = require('assert');
|
||||
|
||||
@ -339,7 +339,7 @@ describe('Watch only wallet', () => {
|
||||
});
|
||||
|
||||
describe('BC-UR', () => {
|
||||
it('can decodeUR() and then combine unfinalized signed PSBT', () => {
|
||||
it('v1: can decodeUR() and then combine unfinalized signed PSBT', () => {
|
||||
const unsignedPayload = decodeUR([
|
||||
'UR:BYTES/TYQ4XURNVF607QGQWYPQQQQQQ9U63JU4AD5C93Y057WNRNTV24AE8QK4DDHVT04GHTKNQZCXYHNW5QGQQQQQPLHLLLLS9LRRQQQQQQQQQQTQQ9P9YMAAVV5GVUNKD49W4GDNJ4C9GJP7383QFCQQQQQQQQQPVQQ5CXKNG9PNTGMDRV0GNWNJZS23KGG3V0KXQQQQQQQQQYQ375XRQQQQQQQQQQTQQ98UXJHTKAHE83Q8W5VGHH2G93698VZLP6PZQCPXW47RAFD36W04SNHNTZK8CLCWHXDJJRRZ2EP998STFNRYWFQPC0CC3N8X87Z5QQQGQQQQQZQQQQQQSQQQQQQQQQQQQQQQYGPQY5M4J23F3Z9TK6HZTRDD6M89QX955DEH3HXGXAC6NJQMT3CHYTJHRZXVUCLC2SQQPQQQQQQGQQQQQZQQZQQQQQQQQQQQQQ3QYQK6E2MCA75ZCRMMWZYWXNQKGKNNJC7JUXPNWR5QPYQC3EYRM4NDQ5VGENNRLP2QQQYQQQQQPQQQQQQGQQQQQQQQZQQQQQQQ6GYX3G',
|
||||
]);
|
||||
@ -360,4 +360,135 @@ describe('BC-UR', () => {
|
||||
'0200000000010179a8cb95eb6982c48fa79d31cd6c557b9382d56b6ec5bea8baed300b0625e6ea0100000000feffffff02fc630000000000001600142526fbd63288672766d4aeaa1b3957054483e89e204e000000000000160014c1ad3414335a36d1b1e89ba7214151b211163ec602473044022077ad33068b4a8da130ac8a64a3efbbaabaf18fa20d705adc86d53cbb69c18258022035eea4daadf9ba51080f57a7a3476d9f300414e82c544d58a24e682161e4be700121026757c3ea5b1d39f584ef358ac7c7f0eb99b290c625642529e0b4cc6472401c3f00000000',
|
||||
);
|
||||
});
|
||||
|
||||
it('v1: decodeUR() txt works', () => {
|
||||
const txtFileFormatMultisigNativeSegwit =
|
||||
'UR:BYTES/TYQHKGEQGDHKYM6KV96KCAPQF46KCARFWD5KWGRNV4682UPQVE5KCEFQ9P3HYETPW3JKGGR0DCSYGVEHG4Q5GWPC9Y9ZXZJWV9KK2W3QGDT97VENGG65YWF3G90NYTFJPFGX7MRFVDUN5GPJYPHKVGPJPFZX2UNFWESHG6T0DCAZQMF0XSUZWTESYUHNQFE0XGNS53N0WFKKZAP6YPGRY46NFQ9Q53PNXAZ5Z3PC8QAZQKNSW43RWDRFDFCXV6Z92F9YU6NGGD94S5NNWP2XGNZ22C6K2M69D4F4YKNYFPC5GANS8944VARY2EZHJ62CDVMHQKRC2F3XVKN629M8X3ZXWPNYGJZ9FPT8G4NS0Q6YG73EG3R42468DCE9S6E40FRN2AF5X4G4GNTNT9FNYAN2DA5YU5G2XYMRS3ZYXCCRXW3QTFC82C3HX4K5Z3FCG448J7ZN0FHHJ5RDGAHXGD29XEXHJ3PHG9XYWNNWV3E824MKX5E8SUR6D9K4552TW44HWAJ9VEV9GJR3D4YRSMNZVF3NVCMR2Q6HGVNPF5EK6AMNXDCYKK2NDE9HQJ6DF4UHGERZFEZ453J40P9H57N5T9RY6WZSDC9QWZ5LU2';
|
||||
const rez = decodeUR([txtFileFormatMultisigNativeSegwit]);
|
||||
const b = Buffer.from(rez, 'hex');
|
||||
assert.strictEqual(
|
||||
b.toString('ascii'),
|
||||
"# CoboVault Multisig setup file (created on D37EAD88)\n#\nName: CV_33B5B91A_2-2\nPolicy: 2 of 2\nDerivation: m/48'/0'/0'/2'\nFormat: P2WSH\n\nD37EAD88: Zpub74ijpfhERJNjhCKXRspTdLJV5eoEmSRZdHqDvp9kVtdVEyiXk7pXxRbfZzQvsDFpfDHEHVtVpx4Dz9DGUWGn2Xk5zG5u45QTMsYS2vjohNQ\n168DD603: Zpub75mAE8EjyxSzoyPmGnd5E6MyD7ALGNndruWv52xpzimZQKukwvEfXTHqmH8nbbc6ccP5t2aM3mws3pKYSnKpKMMytdbNEZFUxKzztYFM8Pn\n",
|
||||
);
|
||||
});
|
||||
|
||||
it('v2: decodeUR() crypto-account works', () => {
|
||||
const payload =
|
||||
'UR:CRYPTO-ACCOUNT/OEADCYADWMTNKIAOLYTAADMWTAADDLOSAOWKAXHDCLAXMDRPFXWKHPTPNEWEVAWKYNFPJEDEMNJKAEGHCFQZLKUOTPLRIHMEFRTECWGRVWWDAAHDCXMHIODYPYLEAXZOGRPKEYPTBGBWGWDWHPZEIMVDBAAOIEVEWLZEGRBKRNFTHFAMMOAHTAADEHOEADADAOAEAMTAADDYOTADLNCSGHYKAEYKAEYKAOCYADWMTNKIAXAXATTAADDYOEADLRAEWKLAWKAXAEAYCYBGHFLOPACMIOWZLB';
|
||||
|
||||
const [index, total] = extractSingleWorkload(payload);
|
||||
assert.strictEqual(index, 1);
|
||||
assert.strictEqual(total, 1);
|
||||
|
||||
const decoded = decodeUR([payload]);
|
||||
|
||||
assert.strictEqual(
|
||||
Buffer.from(decoded, 'hex').toString('ascii'),
|
||||
'{"ExtPubKey":"zpub6qT7amLcp2exr4mU4AhXZMjD9CFkopECVhUxc9LHW8pNsJG2B9ogs5sFbGZpxEeT5TBjLmc7EFYgZA9EeWEM1xkJMFLefzZc8eigRFhKB8Q","MasterFingerprint":"01EBDA7D","AccountKeyPath":"m/84\'/0\'/0\'"}',
|
||||
);
|
||||
});
|
||||
|
||||
it('v2: can decodeUR() PSBT', () => {
|
||||
const payload = decodeUR([
|
||||
'UR:CRYPTO-PSBT/HKADGSJOJKIDJYZMADAEJYAOAEAEAEADWKMTGWJPPFGMCKJLKPNDNDBWAHBEAXCNFHPKRHUTPMGTBAFNWEBTLBECKENNBDJKADAEAEAEAEZMZMZMZMAONBLNADAEAEAEAEAECFKOPTBBCFBGNTGUVAEHNDPECFUYNBHKRNPMCMJNYTBKROYKLOPSVOHTADAEAEAEAEAECMAEBBWEWETAYKBETTTDISVDGYTTGMEHLSDMASFYPSPYDRAEAEAEAEAEADADCTLNZMAOAEAEAEAEAECMAEBBJYLSWNATMWIOEMHTPMFXCWMTGLZSTPVSCMWDLBKKADAYJEAOFLDYFYAOCXGYFNRKKPVYWFWEGLFZTYLDSFWNNEFGCTIMPEFHWMCWNNMTCHHTMYGRSOFRLODSAEAOCXKTKBHDNDCEFLMEBYOESETTIOAACHAXZMVWDNRDHEISHKETAMCHDSEOFXIYDECPHGADCLAOHSHHTYMTPAWKLNFYESCWNBKSWDVDNNYNMNCFLOFNTTWTNYFYNTHERORKDKQDWEGWAEAECPAOAXIHWEMNLPPDZTKSTEJLBNMOWFCSVYKNMNHKHFGDRNKELFRTSFCTSRZSSGJZAXRNHYCSADWMTNKIGHAEAELAAEAEAELAAEAEAELAADAEAEAELNAEAEAEAESSAOMKSP',
|
||||
]);
|
||||
|
||||
const uPsbtB64 = Buffer.from(payload, 'hex').toString('base64');
|
||||
|
||||
const psbtTx = Psbt.fromBase64(uPsbtB64);
|
||||
assert.strictEqual(
|
||||
psbtTx.extractTransaction().toHex(),
|
||||
'02000000000101f4964f72b0521e6f759b9b13051003233faab9ddad4d0e3ced0d7f357c9e0b730100000000ffffffff02a0860100000000001976a91419129d53e6319baf19dba059bead166df90ab8f588ace25a010000000000160014ededd9f510d1d268e751d15231832e0944acab2a024730440220513cbb75e1f3ed4e40d489ccf19f461f6aaf3feb1b9e96175a8f4bc93b8826000220777e589b1c479111a2c1d167041703ffe52bba5f685938061726334366282257012102615cd496b1f48644391ba078eae79ef68e19883cd1f09a449d5fb8bb24b3ed4f00000000',
|
||||
);
|
||||
|
||||
// now, full psbt tx via parts:
|
||||
const decoder = new BlueURDecoder();
|
||||
decoder.receivePart(
|
||||
'UR:CRYPTO-PSBT/33-2/LPCSCLAOCFAOIOCYCSKEMSHLHKADEEFZZEMETDFRIEAEAXFPTACLNTMTTKGRAXKTKGUOFPMOGWCWIYMEKKVDVONETLPRKGBSONGAKKEMCNGALGOXBDFTLSJLHERNWKINJPADAYJEZCROTKRKGHJPFEAMSRJNJLMYETDSGYWFBSSNPFUTPFDIJOCYWZFLZENSKOYNSOVLVSPTVTHFPKTAAOCXCPFYMKKPKPBEGLMSCMDNVSPYJNTNLKGTFMZMRSFWSNZEPYVTWFPTQDDTPERPPSLREEHLNSKNLOENSTFYFDTSSOFZNEIAVYTDISWEOTUYHLFMGWOLCNMSTKZSFLNSPFKTWSDNECCFTNOYFGETGMBBJNYTBKROYKNNPSBBJYLSWNATMDIYDEGLTYFWCWMTGLZSTPZECMZEGADMNNLDDWREFSZTGHLOOSYTEMIEWKBWHKHHNTCPGHBNMELSLGBAPDGELAHYSBWLAMYNCSFRNLFRKPMWFNUEKSAEURNNLYPLPMLTVWVDDKBTVWBWLDJKYKWPCTJZIEEHSANYMHZMNDZEDYCWBZDPKBGSBARKIMAYFLGYIMCFLSMKENPEEOPSRFWSJKINLFYLPECYQZZCLBLSKOHLHGJZRHRYVTLOGLCMHGHLPAHNWYMULS',
|
||||
);
|
||||
decoder.receivePart(
|
||||
'UR:CRYPTO-PSBT/47-2/LPCSDLAOCFAOIOCYCSKEMSHLHKADEEFZZEMETDFRIEAEAXFPTACLNTMTTKGRAXKTKGUOFPMOGWCWIYMEKKVDVONETLPRKGBSONGAKKEMCNGALGOXBDFTLSJLHERNWKINJPADAYJEZCROTKRKGHJPFEAMSRJNJLMYETDSGYWFBSSNPFUTPFDIJOCYWZFLZENSKOYNSOVLVSPTVTHFPKTAAOCXCPFYMKKPKPBEGLMSCMDNVSPYJNTNLKGTFMZMRSFWSNZEPYVTWFPTQDDTPERPPSLREEHLNSKNLOENSTFYFDTSSOFZNEIAVYTDISWEOTUYHLFMGWOLCNMSTKZSFLNSPFKTWSDNECCFTNOYFGETGMBBJNYTBKROYKNNPSBBJYLSWNATMDIYDEGLTYFWCWMTGLZSTPZECMZEGADMNNLDDWREFSZTGHLOOSYTEMIEWKBWHKHHNTCPGHBNMELSLGBAPDGELAHYSBWLAMYNCSFRNLFRKPMWFNUEKSAEURNNLYPLPMLTVWVDDKBTVWBWLDJKYKWPCTJZIEEHSANYMHZMNDZEDYCWBZDPKBGSBARKIMAYFLGYIMCFLSMKENPEEOPSRFWSJKINLFYLPECYQZZCLBLSKOHLHGJZRHRYVTLOGLCMHGHLPAYKAOHEGU',
|
||||
);
|
||||
decoder.receivePart(
|
||||
'UR:CRYPTO-PSBT/73-2/LPCSGAAOCFAOIOCYCSKEMSHLHKADEEFZZEMETDFRIEAEAXFPTACLNTMTTKGRAXKTKGUOFPMOGWCWIYMEKKVDVONETLPRKGBSONGAKKEMCNGALGOXBDFTLSJLHERNWKINJPADAYJEZCROTKRKGHJPFEAMSRJNJLMYETDSGYWFBSSNPFUTPFDIJOCYWZFLZENSKOYNSOVLVSPTVTHFPKTAAOCXCPFYMKKPKPBEGLMSCMDNVSPYJNTNLKGTFMZMRSFWSNZEPYVTWFPTQDDTPERPPSLREEHLNSKNLOENSTFYFDTSSOFZNEIAVYTDISWEOTUYHLFMGWOLCNMSTKZSFLNSPFKTWSDNECCFTNOYFGETGMBBJNYTBKROYKNNPSBBJYLSWNATMDIYDEGLTYFWCWMTGLZSTPZECMZEGADMNNLDDWREFSZTGHLOOSYTEMIEWKBWHKHHNTCPGHBNMELSLGBAPDGELAHYSBWLAMYNCSFRNLFRKPMWFNUEKSAEURNNLYPLPMLTVWVDDKBTVWBWLDJKYKWPCTJZIEEHSANYMHZMNDZEDYCWBZDPKBGSBARKIMAYFLGYIMCFLSMKENPEEOPSRFWSJKINLFYLPECYQZZCLBLSKOHLHGJZRHRYVTLOGLCMHGHLPASAVWCXNE',
|
||||
);
|
||||
decoder.receivePart(
|
||||
'UR:CRYPTO-PSBT/75-2/LPCSGRAOCFAOIOCYCSKEMSHLHKADEECFZTYKOEFDAMJYZTFZTALNNEMTTKGRAEADADCTCYWMAOAEAEAEAEAECMAEBBTYKNASSBGUVSCFDYOEUOWYDRKPSEJOUYMORPISJPADAYJEAOFLDYFYAOCXBYYKBYFMMTSPFYFZFYWESNHNEYSWGDWSIOBNHYBTGETNSBJOEYLNOSGUGOVOPYTAAOCXCPRKIOLELEVETPTPIENDRDREAOPECHTBDPZSPEFPWYSEADHKDMAAZEDIMUHPOYZOADCLAOJSZOEMSTFYFDTSENRSHNNSVTLNFGWDOTUYHLFMGWRSGOFMUYVLGOADVLMEUEPFNYAEADADCTLNZMAOAEAEAEAEAECMAEBBJYLSWNATMWIOEMHTPMFXCWMTGLZSTPVSCMWDLBKKADAYJEAOFLDYFYAOCXIYQZMEGUDAAXSOKNWMGOAAZSLYSGFMWPFDNBDPBDJEFSMSDLPFJSWMHLAXKNTDGEAOCXDKHEPTGMZMYACWCEDEJEEORHOYCAHYSRJTFYDSMHROFTDYPEFGZMRSRLJZBDAMONADCLAOHSHHTYMTPAWKLNFYESCWNBKSWDVDNNYNMNCFLOFNTTWTNYFYNTHERORKDKQDWEGWAEAEAEPDBAJSAH',
|
||||
);
|
||||
assert.strictEqual(decoder.estimatedPercentComplete(), 1);
|
||||
const psbt = Psbt.fromBase64(decoder.toString());
|
||||
assert.ok(psbt);
|
||||
});
|
||||
|
||||
it('v2: can decodeUR() multipart bytes', () => {
|
||||
const decoder = new BlueURDecoder();
|
||||
decoder.receivePart(
|
||||
'UR:BYTES/246-2/LPCSYNAOCFADKECYCEBTIDBGHDRNGRISEEECIOEYFWFLGEFPGOKOFWKSFXGTKTKKHTKOEYEEGOJLECKNJPEEGRGRIAHKHKESEEEOFXFYGEGMGEJNETHSJNFDGOIHJOIOFPHFIEKPGOGOEYINKSGOJOGYESIYGYKNEHBKDYEHFEFWFYFPEMFYFTCXHTJOKPIDEMECENJYGDKSKSKTFDINHKJEHKINGHEHEYFLEYHGGOFYEYIAJOFPFDKKHFHGISIMKOGRGDIDHDJLHKECIMFYHTGUKKJLEMEHKKFLECFXEHEEGSFXKPKTISKKIAGHGHFPKNIOGHGOIAGYIYIEIEGMETFGFGGHGYEHIDGUHGGMENJEKNJNGLIDGTFEHSHFKNGOJPIMEEGSISKSIDJLJTIMJLBKCFCFRYME',
|
||||
);
|
||||
decoder.receivePart(
|
||||
'UR:BYTES/243-2/LPCSWFAOCFADKECYCEBTIDBGHDRNBGINGTCMFLKKDIFMESECFTCSDIHDBAETCWBTEOAHHPGUKPCEGDBAATFYJEDPBKECFNCFCEGDEHCLDNDSDLASCSBAAXISIHGHECDAAHCHGUEHKEHEBYIAENEECAEEAXFGCEBSHLKBHKFWDWDAIECHHFEHHFGHGDCXCYBAHYHFGWGLJOGEHDCSDMGAJEHSCABNDSHDFYDSFGFMFTDRAYFXCAJTKEFPJSKSHDGTHKKGKTGTIMFDGUJKAHENEMEYBTGOCHHSGRBEIYBDFRFMASKTEOFLDWFRGMIYJTHSCXCHCLEMGHIHCNDRCKCHJTCTATDKFRHKGYASENDRGWCFAYGHAABGAXFHFRCHFMADDYDYKPDNCWBKHPCEBDAODIJTBBFGRHFH',
|
||||
);
|
||||
decoder.receivePart(
|
||||
'UR:BYTES/240-2/LPCSWTAOCFADKECYCEBTIDBGHDRNHKADKKCNCXGRIHKKJKJYJLJTIHCXGTKPJZJYINJKINIOCXJKIHJYKPJOCXIYINJZIHCXDEIAJPIHHSJYIHIECXJLJTCXDYEHFEFWFYFPEMFYDTBKCNBKGLHSJNIHFTCXGTKPJZJYINJKINIOCXHFHSKPJZJYBKGDJLJZINIAKKFTCXEYCXJLIYCXEYBKFYIHJPINKOHSJYINJLJTFTCXJNDLEEETDIDLDYDIDLDYDIDLEYDIBKFGJLJPJNHSJYFTCXGDEYHGGUFDBKBKFEEEFGDYFYFWEHEYFTCXHTJOKPIDEMEEFEGLKNFEHFHKFPJOIMISEOHTHSKSKKJPJPESGEJOGLKNHTFPFYGHFWHTFPIOJKJPESJKIHISFDIEIYENASAX',
|
||||
);
|
||||
decoder.receivePart(
|
||||
'UR:BYTES/238-2/LPCSWYAOCFADKECYCEBTIDBGHDRNGRISEEECIOEYFWFLGEFPGOKOFWKSFXGTKTKKHTKOEYEEGOJLECKNJPEEGRGRIAHKHKESEEEOFXFYGEGMGEJNETHSJNFDGOIHJOIOFPHFIEKPGOGOEYINKSGOJOGYESIYGYKNEHBKDYEHFEFWFYFPEMFYFTCXHTJOKPIDEMECENJYGDKSKSKTFDINHKJEHKINGHEHEYFLEYHGGOFYEYIAJOFPFDKKHFHGISIMKOGRGDIDHDJLHKECIMFYHTGUKKJLEMEHKKFLECFXEHEEGSFXKPKTISKKIAGHGHFPKNIOGHGOIAGYIYIEIEGMETFGFGGHGYEHIDGUHGGMENJEKNJNGLIDGTFEHSHFKNGOJPIMEEGSISKSIDJLJTIMJLBKNYBTOSBE',
|
||||
);
|
||||
decoder.receivePart(
|
||||
'UR:BYTES/235-2/LPCSWMAOCFADKECYCEBTIDBGHDRNHKADKKCNCXGRIHKKJKJYJLJTIHCXGTKPJZJYINJKINIOCXJKIHJYKPJOCXIYINJZIHCXDEIAJPIHHSJYIHIECXJLJTCXDYEHFEFWFYFPEMFYDTBKCNBKGLHSJNIHFTCXGTKPJZJYINJKINIOCXHFHSKPJZJYBKGDJLJZINIAKKFTCXEYCXJLIYCXEYBKFYIHJPINKOHSJYINJLJTFTCXJNDLEEETDIDLDYDIDLDYDIDLEYDIBKFGJLJPJNHSJYFTCXGDEYHGGUFDBKBKFEEEFGDYFYFWEHEYFTCXHTJOKPIDEMEEFEGLKNFEHFHKFPJOIMISEOHTHSKSKKJPJPESGEJOGLKNHTFPFYGHFWHTFPIOJKJPESJKIHISFDIETODMPFCY',
|
||||
);
|
||||
decoder.receivePart(
|
||||
'UR:BYTES/224-2/LPCSVTAOCFADKECYCEBTIDBGHDRNBGINGTCMFLKKDIFMESECFTCSDIHDBAETCWBTEOAHHPGUKPCEGDBAATFYJEDPBKECFNCFCEGDEHCLDNDSDLASCSBAAXISIHGHECDAAHCHGUEHKEHEBYIAENEECAEEAXFGCEBSHLKBHKFWDWDAIECHHFEHHFGHGDCXCYBAHYHFGWGLJOGEHDCSDMGAJEHSCABNDSHDFYDSFGFMFTDRAYFXCAJTKEFPJSKSHDGTHKKGKTGTIMFDGUJKAHENEMEYBTGOCHHSGRBEIYBDFRFMASKTEOFLDWFRGMIYJTHSCXCHCLEMGHIHCNDRCKCHJTCTATDKFRHKGYASENDRGWCFAYGHAABGAXFHFRCHFMADDYDYKPDNCWBKHPCEBDAODIJTSAPMYNHK',
|
||||
);
|
||||
assert.strictEqual(decoder.estimatedPercentComplete(), 1);
|
||||
const str = decoder.toString();
|
||||
|
||||
assert.ok(str.includes('E4F0DB12'));
|
||||
assert.ok(str.includes('Keystone Multisig setup file'));
|
||||
});
|
||||
|
||||
it('v1: decodeUR() works', async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // sleep
|
||||
// sleep is needed because in test envirnment setUseURv1() and init function have a race condition
|
||||
await setUseURv1();
|
||||
const txt = 'hello world';
|
||||
const b = Buffer.from(txt, 'ascii');
|
||||
let fragments = encodeUR(b.toString('hex'), 666);
|
||||
assert.deepStrictEqual(fragments, ['ur:bytes/fd5x2mrvdus8wmmjd3jqugwtl9']);
|
||||
assert.strictEqual(Buffer.from(decodeUR(fragments), 'hex').toString('ascii'), txt);
|
||||
|
||||
fragments = encodeUR(b.toString('hex'), 10);
|
||||
assert.deepStrictEqual(fragments, [
|
||||
'ur:bytes/1of3/fc38n9ue84vu8ra8ue6cdnrghws0dwep4f46q4rlrgdncwsg49lsw38e6m/fd5x2mrvdu',
|
||||
'ur:bytes/2of3/fc38n9ue84vu8ra8ue6cdnrghws0dwep4f46q4rlrgdncwsg49lsw38e6m/s8wmmjd3jq',
|
||||
'ur:bytes/3of3/fc38n9ue84vu8ra8ue6cdnrghws0dwep4f46q4rlrgdncwsg49lsw38e6m/ugwtl9',
|
||||
]);
|
||||
assert.strictEqual(Buffer.from(decodeUR(fragments), 'hex').toString('ascii'), txt);
|
||||
});
|
||||
|
||||
it('v2: decodeUR() bytes works', () => {
|
||||
const payload =
|
||||
'UR:BYTES/HKADKNCNCXGRIHKKJKJYJLJTIHCXGTKPJZJYINJKINIOCXJKIHJYKPJOCXIYINJZIHCXDEIAJPIHHSJYIHIECXJLJTCXDYEHFEFWFYFPEMFYDTBKCNBKGLHSJNIHFTCXGRGHHEFGFPFPESDYFEFWENHEEYDPEYBKGDJLJZINIAKKFTCXEYCXJLIYCXEYBKFYIHJPINKOHSJYINJLJTFTCXJNDLEEETDIDLDYDIDLDYDIDLEYDIBKFGJLJPJNHSJYFTCXGDEYHGGUFDBKBKDYEHFEFWFYFPEMFYFTCXHTJOKPIDEMECENJYGDKSKSKTFDINHKJEHKINGHEHEYFLEYHGGOFYEYIAJOFPFDKKHFHGISIMKOGRGDIDHDJLHKECIMFYHTGUKKJLEMEHKKFLECFXEHEEGSFXKPKTISKKIAGHGHFPKNIOGHGOIAGYIYIEIEGMETFGFGGHGYEHIDGUHGGMENJEKNJNGLIDGTFEHSHFKNGOJPIMEEGSISKSIDJLJTIMJLBKESEMFXFWEHECEEEYFTCXHTJOKPIDEMECHFKPHKIYKTJPFXIMEYGLHDKTFGGSGMIEIDKKKOGDGDJNGDKOEMGMJYIHGYKTFGGTHDFDGEGTGDJKFYKPFXEMKOISKOIDHGJSJLJNFEIEEMETHKJOJLGRIEEMJEGRJEIAIAGHGHFXFPJNJNIDECHFJNHDFPEHESHSISEMIMHDGYINIOGRGOIHKSJEIHGSIHKPGRIMKTJYFDKKENECJLBKYLYAHNRS';
|
||||
const result = Buffer.from(decodeUR([payload]), 'hex').toString();
|
||||
assert.ok(result.includes('Keystone Multisig setup file'));
|
||||
});
|
||||
|
||||
it('v2: encodeUR() psbt works', async () => {
|
||||
await clearUseURv1();
|
||||
const psbtHex =
|
||||
'70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000';
|
||||
|
||||
const fragments = encodeUR(psbtHex, 100);
|
||||
assert.strictEqual(fragments.length, 2);
|
||||
assert.deepStrictEqual(fragments, [
|
||||
'ur:crypto-psbt/1-2/lpadaocsptcybkgdcarhhdgohdosjojkidjyzmadaenyaoaeaeaeaohdvsknclrejnpebncnrnmnjojofejzeojlkerdonspkpkkdkykfelokgprpyutkpaeaeaeaeaezmzmzmzmlslgaaditiwpihbkispkfgrkbdaslewdfycprtjsprsgksecdratkkhktimndacnch',
|
||||
'ur:crypto-psbt/2-2/lpaoaocsptcybkgdcarhhdgokewdcaadaeaeaeaezmzmzmzmaojopkwtayaeaeaeaecmaebbtphhdnjstiambdassoloimwmlyhygdnlcatnbggtaevyykahaeaeaeaecmaebbaeplptoevwwtyakoonlourgofgvsjydpcaltaemyaeaeaeaeaeaeaeaeaeaeswhhtptt',
|
||||
]);
|
||||
});
|
||||
|
||||
it('v1: extractSingleWorkload() works', () => {
|
||||
const [index, total] = extractSingleWorkload('ur:bytes/2of3/fc38n9ue84vu8ra8ue6cdnrghws0dwep4f46q4rlrgdncwsg49lsw38e6m/s8wmmjd3jq');
|
||||
assert.strictEqual(index, 2);
|
||||
assert.strictEqual(total, 3);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user