xud
Version:
Exchange Union Daemon
807 lines • 44 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const assert_1 = __importDefault(require("assert"));
const http_1 = __importDefault(require("http"));
const enums_1 = require("../constants/enums");
const errors_1 = __importDefault(require("../swaps/errors"));
const SwapClient_1 = __importStar(require("../swaps/SwapClient"));
const errors_2 = __importStar(require("./errors"));
const utils_1 = require("../utils/utils");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const solidity_1 = require("@ethersproject/solidity");
/**
* Waits for the preimage event from SwapClient for a specified hash
* @param client the swap client instance to listen events from
* @param expectedHash the expected hash of the payment
* @param errorTimeout optional maximum duration of time to wait for the preimage
*/
const waitForPreimageByHash = (client, expectedHash, errorTimeout = 89000) => {
// create an observable that emits values when a preimage
// event is triggered on the client
const preimage$ = rxjs_1.fromEvent(client, 'preimage');
const expectedPreimageReceived$ = preimage$.pipe(
// filter out events that do not match our expected hash
operators_1.filter(preimageEvent => preimageEvent.rHash === expectedHash),
// map the ProvidePreimageEvent object to a string as the consumer is
// only interested in the value of the preimage
operators_1.pluck('preimage'),
// complete the observable and clean up all the listeners
// once we receive 1 event that matches our conditions
operators_1.take(1),
// emit an error if the observable does not emit any values
// for the specified duration
operators_1.timeout(errorTimeout));
// convert the observable to a promise that resolves on complete
// and rejects on error
return expectedPreimageReceived$.toPromise();
};
/**
* A class representing a client to interact with connext.
*/
let ConnextClient = /** @class */ (() => {
class ConnextClient extends SwapClient_1.default {
/**
* Creates a connext client.
*/
constructor({ config, logger, unitConverter, currencyInstances, }) {
super(logger, config.disable);
this.type = enums_1.SwapClientType.Connext;
this.finalLock = 200;
/** A map of currency symbols to token addresses. */
this.tokenAddresses = new Map();
/**
* A map of expected invoices by hash.
* This is equivalent to invoices of lnd with the difference
* being that we're managing the state of invoice on xud level.
*/
this.expectedIncomingTransfers = new Map();
/** The set of hashes for outgoing transfers. */
this.outgoingTransferHashes = new Set();
/** A map of currencies to promises representing balance requests. */
this.getBalancePromises = new Map();
this.outboundAmounts = new Map();
this.inboundAmounts = new Map();
this.pendingRequests = new Set();
this.criticalRequestPaths = ['/hashlock-resolve', '/hashlock-transfer'];
this.initSpecific = () => __awaiter(this, void 0, void 0, function* () {
this.on('transferReceived', this.onTransferReceived.bind(this));
});
this.onTransferReceived = (transferReceivedRequest) => {
const { tokenAddress, units, timelock, rHash, paymentId, } = transferReceivedRequest;
if (this.outgoingTransferHashes.has(rHash)) {
this.outgoingTransferHashes.delete(rHash);
this.logger.debug(`outgoing hash lock transfer with rHash ${rHash} created`);
return;
}
const expectedIncomingTransfer = this.expectedIncomingTransfers.get(rHash);
if (!expectedIncomingTransfer) {
this.logger.warn(`received unexpected incoming transfer created event with rHash ${rHash}, units: ${units}, timelock ${timelock}, token address ${tokenAddress}, and paymentId ${paymentId}`);
return;
}
const { units: expectedUnits, expiry: expectedTimelock, tokenAddress: expectedTokenAddress, } = expectedIncomingTransfer;
const currency = this.getCurrencyByTokenaddress(tokenAddress);
if (tokenAddress === expectedTokenAddress &&
units === expectedUnits &&
timelock === expectedTimelock) {
expectedIncomingTransfer.paymentId = paymentId;
this.logger.debug(`accepting incoming transfer with rHash: ${rHash}, units: ${units}, timelock ${timelock}, currency ${currency}, and paymentId ${paymentId}`);
this.expectedIncomingTransfers.delete(rHash);
this.emit('htlcAccepted', rHash, units, currency);
}
else {
if (tokenAddress !== expectedTokenAddress) {
this.logger.warn(`incoming transfer for rHash ${rHash} with token address ${tokenAddress} does not match expected ${expectedTokenAddress}`);
}
if (units !== expectedUnits) {
this.logger.warn(`incoming transfer for rHash ${rHash} with value ${units} does not match expected ${expectedUnits}`);
}
if (timelock !== expectedTimelock) {
this.logger.warn(`incoming transfer for rHash ${rHash} with time lock ${timelock} does not match expected ${expectedTimelock}`);
}
}
};
// TODO: Ideally, this would be set in the constructor.
// Related issue: https://github.com/ExchangeUnion/xud/issues/1494
this.setSeed = (seed) => {
this.seed = seed;
};
/**
* Initiates wallet for the Connext client
*/
this.initWallet = (seedMnemonic) => __awaiter(this, void 0, void 0, function* () {
const res = yield this.sendRequest('/mnemonic', 'POST', { mnemonic: seedMnemonic });
return yield utils_1.parseResponseBody(res);
});
this.initConnextClient = (seedMnemonic) => __awaiter(this, void 0, void 0, function* () {
const res = yield this.sendRequest('/connect', 'POST', { mnemonic: seedMnemonic });
return yield utils_1.parseResponseBody(res);
});
this.subscribeDeposit = () => __awaiter(this, void 0, void 0, function* () {
yield this.sendRequest('/subscribe', 'POST', {
event: 'DEPOSIT_CONFIRMED_EVENT',
webhook: `http://${this.webhookhost}:${this.webhookport}/deposit-confirmed`,
});
});
this.subscribePreimage = () => __awaiter(this, void 0, void 0, function* () {
yield this.sendRequest('/subscribe', 'POST', {
event: 'CONDITIONAL_TRANSFER_UNLOCKED_EVENT',
webhook: `http://${this.webhookhost}:${this.webhookport}/preimage`,
});
});
this.subscribeIncomingTransfer = () => __awaiter(this, void 0, void 0, function* () {
yield this.sendRequest('/subscribe', 'POST', {
event: 'CONDITIONAL_TRANSFER_CREATED_EVENT',
webhook: `http://${this.webhookhost}:${this.webhookport}/incoming-transfer`,
});
});
/**
* Associate connext with currencies that have a token address
*/
this.setTokenAddresses = (currencyInstances) => {
currencyInstances.forEach((currency) => {
if (currency.tokenAddress && currency.swapClient === enums_1.SwapClientType.Connext) {
this.tokenAddresses.set(currency.id, currency.tokenAddress);
}
});
};
/**
* Checks whether we have a pending collateral request for the currency and,
* if one doesn't exist, starts a new request for the specified amount. Then
* calls channelBalance to refresh the inbound capacity for the currency.
*/
this.requestCollateralInBackground = (_currency, _units) => {
this.logger.info('did not request collateral because this xud version is deprecated');
};
/**
* Checks whether there is sufficient inbound capacity to receive the specified amount
* and throws an error if there isn't, otherwise does nothing.
*/
this.checkInboundCapacity = (inboundAmount, currency) => {
var _a;
const inboundCapacity = this.inboundAmounts.get(currency) || 0;
if (inboundCapacity < inboundAmount) {
// we do not have enough inbound capacity to receive the specified inbound amount so we must request collateral
this.logger.debug(`collateral of ${inboundCapacity} for ${currency} is insufficient for order amount ${inboundAmount}`);
// we want to make a request for the current collateral plus the greater of any
// minimum request size for the currency or the capacity shortage + 5% buffer
const quantityToRequest = inboundCapacity + Math.max(inboundAmount * 1.05 - inboundCapacity, (_a = ConnextClient.MIN_COLLATERAL_REQUEST_SIZES[currency]) !== null && _a !== void 0 ? _a : 0);
const unitsToRequest = this.unitConverter.amountToUnits({ currency, amount: quantityToRequest });
this.requestCollateralInBackground(currency, unitsToRequest);
throw errors_2.default.INSUFFICIENT_COLLATERAL;
}
};
this.setReservedInboundAmount = (reservedInboundAmount, currency) => {
var _a;
const inboundCapacity = this.inboundAmounts.get(currency) || 0;
if (inboundCapacity < reservedInboundAmount) {
// we do not have enough inbound capacity to fill all open orders, so we will request more
this.logger.debug(`collateral of ${inboundCapacity} for ${currency} is insufficient for reserved order amount of ${reservedInboundAmount}`);
// we want to make a request for the current collateral plus the greater of any
// minimum request size for the currency or the capacity shortage + 3% buffer
const quantityToRequest = inboundCapacity + Math.max(reservedInboundAmount * 1.03 - inboundCapacity, (_a = ConnextClient.MIN_COLLATERAL_REQUEST_SIZES[currency]) !== null && _a !== void 0 ? _a : 0);
const unitsToRequest = this.unitConverter.amountToUnits({ currency, amount: quantityToRequest });
// we don't await this request - instead we allow for "lazy collateralization" to complete since
// we don't expect all orders to be filled at once, we can be patient
this.requestCollateralInBackground(currency, unitsToRequest);
}
};
this.updateCapacity = () => __awaiter(this, void 0, void 0, function* () {
try {
const channelBalancePromises = [];
for (const [currency] of this.tokenAddresses) {
channelBalancePromises.push(this.channelBalance(currency));
}
yield Promise.all(channelBalancePromises);
}
catch (e) {
this.logger.error('failed to update total outbound capacity', e);
}
});
this.verifyConnection = () => __awaiter(this, void 0, void 0, function* () {
this.logger.info('trying to verify connection to connext');
try {
if (!this.seed) {
throw errors_2.default.MISSING_SEED;
}
yield this.sendRequest('/health', 'GET');
yield this.initWallet(this.seed);
const config = yield this.initConnextClient(this.seed);
yield Promise.all([
this.subscribePreimage(),
this.subscribeIncomingTransfer(),
this.subscribeDeposit(),
]);
this.userIdentifier = config.userIdentifier;
this.emit('connectionVerified', {
newIdentifier: this.userIdentifier,
});
this.setStatus(SwapClient_1.ClientStatus.ConnectionVerified);
}
catch (err) {
this.logger.error(`could not verify connection to connext, retrying in ${ConnextClient.RECONNECT_INTERVAL} ms`, err);
yield this.disconnect();
}
});
this.sendSmallestAmount = (rHash, destination, currency) => __awaiter(this, void 0, void 0, function* () {
const tokenAddress = this.getTokenAddress(currency);
const secret = yield this.executeHashLockTransfer({
amount: '1',
assetId: tokenAddress,
lockHash: rHash,
timelock: this.finalLock.toString(),
recipient: destination,
});
return secret;
});
this.sendPayment = (deal) => __awaiter(this, void 0, void 0, function* () {
assert_1.default(deal.state === enums_1.SwapState.Active);
assert_1.default(deal.destination);
let amount;
let tokenAddress;
let lockTimeout;
try {
let secret;
if (deal.role === enums_1.SwapRole.Maker) {
// we are the maker paying the taker
amount = deal.takerUnits.toLocaleString('fullwide', { useGrouping: false });
tokenAddress = this.tokenAddresses.get(deal.takerCurrency);
const executeTransfer = this.executeHashLockTransfer({
amount,
assetId: tokenAddress,
timelock: deal.takerCltvDelta.toString(),
lockHash: `0x${deal.rHash}`,
recipient: deal.destination,
});
// @ts-ignore
const [executeTransferResponse, preimage] = yield Promise.all([
executeTransfer,
waitForPreimageByHash(this, deal.rHash),
]);
this.logger.debug(`received preimage ${preimage} for payment with hash ${deal.rHash}`);
secret = preimage;
}
else {
// we are the taker paying the maker
amount = deal.makerUnits.toLocaleString('fullwide', { useGrouping: false });
tokenAddress = this.tokenAddresses.get(deal.makerCurrency);
lockTimeout = deal.makerCltvDelta;
secret = deal.rPreimage;
const executeTransfer = this.executeHashLockTransfer({
amount,
assetId: tokenAddress,
timelock: lockTimeout.toString(),
lockHash: `0x${deal.rHash}`,
recipient: deal.destination,
});
yield executeTransfer;
}
return secret;
}
catch (err) {
switch (err.code) {
case 'ECONNRESET':
case errors_2.errorCodes.UNEXPECTED:
case errors_2.errorCodes.TIMEOUT:
case errors_2.errorCodes.SERVER_ERROR:
case errors_2.errorCodes.INVALID_TOKEN_PAYMENT_RESPONSE:
default:
throw errors_1.default.UNKNOWN_PAYMENT_ERROR(err.message);
}
}
});
this.addInvoice = ({ rHash: expectedHash, units: expectedUnits, expiry: expectedTimelock, currency: expectedCurrency }) => __awaiter(this, void 0, void 0, function* () {
if (!expectedCurrency) {
throw errors_2.default.CURRENCY_MISSING;
}
if (!expectedTimelock) {
throw errors_2.default.EXPIRY_MISSING;
}
const expectedTokenAddress = this.getTokenAddress(expectedCurrency);
const expectedIncomingTransfer = {
rHash: expectedHash,
units: expectedUnits,
expiry: expectedTimelock,
tokenAddress: expectedTokenAddress,
};
this.expectedIncomingTransfers.set(expectedHash, expectedIncomingTransfer);
});
/**
* Resolves a HashLock Transfer on the Connext network.
*/
this.settleInvoice = (rHash, rPreimage, currency) => __awaiter(this, void 0, void 0, function* () {
this.logger.debug(`settling ${currency} invoice for ${rHash} with preimage ${rPreimage}`);
const assetId = this.getTokenAddress(currency);
yield this.sendRequest('/hashlock-resolve', 'POST', {
assetId,
preImage: `0x${rPreimage}`,
});
});
this.removeInvoice = (rHash) => __awaiter(this, void 0, void 0, function* () {
const expectedIncomingTransfer = this.expectedIncomingTransfers.get(rHash);
if (expectedIncomingTransfer) {
const { paymentId } = expectedIncomingTransfer;
if (paymentId) {
// resolve a hashlock with a paymentId but no preimage to cancel it
yield this.sendRequest('/hashlock-resolve', 'POST', {
paymentId,
assetId: expectedIncomingTransfer.tokenAddress,
});
this.logger.debug(`canceled incoming transfer with rHash ${rHash}`);
}
else {
this.logger.warn(`could not find paymentId for incoming transfer with hash ${rHash}`);
}
this.expectedIncomingTransfers.delete(rHash);
}
else {
this.logger.warn(`could not find expected incoming transfer with hash ${rHash}`);
}
});
this.lookupPayment = (rHash, currency) => __awaiter(this, void 0, void 0, function* () {
var _a;
try {
const assetId = this.getTokenAddress(currency);
const transferStatusResponse = yield this.getHashLockStatus(rHash, assetId);
this.logger.trace(`hashlock status for connext transfer with hash ${rHash} is ${transferStatusResponse.status}`);
switch (transferStatusResponse.status) {
case 'PENDING':
return { state: SwapClient_1.PaymentState.Pending };
case 'COMPLETED':
return {
state: SwapClient_1.PaymentState.Succeeded,
preimage: (_a = transferStatusResponse.preImage) === null || _a === void 0 ? void 0 : _a.slice(2),
};
case 'EXPIRED':
const expiredTransferUnlocked$ = rxjs_1.defer(() => rxjs_1.from(
// when the connext transfer (HTLC) expires the funds are not automatically returned to the channel balance
// in order to unlock the funds we'll need to call /hashlock-resolve with the paymentId
this.sendRequest('/hashlock-resolve', 'POST', {
assetId,
// providing a placeholder preImage for rest-api-client because it's a required field
preImage: '0x',
paymentId: solidity_1.sha256(['address', 'bytes32'], [assetId, `0x${rHash}`]),
}))).pipe(operators_1.catchError((e, caught) => {
const RETRY_INTERVAL = 30000;
this.logger.error(`failed to unlock an expired connext transfer with rHash: ${rHash} - retrying in ${RETRY_INTERVAL}ms`, e);
return rxjs_1.timer(RETRY_INTERVAL).pipe(operators_1.mergeMapTo(caught));
}), operators_1.take(1));
expiredTransferUnlocked$.subscribe({
complete: () => {
this.logger.debug(`successfully unlocked an expired connext transfer with rHash: ${rHash}`);
},
});
return { state: SwapClient_1.PaymentState.Failed };
case 'FAILED':
return { state: SwapClient_1.PaymentState.Failed };
default:
this.logger.debug(`no hashlock status for connext transfer with hash ${rHash}: ${JSON.stringify(transferStatusResponse)} - attempting to reject app install proposal`);
try {
yield this.sendRequest('/reject-install', 'POST', {
appIdentityHash: transferStatusResponse.senderAppIdentityHash,
});
this.logger.debug(`connext transfer proposal with hash ${rHash} successfully rejected - transfer state is now failed`);
return { state: SwapClient_1.PaymentState.Failed };
}
catch (e) {
// in case of error we're still consider the payment as pending
this.logger.error('failed to reject connext app install proposal', e);
return { state: SwapClient_1.PaymentState.Pending };
}
}
}
catch (err) {
if (err.code === errors_2.errorCodes.PAYMENT_NOT_FOUND) {
return { state: SwapClient_1.PaymentState.Failed };
}
this.logger.error(`could not lookup connext transfer for ${rHash}`, err);
return { state: SwapClient_1.PaymentState.Pending }; // return pending if we hit an error
}
});
this.getRoute = () => __awaiter(this, void 0, void 0, function* () {
/** A placeholder route value that assumes a fixed lock time of 100 for Connext. */
return {
getTotalTimeLock: () => 101,
};
});
this.canRouteToNode = () => __awaiter(this, void 0, void 0, function* () {
return true;
});
this.getHeight = () => __awaiter(this, void 0, void 0, function* () {
return 1; // connext's API does not tell us the height
});
this.getInfo = () => __awaiter(this, void 0, void 0, function* () {
let address;
let version;
let status = errors_2.default.CONNEXT_CLIENT_NOT_INITIALIZED.message;
if (this.isDisabled()) {
status = errors_2.default.CONNEXT_IS_DISABLED.message;
}
else {
try {
const getInfo$ = rxjs_1.combineLatest(rxjs_1.from(this.getVersion()), rxjs_1.from(this.getClientConfig())).pipe(
// error if no response within 5000 ms
operators_1.timeout(5000),
// complete the stream when we receive 1 value
operators_1.take(1));
const [streamVersion, clientConfig] = yield getInfo$.toPromise();
status = 'Ready';
version = streamVersion;
address = clientConfig.signerAddress;
}
catch (err) {
status = err.message;
}
}
return { status, address, version };
});
/**
* Gets the connext version.
*/
this.getVersion = () => __awaiter(this, void 0, void 0, function* () {
const res = yield this.sendRequest('/version', 'GET');
const { version } = yield utils_1.parseResponseBody(res);
return version;
});
/**
* Gets the configuration of Connext client.
*/
this.getClientConfig = () => __awaiter(this, void 0, void 0, function* () {
const res = yield this.sendRequest('/config', 'GET');
const clientConfig = yield utils_1.parseResponseBody(res);
return clientConfig;
});
this.channelBalance = (currency) => __awaiter(this, void 0, void 0, function* () {
if (!currency) {
return { balance: 0, pendingOpenBalance: 0, inactiveBalance: 0 };
}
const { freeBalanceOffChain, nodeFreeBalanceOffChain } = yield this.getBalance(currency);
const freeBalanceAmount = this.unitConverter.unitsToAmount({
currency,
units: Number(freeBalanceOffChain),
});
const nodeFreeBalanceAmount = this.unitConverter.unitsToAmount({
currency,
units: Number(nodeFreeBalanceOffChain),
});
this.outboundAmounts.set(currency, freeBalanceAmount);
if (nodeFreeBalanceAmount !== this.inboundAmounts.get(currency)) {
this.inboundAmounts.set(currency, nodeFreeBalanceAmount);
this.logger.debug(`new inbound capacity (collateral) for ${currency} of ${nodeFreeBalanceAmount}`);
}
return {
balance: freeBalanceAmount,
inactiveBalance: 0,
pendingOpenBalance: 0,
};
});
this.swapCapacities = (currency) => __awaiter(this, void 0, void 0, function* () {
var _b, _c;
yield this.channelBalance(currency); // refreshes the balances
const outboundAmount = (_b = this.outboundAmounts.get(currency)) !== null && _b !== void 0 ? _b : 0;
const inboundAmount = (_c = this.inboundAmounts.get(currency)) !== null && _c !== void 0 ? _c : 0;
return {
maxOutboundChannelCapacity: outboundAmount,
maxInboundChannelCapacity: inboundAmount,
totalOutboundCapacity: outboundAmount,
totalInboundCapacity: inboundAmount,
};
});
/**
* Returns the balances available in wallet for a specified currency.
*/
this.walletBalance = (currency) => __awaiter(this, void 0, void 0, function* () {
if (!currency) {
return {
totalBalance: 0,
confirmedBalance: 0,
unconfirmedBalance: 0,
};
}
const { freeBalanceOnChain } = yield this.getBalance(currency);
const confirmedBalanceAmount = this.unitConverter.unitsToAmount({
currency,
units: Number(freeBalanceOnChain),
});
return {
totalBalance: confirmedBalanceAmount,
confirmedBalance: confirmedBalanceAmount,
unconfirmedBalance: 0,
};
});
this.getBalance = (currency) => {
// check if we already have a balance request that we are waiting a response for
// it's not helpful to have simultaneous requests for the current balance, as they
// should return the same info.
let getBalancePromise = this.getBalancePromises.get(currency);
if (!getBalancePromise) {
// if not make a new balance request and store the promise that's waiting for a response
const tokenAddress = this.getTokenAddress(currency);
getBalancePromise = this.sendRequest(`/balance/${tokenAddress}`, 'GET').then((res) => {
return utils_1.parseResponseBody(res);
}).finally(() => {
this.getBalancePromises.delete(currency); // clear the stored promise
});
this.getBalancePromises.set(currency, getBalancePromise);
}
return getBalancePromise;
};
this.deposit = () => __awaiter(this, void 0, void 0, function* () {
const clientConfig = yield this.getClientConfig();
return clientConfig.signerAddress;
});
this.openChannel = ({ currency, units }) => __awaiter(this, void 0, void 0, function* () {
if (!currency) {
throw errors_2.default.CURRENCY_MISSING;
}
const assetId = this.getTokenAddress(currency);
const depositResponse = yield this.sendRequest('/deposit', 'POST', {
assetId,
amount: units.toLocaleString('fullwide', { useGrouping: false }),
});
const { txhash } = yield utils_1.parseResponseBody(depositResponse);
const minCollateralRequestQuantity = ConnextClient.MIN_COLLATERAL_REQUEST_SIZES[currency];
if (minCollateralRequestQuantity !== undefined) {
const minCollateralRequestUnits = this.unitConverter.amountToUnits({ currency, amount: minCollateralRequestQuantity });
const depositConfirmed$ = rxjs_1.fromEvent(this, 'depositConfirmed').pipe(operators_1.filter(hash => hash === txhash), // only proceed if the incoming hash matches our expected txhash
operators_1.take(1), // complete the stream after 1 matching event
operators_1.timeout(86400000));
depositConfirmed$.subscribe({
complete: () => {
this.requestCollateralInBackground(currency, minCollateralRequestUnits);
},
});
}
return txhash;
});
this.closeChannel = ({ units, currency, destination }) => __awaiter(this, void 0, void 0, function* () {
if (!currency) {
throw errors_2.default.CURRENCY_MISSING;
}
const { freeBalanceOffChain } = yield this.getBalance(currency);
const availableUnits = Number(freeBalanceOffChain);
if (units && availableUnits < units) {
throw errors_2.default.INSUFFICIENT_BALANCE;
}
const amount = units || freeBalanceOffChain;
if (Number(amount) === 0) {
return []; // there is nothing to withdraw and no tx to return
}
const withdrawResponse = yield this.sendRequest('/withdraw', 'POST', {
recipient: destination,
amount: amount.toLocaleString('fullwide', { useGrouping: false }),
assetId: this.tokenAddresses.get(currency),
});
const { txhash } = yield utils_1.parseResponseBody(withdrawResponse);
return [txhash];
});
/**
* Create a HashLock Transfer on the Connext network.
* @param targetAddress recipient of the payment
* @param tokenAddress contract address of the token
* @param amount
* @param lockHash
*/
this.executeHashLockTransfer = (payload) => __awaiter(this, void 0, void 0, function* () {
this.logger.debug(`sending payment of ${payload.amount} with hash ${payload.lockHash} to ${payload.recipient}`);
this.outgoingTransferHashes.add(payload.lockHash);
const res = yield this.sendRequest('/hashlock-transfer', 'POST', payload);
const { appId } = yield utils_1.parseResponseBody(res);
return appId;
});
/**
* Deposits more of a token to an existing client.
* @param multisigAddress the address of the client to deposit to
* @param balance the amount to deposit to the client
*/
this.depositToChannel = (assetId, amount) => __awaiter(this, void 0, void 0, function* () {
yield this.sendRequest('/hashlock-transfer', 'POST', {
assetId,
amount: amount.toLocaleString('fullwide', { useGrouping: false }),
});
});
this.withdraw = ({ all, currency, amount: argAmount, destination, fee, }) => __awaiter(this, void 0, void 0, function* () {
if (fee) {
// TODO: allow overwriting gas price
throw Error('setting fee for Ethereum withdrawals is not supported yet');
}
let units = '';
const { freeBalanceOnChain } = yield this.getBalance(currency);
if (all) {
if (currency === 'ETH') {
// TODO: query Ether balance, subtract gas price times 21000 (gas usage of transferring Ether), and set that as amount
throw new Error('withdrawing all ETH is not supported yet');
}
units = freeBalanceOnChain;
}
else if (argAmount) {
const argUnits = this.unitConverter.amountToUnits({
currency,
amount: argAmount,
});
if (Number(freeBalanceOnChain) < argUnits) {
throw errors_2.default.INSUFFICIENT_BALANCE;
}
units = argUnits.toString();
}
const res = yield this.sendRequest('/onchain-transfer', 'POST', {
assetId: this.getTokenAddress(currency),
amount: units,
recipient: destination,
});
const { txhash } = yield utils_1.parseResponseBody(res);
return txhash;
});
/** Connext client specific cleanup. */
this.disconnect = () => __awaiter(this, void 0, void 0, function* () {
this.setStatus(SwapClient_1.ClientStatus.Disconnected);
for (const req of this.pendingRequests) {
if (this.criticalRequestPaths.includes(req.path)) {
this.logger.warn(`critical request is pending: ${req.path}`);
continue;
}
this.logger.info(`aborting pending request: ${req.path}`);
req.destroy();
}
});
/**
* Sends a request to the Connext REST API.
* @param endpoint the URL endpoint
* @param method an HTTP request method
* @param payload the request payload
*/
this.sendRequest = (endpoint, method, payload) => {
return new Promise((resolve, reject) => {
const options = {
method,
hostname: this.host,
port: this.port,
path: `${endpoint}`,
};
let payloadStr;
if (payload) {
payloadStr = JSON.stringify(payload);
options.headers = {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payloadStr),
};
}
this.logger.trace(`sending request to ${endpoint}${payloadStr ? `: ${payloadStr}` : ''}`);
let req;
req = http_1.default.request(options, (res) => __awaiter(this, void 0, void 0, function* () {
this.pendingRequests.delete(req);
let err;
let body;
switch (res.statusCode) {
case 200:
case 201:
case 204:
resolve(res);
break;
case 400:
body = yield utils_1.parseResponseBody(res);
this.logger.error(`400 status error: ${JSON.stringify(body)}`);
reject(body);
break;
case 402:
err = errors_2.default.INSUFFICIENT_BALANCE;
break;
case 404:
err = errors_2.default.PAYMENT_NOT_FOUND;
break;
case 408:
err = errors_2.default.TIMEOUT;
break;
case 409:
body = yield utils_1.parseResponseBody(res);
this.logger.error(`409 status error: ${JSON.stringify(body)}`);
reject(body);
break;
case 500:
err = errors_2.default.SERVER_ERROR(res.statusCode, res.statusMessage);
break;
default:
err = errors_2.default.UNEXPECTED(res.statusCode, res.statusMessage);
break;
}
if (err) {
this.logger.error(err.message);
reject(err);
}
}));
req.on('error', (err) => __awaiter(this, void 0, void 0, function* () {
this.pendingRequests.delete(req);
if (err.code === 'ECONNREFUSED') {
yield this.disconnect();
}
this.logger.error(err);
reject(err);
}));
if (payloadStr) {
req.write(payloadStr);
}
req.end();
this.pendingRequests.add(req);
});
};
this.port = config.port;
this.host = config.host;
this.webhookhost = config.webhookhost;
this.webhookport = config.webhookport;
this.unitConverter = unitConverter;
this.setTokenAddresses(currencyInstances);
}
get minutesPerBlock() {
return 0.25; // 15 seconds per block target
}
get label() {
return 'Connext';
}
getTokenAddress(currency) {
const tokenAdress = this.tokenAddresses.get(currency);
if (!tokenAdress) {
throw errors_2.default.TOKEN_ADDRESS_NOT_FOUND;
}
return tokenAdress;
}
getCurrencyByTokenaddress(tokenAddress) {
let currency;
for (const [key, value] of this.tokenAddresses.entries()) {
if (value === tokenAddress) {
currency = key;
}
}
if (!currency) {
throw errors_2.default.CURRENCY_NOT_FOUND_BY_TOKENADDRESS(tokenAddress);
}
return currency;
}
getHashLockStatus(lockHash, assetId) {
return __awaiter(this, void 0, void 0, function* () {
const res = yield this.sendRequest(`/hashlock-status/0x${lockHash}/${assetId}`, 'GET');
const transferStatusResponse = yield utils_1.parseResponseBody(res);
return transferStatusResponse;
});
}
}
/** The minimum incremental quantity that we may use for collateral requests. */
ConnextClient.MIN_COLLATERAL_REQUEST_SIZES = {
ETH: 0.1 * 10 ** 8,
USDT: 100 * 10 ** 8,
DAI: 100 * 10 ** 8,
};
return ConnextClient;
})();
exports.default = ConnextClient;
//# sourceMappingURL=ConnextClient.js.map