diff --git a/docs/api.md b/docs/api.md index 40231ec6b..f25e71baf 100644 --- a/docs/api.md +++ b/docs/api.md @@ -101,9 +101,11 @@ - [OrderSide](#xudrpc.OrderSide) - [Role](#xudrpc.Role) + - [Xud](#xudrpc.Xud) - [XudInit](#xudrpc.XudInit) + - [Scalar Value Types](#scalar-value-types) @@ -1609,6 +1611,7 @@ The primary service for interacting with a running xud node. | CloseChannel | [CloseChannelRequest](#xudrpc.CloseChannelRequest) | [CloseChannelResponse](#xudrpc.CloseChannelResponse) | Closes any existing payment channels with a peer for the specified currency. shell: xucli closechannel <currency> [node_identifier ] [--force] | | Connect | [ConnectRequest](#xudrpc.ConnectRequest) | [ConnectResponse](#xudrpc.ConnectResponse) | Attempts to connect to a node. Once connected, the node is added to the list of peers and becomes available for swaps and trading. A handshake exchanges information about the peer's supported trading and swap clients. Orders will be shared with the peer upon connection and upon new order placements. shell: xucli connect <node_uri> | | WalletDeposit | [DepositRequest](#xudrpc.DepositRequest) | [DepositResponse](#xudrpc.DepositResponse) | Gets an address to deposit a given currency into the xud wallets. shell: xucli walletdeposit <currency> | +| Deposit | [DepositRequest](#xudrpc.DepositRequest) | [DepositResponse](#xudrpc.DepositResponse) | Gets an address to deposit a given currency directly into a channel. shell: xucli deposit <currency> | | DiscoverNodes | [DiscoverNodesRequest](#xudrpc.DiscoverNodesRequest) | [DiscoverNodesResponse](#xudrpc.DiscoverNodesResponse) | Discover nodes from a specific peer and apply new connections | | GetBalance | [GetBalanceRequest](#xudrpc.GetBalanceRequest) | [GetBalanceResponse](#xudrpc.GetBalanceResponse) | Gets the total balance available across all payment channels and wallets for one or all currencies. shell: xucli getbalance [currency] | | GetInfo | [GetInfoRequest](#xudrpc.GetInfoRequest) | [GetInfoResponse](#xudrpc.GetInfoResponse) | Gets general information about this node. shell: xucli getinfo | diff --git a/lib/Xud.ts b/lib/Xud.ts index 490e252c0..3e360c1c2 100644 --- a/lib/Xud.ts +++ b/lib/Xud.ts @@ -245,19 +245,7 @@ class Xud extends EventEmitter { // minimum channelBalance threshold minChannelAmount: 100000000, }, - { - currency: 'USDT', - channelAmount: 100000000000, - minChannelAmount: 100000000, - }, - { - currency: 'DAI', - channelAmount: 150000000000, - minChannelAmount: 100000000, - }, ], - // we check the channel and on-chain balance every 10 seconds - // and refund from faucet if below the walletAmount retryInterval: 10000, }).subscribe({ next: (currency) => { diff --git a/lib/cli/commands/closechannel.ts b/lib/cli/commands/closechannel.ts index 2bb335475..92c0f591a 100644 --- a/lib/cli/commands/closechannel.ts +++ b/lib/cli/commands/closechannel.ts @@ -37,7 +37,7 @@ export const builder = (argv: Argv) => argv .example('$0 closechannel BTC CheeseMonkey', 'close BTC channels by alias') .example('$0 closechannel BTC CheeseMonkey --force', 'force close BTC channels by alias') .example('$0 closechannel BTC CheeseMonkey --fee 25', 'close BTC channels by alias with 25 sat/byte fee') - .example('$0 closechannel ETH --amount 0.1', 'remove 0.1 ETH from a Connext channel'); + .example('$0 closechannel ETH --amount 0.1 --destination 0x7d3447e35c73903C971761AF3DBa76cDB1Cd07e2', 'remove 0.1 ETH from a Connext channel'); export const handler = async (argv: Arguments) => { const request = new CloseChannelRequest(); diff --git a/lib/cli/commands/deposit.ts b/lib/cli/commands/deposit.ts new file mode 100644 index 000000000..d1bb0d176 --- /dev/null +++ b/lib/cli/commands/deposit.ts @@ -0,0 +1,28 @@ +import { Arguments, Argv } from 'yargs'; +import { DepositRequest, DepositResponse } from '../../proto/xudrpc_pb'; +import { callback, loadXudClient } from '../command'; + +export const command = 'deposit '; + +export const describe = 'gets an address to deposit funds to a channel'; + +export const builder = (argv: Argv) => argv + .positional('currency', { + description: 'the ticker symbol of the currency to deposit.', + type: 'string', + }) + .example('$0 deposit ETH', 'get a ETH deposit address'); + +const openChannelText = (depositAddressResponse: DepositResponse.AsObject) => { + console.log(` +You will receive your deposit in the connext channel. + +Your deposit address is: ${depositAddressResponse.address} +`); +}; + +export const handler = async (argv: Arguments) => { + const request = new DepositRequest(); + request.setCurrency(argv.currency.toUpperCase()); + (await loadXudClient(argv)).deposit(request, callback(argv, openChannelText)); +}; diff --git a/lib/connextclient/ConnextClient.ts b/lib/connextclient/ConnextClient.ts index d6ac809d3..2e26de15c 100644 --- a/lib/connextclient/ConnextClient.ts +++ b/lib/connextclient/ConnextClient.ts @@ -14,47 +14,109 @@ import SwapClient, { PaymentStatus, WithdrawArguments, } from '../swaps/SwapClient'; -import { SwapDeal, CloseChannelParams, OpenChannelParams, SwapCapacities } from '../swaps/types'; +import { + SwapDeal, + CloseChannelParams, + OpenChannelParams, + SwapCapacities, +} from '../swaps/types'; import { UnitConverter } from '../utils/UnitConverter'; import errors, { errorCodes } from './errors'; import { ConnextErrorResponse, - ConnextInitWalletResponse, + ConnextConfig, ConnextConfigResponse, ConnextBalanceResponse, + ConnextChannelResponse, + ConnextChannelBalanceResponse, ConnextTransferResponse, ConnextClientConfig, ConnextInfo, - ConnextVersion, - TokenPaymentRequest, - ConnextTransferStatus, + ConnextTransferRequest, + ConnextTransfer, ExpectedIncomingTransfer, ProvidePreimageEvent, TransferReceivedEvent, - ConnextDepositResponse, ConnextWithdrawResponse, OnchainTransferResponse, + ConnextBlockNumberResponse, + ConnextChannelDetails, + GetBlockByNumberResponse, } from './types'; import { parseResponseBody } from '../utils/utils'; -import { Observable, fromEvent, from, combineLatest, defer, timer } from 'rxjs'; -import { take, pluck, timeout, filter, catchError, mergeMapTo } from 'rxjs/operators'; +import { + Observable, + fromEvent, + from, + defer, + Subscription, + throwError, + interval, + timer, + combineLatest, + of, +} from 'rxjs'; +import { + take, + pluck, + timeout, + filter, + mergeMap, + catchError, + mergeMapTo, +} from 'rxjs/operators'; import { sha256 } from '@ethersproject/solidity'; interface ConnextClient { - on(event: 'preimage', listener: (preimageRequest: ProvidePreimageEvent) => void): void; - on(event: 'transferReceived', listener: (transferReceivedRequest: TransferReceivedEvent) => void): void; - on(event: 'htlcAccepted', listener: (rHash: string, amount: number, currency: string) => void): this; - on(event: 'connectionVerified', listener: (swapClientInfo: SwapClientInfo) => void): this; + on( + event: 'preimage', + listener: (preimageRequest: ProvidePreimageEvent) => void, + ): void; + on( + event: 'transferReceived', + listener: (transferReceivedRequest: TransferReceivedEvent) => void, + ): void; + on( + event: 'htlcAccepted', + listener: (rHash: string, amount: number, currency: string) => void, + ): this; + on( + event: 'connectionVerified', + listener: (swapClientInfo: SwapClientInfo) => void, + ): this; on(event: 'depositConfirmed', listener: (hash: string) => void): this; once(event: 'initialized', listener: () => void): this; - emit(event: 'htlcAccepted', rHash: string, amount: number, currency: string): boolean; + emit( + event: 'htlcAccepted', + rHash: string, + amount: number, + currency: string, + ): boolean; emit(event: 'connectionVerified', swapClientInfo: SwapClientInfo): boolean; emit(event: 'initialized'): boolean; emit(event: 'preimage', preimageRequest: ProvidePreimageEvent): void; - emit(event: 'transferReceived', transferReceivedRequest: TransferReceivedEvent): void; + emit( + event: 'transferReceived', + transferReceivedRequest: TransferReceivedEvent, + ): void; emit(event: 'depositConfirmed', hash: string): void; } +const getRouterNodeIdentifier = (network: string): string => { + switch (network) { + case 'regtest': + case 'simnet': + // public key of our simnet router node + return 'vector7Qp3Jj6bMkJbtWZWTqF7EnfJ6UQigt1e5ZWC5VwKYhN7Bg4ZyA'; + case 'testnet': + throw new Error('testnet router node has not been created, yet'); + case 'mainnet': + throw new Error('mainnet router node has not been created, yet'); + default: + throw errors.NOT_FOUND; + } +}; + /** * Waits for the preimage event from SwapClient for a specified hash * @param client the swap client instance to listen events from @@ -68,7 +130,10 @@ const waitForPreimageByHash = ( ): Promise => { // create an observable that emits values when a preimage // event is triggered on the client - const preimage$: Observable = fromEvent(client, 'preimage'); + const preimage$: Observable = fromEvent( + client, + 'preimage', + ); const expectedPreimageReceived$ = preimage$.pipe( // filter out events that do not match our expected hash filter(preimageEvent => preimageEvent.rHash === expectedHash), @@ -87,22 +152,55 @@ const waitForPreimageByHash = ( return expectedPreimageReceived$.toPromise(); }; +const transferReceived = ( + client: SwapClient, + expectedTransfer: ExpectedIncomingTransfer, +): Observable<[ExpectedIncomingTransfer, TransferReceivedEvent]> => { + const transferReceived$: Observable = fromEvent( + client, + 'transferReceived', + ); + return combineLatest(of(expectedTransfer), transferReceived$).pipe( + // filter out events that do not match our expected hash + filter( + ([expectedTransfer, receivedTransfer]) => + expectedTransfer.rHash === receivedTransfer.rHash, + ), + take(1), + ); +}; + +const CHAIN_IDENTIFIERS: { [key: string]: number } = { + regtest: 1337, + simnet: 1337, + // Rinkeby + testnet: 4, + mainnet: 1, +}; + +const AVERAGE_ETH_BLOCK_TIME_SECONDS = 15; +const ONE_SECOND_IN_MS = 1000; + /** * A class representing a client to interact with connext. */ class ConnextClient extends SwapClient { + public get minutesPerBlock() { + return 0.25; // 15 seconds per block target + } + + public get label() { + return 'Connext'; + } public readonly type = SwapClientType.Connext; public readonly finalLock = 200; public address?: string; /** A map of currency symbols to token addresses. */ public tokenAddresses = new Map(); - public userIdentifier?: string; - /** - * 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. - */ - private expectedIncomingTransfers = new Map(); + /** Public identifier for Connext */ + public publicIdentifier: string | undefined; + /** On-chain deposit address */ + public signerAddress: string | undefined; /** The set of hashes for outgoing transfers. */ private outgoingTransferHashes = new Set(); private port: number; @@ -110,19 +208,27 @@ class ConnextClient extends SwapClient { private webhookport: number; private webhookhost: string; private unitConverter: UnitConverter; + private network: string; private seed: string | undefined; + private readonly CHANNEL_ON_CHAIN_DISPUTE_TIMEOUT = '36000'; /** A map of currencies to promises representing balance requests. */ - private getBalancePromises = new Map>(); + private getBalancePromises = new Map>(); /** A map of currencies to promises representing collateral requests. */ private requestCollateralPromises = new Map>(); private outboundAmounts = new Map(); private inboundAmounts = new Map(); + private _reconcileDepositSubscriber: Subscription | undefined; + + /** Channel multisig address */ + private channelAddress: string | undefined; private pendingRequests = new Set(); private criticalRequestPaths = ['/hashlock-resolve', '/hashlock-transfer']; /** The minimum incremental quantity that we may use for collateral requests. */ - private static MIN_COLLATERAL_REQUEST_SIZES: { [key: string]: number | undefined } = { + private static MIN_COLLATERAL_REQUEST_SIZES: { + [key: string]: number | undefined; + } = { ETH: 0.1 * 10 ** 8, USDT: 100 * 10 ** 8, DAI: 100 * 10 ** 8, @@ -136,11 +242,13 @@ class ConnextClient extends SwapClient { logger, unitConverter, currencyInstances, + network, }: { unitConverter: UnitConverter; config: ConnextClientConfig; - currencyInstances: CurrencyInstance[], + currencyInstances: CurrencyInstance[]; logger: Logger; + network: string; }) { super(logger, config.disable); @@ -150,67 +258,10 @@ class ConnextClient extends SwapClient { this.webhookport = config.webhookport; this.unitConverter = unitConverter; this.setTokenAddresses(currencyInstances); + this.network = network; } - public get minutesPerBlock() { - return 0.25; // 15 seconds per block target - } - - public get label() { - return 'Connext'; - } - - public initSpecific = async () => { - this.on('transferReceived', this.onTransferReceived.bind(this)); - } - - private onTransferReceived = (transferReceivedRequest: TransferReceivedEvent) => { - 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}`); - } - } - } + public initSpecific = async () => {}; // TODO: Ideally, this would be set in the constructor. // Related issue: https://github.com/ExchangeUnion/xud/issues/1494 @@ -218,37 +269,28 @@ class ConnextClient extends SwapClient { this.seed = seed; } - /** - * Initiates wallet for the Connext client - */ - public initWallet = async (seedMnemonic: string) => { - const res = await this.sendRequest('/mnemonic', 'POST', { mnemonic: seedMnemonic }); - return await parseResponseBody(res); - } - public initConnextClient = async (seedMnemonic: string) => { - const res = await this.sendRequest('/connect', 'POST', { mnemonic: seedMnemonic }); + const res = await this.sendRequest('/connect', 'POST', { + mnemonic: seedMnemonic, + }); return await parseResponseBody(res); } - private subscribeDeposit = async () => { - await this.sendRequest('/subscribe', 'POST', { - event: 'DEPOSIT_CONFIRMED_EVENT', - webhook: `http://${this.webhookhost}:${this.webhookport}/deposit-confirmed`, + private subscribeIncomingTransfer = async () => { + await this.sendRequest('/event/subscribe', 'POST', { + publicIdentifier: this.publicIdentifier, + events: { + CONDITIONAL_TRANSFER_CREATED: `http://${this.webhookhost}:${this.webhookport}/incoming-transfer`, + }, }); } private subscribePreimage = async () => { - await this.sendRequest('/subscribe', 'POST', { - event: 'CONDITIONAL_TRANSFER_UNLOCKED_EVENT', - webhook: `http://${this.webhookhost}:${this.webhookport}/preimage`, - }); - } - - private subscribeIncomingTransfer = async () => { - await this.sendRequest('/subscribe', 'POST', { - event: 'CONDITIONAL_TRANSFER_CREATED_EVENT', - webhook: `http://${this.webhookhost}:${this.webhookport}/incoming-transfer`, + await this.sendRequest('/event/subscribe', 'POST', { + publicIdentifier: this.publicIdentifier, + events: { + CONDITIONAL_TRANSFER_RESOLVED: `http://${this.webhookhost}:${this.webhookport}/preimage`, + }, }); } @@ -257,7 +299,10 @@ class ConnextClient extends SwapClient { */ private setTokenAddresses = (currencyInstances: CurrencyInstance[]) => { currencyInstances.forEach((currency) => { - if (currency.tokenAddress && currency.swapClient === SwapClientType.Connext) { + if ( + currency.tokenAddress && + currency.swapClient === SwapClientType.Connext + ) { this.tokenAddresses.set(currency.id, currency.tokenAddress); } }); @@ -271,17 +316,25 @@ class ConnextClient extends SwapClient { private requestCollateralInBackground = (currency: string, units: number) => { // first check whether we already have a pending collateral request for this currency // if not start a new request, and when it completes call channelBalance to refresh our inbound capacity - const requestCollateralPromise = this.requestCollateralPromises.get(currency) ?? this.sendRequest('/request-collateral', 'POST', { - assetId: this.tokenAddresses.get(currency), - amount: units.toLocaleString('fullwide', { useGrouping: false }), - }).then(() => { - this.requestCollateralPromises.delete(currency); - this.logger.debug(`completed collateral request of ${units} ${currency} units`); - return this.channelBalance(currency); - }).catch((err) => { - this.requestCollateralPromises.delete(currency); - this.logger.error(err); - }); + const requestCollateralPromise = + this.requestCollateralPromises.get(currency) ?? + this.sendRequest('/request-collateral', 'POST', { + channelAddress: this.channelAddress, + assetId: this.tokenAddresses.get(currency), + amount: units.toLocaleString('fullwide', { useGrouping: false }), + publicIdentifier: this.publicIdentifier, + }) + .then(() => { + this.requestCollateralPromises.delete(currency); + this.logger.debug( + `completed collateral request of ${units} ${currency} units`, + ); + return this.channelBalance(currency); + }) + .catch((err) => { + this.requestCollateralPromises.delete(currency); + this.logger.error(err); + }); this.requestCollateralPromises.set(currency, requestCollateralPromise); } @@ -293,34 +346,51 @@ class ConnextClient extends SwapClient { 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}`); + 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, - ConnextClient.MIN_COLLATERAL_REQUEST_SIZES[currency] ?? 0, - ); - const unitsToRequest = this.unitConverter.amountToUnits({ currency, amount: quantityToRequest }); + const quantityToRequest = + inboundCapacity + + Math.max( + inboundAmount * 1.05 - inboundCapacity, + ConnextClient.MIN_COLLATERAL_REQUEST_SIZES[currency] ?? 0, + ); + const unitsToRequest = this.unitConverter.amountToUnits({ + currency, + amount: quantityToRequest, + }); this.requestCollateralInBackground(currency, unitsToRequest); throw errors.INSUFFICIENT_COLLATERAL; } } - public setReservedInboundAmount = (reservedInboundAmount: number, currency: string) => { + public setReservedInboundAmount = ( + reservedInboundAmount: number, + currency: string, + ) => { 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}`); + 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, - ConnextClient.MIN_COLLATERAL_REQUEST_SIZES[currency] ?? 0, - ); - const unitsToRequest = this.unitConverter.amountToUnits({ currency, amount: quantityToRequest }); + const quantityToRequest = + inboundCapacity + + Math.max( + reservedInboundAmount * 1.03 - inboundCapacity, + ConnextClient.MIN_COLLATERAL_REQUEST_SIZES[currency] ?? 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 @@ -343,7 +413,7 @@ class ConnextClient extends SwapClient { protected getTokenAddress(currency: string): string { const tokenAdress = this.tokenAddresses.get(currency); if (!tokenAdress) { - throw errors.TOKEN_ADDRESS_NOT_FOUND; + throw errors.NOT_FOUND; } return tokenAdress; } @@ -367,19 +437,20 @@ class ConnextClient extends SwapClient { if (!this.seed) { throw errors.MISSING_SEED; } - await this.sendRequest('/health', 'GET'); - await this.initWallet(this.seed); - const config = await this.initConnextClient(this.seed); - await Promise.all([ - this.subscribePreimage(), - this.subscribeIncomingTransfer(), - this.subscribeDeposit(), - ]); - this.userIdentifier = config.userIdentifier; + await this.createNode(this.seed); + const config = await this.getClientConfig(); + const { publicIdentifier, signerAddress } = config; + this.publicIdentifier = publicIdentifier; + this.signerAddress = signerAddress; + const channel = await this.getChannel(); + this.channelAddress = channel; + await this.subscribeIncomingTransfer(); + await this.subscribePreimage(); this.emit('connectionVerified', { - newIdentifier: this.userIdentifier, + newIdentifier: publicIdentifier, }); this.setStatus(ClientStatus.ConnectionVerified); + this.reconcileDeposit(); } catch (err) { this.logger.error( `could not verify connection to connext, retrying in ${ConnextClient.RECONNECT_INTERVAL} ms`, @@ -389,61 +460,127 @@ class ConnextClient extends SwapClient { } } - public sendSmallestAmount = async ( - rHash: string, - destination: string, - currency: string, - ) => { - const tokenAddress = this.getTokenAddress(currency); + private reconcileDeposit = () => { + if (this._reconcileDepositSubscriber) { + this._reconcileDepositSubscriber.unsubscribe(); + } + // TODO: increase interval after testing to 30+ sec + this._reconcileDepositSubscriber = interval(5000) + .pipe( + mergeMap(() => { + if (this.status === ClientStatus.ConnectionVerified) { + return defer(() => { + // TODO: reconcile deposit for all supported currencies + return from( + this.sendRequest('/deposit', 'POST', { + channelAddress: this.channelAddress, + publicIdentifier: this.publicIdentifier, + assetId: '0x0000000000000000000000000000000000000000', + }), + ); + }); + } + return throwError( + 'stopping deposit calls because client is no longer connected', + ); + }), + ) + .subscribe({ + next: () => { + this.logger.trace('deposit successfully reconciled'); + }, + error: (e) => { + this.logger.trace( + `stopped deposit calls because: ${JSON.stringify(e)}`, + ); + }, + }); + } - const secret = await this.executeHashLockTransfer({ - amount: '1', - assetId: tokenAddress, - lockHash: rHash, - timelock: this.finalLock.toString(), - recipient: destination, - }); - return secret; + public sendSmallestAmount = async () => { + throw new Error('Connext.sendSmallestAmount is not implemented'); + } + + private getExpiry = async (locktime: number): Promise => { + const blockHeight = await this.getHeight(); + const currentBlock = await this.getBlockByNumber(blockHeight); + const currentTimeStamp = parseInt(currentBlock.result.timestamp, 16); + const locktimeMilliseconds = + AVERAGE_ETH_BLOCK_TIME_SECONDS * locktime * ONE_SECOND_IN_MS; + const expiry = currentTimeStamp + locktimeMilliseconds; + return expiry.toString(); + } + + private deriveRoutingId = (lockHash: string, assetId: string): string => { + return sha256(['address', 'bytes32'], [assetId, `0x${lockHash}`]); } public sendPayment = async (deal: SwapDeal): Promise => { assert(deal.state === SwapState.Active); assert(deal.destination); + assert(this.channelAddress, 'cannot send transfer without channel address'); + assert(this.publicIdentifier, 'cannot send transfer with channel address'); let amount: string; let tokenAddress: string; - let lockTimeout: number | undefined; try { let secret; if (deal.role === SwapRole.Maker) { // we are the maker paying the taker - amount = deal.takerUnits.toLocaleString('fullwide', { useGrouping: false }); + amount = deal.takerUnits.toLocaleString('fullwide', { + useGrouping: false, + }); tokenAddress = this.tokenAddresses.get(deal.takerCurrency)!; + const expiry = await this.getExpiry(this.finalLock); const executeTransfer = this.executeHashLockTransfer({ amount, + type: 'HashlockTransfer', assetId: tokenAddress, - timelock: deal.takerCltvDelta.toString(), - lockHash: `0x${deal.rHash}`, + details: { + expiry, + lockHash: `0x${deal.rHash}`, + }, recipient: deal.destination, + meta: { + routingId: this.deriveRoutingId(deal.rHash, tokenAddress), + }, + channelAddress: this.channelAddress, + publicIdentifier: this.publicIdentifier, }); // @ts-ignore const [executeTransferResponse, preimage] = await Promise.all([ executeTransfer, waitForPreimageByHash(this, deal.rHash), ]); - this.logger.debug(`received preimage ${preimage} for payment with hash ${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 }); + amount = deal.makerUnits.toLocaleString('fullwide', { + useGrouping: false, + }); tokenAddress = this.tokenAddresses.get(deal.makerCurrency)!; - lockTimeout = deal.makerCltvDelta!; secret = deal.rPreimage!; + assert( + deal.makerCltvDelta, + 'cannot send transfer without deal.makerCltvDelta', + ); + const expiry = await this.getExpiry(deal.makerCltvDelta); const executeTransfer = this.executeHashLockTransfer({ amount, + type: 'HashlockTransfer', assetId: tokenAddress, - timelock: lockTimeout.toString(), - lockHash: `0x${deal.rHash}`, + details: { + expiry, + lockHash: `0x${deal.rHash}`, + }, recipient: deal.destination, + meta: { + routingId: this.deriveRoutingId(deal.rHash, tokenAddress), + }, + channelAddress: this.channelAddress, + publicIdentifier: this.publicIdentifier, }); await executeTransfer; } @@ -461,10 +598,17 @@ class ConnextClient extends SwapClient { } } - public addInvoice = async ( - { rHash: expectedHash, units: expectedUnits, expiry: expectedTimelock, currency: expectedCurrency }: - { rHash: string, units: number, expiry?: number, currency?: string }, - ) => { + public addInvoice = async ({ + rHash: expectedHash, + units: expectedUnits, + expiry: expectedTimelock, + currency: expectedCurrency, + }: { + rHash: string; + units: number; + expiry?: number; + currency?: string; + }) => { if (!expectedCurrency) { throw errors.CURRENCY_MISSING; } @@ -477,104 +621,240 @@ class ConnextClient extends SwapClient { units: expectedUnits, expiry: expectedTimelock, tokenAddress: expectedTokenAddress, + routingId: this.deriveRoutingId(expectedHash, expectedTokenAddress), }; - this.expectedIncomingTransfers.set(expectedHash, expectedIncomingTransfer); + transferReceived(this, expectedIncomingTransfer).subscribe( + async ([expectedIncomingTransfer, transferReceivedRequest]) => { + const { + tokenAddress, + units, + expiry, + rHash, + routingId, + } = transferReceivedRequest; + const { + units: expectedUnits, + expiry: expectedTimelock, + tokenAddress: expectedTokenAddress, + } = expectedIncomingTransfer; + const currency = this.getCurrencyByTokenaddress(tokenAddress); + const blockHeight = await this.getHeight(); + const block = await this.getBlockByNumber(blockHeight); + const currentTime = parseInt(block.result.timestamp, 16); + // The timelock can be up to 10 blocks less than the agreed upon value + const TIMELOCK_BUFFER = 10; + // We calculate the timelock from current timestamp to the expiry + const TIME_DIFF_IN_MS = expiry - currentTime; + const timelock = + TIME_DIFF_IN_MS / ONE_SECOND_IN_MS / AVERAGE_ETH_BLOCK_TIME_SECONDS; + + if ( + tokenAddress === expectedTokenAddress && + units === expectedUnits && + timelock >= expectedTimelock - TIMELOCK_BUFFER + ) { + this.logger.debug( + `accepting incoming transfer with rHash: ${rHash}, units: ${units}, timelock ${timelock.toFixed( + 0, + )}, currency ${currency}, and routingId ${routingId}`, + ); + 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} is not greater than or equal to ${expectedTimelock - TIMELOCK_BUFFER}`, + ); + } + } + }, + ); + } + + private getTransferByRoutingId = async ( + routingId: string, + ): Promise => { + const res = await this.sendRequest( + `/${this.publicIdentifier}/transfers/routing-id/${routingId}`, + 'GET', + ); + const transfers = await parseResponseBody(res); + if (transfers.length !== 1) { + throw new Error(`could not find transfer by routing id ${routingId}`); + } + return transfers[0]; } /** * Resolves a HashLock Transfer on the Connext network. */ - public settleInvoice = async (rHash: string, rPreimage: string, currency: string) => { - this.logger.debug(`settling ${currency} invoice for ${rHash} with preimage ${rPreimage}`); + public settleInvoice = async ( + rHash: string, + rPreimage: string, + currency: string, + ) => { + this.logger.debug( + `settling ${currency} invoice for ${rHash} with preimage ${rPreimage}`, + ); const assetId = this.getTokenAddress(currency); - await this.sendRequest('/hashlock-resolve', 'POST', { - assetId, - preImage: `0x${rPreimage}`, + const routingId = this.deriveRoutingId(rHash, assetId); + const lockHash = `0x${rHash}`; + const pendingTransfer = await this.getTransferByRoutingId(routingId); + const { transferId } = pendingTransfer; + await this.sendRequest('/transfers/resolve', 'POST', { + transferId, + routingId, + conditionType: 'HashlockTransfer', + details: { + lockHash, + }, + transferResolver: { + preImage: `0x${rPreimage}`, + }, + publicIdentifier: this.publicIdentifier, + channelAddress: this.channelAddress, }); } public removeInvoice = async (rHash: string) => { - 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 - await 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}`); + try { + const assetId = this.getTokenAddress('ETH'); // TODO: make it work for multi currency + const transfer = await this.getHashLockStatus(rHash, assetId); + await this.sendRequest('/transfers/resolve', 'POST', { + transferId: transfer.transferId, + conditionType: 'HashlockTransfer', + details: { + lockHash: transfer.transferState.lockHash, + }, + transferResolver: { + preImage: + '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + publicIdentifier: this.publicIdentifier, + channelAddress: this.channelAddress, + routingId: transfer.meta.routingId, + }); + this.logger.debug(`canceled transfer with rHash ${rHash}`); + } catch (e) { + if (e === errors.NOT_FOUND) { + this.logger.debug(`canceled transfer with rHash ${rHash}`); + return; } - this.expectedIncomingTransfers.delete(rHash); - } else { - this.logger.warn(`could not find expected incoming transfer with hash ${rHash}`); + throw e; } } private async getHashLockStatus(lockHash: string, assetId: string) { - const res = await this.sendRequest(`/hashlock-status/0x${lockHash}/${assetId}`, 'GET'); - const transferStatusResponse = await parseResponseBody(res); + const res = await this.sendRequest( + `/${this.publicIdentifier}/channels/${ + this.channelAddress + }/transfers/routing-id/${this.deriveRoutingId(lockHash, assetId)}`, + 'GET', + ); + const transferStatusResponse = await parseResponseBody( + res, + ); return transferStatusResponse; } - public lookupPayment = async (rHash: string, currency: string): Promise => { + public lookupPayment = async ( + rHash: string, + currency: string, + ): Promise => { try { const assetId = this.getTokenAddress(currency); - const transferStatusResponse = await this.getHashLockStatus(rHash, assetId); + const transferStatusResponse = await this.getHashLockStatus( + rHash, + assetId, + ); + const currentBlockHeight = await this.getHeight(); + const expiry = parseInt(transferStatusResponse.transferState.expiry, 10); + + const getStatusFromStatusResponse = ( + currentHeight: number, + transfer: ConnextTransfer, + ): string => { + const preimage = transfer.transferResolver?.preImage; + const HASH_ZERO = + '0x0000000000000000000000000000000000000000000000000000000000000000'; + if (preimage && preimage === HASH_ZERO) { + return 'FAILED'; + } + if (preimage) { + return 'COMPLETED'; + } + if (expiry > 0 && currentHeight >= expiry) { + return 'EXPIRED'; + } + return 'PENDING'; + }; + const transferStatus = getStatusFromStatusResponse( + currentBlockHeight, + transferStatusResponse, + ); - this.logger.trace(`hashlock status for connext transfer with hash ${rHash} is ${transferStatusResponse.status}`); - switch (transferStatusResponse.status) { + this.logger.trace( + `hashlock status for connext transfer with hash ${rHash} is ${transferStatus}`, + ); + switch (transferStatus) { case 'PENDING': return { state: PaymentState.Pending }; case 'COMPLETED': + assert( + transferStatusResponse.transferResolver?.preImage, + 'Cannot mark payment as COMPLETED without preimage', + ); return { state: PaymentState.Succeeded, - preimage: transferStatusResponse.preImage?.slice(2), + preimage: transferStatusResponse.transferResolver.preImage.slice(2), }; case 'EXPIRED': - const expiredTransferUnlocked$ = defer(() => 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: sha256(['address', 'bytes32'], [assetId, `0x${rHash}`]), - }), - )).pipe( + const expiredTransferUnlocked$ = defer(() => + 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 settle the invoice without a preimage + this.settleInvoice(rHash, '', currency), + ), + ).pipe( 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); + this.logger.error( + `failed to unlock an expired connext transfer with rHash: ${rHash} - retrying in ${RETRY_INTERVAL}ms`, + e, + ); return timer(RETRY_INTERVAL).pipe(mergeMapTo(caught)); }), take(1), ); expiredTransferUnlocked$.subscribe({ complete: () => { - this.logger.debug(`successfully unlocked an expired connext transfer with rHash: ${rHash}`); + this.logger.debug( + `successfully unlocked an expired connext transfer with rHash: ${rHash}`, + ); }, }); + // TODO: user funds can remain locked if the above code fails and xud is restarted. In that case the transfer + // is marked as failed and it will not attempt to unlock again. return { state: PaymentState.Failed }; case 'FAILED': return { state: 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 { - await 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: 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: PaymentState.Pending }; - } + return { state: PaymentState.Pending }; } } catch (err) { - if (err.code === errorCodes.PAYMENT_NOT_FOUND) { + if (err.code === errorCodes.NOT_FOUND) { + this.logger.trace( + `hashlock status for connext transfer with hash ${rHash} not found`, + ); return { state: PaymentState.Failed }; } this.logger.error(`could not lookup connext transfer for ${rHash}`, err); @@ -584,8 +864,9 @@ class ConnextClient extends SwapClient { public getRoute = async () => { /** A placeholder route value that assumes a fixed lock time of 100 for Connext. */ + const currentHeight = await this.getHeight(); return { - getTotalTimeLock: () => 101, + getTotalTimeLock: () => currentHeight + 101, }; } @@ -593,8 +874,45 @@ class ConnextClient extends SwapClient { return true; } + private getBlockByNumber = async (blockNumber: number) => { + const res = await this.sendRequest( + `/ethprovider/${CHAIN_IDENTIFIERS[this.network]}`, + 'POST', + { + method: 'eth_getBlockByNumber', + params: [`0x${blockNumber.toString(16)}`, false], + }, + ); + const block = await parseResponseBody(res); + return block; + } + public getHeight = async () => { - return 1; // connext's API does not tell us the height + const res = await this.sendRequest( + `/ethprovider/${CHAIN_IDENTIFIERS[this.network]}`, + 'POST', + { + method: 'eth_blockNumber', + params: [], + }, + ); + const blockNumberResponse = await parseResponseBody(res); + return parseInt(blockNumberResponse.result, 16); + } + + private getBalanceForAddress = async (assetId: string) => { + const res = await this.sendRequest( + `/ethprovider/${CHAIN_IDENTIFIERS[this.network]}`, + 'POST', + { + method: 'eth_getBalance', + params: [assetId, 'latest'], + }, + ); + const getBalanceResponse = await parseResponseBody( + res, + ); + return parseInt(getBalanceResponse.result, 16); } public getInfo = async (): Promise => { @@ -604,44 +922,49 @@ class ConnextClient extends SwapClient { if (this.isDisabled()) { status = errors.CONNEXT_IS_DISABLED.message; } else { - try { - const getInfo$ = combineLatest( - from(this.getVersion()), - from(this.getClientConfig()), - ).pipe( - // error if no response within 5000 ms - timeout(5000), - // complete the stream when we receive 1 value - take(1), - ); - const [streamVersion, clientConfig] = await getInfo$.toPromise(); - status = 'Ready'; - version = streamVersion; - address = clientConfig.signerAddress; - } catch (err) { - status = err.message; - } + status = 'Ready'; + version = 'TODO: Not exposed, yet'; + address = this.channelAddress || 'Waiting for channel'; } return { status, address, version }; } /** - * Gets the connext version. + * Creates connext node */ - public getVersion = async (): Promise => { - const res = await this.sendRequest('/version', 'GET'); - const { version } = await parseResponseBody(res); - return version; + private createNode = async (mnemonic: string): Promise => { + await this.sendRequest('/node', 'POST', { + mnemonic, + index: 0, + }); } /** * Gets the configuration of Connext client. */ - public getClientConfig = async (): Promise => { + private getClientConfig = async (): Promise => { const res = await this.sendRequest('/config', 'GET'); const clientConfig = await parseResponseBody(res); - return clientConfig; + return clientConfig[0]; + } + + private getChannel = async (): Promise => { + const res = await this.sendRequest( + `/${this.publicIdentifier}/channels`, + 'GET', + ); + const channel = await parseResponseBody(res); + if (channel.length === 0) { + await this.sendRequest('/setup', 'POST', { + counterpartyIdentifier: getRouterNodeIdentifier(this.network), + publicIdentifier: this.publicIdentifier, + chainId: CHAIN_IDENTIFIERS[this.network], + timeout: this.CHANNEL_ON_CHAIN_DISPUTE_TIMEOUT, + }); + return await this.getChannel(); + } + return channel[0]; } public channelBalance = async ( @@ -651,12 +974,16 @@ class ConnextClient extends SwapClient { return { balance: 0, pendingOpenBalance: 0, inactiveBalance: 0 }; } - const { freeBalanceOffChain, nodeFreeBalanceOffChain } = await this.getBalance(currency); + const { + freeBalanceOffChain, + nodeFreeBalanceOffChain, + } = await this.getBalance(currency); const freeBalanceAmount = this.unitConverter.unitsToAmount({ currency, units: Number(freeBalanceOffChain), }); + const nodeFreeBalanceAmount = this.unitConverter.unitsToAmount({ currency, units: Number(nodeFreeBalanceOffChain), @@ -665,7 +992,9 @@ class ConnextClient extends SwapClient { 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}`); + this.logger.debug( + `new inbound capacity (collateral) for ${currency} of ${nodeFreeBalanceAmount}`, + ); } return { @@ -690,9 +1019,7 @@ class ConnextClient extends SwapClient { /** * Returns the balances available in wallet for a specified currency. */ - public walletBalance = async ( - currency?: string, - ): Promise => { + public walletBalance = async (currency?: string): Promise => { if (!currency) { return { totalBalance: 0, @@ -715,7 +1042,9 @@ class ConnextClient extends SwapClient { }; } - private getBalance = (currency: string): Promise => { + private getBalance = ( + currency: string, + ): Promise => { // 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. @@ -723,55 +1052,78 @@ class ConnextClient extends SwapClient { 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 parseResponseBody(res); - }).finally(() => { - this.getBalancePromises.delete(currency); // clear the stored promise - }); + getBalancePromise = Promise.all([ + this.sendRequest( + `/${this.publicIdentifier}/channels/${this.channelAddress}`, + 'GET', + ), + this.getBalanceForAddress(this.signerAddress!), + ]) + .then(async ([channelDetailsRes, onChainBalance]) => { + const channelDetails = await parseResponseBody( + channelDetailsRes, + ); + const assetIdIndex = channelDetails.assetIds.indexOf(tokenAddress); + if (assetIdIndex === -1) { + const response = ({ + freeBalanceOffChain: 0, + nodeFreeBalanceOffChain: 0, + freeBalanceOnChain: onChainBalance, + } as unknown) as ConnextChannelBalanceResponse; + return response; + } else { + const inboundBalance = + channelDetails.balances[assetIdIndex].amount[0]; + const balance = channelDetails.balances[assetIdIndex].amount[1]; + const response = ({ + freeBalanceOffChain: balance, + nodeFreeBalanceOffChain: inboundBalance, + freeBalanceOnChain: onChainBalance, + } as unknown) as ConnextChannelBalanceResponse; + return response; + } + }) + .finally(() => { + this.getBalancePromises.delete(currency); // clear the stored promise + }); this.getBalancePromises.set(currency, getBalancePromise); } - return getBalancePromise; } - public deposit = async () => { - const clientConfig = await this.getClientConfig(); - return clientConfig.signerAddress; + // Returns on-chain deposit address + public walletDeposit = async () => { + if (this.signerAddress) { + return this.signerAddress; + } + throw new Error('Could not get signer address'); } - public openChannel = async ({ currency, units }: OpenChannelParams) => { - if (!currency) { - throw errors.CURRENCY_MISSING; - } - const assetId = this.getTokenAddress(currency); - const depositResponse = await this.sendRequest('/deposit', 'POST', { - assetId, - amount: units.toLocaleString('fullwide', { useGrouping: false }), // toLocaleString avoids scientific notation - }); - const { txhash } = await parseResponseBody(depositResponse); - - const minCollateralRequestQuantity = ConnextClient.MIN_COLLATERAL_REQUEST_SIZES[currency]; - if (minCollateralRequestQuantity !== undefined) { - const minCollateralRequestUnits = this.unitConverter.amountToUnits({ currency, amount: minCollateralRequestQuantity }); - const depositConfirmed$ = fromEvent(this, 'depositConfirmed').pipe( - filter(hash => hash === txhash), // only proceed if the incoming hash matches our expected txhash - take(1), // complete the stream after 1 matching event - timeout(86400000), // clear up the listener after 1 day - ); - depositConfirmed$.subscribe({ - complete: () => { - this.requestCollateralInBackground(currency, minCollateralRequestUnits); - }, - }); + // Returns channel deposit address + public deposit = async () => { + if (this.channelAddress) { + return this.channelAddress; } + throw new Error('Could not get channel address'); + } - return txhash; + public openChannel = async (_params: OpenChannelParams) => { + throw new Error( + `Open channel command is disabled for Connext currencies. Please send funds directly to the channel address ${this.channelAddress} in order to open a channel.`, + ); } - public closeChannel = async ({ units, currency, destination }: CloseChannelParams): Promise => { + public closeChannel = async ({ + units, + currency, + destination, + }: CloseChannelParams): Promise => { if (!currency) { throw errors.CURRENCY_MISSING; } + if (!destination) { + throw errors.WITHDRAW_ADDRESS_MISSING; + } const { freeBalanceOffChain } = await this.getBalance(currency); const availableUnits = Number(freeBalanceOffChain); if (units && availableUnits < units) { @@ -784,14 +1136,18 @@ class ConnextClient extends SwapClient { } const withdrawResponse = await this.sendRequest('/withdraw', 'POST', { - recipient: destination, + publicIdentifier: this.publicIdentifier, + channelAddress: this.channelAddress, amount: amount.toLocaleString('fullwide', { useGrouping: false }), assetId: this.tokenAddresses.get(currency), + recipient: destination, + fee: '120', // TODO: estimate fee }); - const { txhash } = await parseResponseBody(withdrawResponse); - - return [txhash]; + const { transferId } = await parseResponseBody( + withdrawResponse, + ); + return [transferId]; } /** @@ -801,29 +1157,22 @@ class ConnextClient extends SwapClient { * @param amount * @param lockHash */ - private executeHashLockTransfer = async (payload: TokenPaymentRequest): Promise => { - this.logger.debug(`sending payment of ${payload.amount} with hash ${payload.lockHash} to ${payload.recipient}`); - this.outgoingTransferHashes.add(payload.lockHash); - const res = await this.sendRequest('/hashlock-transfer', 'POST', payload); - const { appId } = await 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 - */ - public depositToChannel = async ( - assetId: string, - amount: number, - ): Promise => { - await this.sendRequest('/hashlock-transfer', 'POST', { - assetId, - amount: amount.toLocaleString('fullwide', { useGrouping: false }), - }); + private executeHashLockTransfer = async ( + payload: ConnextTransferRequest, + ): Promise => { + const lockHash = payload.details.lockHash; + this.logger.debug( + `sending payment of ${payload.amount} with hash ${lockHash} to ${payload.recipient}`, + ); + this.outgoingTransferHashes.add(lockHash); + const res = await this.sendRequest('/transfers/create', 'POST', payload); + const transferResponse = await parseResponseBody( + res, + ); + return transferResponse; } + // Withdraw on-chain funds public withdraw = async ({ all, currency, @@ -868,6 +1217,9 @@ class ConnextClient extends SwapClient { /** Connext client specific cleanup. */ protected disconnect = async () => { + if (this._reconcileDepositSubscriber) { + this._reconcileDepositSubscriber.unsubscribe(); + } this.setStatus(ClientStatus.Disconnected); for (const req of this.pendingRequests) { @@ -887,7 +1239,11 @@ class ConnextClient extends SwapClient { * @param method an HTTP request method * @param payload the request payload */ - private sendRequest = (endpoint: string, method: string, payload?: object): Promise => { + private sendRequest = ( + endpoint: string, + method: string, + payload?: object, + ): Promise => { return new Promise((resolve, reject) => { const options: http.RequestOptions = { method, @@ -905,7 +1261,9 @@ class ConnextClient extends SwapClient { }; } - this.logger.trace(`sending request to ${endpoint}${payloadStr ? `: ${payloadStr}` : ''}`); + this.logger.trace( + `sending request to ${endpoint}${payloadStr ? `: ${payloadStr}` : ''}`, + ); let req: http.ClientRequest; req = http.request(options, async (res) => { @@ -928,7 +1286,7 @@ class ConnextClient extends SwapClient { err = errors.INSUFFICIENT_BALANCE; break; case 404: - err = errors.PAYMENT_NOT_FOUND; + err = errors.NOT_FOUND; break; case 408: err = errors.TIMEOUT; @@ -946,14 +1304,16 @@ class ConnextClient extends SwapClient { break; } if (err) { - this.logger.error(err.message); + this.logger.error( + `received ${res.statusCode} response from ${endpoint}: ${err.message}`, + ); reject(err); } }); req.on('error', async (err: any) => { this.pendingRequests.delete(req); - if (err.code === 'ECONNREFUSED') { + if (err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET') { await this.disconnect(); } this.logger.error(err); diff --git a/lib/connextclient/README.md b/lib/connextclient/README.md deleted file mode 100644 index abb2bc54a..000000000 --- a/lib/connextclient/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Connext - -## Installing Connext node for local development - -```bash -git clone https://github.com/ConnextProject/indra.git -cd indra -make -``` - -### Running Connext node - -```bash -make start -``` - -For logs -```bash -bash ops/logs.sh node -``` - -## Installing Connext REST API client - -```bash -git clone https://github.com/ConnextProject/rest-api-client -cd rest-api-client -npm install -``` - -### Running Connext REST API client -Create environment variables file and point to the Etheruem and Connext node - -```bash -CONNEXT_ETH_PROVIDER_URL="http://0.0.0.0:8545" -CONNEXT_NODE_URL="http://0.0.0.0:8888" -``` - -Then run the REST API server - -```bash -npm run start -``` - -## Add Connext to xud.conf - -```bash -[connext] -disable = false -host = "localhost" -port = 5040 -``` - -## Add ETH currency - -```bash -./bin/xucli addcurrency ETH Connext 18 --token_address="0x0000000000000000000000000000000000000000" -``` - -A restart of xud is currently required for it to show up under `xucli getbalance` call. - -## Fund the Connext client address -Get the Connext address from the output of - -`./bin/xucli getinfo` - -Initialize MetaMask wallet with seed - -`candy maple cake sugar pudding cream honey rich smooth crumble sweet treat` - -This is the root treasury account for local development network/chain: - -`http://localhost:8545` - -After making a transfer your funds should be visible as wallet balance in the output of - -`./bin/xucli getbalance` - -## Open a payment channel between the node and client - -`./bin/xucli openchannel Connext ETH 1` - -Your funds should now be visiable as channel balance in the output of - -`./bin/xucli getbalance` - -## Add trading pair - -`./bin/xucli addpair ETH BTC` - -A restart of xud is currently required for the trading pair to be swappable. - -At this point you should be able to perform ETHBTC swaps. diff --git a/lib/connextclient/errors.ts b/lib/connextclient/errors.ts index ed22a92a4..475ab1890 100644 --- a/lib/connextclient/errors.ts +++ b/lib/connextclient/errors.ts @@ -8,8 +8,6 @@ const errorCodes = { INVALID: codesPrefix.concat('.4'), SERVER_ERROR: codesPrefix.concat('.5'), UNEXPECTED: codesPrefix.concat('.6'), - TOKEN_ADDRESS_NOT_FOUND: codesPrefix.concat('.7'), - PAYMENT_NOT_FOUND: codesPrefix.concat('.8'), INVALID_TOKEN_PAYMENT_RESPONSE: codesPrefix.concat('.9'), CONNEXT_HAS_NO_ACTIVE_CHANNELS: codesPrefix.concat('.11'), CONNEXT_CLIENT_NOT_INITIALIZED: codesPrefix.concat('.12'), @@ -18,6 +16,8 @@ const errorCodes = { EXPIRY_MISSING: codesPrefix.concat('.15'), MISSING_SEED: codesPrefix.concat('.16'), INSUFFICIENT_COLLATERAL: codesPrefix.concat('.17'), + NOT_FOUND: codesPrefix.concat('.18'), + WITHDRAW_ADDRESS_MISSING: codesPrefix.concat('.19'), }; const errors = { @@ -49,13 +49,13 @@ const errors = { message: `unexpected status from connext request ${statusCode}: ${statusMessage ?? ''}`, code: errorCodes.UNEXPECTED, }), - TOKEN_ADDRESS_NOT_FOUND: { - message: 'connext token address not found', - code: errorCodes.TOKEN_ADDRESS_NOT_FOUND, + NOT_FOUND: { + message: 'connext returned not found response', + code: errorCodes.NOT_FOUND, }, - PAYMENT_NOT_FOUND: { - message: 'connext payment not found', - code: errorCodes.PAYMENT_NOT_FOUND, + WITHDRAW_ADDRESS_MISSING: { + message: 'destination account for the withdrawal is missing', + code: errorCodes.WITHDRAW_ADDRESS_MISSING, }, INVALID_TOKEN_PAYMENT_RESPONSE: { message: 'connext TokenPaymentResponse is invalid', diff --git a/lib/connextclient/types.ts b/lib/connextclient/types.ts index aa67083d9..3de4b3607 100644 --- a/lib/connextclient/types.ts +++ b/lib/connextclient/types.ts @@ -18,22 +18,72 @@ export type ConnextInfo = { version?: string; }; -/** - * The connext version. - */ -export type ConnextVersion = { - version: string; +/** Response for eth_blockNumber call */ +export type ConnextBlockNumberResponse = { + result: string; +}; + +type Balance = { + amount: string[]; + to: string[]; +}; + +/** Connext channel details */ +export type ConnextChannelDetails = { + assetIds: string[]; + balances: Balance[]; + channelAddress: string; + merkleRoot: string; + processedDepositsA: string[]; + processedDepositsB: string[]; + networkContext: { + chainId: number; + channelFactoryAddress: string; + transferRegistryAddress: string; + providerUrl: string; + }; + nonce: number; + alice: string; + aliceIdentifier: string; + bob: string; + bobIdentifier: string; + timeout: string; + latestUpdate: { + assetId: string; + balance: Balance; + channelAddress: string; + details: { + totalDepositsAlice: string; + totalDepositsBob: string; + }; + fromIdentifier: string; + nonce: number; + aliceSignature: string; + bobSignature: string; + toIdentifier: string; + type: string; + }; + defundNonce: string; + inDispute: boolean; }; /** * The payload for tokenPayment call. */ -export type TokenPaymentRequest = { - assetId: string; +export type ConnextTransferRequest = { + type: 'HashlockTransfer'; + channelAddress: string; amount: string; - lockHash: string; - timelock: string; + assetId: string; + details: { + lockHash: string; + expiry: string; + }; recipient: string; + meta: { + routingId: string; + }; + publicIdentifier: string; }; /** @@ -41,130 +91,204 @@ export type TokenPaymentRequest = { */ export type ConnextErrorResponse = { message: string }; +export type ConnextConfig = { + publicIdentifier: string; + signerAddress: string; + index: number; +}; + /** - * The response for initWallet call. + * The response for /config call. */ -export type ConnextInitWalletResponse = { success: boolean }; +export type ConnextConfigResponse = ConnextConfig[]; /** - * The response for /config call. + * The response for /channel call. */ -export type ConnextConfigResponse = { - multisigAddress: string; - natsClusterId: string; - natsToken: string; - nodeUrl: string; - signerAddress: string; - userPublicIdentifier: string; - userIdentifier: string; -}; +export type ConnextChannelResponse = string[]; /** * The response for balance call. */ -export type ConnextBalanceResponse = { +export type ConnextChannelBalanceResponse = { freeBalanceOffChain: string; nodeFreeBalanceOffChain: string; freeBalanceOnChain: string; }; +/** + * The response for ethprovider eth_getBalance call. + */ +export type ConnextBalanceResponse = { + id: number; + jsonrpc: string; + result: string; +}; + +export type GetBlockByNumberResponse = { + result: { + difficulty: string; + extraData: string; + gasLimit: string; + gasUsed: string; + hash: string; + logsBloom: string; + miner: string; + mixHash: string; + nonce: string; + number: string; + parentHash: string; + receiptsRoot: string; + sha3Uncles: string; + size: string; + stateRoot: string; + timestamp: string; + totalDifficulty: string; + transactions: string[]; + transactionsRoot: string; + uncles: string[]; + }; +}; + /** * The response for hashLockTransfer call. */ export type ConnextTransferResponse = { - appId: string; - preImage: string; + channelAddress: string; + transferId: string; + routingId: string; }; /** * The response for withdraw call. */ export type ConnextWithdrawResponse = { - txhash: string; + channelAddress: '0xa929dB0530daB525596f5d48Fb5C322fDa8A337B'; + transferId: '0xc2e4592d3fb6c02ee1d3b07bed83b5914f8ca084b0f91d6b80bf8107e58c9c38'; }; -/** - * The response for deposit call. - */ -export type ConnextDepositResponse = { - txhash: string; +type ConnextRoutingPath = { + recipient: string; + recipientChainId: number; + recipientAssetId: string; }; -/** - * The response for hashLockTransfer call. - */ -export type ConnextTransferStatus = { - senderAppIdentityHash: string; - receiverIdentifier: string; - senderIdentifier: string; +export type ConnextTransfer = { + channelFactoryAddress: string; assetId: string; - amount: string; - lockHash: string; - status: string; + chainId: number; + channelAddress: string; + balance: { + amount: string[]; + to: string[]; + }; + initiator: string; + responder: string; + initialStateHash: string; + transferDefinition: string; + transferEncodings: string[]; + transferId: string; + transferState: { + lockHash: string; + expiry: string; + }; + transferTimeout: string; meta: { - sender: string; - timelock: string; + requireOnline: boolean; + routingId: string; + path: ConnextRoutingPath[]; + senderIdentifier?: string; }; - preImage?: string; - expiry: { - _hex: string; + transferResolver?: { + preImage?: string; }; }; +export type TransfersByRoutingIdResponse = ConnextTransfer[]; + export type ExpectedIncomingTransfer = { rHash: string; units: number; expiry: number; tokenAddress: string; - paymentId?: string; + routingId: string; }; export type ConnextPreimageRequest = { - id: string; - data?: { - type: string; - amount: { - _hex: string; - }; + channelAddress: string; + channelBalance: { + to: string[]; + amount: string[]; + }; + transfer: { + channelFactoryAddress: string; assetId: string; - paymentId: string; - sender: string; - recipient: string; + chainId: string; + channelAddress: string; + balance: { + amount: string[]; + to: string[]; + }; + initiator: string; + responder: string; + initialStateHash: string; + transferDefinition: string; + transferEncodings: string[]; + transferId: string; + transferState: { + lockHash: string; + expiry: string; + }; + transferTimeout: string; meta: { - sender: string; - recipient: string; + requireOnline: boolean; + routingId: string; + path: [ + { + recipient: string; + recipientChainId: number; + recipientAssetId: string; + } + ]; }; - transferMeta: { + transferResolver: { preImage: string; }; - } + }; + conditionType: string; }; export type ConnextIncomingTransferRequest = { - id: string; - data?: { - amount: { - _hex: string; - }; - appIdentityHash: string; + channelAddress: string; + channelBalance: { + to: string[]; + amount: string[]; + }; + transfer: { + channelFactoryAddress: string; assetId: string; - meta: { - recipient: string; - sender: string; - timelock: 200 + chainId: number; + channelAddress: string; + balance: { + amount: string[]; + to: string[]; }; - sender: string; - transferMeta: { + initiator: string; + responder: string; + initialStateHash: string; + transferDefinition: string; + transferEncodings: string[]; + transferId: string; + transferState: { lockHash: string; - expiry: { - _hex: string; - }; - timelock: string; + expiry: string; + }; + transferTimeout: string; + meta: { + routingId: string; }; - type: string; - paymentId: string, - recipient: string; }; + conditionType: string; }; export type ConnextDepositConfirmedRequest = { @@ -179,16 +303,16 @@ export type ConnextDepositConfirmedRequest = { }; export type ProvidePreimageEvent = { - rHash: string, - preimage: string, + rHash: string; + preimage: string; }; export type TransferReceivedEvent = { tokenAddress: string; rHash: string; - timelock: number; + expiry: number; units: number; - paymentId: string; + routingId: string; }; export type OnchainTransferResponse = { diff --git a/lib/db/seeds/simnet.ts b/lib/db/seeds/simnet.ts index 83dad61e6..88c1f0b71 100644 --- a/lib/db/seeds/simnet.ts +++ b/lib/db/seeds/simnet.ts @@ -27,6 +27,7 @@ const currencies = [ decimalPlaces: 18, tokenAddress: '0x0000000000000000000000000000000000000000', }, + /* { id: 'USDT', swapClient: SwapClientType.Connext, @@ -39,6 +40,7 @@ const currencies = [ decimalPlaces: 18, tokenAddress: '0x69C3d485623bA3f382Fc0FB6756c4574d43C1618', }, + */ /* { id: 'XUC', @@ -51,14 +53,14 @@ const currencies = [ const pairs = [ // { baseCurrency: 'BTC', quoteCurrency: 'DAI' }, - { baseCurrency: 'BTC', quoteCurrency: 'USDT' }, + // { baseCurrency: 'BTC', quoteCurrency: 'USDT' }, { baseCurrency: 'ETH', quoteCurrency: 'BTC' }, // { baseCurrency: 'ETH', quoteCurrency: 'DAI' }, // { baseCurrency: 'ETH', quoteCurrency: 'USDT' }, { baseCurrency: 'LTC', quoteCurrency: 'BTC' }, // { baseCurrency: 'LTC', quoteCurrency: 'DAI' }, // { baseCurrency: 'LTC', quoteCurrency: 'USDT' }, - { baseCurrency: 'USDT', quoteCurrency: 'DAI' }, + // { baseCurrency: 'USDT', quoteCurrency: 'DAI' }, // { baseCurrency: 'XUC', quoteCurrency: 'BTC' }, // { baseCurrency: 'XUC', quoteCurrency: 'ETH' }, // { baseCurrency: 'XUC', quoteCurrency: 'DAI' }, diff --git a/lib/grpc/GrpcService.ts b/lib/grpc/GrpcService.ts index 7a581cba5..17d48b6fe 100644 --- a/lib/grpc/GrpcService.ts +++ b/lib/grpc/GrpcService.ts @@ -415,6 +415,23 @@ class GrpcService { } } + /** + * See [[Service.deposit]] + */ + public deposit: grpc.handleUnaryCall = async (call, callback) => { + if (!this.isReady(this.service, callback)) { + return; + } + try { + const address = await this.service.deposit(call.request.toObject()); + const response = new xudrpc.DepositResponse(); + response.setAddress(address); + callback(null, response); + } catch (err) { + callback(getGrpcError(err), null); + } + } + /** * See [[Service.walletWithdraw]] */ diff --git a/lib/grpc/getGrpcError.ts b/lib/grpc/getGrpcError.ts index a059de9f2..22441cc30 100644 --- a/lib/grpc/getGrpcError.ts +++ b/lib/grpc/getGrpcError.ts @@ -23,6 +23,7 @@ const getGrpcError = (err: any) => { case orderErrorCodes.QUANTITY_DOES_NOT_MATCH: case swapErrorCodes.REMOTE_IDENTIFIER_MISSING: case orderErrorCodes.DUPLICATE_PAIR_CURRENCIES: + case connextErrorCodes.WITHDRAW_ADDRESS_MISSING: code = status.INVALID_ARGUMENT; break; case orderErrorCodes.PAIR_DOES_NOT_EXIST: diff --git a/lib/http/HttpService.ts b/lib/http/HttpService.ts index c48c48054..432ee00a6 100644 --- a/lib/http/HttpService.ts +++ b/lib/http/HttpService.ts @@ -12,9 +12,9 @@ class HttpService { public providePreimage = async (preimageRequest: ConnextPreimageRequest): Promise => { if ( - preimageRequest.data && preimageRequest.data.transferMeta + preimageRequest.transfer ) { - const { preImage: preimage } = preimageRequest.data.transferMeta; + const { preImage: preimage } = preimageRequest.transfer.transferResolver; if (!preimage) { throw serviceErrors.INVALID_ARGUMENT('preImage is missing'); } @@ -33,24 +33,25 @@ class HttpService { public incomingTransfer = async ( incomingTransferRequest: ConnextIncomingTransferRequest, ): Promise => { - if (incomingTransferRequest.data) { + if (incomingTransferRequest.transfer) { + const transfer = incomingTransferRequest.transfer; const { - amount: amountHex, + transferState, + meta, assetId, - paymentId, - } = incomingTransferRequest.data; - const { - lockHash, - timelock: timelockString, - } = incomingTransferRequest.data.transferMeta; + balance, + } = transfer; + const { routingId } = meta; + const { lockHash, expiry: expiryString } = transferState; const rHash = lockHash.slice(2); - const timelock = parseInt(timelockString, 10); - const units = parseInt(amountHex._hex, 16); + const expiry = parseInt(expiryString, 10); + const { amount } = balance; + const units = parseInt(amount[0], 10); await this.service.transferReceived({ rHash, - timelock, + expiry, units, - paymentId, + routingId, tokenAddress: assetId, }); return {}; diff --git a/lib/lndclient/LndClient.ts b/lib/lndclient/LndClient.ts index 583fca8b5..af59a58b4 100644 --- a/lib/lndclient/LndClient.ts +++ b/lib/lndclient/LndClient.ts @@ -528,11 +528,15 @@ class LndClient extends SwapClient { return this.unaryCall('closedChannels', new lndrpc.ClosedChannelsRequest()); } - public deposit = async () => { + public walletDeposit = async () => { const depositAddress = await this.newAddress(); return depositAddress; } + public deposit = async () => { + throw new Error('Depositing directly to channel is not supported. Please use a lightning service provider such as Boltz to create a channel with 1 transaction or deposit to the on-chain wallet first.'); + } + public withdraw = async ({ amount, destination, all = false, fee }: WithdrawArguments) => { const request = new lndrpc.SendCoinsRequest(); request.setAddr(destination); diff --git a/lib/proto/xudrpc.swagger.json b/lib/proto/xudrpc.swagger.json index ccdc5b3dc..cd795e197 100644 --- a/lib/proto/xudrpc.swagger.json +++ b/lib/proto/xudrpc.swagger.json @@ -200,6 +200,33 @@ ] } }, + "/v1/deposit": { + "post": { + "summary": "Gets an address to deposit a given currency directly into a channel.\nshell: xucli deposit \u003ccurrency\u003e", + "operationId": "Deposit", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/xudrpcDepositResponse" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/xudrpcDepositRequest" + } + } + ], + "tags": [ + "Xud" + ] + } + }, "/v1/discovernodes": { "post": { "summary": "Discover nodes from a specific peer and apply new connections", diff --git a/lib/proto/xudrpc_grpc_pb.d.ts b/lib/proto/xudrpc_grpc_pb.d.ts index 22051652c..b67fd7b40 100644 --- a/lib/proto/xudrpc_grpc_pb.d.ts +++ b/lib/proto/xudrpc_grpc_pb.d.ts @@ -82,6 +82,7 @@ interface IXudService extends grpc.ServiceDefinition; responseDeserialize: grpc.deserialize; } +interface IXudService_IDeposit extends grpc.MethodDefinition { + path: string; // "/xudrpc.Xud/Deposit" + requestStream: boolean; // false + responseStream: boolean; // false + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} interface IXudService_IDiscoverNodes extends grpc.MethodDefinition { path: string; // "/xudrpc.Xud/DiscoverNodes" requestStream: boolean; // false @@ -428,6 +438,7 @@ export interface IXudServer { closeChannel: grpc.handleUnaryCall; connect: grpc.handleUnaryCall; walletDeposit: grpc.handleUnaryCall; + deposit: grpc.handleUnaryCall; discoverNodes: grpc.handleUnaryCall; getBalance: grpc.handleUnaryCall; getInfo: grpc.handleUnaryCall; @@ -479,6 +490,9 @@ export interface IXudClient { walletDeposit(request: xudrpc_pb.DepositRequest, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DepositResponse) => void): grpc.ClientUnaryCall; walletDeposit(request: xudrpc_pb.DepositRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DepositResponse) => void): grpc.ClientUnaryCall; walletDeposit(request: xudrpc_pb.DepositRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DepositResponse) => void): grpc.ClientUnaryCall; + deposit(request: xudrpc_pb.DepositRequest, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DepositResponse) => void): grpc.ClientUnaryCall; + deposit(request: xudrpc_pb.DepositRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DepositResponse) => void): grpc.ClientUnaryCall; + deposit(request: xudrpc_pb.DepositRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DepositResponse) => void): grpc.ClientUnaryCall; discoverNodes(request: xudrpc_pb.DiscoverNodesRequest, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DiscoverNodesResponse) => void): grpc.ClientUnaryCall; discoverNodes(request: xudrpc_pb.DiscoverNodesRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DiscoverNodesResponse) => void): grpc.ClientUnaryCall; discoverNodes(request: xudrpc_pb.DiscoverNodesRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DiscoverNodesResponse) => void): grpc.ClientUnaryCall; @@ -580,6 +594,9 @@ export class XudClient extends grpc.Client implements IXudClient { public walletDeposit(request: xudrpc_pb.DepositRequest, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DepositResponse) => void): grpc.ClientUnaryCall; public walletDeposit(request: xudrpc_pb.DepositRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DepositResponse) => void): grpc.ClientUnaryCall; public walletDeposit(request: xudrpc_pb.DepositRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DepositResponse) => void): grpc.ClientUnaryCall; + public deposit(request: xudrpc_pb.DepositRequest, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DepositResponse) => void): grpc.ClientUnaryCall; + public deposit(request: xudrpc_pb.DepositRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DepositResponse) => void): grpc.ClientUnaryCall; + public deposit(request: xudrpc_pb.DepositRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DepositResponse) => void): grpc.ClientUnaryCall; public discoverNodes(request: xudrpc_pb.DiscoverNodesRequest, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DiscoverNodesResponse) => void): grpc.ClientUnaryCall; public discoverNodes(request: xudrpc_pb.DiscoverNodesRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DiscoverNodesResponse) => void): grpc.ClientUnaryCall; public discoverNodes(request: xudrpc_pb.DiscoverNodesRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: xudrpc_pb.DiscoverNodesResponse) => void): grpc.ClientUnaryCall; diff --git a/lib/proto/xudrpc_grpc_pb.js b/lib/proto/xudrpc_grpc_pb.js index 8d359ad92..dd60c7532 100644 --- a/lib/proto/xudrpc_grpc_pb.js +++ b/lib/proto/xudrpc_grpc_pb.js @@ -953,6 +953,19 @@ var XudService = exports.XudService = { responseSerialize: serialize_xudrpc_DepositResponse, responseDeserialize: deserialize_xudrpc_DepositResponse, }, + // Gets an address to deposit a given currency directly into a channel. + // shell: xucli deposit + deposit: { + path: '/xudrpc.Xud/Deposit', + requestStream: false, + responseStream: false, + requestType: xudrpc_pb.DepositRequest, + responseType: xudrpc_pb.DepositResponse, + requestSerialize: serialize_xudrpc_DepositRequest, + requestDeserialize: deserialize_xudrpc_DepositRequest, + responseSerialize: serialize_xudrpc_DepositResponse, + responseDeserialize: deserialize_xudrpc_DepositResponse, + }, // Discover nodes from a specific peer and apply new connections discoverNodes: { path: '/xudrpc.Xud/DiscoverNodes', diff --git a/lib/service/Service.ts b/lib/service/Service.ts index f9ded99a7..416d443a9 100644 --- a/lib/service/Service.ts +++ b/lib/service/Service.ts @@ -234,7 +234,13 @@ class Service extends EventEmitter { public walletDeposit = async (args: { currency: string }) => { const { currency } = args; - const address = await this.swapClientManager.deposit(currency); + const address = await this.swapClientManager.walletDeposit(currency.toUpperCase()); + return address; + } + + public deposit = async (args: { currency: string }) => { + const { currency } = args; + const address = await this.swapClientManager.deposit(currency.toUpperCase()); return address; } diff --git a/lib/swaps/SwapClient.ts b/lib/swaps/SwapClient.ts index a60b22d37..0f45cdfac 100644 --- a/lib/swaps/SwapClient.ts +++ b/lib/swaps/SwapClient.ts @@ -330,9 +330,12 @@ abstract class SwapClient extends EventEmitter { { remoteIdentifier, units, currency, destination, force, fee }: CloseChannelParams, ): Promise; - /** Gets a deposit address. */ + /** Gets an address for depositing directly to a channel. */ public abstract async deposit(): Promise; + /** Gets a deposit address for on-chain wallet. */ + public abstract async walletDeposit(): Promise; + /** Withdraws from the onchain wallet of the client and returns the transaction id or transaction hash in case of Ethereum */ public abstract async withdraw(args: WithdrawArguments): Promise; diff --git a/lib/swaps/SwapClientManager.ts b/lib/swaps/SwapClientManager.ts index ebd22b028..cbd27e554 100644 --- a/lib/swaps/SwapClientManager.ts +++ b/lib/swaps/SwapClientManager.ts @@ -90,6 +90,7 @@ class SwapClientManager extends EventEmitter { unitConverter: this.unitConverter, config: this.config.connext, logger: this.loggers.connext, + network: this.config.network, }); } @@ -559,6 +560,16 @@ class SwapClientManager extends EventEmitter { } } + public walletDeposit = async (currency: string) => { + const swapClient = this.get(currency); + if (!swapClient) { + throw errors.SWAP_CLIENT_NOT_FOUND(currency); + } + + const address = await swapClient.walletDeposit(); + return address; + } + public deposit = async (currency: string) => { const swapClient = this.get(currency); if (!swapClient) { diff --git a/lib/swaps/Swaps.ts b/lib/swaps/Swaps.ts index c69463479..6262b2285 100644 --- a/lib/swaps/Swaps.ts +++ b/lib/swaps/Swaps.ts @@ -157,7 +157,7 @@ class Swaps extends EventEmitter { } }); if (this.swapClientManager.connextClient) { - this.pool.updateConnextState(this.swapClientManager.connextClient.tokenAddresses, this.swapClientManager.connextClient.userIdentifier); + this.pool.updateConnextState(this.swapClientManager.connextClient.tokenAddresses, this.swapClientManager.connextClient.publicIdentifier); } this.swapRecovery.beginTimer(); @@ -669,7 +669,20 @@ class Swaps extends EventEmitter { this.logger.debug(`sending swap accepted packet: ${JSON.stringify(responseBody)} to peer: ${peer.nodePubKey}`); const sendSwapAcceptedPromise = peer.sendPacket(new packets.SwapAcceptedPacket(responseBody, requestPacket.header.id)); - await Promise.all([newPhasePromise, sendSwapAcceptedPromise]); + try { + await Promise.all([newPhasePromise, sendSwapAcceptedPromise]); + } catch (e) { + this.logger.trace(`failed to accept deal because: ${JSON.stringify(e)}`); + await this.failDeal({ + deal, + peer, + reqId, + failureReason: SwapFailureReason.UnknownError, + errorMessage: 'Unable to accept deal', + failedCurrency: deal.takerCurrency, + }); + return false; + } return true; } diff --git a/lib/utils/simnet-connext-channels.ts b/lib/utils/simnet-connext-channels.ts index 11998ddb1..addc66d27 100644 --- a/lib/utils/simnet-connext-channels.ts +++ b/lib/utils/simnet-connext-channels.ts @@ -1,16 +1,16 @@ import grpc from 'grpc'; import http from 'http'; -import { defer, empty, from, Observable, of, throwError } from 'rxjs'; +import { defer, from, Observable, of, throwError } from 'rxjs'; import { catchError, concat, concatAll, delay, - mapTo, mergeMap, retryWhen, share, take, + mapTo, } from 'rxjs/operators'; import { loadXudClient } from '../cli/command'; import { XudClient } from '../proto/xudrpc_grpc_pb'; @@ -19,8 +19,6 @@ import { GetBalanceResponse, GetInfoRequest, GetInfoResponse, - OpenChannelRequest, - OpenChannelResponse, } from '../proto/xudrpc_pb'; type Balances = { @@ -63,20 +61,6 @@ const getBalance = async ( return balances as GetBalanceResponse; }; -const openConnextChannel = async ( - client: XudClient, - currency: string, - amount: number, -): Promise => { - const request = new OpenChannelRequest(); - request.setCurrency(currency.toUpperCase()); - request.setAmount(amount); - const openChannelResponse = await new Promise((resolve, reject) => { - client.openChannel(request, processResponse(resolve, reject)); - }); - return openChannelResponse as OpenChannelResponse; -}; - const checkBalanceObservable = ( client: XudClient, currency: string, @@ -91,7 +75,7 @@ const checkBalanceObservable = ( walletBalance: currencyBalance.getWalletBalance(), channelBalance: currencyBalance.getChannelBalance(), }; - if (balances.walletBalance < minimumBalance) { + if (balances.channelBalance < minimumBalance) { // the balance is under our specified threshold // we'll hit the faucet with our connext address // and then recheck the balance @@ -185,7 +169,6 @@ const faucetRequest = (connextAddress: string) => { const createSimnetChannel = ({ client, currency, - minChannelAmount, channelAmount, retryInterval, getBalance$, @@ -204,17 +187,6 @@ const createSimnetChannel = ({ getBalance$, ); const simnetChannel$ = balances$.pipe( - mergeMap((balances) => { - if (balances.channelBalance >= minChannelAmount) { - // in case we already have enough channelBalance we won't attempt - // to open a channel - return empty(); - } else { - return from(openConnextChannel(client, currency, channelAmount)).pipe( - mapTo(currency), - ); - } - }), // when error happens retryWhen(errors => errors.pipe( @@ -226,6 +198,7 @@ const createSimnetChannel = ({ concat(throwError('unrecoverable error happened - giving up')), ), ), + mapTo(currency), // complete the observable when the flow is successful take(1), ); diff --git a/package.json b/package.json index ab6f599fd..1ab509640 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "test:sim:clean:connext": "(cd test/simulation && ./docker-clean.sh connext)", "test:sim:clean:custom-xud": "(cd test/simulation && ./docker-clean.sh custom-xud)", "test:sim:logs": "cd test/simulation && ./logs.sh", - "test:jest": "jest --unhandled-rejections=strict --rootDir test/jest", + "test:jest": "jest --forceExit --unhandled-rejections=strict --rootDir test/jest", "test:seedutil": "jest --rootDir seedutil", "test:jest:watch": "jest --watch", "typedoc": "typedoc --out typedoc --module commonjs --target es6 lib --readme none", diff --git a/proto/xudrpc.proto b/proto/xudrpc.proto index c53c746c7..8c535a8f5 100644 --- a/proto/xudrpc.proto +++ b/proto/xudrpc.proto @@ -110,6 +110,15 @@ service Xud { }; } + /* Gets an address to deposit a given currency directly into a channel. + * shell: xucli deposit */ + rpc Deposit(DepositRequest) returns (DepositResponse) { + option (google.api.http) = { + post: "/v1/deposit" + body: "*" + }; + } + /* Discover nodes from a specific peer and apply new connections */ rpc DiscoverNodes(DiscoverNodesRequest) returns (DiscoverNodesResponse) { option (google.api.http) = { diff --git a/test/jest/Connext.spec.ts b/test/jest/Connext.spec.ts index e6a84048c..12aa34ca0 100644 --- a/test/jest/Connext.spec.ts +++ b/test/jest/Connext.spec.ts @@ -89,6 +89,7 @@ describe('ConnextClient', () => { currencyInstances, logger, unitConverter: new UnitConverter(), + network: 'mainnet', }); }); @@ -227,7 +228,15 @@ describe('ConnextClient', () => { expect.assertions(1); connext['getHashLockStatus'] = jest .fn() - .mockReturnValue({ status: 'PENDING' }); + .mockReturnValue({ + transferState: { + expiry: '10001', + }, + transferResolver: {}, + }); + connext['getHeight'] = jest + .fn() + .mockReturnValue(10000); const result = await connext['lookupPayment']('0x12345', 'ETH'); expect(result).toEqual({ state: PaymentState.Pending }); }); @@ -236,78 +245,58 @@ describe('ConnextClient', () => { expect.assertions(1); connext['getHashLockStatus'] = jest .fn() - .mockReturnValue({ status: 'COMPLETED', preImage: '0x1337' }); + .mockReturnValue({ + transferState: { + expiry: '10001', + }, + transferResolver: { + preImage: '0x1337', + }, + }); + connext['getHeight'] = jest + .fn() + .mockReturnValue(10000); const result = await connext['lookupPayment']('0x12345', 'ETH'); expect(result).toEqual({ state: PaymentState.Succeeded, preimage: '1337' }); }); - it('returns PaymentState.Failed when rejected app install for payment without status field', async () => { - expect.assertions(3); - const senderAppIdentityHash = '12345'; + it('returns PaymentState.Failed when preimage is hash zero', async () => { + expect.assertions(1); connext['getHashLockStatus'] = jest .fn() .mockReturnValue({ - senderAppIdentityHash, + transferState: { + expiry: '10001', + }, + transferResolver: { + preImage: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, }); - connext['sendRequest'] = jest.fn().mockReturnValue(Promise.resolve()); + connext['getHeight'] = jest + .fn() + .mockReturnValue(10000); const result = await connext['lookupPayment']('0x12345', 'ETH'); - expect(connext['sendRequest']).toHaveBeenCalledTimes(1); - expect(connext['sendRequest']).toHaveBeenCalledWith( - '/reject-install', - 'POST', - expect.objectContaining({ appIdentityHash: senderAppIdentityHash }), - ); expect(result).toEqual({ state: PaymentState.Failed }); }); - it('returns PaymentState.Pending when failing to reject app install for payment without status field', async () => { - expect.assertions(3); - const senderAppIdentityHash = '12345'; + it('returns PaymentState.Failed when EXPIRED', async () => { + expect.assertions(1); connext['getHashLockStatus'] = jest .fn() .mockReturnValue({ - senderAppIdentityHash, + transferState: { + expiry: '10001', + }, + transferResolver: {}, }); - connext['sendRequest'] = jest.fn().mockReturnValue(Promise.reject()); - const result = await connext['lookupPayment']('0x12345', 'ETH'); - expect(connext['sendRequest']).toHaveBeenCalledTimes(1); - expect(connext['sendRequest']).toHaveBeenCalledWith( - '/reject-install', - 'POST', - expect.objectContaining({ appIdentityHash: senderAppIdentityHash }), - ); - expect(result).toEqual({ state: PaymentState.Pending }); - }); - - it('returns PaymentState.Failed when EXPIRED', async () => { - expect.assertions(3); - connext['getHashLockStatus'] = jest + connext['getHeight'] = jest .fn() - .mockReturnValue({ status: 'EXPIRED' }); + .mockReturnValue(10001); connext['sendRequest'] = jest.fn().mockReturnValue(Promise.resolve()); const hash = '8f28fb27a164ae992fb4808b11c137d06e8e7d9304043a6b7163323f7cf53920'; const currency = 'ETH'; const result = await connext['lookupPayment'](hash, currency); expect(result).toEqual({ state: PaymentState.Failed }); - expect(connext['sendRequest']).toHaveBeenCalledTimes(1); - expect(connext['sendRequest']).toHaveBeenCalledWith( - '/hashlock-resolve', - 'POST', - expect.objectContaining({ - assetId: ETH_ASSET_ID, - preImage: '0x', - paymentId: '0xb2c0648834d105f3b372c6a05d11b0f19d88a8909f6315c8535e383e59991f8e', - }), - ); - }); - - it('returns PaymentState.Failed when FAILED', async () => { - expect.assertions(1); - connext['getHashLockStatus'] = jest - .fn() - .mockReturnValue({ status: 'FAILED' }); - const result = await connext['lookupPayment']('0x12345', 'ETH'); - expect(result).toEqual({ state: PaymentState.Failed }); }); it('returns PaymentState.Pending when error is unknown', async () => { @@ -321,12 +310,12 @@ describe('ConnextClient', () => { expect(result).toEqual({ state: PaymentState.Pending }); }); - it('returns PaymentState.Failed when error is PAYMENT_NOT_FOUND', async () => { + it('returns PaymentState.Failed when error is NOT_FOUND', async () => { expect.assertions(1); connext['getHashLockStatus'] = jest .fn() .mockImplementation(() => { - throw errors.PAYMENT_NOT_FOUND; + throw errors.NOT_FOUND; }); const result = await connext['lookupPayment']('0x12345', 'ETH'); expect(result).toEqual({ state: PaymentState.Failed }); diff --git a/test/jest/HttpServer.spec.ts b/test/jest/HttpServer.spec.ts index 2d14d42fd..705e98c6c 100644 --- a/test/jest/HttpServer.spec.ts +++ b/test/jest/HttpServer.spec.ts @@ -29,7 +29,11 @@ describe('HttpServer - preimage', () => { it('should receive and parse a preimage request', (done) => { request(`http://localhost:${port}`) .post('/preimage') - .send({ data: { transferMeta: { preImage } } }) + .send({ + transfer: { + transferResolver: { preImage }, + }, + }) .expect(200) .expect('Content-Type', 'application/json') .end((err) => { diff --git a/test/jest/HttpService.spec.ts b/test/jest/HttpService.spec.ts index 09c3c6aa2..3b0eff6d6 100644 --- a/test/jest/HttpService.spec.ts +++ b/test/jest/HttpService.spec.ts @@ -7,10 +7,8 @@ const mockedService = >Service; const rHash = 'd92e2eb0e9118faedc5ce533b65737b33a88c187c10e74e6d8b1be34626ae892'; const preImage = 'd55dd2b285a815f9449d9e665ed61dd19663e08e9d4e84db621ca3e78082fabf'; const preimageRequest: any = { - id: '1', - data: { - type: '', - transferMeta: { + transfer: { + transferResolver: { preImage: `0x${preImage}`, }, }, diff --git a/test/jest/__snapshots__/Connext.spec.ts.snap b/test/jest/__snapshots__/Connext.spec.ts.snap index 2a720846b..2880c5393 100644 --- a/test/jest/__snapshots__/Connext.spec.ts.snap +++ b/test/jest/__snapshots__/Connext.spec.ts.snap @@ -2,8 +2,8 @@ exports[`ConnextClient sendRequest deposit fails with 404 1`] = ` Object { - "code": "8.8", - "message": "connext payment not found", + "code": "8.18", + "message": "connext returned not found response", } `; diff --git a/test/simulation/actions.go b/test/simulation/actions.go index dac411f9d..93060d330 100644 --- a/test/simulation/actions.go +++ b/test/simulation/actions.go @@ -4,8 +4,8 @@ import ( "context" "errors" "fmt" - "github.com/ExchangeUnion/xud-simulation/connexttest" - "math/big" + // "github.com/ExchangeUnion/xud-simulation/connexttest" + // "math/big" "time" "github.com/roasbeef/btcutil" @@ -56,13 +56,14 @@ func (a *actions) init(node *xudtest.HarnessNode) { // Add currencies. a.addCurrency(node, "BTC", xudrpc.Currency_LND, "", 8) a.addCurrency(node, "LTC", xudrpc.Currency_LND, "", 8) - a.addCurrency(node, "ETH", xudrpc.Currency_CONNEXT, connexttest.ETHTokenAddress, 18) + // a.addCurrency(node, "ETH", xudrpc.Currency_CONNEXT, connexttest.ETHTokenAddress, 18) // Add pairs. a.addPair(node, "LTC", "BTC") - a.addPair(node, "BTC", "ETH") + // a.addPair(node, "BTC", "ETH") } +/* func (a *actions) FundETH(net *xudtest.NetworkHarness, node *xudtest.HarnessNode) { // Wait for node's connext connection to catch-up. a.waitConnextReady(node) @@ -84,6 +85,7 @@ func (a *actions) FundETH(net *xudtest.NetworkHarness, node *xudtest.HarnessNode a.assert.Equal(uint64(200000000), ethBal.WalletBalance) a.assert.Equal(uint64(0), ethBal.ChannelBalance) } +*/ func (a *actions) waitConnextReady(node *xudtest.HarnessNode) { isReady := func() bool { diff --git a/test/simulation/docker-build.sh b/test/simulation/docker-build.sh index da4a704c1..4b94b096a 100755 --- a/test/simulation/docker-build.sh +++ b/test/simulation/docker-build.sh @@ -1,14 +1,15 @@ #!/usr/bin/env bash -if [[ $@ == "connext" || $# == 0 ]] -then - mkdir -p temp - pushd temp - git clone https://github.com/ConnextProject/indra.git - cd indra - git checkout indra-7.3.14 - make - popd -fi +# TODO(karl): enable connext V2 simulation tests +# if [[ $@ == "connext" || $# == 0 ]] +# then + # mkdir -p temp + # pushd temp + # git clone https://github.com/ConnextProject/indra.git + # cd indra + # git checkout indra-7.3.14 + # make + # popd +# fi docker-compose build $@ diff --git a/test/simulation/docker-compose.yml b/test/simulation/docker-compose.yml index c40cb107a..0719b9b47 100644 --- a/test/simulation/docker-compose.yml +++ b/test/simulation/docker-compose.yml @@ -8,10 +8,10 @@ services: build: ./docker-lnd volumes: - lnd-vol:/app - connext: - build: ./docker-connext - volumes: - - connext-vol:/app + # connext: + # build: ./docker-connext + # volumes: + # - connext-vol:/app xud: build: ./docker-xud volumes: @@ -29,13 +29,13 @@ services: depends_on: - btcd - lnd - - connext + # - connext - xud - gomod volumes: - btcd-vol:/btcd-vol - lnd-vol:/lnd-vol - - connext-vol:/connext-vol + # - connext-vol:/connext-vol - xud-vol:/xud-vol - custom-xud-vol:/custom-xud-vol - nvm-vol:/nvm-vol @@ -43,12 +43,12 @@ services: volumes: btcd-vol: lnd-vol: - connext-vol: + # connext-vol: xud-vol: custom-xud-vol: nvm-vol: gomod-vol: -networks: - default: - external: - name: indra + # networks: + # default: + # external: + # name: indra diff --git a/test/simulation/docker-run.sh b/test/simulation/docker-run.sh index 852371aef..0fef13519 100755 --- a/test/simulation/docker-run.sh +++ b/test/simulation/docker-run.sh @@ -2,11 +2,11 @@ # create the temp directories with the current user so it is the owner for permissions mkdir -p $PWD/temp/logs -mkdir -p $PWD/temp/indra +# mkdir -p $PWD/temp/indra -pushd temp/indra -make start -popd +# pushd temp/indra +# make start +# popd export DOCKER_CLIENT_TIMEOUT=120 export COMPOSE_HTTP_TIMEOUT=120 @@ -27,8 +27,8 @@ if [[ $testRetCode != 0 ]]; then done fi -pushd temp/indra -make reset -popd +# pushd temp/indra +# make reset +# popd exit $testRetCode diff --git a/test/simulation/tests-instability.go b/test/simulation/tests-instability.go index 7c2ece779..41b9d8fee 100644 --- a/test/simulation/tests-instability.go +++ b/test/simulation/tests-instability.go @@ -22,57 +22,28 @@ var instabilityTestCases = []*testCase{ name: "maker crashed after send payment before preimage resolved; incoming: lnd, outgoing: lnd", // replacing Alice test: testMakerCrashedAfterSendBeforePreimageResolved, }, - { - name: "maker crashed after send payment before preimage resolved; incoming: connext, outgoing: lnd", // replacing Alice - test: testMakerCrashedAfterSendBeforePreimageResolvedConnextIn, - }, { name: "maker crashed after send payment after preimage resolved; incoming: lnd, outgoing: lnd", // replacing Alice test: testMakerCrashedAfterSendAfterPreimageResolved, }, - { - name: "maker crashed after send payment after preimage resolved; incoming: connext, outgoing: lnd", // replacing Alice - test: testMakerCrashedAfterSendAfterPreimageResolvedConnextIn, - }, { name: "maker lnd crashed before order settlement", // replacing Alice test: testMakerLndCrashedBeforeSettlement, }, - { - name: "maker connext client crashed before order settlement", // replacing Alice - test: testMakerConnextClientCrashedBeforeSettlement, - }, { name: "maker crashed after send payment with delayed settlement; incoming: lnd, outgoing: lnd", // replacing Alice + Bob test: testMakerCrashedAfterSendDelayedSettlement, }, - { - name: "maker crashed after send payment with delayed settlement; incoming: connext, outgoing: lnd", // replacing Alice + Bob - test: testMakerCrashedAfterSendDelayedSettlementConnextIn, - }, - { - name: "maker crashed after send payment with delayed settlement; incoming: lnd, outgoing: connext", // replacing Alice + Bob - test: testMakerCrashedAfterSendDelayedSettlementConnextOut, - }, } func testMakerCrashedAfterSendBeforePreimageResolved(net *xudtest.NetworkHarness, ht *harnessTest) { testMakerCrashedDuringSwap(net, ht, []string{"CUSTOM_SCENARIO=INSTABILITY::MAKER_CRASH_AFTER_SEND_BEFORE_PREIMAGE_RESOLVED"}) } -func testMakerCrashedAfterSendBeforePreimageResolvedConnextIn(net *xudtest.NetworkHarness, ht *harnessTest) { - ht.act.FundETH(net, net.Bob) - testMakerCrashedDuringSwapConnextIn(net, ht, []string{"CUSTOM_SCENARIO=INSTABILITY::MAKER_CRASH_AFTER_SEND_BEFORE_PREIMAGE_RESOLVED"}) -} - func testMakerCrashedAfterSendAfterPreimageResolved(net *xudtest.NetworkHarness, ht *harnessTest) { testMakerCrashedDuringSwap(net, ht, []string{"CUSTOM_SCENARIO=INSTABILITY::MAKER_CRASH_AFTER_SEND_AFTER_PREIMAGE_RESOLVED"}) } -func testMakerCrashedAfterSendAfterPreimageResolvedConnextIn(net *xudtest.NetworkHarness, ht *harnessTest) { - testMakerCrashedDuringSwapConnextIn(net, ht, []string{"CUSTOM_SCENARIO=INSTABILITY::MAKER_CRASH_AFTER_SEND_AFTER_PREIMAGE_RESOLVED"}) -} - func testMakerCrashedDuringSwap(net *xudtest.NetworkHarness, ht *harnessTest, customXudMakerEnvVars []string) { var err error net.Alice, err = net.SetCustomXud(ht.ctx, ht, net.Alice, customXudMakerEnvVars) @@ -125,67 +96,6 @@ func testMakerCrashedDuringSwap(net *xudtest.NetworkHarness, ht *harnessTest, cu ht.assert.Equal(alicePrevLtcBalance+ltcQuantity, aliceLtcBalance, "alice did not receive LTC") } -func testMakerCrashedDuringSwapConnextIn(net *xudtest.NetworkHarness, ht *harnessTest, makerEnvArgs []string) { - var err error - net.Alice, err = net.SetCustomXud(ht.ctx, ht, net.Alice, makerEnvArgs) - ht.assert.NoError(err) - ht.act.init(net.Alice) - ht.act.waitConnextReady(net.Alice) - - // Connect Alice to Bob. - ht.act.connect(net.Alice, net.Bob) - ht.act.verifyConnectivity(net.Alice, net.Bob) - - err = openETHChannel(ht.ctx, net.Bob, 40000, 0) - ht.assert.NoError(err) - - // Save the initial balances. - alicePrevBalance, err := net.Alice.Client.GetBalance(ht.ctx, &xudrpc.GetBalanceRequest{Currency: "ETH"}) - ht.assert.NoError(err) - alicePrevEthBalance := alicePrevBalance.Balances["ETH"] - - // Place an order on Alice. - aliceOrderReq := &xudrpc.PlaceOrderRequest{ - OrderId: "testMakerCrashedDuringSwapConnextIn", - Price: 40, - Quantity: 100, - PairId: "BTC/ETH", - Side: xudrpc.OrderSide_SELL, - } - ht.act.placeOrderAndBroadcast(net.Alice, net.Bob, aliceOrderReq) - - // brief wait for collateralization to complete for Alice - time.Sleep(1 * time.Second) - - // Place a matching order on Bob. - bobOrderReq := &xudrpc.PlaceOrderRequest{ - OrderId: "testMakerCrashedDuringSwapConnextIn", - Price: aliceOrderReq.Price, - Quantity: aliceOrderReq.Quantity, - PairId: aliceOrderReq.PairId, - Side: xudrpc.OrderSide_BUY, - } - - _, err = net.Bob.Client.PlaceOrderSync(ht.ctx, bobOrderReq) - ht.assert.NoError(err) - - <-net.Alice.ProcessExit - - err = net.Alice.Start(nil) - ht.assert.NoError(err) - ht.act.waitConnextReady(net.Alice) - - // Brief delay to allow for swap to be recovered consistently. - time.Sleep(3 * time.Second) - - // Verify that Alice recovered ETH funds. - aliceBalance, err := net.Alice.Client.GetBalance(ht.ctx, &xudrpc.GetBalanceRequest{Currency: "ETH"}) - ht.assert.NoError(err) - aliceEthBalance := aliceBalance.Balances["ETH"] - diff := uint64(float64(aliceOrderReq.Quantity) * aliceOrderReq.Price) - ht.assert.Equal(alicePrevEthBalance.ChannelBalance+diff, aliceEthBalance.ChannelBalance, "alice did not recover ETH funds") -} - func testMakerLndCrashedBeforeSettlement(net *xudtest.NetworkHarness, ht *harnessTest) { var err error net.Alice, err = net.SetCustomXud(ht.ctx, ht, net.Alice, []string{ @@ -247,89 +157,6 @@ func testMakerLndCrashedBeforeSettlement(net *xudtest.NetworkHarness, ht *harnes ht.assert.Equal(alicePrevLtcBalance+ltcQuantity, aliceLtcBalance, "alice did not recover LTC funds") } -func testMakerConnextClientCrashedBeforeSettlement(net *xudtest.NetworkHarness, ht *harnessTest) { - var err error - net.Alice, err = net.SetCustomXud(ht.ctx, ht, net.Alice, []string{ - "CUSTOM_SCENARIO=INSTABILITY::MAKER_CLIENT_CRASHED_BEFORE_SETTLE", - "CLIENT_TYPE=ConnextClient", - // connext-client should be replaced, so we're not specifying its current PID, - // as in other client types. - }) - - ht.assert.NoError(err) - ht.act.init(net.Alice) - ht.act.waitConnextReady(net.Alice) - ht.act.waitConnextReady(net.Bob) - - // Connect Alice to Bob. - ht.act.connect(net.Alice, net.Bob) - ht.act.verifyConnectivity(net.Alice, net.Bob) - - err = openETHChannel(ht.ctx, net.Bob, 40000, 0) - ht.assert.NoError(err) - - // Save the initial balances. - alicePrevBalance, err := net.Alice.Client.GetBalance(ht.ctx, &xudrpc.GetBalanceRequest{Currency: "ETH"}) - ht.assert.NoError(err) - alicePrevEthBalance := alicePrevBalance.Balances["ETH"] - - bobPrevBalance, err := net.Bob.Client.GetBalance(ht.ctx, &xudrpc.GetBalanceRequest{Currency: "BTC"}) - ht.assert.NoError(err) - bobPrevBtcBalance := bobPrevBalance.Balances["BTC"] - - // Place an order on Alice. - aliceOrderReq := &xudrpc.PlaceOrderRequest{ - OrderId: "testMakerConnextClientCrashedBeforeSettlement", - Price: 40, - Quantity: 100, - PairId: "BTC/ETH", - Side: xudrpc.OrderSide_SELL, - } - ht.act.placeOrderAndBroadcast(net.Alice, net.Bob, aliceOrderReq) - - // brief wait for collateralization to complete for Alice - time.Sleep(1 * time.Second) - - // Place a matching order on Bob. - bobOrderReq := &xudrpc.PlaceOrderRequest{ - OrderId: "testMakerConnextClientCrashedBeforeSettlement", - Price: aliceOrderReq.Price, - Quantity: aliceOrderReq.Quantity, - PairId: aliceOrderReq.PairId, - Side: xudrpc.OrderSide_BUY, - } - go net.Bob.Client.PlaceOrderSync(ht.ctx, bobOrderReq) - - // Alice's connext-client is expected to be killed by Alice's custom xud. - <-net.Alice.ConnextClient.ProcessExit - - // Wait a bit so that Alice's call to connext-client for settlement would fail. - time.Sleep(5 * time.Second) - - // Restart Alice's connext-client. - err = net.Alice.ConnextClient.Start(nil) - ht.assert.NoError(err) - ht.act.waitConnextReady(net.Alice) - - // Brief delay to allow for swap to be recovered consistently. - // The pending swap recheck interval is usually 5m, but was adjusted in - // Alice's custom xud to 5s (as well as the swap completion timeout interval). - time.Sleep(10 * time.Second) - - // Verify that both parties received their payment. - bobBalance, err := net.Bob.Client.GetBalance(ht.ctx, &xudrpc.GetBalanceRequest{Currency: "BTC"}) - ht.assert.NoError(err) - bobBtcBalance := bobBalance.Balances["BTC"] - diff := bobOrderReq.Quantity - ht.assert.Equal(bobPrevBtcBalance.ChannelBalance+diff, bobBtcBalance.ChannelBalance) - - aliceBalance, err := net.Alice.Client.GetBalance(ht.ctx, &xudrpc.GetBalanceRequest{Currency: "ETH"}) - ht.assert.NoError(err) - aliceEthBalance := aliceBalance.Balances["ETH"] - diff = uint64(float64(aliceOrderReq.Quantity) * aliceOrderReq.Price) - ht.assert.Equal(alicePrevEthBalance.ChannelBalance+diff, aliceEthBalance.ChannelBalance, "alice did not recover ETH funds") -} - func testMakerCrashedAfterSendDelayedSettlement(net *xudtest.NetworkHarness, ht *harnessTest) { var err error net.Alice, err = net.SetCustomXud(ht.ctx, ht, net.Alice, []string{"CUSTOM_SCENARIO=INSTABILITY::MAKER_CRASH_WHILE_SENDING"}) @@ -401,7 +228,7 @@ func testMakerCrashedAfterSendDelayedSettlementConnextOut(net *xudtest.NetworkHa ht.assert.NoError(err) ht.act.init(net.Alice) - ht.act.FundETH(net, net.Alice) + // ht.act.FundETH(net, net.Alice) ht.act.init(net.Bob) ht.act.waitConnextReady(net.Bob) @@ -490,7 +317,7 @@ func testMakerCrashedAfterSendDelayedSettlementConnextIn(net *xudtest.NetworkHar ht.act.waitConnextReady(net.Alice) ht.act.init(net.Bob) - ht.act.FundETH(net, net.Bob) + // ht.act.FundETH(net, net.Bob) // Connect Alice to Bob. ht.act.connect(net.Alice, net.Bob) diff --git a/test/simulation/tests-integration.go b/test/simulation/tests-integration.go index 8c19a6eb2..05c6f334a 100644 --- a/test/simulation/tests-integration.go +++ b/test/simulation/tests-integration.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/ExchangeUnion/xud-simulation/connexttest" + // "github.com/ExchangeUnion/xud-simulation/connexttest" "time" "github.com/ExchangeUnion/xud-simulation/xudrpc" @@ -18,10 +18,6 @@ var integrationTestCases = []*testCase{ name: "order matching and swap", test: testOrderMatchingAndSwap, }, - { - name: "order matching and swap connext", - test: testOrderMatchingAndSwapConnext, - }, { name: "dust order discarded", test: testDustOrderDiscarded, @@ -244,15 +240,15 @@ func testInternalMatchAndInvalidation(net *xudtest.NetworkHarness, ht *harnessTe func testRuntimeAddPairActiveOrders(net *xudtest.NetworkHarness, ht *harnessTest) { // Remove previously-added pairs/currencies from both Alice and Bob. ht.act.removePair(net.Alice, "LTC/BTC") - ht.act.removePair(net.Alice, "BTC/ETH") + // ht.act.removePair(net.Alice, "BTC/ETH") ht.act.removeCurrency(net.Alice, "LTC") ht.act.removeCurrency(net.Alice, "BTC") - ht.act.removeCurrency(net.Alice, "ETH") + // ht.act.removeCurrency(net.Alice, "ETH") ht.act.removePair(net.Bob, "LTC/BTC") - ht.act.removePair(net.Bob, "BTC/ETH") + // ht.act.removePair(net.Bob, "BTC/ETH") ht.act.removeCurrency(net.Bob, "LTC") ht.act.removeCurrency(net.Bob, "BTC") - ht.act.removeCurrency(net.Bob, "ETH") + // ht.act.removeCurrency(net.Bob, "ETH") // Connect Alice to Bob. ht.act.connect(net.Alice, net.Bob) @@ -261,9 +257,10 @@ func testRuntimeAddPairActiveOrders(net *xudtest.NetworkHarness, ht *harnessTest // Re-add the pairs/currencies to Alice after peer connection was already established. ht.act.addCurrency(net.Alice, "BTC", xudrpc.Currency_LND, "", 8) ht.act.addCurrency(net.Alice, "LTC", xudrpc.Currency_LND, "", 8) - ht.act.addCurrency(net.Alice, "ETH", xudrpc.Currency_CONNEXT, connexttest.ETHTokenAddress, 18) + // TODO(karl): enable connext V2 simulation tests + // ht.act.addCurrency(net.Alice, "ETH", xudrpc.Currency_CONNEXT, connexttest.ETHTokenAddress, 18) ht.act.addPair(net.Alice, "LTC", "BTC") - ht.act.addPair(net.Alice, "BTC", "ETH") + // ht.act.addPair(net.Alice, "BTC", "ETH") // Place LTC/BTC order on Alice. req := &xudrpc.PlaceOrderRequest{ @@ -296,33 +293,35 @@ func testRuntimeAddPairActiveOrders(net *xudtest.NetworkHarness, ht *harnessTest ht.act.removeOrderAndInvalidate(net.Alice, net.Bob, res.RemainingOrder) // Place BTC/ETH order on Alice. - req = &xudrpc.PlaceOrderRequest{ - OrderId: "maker_order_id", - Price: 40, - Quantity: 100, - PairId: "BTC/ETH", - Side: xudrpc.OrderSide_BUY, - } - res, err = net.Alice.Client.PlaceOrderSync(ht.ctx, req) - ht.assert.NoError(err) - ht.assert.Len(res.InternalMatches, 0) - ht.assert.Len(res.SwapSuccesses, 0) - ht.assert.Len(res.SwapFailures, 0) - ht.assert.NotNil(res.RemainingOrder) - - // Bob should receive the order once his BTC/ETH pair is re-added. - bobOrdersChan = subscribeOrders(ht.ctx, net.Bob) - ht.act.addCurrency(net.Bob, "ETH", xudrpc.Currency_CONNEXT, connexttest.ETHTokenAddress, 18) - ht.act.addPair(net.Bob, "BTC", "ETH") - - e = <-bobOrdersChan - ht.assert.NoError(e.err) - ht.assert.NotNil(e.orderUpdate) - peerOrder = e.orderUpdate.GetOrder() - ht.assert.Equal(peerOrder.Id, res.RemainingOrder.Id) - ht.assert.Equal(peerOrder.PairId, req.PairId) - ht.assert.Equal(peerOrder.NodeIdentifier.NodePubKey, net.Alice.PubKey()) - ht.act.removeOrderAndInvalidate(net.Alice, net.Bob, res.RemainingOrder) + /* + req = &xudrpc.PlaceOrderRequest{ + OrderId: "maker_order_id", + Price: 40, + Quantity: 100, + PairId: "BTC/ETH", + Side: xudrpc.OrderSide_BUY, + } + res, err = net.Alice.Client.PlaceOrderSync(ht.ctx, req) + ht.assert.NoError(err) + ht.assert.Len(res.InternalMatches, 0) + ht.assert.Len(res.SwapSuccesses, 0) + ht.assert.Len(res.SwapFailures, 0) + ht.assert.NotNil(res.RemainingOrder) + + // Bob should receive the order once his BTC/ETH pair is re-added. + bobOrdersChan = subscribeOrders(ht.ctx, net.Bob) + ht.act.addCurrency(net.Bob, "ETH", xudrpc.Currency_CONNEXT, connexttest.ETHTokenAddress, 18) + ht.act.addPair(net.Bob, "BTC", "ETH") + + e = <-bobOrdersChan + ht.assert.NoError(e.err) + ht.assert.NotNil(e.orderUpdate) + peerOrder = e.orderUpdate.GetOrder() + ht.assert.Equal(peerOrder.Id, res.RemainingOrder.Id) + ht.assert.Equal(peerOrder.PairId, req.PairId) + ht.assert.Equal(peerOrder.NodeIdentifier.NodePubKey, net.Alice.PubKey()) + ht.act.removeOrderAndInvalidate(net.Alice, net.Bob, res.RemainingOrder) + */ // Cleanup. ht.act.disconnect(net.Alice, net.Bob) @@ -491,75 +490,6 @@ func testOrderReplacement(net *xudtest.NetworkHarness, ht *harnessTest) { ht.act.disconnect(net.Alice, net.Bob) } -func testOrderMatchingAndSwapConnext(net *xudtest.NetworkHarness, ht *harnessTest) { - // Connect Alice to Bob. - ht.act.connect(net.Alice, net.Bob) - ht.act.verifyConnectivity(net.Alice, net.Bob) - - ht.act.FundETH(net, net.Alice) - - preChanAliceBal, err := net.Alice.Client.GetBalance(ht.ctx, &xudrpc.GetBalanceRequest{Currency: "ETH"}) - ht.assert.NoError(err) - preChanAliceEthBal := preChanAliceBal.Balances["ETH"] - - // Open channel from Alice. - err = openETHChannel(ht.ctx, net.Alice, 40000, 0) - ht.assert.NoError(err) - // wait for 1 block for the deposit transaction to confirm - time.Sleep(15 * time.Second) - - // Verify Alice ETH balance. - chanFeesThreshold := uint64(2100) - preSwapAliceBal, err := net.Alice.Client.GetBalance(ht.ctx, &xudrpc.GetBalanceRequest{Currency: "ETH"}) - preSwapAliceEthBal := preSwapAliceBal.Balances["ETH"] - ht.assert.True(preChanAliceEthBal.TotalBalance-preSwapAliceEthBal.TotalBalance <= chanFeesThreshold) - ht.assert.Equal(preSwapAliceEthBal.TotalBalance-preSwapAliceEthBal.ChannelBalance, preSwapAliceEthBal.WalletBalance) - ht.assert.Equal(uint64(40000), preSwapAliceEthBal.ChannelBalance) - - // wait for 1 block for node to collateralize ETH channel - time.Sleep(15 * time.Second) - - // Place an order on Alice. - req := &xudrpc.PlaceOrderRequest{ - OrderId: "maker_order_id", - Price: 40, - Quantity: 100, - PairId: "BTC/ETH", - Side: xudrpc.OrderSide_BUY, - } - ht.act.placeOrderAndBroadcast(net.Alice, net.Bob, req) - - // Place a matching order on Bob. - req = &xudrpc.PlaceOrderRequest{ - OrderId: "taker_order_id", - Price: req.Price, - Quantity: req.Quantity, - PairId: req.PairId, - Side: xudrpc.OrderSide_SELL, - } - ht.act.placeOrderAndSwap(net.Bob, net.Alice, req) - - time.Sleep(5 * time.Second) - - // Verify Alice ETH balance. - amt := uint64(req.Price * float64(req.Quantity)) - aliceBal, err := net.Alice.Client.GetBalance(ht.ctx, &xudrpc.GetBalanceRequest{Currency: "ETH"}) - aliceEthBal := aliceBal.Balances["ETH"] - ht.assert.Equal(preSwapAliceEthBal.TotalBalance-amt, aliceEthBal.TotalBalance) - ht.assert.Equal(aliceEthBal.TotalBalance-aliceEthBal.ChannelBalance, aliceEthBal.WalletBalance) - ht.assert.Equal(preSwapAliceEthBal.ChannelBalance-amt, aliceEthBal.ChannelBalance) - - // Verify Bob ETH balance. - bobBalance, err := net.Bob.Client.GetBalance(ht.ctx, &xudrpc.GetBalanceRequest{Currency: "ETH"}) - bobEthBalance := bobBalance.Balances["ETH"] - ht.assert.Equal(amt, bobEthBalance.TotalBalance) - ht.assert.Equal(uint64(0), bobEthBalance.WalletBalance) - ht.assert.Equal(amt, bobEthBalance.ChannelBalance) - - // Cleanup. - ht.act.disconnect(net.Alice, net.Bob) -} - func testMultiHopSwap(net *xudtest.NetworkHarness, ht *harnessTest) { // Connect Alice to Dave. ht.act.connect(net.Alice, net.Dave) diff --git a/test/simulation/xud_test.go b/test/simulation/xud_test.go index 49947c2bd..9648e8710 100644 --- a/test/simulation/xud_test.go +++ b/test/simulation/xud_test.go @@ -3,11 +3,11 @@ package main import ( "context" "fmt" - "github.com/ExchangeUnion/xud-simulation/connexttest" - "github.com/ethereum/go-ethereum/ethclient" + // "github.com/ExchangeUnion/xud-simulation/connexttest" + // "github.com/ethereum/go-ethereum/ethclient" "github.com/stretchr/testify/require" "log" - "net/http" + // "net/http" "os" "strings" "testing" @@ -275,6 +275,7 @@ func TestSecurityUnsettledChannels(t *testing.T) { } } +/* func verifyEthProviderReachability() error { client, err := ethclient.Dial(connexttest.EthProviderURL) if err != nil { @@ -297,14 +298,17 @@ func verifyConnextNodeReachability() error { return nil } +*/ func launchNetwork(noBalanceChecks bool) (*xudtest.NetworkHarness, func()) { - if err := verifyEthProviderReachability(); err != nil { - log.Fatalf("EthProvider reachability failure: %v", err) - } - if err := verifyConnextNodeReachability(); err != nil { - log.Fatalf("Connext node reachability failure: %v", err) - } + /* + if err := verifyEthProviderReachability(); err != nil { + log.Fatalf("EthProvider reachability failure: %v", err) + } + if err := verifyConnextNodeReachability(); err != nil { + log.Fatalf("Connext node reachability failure: %v", err) + } + */ // Create XUD network instance without launching it. log.Printf("xud: creating network") @@ -439,34 +443,36 @@ func launchNetwork(noBalanceChecks bool) (*xudtest.NetworkHarness, func()) { log.Fatalf("lnd-btc: unable to set up test network: %v", err) } - connextNetworkHarness := connexttest.NewNetworkHarness() - go func() { - for { - select { - case err, more := <-connextNetworkHarness.ProcessErrors(): - if !more { - return + /* + connextNetworkHarness := connexttest.NewNetworkHarness() + go func() { + for { + select { + case err, more := <-connextNetworkHarness.ProcessErrors(): + if !more { + return + } + if strings.Contains(err.Err.Error(), "signal: terminated") { + continue + } + + log.Printf("connext: finished with error (stderr):\n%v", err) } - if strings.Contains(err.Err.Error(), "signal: terminated") { - continue - } - - log.Printf("connext: finished with error (stderr):\n%v", err) } + }() + log.Printf("connext: launching network...") + if err := connextNetworkHarness.SetUp(); err != nil { + log.Fatalf("connext: unable to set up test network: %v", err) } - }() - log.Printf("connext: launching network...") - if err := connextNetworkHarness.SetUp(); err != nil { - log.Fatalf("connext: unable to set up test network: %v", err) - } - if err := connextNetworkHarness.Start(); err != nil { - log.Fatalf("connext: unable to start test network: %v", err) - } + if err := connextNetworkHarness.Start(); err != nil { + log.Fatalf("connext: unable to start test network: %v", err) + } + */ // Launch XUD network. xudHarness.SetLnd(lndBtcNetworkHarness, "BTC") xudHarness.SetLnd(lndLtcNetworkHarness, "LTC") - xudHarness.SetConnext(connextNetworkHarness) + // xudHarness.SetConnext(connextNetworkHarness) log.Printf("xud: launching network...") if err := xudHarness.Start(); err != nil { @@ -495,10 +501,12 @@ func launchNetwork(noBalanceChecks bool) (*xudtest.NetworkHarness, func()) { } log.Printf("ltcd: harness teared down") - if err := connextNetworkHarness.TearDownAll(); err != nil { - log.Printf("connext: cannot tear down network harness: %v", err) - } - log.Printf("connext: network harness teared down") + /* + if err := connextNetworkHarness.TearDownAll(); err != nil { + log.Printf("connext: cannot tear down network harness: %v", err) + } + log.Printf("connext: network harness teared down") + */ if err := xudHarness.TearDownAll(cfg.XudKill, cfg.XudCleanup); err != nil { log.Fatalf("cannot tear down xud network harness: %v", err) diff --git a/test/simulation/xudrpc/xudrpc.pb.go b/test/simulation/xudrpc/xudrpc.pb.go index 2c4caacab..5a7a1d24e 100644 --- a/test/simulation/xudrpc/xudrpc.pb.go +++ b/test/simulation/xudrpc/xudrpc.pb.go @@ -4805,268 +4805,268 @@ func init() { func init() { proto.RegisterFile("xudrpc.proto", fileDescriptor_6960a02cc0a63cf6) } var fileDescriptor_6960a02cc0a63cf6 = []byte{ - // 4162 bytes of a gzipped FileDescriptorProto + // 4168 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x3b, 0x4d, 0x8f, 0x1c, 0x49, - 0x56, 0x9d, 0xd5, 0x5d, 0xdd, 0xd5, 0xaf, 0x3e, 0x3b, 0xfa, 0xc3, 0xe5, 0x1a, 0xcf, 0x8c, 0x37, - 0x18, 0x8f, 0x3c, 0x9e, 0x59, 0xdb, 0x78, 0x58, 0x66, 0xc7, 0x8b, 0x47, 0xd3, 0xdd, 0xee, 0x1d, - 0x7b, 0xa6, 0xd7, 0xb6, 0xb2, 0xed, 0xb1, 0x59, 0xc1, 0xa6, 0xb2, 0x32, 0xc3, 0xdd, 0x89, 0xb3, - 0x33, 0x6b, 0x32, 0xb3, 0xdc, 0x36, 0x5c, 0xd0, 0x8a, 0x13, 0x1c, 0x38, 0x20, 0xce, 0x70, 0x42, - 0x48, 0x20, 0xae, 0x9c, 0x90, 0x38, 0x73, 0xe5, 0x00, 0x02, 0x2e, 0x48, 0xfc, 0x02, 0xc4, 0x15, - 0x09, 0xbd, 0xf8, 0xc8, 0x88, 0xc8, 0xcc, 0xea, 0xb5, 0x57, 0xc0, 0xad, 0xe2, 0xc5, 0xcb, 0xf7, - 0x22, 0xde, 0x57, 0xbc, 0xf7, 0x22, 0x0a, 0x7a, 0xaf, 0xe6, 0x61, 0x36, 0x0b, 0xae, 0xcf, 0xb2, - 0xb4, 0x48, 0xc9, 0xaa, 0x18, 0x4d, 0x36, 0xfc, 0x24, 0x49, 0x0b, 0xbf, 0x88, 0xd2, 0x24, 0x17, - 0x53, 0x74, 0x1b, 0x36, 0x77, 0xc3, 0x70, 0x7f, 0x9e, 0x65, 0x2c, 0x09, 0x5e, 0xbb, 0x2c, 0x9f, - 0xa5, 0x49, 0xce, 0xe8, 0xcf, 0x60, 0xb0, 0x1b, 0x86, 0x8f, 0xfc, 0x28, 0x73, 0xd9, 0x77, 0x73, - 0x96, 0x17, 0xe4, 0x03, 0xe8, 0x4f, 0xfd, 0x9c, 0x79, 0x81, 0x44, 0x1d, 0x3b, 0x97, 0x9d, 0xab, - 0xeb, 0xae, 0x0d, 0x24, 0x1f, 0xc2, 0xe0, 0xbb, 0x79, 0x5a, 0x18, 0x68, 0x2d, 0x8e, 0x56, 0x81, - 0xd2, 0x0d, 0x18, 0x96, 0xf4, 0x25, 0xcb, 0xbf, 0x6d, 0xc1, 0xda, 0x9e, 0x1f, 0xfb, 0x49, 0xc0, - 0x90, 0x59, 0x91, 0x16, 0x7e, 0xec, 0x4d, 0x05, 0x80, 0x33, 0x5b, 0x71, 0x6d, 0x20, 0xb9, 0x0a, - 0xc3, 0xe0, 0xc4, 0x4f, 0x12, 0xa6, 0xf1, 0x5a, 0x1c, 0xaf, 0x0a, 0x26, 0x3f, 0x84, 0x0b, 0x33, - 0x96, 0x84, 0x51, 0x72, 0xec, 0x55, 0xbf, 0x58, 0xe6, 0x5f, 0x2c, 0x9a, 0x26, 0xb7, 0x61, 0x1c, - 0x25, 0x7e, 0x50, 0x44, 0x2f, 0x59, 0xed, 0xd3, 0x15, 0xfe, 0xe9, 0xc2, 0x79, 0x14, 0xc6, 0x99, - 0x1f, 0xc7, 0xac, 0x28, 0xbf, 0x68, 0xf3, 0x2f, 0x2a, 0x50, 0xf2, 0x05, 0x4c, 0xe6, 0x49, 0x90, - 0x26, 0xcf, 0xa3, 0xec, 0x94, 0x85, 0x5e, 0xe5, 0x9b, 0x55, 0xfe, 0xcd, 0x39, 0x18, 0xf4, 0xd7, - 0x01, 0xf6, 0xfc, 0x44, 0x29, 0xea, 0x2a, 0x0c, 0x93, 0x34, 0x64, 0x5e, 0x14, 0xb2, 0xa4, 0x88, - 0x9e, 0x47, 0x2c, 0x93, 0xaa, 0xaa, 0x82, 0x69, 0x1f, 0xba, 0xfc, 0x3b, 0xa9, 0x80, 0xcf, 0xa0, - 0xbd, 0x7f, 0xe2, 0x47, 0x09, 0xd9, 0x82, 0x76, 0x80, 0x3f, 0xe4, 0x77, 0x62, 0x40, 0xc6, 0xb0, - 0x96, 0xb0, 0xe2, 0x2c, 0xcd, 0x5e, 0x48, 0x9d, 0xaa, 0x21, 0x9d, 0x41, 0x67, 0x5f, 0x6c, 0x3d, - 0x27, 0x3b, 0xb0, 0x2a, 0xa4, 0xc1, 0x3f, 0xee, 0xbb, 0x72, 0x44, 0x26, 0xd0, 0x51, 0x72, 0xe2, - 0x9f, 0xf7, 0xdd, 0x72, 0x8c, 0x94, 0xa5, 0xf8, 0xb9, 0x36, 0xfa, 0xae, 0x1a, 0x22, 0xb5, 0x20, - 0x4e, 0x73, 0x16, 0x72, 0x59, 0xf7, 0x5d, 0x39, 0xa2, 0x1e, 0x6c, 0x23, 0xc7, 0x63, 0xf6, 0xc8, - 0xcf, 0xf3, 0xb3, 0x34, 0x0b, 0xd5, 0xe6, 0x29, 0xf4, 0x12, 0x76, 0xe6, 0xcd, 0x24, 0x58, 0xee, - 0xc0, 0x82, 0x21, 0x4e, 0x1a, 0x87, 0x1a, 0x47, 0xec, 0xc6, 0x82, 0xd1, 0x31, 0xec, 0x54, 0x19, - 0x48, 0x29, 0xfd, 0x9d, 0x03, 0x9b, 0xfb, 0xb8, 0x0a, 0xb9, 0xe5, 0xb7, 0x16, 0x3b, 0x8a, 0xa2, - 0xe2, 0x1d, 0xe5, 0x18, 0x45, 0xff, 0x3c, 0xcd, 0xa4, 0x59, 0x76, 0x5c, 0x31, 0x20, 0x97, 0xa1, - 0x1b, 0xb2, 0xbc, 0x88, 0x12, 0xee, 0xba, 0x5c, 0x16, 0xeb, 0xae, 0x09, 0xe2, 0x62, 0x3f, 0x4d, - 0xe7, 0x49, 0x21, 0x4d, 0x4c, 0x8e, 0xc8, 0x08, 0x96, 0x9f, 0x33, 0x65, 0x43, 0xf8, 0x93, 0x7e, - 0x09, 0x5b, 0xf6, 0xf2, 0xc5, 0xbe, 0x70, 0xfd, 0x45, 0xe6, 0x27, 0x39, 0xea, 0x24, 0x4d, 0xbc, - 0x28, 0xcc, 0xc7, 0xce, 0xe5, 0x65, 0x5c, 0x7f, 0x05, 0x4c, 0x3f, 0x81, 0xc1, 0x7e, 0x9a, 0x24, - 0x2c, 0x28, 0xd4, 0xde, 0x27, 0xd0, 0xe1, 0x9b, 0x9c, 0x67, 0x91, 0xdc, 0x74, 0x39, 0x46, 0x4f, - 0x2f, 0xb1, 0xa5, 0x08, 0x6f, 0xc0, 0xc6, 0x7e, 0xc6, 0xfc, 0x82, 0x3d, 0x48, 0x43, 0x66, 0xd0, - 0xa8, 0x68, 0xad, 0x1c, 0xd3, 0x3f, 0x75, 0x80, 0x98, 0x5f, 0xc8, 0x25, 0xff, 0x0a, 0xf4, 0x73, - 0xc6, 0x42, 0xef, 0x34, 0x61, 0xa7, 0x69, 0x12, 0x05, 0x72, 0xc1, 0x3d, 0x04, 0xfe, 0x44, 0xc2, - 0xc8, 0x47, 0x30, 0x8a, 0x92, 0xa8, 0x88, 0xfc, 0x38, 0xfa, 0x5d, 0x16, 0x7a, 0x71, 0x12, 0xe6, - 0xe3, 0x96, 0xd8, 0x98, 0x01, 0x3f, 0x4c, 0xc2, 0x9c, 0xdc, 0x80, 0x4d, 0x13, 0x35, 0xc0, 0x65, - 0xbf, 0x2a, 0xa4, 0x2a, 0x88, 0x31, 0xb5, 0x2f, 0x66, 0xe8, 0x3f, 0x39, 0xd0, 0x51, 0xa1, 0xd3, - 0x52, 0xab, 0x53, 0x51, 0xeb, 0x1d, 0xe8, 0xe6, 0x67, 0xfe, 0xcc, 0x0b, 0xe2, 0x88, 0x25, 0x05, - 0xd7, 0xfa, 0xe0, 0xd6, 0x3b, 0xd7, 0x65, 0x90, 0x56, 0x24, 0xae, 0x1f, 0x9d, 0xf9, 0xb3, 0x7d, - 0x8e, 0xe2, 0x9a, 0xf8, 0x22, 0x1c, 0xbe, 0x60, 0x89, 0xe7, 0x87, 0x61, 0xc6, 0xf2, 0x9c, 0x2f, - 0x69, 0xdd, 0xb5, 0x81, 0x18, 0x6e, 0x42, 0x16, 0x44, 0xa7, 0x7e, 0xec, 0xcd, 0x62, 0x3f, 0x60, - 0xb9, 0x74, 0x9a, 0x0a, 0x94, 0x52, 0x00, 0xcd, 0x88, 0xac, 0xc1, 0xf2, 0xe1, 0x83, 0xbb, 0xa3, - 0x25, 0xd2, 0x85, 0xb5, 0xfd, 0x87, 0x0f, 0x1e, 0x1c, 0x3c, 0x7b, 0x3c, 0x6a, 0xa1, 0x8e, 0xef, - 0xb2, 0x59, 0x9a, 0x47, 0xa6, 0x8e, 0x17, 0x6d, 0x8f, 0x7e, 0x0c, 0xc3, 0x12, 0x5b, 0xea, 0x66, - 0x0c, 0x6b, 0x6a, 0xb1, 0x02, 0x5b, 0x0d, 0xd1, 0x00, 0xef, 0x46, 0x79, 0x90, 0xbe, 0x64, 0x19, - 0x6a, 0x33, 0x7f, 0xfb, 0xb8, 0xf5, 0x03, 0xd8, 0xae, 0x50, 0x90, 0x4c, 0x2f, 0xc1, 0x7a, 0x32, - 0x3f, 0xf5, 0x10, 0x3f, 0x97, 0xf1, 0x47, 0x03, 0xe8, 0x1f, 0x3a, 0x40, 0x0e, 0x5e, 0xb1, 0x60, - 0x5e, 0x30, 0xdc, 0xbf, 0xb1, 0xb1, 0x34, 0x0b, 0x59, 0xe6, 0x45, 0xa5, 0xe1, 0xa9, 0x31, 0x8f, - 0x4c, 0x7e, 0xc4, 0xa7, 0x64, 0xcc, 0x93, 0x43, 0x0c, 0x22, 0x33, 0xc6, 0x32, 0x6f, 0x36, 0x9f, - 0x7a, 0x2f, 0xd8, 0x6b, 0xa9, 0x11, 0x0b, 0x86, 0x94, 0xbf, 0x9b, 0xfb, 0x49, 0x11, 0x15, 0xaf, - 0xe5, 0x59, 0x51, 0x8e, 0xd1, 0x07, 0xbe, 0x62, 0x85, 0x3c, 0xef, 0xde, 0x44, 0xc6, 0x7f, 0xe9, - 0x00, 0x31, 0xbf, 0x90, 0x5b, 0xbe, 0x0b, 0x1d, 0x79, 0x0c, 0x08, 0x7f, 0xed, 0xde, 0xba, 0xaa, - 0xcc, 0xaa, 0x8e, 0x7d, 0x5d, 0x8e, 0xf3, 0x83, 0xa4, 0xc8, 0x5e, 0xbb, 0xe5, 0x97, 0x93, 0x43, - 0xe8, 0x5b, 0x53, 0x18, 0x37, 0x70, 0x57, 0x62, 0x11, 0xf8, 0x93, 0x5c, 0x81, 0xf6, 0x4b, 0x3f, - 0x9e, 0x8b, 0xe8, 0xdd, 0xbd, 0x35, 0x54, 0x5c, 0x14, 0x0b, 0x31, 0x7b, 0xbb, 0xf5, 0x43, 0x87, - 0x8e, 0x60, 0xf0, 0x15, 0x2b, 0xee, 0x27, 0xcf, 0x53, 0xb9, 0x31, 0xfa, 0x2f, 0xcb, 0x30, 0x2c, - 0x41, 0xda, 0x42, 0x5e, 0xb2, 0x2c, 0xc7, 0x80, 0x26, 0x2d, 0x44, 0x0e, 0x79, 0x10, 0x47, 0x95, - 0x2b, 0xd9, 0xca, 0x00, 0x6d, 0xc2, 0x08, 0x81, 0x95, 0x79, 0x16, 0xa1, 0x27, 0xa0, 0x2b, 0xf3, - 0xdf, 0x4a, 0xfd, 0xa8, 0x03, 0x65, 0xfb, 0x1a, 0x50, 0xce, 0xfa, 0x51, 0x96, 0xf3, 0x28, 0xa9, - 0x66, 0x11, 0x40, 0x3e, 0x86, 0x55, 0xae, 0xf5, 0x9c, 0xc7, 0xca, 0xee, 0xad, 0x4d, 0xb5, 0xbf, - 0x87, 0x1c, 0xba, 0x8f, 0xd1, 0xd4, 0x95, 0x28, 0xe4, 0x16, 0x2c, 0xc7, 0x49, 0x38, 0x5e, 0xe3, - 0xf2, 0xbe, 0x6c, 0xc8, 0xdb, 0xdc, 0xe0, 0xf5, 0xc3, 0x24, 0x14, 0x72, 0x46, 0x64, 0x8c, 0xec, - 0x7e, 0x1c, 0xf9, 0xf9, 0x78, 0x5d, 0x1c, 0xaa, 0x7c, 0x60, 0x1e, 0xaa, 0x60, 0x1d, 0xaa, 0xe4, - 0x26, 0x6c, 0xaa, 0x9c, 0x84, 0x87, 0x82, 0x13, 0x3f, 0x3f, 0x61, 0xf9, 0xb8, 0xcb, 0xf7, 0xdb, - 0x34, 0x45, 0xbe, 0x0f, 0x6b, 0x2a, 0x64, 0xf5, 0xec, 0x3d, 0xc8, 0x78, 0xc5, 0x57, 0xa7, 0x70, - 0x26, 0x5f, 0x41, 0x47, 0xad, 0xf0, 0x2d, 0xd4, 0x7d, 0x98, 0x84, 0x9c, 0x8c, 0xa1, 0xee, 0x2d, - 0x6e, 0x98, 0x2a, 0xe0, 0x2a, 0x95, 0xff, 0x08, 0x36, 0x2d, 0xa8, 0xd4, 0xfa, 0x07, 0xcd, 0x31, - 0xdb, 0x06, 0xd2, 0x2f, 0x38, 0x49, 0x74, 0x6e, 0xc3, 0x8a, 0xde, 0x22, 0x42, 0xb8, 0x9c, 0xb9, - 0xfe, 0xbe, 0x3c, 0x30, 0x86, 0x19, 0x9b, 0xcd, 0x45, 0x06, 0x7c, 0x14, 0xa4, 0x99, 0xc8, 0x52, - 0x36, 0x5c, 0xd0, 0x60, 0x3c, 0x4a, 0xa7, 0x78, 0x34, 0x0a, 0x97, 0xef, 0xb8, 0x72, 0x44, 0x2f, - 0xc0, 0xf6, 0x61, 0x94, 0x17, 0x32, 0x58, 0x47, 0x65, 0xe0, 0xa2, 0x5f, 0xc3, 0x4e, 0x75, 0x42, - 0xf2, 0xbb, 0x09, 0x10, 0x94, 0x50, 0xe9, 0x9e, 0xa3, 0x6a, 0xd4, 0x77, 0x0d, 0x1c, 0xfa, 0x0f, - 0x0e, 0x6c, 0x20, 0x31, 0x61, 0x75, 0x6a, 0xe3, 0x46, 0x18, 0x72, 0xec, 0x30, 0xf4, 0x03, 0x68, - 0xa7, 0x67, 0x09, 0xcb, 0xe4, 0x91, 0xf2, 0x7e, 0xa9, 0xa6, 0x2a, 0x8d, 0xeb, 0x0f, 0x11, 0xcd, - 0x15, 0xd8, 0x68, 0x8c, 0x71, 0x74, 0x1a, 0x15, 0x32, 0xdf, 0x12, 0x03, 0x94, 0x6f, 0x94, 0x04, - 0xf1, 0x3c, 0x64, 0x1e, 0xb7, 0x4e, 0x79, 0x82, 0x74, 0xdc, 0x2a, 0x98, 0x7e, 0x00, 0x6d, 0x4e, - 0x8f, 0x74, 0x60, 0x65, 0xef, 0xe1, 0xe3, 0x7b, 0xa3, 0x25, 0x3c, 0x47, 0x1e, 0x3e, 0x7d, 0x30, - 0x72, 0x10, 0xf4, 0xe8, 0xe0, 0xc0, 0x1d, 0xb5, 0xe8, 0x9f, 0x39, 0x40, 0xcc, 0x85, 0x48, 0xa9, - 0x7c, 0x51, 0xba, 0x9a, 0x90, 0xc8, 0x87, 0x4d, 0x8b, 0x96, 0x3e, 0x24, 0x86, 0xc2, 0x8d, 0xe4, - 0x57, 0x93, 0xfb, 0xd0, 0x35, 0xc0, 0x0d, 0xb6, 0xfb, 0x81, 0x6d, 0xbb, 0x03, 0xdb, 0x95, 0x4d, - 0xd3, 0x25, 0x30, 0x42, 0xa6, 0x58, 0x87, 0x94, 0xea, 0xfc, 0x48, 0x68, 0x40, 0xc2, 0xe4, 0x9a, - 0xb7, 0xa0, 0x2d, 0x02, 0x87, 0x30, 0x57, 0x31, 0x28, 0x3f, 0x67, 0x5a, 0xce, 0xf4, 0x33, 0xf9, - 0x39, 0x33, 0xb7, 0x4c, 0xa1, 0x2d, 0xa2, 0x92, 0xd8, 0x71, 0x4f, 0xad, 0x08, 0xb1, 0x5c, 0x31, - 0x45, 0xff, 0xcd, 0x81, 0x35, 0xe9, 0x5d, 0x68, 0x83, 0x79, 0xe1, 0x17, 0x73, 0x75, 0x78, 0xca, - 0x11, 0xf9, 0x04, 0x3a, 0xb2, 0xc8, 0xc8, 0xe5, 0xe6, 0xb4, 0x39, 0x49, 0xb8, 0x5b, 0x62, 0x90, - 0x2b, 0xb0, 0xca, 0x53, 0x77, 0x11, 0x25, 0xbb, 0xb7, 0xfa, 0x06, 0x6e, 0x94, 0xb8, 0x72, 0x12, - 0xb3, 0xcb, 0x69, 0x9c, 0x06, 0x2f, 0x4e, 0x58, 0x74, 0x7c, 0x52, 0xc8, 0xc0, 0x69, 0x82, 0xca, - 0x60, 0xdb, 0x36, 0x82, 0xad, 0x11, 0xbe, 0x57, 0xed, 0xf0, 0x5d, 0x46, 0xba, 0x35, 0x23, 0xd2, - 0xd1, 0xaf, 0x61, 0xc0, 0xfd, 0x51, 0xe7, 0xc1, 0xd5, 0x30, 0xef, 0x34, 0x84, 0xf9, 0x92, 0x56, - 0xcb, 0xa4, 0xf5, 0x17, 0x0e, 0x90, 0x87, 0x33, 0x96, 0xfc, 0x9f, 0xa4, 0xe0, 0x3a, 0x95, 0x5e, - 0xb6, 0x52, 0xe9, 0xcb, 0xd0, 0x9d, 0xcd, 0xf3, 0x13, 0x4f, 0x4e, 0x8a, 0x03, 0xdd, 0x04, 0xa9, - 0x64, 0xbb, 0xad, 0x93, 0xed, 0x3b, 0xb0, 0x69, 0xad, 0x53, 0x9a, 0xc3, 0x87, 0x30, 0xb0, 0x93, - 0x6a, 0xb9, 0xce, 0x0a, 0x94, 0xfe, 0x7d, 0x0b, 0xda, 0xdc, 0x68, 0xb9, 0xfd, 0x65, 0x91, 0x2c, - 0x84, 0x1d, 0x57, 0x0c, 0xac, 0x04, 0xa3, 0x65, 0x27, 0x18, 0x66, 0xcc, 0x58, 0xb6, 0x63, 0xc6, - 0x00, 0x5a, 0x51, 0x28, 0x8b, 0x88, 0x56, 0x14, 0x92, 0x2f, 0xeb, 0x62, 0x6b, 0x73, 0xdb, 0xda, - 0x51, 0xf6, 0x62, 0x2b, 0xae, 0x51, 0x9c, 0x71, 0x1a, 0xf8, 0x31, 0x32, 0x13, 0xc6, 0x50, 0x8e, - 0xc9, 0x7b, 0x00, 0x01, 0x4f, 0xdd, 0x43, 0xcf, 0x2f, 0xb8, 0x49, 0xac, 0xb8, 0x06, 0x84, 0x5c, - 0x81, 0x95, 0x3c, 0x0a, 0xd9, 0xb8, 0xc3, 0x03, 0xd8, 0x86, 0xe5, 0xab, 0x47, 0x51, 0xc8, 0x5c, - 0x3e, 0x8d, 0xc6, 0x12, 0xe5, 0x5e, 0x7a, 0x96, 0x78, 0x3c, 0x0a, 0xf0, 0x53, 0xb4, 0xe3, 0x5a, - 0x30, 0x34, 0xd3, 0x93, 0x34, 0x0e, 0xf9, 0x49, 0xba, 0xe2, 0xf2, 0xdf, 0xf4, 0xcf, 0x1d, 0xe8, - 0x71, 0x5a, 0x2e, 0x3b, 0x4d, 0x5f, 0xfa, 0xb1, 0x25, 0x33, 0x67, 0xb1, 0xcc, 0x2a, 0xe9, 0x9e, - 0x99, 0x24, 0x2e, 0x57, 0x92, 0x44, 0x73, 0xf7, 0x2b, 0x95, 0xdd, 0x57, 0x97, 0xdd, 0xae, 0x2f, - 0x9b, 0x9e, 0xc0, 0xaa, 0x88, 0x4c, 0xe4, 0xfb, 0x00, 0xd3, 0xf9, 0x6b, 0xcf, 0x8a, 0x8e, 0x7d, - 0x4b, 0x22, 0xae, 0x81, 0x40, 0x6e, 0x40, 0x37, 0x67, 0x71, 0xac, 0xf0, 0x5b, 0x4d, 0xf8, 0x26, - 0x06, 0xfd, 0x54, 0x45, 0x4e, 0x9e, 0xce, 0xa0, 0xbc, 0x30, 0xf4, 0xc8, 0x4c, 0x99, 0xff, 0x46, - 0x1b, 0x4e, 0xcf, 0x12, 0x59, 0xa2, 0xe3, 0x4f, 0xfa, 0x73, 0x47, 0x7e, 0xf5, 0x64, 0x16, 0xfa, - 0x05, 0xc3, 0xcc, 0x40, 0xec, 0xc5, 0xe1, 0x46, 0x62, 0xf3, 0xbb, 0xb7, 0xe4, 0x8a, 0x59, 0xf2, - 0x1b, 0xd0, 0x17, 0x12, 0xca, 0x84, 0xe0, 0x65, 0xbc, 0xda, 0xb2, 0x97, 0x27, 0xe6, 0xee, 0x2d, - 0xb9, 0x36, 0xf2, 0xde, 0x00, 0x7a, 0x02, 0x30, 0xe7, 0x4c, 0xe9, 0xbf, 0xb6, 0x60, 0x05, 0x83, - 0xe5, 0xe2, 0xba, 0xe2, 0x8d, 0xb2, 0xc6, 0x2f, 0xa1, 0x17, 0x27, 0xa1, 0x1a, 0xaa, 0xb8, 0x78, - 0xc9, 0x0c, 0xc7, 0x98, 0xe1, 0x3c, 0x9a, 0x4f, 0xbf, 0x61, 0xaf, 0xe5, 0xb1, 0x63, 0x7d, 0x81, - 0xfc, 0xa3, 0x64, 0x9a, 0xce, 0x93, 0x50, 0x9e, 0x8d, 0x6a, 0xa8, 0x8f, 0x88, 0xb6, 0x71, 0x44, - 0x60, 0xd4, 0x78, 0x35, 0x0f, 0x3d, 0x3b, 0x54, 0x9a, 0x20, 0xf2, 0x09, 0x6c, 0xe4, 0x2c, 0x48, - 0x93, 0x30, 0x17, 0x15, 0x67, 0x50, 0xb0, 0x90, 0xfb, 0x49, 0xdf, 0xad, 0x4f, 0x34, 0xa7, 0x91, - 0x93, 0x3b, 0x30, 0xac, 0x2c, 0xbb, 0xe1, 0x58, 0xdc, 0x32, 0x8f, 0xc5, 0x75, 0xf3, 0x18, 0xfc, - 0xfd, 0x16, 0x6c, 0x3c, 0xc2, 0xe2, 0x50, 0x2a, 0x45, 0x84, 0xd3, 0xff, 0xcd, 0x98, 0x63, 0xfa, - 0xcf, 0x4a, 0xc5, 0x7f, 0x54, 0x04, 0x68, 0x9f, 0x1f, 0x01, 0xae, 0xc1, 0x28, 0x63, 0xbc, 0x84, - 0xf5, 0x4a, 0x52, 0x42, 0x9c, 0x35, 0x38, 0x26, 0xcf, 0xd1, 0xe9, 0x29, 0x0b, 0x23, 0xbf, 0x40, - 0xa8, 0x17, 0x60, 0x89, 0x12, 0x73, 0xa9, 0x76, 0xdc, 0xa6, 0x29, 0x14, 0x01, 0x31, 0x45, 0x20, - 0x23, 0xf5, 0xe7, 0x30, 0x8a, 0x92, 0x82, 0x65, 0x89, 0x1f, 0x7b, 0xa7, 0x7e, 0x11, 0x9c, 0xb0, - 0x05, 0x7e, 0x59, 0x43, 0x23, 0x3f, 0x82, 0x01, 0xcf, 0xce, 0xf3, 0x79, 0x10, 0xb0, 0x1c, 0x93, - 0x29, 0xe1, 0xa0, 0x65, 0x56, 0x8e, 0x45, 0xe8, 0x91, 0x98, 0x74, 0x2b, 0xa8, 0xe4, 0x33, 0xcc, - 0x54, 0x4f, 0xfd, 0x28, 0xc1, 0x24, 0x5f, 0xb8, 0xdb, 0x72, 0x83, 0xbb, 0xb9, 0x55, 0x2c, 0xf2, - 0x39, 0xf4, 0x39, 0xa9, 0xe7, 0x7e, 0x14, 0xcf, 0x33, 0x9e, 0xc1, 0xd5, 0x98, 0xfe, 0x58, 0xcc, - 0xb9, 0x36, 0x26, 0xfd, 0x4f, 0x07, 0x86, 0x5a, 0x04, 0x07, 0x2f, 0x59, 0x82, 0xd1, 0xb9, 0xcd, - 0xf7, 0xb3, 0xd0, 0xd9, 0xf9, 0x2c, 0xf9, 0x1c, 0x7a, 0xe6, 0x06, 0xa4, 0xaf, 0x37, 0xed, 0xf4, - 0xde, 0x92, 0x6b, 0xa1, 0x92, 0xcf, 0xdf, 0x6c, 0xa7, 0xf7, 0x96, 0x9a, 0xf6, 0xda, 0x33, 0x77, - 0xc0, 0x0d, 0xab, 0x79, 0xab, 0x25, 0x57, 0x89, 0xba, 0xb7, 0x06, 0x6d, 0x86, 0x1b, 0xa4, 0x29, - 0x74, 0x8d, 0xea, 0x68, 0x61, 0xe2, 0x65, 0x84, 0x9d, 0x96, 0x1d, 0x76, 0x8c, 0x3c, 0x68, 0xa5, - 0x96, 0x07, 0x89, 0x36, 0x6a, 0xdb, 0x68, 0xa3, 0xd2, 0x4f, 0x61, 0x9b, 0x47, 0x3d, 0xa6, 0x7b, - 0xee, 0xbf, 0xb8, 0xf8, 0x1f, 0xc3, 0x4e, 0xf5, 0x23, 0xd9, 0x4b, 0x3b, 0x04, 0x22, 0x66, 0x2c, - 0xd7, 0x3d, 0xaf, 0xa7, 0x71, 0x8e, 0x03, 0xd3, 0xbf, 0x72, 0x60, 0xd3, 0x22, 0x27, 0xdd, 0xe0, - 0x3d, 0x18, 0x29, 0x1c, 0x2f, 0x4d, 0x3c, 0x7e, 0xca, 0x3a, 0xfa, 0x94, 0x25, 0xd7, 0x81, 0x68, - 0xe5, 0x54, 0xa8, 0x37, 0xcc, 0x08, 0x5f, 0x46, 0x36, 0xa1, 0xc6, 0x16, 0xd9, 0x56, 0x0d, 0x6e, - 0x06, 0x95, 0x15, 0x2b, 0xa8, 0x68, 0xa9, 0xec, 0xc6, 0xb1, 0x55, 0xec, 0xd0, 0x39, 0x5c, 0xa8, - 0xcd, 0xc8, 0xad, 0x7c, 0x02, 0x1b, 0x8a, 0x85, 0x12, 0x89, 0xca, 0xea, 0xeb, 0x13, 0x88, 0x2d, - 0xf7, 0x6b, 0x60, 0x8b, 0xf6, 0x61, 0x7d, 0x82, 0x7e, 0x1f, 0x36, 0x04, 0x5b, 0xf3, 0xe2, 0x64, - 0x61, 0xf1, 0x86, 0x85, 0xb3, 0x89, 0x2e, 0x35, 0xfa, 0x07, 0x2d, 0x04, 0xe7, 0x45, 0x9a, 0x59, - 0xfd, 0xd1, 0x37, 0x6a, 0x76, 0x9a, 0x4d, 0xd4, 0x96, 0xdd, 0x44, 0x25, 0xdf, 0x40, 0x17, 0x4f, - 0xb2, 0xa9, 0x1f, 0xbc, 0x98, 0xcf, 0xd4, 0xd1, 0x77, 0x4d, 0x39, 0x4b, 0x9d, 0x23, 0x1e, 0x84, - 0x7b, 0x02, 0x59, 0x1c, 0x84, 0x10, 0x97, 0x00, 0xf2, 0x3d, 0x7e, 0xc3, 0xe4, 0x85, 0x7e, 0xe1, - 0x4f, 0xfd, 0x5c, 0x34, 0x98, 0x7b, 0xfc, 0x5c, 0xbb, 0x2b, 0x41, 0xf2, 0x4c, 0x32, 0x29, 0xfc, - 0xa2, 0x33, 0xa9, 0x67, 0x9e, 0x49, 0x0c, 0x2d, 0xd1, 0x58, 0x93, 0xee, 0xf9, 0x66, 0x02, 0x2c, - 0x7b, 0xb9, 0x52, 0x0c, 0x0a, 0xc8, 0x1b, 0xb9, 0x1f, 0xa1, 0x79, 0x49, 0x24, 0xd5, 0x12, 0x11, - 0xc5, 0xfc, 0x50, 0xc1, 0x55, 0x0b, 0xf7, 0x2e, 0x90, 0x23, 0x56, 0x1c, 0xa6, 0xc7, 0x87, 0xec, - 0xa5, 0xae, 0x24, 0xae, 0xc3, 0x7a, 0x9c, 0x1e, 0x7b, 0x31, 0xc2, 0xf8, 0x72, 0x07, 0xba, 0xd0, - 0x2a, 0x71, 0x35, 0x0a, 0xdd, 0x86, 0x4d, 0x8b, 0x8a, 0x54, 0xe5, 0x06, 0x0c, 0x8f, 0x4e, 0xe6, - 0x45, 0x98, 0x9e, 0xa9, 0xdb, 0x19, 0x2c, 0x19, 0x35, 0x48, 0xa2, 0xfd, 0x1a, 0xec, 0x1c, 0xcd, - 0xa7, 0x79, 0x90, 0x45, 0x53, 0x66, 0x17, 0xfe, 0x13, 0xe8, 0xb0, 0x57, 0x51, 0x5e, 0x44, 0xc9, - 0x31, 0x5f, 0x46, 0xc7, 0x2d, 0xc7, 0xf4, 0x7d, 0x78, 0xb7, 0xfc, 0x0a, 0x43, 0x5d, 0xbe, 0x1b, - 0x04, 0x6c, 0x56, 0x30, 0x75, 0x17, 0x42, 0xef, 0xc0, 0xb6, 0x8d, 0x60, 0x5c, 0xe5, 0xa9, 0x82, - 0xbe, 0xf0, 0x5f, 0xc8, 0x4c, 0xae, 0xe3, 0xda, 0x40, 0xfa, 0xdf, 0x2d, 0xe8, 0xe1, 0x67, 0x8a, - 0x2c, 0xb9, 0x58, 0x0b, 0x2a, 0x6b, 0x7c, 0x7c, 0xdf, 0x4e, 0x81, 0x5b, 0x95, 0x14, 0xf8, 0xdc, - 0xa4, 0x60, 0x51, 0x7f, 0x54, 0x27, 0x1f, 0x6d, 0x33, 0xf9, 0xa8, 0x76, 0x5d, 0x57, 0x1b, 0xba, - 0xae, 0x3b, 0xb0, 0x9a, 0xf1, 0x96, 0x98, 0xac, 0x3f, 0xe5, 0x08, 0x63, 0x8e, 0xa8, 0xd3, 0xbc, - 0x8c, 0x05, 0x2c, 0x7a, 0x89, 0x32, 0xed, 0x88, 0x98, 0x53, 0x85, 0x63, 0x81, 0x26, 0x61, 0xb9, - 0xbc, 0x98, 0x5a, 0x17, 0x37, 0x77, 0x36, 0x14, 0xe3, 0x9e, 0x8a, 0xd1, 0x06, 0x55, 0xd1, 0xc9, - 0x6b, 0x98, 0xc1, 0x35, 0x94, 0x50, 0x45, 0xb9, 0x2b, 0x72, 0x98, 0x2a, 0x1c, 0x63, 0x71, 0xd7, - 0x38, 0xc2, 0x7e, 0xc9, 0x3e, 0xb5, 0x29, 0xe3, 0xe5, 0x8a, 0x8c, 0xab, 0xd2, 0x5c, 0x69, 0x90, - 0xe6, 0x87, 0x30, 0x90, 0x67, 0xa6, 0x97, 0x31, 0x3f, 0x4f, 0xd5, 0x69, 0x56, 0x81, 0xd2, 0xbf, - 0x59, 0x16, 0xab, 0x95, 0xc7, 0xfc, 0xff, 0xaf, 0xb1, 0x68, 0x95, 0xb7, 0x2d, 0x95, 0x5f, 0x85, - 0xa1, 0xa5, 0x5a, 0x16, 0x4a, 0x8d, 0x57, 0xc1, 0x98, 0xa6, 0x6b, 0xd5, 0x16, 0x52, 0xdb, 0x26, - 0xa8, 0x26, 0x2c, 0x68, 0x10, 0xd6, 0x65, 0x58, 0xc9, 0xd2, 0x98, 0x71, 0x95, 0x0e, 0x74, 0x97, - 0xc7, 0x4d, 0x63, 0xe6, 0xf2, 0x19, 0x3c, 0x4f, 0x2a, 0x66, 0xc1, 0x42, 0xde, 0xad, 0x5d, 0x77, - 0xeb, 0x13, 0xe8, 0xa8, 0xa6, 0x59, 0x14, 0xe3, 0xbe, 0xb8, 0xf7, 0xb1, 0x80, 0x58, 0x61, 0x67, - 0xde, 0x2c, 0x63, 0xd1, 0xa9, 0x7f, 0xcc, 0xc6, 0x03, 0x8e, 0x62, 0x40, 0xb4, 0x2b, 0x0d, 0x0d, - 0x57, 0xa2, 0xff, 0xd5, 0x82, 0xf6, 0xe3, 0xcc, 0x0f, 0x19, 0x96, 0x91, 0xa7, 0xe8, 0xf1, 0xde, - 0xe2, 0xb2, 0xce, 0x35, 0x31, 0xf0, 0x83, 0xc2, 0xf8, 0xa0, 0xd5, 0xf8, 0x81, 0x81, 0x61, 0xe8, - 0x67, 0xd9, 0xd2, 0xcf, 0x79, 0x3a, 0x35, 0x2c, 0xa1, 0x6d, 0x5b, 0x42, 0xb9, 0x9f, 0x55, 0x33, - 0x34, 0x28, 0xd9, 0xaf, 0x2d, 0x94, 0xfd, 0x65, 0xe8, 0x32, 0x71, 0xfd, 0xc3, 0x5b, 0x11, 0xc2, - 0x12, 0x4c, 0x50, 0x59, 0x89, 0xac, 0x9f, 0x5f, 0x89, 0xdc, 0x86, 0x5e, 0x80, 0x86, 0xc1, 0xb2, - 0x99, 0x9f, 0x15, 0xc2, 0x14, 0x16, 0x77, 0x4b, 0x2c, 0x5c, 0xfa, 0x31, 0x6c, 0x72, 0xa9, 0xdf, - 0x8b, 0xf0, 0x1c, 0x7a, 0x6d, 0xd4, 0x5a, 0xa2, 0x21, 0xeb, 0x18, 0x0d, 0x59, 0x7a, 0x07, 0xb6, - 0x6c, 0x64, 0x79, 0x08, 0x5e, 0x81, 0xd5, 0x02, 0xe1, 0xb5, 0x5a, 0x84, 0x63, 0xbb, 0x72, 0x92, - 0xfe, 0xb1, 0x03, 0x7d, 0x84, 0x44, 0xc9, 0xf1, 0x21, 0xd2, 0xcb, 0x51, 0xe0, 0xa7, 0xfe, 0x2b, - 0x2f, 0x67, 0x71, 0xac, 0x9a, 0x1f, 0x6a, 0x8c, 0x02, 0xc7, 0xdf, 0xd3, 0xb9, 0x4a, 0xdc, 0xd4, - 0x10, 0xcd, 0x30, 0x63, 0x39, 0xcb, 0x30, 0x35, 0xe2, 0x9f, 0x8a, 0x40, 0x62, 0x03, 0xd1, 0x41, - 0x4a, 0x00, 0x12, 0x11, 0x0a, 0xb5, 0x60, 0xf4, 0x96, 0xd8, 0x50, 0xb9, 0xa0, 0x37, 0xc9, 0x7d, - 0xff, 0xda, 0x81, 0xed, 0xca, 0x47, 0x52, 0x0c, 0xbb, 0xb0, 0xca, 0xe5, 0xa4, 0xc4, 0xf0, 0x91, - 0x29, 0x86, 0x1a, 0xfa, 0x75, 0x31, 0x94, 0xbd, 0x64, 0xf1, 0xe1, 0xe4, 0x11, 0x74, 0x0d, 0x70, - 0x43, 0x82, 0xf2, 0xb1, 0xdd, 0x4b, 0xde, 0x6e, 0x66, 0x61, 0xe4, 0x2d, 0xdf, 0x42, 0xef, 0x49, - 0x32, 0xfd, 0x25, 0x9e, 0x63, 0x90, 0x4b, 0xb0, 0x9e, 0x31, 0x59, 0xe9, 0xcb, 0x74, 0x45, 0x03, - 0xe8, 0x10, 0xfa, 0x92, 0xae, 0xbe, 0x45, 0x7f, 0x92, 0xc4, 0x69, 0xf0, 0xe2, 0x4d, 0x6f, 0xd1, - 0x7f, 0x0a, 0xc4, 0xfc, 0x40, 0x27, 0x54, 0x73, 0x0e, 0xad, 0x24, 0x54, 0x0a, 0xc8, 0x13, 0xaa, - 0xf7, 0xa1, 0x6b, 0xa2, 0x88, 0x4b, 0x37, 0xd0, 0x08, 0xf4, 0x8f, 0x1c, 0x18, 0x3e, 0x8d, 0x8a, - 0x93, 0x30, 0xf3, 0xcf, 0xde, 0x40, 0xa9, 0xd5, 0x17, 0x0d, 0xad, 0xf3, 0x5e, 0x34, 0x2c, 0x57, - 0x5f, 0x34, 0xf8, 0x71, 0x2c, 0x9b, 0x2f, 0xf8, 0xd3, 0x6c, 0xbb, 0xf6, 0x45, 0xdb, 0xf5, 0x36, - 0x8c, 0xf4, 0x62, 0xde, 0xae, 0xe7, 0x7a, 0xed, 0x2a, 0xac, 0x97, 0xfe, 0x4e, 0xd6, 0x60, 0x79, - 0xef, 0xc9, 0x6f, 0x8e, 0x96, 0x48, 0x07, 0x56, 0x8e, 0x0e, 0x0e, 0x0f, 0xc5, 0xf5, 0x06, 0xbf, - 0xf1, 0x68, 0x5d, 0xbb, 0x06, 0x2b, 0x18, 0x5d, 0xc8, 0x3a, 0xb4, 0x1f, 0xef, 0x7e, 0x73, 0xe0, - 0x8e, 0x96, 0xf0, 0xe7, 0x4f, 0xf8, 0x4f, 0x87, 0xf4, 0xa0, 0x73, 0xff, 0xc1, 0xe3, 0x03, 0xf7, - 0xc1, 0xee, 0xe1, 0xa8, 0x75, 0xed, 0x29, 0x74, 0x54, 0x76, 0x88, 0x48, 0xbb, 0x87, 0x07, 0xee, - 0x63, 0x81, 0x7f, 0xe0, 0xba, 0x0f, 0x5d, 0x41, 0xf7, 0xe9, 0xae, 0xfb, 0x60, 0xd4, 0xc2, 0x5f, - 0xf7, 0x1f, 0xfc, 0xf8, 0xe1, 0x68, 0x99, 0x74, 0x61, 0xed, 0xdb, 0x03, 0x77, 0xef, 0xe1, 0xd1, - 0xc1, 0x68, 0x05, 0x71, 0xef, 0x1e, 0xec, 0x3d, 0xf9, 0x6a, 0xd4, 0xe6, 0x1c, 0xdd, 0xdd, 0xfd, - 0x83, 0xd1, 0xea, 0xad, 0x7f, 0x77, 0x60, 0xed, 0xd9, 0x3c, 0xbc, 0x9f, 0x44, 0x05, 0x39, 0x00, - 0xd0, 0xaf, 0x24, 0xc8, 0xc5, 0xb2, 0xdb, 0x5f, 0x7d, 0x6b, 0x31, 0x99, 0x34, 0x4d, 0x49, 0xb3, - 0x5a, 0x22, 0xf7, 0xa0, 0x6b, 0x64, 0xde, 0x64, 0xb2, 0xb8, 0x44, 0x98, 0xbc, 0xd3, 0x38, 0x57, - 0x52, 0x3a, 0x00, 0xd0, 0x16, 0xa7, 0x17, 0x54, 0x33, 0x5b, 0xbd, 0xa0, 0xba, 0x81, 0xd2, 0xa5, - 0x5b, 0xff, 0x7c, 0x11, 0x96, 0x9f, 0xcd, 0x43, 0xf2, 0x0c, 0xba, 0xc6, 0x5b, 0x35, 0x52, 0xbb, - 0x49, 0xd3, 0xcb, 0x69, 0x7a, 0xd2, 0x36, 0xf9, 0xf9, 0x3f, 0xfe, 0xc7, 0x9f, 0xb4, 0xb6, 0xe8, - 0xf0, 0xc6, 0xcb, 0x5f, 0xbd, 0xe1, 0x87, 0xa1, 0xb2, 0xc5, 0xdb, 0xce, 0x35, 0xe2, 0xc2, 0x9a, - 0x7c, 0x8e, 0x46, 0x76, 0x0c, 0x1a, 0x46, 0x19, 0x37, 0xb9, 0x50, 0x83, 0x4b, 0xba, 0x3b, 0x9c, - 0xee, 0x88, 0x76, 0x25, 0x5d, 0x3c, 0xa6, 0x90, 0xe6, 0x1e, 0x2c, 0xef, 0xf9, 0x09, 0x21, 0xfa, - 0xa2, 0x5c, 0xc5, 0x84, 0xc9, 0xa6, 0x05, 0x93, 0x74, 0x08, 0xa7, 0xd3, 0xa3, 0x6b, 0x48, 0x67, - 0xea, 0x27, 0x48, 0xe3, 0x18, 0x06, 0xf6, 0x33, 0x24, 0xf2, 0xae, 0x79, 0xdf, 0x53, 0x7b, 0xff, - 0x34, 0x79, 0x6f, 0xd1, 0x74, 0x65, 0xb1, 0x03, 0x64, 0x12, 0x70, 0x1c, 0x8c, 0x0f, 0x24, 0x80, - 0x9e, 0xf9, 0x2a, 0x88, 0xe8, 0xb7, 0x29, 0xf5, 0xa7, 0x4e, 0x93, 0x4b, 0xcd, 0x93, 0x92, 0xc5, - 0x98, 0xb3, 0x20, 0x74, 0xc4, 0x59, 0x20, 0x86, 0xbc, 0x90, 0x42, 0x29, 0xcb, 0xa7, 0x40, 0x5a, - 0xca, 0xf6, 0x4b, 0x22, 0x2d, 0xe5, 0xea, 0x9b, 0x21, 0x4b, 0xca, 0x32, 0x26, 0xa2, 0x84, 0x7e, - 0x06, 0xfd, 0xa7, 0xfc, 0x35, 0x9c, 0x7c, 0x80, 0xa2, 0x29, 0xdb, 0xef, 0x57, 0x34, 0xe5, 0xca, - 0x4b, 0x15, 0x7a, 0x89, 0x53, 0xde, 0xa1, 0x1b, 0x48, 0x59, 0xbc, 0xac, 0x0b, 0x05, 0x0a, 0xd2, - 0xff, 0x1d, 0xe8, 0x5b, 0x6f, 0x4d, 0x48, 0xb9, 0xf9, 0xa6, 0x47, 0x2c, 0x93, 0x77, 0x17, 0xcc, - 0x36, 0xf1, 0x0a, 0x25, 0x0a, 0x7f, 0x9d, 0x82, 0xbc, 0x9e, 0x01, 0xe8, 0x37, 0x1b, 0xda, 0x5d, - 0x6a, 0xef, 0x44, 0xb4, 0xbb, 0xd4, 0x9f, 0x78, 0xd0, 0x4d, 0xce, 0xa2, 0x4f, 0xba, 0xc2, 0x8c, - 0x04, 0xad, 0x43, 0x58, 0x93, 0xaf, 0x13, 0xb4, 0x7c, 0xec, 0x27, 0x1a, 0x5a, 0x3e, 0x95, 0x67, - 0x0c, 0x74, 0xc4, 0x09, 0x02, 0xe9, 0x20, 0xc1, 0x08, 0x49, 0xfc, 0x16, 0x74, 0x8d, 0xab, 0x7d, - 0x62, 0xae, 0xa6, 0xf2, 0x0a, 0x40, 0x7b, 0x64, 0xc3, 0x5b, 0x00, 0xba, 0xc5, 0x29, 0x0f, 0x48, - 0x0f, 0x29, 0xab, 0xbe, 0x86, 0xa4, 0xae, 0xee, 0xee, 0x2d, 0xea, 0x95, 0x07, 0x01, 0x16, 0xf5, - 0xea, 0x65, 0xbf, 0x4d, 0x1d, 0x65, 0xcc, 0xd7, 0xfe, 0x14, 0x40, 0x5f, 0x33, 0x6b, 0x19, 0xd7, - 0xee, 0xcb, 0xb5, 0x8c, 0xeb, 0xb7, 0xd2, 0xca, 0x55, 0x09, 0x20, 0x69, 0x79, 0x19, 0x73, 0x0c, - 0x03, 0xfb, 0x15, 0x80, 0x76, 0xd5, 0xc6, 0x67, 0x03, 0xda, 0x55, 0x9b, 0x1f, 0x0f, 0x28, 0x8b, - 0x27, 0xc2, 0x55, 0x35, 0xd9, 0x23, 0x58, 0x2f, 0xef, 0xa7, 0xc9, 0xd8, 0x24, 0x62, 0x5e, 0x63, - 0x4f, 0x2e, 0x36, 0xcc, 0xa8, 0xb6, 0x04, 0xa7, 0xdc, 0x25, 0xeb, 0x48, 0x59, 0x5c, 0x53, 0x28, - 0xa2, 0xfc, 0xa5, 0x8c, 0x4d, 0xd4, 0xb8, 0xdc, 0xae, 0x10, 0x35, 0xaf, 0xb8, 0x2b, 0x44, 0x39, - 0x1d, 0x0f, 0xba, 0xc6, 0xed, 0xa7, 0xd6, 0x64, 0xfd, 0xea, 0x56, 0x6b, 0xb2, 0xe1, 0xba, 0x94, - 0x5e, 0xe0, 0xa4, 0x37, 0x44, 0xe4, 0x4e, 0x67, 0x2c, 0x51, 0x01, 0xe5, 0xb7, 0x01, 0x74, 0xc3, - 0x5a, 0x2b, 0xb3, 0x76, 0x95, 0xa1, 0x8d, 0xbb, 0xd2, 0xdf, 0xa6, 0x17, 0x39, 0xe9, 0x4d, 0x11, - 0x0f, 0xf9, 0x25, 0x02, 0x57, 0xe7, 0x6d, 0xe7, 0xda, 0x4d, 0x87, 0x3c, 0x87, 0x81, 0xc6, 0x3f, - 0x7a, 0x9d, 0x04, 0xe7, 0xb1, 0x98, 0x34, 0x4d, 0xc9, 0x0d, 0xbc, 0xcb, 0xb9, 0x5c, 0xa0, 0xc4, - 0xe6, 0x92, 0xbf, 0x4e, 0x02, 0xf4, 0xfb, 0x9f, 0x42, 0xd7, 0x78, 0x97, 0xa6, 0xe5, 0x54, 0x7f, - 0xac, 0x36, 0x69, 0x6a, 0xa9, 0xdb, 0x27, 0x9b, 0xac, 0x67, 0xf2, 0x33, 0x7f, 0x86, 0xb4, 0x13, - 0x18, 0xd8, 0x9d, 0x63, 0x6d, 0x96, 0x8d, 0x6d, 0x68, 0x6d, 0x96, 0x0b, 0x1a, 0xce, 0xd6, 0x5e, - 0x44, 0xc3, 0xd4, 0x3c, 0x49, 0xa7, 0x98, 0x3c, 0x94, 0x0d, 0x64, 0x33, 0x79, 0xa8, 0x36, 0xa9, - 0xcd, 0xe4, 0xa1, 0xd6, 0x71, 0xb6, 0xf7, 0x24, 0xd8, 0x28, 0xcd, 0x90, 0x0c, 0x86, 0x95, 0xee, - 0x2e, 0xa9, 0xac, 0xba, 0xda, 0x10, 0x9e, 0xbc, 0xbf, 0x70, 0x5e, 0xf2, 0x7b, 0x8f, 0xf3, 0x1b, - 0xd3, 0x4d, 0xcd, 0xcf, 0x8f, 0x63, 0xa1, 0x26, 0x71, 0xce, 0x80, 0xee, 0xd5, 0x6a, 0x3b, 0xa8, - 0xb5, 0x7b, 0x27, 0x93, 0xa6, 0x29, 0xc9, 0xc4, 0xb2, 0x36, 0xc1, 0x44, 0x65, 0x0b, 0x53, 0xe8, - 0x1a, 0x1d, 0x44, 0x2d, 0xb7, 0x7a, 0x73, 0x52, 0xcb, 0xad, 0xa9, 0xe5, 0x68, 0xc9, 0x2d, 0x67, - 0x45, 0x9c, 0x1e, 0xf3, 0x16, 0x25, 0xf2, 0xf8, 0x16, 0x3a, 0xaa, 0xf7, 0x48, 0x4a, 0x8f, 0xa8, - 0x34, 0x28, 0x27, 0xe3, 0xfa, 0x44, 0xc5, 0x0d, 0x79, 0x40, 0xcd, 0xe5, 0x2c, 0xd2, 0x65, 0x30, - 0xac, 0xf4, 0x2f, 0xb5, 0x3e, 0x9a, 0x1b, 0x9b, 0x13, 0xfb, 0x79, 0x9d, 0xb8, 0x59, 0xa6, 0xef, - 0x70, 0x06, 0xdb, 0x84, 0xeb, 0x20, 0x57, 0x1f, 0x0a, 0x1d, 0xdc, 0x74, 0xc8, 0xac, 0xd2, 0xcf, - 0x94, 0x8d, 0x31, 0x23, 0xd0, 0x36, 0xb6, 0x3b, 0x27, 0x4d, 0x17, 0x42, 0xf4, 0x7b, 0x9c, 0xd7, - 0x3b, 0xe4, 0xa2, 0xc5, 0x0b, 0xbd, 0x46, 0xdd, 0x87, 0xdd, 0x74, 0xc8, 0x14, 0x06, 0x36, 0xc9, - 0xb7, 0x62, 0x55, 0x71, 0x4f, 0x42, 0x6a, 0xac, 0x90, 0xc7, 0xef, 0x19, 0xcd, 0x5f, 0xab, 0x8d, - 0x4b, 0xae, 0x34, 0xf3, 0xaa, 0xb4, 0x79, 0x27, 0x5b, 0x26, 0x4f, 0x35, 0x49, 0x29, 0x67, 0x7a, - 0x89, 0x4c, 0xea, 0x4c, 0x7d, 0x89, 0xc3, 0x23, 0x5c, 0xcf, 0x6c, 0x30, 0xe8, 0xb4, 0xaf, 0xa1, - 0x47, 0xa1, 0xd3, 0xbe, 0xa6, 0x9e, 0x84, 0x52, 0x9e, 0x48, 0xfb, 0x78, 0x03, 0xe2, 0x44, 0x60, - 0x88, 0x3c, 0xb6, 0xd2, 0x88, 0xb8, 0xb4, 0xa0, 0x54, 0xaf, 0x64, 0x51, 0x8d, 0x85, 0xbc, 0x72, - 0x23, 0xb2, 0xa1, 0x58, 0x45, 0xc9, 0xb1, 0xa8, 0xe7, 0xc9, 0xd7, 0xd0, 0xe6, 0x55, 0x32, 0xd9, - 0xd2, 0x15, 0x85, 0x2e, 0xc6, 0x27, 0xdb, 0x15, 0xa8, 0x9d, 0x2a, 0x50, 0x7e, 0x76, 0xcd, 0x13, - 0x99, 0x7c, 0x4f, 0x61, 0x20, 0x52, 0x4b, 0x55, 0x4b, 0x6a, 0xa7, 0xa9, 0x94, 0xba, 0xda, 0x69, - 0xaa, 0x65, 0xa7, 0x1d, 0x2e, 0x45, 0x76, 0x79, 0x26, 0x71, 0x6e, 0x3b, 0xd7, 0xa6, 0xab, 0xfc, - 0x5f, 0x38, 0x9f, 0xfe, 0x4f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7a, 0x9c, 0x44, 0x52, 0xb0, 0x33, - 0x00, 0x00, + 0x56, 0x9d, 0xd5, 0x5d, 0xdd, 0xd5, 0xaf, 0x3e, 0x3b, 0xfa, 0xc3, 0x35, 0x35, 0x9e, 0x19, 0x6f, + 0x30, 0x1e, 0x79, 0x3c, 0x33, 0xb6, 0xf1, 0xb0, 0xcc, 0x8e, 0x17, 0x8f, 0xa6, 0xbb, 0xdd, 0x3b, + 0xf6, 0x4c, 0xaf, 0x6d, 0x65, 0xdb, 0x63, 0xb3, 0x82, 0x4d, 0x65, 0x65, 0x86, 0xbb, 0x13, 0x67, + 0x67, 0xd6, 0x64, 0x66, 0xb9, 0x6d, 0xb8, 0xa0, 0x15, 0x27, 0x38, 0x70, 0x40, 0x9c, 0xe1, 0x84, + 0x90, 0x58, 0x71, 0xe5, 0x84, 0xc4, 0x99, 0x2b, 0x07, 0x24, 0xe0, 0x82, 0xc4, 0x2f, 0x40, 0x5c, + 0x91, 0xd0, 0x8b, 0x8f, 0x8c, 0x88, 0xcc, 0xac, 0x5e, 0x7b, 0x04, 0xdc, 0x2a, 0x5e, 0xbc, 0x7c, + 0x2f, 0xe2, 0x7d, 0xc7, 0x8b, 0x28, 0xe8, 0xbd, 0x9c, 0x87, 0xd9, 0x2c, 0xb8, 0x36, 0xcb, 0xd2, + 0x22, 0x25, 0xab, 0x62, 0x34, 0xd9, 0xf0, 0x93, 0x24, 0x2d, 0xfc, 0x22, 0x4a, 0x93, 0x5c, 0x4c, + 0xd1, 0x6d, 0xd8, 0xdc, 0x0d, 0xc3, 0xfd, 0x79, 0x96, 0xb1, 0x24, 0x78, 0xe5, 0xb2, 0x7c, 0x96, + 0x26, 0x39, 0xa3, 0x3f, 0x87, 0xc1, 0x6e, 0x18, 0x3e, 0xf4, 0xa3, 0xcc, 0x65, 0xdf, 0xcd, 0x59, + 0x5e, 0x90, 0xf7, 0xa1, 0x3f, 0xf5, 0x73, 0xe6, 0x05, 0x12, 0x75, 0xec, 0x5c, 0x72, 0xae, 0xac, + 0xbb, 0x36, 0x90, 0x7c, 0x00, 0x83, 0xef, 0xe6, 0x69, 0x61, 0xa0, 0xb5, 0x38, 0x5a, 0x05, 0x4a, + 0x37, 0x60, 0x58, 0xd2, 0x97, 0x2c, 0xff, 0xae, 0x05, 0x6b, 0x7b, 0x7e, 0xec, 0x27, 0x01, 0x43, + 0x66, 0x45, 0x5a, 0xf8, 0xb1, 0x37, 0x15, 0x00, 0xce, 0x6c, 0xc5, 0xb5, 0x81, 0xe4, 0x0a, 0x0c, + 0x83, 0x13, 0x3f, 0x49, 0x98, 0xc6, 0x6b, 0x71, 0xbc, 0x2a, 0x98, 0xfc, 0x08, 0x2e, 0xcc, 0x58, + 0x12, 0x46, 0xc9, 0xb1, 0x57, 0xfd, 0x62, 0x99, 0x7f, 0xb1, 0x68, 0x9a, 0xdc, 0x82, 0x71, 0x94, + 0xf8, 0x41, 0x11, 0xbd, 0x60, 0xb5, 0x4f, 0x57, 0xf8, 0xa7, 0x0b, 0xe7, 0x51, 0x18, 0x67, 0x7e, + 0x1c, 0xb3, 0xa2, 0xfc, 0xa2, 0xcd, 0xbf, 0xa8, 0x40, 0xc9, 0x17, 0x30, 0x99, 0x27, 0x41, 0x9a, + 0x3c, 0x8b, 0xb2, 0x53, 0x16, 0x7a, 0x95, 0x6f, 0x56, 0xf9, 0x37, 0xe7, 0x60, 0xd0, 0xdf, 0x04, + 0xd8, 0xf3, 0x13, 0xa5, 0xa8, 0x2b, 0x30, 0x4c, 0xd2, 0x90, 0x79, 0x51, 0xc8, 0x92, 0x22, 0x7a, + 0x16, 0xb1, 0x4c, 0xaa, 0xaa, 0x0a, 0xa6, 0x7d, 0xe8, 0xf2, 0xef, 0xa4, 0x02, 0x3e, 0x83, 0xf6, + 0xfe, 0x89, 0x1f, 0x25, 0x64, 0x0b, 0xda, 0x01, 0xfe, 0x90, 0xdf, 0x89, 0x01, 0x19, 0xc3, 0x5a, + 0xc2, 0x8a, 0xb3, 0x34, 0x7b, 0x2e, 0x75, 0xaa, 0x86, 0x74, 0x06, 0x9d, 0x7d, 0xb1, 0xf5, 0x9c, + 0xec, 0xc0, 0xaa, 0x90, 0x06, 0xff, 0xb8, 0xef, 0xca, 0x11, 0x99, 0x40, 0x47, 0xc9, 0x89, 0x7f, + 0xde, 0x77, 0xcb, 0x31, 0x52, 0x96, 0xe2, 0xe7, 0xda, 0xe8, 0xbb, 0x6a, 0x88, 0xd4, 0x82, 0x38, + 0xcd, 0x59, 0xc8, 0x65, 0xdd, 0x77, 0xe5, 0x88, 0x7a, 0xb0, 0x8d, 0x1c, 0x8f, 0xd9, 0x43, 0x3f, + 0xcf, 0xcf, 0xd2, 0x2c, 0x54, 0x9b, 0xa7, 0xd0, 0x4b, 0xd8, 0x99, 0x37, 0x93, 0x60, 0xb9, 0x03, + 0x0b, 0x86, 0x38, 0x69, 0x1c, 0x6a, 0x1c, 0xb1, 0x1b, 0x0b, 0x46, 0xc7, 0xb0, 0x53, 0x65, 0x20, + 0xa5, 0xf4, 0xf7, 0x0e, 0x6c, 0xee, 0xe3, 0x2a, 0xe4, 0x96, 0xdf, 0x58, 0xec, 0x28, 0x8a, 0x8a, + 0x77, 0x94, 0x63, 0x14, 0xfd, 0xb3, 0x34, 0x93, 0x66, 0xd9, 0x71, 0xc5, 0x80, 0x5c, 0x82, 0x6e, + 0xc8, 0xf2, 0x22, 0x4a, 0xb8, 0xeb, 0x72, 0x59, 0xac, 0xbb, 0x26, 0x88, 0x8b, 0xfd, 0x34, 0x9d, + 0x27, 0x85, 0x34, 0x31, 0x39, 0x22, 0x23, 0x58, 0x7e, 0xc6, 0x94, 0x0d, 0xe1, 0x4f, 0xfa, 0x25, + 0x6c, 0xd9, 0xcb, 0x17, 0xfb, 0xc2, 0xf5, 0x17, 0x99, 0x9f, 0xe4, 0xa8, 0x93, 0x34, 0xf1, 0xa2, + 0x30, 0x1f, 0x3b, 0x97, 0x96, 0x71, 0xfd, 0x15, 0x30, 0xfd, 0x18, 0x06, 0xfb, 0x69, 0x92, 0xb0, + 0xa0, 0x50, 0x7b, 0x9f, 0x40, 0x87, 0x6f, 0x72, 0x9e, 0x45, 0x72, 0xd3, 0xe5, 0x18, 0x3d, 0xbd, + 0xc4, 0x96, 0x22, 0xbc, 0x0e, 0x1b, 0xfb, 0x19, 0xf3, 0x0b, 0x76, 0x3f, 0x0d, 0x99, 0x41, 0xa3, + 0xa2, 0xb5, 0x72, 0x4c, 0xff, 0xdc, 0x01, 0x62, 0x7e, 0x21, 0x97, 0xfc, 0x6b, 0xd0, 0xcf, 0x19, + 0x0b, 0xbd, 0xd3, 0x84, 0x9d, 0xa6, 0x49, 0x14, 0xc8, 0x05, 0xf7, 0x10, 0xf8, 0x53, 0x09, 0x23, + 0x1f, 0xc2, 0x28, 0x4a, 0xa2, 0x22, 0xf2, 0xe3, 0xe8, 0xf7, 0x59, 0xe8, 0xc5, 0x49, 0x98, 0x8f, + 0x5b, 0x62, 0x63, 0x06, 0xfc, 0x30, 0x09, 0x73, 0x72, 0x1d, 0x36, 0x4d, 0xd4, 0x00, 0x97, 0xfd, + 0xb2, 0x90, 0xaa, 0x20, 0xc6, 0xd4, 0xbe, 0x98, 0xa1, 0xff, 0xec, 0x40, 0x47, 0x85, 0x4e, 0x4b, + 0xad, 0x4e, 0x45, 0xad, 0xb7, 0xa1, 0x9b, 0x9f, 0xf9, 0x33, 0x2f, 0x88, 0x23, 0x96, 0x14, 0x5c, + 0xeb, 0x83, 0x9b, 0x6f, 0x5f, 0x93, 0x41, 0x5a, 0x91, 0xb8, 0x76, 0x74, 0xe6, 0xcf, 0xf6, 0x39, + 0x8a, 0x6b, 0xe2, 0x8b, 0x70, 0xf8, 0x9c, 0x25, 0x9e, 0x1f, 0x86, 0x19, 0xcb, 0x73, 0xbe, 0xa4, + 0x75, 0xd7, 0x06, 0x62, 0xb8, 0x09, 0x59, 0x10, 0x9d, 0xfa, 0xb1, 0x37, 0x8b, 0xfd, 0x80, 0xe5, + 0xd2, 0x69, 0x2a, 0x50, 0x4a, 0x01, 0x34, 0x23, 0xb2, 0x06, 0xcb, 0x87, 0xf7, 0xef, 0x8c, 0x96, + 0x48, 0x17, 0xd6, 0xf6, 0x1f, 0xdc, 0xbf, 0x7f, 0xf0, 0xf4, 0xd1, 0xa8, 0x85, 0x3a, 0xbe, 0xc3, + 0x66, 0x69, 0x1e, 0x99, 0x3a, 0x5e, 0xb4, 0x3d, 0xfa, 0x11, 0x0c, 0x4b, 0x6c, 0xa9, 0x9b, 0x31, + 0xac, 0xa9, 0xc5, 0x0a, 0x6c, 0x35, 0x44, 0x03, 0xbc, 0x13, 0xe5, 0x41, 0xfa, 0x82, 0x65, 0xa8, + 0xcd, 0xfc, 0xcd, 0xe3, 0xd6, 0x0f, 0x61, 0xbb, 0x42, 0x41, 0x32, 0xbd, 0x08, 0xeb, 0xc9, 0xfc, + 0xd4, 0x43, 0xfc, 0x5c, 0xc6, 0x1f, 0x0d, 0xa0, 0x7f, 0xec, 0x00, 0x39, 0x78, 0xc9, 0x82, 0x79, + 0xc1, 0x70, 0xff, 0xc6, 0xc6, 0xd2, 0x2c, 0x64, 0x99, 0x17, 0x95, 0x86, 0xa7, 0xc6, 0x3c, 0x32, + 0xf9, 0x11, 0x9f, 0x92, 0x31, 0x4f, 0x0e, 0x31, 0x88, 0xcc, 0x18, 0xcb, 0xbc, 0xd9, 0x7c, 0xea, + 0x3d, 0x67, 0xaf, 0xa4, 0x46, 0x2c, 0x18, 0x52, 0xfe, 0x6e, 0xee, 0x27, 0x45, 0x54, 0xbc, 0x92, + 0xb9, 0xa2, 0x1c, 0xa3, 0x0f, 0x7c, 0xc5, 0x0a, 0x99, 0xef, 0x5e, 0x47, 0xc6, 0x7f, 0xed, 0x00, + 0x31, 0xbf, 0x90, 0x5b, 0xbe, 0x03, 0x1d, 0x99, 0x06, 0x84, 0xbf, 0x76, 0x6f, 0x5e, 0x51, 0x66, + 0x55, 0xc7, 0xbe, 0x26, 0xc7, 0xf9, 0x41, 0x52, 0x64, 0xaf, 0xdc, 0xf2, 0xcb, 0xc9, 0x21, 0xf4, + 0xad, 0x29, 0x8c, 0x1b, 0xb8, 0x2b, 0xb1, 0x08, 0xfc, 0x49, 0x2e, 0x43, 0xfb, 0x85, 0x1f, 0xcf, + 0x45, 0xf4, 0xee, 0xde, 0x1c, 0x2a, 0x2e, 0x8a, 0x85, 0x98, 0xbd, 0xd5, 0xfa, 0x91, 0x43, 0x47, + 0x30, 0xf8, 0x8a, 0x15, 0xf7, 0x92, 0x67, 0xa9, 0xdc, 0x18, 0xfd, 0x97, 0x65, 0x18, 0x96, 0x20, + 0x6d, 0x21, 0x2f, 0x58, 0x96, 0x63, 0x40, 0x93, 0x16, 0x22, 0x87, 0x3c, 0x88, 0xa3, 0xca, 0x95, + 0x6c, 0x65, 0x80, 0x36, 0x61, 0x84, 0xc0, 0xca, 0x3c, 0x8b, 0xd0, 0x13, 0xd0, 0x95, 0xf9, 0x6f, + 0xa5, 0x7e, 0xd4, 0x81, 0xb2, 0x7d, 0x0d, 0x28, 0x67, 0xfd, 0x28, 0xcb, 0x79, 0x94, 0x54, 0xb3, + 0x08, 0x20, 0x1f, 0xc1, 0x2a, 0xd7, 0x7a, 0xce, 0x63, 0x65, 0xf7, 0xe6, 0xa6, 0xda, 0xdf, 0x03, + 0x0e, 0xdd, 0xc7, 0x68, 0xea, 0x4a, 0x14, 0x72, 0x13, 0x96, 0xe3, 0x24, 0x1c, 0xaf, 0x71, 0x79, + 0x5f, 0x32, 0xe4, 0x6d, 0x6e, 0xf0, 0xda, 0x61, 0x12, 0x0a, 0x39, 0x23, 0x32, 0x46, 0x76, 0x3f, + 0x8e, 0xfc, 0x7c, 0xbc, 0x2e, 0x92, 0x2a, 0x1f, 0x98, 0x49, 0x15, 0xac, 0xa4, 0x4a, 0x6e, 0xc0, + 0xa6, 0xaa, 0x49, 0x78, 0x28, 0x38, 0xf1, 0xf3, 0x13, 0x96, 0x8f, 0xbb, 0x7c, 0xbf, 0x4d, 0x53, + 0xe4, 0x13, 0x58, 0x53, 0x21, 0xab, 0x67, 0xef, 0x41, 0xc6, 0x2b, 0xbe, 0x3a, 0x85, 0x33, 0xf9, + 0x0a, 0x3a, 0x6a, 0x85, 0x6f, 0xa0, 0xee, 0xc3, 0x24, 0xe4, 0x64, 0x0c, 0x75, 0x6f, 0x71, 0xc3, + 0x54, 0x01, 0x57, 0xa9, 0xfc, 0xc7, 0xb0, 0x69, 0x41, 0xa5, 0xd6, 0xdf, 0x6f, 0x8e, 0xd9, 0x36, + 0x90, 0x7e, 0xc1, 0x49, 0xa2, 0x73, 0x1b, 0x56, 0xf4, 0x06, 0x11, 0xc2, 0xe5, 0xcc, 0xf5, 0xf7, + 0x65, 0xc2, 0x18, 0x66, 0x6c, 0x36, 0x17, 0x15, 0xf0, 0x51, 0x90, 0x66, 0xa2, 0x4a, 0xd9, 0x70, + 0x41, 0x83, 0x31, 0x95, 0x4e, 0x31, 0x35, 0x0a, 0x97, 0xef, 0xb8, 0x72, 0x44, 0x2f, 0xc0, 0xf6, + 0x61, 0x94, 0x17, 0x32, 0x58, 0x47, 0x65, 0xe0, 0xa2, 0x5f, 0xc3, 0x4e, 0x75, 0x42, 0xf2, 0xbb, + 0x01, 0x10, 0x94, 0x50, 0xe9, 0x9e, 0xa3, 0x6a, 0xd4, 0x77, 0x0d, 0x1c, 0xfa, 0x8f, 0x0e, 0x6c, + 0x20, 0x31, 0x61, 0x75, 0x6a, 0xe3, 0x46, 0x18, 0x72, 0xec, 0x30, 0xf4, 0x43, 0x68, 0xa7, 0x67, + 0x09, 0xcb, 0x64, 0x4a, 0x79, 0xaf, 0x54, 0x53, 0x95, 0xc6, 0xb5, 0x07, 0x88, 0xe6, 0x0a, 0x6c, + 0x34, 0xc6, 0x38, 0x3a, 0x8d, 0x0a, 0x59, 0x6f, 0x89, 0x01, 0xca, 0x37, 0x4a, 0x82, 0x78, 0x1e, + 0x32, 0x8f, 0x5b, 0xa7, 0xcc, 0x20, 0x1d, 0xb7, 0x0a, 0xa6, 0xef, 0x43, 0x9b, 0xd3, 0x23, 0x1d, + 0x58, 0xd9, 0x7b, 0xf0, 0xe8, 0xee, 0x68, 0x09, 0xf3, 0xc8, 0x83, 0x27, 0xf7, 0x47, 0x0e, 0x82, + 0x1e, 0x1e, 0x1c, 0xb8, 0xa3, 0x16, 0xfd, 0x0b, 0x07, 0x88, 0xb9, 0x10, 0x29, 0x95, 0x2f, 0x4a, + 0x57, 0x13, 0x12, 0xf9, 0xa0, 0x69, 0xd1, 0xd2, 0x87, 0xc4, 0x50, 0xb8, 0x91, 0xfc, 0x6a, 0x72, + 0x0f, 0xba, 0x06, 0xb8, 0xc1, 0x76, 0xdf, 0xb7, 0x6d, 0x77, 0x60, 0xbb, 0xb2, 0x69, 0xba, 0x04, + 0x46, 0xc8, 0x14, 0xcf, 0x21, 0xa5, 0x3a, 0x3f, 0x14, 0x1a, 0x90, 0x30, 0xb9, 0xe6, 0x2d, 0x68, + 0x8b, 0xc0, 0x21, 0xcc, 0x55, 0x0c, 0xca, 0xcf, 0x99, 0x96, 0x33, 0xfd, 0x4c, 0x7e, 0xce, 0xcc, + 0x2d, 0x53, 0x68, 0x8b, 0xa8, 0x24, 0x76, 0xdc, 0x53, 0x2b, 0x42, 0x2c, 0x57, 0x4c, 0xd1, 0x7f, + 0x73, 0x60, 0x4d, 0x7a, 0x17, 0xda, 0x60, 0x5e, 0xf8, 0xc5, 0x5c, 0x25, 0x4f, 0x39, 0x22, 0x1f, + 0x43, 0x47, 0x1e, 0x32, 0x72, 0xb9, 0x39, 0x6d, 0x4e, 0x12, 0xee, 0x96, 0x18, 0xe4, 0x32, 0xac, + 0xf2, 0xd2, 0x5d, 0x44, 0xc9, 0xee, 0xcd, 0xbe, 0x81, 0x1b, 0x25, 0xae, 0x9c, 0xc4, 0xea, 0x72, + 0x1a, 0xa7, 0xc1, 0xf3, 0x13, 0x16, 0x1d, 0x9f, 0x14, 0x32, 0x70, 0x9a, 0xa0, 0x32, 0xd8, 0xb6, + 0x8d, 0x60, 0x6b, 0x84, 0xef, 0x55, 0x3b, 0x7c, 0x97, 0x91, 0x6e, 0xcd, 0x88, 0x74, 0xf4, 0x6b, + 0x18, 0x70, 0x7f, 0xd4, 0x75, 0x70, 0x35, 0xcc, 0x3b, 0x0d, 0x61, 0xbe, 0xa4, 0xd5, 0x32, 0x69, + 0xfd, 0x95, 0x03, 0xe4, 0xc1, 0x8c, 0x25, 0xff, 0x27, 0x25, 0xb8, 0x2e, 0xa5, 0x97, 0xad, 0x52, + 0xfa, 0x12, 0x74, 0x67, 0xf3, 0xfc, 0xc4, 0x93, 0x93, 0x22, 0xa1, 0x9b, 0x20, 0x55, 0x6c, 0xb7, + 0x75, 0xb1, 0x7d, 0x1b, 0x36, 0xad, 0x75, 0x4a, 0x73, 0xf8, 0x00, 0x06, 0x76, 0x51, 0x2d, 0xd7, + 0x59, 0x81, 0xd2, 0x7f, 0x68, 0x41, 0x9b, 0x1b, 0x2d, 0xb7, 0xbf, 0x2c, 0x92, 0x07, 0x61, 0xc7, + 0x15, 0x03, 0xab, 0xc0, 0x68, 0xd9, 0x05, 0x86, 0x19, 0x33, 0x96, 0xed, 0x98, 0x31, 0x80, 0x56, + 0x14, 0xca, 0x43, 0x44, 0x2b, 0x0a, 0xc9, 0x97, 0x75, 0xb1, 0xb5, 0xb9, 0x6d, 0xed, 0x28, 0x7b, + 0xb1, 0x15, 0xd7, 0x28, 0xce, 0x38, 0x0d, 0xfc, 0x18, 0x99, 0x09, 0x63, 0x28, 0xc7, 0xe4, 0x5d, + 0x80, 0x80, 0x97, 0xee, 0xa1, 0xe7, 0x17, 0xdc, 0x24, 0x56, 0x5c, 0x03, 0x42, 0x2e, 0xc3, 0x4a, + 0x1e, 0x85, 0x6c, 0xdc, 0xe1, 0x01, 0x6c, 0xc3, 0xf2, 0xd5, 0xa3, 0x28, 0x64, 0x2e, 0x9f, 0x46, + 0x63, 0x89, 0x72, 0x2f, 0x3d, 0x4b, 0x3c, 0x1e, 0x05, 0x78, 0x16, 0xed, 0xb8, 0x16, 0x0c, 0xcd, + 0xf4, 0x24, 0x8d, 0x43, 0x9e, 0x49, 0x57, 0x5c, 0xfe, 0x9b, 0xfe, 0xa5, 0x03, 0x3d, 0x4e, 0xcb, + 0x65, 0xa7, 0xe9, 0x0b, 0x3f, 0xb6, 0x64, 0xe6, 0x2c, 0x96, 0x59, 0xa5, 0xdc, 0x33, 0x8b, 0xc4, + 0xe5, 0x4a, 0x91, 0x68, 0xee, 0x7e, 0xa5, 0xb2, 0xfb, 0xea, 0xb2, 0xdb, 0xf5, 0x65, 0xd3, 0x13, + 0x58, 0x15, 0x91, 0x89, 0x7c, 0x02, 0x30, 0x9d, 0xbf, 0xf2, 0xac, 0xe8, 0xd8, 0xb7, 0x24, 0xe2, + 0x1a, 0x08, 0xe4, 0x3a, 0x74, 0x73, 0x16, 0xc7, 0x0a, 0xbf, 0xd5, 0x84, 0x6f, 0x62, 0xd0, 0x4f, + 0x55, 0xe4, 0xe4, 0xe5, 0x0c, 0xca, 0x0b, 0x43, 0x8f, 0xac, 0x94, 0xf9, 0x6f, 0xb4, 0xe1, 0xf4, + 0x2c, 0x91, 0x47, 0x74, 0xfc, 0x49, 0x7f, 0xe1, 0xc8, 0xaf, 0x1e, 0xcf, 0x42, 0xbf, 0x60, 0x58, + 0x19, 0x88, 0xbd, 0x38, 0xdc, 0x48, 0x6c, 0x7e, 0x77, 0x97, 0x5c, 0x31, 0x4b, 0x7e, 0x0b, 0xfa, + 0x42, 0x42, 0x99, 0x10, 0xbc, 0x8c, 0x57, 0x5b, 0xf6, 0xf2, 0xc4, 0xdc, 0xdd, 0x25, 0xd7, 0x46, + 0xde, 0x1b, 0x40, 0x4f, 0x00, 0xe6, 0x9c, 0x29, 0xfd, 0xd7, 0x16, 0xac, 0x60, 0xb0, 0x5c, 0x7c, + 0xae, 0x78, 0xad, 0xaa, 0xf1, 0x4b, 0xe8, 0xc5, 0x49, 0xa8, 0x86, 0x2a, 0x2e, 0x5e, 0x34, 0xc3, + 0x31, 0x56, 0x38, 0x0f, 0xe7, 0xd3, 0x6f, 0xd8, 0x2b, 0x99, 0x76, 0xac, 0x2f, 0x90, 0x7f, 0x94, + 0x4c, 0xd3, 0x79, 0x12, 0xca, 0xdc, 0xa8, 0x86, 0x3a, 0x45, 0xb4, 0x8d, 0x14, 0x81, 0x51, 0xe3, + 0xe5, 0x3c, 0xf4, 0xec, 0x50, 0x69, 0x82, 0xc8, 0xc7, 0xb0, 0x91, 0xb3, 0x20, 0x4d, 0xc2, 0x5c, + 0x9c, 0x38, 0x83, 0x82, 0x85, 0xdc, 0x4f, 0xfa, 0x6e, 0x7d, 0xa2, 0xb9, 0x8c, 0x9c, 0xdc, 0x86, + 0x61, 0x65, 0xd9, 0x0d, 0x69, 0x71, 0xcb, 0x4c, 0x8b, 0xeb, 0x66, 0x1a, 0xfc, 0xc3, 0x16, 0x6c, + 0x3c, 0xc4, 0xc3, 0xa1, 0x54, 0x8a, 0x08, 0xa7, 0xff, 0x9b, 0x31, 0xc7, 0xf4, 0x9f, 0x95, 0x8a, + 0xff, 0xa8, 0x08, 0xd0, 0x3e, 0x3f, 0x02, 0x5c, 0x85, 0x51, 0xc6, 0xf8, 0x11, 0xd6, 0x2b, 0x49, + 0x09, 0x71, 0xd6, 0xe0, 0x58, 0x3c, 0x47, 0xa7, 0xa7, 0x2c, 0x8c, 0xfc, 0x02, 0xa1, 0x5e, 0x80, + 0x47, 0x94, 0x98, 0x4b, 0xb5, 0xe3, 0x36, 0x4d, 0xa1, 0x08, 0x88, 0x29, 0x02, 0x19, 0xa9, 0x3f, + 0x87, 0x51, 0x94, 0x14, 0x2c, 0x4b, 0xfc, 0xd8, 0x3b, 0xf5, 0x8b, 0xe0, 0x84, 0x2d, 0xf0, 0xcb, + 0x1a, 0x1a, 0xf9, 0x31, 0x0c, 0x78, 0x75, 0x9e, 0xcf, 0x83, 0x80, 0xe5, 0x58, 0x4c, 0x09, 0x07, + 0x2d, 0xab, 0x72, 0x3c, 0x84, 0x1e, 0x89, 0x49, 0xb7, 0x82, 0x4a, 0x3e, 0xc3, 0x4a, 0xf5, 0xd4, + 0x8f, 0x12, 0x2c, 0xf2, 0x85, 0xbb, 0x2d, 0x37, 0xb8, 0x9b, 0x5b, 0xc5, 0x22, 0x9f, 0x43, 0x9f, + 0x93, 0x7a, 0xe6, 0x47, 0xf1, 0x3c, 0xe3, 0x15, 0x5c, 0x8d, 0xe9, 0x4f, 0xc4, 0x9c, 0x6b, 0x63, + 0xd2, 0xff, 0x74, 0x60, 0xa8, 0x45, 0x70, 0xf0, 0x82, 0x25, 0x18, 0x9d, 0xdb, 0x7c, 0x3f, 0x0b, + 0x9d, 0x9d, 0xcf, 0x92, 0xcf, 0xa1, 0x67, 0x6e, 0x40, 0xfa, 0x7a, 0xd3, 0x4e, 0xef, 0x2e, 0xb9, + 0x16, 0x2a, 0xf9, 0xfc, 0xf5, 0x76, 0x7a, 0x77, 0xa9, 0x69, 0xaf, 0x3d, 0x73, 0x07, 0xdc, 0xb0, + 0x9a, 0xb7, 0x5a, 0x72, 0x95, 0xa8, 0x7b, 0x6b, 0xd0, 0x66, 0xb8, 0x41, 0x9a, 0x42, 0xd7, 0x38, + 0x1d, 0x2d, 0x2c, 0xbc, 0x8c, 0xb0, 0xd3, 0xb2, 0xc3, 0x8e, 0x51, 0x07, 0xad, 0xd4, 0xea, 0x20, + 0xd1, 0x46, 0x6d, 0x1b, 0x6d, 0x54, 0xfa, 0x29, 0x6c, 0xf3, 0xa8, 0xc7, 0x74, 0xcf, 0xfd, 0x57, + 0x1f, 0xfe, 0xc7, 0xb0, 0x53, 0xfd, 0x48, 0xf6, 0xd2, 0x0e, 0x81, 0x88, 0x19, 0xcb, 0x75, 0xcf, + 0xeb, 0x69, 0x9c, 0xe3, 0xc0, 0xf4, 0x6f, 0x1c, 0xd8, 0xb4, 0xc8, 0x49, 0x37, 0x78, 0x17, 0x46, + 0x0a, 0xc7, 0x4b, 0x13, 0x8f, 0x67, 0x59, 0x47, 0x67, 0x59, 0x72, 0x0d, 0x88, 0x56, 0x4e, 0x85, + 0x7a, 0xc3, 0x8c, 0xf0, 0x65, 0x64, 0x13, 0x6a, 0x6c, 0x51, 0x6d, 0xd5, 0xe0, 0x66, 0x50, 0x59, + 0xb1, 0x82, 0x8a, 0x96, 0xca, 0x6e, 0x1c, 0x5b, 0x87, 0x1d, 0x3a, 0x87, 0x0b, 0xb5, 0x19, 0xb9, + 0x95, 0x8f, 0x61, 0x43, 0xb1, 0x50, 0x22, 0x51, 0x55, 0x7d, 0x7d, 0x02, 0xb1, 0xe5, 0x7e, 0x0d, + 0x6c, 0xd1, 0x3e, 0xac, 0x4f, 0xd0, 0x4f, 0x60, 0x43, 0xb0, 0x35, 0x2f, 0x4e, 0x16, 0x1e, 0xde, + 0xf0, 0xe0, 0x6c, 0xa2, 0x4b, 0x8d, 0xfe, 0x51, 0x0b, 0xc1, 0x79, 0x91, 0x66, 0x56, 0x7f, 0xf4, + 0xb5, 0x9a, 0x9d, 0x66, 0x13, 0xb5, 0x65, 0x37, 0x51, 0xc9, 0x37, 0xd0, 0xc5, 0x4c, 0x36, 0xf5, + 0x83, 0xe7, 0xf3, 0x99, 0x4a, 0x7d, 0x57, 0x95, 0xb3, 0xd4, 0x39, 0x62, 0x22, 0xdc, 0x13, 0xc8, + 0x22, 0x11, 0x42, 0x5c, 0x02, 0xc8, 0x0f, 0xf8, 0x0d, 0x93, 0x17, 0xfa, 0x85, 0x3f, 0xf5, 0x73, + 0xd1, 0x60, 0xee, 0xf1, 0xbc, 0x76, 0x47, 0x82, 0x64, 0x4e, 0x32, 0x29, 0xfc, 0xaa, 0x9c, 0xd4, + 0x33, 0x73, 0x12, 0x43, 0x4b, 0x34, 0xd6, 0xa4, 0x7b, 0xbe, 0x99, 0x00, 0xcb, 0x5e, 0xae, 0x14, + 0x83, 0x02, 0xf2, 0x46, 0xee, 0x87, 0x68, 0x5e, 0x12, 0x49, 0xb5, 0x44, 0xc4, 0x61, 0x7e, 0xa8, + 0xe0, 0xaa, 0x85, 0x7b, 0x07, 0xc8, 0x11, 0x2b, 0x0e, 0xd3, 0xe3, 0x43, 0xf6, 0x42, 0x9f, 0x24, + 0xae, 0xc1, 0x7a, 0x9c, 0x1e, 0x7b, 0x31, 0xc2, 0xf8, 0x72, 0x07, 0xfa, 0xa0, 0x55, 0xe2, 0x6a, + 0x14, 0xba, 0x0d, 0x9b, 0x16, 0x15, 0xa9, 0xca, 0x0d, 0x18, 0x1e, 0x9d, 0xcc, 0x8b, 0x30, 0x3d, + 0x53, 0xb7, 0x33, 0x78, 0x64, 0xd4, 0x20, 0x89, 0xf6, 0x1b, 0xb0, 0x73, 0x34, 0x9f, 0xe6, 0x41, + 0x16, 0x4d, 0x99, 0x7d, 0xf0, 0x9f, 0x40, 0x87, 0xbd, 0x8c, 0xf2, 0x22, 0x4a, 0x8e, 0xf9, 0x32, + 0x3a, 0x6e, 0x39, 0xa6, 0xef, 0xc1, 0x3b, 0xe5, 0x57, 0x18, 0xea, 0xf2, 0xdd, 0x20, 0x60, 0xb3, + 0x82, 0xa9, 0xbb, 0x10, 0x7a, 0x1b, 0xb6, 0x6d, 0x04, 0xe3, 0x2a, 0x4f, 0x1d, 0xe8, 0x0b, 0xff, + 0xb9, 0xac, 0xe4, 0x3a, 0xae, 0x0d, 0xa4, 0xff, 0xdd, 0x82, 0x1e, 0x7e, 0xa6, 0xc8, 0x92, 0xb7, + 0x6a, 0x41, 0x65, 0x8d, 0x8f, 0xef, 0xd9, 0x25, 0x70, 0xab, 0x52, 0x02, 0x9f, 0x5b, 0x14, 0x2c, + 0xea, 0x8f, 0xea, 0xe2, 0xa3, 0x6d, 0x16, 0x1f, 0xd5, 0xae, 0xeb, 0x6a, 0x43, 0xd7, 0x75, 0x07, + 0x56, 0x33, 0xde, 0x12, 0x93, 0xe7, 0x4f, 0x39, 0xc2, 0x98, 0x23, 0xce, 0x69, 0x5e, 0xc6, 0x02, + 0x16, 0xbd, 0x40, 0x99, 0x76, 0x44, 0xcc, 0xa9, 0xc2, 0xf1, 0x80, 0x26, 0x61, 0xb9, 0xbc, 0x98, + 0x5a, 0x17, 0x37, 0x77, 0x36, 0x14, 0xe3, 0x9e, 0x8a, 0xd1, 0x06, 0x55, 0xd1, 0xc9, 0x6b, 0x98, + 0xc1, 0x35, 0x94, 0x50, 0x45, 0xb9, 0x2b, 0x6a, 0x98, 0x2a, 0x1c, 0x63, 0x71, 0xd7, 0x48, 0x61, + 0xdf, 0xb3, 0x4f, 0x6d, 0xca, 0x78, 0xb9, 0x22, 0xe3, 0xaa, 0x34, 0x57, 0x1a, 0xa4, 0xf9, 0x01, + 0x0c, 0x64, 0xce, 0xf4, 0x32, 0xe6, 0xe7, 0xa9, 0xca, 0x66, 0x15, 0x28, 0xfd, 0xdb, 0x65, 0xb1, + 0x5a, 0x99, 0xe6, 0xff, 0x7f, 0x8d, 0x45, 0xab, 0xbc, 0x6d, 0xa9, 0xfc, 0x0a, 0x0c, 0x2d, 0xd5, + 0xb2, 0x50, 0x6a, 0xbc, 0x0a, 0xc6, 0x32, 0x5d, 0xab, 0xb6, 0x90, 0xda, 0x36, 0x41, 0x35, 0x61, + 0x41, 0x83, 0xb0, 0x2e, 0xc1, 0x4a, 0x96, 0xc6, 0x8c, 0xab, 0x74, 0xa0, 0xbb, 0x3c, 0x6e, 0x1a, + 0x33, 0x97, 0xcf, 0x60, 0x3e, 0xa9, 0x98, 0x05, 0x0b, 0x79, 0xb7, 0x76, 0xdd, 0xad, 0x4f, 0xa0, + 0xa3, 0x9a, 0x66, 0x51, 0x8c, 0xfb, 0xe2, 0xde, 0xc7, 0x02, 0xe2, 0x09, 0x3b, 0xf3, 0x66, 0x19, + 0x8b, 0x4e, 0xfd, 0x63, 0x36, 0x1e, 0x70, 0x14, 0x03, 0xa2, 0x5d, 0x69, 0x68, 0xb8, 0x12, 0xfd, + 0xaf, 0x16, 0xb4, 0x1f, 0x65, 0x7e, 0xc8, 0xf0, 0x18, 0x79, 0x8a, 0x1e, 0xef, 0x2d, 0x3e, 0xd6, + 0xb9, 0x26, 0x06, 0x7e, 0x50, 0x18, 0x1f, 0xb4, 0x1a, 0x3f, 0x30, 0x30, 0x0c, 0xfd, 0x2c, 0x5b, + 0xfa, 0x39, 0x4f, 0xa7, 0x86, 0x25, 0xb4, 0x6d, 0x4b, 0x28, 0xf7, 0xb3, 0x6a, 0x86, 0x06, 0x25, + 0xfb, 0xb5, 0x85, 0xb2, 0xbf, 0x04, 0x5d, 0x26, 0xae, 0x7f, 0x78, 0x2b, 0x42, 0x58, 0x82, 0x09, + 0x2a, 0x4f, 0x22, 0xeb, 0xe7, 0x9f, 0x44, 0x6e, 0x41, 0x2f, 0x40, 0xc3, 0x60, 0xd9, 0xcc, 0xcf, + 0x0a, 0x61, 0x0a, 0x8b, 0xbb, 0x25, 0x16, 0x2e, 0xfd, 0x08, 0x36, 0xb9, 0xd4, 0xef, 0x46, 0x98, + 0x87, 0x5e, 0x19, 0x67, 0x2d, 0xd1, 0x90, 0x75, 0x8c, 0x86, 0x2c, 0xbd, 0x0d, 0x5b, 0x36, 0xb2, + 0x4c, 0x82, 0x97, 0x61, 0xb5, 0x40, 0x78, 0xed, 0x2c, 0xc2, 0xb1, 0x5d, 0x39, 0x49, 0xff, 0xd4, + 0x81, 0x3e, 0x42, 0xa2, 0xe4, 0xf8, 0x10, 0xe9, 0xe5, 0x28, 0xf0, 0x53, 0xff, 0xa5, 0x97, 0xb3, + 0x38, 0x56, 0xcd, 0x0f, 0x35, 0x46, 0x81, 0xe3, 0xef, 0xe9, 0x5c, 0x15, 0x6e, 0x6a, 0x88, 0x66, + 0x98, 0xb1, 0x9c, 0x65, 0x58, 0x1a, 0xf1, 0x4f, 0x45, 0x20, 0xb1, 0x81, 0xe8, 0x20, 0x25, 0x00, + 0x89, 0x08, 0x85, 0x5a, 0x30, 0x7a, 0x53, 0x6c, 0xa8, 0x5c, 0xd0, 0xeb, 0xd4, 0xbe, 0xbf, 0x74, + 0x60, 0xbb, 0xf2, 0x91, 0x14, 0xc3, 0x2e, 0xac, 0x72, 0x39, 0x29, 0x31, 0x7c, 0x68, 0x8a, 0xa1, + 0x86, 0x7e, 0x4d, 0x0c, 0x65, 0x2f, 0x59, 0x7c, 0x38, 0x79, 0x08, 0x5d, 0x03, 0xdc, 0x50, 0xa0, + 0x7c, 0x64, 0xf7, 0x92, 0xb7, 0x9b, 0x59, 0x18, 0x75, 0xcb, 0xb7, 0xd0, 0x7b, 0x9c, 0x4c, 0xbf, + 0xc7, 0x73, 0x0c, 0x72, 0x11, 0xd6, 0x33, 0x26, 0x4f, 0xfa, 0xb2, 0x5c, 0xd1, 0x00, 0x3a, 0x84, + 0xbe, 0xa4, 0xab, 0x6f, 0xd1, 0x1f, 0x27, 0x71, 0x1a, 0x3c, 0x7f, 0xdd, 0x5b, 0xf4, 0x9f, 0x01, + 0x31, 0x3f, 0xd0, 0x05, 0xd5, 0x9c, 0x43, 0x2b, 0x05, 0x95, 0x02, 0xf2, 0x82, 0xea, 0x3d, 0xe8, + 0x9a, 0x28, 0xe2, 0xd2, 0x0d, 0x34, 0x02, 0xfd, 0x13, 0x07, 0x86, 0x4f, 0xa2, 0xe2, 0x24, 0xcc, + 0xfc, 0xb3, 0xd7, 0x50, 0x6a, 0xf5, 0x45, 0x43, 0xeb, 0xbc, 0x17, 0x0d, 0xcb, 0xd5, 0x17, 0x0d, + 0x7e, 0x1c, 0xcb, 0xe6, 0x0b, 0xfe, 0x34, 0xdb, 0xae, 0x7d, 0xd1, 0x76, 0xbd, 0x05, 0x23, 0xbd, + 0x98, 0x37, 0xeb, 0xb9, 0x5e, 0xbd, 0x02, 0xeb, 0xa5, 0xbf, 0x93, 0x35, 0x58, 0xde, 0x7b, 0xfc, + 0xdb, 0xa3, 0x25, 0xd2, 0x81, 0x95, 0xa3, 0x83, 0xc3, 0x43, 0x71, 0xbd, 0xc1, 0x6f, 0x3c, 0x5a, + 0x57, 0xaf, 0xc2, 0x0a, 0x46, 0x17, 0xb2, 0x0e, 0xed, 0x47, 0xbb, 0xdf, 0x1c, 0xb8, 0xa3, 0x25, + 0xfc, 0xf9, 0x53, 0xfe, 0xd3, 0x21, 0x3d, 0xe8, 0xdc, 0xbb, 0xff, 0xe8, 0xc0, 0xbd, 0xbf, 0x7b, + 0x38, 0x6a, 0x5d, 0x7d, 0x02, 0x1d, 0x55, 0x1d, 0x22, 0xd2, 0xee, 0xe1, 0x81, 0xfb, 0x48, 0xe0, + 0x1f, 0xb8, 0xee, 0x03, 0x57, 0xd0, 0x7d, 0xb2, 0xeb, 0xde, 0x1f, 0xb5, 0xf0, 0xd7, 0xbd, 0xfb, + 0x3f, 0x79, 0x30, 0x5a, 0x26, 0x5d, 0x58, 0xfb, 0xf6, 0xc0, 0xdd, 0x7b, 0x70, 0x74, 0x30, 0x5a, + 0x41, 0xdc, 0x3b, 0x07, 0x7b, 0x8f, 0xbf, 0x1a, 0xb5, 0x39, 0x47, 0x77, 0x77, 0xff, 0x60, 0xb4, + 0x7a, 0xf3, 0xdf, 0x1d, 0x58, 0x7b, 0x3a, 0x0f, 0xef, 0x25, 0x51, 0x41, 0x0e, 0x00, 0xf4, 0x2b, + 0x09, 0xf2, 0x56, 0xd9, 0xed, 0xaf, 0xbe, 0xb5, 0x98, 0x4c, 0x9a, 0xa6, 0xa4, 0x59, 0x2d, 0x91, + 0xbb, 0xd0, 0x35, 0x2a, 0x6f, 0x32, 0x59, 0x7c, 0x44, 0x98, 0xbc, 0xdd, 0x38, 0x57, 0x52, 0x3a, + 0x00, 0xd0, 0x16, 0xa7, 0x17, 0x54, 0x33, 0x5b, 0xbd, 0xa0, 0xba, 0x81, 0xd2, 0xa5, 0x9b, 0xbf, + 0x9c, 0xc0, 0xf2, 0xd3, 0x79, 0x48, 0x9e, 0x42, 0xd7, 0x78, 0xab, 0x46, 0x6a, 0x37, 0x69, 0x7a, + 0x39, 0x4d, 0x4f, 0xda, 0x26, 0xbf, 0xf8, 0xa7, 0xff, 0xf8, 0xb3, 0xd6, 0x16, 0x1d, 0x5e, 0x7f, + 0xf1, 0xeb, 0xd7, 0xfd, 0x30, 0x54, 0xb6, 0x78, 0xcb, 0xb9, 0x4a, 0x5c, 0x58, 0x93, 0xcf, 0xd1, + 0xc8, 0x8e, 0x41, 0xc3, 0x38, 0xc6, 0x4d, 0x2e, 0xd4, 0xe0, 0x92, 0xee, 0x0e, 0xa7, 0x3b, 0xa2, + 0x5d, 0x49, 0x17, 0xd3, 0x14, 0xd2, 0xdc, 0x83, 0xe5, 0x3d, 0x3f, 0x21, 0x44, 0x5f, 0x94, 0xab, + 0x98, 0x30, 0xd9, 0xb4, 0x60, 0x92, 0x0e, 0xe1, 0x74, 0x7a, 0x74, 0x0d, 0xe9, 0x4c, 0xfd, 0x04, + 0x69, 0x1c, 0xc3, 0xc0, 0x7e, 0x86, 0x44, 0xde, 0x31, 0xef, 0x7b, 0x6a, 0xef, 0x9f, 0x26, 0xef, + 0x2e, 0x9a, 0xae, 0x2c, 0x76, 0x80, 0x4c, 0x02, 0x8e, 0x83, 0xf1, 0x81, 0x04, 0xd0, 0x33, 0x5f, + 0x05, 0x11, 0xfd, 0x36, 0xa5, 0xfe, 0xd4, 0x69, 0x72, 0xb1, 0x79, 0x52, 0xb2, 0x18, 0x73, 0x16, + 0x84, 0x8e, 0x38, 0x0b, 0xc4, 0x90, 0x17, 0x52, 0x28, 0x65, 0xf9, 0x14, 0x48, 0x4b, 0xd9, 0x7e, + 0x49, 0xa4, 0xa5, 0x5c, 0x7d, 0x33, 0x64, 0x49, 0x59, 0xc6, 0x44, 0x94, 0xd0, 0xcf, 0xa1, 0xff, + 0x84, 0xbf, 0x86, 0x93, 0x0f, 0x50, 0x34, 0x65, 0xfb, 0xfd, 0x8a, 0xa6, 0x5c, 0x79, 0xa9, 0x42, + 0x2f, 0x72, 0xca, 0x3b, 0x74, 0x03, 0x29, 0x8b, 0x97, 0x75, 0xa1, 0x40, 0x91, 0x96, 0xf1, 0xbd, + 0x29, 0x5b, 0x6b, 0x36, 0x68, 0xfe, 0x1e, 0xf4, 0xad, 0xf7, 0x2b, 0xa4, 0x14, 0x68, 0xd3, 0xc3, + 0x98, 0xc9, 0x3b, 0x0b, 0x66, 0x9b, 0xd6, 0x1f, 0x4a, 0x14, 0xfe, 0xe2, 0x05, 0x79, 0x3d, 0x05, + 0xd0, 0xef, 0x40, 0xb4, 0x0b, 0xd6, 0xde, 0x9e, 0x68, 0x17, 0xac, 0x3f, 0x1b, 0xa1, 0x9b, 0x9c, + 0x45, 0x9f, 0x74, 0x85, 0x69, 0x0a, 0x5a, 0x87, 0xb0, 0x26, 0x5f, 0x3c, 0x68, 0xc9, 0xd8, 0xcf, + 0x3e, 0xb4, 0x64, 0x2a, 0x4f, 0x23, 0xe8, 0x88, 0x13, 0x04, 0xd2, 0x41, 0x82, 0x11, 0x92, 0xf8, + 0x1d, 0xe8, 0x1a, 0xcf, 0x05, 0x88, 0xb9, 0x9a, 0xca, 0xcb, 0x02, 0xed, 0xe5, 0x0d, 0xef, 0x0b, + 0xe8, 0x16, 0xa7, 0x3c, 0x20, 0x3d, 0xa4, 0xac, 0x7a, 0x25, 0x92, 0xba, 0x7a, 0x0f, 0x60, 0x51, + 0xaf, 0x3c, 0x32, 0xb0, 0xa8, 0x57, 0x1f, 0x10, 0xd8, 0xd4, 0x51, 0xc6, 0x7c, 0xed, 0x4f, 0x00, + 0xf4, 0xd5, 0xb5, 0x96, 0x71, 0xed, 0x0e, 0x5e, 0xcb, 0xb8, 0x7e, 0xd3, 0xad, 0xdc, 0x9f, 0x00, + 0x92, 0x96, 0x17, 0x3c, 0xc7, 0x30, 0xb0, 0x5f, 0x16, 0x68, 0xf7, 0x6f, 0x7c, 0x8a, 0xa0, 0xdd, + 0xbf, 0xf9, 0x41, 0x82, 0xb2, 0x48, 0x22, 0xdc, 0x5f, 0x93, 0x3d, 0x82, 0xf5, 0xf2, 0xce, 0x9b, + 0x8c, 0x4d, 0x22, 0xe6, 0xd5, 0xf8, 0xe4, 0xad, 0x86, 0x19, 0xd5, 0xea, 0xe0, 0x94, 0xbb, 0x64, + 0x1d, 0x29, 0x8b, 0xab, 0x0f, 0x45, 0x94, 0xbf, 0xbe, 0xb1, 0x89, 0x1a, 0x17, 0xe6, 0x15, 0xa2, + 0xe6, 0xb5, 0x79, 0x85, 0x28, 0xa7, 0xe3, 0x41, 0xd7, 0xb8, 0x51, 0xd5, 0x9a, 0xac, 0x5f, 0x07, + 0x6b, 0x4d, 0x36, 0x5c, 0xc1, 0xd2, 0x0b, 0x9c, 0xf4, 0x86, 0xc8, 0x06, 0xe9, 0x8c, 0x25, 0x2a, + 0x48, 0xfd, 0x2e, 0x80, 0x6e, 0x82, 0x6b, 0x65, 0xd6, 0xae, 0x47, 0xb4, 0x71, 0x57, 0x7a, 0xe6, + 0xf4, 0x2d, 0x4e, 0x7a, 0x53, 0xc4, 0x58, 0x7e, 0x31, 0xc1, 0xd5, 0x79, 0xcb, 0xb9, 0x7a, 0xc3, + 0x21, 0xcf, 0x60, 0xa0, 0xf1, 0x8f, 0x5e, 0x25, 0xc1, 0x79, 0x2c, 0x26, 0x4d, 0x53, 0x72, 0x03, + 0xef, 0x70, 0x2e, 0x17, 0x28, 0xb1, 0xb9, 0xe4, 0xaf, 0x92, 0x00, 0xfd, 0xfe, 0x67, 0xd0, 0x35, + 0xde, 0xba, 0x69, 0x39, 0xd5, 0x1f, 0xc0, 0x4d, 0x9a, 0xda, 0xf4, 0x76, 0xb6, 0x94, 0x67, 0xa4, + 0xfc, 0xcc, 0x9f, 0x21, 0xed, 0x04, 0x06, 0x76, 0x37, 0x5a, 0x9b, 0x65, 0x63, 0x6b, 0x5b, 0x9b, + 0xe5, 0x82, 0x26, 0xb6, 0xb5, 0x17, 0xd1, 0x84, 0x35, 0xb3, 0xf3, 0x14, 0x0b, 0x92, 0xb2, 0x29, + 0x6d, 0x16, 0x24, 0xd5, 0xc6, 0xb7, 0x59, 0x90, 0xd4, 0xba, 0xd8, 0xf6, 0x9e, 0x04, 0x1b, 0xa5, + 0x19, 0x92, 0xc1, 0xb0, 0xd2, 0x31, 0x26, 0x95, 0x55, 0x57, 0x9b, 0xcc, 0x93, 0xf7, 0x16, 0xce, + 0x4b, 0x7e, 0xef, 0x72, 0x7e, 0x63, 0xba, 0xa9, 0xf9, 0xf9, 0x71, 0x2c, 0xd4, 0x24, 0x72, 0x17, + 0xe8, 0xfe, 0xaf, 0xb6, 0x83, 0x5a, 0x0b, 0x79, 0x32, 0x69, 0x9a, 0x92, 0x4c, 0x2c, 0x6b, 0x13, + 0x4c, 0x54, 0x05, 0x32, 0x85, 0xae, 0xd1, 0x95, 0xd4, 0x72, 0xab, 0x37, 0x3c, 0xb5, 0xdc, 0x9a, + 0xda, 0x98, 0x96, 0xdc, 0x72, 0x56, 0xc4, 0xe9, 0x31, 0x6f, 0x7b, 0x22, 0x8f, 0x6f, 0xa1, 0xa3, + 0xfa, 0x99, 0xa4, 0xf4, 0x88, 0x4a, 0xd3, 0x73, 0x32, 0xae, 0x4f, 0x54, 0xdc, 0x90, 0x07, 0xd4, + 0x5c, 0xce, 0x22, 0x5d, 0x06, 0xc3, 0x4a, 0x4f, 0x54, 0xeb, 0xa3, 0xb9, 0x59, 0x3a, 0xb1, 0x9f, + 0xec, 0x89, 0xdb, 0x6a, 0xfa, 0x36, 0x67, 0xb0, 0x4d, 0xb8, 0x0e, 0x72, 0xf5, 0xa1, 0xd0, 0xc1, + 0x0d, 0x87, 0xcc, 0x2a, 0x3d, 0x52, 0xd9, 0x6c, 0x33, 0x02, 0x6d, 0x63, 0x0b, 0x75, 0xd2, 0x74, + 0xc9, 0x44, 0x7f, 0xc0, 0x79, 0xbd, 0x4d, 0xde, 0xb2, 0x78, 0xa1, 0xd7, 0xa8, 0x3b, 0xb6, 0x1b, + 0x0e, 0x99, 0xc2, 0xc0, 0x26, 0xf9, 0x46, 0xac, 0x2a, 0xee, 0x49, 0x48, 0x8d, 0x15, 0xf2, 0xf8, + 0x03, 0xa3, 0xa1, 0x6c, 0xb5, 0x86, 0xc9, 0xe5, 0x66, 0x5e, 0x95, 0xd6, 0xf1, 0x64, 0xcb, 0xe4, + 0xa9, 0x26, 0x29, 0xe5, 0x4c, 0x2f, 0x92, 0x49, 0x9d, 0xa9, 0x2f, 0x71, 0x78, 0x84, 0xeb, 0x99, + 0x4d, 0x0b, 0x5d, 0x4a, 0x36, 0xf4, 0x3d, 0x74, 0x29, 0xd9, 0xd4, 0xe7, 0x50, 0xca, 0x13, 0xa5, + 0x24, 0x6f, 0x6a, 0x9c, 0x08, 0x0c, 0x51, 0x1b, 0x57, 0x9a, 0x1b, 0x17, 0x17, 0x1c, 0xff, 0x2b, + 0x55, 0x54, 0x63, 0x73, 0x40, 0xb9, 0x11, 0xd9, 0x50, 0xac, 0xa2, 0xe4, 0x58, 0xf4, 0x08, 0xc8, + 0xd7, 0xd0, 0xe6, 0x27, 0x6f, 0xb2, 0xa5, 0x4f, 0x29, 0xfa, 0x80, 0x3f, 0xd9, 0xae, 0x40, 0xed, + 0x52, 0x81, 0xf2, 0xdc, 0x35, 0x4f, 0x64, 0x41, 0x3f, 0x85, 0x81, 0x28, 0x57, 0xd5, 0xf9, 0x54, + 0x3b, 0x4d, 0xe5, 0xf8, 0xac, 0x9d, 0xa6, 0x7a, 0x94, 0xb5, 0xc3, 0xa5, 0xa8, 0x58, 0xcf, 0x24, + 0xce, 0x2d, 0xe7, 0xea, 0x74, 0x95, 0xff, 0xb3, 0xe7, 0xd3, 0xff, 0x09, 0x00, 0x00, 0xff, 0xff, + 0xda, 0x35, 0x6d, 0xbb, 0x04, 0x34, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -5254,6 +5254,9 @@ type XudClient interface { // Gets an address to deposit a given currency into the xud wallets. // shell: xucli walletdeposit WalletDeposit(ctx context.Context, in *DepositRequest, opts ...grpc.CallOption) (*DepositResponse, error) + // Gets an address to deposit a given currency directly into a channel. + // shell: xucli deposit + Deposit(ctx context.Context, in *DepositRequest, opts ...grpc.CallOption) (*DepositResponse, error) // Discover nodes from a specific peer and apply new connections DiscoverNodes(ctx context.Context, in *DiscoverNodesRequest, opts ...grpc.CallOption) (*DiscoverNodesResponse, error) // Gets the total balance available across all payment channels and wallets for one or all currencies. @@ -5427,6 +5430,15 @@ func (c *xudClient) WalletDeposit(ctx context.Context, in *DepositRequest, opts return out, nil } +func (c *xudClient) Deposit(ctx context.Context, in *DepositRequest, opts ...grpc.CallOption) (*DepositResponse, error) { + out := new(DepositResponse) + err := c.cc.Invoke(ctx, "/xudrpc.Xud/Deposit", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *xudClient) DiscoverNodes(ctx context.Context, in *DiscoverNodesRequest, opts ...grpc.CallOption) (*DiscoverNodesResponse, error) { out := new(DiscoverNodesResponse) err := c.cc.Invoke(ctx, "/xudrpc.Xud/DiscoverNodes", in, out, opts...) @@ -5814,6 +5826,9 @@ type XudServer interface { // Gets an address to deposit a given currency into the xud wallets. // shell: xucli walletdeposit WalletDeposit(context.Context, *DepositRequest) (*DepositResponse, error) + // Gets an address to deposit a given currency directly into a channel. + // shell: xucli deposit + Deposit(context.Context, *DepositRequest) (*DepositResponse, error) // Discover nodes from a specific peer and apply new connections DiscoverNodes(context.Context, *DiscoverNodesRequest) (*DiscoverNodesResponse, error) // Gets the total balance available across all payment channels and wallets for one or all currencies. @@ -6046,6 +6061,24 @@ func _Xud_WalletDeposit_Handler(srv interface{}, ctx context.Context, dec func(i return interceptor(ctx, in, info, handler) } +func _Xud_Deposit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DepositRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(XudServer).Deposit(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/xudrpc.Xud/Deposit", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(XudServer).Deposit(ctx, req.(*DepositRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Xud_DiscoverNodes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(DiscoverNodesRequest) if err := dec(in); err != nil { @@ -6579,6 +6612,10 @@ var _Xud_serviceDesc = grpc.ServiceDesc{ MethodName: "WalletDeposit", Handler: _Xud_WalletDeposit_Handler, }, + { + MethodName: "Deposit", + Handler: _Xud_Deposit_Handler, + }, { MethodName: "DiscoverNodes", Handler: _Xud_DiscoverNodes_Handler, diff --git a/test/simulation/xudtest/harness.go b/test/simulation/xudtest/harness.go index 01f169c73..f1b7a4a5e 100644 --- a/test/simulation/xudtest/harness.go +++ b/test/simulation/xudtest/harness.go @@ -2,8 +2,8 @@ package xudtest import ( "context" - "fmt" - "github.com/ExchangeUnion/xud-simulation/connexttest" + // "fmt" + // "github.com/ExchangeUnion/xud-simulation/connexttest" "sync" "time" @@ -25,9 +25,9 @@ type NetworkHarness struct { Carol *HarnessNode Dave *HarnessNode - LndBtcNetwork *lntest.NetworkHarness - LndLtcNetwork *lntest.NetworkHarness - ConnextNetwork *connexttest.NetworkHarness + LndBtcNetwork *lntest.NetworkHarness + LndLtcNetwork *lntest.NetworkHarness + // ConnextNetwork *connexttest.NetworkHarness errorChan chan *XudError @@ -50,6 +50,7 @@ type CtxSetter interface { SetCtx(ctx context.Context, cancel context.CancelFunc) } +/* func (n *NetworkHarness) newConnextClient(ctx context.Context, node *HarnessNode, envVars *[]string) (*connexttest.HarnessClient, error) { if err := n.ConnextNetwork.TearDown(node.ConnextClient.ID); err != nil { return nil, err @@ -72,14 +73,17 @@ func (n *NetworkHarness) newConnextClient(ctx context.Context, node *HarnessNode return client, nil } +*/ func (n *NetworkHarness) SetCustomXud(ctx context.Context, ctxSetter CtxSetter, node *HarnessNode, envVars []string) (*HarnessNode, error) { t := time.Now() - connextClient, err := n.newConnextClient(ctx, node, &envVars) - if err != nil { - return nil, err - } + /* + connextClient, err := n.newConnextClient(ctx, node, &envVars) + if err != nil { + return nil, err + } + */ if err := node.shutdown(true, true); err != nil { return nil, err @@ -93,7 +97,7 @@ func (n *NetworkHarness) SetCustomXud(ctx context.Context, ctxSetter CtxSetter, customNode.SetEnvVars(envVars) customNode.SetLnd(node.LndBtcNode, "BTC") customNode.SetLnd(node.LndLtcNode, "LTC") - customNode.SetConnextClient(connextClient) + // customNode.SetConnextClient(connextClient) if err := customNode.Start(n.errorChan); err != nil { return nil, err @@ -193,6 +197,7 @@ func (n *NetworkHarness) SetLnd(ln *lntest.NetworkHarness, chain string) { n.Dave.SetLnd(ln.Dave, chain) } +/* func (n *NetworkHarness) SetConnext(net *connexttest.NetworkHarness) { n.ConnextNetwork = net n.Alice.SetConnextClient(net.Alice) @@ -200,6 +205,7 @@ func (n *NetworkHarness) SetConnext(net *connexttest.NetworkHarness) { n.Carol.SetConnextClient(net.Carol) n.Dave.SetConnextClient(net.Dave) } +*/ // ProcessErrors returns a channel used for reporting any fatal process errors. // If any of the active nodes within the harness' test network incur a fatal diff --git a/test/simulation/xudtest/node.go b/test/simulation/xudtest/node.go index ca6921f5d..aff97d607 100644 --- a/test/simulation/xudtest/node.go +++ b/test/simulation/xudtest/node.go @@ -13,7 +13,7 @@ import ( "sync/atomic" "time" - "github.com/ExchangeUnion/xud-simulation/connexttest" + // "github.com/ExchangeUnion/xud-simulation/connexttest" "context" @@ -91,23 +91,26 @@ func (cfg nodeConfig) genArgs() []string { args = append(args, fmt.Sprintf("--lnd.LTC.port=%v", cfg.LndLtcPort)) args = append(args, fmt.Sprintf("--lnd.LTC.certpath=%v", cfg.LndLtcCertPath)) args = append(args, fmt.Sprintf("--lnd.LTC.macaroonpath=%v", cfg.LndLtcMacPath)) + args = append(args, "--connext.disable") + + /* + if !cfg.RaidenDisable { + args = append(args, fmt.Sprintf("--raiden.host=%v", cfg.RaidenHost)) + args = append(args, fmt.Sprintf("--raiden.port=%v", cfg.RaidenPort)) + } else { + args = append(args, "--raiden.disable") + } - if !cfg.RaidenDisable { - args = append(args, fmt.Sprintf("--raiden.host=%v", cfg.RaidenHost)) - args = append(args, fmt.Sprintf("--raiden.port=%v", cfg.RaidenPort)) - } else { - args = append(args, "--raiden.disable") - } - - if !cfg.ConnextDisable { - args = append(args, fmt.Sprintf("--connext.host=%v", cfg.ConnextHost)) - args = append(args, fmt.Sprintf("--connext.port=%v", cfg.ConnextPort)) - args = append(args, "--connext.webhookhost=127.0.0.1") - args = append(args, fmt.Sprintf("--connext.webhookport=%v", cfg.HTTPPort)) + if !cfg.ConnextDisable { + args = append(args, fmt.Sprintf("--connext.host=%v", cfg.ConnextHost)) + args = append(args, fmt.Sprintf("--connext.port=%v", cfg.ConnextPort)) + args = append(args, "--connext.webhookhost=127.0.0.1") + args = append(args, fmt.Sprintf("--connext.webhookport=%v", cfg.HTTPPort)) - } else { - args = append(args, "--connext.disable") - } + } else { + args = append(args, "--connext.disable") + } + */ return args } @@ -124,9 +127,9 @@ type HarnessNode struct { ID int pubKey string - LndBtcNode *lntest.HarnessNode - LndLtcNode *lntest.HarnessNode - ConnextClient *connexttest.HarnessClient + LndBtcNode *lntest.HarnessNode + LndLtcNode *lntest.HarnessNode + // ConnextClient *connexttest.HarnessClient // processExit is a channel that's closed once it's detected that the // process this instance of HarnessNode is bound to has exited. @@ -209,11 +212,13 @@ func (hn *HarnessNode) SetLnd(lndNode *lntest.HarnessNode, chain string) { } } +/* func (hn *HarnessNode) SetConnextClient(client *connexttest.HarnessClient) { hn.Cfg.ConnextHost = "0.0.0.0" hn.Cfg.ConnextPort = client.Cfg.Port hn.ConnextClient = client } +*/ func (hn *HarnessNode) SetEnvVars(envVars []string) { hn.EnvVars = envVars