github.com/stellar/stellar-etl@v1.0.1-0.20240312145900-4874b6bf2b89/internal/transform/operation.go (about) 1 package transform 2 3 import ( 4 "encoding/base64" 5 "fmt" 6 "strconv" 7 "time" 8 9 "github.com/guregu/null" 10 "github.com/pkg/errors" 11 "github.com/stellar/stellar-etl/internal/toid" 12 "github.com/stellar/stellar-etl/internal/utils" 13 14 "github.com/stellar/go/amount" 15 "github.com/stellar/go/ingest" 16 "github.com/stellar/go/protocols/horizon/base" 17 "github.com/stellar/go/strkey" 18 "github.com/stellar/go/xdr" 19 20 "github.com/stellar/go/support/contractevents" 21 ) 22 23 type liquidityPoolDelta struct { 24 ReserveA xdr.Int64 25 ReserveB xdr.Int64 26 TotalPoolShares xdr.Int64 27 } 28 29 // TransformOperation converts an operation from the history archive ingestion system into a form suitable for BigQuery 30 func TransformOperation(operation xdr.Operation, operationIndex int32, transaction ingest.LedgerTransaction, ledgerSeq int32, ledgerCloseMeta xdr.LedgerCloseMeta, network string) (OperationOutput, error) { 31 outputTransactionID := toid.New(ledgerSeq, int32(transaction.Index), 0).ToInt64() 32 outputOperationID := toid.New(ledgerSeq, int32(transaction.Index), operationIndex+1).ToInt64() //operationIndex needs +1 increment to stay in sync with ingest package 33 34 sourceAccount := getOperationSourceAccount(operation, transaction) 35 outputSourceAccount, err := utils.GetAccountAddressFromMuxedAccount(sourceAccount) 36 if err != nil { 37 return OperationOutput{}, fmt.Errorf("for operation %d (ledger id=%d): %v", operationIndex, outputOperationID, err) 38 } 39 40 var outputSourceAccountMuxed null.String 41 if sourceAccount.Type == xdr.CryptoKeyTypeKeyTypeMuxedEd25519 { 42 muxedAddress, err := sourceAccount.GetAddress() 43 if err != nil { 44 return OperationOutput{}, err 45 } 46 outputSourceAccountMuxed = null.StringFrom(muxedAddress) 47 } 48 49 outputOperationType := int32(operation.Body.Type) 50 if outputOperationType < 0 { 51 return OperationOutput{}, fmt.Errorf("The operation type (%d) is negative for operation %d (operation id=%d)", outputOperationType, operationIndex, outputOperationID) 52 } 53 54 outputDetails, err := extractOperationDetails(operation, transaction, operationIndex, network) 55 if err != nil { 56 return OperationOutput{}, err 57 } 58 59 outputOperationTypeString, err := mapOperationType(operation) 60 if err != nil { 61 return OperationOutput{}, err 62 } 63 64 outputCloseTime, err := utils.GetCloseTime(ledgerCloseMeta) 65 if err != nil { 66 return OperationOutput{}, err 67 } 68 69 outputOperationResults, ok := transaction.Result.Result.OperationResults() 70 if !ok { 71 return OperationOutput{}, err 72 } 73 74 outputOperationResultCode := outputOperationResults[operationIndex].Code.String() 75 var outputOperationTraceCode string 76 operationResultTr, ok := outputOperationResults[operationIndex].GetTr() 77 if ok { 78 outputOperationTraceCode, err = mapOperationTrace(operationResultTr) 79 if err != nil { 80 return OperationOutput{}, err 81 } 82 } 83 84 transformedOperation := OperationOutput{ 85 SourceAccount: outputSourceAccount, 86 SourceAccountMuxed: outputSourceAccountMuxed.String, 87 Type: outputOperationType, 88 TypeString: outputOperationTypeString, 89 TransactionID: outputTransactionID, 90 OperationID: outputOperationID, 91 OperationDetails: outputDetails, 92 ClosedAt: outputCloseTime, 93 OperationResultCode: outputOperationResultCode, 94 OperationTraceCode: outputOperationTraceCode, 95 } 96 97 return transformedOperation, nil 98 } 99 100 func mapOperationType(operation xdr.Operation) (string, error) { 101 var op_string_type string 102 operationType := operation.Body.Type 103 104 switch operationType { 105 case xdr.OperationTypeCreateAccount: 106 op_string_type = "create_account" 107 case xdr.OperationTypePayment: 108 op_string_type = "payment" 109 case xdr.OperationTypePathPaymentStrictReceive: 110 op_string_type = "path_payment_strict_receive" 111 case xdr.OperationTypePathPaymentStrictSend: 112 op_string_type = "path_payment_strict_send" 113 case xdr.OperationTypeManageBuyOffer: 114 op_string_type = "manage_buy_offer" 115 case xdr.OperationTypeManageSellOffer: 116 op_string_type = "manage_sell_offer" 117 case xdr.OperationTypeCreatePassiveSellOffer: 118 op_string_type = "create_passive_sell_offer" 119 case xdr.OperationTypeSetOptions: 120 op_string_type = "set_options" 121 case xdr.OperationTypeChangeTrust: 122 op_string_type = "change_trust" 123 case xdr.OperationTypeAllowTrust: 124 op_string_type = "allow_trust" 125 case xdr.OperationTypeAccountMerge: 126 op_string_type = "account_merge" 127 case xdr.OperationTypeInflation: 128 op_string_type = "inflation" 129 case xdr.OperationTypeManageData: 130 op_string_type = "manage_data" 131 case xdr.OperationTypeBumpSequence: 132 op_string_type = "bump_sequence" 133 case xdr.OperationTypeCreateClaimableBalance: 134 op_string_type = "create_claimable_balance" 135 case xdr.OperationTypeClaimClaimableBalance: 136 op_string_type = "claim_claimable_balance" 137 case xdr.OperationTypeBeginSponsoringFutureReserves: 138 op_string_type = "begin_sponsoring_future_reserves" 139 case xdr.OperationTypeEndSponsoringFutureReserves: 140 op_string_type = "end_sponsoring_future_reserves" 141 case xdr.OperationTypeRevokeSponsorship: 142 op_string_type = "revoke_sponsorship" 143 case xdr.OperationTypeClawback: 144 op_string_type = "clawback" 145 case xdr.OperationTypeClawbackClaimableBalance: 146 op_string_type = "clawback_claimable_balance" 147 case xdr.OperationTypeSetTrustLineFlags: 148 op_string_type = "set_trust_line_flags" 149 case xdr.OperationTypeLiquidityPoolDeposit: 150 op_string_type = "liquidity_pool_deposit" 151 case xdr.OperationTypeLiquidityPoolWithdraw: 152 op_string_type = "liquidity_pool_withdraw" 153 case xdr.OperationTypeInvokeHostFunction: 154 op_string_type = "invoke_host_function" 155 case xdr.OperationTypeExtendFootprintTtl: 156 op_string_type = "extend_footprint_ttl" 157 case xdr.OperationTypeRestoreFootprint: 158 op_string_type = "restore_footprint" 159 default: 160 return op_string_type, fmt.Errorf("Unknown operation type: %s", operation.Body.Type.String()) 161 } 162 return op_string_type, nil 163 } 164 165 func mapOperationTrace(operationTrace xdr.OperationResultTr) (string, error) { 166 var operationTraceDescription string 167 operationType := operationTrace.Type 168 169 switch operationType { 170 case xdr.OperationTypeCreateAccount: 171 operationTraceDescription = operationTrace.CreateAccountResult.Code.String() 172 case xdr.OperationTypePayment: 173 operationTraceDescription = operationTrace.PaymentResult.Code.String() 174 case xdr.OperationTypePathPaymentStrictReceive: 175 operationTraceDescription = operationTrace.PathPaymentStrictReceiveResult.Code.String() 176 case xdr.OperationTypePathPaymentStrictSend: 177 operationTraceDescription = operationTrace.PathPaymentStrictSendResult.Code.String() 178 case xdr.OperationTypeManageBuyOffer: 179 operationTraceDescription = operationTrace.ManageBuyOfferResult.Code.String() 180 case xdr.OperationTypeManageSellOffer: 181 operationTraceDescription = operationTrace.ManageSellOfferResult.Code.String() 182 case xdr.OperationTypeCreatePassiveSellOffer: 183 operationTraceDescription = operationTrace.CreatePassiveSellOfferResult.Code.String() 184 case xdr.OperationTypeSetOptions: 185 operationTraceDescription = operationTrace.SetOptionsResult.Code.String() 186 case xdr.OperationTypeChangeTrust: 187 operationTraceDescription = operationTrace.ChangeTrustResult.Code.String() 188 case xdr.OperationTypeAllowTrust: 189 operationTraceDescription = operationTrace.AllowTrustResult.Code.String() 190 case xdr.OperationTypeAccountMerge: 191 operationTraceDescription = operationTrace.AccountMergeResult.Code.String() 192 case xdr.OperationTypeInflation: 193 operationTraceDescription = operationTrace.InflationResult.Code.String() 194 case xdr.OperationTypeManageData: 195 operationTraceDescription = operationTrace.ManageDataResult.Code.String() 196 case xdr.OperationTypeBumpSequence: 197 operationTraceDescription = operationTrace.BumpSeqResult.Code.String() 198 case xdr.OperationTypeCreateClaimableBalance: 199 operationTraceDescription = operationTrace.CreateClaimableBalanceResult.Code.String() 200 case xdr.OperationTypeClaimClaimableBalance: 201 operationTraceDescription = operationTrace.ClaimClaimableBalanceResult.Code.String() 202 case xdr.OperationTypeBeginSponsoringFutureReserves: 203 operationTraceDescription = operationTrace.BeginSponsoringFutureReservesResult.Code.String() 204 case xdr.OperationTypeEndSponsoringFutureReserves: 205 operationTraceDescription = operationTrace.EndSponsoringFutureReservesResult.Code.String() 206 case xdr.OperationTypeRevokeSponsorship: 207 operationTraceDescription = operationTrace.RevokeSponsorshipResult.Code.String() 208 case xdr.OperationTypeClawback: 209 operationTraceDescription = operationTrace.ClawbackResult.Code.String() 210 case xdr.OperationTypeClawbackClaimableBalance: 211 operationTraceDescription = operationTrace.ClawbackClaimableBalanceResult.Code.String() 212 case xdr.OperationTypeSetTrustLineFlags: 213 operationTraceDescription = operationTrace.SetTrustLineFlagsResult.Code.String() 214 case xdr.OperationTypeLiquidityPoolDeposit: 215 operationTraceDescription = operationTrace.LiquidityPoolDepositResult.Code.String() 216 case xdr.OperationTypeLiquidityPoolWithdraw: 217 operationTraceDescription = operationTrace.LiquidityPoolWithdrawResult.Code.String() 218 case xdr.OperationTypeInvokeHostFunction: 219 operationTraceDescription = operationTrace.InvokeHostFunctionResult.Code.String() 220 case xdr.OperationTypeExtendFootprintTtl: 221 operationTraceDescription = operationTrace.ExtendFootprintTtlResult.Code.String() 222 case xdr.OperationTypeRestoreFootprint: 223 operationTraceDescription = operationTrace.RestoreFootprintResult.Code.String() 224 default: 225 return operationTraceDescription, fmt.Errorf("Unknown operation type: %s", operationTrace.Type.String()) 226 } 227 return operationTraceDescription, nil 228 } 229 230 func PoolIDToString(id xdr.PoolId) string { 231 return xdr.Hash(id).HexString() 232 } 233 234 // operation xdr.Operation, operationIndex int32, transaction ingest.LedgerTransaction, ledgerSeq int32 235 func getLiquidityPoolAndProductDelta(operationIndex int32, transaction ingest.LedgerTransaction, lpID *xdr.PoolId) (*xdr.LiquidityPoolEntry, *liquidityPoolDelta, error) { 236 changes, err := transaction.GetOperationChanges(uint32(operationIndex)) 237 if err != nil { 238 return nil, nil, err 239 } 240 241 for _, c := range changes { 242 if c.Type != xdr.LedgerEntryTypeLiquidityPool { 243 continue 244 } 245 // The delta can be caused by a full removal or full creation of the liquidity pool 246 var lp *xdr.LiquidityPoolEntry 247 var preA, preB, preShares xdr.Int64 248 if c.Pre != nil { 249 if lpID != nil && c.Pre.Data.LiquidityPool.LiquidityPoolId != *lpID { 250 // if we were looking for specific pool id, then check on it 251 continue 252 } 253 lp = c.Pre.Data.LiquidityPool 254 if c.Pre.Data.LiquidityPool.Body.Type != xdr.LiquidityPoolTypeLiquidityPoolConstantProduct { 255 return nil, nil, fmt.Errorf("unexpected liquity pool body type %d", c.Pre.Data.LiquidityPool.Body.Type) 256 } 257 cpPre := c.Pre.Data.LiquidityPool.Body.ConstantProduct 258 preA, preB, preShares = cpPre.ReserveA, cpPre.ReserveB, cpPre.TotalPoolShares 259 } 260 var postA, postB, postShares xdr.Int64 261 if c.Post != nil { 262 if lpID != nil && c.Post.Data.LiquidityPool.LiquidityPoolId != *lpID { 263 // if we were looking for specific pool id, then check on it 264 continue 265 } 266 lp = c.Post.Data.LiquidityPool 267 if c.Post.Data.LiquidityPool.Body.Type != xdr.LiquidityPoolTypeLiquidityPoolConstantProduct { 268 return nil, nil, fmt.Errorf("unexpected liquity pool body type %d", c.Post.Data.LiquidityPool.Body.Type) 269 } 270 cpPost := c.Post.Data.LiquidityPool.Body.ConstantProduct 271 postA, postB, postShares = cpPost.ReserveA, cpPost.ReserveB, cpPost.TotalPoolShares 272 } 273 delta := &liquidityPoolDelta{ 274 ReserveA: postA - preA, 275 ReserveB: postB - preB, 276 TotalPoolShares: postShares - preShares, 277 } 278 return lp, delta, nil 279 } 280 281 return nil, nil, fmt.Errorf("Liquidity pool change not found") 282 } 283 284 func getOperationSourceAccount(operation xdr.Operation, transaction ingest.LedgerTransaction) xdr.MuxedAccount { 285 sourceAccount := operation.SourceAccount 286 if sourceAccount != nil { 287 return *sourceAccount 288 } 289 290 return transaction.Envelope.SourceAccount() 291 } 292 293 func getSponsor(operation xdr.Operation, transaction ingest.LedgerTransaction, operationIndex int32) (*xdr.AccountId, error) { 294 changes, err := transaction.GetOperationChanges(uint32(operationIndex)) 295 if err != nil { 296 return nil, err 297 } 298 var signerKey string 299 if setOps, ok := operation.Body.GetSetOptionsOp(); ok && setOps.Signer != nil { 300 signerKey = setOps.Signer.Key.Address() 301 } 302 303 for _, c := range changes { 304 // Check Signer changes 305 if signerKey != "" { 306 if sponsorAccount := getSignerSponsorInChange(signerKey, c); sponsorAccount != nil { 307 return sponsorAccount, nil 308 } 309 } 310 311 // Check Ledger key changes 312 if c.Pre != nil || c.Post == nil { 313 // We are only looking for entry creations denoting that a sponsor 314 // is associated to the ledger entry of the operation. 315 continue 316 } 317 if sponsorAccount := c.Post.SponsoringID(); sponsorAccount != nil { 318 return sponsorAccount, nil 319 } 320 } 321 322 return nil, nil 323 } 324 325 func getSignerSponsorInChange(signerKey string, change ingest.Change) xdr.SponsorshipDescriptor { 326 if change.Type != xdr.LedgerEntryTypeAccount || change.Post == nil { 327 return nil 328 } 329 330 preSigners := map[string]xdr.AccountId{} 331 if change.Pre != nil { 332 account := change.Pre.Data.MustAccount() 333 preSigners = account.SponsorPerSigner() 334 } 335 336 account := change.Post.Data.MustAccount() 337 postSigners := account.SponsorPerSigner() 338 339 pre, preFound := preSigners[signerKey] 340 post, postFound := postSigners[signerKey] 341 342 if !postFound { 343 return nil 344 } 345 346 if preFound { 347 formerSponsor := pre.Address() 348 newSponsor := post.Address() 349 if formerSponsor == newSponsor { 350 return nil 351 } 352 } 353 354 return &post 355 } 356 357 func formatPrefix(p string) string { 358 if p != "" { 359 p += "_" 360 } 361 return p 362 } 363 364 func addAssetDetailsToOperationDetails(result map[string]interface{}, asset xdr.Asset, prefix string) error { 365 var assetType, code, issuer string 366 err := asset.Extract(&assetType, &code, &issuer) 367 if err != nil { 368 return err 369 } 370 371 prefix = formatPrefix(prefix) 372 result[prefix+"asset_type"] = assetType 373 374 if asset.Type == xdr.AssetTypeAssetTypeNative { 375 result[prefix+"asset_id"] = int64(-5706705804583548011) 376 return nil 377 } 378 379 result[prefix+"asset_code"] = code 380 result[prefix+"asset_issuer"] = issuer 381 result[prefix+"asset_id"] = FarmHashAsset(code, issuer, assetType) 382 383 return nil 384 } 385 386 func addLiquidityPoolAssetDetails(result map[string]interface{}, lpp xdr.LiquidityPoolParameters) error { 387 result["asset_type"] = "liquidity_pool_shares" 388 if lpp.Type != xdr.LiquidityPoolTypeLiquidityPoolConstantProduct { 389 return fmt.Errorf("unknown liquidity pool type %d", lpp.Type) 390 } 391 cp := lpp.ConstantProduct 392 poolID, err := xdr.NewPoolId(cp.AssetA, cp.AssetB, cp.Fee) 393 if err != nil { 394 return err 395 } 396 result["liquidity_pool_id"] = PoolIDToString(poolID) 397 return nil 398 } 399 400 func addPriceDetails(result map[string]interface{}, price xdr.Price, prefix string) error { 401 prefix = formatPrefix(prefix) 402 parsedPrice, err := strconv.ParseFloat(price.String(), 64) 403 if err != nil { 404 return err 405 } 406 result[prefix+"price"] = parsedPrice 407 result[prefix+"price_r"] = Price{ 408 Numerator: int32(price.N), 409 Denominator: int32(price.D), 410 } 411 return nil 412 } 413 414 func addAccountAndMuxedAccountDetails(result map[string]interface{}, a xdr.MuxedAccount, prefix string) error { 415 account_id := a.ToAccountId() 416 result[prefix] = account_id.Address() 417 prefix = formatPrefix(prefix) 418 if a.Type == xdr.CryptoKeyTypeKeyTypeMuxedEd25519 { 419 muxedAccountAddress, err := a.GetAddress() 420 if err != nil { 421 return err 422 } 423 result[prefix+"muxed"] = muxedAccountAddress 424 muxedAccountId, err := a.GetId() 425 if err != nil { 426 return err 427 } 428 result[prefix+"muxed_id"] = muxedAccountId 429 } 430 return nil 431 } 432 433 func addTrustLineFlagToDetails(result map[string]interface{}, f xdr.TrustLineFlags, prefix string) { 434 var ( 435 n []int32 436 s []string 437 ) 438 439 if f.IsAuthorized() { 440 n = append(n, int32(xdr.TrustLineFlagsAuthorizedFlag)) 441 s = append(s, "authorized") 442 } 443 444 if f.IsAuthorizedToMaintainLiabilitiesFlag() { 445 n = append(n, int32(xdr.TrustLineFlagsAuthorizedToMaintainLiabilitiesFlag)) 446 s = append(s, "authorized_to_maintain_liabilities") 447 } 448 449 if f.IsClawbackEnabledFlag() { 450 n = append(n, int32(xdr.TrustLineFlagsTrustlineClawbackEnabledFlag)) 451 s = append(s, "clawback_enabled") 452 } 453 454 prefix = formatPrefix(prefix) 455 result[prefix+"flags"] = n 456 result[prefix+"flags_s"] = s 457 } 458 459 func addLedgerKeyToDetails(result map[string]interface{}, ledgerKey xdr.LedgerKey) error { 460 switch ledgerKey.Type { 461 case xdr.LedgerEntryTypeAccount: 462 result["account_id"] = ledgerKey.Account.AccountId.Address() 463 case xdr.LedgerEntryTypeClaimableBalance: 464 marshalHex, err := xdr.MarshalHex(ledgerKey.ClaimableBalance.BalanceId) 465 if err != nil { 466 return errors.Wrapf(err, "in claimable balance") 467 } 468 result["claimable_balance_id"] = marshalHex 469 case xdr.LedgerEntryTypeData: 470 result["data_account_id"] = ledgerKey.Data.AccountId.Address() 471 result["data_name"] = string(ledgerKey.Data.DataName) 472 case xdr.LedgerEntryTypeOffer: 473 result["offer_id"] = int64(ledgerKey.Offer.OfferId) 474 case xdr.LedgerEntryTypeTrustline: 475 result["trustline_account_id"] = ledgerKey.TrustLine.AccountId.Address() 476 if ledgerKey.TrustLine.Asset.Type == xdr.AssetTypeAssetTypePoolShare { 477 result["trustline_liquidity_pool_id"] = PoolIDToString(*ledgerKey.TrustLine.Asset.LiquidityPoolId) 478 } else { 479 result["trustline_asset"] = ledgerKey.TrustLine.Asset.ToAsset().StringCanonical() 480 } 481 case xdr.LedgerEntryTypeLiquidityPool: 482 result["liquidity_pool_id"] = PoolIDToString(ledgerKey.LiquidityPool.LiquidityPoolId) 483 } 484 return nil 485 } 486 487 func transformPath(initialPath []xdr.Asset) []Path { 488 if len(initialPath) == 0 { 489 return nil 490 } 491 var path = make([]Path, 0) 492 for _, pathAsset := range initialPath { 493 var assetType, code, issuer string 494 err := pathAsset.Extract(&assetType, &code, &issuer) 495 if err != nil { 496 return nil 497 } 498 499 path = append(path, Path{ 500 AssetType: assetType, 501 AssetIssuer: issuer, 502 AssetCode: code, 503 }) 504 } 505 return path 506 } 507 508 func findInitatingBeginSponsoringOp(operation xdr.Operation, operationIndex int32, transaction ingest.LedgerTransaction) *SponsorshipOutput { 509 if !transaction.Result.Successful() { 510 // Failed transactions may not have a compliant sandwich structure 511 // we can rely on (e.g. invalid nesting or a being operation with the wrong sponsoree ID) 512 // and thus we bail out since we could return incorrect information. 513 return nil 514 } 515 sponsoree := getOperationSourceAccount(operation, transaction).ToAccountId() 516 operations := transaction.Envelope.Operations() 517 for i := int(operationIndex) - 1; i >= 0; i-- { 518 if beginOp, ok := operations[i].Body.GetBeginSponsoringFutureReservesOp(); ok && 519 beginOp.SponsoredId.Address() == sponsoree.Address() { 520 result := SponsorshipOutput{ 521 Operation: operations[i], 522 OperationIndex: uint32(i), 523 } 524 return &result 525 } 526 } 527 return nil 528 } 529 530 func addOperationFlagToOperationDetails(result map[string]interface{}, flag uint32, prefix string) { 531 intFlags := make([]int32, 0) 532 stringFlags := make([]string, 0) 533 534 if (int64(flag) & int64(xdr.AccountFlagsAuthRequiredFlag)) > 0 { 535 intFlags = append(intFlags, int32(xdr.AccountFlagsAuthRequiredFlag)) 536 stringFlags = append(stringFlags, "auth_required") 537 } 538 539 if (int64(flag) & int64(xdr.AccountFlagsAuthRevocableFlag)) > 0 { 540 intFlags = append(intFlags, int32(xdr.AccountFlagsAuthRevocableFlag)) 541 stringFlags = append(stringFlags, "auth_revocable") 542 } 543 544 if (int64(flag) & int64(xdr.AccountFlagsAuthImmutableFlag)) > 0 { 545 intFlags = append(intFlags, int32(xdr.AccountFlagsAuthImmutableFlag)) 546 stringFlags = append(stringFlags, "auth_immutable") 547 } 548 549 if (int64(flag) & int64(xdr.AccountFlagsAuthClawbackEnabledFlag)) > 0 { 550 intFlags = append(intFlags, int32(xdr.AccountFlagsAuthClawbackEnabledFlag)) 551 stringFlags = append(stringFlags, "auth_clawback_enabled") 552 } 553 554 prefix = formatPrefix(prefix) 555 result[prefix+"flags"] = intFlags 556 result[prefix+"flags_s"] = stringFlags 557 } 558 559 func extractOperationDetails(operation xdr.Operation, transaction ingest.LedgerTransaction, operationIndex int32, network string) (map[string]interface{}, error) { 560 details := map[string]interface{}{} 561 sourceAccount := getOperationSourceAccount(operation, transaction) 562 operationType := operation.Body.Type 563 564 switch operationType { 565 case xdr.OperationTypeCreateAccount: 566 op, ok := operation.Body.GetCreateAccountOp() 567 if !ok { 568 return details, fmt.Errorf("Could not access CreateAccount info for this operation (index %d)", operationIndex) 569 } 570 571 if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "funder"); err != nil { 572 return details, err 573 } 574 details["account"] = op.Destination.Address() 575 details["starting_balance"] = utils.ConvertStroopValueToReal(op.StartingBalance) 576 577 case xdr.OperationTypePayment: 578 op, ok := operation.Body.GetPaymentOp() 579 if !ok { 580 return details, fmt.Errorf("Could not access Payment info for this operation (index %d)", operationIndex) 581 } 582 583 if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "from"); err != nil { 584 return details, err 585 } 586 if err := addAccountAndMuxedAccountDetails(details, op.Destination, "to"); err != nil { 587 return details, err 588 } 589 details["amount"] = utils.ConvertStroopValueToReal(op.Amount) 590 if err := addAssetDetailsToOperationDetails(details, op.Asset, ""); err != nil { 591 return details, err 592 } 593 594 case xdr.OperationTypePathPaymentStrictReceive: 595 op, ok := operation.Body.GetPathPaymentStrictReceiveOp() 596 if !ok { 597 return details, fmt.Errorf("Could not access PathPaymentStrictReceive info for this operation (index %d)", operationIndex) 598 } 599 600 if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "from"); err != nil { 601 return details, err 602 } 603 if err := addAccountAndMuxedAccountDetails(details, op.Destination, "to"); err != nil { 604 return details, err 605 } 606 details["amount"] = utils.ConvertStroopValueToReal(op.DestAmount) 607 details["source_amount"] = amount.String(0) 608 details["source_max"] = utils.ConvertStroopValueToReal(op.SendMax) 609 if err := addAssetDetailsToOperationDetails(details, op.DestAsset, ""); err != nil { 610 return details, err 611 } 612 if err := addAssetDetailsToOperationDetails(details, op.SendAsset, "source"); err != nil { 613 return details, err 614 } 615 616 if transaction.Result.Successful() { 617 allOperationResults, ok := transaction.Result.OperationResults() 618 if !ok { 619 return details, fmt.Errorf("Could not access any results for this transaction") 620 } 621 currentOperationResult := allOperationResults[operationIndex] 622 resultBody, ok := currentOperationResult.GetTr() 623 if !ok { 624 return details, fmt.Errorf("Could not access result body for this operation (index %d)", operationIndex) 625 } 626 result, ok := resultBody.GetPathPaymentStrictReceiveResult() 627 if !ok { 628 return details, fmt.Errorf("Could not access PathPaymentStrictReceive result info for this operation (index %d)", operationIndex) 629 } 630 details["source_amount"] = utils.ConvertStroopValueToReal(result.SendAmount()) 631 } 632 633 details["path"] = transformPath(op.Path) 634 635 case xdr.OperationTypePathPaymentStrictSend: 636 op, ok := operation.Body.GetPathPaymentStrictSendOp() 637 if !ok { 638 return details, fmt.Errorf("Could not access PathPaymentStrictSend info for this operation (index %d)", operationIndex) 639 } 640 641 if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "from"); err != nil { 642 return details, err 643 } 644 if err := addAccountAndMuxedAccountDetails(details, op.Destination, "to"); err != nil { 645 return details, err 646 } 647 details["amount"] = amount.String(0) 648 details["source_amount"] = utils.ConvertStroopValueToReal(op.SendAmount) 649 details["destination_min"] = amount.String(op.DestMin) 650 if err := addAssetDetailsToOperationDetails(details, op.DestAsset, ""); err != nil { 651 return details, err 652 } 653 if err := addAssetDetailsToOperationDetails(details, op.SendAsset, "source"); err != nil { 654 return details, err 655 } 656 657 if transaction.Result.Successful() { 658 allOperationResults, ok := transaction.Result.OperationResults() 659 if !ok { 660 return details, fmt.Errorf("Could not access any results for this transaction") 661 } 662 currentOperationResult := allOperationResults[operationIndex] 663 resultBody, ok := currentOperationResult.GetTr() 664 if !ok { 665 return details, fmt.Errorf("Could not access result body for this operation (index %d)", operationIndex) 666 } 667 result, ok := resultBody.GetPathPaymentStrictSendResult() 668 if !ok { 669 return details, fmt.Errorf("Could not access GetPathPaymentStrictSendResult result info for this operation (index %d)", operationIndex) 670 } 671 details["amount"] = utils.ConvertStroopValueToReal(result.DestAmount()) 672 } 673 674 details["path"] = transformPath(op.Path) 675 676 case xdr.OperationTypeManageBuyOffer: 677 op, ok := operation.Body.GetManageBuyOfferOp() 678 if !ok { 679 return details, fmt.Errorf("Could not access ManageBuyOffer info for this operation (index %d)", operationIndex) 680 } 681 682 details["offer_id"] = int64(op.OfferId) 683 details["amount"] = utils.ConvertStroopValueToReal(op.BuyAmount) 684 if err := addPriceDetails(details, op.Price, ""); err != nil { 685 return details, err 686 } 687 688 if err := addAssetDetailsToOperationDetails(details, op.Buying, "buying"); err != nil { 689 return details, err 690 } 691 if err := addAssetDetailsToOperationDetails(details, op.Selling, "selling"); err != nil { 692 return details, err 693 } 694 695 case xdr.OperationTypeManageSellOffer: 696 op, ok := operation.Body.GetManageSellOfferOp() 697 if !ok { 698 return details, fmt.Errorf("Could not access ManageSellOffer info for this operation (index %d)", operationIndex) 699 } 700 701 details["offer_id"] = int64(op.OfferId) 702 details["amount"] = utils.ConvertStroopValueToReal(op.Amount) 703 if err := addPriceDetails(details, op.Price, ""); err != nil { 704 return details, err 705 } 706 707 if err := addAssetDetailsToOperationDetails(details, op.Buying, "buying"); err != nil { 708 return details, err 709 } 710 if err := addAssetDetailsToOperationDetails(details, op.Selling, "selling"); err != nil { 711 return details, err 712 } 713 714 case xdr.OperationTypeCreatePassiveSellOffer: 715 op, ok := operation.Body.GetCreatePassiveSellOfferOp() 716 if !ok { 717 return details, fmt.Errorf("Could not access CreatePassiveSellOffer info for this operation (index %d)", operationIndex) 718 } 719 720 details["amount"] = utils.ConvertStroopValueToReal(op.Amount) 721 if err := addPriceDetails(details, op.Price, ""); err != nil { 722 return details, err 723 } 724 725 if err := addAssetDetailsToOperationDetails(details, op.Buying, "buying"); err != nil { 726 return details, err 727 } 728 if err := addAssetDetailsToOperationDetails(details, op.Selling, "selling"); err != nil { 729 return details, err 730 } 731 732 case xdr.OperationTypeSetOptions: 733 op, ok := operation.Body.GetSetOptionsOp() 734 if !ok { 735 return details, fmt.Errorf("Could not access GetSetOptions info for this operation (index %d)", operationIndex) 736 } 737 738 if op.InflationDest != nil { 739 details["inflation_dest"] = op.InflationDest.Address() 740 } 741 742 if op.SetFlags != nil && *op.SetFlags > 0 { 743 addOperationFlagToOperationDetails(details, uint32(*op.SetFlags), "set") 744 } 745 746 if op.ClearFlags != nil && *op.ClearFlags > 0 { 747 addOperationFlagToOperationDetails(details, uint32(*op.ClearFlags), "clear") 748 } 749 750 if op.MasterWeight != nil { 751 details["master_key_weight"] = uint32(*op.MasterWeight) 752 } 753 754 if op.LowThreshold != nil { 755 details["low_threshold"] = uint32(*op.LowThreshold) 756 } 757 758 if op.MedThreshold != nil { 759 details["med_threshold"] = uint32(*op.MedThreshold) 760 } 761 762 if op.HighThreshold != nil { 763 details["high_threshold"] = uint32(*op.HighThreshold) 764 } 765 766 if op.HomeDomain != nil { 767 details["home_domain"] = string(*op.HomeDomain) 768 } 769 770 if op.Signer != nil { 771 details["signer_key"] = op.Signer.Key.Address() 772 details["signer_weight"] = uint32(op.Signer.Weight) 773 } 774 775 case xdr.OperationTypeChangeTrust: 776 op, ok := operation.Body.GetChangeTrustOp() 777 if !ok { 778 return details, fmt.Errorf("Could not access GetChangeTrust info for this operation (index %d)", operationIndex) 779 } 780 781 if op.Line.Type == xdr.AssetTypeAssetTypePoolShare { 782 if err := addLiquidityPoolAssetDetails(details, *op.Line.LiquidityPool); err != nil { 783 return details, err 784 } 785 } else { 786 if err := addAssetDetailsToOperationDetails(details, op.Line.ToAsset(), ""); err != nil { 787 return details, err 788 } 789 details["trustee"] = details["asset_issuer"] 790 } 791 792 if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "trustor"); err != nil { 793 return details, err 794 } 795 details["limit"] = utils.ConvertStroopValueToReal(op.Limit) 796 797 case xdr.OperationTypeAllowTrust: 798 op, ok := operation.Body.GetAllowTrustOp() 799 if !ok { 800 return details, fmt.Errorf("Could not access AllowTrust info for this operation (index %d)", operationIndex) 801 } 802 803 if err := addAssetDetailsToOperationDetails(details, op.Asset.ToAsset(sourceAccount.ToAccountId()), ""); err != nil { 804 return details, err 805 } 806 if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "trustee"); err != nil { 807 return details, err 808 } 809 details["trustor"] = op.Trustor.Address() 810 shouldAuth := xdr.TrustLineFlags(op.Authorize).IsAuthorized() 811 details["authorize"] = shouldAuth 812 shouldAuthLiabilities := xdr.TrustLineFlags(op.Authorize).IsAuthorizedToMaintainLiabilitiesFlag() 813 if shouldAuthLiabilities { 814 details["authorize_to_maintain_liabilities"] = shouldAuthLiabilities 815 } 816 shouldClawbackEnabled := xdr.TrustLineFlags(op.Authorize).IsClawbackEnabledFlag() 817 if shouldClawbackEnabled { 818 details["clawback_enabled"] = shouldClawbackEnabled 819 } 820 821 case xdr.OperationTypeAccountMerge: 822 destinationAccount, ok := operation.Body.GetDestination() 823 if !ok { 824 return details, fmt.Errorf("Could not access Destination info for this operation (index %d)", operationIndex) 825 } 826 827 if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "account"); err != nil { 828 return details, err 829 } 830 if err := addAccountAndMuxedAccountDetails(details, destinationAccount, "into"); err != nil { 831 return details, err 832 } 833 834 case xdr.OperationTypeInflation: 835 // Inflation operations don't have information that affects the details struct 836 case xdr.OperationTypeManageData: 837 op, ok := operation.Body.GetManageDataOp() 838 if !ok { 839 return details, fmt.Errorf("Could not access GetManageData info for this operation (index %d)", operationIndex) 840 } 841 842 details["name"] = string(op.DataName) 843 if op.DataValue != nil { 844 details["value"] = base64.StdEncoding.EncodeToString(*op.DataValue) 845 } else { 846 details["value"] = nil 847 } 848 849 case xdr.OperationTypeBumpSequence: 850 op, ok := operation.Body.GetBumpSequenceOp() 851 if !ok { 852 return details, fmt.Errorf("Could not access BumpSequence info for this operation (index %d)", operationIndex) 853 } 854 details["bump_to"] = fmt.Sprintf("%d", op.BumpTo) 855 856 case xdr.OperationTypeCreateClaimableBalance: 857 op := operation.Body.MustCreateClaimableBalanceOp() 858 details["asset"] = op.Asset.StringCanonical() 859 details["amount"] = utils.ConvertStroopValueToReal(op.Amount) 860 details["claimants"] = transformClaimants(op.Claimants) 861 862 case xdr.OperationTypeClaimClaimableBalance: 863 op := operation.Body.MustClaimClaimableBalanceOp() 864 balanceID, err := xdr.MarshalHex(op.BalanceId) 865 if err != nil { 866 return details, fmt.Errorf("Invalid balanceId in op: %d", operationIndex) 867 } 868 details["balance_id"] = balanceID 869 if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "claimant"); err != nil { 870 return details, err 871 } 872 873 case xdr.OperationTypeBeginSponsoringFutureReserves: 874 op := operation.Body.MustBeginSponsoringFutureReservesOp() 875 details["sponsored_id"] = op.SponsoredId.Address() 876 877 case xdr.OperationTypeEndSponsoringFutureReserves: 878 beginSponsorOp := findInitatingBeginSponsoringOp(operation, operationIndex, transaction) 879 if beginSponsorOp != nil { 880 beginSponsorshipSource := getOperationSourceAccount(beginSponsorOp.Operation, transaction) 881 if err := addAccountAndMuxedAccountDetails(details, beginSponsorshipSource, "begin_sponsor"); err != nil { 882 return details, err 883 } 884 } 885 886 case xdr.OperationTypeRevokeSponsorship: 887 op := operation.Body.MustRevokeSponsorshipOp() 888 switch op.Type { 889 case xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry: 890 if err := addLedgerKeyToDetails(details, *op.LedgerKey); err != nil { 891 return details, err 892 } 893 case xdr.RevokeSponsorshipTypeRevokeSponsorshipSigner: 894 details["signer_account_id"] = op.Signer.AccountId.Address() 895 details["signer_key"] = op.Signer.SignerKey.Address() 896 } 897 898 case xdr.OperationTypeClawback: 899 op := operation.Body.MustClawbackOp() 900 if err := addAssetDetailsToOperationDetails(details, op.Asset, ""); err != nil { 901 return details, err 902 } 903 if err := addAccountAndMuxedAccountDetails(details, op.From, "from"); err != nil { 904 return details, err 905 } 906 details["amount"] = utils.ConvertStroopValueToReal(op.Amount) 907 908 case xdr.OperationTypeClawbackClaimableBalance: 909 op := operation.Body.MustClawbackClaimableBalanceOp() 910 balanceID, err := xdr.MarshalHex(op.BalanceId) 911 if err != nil { 912 return details, fmt.Errorf("Invalid balanceId in op: %d", operationIndex) 913 } 914 details["balance_id"] = balanceID 915 916 case xdr.OperationTypeSetTrustLineFlags: 917 op := operation.Body.MustSetTrustLineFlagsOp() 918 details["trustor"] = op.Trustor.Address() 919 if err := addAssetDetailsToOperationDetails(details, op.Asset, ""); err != nil { 920 return details, err 921 } 922 if op.SetFlags > 0 { 923 addTrustLineFlagToDetails(details, xdr.TrustLineFlags(op.SetFlags), "set") 924 925 } 926 if op.ClearFlags > 0 { 927 addTrustLineFlagToDetails(details, xdr.TrustLineFlags(op.ClearFlags), "clear") 928 } 929 930 case xdr.OperationTypeLiquidityPoolDeposit: 931 op := operation.Body.MustLiquidityPoolDepositOp() 932 details["liquidity_pool_id"] = PoolIDToString(op.LiquidityPoolId) 933 var ( 934 assetA, assetB xdr.Asset 935 depositedA, depositedB xdr.Int64 936 sharesReceived xdr.Int64 937 ) 938 if transaction.Result.Successful() { 939 // we will use the defaults (omitted asset and 0 amounts) if the transaction failed 940 lp, delta, err := getLiquidityPoolAndProductDelta(operationIndex, transaction, &op.LiquidityPoolId) 941 if err != nil { 942 return nil, err 943 } 944 params := lp.Body.ConstantProduct.Params 945 assetA, assetB = params.AssetA, params.AssetB 946 depositedA, depositedB = delta.ReserveA, delta.ReserveB 947 sharesReceived = delta.TotalPoolShares 948 } 949 950 // Process ReserveA Details 951 if err := addAssetDetailsToOperationDetails(details, assetA, "reserve_a"); err != nil { 952 return details, err 953 } 954 details["reserve_a_max_amount"] = utils.ConvertStroopValueToReal(op.MaxAmountA) 955 depositA, err := strconv.ParseFloat(amount.String(depositedA), 64) 956 if err != nil { 957 return details, err 958 } 959 details["reserve_a_deposit_amount"] = depositA 960 961 //Process ReserveB Details 962 if err := addAssetDetailsToOperationDetails(details, assetB, "reserve_b"); err != nil { 963 return details, err 964 } 965 details["reserve_b_max_amount"] = utils.ConvertStroopValueToReal(op.MaxAmountB) 966 depositB, err := strconv.ParseFloat(amount.String(depositedB), 64) 967 if err != nil { 968 return details, err 969 } 970 details["reserve_b_deposit_amount"] = depositB 971 972 if err := addPriceDetails(details, op.MinPrice, "min"); err != nil { 973 return details, err 974 } 975 if err := addPriceDetails(details, op.MaxPrice, "max"); err != nil { 976 return details, err 977 } 978 979 sharesToFloat, err := strconv.ParseFloat(amount.String(sharesReceived), 64) 980 if err != nil { 981 return details, err 982 } 983 details["shares_received"] = sharesToFloat 984 985 case xdr.OperationTypeLiquidityPoolWithdraw: 986 op := operation.Body.MustLiquidityPoolWithdrawOp() 987 details["liquidity_pool_id"] = PoolIDToString(op.LiquidityPoolId) 988 var ( 989 assetA, assetB xdr.Asset 990 receivedA, receivedB xdr.Int64 991 ) 992 if transaction.Result.Successful() { 993 // we will use the defaults (omitted asset and 0 amounts) if the transaction failed 994 lp, delta, err := getLiquidityPoolAndProductDelta(operationIndex, transaction, &op.LiquidityPoolId) 995 if err != nil { 996 return nil, err 997 } 998 params := lp.Body.ConstantProduct.Params 999 assetA, assetB = params.AssetA, params.AssetB 1000 receivedA, receivedB = -delta.ReserveA, -delta.ReserveB 1001 } 1002 // Process AssetA Details 1003 if err := addAssetDetailsToOperationDetails(details, assetA, "reserve_a"); err != nil { 1004 return details, err 1005 } 1006 details["reserve_a_min_amount"] = utils.ConvertStroopValueToReal(op.MinAmountA) 1007 details["reserve_a_withdraw_amount"] = utils.ConvertStroopValueToReal(receivedA) 1008 1009 // Process AssetB Details 1010 if err := addAssetDetailsToOperationDetails(details, assetB, "reserve_b"); err != nil { 1011 return details, err 1012 } 1013 details["reserve_b_min_amount"] = utils.ConvertStroopValueToReal(op.MinAmountB) 1014 details["reserve_b_withdraw_amount"] = utils.ConvertStroopValueToReal(receivedB) 1015 1016 details["shares"] = utils.ConvertStroopValueToReal(op.Amount) 1017 1018 case xdr.OperationTypeInvokeHostFunction: 1019 op := operation.Body.MustInvokeHostFunctionOp() 1020 details["function"] = op.HostFunction.Type.String() 1021 1022 switch op.HostFunction.Type { 1023 case xdr.HostFunctionTypeHostFunctionTypeInvokeContract: 1024 invokeArgs := op.HostFunction.MustInvokeContract() 1025 args := make([]xdr.ScVal, 0, len(invokeArgs.Args)+2) 1026 args = append(args, xdr.ScVal{Type: xdr.ScValTypeScvAddress, Address: &invokeArgs.ContractAddress}) 1027 args = append(args, xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &invokeArgs.FunctionName}) 1028 args = append(args, invokeArgs.Args...) 1029 params := make([]map[string]string, 0, len(args)) 1030 paramsDecoded := make([]map[string]string, 0, len(args)) 1031 1032 details["type"] = "invoke_contract" 1033 1034 transactionEnvelope := getTransactionV1Envelope(transaction.Envelope) 1035 details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) 1036 details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) 1037 1038 for _, param := range args { 1039 serializedParam := map[string]string{} 1040 serializedParam["value"] = "n/a" 1041 serializedParam["type"] = "n/a" 1042 1043 serializedParamDecoded := map[string]string{} 1044 serializedParamDecoded["value"] = "n/a" 1045 serializedParamDecoded["type"] = "n/a" 1046 1047 if scValTypeName, ok := param.ArmForSwitch(int32(param.Type)); ok { 1048 serializedParam["type"] = scValTypeName 1049 serializedParamDecoded["type"] = scValTypeName 1050 if raw, err := param.MarshalBinary(); err == nil { 1051 serializedParam["value"] = base64.StdEncoding.EncodeToString(raw) 1052 serializedParamDecoded["value"] = param.String() 1053 } 1054 } 1055 params = append(params, serializedParam) 1056 paramsDecoded = append(paramsDecoded, serializedParamDecoded) 1057 } 1058 details["parameters"] = params 1059 details["parameters_decoded"] = paramsDecoded 1060 1061 if balanceChanges, err := parseAssetBalanceChangesFromContractEvents(transaction, network); err != nil { 1062 return nil, err 1063 } else { 1064 details["asset_balance_changes"] = balanceChanges 1065 } 1066 1067 case xdr.HostFunctionTypeHostFunctionTypeCreateContract: 1068 args := op.HostFunction.MustCreateContract() 1069 details["type"] = "create_contract" 1070 1071 transactionEnvelope := getTransactionV1Envelope(transaction.Envelope) 1072 details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) 1073 details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) 1074 1075 switch args.ContractIdPreimage.Type { 1076 case xdr.ContractIdPreimageTypeContractIdPreimageFromAddress: 1077 fromAddress := args.ContractIdPreimage.MustFromAddress() 1078 address, err := fromAddress.Address.String() 1079 if err != nil { 1080 panic(fmt.Errorf("error obtaining address for: %s", args.ContractIdPreimage.Type)) 1081 } 1082 details["from"] = "address" 1083 details["address"] = address 1084 case xdr.ContractIdPreimageTypeContractIdPreimageFromAsset: 1085 details["from"] = "asset" 1086 details["asset"] = args.ContractIdPreimage.MustFromAsset().StringCanonical() 1087 default: 1088 panic(fmt.Errorf("unknown contract id type: %s", args.ContractIdPreimage.Type)) 1089 } 1090 case xdr.HostFunctionTypeHostFunctionTypeUploadContractWasm: 1091 details["type"] = "upload_wasm" 1092 transactionEnvelope := getTransactionV1Envelope(transaction.Envelope) 1093 details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) 1094 default: 1095 panic(fmt.Errorf("unknown host function type: %s", op.HostFunction.Type)) 1096 } 1097 case xdr.OperationTypeExtendFootprintTtl: 1098 op := operation.Body.MustExtendFootprintTtlOp() 1099 details["type"] = "extend_footprint_ttl" 1100 details["extend_to"] = op.ExtendTo 1101 1102 transactionEnvelope := getTransactionV1Envelope(transaction.Envelope) 1103 details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) 1104 details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) 1105 case xdr.OperationTypeRestoreFootprint: 1106 details["type"] = "restore_footprint" 1107 1108 transactionEnvelope := getTransactionV1Envelope(transaction.Envelope) 1109 details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) 1110 details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) 1111 default: 1112 return details, fmt.Errorf("Unknown operation type: %s", operation.Body.Type.String()) 1113 } 1114 1115 sponsor, err := getSponsor(operation, transaction, operationIndex) 1116 if err != nil { 1117 return nil, err 1118 } 1119 if sponsor != nil { 1120 details["sponsor"] = sponsor.Address() 1121 } 1122 1123 return details, nil 1124 } 1125 1126 // transactionOperationWrapper represents the data for a single operation within a transaction 1127 type transactionOperationWrapper struct { 1128 index uint32 1129 transaction ingest.LedgerTransaction 1130 operation xdr.Operation 1131 ledgerSequence uint32 1132 network string 1133 ledgerClosed time.Time 1134 } 1135 1136 // ID returns the ID for the operation. 1137 func (operation *transactionOperationWrapper) ID() int64 { 1138 return toid.New( 1139 int32(operation.ledgerSequence), 1140 int32(operation.transaction.Index), 1141 int32(operation.index+1), 1142 ).ToInt64() 1143 } 1144 1145 // Order returns the operation order. 1146 func (operation *transactionOperationWrapper) Order() uint32 { 1147 return operation.index + 1 1148 } 1149 1150 // TransactionID returns the id for the transaction related with this operation. 1151 func (operation *transactionOperationWrapper) TransactionID() int64 { 1152 return toid.New(int32(operation.ledgerSequence), int32(operation.transaction.Index), 0).ToInt64() 1153 } 1154 1155 // SourceAccount returns the operation's source account. 1156 func (operation *transactionOperationWrapper) SourceAccount() *xdr.MuxedAccount { 1157 sourceAccount := operation.operation.SourceAccount 1158 if sourceAccount != nil { 1159 return sourceAccount 1160 } else { 1161 ret := operation.transaction.Envelope.SourceAccount() 1162 return &ret 1163 } 1164 } 1165 1166 // OperationType returns the operation type. 1167 func (operation *transactionOperationWrapper) OperationType() xdr.OperationType { 1168 return operation.operation.Body.Type 1169 } 1170 1171 func (operation *transactionOperationWrapper) getSignerSponsorInChange(signerKey string, change ingest.Change) xdr.SponsorshipDescriptor { 1172 if change.Type != xdr.LedgerEntryTypeAccount || change.Post == nil { 1173 return nil 1174 } 1175 1176 preSigners := map[string]xdr.AccountId{} 1177 if change.Pre != nil { 1178 account := change.Pre.Data.MustAccount() 1179 preSigners = account.SponsorPerSigner() 1180 } 1181 1182 account := change.Post.Data.MustAccount() 1183 postSigners := account.SponsorPerSigner() 1184 1185 pre, preFound := preSigners[signerKey] 1186 post, postFound := postSigners[signerKey] 1187 1188 if !postFound { 1189 return nil 1190 } 1191 1192 if preFound { 1193 formerSponsor := pre.Address() 1194 newSponsor := post.Address() 1195 if formerSponsor == newSponsor { 1196 return nil 1197 } 1198 } 1199 1200 return &post 1201 } 1202 1203 func (operation *transactionOperationWrapper) getSponsor() (*xdr.AccountId, error) { 1204 changes, err := operation.transaction.GetOperationChanges(operation.index) 1205 if err != nil { 1206 return nil, err 1207 } 1208 var signerKey string 1209 if setOps, ok := operation.operation.Body.GetSetOptionsOp(); ok && setOps.Signer != nil { 1210 signerKey = setOps.Signer.Key.Address() 1211 } 1212 1213 for _, c := range changes { 1214 // Check Signer changes 1215 if signerKey != "" { 1216 if sponsorAccount := operation.getSignerSponsorInChange(signerKey, c); sponsorAccount != nil { 1217 return sponsorAccount, nil 1218 } 1219 } 1220 1221 // Check Ledger key changes 1222 if c.Pre != nil || c.Post == nil { 1223 // We are only looking for entry creations denoting that a sponsor 1224 // is associated to the ledger entry of the operation. 1225 continue 1226 } 1227 if sponsorAccount := c.Post.SponsoringID(); sponsorAccount != nil { 1228 return sponsorAccount, nil 1229 } 1230 } 1231 1232 return nil, nil 1233 } 1234 1235 var errLiquidityPoolChangeNotFound = errors.New("liquidity pool change not found") 1236 1237 func (operation *transactionOperationWrapper) getLiquidityPoolAndProductDelta(lpID *xdr.PoolId) (*xdr.LiquidityPoolEntry, *liquidityPoolDelta, error) { 1238 changes, err := operation.transaction.GetOperationChanges(operation.index) 1239 if err != nil { 1240 return nil, nil, err 1241 } 1242 1243 for _, c := range changes { 1244 if c.Type != xdr.LedgerEntryTypeLiquidityPool { 1245 continue 1246 } 1247 // The delta can be caused by a full removal or full creation of the liquidity pool 1248 var lp *xdr.LiquidityPoolEntry 1249 var preA, preB, preShares xdr.Int64 1250 if c.Pre != nil { 1251 if lpID != nil && c.Pre.Data.LiquidityPool.LiquidityPoolId != *lpID { 1252 // if we were looking for specific pool id, then check on it 1253 continue 1254 } 1255 lp = c.Pre.Data.LiquidityPool 1256 if c.Pre.Data.LiquidityPool.Body.Type != xdr.LiquidityPoolTypeLiquidityPoolConstantProduct { 1257 return nil, nil, fmt.Errorf("unexpected liquity pool body type %d", c.Pre.Data.LiquidityPool.Body.Type) 1258 } 1259 cpPre := c.Pre.Data.LiquidityPool.Body.ConstantProduct 1260 preA, preB, preShares = cpPre.ReserveA, cpPre.ReserveB, cpPre.TotalPoolShares 1261 } 1262 var postA, postB, postShares xdr.Int64 1263 if c.Post != nil { 1264 if lpID != nil && c.Post.Data.LiquidityPool.LiquidityPoolId != *lpID { 1265 // if we were looking for specific pool id, then check on it 1266 continue 1267 } 1268 lp = c.Post.Data.LiquidityPool 1269 if c.Post.Data.LiquidityPool.Body.Type != xdr.LiquidityPoolTypeLiquidityPoolConstantProduct { 1270 return nil, nil, fmt.Errorf("unexpected liquity pool body type %d", c.Post.Data.LiquidityPool.Body.Type) 1271 } 1272 cpPost := c.Post.Data.LiquidityPool.Body.ConstantProduct 1273 postA, postB, postShares = cpPost.ReserveA, cpPost.ReserveB, cpPost.TotalPoolShares 1274 } 1275 delta := &liquidityPoolDelta{ 1276 ReserveA: postA - preA, 1277 ReserveB: postB - preB, 1278 TotalPoolShares: postShares - preShares, 1279 } 1280 return lp, delta, nil 1281 } 1282 1283 return nil, nil, errLiquidityPoolChangeNotFound 1284 } 1285 1286 // OperationResult returns the operation's result record 1287 func (operation *transactionOperationWrapper) OperationResult() *xdr.OperationResultTr { 1288 results, _ := operation.transaction.Result.OperationResults() 1289 tr := results[operation.index].MustTr() 1290 return &tr 1291 } 1292 1293 func (operation *transactionOperationWrapper) findInitatingBeginSponsoringOp() *transactionOperationWrapper { 1294 if !operation.transaction.Result.Successful() { 1295 // Failed transactions may not have a compliant sandwich structure 1296 // we can rely on (e.g. invalid nesting or a being operation with the wrong sponsoree ID) 1297 // and thus we bail out since we could return incorrect information. 1298 return nil 1299 } 1300 sponsoree := operation.SourceAccount().ToAccountId() 1301 operations := operation.transaction.Envelope.Operations() 1302 for i := int(operation.index) - 1; i >= 0; i-- { 1303 if beginOp, ok := operations[i].Body.GetBeginSponsoringFutureReservesOp(); ok && 1304 beginOp.SponsoredId.Address() == sponsoree.Address() { 1305 result := *operation 1306 result.index = uint32(i) 1307 result.operation = operations[i] 1308 return &result 1309 } 1310 } 1311 return nil 1312 } 1313 1314 // Details returns the operation details as a map which can be stored as JSON. 1315 func (operation *transactionOperationWrapper) Details() (map[string]interface{}, error) { 1316 details := map[string]interface{}{} 1317 source := operation.SourceAccount() 1318 switch operation.OperationType() { 1319 case xdr.OperationTypeCreateAccount: 1320 op := operation.operation.Body.MustCreateAccountOp() 1321 addAccountAndMuxedAccountDetails(details, *source, "funder") 1322 details["account"] = op.Destination.Address() 1323 details["starting_balance"] = amount.String(op.StartingBalance) 1324 case xdr.OperationTypePayment: 1325 op := operation.operation.Body.MustPaymentOp() 1326 addAccountAndMuxedAccountDetails(details, *source, "from") 1327 addAccountAndMuxedAccountDetails(details, op.Destination, "to") 1328 details["amount"] = amount.String(op.Amount) 1329 addAssetDetails(details, op.Asset, "") 1330 case xdr.OperationTypePathPaymentStrictReceive: 1331 op := operation.operation.Body.MustPathPaymentStrictReceiveOp() 1332 addAccountAndMuxedAccountDetails(details, *source, "from") 1333 addAccountAndMuxedAccountDetails(details, op.Destination, "to") 1334 1335 details["amount"] = amount.String(op.DestAmount) 1336 details["source_amount"] = amount.String(0) 1337 details["source_max"] = amount.String(op.SendMax) 1338 addAssetDetails(details, op.DestAsset, "") 1339 addAssetDetails(details, op.SendAsset, "source_") 1340 1341 if operation.transaction.Result.Successful() { 1342 result := operation.OperationResult().MustPathPaymentStrictReceiveResult() 1343 details["source_amount"] = amount.String(result.SendAmount()) 1344 } 1345 1346 var path = make([]map[string]interface{}, len(op.Path)) 1347 for i := range op.Path { 1348 path[i] = make(map[string]interface{}) 1349 addAssetDetails(path[i], op.Path[i], "") 1350 } 1351 details["path"] = path 1352 1353 case xdr.OperationTypePathPaymentStrictSend: 1354 op := operation.operation.Body.MustPathPaymentStrictSendOp() 1355 addAccountAndMuxedAccountDetails(details, *source, "from") 1356 addAccountAndMuxedAccountDetails(details, op.Destination, "to") 1357 1358 details["amount"] = amount.String(0) 1359 details["source_amount"] = amount.String(op.SendAmount) 1360 details["destination_min"] = amount.String(op.DestMin) 1361 addAssetDetails(details, op.DestAsset, "") 1362 addAssetDetails(details, op.SendAsset, "source_") 1363 1364 if operation.transaction.Result.Successful() { 1365 result := operation.OperationResult().MustPathPaymentStrictSendResult() 1366 details["amount"] = amount.String(result.DestAmount()) 1367 } 1368 1369 var path = make([]map[string]interface{}, len(op.Path)) 1370 for i := range op.Path { 1371 path[i] = make(map[string]interface{}) 1372 addAssetDetails(path[i], op.Path[i], "") 1373 } 1374 details["path"] = path 1375 case xdr.OperationTypeManageBuyOffer: 1376 op := operation.operation.Body.MustManageBuyOfferOp() 1377 details["offer_id"] = op.OfferId 1378 details["amount"] = amount.String(op.BuyAmount) 1379 details["price"] = op.Price.String() 1380 details["price_r"] = map[string]interface{}{ 1381 "n": op.Price.N, 1382 "d": op.Price.D, 1383 } 1384 addAssetDetails(details, op.Buying, "buying_") 1385 addAssetDetails(details, op.Selling, "selling_") 1386 case xdr.OperationTypeManageSellOffer: 1387 op := operation.operation.Body.MustManageSellOfferOp() 1388 details["offer_id"] = op.OfferId 1389 details["amount"] = amount.String(op.Amount) 1390 details["price"] = op.Price.String() 1391 details["price_r"] = map[string]interface{}{ 1392 "n": op.Price.N, 1393 "d": op.Price.D, 1394 } 1395 addAssetDetails(details, op.Buying, "buying_") 1396 addAssetDetails(details, op.Selling, "selling_") 1397 case xdr.OperationTypeCreatePassiveSellOffer: 1398 op := operation.operation.Body.MustCreatePassiveSellOfferOp() 1399 details["amount"] = amount.String(op.Amount) 1400 details["price"] = op.Price.String() 1401 details["price_r"] = map[string]interface{}{ 1402 "n": op.Price.N, 1403 "d": op.Price.D, 1404 } 1405 addAssetDetails(details, op.Buying, "buying_") 1406 addAssetDetails(details, op.Selling, "selling_") 1407 case xdr.OperationTypeSetOptions: 1408 op := operation.operation.Body.MustSetOptionsOp() 1409 1410 if op.InflationDest != nil { 1411 details["inflation_dest"] = op.InflationDest.Address() 1412 } 1413 1414 if op.SetFlags != nil && *op.SetFlags > 0 { 1415 addAuthFlagDetails(details, xdr.AccountFlags(*op.SetFlags), "set") 1416 } 1417 1418 if op.ClearFlags != nil && *op.ClearFlags > 0 { 1419 addAuthFlagDetails(details, xdr.AccountFlags(*op.ClearFlags), "clear") 1420 } 1421 1422 if op.MasterWeight != nil { 1423 details["master_key_weight"] = *op.MasterWeight 1424 } 1425 1426 if op.LowThreshold != nil { 1427 details["low_threshold"] = *op.LowThreshold 1428 } 1429 1430 if op.MedThreshold != nil { 1431 details["med_threshold"] = *op.MedThreshold 1432 } 1433 1434 if op.HighThreshold != nil { 1435 details["high_threshold"] = *op.HighThreshold 1436 } 1437 1438 if op.HomeDomain != nil { 1439 details["home_domain"] = *op.HomeDomain 1440 } 1441 1442 if op.Signer != nil { 1443 details["signer_key"] = op.Signer.Key.Address() 1444 details["signer_weight"] = op.Signer.Weight 1445 } 1446 case xdr.OperationTypeChangeTrust: 1447 op := operation.operation.Body.MustChangeTrustOp() 1448 if op.Line.Type == xdr.AssetTypeAssetTypePoolShare { 1449 if err := addLiquidityPoolAssetDetails(details, *op.Line.LiquidityPool); err != nil { 1450 return nil, err 1451 } 1452 } else { 1453 addAssetDetails(details, op.Line.ToAsset(), "") 1454 details["trustee"] = details["asset_issuer"] 1455 } 1456 addAccountAndMuxedAccountDetails(details, *source, "trustor") 1457 details["limit"] = amount.String(op.Limit) 1458 case xdr.OperationTypeAllowTrust: 1459 op := operation.operation.Body.MustAllowTrustOp() 1460 addAssetDetails(details, op.Asset.ToAsset(source.ToAccountId()), "") 1461 addAccountAndMuxedAccountDetails(details, *source, "trustee") 1462 details["trustor"] = op.Trustor.Address() 1463 details["authorize"] = xdr.TrustLineFlags(op.Authorize).IsAuthorized() 1464 authLiabilities := xdr.TrustLineFlags(op.Authorize).IsAuthorizedToMaintainLiabilitiesFlag() 1465 if authLiabilities { 1466 details["authorize_to_maintain_liabilities"] = authLiabilities 1467 } 1468 clawbackEnabled := xdr.TrustLineFlags(op.Authorize).IsClawbackEnabledFlag() 1469 if clawbackEnabled { 1470 details["clawback_enabled"] = clawbackEnabled 1471 } 1472 case xdr.OperationTypeAccountMerge: 1473 addAccountAndMuxedAccountDetails(details, *source, "account") 1474 addAccountAndMuxedAccountDetails(details, operation.operation.Body.MustDestination(), "into") 1475 case xdr.OperationTypeInflation: 1476 // no inflation details, presently 1477 case xdr.OperationTypeManageData: 1478 op := operation.operation.Body.MustManageDataOp() 1479 details["name"] = string(op.DataName) 1480 if op.DataValue != nil { 1481 details["value"] = base64.StdEncoding.EncodeToString(*op.DataValue) 1482 } else { 1483 details["value"] = nil 1484 } 1485 case xdr.OperationTypeBumpSequence: 1486 op := operation.operation.Body.MustBumpSequenceOp() 1487 details["bump_to"] = fmt.Sprintf("%d", op.BumpTo) 1488 case xdr.OperationTypeCreateClaimableBalance: 1489 op := operation.operation.Body.MustCreateClaimableBalanceOp() 1490 details["asset"] = op.Asset.StringCanonical() 1491 details["amount"] = amount.String(op.Amount) 1492 var claimants []Claimant 1493 for _, c := range op.Claimants { 1494 cv0 := c.MustV0() 1495 claimants = append(claimants, Claimant{ 1496 Destination: cv0.Destination.Address(), 1497 Predicate: cv0.Predicate, 1498 }) 1499 } 1500 details["claimants"] = claimants 1501 case xdr.OperationTypeClaimClaimableBalance: 1502 op := operation.operation.Body.MustClaimClaimableBalanceOp() 1503 balanceID, err := xdr.MarshalHex(op.BalanceId) 1504 if err != nil { 1505 panic(fmt.Errorf("Invalid balanceId in op: %d", operation.index)) 1506 } 1507 details["balance_id"] = balanceID 1508 addAccountAndMuxedAccountDetails(details, *source, "claimant") 1509 case xdr.OperationTypeBeginSponsoringFutureReserves: 1510 op := operation.operation.Body.MustBeginSponsoringFutureReservesOp() 1511 details["sponsored_id"] = op.SponsoredId.Address() 1512 case xdr.OperationTypeEndSponsoringFutureReserves: 1513 beginSponsorshipOp := operation.findInitatingBeginSponsoringOp() 1514 if beginSponsorshipOp != nil { 1515 beginSponsorshipSource := beginSponsorshipOp.SourceAccount() 1516 addAccountAndMuxedAccountDetails(details, *beginSponsorshipSource, "begin_sponsor") 1517 } 1518 case xdr.OperationTypeRevokeSponsorship: 1519 op := operation.operation.Body.MustRevokeSponsorshipOp() 1520 switch op.Type { 1521 case xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry: 1522 if err := addLedgerKeyDetails(details, *op.LedgerKey); err != nil { 1523 return nil, err 1524 } 1525 case xdr.RevokeSponsorshipTypeRevokeSponsorshipSigner: 1526 details["signer_account_id"] = op.Signer.AccountId.Address() 1527 details["signer_key"] = op.Signer.SignerKey.Address() 1528 } 1529 case xdr.OperationTypeClawback: 1530 op := operation.operation.Body.MustClawbackOp() 1531 addAssetDetails(details, op.Asset, "") 1532 addAccountAndMuxedAccountDetails(details, op.From, "from") 1533 details["amount"] = amount.String(op.Amount) 1534 case xdr.OperationTypeClawbackClaimableBalance: 1535 op := operation.operation.Body.MustClawbackClaimableBalanceOp() 1536 balanceID, err := xdr.MarshalHex(op.BalanceId) 1537 if err != nil { 1538 panic(fmt.Errorf("Invalid balanceId in op: %d", operation.index)) 1539 } 1540 details["balance_id"] = balanceID 1541 case xdr.OperationTypeSetTrustLineFlags: 1542 op := operation.operation.Body.MustSetTrustLineFlagsOp() 1543 details["trustor"] = op.Trustor.Address() 1544 addAssetDetails(details, op.Asset, "") 1545 if op.SetFlags > 0 { 1546 addTrustLineFlagDetails(details, xdr.TrustLineFlags(op.SetFlags), "set") 1547 } 1548 1549 if op.ClearFlags > 0 { 1550 addTrustLineFlagDetails(details, xdr.TrustLineFlags(op.ClearFlags), "clear") 1551 } 1552 case xdr.OperationTypeLiquidityPoolDeposit: 1553 op := operation.operation.Body.MustLiquidityPoolDepositOp() 1554 details["liquidity_pool_id"] = PoolIDToString(op.LiquidityPoolId) 1555 var ( 1556 assetA, assetB string 1557 depositedA, depositedB xdr.Int64 1558 sharesReceived xdr.Int64 1559 ) 1560 if operation.transaction.Result.Successful() { 1561 // we will use the defaults (omitted asset and 0 amounts) if the transaction failed 1562 lp, delta, err := operation.getLiquidityPoolAndProductDelta(&op.LiquidityPoolId) 1563 if err != nil { 1564 return nil, err 1565 } 1566 params := lp.Body.ConstantProduct.Params 1567 assetA, assetB = params.AssetA.StringCanonical(), params.AssetB.StringCanonical() 1568 depositedA, depositedB = delta.ReserveA, delta.ReserveB 1569 sharesReceived = delta.TotalPoolShares 1570 } 1571 details["reserves_max"] = []base.AssetAmount{ 1572 {Asset: assetA, Amount: amount.String(op.MaxAmountA)}, 1573 {Asset: assetB, Amount: amount.String(op.MaxAmountB)}, 1574 } 1575 details["min_price"] = op.MinPrice.String() 1576 details["min_price_r"] = map[string]interface{}{ 1577 "n": op.MinPrice.N, 1578 "d": op.MinPrice.D, 1579 } 1580 details["max_price"] = op.MaxPrice.String() 1581 details["max_price_r"] = map[string]interface{}{ 1582 "n": op.MaxPrice.N, 1583 "d": op.MaxPrice.D, 1584 } 1585 details["reserves_deposited"] = []base.AssetAmount{ 1586 {Asset: assetA, Amount: amount.String(depositedA)}, 1587 {Asset: assetB, Amount: amount.String(depositedB)}, 1588 } 1589 details["shares_received"] = amount.String(sharesReceived) 1590 case xdr.OperationTypeLiquidityPoolWithdraw: 1591 op := operation.operation.Body.MustLiquidityPoolWithdrawOp() 1592 details["liquidity_pool_id"] = PoolIDToString(op.LiquidityPoolId) 1593 var ( 1594 assetA, assetB string 1595 receivedA, receivedB xdr.Int64 1596 ) 1597 if operation.transaction.Result.Successful() { 1598 // we will use the defaults (omitted asset and 0 amounts) if the transaction failed 1599 lp, delta, err := operation.getLiquidityPoolAndProductDelta(&op.LiquidityPoolId) 1600 if err != nil { 1601 return nil, err 1602 } 1603 params := lp.Body.ConstantProduct.Params 1604 assetA, assetB = params.AssetA.StringCanonical(), params.AssetB.StringCanonical() 1605 receivedA, receivedB = -delta.ReserveA, -delta.ReserveB 1606 } 1607 details["reserves_min"] = []base.AssetAmount{ 1608 {Asset: assetA, Amount: amount.String(op.MinAmountA)}, 1609 {Asset: assetB, Amount: amount.String(op.MinAmountB)}, 1610 } 1611 details["shares"] = amount.String(op.Amount) 1612 details["reserves_received"] = []base.AssetAmount{ 1613 {Asset: assetA, Amount: amount.String(receivedA)}, 1614 {Asset: assetB, Amount: amount.String(receivedB)}, 1615 } 1616 case xdr.OperationTypeInvokeHostFunction: 1617 op := operation.operation.Body.MustInvokeHostFunctionOp() 1618 details["function"] = op.HostFunction.Type.String() 1619 1620 switch op.HostFunction.Type { 1621 case xdr.HostFunctionTypeHostFunctionTypeInvokeContract: 1622 invokeArgs := op.HostFunction.MustInvokeContract() 1623 args := make([]xdr.ScVal, 0, len(invokeArgs.Args)+2) 1624 args = append(args, xdr.ScVal{Type: xdr.ScValTypeScvAddress, Address: &invokeArgs.ContractAddress}) 1625 args = append(args, xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &invokeArgs.FunctionName}) 1626 args = append(args, invokeArgs.Args...) 1627 params := make([]map[string]string, 0, len(args)) 1628 paramsDecoded := make([]map[string]string, 0, len(args)) 1629 1630 details["type"] = "invoke_contract" 1631 1632 transactionEnvelope := getTransactionV1Envelope(operation.transaction.Envelope) 1633 details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) 1634 details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) 1635 1636 for _, param := range args { 1637 serializedParam := map[string]string{} 1638 serializedParam["value"] = "n/a" 1639 serializedParam["type"] = "n/a" 1640 1641 serializedParamDecoded := map[string]string{} 1642 serializedParamDecoded["value"] = "n/a" 1643 serializedParamDecoded["type"] = "n/a" 1644 1645 if scValTypeName, ok := param.ArmForSwitch(int32(param.Type)); ok { 1646 serializedParam["type"] = scValTypeName 1647 serializedParamDecoded["type"] = scValTypeName 1648 if raw, err := param.MarshalBinary(); err == nil { 1649 serializedParam["value"] = base64.StdEncoding.EncodeToString(raw) 1650 serializedParamDecoded["value"] = param.String() 1651 } 1652 } 1653 params = append(params, serializedParam) 1654 paramsDecoded = append(paramsDecoded, serializedParamDecoded) 1655 } 1656 details["parameters"] = params 1657 details["parameters_decoded"] = paramsDecoded 1658 1659 if balanceChanges, err := operation.parseAssetBalanceChangesFromContractEvents(); err != nil { 1660 return nil, err 1661 } else { 1662 details["asset_balance_changes"] = balanceChanges 1663 } 1664 1665 case xdr.HostFunctionTypeHostFunctionTypeCreateContract: 1666 args := op.HostFunction.MustCreateContract() 1667 details["type"] = "create_contract" 1668 1669 transactionEnvelope := getTransactionV1Envelope(operation.transaction.Envelope) 1670 details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) 1671 details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) 1672 1673 switch args.ContractIdPreimage.Type { 1674 case xdr.ContractIdPreimageTypeContractIdPreimageFromAddress: 1675 fromAddress := args.ContractIdPreimage.MustFromAddress() 1676 address, err := fromAddress.Address.String() 1677 if err != nil { 1678 panic(fmt.Errorf("error obtaining address for: %s", args.ContractIdPreimage.Type)) 1679 } 1680 details["from"] = "address" 1681 details["address"] = address 1682 case xdr.ContractIdPreimageTypeContractIdPreimageFromAsset: 1683 details["from"] = "asset" 1684 details["asset"] = args.ContractIdPreimage.MustFromAsset().StringCanonical() 1685 default: 1686 panic(fmt.Errorf("unknown contract id type: %s", args.ContractIdPreimage.Type)) 1687 } 1688 case xdr.HostFunctionTypeHostFunctionTypeUploadContractWasm: 1689 details["type"] = "upload_wasm" 1690 transactionEnvelope := getTransactionV1Envelope(operation.transaction.Envelope) 1691 details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) 1692 default: 1693 panic(fmt.Errorf("unknown host function type: %s", op.HostFunction.Type)) 1694 } 1695 case xdr.OperationTypeExtendFootprintTtl: 1696 op := operation.operation.Body.MustExtendFootprintTtlOp() 1697 details["type"] = "extend_footprint_ttl" 1698 details["extend_to"] = op.ExtendTo 1699 1700 transactionEnvelope := getTransactionV1Envelope(operation.transaction.Envelope) 1701 details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) 1702 details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) 1703 case xdr.OperationTypeRestoreFootprint: 1704 details["type"] = "restore_footprint" 1705 1706 transactionEnvelope := getTransactionV1Envelope(operation.transaction.Envelope) 1707 details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) 1708 details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) 1709 default: 1710 panic(fmt.Errorf("Unknown operation type: %s", operation.OperationType())) 1711 } 1712 1713 sponsor, err := operation.getSponsor() 1714 if err != nil { 1715 return nil, err 1716 } 1717 if sponsor != nil { 1718 details["sponsor"] = sponsor.Address() 1719 } 1720 1721 return details, nil 1722 } 1723 1724 func getTransactionV1Envelope(transactionEnvelope xdr.TransactionEnvelope) xdr.TransactionV1Envelope { 1725 switch transactionEnvelope.Type { 1726 case xdr.EnvelopeTypeEnvelopeTypeTx: 1727 return transactionEnvelope.MustV1() 1728 case xdr.EnvelopeTypeEnvelopeTypeTxFeeBump: 1729 return transactionEnvelope.MustFeeBump().Tx.InnerTx.MustV1() 1730 } 1731 1732 return xdr.TransactionV1Envelope{} 1733 } 1734 1735 func contractIdFromTxEnvelope(transactionEnvelope xdr.TransactionV1Envelope) string { 1736 for _, ledgerKey := range transactionEnvelope.Tx.Ext.SorobanData.Resources.Footprint.ReadWrite { 1737 contractId := contractIdFromContractData(ledgerKey) 1738 if contractId != "" { 1739 return contractId 1740 } 1741 } 1742 1743 for _, ledgerKey := range transactionEnvelope.Tx.Ext.SorobanData.Resources.Footprint.ReadOnly { 1744 contractId := contractIdFromContractData(ledgerKey) 1745 if contractId != "" { 1746 return contractId 1747 } 1748 } 1749 1750 return "" 1751 } 1752 1753 func contractIdFromContractData(ledgerKey xdr.LedgerKey) string { 1754 contractData, ok := ledgerKey.GetContractData() 1755 if !ok { 1756 return "" 1757 } 1758 contractIdHash, ok := contractData.Contract.GetContractId() 1759 if !ok { 1760 return "" 1761 } 1762 1763 contractIdByte, _ := contractIdHash.MarshalBinary() 1764 contractId, _ := strkey.Encode(strkey.VersionByteContract, contractIdByte) 1765 return contractId 1766 } 1767 1768 func contractCodeHashFromTxEnvelope(transactionEnvelope xdr.TransactionV1Envelope) string { 1769 for _, ledgerKey := range transactionEnvelope.Tx.Ext.SorobanData.Resources.Footprint.ReadOnly { 1770 contractCode := contractCodeFromContractData(ledgerKey) 1771 if contractCode != "" { 1772 return contractCode 1773 } 1774 } 1775 1776 for _, ledgerKey := range transactionEnvelope.Tx.Ext.SorobanData.Resources.Footprint.ReadWrite { 1777 contractCode := contractCodeFromContractData(ledgerKey) 1778 if contractCode != "" { 1779 return contractCode 1780 } 1781 } 1782 1783 return "" 1784 } 1785 1786 func contractCodeFromContractData(ledgerKey xdr.LedgerKey) string { 1787 contractCode, ok := ledgerKey.GetContractCode() 1788 if !ok { 1789 return "" 1790 } 1791 1792 contractCodeHash := contractCode.Hash.HexString() 1793 return contractCodeHash 1794 } 1795 1796 func filterEvents(diagnosticEvents []xdr.DiagnosticEvent) []xdr.ContractEvent { 1797 var filtered []xdr.ContractEvent 1798 for _, diagnosticEvent := range diagnosticEvents { 1799 if !diagnosticEvent.InSuccessfulContractCall || diagnosticEvent.Event.Type != xdr.ContractEventTypeContract { 1800 continue 1801 } 1802 filtered = append(filtered, diagnosticEvent.Event) 1803 } 1804 return filtered 1805 } 1806 1807 // Searches an operation for SAC events that are of a type which represent 1808 // asset balances having changed. 1809 // 1810 // SAC events have a one-to-one association to SAC contract fn invocations. 1811 // i.e. invoke the 'mint' function, will trigger one Mint Event to be emitted capturing the fn args. 1812 // 1813 // SAC events that involve asset balance changes follow some standard data formats. 1814 // The 'amount' in the event is expressed as Int128Parts, which carries a sign, however it's expected 1815 // that value will not be signed as it represents a absolute delta, the event type can provide the 1816 // context of whether an amount was considered incremental or decremental, i.e. credit or debit to a balance. 1817 func (operation *transactionOperationWrapper) parseAssetBalanceChangesFromContractEvents() ([]map[string]interface{}, error) { 1818 balanceChanges := []map[string]interface{}{} 1819 1820 diagnosticEvents, err := operation.transaction.GetDiagnosticEvents() 1821 if err != nil { 1822 // this operation in this context must be an InvokeHostFunctionOp, therefore V3Meta should be present 1823 // as it's in same soroban model, so if any err, it's real, 1824 return nil, err 1825 } 1826 1827 for _, contractEvent := range filterEvents(diagnosticEvents) { 1828 // Parse the xdr contract event to contractevents.StellarAssetContractEvent model 1829 1830 // has some convenience like to/from attributes are expressed in strkey format for accounts(G...) and contracts(C...) 1831 if sacEvent, err := contractevents.NewStellarAssetContractEvent(&contractEvent, operation.network); err == nil { 1832 switch sacEvent.GetType() { 1833 case contractevents.EventTypeTransfer: 1834 transferEvt := sacEvent.(*contractevents.TransferEvent) 1835 balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(transferEvt.From, transferEvt.To, transferEvt.Amount, transferEvt.Asset, "transfer")) 1836 case contractevents.EventTypeMint: 1837 mintEvt := sacEvent.(*contractevents.MintEvent) 1838 balanceChanges = append(balanceChanges, createSACBalanceChangeEntry("", mintEvt.To, mintEvt.Amount, mintEvt.Asset, "mint")) 1839 case contractevents.EventTypeClawback: 1840 clawbackEvt := sacEvent.(*contractevents.ClawbackEvent) 1841 balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(clawbackEvt.From, "", clawbackEvt.Amount, clawbackEvt.Asset, "clawback")) 1842 case contractevents.EventTypeBurn: 1843 burnEvt := sacEvent.(*contractevents.BurnEvent) 1844 balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(burnEvt.From, "", burnEvt.Amount, burnEvt.Asset, "burn")) 1845 } 1846 } 1847 } 1848 1849 return balanceChanges, nil 1850 } 1851 1852 func parseAssetBalanceChangesFromContractEvents(transaction ingest.LedgerTransaction, network string) ([]map[string]interface{}, error) { 1853 balanceChanges := []map[string]interface{}{} 1854 1855 diagnosticEvents, err := transaction.GetDiagnosticEvents() 1856 if err != nil { 1857 // this operation in this context must be an InvokeHostFunctionOp, therefore V3Meta should be present 1858 // as it's in same soroban model, so if any err, it's real, 1859 return nil, err 1860 } 1861 1862 for _, contractEvent := range filterEvents(diagnosticEvents) { 1863 // Parse the xdr contract event to contractevents.StellarAssetContractEvent model 1864 1865 // has some convenience like to/from attributes are expressed in strkey format for accounts(G...) and contracts(C...) 1866 if sacEvent, err := contractevents.NewStellarAssetContractEvent(&contractEvent, network); err == nil { 1867 switch sacEvent.GetType() { 1868 case contractevents.EventTypeTransfer: 1869 transferEvt := sacEvent.(*contractevents.TransferEvent) 1870 balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(transferEvt.From, transferEvt.To, transferEvt.Amount, transferEvt.Asset, "transfer")) 1871 case contractevents.EventTypeMint: 1872 mintEvt := sacEvent.(*contractevents.MintEvent) 1873 balanceChanges = append(balanceChanges, createSACBalanceChangeEntry("", mintEvt.To, mintEvt.Amount, mintEvt.Asset, "mint")) 1874 case contractevents.EventTypeClawback: 1875 clawbackEvt := sacEvent.(*contractevents.ClawbackEvent) 1876 balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(clawbackEvt.From, "", clawbackEvt.Amount, clawbackEvt.Asset, "clawback")) 1877 case contractevents.EventTypeBurn: 1878 burnEvt := sacEvent.(*contractevents.BurnEvent) 1879 balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(burnEvt.From, "", burnEvt.Amount, burnEvt.Asset, "burn")) 1880 } 1881 } 1882 } 1883 1884 return balanceChanges, nil 1885 } 1886 1887 // fromAccount - strkey format of contract or address 1888 // toAccount - strkey format of contract or address, or nillable 1889 // amountChanged - absolute value that asset balance changed 1890 // asset - the fully qualified issuer:code for asset that had balance change 1891 // changeType - the type of source sac event that triggered this change 1892 // 1893 // return - a balance changed record expressed as map of key/value's 1894 func createSACBalanceChangeEntry(fromAccount string, toAccount string, amountChanged xdr.Int128Parts, asset xdr.Asset, changeType string) map[string]interface{} { 1895 balanceChange := map[string]interface{}{} 1896 1897 if fromAccount != "" { 1898 balanceChange["from"] = fromAccount 1899 } 1900 if toAccount != "" { 1901 balanceChange["to"] = toAccount 1902 } 1903 1904 balanceChange["type"] = changeType 1905 balanceChange["amount"] = amount.String128(amountChanged) 1906 addAssetDetails(balanceChange, asset, "") 1907 return balanceChange 1908 } 1909 1910 // addAssetDetails sets the details for `a` on `result` using keys with `prefix` 1911 func addAssetDetails(result map[string]interface{}, a xdr.Asset, prefix string) error { 1912 var ( 1913 assetType string 1914 code string 1915 issuer string 1916 ) 1917 err := a.Extract(&assetType, &code, &issuer) 1918 if err != nil { 1919 err = errors.Wrap(err, "xdr.Asset.Extract error") 1920 return err 1921 } 1922 result[prefix+"asset_type"] = assetType 1923 1924 if a.Type == xdr.AssetTypeAssetTypeNative { 1925 return nil 1926 } 1927 1928 result[prefix+"asset_code"] = code 1929 result[prefix+"asset_issuer"] = issuer 1930 return nil 1931 } 1932 1933 // addAuthFlagDetails adds the account flag details for `f` on `result`. 1934 func addAuthFlagDetails(result map[string]interface{}, f xdr.AccountFlags, prefix string) { 1935 var ( 1936 n []int32 1937 s []string 1938 ) 1939 1940 if f.IsAuthRequired() { 1941 n = append(n, int32(xdr.AccountFlagsAuthRequiredFlag)) 1942 s = append(s, "auth_required") 1943 } 1944 1945 if f.IsAuthRevocable() { 1946 n = append(n, int32(xdr.AccountFlagsAuthRevocableFlag)) 1947 s = append(s, "auth_revocable") 1948 } 1949 1950 if f.IsAuthImmutable() { 1951 n = append(n, int32(xdr.AccountFlagsAuthImmutableFlag)) 1952 s = append(s, "auth_immutable") 1953 } 1954 1955 if f.IsAuthClawbackEnabled() { 1956 n = append(n, int32(xdr.AccountFlagsAuthClawbackEnabledFlag)) 1957 s = append(s, "auth_clawback_enabled") 1958 } 1959 1960 result[prefix+"_flags"] = n 1961 result[prefix+"_flags_s"] = s 1962 } 1963 1964 // addTrustLineFlagDetails adds the trustline flag details for `f` on `result`. 1965 func addTrustLineFlagDetails(result map[string]interface{}, f xdr.TrustLineFlags, prefix string) { 1966 var ( 1967 n []int32 1968 s []string 1969 ) 1970 1971 if f.IsAuthorized() { 1972 n = append(n, int32(xdr.TrustLineFlagsAuthorizedFlag)) 1973 s = append(s, "authorized") 1974 } 1975 1976 if f.IsAuthorizedToMaintainLiabilitiesFlag() { 1977 n = append(n, int32(xdr.TrustLineFlagsAuthorizedToMaintainLiabilitiesFlag)) 1978 s = append(s, "authorized_to_maintain_liabilites") 1979 } 1980 1981 if f.IsClawbackEnabledFlag() { 1982 n = append(n, int32(xdr.TrustLineFlagsTrustlineClawbackEnabledFlag)) 1983 s = append(s, "clawback_enabled") 1984 } 1985 1986 result[prefix+"_flags"] = n 1987 result[prefix+"_flags_s"] = s 1988 } 1989 1990 func addLedgerKeyDetails(result map[string]interface{}, ledgerKey xdr.LedgerKey) error { 1991 switch ledgerKey.Type { 1992 case xdr.LedgerEntryTypeAccount: 1993 result["account_id"] = ledgerKey.Account.AccountId.Address() 1994 case xdr.LedgerEntryTypeClaimableBalance: 1995 marshalHex, err := xdr.MarshalHex(ledgerKey.ClaimableBalance.BalanceId) 1996 if err != nil { 1997 return errors.Wrapf(err, "in claimable balance") 1998 } 1999 result["claimable_balance_id"] = marshalHex 2000 case xdr.LedgerEntryTypeData: 2001 result["data_account_id"] = ledgerKey.Data.AccountId.Address() 2002 result["data_name"] = ledgerKey.Data.DataName 2003 case xdr.LedgerEntryTypeOffer: 2004 result["offer_id"] = fmt.Sprintf("%d", ledgerKey.Offer.OfferId) 2005 case xdr.LedgerEntryTypeTrustline: 2006 result["trustline_account_id"] = ledgerKey.TrustLine.AccountId.Address() 2007 if ledgerKey.TrustLine.Asset.Type == xdr.AssetTypeAssetTypePoolShare { 2008 result["trustline_liquidity_pool_id"] = PoolIDToString(*ledgerKey.TrustLine.Asset.LiquidityPoolId) 2009 } else { 2010 result["trustline_asset"] = ledgerKey.TrustLine.Asset.ToAsset().StringCanonical() 2011 } 2012 case xdr.LedgerEntryTypeLiquidityPool: 2013 result["liquidity_pool_id"] = PoolIDToString(ledgerKey.LiquidityPool.LiquidityPoolId) 2014 } 2015 return nil 2016 } 2017 2018 func getLedgerKeyParticipants(ledgerKey xdr.LedgerKey) []xdr.AccountId { 2019 var result []xdr.AccountId 2020 switch ledgerKey.Type { 2021 case xdr.LedgerEntryTypeAccount: 2022 result = append(result, ledgerKey.Account.AccountId) 2023 case xdr.LedgerEntryTypeClaimableBalance: 2024 // nothing to do 2025 case xdr.LedgerEntryTypeData: 2026 result = append(result, ledgerKey.Data.AccountId) 2027 case xdr.LedgerEntryTypeOffer: 2028 result = append(result, ledgerKey.Offer.SellerId) 2029 case xdr.LedgerEntryTypeTrustline: 2030 result = append(result, ledgerKey.TrustLine.AccountId) 2031 } 2032 return result 2033 } 2034 2035 // Participants returns the accounts taking part in the operation. 2036 func (operation *transactionOperationWrapper) Participants() ([]xdr.AccountId, error) { 2037 participants := []xdr.AccountId{} 2038 participants = append(participants, operation.SourceAccount().ToAccountId()) 2039 op := operation.operation 2040 2041 switch operation.OperationType() { 2042 case xdr.OperationTypeCreateAccount: 2043 participants = append(participants, op.Body.MustCreateAccountOp().Destination) 2044 case xdr.OperationTypePayment: 2045 participants = append(participants, op.Body.MustPaymentOp().Destination.ToAccountId()) 2046 case xdr.OperationTypePathPaymentStrictReceive: 2047 participants = append(participants, op.Body.MustPathPaymentStrictReceiveOp().Destination.ToAccountId()) 2048 case xdr.OperationTypePathPaymentStrictSend: 2049 participants = append(participants, op.Body.MustPathPaymentStrictSendOp().Destination.ToAccountId()) 2050 case xdr.OperationTypeManageBuyOffer: 2051 // the only direct participant is the source_account 2052 case xdr.OperationTypeManageSellOffer: 2053 // the only direct participant is the source_account 2054 case xdr.OperationTypeCreatePassiveSellOffer: 2055 // the only direct participant is the source_account 2056 case xdr.OperationTypeSetOptions: 2057 // the only direct participant is the source_account 2058 case xdr.OperationTypeChangeTrust: 2059 // the only direct participant is the source_account 2060 case xdr.OperationTypeAllowTrust: 2061 participants = append(participants, op.Body.MustAllowTrustOp().Trustor) 2062 case xdr.OperationTypeAccountMerge: 2063 participants = append(participants, op.Body.MustDestination().ToAccountId()) 2064 case xdr.OperationTypeInflation: 2065 // the only direct participant is the source_account 2066 case xdr.OperationTypeManageData: 2067 // the only direct participant is the source_account 2068 case xdr.OperationTypeBumpSequence: 2069 // the only direct participant is the source_account 2070 case xdr.OperationTypeCreateClaimableBalance: 2071 for _, c := range op.Body.MustCreateClaimableBalanceOp().Claimants { 2072 participants = append(participants, c.MustV0().Destination) 2073 } 2074 case xdr.OperationTypeClaimClaimableBalance: 2075 // the only direct participant is the source_account 2076 case xdr.OperationTypeBeginSponsoringFutureReserves: 2077 participants = append(participants, op.Body.MustBeginSponsoringFutureReservesOp().SponsoredId) 2078 case xdr.OperationTypeEndSponsoringFutureReserves: 2079 beginSponsorshipOp := operation.findInitatingBeginSponsoringOp() 2080 if beginSponsorshipOp != nil { 2081 participants = append(participants, beginSponsorshipOp.SourceAccount().ToAccountId()) 2082 } 2083 case xdr.OperationTypeRevokeSponsorship: 2084 op := operation.operation.Body.MustRevokeSponsorshipOp() 2085 switch op.Type { 2086 case xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry: 2087 participants = append(participants, getLedgerKeyParticipants(*op.LedgerKey)...) 2088 case xdr.RevokeSponsorshipTypeRevokeSponsorshipSigner: 2089 participants = append(participants, op.Signer.AccountId) 2090 // We don't add signer as a participant because a signer can be arbitrary account. 2091 // This can spam successful operations history of any account. 2092 } 2093 case xdr.OperationTypeClawback: 2094 op := operation.operation.Body.MustClawbackOp() 2095 participants = append(participants, op.From.ToAccountId()) 2096 case xdr.OperationTypeClawbackClaimableBalance: 2097 // the only direct participant is the source_account 2098 case xdr.OperationTypeSetTrustLineFlags: 2099 op := operation.operation.Body.MustSetTrustLineFlagsOp() 2100 participants = append(participants, op.Trustor) 2101 case xdr.OperationTypeLiquidityPoolDeposit: 2102 // the only direct participant is the source_account 2103 case xdr.OperationTypeLiquidityPoolWithdraw: 2104 // the only direct participant is the source_account 2105 case xdr.OperationTypeInvokeHostFunction: 2106 // the only direct participant is the source_account 2107 case xdr.OperationTypeExtendFootprintTtl: 2108 // the only direct participant is the source_account 2109 case xdr.OperationTypeRestoreFootprint: 2110 // the only direct participant is the source_account 2111 default: 2112 return participants, fmt.Errorf("Unknown operation type: %s", op.Body.Type) 2113 } 2114 2115 sponsor, err := operation.getSponsor() 2116 if err != nil { 2117 return nil, err 2118 } 2119 if sponsor != nil { 2120 participants = append(participants, *sponsor) 2121 } 2122 2123 return dedupeParticipants(participants), nil 2124 } 2125 2126 // dedupeParticipants remove any duplicate ids from `in` 2127 func dedupeParticipants(in []xdr.AccountId) (out []xdr.AccountId) { 2128 set := map[string]xdr.AccountId{} 2129 for _, id := range in { 2130 set[id.Address()] = id 2131 } 2132 2133 for _, id := range set { 2134 out = append(out, id) 2135 } 2136 return 2137 } 2138 2139 // OperationsParticipants returns a map with all participants per operation 2140 func operationsParticipants(transaction ingest.LedgerTransaction, sequence uint32) (map[int64][]xdr.AccountId, error) { 2141 participants := map[int64][]xdr.AccountId{} 2142 2143 for opi, op := range transaction.Envelope.Operations() { 2144 operation := transactionOperationWrapper{ 2145 index: uint32(opi), 2146 transaction: transaction, 2147 operation: op, 2148 ledgerSequence: sequence, 2149 } 2150 2151 p, err := operation.Participants() 2152 if err != nil { 2153 return participants, errors.Wrapf(err, "reading operation %v participants", operation.ID()) 2154 } 2155 participants[operation.ID()] = p 2156 } 2157 2158 return participants, nil 2159 }