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

     1  package transform
     2  
     3  import (
     4  	"encoding/base64"
     5  
     6  	"fmt"
     7  	"reflect"
     8  	"sort"
     9  	"strconv"
    10  
    11  	"github.com/guregu/null"
    12  	"github.com/stellar/go/amount"
    13  	"github.com/stellar/go/ingest"
    14  	"github.com/stellar/go/keypair"
    15  	"github.com/stellar/go/protocols/horizon/base"
    16  	"github.com/stellar/go/strkey"
    17  	"github.com/stellar/go/support/contractevents"
    18  	"github.com/stellar/go/support/errors"
    19  	"github.com/stellar/go/xdr"
    20  	"github.com/stellar/stellar-etl/internal/utils"
    21  )
    22  
    23  func TransformEffect(transaction ingest.LedgerTransaction, ledgerSeq uint32, ledgerCloseMeta xdr.LedgerCloseMeta, networkPassphrase string) ([]EffectOutput, error) {
    24  	effects := []EffectOutput{}
    25  
    26  	outputCloseTime, err := utils.GetCloseTime(ledgerCloseMeta)
    27  	if err != nil {
    28  		return effects, err
    29  	}
    30  
    31  	for opi, op := range transaction.Envelope.Operations() {
    32  		operation := transactionOperationWrapper{
    33  			index:          uint32(opi),
    34  			transaction:    transaction,
    35  			operation:      op,
    36  			ledgerSequence: ledgerSeq,
    37  			network:        networkPassphrase,
    38  			ledgerClosed:   outputCloseTime,
    39  		}
    40  
    41  		p, err := operation.effects()
    42  		if err != nil {
    43  			return effects, errors.Wrapf(err, "reading operation %v effects", operation.ID())
    44  		}
    45  
    46  		effects = append(effects, p...)
    47  
    48  	}
    49  
    50  	return effects, nil
    51  }
    52  
    53  // Effects returns the operation effects
    54  func (operation *transactionOperationWrapper) effects() ([]EffectOutput, error) {
    55  	if !operation.transaction.Result.Successful() {
    56  		return []EffectOutput{}, nil
    57  	}
    58  	var (
    59  		op  = operation.operation
    60  		err error
    61  	)
    62  
    63  	changes, err := operation.transaction.GetOperationChanges(operation.index)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	wrapper := &effectsWrapper{
    69  		effects:   []EffectOutput{},
    70  		operation: operation,
    71  	}
    72  
    73  	switch operation.OperationType() {
    74  	case xdr.OperationTypeCreateAccount:
    75  		wrapper.addAccountCreatedEffects()
    76  	case xdr.OperationTypePayment:
    77  		wrapper.addPaymentEffects()
    78  	case xdr.OperationTypePathPaymentStrictReceive:
    79  		err = wrapper.pathPaymentStrictReceiveEffects()
    80  	case xdr.OperationTypePathPaymentStrictSend:
    81  		err = wrapper.addPathPaymentStrictSendEffects()
    82  	case xdr.OperationTypeManageSellOffer:
    83  		err = wrapper.addManageSellOfferEffects()
    84  	case xdr.OperationTypeManageBuyOffer:
    85  		err = wrapper.addManageBuyOfferEffects()
    86  	case xdr.OperationTypeCreatePassiveSellOffer:
    87  		err = wrapper.addCreatePassiveSellOfferEffect()
    88  	case xdr.OperationTypeSetOptions:
    89  		wrapper.addSetOptionsEffects()
    90  	case xdr.OperationTypeChangeTrust:
    91  		err = wrapper.addChangeTrustEffects()
    92  	case xdr.OperationTypeAllowTrust:
    93  		err = wrapper.addAllowTrustEffects()
    94  	case xdr.OperationTypeAccountMerge:
    95  		wrapper.addAccountMergeEffects()
    96  	case xdr.OperationTypeInflation:
    97  		wrapper.addInflationEffects()
    98  	case xdr.OperationTypeManageData:
    99  		err = wrapper.addManageDataEffects()
   100  	case xdr.OperationTypeBumpSequence:
   101  		err = wrapper.addBumpSequenceEffects()
   102  	case xdr.OperationTypeCreateClaimableBalance:
   103  		err = wrapper.addCreateClaimableBalanceEffects(changes)
   104  	case xdr.OperationTypeClaimClaimableBalance:
   105  		err = wrapper.addClaimClaimableBalanceEffects(changes)
   106  	case xdr.OperationTypeBeginSponsoringFutureReserves, xdr.OperationTypeEndSponsoringFutureReserves, xdr.OperationTypeRevokeSponsorship:
   107  	// The effects of these operations are obtained  indirectly from the ledger entries
   108  	case xdr.OperationTypeClawback:
   109  		err = wrapper.addClawbackEffects()
   110  	case xdr.OperationTypeClawbackClaimableBalance:
   111  		err = wrapper.addClawbackClaimableBalanceEffects(changes)
   112  	case xdr.OperationTypeSetTrustLineFlags:
   113  		err = wrapper.addSetTrustLineFlagsEffects()
   114  	case xdr.OperationTypeLiquidityPoolDeposit:
   115  		err = wrapper.addLiquidityPoolDepositEffect()
   116  	case xdr.OperationTypeLiquidityPoolWithdraw:
   117  		err = wrapper.addLiquidityPoolWithdrawEffect()
   118  	case xdr.OperationTypeInvokeHostFunction:
   119  		// If there's an invokeHostFunction operation, there's definitely V3
   120  		// meta in the transaction, which means this error is real.
   121  		diagnosticEvents, innerErr := operation.transaction.GetDiagnosticEvents()
   122  		if innerErr != nil {
   123  			return nil, innerErr
   124  		}
   125  
   126  		// For now, the only effects are related to the events themselves.
   127  		// Possible add'l work: https://github.com/stellar/go/issues/4585
   128  		err = wrapper.addInvokeHostFunctionEffects(filterEvents(diagnosticEvents))
   129  	case xdr.OperationTypeExtendFootprintTtl:
   130  		err = wrapper.addExtendFootprintTtlEffect()
   131  	case xdr.OperationTypeRestoreFootprint:
   132  		err = wrapper.addRestoreFootprintExpirationEffect()
   133  	default:
   134  		return nil, fmt.Errorf("Unknown operation type: %s", op.Body.Type)
   135  	}
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  
   140  	// Effects generated for multiple operations. Keep the effect categories
   141  	// separated so they are "together" in case of different order or meta
   142  	// changes generate by core (unordered_map).
   143  
   144  	// Sponsorships
   145  	for _, change := range changes {
   146  		if err = wrapper.addLedgerEntrySponsorshipEffects(change); err != nil {
   147  			return nil, err
   148  		}
   149  		wrapper.addSignerSponsorshipEffects(change)
   150  	}
   151  
   152  	// Liquidity pools
   153  	for _, change := range changes {
   154  		// Effects caused by ChangeTrust (creation), AllowTrust and SetTrustlineFlags (removal through revocation)
   155  		wrapper.addLedgerEntryLiquidityPoolEffects(change)
   156  	}
   157  
   158  	for i := range wrapper.effects {
   159  		wrapper.effects[i].LedgerClosed = operation.ledgerClosed
   160  	}
   161  
   162  	return wrapper.effects, nil
   163  }
   164  
   165  type effectsWrapper struct {
   166  	effects   []EffectOutput
   167  	operation *transactionOperationWrapper
   168  }
   169  
   170  func (e *effectsWrapper) add(address string, addressMuxed null.String, effectType EffectType, details map[string]interface{}) {
   171  	e.effects = append(e.effects, EffectOutput{
   172  		Address:      address,
   173  		AddressMuxed: addressMuxed,
   174  		OperationID:  e.operation.ID(),
   175  		TypeString:   EffectTypeNames[effectType],
   176  		Type:         int32(effectType),
   177  		Details:      details,
   178  	})
   179  }
   180  
   181  func (e *effectsWrapper) addUnmuxed(address *xdr.AccountId, effectType EffectType, details map[string]interface{}) {
   182  	e.add(address.Address(), null.String{}, effectType, details)
   183  }
   184  
   185  func (e *effectsWrapper) addMuxed(address *xdr.MuxedAccount, effectType EffectType, details map[string]interface{}) {
   186  	var addressMuxed null.String
   187  	if address.Type == xdr.CryptoKeyTypeKeyTypeMuxedEd25519 {
   188  		addressMuxed = null.StringFrom(address.Address())
   189  	}
   190  	accID := address.ToAccountId()
   191  	e.add(accID.Address(), addressMuxed, effectType, details)
   192  }
   193  
   194  var sponsoringEffectsTable = map[xdr.LedgerEntryType]struct {
   195  	created, updated, removed EffectType
   196  }{
   197  	xdr.LedgerEntryTypeAccount: {
   198  		created: EffectAccountSponsorshipCreated,
   199  		updated: EffectAccountSponsorshipUpdated,
   200  		removed: EffectAccountSponsorshipRemoved,
   201  	},
   202  	xdr.LedgerEntryTypeTrustline: {
   203  		created: EffectTrustlineSponsorshipCreated,
   204  		updated: EffectTrustlineSponsorshipUpdated,
   205  		removed: EffectTrustlineSponsorshipRemoved,
   206  	},
   207  	xdr.LedgerEntryTypeData: {
   208  		created: EffectDataSponsorshipCreated,
   209  		updated: EffectDataSponsorshipUpdated,
   210  		removed: EffectDataSponsorshipRemoved,
   211  	},
   212  	xdr.LedgerEntryTypeClaimableBalance: {
   213  		created: EffectClaimableBalanceSponsorshipCreated,
   214  		updated: EffectClaimableBalanceSponsorshipUpdated,
   215  		removed: EffectClaimableBalanceSponsorshipRemoved,
   216  	},
   217  
   218  	// We intentionally don't have Sponsoring effects for Offer
   219  	// entries because we don't generate creation effects for them.
   220  }
   221  
   222  func (e *effectsWrapper) addSignerSponsorshipEffects(change ingest.Change) {
   223  	if change.Type != xdr.LedgerEntryTypeAccount {
   224  		return
   225  	}
   226  
   227  	preSigners := map[string]xdr.AccountId{}
   228  	postSigners := map[string]xdr.AccountId{}
   229  	if change.Pre != nil {
   230  		account := change.Pre.Data.MustAccount()
   231  		preSigners = account.SponsorPerSigner()
   232  	}
   233  	if change.Post != nil {
   234  		account := change.Post.Data.MustAccount()
   235  		postSigners = account.SponsorPerSigner()
   236  	}
   237  
   238  	var all []string
   239  	for signer := range preSigners {
   240  		all = append(all, signer)
   241  	}
   242  	for signer := range postSigners {
   243  		if _, ok := preSigners[signer]; ok {
   244  			continue
   245  		}
   246  		all = append(all, signer)
   247  	}
   248  	sort.Strings(all)
   249  
   250  	for _, signer := range all {
   251  		pre, foundPre := preSigners[signer]
   252  		post, foundPost := postSigners[signer]
   253  		details := map[string]interface{}{}
   254  
   255  		switch {
   256  		case !foundPre && !foundPost:
   257  			continue
   258  		case !foundPre && foundPost:
   259  			details["sponsor"] = post.Address()
   260  			details["signer"] = signer
   261  			srcAccount := change.Post.Data.MustAccount().AccountId
   262  			e.addUnmuxed(&srcAccount, EffectSignerSponsorshipCreated, details)
   263  		case !foundPost && foundPre:
   264  			details["former_sponsor"] = pre.Address()
   265  			details["signer"] = signer
   266  			srcAccount := change.Pre.Data.MustAccount().AccountId
   267  			e.addUnmuxed(&srcAccount, EffectSignerSponsorshipRemoved, details)
   268  		case foundPre && foundPost:
   269  			formerSponsor := pre.Address()
   270  			newSponsor := post.Address()
   271  			if formerSponsor == newSponsor {
   272  				continue
   273  			}
   274  
   275  			details["former_sponsor"] = formerSponsor
   276  			details["new_sponsor"] = newSponsor
   277  			details["signer"] = signer
   278  			srcAccount := change.Post.Data.MustAccount().AccountId
   279  			e.addUnmuxed(&srcAccount, EffectSignerSponsorshipUpdated, details)
   280  		}
   281  	}
   282  }
   283  
   284  func (e *effectsWrapper) addLedgerEntrySponsorshipEffects(change ingest.Change) error {
   285  	effectsForEntryType, found := sponsoringEffectsTable[change.Type]
   286  	if !found {
   287  		return nil
   288  	}
   289  
   290  	details := map[string]interface{}{}
   291  	var effectType EffectType
   292  
   293  	switch {
   294  	case (change.Pre == nil || change.Pre.SponsoringID() == nil) &&
   295  		(change.Post != nil && change.Post.SponsoringID() != nil):
   296  		effectType = effectsForEntryType.created
   297  		details["sponsor"] = (*change.Post.SponsoringID()).Address()
   298  	case (change.Pre != nil && change.Pre.SponsoringID() != nil) &&
   299  		(change.Post == nil || change.Post.SponsoringID() == nil):
   300  		effectType = effectsForEntryType.removed
   301  		details["former_sponsor"] = (*change.Pre.SponsoringID()).Address()
   302  	case (change.Pre != nil && change.Pre.SponsoringID() != nil) &&
   303  		(change.Post != nil && change.Post.SponsoringID() != nil):
   304  		preSponsor := (*change.Pre.SponsoringID()).Address()
   305  		postSponsor := (*change.Post.SponsoringID()).Address()
   306  		if preSponsor == postSponsor {
   307  			return nil
   308  		}
   309  		effectType = effectsForEntryType.updated
   310  		details["new_sponsor"] = postSponsor
   311  		details["former_sponsor"] = preSponsor
   312  	default:
   313  		return nil
   314  	}
   315  
   316  	var (
   317  		accountID    *xdr.AccountId
   318  		muxedAccount *xdr.MuxedAccount
   319  	)
   320  
   321  	var data xdr.LedgerEntryData
   322  	if change.Post != nil {
   323  		data = change.Post.Data
   324  	} else {
   325  		data = change.Pre.Data
   326  	}
   327  
   328  	switch change.Type {
   329  	case xdr.LedgerEntryTypeAccount:
   330  		a := data.MustAccount().AccountId
   331  		accountID = &a
   332  	case xdr.LedgerEntryTypeTrustline:
   333  		tl := data.MustTrustLine()
   334  		accountID = &tl.AccountId
   335  		if tl.Asset.Type == xdr.AssetTypeAssetTypePoolShare {
   336  			details["asset_type"] = "liquidity_pool"
   337  			details["liquidity_pool_id"] = PoolIDToString(*tl.Asset.LiquidityPoolId)
   338  		} else {
   339  			details["asset"] = tl.Asset.ToAsset().StringCanonical()
   340  		}
   341  	case xdr.LedgerEntryTypeData:
   342  		muxedAccount = e.operation.SourceAccount()
   343  		details["data_name"] = data.MustData().DataName
   344  	case xdr.LedgerEntryTypeClaimableBalance:
   345  		muxedAccount = e.operation.SourceAccount()
   346  		var err error
   347  		details["balance_id"], err = xdr.MarshalHex(data.MustClaimableBalance().BalanceId)
   348  		if err != nil {
   349  			return errors.Wrapf(err, "Invalid balanceId in change from op %d", e.operation.index)
   350  		}
   351  	case xdr.LedgerEntryTypeLiquidityPool:
   352  		// liquidity pools cannot be sponsored
   353  		fallthrough
   354  	default:
   355  		return errors.Errorf("invalid sponsorship ledger entry type %v", change.Type.String())
   356  	}
   357  
   358  	if accountID != nil {
   359  		e.addUnmuxed(accountID, effectType, details)
   360  	} else {
   361  		e.addMuxed(muxedAccount, effectType, details)
   362  	}
   363  
   364  	return nil
   365  }
   366  
   367  func (e *effectsWrapper) addLedgerEntryLiquidityPoolEffects(change ingest.Change) error {
   368  	if change.Type != xdr.LedgerEntryTypeLiquidityPool {
   369  		return nil
   370  	}
   371  	var effectType EffectType
   372  
   373  	var details map[string]interface{}
   374  	switch {
   375  	case change.Pre == nil && change.Post != nil:
   376  		effectType = EffectLiquidityPoolCreated
   377  		details = map[string]interface{}{
   378  			"liquidity_pool": liquidityPoolDetails(change.Post.Data.LiquidityPool),
   379  		}
   380  	case change.Pre != nil && change.Post == nil:
   381  		effectType = EffectLiquidityPoolRemoved
   382  		poolID := change.Pre.Data.LiquidityPool.LiquidityPoolId
   383  		details = map[string]interface{}{
   384  			"liquidity_pool_id": PoolIDToString(poolID),
   385  		}
   386  	default:
   387  		return nil
   388  	}
   389  	e.addMuxed(
   390  		e.operation.SourceAccount(),
   391  		effectType,
   392  		details,
   393  	)
   394  
   395  	return nil
   396  }
   397  
   398  func (e *effectsWrapper) addAccountCreatedEffects() {
   399  	op := e.operation.operation.Body.MustCreateAccountOp()
   400  
   401  	e.addUnmuxed(
   402  		&op.Destination,
   403  		EffectAccountCreated,
   404  		map[string]interface{}{
   405  			"starting_balance": amount.String(op.StartingBalance),
   406  		},
   407  	)
   408  	e.addMuxed(
   409  		e.operation.SourceAccount(),
   410  		EffectAccountDebited,
   411  		map[string]interface{}{
   412  			"asset_type": "native",
   413  			"amount":     amount.String(op.StartingBalance),
   414  		},
   415  	)
   416  	e.addUnmuxed(
   417  		&op.Destination,
   418  		EffectSignerCreated,
   419  		map[string]interface{}{
   420  			"public_key": op.Destination.Address(),
   421  			"weight":     keypair.DefaultSignerWeight,
   422  		},
   423  	)
   424  }
   425  
   426  func (e *effectsWrapper) addPaymentEffects() {
   427  	op := e.operation.operation.Body.MustPaymentOp()
   428  
   429  	details := map[string]interface{}{"amount": amount.String(op.Amount)}
   430  	addAssetDetails(details, op.Asset, "")
   431  
   432  	e.addMuxed(
   433  		&op.Destination,
   434  		EffectAccountCredited,
   435  		details,
   436  	)
   437  	e.addMuxed(
   438  		e.operation.SourceAccount(),
   439  		EffectAccountDebited,
   440  		details,
   441  	)
   442  }
   443  
   444  func (e *effectsWrapper) pathPaymentStrictReceiveEffects() error {
   445  	op := e.operation.operation.Body.MustPathPaymentStrictReceiveOp()
   446  	resultSuccess := e.operation.OperationResult().MustPathPaymentStrictReceiveResult().MustSuccess()
   447  	source := e.operation.SourceAccount()
   448  
   449  	details := map[string]interface{}{"amount": amount.String(op.DestAmount)}
   450  	addAssetDetails(details, op.DestAsset, "")
   451  
   452  	e.addMuxed(
   453  		&op.Destination,
   454  		EffectAccountCredited,
   455  		details,
   456  	)
   457  
   458  	result := e.operation.OperationResult().MustPathPaymentStrictReceiveResult()
   459  	details = map[string]interface{}{"amount": amount.String(result.SendAmount())}
   460  	addAssetDetails(details, op.SendAsset, "")
   461  
   462  	e.addMuxed(
   463  		source,
   464  		EffectAccountDebited,
   465  		details,
   466  	)
   467  
   468  	return e.addIngestTradeEffects(*source, resultSuccess.Offers, false)
   469  }
   470  
   471  func (e *effectsWrapper) addPathPaymentStrictSendEffects() error {
   472  	source := e.operation.SourceAccount()
   473  	op := e.operation.operation.Body.MustPathPaymentStrictSendOp()
   474  	resultSuccess := e.operation.OperationResult().MustPathPaymentStrictSendResult().MustSuccess()
   475  	result := e.operation.OperationResult().MustPathPaymentStrictSendResult()
   476  
   477  	details := map[string]interface{}{"amount": amount.String(result.DestAmount())}
   478  	addAssetDetails(details, op.DestAsset, "")
   479  	e.addMuxed(&op.Destination, EffectAccountCredited, details)
   480  
   481  	details = map[string]interface{}{"amount": amount.String(op.SendAmount)}
   482  	addAssetDetails(details, op.SendAsset, "")
   483  	e.addMuxed(source, EffectAccountDebited, details)
   484  
   485  	return e.addIngestTradeEffects(*source, resultSuccess.Offers, true)
   486  }
   487  
   488  func (e *effectsWrapper) addManageSellOfferEffects() error {
   489  	source := e.operation.SourceAccount()
   490  	result := e.operation.OperationResult().MustManageSellOfferResult().MustSuccess()
   491  	return e.addIngestTradeEffects(*source, result.OffersClaimed, false)
   492  }
   493  
   494  func (e *effectsWrapper) addManageBuyOfferEffects() error {
   495  	source := e.operation.SourceAccount()
   496  	result := e.operation.OperationResult().MustManageBuyOfferResult().MustSuccess()
   497  	return e.addIngestTradeEffects(*source, result.OffersClaimed, false)
   498  }
   499  
   500  func (e *effectsWrapper) addCreatePassiveSellOfferEffect() error {
   501  	result := e.operation.OperationResult()
   502  	source := e.operation.SourceAccount()
   503  
   504  	var claims []xdr.ClaimAtom
   505  
   506  	// KNOWN ISSUE:  stellar-core creates results for CreatePassiveOffer operations
   507  	// with the wrong result arm set.
   508  	if result.Type == xdr.OperationTypeManageSellOffer {
   509  		claims = result.MustManageSellOfferResult().MustSuccess().OffersClaimed
   510  	} else {
   511  		claims = result.MustCreatePassiveSellOfferResult().MustSuccess().OffersClaimed
   512  	}
   513  
   514  	return e.addIngestTradeEffects(*source, claims, false)
   515  }
   516  
   517  func (e *effectsWrapper) addSetOptionsEffects() error {
   518  	source := e.operation.SourceAccount()
   519  	op := e.operation.operation.Body.MustSetOptionsOp()
   520  
   521  	if op.HomeDomain != nil {
   522  		e.addMuxed(source, EffectAccountHomeDomainUpdated,
   523  			map[string]interface{}{
   524  				"home_domain": string(*op.HomeDomain),
   525  			},
   526  		)
   527  	}
   528  
   529  	thresholdDetails := map[string]interface{}{}
   530  
   531  	if op.LowThreshold != nil {
   532  		thresholdDetails["low_threshold"] = *op.LowThreshold
   533  	}
   534  
   535  	if op.MedThreshold != nil {
   536  		thresholdDetails["med_threshold"] = *op.MedThreshold
   537  	}
   538  
   539  	if op.HighThreshold != nil {
   540  		thresholdDetails["high_threshold"] = *op.HighThreshold
   541  	}
   542  
   543  	if len(thresholdDetails) > 0 {
   544  		e.addMuxed(source, EffectAccountThresholdsUpdated, thresholdDetails)
   545  	}
   546  
   547  	flagDetails := map[string]interface{}{}
   548  	if op.SetFlags != nil {
   549  		setAuthFlagDetails(flagDetails, xdr.AccountFlags(*op.SetFlags), true)
   550  	}
   551  	if op.ClearFlags != nil {
   552  		setAuthFlagDetails(flagDetails, xdr.AccountFlags(*op.ClearFlags), false)
   553  	}
   554  
   555  	if len(flagDetails) > 0 {
   556  		e.addMuxed(source, EffectAccountFlagsUpdated, flagDetails)
   557  	}
   558  
   559  	if op.InflationDest != nil {
   560  		e.addMuxed(source, EffectAccountInflationDestinationUpdated,
   561  			map[string]interface{}{
   562  				"inflation_destination": op.InflationDest.Address(),
   563  			},
   564  		)
   565  	}
   566  	changes, err := e.operation.transaction.GetOperationChanges(e.operation.index)
   567  	if err != nil {
   568  		return err
   569  	}
   570  
   571  	for _, change := range changes {
   572  		if change.Type != xdr.LedgerEntryTypeAccount {
   573  			continue
   574  		}
   575  
   576  		beforeAccount := change.Pre.Data.MustAccount()
   577  		afterAccount := change.Post.Data.MustAccount()
   578  
   579  		before := beforeAccount.SignerSummary()
   580  		after := afterAccount.SignerSummary()
   581  
   582  		// if before and after are the same, the signers have not changed
   583  		if reflect.DeepEqual(before, after) {
   584  			continue
   585  		}
   586  
   587  		beforeSortedSigners := []string{}
   588  		for signer := range before {
   589  			beforeSortedSigners = append(beforeSortedSigners, signer)
   590  		}
   591  		sort.Strings(beforeSortedSigners)
   592  
   593  		for _, addy := range beforeSortedSigners {
   594  			weight, ok := after[addy]
   595  			if !ok {
   596  				e.addMuxed(source, EffectSignerRemoved, map[string]interface{}{
   597  					"public_key": addy,
   598  				})
   599  				continue
   600  			}
   601  
   602  			if weight != before[addy] {
   603  				e.addMuxed(source, EffectSignerUpdated, map[string]interface{}{
   604  					"public_key": addy,
   605  					"weight":     weight,
   606  				})
   607  			}
   608  		}
   609  
   610  		afterSortedSigners := []string{}
   611  		for signer := range after {
   612  			afterSortedSigners = append(afterSortedSigners, signer)
   613  		}
   614  		sort.Strings(afterSortedSigners)
   615  
   616  		// Add the "created" effects
   617  		for _, addy := range afterSortedSigners {
   618  			weight := after[addy]
   619  			// if `addy` is in before, the previous for loop should have recorded
   620  			// the update, so skip this key
   621  			if _, ok := before[addy]; ok {
   622  				continue
   623  			}
   624  
   625  			e.addMuxed(source, EffectSignerCreated, map[string]interface{}{
   626  				"public_key": addy,
   627  				"weight":     weight,
   628  			})
   629  		}
   630  	}
   631  	return nil
   632  }
   633  
   634  func (e *effectsWrapper) addChangeTrustEffects() error {
   635  	source := e.operation.SourceAccount()
   636  
   637  	op := e.operation.operation.Body.MustChangeTrustOp()
   638  	changes, err := e.operation.transaction.GetOperationChanges(e.operation.index)
   639  	if err != nil {
   640  		return err
   641  	}
   642  
   643  	// NOTE:  when an account trusts itself, the transaction is successful but
   644  	// no ledger entries are actually modified.
   645  	for _, change := range changes {
   646  		if change.Type != xdr.LedgerEntryTypeTrustline {
   647  			continue
   648  		}
   649  
   650  		var (
   651  			effect    EffectType
   652  			trustLine xdr.TrustLineEntry
   653  		)
   654  
   655  		switch {
   656  		case change.Pre == nil && change.Post != nil:
   657  			effect = EffectTrustlineCreated
   658  			trustLine = *change.Post.Data.TrustLine
   659  		case change.Pre != nil && change.Post == nil:
   660  			effect = EffectTrustlineRemoved
   661  			trustLine = *change.Pre.Data.TrustLine
   662  		case change.Pre != nil && change.Post != nil:
   663  			effect = EffectTrustlineUpdated
   664  			trustLine = *change.Post.Data.TrustLine
   665  		default:
   666  			panic("Invalid change")
   667  		}
   668  
   669  		// We want to add a single effect for change_trust op. If it's modifying
   670  		// credit_asset search for credit_asset trustline, otherwise search for
   671  		// liquidity_pool.
   672  		if op.Line.Type != trustLine.Asset.Type {
   673  			continue
   674  		}
   675  
   676  		details := map[string]interface{}{"limit": amount.String(op.Limit)}
   677  		if trustLine.Asset.Type == xdr.AssetTypeAssetTypePoolShare {
   678  			// The only change_trust ops that can modify LP are those with
   679  			// asset=liquidity_pool so *op.Line.LiquidityPool below is available.
   680  			if err := addLiquidityPoolAssetDetails(details, *op.Line.LiquidityPool); err != nil {
   681  				return err
   682  			}
   683  		} else {
   684  			addAssetDetails(details, op.Line.ToAsset(), "")
   685  		}
   686  
   687  		e.addMuxed(source, effect, details)
   688  		break
   689  	}
   690  
   691  	return nil
   692  }
   693  
   694  func (e *effectsWrapper) addAllowTrustEffects() error {
   695  	source := e.operation.SourceAccount()
   696  	op := e.operation.operation.Body.MustAllowTrustOp()
   697  	asset := op.Asset.ToAsset(source.ToAccountId())
   698  	details := map[string]interface{}{
   699  		"trustor": op.Trustor.Address(),
   700  	}
   701  	addAssetDetails(details, asset, "")
   702  
   703  	switch {
   704  	case xdr.TrustLineFlags(op.Authorize).IsAuthorized():
   705  		e.addMuxed(source, EffectTrustlineFlagsUpdated, details)
   706  		// Forward compatibility
   707  		setFlags := xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag)
   708  		e.addTrustLineFlagsEffect(source, &op.Trustor, asset, &setFlags, nil)
   709  	case xdr.TrustLineFlags(op.Authorize).IsAuthorizedToMaintainLiabilitiesFlag():
   710  		e.addMuxed(
   711  			source,
   712  			EffectTrustlineFlagsUpdated,
   713  			details,
   714  		)
   715  		// Forward compatibility
   716  		setFlags := xdr.Uint32(xdr.TrustLineFlagsAuthorizedToMaintainLiabilitiesFlag)
   717  		e.addTrustLineFlagsEffect(source, &op.Trustor, asset, &setFlags, nil)
   718  	default:
   719  		e.addMuxed(source, EffectTrustlineFlagsUpdated, details)
   720  		// Forward compatibility, show both as cleared
   721  		clearFlags := xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag | xdr.TrustLineFlagsAuthorizedToMaintainLiabilitiesFlag)
   722  		e.addTrustLineFlagsEffect(source, &op.Trustor, asset, nil, &clearFlags)
   723  	}
   724  	return e.addLiquidityPoolRevokedEffect()
   725  }
   726  
   727  func (e *effectsWrapper) addAccountMergeEffects() {
   728  	source := e.operation.SourceAccount()
   729  
   730  	dest := e.operation.operation.Body.MustDestination()
   731  	result := e.operation.OperationResult().MustAccountMergeResult()
   732  	details := map[string]interface{}{
   733  		"amount":     amount.String(result.MustSourceAccountBalance()),
   734  		"asset_type": "native",
   735  	}
   736  
   737  	e.addMuxed(source, EffectAccountDebited, details)
   738  	e.addMuxed(&dest, EffectAccountCredited, details)
   739  	e.addMuxed(source, EffectAccountRemoved, map[string]interface{}{})
   740  }
   741  
   742  func (e *effectsWrapper) addInflationEffects() {
   743  	payouts := e.operation.OperationResult().MustInflationResult().MustPayouts()
   744  	for _, payout := range payouts {
   745  		e.addUnmuxed(&payout.Destination, EffectAccountCredited,
   746  			map[string]interface{}{
   747  				"amount":     amount.String(payout.Amount),
   748  				"asset_type": "native",
   749  			},
   750  		)
   751  	}
   752  }
   753  
   754  func (e *effectsWrapper) addManageDataEffects() error {
   755  	source := e.operation.SourceAccount()
   756  	op := e.operation.operation.Body.MustManageDataOp()
   757  	details := map[string]interface{}{"name": op.DataName}
   758  	effect := EffectType(0)
   759  	changes, err := e.operation.transaction.GetOperationChanges(e.operation.index)
   760  	if err != nil {
   761  		return err
   762  	}
   763  
   764  	for _, change := range changes {
   765  		if change.Type != xdr.LedgerEntryTypeData {
   766  			continue
   767  		}
   768  
   769  		before := change.Pre
   770  		after := change.Post
   771  
   772  		if after != nil {
   773  			raw := after.Data.MustData().DataValue
   774  			details["value"] = base64.StdEncoding.EncodeToString(raw)
   775  		}
   776  
   777  		switch {
   778  		case before == nil && after != nil:
   779  			effect = EffectDataCreated
   780  		case before != nil && after == nil:
   781  			effect = EffectDataRemoved
   782  		case before != nil && after != nil:
   783  			effect = EffectDataUpdated
   784  		default:
   785  			panic("Invalid before-and-after state")
   786  		}
   787  
   788  		break
   789  	}
   790  
   791  	e.addMuxed(source, effect, details)
   792  	return nil
   793  }
   794  
   795  func (e *effectsWrapper) addBumpSequenceEffects() error {
   796  	source := e.operation.SourceAccount()
   797  	changes, err := e.operation.transaction.GetOperationChanges(e.operation.index)
   798  	if err != nil {
   799  		return err
   800  	}
   801  
   802  	for _, change := range changes {
   803  		if change.Type != xdr.LedgerEntryTypeAccount {
   804  			continue
   805  		}
   806  
   807  		before := change.Pre
   808  		after := change.Post
   809  
   810  		beforeAccount := before.Data.MustAccount()
   811  		afterAccount := after.Data.MustAccount()
   812  
   813  		if beforeAccount.SeqNum != afterAccount.SeqNum {
   814  			details := map[string]interface{}{"new_seq": afterAccount.SeqNum}
   815  			e.addMuxed(source, EffectSequenceBumped, details)
   816  		}
   817  		break
   818  	}
   819  
   820  	return nil
   821  }
   822  
   823  func setClaimableBalanceFlagDetails(details map[string]interface{}, flags xdr.ClaimableBalanceFlags) {
   824  	if flags.IsClawbackEnabled() {
   825  		details["claimable_balance_clawback_enabled_flag"] = true
   826  		return
   827  	}
   828  }
   829  
   830  func (e *effectsWrapper) addCreateClaimableBalanceEffects(changes []ingest.Change) error {
   831  	source := e.operation.SourceAccount()
   832  	var cb *xdr.ClaimableBalanceEntry
   833  	for _, change := range changes {
   834  		if change.Type != xdr.LedgerEntryTypeClaimableBalance || change.Post == nil {
   835  			continue
   836  		}
   837  		cb = change.Post.Data.ClaimableBalance
   838  		e.addClaimableBalanceEntryCreatedEffects(source, cb)
   839  		break
   840  	}
   841  	if cb == nil {
   842  		return errors.New("claimable balance entry not found")
   843  	}
   844  
   845  	details := map[string]interface{}{
   846  		"amount": amount.String(cb.Amount),
   847  	}
   848  	addAssetDetails(details, cb.Asset, "")
   849  	e.addMuxed(
   850  		source,
   851  		EffectAccountDebited,
   852  		details,
   853  	)
   854  
   855  	return nil
   856  }
   857  
   858  func (e *effectsWrapper) addClaimableBalanceEntryCreatedEffects(source *xdr.MuxedAccount, cb *xdr.ClaimableBalanceEntry) error {
   859  	id, err := xdr.MarshalHex(cb.BalanceId)
   860  	if err != nil {
   861  		return err
   862  	}
   863  	details := map[string]interface{}{
   864  		"balance_id": id,
   865  		"amount":     amount.String(cb.Amount),
   866  		"asset":      cb.Asset.StringCanonical(),
   867  	}
   868  	setClaimableBalanceFlagDetails(details, cb.Flags())
   869  	e.addMuxed(
   870  		source,
   871  		EffectClaimableBalanceCreated,
   872  		details,
   873  	)
   874  	// EffectClaimableBalanceClaimantCreated can be generated by
   875  	// `create_claimable_balance` operation but also by `liquidity_pool_withdraw`
   876  	// operation causing a revocation.
   877  	// In case of `create_claimable_balance` we use `op.Claimants` to make
   878  	// effects backward compatible. The reason for this is that Stellar-Core
   879  	// changes all `rel_before` predicated to `abs_before` when tx is included
   880  	// in the ledger.
   881  	var claimants []xdr.Claimant
   882  	if op, ok := e.operation.operation.Body.GetCreateClaimableBalanceOp(); ok {
   883  		claimants = op.Claimants
   884  	} else {
   885  		claimants = cb.Claimants
   886  	}
   887  	for _, c := range claimants {
   888  		cv0 := c.MustV0()
   889  		e.addUnmuxed(
   890  			&cv0.Destination,
   891  			EffectClaimableBalanceClaimantCreated,
   892  			map[string]interface{}{
   893  				"balance_id": id,
   894  				"amount":     amount.String(cb.Amount),
   895  				"predicate":  cv0.Predicate,
   896  				"asset":      cb.Asset.StringCanonical(),
   897  			},
   898  		)
   899  	}
   900  	return err
   901  }
   902  
   903  func (e *effectsWrapper) addClaimClaimableBalanceEffects(changes []ingest.Change) error {
   904  	op := e.operation.operation.Body.MustClaimClaimableBalanceOp()
   905  
   906  	balanceID, err := xdr.MarshalHex(op.BalanceId)
   907  	if err != nil {
   908  		return fmt.Errorf("Invalid balanceId in op: %d", e.operation.index)
   909  	}
   910  
   911  	var cBalance xdr.ClaimableBalanceEntry
   912  	found := false
   913  	for _, change := range changes {
   914  		if change.Type != xdr.LedgerEntryTypeClaimableBalance {
   915  			continue
   916  		}
   917  
   918  		if change.Pre != nil && change.Post == nil {
   919  			cBalance = change.Pre.Data.MustClaimableBalance()
   920  			preBalanceID, err := xdr.MarshalHex(cBalance.BalanceId)
   921  			if err != nil {
   922  				return fmt.Errorf("Invalid balanceId in meta changes for op: %d", e.operation.index)
   923  			}
   924  
   925  			if preBalanceID == balanceID {
   926  				found = true
   927  				break
   928  			}
   929  		}
   930  	}
   931  
   932  	if !found {
   933  		return fmt.Errorf("Change not found for balanceId : %s", balanceID)
   934  	}
   935  
   936  	details := map[string]interface{}{
   937  		"amount":     amount.String(cBalance.Amount),
   938  		"balance_id": balanceID,
   939  		"asset":      cBalance.Asset.StringCanonical(),
   940  	}
   941  	setClaimableBalanceFlagDetails(details, cBalance.Flags())
   942  	source := e.operation.SourceAccount()
   943  	e.addMuxed(
   944  		source,
   945  		EffectClaimableBalanceClaimed,
   946  		details,
   947  	)
   948  
   949  	details = map[string]interface{}{
   950  		"amount": amount.String(cBalance.Amount),
   951  	}
   952  	addAssetDetails(details, cBalance.Asset, "")
   953  	e.addMuxed(
   954  		source,
   955  		EffectAccountCredited,
   956  		details,
   957  	)
   958  
   959  	return nil
   960  }
   961  
   962  func (e *effectsWrapper) addIngestTradeEffects(buyer xdr.MuxedAccount, claims []xdr.ClaimAtom, isPathPayment bool) error {
   963  	for _, claim := range claims {
   964  		if claim.AmountSold() == 0 && claim.AmountBought() == 0 {
   965  			continue
   966  		}
   967  		switch claim.Type {
   968  		case xdr.ClaimAtomTypeClaimAtomTypeLiquidityPool:
   969  			if err := e.addClaimLiquidityPoolTradeEffect(claim); err != nil {
   970  				return err
   971  			}
   972  		default:
   973  			e.addClaimTradeEffects(buyer, claim, isPathPayment)
   974  		}
   975  	}
   976  	return nil
   977  }
   978  
   979  func (e *effectsWrapper) addClaimTradeEffects(buyer xdr.MuxedAccount, claim xdr.ClaimAtom, isPathPayment bool) {
   980  	seller := claim.SellerId()
   981  	bd, sd := tradeDetails(buyer, seller, claim)
   982  
   983  	tradeEffects := []EffectType{
   984  		EffectTrade,
   985  		EffectOfferUpdated,
   986  		EffectOfferRemoved,
   987  		EffectOfferCreated,
   988  	}
   989  
   990  	for n, effect := range tradeEffects {
   991  		// skip EffectOfferCreated if OperationType is path_payment
   992  		if n == 3 && isPathPayment {
   993  			continue
   994  		}
   995  
   996  		e.addMuxed(
   997  			&buyer,
   998  			effect,
   999  			bd,
  1000  		)
  1001  
  1002  		e.addUnmuxed(
  1003  			&seller,
  1004  			effect,
  1005  			sd,
  1006  		)
  1007  	}
  1008  }
  1009  
  1010  func (e *effectsWrapper) addClaimLiquidityPoolTradeEffect(claim xdr.ClaimAtom) error {
  1011  	lp, _, err := e.operation.getLiquidityPoolAndProductDelta(&claim.LiquidityPool.LiquidityPoolId)
  1012  	if err != nil {
  1013  		return err
  1014  	}
  1015  	details := map[string]interface{}{
  1016  		"liquidity_pool": liquidityPoolDetails(lp),
  1017  		"sold": map[string]string{
  1018  			"asset":  claim.LiquidityPool.AssetSold.StringCanonical(),
  1019  			"amount": amount.String(claim.LiquidityPool.AmountSold),
  1020  		},
  1021  		"bought": map[string]string{
  1022  			"asset":  claim.LiquidityPool.AssetBought.StringCanonical(),
  1023  			"amount": amount.String(claim.LiquidityPool.AmountBought),
  1024  		},
  1025  	}
  1026  	e.addMuxed(e.operation.SourceAccount(), EffectLiquidityPoolTrade, details)
  1027  	return nil
  1028  }
  1029  
  1030  func (e *effectsWrapper) addClawbackEffects() error {
  1031  	op := e.operation.operation.Body.MustClawbackOp()
  1032  	details := map[string]interface{}{
  1033  		"amount": amount.String(op.Amount),
  1034  	}
  1035  	source := e.operation.SourceAccount()
  1036  	addAssetDetails(details, op.Asset, "")
  1037  
  1038  	// The funds will be burned, but even with that, we generated an account credited effect
  1039  	e.addMuxed(
  1040  		source,
  1041  		EffectAccountCredited,
  1042  		details,
  1043  	)
  1044  
  1045  	e.addMuxed(
  1046  		&op.From,
  1047  		EffectAccountDebited,
  1048  		details,
  1049  	)
  1050  
  1051  	return nil
  1052  }
  1053  
  1054  func (e *effectsWrapper) addClawbackClaimableBalanceEffects(changes []ingest.Change) error {
  1055  	op := e.operation.operation.Body.MustClawbackClaimableBalanceOp()
  1056  	balanceId, err := xdr.MarshalHex(op.BalanceId)
  1057  	if err != nil {
  1058  		return errors.Wrapf(err, "Invalid balanceId in op %d", e.operation.index)
  1059  	}
  1060  	details := map[string]interface{}{
  1061  		"balance_id": balanceId,
  1062  	}
  1063  	source := e.operation.SourceAccount()
  1064  	e.addMuxed(
  1065  		source,
  1066  		EffectClaimableBalanceClawedBack,
  1067  		details,
  1068  	)
  1069  
  1070  	// Generate the account credited effect (although the funds will be burned) for the asset issuer
  1071  	for _, c := range changes {
  1072  		if c.Type == xdr.LedgerEntryTypeClaimableBalance && c.Post == nil && c.Pre != nil {
  1073  			cb := c.Pre.Data.ClaimableBalance
  1074  			details = map[string]interface{}{"amount": amount.String(cb.Amount)}
  1075  			addAssetDetails(details, cb.Asset, "")
  1076  			e.addMuxed(
  1077  				source,
  1078  				EffectAccountCredited,
  1079  				details,
  1080  			)
  1081  			break
  1082  		}
  1083  	}
  1084  
  1085  	return nil
  1086  }
  1087  
  1088  func (e *effectsWrapper) addSetTrustLineFlagsEffects() error {
  1089  	source := e.operation.SourceAccount()
  1090  	op := e.operation.operation.Body.MustSetTrustLineFlagsOp()
  1091  	e.addTrustLineFlagsEffect(source, &op.Trustor, op.Asset, &op.SetFlags, &op.ClearFlags)
  1092  	return e.addLiquidityPoolRevokedEffect()
  1093  }
  1094  
  1095  func (e *effectsWrapper) addTrustLineFlagsEffect(
  1096  	account *xdr.MuxedAccount,
  1097  	trustor *xdr.AccountId,
  1098  	asset xdr.Asset,
  1099  	setFlags *xdr.Uint32,
  1100  	clearFlags *xdr.Uint32) {
  1101  	details := map[string]interface{}{
  1102  		"trustor": trustor.Address(),
  1103  	}
  1104  	addAssetDetails(details, asset, "")
  1105  
  1106  	var flagDetailsAdded bool
  1107  	if setFlags != nil {
  1108  		setTrustLineFlagDetails(details, xdr.TrustLineFlags(*setFlags), true)
  1109  		flagDetailsAdded = true
  1110  	}
  1111  	if clearFlags != nil {
  1112  		setTrustLineFlagDetails(details, xdr.TrustLineFlags(*clearFlags), false)
  1113  		flagDetailsAdded = true
  1114  	}
  1115  
  1116  	if flagDetailsAdded {
  1117  		e.addMuxed(account, EffectTrustlineFlagsUpdated, details)
  1118  	}
  1119  }
  1120  
  1121  func setTrustLineFlagDetails(flagDetails map[string]interface{}, flags xdr.TrustLineFlags, setValue bool) {
  1122  	if flags.IsAuthorized() {
  1123  		flagDetails["authorized_flag"] = setValue
  1124  	}
  1125  	if flags.IsAuthorizedToMaintainLiabilitiesFlag() {
  1126  		flagDetails["authorized_to_maintain_liabilites"] = setValue
  1127  	}
  1128  	if flags.IsClawbackEnabledFlag() {
  1129  		flagDetails["clawback_enabled_flag"] = setValue
  1130  	}
  1131  }
  1132  
  1133  type sortableClaimableBalanceEntries []*xdr.ClaimableBalanceEntry
  1134  
  1135  func (s sortableClaimableBalanceEntries) Len() int           { return len(s) }
  1136  func (s sortableClaimableBalanceEntries) Less(i, j int) bool { return s[i].Asset.LessThan(s[j].Asset) }
  1137  func (s sortableClaimableBalanceEntries) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
  1138  
  1139  func (e *effectsWrapper) addLiquidityPoolRevokedEffect() error {
  1140  	source := e.operation.SourceAccount()
  1141  	lp, delta, err := e.operation.getLiquidityPoolAndProductDelta(nil)
  1142  	if err != nil {
  1143  		if err == errLiquidityPoolChangeNotFound {
  1144  			// no revocation happened
  1145  			return nil
  1146  		}
  1147  		return err
  1148  	}
  1149  	changes, err := e.operation.transaction.GetOperationChanges(e.operation.index)
  1150  	if err != nil {
  1151  		return err
  1152  	}
  1153  	assetToCBID := map[string]string{}
  1154  	var cbs sortableClaimableBalanceEntries
  1155  	for _, change := range changes {
  1156  		if change.Type == xdr.LedgerEntryTypeClaimableBalance && change.Pre == nil && change.Post != nil {
  1157  			cb := change.Post.Data.ClaimableBalance
  1158  			id, err := xdr.MarshalHex(cb.BalanceId)
  1159  			if err != nil {
  1160  				return err
  1161  			}
  1162  			assetToCBID[cb.Asset.StringCanonical()] = id
  1163  			cbs = append(cbs, cb)
  1164  		}
  1165  	}
  1166  	if len(assetToCBID) == 0 {
  1167  		// no claimable balances were created, and thus, no revocation happened
  1168  		return nil
  1169  	}
  1170  	// Core's claimable balance metadata isn't ordered, so we order it ourselves
  1171  	// so that effects are ordered consistently
  1172  	sort.Sort(cbs)
  1173  	for _, cb := range cbs {
  1174  		if err := e.addClaimableBalanceEntryCreatedEffects(source, cb); err != nil {
  1175  			return err
  1176  		}
  1177  	}
  1178  
  1179  	reservesRevoked := make([]map[string]string, 0, 2)
  1180  	for _, aa := range []base.AssetAmount{
  1181  		{
  1182  			Asset:  lp.Body.ConstantProduct.Params.AssetA.StringCanonical(),
  1183  			Amount: amount.String(-delta.ReserveA),
  1184  		},
  1185  		{
  1186  			Asset:  lp.Body.ConstantProduct.Params.AssetB.StringCanonical(),
  1187  			Amount: amount.String(-delta.ReserveB),
  1188  		},
  1189  	} {
  1190  		if cbID, ok := assetToCBID[aa.Asset]; ok {
  1191  			assetAmountDetail := map[string]string{
  1192  				"asset":                aa.Asset,
  1193  				"amount":               aa.Amount,
  1194  				"claimable_balance_id": cbID,
  1195  			}
  1196  			reservesRevoked = append(reservesRevoked, assetAmountDetail)
  1197  		}
  1198  	}
  1199  	details := map[string]interface{}{
  1200  		"liquidity_pool":   liquidityPoolDetails(lp),
  1201  		"reserves_revoked": reservesRevoked,
  1202  		"shares_revoked":   amount.String(-delta.TotalPoolShares),
  1203  	}
  1204  	e.addMuxed(source, EffectLiquidityPoolRevoked, details)
  1205  	return nil
  1206  }
  1207  
  1208  func setAuthFlagDetails(flagDetails map[string]interface{}, flags xdr.AccountFlags, setValue bool) {
  1209  	if flags.IsAuthRequired() {
  1210  		flagDetails["auth_required_flag"] = setValue
  1211  	}
  1212  	if flags.IsAuthRevocable() {
  1213  		flagDetails["auth_revocable_flag"] = setValue
  1214  	}
  1215  	if flags.IsAuthImmutable() {
  1216  		flagDetails["auth_immutable_flag"] = setValue
  1217  	}
  1218  	if flags.IsAuthClawbackEnabled() {
  1219  		flagDetails["auth_clawback_enabled_flag"] = setValue
  1220  	}
  1221  }
  1222  
  1223  func tradeDetails(buyer xdr.MuxedAccount, seller xdr.AccountId, claim xdr.ClaimAtom) (bd map[string]interface{}, sd map[string]interface{}) {
  1224  	bd = map[string]interface{}{
  1225  		"offer_id":      claim.OfferId(),
  1226  		"seller":        seller.Address(),
  1227  		"bought_amount": amount.String(claim.AmountSold()),
  1228  		"sold_amount":   amount.String(claim.AmountBought()),
  1229  	}
  1230  	addAssetDetails(bd, claim.AssetSold(), "bought_")
  1231  	addAssetDetails(bd, claim.AssetBought(), "sold_")
  1232  
  1233  	sd = map[string]interface{}{
  1234  		"offer_id":      claim.OfferId(),
  1235  		"bought_amount": amount.String(claim.AmountBought()),
  1236  		"sold_amount":   amount.String(claim.AmountSold()),
  1237  	}
  1238  	addAccountAndMuxedAccountDetails(sd, buyer, "seller")
  1239  	addAssetDetails(sd, claim.AssetBought(), "bought_")
  1240  	addAssetDetails(sd, claim.AssetSold(), "sold_")
  1241  
  1242  	return
  1243  }
  1244  
  1245  func liquidityPoolDetails(lp *xdr.LiquidityPoolEntry) map[string]interface{} {
  1246  	return map[string]interface{}{
  1247  		"id":               PoolIDToString(lp.LiquidityPoolId),
  1248  		"fee_bp":           uint32(lp.Body.ConstantProduct.Params.Fee),
  1249  		"type":             "constant_product",
  1250  		"total_trustlines": strconv.FormatInt(int64(lp.Body.ConstantProduct.PoolSharesTrustLineCount), 10),
  1251  		"total_shares":     amount.String(lp.Body.ConstantProduct.TotalPoolShares),
  1252  		"reserves": []base.AssetAmount{
  1253  			{
  1254  				Asset:  lp.Body.ConstantProduct.Params.AssetA.StringCanonical(),
  1255  				Amount: amount.String(lp.Body.ConstantProduct.ReserveA),
  1256  			},
  1257  			{
  1258  				Asset:  lp.Body.ConstantProduct.Params.AssetB.StringCanonical(),
  1259  				Amount: amount.String(lp.Body.ConstantProduct.ReserveB),
  1260  			},
  1261  		},
  1262  	}
  1263  }
  1264  
  1265  func (e *effectsWrapper) addLiquidityPoolDepositEffect() error {
  1266  	op := e.operation.operation.Body.MustLiquidityPoolDepositOp()
  1267  	lp, delta, err := e.operation.getLiquidityPoolAndProductDelta(&op.LiquidityPoolId)
  1268  	if err != nil {
  1269  		return err
  1270  	}
  1271  	details := map[string]interface{}{
  1272  		"liquidity_pool": liquidityPoolDetails(lp),
  1273  		"reserves_deposited": []base.AssetAmount{
  1274  			{
  1275  				Asset:  lp.Body.ConstantProduct.Params.AssetA.StringCanonical(),
  1276  				Amount: amount.String(delta.ReserveA),
  1277  			},
  1278  			{
  1279  				Asset:  lp.Body.ConstantProduct.Params.AssetB.StringCanonical(),
  1280  				Amount: amount.String(delta.ReserveB),
  1281  			},
  1282  		},
  1283  		"shares_received": amount.String(delta.TotalPoolShares),
  1284  	}
  1285  	e.addMuxed(e.operation.SourceAccount(), EffectLiquidityPoolDeposited, details)
  1286  	return nil
  1287  }
  1288  
  1289  func (e *effectsWrapper) addLiquidityPoolWithdrawEffect() error {
  1290  	op := e.operation.operation.Body.MustLiquidityPoolWithdrawOp()
  1291  	lp, delta, err := e.operation.getLiquidityPoolAndProductDelta(&op.LiquidityPoolId)
  1292  	if err != nil {
  1293  		return err
  1294  	}
  1295  	details := map[string]interface{}{
  1296  		"liquidity_pool": liquidityPoolDetails(lp),
  1297  		"reserves_received": []base.AssetAmount{
  1298  			{
  1299  				Asset:  lp.Body.ConstantProduct.Params.AssetA.StringCanonical(),
  1300  				Amount: amount.String(-delta.ReserveA),
  1301  			},
  1302  			{
  1303  				Asset:  lp.Body.ConstantProduct.Params.AssetB.StringCanonical(),
  1304  				Amount: amount.String(-delta.ReserveB),
  1305  			},
  1306  		},
  1307  		"shares_redeemed": amount.String(-delta.TotalPoolShares),
  1308  	}
  1309  	e.addMuxed(e.operation.SourceAccount(), EffectLiquidityPoolWithdrew, details)
  1310  	return nil
  1311  }
  1312  
  1313  // addInvokeHostFunctionEffects iterates through the events and generates
  1314  // account_credited and account_debited effects when it sees events related to
  1315  // the Stellar Asset Contract corresponding to those effects.
  1316  func (e *effectsWrapper) addInvokeHostFunctionEffects(events []contractevents.Event) error {
  1317  	if e.operation.network == "" {
  1318  		return errors.New("invokeHostFunction effects cannot be determined unless network passphrase is set")
  1319  	}
  1320  
  1321  	source := e.operation.SourceAccount()
  1322  	for _, event := range events {
  1323  		evt, err := contractevents.NewStellarAssetContractEvent(&event, e.operation.network)
  1324  		if err != nil {
  1325  			continue // irrelevant or unsupported event
  1326  		}
  1327  
  1328  		details := make(map[string]interface{}, 4)
  1329  		addAssetDetails(details, evt.GetAsset(), "")
  1330  
  1331  		//
  1332  		// Note: We ignore effects that involve contracts (until the day we have
  1333  		// contract_debited/credited effects, may it never come :pray:)
  1334  		//
  1335  
  1336  		switch evt.GetType() {
  1337  		// Transfer events generate an `account_debited` effect for the `from`
  1338  		// (sender) and an `account_credited` effect for the `to` (recipient).
  1339  		case contractevents.EventTypeTransfer:
  1340  			details["contract_event_type"] = "transfer"
  1341  			transferEvent := evt.(*contractevents.TransferEvent)
  1342  			details["amount"] = amount.String128(transferEvent.Amount)
  1343  			toDetails := map[string]interface{}{}
  1344  			for key, val := range details {
  1345  				toDetails[key] = val
  1346  			}
  1347  
  1348  			if strkey.IsValidEd25519PublicKey(transferEvent.From) {
  1349  				e.add(
  1350  					transferEvent.From,
  1351  					null.String{},
  1352  					EffectAccountDebited,
  1353  					details,
  1354  				)
  1355  			} else {
  1356  				details["contract"] = transferEvent.From
  1357  				e.addMuxed(source, EffectContractDebited, details)
  1358  			}
  1359  
  1360  			if strkey.IsValidEd25519PublicKey(transferEvent.To) {
  1361  				e.add(
  1362  					transferEvent.To,
  1363  					null.String{},
  1364  					EffectAccountCredited,
  1365  					toDetails,
  1366  				)
  1367  			} else {
  1368  				toDetails["contract"] = transferEvent.To
  1369  				e.addMuxed(source, EffectContractCredited, toDetails)
  1370  			}
  1371  
  1372  		// Mint events imply a non-native asset, and it results in a credit to
  1373  		// the `to` recipient.
  1374  		case contractevents.EventTypeMint:
  1375  			details["contract_event_type"] = "mint"
  1376  			mintEvent := evt.(*contractevents.MintEvent)
  1377  			details["amount"] = amount.String128(mintEvent.Amount)
  1378  			if strkey.IsValidEd25519PublicKey(mintEvent.To) {
  1379  				e.add(
  1380  					mintEvent.To,
  1381  					null.String{},
  1382  					EffectAccountCredited,
  1383  					details,
  1384  				)
  1385  			} else {
  1386  				details["contract"] = mintEvent.To
  1387  				e.addMuxed(source, EffectContractCredited, details)
  1388  			}
  1389  
  1390  		// Clawback events result in a debit to the `from` address, but acts
  1391  		// like a burn to the recipient, so these are functionally equivalent
  1392  		case contractevents.EventTypeClawback:
  1393  			details["contract_event_type"] = "clawback"
  1394  			cbEvent := evt.(*contractevents.ClawbackEvent)
  1395  			details["amount"] = amount.String128(cbEvent.Amount)
  1396  			if strkey.IsValidEd25519PublicKey(cbEvent.From) {
  1397  				e.add(
  1398  					cbEvent.From,
  1399  					null.String{},
  1400  					EffectAccountDebited,
  1401  					details,
  1402  				)
  1403  			} else {
  1404  				details["contract"] = cbEvent.From
  1405  				e.addMuxed(source, EffectContractDebited, details)
  1406  			}
  1407  
  1408  		case contractevents.EventTypeBurn:
  1409  			details["contract_event_type"] = "burn"
  1410  			burnEvent := evt.(*contractevents.BurnEvent)
  1411  			details["amount"] = amount.String128(burnEvent.Amount)
  1412  			if strkey.IsValidEd25519PublicKey(burnEvent.From) {
  1413  				e.add(
  1414  					burnEvent.From,
  1415  					null.String{},
  1416  					EffectAccountDebited,
  1417  					details,
  1418  				)
  1419  			} else {
  1420  				details["contract"] = burnEvent.From
  1421  				e.addMuxed(source, EffectContractDebited, details)
  1422  			}
  1423  		}
  1424  	}
  1425  
  1426  	return nil
  1427  }
  1428  
  1429  func (e *effectsWrapper) addExtendFootprintTtlEffect() error {
  1430  	op := e.operation.operation.Body.MustExtendFootprintTtlOp()
  1431  
  1432  	// Figure out which entries were affected
  1433  	changes, err := e.operation.transaction.GetOperationChanges(e.operation.index)
  1434  	if err != nil {
  1435  		return err
  1436  	}
  1437  	entries := make([]string, 0, len(changes))
  1438  	for _, change := range changes {
  1439  		// They should all have a post
  1440  		if change.Post == nil {
  1441  			return fmt.Errorf("invalid bump footprint expiration operation: %v", op)
  1442  		}
  1443  		var key xdr.LedgerKey
  1444  		switch change.Post.Data.Type {
  1445  		case xdr.LedgerEntryTypeTtl:
  1446  			v := change.Post.Data.MustTtl()
  1447  			if err := key.SetTtl(v.KeyHash); err != nil {
  1448  				return err
  1449  			}
  1450  		default:
  1451  			// Ignore any non-contract entries, as they couldn't have been affected.
  1452  			//
  1453  			// Should we error here? No, because there might be other entries
  1454  			// affected, for example, the user's balance.
  1455  			continue
  1456  		}
  1457  		b64, err := xdr.MarshalBase64(key)
  1458  		if err != nil {
  1459  			return err
  1460  		}
  1461  		entries = append(entries, b64)
  1462  	}
  1463  	details := map[string]interface{}{
  1464  		"entries":   entries,
  1465  		"extend_to": op.ExtendTo,
  1466  	}
  1467  	e.addMuxed(e.operation.SourceAccount(), EffectExtendFootprintTtl, details)
  1468  	return nil
  1469  }
  1470  
  1471  func (e *effectsWrapper) addRestoreFootprintExpirationEffect() error {
  1472  	op := e.operation.operation.Body.MustRestoreFootprintOp()
  1473  
  1474  	// Figure out which entries were affected
  1475  	changes, err := e.operation.transaction.GetOperationChanges(e.operation.index)
  1476  	if err != nil {
  1477  		return err
  1478  	}
  1479  	entries := make([]string, 0, len(changes))
  1480  	for _, change := range changes {
  1481  		// They should all have a post
  1482  		if change.Post == nil {
  1483  			return fmt.Errorf("invalid restore footprint operation: %v", op)
  1484  		}
  1485  		var key xdr.LedgerKey
  1486  		switch change.Post.Data.Type {
  1487  		case xdr.LedgerEntryTypeTtl:
  1488  			v := change.Post.Data.MustTtl()
  1489  			if err := key.SetTtl(v.KeyHash); err != nil {
  1490  				return err
  1491  			}
  1492  		default:
  1493  			// Ignore any non-contract entries, as they couldn't have been affected.
  1494  			//
  1495  			// Should we error here? No, because there might be other entries
  1496  			// affected, for example, the user's balance.
  1497  			continue
  1498  		}
  1499  		b64, err := xdr.MarshalBase64(key)
  1500  		if err != nil {
  1501  			return err
  1502  		}
  1503  		entries = append(entries, b64)
  1504  	}
  1505  	details := map[string]interface{}{
  1506  		"entries": entries,
  1507  	}
  1508  	e.addMuxed(e.operation.SourceAccount(), EffectRestoreFootprint, details)
  1509  	return nil
  1510  }