From ee9102df04a3b84f9d24bc282c9c31e37ada54d6 Mon Sep 17 00:00:00 2001 From: Antoine Gelloz Date: Tue, 21 Jun 2022 15:05:55 +0200 Subject: [PATCH] Add new transactions tests and move processTx() with its tests to separate files (#229) --- .../transaction_controller_test.go | 288 +++++++++++++++++ pkg/core/log.go | 6 +- pkg/core/volumes.go | 15 +- pkg/ledger/ledger.go | 113 ------- pkg/ledger/process.go | 121 +++++++ pkg/ledger/process_test.go | 304 ++++++++++++++++++ 6 files changed, 725 insertions(+), 122 deletions(-) create mode 100644 pkg/ledger/process.go create mode 100644 pkg/ledger/process_test.go diff --git a/pkg/api/controllers/transaction_controller_test.go b/pkg/api/controllers/transaction_controller_test.go index ec7ec612f..420647999 100644 --- a/pkg/api/controllers/transaction_controller_test.go +++ b/pkg/api/controllers/transaction_controller_test.go @@ -503,6 +503,294 @@ func TestGetTransactions(t *testing.T) { })) } +type transaction struct { + core.Transaction + PreCommitVolumes accountsVolumes `json:"preCommitVolumes,omitempty"` + PostCommitVolumes accountsVolumes `json:"postCommitVolumes,omitempty"` +} +type accountsVolumes map[string]assetsVolumes +type assetsVolumes map[string]core.VolumesWithBalance + +func TestTransactionsVolumes(t *testing.T) { + internal.RunTest(t, fx.Invoke(func(lc fx.Lifecycle, api *api.API, driver storage.Driver) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + + // Single posting - single asset + + const worldAliceUSD int64 = 100 + + rsp := internal.PostTransaction(t, api, + core.TransactionData{ + Postings: core.Postings{ + { + Source: "world", + Destination: "alice", + Amount: worldAliceUSD, + Asset: "USD", + }, + }, + }) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) + txs, ok := internal.DecodeSingleResponse[[]transaction](t, rsp.Body) + require.True(t, ok) + require.Len(t, txs, 1) + + expPreVolumes := accountsVolumes{ + "alice": assetsVolumes{ + "USD": core.VolumesWithBalance{}, + }, + "world": assetsVolumes{ + "USD": core.VolumesWithBalance{}, + }, + } + + expPostVolumes := accountsVolumes{ + "alice": assetsVolumes{ + "USD": core.VolumesWithBalance{ + Input: worldAliceUSD, + Balance: worldAliceUSD, + }, + }, + "world": assetsVolumes{ + "USD": core.VolumesWithBalance{ + Output: worldAliceUSD, + Balance: -worldAliceUSD, + }, + }, + } + + assert.Equal(t, expPreVolumes, txs[0].PreCommitVolumes) + assert.Equal(t, expPostVolumes, txs[0].PostCommitVolumes) + + rsp = internal.GetTransactions(api, url.Values{}) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) + cursor := internal.DecodeCursorResponse[transaction](t, rsp.Body) + require.Len(t, cursor.Data, 1) + + assert.Equal(t, expPreVolumes, cursor.Data[0].PreCommitVolumes) + assert.Equal(t, expPostVolumes, cursor.Data[0].PostCommitVolumes) + + prevVolAliceUSD := expPostVolumes["alice"]["USD"] + + // Single posting - single asset + + const aliceBobUSD int64 = 93 + + rsp = internal.PostTransaction(t, api, + core.TransactionData{ + Postings: core.Postings{ + { + Source: "alice", + Destination: "bob", + Amount: aliceBobUSD, + Asset: "USD", + }, + }, + }) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) + txs, ok = internal.DecodeSingleResponse[[]transaction](t, rsp.Body) + require.True(t, ok) + require.Len(t, txs, 1) + + expPreVolumes = accountsVolumes{ + "alice": assetsVolumes{ + "USD": prevVolAliceUSD, + }, + "bob": assetsVolumes{ + "USD": core.VolumesWithBalance{}, + }, + } + + expPostVolumes = accountsVolumes{ + "alice": assetsVolumes{ + "USD": core.VolumesWithBalance{ + Input: prevVolAliceUSD.Input, + Output: prevVolAliceUSD.Output + aliceBobUSD, + Balance: prevVolAliceUSD.Input - prevVolAliceUSD.Output - aliceBobUSD, + }, + }, + "bob": assetsVolumes{ + "USD": core.VolumesWithBalance{ + Input: aliceBobUSD, + Balance: aliceBobUSD, + }, + }, + } + + assert.Equal(t, expPreVolumes, txs[0].PreCommitVolumes) + assert.Equal(t, expPostVolumes, txs[0].PostCommitVolumes) + + rsp = internal.GetTransactions(api, url.Values{}) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) + cursor = internal.DecodeCursorResponse[transaction](t, rsp.Body) + require.Len(t, cursor.Data, 2) + + assert.Equal(t, expPreVolumes, cursor.Data[0].PreCommitVolumes) + assert.Equal(t, expPostVolumes, cursor.Data[0].PostCommitVolumes) + + prevVolAliceUSD = expPostVolumes["alice"]["USD"] + prevVolBobUSD := expPostVolumes["bob"]["USD"] + + // Multi posting - single asset + + const worldBobEUR int64 = 156 + const bobAliceEUR int64 = 3 + + rsp = internal.PostTransaction(t, api, + core.TransactionData{ + Postings: core.Postings{ + { + Source: "world", + Destination: "bob", + Amount: worldBobEUR, + Asset: "EUR", + }, + { + Source: "bob", + Destination: "alice", + Amount: bobAliceEUR, + Asset: "EUR", + }, + }, + }) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) + txs, ok = internal.DecodeSingleResponse[[]transaction](t, rsp.Body) + require.True(t, ok) + require.Len(t, txs, 1) + + expPreVolumes = accountsVolumes{ + "alice": assetsVolumes{ + "EUR": core.VolumesWithBalance{}, + }, + "bob": assetsVolumes{ + "EUR": core.VolumesWithBalance{}, + }, + "world": assetsVolumes{ + "EUR": core.VolumesWithBalance{}, + }, + } + + expPostVolumes = accountsVolumes{ + "alice": assetsVolumes{ + "EUR": core.VolumesWithBalance{ + Input: bobAliceEUR, + Output: 0, + Balance: bobAliceEUR, + }, + }, + "bob": assetsVolumes{ + "EUR": core.VolumesWithBalance{ + Input: worldBobEUR, + Output: bobAliceEUR, + Balance: worldBobEUR - bobAliceEUR, + }, + }, + "world": assetsVolumes{ + "EUR": core.VolumesWithBalance{ + Input: 0, + Output: worldBobEUR, + Balance: -worldBobEUR, + }, + }, + } + + assert.Equal(t, expPreVolumes, txs[0].PreCommitVolumes) + assert.Equal(t, expPostVolumes, txs[0].PostCommitVolumes) + + rsp = internal.GetTransactions(api, url.Values{}) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) + cursor = internal.DecodeCursorResponse[transaction](t, rsp.Body) + require.Len(t, cursor.Data, 3) + + assert.Equal(t, expPreVolumes, cursor.Data[0].PreCommitVolumes) + assert.Equal(t, expPostVolumes, cursor.Data[0].PostCommitVolumes) + + prevVolAliceEUR := expPostVolumes["alice"]["EUR"] + prevVolBobEUR := expPostVolumes["bob"]["EUR"] + + // Multi postings - multi assets + + const bobAliceUSD int64 = 1 + const aliceBobEUR int64 = 2 + + rsp = internal.PostTransaction(t, api, + core.TransactionData{ + Postings: core.Postings{ + { + Source: "bob", + Destination: "alice", + Amount: bobAliceUSD, + Asset: "USD", + }, + { + Source: "alice", + Destination: "bob", + Amount: aliceBobEUR, + Asset: "EUR", + }, + }, + }) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) + txs, ok = internal.DecodeSingleResponse[[]transaction](t, rsp.Body) + require.True(t, ok) + require.Len(t, txs, 1) + + expPreVolumes = accountsVolumes{ + "alice": assetsVolumes{ + "EUR": prevVolAliceEUR, + "USD": prevVolAliceUSD, + }, + "bob": assetsVolumes{ + "EUR": prevVolBobEUR, + "USD": prevVolBobUSD, + }, + } + + expPostVolumes = accountsVolumes{ + "alice": assetsVolumes{ + "EUR": core.VolumesWithBalance{ + Input: prevVolAliceEUR.Input, + Output: prevVolAliceEUR.Output + aliceBobEUR, + Balance: prevVolAliceEUR.Balance - aliceBobEUR, + }, + "USD": core.VolumesWithBalance{ + Input: prevVolAliceUSD.Input + bobAliceUSD, + Output: prevVolAliceUSD.Output, + Balance: prevVolAliceUSD.Balance + bobAliceUSD, + }, + }, + "bob": assetsVolumes{ + "EUR": core.VolumesWithBalance{ + Input: prevVolBobEUR.Input + aliceBobEUR, + Output: prevVolBobEUR.Output, + Balance: prevVolBobEUR.Balance + aliceBobEUR, + }, + "USD": core.VolumesWithBalance{ + Input: prevVolBobUSD.Input, + Output: prevVolBobUSD.Output + bobAliceUSD, + Balance: prevVolBobUSD.Balance - bobAliceUSD, + }, + }, + } + + assert.Equal(t, expPreVolumes, txs[0].PreCommitVolumes) + assert.Equal(t, expPostVolumes, txs[0].PostCommitVolumes) + + rsp = internal.GetTransactions(api, url.Values{}) + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) + cursor = internal.DecodeCursorResponse[transaction](t, rsp.Body) + require.Len(t, cursor.Data, 4) + + assert.Equal(t, expPreVolumes, cursor.Data[0].PreCommitVolumes) + assert.Equal(t, expPostVolumes, cursor.Data[0].PostCommitVolumes) + + return nil + }, + }) + })) +} + func TestPostTransactionMetadata(t *testing.T) { internal.RunTest(t, fx.Invoke(func(lc fx.Lifecycle, api *api.API) { lc.Append(fx.Hook{ diff --git a/pkg/core/log.go b/pkg/core/log.go index 8b6c43bbc..109ab465f 100644 --- a/pkg/core/log.go +++ b/pkg/core/log.go @@ -11,9 +11,9 @@ import ( const SetMetadataType = "SET_METADATA" const NewTransactionType = "NEW_TRANSACTION" -type loggedTX Transaction +type LoggedTX Transaction -func (m loggedTX) MarshalJSON() ([]byte, error) { +func (m LoggedTX) MarshalJSON() ([]byte, error) { metadata := make(map[string]interface{}) for k, v := range m.Metadata { var i interface{} @@ -50,7 +50,7 @@ func NewTransactionLogWithDate(previousLog *Log, tx Transaction, time time.Time) ID: id, Type: NewTransactionType, Date: time, - Data: loggedTX(tx), + Data: LoggedTX(tx), } l.Hash = Hash(previousLog, &l) return l diff --git a/pkg/core/volumes.go b/pkg/core/volumes.go index cd4f2d22b..9e6c44d45 100644 --- a/pkg/core/volumes.go +++ b/pkg/core/volumes.go @@ -10,13 +10,16 @@ type Volume struct { Output int64 `json:"output"` } +type VolumesWithBalance struct { + Input int64 `json:"input"` + Output int64 `json:"output"` + Balance int64 `json:"balance"` +} + func (v Volume) MarshalJSON() ([]byte, error) { - type volume Volume - return json.Marshal(struct { - volume - Balance int64 `json:"balance"` - }{ - volume: volume(v), + return json.Marshal(VolumesWithBalance{ + Input: v.Input, + Output: v.Output, Balance: v.Input - v.Output, }) } diff --git a/pkg/ledger/ledger.go b/pkg/ledger/ledger.go index 69f284982..4b41a31a8 100644 --- a/pkg/ledger/ledger.go +++ b/pkg/ledger/ledger.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "strings" - "time" "github.com/numary/go-libs/sharedapi" "github.com/numary/ledger/pkg/core" @@ -57,118 +56,6 @@ type CommitResult struct { GeneratedLogs []core.Log } -func (l *Ledger) processTx(ctx context.Context, ts []core.TransactionData) (*CommitResult, error) { - mapping, err := l.store.LoadMapping(ctx) - if err != nil { - return nil, errors.Wrap(err, "loading mapping") - } - - lastLog, err := l.store.LastLog(ctx) - if err != nil { - return nil, err - } - - var nextTxId uint64 - lastTx, err := l.store.GetLastTransaction(ctx) - if err != nil { - return nil, err - } - if lastTx != nil { - nextTxId = lastTx.ID + 1 - } - - volumeAggregator := newVolumeAggregator(l.store) - - generatedTxs := make([]core.Transaction, 0) - accounts := make(map[string]core.Account, 0) - generatedLogs := make([]core.Log, 0) - contracts := make([]core.Contract, 0) - if mapping != nil { - contracts = append(contracts, mapping.Contracts...) - } - contracts = append(contracts, DefaultContracts...) - - for i, t := range ts { - if len(t.Postings) == 0 { - return nil, NewTransactionCommitError(i, NewValidationError("transaction has no postings")) - } - - txVolumeAggregator := volumeAggregator.nextTx() - - for _, p := range t.Postings { - if p.Amount < 0 { - return nil, NewTransactionCommitError(i, NewValidationError("negative amount")) - } - if !core.ValidateAddress(p.Source) { - return nil, NewTransactionCommitError(i, NewValidationError("invalid source address")) - } - if !core.ValidateAddress(p.Destination) { - return nil, NewTransactionCommitError(i, NewValidationError("invalid destination address")) - } - if !core.AssetIsValid(p.Asset) { - return nil, NewTransactionCommitError(i, NewValidationError("invalid asset")) - } - err := txVolumeAggregator.transfer(ctx, p.Source, p.Destination, p.Asset, uint64(p.Amount)) - if err != nil { - return nil, NewTransactionCommitError(i, err) - } - } - - for addr, volumes := range txVolumeAggregator.postCommitVolumes() { - for asset, volume := range volumes { - if addr == "world" { - continue - } - - expectedBalance := volume.Balance() - for _, contract := range contracts { - if contract.Match(addr) { - account, ok := accounts[addr] - if !ok { - account, err = l.store.GetAccount(ctx, addr) - if err != nil { - return nil, err - } - accounts[addr] = account - } - - if ok = contract.Expr.Eval(core.EvalContext{ - Variables: map[string]interface{}{ - "balance": float64(expectedBalance), - }, - Metadata: account.Metadata, - Asset: asset, - }); !ok { - return nil, NewTransactionCommitError(i, NewInsufficientFundError(asset)) - } - break - } - } - } - } - - tx := core.Transaction{ - TransactionData: t, - ID: nextTxId, - Timestamp: time.Now().UTC().Format(time.RFC3339), - PostCommitVolumes: txVolumeAggregator.postCommitVolumes(), - PreCommitVolumes: txVolumeAggregator.preCommitVolumes(), - } - generatedTxs = append(generatedTxs, tx) - newLog := core.NewTransactionLog(lastLog, tx) - lastLog = &newLog - generatedLogs = append(generatedLogs, newLog) - nextTxId++ - } - - return &CommitResult{ - PreCommitVolumes: volumeAggregator.aggregatedPreCommitVolumes(), - PostCommitVolumes: volumeAggregator.aggregatedPostCommitVolumes(), - GeneratedTransactions: generatedTxs, - GeneratedLogs: generatedLogs, - }, nil -} - func (l *Ledger) Commit(ctx context.Context, ts []core.TransactionData) (core.AggregatedVolumes, []core.Transaction, error) { unlock, err := l.locker.Lock(ctx, l.name) if err != nil { diff --git a/pkg/ledger/process.go b/pkg/ledger/process.go new file mode 100644 index 000000000..421854723 --- /dev/null +++ b/pkg/ledger/process.go @@ -0,0 +1,121 @@ +package ledger + +import ( + "context" + "time" + + "github.com/numary/ledger/pkg/core" + "github.com/pkg/errors" +) + +func (l *Ledger) processTx(ctx context.Context, ts []core.TransactionData) (*CommitResult, error) { + mapping, err := l.store.LoadMapping(ctx) + if err != nil { + return nil, errors.Wrap(err, "loading mapping") + } + + lastLog, err := l.store.LastLog(ctx) + if err != nil { + return nil, err + } + + var nextTxId uint64 + lastTx, err := l.store.GetLastTransaction(ctx) + if err != nil { + return nil, err + } + if lastTx != nil { + nextTxId = lastTx.ID + 1 + } + + volumeAggregator := newVolumeAggregator(l.store) + + generatedTxs := make([]core.Transaction, 0) + accounts := make(map[string]core.Account, 0) + generatedLogs := make([]core.Log, 0) + contracts := make([]core.Contract, 0) + if mapping != nil { + contracts = append(contracts, mapping.Contracts...) + } + contracts = append(contracts, DefaultContracts...) + + for i, t := range ts { + if len(t.Postings) == 0 { + return nil, NewTransactionCommitError(i, NewValidationError("transaction has no postings")) + } + + txVolumeAggregator := volumeAggregator.nextTx() + + for _, p := range t.Postings { + if p.Amount < 0 { + return nil, NewTransactionCommitError(i, NewValidationError("negative amount")) + } + if !core.ValidateAddress(p.Source) { + return nil, NewTransactionCommitError(i, NewValidationError("invalid source address")) + } + if !core.ValidateAddress(p.Destination) { + return nil, NewTransactionCommitError(i, NewValidationError("invalid destination address")) + } + if !core.AssetIsValid(p.Asset) { + return nil, NewTransactionCommitError(i, NewValidationError("invalid asset")) + } + err := txVolumeAggregator.transfer(ctx, p.Source, p.Destination, p.Asset, uint64(p.Amount)) + if err != nil { + return nil, NewTransactionCommitError(i, err) + } + } + + for addr, volumes := range txVolumeAggregator.postCommitVolumes() { + for asset, volume := range volumes { + if addr == "world" { + continue + } + + expectedBalance := volume.Balance() + for _, contract := range contracts { + if contract.Match(addr) { + account, ok := accounts[addr] + if !ok { + account, err = l.store.GetAccount(ctx, addr) + if err != nil { + return nil, err + } + accounts[addr] = account + } + + if ok = contract.Expr.Eval(core.EvalContext{ + Variables: map[string]interface{}{ + "balance": float64(expectedBalance), + }, + Metadata: account.Metadata, + Asset: asset, + }); !ok { + return nil, NewTransactionCommitError(i, NewInsufficientFundError(asset)) + } + break + } + } + } + } + + tx := core.Transaction{ + TransactionData: t, + ID: nextTxId, + Timestamp: time.Now().UTC().Format(time.RFC3339), + PostCommitVolumes: txVolumeAggregator.postCommitVolumes(), + PreCommitVolumes: txVolumeAggregator.preCommitVolumes(), + } + generatedTxs = append(generatedTxs, tx) + newLog := core.NewTransactionLog(lastLog, tx) + lastLog = &newLog + generatedLogs = append(generatedLogs, newLog) + nextTxId++ + } + + return &CommitResult{ + PreCommitVolumes: volumeAggregator.aggregatedPreCommitVolumes(), + PostCommitVolumes: volumeAggregator.aggregatedPostCommitVolumes(), + GeneratedTransactions: generatedTxs, + GeneratedLogs: generatedLogs, + }, nil +} diff --git a/pkg/ledger/process_test.go b/pkg/ledger/process_test.go new file mode 100644 index 000000000..a2af971a5 --- /dev/null +++ b/pkg/ledger/process_test.go @@ -0,0 +1,304 @@ +package ledger + +import ( + "context" + "testing" + "time" + + "github.com/numary/ledger/pkg/core" + "github.com/stretchr/testify/assert" +) + +func TestLedger_processTx(t *testing.T) { + runOnLedger(func(l *Ledger) { + t.Run("multi assets", func(t *testing.T) { + const ( + worldTotoUSD int64 = 43 + worldAliceUSD int64 = 98 + aliceTotoUSD int64 = 45 + worldTotoEUR int64 = 15 + worldAliceEUR int64 = 10 + totoAliceEUR int64 = 5 + ) + + postings := []core.Posting{ + { + Source: "world", + Destination: "toto", + Amount: worldTotoUSD, + Asset: "USD", + }, + { + Source: "world", + Destination: "alice", + Amount: worldAliceUSD, + Asset: "USD", + }, + { + Source: "alice", + Destination: "toto", + Amount: aliceTotoUSD, + Asset: "USD", + }, + { + Source: "world", + Destination: "toto", + Amount: worldTotoEUR, + Asset: "EUR", + }, + { + Source: "world", + Destination: "alice", + Amount: worldAliceEUR, + Asset: "EUR", + }, + { + Source: "toto", + Destination: "alice", + Amount: totoAliceEUR, + Asset: "EUR", + }, + } + + expectedPreCommitVol := core.AggregatedVolumes{ + "alice": core.Volumes{ + "USD": {}, + "EUR": {}, + }, + "toto": core.Volumes{ + "USD": {}, + "EUR": {}, + }, + "world": core.Volumes{ + "USD": {}, + "EUR": {}, + }, + } + + expectedPostCommitVol := core.AggregatedVolumes{ + "alice": core.Volumes{ + "USD": { + Input: worldAliceUSD, + Output: aliceTotoUSD, + }, + "EUR": { + Input: worldAliceEUR + totoAliceEUR, + }, + }, + "toto": core.Volumes{ + "USD": { + Input: worldTotoUSD + aliceTotoUSD, + }, + "EUR": { + Input: worldTotoEUR, + Output: totoAliceEUR, + }, + }, + "world": core.Volumes{ + "USD": { + Output: worldTotoUSD + worldAliceUSD, + }, + "EUR": { + Output: worldTotoEUR + worldAliceEUR, + }, + }, + } + + t.Run("single transaction multi postings", func(t *testing.T) { + txsData := []core.TransactionData{ + {Postings: postings}, + } + + res, err := l.processTx(context.Background(), txsData) + assert.NoError(t, err) + + assert.Equal(t, expectedPreCommitVol, res.PreCommitVolumes) + assert.Equal(t, expectedPostCommitVol, res.PostCommitVolumes) + + expectedTxs := []core.Transaction{{ + TransactionData: txsData[0], + ID: 0, + Timestamp: time.Now().UTC().Format(time.RFC3339), + PreCommitVolumes: expectedPreCommitVol, + PostCommitVolumes: expectedPostCommitVol, + }} + assert.Equal(t, expectedTxs, res.GeneratedTransactions) + + assert.True(t, time.Until(res.GeneratedLogs[0].Date) < time.Millisecond) + + expectedLogs := []core.Log{{ + ID: 0, + Type: core.NewTransactionType, + Data: core.LoggedTX(expectedTxs[0]), + Date: res.GeneratedLogs[0].Date, + }} + expectedLogs[0].Hash = core.Hash(nil, expectedLogs[0]) + + assert.Equal(t, expectedLogs, res.GeneratedLogs) + }) + + t.Run("multi transactions single postings", func(t *testing.T) { + txsData := []core.TransactionData{ + {Postings: []core.Posting{postings[0]}}, + {Postings: []core.Posting{postings[1]}}, + {Postings: []core.Posting{postings[2]}}, + {Postings: []core.Posting{postings[3]}}, + {Postings: []core.Posting{postings[4]}}, + {Postings: []core.Posting{postings[5]}}, + } + + res, err := l.processTx(context.Background(), txsData) + assert.NoError(t, err) + + assert.Equal(t, expectedPreCommitVol, res.PreCommitVolumes) + assert.Equal(t, expectedPostCommitVol, res.PostCommitVolumes) + + expectedTxs := []core.Transaction{ + { + TransactionData: core.TransactionData{Postings: core.Postings{postings[0]}}, + ID: 0, + Timestamp: time.Now().UTC().Format(time.RFC3339), + PreCommitVolumes: core.AggregatedVolumes{ + "toto": core.Volumes{"USD": core.Volume{Input: 0, Output: 0}}, + "world": core.Volumes{"USD": core.Volume{Input: 0, Output: 0}}}, + PostCommitVolumes: core.AggregatedVolumes{ + "toto": core.Volumes{"USD": core.Volume{Input: worldTotoUSD, Output: 0}}, + "world": core.Volumes{"USD": core.Volume{Input: 0, Output: worldTotoUSD}}}, + }, + { + TransactionData: core.TransactionData{Postings: core.Postings{postings[1]}}, + ID: 1, + Timestamp: time.Now().UTC().Format(time.RFC3339), + PreCommitVolumes: core.AggregatedVolumes{ + "world": core.Volumes{"USD": core.Volume{Input: 0, Output: worldTotoUSD}}, + "alice": core.Volumes{"USD": core.Volume{Input: 0, Output: 0}}, + }, + PostCommitVolumes: core.AggregatedVolumes{ + "world": core.Volumes{"USD": core.Volume{Input: 0, Output: worldTotoUSD + worldAliceUSD}}, + "alice": core.Volumes{"USD": core.Volume{Input: worldAliceUSD, Output: 0}}, + }, + }, + { + TransactionData: core.TransactionData{Postings: core.Postings{postings[2]}}, + ID: 2, + Timestamp: time.Now().UTC().Format(time.RFC3339), + PreCommitVolumes: core.AggregatedVolumes{ + "alice": core.Volumes{"USD": core.Volume{Input: worldAliceUSD, Output: 0}}, + "toto": core.Volumes{"USD": core.Volume{Input: worldTotoUSD, Output: 0}}, + }, + PostCommitVolumes: core.AggregatedVolumes{ + "alice": core.Volumes{"USD": core.Volume{Input: worldAliceUSD, Output: aliceTotoUSD}}, + "toto": core.Volumes{"USD": core.Volume{Input: worldTotoUSD + aliceTotoUSD, Output: 0}}, + }, + }, + { + TransactionData: core.TransactionData{Postings: core.Postings{postings[3]}}, + ID: 3, + Timestamp: time.Now().UTC().Format(time.RFC3339), + PreCommitVolumes: core.AggregatedVolumes{ + "world": core.Volumes{"EUR": core.Volume{Input: 0, Output: 0}}, + "toto": core.Volumes{"EUR": core.Volume{Input: 0, Output: 0}}, + }, + PostCommitVolumes: core.AggregatedVolumes{ + "world": core.Volumes{"EUR": core.Volume{Input: 0, Output: worldTotoEUR}}, + "toto": core.Volumes{"EUR": core.Volume{Input: worldTotoEUR, Output: 0}}, + }, + }, + { + TransactionData: core.TransactionData{Postings: core.Postings{postings[4]}}, + ID: 4, + Timestamp: time.Now().UTC().Format(time.RFC3339), + PreCommitVolumes: core.AggregatedVolumes{ + "world": core.Volumes{"EUR": core.Volume{Input: 0, Output: worldTotoEUR}}, + "alice": core.Volumes{"EUR": core.Volume{Input: 0, Output: 0}}, + }, + PostCommitVolumes: core.AggregatedVolumes{ + "world": core.Volumes{"EUR": core.Volume{Input: 0, Output: worldTotoEUR + worldAliceEUR}}, + "alice": core.Volumes{"EUR": core.Volume{Input: worldAliceEUR, Output: 0}}, + }, + }, + { + TransactionData: core.TransactionData{Postings: core.Postings{postings[5]}}, + ID: 5, + Timestamp: time.Now().UTC().Format(time.RFC3339), + PreCommitVolumes: core.AggregatedVolumes{ + "toto": core.Volumes{"EUR": core.Volume{Input: worldTotoEUR, Output: 0}}, + "alice": core.Volumes{"EUR": core.Volume{Input: worldAliceEUR, Output: 0}}, + }, + PostCommitVolumes: core.AggregatedVolumes{ + "toto": core.Volumes{"EUR": core.Volume{Input: worldTotoEUR, Output: totoAliceEUR}}, + "alice": core.Volumes{"EUR": core.Volume{Input: worldAliceEUR + totoAliceEUR, Output: 0}}, + }, + }, + } + + assert.Equal(t, expectedTxs, res.GeneratedTransactions) + + expectedLogs := []core.Log{ + { + ID: 0, + Type: core.NewTransactionType, + Data: core.LoggedTX(expectedTxs[0]), + Date: res.GeneratedLogs[0].Date, + }, + { + ID: 1, + Type: core.NewTransactionType, + Data: core.LoggedTX(expectedTxs[1]), + Date: res.GeneratedLogs[1].Date, + }, + { + ID: 2, + Type: core.NewTransactionType, + Data: core.LoggedTX(expectedTxs[2]), + Date: res.GeneratedLogs[2].Date, + }, + { + ID: 3, + Type: core.NewTransactionType, + Data: core.LoggedTX(expectedTxs[3]), + Date: res.GeneratedLogs[3].Date, + }, + { + ID: 4, + Type: core.NewTransactionType, + Data: core.LoggedTX(expectedTxs[4]), + Date: res.GeneratedLogs[4].Date, + }, + { + ID: 5, + Type: core.NewTransactionType, + Data: core.LoggedTX(expectedTxs[5]), + Date: res.GeneratedLogs[5].Date, + }, + } + expectedLogs[0].Hash = core.Hash(nil, expectedLogs[0]) + expectedLogs[1].Hash = core.Hash(expectedLogs[0], expectedLogs[1]) + expectedLogs[2].Hash = core.Hash(expectedLogs[1], expectedLogs[2]) + expectedLogs[3].Hash = core.Hash(expectedLogs[2], expectedLogs[3]) + expectedLogs[4].Hash = core.Hash(expectedLogs[3], expectedLogs[4]) + expectedLogs[5].Hash = core.Hash(expectedLogs[4], expectedLogs[5]) + + assert.True(t, time.Until(res.GeneratedLogs[0].Date) < time.Millisecond) + assert.True(t, time.Until(res.GeneratedLogs[1].Date) < time.Millisecond) + assert.True(t, time.Until(res.GeneratedLogs[2].Date) < time.Millisecond) + assert.True(t, time.Until(res.GeneratedLogs[3].Date) < time.Millisecond) + assert.True(t, time.Until(res.GeneratedLogs[4].Date) < time.Millisecond) + assert.True(t, time.Until(res.GeneratedLogs[5].Date) < time.Millisecond) + + assert.Equal(t, expectedLogs, res.GeneratedLogs) + }) + }) + + t.Run("no transactions", func(t *testing.T) { + result, err := l.processTx(context.Background(), []core.TransactionData{}) + assert.NoError(t, err) + assert.Equal(t, &CommitResult{ + PreCommitVolumes: core.AggregatedVolumes{}, + PostCommitVolumes: core.AggregatedVolumes{}, + GeneratedTransactions: []core.Transaction{}, + GeneratedLogs: []core.Log{}, + }, result) + }) + }) +}