on master help sign in
bitcoin: add testnet support
a028757pkova
package.json+12−4
@@ -18,6 +18,11 @@
18 "@hot-loader/react-dom": "^16.8.6",
18 "@hot-loader/react-dom": "^16.8.6",
19 "@ledgerhq/hw-app-eth": "^5.5.0",
19 "@ledgerhq/hw-app-eth": "^5.5.0",
20 "@ledgerhq/hw-transport-u2f": "^5.5.0",
20 "@ledgerhq/hw-transport-u2f": "^5.5.0",
21+ "@reach/disclosure": "^0.15.0",
22+ "@reach/menu-button": "^0.15.1",
23+ "@reach/tabs": "^0.15.0",
24+ "@tlon/indigo-light": "^1.0.7",
25+ "@tlon/indigo-react": "^1.2.23",
21 "@tlon/sigil-js": "^1.4.1",
26 "@tlon/sigil-js": "^1.4.1",
22 "@welldone-software/why-did-you-render": "^3.2.3",
27 "@welldone-software/why-did-you-render": "^3.2.3",
23 "async-retry": "^1.2.3",
28 "async-retry": "^1.2.3",
@@ -37,25 +42,28 @@
37 "final-form-arrays": "^3.0.1",
42 "final-form-arrays": "^3.0.1",
38 "final-form-set-field-data": "^1.0.2",
43 "final-form-set-field-data": "^1.0.2",
39 "folktale": "^2.3.1",
44 "folktale": "^2.3.1",
45+ "formik": "^2.2.7",
40 "indigo-tokens": "^1.2.3",
46 "indigo-tokens": "^1.2.3",
41 "jszip": "^3.1.5",
47 "jszip": "^3.1.5",
42 "keccak": "^1.4.0",
48 "keccak": "^1.4.0",
43 "keythereum": "^1.0.4",
49 "keythereum": "^1.0.4",
44 "lodash": "^4.17.14",
50 "lodash": "^4.17.14",
51+ "markdown-to-jsx": "^7.1.2",
45 "more-entropy": "^0.0.7",
52 "more-entropy": "^0.0.7",
46 "new-github-issue-url": "^0.2.1",
53 "new-github-issue-url": "^0.2.1",
47 "prop-types": "^15.6.2",
54 "prop-types": "^15.6.2",
48− "react": "^16.8.6",
55+ "react": "^16.14.0",
49− "react-dom": "^16.8.6",
56+ "react-dom": "^16.14.0",
50 "react-final-form": "^6.3.0",
57 "react-final-form": "^6.3.0",
51 "react-final-form-arrays": "^3.1.0",
58 "react-final-form-arrays": "^3.1.0",
52 "react-hot-loader": "^4.12.0",
59 "react-hot-loader": "^4.12.0",
53 "react-scripts": "3.0.1",
60 "react-scripts": "3.0.1",
54 "react-teleporter": "^1.1.0",
61 "react-teleporter": "^1.1.0",
55 "secp256k1": "^3.5.2",
62 "secp256k1": "^3.5.2",
56− "bs58check": "^2.1.2",
63+ "styled-components": "^5.3.0",
64+ "styled-system": "^5.1.5",
57 "trezor-connect": "^8.1.14",
65 "trezor-connect": "^8.1.14",
58− "urbit-key-generation": "^0.18.0",
66+ "urbit-key-generation": "^0.19.0",
59 "urbit-ob": "^4.1.4",
67 "urbit-ob": "^4.1.4",
60 "urbit-paper-renderer": "^2.0.9",
68 "urbit-paper-renderer": "^2.0.9",
61 "web3": "1.0.0-beta.54",
69 "web3": "1.0.0-beta.54",
src/form/validators.js+1−2
@@ -20,7 +20,6 @@ import {
20 validateLength,
20 validateLength,
21 validateUnique,
21 validateUnique,
22 validatePsbt,
22 validatePsbt,
23− validateSignablePsbt,
24} from 'lib/validators';
23} from 'lib/validators';
25import isPromise from 'lib/isPromise';
24import isPromise from 'lib/isPromise';
26
25
@@ -125,7 +124,7 @@ export const buildEmailArrayValidator = () =>
125 buildValidator([validateUnique, buildArrayValidator(buildEmailValidator())]);
124 buildValidator([validateUnique, buildArrayValidator(buildEmailValidator())]);
126
125
127export const buildPsbtValidator = btcWallet =>
126export const buildPsbtValidator = btcWallet =>
128− buildValidator([validatePsbt, validateSignablePsbt(btcWallet)]);
127+ buildValidator([validatePsbt]);
129
128
130// the form validator is the composition of all of the field validators
129// the form validator is the composition of all of the field validators
131// plus an additional form validator function
130// plus an additional form validator function
src/lib/validators.js−19
@@ -141,22 +141,3 @@ export const validatePsbt = base64 => {
141 return 'Invalid Partially Signed Bitcoin Transaction';
141 return 'Invalid Partially Signed Bitcoin Transaction';
142 }
142 }
143};
143};
144−
145−export const validateSignablePsbt = btcWallet => base64 => {
146− const psbt = bitcoin.Psbt.fromBase64(base64);
147− return !(
148− psbt.data.inputs.some((input, idx) => {
149− try {
150− // removing already derived part, eg m/84'/0'/0'/0/0 becomes 0/0
151− const path = input.bip32Derivation[0].path
152− .split("m/84'/0'/0'/")
153− .join('');
154− const prv = btcWallet.derivePath(path).privateKey;
155− psbt.signInput(idx, bitcoin.ECPair.fromPrivateKey(prv));
156− return true;
157− } catch (e) {
158− return false;
159− }
160− }) && 'No inputs were signed'
161− );
162−};
src/views/Bitcoin/SignTransaction.js+56−30
@@ -18,9 +18,32 @@ import * as bitcoin from 'bitcoinjs-lib';
18import BridgeForm from 'form/BridgeForm';
18import BridgeForm from 'form/BridgeForm';
19import { PsbtInput } from 'form/Inputs';
19import { PsbtInput } from 'form/Inputs';
20
20
21−import { Buffer } from 'buffer';
22import { composeValidator, buildPsbtValidator } from 'form/validators';
21import { composeValidator, buildPsbtValidator } from 'form/validators';
23
22
23+const BITCOIN_MAINNET_INFO = {
24+ messagePrefix: '\x18Bitcoin Signed Message:\n',
25+ bech32: 'bc',
26+ bip32: {
27+ public: 0x04b24746,
28+ private: 0x04b2430c,
29+ },
30+ pubKeyHash: 0x00,
31+ scriptHash: 0x05,
32+ wif: 0x80,
33+};
34+
35+const BITCOIN_TESTNET_INFO = {
36+ messagePrefix: '\x18Bitcoin Signed Message:\n',
37+ bech32: 'tb',
38+ bip32: {
39+ public: 0x045f1cf6,
40+ private: 0x045f18bc,
41+ },
42+ pubKeyHash: 0x6f,
43+ scriptHash: 0xc4,
44+ wif: 0xef,
45+};
46+
24export default function SignTransaction() {
47export default function SignTransaction() {
25 const { pop } = useLocalRouter();
48 const { pop } = useLocalRouter();
26 const { urbitWallet } = useWallet();
49 const { urbitWallet } = useWallet();
@@ -29,41 +52,44 @@ export default function SignTransaction() {
29 const [signedTx, setSignedTx] = useState('');
52 const [signedTx, setSignedTx] = useState('');
30 const [psbt, setPsbt] = useState({});
53 const [psbt, setPsbt] = useState({});
31
54
32− const { private: privKey, chain } = need.urbitWallet(
55+ const { xprv: zprv } = need.urbitWallet(urbitWallet).bitcoinMainnet.keys;
33− urbitWallet
56+ const { xprv: vprv } = need.urbitWallet(urbitWallet).bitcoinTestnet.keys;
34− ).bitcoin.keys;
35−
36− const btcWallet = bitcoin.bip32.fromPrivateKey(
37− Buffer.from(privKey, 'hex'),
38− Buffer.from(chain, 'hex'),
39− bitcoin.networks.bitcoin
40− );
41
57
42 const validate = composeValidator({
58 const validate = composeValidator({
43− unsignedTransaction: buildPsbtValidator(btcWallet),
59+ unsignedTransaction: buildPsbtValidator(),
44 });
60 });
45
61
46− const onSubmit = values => {
62+ const onSubmit = (values, form) => {
47 const newPsbt = bitcoin.Psbt.fromBase64(values.unsignedTransaction);
63 const newPsbt = bitcoin.Psbt.fromBase64(values.unsignedTransaction);
64+
65+ const isTestnet = newPsbt.data.inputs[0].bip32Derivation[0].path.startsWith(
66+ "m/84'/1'/0'/"
67+ );
68+
69+ const derivationPrefix = isTestnet ? "m/84'/1'/0'/" : "m/84'/0'/0'/";
70+
71+ const btcWallet = isTestnet
72+ ? bitcoin.bip32.fromBase58(vprv, BITCOIN_TESTNET_INFO)
73+ : bitcoin.bip32.fromBase58(zprv, BITCOIN_MAINNET_INFO);
74+
75+ try {
48− const hex = newPsbt.data.inputs
76+ const hex = newPsbt.data.inputs
49− .reduce((psbt, input, idx) => {
77+ .reduce((psbt, input, idx) => {
50− // removing already derived part, eg m/84'/0'/0'/0/0 becomes 0/0
78+ // removing already derived part, eg m/84'/0'/0'/0/0 becomes 0/0
51− const path = input.bip32Derivation[0].path
79+ const path = input.bip32Derivation[0].path
52− .split("m/84'/0'/0'/")
80+ .split(derivationPrefix)
53− .join('');
81+ .join('');
54− const prv = btcWallet.derivePath(path).privateKey;
82+ const prv = btcWallet.derivePath(path).privateKey;
55− try {
56 return psbt.signInput(idx, bitcoin.ECPair.fromPrivateKey(prv));
83 return psbt.signInput(idx, bitcoin.ECPair.fromPrivateKey(prv));
57− } catch (e) {
58− return psbt;
59− }
60− }, newPsbt)
84+ }, newPsbt)
61− .finalizeAllInputs()
85+ .finalizeAllInputs()
62− .extractTransaction()
86+ .extractTransaction()
63− .toHex();
87+ .toHex();
64−
65− setPsbt(newPsbt);
88+ setPsbt(newPsbt);
66− setSignedTx(hex);
89+ setSignedTx(hex);
90+ } catch (error) {
91+ return { unsignedTransaction: 'Can not sign transaction' };
92+ }
67 };
93 };
68
94
69 const initialValues = {};
95 const initialValues = {};
src/views/Bitcoin/Xpub.js+23−21
@@ -1,5 +1,7 @@
1import React from 'react';
1import React from 'react';
2+import { useState } from 'react';
2import { Grid, Text } from 'indigo-react';
3import { Grid, Text } from 'indigo-react';
4+import { ToggleSwitch } from '@tlon/indigo-react';
3import { useWallet } from 'store/wallet';
5import { useWallet } from 'store/wallet';
4
6
5import { useLocalRouter } from 'lib/LocalRouter';
7import { useLocalRouter } from 'lib/LocalRouter';
@@ -12,32 +14,16 @@ import CopiableAddressWrap from 'components/CopiableAddressWrap';
12import Highlighted from 'components/Highlighted';
14import Highlighted from 'components/Highlighted';
13
15
14import * as need from 'lib/need';
16import * as need from 'lib/need';
15−import * as bitcoin from 'bitcoinjs-lib';
16−import * as bs58check from 'bs58check';
17−import { Buffer } from 'buffer';
18−
19−function xpubToZpub(xpub) {
20− var data = bs58check.decode(xpub);
21− data = data.slice(4);
22− data = Buffer.concat([Buffer.from('04b24746', 'hex'), data]);
23− return bs58check.encode(data);
24−}
25
17
26export default function Xpub() {
18export default function Xpub() {
27 const { pop } = useLocalRouter();
19 const { pop } = useLocalRouter();
28 const { urbitWallet } = useWallet();
20 const { urbitWallet } = useWallet();
29
21
22+ const { xpub: zpub } = need.urbitWallet(urbitWallet).bitcoinMainnet.keys;
30− const { public: pubKey, chain } = need.urbitWallet(urbitWallet).bitcoin.keys;
23+ const { xpub: vpub } = need.urbitWallet(urbitWallet).bitcoinTestnet.keys;
31
24
32− const zPub = xpubToZpub(
25+ const [xpub, setXpub] = useState(zpub);
33− bitcoin.bip32
26+ const [btcMainnet, setBtcMainnet] = useState(true);
34− .fromPublicKey(
35− Buffer.from(pubKey, 'hex'),
36− Buffer.from(chain, 'hex'),
37− bitcoin.networks.bitcoin
38− )
39− .toBase58()
40− );
41
27
42 return (
28 return (
43 <View pop={pop} inset>
29 <View pop={pop} inset>
@@ -55,9 +41,25 @@ export default function Xpub() {
55 <Grid.Item full className="mt4" as={Text}>
41 <Grid.Item full className="mt4" as={Text}>
56 Paste it into landscape while setting up your wallet.
42 Paste it into landscape while setting up your wallet.
57 </Grid.Item>
43 </Grid.Item>
44+ <Grid.Item
45+ full
46+ className="mt4"
47+ as={Text}
48+ style={{ fontSize: '14px', color: 'gray' }}>
49+ {btcMainnet ? 'BTC Mainnet' : 'BTC Testnet'}
50+ </Grid.Item>
51+ <Grid.Item full className="mt1">
52+ <ToggleSwitch
53+ selected={btcMainnet}
54+ onClick={() => {
55+ btcMainnet ? setXpub(vpub) : setXpub(zpub);
56+ setBtcMainnet(!btcMainnet);
57+ }}
58+ />
59+ </Grid.Item>
58 <Grid.Item full className="mt4" as={Text}>
60 <Grid.Item full className="mt4" as={Text}>
59 <Highlighted>
61 <Highlighted>
60− <CopiableAddressWrap>{zPub}</CopiableAddressWrap>
62+ <CopiableAddressWrap>{xpub}</CopiableAddressWrap>
61 </Highlighted>
63 </Highlighted>
62 </Grid.Item>
64 </Grid.Item>
63 </Grid>
65 </Grid>