github.com/hashgraph/hedera-sdk-go/v2@v2.48.0/transaction_record.go (about) 1 package hedera 2 3 /*- 4 * 5 * Hedera Go SDK 6 * 7 * Copyright (C) 2020 - 2024 Hedera Hashgraph, LLC 8 * 9 * Licensed under the Apache License, Version 2.0 (the "License"); 10 * you may not use this file except in compliance with the License. 11 * You may obtain a copy of the License at 12 * 13 * http://www.apache.org/licenses/LICENSE-2.0 14 * 15 * Unless required by applicable law or agreed to in writing, software 16 * distributed under the License is distributed on an "AS IS" BASIS, 17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 * See the License for the specific language governing permissions and 19 * limitations under the License. 20 * 21 */ 22 23 import ( 24 "encoding/hex" 25 "fmt" 26 "sort" 27 "time" 28 29 jsoniter "github.com/json-iterator/go" 30 "google.golang.org/protobuf/types/known/wrapperspb" 31 32 protobuf "google.golang.org/protobuf/proto" 33 34 "github.com/hashgraph/hedera-protobufs-go/services" 35 ) 36 37 // The complete record for a transaction on Hedera that has reached consensus. 38 // This is not-free to request and is available for 1 hour after a transaction reaches consensus. 39 type TransactionRecord struct { 40 Receipt TransactionReceipt 41 TransactionHash []byte 42 ConsensusTimestamp time.Time 43 TransactionID TransactionID 44 TransactionMemo string 45 TransactionFee Hbar 46 Transfers []Transfer 47 TokenTransfers map[TokenID][]TokenTransfer 48 NftTransfers map[TokenID][]_TokenNftTransfer 49 ExpectedDecimals map[TokenID]uint32 50 CallResult *ContractFunctionResult 51 CallResultIsCreate bool 52 AssessedCustomFees []AssessedCustomFee 53 AutomaticTokenAssociations []TokenAssociation 54 ParentConsensusTimestamp time.Time 55 AliasKey *PublicKey 56 Duplicates []TransactionRecord 57 Children []TransactionRecord 58 // Deprecated 59 HbarAllowances []HbarAllowance 60 // Deprecated 61 TokenAllowances []TokenAllowance 62 // Deprecated 63 TokenNftAllowances []TokenNftAllowance 64 EthereumHash []byte 65 PaidStakingRewards map[AccountID]Hbar 66 PrngBytes []byte 67 PrngNumber *int32 68 EvmAddress []byte 69 PendingAirdropRecords []PendingAirdropRecord 70 } 71 72 // MarshalJSON returns the JSON representation of the TransactionRecord 73 func (record TransactionRecord) MarshalJSON() ([]byte, error) { 74 var json = jsoniter.ConfigCompatibleWithStandardLibrary 75 m := make(map[string]interface{}) 76 type TransferJSON struct { 77 AccountID string `json:"accountId"` 78 Amount interface{} `json:"amount"` 79 IsApproved bool `json:"isApproved"` 80 } 81 var transfersJSON []TransferJSON 82 for _, t := range record.Transfers { 83 transfersJSON = append(transfersJSON, TransferJSON{ 84 AccountID: t.AccountID.String(), 85 Amount: fmt.Sprint(t.Amount.AsTinybar()), 86 IsApproved: t.IsApproved, 87 }) 88 } 89 90 // It's weird because we need it without field names to match the specification 91 tokenTransfersMap := make(map[string]map[string]string) 92 93 for tokenId, transfers := range record.TokenTransfers { 94 accountIdMap := make(map[string]string) 95 for _, transfer := range transfers { 96 accountIdMap[transfer.AccountID.String()] = fmt.Sprint(transfer.Amount) 97 } 98 tokenTransfersMap[tokenId.String()] = accountIdMap 99 } 100 101 type TransferNftTokensJSON struct { 102 SenderAccountID string `json:"sender"` 103 ReceiverAccountIDAccountID string `json:"recipient"` 104 IsApproved bool `json:"isApproved"` 105 SerialNumber int64 `json:"serial"` 106 } 107 var transfersNftTokenJSON []TransferNftTokensJSON 108 tokenTransfersNftMap := make(map[string][]TransferNftTokensJSON) 109 for k, v := range record.NftTransfers { 110 for _, tt := range v { 111 transfersNftTokenJSON = append(transfersNftTokenJSON, TransferNftTokensJSON{ 112 SenderAccountID: tt.SenderAccountID.String(), 113 ReceiverAccountIDAccountID: tt.ReceiverAccountID.String(), 114 IsApproved: tt.IsApproved, 115 SerialNumber: tt.SerialNumber, 116 }) 117 } 118 tokenTransfersNftMap[k.String()] = transfersNftTokenJSON 119 } 120 121 m["transactionHash"] = hex.EncodeToString(record.TransactionHash) 122 m["transactionId"] = record.TransactionID.String() 123 m["transactionMemo"] = record.TransactionMemo 124 m["transactionFee"] = fmt.Sprint(record.TransactionFee.AsTinybar()) 125 m["transfers"] = transfersJSON 126 m["tokenTransfers"] = tokenTransfersMap 127 m["nftTransfers"] = tokenTransfersNftMap 128 m["expectedDecimals"] = record.ExpectedDecimals 129 m["callResultIsCreate"] = record.CallResultIsCreate 130 131 type AssessedCustomFeeJSON struct { 132 FeeCollectorAccountID string `json:"feeCollectorAccountId"` 133 TokenID string `json:"tokenId"` 134 Amount string `json:"amount"` 135 PayerAccountIDs []string `json:"payerAccountIds"` 136 } 137 138 customFeesJSON := make([]AssessedCustomFeeJSON, len(record.AssessedCustomFees)) 139 140 for i, fee := range record.AssessedCustomFees { 141 payerAccountIDsStr := make([]string, len(fee.PayerAccountIDs)) 142 for j, accID := range fee.PayerAccountIDs { 143 payerAccountIDsStr[j] = accID.String() 144 } 145 customFeesJSON[i] = AssessedCustomFeeJSON{ 146 FeeCollectorAccountID: fee.FeeCollectorAccountId.String(), 147 TokenID: fee.TokenID.String(), 148 Amount: fmt.Sprint(fee.Amount), 149 PayerAccountIDs: payerAccountIDsStr, 150 } 151 } 152 153 m["assessedCustomFees"] = customFeesJSON 154 155 type TokenAssociationOutput struct { 156 TokenID string `json:"tokenId"` 157 AccountID string `json:"accountId"` 158 } 159 automaticTokenAssociations := make([]TokenAssociationOutput, len(record.AutomaticTokenAssociations)) 160 for i, ta := range record.AutomaticTokenAssociations { 161 automaticTokenAssociations[i] = TokenAssociationOutput{ 162 TokenID: ta.TokenID.String(), 163 AccountID: ta.AccountID.String(), 164 } 165 } 166 m["automaticTokenAssociations"] = automaticTokenAssociations 167 168 consensusTime := record.ConsensusTimestamp.UTC().Format("2006-01-02T15:04:05.000Z") 169 parentConsesnusTime := record.ParentConsensusTimestamp.UTC().Format("2006-01-02T15:04:05.000Z") 170 m["consensusTimestamp"] = consensusTime 171 m["parentConsensusTimestamp"] = parentConsesnusTime 172 173 m["aliasKey"] = fmt.Sprint(record.AliasKey) 174 m["ethereumHash"] = hex.EncodeToString(record.EthereumHash) 175 type PaidStakingReward struct { 176 AccountID string `json:"accountId"` 177 Amount string `json:"amount"` 178 IsApproved bool `json:"isApproved"` 179 } 180 var paidStakingRewards []PaidStakingReward 181 for k, reward := range record.PaidStakingRewards { 182 paidStakingReward := PaidStakingReward{ 183 AccountID: k.String(), 184 Amount: fmt.Sprint(reward.AsTinybar()), 185 IsApproved: false, 186 } 187 paidStakingRewards = append(paidStakingRewards, paidStakingReward) 188 } 189 190 sort.Slice(paidStakingRewards, func(i, j int) bool { 191 return paidStakingRewards[i].AccountID < paidStakingRewards[j].AccountID 192 }) 193 194 m["paidStakingRewards"] = paidStakingRewards 195 196 m["prngBytes"] = hex.EncodeToString(record.PrngBytes) 197 m["prngNumber"] = record.PrngNumber 198 m["evmAddress"] = hex.EncodeToString(record.EvmAddress) 199 m["receipt"] = record.Receipt 200 m["children"] = record.Children 201 m["duplicates"] = record.Duplicates 202 203 type PendingAirdropIdOutput struct { 204 Sender string `json:"sender"` 205 Receiver string `json:"receiver"` 206 TokenID string `json:"tokenId"` 207 NftID string `json:"nftId"` 208 } 209 type PendingAirdropsOutput struct { 210 PendingAirdropId PendingAirdropIdOutput `json:"pendingAirdropId"` 211 PendingAirdropAmount string `json:"pendingAirdropAmount"` 212 } 213 pendingAirdropRecords := make([]PendingAirdropsOutput, len(record.PendingAirdropRecords)) 214 for i, p := range record.PendingAirdropRecords { 215 var tokenID string 216 var nftID string 217 var sender string 218 var receiver string 219 if p.pendingAirdropId.tokenID != nil { 220 tokenID = p.pendingAirdropId.tokenID.String() 221 } 222 if p.pendingAirdropId.nftID != nil { 223 nftID = p.pendingAirdropId.nftID.String() 224 } 225 if p.pendingAirdropId.sender != nil { 226 sender = p.pendingAirdropId.sender.String() 227 } 228 if p.pendingAirdropId.receiver != nil { 229 receiver = p.pendingAirdropId.receiver.String() 230 } 231 pendingAirdropRecords[i] = PendingAirdropsOutput{ 232 PendingAirdropId: PendingAirdropIdOutput{ 233 Sender: sender, 234 Receiver: receiver, 235 TokenID: tokenID, 236 NftID: nftID, 237 }, 238 PendingAirdropAmount: fmt.Sprint(p.pendingAirdropAmount), 239 } 240 } 241 m["pendingAirdropRecords"] = pendingAirdropRecords 242 243 receiptBytes, err := record.Receipt.MarshalJSON() 244 if err != nil { 245 return nil, err 246 } 247 var receiptInterface interface{} 248 err = json.Unmarshal(receiptBytes, &receiptInterface) 249 if err != nil { 250 return nil, err 251 } 252 m["receipt"] = receiptInterface 253 254 result, err := json.Marshal(m) 255 return result, err 256 } 257 258 // GetContractExecuteResult returns the ContractFunctionResult if the transaction was a contract call 259 func (record TransactionRecord) GetContractExecuteResult() (ContractFunctionResult, error) { 260 if record.CallResult == nil || record.CallResultIsCreate { 261 return ContractFunctionResult{}, fmt.Errorf("record does not contain a contract execute result") 262 } 263 264 return *record.CallResult, nil 265 } 266 267 // GetContractCreateResult returns the ContractFunctionResult if the transaction was a contract create 268 func (record TransactionRecord) GetContractCreateResult() (ContractFunctionResult, error) { 269 if record.CallResult == nil || !record.CallResultIsCreate { 270 return ContractFunctionResult{}, fmt.Errorf("record does not contain a contract create result") 271 } 272 273 return *record.CallResult, nil 274 } 275 276 func _TransactionRecordFromProtobuf(protoResponse *services.TransactionGetRecordResponse, txID *TransactionID) TransactionRecord { 277 if protoResponse == nil { 278 return TransactionRecord{} 279 } 280 pb := protoResponse.GetTransactionRecord() 281 if pb == nil { 282 return TransactionRecord{} 283 } 284 var accountTransfers = make([]Transfer, 0) 285 var tokenTransfers = make(map[TokenID][]TokenTransfer) 286 var nftTransfers = make(map[TokenID][]_TokenNftTransfer) 287 var expectedDecimals = make(map[TokenID]uint32) 288 289 if pb.TransferList != nil { 290 for _, element := range pb.TransferList.AccountAmounts { 291 accountTransfers = append(accountTransfers, _TransferFromProtobuf(element)) 292 } 293 } 294 295 for _, tokenTransfer := range pb.TokenTransferLists { 296 for _, nftTransfer := range tokenTransfer.NftTransfers { 297 if token := _TokenIDFromProtobuf(tokenTransfer.Token); token != nil { 298 nftTransfers[*token] = append(nftTransfers[*token], _NftTransferFromProtobuf(nftTransfer)) 299 } 300 } 301 302 for _, accountAmount := range tokenTransfer.Transfers { 303 if token := _TokenIDFromProtobuf(tokenTransfer.Token); token != nil { 304 tokenTransfers[*token] = append(tokenTransfers[*token], _TokenTransferFromProtobuf(accountAmount)) 305 } 306 } 307 308 if tokenTransfer.ExpectedDecimals != nil { 309 if token := _TokenIDFromProtobuf(tokenTransfer.Token); token != nil { 310 expectedDecimals[*token] = tokenTransfer.ExpectedDecimals.Value 311 } 312 } 313 } 314 315 assessedCustomFees := make([]AssessedCustomFee, 0) 316 for _, fee := range pb.AssessedCustomFees { 317 assessedCustomFees = append(assessedCustomFees, _AssessedCustomFeeFromProtobuf(fee)) 318 } 319 320 tokenAssociation := make([]TokenAssociation, 0) 321 for _, association := range pb.AutomaticTokenAssociations { 322 tokenAssociation = append(tokenAssociation, tokenAssociationFromProtobuf(association)) 323 } 324 325 paidStakingRewards := make(map[AccountID]Hbar) 326 for _, aa := range pb.PaidStakingRewards { 327 account := _AccountIDFromProtobuf(aa.AccountID) 328 if val, ok := paidStakingRewards[*account]; ok { 329 paidStakingRewards[*account] = HbarFromTinybar(val.tinybar + aa.Amount) 330 } 331 332 paidStakingRewards[*account] = HbarFromTinybar(aa.Amount) 333 } 334 335 var alias *PublicKey 336 if len(pb.Alias) != 0 { 337 pbKey := services.Key{} 338 _ = protobuf.Unmarshal(pb.Alias, &pbKey) 339 initialKey, _ := _KeyFromProtobuf(&pbKey) 340 switch t2 := initialKey.(type) { //nolint 341 case PublicKey: 342 alias = &t2 343 } 344 } 345 346 childReceipts := make([]TransactionRecord, 0) 347 if len(protoResponse.ChildTransactionRecords) > 0 { 348 for _, r := range protoResponse.ChildTransactionRecords { 349 childReceipts = append(childReceipts, _TransactionRecordFromProtobuf(&services.TransactionGetRecordResponse{TransactionRecord: r}, txID)) 350 } 351 } 352 353 duplicateReceipts := make([]TransactionRecord, 0) 354 if len(protoResponse.DuplicateTransactionRecords) > 0 { 355 for _, r := range protoResponse.DuplicateTransactionRecords { 356 duplicateReceipts = append(duplicateReceipts, _TransactionRecordFromProtobuf(&services.TransactionGetRecordResponse{TransactionRecord: r}, txID)) 357 } 358 } 359 360 var transactionID TransactionID 361 if pb.TransactionID != nil { 362 transactionID = _TransactionIDFromProtobuf(pb.TransactionID) 363 } 364 365 var pendingAirdropRecords []PendingAirdropRecord 366 for _, pendingAirdropRecord := range pb.NewPendingAirdrops { 367 pendingAirdropRecords = append(pendingAirdropRecords, _PendingAirdropRecordFromProtobuf(pendingAirdropRecord)) 368 } 369 370 txRecord := TransactionRecord{ 371 Receipt: _TransactionReceiptFromProtobuf(&services.TransactionGetReceiptResponse{Receipt: pb.GetReceipt()}, txID), 372 TransactionHash: pb.TransactionHash, 373 ConsensusTimestamp: _TimeFromProtobuf(pb.ConsensusTimestamp), 374 TransactionID: transactionID, 375 TransactionMemo: pb.Memo, 376 TransactionFee: HbarFromTinybar(int64(pb.TransactionFee)), 377 Transfers: accountTransfers, 378 TokenTransfers: tokenTransfers, 379 NftTransfers: nftTransfers, 380 CallResultIsCreate: true, 381 AssessedCustomFees: assessedCustomFees, 382 AutomaticTokenAssociations: tokenAssociation, 383 ParentConsensusTimestamp: _TimeFromProtobuf(pb.ParentConsensusTimestamp), 384 AliasKey: alias, 385 Duplicates: duplicateReceipts, 386 Children: childReceipts, 387 EthereumHash: pb.EthereumHash, 388 PaidStakingRewards: paidStakingRewards, 389 EvmAddress: pb.EvmAddress, 390 PendingAirdropRecords: pendingAirdropRecords, 391 } 392 393 if w, ok := pb.Entropy.(*services.TransactionRecord_PrngBytes); ok { 394 txRecord.PrngBytes = w.PrngBytes 395 } 396 397 if w, ok := pb.Entropy.(*services.TransactionRecord_PrngNumber); ok { 398 txRecord.PrngNumber = &w.PrngNumber 399 } 400 401 if pb.GetContractCreateResult() != nil { 402 result := _ContractFunctionResultFromProtobuf(pb.GetContractCreateResult()) 403 404 txRecord.CallResult = &result 405 } else if pb.GetContractCallResult() != nil { 406 result := _ContractFunctionResultFromProtobuf(pb.GetContractCallResult()) 407 408 txRecord.CallResult = &result 409 txRecord.CallResultIsCreate = false 410 } 411 412 return txRecord 413 } 414 415 func (record TransactionRecord) _ToProtobuf() (*services.TransactionGetRecordResponse, error) { 416 var amounts = make([]*services.AccountAmount, 0) 417 for _, amount := range record.Transfers { 418 amounts = append(amounts, &services.AccountAmount{ 419 AccountID: amount.AccountID._ToProtobuf(), 420 Amount: amount.Amount.tinybar, 421 }) 422 } 423 424 var transferList = services.TransferList{ 425 AccountAmounts: amounts, 426 } 427 428 var tokenTransfers = make([]*services.TokenTransferList, 0) 429 430 for tokenID, tokenTransfer := range record.TokenTransfers { 431 tokenTemp := make([]*services.AccountAmount, 0) 432 433 for _, accountAmount := range tokenTransfer { 434 tokenTemp = append(tokenTemp, accountAmount._ToProtobuf()) 435 } 436 437 bod := &services.TokenTransferList{ 438 Token: tokenID._ToProtobuf(), 439 Transfers: tokenTemp, 440 } 441 442 if decimal, ok := record.ExpectedDecimals[tokenID]; ok { 443 bod.ExpectedDecimals = &wrapperspb.UInt32Value{Value: decimal} 444 } 445 446 tokenTransfers = append(tokenTransfers, bod) 447 } 448 449 for tokenID, nftTransfers := range record.NftTransfers { 450 nftTemp := make([]*services.NftTransfer, 0) 451 452 for _, nftTransfer := range nftTransfers { 453 nftTemp = append(nftTemp, nftTransfer._ToProtobuf()) 454 } 455 456 tokenTransfers = append(tokenTransfers, &services.TokenTransferList{ 457 Token: tokenID._ToProtobuf(), 458 NftTransfers: nftTemp, 459 }) 460 } 461 462 assessedCustomFees := make([]*services.AssessedCustomFee, 0) 463 for _, fee := range record.AssessedCustomFees { 464 assessedCustomFees = append(assessedCustomFees, fee._ToProtobuf()) 465 } 466 467 tokenAssociation := make([]*services.TokenAssociation, 0) 468 for _, association := range record.AutomaticTokenAssociations { 469 tokenAssociation = append(tokenAssociation, association.toProtobuf()) 470 } 471 472 var alias []byte 473 if record.AliasKey != nil { 474 alias, _ = protobuf.Marshal(record.AliasKey._ToProtoKey()) 475 } 476 477 paidStakingRewards := make([]*services.AccountAmount, 0) 478 for account, hbar := range record.PaidStakingRewards { 479 paidStakingRewards = append(paidStakingRewards, &services.AccountAmount{ 480 AccountID: account._ToProtobuf(), 481 Amount: hbar.AsTinybar(), 482 }) 483 } 484 485 var tRecord = services.TransactionRecord{ 486 Receipt: record.Receipt._ToProtobuf().GetReceipt(), 487 TransactionHash: record.TransactionHash, 488 ConsensusTimestamp: &services.Timestamp{ 489 Seconds: int64(record.ConsensusTimestamp.Second()), 490 Nanos: int32(record.ConsensusTimestamp.Nanosecond()), 491 }, 492 TransactionID: record.TransactionID._ToProtobuf(), 493 Memo: record.TransactionMemo, 494 TransactionFee: uint64(record.TransactionFee.AsTinybar()), 495 TransferList: &transferList, 496 TokenTransferLists: tokenTransfers, 497 AssessedCustomFees: assessedCustomFees, 498 AutomaticTokenAssociations: tokenAssociation, 499 ParentConsensusTimestamp: &services.Timestamp{ 500 Seconds: int64(record.ParentConsensusTimestamp.Second()), 501 Nanos: int32(record.ParentConsensusTimestamp.Nanosecond()), 502 }, 503 Alias: alias, 504 EthereumHash: record.EthereumHash, 505 PaidStakingRewards: paidStakingRewards, 506 EvmAddress: record.EvmAddress, 507 } 508 509 if record.PrngNumber != nil { 510 tRecord.Entropy = &services.TransactionRecord_PrngNumber{PrngNumber: *record.PrngNumber} 511 } else if len(record.PrngBytes) > 0 { 512 tRecord.Entropy = &services.TransactionRecord_PrngBytes{PrngBytes: record.PrngBytes} 513 } 514 515 var err error 516 if record.CallResultIsCreate { 517 var choice, err = record.GetContractCreateResult() 518 519 if err != nil { 520 return nil, err 521 } 522 523 tRecord.Body = &services.TransactionRecord_ContractCreateResult{ 524 ContractCreateResult: choice._ToProtobuf(), 525 } 526 } else { 527 var choice, err = record.GetContractExecuteResult() 528 529 if err != nil { 530 return nil, err 531 } 532 533 tRecord.Body = &services.TransactionRecord_ContractCallResult{ 534 ContractCallResult: choice._ToProtobuf(), 535 } 536 } 537 538 childReceipts := make([]*services.TransactionRecord, 0) 539 if len(record.Children) > 0 { 540 for _, r := range record.Children { 541 temp, err := r._ToProtobuf() 542 if err != nil { 543 return nil, err 544 } 545 childReceipts = append(childReceipts, temp.GetTransactionRecord()) 546 } 547 } 548 549 duplicateReceipts := make([]*services.TransactionRecord, 0) 550 if len(record.Duplicates) > 0 { 551 for _, r := range record.Duplicates { 552 temp, err := r._ToProtobuf() 553 if err != nil { 554 return nil, err 555 } 556 duplicateReceipts = append(duplicateReceipts, temp.GetTransactionRecord()) 557 } 558 } 559 560 if record.PendingAirdropRecords != nil { 561 for _, pendingAirdropRecord := range record.PendingAirdropRecords { 562 tRecord.NewPendingAirdrops = append(tRecord.NewPendingAirdrops, pendingAirdropRecord._ToProtobuf()) 563 } 564 } 565 566 return &services.TransactionGetRecordResponse{ 567 TransactionRecord: &tRecord, 568 ChildTransactionRecords: childReceipts, 569 DuplicateTransactionRecords: duplicateReceipts, 570 }, err 571 } 572 573 // Validate checks that the receipt status is Success 574 func (record TransactionRecord) ValidateReceiptStatus(shouldValidate bool) error { 575 return record.Receipt.ValidateStatus(shouldValidate) 576 } 577 578 // ToBytes returns the serialized bytes of a TransactionRecord 579 func (record TransactionRecord) ToBytes() []byte { 580 rec, err := record._ToProtobuf() 581 if err != nil { 582 return make([]byte, 0) 583 } 584 data, err := protobuf.Marshal(rec) 585 if err != nil { 586 return make([]byte, 0) 587 } 588 589 return data 590 } 591 592 // TransactionRecordFromBytes returns a TransactionRecord from a raw protobuf byte array 593 func TransactionRecordFromBytes(data []byte) (TransactionRecord, error) { 594 if data == nil { 595 return TransactionRecord{}, errByteArrayNull 596 } 597 pb := services.TransactionGetRecordResponse{} 598 err := protobuf.Unmarshal(data, &pb) 599 if err != nil { 600 return TransactionRecord{}, err 601 } 602 603 return _TransactionRecordFromProtobuf(&pb, nil), nil 604 }