on master help sign in
bitcoin: split xpub and transaction signing to separate screens
fb30b41pkova
src/lib/routeNames.js+3
@@ -27,4 +27,7 @@ export const ROUTE_NAMES = {
27 URBIT_ID: Symbol('URBIT_ID'),
27 URBIT_ID: Symbol('URBIT_ID'),
28 RESIDENTS: Symbol('RESIDENTS'),
28 RESIDENTS: Symbol('RESIDENTS'),
29 ADOPT: Symbol('ADOPT'),
29 ADOPT: Symbol('ADOPT'),
30+ BITCOIN: Symbol('BITCOIN'),
31+ BITCOIN_XPUB: Symbol('BITCOIN_XPUB'),
32+ BITCOIN_SIGN_TRANSACTION: Symbol('BITCOIN_SIGN_TRANSACTION'),
30};
33};
src/lib/router.js+5−1
@@ -18,7 +18,9 @@ import UrbitOS from 'views/UrbitOS';
18import UrbitID from 'views/UrbitID';
18import UrbitID from 'views/UrbitID';
19import Residents from 'views/Residents';
19import Residents from 'views/Residents';
20import Adopt from 'views/Adopt';
20import Adopt from 'views/Adopt';
21−import Bitcoin from 'views/Bitcoin';
21+import Bitcoin from 'views/Bitcoin/Bitcoin';
22+import Xpub from 'views/Bitcoin/Xpub';
23+import SignTransaction from 'views/Bitcoin/SignTransaction';
22
24
23import { ROUTE_NAMES } from './routeNames';
25import { ROUTE_NAMES } from './routeNames';
24
26
@@ -44,4 +46,6 @@ export const ROUTES = {
44 [ROUTE_NAMES.RESIDENTS]: Residents,
46 [ROUTE_NAMES.RESIDENTS]: Residents,
45 [ROUTE_NAMES.ADOPT]: Adopt,
47 [ROUTE_NAMES.ADOPT]: Adopt,
46 [ROUTE_NAMES.BITCOIN]: Bitcoin,
48 [ROUTE_NAMES.BITCOIN]: Bitcoin,
49+ [ROUTE_NAMES.BITCOIN_XPUB]: Xpub,
50+ [ROUTE_NAMES.BITCOIN_SIGN_TRANSACTION]: SignTransaction,
47};
51};
src/views/Bitcoin.jsdeleted−155
@@ -1,155 +0,0 @@
1−import React, { useState } from 'react';
2−import { Grid, Text } from 'indigo-react';
3−import { useWallet } from 'store/wallet';
4−
5−import { useLocalRouter } from 'lib/LocalRouter';
6−
7−import ViewHeader from 'components/ViewHeader';
8−import View from 'components/View';
9−import WarningBox from 'components/WarningBox';
10−
11−import CopiableAddressWrap from 'components/CopiableAddressWrap';
12−import Highlighted from 'components/Highlighted';
13−
14−import { PsbtInput } from 'form/Inputs';
15−import BridgeForm from 'form/BridgeForm';
16−
17−import * as need from 'lib/need';
18−import * as bitcoin from 'bitcoinjs-lib';
19−import * as bs58check from 'bs58check';
20−import { Buffer } from 'buffer';
21−import { composeValidator, buildPsbtValidator } from 'form/validators';
22−
23−function xpubToZpub(xpub) {
24− var data = bs58check.decode(xpub);
25− data = data.slice(4);
26− data = Buffer.concat([Buffer.from('04b24746', 'hex'), data]);
27− return bs58check.encode(data);
28−}
29−
30−export default function Bitcoin() {
31− const { pop } = useLocalRouter();
32− const { urbitWallet } = useWallet();
33−
34−
35− const empty = new bitcoin.Psbt();
36−
37− const [signedTx, setSignedTx] = useState('');
38− const [psbt, setPsbt] = useState(empty);
39−
40− const privKey = need.urbitWallet(urbitWallet).bitcoin.keys.private;
41− const pubKey = need.urbitWallet(urbitWallet).bitcoin.keys.public;
42− const chain = need.urbitWallet(urbitWallet).bitcoin.keys.chain;
43−
44− const btcWallet = bitcoin.bip32.fromPrivateKey(
45− Buffer.from(privKey, 'hex'),
46− Buffer.from(chain, 'hex'),
47− bitcoin.networks.bitcoin
48− );
49−
50− const zPub = xpubToZpub(
51− bitcoin.bip32
52− .fromPublicKey(
53− Buffer.from(pubKey, 'hex'),
54− Buffer.from(chain, 'hex'),
55− bitcoin.networks.bitcoin
56− )
57− .toBase58()
58− );
59−
60− const validate = composeValidator({
61− unsignedTransaction: buildPsbtValidator(btcWallet),
62− });
63−
64− const onValues = ({ valid, values, form }) => {
65− if (valid) {
66− const newPsbt = bitcoin.Psbt.fromBase64(values.unsignedTransaction);
67− const hex = newPsbt.data.inputs
68− .reduce((psbt, input, idx) => {
69− // removing already derived part, eg m/84'/0'/0'/0/0 becomes 0/0
70− const path = input.bip32Derivation[0].path
71− .split("m/84'/0'/0'/")
72− .join('');
73− const prv = btcWallet.derivePath(path).privateKey;
74− try {
75− return psbt.signInput(idx, bitcoin.ECPair.fromPrivateKey(prv));
76− } catch (e) {
77− return psbt;
78− }
79− }, newPsbt)
80− .finalizeAllInputs()
81− .extractTransaction()
82− .toHex();
83−
84− if (psbt.toHex() !== newPsbt.toHex()) {
85− setPsbt(newPsbt);
86− }
87− setSignedTx(hex);
88− } else {
89− setSignedTx('');
90− }
91− };
92−
93− return (
94− <View pop={pop} inset>
95− <Grid className="mt4">
96− <Grid.Item full as={ViewHeader}>
97− Bitcoin
98− </Grid.Item>
99− <Grid.Item full as={WarningBox}>
100− We do not recommend using this feature for significant amounts of
101− money, since your private keys are exposed to the browser.
102− </Grid.Item>
103− <Grid.Item full className="mt4" as={Text}>
104− Below is the bitcoin extended public key derived from your master
105− ticket. You can paste it to Landscape to generate an unsigned
106− transaction.
107− </Grid.Item>
108− <Grid.Item full className="mt4" as={Text}>
109− <Highlighted>
110− <CopiableAddressWrap>{zPub}</CopiableAddressWrap>
111− </Highlighted>
112− </Grid.Item>
113− <Grid.Item full className="mt4" as={Text}>
114− Next, paste the unsigned transaction below to sign it with your
115− private key.
116− </Grid.Item>
117− <BridgeForm validate={validate} onValues={onValues}>
118− {() => (
119− <Grid.Item
120− full
121− as={PsbtInput}
122− name="unsignedTransaction"
123− label="Unsigned transaction"
124− className="mt4"
125− />
126− )}
127− </BridgeForm>
128− {signedTx && (
129− <Grid.Item full className="mt4" as={Text}>
130− Transaction outputs:
131− </Grid.Item>
132− )}
133− {signedTx &&
134− psbt.txOutputs.map(e => (
135− <Grid.Item full className="mt4" key={e.address}>
136− <span>{e.address}</span>
137− <div className="mt1 mono black f6">{e.value} Satoshis</div>
138− </Grid.Item>
139− ))}
140− {signedTx && (
141− <>
142− <Grid.Item full className="mt4" as={ViewHeader}>
143− Signed transaction:
144− </Grid.Item>
145− <Grid.Item full as={Text}>
146− <Highlighted>
147− <CopiableAddressWrap>{signedTx}</CopiableAddressWrap>
148− </Highlighted>
149− </Grid.Item>
150− </>
151− )}
152− </Grid>
153− </View>
154− );
155−}
src/views/Bitcoin/Bitcoin.jsnew file+44
@@ -0,0 +1,44 @@
1+import React, { useCallback } from 'react';
2+import { Grid } from 'indigo-react';
3+
4+import { useLocalRouter } from 'lib/LocalRouter';
5+
6+import View from 'components/View';
7+import { ForwardButton } from 'components/Buttons';
8+
9+export default function Bitcoin() {
10+ const { pop, push, names } = useLocalRouter();
11+
12+ const goXpub = useCallback(() => push(names.BITCOIN_XPUB), [push, names]);
13+ const goSignTransaction = useCallback(
14+ () => push(names.BITCOIN_SIGN_TRANSACTION),
15+ [push, names]
16+ );
17+
18+ return (
19+ <View pop={pop} inset>
20+ <Grid.Item full className="mb4 f5">
21+ Bitcoin
22+ </Grid.Item>
23+ <Grid.Divider />
24+ <Grid className="pt2">
25+ <Grid.Item
26+ full
27+ as={ForwardButton}
28+ detail="Xpub for Landscape wallet"
29+ onClick={goXpub}>
30+ Xpub
31+ </Grid.Item>
32+ <Grid.Divider />
33+ <Grid.Item
34+ full
35+ as={ForwardButton}
36+ className="mt1"
37+ detail="Sign transaction from Landscape"
38+ onClick={goSignTransaction}>
39+ Sign Transaction
40+ </Grid.Item>
41+ </Grid>
42+ </View>
43+ );
44+}
src/views/Bitcoin/SignTransaction.jsnew file+122
@@ -0,0 +1,122 @@
1+import React, { useState } from 'react';
2+import { Grid, Text } from 'indigo-react';
3+
4+import { useLocalRouter } from 'lib/LocalRouter';
5+
6+import View from 'components/View';
7+import Highlighted from 'components/Highlighted';
8+import CopiableAddressWrap from 'components/CopiableAddressWrap';
9+import ViewHeader from 'components/ViewHeader';
10+import SubmitButton from 'form/SubmitButton';
11+
12+import { useWallet } from 'store/wallet';
13+
14+import * as need from 'lib/need';
15+import * as bitcoin from 'bitcoinjs-lib';
16+
17+import BridgeForm from 'form/BridgeForm';
18+import { PsbtInput } from 'form/Inputs';
19+
20+import { Buffer } from 'buffer';
21+import { composeValidator, buildPsbtValidator } from 'form/validators';
22+
23+export default function SignTransaction() {
24+ const { pop } = useLocalRouter();
25+ const { urbitWallet } = useWallet();
26+
27+ const [signedTx, setSignedTx] = useState('');
28+ const [psbt, setPsbt] = useState({});
29+
30+ const { private: privKey, chain } = need.urbitWallet(
31+ urbitWallet
32+ ).bitcoin.keys;
33+
34+ const btcWallet = bitcoin.bip32.fromPrivateKey(
35+ Buffer.from(privKey, 'hex'),
36+ Buffer.from(chain, 'hex'),
37+ bitcoin.networks.bitcoin
38+ );
39+
40+ const validate = composeValidator({
41+ unsignedTransaction: buildPsbtValidator(btcWallet),
42+ });
43+
44+ const onSubmit = values => {
45+ const newPsbt = bitcoin.Psbt.fromBase64(values.unsignedTransaction);
46+ const hex = newPsbt.data.inputs
47+ .reduce((psbt, input, idx) => {
48+ // removing already derived part, eg m/84'/0'/0'/0/0 becomes 0/0
49+ const path = input.bip32Derivation[0].path
50+ .split("m/84'/0'/0'/")
51+ .join('');
52+ const prv = btcWallet.derivePath(path).privateKey;
53+ try {
54+ return psbt.signInput(idx, bitcoin.ECPair.fromPrivateKey(prv));
55+ } catch (e) {
56+ return psbt;
57+ }
58+ }, newPsbt)
59+ .finalizeAllInputs()
60+ .extractTransaction()
61+ .toHex();
62+
63+ setPsbt(newPsbt);
64+ setSignedTx(hex);
65+ };
66+
67+ return (
68+ <View pop={pop} inset>
69+ <Grid className="mt4">
70+ <Grid.Item full className="mt4" as={Text}>
71+ Paste unsigned transactions from Landscape here
72+ </Grid.Item>
73+ <BridgeForm validate={validate} onSubmit={onSubmit}>
74+ {({ handleSubmit }) => (
75+ <>
76+ <Grid.Item
77+ full
78+ as={PsbtInput}
79+ name="unsignedTransaction"
80+ label="Unsigned transaction"
81+ className="mt4"
82+ />
83+ <Grid.Item
84+ full
85+ solid
86+ className="mt2"
87+ accessory=""
88+ handleSubmit={handleSubmit}
89+ as={SubmitButton}>
90+ Sign Transaction
91+ </Grid.Item>
92+ </>
93+ )}
94+ </BridgeForm>
95+ {signedTx && (
96+ <Grid.Item full className="mt4" as={Text}>
97+ Transaction outputs:
98+ </Grid.Item>
99+ )}
100+ {signedTx &&
101+ psbt.txOutputs.map(e => (
102+ <Grid.Item full className="mt4" key={e.address}>
103+ <span>{e.address}</span>
104+ <div className="mt1 mono black f6">{e.value} sats</div>
105+ </Grid.Item>
106+ ))}
107+ {signedTx && (
108+ <>
109+ <Grid.Item full className="mt4" as={ViewHeader}>
110+ Signed transaction:
111+ </Grid.Item>
112+ <Grid.Item full as={Text}>
113+ <Highlighted>
114+ <CopiableAddressWrap>{signedTx}</CopiableAddressWrap>
115+ </Highlighted>
116+ </Grid.Item>
117+ </>
118+ )}
119+ </Grid>
120+ </View>
121+ );
122+}
src/views/Bitcoin/Xpub.jsnew file+66
@@ -0,0 +1,66 @@
1+import React from 'react';
2+import { Grid, Text } from 'indigo-react';
3+import { useWallet } from 'store/wallet';
4+
5+import { useLocalRouter } from 'lib/LocalRouter';
6+
7+import ViewHeader from 'components/ViewHeader';
8+import View from 'components/View';
9+import WarningBox from 'components/WarningBox';
10+
11+import CopiableAddressWrap from 'components/CopiableAddressWrap';
12+import Highlighted from 'components/Highlighted';
13+
14+import * 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+
26+export default function Xpub() {
27+ const { pop } = useLocalRouter();
28+ const { urbitWallet } = useWallet();
29+
30+ const { public: pubKey, chain } = need.urbitWallet(urbitWallet).bitcoin.keys;
31+
32+ const zPub = xpubToZpub(
33+ bitcoin.bip32
34+ .fromPublicKey(
35+ Buffer.from(pubKey, 'hex'),
36+ Buffer.from(chain, 'hex'),
37+ bitcoin.networks.bitcoin
38+ )
39+ .toBase58()
40+ );
41+
42+ return (
43+ <View pop={pop} inset>
44+ <Grid className="mt4">
45+ <Grid.Item full as={ViewHeader}>
46+ Bitcoin
47+ </Grid.Item>
48+ <Grid.Item full as={WarningBox}>
49+ We do not recommend using this feature for significant amounts of
50+ money, since your private keys are exposed to the browser.
51+ </Grid.Item>
52+ <Grid.Item full className="mt4" as={Text}>
53+ The public key derived from your master ticket.
54+ </Grid.Item>
55+ <Grid.Item full className="mt4" as={Text}>
56+ Paste it into landscape while setting up your wallet.
57+ </Grid.Item>
58+ <Grid.Item full className="mt4" as={Text}>
59+ <Highlighted>
60+ <CopiableAddressWrap>{zPub}</CopiableAddressWrap>
61+ </Highlighted>
62+ </Grid.Item>
63+ </Grid>
64+ </View>
65+ );
66+}