Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement XRP DEX order parsing -> EdgeTxAction's #641

Merged
merged 1 commit into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# edge-currency-accountbased

## Unreleased
- added: Parse XRP DEX orders into EdgeTxActions

## 2.8.2 (2023-10-25)

Expand Down
194 changes: 192 additions & 2 deletions src/ripple/RippleEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,28 @@ import {
EdgeActivationApproveOptions,
EdgeActivationQuote,
EdgeActivationResult,
EdgeAssetAmount,
EdgeCurrencyEngine,
EdgeCurrencyEngineOptions,
EdgeEngineActivationOptions,
EdgeEngineGetActivationAssetsOptions,
EdgeGetActivationAssetsResults,
EdgeSpendInfo,
EdgeTransaction,
EdgeTxActionSwap,
EdgeTxActionSwapType,
EdgeWalletInfo,
InsufficientFundsError,
JsonObject,
NoAmountSpecifiedError
} from 'edge-core-js/types'
import { base16 } from 'rfc4648'
import {
DeletedNode,
getBalanceChanges,
isCreatedNode,
isDeletedNode,
isModifiedNode,
OfferCreate,
Payment as PaymentJson,
rippleTimeToUnixTime,
Expand All @@ -38,7 +45,10 @@ import {
Wallet
} from 'xrpl'
import { Amount } from 'xrpl/dist/npm/models/common'
import { AccountTxResponse } from 'xrpl/dist/npm/models/methods/accountTx'
import {
AccountTxResponse,
AccountTxTransaction
} from 'xrpl/dist/npm/models/methods/accountTx'
import { validatePayment } from 'xrpl/dist/npm/models/transactions/payment'

import { CurrencyEngine } from '../common/CurrencyEngine'
Expand All @@ -55,12 +65,14 @@ import {
import { DIVIDE_PRECISION, EST_BLOCK_TIME_MS } from './rippleInfo'
import { RippleTools } from './RippleTools'
import {
asFinalFieldsCanceledOffer,
asMaybeActivateTokenParams,
asRipplePrivateKeys,
asSafeRippleWalletInfo,
asXrpNetworkLocation,
asXrpTransaction,
asXrpWalletOtherData,
FinalFieldsCanceledOffer,
MakeTxParams,
RippleOtherMethods,
SafeRippleWalletInfo,
Expand Down Expand Up @@ -161,8 +173,24 @@ export class XrpEngine extends CurrencyEngine<
fromTokenId == null
? this.currencyInfo
: this.allTokensMap[fromTokenId]
const { pluginId } = this.currencyInfo

const out: EdgeTransaction = {
action: {
type: 'swapOrderPost',
orderId: undefined,
canBePartial: true,
sourceAsset: {
pluginId,
tokenId: fromTokenId,
nativeAmount: fromNativeAmount
},
destAsset: {
pluginId,
tokenId: toTokenId,
nativeAmount: toNativeAmount
}
},
blockHeight: 0, // blockHeight,
currencyCode,
date: Date.now() / 1000,
Expand Down Expand Up @@ -275,6 +303,159 @@ export class XrpEngine extends CurrencyEngine<
}
}

/**
* Parse TakerGets or TakerPays into an EdgeAssetAmount
* */
parseRippleDexTxAmount = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace most of parseRippleDexTxAmount and processRippleDexTx with getBalances to determine src/dest amounts of order fulfillments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I had it done is more accurate and complete according to xrpscan.com.

The balances object is good for including mainnet fees in the xrp amount total, but for some reason underreports the actual amounts involved in the trades (regardless of fees - token amounts are underreported). Also there's no way to tell unfilled order amounts from the Balances obj either.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comparing against xrpscan.com shows alignment between my amounts and the summary amounts given by api queries to xrp explorers

takerAmount: Amount
): EdgeAssetAmount | undefined => {
const {
currency,
issuer,
value
// Taker pays/gets XRP if 'TakerPays/Gets' is a plain string
} =
typeof takerAmount === 'string'
? { currency: 'XRP', issuer: undefined, value: takerAmount }
: takerAmount
const isTakerToken = currency !== 'XRP' && issuer != null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be an assert case if currency !== 'XRP' && issuer == null. Token check is just currency !== 'XRP'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add the assert, but we still need to keep the entire isTakerToken statement, or else makeTokenId right after complains

if (isTakerToken && issuer == null) {
this.error('parseRippleDexTxAmount: No ussuer for token')
return
}
const tokenId = isTakerToken
? makeTokenId({
currency,
issuer
})
: undefined

const takerVal = isTakerToken ? value : String(takerAmount)

if (takerVal == null) {
this.error(
`parseRippleDexTxAmount: Transaction has token code ${currency} with no value`
)
return
}
const takerDenom =
tokenId == null
? this.currencyInfo.denominations[0]
: this.builtinTokens[tokenId].denominations[0]
if (takerDenom == null) {
this.error(`parseRippleDexTxAmount: Unknown denom ${currency}`)
return
}
const nativeAmount = mul(takerVal, takerDenom.multiplier)

return {
nativeAmount,
pluginId: this.currencyInfo.pluginId,
tokenId
}
}

/**
* Parse potential DEX trades.
* Parse offer-related nodes to determine order status for saving to the
* EdgeTxAction
**/
processRippleDexTx = (
accountTx: AccountTxTransaction
): EdgeTxActionSwap | undefined => {
const { meta, tx } = accountTx
if (tx == null || typeof meta !== 'object') return

const { AffectedNodes } = meta
const deletedNodes = AffectedNodes.filter(
node =>
isDeletedNode(node) && node.DeletedNode.LedgerEntryType === 'Offer'
) as DeletedNode[]
const hasDeletedNodes = deletedNodes.length > 0
const hasModifiedNodes =
AffectedNodes.filter(
node =>
isModifiedNode(node) && node.ModifiedNode.LedgerEntryType === 'Offer'
).length > 0
const createdNodes = AffectedNodes.filter(
node =>
isCreatedNode(node) && node.CreatedNode.LedgerEntryType === 'Offer'
)
// Shouldn't happen. Only possible to have one created node per order tx
if (createdNodes.length > 1) {
this.error('processRippleDexTx: OfferCreate: multiple created nodes')
return
}

let type: EdgeTxActionSwapType | undefined
let sourceAsset: EdgeAssetAmount | undefined
let destAsset: EdgeAssetAmount | undefined
// Any kind of limit order state - post (open & unfilled), partially
// filled, fully filled, but NOT canceled.
if (tx.TransactionType === 'OfferCreate') {
// Exactly one node was created. Order opened without any fills
const isOpenOrder = createdNodes.length === 1 // check modifiedNodes?

// Either an existing order that had partial fills, OR
// a new order that only matched exact offer amounts in the book
const isPartiallyFilled =
hasModifiedNodes || (isOpenOrder && hasDeletedNodes)

// Order was fully filled
const isFullyFilled = hasDeletedNodes && !isOpenOrder

// Don't care about partial fills - counting them as general fills
type =
isFullyFilled || isPartiallyFilled ? 'swapOrderFill' : 'swapOrderPost'

// Parse amounts
const { TakerPays, TakerGets } = tx
sourceAsset = this.parseRippleDexTxAmount(TakerGets)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This always get the amounts from the tx object. But in the case of a partial order fulfillment, the correct amounts are in the Nodes.

Copy link
Contributor Author

@Jon-edge Jon-edge Oct 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't actually display the amounts from the GUI at this time.

This implementation is tracking 2 things consistently across all order states: the state of the order, and the posted order amount.

Fill amount is a completely separate thing and I think is a mistake to conflate into the source/destAsset amounts described here. We also don't differentiate full and partial fills in the GUI right now, either.

If we only track fill amount, we actually are mis-reporting later down the road when the order status changes. We won't have knowledge of that unless we go back and modify related transactions as we process newer transactions. Imo, fill amount should be a completely separate field.

At this point in time, the only thing we are certain of is the total amounts of the original offer. Even if we have a partial fill immediately upon posting, we don't know if a later tx from someone else further modified the fill amount.

destAsset = this.parseRippleDexTxAmount(TakerPays)
} else if (tx.TransactionType === 'OfferCancel') {
// Assert only one offer is canceled per OfferCancel transaction
if (deletedNodes.length > 1) {
this.error('processRippleDexTx: OfferCancel: multiple deleted nodes')
return
}
if (deletedNodes.length === 1) {
// Reference the canceled offer for asset types/amounts
let canceledOffer: FinalFieldsCanceledOffer
try {
canceledOffer = asFinalFieldsCanceledOffer(
deletedNodes[0].DeletedNode.FinalFields
)
} catch (error) {
this.error(`Cleaning DeletedNodes FinalFields failed: ${error}`)
return
}
type = 'swapOrderCancel'

// Parse amounts
const { TakerPays, TakerGets } = canceledOffer
sourceAsset = this.parseRippleDexTxAmount(TakerGets)
destAsset = this.parseRippleDexTxAmount(TakerPays)
} else {
// The offer could not be canceled, possibly because it was already filled or expired
this.log.warn(
'processRippleDexTx: OfferCancel: without actual cancellation'
)
return
}
}

if (sourceAsset == null || destAsset == null || type == null) {
return
}

// Succeeded all checks
return {
type,
sourceAsset,
destAsset
}
}

processRippleTransaction(accountTx: AccountTransaction): void {
const { log } = this
const { publicKey: publicAddress } = this.walletLocalData
Expand Down Expand Up @@ -335,6 +516,7 @@ export class XrpEngine extends CurrencyEngine<
}
// Parent currency like XRP
this.addTransaction(currency, {
action: this.processRippleDexTx(accountTx),
blockHeight: tx.ledger_index ?? -1,
currencyCode: currency,
date: rippleTimeToUnixTime(date) / 1000, // Returned date is in "ripple time" which is unix time if it had started on Jan 1 2000
Expand Down Expand Up @@ -371,6 +553,7 @@ export class XrpEngine extends CurrencyEngine<
}

this.addTransaction(currencyCode, {
action: this.processRippleDexTx(accountTx),
blockHeight: tx.ledger_index ?? -1,
currencyCode,
date: rippleTimeToUnixTime(date) / 1000, // Returned date is in "ripple time" which is unix time if it had started on Jan 1 2000
Expand All @@ -393,7 +576,14 @@ export class XrpEngine extends CurrencyEngine<
const blockHeight = this.walletLocalData.blockHeight
const address = this.walletLocalData.publicKey
let startBlock: number = -1 // A value of -1 instructs the server to use the earliest validated ledger version available
if (

// See if we need to add new data to the existing EdgeTransactions on disk
if (this.otherData.txListReset) {
this.log('Resetting Ripple tx list...')
this.otherData.txListReset = false
this.walletLocalData.lastAddressQueryHeight = 0
this.walletLocalDataDirty = true
} else if (
this.walletLocalData.lastAddressQueryHeight >
ADDRESS_QUERY_LOOKBACK_BLOCKS
) {
Expand Down
33 changes: 31 additions & 2 deletions src/ripple/rippleTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { asMaybe, asNumber, asObject, asOptional, asString } from 'cleaners'
import {
asBoolean,
asEither,
asMaybe,
asNumber,
asObject,
asOptional,
asString
} from 'cleaners'
import { EdgeMetadata, EdgeTransaction, EdgeTxSwap } from 'edge-core-js/types'

import { asSafeCommonWalletInfo } from '../common/types'
Expand All @@ -24,7 +32,11 @@ export const asMaybeActivateTokenParams = asMaybe(
)

export const asXrpWalletOtherData = asObject({
recommendedFee: asMaybe(asString, '0') // Floating point value in full XRP value
// A one-time flag to re-process transactions to add new data
txListReset: asMaybe(asBoolean, true),

// Floating point value in full XRP value
recommendedFee: asMaybe(asString, '0')
})

export type XrpWalletOtherData = ReturnType<typeof asXrpWalletOtherData>
Expand Down Expand Up @@ -77,3 +89,20 @@ export type MakeTxParams =
export interface RippleOtherMethods {
makeTx: (makeTxParams: MakeTxParams) => Promise<EdgeTransaction>
}

// Nice-to-haves missing from xrpl lib:
export const asIssuedCurrencyAmount = asObject({
currency: asString,
issuer: asString,
value: asString
})
export const asAmount = asEither(asIssuedCurrencyAmount, asString)

export const asFinalFieldsCanceledOffer = asObject({
TakerPays: asAmount,
TakerGets: asAmount
// Add other fields that might appear in `FinalFields` as needed
})
export type FinalFieldsCanceledOffer = ReturnType<
typeof asFinalFieldsCanceledOffer
>