UNPKG

xud

Version:
807 lines 44 kB
"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