@@ -0,0 +1,203 @@
1+import React, { useMemo, useState } from 'react';
2+import { Grid, Text } from 'indigo-react';
3+import { Box, Button, Row, Icon } from '@tlon/indigo-react';
4+
5+import { useWallet } from 'store/wallet';
6+
7+import * as bitcoin from 'bitcoinjs-lib';
8+import { useLocalRouter } from 'lib/LocalRouter';
9+import { COMMANDS, useFlowCommand } from 'lib/flowCommand';
10+import * as need from 'lib/need';
11+import useCopiable from 'lib/useCopiable';
12+
13+import View from 'components/View';
14+import L2BackHeader from 'components/L2/Headers/L2BackHeader';
15+import Window from 'components/L2/Window/Window';
16+import HeaderPane from 'components/L2/Window/HeaderPane';
17+import BodyPane from 'components/L2/Window/BodyPane';
18+import Highlighted from 'components/Highlighted';
19+import CopiableAddressWrap from 'components/copiable/CopiableAddressWrap';
20+import ViewHeader from 'components/ViewHeader';
21+import SubmitButton from 'form/SubmitButton';
22+import BridgeForm from 'form/BridgeForm';
23+import { PsbtInput } from 'form/Inputs';
24+import { composeValidator, buildPsbtValidator } from 'form/validators';
25+
26+import './Bitcoin.scss';
27+
28+const BITCOIN_MAINNET_INFO = {
29+ messagePrefix: '\x18Bitcoin Signed Message:\n',
30+ bech32: 'bc',
31+ bip32: {
32+ public: 0x04b24746,
33+ private: 0x04b2430c,
34+ },
35+ pubKeyHash: 0x00,
36+ scriptHash: 0x05,
37+ wif: 0x80,
38+};
39+
40+const BITCOIN_TESTNET_INFO = {
41+ messagePrefix: '\x18Bitcoin Signed Message:\n',
42+ bech32: 'tb',
43+ bip32: {
44+ public: 0x045f1cf6,
45+ private: 0x045f18bc,
46+ },
47+ pubKeyHash: 0x6f,
48+ scriptHash: 0xc4,
49+ wif: 0xef,
50+};
51+
52+export default function Bitcoin() {
53+ const { pop }: any = useLocalRouter();
54+ const { urbitWallet }: any = useWallet();
55+ const flow = useFlowCommand();
56+
57+ const [signedTx, setSignedTx] = useState('');
58+ const [psbt, setPsbt] = useState({});
59+
60+ // set to false for BTC testnet
61+ const isMainnet = true;
62+ const { xpub: zpub } = need.urbitWallet(urbitWallet).bitcoinMainnet.keys;
63+ const { xpub: vpub } = need.urbitWallet(urbitWallet).bitcoinTestnet.keys;
64+
65+ const { xprv: zprv } = need.urbitWallet(urbitWallet).bitcoinMainnet.keys;
66+ const { xprv: vprv } = need.urbitWallet(urbitWallet).bitcoinTestnet.keys;
67+
68+ const validate = composeValidator({
69+ unsignedTransaction: buildPsbtValidator(),
70+ });
71+
72+ const xpub = useMemo(() => (isMainnet ? zpub : vpub), [
73+ isMainnet,
74+ zpub,
75+ vpub,
76+ ]);
77+
78+ const [doCopy, didCopy] = useCopiable(xpub);
79+
80+ const onSubmit = values => {
81+ const newPsbt = bitcoin.Psbt.fromBase64(values.unsignedTransaction);
82+
83+ const isTestnet = newPsbt.data.inputs[0].bip32Derivation[0].path.startsWith(
84+ "m/84'/1'/0'/"
85+ );
86+
87+ const derivationPrefix = isTestnet ? "m/84'/1'/0'/" : "m/84'/0'/0'/";
88+
89+ const btcWallet = isTestnet
90+ ? bitcoin.bip32.fromBase58(vprv, BITCOIN_TESTNET_INFO)
91+ : bitcoin.bip32.fromBase58(zprv, BITCOIN_MAINNET_INFO);
92+
93+ try {
94+ const hex = newPsbt.data.inputs
95+ .reduce((psbt, input, idx) => {
96+ // removing already derived part, eg m/84'/0'/0'/0/0 becomes 0/0
97+ const path = input.bip32Derivation[0].path
98+ .split(derivationPrefix)
99+ .join('');
100+ const prv = btcWallet.derivePath(path).privateKey;
101+ return psbt.signInput(idx, bitcoin.ECPair.fromPrivateKey(prv));
102+ }, newPsbt)
103+ .finalizeAllInputs()
104+ .extractTransaction()
105+ .toHex();
106+ setPsbt(newPsbt);
107+ setSignedTx(hex);
108+ } catch (error) {
109+ return {
110+ unsignedTransaction: 'Cannot sign transaction, mismatching keys.',
111+ };
112+ }
113+ };
114+
115+ const initialValues = {};
116+ if (flow && COMMANDS.BITCOIN === flow.kind) {
117+ initialValues.unsignedTransaction = flow.utx;
118+ }
119+
120+ return (
121+ <View
122+ className="bitcoin"
123+ pop={pop}
124+ inset
125+ header={<L2BackHeader />}
126+ hideBack>
127+ <Window className="os-home">
128+ <HeaderPane>
129+ <Row className="header-row">
130+ <h5>Bitcoin</h5>
131+ <Button onClick={doCopy} className="copy-button">
132+ {didCopy ? (
133+ 'Copied!'
134+ ) : (
135+ <>
136+ <Icon icon="Copy" color="#fff" />
137+ Copy Xpub
138+ </>
139+ )}
140+ </Button>
141+ </Row>
142+ </HeaderPane>
143+ <BodyPane>
144+ <Grid className="w-full h-full">
145+ <BridgeForm
146+ validate={validate}
147+ initialValues={initialValues}
148+ onSubmit={onSubmit}>
149+ {({ handleSubmit }) => (
150+ <Grid.Item full className="h-full flex-col justify-between">
151+ <Box className="full">
152+ <Box className="label full">Unsigned Transaction</Box>
153+ <Grid.Item
154+ full
155+ placeholder="0x"
156+ as={PsbtInput}
157+ name="unsignedTransaction"
158+ className="mt2"
159+ />
160+ </Box>
161+ <Grid.Item
162+ full
163+ solid
164+ center
165+ className="mt2"
166+ accessory=""
167+ handleSubmit={handleSubmit}
168+ as={SubmitButton}>
169+ Sign Bitcoin Transaction
170+ </Grid.Item>
171+ </Grid.Item>
172+ )}
173+ </BridgeForm>
174+ {signedTx && (
175+ <Grid.Item full className="mt4" as={Text}>
176+ Transaction outputs:
177+ </Grid.Item>
178+ )}
179+ {signedTx &&
180+ psbt.txOutputs.map(e => (
181+ <Grid.Item full className="mt4" key={e.address}>
182+ <span>{e.address}</span>
183+ <div className="mt1 mono black f6">{e.value} sats</div>
184+ </Grid.Item>
185+ ))}
186+ {signedTx && (
187+ <>
188+ <Grid.Item full className="mt4" as={ViewHeader}>
189+ Signed transaction:
190+ </Grid.Item>
191+ <Grid.Item full as={Text}>
192+ <Highlighted>
193+ <CopiableAddressWrap>{signedTx}</CopiableAddressWrap>
194+ </Highlighted>
195+ </Grid.Item>
196+ </>
197+ )}
198+ </Grid>
199+ </BodyPane>
200+ </Window>
201+ </View>
202+ );
203+}