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

     1  package transform
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"strconv"
     8  
     9  	"github.com/guregu/null"
    10  	"github.com/lib/pq"
    11  	"github.com/stellar/stellar-etl/internal/toid"
    12  	"github.com/stellar/stellar-etl/internal/utils"
    13  
    14  	"github.com/stellar/go/ingest"
    15  	"github.com/stellar/go/xdr"
    16  )
    17  
    18  // TransformTransaction converts a transaction from the history archive ingestion system into a form suitable for BigQuery
    19  func TransformTransaction(transaction ingest.LedgerTransaction, lhe xdr.LedgerHeaderHistoryEntry) (TransactionOutput, error) {
    20  	ledgerHeader := lhe.Header
    21  	outputTransactionHash := utils.HashToHexString(transaction.Result.TransactionHash)
    22  	outputLedgerSequence := uint32(ledgerHeader.LedgerSeq)
    23  
    24  	transactionIndex := uint32(transaction.Index)
    25  
    26  	outputTransactionID := toid.New(int32(outputLedgerSequence), int32(transactionIndex), 0).ToInt64()
    27  
    28  	sourceAccount := transaction.Envelope.SourceAccount()
    29  	outputAccount, err := utils.GetAccountAddressFromMuxedAccount(transaction.Envelope.SourceAccount())
    30  	if err != nil {
    31  		return TransactionOutput{}, fmt.Errorf("for ledger %d; transaction %d (transaction id=%d): %v", outputLedgerSequence, transactionIndex, outputTransactionID, err)
    32  	}
    33  
    34  	outputAccountSequence := transaction.Envelope.SeqNum()
    35  	if outputAccountSequence < 0 {
    36  		return TransactionOutput{}, fmt.Errorf("The account's sequence number (%d) is negative for ledger %d; transaction %d (transaction id=%d)", outputAccountSequence, outputLedgerSequence, transactionIndex, outputTransactionID)
    37  	}
    38  
    39  	outputMaxFee := transaction.Envelope.Fee()
    40  	if outputMaxFee < 0 {
    41  		return TransactionOutput{}, fmt.Errorf("The fee (%d) is negative for ledger %d; transaction %d (transaction id=%d)", outputMaxFee, outputLedgerSequence, transactionIndex, outputTransactionID)
    42  	}
    43  
    44  	outputFeeCharged := int64(transaction.Result.Result.FeeCharged)
    45  	if outputFeeCharged < 0 {
    46  		return TransactionOutput{}, fmt.Errorf("The fee charged (%d) is negative for ledger %d; transaction %d (transaction id=%d)", outputFeeCharged, outputLedgerSequence, transactionIndex, outputTransactionID)
    47  	}
    48  
    49  	outputOperationCount := int32(len(transaction.Envelope.Operations()))
    50  
    51  	outputTxEnvelope, err := xdr.MarshalBase64(transaction.Envelope)
    52  	if err != nil {
    53  		return TransactionOutput{}, err
    54  	}
    55  
    56  	outputTxResult, err := xdr.MarshalBase64(&transaction.Result.Result)
    57  	if err != nil {
    58  		return TransactionOutput{}, err
    59  	}
    60  
    61  	outputTxMeta, err := xdr.MarshalBase64(transaction.UnsafeMeta)
    62  	if err != nil {
    63  		return TransactionOutput{}, err
    64  	}
    65  
    66  	outputTxFeeMeta, err := xdr.MarshalBase64(transaction.FeeChanges)
    67  	if err != nil {
    68  		return TransactionOutput{}, err
    69  	}
    70  
    71  	outputCreatedAt, err := utils.TimePointToUTCTimeStamp(ledgerHeader.ScpValue.CloseTime)
    72  	if err != nil {
    73  		return TransactionOutput{}, fmt.Errorf("for ledger %d; transaction %d (transaction id=%d): %v", outputLedgerSequence, transactionIndex, outputTransactionID, err)
    74  	}
    75  
    76  	memoObject := transaction.Envelope.Memo()
    77  	outputMemoContents := ""
    78  	switch xdr.MemoType(memoObject.Type) {
    79  	case xdr.MemoTypeMemoText:
    80  		outputMemoContents = memoObject.MustText()
    81  	case xdr.MemoTypeMemoId:
    82  		outputMemoContents = strconv.FormatUint(uint64(memoObject.MustId()), 10)
    83  	case xdr.MemoTypeMemoHash:
    84  		hash := memoObject.MustHash()
    85  		outputMemoContents = base64.StdEncoding.EncodeToString(hash[:])
    86  	case xdr.MemoTypeMemoReturn:
    87  		hash := memoObject.MustRetHash()
    88  		outputMemoContents = base64.StdEncoding.EncodeToString(hash[:])
    89  	}
    90  
    91  	outputMemoType := memoObject.Type.String()
    92  	timeBound := transaction.Envelope.TimeBounds()
    93  	outputTimeBounds := ""
    94  	if timeBound != nil {
    95  		if timeBound.MaxTime < timeBound.MinTime && timeBound.MaxTime != 0 {
    96  
    97  			return TransactionOutput{}, fmt.Errorf("The max time is earlier than the min time (%d < %d) for ledger %d; transaction %d (transaction id=%d)",
    98  				timeBound.MaxTime, timeBound.MinTime, outputLedgerSequence, transactionIndex, outputTransactionID)
    99  		}
   100  
   101  		if timeBound.MaxTime == 0 {
   102  			outputTimeBounds = fmt.Sprintf("[%d,)", timeBound.MinTime)
   103  		} else {
   104  			outputTimeBounds = fmt.Sprintf("[%d,%d)", timeBound.MinTime, timeBound.MaxTime)
   105  		}
   106  
   107  	}
   108  
   109  	ledgerBound := transaction.Envelope.LedgerBounds()
   110  	outputLedgerBound := ""
   111  	if ledgerBound != nil {
   112  		outputLedgerBound = fmt.Sprintf("[%d,%d)", int64(ledgerBound.MinLedger), int64(ledgerBound.MaxLedger))
   113  	}
   114  
   115  	minSequenceNumber := transaction.Envelope.MinSeqNum()
   116  	outputMinSequence := null.Int{}
   117  	if minSequenceNumber != nil {
   118  		outputMinSequence = null.IntFrom(int64(*minSequenceNumber))
   119  	}
   120  
   121  	minSequenceAge := transaction.Envelope.MinSeqAge()
   122  	outputMinSequenceAge := null.Int{}
   123  	if minSequenceAge != nil {
   124  		outputMinSequenceAge = null.IntFrom(int64(*minSequenceAge))
   125  	}
   126  
   127  	minSequenceLedgerGap := transaction.Envelope.MinSeqLedgerGap()
   128  	outputMinSequenceLedgerGap := null.Int{}
   129  	if minSequenceLedgerGap != nil {
   130  		outputMinSequenceLedgerGap = null.IntFrom(int64(*minSequenceLedgerGap))
   131  	}
   132  
   133  	// Soroban fees and resources
   134  	// Note: MaxFee and FeeCharged is the sum of base transaction fees + Soroban fees
   135  	// Breakdown of Soroban fees can be calculated by the config_setting resource pricing * the resources used
   136  
   137  	var sorobanData xdr.SorobanTransactionData
   138  	var hasSorobanData bool
   139  	var outputResourceFee int64
   140  	var outputSorobanResourcesInstructions uint32
   141  	var outputSorobanResourcesReadBytes uint32
   142  	var outputSorobanResourcesWriteBytes uint32
   143  	var outputInclusionFeeBid int64
   144  	var outputInclusionFeeCharged int64
   145  	var outputResourceFeeRefund int64
   146  
   147  	// Soroban data can exist in V1 and FeeBump transactionEnvelopes
   148  	switch transaction.Envelope.Type {
   149  	case xdr.EnvelopeTypeEnvelopeTypeTx:
   150  		sorobanData, hasSorobanData = transaction.Envelope.V1.Tx.Ext.GetSorobanData()
   151  	case xdr.EnvelopeTypeEnvelopeTypeTxFeeBump:
   152  		sorobanData, hasSorobanData = transaction.Envelope.FeeBump.Tx.InnerTx.V1.Tx.Ext.GetSorobanData()
   153  	}
   154  
   155  	if hasSorobanData {
   156  		outputResourceFee = int64(sorobanData.ResourceFee)
   157  		outputSorobanResourcesInstructions = uint32(sorobanData.Resources.Instructions)
   158  		outputSorobanResourcesReadBytes = uint32(sorobanData.Resources.ReadBytes)
   159  		outputSorobanResourcesWriteBytes = uint32(sorobanData.Resources.WriteBytes)
   160  		outputInclusionFeeBid = int64(transaction.Envelope.Fee()) - outputResourceFee
   161  
   162  		accountBalanceStart, accountBalanceEnd := getAccountBalanceFromLedgerEntryChanges(transaction.FeeChanges, sourceAccount.Address())
   163  		initialFeeCharged := accountBalanceStart - accountBalanceEnd
   164  		outputInclusionFeeCharged = initialFeeCharged - outputResourceFee
   165  
   166  		meta, ok := transaction.UnsafeMeta.GetV3()
   167  		if ok {
   168  			accountBalanceStart, accountBalanceEnd := getAccountBalanceFromLedgerEntryChanges(meta.TxChangesAfter, sourceAccount.Address())
   169  			outputResourceFeeRefund = accountBalanceEnd - accountBalanceStart
   170  		}
   171  
   172  		// TODO: FeeCharged is calculated incorrectly in protocol 20. Remove when protocol is updated and the bug is fixed
   173  		outputFeeCharged = outputFeeCharged - outputResourceFeeRefund
   174  	}
   175  
   176  	outputCloseTime, err := utils.TimePointToUTCTimeStamp(ledgerHeader.ScpValue.CloseTime)
   177  	if err != nil {
   178  		return TransactionOutput{}, fmt.Errorf("for ledger %d; transaction %d (transaction id=%d): %v", outputLedgerSequence, transactionIndex, outputTransactionID, err)
   179  	}
   180  
   181  	outputTxResultCode := transaction.Result.Result.Result.Code.String()
   182  
   183  	outputSuccessful := transaction.Result.Successful()
   184  	transformedTransaction := TransactionOutput{
   185  		TransactionHash:              outputTransactionHash,
   186  		LedgerSequence:               outputLedgerSequence,
   187  		TransactionID:                outputTransactionID,
   188  		Account:                      outputAccount,
   189  		AccountSequence:              outputAccountSequence,
   190  		MaxFee:                       outputMaxFee,
   191  		FeeCharged:                   outputFeeCharged,
   192  		OperationCount:               outputOperationCount,
   193  		TxEnvelope:                   outputTxEnvelope,
   194  		TxResult:                     outputTxResult,
   195  		TxMeta:                       outputTxMeta,
   196  		TxFeeMeta:                    outputTxFeeMeta,
   197  		CreatedAt:                    outputCreatedAt,
   198  		MemoType:                     outputMemoType,
   199  		Memo:                         outputMemoContents,
   200  		TimeBounds:                   outputTimeBounds,
   201  		Successful:                   outputSuccessful,
   202  		LedgerBounds:                 outputLedgerBound,
   203  		MinAccountSequence:           outputMinSequence,
   204  		MinAccountSequenceAge:        outputMinSequenceAge,
   205  		MinAccountSequenceLedgerGap:  outputMinSequenceLedgerGap,
   206  		ExtraSigners:                 formatSigners(transaction.Envelope.ExtraSigners()),
   207  		ClosedAt:                     outputCloseTime,
   208  		ResourceFee:                  outputResourceFee,
   209  		SorobanResourcesInstructions: outputSorobanResourcesInstructions,
   210  		SorobanResourcesReadBytes:    outputSorobanResourcesReadBytes,
   211  		SorobanResourcesWriteBytes:   outputSorobanResourcesWriteBytes,
   212  		TransactionResultCode:        outputTxResultCode,
   213  		InclusionFeeBid:              outputInclusionFeeBid,
   214  		InclusionFeeCharged:          outputInclusionFeeCharged,
   215  		ResourceFeeRefund:            outputResourceFeeRefund,
   216  	}
   217  
   218  	// Add Muxed Account Details, if exists
   219  	if sourceAccount.Type == xdr.CryptoKeyTypeKeyTypeMuxedEd25519 {
   220  		muxedAddress, err := sourceAccount.GetAddress()
   221  		if err != nil {
   222  			return TransactionOutput{}, err
   223  		}
   224  		transformedTransaction.AccountMuxed = muxedAddress
   225  
   226  	}
   227  
   228  	// Add Fee Bump Details, if exists
   229  	if transaction.Envelope.IsFeeBump() {
   230  		feeBumpAccount := transaction.Envelope.FeeBumpAccount()
   231  		feeAccount := feeBumpAccount.ToAccountId()
   232  		if sourceAccount.Type == xdr.CryptoKeyTypeKeyTypeMuxedEd25519 {
   233  			feeAccountMuxed := feeAccount.Address()
   234  			transformedTransaction.FeeAccountMuxed = feeAccountMuxed
   235  		}
   236  		transformedTransaction.FeeAccount = feeAccount.Address()
   237  		innerHash := transaction.Result.InnerHash()
   238  		transformedTransaction.InnerTransactionHash = hex.EncodeToString(innerHash[:])
   239  		transformedTransaction.NewMaxFee = uint32(transaction.Envelope.FeeBumpFee())
   240  	}
   241  
   242  	return transformedTransaction, nil
   243  }
   244  
   245  func getAccountBalanceFromLedgerEntryChanges(changes xdr.LedgerEntryChanges, sourceAccountAddress string) (int64, int64) {
   246  	var accountBalanceStart int64
   247  	var accountBalanceEnd int64
   248  
   249  	for _, change := range changes {
   250  		switch change.Type {
   251  		case xdr.LedgerEntryChangeTypeLedgerEntryUpdated:
   252  			accountEntry, ok := change.Updated.Data.GetAccount()
   253  			if !ok {
   254  				continue
   255  			}
   256  
   257  			if accountEntry.AccountId.Address() == sourceAccountAddress {
   258  				accountBalanceEnd = int64(accountEntry.Balance)
   259  			}
   260  		case xdr.LedgerEntryChangeTypeLedgerEntryState:
   261  			accountEntry, ok := change.State.Data.GetAccount()
   262  			if !ok {
   263  				continue
   264  			}
   265  
   266  			if accountEntry.AccountId.Address() == sourceAccountAddress {
   267  				accountBalanceStart = int64(accountEntry.Balance)
   268  			}
   269  		}
   270  	}
   271  
   272  	return accountBalanceStart, accountBalanceEnd
   273  }
   274  
   275  func formatSigners(s []xdr.SignerKey) pq.StringArray {
   276  	if s == nil {
   277  		return nil
   278  	}
   279  
   280  	signers := make([]string, len(s))
   281  	for i, key := range s {
   282  		signers[i] = key.Address()
   283  	}
   284  
   285  	return signers
   286  }