on master help sign in
bitcoin: implement bitcoin view
fb2aecbpkova
package.json+3−1
@@ -25,7 +25,9 @@
25 "azimuth-solidity": "^1.2.1",
25 "azimuth-solidity": "^1.2.1",
26 "bip32": "^1.0.2",
26 "bip32": "^1.0.2",
27 "bip39": "^2.5.0",
27 "bip39": "^2.5.0",
28+ "bitcoinjs-lib": "^5.2.0",
28 "bn.js": "^4.11.8",
29 "bn.js": "^4.11.8",
30+ "buffer": "^5.6.1",
29 "classnames": "^2.2.6",
31 "classnames": "^2.2.6",
30 "copy-to-clipboard": "^3.2.0",
32 "copy-to-clipboard": "^3.2.0",
31 "ethereum-blockies-base64": "^1.0.2",
33 "ethereum-blockies-base64": "^1.0.2",
@@ -52,7 +54,7 @@
52 "react-teleporter": "^1.1.0",
54 "react-teleporter": "^1.1.0",
53 "secp256k1": "^3.5.2",
55 "secp256k1": "^3.5.2",
54 "trezor-connect": "^8.1.14",
56 "trezor-connect": "^8.1.14",
55− "urbit-key-generation": "^0.17.4",
57+ "urbit-key-generation": "^0.18.0",
56 "urbit-ob": "^4.1.4",
58 "urbit-ob": "^4.1.4",
57 "urbit-paper-renderer": "^2.0.9",
59 "urbit-paper-renderer": "^2.0.9",
58 "web3": "1.0.0-beta.54",
60 "web3": "1.0.0-beta.54",
src/components/CopiableAddress.js+6−1
@@ -4,5 +4,10 @@ import cn from 'classnames';
4import CopiableWithTooltip from './CopiableWithTooltip';
4import CopiableWithTooltip from './CopiableWithTooltip';
5
5
6export default function CopiableAddress({ className, ...rest }) {
6export default function CopiableAddress({ className, ...rest }) {
7+ return (
8+ <CopiableWithTooltip
7− return <CopiableWithTooltip className={cn(className, 'mono')} {...rest} />;
9+ className={cn(className, 'mono', 'nowrap')}
10+ {...rest}
11+ />
12+ );
8}
13}
src/components/CopiableAddressWrap.jsnew file+10
@@ -0,0 +1,10 @@
1+import React from 'react';
2+import cn from 'classnames';
3+
4+import CopiableWithTooltip from './CopiableWithTooltip';
5+
6+export default function CopiableAddressWrap({ className, ...rest }) {
7+ return (
8+ <CopiableWithTooltip className={cn(className, 'mono', 'wrap')} {...rest} />
9+ );
10+}
src/components/CopiableWithTooltip.js+1−2
@@ -1,5 +1,4 @@
1import React from 'react';
1import React from 'react';
2−import cn from 'classnames';
3import { ReactComponent as Copy } from 'assets/copy.svg';
2import { ReactComponent as Copy } from 'assets/copy.svg';
4
3
5import useCopiable from 'lib/useCopiable';
4import useCopiable from 'lib/useCopiable';
@@ -15,7 +14,7 @@ export default function CopiableWithTooltip({
15 const [doCopy, didCopy] = useCopiable(text || children);
14 const [doCopy, didCopy] = useCopiable(text || children);
16
15
17 return (
16 return (
18− <As className={cn(className, 'nowrap')} {...rest}>
17+ <As className={className} {...rest}>
19 {children}
18 {children}
20 <WithTooltip content={didCopy ? 'Copied!' : 'Copy'}>
19 <WithTooltip content={didCopy ? 'Copied!' : 'Copy'}>
21 <Copy
20 <Copy
src/form/Inputs.js+11
@@ -163,6 +163,17 @@ export function PrivateKeyInput({ ...rest }) {
163 );
163 );
164}
164}
165
165
166+export function PsbtInput({ ...rest }) {
167+ return (
168+ <HexInput
169+ type="text"
170+ placeholder=""
171+ config={{ parse: stripHexPrefix }}
172+ {...rest}
173+ />
174+ );
175+}
176+
166export function AddressInput({ ...rest }) {
177export function AddressInput({ ...rest }) {
167 return (
178 return (
168 <Input
179 <Input
src/form/validators.js+6
@@ -19,6 +19,8 @@ import {
19 validateHexPrefix,
19 validateHexPrefix,
20 validateLength,
20 validateLength,
21 validateUnique,
21 validateUnique,
22+ validatePsbt,
23+ validateSignablePsbt,
22} from 'lib/validators';
24} from 'lib/validators';
23import isPromise from 'lib/isPromise';
25import isPromise from 'lib/isPromise';
24
26
@@ -121,6 +123,10 @@ export const buildEmailValidator = validate =>
121
123
122export const buildEmailArrayValidator = () =>
124export const buildEmailArrayValidator = () =>
123 buildValidator([validateUnique, buildArrayValidator(buildEmailValidator())]);
125 buildValidator([validateUnique, buildArrayValidator(buildEmailValidator())]);
126+
127+export const buildPsbtValidator = hd =>
128+ buildValidator([validatePsbt, validateSignablePsbt(hd)]);
129+
124// the form validator is the composition of all of the field validators
130// the form validator is the composition of all of the field validators
125// plus an additional form validator function
131// plus an additional form validator function
126export const composeValidator = (
132export const composeValidator = (
src/lib/need.js+4
@@ -33,6 +33,10 @@ export const wallet = needBuilder(() => {
33 throw new Error(BRIDGE_ERROR.MISSING_WALLET);
33 throw new Error(BRIDGE_ERROR.MISSING_WALLET);
34});
34});
35
35
36+export const urbitWallet = needBuilder(() => {
37+ throw new Error(BRIDGE_ERROR.MISSING_URBIT_WALLET);
38+});
39+
36export const addressFromWallet = obj => wallet(obj).address;
40export const addressFromWallet = obj => wallet(obj).address;
37
41
38export const point = needBuilder(() => {
42export const point = needBuilder(() => {
src/lib/router.js+2
@@ -18,6 +18,7 @@ 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
22
22import { ROUTE_NAMES } from './routeNames';
23import { ROUTE_NAMES } from './routeNames';
23
24
@@ -42,4 +43,5 @@ export const ROUTES = {
42 [ROUTE_NAMES.URBIT_ID]: UrbitID,
43 [ROUTE_NAMES.URBIT_ID]: UrbitID,
43 [ROUTE_NAMES.RESIDENTS]: Residents,
44 [ROUTE_NAMES.RESIDENTS]: Residents,
44 [ROUTE_NAMES.ADOPT]: Adopt,
45 [ROUTE_NAMES.ADOPT]: Adopt,
46+ [ROUTE_NAMES.BITCOIN]: Bitcoin,
45};
47};
src/lib/validators.js+17
@@ -1,4 +1,5 @@
1import * as bip39 from 'bip39';
1import * as bip39 from 'bip39';
2+import * as bitcoin from 'bitcoinjs-lib';
2import ob from 'urbit-ob';
3import ob from 'urbit-ob';
3import { includes } from 'lodash';
4import { includes } from 'lodash';
4
5
@@ -132,3 +133,19 @@ export const validateUnique = arr => {
132
133
133export const validateChild = ourShip => ship =>
134export const validateChild = ourShip => ship =>
134 ourShip !== ob.sein(ship) && `This point is not a child of ${ourShip}.`;
135 ourShip !== ob.sein(ship) && `This point is not a child of ${ourShip}.`;
136+
137+export const validatePsbt = hex => {
138+ try {
139+ bitcoin.Psbt.fromHex(hex);
140+ } catch (e) {
141+ return 'Invalid Partially Signed Bitcoin Transaction';
142+ }
143+};
144+
145+export const validateSignablePsbt = hd => hex => {
146+ try {
147+ bitcoin.Psbt.fromHex(hex).signAllInputsHD(hd);
148+ } catch (e) {
149+ return 'No inputs were signed';
150+ }
151+};
src/views/Bitcoin.jsnew file+138
@@ -0,0 +1,138 @@
1+import React, { useState } from 'react';
2+import { Grid, Text } from 'indigo-react';
3+import { useWallet } from 'store/wallet';
4+import { usePointCursor } from 'store/pointCursor';
5+
6+import { useLocalRouter } from 'lib/LocalRouter';
7+
8+import ViewHeader from 'components/ViewHeader';
9+import View from 'components/View';
10+import WarningBox from 'components/WarningBox';
11+
12+import CopiableAddressWrap from 'components/CopiableAddressWrap';
13+import Highlighted from 'components/Highlighted';
14+
15+import { PsbtInput } from 'form/Inputs';
16+import BridgeForm from 'form/BridgeForm';
17+
18+import * as need from 'lib/need';
19+import * as bitcoin from 'bitcoinjs-lib';
20+import ob from 'urbit-ob';
21+import { Buffer } from 'buffer';
22+import { composeValidator, buildPsbtValidator } from 'form/validators';
23+
24+export default function Bitcoin() {
25+ const { pop } = useLocalRouter();
26+ const { urbitWallet } = useWallet();
27+ const { pointCursor } = usePointCursor();
28+
29+ const empty = new bitcoin.Psbt();
30+
31+ const [signedTx, setSignedTx] = useState('');
32+ const [psbt, setPsbt] = useState(empty);
33+
34+ const pointName = ob.patp(need.point(pointCursor));
35+
36+ const privKey = need.urbitWallet(urbitWallet).bitcoin.keys.private;
37+ const pubKey = need.urbitWallet(urbitWallet).bitcoin.keys.public;
38+ const chain = need.urbitWallet(urbitWallet).bitcoin.keys.chain;
39+
40+ const hd = bitcoin.bip32.fromPrivateKey(
41+ Buffer.from(privKey, 'hex'),
42+ Buffer.from(chain, 'hex'),
43+ bitcoin.networks.bitcoin
44+ );
45+
46+ const xPub = bitcoin.bip32
47+ .fromPublicKey(
48+ Buffer.from(pubKey, 'hex'),
49+ Buffer.from(chain, 'hex'),
50+ bitcoin.networks.bitcoin
51+ )
52+ .toBase58();
53+
54+ const validate = composeValidator({
55+ unsignedTransaction: buildPsbtValidator(hd),
56+ });
57+
58+ const onValues = ({ valid, values, form }) => {
59+ if (valid) {
60+ const newPsbt = bitcoin.Psbt.fromHex(values.unsignedTransaction);
61+ const hex = newPsbt
62+ .signAllInputsHD(hd)
63+ .finalizeAllInputs()
64+ .extractTransaction()
65+ .toHex();
66+
67+ if (psbt.toHex() !== newPsbt.toHex()) {
68+ setPsbt(newPsbt);
69+ }
70+ setSignedTx(hex);
71+ } else {
72+ setSignedTx('');
73+ }
74+ };
75+
76+ return (
77+ <View pop={pop} inset>
78+ <Grid className="mt4">
79+ <Grid.Item full as={ViewHeader}>
80+ Bitcoin
81+ </Grid.Item>
82+ <Grid.Item full as={WarningBox}>
83+ We do not recommend using this feature for significant amounts of
84+ money, since your private keys are exposed to the browser.
85+ </Grid.Item>
86+ <Grid.Item full className="mt4" as={Text}>
87+ Below is the bitcoin extended public key derived from your point
88+ {' ' + pointName}. You can paste it to Landscape to generate an
89+ unsigned transaction.
90+ </Grid.Item>
91+ <Grid.Item full className="mt4" as={Text}>
92+ <Highlighted>
93+ <CopiableAddressWrap>{xPub}</CopiableAddressWrap>
94+ </Highlighted>
95+ </Grid.Item>
96+ <Grid.Item full className="mt4" as={Text}>
97+ Next, paste the unsigned transaction below to sign it with your
98+ private key.
99+ </Grid.Item>
100+ <BridgeForm validate={validate} onValues={onValues}>
101+ {() => (
102+ <Grid.Item
103+ full
104+ as={PsbtInput}
105+ name="unsignedTransaction"
106+ label="Unsigned transaction"
107+ className="mt4"
108+ />
109+ )}
110+ </BridgeForm>
111+ {signedTx && (
112+ <Grid.Item full className="mt4" as={Text}>
113+ Transaction outputs:
114+ </Grid.Item>
115+ )}
116+ {signedTx &&
117+ psbt.txOutputs.map(e => (
118+ <Grid.Item full className="mt4">
119+ <span>{e.address}</span>
120+ <div className="mt1 mono black f6">{e.value} Satoshis</div>
121+ </Grid.Item>
122+ ))}
123+ {signedTx && (
124+ <>
125+ <Grid.Item full className="mt4" as={ViewHeader}>
126+ Signed transaction:
127+ </Grid.Item>
128+ <Grid.Item full as={Text}>
129+ <Highlighted>
130+ <CopiableAddressWrap>{signedTx}</CopiableAddressWrap>
131+ </Highlighted>
132+ </Grid.Item>
133+ </>
134+ )}
135+ </Grid>
136+ </View>
137+ );
138+}
src/views/Point.js+15−1
@@ -114,7 +114,7 @@ export default function Point() {
114 const { pop, push, names } = useLocalRouter();
114 const { pop, push, names } = useLocalRouter();
115 const { pointCursor } = usePointCursor();
115 const { pointCursor } = usePointCursor();
116
116
117− const { wallet } = useWallet();
117+ const { wallet, urbitWallet } = useWallet();
118 const point = need.point(pointCursor);
118 const point = need.point(pointCursor);
119
119
120 const { getResidents } = usePointCache();
120 const { getResidents } = usePointCache();
@@ -130,6 +130,8 @@ export default function Point() {
130 isOwner,
130 isOwner,
131 } = useCurrentPermissions();
131 } = useCurrentPermissions();
132
132
133+ const canBitcoin = Just.hasInstance(urbitWallet);
134+
133 // fetch the invites for the current cursor
135 // fetch the invites for the current cursor
134 const invites = useInvites(point);
136 const invites = useInvites(point);
135 const {
137 const {
@@ -156,6 +158,8 @@ export default function Point() {
156
158
157 const goUrbitOS = useCallback(() => push(names.URBIT_OS), [push, names]);
159 const goUrbitOS = useCallback(() => push(names.URBIT_OS), [push, names]);
158
160
161+ const goBitcoin = useCallback(() => push(names.BITCOIN), [push, names]);
162+
159 const goUrbitID = useCallback(() => push(names.URBIT_ID), [push, names]);
163 const goUrbitID = useCallback(() => push(names.URBIT_ID), [push, names]);
160
164
161 const goResidents = useCallback(() => push(names.RESIDENTS), [push, names]);
165 const goResidents = useCallback(() => push(names.RESIDENTS), [push, names]);
@@ -287,6 +291,16 @@ export default function Point() {
287 OS
291 OS
288 </Grid.Item>
292 </Grid.Item>
289 <Grid.Divider />
293 <Grid.Divider />
294+ <Grid.Item
295+ full
296+ as={ForwardButton}
297+ disabled={!canBitcoin}
298+ className="mt1"
299+ detail="Bitcoin management"
300+ onClick={goBitcoin}>
301+ Bitcoin
302+ </Grid.Item>
303+ <Grid.Divider />
290 {isParent && (
304 {isParent && (
291 <>
305 <>
292 <Grid.Item
306 <Grid.Item