Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically fix stuck swaps #416

Merged
merged 29 commits into from
Jul 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1e7ceee
Listen for swap updates for all UUIDs in our history
lukechilds Jun 26, 2018
5eaedd9
Remove api.subscribeToSwap(uuid)
lukechilds Jun 26, 2018
7518fc4
⚠️⚠️⚠️ REVERT BEFORE MERGING ⚠️⚠️⚠️ Log swap messages
lukechilds Jun 26, 2018
514f91b
Expose requestId/quoteId on swap objects
lukechilds Jul 3, 2018
2e89499
Force kickstart stuck swaps on startup and every hour
lukechilds Jul 4, 2018
6514951
Kickstart all pending swaps on initial launch
lukechilds Jul 4, 2018
e06d60f
Don't save kickstart response
lukechilds Jul 4, 2018
f7357a1
Remove uneeded imports
lukechilds Jul 4, 2018
51a0b6e
Disable kickstarting while we test some stuff
lukechilds Jul 5, 2018
fc1caab
Reconstruct transaction history on swap completion
lukechilds Jul 5, 2018
c0c3d0c
Include txs based on sentflags not the amounts array
lukechilds Jul 6, 2018
977c0e1
Handle bug where marketmaker doesn't set aliceclaim amount
lukechilds Jul 6, 2018
45b5b49
Use real tx values for calculating the executed amounts
lukechilds Jul 6, 2018
1a95811
Don't store duplicate swap entries in the transaction array
lukechilds Jul 6, 2018
e009031
Revert "Disable kickstarting while we test some stuff"
lukechilds Jul 6, 2018
3834ded
Reduce kickstart interval from one hour to fifteen minutes.
lukechilds Jul 6, 2018
c28635d
Use Array.map().includes() over Array.some(check) for readability
lukechilds Jul 6, 2018
ede5cb1
Fix lint error
lukechilds Jul 6, 2018
2994519
Add comment to remind me to set myfee txid when marketmaker is fixed
lukechilds Jul 6, 2018
0e55952
Show aliceclaim and alicereclaim transactions in swap modal
lukechilds Jul 9, 2018
d88d39d
Use Array.find() over Array.map().includes() for readability
lukechilds Jul 9, 2018
a9d0b39
Set formatted status for reverted swaps
lukechilds Jul 9, 2018
b202d58
Add more information for reverted swaps
lukechilds Jul 9, 2018
86ed5de
Fix lint error
lukechilds Jul 9, 2018
4f7883d
Set swap status as failed if we don't get a successful completion tx
lukechilds Jul 9, 2018
c391432
Add refactor TODO comment
lukechilds Jul 12, 2018
ad3c676
Use new fireEvery() syntax
lukechilds Jul 13, 2018
f02cdcc
Get myfee TXID from completed message
lukechilds Jul 13, 2018
169a150
Revert "⚠️⚠️⚠️ REVERT BEFORE MERGING ⚠️⚠️⚠️ Log swap messages"
lukechilds Jul 13, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/locales/en-US/swap.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"timedOut": "Swap timed out",
"title": "Swap",
"details": {
"aliceclaim": "Alice Claim",
"alicereclaim": "Alice Reclaim",
"alicepayment": "Alice Payment",
"alicespend": "Alice Spend",
"bobdeposit": "Bob Deposit",
Expand Down Expand Up @@ -35,7 +37,11 @@
"completed": "Completed",
"failed": "Failed",
"matched": "Matched",
"reverted": "Reverted",
"pending": "Pending",
"unmatched": "Unmatched"
},
"statusInformation": {
"reverted": "The swap was reverted due to connectivity issues."
}
}
21 changes: 11 additions & 10 deletions app/renderer/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,17 @@ export default class Api {
return getCurrency(opts.symbol).etomic ? this._withdrawEth(opts) : this._withdrawBtcFork(opts);
}

kickstart(opts) {
ow(opts.requestId, ow.number.positive.finite.label('requestId'));
ow(opts.quoteId, ow.number.positive.finite.label('quoteId'));

return this.request({
method: 'kickstart',
requestid: opts.requestId,
quoteid: opts.quoteId,
});
}

listUnspent(coin, address) {
ow(coin, symbolPredicate.label('coin'));
ow(address, ow.string.label('address'));
Expand All @@ -376,14 +387,4 @@ export default class Api {
this.queue.pause();
this.queue.clear();
}

subscribeToSwap(uuid) {
ow(uuid, uuidPredicate.label('uuid'));

if (!this.socket) {
throw new Error('Swap subscriptions require the socket to be enabled');
}

return this.socket.subscribeToSwap(uuid);
}
}
54 changes: 28 additions & 26 deletions app/renderer/components/SwapDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,33 +52,32 @@ class SwapDetails extends React.Component {
const {swap} = this.props;
const {baseCurrency, quoteCurrency} = swap;

let hasTransactions = false;
const transactions = swapTransactions.map(stage => {
const tx = swap.transactions.find(tx => tx.stage === stage);

if (!tx) {
return (
<React.Fragment key={stage}>
<div className="arrow">→</div>
<div className="item">
<h6>{t(`details.${stage}`)}</h6>
</div>
</React.Fragment>
);
}
const transactions = swap.transactions.map(tx => (
<React.Fragment key={tx.stage}>
<div className="arrow completed">→</div>
<div className="item completed" title={tx.txid}>
<h6>{t(`details.${tx.stage}`)}</h6>
<p>{tx.amount}<br/>{tx.coin}</p>
</div>
</React.Fragment>
));

hasTransactions = true;
if (swap.status === 'swapping') {
swapTransactions.forEach(stage => {
const tx = swap.transactions.find(tx => tx.stage === stage);

return (
<React.Fragment key={stage}>
<div className="arrow completed">→</div>
<div className="item completed" title={tx.txid}>
<h6>{t(`details.${stage}`)}</h6>
<p>{tx.amount}<br/>{tx.coin}</p>
</div>
</React.Fragment>
);
});
if (!tx) {
transactions.push(
<React.Fragment key={stage}>
<div className="arrow">→</div>
<div className="item">
<h6>{t(`details.${stage}`)}</h6>
</div>
</React.Fragment>
);
}
});
}

const prices = ['requested', 'broadcast', 'executed'].map(value => {
if (!swap[value].price) {
Expand Down Expand Up @@ -136,6 +135,9 @@ class SwapDetails extends React.Component {
</React.Fragment>
)}
</p>
{swap.statusInformation && (
<p>{swap.statusInformation}</p>
)}
</div>
<div className="section details">
<div className="offer-wrapper">
Expand All @@ -151,7 +153,7 @@ class SwapDetails extends React.Component {
</p>
)}
</div>
{hasTransactions && (
{(transactions.length > 0) && (
<React.Fragment>
<h4>{t('details.transactions')}</h4>
<div className="transactions">
Expand Down
33 changes: 31 additions & 2 deletions app/renderer/containers/Exchange.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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() {
Expand All @@ -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()});
};
Expand Down
48 changes: 0 additions & 48 deletions app/renderer/marketmaker-socket.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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;
Loading