@@ -151,7 +153,7 @@ class SwapDetails extends React.Component {
)}
{t('details.transactions')}
diff --git a/app/renderer/containers/Exchange.js b/app/renderer/containers/Exchange.js
index e99ce20d4b..e06cc122dd 100644
--- a/app/renderer/containers/Exchange.js
+++ b/app/renderer/containers/Exchange.js
@@ -1,6 +1,7 @@
/* eslint-disable react/no-access-state-in-setstate */
import {is, api, activeWindow, appLaunchTimestamp} from 'electron-util';
import _ from 'lodash';
+import {isPast, addHours} from 'date-fns';
import SuperContainer from 'containers/SuperContainer';
import appContainer from 'containers/App';
import {translate} from '../translate';
@@ -22,12 +23,23 @@ class ExchangeContainer extends SuperContainer {
askdepth: 0,
},
isSendingOrder: false,
+ doneInitialKickstart: false,
};
}
- componentDidInitialMount() {
- this.setSwapHistory();
+ async componentDidInitialMount() {
+ await this.setSwapHistory();
appContainer.swapDB.on('change', this.setSwapHistory);
+ appContainer.api.socket.on('message', message => {
+ const uuids = this.state.swapHistory.map(swap => swap.uuid);
+ if (uuids.includes(message.uuid)) {
+ appContainer.swapDB.updateSwapData(message);
+ }
+ });
+
+ fireEvery({minutes: 15}, async () => {
+ await this.kickstartStuckSwaps();
+ });
}
constructor() {
@@ -49,6 +61,23 @@ class ExchangeContainer extends SuperContainer {
});
}
+ async kickstartStuckSwaps() {
+ const {doneInitialKickstart} = this.state;
+ this.state.swapHistory
+ .filter(swap => (
+ swap.status === 'swapping' &&
+ (!doneInitialKickstart || isPast(addHours(swap.timeStarted, 4)))
+ ))
+ .forEach(async swap => {
+ const {requestId, quoteId} = swap;
+ await appContainer.api.kickstart({requestId, quoteId});
+ });
+
+ if (!doneInitialKickstart) {
+ this.setState({doneInitialKickstart: true});
+ }
+ }
+
setSwapHistory = async () => {
this.setState({swapHistory: await appContainer.swapDB.getSwaps()});
};
diff --git a/app/renderer/marketmaker-socket.js b/app/renderer/marketmaker-socket.js
index 08e48beef5..764c62b9e0 100644
--- a/app/renderer/marketmaker-socket.js
+++ b/app/renderer/marketmaker-socket.js
@@ -1,8 +1,6 @@
import Emittery from 'emittery';
import pEvent from 'p-event';
import readBlob from 'read-blob';
-import pAny from 'p-any';
-import delay from 'delay';
class MarketmakerSocket {
constructor(endpoint) {
@@ -32,52 +30,6 @@ class MarketmakerSocket {
}
getResponse = queueId => this._ee.once(`id_${queueId}`);
-
- // Returns an EventEmitter that will emit events as the status of the swap progresses
- // Important events:
- // - 'progress': All messages related to the swap.
- // - 'connected': The swap has been matched.
- // - 'update': The atomic swap has advanced a step, the step flag is in the `update` property.
- // - 'finished': The atomic swap level has successfully completed.
- // - 'failed': The atomic swap has failed, the error code is in the property `error`.
- //
- // Any other message with a `method` property will also be emitted via en event of the same name.
- subscribeToSwap(uuid) {
- if (typeof uuid === 'undefined') {
- throw new TypeError(`uuid is required`);
- }
-
- const swapEmitter = new Emittery();
-
- const removeListener = this.on('message', message => {
- if (message.uuid !== uuid) {
- return;
- }
-
- swapEmitter.emit('progress', message);
- if (message.method) {
- swapEmitter.emit(message.method, message);
- }
- if (message.method === 'tradestatus' && message.status === 'finished') {
- swapEmitter.emit('completed', message);
- }
- });
-
- // We should wait for 10 minutes before removing the listener
- // to handle edge cases where swaps can breifly have an incorrect
- // status due to communication issues:
- // https://github.com/jl777/SuperNET/issues/756
- const TEN_MINUTES = 1000 * 60 * 10;
- pAny([
- swapEmitter.once('failed'),
- swapEmitter.once('completed'),
- ]).then(async () => {
- await delay(TEN_MINUTES);
- removeListener();
- });
-
- return swapEmitter;
- }
}
export default MarketmakerSocket;
diff --git a/app/renderer/swap-db.js b/app/renderer/swap-db.js
index 47cbce01d6..02ff9615d1 100644
--- a/app/renderer/swap-db.js
+++ b/app/renderer/swap-db.js
@@ -93,6 +93,7 @@ class SwapDB {
});
}
+ // TODO: We should refactor this into a seperate file
_formatSwap(data) {
const MATCHED_STEP = 1;
const TOTAL_PROGRESS_STEPS = swapTransactions.length + MATCHED_STEP;
@@ -108,6 +109,8 @@ class SwapDB {
const swap = {
uuid,
+ requestId: undefined,
+ quoteId: undefined,
timeStarted,
orderType: isBuyOrder ? 'buy' : 'sell',
status: 'pending',
@@ -144,6 +147,13 @@ class SwapDB {
};
messages.forEach(message => {
+ if (message.requestid) {
+ swap.requestId = message.requestid;
+ }
+ if (message.quoteid) {
+ swap.quoteId = message.quoteid;
+ }
+
if (message.method === 'connected') {
swap.status = 'matched';
swap.progress = MATCHED_STEP / TOTAL_PROGRESS_STEPS;
@@ -151,36 +161,130 @@ class SwapDB {
if (message.method === 'update') {
swap.status = 'swapping';
- swap.transactions.push({
- stage: message.name,
- coin: message.coin,
- txid: message.txid,
- amount: message.amount,
- });
+ // Don't push duplicate messages
+ if (!swap.transactions.find(tx => tx.stage === message.name)) {
+ swap.transactions.push({
+ stage: message.name,
+ coin: message.coin,
+ txid: message.txid,
+ amount: message.amount,
+ });
+ }
}
if (message.method === 'tradestatus' && message.status === 'finished') {
swap.status = 'completed';
swap.progress = 1;
- swap.transactions.push({
- stage: 'alicespend',
- coin: message.bob,
- txid: message.paymentspent,
- amount: message.srcamount,
- });
+ // Nuke transaction history and rebuild it from this message.
+ // This will normally result in the same transaction array but
+ // if we were offline and missed some messages this will allow us
+ // to reconstruct what happened.
+ //
+ // It also allows us to correctly rebuild the tx chain of swaps that
+ // didn't quite go to plan like claiming bobdeposit or alicepayment.
+ const amounts = (() => {
+ const [alicespend, bobspend, bobpayment, alicepayment, bobdeposit, otherfee, myfee, bobrefund, bobreclaim, alicereclaim, aliceclaim] = message.values;
+ return {alicespend, bobspend, bobpayment, alicepayment, bobdeposit, otherfee, myfee, bobrefund, bobreclaim, alicereclaim, aliceclaim};
+ })();
+
+ swap.transactions = [];
+ if (message.sentflags.includes('myfee')) {
+ swap.transactions.push({
+ stage: 'myfee',
+ coin: message.alice,
+ txid: message.alicedexfee,
+ amount: amounts.myfee,
+ });
+ }
+ if (message.sentflags.includes('bobdeposit')) {
+ swap.transactions.push({
+ stage: 'bobdeposit',
+ coin: message.bob,
+ txid: message.bobdeposit,
+ amount: amounts.bobdeposit,
+ });
+ }
+ if (message.sentflags.includes('alicepayment')) {
+ swap.transactions.push({
+ stage: 'alicepayment',
+ coin: message.alice,
+ txid: message.alicepayment,
+ amount: amounts.alicepayment,
+ });
+ }
+ if (message.sentflags.includes('bobpayment')) {
+ swap.transactions.push({
+ stage: 'bobpayment',
+ coin: message.bob,
+ txid: message.bobpayment,
+ amount: amounts.bobpayment,
+ });
+ }
- const executedBaseCurrencyAmount = isBuyOrder ? message.srcamount : message.destamount;
- const executedQuoteCurrencyAmount = isBuyOrder ? message.destamount : message.srcamount;
+ // This is the final tx claiming bobpayment for a trade that completed as expected.
+ if (message.sentflags.includes('alicespend')) {
+ swap.transactions.push({
+ stage: 'alicespend',
+ coin: message.bob,
+ txid: message.paymentspent,
+ amount: amounts.alicespend,
+ });
+ }
+
+ // This is the final tx in the case that bob doesn't send bobpayment.
+ // We can claim bobdeposit which gives us a 12.5% bonus to punish bob.
+ if (message.sentflags.includes('aliceclaim')) {
+ // There is a bug in marketmaker where it doesn't always correctly report
+ // the values. If aliceclaim is 0 we fallback to bobdeposit as it should
+ // be very close (same amount minus our claim txfee)
+ // https://github.com/jl777/SuperNET/issues/920
+ swap.transactions.push({
+ stage: 'aliceclaim',
+ coin: message.bob,
+ txid: message.depositspent,
+ amount: amounts.aliceclaim || amounts.bobdeposit,
+ });
+ }
+
+ // This is the final tx in the case that bob doesn't send anything after
+ // we've already sent alicepayment. We don't get any of the currency we
+ // want, just claim our original payment back.
+ if (message.sentflags.includes('alicereclaim')) {
+ swap.transactions.push({
+ stage: 'alicereclaim',
+ coin: message.alice,
+ txid: message.Apaymentspent,
+ amount: amounts.alicereclaim,
+ });
+ }
+
+ // Overrride status to failed if we don't have a successful completion transaction
+ if (!(
+ message.sentflags.includes('alicespend') ||
+ message.sentflags.includes('aliceclaim')
+ )) {
+ swap.status = 'failed';
+ }
+
+ const startTx = swap.transactions.find(tx => tx.stage === 'alicepayment');
+ const startAmount = startTx ? startTx.amount : 0;
+ const endTx = swap.transactions.find(tx => ['alicespend', 'aliceclaim'].includes(tx.stage));
+ const endAmount = endTx ? endTx.amount : 0;
+ const executedBaseCurrencyAmount = isBuyOrder ? endAmount : startAmount;
+ const executedQuoteCurrencyAmount = isBuyOrder ? startAmount : endAmount;
swap.baseCurrencyAmount = roundTo(executedBaseCurrencyAmount, 8);
swap.quoteCurrencyAmount = roundTo(executedQuoteCurrencyAmount, 8);
- swap.price = roundTo(executedQuoteCurrencyAmount / executedBaseCurrencyAmount, 8);
swap.executed.baseCurrencyAmount = swap.baseCurrencyAmount;
swap.executed.quoteCurrencyAmount = swap.quoteCurrencyAmount;
- swap.executed.price = swap.price;
- swap.executed.percentCheaperThanRequested = roundTo(100 - ((swap.executed.price / swap.requested.price) * 100), 2);
- if (!isBuyOrder) {
- swap.executed.percentCheaperThanRequested = -swap.executed.percentCheaperThanRequested;
+
+ if (endAmount > 0 && startAmount > 0) {
+ swap.price = roundTo(executedQuoteCurrencyAmount / executedBaseCurrencyAmount, 8);
+ swap.executed.price = swap.price;
+ swap.executed.percentCheaperThanRequested = roundTo(100 - ((swap.executed.price / swap.requested.price) * 100), 2);
+ if (!isBuyOrder) {
+ swap.executed.percentCheaperThanRequested = -swap.executed.percentCheaperThanRequested;
+ }
}
}
@@ -218,8 +322,16 @@ class SwapDB {
swap.statusFormatted = `swap ${swapProgress}/${swapTransactions.length}`;
swap.progress = (swapProgress + MATCHED_STEP) / TOTAL_PROGRESS_STEPS;
- } else if (swap.status === 'failed' && (swap.error.code === -9999 || timedOut)) {
- swap.statusFormatted = t('status.unmatched').toLowerCase();
+ }
+
+ if (swap.status === 'failed') {
+ if (swap.error.code === -9999 || timedOut) {
+ swap.statusFormatted = t('status.unmatched').toLowerCase();
+ }
+ if (swap.transactions.find(tx => tx.stage === 'alicereclaim')) {
+ swap.statusFormatted = t('status.reverted').toLowerCase();
+ swap.statusInformation = t('statusInformation.reverted');
+ }
}
return swap;
diff --git a/app/renderer/views/Exchange/Order.js b/app/renderer/views/Exchange/Order.js
index 624a3cf0f4..0a53e67efd 100644
--- a/app/renderer/views/Exchange/Order.js
+++ b/app/renderer/views/Exchange/Order.js
@@ -141,7 +141,6 @@ class Bottom extends React.Component {
const swap = result.pending;
const {swapDB} = appContainer;
- api.subscribeToSwap(swap.uuid).on('progress', swapDB.updateSwapData);
await swapDB.insertSwapData(swap, requestOpts);
exchangeContainer.setIsSendingOrder(false);
};