diff --git a/exp/xdrill/ledger/ledger.go b/exp/xdrill/ledger/ledger.go new file mode 100644 index 0000000000..fc749eee42 --- /dev/null +++ b/exp/xdrill/ledger/ledger.go @@ -0,0 +1,193 @@ +package ledger + +import ( + "encoding/base64" + "fmt" + "time" + + "github.com/stellar/go/exp/xdrill/utils" + "github.com/stellar/go/xdr" +) + +type Ledger struct { + xdr.LedgerCloseMeta +} + +func (l Ledger) Sequence() uint32 { + return uint32(l.LedgerHeaderHistoryEntry().Header.LedgerSeq) +} + +func (l Ledger) ID() int64 { + return utils.NewID(int32(l.LedgerSequence()), 0, 0).ToInt64() +} + +func (l Ledger) Hash() string { + return utils.HashToHexString(l.LedgerHeaderHistoryEntry().Hash) +} + +func (l Ledger) PreviousHash() string { + return utils.HashToHexString(l.PreviousLedgerHash()) +} + +func (l Ledger) CloseTime() int64 { + return l.LedgerCloseTime() +} + +func (l Ledger) ClosedAt() time.Time { + return time.Unix(l.CloseTime(), 0).UTC() +} + +func (l Ledger) TotalCoins() int64 { + return int64(l.LedgerHeaderHistoryEntry().Header.TotalCoins) +} + +func (l Ledger) FeePool() int64 { + return int64(l.LedgerHeaderHistoryEntry().Header.FeePool) +} + +func (l Ledger) BaseFee() uint32 { + return uint32(l.LedgerHeaderHistoryEntry().Header.BaseFee) +} + +func (l Ledger) BaseReserve() uint32 { + return uint32(l.LedgerHeaderHistoryEntry().Header.BaseReserve) +} + +func (l Ledger) MaxTxSetSize() uint32 { + return uint32(l.LedgerHeaderHistoryEntry().Header.MaxTxSetSize) +} + +func (l Ledger) LedgerVersion() uint32 { + return uint32(l.LedgerHeaderHistoryEntry().Header.LedgerVersion) +} + +func (l Ledger) SorobanFeeWrite1Kb() (int64, bool) { + lcmV1, ok := l.GetV1() + if ok { + extV1, ok := lcmV1.Ext.GetV1() + if ok { + return int64(extV1.SorobanFeeWrite1Kb), true + } + } + + return 0, false +} + +func (l Ledger) TotalByteSizeOfBucketList() (uint64, bool) { + lcmV1, ok := l.GetV1() + if ok { + return uint64(lcmV1.TotalByteSizeOfBucketList), true + } + + return 0, false +} + +func (l Ledger) NodeID() (string, bool) { + LedgerCloseValueSignature, ok := l.LedgerHeaderHistoryEntry().Header.ScpValue.Ext.GetLcValueSignature() + if ok { + nodeID, ok := utils.GetAddress(LedgerCloseValueSignature.NodeId) + if ok { + return nodeID, true + } + } + + return "", false +} + +func (l Ledger) Signature() (string, bool) { + LedgerCloseValueSignature, ok := l.LedgerHeaderHistoryEntry().Header.ScpValue.Ext.GetLcValueSignature() + if ok { + return base64.StdEncoding.EncodeToString(LedgerCloseValueSignature.Signature), true + } + + return "", false +} + +// Add docstring to larger, more complicated functions +func (l Ledger) TransactionCounts() (successTxCount, failedTxCount int32, ok bool) { + transactions := getTransactionSet(l) + results := l.V0.TxProcessing + txCount := len(transactions) + if txCount != len(results) { + return 0, 0, false + } + + for i := 0; i < txCount; i++ { + if results[i].Result.Successful() { + successTxCount++ + } else { + failedTxCount++ + } + } + + return successTxCount, failedTxCount, true +} + +// Add docstring to larger, more complicated functions +func (l Ledger) OperationCounts() (operationCount, txSetOperationCount int32, ok bool) { + transactions := getTransactionSet(l) + results := l.V0.TxProcessing + txCount := len(transactions) + if txCount != len(results) { + return 0, 0, false + } + + for i := 0; i < txCount; i++ { + operations := transactions[i].Operations() + numberOfOps := int32(len(operations)) + txSetOperationCount += 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 { + return 0, 0, false + } + + operationCount += int32(len(operationResults)) + } + + } + + return operationCount, txSetOperationCount, true +} + +func getTransactionSet(l Ledger) (transactionProcessing []xdr.TransactionEnvelope) { + switch l.V { + case 0: + return l.V0.TxSet.Txs + case 1: + switch l.V1.TxSet.V { + case 0: + return getTransactionPhase(l.V1.TxSet.V1TxSet.Phases) + default: + panic(fmt.Sprintf("unsupported LedgerCloseMeta.V1.TxSet.V: %d", l.V1.TxSet.V)) + } + default: + panic(fmt.Sprintf("unsupported LedgerCloseMeta.V: %d", l.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 +} diff --git a/exp/xdrill/ledgerentry/ledger_entry.go b/exp/xdrill/ledgerentry/ledger_entry.go new file mode 100644 index 0000000000..fac64cf944 --- /dev/null +++ b/exp/xdrill/ledgerentry/ledger_entry.go @@ -0,0 +1,3 @@ +package ledgerentry + +// TODO: create low level helper functions diff --git a/exp/xdrill/operation/operation.go b/exp/xdrill/operation/operation.go new file mode 100644 index 0000000000..b6afd5e7e2 --- /dev/null +++ b/exp/xdrill/operation/operation.go @@ -0,0 +1,3 @@ +package operation + +// TODO: create low level helper functions diff --git a/exp/xdrill/transaction/transaction.go b/exp/xdrill/transaction/transaction.go new file mode 100644 index 0000000000..afd7b60215 --- /dev/null +++ b/exp/xdrill/transaction/transaction.go @@ -0,0 +1,12 @@ +package transaction + +import ( + "github.com/stellar/go/ingest" +) + +type Transaction struct { + // Use ingest.LedgerTransaction to be used with TransactionReader + ingest.LedgerTransaction +} + +// TODO: create low level helper functions diff --git a/exp/xdrill/transform_ledger.go b/exp/xdrill/transform_ledger.go new file mode 100644 index 0000000000..fc78b911e6 --- /dev/null +++ b/exp/xdrill/transform_ledger.go @@ -0,0 +1,106 @@ +// Note: This is placed in the xdrill directory/package just for this example +// Processors may be placed in a different location/package; To be discussed +package xdrill + +import ( + "fmt" + "time" + + "github.com/stellar/go/exp/xdrill/ledger" + "github.com/stellar/go/xdr" +) + +type LedgerClosedOutput 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"` +} + +func TransformLedger(lcm xdr.LedgerCloseMeta) (LedgerClosedOutput, error) { + ledger := ledger.Ledger{ + LedgerCloseMeta: lcm, + } + + outputLedgerHeader, err := xdr.MarshalBase64(ledger.LedgerHeaderHistoryEntry().Header) + if err != nil { + return LedgerClosedOutput{}, err + } + + outputSuccessfulTransactionCount, outputFailedTransactionCount, ok := ledger.TransactionCounts() + if !ok { + return LedgerClosedOutput{}, fmt.Errorf("could not get transaction counts") + } + + outputOperationCount, outputTxSetOperationCount, ok := ledger.OperationCounts() + if !ok { + return LedgerClosedOutput{}, fmt.Errorf("could not get operation counts") + } + + var outputSorobanFeeWrite1Kb int64 + sorobanFeeWrite1Kb, ok := ledger.SorobanFeeWrite1Kb() + if ok { + outputSorobanFeeWrite1Kb = sorobanFeeWrite1Kb + } + + var outputTotalByteSizeOfBucketList uint64 + totalByteSizeOfBucketList, ok := ledger.TotalByteSizeOfBucketList() + if ok { + outputTotalByteSizeOfBucketList = totalByteSizeOfBucketList + } + + var outputNodeID string + nodeID, ok := ledger.NodeID() + if ok { + outputNodeID = nodeID + } + + var outputSigature string + signature, ok := ledger.Signature() + if ok { + outputSigature = signature + } + + ledgerOutput := LedgerClosedOutput{ + Sequence: ledger.LedgerSequence(), + LedgerHash: ledger.Hash(), + PreviousLedgerHash: ledger.Hash(), + LedgerHeader: outputLedgerHeader, + TransactionCount: outputSuccessfulTransactionCount, + OperationCount: outputOperationCount, + SuccessfulTransactionCount: outputSuccessfulTransactionCount, + FailedTransactionCount: outputFailedTransactionCount, + TxSetOperationCount: string(outputTxSetOperationCount), + ClosedAt: ledger.ClosedAt(), + TotalCoins: ledger.TotalCoins(), + FeePool: ledger.FeePool(), + BaseFee: ledger.BaseFee(), + BaseReserve: ledger.BaseReserve(), + MaxTxSetSize: ledger.MaxTxSetSize(), + ProtocolVersion: ledger.LedgerVersion(), + LedgerID: ledger.ID(), + SorobanFeeWrite1Kb: outputSorobanFeeWrite1Kb, + NodeID: outputNodeID, + Signature: outputSigature, + TotalByteSizeOfBucketList: outputTotalByteSizeOfBucketList, + } + + return ledgerOutput, nil +} diff --git a/exp/xdrill/transform_ledger_xdr.go b/exp/xdrill/transform_ledger_xdr.go new file mode 100644 index 0000000000..dcfaa82808 --- /dev/null +++ b/exp/xdrill/transform_ledger_xdr.go @@ -0,0 +1,64 @@ +// Note: This is placed in the xdrill directory/package just for this example +// Processors may be placed in a different location/package; To be discussed +package xdrill + +import ( + "github.com/stellar/go/xdr" +) + +func TransformLedgerXDR(lcm xdr.LedgerCloseMeta) (LedgerClosedOutput, error) { + outputLedgerHeader, err := xdr.MarshalBase64(lcm.LedgerHeaderHistoryEntry().Header) + if err != nil { + return LedgerClosedOutput{}, err + } + + var outputSorobanFeeWrite1Kb int64 + sorobanFeeWrite1Kb, ok := lcm.SorobanFeeWrite1Kb() + if ok { + outputSorobanFeeWrite1Kb = sorobanFeeWrite1Kb + } + + var outputTotalByteSizeOfBucketList uint64 + totalByteSizeOfBucketList, ok := lcm.TotalByteSizeOfBucketList() + if ok { + outputTotalByteSizeOfBucketList = totalByteSizeOfBucketList + } + + var outputNodeID string + nodeID, ok := lcm.NodeID() + if ok { + outputNodeID = nodeID + } + + var outputSigature string + signature, ok := lcm.Signature() + if ok { + outputSigature = signature + } + + ledgerOutput := LedgerClosedOutput{ + Sequence: lcm.LedgerSequence(), + LedgerHash: lcm.LedgerHash().String(), + PreviousLedgerHash: lcm.PreviousLedgerHash().String(), + LedgerHeader: outputLedgerHeader, + TransactionCount: int32(lcm.CountTransactions()), + OperationCount: int32(lcm.CountOperations()), + SuccessfulTransactionCount: int32(lcm.CountSuccessfulTransactions()), + FailedTransactionCount: int32(lcm.CountFailedTransactions()), + TxSetOperationCount: string(lcm.CountSuccessfulOperations()), + ClosedAt: lcm.LedgerClosedAt(), + TotalCoins: lcm.TotalCoins(), + FeePool: lcm.FeePool(), + BaseFee: lcm.BaseFee(), + BaseReserve: lcm.BaseReserve(), + MaxTxSetSize: lcm.MaxTxSetSize(), + ProtocolVersion: lcm.ProtocolVersion(), + LedgerID: lcm.LedgerID(), + SorobanFeeWrite1Kb: outputSorobanFeeWrite1Kb, + NodeID: outputNodeID, + Signature: outputSigature, + TotalByteSizeOfBucketList: outputTotalByteSizeOfBucketList, + } + + return ledgerOutput, nil +} diff --git a/exp/xdrill/utils/utils.go b/exp/xdrill/utils/utils.go new file mode 100644 index 0000000000..0bc8d30c3e --- /dev/null +++ b/exp/xdrill/utils/utils.go @@ -0,0 +1,92 @@ +package utils + +import ( + "encoding/hex" + + "github.com/stellar/go/strkey" + "github.com/stellar/go/xdr" +) + +// 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 +} + +type ID struct { + LedgerSequence int32 + TransactionOrder int32 + OperationOrder int32 +} + +const ( + // LedgerMask is the bitmask to mask out ledger sequences in a + // TotalOrderID + LedgerMask = (1 << 32) - 1 + // TransactionMask is the bitmask to mask out transaction indexes + TransactionMask = (1 << 20) - 1 + // OperationMask is the bitmask to mask out operation indexes + OperationMask = (1 << 12) - 1 + + // LedgerShift is the number of bits to shift an int64 to target the + // ledger component + LedgerShift = 32 + // TransactionShift is the number of bits to shift an int64 to + // target the transaction component + TransactionShift = 12 + // OperationShift is the number of bits to shift an int64 to target + // the operation component + OperationShift = 0 +) + +// New creates a new total order ID +func NewID(ledger int32, tx int32, op int32) *ID { + return &ID{ + LedgerSequence: ledger, + TransactionOrder: tx, + OperationOrder: op, + } +} + +// ToInt64 converts this struct back into an int64 +func (id ID) ToInt64() (result int64) { + + if id.LedgerSequence < 0 { + panic("invalid ledger sequence") + } + + if id.TransactionOrder > TransactionMask { + panic("transaction order overflow") + } + + if id.OperationOrder > OperationMask { + panic("operation order overflow") + } + + result = result | ((int64(id.LedgerSequence) & LedgerMask) << LedgerShift) + result = result | ((int64(id.TransactionOrder) & TransactionMask) << TransactionShift) + result = result | ((int64(id.OperationOrder) & OperationMask) << OperationShift) + return +} + +// 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, bool) { + switch nodeID.Type { + case xdr.PublicKeyTypePublicKeyTypeEd25519: + ed, ok := nodeID.GetEd25519() + if !ok { + return "", false + } + raw := make([]byte, 32) + copy(raw, ed[:]) + encodedAddress, err := strkey.Encode(strkey.VersionByteAccountID, raw) + if err != nil { + return "", false + } + return encodedAddress, true + default: + return "", false + } +} diff --git a/go.mod b/go.mod index 531a7ecd3f..e0f73a567e 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,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.0.3+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 13d3a0acf0..830db50d41 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/change.go b/ingest/change.go index 0a2c063f1c..9b39f2f996 100644 --- a/ingest/change.go +++ b/ingest/change.go @@ -252,3 +252,15 @@ func (c Change) AccountChangedExceptSigners() (bool, error) { return !bytes.Equal(preBinary, postBinary), nil } + +// ExtractEntryFromChange gets the most recent state of an entry from an ingestion change, as well as if the entry was deleted +func (c Change) ExtractEntryFromChange() (xdr.LedgerEntry, xdr.LedgerEntryChangeType, bool, error) { + switch changeType := c.LedgerEntryChangeType(); changeType { + case xdr.LedgerEntryChangeTypeLedgerEntryCreated, xdr.LedgerEntryChangeTypeLedgerEntryUpdated: + return *c.Post, changeType, false, nil + case xdr.LedgerEntryChangeTypeLedgerEntryRemoved: + return *c.Pre, changeType, true, nil + default: + return xdr.LedgerEntry{}, changeType, false, fmt.Errorf("unable to extract ledger entry type from change") + } +} diff --git a/ingest/ledger_operation.go b/ingest/ledger_operation.go new file mode 100644 index 0000000000..e1e52e84a8 --- /dev/null +++ b/ingest/ledger_operation.go @@ -0,0 +1,291 @@ +package ingest + +import ( + "fmt" + "time" + + "github.com/dgryski/go-farm" + "github.com/guregu/null" + "github.com/stellar/go/amount" + "github.com/stellar/go/toid" + "github.com/stellar/go/xdr" +) + +type LedgerOperation struct { + OperationIndex int32 + Operation xdr.Operation + Transaction LedgerTransaction + LedgerCloseMeta xdr.LedgerCloseMeta +} + +func (o LedgerOperation) sourceAccountXDR() xdr.MuxedAccount { + sourceAccount := o.Operation.SourceAccount + if sourceAccount != nil { + return *sourceAccount + } + + return o.Transaction.Envelope.SourceAccount() +} + +func (o LedgerOperation) SourceAccount() string { + muxedAccount := o.sourceAccountXDR() + + providedID := muxedAccount.ToAccountId() + pointerToID := &providedID + return pointerToID.Address() +} + +func (o LedgerOperation) Type() int32 { + return int32(o.Operation.Body.Type) +} + +func (o LedgerOperation) TypeString() string { + return xdr.OperationTypeToStringMap[o.Type()] +} + +func (o LedgerOperation) ID() int64 { + //operationIndex needs +1 increment to stay in sync with ingest package + return toid.New(int32(o.LedgerCloseMeta.LedgerSequence()), int32(o.Transaction.Index), o.OperationIndex+1).ToInt64() +} + +func (o LedgerOperation) SourceAccountMuxed() null.String { + var address null.String + muxedAccount := o.sourceAccountXDR() + + if muxedAccount.Type == xdr.CryptoKeyTypeKeyTypeMuxedEd25519 { + return null.StringFrom(muxedAccount.Address()) + } + + return address +} + +func (o LedgerOperation) TransactionID() int64 { + return o.Transaction.TransactionID() +} + +func (o LedgerOperation) LedgerSequence() uint32 { + return o.LedgerCloseMeta.LedgerSequence() +} + +func (o LedgerOperation) LedgerClosedAt() time.Time { + return o.LedgerCloseMeta.LedgerClosedAt() +} + +func (o LedgerOperation) OperationResultCode() string { + var operationResultCode string + operationResults, ok := o.Transaction.Result.Result.OperationResults() + if ok { + operationResultCode = operationResults[o.OperationIndex].Code.String() + } + + return operationResultCode +} + +func (o LedgerOperation) OperationTraceCode() string { + var operationTraceCode string + + operationResults, ok := o.Transaction.Result.Result.OperationResults() + if ok { + operationResultTr, ok := operationResults[o.OperationIndex].GetTr() + if ok { + operationTraceCode, err := operationResultTr.MapOperationResultTr() + if err != nil { + panic(err) + } + return operationTraceCode + } + } + + return operationTraceCode +} + +func (o LedgerOperation) OperationDetails() (map[string]interface{}, error) { + details := map[string]interface{}{} + + switch o.Operation.Body.Type { + case xdr.OperationTypeCreateAccount: + details, err := o.CreateAccountDetails() + if err != nil { + return details, err + } + case xdr.OperationTypePayment: + details, err := o.PaymentDetails() + if err != nil { + return details, err + } + case xdr.OperationTypePathPaymentStrictReceive: + details, err := o.PathPaymentStrictReceiveDetails() + if err != nil { + return details, err + } + // same for all other operations + default: + return details, fmt.Errorf("unknown operation type: %s", o.Operation.Body.Type.String()) + } + + return details, nil +} + +func (o LedgerOperation) CreateAccountDetails() (map[string]interface{}, error) { + details := map[string]interface{}{} + op, ok := o.Operation.Body.GetCreateAccountOp() + if !ok { + return details, fmt.Errorf("could not access CreateAccount info for this operation (index %d)", o.OperationIndex) + } + + if err := addAccountAndMuxedAccountDetails(details, o.sourceAccountXDR(), "funder"); err != nil { + return details, err + } + details["account"] = op.Destination.Address() + details["starting_balance"] = xdr.ConvertStroopValueToReal(op.StartingBalance) + + return details, 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 formatPrefix(p string) string { + if p != "" { + p += "_" + } + return p +} + +func (o LedgerOperation) PaymentDetails() (map[string]interface{}, error) { + details := map[string]interface{}{} + op, ok := o.Operation.Body.GetPaymentOp() + if !ok { + return details, fmt.Errorf("could not access Payment info for this operation (index %d)", o.OperationIndex) + } + + if err := addAccountAndMuxedAccountDetails(details, o.sourceAccountXDR(), "from"); err != nil { + return details, err + } + if err := addAccountAndMuxedAccountDetails(details, op.Destination, "to"); err != nil { + return details, err + } + details["amount"] = xdr.ConvertStroopValueToReal(op.Amount) + if err := addAssetDetailsToOperationDetails(details, op.Asset, ""); err != nil { + return details, err + } + + return details, nil +} + +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 farmHashAsset(assetCode, assetIssuer, assetType string) int64 { + asset := fmt.Sprintf("%s%s%s", assetCode, assetIssuer, assetType) + hash := farm.Fingerprint64([]byte(asset)) + + return int64(hash) +} + +func (o LedgerOperation) PathPaymentStrictReceiveDetails() (map[string]interface{}, error) { + details := map[string]interface{}{} + op, ok := o.Operation.Body.GetPathPaymentStrictReceiveOp() + if !ok { + return details, fmt.Errorf("could not access PathPaymentStrictReceive info for this operation (index %d)", o.OperationIndex) + } + + if err := addAccountAndMuxedAccountDetails(details, o.sourceAccountXDR(), "from"); err != nil { + return details, err + } + if err := addAccountAndMuxedAccountDetails(details, op.Destination, "to"); err != nil { + return details, err + } + details["amount"] = xdr.ConvertStroopValueToReal(op.DestAmount) + details["source_amount"] = amount.String(0) + details["source_max"] = xdr.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 o.Transaction.Result.Successful() { + allOperationResults, ok := o.Transaction.Result.OperationResults() + if !ok { + return details, fmt.Errorf("could not access any results for this transaction") + } + currentOperationResult := allOperationResults[o.OperationIndex] + resultBody, ok := currentOperationResult.GetTr() + if !ok { + return details, fmt.Errorf("could not access result body for this operation (index %d)", o.OperationIndex) + } + result, ok := resultBody.GetPathPaymentStrictReceiveResult() + if !ok { + return details, fmt.Errorf("could not access PathPaymentStrictReceive result info for this operation (index %d)", o.OperationIndex) + } + details["source_amount"] = xdr.ConvertStroopValueToReal(result.SendAmount()) + } + + details["path"] = transformPath(op.Path) + return details, nil +} + +// 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"` +} + +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 +} diff --git a/ingest/ledger_transaction.go b/ingest/ledger_transaction.go index 77ca777206..87a618bd08 100644 --- a/ingest/ledger_transaction.go +++ b/ingest/ledger_transaction.go @@ -2,6 +2,7 @@ package ingest import ( "github.com/stellar/go/support/errors" + "github.com/stellar/go/toid" "github.com/stellar/go/xdr" ) @@ -14,9 +15,10 @@ type LedgerTransaction struct { // you know what you are doing. // Use LedgerTransaction.GetChanges() for higher level access to ledger // entry changes. - FeeChanges xdr.LedgerEntryChanges - UnsafeMeta xdr.TransactionMeta - LedgerVersion uint32 + FeeChanges xdr.LedgerEntryChanges + UnsafeMeta xdr.TransactionMeta + LedgerVersion uint32 + LedgerCloseMeta xdr.LedgerCloseMeta } func (t *LedgerTransaction) txInternalError() bool { @@ -155,3 +157,27 @@ func operationChanges(ops []xdr.OperationMeta, index uint32) []Change { func (t *LedgerTransaction) GetDiagnosticEvents() ([]xdr.DiagnosticEvent, error) { return t.UnsafeMeta.GetDiagnosticEvents() } + +func (t *LedgerTransaction) GetOperations() []LedgerOperation { + var ledgerOperations []LedgerOperation + + for i, operation := range t.Envelope.Operations() { + ledgerOperation := LedgerOperation{ + Operation: operation, + OperationIndex: int32(i), + Transaction: *t, + LedgerCloseMeta: t.LedgerCloseMeta, + } + ledgerOperations = append(ledgerOperations, ledgerOperation) + } + + return ledgerOperations +} + +func (t *LedgerTransaction) TransactionID() int64 { + return toid.New(int32(t.LedgerCloseMeta.LedgerSequence()), int32(t.Index), 0).ToInt64() +} + +func (t *LedgerTransaction) TransactionHash() string { + return t.Result.TransactionHash.HexString() +} diff --git a/xdr/account_entry.go b/xdr/account_entry.go index 0649a71bb9..66955fdce1 100644 --- a/xdr/account_entry.go +++ b/xdr/account_entry.go @@ -113,3 +113,50 @@ func (account *AccountEntry) SeqLedger() Uint32 { } return 0 } + +func (account *AccountEntry) AccountID() string { + return account.AccountId.Address() +} + +func (account *AccountEntry) BalanceFloat() float64 { + return ConvertStroopValueToReal(account.Balance) +} + +func (account *AccountEntry) BuyingLiabilities() float64 { + var buyingLiabilities float64 + accountExtensionInfo, V1Found := account.Ext.GetV1() + if V1Found { + return ConvertStroopValueToReal(accountExtensionInfo.Liabilities.Buying) + } + return buyingLiabilities +} + +func (account *AccountEntry) SellingLiabilities() float64 { + var sellingLiabilities float64 + accountExtensionInfo, V1Found := account.Ext.GetV1() + if V1Found { + return ConvertStroopValueToReal(accountExtensionInfo.Liabilities.Selling) + } + return sellingLiabilities +} + +func (account *AccountEntry) SequenceNumber() int64 { + return int64(account.SeqNum) +} + +func (account *AccountEntry) SequenceLedger() int64 { + return int64(account.SeqLedger()) +} + +func (account *AccountEntry) SequenceTime() int64 { + return int64(account.SeqTime()) +} + +func (account *AccountEntry) InflationDestination() string { + var inflationDest string + inflationDestAccountID := account.InflationDest + if inflationDestAccountID != nil { + return inflationDestAccountID.Address() + } + return inflationDest +} diff --git a/xdr/hash.go b/xdr/hash.go index 2a15c18c9c..86aae508ac 100644 --- a/xdr/hash.go +++ b/xdr/hash.go @@ -17,3 +17,7 @@ func (s Hash) Equals(o Hash) bool { } return true } + +func (h Hash) String() string { + return HashToHexString(h) +} diff --git a/xdr/ledger_close_meta.go b/xdr/ledger_close_meta.go index 30e80b2e38..d9536c3428 100644 --- a/xdr/ledger_close_meta.go +++ b/xdr/ledger_close_meta.go @@ -1,7 +1,9 @@ package xdr import ( + "encoding/base64" "fmt" + "time" ) func (l LedgerCloseMeta) LedgerHeaderHistoryEntry() LedgerHeaderHistoryEntry { @@ -156,3 +158,155 @@ func (l LedgerCloseMeta) EvictedPersistentLedgerEntries() ([]LedgerEntry, error) panic(fmt.Sprintf("Unsupported LedgerCloseMeta.V: %d", l.V)) } } + +func (l LedgerCloseMeta) LedgerID() int64 { + return NewID(int32(l.LedgerSequence()), 0, 0).ToInt64() +} + +func (l LedgerCloseMeta) LedgerClosedAt() time.Time { + return time.Unix(l.LedgerCloseTime(), 0).UTC() +} + +func (l LedgerCloseMeta) TotalCoins() int64 { + return int64(l.LedgerHeaderHistoryEntry().Header.TotalCoins) +} + +func (l LedgerCloseMeta) FeePool() int64 { + return int64(l.LedgerHeaderHistoryEntry().Header.FeePool) +} + +func (l LedgerCloseMeta) BaseFee() uint32 { + return uint32(l.LedgerHeaderHistoryEntry().Header.BaseFee) +} + +func (l LedgerCloseMeta) BaseReserve() uint32 { + return uint32(l.LedgerHeaderHistoryEntry().Header.BaseReserve) +} + +func (l LedgerCloseMeta) MaxTxSetSize() uint32 { + return uint32(l.LedgerHeaderHistoryEntry().Header.MaxTxSetSize) +} + +func (l LedgerCloseMeta) SorobanFeeWrite1Kb() (int64, bool) { + lcmV1, ok := l.GetV1() + if ok { + extV1 := lcmV1.Ext.MustV1() + return int64(extV1.SorobanFeeWrite1Kb), true + } + + return 0, false +} + +func (l LedgerCloseMeta) TotalByteSizeOfBucketList() (uint64, bool) { + lcmV1, ok := l.GetV1() + if ok { + return uint64(lcmV1.TotalByteSizeOfBucketList), true + } + + return 0, false +} + +func (l LedgerCloseMeta) NodeID() (string, bool) { + LedgerCloseValueSignature, ok := l.LedgerHeaderHistoryEntry().Header.ScpValue.Ext.GetLcValueSignature() + if ok { + nodeID, ok := GetAddress(LedgerCloseValueSignature.NodeId) + if ok { + return nodeID, true + } + } + + return "", false +} + +func (l LedgerCloseMeta) Signature() (string, bool) { + LedgerCloseValueSignature, ok := l.LedgerHeaderHistoryEntry().Header.ScpValue.Ext.GetLcValueSignature() + if ok { + return base64.StdEncoding.EncodeToString(LedgerCloseValueSignature.Signature), true + } + + return "", false +} + +func (l LedgerCloseMeta) TransactionProcessing() []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 (l LedgerCloseMeta) CountSuccessfulTransactions() int { + var successfulTransactionCount int + results := l.TransactionProcessing() + + for _, result := range results { + if result.Result.Successful() { + successfulTransactionCount++ + } + } + + return successfulTransactionCount +} + +func (l LedgerCloseMeta) CountFailedTransactions() int { + var failedTransactionCount int + results := l.TransactionProcessing() + + for _, result := range results { + if !result.Result.Successful() { + failedTransactionCount++ + } + } + + return failedTransactionCount +} + +func (l LedgerCloseMeta) CountOperations() (operationCount int) { + transactions := l.TransactionEnvelopes() + + for _, transaction := range transactions { + operations := transaction.Operations() + numberOfOps := len(operations) + operationCount += numberOfOps + } + + return +} + +func (l LedgerCloseMeta) CountSuccessfulOperations() (operationCount int) { + results := l.TransactionProcessing() + + for _, result := range results { + if result.Result.Successful() { + operationResults, ok := result.Result.OperationResults() + if !ok { + panic("could not get []OperationResult") + } + + operationCount += len(operationResults) + } + } + + return +} + +func (l LedgerCloseMeta) CountFailedOperations() (operationCount int) { + results := l.TransactionProcessing() + + for _, result := range results { + if !result.Result.Successful() { + operationResults, ok := result.Result.OperationResults() + if !ok { + panic("could not get []OperationResult") + } + + operationCount += len(operationResults) + } + } + + return +} diff --git a/xdr/operation.go b/xdr/operation.go new file mode 100644 index 0000000000..b4c3463e75 --- /dev/null +++ b/xdr/operation.go @@ -0,0 +1,9 @@ +package xdr + +//func (o Operation) Type() int32 { +// return int32(o.Body.Type) +//} +// +//func (o Operation) TypeString() string { +// return operationTypeMap[o.Type()] +//} diff --git a/xdr/operation_result_trace.go b/xdr/operation_result_trace.go new file mode 100644 index 0000000000..2325005d72 --- /dev/null +++ b/xdr/operation_result_trace.go @@ -0,0 +1,76 @@ +package xdr + +import "fmt" + +//func (o Operation) Type() int32 { +// return int32(o.Body.Type) +//} +// +//func (o Operation) TypeString() string { +// return operationTypeMap[o.Type()] +//} + +func (o OperationResultTr) MapOperationResultTr() (string, error) { + var operationTraceDescription string + operationType := o.Type + + switch operationType { + case OperationTypeCreateAccount: + operationTraceDescription = o.CreateAccountResult.Code.String() + case OperationTypePayment: + operationTraceDescription = o.PaymentResult.Code.String() + case OperationTypePathPaymentStrictReceive: + operationTraceDescription = o.PathPaymentStrictReceiveResult.Code.String() + case OperationTypePathPaymentStrictSend: + operationTraceDescription = o.PathPaymentStrictSendResult.Code.String() + case OperationTypeManageBuyOffer: + operationTraceDescription = o.ManageBuyOfferResult.Code.String() + case OperationTypeManageSellOffer: + operationTraceDescription = o.ManageSellOfferResult.Code.String() + case OperationTypeCreatePassiveSellOffer: + operationTraceDescription = o.CreatePassiveSellOfferResult.Code.String() + case OperationTypeSetOptions: + operationTraceDescription = o.SetOptionsResult.Code.String() + case OperationTypeChangeTrust: + operationTraceDescription = o.ChangeTrustResult.Code.String() + case OperationTypeAllowTrust: + operationTraceDescription = o.AllowTrustResult.Code.String() + case OperationTypeAccountMerge: + operationTraceDescription = o.AccountMergeResult.Code.String() + case OperationTypeInflation: + operationTraceDescription = o.InflationResult.Code.String() + case OperationTypeManageData: + operationTraceDescription = o.ManageDataResult.Code.String() + case OperationTypeBumpSequence: + operationTraceDescription = o.BumpSeqResult.Code.String() + case OperationTypeCreateClaimableBalance: + operationTraceDescription = o.CreateClaimableBalanceResult.Code.String() + case OperationTypeClaimClaimableBalance: + operationTraceDescription = o.ClaimClaimableBalanceResult.Code.String() + case OperationTypeBeginSponsoringFutureReserves: + operationTraceDescription = o.BeginSponsoringFutureReservesResult.Code.String() + case OperationTypeEndSponsoringFutureReserves: + operationTraceDescription = o.EndSponsoringFutureReservesResult.Code.String() + case OperationTypeRevokeSponsorship: + operationTraceDescription = o.RevokeSponsorshipResult.Code.String() + case OperationTypeClawback: + operationTraceDescription = o.ClawbackResult.Code.String() + case OperationTypeClawbackClaimableBalance: + operationTraceDescription = o.ClawbackClaimableBalanceResult.Code.String() + case OperationTypeSetTrustLineFlags: + operationTraceDescription = o.SetTrustLineFlagsResult.Code.String() + case OperationTypeLiquidityPoolDeposit: + operationTraceDescription = o.LiquidityPoolDepositResult.Code.String() + case OperationTypeLiquidityPoolWithdraw: + operationTraceDescription = o.LiquidityPoolWithdrawResult.Code.String() + case OperationTypeInvokeHostFunction: + operationTraceDescription = o.InvokeHostFunctionResult.Code.String() + case OperationTypeExtendFootprintTtl: + operationTraceDescription = o.ExtendFootprintTtlResult.Code.String() + case OperationTypeRestoreFootprint: + operationTraceDescription = o.RestoreFootprintResult.Code.String() + default: + return operationTraceDescription, fmt.Errorf("unknown operation type: %s", o.Type.String()) + } + return operationTraceDescription, nil +} diff --git a/xdr/utils.go b/xdr/utils.go new file mode 100644 index 0000000000..f9643e0c24 --- /dev/null +++ b/xdr/utils.go @@ -0,0 +1,97 @@ +package xdr + +import ( + "encoding/hex" + "math/big" + + "github.com/stellar/go/strkey" +) + +// HashToHexString is utility function that converts and xdr.Hash type to a hex string +func HashToHexString(inputHash Hash) string { + sliceHash := inputHash[:] + hexString := hex.EncodeToString(sliceHash) + return hexString +} + +type ID struct { + LedgerSequence int32 + TransactionOrder int32 + OperationOrder int32 +} + +const ( + // LedgerMask is the bitmask to mask out ledger sequences in a + // TotalOrderID + LedgerMask = (1 << 32) - 1 + // TransactionMask is the bitmask to mask out transaction indexes + TransactionMask = (1 << 20) - 1 + // OperationMask is the bitmask to mask out operation indexes + OperationMask = (1 << 12) - 1 + + // LedgerShift is the number of bits to shift an int64 to target the + // ledger component + LedgerShift = 32 + // TransactionShift is the number of bits to shift an int64 to + // target the transaction component + TransactionShift = 12 + // OperationShift is the number of bits to shift an int64 to target + // the operation component + OperationShift = 0 +) + +// New creates a new total order ID +func NewID(ledger int32, tx int32, op int32) *ID { + return &ID{ + LedgerSequence: ledger, + TransactionOrder: tx, + OperationOrder: op, + } +} + +// ToInt64 converts this struct back into an int64 +func (id ID) ToInt64() (result int64) { + + if id.LedgerSequence < 0 { + panic("invalid ledger sequence") + } + + if id.TransactionOrder > TransactionMask { + panic("transaction order overflow") + } + + if id.OperationOrder > OperationMask { + panic("operation order overflow") + } + + result = result | ((int64(id.LedgerSequence) & LedgerMask) << LedgerShift) + result = result | ((int64(id.TransactionOrder) & TransactionMask) << TransactionShift) + result = result | ((int64(id.OperationOrder) & OperationMask) << OperationShift) + return +} + +// 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 NodeId) (string, bool) { + switch nodeID.Type { + case PublicKeyTypePublicKeyTypeEd25519: + ed, ok := nodeID.GetEd25519() + if !ok { + return "", false + } + raw := make([]byte, 32) + copy(raw, ed[:]) + encodedAddress, err := strkey.Encode(strkey.VersionByteAccountID, raw) + if err != nil { + return "", false + } + return encodedAddress, true + default: + return "", false + } +} + +func ConvertStroopValueToReal(input Int64) float64 { + output, _ := big.NewRat(int64(input), int64(10000000)).Float64() + return output +}