github.com/stellar/stellar-etl@v1.0.1-0.20240312145900-4874b6bf2b89/cmd/export_orderbooks.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"math"
     7  	"os"
     8  	"path/filepath"
     9  
    10  	"github.com/spf13/cobra"
    11  
    12  	"github.com/stellar/stellar-etl/internal/input"
    13  	"github.com/stellar/stellar-etl/internal/utils"
    14  
    15  	"github.com/stellar/go/xdr"
    16  )
    17  
    18  // exportOrderbooksCmd represents the exportOrderbooks command
    19  var exportOrderbooksCmd = &cobra.Command{
    20  	Use:   "export_orderbooks",
    21  	Short: "This command exports the historical orderbooks",
    22  	Long: `This command instantiates a stellar-core instance and uses it to export normalized orderbooks.
    23  	The information is exported in batches determined by the batch-size flag. The normalized data is exported in multiple 
    24  	different files within the exported data folder. These files are dimAccounts.txt, dimOffers.txt, dimMarkets.txt, and factEvents.txt.
    25  	These files contain normalized data that helps save storage space. 
    26  	
    27  	If the end-ledger is omitted, then the stellar-core node will continue running and exporting information as new ledgers are 
    28  	confirmed by the Stellar network. In this unbounded case, a stellar-core config path is required to utilize the Captive Core toml.`,
    29  	Run: func(cmd *cobra.Command, args []string) {
    30  		endNum, strictExport, isTest, isFuture, extra := utils.MustCommonFlags(cmd.Flags(), cmdLogger)
    31  		cmdLogger.StrictExport = strictExport
    32  		env := utils.GetEnvironmentDetails(isTest, isFuture)
    33  
    34  		execPath, configPath, startNum, batchSize, outputFolder := utils.MustCoreFlags(cmd.Flags(), cmdLogger)
    35  		cloudStorageBucket, cloudCredentials, cloudProvider := utils.MustCloudStorageFlags(cmd.Flags(), cmdLogger)
    36  
    37  		if batchSize <= 0 {
    38  			cmdLogger.Fatalf("batch-size (%d) must be greater than 0", batchSize)
    39  		}
    40  
    41  		if configPath == "" && endNum == 0 {
    42  			cmdLogger.Fatal("stellar-core needs a config file path when exporting ledgers continuously (endNum = 0)")
    43  		}
    44  
    45  		var err error
    46  		execPath, err = filepath.Abs(execPath)
    47  		if err != nil {
    48  			cmdLogger.Fatal("could not get absolute filepath for stellar-core executable: ", err)
    49  		}
    50  
    51  		configPath, err = filepath.Abs(configPath)
    52  		if err != nil {
    53  			cmdLogger.Fatal("could not get absolute filepath for the config file: ", err)
    54  		}
    55  
    56  		checkpointSeq := utils.GetMostRecentCheckpoint(startNum)
    57  		core, err := input.PrepareCaptiveCore(execPath, configPath, checkpointSeq, endNum, env)
    58  		if err != nil {
    59  			cmdLogger.Fatal("error creating a prepared captive core instance: ", err)
    60  		}
    61  
    62  		orderbook, err := input.GetEntriesFromGenesis(checkpointSeq, xdr.LedgerEntryTypeOffer, env.ArchiveURLs)
    63  		if err != nil {
    64  			cmdLogger.Fatal("could not read initial orderbook: ", err)
    65  		}
    66  
    67  		orderbookChannel := make(chan input.OrderbookBatch)
    68  
    69  		go input.StreamOrderbooks(core, startNum, endNum, batchSize, orderbookChannel, orderbook, env, cmdLogger)
    70  
    71  		// If the end sequence number is defined, we work in a closed range and export a finite number of batches
    72  		if endNum != 0 {
    73  			batchCount := uint32(math.Ceil(float64(endNum-startNum+1) / float64(batchSize)))
    74  			for i := uint32(0); i < batchCount; i++ {
    75  				batchStart := startNum + i*batchSize
    76  				// Subtract 1 from the end batch number because batches do not include the last batch in the range
    77  				batchEnd := batchStart + batchSize - 1
    78  				if batchEnd > endNum {
    79  					batchEnd = endNum
    80  				}
    81  
    82  				parser := input.ReceiveParsedOrderbooks(orderbookChannel, cmdLogger)
    83  				exportOrderbook(batchStart, batchEnd, outputFolder, parser, cloudCredentials, cloudStorageBucket, cloudProvider, extra)
    84  			}
    85  		} else {
    86  			// otherwise, we export in an unbounded manner where batches are constantly exported
    87  			var batchNum uint32 = 0
    88  			for {
    89  				batchStart := startNum + batchNum*batchSize
    90  				batchEnd := batchStart + batchSize - 1
    91  				parser := input.ReceiveParsedOrderbooks(orderbookChannel, cmdLogger)
    92  				exportOrderbook(batchStart, batchEnd, outputFolder, parser, cloudCredentials, cloudStorageBucket, cloudProvider, extra)
    93  				batchNum++
    94  			}
    95  		}
    96  	},
    97  }
    98  
    99  // writeSlice writes the slice either to a file.
   100  func writeSlice(file *os.File, slice [][]byte, extra map[string]string) error {
   101  
   102  	for _, data := range slice {
   103  		bytesToWrite := data
   104  		if len(extra) > 0 {
   105  			i := map[string]interface{}{}
   106  			decoder := json.NewDecoder(bytes.NewReader(data))
   107  			decoder.UseNumber()
   108  			err := decoder.Decode(&i)
   109  			if err != nil {
   110  				return err
   111  			}
   112  			for k, v := range extra {
   113  				i[k] = v
   114  			}
   115  			bytesToWrite, err = json.Marshal(i)
   116  			if err != nil {
   117  				return err
   118  			}
   119  		}
   120  		file.WriteString(string(bytesToWrite) + "\n")
   121  	}
   122  
   123  	file.Close()
   124  	return nil
   125  }
   126  
   127  func exportOrderbook(
   128  	start, end uint32,
   129  	folderPath string,
   130  	parser *input.OrderbookParser,
   131  	cloudCredentials, cloudStorageBucket, cloudProvider string,
   132  	extra map[string]string) {
   133  	marketsFilePath := filepath.Join(folderPath, exportFilename(start, end, "dimMarkets"))
   134  	offersFilePath := filepath.Join(folderPath, exportFilename(start, end, "dimOffers"))
   135  	accountsFilePath := filepath.Join(folderPath, exportFilename(start, end, "dimAccounts"))
   136  	eventsFilePath := filepath.Join(folderPath, exportFilename(start, end, "factEvents"))
   137  
   138  	marketsFile := mustOutFile(marketsFilePath)
   139  	offersFile := mustOutFile(offersFilePath)
   140  	accountsFile := mustOutFile(accountsFilePath)
   141  	eventsFile := mustOutFile(eventsFilePath)
   142  
   143  	err := writeSlice(marketsFile, parser.Markets, extra)
   144  	if err != nil {
   145  		cmdLogger.LogError(err)
   146  	}
   147  	err = writeSlice(offersFile, parser.Offers, extra)
   148  	if err != nil {
   149  		cmdLogger.LogError(err)
   150  	}
   151  	err = writeSlice(accountsFile, parser.Accounts, extra)
   152  	if err != nil {
   153  		cmdLogger.LogError(err)
   154  	}
   155  	err = writeSlice(eventsFile, parser.Events, extra)
   156  	if err != nil {
   157  		cmdLogger.LogError(err)
   158  	}
   159  
   160  	maybeUpload(cloudCredentials, cloudStorageBucket, cloudProvider, marketsFilePath)
   161  	maybeUpload(cloudCredentials, cloudStorageBucket, cloudProvider, offersFilePath)
   162  	maybeUpload(cloudCredentials, cloudStorageBucket, cloudProvider, accountsFilePath)
   163  	maybeUpload(cloudCredentials, cloudStorageBucket, cloudProvider, eventsFilePath)
   164  }
   165  
   166  func init() {
   167  	rootCmd.AddCommand(exportOrderbooksCmd)
   168  	utils.AddCommonFlags(exportOrderbooksCmd.Flags())
   169  	utils.AddCoreFlags(exportOrderbooksCmd.Flags(), "orderbooks_output/")
   170  	utils.AddCloudStorageFlags(exportOrderbooksCmd.Flags())
   171  
   172  	exportOrderbooksCmd.MarkFlagRequired("start-ledger")
   173  	/*
   174  		Current flags:
   175  			start-ledger: the ledger sequence number for the beginning of the export period
   176  			end-ledger: the ledger sequence number for the end of the export range
   177  
   178  			output-folder: folder that will contain the output files
   179  			limit: maximum number of changes to export in a given batch; if negative then everything gets exported
   180  			batch-size: size of the export batches
   181  
   182  			core-executable: path to stellar-core executable
   183  			core-config: path to stellar-core config file
   184  	*/
   185  }