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 }