From c29e2946b6152b50ecc664bc47b92a885131fd3c Mon Sep 17 00:00:00 2001 From: chowbao Date: Fri, 10 Jan 2025 10:12:26 -0700 Subject: [PATCH] Inital commit of the processors package (#5567) --- go.mod | 1 + go.sum | 2 + ingest/processors/account.go | 111 + ingest/processors/account_signer.go | 54 + ingest/processors/account_signer_test.go | 165 + ingest/processors/account_test.go | 196 + ingest/processors/asset.go | 75 + ingest/processors/asset_test.go | 124 + ingest/processors/claimable_balance.go | 71 + ingest/processors/claimable_balance_test.go | 130 + ingest/processors/config_setting.go | 162 + ingest/processors/config_setting_test.go | 140 + ingest/processors/contract_code.go | 86 + ingest/processors/contract_code_test.go | 123 + ingest/processors/contract_data.go | 357 ++ ingest/processors/contract_data_test.go | 174 + ingest/processors/contract_events.go | 151 + ingest/processors/contract_events_test.go | 208 + ingest/processors/effects.go | 1512 +++++++ ingest/processors/effects_test.go | 4143 +++++++++++++++++++ ingest/processors/ledger.go | 205 + ingest/processors/ledger_test.go | 205 + ingest/processors/liquidity_pool.go | 81 + ingest/processors/liquidity_pool_test.go | 118 + ingest/processors/offer.go | 101 + ingest/processors/offer_normalized.go | 181 + ingest/processors/offer_normalized_test.go | 122 + ingest/processors/offer_test.go | 184 + ingest/processors/operation.go | 2205 ++++++++++ ingest/processors/operation_test.go | 2077 ++++++++++ ingest/processors/schema.go | 637 +++ ingest/processors/test_variables_test.go | 232 ++ ingest/processors/trade.go | 399 ++ ingest/processors/trade_test.go | 842 ++++ ingest/processors/transaction.go | 331 ++ ingest/processors/transaction_test.go | 414 ++ ingest/processors/trustline.go | 109 + ingest/processors/trustline_test.go | 164 + ingest/processors/ttl.go | 48 + ingest/processors/ttl_test.go | 107 + ingest/processors/utils.go | 232 ++ toid/main.go | 5 +- toid/synt_offer_id.go | 42 + 43 files changed, 17023 insertions(+), 3 deletions(-) create mode 100644 ingest/processors/account.go create mode 100644 ingest/processors/account_signer.go create mode 100644 ingest/processors/account_signer_test.go create mode 100644 ingest/processors/account_test.go create mode 100644 ingest/processors/asset.go create mode 100644 ingest/processors/asset_test.go create mode 100644 ingest/processors/claimable_balance.go create mode 100644 ingest/processors/claimable_balance_test.go create mode 100644 ingest/processors/config_setting.go create mode 100644 ingest/processors/config_setting_test.go create mode 100644 ingest/processors/contract_code.go create mode 100644 ingest/processors/contract_code_test.go create mode 100644 ingest/processors/contract_data.go create mode 100644 ingest/processors/contract_data_test.go create mode 100644 ingest/processors/contract_events.go create mode 100644 ingest/processors/contract_events_test.go create mode 100644 ingest/processors/effects.go create mode 100644 ingest/processors/effects_test.go create mode 100644 ingest/processors/ledger.go create mode 100644 ingest/processors/ledger_test.go create mode 100644 ingest/processors/liquidity_pool.go create mode 100644 ingest/processors/liquidity_pool_test.go create mode 100644 ingest/processors/offer.go create mode 100644 ingest/processors/offer_normalized.go create mode 100644 ingest/processors/offer_normalized_test.go create mode 100644 ingest/processors/offer_test.go create mode 100644 ingest/processors/operation.go create mode 100644 ingest/processors/operation_test.go create mode 100644 ingest/processors/schema.go create mode 100644 ingest/processors/test_variables_test.go create mode 100644 ingest/processors/trade.go create mode 100644 ingest/processors/trade_test.go create mode 100644 ingest/processors/transaction.go create mode 100644 ingest/processors/transaction_test.go create mode 100644 ingest/processors/trustline.go create mode 100644 ingest/processors/trustline_test.go create mode 100644 ingest/processors/ttl.go create mode 100644 ingest/processors/ttl_test.go create mode 100644 ingest/processors/utils.go create mode 100644 toid/synt_offer_id.go diff --git a/go.mod b/go.mod index 341c85543e..6d7e5fab5d 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,7 @@ require ( require ( github.com/cenkalti/backoff/v4 v4.3.0 + github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da github.com/docker/docker v27.3.1+incompatible github.com/docker/go-connections v0.5.0 github.com/fsouza/fake-gcs-server v1.49.2 diff --git a/go.sum b/go.sum index a3c8ce08fa..b071601459 100644 --- a/go.sum +++ b/go.sum @@ -116,6 +116,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/fscache v0.10.1 h1:hDv+RGyvD+UDKyRYuLoVNbuRTnf2SrA2K3VyR1br9lk= diff --git a/ingest/processors/account.go b/ingest/processors/account.go new file mode 100644 index 0000000000..de586d959a --- /dev/null +++ b/ingest/processors/account.go @@ -0,0 +1,111 @@ +package processors + +import ( + "fmt" + + "github.com/guregu/null/zero" + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +// TransformAccount converts an account from the history archive ingestion system into a form suitable for BigQuery +func TransformAccount(ledgerChange ingest.Change, header xdr.LedgerHeaderHistoryEntry) (AccountOutput, error) { + ledgerEntry, changeType, outputDeleted, err := ExtractEntryFromChange(ledgerChange) + if err != nil { + return AccountOutput{}, err + } + + accountEntry, accountFound := ledgerEntry.Data.GetAccount() + if !accountFound { + return AccountOutput{}, fmt.Errorf("could not extract account data from ledger entry; actual type is %s", ledgerEntry.Data.Type) + } + + outputID, err := accountEntry.AccountId.GetAddress() + if err != nil { + return AccountOutput{}, err + } + + outputBalance := accountEntry.Balance + if outputBalance < 0 { + return AccountOutput{}, fmt.Errorf("balance is negative (%d) for account: %s", outputBalance, outputID) + } + + //The V1 struct is the first version of the extender from accountEntry. It contains information on liabilities, and in the future + //more extensions may contain extra information + accountExtensionInfo, V1Found := accountEntry.Ext.GetV1() + var outputBuyingLiabilities, outputSellingLiabilities xdr.Int64 + if V1Found { + liabilities := accountExtensionInfo.Liabilities + outputBuyingLiabilities, outputSellingLiabilities = liabilities.Buying, liabilities.Selling + if outputBuyingLiabilities < 0 { + return AccountOutput{}, fmt.Errorf("the buying liabilities count is negative (%d) for account: %s", outputBuyingLiabilities, outputID) + } + + if outputSellingLiabilities < 0 { + return AccountOutput{}, fmt.Errorf("the selling liabilities count is negative (%d) for account: %s", outputSellingLiabilities, outputID) + } + } + + outputSequenceNumber := int64(accountEntry.SeqNum) + if outputSequenceNumber < 0 { + return AccountOutput{}, fmt.Errorf("account sequence number is negative (%d) for account: %s", outputSequenceNumber, outputID) + } + outputSequenceLedger := accountEntry.SeqLedger() + outputSequenceTime := accountEntry.SeqTime() + + outputNumSubentries := uint32(accountEntry.NumSubEntries) + + inflationDestAccountID := accountEntry.InflationDest + var outputInflationDest string + if inflationDestAccountID != nil { + outputInflationDest, err = inflationDestAccountID.GetAddress() + if err != nil { + return AccountOutput{}, err + } + } + + outputFlags := uint32(accountEntry.Flags) + + outputHomeDomain := string(accountEntry.HomeDomain) + + outputMasterWeight := int32(accountEntry.MasterKeyWeight()) + outputThreshLow := int32(accountEntry.ThresholdLow()) + outputThreshMed := int32(accountEntry.ThresholdMedium()) + outputThreshHigh := int32(accountEntry.ThresholdHigh()) + + outputLastModifiedLedger := uint32(ledgerEntry.LastModifiedLedgerSeq) + + closedAt, err := TimePointToUTCTimeStamp(header.Header.ScpValue.CloseTime) + if err != nil { + return AccountOutput{}, err + } + + ledgerSequence := header.Header.LedgerSeq + + transformedAccount := AccountOutput{ + AccountID: outputID, + Balance: ConvertStroopValueToReal(outputBalance), + BuyingLiabilities: ConvertStroopValueToReal(outputBuyingLiabilities), + SellingLiabilities: ConvertStroopValueToReal(outputSellingLiabilities), + SequenceNumber: outputSequenceNumber, + SequenceLedger: zero.IntFrom(int64(outputSequenceLedger)), + SequenceTime: zero.IntFrom(int64(outputSequenceTime)), + NumSubentries: outputNumSubentries, + InflationDestination: outputInflationDest, + Flags: outputFlags, + HomeDomain: outputHomeDomain, + MasterWeight: outputMasterWeight, + ThresholdLow: outputThreshLow, + ThresholdMedium: outputThreshMed, + ThresholdHigh: outputThreshHigh, + LastModifiedLedger: outputLastModifiedLedger, + Sponsor: ledgerEntrySponsorToNullString(ledgerEntry), + NumSponsored: uint32(accountEntry.NumSponsored()), + NumSponsoring: uint32(accountEntry.NumSponsoring()), + LedgerEntryChange: uint32(changeType), + Deleted: outputDeleted, + ClosedAt: closedAt, + LedgerSequence: uint32(ledgerSequence), + } + return transformedAccount, nil +} diff --git a/ingest/processors/account_signer.go b/ingest/processors/account_signer.go new file mode 100644 index 0000000000..8b2e5c3525 --- /dev/null +++ b/ingest/processors/account_signer.go @@ -0,0 +1,54 @@ +package processors + +import ( + "fmt" + "sort" + + "github.com/guregu/null" + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +// TransformAccountSigners converts account signers from the history archive ingestion system into a form suitable for BigQuery +func TransformAccountSigners(ledgerChange ingest.Change, header xdr.LedgerHeaderHistoryEntry) ([]AccountSignerOutput, error) { + var signers []AccountSignerOutput + + ledgerEntry, changeType, outputDeleted, err := ExtractEntryFromChange(ledgerChange) + if err != nil { + return signers, err + } + outputLastModifiedLedger := uint32(ledgerEntry.LastModifiedLedgerSeq) + accountEntry, accountFound := ledgerEntry.Data.GetAccount() + if !accountFound { + return signers, fmt.Errorf("could not extract signer data from ledger entry of type: %+v", ledgerEntry.Data.Type) + } + + closedAt, err := TimePointToUTCTimeStamp(header.Header.ScpValue.CloseTime) + if err != nil { + return signers, err + } + + ledgerSequence := header.Header.LedgerSeq + + sponsors := accountEntry.SponsorPerSigner() + for signer, weight := range accountEntry.SignerSummary() { + var sponsor null.String + if sponsorDesc, isSponsored := sponsors[signer]; isSponsored { + sponsor = null.StringFrom(sponsorDesc.Address()) + } + + signers = append(signers, AccountSignerOutput{ + AccountID: accountEntry.AccountId.Address(), + Signer: signer, + Weight: weight, + Sponsor: sponsor, + LastModifiedLedger: outputLastModifiedLedger, + LedgerEntryChange: uint32(changeType), + Deleted: outputDeleted, + ClosedAt: closedAt, + LedgerSequence: uint32(ledgerSequence), + }) + } + sort.Slice(signers, func(a, b int) bool { return signers[a].Weight < signers[b].Weight }) + return signers, nil +} diff --git a/ingest/processors/account_signer_test.go b/ingest/processors/account_signer_test.go new file mode 100644 index 0000000000..4ffd55d8b9 --- /dev/null +++ b/ingest/processors/account_signer_test.go @@ -0,0 +1,165 @@ +package processors + +import ( + "fmt" + "testing" + "time" + + "github.com/guregu/null" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformAccountSigner(t *testing.T) { + type inputStruct struct { + injest ingest.Change + } + + type transformTest struct { + input inputStruct + wantOutput []AccountSignerOutput + wantErr error + } + + hardCodedInput := makeSignersTestInput() + hardCodedOutput := makeSignersTestOutput() + + tests := []transformTest{ + { + inputStruct{ + ingest.Change{ + Type: xdr.LedgerEntryTypeOffer, + Pre: nil, + Post: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + }, + }, + }, + }, + nil, fmt.Errorf("could not extract signer data from ledger entry of type: LedgerEntryTypeOffer"), + }, + { + inputStruct{ + hardCodedInput, + }, + hardCodedOutput, nil, + }, + } + + for _, test := range tests { + header := xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + ScpValue: xdr.StellarValue{ + CloseTime: 1000, + }, + LedgerSeq: 10, + }, + } + actualOutput, actualError := TransformAccountSigners(test.input.injest, header) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func makeSignersTestInput() ingest.Change { + sponsor, _ := xdr.AddressToAccountId("GBADGWKHSUFOC4C7E3KXKINZSRX5KPHUWHH67UGJU77LEORGVLQ3BN3B") + + var ledgerEntry = xdr.LedgerEntry{ + LastModifiedLedgerSeq: 30705278, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: testAccount1ID, + Balance: 10959979, + SeqNum: 117801117454198833, + NumSubEntries: 141, + InflationDest: &testAccount2ID, + Flags: 4, + HomeDomain: "examplehome.com", + Thresholds: xdr.Thresholds([4]byte{2, 1, 3, 5}), + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryExtensionV1{ + Liabilities: xdr.Liabilities{ + Buying: 1000, + Selling: 1500, + }, + Ext: xdr.AccountEntryExtensionV1Ext{ + V: 2, + V2: &xdr.AccountEntryExtensionV2{ + SignerSponsoringIDs: []xdr.SponsorshipDescriptor{ + &sponsor, + nil, + }, + }, + }, + }, + }, + Signers: []xdr.Signer{ + { + Key: xdr.SignerKey{ + Type: xdr.SignerKeyTypeSignerKeyTypeEd25519, + Ed25519: &xdr.Uint256{4, 5, 6}, + PreAuthTx: nil, + HashX: nil, + }, + Weight: 10.0, + }, { + Key: xdr.SignerKey{ + Type: xdr.SignerKeyTypeSignerKeyTypeEd25519, + Ed25519: &xdr.Uint256{10, 11, 12}, + PreAuthTx: nil, + HashX: nil, + }, + Weight: 20.0, + }, + }, + }, + }, + } + return ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &ledgerEntry, + Post: nil, + } +} + +func makeSignersTestOutput() []AccountSignerOutput { + return []AccountSignerOutput{ + { + AccountID: testAccount1ID.Address(), + Signer: "GCEODJVUUVYVFD5KT4TOEDTMXQ76OPFOQC2EMYYMLPXQCUVPOB6XRWPQ", + Weight: 2.0, + Sponsor: null.String{}, + LastModifiedLedger: 30705278, + LedgerEntryChange: 2, + Deleted: true, + LedgerSequence: 10, + ClosedAt: time.Date(1970, time.January, 1, 0, 16, 40, 0, time.UTC), + }, { + AccountID: testAccount1ID.Address(), + Signer: "GACAKBQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB3BQ", + Weight: 10.0, + Sponsor: null.StringFrom("GBADGWKHSUFOC4C7E3KXKINZSRX5KPHUWHH67UGJU77LEORGVLQ3BN3B"), + LastModifiedLedger: 30705278, + LedgerEntryChange: 2, + Deleted: true, + LedgerSequence: 10, + ClosedAt: time.Date(1970, time.January, 1, 0, 16, 40, 0, time.UTC), + }, { + AccountID: testAccount1ID.Address(), + Signer: "GAFAWDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABNDC", + Weight: 20.0, + Sponsor: null.String{}, + LastModifiedLedger: 30705278, + LedgerEntryChange: 2, + Deleted: true, + LedgerSequence: 10, + ClosedAt: time.Date(1970, time.January, 1, 0, 16, 40, 0, time.UTC), + }, + } +} diff --git a/ingest/processors/account_test.go b/ingest/processors/account_test.go new file mode 100644 index 0000000000..a847d4f277 --- /dev/null +++ b/ingest/processors/account_test.go @@ -0,0 +1,196 @@ +package processors + +import ( + "fmt" + "testing" + "time" + + "github.com/guregu/null" + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformAccount(t *testing.T) { + type inputStruct struct { + ledgerChange ingest.Change + } + + type transformTest struct { + input inputStruct + wantOutput AccountOutput + wantErr error + } + + hardCodedInput := makeAccountTestInput() + hardCodedOutput := makeAccountTestOutput() + + tests := []transformTest{ + { + inputStruct{ingest.Change{ + Type: xdr.LedgerEntryTypeOffer, + Pre: nil, + Post: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + }, + }, + }, + }, + AccountOutput{}, fmt.Errorf("could not extract account data from ledger entry; actual type is LedgerEntryTypeOffer"), + }, + { + inputStruct{wrapAccountEntry(xdr.AccountEntry{ + AccountId: genericAccountID, + Balance: -1, + }, 0), + }, + AccountOutput{}, fmt.Errorf("balance is negative (-1) for account: %s", genericAccountAddress), + }, + { + inputStruct{wrapAccountEntry(xdr.AccountEntry{ + AccountId: genericAccountID, + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryExtensionV1{ + Liabilities: xdr.Liabilities{ + Buying: -1, + }, + }, + }, + }, 0), + }, + AccountOutput{}, fmt.Errorf("the buying liabilities count is negative (-1) for account: %s", genericAccountAddress), + }, + { + inputStruct{wrapAccountEntry(xdr.AccountEntry{ + AccountId: genericAccountID, + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryExtensionV1{ + Liabilities: xdr.Liabilities{ + Selling: -2, + }, + }, + }, + }, 0), + }, + AccountOutput{}, fmt.Errorf("the selling liabilities count is negative (-2) for account: %s", genericAccountAddress), + }, + { + inputStruct{wrapAccountEntry(xdr.AccountEntry{ + AccountId: genericAccountID, + SeqNum: -3, + }, 0), + }, + AccountOutput{}, fmt.Errorf("account sequence number is negative (-3) for account: %s", genericAccountAddress), + }, + { + inputStruct{ + hardCodedInput, + }, + hardCodedOutput, nil, + }, + } + + for _, test := range tests { + header := xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + ScpValue: xdr.StellarValue{ + CloseTime: 1000, + }, + LedgerSeq: 10, + }, + } + actualOutput, actualError := TransformAccount(test.input.ledgerChange, header) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func wrapAccountEntry(accountEntry xdr.AccountEntry, lastModified int) ingest.Change { + return ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: xdr.Uint32(lastModified), + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &accountEntry, + }, + }, + } +} + +func makeAccountTestInput() ingest.Change { + + ledgerEntry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: 30705278, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: testAccount1ID, + Balance: 10959979, + SeqNum: 117801117454198833, + NumSubEntries: 141, + InflationDest: &testAccount2ID, + Flags: 4, + HomeDomain: "examplehome.com", + Thresholds: xdr.Thresholds([4]byte{2, 1, 3, 5}), + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryExtensionV1{ + Liabilities: xdr.Liabilities{ + Buying: 1000, + Selling: 1500, + }, + Ext: xdr.AccountEntryExtensionV1Ext{ + V: 2, + V2: &xdr.AccountEntryExtensionV2{ + NumSponsored: 3, + NumSponsoring: 1, + }, + }, + }, + }, + }, + }, + Ext: xdr.LedgerEntryExt{ + V: 1, + V1: &xdr.LedgerEntryExtensionV1{ + SponsoringId: &testAccount3ID, + }, + }, + } + return ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &ledgerEntry, + Post: nil, + } +} + +func makeAccountTestOutput() AccountOutput { + return AccountOutput{ + AccountID: testAccount1Address, + Balance: 1.0959979, + BuyingLiabilities: 0.0001, + SellingLiabilities: 0.00015, + SequenceNumber: 117801117454198833, + NumSubentries: 141, + InflationDestination: testAccount2Address, + Flags: 4, + HomeDomain: "examplehome.com", + MasterWeight: 2, + ThresholdLow: 1, + ThresholdMedium: 3, + ThresholdHigh: 5, + Sponsor: null.StringFrom(testAccount3Address), + NumSponsored: 3, + NumSponsoring: 1, + LastModifiedLedger: 30705278, + LedgerEntryChange: 2, + Deleted: true, + LedgerSequence: 10, + ClosedAt: time.Date(1970, time.January, 1, 0, 16, 40, 0, time.UTC), + } +} diff --git a/ingest/processors/asset.go b/ingest/processors/asset.go new file mode 100644 index 0000000000..abd728d613 --- /dev/null +++ b/ingest/processors/asset.go @@ -0,0 +1,75 @@ +package processors + +import ( + "fmt" + + "github.com/dgryski/go-farm" + "github.com/stellar/go/toid" + "github.com/stellar/go/xdr" +) + +// TransformAsset converts an asset from a payment operation into a form suitable for BigQuery +func TransformAsset(operation xdr.Operation, operationIndex int32, transactionIndex int32, ledgerSeq int32, lcm xdr.LedgerCloseMeta) (AssetOutput, error) { + operationID := toid.New(ledgerSeq, int32(transactionIndex), operationIndex).ToInt64() + + opType := operation.Body.Type + if opType != xdr.OperationTypePayment && opType != xdr.OperationTypeManageSellOffer { + return AssetOutput{}, fmt.Errorf("operation of type %d cannot issue an asset (id %d)", opType, operationID) + } + + asset := xdr.Asset{} + switch opType { + case xdr.OperationTypeManageSellOffer: + opSellOf, ok := operation.Body.GetManageSellOfferOp() + if !ok { + return AssetOutput{}, fmt.Errorf("operation of type ManageSellOfferOp cannot issue an asset (id %d)", operationID) + } + asset = opSellOf.Selling + + case xdr.OperationTypePayment: + opPayment, ok := operation.Body.GetPaymentOp() + if !ok { + return AssetOutput{}, fmt.Errorf("could not access Payment info for this operation (id %d)", operationID) + } + asset = opPayment.Asset + + } + + outputAsset, err := transformSingleAsset(asset) + if err != nil { + return AssetOutput{}, fmt.Errorf("%s (id %d)", err.Error(), operationID) + } + + outputCloseTime, err := GetCloseTime(lcm) + if err != nil { + return AssetOutput{}, err + } + outputAsset.ClosedAt = outputCloseTime + outputAsset.LedgerSequence = GetLedgerSequence(lcm) + + return outputAsset, nil +} + +func transformSingleAsset(asset xdr.Asset) (AssetOutput, error) { + var outputAssetType, outputAssetCode, outputAssetIssuer string + err := asset.Extract(&outputAssetType, &outputAssetCode, &outputAssetIssuer) + if err != nil { + return AssetOutput{}, fmt.Errorf("could not extract asset from this operation") + } + + farmAssetID := FarmHashAsset(outputAssetCode, outputAssetIssuer, outputAssetType) + + return AssetOutput{ + AssetCode: outputAssetCode, + AssetIssuer: outputAssetIssuer, + AssetType: outputAssetType, + AssetID: farmAssetID, + }, nil +} + +func FarmHashAsset(assetCode, assetIssuer, assetType string) int64 { + asset := fmt.Sprintf("%s%s%s", assetCode, assetIssuer, assetType) + hash := farm.Fingerprint64([]byte(asset)) + + return int64(hash) +} diff --git a/ingest/processors/asset_test.go b/ingest/processors/asset_test.go new file mode 100644 index 0000000000..b45eefa600 --- /dev/null +++ b/ingest/processors/asset_test.go @@ -0,0 +1,124 @@ +package processors + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformAsset(t *testing.T) { + + type assetInput struct { + operation xdr.Operation + index int32 + txnIndex int32 + // transaction xdr.TransactionEnvelope + lcm xdr.LedgerCloseMeta + } + + type transformTest struct { + input assetInput + wantOutput AssetOutput + wantErr error + } + + nonPaymentInput := assetInput{ + operation: genericBumpOperation, + txnIndex: 0, + index: 0, + lcm: genericLedgerCloseMeta, + } + + tests := []transformTest{ + { + input: nonPaymentInput, + wantOutput: AssetOutput{}, + wantErr: fmt.Errorf("operation of type 11 cannot issue an asset (id 0)"), + }, + } + + hardCodedInputTransaction, err := makeAssetTestInput() + assert.NoError(t, err) + hardCodedOutputArray := makeAssetTestOutput() + + for i, op := range hardCodedInputTransaction.Envelope.Operations() { + tests = append(tests, transformTest{ + input: assetInput{ + operation: op, + index: int32(i), + txnIndex: int32(i), + lcm: genericLedgerCloseMeta}, + wantOutput: hardCodedOutputArray[i], + wantErr: nil, + }) + } + + for _, test := range tests { + actualOutput, actualError := TransformAsset(test.input.operation, test.input.index, test.input.txnIndex, 0, test.input.lcm) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func makeAssetTestInput() (inputTransaction ingest.LedgerTransaction, err error) { + inputTransaction = genericLedgerTransaction + inputEnvelope := genericBumpOperationEnvelope + + inputEnvelope.Tx.SourceAccount = testAccount1 + + inputOperations := []xdr.Operation{ + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypePayment, + PaymentOp: &xdr.PaymentOp{ + Destination: testAccount2, + Asset: usdtAsset, + Amount: 350000000, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypePayment, + PaymentOp: &xdr.PaymentOp{ + Destination: testAccount3, + Asset: nativeAsset, + Amount: 350000000, + }, + }, + }, + } + + inputEnvelope.Tx.Operations = inputOperations + inputTransaction.Envelope.V1 = &inputEnvelope + return +} + +func makeAssetTestOutput() (transformedAssets []AssetOutput) { + transformedAssets = []AssetOutput{ + { + AssetCode: "USDT", + AssetIssuer: "GBVVRXLMNCJQW3IDDXC3X6XCH35B5Q7QXNMMFPENSOGUPQO7WO7HGZPA", + AssetType: "credit_alphanum4", + AssetID: -8205667356306085451, + ClosedAt: time.Date(1970, time.January, 1, 0, 0, 10, 0, time.UTC), + LedgerSequence: 2, + }, + { + AssetCode: "", + AssetIssuer: "", + AssetType: "native", + AssetID: -5706705804583548011, + ClosedAt: time.Date(1970, time.January, 1, 0, 0, 10, 0, time.UTC), + LedgerSequence: 2, + }, + } + return +} diff --git a/ingest/processors/claimable_balance.go b/ingest/processors/claimable_balance.go new file mode 100644 index 0000000000..b21a476a4b --- /dev/null +++ b/ingest/processors/claimable_balance.go @@ -0,0 +1,71 @@ +package processors + +import ( + "fmt" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func transformClaimants(claimants []xdr.Claimant) []Claimant { + var transformed []Claimant + for _, c := range claimants { + cv0 := c.MustV0() + transformed = append(transformed, Claimant{ + Destination: cv0.Destination.Address(), + Predicate: cv0.Predicate, + }) + } + return transformed +} + +// TransformClaimableBalance converts a claimable balance from the history archive ingestion system into a form suitable for BigQuery +func TransformClaimableBalance(ledgerChange ingest.Change, header xdr.LedgerHeaderHistoryEntry) (ClaimableBalanceOutput, error) { + ledgerEntry, changeType, outputDeleted, err := ExtractEntryFromChange(ledgerChange) + if err != nil { + return ClaimableBalanceOutput{}, err + } + + balanceEntry, balanceFound := ledgerEntry.Data.GetClaimableBalance() + if !balanceFound { + return ClaimableBalanceOutput{}, fmt.Errorf("could not extract claimable balance data from ledger entry; actual type is %s", ledgerEntry.Data.Type) + } + balanceID, err := xdr.MarshalHex(balanceEntry.BalanceId) + if err != nil { + return ClaimableBalanceOutput{}, fmt.Errorf("invalid balanceId in op: %d", uint32(ledgerEntry.LastModifiedLedgerSeq)) + } + outputFlags := uint32(balanceEntry.Flags()) + outputAsset, err := transformSingleAsset(balanceEntry.Asset) + if err != nil { + return ClaimableBalanceOutput{}, err + } + outputClaimants := transformClaimants(balanceEntry.Claimants) + outputAmount := balanceEntry.Amount + + outputLastModifiedLedger := uint32(ledgerEntry.LastModifiedLedgerSeq) + + closedAt, err := TimePointToUTCTimeStamp(header.Header.ScpValue.CloseTime) + if err != nil { + return ClaimableBalanceOutput{}, err + } + + ledgerSequence := header.Header.LedgerSeq + + transformed := ClaimableBalanceOutput{ + BalanceID: balanceID, + AssetCode: outputAsset.AssetCode, + AssetIssuer: outputAsset.AssetIssuer, + AssetType: outputAsset.AssetType, + AssetID: outputAsset.AssetID, + Claimants: outputClaimants, + AssetAmount: float64(outputAmount) / 1.0e7, + Sponsor: ledgerEntrySponsorToNullString(ledgerEntry), + LastModifiedLedger: outputLastModifiedLedger, + LedgerEntryChange: uint32(changeType), + Flags: outputFlags, + Deleted: outputDeleted, + ClosedAt: closedAt, + LedgerSequence: uint32(ledgerSequence), + } + return transformed, nil +} diff --git a/ingest/processors/claimable_balance_test.go b/ingest/processors/claimable_balance_test.go new file mode 100644 index 0000000000..faf8904fcb --- /dev/null +++ b/ingest/processors/claimable_balance_test.go @@ -0,0 +1,130 @@ +package processors + +import ( + "testing" + "time" + + "github.com/guregu/null" + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" +) + +func TestTransformClaimableBalance(t *testing.T) { + type inputStruct struct { + ingest ingest.Change + } + type transformTest struct { + input inputStruct + wantOutput ClaimableBalanceOutput + wantErr error + } + inputChange := makeClaimableBalanceTestInput() + output := makeClaimableBalanceTestOutput() + + input := inputStruct{ + inputChange, + } + + tests := []transformTest{ + { + input: input, + wantOutput: output, + wantErr: nil, + }, + } + + for _, test := range tests { + header := xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + ScpValue: xdr.StellarValue{ + CloseTime: 1000, + }, + LedgerSeq: 10, + }, + } + actualOutput, actualError := TransformClaimableBalance(test.input.ingest, header) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func makeClaimableBalanceTestInput() ingest.Change { + ledgerEntry := xdr.LedgerEntry{ + Ext: xdr.LedgerEntryExt{ + V: 1, + V1: &xdr.LedgerEntryExtensionV1{ + SponsoringId: &xdr.AccountId{ + Type: 0, + Ed25519: &xdr.Uint256{1, 2, 3, 4, 5, 6, 7, 8, 9}, + }, + Ext: xdr.LedgerEntryExtensionV1Ext{ + V: 1, + }, + }, + }, + LastModifiedLedgerSeq: 30705278, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeClaimableBalance, + ClaimableBalance: &xdr.ClaimableBalanceEntry{ + BalanceId: genericClaimableBalance, + Claimants: []xdr.Claimant{ + { + Type: 0, + V0: &xdr.ClaimantV0{ + Destination: testAccount1ID, + }, + }, + }, + Asset: xdr.Asset{ + Type: xdr.AssetTypeAssetTypeCreditAlphanum12, + AlphaNum12: &xdr.AlphaNum12{ + AssetCode: xdr.AssetCode12{1, 2, 3, 4, 5, 6, 7, 8, 9}, + Issuer: testAccount3ID, + }, + }, + Amount: 9990000000, + Ext: xdr.ClaimableBalanceEntryExt{ + V: 1, + V1: &xdr.ClaimableBalanceEntryExtensionV1{ + Ext: xdr.ClaimableBalanceEntryExtensionV1Ext{ + V: 1, + }, + Flags: 10, + }, + }, + }, + }, + } + return ingest.Change{ + Type: xdr.LedgerEntryTypeClaimableBalance, + Pre: &ledgerEntry, + Post: nil, + } +} + +func makeClaimableBalanceTestOutput() ClaimableBalanceOutput { + return ClaimableBalanceOutput{ + BalanceID: "000000000102030405060708090000000000000000000000000000000000000000000000", + Claimants: []Claimant{ + { + Destination: "GCEODJVUUVYVFD5KT4TOEDTMXQ76OPFOQC2EMYYMLPXQCUVPOB6XRWPQ", + Predicate: xdr.ClaimPredicate{ + Type: xdr.ClaimPredicateTypeClaimPredicateUnconditional, + }, + }, + }, + AssetIssuer: "GBT4YAEGJQ5YSFUMNKX6BPBUOCPNAIOFAVZOF6MIME2CECBMEIUXFZZN", + AssetType: "credit_alphanum12", + AssetCode: "\x01\x02\x03\x04\x05\x06\a\b\t", + AssetAmount: 999, + AssetID: -4023078858747574648, + Sponsor: null.StringFrom("GAAQEAYEAUDAOCAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABO3W"), + Flags: 10, + LastModifiedLedger: 30705278, + LedgerEntryChange: 2, + Deleted: true, + LedgerSequence: 10, + ClosedAt: time.Date(1970, time.January, 1, 0, 16, 40, 0, time.UTC), + } +} diff --git a/ingest/processors/config_setting.go b/ingest/processors/config_setting.go new file mode 100644 index 0000000000..822a4bc4a8 --- /dev/null +++ b/ingest/processors/config_setting.go @@ -0,0 +1,162 @@ +package processors + +import ( + "fmt" + "strconv" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +// TransformConfigSetting converts an config setting ledger change entry into a form suitable for BigQuery +func TransformConfigSetting(ledgerChange ingest.Change, header xdr.LedgerHeaderHistoryEntry) (ConfigSettingOutput, error) { + ledgerEntry, changeType, outputDeleted, err := ExtractEntryFromChange(ledgerChange) + if err != nil { + return ConfigSettingOutput{}, err + } + + configSetting, ok := ledgerEntry.Data.GetConfigSetting() + if !ok { + return ConfigSettingOutput{}, fmt.Errorf("could not extract config setting from ledger entry; actual type is %s", ledgerEntry.Data.Type) + } + + configSettingId := configSetting.ConfigSettingId + + contractMaxSizeBytes, _ := configSetting.GetContractMaxSizeBytes() + + contractCompute, _ := configSetting.GetContractCompute() + ledgerMaxInstructions := contractCompute.LedgerMaxInstructions + txMaxInstructions := contractCompute.TxMaxInstructions + feeRatePerInstructionsIncrement := contractCompute.FeeRatePerInstructionsIncrement + txMemoryLimit := contractCompute.TxMemoryLimit + + contractLedgerCost, _ := configSetting.GetContractLedgerCost() + ledgerMaxReadLedgerEntries := contractLedgerCost.LedgerMaxReadLedgerEntries + ledgerMaxReadBytes := contractLedgerCost.LedgerMaxReadBytes + ledgerMaxWriteLedgerEntries := contractLedgerCost.LedgerMaxWriteLedgerEntries + ledgerMaxWriteBytes := contractLedgerCost.LedgerMaxWriteBytes + txMaxReadLedgerEntries := contractLedgerCost.TxMaxReadLedgerEntries + txMaxReadBytes := contractLedgerCost.TxMaxReadBytes + txMaxWriteLedgerEntries := contractLedgerCost.TxMaxWriteLedgerEntries + txMaxWriteBytes := contractLedgerCost.TxMaxWriteBytes + feeReadLedgerEntry := contractLedgerCost.FeeReadLedgerEntry + feeWriteLedgerEntry := contractLedgerCost.FeeWriteLedgerEntry + feeRead1Kb := contractLedgerCost.FeeRead1Kb + bucketListTargetSizeBytes := contractLedgerCost.BucketListTargetSizeBytes + writeFee1KbBucketListLow := contractLedgerCost.WriteFee1KbBucketListLow + writeFee1KbBucketListHigh := contractLedgerCost.WriteFee1KbBucketListHigh + bucketListWriteFeeGrowthFactor := contractLedgerCost.BucketListWriteFeeGrowthFactor + + contractHistoricalData, _ := configSetting.GetContractHistoricalData() + feeHistorical1Kb := contractHistoricalData.FeeHistorical1Kb + + contractMetaData, _ := configSetting.GetContractEvents() + txMaxContractEventsSizeBytes := contractMetaData.TxMaxContractEventsSizeBytes + feeContractEvents1Kb := contractMetaData.FeeContractEvents1Kb + + contractBandwidth, _ := configSetting.GetContractBandwidth() + ledgerMaxTxsSizeBytes := contractBandwidth.LedgerMaxTxsSizeBytes + txMaxSizeBytes := contractBandwidth.TxMaxSizeBytes + feeTxSize1Kb := contractBandwidth.FeeTxSize1Kb + + paramsCpuInsns, _ := configSetting.GetContractCostParamsCpuInsns() + contractCostParamsCpuInsns := serializeParams(paramsCpuInsns) + + paramsMemBytes, _ := configSetting.GetContractCostParamsMemBytes() + contractCostParamsMemBytes := serializeParams(paramsMemBytes) + + contractDataKeySizeBytes, _ := configSetting.GetContractDataKeySizeBytes() + + contractDataEntrySizeBytes, _ := configSetting.GetContractDataEntrySizeBytes() + + stateArchivalSettings, _ := configSetting.GetStateArchivalSettings() + maxEntryTtl := stateArchivalSettings.MaxEntryTtl + minTemporaryTtl := stateArchivalSettings.MinTemporaryTtl + minPersistentTtl := stateArchivalSettings.MinPersistentTtl + persistentRentRateDenominator := stateArchivalSettings.PersistentRentRateDenominator + tempRentRateDenominator := stateArchivalSettings.TempRentRateDenominator + maxEntriesToArchive := stateArchivalSettings.MaxEntriesToArchive + bucketListSizeWindowSampleSize := stateArchivalSettings.BucketListSizeWindowSampleSize + evictionScanSize := stateArchivalSettings.EvictionScanSize + startingEvictionScanLevel := stateArchivalSettings.StartingEvictionScanLevel + + contractExecutionLanes, _ := configSetting.GetContractExecutionLanes() + ledgerMaxTxCount := contractExecutionLanes.LedgerMaxTxCount + + bucketList, _ := configSetting.GetBucketListSizeWindow() + bucketListSizeWindow := make([]uint64, 0, len(bucketList)) + for _, sizeWindow := range bucketList { + bucketListSizeWindow = append(bucketListSizeWindow, uint64(sizeWindow)) + } + + closedAt, err := TimePointToUTCTimeStamp(header.Header.ScpValue.CloseTime) + if err != nil { + return ConfigSettingOutput{}, err + } + + ledgerSequence := header.Header.LedgerSeq + + transformedConfigSetting := ConfigSettingOutput{ + ConfigSettingId: int32(configSettingId), + ContractMaxSizeBytes: uint32(contractMaxSizeBytes), + LedgerMaxInstructions: int64(ledgerMaxInstructions), + TxMaxInstructions: int64(txMaxInstructions), + FeeRatePerInstructionsIncrement: int64(feeRatePerInstructionsIncrement), + TxMemoryLimit: uint32(txMemoryLimit), + LedgerMaxReadLedgerEntries: uint32(ledgerMaxReadLedgerEntries), + LedgerMaxReadBytes: uint32(ledgerMaxReadBytes), + LedgerMaxWriteLedgerEntries: uint32(ledgerMaxWriteLedgerEntries), + LedgerMaxWriteBytes: uint32(ledgerMaxWriteBytes), + TxMaxReadLedgerEntries: uint32(txMaxReadLedgerEntries), + TxMaxReadBytes: uint32(txMaxReadBytes), + TxMaxWriteLedgerEntries: uint32(txMaxWriteLedgerEntries), + TxMaxWriteBytes: uint32(txMaxWriteBytes), + FeeReadLedgerEntry: int64(feeReadLedgerEntry), + FeeWriteLedgerEntry: int64(feeWriteLedgerEntry), + FeeRead1Kb: int64(feeRead1Kb), + BucketListTargetSizeBytes: int64(bucketListTargetSizeBytes), + WriteFee1KbBucketListLow: int64(writeFee1KbBucketListLow), + WriteFee1KbBucketListHigh: int64(writeFee1KbBucketListHigh), + BucketListWriteFeeGrowthFactor: uint32(bucketListWriteFeeGrowthFactor), + FeeHistorical1Kb: int64(feeHistorical1Kb), + TxMaxContractEventsSizeBytes: uint32(txMaxContractEventsSizeBytes), + FeeContractEvents1Kb: int64(feeContractEvents1Kb), + LedgerMaxTxsSizeBytes: uint32(ledgerMaxTxsSizeBytes), + TxMaxSizeBytes: uint32(txMaxSizeBytes), + FeeTxSize1Kb: int64(feeTxSize1Kb), + ContractCostParamsCpuInsns: contractCostParamsCpuInsns, + ContractCostParamsMemBytes: contractCostParamsMemBytes, + ContractDataKeySizeBytes: uint32(contractDataKeySizeBytes), + ContractDataEntrySizeBytes: uint32(contractDataEntrySizeBytes), + MaxEntryTtl: uint32(maxEntryTtl), + MinTemporaryTtl: uint32(minTemporaryTtl), + MinPersistentTtl: uint32(minPersistentTtl), + PersistentRentRateDenominator: int64(persistentRentRateDenominator), + TempRentRateDenominator: int64(tempRentRateDenominator), + MaxEntriesToArchive: uint32(maxEntriesToArchive), + BucketListSizeWindowSampleSize: uint32(bucketListSizeWindowSampleSize), + EvictionScanSize: uint64(evictionScanSize), + StartingEvictionScanLevel: uint32(startingEvictionScanLevel), + LedgerMaxTxCount: uint32(ledgerMaxTxCount), + BucketListSizeWindow: bucketListSizeWindow, + LastModifiedLedger: uint32(ledgerEntry.LastModifiedLedgerSeq), + LedgerEntryChange: uint32(changeType), + Deleted: outputDeleted, + ClosedAt: closedAt, + LedgerSequence: uint32(ledgerSequence), + } + return transformedConfigSetting, nil +} + +func serializeParams(costParams xdr.ContractCostParams) []map[string]string { + params := make([]map[string]string, 0, len(costParams)) + for _, contractCostParam := range costParams { + serializedParam := map[string]string{} + serializedParam["ExtV"] = strconv.Itoa(int(contractCostParam.Ext.V)) + serializedParam["ConstTerm"] = strconv.Itoa(int(contractCostParam.ConstTerm)) + serializedParam["LinearTerm"] = strconv.Itoa(int(contractCostParam.LinearTerm)) + params = append(params, serializedParam) + } + + return params +} diff --git a/ingest/processors/config_setting_test.go b/ingest/processors/config_setting_test.go new file mode 100644 index 0000000000..d545a51c14 --- /dev/null +++ b/ingest/processors/config_setting_test.go @@ -0,0 +1,140 @@ +package processors + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformConfigSetting(t *testing.T) { + type transformTest struct { + input ingest.Change + wantOutput ConfigSettingOutput + wantErr error + } + + hardCodedInput := makeConfigSettingTestInput() + hardCodedOutput := makeConfigSettingTestOutput() + tests := []transformTest{ + { + ingest.Change{ + Type: xdr.LedgerEntryTypeOffer, + Pre: nil, + Post: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + }, + }, + }, + ConfigSettingOutput{}, fmt.Errorf("could not extract config setting from ledger entry; actual type is LedgerEntryTypeOffer"), + }, + } + + for i := range hardCodedInput { + tests = append(tests, transformTest{ + input: hardCodedInput[i], + wantOutput: hardCodedOutput[i], + wantErr: nil, + }) + } + + for _, test := range tests { + header := xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + ScpValue: xdr.StellarValue{ + CloseTime: 1000, + }, + LedgerSeq: 10, + }, + } + actualOutput, actualError := TransformConfigSetting(test.input, header) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func makeConfigSettingTestInput() []ingest.Change { + var contractMaxByte xdr.Uint32 = 0 + + contractDataLedgerEntry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: 24229503, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeConfigSetting, + ConfigSetting: &xdr.ConfigSettingEntry{ + ConfigSettingId: xdr.ConfigSettingIdConfigSettingContractMaxSizeBytes, + ContractMaxSizeBytes: &contractMaxByte, + }, + }, + } + + return []ingest.Change{ + { + Type: xdr.LedgerEntryTypeConfigSetting, + Pre: &xdr.LedgerEntry{}, + Post: &contractDataLedgerEntry, + }, + } +} + +func makeConfigSettingTestOutput() []ConfigSettingOutput { + contractMapType := make([]map[string]string, 0) + bucket := make([]uint64, 0) + + return []ConfigSettingOutput{ + { + ConfigSettingId: 0, + ContractMaxSizeBytes: 0, + LedgerMaxInstructions: 0, + TxMaxInstructions: 0, + FeeRatePerInstructionsIncrement: 0, + TxMemoryLimit: 0, + LedgerMaxReadLedgerEntries: 0, + LedgerMaxReadBytes: 0, + LedgerMaxWriteLedgerEntries: 0, + LedgerMaxWriteBytes: 0, + TxMaxReadLedgerEntries: 0, + TxMaxReadBytes: 0, + TxMaxWriteLedgerEntries: 0, + TxMaxWriteBytes: 0, + FeeReadLedgerEntry: 0, + FeeWriteLedgerEntry: 0, + FeeRead1Kb: 0, + BucketListTargetSizeBytes: 0, + WriteFee1KbBucketListLow: 0, + WriteFee1KbBucketListHigh: 0, + BucketListWriteFeeGrowthFactor: 0, + FeeHistorical1Kb: 0, + TxMaxContractEventsSizeBytes: 0, + FeeContractEvents1Kb: 0, + LedgerMaxTxsSizeBytes: 0, + TxMaxSizeBytes: 0, + FeeTxSize1Kb: 0, + ContractCostParamsCpuInsns: contractMapType, + ContractCostParamsMemBytes: contractMapType, + ContractDataKeySizeBytes: 0, + ContractDataEntrySizeBytes: 0, + MaxEntryTtl: 0, + MinTemporaryTtl: 0, + MinPersistentTtl: 0, + AutoBumpLedgers: 0, + PersistentRentRateDenominator: 0, + TempRentRateDenominator: 0, + MaxEntriesToArchive: 0, + BucketListSizeWindowSampleSize: 0, + EvictionScanSize: 0, + StartingEvictionScanLevel: 0, + LedgerMaxTxCount: 0, + BucketListSizeWindow: bucket, + LastModifiedLedger: 24229503, + LedgerEntryChange: 1, + Deleted: false, + LedgerSequence: 10, + ClosedAt: time.Date(1970, time.January, 1, 0, 16, 40, 0, time.UTC), + }, + } +} diff --git a/ingest/processors/contract_code.go b/ingest/processors/contract_code.go new file mode 100644 index 0000000000..fe4b1dd4f8 --- /dev/null +++ b/ingest/processors/contract_code.go @@ -0,0 +1,86 @@ +package processors + +import ( + "fmt" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +// TransformContractCode converts a contract code ledger change entry into a form suitable for BigQuery +func TransformContractCode(ledgerChange ingest.Change, header xdr.LedgerHeaderHistoryEntry) (ContractCodeOutput, error) { + ledgerEntry, changeType, outputDeleted, err := ExtractEntryFromChange(ledgerChange) + if err != nil { + return ContractCodeOutput{}, err + } + + contractCode, ok := ledgerEntry.Data.GetContractCode() + if !ok { + return ContractCodeOutput{}, fmt.Errorf("could not extract contract code from ledger entry; actual type is %s", ledgerEntry.Data.Type) + } + + // LedgerEntryChange must contain a contract code change to be parsed, otherwise skip + if ledgerEntry.Data.Type != xdr.LedgerEntryTypeContractCode { + return ContractCodeOutput{}, nil + } + + ledgerKeyHash := LedgerEntryToLedgerKeyHash(ledgerEntry) + + contractCodeExtV := contractCode.Ext.V + + contractCodeHash := contractCode.Hash.HexString() + + closedAt, err := TimePointToUTCTimeStamp(header.Header.ScpValue.CloseTime) + if err != nil { + return ContractCodeOutput{}, err + } + + ledgerSequence := header.Header.LedgerSeq + + var outputNInstructions uint32 + var outputNFunctions uint32 + var outputNGlobals uint32 + var outputNTableEntries uint32 + var outputNTypes uint32 + var outputNDataSegments uint32 + var outputNElemSegments uint32 + var outputNImports uint32 + var outputNExports uint32 + var outputNDataSegmentBytes uint32 + + extV1, ok := contractCode.Ext.GetV1() + if ok { + outputNInstructions = uint32(extV1.CostInputs.NInstructions) + outputNFunctions = uint32(extV1.CostInputs.NFunctions) + outputNGlobals = uint32(extV1.CostInputs.NGlobals) + outputNTableEntries = uint32(extV1.CostInputs.NTableEntries) + outputNTypes = uint32(extV1.CostInputs.NTypes) + outputNDataSegments = uint32(extV1.CostInputs.NDataSegments) + outputNElemSegments = uint32(extV1.CostInputs.NElemSegments) + outputNImports = uint32(extV1.CostInputs.NImports) + outputNExports = uint32(extV1.CostInputs.NExports) + outputNDataSegmentBytes = uint32(extV1.CostInputs.NDataSegmentBytes) + } + + transformedCode := ContractCodeOutput{ + ContractCodeHash: contractCodeHash, + ContractCodeExtV: int32(contractCodeExtV), + LastModifiedLedger: uint32(ledgerEntry.LastModifiedLedgerSeq), + LedgerEntryChange: uint32(changeType), + Deleted: outputDeleted, + ClosedAt: closedAt, + LedgerSequence: uint32(ledgerSequence), + LedgerKeyHash: ledgerKeyHash, + NInstructions: outputNInstructions, + NFunctions: outputNFunctions, + NGlobals: outputNGlobals, + NTableEntries: outputNTableEntries, + NTypes: outputNTypes, + NDataSegments: outputNDataSegments, + NElemSegments: outputNElemSegments, + NImports: outputNImports, + NExports: outputNExports, + NDataSegmentBytes: outputNDataSegmentBytes, + } + return transformedCode, nil +} diff --git a/ingest/processors/contract_code_test.go b/ingest/processors/contract_code_test.go new file mode 100644 index 0000000000..2027ff7e81 --- /dev/null +++ b/ingest/processors/contract_code_test.go @@ -0,0 +1,123 @@ +package processors + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformContractCode(t *testing.T) { + type transformTest struct { + input ingest.Change + wantOutput ContractCodeOutput + wantErr error + } + + hardCodedInput := makeContractCodeTestInput() + hardCodedOutput := makeContractCodeTestOutput() + tests := []transformTest{ + { + ingest.Change{ + Type: xdr.LedgerEntryTypeOffer, + Pre: nil, + Post: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + }, + }, + }, + ContractCodeOutput{}, fmt.Errorf("could not extract contract code from ledger entry; actual type is LedgerEntryTypeOffer"), + }, + } + + for i := range hardCodedInput { + tests = append(tests, transformTest{ + input: hardCodedInput[i], + wantOutput: hardCodedOutput[i], + wantErr: nil, + }) + } + + for _, test := range tests { + header := xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + ScpValue: xdr.StellarValue{ + CloseTime: 1000, + }, + LedgerSeq: 10, + }, + } + actualOutput, actualError := TransformContractCode(test.input, header) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func makeContractCodeTestInput() []ingest.Change { + var hash [32]byte + + contractCodeLedgerEntry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: 24229503, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeContractCode, + ContractCode: &xdr.ContractCodeEntry{ + Hash: hash, + Ext: xdr.ContractCodeEntryExt{ + V: 1, + V1: &xdr.ContractCodeEntryV1{ + CostInputs: xdr.ContractCodeCostInputs{ + NInstructions: 1, + NFunctions: 2, + NGlobals: 3, + NTableEntries: 4, + NTypes: 5, + NDataSegments: 6, + NElemSegments: 7, + NImports: 8, + NExports: 9, + NDataSegmentBytes: 10, + }, + }, + }, + }, + }, + } + + return []ingest.Change{ + { + Type: xdr.LedgerEntryTypeContractCode, + Pre: &xdr.LedgerEntry{}, + Post: &contractCodeLedgerEntry, + }, + } +} + +func makeContractCodeTestOutput() []ContractCodeOutput { + return []ContractCodeOutput{ + { + ContractCodeHash: "0000000000000000000000000000000000000000000000000000000000000000", + ContractCodeExtV: 1, + LastModifiedLedger: 24229503, + LedgerEntryChange: 1, + Deleted: false, + LedgerSequence: 10, + ClosedAt: time.Date(1970, time.January, 1, 0, 16, 40, 0, time.UTC), + LedgerKeyHash: "dfed061dbe464e0ff320744fcd604ac08b39daa74fa24110936654cbcb915ccc", + NInstructions: 1, + NFunctions: 2, + NGlobals: 3, + NTableEntries: 4, + NTypes: 5, + NDataSegments: 6, + NElemSegments: 7, + NImports: 8, + NExports: 9, + NDataSegmentBytes: 10, + }, + } +} diff --git a/ingest/processors/contract_data.go b/ingest/processors/contract_data.go new file mode 100644 index 0000000000..aa2d288de4 --- /dev/null +++ b/ingest/processors/contract_data.go @@ -0,0 +1,357 @@ +package processors + +import ( + "fmt" + "math/big" + "strings" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/strkey" + "github.com/stellar/go/xdr" +) + +var ( + // these are storage DataKey enum + // https://github.com/stellar/rs-soroban-env/blob/v0.0.16/soroban-env-host/src/native_contract/token/storage_types.rs#L23 + balanceMetadataSym = xdr.ScSymbol("Balance") + issuerSym = xdr.ScSymbol("issuer") + assetCodeSym = xdr.ScSymbol("asset_code") + assetInfoSym = xdr.ScSymbol("AssetInfo") + assetInfoVec = &xdr.ScVec{ + xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &assetInfoSym, + }, + } + assetInfoKey = xdr.ScVal{ + Type: xdr.ScValTypeScvVec, + Vec: &assetInfoVec, + } +) + +type AssetFromContractDataFunc func(ledgerEntry xdr.LedgerEntry, passphrase string) *xdr.Asset +type ContractBalanceFromContractDataFunc func(ledgerEntry xdr.LedgerEntry, passphrase string) ([32]byte, *big.Int, bool) + +type TransformContractDataStruct struct { + AssetFromContractData AssetFromContractDataFunc + ContractBalanceFromContractData ContractBalanceFromContractDataFunc +} + +func NewTransformContractDataStruct(assetFrom AssetFromContractDataFunc, contractBalance ContractBalanceFromContractDataFunc) *TransformContractDataStruct { + return &TransformContractDataStruct{ + AssetFromContractData: assetFrom, + ContractBalanceFromContractData: contractBalance, + } +} + +// TransformContractData converts a contract data ledger change entry into a form suitable for BigQuery +func (t *TransformContractDataStruct) TransformContractData(ledgerChange ingest.Change, passphrase string, header xdr.LedgerHeaderHistoryEntry) (ContractDataOutput, error, bool) { + ledgerEntry, changeType, outputDeleted, err := ExtractEntryFromChange(ledgerChange) + if err != nil { + return ContractDataOutput{}, err, false + } + + contractData, ok := ledgerEntry.Data.GetContractData() + if !ok { + return ContractDataOutput{}, fmt.Errorf("could not extract contract data from ledger entry; actual type is %s", ledgerEntry.Data.Type), false + } + + if contractData.Key.Type.String() == "ScValTypeScvLedgerKeyNonce" { + // Is a nonce and should be discarded + return ContractDataOutput{}, nil, false + } + + ledgerKeyHash := LedgerEntryToLedgerKeyHash(ledgerEntry) + + var contractDataAssetType string + var contractDataAssetCode string + var contractDataAssetIssuer string + + contractDataAsset := t.AssetFromContractData(ledgerEntry, passphrase) + if contractDataAsset != nil { + contractDataAssetType = contractDataAsset.Type.String() + contractDataAssetCode = contractDataAsset.GetCode() + contractDataAssetCode = strings.ReplaceAll(contractDataAssetCode, "\x00", "") + contractDataAssetIssuer = contractDataAsset.GetIssuer() + } + + var contractDataBalanceHolder string + var contractDataBalance string + + dataBalanceHolder, dataBalance, _ := t.ContractBalanceFromContractData(ledgerEntry, passphrase) + if dataBalance != nil { + holderHashByte, _ := xdr.Hash(dataBalanceHolder).MarshalBinary() + contractDataBalanceHolder, _ = strkey.Encode(strkey.VersionByteContract, holderHashByte) + contractDataBalance = dataBalance.String() + } + + contractDataContractId, ok := contractData.Contract.GetContractId() + if !ok { + return ContractDataOutput{}, fmt.Errorf("could not extract contractId data information from contractData"), false + } + + contractDataKeyType := contractData.Key.Type.String() + contractDataContractIdByte, _ := contractDataContractId.MarshalBinary() + outputContractDataContractId, _ := strkey.Encode(strkey.VersionByteContract, contractDataContractIdByte) + + contractDataDurability := contractData.Durability.String() + + closedAt, err := TimePointToUTCTimeStamp(header.Header.ScpValue.CloseTime) + if err != nil { + return ContractDataOutput{}, err, false + } + + ledgerSequence := header.Header.LedgerSeq + + outputKey, outputKeyDecoded := serializeScVal(contractData.Key) + outputVal, outputValDecoded := serializeScVal(contractData.Val) + + outputContractDataXDR, err := xdr.MarshalBase64(contractData) + if err != nil { + return ContractDataOutput{}, err, false + } + + transformedData := ContractDataOutput{ + ContractId: outputContractDataContractId, + ContractKeyType: contractDataKeyType, + ContractDurability: contractDataDurability, + ContractDataAssetCode: contractDataAssetCode, + ContractDataAssetIssuer: contractDataAssetIssuer, + ContractDataAssetType: contractDataAssetType, + ContractDataBalanceHolder: contractDataBalanceHolder, + ContractDataBalance: contractDataBalance, + LastModifiedLedger: uint32(ledgerEntry.LastModifiedLedgerSeq), + LedgerEntryChange: uint32(changeType), + Deleted: outputDeleted, + ClosedAt: closedAt, + LedgerSequence: uint32(ledgerSequence), + LedgerKeyHash: ledgerKeyHash, + Key: outputKey, + KeyDecoded: outputKeyDecoded, + Val: outputVal, + ValDecoded: outputValDecoded, + ContractDataXDR: outputContractDataXDR, + } + return transformedData, nil, true +} + +// AssetFromContractData takes a ledger entry and verifies if the ledger entry +// corresponds to the asset info entry written to contract storage by the Stellar +// Asset Contract upon initialization. +// +// Note that AssetFromContractData will ignore forged asset info entries by +// deriving the Stellar Asset Contract ID from the asset info entry and comparing +// it to the contract ID found in the ledger entry. +// +// If the given ledger entry is a verified asset info entry, +// AssetFromContractData will return the corresponding Stellar asset. Otherwise, +// it returns nil. +// +// References: +// https://github.com/stellar/rs-soroban-env/blob/v0.0.16/soroban-env-host/src/native_contract/token/public_types.rs#L21 +// https://github.com/stellar/rs-soroban-env/blob/v0.0.16/soroban-env-host/src/native_contract/token/asset_info.rs#L6 +// https://github.com/stellar/rs-soroban-env/blob/v0.0.16/soroban-env-host/src/native_contract/token/contract.rs#L115 +// +// The asset info in `ContractData` entry takes the following form: +// +// - Instance storage - it's part of contract instance data storage +// +// - Key: a vector with one element, which is the symbol "AssetInfo" +// +// ScVal{ Vec: ScVec({ ScVal{ Sym: ScSymbol("AssetInfo") }})} +// +// - Value: a map with two key-value pairs: code and issuer +// +// ScVal{ Map: ScMap( +// { ScVal{ Sym: ScSymbol("asset_code") } -> ScVal{ Str: ScString(...) } }, +// { ScVal{ Sym: ScSymbol("issuer") } -> ScVal{ Bytes: ScBytes(...) } } +// )} +func AssetFromContractData(ledgerEntry xdr.LedgerEntry, passphrase string) *xdr.Asset { + contractData, ok := ledgerEntry.Data.GetContractData() + if !ok { + return nil + } + if contractData.Key.Type != xdr.ScValTypeScvLedgerKeyContractInstance { + return nil + } + contractInstanceData, ok := contractData.Val.GetInstance() + if !ok || contractInstanceData.Storage == nil { + return nil + } + + nativeAssetContractID, err := xdr.MustNewNativeAsset().ContractID(passphrase) + if err != nil { + return nil + } + + var assetInfo *xdr.ScVal + for _, mapEntry := range *contractInstanceData.Storage { + if mapEntry.Key.Equals(assetInfoKey) { + // clone the map entry to avoid reference to loop iterator + mapValXdr, cloneErr := mapEntry.Val.MarshalBinary() + if cloneErr != nil { + return nil + } + assetInfo = &xdr.ScVal{} + cloneErr = assetInfo.UnmarshalBinary(mapValXdr) + if cloneErr != nil { + return nil + } + break + } + } + + if assetInfo == nil { + return nil + } + + vecPtr, ok := assetInfo.GetVec() + if !ok || vecPtr == nil || len(*vecPtr) != 2 { + return nil + } + vec := *vecPtr + + sym, ok := vec[0].GetSym() + if !ok { + return nil + } + switch sym { + case "AlphaNum4": + case "AlphaNum12": + case "Native": + if contractData.Contract.ContractId != nil && (*contractData.Contract.ContractId) == nativeAssetContractID { + asset := xdr.MustNewNativeAsset() + return &asset + } + default: + return nil + } + + var assetCode, assetIssuer string + assetMapPtr, ok := vec[1].GetMap() + if !ok || assetMapPtr == nil || len(*assetMapPtr) != 2 { + return nil + } + assetMap := *assetMapPtr + + assetCodeEntry, assetIssuerEntry := assetMap[0], assetMap[1] + if sym, ok = assetCodeEntry.Key.GetSym(); !ok || sym != assetCodeSym { + return nil + } + assetCodeSc, ok := assetCodeEntry.Val.GetStr() + if !ok { + return nil + } + if assetCode = string(assetCodeSc); assetCode == "" { + return nil + } + + if sym, ok = assetIssuerEntry.Key.GetSym(); !ok || sym != issuerSym { + return nil + } + assetIssuerSc, ok := assetIssuerEntry.Val.GetBytes() + if !ok { + return nil + } + assetIssuer, err = strkey.Encode(strkey.VersionByteAccountID, assetIssuerSc) + if err != nil { + return nil + } + + asset, err := xdr.NewCreditAsset(assetCode, assetIssuer) + if err != nil { + return nil + } + + expectedID, err := asset.ContractID(passphrase) + if err != nil { + return nil + } + if contractData.Contract.ContractId == nil || expectedID != *(contractData.Contract.ContractId) { + return nil + } + + return &asset +} + +// ContractBalanceFromContractData takes a ledger entry and verifies that the +// ledger entry corresponds to the balance entry written to contract storage by +// the Stellar Asset Contract. +// +// Reference: +// +// https://github.com/stellar/rs-soroban-env/blob/da325551829d31dcbfa71427d51c18e71a121c5f/soroban-env-host/src/native_contract/token/storage_types.rs#L11-L24 +func ContractBalanceFromContractData(ledgerEntry xdr.LedgerEntry, passphrase string) ([32]byte, *big.Int, bool) { + contractData, ok := ledgerEntry.Data.GetContractData() + if !ok { + return [32]byte{}, nil, false + } + + _, err := xdr.MustNewNativeAsset().ContractID(passphrase) + if err != nil { + return [32]byte{}, nil, false + } + + if contractData.Contract.ContractId == nil { + return [32]byte{}, nil, false + } + + keyEnumVecPtr, ok := contractData.Key.GetVec() + if !ok || keyEnumVecPtr == nil { + return [32]byte{}, nil, false + } + keyEnumVec := *keyEnumVecPtr + if len(keyEnumVec) != 2 || !keyEnumVec[0].Equals( + xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &balanceMetadataSym, + }, + ) { + return [32]byte{}, nil, false + } + + scAddress, ok := keyEnumVec[1].GetAddress() + if !ok { + return [32]byte{}, nil, false + } + + holder, ok := scAddress.GetContractId() + if !ok { + return [32]byte{}, nil, false + } + + balanceMapPtr, ok := contractData.Val.GetMap() + if !ok || balanceMapPtr == nil { + return [32]byte{}, nil, false + } + balanceMap := *balanceMapPtr + if !ok || len(balanceMap) != 3 { + return [32]byte{}, nil, false + } + + var keySym xdr.ScSymbol + if keySym, ok = balanceMap[0].Key.GetSym(); !ok || keySym != "amount" { + return [32]byte{}, nil, false + } + if keySym, ok = balanceMap[1].Key.GetSym(); !ok || keySym != "authorized" || + !balanceMap[1].Val.IsBool() { + return [32]byte{}, nil, false + } + if keySym, ok = balanceMap[2].Key.GetSym(); !ok || keySym != "clawback" || + !balanceMap[2].Val.IsBool() { + return [32]byte{}, nil, false + } + amount, ok := balanceMap[0].Val.GetI128() + if !ok { + return [32]byte{}, nil, false + } + + // amount cannot be negative + // https://github.com/stellar/rs-soroban-env/blob/a66f0815ba06a2f5328ac420950690fd1642f887/soroban-env-host/src/native_contract/token/balance.rs#L92-L93 + if int64(amount.Hi) < 0 { + return [32]byte{}, nil, false + } + amt := new(big.Int).Lsh(new(big.Int).SetInt64(int64(amount.Hi)), 64) + amt.Add(amt, new(big.Int).SetUint64(uint64(amount.Lo))) + return holder, amt, true +} diff --git a/ingest/processors/contract_data_test.go b/ingest/processors/contract_data_test.go new file mode 100644 index 0000000000..0c7b199e17 --- /dev/null +++ b/ingest/processors/contract_data_test.go @@ -0,0 +1,174 @@ +package processors + +import ( + "fmt" + "math/big" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformContractData(t *testing.T) { + type transformTest struct { + input ingest.Change + passphrase string + wantOutput ContractDataOutput + wantErr error + } + + hardCodedInput := makeContractDataTestInput() + hardCodedOutput := makeContractDataTestOutput() + tests := []transformTest{ + { + ingest.Change{ + Type: xdr.LedgerEntryTypeOffer, + Pre: nil, + Post: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + }, + }, + }, + "unit test", + ContractDataOutput{}, fmt.Errorf("could not extract contract data from ledger entry; actual type is LedgerEntryTypeOffer"), + }, + } + + for i := range hardCodedInput { + tests = append(tests, transformTest{ + input: hardCodedInput[i], + passphrase: "unit test", + wantOutput: hardCodedOutput[i], + wantErr: nil, + }) + } + + for _, test := range tests { + header := xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + ScpValue: xdr.StellarValue{ + CloseTime: 1000, + }, + LedgerSeq: 10, + }, + } + TransformContractData := NewTransformContractDataStruct(MockAssetFromContractData, MockContractBalanceFromContractData) + actualOutput, actualError, _ := TransformContractData.TransformContractData(test.input, test.passphrase, header) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func MockAssetFromContractData(ledgerEntry xdr.LedgerEntry, passphrase string) *xdr.Asset { + return &xdr.Asset{ + Type: xdr.AssetTypeAssetTypeNative, + } +} + +func MockContractBalanceFromContractData(ledgerEntry xdr.LedgerEntry, passphrase string) ([32]byte, *big.Int, bool) { + var holder [32]byte + return holder, big.NewInt(0), true +} + +func makeContractDataTestInput() []ingest.Change { + var hash xdr.Hash + var scStr xdr.ScString = "a" + var testVal bool = true + + contractDataLedgerEntry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: 24229503, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.ContractDataEntry{ + Contract: xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + ContractId: &hash, + }, + Key: xdr.ScVal{ + Type: xdr.ScValTypeScvContractInstance, + Instance: &xdr.ScContractInstance{ + Executable: xdr.ContractExecutable{ + Type: xdr.ContractExecutableTypeContractExecutableWasm, + WasmHash: &hash, + }, + Storage: &xdr.ScMap{ + xdr.ScMapEntry{ + Key: xdr.ScVal{ + Type: xdr.ScValTypeScvString, + Str: &scStr, + }, + Val: xdr.ScVal{ + Type: xdr.ScValTypeScvString, + Str: &scStr, + }, + }, + }, + }, + }, + Durability: xdr.ContractDataDurabilityPersistent, + Val: xdr.ScVal{ + Type: xdr.ScValTypeScvBool, + B: &testVal, + }, + }, + }, + } + + return []ingest.Change{ + { + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{}, + Post: &contractDataLedgerEntry, + }, + } +} + +func makeContractDataTestOutput() []ContractDataOutput { + key := map[string]string{ + "type": "Instance", + "value": "AAAAEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAADgAAAAFhAAAAAAAADgAAAAFhAAAA", + } + + keyDecoded := map[string]string{ + "type": "Instance", + "value": "0000000000000000000000000000000000000000000000000000000000000000: [{a a}]", + } + + val := map[string]string{ + "type": "B", + "value": "AAAAAAAAAAE=", + } + + valDecoded := map[string]string{ + "type": "B", + "value": "true", + } + + return []ContractDataOutput{ + { + ContractId: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + ContractKeyType: "ScValTypeScvContractInstance", + ContractDurability: "ContractDataDurabilityPersistent", + ContractDataAssetCode: "", + ContractDataAssetIssuer: "", + ContractDataAssetType: "AssetTypeAssetTypeNative", + ContractDataBalanceHolder: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + ContractDataBalance: "0", + LastModifiedLedger: 24229503, + LedgerEntryChange: 1, + Deleted: false, + LedgerSequence: 10, + ClosedAt: time.Date(1970, time.January, 1, 0, 16, 40, 0, time.UTC), + LedgerKeyHash: "abfc33272095a9df4c310cff189040192a8aee6f6a23b6b462889114d80728ca", + Key: key, + KeyDecoded: keyDecoded, + Val: val, + ValDecoded: valDecoded, + ContractDataXDR: "AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAA4AAAABYQAAAAAAAA4AAAABYQAAAAAAAAEAAAAAAAAAAQ==", + }, + } +} diff --git a/ingest/processors/contract_events.go b/ingest/processors/contract_events.go new file mode 100644 index 0000000000..19b36b47b2 --- /dev/null +++ b/ingest/processors/contract_events.go @@ -0,0 +1,151 @@ +package processors + +import ( + "encoding/base64" + "fmt" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/strkey" + "github.com/stellar/go/toid" + "github.com/stellar/go/xdr" +) + +// TransformContractEvent converts a transaction's contract events and diagnostic events into a form suitable for BigQuery. +// It is known that contract events are a subset of the diagnostic events XDR definition. We are opting to call all of these events +// contract events for better clarity to data analytics users. +func TransformContractEvent(transaction ingest.LedgerTransaction, lhe xdr.LedgerHeaderHistoryEntry) ([]ContractEventOutput, error) { + ledgerHeader := lhe.Header + outputTransactionHash := HashToHexString(transaction.Result.TransactionHash) + outputLedgerSequence := uint32(ledgerHeader.LedgerSeq) + + transactionIndex := uint32(transaction.Index) + + outputTransactionID := toid.New(int32(outputLedgerSequence), int32(transactionIndex), 0).ToInt64() + + outputCloseTime, err := TimePointToUTCTimeStamp(ledgerHeader.ScpValue.CloseTime) + if err != nil { + return []ContractEventOutput{}, fmt.Errorf("for ledger %d; transaction %d (transaction id=%d): %v", outputLedgerSequence, transactionIndex, outputTransactionID, err) + } + + // GetDiagnosticEvents will return all contract events and diagnostic events emitted + contractEvents, err := transaction.GetDiagnosticEvents() + if err != nil { + return []ContractEventOutput{}, err + } + + var transformedContractEvents []ContractEventOutput + + for _, contractEvent := range contractEvents { + var outputContractId string + outputTopicsJson := make(map[string][]map[string]string, 1) + outputTopicsDecodedJson := make(map[string][]map[string]string, 1) + + outputInSuccessfulContractCall := contractEvent.InSuccessfulContractCall + event := contractEvent.Event + outputType := event.Type + outputTypeString := event.Type.String() + + eventTopics := getEventTopics(event.Body) + outputTopics, outputTopicsDecoded := serializeScValArray(eventTopics) + outputTopicsJson["topics"] = outputTopics + outputTopicsDecodedJson["topics_decoded"] = outputTopicsDecoded + + eventData := getEventData(event.Body) + outputData, outputDataDecoded := serializeScVal(eventData) + + // Convert the xdrContactId to string + // TODO: https://stellarorg.atlassian.net/browse/HUBBLE-386 this should be a stellar/go/xdr function + if event.ContractId != nil { + contractId := *event.ContractId + contractIdByte, _ := contractId.MarshalBinary() + outputContractId, _ = strkey.Encode(strkey.VersionByteContract, contractIdByte) + } + + outputContractEventXDR, err := xdr.MarshalBase64(contractEvent) + if err != nil { + return []ContractEventOutput{}, err + } + + outputTransactionID := toid.New(int32(outputLedgerSequence), int32(transactionIndex), 0).ToInt64() + outputSuccessful := transaction.Result.Successful() + + transformedDiagnosticEvent := ContractEventOutput{ + TransactionHash: outputTransactionHash, + TransactionID: outputTransactionID, + Successful: outputSuccessful, + LedgerSequence: outputLedgerSequence, + ClosedAt: outputCloseTime, + InSuccessfulContractCall: outputInSuccessfulContractCall, + ContractId: outputContractId, + Type: int32(outputType), + TypeString: outputTypeString, + Topics: outputTopicsJson, + TopicsDecoded: outputTopicsDecodedJson, + Data: outputData, + DataDecoded: outputDataDecoded, + ContractEventXDR: outputContractEventXDR, + } + + transformedContractEvents = append(transformedContractEvents, transformedDiagnosticEvent) + } + + return transformedContractEvents, nil +} + +// TODO this should be a stellar/go/xdr function +func getEventTopics(eventBody xdr.ContractEventBody) []xdr.ScVal { + switch eventBody.V { + case 0: + contractEventV0 := eventBody.MustV0() + return contractEventV0.Topics + default: + panic("unsupported event body version: " + string(eventBody.V)) + } +} + +// TODO this should be a stellar/go/xdr function +func getEventData(eventBody xdr.ContractEventBody) xdr.ScVal { + switch eventBody.V { + case 0: + contractEventV0 := eventBody.MustV0() + return contractEventV0.Data + default: + panic("unsupported event body version: " + string(eventBody.V)) + } +} + +// TODO this should also be used in the operations processor +func serializeScVal(scVal xdr.ScVal) (map[string]string, map[string]string) { + serializedData := map[string]string{} + serializedData["value"] = "n/a" + serializedData["type"] = "n/a" + + serializedDataDecoded := map[string]string{} + serializedDataDecoded["value"] = "n/a" + serializedDataDecoded["type"] = "n/a" + + if scValTypeName, ok := scVal.ArmForSwitch(int32(scVal.Type)); ok { + serializedData["type"] = scValTypeName + serializedDataDecoded["type"] = scValTypeName + if raw, err := scVal.MarshalBinary(); err == nil { + serializedData["value"] = base64.StdEncoding.EncodeToString(raw) + serializedDataDecoded["value"] = scVal.String() + } + } + + return serializedData, serializedDataDecoded +} + +// TODO this should also be used in the operations processor +func serializeScValArray(scVals []xdr.ScVal) ([]map[string]string, []map[string]string) { + data := make([]map[string]string, 0, len(scVals)) + dataDecoded := make([]map[string]string, 0, len(scVals)) + + for _, scVal := range scVals { + serializedData, serializedDataDecoded := serializeScVal(scVal) + data = append(data, serializedData) + dataDecoded = append(dataDecoded, serializedDataDecoded) + } + + return data, dataDecoded +} diff --git a/ingest/processors/contract_events_test.go b/ingest/processors/contract_events_test.go new file mode 100644 index 0000000000..994c170868 --- /dev/null +++ b/ingest/processors/contract_events_test.go @@ -0,0 +1,208 @@ +package processors + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformContractEvent(t *testing.T) { + type inputStruct struct { + transaction ingest.LedgerTransaction + historyHeader xdr.LedgerHeaderHistoryEntry + } + type transformTest struct { + input inputStruct + wantOutput []ContractEventOutput + wantErr error + } + + hardCodedTransaction, hardCodedLedgerHeader, err := makeContractEventTestInput() + assert.NoError(t, err) + hardCodedOutput, err := makeContractEventTestOutput() + assert.NoError(t, err) + + tests := []transformTest{} + + for i := range hardCodedTransaction { + tests = append(tests, transformTest{ + input: inputStruct{hardCodedTransaction[i], hardCodedLedgerHeader[i]}, + wantOutput: hardCodedOutput[i], + wantErr: nil, + }) + } + + for _, test := range tests { + actualOutput, actualError := TransformContractEvent(test.input.transaction, test.input.historyHeader) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func makeContractEventTestOutput() (output [][]ContractEventOutput, err error) { + + topics := make(map[string][]map[string]string, 1) + topics["topics"] = []map[string]string{ + { + "type": "B", + "value": "AAAAAAAAAAE=", + }, + } + + topicsDecoded := make(map[string][]map[string]string, 1) + topicsDecoded["topics_decoded"] = []map[string]string{ + { + "type": "B", + "value": "true", + }, + } + + data := map[string]string{ + "type": "B", + "value": "AAAAAAAAAAE=", + } + + dataDecoded := map[string]string{ + "type": "B", + "value": "true", + } + + output = [][]ContractEventOutput{{ + ContractEventOutput{ + TransactionHash: "a87fef5eeb260269c380f2de456aad72b59bb315aaac777860456e09dac0bafb", + TransactionID: 131090201534533632, + Successful: false, + LedgerSequence: 30521816, + ClosedAt: time.Date(2020, time.July, 9, 5, 28, 42, 0, time.UTC), + InSuccessfulContractCall: true, + ContractId: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + Type: 2, + TypeString: "ContractEventTypeDiagnostic", + Topics: topics, + TopicsDecoded: topicsDecoded, + Data: data, + DataDecoded: dataDecoded, + ContractEventXDR: "AAAAAQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAB", + }, + }} + return +} +func makeContractEventTestInput() (transaction []ingest.LedgerTransaction, historyHeader []xdr.LedgerHeaderHistoryEntry, err error) { + hardCodedMemoText := "HL5aCgozQHIW7sSc5XdcfmR" + hardCodedTransactionHash := xdr.Hash([32]byte{0xa8, 0x7f, 0xef, 0x5e, 0xeb, 0x26, 0x2, 0x69, 0xc3, 0x80, 0xf2, 0xde, 0x45, 0x6a, 0xad, 0x72, 0xb5, 0x9b, 0xb3, 0x15, 0xaa, 0xac, 0x77, 0x78, 0x60, 0x45, 0x6e, 0x9, 0xda, 0xc0, 0xba, 0xfb}) + var hardCodedContractId xdr.Hash + hardCodedBool := true + hardCodedTxMetaV3 := xdr.TransactionMetaV3{ + SorobanMeta: &xdr.SorobanTransactionMeta{ + DiagnosticEvents: []xdr.DiagnosticEvent{ + { + InSuccessfulContractCall: true, + Event: xdr.ContractEvent{ + Ext: xdr.ExtensionPoint{ + V: 0, + }, + ContractId: &hardCodedContractId, + Type: xdr.ContractEventTypeDiagnostic, + Body: xdr.ContractEventBody{ + V: 0, + V0: &xdr.ContractEventV0{ + Topics: []xdr.ScVal{ + { + Type: xdr.ScValTypeScvBool, + B: &hardCodedBool, + }, + }, + Data: xdr.ScVal{ + Type: xdr.ScValTypeScvBool, + B: &hardCodedBool, + }, + }, + }, + }, + }, + }, + }, + } + + genericResultResults := &[]xdr.OperationResult{ + { + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeCreateAccount, + CreateAccountResult: &xdr.CreateAccountResult{ + Code: 0, + }, + }, + }, + } + hardCodedMeta := xdr.TransactionMeta{ + V: 3, + V3: &hardCodedTxMetaV3, + } + + destination := xdr.MuxedAccount{ + Type: xdr.CryptoKeyTypeKeyTypeEd25519, + Ed25519: &xdr.Uint256{1, 2, 3}, + } + + transaction = []ingest.LedgerTransaction{ + { + Index: 1, + UnsafeMeta: hardCodedMeta, + Envelope: xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: testAccount1, + SeqNum: 112351890582290871, + Memo: xdr.Memo{ + Type: xdr.MemoTypeMemoText, + Text: &hardCodedMemoText, + }, + Fee: 90000, + Cond: xdr.Preconditions{ + Type: xdr.PreconditionTypePrecondTime, + TimeBounds: &xdr.TimeBounds{ + MinTime: 0, + MaxTime: 1594272628, + }, + }, + Operations: []xdr.Operation{ + { + SourceAccount: &testAccount2, + Body: xdr.OperationBody{ + Type: xdr.OperationTypePathPaymentStrictReceive, + PathPaymentStrictReceiveOp: &xdr.PathPaymentStrictReceiveOp{ + Destination: destination, + }, + }, + }, + }, + }, + }, + }, + Result: xdr.TransactionResultPair{ + TransactionHash: hardCodedTransactionHash, + Result: xdr.TransactionResult{ + FeeCharged: 300, + Result: xdr.TransactionResultResult{ + Code: xdr.TransactionResultCodeTxFailed, + Results: genericResultResults, + }, + }, + }, + }, + } + historyHeader = []xdr.LedgerHeaderHistoryEntry{ + { + Header: xdr.LedgerHeader{ + LedgerSeq: 30521816, + ScpValue: xdr.StellarValue{CloseTime: 1594272522}, + }, + }, + } + return +} diff --git a/ingest/processors/effects.go b/ingest/processors/effects.go new file mode 100644 index 0000000000..5bcf0a8676 --- /dev/null +++ b/ingest/processors/effects.go @@ -0,0 +1,1512 @@ +package processors + +import ( + "encoding/base64" + + "fmt" + "reflect" + "sort" + "strconv" + + "github.com/guregu/null" + "github.com/stellar/go/amount" + "github.com/stellar/go/ingest" + "github.com/stellar/go/keypair" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/strkey" + "github.com/stellar/go/support/contractevents" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +func TransformEffect(transaction ingest.LedgerTransaction, ledgerSeq uint32, ledgerCloseMeta xdr.LedgerCloseMeta, networkPassphrase string) ([]EffectOutput, error) { + effects := []EffectOutput{} + + outputCloseTime, err := GetCloseTime(ledgerCloseMeta) + if err != nil { + return effects, err + } + + for opi, op := range transaction.Envelope.Operations() { + operation := transactionOperationWrapper{ + index: uint32(opi), + transaction: transaction, + operation: op, + ledgerSequence: ledgerSeq, + network: networkPassphrase, + ledgerClosed: outputCloseTime, + } + + p, err := operation.effects() + if err != nil { + return effects, errors.Wrapf(err, "reading operation %v effects", operation.ID()) + } + + effects = append(effects, p...) + + } + + return effects, nil +} + +// Effects returns the operation effects +func (operation *transactionOperationWrapper) effects() ([]EffectOutput, error) { + if !operation.transaction.Result.Successful() { + return []EffectOutput{}, nil + } + var ( + op = operation.operation + err error + ) + + changes, err := operation.transaction.GetOperationChanges(operation.index) + if err != nil { + return nil, err + } + + wrapper := &effectsWrapper{ + effects: []EffectOutput{}, + operation: operation, + } + + switch operation.OperationType() { + case xdr.OperationTypeCreateAccount: + wrapper.addAccountCreatedEffects() + case xdr.OperationTypePayment: + wrapper.addPaymentEffects() + case xdr.OperationTypePathPaymentStrictReceive: + err = wrapper.pathPaymentStrictReceiveEffects() + case xdr.OperationTypePathPaymentStrictSend: + err = wrapper.addPathPaymentStrictSendEffects() + case xdr.OperationTypeManageSellOffer: + err = wrapper.addManageSellOfferEffects() + case xdr.OperationTypeManageBuyOffer: + err = wrapper.addManageBuyOfferEffects() + case xdr.OperationTypeCreatePassiveSellOffer: + err = wrapper.addCreatePassiveSellOfferEffect() + case xdr.OperationTypeSetOptions: + wrapper.addSetOptionsEffects() + case xdr.OperationTypeChangeTrust: + err = wrapper.addChangeTrustEffects() + case xdr.OperationTypeAllowTrust: + err = wrapper.addAllowTrustEffects() + case xdr.OperationTypeAccountMerge: + wrapper.addAccountMergeEffects() + case xdr.OperationTypeInflation: + wrapper.addInflationEffects() + case xdr.OperationTypeManageData: + err = wrapper.addManageDataEffects() + case xdr.OperationTypeBumpSequence: + err = wrapper.addBumpSequenceEffects() + case xdr.OperationTypeCreateClaimableBalance: + err = wrapper.addCreateClaimableBalanceEffects(changes) + case xdr.OperationTypeClaimClaimableBalance: + err = wrapper.addClaimClaimableBalanceEffects(changes) + case xdr.OperationTypeBeginSponsoringFutureReserves, xdr.OperationTypeEndSponsoringFutureReserves, xdr.OperationTypeRevokeSponsorship: + // The effects of these operations are obtained indirectly from the ledger entries + case xdr.OperationTypeClawback: + err = wrapper.addClawbackEffects() + case xdr.OperationTypeClawbackClaimableBalance: + err = wrapper.addClawbackClaimableBalanceEffects(changes) + case xdr.OperationTypeSetTrustLineFlags: + err = wrapper.addSetTrustLineFlagsEffects() + case xdr.OperationTypeLiquidityPoolDeposit: + err = wrapper.addLiquidityPoolDepositEffect() + case xdr.OperationTypeLiquidityPoolWithdraw: + err = wrapper.addLiquidityPoolWithdrawEffect() + case xdr.OperationTypeInvokeHostFunction: + // If there's an invokeHostFunction operation, there's definitely V3 + // meta in the transaction, which means this error is real. + diagnosticEvents, innerErr := operation.transaction.GetDiagnosticEvents() + if innerErr != nil { + return nil, innerErr + } + + // For now, the only effects are related to the events themselves. + // Possible add'l work: https://github.com/stellar/go/issues/4585 + err = wrapper.addInvokeHostFunctionEffects(filterEvents(diagnosticEvents)) + case xdr.OperationTypeExtendFootprintTtl: + err = wrapper.addExtendFootprintTtlEffect() + case xdr.OperationTypeRestoreFootprint: + err = wrapper.addRestoreFootprintExpirationEffect() + default: + return nil, fmt.Errorf("unknown operation type: %s", op.Body.Type) + } + if err != nil { + return nil, err + } + + // Effects generated for multiple operations. Keep the effect categories + // separated so they are "together" in case of different order or meta + // changes generate by core (unordered_map). + + // Sponsorships + for _, change := range changes { + if err = wrapper.addLedgerEntrySponsorshipEffects(change); err != nil { + return nil, err + } + wrapper.addSignerSponsorshipEffects(change) + } + + // Liquidity pools + for _, change := range changes { + // Effects caused by ChangeTrust (creation), AllowTrust and SetTrustlineFlags (removal through revocation) + wrapper.addLedgerEntryLiquidityPoolEffects(change) + } + + for i := range wrapper.effects { + wrapper.effects[i].LedgerClosed = operation.ledgerClosed + wrapper.effects[i].LedgerSequence = operation.ledgerSequence + wrapper.effects[i].EffectIndex = uint32(i) + wrapper.effects[i].EffectId = fmt.Sprintf("%d-%d", wrapper.effects[i].OperationID, wrapper.effects[i].EffectIndex) + } + + return wrapper.effects, nil +} + +type effectsWrapper struct { + effects []EffectOutput + operation *transactionOperationWrapper +} + +func (e *effectsWrapper) add(address string, addressMuxed null.String, effectType EffectType, details map[string]interface{}) { + e.effects = append(e.effects, EffectOutput{ + Address: address, + AddressMuxed: addressMuxed, + OperationID: e.operation.ID(), + TypeString: EffectTypeNames[effectType], + Type: int32(effectType), + Details: details, + }) +} + +func (e *effectsWrapper) addUnmuxed(address *xdr.AccountId, effectType EffectType, details map[string]interface{}) { + e.add(address.Address(), null.String{}, effectType, details) +} + +func (e *effectsWrapper) addMuxed(address *xdr.MuxedAccount, effectType EffectType, details map[string]interface{}) { + var addressMuxed null.String + if address.Type == xdr.CryptoKeyTypeKeyTypeMuxedEd25519 { + addressMuxed = null.StringFrom(address.Address()) + } + accID := address.ToAccountId() + e.add(accID.Address(), addressMuxed, effectType, details) +} + +var sponsoringEffectsTable = map[xdr.LedgerEntryType]struct { + created, updated, removed EffectType +}{ + xdr.LedgerEntryTypeAccount: { + created: EffectAccountSponsorshipCreated, + updated: EffectAccountSponsorshipUpdated, + removed: EffectAccountSponsorshipRemoved, + }, + xdr.LedgerEntryTypeTrustline: { + created: EffectTrustlineSponsorshipCreated, + updated: EffectTrustlineSponsorshipUpdated, + removed: EffectTrustlineSponsorshipRemoved, + }, + xdr.LedgerEntryTypeData: { + created: EffectDataSponsorshipCreated, + updated: EffectDataSponsorshipUpdated, + removed: EffectDataSponsorshipRemoved, + }, + xdr.LedgerEntryTypeClaimableBalance: { + created: EffectClaimableBalanceSponsorshipCreated, + updated: EffectClaimableBalanceSponsorshipUpdated, + removed: EffectClaimableBalanceSponsorshipRemoved, + }, + + // We intentionally don't have Sponsoring effects for Offer + // entries because we don't generate creation effects for them. +} + +func (e *effectsWrapper) addSignerSponsorshipEffects(change ingest.Change) { + if change.Type != xdr.LedgerEntryTypeAccount { + return + } + + preSigners := map[string]xdr.AccountId{} + postSigners := map[string]xdr.AccountId{} + if change.Pre != nil { + account := change.Pre.Data.MustAccount() + preSigners = account.SponsorPerSigner() + } + if change.Post != nil { + account := change.Post.Data.MustAccount() + postSigners = account.SponsorPerSigner() + } + + var all []string + for signer := range preSigners { + all = append(all, signer) + } + for signer := range postSigners { + if _, ok := preSigners[signer]; ok { + continue + } + all = append(all, signer) + } + sort.Strings(all) + + for _, signer := range all { + pre, foundPre := preSigners[signer] + post, foundPost := postSigners[signer] + details := map[string]interface{}{} + + switch { + case !foundPre && !foundPost: + continue + case !foundPre && foundPost: + details["sponsor"] = post.Address() + details["signer"] = signer + srcAccount := change.Post.Data.MustAccount().AccountId + e.addUnmuxed(&srcAccount, EffectSignerSponsorshipCreated, details) + case !foundPost && foundPre: + details["former_sponsor"] = pre.Address() + details["signer"] = signer + srcAccount := change.Pre.Data.MustAccount().AccountId + e.addUnmuxed(&srcAccount, EffectSignerSponsorshipRemoved, details) + case foundPre && foundPost: + formerSponsor := pre.Address() + newSponsor := post.Address() + if formerSponsor == newSponsor { + continue + } + + details["former_sponsor"] = formerSponsor + details["new_sponsor"] = newSponsor + details["signer"] = signer + srcAccount := change.Post.Data.MustAccount().AccountId + e.addUnmuxed(&srcAccount, EffectSignerSponsorshipUpdated, details) + } + } +} + +func (e *effectsWrapper) addLedgerEntrySponsorshipEffects(change ingest.Change) error { + effectsForEntryType, found := sponsoringEffectsTable[change.Type] + if !found { + return nil + } + + details := map[string]interface{}{} + var effectType EffectType + + switch { + case (change.Pre == nil || change.Pre.SponsoringID() == nil) && + (change.Post != nil && change.Post.SponsoringID() != nil): + effectType = effectsForEntryType.created + details["sponsor"] = (*change.Post.SponsoringID()).Address() + case (change.Pre != nil && change.Pre.SponsoringID() != nil) && + (change.Post == nil || change.Post.SponsoringID() == nil): + effectType = effectsForEntryType.removed + details["former_sponsor"] = (*change.Pre.SponsoringID()).Address() + case (change.Pre != nil && change.Pre.SponsoringID() != nil) && + (change.Post != nil && change.Post.SponsoringID() != nil): + preSponsor := (*change.Pre.SponsoringID()).Address() + postSponsor := (*change.Post.SponsoringID()).Address() + if preSponsor == postSponsor { + return nil + } + effectType = effectsForEntryType.updated + details["new_sponsor"] = postSponsor + details["former_sponsor"] = preSponsor + default: + return nil + } + + var ( + accountID *xdr.AccountId + muxedAccount *xdr.MuxedAccount + ) + + var data xdr.LedgerEntryData + if change.Post != nil { + data = change.Post.Data + } else { + data = change.Pre.Data + } + + switch change.Type { + case xdr.LedgerEntryTypeAccount: + a := data.MustAccount().AccountId + accountID = &a + case xdr.LedgerEntryTypeTrustline: + tl := data.MustTrustLine() + accountID = &tl.AccountId + if tl.Asset.Type == xdr.AssetTypeAssetTypePoolShare { + details["asset_type"] = "liquidity_pool" + details["liquidity_pool_id"] = PoolIDToString(*tl.Asset.LiquidityPoolId) + } else { + details["asset"] = tl.Asset.ToAsset().StringCanonical() + } + case xdr.LedgerEntryTypeData: + muxedAccount = e.operation.SourceAccount() + details["data_name"] = data.MustData().DataName + case xdr.LedgerEntryTypeClaimableBalance: + muxedAccount = e.operation.SourceAccount() + var err error + details["balance_id"], err = xdr.MarshalHex(data.MustClaimableBalance().BalanceId) + if err != nil { + return errors.Wrapf(err, "Invalid balanceId in change from op %d", e.operation.index) + } + case xdr.LedgerEntryTypeLiquidityPool: + // liquidity pools cannot be sponsored + fallthrough + default: + return errors.Errorf("invalid sponsorship ledger entry type %v", change.Type.String()) + } + + if accountID != nil { + e.addUnmuxed(accountID, effectType, details) + } else { + e.addMuxed(muxedAccount, effectType, details) + } + + return nil +} + +func (e *effectsWrapper) addLedgerEntryLiquidityPoolEffects(change ingest.Change) error { + if change.Type != xdr.LedgerEntryTypeLiquidityPool { + return nil + } + var effectType EffectType + + var details map[string]interface{} + switch { + case change.Pre == nil && change.Post != nil: + effectType = EffectLiquidityPoolCreated + details = map[string]interface{}{ + "liquidity_pool": liquidityPoolDetails(change.Post.Data.LiquidityPool), + } + case change.Pre != nil && change.Post == nil: + effectType = EffectLiquidityPoolRemoved + poolID := change.Pre.Data.LiquidityPool.LiquidityPoolId + details = map[string]interface{}{ + "liquidity_pool_id": PoolIDToString(poolID), + } + default: + return nil + } + e.addMuxed( + e.operation.SourceAccount(), + effectType, + details, + ) + + return nil +} + +func (e *effectsWrapper) addAccountCreatedEffects() { + op := e.operation.operation.Body.MustCreateAccountOp() + + e.addUnmuxed( + &op.Destination, + EffectAccountCreated, + map[string]interface{}{ + "starting_balance": amount.String(op.StartingBalance), + }, + ) + e.addMuxed( + e.operation.SourceAccount(), + EffectAccountDebited, + map[string]interface{}{ + "asset_type": "native", + "amount": amount.String(op.StartingBalance), + }, + ) + e.addUnmuxed( + &op.Destination, + EffectSignerCreated, + map[string]interface{}{ + "public_key": op.Destination.Address(), + "weight": keypair.DefaultSignerWeight, + }, + ) +} + +func (e *effectsWrapper) addPaymentEffects() { + op := e.operation.operation.Body.MustPaymentOp() + + details := map[string]interface{}{"amount": amount.String(op.Amount)} + addAssetDetails(details, op.Asset, "") + + e.addMuxed( + &op.Destination, + EffectAccountCredited, + details, + ) + e.addMuxed( + e.operation.SourceAccount(), + EffectAccountDebited, + details, + ) +} + +func (e *effectsWrapper) pathPaymentStrictReceiveEffects() error { + op := e.operation.operation.Body.MustPathPaymentStrictReceiveOp() + resultSuccess := e.operation.OperationResult().MustPathPaymentStrictReceiveResult().MustSuccess() + source := e.operation.SourceAccount() + + details := map[string]interface{}{"amount": amount.String(op.DestAmount)} + addAssetDetails(details, op.DestAsset, "") + + e.addMuxed( + &op.Destination, + EffectAccountCredited, + details, + ) + + result := e.operation.OperationResult().MustPathPaymentStrictReceiveResult() + details = map[string]interface{}{"amount": amount.String(result.SendAmount())} + addAssetDetails(details, op.SendAsset, "") + + e.addMuxed( + source, + EffectAccountDebited, + details, + ) + + return e.addIngestTradeEffects(*source, resultSuccess.Offers, false) +} + +func (e *effectsWrapper) addPathPaymentStrictSendEffects() error { + source := e.operation.SourceAccount() + op := e.operation.operation.Body.MustPathPaymentStrictSendOp() + resultSuccess := e.operation.OperationResult().MustPathPaymentStrictSendResult().MustSuccess() + result := e.operation.OperationResult().MustPathPaymentStrictSendResult() + + details := map[string]interface{}{"amount": amount.String(result.DestAmount())} + addAssetDetails(details, op.DestAsset, "") + e.addMuxed(&op.Destination, EffectAccountCredited, details) + + details = map[string]interface{}{"amount": amount.String(op.SendAmount)} + addAssetDetails(details, op.SendAsset, "") + e.addMuxed(source, EffectAccountDebited, details) + + return e.addIngestTradeEffects(*source, resultSuccess.Offers, true) +} + +func (e *effectsWrapper) addManageSellOfferEffects() error { + source := e.operation.SourceAccount() + result := e.operation.OperationResult().MustManageSellOfferResult().MustSuccess() + return e.addIngestTradeEffects(*source, result.OffersClaimed, false) +} + +func (e *effectsWrapper) addManageBuyOfferEffects() error { + source := e.operation.SourceAccount() + result := e.operation.OperationResult().MustManageBuyOfferResult().MustSuccess() + return e.addIngestTradeEffects(*source, result.OffersClaimed, false) +} + +func (e *effectsWrapper) addCreatePassiveSellOfferEffect() error { + result := e.operation.OperationResult() + source := e.operation.SourceAccount() + + var claims []xdr.ClaimAtom + + // KNOWN ISSUE: stellar-core creates results for CreatePassiveOffer operations + // with the wrong result arm set. + if result.Type == xdr.OperationTypeManageSellOffer { + claims = result.MustManageSellOfferResult().MustSuccess().OffersClaimed + } else { + claims = result.MustCreatePassiveSellOfferResult().MustSuccess().OffersClaimed + } + + return e.addIngestTradeEffects(*source, claims, false) +} + +func (e *effectsWrapper) addSetOptionsEffects() error { + source := e.operation.SourceAccount() + op := e.operation.operation.Body.MustSetOptionsOp() + + if op.HomeDomain != nil { + e.addMuxed(source, EffectAccountHomeDomainUpdated, + map[string]interface{}{ + "home_domain": string(*op.HomeDomain), + }, + ) + } + + thresholdDetails := map[string]interface{}{} + + if op.LowThreshold != nil { + thresholdDetails["low_threshold"] = *op.LowThreshold + } + + if op.MedThreshold != nil { + thresholdDetails["med_threshold"] = *op.MedThreshold + } + + if op.HighThreshold != nil { + thresholdDetails["high_threshold"] = *op.HighThreshold + } + + if len(thresholdDetails) > 0 { + e.addMuxed(source, EffectAccountThresholdsUpdated, thresholdDetails) + } + + flagDetails := map[string]interface{}{} + if op.SetFlags != nil { + setAuthFlagDetails(flagDetails, xdr.AccountFlags(*op.SetFlags), true) + } + if op.ClearFlags != nil { + setAuthFlagDetails(flagDetails, xdr.AccountFlags(*op.ClearFlags), false) + } + + if len(flagDetails) > 0 { + e.addMuxed(source, EffectAccountFlagsUpdated, flagDetails) + } + + if op.InflationDest != nil { + e.addMuxed(source, EffectAccountInflationDestinationUpdated, + map[string]interface{}{ + "inflation_destination": op.InflationDest.Address(), + }, + ) + } + changes, err := e.operation.transaction.GetOperationChanges(e.operation.index) + if err != nil { + return err + } + + for _, change := range changes { + if change.Type != xdr.LedgerEntryTypeAccount { + continue + } + + beforeAccount := change.Pre.Data.MustAccount() + afterAccount := change.Post.Data.MustAccount() + + before := beforeAccount.SignerSummary() + after := afterAccount.SignerSummary() + + // if before and after are the same, the signers have not changed + if reflect.DeepEqual(before, after) { + continue + } + + beforeSortedSigners := []string{} + for signer := range before { + beforeSortedSigners = append(beforeSortedSigners, signer) + } + sort.Strings(beforeSortedSigners) + + for _, addy := range beforeSortedSigners { + weight, ok := after[addy] + if !ok { + e.addMuxed(source, EffectSignerRemoved, map[string]interface{}{ + "public_key": addy, + }) + continue + } + + if weight != before[addy] { + e.addMuxed(source, EffectSignerUpdated, map[string]interface{}{ + "public_key": addy, + "weight": weight, + }) + } + } + + afterSortedSigners := []string{} + for signer := range after { + afterSortedSigners = append(afterSortedSigners, signer) + } + sort.Strings(afterSortedSigners) + + // Add the "created" effects + for _, addy := range afterSortedSigners { + weight := after[addy] + // if `addy` is in before, the previous for loop should have recorded + // the update, so skip this key + if _, ok := before[addy]; ok { + continue + } + + e.addMuxed(source, EffectSignerCreated, map[string]interface{}{ + "public_key": addy, + "weight": weight, + }) + } + } + return nil +} + +func (e *effectsWrapper) addChangeTrustEffects() error { + source := e.operation.SourceAccount() + + op := e.operation.operation.Body.MustChangeTrustOp() + changes, err := e.operation.transaction.GetOperationChanges(e.operation.index) + if err != nil { + return err + } + + // NOTE: when an account trusts itself, the transaction is successful but + // no ledger entries are actually modified. + for _, change := range changes { + if change.Type != xdr.LedgerEntryTypeTrustline { + continue + } + + var ( + effect EffectType + trustLine xdr.TrustLineEntry + ) + + switch { + case change.Pre == nil && change.Post != nil: + effect = EffectTrustlineCreated + trustLine = *change.Post.Data.TrustLine + case change.Pre != nil && change.Post == nil: + effect = EffectTrustlineRemoved + trustLine = *change.Pre.Data.TrustLine + case change.Pre != nil && change.Post != nil: + effect = EffectTrustlineUpdated + trustLine = *change.Post.Data.TrustLine + default: + panic("Invalid change") + } + + // We want to add a single effect for change_trust op. If it's modifying + // credit_asset search for credit_asset trustline, otherwise search for + // liquidity_pool. + if op.Line.Type != trustLine.Asset.Type { + continue + } + + details := map[string]interface{}{"limit": amount.String(op.Limit)} + if trustLine.Asset.Type == xdr.AssetTypeAssetTypePoolShare { + // The only change_trust ops that can modify LP are those with + // asset=liquidity_pool so *op.Line.LiquidityPool below is available. + if err := addLiquidityPoolAssetDetails(details, *op.Line.LiquidityPool); err != nil { + return err + } + } else { + addAssetDetails(details, op.Line.ToAsset(), "") + } + + e.addMuxed(source, effect, details) + break + } + + return nil +} + +func (e *effectsWrapper) addAllowTrustEffects() error { + source := e.operation.SourceAccount() + op := e.operation.operation.Body.MustAllowTrustOp() + asset := op.Asset.ToAsset(source.ToAccountId()) + details := map[string]interface{}{ + "trustor": op.Trustor.Address(), + } + addAssetDetails(details, asset, "") + + switch { + case xdr.TrustLineFlags(op.Authorize).IsAuthorized(): + e.addMuxed(source, EffectTrustlineFlagsUpdated, details) + // Forward compatibility + setFlags := xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag) + e.addTrustLineFlagsEffect(source, &op.Trustor, asset, &setFlags, nil) + case xdr.TrustLineFlags(op.Authorize).IsAuthorizedToMaintainLiabilitiesFlag(): + e.addMuxed( + source, + EffectTrustlineFlagsUpdated, + details, + ) + // Forward compatibility + setFlags := xdr.Uint32(xdr.TrustLineFlagsAuthorizedToMaintainLiabilitiesFlag) + e.addTrustLineFlagsEffect(source, &op.Trustor, asset, &setFlags, nil) + default: + e.addMuxed(source, EffectTrustlineFlagsUpdated, details) + // Forward compatibility, show both as cleared + clearFlags := xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag | xdr.TrustLineFlagsAuthorizedToMaintainLiabilitiesFlag) + e.addTrustLineFlagsEffect(source, &op.Trustor, asset, nil, &clearFlags) + } + return e.addLiquidityPoolRevokedEffect() +} + +func (e *effectsWrapper) addAccountMergeEffects() { + source := e.operation.SourceAccount() + + dest := e.operation.operation.Body.MustDestination() + result := e.operation.OperationResult().MustAccountMergeResult() + details := map[string]interface{}{ + "amount": amount.String(result.MustSourceAccountBalance()), + "asset_type": "native", + } + + e.addMuxed(source, EffectAccountDebited, details) + e.addMuxed(&dest, EffectAccountCredited, details) + e.addMuxed(source, EffectAccountRemoved, map[string]interface{}{}) +} + +func (e *effectsWrapper) addInflationEffects() { + payouts := e.operation.OperationResult().MustInflationResult().MustPayouts() + for _, payout := range payouts { + e.addUnmuxed(&payout.Destination, EffectAccountCredited, + map[string]interface{}{ + "amount": amount.String(payout.Amount), + "asset_type": "native", + }, + ) + } +} + +func (e *effectsWrapper) addManageDataEffects() error { + source := e.operation.SourceAccount() + op := e.operation.operation.Body.MustManageDataOp() + details := map[string]interface{}{"name": op.DataName} + effect := EffectType(0) + changes, err := e.operation.transaction.GetOperationChanges(e.operation.index) + if err != nil { + return err + } + + for _, change := range changes { + if change.Type != xdr.LedgerEntryTypeData { + continue + } + + before := change.Pre + after := change.Post + + if after != nil { + raw := after.Data.MustData().DataValue + details["value"] = base64.StdEncoding.EncodeToString(raw) + } + + switch { + case before == nil && after != nil: + effect = EffectDataCreated + case before != nil && after == nil: + effect = EffectDataRemoved + case before != nil && after != nil: + effect = EffectDataUpdated + default: + panic("Invalid before-and-after state") + } + + break + } + + e.addMuxed(source, effect, details) + return nil +} + +func (e *effectsWrapper) addBumpSequenceEffects() error { + source := e.operation.SourceAccount() + changes, err := e.operation.transaction.GetOperationChanges(e.operation.index) + if err != nil { + return err + } + + for _, change := range changes { + if change.Type != xdr.LedgerEntryTypeAccount { + continue + } + + before := change.Pre + after := change.Post + + beforeAccount := before.Data.MustAccount() + afterAccount := after.Data.MustAccount() + + if beforeAccount.SeqNum != afterAccount.SeqNum { + details := map[string]interface{}{"new_seq": afterAccount.SeqNum} + e.addMuxed(source, EffectSequenceBumped, details) + } + break + } + + return nil +} + +func setClaimableBalanceFlagDetails(details map[string]interface{}, flags xdr.ClaimableBalanceFlags) { + if flags.IsClawbackEnabled() { + details["claimable_balance_clawback_enabled_flag"] = true + return + } +} + +func (e *effectsWrapper) addCreateClaimableBalanceEffects(changes []ingest.Change) error { + source := e.operation.SourceAccount() + var cb *xdr.ClaimableBalanceEntry + for _, change := range changes { + if change.Type != xdr.LedgerEntryTypeClaimableBalance || change.Post == nil { + continue + } + cb = change.Post.Data.ClaimableBalance + e.addClaimableBalanceEntryCreatedEffects(source, cb) + break + } + if cb == nil { + return errors.New("claimable balance entry not found") + } + + details := map[string]interface{}{ + "amount": amount.String(cb.Amount), + } + addAssetDetails(details, cb.Asset, "") + e.addMuxed( + source, + EffectAccountDebited, + details, + ) + + return nil +} + +func (e *effectsWrapper) addClaimableBalanceEntryCreatedEffects(source *xdr.MuxedAccount, cb *xdr.ClaimableBalanceEntry) error { + id, err := xdr.MarshalHex(cb.BalanceId) + if err != nil { + return err + } + details := map[string]interface{}{ + "balance_id": id, + "amount": amount.String(cb.Amount), + "asset": cb.Asset.StringCanonical(), + } + setClaimableBalanceFlagDetails(details, cb.Flags()) + e.addMuxed( + source, + EffectClaimableBalanceCreated, + details, + ) + // EffectClaimableBalanceClaimantCreated can be generated by + // `create_claimable_balance` operation but also by `liquidity_pool_withdraw` + // operation causing a revocation. + // In case of `create_claimable_balance` we use `op.Claimants` to make + // effects backward compatible. The reason for this is that Stellar-Core + // changes all `rel_before` predicated to `abs_before` when tx is included + // in the ledger. + var claimants []xdr.Claimant + if op, ok := e.operation.operation.Body.GetCreateClaimableBalanceOp(); ok { + claimants = op.Claimants + } else { + claimants = cb.Claimants + } + for _, c := range claimants { + cv0 := c.MustV0() + e.addUnmuxed( + &cv0.Destination, + EffectClaimableBalanceClaimantCreated, + map[string]interface{}{ + "balance_id": id, + "amount": amount.String(cb.Amount), + "predicate": cv0.Predicate, + "asset": cb.Asset.StringCanonical(), + }, + ) + } + return err +} + +func (e *effectsWrapper) addClaimClaimableBalanceEffects(changes []ingest.Change) error { + op := e.operation.operation.Body.MustClaimClaimableBalanceOp() + + balanceID, err := xdr.MarshalHex(op.BalanceId) + if err != nil { + return fmt.Errorf("invalid balanceId in op: %d", e.operation.index) + } + + var cBalance xdr.ClaimableBalanceEntry + found := false + for _, change := range changes { + if change.Type != xdr.LedgerEntryTypeClaimableBalance { + continue + } + + if change.Pre != nil && change.Post == nil { + cBalance = change.Pre.Data.MustClaimableBalance() + preBalanceID, err := xdr.MarshalHex(cBalance.BalanceId) + if err != nil { + return fmt.Errorf("invalid balanceId in meta changes for op: %d", e.operation.index) + } + + if preBalanceID == balanceID { + found = true + break + } + } + } + + if !found { + return fmt.Errorf("change not found for balanceId : %s", balanceID) + } + + details := map[string]interface{}{ + "amount": amount.String(cBalance.Amount), + "balance_id": balanceID, + "asset": cBalance.Asset.StringCanonical(), + } + setClaimableBalanceFlagDetails(details, cBalance.Flags()) + source := e.operation.SourceAccount() + e.addMuxed( + source, + EffectClaimableBalanceClaimed, + details, + ) + + details = map[string]interface{}{ + "amount": amount.String(cBalance.Amount), + } + addAssetDetails(details, cBalance.Asset, "") + e.addMuxed( + source, + EffectAccountCredited, + details, + ) + + return nil +} + +func (e *effectsWrapper) addIngestTradeEffects(buyer xdr.MuxedAccount, claims []xdr.ClaimAtom, isPathPayment bool) error { + for _, claim := range claims { + if claim.AmountSold() == 0 && claim.AmountBought() == 0 { + continue + } + switch claim.Type { + case xdr.ClaimAtomTypeClaimAtomTypeLiquidityPool: + if err := e.addClaimLiquidityPoolTradeEffect(claim); err != nil { + return err + } + default: + e.addClaimTradeEffects(buyer, claim, isPathPayment) + } + } + return nil +} + +func (e *effectsWrapper) addClaimTradeEffects(buyer xdr.MuxedAccount, claim xdr.ClaimAtom, isPathPayment bool) { + seller := claim.SellerId() + bd, sd := tradeDetails(buyer, seller, claim) + + tradeEffects := []EffectType{ + EffectTrade, + EffectOfferUpdated, + EffectOfferRemoved, + EffectOfferCreated, + } + + for n, effect := range tradeEffects { + // skip EffectOfferCreated if OperationType is path_payment + if n == 3 && isPathPayment { + continue + } + + e.addMuxed( + &buyer, + effect, + bd, + ) + + e.addUnmuxed( + &seller, + effect, + sd, + ) + } +} + +func (e *effectsWrapper) addClaimLiquidityPoolTradeEffect(claim xdr.ClaimAtom) error { + lp, _, err := e.operation.getLiquidityPoolAndProductDelta(&claim.LiquidityPool.LiquidityPoolId) + if err != nil { + return err + } + details := map[string]interface{}{ + "liquidity_pool": liquidityPoolDetails(lp), + "sold": map[string]string{ + "asset": claim.LiquidityPool.AssetSold.StringCanonical(), + "amount": amount.String(claim.LiquidityPool.AmountSold), + }, + "bought": map[string]string{ + "asset": claim.LiquidityPool.AssetBought.StringCanonical(), + "amount": amount.String(claim.LiquidityPool.AmountBought), + }, + } + e.addMuxed(e.operation.SourceAccount(), EffectLiquidityPoolTrade, details) + return nil +} + +func (e *effectsWrapper) addClawbackEffects() error { + op := e.operation.operation.Body.MustClawbackOp() + details := map[string]interface{}{ + "amount": amount.String(op.Amount), + } + source := e.operation.SourceAccount() + addAssetDetails(details, op.Asset, "") + + // The funds will be burned, but even with that, we generated an account credited effect + e.addMuxed( + source, + EffectAccountCredited, + details, + ) + + e.addMuxed( + &op.From, + EffectAccountDebited, + details, + ) + + return nil +} + +func (e *effectsWrapper) addClawbackClaimableBalanceEffects(changes []ingest.Change) error { + op := e.operation.operation.Body.MustClawbackClaimableBalanceOp() + balanceId, err := xdr.MarshalHex(op.BalanceId) + if err != nil { + return errors.Wrapf(err, "Invalid balanceId in op %d", e.operation.index) + } + details := map[string]interface{}{ + "balance_id": balanceId, + } + source := e.operation.SourceAccount() + e.addMuxed( + source, + EffectClaimableBalanceClawedBack, + details, + ) + + // Generate the account credited effect (although the funds will be burned) for the asset issuer + for _, c := range changes { + if c.Type == xdr.LedgerEntryTypeClaimableBalance && c.Post == nil && c.Pre != nil { + cb := c.Pre.Data.ClaimableBalance + details = map[string]interface{}{"amount": amount.String(cb.Amount)} + addAssetDetails(details, cb.Asset, "") + e.addMuxed( + source, + EffectAccountCredited, + details, + ) + break + } + } + + return nil +} + +func (e *effectsWrapper) addSetTrustLineFlagsEffects() error { + source := e.operation.SourceAccount() + op := e.operation.operation.Body.MustSetTrustLineFlagsOp() + e.addTrustLineFlagsEffect(source, &op.Trustor, op.Asset, &op.SetFlags, &op.ClearFlags) + return e.addLiquidityPoolRevokedEffect() +} + +func (e *effectsWrapper) addTrustLineFlagsEffect( + account *xdr.MuxedAccount, + trustor *xdr.AccountId, + asset xdr.Asset, + setFlags *xdr.Uint32, + clearFlags *xdr.Uint32) { + details := map[string]interface{}{ + "trustor": trustor.Address(), + } + addAssetDetails(details, asset, "") + + var flagDetailsAdded bool + if setFlags != nil { + setTrustLineFlagDetails(details, xdr.TrustLineFlags(*setFlags), true) + flagDetailsAdded = true + } + if clearFlags != nil { + setTrustLineFlagDetails(details, xdr.TrustLineFlags(*clearFlags), false) + flagDetailsAdded = true + } + + if flagDetailsAdded { + e.addMuxed(account, EffectTrustlineFlagsUpdated, details) + } +} + +func setTrustLineFlagDetails(flagDetails map[string]interface{}, flags xdr.TrustLineFlags, setValue bool) { + if flags.IsAuthorized() { + flagDetails["authorized_flag"] = setValue + } + if flags.IsAuthorizedToMaintainLiabilitiesFlag() { + flagDetails["authorized_to_maintain_liabilites"] = setValue + } + if flags.IsClawbackEnabledFlag() { + flagDetails["clawback_enabled_flag"] = setValue + } +} + +type sortableClaimableBalanceEntries []*xdr.ClaimableBalanceEntry + +func (s sortableClaimableBalanceEntries) Len() int { return len(s) } +func (s sortableClaimableBalanceEntries) Less(i, j int) bool { return s[i].Asset.LessThan(s[j].Asset) } +func (s sortableClaimableBalanceEntries) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +func (e *effectsWrapper) addLiquidityPoolRevokedEffect() error { + source := e.operation.SourceAccount() + lp, delta, err := e.operation.getLiquidityPoolAndProductDelta(nil) + if err != nil { + if err == errLiquidityPoolChangeNotFound { + // no revocation happened + return nil + } + return err + } + changes, err := e.operation.transaction.GetOperationChanges(e.operation.index) + if err != nil { + return err + } + assetToCBID := map[string]string{} + var cbs sortableClaimableBalanceEntries + for _, change := range changes { + if change.Type == xdr.LedgerEntryTypeClaimableBalance && change.Pre == nil && change.Post != nil { + cb := change.Post.Data.ClaimableBalance + id, err := xdr.MarshalHex(cb.BalanceId) + if err != nil { + return err + } + assetToCBID[cb.Asset.StringCanonical()] = id + cbs = append(cbs, cb) + } + } + if len(assetToCBID) == 0 { + // no claimable balances were created, and thus, no revocation happened + return nil + } + // Core's claimable balance metadata isn't ordered, so we order it ourselves + // so that effects are ordered consistently + sort.Sort(cbs) + for _, cb := range cbs { + if err := e.addClaimableBalanceEntryCreatedEffects(source, cb); err != nil { + return err + } + } + + reservesRevoked := make([]map[string]string, 0, 2) + for _, aa := range []base.AssetAmount{ + { + Asset: lp.Body.ConstantProduct.Params.AssetA.StringCanonical(), + Amount: amount.String(-delta.ReserveA), + }, + { + Asset: lp.Body.ConstantProduct.Params.AssetB.StringCanonical(), + Amount: amount.String(-delta.ReserveB), + }, + } { + if cbID, ok := assetToCBID[aa.Asset]; ok { + assetAmountDetail := map[string]string{ + "asset": aa.Asset, + "amount": aa.Amount, + "claimable_balance_id": cbID, + } + reservesRevoked = append(reservesRevoked, assetAmountDetail) + } + } + details := map[string]interface{}{ + "liquidity_pool": liquidityPoolDetails(lp), + "reserves_revoked": reservesRevoked, + "shares_revoked": amount.String(-delta.TotalPoolShares), + } + e.addMuxed(source, EffectLiquidityPoolRevoked, details) + return nil +} + +func setAuthFlagDetails(flagDetails map[string]interface{}, flags xdr.AccountFlags, setValue bool) { + if flags.IsAuthRequired() { + flagDetails["auth_required_flag"] = setValue + } + if flags.IsAuthRevocable() { + flagDetails["auth_revocable_flag"] = setValue + } + if flags.IsAuthImmutable() { + flagDetails["auth_immutable_flag"] = setValue + } + if flags.IsAuthClawbackEnabled() { + flagDetails["auth_clawback_enabled_flag"] = setValue + } +} + +func tradeDetails(buyer xdr.MuxedAccount, seller xdr.AccountId, claim xdr.ClaimAtom) (bd map[string]interface{}, sd map[string]interface{}) { + bd = map[string]interface{}{ + "offer_id": claim.OfferId(), + "seller": seller.Address(), + "bought_amount": amount.String(claim.AmountSold()), + "sold_amount": amount.String(claim.AmountBought()), + } + addAssetDetails(bd, claim.AssetSold(), "bought_") + addAssetDetails(bd, claim.AssetBought(), "sold_") + + sd = map[string]interface{}{ + "offer_id": claim.OfferId(), + "bought_amount": amount.String(claim.AmountBought()), + "sold_amount": amount.String(claim.AmountSold()), + } + addAccountAndMuxedAccountDetails(sd, buyer, "seller") + addAssetDetails(sd, claim.AssetBought(), "bought_") + addAssetDetails(sd, claim.AssetSold(), "sold_") + + return +} + +func liquidityPoolDetails(lp *xdr.LiquidityPoolEntry) map[string]interface{} { + return map[string]interface{}{ + "id": PoolIDToString(lp.LiquidityPoolId), + "fee_bp": uint32(lp.Body.ConstantProduct.Params.Fee), + "type": "constant_product", + "total_trustlines": strconv.FormatInt(int64(lp.Body.ConstantProduct.PoolSharesTrustLineCount), 10), + "total_shares": amount.String(lp.Body.ConstantProduct.TotalPoolShares), + "reserves": []base.AssetAmount{ + { + Asset: lp.Body.ConstantProduct.Params.AssetA.StringCanonical(), + Amount: amount.String(lp.Body.ConstantProduct.ReserveA), + }, + { + Asset: lp.Body.ConstantProduct.Params.AssetB.StringCanonical(), + Amount: amount.String(lp.Body.ConstantProduct.ReserveB), + }, + }, + } +} + +func (e *effectsWrapper) addLiquidityPoolDepositEffect() error { + op := e.operation.operation.Body.MustLiquidityPoolDepositOp() + lp, delta, err := e.operation.getLiquidityPoolAndProductDelta(&op.LiquidityPoolId) + if err != nil { + return err + } + details := map[string]interface{}{ + "liquidity_pool": liquidityPoolDetails(lp), + "reserves_deposited": []base.AssetAmount{ + { + Asset: lp.Body.ConstantProduct.Params.AssetA.StringCanonical(), + Amount: amount.String(delta.ReserveA), + }, + { + Asset: lp.Body.ConstantProduct.Params.AssetB.StringCanonical(), + Amount: amount.String(delta.ReserveB), + }, + }, + "shares_received": amount.String(delta.TotalPoolShares), + } + e.addMuxed(e.operation.SourceAccount(), EffectLiquidityPoolDeposited, details) + return nil +} + +func (e *effectsWrapper) addLiquidityPoolWithdrawEffect() error { + op := e.operation.operation.Body.MustLiquidityPoolWithdrawOp() + lp, delta, err := e.operation.getLiquidityPoolAndProductDelta(&op.LiquidityPoolId) + if err != nil { + return err + } + details := map[string]interface{}{ + "liquidity_pool": liquidityPoolDetails(lp), + "reserves_received": []base.AssetAmount{ + { + Asset: lp.Body.ConstantProduct.Params.AssetA.StringCanonical(), + Amount: amount.String(-delta.ReserveA), + }, + { + Asset: lp.Body.ConstantProduct.Params.AssetB.StringCanonical(), + Amount: amount.String(-delta.ReserveB), + }, + }, + "shares_redeemed": amount.String(-delta.TotalPoolShares), + } + e.addMuxed(e.operation.SourceAccount(), EffectLiquidityPoolWithdrew, details) + return nil +} + +// addInvokeHostFunctionEffects iterates through the events and generates +// account_credited and account_debited effects when it sees events related to +// the Stellar Asset Contract corresponding to those effects. +func (e *effectsWrapper) addInvokeHostFunctionEffects(events []contractevents.Event) error { + if e.operation.network == "" { + return errors.New("invokeHostFunction effects cannot be determined unless network passphrase is set") + } + + source := e.operation.SourceAccount() + for _, event := range events { + evt, err := contractevents.NewStellarAssetContractEvent(&event, e.operation.network) + if err != nil { + continue // irrelevant or unsupported event + } + + details := make(map[string]interface{}, 4) + addAssetDetails(details, evt.GetAsset(), "") + + // + // Note: We ignore effects that involve contracts (until the day we have + // contract_debited/credited effects, may it never come :pray:) + // + + switch evt.GetType() { + // Transfer events generate an `account_debited` effect for the `from` + // (sender) and an `account_credited` effect for the `to` (recipient). + case contractevents.EventTypeTransfer: + details["contract_event_type"] = "transfer" + transferEvent := evt.(*contractevents.TransferEvent) + details["amount"] = amount.String128(transferEvent.Amount) + toDetails := map[string]interface{}{} + for key, val := range details { + toDetails[key] = val + } + + if strkey.IsValidEd25519PublicKey(transferEvent.From) { + e.add( + transferEvent.From, + null.String{}, + EffectAccountDebited, + details, + ) + } else { + details["contract"] = transferEvent.From + e.addMuxed(source, EffectContractDebited, details) + } + + if strkey.IsValidEd25519PublicKey(transferEvent.To) { + e.add( + transferEvent.To, + null.String{}, + EffectAccountCredited, + toDetails, + ) + } else { + toDetails["contract"] = transferEvent.To + e.addMuxed(source, EffectContractCredited, toDetails) + } + + // Mint events imply a non-native asset, and it results in a credit to + // the `to` recipient. + case contractevents.EventTypeMint: + details["contract_event_type"] = "mint" + mintEvent := evt.(*contractevents.MintEvent) + details["amount"] = amount.String128(mintEvent.Amount) + if strkey.IsValidEd25519PublicKey(mintEvent.To) { + e.add( + mintEvent.To, + null.String{}, + EffectAccountCredited, + details, + ) + } else { + details["contract"] = mintEvent.To + e.addMuxed(source, EffectContractCredited, details) + } + + // Clawback events result in a debit to the `from` address, but acts + // like a burn to the recipient, so these are functionally equivalent + case contractevents.EventTypeClawback: + details["contract_event_type"] = "clawback" + cbEvent := evt.(*contractevents.ClawbackEvent) + details["amount"] = amount.String128(cbEvent.Amount) + if strkey.IsValidEd25519PublicKey(cbEvent.From) { + e.add( + cbEvent.From, + null.String{}, + EffectAccountDebited, + details, + ) + } else { + details["contract"] = cbEvent.From + e.addMuxed(source, EffectContractDebited, details) + } + + case contractevents.EventTypeBurn: + details["contract_event_type"] = "burn" + burnEvent := evt.(*contractevents.BurnEvent) + details["amount"] = amount.String128(burnEvent.Amount) + if strkey.IsValidEd25519PublicKey(burnEvent.From) { + e.add( + burnEvent.From, + null.String{}, + EffectAccountDebited, + details, + ) + } else { + details["contract"] = burnEvent.From + e.addMuxed(source, EffectContractDebited, details) + } + } + } + + return nil +} + +func (e *effectsWrapper) addExtendFootprintTtlEffect() error { + op := e.operation.operation.Body.MustExtendFootprintTtlOp() + + // Figure out which entries were affected + changes, err := e.operation.transaction.GetOperationChanges(e.operation.index) + if err != nil { + return err + } + entries := make([]string, 0, len(changes)) + for _, change := range changes { + // They should all have a post + if change.Post == nil { + return fmt.Errorf("invalid bump footprint expiration operation: %v", op) + } + var key xdr.LedgerKey + switch change.Post.Data.Type { + case xdr.LedgerEntryTypeTtl: + v := change.Post.Data.MustTtl() + if err := key.SetTtl(v.KeyHash); err != nil { + return err + } + default: + // Ignore any non-contract entries, as they couldn't have been affected. + // + // Should we error here? No, because there might be other entries + // affected, for example, the user's balance. + continue + } + b64, err := xdr.MarshalBase64(key) + if err != nil { + return err + } + entries = append(entries, b64) + } + details := map[string]interface{}{ + "entries": entries, + "extend_to": op.ExtendTo, + } + e.addMuxed(e.operation.SourceAccount(), EffectExtendFootprintTtl, details) + return nil +} + +func (e *effectsWrapper) addRestoreFootprintExpirationEffect() error { + op := e.operation.operation.Body.MustRestoreFootprintOp() + + // Figure out which entries were affected + changes, err := e.operation.transaction.GetOperationChanges(e.operation.index) + if err != nil { + return err + } + entries := make([]string, 0, len(changes)) + for _, change := range changes { + // They should all have a post + if change.Post == nil { + return fmt.Errorf("invalid restore footprint operation: %v", op) + } + var key xdr.LedgerKey + switch change.Post.Data.Type { + case xdr.LedgerEntryTypeTtl: + v := change.Post.Data.MustTtl() + if err := key.SetTtl(v.KeyHash); err != nil { + return err + } + default: + // Ignore any non-contract entries, as they couldn't have been affected. + // + // Should we error here? No, because there might be other entries + // affected, for example, the user's balance. + continue + } + b64, err := xdr.MarshalBase64(key) + if err != nil { + return err + } + entries = append(entries, b64) + } + details := map[string]interface{}{ + "entries": entries, + } + e.addMuxed(e.operation.SourceAccount(), EffectRestoreFootprint, details) + return nil +} diff --git a/ingest/processors/effects_test.go b/ingest/processors/effects_test.go new file mode 100644 index 0000000000..dd53df7fba --- /dev/null +++ b/ingest/processors/effects_test.go @@ -0,0 +1,4143 @@ +package processors + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "math/big" + "strings" + "testing" + "time" + + "github.com/guregu/null" + "github.com/stellar/go/keypair" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/strkey" + "github.com/stellar/go/support/contractevents" + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/suite" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/toid" + "github.com/stellar/go/xdr" +) + +func TestEffectsCoversAllOperationTypes(t *testing.T) { + for typ, s := range xdr.OperationTypeToStringMap { + op := xdr.Operation{ + Body: xdr.OperationBody{ + Type: xdr.OperationType(typ), + }, + } + operation := transactionOperationWrapper{ + index: 0, + transaction: ingest.LedgerTransaction{ + UnsafeMeta: xdr.TransactionMeta{ + V: 2, + V2: &xdr.TransactionMetaV2{}, + }, + }, + operation: op, + ledgerSequence: 1, + network: "testnet", + ledgerClosed: genericCloseTime.UTC(), + } + // calling effects should either panic (because the operation field is set to nil) + // or not error + func() { + var err error + defer func() { + err2 := recover() + if err != nil { + assert.NotContains(t, err.Error(), "Unknown operation type") + } + assert.True(t, err2 != nil || err == nil, s) + }() + _, err = operation.effects() + }() + } + + // make sure the check works for an unknown operation type + op := xdr.Operation{ + Body: xdr.OperationBody{ + Type: xdr.OperationType(20000), + }, + } + operation := transactionOperationWrapper{ + index: 0, + transaction: ingest.LedgerTransaction{ + UnsafeMeta: xdr.TransactionMeta{ + V: 2, + V2: &xdr.TransactionMetaV2{}, + }, + }, + operation: op, + ledgerSequence: 1, + ledgerClosed: genericCloseTime.UTC(), + } + // calling effects should error due to the unknown operation + _, err := operation.effects() + assert.Contains(t, err.Error(), "unknown operation type") +} + +func TestOperationEffects(t *testing.T) { + + sourceAID := xdr.MustAddress("GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V") + sourceAccount := xdr.MuxedAccount{ + Type: xdr.CryptoKeyTypeKeyTypeMuxedEd25519, + Med25519: &xdr.MuxedAccountMed25519{ + Id: 0xcafebabe, + Ed25519: *sourceAID.Ed25519, + }, + } + destAID := xdr.MustAddress("GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ") + dest := xdr.MuxedAccount{ + Type: xdr.CryptoKeyTypeKeyTypeMuxedEd25519, + Med25519: &xdr.MuxedAccountMed25519{ + Id: 0xcafebabe, + Ed25519: *destAID.Ed25519, + }, + } + strictPaymentWithMuxedAccountsTx := xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: sourceAccount, + Fee: 100, + SeqNum: 3684420515004429, + Operations: []xdr.Operation{ + { + Body: xdr.OperationBody{ + Type: xdr.OperationTypePathPaymentStrictSend, + PathPaymentStrictSendOp: &xdr.PathPaymentStrictSendOp{ + SendAsset: xdr.Asset{ + Type: xdr.AssetTypeAssetTypeCreditAlphanum4, + AlphaNum4: &xdr.AlphaNum4{ + AssetCode: xdr.AssetCode4{66, 82, 76, 0}, + Issuer: xdr.MustAddress("GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF"), + }, + }, + SendAmount: 300000, + Destination: dest, + DestAsset: xdr.Asset{ + Type: 1, + AlphaNum4: &xdr.AlphaNum4{ + AssetCode: xdr.AssetCode4{65, 82, 83, 0}, + Issuer: xdr.MustAddress("GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF"), + }, + }, + DestMin: 10000000, + Path: []xdr.Asset{ + { + Type: xdr.AssetTypeAssetTypeCreditAlphanum4, + AlphaNum4: &xdr.AlphaNum4{ + AssetCode: xdr.AssetCode4{65, 82, 83, 0}, + Issuer: xdr.MustAddress("GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF"), + }, + }, + }, + }, + }, + }, + }, + }, + Signatures: []xdr.DecoratedSignature{ + { + Hint: xdr.SignatureHint{99, 66, 175, 143}, + Signature: xdr.Signature{244, 107, 139, 92, 189, 156, 207, 79, 84, 56, 2, 70, 75, 22, 237, 50, 100, 242, 159, 177, 27, 240, 66, 122, 182, 45, 189, 78, 5, 127, 26, 61, 179, 238, 229, 76, 32, 206, 122, 13, 154, 133, 148, 149, 29, 250, 48, 132, 44, 86, 163, 56, 32, 44, 75, 87, 226, 251, 76, 4, 59, 182, 132, 8}, + }, + }, + }, + } + strictPaymentWithMuxedAccountsTxBase64, err := xdr.MarshalBase64(strictPaymentWithMuxedAccountsTx) + assert.NoError(t, err) + + creator := xdr.MustAddress("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") + created := xdr.MustAddress("GCQZP3IU7XU6EJ63JZXKCQOYT2RNXN3HB5CNHENNUEUHSMA4VUJJJSEN") + sponsor := xdr.MustAddress("GAHK7EEG2WWHVKDNT4CEQFZGKF2LGDSW2IVM4S5DP42RBW3K6BTODB4A") + sponsor2 := xdr.MustAddress("GACMZD5VJXTRLKVET72CETCYKELPNCOTTBDC6DHFEUPLG5DHEK534JQX") + createAccountMeta := &xdr.TransactionMeta{ + V: 1, + V1: &xdr.TransactionMetaV1{ + TxChanges: xdr.LedgerEntryChanges{ + { + Type: 3, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 0x39, + Data: xdr.LedgerEntryData{ + Type: 0, + Account: &xdr.AccountEntry{ + AccountId: creator, + Balance: 800152377009533292, + SeqNum: 25, + InflationDest: &creator, + Thresholds: xdr.Thresholds{0x1, 0x0, 0x0, 0x0}, + }, + }, + }, + }, + { + Type: 1, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 0x39, + Data: xdr.LedgerEntryData{ + Type: 0, + Account: &xdr.AccountEntry{ + AccountId: creator, + Balance: 800152377009533292, + SeqNum: 26, + InflationDest: &creator, + }, + }, + Ext: xdr.LedgerEntryExt{}, + }, + }, + }, + Operations: []xdr.OperationMeta{ + { + Changes: xdr.LedgerEntryChanges{ + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 0x39, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: creator, + Balance: 800152367009533292, + SeqNum: 26, + InflationDest: &creator, + Thresholds: xdr.Thresholds{0x1, 0x0, 0x0, 0x0}, + }, + }, + Ext: xdr.LedgerEntryExt{ + V: 1, + V1: &xdr.LedgerEntryExtensionV1{ + SponsoringId: &sponsor2, + }, + }, + }, + }, + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryRemoved, + Removed: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.LedgerKeyAccount{ + AccountId: created, + }, + }, + }, + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 0x39, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: creator, + Balance: 800152367009533292, + SeqNum: 26, + InflationDest: &creator, + Thresholds: xdr.Thresholds{0x1, 0x0, 0x0, 0x0}, + }, + }, + Ext: xdr.LedgerEntryExt{ + V: 1, + V1: &xdr.LedgerEntryExtensionV1{ + SponsoringId: &sponsor, + }, + }, + }, + }, + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 0x39, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: creator, + Balance: 800152367009533292, + SeqNum: 26, + InflationDest: &creator, + Thresholds: xdr.Thresholds{0x1, 0x0, 0x0, 0x0}, + }, + }, + Ext: xdr.LedgerEntryExt{ + V: 1, + V1: &xdr.LedgerEntryExtensionV1{ + SponsoringId: &sponsor2, + }, + }, + }, + }, + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 0x39, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: creator, + Balance: 800152377009533292, + SeqNum: 26, + InflationDest: &creator, + Thresholds: xdr.Thresholds{0x1, 0x0, 0x0, 0x0}, + }, + }, + Ext: xdr.LedgerEntryExt{ + V: 1, + V1: &xdr.LedgerEntryExtensionV1{ + SponsoringId: &sponsor, + }, + }, + }, + }, + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 0x39, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: creator, + Balance: 800152367009533292, + SeqNum: 26, + InflationDest: &creator, + Thresholds: xdr.Thresholds{0x1, 0x0, 0x0, 0x0}, + }, + }, + Ext: xdr.LedgerEntryExt{ + V: 1, + V1: &xdr.LedgerEntryExtensionV1{ + SponsoringId: &sponsor, + }, + }, + }, + }, + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 0x39, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: created, + Balance: 10000000000, + SeqNum: 244813135872, + Thresholds: xdr.Thresholds{0x1, 0x0, 0x0, 0x0}, + }, + }, + Ext: xdr.LedgerEntryExt{ + V: 1, + V1: &xdr.LedgerEntryExtensionV1{ + SponsoringId: &sponsor, + }, + }, + }, + }, + }, + }, + }, + }, + } + + createAccountMetaB64, err := xdr.MarshalBase64(createAccountMeta) + assert.NoError(t, err) + assert.NoError(t, err) + + harCodedCloseMetaInput := makeLedgerCloseMeta() + LedgerClosed, err := GetCloseTime(harCodedCloseMetaInput) + assert.NoError(t, err) + + revokeSponsorshipMeta, revokeSponsorshipEffects := getRevokeSponsorshipMeta(t) + + testCases := []struct { + desc string + envelopeXDR string + resultXDR string + metaXDR string + feeChangesXDR string + hash string + index uint32 + sequence uint32 + expected []EffectOutput + }{ + { + desc: "createAccount", + envelopeXDR: "AAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3AAAAZAAAAAAAAAAaAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAoZftFP3p4ifbTm6hQdieotu3Zw9E05GtoSh5MBytEpQAAAACVAvkAAAAAAAAAAABVvwF9wAAAEDHU95E9wxgETD8TqxUrkgC0/7XHyNDts6Q5huRHfDRyRcoHdv7aMp/sPvC3RPkXjOMjgbKJUX7SgExUeYB5f8F", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=", + metaXDR: createAccountMetaB64, + feeChangesXDR: "AAAAAgAAAAMAAAA3AAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9wsatlj11nHQAAAAAAAAABkAAAAAAAAAAQAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9wAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAA5AAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9wsatlj11nFsAAAAAAAAABkAAAAAAAAAAQAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9wAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "0e5bd332291e3098e49886df2cdb9b5369a5f9e0a9973f0d9e1a9489c6581ba2", + index: 0, + sequence: 57, + expected: []EffectOutput{ + { + Address: "GCQZP3IU7XU6EJ63JZXKCQOYT2RNXN3HB5CNHENNUEUHSMA4VUJJJSEN", + OperationID: int64(244813139969), + Details: map[string]interface{}{ + "starting_balance": "1000.0000000", + }, + Type: int32(EffectAccountCreated), + TypeString: EffectTypeNames[EffectAccountCreated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 57, + }, + { + Address: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + OperationID: int64(244813139969), + Details: map[string]interface{}{ + "amount": "1000.0000000", + "asset_type": "native", + }, + Type: int32(EffectAccountDebited), + TypeString: EffectTypeNames[EffectAccountDebited], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 57, + }, + { + Address: "GCQZP3IU7XU6EJ63JZXKCQOYT2RNXN3HB5CNHENNUEUHSMA4VUJJJSEN", + OperationID: int64(244813139969), + Details: map[string]interface{}{ + "public_key": "GCQZP3IU7XU6EJ63JZXKCQOYT2RNXN3HB5CNHENNUEUHSMA4VUJJJSEN", + "weight": 1, + }, + Type: int32(EffectSignerCreated), + TypeString: EffectTypeNames[EffectSignerCreated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 57, + }, + { + Address: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + OperationID: int64(244813139969), + Details: map[string]interface{}{ + "former_sponsor": "GACMZD5VJXTRLKVET72CETCYKELPNCOTTBDC6DHFEUPLG5DHEK534JQX", + }, + Type: int32(EffectAccountSponsorshipRemoved), + TypeString: EffectTypeNames[EffectAccountSponsorshipRemoved], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 57, + }, + { + Address: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + OperationID: int64(244813139969), + Details: map[string]interface{}{ + "former_sponsor": "GAHK7EEG2WWHVKDNT4CEQFZGKF2LGDSW2IVM4S5DP42RBW3K6BTODB4A", + "new_sponsor": "GACMZD5VJXTRLKVET72CETCYKELPNCOTTBDC6DHFEUPLG5DHEK534JQX", + }, + Type: int32(EffectAccountSponsorshipUpdated), + TypeString: EffectTypeNames[EffectAccountSponsorshipUpdated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 57, + }, + { + Address: "GCQZP3IU7XU6EJ63JZXKCQOYT2RNXN3HB5CNHENNUEUHSMA4VUJJJSEN", + OperationID: int64(244813139969), + Details: map[string]interface{}{ + "sponsor": "GAHK7EEG2WWHVKDNT4CEQFZGKF2LGDSW2IVM4S5DP42RBW3K6BTODB4A", + }, + Type: int32(EffectAccountSponsorshipCreated), + TypeString: EffectTypeNames[EffectAccountSponsorshipCreated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 57, + }, + }, + }, + { + desc: "payment", + envelopeXDR: "AAAAABpcjiETZ0uhwxJJhgBPYKWSVJy2TZ2LI87fqV1cUf/UAAAAZAAAADcAAAABAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAGlyOIRNnS6HDEkmGAE9gpZJUnLZNnYsjzt+pXVxR/9QAAAAAAAAAAAX14QAAAAAAAAAAAVxR/9QAAABAK6pcXYMzAEmH08CZ1LWmvtNDKauhx+OImtP/Lk4hVTMJRVBOebVs5WEPj9iSrgGT0EswuDCZ2i5AEzwgGof9Ag==", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADAAAAOAAAAAAAAAAAGlyOIRNnS6HDEkmGAE9gpZJUnLZNnYsjzt+pXVxR/9QAAAACVAvjnAAAADcAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAOAAAAAAAAAAAGlyOIRNnS6HDEkmGAE9gpZJUnLZNnYsjzt+pXVxR/9QAAAACVAvjnAAAADcAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA==", + feeChangesXDR: "AAAAAgAAAAMAAAA3AAAAAAAAAAAaXI4hE2dLocMSSYYAT2ClklSctk2diyPO36ldXFH/1AAAAAJUC+QAAAAANwAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAA4AAAAAAAAAAAaXI4hE2dLocMSSYYAT2ClklSctk2diyPO36ldXFH/1AAAAAJUC+OcAAAANwAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "2a805712c6d10f9e74bb0ccf54ae92a2b4b1e586451fe8133a2433816f6b567c", + index: 0, + sequence: 56, + expected: []EffectOutput{ + { + Address: "GANFZDRBCNTUXIODCJEYMACPMCSZEVE4WZGZ3CZDZ3P2SXK4KH75IK6Y", + Details: map[string]interface{}{ + "amount": "10.0000000", + "asset_type": "native", + }, + Type: int32(EffectAccountCredited), + TypeString: EffectTypeNames[EffectAccountCredited], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GANFZDRBCNTUXIODCJEYMACPMCSZEVE4WZGZ3CZDZ3P2SXK4KH75IK6Y", + Details: map[string]interface{}{ + "amount": "10.0000000", + "asset_type": "native", + }, + Type: int32(EffectAccountDebited), + TypeString: EffectTypeNames[EffectAccountDebited], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + }, + }, + { + desc: "pathPaymentStrictSend", + envelopeXDR: "AAAAAPbGHHrGbL7EFLG87cWA6eecM/LaVyzrO+pakFpjQq+PAAAAZAANFvYAAAANAAAAAAAAAAAAAAABAAAAAAAAAA0AAAABQlJMAAAAAACuj0P7T8viUkHM324bjqGqM4AvwXVOKd9lSX7px+1ZWgAAAAAABJPgAAAAAMjq0GsWJu54fjD9Y/wlJJ4a9iAQvF82hRIjT716u5sDAAAAAUFSUwAAAAAAro9D+0/L4lJBzN9uG46hqjOAL8F1TinfZUl+6cftWVoAAAAAAJiWgAAAAAEAAAABQVJTAAAAAACuj0P7T8viUkHM324bjqGqM4AvwXVOKd9lSX7px+1ZWgAAAAAAAAABY0KvjwAAAED0a4tcvZzPT1Q4AkZLFu0yZPKfsRvwQnq2Lb1OBX8aPbPu5UwgznoNmoWUlR36MIQsVqM4ICxLV+L7TAQ7toQI", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAANAAAAAAAAAAEAAAAAyOrQaxYm7nh+MP1j/CUknhr2IBC8XzaFEiNPvXq7mwMAAAAAAJmwQAAAAAFBUlMAAAAAAK6PQ/tPy+JSQczfbhuOoaozgC/BdU4p32VJfunH7VlaAAAAAACYloAAAAABQlJMAAAAAACuj0P7T8viUkHM324bjqGqM4AvwXVOKd9lSX7px+1ZWgAAAAAABJPgAAAAAMjq0GsWJu54fjD9Y/wlJJ4a9iAQvF82hRIjT716u5sDAAAAAUFSUwAAAAAAro9D+0/L4lJBzN9uG46hqjOAL8F1TinfZUl+6cftWVoAAAAAAJiWgAAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADAA0aVQAAAAAAAAAA9sYcesZsvsQUsbztxYDp55wz8tpXLOs76lqQWmNCr48AAAAXSHbi7AANFvYAAAAMAAAAAwAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAA0aVQAAAAAAAAAA9sYcesZsvsQUsbztxYDp55wz8tpXLOs76lqQWmNCr48AAAAXSHbi7AANFvYAAAANAAAAAwAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAACAAAAAMADRo0AAAAAQAAAAD2xhx6xmy+xBSxvO3FgOnnnDPy2lcs6zvqWpBaY0KvjwAAAAFCUkwAAAAAAK6PQ/tPy+JSQczfbhuOoaozgC/BdU4p32VJfunH7VlaAAAAAB22gaB//////////wAAAAEAAAABAAAAAAC3GwAAAAAAAAAAAAAAAAAAAAAAAAAAAQANGlUAAAABAAAAAPbGHHrGbL7EFLG87cWA6eecM/LaVyzrO+pakFpjQq+PAAAAAUJSTAAAAAAAro9D+0/L4lJBzN9uG46hqjOAL8F1TinfZUl+6cftWVoAAAAAHbHtwH//////////AAAAAQAAAAEAAAAAALcbAAAAAAAAAAAAAAAAAAAAAAAAAAADAA0aNAAAAAIAAAAAyOrQaxYm7nh+MP1j/CUknhr2IBC8XzaFEiNPvXq7mwMAAAAAAJmwQAAAAAFBUlMAAAAAAK6PQ/tPy+JSQczfbhuOoaozgC/BdU4p32VJfunH7VlaAAAAAUJSTAAAAAAAro9D+0/L4lJBzN9uG46hqjOAL8F1TinfZUl+6cftWVoAAAAAFNyTgAAAAAMAAABkAAAAAAAAAAAAAAAAAAAAAQANGlUAAAACAAAAAMjq0GsWJu54fjD9Y/wlJJ4a9iAQvF82hRIjT716u5sDAAAAAACZsEAAAAABQVJTAAAAAACuj0P7T8viUkHM324bjqGqM4AvwXVOKd9lSX7px+1ZWgAAAAFCUkwAAAAAAK6PQ/tPy+JSQczfbhuOoaozgC/BdU4p32VJfunH7VlaAAAAABRD/QAAAAADAAAAZAAAAAAAAAAAAAAAAAAAAAMADRo0AAAAAQAAAADI6tBrFibueH4w/WP8JSSeGvYgELxfNoUSI0+9erubAwAAAAFCUkwAAAAAAK6PQ/tPy+JSQczfbhuOoaozgC/BdU4p32VJfunH7VlaAAAAAB3kSGB//////////wAAAAEAAAABAAAAAACgN6AAAAAAAAAAAAAAAAAAAAAAAAAAAQANGlUAAAABAAAAAMjq0GsWJu54fjD9Y/wlJJ4a9iAQvF82hRIjT716u5sDAAAAAUJSTAAAAAAAro9D+0/L4lJBzN9uG46hqjOAL8F1TinfZUl+6cftWVoAAAAAHejcQH//////////AAAAAQAAAAEAAAAAAJujwAAAAAAAAAAAAAAAAAAAAAAAAAADAA0aNAAAAAEAAAAAyOrQaxYm7nh+MP1j/CUknhr2IBC8XzaFEiNPvXq7mwMAAAABQVJTAAAAAACuj0P7T8viUkHM324bjqGqM4AvwXVOKd9lSX7px+1ZWgAAAAB2BGcAf/////////8AAAABAAAAAQAAAAAAAAAAAAAAABTck4AAAAAAAAAAAAAAAAEADRpVAAAAAQAAAADI6tBrFibueH4w/WP8JSSeGvYgELxfNoUSI0+9erubAwAAAAFBUlMAAAAAAK6PQ/tPy+JSQczfbhuOoaozgC/BdU4p32VJfunH7VlaAAAAAHYEZwB//////////wAAAAEAAAABAAAAAAAAAAAAAAAAFEP9AAAAAAAAAAAA", + feeChangesXDR: "AAAAAgAAAAMADRpIAAAAAAAAAAD2xhx6xmy+xBSxvO3FgOnnnDPy2lcs6zvqWpBaY0KvjwAAABdIduNQAA0W9gAAAAwAAAADAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEADRpVAAAAAAAAAAD2xhx6xmy+xBSxvO3FgOnnnDPy2lcs6zvqWpBaY0KvjwAAABdIduLsAA0W9gAAAAwAAAADAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "96415ac1d2f79621b26b1568f963fd8dd6c50c20a22c7428cefbfe9dee867588", + index: 0, + sequence: 20, + expected: []EffectOutput{ + + { + Address: "GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ", + Details: map[string]interface{}{ + "amount": "1.0000000", + "asset_code": "ARS", + "asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "asset_type": "credit_alphanum4", + }, + Type: int32(EffectAccountCredited), + TypeString: EffectTypeNames[EffectAccountCredited], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + { + Address: "GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V", + Details: map[string]interface{}{ + "amount": "0.0300000", + "asset_code": "BRL", + "asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "asset_type": "credit_alphanum4", + }, + Type: int32(EffectAccountDebited), + TypeString: EffectTypeNames[EffectAccountDebited], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + { + Address: "GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V", + Details: map[string]interface{}{ + "bought_amount": "1.0000000", + "bought_asset_code": "ARS", + "bought_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10072128), + "seller": "GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ", + "sold_amount": "0.0300000", + "sold_asset_code": "BRL", + "sold_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectTrade), + TypeString: EffectTypeNames[EffectTrade], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + { + Address: "GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ", + Details: map[string]interface{}{ + "bought_amount": "0.0300000", + "bought_asset_code": "BRL", + "bought_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10072128), + "seller": "GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V", + "sold_amount": "1.0000000", + "sold_asset_code": "ARS", + "sold_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectTrade), + TypeString: EffectTypeNames[EffectTrade], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + { + Address: "GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V", + Details: map[string]interface{}{ + "bought_amount": "1.0000000", + "bought_asset_code": "ARS", + "bought_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10072128), + "seller": "GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ", + "sold_amount": "0.0300000", + "sold_asset_code": "BRL", + "sold_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectOfferUpdated), + TypeString: EffectTypeNames[EffectOfferUpdated], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + { + Address: "GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ", + Details: map[string]interface{}{ + "bought_amount": "0.0300000", + "bought_asset_code": "BRL", + "bought_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10072128), + "seller": "GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V", + "sold_amount": "1.0000000", + "sold_asset_code": "ARS", + "sold_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectOfferUpdated), + TypeString: EffectTypeNames[EffectOfferUpdated], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + { + Address: "GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V", + Details: map[string]interface{}{ + "bought_amount": "1.0000000", + "bought_asset_code": "ARS", + "bought_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10072128), + "seller": "GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ", + "sold_amount": "0.0300000", + "sold_asset_code": "BRL", + "sold_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectOfferRemoved), + TypeString: EffectTypeNames[EffectOfferRemoved], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + { + Address: "GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ", + Details: map[string]interface{}{ + "bought_amount": "0.0300000", + "bought_asset_code": "BRL", + "bought_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10072128), + "seller": "GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V", + "sold_amount": "1.0000000", + "sold_asset_code": "ARS", + "sold_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectOfferRemoved), + TypeString: EffectTypeNames[EffectOfferRemoved], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + }, + }, + { + desc: "pathPaymentStrictSend with muxed accounts", + envelopeXDR: strictPaymentWithMuxedAccountsTxBase64, + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAANAAAAAAAAAAEAAAAAyOrQaxYm7nh+MP1j/CUknhr2IBC8XzaFEiNPvXq7mwMAAAAAAJmwQAAAAAFBUlMAAAAAAK6PQ/tPy+JSQczfbhuOoaozgC/BdU4p32VJfunH7VlaAAAAAACYloAAAAABQlJMAAAAAACuj0P7T8viUkHM324bjqGqM4AvwXVOKd9lSX7px+1ZWgAAAAAABJPgAAAAAMjq0GsWJu54fjD9Y/wlJJ4a9iAQvF82hRIjT716u5sDAAAAAUFSUwAAAAAAro9D+0/L4lJBzN9uG46hqjOAL8F1TinfZUl+6cftWVoAAAAAAJiWgAAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADAA0aVQAAAAAAAAAA9sYcesZsvsQUsbztxYDp55wz8tpXLOs76lqQWmNCr48AAAAXSHbi7AANFvYAAAAMAAAAAwAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAA0aVQAAAAAAAAAA9sYcesZsvsQUsbztxYDp55wz8tpXLOs76lqQWmNCr48AAAAXSHbi7AANFvYAAAANAAAAAwAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAACAAAAAMADRo0AAAAAQAAAAD2xhx6xmy+xBSxvO3FgOnnnDPy2lcs6zvqWpBaY0KvjwAAAAFCUkwAAAAAAK6PQ/tPy+JSQczfbhuOoaozgC/BdU4p32VJfunH7VlaAAAAAB22gaB//////////wAAAAEAAAABAAAAAAC3GwAAAAAAAAAAAAAAAAAAAAAAAAAAAQANGlUAAAABAAAAAPbGHHrGbL7EFLG87cWA6eecM/LaVyzrO+pakFpjQq+PAAAAAUJSTAAAAAAAro9D+0/L4lJBzN9uG46hqjOAL8F1TinfZUl+6cftWVoAAAAAHbHtwH//////////AAAAAQAAAAEAAAAAALcbAAAAAAAAAAAAAAAAAAAAAAAAAAADAA0aNAAAAAIAAAAAyOrQaxYm7nh+MP1j/CUknhr2IBC8XzaFEiNPvXq7mwMAAAAAAJmwQAAAAAFBUlMAAAAAAK6PQ/tPy+JSQczfbhuOoaozgC/BdU4p32VJfunH7VlaAAAAAUJSTAAAAAAAro9D+0/L4lJBzN9uG46hqjOAL8F1TinfZUl+6cftWVoAAAAAFNyTgAAAAAMAAABkAAAAAAAAAAAAAAAAAAAAAQANGlUAAAACAAAAAMjq0GsWJu54fjD9Y/wlJJ4a9iAQvF82hRIjT716u5sDAAAAAACZsEAAAAABQVJTAAAAAACuj0P7T8viUkHM324bjqGqM4AvwXVOKd9lSX7px+1ZWgAAAAFCUkwAAAAAAK6PQ/tPy+JSQczfbhuOoaozgC/BdU4p32VJfunH7VlaAAAAABRD/QAAAAADAAAAZAAAAAAAAAAAAAAAAAAAAAMADRo0AAAAAQAAAADI6tBrFibueH4w/WP8JSSeGvYgELxfNoUSI0+9erubAwAAAAFCUkwAAAAAAK6PQ/tPy+JSQczfbhuOoaozgC/BdU4p32VJfunH7VlaAAAAAB3kSGB//////////wAAAAEAAAABAAAAAACgN6AAAAAAAAAAAAAAAAAAAAAAAAAAAQANGlUAAAABAAAAAMjq0GsWJu54fjD9Y/wlJJ4a9iAQvF82hRIjT716u5sDAAAAAUJSTAAAAAAAro9D+0/L4lJBzN9uG46hqjOAL8F1TinfZUl+6cftWVoAAAAAHejcQH//////////AAAAAQAAAAEAAAAAAJujwAAAAAAAAAAAAAAAAAAAAAAAAAADAA0aNAAAAAEAAAAAyOrQaxYm7nh+MP1j/CUknhr2IBC8XzaFEiNPvXq7mwMAAAABQVJTAAAAAACuj0P7T8viUkHM324bjqGqM4AvwXVOKd9lSX7px+1ZWgAAAAB2BGcAf/////////8AAAABAAAAAQAAAAAAAAAAAAAAABTck4AAAAAAAAAAAAAAAAEADRpVAAAAAQAAAADI6tBrFibueH4w/WP8JSSeGvYgELxfNoUSI0+9erubAwAAAAFBUlMAAAAAAK6PQ/tPy+JSQczfbhuOoaozgC/BdU4p32VJfunH7VlaAAAAAHYEZwB//////////wAAAAEAAAABAAAAAAAAAAAAAAAAFEP9AAAAAAAAAAAA", + feeChangesXDR: "AAAAAgAAAAMADRpIAAAAAAAAAAD2xhx6xmy+xBSxvO3FgOnnnDPy2lcs6zvqWpBaY0KvjwAAABdIduNQAA0W9gAAAAwAAAADAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEADRpVAAAAAAAAAAD2xhx6xmy+xBSxvO3FgOnnnDPy2lcs6zvqWpBaY0KvjwAAABdIduLsAA0W9gAAAAwAAAADAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "96415ac1d2f79621b26b1568f963fd8dd6c50c20a22c7428cefbfe9dee867588", + index: 0, + sequence: 20, + expected: []EffectOutput{ + { + Address: "GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ", + AddressMuxed: null.StringFrom("MDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQGAAAAAAMV7V2X24II"), + Details: map[string]interface{}{ + "amount": "1.0000000", + "asset_code": "ARS", + "asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "asset_type": "credit_alphanum4", + }, + Type: int32(EffectAccountCredited), + TypeString: EffectTypeNames[EffectAccountCredited], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + { + Address: "GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V", + AddressMuxed: null.StringFrom("MD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY6AAAAAAMV7V2XZY4C"), + Details: map[string]interface{}{ + "amount": "0.0300000", + "asset_code": "BRL", + "asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "asset_type": "credit_alphanum4", + }, + Type: int32(EffectAccountDebited), + TypeString: EffectTypeNames[EffectAccountDebited], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + { + Address: "GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V", + AddressMuxed: null.StringFrom("MD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY6AAAAAAMV7V2XZY4C"), + Details: map[string]interface{}{ + "bought_amount": "1.0000000", + "bought_asset_code": "ARS", + "bought_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10072128), + "seller": "GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ", + "sold_amount": "0.0300000", + "sold_asset_code": "BRL", + "sold_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectTrade), + TypeString: EffectTypeNames[EffectTrade], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + { + Address: "GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ", + Details: map[string]interface{}{ + "bought_amount": "0.0300000", + "bought_asset_code": "BRL", + "bought_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10072128), + "seller": "GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V", + "seller_muxed": "MD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY6AAAAAAMV7V2XZY4C", + "seller_muxed_id": uint64(0xcafebabe), + "sold_amount": "1.0000000", + "sold_asset_code": "ARS", + "sold_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectTrade), + TypeString: EffectTypeNames[EffectTrade], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + { + Address: "GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V", + AddressMuxed: null.StringFrom("MD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY6AAAAAAMV7V2XZY4C"), + Details: map[string]interface{}{ + "bought_amount": "1.0000000", + "bought_asset_code": "ARS", + "bought_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10072128), + "seller": "GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ", + "sold_amount": "0.0300000", + "sold_asset_code": "BRL", + "sold_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectOfferUpdated), + TypeString: EffectTypeNames[EffectOfferUpdated], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + { + Address: "GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ", + Details: map[string]interface{}{ + "bought_amount": "0.0300000", + "bought_asset_code": "BRL", + "bought_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10072128), + "seller": "GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V", + "seller_muxed": "MD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY6AAAAAAMV7V2XZY4C", + "seller_muxed_id": uint64(0xcafebabe), + "sold_amount": "1.0000000", + "sold_asset_code": "ARS", + "sold_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectOfferUpdated), + TypeString: EffectTypeNames[EffectOfferUpdated], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + { + Address: "GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V", + AddressMuxed: null.StringFrom("MD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY6AAAAAAMV7V2XZY4C"), + Details: map[string]interface{}{ + "bought_amount": "1.0000000", + "bought_asset_code": "ARS", + "bought_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10072128), + "seller": "GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ", + "sold_amount": "0.0300000", + "sold_asset_code": "BRL", + "sold_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectOfferRemoved), + TypeString: EffectTypeNames[EffectOfferRemoved], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + { + Address: "GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ", + Details: map[string]interface{}{ + "bought_amount": "0.0300000", + "bought_asset_code": "BRL", + "bought_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10072128), + "seller": "GD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY737V", + "seller_muxed": "MD3MMHD2YZWL5RAUWG6O3RMA5HTZYM7S3JLSZ2Z35JNJAWTDIKXY6AAAAAAMV7V2XZY4C", + "seller_muxed_id": uint64(0xcafebabe), + "sold_amount": "1.0000000", + "sold_asset_code": "ARS", + "sold_asset_issuer": "GCXI6Q73J7F6EUSBZTPW4G4OUGVDHABPYF2U4KO7MVEX52OH5VMVUCRF", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectOfferRemoved), + TypeString: EffectTypeNames[EffectOfferRemoved], + OperationID: int64(85899350017), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 20, + }, + }, + }, + { + desc: "manageSellOffer - without claims", + envelopeXDR: "AAAAAC7C83M2T23Bu4kdQGqdfboZgjcxsJ2lBT23ifoRVFexAAAAZAAAABAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAMAAAAAAAAAAVVTRAAAAAAALsLzczZPbcG7iR1Aap19uhmCNzGwnaUFPbeJ+hFUV7EAAAAA7msoAAAAAAEAAAACAAAAAAAAAAAAAAAAAAAAARFUV7EAAABALuai5QxceFbtAiC5nkntNVnvSPeWR+C+FgplPAdRgRS+PPESpUiSCyuiwuhmvuDw7kwxn+A6E0M4ca1s2qzMAg==", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAC7C83M2T23Bu4kdQGqdfboZgjcxsJ2lBT23ifoRVFexAAAAAAAAAAEAAAAAAAAAAVVTRAAAAAAALsLzczZPbcG7iR1Aap19uhmCNzGwnaUFPbeJ+hFUV7EAAAAA7msoAAAAAAEAAAACAAAAAAAAAAAAAAAA", + metaXDR: "AAAAAQAAAAIAAAADAAAAEgAAAAAAAAAALsLzczZPbcG7iR1Aap19uhmCNzGwnaUFPbeJ+hFUV7EAAAACVAvi1AAAABAAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAEgAAAAAAAAAALsLzczZPbcG7iR1Aap19uhmCNzGwnaUFPbeJ+hFUV7EAAAACVAvi1AAAABAAAAACAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAMAAAASAAAAAAAAAAAuwvNzNk9twbuJHUBqnX26GYI3MbCdpQU9t4n6EVRXsQAAAAJUC+LUAAAAEAAAAAIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAASAAAAAAAAAAAuwvNzNk9twbuJHUBqnX26GYI3MbCdpQU9t4n6EVRXsQAAAAJUC+LUAAAAEAAAAAIAAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAA7msoAAAAAAAAAAAAAAAAAAAAABIAAAACAAAAAC7C83M2T23Bu4kdQGqdfboZgjcxsJ2lBT23ifoRVFexAAAAAAAAAAEAAAAAAAAAAVVTRAAAAAAALsLzczZPbcG7iR1Aap19uhmCNzGwnaUFPbeJ+hFUV7EAAAAA7msoAAAAAAEAAAACAAAAAAAAAAAAAAAA", + feeChangesXDR: "AAAAAgAAAAMAAAASAAAAAAAAAAAuwvNzNk9twbuJHUBqnX26GYI3MbCdpQU9t4n6EVRXsQAAAAJUC+OcAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAASAAAAAAAAAAAuwvNzNk9twbuJHUBqnX26GYI3MbCdpQU9t4n6EVRXsQAAAAJUC+M4AAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "ca756d1519ceda79f8722042b12cea7ba004c3bd961adb62b59f88a867f86eb3", + index: 0, + sequence: 56, + expected: []EffectOutput{}, + }, + { + desc: "manageSellOffer - with claims", + envelopeXDR: "AAAAAPrjQnnOn4RqMmOSDwYfEMVtJuC4VR9fKvPfEtM7DS7VAAAAZAAMDl8AAAADAAAAAAAAAAAAAAABAAAAAAAAAAMAAAAAAAAAAVNUUgAAAAAASYK2XlJiUiNav1waFVDq1fzoualYC4UNFqThKBroJe0AAAACVAvkAAAAAGMAAADIAAAAAAAAAAAAAAAAAAAAATsNLtUAAABABmA0aLobgdSrjIrus94Y8PWeD6dDfl7Sya12t2uZasJFI7mZ+yowE1enUMzC/cAhDTypK8QuH2EVXPQC3xpYDA==", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAADAAAAAAAAAAEAAAAADkfaGg9y56NND7n4CRcr4R4fvivwAcMd4ZrCm4jAe5AAAAAAAI0f+AAAAAFTVFIAAAAAAEmCtl5SYlIjWr9cGhVQ6tX86LmpWAuFDRak4Sga6CXtAAAAAS0Il1oAAAAAAAAAAlQL4/8AAAACAAAAAA==", + metaXDR: "AAAAAQAAAAIAAAADAAxMfwAAAAAAAAAA+uNCec6fhGoyY5IPBh8QxW0m4LhVH18q898S0zsNLtUAAAAU9GsC1QAMDl8AAAACAAAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAxMfwAAAAAAAAAA+uNCec6fhGoyY5IPBh8QxW0m4LhVH18q898S0zsNLtUAAAAU9GsC1QAMDl8AAAADAAAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAACgAAAAMADEx+AAAAAgAAAAAOR9oaD3Lno00PufgJFyvhHh++K/ABwx3hmsKbiMB7kAAAAAAAjR/4AAAAAVNUUgAAAAAASYK2XlJiUiNav1waFVDq1fzoualYC4UNFqThKBroJe0AAAAAAAAAA2L6BdYAAABjAAAAMgAAAAAAAAAAAAAAAAAAAAEADEx/AAAAAgAAAAAOR9oaD3Lno00PufgJFyvhHh++K/ABwx3hmsKbiMB7kAAAAAAAjR/4AAAAAVNUUgAAAAAASYK2XlJiUiNav1waFVDq1fzoualYC4UNFqThKBroJe0AAAAAAAAAAjXxbnwAAABjAAAAMgAAAAAAAAAAAAAAAAAAAAMADEx+AAAAAAAAAAAOR9oaD3Lno00PufgJFyvhHh++K/ABwx3hmsKbiMB7kAAAABnMMdMvAAwOZQAAAAIAAAACAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAABrSdIAkAAAAAAAAAAAAAAAAAAAAAAAAAAQAMTH8AAAAAAAAAAA5H2hoPcuejTQ+5+AkXK+EeH74r8AHDHeGawpuIwHuQAAAAHCA9ty4ADA5lAAAAAgAAAAIAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAEYJE8CgAAAAAAAAAAAAAAAAAAAAAAAAADAAxMfgAAAAEAAAAADkfaGg9y56NND7n4CRcr4R4fvivwAcMd4ZrCm4jAe5AAAAABU1RSAAAAAABJgrZeUmJSI1q/XBoVUOrV/Oi5qVgLhQ0WpOEoGugl7QAAABYDWSXWf/////////8AAAABAAAAAQAAAAAAAAAAAAAAA2L6BdYAAAAAAAAAAAAAAAEADEx/AAAAAQAAAAAOR9oaD3Lno00PufgJFyvhHh++K/ABwx3hmsKbiMB7kAAAAAFTVFIAAAAAAEmCtl5SYlIjWr9cGhVQ6tX86LmpWAuFDRak4Sga6CXtAAAAFNZQjnx//////////wAAAAEAAAABAAAAAAAAAAAAAAACNfFufAAAAAAAAAAAAAAAAwAMDnEAAAABAAAAAPrjQnnOn4RqMmOSDwYfEMVtJuC4VR9fKvPfEtM7DS7VAAAAAVNUUgAAAAAASYK2XlJiUiNav1waFVDq1fzoualYC4UNFqThKBroJe0AAAAYdX9/Wn//////////AAAAAQAAAAAAAAAAAAAAAQAMTH8AAAABAAAAAPrjQnnOn4RqMmOSDwYfEMVtJuC4VR9fKvPfEtM7DS7VAAAAAVNUUgAAAAAASYK2XlJiUiNav1waFVDq1fzoualYC4UNFqThKBroJe0AAAAZoogWtH//////////AAAAAQAAAAAAAAAAAAAAAwAMTH8AAAAAAAAAAPrjQnnOn4RqMmOSDwYfEMVtJuC4VR9fKvPfEtM7DS7VAAAAFPRrAtUADA5fAAAAAwAAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAMTH8AAAAAAAAAAPrjQnnOn4RqMmOSDwYfEMVtJuC4VR9fKvPfEtM7DS7VAAAAEqBfHtYADA5fAAAAAwAAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA", + feeChangesXDR: "AAAAAgAAAAMADA5xAAAAAAAAAAD640J5zp+EajJjkg8GHxDFbSbguFUfXyrz3xLTOw0u1QAAABT0awM5AAwOXwAAAAIAAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEADEx/AAAAAAAAAAD640J5zp+EajJjkg8GHxDFbSbguFUfXyrz3xLTOw0u1QAAABT0awLVAAwOXwAAAAIAAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "ef62da32b6b3eb3c4534dac2be1088387fb93b0093b47e113073c1431fac9db7", + index: 0, + sequence: 56, + expected: []EffectOutput{ + { + Address: "GD5OGQTZZ2PYI2RSMOJA6BQ7CDCW2JXAXBKR6XZK6PPRFUZ3BUXNLFKP", + Details: map[string]interface{}{ + "bought_amount": "505.0505050", + "bought_asset_code": "STR", + "bought_asset_issuer": "GBEYFNS6KJRFEI22X5OBUFKQ5LK7Z2FZVFMAXBINC2SOCKA25AS62PUN", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(9248760), + "seller": "GAHEPWQ2B5ZOPI2NB647QCIXFPQR4H56FPYADQY54GNMFG4IYB5ZAJ5H", + "sold_amount": "999.9999999", + "sold_asset_type": "native", + }, + Type: int32(EffectTrade), + TypeString: EffectTypeNames[EffectTrade], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GAHEPWQ2B5ZOPI2NB647QCIXFPQR4H56FPYADQY54GNMFG4IYB5ZAJ5H", + Details: map[string]interface{}{ + "bought_amount": "999.9999999", + "bought_asset_type": "native", + "offer_id": xdr.Int64(9248760), + "seller": "GD5OGQTZZ2PYI2RSMOJA6BQ7CDCW2JXAXBKR6XZK6PPRFUZ3BUXNLFKP", + "sold_amount": "505.0505050", + "sold_asset_code": "STR", + "sold_asset_issuer": "GBEYFNS6KJRFEI22X5OBUFKQ5LK7Z2FZVFMAXBINC2SOCKA25AS62PUN", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectTrade), + TypeString: EffectTypeNames[EffectTrade], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GD5OGQTZZ2PYI2RSMOJA6BQ7CDCW2JXAXBKR6XZK6PPRFUZ3BUXNLFKP", + Details: map[string]interface{}{ + "bought_amount": "505.0505050", + "bought_asset_code": "STR", + "bought_asset_issuer": "GBEYFNS6KJRFEI22X5OBUFKQ5LK7Z2FZVFMAXBINC2SOCKA25AS62PUN", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(9248760), + "seller": "GAHEPWQ2B5ZOPI2NB647QCIXFPQR4H56FPYADQY54GNMFG4IYB5ZAJ5H", + "sold_amount": "999.9999999", + "sold_asset_type": "native", + }, + Type: int32(EffectOfferUpdated), + TypeString: EffectTypeNames[EffectOfferUpdated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GAHEPWQ2B5ZOPI2NB647QCIXFPQR4H56FPYADQY54GNMFG4IYB5ZAJ5H", + Details: map[string]interface{}{ + "bought_amount": "999.9999999", + "bought_asset_type": "native", + "offer_id": xdr.Int64(9248760), + "seller": "GD5OGQTZZ2PYI2RSMOJA6BQ7CDCW2JXAXBKR6XZK6PPRFUZ3BUXNLFKP", + "sold_amount": "505.0505050", + "sold_asset_code": "STR", + "sold_asset_issuer": "GBEYFNS6KJRFEI22X5OBUFKQ5LK7Z2FZVFMAXBINC2SOCKA25AS62PUN", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectOfferUpdated), + TypeString: EffectTypeNames[EffectOfferUpdated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GD5OGQTZZ2PYI2RSMOJA6BQ7CDCW2JXAXBKR6XZK6PPRFUZ3BUXNLFKP", + Details: map[string]interface{}{ + "bought_amount": "505.0505050", + "bought_asset_code": "STR", + "bought_asset_issuer": "GBEYFNS6KJRFEI22X5OBUFKQ5LK7Z2FZVFMAXBINC2SOCKA25AS62PUN", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(9248760), + "seller": "GAHEPWQ2B5ZOPI2NB647QCIXFPQR4H56FPYADQY54GNMFG4IYB5ZAJ5H", + "sold_amount": "999.9999999", + "sold_asset_type": "native", + }, + Type: int32(EffectOfferRemoved), + TypeString: EffectTypeNames[EffectOfferRemoved], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GAHEPWQ2B5ZOPI2NB647QCIXFPQR4H56FPYADQY54GNMFG4IYB5ZAJ5H", + Details: map[string]interface{}{ + "bought_amount": "999.9999999", + "bought_asset_type": "native", + "offer_id": xdr.Int64(9248760), + "seller": "GD5OGQTZZ2PYI2RSMOJA6BQ7CDCW2JXAXBKR6XZK6PPRFUZ3BUXNLFKP", + "sold_amount": "505.0505050", + "sold_asset_code": "STR", + "sold_asset_issuer": "GBEYFNS6KJRFEI22X5OBUFKQ5LK7Z2FZVFMAXBINC2SOCKA25AS62PUN", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectOfferRemoved), + TypeString: EffectTypeNames[EffectOfferRemoved], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GD5OGQTZZ2PYI2RSMOJA6BQ7CDCW2JXAXBKR6XZK6PPRFUZ3BUXNLFKP", + Details: map[string]interface{}{ + "bought_amount": "505.0505050", + "bought_asset_code": "STR", + "bought_asset_issuer": "GBEYFNS6KJRFEI22X5OBUFKQ5LK7Z2FZVFMAXBINC2SOCKA25AS62PUN", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(9248760), + "seller": "GAHEPWQ2B5ZOPI2NB647QCIXFPQR4H56FPYADQY54GNMFG4IYB5ZAJ5H", + "sold_amount": "999.9999999", + "sold_asset_type": "native", + }, + Type: int32(EffectOfferCreated), + TypeString: EffectTypeNames[EffectOfferCreated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GAHEPWQ2B5ZOPI2NB647QCIXFPQR4H56FPYADQY54GNMFG4IYB5ZAJ5H", + Details: map[string]interface{}{ + "bought_amount": "999.9999999", + "bought_asset_type": "native", + "offer_id": xdr.Int64(9248760), + "seller": "GD5OGQTZZ2PYI2RSMOJA6BQ7CDCW2JXAXBKR6XZK6PPRFUZ3BUXNLFKP", + "sold_amount": "505.0505050", + "sold_asset_code": "STR", + "sold_asset_issuer": "GBEYFNS6KJRFEI22X5OBUFKQ5LK7Z2FZVFMAXBINC2SOCKA25AS62PUN", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectOfferCreated), + TypeString: EffectTypeNames[EffectOfferCreated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + }, + }, + { + desc: "manageBuyOffer - with claims", + envelopeXDR: "AAAAAEotqBM9oOzudkkctgQlY/PHS0rFcxVasWQVnSytiuBEAAAAZAANIfEAAAADAAAAAAAAAAAAAAABAAAAAAAAAAwAAAAAAAAAAlRYVGFscGhhNAAAAAAAAABKLagTPaDs7nZJHLYEJWPzx0tKxXMVWrFkFZ0srYrgRAAAAAB3NZQAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABrYrgRAAAAEAh57TBifjJuUPj1TI7zIvaAZmyRjWLY4ktc0F16Knmy4Fw07L7cC5vCwjn4ZXyrgr9bpEGhv4oN6znbPpNLQUH", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAMAAAAAAAAAAEAAAAAgbI9jY68fYXd6+DwMcZQQIYCK4HsKKvqnR5o+1IdVoUAAAAAAJovcgAAAAJUWFRhbHBoYTQAAAAAAAAASi2oEz2g7O52SRy2BCVj88dLSsVzFVqxZBWdLK2K4EQAAAAAdzWUAAAAAAAAAAAAdzWUAAAAAAIAAAAA", + metaXDR: "AAAAAQAAAAIAAAADAA0pGAAAAAAAAAAASi2oEz2g7O52SRy2BCVj88dLSsVzFVqxZBWdLK2K4EQAAAAXSHbm1AANIfEAAAACAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAA0pGAAAAAAAAAAASi2oEz2g7O52SRy2BCVj88dLSsVzFVqxZBWdLK2K4EQAAAAXSHbm1AANIfEAAAADAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAACAAAAAMADSkYAAAAAAAAAABKLagTPaDs7nZJHLYEJWPzx0tKxXMVWrFkFZ0srYrgRAAAABdIdubUAA0h8QAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEADSkYAAAAAAAAAABKLagTPaDs7nZJHLYEJWPzx0tKxXMVWrFkFZ0srYrgRAAAABbRQVLUAA0h8QAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAMADSjEAAAAAgAAAACBsj2Njrx9hd3r4PAxxlBAhgIrgewoq+qdHmj7Uh1WhQAAAAAAmi9yAAAAAlRYVGFscGhhNAAAAAAAAABKLagTPaDs7nZJHLYEJWPzx0tKxXMVWrFkFZ0srYrgRAAAAAAAAAAAstBeAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAQANKRgAAAACAAAAAIGyPY2OvH2F3evg8DHGUECGAiuB7Cir6p0eaPtSHVaFAAAAAACaL3IAAAACVFhUYWxwaGE0AAAAAAAAAEotqBM9oOzudkkctgQlY/PHS0rFcxVasWQVnSytiuBEAAAAAAAAAAA7msoAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAADAA0oxAAAAAAAAAAAgbI9jY68fYXd6+DwMcZQQIYCK4HsKKvqnR5o+1IdVoUAAAAZJU0xXAANGSMAAAARAAAABAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQADMowLgdQAAAAAAAAAAAAAAAAAAAAAAAAAAAEADSkYAAAAAAAAAACBsj2Njrx9hd3r4PAxxlBAhgIrgewoq+qdHmj7Uh1WhQAAABmcgsVcAA0ZIwAAABEAAAAEAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAMyi5RMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAwANKMQAAAABAAAAAIGyPY2OvH2F3evg8DHGUECGAiuB7Cir6p0eaPtSHVaFAAAAAlRYVGFscGhhNAAAAAAAAABKLagTPaDs7nZJHLYEJWPzx0tKxXMVWrFkFZ0srYrgRAAACRatNxoAf/////////8AAAABAAAAAQAAAAAAAAAAAAAAALLQXgAAAAAAAAAAAAAAAAEADSkYAAAAAQAAAACBsj2Njrx9hd3r4PAxxlBAhgIrgewoq+qdHmj7Uh1WhQAAAAJUWFRhbHBoYTQAAAAAAAAASi2oEz2g7O52SRy2BCVj88dLSsVzFVqxZBWdLK2K4EQAAAkWNgGGAH//////////AAAAAQAAAAEAAAAAAAAAAAAAAAA7msoAAAAAAAAAAAA=", + feeChangesXDR: "AAAAAgAAAAMADSSgAAAAAAAAAABKLagTPaDs7nZJHLYEJWPzx0tKxXMVWrFkFZ0srYrgRAAAABdIduc4AA0h8QAAAAIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEADSkYAAAAAAAAAABKLagTPaDs7nZJHLYEJWPzx0tKxXMVWrFkFZ0srYrgRAAAABdIdubUAA0h8QAAAAIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "9caa91eec6e29730f4aabafb60898a8ecedd3bf67b8628e6e32066fbba9bec5d", + index: 0, + sequence: 56, + expected: []EffectOutput{ + { + Address: "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + Details: map[string]interface{}{ + "bought_amount": "200.0000000", + "bought_asset_code": "TXTalpha4", + "bought_asset_issuer": "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + "bought_asset_type": "credit_alphanum12", + "offer_id": xdr.Int64(10104690), + "seller": "GCA3EPMNR26H3BO55PQPAMOGKBAIMARLQHWCRK7KTUPGR62SDVLIL7D6", + "sold_amount": "200.0000000", + "sold_asset_type": "native", + }, + Type: int32(EffectTrade), + TypeString: EffectTypeNames[EffectTrade], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GCA3EPMNR26H3BO55PQPAMOGKBAIMARLQHWCRK7KTUPGR62SDVLIL7D6", + Details: map[string]interface{}{ + "bought_amount": "200.0000000", + "bought_asset_type": "native", + "offer_id": xdr.Int64(10104690), + "seller": "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + "sold_amount": "200.0000000", + "sold_asset_code": "TXTalpha4", + "sold_asset_issuer": "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + "sold_asset_type": "credit_alphanum12", + }, + Type: int32(EffectTrade), + TypeString: EffectTypeNames[EffectTrade], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + Details: map[string]interface{}{ + "bought_amount": "200.0000000", + "bought_asset_code": "TXTalpha4", + "bought_asset_issuer": "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + "bought_asset_type": "credit_alphanum12", + "offer_id": xdr.Int64(10104690), + "seller": "GCA3EPMNR26H3BO55PQPAMOGKBAIMARLQHWCRK7KTUPGR62SDVLIL7D6", + "sold_amount": "200.0000000", + "sold_asset_type": "native", + }, + Type: int32(EffectOfferUpdated), + TypeString: EffectTypeNames[EffectOfferUpdated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GCA3EPMNR26H3BO55PQPAMOGKBAIMARLQHWCRK7KTUPGR62SDVLIL7D6", + Details: map[string]interface{}{ + "bought_amount": "200.0000000", + "bought_asset_type": "native", + "offer_id": xdr.Int64(10104690), + "seller": "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + "sold_amount": "200.0000000", + "sold_asset_code": "TXTalpha4", + "sold_asset_issuer": "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + "sold_asset_type": "credit_alphanum12", + }, + Type: int32(EffectOfferUpdated), + TypeString: EffectTypeNames[EffectOfferUpdated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + Details: map[string]interface{}{ + "bought_amount": "200.0000000", + "bought_asset_code": "TXTalpha4", + "bought_asset_issuer": "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + "bought_asset_type": "credit_alphanum12", + "offer_id": xdr.Int64(10104690), + "seller": "GCA3EPMNR26H3BO55PQPAMOGKBAIMARLQHWCRK7KTUPGR62SDVLIL7D6", + "sold_amount": "200.0000000", + "sold_asset_type": "native", + }, + Type: int32(EffectOfferRemoved), + TypeString: EffectTypeNames[EffectOfferRemoved], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GCA3EPMNR26H3BO55PQPAMOGKBAIMARLQHWCRK7KTUPGR62SDVLIL7D6", + Details: map[string]interface{}{ + "bought_amount": "200.0000000", + "bought_asset_type": "native", + "offer_id": xdr.Int64(10104690), + "seller": "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + "sold_amount": "200.0000000", + "sold_asset_code": "TXTalpha4", + "sold_asset_issuer": "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + "sold_asset_type": "credit_alphanum12", + }, + Type: int32(EffectOfferRemoved), + TypeString: EffectTypeNames[EffectOfferRemoved], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + Details: map[string]interface{}{ + "bought_amount": "200.0000000", + "bought_asset_code": "TXTalpha4", + "bought_asset_issuer": "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + "bought_asset_type": "credit_alphanum12", + "offer_id": xdr.Int64(10104690), + "seller": "GCA3EPMNR26H3BO55PQPAMOGKBAIMARLQHWCRK7KTUPGR62SDVLIL7D6", + "sold_amount": "200.0000000", + "sold_asset_type": "native", + }, + Type: int32(EffectOfferCreated), + TypeString: EffectTypeNames[EffectOfferCreated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GCA3EPMNR26H3BO55PQPAMOGKBAIMARLQHWCRK7KTUPGR62SDVLIL7D6", + Details: map[string]interface{}{ + "bought_amount": "200.0000000", + "bought_asset_type": "native", + "offer_id": xdr.Int64(10104690), + "seller": "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + "sold_amount": "200.0000000", + "sold_asset_code": "TXTalpha4", + "sold_asset_issuer": "GBFC3KATHWQOZ3TWJEOLMBBFMPZ4OS2KYVZRKWVRMQKZ2LFNRLQEIRCV", + "sold_asset_type": "credit_alphanum12", + }, + Type: int32(EffectOfferCreated), + TypeString: EffectTypeNames[EffectOfferCreated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + }, + }, + { + desc: "createPassiveSellOffer", + envelopeXDR: "AAAAAAHwZwJPu1TJhQGgsLRXBzcIeySkeGXzEqh0W9AHWvFDAAAAZAAN3tMAAAACAAAAAQAAAAAAAAAAAAAAAF4FBqwAAAAAAAAAAQAAAAAAAAAEAAAAAAAAAAFDT1AAAAAAALly/iTceP/82O3aZAmd8hyqUjYAANfc5RfN0/iibCtTAAAAADuaygAAAAAJAAAACgAAAAAAAAABB1rxQwAAAEDz2JIw8Z3Owoc5c2tsiY3kzOYUmh32155u00Xs+RYxO5fL0ApYd78URHcYCbe0R32YmuLTfefWQStR3RfhqKAL", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAADAAAAAAAAAAEAAAAAMgQ65fmCczzuwmU3oQLivASzvZdhzjhJOQ6C+xTSDu8AAAAAAKMvZgAAAAFDT1AAAAAAALly/iTceP/82O3aZAmd8hyqUjYAANfc5RfN0/iibCtTAAAA6NSlEAAAAAAAAAAAADuaygAAAAACAAAAAA==", + metaXDR: "AAAAAQAAAAIAAAADAA3fGgAAAAAAAAAAAfBnAk+7VMmFAaCwtFcHNwh7JKR4ZfMSqHRb0Ada8UMAAAAXSHbnOAAN3tMAAAABAAAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAA3fGgAAAAAAAAAAAfBnAk+7VMmFAaCwtFcHNwh7JKR4ZfMSqHRb0Ada8UMAAAAXSHbnOAAN3tMAAAACAAAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAACgAAAAMADd72AAAAAgAAAAAyBDrl+YJzPO7CZTehAuK8BLO9l2HOOEk5DoL7FNIO7wAAAAAAoy9mAAAAAUNPUAAAAAAAuXL+JNx4//zY7dpkCZ3yHKpSNgAA19zlF83T+KJsK1MAAAAAAAAA6NSlEAAAAAABAAAD6AAAAAAAAAAAAAAAAAAAAAIAAAACAAAAADIEOuX5gnM87sJlN6EC4rwEs72XYc44STkOgvsU0g7vAAAAAACjL2YAAAADAA3fGQAAAAAAAAAAMgQ65fmCczzuwmU3oQLivASzvZdhzjhJOQ6C+xTSDu8AAAAXSHbkfAAIGHsAAAAJAAAAAwAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAB3NZQAAAAAAAAAAAAAAAAAAAAAAAAAAAEADd8aAAAAAAAAAAAyBDrl+YJzPO7CZTehAuK8BLO9l2HOOEk5DoL7FNIO7wAAABeEEa58AAgYewAAAAkAAAACAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAADuaygAAAAAAAAAAAAAAAAAAAAAAAAAAAwAN3xkAAAABAAAAADIEOuX5gnM87sJlN6EC4rwEs72XYc44STkOgvsU0g7vAAAAAUNPUAAAAAAAuXL+JNx4//zY7dpkCZ3yHKpSNgAA19zlF83T+KJsK1MAABI3mQjsAH//////////AAAAAQAAAAEAAAAAAAAAAAAAAdGpSiAAAAAAAAAAAAAAAAABAA3fGgAAAAEAAAAAMgQ65fmCczzuwmU3oQLivASzvZdhzjhJOQ6C+xTSDu8AAAABQ09QAAAAAAC5cv4k3Hj//Njt2mQJnfIcqlI2AADX3OUXzdP4omwrUwAAEU7EY9wAf/////////8AAAABAAAAAQAAAAAAAAAAAAAA6NSlEAAAAAAAAAAAAAAAAAMADd7UAAAAAQAAAAAB8GcCT7tUyYUBoLC0Vwc3CHskpHhl8xKodFvQB1rxQwAAAAFDT1AAAAAAALly/iTceP/82O3aZAmd8hyqUjYAANfc5RfN0/iibCtTAAAAAAAAAAB//////////wAAAAEAAAAAAAAAAAAAAAEADd8aAAAAAQAAAAAB8GcCT7tUyYUBoLC0Vwc3CHskpHhl8xKodFvQB1rxQwAAAAFDT1AAAAAAALly/iTceP/82O3aZAmd8hyqUjYAANfc5RfN0/iibCtTAAAA6NSlEAB//////////wAAAAEAAAAAAAAAAAAAAAMADd8aAAAAAAAAAAAB8GcCT7tUyYUBoLC0Vwc3CHskpHhl8xKodFvQB1rxQwAAABdIduc4AA3e0wAAAAIAAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEADd8aAAAAAAAAAAAB8GcCT7tUyYUBoLC0Vwc3CHskpHhl8xKodFvQB1rxQwAAABcM3B04AA3e0wAAAAIAAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + feeChangesXDR: "AAAAAgAAAAMADd7UAAAAAAAAAAAB8GcCT7tUyYUBoLC0Vwc3CHskpHhl8xKodFvQB1rxQwAAABdIduecAA3e0wAAAAEAAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEADd8aAAAAAAAAAAAB8GcCT7tUyYUBoLC0Vwc3CHskpHhl8xKodFvQB1rxQwAAABdIduc4AA3e0wAAAAEAAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "e4b286344ae1c863ab15773ddf6649b08fe031383135194f8613a3a475c41a5a", + index: 0, + sequence: 56, + expected: []EffectOutput{ + { + Address: "GAA7AZYCJ65VJSMFAGQLBNCXA43QQ6ZEUR4GL4YSVB2FXUAHLLYUHIO5", + Details: map[string]interface{}{ + "bought_amount": "100000.0000000", + "bought_asset_code": "COP", + "bought_asset_issuer": "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10694502), + "seller": "GAZAIOXF7GBHGPHOYJSTPIIC4K6AJM55S5Q44OCJHEHIF6YU2IHO6VHU", + "sold_amount": "100.0000000", + "sold_asset_type": "native", + }, + Type: int32(EffectTrade), + TypeString: EffectTypeNames[EffectTrade], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GAZAIOXF7GBHGPHOYJSTPIIC4K6AJM55S5Q44OCJHEHIF6YU2IHO6VHU", + Details: map[string]interface{}{ + "bought_amount": "100.0000000", + "bought_asset_type": "native", + "offer_id": xdr.Int64(10694502), + "seller": "GAA7AZYCJ65VJSMFAGQLBNCXA43QQ6ZEUR4GL4YSVB2FXUAHLLYUHIO5", + "sold_amount": "100000.0000000", + "sold_asset_code": "COP", + "sold_asset_issuer": "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectTrade), + TypeString: EffectTypeNames[EffectTrade], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GAA7AZYCJ65VJSMFAGQLBNCXA43QQ6ZEUR4GL4YSVB2FXUAHLLYUHIO5", + Details: map[string]interface{}{ + "bought_amount": "100000.0000000", + "bought_asset_code": "COP", + "bought_asset_issuer": "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10694502), + "seller": "GAZAIOXF7GBHGPHOYJSTPIIC4K6AJM55S5Q44OCJHEHIF6YU2IHO6VHU", + "sold_amount": "100.0000000", + "sold_asset_type": "native", + }, + Type: int32(EffectOfferUpdated), + TypeString: EffectTypeNames[EffectOfferUpdated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GAZAIOXF7GBHGPHOYJSTPIIC4K6AJM55S5Q44OCJHEHIF6YU2IHO6VHU", + Details: map[string]interface{}{ + "bought_amount": "100.0000000", + "bought_asset_type": "native", + "offer_id": xdr.Int64(10694502), + "seller": "GAA7AZYCJ65VJSMFAGQLBNCXA43QQ6ZEUR4GL4YSVB2FXUAHLLYUHIO5", + "sold_amount": "100000.0000000", + "sold_asset_code": "COP", + "sold_asset_issuer": "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectOfferUpdated), + TypeString: EffectTypeNames[EffectOfferUpdated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GAA7AZYCJ65VJSMFAGQLBNCXA43QQ6ZEUR4GL4YSVB2FXUAHLLYUHIO5", + Details: map[string]interface{}{ + "bought_amount": "100000.0000000", + "bought_asset_code": "COP", + "bought_asset_issuer": "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10694502), + "seller": "GAZAIOXF7GBHGPHOYJSTPIIC4K6AJM55S5Q44OCJHEHIF6YU2IHO6VHU", + "sold_amount": "100.0000000", + "sold_asset_type": "native", + }, + Type: int32(EffectOfferRemoved), + TypeString: EffectTypeNames[EffectOfferRemoved], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GAZAIOXF7GBHGPHOYJSTPIIC4K6AJM55S5Q44OCJHEHIF6YU2IHO6VHU", + Details: map[string]interface{}{ + "bought_amount": "100.0000000", + "bought_asset_type": "native", + "offer_id": xdr.Int64(10694502), + "seller": "GAA7AZYCJ65VJSMFAGQLBNCXA43QQ6ZEUR4GL4YSVB2FXUAHLLYUHIO5", + "sold_amount": "100000.0000000", + "sold_asset_code": "COP", + "sold_asset_issuer": "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectOfferRemoved), + TypeString: EffectTypeNames[EffectOfferRemoved], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GAA7AZYCJ65VJSMFAGQLBNCXA43QQ6ZEUR4GL4YSVB2FXUAHLLYUHIO5", + Details: map[string]interface{}{ + "bought_amount": "100000.0000000", + "bought_asset_code": "COP", + "bought_asset_issuer": "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + "bought_asset_type": "credit_alphanum4", + "offer_id": xdr.Int64(10694502), + "seller": "GAZAIOXF7GBHGPHOYJSTPIIC4K6AJM55S5Q44OCJHEHIF6YU2IHO6VHU", + "sold_amount": "100.0000000", + "sold_asset_type": "native", + }, + Type: int32(EffectOfferCreated), + TypeString: EffectTypeNames[EffectOfferCreated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GAZAIOXF7GBHGPHOYJSTPIIC4K6AJM55S5Q44OCJHEHIF6YU2IHO6VHU", + Details: map[string]interface{}{ + "bought_amount": "100.0000000", + "bought_asset_type": "native", + "offer_id": xdr.Int64(10694502), + "seller": "GAA7AZYCJ65VJSMFAGQLBNCXA43QQ6ZEUR4GL4YSVB2FXUAHLLYUHIO5", + "sold_amount": "100000.0000000", + "sold_asset_code": "COP", + "sold_asset_issuer": "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + "sold_asset_type": "credit_alphanum4", + }, + Type: int32(EffectOfferCreated), + TypeString: EffectTypeNames[EffectOfferCreated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + }, + }, + { + desc: "setOption", + envelopeXDR: "AAAAALly/iTceP/82O3aZAmd8hyqUjYAANfc5RfN0/iibCtTAAAAZAAIGHoAAAAHAAAAAQAAAAAAAAAAAAAAAF4FFtcAAAAAAAAAAQAAAAAAAAAFAAAAAQAAAAAge0MBDbX9OddsGMWIHbY1cGXuGYP4bl1ylIvUklO73AAAAAEAAAACAAAAAQAAAAEAAAABAAAAAwAAAAEAAAABAAAAAQAAAAIAAAABAAAAAwAAAAEAAAAVaHR0cHM6Ly93d3cuaG9tZS5vcmcvAAAAAAAAAQAAAAAge0MBDbX9OddsGMWIHbY1cGXuGYP4bl1ylIvUklO73AAAAAIAAAAAAAAAAaJsK1MAAABAiQjCxE53GjInjJtvNr6gdhztRi0GWOZKlUS2KZBLjX3n2N/y7RRNt7B1ZuFcZAxrnxWHD/fF2XcrEwFAuf4TDA==", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAFAAAAAAAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADAA3iDQAAAAAAAAAAuXL+JNx4//zY7dpkCZ3yHKpSNgAA19zlF83T+KJsK1MAAAAXSHblRAAIGHoAAAAGAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAA3iDQAAAAAAAAAAuXL+JNx4//zY7dpkCZ3yHKpSNgAA19zlF83T+KJsK1MAAAAXSHblRAAIGHoAAAAHAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAgAAAAMADeINAAAAAAAAAAC5cv4k3Hj//Njt2mQJnfIcqlI2AADX3OUXzdP4omwrUwAAABdIduVEAAgYegAAAAcAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEADeINAAAAAAAAAAC5cv4k3Hj//Njt2mQJnfIcqlI2AADX3OUXzdP4omwrUwAAABdIduVEAAgYegAAAAcAAAABAAAAAQAAAAAge0MBDbX9OddsGMWIHbY1cGXuGYP4bl1ylIvUklO73AAAAAEAAAAVaHR0cHM6Ly93d3cuaG9tZS5vcmcvAAAAAwECAwAAAAEAAAAAIHtDAQ21/TnXbBjFiB22NXBl7hmD+G5dcpSL1JJTu9wAAAACAAAAAAAAAAA=", + feeChangesXDR: "AAAAAgAAAAMADd8YAAAAAAAAAAC5cv4k3Hj//Njt2mQJnfIcqlI2AADX3OUXzdP4omwrUwAAABdIduWoAAgYegAAAAYAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEADeINAAAAAAAAAAC5cv4k3Hj//Njt2mQJnfIcqlI2AADX3OUXzdP4omwrUwAAABdIduVEAAgYegAAAAYAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "e76b7b0133690fbfb2de8fa9ca2273cb4f2e29447e0cf0e14a5f82d0daa48760", + index: 0, + sequence: 56, + expected: []EffectOutput{ + { + Address: "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + Details: map[string]interface{}{ + "home_domain": "https://www.home.org/", + }, + Type: int32(EffectAccountHomeDomainUpdated), + TypeString: EffectTypeNames[EffectAccountHomeDomainUpdated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + Details: map[string]interface{}{ + "high_threshold": xdr.Uint32(3), + "low_threshold": xdr.Uint32(1), + "med_threshold": xdr.Uint32(2), + }, + Type: int32(EffectAccountThresholdsUpdated), + TypeString: EffectTypeNames[EffectAccountThresholdsUpdated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + Details: map[string]interface{}{ + "auth_required_flag": true, + "auth_revocable_flag": false, + }, + Type: int32(EffectAccountFlagsUpdated), + TypeString: EffectTypeNames[EffectAccountFlagsUpdated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + Details: map[string]interface{}{ + "inflation_destination": "GAQHWQYBBW272OOXNQMMLCA5WY2XAZPODGB7Q3S5OKKIXVESKO55ZQ7C", + }, + Type: int32(EffectAccountInflationDestinationUpdated), + TypeString: EffectTypeNames[EffectAccountInflationDestinationUpdated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + Details: map[string]interface{}{ + "public_key": "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + "weight": int32(3), + }, + Type: int32(EffectSignerUpdated), + TypeString: EffectTypeNames[EffectSignerUpdated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + { + Address: "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + Details: map[string]interface{}{ + "public_key": "GAQHWQYBBW272OOXNQMMLCA5WY2XAZPODGB7Q3S5OKKIXVESKO55ZQ7C", + "weight": int32(2), + }, + Type: int32(EffectSignerCreated), + TypeString: EffectTypeNames[EffectSignerCreated], + OperationID: int64(240518172673), + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 56, + }, + }, + }, + { + desc: "changeTrust - trustline created", + envelopeXDR: "AAAAAKturFHJX/eRt5gM6qIXAMbaXvlImqLysA6Qr9tLemxfAAAAZAAAACYAAAABAAAAAAAAAAAAAAABAAAAAAAAAAYAAAABVVNEAAAAAAD5Jjibq+Rf5jsUyQ2/tGzCwiRg0Zd5nj9jARA1Skjz+H//////////AAAAAAAAAAFLemxfAAAAQKN8LftAafeoAGmvpsEokqm47jAuqw4g1UWjmL0j6QPm1jxoalzDwDS3W+N2HOHdjSJlEQaTxGBfQKHhr6nNsAA=", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADAAAAKAAAAAAAAAAAq26sUclf95G3mAzqohcAxtpe+UiaovKwDpCv20t6bF8AAAACVAvjOAAAACYAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAKAAAAAAAAAAAq26sUclf95G3mAzqohcAxtpe+UiaovKwDpCv20t6bF8AAAACVAvjOAAAACYAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAMAAAAoAAAAAAAAAACrbqxRyV/3kbeYDOqiFwDG2l75SJqi8rAOkK/bS3psXwAAAAJUC+M4AAAAJgAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAoAAAAAAAAAACrbqxRyV/3kbeYDOqiFwDG2l75SJqi8rAOkK/bS3psXwAAAAJUC+M4AAAAJgAAAAEAAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAQAAAACrbqxRyV/3kbeYDOqiFwDG2l75SJqi8rAOkK/bS3psXwAAAAFVU0QAAAAAAPkmOJur5F/mOxTJDb+0bMLCJGDRl3meP2MBEDVKSPP4AAAAAAAAAAB//////////wAAAAAAAAAAAAAAAA==", + feeChangesXDR: "AAAAAgAAAAMAAAAmAAAAAAAAAACrbqxRyV/3kbeYDOqiFwDG2l75SJqi8rAOkK/bS3psXwAAAAJUC+QAAAAAJgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAoAAAAAAAAAACrbqxRyV/3kbeYDOqiFwDG2l75SJqi8rAOkK/bS3psXwAAAAJUC+OcAAAAJgAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "6fa467b53f5386d77ad35c2502ed2cd3dd8b460a5be22b6b2818b81bcd3ed2da", + index: 0, + sequence: 40, + expected: []EffectOutput{ + { + Address: "GCVW5LCRZFP7PENXTAGOVIQXADDNUXXZJCNKF4VQB2IK7W2LPJWF73UG", + Type: int32(EffectTrustlineCreated), + TypeString: EffectTypeNames[EffectTrustlineCreated], + OperationID: int64(171798695937), + Details: map[string]interface{}{ + "limit": "922337203685.4775807", + "asset_code": "USD", + "asset_type": "credit_alphanum4", + "asset_issuer": "GD4SMOE3VPSF7ZR3CTEQ3P5UNTBMEJDA2GLXTHR7MMARANKKJDZ7RPGF", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 40, + }, + }, + }, + { + desc: "changeTrust - trustline removed", + envelopeXDR: "AAAAABwDSftLnTVAHpKUGYPZfTJr6rIm5Z5IqDHVBFuTI3ubAAAAZAARM9kAAAADAAAAAQAAAAAAAAAAAAAAAF4XMm8AAAAAAAAAAQAAAAAAAAAGAAAAAk9DSVRva2VuAAAAAAAAAABJxf/HoI4oaD9CLBvECRhG9GPMNa/65PTI9N7F37o4nwAAAAAAAAAAAAAAAAAAAAGTI3ubAAAAQMHTFPeyHA+W2EYHVDut4dQ18zvF+47SsTPaePwZUaCgw/A3tKDx7sO7R8xlI3GwKQl91Ljmm1dbvAONU9nk/AQ=", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAGAAAAAAAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADABEz3wAAAAAAAAAAHANJ+0udNUAekpQZg9l9MmvqsiblnkioMdUEW5Mje5sAAAAXSHbm1AARM9kAAAACAAAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABABEz3wAAAAAAAAAAHANJ+0udNUAekpQZg9l9MmvqsiblnkioMdUEW5Mje5sAAAAXSHbm1AARM9kAAAADAAAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAABAAAAAMAETPeAAAAAQAAAAAcA0n7S501QB6SlBmD2X0ya+qyJuWeSKgx1QRbkyN7mwAAAAJPQ0lUb2tlbgAAAAAAAAAAScX/x6COKGg/QiwbxAkYRvRjzDWv+uT0yPTexd+6OJ8AAAAAAAAAAH//////////AAAAAQAAAAAAAAAAAAAAAgAAAAEAAAAAHANJ+0udNUAekpQZg9l9MmvqsiblnkioMdUEW5Mje5sAAAACT0NJVG9rZW4AAAAAAAAAAEnF/8egjihoP0IsG8QJGEb0Y8w1r/rk9Mj03sXfujifAAAAAwARM98AAAAAAAAAABwDSftLnTVAHpKUGYPZfTJr6rIm5Z5IqDHVBFuTI3ubAAAAF0h25tQAETPZAAAAAwAAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAQARM98AAAAAAAAAABwDSftLnTVAHpKUGYPZfTJr6rIm5Z5IqDHVBFuTI3ubAAAAF0h25tQAETPZAAAAAwAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA", + feeChangesXDR: "AAAAAgAAAAMAETPeAAAAAAAAAAAcA0n7S501QB6SlBmD2X0ya+qyJuWeSKgx1QRbkyN7mwAAABdIduc4ABEz2QAAAAIAAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAETPfAAAAAAAAAAAcA0n7S501QB6SlBmD2X0ya+qyJuWeSKgx1QRbkyN7mwAAABdIdubUABEz2QAAAAIAAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "0f1e93ed9a83edb01ad8ccab67fd59dc7a513c413a8d5a580c5eb7a9c44f2844", + index: 0, + sequence: 40, + expected: []EffectOutput{ + { + Address: "GAOAGSP3JOOTKQA6SKKBTA6ZPUZGX2VSE3SZ4SFIGHKQIW4TEN5ZX3WW", + Type: int32(EffectTrustlineRemoved), + TypeString: EffectTypeNames[EffectTrustlineRemoved], + OperationID: int64(171798695937), + Details: map[string]interface{}{ + "limit": "0.0000000", + "asset_code": "OCIToken", + "asset_type": "credit_alphanum12", + "asset_issuer": "GBE4L76HUCHCQ2B7IIWBXRAJDBDPIY6MGWX7VZHUZD2N5RO7XI4J6GTJ", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 40, + }, + }, + }, + { + desc: "changeTrust - trustline updated", + envelopeXDR: "AAAAAHHbEhVipyZ2k4byyCZkS1Bdvpj7faBChuYo8S/Rt89UAAAAZAAQuJIAAAAHAAAAAQAAAAAAAAAAAAAAAF4XVskAAAAAAAAAAQAAAAAAAAAGAAAAAlRFU1RBU1NFVAAAAAAAAAA7JUkkD+tgCi2xTVyEcs4WZXOA0l7w2orZg/bghXOgkAAAAAA7msoAAAAAAAAAAAHRt89UAAAAQOCi2ylqRvvRzZaCFjGkLYFk7DCjJA5uZ1nXo8FaPCRl2LZczoMbc46sZIlHh0ENzk7fKjFnRPMo8XAirrrf2go=", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAGAAAAAAAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADABE6jwAAAAAAAAAAcdsSFWKnJnaThvLIJmRLUF2+mPt9oEKG5ijxL9G3z1QAAAAAO5rHRAAQuJIAAAAGAAAAAgAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABABE6jwAAAAAAAAAAcdsSFWKnJnaThvLIJmRLUF2+mPt9oEKG5ijxL9G3z1QAAAAAO5rHRAAQuJIAAAAHAAAAAgAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAgAAAAMAETqAAAAAAQAAAABx2xIVYqcmdpOG8sgmZEtQXb6Y+32gQobmKPEv0bfPVAAAAAJURVNUQVNTRVQAAAAAAAAAOyVJJA/rYAotsU1chHLOFmVzgNJe8NqK2YP24IVzoJAAAAAAO5rKAAAAAAA7msoAAAAAAQAAAAAAAAAAAAAAAQAROo8AAAABAAAAAHHbEhVipyZ2k4byyCZkS1Bdvpj7faBChuYo8S/Rt89UAAAAAlRFU1RBU1NFVAAAAAAAAAA7JUkkD+tgCi2xTVyEcs4WZXOA0l7w2orZg/bghXOgkAAAAAA7msoAAAAAADuaygAAAAABAAAAAAAAAAA=", + feeChangesXDR: "AAAAAgAAAAMAETp/AAAAAAAAAABx2xIVYqcmdpOG8sgmZEtQXb6Y+32gQobmKPEv0bfPVAAAAAA7mseoABC4kgAAAAYAAAACAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAETqPAAAAAAAAAABx2xIVYqcmdpOG8sgmZEtQXb6Y+32gQobmKPEv0bfPVAAAAAA7msdEABC4kgAAAAYAAAACAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "dc8d4714d7db3d0e27ae07f629bc72f1605fc24a2d178af04edbb602592791aa", + index: 0, + sequence: 40, + expected: []EffectOutput{ + { + Address: "GBY5WEQVMKTSM5UTQ3ZMQJTEJNIF3PUY7N62AQUG4YUPCL6RW7HVJARI", + Type: int32(EffectTrustlineUpdated), + TypeString: EffectTypeNames[EffectTrustlineUpdated], + OperationID: int64(171798695937), + Details: map[string]interface{}{ + "limit": "100.0000000", + "asset_code": "TESTASSET", + "asset_type": "credit_alphanum12", + "asset_issuer": "GA5SKSJEB7VWACRNWFGVZBDSZYLGK44A2JPPBWUK3GB7NYEFOOQJAC2B", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 40, + }, + }, + }, + { + desc: "allowTrust", + envelopeXDR: "AAAAAPkmOJur5F/mOxTJDb+0bMLCJGDRl3meP2MBEDVKSPP4AAAAZAAAACYAAAACAAAAAAAAAAAAAAABAAAAAAAAAAcAAAAAq26sUclf95G3mAzqohcAxtpe+UiaovKwDpCv20t6bF8AAAABVVNEAAAAAAEAAAAAAAAAAUpI8/gAAABA6O2fe1gQBwoO0fMNNEUKH0QdVXVjEWbN5VL51DmRUedYMMXtbX5JKVSzla2kIGvWgls1dXuXHZY/IOlaK01rBQ==", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADAAAAKQAAAAAAAAAA+SY4m6vkX+Y7FMkNv7RswsIkYNGXeZ4/YwEQNUpI8/gAAAACVAvi1AAAACYAAAABAAAAAAAAAAAAAAADAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAKQAAAAAAAAAA+SY4m6vkX+Y7FMkNv7RswsIkYNGXeZ4/YwEQNUpI8/gAAAACVAvi1AAAACYAAAACAAAAAAAAAAAAAAADAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAgAAAAMAAAAoAAAAAQAAAACrbqxRyV/3kbeYDOqiFwDG2l75SJqi8rAOkK/bS3psXwAAAAFVU0QAAAAAAPkmOJur5F/mOxTJDb+0bMLCJGDRl3meP2MBEDVKSPP4AAAAAAAAAAB//////////wAAAAAAAAAAAAAAAAAAAAEAAAApAAAAAQAAAACrbqxRyV/3kbeYDOqiFwDG2l75SJqi8rAOkK/bS3psXwAAAAFVU0QAAAAAAPkmOJur5F/mOxTJDb+0bMLCJGDRl3meP2MBEDVKSPP4AAAAAAAAAAB//////////wAAAAEAAAAAAAAAAA==", + feeChangesXDR: "AAAAAgAAAAMAAAAnAAAAAAAAAAD5Jjibq+Rf5jsUyQ2/tGzCwiRg0Zd5nj9jARA1Skjz+AAAAAJUC+OcAAAAJgAAAAEAAAAAAAAAAAAAAAMAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAApAAAAAAAAAAD5Jjibq+Rf5jsUyQ2/tGzCwiRg0Zd5nj9jARA1Skjz+AAAAAJUC+M4AAAAJgAAAAEAAAAAAAAAAAAAAAMAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "6d2e30fd57492bf2e2b132e1bc91a548a369189bebf77eb2b3d829121a9d2c50", + index: 0, + sequence: 41, + expected: []EffectOutput{ + { + Address: "GD4SMOE3VPSF7ZR3CTEQ3P5UNTBMEJDA2GLXTHR7MMARANKKJDZ7RPGF", + Type: int32(EffectTrustlineFlagsUpdated), + TypeString: EffectTypeNames[EffectTrustlineFlagsUpdated], + OperationID: int64(176093663233), + Details: map[string]interface{}{ + "trustor": "GCVW5LCRZFP7PENXTAGOVIQXADDNUXXZJCNKF4VQB2IK7W2LPJWF73UG", + "asset_code": "USD", + "asset_type": "credit_alphanum4", + "asset_issuer": "GD4SMOE3VPSF7ZR3CTEQ3P5UNTBMEJDA2GLXTHR7MMARANKKJDZ7RPGF", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 41, + }, + { + Address: "GD4SMOE3VPSF7ZR3CTEQ3P5UNTBMEJDA2GLXTHR7MMARANKKJDZ7RPGF", + Type: int32(EffectTrustlineFlagsUpdated), + TypeString: EffectTypeNames[EffectTrustlineFlagsUpdated], + OperationID: int64(176093663233), + Details: map[string]interface{}{ + "asset_code": "USD", + "asset_issuer": "GD4SMOE3VPSF7ZR3CTEQ3P5UNTBMEJDA2GLXTHR7MMARANKKJDZ7RPGF", + "asset_type": "credit_alphanum4", + "authorized_flag": true, + "trustor": "GCVW5LCRZFP7PENXTAGOVIQXADDNUXXZJCNKF4VQB2IK7W2LPJWF73UG", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 41, + }, + }, + }, + { + desc: "accountMerge (Destination)", + envelopeXDR: "AAAAAI77mqNTy9VPgmgn+//uvjP8VJxJ1FHQ4jCrYS+K4+HvAAAAZAAAACsAAAABAAAAAAAAAAAAAAABAAAAAAAAAAgAAAAAYvwdC9CRsrYcDdZWNGsqaNfTR8bywsjubQRHAlb8BfcAAAAAAAAAAYrj4e8AAABA3jJ7wBrRpsrcnqBQWjyzwvVz2v5UJ56G60IhgsaWQFSf+7om462KToc+HJ27aLVOQ83dGh1ivp+VIuREJq/SBw==", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAJUC+OcAAAAAA==", + metaXDR: "AAAAAQAAAAIAAAADAAAALAAAAAAAAAAAjvuao1PL1U+CaCf7/+6+M/xUnEnUUdDiMKthL4rj4e8AAAACVAvjnAAAACsAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAALAAAAAAAAAAAjvuao1PL1U+CaCf7/+6+M/xUnEnUUdDiMKthL4rj4e8AAAACVAvjnAAAACsAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAABAAAAAMAAAArAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtonM3Az4AAAAAAAAABIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAsAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9w3gtowg5/CUAAAAAAAAABIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAMAAAAsAAAAAAAAAACO+5qjU8vVT4JoJ/v/7r4z/FScSdRR0OIwq2EviuPh7wAAAAJUC+OcAAAAKwAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAI77mqNTy9VPgmgn+//uvjP8VJxJ1FHQ4jCrYS+K4+Hv", + feeChangesXDR: "AAAAAgAAAAMAAAArAAAAAAAAAACO+5qjU8vVT4JoJ/v/7r4z/FScSdRR0OIwq2EviuPh7wAAAAJUC+QAAAAAKwAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAsAAAAAAAAAACO+5qjU8vVT4JoJ/v/7r4z/FScSdRR0OIwq2EviuPh7wAAAAJUC+OcAAAAKwAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "e0773d07aba23d11e6a06b021682294be1f9f202a2926827022539662ce2c7fc", + index: 0, + sequence: 44, + expected: []EffectOutput{ + { + Address: "GCHPXGVDKPF5KT4CNAT7X77OXYZ7YVE4JHKFDUHCGCVWCL4K4PQ67KKZ", + Type: int32(EffectAccountDebited), + TypeString: EffectTypeNames[EffectAccountDebited], + OperationID: int64(188978565121), + Details: map[string]interface{}{ + "amount": "999.9999900", + "asset_type": "native", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 44, + }, + { + Address: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + Type: int32(EffectAccountCredited), + TypeString: EffectTypeNames[EffectAccountCredited], + OperationID: int64(188978565121), + Details: map[string]interface{}{ + "amount": "999.9999900", + "asset_type": "native", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 44, + }, + { + Address: "GCHPXGVDKPF5KT4CNAT7X77OXYZ7YVE4JHKFDUHCGCVWCL4K4PQ67KKZ", + Type: int32(EffectAccountRemoved), + TypeString: EffectTypeNames[EffectAccountRemoved], + OperationID: int64(188978565121), + Details: map[string]interface{}{}, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 44, + }, + }, + }, + { + desc: "inflation", + envelopeXDR: "AAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3AAAAZAAAAAAAAAAVAAAAAAAAAAAAAAABAAAAAAAAAAkAAAAAAAAAAVb8BfcAAABABUHuXY+MTgW/wDv5+NDVh9fw4meszxeXO98HEQfgXVeCZ7eObCI2orSGUNA/SK6HV9/uTVSxIQQWIso1QoxHBQ==", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAJAAAAAAAAAAIAAAAAYvwdC9CRsrYcDdZWNGsqaNfTR8bywsjubQRHAlb8BfcAAIrEjCYwXAAAAADj3dgEQp1N5U3fBSOCx/nr5XtiCmNJ2oMJZMx+MYK3JwAAIrEjfceLAAAAAA==", + metaXDR: "AAAAAQAAAAIAAAADAAAALwAAAAAAAAAAYvwdC9CRsrYcDdZWNGsqaNfTR8bywsjubQRHAlb8BfcLGiubZdPvaAAAAAAAAAAUAAAAAAAAAAEAAAAAYvwdC9CRsrYcDdZWNGsqaNfTR8bywsjubQRHAlb8BfcAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAALwAAAAAAAAAAYvwdC9CRsrYcDdZWNGsqaNfTR8bywsjubQRHAlb8BfcLGiubZdPvaAAAAAAAAAAVAAAAAAAAAAEAAAAAYvwdC9CRsrYcDdZWNGsqaNfTR8bywsjubQRHAlb8BfcAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAABAAAAAMAAAAvAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9wsaK5tl0+9oAAAAAAAAABUAAAAAAAAAAQAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9wAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAvAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9wsatl/x+h/EAAAAAAAAABUAAAAAAAAAAQAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9wAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAMAAAAuAAAAAAAAAADj3dgEQp1N5U3fBSOCx/nr5XtiCmNJ2oMJZMx+MYK3JwLGivC7E/+cAAAALQAAAAEAAAAAAAAAAQAAAADj3dgEQp1N5U3fBSOCx/nr5XtiCmNJ2oMJZMx+MYK3JwAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAvAAAAAAAAAADj3dgEQp1N5U3fBSOCx/nr5XtiCmNJ2oMJZMx+MYK3JwLGraHekccnAAAALQAAAAEAAAAAAAAAAQAAAADj3dgEQp1N5U3fBSOCx/nr5XtiCmNJ2oMJZMx+MYK3JwAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + feeChangesXDR: "AAAAAgAAAAMAAAAuAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9wsaK5tl0+/MAAAAAAAAABQAAAAAAAAAAQAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9wAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAvAAAAAAAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9wsaK5tl0+9oAAAAAAAAABQAAAAAAAAAAQAAAABi/B0L0JGythwN1lY0aypo19NHxvLCyO5tBEcCVvwF9wAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "ea93efd8c2f4e45c0318c69ec958623a0e4374f40d569eec124d43c8a54d6256", + index: 0, + sequence: 47, + expected: []EffectOutput{ + { + Address: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + Type: int32(EffectAccountCredited), + TypeString: EffectTypeNames[EffectAccountCredited], + OperationID: int64(201863467009), + Details: map[string]interface{}{ + "amount": "15257676.9536092", + "asset_type": "native", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 47, + }, + { + Address: "GDR53WAEIKOU3ZKN34CSHAWH7HV6K63CBJRUTWUDBFSMY7RRQK3SPKOS", + Type: int32(EffectAccountCredited), + TypeString: EffectTypeNames[EffectAccountCredited], + OperationID: int64(201863467009), + Details: map[string]interface{}{ + "amount": "3814420.0001419", + "asset_type": "native", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 47, + }, + }, + }, + { + desc: "manageData - data created", + envelopeXDR: "AAAAADEhMVDHiYXdz5z8l73XGyrQ2RN85ZRW1uLsCNQumfsZAAAAZAAAADAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAoAAAAFbmFtZTIAAAAAAAABAAAABDU2NzgAAAAAAAAAAS6Z+xkAAABAjxgnTRBCa0n1efZocxpEjXeITQ5sEYTVd9fowuto2kPw5eFwgVnz6OrKJwCRt5L8ylmWiATXVI3Zyfi3yTKqBA==", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAKAAAAAAAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADAAAAMQAAAAAAAAAAMSExUMeJhd3PnPyXvdcbKtDZE3zllFbW4uwI1C6Z+xkAAAACVAvi1AAAADAAAAABAAAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAMQAAAAAAAAAAMSExUMeJhd3PnPyXvdcbKtDZE3zllFbW4uwI1C6Z+xkAAAACVAvi1AAAADAAAAACAAAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAMAAAAxAAAAAAAAAAAxITFQx4mF3c+c/Je91xsq0NkTfOWUVtbi7AjULpn7GQAAAAJUC+LUAAAAMAAAAAIAAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAxAAAAAAAAAAAxITFQx4mF3c+c/Je91xsq0NkTfOWUVtbi7AjULpn7GQAAAAJUC+LUAAAAMAAAAAIAAAACAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxAAAAAwAAAAAxITFQx4mF3c+c/Je91xsq0NkTfOWUVtbi7AjULpn7GQAAAAVuYW1lMgAAAAAAAAQ1Njc4AAAAAAAAAAA=", + feeChangesXDR: "AAAAAgAAAAMAAAAxAAAAAAAAAAAxITFQx4mF3c+c/Je91xsq0NkTfOWUVtbi7AjULpn7GQAAAAJUC+OcAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAxAAAAAAAAAAAxITFQx4mF3c+c/Je91xsq0NkTfOWUVtbi7AjULpn7GQAAAAJUC+M4AAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "e4609180751e7702466a8845857df43e4d154ec84b6bad62ce507fe12f1daf99", + index: 0, + sequence: 49, + expected: []EffectOutput{ + { + Address: "GAYSCMKQY6EYLXOPTT6JPPOXDMVNBWITPTSZIVWW4LWARVBOTH5RTLAD", + Type: int32(EffectDataCreated), + TypeString: EffectTypeNames[EffectDataCreated], + OperationID: int64(210453401601), + Details: map[string]interface{}{ + "name": xdr.String64("name2"), + "value": "NTY3OA==", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 49, + }, + }, + }, + { + desc: "manageData - data removed", + envelopeXDR: "AAAAALly/iTceP/82O3aZAmd8hyqUjYAANfc5RfN0/iibCtTAAAAZAAIGHoAAAAKAAAAAQAAAAAAAAAAAAAAAF4XaMIAAAAAAAAAAQAAAAAAAAAKAAAABWhlbGxvAAAAAAAAAAAAAAAAAAABomwrUwAAAEDyu3HI9bdkzNBs4UgTjVmYt3LQ0CC/6a8yWBmz8OiKeY/RJ9wJvV9/m0JWGtFWbPOXWBg/Pj3ttgKMiHh9TKoF", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAKAAAAAAAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADABE92wAAAAAAAAAAuXL+JNx4//zY7dpkCZ3yHKpSNgAA19zlF83T+KJsK1MAAAAXSHbkGAAIGHoAAAAJAAAAAgAAAAEAAAAAIHtDAQ21/TnXbBjFiB22NXBl7hmD+G5dcpSL1JJTu9wAAAABAAAAFWh0dHBzOi8vd3d3LmhvbWUub3JnLwAAAAMBAgMAAAABAAAAACB7QwENtf0512wYxYgdtjVwZe4Zg/huXXKUi9SSU7vcAAAAAgAAAAAAAAAAAAAAAQARPdsAAAAAAAAAALly/iTceP/82O3aZAmd8hyqUjYAANfc5RfN0/iibCtTAAAAF0h25BgACBh6AAAACgAAAAIAAAABAAAAACB7QwENtf0512wYxYgdtjVwZe4Zg/huXXKUi9SSU7vcAAAAAQAAABVodHRwczovL3d3dy5ob21lLm9yZy8AAAADAQIDAAAAAQAAAAAge0MBDbX9OddsGMWIHbY1cGXuGYP4bl1ylIvUklO73AAAAAIAAAAAAAAAAAAAAAEAAAAEAAAAAwARPcsAAAADAAAAALly/iTceP/82O3aZAmd8hyqUjYAANfc5RfN0/iibCtTAAAABWhlbGxvAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAMAAAAAuXL+JNx4//zY7dpkCZ3yHKpSNgAA19zlF83T+KJsK1MAAAAFaGVsbG8AAAAAAAADABE92wAAAAAAAAAAuXL+JNx4//zY7dpkCZ3yHKpSNgAA19zlF83T+KJsK1MAAAAXSHbkGAAIGHoAAAAKAAAAAgAAAAEAAAAAIHtDAQ21/TnXbBjFiB22NXBl7hmD+G5dcpSL1JJTu9wAAAABAAAAFWh0dHBzOi8vd3d3LmhvbWUub3JnLwAAAAMBAgMAAAABAAAAACB7QwENtf0512wYxYgdtjVwZe4Zg/huXXKUi9SSU7vcAAAAAgAAAAAAAAAAAAAAAQARPdsAAAAAAAAAALly/iTceP/82O3aZAmd8hyqUjYAANfc5RfN0/iibCtTAAAAF0h25BgACBh6AAAACgAAAAEAAAABAAAAACB7QwENtf0512wYxYgdtjVwZe4Zg/huXXKUi9SSU7vcAAAAAQAAABVodHRwczovL3d3dy5ob21lLm9yZy8AAAADAQIDAAAAAQAAAAAge0MBDbX9OddsGMWIHbY1cGXuGYP4bl1ylIvUklO73AAAAAIAAAAAAAAAAA==", + feeChangesXDR: "AAAAAgAAAAMAET3LAAAAAAAAAAC5cv4k3Hj//Njt2mQJnfIcqlI2AADX3OUXzdP4omwrUwAAABdIduR8AAgYegAAAAkAAAACAAAAAQAAAAAge0MBDbX9OddsGMWIHbY1cGXuGYP4bl1ylIvUklO73AAAAAEAAAAVaHR0cHM6Ly93d3cuaG9tZS5vcmcvAAAAAwECAwAAAAEAAAAAIHtDAQ21/TnXbBjFiB22NXBl7hmD+G5dcpSL1JJTu9wAAAACAAAAAAAAAAAAAAABABE92wAAAAAAAAAAuXL+JNx4//zY7dpkCZ3yHKpSNgAA19zlF83T+KJsK1MAAAAXSHbkGAAIGHoAAAAJAAAAAgAAAAEAAAAAIHtDAQ21/TnXbBjFiB22NXBl7hmD+G5dcpSL1JJTu9wAAAABAAAAFWh0dHBzOi8vd3d3LmhvbWUub3JnLwAAAAMBAgMAAAABAAAAACB7QwENtf0512wYxYgdtjVwZe4Zg/huXXKUi9SSU7vcAAAAAgAAAAAAAAAA", + hash: "397b208adb3d484d14ddd3237422baae0b6bd1e8feb3c970147bc6bcc493d112", + index: 0, + sequence: 49, + expected: []EffectOutput{ + { + Address: "GC4XF7RE3R4P77GY5XNGICM56IOKUURWAAANPXHFC7G5H6FCNQVVH3OH", + Type: int32(EffectDataRemoved), + TypeString: EffectTypeNames[EffectDataRemoved], + OperationID: int64(210453401601), + Details: map[string]interface{}{ + "name": xdr.String64("hello"), + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 49, + }, + }, + }, + { + desc: "manageData - data updated", + envelopeXDR: "AAAAAKO5w1Op9wij5oMFtCTUoGO9YgewUKQyeIw1g/L0mMP+AAAAZAAALbYAADNjAAAAAQAAAAAAAAAAAAAAAF4WVfgAAAAAAAAAAQAAAAEAAAAAOO6NdKTWKbGao6zsPag+izHxq3eUPLiwjREobLhQAmQAAAAKAAAAOEdDUjNUUTJUVkgzUVJJN0dRTUMzSUpHVVVCUjMyWVFIV0JJS0lNVFlSUTJZSDRYVVREQjc1VUtFAAAAAQAAABQxNTc4NTIxMjA0XzI5MzI5MDI3OAAAAAAAAAAC0oPafQAAAEAcsS0iq/t8i+p85xwLsRy8JpRNEeqobEC5yuhO9ouVf3PE0VjLqv8sDd0St4qbtXU5fqlHd49R9CR+z7tiRLEB9JjD/gAAAEBmaa9sGxQhEhrakzXcSNpMbR4nox/Ha0p/1sI4tabNEzjgYLwKMn1U9tIdVvKKDwE22jg+CI2FlPJ3+FJPmKUA", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAKAAAAAAAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADABEK2wAAAAAAAAAAo7nDU6n3CKPmgwW0JNSgY71iB7BQpDJ4jDWD8vSYw/4AAAAXSGLVVAAALbYAADNiAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABABEK2wAAAAAAAAAAo7nDU6n3CKPmgwW0JNSgY71iB7BQpDJ4jDWD8vSYw/4AAAAXSGLVVAAALbYAADNjAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAgAAAAMAEQqbAAAAAwAAAAA47o10pNYpsZqjrOw9qD6LMfGrd5Q8uLCNEShsuFACZAAAADhHQ1IzVFEyVFZIM1FSSTdHUU1DM0lKR1VVQlIzMllRSFdCSUtJTVRZUlEyWUg0WFVUREI3NVVLRQAAABQxNTc4NTIwODU4XzI1MjM5MTc2OAAAAAAAAAAAAAAAAQARCtsAAAADAAAAADjujXSk1imxmqOs7D2oPosx8at3lDy4sI0RKGy4UAJkAAAAOEdDUjNUUTJUVkgzUVJJN0dRTUMzSUpHVVVCUjMyWVFIV0JJS0lNVFlSUTJZSDRYVVREQjc1VUtFAAAAFDE1Nzg1MjEyMDRfMjkzMjkwMjc4AAAAAAAAAAA=", + feeChangesXDR: "AAAAAgAAAAMAEQqbAAAAAAAAAACjucNTqfcIo+aDBbQk1KBjvWIHsFCkMniMNYPy9JjD/gAAABdIYtW4AAAttgAAM2IAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAEQrbAAAAAAAAAACjucNTqfcIo+aDBbQk1KBjvWIHsFCkMniMNYPy9JjD/gAAABdIYtVUAAAttgAAM2IAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "c60b74a14b628d06d3683db8b36ce81344967ac13bc433124bcef44115fbb257", + index: 0, + sequence: 49, + expected: []EffectOutput{ + { + Address: "GA4O5DLUUTLCTMM2UOWOYPNIH2FTD4NLO6KDZOFQRUISQ3FYKABGJLPC", + Type: int32(EffectDataUpdated), + TypeString: EffectTypeNames[EffectDataUpdated], + OperationID: int64(210453401601), + Details: map[string]interface{}{ + "name": xdr.String64("GCR3TQ2TVH3QRI7GQMC3IJGUUBR32YQHWBIKIMTYRQ2YH4XUTDB75UKE"), + "value": "MTU3ODUyMTIwNF8yOTMyOTAyNzg=", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 49, + }, + }, + }, + { + desc: "bumpSequence - new_seq is the same as current sequence", + envelopeXDR: "AAAAAKGX7RT96eIn205uoUHYnqLbt2cPRNORraEoeTAcrRKUAAAAZAAAAEXZZLgDAAAAAAAAAAAAAAABAAAAAAAAAAsAAABF2WS4AwAAAAAAAAABHK0SlAAAAECcI6ex0Dq6YAh6aK14jHxuAvhvKG2+NuzboAKrfYCaC1ZSQ77BYH/5MghPX97JO9WXV17ehNK7d0umxBgaJj8A", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAALAAAAAAAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADAAAAPQAAAAAAAAAAoZftFP3p4ifbTm6hQdieotu3Zw9E05GtoSh5MBytEpQAAAACVAvicAAAAEXZZLgCAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAPQAAAAAAAAAAoZftFP3p4ifbTm6hQdieotu3Zw9E05GtoSh5MBytEpQAAAACVAvicAAAAEXZZLgDAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA==", + feeChangesXDR: "AAAAAgAAAAMAAAA8AAAAAAAAAAChl+0U/eniJ9tObqFB2J6i27dnD0TTka2hKHkwHK0SlAAAAAJUC+LUAAAARdlkuAIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAA9AAAAAAAAAAChl+0U/eniJ9tObqFB2J6i27dnD0TTka2hKHkwHK0SlAAAAAJUC+JwAAAARdlkuAIAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "bc11b5c41de791369fd85fa1ccf01c35c20df5f98ff2f75d02ead61bfd520e21", + index: 0, + sequence: 61, + expected: []EffectOutput{}, + }, + { + + desc: "bumpSequence - new_seq is lower than current sequence", + envelopeXDR: "AAAAAKGX7RT96eIn205uoUHYnqLbt2cPRNORraEoeTAcrRKUAAAAZAAAAEXZZLgCAAAAAAAAAAAAAAABAAAAAAAAAAsAAABF2WS4AQAAAAAAAAABHK0SlAAAAEC4H7TDntOUXDMg4MfoCPlbLRQZH7VwNpUHMvtnRWqWIiY/qnYYu0bvgYUVtoFOOeqElRKLYqtOW3Fz9iKl0WQJ", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAALAAAAAAAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADAAAAPAAAAAAAAAAAoZftFP3p4ifbTm6hQdieotu3Zw9E05GtoSh5MBytEpQAAAACVAvi1AAAAEXZZLgBAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAPAAAAAAAAAAAoZftFP3p4ifbTm6hQdieotu3Zw9E05GtoSh5MBytEpQAAAACVAvi1AAAAEXZZLgCAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA==", + feeChangesXDR: "AAAAAgAAAAMAAAA7AAAAAAAAAAChl+0U/eniJ9tObqFB2J6i27dnD0TTka2hKHkwHK0SlAAAAAJUC+M4AAAARdlkuAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAA8AAAAAAAAAAChl+0U/eniJ9tObqFB2J6i27dnD0TTka2hKHkwHK0SlAAAAAJUC+LUAAAARdlkuAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "c8132b95c0063cafd20b26d27f06c12e688609d2d9d3724b840821e861870b8e", + index: 0, + sequence: 60, + expected: []EffectOutput{}, + }, + { + + desc: "bumpSequence - new_seq is higher than current sequence", + envelopeXDR: "AAAAAKGX7RT96eIn205uoUHYnqLbt2cPRNORraEoeTAcrRKUAAAAZAAAADkAAAABAAAAAAAAAAAAAAABAAAAAAAAAAsAAABF2WS4AAAAAAAAAAABHK0SlAAAAEDq0JVhKNIq9ag0sR+R/cv3d9tEuaYEm2BazIzILRdGj9alaVMZBhxoJ3ZIpP3rraCJzyoKZO+p5HBVe10a2+UG", + resultXDR: "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAALAAAAAAAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADAAAAOgAAAAAAAAAAoZftFP3p4ifbTm6hQdieotu3Zw9E05GtoSh5MBytEpQAAAACVAvjnAAAADkAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAOgAAAAAAAAAAoZftFP3p4ifbTm6hQdieotu3Zw9E05GtoSh5MBytEpQAAAACVAvjnAAAADkAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAgAAAAMAAAA6AAAAAAAAAAChl+0U/eniJ9tObqFB2J6i27dnD0TTka2hKHkwHK0SlAAAAAJUC+OcAAAAOQAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAA6AAAAAAAAAAChl+0U/eniJ9tObqFB2J6i27dnD0TTka2hKHkwHK0SlAAAAAJUC+OcAAAARdlkuAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + feeChangesXDR: "AAAAAgAAAAMAAAA5AAAAAAAAAAChl+0U/eniJ9tObqFB2J6i27dnD0TTka2hKHkwHK0SlAAAAAJUC+QAAAAAOQAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAA6AAAAAAAAAAChl+0U/eniJ9tObqFB2J6i27dnD0TTka2hKHkwHK0SlAAAAAJUC+OcAAAAOQAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + hash: "829d53f2dceebe10af8007564b0aefde819b95734ad431df84270651e7ed8a90", + index: 0, + sequence: 58, + expected: []EffectOutput{ + { + Address: "GCQZP3IU7XU6EJ63JZXKCQOYT2RNXN3HB5CNHENNUEUHSMA4VUJJJSEN", + Type: int32(EffectSequenceBumped), + TypeString: EffectTypeNames[EffectSequenceBumped], + OperationID: int64(249108107265), + Details: map[string]interface{}{ + "new_seq": xdr.SequenceNumber(300000000000), + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 58, + }, + }, + }, + { + desc: "revokeSponsorship (signer)", + envelopeXDR: getRevokeSponsorshipEnvelopeXDR(t), + resultXDR: "AAAAAAAAAAAAAAAAAAAAAAAAAAA=", + metaXDR: revokeSponsorshipMeta, + feeChangesXDR: "AAAAAA==", + hash: "a41d1c8cdf515203ac5a10d945d5023325076b23dbe7d65ae402cd5f8cd9f891", + index: 0, + sequence: 58, + expected: revokeSponsorshipEffects, + }, + { + desc: "Failed transaction", + envelopeXDR: "AAAAAPCq/iehD2ASJorqlTyEt0usn2WG3yF4w9xBkgd4itu6AAAAZAAMpboAADNGAAAAAAAAAAAAAAABAAAAAAAAAAMAAAABVEVTVAAAAAAObS6P1g8rj8sCVzRQzYgHhWFkbh1oV+1s47LFPstSpQAAAAAAAAACVAvkAAAAAfcAAAD6AAAAAAAAAAAAAAAAAAAAAXiK27oAAABAHHk5mvM6xBRsvu3RBvzzPIb8GpXaL2M7InPn65LIhFJ2RnHIYrpP6ufZc6SUtKqChNRaN4qw5rjwFXNezmrBCw==", + resultXDR: "AAAAAAAAAGT/////AAAAAQAAAAAAAAAD////+QAAAAA=", + metaXDR: "AAAAAQAAAAIAAAADABDLGAAAAAAAAAAA8Kr+J6EPYBImiuqVPIS3S6yfZYbfIXjD3EGSB3iK27oAAAB2ucIg2AAMpboAADNFAAAA4wAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAABHT9ws4fAAAAAAAAAAAAAAAAAAAAAAAAAAEAEMsYAAAAAAAAAADwqv4noQ9gEiaK6pU8hLdLrJ9lht8heMPcQZIHeIrbugAAAHa5wiDYAAylugAAM0YAAADjAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAEdP3Czh8AAAAAAAAAAAAAAAAAAAAAAAAAAA==", + feeChangesXDR: "AAAAAgAAAAMAEMsCAAAAAAAAAADwqv4noQ9gEiaK6pU8hLdLrJ9lht8heMPcQZIHeIrbugAAAHa5wiE8AAylugAAM0UAAADjAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAEdP3Czh8AAAAAAAAAAAAAAAAAAAAAAAAAAQAQyxgAAAAAAAAAAPCq/iehD2ASJorqlTyEt0usn2WG3yF4w9xBkgd4itu6AAAAdrnCINgADKW6AAAzRQAAAOMAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAR0/cLOHwAAAAAAAAAAAAAAAAAAAAA=", + hash: "24206737a02f7f855c46e367418e38c223f897792c76bbfb948e1b0dbd695f8b", + index: 0, + sequence: 58, + expected: []EffectOutput{}, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + tt := assert.New(t) + transaction := BuildLedgerTransaction( + t, + TestTransaction{ + Index: 1, + EnvelopeXDR: tc.envelopeXDR, + ResultXDR: tc.resultXDR, + MetaXDR: tc.metaXDR, + FeeChangesXDR: tc.feeChangesXDR, + Hash: tc.hash, + }, + ) + + operation := transactionOperationWrapper{ + index: tc.index, + transaction: transaction, + operation: transaction.Envelope.Operations()[tc.index], + ledgerSequence: tc.sequence, + ledgerClosed: LedgerClosed, + } + for i := range tc.expected { + tc.expected[i].EffectIndex = uint32(i) + tc.expected[i].EffectId = fmt.Sprintf("%d-%d", tc.expected[i].OperationID, tc.expected[i].EffectIndex) + } + + effects, err := operation.effects() + tt.NoError(err) + tt.Equal(tc.expected, effects) + }) + } +} + +func TestOperationEffectsSetOptionsSignersOrder(t *testing.T) { + tt := assert.New(t) + transaction := ingest.LedgerTransaction{ + UnsafeMeta: createTransactionMeta([]xdr.OperationMeta{ + { + Changes: []xdr.LedgerEntryChange{ + // State + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner("GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV"), + Weight: 10, + }, + { + Key: xdr.MustSigner("GCAHY6JSXQFKWKP6R7U5JPXDVNV4DJWOWRFLY3Y6YPBF64QRL4BPFDNS"), + Weight: 10, + }, + }, + }, + }, + }, + }, + // Updated + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner("GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV"), + Weight: 16, + }, + { + Key: xdr.MustSigner("GCAHY6JSXQFKWKP6R7U5JPXDVNV4DJWOWRFLY3Y6YPBF64QRL4BPFDNS"), + Weight: 15, + }, + { + Key: xdr.MustSigner("GCR3TQ2TVH3QRI7GQMC3IJGUUBR32YQHWBIKIMTYRQ2YH4XUTDB75UKE"), + Weight: 14, + }, + { + Key: xdr.MustSigner("GA4O5DLUUTLCTMM2UOWOYPNIH2FTD4NLO6KDZOFQRUISQ3FYKABGJLPC"), + Weight: 17, + }, + }, + }, + }, + }, + }, + }, + }, + }), + } + transaction.Index = 1 + transaction.Envelope.Type = xdr.EnvelopeTypeEnvelopeTypeTx + aid := xdr.MustAddress("GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV") + transaction.Envelope.V1 = &xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: aid.ToMuxedAccount(), + }, + } + + operation := transactionOperationWrapper{ + index: 0, + transaction: transaction, + operation: xdr.Operation{ + Body: xdr.OperationBody{ + Type: xdr.OperationTypeSetOptions, + SetOptionsOp: &xdr.SetOptionsOp{}, + }, + }, + ledgerSequence: 46, + ledgerClosed: genericCloseTime.UTC(), + } + + effects, err := operation.effects() + tt.NoError(err) + expected := []EffectOutput{ + { + Address: "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + OperationID: int64(197568499713), + Details: map[string]interface{}{ + "public_key": "GCAHY6JSXQFKWKP6R7U5JPXDVNV4DJWOWRFLY3Y6YPBF64QRL4BPFDNS", + "weight": int32(15), + }, + Type: int32(EffectSignerUpdated), + TypeString: EffectTypeNames[EffectSignerUpdated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 46, + }, + { + Address: "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + OperationID: int64(197568499713), + Details: map[string]interface{}{ + "public_key": "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + "weight": int32(16), + }, + Type: int32(EffectSignerUpdated), + TypeString: EffectTypeNames[EffectSignerUpdated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 46, + }, + { + Address: "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + OperationID: int64(197568499713), + Details: map[string]interface{}{ + "public_key": "GA4O5DLUUTLCTMM2UOWOYPNIH2FTD4NLO6KDZOFQRUISQ3FYKABGJLPC", + "weight": int32(17), + }, + Type: int32(EffectSignerCreated), + TypeString: EffectTypeNames[EffectSignerCreated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 46, + }, + { + Address: "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + OperationID: int64(197568499713), + Details: map[string]interface{}{ + "public_key": "GCR3TQ2TVH3QRI7GQMC3IJGUUBR32YQHWBIKIMTYRQ2YH4XUTDB75UKE", + "weight": int32(14), + }, + Type: int32(EffectSignerCreated), + TypeString: EffectTypeNames[EffectSignerCreated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 46, + }, + } + for i := range expected { + expected[i].EffectIndex = uint32(i) + expected[i].EffectId = fmt.Sprintf("%d-%d", expected[i].OperationID, expected[i].EffectIndex) + } + + tt.Equal(expected, effects) +} + +func TestOperationEffectsSetOptionsSignersNoUpdated(t *testing.T) { + tt := assert.New(t) + transaction := ingest.LedgerTransaction{ + UnsafeMeta: createTransactionMeta([]xdr.OperationMeta{ + { + Changes: []xdr.LedgerEntryChange{ + // State + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner("GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV"), + Weight: 10, + }, + { + Key: xdr.MustSigner("GCAHY6JSXQFKWKP6R7U5JPXDVNV4DJWOWRFLY3Y6YPBF64QRL4BPFDNS"), + Weight: 10, + }, + { + Key: xdr.MustSigner("GA4O5DLUUTLCTMM2UOWOYPNIH2FTD4NLO6KDZOFQRUISQ3FYKABGJLPC"), + Weight: 17, + }, + }, + }, + }, + }, + }, + // Updated + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner("GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV"), + Weight: 16, + }, + { + Key: xdr.MustSigner("GCAHY6JSXQFKWKP6R7U5JPXDVNV4DJWOWRFLY3Y6YPBF64QRL4BPFDNS"), + Weight: 10, + }, + { + Key: xdr.MustSigner("GCR3TQ2TVH3QRI7GQMC3IJGUUBR32YQHWBIKIMTYRQ2YH4XUTDB75UKE"), + Weight: 14, + }, + }, + }, + }, + }, + }, + }, + }, + }), + } + transaction.Index = 1 + transaction.Envelope.Type = xdr.EnvelopeTypeEnvelopeTypeTx + aid := xdr.MustAddress("GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV") + transaction.Envelope.V1 = &xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: aid.ToMuxedAccount(), + }, + } + + operation := transactionOperationWrapper{ + index: 0, + transaction: transaction, + operation: xdr.Operation{ + Body: xdr.OperationBody{ + Type: xdr.OperationTypeSetOptions, + SetOptionsOp: &xdr.SetOptionsOp{}, + }, + }, + ledgerSequence: 46, + ledgerClosed: genericCloseTime.UTC(), + } + + effects, err := operation.effects() + tt.NoError(err) + expected := []EffectOutput{ + { + Address: "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + OperationID: int64(197568499713), + Details: map[string]interface{}{ + "public_key": "GA4O5DLUUTLCTMM2UOWOYPNIH2FTD4NLO6KDZOFQRUISQ3FYKABGJLPC", + }, + Type: int32(EffectSignerRemoved), + TypeString: EffectTypeNames[EffectSignerRemoved], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 46, + }, + { + Address: "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + OperationID: int64(197568499713), + Details: map[string]interface{}{ + "public_key": "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + "weight": int32(16), + }, + Type: int32(EffectSignerUpdated), + TypeString: EffectTypeNames[EffectSignerUpdated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 46, + }, + { + Address: "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + OperationID: int64(197568499713), + Details: map[string]interface{}{ + "public_key": "GCR3TQ2TVH3QRI7GQMC3IJGUUBR32YQHWBIKIMTYRQ2YH4XUTDB75UKE", + "weight": int32(14), + }, + Type: int32(EffectSignerCreated), + TypeString: EffectTypeNames[EffectSignerCreated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 46, + }, + } + for i := range expected { + expected[i].EffectIndex = uint32(i) + expected[i].EffectId = fmt.Sprintf("%d-%d", expected[i].OperationID, expected[i].EffectIndex) + } + + tt.Equal(expected, effects) +} + +func TestOperationRegressionAccountTrustItself(t *testing.T) { + tt := assert.New(t) + // NOTE: when an account trusts itself, the transaction is successful but + // no ledger entries are actually modified. + transaction := ingest.LedgerTransaction{ + UnsafeMeta: createTransactionMeta([]xdr.OperationMeta{}), + } + transaction.Index = 1 + transaction.Envelope.Type = xdr.EnvelopeTypeEnvelopeTypeTx + aid := xdr.MustAddress("GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV") + transaction.Envelope.V1 = &xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: aid.ToMuxedAccount(), + }, + } + operation := transactionOperationWrapper{ + index: 0, + transaction: transaction, + operation: xdr.Operation{ + Body: xdr.OperationBody{ + Type: xdr.OperationTypeChangeTrust, + ChangeTrustOp: &xdr.ChangeTrustOp{ + Line: xdr.MustNewCreditAsset("COP", "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV").ToChangeTrustAsset(), + Limit: xdr.Int64(1000), + }, + }, + }, + ledgerSequence: 46, + } + + effects, err := operation.effects() + tt.NoError(err) + tt.Equal([]EffectOutput{}, effects) +} + +func TestOperationEffectsAllowTrustAuthorizedToMaintainLiabilities(t *testing.T) { + tt := assert.New(t) + asset := xdr.Asset{} + allowTrustAsset, err := asset.ToAssetCode("COP") + tt.NoError(err) + aid := xdr.MustAddress("GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD") + source := aid.ToMuxedAccount() + op := xdr.Operation{ + SourceAccount: &source, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeAllowTrust, + AllowTrustOp: &xdr.AllowTrustOp{ + Trustor: xdr.MustAddress("GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3"), + Asset: allowTrustAsset, + Authorize: xdr.Uint32(xdr.TrustLineFlagsAuthorizedToMaintainLiabilitiesFlag), + }, + }, + } + + operation := transactionOperationWrapper{ + index: 0, + transaction: ingest.LedgerTransaction{ + UnsafeMeta: xdr.TransactionMeta{ + V: 2, + V2: &xdr.TransactionMetaV2{}, + }, + }, + operation: op, + ledgerSequence: 1, + ledgerClosed: genericCloseTime.UTC(), + } + + effects, err := operation.effects() + tt.NoError(err) + + expected := []EffectOutput{ + { + Address: "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD", + OperationID: 4294967297, + Details: map[string]interface{}{ + "asset_code": "COP", + "asset_issuer": "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD", + "asset_type": "credit_alphanum4", + "trustor": "GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3", + }, + Type: int32(EffectTrustlineFlagsUpdated), + TypeString: EffectTypeNames[EffectTrustlineFlagsUpdated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + { + Address: "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD", + OperationID: int64(4294967297), + Details: map[string]interface{}{ + "asset_code": "COP", + "asset_issuer": "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD", + "asset_type": "credit_alphanum4", + "authorized_to_maintain_liabilites": true, + "trustor": "GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3", + }, + Type: int32(EffectTrustlineFlagsUpdated), + TypeString: EffectTypeNames[EffectTrustlineFlagsUpdated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + } + for i := range expected { + expected[i].EffectIndex = uint32(i) + expected[i].EffectId = fmt.Sprintf("%d-%d", expected[i].OperationID, expected[i].EffectIndex) + } + + tt.Equal(expected, effects) +} + +func TestOperationEffectsClawback(t *testing.T) { + tt := assert.New(t) + aid := xdr.MustAddress("GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD") + source := aid.ToMuxedAccount() + op := xdr.Operation{ + SourceAccount: &source, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeClawback, + ClawbackOp: &xdr.ClawbackOp{ + Asset: xdr.MustNewCreditAsset("COP", "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD"), + From: xdr.MustMuxedAddress("GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3"), + Amount: 34, + }, + }, + } + + operation := transactionOperationWrapper{ + index: 0, + transaction: ingest.LedgerTransaction{ + UnsafeMeta: xdr.TransactionMeta{ + V: 2, + V2: &xdr.TransactionMetaV2{}, + }, + }, + operation: op, + ledgerSequence: 1, + ledgerClosed: genericCloseTime.UTC(), + } + + effects, err := operation.effects() + tt.NoError(err) + + expected := []EffectOutput{ + { + Address: "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD", + OperationID: 4294967297, + Details: map[string]interface{}{ + "asset_code": "COP", + "asset_issuer": "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD", + "asset_type": "credit_alphanum4", + "amount": "0.0000034", + }, + Type: int32(EffectAccountCredited), + TypeString: EffectTypeNames[EffectAccountCredited], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + { + Address: "GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3", + OperationID: 4294967297, + Details: map[string]interface{}{ + "asset_code": "COP", + "asset_issuer": "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD", + "asset_type": "credit_alphanum4", + "amount": "0.0000034", + }, + Type: int32(EffectAccountDebited), + TypeString: EffectTypeNames[EffectAccountDebited], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + } + for i := range expected { + expected[i].EffectIndex = uint32(i) + expected[i].EffectId = fmt.Sprintf("%d-%d", expected[i].OperationID, expected[i].EffectIndex) + } + + tt.Equal(expected, effects) +} + +func TestOperationEffectsClawbackClaimableBalance(t *testing.T) { + tt := assert.New(t) + aid := xdr.MustAddress("GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD") + source := aid.ToMuxedAccount() + var balanceID xdr.ClaimableBalanceId + xdr.SafeUnmarshalBase64("AAAAANoNV9p9SFDn/BDSqdDrxzH3r7QFdMAzlbF9SRSbkfW+", &balanceID) + op := xdr.Operation{ + SourceAccount: &source, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeClawbackClaimableBalance, + ClawbackClaimableBalanceOp: &xdr.ClawbackClaimableBalanceOp{ + BalanceId: balanceID, + }, + }, + } + + operation := transactionOperationWrapper{ + index: 0, + transaction: ingest.LedgerTransaction{ + UnsafeMeta: xdr.TransactionMeta{ + V: 2, + V2: &xdr.TransactionMetaV2{}, + }, + }, + operation: op, + ledgerSequence: 1, + ledgerClosed: genericCloseTime.UTC(), + } + + effects, err := operation.effects() + tt.NoError(err) + + expected := []EffectOutput{ + { + Address: "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD", + OperationID: 4294967297, + Details: map[string]interface{}{ + "balance_id": "00000000da0d57da7d4850e7fc10d2a9d0ebc731f7afb40574c03395b17d49149b91f5be", + }, + Type: int32(EffectClaimableBalanceClawedBack), + TypeString: EffectTypeNames[EffectClaimableBalanceClawedBack], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + } + for i := range expected { + expected[i].EffectIndex = uint32(i) + expected[i].EffectId = fmt.Sprintf("%d-%d", expected[i].OperationID, expected[i].EffectIndex) + } + + tt.Equal(expected, effects) +} + +func TestOperationEffectsSetTrustLineFlags(t *testing.T) { + tt := assert.New(t) + aid := xdr.MustAddress("GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD") + source := aid.ToMuxedAccount() + trustor := xdr.MustAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY") + setFlags := xdr.Uint32(xdr.TrustLineFlagsAuthorizedToMaintainLiabilitiesFlag) + clearFlags := xdr.Uint32(xdr.TrustLineFlagsTrustlineClawbackEnabledFlag | xdr.TrustLineFlagsAuthorizedFlag) + op := xdr.Operation{ + SourceAccount: &source, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeSetTrustLineFlags, + SetTrustLineFlagsOp: &xdr.SetTrustLineFlagsOp{ + Trustor: trustor, + Asset: xdr.MustNewCreditAsset("USD", "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD"), + ClearFlags: clearFlags, + SetFlags: setFlags, + }, + }, + } + + operation := transactionOperationWrapper{ + index: 0, + transaction: ingest.LedgerTransaction{ + UnsafeMeta: xdr.TransactionMeta{ + V: 2, + V2: &xdr.TransactionMetaV2{}, + }, + }, + operation: op, + ledgerSequence: 1, + ledgerClosed: genericCloseTime.UTC(), + } + + effects, err := operation.effects() + tt.NoError(err) + + expected := []EffectOutput{ + { + Address: "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD", + OperationID: 4294967297, + Details: map[string]interface{}{ + "asset_code": "USD", + "asset_issuer": "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD", + "asset_type": "credit_alphanum4", + "authorized_flag": false, + "authorized_to_maintain_liabilites": true, + "clawback_enabled_flag": false, + "trustor": "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + }, + Type: int32(EffectTrustlineFlagsUpdated), + TypeString: EffectTypeNames[EffectTrustlineFlagsUpdated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + } + for i := range expected { + expected[i].EffectIndex = uint32(i) + expected[i].EffectId = fmt.Sprintf("%d-%d", expected[i].OperationID, expected[i].EffectIndex) + } + + tt.Equal(expected, effects) +} + +func TestCreateClaimableBalanceEffectsTestSuite(t *testing.T) { + suite.Run(t, new(CreateClaimableBalanceEffectsTestSuite)) +} + +func TestClaimClaimableBalanceEffectsTestSuite(t *testing.T) { + suite.Run(t, new(ClaimClaimableBalanceEffectsTestSuite)) +} + +func TestTrustlineSponsorhipEffects(t *testing.T) { + source := xdr.MustMuxedAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY") + usdAsset := xdr.MustNewCreditAsset("USD", "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY") + poolIDStr := "19cc788419412926a11049b9fb1f87906b8f02bc6bf8f73d8fd347ede0b79fa5" + var poolID xdr.PoolId + poolIDBytes, err := hex.DecodeString(poolIDStr) + assert.NoError(t, err) + copy(poolID[:], poolIDBytes) + baseAssetTrustLineEntry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: 20, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.TrustLineEntry{ + AccountId: source.ToAccountId(), + Asset: usdAsset.ToTrustLineAsset(), + Balance: 100, + Limit: 1000, + Flags: 0, + }, + }, + } + baseLiquidityPoolTrustLineEntry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: 20, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.TrustLineEntry{ + AccountId: source.ToAccountId(), + Asset: xdr.TrustLineAsset{ + Type: xdr.AssetTypeAssetTypePoolShare, + LiquidityPoolId: &poolID, + }, + Balance: 100, + Limit: 1000, + Flags: 0, + }, + }, + } + + sponsor1 := xdr.MustAddress("GDMQUXK7ZUCWM5472ZU3YLDP4BMJLQQ76DEMNYDEY2ODEEGGRKLEWGW2") + sponsor2 := xdr.MustAddress("GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD") + withSponsor := func(le *xdr.LedgerEntry, accID *xdr.AccountId) *xdr.LedgerEntry { + le2 := *le + le2.Ext = xdr.LedgerEntryExt{ + V: 1, + V1: &xdr.LedgerEntryExtensionV1{ + SponsoringId: accID, + }, + } + return &le2 + } + + changes := xdr.LedgerEntryChanges{ + // create asset sponsorship + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &baseAssetTrustLineEntry, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: withSponsor(&baseAssetTrustLineEntry, &sponsor1), + }, + // update asset sponsorship + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: withSponsor(&baseAssetTrustLineEntry, &sponsor1), + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: withSponsor(&baseAssetTrustLineEntry, &sponsor2), + }, + // remove asset sponsorship + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: withSponsor(&baseAssetTrustLineEntry, &sponsor2), + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &baseAssetTrustLineEntry, + }, + + // create liquidity pool sponsorship + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &baseLiquidityPoolTrustLineEntry, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: withSponsor(&baseLiquidityPoolTrustLineEntry, &sponsor1), + }, + // update liquidity pool sponsorship + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: withSponsor(&baseLiquidityPoolTrustLineEntry, &sponsor1), + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: withSponsor(&baseLiquidityPoolTrustLineEntry, &sponsor2), + }, + // remove liquidity pool sponsorship + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: withSponsor(&baseLiquidityPoolTrustLineEntry, &sponsor2), + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &baseLiquidityPoolTrustLineEntry, + }, + } + expected := []EffectOutput{ + { + Type: int32(EffectTrustlineSponsorshipCreated), + TypeString: EffectTypeNames[EffectTrustlineSponsorshipCreated], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "asset": "USD:GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + // `asset_type` set in `Effect.UnmarshalDetails` to prevent reingestion + "sponsor": "GDMQUXK7ZUCWM5472ZU3YLDP4BMJLQQ76DEMNYDEY2ODEEGGRKLEWGW2", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + { + Type: int32(EffectTrustlineSponsorshipUpdated), + TypeString: EffectTypeNames[EffectTrustlineSponsorshipUpdated], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "asset": "USD:GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + // `asset_type` set in `Effect.UnmarshalDetails` to prevent reingestion + "former_sponsor": "GDMQUXK7ZUCWM5472ZU3YLDP4BMJLQQ76DEMNYDEY2ODEEGGRKLEWGW2", + "new_sponsor": "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + { + Type: int32(EffectTrustlineSponsorshipRemoved), + TypeString: EffectTypeNames[EffectTrustlineSponsorshipRemoved], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "asset": "USD:GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + // `asset_type` set in `Effect.UnmarshalDetails` to prevent reingestion + "former_sponsor": "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + { + Type: int32(EffectTrustlineSponsorshipCreated), + TypeString: EffectTypeNames[EffectTrustlineSponsorshipCreated], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "liquidity_pool_id": poolIDStr, + "asset_type": "liquidity_pool", + "sponsor": "GDMQUXK7ZUCWM5472ZU3YLDP4BMJLQQ76DEMNYDEY2ODEEGGRKLEWGW2", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + { + Type: int32(EffectTrustlineSponsorshipUpdated), + TypeString: EffectTypeNames[EffectTrustlineSponsorshipUpdated], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "liquidity_pool_id": poolIDStr, + "asset_type": "liquidity_pool", + "former_sponsor": "GDMQUXK7ZUCWM5472ZU3YLDP4BMJLQQ76DEMNYDEY2ODEEGGRKLEWGW2", + "new_sponsor": "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + { + Type: int32(EffectTrustlineSponsorshipRemoved), + TypeString: EffectTypeNames[EffectTrustlineSponsorshipRemoved], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "liquidity_pool_id": poolIDStr, + "asset_type": "liquidity_pool", + "former_sponsor": "GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + } + for i := range expected { + expected[i].EffectIndex = uint32(i) + expected[i].EffectId = fmt.Sprintf("%d-%d", expected[i].OperationID, expected[i].EffectIndex) + } + + // pick an operation with no intrinsic effects + // (the sponsosrhip effects are obtained from the changes, so it doesn't matter) + phonyOp := xdr.Operation{ + Body: xdr.OperationBody{ + Type: xdr.OperationTypeEndSponsoringFutureReserves, + }, + } + tx := ingest.LedgerTransaction{ + Index: 0, + Envelope: xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: source, + Operations: []xdr.Operation{phonyOp}, + }, + }, + }, + UnsafeMeta: xdr.TransactionMeta{ + V: 2, + V2: &xdr.TransactionMetaV2{ + Operations: []xdr.OperationMeta{{Changes: changes}}, + }, + }, + } + + operation := transactionOperationWrapper{ + index: 0, + transaction: tx, + operation: phonyOp, + ledgerSequence: 1, + ledgerClosed: genericCloseTime.UTC(), + } + + effects, err := operation.effects() + assert.NoError(t, err) + assert.Equal(t, expected, effects) + +} + +func TestLiquidityPoolEffects(t *testing.T) { + source := xdr.MustMuxedAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY") + usdAsset := xdr.MustNewCreditAsset("USD", "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY") + poolIDStr := "ea4e3e63a95fd840c1394f195722ffdcb2d0d4f0a26589c6ab557d81e6b0bf9d" + var poolID xdr.PoolId + poolIDBytes, err := hex.DecodeString(poolIDStr) + assert.NoError(t, err) + copy(poolID[:], poolIDBytes) + baseLiquidityPoolEntry := xdr.LiquidityPoolEntry{ + LiquidityPoolId: poolID, + Body: xdr.LiquidityPoolEntryBody{ + Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, + ConstantProduct: &xdr.LiquidityPoolEntryConstantProduct{ + Params: xdr.LiquidityPoolConstantProductParameters{ + AssetA: xdr.MustNewNativeAsset(), + AssetB: usdAsset, + Fee: 20, + }, + ReserveA: 200, + ReserveB: 100, + TotalPoolShares: 1000, + PoolSharesTrustLineCount: 10, + }, + }, + } + baseState := xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 20, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeLiquidityPool, + LiquidityPool: &baseLiquidityPoolEntry, + }, + }, + } + updateState := func(cp xdr.LiquidityPoolEntryConstantProduct) xdr.LedgerEntryChange { + return xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 20, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeLiquidityPool, + LiquidityPool: &xdr.LiquidityPoolEntry{ + LiquidityPoolId: poolID, + Body: xdr.LiquidityPoolEntryBody{ + Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, + ConstantProduct: &cp, + }, + }, + }, + }, + } + } + + testCases := []struct { + desc string + op xdr.OperationBody + result xdr.OperationResult + changes xdr.LedgerEntryChanges + expected []EffectOutput + }{ + { + desc: "liquidity pool creation", + op: xdr.OperationBody{ + Type: xdr.OperationTypeChangeTrust, + ChangeTrustOp: &xdr.ChangeTrustOp{ + Line: xdr.ChangeTrustAsset{ + Type: xdr.AssetTypeAssetTypePoolShare, + LiquidityPool: &xdr.LiquidityPoolParameters{ + Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, + ConstantProduct: &baseLiquidityPoolEntry.Body.ConstantProduct.Params, + }, + }, + Limit: 1000, + }, + }, + changes: xdr.LedgerEntryChanges{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 20, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeLiquidityPool, + LiquidityPool: &baseLiquidityPoolEntry, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 20, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.TrustLineEntry{ + AccountId: source.ToAccountId(), + Asset: xdr.TrustLineAsset{ + Type: xdr.AssetTypeAssetTypePoolShare, + LiquidityPoolId: &poolID, + }, + }, + }, + }, + }, + }, + expected: []EffectOutput{ + { + Type: int32(EffectTrustlineCreated), + TypeString: EffectTypeNames[EffectTrustlineCreated], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "asset_type": "liquidity_pool_shares", + "limit": "0.0001000", + "liquidity_pool_id": poolIDStr, + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + { + Type: int32(EffectLiquidityPoolCreated), + TypeString: EffectTypeNames[EffectLiquidityPoolCreated], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "liquidity_pool": map[string]interface{}{ + "fee_bp": uint32(20), + "id": poolIDStr, + "reserves": []base.AssetAmount{ + { + Asset: "native", + Amount: "0.0000200", + }, + { + Asset: "USD:GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + Amount: "0.0000100", + }, + }, + "total_shares": "0.0001000", + "total_trustlines": "10", + "type": "constant_product", + }, + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + }, + }, + { + desc: "liquidity pool deposit", + op: xdr.OperationBody{ + Type: xdr.OperationTypeLiquidityPoolDeposit, + LiquidityPoolDepositOp: &xdr.LiquidityPoolDepositOp{ + LiquidityPoolId: poolID, + MaxAmountA: 100, + MaxAmountB: 200, + MinPrice: xdr.Price{ + N: 50, + D: 3, + }, + MaxPrice: xdr.Price{ + N: 100, + D: 2, + }, + }, + }, + changes: xdr.LedgerEntryChanges{ + baseState, + updateState(xdr.LiquidityPoolEntryConstantProduct{ + + Params: baseLiquidityPoolEntry.Body.ConstantProduct.Params, + ReserveA: baseLiquidityPoolEntry.Body.ConstantProduct.ReserveA + 50, + ReserveB: baseLiquidityPoolEntry.Body.ConstantProduct.ReserveB + 60, + TotalPoolShares: baseLiquidityPoolEntry.Body.ConstantProduct.TotalPoolShares + 10, + PoolSharesTrustLineCount: baseLiquidityPoolEntry.Body.ConstantProduct.PoolSharesTrustLineCount, + }), + }, + expected: []EffectOutput{ + { + Type: int32(EffectLiquidityPoolDeposited), + TypeString: EffectTypeNames[EffectLiquidityPoolDeposited], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "liquidity_pool": map[string]interface{}{ + "fee_bp": uint32(20), + "id": poolIDStr, + "reserves": []base.AssetAmount{ + { + Asset: "native", + Amount: "0.0000250", + }, + { + Asset: "USD:GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + Amount: "0.0000160", + }, + }, + "total_shares": "0.0001010", + "total_trustlines": "10", + "type": "constant_product", + }, + "reserves_deposited": []base.AssetAmount{ + { + Asset: "native", + Amount: "0.0000050", + }, + { + Asset: "USD:GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + Amount: "0.0000060", + }, + }, + "shares_received": "0.0000010", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + }, + }, + { + desc: "liquidity pool withdrawal", + op: xdr.OperationBody{ + Type: xdr.OperationTypeLiquidityPoolWithdraw, + LiquidityPoolWithdrawOp: &xdr.LiquidityPoolWithdrawOp{ + LiquidityPoolId: poolID, + Amount: 10, + MinAmountA: 10, + MinAmountB: 5, + }, + }, + changes: xdr.LedgerEntryChanges{ + baseState, + updateState(xdr.LiquidityPoolEntryConstantProduct{ + + Params: baseLiquidityPoolEntry.Body.ConstantProduct.Params, + ReserveA: baseLiquidityPoolEntry.Body.ConstantProduct.ReserveA - 11, + ReserveB: baseLiquidityPoolEntry.Body.ConstantProduct.ReserveB - 6, + TotalPoolShares: baseLiquidityPoolEntry.Body.ConstantProduct.TotalPoolShares - 10, + PoolSharesTrustLineCount: baseLiquidityPoolEntry.Body.ConstantProduct.PoolSharesTrustLineCount, + }), + }, + expected: []EffectOutput{ + { + Type: int32(EffectLiquidityPoolWithdrew), + TypeString: EffectTypeNames[EffectLiquidityPoolWithdrew], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "liquidity_pool": map[string]interface{}{ + "fee_bp": uint32(20), + "id": poolIDStr, + "reserves": []base.AssetAmount{ + { + Asset: "native", + Amount: "0.0000189", + }, + { + Asset: "USD:GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + Amount: "0.0000094", + }, + }, + "total_shares": "0.0000990", + "total_trustlines": "10", + "type": "constant_product", + }, + "reserves_received": []base.AssetAmount{ + { + Asset: "native", + Amount: "0.0000011", + }, + { + Asset: "USD:GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + Amount: "0.0000006", + }, + }, + "shares_redeemed": "0.0000010", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + }, + }, + { + desc: "liquidity pool trade", + op: xdr.OperationBody{ + Type: xdr.OperationTypePathPaymentStrictSend, + PathPaymentStrictSendOp: &xdr.PathPaymentStrictSendOp{ + SendAsset: xdr.MustNewNativeAsset(), + SendAmount: 10, + Destination: xdr.MustMuxedAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY"), + DestAsset: usdAsset, + DestMin: 5, + Path: nil, + }, + }, + changes: xdr.LedgerEntryChanges{ + baseState, + updateState(xdr.LiquidityPoolEntryConstantProduct{ + + Params: baseLiquidityPoolEntry.Body.ConstantProduct.Params, + ReserveA: baseLiquidityPoolEntry.Body.ConstantProduct.ReserveA - 11, + ReserveB: baseLiquidityPoolEntry.Body.ConstantProduct.ReserveB - 6, + TotalPoolShares: baseLiquidityPoolEntry.Body.ConstantProduct.TotalPoolShares - 10, + PoolSharesTrustLineCount: baseLiquidityPoolEntry.Body.ConstantProduct.PoolSharesTrustLineCount, + }), + }, + result: xdr.OperationResult{ + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypePathPaymentStrictSend, + PathPaymentStrictSendResult: &xdr.PathPaymentStrictSendResult{ + Code: xdr.PathPaymentStrictSendResultCodePathPaymentStrictSendSuccess, + Success: &xdr.PathPaymentStrictSendResultSuccess{ + Last: xdr.SimplePaymentResult{ + Destination: xdr.MustAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY"), + Asset: xdr.MustNewCreditAsset("USD", "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY"), + Amount: 5, + }, + Offers: []xdr.ClaimAtom{ + { + Type: xdr.ClaimAtomTypeClaimAtomTypeLiquidityPool, + LiquidityPool: &xdr.ClaimLiquidityAtom{ + LiquidityPoolId: poolID, + AssetSold: xdr.MustNewNativeAsset(), + AmountSold: 10, + AssetBought: xdr.MustNewCreditAsset("USD", "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY"), + AmountBought: 5, + }, + }, + }, + }, + }, + }, + }, + expected: []EffectOutput{ + { + Type: int32(EffectAccountCredited), + TypeString: EffectTypeNames[EffectAccountCredited], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "amount": "0.0000005", + "asset_code": "USD", + "asset_issuer": "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + "asset_type": "credit_alphanum4", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + { + Type: int32(EffectAccountDebited), + TypeString: EffectTypeNames[EffectAccountDebited], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "amount": "0.0000010", + "asset_type": "native", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + { + Type: int32(EffectLiquidityPoolTrade), + TypeString: EffectTypeNames[EffectLiquidityPoolTrade], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "bought": map[string]string{ + "amount": "0.0000005", + "asset": "USD:GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + }, + "liquidity_pool": map[string]interface{}{ + "fee_bp": uint32(20), + "id": poolIDStr, + "reserves": []base.AssetAmount{ + { + Asset: "native", + Amount: "0.0000189", + }, + { + Asset: "USD:GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + Amount: "0.0000094", + }, + }, + "total_shares": "0.0000990", + "total_trustlines": "10", + "type": "constant_product", + }, + "sold": map[string]string{ + "amount": "0.0000010", + "asset": "native", + }, + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + }, + }, + { + desc: "liquidity pool revocation", + // Deauthorize an asset + // + // This scenario assumes that the asset being deauthorized is also part of a liquidity pool trustline + // from the same account. This results in a revocation (with a claimable balance being created). + // + // This scenario also assumes that the liquidity pool trustline was the last one, cause a liquidity pool removal. + op: xdr.OperationBody{ + Type: xdr.OperationTypeSetTrustLineFlags, + SetTrustLineFlagsOp: &xdr.SetTrustLineFlagsOp{ + Trustor: xdr.MustAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY"), + Asset: usdAsset, + ClearFlags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + }, + }, + changes: xdr.LedgerEntryChanges{ + // Asset trustline update + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 20, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY"), + Asset: usdAsset.ToTrustLineAsset(), + Balance: 5, + Limit: 100, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + }, + }, + }, + }, + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 20, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY"), + Asset: usdAsset.ToTrustLineAsset(), + Balance: 5, + Limit: 100, + Flags: 0, + }, + }, + }, + }, + // Liquidity pool trustline removal + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 20, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY"), + Asset: xdr.TrustLineAsset{ + Type: xdr.AssetTypeAssetTypePoolShare, + LiquidityPoolId: &poolID, + }, + Balance: 1000, + Limit: 2000, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + }, + }, + }, + }, + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryRemoved, + Removed: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.LedgerKeyTrustLine{ + AccountId: xdr.MustAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY"), + Asset: xdr.TrustLineAsset{ + Type: xdr.AssetTypeAssetTypePoolShare, + LiquidityPoolId: &poolID, + }, + }, + }, + }, + // create claimable balance for USD asset as part of the revocation (in reality there would probably be another claimable + // balance crested for the native asset, but let's keep this simple) + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, + Created: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 20, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeClaimableBalance, + ClaimableBalance: &xdr.ClaimableBalanceEntry{ + BalanceId: xdr.ClaimableBalanceId{ + Type: xdr.ClaimableBalanceIdTypeClaimableBalanceIdTypeV0, + V0: &xdr.Hash{0xa, 0xb}, + }, + Claimants: []xdr.Claimant{ + { + Type: xdr.ClaimantTypeClaimantTypeV0, + V0: &xdr.ClaimantV0{ + Destination: xdr.MustAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY"), + Predicate: xdr.ClaimPredicate{ + Type: xdr.ClaimPredicateTypeClaimPredicateUnconditional, + }, + }, + }, + }, + Asset: usdAsset, + Amount: 100, + }, + }, + }, + }, + // Liquidity pool removal + baseState, + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryRemoved, + Removed: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeLiquidityPool, + LiquidityPool: &xdr.LedgerKeyLiquidityPool{ + LiquidityPoolId: poolID, + }, + }, + }, + }, + expected: []EffectOutput{ + { + Type: int32(EffectTrustlineFlagsUpdated), + TypeString: EffectTypeNames[EffectTrustlineFlagsUpdated], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "asset_code": "USD", + "asset_issuer": "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + "asset_type": "credit_alphanum4", + "authorized_flag": false, + "trustor": "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + { + Type: int32(EffectClaimableBalanceCreated), + TypeString: EffectTypeNames[EffectClaimableBalanceCreated], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "amount": "0.0000100", + "asset": "USD:GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + "balance_id": "000000000a0b000000000000000000000000000000000000000000000000000000000000", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + { + Type: int32(EffectClaimableBalanceClaimantCreated), + TypeString: EffectTypeNames[EffectClaimableBalanceClaimantCreated], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "amount": "0.0000100", + "asset": "USD:GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + "balance_id": "000000000a0b000000000000000000000000000000000000000000000000000000000000", + "predicate": xdr.ClaimPredicate{}, + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + { + Type: int32(EffectLiquidityPoolRevoked), + TypeString: EffectTypeNames[EffectLiquidityPoolRevoked], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "liquidity_pool": map[string]interface{}{ + "fee_bp": uint32(20), + "id": poolIDStr, + "reserves": []base.AssetAmount{ + { + Asset: "native", + Amount: "0.0000200", + }, + { + Asset: "USD:GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + Amount: "0.0000100", + }, + }, + "total_shares": "0.0001000", + "total_trustlines": "10", + "type": "constant_product", + }, + "reserves_revoked": []map[string]string{ + { + "amount": "0.0000100", + "asset": "USD:GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + "claimable_balance_id": "000000000a0b000000000000000000000000000000000000000000000000000000000000", + }, + }, + "shares_revoked": "0.0001000", + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + { + Type: int32(EffectLiquidityPoolRemoved), + TypeString: EffectTypeNames[EffectLiquidityPoolRemoved], + Address: "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY", + OperationID: 4294967297, + Details: map[string]interface{}{ + "liquidity_pool_id": poolIDStr, + }, + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 1, + }, + }, + }, + } + for _, tc := range testCases { + + op := xdr.Operation{Body: tc.op} + tx := ingest.LedgerTransaction{ + Index: 0, + Envelope: xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: source, + Operations: []xdr.Operation{op}, + }, + }, + }, + Result: xdr.TransactionResultPair{ + Result: xdr.TransactionResult{ + Result: xdr.TransactionResultResult{ + Results: &[]xdr.OperationResult{ + tc.result, + }, + }, + }, + }, + UnsafeMeta: xdr.TransactionMeta{ + V: 2, + V2: &xdr.TransactionMetaV2{ + Operations: []xdr.OperationMeta{{Changes: tc.changes}}, + }, + }, + } + + for i := range tc.expected { + tc.expected[i].EffectIndex = uint32(i) + tc.expected[i].EffectId = fmt.Sprintf("%d-%d", tc.expected[i].OperationID, tc.expected[i].EffectIndex) + } + + t.Run(tc.desc, func(t *testing.T) { + operation := transactionOperationWrapper{ + index: 0, + transaction: tx, + operation: op, + ledgerSequence: 1, + ledgerClosed: genericCloseTime.UTC(), + } + + effects, err := operation.effects() + assert.NoError(t, err) + assert.Equal(t, tc.expected, effects) + }) + } + +} + +func getRevokeSponsorshipEnvelopeXDR(t *testing.T) string { + source := xdr.MustMuxedAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY") + env := &xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: source, + Memo: xdr.Memo{Type: xdr.MemoTypeMemoNone}, + Operations: []xdr.Operation{ + { + SourceAccount: &source, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipOp: &xdr.RevokeSponsorshipOp{ + Type: xdr.RevokeSponsorshipTypeRevokeSponsorshipSigner, + Signer: &xdr.RevokeSponsorshipOpSigner{ + AccountId: xdr.MustAddress("GAHK7EEG2WWHVKDNT4CEQFZGKF2LGDSW2IVM4S5DP42RBW3K6BTODB4A"), + SignerKey: xdr.MustSigner("GCAHY6JSXQFKWKP6R7U5JPXDVNV4DJWOWRFLY3Y6YPBF64QRL4BPFDNS"), + }, + }, + }, + }, + }, + }, + }, + } + b64, err := xdr.MarshalBase64(env) + assert.NoError(t, err) + return b64 +} + +func BuildLedgerTransaction(t *testing.T, tx TestTransaction) ingest.LedgerTransaction { + transaction := ingest.LedgerTransaction{ + Index: tx.Index, + Envelope: xdr.TransactionEnvelope{}, + Result: xdr.TransactionResultPair{}, + FeeChanges: xdr.LedgerEntryChanges{}, + UnsafeMeta: xdr.TransactionMeta{}, + } + + tt := assert.New(t) + + err := xdr.SafeUnmarshalBase64(tx.EnvelopeXDR, &transaction.Envelope) + tt.NoError(err) + err = xdr.SafeUnmarshalBase64(tx.ResultXDR, &transaction.Result.Result) + tt.NoError(err) + err = xdr.SafeUnmarshalBase64(tx.MetaXDR, &transaction.UnsafeMeta) + tt.NoError(err) + err = xdr.SafeUnmarshalBase64(tx.FeeChangesXDR, &transaction.FeeChanges) + tt.NoError(err) + + _, err = hex.Decode(transaction.Result.TransactionHash[:], []byte(tx.Hash)) + tt.NoError(err) + + return transaction +} + +func createTransactionMeta(opMeta []xdr.OperationMeta) xdr.TransactionMeta { + return xdr.TransactionMeta{ + V: 1, + V1: &xdr.TransactionMetaV1{ + Operations: opMeta, + }, + } +} + +func getRevokeSponsorshipMeta(t *testing.T) (string, []EffectOutput) { + source := xdr.MustAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY") + firstSigner := xdr.MustAddress("GCQZP3IU7XU6EJ63JZXKCQOYT2RNXN3HB5CNHENNUEUHSMA4VUJJJSEN") + secondSigner := xdr.MustAddress("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") + thirdSigner := xdr.MustAddress("GACMZD5VJXTRLKVET72CETCYKELPNCOTTBDC6DHFEUPLG5DHEK534JQX") + formerSponsor := xdr.MustAddress("GAHK7EEG2WWHVKDNT4CEQFZGKF2LGDSW2IVM4S5DP42RBW3K6BTODB4A") + oldSponsor := xdr.MustAddress("GANFZDRBCNTUXIODCJEYMACPMCSZEVE4WZGZ3CZDZ3P2SXK4KH75IK6Y") + updatedSponsor := xdr.MustAddress("GAHK7EEG2WWHVKDNT4CEQFZGKF2LGDSW2IVM4S5DP42RBW3K6BTODB4A") + newSponsor := xdr.MustAddress("GDEOVUDLCYTO46D6GD6WH7BFESPBV5RACC6F6NUFCIRU7PL2XONQHVGJ") + + expectedEffects := []EffectOutput{ + { + Address: source.Address(), + OperationID: 249108107265, + Details: map[string]interface{}{ + "sponsor": newSponsor.Address(), + "signer": thirdSigner.Address(), + }, + Type: int32(EffectSignerSponsorshipCreated), + TypeString: EffectTypeNames[EffectSignerSponsorshipCreated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 58, + }, + { + Address: source.Address(), + OperationID: 249108107265, + Details: map[string]interface{}{ + "former_sponsor": oldSponsor.Address(), + "new_sponsor": updatedSponsor.Address(), + "signer": secondSigner.Address(), + }, + Type: int32(EffectSignerSponsorshipUpdated), + TypeString: EffectTypeNames[EffectSignerSponsorshipUpdated], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 58, + }, + { + Address: source.Address(), + OperationID: 249108107265, + Details: map[string]interface{}{ + "former_sponsor": formerSponsor.Address(), + "signer": firstSigner.Address(), + }, + Type: int32(EffectSignerSponsorshipRemoved), + TypeString: EffectTypeNames[EffectSignerSponsorshipRemoved], + LedgerClosed: genericCloseTime.UTC(), + LedgerSequence: 58, + }, + } + + accountSignersMeta := &xdr.TransactionMeta{ + V: 1, + V1: &xdr.TransactionMetaV1{ + TxChanges: xdr.LedgerEntryChanges{}, + Operations: []xdr.OperationMeta{ + { + Changes: xdr.LedgerEntryChanges{ + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 0x39, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: source, + Balance: 800152367009533292, + SeqNum: 26, + InflationDest: &source, + Thresholds: xdr.Thresholds{0x1, 0x0, 0x0, 0x0}, + Signers: []xdr.Signer{ + { + Key: xdr.SignerKey{ + Type: xdr.SignerKeyTypeSignerKeyTypeEd25519, + Ed25519: firstSigner.Ed25519, + }, + Weight: 10, + }, + { + Key: xdr.SignerKey{ + Type: xdr.SignerKeyTypeSignerKeyTypeEd25519, + Ed25519: secondSigner.Ed25519, + }, + Weight: 10, + }, + { + Key: xdr.SignerKey{ + Type: xdr.SignerKeyTypeSignerKeyTypeEd25519, + Ed25519: thirdSigner.Ed25519, + }, + Weight: 10, + }, + }, + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryExtensionV1{ + Liabilities: xdr.Liabilities{}, + Ext: xdr.AccountEntryExtensionV1Ext{ + V: 2, + V2: &xdr.AccountEntryExtensionV2{ + NumSponsored: 0, + NumSponsoring: 0, + SignerSponsoringIDs: []xdr.SponsorshipDescriptor{ + &formerSponsor, + &oldSponsor, + nil, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 0x39, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: source, + Balance: 800152367009533292, + SeqNum: 26, + InflationDest: &source, + Thresholds: xdr.Thresholds{0x1, 0x0, 0x0, 0x0}, + Signers: []xdr.Signer{ + { + Key: xdr.SignerKey{ + Type: xdr.SignerKeyTypeSignerKeyTypeEd25519, + Ed25519: secondSigner.Ed25519, + }, + Weight: 10, + }, + { + Key: xdr.SignerKey{ + Type: xdr.SignerKeyTypeSignerKeyTypeEd25519, + Ed25519: thirdSigner.Ed25519, + }, + Weight: 10, + }, + }, + Ext: xdr.AccountEntryExt{ + V: 1, + V1: &xdr.AccountEntryExtensionV1{ + Liabilities: xdr.Liabilities{}, + Ext: xdr.AccountEntryExtensionV1Ext{ + V: 2, + V2: &xdr.AccountEntryExtensionV2{ + NumSponsored: 0, + NumSponsoring: 0, + SignerSponsoringIDs: []xdr.SponsorshipDescriptor{ + &updatedSponsor, + &newSponsor, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + b64, err := xdr.MarshalBase64(accountSignersMeta) + assert.NoError(t, err) + + return b64, expectedEffects +} + +type ClaimClaimableBalanceEffectsTestSuite struct { + suite.Suite +} + +type CreateClaimableBalanceEffectsTestSuite struct { + suite.Suite +} + +const ( + networkPassphrase = "Arbitrary Testing Passphrase" +) + +func TestInvokeHostFunctionEffects(t *testing.T) { + randAddr := func() string { + return keypair.MustRandom().Address() + } + + admin := randAddr() + asset := xdr.MustNewCreditAsset("TESTER", admin) + from, to := randAddr(), randAddr() + fromContractBytes, toContractBytes := xdr.Hash{}, xdr.Hash{1} + fromContract := strkey.MustEncode(strkey.VersionByteContract, fromContractBytes[:]) + toContract := strkey.MustEncode(strkey.VersionByteContract, toContractBytes[:]) + amount := big.NewInt(12345) + + rawContractId := [64]byte{} + rand.Read(rawContractId[:]) + + testCases := []struct { + desc string + asset xdr.Asset + from, to string + eventType contractevents.EventType + expected []EffectOutput + }{ + { + desc: "transfer", + asset: asset, + eventType: contractevents.EventTypeTransfer, + expected: []EffectOutput{ + { + Address: from, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_code": strings.Trim(asset.GetCode(), "\x00"), + "asset_issuer": asset.GetIssuer(), + "asset_type": "credit_alphanum12", + "contract_event_type": "transfer", + }, + Type: int32(EffectAccountDebited), + TypeString: EffectTypeNames[EffectAccountDebited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, { + Address: to, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_code": strings.Trim(asset.GetCode(), "\x00"), + "asset_issuer": asset.GetIssuer(), + "asset_type": "credit_alphanum12", + "contract_event_type": "transfer", + }, + Type: int32(EffectAccountCredited), + TypeString: EffectTypeNames[EffectAccountCredited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, + }, + }, { + desc: "transfer between contracts", + asset: asset, + eventType: contractevents.EventTypeTransfer, + from: fromContract, + to: toContract, + expected: []EffectOutput{ + { + Address: admin, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_code": strings.Trim(asset.GetCode(), "\x00"), + "asset_issuer": asset.GetIssuer(), + "asset_type": "credit_alphanum12", + "contract": fromContract, + "contract_event_type": "transfer", + }, + Type: int32(EffectContractDebited), + TypeString: EffectTypeNames[EffectContractDebited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, { + Address: admin, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_code": strings.Trim(asset.GetCode(), "\x00"), + "asset_issuer": asset.GetIssuer(), + "asset_type": "credit_alphanum12", + "contract": toContract, + "contract_event_type": "transfer", + }, + Type: int32(EffectContractCredited), + TypeString: EffectTypeNames[EffectContractCredited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, + }, + }, { + desc: "mint", + asset: asset, + eventType: contractevents.EventTypeMint, + expected: []EffectOutput{ + { + Address: to, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_code": strings.Trim(asset.GetCode(), "\x00"), + "asset_issuer": asset.GetIssuer(), + "asset_type": "credit_alphanum12", + "contract_event_type": "mint", + }, + Type: int32(EffectAccountCredited), + TypeString: EffectTypeNames[EffectAccountCredited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, + }, + }, { + desc: "burn", + asset: asset, + eventType: contractevents.EventTypeBurn, + expected: []EffectOutput{ + { + Address: from, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_code": strings.Trim(asset.GetCode(), "\x00"), + "asset_issuer": asset.GetIssuer(), + "asset_type": "credit_alphanum12", + "contract_event_type": "burn", + }, + Type: int32(EffectAccountDebited), + TypeString: EffectTypeNames[EffectAccountDebited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, + }, + }, { + desc: "burn from contract", + asset: asset, + eventType: contractevents.EventTypeBurn, + from: fromContract, + expected: []EffectOutput{ + { + Address: admin, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_code": strings.Trim(asset.GetCode(), "\x00"), + "asset_issuer": asset.GetIssuer(), + "asset_type": "credit_alphanum12", + "contract": fromContract, + "contract_event_type": "burn", + }, + Type: int32(EffectContractDebited), + TypeString: EffectTypeNames[EffectContractDebited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, + }, + }, { + desc: "clawback", + asset: asset, + eventType: contractevents.EventTypeClawback, + expected: []EffectOutput{ + { + Address: from, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_code": strings.Trim(asset.GetCode(), "\x00"), + "asset_issuer": asset.GetIssuer(), + "asset_type": "credit_alphanum12", + "contract_event_type": "clawback", + }, + Type: int32(EffectAccountDebited), + TypeString: EffectTypeNames[EffectAccountDebited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, + }, + }, { + desc: "clawback from contract", + asset: asset, + eventType: contractevents.EventTypeClawback, + from: fromContract, + expected: []EffectOutput{ + { + Address: admin, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_code": strings.Trim(asset.GetCode(), "\x00"), + "asset_issuer": asset.GetIssuer(), + "asset_type": "credit_alphanum12", + "contract": fromContract, + "contract_event_type": "clawback", + }, + Type: int32(EffectContractDebited), + TypeString: EffectTypeNames[EffectContractDebited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, + }, + }, { + desc: "transfer native", + asset: xdr.MustNewNativeAsset(), + eventType: contractevents.EventTypeTransfer, + expected: []EffectOutput{ + { + Address: from, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_type": "native", + "contract_event_type": "transfer", + }, + Type: int32(EffectAccountDebited), + TypeString: EffectTypeNames[EffectAccountDebited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, { + Address: to, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_type": "native", + "contract_event_type": "transfer", + }, + Type: int32(EffectAccountCredited), + TypeString: EffectTypeNames[EffectAccountCredited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, + }, + }, { + desc: "transfer into contract", + asset: asset, + to: toContract, + eventType: contractevents.EventTypeTransfer, + expected: []EffectOutput{ + { + Address: from, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_code": strings.Trim(asset.GetCode(), "\x00"), + "asset_issuer": asset.GetIssuer(), + "asset_type": "credit_alphanum12", + "contract_event_type": "transfer", + }, + Type: int32(EffectAccountDebited), + TypeString: EffectTypeNames[EffectAccountDebited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, { + Address: admin, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_code": strings.Trim(asset.GetCode(), "\x00"), + "asset_issuer": asset.GetIssuer(), + "asset_type": "credit_alphanum12", + "contract": toContract, + "contract_event_type": "transfer", + }, + Type: int32(EffectContractCredited), + TypeString: EffectTypeNames[EffectContractCredited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, + }, + }, { + desc: "transfer out of contract", + asset: asset, + from: fromContract, + eventType: contractevents.EventTypeTransfer, + expected: []EffectOutput{ + { + Address: admin, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_code": strings.Trim(asset.GetCode(), "\x00"), + "asset_issuer": asset.GetIssuer(), + "asset_type": "credit_alphanum12", + "contract": fromContract, + "contract_event_type": "transfer", + }, + Type: int32(EffectContractDebited), + TypeString: EffectTypeNames[EffectContractDebited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, { + Address: to, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "amount": "0.0012345", + "asset_code": strings.Trim(asset.GetCode(), "\x00"), + "asset_issuer": asset.GetIssuer(), + "asset_type": "credit_alphanum12", + "contract_event_type": "transfer", + }, + Type: int32(EffectAccountCredited), + TypeString: EffectTypeNames[EffectAccountCredited], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.desc, func(t *testing.T) { + var tx ingest.LedgerTransaction + + fromAddr := from + if testCase.from != "" { + fromAddr = testCase.from + } + + toAddr := to + if testCase.to != "" { + toAddr = testCase.to + } + + tx = makeInvocationTransaction( + fromAddr, toAddr, + admin, + testCase.asset, + amount, + testCase.eventType, + ) + assert.True(t, tx.Result.Successful()) // sanity check + + operation := transactionOperationWrapper{ + index: 0, + transaction: tx, + operation: tx.Envelope.Operations()[0], + ledgerSequence: 1, + network: networkPassphrase, + } + + for i := range testCase.expected { + testCase.expected[i].EffectIndex = uint32(i) + testCase.expected[i].EffectId = fmt.Sprintf("%d-%d", testCase.expected[i].OperationID, testCase.expected[i].EffectIndex) + } + + effects, err := operation.effects() + assert.NoErrorf(t, err, "event type %v", testCase.eventType) + assert.Lenf(t, effects, len(testCase.expected), "event type %v", testCase.eventType) + assert.Equalf(t, testCase.expected, effects, "event type %v", testCase.eventType) + }) + } +} + +// makeInvocationTransaction returns a single transaction containing a single +// invokeHostFunction operation that generates the specified Stellar Asset +// Contract events in its txmeta. +func makeInvocationTransaction( + from, to, admin string, + asset xdr.Asset, + amount *big.Int, + types ...contractevents.EventType, +) ingest.LedgerTransaction { + meta := xdr.TransactionMetaV3{ + // irrelevant for contract invocations: only events are inspected + Operations: []xdr.OperationMeta{}, + SorobanMeta: &xdr.SorobanTransactionMeta{ + Events: make([]xdr.ContractEvent, len(types)), + }, + } + + for idx, type_ := range types { + event := contractevents.GenerateEvent( + type_, + from, to, admin, + asset, + amount, + networkPassphrase, + ) + meta.SorobanMeta.Events[idx] = event + } + + envelope := xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + // the rest doesn't matter for effect ingestion + Operations: []xdr.Operation{ + { + SourceAccount: xdr.MustMuxedAddressPtr(admin), + Body: xdr.OperationBody{ + Type: xdr.OperationTypeInvokeHostFunction, + // contents of the op are irrelevant as they aren't + // parsed by anyone yet, e.g. effects are generated + // purely from events + InvokeHostFunctionOp: &xdr.InvokeHostFunctionOp{}, + }, + }, + }, + }, + } + + return ingest.LedgerTransaction{ + Index: 0, + Envelope: xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &envelope, + }, + // the result just needs enough to look successful + Result: xdr.TransactionResultPair{ + TransactionHash: xdr.Hash([32]byte{}), + Result: xdr.TransactionResult{ + FeeCharged: 1234, + Result: xdr.TransactionResultResult{ + Code: xdr.TransactionResultCodeTxSuccess, + }, + }, + }, + UnsafeMeta: xdr.TransactionMeta{V: 3, V3: &meta}, + } +} + +func TestBumpFootprintExpirationEffects(t *testing.T) { + randAddr := func() string { + return keypair.MustRandom().Address() + } + + admin := randAddr() + keyHash := xdr.Hash{} + + ledgerEntryKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.LedgerKeyTtl{ + KeyHash: keyHash, + }, + } + ledgerEntryKeyStr, err := xdr.MarshalBase64(ledgerEntryKey) + assert.NoError(t, err) + + meta := xdr.TransactionMetaV3{ + Operations: []xdr.OperationMeta{ + { + Changes: xdr.LedgerEntryChanges{ + // TODO: Confirm this STATE entry is emitted from core as part of the + // ledger close meta we get. + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: keyHash, + LiveUntilLedgerSeq: 1, + }, + }, + }, + }, + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: keyHash, + LiveUntilLedgerSeq: 1234, + }, + }, + }, + }, + }, + }, + }, + } + + envelope := xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + // the rest doesn't matter for effect ingestion + Operations: []xdr.Operation{ + { + SourceAccount: xdr.MustMuxedAddressPtr(admin), + Body: xdr.OperationBody{ + Type: xdr.OperationTypeExtendFootprintTtl, + ExtendFootprintTtlOp: &xdr.ExtendFootprintTtlOp{ + Ext: xdr.ExtensionPoint{ + V: 0, + }, + ExtendTo: xdr.Uint32(1234), + }, + }, + }, + }, + }, + } + tx := ingest.LedgerTransaction{ + Index: 0, + Envelope: xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &envelope, + }, + UnsafeMeta: xdr.TransactionMeta{ + V: 3, + Operations: &meta.Operations, + V3: &meta, + }, + } + + operation := transactionOperationWrapper{ + index: 0, + transaction: tx, + operation: tx.Envelope.Operations()[0], + ledgerSequence: 1, + network: networkPassphrase, + } + + effects, err := operation.effects() + assert.NoError(t, err) + assert.Len(t, effects, 1) + assert.Equal(t, + []EffectOutput{ + { + Address: admin, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "entries": []string{ + ledgerEntryKeyStr, + }, + "extend_to": xdr.Uint32(1234), + }, + Type: int32(EffectExtendFootprintTtl), + TypeString: EffectTypeNames[EffectExtendFootprintTtl], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + EffectIndex: 0, + EffectId: fmt.Sprintf("%d-%d", toid.New(1, 0, 1).ToInt64(), 0), + }, + }, + effects, + ) +} + +func TestAddRestoreFootprintExpirationEffect(t *testing.T) { + randAddr := func() string { + return keypair.MustRandom().Address() + } + + admin := randAddr() + keyHash := xdr.Hash{} + + ledgerEntryKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.LedgerKeyTtl{ + KeyHash: keyHash, + }, + } + ledgerEntryKeyStr, err := xdr.MarshalBase64(ledgerEntryKey) + assert.NoError(t, err) + + meta := xdr.TransactionMetaV3{ + Operations: []xdr.OperationMeta{ + { + Changes: xdr.LedgerEntryChanges{ + // TODO: Confirm this STATE entry is emitted from core as part of the + // ledger close meta we get. + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: keyHash, + LiveUntilLedgerSeq: 1, + }, + }, + }, + }, + { + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: keyHash, + LiveUntilLedgerSeq: 1234, + }, + }, + }, + }, + }, + }, + }, + } + + envelope := xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + // the rest doesn't matter for effect ingestion + Operations: []xdr.Operation{ + { + SourceAccount: xdr.MustMuxedAddressPtr(admin), + Body: xdr.OperationBody{ + Type: xdr.OperationTypeRestoreFootprint, + RestoreFootprintOp: &xdr.RestoreFootprintOp{ + Ext: xdr.ExtensionPoint{ + V: 0, + }, + }, + }, + }, + }, + }, + } + tx := ingest.LedgerTransaction{ + Index: 0, + Envelope: xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &envelope, + }, + UnsafeMeta: xdr.TransactionMeta{ + V: 3, + Operations: &meta.Operations, + V3: &meta, + }, + } + + operation := transactionOperationWrapper{ + index: 0, + transaction: tx, + operation: tx.Envelope.Operations()[0], + ledgerSequence: 1, + network: networkPassphrase, + } + + effects, err := operation.effects() + assert.NoError(t, err) + assert.Len(t, effects, 1) + assert.Equal(t, + []EffectOutput{ + { + Address: admin, + OperationID: toid.New(1, 0, 1).ToInt64(), + Details: map[string]interface{}{ + "entries": []string{ + ledgerEntryKeyStr, + }, + }, + Type: int32(EffectRestoreFootprint), + TypeString: EffectTypeNames[EffectRestoreFootprint], + LedgerClosed: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + LedgerSequence: 1, + EffectIndex: 0, + EffectId: fmt.Sprintf("%d-%d", toid.New(1, 0, 1).ToInt64(), 0), + }, + }, + effects, + ) +} diff --git a/ingest/processors/ledger.go b/ingest/processors/ledger.go new file mode 100644 index 0000000000..1377746583 --- /dev/null +++ b/ingest/processors/ledger.go @@ -0,0 +1,205 @@ +package processors + +import ( + "encoding/base64" + "fmt" + "strconv" + + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/strkey" + "github.com/stellar/go/toid" + "github.com/stellar/go/xdr" +) + +// TransformLedger converts a ledger from the history archive ingestion system into a form suitable for BigQuery +func TransformLedger(inputLedger historyarchive.Ledger, lcm xdr.LedgerCloseMeta) (LedgerOutput, error) { + ledgerHeader := inputLedger.Header.Header + + outputSequence := uint32(ledgerHeader.LedgerSeq) + + outputLedgerID := toid.New(int32(outputSequence), 0, 0).ToInt64() + + outputLedgerHash := HashToHexString(inputLedger.Header.Hash) + outputPreviousHash := HashToHexString(ledgerHeader.PreviousLedgerHash) + + outputLedgerHeader, err := xdr.MarshalBase64(ledgerHeader) + if err != nil { + return LedgerOutput{}, fmt.Errorf("for ledger %d (ledger id=%d): %v", outputSequence, outputLedgerID, err) + } + + outputTransactionCount, outputOperationCount, outputSuccessfulCount, outputFailedCount, outputTxSetOperationCount, err := extractCounts(inputLedger) + if err != nil { + return LedgerOutput{}, fmt.Errorf("for ledger %d (ledger id=%d): %v", outputSequence, outputLedgerID, err) + } + + outputCloseTime, err := TimePointToUTCTimeStamp(ledgerHeader.ScpValue.CloseTime) + if err != nil { + return LedgerOutput{}, err + } + + outputTotalCoins := int64(ledgerHeader.TotalCoins) + if outputTotalCoins < 0 { + return LedgerOutput{}, fmt.Errorf("the total number of coins (%d) is negative for ledger %d (ledger id=%d)", outputTotalCoins, outputSequence, outputLedgerID) + } + + outputFeePool := int64(ledgerHeader.FeePool) + if outputFeePool < 0 { + return LedgerOutput{}, fmt.Errorf("the fee pool (%d) is negative for ledger %d (ledger id=%d)", outputFeePool, outputSequence, outputLedgerID) + } + + outputBaseFee := uint32(ledgerHeader.BaseFee) + + outputBaseReserve := uint32(ledgerHeader.BaseReserve) + + outputMaxTxSetSize := uint32(ledgerHeader.MaxTxSetSize) + + outputProtocolVersion := uint32(ledgerHeader.LedgerVersion) + + var outputSorobanFeeWrite1Kb int64 + var outputTotalByteSizeOfBucketList uint64 + + lcmV1, ok := lcm.GetV1() + if ok { + var extV1 xdr.LedgerCloseMetaExtV1 + extV1, ok = lcmV1.Ext.GetV1() + if ok { + outputSorobanFeeWrite1Kb = int64(extV1.SorobanFeeWrite1Kb) + } + totalByteSizeOfBucketList := lcmV1.TotalByteSizeOfBucketList + outputTotalByteSizeOfBucketList = uint64(totalByteSizeOfBucketList) + } + + var outputNodeID string + var outputSignature string + LedgerCloseValueSignature, ok := ledgerHeader.ScpValue.Ext.GetLcValueSignature() + if ok { + outputNodeID, err = getAddress(LedgerCloseValueSignature.NodeId) + if err != nil { + return LedgerOutput{}, err + } + outputSignature = base64.StdEncoding.EncodeToString(LedgerCloseValueSignature.Signature) + } + + transformedLedger := LedgerOutput{ + Sequence: outputSequence, + LedgerID: outputLedgerID, + LedgerHash: outputLedgerHash, + PreviousLedgerHash: outputPreviousHash, + LedgerHeader: outputLedgerHeader, + TransactionCount: outputTransactionCount, + OperationCount: outputOperationCount, + SuccessfulTransactionCount: outputSuccessfulCount, + FailedTransactionCount: outputFailedCount, + TxSetOperationCount: outputTxSetOperationCount, + ClosedAt: outputCloseTime, + TotalCoins: outputTotalCoins, + FeePool: outputFeePool, + BaseFee: outputBaseFee, + BaseReserve: outputBaseReserve, + MaxTxSetSize: outputMaxTxSetSize, + ProtocolVersion: outputProtocolVersion, + SorobanFeeWrite1Kb: outputSorobanFeeWrite1Kb, + NodeID: outputNodeID, + Signature: outputSignature, + TotalByteSizeOfBucketList: outputTotalByteSizeOfBucketList, + } + return transformedLedger, nil +} + +func TransactionProcessing(l xdr.LedgerCloseMeta) []xdr.TransactionResultMeta { + switch l.V { + case 0: + return l.MustV0().TxProcessing + case 1: + return l.MustV1().TxProcessing + default: + panic(fmt.Sprintf("Unsupported LedgerCloseMeta.V: %d", l.V)) + } +} + +func extractCounts(ledger historyarchive.Ledger) (transactionCount int32, operationCount int32, successTxCount int32, failedTxCount int32, txSetOperationCount string, err error) { + transactions := GetTransactionSet(ledger) + results := ledger.TransactionResult.TxResultSet.Results + txCount := len(transactions) + if txCount != len(results) { + err = fmt.Errorf("the number of transactions and results are different (%d != %d)", txCount, len(results)) + return + } + + txSetOperationCounter := int32(0) + for i := 0; i < txCount; i++ { + operations := transactions[i].Operations() + numberOfOps := int32(len(operations)) + txSetOperationCounter += numberOfOps + + // for successful transactions, the operation count is based on the operations results slice + if results[i].Result.Successful() { + operationResults, ok := results[i].Result.OperationResults() + if !ok { + err = fmt.Errorf("could not access operation results for result %d", i) + return + } + + successTxCount++ + operationCount += int32(len(operationResults)) + } else { + failedTxCount++ + } + + } + transactionCount = int32(txCount) - failedTxCount + txSetOperationCount = strconv.FormatInt(int64(txSetOperationCounter), 10) + return +} + +func GetTransactionSet(transactionEntry historyarchive.Ledger) (transactionProcessing []xdr.TransactionEnvelope) { + switch transactionEntry.Transaction.Ext.V { + case 0: + return transactionEntry.Transaction.TxSet.Txs + case 1: + return getTransactionPhase(transactionEntry.Transaction.Ext.GeneralizedTxSet.V1TxSet.Phases) + default: + panic(fmt.Sprintf("Unsupported TransactionHistoryEntry.Ext: %d", transactionEntry.Transaction.Ext.V)) + } +} + +func getTransactionPhase(transactionPhase []xdr.TransactionPhase) (transactionEnvelope []xdr.TransactionEnvelope) { + transactionSlice := []xdr.TransactionEnvelope{} + for _, phase := range transactionPhase { + switch phase.V { + case 0: + components := phase.MustV0Components() + for _, component := range components { + switch component.Type { + case 0: + transactionSlice = append(transactionSlice, component.TxsMaybeDiscountedFee.Txs...) + + default: + panic(fmt.Sprintf("Unsupported TxSetComponentType: %d", component.Type)) + } + + } + default: + panic(fmt.Sprintf("Unsupported TransactionPhase.V: %d", phase.V)) + } + } + return transactionSlice + +} + +// TODO: This should be moved into the go monorepo xdr functions +// Or nodeID should just be an xdr.AccountId but the error message would be incorrect +func getAddress(nodeID xdr.NodeId) (string, error) { + switch nodeID.Type { + case xdr.PublicKeyTypePublicKeyTypeEd25519: + ed, ok := nodeID.GetEd25519() + if !ok { + return "", fmt.Errorf("could not get Ed25519") + } + raw := make([]byte, 32) + copy(raw, ed[:]) + return strkey.Encode(strkey.VersionByteAccountID, raw) + default: + return "", fmt.Errorf("unknown node id type: %v", nodeID.Type) + } +} diff --git a/ingest/processors/ledger_test.go b/ingest/processors/ledger_test.go new file mode 100644 index 0000000000..767762faa8 --- /dev/null +++ b/ingest/processors/ledger_test.go @@ -0,0 +1,205 @@ +package processors + +import ( + "fmt" + "testing" + "time" + + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" +) + +func TestTransformLedger(t *testing.T) { + type transformTest struct { + input HistoryArchiveLedgerAndLCM + wantOutput LedgerOutput + wantErr error + } + hardCodedLedger, err := makeLedgerTestInput() + assert.NoError(t, err) + + hardCodedOutput, err := makeLedgerTestOutput() + assert.NoError(t, err) + + tests := []transformTest{ + { + HistoryArchiveLedgerAndLCM{ + Ledger: historyarchive.Ledger{ + Header: xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + TotalCoins: -1, + }, + }, + }, + LCM: xdr.LedgerCloseMeta{ + V: 1, + V1: &xdr.LedgerCloseMetaV1{ + Ext: xdr.LedgerCloseMetaExt{ + V: 1, + V1: &xdr.LedgerCloseMetaExtV1{ + SorobanFeeWrite1Kb: xdr.Int64(1234), + }, + }, + }, + }, + }, + LedgerOutput{}, + fmt.Errorf("the total number of coins (-1) is negative for ledger 0 (ledger id=0)"), + }, + { + HistoryArchiveLedgerAndLCM{ + Ledger: historyarchive.Ledger{ + Header: xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + FeePool: -1, + }, + }, + }, + LCM: xdr.LedgerCloseMeta{ + V: 1, + V1: &xdr.LedgerCloseMetaV1{ + Ext: xdr.LedgerCloseMetaExt{ + V: 1, + V1: &xdr.LedgerCloseMetaExtV1{ + SorobanFeeWrite1Kb: xdr.Int64(1234), + }, + }, + }, + }, + }, + LedgerOutput{}, + fmt.Errorf("the fee pool (-1) is negative for ledger 0 (ledger id=0)"), + }, + { + hardCodedLedger, + hardCodedOutput, + nil, + }, + } + + for _, test := range tests { + actualOutput, actualError := TransformLedger(test.input.Ledger, test.input.LCM) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func makeLedgerTestOutput() (output LedgerOutput, err error) { + correctTime, err := time.Parse("2006-1-2 15:04:05 MST", "2020-07-12 20:09:07 UTC") + if err != nil { + return + } + + correctBytes := []byte{0x41, 0x41, 0x41, 0x41, 0x44, 0x66, 0x59, 0x38, 0x46, 0x64, 0x44, 0x71, 0x39, 0x49, 0x72, 0x37, 0x31, 0x31, 0x47, 0x6b, 0x78, 0x4e, 0x2b, 0x74, 0x35, 0x55, 0x6f, 0x30, 0x53, 0x41, 0x55, 0x38, 0x52, 0x38, 0x57, 0x6e, 0x48, 0x57, 0x49, 0x6d, 0x61, 0x4b, 0x34, 0x4d, 0x77, 0x71, 0x49, 0x49, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x58, 0x77, 0x74, 0x74, 0x34, 0x77, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x48, 0x53, 0x6d, 0x53, 0x55, 0x4f, 0x6f, 0x68, 0x36, 0x7a, 0x37, 0x48, 0x6c, 0x62, 0x59, 0x51, 0x41, 0x41, 0x45, 0x49, 0x4c, 0x41, 0x79, 0x55, 0x61, 0x4a, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x5a, 0x41, 0x42, 0x4d, 0x53, 0x30, 0x41, 0x41, 0x41, 0x41, 0x50, 0x6f, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41} + output = LedgerOutput{ + Sequence: uint32(30578981), + LedgerID: 131335723340005376, + LedgerHash: "26932dc4d84b5fabe9ae744cb43ce4c6daccf98c86a991b2a14945b1adac4d59", + PreviousLedgerHash: "f63c15d0eaf48afbd751a4c4dfade54a3448053c47c5a71d622668ae0cc2a208", + LedgerHeader: string(correctBytes), + ClosedAt: correctTime, + + TotalCoins: 1054439020873472865, + FeePool: 18153766209161, + BaseFee: 100, + BaseReserve: 5000000, + MaxTxSetSize: 1000, + ProtocolVersion: 13, + + TransactionCount: 1, + OperationCount: 10, + SuccessfulTransactionCount: 1, + FailedTransactionCount: 1, + TxSetOperationCount: "13", + SorobanFeeWrite1Kb: 1234, + } + return +} + +func makeLedgerTestInput() (lcm HistoryArchiveLedgerAndLCM, err error) { + hardCodedTxSet := xdr.TransactionSet{ + Txs: []xdr.TransactionEnvelope{ + CreateSampleTx(0, 3), + CreateSampleTx(1, 10), + }, + } + hardCodedTxProcessing := []xdr.TransactionResultPair{ + CreateSampleResultPair(false, 3), + CreateSampleResultPair(true, 10), + } + ledger := historyarchive.Ledger{ + Header: xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + LedgerSeq: 30578981, + TotalCoins: 1054439020873472865, + FeePool: 18153766209161, + BaseFee: 100, + BaseReserve: 5000000, + MaxTxSetSize: 1000, + LedgerVersion: 13, + PreviousLedgerHash: xdr.Hash{0xf6, 0x3c, 0x15, 0xd0, 0xea, 0xf4, 0x8a, 0xfb, 0xd7, 0x51, 0xa4, 0xc4, 0xdf, 0xad, 0xe5, 0x4a, 0x34, 0x48, 0x5, 0x3c, 0x47, 0xc5, 0xa7, 0x1d, 0x62, 0x26, 0x68, 0xae, 0xc, 0xc2, 0xa2, 0x8}, + ScpValue: xdr.StellarValue{CloseTime: 1594584547}, + }, + Hash: xdr.Hash{0x26, 0x93, 0x2d, 0xc4, 0xd8, 0x4b, 0x5f, 0xab, 0xe9, 0xae, 0x74, 0x4c, 0xb4, 0x3c, 0xe4, 0xc6, 0xda, 0xcc, 0xf9, 0x8c, 0x86, 0xa9, 0x91, 0xb2, 0xa1, 0x49, 0x45, 0xb1, 0xad, 0xac, 0x4d, 0x59}, + }, + Transaction: xdr.TransactionHistoryEntry{ + LedgerSeq: 30578981, + TxSet: hardCodedTxSet, + }, + TransactionResult: xdr.TransactionHistoryResultEntry{ + LedgerSeq: 30578981, + TxResultSet: xdr.TransactionResultSet{ + Results: hardCodedTxProcessing, + }, + Ext: xdr.TransactionHistoryResultEntryExt{}, + }, + } + + lcm = HistoryArchiveLedgerAndLCM{ + Ledger: ledger, + LCM: xdr.LedgerCloseMeta{ + V: 1, + V1: &xdr.LedgerCloseMetaV1{ + Ext: xdr.LedgerCloseMetaExt{ + V: 1, + V1: &xdr.LedgerCloseMetaExtV1{ + SorobanFeeWrite1Kb: xdr.Int64(1234), + }, + }, + }, + }, + } + + return lcm, nil +} + +func CreateSampleResultPair(successful bool, subOperationCount int) xdr.TransactionResultPair { + resultCode := xdr.TransactionResultCodeTxFailed + if successful { + resultCode = xdr.TransactionResultCodeTxSuccess + } + operationResults := []xdr.OperationResult{} + operationResultTr := &xdr.OperationResultTr{ + Type: xdr.OperationTypeCreateAccount, + CreateAccountResult: &xdr.CreateAccountResult{ + Code: 0, + }, + } + + for i := 0; i < subOperationCount; i++ { + operationResults = append(operationResults, xdr.OperationResult{ + Code: xdr.OperationResultCodeOpInner, + Tr: operationResultTr, + }) + } + + return xdr.TransactionResultPair{ + Result: xdr.TransactionResult{ + Result: xdr.TransactionResultResult{ + Code: resultCode, + Results: &operationResults, + }, + }, + } +} diff --git a/ingest/processors/liquidity_pool.go b/ingest/processors/liquidity_pool.go new file mode 100644 index 0000000000..ee2fef3f31 --- /dev/null +++ b/ingest/processors/liquidity_pool.go @@ -0,0 +1,81 @@ +package processors + +import ( + "fmt" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +// TransformPool converts an liquidity pool ledger change entry into a form suitable for BigQuery +func TransformPool(ledgerChange ingest.Change, header xdr.LedgerHeaderHistoryEntry) (PoolOutput, error) { + ledgerEntry, changeType, outputDeleted, err := ExtractEntryFromChange(ledgerChange) + if err != nil { + return PoolOutput{}, err + } + + // LedgerEntryChange must contain a liquidity pool state change to be parsed, otherwise skip + if ledgerEntry.Data.Type != xdr.LedgerEntryTypeLiquidityPool { + return PoolOutput{}, nil + } + + lp, ok := ledgerEntry.Data.GetLiquidityPool() + if !ok { + return PoolOutput{}, fmt.Errorf("could not extract liquidity pool data from ledger entry; actual type is %s", ledgerEntry.Data.Type) + } + + cp, ok := lp.Body.GetConstantProduct() + if !ok { + return PoolOutput{}, fmt.Errorf("could not extract constant product information for liquidity pool %s", xdr.Hash(lp.LiquidityPoolId).HexString()) + } + + poolType, ok := xdr.LiquidityPoolTypeToString[lp.Body.Type] + if !ok { + return PoolOutput{}, fmt.Errorf("unknown liquidity pool type: %d", lp.Body.Type) + } + + var assetAType, assetACode, assetAIssuer string + err = cp.Params.AssetA.Extract(&assetAType, &assetACode, &assetAIssuer) + if err != nil { + return PoolOutput{}, err + } + assetAID := FarmHashAsset(assetACode, assetAIssuer, assetAType) + + var assetBType, assetBCode, assetBIssuer string + err = cp.Params.AssetB.Extract(&assetBType, &assetBCode, &assetBIssuer) + if err != nil { + return PoolOutput{}, err + } + assetBID := FarmHashAsset(assetBCode, assetBIssuer, assetBType) + + closedAt, err := TimePointToUTCTimeStamp(header.Header.ScpValue.CloseTime) + if err != nil { + return PoolOutput{}, err + } + + ledgerSequence := header.Header.LedgerSeq + + transformedPool := PoolOutput{ + PoolID: PoolIDToString(lp.LiquidityPoolId), + PoolType: poolType, + PoolFee: uint32(cp.Params.Fee), + TrustlineCount: uint64(cp.PoolSharesTrustLineCount), + PoolShareCount: ConvertStroopValueToReal(cp.TotalPoolShares), + AssetAType: assetAType, + AssetACode: assetACode, + AssetAIssuer: assetAIssuer, + AssetAID: assetAID, + AssetAReserve: ConvertStroopValueToReal(cp.ReserveA), + AssetBType: assetBType, + AssetBCode: assetBCode, + AssetBIssuer: assetBIssuer, + AssetBID: assetBID, + AssetBReserve: ConvertStroopValueToReal(cp.ReserveB), + LastModifiedLedger: uint32(ledgerEntry.LastModifiedLedgerSeq), + LedgerEntryChange: uint32(changeType), + Deleted: outputDeleted, + ClosedAt: closedAt, + LedgerSequence: uint32(ledgerSequence), + } + return transformedPool, nil +} diff --git a/ingest/processors/liquidity_pool_test.go b/ingest/processors/liquidity_pool_test.go new file mode 100644 index 0000000000..2d200f0053 --- /dev/null +++ b/ingest/processors/liquidity_pool_test.go @@ -0,0 +1,118 @@ +package processors + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformPool(t *testing.T) { + type inputStruct struct { + ingest ingest.Change + } + type transformTest struct { + input inputStruct + wantOutput PoolOutput + wantErr error + } + + hardCodedInput := makePoolTestInput() + hardCodedOutput := makePoolTestOutput() + + tests := []transformTest{ + { + inputStruct{ + ingest.Change{ + Type: xdr.LedgerEntryTypeOffer, + Pre: nil, + Post: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + }, + }, + }, + }, + PoolOutput{}, nil, + }, + { + inputStruct{ + hardCodedInput, + }, + hardCodedOutput, nil, + }, + } + + for _, test := range tests { + header := xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + ScpValue: xdr.StellarValue{ + CloseTime: 1000, + }, + LedgerSeq: 10, + }, + } + actualOutput, actualError := TransformPool(test.input.ingest, header) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func makePoolTestInput() ingest.Change { + ledgerEntry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: 30705278, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeLiquidityPool, + LiquidityPool: &xdr.LiquidityPoolEntry{ + LiquidityPoolId: xdr.PoolId{23, 45, 67}, + Body: xdr.LiquidityPoolEntryBody{ + Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, + ConstantProduct: &xdr.LiquidityPoolEntryConstantProduct{ + Params: xdr.LiquidityPoolConstantProductParameters{ + AssetA: lpAssetA, + AssetB: lpAssetB, + Fee: 30, + }, + ReserveA: 105, + ReserveB: 10, + TotalPoolShares: 35, + PoolSharesTrustLineCount: 5, + }, + }, + }, + }, + } + return ingest.Change{ + Type: xdr.LedgerEntryTypeLiquidityPool, + Pre: &ledgerEntry, + Post: nil, + } +} + +func makePoolTestOutput() PoolOutput { + return PoolOutput{ + PoolID: "172d430000000000000000000000000000000000000000000000000000000000", + PoolType: "constant_product", + PoolFee: 30, + TrustlineCount: 5, + PoolShareCount: 0.0000035, + AssetAType: "native", + AssetACode: lpAssetA.GetCode(), + AssetAIssuer: lpAssetA.GetIssuer(), + AssetAID: -5706705804583548011, + AssetAReserve: 0.0000105, + AssetBType: "credit_alphanum4", + AssetBCode: lpAssetB.GetCode(), + AssetBID: 6690054458235693884, + AssetBIssuer: lpAssetB.GetIssuer(), + AssetBReserve: 0.0000010, + LastModifiedLedger: 30705278, + LedgerEntryChange: 2, + Deleted: true, + LedgerSequence: 10, + ClosedAt: time.Date(1970, time.January, 1, 0, 16, 40, 0, time.UTC), + } +} diff --git a/ingest/processors/offer.go b/ingest/processors/offer.go new file mode 100644 index 0000000000..83ea1cea56 --- /dev/null +++ b/ingest/processors/offer.go @@ -0,0 +1,101 @@ +package processors + +import ( + "fmt" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +// TransformOffer converts an account from the history archive ingestion system into a form suitable for BigQuery +func TransformOffer(ledgerChange ingest.Change, header xdr.LedgerHeaderHistoryEntry) (OfferOutput, error) { + ledgerEntry, changeType, outputDeleted, err := ExtractEntryFromChange(ledgerChange) + if err != nil { + return OfferOutput{}, err + } + + offerEntry, offerFound := ledgerEntry.Data.GetOffer() + if !offerFound { + return OfferOutput{}, fmt.Errorf("could not extract offer data from ledger entry; actual type is %s", ledgerEntry.Data.Type) + } + + outputSellerID, err := offerEntry.SellerId.GetAddress() + if err != nil { + return OfferOutput{}, err + } + + outputOfferID := int64(offerEntry.OfferId) + if outputOfferID < 0 { + return OfferOutput{}, fmt.Errorf("offerID is negative (%d) for offer from account: %s", outputOfferID, outputSellerID) + } + + outputSellingAsset, err := transformSingleAsset(offerEntry.Selling) + if err != nil { + return OfferOutput{}, err + } + + outputBuyingAsset, err := transformSingleAsset(offerEntry.Buying) + if err != nil { + return OfferOutput{}, err + } + + outputAmount := offerEntry.Amount + if outputAmount < 0 { + return OfferOutput{}, fmt.Errorf("amount is negative (%d) for offer %d", outputAmount, outputOfferID) + } + + outputPriceN := int32(offerEntry.Price.N) + if outputPriceN < 0 { + return OfferOutput{}, fmt.Errorf("price numerator is negative (%d) for offer %d", outputPriceN, outputOfferID) + } + + outputPriceD := int32(offerEntry.Price.D) + if outputPriceD == 0 { + return OfferOutput{}, fmt.Errorf("price denominator is 0 for offer %d", outputOfferID) + } + + if outputPriceD < 0 { + return OfferOutput{}, fmt.Errorf("price denominator is negative (%d) for offer %d", outputPriceD, outputOfferID) + } + + var outputPrice float64 + if outputPriceN > 0 { + outputPrice = float64(outputPriceN) / float64(outputPriceD) + } + + outputFlags := uint32(offerEntry.Flags) + + outputLastModifiedLedger := uint32(ledgerEntry.LastModifiedLedgerSeq) + + closedAt, err := TimePointToUTCTimeStamp(header.Header.ScpValue.CloseTime) + if err != nil { + return OfferOutput{}, err + } + + ledgerSequence := header.Header.LedgerSeq + + transformedOffer := OfferOutput{ + SellerID: outputSellerID, + OfferID: outputOfferID, + SellingAssetType: outputSellingAsset.AssetType, + SellingAssetCode: outputSellingAsset.AssetCode, + SellingAssetIssuer: outputSellingAsset.AssetIssuer, + SellingAssetID: outputSellingAsset.AssetID, + BuyingAssetType: outputBuyingAsset.AssetType, + BuyingAssetCode: outputBuyingAsset.AssetCode, + BuyingAssetIssuer: outputBuyingAsset.AssetIssuer, + BuyingAssetID: outputBuyingAsset.AssetID, + Amount: ConvertStroopValueToReal(outputAmount), + PriceN: outputPriceN, + PriceD: outputPriceD, + Price: outputPrice, + Flags: outputFlags, + LastModifiedLedger: outputLastModifiedLedger, + LedgerEntryChange: uint32(changeType), + Deleted: outputDeleted, + Sponsor: ledgerEntrySponsorToNullString(ledgerEntry), + ClosedAt: closedAt, + LedgerSequence: uint32(ledgerSequence), + } + return transformedOffer, nil +} diff --git a/ingest/processors/offer_normalized.go b/ingest/processors/offer_normalized.go new file mode 100644 index 0000000000..f80e6e5bf1 --- /dev/null +++ b/ingest/processors/offer_normalized.go @@ -0,0 +1,181 @@ +package processors + +import ( + "fmt" + "hash/fnv" + "sort" + "strings" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +// TransformOfferNormalized converts an offer into a normalized form, allowing it to be stored as part of the historical orderbook dataset +func TransformOfferNormalized(ledgerChange ingest.Change, ledgerSeq uint32) (NormalizedOfferOutput, error) { + + var header xdr.LedgerHeaderHistoryEntry + transformed, err := TransformOffer(ledgerChange, header) + if err != nil { + return NormalizedOfferOutput{}, err + } + + if transformed.Deleted { + return NormalizedOfferOutput{}, fmt.Errorf("offer %d is deleted", transformed.OfferID) + } + + buyingAsset, sellingAsset, err := extractAssets(ledgerChange) + if err != nil { + return NormalizedOfferOutput{}, err + } + + outputMarket, err := extractDimMarket(transformed, buyingAsset, sellingAsset) + if err != nil { + return NormalizedOfferOutput{}, err + } + + outputAccount, err := extractDimAccount(transformed) + if err != nil { + return NormalizedOfferOutput{}, err + } + + outputOffer, err := extractDimOffer(transformed, buyingAsset, sellingAsset, outputMarket.ID, outputAccount.ID) + if err != nil { + return NormalizedOfferOutput{}, err + } + + return NormalizedOfferOutput{ + Market: outputMarket, + Account: outputAccount, + Offer: outputOffer, + Event: FactOfferEvent{ + LedgerSeq: ledgerSeq, + OfferInstanceID: outputOffer.DimOfferID, + }, + }, nil +} + +// extractAssets extracts the buying and selling assets as strings of the format code:issuer +func extractAssets(ledgerChange ingest.Change) (string, string, error) { + ledgerEntry, _, _, err := ExtractEntryFromChange(ledgerChange) + if err != nil { + return "", "", err + } + + offerEntry, offerFound := ledgerEntry.Data.GetOffer() + if !offerFound { + return "", "", fmt.Errorf("could not extract offer data from ledger entry; actual type is %s", ledgerEntry.Data.Type) + } + + var sellType, sellCode, sellIssuer string + err = offerEntry.Selling.Extract(&sellType, &sellCode, &sellIssuer) + if err != nil { + return "", "", err + } + + var outputSellingAsset string + if sellType != "native" { + outputSellingAsset = fmt.Sprintf("%s:%s", sellCode, sellIssuer) + } else { + // native assets have an empty issuer + outputSellingAsset = "native:" + } + + var buyType, buyCode, buyIssuer string + err = offerEntry.Buying.Extract(&buyType, &buyCode, &buyIssuer) + if err != nil { + return "", "", err + } + + var outputBuyingAsset string + if buyType != "native" { + outputBuyingAsset = fmt.Sprintf("%s:%s", buyCode, buyIssuer) + } else { + outputBuyingAsset = "native:" + } + + return outputBuyingAsset, outputSellingAsset, nil +} + +// extractDimMarket gets the DimMarket struct that corresponds to the provided offer and its buying/selling assets +func extractDimMarket(offer OfferOutput, buyingAsset, sellingAsset string) (DimMarket, error) { + assets := []string{buyingAsset, sellingAsset} + // sort in order to ensure markets have consistent base/counter pairs + // markets are stored as selling/buying == base/counter + sort.Strings(assets) + + fnvHasher := fnv.New64a() + if _, err := fnvHasher.Write([]byte(strings.Join(assets, "/"))); err != nil { + return DimMarket{}, err + } + + hash := fnvHasher.Sum64() + + sellSplit := strings.Split(assets[0], ":") + buySplit := strings.Split(assets[1], ":") + + if len(sellSplit) < 2 { + return DimMarket{}, fmt.Errorf("unable to get sell code and issuer for offer %d", offer.OfferID) + } + + if len(buySplit) < 2 { + return DimMarket{}, fmt.Errorf("unable to get buy code and issuer for offer %d", offer.OfferID) + } + + baseCode, baseIssuer := sellSplit[0], sellSplit[1] + counterCode, counterIssuer := buySplit[0], buySplit[1] + + return DimMarket{ + ID: hash, + BaseCode: baseCode, + BaseIssuer: baseIssuer, + CounterCode: counterCode, + CounterIssuer: counterIssuer, + }, nil +} + +// extractDimOffer extracts the DimOffer struct from the provided offer and its buying/selling assets +func extractDimOffer(offer OfferOutput, buyingAsset, sellingAsset string, marketID, makerID uint64) (DimOffer, error) { + importantFields := fmt.Sprintf("%d/%f/%f", offer.OfferID, offer.Amount, offer.Price) + + fnvHasher := fnv.New64a() + if _, err := fnvHasher.Write([]byte(importantFields)); err != nil { + return DimOffer{}, err + } + + offerHash := fnvHasher.Sum64() + + assets := []string{buyingAsset, sellingAsset} + sort.Strings(assets) + + var action string + if sellingAsset == assets[0] { + action = "s" + } else { + action = "b" + } + + return DimOffer{ + HorizonID: offer.OfferID, + DimOfferID: offerHash, + MarketID: marketID, + MakerID: makerID, + Action: action, + BaseAmount: offer.Amount, + CounterAmount: float64(offer.Amount) * offer.Price, + Price: offer.Price, + }, nil +} + +// extractDimAccount gets the DimAccount struct that corresponds to the provided offer +func extractDimAccount(offer OfferOutput) (DimAccount, error) { + var fnvHasher = fnv.New64a() + if _, err := fnvHasher.Write([]byte(offer.SellerID)); err != nil { + return DimAccount{}, err + } + + accountID := fnvHasher.Sum64() + return DimAccount{ + Address: offer.SellerID, + ID: accountID, + }, nil +} diff --git a/ingest/processors/offer_normalized_test.go b/ingest/processors/offer_normalized_test.go new file mode 100644 index 0000000000..757700ac1a --- /dev/null +++ b/ingest/processors/offer_normalized_test.go @@ -0,0 +1,122 @@ +package processors + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformOfferNormalized(t *testing.T) { + type testInput struct { + change ingest.Change + ledger uint32 + } + type transformTest struct { + input testInput + wantOutput NormalizedOfferOutput + wantErr error + } + + hardCodedInput, err := makeOfferNormalizedTestInput() + assert.NoError(t, err) + hardCodedOutput := makeOfferNormalizedTestOutput() + + tests := []transformTest{ + { + input: testInput{ingest.Change{ + Type: xdr.LedgerEntryTypeOffer, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: xdr.Uint32(100), + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: genericAccountID, + Price: xdr.Price{ + N: 5, + D: 34, + }, + }, + }, + }, + Post: nil, + }, 100}, + wantOutput: NormalizedOfferOutput{}, + wantErr: fmt.Errorf("offer 0 is deleted"), + }, + { + input: testInput{hardCodedInput, 100}, + wantOutput: hardCodedOutput, + wantErr: nil, + }, + } + + for _, test := range tests { + actualOutput, actualError := TransformOfferNormalized(test.input.change, test.input.ledger) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func makeOfferNormalizedTestInput() (ledgerChange ingest.Change, err error) { + ledgerChange = ingest.Change{ + Type: xdr.LedgerEntryTypeOffer, + Pre: nil, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: xdr.Uint32(30715263), + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: testAccount1ID, + OfferId: 260678439, + Selling: nativeAsset, + Buying: ethAsset, + Amount: 2628450327, + Price: xdr.Price{ + N: 920936891, + D: 1790879058, + }, + Flags: 2, + }, + }, + }, + } + return +} + +func makeOfferNormalizedTestOutput() NormalizedOfferOutput { + var dimOfferID, marketID, accountID uint64 + dimOfferID = 16030420496366177311 + marketID = 10357275879248593505 + accountID = 4268167189990212240 + return NormalizedOfferOutput{ + Market: DimMarket{ + ID: marketID, + BaseCode: "ETH", + BaseIssuer: testAccount3Address, + CounterCode: "native", + CounterIssuer: "", + }, + Offer: DimOffer{ + HorizonID: 260678439, + DimOfferID: dimOfferID, + MarketID: marketID, + MakerID: accountID, + Action: "b", + BaseAmount: 262.8450327, + CounterAmount: 135.16473161502083, + Price: 0.5142373444404865, + }, + Account: DimAccount{ + Address: testAccount1Address, + ID: accountID, + }, + Event: FactOfferEvent{ + LedgerSeq: 100, + OfferInstanceID: dimOfferID, + }, + } +} diff --git a/ingest/processors/offer_test.go b/ingest/processors/offer_test.go new file mode 100644 index 0000000000..e4af4b9b68 --- /dev/null +++ b/ingest/processors/offer_test.go @@ -0,0 +1,184 @@ +package processors + +import ( + "fmt" + "testing" + "time" + + "github.com/guregu/null" + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformOffer(t *testing.T) { + type inputStruct struct { + ingest ingest.Change + } + type transformTest struct { + input inputStruct + wantOutput OfferOutput + wantErr error + } + + hardCodedInput, err := makeOfferTestInput() + assert.NoError(t, err) + hardCodedOutput := makeOfferTestOutput() + + tests := []transformTest{ + { + inputStruct{ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Post: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + }, + }, + }, + }, + OfferOutput{}, fmt.Errorf("could not extract offer data from ledger entry; actual type is LedgerEntryTypeAccount"), + }, + { + inputStruct{wrapOfferEntry(xdr.OfferEntry{ + SellerId: genericAccountID, + OfferId: -1, + }, 0), + }, + OfferOutput{}, fmt.Errorf("offerID is negative (-1) for offer from account: %s", genericAccountAddress), + }, + { + inputStruct{wrapOfferEntry(xdr.OfferEntry{ + SellerId: genericAccountID, + Amount: -2, + }, 0), + }, + OfferOutput{}, fmt.Errorf("amount is negative (-2) for offer 0"), + }, + { + inputStruct{wrapOfferEntry(xdr.OfferEntry{ + SellerId: genericAccountID, + Price: xdr.Price{ + N: -3, + D: 10, + }, + }, 0), + }, + OfferOutput{}, fmt.Errorf("price numerator is negative (-3) for offer 0"), + }, + { + inputStruct{wrapOfferEntry(xdr.OfferEntry{ + SellerId: genericAccountID, + Price: xdr.Price{ + N: 5, + D: -4, + }, + }, 0), + }, + OfferOutput{}, fmt.Errorf("price denominator is negative (-4) for offer 0"), + }, + { + inputStruct{wrapOfferEntry(xdr.OfferEntry{ + SellerId: genericAccountID, + Price: xdr.Price{ + N: 5, + D: 0, + }, + }, 0), + }, + OfferOutput{}, fmt.Errorf("price denominator is 0 for offer 0"), + }, + { + inputStruct{ + hardCodedInput, + }, + hardCodedOutput, nil, + }, + } + + for _, test := range tests { + header := xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + ScpValue: xdr.StellarValue{ + CloseTime: 1000, + }, + LedgerSeq: 10, + }, + } + actualOutput, actualError := TransformOffer(test.input.ingest, header) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func wrapOfferEntry(offerEntry xdr.OfferEntry, lastModified int) ingest.Change { + return ingest.Change{ + Type: xdr.LedgerEntryTypeOffer, + Pre: nil, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: xdr.Uint32(lastModified), + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &offerEntry, + }, + }, + } +} + +func makeOfferTestInput() (ledgerChange ingest.Change, err error) { + ledgerChange = ingest.Change{ + Type: xdr.LedgerEntryTypeOffer, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: xdr.Uint32(30715263), + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: testAccount1ID, + OfferId: 260678439, + Selling: nativeAsset, + Buying: ethAsset, + Amount: 2628450327, + Price: xdr.Price{ + N: 920936891, + D: 1790879058, + }, + Flags: 2, + }, + }, + Ext: xdr.LedgerEntryExt{ + V: 1, + V1: &xdr.LedgerEntryExtensionV1{ + SponsoringId: &testAccount3ID, + }, + }, + }, + Post: nil, + } + return +} + +func makeOfferTestOutput() OfferOutput { + return OfferOutput{ + SellerID: testAccount1Address, + OfferID: 260678439, + SellingAssetType: "native", + SellingAssetCode: "", + SellingAssetIssuer: "", + SellingAssetID: -5706705804583548011, + BuyingAssetType: "credit_alphanum4", + BuyingAssetCode: "ETH", + BuyingAssetIssuer: testAccount3Address, + BuyingAssetID: 4476940172956910889, + Amount: 262.8450327, + PriceN: 920936891, + PriceD: 1790879058, + Price: 0.5142373444404865, + Flags: 2, + LastModifiedLedger: 30715263, + LedgerEntryChange: 2, + Deleted: true, + Sponsor: null.StringFrom(testAccount3Address), + LedgerSequence: 10, + ClosedAt: time.Date(1970, time.January, 1, 0, 16, 40, 0, time.UTC), + } +} diff --git a/ingest/processors/operation.go b/ingest/processors/operation.go new file mode 100644 index 0000000000..80a43493d0 --- /dev/null +++ b/ingest/processors/operation.go @@ -0,0 +1,2205 @@ +package processors + +import ( + "encoding/base64" + "fmt" + "strconv" + "time" + + "github.com/guregu/null" + "github.com/pkg/errors" + + "github.com/stellar/go/amount" + "github.com/stellar/go/ingest" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/strkey" + "github.com/stellar/go/toid" + "github.com/stellar/go/xdr" + + "github.com/stellar/go/support/contractevents" +) + +type liquidityPoolDelta struct { + ReserveA xdr.Int64 + ReserveB xdr.Int64 + TotalPoolShares xdr.Int64 +} + +// TransformOperation converts an operation from the history archive ingestion system into a form suitable for BigQuery +func TransformOperation(operation xdr.Operation, operationIndex int32, transaction ingest.LedgerTransaction, ledgerSeq int32, ledgerCloseMeta xdr.LedgerCloseMeta, network string) (OperationOutput, error) { + outputTransactionID := toid.New(ledgerSeq, int32(transaction.Index), 0).ToInt64() + outputOperationID := toid.New(ledgerSeq, int32(transaction.Index), operationIndex+1).ToInt64() //operationIndex needs +1 increment to stay in sync with ingest package + + sourceAccount := getOperationSourceAccount(operation, transaction) + outputSourceAccount, err := GetAccountAddressFromMuxedAccount(sourceAccount) + if err != nil { + return OperationOutput{}, fmt.Errorf("for operation %d (ledger id=%d): %v", operationIndex, outputOperationID, err) + } + + var outputSourceAccountMuxed null.String + if sourceAccount.Type == xdr.CryptoKeyTypeKeyTypeMuxedEd25519 { + var muxedAddress string + muxedAddress, err = sourceAccount.GetAddress() + if err != nil { + return OperationOutput{}, err + } + outputSourceAccountMuxed = null.StringFrom(muxedAddress) + } + + outputOperationType := int32(operation.Body.Type) + if outputOperationType < 0 { + return OperationOutput{}, fmt.Errorf("the operation type (%d) is negative for operation %d (operation id=%d)", outputOperationType, operationIndex, outputOperationID) + } + + outputDetails, err := extractOperationDetails(operation, transaction, operationIndex, network) + if err != nil { + return OperationOutput{}, err + } + + outputOperationTypeString, err := mapOperationType(operation) + if err != nil { + return OperationOutput{}, err + } + + outputCloseTime, err := GetCloseTime(ledgerCloseMeta) + if err != nil { + return OperationOutput{}, err + } + + var outputOperationResultCode string + var outputOperationTraceCode string + outputOperationResults, ok := transaction.Result.Result.OperationResults() + if ok { + outputOperationResultCode = outputOperationResults[operationIndex].Code.String() + operationResultTr, ok := outputOperationResults[operationIndex].GetTr() + if ok { + outputOperationTraceCode, err = mapOperationTrace(operationResultTr) + if err != nil { + return OperationOutput{}, err + } + } + } + + outputLedgerSequence := GetLedgerSequence(ledgerCloseMeta) + + transformedOperation := OperationOutput{ + SourceAccount: outputSourceAccount, + SourceAccountMuxed: outputSourceAccountMuxed.String, + Type: outputOperationType, + TypeString: outputOperationTypeString, + TransactionID: outputTransactionID, + OperationID: outputOperationID, + OperationDetails: outputDetails, + ClosedAt: outputCloseTime, + OperationResultCode: outputOperationResultCode, + OperationTraceCode: outputOperationTraceCode, + LedgerSequence: outputLedgerSequence, + OperationDetailsJSON: outputDetails, + } + + return transformedOperation, nil +} + +func mapOperationType(operation xdr.Operation) (string, error) { + var op_string_type string + operationType := operation.Body.Type + + switch operationType { + case xdr.OperationTypeCreateAccount: + op_string_type = "create_account" + case xdr.OperationTypePayment: + op_string_type = "payment" + case xdr.OperationTypePathPaymentStrictReceive: + op_string_type = "path_payment_strict_receive" + case xdr.OperationTypePathPaymentStrictSend: + op_string_type = "path_payment_strict_send" + case xdr.OperationTypeManageBuyOffer: + op_string_type = "manage_buy_offer" + case xdr.OperationTypeManageSellOffer: + op_string_type = "manage_sell_offer" + case xdr.OperationTypeCreatePassiveSellOffer: + op_string_type = "create_passive_sell_offer" + case xdr.OperationTypeSetOptions: + op_string_type = "set_options" + case xdr.OperationTypeChangeTrust: + op_string_type = "change_trust" + case xdr.OperationTypeAllowTrust: + op_string_type = "allow_trust" + case xdr.OperationTypeAccountMerge: + op_string_type = "account_merge" + case xdr.OperationTypeInflation: + op_string_type = "inflation" + case xdr.OperationTypeManageData: + op_string_type = "manage_data" + case xdr.OperationTypeBumpSequence: + op_string_type = "bump_sequence" + case xdr.OperationTypeCreateClaimableBalance: + op_string_type = "create_claimable_balance" + case xdr.OperationTypeClaimClaimableBalance: + op_string_type = "claim_claimable_balance" + case xdr.OperationTypeBeginSponsoringFutureReserves: + op_string_type = "begin_sponsoring_future_reserves" + case xdr.OperationTypeEndSponsoringFutureReserves: + op_string_type = "end_sponsoring_future_reserves" + case xdr.OperationTypeRevokeSponsorship: + op_string_type = "revoke_sponsorship" + case xdr.OperationTypeClawback: + op_string_type = "clawback" + case xdr.OperationTypeClawbackClaimableBalance: + op_string_type = "clawback_claimable_balance" + case xdr.OperationTypeSetTrustLineFlags: + op_string_type = "set_trust_line_flags" + case xdr.OperationTypeLiquidityPoolDeposit: + op_string_type = "liquidity_pool_deposit" + case xdr.OperationTypeLiquidityPoolWithdraw: + op_string_type = "liquidity_pool_withdraw" + case xdr.OperationTypeInvokeHostFunction: + op_string_type = "invoke_host_function" + case xdr.OperationTypeExtendFootprintTtl: + op_string_type = "extend_footprint_ttl" + case xdr.OperationTypeRestoreFootprint: + op_string_type = "restore_footprint" + default: + return op_string_type, fmt.Errorf("unknown operation type: %s", operation.Body.Type.String()) + } + return op_string_type, nil +} + +func mapOperationTrace(operationTrace xdr.OperationResultTr) (string, error) { + var operationTraceDescription string + operationType := operationTrace.Type + + switch operationType { + case xdr.OperationTypeCreateAccount: + operationTraceDescription = operationTrace.CreateAccountResult.Code.String() + case xdr.OperationTypePayment: + operationTraceDescription = operationTrace.PaymentResult.Code.String() + case xdr.OperationTypePathPaymentStrictReceive: + operationTraceDescription = operationTrace.PathPaymentStrictReceiveResult.Code.String() + case xdr.OperationTypePathPaymentStrictSend: + operationTraceDescription = operationTrace.PathPaymentStrictSendResult.Code.String() + case xdr.OperationTypeManageBuyOffer: + operationTraceDescription = operationTrace.ManageBuyOfferResult.Code.String() + case xdr.OperationTypeManageSellOffer: + operationTraceDescription = operationTrace.ManageSellOfferResult.Code.String() + case xdr.OperationTypeCreatePassiveSellOffer: + operationTraceDescription = operationTrace.CreatePassiveSellOfferResult.Code.String() + case xdr.OperationTypeSetOptions: + operationTraceDescription = operationTrace.SetOptionsResult.Code.String() + case xdr.OperationTypeChangeTrust: + operationTraceDescription = operationTrace.ChangeTrustResult.Code.String() + case xdr.OperationTypeAllowTrust: + operationTraceDescription = operationTrace.AllowTrustResult.Code.String() + case xdr.OperationTypeAccountMerge: + operationTraceDescription = operationTrace.AccountMergeResult.Code.String() + case xdr.OperationTypeInflation: + operationTraceDescription = operationTrace.InflationResult.Code.String() + case xdr.OperationTypeManageData: + operationTraceDescription = operationTrace.ManageDataResult.Code.String() + case xdr.OperationTypeBumpSequence: + operationTraceDescription = operationTrace.BumpSeqResult.Code.String() + case xdr.OperationTypeCreateClaimableBalance: + operationTraceDescription = operationTrace.CreateClaimableBalanceResult.Code.String() + case xdr.OperationTypeClaimClaimableBalance: + operationTraceDescription = operationTrace.ClaimClaimableBalanceResult.Code.String() + case xdr.OperationTypeBeginSponsoringFutureReserves: + operationTraceDescription = operationTrace.BeginSponsoringFutureReservesResult.Code.String() + case xdr.OperationTypeEndSponsoringFutureReserves: + operationTraceDescription = operationTrace.EndSponsoringFutureReservesResult.Code.String() + case xdr.OperationTypeRevokeSponsorship: + operationTraceDescription = operationTrace.RevokeSponsorshipResult.Code.String() + case xdr.OperationTypeClawback: + operationTraceDescription = operationTrace.ClawbackResult.Code.String() + case xdr.OperationTypeClawbackClaimableBalance: + operationTraceDescription = operationTrace.ClawbackClaimableBalanceResult.Code.String() + case xdr.OperationTypeSetTrustLineFlags: + operationTraceDescription = operationTrace.SetTrustLineFlagsResult.Code.String() + case xdr.OperationTypeLiquidityPoolDeposit: + operationTraceDescription = operationTrace.LiquidityPoolDepositResult.Code.String() + case xdr.OperationTypeLiquidityPoolWithdraw: + operationTraceDescription = operationTrace.LiquidityPoolWithdrawResult.Code.String() + case xdr.OperationTypeInvokeHostFunction: + operationTraceDescription = operationTrace.InvokeHostFunctionResult.Code.String() + case xdr.OperationTypeExtendFootprintTtl: + operationTraceDescription = operationTrace.ExtendFootprintTtlResult.Code.String() + case xdr.OperationTypeRestoreFootprint: + operationTraceDescription = operationTrace.RestoreFootprintResult.Code.String() + default: + return operationTraceDescription, fmt.Errorf("unknown operation type: %s", operationTrace.Type.String()) + } + return operationTraceDescription, nil +} + +func PoolIDToString(id xdr.PoolId) string { + return xdr.Hash(id).HexString() +} + +// operation xdr.Operation, operationIndex int32, transaction ingest.LedgerTransaction, ledgerSeq int32 +func getLiquidityPoolAndProductDelta(operationIndex int32, transaction ingest.LedgerTransaction, lpID *xdr.PoolId) (*xdr.LiquidityPoolEntry, *liquidityPoolDelta, error) { + changes, err := transaction.GetOperationChanges(uint32(operationIndex)) + if err != nil { + return nil, nil, err + } + + for _, c := range changes { + if c.Type != xdr.LedgerEntryTypeLiquidityPool { + continue + } + // The delta can be caused by a full removal or full creation of the liquidity pool + var lp *xdr.LiquidityPoolEntry + var preA, preB, preShares xdr.Int64 + if c.Pre != nil { + if lpID != nil && c.Pre.Data.LiquidityPool.LiquidityPoolId != *lpID { + // if we were looking for specific pool id, then check on it + continue + } + lp = c.Pre.Data.LiquidityPool + if c.Pre.Data.LiquidityPool.Body.Type != xdr.LiquidityPoolTypeLiquidityPoolConstantProduct { + return nil, nil, fmt.Errorf("unexpected liquity pool body type %d", c.Pre.Data.LiquidityPool.Body.Type) + } + cpPre := c.Pre.Data.LiquidityPool.Body.ConstantProduct + preA, preB, preShares = cpPre.ReserveA, cpPre.ReserveB, cpPre.TotalPoolShares + } + var postA, postB, postShares xdr.Int64 + if c.Post != nil { + if lpID != nil && c.Post.Data.LiquidityPool.LiquidityPoolId != *lpID { + // if we were looking for specific pool id, then check on it + continue + } + lp = c.Post.Data.LiquidityPool + if c.Post.Data.LiquidityPool.Body.Type != xdr.LiquidityPoolTypeLiquidityPoolConstantProduct { + return nil, nil, fmt.Errorf("unexpected liquity pool body type %d", c.Post.Data.LiquidityPool.Body.Type) + } + cpPost := c.Post.Data.LiquidityPool.Body.ConstantProduct + postA, postB, postShares = cpPost.ReserveA, cpPost.ReserveB, cpPost.TotalPoolShares + } + delta := &liquidityPoolDelta{ + ReserveA: postA - preA, + ReserveB: postB - preB, + TotalPoolShares: postShares - preShares, + } + return lp, delta, nil + } + + return nil, nil, fmt.Errorf("liquidity pool change not found") +} + +func getOperationSourceAccount(operation xdr.Operation, transaction ingest.LedgerTransaction) xdr.MuxedAccount { + sourceAccount := operation.SourceAccount + if sourceAccount != nil { + return *sourceAccount + } + + return transaction.Envelope.SourceAccount() +} + +func getSponsor(operation xdr.Operation, transaction ingest.LedgerTransaction, operationIndex int32) (*xdr.AccountId, error) { + changes, err := transaction.GetOperationChanges(uint32(operationIndex)) + if err != nil { + return nil, err + } + var signerKey string + if setOps, ok := operation.Body.GetSetOptionsOp(); ok && setOps.Signer != nil { + signerKey = setOps.Signer.Key.Address() + } + + for _, c := range changes { + // Check Signer changes + if signerKey != "" { + if sponsorAccount := getSignerSponsorInChange(signerKey, c); sponsorAccount != nil { + return sponsorAccount, nil + } + } + + // Check Ledger key changes + if c.Pre != nil || c.Post == nil { + // We are only looking for entry creations denoting that a sponsor + // is associated to the ledger entry of the operation. + continue + } + if sponsorAccount := c.Post.SponsoringID(); sponsorAccount != nil { + return sponsorAccount, nil + } + } + + return nil, nil +} + +func getSignerSponsorInChange(signerKey string, change ingest.Change) xdr.SponsorshipDescriptor { + if change.Type != xdr.LedgerEntryTypeAccount || change.Post == nil { + return nil + } + + preSigners := map[string]xdr.AccountId{} + if change.Pre != nil { + account := change.Pre.Data.MustAccount() + preSigners = account.SponsorPerSigner() + } + + account := change.Post.Data.MustAccount() + postSigners := account.SponsorPerSigner() + + pre, preFound := preSigners[signerKey] + post, postFound := postSigners[signerKey] + + if !postFound { + return nil + } + + if preFound { + formerSponsor := pre.Address() + newSponsor := post.Address() + if formerSponsor == newSponsor { + return nil + } + } + + return &post +} + +func formatPrefix(p string) string { + if p != "" { + p += "_" + } + return p +} + +func addAssetDetailsToOperationDetails(result map[string]interface{}, asset xdr.Asset, prefix string) error { + var assetType, code, issuer string + err := asset.Extract(&assetType, &code, &issuer) + if err != nil { + return err + } + + prefix = formatPrefix(prefix) + result[prefix+"asset_type"] = assetType + + if asset.Type == xdr.AssetTypeAssetTypeNative { + result[prefix+"asset_id"] = int64(-5706705804583548011) + return nil + } + + result[prefix+"asset_code"] = code + result[prefix+"asset_issuer"] = issuer + result[prefix+"asset_id"] = FarmHashAsset(code, issuer, assetType) + + return nil +} + +func addLiquidityPoolAssetDetails(result map[string]interface{}, lpp xdr.LiquidityPoolParameters) error { + result["asset_type"] = "liquidity_pool_shares" + if lpp.Type != xdr.LiquidityPoolTypeLiquidityPoolConstantProduct { + return fmt.Errorf("unknown liquidity pool type %d", lpp.Type) + } + cp := lpp.ConstantProduct + poolID, err := xdr.NewPoolId(cp.AssetA, cp.AssetB, cp.Fee) + if err != nil { + return err + } + result["liquidity_pool_id"] = PoolIDToString(poolID) + return nil +} + +func addPriceDetails(result map[string]interface{}, price xdr.Price, prefix string) error { + prefix = formatPrefix(prefix) + parsedPrice, err := strconv.ParseFloat(price.String(), 64) + if err != nil { + return err + } + result[prefix+"price"] = parsedPrice + result[prefix+"price_r"] = Price{ + Numerator: int32(price.N), + Denominator: int32(price.D), + } + return nil +} + +func addAccountAndMuxedAccountDetails(result map[string]interface{}, a xdr.MuxedAccount, prefix string) error { + account_id := a.ToAccountId() + result[prefix] = account_id.Address() + prefix = formatPrefix(prefix) + if a.Type == xdr.CryptoKeyTypeKeyTypeMuxedEd25519 { + muxedAccountAddress, err := a.GetAddress() + if err != nil { + return err + } + result[prefix+"muxed"] = muxedAccountAddress + muxedAccountId, err := a.GetId() + if err != nil { + return err + } + result[prefix+"muxed_id"] = muxedAccountId + } + return nil +} + +func addTrustLineFlagToDetails(result map[string]interface{}, f xdr.TrustLineFlags, prefix string) { + var ( + n []int32 + s []string + ) + + if f.IsAuthorized() { + n = append(n, int32(xdr.TrustLineFlagsAuthorizedFlag)) + s = append(s, "authorized") + } + + if f.IsAuthorizedToMaintainLiabilitiesFlag() { + n = append(n, int32(xdr.TrustLineFlagsAuthorizedToMaintainLiabilitiesFlag)) + s = append(s, "authorized_to_maintain_liabilities") + } + + if f.IsClawbackEnabledFlag() { + n = append(n, int32(xdr.TrustLineFlagsTrustlineClawbackEnabledFlag)) + s = append(s, "clawback_enabled") + } + + prefix = formatPrefix(prefix) + result[prefix+"flags"] = n + result[prefix+"flags_s"] = s +} + +func addLedgerKeyToDetails(result map[string]interface{}, ledgerKey xdr.LedgerKey) error { + switch ledgerKey.Type { + case xdr.LedgerEntryTypeAccount: + result["account_id"] = ledgerKey.Account.AccountId.Address() + case xdr.LedgerEntryTypeClaimableBalance: + marshalHex, err := xdr.MarshalHex(ledgerKey.ClaimableBalance.BalanceId) + if err != nil { + return errors.Wrapf(err, "in claimable balance") + } + result["claimable_balance_id"] = marshalHex + case xdr.LedgerEntryTypeData: + result["data_account_id"] = ledgerKey.Data.AccountId.Address() + result["data_name"] = string(ledgerKey.Data.DataName) + case xdr.LedgerEntryTypeOffer: + result["offer_id"] = int64(ledgerKey.Offer.OfferId) + case xdr.LedgerEntryTypeTrustline: + result["trustline_account_id"] = ledgerKey.TrustLine.AccountId.Address() + if ledgerKey.TrustLine.Asset.Type == xdr.AssetTypeAssetTypePoolShare { + result["trustline_liquidity_pool_id"] = PoolIDToString(*ledgerKey.TrustLine.Asset.LiquidityPoolId) + } else { + result["trustline_asset"] = ledgerKey.TrustLine.Asset.ToAsset().StringCanonical() + } + case xdr.LedgerEntryTypeLiquidityPool: + result["liquidity_pool_id"] = PoolIDToString(ledgerKey.LiquidityPool.LiquidityPoolId) + } + return nil +} + +func transformPath(initialPath []xdr.Asset) []Path { + if len(initialPath) == 0 { + return nil + } + var path = make([]Path, 0) + for _, pathAsset := range initialPath { + var assetType, code, issuer string + err := pathAsset.Extract(&assetType, &code, &issuer) + if err != nil { + return nil + } + + path = append(path, Path{ + AssetType: assetType, + AssetIssuer: issuer, + AssetCode: code, + }) + } + return path +} + +func findInitatingBeginSponsoringOp(operation xdr.Operation, operationIndex int32, transaction ingest.LedgerTransaction) *SponsorshipOutput { + if !transaction.Result.Successful() { + // Failed transactions may not have a compliant sandwich structure + // we can rely on (e.g. invalid nesting or a being operation with the wrong sponsoree ID) + // and thus we bail out since we could return incorrect information. + return nil + } + sponsoree := getOperationSourceAccount(operation, transaction).ToAccountId() + operations := transaction.Envelope.Operations() + for i := int(operationIndex) - 1; i >= 0; i-- { + if beginOp, ok := operations[i].Body.GetBeginSponsoringFutureReservesOp(); ok && + beginOp.SponsoredId.Address() == sponsoree.Address() { + result := SponsorshipOutput{ + Operation: operations[i], + OperationIndex: uint32(i), + } + return &result + } + } + return nil +} + +func addOperationFlagToOperationDetails(result map[string]interface{}, flag uint32, prefix string) { + intFlags := make([]int32, 0) + stringFlags := make([]string, 0) + + if (int64(flag) & int64(xdr.AccountFlagsAuthRequiredFlag)) > 0 { + intFlags = append(intFlags, int32(xdr.AccountFlagsAuthRequiredFlag)) + stringFlags = append(stringFlags, "auth_required") + } + + if (int64(flag) & int64(xdr.AccountFlagsAuthRevocableFlag)) > 0 { + intFlags = append(intFlags, int32(xdr.AccountFlagsAuthRevocableFlag)) + stringFlags = append(stringFlags, "auth_revocable") + } + + if (int64(flag) & int64(xdr.AccountFlagsAuthImmutableFlag)) > 0 { + intFlags = append(intFlags, int32(xdr.AccountFlagsAuthImmutableFlag)) + stringFlags = append(stringFlags, "auth_immutable") + } + + if (int64(flag) & int64(xdr.AccountFlagsAuthClawbackEnabledFlag)) > 0 { + intFlags = append(intFlags, int32(xdr.AccountFlagsAuthClawbackEnabledFlag)) + stringFlags = append(stringFlags, "auth_clawback_enabled") + } + + prefix = formatPrefix(prefix) + result[prefix+"flags"] = intFlags + result[prefix+"flags_s"] = stringFlags +} + +func extractOperationDetails(operation xdr.Operation, transaction ingest.LedgerTransaction, operationIndex int32, network string) (map[string]interface{}, error) { + details := map[string]interface{}{} + sourceAccount := getOperationSourceAccount(operation, transaction) + operationType := operation.Body.Type + + switch operationType { + case xdr.OperationTypeCreateAccount: + op, ok := operation.Body.GetCreateAccountOp() + if !ok { + return details, fmt.Errorf("could not access CreateAccount info for this operation (index %d)", operationIndex) + } + + if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "funder"); err != nil { + return details, err + } + details["account"] = op.Destination.Address() + details["starting_balance"] = ConvertStroopValueToReal(op.StartingBalance) + + case xdr.OperationTypePayment: + op, ok := operation.Body.GetPaymentOp() + if !ok { + return details, fmt.Errorf("could not access Payment info for this operation (index %d)", operationIndex) + } + + if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "from"); err != nil { + return details, err + } + if err := addAccountAndMuxedAccountDetails(details, op.Destination, "to"); err != nil { + return details, err + } + details["amount"] = ConvertStroopValueToReal(op.Amount) + if err := addAssetDetailsToOperationDetails(details, op.Asset, ""); err != nil { + return details, err + } + + case xdr.OperationTypePathPaymentStrictReceive: + op, ok := operation.Body.GetPathPaymentStrictReceiveOp() + if !ok { + return details, fmt.Errorf("could not access PathPaymentStrictReceive info for this operation (index %d)", operationIndex) + } + + if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "from"); err != nil { + return details, err + } + if err := addAccountAndMuxedAccountDetails(details, op.Destination, "to"); err != nil { + return details, err + } + details["amount"] = ConvertStroopValueToReal(op.DestAmount) + details["source_amount"] = amount.String(0) + details["source_max"] = ConvertStroopValueToReal(op.SendMax) + if err := addAssetDetailsToOperationDetails(details, op.DestAsset, ""); err != nil { + return details, err + } + if err := addAssetDetailsToOperationDetails(details, op.SendAsset, "source"); err != nil { + return details, err + } + + if transaction.Result.Successful() { + allOperationResults, ok := transaction.Result.OperationResults() + if !ok { + return details, fmt.Errorf("could not access any results for this transaction") + } + currentOperationResult := allOperationResults[operationIndex] + resultBody, ok := currentOperationResult.GetTr() + if !ok { + return details, fmt.Errorf("could not access result body for this operation (index %d)", operationIndex) + } + result, ok := resultBody.GetPathPaymentStrictReceiveResult() + if !ok { + return details, fmt.Errorf("could not access PathPaymentStrictReceive result info for this operation (index %d)", operationIndex) + } + details["source_amount"] = ConvertStroopValueToReal(result.SendAmount()) + } + + details["path"] = transformPath(op.Path) + + case xdr.OperationTypePathPaymentStrictSend: + op, ok := operation.Body.GetPathPaymentStrictSendOp() + if !ok { + return details, fmt.Errorf("could not access PathPaymentStrictSend info for this operation (index %d)", operationIndex) + } + + if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "from"); err != nil { + return details, err + } + if err := addAccountAndMuxedAccountDetails(details, op.Destination, "to"); err != nil { + return details, err + } + details["amount"] = amount.String(0) + details["source_amount"] = ConvertStroopValueToReal(op.SendAmount) + details["destination_min"] = amount.String(op.DestMin) + if err := addAssetDetailsToOperationDetails(details, op.DestAsset, ""); err != nil { + return details, err + } + if err := addAssetDetailsToOperationDetails(details, op.SendAsset, "source"); err != nil { + return details, err + } + + if transaction.Result.Successful() { + allOperationResults, ok := transaction.Result.OperationResults() + if !ok { + return details, fmt.Errorf("could not access any results for this transaction") + } + currentOperationResult := allOperationResults[operationIndex] + resultBody, ok := currentOperationResult.GetTr() + if !ok { + return details, fmt.Errorf("could not access result body for this operation (index %d)", operationIndex) + } + result, ok := resultBody.GetPathPaymentStrictSendResult() + if !ok { + return details, fmt.Errorf("could not access GetPathPaymentStrictSendResult result info for this operation (index %d)", operationIndex) + } + details["amount"] = ConvertStroopValueToReal(result.DestAmount()) + } + + details["path"] = transformPath(op.Path) + + case xdr.OperationTypeManageBuyOffer: + op, ok := operation.Body.GetManageBuyOfferOp() + if !ok { + return details, fmt.Errorf("could not access ManageBuyOffer info for this operation (index %d)", operationIndex) + } + + details["offer_id"] = int64(op.OfferId) + details["amount"] = ConvertStroopValueToReal(op.BuyAmount) + if err := addPriceDetails(details, op.Price, ""); err != nil { + return details, err + } + + if err := addAssetDetailsToOperationDetails(details, op.Buying, "buying"); err != nil { + return details, err + } + if err := addAssetDetailsToOperationDetails(details, op.Selling, "selling"); err != nil { + return details, err + } + + case xdr.OperationTypeManageSellOffer: + op, ok := operation.Body.GetManageSellOfferOp() + if !ok { + return details, fmt.Errorf("could not access ManageSellOffer info for this operation (index %d)", operationIndex) + } + + details["offer_id"] = int64(op.OfferId) + details["amount"] = ConvertStroopValueToReal(op.Amount) + if err := addPriceDetails(details, op.Price, ""); err != nil { + return details, err + } + + if err := addAssetDetailsToOperationDetails(details, op.Buying, "buying"); err != nil { + return details, err + } + if err := addAssetDetailsToOperationDetails(details, op.Selling, "selling"); err != nil { + return details, err + } + + case xdr.OperationTypeCreatePassiveSellOffer: + op, ok := operation.Body.GetCreatePassiveSellOfferOp() + if !ok { + return details, fmt.Errorf("could not access CreatePassiveSellOffer info for this operation (index %d)", operationIndex) + } + + details["amount"] = ConvertStroopValueToReal(op.Amount) + if err := addPriceDetails(details, op.Price, ""); err != nil { + return details, err + } + + if err := addAssetDetailsToOperationDetails(details, op.Buying, "buying"); err != nil { + return details, err + } + if err := addAssetDetailsToOperationDetails(details, op.Selling, "selling"); err != nil { + return details, err + } + + case xdr.OperationTypeSetOptions: + op, ok := operation.Body.GetSetOptionsOp() + if !ok { + return details, fmt.Errorf("could not access GetSetOptions info for this operation (index %d)", operationIndex) + } + + if op.InflationDest != nil { + details["inflation_dest"] = op.InflationDest.Address() + } + + if op.SetFlags != nil && *op.SetFlags > 0 { + addOperationFlagToOperationDetails(details, uint32(*op.SetFlags), "set") + } + + if op.ClearFlags != nil && *op.ClearFlags > 0 { + addOperationFlagToOperationDetails(details, uint32(*op.ClearFlags), "clear") + } + + if op.MasterWeight != nil { + details["master_key_weight"] = uint32(*op.MasterWeight) + } + + if op.LowThreshold != nil { + details["low_threshold"] = uint32(*op.LowThreshold) + } + + if op.MedThreshold != nil { + details["med_threshold"] = uint32(*op.MedThreshold) + } + + if op.HighThreshold != nil { + details["high_threshold"] = uint32(*op.HighThreshold) + } + + if op.HomeDomain != nil { + details["home_domain"] = string(*op.HomeDomain) + } + + if op.Signer != nil { + details["signer_key"] = op.Signer.Key.Address() + details["signer_weight"] = uint32(op.Signer.Weight) + } + + case xdr.OperationTypeChangeTrust: + op, ok := operation.Body.GetChangeTrustOp() + if !ok { + return details, fmt.Errorf("could not access GetChangeTrust info for this operation (index %d)", operationIndex) + } + + if op.Line.Type == xdr.AssetTypeAssetTypePoolShare { + if err := addLiquidityPoolAssetDetails(details, *op.Line.LiquidityPool); err != nil { + return details, err + } + } else { + if err := addAssetDetailsToOperationDetails(details, op.Line.ToAsset(), ""); err != nil { + return details, err + } + details["trustee"] = details["asset_issuer"] + } + + if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "trustor"); err != nil { + return details, err + } + details["limit"] = ConvertStroopValueToReal(op.Limit) + + case xdr.OperationTypeAllowTrust: + op, ok := operation.Body.GetAllowTrustOp() + if !ok { + return details, fmt.Errorf("could not access AllowTrust info for this operation (index %d)", operationIndex) + } + + if err := addAssetDetailsToOperationDetails(details, op.Asset.ToAsset(sourceAccount.ToAccountId()), ""); err != nil { + return details, err + } + if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "trustee"); err != nil { + return details, err + } + details["trustor"] = op.Trustor.Address() + shouldAuth := xdr.TrustLineFlags(op.Authorize).IsAuthorized() + details["authorize"] = shouldAuth + shouldAuthLiabilities := xdr.TrustLineFlags(op.Authorize).IsAuthorizedToMaintainLiabilitiesFlag() + if shouldAuthLiabilities { + details["authorize_to_maintain_liabilities"] = shouldAuthLiabilities + } + shouldClawbackEnabled := xdr.TrustLineFlags(op.Authorize).IsClawbackEnabledFlag() + if shouldClawbackEnabled { + details["clawback_enabled"] = shouldClawbackEnabled + } + + case xdr.OperationTypeAccountMerge: + destinationAccount, ok := operation.Body.GetDestination() + if !ok { + return details, fmt.Errorf("could not access Destination info for this operation (index %d)", operationIndex) + } + + if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "account"); err != nil { + return details, err + } + if err := addAccountAndMuxedAccountDetails(details, destinationAccount, "into"); err != nil { + return details, err + } + + case xdr.OperationTypeInflation: + // Inflation operations don't have information that affects the details struct + case xdr.OperationTypeManageData: + op, ok := operation.Body.GetManageDataOp() + if !ok { + return details, fmt.Errorf("could not access GetManageData info for this operation (index %d)", operationIndex) + } + + details["name"] = string(op.DataName) + if op.DataValue != nil { + details["value"] = base64.StdEncoding.EncodeToString(*op.DataValue) + } else { + details["value"] = nil + } + + case xdr.OperationTypeBumpSequence: + op, ok := operation.Body.GetBumpSequenceOp() + if !ok { + return details, fmt.Errorf("could not access BumpSequence info for this operation (index %d)", operationIndex) + } + details["bump_to"] = fmt.Sprintf("%d", op.BumpTo) + + case xdr.OperationTypeCreateClaimableBalance: + op := operation.Body.MustCreateClaimableBalanceOp() + details["asset"] = op.Asset.StringCanonical() + details["amount"] = ConvertStroopValueToReal(op.Amount) + details["claimants"] = transformClaimants(op.Claimants) + + case xdr.OperationTypeClaimClaimableBalance: + op := operation.Body.MustClaimClaimableBalanceOp() + balanceID, err := xdr.MarshalHex(op.BalanceId) + if err != nil { + return details, fmt.Errorf("invalid balanceId in op: %d", operationIndex) + } + details["balance_id"] = balanceID + if err := addAccountAndMuxedAccountDetails(details, sourceAccount, "claimant"); err != nil { + return details, err + } + + case xdr.OperationTypeBeginSponsoringFutureReserves: + op := operation.Body.MustBeginSponsoringFutureReservesOp() + details["sponsored_id"] = op.SponsoredId.Address() + + case xdr.OperationTypeEndSponsoringFutureReserves: + beginSponsorOp := findInitatingBeginSponsoringOp(operation, operationIndex, transaction) + if beginSponsorOp != nil { + beginSponsorshipSource := getOperationSourceAccount(beginSponsorOp.Operation, transaction) + if err := addAccountAndMuxedAccountDetails(details, beginSponsorshipSource, "begin_sponsor"); err != nil { + return details, err + } + } + + case xdr.OperationTypeRevokeSponsorship: + op := operation.Body.MustRevokeSponsorshipOp() + switch op.Type { + case xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry: + if err := addLedgerKeyToDetails(details, *op.LedgerKey); err != nil { + return details, err + } + case xdr.RevokeSponsorshipTypeRevokeSponsorshipSigner: + details["signer_account_id"] = op.Signer.AccountId.Address() + details["signer_key"] = op.Signer.SignerKey.Address() + } + + case xdr.OperationTypeClawback: + op := operation.Body.MustClawbackOp() + if err := addAssetDetailsToOperationDetails(details, op.Asset, ""); err != nil { + return details, err + } + if err := addAccountAndMuxedAccountDetails(details, op.From, "from"); err != nil { + return details, err + } + details["amount"] = ConvertStroopValueToReal(op.Amount) + + case xdr.OperationTypeClawbackClaimableBalance: + op := operation.Body.MustClawbackClaimableBalanceOp() + balanceID, err := xdr.MarshalHex(op.BalanceId) + if err != nil { + return details, fmt.Errorf("invalid balanceId in op: %d", operationIndex) + } + details["balance_id"] = balanceID + + case xdr.OperationTypeSetTrustLineFlags: + op := operation.Body.MustSetTrustLineFlagsOp() + details["trustor"] = op.Trustor.Address() + if err := addAssetDetailsToOperationDetails(details, op.Asset, ""); err != nil { + return details, err + } + if op.SetFlags > 0 { + addTrustLineFlagToDetails(details, xdr.TrustLineFlags(op.SetFlags), "set") + + } + if op.ClearFlags > 0 { + addTrustLineFlagToDetails(details, xdr.TrustLineFlags(op.ClearFlags), "clear") + } + + case xdr.OperationTypeLiquidityPoolDeposit: + op := operation.Body.MustLiquidityPoolDepositOp() + details["liquidity_pool_id"] = PoolIDToString(op.LiquidityPoolId) + var ( + assetA, assetB xdr.Asset + depositedA, depositedB xdr.Int64 + sharesReceived xdr.Int64 + ) + if transaction.Result.Successful() { + // we will use the defaults (omitted asset and 0 amounts) if the transaction failed + lp, delta, err := getLiquidityPoolAndProductDelta(operationIndex, transaction, &op.LiquidityPoolId) + if err != nil { + return nil, err + } + params := lp.Body.ConstantProduct.Params + assetA, assetB = params.AssetA, params.AssetB + depositedA, depositedB = delta.ReserveA, delta.ReserveB + sharesReceived = delta.TotalPoolShares + } + + // Process ReserveA Details + if err := addAssetDetailsToOperationDetails(details, assetA, "reserve_a"); err != nil { + return details, err + } + details["reserve_a_max_amount"] = ConvertStroopValueToReal(op.MaxAmountA) + depositA, err := strconv.ParseFloat(amount.String(depositedA), 64) + if err != nil { + return details, err + } + details["reserve_a_deposit_amount"] = depositA + + //Process ReserveB Details + if err = addAssetDetailsToOperationDetails(details, assetB, "reserve_b"); err != nil { + return details, err + } + details["reserve_b_max_amount"] = ConvertStroopValueToReal(op.MaxAmountB) + depositB, err := strconv.ParseFloat(amount.String(depositedB), 64) + if err != nil { + return details, err + } + details["reserve_b_deposit_amount"] = depositB + + if err = addPriceDetails(details, op.MinPrice, "min"); err != nil { + return details, err + } + if err = addPriceDetails(details, op.MaxPrice, "max"); err != nil { + return details, err + } + + sharesToFloat, err := strconv.ParseFloat(amount.String(sharesReceived), 64) + if err != nil { + return details, err + } + details["shares_received"] = sharesToFloat + + case xdr.OperationTypeLiquidityPoolWithdraw: + op := operation.Body.MustLiquidityPoolWithdrawOp() + details["liquidity_pool_id"] = PoolIDToString(op.LiquidityPoolId) + var ( + assetA, assetB xdr.Asset + receivedA, receivedB xdr.Int64 + ) + if transaction.Result.Successful() { + // we will use the defaults (omitted asset and 0 amounts) if the transaction failed + lp, delta, err := getLiquidityPoolAndProductDelta(operationIndex, transaction, &op.LiquidityPoolId) + if err != nil { + return nil, err + } + params := lp.Body.ConstantProduct.Params + assetA, assetB = params.AssetA, params.AssetB + receivedA, receivedB = -delta.ReserveA, -delta.ReserveB + } + // Process AssetA Details + if err := addAssetDetailsToOperationDetails(details, assetA, "reserve_a"); err != nil { + return details, err + } + details["reserve_a_min_amount"] = ConvertStroopValueToReal(op.MinAmountA) + details["reserve_a_withdraw_amount"] = ConvertStroopValueToReal(receivedA) + + // Process AssetB Details + if err := addAssetDetailsToOperationDetails(details, assetB, "reserve_b"); err != nil { + return details, err + } + details["reserve_b_min_amount"] = ConvertStroopValueToReal(op.MinAmountB) + details["reserve_b_withdraw_amount"] = ConvertStroopValueToReal(receivedB) + + details["shares"] = ConvertStroopValueToReal(op.Amount) + + case xdr.OperationTypeInvokeHostFunction: + op := operation.Body.MustInvokeHostFunctionOp() + details["function"] = op.HostFunction.Type.String() + + switch op.HostFunction.Type { + case xdr.HostFunctionTypeHostFunctionTypeInvokeContract: + invokeArgs := op.HostFunction.MustInvokeContract() + args := make([]xdr.ScVal, 0, len(invokeArgs.Args)+2) + args = append(args, xdr.ScVal{Type: xdr.ScValTypeScvAddress, Address: &invokeArgs.ContractAddress}) + args = append(args, xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &invokeArgs.FunctionName}) + args = append(args, invokeArgs.Args...) + + details["type"] = "invoke_contract" + + contractId, err := invokeArgs.ContractAddress.String() + if err != nil { + return nil, err + } + + transactionEnvelope := getTransactionV1Envelope(transaction.Envelope) + details["ledger_key_hash"] = ledgerKeyHashFromTxEnvelope(transactionEnvelope) + details["contract_id"] = contractId + details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) + + details["parameters"], details["parameters_decoded"] = serializeParameters(args) + + if balanceChanges, err := parseAssetBalanceChangesFromContractEvents(transaction, network); err != nil { + return nil, err + } else { + details["asset_balance_changes"] = balanceChanges + } + + case xdr.HostFunctionTypeHostFunctionTypeCreateContract: + args := op.HostFunction.MustCreateContract() + details["type"] = "create_contract" + + transactionEnvelope := getTransactionV1Envelope(transaction.Envelope) + details["ledger_key_hash"] = ledgerKeyHashFromTxEnvelope(transactionEnvelope) + details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) + details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) + + preimageTypeMap := switchContractIdPreimageType(args.ContractIdPreimage) + for key, val := range preimageTypeMap { + if _, ok := preimageTypeMap[key]; ok { + details[key] = val + } + } + case xdr.HostFunctionTypeHostFunctionTypeUploadContractWasm: + details["type"] = "upload_wasm" + transactionEnvelope := getTransactionV1Envelope(transaction.Envelope) + details["ledger_key_hash"] = ledgerKeyHashFromTxEnvelope(transactionEnvelope) + details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) + case xdr.HostFunctionTypeHostFunctionTypeCreateContractV2: + args := op.HostFunction.MustCreateContractV2() + details["type"] = "create_contract_v2" + + transactionEnvelope := getTransactionV1Envelope(transaction.Envelope) + details["ledger_key_hash"] = ledgerKeyHashFromTxEnvelope(transactionEnvelope) + details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) + details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) + + // ConstructorArgs is a list of ScVals + // This will initially be handled the same as InvokeContractParams until a different + // model is found necessary. + constructorArgs := args.ConstructorArgs + details["parameters"], details["parameters_decoded"] = serializeParameters(constructorArgs) + + preimageTypeMap := switchContractIdPreimageType(args.ContractIdPreimage) + for key, val := range preimageTypeMap { + if _, ok := preimageTypeMap[key]; ok { + details[key] = val + } + } + default: + panic(fmt.Errorf("unknown host function type: %s", op.HostFunction.Type)) + } + case xdr.OperationTypeExtendFootprintTtl: + op := operation.Body.MustExtendFootprintTtlOp() + details["type"] = "extend_footprint_ttl" + details["extend_to"] = op.ExtendTo + + transactionEnvelope := getTransactionV1Envelope(transaction.Envelope) + details["ledger_key_hash"] = ledgerKeyHashFromTxEnvelope(transactionEnvelope) + details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) + details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) + case xdr.OperationTypeRestoreFootprint: + details["type"] = "restore_footprint" + + transactionEnvelope := getTransactionV1Envelope(transaction.Envelope) + details["ledger_key_hash"] = ledgerKeyHashFromTxEnvelope(transactionEnvelope) + details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) + details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) + default: + return details, fmt.Errorf("unknown operation type: %s", operation.Body.Type.String()) + } + + sponsor, err := getSponsor(operation, transaction, operationIndex) + if err != nil { + return nil, err + } + if sponsor != nil { + details["sponsor"] = sponsor.Address() + } + + return details, nil +} + +// transactionOperationWrapper represents the data for a single operation within a transaction +type transactionOperationWrapper struct { + index uint32 + transaction ingest.LedgerTransaction + operation xdr.Operation + ledgerSequence uint32 + network string + ledgerClosed time.Time +} + +// ID returns the ID for the operation. +func (operation *transactionOperationWrapper) ID() int64 { + return toid.New( + int32(operation.ledgerSequence), + int32(operation.transaction.Index), + int32(operation.index+1), + ).ToInt64() +} + +// Order returns the operation order. +func (operation *transactionOperationWrapper) Order() uint32 { + return operation.index + 1 +} + +// TransactionID returns the id for the transaction related with this operation. +func (operation *transactionOperationWrapper) TransactionID() int64 { + return toid.New(int32(operation.ledgerSequence), int32(operation.transaction.Index), 0).ToInt64() +} + +// SourceAccount returns the operation's source account. +func (operation *transactionOperationWrapper) SourceAccount() *xdr.MuxedAccount { + sourceAccount := operation.operation.SourceAccount + if sourceAccount != nil { + return sourceAccount + } else { + ret := operation.transaction.Envelope.SourceAccount() + return &ret + } +} + +// OperationType returns the operation type. +func (operation *transactionOperationWrapper) OperationType() xdr.OperationType { + return operation.operation.Body.Type +} + +func (operation *transactionOperationWrapper) getSignerSponsorInChange(signerKey string, change ingest.Change) xdr.SponsorshipDescriptor { + if change.Type != xdr.LedgerEntryTypeAccount || change.Post == nil { + return nil + } + + preSigners := map[string]xdr.AccountId{} + if change.Pre != nil { + account := change.Pre.Data.MustAccount() + preSigners = account.SponsorPerSigner() + } + + account := change.Post.Data.MustAccount() + postSigners := account.SponsorPerSigner() + + pre, preFound := preSigners[signerKey] + post, postFound := postSigners[signerKey] + + if !postFound { + return nil + } + + if preFound { + formerSponsor := pre.Address() + newSponsor := post.Address() + if formerSponsor == newSponsor { + return nil + } + } + + return &post +} + +func (operation *transactionOperationWrapper) getSponsor() (*xdr.AccountId, error) { + changes, err := operation.transaction.GetOperationChanges(operation.index) + if err != nil { + return nil, err + } + var signerKey string + if setOps, ok := operation.operation.Body.GetSetOptionsOp(); ok && setOps.Signer != nil { + signerKey = setOps.Signer.Key.Address() + } + + for _, c := range changes { + // Check Signer changes + if signerKey != "" { + if sponsorAccount := operation.getSignerSponsorInChange(signerKey, c); sponsorAccount != nil { + return sponsorAccount, nil + } + } + + // Check Ledger key changes + if c.Pre != nil || c.Post == nil { + // We are only looking for entry creations denoting that a sponsor + // is associated to the ledger entry of the operation. + continue + } + if sponsorAccount := c.Post.SponsoringID(); sponsorAccount != nil { + return sponsorAccount, nil + } + } + + return nil, nil +} + +var errLiquidityPoolChangeNotFound = errors.New("liquidity pool change not found") + +func (operation *transactionOperationWrapper) getLiquidityPoolAndProductDelta(lpID *xdr.PoolId) (*xdr.LiquidityPoolEntry, *liquidityPoolDelta, error) { + changes, err := operation.transaction.GetOperationChanges(operation.index) + if err != nil { + return nil, nil, err + } + + for _, c := range changes { + if c.Type != xdr.LedgerEntryTypeLiquidityPool { + continue + } + // The delta can be caused by a full removal or full creation of the liquidity pool + var lp *xdr.LiquidityPoolEntry + var preA, preB, preShares xdr.Int64 + if c.Pre != nil { + if lpID != nil && c.Pre.Data.LiquidityPool.LiquidityPoolId != *lpID { + // if we were looking for specific pool id, then check on it + continue + } + lp = c.Pre.Data.LiquidityPool + if c.Pre.Data.LiquidityPool.Body.Type != xdr.LiquidityPoolTypeLiquidityPoolConstantProduct { + return nil, nil, fmt.Errorf("unexpected liquity pool body type %d", c.Pre.Data.LiquidityPool.Body.Type) + } + cpPre := c.Pre.Data.LiquidityPool.Body.ConstantProduct + preA, preB, preShares = cpPre.ReserveA, cpPre.ReserveB, cpPre.TotalPoolShares + } + var postA, postB, postShares xdr.Int64 + if c.Post != nil { + if lpID != nil && c.Post.Data.LiquidityPool.LiquidityPoolId != *lpID { + // if we were looking for specific pool id, then check on it + continue + } + lp = c.Post.Data.LiquidityPool + if c.Post.Data.LiquidityPool.Body.Type != xdr.LiquidityPoolTypeLiquidityPoolConstantProduct { + return nil, nil, fmt.Errorf("unexpected liquity pool body type %d", c.Post.Data.LiquidityPool.Body.Type) + } + cpPost := c.Post.Data.LiquidityPool.Body.ConstantProduct + postA, postB, postShares = cpPost.ReserveA, cpPost.ReserveB, cpPost.TotalPoolShares + } + delta := &liquidityPoolDelta{ + ReserveA: postA - preA, + ReserveB: postB - preB, + TotalPoolShares: postShares - preShares, + } + return lp, delta, nil + } + + return nil, nil, errLiquidityPoolChangeNotFound +} + +// OperationResult returns the operation's result record +func (operation *transactionOperationWrapper) OperationResult() *xdr.OperationResultTr { + results, _ := operation.transaction.Result.OperationResults() + tr := results[operation.index].MustTr() + return &tr +} + +func (operation *transactionOperationWrapper) findInitatingBeginSponsoringOp() *transactionOperationWrapper { + if !operation.transaction.Result.Successful() { + // Failed transactions may not have a compliant sandwich structure + // we can rely on (e.g. invalid nesting or a being operation with the wrong sponsoree ID) + // and thus we bail out since we could return incorrect information. + return nil + } + sponsoree := operation.SourceAccount().ToAccountId() + operations := operation.transaction.Envelope.Operations() + for i := int(operation.index) - 1; i >= 0; i-- { + if beginOp, ok := operations[i].Body.GetBeginSponsoringFutureReservesOp(); ok && + beginOp.SponsoredId.Address() == sponsoree.Address() { + result := *operation + result.index = uint32(i) + result.operation = operations[i] + return &result + } + } + return nil +} + +// Details returns the operation details as a map which can be stored as JSON. +func (operation *transactionOperationWrapper) Details() (map[string]interface{}, error) { + details := map[string]interface{}{} + source := operation.SourceAccount() + switch operation.OperationType() { + case xdr.OperationTypeCreateAccount: + op := operation.operation.Body.MustCreateAccountOp() + addAccountAndMuxedAccountDetails(details, *source, "funder") + details["account"] = op.Destination.Address() + details["starting_balance"] = amount.String(op.StartingBalance) + case xdr.OperationTypePayment: + op := operation.operation.Body.MustPaymentOp() + addAccountAndMuxedAccountDetails(details, *source, "from") + addAccountAndMuxedAccountDetails(details, op.Destination, "to") + details["amount"] = amount.String(op.Amount) + addAssetDetails(details, op.Asset, "") + case xdr.OperationTypePathPaymentStrictReceive: + op := operation.operation.Body.MustPathPaymentStrictReceiveOp() + addAccountAndMuxedAccountDetails(details, *source, "from") + addAccountAndMuxedAccountDetails(details, op.Destination, "to") + + details["amount"] = amount.String(op.DestAmount) + details["source_amount"] = amount.String(0) + details["source_max"] = amount.String(op.SendMax) + addAssetDetails(details, op.DestAsset, "") + addAssetDetails(details, op.SendAsset, "source_") + + if operation.transaction.Result.Successful() { + result := operation.OperationResult().MustPathPaymentStrictReceiveResult() + details["source_amount"] = amount.String(result.SendAmount()) + } + + var path = make([]map[string]interface{}, len(op.Path)) + for i := range op.Path { + path[i] = make(map[string]interface{}) + addAssetDetails(path[i], op.Path[i], "") + } + details["path"] = path + + case xdr.OperationTypePathPaymentStrictSend: + op := operation.operation.Body.MustPathPaymentStrictSendOp() + addAccountAndMuxedAccountDetails(details, *source, "from") + addAccountAndMuxedAccountDetails(details, op.Destination, "to") + + details["amount"] = amount.String(0) + details["source_amount"] = amount.String(op.SendAmount) + details["destination_min"] = amount.String(op.DestMin) + addAssetDetails(details, op.DestAsset, "") + addAssetDetails(details, op.SendAsset, "source_") + + if operation.transaction.Result.Successful() { + result := operation.OperationResult().MustPathPaymentStrictSendResult() + details["amount"] = amount.String(result.DestAmount()) + } + + var path = make([]map[string]interface{}, len(op.Path)) + for i := range op.Path { + path[i] = make(map[string]interface{}) + addAssetDetails(path[i], op.Path[i], "") + } + details["path"] = path + case xdr.OperationTypeManageBuyOffer: + op := operation.operation.Body.MustManageBuyOfferOp() + details["offer_id"] = op.OfferId + details["amount"] = amount.String(op.BuyAmount) + details["price"] = op.Price.String() + details["price_r"] = map[string]interface{}{ + "n": op.Price.N, + "d": op.Price.D, + } + addAssetDetails(details, op.Buying, "buying_") + addAssetDetails(details, op.Selling, "selling_") + case xdr.OperationTypeManageSellOffer: + op := operation.operation.Body.MustManageSellOfferOp() + details["offer_id"] = op.OfferId + details["amount"] = amount.String(op.Amount) + details["price"] = op.Price.String() + details["price_r"] = map[string]interface{}{ + "n": op.Price.N, + "d": op.Price.D, + } + addAssetDetails(details, op.Buying, "buying_") + addAssetDetails(details, op.Selling, "selling_") + case xdr.OperationTypeCreatePassiveSellOffer: + op := operation.operation.Body.MustCreatePassiveSellOfferOp() + details["amount"] = amount.String(op.Amount) + details["price"] = op.Price.String() + details["price_r"] = map[string]interface{}{ + "n": op.Price.N, + "d": op.Price.D, + } + addAssetDetails(details, op.Buying, "buying_") + addAssetDetails(details, op.Selling, "selling_") + case xdr.OperationTypeSetOptions: + op := operation.operation.Body.MustSetOptionsOp() + + if op.InflationDest != nil { + details["inflation_dest"] = op.InflationDest.Address() + } + + if op.SetFlags != nil && *op.SetFlags > 0 { + addAuthFlagDetails(details, xdr.AccountFlags(*op.SetFlags), "set") + } + + if op.ClearFlags != nil && *op.ClearFlags > 0 { + addAuthFlagDetails(details, xdr.AccountFlags(*op.ClearFlags), "clear") + } + + if op.MasterWeight != nil { + details["master_key_weight"] = *op.MasterWeight + } + + if op.LowThreshold != nil { + details["low_threshold"] = *op.LowThreshold + } + + if op.MedThreshold != nil { + details["med_threshold"] = *op.MedThreshold + } + + if op.HighThreshold != nil { + details["high_threshold"] = *op.HighThreshold + } + + if op.HomeDomain != nil { + details["home_domain"] = *op.HomeDomain + } + + if op.Signer != nil { + details["signer_key"] = op.Signer.Key.Address() + details["signer_weight"] = op.Signer.Weight + } + case xdr.OperationTypeChangeTrust: + op := operation.operation.Body.MustChangeTrustOp() + if op.Line.Type == xdr.AssetTypeAssetTypePoolShare { + if err := addLiquidityPoolAssetDetails(details, *op.Line.LiquidityPool); err != nil { + return nil, err + } + } else { + addAssetDetails(details, op.Line.ToAsset(), "") + details["trustee"] = details["asset_issuer"] + } + addAccountAndMuxedAccountDetails(details, *source, "trustor") + details["limit"] = amount.String(op.Limit) + case xdr.OperationTypeAllowTrust: + op := operation.operation.Body.MustAllowTrustOp() + addAssetDetails(details, op.Asset.ToAsset(source.ToAccountId()), "") + addAccountAndMuxedAccountDetails(details, *source, "trustee") + details["trustor"] = op.Trustor.Address() + details["authorize"] = xdr.TrustLineFlags(op.Authorize).IsAuthorized() + authLiabilities := xdr.TrustLineFlags(op.Authorize).IsAuthorizedToMaintainLiabilitiesFlag() + if authLiabilities { + details["authorize_to_maintain_liabilities"] = authLiabilities + } + clawbackEnabled := xdr.TrustLineFlags(op.Authorize).IsClawbackEnabledFlag() + if clawbackEnabled { + details["clawback_enabled"] = clawbackEnabled + } + case xdr.OperationTypeAccountMerge: + addAccountAndMuxedAccountDetails(details, *source, "account") + addAccountAndMuxedAccountDetails(details, operation.operation.Body.MustDestination(), "into") + case xdr.OperationTypeInflation: + // no inflation details, presently + case xdr.OperationTypeManageData: + op := operation.operation.Body.MustManageDataOp() + details["name"] = string(op.DataName) + if op.DataValue != nil { + details["value"] = base64.StdEncoding.EncodeToString(*op.DataValue) + } else { + details["value"] = nil + } + case xdr.OperationTypeBumpSequence: + op := operation.operation.Body.MustBumpSequenceOp() + details["bump_to"] = fmt.Sprintf("%d", op.BumpTo) + case xdr.OperationTypeCreateClaimableBalance: + op := operation.operation.Body.MustCreateClaimableBalanceOp() + details["asset"] = op.Asset.StringCanonical() + details["amount"] = amount.String(op.Amount) + var claimants []Claimant + for _, c := range op.Claimants { + cv0 := c.MustV0() + claimants = append(claimants, Claimant{ + Destination: cv0.Destination.Address(), + Predicate: cv0.Predicate, + }) + } + details["claimants"] = claimants + case xdr.OperationTypeClaimClaimableBalance: + op := operation.operation.Body.MustClaimClaimableBalanceOp() + balanceID, err := xdr.MarshalHex(op.BalanceId) + if err != nil { + panic(fmt.Errorf("invalid balanceId in op: %d", operation.index)) + } + details["balance_id"] = balanceID + addAccountAndMuxedAccountDetails(details, *source, "claimant") + case xdr.OperationTypeBeginSponsoringFutureReserves: + op := operation.operation.Body.MustBeginSponsoringFutureReservesOp() + details["sponsored_id"] = op.SponsoredId.Address() + case xdr.OperationTypeEndSponsoringFutureReserves: + beginSponsorshipOp := operation.findInitatingBeginSponsoringOp() + if beginSponsorshipOp != nil { + beginSponsorshipSource := beginSponsorshipOp.SourceAccount() + addAccountAndMuxedAccountDetails(details, *beginSponsorshipSource, "begin_sponsor") + } + case xdr.OperationTypeRevokeSponsorship: + op := operation.operation.Body.MustRevokeSponsorshipOp() + switch op.Type { + case xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry: + if err := addLedgerKeyDetails(details, *op.LedgerKey); err != nil { + return nil, err + } + case xdr.RevokeSponsorshipTypeRevokeSponsorshipSigner: + details["signer_account_id"] = op.Signer.AccountId.Address() + details["signer_key"] = op.Signer.SignerKey.Address() + } + case xdr.OperationTypeClawback: + op := operation.operation.Body.MustClawbackOp() + addAssetDetails(details, op.Asset, "") + addAccountAndMuxedAccountDetails(details, op.From, "from") + details["amount"] = amount.String(op.Amount) + case xdr.OperationTypeClawbackClaimableBalance: + op := operation.operation.Body.MustClawbackClaimableBalanceOp() + balanceID, err := xdr.MarshalHex(op.BalanceId) + if err != nil { + panic(fmt.Errorf("invalid balanceId in op: %d", operation.index)) + } + details["balance_id"] = balanceID + case xdr.OperationTypeSetTrustLineFlags: + op := operation.operation.Body.MustSetTrustLineFlagsOp() + details["trustor"] = op.Trustor.Address() + addAssetDetails(details, op.Asset, "") + if op.SetFlags > 0 { + addTrustLineFlagDetails(details, xdr.TrustLineFlags(op.SetFlags), "set") + } + + if op.ClearFlags > 0 { + addTrustLineFlagDetails(details, xdr.TrustLineFlags(op.ClearFlags), "clear") + } + case xdr.OperationTypeLiquidityPoolDeposit: + op := operation.operation.Body.MustLiquidityPoolDepositOp() + details["liquidity_pool_id"] = PoolIDToString(op.LiquidityPoolId) + var ( + assetA, assetB string + depositedA, depositedB xdr.Int64 + sharesReceived xdr.Int64 + ) + if operation.transaction.Result.Successful() { + // we will use the defaults (omitted asset and 0 amounts) if the transaction failed + lp, delta, err := operation.getLiquidityPoolAndProductDelta(&op.LiquidityPoolId) + if err != nil { + return nil, err + } + params := lp.Body.ConstantProduct.Params + assetA, assetB = params.AssetA.StringCanonical(), params.AssetB.StringCanonical() + depositedA, depositedB = delta.ReserveA, delta.ReserveB + sharesReceived = delta.TotalPoolShares + } + details["reserves_max"] = []base.AssetAmount{ + {Asset: assetA, Amount: amount.String(op.MaxAmountA)}, + {Asset: assetB, Amount: amount.String(op.MaxAmountB)}, + } + details["min_price"] = op.MinPrice.String() + details["min_price_r"] = map[string]interface{}{ + "n": op.MinPrice.N, + "d": op.MinPrice.D, + } + details["max_price"] = op.MaxPrice.String() + details["max_price_r"] = map[string]interface{}{ + "n": op.MaxPrice.N, + "d": op.MaxPrice.D, + } + details["reserves_deposited"] = []base.AssetAmount{ + {Asset: assetA, Amount: amount.String(depositedA)}, + {Asset: assetB, Amount: amount.String(depositedB)}, + } + details["shares_received"] = amount.String(sharesReceived) + case xdr.OperationTypeLiquidityPoolWithdraw: + op := operation.operation.Body.MustLiquidityPoolWithdrawOp() + details["liquidity_pool_id"] = PoolIDToString(op.LiquidityPoolId) + var ( + assetA, assetB string + receivedA, receivedB xdr.Int64 + ) + if operation.transaction.Result.Successful() { + // we will use the defaults (omitted asset and 0 amounts) if the transaction failed + lp, delta, err := operation.getLiquidityPoolAndProductDelta(&op.LiquidityPoolId) + if err != nil { + return nil, err + } + params := lp.Body.ConstantProduct.Params + assetA, assetB = params.AssetA.StringCanonical(), params.AssetB.StringCanonical() + receivedA, receivedB = -delta.ReserveA, -delta.ReserveB + } + details["reserves_min"] = []base.AssetAmount{ + {Asset: assetA, Amount: amount.String(op.MinAmountA)}, + {Asset: assetB, Amount: amount.String(op.MinAmountB)}, + } + details["shares"] = amount.String(op.Amount) + details["reserves_received"] = []base.AssetAmount{ + {Asset: assetA, Amount: amount.String(receivedA)}, + {Asset: assetB, Amount: amount.String(receivedB)}, + } + case xdr.OperationTypeInvokeHostFunction: + op := operation.operation.Body.MustInvokeHostFunctionOp() + details["function"] = op.HostFunction.Type.String() + + switch op.HostFunction.Type { + case xdr.HostFunctionTypeHostFunctionTypeInvokeContract: + invokeArgs := op.HostFunction.MustInvokeContract() + args := make([]xdr.ScVal, 0, len(invokeArgs.Args)+2) + args = append(args, xdr.ScVal{Type: xdr.ScValTypeScvAddress, Address: &invokeArgs.ContractAddress}) + args = append(args, xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &invokeArgs.FunctionName}) + args = append(args, invokeArgs.Args...) + + details["type"] = "invoke_contract" + + contractId, err := invokeArgs.ContractAddress.String() + if err != nil { + return nil, err + } + + transactionEnvelope := getTransactionV1Envelope(operation.transaction.Envelope) + details["ledger_key_hash"] = ledgerKeyHashFromTxEnvelope(transactionEnvelope) + details["contract_id"] = contractId + details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) + + details["parameters"], details["parameters_decoded"] = serializeParameters(args) + + if balanceChanges, err := operation.parseAssetBalanceChangesFromContractEvents(); err != nil { + return nil, err + } else { + details["asset_balance_changes"] = balanceChanges + } + + case xdr.HostFunctionTypeHostFunctionTypeCreateContract: + args := op.HostFunction.MustCreateContract() + details["type"] = "create_contract" + + transactionEnvelope := getTransactionV1Envelope(operation.transaction.Envelope) + details["ledger_key_hash"] = ledgerKeyHashFromTxEnvelope(transactionEnvelope) + details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) + details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) + + preimageTypeMap := switchContractIdPreimageType(args.ContractIdPreimage) + for key, val := range preimageTypeMap { + if _, ok := preimageTypeMap[key]; ok { + details[key] = val + } + } + case xdr.HostFunctionTypeHostFunctionTypeUploadContractWasm: + details["type"] = "upload_wasm" + transactionEnvelope := getTransactionV1Envelope(operation.transaction.Envelope) + details["ledger_key_hash"] = ledgerKeyHashFromTxEnvelope(transactionEnvelope) + details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) + case xdr.HostFunctionTypeHostFunctionTypeCreateContractV2: + args := op.HostFunction.MustCreateContractV2() + details["type"] = "create_contract_v2" + + transactionEnvelope := getTransactionV1Envelope(operation.transaction.Envelope) + details["ledger_key_hash"] = ledgerKeyHashFromTxEnvelope(transactionEnvelope) + details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) + details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) + + // ConstructorArgs is a list of ScVals + // This will initially be handled the same as InvokeContractParams until a different + // model is found necessary. + constructorArgs := args.ConstructorArgs + details["parameters"], details["parameters_decoded"] = serializeParameters(constructorArgs) + + preimageTypeMap := switchContractIdPreimageType(args.ContractIdPreimage) + for key, val := range preimageTypeMap { + if _, ok := preimageTypeMap[key]; ok { + details[key] = val + } + } + default: + panic(fmt.Errorf("unknown host function type: %s", op.HostFunction.Type)) + } + case xdr.OperationTypeExtendFootprintTtl: + op := operation.operation.Body.MustExtendFootprintTtlOp() + details["type"] = "extend_footprint_ttl" + details["extend_to"] = op.ExtendTo + + transactionEnvelope := getTransactionV1Envelope(operation.transaction.Envelope) + details["ledger_key_hash"] = ledgerKeyHashFromTxEnvelope(transactionEnvelope) + details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) + details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) + case xdr.OperationTypeRestoreFootprint: + details["type"] = "restore_footprint" + + transactionEnvelope := getTransactionV1Envelope(operation.transaction.Envelope) + details["ledger_key_hash"] = ledgerKeyHashFromTxEnvelope(transactionEnvelope) + details["contract_id"] = contractIdFromTxEnvelope(transactionEnvelope) + details["contract_code_hash"] = contractCodeHashFromTxEnvelope(transactionEnvelope) + default: + panic(fmt.Errorf("unknown operation type: %s", operation.OperationType())) + } + + sponsor, err := operation.getSponsor() + if err != nil { + return nil, err + } + if sponsor != nil { + details["sponsor"] = sponsor.Address() + } + + return details, nil +} + +func getTransactionV1Envelope(transactionEnvelope xdr.TransactionEnvelope) xdr.TransactionV1Envelope { + switch transactionEnvelope.Type { + case xdr.EnvelopeTypeEnvelopeTypeTx: + return transactionEnvelope.MustV1() + case xdr.EnvelopeTypeEnvelopeTypeTxFeeBump: + return transactionEnvelope.MustFeeBump().Tx.InnerTx.MustV1() + } + + return xdr.TransactionV1Envelope{} +} + +func contractIdFromTxEnvelope(transactionEnvelope xdr.TransactionV1Envelope) string { + for _, ledgerKey := range transactionEnvelope.Tx.Ext.SorobanData.Resources.Footprint.ReadWrite { + contractId := contractIdFromContractData(ledgerKey) + if contractId != "" { + return contractId + } + } + + for _, ledgerKey := range transactionEnvelope.Tx.Ext.SorobanData.Resources.Footprint.ReadOnly { + contractId := contractIdFromContractData(ledgerKey) + if contractId != "" { + return contractId + } + } + + return "" +} + +func contractIdFromContractData(ledgerKey xdr.LedgerKey) string { + contractData, ok := ledgerKey.GetContractData() + if !ok { + return "" + } + contractIdHash, ok := contractData.Contract.GetContractId() + if !ok { + return "" + } + + contractIdByte, _ := contractIdHash.MarshalBinary() + contractId, _ := strkey.Encode(strkey.VersionByteContract, contractIdByte) + return contractId +} + +func contractCodeHashFromTxEnvelope(transactionEnvelope xdr.TransactionV1Envelope) string { + for _, ledgerKey := range transactionEnvelope.Tx.Ext.SorobanData.Resources.Footprint.ReadOnly { + contractCode := contractCodeFromContractData(ledgerKey) + if contractCode != "" { + return contractCode + } + } + + for _, ledgerKey := range transactionEnvelope.Tx.Ext.SorobanData.Resources.Footprint.ReadWrite { + contractCode := contractCodeFromContractData(ledgerKey) + if contractCode != "" { + return contractCode + } + } + + return "" +} + +func ledgerKeyHashFromTxEnvelope(transactionEnvelope xdr.TransactionV1Envelope) []string { + var ledgerKeyHash []string + for _, ledgerKey := range transactionEnvelope.Tx.Ext.SorobanData.Resources.Footprint.ReadOnly { + if LedgerKeyToLedgerKeyHash(ledgerKey) != "" { + ledgerKeyHash = append(ledgerKeyHash, LedgerKeyToLedgerKeyHash(ledgerKey)) + } + } + + for _, ledgerKey := range transactionEnvelope.Tx.Ext.SorobanData.Resources.Footprint.ReadWrite { + if LedgerKeyToLedgerKeyHash(ledgerKey) != "" { + ledgerKeyHash = append(ledgerKeyHash, LedgerKeyToLedgerKeyHash(ledgerKey)) + } + } + + return ledgerKeyHash +} + +func contractCodeFromContractData(ledgerKey xdr.LedgerKey) string { + contractCode, ok := ledgerKey.GetContractCode() + if !ok { + return "" + } + + contractCodeHash := contractCode.Hash.HexString() + return contractCodeHash +} + +func filterEvents(diagnosticEvents []xdr.DiagnosticEvent) []xdr.ContractEvent { + var filtered []xdr.ContractEvent + for _, diagnosticEvent := range diagnosticEvents { + if !diagnosticEvent.InSuccessfulContractCall || diagnosticEvent.Event.Type != xdr.ContractEventTypeContract { + continue + } + filtered = append(filtered, diagnosticEvent.Event) + } + return filtered +} + +// Searches an operation for SAC events that are of a type which represent +// asset balances having changed. +// +// SAC events have a one-to-one association to SAC contract fn invocations. +// i.e. invoke the 'mint' function, will trigger one Mint Event to be emitted capturing the fn args. +// +// SAC events that involve asset balance changes follow some standard data formats. +// The 'amount' in the event is expressed as Int128Parts, which carries a sign, however it's expected +// that value will not be signed as it represents a absolute delta, the event type can provide the +// context of whether an amount was considered incremental or decremental, i.e. credit or debit to a balance. +func (operation *transactionOperationWrapper) parseAssetBalanceChangesFromContractEvents() ([]map[string]interface{}, error) { + balanceChanges := []map[string]interface{}{} + + diagnosticEvents, err := operation.transaction.GetDiagnosticEvents() + if err != nil { + // this operation in this context must be an InvokeHostFunctionOp, therefore V3Meta should be present + // as it's in same soroban model, so if any err, it's real, + return nil, err + } + + for _, contractEvent := range filterEvents(diagnosticEvents) { + // Parse the xdr contract event to contractevents.StellarAssetContractEvent model + + // has some convenience like to/from attributes are expressed in strkey format for accounts(G...) and contracts(C...) + if sacEvent, err := contractevents.NewStellarAssetContractEvent(&contractEvent, operation.network); err == nil { + switch sacEvent.GetType() { + case contractevents.EventTypeTransfer: + transferEvt := sacEvent.(*contractevents.TransferEvent) + balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(transferEvt.From, transferEvt.To, transferEvt.Amount, transferEvt.Asset, "transfer")) + case contractevents.EventTypeMint: + mintEvt := sacEvent.(*contractevents.MintEvent) + balanceChanges = append(balanceChanges, createSACBalanceChangeEntry("", mintEvt.To, mintEvt.Amount, mintEvt.Asset, "mint")) + case contractevents.EventTypeClawback: + clawbackEvt := sacEvent.(*contractevents.ClawbackEvent) + balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(clawbackEvt.From, "", clawbackEvt.Amount, clawbackEvt.Asset, "clawback")) + case contractevents.EventTypeBurn: + burnEvt := sacEvent.(*contractevents.BurnEvent) + balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(burnEvt.From, "", burnEvt.Amount, burnEvt.Asset, "burn")) + } + } + } + + return balanceChanges, nil +} + +func parseAssetBalanceChangesFromContractEvents(transaction ingest.LedgerTransaction, network string) ([]map[string]interface{}, error) { + balanceChanges := []map[string]interface{}{} + + diagnosticEvents, err := transaction.GetDiagnosticEvents() + if err != nil { + // this operation in this context must be an InvokeHostFunctionOp, therefore V3Meta should be present + // as it's in same soroban model, so if any err, it's real, + return nil, err + } + + for _, contractEvent := range filterEvents(diagnosticEvents) { + // Parse the xdr contract event to contractevents.StellarAssetContractEvent model + + // has some convenience like to/from attributes are expressed in strkey format for accounts(G...) and contracts(C...) + if sacEvent, err := contractevents.NewStellarAssetContractEvent(&contractEvent, network); err == nil { + switch sacEvent.GetType() { + case contractevents.EventTypeTransfer: + transferEvt := sacEvent.(*contractevents.TransferEvent) + balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(transferEvt.From, transferEvt.To, transferEvt.Amount, transferEvt.Asset, "transfer")) + case contractevents.EventTypeMint: + mintEvt := sacEvent.(*contractevents.MintEvent) + balanceChanges = append(balanceChanges, createSACBalanceChangeEntry("", mintEvt.To, mintEvt.Amount, mintEvt.Asset, "mint")) + case contractevents.EventTypeClawback: + clawbackEvt := sacEvent.(*contractevents.ClawbackEvent) + balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(clawbackEvt.From, "", clawbackEvt.Amount, clawbackEvt.Asset, "clawback")) + case contractevents.EventTypeBurn: + burnEvt := sacEvent.(*contractevents.BurnEvent) + balanceChanges = append(balanceChanges, createSACBalanceChangeEntry(burnEvt.From, "", burnEvt.Amount, burnEvt.Asset, "burn")) + } + } + } + + return balanceChanges, nil +} + +// fromAccount - strkey format of contract or address +// toAccount - strkey format of contract or address, or nillable +// amountChanged - absolute value that asset balance changed +// asset - the fully qualified issuer:code for asset that had balance change +// changeType - the type of source sac event that triggered this change +// +// return - a balance changed record expressed as map of key/value's +func createSACBalanceChangeEntry(fromAccount string, toAccount string, amountChanged xdr.Int128Parts, asset xdr.Asset, changeType string) map[string]interface{} { + balanceChange := map[string]interface{}{} + + if fromAccount != "" { + balanceChange["from"] = fromAccount + } + if toAccount != "" { + balanceChange["to"] = toAccount + } + + balanceChange["type"] = changeType + balanceChange["amount"] = amount.String128(amountChanged) + addAssetDetails(balanceChange, asset, "") + return balanceChange +} + +// addAssetDetails sets the details for `a` on `result` using keys with `prefix` +func addAssetDetails(result map[string]interface{}, a xdr.Asset, prefix string) error { + var ( + assetType string + code string + issuer string + ) + err := a.Extract(&assetType, &code, &issuer) + if err != nil { + err = errors.Wrap(err, "xdr.Asset.Extract error") + return err + } + result[prefix+"asset_type"] = assetType + + if a.Type == xdr.AssetTypeAssetTypeNative { + return nil + } + + result[prefix+"asset_code"] = code + result[prefix+"asset_issuer"] = issuer + return nil +} + +// addAuthFlagDetails adds the account flag details for `f` on `result`. +func addAuthFlagDetails(result map[string]interface{}, f xdr.AccountFlags, prefix string) { + var ( + n []int32 + s []string + ) + + if f.IsAuthRequired() { + n = append(n, int32(xdr.AccountFlagsAuthRequiredFlag)) + s = append(s, "auth_required") + } + + if f.IsAuthRevocable() { + n = append(n, int32(xdr.AccountFlagsAuthRevocableFlag)) + s = append(s, "auth_revocable") + } + + if f.IsAuthImmutable() { + n = append(n, int32(xdr.AccountFlagsAuthImmutableFlag)) + s = append(s, "auth_immutable") + } + + if f.IsAuthClawbackEnabled() { + n = append(n, int32(xdr.AccountFlagsAuthClawbackEnabledFlag)) + s = append(s, "auth_clawback_enabled") + } + + result[prefix+"_flags"] = n + result[prefix+"_flags_s"] = s +} + +// addTrustLineFlagDetails adds the trustline flag details for `f` on `result`. +func addTrustLineFlagDetails(result map[string]interface{}, f xdr.TrustLineFlags, prefix string) { + var ( + n []int32 + s []string + ) + + if f.IsAuthorized() { + n = append(n, int32(xdr.TrustLineFlagsAuthorizedFlag)) + s = append(s, "authorized") + } + + if f.IsAuthorizedToMaintainLiabilitiesFlag() { + n = append(n, int32(xdr.TrustLineFlagsAuthorizedToMaintainLiabilitiesFlag)) + s = append(s, "authorized_to_maintain_liabilites") + } + + if f.IsClawbackEnabledFlag() { + n = append(n, int32(xdr.TrustLineFlagsTrustlineClawbackEnabledFlag)) + s = append(s, "clawback_enabled") + } + + result[prefix+"_flags"] = n + result[prefix+"_flags_s"] = s +} + +func addLedgerKeyDetails(result map[string]interface{}, ledgerKey xdr.LedgerKey) error { + switch ledgerKey.Type { + case xdr.LedgerEntryTypeAccount: + result["account_id"] = ledgerKey.Account.AccountId.Address() + case xdr.LedgerEntryTypeClaimableBalance: + marshalHex, err := xdr.MarshalHex(ledgerKey.ClaimableBalance.BalanceId) + if err != nil { + return errors.Wrapf(err, "in claimable balance") + } + result["claimable_balance_id"] = marshalHex + case xdr.LedgerEntryTypeData: + result["data_account_id"] = ledgerKey.Data.AccountId.Address() + result["data_name"] = ledgerKey.Data.DataName + case xdr.LedgerEntryTypeOffer: + result["offer_id"] = fmt.Sprintf("%d", ledgerKey.Offer.OfferId) + case xdr.LedgerEntryTypeTrustline: + result["trustline_account_id"] = ledgerKey.TrustLine.AccountId.Address() + if ledgerKey.TrustLine.Asset.Type == xdr.AssetTypeAssetTypePoolShare { + result["trustline_liquidity_pool_id"] = PoolIDToString(*ledgerKey.TrustLine.Asset.LiquidityPoolId) + } else { + result["trustline_asset"] = ledgerKey.TrustLine.Asset.ToAsset().StringCanonical() + } + case xdr.LedgerEntryTypeLiquidityPool: + result["liquidity_pool_id"] = PoolIDToString(ledgerKey.LiquidityPool.LiquidityPoolId) + } + return nil +} + +func getLedgerKeyParticipants(ledgerKey xdr.LedgerKey) []xdr.AccountId { + var result []xdr.AccountId + switch ledgerKey.Type { + case xdr.LedgerEntryTypeAccount: + result = append(result, ledgerKey.Account.AccountId) + case xdr.LedgerEntryTypeClaimableBalance: + // nothing to do + case xdr.LedgerEntryTypeData: + result = append(result, ledgerKey.Data.AccountId) + case xdr.LedgerEntryTypeOffer: + result = append(result, ledgerKey.Offer.SellerId) + case xdr.LedgerEntryTypeTrustline: + result = append(result, ledgerKey.TrustLine.AccountId) + } + return result +} + +// Participants returns the accounts taking part in the operation. +func (operation *transactionOperationWrapper) Participants() ([]xdr.AccountId, error) { + participants := []xdr.AccountId{} + participants = append(participants, operation.SourceAccount().ToAccountId()) + op := operation.operation + + switch operation.OperationType() { + case xdr.OperationTypeCreateAccount: + participants = append(participants, op.Body.MustCreateAccountOp().Destination) + case xdr.OperationTypePayment: + participants = append(participants, op.Body.MustPaymentOp().Destination.ToAccountId()) + case xdr.OperationTypePathPaymentStrictReceive: + participants = append(participants, op.Body.MustPathPaymentStrictReceiveOp().Destination.ToAccountId()) + case xdr.OperationTypePathPaymentStrictSend: + participants = append(participants, op.Body.MustPathPaymentStrictSendOp().Destination.ToAccountId()) + case xdr.OperationTypeManageBuyOffer: + // the only direct participant is the source_account + case xdr.OperationTypeManageSellOffer: + // the only direct participant is the source_account + case xdr.OperationTypeCreatePassiveSellOffer: + // the only direct participant is the source_account + case xdr.OperationTypeSetOptions: + // the only direct participant is the source_account + case xdr.OperationTypeChangeTrust: + // the only direct participant is the source_account + case xdr.OperationTypeAllowTrust: + participants = append(participants, op.Body.MustAllowTrustOp().Trustor) + case xdr.OperationTypeAccountMerge: + participants = append(participants, op.Body.MustDestination().ToAccountId()) + case xdr.OperationTypeInflation: + // the only direct participant is the source_account + case xdr.OperationTypeManageData: + // the only direct participant is the source_account + case xdr.OperationTypeBumpSequence: + // the only direct participant is the source_account + case xdr.OperationTypeCreateClaimableBalance: + for _, c := range op.Body.MustCreateClaimableBalanceOp().Claimants { + participants = append(participants, c.MustV0().Destination) + } + case xdr.OperationTypeClaimClaimableBalance: + // the only direct participant is the source_account + case xdr.OperationTypeBeginSponsoringFutureReserves: + participants = append(participants, op.Body.MustBeginSponsoringFutureReservesOp().SponsoredId) + case xdr.OperationTypeEndSponsoringFutureReserves: + beginSponsorshipOp := operation.findInitatingBeginSponsoringOp() + if beginSponsorshipOp != nil { + participants = append(participants, beginSponsorshipOp.SourceAccount().ToAccountId()) + } + case xdr.OperationTypeRevokeSponsorship: + op := operation.operation.Body.MustRevokeSponsorshipOp() + switch op.Type { + case xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry: + participants = append(participants, getLedgerKeyParticipants(*op.LedgerKey)...) + case xdr.RevokeSponsorshipTypeRevokeSponsorshipSigner: + participants = append(participants, op.Signer.AccountId) + // We don't add signer as a participant because a signer can be arbitrary account. + // This can spam successful operations history of any account. + } + case xdr.OperationTypeClawback: + op := operation.operation.Body.MustClawbackOp() + participants = append(participants, op.From.ToAccountId()) + case xdr.OperationTypeClawbackClaimableBalance: + // the only direct participant is the source_account + case xdr.OperationTypeSetTrustLineFlags: + op := operation.operation.Body.MustSetTrustLineFlagsOp() + participants = append(participants, op.Trustor) + case xdr.OperationTypeLiquidityPoolDeposit: + // the only direct participant is the source_account + case xdr.OperationTypeLiquidityPoolWithdraw: + // the only direct participant is the source_account + case xdr.OperationTypeInvokeHostFunction: + // the only direct participant is the source_account + case xdr.OperationTypeExtendFootprintTtl: + // the only direct participant is the source_account + case xdr.OperationTypeRestoreFootprint: + // the only direct participant is the source_account + default: + return participants, fmt.Errorf("unknown operation type: %s", op.Body.Type) + } + + sponsor, err := operation.getSponsor() + if err != nil { + return nil, err + } + if sponsor != nil { + participants = append(participants, *sponsor) + } + + return dedupeParticipants(participants), nil +} + +// dedupeParticipants remove any duplicate ids from `in` +func dedupeParticipants(in []xdr.AccountId) (out []xdr.AccountId) { + set := map[string]xdr.AccountId{} + for _, id := range in { + set[id.Address()] = id + } + + for _, id := range set { + out = append(out, id) + } + return +} + +func serializeParameters(args []xdr.ScVal) ([]map[string]string, []map[string]string) { + params := make([]map[string]string, 0, len(args)) + paramsDecoded := make([]map[string]string, 0, len(args)) + + for _, param := range args { + serializedParam := map[string]string{} + serializedParam["value"] = "n/a" + serializedParam["type"] = "n/a" + + serializedParamDecoded := map[string]string{} + serializedParamDecoded["value"] = "n/a" + serializedParamDecoded["type"] = "n/a" + + if scValTypeName, ok := param.ArmForSwitch(int32(param.Type)); ok { + serializedParam["type"] = scValTypeName + serializedParamDecoded["type"] = scValTypeName + if raw, err := param.MarshalBinary(); err == nil { + serializedParam["value"] = base64.StdEncoding.EncodeToString(raw) + serializedParamDecoded["value"] = param.String() + } + } + params = append(params, serializedParam) + paramsDecoded = append(paramsDecoded, serializedParamDecoded) + } + + return params, paramsDecoded +} + +func switchContractIdPreimageType(contractIdPreimage xdr.ContractIdPreimage) map[string]interface{} { + details := map[string]interface{}{} + + switch contractIdPreimage.Type { + case xdr.ContractIdPreimageTypeContractIdPreimageFromAddress: + fromAddress := contractIdPreimage.MustFromAddress() + address, err := fromAddress.Address.String() + if err != nil { + panic(fmt.Errorf("error obtaining address for: %s", contractIdPreimage.Type)) + } + details["from"] = "address" + details["address"] = address + case xdr.ContractIdPreimageTypeContractIdPreimageFromAsset: + details["from"] = "asset" + details["asset"] = contractIdPreimage.MustFromAsset().StringCanonical() + default: + panic(fmt.Errorf("unknown contract id type: %s", contractIdPreimage.Type)) + } + + return details +} diff --git a/ingest/processors/operation_test.go b/ingest/processors/operation_test.go new file mode 100644 index 0000000000..0334b4d521 --- /dev/null +++ b/ingest/processors/operation_test.go @@ -0,0 +1,2077 @@ +package processors + +import ( + "encoding/base64" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformOperation(t *testing.T) { + type operationInput struct { + operation xdr.Operation + index int32 + transaction ingest.LedgerTransaction + ledgerClosedMeta xdr.LedgerCloseMeta + } + type transformTest struct { + input operationInput + wantOutput OperationOutput + wantErr error + } + genericInput := operationInput{ + operation: genericBumpOperation, + index: 1, + transaction: genericLedgerTransaction, + } + + negativeOpTypeInput := genericInput + negativeOpTypeEnvelope := genericBumpOperationEnvelope + negativeOpTypeEnvelope.Tx.Operations[0].Body.Type = xdr.OperationType(-1) + negativeOpTypeInput.operation.Body.Type = xdr.OperationType(-1) + negativeOpTypeInput.transaction.Envelope.V1 = &negativeOpTypeEnvelope + + unknownOpTypeInput := genericInput + unknownOpTypeEnvelope := genericBumpOperationEnvelope + unknownOpTypeEnvelope.Tx.Operations[0].Body.Type = xdr.OperationType(99) + unknownOpTypeInput.operation.Body.Type = xdr.OperationType(99) + unknownOpTypeInput.transaction.Envelope.V1 = &unknownOpTypeEnvelope + + tests := []transformTest{ + { + negativeOpTypeInput, + OperationOutput{}, + fmt.Errorf("the operation type (-1) is negative for operation 1 (operation id=4098)"), + }, + { + unknownOpTypeInput, + OperationOutput{}, + fmt.Errorf("unknown operation type: "), + }, + } + hardCodedInputTransaction, err := makeOperationTestInput() + assert.NoError(t, err) + hardCodedOutputArray := makeOperationTestOutputs() + hardCodedInputLedgerCloseMeta := makeLedgerCloseMeta() + + for i, op := range hardCodedInputTransaction.Envelope.Operations() { + tests = append(tests, transformTest{ + input: operationInput{op, int32(i), hardCodedInputTransaction, hardCodedInputLedgerCloseMeta}, + wantOutput: hardCodedOutputArray[i], + wantErr: nil, + }) + } + + for _, test := range tests { + actualOutput, actualError := TransformOperation(test.input.operation, test.input.index, test.input.transaction, 0, test.input.ledgerClosedMeta, "") + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func makeLedgerCloseMeta() (ledgerCloseMeta xdr.LedgerCloseMeta) { + return xdr.LedgerCloseMeta{ + V: 0, + V0: &xdr.LedgerCloseMetaV0{ + LedgerHeader: xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + ScpValue: xdr.StellarValue{ + CloseTime: 0, + }, + LedgerSeq: 0, + }, + }, + }, + } +} + +// Creates a single transaction that contains one of every operation type +func makeOperationTestInput() (inputTransaction ingest.LedgerTransaction, err error) { + inputTransaction = genericLedgerTransaction + inputEnvelope := genericBumpOperationEnvelope + + inputEnvelope.Tx.SourceAccount = testAccount3 + hardCodedInflationDest := testAccount4ID + + hardCodedTrustAsset, err := usdtAsset.ToAssetCode("USDT") + if err != nil { + return + } + + contractHash := xdr.Hash{} + salt := [32]byte{} + assetCode := [12]byte{} + assetIssuer := xdr.Uint256{} + wasm := []byte{} + dummyBool := true + + hardCodedClearFlags := xdr.Uint32(3) + hardCodedSetFlags := xdr.Uint32(4) + hardCodedMasterWeight := xdr.Uint32(3) + hardCodedLowThresh := xdr.Uint32(1) + hardCodedMedThresh := xdr.Uint32(3) + hardCodedHighThresh := xdr.Uint32(5) + hardCodedHomeDomain := xdr.String32("2019=DRA;n-test") + hardCodedSignerKey, err := xdr.NewSignerKey(xdr.SignerKeyTypeSignerKeyTypeEd25519, xdr.Uint256([32]byte{})) + if err != nil { + return + } + + hardCodedSigner := xdr.Signer{ + Key: hardCodedSignerKey, + Weight: xdr.Uint32(1), + } + + hardCodedClaimableBalance := genericClaimableBalance + hardCodedClaimant := testClaimant + hardCodedDataValue := xdr.DataValue([]byte{0x76, 0x61, 0x6c, 0x75, 0x65}) + hardCodedSequenceNumber := xdr.SequenceNumber(100) + inputOperations := []xdr.Operation{ + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeCreateAccount, + CreateAccountOp: &xdr.CreateAccountOp{ + StartingBalance: 25000000, + Destination: testAccount4ID, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypePayment, + PaymentOp: &xdr.PaymentOp{ + Destination: testAccount4, + Asset: usdtAsset, + Amount: 350000000, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypePayment, + PaymentOp: &xdr.PaymentOp{ + Destination: testAccount4, + Asset: nativeAsset, + Amount: 350000000, + }, + }, + }, + { + SourceAccount: &testAccount3, + Body: xdr.OperationBody{ + Type: xdr.OperationTypePathPaymentStrictReceive, + PathPaymentStrictReceiveOp: &xdr.PathPaymentStrictReceiveOp{ + SendAsset: nativeAsset, + SendMax: 8951495900, + Destination: testAccount4, + DestAsset: nativeAsset, + DestAmount: 8951495900, + Path: []xdr.Asset{usdtAsset}, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeManageSellOffer, + ManageSellOfferOp: &xdr.ManageSellOfferOp{ + Selling: usdtAsset, + Buying: nativeAsset, + Amount: 765860000, + Price: xdr.Price{ + N: 128523, + D: 250000, + }, + OfferId: 0, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeCreatePassiveSellOffer, + CreatePassiveSellOfferOp: &xdr.CreatePassiveSellOfferOp{ + Selling: nativeAsset, + Buying: usdtAsset, + Amount: 631595000, + Price: xdr.Price{ + N: 99583200, + D: 1257990000, + }, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeSetOptions, + SetOptionsOp: &xdr.SetOptionsOp{ + InflationDest: &hardCodedInflationDest, + ClearFlags: &hardCodedClearFlags, + SetFlags: &hardCodedSetFlags, + MasterWeight: &hardCodedMasterWeight, + LowThreshold: &hardCodedLowThresh, + MedThreshold: &hardCodedMedThresh, + HighThreshold: &hardCodedHighThresh, + HomeDomain: &hardCodedHomeDomain, + Signer: &hardCodedSigner, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeChangeTrust, + ChangeTrustOp: &xdr.ChangeTrustOp{ + Line: usdtChangeTrustAsset, + Limit: xdr.Int64(500000000000000000), + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeChangeTrust, + ChangeTrustOp: &xdr.ChangeTrustOp{ + Line: usdtLiquidityPoolShare, + Limit: xdr.Int64(500000000000000000), + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeAllowTrust, + AllowTrustOp: &xdr.AllowTrustOp{ + Trustor: testAccount4ID, + Asset: hardCodedTrustAsset, + Authorize: xdr.Uint32(1), + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeAccountMerge, + Destination: &testAccount4, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeInflation, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeManageData, + ManageDataOp: &xdr.ManageDataOp{ + DataName: "test", + DataValue: &hardCodedDataValue, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeBumpSequence, + BumpSequenceOp: &xdr.BumpSequenceOp{ + BumpTo: hardCodedSequenceNumber, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeManageBuyOffer, + ManageBuyOfferOp: &xdr.ManageBuyOfferOp{ + Selling: usdtAsset, + Buying: nativeAsset, + BuyAmount: 7654501001, + Price: xdr.Price{ + N: 635863285, + D: 1818402817, + }, + OfferId: 100, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypePathPaymentStrictSend, + PathPaymentStrictSendOp: &xdr.PathPaymentStrictSendOp{ + SendAsset: nativeAsset, + SendAmount: 1598182, + Destination: testAccount4, + DestAsset: nativeAsset, + DestMin: 4280460538, + Path: []xdr.Asset{usdtAsset}, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeCreateClaimableBalance, + CreateClaimableBalanceOp: &xdr.CreateClaimableBalanceOp{ + Asset: usdtAsset, + Amount: 1234567890000, + Claimants: []xdr.Claimant{hardCodedClaimant}, + }, + }, + }, + { + SourceAccount: &testAccount3, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeClaimClaimableBalance, + ClaimClaimableBalanceOp: &xdr.ClaimClaimableBalanceOp{ + BalanceId: hardCodedClaimableBalance, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeBeginSponsoringFutureReserves, + BeginSponsoringFutureReservesOp: &xdr.BeginSponsoringFutureReservesOp{ + SponsoredId: testAccount4ID, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipOp: &xdr.RevokeSponsorshipOp{ + Type: xdr.RevokeSponsorshipTypeRevokeSponsorshipSigner, + Signer: &xdr.RevokeSponsorshipOpSigner{ + AccountId: testAccount4ID, + SignerKey: hardCodedSigner.Key, + }, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipOp: &xdr.RevokeSponsorshipOp{ + Type: xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry, + LedgerKey: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.LedgerKeyAccount{ + AccountId: testAccount4ID, + }, + }, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipOp: &xdr.RevokeSponsorshipOp{ + Type: xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry, + LedgerKey: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeClaimableBalance, + ClaimableBalance: &xdr.LedgerKeyClaimableBalance{ + BalanceId: hardCodedClaimableBalance, + }, + }, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipOp: &xdr.RevokeSponsorshipOp{ + Type: xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry, + LedgerKey: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeData, + Data: &xdr.LedgerKeyData{ + AccountId: testAccount4ID, + DataName: "test", + }, + }, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipOp: &xdr.RevokeSponsorshipOp{ + Type: xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry, + LedgerKey: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.LedgerKeyOffer{ + SellerId: testAccount3ID, + OfferId: 100, + }, + }, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipOp: &xdr.RevokeSponsorshipOp{ + Type: xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry, + LedgerKey: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.LedgerKeyTrustLine{ + AccountId: testAccount3ID, + Asset: usdtTrustLineAsset, + }, + }, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipOp: &xdr.RevokeSponsorshipOp{ + Type: xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry, + LedgerKey: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeLiquidityPool, + LiquidityPool: &xdr.LedgerKeyLiquidityPool{ + LiquidityPoolId: xdr.PoolId{1, 2, 3, 4, 5, 6, 7, 8, 9}, + }, + }, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeClawback, + ClawbackOp: &xdr.ClawbackOp{ + Asset: usdtAsset, + From: testAccount4, + Amount: 1598182, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeClawbackClaimableBalance, + ClawbackClaimableBalanceOp: &xdr.ClawbackClaimableBalanceOp{ + BalanceId: hardCodedClaimableBalance, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeSetTrustLineFlags, + SetTrustLineFlagsOp: &xdr.SetTrustLineFlagsOp{ + Trustor: testAccount4ID, + Asset: usdtAsset, + SetFlags: hardCodedSetFlags, + ClearFlags: hardCodedClearFlags, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeLiquidityPoolDeposit, + LiquidityPoolDepositOp: &xdr.LiquidityPoolDepositOp{ + LiquidityPoolId: xdr.PoolId{1, 2, 3, 4, 5, 6, 7, 8, 9}, + MaxAmountA: 1000, + MaxAmountB: 100, + MinPrice: xdr.Price{ + N: 1, + D: 1000000, + }, + MaxPrice: xdr.Price{ + N: 1000000, + D: 1, + }, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeLiquidityPoolWithdraw, + LiquidityPoolWithdrawOp: &xdr.LiquidityPoolWithdrawOp{ + LiquidityPoolId: xdr.PoolId{1, 2, 3, 4, 5, 6, 7, 8, 9}, + Amount: 4, + MinAmountA: 1, + MinAmountB: 1, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeInvokeHostFunction, + InvokeHostFunctionOp: &xdr.InvokeHostFunctionOp{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, + InvokeContract: &xdr.InvokeContractArgs{ + ContractAddress: xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + ContractId: &contractHash, + }, + FunctionName: "test", + Args: []xdr.ScVal{}, + }, + }, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeInvokeHostFunction, + InvokeHostFunctionOp: &xdr.InvokeHostFunctionOp{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeCreateContract, + CreateContract: &xdr.CreateContractArgs{ + ContractIdPreimage: xdr.ContractIdPreimage{ + Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAddress, + FromAddress: &xdr.ContractIdPreimageFromAddress{ + Address: xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + ContractId: &contractHash, + }, + Salt: salt, + }, + }, + Executable: xdr.ContractExecutable{}, + }, + }, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeInvokeHostFunction, + InvokeHostFunctionOp: &xdr.InvokeHostFunctionOp{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeCreateContract, + CreateContract: &xdr.CreateContractArgs{ + ContractIdPreimage: xdr.ContractIdPreimage{ + Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAsset, + FromAsset: &xdr.Asset{ + Type: xdr.AssetTypeAssetTypeCreditAlphanum12, + AlphaNum12: &xdr.AlphaNum12{ + AssetCode: assetCode, + Issuer: xdr.AccountId{ + Type: xdr.PublicKeyTypePublicKeyTypeEd25519, + Ed25519: &assetIssuer, + }, + }, + }, + }, + Executable: xdr.ContractExecutable{}, + }, + }, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeInvokeHostFunction, + InvokeHostFunctionOp: &xdr.InvokeHostFunctionOp{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeCreateContractV2, + CreateContractV2: &xdr.CreateContractArgsV2{ + ContractIdPreimage: xdr.ContractIdPreimage{ + Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAsset, + FromAsset: &xdr.Asset{ + Type: xdr.AssetTypeAssetTypeCreditAlphanum12, + AlphaNum12: &xdr.AlphaNum12{ + AssetCode: assetCode, + Issuer: xdr.AccountId{ + Type: xdr.PublicKeyTypePublicKeyTypeEd25519, + Ed25519: &assetIssuer, + }, + }, + }, + }, + Executable: xdr.ContractExecutable{}, + ConstructorArgs: []xdr.ScVal{ + { + Type: xdr.ScValTypeScvBool, + B: &dummyBool, + }, + }, + }, + }, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeInvokeHostFunction, + InvokeHostFunctionOp: &xdr.InvokeHostFunctionOp{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeUploadContractWasm, + Wasm: &wasm, + }, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeExtendFootprintTtl, + ExtendFootprintTtlOp: &xdr.ExtendFootprintTtlOp{ + Ext: xdr.ExtensionPoint{ + V: 0, + }, + ExtendTo: 1234, + }, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeRestoreFootprint, + RestoreFootprintOp: &xdr.RestoreFootprintOp{ + Ext: xdr.ExtensionPoint{ + V: 0, + }, + }, + }, + }, + } + inputEnvelope.Tx.Operations = inputOperations + results := []xdr.OperationResult{ + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeCreateAccount, + CreateAccountResult: &xdr.CreateAccountResult{ + Code: xdr.CreateAccountResultCodeCreateAccountSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypePayment, + PaymentResult: &xdr.PaymentResult{ + Code: xdr.PaymentResultCodePaymentSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypePayment, + PaymentResult: &xdr.PaymentResult{ + Code: xdr.PaymentResultCodePaymentSuccess, + }, + }, + }, + // There needs to be a true result for path payment receive and send + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypePathPaymentStrictReceive, + PathPaymentStrictReceiveResult: &xdr.PathPaymentStrictReceiveResult{ + Code: xdr.PathPaymentStrictReceiveResultCodePathPaymentStrictReceiveSuccess, + Success: &xdr.PathPaymentStrictReceiveResultSuccess{ + Last: xdr.SimplePaymentResult{Amount: 8946764349}, + }, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeManageSellOffer, + ManageSellOfferResult: &xdr.ManageSellOfferResult{ + Code: xdr.ManageSellOfferResultCodeManageSellOfferSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeManageSellOffer, + ManageSellOfferResult: &xdr.ManageSellOfferResult{ + Code: xdr.ManageSellOfferResultCodeManageSellOfferSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeSetOptions, + SetOptionsResult: &xdr.SetOptionsResult{ + Code: xdr.SetOptionsResultCodeSetOptionsSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeChangeTrust, + ChangeTrustResult: &xdr.ChangeTrustResult{ + Code: xdr.ChangeTrustResultCodeChangeTrustSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeChangeTrust, + ChangeTrustResult: &xdr.ChangeTrustResult{ + Code: xdr.ChangeTrustResultCodeChangeTrustSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeAllowTrust, + AllowTrustResult: &xdr.AllowTrustResult{ + Code: xdr.AllowTrustResultCodeAllowTrustSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeAccountMerge, + AccountMergeResult: &xdr.AccountMergeResult{ + Code: xdr.AccountMergeResultCodeAccountMergeSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeInflation, + InflationResult: &xdr.InflationResult{ + Code: xdr.InflationResultCodeInflationSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeManageData, + ManageDataResult: &xdr.ManageDataResult{ + Code: xdr.ManageDataResultCodeManageDataSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeBumpSequence, + BumpSeqResult: &xdr.BumpSequenceResult{ + Code: xdr.BumpSequenceResultCodeBumpSequenceSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeManageBuyOffer, + ManageBuyOfferResult: &xdr.ManageBuyOfferResult{ + Code: xdr.ManageBuyOfferResultCodeManageBuyOfferSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypePathPaymentStrictSend, + PathPaymentStrictSendResult: &xdr.PathPaymentStrictSendResult{ + Code: xdr.PathPaymentStrictSendResultCodePathPaymentStrictSendSuccess, + Success: &xdr.PathPaymentStrictSendResultSuccess{ + Last: xdr.SimplePaymentResult{Amount: 4334043858}, + }, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeCreateClaimableBalance, + CreateClaimableBalanceResult: &xdr.CreateClaimableBalanceResult{ + Code: xdr.CreateClaimableBalanceResultCodeCreateClaimableBalanceSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeClaimClaimableBalance, + ClaimClaimableBalanceResult: &xdr.ClaimClaimableBalanceResult{ + Code: xdr.ClaimClaimableBalanceResultCodeClaimClaimableBalanceSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeBeginSponsoringFutureReserves, + BeginSponsoringFutureReservesResult: &xdr.BeginSponsoringFutureReservesResult{ + Code: xdr.BeginSponsoringFutureReservesResultCodeBeginSponsoringFutureReservesSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipResult: &xdr.RevokeSponsorshipResult{ + Code: xdr.RevokeSponsorshipResultCodeRevokeSponsorshipSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipResult: &xdr.RevokeSponsorshipResult{ + Code: xdr.RevokeSponsorshipResultCodeRevokeSponsorshipSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipResult: &xdr.RevokeSponsorshipResult{ + Code: xdr.RevokeSponsorshipResultCodeRevokeSponsorshipSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipResult: &xdr.RevokeSponsorshipResult{ + Code: xdr.RevokeSponsorshipResultCodeRevokeSponsorshipSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipResult: &xdr.RevokeSponsorshipResult{ + Code: xdr.RevokeSponsorshipResultCodeRevokeSponsorshipSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipResult: &xdr.RevokeSponsorshipResult{ + Code: xdr.RevokeSponsorshipResultCodeRevokeSponsorshipSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeRevokeSponsorship, + RevokeSponsorshipResult: &xdr.RevokeSponsorshipResult{ + Code: xdr.RevokeSponsorshipResultCodeRevokeSponsorshipSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeClawback, + ClawbackResult: &xdr.ClawbackResult{ + Code: xdr.ClawbackResultCodeClawbackSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeClawbackClaimableBalance, + ClawbackClaimableBalanceResult: &xdr.ClawbackClaimableBalanceResult{ + Code: xdr.ClawbackClaimableBalanceResultCodeClawbackClaimableBalanceSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeSetTrustLineFlags, + SetTrustLineFlagsResult: &xdr.SetTrustLineFlagsResult{ + Code: xdr.SetTrustLineFlagsResultCodeSetTrustLineFlagsSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeLiquidityPoolDeposit, + LiquidityPoolDepositResult: &xdr.LiquidityPoolDepositResult{ + Code: xdr.LiquidityPoolDepositResultCodeLiquidityPoolDepositSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeLiquidityPoolWithdraw, + LiquidityPoolWithdrawResult: &xdr.LiquidityPoolWithdrawResult{ + Code: xdr.LiquidityPoolWithdrawResultCodeLiquidityPoolWithdrawSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeInvokeHostFunction, + InvokeHostFunctionResult: &xdr.InvokeHostFunctionResult{ + Code: xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeInvokeHostFunction, + InvokeHostFunctionResult: &xdr.InvokeHostFunctionResult{ + Code: xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeInvokeHostFunction, + InvokeHostFunctionResult: &xdr.InvokeHostFunctionResult{ + Code: xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeInvokeHostFunction, + InvokeHostFunctionResult: &xdr.InvokeHostFunctionResult{ + Code: xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeInvokeHostFunction, + InvokeHostFunctionResult: &xdr.InvokeHostFunctionResult{ + Code: xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeInvokeHostFunction, + InvokeHostFunctionResult: &xdr.InvokeHostFunctionResult{ + Code: xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeInvokeHostFunction, + InvokeHostFunctionResult: &xdr.InvokeHostFunctionResult{ + Code: xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess, + }, + }, + }, + } + inputTransaction.Result.Result.Result.Results = &results + inputTransaction.Envelope.V1 = &inputEnvelope + return +} + +func makeOperationTestOutputs() (transformedOperations []OperationOutput) { + hardCodedSourceAccountAddress := testAccount3Address + hardCodedDestAccountAddress := testAccount4Address + hardCodedLedgerClose := genericCloseTime.UTC() + var nilStringArray []string + + transformedOperations = []OperationOutput{ + { + SourceAccount: hardCodedSourceAccountAddress, + Type: 0, + TypeString: "create_account", + TransactionID: 4096, + OperationID: 4097, + OperationDetails: map[string]interface{}{ + "account": hardCodedDestAccountAddress, + "funder": hardCodedSourceAccountAddress, + "starting_balance": 2.5, + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "CreateAccountResultCodeCreateAccountSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "account": hardCodedDestAccountAddress, + "funder": hardCodedSourceAccountAddress, + "starting_balance": 2.5, + }, + }, + { + Type: 1, + TypeString: "payment", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4098, + OperationDetails: map[string]interface{}{ + "from": hardCodedSourceAccountAddress, + "to": hardCodedDestAccountAddress, + "amount": 35.0, + "asset_code": "USDT", + "asset_type": "credit_alphanum4", + "asset_issuer": hardCodedDestAccountAddress, + "asset_id": int64(-8205667356306085451), + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "PaymentResultCodePaymentSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "from": hardCodedSourceAccountAddress, + "to": hardCodedDestAccountAddress, + "amount": 35.0, + "asset_code": "USDT", + "asset_type": "credit_alphanum4", + "asset_issuer": hardCodedDestAccountAddress, + "asset_id": int64(-8205667356306085451), + }, + }, + { + Type: 1, + TypeString: "payment", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4099, + OperationDetails: map[string]interface{}{ + "from": hardCodedSourceAccountAddress, + "to": hardCodedDestAccountAddress, + "amount": 35.0, + "asset_type": "native", + "asset_id": int64(-5706705804583548011), + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "PaymentResultCodePaymentSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "from": hardCodedSourceAccountAddress, + "to": hardCodedDestAccountAddress, + "amount": 35.0, + "asset_type": "native", + "asset_id": int64(-5706705804583548011), + }, + }, + { + Type: 2, + TypeString: "path_payment_strict_receive", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4100, + OperationDetails: map[string]interface{}{ + "from": hardCodedSourceAccountAddress, + "to": hardCodedDestAccountAddress, + "source_amount": 894.6764349, + "source_max": 895.14959, + "amount": 895.14959, + "source_asset_type": "native", + "source_asset_id": int64(-5706705804583548011), + "asset_type": "native", + "asset_id": int64(-5706705804583548011), + "path": []Path{usdtAssetPath}, + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "PathPaymentStrictReceiveResultCodePathPaymentStrictReceiveSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "from": hardCodedSourceAccountAddress, + "to": hardCodedDestAccountAddress, + "source_amount": 894.6764349, + "source_max": 895.14959, + "amount": 895.14959, + "source_asset_type": "native", + "source_asset_id": int64(-5706705804583548011), + "asset_type": "native", + "asset_id": int64(-5706705804583548011), + "path": []Path{usdtAssetPath}, + }, + }, + { + Type: 3, + TypeString: "manage_sell_offer", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4101, + OperationDetails: map[string]interface{}{ + "price": 0.514092, + "amount": 76.586, + "offer_id": int64(0.0), + "price_r": Price{ + Numerator: 128523, + Denominator: 250000, + }, + "selling_asset_code": "USDT", + "selling_asset_type": "credit_alphanum4", + "selling_asset_issuer": hardCodedDestAccountAddress, + "selling_asset_id": int64(-8205667356306085451), + "buying_asset_type": "native", + "buying_asset_id": int64(-5706705804583548011), + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "ManageSellOfferResultCodeManageSellOfferSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "price": 0.514092, + "amount": 76.586, + "offer_id": int64(0.0), + "price_r": Price{ + Numerator: 128523, + Denominator: 250000, + }, + "selling_asset_code": "USDT", + "selling_asset_type": "credit_alphanum4", + "selling_asset_issuer": hardCodedDestAccountAddress, + "selling_asset_id": int64(-8205667356306085451), + "buying_asset_type": "native", + "buying_asset_id": int64(-5706705804583548011), + }, + }, + { + Type: 4, + TypeString: "create_passive_sell_offer", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4102, + OperationDetails: map[string]interface{}{ + "amount": 63.1595, + "price": 0.0791606, + "price_r": Price{ + Numerator: 99583200, + Denominator: 1257990000, + }, + "buying_asset_code": "USDT", + "buying_asset_type": "credit_alphanum4", + "buying_asset_issuer": hardCodedDestAccountAddress, + "buying_asset_id": int64(-8205667356306085451), + "selling_asset_type": "native", + "selling_asset_id": int64(-5706705804583548011), + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "ManageSellOfferResultCodeManageSellOfferSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "amount": 63.1595, + "price": 0.0791606, + "price_r": Price{ + Numerator: 99583200, + Denominator: 1257990000, + }, + "buying_asset_code": "USDT", + "buying_asset_type": "credit_alphanum4", + "buying_asset_issuer": hardCodedDestAccountAddress, + "buying_asset_id": int64(-8205667356306085451), + "selling_asset_type": "native", + "selling_asset_id": int64(-5706705804583548011), + }, + }, + { + Type: 5, + TypeString: "set_options", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4103, + OperationDetails: map[string]interface{}{ + "inflation_dest": hardCodedDestAccountAddress, + "clear_flags": []int32{1, 2}, + "clear_flags_s": []string{"auth_required", "auth_revocable"}, + "set_flags": []int32{4}, + "set_flags_s": []string{"auth_immutable"}, + "master_key_weight": uint32(3), + "low_threshold": uint32(1), + "med_threshold": uint32(3), + "high_threshold": uint32(5), + "home_domain": "2019=DRA;n-test", + "signer_key": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + "signer_weight": uint32(1), + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "SetOptionsResultCodeSetOptionsSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "inflation_dest": hardCodedDestAccountAddress, + "clear_flags": []int32{1, 2}, + "clear_flags_s": []string{"auth_required", "auth_revocable"}, + "set_flags": []int32{4}, + "set_flags_s": []string{"auth_immutable"}, + "master_key_weight": uint32(3), + "low_threshold": uint32(1), + "med_threshold": uint32(3), + "high_threshold": uint32(5), + "home_domain": "2019=DRA;n-test", + "signer_key": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + "signer_weight": uint32(1), + }, + }, + { + Type: 6, + TypeString: "change_trust", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4104, + OperationDetails: map[string]interface{}{ + "trustor": hardCodedSourceAccountAddress, + "trustee": hardCodedDestAccountAddress, + "limit": 50000000000.0, + "asset_code": "USSD", + "asset_type": "credit_alphanum4", + "asset_issuer": hardCodedDestAccountAddress, + "asset_id": int64(6690054458235693884), + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "ChangeTrustResultCodeChangeTrustSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "trustor": hardCodedSourceAccountAddress, + "trustee": hardCodedDestAccountAddress, + "limit": 50000000000.0, + "asset_code": "USSD", + "asset_type": "credit_alphanum4", + "asset_issuer": hardCodedDestAccountAddress, + "asset_id": int64(6690054458235693884), + }, + }, + { + Type: 6, + TypeString: "change_trust", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4105, + OperationDetails: map[string]interface{}{ + "trustor": hardCodedSourceAccountAddress, + "limit": 50000000000.0, + "asset_type": "liquidity_pool_shares", + "liquidity_pool_id": "185a6b384c651552ba09b32851b79f5f6ab61e80883d303f52bea1406a4923f0", + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "ChangeTrustResultCodeChangeTrustSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "trustor": hardCodedSourceAccountAddress, + "limit": 50000000000.0, + "asset_type": "liquidity_pool_shares", + "liquidity_pool_id": "185a6b384c651552ba09b32851b79f5f6ab61e80883d303f52bea1406a4923f0", + }, + }, + { + Type: 7, + TypeString: "allow_trust", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4106, + OperationDetails: map[string]interface{}{ + "trustee": hardCodedSourceAccountAddress, + "trustor": hardCodedDestAccountAddress, + "authorize": true, + "asset_code": "USDT", + "asset_type": "credit_alphanum4", + "asset_issuer": hardCodedSourceAccountAddress, + "asset_id": int64(8485542065083974675), + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "AllowTrustResultCodeAllowTrustSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "trustee": hardCodedSourceAccountAddress, + "trustor": hardCodedDestAccountAddress, + "authorize": true, + "asset_code": "USDT", + "asset_type": "credit_alphanum4", + "asset_issuer": hardCodedSourceAccountAddress, + "asset_id": int64(8485542065083974675), + }, + }, + { + Type: 8, + TypeString: "account_merge", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4107, + OperationDetails: map[string]interface{}{ + "account": hardCodedSourceAccountAddress, + "into": hardCodedDestAccountAddress, + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "AccountMergeResultCodeAccountMergeSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "account": hardCodedSourceAccountAddress, + "into": hardCodedDestAccountAddress, + }, + }, + { + Type: 9, + TypeString: "inflation", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4108, + OperationDetails: map[string]interface{}{}, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "InflationResultCodeInflationSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{}, + }, + { + Type: 10, + TypeString: "manage_data", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4109, + OperationDetails: map[string]interface{}{ + "name": "test", + "value": base64.StdEncoding.EncodeToString([]byte{0x76, 0x61, 0x6c, 0x75, 0x65}), + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "ManageDataResultCodeManageDataSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "name": "test", + "value": base64.StdEncoding.EncodeToString([]byte{0x76, 0x61, 0x6c, 0x75, 0x65}), + }, + }, + { + Type: 11, + TypeString: "bump_sequence", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4110, + OperationDetails: map[string]interface{}{ + "bump_to": "100", + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "BumpSequenceResultCodeBumpSequenceSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "bump_to": "100", + }, + }, + { + Type: 12, + TypeString: "manage_buy_offer", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4111, + OperationDetails: map[string]interface{}{ + "price": 0.3496823, + "amount": 765.4501001, + "price_r": Price{ + Numerator: 635863285, + Denominator: 1818402817, + }, + "selling_asset_code": "USDT", + "selling_asset_type": "credit_alphanum4", + "selling_asset_issuer": hardCodedDestAccountAddress, + "selling_asset_id": int64(-8205667356306085451), + "buying_asset_type": "native", + "buying_asset_id": int64(-5706705804583548011), + "offer_id": int64(100), + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "ManageBuyOfferResultCodeManageBuyOfferSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "price": 0.3496823, + "amount": 765.4501001, + "price_r": Price{ + Numerator: 635863285, + Denominator: 1818402817, + }, + "selling_asset_code": "USDT", + "selling_asset_type": "credit_alphanum4", + "selling_asset_issuer": hardCodedDestAccountAddress, + "selling_asset_id": int64(-8205667356306085451), + "buying_asset_type": "native", + "buying_asset_id": int64(-5706705804583548011), + "offer_id": int64(100), + }, + }, + { + Type: 13, + TypeString: "path_payment_strict_send", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4112, + OperationDetails: map[string]interface{}{ + "from": hardCodedSourceAccountAddress, + "to": hardCodedDestAccountAddress, + "source_amount": 0.1598182, + "destination_min": "428.0460538", + "amount": 433.4043858, + "path": []Path{usdtAssetPath}, + "source_asset_type": "native", + "source_asset_id": int64(-5706705804583548011), + "asset_type": "native", + "asset_id": int64(-5706705804583548011), + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "PathPaymentStrictSendResultCodePathPaymentStrictSendSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "from": hardCodedSourceAccountAddress, + "to": hardCodedDestAccountAddress, + "source_amount": 0.1598182, + "destination_min": "428.0460538", + "amount": 433.4043858, + "path": []Path{usdtAssetPath}, + "source_asset_type": "native", + "source_asset_id": int64(-5706705804583548011), + "asset_type": "native", + "asset_id": int64(-5706705804583548011), + }, + }, + { + Type: 14, + TypeString: "create_claimable_balance", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4113, + OperationDetails: map[string]interface{}{ + "asset": "USDT:GBVVRXLMNCJQW3IDDXC3X6XCH35B5Q7QXNMMFPENSOGUPQO7WO7HGZPA", + "amount": 123456.789, + "claimants": []Claimant{testClaimantDetails}, + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "CreateClaimableBalanceResultCodeCreateClaimableBalanceSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "asset": "USDT:GBVVRXLMNCJQW3IDDXC3X6XCH35B5Q7QXNMMFPENSOGUPQO7WO7HGZPA", + "amount": 123456.789, + "claimants": []Claimant{testClaimantDetails}, + }, + }, + { + Type: 15, + TypeString: "claim_claimable_balance", + SourceAccount: testAccount3Address, + TransactionID: 4096, + OperationID: 4114, + OperationDetails: map[string]interface{}{ + "claimant": hardCodedSourceAccountAddress, + "balance_id": "000000000102030405060708090000000000000000000000000000000000000000000000", + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "ClaimClaimableBalanceResultCodeClaimClaimableBalanceSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "claimant": hardCodedSourceAccountAddress, + "balance_id": "000000000102030405060708090000000000000000000000000000000000000000000000", + }, + }, + { + Type: 16, + TypeString: "begin_sponsoring_future_reserves", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4115, + OperationDetails: map[string]interface{}{ + "sponsored_id": hardCodedDestAccountAddress, + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "BeginSponsoringFutureReservesResultCodeBeginSponsoringFutureReservesSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "sponsored_id": hardCodedDestAccountAddress, + }, + }, + { + Type: 18, + TypeString: "revoke_sponsorship", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4116, + OperationDetails: map[string]interface{}{ + "signer_account_id": hardCodedDestAccountAddress, + "signer_key": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "RevokeSponsorshipResultCodeRevokeSponsorshipSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "signer_account_id": hardCodedDestAccountAddress, + "signer_key": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + }, + }, + { + Type: 18, + TypeString: "revoke_sponsorship", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4117, + OperationDetails: map[string]interface{}{ + "account_id": hardCodedDestAccountAddress, + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "RevokeSponsorshipResultCodeRevokeSponsorshipSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "account_id": hardCodedDestAccountAddress, + }, + }, + { + Type: 18, + TypeString: "revoke_sponsorship", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4118, + OperationDetails: map[string]interface{}{ + "claimable_balance_id": "000000000102030405060708090000000000000000000000000000000000000000000000", + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "RevokeSponsorshipResultCodeRevokeSponsorshipSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "claimable_balance_id": "000000000102030405060708090000000000000000000000000000000000000000000000", + }, + }, + { + Type: 18, + TypeString: "revoke_sponsorship", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4119, + OperationDetails: map[string]interface{}{ + "data_account_id": hardCodedDestAccountAddress, + "data_name": "test", + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "RevokeSponsorshipResultCodeRevokeSponsorshipSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "data_account_id": hardCodedDestAccountAddress, + "data_name": "test", + }, + }, + { + Type: 18, + TypeString: "revoke_sponsorship", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4120, + OperationDetails: map[string]interface{}{ + "offer_id": int64(100), + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "RevokeSponsorshipResultCodeRevokeSponsorshipSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "offer_id": int64(100), + }, + }, + { + Type: 18, + TypeString: "revoke_sponsorship", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4121, + OperationDetails: map[string]interface{}{ + "trustline_account_id": testAccount3Address, + "trustline_asset": "USTT:GBT4YAEGJQ5YSFUMNKX6BPBUOCPNAIOFAVZOF6MIME2CECBMEIUXFZZN", + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "RevokeSponsorshipResultCodeRevokeSponsorshipSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "trustline_account_id": testAccount3Address, + "trustline_asset": "USTT:GBT4YAEGJQ5YSFUMNKX6BPBUOCPNAIOFAVZOF6MIME2CECBMEIUXFZZN", + }, + }, + { + Type: 18, + TypeString: "revoke_sponsorship", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4122, + OperationDetails: map[string]interface{}{ + "liquidity_pool_id": "0102030405060708090000000000000000000000000000000000000000000000", + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "RevokeSponsorshipResultCodeRevokeSponsorshipSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "liquidity_pool_id": "0102030405060708090000000000000000000000000000000000000000000000", + }, + }, + { + Type: 19, + TypeString: "clawback", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4123, + OperationDetails: map[string]interface{}{ + "from": hardCodedDestAccountAddress, + "amount": 0.1598182, + "asset_code": "USDT", + "asset_issuer": "GBVVRXLMNCJQW3IDDXC3X6XCH35B5Q7QXNMMFPENSOGUPQO7WO7HGZPA", + "asset_type": "credit_alphanum4", + "asset_id": int64(-8205667356306085451), + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "ClawbackResultCodeClawbackSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "from": hardCodedDestAccountAddress, + "amount": 0.1598182, + "asset_code": "USDT", + "asset_issuer": "GBVVRXLMNCJQW3IDDXC3X6XCH35B5Q7QXNMMFPENSOGUPQO7WO7HGZPA", + "asset_type": "credit_alphanum4", + "asset_id": int64(-8205667356306085451), + }, + }, + { + Type: 20, + TypeString: "clawback_claimable_balance", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4124, + OperationDetails: map[string]interface{}{ + "balance_id": "000000000102030405060708090000000000000000000000000000000000000000000000", + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "ClawbackClaimableBalanceResultCodeClawbackClaimableBalanceSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "balance_id": "000000000102030405060708090000000000000000000000000000000000000000000000", + }, + }, + { + Type: 21, + TypeString: "set_trust_line_flags", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4125, + OperationDetails: map[string]interface{}{ + "asset_code": "USDT", + "asset_issuer": "GBVVRXLMNCJQW3IDDXC3X6XCH35B5Q7QXNMMFPENSOGUPQO7WO7HGZPA", + "asset_type": "credit_alphanum4", + "asset_id": int64(-8205667356306085451), + "trustor": testAccount4Address, + "clear_flags": []int32{1, 2}, + "clear_flags_s": []string{"authorized", "authorized_to_maintain_liabilities"}, + "set_flags": []int32{4}, + "set_flags_s": []string{"clawback_enabled"}, + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "SetTrustLineFlagsResultCodeSetTrustLineFlagsSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "asset_code": "USDT", + "asset_issuer": "GBVVRXLMNCJQW3IDDXC3X6XCH35B5Q7QXNMMFPENSOGUPQO7WO7HGZPA", + "asset_type": "credit_alphanum4", + "asset_id": int64(-8205667356306085451), + "trustor": testAccount4Address, + "clear_flags": []int32{1, 2}, + "clear_flags_s": []string{"authorized", "authorized_to_maintain_liabilities"}, + "set_flags": []int32{4}, + "set_flags_s": []string{"clawback_enabled"}, + }, + }, + { + Type: 22, + TypeString: "liquidity_pool_deposit", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4126, + OperationDetails: map[string]interface{}{ + "liquidity_pool_id": "0102030405060708090000000000000000000000000000000000000000000000", + "reserve_a_asset_type": "native", + "reserve_a_asset_id": int64(-5706705804583548011), + "reserve_a_max_amount": 0.0001, + "reserve_a_deposit_amount": 0.0001, + "reserve_b_asset_type": "credit_alphanum4", + "reserve_b_asset_code": "USSD", + "reserve_b_asset_issuer": "GBVVRXLMNCJQW3IDDXC3X6XCH35B5Q7QXNMMFPENSOGUPQO7WO7HGZPA", + "reserve_b_asset_id": int64(6690054458235693884), + "reserve_b_deposit_amount": 0.00001, + "reserve_b_max_amount": 0.00001, + "max_price": 1000000.0000000, + "max_price_r": Price{ + Numerator: 1000000, + Denominator: 1, + }, + "min_price": 0.0000010, + "min_price_r": Price{ + Numerator: 1, + Denominator: 1000000, + }, + "shares_received": 0.0000002, + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "LiquidityPoolDepositResultCodeLiquidityPoolDepositSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "liquidity_pool_id": "0102030405060708090000000000000000000000000000000000000000000000", + "reserve_a_asset_type": "native", + "reserve_a_asset_id": int64(-5706705804583548011), + "reserve_a_max_amount": 0.0001, + "reserve_a_deposit_amount": 0.0001, + "reserve_b_asset_type": "credit_alphanum4", + "reserve_b_asset_code": "USSD", + "reserve_b_asset_issuer": "GBVVRXLMNCJQW3IDDXC3X6XCH35B5Q7QXNMMFPENSOGUPQO7WO7HGZPA", + "reserve_b_asset_id": int64(6690054458235693884), + "reserve_b_deposit_amount": 0.00001, + "reserve_b_max_amount": 0.00001, + "max_price": 1000000.0000000, + "max_price_r": Price{ + Numerator: 1000000, + Denominator: 1, + }, + "min_price": 0.0000010, + "min_price_r": Price{ + Numerator: 1, + Denominator: 1000000, + }, + "shares_received": 0.0000002, + }, + }, + { + Type: 23, + TypeString: "liquidity_pool_withdraw", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4127, + OperationDetails: map[string]interface{}{ + "liquidity_pool_id": "0102030405060708090000000000000000000000000000000000000000000000", + "reserve_a_asset_type": "native", + "reserve_a_asset_id": int64(-5706705804583548011), + "reserve_a_min_amount": 0.0000001, + "reserve_a_withdraw_amount": -0.0001, + "reserve_b_asset_type": "credit_alphanum4", + "reserve_b_asset_code": "USSD", + "reserve_b_asset_issuer": "GBVVRXLMNCJQW3IDDXC3X6XCH35B5Q7QXNMMFPENSOGUPQO7WO7HGZPA", + "reserve_b_asset_id": int64(6690054458235693884), + "reserve_b_withdraw_amount": -0.00001, + "reserve_b_min_amount": 0.0000001, + "shares": 0.0000004, + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "LiquidityPoolWithdrawResultCodeLiquidityPoolWithdrawSuccess", + LedgerSequence: 0, + OperationDetailsJSON: map[string]interface{}{ + "liquidity_pool_id": "0102030405060708090000000000000000000000000000000000000000000000", + "reserve_a_asset_type": "native", + "reserve_a_asset_id": int64(-5706705804583548011), + "reserve_a_min_amount": 0.0000001, + "reserve_a_withdraw_amount": -0.0001, + "reserve_b_asset_type": "credit_alphanum4", + "reserve_b_asset_code": "USSD", + "reserve_b_asset_issuer": "GBVVRXLMNCJQW3IDDXC3X6XCH35B5Q7QXNMMFPENSOGUPQO7WO7HGZPA", + "reserve_b_asset_id": int64(6690054458235693884), + "reserve_b_withdraw_amount": -0.00001, + "reserve_b_min_amount": 0.0000001, + "shares": 0.0000004, + }, + }, + { + Type: 24, + TypeString: "invoke_host_function", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4128, + OperationDetails: map[string]interface{}{ + "function": "HostFunctionTypeHostFunctionTypeInvokeContract", + "type": "invoke_contract", + "contract_id": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + "contract_code_hash": "", + "asset_balance_changes": []map[string]interface{}{}, + "ledger_key_hash": nilStringArray, + "parameters": []map[string]string{ + { + "type": "Address", + "value": "AAAAEgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + }, + { + "type": "Sym", + "value": "AAAADwAAAAR0ZXN0", + }, + }, + "parameters_decoded": []map[string]string{ + { + "type": "Address", + "value": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + }, + { + "type": "Sym", + "value": "test", + }, + }, + }, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "InvokeHostFunctionResultCodeInvokeHostFunctionSuccess", + ClosedAt: hardCodedLedgerClose, + OperationDetailsJSON: map[string]interface{}{ + "function": "HostFunctionTypeHostFunctionTypeInvokeContract", + "type": "invoke_contract", + "contract_id": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + "contract_code_hash": "", + "asset_balance_changes": []map[string]interface{}{}, + "ledger_key_hash": nilStringArray, + "parameters": []map[string]string{ + { + "type": "Address", + "value": "AAAAEgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + }, + { + "type": "Sym", + "value": "AAAADwAAAAR0ZXN0", + }, + }, + "parameters_decoded": []map[string]string{ + { + "type": "Address", + "value": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + }, + { + "type": "Sym", + "value": "test", + }, + }, + }, + }, + { + Type: 24, + TypeString: "invoke_host_function", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4129, + OperationDetails: map[string]interface{}{ + "function": "HostFunctionTypeHostFunctionTypeCreateContract", + "type": "create_contract", + "contract_id": "", + "contract_code_hash": "", + "from": "address", + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + "ledger_key_hash": nilStringArray, + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "InvokeHostFunctionResultCodeInvokeHostFunctionSuccess", + OperationDetailsJSON: map[string]interface{}{ + "function": "HostFunctionTypeHostFunctionTypeCreateContract", + "type": "create_contract", + "contract_id": "", + "contract_code_hash": "", + "from": "address", + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", + "ledger_key_hash": nilStringArray, + }, + }, + { + Type: 24, + TypeString: "invoke_host_function", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4130, + OperationDetails: map[string]interface{}{ + "function": "HostFunctionTypeHostFunctionTypeCreateContract", + "type": "create_contract", + "contract_id": "", + "contract_code_hash": "", + "from": "asset", + "asset": ":GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + "ledger_key_hash": nilStringArray, + }, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "InvokeHostFunctionResultCodeInvokeHostFunctionSuccess", + ClosedAt: hardCodedLedgerClose, + OperationDetailsJSON: map[string]interface{}{ + "function": "HostFunctionTypeHostFunctionTypeCreateContract", + "type": "create_contract", + "contract_id": "", + "contract_code_hash": "", + "from": "asset", + "asset": ":GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + "ledger_key_hash": nilStringArray, + }, + }, + { + Type: 24, + TypeString: "invoke_host_function", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4131, + OperationDetails: map[string]interface{}{ + "function": "HostFunctionTypeHostFunctionTypeCreateContractV2", + "type": "create_contract_v2", + "contract_id": "", + "contract_code_hash": "", + "from": "asset", + "asset": ":GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + "ledger_key_hash": nilStringArray, + "parameters": []map[string]string{ + { + "type": "B", + "value": "AAAAAAAAAAE=", + }, + }, + "parameters_decoded": []map[string]string{ + { + "type": "B", + "value": "true", + }, + }, + }, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "InvokeHostFunctionResultCodeInvokeHostFunctionSuccess", + ClosedAt: hardCodedLedgerClose, + OperationDetailsJSON: map[string]interface{}{ + "function": "HostFunctionTypeHostFunctionTypeCreateContractV2", + "type": "create_contract_v2", + "contract_id": "", + "contract_code_hash": "", + "from": "asset", + "asset": ":GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", + "ledger_key_hash": nilStringArray, + "parameters": []map[string]string{ + { + "type": "B", + "value": "AAAAAAAAAAE=", + }, + }, + "parameters_decoded": []map[string]string{ + { + "type": "B", + "value": "true", + }, + }, + }, + }, + { + Type: 24, + TypeString: "invoke_host_function", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4132, + OperationDetails: map[string]interface{}{ + "function": "HostFunctionTypeHostFunctionTypeUploadContractWasm", + "type": "upload_wasm", + "contract_code_hash": "", + "ledger_key_hash": nilStringArray, + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "InvokeHostFunctionResultCodeInvokeHostFunctionSuccess", + OperationDetailsJSON: map[string]interface{}{ + "function": "HostFunctionTypeHostFunctionTypeUploadContractWasm", + "type": "upload_wasm", + "contract_code_hash": "", + "ledger_key_hash": nilStringArray, + }, + }, + { + Type: 25, + TypeString: "extend_footprint_ttl", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4133, + OperationDetails: map[string]interface{}{ + "type": "extend_footprint_ttl", + "extend_to": xdr.Uint32(1234), + "contract_id": "", + "contract_code_hash": "", + "ledger_key_hash": nilStringArray, + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "InvokeHostFunctionResultCodeInvokeHostFunctionSuccess", + OperationDetailsJSON: map[string]interface{}{ + "type": "extend_footprint_ttl", + "extend_to": xdr.Uint32(1234), + "contract_id": "", + "contract_code_hash": "", + "ledger_key_hash": nilStringArray, + }, + }, + { + Type: 26, + TypeString: "restore_footprint", + SourceAccount: hardCodedSourceAccountAddress, + TransactionID: 4096, + OperationID: 4134, + OperationDetails: map[string]interface{}{ + "type": "restore_footprint", + "contract_id": "", + "contract_code_hash": "", + "ledger_key_hash": nilStringArray, + }, + ClosedAt: hardCodedLedgerClose, + OperationResultCode: "OperationResultCodeOpInner", + OperationTraceCode: "InvokeHostFunctionResultCodeInvokeHostFunctionSuccess", + OperationDetailsJSON: map[string]interface{}{ + "type": "restore_footprint", + "contract_id": "", + "contract_code_hash": "", + "ledger_key_hash": nilStringArray, + }, + }, + } + return +} diff --git a/ingest/processors/schema.go b/ingest/processors/schema.go new file mode 100644 index 0000000000..da0e92b85d --- /dev/null +++ b/ingest/processors/schema.go @@ -0,0 +1,637 @@ +package processors + +import ( + "time" + + "github.com/guregu/null" + "github.com/guregu/null/zero" + "github.com/lib/pq" + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/xdr" +) + +// LedgerOutput is a representation of a ledger that aligns with the BigQuery table history_ledgers +type LedgerOutput struct { + Sequence uint32 `json:"sequence"` // sequence number of the ledger + LedgerHash string `json:"ledger_hash"` + PreviousLedgerHash string `json:"previous_ledger_hash"` + LedgerHeader string `json:"ledger_header"` // base 64 encoding of the ledger header + TransactionCount int32 `json:"transaction_count"` + OperationCount int32 `json:"operation_count"` // counts only operations that were a part of successful transactions + SuccessfulTransactionCount int32 `json:"successful_transaction_count"` + FailedTransactionCount int32 `json:"failed_transaction_count"` + TxSetOperationCount string `json:"tx_set_operation_count"` // counts all operations, even those that are part of failed transactions + ClosedAt time.Time `json:"closed_at"` // UTC timestamp + TotalCoins int64 `json:"total_coins"` + FeePool int64 `json:"fee_pool"` + BaseFee uint32 `json:"base_fee"` + BaseReserve uint32 `json:"base_reserve"` + MaxTxSetSize uint32 `json:"max_tx_set_size"` + ProtocolVersion uint32 `json:"protocol_version"` + LedgerID int64 `json:"id"` + SorobanFeeWrite1Kb int64 `json:"soroban_fee_write_1kb"` + NodeID string `json:"node_id"` + Signature string `json:"signature"` + TotalByteSizeOfBucketList uint64 `json:"total_byte_size_of_bucket_list"` +} + +// TransactionOutput is a representation of a transaction that aligns with the BigQuery table history_transactions +type TransactionOutput struct { + TransactionHash string `json:"transaction_hash"` + LedgerSequence uint32 `json:"ledger_sequence"` + Account string `json:"account"` + AccountMuxed string `json:"account_muxed,omitempty"` + AccountSequence int64 `json:"account_sequence"` + MaxFee uint32 `json:"max_fee"` + FeeCharged int64 `json:"fee_charged"` + OperationCount int32 `json:"operation_count"` + TxEnvelope string `json:"tx_envelope"` + TxResult string `json:"tx_result"` + TxMeta string `json:"tx_meta"` + TxFeeMeta string `json:"tx_fee_meta"` + CreatedAt time.Time `json:"created_at"` + MemoType string `json:"memo_type"` + Memo string `json:"memo"` + TimeBounds string `json:"time_bounds"` + Successful bool `json:"successful"` + TransactionID int64 `json:"id"` + FeeAccount string `json:"fee_account,omitempty"` + FeeAccountMuxed string `json:"fee_account_muxed,omitempty"` + InnerTransactionHash string `json:"inner_transaction_hash,omitempty"` + NewMaxFee uint32 `json:"new_max_fee,omitempty"` + LedgerBounds string `json:"ledger_bounds"` + MinAccountSequence null.Int `json:"min_account_sequence"` + MinAccountSequenceAge null.Int `json:"min_account_sequence_age"` + MinAccountSequenceLedgerGap null.Int `json:"min_account_sequence_ledger_gap"` + ExtraSigners pq.StringArray `json:"extra_signers"` + ClosedAt time.Time `json:"closed_at"` + ResourceFee int64 `json:"resource_fee"` + SorobanResourcesInstructions uint32 `json:"soroban_resources_instructions"` + SorobanResourcesReadBytes uint32 `json:"soroban_resources_read_bytes"` + SorobanResourcesWriteBytes uint32 `json:"soroban_resources_write_bytes"` + TransactionResultCode string `json:"transaction_result_code"` + InclusionFeeBid int64 `json:"inclusion_fee_bid"` + InclusionFeeCharged int64 `json:"inclusion_fee_charged"` + ResourceFeeRefund int64 `json:"resource_fee_refund"` + TotalNonRefundableResourceFeeCharged int64 `json:"non_refundable_resource_fee_charged"` + TotalRefundableResourceFeeCharged int64 `json:"refundable_resource_fee_charged"` + RentFeeCharged int64 `json:"rent_fee_charged"` + TxSigners []string `json:"tx_signers"` +} + +type LedgerTransactionOutput struct { + LedgerSequence uint32 `json:"ledger_sequence"` + TxEnvelope string `json:"tx_envelope"` + TxResult string `json:"tx_result"` + TxMeta string `json:"tx_meta"` + TxFeeMeta string `json:"tx_fee_meta"` + TxLedgerHistory string `json:"tx_ledger_history"` + ClosedAt time.Time `json:"closed_at"` +} + +// AccountOutput is a representation of an account that aligns with the BigQuery table accounts +type AccountOutput struct { + AccountID string `json:"account_id"` // account address + Balance float64 `json:"balance"` + BuyingLiabilities float64 `json:"buying_liabilities"` + SellingLiabilities float64 `json:"selling_liabilities"` + SequenceNumber int64 `json:"sequence_number"` + SequenceLedger zero.Int `json:"sequence_ledger"` + SequenceTime zero.Int `json:"sequence_time"` + NumSubentries uint32 `json:"num_subentries"` + InflationDestination string `json:"inflation_destination"` + Flags uint32 `json:"flags"` + HomeDomain string `json:"home_domain"` + MasterWeight int32 `json:"master_weight"` + ThresholdLow int32 `json:"threshold_low"` + ThresholdMedium int32 `json:"threshold_medium"` + ThresholdHigh int32 `json:"threshold_high"` + Sponsor null.String `json:"sponsor"` + NumSponsored uint32 `json:"num_sponsored"` + NumSponsoring uint32 `json:"num_sponsoring"` + LastModifiedLedger uint32 `json:"last_modified_ledger"` + LedgerEntryChange uint32 `json:"ledger_entry_change"` + Deleted bool `json:"deleted"` + ClosedAt time.Time `json:"closed_at"` + LedgerSequence uint32 `json:"ledger_sequence"` +} + +// AccountSignerOutput is a representation of an account signer that aligns with the BigQuery table account_signers +type AccountSignerOutput struct { + AccountID string `json:"account_id"` + Signer string `json:"signer"` + Weight int32 `json:"weight"` + Sponsor null.String `json:"sponsor"` + LastModifiedLedger uint32 `json:"last_modified_ledger"` + LedgerEntryChange uint32 `json:"ledger_entry_change"` + Deleted bool `json:"deleted"` + ClosedAt time.Time `json:"closed_at"` + LedgerSequence uint32 `json:"ledger_sequence"` +} + +// OperationOutput is a representation of an operation that aligns with the BigQuery table history_operations +type OperationOutput struct { + SourceAccount string `json:"source_account"` + SourceAccountMuxed string `json:"source_account_muxed,omitempty"` + Type int32 `json:"type"` + TypeString string `json:"type_string"` + OperationDetails map[string]interface{} `json:"details"` //Details is a JSON object that varies based on operation type + TransactionID int64 `json:"transaction_id"` + OperationID int64 `json:"id"` + ClosedAt time.Time `json:"closed_at"` + OperationResultCode string `json:"operation_result_code"` + OperationTraceCode string `json:"operation_trace_code"` + LedgerSequence uint32 `json:"ledger_sequence"` + OperationDetailsJSON map[string]interface{} `json:"details_json"` +} + +// ClaimableBalanceOutput is a representation of a claimable balances that aligns with the BigQuery table claimable_balances +type ClaimableBalanceOutput struct { + BalanceID string `json:"balance_id"` + Claimants []Claimant `json:"claimants"` + AssetCode string `json:"asset_code"` + AssetIssuer string `json:"asset_issuer"` + AssetType string `json:"asset_type"` + AssetID int64 `json:"asset_id"` + AssetAmount float64 `json:"asset_amount"` + Sponsor null.String `json:"sponsor"` + Flags uint32 `json:"flags"` + LastModifiedLedger uint32 `json:"last_modified_ledger"` + LedgerEntryChange uint32 `json:"ledger_entry_change"` + Deleted bool `json:"deleted"` + ClosedAt time.Time `json:"closed_at"` + LedgerSequence uint32 `json:"ledger_sequence"` +} + +// Claimants +type Claimant struct { + Destination string `json:"destination"` + Predicate xdr.ClaimPredicate `json:"predicate"` +} + +// Price represents the price of an asset as a fraction +type Price struct { + Numerator int32 `json:"n"` + Denominator int32 `json:"d"` +} + +// Path is a representation of an asset without an ID that forms part of a path in a path payment +type Path struct { + AssetCode string `json:"asset_code"` + AssetIssuer string `json:"asset_issuer"` + AssetType string `json:"asset_type"` +} + +// LiquidityPoolAsset represents the asset pairs in a liquidity pool +type LiquidityPoolAsset struct { + AssetAType string + AssetACode string + AssetAIssuer string + AssetAAmount float64 + AssetBType string + AssetBCode string + AssetBIssuer string + AssetBAmount float64 +} + +// PoolOutput is a representation of a liquidity pool that aligns with the Bigquery table liquidity_pools +type PoolOutput struct { + PoolID string `json:"liquidity_pool_id"` + PoolType string `json:"type"` + PoolFee uint32 `json:"fee"` + TrustlineCount uint64 `json:"trustline_count"` + PoolShareCount float64 `json:"pool_share_count"` + AssetAType string `json:"asset_a_type"` + AssetACode string `json:"asset_a_code"` + AssetAIssuer string `json:"asset_a_issuer"` + AssetAReserve float64 `json:"asset_a_amount"` + AssetAID int64 `json:"asset_a_id"` + AssetBType string `json:"asset_b_type"` + AssetBCode string `json:"asset_b_code"` + AssetBIssuer string `json:"asset_b_issuer"` + AssetBReserve float64 `json:"asset_b_amount"` + AssetBID int64 `json:"asset_b_id"` + LastModifiedLedger uint32 `json:"last_modified_ledger"` + LedgerEntryChange uint32 `json:"ledger_entry_change"` + Deleted bool `json:"deleted"` + ClosedAt time.Time `json:"closed_at"` + LedgerSequence uint32 `json:"ledger_sequence"` +} + +// AssetOutput is a representation of an asset that aligns with the BigQuery table history_assets +type AssetOutput struct { + AssetCode string `json:"asset_code"` + AssetIssuer string `json:"asset_issuer"` + AssetType string `json:"asset_type"` + AssetID int64 `json:"asset_id"` + ClosedAt time.Time `json:"closed_at"` + LedgerSequence uint32 `json:"ledger_sequence"` +} + +// TrustlineOutput is a representation of a trustline that aligns with the BigQuery table trust_lines +type TrustlineOutput struct { + LedgerKey string `json:"ledger_key"` + AccountID string `json:"account_id"` + AssetCode string `json:"asset_code"` + AssetIssuer string `json:"asset_issuer"` + AssetType string `json:"asset_type"` + AssetID int64 `json:"asset_id"` + Balance float64 `json:"balance"` + TrustlineLimit int64 `json:"trust_line_limit"` + LiquidityPoolID string `json:"liquidity_pool_id"` + BuyingLiabilities float64 `json:"buying_liabilities"` + SellingLiabilities float64 `json:"selling_liabilities"` + Flags uint32 `json:"flags"` + LastModifiedLedger uint32 `json:"last_modified_ledger"` + LedgerEntryChange uint32 `json:"ledger_entry_change"` + Sponsor null.String `json:"sponsor"` + Deleted bool `json:"deleted"` + ClosedAt time.Time `json:"closed_at"` + LedgerSequence uint32 `json:"ledger_sequence"` +} + +// OfferOutput is a representation of an offer that aligns with the BigQuery table offers +type OfferOutput struct { + SellerID string `json:"seller_id"` // Account address of the seller + OfferID int64 `json:"offer_id"` + SellingAssetType string `json:"selling_asset_type"` + SellingAssetCode string `json:"selling_asset_code"` + SellingAssetIssuer string `json:"selling_asset_issuer"` + SellingAssetID int64 `json:"selling_asset_id"` + BuyingAssetType string `json:"buying_asset_type"` + BuyingAssetCode string `json:"buying_asset_code"` + BuyingAssetIssuer string `json:"buying_asset_issuer"` + BuyingAssetID int64 `json:"buying_asset_id"` + Amount float64 `json:"amount"` + PriceN int32 `json:"pricen"` + PriceD int32 `json:"priced"` + Price float64 `json:"price"` + Flags uint32 `json:"flags"` + LastModifiedLedger uint32 `json:"last_modified_ledger"` + LedgerEntryChange uint32 `json:"ledger_entry_change"` + Deleted bool `json:"deleted"` + Sponsor null.String `json:"sponsor"` + ClosedAt time.Time `json:"closed_at"` + LedgerSequence uint32 `json:"ledger_sequence"` +} + +// TradeOutput is a representation of a trade that aligns with the BigQuery table history_trades +type TradeOutput struct { + Order int32 `json:"order"` + LedgerClosedAt time.Time `json:"ledger_closed_at"` + SellingAccountAddress string `json:"selling_account_address"` + SellingAssetCode string `json:"selling_asset_code"` + SellingAssetIssuer string `json:"selling_asset_issuer"` + SellingAssetType string `json:"selling_asset_type"` + SellingAssetID int64 `json:"selling_asset_id"` + SellingAmount float64 `json:"selling_amount"` + BuyingAccountAddress string `json:"buying_account_address"` + BuyingAssetCode string `json:"buying_asset_code"` + BuyingAssetIssuer string `json:"buying_asset_issuer"` + BuyingAssetType string `json:"buying_asset_type"` + BuyingAssetID int64 `json:"buying_asset_id"` + BuyingAmount float64 `json:"buying_amount"` + PriceN int64 `json:"price_n"` + PriceD int64 `json:"price_d"` + SellingOfferID null.Int `json:"selling_offer_id"` + BuyingOfferID null.Int `json:"buying_offer_id"` + SellingLiquidityPoolID null.String `json:"selling_liquidity_pool_id"` + LiquidityPoolFee null.Int `json:"liquidity_pool_fee"` + HistoryOperationID int64 `json:"history_operation_id"` + TradeType int32 `json:"trade_type"` + RoundingSlippage null.Int `json:"rounding_slippage"` + SellerIsExact null.Bool `json:"seller_is_exact"` +} + +// DimAccount is a representation of an account that aligns with the BigQuery table dim_accounts +type DimAccount struct { + ID uint64 `json:"account_id"` + Address string `json:"address"` +} + +// DimOffer is a representation of an account that aligns with the BigQuery table dim_offers +type DimOffer struct { + HorizonID int64 `json:"horizon_offer_id"` + DimOfferID uint64 `json:"dim_offer_id"` + MarketID uint64 `json:"market_id"` + MakerID uint64 `json:"maker_id"` + Action string `json:"action"` + BaseAmount float64 `json:"base_amount"` + CounterAmount float64 `json:"counter_amount"` + Price float64 `json:"price"` +} + +// FactOfferEvent is a representation of an offer event that aligns with the BigQuery table fact_offer_events +type FactOfferEvent struct { + LedgerSeq uint32 `json:"ledger_id"` + OfferInstanceID uint64 `json:"offer_instance_id"` +} + +// DimMarket is a representation of an account that aligns with the BigQuery table dim_markets +type DimMarket struct { + ID uint64 `json:"market_id"` + BaseCode string `json:"base_code"` + BaseIssuer string `json:"base_issuer"` + CounterCode string `json:"counter_code"` + CounterIssuer string `json:"counter_issuer"` +} + +// NormalizedOfferOutput ties together the information for dim_markets, dim_offers, dim_accounts, and fact_offer-events +type NormalizedOfferOutput struct { + Market DimMarket + Offer DimOffer + Account DimAccount + Event FactOfferEvent +} + +type SponsorshipOutput struct { + Operation xdr.Operation + OperationIndex uint32 +} + +// EffectOutput is a representation of an operation that aligns with the BigQuery table history_effects +type EffectOutput struct { + Address string `json:"address"` + AddressMuxed null.String `json:"address_muxed,omitempty"` + OperationID int64 `json:"operation_id"` + Details map[string]interface{} `json:"details"` + Type int32 `json:"type"` + TypeString string `json:"type_string"` + LedgerClosed time.Time `json:"closed_at"` + LedgerSequence uint32 `json:"ledger_sequence"` + EffectIndex uint32 `json:"index"` + EffectId string `json:"id"` +} + +// EffectType is the numeric type for an effect +type EffectType int + +const ( + EffectAccountCreated EffectType = 0 + EffectAccountRemoved EffectType = 1 + EffectAccountCredited EffectType = 2 + EffectAccountDebited EffectType = 3 + EffectAccountThresholdsUpdated EffectType = 4 + EffectAccountHomeDomainUpdated EffectType = 5 + EffectAccountFlagsUpdated EffectType = 6 + EffectAccountInflationDestinationUpdated EffectType = 7 + EffectSignerCreated EffectType = 10 + EffectSignerRemoved EffectType = 11 + EffectSignerUpdated EffectType = 12 + EffectTrustlineCreated EffectType = 20 + EffectTrustlineRemoved EffectType = 21 + EffectTrustlineUpdated EffectType = 22 + EffectTrustlineFlagsUpdated EffectType = 26 + EffectOfferCreated EffectType = 30 + EffectOfferRemoved EffectType = 31 + EffectOfferUpdated EffectType = 32 + EffectTrade EffectType = 33 + EffectDataCreated EffectType = 40 + EffectDataRemoved EffectType = 41 + EffectDataUpdated EffectType = 42 + EffectSequenceBumped EffectType = 43 + EffectClaimableBalanceCreated EffectType = 50 + EffectClaimableBalanceClaimantCreated EffectType = 51 + EffectClaimableBalanceClaimed EffectType = 52 + EffectAccountSponsorshipCreated EffectType = 60 + EffectAccountSponsorshipUpdated EffectType = 61 + EffectAccountSponsorshipRemoved EffectType = 62 + EffectTrustlineSponsorshipCreated EffectType = 63 + EffectTrustlineSponsorshipUpdated EffectType = 64 + EffectTrustlineSponsorshipRemoved EffectType = 65 + EffectDataSponsorshipCreated EffectType = 66 + EffectDataSponsorshipUpdated EffectType = 67 + EffectDataSponsorshipRemoved EffectType = 68 + EffectClaimableBalanceSponsorshipCreated EffectType = 69 + EffectClaimableBalanceSponsorshipUpdated EffectType = 70 + EffectClaimableBalanceSponsorshipRemoved EffectType = 71 + EffectSignerSponsorshipCreated EffectType = 72 + EffectSignerSponsorshipUpdated EffectType = 73 + EffectSignerSponsorshipRemoved EffectType = 74 + EffectClaimableBalanceClawedBack EffectType = 80 + EffectLiquidityPoolDeposited EffectType = 90 + EffectLiquidityPoolWithdrew EffectType = 91 + EffectLiquidityPoolTrade EffectType = 92 + EffectLiquidityPoolCreated EffectType = 93 + EffectLiquidityPoolRemoved EffectType = 94 + EffectLiquidityPoolRevoked EffectType = 95 + EffectContractCredited EffectType = 96 + EffectContractDebited EffectType = 97 + EffectExtendFootprintTtl EffectType = 98 + EffectRestoreFootprint EffectType = 99 +) + +// EffectTypeNames stores a map of effect type ID and names +var EffectTypeNames = map[EffectType]string{ + EffectAccountCreated: "account_created", + EffectAccountRemoved: "account_removed", + EffectAccountCredited: "account_credited", + EffectAccountDebited: "account_debited", + EffectAccountThresholdsUpdated: "account_thresholds_updated", + EffectAccountHomeDomainUpdated: "account_home_domain_updated", + EffectAccountFlagsUpdated: "account_flags_updated", + EffectAccountInflationDestinationUpdated: "account_inflation_destination_updated", + EffectSignerCreated: "signer_created", + EffectSignerRemoved: "signer_removed", + EffectSignerUpdated: "signer_updated", + EffectTrustlineCreated: "trustline_created", + EffectTrustlineRemoved: "trustline_removed", + EffectTrustlineUpdated: "trustline_updated", + EffectTrustlineFlagsUpdated: "trustline_flags_updated", + EffectOfferCreated: "offer_created", + EffectOfferRemoved: "offer_removed", + EffectOfferUpdated: "offer_updated", + EffectTrade: "trade", + EffectDataCreated: "data_created", + EffectDataRemoved: "data_removed", + EffectDataUpdated: "data_updated", + EffectSequenceBumped: "sequence_bumped", + EffectClaimableBalanceCreated: "claimable_balance_created", + EffectClaimableBalanceClaimed: "claimable_balance_claimed", + EffectClaimableBalanceClaimantCreated: "claimable_balance_claimant_created", + EffectAccountSponsorshipCreated: "account_sponsorship_created", + EffectAccountSponsorshipUpdated: "account_sponsorship_updated", + EffectAccountSponsorshipRemoved: "account_sponsorship_removed", + EffectTrustlineSponsorshipCreated: "trustline_sponsorship_created", + EffectTrustlineSponsorshipUpdated: "trustline_sponsorship_updated", + EffectTrustlineSponsorshipRemoved: "trustline_sponsorship_removed", + EffectDataSponsorshipCreated: "data_sponsorship_created", + EffectDataSponsorshipUpdated: "data_sponsorship_updated", + EffectDataSponsorshipRemoved: "data_sponsorship_removed", + EffectClaimableBalanceSponsorshipCreated: "claimable_balance_sponsorship_created", + EffectClaimableBalanceSponsorshipUpdated: "claimable_balance_sponsorship_updated", + EffectClaimableBalanceSponsorshipRemoved: "claimable_balance_sponsorship_removed", + EffectSignerSponsorshipCreated: "signer_sponsorship_created", + EffectSignerSponsorshipUpdated: "signer_sponsorship_updated", + EffectSignerSponsorshipRemoved: "signer_sponsorship_removed", + EffectClaimableBalanceClawedBack: "claimable_balance_clawed_back", + EffectLiquidityPoolDeposited: "liquidity_pool_deposited", + EffectLiquidityPoolWithdrew: "liquidity_pool_withdrew", + EffectLiquidityPoolTrade: "liquidity_pool_trade", + EffectLiquidityPoolCreated: "liquidity_pool_created", + EffectLiquidityPoolRemoved: "liquidity_pool_removed", + EffectLiquidityPoolRevoked: "liquidity_pool_revoked", + EffectContractCredited: "contract_credited", + EffectContractDebited: "contract_debited", + EffectExtendFootprintTtl: "extend_footprint_ttl", + EffectRestoreFootprint: "restore_footprint", +} + +// TradeEffectDetails is a struct of data from `effects.DetailsString` +// when the effect type is trade +type TradeEffectDetails struct { + Seller string `json:"seller"` + SellerMuxed string `json:"seller_muxed,omitempty"` + SellerMuxedID uint64 `json:"seller_muxed_id,omitempty"` + OfferID int64 `json:"offer_id"` + SoldAmount string `json:"sold_amount"` + SoldAssetType string `json:"sold_asset_type"` + SoldAssetCode string `json:"sold_asset_code,omitempty"` + SoldAssetIssuer string `json:"sold_asset_issuer,omitempty"` + BoughtAmount string `json:"bought_amount"` + BoughtAssetType string `json:"bought_asset_type"` + BoughtAssetCode string `json:"bought_asset_code,omitempty"` + BoughtAssetIssuer string `json:"bought_asset_issuer,omitempty"` +} + +// TestTransaction transaction meta +type TestTransaction struct { + Index uint32 + EnvelopeXDR string + ResultXDR string + FeeChangesXDR string + MetaXDR string + Hash string +} + +// ContractDataOutput is a representation of contract data that aligns with the Bigquery table soroban_contract_data +type ContractDataOutput struct { + ContractId string `json:"contract_id"` + ContractKeyType string `json:"contract_key_type"` + ContractDurability string `json:"contract_durability"` + ContractDataAssetCode string `json:"asset_code"` + ContractDataAssetIssuer string `json:"asset_issuer"` + ContractDataAssetType string `json:"asset_type"` + ContractDataBalanceHolder string `json:"balance_holder"` + ContractDataBalance string `json:"balance"` // balance is a string because it is go type big.Int + LastModifiedLedger uint32 `json:"last_modified_ledger"` + LedgerEntryChange uint32 `json:"ledger_entry_change"` + Deleted bool `json:"deleted"` + ClosedAt time.Time `json:"closed_at"` + LedgerSequence uint32 `json:"ledger_sequence"` + LedgerKeyHash string `json:"ledger_key_hash"` + Key map[string]string `json:"key"` + KeyDecoded map[string]string `json:"key_decoded"` + Val map[string]string `json:"val"` + ValDecoded map[string]string `json:"val_decoded"` + ContractDataXDR string `json:"contract_data_xdr"` +} + +// ContractCodeOutput is a representation of contract code that aligns with the Bigquery table soroban_contract_code +type ContractCodeOutput struct { + ContractCodeHash string `json:"contract_code_hash"` + ContractCodeExtV int32 `json:"contract_code_ext_v"` + LastModifiedLedger uint32 `json:"last_modified_ledger"` + LedgerEntryChange uint32 `json:"ledger_entry_change"` + Deleted bool `json:"deleted"` + ClosedAt time.Time `json:"closed_at"` + LedgerSequence uint32 `json:"ledger_sequence"` + LedgerKeyHash string `json:"ledger_key_hash"` + //ContractCodeCode string `json:"contract_code"` + NInstructions uint32 `json:"n_instructions"` + NFunctions uint32 `json:"n_functions"` + NGlobals uint32 `json:"n_globals"` + NTableEntries uint32 `json:"n_table_entries"` + NTypes uint32 `json:"n_types"` + NDataSegments uint32 `json:"n_data_segments"` + NElemSegments uint32 `json:"n_elem_segments"` + NImports uint32 `json:"n_imports"` + NExports uint32 `json:"n_exports"` + NDataSegmentBytes uint32 `json:"n_data_segment_bytes"` +} + +// ConfigSettingOutput is a representation of soroban config settings that aligns with the Bigquery table config_settings +type ConfigSettingOutput struct { + ConfigSettingId int32 `json:"config_setting_id"` + ContractMaxSizeBytes uint32 `json:"contract_max_size_bytes"` + LedgerMaxInstructions int64 `json:"ledger_max_instructions"` + TxMaxInstructions int64 `json:"tx_max_instructions"` + FeeRatePerInstructionsIncrement int64 `json:"fee_rate_per_instructions_increment"` + TxMemoryLimit uint32 `json:"tx_memory_limit"` + LedgerMaxReadLedgerEntries uint32 `json:"ledger_max_read_ledger_entries"` + LedgerMaxReadBytes uint32 `json:"ledger_max_read_bytes"` + LedgerMaxWriteLedgerEntries uint32 `json:"ledger_max_write_ledger_entries"` + LedgerMaxWriteBytes uint32 `json:"ledger_max_write_bytes"` + TxMaxReadLedgerEntries uint32 `json:"tx_max_read_ledger_entries"` + TxMaxReadBytes uint32 `json:"tx_max_read_bytes"` + TxMaxWriteLedgerEntries uint32 `json:"tx_max_write_ledger_entries"` + TxMaxWriteBytes uint32 `json:"tx_max_write_bytes"` + FeeReadLedgerEntry int64 `json:"fee_read_ledger_entry"` + FeeWriteLedgerEntry int64 `json:"fee_write_ledger_entry"` + FeeRead1Kb int64 `json:"fee_read_1kb"` + BucketListTargetSizeBytes int64 `json:"bucket_list_target_size_bytes"` + WriteFee1KbBucketListLow int64 `json:"write_fee_1kb_bucket_list_low"` + WriteFee1KbBucketListHigh int64 `json:"write_fee_1kb_bucket_list_high"` + BucketListWriteFeeGrowthFactor uint32 `json:"bucket_list_write_fee_growth_factor"` + FeeHistorical1Kb int64 `json:"fee_historical_1kb"` + TxMaxContractEventsSizeBytes uint32 `json:"tx_max_contract_events_size_bytes"` + FeeContractEvents1Kb int64 `json:"fee_contract_events_1kb"` + LedgerMaxTxsSizeBytes uint32 `json:"ledger_max_txs_size_bytes"` + TxMaxSizeBytes uint32 `json:"tx_max_size_bytes"` + FeeTxSize1Kb int64 `json:"fee_tx_size_1kb"` + ContractCostParamsCpuInsns []map[string]string `json:"contract_cost_params_cpu_insns"` + ContractCostParamsMemBytes []map[string]string `json:"contract_cost_params_mem_bytes"` + ContractDataKeySizeBytes uint32 `json:"contract_data_key_size_bytes"` + ContractDataEntrySizeBytes uint32 `json:"contract_data_entry_size_bytes"` + MaxEntryTtl uint32 `json:"max_entry_ttl"` + MinTemporaryTtl uint32 `json:"min_temporary_ttl"` + MinPersistentTtl uint32 `json:"min_persistent_ttl"` + AutoBumpLedgers uint32 `json:"auto_bump_ledgers"` + PersistentRentRateDenominator int64 `json:"persistent_rent_rate_denominator"` + TempRentRateDenominator int64 `json:"temp_rent_rate_denominator"` + MaxEntriesToArchive uint32 `json:"max_entries_to_archive"` + BucketListSizeWindowSampleSize uint32 `json:"bucket_list_size_window_sample_size"` + EvictionScanSize uint64 `json:"eviction_scan_size"` + StartingEvictionScanLevel uint32 `json:"starting_eviction_scan_level"` + LedgerMaxTxCount uint32 `json:"ledger_max_tx_count"` + BucketListSizeWindow []uint64 `json:"bucket_list_size_window"` + LastModifiedLedger uint32 `json:"last_modified_ledger"` + LedgerEntryChange uint32 `json:"ledger_entry_change"` + Deleted bool `json:"deleted"` + ClosedAt time.Time `json:"closed_at"` + LedgerSequence uint32 `json:"ledger_sequence"` +} + +// TtlOutput is a representation of soroban ttl that aligns with the Bigquery table ttls +type TtlOutput struct { + KeyHash string `json:"key_hash"` // key_hash is contract_code_hash or contract_id + LiveUntilLedgerSeq uint32 `json:"live_until_ledger_seq"` + LastModifiedLedger uint32 `json:"last_modified_ledger"` + LedgerEntryChange uint32 `json:"ledger_entry_change"` + Deleted bool `json:"deleted"` + ClosedAt time.Time `json:"closed_at"` + LedgerSequence uint32 `json:"ledger_sequence"` +} + +// ContractEventOutput is a representation of soroban contract events and diagnostic events +type ContractEventOutput struct { + TransactionHash string `json:"transaction_hash"` + TransactionID int64 `json:"transaction_id"` + Successful bool `json:"successful"` + LedgerSequence uint32 `json:"ledger_sequence"` + ClosedAt time.Time `json:"closed_at"` + InSuccessfulContractCall bool `json:"in_successful_contract_call"` + ContractId string `json:"contract_id"` + Type int32 `json:"type"` + TypeString string `json:"type_string"` + Topics map[string][]map[string]string `json:"topics"` + TopicsDecoded map[string][]map[string]string `json:"topics_decoded"` + Data map[string]string `json:"data"` + DataDecoded map[string]string `json:"data_decoded"` + ContractEventXDR string `json:"contract_event_xdr"` +} + +type HistoryArchiveLedgerAndLCM struct { + Ledger historyarchive.Ledger + LCM xdr.LedgerCloseMeta +} diff --git a/ingest/processors/test_variables_test.go b/ingest/processors/test_variables_test.go new file mode 100644 index 0000000000..18551ad8f4 --- /dev/null +++ b/ingest/processors/test_variables_test.go @@ -0,0 +1,232 @@ +package processors + +import ( + "time" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +var genericSourceAccount, _ = xdr.NewMuxedAccount(xdr.CryptoKeyTypeKeyTypeEd25519, xdr.Uint256([32]byte{})) +var genericAccountID, _ = xdr.NewAccountId(xdr.PublicKeyTypePublicKeyTypeEd25519, xdr.Uint256([32]byte{})) +var genericAccountAddress, _ = genericAccountID.GetAddress() +var genericManageBuyOfferOperation = xdr.Operation{ + SourceAccount: &genericSourceAccount, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeManageBuyOffer, + ManageBuyOfferOp: &xdr.ManageBuyOfferOp{}, + }, +} +var genericBumpOperation = xdr.Operation{ + SourceAccount: &genericSourceAccount, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeBumpSequence, + BumpSequenceOp: &xdr.BumpSequenceOp{}, + }, +} +var genericBumpOperationEnvelope = xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: genericSourceAccount, + Memo: xdr.Memo{}, + Operations: []xdr.Operation{ + genericBumpOperation, + }, + Ext: xdr.TransactionExt{ + V: 0, + SorobanData: &xdr.SorobanTransactionData{ + Ext: xdr.ExtensionPoint{ + V: 0, + }, + Resources: xdr.SorobanResources{ + Footprint: xdr.LedgerFootprint{ + ReadOnly: []xdr.LedgerKey{}, + ReadWrite: []xdr.LedgerKey{}, + }, + }, + ResourceFee: 100, + }, + }, + }, +} +var genericBumpOperationForTransaction = xdr.Operation{ + SourceAccount: &genericSourceAccount, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeBumpSequence, + BumpSequenceOp: &xdr.BumpSequenceOp{}, + }, +} +var genericBumpOperationEnvelopeForTransaction = xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: genericSourceAccount, + Memo: xdr.Memo{}, + Operations: []xdr.Operation{ + genericBumpOperationForTransaction, + }, + }, +} +var genericManageBuyOfferEnvelope = xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: genericSourceAccount, + Memo: xdr.Memo{}, + Operations: []xdr.Operation{ + genericManageBuyOfferOperation, + }, + }, +} + +var genericTxMeta = CreateSampleTxMeta(29, lpAssetA, lpAssetB) + +var genericLedgerTransaction = ingest.LedgerTransaction{ + Index: 1, + Envelope: xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &genericBumpOperationEnvelope, + }, + Result: CreateSampleResultMeta(true, 10).Result, + UnsafeMeta: xdr.TransactionMeta{ + V: 1, + V1: genericTxMeta, + }, +} +var genericLedgerHeaderHistoryEntry = xdr.LedgerHeaderHistoryEntry{} +var genericCloseTime = time.Unix(0, 0) + +// a selection of hardcoded accounts with their IDs and addresses +var testAccount1Address = "GCEODJVUUVYVFD5KT4TOEDTMXQ76OPFOQC2EMYYMLPXQCUVPOB6XRWPQ" +var testAccount1ID, _ = xdr.AddressToAccountId(testAccount1Address) +var testAccount1 = testAccount1ID.ToMuxedAccount() + +var testAccount2Address = "GAOEOQMXDDXPVJC3HDFX6LZFKANJ4OOLQOD2MNXJ7PGAY5FEO4BRRAQU" +var testAccount2ID, _ = xdr.AddressToAccountId(testAccount2Address) +var testAccount2 = testAccount2ID.ToMuxedAccount() + +var testAccount3Address = "GBT4YAEGJQ5YSFUMNKX6BPBUOCPNAIOFAVZOF6MIME2CECBMEIUXFZZN" +var testAccount3ID, _ = xdr.AddressToAccountId(testAccount3Address) +var testAccount3 = testAccount3ID.ToMuxedAccount() + +var testAccount4Address = "GBVVRXLMNCJQW3IDDXC3X6XCH35B5Q7QXNMMFPENSOGUPQO7WO7HGZPA" +var testAccount4ID, _ = xdr.AddressToAccountId(testAccount4Address) +var testAccount4 = testAccount4ID.ToMuxedAccount() + +var dummyEd25519 [32]byte +var testAccount5 = xdr.MuxedAccount{ + Type: xdr.CryptoKeyTypeKeyTypeMuxedEd25519, + Med25519: &xdr.MuxedAccountMed25519{ + Id: xdr.Uint64(1), + Ed25519: xdr.Uint256(dummyEd25519), + }, +} +var testAccount5Address = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF" + +// a selection of hardcoded assets and their AssetOutput representations + +var usdtAsset = xdr.Asset{ + Type: xdr.AssetTypeAssetTypeCreditAlphanum4, + AlphaNum4: &xdr.AlphaNum4{ + AssetCode: xdr.AssetCode4([4]byte{0x55, 0x53, 0x44, 0x54}), + Issuer: testAccount4ID, + }, +} + +var usdtTrustLineAsset = xdr.TrustLineAsset{ + Type: xdr.AssetTypeAssetTypeCreditAlphanum4, + AlphaNum4: &xdr.AlphaNum4{ + AssetCode: xdr.AssetCode4([4]byte{0x55, 0x53, 0x54, 0x54}), + Issuer: testAccount3ID, + }, +} + +var usdtChangeTrustAsset = xdr.ChangeTrustAsset{ + Type: xdr.AssetTypeAssetTypeCreditAlphanum4, + AlphaNum4: &xdr.AlphaNum4{ + AssetCode: xdr.AssetCode4([4]byte{0x55, 0x53, 0x53, 0x44}), + Issuer: testAccount4ID, + }, +} + +var lpAssetA = xdr.Asset{ + Type: xdr.AssetTypeAssetTypeNative, +} + +var lpAssetB = xdr.Asset{ + Type: xdr.AssetTypeAssetTypeCreditAlphanum4, + AlphaNum4: &xdr.AlphaNum4{ + AssetCode: xdr.AssetCode4([4]byte{0x55, 0x53, 0x53, 0x44}), + Issuer: testAccount4ID, + }, +} + +var usdtLiquidityPoolShare = xdr.ChangeTrustAsset{ + Type: xdr.AssetTypeAssetTypePoolShare, + LiquidityPool: &xdr.LiquidityPoolParameters{ + Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, + ConstantProduct: &xdr.LiquidityPoolConstantProductParameters{ + AssetA: lpAssetA, + AssetB: lpAssetB, + Fee: 30, + }, + }, +} + +var usdtAssetPath = Path{ + AssetType: "credit_alphanum4", + AssetCode: "USDT", + AssetIssuer: testAccount4Address, +} + +var ethAsset = xdr.Asset{ + Type: xdr.AssetTypeAssetTypeCreditAlphanum4, + AlphaNum4: &xdr.AlphaNum4{ + AssetCode: xdr.AssetCode4([4]byte{0x45, 0x54, 0x48}), + Issuer: testAccount3ID, + }, +} + +var ethTrustLineAsset = xdr.TrustLineAsset{ + Type: xdr.AssetTypeAssetTypeCreditAlphanum4, + AlphaNum4: &xdr.AlphaNum4{ + AssetCode: xdr.AssetCode4([4]byte{0x45, 0x54, 0x48}), + Issuer: testAccount3ID, + }, +} + +var liquidityPoolAsset = xdr.TrustLineAsset{ + Type: xdr.AssetTypeAssetTypePoolShare, + LiquidityPoolId: &xdr.PoolId{1, 3, 4, 5, 7, 9}, +} + +var nativeAsset = xdr.MustNewNativeAsset() + +var genericClaimableBalance = xdr.ClaimableBalanceId{ + Type: xdr.ClaimableBalanceIdTypeClaimableBalanceIdTypeV0, + V0: &xdr.Hash{1, 2, 3, 4, 5, 6, 7, 8, 9}, +} + +var testClaimant = xdr.Claimant{ + Type: xdr.ClaimantTypeClaimantTypeV0, + V0: &xdr.ClaimantV0{ + Destination: testAccount1ID, + Predicate: xdr.ClaimPredicate{ + Type: xdr.ClaimPredicateTypeClaimPredicateUnconditional, + }, + }, +} + +var testClaimantDetails = Claimant{ + Destination: testAccount1Address, + Predicate: xdr.ClaimPredicate{}, +} + +var genericLedgerCloseMeta = xdr.LedgerCloseMeta{ + V: 0, + V0: &xdr.LedgerCloseMetaV0{ + LedgerHeader: xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + LedgerSeq: 2, + ScpValue: xdr.StellarValue{ + CloseTime: 10, + }, + }, + }, + }, +} diff --git a/ingest/processors/trade.go b/ingest/processors/trade.go new file mode 100644 index 0000000000..8efcf5c3bd --- /dev/null +++ b/ingest/processors/trade.go @@ -0,0 +1,399 @@ +package processors + +import ( + "fmt" + "math" + "time" + + "github.com/guregu/null" + "github.com/pkg/errors" + + "github.com/stellar/go/exp/orderbook" + "github.com/stellar/go/ingest" + "github.com/stellar/go/support/log" + "github.com/stellar/go/toid" + "github.com/stellar/go/xdr" +) + +// TransformTrade converts a relevant operation from the history archive ingestion system into a form suitable for BigQuery +func TransformTrade(operationIndex int32, operationID int64, transaction ingest.LedgerTransaction, ledgerCloseTime time.Time) ([]TradeOutput, error) { + operationResults, ok := transaction.Result.OperationResults() + if !ok { + return []TradeOutput{}, fmt.Errorf("could not get any results from this transaction") + } + + if !transaction.Result.Successful() { + return []TradeOutput{}, fmt.Errorf("transaction failed; no trades") + } + + operation := transaction.Envelope.Operations()[operationIndex] + // operation id is +1 incremented to stay in sync with ingest package + outputOperationID := operationID + 1 + claimedOffers, BuyingOffer, sellerIsExact, err := extractClaimedOffers(operationResults, operationIndex, operation.Body.Type) + if err != nil { + return []TradeOutput{}, err + } + + transformedTrades := []TradeOutput{} + + for claimOrder, claimOffer := range claimedOffers { + outputOrder := int32(claimOrder) + outputLedgerClosedAt := ledgerCloseTime + + var outputSellingAssetType, outputSellingAssetCode, outputSellingAssetIssuer string + err = claimOffer.AssetSold().Extract(&outputSellingAssetType, &outputSellingAssetCode, &outputSellingAssetIssuer) + if err != nil { + return []TradeOutput{}, err + } + outputSellingAssetID := FarmHashAsset(outputSellingAssetCode, outputSellingAssetIssuer, outputSellingAssetType) + + outputSellingAmount := claimOffer.AmountSold() + if outputSellingAmount < 0 { + return []TradeOutput{}, fmt.Errorf("amount sold is negative (%d) for operation at index %d", outputSellingAmount, operationIndex) + } + + var outputBuyingAssetType, outputBuyingAssetCode, outputBuyingAssetIssuer string + err = claimOffer.AssetBought().Extract(&outputBuyingAssetType, &outputBuyingAssetCode, &outputBuyingAssetIssuer) + if err != nil { + return []TradeOutput{}, err + } + outputBuyingAssetID := FarmHashAsset(outputBuyingAssetCode, outputBuyingAssetIssuer, outputBuyingAssetType) + + outputBuyingAmount := int64(claimOffer.AmountBought()) + if outputBuyingAmount < 0 { + return []TradeOutput{}, fmt.Errorf("amount bought is negative (%d) for operation at index %d", outputBuyingAmount, operationIndex) + } + + if outputSellingAmount == 0 && outputBuyingAmount == 0 { + log.Debugf("Both Selling and Buying amount are 0 for operation at index %d", operationIndex) + continue + } + + // Final price should be buy / sell + outputPriceN, outputPriceD, err := findTradeSellPrice(transaction, operationIndex, claimOffer) + if err != nil { + return []TradeOutput{}, err + } + + var outputSellingAccountAddress string + var liquidityPoolID null.String + var outputPoolFee, roundingSlippageBips null.Int + var outputSellingOfferID, outputBuyingOfferID null.Int + var tradeType int32 + if claimOffer.Type == xdr.ClaimAtomTypeClaimAtomTypeLiquidityPool { + id := claimOffer.MustLiquidityPool().LiquidityPoolId + liquidityPoolID = null.StringFrom(PoolIDToString(id)) + tradeType = int32(2) + var fee uint32 + if fee, err = findPoolFee(transaction, operationIndex, id); err != nil { + return []TradeOutput{}, fmt.Errorf("cannot parse fee for liquidity pool %v", liquidityPoolID) + } + outputPoolFee = null.IntFrom(int64(fee)) + + change, err := liquidityPoolChange(transaction, operationIndex, claimOffer) + if err != nil { + return nil, err + } + if change != nil { + roundingSlippageBips, err = roundingSlippage(transaction, operationIndex, claimOffer, change) + if err != nil { + return nil, err + } + } + } else { + outputSellingOfferID = null.IntFrom(int64(claimOffer.OfferId())) + outputSellingAccountAddress = claimOffer.SellerId().Address() + tradeType = int32(1) + } + + if BuyingOffer != nil { + outputBuyingOfferID = null.IntFrom(int64(BuyingOffer.OfferId)) + } else { + outputBuyingOfferID = null.IntFrom(toid.EncodeOfferId(uint64(operationID)+1, toid.TOIDType)) + } + + var outputBuyingAccountAddress string + if buyer := operation.SourceAccount; buyer != nil { + accid := buyer.ToAccountId() + outputBuyingAccountAddress = accid.Address() + } else { + sa := transaction.Envelope.SourceAccount().ToAccountId() + outputBuyingAccountAddress = sa.Address() + } + + trade := TradeOutput{ + Order: outputOrder, + LedgerClosedAt: outputLedgerClosedAt, + SellingAccountAddress: outputSellingAccountAddress, + SellingAssetType: outputSellingAssetType, + SellingAssetCode: outputSellingAssetCode, + SellingAssetIssuer: outputSellingAssetIssuer, + SellingAssetID: outputSellingAssetID, + SellingAmount: ConvertStroopValueToReal(outputSellingAmount), + BuyingAccountAddress: outputBuyingAccountAddress, + BuyingAssetType: outputBuyingAssetType, + BuyingAssetCode: outputBuyingAssetCode, + BuyingAssetIssuer: outputBuyingAssetIssuer, + BuyingAssetID: outputBuyingAssetID, + BuyingAmount: ConvertStroopValueToReal(xdr.Int64(outputBuyingAmount)), + PriceN: outputPriceN, + PriceD: outputPriceD, + SellingOfferID: outputSellingOfferID, + BuyingOfferID: outputBuyingOfferID, + SellingLiquidityPoolID: liquidityPoolID, + LiquidityPoolFee: outputPoolFee, + HistoryOperationID: outputOperationID, + TradeType: tradeType, + RoundingSlippage: roundingSlippageBips, + SellerIsExact: sellerIsExact, + } + + transformedTrades = append(transformedTrades, trade) + } + return transformedTrades, nil +} + +func extractClaimedOffers(operationResults []xdr.OperationResult, operationIndex int32, operationType xdr.OperationType) (claimedOffers []xdr.ClaimAtom, BuyingOffer *xdr.OfferEntry, sellerIsExact null.Bool, err error) { + if operationIndex >= int32(len(operationResults)) { + err = fmt.Errorf("operation index of %d is out of bounds in result slice (len = %d)", operationIndex, len(operationResults)) + return + } + + if operationResults[operationIndex].Tr == nil { + err = fmt.Errorf("could not get result Tr for operation at index %d", operationIndex) + return + } + + operationTr, ok := operationResults[operationIndex].GetTr() + if !ok { + err = fmt.Errorf("could not get result Tr for operation at index %d", operationIndex) + return + } + switch operationType { + case xdr.OperationTypeManageBuyOffer: + var buyOfferResult xdr.ManageBuyOfferResult + var success xdr.ManageOfferSuccessResult + + if buyOfferResult, ok = operationTr.GetManageBuyOfferResult(); !ok { + err = fmt.Errorf("could not get ManageBuyOfferResult for operation at index %d", operationIndex) + return + } + if success, ok = buyOfferResult.GetSuccess(); ok { + claimedOffers = success.OffersClaimed + BuyingOffer = success.Offer.Offer + return + } + + err = fmt.Errorf("could not get ManageOfferSuccess for operation at index %d", operationIndex) + + case xdr.OperationTypeManageSellOffer: + var sellOfferResult xdr.ManageSellOfferResult + var success xdr.ManageOfferSuccessResult + if sellOfferResult, ok = operationTr.GetManageSellOfferResult(); !ok { + err = fmt.Errorf("could not get ManageSellOfferResult for operation at index %d", operationIndex) + return + } + + if success, ok = sellOfferResult.GetSuccess(); ok { + claimedOffers = success.OffersClaimed + BuyingOffer = success.Offer.Offer + return + } + + err = fmt.Errorf("could not get ManageOfferSuccess for operation at index %d", operationIndex) + + case xdr.OperationTypeCreatePassiveSellOffer: + // KNOWN ISSUE: stellar-core creates results for CreatePassiveOffer operations + // with the wrong result arm set. + if operationTr.Type == xdr.OperationTypeManageSellOffer { + passiveSellResult := operationTr.MustManageSellOfferResult().MustSuccess() + claimedOffers = passiveSellResult.OffersClaimed + BuyingOffer = passiveSellResult.Offer.Offer + return + } else { + passiveSellResult := operationTr.MustCreatePassiveSellOfferResult().MustSuccess() + claimedOffers = passiveSellResult.OffersClaimed + BuyingOffer = passiveSellResult.Offer.Offer + return + } + + case xdr.OperationTypePathPaymentStrictSend: + var pathSendResult xdr.PathPaymentStrictSendResult + var success xdr.PathPaymentStrictSendResultSuccess + + sellerIsExact = null.BoolFrom(false) + if pathSendResult, ok = operationTr.GetPathPaymentStrictSendResult(); !ok { + err = fmt.Errorf("could not get PathPaymentStrictSendResult for operation at index %d", operationIndex) + return + } + + success, ok = pathSendResult.GetSuccess() + if ok { + claimedOffers = success.Offers + return + } + + err = fmt.Errorf("could not get PathPaymentStrictSendSuccess for operation at index %d", operationIndex) + + case xdr.OperationTypePathPaymentStrictReceive: + var pathReceiveResult xdr.PathPaymentStrictReceiveResult + sellerIsExact = null.BoolFrom(true) + if pathReceiveResult, ok = operationTr.GetPathPaymentStrictReceiveResult(); !ok { + err = fmt.Errorf("could not get PathPaymentStrictReceiveResult for operation at index %d", operationIndex) + return + } + + if success, ok := pathReceiveResult.GetSuccess(); ok { + claimedOffers = success.Offers + return + } + + err = fmt.Errorf("could not get GetPathPaymentStrictReceiveSuccess for operation at index %d", operationIndex) + + default: + err = fmt.Errorf("operation of type %s at index %d does not result in trades", operationType, operationIndex) + return + } + + return +} + +func findTradeSellPrice(t ingest.LedgerTransaction, operationIndex int32, trade xdr.ClaimAtom) (n, d int64, err error) { + if trade.Type == xdr.ClaimAtomTypeClaimAtomTypeLiquidityPool { + return int64(trade.AmountBought()), int64(trade.AmountSold()), nil + } + + key := xdr.LedgerKey{} + if err = key.SetOffer(trade.SellerId(), uint64(trade.OfferId())); err != nil { + return 0, 0, errors.Wrap(err, "Could not create offer ledger key") + } + var change ingest.Change + change, err = findLatestOperationChange(t, operationIndex, key) + if err != nil { + return 0, 0, errors.Wrap(err, "could not find change for trade offer") + } + + return int64(change.Pre.Data.MustOffer().Price.N), int64(change.Pre.Data.MustOffer().Price.D), nil +} + +func findLatestOperationChange(t ingest.LedgerTransaction, operationIndex int32, key xdr.LedgerKey) (ingest.Change, error) { + changes, err := t.GetOperationChanges(uint32(operationIndex)) + if err != nil { + return ingest.Change{}, errors.Wrap(err, "could not determine changes for operation") + } + + var change ingest.Change + // traverse through the slice in reverse order + for i := len(changes) - 1; i >= 0; i-- { + change = changes[i] + if change.Pre != nil { + var preKey xdr.LedgerKey + preKey, err = change.Pre.LedgerKey() + if err != nil { + return ingest.Change{}, errors.Wrap(err, "could not determine ledger key for change") + + } + if key.Equals(preKey) { + return change, nil + } + } + + } + return ingest.Change{}, errors.Errorf("could not find operation for key %v", key) +} + +func findPoolFee(t ingest.LedgerTransaction, operationIndex int32, poolID xdr.PoolId) (fee uint32, err error) { + key := xdr.LedgerKey{} + if err = key.SetLiquidityPool(poolID); err != nil { + return 0, errors.Wrap(err, "Could not create liquidity pool ledger key") + } + var change ingest.Change + change, err = findLatestOperationChange(t, operationIndex, key) + if err != nil { + return 0, errors.Wrap(err, "could not find change for liquidity pool") + } + + return uint32(change.Pre.Data.MustLiquidityPool().Body.MustConstantProduct().Params.Fee), nil +} + +func liquidityPoolChange(t ingest.LedgerTransaction, operationIndex int32, trade xdr.ClaimAtom) (*ingest.Change, error) { + if trade.Type != xdr.ClaimAtomTypeClaimAtomTypeLiquidityPool { + return nil, nil + } + + poolID := trade.LiquidityPool.LiquidityPoolId + + key := xdr.LedgerKey{} + if err := key.SetLiquidityPool(poolID); err != nil { + return nil, errors.Wrap(err, "Could not create liquidity pool ledger key") + } + + change, err := findLatestOperationChange(t, operationIndex, key) + if err != nil { + return nil, errors.Wrap(err, "Could not find change for liquidity pool") + } + + return &change, nil +} + +func liquidityPoolReserves(trade xdr.ClaimAtom, change *ingest.Change) (int64, int64) { + pre := change.Pre.Data.MustLiquidityPool().Body.ConstantProduct + a := int64(pre.ReserveA) + b := int64(pre.ReserveB) + if !trade.AssetSold().Equals(pre.Params.AssetA) { + a, b = b, a + } + + return a, b +} + +func roundingSlippage(t ingest.LedgerTransaction, operationIndex int32, trade xdr.ClaimAtom, change *ingest.Change) (null.Int, error) { + disbursedReserves, depositedReserves := liquidityPoolReserves(trade, change) + + pre := change.Pre.Data.MustLiquidityPool().Body.ConstantProduct + + op, found := t.GetOperation(uint32(operationIndex)) + if !found { + return null.Int{}, errors.New("Could not find operation") + } + + amountDeposited := trade.AmountBought() + amountDisbursed := trade.AmountSold() + + switch op.Body.Type { + case xdr.OperationTypePathPaymentStrictReceive: + // User specified the disbursed amount + _, roundingSlippageBips, ok := orderbook.CalculatePoolPayout( + xdr.Int64(depositedReserves), + xdr.Int64(disbursedReserves), + amountDisbursed, + pre.Params.Fee, + true, + ) + if !ok { + // This is a temporary workaround and will be addressed when + // https://github.com/stellar/go/issues/4203 is closed + roundingSlippageBips = xdr.Int64(math.MaxInt64) + } + return null.IntFrom(int64(roundingSlippageBips)), nil + case xdr.OperationTypePathPaymentStrictSend: + // User specified the deposited amount + _, roundingSlippageBips, ok := orderbook.CalculatePoolPayout( + xdr.Int64(depositedReserves), + xdr.Int64(disbursedReserves), + amountDeposited, + pre.Params.Fee, + true, + ) + if !ok { + // Temporary workaround for https://github.com/stellar/go/issues/4203 + // Given strict receives that would overflow here, minimum slippage + // so they get excluded. + roundingSlippageBips = xdr.Int64(math.MinInt64) + } + return null.IntFrom(int64(roundingSlippageBips)), nil + default: + return null.Int{}, fmt.Errorf("unexpected trade operation type: %v", op.Body.Type) + } + +} diff --git a/ingest/processors/trade_test.go b/ingest/processors/trade_test.go new file mode 100644 index 0000000000..c8c7d73eaf --- /dev/null +++ b/ingest/processors/trade_test.go @@ -0,0 +1,842 @@ +package processors + +import ( + "fmt" + "testing" + "time" + + "github.com/guregu/null" + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformTrade(t *testing.T) { + type tradeInput struct { + index int32 + transaction ingest.LedgerTransaction + closeTime time.Time + } + type transformTest struct { + input tradeInput + wantOutput []TradeOutput + wantErr error + } + + hardCodedInputTransaction := makeTradeTestInput() + hardCodedOutputArray := makeTradeTestOutput() + + genericInput := tradeInput{ + index: 0, + transaction: genericLedgerTransaction, + closeTime: genericCloseTime, + } + + wrongTypeInput := genericInput + wrongTypeInput.transaction = ingest.LedgerTransaction{ + Index: 1, + Envelope: xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: genericSourceAccount, + Memo: xdr.Memo{}, + Operations: []xdr.Operation{ + genericBumpOperation, + }, + }, + }, + }, + Result: CreateSampleResultMeta(true, 1).Result, + } + + resultOutOfRangeInput := genericInput + resultOutOfRangeEnvelope := genericManageBuyOfferEnvelope + resultOutOfRangeInput.transaction.Envelope.V1 = &resultOutOfRangeEnvelope + resultOutOfRangeInput.transaction.Result = wrapOperationsResultsSlice([]xdr.OperationResult{}, true) + + failedTxInput := genericInput + failedTxInput.transaction.Result = wrapOperationsResultsSlice([]xdr.OperationResult{}, false) + + noTrInput := genericInput + noTrEnvelope := genericManageBuyOfferEnvelope + noTrInput.transaction.Envelope.V1 = &noTrEnvelope + noTrInput.transaction.Result = wrapOperationsResultsSlice([]xdr.OperationResult{ + {Tr: nil}, + }, true) + + failedResultInput := genericInput + failedResultEnvelope := genericManageBuyOfferEnvelope + failedResultInput.transaction.Envelope.V1 = &failedResultEnvelope + failedResultInput.transaction.Result = wrapOperationsResultsSlice([]xdr.OperationResult{ + { + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeManageBuyOffer, + ManageBuyOfferResult: &xdr.ManageBuyOfferResult{ + Code: xdr.ManageBuyOfferResultCodeManageBuyOfferMalformed, + }, + }}, + }, true) + + negBaseAmountInput := genericInput + negBaseAmountEnvelope := genericManageBuyOfferEnvelope + negBaseAmountInput.transaction.Envelope.V1 = &negBaseAmountEnvelope + negBaseAmountInput.transaction.Result = wrapOperationsResultsSlice([]xdr.OperationResult{ + { + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeManageBuyOffer, + ManageBuyOfferResult: &xdr.ManageBuyOfferResult{ + Code: xdr.ManageBuyOfferResultCodeManageBuyOfferSuccess, + Success: &xdr.ManageOfferSuccessResult{ + OffersClaimed: []xdr.ClaimAtom{ + { + Type: xdr.ClaimAtomTypeClaimAtomTypeOrderBook, + OrderBook: &xdr.ClaimOfferAtom{ + SellerId: genericAccountID, + AmountSold: -1, + }, + }, + }, + }, + }, + }}, + }, true) + + negCounterAmountInput := genericInput + negCounterAmountEnvelope := genericManageBuyOfferEnvelope + negCounterAmountInput.transaction.Envelope.V1 = &negCounterAmountEnvelope + negCounterAmountInput.transaction.Result = wrapOperationsResultsSlice([]xdr.OperationResult{ + { + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeManageBuyOffer, + ManageBuyOfferResult: &xdr.ManageBuyOfferResult{ + Code: xdr.ManageBuyOfferResultCodeManageBuyOfferSuccess, + Success: &xdr.ManageOfferSuccessResult{ + OffersClaimed: []xdr.ClaimAtom{ + { + Type: xdr.ClaimAtomTypeClaimAtomTypeOrderBook, + OrderBook: &xdr.ClaimOfferAtom{ + SellerId: genericAccountID, + AmountBought: -2, + }, + }, + }, + }, + }, + }}, + }, true) + + tests := []transformTest{ + { + wrongTypeInput, + []TradeOutput{}, fmt.Errorf("operation of type OperationTypeBumpSequence at index 0 does not result in trades"), + }, + { + resultOutOfRangeInput, + []TradeOutput{}, fmt.Errorf("operation index of 0 is out of bounds in result slice (len = 0)"), + }, + { + failedTxInput, + []TradeOutput{}, fmt.Errorf("transaction failed; no trades"), + }, + { + noTrInput, + []TradeOutput{}, fmt.Errorf("could not get result Tr for operation at index 0"), + }, + { + failedResultInput, + []TradeOutput{}, fmt.Errorf("could not get ManageOfferSuccess for operation at index 0"), + }, + { + negBaseAmountInput, + []TradeOutput{}, fmt.Errorf("amount sold is negative (-1) for operation at index 0"), + }, + { + negCounterAmountInput, + []TradeOutput{}, fmt.Errorf("amount bought is negative (-2) for operation at index 0"), + }, + } + + for i := range hardCodedInputTransaction.Envelope.Operations() { + tests = append(tests, transformTest{ + input: tradeInput{index: int32(i), transaction: hardCodedInputTransaction, closeTime: genericCloseTime}, + wantOutput: hardCodedOutputArray[i], + wantErr: nil, + }) + } + + for _, test := range tests { + actualOutput, actualError := TransformTrade(test.input.index, 100, test.input.transaction, test.input.closeTime) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func wrapOperationsResultsSlice(results []xdr.OperationResult, successful bool) xdr.TransactionResultPair { + resultCode := xdr.TransactionResultCodeTxFailed + if successful { + resultCode = xdr.TransactionResultCodeTxSuccess + } + return xdr.TransactionResultPair{ + Result: xdr.TransactionResult{ + Result: xdr.TransactionResultResult{ + Code: resultCode, + Results: &results, + }, + }, + } +} + +func makeTradeTestInput() (inputTransaction ingest.LedgerTransaction) { + inputTransaction = genericLedgerTransaction + inputEnvelope := genericBumpOperationEnvelope + + inputEnvelope.Tx.SourceAccount = testAccount3 + offerOne := xdr.ClaimAtom{ + Type: xdr.ClaimAtomTypeClaimAtomTypeOrderBook, + OrderBook: &xdr.ClaimOfferAtom{ + SellerId: testAccount1ID, + OfferId: 97684906, + AssetSold: ethAsset, + AssetBought: usdtAsset, + AmountSold: 13300347, + AmountBought: 12634, + }, + } + offerTwo := xdr.ClaimAtom{ + Type: xdr.ClaimAtomTypeClaimAtomTypeOrderBook, + OrderBook: &xdr.ClaimOfferAtom{ + SellerId: testAccount3ID, + OfferId: 86106895, + AssetSold: usdtAsset, + AssetBought: nativeAsset, + AmountSold: 500, + AmountBought: 20, + }, + } + lPOne := xdr.ClaimAtom{ + Type: xdr.ClaimAtomTypeClaimAtomTypeLiquidityPool, + LiquidityPool: &xdr.ClaimLiquidityAtom{ + LiquidityPoolId: xdr.PoolId{4, 5, 6}, + AssetSold: xdr.MustNewCreditAsset("WER", testAccount4Address), + AmountSold: 123, + AssetBought: xdr.MustNewCreditAsset("NIJ", testAccount1Address), + AmountBought: 456, + }, + } + + lPTwo := xdr.ClaimAtom{ + Type: xdr.ClaimAtomTypeClaimAtomTypeLiquidityPool, + LiquidityPool: &xdr.ClaimLiquidityAtom{ + LiquidityPoolId: xdr.PoolId{1, 2, 3, 4, 5, 6}, + AssetSold: xdr.MustNewCreditAsset("HAH", testAccount1Address), + AmountSold: 1, + AssetBought: xdr.MustNewCreditAsset("WHO", testAccount4Address), + AmountBought: 1, + }, + } + + inputOperations := []xdr.Operation{ + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeManageSellOffer, + ManageSellOfferOp: &xdr.ManageSellOfferOp{}, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeManageBuyOffer, + ManageBuyOfferOp: &xdr.ManageBuyOfferOp{}, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypePathPaymentStrictSend, + PathPaymentStrictSendOp: &xdr.PathPaymentStrictSendOp{ + Destination: testAccount1, + }, + }, + }, + { + SourceAccount: &testAccount3, + Body: xdr.OperationBody{ + Type: xdr.OperationTypePathPaymentStrictReceive, + PathPaymentStrictReceiveOp: &xdr.PathPaymentStrictReceiveOp{ + Destination: testAccount1, + }, + }, + }, + { + SourceAccount: &testAccount3, + Body: xdr.OperationBody{ + Type: xdr.OperationTypePathPaymentStrictSend, + PathPaymentStrictSendOp: &xdr.PathPaymentStrictSendOp{}, + }, + }, + { + SourceAccount: &testAccount3, + Body: xdr.OperationBody{ + Type: xdr.OperationTypePathPaymentStrictReceive, + PathPaymentStrictReceiveOp: &xdr.PathPaymentStrictReceiveOp{}, + }, + }, + { + SourceAccount: nil, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeCreatePassiveSellOffer, + CreatePassiveSellOfferOp: &xdr.CreatePassiveSellOfferOp{}, + }, + }, + } + inputEnvelope.Tx.Operations = inputOperations + results := []xdr.OperationResult{ + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeManageSellOffer, + ManageSellOfferResult: &xdr.ManageSellOfferResult{ + Code: xdr.ManageSellOfferResultCodeManageSellOfferSuccess, + Success: &xdr.ManageOfferSuccessResult{ + OffersClaimed: []xdr.ClaimAtom{ + offerOne, + }, + }, + }, + }, + }, + + { + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeManageBuyOffer, + ManageBuyOfferResult: &xdr.ManageBuyOfferResult{ + Code: xdr.ManageBuyOfferResultCodeManageBuyOfferSuccess, + Success: &xdr.ManageOfferSuccessResult{ + OffersClaimed: []xdr.ClaimAtom{ + offerTwo, + }, + }, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypePathPaymentStrictSend, + PathPaymentStrictSendResult: &xdr.PathPaymentStrictSendResult{ + Code: xdr.PathPaymentStrictSendResultCodePathPaymentStrictSendSuccess, + Success: &xdr.PathPaymentStrictSendResultSuccess{ + Offers: []xdr.ClaimAtom{ + offerOne, offerTwo, + }, + }, + }, + }, + }, + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypePathPaymentStrictReceive, + PathPaymentStrictReceiveResult: &xdr.PathPaymentStrictReceiveResult{ + Code: xdr.PathPaymentStrictReceiveResultCodePathPaymentStrictReceiveSuccess, + Success: &xdr.PathPaymentStrictReceiveResultSuccess{ + Offers: []xdr.ClaimAtom{ + offerTwo, offerOne, + }, + }, + }, + }, + }, + { + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypePathPaymentStrictSend, + PathPaymentStrictSendResult: &xdr.PathPaymentStrictSendResult{ + Code: xdr.PathPaymentStrictSendResultCodePathPaymentStrictSendSuccess, + Success: &xdr.PathPaymentStrictSendResultSuccess{ + Offers: []xdr.ClaimAtom{ + lPOne, + }, + }, + }, + }, + }, + { + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypePathPaymentStrictReceive, + PathPaymentStrictReceiveResult: &xdr.PathPaymentStrictReceiveResult{ + Code: xdr.PathPaymentStrictReceiveResultCodePathPaymentStrictReceiveSuccess, + Success: &xdr.PathPaymentStrictReceiveResultSuccess{ + Offers: []xdr.ClaimAtom{ + lPTwo, + }, + }, + }, + }, + }, + { + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeCreatePassiveSellOffer, + CreatePassiveSellOfferResult: &xdr.ManageSellOfferResult{ + Code: xdr.ManageSellOfferResultCodeManageSellOfferSuccess, + Success: &xdr.ManageOfferSuccessResult{ + OffersClaimed: []xdr.ClaimAtom{}, + }, + }, + }, + }, + } + + unsafeMeta := xdr.TransactionMetaV1{ + Operations: []xdr.OperationMeta{ + { + Changes: xdr.LedgerEntryChanges{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: testAccount1ID, + OfferId: 97684906, + Price: xdr.Price{ + N: 12634, + D: 13300347, + }, + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: testAccount1ID, + OfferId: 97684906, + Price: xdr.Price{ + N: 2, + D: 4, + }, + }, + }, + }, + }, + }, + }, + { + Changes: xdr.LedgerEntryChanges{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: testAccount3ID, + OfferId: 86106895, + Price: xdr.Price{ + N: 25, + D: 1, + }, + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: testAccount3ID, + OfferId: 86106895, + Price: xdr.Price{ + N: 1111, + D: 12, + }, + }, + }, + }, + }, + }, + }, + { + Changes: xdr.LedgerEntryChanges{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: testAccount1ID, + OfferId: 97684906, + Price: xdr.Price{ + N: 12634, + D: 13300347, + }, + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: testAccount1ID, + OfferId: 97684906, + Price: xdr.Price{ + N: 1111, + D: 12, + }, + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: testAccount3ID, + OfferId: 86106895, + Price: xdr.Price{ + N: 20, + D: 500, + }, + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: testAccount3ID, + OfferId: 86106895, + Price: xdr.Price{ + N: 1111, + D: 12, + }, + }, + }, + }, + }, + }, + }, + { + Changes: xdr.LedgerEntryChanges{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: testAccount3ID, + OfferId: 86106895, + Price: xdr.Price{ + N: 20, + D: 500, + }, + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: testAccount1ID, + OfferId: 97684906, + Price: xdr.Price{ + N: 12634, + D: 13300347, + }, + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: testAccount1ID, + OfferId: 97684906, + Price: xdr.Price{ + N: 12634, + D: 13300347, + }, + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + SellerId: testAccount1ID, + Price: xdr.Price{ + N: 12634, + D: 1330, + }, + }, + }, + }, + }, + }, + }, + { + Changes: xdr.LedgerEntryChanges{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeLiquidityPool, + LiquidityPool: &xdr.LiquidityPoolEntry{ + LiquidityPoolId: xdr.PoolId{4, 5, 6}, + Body: xdr.LiquidityPoolEntryBody{ + Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, + ConstantProduct: &xdr.LiquidityPoolEntryConstantProduct{ + Params: xdr.LiquidityPoolConstantProductParameters{ + AssetA: xdr.MustNewCreditAsset("NIJ", testAccount1Address), + AssetB: xdr.MustNewCreditAsset("WER", testAccount4Address), + Fee: xdr.LiquidityPoolFeeV18, + }, + ReserveA: 400, + ReserveB: 800, + TotalPoolShares: 40, + PoolSharesTrustLineCount: 50, + }, + }, + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeLiquidityPool, + LiquidityPool: &xdr.LiquidityPoolEntry{ + LiquidityPoolId: xdr.PoolId{4, 5, 6}, + Body: xdr.LiquidityPoolEntryBody{ + Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, + ConstantProduct: &xdr.LiquidityPoolEntryConstantProduct{ + Params: xdr.LiquidityPoolConstantProductParameters{ + AssetA: xdr.MustNewCreditAsset("NIJ", testAccount1Address), + AssetB: xdr.MustNewCreditAsset("WER", testAccount4Address), + Fee: xdr.LiquidityPoolFeeV18, + }, + ReserveA: 500, + ReserveB: 750, + TotalPoolShares: 40, + PoolSharesTrustLineCount: 50, + }, + }, + }, + }, + }, + }, + }, + }, + { + Changes: xdr.LedgerEntryChanges{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeLiquidityPool, + LiquidityPool: &xdr.LiquidityPoolEntry{ + LiquidityPoolId: xdr.PoolId{1, 2, 3, 4, 5, 6}, + Body: xdr.LiquidityPoolEntryBody{ + Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, + ConstantProduct: &xdr.LiquidityPoolEntryConstantProduct{ + Params: xdr.LiquidityPoolConstantProductParameters{ + AssetA: xdr.MustNewCreditAsset("HAH", testAccount4Address), + AssetB: xdr.MustNewCreditAsset("WHO", testAccount1Address), + Fee: xdr.LiquidityPoolFeeV18, + }, + ReserveA: 100000, + ReserveB: 10000, + TotalPoolShares: 40, + PoolSharesTrustLineCount: 50, + }, + }, + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeLiquidityPool, + LiquidityPool: &xdr.LiquidityPoolEntry{ + LiquidityPoolId: xdr.PoolId{4, 5, 6}, + Body: xdr.LiquidityPoolEntryBody{ + Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, + ConstantProduct: &xdr.LiquidityPoolEntryConstantProduct{ + Params: xdr.LiquidityPoolConstantProductParameters{ + AssetA: xdr.MustNewCreditAsset("HAH", testAccount4Address), + AssetB: xdr.MustNewCreditAsset("WHO", testAccount1Address), + Fee: xdr.LiquidityPoolFeeV18, + }, + ReserveA: 999999, + ReserveB: 10001, + TotalPoolShares: 40, + PoolSharesTrustLineCount: 50, + }, + }, + }, + }, + }, + }, + }, + }, + {}, + }} + + inputTransaction.Result.Result.Result.Results = &results + inputTransaction.Envelope.V1 = &inputEnvelope + inputTransaction.UnsafeMeta.V1 = &unsafeMeta + return +} + +func makeTradeTestOutput() [][]TradeOutput { + offerOneOutput := TradeOutput{ + Order: 0, + LedgerClosedAt: genericCloseTime, + SellingAccountAddress: testAccount1Address, + SellingAssetCode: "ETH", + SellingAssetIssuer: testAccount3Address, + SellingAssetType: "credit_alphanum4", + SellingAssetID: 4476940172956910889, + SellingAmount: 13300347 * 0.0000001, + BuyingAccountAddress: testAccount3Address, + BuyingAssetCode: "USDT", + BuyingAssetIssuer: testAccount4Address, + BuyingAssetType: "credit_alphanum4", + BuyingAssetID: -8205667356306085451, + BuyingAmount: 12634 * 0.0000001, + PriceN: 12634, + PriceD: 13300347, + SellingOfferID: null.IntFrom(97684906), + BuyingOfferID: null.IntFrom(4611686018427388005), + HistoryOperationID: 101, + TradeType: 1, + } + offerTwoOutput := TradeOutput{ + Order: 0, + LedgerClosedAt: genericCloseTime, + SellingAccountAddress: testAccount3Address, + SellingAssetCode: "USDT", + SellingAssetIssuer: testAccount4Address, + SellingAssetType: "credit_alphanum4", + SellingAssetID: -8205667356306085451, + SellingAmount: 500 * 0.0000001, + BuyingAccountAddress: testAccount3Address, + BuyingAssetCode: "", + BuyingAssetIssuer: "", + BuyingAssetType: "native", + BuyingAssetID: -5706705804583548011, + BuyingAmount: 20 * 0.0000001, + PriceN: 25, + PriceD: 1, + SellingOfferID: null.IntFrom(86106895), + BuyingOfferID: null.IntFrom(4611686018427388005), + HistoryOperationID: 101, + TradeType: 1, + } + + lPOneOutput := TradeOutput{ + Order: 0, + LedgerClosedAt: genericCloseTime, + SellingAssetCode: "WER", + SellingAssetIssuer: testAccount4Address, + SellingAssetType: "credit_alphanum4", + SellingAssetID: -7615773297180926952, + SellingAmount: 123 * 0.0000001, + BuyingAccountAddress: testAccount3Address, + BuyingAssetCode: "NIJ", + BuyingAssetIssuer: testAccount1Address, + BuyingAssetType: "credit_alphanum4", + BuyingAssetID: -8061435944444096568, + BuyingAmount: 456 * 0.0000001, + PriceN: 456, + PriceD: 123, + BuyingOfferID: null.IntFrom(4611686018427388005), + SellingLiquidityPoolID: null.StringFrom("0405060000000000000000000000000000000000000000000000000000000000"), + LiquidityPoolFee: null.IntFrom(30), + HistoryOperationID: 101, + TradeType: 2, + RoundingSlippage: null.IntFrom(0), + SellerIsExact: null.BoolFrom(false), + } + + lPTwoOutput := TradeOutput{ + Order: 0, + LedgerClosedAt: genericCloseTime, + SellingAssetCode: "HAH", + SellingAssetIssuer: testAccount1Address, + SellingAssetType: "credit_alphanum4", + SellingAssetID: -6231594281606355691, + SellingAmount: 1 * 0.0000001, + BuyingAccountAddress: testAccount3Address, + BuyingAssetCode: "WHO", + BuyingAssetIssuer: testAccount4Address, + BuyingAssetType: "credit_alphanum4", + BuyingAssetID: -680582465233747022, + BuyingAmount: 1 * 0.0000001, + PriceN: 1, + PriceD: 1, + BuyingOfferID: null.IntFrom(4611686018427388005), + SellingLiquidityPoolID: null.StringFrom("0102030405060000000000000000000000000000000000000000000000000000"), + LiquidityPoolFee: null.IntFrom(30), + HistoryOperationID: 101, + TradeType: 2, + RoundingSlippage: null.IntFrom(9223372036854775807), + SellerIsExact: null.BoolFrom(true), + } + + onePriceIsAmount := offerOneOutput + onePriceIsAmount.PriceN = 12634 + onePriceIsAmount.PriceD = 13300347 + onePriceIsAmount.SellerIsExact = null.BoolFrom(false) + + offerOneOutputSecondPlace := onePriceIsAmount + offerOneOutputSecondPlace.Order = 1 + offerOneOutputSecondPlace.SellerIsExact = null.BoolFrom(true) + + twoPriceIsAmount := offerTwoOutput + twoPriceIsAmount.PriceN = int64(twoPriceIsAmount.BuyingAmount * 10000000) + twoPriceIsAmount.PriceD = int64(twoPriceIsAmount.SellingAmount * 10000000) + twoPriceIsAmount.SellerIsExact = null.BoolFrom(true) + + offerTwoOutputSecondPlace := twoPriceIsAmount + offerTwoOutputSecondPlace.Order = 1 + offerTwoOutputSecondPlace.SellerIsExact = null.BoolFrom(false) + + output := [][]TradeOutput{ + {offerOneOutput}, + {offerTwoOutput}, + {onePriceIsAmount, offerTwoOutputSecondPlace}, + {twoPriceIsAmount, offerOneOutputSecondPlace}, + {lPOneOutput}, + {lPTwoOutput}, + {}, + } + return output +} diff --git a/ingest/processors/transaction.go b/ingest/processors/transaction.go new file mode 100644 index 0000000000..7a29034242 --- /dev/null +++ b/ingest/processors/transaction.go @@ -0,0 +1,331 @@ +package processors + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "strconv" + + "github.com/guregu/null" + "github.com/lib/pq" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/strkey" + "github.com/stellar/go/toid" + "github.com/stellar/go/xdr" +) + +// TransformTransaction converts a transaction from the history archive ingestion system into a form suitable for BigQuery +func TransformTransaction(transaction ingest.LedgerTransaction, lhe xdr.LedgerHeaderHistoryEntry) (TransactionOutput, error) { + ledgerHeader := lhe.Header + outputTransactionHash := HashToHexString(transaction.Result.TransactionHash) + outputLedgerSequence := uint32(ledgerHeader.LedgerSeq) + + transactionIndex := uint32(transaction.Index) + + outputTransactionID := toid.New(int32(outputLedgerSequence), int32(transactionIndex), 0).ToInt64() + + sourceAccount := transaction.Envelope.SourceAccount() + outputAccount, err := GetAccountAddressFromMuxedAccount(transaction.Envelope.SourceAccount()) + if err != nil { + return TransactionOutput{}, fmt.Errorf("for ledger %d; transaction %d (transaction id=%d): %v", outputLedgerSequence, transactionIndex, outputTransactionID, err) + } + + outputAccountSequence := transaction.Envelope.SeqNum() + if outputAccountSequence < 0 { + return TransactionOutput{}, fmt.Errorf("the account's sequence number (%d) is negative for ledger %d; transaction %d (transaction id=%d)", outputAccountSequence, outputLedgerSequence, transactionIndex, outputTransactionID) + } + + outputMaxFee := transaction.Envelope.Fee() + + outputFeeCharged := int64(transaction.Result.Result.FeeCharged) + if outputFeeCharged < 0 { + return TransactionOutput{}, fmt.Errorf("the fee charged (%d) is negative for ledger %d; transaction %d (transaction id=%d)", outputFeeCharged, outputLedgerSequence, transactionIndex, outputTransactionID) + } + + outputOperationCount := int32(len(transaction.Envelope.Operations())) + + outputTxEnvelope, err := xdr.MarshalBase64(transaction.Envelope) + if err != nil { + return TransactionOutput{}, err + } + + outputTxResult, err := xdr.MarshalBase64(&transaction.Result.Result) + if err != nil { + return TransactionOutput{}, err + } + + outputTxMeta, err := xdr.MarshalBase64(transaction.UnsafeMeta) + if err != nil { + return TransactionOutput{}, err + } + + outputTxFeeMeta, err := xdr.MarshalBase64(transaction.FeeChanges) + if err != nil { + return TransactionOutput{}, err + } + + outputCreatedAt, err := TimePointToUTCTimeStamp(ledgerHeader.ScpValue.CloseTime) + if err != nil { + return TransactionOutput{}, fmt.Errorf("for ledger %d; transaction %d (transaction id=%d): %v", outputLedgerSequence, transactionIndex, outputTransactionID, err) + } + + memoObject := transaction.Envelope.Memo() + outputMemoContents := "" + switch xdr.MemoType(memoObject.Type) { + case xdr.MemoTypeMemoText: + outputMemoContents = memoObject.MustText() + case xdr.MemoTypeMemoId: + outputMemoContents = strconv.FormatUint(uint64(memoObject.MustId()), 10) + case xdr.MemoTypeMemoHash: + hash := memoObject.MustHash() + outputMemoContents = base64.StdEncoding.EncodeToString(hash[:]) + case xdr.MemoTypeMemoReturn: + hash := memoObject.MustRetHash() + outputMemoContents = base64.StdEncoding.EncodeToString(hash[:]) + } + + outputMemoType := memoObject.Type.String() + timeBound := transaction.Envelope.TimeBounds() + outputTimeBounds := "" + if timeBound != nil { + if timeBound.MaxTime < timeBound.MinTime && timeBound.MaxTime != 0 { + + return TransactionOutput{}, fmt.Errorf("the max time is earlier than the min time (%d < %d) for ledger %d; transaction %d (transaction id=%d)", + timeBound.MaxTime, timeBound.MinTime, outputLedgerSequence, transactionIndex, outputTransactionID) + } + + if timeBound.MaxTime == 0 { + outputTimeBounds = fmt.Sprintf("[%d,)", timeBound.MinTime) + } else { + outputTimeBounds = fmt.Sprintf("[%d,%d)", timeBound.MinTime, timeBound.MaxTime) + } + + } + + ledgerBound := transaction.Envelope.LedgerBounds() + outputLedgerBound := "" + if ledgerBound != nil { + outputLedgerBound = fmt.Sprintf("[%d,%d)", int64(ledgerBound.MinLedger), int64(ledgerBound.MaxLedger)) + } + + minSequenceNumber := transaction.Envelope.MinSeqNum() + outputMinSequence := null.Int{} + if minSequenceNumber != nil { + outputMinSequence = null.IntFrom(int64(*minSequenceNumber)) + } + + minSequenceAge := transaction.Envelope.MinSeqAge() + outputMinSequenceAge := null.Int{} + if minSequenceAge != nil { + outputMinSequenceAge = null.IntFrom(int64(*minSequenceAge)) + } + + minSequenceLedgerGap := transaction.Envelope.MinSeqLedgerGap() + outputMinSequenceLedgerGap := null.Int{} + if minSequenceLedgerGap != nil { + outputMinSequenceLedgerGap = null.IntFrom(int64(*minSequenceLedgerGap)) + } + + // Soroban fees and resources + // Note: MaxFee and FeeCharged is the sum of base transaction fees + Soroban fees + // Breakdown of Soroban fees can be calculated by the config_setting resource pricing * the resources used + + var sorobanData xdr.SorobanTransactionData + var hasSorobanData bool + var outputResourceFee int64 + var outputSorobanResourcesInstructions uint32 + var outputSorobanResourcesReadBytes uint32 + var outputSorobanResourcesWriteBytes uint32 + var outputInclusionFeeBid int64 + var outputInclusionFeeCharged int64 + var outputResourceFeeRefund int64 + var outputTotalNonRefundableResourceFeeCharged int64 + var outputTotalRefundableResourceFeeCharged int64 + var outputRentFeeCharged int64 + var feeAccountAddress string + + // Soroban data can exist in V1 and FeeBump transactionEnvelopes + switch transaction.Envelope.Type { + case xdr.EnvelopeTypeEnvelopeTypeTx: + sorobanData, hasSorobanData = transaction.Envelope.V1.Tx.Ext.GetSorobanData() + feeAccountAddress = sourceAccount.Address() + case xdr.EnvelopeTypeEnvelopeTypeTxFeeBump: + sorobanData, hasSorobanData = transaction.Envelope.FeeBump.Tx.InnerTx.V1.Tx.Ext.GetSorobanData() + feeBumpAccount := transaction.Envelope.FeeBumpAccount() + feeAccountAddress = feeBumpAccount.Address() + } + + if hasSorobanData { + outputResourceFee = int64(sorobanData.ResourceFee) + outputSorobanResourcesInstructions = uint32(sorobanData.Resources.Instructions) + outputSorobanResourcesReadBytes = uint32(sorobanData.Resources.ReadBytes) + outputSorobanResourcesWriteBytes = uint32(sorobanData.Resources.WriteBytes) + outputInclusionFeeBid = int64(transaction.Envelope.Fee()) - outputResourceFee + + accountBalanceStart, accountBalanceEnd := getAccountBalanceFromLedgerEntryChanges(transaction.FeeChanges, feeAccountAddress) + initialFeeCharged := accountBalanceStart - accountBalanceEnd + outputInclusionFeeCharged = initialFeeCharged - outputResourceFee + + meta, ok := transaction.UnsafeMeta.GetV3() + if ok { + accountBalanceStart, accountBalanceEnd := getAccountBalanceFromLedgerEntryChanges(meta.TxChangesAfter, feeAccountAddress) + outputResourceFeeRefund = accountBalanceEnd - accountBalanceStart + if meta.SorobanMeta != nil { + extV1, ok := meta.SorobanMeta.Ext.GetV1() + if ok { + outputTotalNonRefundableResourceFeeCharged = int64(extV1.TotalNonRefundableResourceFeeCharged) + outputTotalRefundableResourceFeeCharged = int64(extV1.TotalRefundableResourceFeeCharged) + outputRentFeeCharged = int64(extV1.RentFeeCharged) + } + } + } + + // Protocol 20 contained a bug where the feeCharged was incorrectly calculated but was fixed for + // Protocol 21 with https://github.com/stellar/stellar-core/issues/4188 + // Any Soroban Fee Bump transactions before P21 will need the below logic to calculate the correct feeCharged + if ledgerHeader.LedgerVersion < 21 && transaction.Envelope.Type == xdr.EnvelopeTypeEnvelopeTypeTxFeeBump { + outputFeeCharged = outputResourceFee - outputResourceFeeRefund + outputInclusionFeeCharged + } + } + + outputCloseTime, err := TimePointToUTCTimeStamp(ledgerHeader.ScpValue.CloseTime) + if err != nil { + return TransactionOutput{}, fmt.Errorf("for ledger %d; transaction %d (transaction id=%d): %v", outputLedgerSequence, transactionIndex, outputTransactionID, err) + } + + outputTxResultCode := transaction.Result.Result.Result.Code.String() + + txSigners, err := getTxSigners(transaction.Envelope.Signatures()) + if err != nil { + return TransactionOutput{}, err + } + + outputSuccessful := transaction.Result.Successful() + transformedTransaction := TransactionOutput{ + TransactionHash: outputTransactionHash, + LedgerSequence: outputLedgerSequence, + TransactionID: outputTransactionID, + Account: outputAccount, + AccountSequence: outputAccountSequence, + MaxFee: outputMaxFee, + FeeCharged: outputFeeCharged, + OperationCount: outputOperationCount, + TxEnvelope: outputTxEnvelope, + TxResult: outputTxResult, + TxMeta: outputTxMeta, + TxFeeMeta: outputTxFeeMeta, + CreatedAt: outputCreatedAt, + MemoType: outputMemoType, + Memo: outputMemoContents, + TimeBounds: outputTimeBounds, + Successful: outputSuccessful, + LedgerBounds: outputLedgerBound, + MinAccountSequence: outputMinSequence, + MinAccountSequenceAge: outputMinSequenceAge, + MinAccountSequenceLedgerGap: outputMinSequenceLedgerGap, + ExtraSigners: formatSigners(transaction.Envelope.ExtraSigners()), + ClosedAt: outputCloseTime, + ResourceFee: outputResourceFee, + SorobanResourcesInstructions: outputSorobanResourcesInstructions, + SorobanResourcesReadBytes: outputSorobanResourcesReadBytes, + SorobanResourcesWriteBytes: outputSorobanResourcesWriteBytes, + TransactionResultCode: outputTxResultCode, + InclusionFeeBid: outputInclusionFeeBid, + InclusionFeeCharged: outputInclusionFeeCharged, + ResourceFeeRefund: outputResourceFeeRefund, + TotalNonRefundableResourceFeeCharged: outputTotalNonRefundableResourceFeeCharged, + TotalRefundableResourceFeeCharged: outputTotalRefundableResourceFeeCharged, + RentFeeCharged: outputRentFeeCharged, + TxSigners: txSigners, + } + + // Add Muxed Account Details, if exists + if sourceAccount.Type == xdr.CryptoKeyTypeKeyTypeMuxedEd25519 { + muxedAddress, err := sourceAccount.GetAddress() + if err != nil { + return TransactionOutput{}, err + } + transformedTransaction.AccountMuxed = muxedAddress + + } + + // Add Fee Bump Details, if exists + if transaction.Envelope.IsFeeBump() { + feeBumpAccount := transaction.Envelope.FeeBumpAccount() + feeAccount := feeBumpAccount.ToAccountId() + if feeBumpAccount.Type == xdr.CryptoKeyTypeKeyTypeMuxedEd25519 { + feeAccountMuxed := feeBumpAccount.Address() + transformedTransaction.FeeAccountMuxed = feeAccountMuxed + } + transformedTransaction.FeeAccount = feeAccount.Address() + innerHash := transaction.Result.InnerHash() + transformedTransaction.InnerTransactionHash = hex.EncodeToString(innerHash[:]) + transformedTransaction.NewMaxFee = uint32(transaction.Envelope.FeeBumpFee()) + txSigners, err := getTxSigners(transaction.Envelope.FeeBump.Signatures) + if err != nil { + return TransactionOutput{}, err + } + + transformedTransaction.TxSigners = txSigners + } + + return transformedTransaction, nil +} + +func getAccountBalanceFromLedgerEntryChanges(changes xdr.LedgerEntryChanges, sourceAccountAddress string) (int64, int64) { + var accountBalanceStart int64 + var accountBalanceEnd int64 + + for _, change := range changes { + switch change.Type { + case xdr.LedgerEntryChangeTypeLedgerEntryUpdated: + accountEntry, ok := change.Updated.Data.GetAccount() + if !ok { + continue + } + + if accountEntry.AccountId.Address() == sourceAccountAddress { + accountBalanceEnd = int64(accountEntry.Balance) + } + case xdr.LedgerEntryChangeTypeLedgerEntryState: + accountEntry, ok := change.State.Data.GetAccount() + if !ok { + continue + } + + if accountEntry.AccountId.Address() == sourceAccountAddress { + accountBalanceStart = int64(accountEntry.Balance) + } + } + } + + return accountBalanceStart, accountBalanceEnd +} + +func formatSigners(s []xdr.SignerKey) pq.StringArray { + if s == nil { + return nil + } + + signers := make([]string, len(s)) + for i, key := range s { + signers[i] = key.Address() + } + + return signers +} + +func getTxSigners(xdrSignatures []xdr.DecoratedSignature) ([]string, error) { + signers := make([]string, len(xdrSignatures)) + + for i, sig := range xdrSignatures { + signerAccount, err := strkey.Encode(strkey.VersionByteAccountID, sig.Signature) + if err != nil { + return nil, err + } + signers[i] = signerAccount + } + + return signers, nil +} diff --git a/ingest/processors/transaction_test.go b/ingest/processors/transaction_test.go new file mode 100644 index 0000000000..8b10a9906e --- /dev/null +++ b/ingest/processors/transaction_test.go @@ -0,0 +1,414 @@ +package processors + +import ( + "fmt" + "testing" + "time" + + "github.com/guregu/null" + "github.com/lib/pq" + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformTransaction(t *testing.T) { + type inputStruct struct { + transaction ingest.LedgerTransaction + historyHeader xdr.LedgerHeaderHistoryEntry + } + type transformTest struct { + input inputStruct + wantOutput TransactionOutput + wantErr error + } + genericInput := inputStruct{genericLedgerTransaction, genericLedgerHeaderHistoryEntry} + negativeSeqInput := genericInput + negativeSeqEnvelope := genericBumpOperationEnvelopeForTransaction + negativeSeqEnvelope.Tx.SeqNum = xdr.SequenceNumber(-1) + negativeSeqInput.transaction.Envelope.V1 = &negativeSeqEnvelope + + badTimeboundInput := genericInput + badTimeboundEnvelope := genericBumpOperationEnvelopeForTransaction + badTimeboundEnvelope.Tx.Cond.Type = xdr.PreconditionTypePrecondTime + badTimeboundEnvelope.Tx.Cond.TimeBounds = &xdr.TimeBounds{ + MinTime: 1594586912, + MaxTime: 100, + } + badTimeboundInput.transaction.Envelope.V1 = &badTimeboundEnvelope + + badFeeChargedInput := genericInput + badFeeChargedInput.transaction.Result.Result.FeeCharged = -1 + + hardCodedTransaction, hardCodedLedgerHeader, err := makeTransactionTestInput() + assert.NoError(t, err) + hardCodedOutput, err := makeTransactionTestOutput() + assert.NoError(t, err) + + tests := []transformTest{ + { + negativeSeqInput, + TransactionOutput{}, + fmt.Errorf("the account's sequence number (-1) is negative for ledger 0; transaction 1 (transaction id=4096)"), + }, + { + badFeeChargedInput, + TransactionOutput{}, + fmt.Errorf("the fee charged (-1) is negative for ledger 0; transaction 1 (transaction id=4096)"), + }, + { + badTimeboundInput, + TransactionOutput{}, + fmt.Errorf("the max time is earlier than the min time (100 < 1594586912) for ledger 0; transaction 1 (transaction id=4096)"), + }, + } + + for i := range hardCodedTransaction { + tests = append(tests, transformTest{ + input: inputStruct{hardCodedTransaction[i], hardCodedLedgerHeader[i]}, + wantOutput: hardCodedOutput[i], + wantErr: nil, + }) + } + + for _, test := range tests { + actualOutput, actualError := TransformTransaction(test.input.transaction, test.input.historyHeader) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func makeTransactionTestOutput() (output []TransactionOutput, err error) { + correctTime, err := time.Parse("2006-1-2 15:04:05 MST", "2020-07-09 05:28:42 UTC") + output = []TransactionOutput{ + { + TxEnvelope: "AAAAAgAAAACI4aa0pXFSj6qfJuIObLw/5zyugLRGYwxb7wFSr3B9eAABX5ABjydzAABBtwAAAAEAAAAAAAAAAAAAAABfBqt0AAAAAQAAABdITDVhQ2dvelFISVc3c1NjNVhkY2ZtUgAAAAABAAAAAQAAAAAcR0GXGO76pFs4y38vJVAanjnLg4emNun7zAx0pHcDGAAAAAIAAAAAAAAAAAAAAAAAAAAAAQIDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFjQq+PAAAAQPRri1y9nM9PVDgCRksW7TJk8p+xG/BCerYtvU4Ffxo9s+7lTCDOeg2ahZSVHfowhCxWozggLEtX4vtMBDu2hAg=", + TxResult: "AAAAAAAAASz/////AAAAAQAAAAAAAAAAAAAAAAAAAAA=", + TxMeta: "AAAAAQAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAwAAAAAAAAAFAQIDBAUGBwgJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFVU1NEAAAAAGtY3WxokwttAx3Fu/riPvoew/C7WMK8jZONR8Hfs75zAAAAHgAAAAAAAYagAAAAAAAAA+gAAAAAAAAB9AAAAAAAAAAZAAAAAAAAAAEAAAAAAAAABQECAwQFBgcICQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVVNTRAAAAABrWN1saJMLbQMdxbv64j76HsPwu1jCvI2TjUfB37O+cwAAAB4AAAAAAAGKiAAAAAAAAARMAAAAAAAAAfYAAAAAAAAAGgAAAAAAAAACAAAAAwAAAAAAAAAFAQIDBAUGBwgJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFVU1NEAAAAAGtY3WxokwttAx3Fu/riPvoew/C7WMK8jZONR8Hfs75zAAAAHgAAAAAAAYagAAAAAAAAA+gAAAAAAAAB9AAAAAAAAAAZAAAAAAAAAAEAAAAAAAAABQECAwQFBgcICQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVVNTRAAAAABrWN1saJMLbQMdxbv64j76HsPwu1jCvI2TjUfB37O+cwAAAB4AAAAAAAGKiAAAAAAAAARMAAAAAAAAAfYAAAAAAAAAGgAAAAAAAAAA", + TxFeeMeta: "AAAAAA==", + TransactionHash: "a87fef5eeb260269c380f2de456aad72b59bb315aaac777860456e09dac0bafb", + LedgerSequence: 30521816, + TransactionID: 131090201534533632, + Account: testAccount1Address, + AccountSequence: 112351890582290871, + MaxFee: 90000, + FeeCharged: 300, + OperationCount: 1, + CreatedAt: correctTime, + MemoType: "MemoTypeMemoText", + Memo: "HL5aCgozQHIW7sSc5XdcfmR", + TimeBounds: "[0,1594272628)", + Successful: false, + ClosedAt: time.Date(2020, time.July, 9, 5, 28, 42, 0, time.UTC), + ResourceFee: 0, + SorobanResourcesInstructions: 0, + SorobanResourcesReadBytes: 0, + SorobanResourcesWriteBytes: 0, + TransactionResultCode: "TransactionResultCodeTxFailed", + TxSigners: []string{"GD2GXC24XWOM6T2UHABEMSYW5UZGJ4U7WEN7AQT2WYW32TQFP4ND3M7O4VGCBTT2BWNILFEVDX5DBBBMK2RTQIBMJNL6F62MAQ53NBAIXUDA"}, + }, + { + TxEnvelope: "AAAABQAAAQAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHCAAAAACAAAAAIjhprSlcVKPqp8m4g5svD/nPK6AtEZjDFvvAVKvcH14AAAAAAIU9jYAAAB9AAAAAQAAAAAAAAAAAAAAAF8Gq3QAAAABAAAAF0hMNWFDZ296UUhJVzdzU2M1WGRjZm1SAAAAAAEAAAABAAAAABxHQZcY7vqkWzjLfy8lUBqeOcuDh6Y26fvMDHSkdwMYAAAAAgAAAAAAAAAAAAAAAAAAAAABAgMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABY0KvjwAAAED0a4tcvZzPT1Q4AkZLFu0yZPKfsRvwQnq2Lb1OBX8aPbPu5UwgznoNmoWUlR36MIQsVqM4ICxLV+L7TAQ7toQI", + TxResult: "AAAAAAAAASwAAAABqH/vXusmAmnDgPLeRWqtcrWbsxWqrHd4YEVuCdrAuvsAAAAAAAAAZAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + TxMeta: "AAAAAQAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAwAAAAAAAAAFAQIDBAUGBwgJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFVU1NEAAAAAGtY3WxokwttAx3Fu/riPvoew/C7WMK8jZONR8Hfs75zAAAAHgAAAAAAAYagAAAAAAAAA+gAAAAAAAAB9AAAAAAAAAAZAAAAAAAAAAEAAAAAAAAABQECAwQFBgcICQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVVNTRAAAAABrWN1saJMLbQMdxbv64j76HsPwu1jCvI2TjUfB37O+cwAAAB4AAAAAAAGKiAAAAAAAAARMAAAAAAAAAfYAAAAAAAAAGgAAAAAAAAACAAAAAwAAAAAAAAAFAQIDBAUGBwgJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFVU1NEAAAAAGtY3WxokwttAx3Fu/riPvoew/C7WMK8jZONR8Hfs75zAAAAHgAAAAAAAYagAAAAAAAAA+gAAAAAAAAB9AAAAAAAAAAZAAAAAAAAAAEAAAAAAAAABQECAwQFBgcICQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVVNTRAAAAABrWN1saJMLbQMdxbv64j76HsPwu1jCvI2TjUfB37O+cwAAAB4AAAAAAAGKiAAAAAAAAARMAAAAAAAAAfYAAAAAAAAAGgAAAAAAAAAA", + TxFeeMeta: "AAAAAA==", + TransactionHash: "a87fef5eeb260269c380f2de456aad72b59bb315aaac777860456e09dac0bafb", + LedgerSequence: 30521817, + TransactionID: 131090205829500928, + Account: testAccount1Address, + AccountSequence: 150015399398735997, + MaxFee: 0, + FeeCharged: 300, + OperationCount: 1, + CreatedAt: correctTime, + MemoType: "MemoTypeMemoText", + Memo: "HL5aCgozQHIW7sSc5XdcfmR", + TimeBounds: "[0,1594272628)", + Successful: true, + InnerTransactionHash: "a87fef5eeb260269c380f2de456aad72b59bb315aaac777860456e09dac0bafb", + FeeAccount: testAccount5Address, + FeeAccountMuxed: "MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFNZG", + NewMaxFee: 7200, + ClosedAt: time.Date(2020, time.July, 9, 5, 28, 42, 0, time.UTC), + ResourceFee: 0, + SorobanResourcesInstructions: 0, + SorobanResourcesReadBytes: 0, + SorobanResourcesWriteBytes: 0, + TransactionResultCode: "TransactionResultCodeTxFeeBumpInnerSuccess", //inner fee bump success + TxSigners: []string{"GD2GXC24XWOM6T2UHABEMSYW5UZGJ4U7WEN7AQT2WYW32TQFP4ND3M7O4VGCBTT2BWNILFEVDX5DBBBMK2RTQIBMJNL6F62MAQ53NBAIXUDA"}, + }, + { + TxEnvelope: "AAAAAgAAAAAcR0GXGO76pFs4y38vJVAanjnLg4emNun7zAx0pHcDGAAAAGQBpLyvsiV6gwAAAAIAAAABAAAAAAAAAAAAAAAAXwardAAAAAEAAAAFAAAACgAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAMCAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAABdITDVhQ2dvelFISVc3c1NjNVhkY2ZtUgAAAAABAAAAAQAAAABrWN1saJMLbQMdxbv64j76HsPwu1jCvI2TjUfB37O+cwAAAAIAAAAAAAAAAAAAAAAAAAAAAQIDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFjQq+PAAAAQPRri1y9nM9PVDgCRksW7TJk8p+xG/BCerYtvU4Ffxo9s+7lTCDOeg2ahZSVHfowhCxWozggLEtX4vtMBDu2hAg=", + TxResult: "AAAAAAAAAGT////5AAAAAA==", + TxMeta: "AAAAAQAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAwAAAAAAAAAFAQIDBAUGBwgJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFVU1NEAAAAAGtY3WxokwttAx3Fu/riPvoew/C7WMK8jZONR8Hfs75zAAAAHgAAAAAAAYagAAAAAAAAA+gAAAAAAAAB9AAAAAAAAAAZAAAAAAAAAAEAAAAAAAAABQECAwQFBgcICQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVVNTRAAAAABrWN1saJMLbQMdxbv64j76HsPwu1jCvI2TjUfB37O+cwAAAB4AAAAAAAGKiAAAAAAAAARMAAAAAAAAAfYAAAAAAAAAGgAAAAAAAAACAAAAAwAAAAAAAAAFAQIDBAUGBwgJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFVU1NEAAAAAGtY3WxokwttAx3Fu/riPvoew/C7WMK8jZONR8Hfs75zAAAAHgAAAAAAAYagAAAAAAAAA+gAAAAAAAAB9AAAAAAAAAAZAAAAAAAAAAEAAAAAAAAABQECAwQFBgcICQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVVNTRAAAAABrWN1saJMLbQMdxbv64j76HsPwu1jCvI2TjUfB37O+cwAAAB4AAAAAAAGKiAAAAAAAAARMAAAAAAAAAfYAAAAAAAAAGgAAAAAAAAAA", + TxFeeMeta: "AAAAAA==", + TransactionHash: "a87fef5eeb260269c380f2de456aad72b59bb315aaac777860456e09dac0bafb", + LedgerSequence: 30521818, + TransactionID: 131090210124468224, + Account: testAccount2Address, + AccountSequence: 118426953012574851, + MaxFee: 100, + FeeCharged: 100, + OperationCount: 1, + CreatedAt: correctTime, + MemoType: "MemoTypeMemoText", + Memo: "HL5aCgozQHIW7sSc5XdcfmR", + TimeBounds: "[0,1594272628)", + Successful: false, + LedgerBounds: "[5,10)", + ExtraSigners: pq.StringArray{"GABQEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB7QL"}, + MinAccountSequenceAge: null.IntFrom(0), + MinAccountSequenceLedgerGap: null.IntFrom(0), + ClosedAt: time.Date(2020, time.July, 9, 5, 28, 42, 0, time.UTC), + ResourceFee: 0, + SorobanResourcesInstructions: 0, + SorobanResourcesReadBytes: 0, + SorobanResourcesWriteBytes: 0, + TransactionResultCode: "TransactionResultCodeTxInsufficientBalance", + TxSigners: []string{"GD2GXC24XWOM6T2UHABEMSYW5UZGJ4U7WEN7AQT2WYW32TQFP4ND3M7O4VGCBTT2BWNILFEVDX5DBBBMK2RTQIBMJNL6F62MAQ53NBAIXUDA"}, + }, + } + return +} +func makeTransactionTestInput() (transaction []ingest.LedgerTransaction, historyHeader []xdr.LedgerHeaderHistoryEntry, err error) { + hardCodedMemoText := "HL5aCgozQHIW7sSc5XdcfmR" + hardCodedTransactionHash := xdr.Hash([32]byte{0xa8, 0x7f, 0xef, 0x5e, 0xeb, 0x26, 0x2, 0x69, 0xc3, 0x80, 0xf2, 0xde, 0x45, 0x6a, 0xad, 0x72, 0xb5, 0x9b, 0xb3, 0x15, 0xaa, 0xac, 0x77, 0x78, 0x60, 0x45, 0x6e, 0x9, 0xda, 0xc0, 0xba, 0xfb}) + genericResultResults := &[]xdr.OperationResult{ + { + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeCreateAccount, + CreateAccountResult: &xdr.CreateAccountResult{ + Code: 0, + }, + }, + }, + } + hardCodedMeta := xdr.TransactionMeta{ + V: 1, + V1: genericTxMeta, + } + + source := xdr.MuxedAccount{ + Type: xdr.CryptoKeyTypeKeyTypeEd25519, + Ed25519: &xdr.Uint256{3, 2, 1}, + } + destination := xdr.MuxedAccount{ + Type: xdr.CryptoKeyTypeKeyTypeEd25519, + Ed25519: &xdr.Uint256{1, 2, 3}, + } + signerKey := xdr.SignerKey{ + Type: xdr.SignerKeyTypeSignerKeyTypeEd25519, + Ed25519: source.Ed25519, + } + transaction = []ingest.LedgerTransaction{ + { + Index: 1, + UnsafeMeta: hardCodedMeta, + Envelope: xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: testAccount1, + SeqNum: 112351890582290871, + Memo: xdr.Memo{ + Type: xdr.MemoTypeMemoText, + Text: &hardCodedMemoText, + }, + Fee: 90000, + Cond: xdr.Preconditions{ + Type: xdr.PreconditionTypePrecondTime, + TimeBounds: &xdr.TimeBounds{ + MinTime: 0, + MaxTime: 1594272628, + }, + }, + Operations: []xdr.Operation{ + { + SourceAccount: &testAccount2, + Body: xdr.OperationBody{ + Type: xdr.OperationTypePathPaymentStrictReceive, + PathPaymentStrictReceiveOp: &xdr.PathPaymentStrictReceiveOp{ + Destination: destination, + }, + }, + }, + }, + }, + Signatures: []xdr.DecoratedSignature{ + { + Hint: xdr.SignatureHint{99, 66, 175, 143}, + Signature: xdr.Signature{244, 107, 139, 92, 189, 156, 207, 79, 84, 56, 2, 70, 75, 22, 237, 50, 100, 242, 159, 177, 27, 240, 66, 122, 182, 45, 189, 78, 5, 127, 26, 61, 179, 238, 229, 76, 32, 206, 122, 13, 154, 133, 148, 149, 29, 250, 48, 132, 44, 86, 163, 56, 32, 44, 75, 87, 226, 251, 76, 4, 59, 182, 132, 8}, + }, + }, + }, + }, + Result: xdr.TransactionResultPair{ + TransactionHash: hardCodedTransactionHash, + Result: xdr.TransactionResult{ + FeeCharged: 300, + Result: xdr.TransactionResultResult{ + Code: xdr.TransactionResultCodeTxFailed, + Results: genericResultResults, + }, + }, + }, + }, + { + Index: 1, + UnsafeMeta: hardCodedMeta, + Envelope: xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTxFeeBump, + FeeBump: &xdr.FeeBumpTransactionEnvelope{ + Tx: xdr.FeeBumpTransaction{ + FeeSource: testAccount5, + Fee: 7200, + InnerTx: xdr.FeeBumpTransactionInnerTx{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: testAccount1, + SeqNum: 150015399398735997, + Memo: xdr.Memo{ + Type: xdr.MemoTypeMemoText, + Text: &hardCodedMemoText, + }, + Cond: xdr.Preconditions{ + Type: xdr.PreconditionTypePrecondTime, + TimeBounds: &xdr.TimeBounds{ + MinTime: 0, + MaxTime: 1594272628, + }, + }, + Operations: []xdr.Operation{ + { + SourceAccount: &testAccount2, + Body: xdr.OperationBody{ + Type: xdr.OperationTypePathPaymentStrictReceive, + PathPaymentStrictReceiveOp: &xdr.PathPaymentStrictReceiveOp{ + Destination: destination, + }, + }, + }, + }, + }, + }, + }, + }, + Signatures: []xdr.DecoratedSignature{ + { + Hint: xdr.SignatureHint{99, 66, 175, 143}, + Signature: xdr.Signature{244, 107, 139, 92, 189, 156, 207, 79, 84, 56, 2, 70, 75, 22, 237, 50, 100, 242, 159, 177, 27, 240, 66, 122, 182, 45, 189, 78, 5, 127, 26, 61, 179, 238, 229, 76, 32, 206, 122, 13, 154, 133, 148, 149, 29, 250, 48, 132, 44, 86, 163, 56, 32, 44, 75, 87, 226, 251, 76, 4, 59, 182, 132, 8}, + }, + }, + }, + }, + Result: xdr.TransactionResultPair{ + TransactionHash: hardCodedTransactionHash, + Result: xdr.TransactionResult{ + FeeCharged: 300, + Result: xdr.TransactionResultResult{ + Code: xdr.TransactionResultCodeTxFeeBumpInnerSuccess, + InnerResultPair: &xdr.InnerTransactionResultPair{ + TransactionHash: hardCodedTransactionHash, + Result: xdr.InnerTransactionResult{ + FeeCharged: 100, + Result: xdr.InnerTransactionResultResult{ + Code: xdr.TransactionResultCodeTxSuccess, + Results: &[]xdr.OperationResult{ + { + Tr: &xdr.OperationResultTr{ + CreateAccountResult: &xdr.CreateAccountResult{}, + }, + }, + }, + }, + }, + }, + Results: &[]xdr.OperationResult{{}}, + }, + }, + }, + }, + { + Index: 1, + UnsafeMeta: hardCodedMeta, + Envelope: xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: testAccount2, + SeqNum: 118426953012574851, + Memo: xdr.Memo{ + Type: xdr.MemoTypeMemoText, + Text: &hardCodedMemoText, + }, + Fee: 100, + Cond: xdr.Preconditions{ + Type: xdr.PreconditionTypePrecondV2, + V2: &xdr.PreconditionsV2{ + TimeBounds: &xdr.TimeBounds{ + MinTime: 0, + MaxTime: 1594272628, + }, + LedgerBounds: &xdr.LedgerBounds{ + MinLedger: 5, + MaxLedger: 10, + }, + ExtraSigners: []xdr.SignerKey{signerKey}, + }, + }, + Operations: []xdr.Operation{ + { + SourceAccount: &testAccount4, + Body: xdr.OperationBody{ + Type: xdr.OperationTypePathPaymentStrictReceive, + PathPaymentStrictReceiveOp: &xdr.PathPaymentStrictReceiveOp{ + Destination: destination, + }, + }, + }, + }, + }, + Signatures: []xdr.DecoratedSignature{ + { + Hint: xdr.SignatureHint{99, 66, 175, 143}, + Signature: xdr.Signature{244, 107, 139, 92, 189, 156, 207, 79, 84, 56, 2, 70, 75, 22, 237, 50, 100, 242, 159, 177, 27, 240, 66, 122, 182, 45, 189, 78, 5, 127, 26, 61, 179, 238, 229, 76, 32, 206, 122, 13, 154, 133, 148, 149, 29, 250, 48, 132, 44, 86, 163, 56, 32, 44, 75, 87, 226, 251, 76, 4, 59, 182, 132, 8}, + }, + }, + }, + }, + Result: xdr.TransactionResultPair{ + TransactionHash: hardCodedTransactionHash, + Result: xdr.TransactionResult{ + FeeCharged: 100, + Result: xdr.TransactionResultResult{ + Code: xdr.TransactionResultCodeTxInsufficientBalance, + Results: genericResultResults, + }, + }, + }, + }, + } + historyHeader = []xdr.LedgerHeaderHistoryEntry{ + { + Header: xdr.LedgerHeader{ + LedgerSeq: 30521816, + ScpValue: xdr.StellarValue{CloseTime: 1594272522}, + }, + }, + { + Header: xdr.LedgerHeader{ + LedgerSeq: 30521817, + ScpValue: xdr.StellarValue{CloseTime: 1594272522}, + }, + }, + { + Header: xdr.LedgerHeader{ + LedgerSeq: 30521818, + ScpValue: xdr.StellarValue{CloseTime: 1594272522}, + }, + }, + } + return +} diff --git a/ingest/processors/trustline.go b/ingest/processors/trustline.go new file mode 100644 index 0000000000..1accfe057b --- /dev/null +++ b/ingest/processors/trustline.go @@ -0,0 +1,109 @@ +package processors + +import ( + "encoding/base64" + "fmt" + + "github.com/guregu/null" + "github.com/pkg/errors" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +// TransformTrustline converts a trustline from the history archive ingestion system into a form suitable for BigQuery +func TransformTrustline(ledgerChange ingest.Change, header xdr.LedgerHeaderHistoryEntry) (TrustlineOutput, error) { + ledgerEntry, changeType, outputDeleted, err := ExtractEntryFromChange(ledgerChange) + if err != nil { + return TrustlineOutput{}, err + } + + trustEntry, ok := ledgerEntry.Data.GetTrustLine() + if !ok { + return TrustlineOutput{}, fmt.Errorf("could not extract trustline data from ledger entry; actual type is %s", ledgerEntry.Data.Type) + } + + outputAccountID, err := trustEntry.AccountId.GetAddress() + if err != nil { + return TrustlineOutput{}, err + } + + var assetType, outputAssetCode, outputAssetIssuer, poolID string + + asset := trustEntry.Asset + + outputLedgerKey, err := trustLineEntryToLedgerKeyString(trustEntry) + if err != nil { + return TrustlineOutput{}, errors.Wrap(err, fmt.Sprintf("could not create ledger key string for trustline with account %s and asset %s", outputAccountID, asset.ToAsset().StringCanonical())) + } + + if asset.Type == xdr.AssetTypeAssetTypePoolShare { + poolID = PoolIDToString(trustEntry.Asset.MustLiquidityPoolId()) + assetType = "pool_share" + } else { + if err = asset.Extract(&assetType, &outputAssetCode, &outputAssetIssuer); err != nil { + return TrustlineOutput{}, errors.Wrap(err, fmt.Sprintf("could not parse asset for trustline with account %s", outputAccountID)) + } + } + + outputAssetID := FarmHashAsset(outputAssetCode, outputAssetIssuer, asset.Type.String()) + + liabilities := trustEntry.Liabilities() + + closedAt, err := TimePointToUTCTimeStamp(header.Header.ScpValue.CloseTime) + if err != nil { + return TrustlineOutput{}, err + } + + ledgerSequence := header.Header.LedgerSeq + + transformedTrustline := TrustlineOutput{ + LedgerKey: outputLedgerKey, + AccountID: outputAccountID, + AssetType: assetType, + AssetCode: outputAssetCode, + AssetIssuer: outputAssetIssuer, + AssetID: outputAssetID, + Balance: ConvertStroopValueToReal(trustEntry.Balance), + TrustlineLimit: int64(trustEntry.Limit), + LiquidityPoolID: poolID, + BuyingLiabilities: ConvertStroopValueToReal(liabilities.Buying), + SellingLiabilities: ConvertStroopValueToReal(liabilities.Selling), + Flags: uint32(trustEntry.Flags), + LastModifiedLedger: uint32(ledgerEntry.LastModifiedLedgerSeq), + LedgerEntryChange: uint32(changeType), + Sponsor: ledgerEntrySponsorToNullString(ledgerEntry), + Deleted: outputDeleted, + ClosedAt: closedAt, + LedgerSequence: uint32(ledgerSequence), + } + + return transformedTrustline, nil +} + +func trustLineEntryToLedgerKeyString(trustLine xdr.TrustLineEntry) (string, error) { + ledgerKey := &xdr.LedgerKey{} + err := ledgerKey.SetTrustline(trustLine.AccountId, trustLine.Asset) + if err != nil { + return "", fmt.Errorf("error running ledgerKey.SetTrustline when calculating ledger key") + } + + key, err := ledgerKey.MarshalBinary() + if err != nil { + return "", fmt.Errorf("error running MarshalBinaryCompress when calculating ledger key") + } + + return base64.StdEncoding.EncodeToString(key), nil + +} + +func ledgerEntrySponsorToNullString(entry xdr.LedgerEntry) null.String { + sponsoringID := entry.SponsoringID() + + var sponsor null.String + if sponsoringID != nil { + sponsor.SetValid((*sponsoringID).Address()) + } + + return sponsor +} diff --git a/ingest/processors/trustline_test.go b/ingest/processors/trustline_test.go new file mode 100644 index 0000000000..f8d1a06033 --- /dev/null +++ b/ingest/processors/trustline_test.go @@ -0,0 +1,164 @@ +package processors + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformTrustline(t *testing.T) { + type inputStruct struct { + ingest ingest.Change + } + type transformTest struct { + input inputStruct + wantOutput TrustlineOutput + wantErr error + } + + hardCodedInput := makeTrustlineTestInput() + hardCodedOutput := makeTrustlineTestOutput() + + tests := []transformTest{ + { + inputStruct{ + ingest.Change{ + Type: xdr.LedgerEntryTypeOffer, + Pre: nil, + Post: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + }, + }, + }, + }, + TrustlineOutput{}, fmt.Errorf("could not extract trustline data from ledger entry; actual type is LedgerEntryTypeOffer"), + }, + } + + for i := range hardCodedInput { + tests = append(tests, transformTest{ + input: inputStruct{hardCodedInput[i]}, + wantOutput: hardCodedOutput[i], + wantErr: nil, + }) + } + + for _, test := range tests { + header := xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + ScpValue: xdr.StellarValue{ + CloseTime: 1000, + }, + LedgerSeq: 10, + }, + } + actualOutput, actualError := TransformTrustline(test.input.ingest, header) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func makeTrustlineTestInput() []ingest.Change { + assetLedgerEntry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: 24229503, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.TrustLineEntry{ + AccountId: testAccount1ID, + Asset: ethTrustLineAsset, + Balance: 6203000, + Limit: 9000000000000000000, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + Ext: xdr.TrustLineEntryExt{ + V: 1, + V1: &xdr.TrustLineEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 1000, + Selling: 2000, + }, + }, + }, + }, + }, + } + lpLedgerEntry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: 123456789, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &xdr.TrustLineEntry{ + AccountId: testAccount2ID, + Asset: liquidityPoolAsset, + Balance: 5000000, + Limit: 1111111111111111111, + Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), + Ext: xdr.TrustLineEntryExt{ + V: 1, + V1: &xdr.TrustLineEntryV1{ + Liabilities: xdr.Liabilities{ + Buying: 15000, + Selling: 5000, + }, + }, + }, + }, + }, + } + return []ingest.Change{ + { + Type: xdr.LedgerEntryTypeTrustline, + Pre: &xdr.LedgerEntry{}, + Post: &assetLedgerEntry, + }, + { + Type: xdr.LedgerEntryTypeTrustline, + Pre: &xdr.LedgerEntry{}, + Post: &lpLedgerEntry, + }, + } +} + +func makeTrustlineTestOutput() []TrustlineOutput { + return []TrustlineOutput{ + { + LedgerKey: "AAAAAQAAAACI4aa0pXFSj6qfJuIObLw/5zyugLRGYwxb7wFSr3B9eAAAAAFFVEgAAAAAAGfMAIZMO4kWjGqv4Lw0cJ7QIcUFcuL5iGE0IggsIily", + AccountID: testAccount1Address, + AssetType: "credit_alphanum4", + AssetIssuer: testAccount3Address, + AssetCode: "ETH", + AssetID: -2311386320395871674, + Balance: 0.6203, + TrustlineLimit: 9000000000000000000, + Flags: 1, + BuyingLiabilities: 0.0001, + SellingLiabilities: 0.0002, + LastModifiedLedger: 24229503, + LedgerEntryChange: 1, + Deleted: false, + LedgerSequence: 10, + ClosedAt: time.Date(1970, time.January, 1, 0, 16, 40, 0, time.UTC), + }, + { + LedgerKey: "AAAAAQAAAAAcR0GXGO76pFs4y38vJVAanjnLg4emNun7zAx0pHcDGAAAAAMBAwQFBwkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + AccountID: testAccount2Address, + AssetType: "pool_share", + AssetID: -1967220342708457407, + Balance: 0.5, + TrustlineLimit: 1111111111111111111, + LiquidityPoolID: "0103040507090000000000000000000000000000000000000000000000000000", + Flags: 1, + BuyingLiabilities: 0.0015, + SellingLiabilities: 0.0005, + LastModifiedLedger: 123456789, + LedgerEntryChange: 1, + Deleted: false, + LedgerSequence: 10, + ClosedAt: time.Date(1970, time.January, 1, 0, 16, 40, 0, time.UTC), + }, + } +} diff --git a/ingest/processors/ttl.go b/ingest/processors/ttl.go new file mode 100644 index 0000000000..cd2e3086e2 --- /dev/null +++ b/ingest/processors/ttl.go @@ -0,0 +1,48 @@ +package processors + +import ( + "fmt" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +// TransformTtl converts an ttl ledger change entry into a form suitable for BigQuery +func TransformTtl(ledgerChange ingest.Change, header xdr.LedgerHeaderHistoryEntry) (TtlOutput, error) { + ledgerEntry, changeType, outputDeleted, err := ExtractEntryFromChange(ledgerChange) + if err != nil { + return TtlOutput{}, err + } + + ttl, ok := ledgerEntry.Data.GetTtl() + if !ok { + return TtlOutput{}, fmt.Errorf("could not extract ttl from ledger entry; actual type is %s", ledgerEntry.Data.Type) + } + + // LedgerEntryChange must contain a ttl change to be parsed, otherwise skip + if ledgerEntry.Data.Type != xdr.LedgerEntryTypeTtl { + return TtlOutput{}, nil + } + + keyHash := ttl.KeyHash.HexString() + liveUntilLedgerSeq := ttl.LiveUntilLedgerSeq + + closedAt, err := TimePointToUTCTimeStamp(header.Header.ScpValue.CloseTime) + if err != nil { + return TtlOutput{}, err + } + + ledgerSequence := header.Header.LedgerSeq + + transformedPool := TtlOutput{ + KeyHash: keyHash, + LiveUntilLedgerSeq: uint32(liveUntilLedgerSeq), + LastModifiedLedger: uint32(ledgerEntry.LastModifiedLedgerSeq), + LedgerEntryChange: uint32(changeType), + Deleted: outputDeleted, + ClosedAt: closedAt, + LedgerSequence: uint32(ledgerSequence), + } + + return transformedPool, nil +} diff --git a/ingest/processors/ttl_test.go b/ingest/processors/ttl_test.go new file mode 100644 index 0000000000..8696a53270 --- /dev/null +++ b/ingest/processors/ttl_test.go @@ -0,0 +1,107 @@ +package processors + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestTransformTtl(t *testing.T) { + type transformTest struct { + input ingest.Change + wantOutput TtlOutput + wantErr error + } + + hardCodedInput := makeTtlTestInput() + hardCodedOutput := makeTtlTestOutput() + tests := []transformTest{ + { + ingest.Change{ + Type: xdr.LedgerEntryTypeOffer, + Pre: nil, + Post: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + }, + }, + }, + TtlOutput{}, fmt.Errorf("could not extract ttl from ledger entry; actual type is LedgerEntryTypeOffer"), + }, + } + + for i := range hardCodedInput { + tests = append(tests, transformTest{ + input: hardCodedInput[i], + wantOutput: hardCodedOutput[i], + wantErr: nil, + }) + } + + for _, test := range tests { + header := xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + ScpValue: xdr.StellarValue{ + CloseTime: 1000, + }, + LedgerSeq: 10, + }, + } + actualOutput, actualError := TransformTtl(test.input, header) + assert.Equal(t, test.wantErr, actualError) + assert.Equal(t, test.wantOutput, actualOutput) + } +} + +func makeTtlTestInput() []ingest.Change { + var hash xdr.Hash + + preTtlLedgerEntry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: 0, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: hash, + LiveUntilLedgerSeq: 0, + }, + }, + } + + TtlLedgerEntry := xdr.LedgerEntry{ + LastModifiedLedgerSeq: 1, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: hash, + LiveUntilLedgerSeq: 123, + }, + }, + } + + return []ingest.Change{ + { + Type: xdr.LedgerEntryTypeTtl, + Pre: &preTtlLedgerEntry, + Post: &TtlLedgerEntry, + }, + } +} + +func makeTtlTestOutput() []TtlOutput { + return []TtlOutput{ + { + KeyHash: "0000000000000000000000000000000000000000000000000000000000000000", + LiveUntilLedgerSeq: 123, + LastModifiedLedger: 1, + LedgerEntryChange: 1, + Deleted: false, + LedgerSequence: 10, + ClosedAt: time.Date(1970, time.January, 1, 0, 16, 40, 0, time.UTC), + }, + } +} diff --git a/ingest/processors/utils.go b/ingest/processors/utils.go new file mode 100644 index 0000000000..a133e3f56a --- /dev/null +++ b/ingest/processors/utils.go @@ -0,0 +1,232 @@ +package processors + +import ( + "encoding/hex" + "fmt" + "math/big" + "time" + + "github.com/stellar/go/hash" + "github.com/stellar/go/ingest" + "github.com/stellar/go/keypair" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" +) + +// ExtractEntryFromChange gets the most recent state of an entry from an ingestion change, as well as if the entry was deleted +func ExtractEntryFromChange(change ingest.Change) (xdr.LedgerEntry, xdr.LedgerEntryChangeType, bool, error) { + switch changeType := change.LedgerEntryChangeType(); changeType { + case xdr.LedgerEntryChangeTypeLedgerEntryCreated, xdr.LedgerEntryChangeTypeLedgerEntryUpdated: + return *change.Post, changeType, false, nil + case xdr.LedgerEntryChangeTypeLedgerEntryRemoved: + return *change.Pre, changeType, true, nil + default: + return xdr.LedgerEntry{}, changeType, false, fmt.Errorf("unable to extract ledger entry type from change") + } +} + +// TimePointToUTCTimeStamp takes in an xdr TimePoint and converts it to a time.Time struct in UTC. It returns an error for negative timepoints +func TimePointToUTCTimeStamp(providedTime xdr.TimePoint) (time.Time, error) { + intTime := int64(providedTime) + if intTime < 0 { + return time.Now(), fmt.Errorf("the timepoint is negative") + } + return time.Unix(intTime, 0).UTC(), nil +} + +func CreateSampleTxMeta(subOperationCount int, AssetA, AssetB xdr.Asset) *xdr.TransactionMetaV1 { + operationMeta := []xdr.OperationMeta{} + for i := 0; i < subOperationCount; i++ { + operationMeta = append(operationMeta, xdr.OperationMeta{ + Changes: xdr.LedgerEntryChanges{}, + }) + } + + operationMeta = AddLPOperations(operationMeta, AssetA, AssetB) + operationMeta = AddLPOperations(operationMeta, AssetA, AssetB) + + operationMeta = append(operationMeta, xdr.OperationMeta{ + Changes: xdr.LedgerEntryChanges{}, + }) + + return &xdr.TransactionMetaV1{ + Operations: operationMeta, + } +} + +func AddLPOperations(txMeta []xdr.OperationMeta, AssetA, AssetB xdr.Asset) []xdr.OperationMeta { + txMeta = append(txMeta, xdr.OperationMeta{ + Changes: xdr.LedgerEntryChanges{ + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryState, + State: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeLiquidityPool, + LiquidityPool: &xdr.LiquidityPoolEntry{ + LiquidityPoolId: xdr.PoolId{1, 2, 3, 4, 5, 6, 7, 8, 9}, + Body: xdr.LiquidityPoolEntryBody{ + Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, + ConstantProduct: &xdr.LiquidityPoolEntryConstantProduct{ + Params: xdr.LiquidityPoolConstantProductParameters{ + AssetA: AssetA, + AssetB: AssetB, + Fee: 30, + }, + ReserveA: 100000, + ReserveB: 1000, + TotalPoolShares: 500, + PoolSharesTrustLineCount: 25, + }, + }, + }, + }, + }, + }, + xdr.LedgerEntryChange{ + Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, + Updated: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeLiquidityPool, + LiquidityPool: &xdr.LiquidityPoolEntry{ + LiquidityPoolId: xdr.PoolId{1, 2, 3, 4, 5, 6, 7, 8, 9}, + Body: xdr.LiquidityPoolEntryBody{ + Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, + ConstantProduct: &xdr.LiquidityPoolEntryConstantProduct{ + Params: xdr.LiquidityPoolConstantProductParameters{ + AssetA: AssetA, + AssetB: AssetB, + Fee: 30, + }, + ReserveA: 101000, + ReserveB: 1100, + TotalPoolShares: 502, + PoolSharesTrustLineCount: 26, + }, + }, + }, + }, + }, + }, + }}) + + return txMeta +} + +// CreateSampleResultMeta creates Transaction results with the desired success flag and number of sub operation results +func CreateSampleResultMeta(successful bool, subOperationCount int) xdr.TransactionResultMeta { + resultCode := xdr.TransactionResultCodeTxFailed + if successful { + resultCode = xdr.TransactionResultCodeTxSuccess + } + operationResults := []xdr.OperationResult{} + operationResultTr := &xdr.OperationResultTr{ + Type: xdr.OperationTypeCreateAccount, + CreateAccountResult: &xdr.CreateAccountResult{ + Code: 0, + }, + } + + for i := 0; i < subOperationCount; i++ { + operationResults = append(operationResults, xdr.OperationResult{ + Code: xdr.OperationResultCodeOpInner, + Tr: operationResultTr, + }) + } + + return xdr.TransactionResultMeta{ + Result: xdr.TransactionResultPair{ + Result: xdr.TransactionResult{ + Result: xdr.TransactionResultResult{ + Code: resultCode, + Results: &operationResults, + }, + }, + }, + } +} + +// ConvertStroopValueToReal converts a value in stroops, the smallest amount unit, into real units +func ConvertStroopValueToReal(input xdr.Int64) float64 { + output, _ := big.NewRat(int64(input), int64(10000000)).Float64() + return output +} + +func GetCloseTime(lcm xdr.LedgerCloseMeta) (time.Time, error) { + headerHistoryEntry := lcm.LedgerHeaderHistoryEntry() + return ExtractLedgerCloseTime(headerHistoryEntry) +} + +func GetLedgerSequence(lcm xdr.LedgerCloseMeta) uint32 { + headerHistoryEntry := lcm.LedgerHeaderHistoryEntry() + return uint32(headerHistoryEntry.Header.LedgerSeq) +} + +// ExtractLedgerCloseTime gets the close time of the provided ledger +func ExtractLedgerCloseTime(ledger xdr.LedgerHeaderHistoryEntry) (time.Time, error) { + return TimePointToUTCTimeStamp(ledger.Header.ScpValue.CloseTime) +} + +func LedgerEntryToLedgerKeyHash(ledgerEntry xdr.LedgerEntry) string { + ledgerKey, _ := ledgerEntry.LedgerKey() + ledgerKeyByte, _ := ledgerKey.MarshalBinary() + hashedLedgerKeyByte := hash.Hash(ledgerKeyByte) + ledgerKeyHash := hex.EncodeToString(hashedLedgerKeyByte[:]) + + return ledgerKeyHash +} + +// HashToHexString is utility function that converts and xdr.Hash type to a hex string +func HashToHexString(inputHash xdr.Hash) string { + sliceHash := inputHash[:] + hexString := hex.EncodeToString(sliceHash) + return hexString +} + +func CreateSampleTx(sequence int64, operationCount int) xdr.TransactionEnvelope { + kp, err := keypair.Random() + PanicOnError(err) + + operations := []txnbuild.Operation{} + operationType := &txnbuild.BumpSequence{ + BumpTo: 0, + } + for i := 0; i < operationCount; i++ { + operations = append(operations, operationType) + } + + sourceAccount := txnbuild.NewSimpleAccount(kp.Address(), int64(0)) + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: &sourceAccount, + Operations: operations, + BaseFee: txnbuild.MinBaseFee, + Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewInfiniteTimeout()}, + }, + ) + PanicOnError(err) + + env := tx.ToXDR() + return env +} + +// PanicOnError is a function that panics if the provided error is not nil +func PanicOnError(err error) { + if err != nil { + panic(err) + } +} + +func LedgerKeyToLedgerKeyHash(ledgerKey xdr.LedgerKey) string { + ledgerKeyByte, _ := ledgerKey.MarshalBinary() + hashedLedgerKeyByte := hash.Hash(ledgerKeyByte) + ledgerKeyHash := hex.EncodeToString(hashedLedgerKeyByte[:]) + + return ledgerKeyHash +} + +// GetAccountAddressFromMuxedAccount takes in a muxed account and returns the address of the account +func GetAccountAddressFromMuxedAccount(account xdr.MuxedAccount) (string, error) { + providedID := account.ToAccountId() + pointerToID := &providedID + return pointerToID.GetAddress() +} diff --git a/toid/main.go b/toid/main.go index 77616ab688..e910281759 100644 --- a/toid/main.go +++ b/toid/main.go @@ -1,7 +1,6 @@ package toid import ( - "errors" "fmt" ) @@ -92,11 +91,11 @@ func AfterLedger(seq int32) *ID { // this value make sure < order is used. func LedgerRangeInclusive(from, to int32) (int64, int64, error) { if from > to { - return 0, 0, errors.New("Invalid range: from > to") + return 0, 0, fmt.Errorf("invalid range: from > to") } if from <= 0 || to <= 0 { - return 0, 0, errors.New("Invalid range: from or to negative") + return 0, 0, fmt.Errorf("invalid range: from or to negative") } var toidFrom, toidTo int64 diff --git a/toid/synt_offer_id.go b/toid/synt_offer_id.go new file mode 100644 index 0000000000..3977710984 --- /dev/null +++ b/toid/synt_offer_id.go @@ -0,0 +1,42 @@ +package toid + +type OfferIDType uint64 + +const ( + CoreOfferIDType OfferIDType = 0 + TOIDType OfferIDType = 1 + + mask uint64 = 0xC000000000000000 +) + +// Taken from https://github.com/stellar/go/tree/master/services/horizon/internal/db2/history + +// EncodeOfferId creates synthetic offer ids to be used by trade resources +// +// This is required because stellar-core does not allocate offer ids for immediately filled offers, +// while clients expect them for aggregated views. +// +// The encoded value is of type int64 for sql compatibility. The 2nd bit is used to differentiate between stellar-core +// offer ids and operation ids, which are toids. +// +// Due to the 2nd bit being used, the largest possible toid is: +// 0011111111111111111111111111111100000000000000000001000000000001 +// \ ledger /\ transaction /\ op / +// +// = 1073741823 +// with avg. 5 sec close time will reach in ~170 years +func EncodeOfferId(id uint64, typ OfferIDType) int64 { + // First ensure the bits we're going to change are 0s + if id&mask != 0 { + panic("Value too big to encode") + } + return int64(id | uint64(typ)<<62) +} + +// DecodeOfferID performs the reverse operation of EncodeOfferID +func DecodeOfferID(encodedId int64) (uint64, OfferIDType) { + if encodedId < 0 { + panic("Negative offer ids can not be decoded") + } + return uint64(encodedId<<2) >> 2, OfferIDType(encodedId >> 62) +}