Skip to content

Commit

Permalink
ADD: support for URv2 QR codes
Browse files Browse the repository at this point in the history
  • Loading branch information
Overtorment committed Jul 10, 2021
1 parent 7b3e6b5 commit 7526c82
Show file tree
Hide file tree
Showing 13 changed files with 642 additions and 16 deletions.
2 changes: 1 addition & 1 deletion BlueComponents.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
228 changes: 228 additions & 0 deletions blue_modules/ur/index.js
Original file line number Diff line number Diff line change
@@ -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 };
59 changes: 59 additions & 0 deletions class/multisig-cosigner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
}
}
2 changes: 1 addition & 1 deletion class/wallets/multisig-hd-wallet.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
2 changes: 1 addition & 1 deletion components/DynamicQRCode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading

0 comments on commit 7526c82

Please sign in to comment.