Skip to content

Commit

Permalink
Implement XRP DEX order parsing -> EdgeTxAction's
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon-edge committed Oct 11, 2023
1 parent 2798d82 commit 3cba915
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- added: Parse XRP DEX orders into EdgeTxActions

## 2.6.0 (2023-10-09)

- changed: Upgrade react-native-piratechain to v0.4.0
Expand Down
193 changes: 191 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,158 @@ export class XrpEngine extends CurrencyEngine<
}
}

/**
* Parse TakerGets or TakerPays into an EdgeAssetAmount
* */
parseRippleDexTxAmount = (
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
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
let sourceAsset
let destAsset
// 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)
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.log.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) {
this.log.warn(
'processRippleDexTx: no sourceAsset or destAsset for canceled offer'
)
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 +515,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 +552,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 +575,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
>

0 comments on commit 3cba915

Please sign in to comment.