on master help sign in
txn: generalize fake signed transactions
7e11fceFang
src/components/InlineEthereumTransaction.js+11−1
@@ -26,6 +26,7 @@ import CopyButton from './CopyButton';
26import ProgressButton from './ProgressButton';
26import ProgressButton from './ProgressButton';
27import { convertToInt } from 'lib/convertToInt';
27import { convertToInt } from 'lib/convertToInt';
28import NeedFundsNotice from './NeedFundsNotice';
28import NeedFundsNotice from './NeedFundsNotice';
29+import NoticeBox from './NoticeBox';
29
30
30export default function InlineEthereumTransaction({
31export default function InlineEthereumTransaction({
31 // from useEthereumTransaction.bind
32 // from useEthereumTransaction.bind
@@ -33,6 +34,7 @@ export default function InlineEthereumTransaction({
33 canSign,
34 canSign,
34 generateAndSign,
35 generateAndSign,
35 signed,
36 signed,
37+ fakeSigned,
36 broadcast,
38 broadcast,
37 broadcasted,
39 broadcasted,
38 confirmed,
40 confirmed,
@@ -277,7 +279,15 @@ export default function InlineEthereumTransaction({
277 </>
279 </>
278 )}
280 )}
279
281
280− {showSignedTx && (
282+ {showSignedTx && fakeSigned && (
283+ <>
284+ <Grid.Item full as={NoticeBox} className="mt2">
285+ Your wallet will sign the transaction upon sending it.
286+ </Grid.Item>
287+ </>
288+ )}
289+
290+ {showSignedTx && !fakeSigned && (
281 <>
291 <>
282 <Grid.Item
292 <Grid.Item
283 full
293 full
src/lib/WalletConnect.ts+21−1
@@ -1,27 +1,47 @@
1import { ITxData } from '@walletconnect/types';
1import { ITxData } from '@walletconnect/types';
2import { Transaction } from '@ethereumjs/tx';
2import { Transaction } from '@ethereumjs/tx';
3import { stripHexPrefix } from './utils/address';
3import { stripHexPrefix } from './utils/address';
4+import { FakeSignResult } from './txn';
4
5
5type SignWalletConnectTransactionArgs = {
6type SignWalletConnectTransactionArgs = {
6 from: string;
7 from: string;
7 txn: Transaction;
8 txn: Transaction;
8 txnSigner: (args: ITxData) => Promise<string>;
9 txnSigner: (args: ITxData) => Promise<string>;
10+ txnSender: (args: ITxData) => Promise<string>;
9};
11};
10
12
11const walletConnectSignTransaction = async ({
13const walletConnectSignTransaction = async ({
12 from,
14 from,
13 txn,
15 txn,
14 txnSigner,
16 txnSigner,
17+ txnSender,
15}: SignWalletConnectTransactionArgs) => {
18}: SignWalletConnectTransactionArgs) => {
16 let wcFormattedTx = txn.toJSON();
19 let wcFormattedTx = txn.toJSON();
17 wcFormattedTx.from = from;
20 wcFormattedTx.from = from;
18 wcFormattedTx.gas = wcFormattedTx.gasLimit;
21 wcFormattedTx.gas = wcFormattedTx.gasLimit;
19
22
23+ let signature;
24+ try {
20− const signature = await txnSigner(wcFormattedTx);
25+ signature = await txnSigner(wcFormattedTx);
26+ } catch (e) {
27+ if (e.message === 'METHOD_NOT_SUPPORTED') {
28+ console.log('connected wc wallet does not support tx signing.');
29+ return new FakeSignResult(
30+ wcFormattedTx,
31+ walletConnectSendTransaction(txnSender)
32+ );
33+ } else {
34+ throw e;
35+ }
36+ }
21 const serializedTx = Buffer.from(stripHexPrefix(signature), 'hex');
37 const serializedTx = Buffer.from(stripHexPrefix(signature), 'hex');
22 const signedTx = Transaction.fromSerializedTx(serializedTx);
38 const signedTx = Transaction.fromSerializedTx(serializedTx);
23
39
24 return signedTx;
40 return signedTx;
25};
41};
26
42
43+const walletConnectSendTransaction = (
44+ txnSender: (tx: any) => Promise<string>
45+) => (_web3, txn): Promise<string> => txnSender(txn);
46+
27export { walletConnectSignTransaction };
47export { walletConnectSignTransaction };
src/lib/metamask.js+15−11
@@ -1,28 +1,32 @@
1import { toHex } from 'web3-utils';
1import { toHex } from 'web3-utils';
2+import { FakeSignResult } from './txn';
2
3
3export function MetamaskWallet(address) {
4export function MetamaskWallet(address) {
4 this.address = address;
5 this.address = address;
5}
6}
6
7
7−export function FakeMetamaskTransaction(txnData) {
8− this.txnData = txnData;
9−
10− this.serialize = function() {
11− return '0';
12− };
13−}
14−
15// Cheat a bit and pretend to sign transaction
8// Cheat a bit and pretend to sign transaction
16// Sign it actually when sending
9// Sign it actually when sending
17export const metamaskSignTransaction = async (txn, from) => {
10export const metamaskSignTransaction = async (txn, from) => {
11+ txn.from = from;
12+ return new FakeSignResult(txn, metamaskSendTransaction);
13+};
14+
15+const metamaskSendTransaction = async (web3, txn) => {
18 const metamaskFormattedTxn = {
16 const metamaskFormattedTxn = {
19 data: txn.data,
17 data: txn.data,
20 gasLimit: txn.gasLimit,
18 gasLimit: txn.gasLimit,
21 gasPrice: txn.gasPrice,
19 gasPrice: txn.gasPrice,
22 nonce: txn.nonce,
20 nonce: txn.nonce,
23 to: toHex(txn.to),
21 to: toHex(txn.to),
24− from: toHex(from),
22+ from: toHex(txn.from),
25 };
23 };
26
24
27− return new FakeMetamaskTransaction(metamaskFormattedTxn);
25+ //NOTE since this gives us a receipt instead of just the hash of the signed
26+ // tx, it makes us wait for confirmation outside of our own
27+ // waitForTransactionConfirm. because of this the progress bar loses
28+ // its progressiveness for metamask users.
29+ //TODO can we do better?
30+ const receipt = await web3.eth.sendTransaction(metamaskFormattedTxn);
31+ return receipt.transactionHash;
28−};
32+}
src/lib/tank.js+6−5
@@ -64,6 +64,12 @@ const ensureFundsFor = async (
64 throw new Error('tank: no transactions provided!');
64 throw new Error('tank: no transactions provided!');
65 }
65 }
66
66
67+ //TODO this depends on FakeSignResult serialization in a somewhat ugly way
68+ if (signedTxs.some(tx => tx === '0x???')) {
69+ console.log('tank: disabling for unsigned sender wallet');
70+ return false;
71+ }
72+
67 const balance = toBN(await web3.eth.getBalance(address));
73 const balance = toBN(await web3.eth.getBalance(address));
68 cost = toBN(cost);
74 cost = toBN(cost);
69
75
@@ -76,11 +82,6 @@ const ensureFundsFor = async (
76 return false;
82 return false;
77 }
83 }
78
84
79− if (walletType === WALLET_TYPES.METAMASK) {
80− console.log('tank: disabling for metamask login');
81−
82− return false;
83− }
84 try {
85 try {
85 // TODO: if we can't always (easily) provide a point, and the
86 // TODO: if we can't always (easily) provide a point, and the
86 // fundTransactions call is gonna fail anyway, should we maybe
87 // fundTransactions call is gonna fail anyway, should we maybe
src/lib/txn.ts+39−12
@@ -8,7 +8,7 @@ import { NETWORK_TYPES } from './network';
8import { ledgerSignTransaction } from './ledger';
8import { ledgerSignTransaction } from './ledger';
9import { trezorSignTransaction } from './trezor';
9import { trezorSignTransaction } from './trezor';
10import { walletConnectSignTransaction } from './WalletConnect';
10import { walletConnectSignTransaction } from './WalletConnect';
11−import { metamaskSignTransaction, FakeMetamaskTransaction } from './metamask';
11+import { metamaskSignTransaction } from './metamask';
12import { addHexPrefix } from './utils/address';
12import { addHexPrefix } from './utils/address';
13import { CHECK_BLOCK_EVERY_MS, WALLET_TYPES } from './constants';
13import { CHECK_BLOCK_EVERY_MS, WALLET_TYPES } from './constants';
14import { patp2dec } from './patp2dec';
14import { patp2dec } from './patp2dec';
@@ -20,6 +20,22 @@ const RETRY_OPTIONS = {
20 randomize: false,
20 randomize: false,
21};
21};
22
22
23+type SignedTx = {
24+ serialize: () => string;
25+};
26+
27+//TODO merge with FakeSignResult, get it type-checking properly
28+type FakeSignedTx = SignedTx & {
29+ txn: any;
30+ send: (web3: any, txn: any) => Promise<string>; // txhash
31+};
32+
33+function FakeSignResult(txn, send) {
34+ this.txn = txn;
35+ this.serialize = () => '???'; //NOTE update tank.js when changing this!
36+ this.send = send;
37+}
38+
23const signTransaction = async ({
39const signTransaction = async ({
24 wallet,
40 wallet,
25 walletType,
41 walletType,
@@ -30,7 +46,8 @@ const signTransaction = async ({
30 chainId, // number
46 chainId, // number
31 gasPrice, // string, in gwei
47 gasPrice, // string, in gwei
32 gasLimit, // string | number
48 gasLimit, // string | number
33− txnSigner, // optionally inject a transaction signing function
49+ txnSigner, // optionally inject a transaction signing function,
50+ txnSender, // and a sending function, for wallets that need these passed in.
34}) => {
51}) => {
35 // TODO: require these in txn object
52 // TODO: require these in txn object
36 nonce = toHex(nonce);
53 nonce = toHex(nonce);
@@ -115,6 +132,7 @@ const signTransaction = async ({
115 from,
132 from,
116 txn: stx,
133 txn: stx,
117 txnSigner,
134 txnSigner,
135+ txnSender,
118 });
136 });
119 } else {
137 } else {
120 stx = stx.sign(wallet.privateKey);
138 stx = stx.sign(wallet.privateKey);
@@ -123,19 +141,27 @@ const signTransaction = async ({
123 return stx;
141 return stx;
124};
142};
125
143
126−const sendSignedTransaction = (web3, stx, doubtNonceError) => {
144+const sendSignedTransaction = (
127− // Handle fake metamask transaction
145+ web3,
128− let eventEmitter;
146+ stx: SignedTx,
129− let rawTx;
147+ doubtNonceError: boolean
130− if (stx instanceof FakeMetamaskTransaction) {
148+): Promise<string> => {
131− eventEmitter = web3.eth.sendTransaction(stx.txnData);
149+ // if we couldn't sign it, we depend on the given sender function
132− } else {
150+ if (stx instanceof FakeSignResult) { //TODO set up for proper typecheck
133− rawTx = hexify(stx.serialize());
151+ if (doubtNonceError) {
134− eventEmitter = web3.eth.sendSignedTransaction(rawTx);
152+ console.log('why doubting nonce error? tank unavailable without rawtx.');
153+ }
154+ if (!stx.send) {
155+ throw new Error('no sign+sending function available');
156+ }
157+ return stx.send(web3, stx.txn);
135 }
158 }
159+
160+ let rawTx: string = hexify(stx.serialize());
161+ let eventEmitter = web3.eth.sendSignedTransaction(rawTx);
136 return new Promise(async (resolve, reject) => {
162 return new Promise(async (resolve, reject) => {
137 eventEmitter
163 eventEmitter
138− .on('transactionHash', hash => {
164+ .on('transactionHash', (hash: string) => {
139 resolve(hash);
165 resolve(hash);
140 })
166 })
141 .on('error', err => {
167 .on('error', err => {
@@ -240,6 +266,7 @@ const canDecodePatp = p => {
240
266
241export {
267export {
242 RETRY_OPTIONS,
268 RETRY_OPTIONS,
269+ FakeSignResult,
243 signTransaction,
270 signTransaction,
244 sendSignedTransaction,
271 sendSignedTransaction,
245 waitForTransactionConfirm,
272 waitForTransactionConfirm,
src/lib/useEthereumTransaction.js+19−3
@@ -17,6 +17,7 @@ import {
17 sendSignedTransaction,
17 sendSignedTransaction,
18 waitForTransactionConfirm,
18 waitForTransactionConfirm,
19 hexify,
19 hexify,
20+ FakeSignResult,
20} from './txn';
21} from './txn';
21import * as need from 'lib/need';
22import * as need from 'lib/need';
22import { ensureFundsFor } from 'lib/tank';
23import { ensureFundsFor } from 'lib/tank';
@@ -29,6 +30,7 @@ import { useWalletConnect } from './useWalletConnect';
29const STATE = {
30const STATE = {
30 NONE: 'NONE',
31 NONE: 'NONE',
31 SIGNED: 'SIGNED',
32 SIGNED: 'SIGNED',
33+ FAKE_SIGNED: 'FAKE_SIGNED',
32 BROADCASTED: 'BROADCASTED',
34 BROADCASTED: 'BROADCASTED',
33 CONFIRMED: 'CONFIRMED',
35 CONFIRMED: 'CONFIRMED',
34 COMPLETED: 'COMPLETED',
36 COMPLETED: 'COMPLETED',
@@ -50,7 +52,10 @@ export default function useEthereumTransaction(
50 const { wallet, walletType, walletHdPath } = useWallet();
52 const { wallet, walletType, walletHdPath } = useWallet();
51 const { web3, networkType } = useNetwork();
53 const { web3, networkType } = useNetwork();
52 const { pointCursor } = usePointCursor();
54 const { pointCursor } = usePointCursor();
55+ const {
53− const { signTransaction: wcSign } = useWalletConnect();
56+ signTransaction: wcSign,
57+ sendTransaction: wcSend,
58+ } = useWalletConnect();
54
59
55 const _web3 = need.web3(web3);
60 const _web3 = need.web3(web3);
56 const _wallet = need.wallet(wallet);
61 const _wallet = need.wallet(wallet);
@@ -81,7 +86,8 @@ export default function useEthereumTransaction(
81 const initializing = nonce === undefined || chainId === undefined;
86 const initializing = nonce === undefined || chainId === undefined;
82 const constructed = !!unsignedTransactions;
87 const constructed = !!unsignedTransactions;
83 const isDefaultState = state === STATE.NONE;
88 const isDefaultState = state === STATE.NONE;
84− const signed = state === STATE.SIGNED;
89+ const signed = state === STATE.SIGNED || state === STATE.FAKE_SIGNED;
90+ const fakeSigned = state === STATE.FAKE_SIGNED;
85 const broadcasted = state === STATE.BROADCASTED;
91 const broadcasted = state === STATE.BROADCASTED;
86 const confirmed = state === STATE.CONFIRMED;
92 const confirmed = state === STATE.CONFIRMED;
87 const completed = state === STATE.COMPLETED;
93 const completed = state === STATE.COMPLETED;
@@ -115,6 +121,8 @@ export default function useEthereumTransaction(
115 // https://reactjs.org/docs/hooks-rules.html
121 // https://reactjs.org/docs/hooks-rules.html
116 const txnSigner =
122 const txnSigner =
117 walletType === WALLET_TYPES.WALLET_CONNECT ? wcSign : undefined;
123 walletType === WALLET_TYPES.WALLET_CONNECT ? wcSign : undefined;
124+ const txnSender =
125+ walletType === WALLET_TYPES.WALLET_CONNECT ? wcSend : undefined;
118
126
119 const txns = await Promise.all(
127 const txns = await Promise.all(
120 utxs.map((utx, i) =>
128 utxs.map((utx, i) =>
@@ -129,12 +137,18 @@ export default function useEthereumTransaction(
129 gasPrice: gasPrice.toFixed(0),
137 gasPrice: gasPrice.toFixed(0),
130 gasLimit,
138 gasLimit,
131 txnSigner,
139 txnSigner,
140+ txnSender,
132 })
141 })
133 )
142 )
134 );
143 );
135
144
136 setSignedTransactions(txns);
145 setSignedTransactions(txns);
146+
147+ if (txns.some(tx => tx instanceof FakeSignResult)) {
148+ setState(STATE.FAKE_SIGNED);
149+ } else {
137− setState(STATE.SIGNED);
150+ setState(STATE.SIGNED);
151+ }
138 } catch (error) {
152 } catch (error) {
139 console.error('error signing txs', error);
153 console.error('error signing txs', error);
140 setError(error);
154 setError(error);
@@ -151,6 +165,7 @@ export default function useEthereumTransaction(
151 walletHdPath,
165 walletHdPath,
152 walletType,
166 walletType,
153 wcSign,
167 wcSign,
168+ wcSend,
154 ]);
169 ]);
155
170
156 const broadcast = useCallback(async () => {
171 const broadcast = useCallback(async () => {
@@ -327,6 +342,7 @@ export default function useEthereumTransaction(
327 canSign,
342 canSign,
328 generateAndSign,
343 generateAndSign,
329 signed,
344 signed,
345+ fakeSigned,
330 broadcast,
346 broadcast,
331 broadcasted,
347 broadcasted,
332 confirmed,
348 confirmed,
src/lib/useWalletConnect.ts+32
@@ -152,6 +152,37 @@ export const useWalletConnect = () => {
152 data,
152 data,
153 nonce,
153 nonce,
154 })
154 })
155+ .then((signature: string) => resolve(signature))
156+ .catch((error: Error) => reject(error));
157+ });
158+ };
159+
160+ const sendTransaction = async ({
161+ from,
162+ to,
163+ gas,
164+ gasPrice,
165+ value,
166+ data,
167+ nonce,
168+ }: ITxData): Promise<string> => {
169+ return new Promise((resolve, reject) => {
170+ if (!connector || !isConnected()) {
171+ reject(new Error('No connected wallet available for signing'));
172+ return;
173+ }
174+
175+ //REVIEW .then path untested
176+ return connector
177+ .sendTransaction({
178+ from,
179+ to,
180+ gas,
181+ gasPrice,
182+ value,
183+ data,
184+ nonce,
185+ })
155 .then((txHash: string) => resolve(txHash))
186 .then((txHash: string) => resolve(txHash))
156 .catch((error: Error) => reject(error));
187 .catch((error: Error) => reject(error));
157 });
188 });
@@ -247,5 +278,6 @@ export const useWalletConnect = () => {
247 peerMeta,
278 peerMeta,
248 resetConnector,
279 resetConnector,
249 signTransaction,
280 signTransaction,
281+ sendTransaction,
250 };
282 };
251};
283};