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  }