github.com/stellar/stellar-etl@v1.0.1-0.20240312145900-4874b6bf2b89/internal/transform/offer_normalized.go (about) 1 package transform 2 3 import ( 4 "fmt" 5 "hash/fnv" 6 "sort" 7 "strings" 8 9 "github.com/stellar/stellar-etl/internal/utils" 10 11 "github.com/stellar/go/ingest" 12 "github.com/stellar/go/xdr" 13 ) 14 15 // TransformOfferNormalized converts an offer into a normalized form, allowing it to be stored as part of the historical orderbook dataset 16 func TransformOfferNormalized(ledgerChange ingest.Change, ledgerSeq uint32) (NormalizedOfferOutput, error) { 17 18 var header xdr.LedgerHeaderHistoryEntry 19 transformed, err := TransformOffer(ledgerChange, header) 20 if err != nil { 21 return NormalizedOfferOutput{}, err 22 } 23 24 if transformed.Deleted { 25 return NormalizedOfferOutput{}, fmt.Errorf("offer %d is deleted", transformed.OfferID) 26 } 27 28 buyingAsset, sellingAsset, err := extractAssets(ledgerChange, transformed) 29 if err != nil { 30 return NormalizedOfferOutput{}, err 31 } 32 33 outputMarket, err := extractDimMarket(transformed, buyingAsset, sellingAsset) 34 if err != nil { 35 return NormalizedOfferOutput{}, err 36 } 37 38 outputAccount, err := extractDimAccount(transformed) 39 if err != nil { 40 return NormalizedOfferOutput{}, err 41 } 42 43 outputOffer, err := extractDimOffer(transformed, buyingAsset, sellingAsset, outputMarket.ID, outputAccount.ID) 44 if err != nil { 45 return NormalizedOfferOutput{}, err 46 } 47 48 return NormalizedOfferOutput{ 49 Market: outputMarket, 50 Account: outputAccount, 51 Offer: outputOffer, 52 Event: FactOfferEvent{ 53 LedgerSeq: ledgerSeq, 54 OfferInstanceID: outputOffer.DimOfferID, 55 }, 56 }, nil 57 } 58 59 // extractAssets extracts the buying and selling assets as strings of the format code:issuer 60 func extractAssets(ledgerChange ingest.Change, transformed OfferOutput) (string, string, error) { 61 ledgerEntry, _, _, err := utils.ExtractEntryFromChange(ledgerChange) 62 if err != nil { 63 return "", "", err 64 } 65 66 offerEntry, offerFound := ledgerEntry.Data.GetOffer() 67 if !offerFound { 68 return "", "", fmt.Errorf("Could not extract offer data from ledger entry; actual type is %s", ledgerEntry.Data.Type) 69 } 70 71 var sellType, sellCode, sellIssuer string 72 err = offerEntry.Selling.Extract(&sellType, &sellCode, &sellIssuer) 73 if err != nil { 74 return "", "", err 75 } 76 77 var outputSellingAsset string 78 if sellType != "native" { 79 outputSellingAsset = fmt.Sprintf("%s:%s", sellCode, sellIssuer) 80 } else { 81 // native assets have an empty issuer 82 outputSellingAsset = "native:" 83 } 84 85 var buyType, buyCode, buyIssuer string 86 err = offerEntry.Buying.Extract(&buyType, &buyCode, &buyIssuer) 87 if err != nil { 88 return "", "", err 89 } 90 91 var outputBuyingAsset string 92 if buyType != "native" { 93 outputBuyingAsset = fmt.Sprintf("%s:%s", buyCode, buyIssuer) 94 } else { 95 outputBuyingAsset = "native:" 96 } 97 98 return outputBuyingAsset, outputSellingAsset, nil 99 } 100 101 // extractDimMarket gets the DimMarket struct that corresponds to the provided offer and its buying/selling assets 102 func extractDimMarket(offer OfferOutput, buyingAsset, sellingAsset string) (DimMarket, error) { 103 assets := []string{buyingAsset, sellingAsset} 104 // sort in order to ensure markets have consistent base/counter pairs 105 // markets are stored as selling/buying == base/counter 106 sort.Strings(assets) 107 108 fnvHasher := fnv.New64a() 109 if _, err := fnvHasher.Write([]byte(strings.Join(assets, "/"))); err != nil { 110 return DimMarket{}, err 111 } 112 113 hash := fnvHasher.Sum64() 114 115 sellSplit := strings.Split(assets[0], ":") 116 buySplit := strings.Split(assets[1], ":") 117 118 if len(sellSplit) < 2 { 119 return DimMarket{}, fmt.Errorf("unable to get sell code and issuer for offer %d", offer.OfferID) 120 } 121 122 if len(buySplit) < 2 { 123 return DimMarket{}, fmt.Errorf("unable to get buy code and issuer for offer %d", offer.OfferID) 124 } 125 126 baseCode, baseIssuer := sellSplit[0], sellSplit[1] 127 counterCode, counterIssuer := buySplit[0], buySplit[1] 128 129 return DimMarket{ 130 ID: hash, 131 BaseCode: baseCode, 132 BaseIssuer: baseIssuer, 133 CounterCode: counterCode, 134 CounterIssuer: counterIssuer, 135 }, nil 136 } 137 138 // extractDimOffer extracts the DimOffer struct from the provided offer and its buying/selling assets 139 func extractDimOffer(offer OfferOutput, buyingAsset, sellingAsset string, marketID, makerID uint64) (DimOffer, error) { 140 importantFields := fmt.Sprintf("%d/%f/%f", offer.OfferID, offer.Amount, offer.Price) 141 142 fnvHasher := fnv.New64a() 143 if _, err := fnvHasher.Write([]byte(importantFields)); err != nil { 144 return DimOffer{}, err 145 } 146 147 offerHash := fnvHasher.Sum64() 148 149 assets := []string{buyingAsset, sellingAsset} 150 sort.Strings(assets) 151 152 var action string 153 if sellingAsset == assets[0] { 154 action = "s" 155 } else { 156 action = "b" 157 } 158 159 return DimOffer{ 160 HorizonID: offer.OfferID, 161 DimOfferID: offerHash, 162 MarketID: marketID, 163 MakerID: makerID, 164 Action: action, 165 BaseAmount: offer.Amount, 166 CounterAmount: float64(offer.Amount) * offer.Price, 167 Price: offer.Price, 168 }, nil 169 } 170 171 // extractDimAccount gets the DimAccount struct that corresponds to the provided offer 172 func extractDimAccount(offer OfferOutput) (DimAccount, error) { 173 var fnvHasher = fnv.New64a() 174 if _, err := fnvHasher.Write([]byte(offer.SellerID)); err != nil { 175 return DimAccount{}, err 176 } 177 178 accountID := fnvHasher.Sum64() 179 return DimAccount{ 180 Address: offer.SellerID, 181 ID: accountID, 182 }, nil 183 }