Skip to content

Commit

Permalink
feature: handle wallet change after transaction has been generated
Browse files Browse the repository at this point in the history
  • Loading branch information
yushih committed Jul 16, 2019
1 parent 76c1bf9 commit b373629
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 8 deletions.
1 change: 1 addition & 0 deletions app/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@
"yoroiTransfer.error.noTransferTxError": "There is no transfer transaction to send.",
"yoroiTransfer.errorPage.title.label": "Unable to transfer from another wallet",
"yoroiTransfer.error.transferFundsError": "Unable to transfer funds.",
"yoroiTransfer.error.walletChangedError": "The wallet has changed. Please re-confirm your transaction.",
"yoroiTransfer.form.instructions.step0.text": "Enter the 15-word recovery phrase used to back up your other wallet to restore the balance and transfer all the funds to current wallet.",
"yoroiTransfer.start.instructions.text": "Transfer funds from another wallet (Yoroi, AdaLite, etc.).",
"yoroiTransfer.successPage.text": "Your funds were successfully transfered.",
Expand Down
60 changes: 56 additions & 4 deletions app/stores/ada/YoroiTransferStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export default class YoroiTransferStore extends Store {
= new Request(this.api.ada.restoreWalletForTransfer);
@observable error: ?LocalizableError = null;
@observable transferTx: ?TransferTx = null;
// The addresses and their corresponding keys from which funds will be transferred.
addressKeysCache: ?{[addr: string]: RustModule.Wallet.PrivateKey} = null;

_errorWrapper = <PT, RT>(func: PT=>Promise<RT>): (PT => Promise<RT>) => (async (payload) => {
try {
Expand Down Expand Up @@ -114,6 +116,7 @@ export default class YoroiTransferStore extends Store {
const keyPrv = chainPrv.address_key(RustModule.Wallet.AddressKeyIndex.new(index));
addressKeys[address] = keyPrv;
});
this.addressKeysCache = addressKeys;

const outputAddr = await getReceiverAddress();
// Possible exception: NotEnoughMoneyToSendError
Expand Down Expand Up @@ -159,10 +162,45 @@ export default class YoroiTransferStore extends Store {
await next();
this._reset();
} catch (error) {
Logger.error(`YoroiTransferStore::transferFunds ${stringifyError(error)}`);
runInAction(() => {
this.error = new TransferFundsError();
});
/* Determine and handle the scenario where the wallet has changed since
the tx has been generated.
Since the backend API returns a generic error message, we determine
the case by re-generate the transaction and compare with the previous
one.
*/
let walletChanged = false;
let transferTx;
if (error.id === 'api.errors.sendTransactionApiError') {
const outputAddr = await getReceiverAddress();
if (!this.addressKeysCache) {
// Should never happen but need to please flow
throw new Error('No address keys');
}
transferTx = await generateTransferTx({
outputAddr,
addressKeys: this.addressKeysCache,
getUTXOsForAddresses:
this.stores.substores.ada.stateFetchStore.fetcher.getUTXOsForAddresses,
filterSenders: true
});
if (!this.transferTx) {
throw new NoTransferTxError();
}
if (!transferTx.recoveredBalance.isEqualTo(this.transferTx.recoveredBalance)) {
walletChanged = true;
}
}
if (walletChanged) {
runInAction(() => {
this.transferTx = transferTx;
this.error = new WalletChangedError();
});
} else {
Logger.error(`YoroiTransferStore::transferFunds ${stringifyError(error)}`);
runInAction(() => {
this.error = new TransferFundsError();
});
}
}
}

Expand Down Expand Up @@ -206,6 +244,10 @@ const messages = defineMessages({
id: 'yoroiTransfer.error.noTransferTxError',
defaultMessage: '!!!There is no transfer transaction to send.',
},
walletChangedError: {
id: 'yoroiTransfer.error.walletChangedError',
defaultMessage: '!!!The wallet has changed. Please re-confirm your transaction.',
}
});

export class TransferFundsError extends LocalizableError {
Expand All @@ -227,3 +269,13 @@ export class NoTransferTxError extends LocalizableError {
});
}
}

class WalletChangedError extends LocalizableError {
constructor() {
super({
id: messages.walletChangedError.id,
defaultMessage: messages.walletChangedError.defaultMessage || '',
description: messages.walletChangedError.description,
});
}
}
24 changes: 21 additions & 3 deletions features/mock-chain/mockServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ function _defaultSignedTransaction(

let MockServer = null;

export const signedTransactionHandler = [];
export const utxoForAddressesHook = [];

export function getMockServer(
settings: {
signedTransaction?: (
Expand All @@ -78,13 +81,16 @@ export function getMockServer(
): void => {
chai.assert.isTrue(_validateAddressesReq(req.body));
const utxoForAddresses = mockImporter.utxoForAddresses();
const filteredUtxos = Object.keys(utxoForAddresses)
let filteredUtxos = Object.keys(utxoForAddresses)
.filter(addr => req.body.addresses.includes(addr))
.map(addr => utxoForAddresses[addr])
.reduce((utxos, arr) => {
utxos.push(...arr);
return utxos;
}, []);
if (utxoForAddressesHook.length) {
filteredUtxos = utxoForAddressesHook.pop()(filteredUtxos);
}
res.send(filteredUtxos);
});

Expand Down Expand Up @@ -128,8 +134,20 @@ export function getMockServer(
res.send(filteredTxs.slice(0, txsLimit));
});

server.post('/api/txs/signed', settings.signedTransaction ?
settings.signedTransaction : _defaultSignedTransaction);
server.post('/api/txs/signed', (
req: {
body: SignedRequest
},
res: { send(arg: SignedResponse): any, status: Function }
): void => {
if (signedTransactionHandler.length) {
signedTransactionHandler.pop()(req, res);
} else if (settings.signedTransaction) {
settings.signedTransaction(req, res);
} else {
_defaultSignedTransaction(req, res);
}
});

server.post('/api/addresses/filterUsed', (
req: {
Expand Down
28 changes: 28 additions & 0 deletions features/step_definitions/yoroi-transfer-steps.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow

import { Given, When, Then } from 'cucumber';
import BigNumber from 'bignumber.js';
import {
navigateTo,
waitUntilUrlEquals
Expand All @@ -10,6 +11,10 @@ import {
checkAddressesRecoveredAreCorrect,
checkTotalAmountIsCorrect
} from '../support/helpers/transfer-helpers';
import {
signedTransactionHandler,
utxoForAddressesHook,
} from '../mock-chain/mockServer';

Given(/^I am on the Yoroi Transfer start screen$/, async function () {
await navigateTo.call(this, '/transfer/yoroi');
Expand Down Expand Up @@ -50,3 +55,26 @@ Then(/^I should see the next button on the Yoroi transfer start screen disabled$
Then(/^I should see the "CREATE YOROI WALLET" button disabled$/, async function () {
await this.waitDisable('.createYoroiWallet.YoroiTransferStartPage_button');
});

Then(/^I transfer some Ada out of the source wallet$/, async (table) => {
const { fromAddress, amount } = table.hashes()[0];
// Next request to /api/txs/signed should fail
signedTransactionHandler.push((req, res) => {
res.status(500);
// Mimicking the backend behavior
res.send({ code: 'Internal', message: 'Error trying to connect with importer' });
});
utxoForAddressesHook.push(utxos => utxos.map(utxo => {
if (utxo.receiver === fromAddress) {
return Object.assign(utxo, { amount:
new BigNumber(utxo.amount).minus(new BigNumber(amount)).toString() });
}
return utxo;
}));
});

Then(/^I should see wallet changed notice$/, async function () {
const walletChangedError = await i18n.formatMessage(this.driver,
{ id: 'yoroiTransfer.error.walletChangedError' });
await this.waitUntilText('.TransferSummaryPage_error', walletChangedError);
});
25 changes: 24 additions & 1 deletion features/yoroi-transfer.feature
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,27 @@ Feature: Transfer Yoroi Wallet funds
And I am on the Yoroi Transfer start screen
Then I should see the next button on the Yoroi transfer start screen disabled
When I click on the create Yoroi wallet button
Then I should see the Create wallet screen
Then I should see the Create wallet screen

@it-113
Scenario: Wallet changes after transaction is generated (IT-113)
Given There is a wallet stored named empty-wallet
And I am on the Yoroi Transfer start screen
When I click on the next button on the Yoroi Transfer start screen
And I enter the recovery phrase:
| recoveryPhrase |
| dragon mango general very inmate idea rabbit pencil element bleak term cart critic kite pill |
And I proceed with the recovery
Then I should see on the Yoroi transfer summary screen:
| fromAddress | amount |
| Ae2tdPwUPEYx2dK1AMzRN1GqNd2eY7GCd7Z6aikMPJL3EkqqugoFQComQnV | 1234567898765 |
Then I transfer some Ada out of the source wallet
| fromAddress | amount |
| Ae2tdPwUPEYx2dK1AMzRN1GqNd2eY7GCd7Z6aikMPJL3EkqqugoFQComQnV | 1000000000 |
When I confirm Yoroi transfer funds
Then I should see wallet changed notice
And I should see on the Yoroi transfer summary screen:
| fromAddress | amount |
| Ae2tdPwUPEYx2dK1AMzRN1GqNd2eY7GCd7Z6aikMPJL3EkqqugoFQComQnV | 1233567898765 |
When I confirm Yoroi transfer funds
Then I should see the Yoroi transfer success screen

0 comments on commit b373629

Please sign in to comment.