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  }