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 }