on master help sign in
txn: generalize fake signed transactions

Not all wallets support transaction signing as separate from sending. Metamask was one such case, we handled it through special logic. With WalletConnect, we might connect to a wallet that doesn't support transaction signing either. As such, we generalize Metamask's special logic. When the wallet doesn't support transaction signing, we pretend to succeed, but produce a "fake signed transaction" instead. This has stubbed out serialization logic, and contains the original transaction data plus a callback for sending that transaction. In these cases, the user is informed that their transaction will be signed by their wallet when sending. The signed transaction fold-out is not shown. Worth noting that, for the WalletConnect case, we cannot tell whether the connected wallet supports transaction signing or not. So we simply try to do it, and if it fails with METHOD_NOT_SUPPORTED, we know what's up and proceed with the "fake signed" logic accordingly.

commit7e11fce8fb8adc46dda7a31fc47170ee5a7946c1 authorFang <git@fang.io> dateThu 19 Aug 2021, 00:21 (5 years ago) parente1b0301
7 files changed+143−33
src/components/InlineEthereumTransaction.js+11−1 src/lib/WalletConnect.ts+21−1 src/lib/metamask.js+15−11 src/lib/tank.js+6−5 src/lib/txn.ts+39−12 src/lib/useEthereumTransaction.js+19−3 src/lib/useWalletConnect.ts+32
src/components/InlineEthereumTransaction.js+11−1
@@ -26,6 +26,7 @@ import CopyButton from './CopyButton';
2626import ProgressButton from './ProgressButton';
2727import { convertToInt } from 'lib/convertToInt';
2828import NeedFundsNotice from './NeedFundsNotice';
29+import NoticeBox from './NoticeBox';
2930
3031export default function InlineEthereumTransaction({
3132 // from useEthereumTransaction.bind
@@ -33,6 +34,7 @@ export default function InlineEthereumTransaction({
3334 canSign,
3435 generateAndSign,
3536 signed,
37+ fakeSigned,
3638 broadcast,
3739 broadcasted,
3840 confirmed,
@@ -277,7 +279,15 @@ export default function InlineEthereumTransaction({
277279 </>
278280 )}
279281
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 && (
281291 <>
282292 <Grid.Item
283293 full
src/lib/WalletConnect.ts+21−1
@@ -1,27 +1,47 @@
11import { ITxData } from '@walletconnect/types';
22import { Transaction } from '@ethereumjs/tx';
33import { stripHexPrefix } from './utils/address';
4+import { FakeSignResult } from './txn';
45
56type SignWalletConnectTransactionArgs = {
67 from: string;
78 txn: Transaction;
89 txnSigner: (args: ITxData) => Promise<string>;
10+ txnSender: (args: ITxData) => Promise<string>;
911};
1012
1113const walletConnectSignTransaction = async ({
1214 from,
1315 txn,
1416 txnSigner,
17+ txnSender,
1518}: SignWalletConnectTransactionArgs) => {
1619 let wcFormattedTx = txn.toJSON();
1720 wcFormattedTx.from = from;
1821 wcFormattedTx.gas = wcFormattedTx.gasLimit;
1922
20− const signature = await txnSigner(wcFormattedTx);
23+ let signature;
24+ try {
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+ }
2137 const serializedTx = Buffer.from(stripHexPrefix(signature), 'hex');
2238 const signedTx = Transaction.fromSerializedTx(serializedTx);
2339
2440 return signedTx;
2541};
2642
43+const walletConnectSendTransaction = (
44+ txnSender: (tx: any) => Promise<string>
45+) => (_web3, txn): Promise<string> => txnSender(txn);
46+
2747export { walletConnectSignTransaction };
src/lib/metamask.js+15−11
@@ -1,28 +1,32 @@
11import { toHex } from 'web3-utils';
2+import { FakeSignResult } from './txn';
23
34export function MetamaskWallet(address) {
45 this.address = address;
56}
67
7−export function FakeMetamaskTransaction(txnData) {
8− this.txnData = txnData;
9−
10− this.serialize = function() {
11− return '0';
12− };
13−}
14−
158// Cheat a bit and pretend to sign transaction
169// Sign it actually when sending
1710export const metamaskSignTransaction = async (txn, from) => {
11+ txn.from = from;
12+ return new FakeSignResult(txn, metamaskSendTransaction);
13+};
14+
15+const metamaskSendTransaction = async (web3, txn) => {
1816 const metamaskFormattedTxn = {
1917 data: txn.data,
2018 gasLimit: txn.gasLimit,
2119 gasPrice: txn.gasPrice,
2220 nonce: txn.nonce,
2321 to: toHex(txn.to),
24− from: toHex(from),
22+ from: toHex(txn.from),
2523 };
2624
27− return new FakeMetamaskTransaction(metamaskFormattedTxn);
28−};
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;
32+}
src/lib/tank.js+6−5
@@ -64,6 +64,12 @@ const ensureFundsFor = async (
6464 throw new Error('tank: no transactions provided!');
6565 }
6666
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+
6773 const balance = toBN(await web3.eth.getBalance(address));
6874 cost = toBN(cost);
6975
@@ -76,11 +82,6 @@ const ensureFundsFor = async (
7682 return false;
7783 }
7884
79− if (walletType === WALLET_TYPES.METAMASK) {
80− console.log('tank: disabling for metamask login');
81−
82− return false;
83− }
8485 try {
8586 // TODO: if we can't always (easily) provide a point, and the
8687 // fundTransactions call is gonna fail anyway, should we maybe
src/lib/txn.ts+39−12
@@ -8,7 +8,7 @@ import { NETWORK_TYPES } from './network';
88import { ledgerSignTransaction } from './ledger';
99import { trezorSignTransaction } from './trezor';
1010import { walletConnectSignTransaction } from './WalletConnect';
11−import { metamaskSignTransaction, FakeMetamaskTransaction } from './metamask';
11+import { metamaskSignTransaction } from './metamask';
1212import { addHexPrefix } from './utils/address';
1313import { CHECK_BLOCK_EVERY_MS, WALLET_TYPES } from './constants';
1414import { patp2dec } from './patp2dec';
@@ -20,6 +20,22 @@ const RETRY_OPTIONS = {
2020 randomize: false,
2121};
2222
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+
2339const signTransaction = async ({
2440 wallet,
2541 walletType,
@@ -30,7 +46,8 @@ const signTransaction = async ({
3046 chainId, // number
3147 gasPrice, // string, in gwei
3248 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.
3451}) => {
3552 // TODO: require these in txn object
3653 nonce = toHex(nonce);
@@ -115,6 +132,7 @@ const signTransaction = async ({
115132 from,
116133 txn: stx,
117134 txnSigner,
135+ txnSender,
118136 });
119137 } else {
120138 stx = stx.sign(wallet.privateKey);
@@ -123,19 +141,27 @@ const signTransaction = async ({
123141 return stx;
124142};
125143
126−const sendSignedTransaction = (web3, stx, doubtNonceError) => {
127− // Handle fake metamask transaction
128− let eventEmitter;
129− let rawTx;
130− if (stx instanceof FakeMetamaskTransaction) {
131− eventEmitter = web3.eth.sendTransaction(stx.txnData);
132− } else {
133− rawTx = hexify(stx.serialize());
134− eventEmitter = web3.eth.sendSignedTransaction(rawTx);
144+const sendSignedTransaction = (
145+ web3,
146+ stx: SignedTx,
147+ doubtNonceError: boolean
148+): Promise<string> => {
149+ // if we couldn't sign it, we depend on the given sender function
150+ if (stx instanceof FakeSignResult) { //TODO set up for proper typecheck
151+ if (doubtNonceError) {
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);
135158 }
159+
160+ let rawTx: string = hexify(stx.serialize());
161+ let eventEmitter = web3.eth.sendSignedTransaction(rawTx);
136162 return new Promise(async (resolve, reject) => {
137163 eventEmitter
138− .on('transactionHash', hash => {
164+ .on('transactionHash', (hash: string) => {
139165 resolve(hash);
140166 })
141167 .on('error', err => {
@@ -240,6 +266,7 @@ const canDecodePatp = p => {
240266
241267export {
242268 RETRY_OPTIONS,
269+ FakeSignResult,
243270 signTransaction,
244271 sendSignedTransaction,
245272 waitForTransactionConfirm,
src/lib/useEthereumTransaction.js+19−3
@@ -17,6 +17,7 @@ import {
1717 sendSignedTransaction,
1818 waitForTransactionConfirm,
1919 hexify,
20+ FakeSignResult,
2021} from './txn';
2122import * as need from 'lib/need';
2223import { ensureFundsFor } from 'lib/tank';
@@ -29,6 +30,7 @@ import { useWalletConnect } from './useWalletConnect';
2930const STATE = {
3031 NONE: 'NONE',
3132 SIGNED: 'SIGNED',
33+ FAKE_SIGNED: 'FAKE_SIGNED',
3234 BROADCASTED: 'BROADCASTED',
3335 CONFIRMED: 'CONFIRMED',
3436 COMPLETED: 'COMPLETED',
@@ -50,7 +52,10 @@ export default function useEthereumTransaction(
5052 const { wallet, walletType, walletHdPath } = useWallet();
5153 const { web3, networkType } = useNetwork();
5254 const { pointCursor } = usePointCursor();
53− const { signTransaction: wcSign } = useWalletConnect();
55+ const {
56+ signTransaction: wcSign,
57+ sendTransaction: wcSend,
58+ } = useWalletConnect();
5459
5560 const _web3 = need.web3(web3);
5661 const _wallet = need.wallet(wallet);
@@ -81,7 +86,8 @@ export default function useEthereumTransaction(
8186 const initializing = nonce === undefined || chainId === undefined;
8287 const constructed = !!unsignedTransactions;
8388 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;
8591 const broadcasted = state === STATE.BROADCASTED;
8692 const confirmed = state === STATE.CONFIRMED;
8793 const completed = state === STATE.COMPLETED;
@@ -115,6 +121,8 @@ export default function useEthereumTransaction(
115121 // https://reactjs.org/docs/hooks-rules.html
116122 const txnSigner =
117123 walletType === WALLET_TYPES.WALLET_CONNECT ? wcSign : undefined;
124+ const txnSender =
125+ walletType === WALLET_TYPES.WALLET_CONNECT ? wcSend : undefined;
118126
119127 const txns = await Promise.all(
120128 utxs.map((utx, i) =>
@@ -129,12 +137,18 @@ export default function useEthereumTransaction(
129137 gasPrice: gasPrice.toFixed(0),
130138 gasLimit,
131139 txnSigner,
140+ txnSender,
132141 })
133142 )
134143 );
135144
136145 setSignedTransactions(txns);
137− setState(STATE.SIGNED);
146+
147+ if (txns.some(tx => tx instanceof FakeSignResult)) {
148+ setState(STATE.FAKE_SIGNED);
149+ } else {
150+ setState(STATE.SIGNED);
151+ }
138152 } catch (error) {
139153 console.error('error signing txs', error);
140154 setError(error);
@@ -151,6 +165,7 @@ export default function useEthereumTransaction(
151165 walletHdPath,
152166 walletType,
153167 wcSign,
168+ wcSend,
154169 ]);
155170
156171 const broadcast = useCallback(async () => {
@@ -327,6 +342,7 @@ export default function useEthereumTransaction(
327342 canSign,
328343 generateAndSign,
329344 signed,
345+ fakeSigned,
330346 broadcast,
331347 broadcasted,
332348 confirmed,
src/lib/useWalletConnect.ts+32
@@ -152,6 +152,37 @@ export const useWalletConnect = () => {
152152 data,
153153 nonce,
154154 })
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+ })
155186 .then((txHash: string) => resolve(txHash))
156187 .catch((error: Error) => reject(error));
157188 });
@@ -247,5 +278,6 @@ export const useWalletConnect = () => {
247278 peerMeta,
248279 resetConnector,
249280 signTransaction,
281+ sendTransaction,
250282 };
251283};