github.com/stellar/stellar-etl@v1.0.1-0.20240312145900-4874b6bf2b89/internal/utils/main.go (about)

     1  package utils
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"errors"
     7  	"fmt"
     8  	"math/big"
     9  	"time"
    10  
    11  	"github.com/spf13/pflag"
    12  
    13  	"github.com/stellar/go/hash"
    14  	"github.com/stellar/go/historyarchive"
    15  	"github.com/stellar/go/ingest"
    16  	"github.com/stellar/go/ingest/ledgerbackend"
    17  	"github.com/stellar/go/keypair"
    18  	"github.com/stellar/go/network"
    19  	"github.com/stellar/go/support/storage"
    20  	"github.com/stellar/go/txnbuild"
    21  	"github.com/stellar/go/xdr"
    22  )
    23  
    24  // PanicOnError is a function that panics if the provided error is not nil
    25  func PanicOnError(err error) {
    26  	if err != nil {
    27  		panic(err)
    28  	}
    29  }
    30  
    31  // HashToHexString is utility function that converts and xdr.Hash type to a hex string
    32  func HashToHexString(inputHash xdr.Hash) string {
    33  	sliceHash := inputHash[:]
    34  	hexString := hex.EncodeToString(sliceHash)
    35  	return hexString
    36  }
    37  
    38  // TimePointToUTCTimeStamp takes in an xdr TimePoint and converts it to a time.Time struct in UTC. It returns an error for negative timepoints
    39  func TimePointToUTCTimeStamp(providedTime xdr.TimePoint) (time.Time, error) {
    40  	intTime := int64(providedTime)
    41  	if intTime < 0 {
    42  		return time.Now(), errors.New("the timepoint is negative")
    43  	}
    44  	return time.Unix(intTime, 0).UTC(), nil
    45  }
    46  
    47  // GetAccountAddressFromMuxedAccount takes in a muxed account and returns the address of the account
    48  func GetAccountAddressFromMuxedAccount(account xdr.MuxedAccount) (string, error) {
    49  	providedID := account.ToAccountId()
    50  	pointerToID := &providedID
    51  	return pointerToID.GetAddress()
    52  }
    53  
    54  // CreateSampleTx creates a transaction with a single operation (BumpSequence), the min base fee, and infinite timebounds
    55  func CreateSampleTx(sequence int64, operationCount int) xdr.TransactionEnvelope {
    56  	kp, err := keypair.Random()
    57  	PanicOnError(err)
    58  
    59  	operations := []txnbuild.Operation{}
    60  	operationType := &txnbuild.BumpSequence{
    61  		BumpTo: 0,
    62  	}
    63  	for i := 0; i < operationCount; i++ {
    64  		operations = append(operations, operationType)
    65  	}
    66  
    67  	sourceAccount := txnbuild.NewSimpleAccount(kp.Address(), int64(0))
    68  	tx, err := txnbuild.NewTransaction(
    69  		txnbuild.TransactionParams{
    70  			SourceAccount: &sourceAccount,
    71  			Operations:    operations,
    72  			BaseFee:       txnbuild.MinBaseFee,
    73  			Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewInfiniteTimeout()},
    74  		},
    75  	)
    76  	PanicOnError(err)
    77  
    78  	env := tx.ToXDR()
    79  	return env
    80  }
    81  
    82  // ConvertStroopValueToReal converts a value in stroops, the smallest amount unit, into real units
    83  func ConvertStroopValueToReal(input xdr.Int64) float64 {
    84  	output, _ := big.NewRat(int64(input), int64(10000000)).Float64()
    85  	return output
    86  }
    87  
    88  // CreateSampleResultMeta creates Transaction results with the desired success flag and number of sub operation results
    89  func CreateSampleResultMeta(successful bool, subOperationCount int) xdr.TransactionResultMeta {
    90  	resultCode := xdr.TransactionResultCodeTxFailed
    91  	if successful {
    92  		resultCode = xdr.TransactionResultCodeTxSuccess
    93  	}
    94  	operationResults := []xdr.OperationResult{}
    95  	operationResultTr := &xdr.OperationResultTr{
    96  		Type: xdr.OperationTypeCreateAccount,
    97  		CreateAccountResult: &xdr.CreateAccountResult{
    98  			Code: 0,
    99  		},
   100  	}
   101  
   102  	for i := 0; i < subOperationCount; i++ {
   103  		operationResults = append(operationResults, xdr.OperationResult{
   104  			Code: xdr.OperationResultCodeOpInner,
   105  			Tr:   operationResultTr,
   106  		})
   107  	}
   108  
   109  	return xdr.TransactionResultMeta{
   110  		Result: xdr.TransactionResultPair{
   111  			Result: xdr.TransactionResult{
   112  				Result: xdr.TransactionResultResult{
   113  					Code:    resultCode,
   114  					Results: &operationResults,
   115  				},
   116  			},
   117  		},
   118  	}
   119  }
   120  
   121  func CreateSampleResultPair(successful bool, subOperationCount int) xdr.TransactionResultPair {
   122  	resultCode := xdr.TransactionResultCodeTxFailed
   123  	if successful {
   124  		resultCode = xdr.TransactionResultCodeTxSuccess
   125  	}
   126  	operationResults := []xdr.OperationResult{}
   127  	operationResultTr := &xdr.OperationResultTr{
   128  		Type: xdr.OperationTypeCreateAccount,
   129  		CreateAccountResult: &xdr.CreateAccountResult{
   130  			Code: 0,
   131  		},
   132  	}
   133  
   134  	for i := 0; i < subOperationCount; i++ {
   135  		operationResults = append(operationResults, xdr.OperationResult{
   136  			Code: xdr.OperationResultCodeOpInner,
   137  			Tr:   operationResultTr,
   138  		})
   139  	}
   140  
   141  	return xdr.TransactionResultPair{
   142  		Result: xdr.TransactionResult{
   143  			Result: xdr.TransactionResultResult{
   144  				Code:    resultCode,
   145  				Results: &operationResults,
   146  			},
   147  		},
   148  	}
   149  }
   150  
   151  func CreateSampleTxMeta(subOperationCount int, AssetA, AssetB xdr.Asset) *xdr.TransactionMetaV1 {
   152  	operationMeta := []xdr.OperationMeta{}
   153  	for i := 0; i < subOperationCount; i++ {
   154  		operationMeta = append(operationMeta, xdr.OperationMeta{
   155  			Changes: xdr.LedgerEntryChanges{},
   156  		})
   157  	}
   158  
   159  	operationMeta = AddLPOperations(operationMeta, AssetA, AssetB)
   160  	operationMeta = AddLPOperations(operationMeta, AssetA, AssetB)
   161  
   162  	operationMeta = append(operationMeta, xdr.OperationMeta{
   163  		Changes: xdr.LedgerEntryChanges{},
   164  	})
   165  
   166  	return &xdr.TransactionMetaV1{
   167  		Operations: operationMeta,
   168  	}
   169  }
   170  
   171  func AddLPOperations(txMeta []xdr.OperationMeta, AssetA, AssetB xdr.Asset) []xdr.OperationMeta {
   172  	txMeta = append(txMeta, xdr.OperationMeta{
   173  		Changes: xdr.LedgerEntryChanges{
   174  			xdr.LedgerEntryChange{
   175  				Type: xdr.LedgerEntryChangeTypeLedgerEntryState,
   176  				State: &xdr.LedgerEntry{
   177  					Data: xdr.LedgerEntryData{
   178  						Type: xdr.LedgerEntryTypeLiquidityPool,
   179  						LiquidityPool: &xdr.LiquidityPoolEntry{
   180  							LiquidityPoolId: xdr.PoolId{1, 2, 3, 4, 5, 6, 7, 8, 9},
   181  							Body: xdr.LiquidityPoolEntryBody{
   182  								Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct,
   183  								ConstantProduct: &xdr.LiquidityPoolEntryConstantProduct{
   184  									Params: xdr.LiquidityPoolConstantProductParameters{
   185  										AssetA: AssetA,
   186  										AssetB: AssetB,
   187  										Fee:    30,
   188  									},
   189  									ReserveA:                 100000,
   190  									ReserveB:                 1000,
   191  									TotalPoolShares:          500,
   192  									PoolSharesTrustLineCount: 25,
   193  								},
   194  							},
   195  						},
   196  					},
   197  				},
   198  			},
   199  			xdr.LedgerEntryChange{
   200  				Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated,
   201  				Updated: &xdr.LedgerEntry{
   202  					Data: xdr.LedgerEntryData{
   203  						Type: xdr.LedgerEntryTypeLiquidityPool,
   204  						LiquidityPool: &xdr.LiquidityPoolEntry{
   205  							LiquidityPoolId: xdr.PoolId{1, 2, 3, 4, 5, 6, 7, 8, 9},
   206  							Body: xdr.LiquidityPoolEntryBody{
   207  								Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct,
   208  								ConstantProduct: &xdr.LiquidityPoolEntryConstantProduct{
   209  									Params: xdr.LiquidityPoolConstantProductParameters{
   210  										AssetA: AssetA,
   211  										AssetB: AssetB,
   212  										Fee:    30,
   213  									},
   214  									ReserveA:                 101000,
   215  									ReserveB:                 1100,
   216  									TotalPoolShares:          502,
   217  									PoolSharesTrustLineCount: 26,
   218  								},
   219  							},
   220  						},
   221  					},
   222  				},
   223  			},
   224  		}})
   225  
   226  	return txMeta
   227  }
   228  
   229  // AddCommonFlags adds the flags common to all commands: end-ledger, stdout, and strict-export
   230  func AddCommonFlags(flags *pflag.FlagSet) {
   231  	flags.Uint32P("end-ledger", "e", 0, "The ledger sequence number for the end of the export range")
   232  	flags.Bool("strict-export", false, "If set, transform errors will be fatal.")
   233  	flags.Bool("testnet", false, "If set, will connect to Testnet instead of Mainnet.")
   234  	flags.Bool("futurenet", false, "If set, will connect to Futurenet instead of Mainnet.")
   235  	flags.StringToStringP("extra-fields", "u", map[string]string{}, "Additional fields to append to output jsons. Used for appending metadata")
   236  }
   237  
   238  // AddArchiveFlags adds the history archive specific flags: start-ledger, output, and limit
   239  func AddArchiveFlags(objectName string, flags *pflag.FlagSet) {
   240  	flags.Uint32P("start-ledger", "s", 2, "The ledger sequence number for the beginning of the export period. Defaults to genesis ledger")
   241  	flags.StringP("output", "o", "exported_"+objectName+".txt", "Filename of the output file")
   242  	flags.Int64P("limit", "l", -1, "Maximum number of "+objectName+" to export. If the limit is set to a negative number, all the objects in the provided range are exported")
   243  }
   244  
   245  // AddBucketFlags adds the bucket list specifc flags: output
   246  func AddBucketFlags(objectName string, flags *pflag.FlagSet) {
   247  	flags.StringP("output", "o", "exported_"+objectName+".txt", "Filename of the output file")
   248  }
   249  
   250  // AddCloudStorageFlags adds the cloud storage releated flags: cloud-storage-bucket, cloud-credentials
   251  func AddCloudStorageFlags(flags *pflag.FlagSet) {
   252  	flags.String("cloud-storage-bucket", "stellar-etl-cli", "Cloud storage bucket to export to.")
   253  	flags.String("cloud-credentials", "", "Path to cloud provider service account credentials. Only used for local/dev purposes. "+
   254  		"When run on GCP, credentials should be inferred by service account json.")
   255  	flags.String("cloud-provider", "", "Cloud provider for storage services.")
   256  }
   257  
   258  // AddCoreFlags adds the captive core specific flags: core-executable, core-config, batch-size, and output flags
   259  func AddCoreFlags(flags *pflag.FlagSet, defaultFolder string) {
   260  	flags.StringP("core-executable", "x", "", "Filepath to the stellar-core executable")
   261  	flags.StringP("core-config", "c", "", "Filepath to the config file for stellar-core")
   262  
   263  	flags.Uint32P("batch-size", "b", 64, "number of ledgers to export changes from in each batches")
   264  	flags.StringP("output", "o", defaultFolder, "Folder that will contain the output files")
   265  
   266  	flags.Uint32P("start-ledger", "s", 2, "The ledger sequence number for the beginning of the export period. Defaults to genesis ledger")
   267  }
   268  
   269  // AddExportTypeFlags adds the captive core specifc flags: export-{type} flags
   270  func AddExportTypeFlags(flags *pflag.FlagSet) {
   271  	flags.BoolP("export-accounts", "a", false, "set in order to export account changes")
   272  	flags.BoolP("export-trustlines", "t", false, "set in order to export trustline changes")
   273  	flags.BoolP("export-offers", "f", false, "set in order to export offer changes")
   274  	flags.BoolP("export-pools", "p", false, "set in order to export liquidity pool changes")
   275  	flags.BoolP("export-balances", "l", false, "set in order to export claimable balance changes")
   276  	flags.BoolP("export-contract-code", "", false, "set in order to export contract code changes")
   277  	flags.BoolP("export-contract-data", "", false, "set in order to export contract data changes")
   278  	flags.BoolP("export-config-settings", "", false, "set in order to export config settings changes")
   279  	flags.BoolP("export-ttl", "", false, "set in order to export ttl changes")
   280  }
   281  
   282  // MustCommonFlags gets the values of the the flags common to all commands: end-ledger and strict-export. If any do not exist, it stops the program fatally using the logger
   283  func MustCommonFlags(flags *pflag.FlagSet, logger *EtlLogger) (endNum uint32, strictExport, isTest bool, isFuture bool, extra map[string]string) {
   284  	endNum, err := flags.GetUint32("end-ledger")
   285  	if err != nil {
   286  		logger.Fatal("could not get end sequence number: ", err)
   287  	}
   288  
   289  	strictExport, err = flags.GetBool("strict-export")
   290  	if err != nil {
   291  		logger.Fatal("could not get strict-export boolean: ", err)
   292  	}
   293  
   294  	isTest, err = flags.GetBool("testnet")
   295  	if err != nil {
   296  		logger.Fatal("could not get testnet boolean: ", err)
   297  	}
   298  
   299  	isFuture, err = flags.GetBool("futurenet")
   300  	if err != nil {
   301  		logger.Fatal("could not get futurenet boolean: ", err)
   302  	}
   303  
   304  	extra, err = flags.GetStringToString("extra-fields")
   305  	if err != nil {
   306  		logger.Fatal("could not get extra fields string: ", err)
   307  	}
   308  	return
   309  }
   310  
   311  // MustArchiveFlags gets the values of the the history archive specific flags: start-ledger, output, and limit
   312  func MustArchiveFlags(flags *pflag.FlagSet, logger *EtlLogger) (startNum uint32, path string, limit int64) {
   313  	startNum, err := flags.GetUint32("start-ledger")
   314  	if err != nil {
   315  		logger.Fatal("could not get start sequence number: ", err)
   316  	}
   317  
   318  	path, err = flags.GetString("output")
   319  	if err != nil {
   320  		logger.Fatal("could not get output filename: ", err)
   321  	}
   322  
   323  	limit, err = flags.GetInt64("limit")
   324  	if err != nil {
   325  		logger.Fatal("could not get limit: ", err)
   326  	}
   327  
   328  	return
   329  }
   330  
   331  // MustBucketFlags gets the values of the bucket list specific flags: output
   332  func MustBucketFlags(flags *pflag.FlagSet, logger *EtlLogger) (path string) {
   333  	path, err := flags.GetString("output")
   334  	if err != nil {
   335  		logger.Fatal("could not get output filename: ", err)
   336  	}
   337  
   338  	return
   339  }
   340  
   341  // MustCloudStorageFlags gets the values of the bucket list specific flags: cloud-storage-bucket, cloud-credentials
   342  func MustCloudStorageFlags(flags *pflag.FlagSet, logger *EtlLogger) (bucket, credentials, provider string) {
   343  	bucket, err := flags.GetString("cloud-storage-bucket")
   344  	if err != nil {
   345  		logger.Fatal("could not get cloud storage bucket: ", err)
   346  	}
   347  
   348  	credentials, err = flags.GetString("cloud-credentials")
   349  	if err != nil {
   350  		logger.Fatal("could not get cloud credentials file: ", err)
   351  	}
   352  
   353  	provider, err = flags.GetString("cloud-provider")
   354  	if err != nil {
   355  		logger.Fatal("could not get cloud provider: ", err)
   356  	}
   357  
   358  	return
   359  }
   360  
   361  // MustCoreFlags gets the values for the core-executable, core-config, start ledger batch-size, and output flags. If any do not exist, it stops the program fatally using the logger
   362  func MustCoreFlags(flags *pflag.FlagSet, logger *EtlLogger) (execPath, configPath string, startNum, batchSize uint32, path string) {
   363  	execPath, err := flags.GetString("core-executable")
   364  	if err != nil {
   365  		logger.Fatal("could not get path to stellar-core executable, which is mandatory when not starting at the genesis ledger (ledger 1): ", err)
   366  	}
   367  
   368  	configPath, err = flags.GetString("core-config")
   369  	if err != nil {
   370  		logger.Fatal("could not get path to stellar-core config file, is mandatory when not starting at the genesis ledger (ledger 1): ", err)
   371  	}
   372  
   373  	path, err = flags.GetString("output")
   374  	if err != nil {
   375  		logger.Fatal("could not get output filename: ", err)
   376  	}
   377  
   378  	startNum, err = flags.GetUint32("start-ledger")
   379  	if err != nil {
   380  		logger.Fatal("could not get start sequence number: ", err)
   381  	}
   382  
   383  	batchSize, err = flags.GetUint32("batch-size")
   384  	if err != nil {
   385  		logger.Fatal("could not get batch size: ", err)
   386  	}
   387  
   388  	return
   389  }
   390  
   391  // MustExportTypeFlags gets the values for the export-accounts, export-offers, and export-trustlines flags. If any do not exist, it stops the program fatally using the logger
   392  // func MustExportTypeFlags(flags *pflag.FlagSet, logger *EtlLogger) (exportAccounts, exportOffers, exportTrustlines, exportPools, exportBalances, exportContractCode, exportContractData, exportConfigSettings, exportTtl bool) {
   393  func MustExportTypeFlags(flags *pflag.FlagSet, logger *EtlLogger) map[string]bool {
   394  	var err error
   395  	exports := map[string]bool{
   396  		"export-accounts":        false,
   397  		"export-trustlines":      false,
   398  		"export-offers":          false,
   399  		"export-pools":           false,
   400  		"export-balances":        false,
   401  		"export-contract-code":   false,
   402  		"export-contract-data":   false,
   403  		"export-config-settings": false,
   404  		"export-ttl":             false,
   405  	}
   406  
   407  	for export_name, _ := range exports {
   408  		exports[export_name], err = flags.GetBool(export_name)
   409  		if err != nil {
   410  			logger.Fatalf("could not get %s flag: %v", export_name, err)
   411  		}
   412  	}
   413  
   414  	return exports
   415  }
   416  
   417  type historyArchiveBackend struct {
   418  	client  historyarchive.ArchiveInterface
   419  	ledgers map[uint32]*historyarchive.Ledger
   420  }
   421  
   422  func (h historyArchiveBackend) GetLatestLedgerSequence(ctx context.Context) (sequence uint32, err error) {
   423  	root, err := h.client.GetRootHAS()
   424  	if err != nil {
   425  		return 0, err
   426  	}
   427  	return root.CurrentLedger, nil
   428  }
   429  
   430  func (h historyArchiveBackend) GetLedgers(ctx context.Context) (map[uint32]*historyarchive.Ledger, error) {
   431  
   432  	return h.ledgers, nil
   433  }
   434  
   435  func (h historyArchiveBackend) GetLedgerArchive(ctx context.Context, sequence uint32) (historyarchive.Ledger, error) {
   436  	ledger, ok := h.ledgers[sequence]
   437  	if !ok {
   438  		return historyarchive.Ledger{}, fmt.Errorf("ledger %d is missing from map", sequence)
   439  	}
   440  
   441  	historyLedger := historyarchive.Ledger{
   442  		Header:            ledger.Header,
   443  		Transaction:       ledger.Transaction,
   444  		TransactionResult: ledger.TransactionResult,
   445  	}
   446  
   447  	return historyLedger, nil
   448  }
   449  
   450  func (h historyArchiveBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) {
   451  	ledger, ok := h.ledgers[sequence]
   452  	if !ok {
   453  		return xdr.LedgerCloseMeta{}, fmt.Errorf("ledger %d is missing from map", sequence)
   454  	}
   455  
   456  	lcm := xdr.LedgerCloseMeta{
   457  		V: 0,
   458  		V0: &xdr.LedgerCloseMetaV0{
   459  			LedgerHeader: ledger.Header,
   460  			TxSet:        ledger.Transaction.TxSet,
   461  		},
   462  	}
   463  	lcm.V0.TxProcessing = make([]xdr.TransactionResultMeta, len(ledger.TransactionResult.TxResultSet.Results))
   464  	for i, result := range ledger.TransactionResult.TxResultSet.Results {
   465  		lcm.V0.TxProcessing[i].Result = result
   466  	}
   467  
   468  	return lcm, nil
   469  }
   470  
   471  func (h historyArchiveBackend) PrepareRange(ctx context.Context, ledgerRange ledgerbackend.Range) error {
   472  	return nil
   473  }
   474  
   475  func (h historyArchiveBackend) IsPrepared(ctx context.Context, ledgerRange ledgerbackend.Range) (bool, error) {
   476  	return true, nil
   477  }
   478  
   479  func (h historyArchiveBackend) Close() error {
   480  	return nil
   481  }
   482  
   483  // ValidateLedgerRange validates the given ledger range
   484  func ValidateLedgerRange(start, end, latestNum uint32) error {
   485  	if start == 0 {
   486  		return fmt.Errorf("start sequence number equal to 0. There is no ledger 0 (genesis ledger is ledger 1)")
   487  	}
   488  
   489  	if end == 0 {
   490  		return fmt.Errorf("end sequence number equal to 0. There is no ledger 0 (genesis ledger is ledger 1)")
   491  	}
   492  
   493  	if end < start {
   494  		return fmt.Errorf("end sequence number is less than start (%d < %d)", end, start)
   495  	}
   496  
   497  	if latestNum < start {
   498  		return fmt.Errorf("latest sequence number is less than start sequence number (%d < %d)", latestNum, start)
   499  	}
   500  
   501  	if latestNum < end {
   502  		return fmt.Errorf("latest sequence number is less than end sequence number (%d < %d)", latestNum, end)
   503  	}
   504  
   505  	return nil
   506  }
   507  
   508  func CreateBackend(start, end uint32, archiveURLs []string) (historyArchiveBackend, error) {
   509  	client, err := CreateHistoryArchiveClient(archiveURLs)
   510  	if err != nil {
   511  		return historyArchiveBackend{}, err
   512  	}
   513  
   514  	root, err := client.GetRootHAS()
   515  	if err != nil {
   516  		return historyArchiveBackend{}, err
   517  	}
   518  	if err = ValidateLedgerRange(start, end, root.CurrentLedger); err != nil {
   519  		return historyArchiveBackend{}, err
   520  	}
   521  
   522  	ledgers, err := client.GetLedgers(start, end)
   523  	if err != nil {
   524  		return historyArchiveBackend{}, err
   525  	}
   526  	return historyArchiveBackend{client: client, ledgers: ledgers}, nil
   527  }
   528  
   529  // mainnet history archive URLs
   530  var mainArchiveURLs = []string{
   531  	"https://history.stellar.org/prd/core-live/core_live_001",
   532  	"https://history.stellar.org/prd/core-live/core_live_002",
   533  	"https://history.stellar.org/prd/core-live/core_live_003",
   534  }
   535  
   536  // testnet is only used for local testing with new Protocol features
   537  var testArchiveURLs = []string{
   538  	"https://history.stellar.org/prd/core-testnet/core_testnet_001",
   539  	"https://history.stellar.org/prd/core-testnet/core_testnet_002",
   540  	"https://history.stellar.org/prd/core-testnet/core_testnet_003",
   541  }
   542  
   543  // futrenet is used for testing new Protocol features
   544  var futureArchiveURLs = []string{
   545  	"https://history-futurenet.stellar.org/",
   546  }
   547  
   548  func CreateHistoryArchiveClient(archiveURLS []string) (historyarchive.ArchiveInterface, error) {
   549  	archiveOptions := historyarchive.ArchiveOptions{
   550  		ConnectOptions: storage.ConnectOptions{
   551  			UserAgent: "stellar-etl/1.0.0",
   552  		},
   553  	}
   554  	return historyarchive.NewArchivePool(archiveURLS, archiveOptions)
   555  }
   556  
   557  // GetLatestLedgerSequence returns the latest ledger sequence
   558  func GetLatestLedgerSequence(archiveURLs []string) (uint32, error) {
   559  	client, err := CreateHistoryArchiveClient(archiveURLs)
   560  	if err != nil {
   561  		return 0, err
   562  	}
   563  
   564  	root, err := client.GetRootHAS()
   565  	if err != nil {
   566  		return 0, err
   567  	}
   568  
   569  	return root.CurrentLedger, nil
   570  }
   571  
   572  // GetCheckpointNum gets the ledger sequence number of the checkpoint containing the provided ledger. If the checkpoint does not exist, an error is returned
   573  func GetCheckpointNum(seq, maxSeq uint32) (uint32, error) {
   574  	/*
   575  		Checkpoints are made "every 64 ledgers", when LCL is one-less-than a multiple
   576  		of 64. In other words, at LCL=63, 127, 191, 255, etc. or in other other words
   577  		checkpoint K covers the inclusive ledger range [K*64, ((K+1)*64)-1], and each
   578  		of those ranges should contain exactly 64 ledgers, with the exception of the
   579  		first checkpoint, which has only 63 ledgers: there is no ledger 0.
   580  	*/
   581  	remainder := (seq + 1) % 64
   582  	if remainder == 0 {
   583  		return seq, nil
   584  	}
   585  
   586  	checkpoint := seq + 64 - remainder
   587  	if checkpoint > maxSeq {
   588  		return 0, fmt.Errorf("the checkpoint ledger %d is greater than the max ledger number %d", checkpoint, maxSeq)
   589  	}
   590  
   591  	return checkpoint, nil
   592  }
   593  
   594  // ExtractLedgerCloseTime gets the close time of the provided ledger
   595  func ExtractLedgerCloseTime(ledger xdr.LedgerHeaderHistoryEntry) (time.Time, error) {
   596  	return TimePointToUTCTimeStamp(ledger.Header.ScpValue.CloseTime)
   597  }
   598  
   599  // ExtractEntryFromChange gets the most recent state of an entry from an ingestio change, as well as if the entry was deleted
   600  func ExtractEntryFromChange(change ingest.Change) (xdr.LedgerEntry, xdr.LedgerEntryChangeType, bool, error) {
   601  	switch changeType := change.LedgerEntryChangeType(); changeType {
   602  	case xdr.LedgerEntryChangeTypeLedgerEntryCreated, xdr.LedgerEntryChangeTypeLedgerEntryUpdated:
   603  		return *change.Post, changeType, false, nil
   604  	case xdr.LedgerEntryChangeTypeLedgerEntryRemoved:
   605  		return *change.Pre, changeType, true, nil
   606  	default:
   607  		return xdr.LedgerEntry{}, changeType, false, fmt.Errorf("unable to extract ledger entry type from change")
   608  	}
   609  }
   610  
   611  // GetMostRecentCheckpoint returns the most recent checkpoint before the provided ledger
   612  func GetMostRecentCheckpoint(seq uint32) uint32 {
   613  	remainder := (seq + 1) % 64
   614  	if remainder == 0 {
   615  		return seq
   616  	}
   617  	return seq - remainder
   618  }
   619  
   620  type EnvironmentDetails struct {
   621  	NetworkPassphrase string
   622  	ArchiveURLs       []string
   623  	BinaryPath        string
   624  	CoreConfig        string
   625  }
   626  
   627  // GetPassphrase returns the correct Network Passphrase based on env preference
   628  func GetEnvironmentDetails(isTest bool, isFuture bool) (details EnvironmentDetails) {
   629  	if isTest {
   630  		// testnet passphrase to be used for testing
   631  		details.NetworkPassphrase = network.TestNetworkPassphrase
   632  		details.ArchiveURLs = testArchiveURLs
   633  		details.BinaryPath = "/usr/bin/stellar-core"
   634  		details.CoreConfig = "docker/stellar-core_testnet.cfg"
   635  		return details
   636  	} else if isFuture {
   637  		// details.NetworkPassphrase = network.FutureNetworkPassphrase
   638  		details.NetworkPassphrase = "Test SDF Future Network ; October 2022"
   639  		details.ArchiveURLs = futureArchiveURLs
   640  		details.BinaryPath = "/usr/bin/stellar-core"
   641  		details.CoreConfig = "docker/stellar-core_futurenet.cfg"
   642  		return details
   643  	} else {
   644  		// default: mainnet
   645  		details.NetworkPassphrase = network.PublicNetworkPassphrase
   646  		details.ArchiveURLs = mainArchiveURLs
   647  		details.BinaryPath = "/usr/bin/stellar-core"
   648  		details.CoreConfig = "docker/stellar-core.cfg"
   649  		return details
   650  	}
   651  }
   652  
   653  type CaptiveCore interface {
   654  	CreateCaptiveCoreBackend() (ledgerbackend.CaptiveStellarCore, error)
   655  }
   656  
   657  func (e EnvironmentDetails) CreateCaptiveCoreBackend() (*ledgerbackend.CaptiveStellarCore, error) {
   658  	captiveCoreToml, err := ledgerbackend.NewCaptiveCoreTomlFromFile(
   659  		e.CoreConfig,
   660  		ledgerbackend.CaptiveCoreTomlParams{
   661  			NetworkPassphrase:  e.NetworkPassphrase,
   662  			HistoryArchiveURLs: e.ArchiveURLs,
   663  			Strict:             true,
   664  			UseDB:              false,
   665  		},
   666  	)
   667  	if err != nil {
   668  		return &ledgerbackend.CaptiveStellarCore{}, err
   669  	}
   670  	backend, err := ledgerbackend.NewCaptive(
   671  		ledgerbackend.CaptiveCoreConfig{
   672  			BinaryPath:         e.BinaryPath,
   673  			Toml:               captiveCoreToml,
   674  			NetworkPassphrase:  e.NetworkPassphrase,
   675  			HistoryArchiveURLs: e.ArchiveURLs,
   676  			UseDB:              false,
   677  			UserAgent:          "stellar-etl/1.0.0",
   678  		},
   679  	)
   680  	return backend, err
   681  }
   682  
   683  func (e EnvironmentDetails) GetUnboundedLedgerCloseMeta(end uint32) (xdr.LedgerCloseMeta, error) {
   684  	ctx := context.Background()
   685  
   686  	backend, err := e.CreateCaptiveCoreBackend()
   687  
   688  	ledgerRange := ledgerbackend.UnboundedRange(end)
   689  
   690  	err = backend.PrepareRange(ctx, ledgerRange)
   691  	if err != nil {
   692  		return xdr.LedgerCloseMeta{}, err
   693  	}
   694  
   695  	ledgerCloseMeta, err := backend.GetLedger(ctx, end)
   696  	if err != nil {
   697  		return xdr.LedgerCloseMeta{}, err
   698  	}
   699  
   700  	return ledgerCloseMeta, nil
   701  }
   702  
   703  func GetCloseTime(lcm xdr.LedgerCloseMeta) (time.Time, error) {
   704  	headerHistoryEntry := lcm.LedgerHeaderHistoryEntry()
   705  	return ExtractLedgerCloseTime(headerHistoryEntry)
   706  }
   707  
   708  func LedgerEntryToLedgerKeyHash(ledgerEntry xdr.LedgerEntry) string {
   709  	ledgerKey, _ := ledgerEntry.LedgerKey()
   710  	ledgerKeyByte, _ := ledgerKey.MarshalBinary()
   711  	hashedLedgerKeyByte := hash.Hash(ledgerKeyByte)
   712  	ledgerKeyHash := hex.EncodeToString(hashedLedgerKeyByte[:])
   713  
   714  	return ledgerKeyHash
   715  }