Skip to content

Commit d32dfb6

Browse files
committed
feat(swaps): UNKNOWN_PAYMENT_ERROR code
This adds a swap error to be used when a payment call fails but we are uncertain about the status of the payment - the payment may have gone through or may eventually go through.
1 parent 93ba501 commit d32dfb6

File tree

7 files changed

+84
-57
lines changed

7 files changed

+84
-57
lines changed

Diff for: lib/lndclient/LndClient.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -540,20 +540,28 @@ class LndClient extends SwapClient {
540540
private executeSendRequest = async (
541541
request: lndrpc.SendRequest,
542542
): Promise<string> => {
543+
if (!this.isConnected()) {
544+
throw swapErrors.FINAL_PAYMENT_ERROR(errors.UNAVAILABLE(this.currency, this.status).message);
545+
}
546+
543547
this.logger.trace(`sending payment with ${JSON.stringify(request.toObject())}`);
544548
let sendPaymentResponse: lndrpc.SendResponse;
545549
try {
546550
sendPaymentResponse = await this.sendPaymentSync(request);
547551
} catch (err) {
548552
this.logger.error('got exception from sendPaymentSync', err);
549-
throw swapErrors.PAYMENT_ERROR(err.message);
553+
if (typeof err.message === 'string' && err.message.includes('chain backend is still syncing')) {
554+
throw swapErrors.FINAL_PAYMENT_ERROR(err.message);
555+
} else {
556+
throw swapErrors.UNKNOWN_PAYMENT_ERROR(err.message);
557+
}
550558
}
551559
const paymentError = sendPaymentResponse.getPaymentError();
552560
if (paymentError) {
553561
if (paymentError.includes('UnknownPaymentHash') || paymentError.includes('IncorrectOrUnknownPaymentDetails')) {
554562
throw swapErrors.PAYMENT_REJECTED;
555563
} else {
556-
throw swapErrors.PAYMENT_ERROR(paymentError);
564+
throw swapErrors.FINAL_PAYMENT_ERROR(paymentError);
557565
}
558566
}
559567
return base64ToHex(sendPaymentResponse.getPaymentPreimage_asB64());

Diff for: lib/raidenclient/RaidenClient.ts

+25-19
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,13 @@ import http from 'http';
33
import { SwapClientType, SwapRole, SwapState } from '../constants/enums';
44
import { CurrencyInstance } from '../db/types';
55
import Logger from '../Logger';
6+
import swapErrors from '../swaps/errors';
67
import SwapClient, { ChannelBalance, ClientStatus, PaymentState, WalletBalance } from '../swaps/SwapClient';
78
import { SwapDeal } from '../swaps/types';
89
import { UnitConverter } from '../utils/UnitConverter';
9-
import errors from './errors';
10-
import {
11-
Channel,
12-
OpenChannelPayload,
13-
PaymentEvent,
14-
RaidenChannelCount,
15-
RaidenClientConfig,
16-
RaidenInfo, RaidenVersion,
17-
TokenPaymentRequest,
18-
TokenPaymentResponse,
19-
} from './types';
10+
import errors, { errorCodes } from './errors';
11+
import { Channel, OpenChannelPayload, PaymentEvent, RaidenChannelCount, RaidenClientConfig,
12+
RaidenInfo, RaidenVersion, TokenPaymentRequest, TokenPaymentResponse } from './types';
2013

2114
type RaidenErrorResponse = { errors: string };
2215

@@ -191,14 +184,27 @@ class RaidenClient extends SwapClient {
191184
if (!tokenAddress) {
192185
throw(errors.TOKEN_ADDRESS_NOT_FOUND);
193186
}
194-
const tokenPaymentResponse = await this.tokenPayment({
195-
amount,
196-
lock_timeout,
197-
token_address: tokenAddress,
198-
target_address: deal.destination!,
199-
secret_hash: deal.rHash,
200-
});
201-
return this.sanitizeTokenPaymentResponse(tokenPaymentResponse);
187+
try {
188+
const tokenPaymentResponse = await this.tokenPayment({
189+
amount,
190+
lock_timeout,
191+
token_address: tokenAddress,
192+
target_address: deal.destination!,
193+
secret_hash: deal.rHash,
194+
});
195+
return this.sanitizeTokenPaymentResponse(tokenPaymentResponse);
196+
} catch (err) {
197+
switch (err.code) {
198+
case 'ECONNRESET':
199+
case errorCodes.UNEXPECTED:
200+
case errorCodes.TIMEOUT:
201+
case errorCodes.SERVER_ERROR:
202+
case errorCodes.INVALID_TOKEN_PAYMENT_RESPONSE:
203+
throw swapErrors.UNKNOWN_PAYMENT_ERROR(err.message);
204+
default:
205+
throw swapErrors.FINAL_PAYMENT_ERROR(err.message);
206+
}
207+
}
202208
}
203209

204210
private sanitizeTokenPaymentResponse = (response: TokenPaymentResponse) => {

Diff for: lib/swaps/Swaps.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ class Swaps extends EventEmitter {
252252
private persistDeal = async (deal: SwapDeal) => {
253253
await this.repository.saveSwapDeal(deal);
254254
if (deal.state !== SwapState.Active) {
255-
this.removeDeal(deal);
255+
this.deals.delete(deal.rHash);
256256
}
257257
}
258258

@@ -275,10 +275,6 @@ class Swaps extends EventEmitter {
275275
this.logger.debug(`New deal: ${JSON.stringify(deal)}`);
276276
}
277277

278-
public removeDeal = (deal: SwapDeal) => {
279-
this.deals.delete(deal.rHash);
280-
}
281-
282278
/**
283279
* Checks if a swap for two given orders can be executed by ensuring both swap clients are active
284280
* and if there exists a route to the maker.
@@ -882,8 +878,19 @@ class Swaps extends EventEmitter {
882878
deal.rPreimage = await swapClient.sendPayment(deal);
883879
return deal.rPreimage;
884880
} catch (err) {
885-
this.failDeal(deal, SwapFailureReason.SendPaymentFailure, err.message);
886-
throw new Error(`Got exception from sendPaymentSync ${err.message}`);
881+
if (err.code === errorCodes.UNKNOWN_PAYMENT_ERROR) {
882+
// the payment failed but we are unsure of its final status, so we fail
883+
// the deal and assign the payment to be checked in swap recovery
884+
clearTimeout(this.timeouts.get(deal.rHash));
885+
this.timeouts.delete(deal.rHash);
886+
this.emit('swap.failed', deal);
887+
this.deals.delete(deal.rHash);
888+
const swapDealInstance = await this.repository.getSwapDeal(rHash);
889+
this.swapRecovery.pendingSwaps.add(swapDealInstance!);
890+
} else {
891+
this.failDeal(deal, SwapFailureReason.SendPaymentFailure, err.message);
892+
}
893+
throw err;
887894
}
888895
} else {
889896
// If we are here we are the taker

Diff for: lib/swaps/errors.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ const errorCodes = {
55
SWAP_CLIENT_NOT_FOUND: codesPrefix.concat('.1'),
66
SWAP_CLIENT_NOT_CONFIGURED: codesPrefix.concat('.2'),
77
PAYMENT_HASH_NOT_FOUND: codesPrefix.concat('.3'),
8-
PAYMENT_ERROR: codesPrefix.concat('.4'),
8+
FINAL_PAYMENT_ERROR: codesPrefix.concat('.4'),
99
PAYMENT_REJECTED: codesPrefix.concat('.5'),
1010
INVALID_RESOLVE_REQUEST: codesPrefix.concat('.6'),
1111
SWAP_CLIENT_WALLET_NOT_CREATED: codesPrefix.concat('.7'),
1212
SWAP_CLIENT_MISCONFIGURED: codesPrefix.concat('.8'),
13+
UNKNOWN_PAYMENT_ERROR: codesPrefix.concat('.9'),
1314
};
1415

1516
const errors = {
@@ -25,9 +26,10 @@ const errors = {
2526
message: `deal for rHash ${rHash} not found`,
2627
code: errorCodes.PAYMENT_HASH_NOT_FOUND,
2728
}),
28-
PAYMENT_ERROR: (message: string) => ({
29+
/** A payment error that indicates the payment has permanently failed. */
30+
FINAL_PAYMENT_ERROR: (message: string) => ({
2931
message,
30-
code: errorCodes.PAYMENT_ERROR,
32+
code: errorCodes.FINAL_PAYMENT_ERROR,
3133
}),
3234
PAYMENT_REJECTED: {
3335
message: 'the recipient rejected our payment for the swap',
@@ -45,6 +47,14 @@ const errors = {
4547
message: `the following swap clients are misconfigured: ${clientLabels.join(', ')}`,
4648
code: errorCodes.SWAP_CLIENT_MISCONFIGURED,
4749
}),
50+
/**
51+
* A payment error that indicates we are unsure of the current state of the
52+
* payment and it may have succeeded or may eventually succeed.
53+
*/
54+
UNKNOWN_PAYMENT_ERROR: (message: string) => ({
55+
message,
56+
code: errorCodes.UNKNOWN_PAYMENT_ERROR,
57+
}),
4858
};
4959

5060
export { errorCodes };

Diff for: test/jest/LndClient.spec.ts

+4-12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { LndClientConfig } from '../../lib/lndclient/types';
33
import Logger from '../../lib/Logger';
44
import { getValidDeal } from '../utils';
55
import { SwapRole } from '../../lib/constants/enums';
6+
import { ClientStatus } from '../../lib/swaps/SwapClient';
67

78
const getSendPaymentSyncResponse = () => {
89
return {
@@ -41,6 +42,9 @@ describe('LndClient', () => {
4142
logger.error = jest.fn();
4243
logger.info = jest.fn();
4344
logger.trace = jest.fn();
45+
46+
lnd = new LndClient({ config, currency, logger });
47+
lnd['status'] = ClientStatus.ConnectionVerified;
4448
});
4549

4650
afterEach(async () => {
@@ -60,7 +64,6 @@ describe('LndClient', () => {
6064

6165
test('it throws when connectPeer fails', async () => {
6266
expect.assertions(3);
63-
lnd = new LndClient({ config, currency, logger });
6467
lnd['connectPeer'] = jest.fn().mockImplementation(() => {
6568
throw new Error('connectPeer failed');
6669
});
@@ -80,7 +83,6 @@ describe('LndClient', () => {
8083

8184
test('it tries all 2 lnd uris when connectPeer to first one fails', async () => {
8285
expect.assertions(3);
83-
lnd = new LndClient({ config, currency, logger });
8486
lnd['openChannelSync'] = jest.fn().mockReturnValue(Promise.resolve());
8587
const connectPeerFail = () => {
8688
throw new Error('connectPeer failed');
@@ -104,7 +106,6 @@ describe('LndClient', () => {
104106

105107
test('it does succeed when connecting to already connected peer', async () => {
106108
expect.assertions(4);
107-
lnd = new LndClient({ config, currency, logger });
108109
lnd['openChannelSync'] = jest.fn().mockReturnValue(Promise.resolve());
109110
const alreadyConnected = () => {
110111
throw new Error('already connected');
@@ -127,7 +128,6 @@ describe('LndClient', () => {
127128
test('it throws when timeout reached', async () => {
128129
expect.assertions(3);
129130
jest.useFakeTimers();
130-
lnd = new LndClient({ config, currency, logger });
131131
lnd['openChannelSync'] = jest.fn().mockReturnValue(Promise.resolve());
132132
const timeOut = () => {
133133
jest.runAllTimers();
@@ -151,7 +151,6 @@ describe('LndClient', () => {
151151

152152
test('it stops trying to connect to lnd uris when first once succeeds', async () => {
153153
expect.assertions(3);
154-
lnd = new LndClient({ config, currency, logger });
155154
lnd['openChannelSync'] = jest.fn().mockReturnValue(Promise.resolve());
156155
lnd['connectPeer'] = jest.fn()
157156
.mockImplementationOnce(() => {
@@ -171,7 +170,6 @@ describe('LndClient', () => {
171170

172171
test('it throws when openchannel fails', async () => {
173172
expect.assertions(2);
174-
lnd = new LndClient({ config, currency, logger });
175173
lnd['connectPeer'] = jest.fn().mockReturnValue(Promise.resolve());
176174
lnd['openChannelSync'] = jest.fn().mockImplementation(() => {
177175
throw new Error('openChannelSync error');
@@ -193,7 +191,6 @@ describe('LndClient', () => {
193191
describe('sendPayment', () => {
194192

195193
test('it resolves upon maker success', async () => {
196-
lnd = new LndClient({ config, currency, logger });
197194
lnd['sendPaymentSync'] = jest.fn()
198195
.mockReturnValue(Promise.resolve(getSendPaymentSyncResponse()));
199196
const deal = getValidDeal();
@@ -210,7 +207,6 @@ describe('LndClient', () => {
210207
});
211208

212209
test('it resolves upon taker success', async () => {
213-
lnd = new LndClient({ config, currency, logger });
214210
lnd['sendPaymentSync'] = jest.fn()
215211
.mockReturnValue(Promise.resolve(getSendPaymentSyncResponse()));
216212
const deal = {
@@ -229,15 +225,13 @@ describe('LndClient', () => {
229225
});
230226

231227
test('it rejects upon sendPaymentSync error', async () => {
232-
lnd = new LndClient({ config, currency, logger });
233228
lnd['sendPaymentSync'] = jest.fn()
234229
.mockReturnValue(Promise.resolve(getSendPaymentSyncErrorResponse()));
235230
await expect(lnd.sendPayment(getValidDeal()))
236231
.rejects.toMatchSnapshot();
237232
});
238233

239234
test('it resolves upon sendSmallestAmount success', async () => {
240-
lnd = new LndClient({ config, currency, logger });
241235
lnd['sendPaymentSync'] = jest.fn()
242236
.mockReturnValue(Promise.resolve(getSendPaymentSyncResponse()));
243237
const buildSendRequestSpy = jest.spyOn(lnd as any, 'buildSendRequest');
@@ -252,7 +246,5 @@ describe('LndClient', () => {
252246
amount: 1,
253247
});
254248
});
255-
256249
});
257-
258250
});

Diff for: test/jest/RaidenClient.spec.ts

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
1+
import { CurrencyInstance } from '../../lib/db/types';
2+
import Logger from '../../lib/Logger';
3+
import errors from '../../lib/raidenclient/errors';
14
import RaidenClient from '../../lib/raidenclient/RaidenClient';
25
import { RaidenClientConfig, TokenPaymentResponse } from '../../lib/raidenclient/types';
3-
import Logger from '../../lib/Logger';
6+
import { errorCodes as swapErrorCodes } from '../../lib/swaps/errors';
7+
import { PaymentState } from '../../lib/swaps/SwapClient';
48
import { SwapDeal } from '../../lib/swaps/types';
59
import { UnitConverter } from '../../lib/utils/UnitConverter';
6-
import { CurrencyInstance } from '../../lib/db/types';
710
import { getValidDeal } from '../utils';
8-
import { PaymentState } from '../../lib/swaps/SwapClient';
911

12+
const token_address = '0x4c354C76d5f73A63a90Be776897DC81Fb6238772';
1013
const getValidTokenPaymentResponse = () => {
1114
return {
15+
token_address,
1216
amount: 100000000000000,
1317
initiator_address: '0x7ed0299Fa1ADA71D10536B866231D447cDFa48b9',
1418
secret_hash: '0xb8a0243672b503714822b454405de879e2b8300c7579d60295c35607ffd5613e',
1519
secret: '0x9f345e3751d8b7f38d34b7a3dd636a9d7a0c2f36d991615e6653501f30c6ec56',
1620
identifier: 3820989367401102,
1721
hashalgo: 'SHA256',
1822
target_address: '0x2B88992DEd5C96aa7Eaa9CFE1AE52350df7dc5DF',
19-
token_address: '0x4c354C76d5f73A63a90Be776897DC81Fb6238772',
2023
};
2124
};
2225

@@ -104,9 +107,8 @@ describe('RaidenClient', () => {
104107
.resolves.toMatchSnapshot();
105108
});
106109

107-
test('it rejects in case of empty secret response', async () => {
110+
test('it rejects with unknown error in case of empty secret response', async () => {
108111
raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger });
109-
await raiden.init(currencyInstances as CurrencyInstance[]);
110112
const invalidTokenPaymentResponse: TokenPaymentResponse = {
111113
...getValidTokenPaymentResponse(),
112114
secret: '',
@@ -116,7 +118,16 @@ describe('RaidenClient', () => {
116118
raiden.tokenAddresses.get = jest.fn().mockReturnValue(invalidTokenPaymentResponse.token_address);
117119
const deal: SwapDeal = getValidDeal();
118120
await expect(raiden.sendPayment(deal))
119-
.rejects.toMatchSnapshot();
121+
.rejects.toHaveProperty('code', swapErrorCodes.UNKNOWN_PAYMENT_ERROR);
122+
});
123+
124+
test('it rejects with final error in case of insufficient balance', async () => {
125+
raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger });
126+
raiden['tokenPayment'] = jest.fn().mockRejectedValue(errors.INSUFFICIENT_BALANCE);
127+
raiden.tokenAddresses.get = jest.fn().mockReturnValue(token_address);
128+
const deal: SwapDeal = getValidDeal();
129+
await expect(raiden.sendPayment(deal))
130+
.rejects.toHaveProperty('code', swapErrorCodes.FINAL_PAYMENT_ERROR);
120131
});
121132
});
122133

Diff for: test/jest/__snapshots__/RaidenClient.spec.ts.snap

-7
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,4 @@ Object {
99

1010
exports[`RaidenClient openChannel it throws when openChannel fails 1`] = `[Error: openChannelRequest error]`;
1111

12-
exports[`RaidenClient sendPayment it rejects in case of empty secret response 1`] = `
13-
Object {
14-
"code": "5.8",
15-
"message": "raiden TokenPaymentResponse is invalid",
16-
}
17-
`;
18-
1912
exports[`RaidenClient sendPayment it removes 0x from secret 1`] = `"9f345e3751d8b7f38d34b7a3dd636a9d7a0c2f36d991615e6653501f30c6ec56"`;

0 commit comments

Comments
 (0)