diff --git a/src/chains/xrpl/xrpl.controllers.ts b/src/chains/xrpl/xrpl.controllers.ts index f632484fbf..4137115592 100644 --- a/src/chains/xrpl/xrpl.controllers.ts +++ b/src/chains/xrpl/xrpl.controllers.ts @@ -13,6 +13,7 @@ import { XRPLBalanceResponse, XRPLPollRequest, XRPLPollResponse, + BalanceRecord, } from './xrpl.requests'; import { @@ -48,9 +49,24 @@ export class XRPLController { const xrplBalances = await xrplish.getAllBalance(wallet); - const balances: Record = {}; + const xrplSubtractedBalances = await xrplish.subtractBalancesWithOpenOffers( + xrplBalances, + wallet + ); + + const balances: Record = {}; xrplBalances.forEach((balance) => { - balances[balance.currency] = balance.value; + balances[balance.currency] = { + total_balance: balance.value, + available_balance: balance.value, + }; + }); + + xrplSubtractedBalances.forEach((balance) => { + balances[balance.currency] = { + ...balances[balance.currency], + available_balance: balance.value, + }; }); return { diff --git a/src/chains/xrpl/xrpl.requests.ts b/src/chains/xrpl/xrpl.requests.ts index a4682054ed..5767f5e345 100644 --- a/src/chains/xrpl/xrpl.requests.ts +++ b/src/chains/xrpl/xrpl.requests.ts @@ -11,7 +11,12 @@ export interface XRPLBalanceResponse { timestamp: number; latency: number; address: string; - balances: Record; + balances: Record; +} + +export interface BalanceRecord { + total_balance: string; + available_balance: string; } export type TokenBalance = { diff --git a/src/chains/xrpl/xrpl.ts b/src/chains/xrpl/xrpl.ts index de63335f22..d5d5f0587f 100644 --- a/src/chains/xrpl/xrpl.ts +++ b/src/chains/xrpl/xrpl.ts @@ -9,6 +9,8 @@ import { PathFindStream, TxResponse, TransactionMetadata, + AccountOffersResponse, + dropsToXrp, } from 'xrpl'; import axios from 'axios'; import { promises as fs } from 'fs'; @@ -25,6 +27,7 @@ import { XRPLOrderStorage } from './xrpl.order-storage'; import { OrderTracker } from './xrpl.order-tracker'; import { ReferenceCountingCloseable } from '../../services/refcounting-closeable'; import { XRPLController } from './xrpl.controllers'; +import { convertHexToString } from '../../connectors/xrpl/xrpl.utils'; export type XRPTokenInfo = { id: number; @@ -39,7 +42,9 @@ export type MarketInfo = { id: number; marketId: string; baseIssuer: string; + baseCode: string; quoteIssuer: string; + quoteCode: string; baseTokenID: number; quoteTokenID: number; }; @@ -58,7 +63,7 @@ export class XRPL implements XRPLish { protected tokenList: XRPTokenInfo[] = []; protected marketList: MarketInfo[] = []; - private _tokenMap: Record = {}; + private _tokenMap: Record = {}; // TODO: tokenMap should be identified by code and issuer to prevent duplicate codes private _marketMap: Record = {}; private _client: Client; @@ -71,6 +76,8 @@ export class XRPL implements XRPLish { private _marketListSource: string; private _tokenListType: TokenListType; private _marketListType: MarketListType; + private _reserveBaseXrp: number; + private _reserveIncrementXrp: number; private _ready: boolean = false; private initializing: boolean = false; @@ -91,6 +98,8 @@ export class XRPL implements XRPLish { this._tokenListType = config.network.tokenListType; this._marketListSource = config.network.marketListSource; this._marketListType = config.network.marketListType; + this._reserveBaseXrp = 0; + this._reserveIncrementXrp = 0; this._client = new Client(this.rpcUrl, { timeout: config.requestTimeout, @@ -119,6 +128,10 @@ export class XRPL implements XRPLish { ); this._orderStorage.declareOwnership(this._refCountingHandle); this.controller = XRPLController; + + this.onDisconnected(async (_code: number) => { + this.ensureConnection(); + }); } public static getInstance(network: string): XRPL { @@ -189,6 +202,7 @@ export class XRPL implements XRPLish { if (!this.ready() && !this.initializing) { this.initializing = true; await this.ensureConnection(); + await this.getReserveInfo(); await this.loadTokens(this._tokenListSource, this._tokenListType); await this.loadMarkets(this._marketListSource, this._marketListType); await this.getFee(); @@ -270,7 +284,14 @@ export class XRPL implements XRPLish { } public getTokenForSymbol(code: string): XRPTokenInfo[] | undefined { - return this._tokenMap[code] ? this._tokenMap[code] : undefined; + let query = code; + + // Special case for SOLO on mainnet + if (code === 'SOLO') { + query = '534F4C4F00000000000000000000000000000000'; + } + + return this._tokenMap[query] ? this._tokenMap[query] : undefined; } public getWalletFromSeed(seed: string): Wallet { @@ -339,6 +360,24 @@ export class XRPL implements XRPLish { return balance; } + async getNativeAvailableBalance(wallet: Wallet): Promise { + await this.ensureConnection(); + + const AccountInfoResponse = await this._client.request({ + command: 'account_info', + account: wallet.address, + }); + + const ownerItem = AccountInfoResponse.result.account_data.OwnerCount; + const totalReserve = + this._reserveBaseXrp + ownerItem * this._reserveIncrementXrp; + + const balance = + parseFloat(await this._client.getXrpBalance(wallet.address)) - + totalReserve; + return balance.toString(); + } + async getAllBalance(wallet: Wallet): Promise> { await this.ensureConnection(); const balances: Array = []; @@ -359,7 +398,7 @@ export class XRPL implements XRPLish { } balances.push({ - currency: token.currency, + currency: convertHexToString(token.currency), issuer: token.issuer, value: token.value, }); @@ -383,6 +422,17 @@ export class XRPL implements XRPLish { } } + async getReserveInfo() { + await this.ensureConnection(); + const reserveInfoResp = await this._client.request({ + command: 'server_info', + }); + this._reserveBaseXrp = + reserveInfoResp.result.info.validated_ledger?.reserve_base_xrp ?? 0; + this._reserveIncrementXrp = + reserveInfoResp.result.info.validated_ledger?.reserve_inc_xrp ?? 0; + } + public get chain(): string { return this._chain; } @@ -495,6 +545,67 @@ export class XRPL implements XRPLish { public get orderStorage(): XRPLOrderStorage { return this._orderStorage; } + + async subtractBalancesWithOpenOffers( + balances: TokenBalance[], + wallet: Wallet + ) { + await this.ensureConnection(); + const accountOffcersResp: AccountOffersResponse = + await this._client.request({ + command: 'account_offers', + account: wallet.address, + }); + + const offers = accountOffcersResp.result.offers; + + // create new balances array with deepcopy + const subtractedBalances: TokenBalance[] = JSON.parse( + JSON.stringify(balances) + ); + + // Subtract XRP balance with reverses + const xrpBalance = subtractedBalances.find( + (balance) => balance.currency === 'XRP' + ); + + const currentNativeBalance = await this.getNativeAvailableBalance(wallet); + if (xrpBalance) xrpBalance.value = currentNativeBalance; + + // Subtract balances with open offers + if (offers !== undefined) { + offers.forEach((offer) => { + const takerGetsBalance = offer.taker_gets as TokenBalance | string; + + if (typeof takerGetsBalance === 'string') { + // XRP open offer, find XRP in balances and subtract it + const xrpBalance = subtractedBalances.find( + (balance) => balance.currency === 'XRP' + ); + if (xrpBalance) { + xrpBalance.value = ( + parseFloat(xrpBalance.value) - + parseFloat(dropsToXrp(takerGetsBalance)) + ).toString(); + } + } else { + // Token open offer, find token in balances and subtract it + const tokenBalance = subtractedBalances.find( + (balance) => + balance.currency === convertHexToString(takerGetsBalance.currency) + ); + if (tokenBalance) { + tokenBalance.value = ( + parseFloat(tokenBalance.value) - + parseFloat(takerGetsBalance.value) + ).toString(); + } + } + }); + } + + return subtractedBalances; + } } export type XRPLish = XRPL; diff --git a/src/connectors/xrpl/xrpl.ts b/src/connectors/xrpl/xrpl.ts index f065ee75e4..75fc418bda 100644 --- a/src/connectors/xrpl/xrpl.ts +++ b/src/connectors/xrpl/xrpl.ts @@ -23,6 +23,7 @@ import { getTakerPaysAmount, getTakerGetsFundedAmount, getTakerPaysFundedAmount, + convertHexToString, } from './xrpl.utils'; import { ClobMarketsRequest, @@ -44,7 +45,7 @@ import { getXRPLConfig } from '../../chains/xrpl/xrpl.config'; import { isUndefined } from 'mathjs'; // const XRP_FACTOR = 1000000; -const ORDERBOOK_LIMIT = 100; +const ORDERBOOK_LIMIT = 50; const TXN_SUBMIT_DELAY = 100; export class XRPLCLOB implements CLOBish { private static _instances: LRUCache; @@ -153,11 +154,13 @@ export class XRPLCLOB implements CLOBish { quoteTransferRate: number; const zeroTransferRate = 1000000000; - const [baseCurrency, quoteCurrency] = market.marketId.split('-'); + const baseCurrency = market.baseCode; + const quoteCurrency = market.quoteCode; const baseIssuer = market.baseIssuer; const quoteIssuer = market.quoteIssuer; if (baseCurrency != 'XRP') { + await this._xrpl.ensureConnection(); const baseMarketResp: AccountInfoResponse = await this._client.request({ command: 'account_info', ledger_index: 'validated', @@ -179,6 +182,7 @@ export class XRPLCLOB implements CLOBish { } if (quoteCurrency != 'XRP') { + await this._xrpl.ensureConnection(); const quoteMarketResp: AccountInfoResponse = await this._client.request({ command: 'account_info', ledger_index: 'validated', @@ -220,26 +224,27 @@ export class XRPLCLOB implements CLOBish { } async getOrderBook( - market: MarketInfo, + market: Market, limit: number = ORDERBOOK_LIMIT ): Promise { - const [baseCurrency, quoteCurrency] = market.marketId.split('-'); - const baseIssuer = market.baseIssuer; - const quoteIssuer = market.quoteIssuer; - const baseRequest: any = { - currency: baseCurrency, + currency: market.baseCurrency, + issuer: market.baseIssuer, }; const quoteRequest: any = { - currency: quoteCurrency, + currency: market.quoteCurrency, + issuer: market.quoteIssuer, }; - if (baseIssuer) { - baseRequest['issuer'] = baseIssuer; + if (market.baseCurrency == 'XRP') { + // remove issuer + delete baseRequest['issuer']; } - if (quoteIssuer) { - quoteRequest['issuer'] = quoteIssuer; + + if (market.quoteCurrency == 'XRP') { + // remove issuer + delete quoteRequest['issuer']; } const { bids, asks } = await this.getOrderBookFromXRPL( @@ -380,7 +385,8 @@ export class XRPLCLOB implements CLOBish { req: ClobPostOrderRequest ): Promise<{ txHash: string }> { const market = this.parsedMarkets[req.market] as Market; - const [baseCurrency, quoteCurrency] = market.marketId.split('-'); + const baseCurrency = market.baseCurrency; + const quoteCurrency = market.quoteCurrency; const baseIssuer = market.baseIssuer; const quoteIssuer = market.quoteIssuer; @@ -444,7 +450,10 @@ export class XRPLCLOB implements CLOBish { const order: Order = { hash: prepared.Sequence ? prepared.Sequence : 0, - marketId: baseCurrency + '-' + quoteCurrency, + marketId: + convertHexToString(baseCurrency) + + '-' + + convertHexToString(quoteCurrency), price: req.price, amount: req.amount, filledAmount: '0', @@ -524,6 +533,7 @@ export class XRPLCLOB implements CLOBish { this._isSubmittingTxn = true; const prepared = await this._client.autofill(offer); const signed = wallet.sign(prepared); + await this._xrpl.ensureConnection(); await this._client.submit(signed.tx_blob); this._isSubmittingTxn = false; return { prepared, signed }; @@ -539,6 +549,7 @@ export class XRPLCLOB implements CLOBish { quoteRequest: any, limit: number ) { + await this._xrpl.ensureConnection(); const orderbook_resp_ask: BookOffersResponse = await this._client.request({ command: 'book_offers', ledger_index: 'validated', @@ -547,6 +558,7 @@ export class XRPLCLOB implements CLOBish { limit, }); + await this._xrpl.ensureConnection(); const orderbook_resp_bid: BookOffersResponse = await this._client.request({ command: 'book_offers', ledger_index: 'validated', @@ -561,6 +573,7 @@ export class XRPLCLOB implements CLOBish { } private async getCurrentBlockNumber() { + await this._xrpl.ensureConnection(); return await this._client.getLedgerIndex(); } @@ -595,7 +608,7 @@ export class XRPLCLOB implements CLOBish { await orderTracker.addOrder(order); } - private async getMidPriceForMarket(market: MarketInfo) { + private async getMidPriceForMarket(market: Market) { const orderbook = await this.getOrderBook(market, 1); try { const bestAsk = orderbook.sells[0]; diff --git a/src/connectors/xrpl/xrpl.utils.ts b/src/connectors/xrpl/xrpl.utils.ts index 78c5f4008a..ba36bf655e 100644 --- a/src/connectors/xrpl/xrpl.utils.ts +++ b/src/connectors/xrpl/xrpl.utils.ts @@ -112,3 +112,13 @@ export async function getsSequenceNumberFromTxn( return undefined; } + +// check if string is 160-bit hexadecimal, if so convert it to string +export function convertHexToString(hex: string): string { + if (hex.length === 40) { + const str = Buffer.from(hex, 'hex').toString(); + return str.replace(/\0/g, ''); + } + + return hex; +} diff --git a/src/templates/lists/xrpl_markets.json b/src/templates/lists/xrpl_markets.json index b385debb72..ec4a4a1e9b 100644 --- a/src/templates/lists/xrpl_markets.json +++ b/src/templates/lists/xrpl_markets.json @@ -3,7 +3,9 @@ "id": 1, "marketId": "SOLO-XRP", "baseIssuer": "rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz", + "baseCode": "534F4C4F00000000000000000000000000000000", "quoteIssuer": "", + "quoteCode": "XRP", "baseTokenID": 31, "quoteTokenID": 0 }, @@ -11,7 +13,9 @@ "id": 2, "marketId": "SOLO-USDC", "baseIssuer": "rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz", + "baseCode": "534F4C4F00000000000000000000000000000000", "quoteIssuer": "rcEGREd8NmkKRE8GE424sksyt1tJVFZwu", + "quoteCode": "USDC", "baseTokenID": 31, "quoteTokenID": 18465 }, @@ -19,7 +23,9 @@ "id": 3, "marketId": "USDC-XRP", "baseIssuer": "rcEGREd8NmkKRE8GE424sksyt1tJVFZwu", + "baseCode": "USDC", "quoteIssuer": "", + "quoteCode": "XRP", "baseTokenID": 18465, "quoteTokenID": 0 }, @@ -27,7 +33,9 @@ "id": 4, "marketId": "USDC-USD", "baseIssuer": "rcEGREd8NmkKRE8GE424sksyt1tJVFZwu", + "baseCode": "USDC", "quoteIssuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq", + "quoteCode": "USD", "baseTokenID": 18465, "quoteTokenID": 16603 }, @@ -35,7 +43,9 @@ "id": 5, "marketId": "BTC-XRP", "baseIssuer": "rchGBxcD1A1C2tdxF6papQYZ8kjRKMYcL", + "baseCode": "BTC", "quoteIssuer": "", + "quoteCode": "XRP", "baseTokenID": 1381, "quoteTokenID": 0 }, @@ -43,8 +53,21 @@ "id": 6, "marketId": "BTC-USD", "baseIssuer": "rchGBxcD1A1C2tdxF6papQYZ8kjRKMYcL", + "baseCode": "BTC", "quoteIssuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq", + "quoteCode": "USD", "baseTokenID": 1381, "quoteTokenID": 16603 } + , + { + "id": 7, + "marketId": "XRP-USD", + "baseIssuer": "", + "baseCode": "XRP", + "quoteIssuer": "rhub8VRN55s94qWKDv6jmDy1pUykJzF3wq", + "quoteCode": "USD", + "baseTokenID": 0, + "quoteTokenID": 16603 + } ] diff --git a/src/templates/lists/xrpl_tokens.json b/src/templates/lists/xrpl_tokens.json index 2cf5cadba0..c343b41892 100644 --- a/src/templates/lists/xrpl_tokens.json +++ b/src/templates/lists/xrpl_tokens.json @@ -9,7 +9,7 @@ }, { "id": 31, - "code": "SOLO", + "code": "534F4C4F00000000000000000000000000000000", "issuer": "rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz", "title": "Sologenic", "trustlines": 288976,