-
Notifications
You must be signed in to change notification settings - Fork 39
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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' | ||
|
@@ -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, | ||
|
@@ -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, | ||
|
@@ -275,6 +303,159 @@ 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be an assert case if There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This always get the amounts from the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 | ||
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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 | ||
) { | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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