on df0c310 help sign in
bridge/src/views/Bitcoin/Bitcoin.tsx
213 lines 6.9 kB history raw
1import React, { useMemo, useState } from 'react';
2import { Grid, Text } from 'indigo-react';
3import { Box, Button, Row, Icon } from '@tlon/indigo-react';
5import { useWallet } from 'store/wallet';
7import * as bitcoin from 'bitcoinjs-lib';
8import { useLocalRouter } from 'lib/LocalRouter';
9import { COMMANDS, FlowType, useFlowCommand } from 'lib/flowCommand';
10import * as need from 'lib/need';
11import useCopiable from 'lib/useCopiable';
13import View from 'components/View';
14import L2BackHeader from 'components/L2/Headers/L2BackHeader';
15import Window from 'components/L2/Window/Window';
16import HeaderPane from 'components/L2/Window/HeaderPane';
17import BodyPane from 'components/L2/Window/BodyPane';
18import Highlighted from 'components/Highlighted';
19import CopiableAddressWrap from 'components/copiable/CopiableAddressWrap';
20import ViewHeader from 'components/ViewHeader';
21import SubmitButton from 'form/SubmitButton';
22import BridgeForm from 'form/BridgeForm';
23import { PsbtInput } from 'form/Inputs';
24import { composeValidator, buildPsbtValidator } from 'form/validators';
26import './Bitcoin.scss';
28const 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};
40const 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};
52export default function Bitcoin() {
53 const { pop }: any = useLocalRouter();
54 const { urbitWallet }: any = useWallet();
55 const flow: FlowType = useFlowCommand();
57 const [signedTx, setSignedTx] = useState('');
58 const [psbt, setPsbt] = useState<ReturnType<
59 typeof bitcoin.Psbt.fromBase64
60 > | null>(null);
62 // set to false for BTC testnet
63 const isMainnet = true;
64 const { xpub: zpub } = need.urbitWallet(urbitWallet).bitcoinMainnet.keys;
65 const { xpub: vpub } = need.urbitWallet(urbitWallet).bitcoinTestnet.keys;
67 const { xprv: zprv } = need.urbitWallet(urbitWallet).bitcoinMainnet.keys;
68 const { xprv: vprv } = need.urbitWallet(urbitWallet).bitcoinTestnet.keys;
70 const validate = composeValidator({
71 unsignedTransaction: buildPsbtValidator(),
72 });
74 const xpub = useMemo(() => (isMainnet ? zpub : vpub), [
75 isMainnet,
76 zpub,
77 vpub,
78 ]);
80 const [doCopy, didCopy] = useCopiable(xpub);
82 const onSubmit = (values: { unsignedTransaction: string }) => {
83 const newPsbt = bitcoin.Psbt.fromBase64(values.unsignedTransaction);
84 const derivations = newPsbt.data.inputs[0]?.bip32Derivation;
85 const firstDerivation =
86 derivations && derivations.length > 0 ? derivations[0] : null;
87 const firstDerivationPath = firstDerivation ? firstDerivation.path : null;
88 const isTestnet =
89 firstDerivationPath && firstDerivationPath.startsWith("m/84'/1'/0'/");
90 const derivationPrefix = isTestnet ? "m/84'/1'/0'/" : "m/84'/0'/0'/";
92 const btcWallet = isTestnet
93 ? bitcoin.bip32.fromBase58(vprv, BITCOIN_TESTNET_INFO)
94 : bitcoin.bip32.fromBase58(zprv, BITCOIN_MAINNET_INFO);
96 try {
97 if (!firstDerivationPath) {
98 throw new Error('No derivation path available');
99 }
100 const hex = newPsbt.data.inputs
101 .reduce((psbt, input, idx) => {
102 // removing already derived part, eg m/84'/0'/0'/0/0 becomes 0/0
103 const path = firstDerivationPath.split(derivationPrefix).join('');
104 const prv = btcWallet.derivePath(path).privateKey;
105 if (!prv) {
106 throw new Error('Private key unavailable');
107 }
108 return psbt.signInput(idx, bitcoin.ECPair.fromPrivateKey(prv));
109 }, newPsbt)
110 .finalizeAllInputs()
111 .extractTransaction()
112 .toHex();
113 setPsbt(newPsbt);
114 setSignedTx(hex);
115 } catch (error) {
116 return {
117 unsignedTransaction: 'Cannot sign transaction, mismatching keys.',
118 };
119 }
120 };
122 let initialValues = {
123 unsignedTransaction: '',
124 };
125 if (flow && COMMANDS.BITCOIN === flow.kind) {
126 initialValues.unsignedTransaction = flow.utx as string;
127 }
129 return (
130 <View
131 className="bitcoin"
132 pop={pop}
133 inset
134 header={<L2BackHeader hideBalance />}
135 hideBack>
136 <Window className="os-home">
137 <HeaderPane>
138 <Row className="header-row">
139 <h5>Bitcoin</h5>
140 <Button onClick={doCopy} className="copy-button">
141 {didCopy ? (
142 'Copied!'
143 ) : (
144 <>
145 <Icon icon="Copy" color="#fff" />
146 Copy Xpub
147 </>
148 )}
149 </Button>
150 </Row>
151 </HeaderPane>
152 <BodyPane>
153 <Grid className="w-full h-full">
154 <BridgeForm
155 validate={validate}
156 initialValues={initialValues}
157 onSubmit={onSubmit}>
158 {({ handleSubmit }: any) => (
159 <Grid.Item full className="h-full flex-col justify-between">
160 <Box className="full">
161 <Box className="label full">Unsigned Transaction</Box>
162 <Grid.Item
163 full
164 placeholder="0x"
165 as={PsbtInput}
166 name="unsignedTransaction"
167 className="mt2"
168 />
169 </Box>
170 <Grid.Item
171 full
172 solid
173 center
174 className="mt2"
175 accessory=""
176 handleSubmit={handleSubmit}
177 as={SubmitButton}>
178 Sign Bitcoin Transaction
179 </Grid.Item>
180 </Grid.Item>
181 )}
182 </BridgeForm>
183 {signedTx && (
184 <Grid.Item full className="mt4" as={Text}>
185 Transaction outputs:
186 </Grid.Item>
187 )}
188 {signedTx &&
189 psbt &&
190 psbt.txOutputs.map(e => (
191 <Grid.Item full className="mt4" key={e.address}>
192 <span>{e.address}</span>
193 <div className="mt1 mono black f6">{e.value} sats</div>
194 </Grid.Item>
195 ))}
196 {signedTx && (
197 <>
198 <Grid.Item full className="mt4" as={ViewHeader}>
199 Signed transaction:
200 </Grid.Item>
201 <Grid.Item full as={Text}>
202 <Highlighted>
203 <CopiableAddressWrap>{signedTx}</CopiableAddressWrap>
204 </Highlighted>
205 </Grid.Item>
206 </>
207 )}
208 </Grid>
209 </BodyPane>
210 </Window>
211 </View>
212 );