go-ethereum/XDCxlending/XDCxlending.go
2024-12-28 09:06:31 +08:00

932 lines
40 KiB
Go

package XDCxlending
import (
"encoding/json"
"errors"
"fmt"
"math/big"
"strconv"
"time"
"github.com/XinFinOrg/XDPoSChain/XDCx"
"github.com/XinFinOrg/XDPoSChain/XDCx/tradingstate"
"github.com/XinFinOrg/XDPoSChain/XDCxDAO"
"github.com/XinFinOrg/XDPoSChain/XDCxlending/lendingstate"
"github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/common/lru"
"github.com/XinFinOrg/XDPoSChain/common/prque"
"github.com/XinFinOrg/XDPoSChain/consensus"
"github.com/XinFinOrg/XDPoSChain/core/state"
"github.com/XinFinOrg/XDPoSChain/core/types"
"github.com/XinFinOrg/XDPoSChain/log"
"github.com/XinFinOrg/XDPoSChain/p2p"
"github.com/XinFinOrg/XDPoSChain/rpc"
)
const (
ProtocolName = "XDCxlending"
ProtocolVersion = uint64(1)
ProtocolVersionStr = "1.0"
defaultCacheLimit = 1024
)
var (
ErrNonceTooHigh = errors.New("nonce too high")
ErrNonceTooLow = errors.New("nonce too low")
)
type Lending struct {
Triegc *prque.Prque[int64, common.Hash] // Priority queue mapping block numbers to tries to gc
StateCache lendingstate.Database // State database to reuse between imports (contains state cache) *lendingstate.TradingStateDB
orderNonce map[common.Address]*big.Int
XDCx *XDCx.XDCX
lendingItemHistory *lru.Cache[common.Hash, map[common.Hash]lendingstate.LendingItemHistoryItem]
lendingTradeHistory *lru.Cache[common.Hash, map[common.Hash]lendingstate.LendingTradeHistoryItem]
}
func (l *Lending) Protocols() []p2p.Protocol {
return []p2p.Protocol{}
}
func (l *Lending) Start(server *p2p.Server) error {
return nil
}
func (l *Lending) Stop() error {
return nil
}
func New(XDCx *XDCx.XDCX) *Lending {
lending := &Lending{
orderNonce: make(map[common.Address]*big.Int),
Triegc: prque.New[int64, common.Hash](nil),
lendingItemHistory: lru.NewCache[common.Hash, map[common.Hash]lendingstate.LendingItemHistoryItem](defaultCacheLimit),
lendingTradeHistory: lru.NewCache[common.Hash, map[common.Hash]lendingstate.LendingTradeHistoryItem](defaultCacheLimit),
}
lending.StateCache = lendingstate.NewDatabase(XDCx.GetLevelDB())
lending.XDCx = XDCx
return lending
}
func (l *Lending) GetLevelDB() XDCxDAO.XDCXDAO {
return l.XDCx.GetLevelDB()
}
func (l *Lending) GetMongoDB() XDCxDAO.XDCXDAO {
return l.XDCx.GetMongoDB()
}
// APIs returns the RPC descriptors the Lending implementation offers
func (l *Lending) APIs() []rpc.API {
return []rpc.API{
{
Namespace: ProtocolName,
Version: ProtocolVersionStr,
Service: NewPublicXDCXLendingAPI(l),
Public: true,
},
}
}
// Version returns the Lending sub-protocols version number.
func (l *Lending) Version() uint64 {
return ProtocolVersion
}
func (l *Lending) ProcessOrderPending(header *types.Header, coinbase common.Address, chain consensus.ChainContext, pending map[common.Address]types.LendingTransactions, statedb *state.StateDB, lendingStatedb *lendingstate.LendingStateDB, tradingStateDb *tradingstate.TradingStateDB) ([]*lendingstate.LendingItem, map[common.Hash]lendingstate.MatchingResult) {
lendingItems := []*lendingstate.LendingItem{}
matchingResults := map[common.Hash]lendingstate.MatchingResult{}
txs := types.NewLendingTransactionByNonce(types.LendingTxSigner{}, pending)
for {
tx := txs.Peek()
if tx == nil {
break
}
log.Debug("ProcessOrderPending start", "len", len(pending))
log.Debug("Get pending orders to process", "address", tx.UserAddress(), "nonce", tx.Nonce())
V, R, S := tx.Signature()
bigstr := V.String()
n, e := strconv.ParseInt(bigstr, 10, 8)
if e != nil {
continue
}
order := &lendingstate.LendingItem{
Nonce: big.NewInt(int64(tx.Nonce())),
Quantity: tx.Quantity(),
Interest: new(big.Int).SetUint64(tx.Interest()),
Relayer: tx.RelayerAddress(),
Term: tx.Term(),
UserAddress: tx.UserAddress(),
LendingToken: tx.LendingToken(),
CollateralToken: tx.CollateralToken(),
AutoTopUp: tx.AutoTopUp(),
Status: tx.Status(),
Side: tx.Side(),
Type: tx.Type(),
Hash: tx.LendingHash(),
LendingId: tx.LendingId(),
LendingTradeId: tx.LendingTradeId(),
ExtraData: tx.ExtraData(),
Signature: &lendingstate.Signature{
V: byte(n),
R: common.BigToHash(R),
S: common.BigToHash(S),
},
}
log.Info("Process order pending", "orderPending", order, "LendingToken", order.LendingToken.Hex(), "CollateralToken", order.CollateralToken)
originalOrder := &lendingstate.LendingItem{}
*originalOrder = *order
originalOrder.Quantity = lendingstate.CloneBigInt(order.Quantity)
newTrades, newRejectedOrders, err := l.CommitOrder(header, coinbase, chain, statedb, lendingStatedb, tradingStateDb, lendingstate.GetLendingOrderBookHash(order.LendingToken, order.Term), order)
for _, reject := range newRejectedOrders {
log.Debug("Reject order", "reject", *reject)
}
switch err {
case ErrNonceTooLow:
// New head notification data race between the transaction pool and miner, shift
log.Debug("Skipping order with low nonce", "sender", tx.UserAddress(), "nonce", tx.Nonce())
txs.Shift()
continue
case ErrNonceTooHigh:
// Reorg notification data race between the transaction pool and miner, skip account =
log.Debug("Skipping order account with high nonce", "sender", tx.UserAddress(), "nonce", tx.Nonce())
txs.Pop()
continue
case nil:
// everything ok
txs.Shift()
default:
// Strange error, discard the transaction and get the next in line (note, the
// nonce-too-high clause will prevent us from executing in vain).
log.Debug("Transaction failed, account skipped", "hash", tx.Hash(), "err", err)
txs.Shift()
continue
}
// orderID has been updated
originalOrder.LendingId = order.LendingId
originalOrder.ExtraData = order.ExtraData
lendingItems = append(lendingItems, originalOrder)
matchingResults[lendingstate.GetLendingCacheKey(order)] = lendingstate.MatchingResult{
Trades: newTrades,
Rejects: newRejectedOrders,
}
}
return lendingItems, matchingResults
}
// there are 3 tasks need to complete (for SDK nodes) after matching
// 1. Put takerLendingItem to database
// 2.a Update status, filledAmount of makerLendingItem
// 2.b. Put lendingTrade to database
// 3. Update status of rejected items
func (l *Lending) SyncDataToSDKNode(chain consensus.ChainContext, statedb *state.StateDB, block *types.Block, takerLendingItem *lendingstate.LendingItem, txHash common.Hash, txMatchTime time.Time, trades []*lendingstate.LendingTrade, rejectedItems []*lendingstate.LendingItem, dirtyOrderCount *uint64) error {
var (
// originTakerLendingItem: item getting from database
originTakerLendingItem, updatedTakerLendingItem *lendingstate.LendingItem
makerDirtyHashes []string
makerDirtyFilledAmount map[string]*big.Int
err error
)
db := l.GetMongoDB()
db.InitLendingBulk()
if takerLendingItem.Status == lendingstate.LendingStatusCancelled && len(rejectedItems) > 0 {
// cancel order is rejected -> nothing change
log.Debug("Cancel order is rejected", "order", lendingstate.ToJSON(takerLendingItem))
return nil
}
// 1. put processed takerLendingItem to database
lastState := lendingstate.LendingItemHistoryItem{}
// Typically, takerItem has never existed in database
// except cancel case: in this case, item existed in database with status = OPEN, then use send another lendingItem to cancel it
val, err := db.GetObject(takerLendingItem.Hash, &lendingstate.LendingItem{Type: takerLendingItem.Type})
if err == nil && val != nil {
originTakerLendingItem = val.(*lendingstate.LendingItem)
lastState = lendingstate.LendingItemHistoryItem{
TxHash: originTakerLendingItem.TxHash,
FilledAmount: lendingstate.CloneBigInt(originTakerLendingItem.FilledAmount),
Status: originTakerLendingItem.Status,
UpdatedAt: originTakerLendingItem.UpdatedAt,
}
}
if originTakerLendingItem != nil {
updatedTakerLendingItem = originTakerLendingItem
} else {
updatedTakerLendingItem = takerLendingItem
updatedTakerLendingItem.FilledAmount = new(big.Int)
}
if takerLendingItem.Status == lendingstate.LendingStatusNew {
updatedTakerLendingItem.Status = lendingstate.LendingStatusOpen
} else if takerLendingItem.Status == lendingstate.LendingStatusCancelled {
updatedTakerLendingItem.Status = lendingstate.LendingStatusCancelled
updatedTakerLendingItem.ExtraData = takerLendingItem.ExtraData
}
updatedTakerLendingItem.TxHash = txHash
if updatedTakerLendingItem.CreatedAt.IsZero() {
updatedTakerLendingItem.CreatedAt = txMatchTime
}
if txMatchTime.Before(updatedTakerLendingItem.UpdatedAt) || (txMatchTime.Equal(updatedTakerLendingItem.UpdatedAt) && *dirtyOrderCount == 0) {
log.Debug("Ignore old lendingItem/lendingTrades taker", "txHash", txHash.Hex(), "txTime", txMatchTime.UnixNano(), "updatedAt", updatedTakerLendingItem.UpdatedAt.UnixNano())
return nil
}
*dirtyOrderCount++
l.UpdateLendingItemCache(updatedTakerLendingItem.LendingToken, updatedTakerLendingItem.CollateralToken, updatedTakerLendingItem.Hash, txHash, lastState)
updatedTakerLendingItem.UpdatedAt = txMatchTime
// 2. put trades to database and update status
log.Debug("Got lendingTrades", "number", len(trades), "txhash", txHash.Hex())
makerDirtyFilledAmount = make(map[string]*big.Int)
tradeList := map[common.Hash]*lendingstate.LendingTrade{}
for _, tradeRecord := range trades {
// 2.a. put to trades
if tradeRecord == nil {
continue
}
if updatedTakerLendingItem.Type == lendingstate.Repay || updatedTakerLendingItem.Type == lendingstate.TopUp || updatedTakerLendingItem.Type == lendingstate.Recall {
// repay, topup: assign hash = trade.hash
updatedTakerLendingItem.Hash = tradeRecord.Hash
updatedTakerLendingItem.CollateralToken = tradeRecord.CollateralToken
updatedTakerLendingItem.FilledAmount = updatedTakerLendingItem.Quantity
updatedTakerLendingItem.Interest = new(big.Int).SetUint64(tradeRecord.Interest)
switch updatedTakerLendingItem.Type {
case lendingstate.TopUp:
updatedTakerLendingItem.Status = lendingstate.TopUp
extraData, _ := json.Marshal(struct {
Price *big.Int
}{
Price: new(big.Int).Div(new(big.Int).Mul(tradeRecord.LiquidationPrice, tradeRecord.DepositRate), tradeRecord.LiquidationRate),
})
updatedTakerLendingItem.ExtraData = string(extraData)
// manual topUp item
updatedTakerLendingItem.AutoTopUp = false
case lendingstate.Repay:
updatedTakerLendingItem.Status = lendingstate.Repay
paymentBalance := lendingstate.CalculateTotalRepayValue(block.Time().Uint64(), tradeRecord.LiquidationTime, tradeRecord.Term, tradeRecord.Interest, tradeRecord.Amount)
updatedTakerLendingItem.Quantity = paymentBalance
updatedTakerLendingItem.FilledAmount = paymentBalance
// manual repay item
updatedTakerLendingItem.AutoTopUp = false
case lendingstate.Recall:
updatedTakerLendingItem.Status = lendingstate.Recall
// manual recall item
updatedTakerLendingItem.AutoTopUp = false
}
log.Debug("UpdateLendingTrade:", "type", updatedTakerLendingItem.Type, "hash", tradeRecord.Hash.Hex(), "status", tradeRecord.Status, "tradeId", tradeRecord.TradeId)
tradeList[tradeRecord.Hash] = tradeRecord
continue
}
if tradeRecord.CreatedAt.IsZero() {
tradeRecord.CreatedAt = txMatchTime
}
tradeRecord.UpdatedAt = txMatchTime
tradeRecord.TxHash = txHash
tradeRecord.Hash = tradeRecord.ComputeHash()
tradeList[tradeRecord.Hash] = tradeRecord
// 2.b. update status and filledAmount
filledAmount := new(big.Int)
if tradeRecord.Amount != nil {
filledAmount = lendingstate.CloneBigInt(tradeRecord.Amount)
}
// maker dirty order
makerFilledAmount := big.NewInt(0)
var makerOrderHash common.Hash
if updatedTakerLendingItem.Side == lendingstate.Borrowing {
makerOrderHash = tradeRecord.InvestingOrderHash
} else {
makerOrderHash = tradeRecord.BorrowingOrderHash
}
if amount, ok := makerDirtyFilledAmount[makerOrderHash.Hex()]; ok {
makerFilledAmount = lendingstate.CloneBigInt(amount)
}
makerFilledAmount = new(big.Int).Add(makerFilledAmount, filledAmount)
makerDirtyFilledAmount[makerOrderHash.Hex()] = makerFilledAmount
makerDirtyHashes = append(makerDirtyHashes, makerOrderHash.Hex())
if updatedTakerLendingItem.Type == lendingstate.Limit || updatedTakerLendingItem.Type == lendingstate.Market {
//updatedTakerOrder = l.updateMatchedOrder(updatedTakerOrder, filledAmount, txMatchTime, txHash)
// update filledAmount, status of takerOrder
updatedTakerLendingItem.FilledAmount = new(big.Int).Add(updatedTakerLendingItem.FilledAmount, filledAmount)
if updatedTakerLendingItem.FilledAmount.Cmp(updatedTakerLendingItem.Quantity) < 0 && updatedTakerLendingItem.Type == lendingstate.Limit {
updatedTakerLendingItem.Status = lendingstate.LendingStatusPartialFilled
} else {
updatedTakerLendingItem.Status = lendingstate.LendingStatusFilled
}
}
}
if err := l.UpdateLendingTrade(tradeList, txHash, txMatchTime); err != nil {
return err
}
// for Market orders
// filledAmount > 0 : FILLED
// otherwise: REJECTED
if updatedTakerLendingItem.Type == lendingstate.Market {
if updatedTakerLendingItem.FilledAmount.Sign() > 0 {
updatedTakerLendingItem.Status = lendingstate.LendingStatusFilled
} else {
updatedTakerLendingItem.Status = lendingstate.LendingStatusReject
}
}
log.Debug("PutObject processed takerLendingItem",
"term", updatedTakerLendingItem.Term, "userAddr", updatedTakerLendingItem.UserAddress.Hex(), "side", updatedTakerLendingItem.Side,
"Interest", updatedTakerLendingItem.Interest, "quantity", updatedTakerLendingItem.Quantity, "filledAmount", updatedTakerLendingItem.FilledAmount, "status", updatedTakerLendingItem.Status,
"hash", updatedTakerLendingItem.Hash.Hex(), "txHash", updatedTakerLendingItem.TxHash.Hex())
if !(updatedTakerLendingItem.Type == lendingstate.Repay || updatedTakerLendingItem.Type == lendingstate.TopUp || updatedTakerLendingItem.Type == lendingstate.Recall) || updatedTakerLendingItem.Status != lendingstate.LendingStatusOpen {
if err := db.PutObject(updatedTakerLendingItem.Hash, updatedTakerLendingItem); err != nil {
return fmt.Errorf("SDKNode: failed to put processed takerOrder. Hash: %s Error: %s", updatedTakerLendingItem.Hash.Hex(), err.Error())
}
}
items := db.GetListItemByHashes(makerDirtyHashes, &lendingstate.LendingItem{})
if items != nil {
makerItems := items.([]*lendingstate.LendingItem)
log.Debug("Maker dirty lendingItem", "len", len(makerItems), "txhash", txHash.Hex())
for _, m := range makerItems {
if txMatchTime.Before(m.UpdatedAt) {
log.Debug("Ignore old lendingItem/lendingTrades maker", "txHash", txHash.Hex(), "txTime", txMatchTime.UnixNano(), "updatedAt", m.UpdatedAt.UnixNano())
continue
}
lastState = lendingstate.LendingItemHistoryItem{
TxHash: m.TxHash,
FilledAmount: lendingstate.CloneBigInt(m.FilledAmount),
Status: m.Status,
UpdatedAt: m.UpdatedAt,
}
l.UpdateLendingItemCache(m.LendingToken, m.CollateralToken, m.Hash, txHash, lastState)
m.TxHash = txHash
m.UpdatedAt = txMatchTime
m.FilledAmount = new(big.Int).Add(m.FilledAmount, makerDirtyFilledAmount[m.Hash.Hex()])
if m.FilledAmount.Cmp(m.Quantity) < 0 {
m.Status = lendingstate.LendingStatusPartialFilled
} else {
m.Status = lendingstate.LendingStatusFilled
}
log.Debug("PutObject processed makerLendingItem",
"term", m.Term, "userAddr", m.UserAddress.Hex(), "side", m.Side,
"Interest", m.Interest, "quantity", m.Quantity, "filledAmount", m.FilledAmount, "status", m.Status,
"hash", m.Hash.Hex(), "txHash", m.TxHash.Hex())
if err := db.PutObject(m.Hash, m); err != nil {
return fmt.Errorf("SDKNode: failed to put processed makerOrder. Hash: %s Error: %s", m.Hash.Hex(), err.Error())
}
}
}
// 3. put rejected orders to leveldb and update status REJECTED
log.Debug("Got rejected lendingItems", "number", len(rejectedItems), "rejectedLendingItems", rejectedItems)
if len(rejectedItems) > 0 {
var rejectedHashes []string
// updateRejectedOrders
for _, r := range rejectedItems {
rejectedHashes = append(rejectedHashes, r.Hash.Hex())
if updatedTakerLendingItem.Hash == r.Hash && !txMatchTime.Before(r.UpdatedAt) {
// cache r history for handling reorg
historyRecord := lendingstate.LendingItemHistoryItem{
TxHash: updatedTakerLendingItem.TxHash,
FilledAmount: lendingstate.CloneBigInt(updatedTakerLendingItem.FilledAmount),
Status: updatedTakerLendingItem.Status,
UpdatedAt: updatedTakerLendingItem.UpdatedAt,
}
l.UpdateLendingItemCache(updatedTakerLendingItem.LendingToken, updatedTakerLendingItem.CollateralToken, updatedTakerLendingItem.Hash, txHash, historyRecord)
// if whole order is rejected, status = REJECTED
// otherwise, status = FILLED
if updatedTakerLendingItem.FilledAmount.Sign() > 0 {
updatedTakerLendingItem.Status = lendingstate.LendingStatusFilled
} else {
updatedTakerLendingItem.Status = lendingstate.LendingStatusReject
}
updatedTakerLendingItem.TxHash = txHash
updatedTakerLendingItem.UpdatedAt = txMatchTime
if err := db.PutObject(updatedTakerLendingItem.Hash, updatedTakerLendingItem); err != nil {
return fmt.Errorf("SDKNode: failed to reject takerOrder. Hash: %s Error: %s", updatedTakerLendingItem.Hash.Hex(), err.Error())
}
}
}
items := db.GetListItemByHashes(rejectedHashes, &lendingstate.LendingItem{})
if items != nil {
dirtyRejectedItems := items.([]*lendingstate.LendingItem)
for _, r := range dirtyRejectedItems {
if txMatchTime.Before(r.UpdatedAt) {
log.Debug("Ignore old orders/trades reject", "txHash", txHash.Hex(), "txTime", txMatchTime.UnixNano(), "updatedAt", updatedTakerLendingItem.UpdatedAt.UnixNano())
continue
}
// cache lendingItem for handling reorg
historyRecord := lendingstate.LendingItemHistoryItem{
TxHash: r.TxHash,
FilledAmount: lendingstate.CloneBigInt(r.FilledAmount),
Status: r.Status,
UpdatedAt: r.UpdatedAt,
}
l.UpdateLendingItemCache(r.LendingToken, r.CollateralToken, r.Hash, txHash, historyRecord)
dirtyFilledAmount, ok := makerDirtyFilledAmount[r.Hash.Hex()]
if ok && dirtyFilledAmount != nil {
r.FilledAmount = new(big.Int).Add(r.FilledAmount, dirtyFilledAmount)
}
// if whole order is rejected, status = REJECTED
// otherwise, status = FILLED
if r.FilledAmount.Sign() > 0 {
r.Status = lendingstate.LendingStatusFilled
} else {
r.Status = lendingstate.LendingStatusReject
}
r.TxHash = txHash
r.UpdatedAt = txMatchTime
if err = db.PutObject(r.Hash, r); err != nil {
return fmt.Errorf("SDKNode: failed to update rejectedOder to sdkNode %s", err.Error())
}
}
}
}
if err := db.CommitLendingBulk(); err != nil {
return fmt.Errorf("SDKNode fail to commit bulk update lendingItem/lendingTrades at txhash %s . Error: %s", txHash.Hex(), err.Error())
}
return nil
}
func (l *Lending) UpdateLiquidatedTrade(blockTime uint64, result lendingstate.FinalizedResult, trades map[common.Hash]*lendingstate.LendingTrade) error {
db := l.GetMongoDB()
db.InitLendingBulk()
txhash := result.TxHash
txTime := time.Unix(int64(blockTime), 0).UTC()
if err := l.UpdateLendingTrade(trades, txhash, txTime); err != nil {
return err
}
// adding auto repay transaction
if len(result.AutoRepay) > 0 {
for _, hash := range result.AutoRepay {
trade := trades[hash]
if trade == nil {
continue
}
paymentBalance := lendingstate.CalculateTotalRepayValue(blockTime, trade.LiquidationTime, trade.Term, trade.Interest, trade.Amount)
repayItem := &lendingstate.LendingItem{
Quantity: paymentBalance,
Interest: big.NewInt(int64(trade.Interest)),
Side: "",
Type: lendingstate.Repay,
LendingToken: trade.LendingToken,
CollateralToken: trade.CollateralToken,
FilledAmount: paymentBalance,
Status: lendingstate.Repay,
Relayer: trade.BorrowingRelayer,
Term: trade.Term,
UserAddress: trade.Borrower,
Signature: nil,
Hash: trade.Hash,
TxHash: txhash,
Nonce: nil,
CreatedAt: txTime,
UpdatedAt: txTime,
LendingId: 0,
LendingTradeId: trade.TradeId,
AutoTopUp: true, // auto repay
ExtraData: "",
}
if err := db.PutObject(repayItem.Hash, repayItem); err != nil {
return err
}
}
}
// adding auto topup transaction
if len(result.AutoTopUp) > 0 {
oldTradeHashes := []string{}
for _, hash := range result.AutoTopUp {
oldTradeHashes = append(oldTradeHashes, hash.Hex())
}
items := db.GetListItemByHashes(oldTradeHashes, &lendingstate.LendingTrade{})
if items != nil && len(items.([]*lendingstate.LendingTrade)) > 0 {
for _, oldTrade := range items.([]*lendingstate.LendingTrade) {
newTrade := trades[oldTrade.Hash]
topUpAmount := new(big.Int).Sub(newTrade.CollateralLockedAmount, oldTrade.CollateralLockedAmount)
extraData, _ := json.Marshal(struct {
Price *big.Int
}{
Price: new(big.Int).Div(new(big.Int).Mul(newTrade.LiquidationPrice, common.BaseTopUp), common.RateTopUp),
})
topUpItem := &lendingstate.LendingItem{
Quantity: topUpAmount,
Interest: big.NewInt(int64(oldTrade.Interest)),
Side: "",
Type: lendingstate.TopUp,
LendingToken: oldTrade.LendingToken,
CollateralToken: oldTrade.CollateralToken,
FilledAmount: topUpAmount,
Status: lendingstate.TopUp,
AutoTopUp: true, // auto topup
Relayer: oldTrade.BorrowingRelayer,
Term: oldTrade.Term,
UserAddress: oldTrade.Borrower,
Signature: nil,
Hash: oldTrade.Hash,
TxHash: txhash,
Nonce: nil,
CreatedAt: txTime,
UpdatedAt: txTime,
LendingId: 0,
LendingTradeId: oldTrade.TradeId,
ExtraData: string(extraData),
}
if err := db.PutObject(topUpItem.Hash, topUpItem); err != nil {
return err
}
}
}
}
// adding auto recall transaction
if len(result.AutoRecall) > 0 {
oldTradeHashes := []string{}
for _, hash := range result.AutoRecall {
oldTradeHashes = append(oldTradeHashes, hash.Hex())
}
items := db.GetListItemByHashes(oldTradeHashes, &lendingstate.LendingTrade{})
if items != nil && len(items.([]*lendingstate.LendingTrade)) > 0 {
for _, oldTrade := range items.([]*lendingstate.LendingTrade) {
newTrade := trades[oldTrade.Hash]
recallAmount := new(big.Int).Sub(oldTrade.CollateralLockedAmount, newTrade.CollateralLockedAmount)
extraData, _ := json.Marshal(struct {
Price *big.Int
}{
Price: new(big.Int).Div(new(big.Int).Mul(newTrade.LiquidationPrice, oldTrade.DepositRate), oldTrade.LiquidationRate),
})
topUpItem := &lendingstate.LendingItem{
Quantity: recallAmount,
Interest: big.NewInt(int64(oldTrade.Interest)),
Side: "",
Type: lendingstate.Recall,
LendingToken: oldTrade.LendingToken,
CollateralToken: oldTrade.CollateralToken,
FilledAmount: recallAmount,
Status: lendingstate.Recall,
AutoTopUp: true, // auto recall
Relayer: oldTrade.BorrowingRelayer,
Term: oldTrade.Term,
UserAddress: oldTrade.Borrower,
Signature: nil,
Hash: oldTrade.Hash,
TxHash: txhash,
Nonce: nil,
CreatedAt: txTime,
UpdatedAt: txTime,
LendingId: 0,
LendingTradeId: oldTrade.TradeId,
ExtraData: string(extraData),
}
if err := db.PutObject(topUpItem.Hash, topUpItem); err != nil {
return err
}
}
}
}
if err := db.CommitLendingBulk(); err != nil {
return fmt.Errorf("failed to updateLendingTrade . Err: %v", err)
}
return nil
}
func (l *Lending) UpdateLendingTrade(trades map[common.Hash]*lendingstate.LendingTrade, txhash common.Hash, txTime time.Time) error {
db := l.GetMongoDB()
hashQuery := []string{}
if len(trades) == 0 {
return nil
}
for _, trade := range trades {
hashQuery = append(hashQuery, trade.Hash.Hex())
}
items := db.GetListItemByHashes(hashQuery, &lendingstate.LendingTrade{})
if items != nil && len(items.([]*lendingstate.LendingTrade)) > 0 {
for _, trade := range items.([]*lendingstate.LendingTrade) {
history := lendingstate.LendingTradeHistoryItem{
TxHash: trade.TxHash,
CollateralLockedAmount: trade.CollateralLockedAmount,
LiquidationPrice: trade.LiquidationPrice,
Status: trade.Status,
UpdatedAt: trade.UpdatedAt,
}
l.UpdateLendingTradeCache(trade.Hash, txhash, history)
trade.TxHash = txhash
trade.UpdatedAt = txTime
newTrade := trades[trade.Hash]
trade.CollateralLockedAmount = newTrade.CollateralLockedAmount
trade.Status = newTrade.Status
trade.LiquidationPrice = newTrade.LiquidationPrice
trade.ExtraData = newTrade.ExtraData
if err := db.PutObject(trade.Hash, trade); err != nil {
return err
}
}
log.Debug("UpdateLendingTrade successfully", "txhash", txhash, "hash", hashQuery)
} else {
// not update, just upsert
for _, trade := range trades {
if err := db.PutObject(trade.Hash, trade); err != nil {
return err
}
}
}
return nil
}
func (l *Lending) GetLendingState(block *types.Block, author common.Address) (*lendingstate.LendingStateDB, error) {
root, err := l.GetLendingStateRoot(block, author)
if err != nil {
return nil, err
}
if l.StateCache == nil {
return nil, errors.New("not initialized XDCx")
}
state, err := lendingstate.New(root, l.StateCache)
if err != nil {
log.Info("Not found lending state when GetLendingState", "block", block.Number(), "lendingRoot", root.Hex())
}
return state, err
}
func (l *Lending) GetStateCache() lendingstate.Database {
return l.StateCache
}
func (l *Lending) HasLendingState(block *types.Block, author common.Address) bool {
root, err := l.GetLendingStateRoot(block, author)
if err != nil {
return false
}
_, err = l.StateCache.OpenTrie(root)
return err == nil
}
func (l *Lending) GetTriegc() *prque.Prque[int64, common.Hash] {
return l.Triegc
}
func (l *Lending) GetLendingStateRoot(block *types.Block, author common.Address) (common.Hash, error) {
for _, tx := range block.Transactions() {
to := tx.To()
if to != nil && *to == common.TradingStateAddrBinary && *tx.From() == author {
data := tx.Data()
if len(data) >= 64 {
return common.BytesToHash(data[32:]), nil
}
}
}
return lendingstate.EmptyRoot, nil
}
func (l *Lending) UpdateLendingItemCache(LendingToken, CollateralToken common.Address, hash common.Hash, txhash common.Hash, lastState lendingstate.LendingItemHistoryItem) {
lendingCacheAtTxHash, ok := l.lendingItemHistory.Get(txhash)
if !ok || lendingCacheAtTxHash == nil {
lendingCacheAtTxHash = make(map[common.Hash]lendingstate.LendingItemHistoryItem)
}
orderKey := lendingstate.GetLendingItemHistoryKey(LendingToken, CollateralToken, hash)
_, ok = lendingCacheAtTxHash[orderKey]
if !ok {
lendingCacheAtTxHash[orderKey] = lastState
}
l.lendingItemHistory.Add(txhash, lendingCacheAtTxHash)
}
func (l *Lending) UpdateLendingTradeCache(hash common.Hash, txhash common.Hash, lastState lendingstate.LendingTradeHistoryItem) {
var lendingCacheAtTxHash map[common.Hash]lendingstate.LendingTradeHistoryItem
lendingCacheAtTxHash, ok := l.lendingTradeHistory.Get(txhash)
if !ok || lendingCacheAtTxHash == nil {
lendingCacheAtTxHash = make(map[common.Hash]lendingstate.LendingTradeHistoryItem)
}
_, ok = lendingCacheAtTxHash[hash]
if !ok {
lendingCacheAtTxHash[hash] = lastState
}
l.lendingTradeHistory.Add(txhash, lendingCacheAtTxHash)
}
func (l *Lending) RollbackLendingData(txhash common.Hash) error {
db := l.GetMongoDB()
db.InitLendingBulk()
// rollback lendingItem
items := db.GetListItemByTxHash(txhash, &lendingstate.LendingItem{})
if items != nil {
for _, item := range items.([]*lendingstate.LendingItem) {
cacheAtTxHash, ok := l.lendingItemHistory.Get(txhash)
log.Debug("XDCxlending reorg: rollback lendingItem", "txhash", txhash.Hex(), "item", lendingstate.ToJSON(item), "lendingItemHistory", cacheAtTxHash)
if !ok || cacheAtTxHash == nil {
log.Debug("XDCxlending reorg: remove item due to no lendingItemHistory", "item", lendingstate.ToJSON(item))
if err := db.DeleteObject(item.Hash, &lendingstate.LendingItem{}); err != nil {
return fmt.Errorf("failed to remove reorg LendingItem. Err: %v . Item: %s", err.Error(), lendingstate.ToJSON(item))
}
continue
}
lendingItemHistory := cacheAtTxHash[lendingstate.GetLendingItemHistoryKey(item.LendingToken, item.CollateralToken, item.Hash)]
if (lendingItemHistory == lendingstate.LendingItemHistoryItem{}) {
log.Debug("XDCxlending reorg: remove item due to empty lendingItemHistory", "item", lendingstate.ToJSON(item))
if err := db.DeleteObject(item.Hash, &lendingstate.LendingItem{}); err != nil {
return fmt.Errorf("failed to remove reorg LendingItem. Err: %v . Item: %s", err.Error(), lendingstate.ToJSON(item))
}
continue
}
item.TxHash = lendingItemHistory.TxHash
item.Status = lendingItemHistory.Status
item.FilledAmount = lendingstate.CloneBigInt(lendingItemHistory.FilledAmount)
item.UpdatedAt = lendingItemHistory.UpdatedAt
log.Debug("XDCxlending reorg: update item to the last lendingItemHistory", "item", lendingstate.ToJSON(item), "lendingItemHistory", lendingItemHistory)
if err := db.PutObject(item.Hash, item); err != nil {
return fmt.Errorf("failed to update reorg LendingItem. Err: %v . Item: %s", err.Error(), lendingstate.ToJSON(item))
}
}
}
// rollback lendingTrade
items = db.GetListItemByTxHash(txhash, &lendingstate.LendingTrade{})
if items != nil {
for _, trade := range items.([]*lendingstate.LendingTrade) {
cacheAtTxHash, ok := l.lendingTradeHistory.Get(txhash)
log.Debug("XDCxlending reorg: rollback LendingTrade", "txhash", txhash.Hex(), "trade", lendingstate.ToJSON(trade), "LendingTradeHistory", cacheAtTxHash)
if !ok || cacheAtTxHash == nil {
log.Debug("XDCxlending reorg: remove trade due to no LendingTradeHistory", "trade", lendingstate.ToJSON(trade))
if err := db.DeleteObject(trade.Hash, &lendingstate.LendingTrade{}); err != nil {
return fmt.Errorf("failed to remove reorg LendingTrade. Err: %v . Trade: %s", err.Error(), lendingstate.ToJSON(trade))
}
continue
}
lendingTradeHistoryItem := cacheAtTxHash[trade.Hash]
if (lendingTradeHistoryItem == lendingstate.LendingTradeHistoryItem{}) {
log.Debug("XDCxlending reorg: remove trade due to empty LendingTradeHistory", "trade", lendingstate.ToJSON(trade))
if err := db.DeleteObject(trade.Hash, &lendingstate.LendingTrade{}); err != nil {
return fmt.Errorf("failed to remove reorg LendingTrade. Err: %v . Trade: %s", err.Error(), lendingstate.ToJSON(trade))
}
continue
}
trade.TxHash = lendingTradeHistoryItem.TxHash
trade.Status = lendingTradeHistoryItem.Status
trade.CollateralLockedAmount = lendingstate.CloneBigInt(lendingTradeHistoryItem.CollateralLockedAmount)
trade.LiquidationPrice = lendingstate.CloneBigInt(lendingTradeHistoryItem.LiquidationPrice)
trade.UpdatedAt = lendingTradeHistoryItem.UpdatedAt
log.Debug("XDCxlending reorg: update trade to the last lendingTradeHistoryItem", "trade", lendingstate.ToJSON(trade), "lendingTradeHistoryItem", lendingTradeHistoryItem)
if err := db.PutObject(trade.Hash, trade); err != nil {
return fmt.Errorf("failed to update reorg LendingTrade. Err: %v . Trade: %s", err.Error(), lendingstate.ToJSON(trade))
}
}
}
// remove repay/topup/recall history
db.DeleteItemByTxHash(txhash, &lendingstate.LendingItem{Type: lendingstate.Repay})
db.DeleteItemByTxHash(txhash, &lendingstate.LendingItem{Type: lendingstate.TopUp})
db.DeleteItemByTxHash(txhash, &lendingstate.LendingItem{Type: lendingstate.Recall})
if err := db.CommitLendingBulk(); err != nil {
return fmt.Errorf("failed to RollbackLendingData. %v", err)
}
return nil
}
func (l *Lending) ProcessLiquidationData(header *types.Header, chain consensus.ChainContext, statedb *state.StateDB, tradingState *tradingstate.TradingStateDB, lendingState *lendingstate.LendingStateDB) (updatedTrades map[common.Hash]*lendingstate.LendingTrade, liquidatedTrades, autoRepayTrades, autoTopUpTrades, autoRecallTrades []*lendingstate.LendingTrade, err error) {
time := header.Time
updatedTrades = map[common.Hash]*lendingstate.LendingTrade{} // sum of liquidatedTrades, autoRepayTrades, autoTopUpTrades, autoRecallTrades
liquidatedTrades = []*lendingstate.LendingTrade{}
autoRepayTrades = []*lendingstate.LendingTrade{}
autoTopUpTrades = []*lendingstate.LendingTrade{}
autoRecallTrades = []*lendingstate.LendingTrade{}
allPairs, err := lendingstate.GetAllLendingPairs(statedb)
if err != nil {
log.Debug("Not found all trading pairs", "error", err)
return updatedTrades, liquidatedTrades, autoRepayTrades, autoTopUpTrades, autoRecallTrades, nil
}
allLendingBooks, err := lendingstate.GetAllLendingBooks(statedb)
if err != nil {
log.Debug("Not found all lending books", "error", err)
return updatedTrades, liquidatedTrades, autoRepayTrades, autoTopUpTrades, autoRecallTrades, nil
}
// liquidate trades by time
for lendingBook := range allLendingBooks {
lowestTime, tradingIds := lendingState.GetLowestLiquidationTime(lendingBook, time)
log.Debug("ProcessLiquidationData time", "tradeIds", len(tradingIds))
for lowestTime.Sign() > 0 && lowestTime.Cmp(time) < 0 {
for _, tradingId := range tradingIds {
log.Debug("ProcessRepay", "lowestTime", lowestTime, "time", time, "lendingBook", lendingBook.Hex(), "tradingId", tradingId.Hex())
trade, err := l.ProcessRepayLendingTrade(header, chain, lendingState, statedb, tradingState, lendingBook, tradingId.Big().Uint64())
if err != nil {
log.Error("Fail when process payment ", "time", time, "lendingBook", lendingBook.Hex(), "tradingId", tradingId, "error", err)
return updatedTrades, liquidatedTrades, autoRepayTrades, autoTopUpTrades, autoRecallTrades, err
}
if trade != nil && trade.Hash != (common.Hash{}) {
updatedTrades[trade.Hash] = trade
if trade.Status == lendingstate.TradeStatusLiquidated {
liquidatedTrades = append(liquidatedTrades, trade)
} else if trade.Status == lendingstate.TradeStatusClosed {
autoRepayTrades = append(autoRepayTrades, trade)
}
}
}
lowestTime, tradingIds = lendingState.GetLowestLiquidationTime(lendingBook, time)
}
}
for _, lendingPair := range allPairs {
orderbook := tradingstate.GetTradingOrderBookHash(lendingPair.CollateralToken, lendingPair.LendingToken)
_, collateralPrice, err := l.GetCollateralPrices(header, chain, statedb, tradingState, lendingPair.CollateralToken, lendingPair.LendingToken)
if err != nil || collateralPrice == nil || collateralPrice.Sign() == 0 {
log.Error("Fail when get price collateral/lending ", "CollateralToken", lendingPair.CollateralToken.Hex(), "LendingToken", lendingPair.LendingToken.Hex(), "error", err)
// ignore this pair, do not throw error
continue
}
// liquidate trades
highestLiquidatePrice, liquidationData := tradingState.GetHighestLiquidationPriceData(orderbook, collateralPrice)
for highestLiquidatePrice.Sign() > 0 && collateralPrice.Cmp(highestLiquidatePrice) < 0 {
for lendingBook, tradingIds := range liquidationData {
for _, tradingIdHash := range tradingIds {
trade := lendingState.GetLendingTrade(lendingBook, tradingIdHash)
if trade.AutoTopUp {
if newTrade, err := l.AutoTopUp(statedb, tradingState, lendingState, lendingBook, tradingIdHash, collateralPrice); err == nil {
// if this action complete successfully, do not liquidate this trade in this epoch
log.Debug("AutoTopUp", "borrower", trade.Borrower.Hex(), "collateral", newTrade.CollateralToken.Hex(), "tradingIdHash", tradingIdHash.Hex(), "newLockedAmount", newTrade.CollateralLockedAmount)
autoTopUpTrades = append(autoTopUpTrades, newTrade)
updatedTrades[newTrade.Hash] = newTrade
continue
}
}
log.Debug("LiquidationTrade", "highestLiquidatePrice", highestLiquidatePrice, "lendingBook", lendingBook.Hex(), "tradingIdHash", tradingIdHash.Hex())
newTrade, err := l.LiquidationTrade(lendingState, statedb, tradingState, lendingBook, tradingIdHash.Big().Uint64())
if err != nil {
log.Error("Fail when remove liquidation newTrade", "time", time, "lendingBook", lendingBook.Hex(), "tradingIdHash", tradingIdHash.Hex(), "error", err)
return updatedTrades, liquidatedTrades, autoRepayTrades, autoTopUpTrades, autoRecallTrades, err
}
if newTrade != nil && newTrade.Hash != (common.Hash{}) {
newTrade.Status = lendingstate.TradeStatusLiquidated
liquidationData := lendingstate.LiquidationData{
RecallAmount: common.Big0,
LiquidationAmount: newTrade.CollateralLockedAmount,
CollateralPrice: collateralPrice,
Reason: lendingstate.LiquidatedByPrice,
}
extraData, _ := json.Marshal(liquidationData)
newTrade.ExtraData = string(extraData)
liquidatedTrades = append(liquidatedTrades, newTrade)
updatedTrades[newTrade.Hash] = newTrade
}
}
}
highestLiquidatePrice, liquidationData = tradingState.GetHighestLiquidationPriceData(orderbook, collateralPrice)
}
// recall trades
depositRate, liquidationRate, recallRate := lendingstate.GetCollateralDetail(statedb, lendingPair.CollateralToken)
recalLiquidatePrice := new(big.Int).Mul(collateralPrice, common.BaseRecall)
recalLiquidatePrice = new(big.Int).Div(recalLiquidatePrice, recallRate)
newLiquidatePrice := new(big.Int).Mul(collateralPrice, liquidationRate)
newLiquidatePrice = new(big.Int).Div(newLiquidatePrice, depositRate)
allLowertLiquidationData := tradingState.GetAllLowerLiquidationPriceData(orderbook, recalLiquidatePrice)
log.Debug("ProcessLiquidationData", "orderbook", orderbook.Hex(), "collateralPrice", collateralPrice, "recallRate", recallRate, "recalLiquidatePrice", recalLiquidatePrice, "newLiquidatePrice", newLiquidatePrice, "allLowertLiquidationData", len(allLowertLiquidationData))
for price, liquidationData := range allLowertLiquidationData {
if price.Sign() > 0 && recalLiquidatePrice.Cmp(price) > 0 {
for lendingBook, tradingIds := range liquidationData {
for _, tradingIdHash := range tradingIds {
log.Debug("Process Recall", "price", price, "lendingBook", lendingBook, "tradingIdHash", tradingIdHash.Hex())
trade := lendingState.GetLendingTrade(lendingBook, tradingIdHash)
log.Debug("TestRecall", "borrower", trade.Borrower.Hex(), "lendingToken", trade.LendingToken.Hex(), "collateral", trade.CollateralToken.Hex(), "price", price, "tradingIdHash", tradingIdHash.Hex())
if trade.AutoTopUp {
_, newTrade, err := l.ProcessRecallLendingTrade(lendingState, statedb, tradingState, lendingBook, tradingIdHash, newLiquidatePrice)
if err != nil {
log.Error("ProcessRecallLendingTrade", "lendingBook", lendingBook.Hex(), "tradingIdHash", tradingIdHash.Hex(), "newLiquidatePrice", newLiquidatePrice, "err", err)
return updatedTrades, liquidatedTrades, autoRepayTrades, autoTopUpTrades, autoRecallTrades, err
}
// if this action complete successfully, do not liquidate this trade in this epoch
log.Debug("AutoRecall", "borrower", trade.Borrower.Hex(), "collateral", newTrade.CollateralToken.Hex(), "lendingBook", lendingBook.Hex(), "tradingIdHash", tradingIdHash.Hex(), "newLockedAmount", newTrade.CollateralLockedAmount)
autoRecallTrades = append(autoRecallTrades, newTrade)
updatedTrades[newTrade.Hash] = newTrade
}
}
}
}
}
}
log.Debug("ProcessLiquidationData", "updatedTrades", len(updatedTrades), "liquidated", len(liquidatedTrades), "autoRepay", len(autoRepayTrades), "autoTopUp", len(autoTopUpTrades), "autoRecall", len(autoRecallTrades))
return updatedTrades, liquidatedTrades, autoRepayTrades, autoTopUpTrades, autoRecallTrades, nil
}