diff --git a/ingest/ledger/ledger.go b/ingest/ledger/ledger.go new file mode 100644 index 0000000000..3a6e0e966e --- /dev/null +++ b/ingest/ledger/ledger.go @@ -0,0 +1,143 @@ +package ledger + +import ( + "encoding/base64" + "fmt" + "time" + + "github.com/stellar/go/xdr" +) + +func Sequence(l xdr.LedgerCloseMeta) uint32 { + return uint32(l.LedgerHeaderHistoryEntry().Header.LedgerSeq) +} + +func Hash(l xdr.LedgerCloseMeta) string { + return l.LedgerHeaderHistoryEntry().Hash.HexString() +} + +func PreviousHash(l xdr.LedgerCloseMeta) string { + return l.PreviousLedgerHash().HexString() +} + +func CloseTime(l xdr.LedgerCloseMeta) int64 { + return l.LedgerCloseTime() +} + +func ClosedAt(l xdr.LedgerCloseMeta) time.Time { + return time.Unix(l.LedgerCloseTime(), 0).UTC() +} + +func TotalCoins(l xdr.LedgerCloseMeta) int64 { + return int64(l.LedgerHeaderHistoryEntry().Header.TotalCoins) +} + +func FeePool(l xdr.LedgerCloseMeta) int64 { + return int64(l.LedgerHeaderHistoryEntry().Header.FeePool) +} + +func BaseFee(l xdr.LedgerCloseMeta) uint32 { + return uint32(l.LedgerHeaderHistoryEntry().Header.BaseFee) +} + +func BaseReserve(l xdr.LedgerCloseMeta) uint32 { + return uint32(l.LedgerHeaderHistoryEntry().Header.BaseReserve) +} + +func MaxTxSetSize(l xdr.LedgerCloseMeta) uint32 { + return uint32(l.LedgerHeaderHistoryEntry().Header.MaxTxSetSize) +} + +func LedgerVersion(l xdr.LedgerCloseMeta) uint32 { + return uint32(l.LedgerHeaderHistoryEntry().Header.LedgerVersion) +} + +func SorobanFeeWrite1Kb(l xdr.LedgerCloseMeta) (int64, bool) { + lcmV1, ok := l.GetV1() + if !ok { + return 0, false + } + + extV1, ok := lcmV1.Ext.GetV1() + if !ok { + return 0, false + } + + return int64(extV1.SorobanFeeWrite1Kb), true +} + +func TotalByteSizeOfBucketList(l xdr.LedgerCloseMeta) (uint64, bool) { + lcmV1, ok := l.GetV1() + if !ok { + return 0, false + } + + return uint64(lcmV1.TotalByteSizeOfBucketList), true +} + +func NodeID(l xdr.LedgerCloseMeta) (string, error) { + LedgerCloseValueSignature, ok := l.LedgerHeaderHistoryEntry().Header.ScpValue.Ext.GetLcValueSignature() + if !ok { + return "", fmt.Errorf("could not get LedgerCloseValueSignature") + + } + return LedgerCloseValueSignature.NodeId.GetAddress() +} + +func Signature(l xdr.LedgerCloseMeta) (string, bool) { + LedgerCloseValueSignature, ok := l.LedgerHeaderHistoryEntry().Header.ScpValue.Ext.GetLcValueSignature() + if !ok { + return "", false + } + + return base64.StdEncoding.EncodeToString(LedgerCloseValueSignature.Signature), true +} + +// TransactionCounts calculates and returns the number of successful and total transactions +func TransactionCounts(l xdr.LedgerCloseMeta) (successTxCount, totalTxCount uint32) { + transactions := l.TransactionEnvelopes() + results, err := l.TxProcessing() + if err != nil { + panic(err) + } + + txCount := len(transactions) + if txCount != len(results) { + panic("transaction count and number of TransactionResultMeta not equal") + } + + for i := 0; i < txCount; i++ { + if results[i].Result.Successful() { + successTxCount++ + } + } + + return successTxCount, uint32(txCount) +} + +// OperationCounts calculates and returns the number of successful operations and the total operations within +// a LedgerCloseMeta +func OperationCounts(l xdr.LedgerCloseMeta) (successfulOperationCount, totalOperationCount uint32) { + transactions := l.TransactionEnvelopes() + results, err := l.TxProcessing() + if err != nil { + panic(err) + } + + for i, result := range results { + operations := transactions[i].OperationsCount() + totalOperationCount += operations + + // for successful transactions, the operation count is based on the operations results slice + if result.Result.Successful() { + operationResults, ok := result.Result.OperationResults() + if !ok { + panic("could not get OperationResults") + } + + successfulOperationCount += uint32(len(operationResults)) + } + } + + return successfulOperationCount, totalOperationCount +} diff --git a/ingest/ledger/ledger_test.go b/ingest/ledger/ledger_test.go new file mode 100644 index 0000000000..70c2c75d01 --- /dev/null +++ b/ingest/ledger/ledger_test.go @@ -0,0 +1,199 @@ +package ledger + +import ( + "testing" + "time" + + "github.com/stellar/go/keypair" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" +) + +func TestLedger(t *testing.T) { + ledger := ledgerTestInput() + + assert.Equal(t, uint32(30578981), Sequence(ledger)) + assert.Equal(t, "26932dc4d84b5fabe9ae744cb43ce4c6daccf98c86a991b2a14945b1adac4d59", Hash(ledger)) + assert.Equal(t, "f63c15d0eaf48afbd751a4c4dfade54a3448053c47c5a71d622668ae0cc2a208", PreviousHash(ledger)) + assert.Equal(t, int64(1594584547), CloseTime(ledger)) + assert.Equal(t, time.Time(time.Date(2020, time.July, 12, 20, 9, 7, 0, time.UTC)), ClosedAt(ledger)) + assert.Equal(t, int64(1054439020873472865), TotalCoins(ledger)) + assert.Equal(t, int64(18153766209161), FeePool(ledger)) + assert.Equal(t, uint32(100), BaseFee(ledger)) + assert.Equal(t, uint32(5000000), BaseReserve(ledger)) + assert.Equal(t, uint32(1000), MaxTxSetSize(ledger)) + assert.Equal(t, uint32(13), LedgerVersion(ledger)) + + var ok bool + var freeWrite int64 + freeWrite, ok = SorobanFeeWrite1Kb(ledger) + assert.Equal(t, true, ok) + assert.Equal(t, int64(12), freeWrite) + + var bucketSize uint64 + bucketSize, ok = TotalByteSizeOfBucketList(ledger) + assert.Equal(t, true, ok) + assert.Equal(t, uint64(56), bucketSize) + + var nodeID string + var err error + nodeID, err = NodeID(ledger) + assert.Equal(t, nil, err) + assert.Equal(t, "GARAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA76O", nodeID) + + var signature string + signature, ok = Signature(ledger) + assert.Equal(t, true, ok) + assert.Equal(t, "9g==", signature) + + var success uint32 + var total uint32 + success, total = TransactionCounts(ledger) + assert.Equal(t, uint32(1), success) + assert.Equal(t, uint32(2), total) + + success, total = OperationCounts(ledger) + assert.Equal(t, uint32(1), success) + assert.Equal(t, uint32(13), total) +} + +func ledgerTestInput() (lcm xdr.LedgerCloseMeta) { + lcm = xdr.LedgerCloseMeta{ + V: 1, + V1: &xdr.LedgerCloseMetaV1{ + Ext: xdr.LedgerCloseMetaExt{ + V: 1, + V1: &xdr.LedgerCloseMetaExtV1{ + SorobanFeeWrite1Kb: xdr.Int64(12), + }, + }, + LedgerHeader: xdr.LedgerHeaderHistoryEntry{ + 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}, + 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{ + Ext: xdr.StellarValueExt{ + V: 1, + LcValueSignature: &xdr.LedgerCloseValueSignature{ + NodeId: xdr.NodeId{ + Type: 0, + Ed25519: &xdr.Uint256{34}, + }, + Signature: []byte{0xf6}, + }, + }, + CloseTime: 1594584547, + }, + }, + }, + TotalByteSizeOfBucketList: xdr.Uint64(56), + TxSet: xdr.GeneralizedTransactionSet{ + V: 0, + V1TxSet: &xdr.TransactionSetV1{ + Phases: []xdr.TransactionPhase{ + { + V: 0, + V0Components: &[]xdr.TxSetComponent{ + { + Type: 0, + TxsMaybeDiscountedFee: &xdr.TxSetComponentTxsMaybeDiscountedFee{ + Txs: []xdr.TransactionEnvelope{ + createSampleTx(3), + createSampleTx(10), + }, + }, + }, + }, + }, + }, + }, + }, + TxProcessing: []xdr.TransactionResultMeta{ + { + Result: xdr.TransactionResultPair{ + Result: xdr.TransactionResult{ + Result: xdr.TransactionResultResult{ + Code: xdr.TransactionResultCodeTxSuccess, + Results: &[]xdr.OperationResult{ + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeCreateAccount, + CreateAccountResult: &xdr.CreateAccountResult{ + Code: 0, + }, + }, + }, + }, + }, + }, + }, + }, + { + Result: xdr.TransactionResultPair{ + Result: xdr.TransactionResult{ + Result: xdr.TransactionResultResult{ + Code: xdr.TransactionResultCodeTxFailed, + Results: &[]xdr.OperationResult{ + { + Code: xdr.OperationResultCodeOpInner, + Tr: &xdr.OperationResultTr{ + Type: xdr.OperationTypeCreateAccount, + CreateAccountResult: &xdr.CreateAccountResult{ + Code: 0, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + return lcm +} + +func createSampleTx(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) + } +} diff --git a/xdr/ledger_close_meta.go b/xdr/ledger_close_meta.go index 30e80b2e38..ace0a0f378 100644 --- a/xdr/ledger_close_meta.go +++ b/xdr/ledger_close_meta.go @@ -156,3 +156,15 @@ func (l LedgerCloseMeta) EvictedPersistentLedgerEntries() ([]LedgerEntry, error) panic(fmt.Sprintf("Unsupported LedgerCloseMeta.V: %d", l.V)) } } + +// TxProcessing returns the TransactionResultMeta in this ledger +func (l LedgerCloseMeta) TxProcessing() ([]TransactionResultMeta, error) { + switch l.V { + case 0: + return l.MustV0().TxProcessing, nil + case 1: + return l.MustV1().TxProcessing, nil + default: + return []TransactionResultMeta{}, fmt.Errorf("Unsupported LedgerCloseMeta.V: %d", l.V) + } +} diff --git a/xdr/node_id.go b/xdr/node_id.go new file mode 100644 index 0000000000..7f7420fb76 --- /dev/null +++ b/xdr/node_id.go @@ -0,0 +1,26 @@ +package xdr + +import ( + "fmt" + + "github.com/stellar/go/strkey" +) + +func (n NodeId) GetAddress() (string, error) { + switch n.Type { + case PublicKeyTypePublicKeyTypeEd25519: + ed, ok := n.GetEd25519() + if !ok { + return "", fmt.Errorf("could not get NodeID.Ed25519") + } + raw := make([]byte, 32) + copy(raw, ed[:]) + encodedAddress, err := strkey.Encode(strkey.VersionByteAccountID, raw) + if err != nil { + return "", err + } + return encodedAddress, nil + default: + return "", fmt.Errorf("unknown NodeId.PublicKeyType") + } +} diff --git a/xdr/transaction_envelope.go b/xdr/transaction_envelope.go index 6d513ff342..2cde059e5a 100644 --- a/xdr/transaction_envelope.go +++ b/xdr/transaction_envelope.go @@ -242,3 +242,7 @@ func (e TransactionEnvelope) Memo() Memo { panic("unsupported transaction type: " + e.Type.String()) } } + +func (e TransactionEnvelope) OperationsCount() uint32 { + return uint32(len(e.Operations())) +}