on master help sign in
Merge branch 'bitcoin' into master
f114582fang
package.json+4−1
@@ -25,7 +25,9 @@
25 "azimuth-solidity": "^1.2.2",
25 "azimuth-solidity": "^1.2.2",
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",
@@ -51,8 +53,9 @@
51 "react-scripts": "3.0.1",
53 "react-scripts": "3.0.1",
52 "react-teleporter": "^1.1.0",
54 "react-teleporter": "^1.1.0",
53 "secp256k1": "^3.5.2",
55 "secp256k1": "^3.5.2",
56+ "bs58check": "^2.1.2",
54 "trezor-connect": "^8.1.14",
57 "trezor-connect": "^8.1.14",
55− "urbit-key-generation": "^0.17.4",
58+ "urbit-key-generation": "^0.18.0",
56 "urbit-ob": "^4.1.4",
59 "urbit-ob": "^4.1.4",
57 "urbit-paper-renderer": "^2.0.9",
60 "urbit-paper-renderer": "^2.0.9",
58 "web3": "1.0.0-beta.54",
61 "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 = btcWallet =>
128+ buildValidator([validatePsbt, validateSignablePsbt(btcWallet)]);
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/flowCommand.js+12−5
@@ -5,18 +5,17 @@ import { isValidAddress } from './wallet';
5// which would indicate bridge is to be used for one specific flow.
5// which would indicate bridge is to be used for one specific flow.
6// expects to find any of the following sets of parameters, varying by kind.
6// expects to find any of the following sets of parameters, varying by kind.
7//
7//
8−// ? kind = invite
9−// & as = ~ship // who to send the invite as
10−// & ship = ~ship // what planet to send as the invite
11−// & email = ex@amp.le // email to send the invite code to
12−//
13// ? kind = takeLockup
8// ? kind = takeLockup
14// & lock = linear | conditional // which lockup contract, defaults to linear
9// & lock = linear | conditional // which lockup contract, defaults to linear
15// & from = 0x1234abcd // ethereum address to accept transfer from
10// & from = 0x1234abcd // ethereum address to accept transfer from
16//
11//
12+// ? kind = btc
13+// & utx = somebase64string
14+//
17
15
18const COMMANDS = {
16const COMMANDS = {
19 TAKE_LOCKUP: 'takeLockup',
17 TAKE_LOCKUP: 'takeLockup',
18+ BITCOIN: 'btc',
20};
19};
21
20
22const useFlowCommand = () => {
21const useFlowCommand = () => {
@@ -44,6 +43,14 @@ const useFlowCommand = () => {
44 }
43 }
45 break;
44 break;
46 //
45 //
46+ case COMMANDS.BITCOIN:
47+ try {
48+ atob(flow.utx);
49+ } catch (e) {
50+ flow = null;
51+ }
52+ break;
53+ //
47 default:
54 default:
48 console.log('unrecognized kind of flow:', flow.kind);
55 console.log('unrecognized kind of flow:', flow.kind);
49 // eslint-disable-next-line no-fallthrough
56 // eslint-disable-next-line no-fallthrough
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/routeNames.js+3
@@ -28,4 +28,7 @@ export const ROUTE_NAMES = {
28 URBIT_ID: Symbol('URBIT_ID'),
28 URBIT_ID: Symbol('URBIT_ID'),
29 RESIDENTS: Symbol('RESIDENTS'),
29 RESIDENTS: Symbol('RESIDENTS'),
30 ADOPT: Symbol('ADOPT'),
30 ADOPT: Symbol('ADOPT'),
31+ BITCOIN: Symbol('BITCOIN'),
32+ BITCOIN_XPUB: Symbol('BITCOIN_XPUB'),
33+ BITCOIN_SIGN_TRANSACTION: Symbol('BITCOIN_SIGN_TRANSACTION'),
31};
34};
src/lib/router.js+6
@@ -19,6 +19,9 @@ import UrbitOS from 'views/UrbitOS';
19import UrbitID from 'views/UrbitID';
19import UrbitID from 'views/UrbitID';
20import Residents from 'views/Residents';
20import Residents from 'views/Residents';
21import Adopt from 'views/Adopt';
21import Adopt from 'views/Adopt';
22+import Bitcoin from 'views/Bitcoin/Bitcoin';
23+import Xpub from 'views/Bitcoin/Xpub';
24+import SignTransaction from 'views/Bitcoin/SignTransaction';
22
25
23import { ROUTE_NAMES } from './routeNames';
26import { ROUTE_NAMES } from './routeNames';
24
27
@@ -44,4 +47,7 @@ export const ROUTES = {
44 [ROUTE_NAMES.URBIT_ID]: UrbitID,
47 [ROUTE_NAMES.URBIT_ID]: UrbitID,
45 [ROUTE_NAMES.RESIDENTS]: Residents,
48 [ROUTE_NAMES.RESIDENTS]: Residents,
46 [ROUTE_NAMES.ADOPT]: Adopt,
49 [ROUTE_NAMES.ADOPT]: Adopt,
50+ [ROUTE_NAMES.BITCOIN]: Bitcoin,
51+ [ROUTE_NAMES.BITCOIN_XPUB]: Xpub,
52+ [ROUTE_NAMES.BITCOIN_SIGN_TRANSACTION]: SignTransaction,
47};
53};
src/lib/validators.js+28
@@ -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,30 @@ 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 = base64 => {
138+ try {
139+ bitcoin.Psbt.fromBase64(base64);
140+ } catch (e) {
141+ return 'Invalid Partially Signed Bitcoin Transaction';
142+ }
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/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+132
@@ -0,0 +1,132 @@
1+import React, { useState } from 'react';
2+import { Grid, Text } from 'indigo-react';
3+
4+import { useLocalRouter } from 'lib/LocalRouter';
5+import { COMMANDS, useFlowCommand } from 'lib/flowCommand';
6+
7+import View from 'components/View';
8+import Highlighted from 'components/Highlighted';
9+import CopiableAddressWrap from 'components/CopiableAddressWrap';
10+import ViewHeader from 'components/ViewHeader';
11+import SubmitButton from 'form/SubmitButton';
12+
13+import { useWallet } from 'store/wallet';
14+
15+import * as need from 'lib/need';
16+import * as bitcoin from 'bitcoinjs-lib';
17+
18+import BridgeForm from 'form/BridgeForm';
19+import { PsbtInput } from 'form/Inputs';
20+
21+import { Buffer } from 'buffer';
22+import { composeValidator, buildPsbtValidator } from 'form/validators';
23+
24+export default function SignTransaction() {
25+ const { pop } = useLocalRouter();
26+ const { urbitWallet } = useWallet();
27+ const flow = useFlowCommand();
28+
29+ const [signedTx, setSignedTx] = useState('');
30+ const [psbt, setPsbt] = useState({});
31+
32+ const { private: privKey, chain } = need.urbitWallet(
33+ urbitWallet
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+
42+ const validate = composeValidator({
43+ unsignedTransaction: buildPsbtValidator(btcWallet),
44+ });
45+
46+ const onSubmit = values => {
47+ const newPsbt = bitcoin.Psbt.fromBase64(values.unsignedTransaction);
48+ const hex = newPsbt.data.inputs
49+ .reduce((psbt, input, idx) => {
50+ // removing already derived part, eg m/84'/0'/0'/0/0 becomes 0/0
51+ const path = input.bip32Derivation[0].path
52+ .split("m/84'/0'/0'/")
53+ .join('');
54+ const prv = btcWallet.derivePath(path).privateKey;
55+ try {
56+ return psbt.signInput(idx, bitcoin.ECPair.fromPrivateKey(prv));
57+ } catch (e) {
58+ return psbt;
59+ }
60+ }, newPsbt)
61+ .finalizeAllInputs()
62+ .extractTransaction()
63+ .toHex();
64+
65+ setPsbt(newPsbt);
66+ setSignedTx(hex);
67+ };
68+
69+ const initialValues = {};
70+ if (flow && COMMANDS.BITCOIN === flow.kind) {
71+ initialValues.unsignedTransaction = flow.utx;
72+ }
73+
74+ return (
75+ <View pop={pop} inset>
76+ <Grid className="mt4">
77+ <Grid.Item full className="mt4" as={Text}>
78+ Paste unsigned transactions from Landscape here
79+ </Grid.Item>
80+ <BridgeForm
81+ validate={validate}
82+ initialValues={initialValues}
83+ onSubmit={onSubmit}>
84+ {({ handleSubmit }) => (
85+ <>
86+ <Grid.Item
87+ full
88+ as={PsbtInput}
89+ name="unsignedTransaction"
90+ label="Unsigned transaction"
91+ className="mt4"
92+ />
93+ <Grid.Item
94+ full
95+ solid
96+ className="mt2"
97+ accessory=""
98+ handleSubmit={handleSubmit}
99+ as={SubmitButton}>
100+ Sign Transaction
101+ </Grid.Item>
102+ </>
103+ )}
104+ </BridgeForm>
105+ {signedTx && (
106+ <Grid.Item full className="mt4" as={Text}>
107+ Transaction outputs:
108+ </Grid.Item>
109+ )}
110+ {signedTx &&
111+ psbt.txOutputs.map(e => (
112+ <Grid.Item full className="mt4" key={e.address}>
113+ <span>{e.address}</span>
114+ <div className="mt1 mono black f6">{e.value} sats</div>
115+ </Grid.Item>
116+ ))}
117+ {signedTx && (
118+ <>
119+ <Grid.Item full className="mt4" as={ViewHeader}>
120+ Signed transaction:
121+ </Grid.Item>
122+ <Grid.Item full as={Text}>
123+ <Highlighted>
124+ <CopiableAddressWrap>{signedTx}</CopiableAddressWrap>
125+ </Highlighted>
126+ </Grid.Item>
127+ </>
128+ )}
129+ </Grid>
130+ </View>
131+ );
132+}
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+}
src/views/Login.js+7
@@ -63,6 +63,9 @@ export default function Login() {
63 switch (flow.kind) {
63 switch (flow.kind) {
64 case COMMANDS.TAKE_LOCKUP:
64 case COMMANDS.TAKE_LOCKUP:
65 push(names.ACCEPT_LOCKUP);
65 push(names.ACCEPT_LOCKUP);
66+ //
67+ case COMMANDS.BITCOIN:
68+ push(names.BITCOIN_SIGN_TRANSACTION);
66 break;
69 break;
67 //
70 //
68 default:
71 default:
@@ -76,6 +79,10 @@ export default function Login() {
76 case COMMANDS.TAKE_LOCKUP:
79 case COMMANDS.TAKE_LOCKUP:
77 //TODO kind of want "sign in as 0xabc.." here...
80 //TODO kind of want "sign in as 0xabc.." here...
78 return <>To accept a star lockup batch, please sign in.</>;
81 return <>To accept a star lockup batch, please sign in.</>;
82+ //
83+ case COMMANDS.BITCOIN:
84+ return <>To sign a Bitcoin transaction, please sign in.</>;
85+ //
79 default:
86 default:
80 return <>Flow: {command.kind}</>;
87 return <>Flow: {command.kind}</>;
81 }
88 }
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