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 }