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  }