From fd69ebce1a1ce0194594f134edafa0b515bdb221 Mon Sep 17 00:00:00 2001 From: Karthik Iyer Date: Fri, 6 Dec 2024 18:18:31 -0800 Subject: [PATCH 1/6] sample test to test offer updates --- .../internal/integration/change_test.go | 77 +++++++++++++++++++ xdr/ledger_entry.go | 19 ++++- xdr/transaction_envelope.go | 14 ++++ 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/services/horizon/internal/integration/change_test.go b/services/horizon/internal/integration/change_test.go index bf2df266eb..6cfbae76bd 100644 --- a/services/horizon/internal/integration/change_test.go +++ b/services/horizon/internal/integration/change_test.go @@ -2,6 +2,7 @@ package integration import ( "context" + "encoding/json" "github.com/stellar/go/ingest" "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/keypair" @@ -123,6 +124,82 @@ func TestOneTxOneOperationChanges(t *testing.T) { tt.True(accountFromEntry(destAccChange.Pre).Balance < accountFromEntry(destAccChange.Post).Balance) } +func TestSomething(t *testing.T) { + //tt := assert.New(t) + itest := integration.NewTest(t, integration.Config{}) + master := itest.Master() + keys, accounts := itest.CreateAccounts(3, "1000") + keyA, keyB := keys[0], keys[1] + accountA, accountB := accounts[0], accounts[1] + + // Some random asset + xyzAsset := txnbuild.CreditAsset{Code: "XYZ", Issuer: itest.Master().Address()} + itest.MustEstablishTrustline(keyA, accountA, xyzAsset) + + itest.MustEstablishTrustline(keyB, accountB, xyzAsset) + + t.Logf("*****") + paymentOperation := txnbuild.Payment{ + Destination: keyA.Address(), + Asset: xyzAsset, + Amount: "2000", + } + txResp := itest.MustSubmitOperations(itest.MasterAccount(), itest.Master(), &paymentOperation) + data, _ := json.MarshalIndent(txResp, "", " ") + + t.Logf("Acc A: %v, Acc B: %v", keyA.Address(), keyB.Address()) + + sellOfferOperationFromA := txnbuild.ManageSellOffer{ + Selling: xyzAsset, + Buying: txnbuild.NativeAsset{}, + Amount: "50", + Price: xdr.Price{N: 1, D: 1}, + SourceAccount: keyA.Address(), + } + txResp = itest.MustSubmitMultiSigOperations(itest.MasterAccount(), []*keypair.Full{master, keyA}, &sellOfferOperationFromA) + data, _ = json.MarshalIndent(txResp, "", " ") + t.Logf("Transaction Sell Offer: %v", string(data)) + t.Logf("Tx response meta xdr Sell Offer: %v", txResp.ResultMetaXdr) + t.Logf("*****") + + sellOfferLedgerSeq := uint32(txResp.Ledger) + + buyOfferOperationFromB := txnbuild.ManageBuyOffer{ + Buying: xyzAsset, + Selling: txnbuild.NativeAsset{}, + Amount: "66", + Price: xdr.Price{N: 1, D: 1}, + SourceAccount: keyB.Address(), + } + + txResp = itest.MustSubmitMultiSigOperations(itest.MasterAccount(), []*keypair.Full{master, keyB}, &buyOfferOperationFromB) + data, _ = json.MarshalIndent(txResp, "", " ") + t.Logf("Transaction Buy Offer: %v", string(data)) + t.Logf("Tx response meta xdr Buy Offer: %v", txResp.ResultMetaXdr) + t.Logf("*****") + + buyOfferLedgerSeq := uint32(txResp.Ledger) + + t.Logf("Sell Offer Ledger:%v, Buy Offer Ledger: %v", sellOfferLedgerSeq, buyOfferLedgerSeq) + + waitForLedgerInArchive(t, 15*time.Second, buyOfferLedgerSeq) + + ledgerMap := getLedgers(itest, sellOfferLedgerSeq, buyOfferLedgerSeq) + for ledgerSeq, ledger := range ledgerMap { + t.Logf("LedgerSeq:::::::::::::::::::::: %v", ledgerSeq) + changes := getChangesFromLedger(itest, ledger) + for _, change := range changes { + if change.Reason != ingest.LedgerEntryChangeReasonOperation { + continue + } + typ := change.Type.String() + pre, _ := change.Pre.MarshalBinaryBase64() + post, _ := change.Post.MarshalBinaryBase64() + t.Logf("ledger: %v, Change - type - %v, pre: %v, post: %v", ledgerSeq, typ, pre, post) + } + } +} + func getChangesFromLedger(itest *integration.Test, ledger xdr.LedgerCloseMeta) []ingest.Change { t := itest.CurrentTest() changeReader, err := ingest.NewLedgerChangeReaderFromLedgerCloseMeta(itest.GetPassPhrase(), ledger) diff --git a/xdr/ledger_entry.go b/xdr/ledger_entry.go index c04defb693..1f2345abe9 100644 --- a/xdr/ledger_entry.go +++ b/xdr/ledger_entry.go @@ -1,6 +1,9 @@ package xdr -import "fmt" +import ( + "encoding/base64" + "fmt" +) // LedgerKey implements the `Keyer` interface func (entry *LedgerEntry) LedgerKey() (LedgerKey, error) { @@ -183,3 +186,17 @@ func (data *LedgerEntryData) LedgerKey() (LedgerKey, error) { return key, nil } + +// MarshalBinaryBase64 marshals XDR into a binary form and then encodes it +// using base64. +func (e *LedgerEntry) MarshalBinaryBase64() (string, error) { + if e == nil { + return "nil", nil + } + b, err := e.MarshalBinary() + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString(b), nil +} diff --git a/xdr/transaction_envelope.go b/xdr/transaction_envelope.go index 6d513ff342..921a0ca403 100644 --- a/xdr/transaction_envelope.go +++ b/xdr/transaction_envelope.go @@ -1,5 +1,7 @@ package xdr +import "encoding/base64" + // IsFeeBump returns true if the transaction envelope is a fee bump transaction func (e TransactionEnvelope) IsFeeBump() bool { return e.Type == EnvelopeTypeEnvelopeTypeTxFeeBump @@ -242,3 +244,15 @@ func (e TransactionEnvelope) Memo() Memo { panic("unsupported transaction type: " + e.Type.String()) } } + +func (e *TransactionEnvelope) MarshalBinaryBase64() (string, error) { + if e == nil { + return "nil", nil + } + b, err := e.MarshalBinary() + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString(b), nil +} From acdf16e07c319ed609aac3b84a2f4782e8ba7b16 Mon Sep 17 00:00:00 2001 From: Karthik Iyer Date: Fri, 6 Dec 2024 18:44:51 -0800 Subject: [PATCH 2/6] Initial commit for generic trades processor --- ingest/trades/main.go | 115 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 ingest/trades/main.go diff --git a/ingest/trades/main.go b/ingest/trades/main.go new file mode 100644 index 0000000000..b27ee6edc9 --- /dev/null +++ b/ingest/trades/main.go @@ -0,0 +1,115 @@ +package trades + +import "github.com/stellar/go/xdr" + +type TradeEventType int + +const ( + TradeEventTypeUnknown TradeEventType = iota // Default value + TradeEventTypeOfferCreated // Offer created event + TradeEventTypeOfferUpdated // Offer updated event + TradeEventTypeOfferClosed // Offer closed event + TradeEventTypeLiquidityPoolUpdated // Liquidity pool update event +) + +type TradeEvent interface { + GetTradeEventType() TradeEventType // Method to retrieve the type of the trade event +} + +type OfferBase struct { + Selling xdr.Asset // Asset being sold + Buying xdr.Asset // Asset being bought + Amount xdr.Int64 // Total amount of the selling asset + Price xdr.Price // Price of the offer + Flags uint32 // Flags for the offer (e.g., passive, sell offers) +} + +type OfferCreatedEvent struct { + SellerId xdr.AccountId // Account ID of the seller + OfferId xdr.Int64 // ID of the created offer + OfferState OfferBase // Initial state of the offer + CreatedLedgerSeq uint32 // Ledger sequence where the offer was created + Fills []FillInfo // List of fills that occurred during the creation +} + +func (e OfferCreatedEvent) GetTradeEventType() TradeEventType { + return TradeEventTypeOfferCreated +} + +type OfferUpdatedEvent struct { + SellerId xdr.AccountId // Account ID of the seller + OfferId xdr.Int64 // ID of the updated offer + PrevUpdatedLedgerSeq uint32 // Ledger sequence of the previous update + PreviousOfferState OfferBase // Previous state of the offer + UpdatedOfferState OfferBase // Updated state of the offer + UpdatedLedgerSeq uint32 // Ledger sequence where the offer was updated + Fills []FillInfo // List of fills that occurred during the update +} + +func (e OfferUpdatedEvent) GetTradeEventType() TradeEventType { + return TradeEventTypeOfferUpdated +} + +type OfferClosedEvent struct { + SellerId xdr.AccountId // Account ID of the seller + OfferId xdr.Int64 // ID of the closed offer + LastOfferState OfferBase // Last state of the offer before closing + ClosedLedgerSeq uint32 // Ledger sequence where the offer was closed +} + +func (e OfferClosedEvent) GetTradeEventType() TradeEventType { + return TradeEventTypeOfferClosed +} + +type LiquidityPoolUpdateEvent struct { + Fills []FillInfo // List of fills for this liquidity pool update +} + +func (e LiquidityPoolUpdateEvent) GetTradeEventType() TradeEventType { + return TradeEventTypeLiquidityPoolUpdated +} + +type FillSourceOperationType uint32 + +const ( + FillSourceOperationTypeUnknown FillSourceOperationType = iota + FillSourceOperationTypeManageBuy + FillSourceOperationTypeManageSell + FillSourceOperationTypePathPaymentStrictSend + FillSourceOperationTypePathPaymentStrictReceive +) + +type FillSource struct { + // Type of the operation that caused this fill (ManageBuyOffer, ManageSellOffer, PathPaymentStrictSend, PathPaymentStrictReceive) + SourceOperation FillSourceOperationType + + // The taker's information. Who caused this fill??? + ManageOfferInfo *ManageOfferInfo // Details of a ManageBuy/ManageSell operation (optional) + PathPaymentInfo *PathPaymentInfo // Details of a PathPayment operation (optional) +} + +// ManageBuy/ManageSell operation details +type ManageOfferInfo struct { + // Account that initiated the operation. Source of operation or source of transaction + SourceAccount xdr.AccountId + + // Did the taking operation create an offerId/offerEntry that rested after being partially filled OR Was it fully filled + OfferFullyFilled bool + + OfferId *xdr.Int64 // Offer ID, if an offer entry was created (nil if fully filled) +} + +type PathPaymentInfo struct { + SourceAccount xdr.AccountId // Source account of the PathPayment + DestinationAccount xdr.AccountId // Destination account of the PathPayment +} + +type FillInfo struct { + AssetSold xdr.Asset // Asset sold in this fill + AmountSold xdr.Int64 // Amount of the asset sold in this fill + AssetBought xdr.Asset // Asset bought in this fill + AmountBought xdr.Int64 // Amount of the asset bought in this fill + LedgerSeq uint32 // Ledger sequence in which the fill occurred + + FillSourceInfo FillSource // Details about what operation (and details) caused this fill +} From 3b3bf8c12660455922877ae0efacf62b7c6bafbc Mon Sep 17 00:00:00 2001 From: Karthik Iyer Date: Sat, 7 Dec 2024 15:02:55 -0800 Subject: [PATCH 3/6] Helper functions for xdr.LedgerCloseMeta --- xdr/ledger_close_meta.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/xdr/ledger_close_meta.go b/xdr/ledger_close_meta.go index 30e80b2e38..9624c8d3ba 100644 --- a/xdr/ledger_close_meta.go +++ b/xdr/ledger_close_meta.go @@ -131,6 +131,20 @@ func (l LedgerCloseMeta) UpgradesProcessing() []UpgradeEntryMeta { } } +func (l LedgerCloseMeta) HasUpgradeChanges() bool { + return len(l.UpgradesProcessing()) != 0 +} + +func (l LedgerCloseMeta) IsFirstLedgerAfterProtocolUpgrade() bool { + upgradeMeta := l.UpgradesProcessing() + for _, upgrade := range upgradeMeta { + if upgrade.Upgrade.Type == LedgerUpgradeTypeLedgerUpgradeVersion { + return true + } + } + return false +} + // EvictedTemporaryLedgerKeys returns a slice of ledger keys for // temporary ledger entries that have been evicted in this ledger. func (l LedgerCloseMeta) EvictedTemporaryLedgerKeys() ([]LedgerKey, error) { From b4cf0338d27864f8ed93211ad220027bd8ccc904 Mon Sep 17 00:00:00 2001 From: Karthik Iyer Date: Sat, 7 Dec 2024 19:59:43 -0800 Subject: [PATCH 4/6] scaffolding code for generic trade processor --- .../trades/generic_trades_offers_processor.go | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 ingest/trades/generic_trades_offers_processor.go diff --git a/ingest/trades/generic_trades_offers_processor.go b/ingest/trades/generic_trades_offers_processor.go new file mode 100644 index 0000000000..0fb513ca76 --- /dev/null +++ b/ingest/trades/generic_trades_offers_processor.go @@ -0,0 +1,70 @@ +package trades + +import ( + "github.com/stellar/go/ingest" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" + "io" +) + +func ProcessTradesFromLedger(ledger xdr.LedgerCloseMeta, networkPassPhrase string) ([]TradeEvent, error) { + changeReader, err := ingest.NewLedgerChangeReaderFromLedgerCloseMeta(networkPassPhrase, ledger) + if err != nil { + return []TradeEvent{}, errors.Wrap(err, "Error creating ledger change reader") + } + defer changeReader.Close() + + tradeEvents := make([]TradeEvent, 0) + for { + change, err := changeReader.Read() + if err == io.EOF { + break + } + if err != nil { + return []TradeEvent{}, errors.Wrap(err, "Error reading ledger change") + } + // Process trades from the change + tradesFromChange, err := processTradesFromChange(change) + if err != nil { + return nil, errors.Wrap(err, "Error processing trades from change") + } + + // Append to the overall trade events slice + tradeEvents = append(tradeEvents, tradesFromChange...) + } + + return []TradeEvent{}, nil +} + +func processTradesFromChange(change ingest.Change) ([]TradeEvent, error) { + var tradeEvents []TradeEvent + + switch change.Type { + case xdr.LedgerEntryTypeOffer: + trades, err := processOffersFromChange(change) + if err != nil { + return nil, errors.Wrap(err, "Error processing offers") + } + tradeEvents = append(tradeEvents, trades...) + case xdr.LedgerEntryTypeLiquidityPool: + trades, err := processLiquidityPoolEventsFromChange(change) + if err != nil { + return nil, errors.Wrap(err, "Error processing liquidity pool events") + } + tradeEvents = append(tradeEvents, trades...) + + default: + // Nothing to do + } + + return tradeEvents, nil +} + +func processOffersFromChange(change ingest.Change) ([]TradeEvent, error) { + + return []TradeEvent{}, nil +} + +func processLiquidityPoolEventsFromChange(change ingest.Change) ([]TradeEvent, error) { + return []TradeEvent{}, nil +} From e303f428700484ab24e374bb308047e5c82e4c83 Mon Sep 17 00:00:00 2001 From: Karthik Iyer Date: Sat, 7 Dec 2024 20:11:32 -0800 Subject: [PATCH 5/6] fix staticcheck --- ingest/trades/generic_trades_offers_processor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ingest/trades/generic_trades_offers_processor.go b/ingest/trades/generic_trades_offers_processor.go index 0fb513ca76..3b2670dc65 100644 --- a/ingest/trades/generic_trades_offers_processor.go +++ b/ingest/trades/generic_trades_offers_processor.go @@ -33,7 +33,7 @@ func ProcessTradesFromLedger(ledger xdr.LedgerCloseMeta, networkPassPhrase strin tradeEvents = append(tradeEvents, tradesFromChange...) } - return []TradeEvent{}, nil + return tradeEvents, nil } func processTradesFromChange(change ingest.Change) ([]TradeEvent, error) { From b8c1cee02d42904489434de8121362b1fe95444b Mon Sep 17 00:00:00 2001 From: Karthik Iyer Date: Wed, 8 Jan 2025 09:30:11 -0800 Subject: [PATCH 6/6] All temp code --- .../trades/generic_trades_offers_processor.go | 240 +++++++++++++++++- ingest/trades/main.go | 53 ++-- xdr/claim_atom.go | 11 + 3 files changed, 277 insertions(+), 27 deletions(-) diff --git a/ingest/trades/generic_trades_offers_processor.go b/ingest/trades/generic_trades_offers_processor.go index 3b2670dc65..ee9c0f47ff 100644 --- a/ingest/trades/generic_trades_offers_processor.go +++ b/ingest/trades/generic_trades_offers_processor.go @@ -41,7 +41,7 @@ func processTradesFromChange(change ingest.Change) ([]TradeEvent, error) { switch change.Type { case xdr.LedgerEntryTypeOffer: - trades, err := processOffersFromChange(change) + trades, err := processOfferEventsFromChange(change) if err != nil { return nil, errors.Wrap(err, "Error processing offers") } @@ -60,11 +60,245 @@ func processTradesFromChange(change ingest.Change) ([]TradeEvent, error) { return tradeEvents, nil } -func processOffersFromChange(change ingest.Change) ([]TradeEvent, error) { +func getOperationAndResultFromTransaction(index uint32, tx *ingest.LedgerTransaction) (xdr.Operation, xdr.OperationResult, error) { + opResults, ok := tx.Result.OperationResults() + if !ok { + return xdr.Operation{}, xdr.OperationResult{}, errors.New("transaction has no operation results") + } + return tx.Envelope.Operations()[index], opResults[index], nil +} - return []TradeEvent{}, nil +func getClaimsFromOperationAndResult(op xdr.Operation, opResult xdr.OperationResult) ([]xdr.ClaimAtom, *xdr.OfferEntry) { + var claims []xdr.ClaimAtom + var offer *xdr.OfferEntry + + switch op.Body.Type { + case xdr.OperationTypePathPaymentStrictReceive: + claims, offer = opResult.MustTr().MustPathPaymentStrictReceiveResult(). + MustSuccess(). + Offers, nil + + case xdr.OperationTypePathPaymentStrictSend: + claims, offer = opResult.MustTr(). + MustPathPaymentStrictSendResult(). + MustSuccess(). + Offers, nil + + case xdr.OperationTypeManageBuyOffer: + manageOfferResult := opResult.MustTr().MustManageBuyOfferResult(). + MustSuccess() + claims, offer = manageOfferResult.OffersClaimed, manageOfferResult.Offer.Offer + + case xdr.OperationTypeManageSellOffer: + manageOfferResult := opResult.MustTr().MustManageSellOfferResult(). + MustSuccess() + claims, offer = manageOfferResult.OffersClaimed, manageOfferResult.Offer.Offer + + case xdr.OperationTypeCreatePassiveSellOffer: + result := opResult.MustTr() + + // KNOWN ISSUE: stellar-core creates results for CreatePassiveOffer operations + // with the wrong result arm set. + if result.Type == xdr.OperationTypeManageSellOffer { + manageOfferResult := result.MustManageSellOfferResult().MustSuccess() + claims, offer = manageOfferResult.OffersClaimed, manageOfferResult.Offer.Offer + + } else { + passiveOfferResult := result.MustCreatePassiveSellOfferResult().MustSuccess() + claims, offer = passiveOfferResult.OffersClaimed, passiveOfferResult.Offer.Offer + } + default: + // pass + } + return claims, offer +} + +func groupClaimsByOfferId(claims []xdr.ClaimAtom) map[xdr.Int64]xdr.ClaimAtom { + offerIdToClaimMap := make(map[xdr.Int64]xdr.ClaimAtom) + for _, claim := range claims { + offerIdToClaimMap[claim.OfferId()] = claim + } + return offerIdToClaimMap +} + +func operationSourceAccount(tx *ingest.LedgerTransaction, op xdr.Operation) xdr.AccountId { + var accountId xdr.AccountId + if acc := op.SourceAccount; acc != nil { + accountId = acc.ToAccountId() + } else { + accountId = tx.Envelope.SourceAccount().ToAccountId() + } + return accountId +} + +func fillSourceInfo(tx *ingest.LedgerTransaction, op xdr.Operation, opResult xdr.OperationResult, takerOfferEntry *xdr.OfferEntry) FillSource { + fillSource := FillSource{} + + // Helper function to create ManageOfferInfo + createManageOfferInfo := func(operationType FillSourceOperationType) *ManageOfferInfo { + offerInfo := &ManageOfferInfo{ + SourceAccount: operationSourceAccount(tx, op), + OfferFullyFilled: takerOfferEntry != nil, + } + if takerOfferEntry != nil { + offerInfo.OfferId = &takerOfferEntry.OfferId + } else { + offerInfo.OfferId = nil + } + return offerInfo + } + + // Helper function to create PathPaymentInfo + createPathPaymentInfo := func(opType xdr.OperationType, opResult xdr.OperationResult) *PathPaymentInfo { + var destinationAccount xdr.AccountId + if opType == xdr.OperationTypePathPaymentStrictReceive { + destinationAccount = opResult.MustTr().PathPaymentStrictReceiveResult.Success.Last.Destination + } else if opType == xdr.OperationTypePathPaymentStrictSend { + destinationAccount = opResult.MustTr().PathPaymentStrictSendResult.Success.Last.Destination + } + return &PathPaymentInfo{ + SourceAccount: operationSourceAccount(tx, op), + DestinationAccount: destinationAccount, + } + } + + // Switch on the operation type and use helper functions to reduce duplication + switch op.Body.Type { + case xdr.OperationTypePathPaymentStrictReceive: + fillSource.SourceOperation = FillSourceOperationTypePathPaymentStrictSend + fillSource.PathPaymentInfo = createPathPaymentInfo(xdr.OperationTypePathPaymentStrictReceive, opResult) + + case xdr.OperationTypePathPaymentStrictSend: + fillSource.SourceOperation = FillSourceOperationTypePathPaymentStrictReceive + fillSource.PathPaymentInfo = createPathPaymentInfo(xdr.OperationTypePathPaymentStrictSend, opResult) + + case xdr.OperationTypeManageBuyOffer: + fillSource.SourceOperation = FillSourceOperationTypeManageBuy + fillSource.ManageOfferInfo = createManageOfferInfo(FillSourceOperationTypeManageBuy) + + case xdr.OperationTypeManageSellOffer: + fillSource.SourceOperation = FillSourceOperationTypeManageSell + fillSource.ManageOfferInfo = createManageOfferInfo(FillSourceOperationTypeManageSell) + + case xdr.OperationTypeCreatePassiveSellOffer: + fillSource.SourceOperation = FillSourceOperationTypePassiveSellOffer + fillSource.ManageOfferInfo = createManageOfferInfo(FillSourceOperationTypePassiveSellOffer) + } + + return fillSource +} + +type FillInfo2 struct { + AssetSold xdr.Asset + AmountSold xdr.Int64 + AssetBought xdr.Asset + AmountBought xdr.Int64 + Type // Either MatchingOffer or LiquidtiyPool + OfferId *int64 + SellerId *AccountId + + PoolId *PoolId +} + +func generateOfferFills(tx *ingest.LedgerTransaction, op xdr.Operation, opResult xdr.OperationResult, claimsMap []xdr.ClaimAtom, takerOfferEntry *xdr.OfferEntry) []FillInfo { + fills := make([]FillInfo, 0) + fillSource := fillSourceInfo(tx, op, opResult, takerOfferEntry) + + for _, claim := range claimsMap { + switch claim.Type { + case xdr.ClaimAtomTypeClaimAtomTypeV0, xdr.ClaimAtomTypeClaimAtomTypeOrderBook: + fill := FillInfo{ + AssetSold: claim.AssetSold(), + AssetBought: claim.AssetBought(), + AmountSold: claim.AmountSold(), + AmountBought: claim.AmountBought(), + LedgerSeq: tx.Ledger.LedgerSequence(), + // FillSourceInfo: fillSource, + } + fills = append(fills, fill) + case xdr.ClaimAtomTypeClaimAtomTypeLiquidityPool: + continue // You dont expect LiquidityPool type claim atoms to show up here + } + } + return fills +} + +func processOfferEventsFromChange(change ingest.Change) ([]TradeEvent, error) { + if !(change.Type == xdr.LedgerEntryTypeOffer) { + return nil, nil + } + + //TODO: Handle the case where the an upgrade might have caused an eviction of offerEntry + if !(change.Reason == ingest.LedgerEntryChangeReasonOperation) { + return nil, nil + } + + op, opResult, err := getOperationAndResultFromTransaction(change.OperationIndex, change.Transaction) + if err != nil { + return []TradeEvent{}, err + } + + offerEvents := make([]TradeEvent, 0) + claims, takerOfferEntry := getClaimsFromOperationAndResult(op, opResult) + + switch { + case change.Pre == nil && change.Post != nil: + // A new offer Entry is Created because of an operation. Might have partial fills + // This is the case where there is a incoming new offer + // You WONT be in this case statement, if an order was immediately filled, i.e if takerOfferEntry exists + newOffer := change.Post.Data.MustOffer() + offerEvent := OfferCreatedEvent{ + SellerId: newOffer.SellerId, + OfferId: newOffer.OfferId, // offerID = 222 aka new offerId + CreatedLedgerSeq: uint32(change.Post.LastModifiedLedgerSeq), + OfferState: newOffer, + Fills: generateOfferFills(change.Transaction, op, opResult, claims, nil), + } + offerEvents = append(offerEvents, offerEvent) + + case change.Pre != nil && change.Post != nil: + // An existing offer is updated. Might have fills + // You will be in this case only if you are an already existing order, i.e maker + oldOffer := change.Pre.Data.MustOffer() + updatedOffer := change.Post.Data.MustOffer() + offerEvent := OfferUpdatedEvent{ + SellerId: oldOffer.SellerId, + OfferId: oldOffer.OfferId, + UpdatedLedgerSeq: uint32(change.Post.LastModifiedLedgerSeq), + PrevUpdatedLedgerSeq: uint32(change.Pre.LastModifiedLedgerSeq), + PreviousOfferState: oldOffer, + UpdatedOfferState: updatedOffer, + Fills: generateOfferFills(change.Transaction, op, opResult, claims, takerOfferEntry), + } + offerEvents = append(offerEvents, offerEvent) + + case change.Pre != nil && change.Post == nil: + oldOffer := change.Pre.Data.MustOffer() + fills := generateOfferFills(change.Transaction, op, opResult, claims, takerOfferEntry) + var closeReason OfferCloseReason + if len(fills) > 0 { + closeReason = OfferCloseReasonOfferFullyFilled + } else { + closeReason = OfferCloseReasonOfferCancelled + } + offerEvent := OfferClosedEvent{ + SellerId: oldOffer.SellerId, + OfferId: oldOffer.OfferId, + PreviousOfferState: oldOffer, + PrevUpdatedLedgerSeq: uint32(change.Pre.LastModifiedLedgerSeq), + ClosedLedgerSeq: change.Ledger.LedgerSequence(), + CloseReason: closeReason, + Fills: fills, + } + offerEvents = append(offerEvents, offerEvent) + } + + return offerEvents, nil } func processLiquidityPoolEventsFromChange(change ingest.Change) ([]TradeEvent, error) { + if !(change.Type == xdr.LedgerEntryTypeLiquidityPool) { + return nil, nil + } return []TradeEvent{}, nil } diff --git a/ingest/trades/main.go b/ingest/trades/main.go index b27ee6edc9..0016639666 100644 --- a/ingest/trades/main.go +++ b/ingest/trades/main.go @@ -16,20 +16,12 @@ type TradeEvent interface { GetTradeEventType() TradeEventType // Method to retrieve the type of the trade event } -type OfferBase struct { - Selling xdr.Asset // Asset being sold - Buying xdr.Asset // Asset being bought - Amount xdr.Int64 // Total amount of the selling asset - Price xdr.Price // Price of the offer - Flags uint32 // Flags for the offer (e.g., passive, sell offers) -} - type OfferCreatedEvent struct { - SellerId xdr.AccountId // Account ID of the seller - OfferId xdr.Int64 // ID of the created offer - OfferState OfferBase // Initial state of the offer - CreatedLedgerSeq uint32 // Ledger sequence where the offer was created - Fills []FillInfo // List of fills that occurred during the creation + SellerId xdr.AccountId // Account ID of the seller + OfferId xdr.Int64 // ID of the created offer + OfferState xdr.OfferEntry // Initial state of the offer + CreatedLedgerSeq uint32 // Ledger sequence where the offer was created + Fills []FillInfo // List of fills that occurred during the creation } func (e OfferCreatedEvent) GetTradeEventType() TradeEventType { @@ -37,24 +29,36 @@ func (e OfferCreatedEvent) GetTradeEventType() TradeEventType { } type OfferUpdatedEvent struct { - SellerId xdr.AccountId // Account ID of the seller - OfferId xdr.Int64 // ID of the updated offer - PrevUpdatedLedgerSeq uint32 // Ledger sequence of the previous update - PreviousOfferState OfferBase // Previous state of the offer - UpdatedOfferState OfferBase // Updated state of the offer - UpdatedLedgerSeq uint32 // Ledger sequence where the offer was updated - Fills []FillInfo // List of fills that occurred during the update + SellerId xdr.AccountId // Account ID of the seller + OfferId xdr.Int64 // ID of the updated offer + PrevUpdatedLedgerSeq uint32 // Ledger sequence of the previous update + PreviousOfferState xdr.OfferEntry // Previous state of the offer + UpdatedOfferState xdr.OfferEntry // Updated state of the offer + UpdatedLedgerSeq uint32 // Ledger sequence where the offer was updated + Fills []FillInfo // List of fills that occurred during the update } func (e OfferUpdatedEvent) GetTradeEventType() TradeEventType { return TradeEventTypeOfferUpdated } +type OfferCloseReason uint32 + +const ( + OfferCloseReasonUnknown OfferCloseReason = iota + OfferCloseReasonOfferCancelled + OfferCloseReasonOfferFullyFilled + OfferCloseReasonUpgrade +) + type OfferClosedEvent struct { - SellerId xdr.AccountId // Account ID of the seller - OfferId xdr.Int64 // ID of the closed offer - LastOfferState OfferBase // Last state of the offer before closing - ClosedLedgerSeq uint32 // Ledger sequence where the offer was closed + SellerId xdr.AccountId // Account ID of the seller + OfferId xdr.Int64 // ID of the closed offer + PrevUpdatedLedgerSeq uint32 // Ledger sequence of the previous update + PreviousOfferState xdr.OfferEntry // Last state of the offer before closing + Fills []FillInfo // You could still have fills as a part of the offer being evicted + ClosedLedgerSeq uint32 // Ledger sequence where the offer was closed + CloseReason OfferCloseReason } func (e OfferClosedEvent) GetTradeEventType() TradeEventType { @@ -77,6 +81,7 @@ const ( FillSourceOperationTypeManageSell FillSourceOperationTypePathPaymentStrictSend FillSourceOperationTypePathPaymentStrictReceive + FillSourceOperationTypePassiveSellOffer ) type FillSource struct { diff --git a/xdr/claim_atom.go b/xdr/claim_atom.go index 7adacfb922..e1972ef635 100644 --- a/xdr/claim_atom.go +++ b/xdr/claim_atom.go @@ -19,6 +19,17 @@ func (a ClaimAtom) OfferId() Int64 { } } +func (a ClaimAtom) LiquidityPoolId() PoolId { + switch a.Type { + case ClaimAtomTypeClaimAtomTypeLiquidityPool: + return a.LiquidityPool.LiquidityPoolId + case ClaimAtomTypeClaimAtomTypeV0, ClaimAtomTypeClaimAtomTypeOrderBook: + panic(errors.New("orderbooks don't have LiquidityPoolId")) + default: + panic(fmt.Errorf("Unknown ClaimAtom type: %v", a.Type)) + } +} + func (a ClaimAtom) SellerId() AccountId { switch a.Type { case ClaimAtomTypeClaimAtomTypeV0: