github.com/stellar/stellar-etl@v1.0.1-0.20240312145900-4874b6bf2b89/cmd/export_ledger_entry_changes.go (about) 1 package cmd 2 3 import ( 4 "fmt" 5 "math" 6 "os" 7 "path/filepath" 8 9 "github.com/spf13/cobra" 10 "github.com/stellar/go/xdr" 11 "github.com/stellar/stellar-etl/internal/input" 12 "github.com/stellar/stellar-etl/internal/transform" 13 "github.com/stellar/stellar-etl/internal/utils" 14 ) 15 16 var exportLedgerEntryChangesCmd = &cobra.Command{ 17 Use: "export_ledger_entry_changes", 18 Short: "This command exports the changes in accounts, offers, trustlines and liquidity pools.", 19 Long: `This command instantiates a stellar-core instance and uses it to export about accounts, offers, trustlines and liquidity pools. 20 The information is exported in batches determined by the batch-size flag. Each exported file will include the changes to the 21 relevant data type that occurred during that batch. 22 23 If the end-ledger is omitted, then the stellar-core node will continue running and exporting information as new ledgers are 24 confirmed by the Stellar network. 25 26 If no data type flags are set, then by default all of them are exported. If any are set, it is assumed that the others should not 27 be exported.`, 28 Run: func(cmd *cobra.Command, args []string) { 29 endNum, strictExport, isTest, isFuture, extra := utils.MustCommonFlags(cmd.Flags(), cmdLogger) 30 cmdLogger.StrictExport = strictExport 31 env := utils.GetEnvironmentDetails(isTest, isFuture) 32 33 execPath, configPath, startNum, batchSize, outputFolder := utils.MustCoreFlags(cmd.Flags(), cmdLogger) 34 exports := utils.MustExportTypeFlags(cmd.Flags(), cmdLogger) 35 cloudStorageBucket, cloudCredentials, cloudProvider := utils.MustCloudStorageFlags(cmd.Flags(), cmdLogger) 36 37 err := os.MkdirAll(outputFolder, os.ModePerm) 38 if err != nil { 39 cmdLogger.Fatalf("unable to mkdir %s: %v", outputFolder, err) 40 } 41 42 if batchSize <= 0 { 43 cmdLogger.Fatalf("batch-size (%d) must be greater than 0", batchSize) 44 } 45 46 // If none of the export flags are set, then we assume that everything should be exported 47 allFalse := true 48 for _, value := range exports { 49 if true == value { 50 allFalse = false 51 break 52 } 53 } 54 55 if allFalse { 56 for export_name, _ := range exports { 57 exports[export_name] = true 58 } 59 } 60 61 if configPath == "" && endNum == 0 { 62 cmdLogger.Fatal("stellar-core needs a config file path when exporting ledgers continuously (endNum = 0)") 63 } 64 65 execPath, err = filepath.Abs(execPath) 66 if err != nil { 67 cmdLogger.Fatal("could not get absolute filepath for stellar-core executable: ", err) 68 } 69 70 configPath, err = filepath.Abs(configPath) 71 if err != nil { 72 cmdLogger.Fatal("could not get absolute filepath for the config file: ", err) 73 } 74 75 core, err := input.PrepareCaptiveCore(execPath, configPath, startNum, endNum, env) 76 if err != nil { 77 cmdLogger.Fatal("error creating a prepared captive core instance: ", err) 78 } 79 80 if endNum == 0 { 81 endNum = math.MaxInt32 82 } 83 84 changeChan := make(chan input.ChangeBatch) 85 closeChan := make(chan int) 86 go input.StreamChanges(core, startNum, endNum, batchSize, changeChan, closeChan, env, cmdLogger) 87 88 for { 89 select { 90 case <-closeChan: 91 return 92 case batch, ok := <-changeChan: 93 if !ok { 94 continue 95 } 96 transformedOutputs := map[string][]interface{}{ 97 "accounts": {}, 98 "signers": {}, 99 "claimable_balances": {}, 100 "offers": {}, 101 "trustlines": {}, 102 "liquidity_pools": {}, 103 "contract_data": {}, 104 "contract_code": {}, 105 "config_settings": {}, 106 "ttl": {}, 107 } 108 109 for entryType, changes := range batch.Changes { 110 switch entryType { 111 case xdr.LedgerEntryTypeAccount: 112 if !exports["export-accounts"] { 113 continue 114 } 115 for i, change := range changes.Changes { 116 if changed, err := change.AccountChangedExceptSigners(); err != nil { 117 cmdLogger.LogError(fmt.Errorf("unable to identify changed accounts: %v", err)) 118 continue 119 } else if changed { 120 121 acc, err := transform.TransformAccount(change, changes.LedgerHeaders[i]) 122 if err != nil { 123 entry, _, _, _ := utils.ExtractEntryFromChange(change) 124 cmdLogger.LogError(fmt.Errorf("error transforming account entry last updated at %d: %s", entry.LastModifiedLedgerSeq, err)) 125 continue 126 } 127 transformedOutputs["accounts"] = append(transformedOutputs["accounts"], acc) 128 } 129 if change.AccountSignersChanged() { 130 signers, err := transform.TransformSigners(change, changes.LedgerHeaders[i]) 131 if err != nil { 132 entry, _, _, _ := utils.ExtractEntryFromChange(change) 133 cmdLogger.LogError(fmt.Errorf("error transforming account signers from %d :%s", entry.LastModifiedLedgerSeq, err)) 134 continue 135 } 136 for _, s := range signers { 137 transformedOutputs["signers"] = append(transformedOutputs["signers"], s) 138 } 139 } 140 } 141 case xdr.LedgerEntryTypeClaimableBalance: 142 if !exports["export-balances"] { 143 continue 144 } 145 for i, change := range changes.Changes { 146 balance, err := transform.TransformClaimableBalance(change, changes.LedgerHeaders[i]) 147 if err != nil { 148 entry, _, _, _ := utils.ExtractEntryFromChange(change) 149 cmdLogger.LogError(fmt.Errorf("error transforming balance entry last updated at %d: %s", entry.LastModifiedLedgerSeq, err)) 150 continue 151 } 152 transformedOutputs["claimable_balances"] = append(transformedOutputs["claimable_balances"], balance) 153 } 154 case xdr.LedgerEntryTypeOffer: 155 if !exports["export-offers"] { 156 continue 157 } 158 for i, change := range changes.Changes { 159 offer, err := transform.TransformOffer(change, changes.LedgerHeaders[i]) 160 if err != nil { 161 entry, _, _, _ := utils.ExtractEntryFromChange(change) 162 cmdLogger.LogError(fmt.Errorf("error transforming offer entry last updated at %d: %s", entry.LastModifiedLedgerSeq, err)) 163 continue 164 } 165 transformedOutputs["offers"] = append(transformedOutputs["offers"], offer) 166 } 167 case xdr.LedgerEntryTypeTrustline: 168 if !exports["export-trustlines"] { 169 continue 170 } 171 for i, change := range changes.Changes { 172 trust, err := transform.TransformTrustline(change, changes.LedgerHeaders[i]) 173 if err != nil { 174 entry, _, _, _ := utils.ExtractEntryFromChange(change) 175 cmdLogger.LogError(fmt.Errorf("error transforming trustline entry last updated at %d: %s", entry.LastModifiedLedgerSeq, err)) 176 continue 177 } 178 transformedOutputs["trustlines"] = append(transformedOutputs["trustlines"], trust) 179 } 180 case xdr.LedgerEntryTypeLiquidityPool: 181 if !exports["export-pools"] { 182 continue 183 } 184 for i, change := range changes.Changes { 185 pool, err := transform.TransformPool(change, changes.LedgerHeaders[i]) 186 if err != nil { 187 entry, _, _, _ := utils.ExtractEntryFromChange(change) 188 cmdLogger.LogError(fmt.Errorf("error transforming liquidity pool entry last updated at %d: %s", entry.LastModifiedLedgerSeq, err)) 189 continue 190 } 191 transformedOutputs["liquidity_pools"] = append(transformedOutputs["liquidity_pools"], pool) 192 } 193 case xdr.LedgerEntryTypeContractData: 194 if !exports["export-contract-data"] { 195 continue 196 } 197 for i, change := range changes.Changes { 198 TransformContractData := transform.NewTransformContractDataStruct(transform.AssetFromContractData, transform.ContractBalanceFromContractData) 199 contractData, err, _ := TransformContractData.TransformContractData(change, env.NetworkPassphrase, changes.LedgerHeaders[i]) 200 if err != nil { 201 entry, _, _, _ := utils.ExtractEntryFromChange(change) 202 cmdLogger.LogError(fmt.Errorf("error transforming contract data entry last updated at %d: %s", entry.LastModifiedLedgerSeq, err)) 203 continue 204 } 205 206 // Empty contract data that has no error is a nonce. Does not need to be recorded 207 if contractData == (transform.ContractDataOutput{}) { 208 continue 209 } 210 211 transformedOutputs["contract_data"] = append(transformedOutputs["contract_data"], contractData) 212 } 213 case xdr.LedgerEntryTypeContractCode: 214 if !exports["export-contract-code"] { 215 continue 216 } 217 for i, change := range changes.Changes { 218 contractCode, err := transform.TransformContractCode(change, changes.LedgerHeaders[i]) 219 if err != nil { 220 entry, _, _, _ := utils.ExtractEntryFromChange(change) 221 cmdLogger.LogError(fmt.Errorf("error transforming contract code entry last updated at %d: %s", entry.LastModifiedLedgerSeq, err)) 222 continue 223 } 224 transformedOutputs["contract_code"] = append(transformedOutputs["contract_code"], contractCode) 225 } 226 case xdr.LedgerEntryTypeConfigSetting: 227 if !exports["export-config-settings"] { 228 continue 229 } 230 for i, change := range changes.Changes { 231 configSettings, err := transform.TransformConfigSetting(change, changes.LedgerHeaders[i]) 232 if err != nil { 233 entry, _, _, _ := utils.ExtractEntryFromChange(change) 234 cmdLogger.LogError(fmt.Errorf("error transforming config settings entry last updated at %d: %s", entry.LastModifiedLedgerSeq, err)) 235 continue 236 } 237 transformedOutputs["config_settings"] = append(transformedOutputs["config_settings"], configSettings) 238 } 239 case xdr.LedgerEntryTypeTtl: 240 if !exports["export-ttl"] { 241 continue 242 } 243 for i, change := range changes.Changes { 244 ttl, err := transform.TransformTtl(change, changes.LedgerHeaders[i]) 245 if err != nil { 246 entry, _, _, _ := utils.ExtractEntryFromChange(change) 247 cmdLogger.LogError(fmt.Errorf("error transforming ttl entry last updated at %d: %s", entry.LastModifiedLedgerSeq, err)) 248 continue 249 } 250 transformedOutputs["ttl"] = append(transformedOutputs["ttl"], ttl) 251 } 252 } 253 } 254 255 err := exportTransformedData(batch.BatchStart, batch.BatchEnd, outputFolder, transformedOutputs, cloudCredentials, cloudStorageBucket, cloudProvider, extra) 256 if err != nil { 257 cmdLogger.LogError(err) 258 continue 259 } 260 } 261 } 262 }, 263 } 264 265 func exportTransformedData( 266 start, end uint32, 267 folderPath string, 268 transformedOutput map[string][]interface{}, 269 cloudCredentials, cloudStorageBucket, cloudProvider string, 270 extra map[string]string) error { 271 272 for resource, output := range transformedOutput { 273 // Filenames are typically exclusive of end point. This processor 274 // is different and we have to increment by 1 since the end batch number 275 // is included in this filename. 276 path := filepath.Join(folderPath, exportFilename(start, end+1, resource)) 277 outFile := mustOutFile(path) 278 for _, o := range output { 279 _, err := exportEntry(o, outFile, extra) 280 if err != nil { 281 return err 282 } 283 } 284 maybeUpload(cloudCredentials, cloudStorageBucket, cloudProvider, path) 285 } 286 287 return nil 288 } 289 290 func init() { 291 rootCmd.AddCommand(exportLedgerEntryChangesCmd) 292 utils.AddCommonFlags(exportLedgerEntryChangesCmd.Flags()) 293 utils.AddCoreFlags(exportLedgerEntryChangesCmd.Flags(), "changes_output/") 294 utils.AddExportTypeFlags(exportLedgerEntryChangesCmd.Flags()) 295 utils.AddCloudStorageFlags(exportLedgerEntryChangesCmd.Flags()) 296 297 exportLedgerEntryChangesCmd.MarkFlagRequired("start-ledger") 298 exportLedgerEntryChangesCmd.MarkFlagRequired("core-executable") 299 /* 300 Current flags: 301 start-ledger: the ledger sequence number for the beginning of the export period 302 end-ledger: the ledger sequence number for the end of the export range 303 304 output-folder: folder that will contain the output files 305 limit: maximum number of changes to export in a given batch; if negative then everything gets exported 306 batch-size: size of the export batches 307 308 core-executable: path to stellar-core executable 309 core-config: path to stellar-core config file 310 311 If none of the export_X flags are set, assume everything should be exported 312 export_accounts: boolean flag; if set then accounts should be exported 313 export_trustlines: boolean flag; if set then trustlines should be exported 314 export_offers: boolean flag; if set then offers should be exported 315 316 TODO: implement extra flags if possible 317 serialize-method: the method for serialization of the output data (JSON, XDR, etc) 318 start and end time as a replacement for start and end sequence numbers 319 */ 320 }