From bbd082afa85f574dcb36b614b3713a48c7051d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Sala=C3=BCn?= Date: Wed, 31 Aug 2022 18:39:46 +0200 Subject: [PATCH] feat: introduce new monetary int (#310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: introduce new monetary int type * fix: apply suggestions from code review Co-authored-by: Antoine Gelloz Signed-off-by: Clément Salaün * fix: remove comment in pkg/ledger/process_test.go Co-authored-by: Antoine Gelloz Signed-off-by: Clément Salaün * chore: clean * feat: Make verbose configurable on tests * feat: use dotenv on Taskfile to allow developpers to have their own set of parameters * chore: remove useless file * fix: remove binary and change verbose to bool * fix: after merge Signed-off-by: Clément Salaün Signed-off-by: Ragot Geoffrey Co-authored-by: Antoine Gelloz Co-authored-by: Geoffrey Ragot --- .gitignore | 4 +- Taskfile.yaml | 41 +-- cmd/container.go | 3 +- cmd/script_check.go | 4 +- cmd/script_exec.go | 12 +- go.mod | 5 +- go.sum | 10 +- pkg/analytics/segment_test.go | 4 +- .../controllers/account_controller_test.go | 13 +- .../controllers/balance_controller_test.go | 26 +- pkg/api/controllers/ledger_controller_test.go | 4 +- .../controllers/mapping_controller_test.go | 5 +- pkg/api/controllers/pagination_test.go | 2 +- .../transaction_controller_test.go | 145 +++++---- pkg/core/contract.go | 25 +- pkg/core/expr.go | 294 ------------------ pkg/core/expr_test.go | 136 -------- pkg/core/log.go | 3 +- pkg/core/log_test.go | 72 ++--- pkg/core/monetary.go | 120 +++++++ pkg/core/posting.go | 8 +- pkg/core/posting_test.go | 12 +- pkg/core/transaction.go | 3 +- pkg/core/transaction_test.go | 8 +- pkg/core/volumes.go | 54 ++-- pkg/ledger/executor.go | 7 +- pkg/ledger/executor_test.go | 22 +- pkg/ledger/ledger.go | 9 +- pkg/ledger/ledger_test.go | 74 ++--- pkg/ledger/process.go | 18 +- pkg/ledger/process_test.go | 105 ++++--- pkg/ledger/volume_agg.go | 20 +- pkg/ledger/volume_agg_test.go | 112 ++++--- pkg/storage/sqlstorage/aggregations.go | 38 ++- pkg/storage/sqlstorage/balances.go | 14 +- pkg/storage/sqlstorage/balances_test.go | 18 +- pkg/storage/sqlstorage/log.go | 2 +- .../migrates/13-amounts-numeric/postgres.sql | 4 + .../migrates/9-add-pre-post-volumes/any.go | 48 ++- .../9-add-pre-post-volumes/any_test.go | 91 ++++-- pkg/storage/sqlstorage/store_test.go | 74 +++-- pkg/storage/sqlstorage/volumes.go | 7 +- 42 files changed, 774 insertions(+), 902 deletions(-) delete mode 100644 pkg/core/expr.go delete mode 100644 pkg/core/expr_test.go create mode 100644 pkg/core/monetary.go create mode 100644 pkg/storage/sqlstorage/migrates/13-amounts-numeric/postgres.sql diff --git a/.gitignore b/.gitignore index e27915adc..fbbedf61d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ cmd/control/* vendor sdk/swagger.yaml sdk/swagger.yaml-e -sdk/sdks \ No newline at end of file +sdk/sdks +.vscode +.env diff --git a/Taskfile.yaml b/Taskfile.yaml index 862281150..79a29ce8e 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -1,6 +1,9 @@ # https://taskfile.dev -version: '3' +version: "3" + +dotenv: +- .env vars: PKG: "./..." @@ -17,11 +20,11 @@ tasks: lint: cmds: - - golangci-lint run -v --fix + - golangci-lint run --fix {{if eq .VERBOSE "true"}}-v{{end}} tests: cmds: - - go test {{.TAGS}} -v -coverpkg {{.PKG}} -coverprofile coverage.out -covermode atomic {{.PKG}} + - go test {{.TAGS}} {{if eq .VERBOSE "true"}}-v{{end}} -coverpkg {{.PKG}} -coverprofile coverage.out -covermode atomic {{.PKG}} tests:local: cmds: @@ -31,7 +34,7 @@ tasks: tests:local:sqlite: cmds: - > - go test {{.TAGS}} -v {{.FAILFAST}} -coverpkg {{.PKG}} -coverprofile coverage.out -covermode atomic + go test {{.TAGS}} {{if eq .VERBOSE "true"}}-v{{end}} {{.FAILFAST}} -coverpkg {{.PKG}} -coverprofile coverage.out -covermode atomic -run {{.RUN}} -timeout {{.TIMEOUT}} {{.PKG}} | sed ''/PASS/s//$(printf "\033[32mPASS\033[0m")/'' | sed ''/FAIL/s//$(printf "\033[31mFAIL\033[0m")/'' | @@ -42,7 +45,7 @@ tasks: deps: [postgres] cmds: - > - go test {{.TAGS}} -v {{.FAILFAST}} -coverpkg {{.PKG}} -coverprofile coverage.out -covermode atomic + go test {{.TAGS}} {{if eq .VERBOSE "true"}}-v{{end}} {{.FAILFAST}} -coverpkg {{.PKG}} -coverprofile coverage.out -covermode atomic -run {{.RUN}} -timeout {{.TIMEOUT}} {{.PKG}} | sed ''/PASS/s//$(printf "\033[32mPASS\033[0m")/'' | sed ''/FAIL/s//$(printf "\033[31mFAIL\033[0m")/'' | @@ -54,7 +57,7 @@ tasks: bench: cmds: - - go test {{.TAGS}} -run=XXX -bench=. {{.PKG}} + - go test {{.TAGS}} {{if eq .VERBOSE "true"}}-v{{end}} -run=XXX -bench=. {{.PKG}} print:coverage: cmds: @@ -88,7 +91,7 @@ tasks: desc: Extract templates dir: ./sdk preconditions: - - sh: "[ \"{{.CLI_ARGS}}\" != \"\" ]" + - sh: '[ "{{.CLI_ARGS}}" != "" ]' msg: Please specify generator as first cli arg (ie "task template -- go") cmds: - > @@ -99,7 +102,7 @@ tasks: desc: Generate client code dir: ./sdk preconditions: - - sh: "[ \"{{.CLI_ARGS}}\" != \"\" ]" + - sh: '[ "{{.CLI_ARGS}}" != "" ]' msg: Please specify generator as first cli arg (ie "task generate -- go") cmds: - wget https://raw.githubusercontent.com/numary/ledger/{{.VERSION}}/pkg/api/controllers/swagger.yaml -O swagger.yaml @@ -118,7 +121,7 @@ tasks: desc: Test client code dir: ./sdk preconditions: - - sh: "[ \"{{.CLI_ARGS}}\" != \"\" ]" + - sh: '[ "{{.CLI_ARGS}}" != "" ]' msg: Please specify generator as first cli arg (ie "task test -- go") - sh: "[[ -e sdks/{{.CLI_ARGS}}/Taskfile.yml ]]" msg: "Not Taskfile found. You have to create a taskfile in ./sdks/{{.CLI_ARGS}}/ with a 'test' task" @@ -138,31 +141,31 @@ tasks: goreleaser:test:rpm: desc: Tests rpm packages vars: - rpm: 'rpm --nodeps -ivh' + rpm: "rpm --nodeps -ivh" cmds: - task: goreleaser:test:pkg vars: - Platform: 'amd64' + Platform: "amd64" Image: fedora - Cmd: '{{.rpm}} numary_*_linux_amd64.rpm' + Cmd: "{{.rpm}} numary_*_linux_amd64.rpm" - task: goreleaser:test:pkg vars: - Platform: 'arm64' + Platform: "arm64" Image: fedora - Cmd: '{{.rpm}} numary_*_linux_arm64.rpm' + Cmd: "{{.rpm}} numary_*_linux_arm64.rpm" goreleaser:test:deb: desc: Tests deb packages vars: - dpkg: 'dpkg --ignore-depends=git -i' + dpkg: "dpkg --ignore-depends=git -i" cmds: - task: goreleaser:test:pkg vars: - Platform: 'amd64' + Platform: "amd64" Image: ubuntu - Cmd: '{{.dpkg}} numary_*_linux_amd64.deb' + Cmd: "{{.dpkg}} numary_*_linux_amd64.deb" - task: goreleaser:test:pkg vars: - Platform: 'arm64' + Platform: "arm64" Image: ubuntu - Cmd: '{{.dpkg}} numary_*_linux_arm64.deb' + Cmd: "{{.dpkg}} numary_*_linux_arm64.deb" diff --git a/cmd/container.go b/cmd/container.go index 517dde88e..bc80e05d2 100644 --- a/cmd/container.go +++ b/cmd/container.go @@ -4,7 +4,6 @@ import ( "crypto/tls" "fmt" "io" - "io/ioutil" "log" "net/http" "os" @@ -258,7 +257,7 @@ func NewContainer(v *viper.Viper, userOptions ...fx.Option) *fx.App { res = append(res, middlewares.Log()) var writer io.Writer = os.Stderr if v.GetBool(sharedotlptraces.OtelTracesFlag) { - writer = ioutil.Discard + writer = io.Discard res = append(res, opentelemetrytraces.Middleware()) } res = append(res, gin.CustomRecoveryWithWriter(writer, func(c *gin.Context, err interface{}) { diff --git a/cmd/script_check.go b/cmd/script_check.go index 01f26b0fc..850292a87 100644 --- a/cmd/script_check.go +++ b/cmd/script_check.go @@ -2,7 +2,7 @@ package cmd import ( "fmt" - "io/ioutil" + "os" "github.com/numary/machine/script/compiler" "github.com/sirupsen/logrus" @@ -14,7 +14,7 @@ func NewScriptCheck() *cobra.Command { Use: "check [script]", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - b, err := ioutil.ReadFile(args[0]) + b, err := os.ReadFile(args[0]) if err != nil { logrus.Fatal(err) } diff --git a/cmd/script_exec.go b/cmd/script_exec.go index 8c8241776..a1a8d1322 100644 --- a/cmd/script_exec.go +++ b/cmd/script_exec.go @@ -4,9 +4,9 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" "net/http" "net/url" + "os" "regexp" "github.com/gin-gonic/gin" @@ -25,7 +25,7 @@ func NewScriptExec() *cobra.Command { Use: "exec [ledger] [script]", Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { - b, err := ioutil.ReadFile(args[1]) + b, err := os.ReadFile(args[1]) if err != nil { logrus.Fatal(err) } @@ -82,7 +82,13 @@ func NewScriptExec() *cobra.Command { fmt.Printf("ID: %d\r\n", result.Transaction.ID) fmt.Println("Postings:") for _, p := range result.Transaction.Postings { - fmt.Printf("\t Source: %s, Destination: %s, Amount: %d, Asset: %s\r\n", p.Source, p.Destination, p.Amount, p.Asset) + fmt.Printf( + "\t Source: %s, Destination: %s, Amount: %s, Asset: %s\r\n", + p.Source, + p.Destination, + p.Amount, + p.Asset, + ) } if !viper.GetBool(previewFlag) { fmt.Printf("Created transaction: http://%s/%s/transactions/%d\r\n", diff --git a/go.mod b/go.mod index 327eb8dad..d0b48318d 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/jackc/pgconn v1.10.1 github.com/jackc/pgx/v4 v4.14.1 github.com/mattn/go-sqlite3 v1.14.9 - github.com/numary/machine v1.2.0 + github.com/numary/machine v1.2.1-0.20220811010804-0a87156a9a4b github.com/ory/dockertest/v3 v3.8.1 github.com/pborman/uuid v1.2.1 github.com/pkg/errors v0.9.1 @@ -40,14 +40,13 @@ require ( github.com/Shopify/sarama v1.32.0 github.com/ThreeDotsLabs/watermill v1.1.1 github.com/buger/jsonparser v1.1.1 - github.com/gibson042/canonicaljson-go v1.0.3 github.com/go-redis/redis/v8 v8.11.4 github.com/go-redis/redismock/v8 v8.0.6 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/lib/pq v1.10.2 github.com/mitchellh/mapstructure v1.5.0 github.com/numary/go-libs v0.0.0-20220609103351-69aecd5d4097 - github.com/numary/go-libs/sharedhealth v0.0.0-20220801152411-b600be8e0d85 + github.com/numary/go-libs/sharedhealth v0.0.0-20220829123039-3eeb76619d81 github.com/numary/go-libs/sharedotlp v0.0.0-20220802090414-d0ae0613f325 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef diff --git a/go.sum b/go.sum index c6defb90c..171535b46 100644 --- a/go.sum +++ b/go.sum @@ -161,8 +161,6 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gibson042/canonicaljson-go v1.0.3 h1:EAyF8L74AWabkyUmrvEFHEt/AGFQeD6RfwbAuf0j1bI= -github.com/gibson042/canonicaljson-go v1.0.3/go.mod h1:DsLpJTThXyGNO+KZlI85C1/KDcImpP67k/RKVjcaEqo= github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -454,12 +452,12 @@ github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/numary/go-libs v0.0.0-20220609103351-69aecd5d4097 h1:50vHv20bk4REFNkbw2sOS3Fvzn7HzlzML0tY3QLxk3U= github.com/numary/go-libs v0.0.0-20220609103351-69aecd5d4097/go.mod h1:7StJNTZ3QbU2uBWpOeryPaHD6xMYUN8AWXWTjP67kv0= -github.com/numary/go-libs/sharedhealth v0.0.0-20220801152411-b600be8e0d85 h1:+KA84QMomrNaoHrtrDIRbE4HkUrgu3JAsXcw8XF3OGQ= -github.com/numary/go-libs/sharedhealth v0.0.0-20220801152411-b600be8e0d85/go.mod h1:DzBUp4HCdWscgXmt8/5F/KlCSktv0OwgOwDfRdz5Hzo= +github.com/numary/go-libs/sharedhealth v0.0.0-20220829123039-3eeb76619d81 h1:VhHu7IUQZ6LOTY6a2QlWb5U3MofOV2yP+LGXywaTnP0= +github.com/numary/go-libs/sharedhealth v0.0.0-20220829123039-3eeb76619d81/go.mod h1:DzBUp4HCdWscgXmt8/5F/KlCSktv0OwgOwDfRdz5Hzo= github.com/numary/go-libs/sharedotlp v0.0.0-20220802090414-d0ae0613f325 h1:5hbV1OF6A4CYuZYtMJsX3bcTmgtKdY3DsaIKKpCoqJw= github.com/numary/go-libs/sharedotlp v0.0.0-20220802090414-d0ae0613f325/go.mod h1:4QEZTmjeQbNMjWd/pKADguTjxTvAuWgh+ZBWzJ7xDiI= -github.com/numary/machine v1.2.0 h1:Jiy8FMPCJb4Cep5nACXyIQFzVPHse+t2w9Jzzqc7Ojk= -github.com/numary/machine v1.2.0/go.mod h1:vK6ftGapdOvYjFxbRHFYKxzce4doaCpfk9Wqkhozkfw= +github.com/numary/machine v1.2.1-0.20220811010804-0a87156a9a4b h1:dw6XcO2+7gETGB9JnugRfTyXm5WVQSkzHjYn+AYvxSw= +github.com/numary/machine v1.2.1-0.20220811010804-0a87156a9a4b/go.mod h1:Urkud39TPaMxmSwOd6H3hCqob710Et/tcWSv0QjbyGo= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= diff --git a/pkg/analytics/segment_test.go b/pkg/analytics/segment_test.go index 82e5f453b..06d4571a8 100644 --- a/pkg/analytics/segment_test.go +++ b/pkg/analytics/segment_test.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" "errors" - "io/ioutil" + "io" "net/http" "os" "sync" @@ -103,7 +103,7 @@ func EventuallyQueueNotEmpty[ITEM any](t *testing.T, queue *Queue[ITEM]) { } var emptyHttpResponse = &http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte{})), + Body: io.NopCloser(bytes.NewReader([]byte{})), StatusCode: http.StatusOK, } diff --git a/pkg/api/controllers/account_controller_test.go b/pkg/api/controllers/account_controller_test.go index 6fab312c3..f763f65b3 100644 --- a/pkg/api/controllers/account_controller_test.go +++ b/pkg/api/controllers/account_controller_test.go @@ -31,7 +31,7 @@ func TestGetAccounts(t *testing.T) { { Source: "world", Destination: "alice", - Amount: 150, + Amount: core.NewMonetaryInt(150), Asset: "USD", }, }, @@ -43,7 +43,7 @@ func TestGetAccounts(t *testing.T) { { Source: "world", Destination: "bob", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -422,7 +422,7 @@ func TestGetAccount(t *testing.T) { { Source: "world", Destination: "alice", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -448,11 +448,12 @@ func TestGetAccount(t *testing.T) { }, }, Balances: core.AssetsBalances{ - "USD": 100, + "USD": core.NewMonetaryInt(100), }, Volumes: core.AssetsVolumes{ "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, }, }, resp) @@ -499,7 +500,7 @@ func TestPostAccountMetadata(t *testing.T) { { Source: "world", Destination: "alice", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, diff --git a/pkg/api/controllers/balance_controller_test.go b/pkg/api/controllers/balance_controller_test.go index 54a1a4c39..a7e66edd7 100644 --- a/pkg/api/controllers/balance_controller_test.go +++ b/pkg/api/controllers/balance_controller_test.go @@ -26,7 +26,7 @@ func TestGetBalancesAggregated(t *testing.T) { { Source: "world", Destination: "alice", - Amount: 150, + Amount: core.NewMonetaryInt(150), Asset: "USD", }, }, @@ -38,7 +38,7 @@ func TestGetBalancesAggregated(t *testing.T) { { Source: "world", Destination: "bob", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -51,7 +51,7 @@ func TestGetBalancesAggregated(t *testing.T) { resp, ok := internal.DecodeSingleResponse[core.AssetsBalances](t, rsp.Body) assert.Equal(t, ok, true) - assert.Equal(t, core.AssetsBalances{"USD": 0}, resp) + assert.Equal(t, core.AssetsBalances{"USD": core.NewMonetaryInt(0)}, resp) }) t.Run("filter by address", func(t *testing.T) { @@ -60,7 +60,7 @@ func TestGetBalancesAggregated(t *testing.T) { resp, ok := internal.DecodeSingleResponse[core.AssetsBalances](t, rsp.Body) assert.Equal(t, true, ok) - assert.Equal(t, core.AssetsBalances{"USD": -250}, resp) + assert.Equal(t, core.AssetsBalances{"USD": core.NewMonetaryInt(-250)}, resp) }) t.Run("filter by address no result", func(t *testing.T) { @@ -87,7 +87,7 @@ func TestGetBalances(t *testing.T) { { Source: "world", Destination: "alice", - Amount: 150, + Amount: core.NewMonetaryInt(150), Asset: "USD", }, }, @@ -99,7 +99,7 @@ func TestGetBalances(t *testing.T) { { Source: "world", Destination: "bob", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -111,7 +111,7 @@ func TestGetBalances(t *testing.T) { { Source: "world", Destination: "alice", - Amount: 200, + Amount: core.NewMonetaryInt(200), Asset: "CAD", }, }, @@ -123,7 +123,7 @@ func TestGetBalances(t *testing.T) { { Source: "world", Destination: "alice", - Amount: 400, + Amount: core.NewMonetaryInt(400), Asset: "EUR", }, }, @@ -163,9 +163,9 @@ func TestGetBalances(t *testing.T) { resp := internal.DecodeCursorResponse[core.AccountsBalances](t, rsp.Body) assert.Equal(t, []core.AccountsBalances{ - {"world": core.AssetsBalances{"USD": -250, "EUR": -400, "CAD": -200}}, - {"bob": core.AssetsBalances{"USD": 100}}, - {"alice": core.AssetsBalances{"USD": 150, "EUR": 400, "CAD": 200}}, + {"world": core.AssetsBalances{"USD": core.NewMonetaryInt(-250), "EUR": core.NewMonetaryInt(-400), "CAD": core.NewMonetaryInt(-200)}}, + {"bob": core.AssetsBalances{"USD": core.NewMonetaryInt(100)}}, + {"alice": core.AssetsBalances{"USD": core.NewMonetaryInt(150), "EUR": core.NewMonetaryInt(400), "CAD": core.NewMonetaryInt(200)}}, }, resp.Data) }) @@ -175,7 +175,7 @@ func TestGetBalances(t *testing.T) { resp := internal.DecodeCursorResponse[core.AccountsBalances](t, rsp.Body) assert.Equal(t, []core.AccountsBalances{ - {"alice": core.AssetsBalances{"USD": 150, "EUR": 400, "CAD": 200}}, + {"alice": core.AssetsBalances{"USD": core.NewMonetaryInt(150), "EUR": core.NewMonetaryInt(400), "CAD": core.NewMonetaryInt(200)}}, }, resp.Data) }) @@ -185,7 +185,7 @@ func TestGetBalances(t *testing.T) { resp := internal.DecodeCursorResponse[core.AccountsBalances](t, rsp.Body) assert.Equal(t, []core.AccountsBalances{ - {"world": core.AssetsBalances{"USD": -250, "EUR": -400, "CAD": -200}}, + {"world": core.AssetsBalances{"USD": core.NewMonetaryInt(-250), "EUR": core.NewMonetaryInt(-400), "CAD": core.NewMonetaryInt(-200)}}, }, resp.Data) }) diff --git a/pkg/api/controllers/ledger_controller_test.go b/pkg/api/controllers/ledger_controller_test.go index dc026a5a1..4b7be7dd4 100644 --- a/pkg/api/controllers/ledger_controller_test.go +++ b/pkg/api/controllers/ledger_controller_test.go @@ -22,7 +22,7 @@ func TestGetStats(t *testing.T) { { Source: "world", Destination: "alice", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -34,7 +34,7 @@ func TestGetStats(t *testing.T) { { Source: "world", Destination: "boc", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, diff --git a/pkg/api/controllers/mapping_controller_test.go b/pkg/api/controllers/mapping_controller_test.go index f91bc5fcf..51cf5ff32 100644 --- a/pkg/api/controllers/mapping_controller_test.go +++ b/pkg/api/controllers/mapping_controller_test.go @@ -19,10 +19,7 @@ func TestMapping(t *testing.T) { m := core.Mapping{ Contracts: []core.Contract{ { - Expr: &core.ExprGt{ - Op1: core.VariableExpr{Name: "balance"}, - Op2: core.ConstantExpr{Value: float64(0)}, - }, + Name: "default", Account: "*", }, }, diff --git a/pkg/api/controllers/pagination_test.go b/pkg/api/controllers/pagination_test.go index 747355259..f0aa4b8b3 100644 --- a/pkg/api/controllers/pagination_test.go +++ b/pkg/api/controllers/pagination_test.go @@ -48,7 +48,7 @@ func testGetPagination(t *testing.T, api *api.API, txsPages, additionalTxs int) { Source: "world", Destination: fmt.Sprintf("accounts:%06d", i), - Amount: 10, + Amount: core.NewMonetaryInt(10), Asset: "USD", }, }, diff --git a/pkg/api/controllers/transaction_controller_test.go b/pkg/api/controllers/transaction_controller_test.go index b9ab027e3..e3671da3b 100644 --- a/pkg/api/controllers/transaction_controller_test.go +++ b/pkg/api/controllers/transaction_controller_test.go @@ -47,7 +47,7 @@ func TestPostTransactions(t *testing.T) { { Source: "world", Destination: "central_bank", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "USB", }, }, @@ -74,7 +74,7 @@ func TestPostTransactions(t *testing.T) { { Source: "world", Destination: "central_bank", - Amount: -1000, + Amount: core.NewMonetaryInt(-1000), Asset: "USB", }, }, @@ -91,7 +91,7 @@ func TestPostTransactions(t *testing.T) { { Source: "world", Destination: "central_bank", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "@TOK", }, }, @@ -108,7 +108,7 @@ func TestPostTransactions(t *testing.T) { { Source: "world", Destination: "#fake", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "TOK", }, }, @@ -125,7 +125,7 @@ func TestPostTransactions(t *testing.T) { { Source: "foo", Destination: "bar", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "TOK", }, }, @@ -142,7 +142,7 @@ func TestPostTransactions(t *testing.T) { { Source: "world", Destination: "bar", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "TOK", }, }, @@ -153,7 +153,7 @@ func TestPostTransactions(t *testing.T) { { Source: "world", Destination: "bar", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "TOK", }, }, @@ -170,7 +170,7 @@ func TestPostTransactions(t *testing.T) { { Source: "world", Destination: "bar", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "TOK", }, }, @@ -188,7 +188,7 @@ func TestPostTransactions(t *testing.T) { { Source: "world", Destination: "bar", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "TOK", }, }, @@ -199,7 +199,7 @@ func TestPostTransactions(t *testing.T) { { Source: "world", Destination: "bar", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "TOK", }, }, @@ -280,7 +280,7 @@ func TestGetTransaction(t *testing.T) { { Source: "world", Destination: "central_bank", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "USD", }, }, @@ -297,7 +297,7 @@ func TestGetTransaction(t *testing.T) { { Source: "world", Destination: "central_bank", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "USD", }, }, ret.Postings) @@ -307,21 +307,29 @@ func TestGetTransaction(t *testing.T) { assert.NotEmpty(t, ret.Timestamp) assert.EqualValues(t, core.AccountsAssetsVolumes{ "world": core.AssetsVolumes{ - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, "central_bank": core.AssetsVolumes{ - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, }, ret.PreCommitVolumes) assert.EqualValues(t, core.AccountsAssetsVolumes{ "world": core.AssetsVolumes{ "USD": { - Output: 1000, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(1000), }, }, "central_bank": core.AssetsVolumes{ "USD": { - Input: 1000, + Input: core.NewMonetaryInt(1000), + Output: core.NewMonetaryInt(0), }, }, }, ret.PostCommitVolumes) @@ -366,7 +374,7 @@ func TestPreviewTransaction(t *testing.T) { { Source: "world", Destination: "central_bank", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "USD", }, }, @@ -391,7 +399,7 @@ func TestGetTransactions(t *testing.T) { { Source: "world", Destination: "central_bank1", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "USD", }, }, @@ -408,7 +416,7 @@ func TestGetTransactions(t *testing.T) { { Source: "world", Destination: "central_bank2", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "USD", }, }, @@ -428,7 +436,7 @@ func TestGetTransactions(t *testing.T) { { Source: "central_bank1", Destination: "alice", - Amount: 10, + Amount: core.NewMonetaryInt(10), Asset: "USD", }, }, @@ -679,7 +687,7 @@ func TestGetTransactionsWithPageSize(t *testing.T) { { Source: "world", Destination: fmt.Sprintf("account:%d", i), - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "USD", }, }, @@ -761,7 +769,7 @@ func TestTransactionsVolumes(t *testing.T) { OnStart: func(ctx context.Context) error { // Single posting - single asset - const worldAliceUSD int64 = 100 + worldAliceUSD := core.NewMonetaryInt(100) rsp := internal.PostTransaction(t, api, core.TransactionData{ @@ -781,10 +789,18 @@ func TestTransactionsVolumes(t *testing.T) { expPreVolumes := accountsVolumes{ "alice": assetsVolumes{ - "USD": core.VolumesWithBalance{}, + "USD": core.VolumesWithBalance{ + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + Balance: core.NewMonetaryInt(0), + }, }, "world": assetsVolumes{ - "USD": core.VolumesWithBalance{}, + "USD": core.VolumesWithBalance{ + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + Balance: core.NewMonetaryInt(0), + }, }, } @@ -792,13 +808,15 @@ func TestTransactionsVolumes(t *testing.T) { "alice": assetsVolumes{ "USD": core.VolumesWithBalance{ Input: worldAliceUSD, + Output: core.NewMonetaryInt(0), Balance: worldAliceUSD, }, }, "world": assetsVolumes{ "USD": core.VolumesWithBalance{ + Input: core.NewMonetaryInt(0), Output: worldAliceUSD, - Balance: -worldAliceUSD, + Balance: worldAliceUSD.Neg(), }, }, } @@ -818,7 +836,7 @@ func TestTransactionsVolumes(t *testing.T) { // Single posting - single asset - const aliceBobUSD int64 = 93 + aliceBobUSD := core.NewMonetaryInt(93) rsp = internal.PostTransaction(t, api, core.TransactionData{ @@ -841,7 +859,11 @@ func TestTransactionsVolumes(t *testing.T) { "USD": prevVolAliceUSD, }, "bob": assetsVolumes{ - "USD": core.VolumesWithBalance{}, + "USD": core.VolumesWithBalance{ + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + Balance: core.NewMonetaryInt(0), + }, }, } @@ -849,13 +871,14 @@ func TestTransactionsVolumes(t *testing.T) { "alice": assetsVolumes{ "USD": core.VolumesWithBalance{ Input: prevVolAliceUSD.Input, - Output: prevVolAliceUSD.Output + aliceBobUSD, - Balance: prevVolAliceUSD.Input - prevVolAliceUSD.Output - aliceBobUSD, + Output: prevVolAliceUSD.Output.Add(aliceBobUSD), + Balance: prevVolAliceUSD.Input.Sub(prevVolAliceUSD.Output).Sub(aliceBobUSD), }, }, "bob": assetsVolumes{ "USD": core.VolumesWithBalance{ Input: aliceBobUSD, + Output: core.NewMonetaryInt(0), Balance: aliceBobUSD, }, }, @@ -877,8 +900,8 @@ func TestTransactionsVolumes(t *testing.T) { // Multi posting - single asset - const worldBobEUR int64 = 156 - const bobAliceEUR int64 = 3 + worldBobEUR := core.NewMonetaryInt(156) + bobAliceEUR := core.NewMonetaryInt(3) rsp = internal.PostTransaction(t, api, core.TransactionData{ @@ -904,13 +927,25 @@ func TestTransactionsVolumes(t *testing.T) { expPreVolumes = accountsVolumes{ "alice": assetsVolumes{ - "EUR": core.VolumesWithBalance{}, + "EUR": core.VolumesWithBalance{ + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + Balance: core.NewMonetaryInt(0), + }, }, "bob": assetsVolumes{ - "EUR": core.VolumesWithBalance{}, + "EUR": core.VolumesWithBalance{ + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + Balance: core.NewMonetaryInt(0), + }, }, "world": assetsVolumes{ - "EUR": core.VolumesWithBalance{}, + "EUR": core.VolumesWithBalance{ + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + Balance: core.NewMonetaryInt(0), + }, }, } @@ -918,7 +953,7 @@ func TestTransactionsVolumes(t *testing.T) { "alice": assetsVolumes{ "EUR": core.VolumesWithBalance{ Input: bobAliceEUR, - Output: 0, + Output: core.NewMonetaryInt(0), Balance: bobAliceEUR, }, }, @@ -926,14 +961,14 @@ func TestTransactionsVolumes(t *testing.T) { "EUR": core.VolumesWithBalance{ Input: worldBobEUR, Output: bobAliceEUR, - Balance: worldBobEUR - bobAliceEUR, + Balance: worldBobEUR.Sub(bobAliceEUR), }, }, "world": assetsVolumes{ "EUR": core.VolumesWithBalance{ - Input: 0, + Input: core.NewMonetaryInt(0), Output: worldBobEUR, - Balance: -worldBobEUR, + Balance: worldBobEUR.Neg(), }, }, } @@ -954,8 +989,8 @@ func TestTransactionsVolumes(t *testing.T) { // Multi postings - multi assets - const bobAliceUSD int64 = 1 - const aliceBobEUR int64 = 2 + bobAliceUSD := core.NewMonetaryInt(1) + aliceBobEUR := core.NewMonetaryInt(2) rsp = internal.PostTransaction(t, api, core.TransactionData{ @@ -994,25 +1029,25 @@ func TestTransactionsVolumes(t *testing.T) { "alice": assetsVolumes{ "EUR": core.VolumesWithBalance{ Input: prevVolAliceEUR.Input, - Output: prevVolAliceEUR.Output + aliceBobEUR, - Balance: prevVolAliceEUR.Balance - aliceBobEUR, + Output: prevVolAliceEUR.Output.Add(aliceBobEUR), + Balance: prevVolAliceEUR.Balance.Sub(aliceBobEUR), }, "USD": core.VolumesWithBalance{ - Input: prevVolAliceUSD.Input + bobAliceUSD, + Input: prevVolAliceUSD.Input.Add(bobAliceUSD), Output: prevVolAliceUSD.Output, - Balance: prevVolAliceUSD.Balance + bobAliceUSD, + Balance: prevVolAliceUSD.Balance.Add(bobAliceUSD), }, }, "bob": assetsVolumes{ "EUR": core.VolumesWithBalance{ - Input: prevVolBobEUR.Input + aliceBobEUR, + Input: prevVolBobEUR.Input.Add(aliceBobEUR), Output: prevVolBobEUR.Output, - Balance: prevVolBobEUR.Balance + aliceBobEUR, + Balance: prevVolBobEUR.Balance.Add(aliceBobEUR), }, "USD": core.VolumesWithBalance{ Input: prevVolBobUSD.Input, - Output: prevVolBobUSD.Output + bobAliceUSD, - Balance: prevVolBobUSD.Balance - bobAliceUSD, + Output: prevVolBobUSD.Output.Add(bobAliceUSD), + Balance: prevVolBobUSD.Balance.Sub(bobAliceUSD), }, }, } @@ -1043,7 +1078,7 @@ func TestPostTransactionMetadata(t *testing.T) { { Source: "world", Destination: "central_bank", - Amount: 1000, + Amount: core.NewMonetaryInt(1000), Asset: "USD", }, }, @@ -1166,7 +1201,7 @@ func TestRevertTransaction(t *testing.T) { { Source: "world", Destination: "alice", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -1182,7 +1217,7 @@ func TestRevertTransaction(t *testing.T) { { Source: "world", Destination: "bob", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -1198,7 +1233,7 @@ func TestRevertTransaction(t *testing.T) { { Source: "alice", Destination: "bob", - Amount: 3, + Amount: core.NewMonetaryInt(3), Asset: "USD", }, }, @@ -1295,7 +1330,7 @@ func TestPostTransactionsBatch(t *testing.T) { { Source: "world", Destination: "alice", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -1305,7 +1340,7 @@ func TestPostTransactionsBatch(t *testing.T) { { Source: "world", Destination: "bob", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -1330,7 +1365,7 @@ func TestPostTransactionsBatch(t *testing.T) { { Source: "world", Destination: "alice", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, diff --git a/pkg/core/contract.go b/pkg/core/contract.go index 4d6bb4091..43e2c636d 100644 --- a/pkg/core/contract.go +++ b/pkg/core/contract.go @@ -1,38 +1,15 @@ package core import ( - "encoding/json" "regexp" "strings" ) type Contract struct { - Expr Expr `json:"expr"` + Name string `json:"name"` Account string `json:"account"` } -func (c *Contract) UnmarshalJSON(data []byte) error { - type AuxContract Contract - type Aux struct { - AuxContract - Expr map[string]interface{} `json:"expr"` - } - aux := Aux{} - err := json.Unmarshal(data, &aux) - if err != nil { - return err - } - expr, err := ParseRuleExpr(aux.Expr) - if err != nil { - return err - } - *c = Contract{ - Expr: expr, - Account: aux.Account, - } - return nil -} - func (c Contract) Match(addr string) bool { r := strings.ReplaceAll(c.Account, "*", ".*") return regexp.MustCompile(r).Match([]byte(addr)) diff --git a/pkg/core/expr.go b/pkg/core/expr.go deleted file mode 100644 index 42c3bed41..000000000 --- a/pkg/core/expr.go +++ /dev/null @@ -1,294 +0,0 @@ -package core - -import ( - "encoding/json" - "errors" - "fmt" - "reflect" - "strings" -) - -type EvalContext struct { - Variables map[string]interface{} - Metadata Metadata - Asset string -} - -type Expr interface { - Eval(EvalContext) bool -} - -type Value interface { - eval(ctx EvalContext) interface{} -} - -type ExprOr []Expr - -func (o ExprOr) Eval(ctx EvalContext) bool { - for _, e := range o { - if e.Eval(ctx) { - return true - } - } - return false -} - -func (e ExprOr) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "$or": []Expr(e), - }) -} - -type ExprAnd []Expr - -func (o ExprAnd) Eval(ctx EvalContext) bool { - for _, e := range o { - if !e.Eval(ctx) { - return false - } - } - return true -} - -func (e ExprAnd) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "$and": []Expr(e), - }) -} - -type ExprEq struct { - Op1 Value - Op2 Value -} - -func (o *ExprEq) Eval(ctx EvalContext) bool { - return reflect.DeepEqual(o.Op1.eval(ctx), o.Op2.eval(ctx)) -} - -func (e ExprEq) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "$eq": []interface{}{e.Op1, e.Op2}, - }) -} - -type ExprGt struct { - Op1 Value - Op2 Value -} - -func (o *ExprGt) Eval(ctx EvalContext) bool { - return o.Op1.eval(ctx).(float64) > o.Op2.eval(ctx).(float64) -} - -func (e ExprGt) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "$gt": []interface{}{e.Op1, e.Op2}, - }) -} - -type ExprLt struct { - Op1 Value - Op2 Value -} - -func (o *ExprLt) Eval(ctx EvalContext) bool { - return o.Op1.eval(ctx).(float64) < o.Op2.eval(ctx).(float64) -} - -func (e ExprLt) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "$lt": []interface{}{e.Op1, e.Op2}, - }) -} - -type ExprGte struct { - Op1 Value - Op2 Value -} - -func (o *ExprGte) Eval(ctx EvalContext) bool { - return o.Op1.eval(ctx).(float64) >= o.Op2.eval(ctx).(float64) -} - -func (e ExprGte) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "$gte": []interface{}{e.Op1, e.Op2}, - }) -} - -type ExprLte struct { - Op1 Value - Op2 Value -} - -func (o *ExprLte) Eval(ctx EvalContext) bool { - return o.Op1.eval(ctx).(float64) <= o.Op2.eval(ctx).(float64) -} - -func (e ExprLte) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "$lte": []interface{}{e.Op1, e.Op2}, - }) -} - -type ConstantExpr struct { - Value interface{} -} - -func (e ConstantExpr) eval(ctx EvalContext) interface{} { - return e.Value -} - -func (e ConstantExpr) MarshalJSON() ([]byte, error) { - return json.Marshal(e.Value) -} - -type VariableExpr struct { - Name string -} - -func (e VariableExpr) eval(ctx EvalContext) interface{} { - return ctx.Variables[e.Name] -} - -func (e VariableExpr) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf(`"$%s"`, e.Name)), nil -} - -type MetaExpr struct { - Name string -} - -func (e MetaExpr) eval(ctx EvalContext) interface{} { - return ctx.Metadata[e.Name] -} - -func (e MetaExpr) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "$meta": e.Name, - }) -} - -func parse(v interface{}) (expr interface{}, err error) { - switch vv := v.(type) { - case map[string]interface{}: - if len(vv) != 1 { - return nil, errors.New("malformed expression") - } - for key, vvv := range vv { - switch { - case strings.HasPrefix(key, "$"): - switch key { - case "$meta": - value, ok := vvv.(string) - if !ok { - return nil, errors.New("$meta operator invalid") - } - return &MetaExpr{Name: value}, nil - case "$or", "$and": - slice, ok := vvv.([]interface{}) - if !ok { - return nil, errors.New("Expected slice for operator " + key) - } - exprs := make([]Expr, 0) - for _, item := range slice { - r, err := parse(item) - if err != nil { - return nil, err - } - expr, ok := r.(Expr) - if !ok { - return nil, errors.New("unexpected value when parsing " + key) - } - exprs = append(exprs, expr) - } - switch key { - case "$and": - expr = ExprAnd(exprs) - case "$or": - expr = ExprOr(exprs) - } - case "$eq", "$gt", "$gte", "$lt", "$lte": - vv, ok := vvv.([]interface{}) - if !ok { - return nil, errors.New("expected array when using $eq") - } - if len(vv) != 2 { - return nil, errors.New("expected 2 items when using $eq") - } - op1, err := parse(vv[0]) - if err != nil { - return nil, err - } - op1Value, ok := op1.(Value) - if !ok { - return nil, errors.New("op1 must be valuable") - } - op2, err := parse(vv[1]) - if err != nil { - return nil, err - } - op2Value, ok := op2.(Value) - if !ok { - return nil, errors.New("op2 must be valuable") - } - switch key { - case "$eq": - expr = &ExprEq{ - Op1: op1Value, - Op2: op2Value, - } - case "$gt": - expr = &ExprGt{ - Op1: op1Value, - Op2: op2Value, - } - case "$gte": - expr = &ExprGte{ - Op1: op1Value, - Op2: op2Value, - } - case "$lt": - expr = &ExprLt{ - Op1: op1Value, - Op2: op2Value, - } - case "$lte": - expr = &ExprLte{ - Op1: op1Value, - Op2: op2Value, - } - } - default: - return nil, errors.New("unknown operator '" + key + "'") - } - } - } - case string: - if !strings.HasPrefix(vv, "$") { - return ConstantExpr{v}, nil - } - return VariableExpr{vv[1:]}, nil - default: - return ConstantExpr{v}, nil - } - - return expr, nil -} - -func ParseRuleExpr(v map[string]interface{}) (Expr, error) { - ret, err := parse(v) - if err != nil { - return nil, err - } - return ret.(Expr), nil -} - -func ParseRule(data string) (Expr, error) { - m := make(map[string]interface{}) - err := json.Unmarshal([]byte(data), &m) - if err != nil { - return nil, err - } - return ParseRuleExpr(m) -} diff --git a/pkg/core/expr_test.go b/pkg/core/expr_test.go deleted file mode 100644 index 02a353fda..000000000 --- a/pkg/core/expr_test.go +++ /dev/null @@ -1,136 +0,0 @@ -package core - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRules(t *testing.T) { - - type testCase struct { - rule map[string]interface{} - context EvalContext - shouldBeAccepted bool - } - - var tests = []testCase{ - { - rule: map[string]interface{}{ - "$or": []interface{}{ - map[string]interface{}{ - "$gt": []interface{}{ - "$balance", float64(0), - }, - }, - map[string]interface{}{ - "$eq": []interface{}{ - map[string]interface{}{ - "$meta": "approved", - }, - "yes", - }, - }, - }, - }, - context: EvalContext{ - Variables: map[string]interface{}{ - "balance": float64(-10), - }, - Metadata: Metadata{ - "approved": "yes", - }, - }, - shouldBeAccepted: true, - }, - { - rule: map[string]interface{}{ - "$or": []interface{}{ - map[string]interface{}{ - "$gte": []interface{}{ - "$balance", float64(0), - }, - }, - map[string]interface{}{ - "$lte": []interface{}{ - "$balance", float64(0), - }, - }, - }, - }, - context: EvalContext{ - Variables: map[string]interface{}{ - "balance": float64(-100), - }, - Metadata: Metadata{}, - }, - shouldBeAccepted: true, - }, - { - rule: map[string]interface{}{ - "$lt": []interface{}{ - "$balance", float64(0), - }, - }, - context: EvalContext{ - Variables: map[string]interface{}{ - "balance": float64(100), - }, - Metadata: Metadata{}, - }, - shouldBeAccepted: false, - }, - { - rule: map[string]interface{}{ - "$lte": []interface{}{ - "$balance", float64(0), - }, - }, - context: EvalContext{ - Variables: map[string]interface{}{ - "balance": float64(0), - }, - Metadata: Metadata{}, - }, - shouldBeAccepted: true, - }, - { - rule: map[string]interface{}{ - "$and": []interface{}{ - map[string]interface{}{ - "$gt": []interface{}{ - "$balance", float64(0), - }, - }, - map[string]interface{}{ - "$eq": []interface{}{ - map[string]interface{}{ - "$meta": "approved", - }, - "yes", - }, - }, - }, - }, - context: EvalContext{ - Variables: map[string]interface{}{ - "balance": float64(10), - }, - Metadata: Metadata{ - "approved": "no", - }, - }, - shouldBeAccepted: false, - }, - } - - for i, test := range tests { - t.Run(fmt.Sprintf("test%d", i), func(t *testing.T) { - r, err := ParseRuleExpr(test.rule) - assert.NoError(t, err) - assert.Equal(t, test.shouldBeAccepted, r.Eval(test.context)) - }) - } - -} diff --git a/pkg/core/log.go b/pkg/core/log.go index 4e7a5413c..65d6ad66d 100644 --- a/pkg/core/log.go +++ b/pkg/core/log.go @@ -1,11 +1,10 @@ package core import ( + "encoding/json" "strconv" "strings" "time" - - json "github.com/gibson042/canonicaljson-go" ) const SetMetadataType = "SET_METADATA" diff --git a/pkg/core/log_test.go b/pkg/core/log_test.go index 111389689..69d5c3865 100644 --- a/pkg/core/log_test.go +++ b/pkg/core/log_test.go @@ -23,7 +23,7 @@ func TestLog(t *testing.T) { Metadata: Metadata{}, }, }, d) - if !assert.Equal(t, "3070ef3437354b5cb5ece914f8610d8d1276c6a9df127c0d2a49c48e3f81b017", log2.Hash) { + if !assert.Equal(t, "9ee060170400f556b7e1575cb13f9db004f150a08355c7431c62bc639166431e", log2.Hash) { return } } @@ -37,7 +37,7 @@ func TestLogProcessor(t *testing.T) { { Source: "world", Destination: "orders:1234", - Amount: 100, + Amount: NewMonetaryInt(100), Asset: "USD", }, }, @@ -51,13 +51,13 @@ func TestLogProcessor(t *testing.T) { { Source: "orders:1234", Destination: "merchant:1234", - Amount: 90, + Amount: NewMonetaryInt(90), Asset: "USD", }, { Source: "orders:1234", Destination: "fees", - Amount: 10, + Amount: NewMonetaryInt(10), Asset: "USD", }, }, @@ -103,7 +103,7 @@ func TestLogProcessor(t *testing.T) { { Source: "world", Destination: "orders:1234", - Amount: 100, + Amount: NewMonetaryInt(100), Asset: "USD", }, }, @@ -116,28 +116,28 @@ func TestLogProcessor(t *testing.T) { PreCommitVolumes: AccountsAssetsVolumes{ "world": { "USD": { - Input: 0, - Output: 0, + Input: NewMonetaryInt(0), + Output: NewMonetaryInt(0), }, }, "orders:1234": { "USD": { - Input: 0, - Output: 0, + Input: NewMonetaryInt(0), + Output: NewMonetaryInt(0), }, }, }, PostCommitVolumes: AccountsAssetsVolumes{ "world": { "USD": { - Input: 0, - Output: 100, + Input: NewMonetaryInt(0), + Output: NewMonetaryInt(100), }, }, "orders:1234": { "USD": { - Input: 100, - Output: 0, + Input: NewMonetaryInt(100), + Output: NewMonetaryInt(0), }, }, }, @@ -149,13 +149,13 @@ func TestLogProcessor(t *testing.T) { { Source: "orders:1234", Destination: "merchant:1234", - Amount: 90, + Amount: NewMonetaryInt(90), Asset: "USD", }, { Source: "orders:1234", Destination: "fees", - Amount: 10, + Amount: NewMonetaryInt(10), Asset: "USD", }, }, @@ -166,40 +166,40 @@ func TestLogProcessor(t *testing.T) { PreCommitVolumes: AccountsAssetsVolumes{ "orders:1234": { "USD": { - Input: 100, - Output: 0, + Input: NewMonetaryInt(100), + Output: NewMonetaryInt(0), }, }, "merchant:1234": { "USD": { - Input: 0, - Output: 0, + Input: NewMonetaryInt(0), + Output: NewMonetaryInt(0), }, }, "fees": { "USD": { - Input: 0, - Output: 0, + Input: NewMonetaryInt(0), + Output: NewMonetaryInt(0), }, }, }, PostCommitVolumes: AccountsAssetsVolumes{ "orders:1234": { "USD": { - Input: 100, - Output: 100, + Input: NewMonetaryInt(100), + Output: NewMonetaryInt(100), }, }, "merchant:1234": { "USD": { - Input: 90, - Output: 0, + Input: NewMonetaryInt(90), + Output: NewMonetaryInt(0), }, }, "fees": { "USD": { - Input: 10, - Output: 0, + Input: NewMonetaryInt(10), + Output: NewMonetaryInt(0), }, }, }, @@ -208,30 +208,30 @@ func TestLogProcessor(t *testing.T) { require.Equal(t, AccountsAssetsVolumes{ "world": { "USD": { - Input: 0, - Output: 100, + Input: NewMonetaryInt(0), + Output: NewMonetaryInt(100), }, }, "orders:1234": { "USD": { - Input: 100, - Output: 100, + Input: NewMonetaryInt(100), + Output: NewMonetaryInt(100), }, }, "merchant:1234": { "USD": { - Input: 90, - Output: 0, + Input: NewMonetaryInt(90), + Output: NewMonetaryInt(0), }, }, "fees": { "USD": { - Input: 10, - Output: 0, + Input: NewMonetaryInt(10), + Output: NewMonetaryInt(0), }, }, }, p.Volumes) - require.Equal(t, Accounts{ + require.EqualValues(t, Accounts{ "world": { Address: "world", Metadata: Metadata{}, diff --git a/pkg/core/monetary.go b/pkg/core/monetary.go new file mode 100644 index 000000000..36b7889ab --- /dev/null +++ b/pkg/core/monetary.go @@ -0,0 +1,120 @@ +package core + +import ( + "errors" + "math/big" +) + +type MonetaryInt big.Int + +func (a *MonetaryInt) Add(b *MonetaryInt) *MonetaryInt { + if a == nil { + a = NewMonetaryInt(0) + } + + if b == nil { + b = NewMonetaryInt(0) + } + + return (*MonetaryInt)(big.NewInt(0).Add((*big.Int)(a), (*big.Int)(b))) +} + +func (a *MonetaryInt) Sub(b *MonetaryInt) *MonetaryInt { + if a == nil { + a = NewMonetaryInt(0) + } + + if b == nil { + b = NewMonetaryInt(0) + } + + return (*MonetaryInt)(big.NewInt(0).Sub((*big.Int)(a), (*big.Int)(b))) +} + +func (a *MonetaryInt) Neg() *MonetaryInt { + return (*MonetaryInt)(big.NewInt(0).Neg((*big.Int)(a))) +} + +func (a *MonetaryInt) OrZero() *MonetaryInt { + if a == nil { + return NewMonetaryInt(0) + } + + return a +} + +func (a *MonetaryInt) Lte(b *MonetaryInt) bool { + return (*big.Int)(a).Cmp((*big.Int)(b)) <= 0 +} + +func (a *MonetaryInt) Gte(b *MonetaryInt) bool { + return (*big.Int)(a).Cmp((*big.Int)(b)) >= 0 +} + +func (a *MonetaryInt) Lt(b *MonetaryInt) bool { + return (*big.Int)(a).Cmp((*big.Int)(b)) < 0 +} + +func (a *MonetaryInt) Ltz() bool { + return (*big.Int)(a).Cmp(big.NewInt(0)) < 0 +} + +func (a *MonetaryInt) Gt(b *MonetaryInt) bool { + return (*big.Int)(a).Cmp((*big.Int)(b)) > 0 +} + +func (a *MonetaryInt) Eq(b *MonetaryInt) bool { + return (*big.Int)(a).Cmp((*big.Int)(b)) == 0 +} + +func (a *MonetaryInt) Equal(b *MonetaryInt) bool { + return (*big.Int)(a).Cmp((*big.Int)(b)) == 0 +} + +func (a *MonetaryInt) Cmp(b *MonetaryInt) int { + return (*big.Int)(a).Cmp((*big.Int)(b)) +} + +func (a *MonetaryInt) Uint64() uint64 { + return (*big.Int)(a).Uint64() +} + +func (a *MonetaryInt) String() string { + if a == nil { + return "0" + } + + return (*big.Int)(a).String() +} + +func (a *MonetaryInt) UnmarshalJSON(b []byte) error { + return (*big.Int)(a).UnmarshalJSON(b) +} + +func (a *MonetaryInt) MarshalJSON() ([]byte, error) { + if a == nil { + return []byte("0"), nil + } + return (*big.Int)(a).MarshalJSON() +} + +func (a *MonetaryInt) MarshalText() ([]byte, error) { + return (*big.Int)(a).MarshalText() +} + +func (a *MonetaryInt) UnmarshalText(b []byte) error { + return (*big.Int)(a).UnmarshalText(b) +} + +func NewMonetaryInt(i int64) *MonetaryInt { + return (*MonetaryInt)(big.NewInt(i)) +} + +func ParseMonetaryInt(s string) (*MonetaryInt, error) { + i, ok := big.NewInt(0).SetString(s, 10) + if !ok { + return nil, errors.New("invalid monetary int") + } + + return (*MonetaryInt)(i), nil +} diff --git a/pkg/core/posting.go b/pkg/core/posting.go index 300fe801a..4110b20e1 100644 --- a/pkg/core/posting.go +++ b/pkg/core/posting.go @@ -7,10 +7,10 @@ import ( ) type Posting struct { - Source string `json:"source"` - Destination string `json:"destination"` - Amount int64 `json:"amount"` - Asset string `json:"asset"` + Source string `json:"source"` + Destination string `json:"destination"` + Amount *MonetaryInt `json:"amount"` + Asset string `json:"asset"` } type Postings []Posting diff --git a/pkg/core/posting_test.go b/pkg/core/posting_test.go index b7d81d74d..06a0b3f9d 100644 --- a/pkg/core/posting_test.go +++ b/pkg/core/posting_test.go @@ -11,13 +11,13 @@ func TestReverseMultiple(t *testing.T) { { Source: "world", Destination: "users:001", - Amount: 100, + Amount: NewMonetaryInt(100), Asset: "COIN", }, { Source: "users:001", Destination: "payments:001", - Amount: 100, + Amount: NewMonetaryInt(100), Asset: "COIN", }, } @@ -26,13 +26,13 @@ func TestReverseMultiple(t *testing.T) { { Source: "payments:001", Destination: "users:001", - Amount: 100, + Amount: NewMonetaryInt(100), Asset: "COIN", }, { Source: "users:001", Destination: "world", - Amount: 100, + Amount: NewMonetaryInt(100), Asset: "COIN", }, } @@ -49,7 +49,7 @@ func TestReverseSingle(t *testing.T) { { Source: "world", Destination: "users:001", - Amount: 100, + Amount: NewMonetaryInt(100), Asset: "COIN", }, } @@ -58,7 +58,7 @@ func TestReverseSingle(t *testing.T) { { Source: "users:001", Destination: "world", - Amount: 100, + Amount: NewMonetaryInt(100), Asset: "COIN", }, } diff --git a/pkg/core/transaction.go b/pkg/core/transaction.go index 5ec81362a..bf5d3e5e9 100644 --- a/pkg/core/transaction.go +++ b/pkg/core/transaction.go @@ -2,10 +2,9 @@ package core import ( "crypto/sha256" + "encoding/json" "fmt" "time" - - json "github.com/gibson042/canonicaljson-go" ) type Transactions struct { diff --git a/pkg/core/transaction_test.go b/pkg/core/transaction_test.go index 9016dc697..7cc935e85 100644 --- a/pkg/core/transaction_test.go +++ b/pkg/core/transaction_test.go @@ -14,13 +14,13 @@ func TestReverseTransaction(t *testing.T) { { Source: "world", Destination: "users:001", - Amount: 100, + Amount: NewMonetaryInt(100), Asset: "COIN", }, { Source: "users:001", Destination: "payments:001", - Amount: 100, + Amount: NewMonetaryInt(100), Asset: "COIN", }, }, @@ -34,13 +34,13 @@ func TestReverseTransaction(t *testing.T) { { Source: "payments:001", Destination: "users:001", - Amount: 100, + Amount: NewMonetaryInt(100), Asset: "COIN", }, { Source: "users:001", Destination: "world", - Amount: 100, + Amount: NewMonetaryInt(100), Asset: "COIN", }, }, diff --git a/pkg/core/volumes.go b/pkg/core/volumes.go index 6643fc876..0888fbf04 100644 --- a/pkg/core/volumes.go +++ b/pkg/core/volumes.go @@ -6,29 +6,29 @@ import ( ) type Volumes struct { - Input int64 `json:"input"` - Output int64 `json:"output"` + Input *MonetaryInt `json:"input"` + Output *MonetaryInt `json:"output"` } type VolumesWithBalance struct { - Input int64 `json:"input"` - Output int64 `json:"output"` - Balance int64 `json:"balance"` + Input *MonetaryInt `json:"input"` + Output *MonetaryInt `json:"output"` + Balance *MonetaryInt `json:"balance"` } func (v Volumes) MarshalJSON() ([]byte, error) { return json.Marshal(VolumesWithBalance{ Input: v.Input, Output: v.Output, - Balance: v.Input - v.Output, + Balance: v.Input.Sub(v.Output), }) } -func (v Volumes) Balance() int64 { - return v.Input - v.Output +func (v Volumes) Balance() *MonetaryInt { + return v.Input.Sub(v.Output) } -type AssetsBalances map[string]int64 +type AssetsBalances map[string]*MonetaryInt type AssetsVolumes map[string]Volumes type AccountsBalances map[string]AssetsBalances @@ -36,7 +36,7 @@ type AccountsBalances map[string]AssetsBalances func (v AssetsVolumes) Balances() AssetsBalances { balances := AssetsBalances{} for asset, vv := range v { - balances[asset] = vv.Input - vv.Output + balances[asset] = vv.Input.Sub(vv.Output) } return balances } @@ -45,46 +45,60 @@ type AccountsAssetsVolumes map[string]AssetsVolumes func (a AccountsAssetsVolumes) GetVolumes(account, asset string) Volumes { if assetsVolumes, ok := a[account]; !ok { - return Volumes{} + return Volumes{ + Input: NewMonetaryInt(0), + Output: NewMonetaryInt(0), + } } else { - return assetsVolumes[asset] + return Volumes{ + Input: assetsVolumes[asset].Input.OrZero(), + Output: assetsVolumes[asset].Output.OrZero(), + } } } func (a AccountsAssetsVolumes) SetVolumes(account, asset string, volumes Volumes) { if assetsVolumes, ok := a[account]; !ok { a[account] = map[string]Volumes{ - asset: volumes, + asset: { + Input: volumes.Input.OrZero(), + Output: volumes.Output.OrZero(), + }, } } else { - assetsVolumes[asset] = volumes + assetsVolumes[asset] = Volumes{ + Input: volumes.Input.OrZero(), + Output: volumes.Output.OrZero(), + } } } -func (a AccountsAssetsVolumes) AddInput(account, asset string, input int64) { +func (a AccountsAssetsVolumes) AddInput(account, asset string, input *MonetaryInt) { if assetsVolumes, ok := a[account]; !ok { a[account] = map[string]Volumes{ asset: { - Input: input, + Input: input.OrZero(), + Output: NewMonetaryInt(0), }, } } else { volumes := assetsVolumes[asset] - volumes.Input += input + volumes.Input = volumes.Input.Add(input) assetsVolumes[asset] = volumes } } -func (a AccountsAssetsVolumes) AddOutput(account, asset string, output int64) { +func (a AccountsAssetsVolumes) AddOutput(account, asset string, output *MonetaryInt) { if assetsVolumes, ok := a[account]; !ok { a[account] = map[string]Volumes{ asset: { - Output: output, + Output: output.OrZero(), + Input: NewMonetaryInt(0), }, } } else { volumes := assetsVolumes[asset] - volumes.Output += output + volumes.Output = volumes.Output.Add(output) assetsVolumes[asset] = volumes } } diff --git a/pkg/ledger/executor.go b/pkg/ledger/executor.go index 8ac1b77fe..38ca6d04c 100644 --- a/pkg/ledger/executor.go +++ b/pkg/ledger/executor.go @@ -22,7 +22,7 @@ func (l *Ledger) execute(ctx context.Context, script core.Script) (*core.Transac return nil, NewScriptError(ScriptErrorCompilationFailed, err.Error()) } - m := vm.NewMachine(p) + m := vm.NewMachine(*p) err = m.SetVarsFromJSON(script.Vars) if err != nil { @@ -73,10 +73,7 @@ func (l *Ledger) execute(ctx context.Context, script core.Script) (*core.Transac return nil, fmt.Errorf("could not get account %q: %v", req.Account, err) } amt := account.Balances[req.Asset] - if amt < 0 { - amt = 0 - } - req.Response <- uint64(amt) + req.Response <- *amt.OrZero() } } diff --git a/pkg/ledger/executor_test.go b/pkg/ledger/executor_test.go index 8d5e7a773..7ff97d741 100644 --- a/pkg/ledger/executor_test.go +++ b/pkg/ledger/executor_test.go @@ -10,13 +10,13 @@ import ( "github.com/stretchr/testify/require" ) -func assertBalance(t *testing.T, l *Ledger, account, asset string, amount int64) { +func assertBalance(t *testing.T, l *Ledger, account, asset string, amount *core.MonetaryInt) { user, err := l.GetAccount(context.Background(), account) require.NoError(t, err) b := user.Balances[asset] - assert.Equalf(t, amount, b, - "wrong %v balance for account %v, expected: %d got: %d", + assert.Equalf(t, amount.String(), b.String(), + "wrong %v balance for account %v, expected: %s got: %s", asset, account, amount, b, ) @@ -86,7 +86,7 @@ func TestSend(t *testing.T) { _, err := l.Execute(context.Background(), script) require.NoError(t, err) - assertBalance(t, l, "user:001", "USD/2", 99) + assertBalance(t, l, "user:001", "USD/2", core.NewMonetaryInt(99)) }) } @@ -132,7 +132,7 @@ func TestVariables(t *testing.T) { require.NoError(t, err) b := user.Balances["CAD/2"] - assert.Equalf(t, int64(42), b, + assert.Equalf(t, core.NewMonetaryInt(42), b, "wrong CAD/2 balance for account user:042, expected: %d got: %d", 42, b, ) @@ -150,7 +150,7 @@ func TestEnoughFunds(t *testing.T) { { Source: "world", Destination: "user:001", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "COIN", }, }, @@ -183,7 +183,7 @@ func TestNotEnoughFunds(t *testing.T) { { Source: "world", Destination: "user:002", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "COIN", }, }, @@ -246,7 +246,7 @@ func TestMetadata(t *testing.T) { { Source: "world", Destination: "sales:042", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "COIN", }, }, @@ -298,11 +298,11 @@ func TestMetadata(t *testing.T) { _, err = l.Execute(context.Background(), script) require.NoError(t, err) - assertBalance(t, l, "sales:042", "COIN", 0) + assertBalance(t, l, "sales:042", "COIN", core.NewMonetaryInt(0)) - assertBalance(t, l, "users:053", "COIN", 85) + assertBalance(t, l, "users:053", "COIN", core.NewMonetaryInt(85)) - assertBalance(t, l, "platform", "COIN", 15) + assertBalance(t, l, "platform", "COIN", core.NewMonetaryInt(15)) }) } diff --git a/pkg/ledger/ledger.go b/pkg/ledger/ledger.go index 8d25b1e30..71761159c 100644 --- a/pkg/ledger/ledger.go +++ b/pkg/ledger/ledger.go @@ -13,14 +13,7 @@ import ( var DefaultContracts = []core.Contract{ { - Expr: &core.ExprGte{ - Op1: core.VariableExpr{ - Name: "balance", - }, - Op2: core.ConstantExpr{ - Value: float64(0), - }, - }, + Name: "default", Account: "*", // world still an exception }, } diff --git a/pkg/ledger/ledger_test.go b/pkg/ledger/ledger_test.go index 76982f369..962ec4bb9 100644 --- a/pkg/ledger/ledger_test.go +++ b/pkg/ledger/ledger_test.go @@ -100,13 +100,13 @@ func TestMain(m *testing.M) { func TestTransaction(t *testing.T) { runOnLedger(func(l *Ledger) { testsize := 1e4 - total := 0 + total := core.NewMonetaryInt(0) batch := []core.TransactionData{} for i := 1; i <= int(testsize); i++ { user := fmt.Sprintf("users:%03d", 1+rand.Intn(100)) - amount := 100 - total += amount + amount := core.NewMonetaryInt(100) + total = total.Add(amount) batch = append(batch, core.TransactionData{ Postings: []core.Posting{ @@ -114,13 +114,13 @@ func TestTransaction(t *testing.T) { Source: "world", Destination: "mint", Asset: "GEM", - Amount: int64(amount), + Amount: amount, }, { Source: "mint", Destination: user, Asset: "GEM", - Amount: int64(amount), + Amount: amount, }, }, }) @@ -138,7 +138,7 @@ func TestTransaction(t *testing.T) { world, err := l.GetAccount(context.Background(), "world") require.NoError(t, err) - expected := int64(-1 * total) + expected := total.Neg() b := world.Balances["GEM"] assert.Equalf(t, expected, b, "wrong GEM balance for account world, expected: %d got: %d", @@ -157,7 +157,7 @@ func TestTransactionBatchWithIntermediateWrongState(t *testing.T) { Source: "world", Destination: "player2", Asset: "GEM", - Amount: int64(100), + Amount: core.NewMonetaryInt(100), }, }, }, @@ -167,7 +167,7 @@ func TestTransactionBatchWithIntermediateWrongState(t *testing.T) { Source: "player", Destination: "game", Asset: "GEM", - Amount: int64(100), + Amount: core.NewMonetaryInt(100), }, }, }, @@ -177,7 +177,7 @@ func TestTransactionBatchWithIntermediateWrongState(t *testing.T) { Source: "world", Destination: "player", Asset: "GEM", - Amount: int64(100), + Amount: core.NewMonetaryInt(100), }, }, }, @@ -200,7 +200,7 @@ func TestTransactionBatchWithConflictingReference(t *testing.T) { Source: "world", Destination: "player", Asset: "GEM", - Amount: int64(100), + Amount: core.NewMonetaryInt(100), }, }, Reference: "ref1", @@ -211,7 +211,7 @@ func TestTransactionBatchWithConflictingReference(t *testing.T) { Source: "player", Destination: "game", Asset: "GEM", - Amount: int64(100), + Amount: core.NewMonetaryInt(100), }, }, Reference: "ref2", @@ -222,7 +222,7 @@ func TestTransactionBatchWithConflictingReference(t *testing.T) { Source: "player", Destination: "player2", Asset: "GEM", - Amount: int64(1000), // Should trigger an insufficient fund error but the conflict error has precedence over it + Amount: core.NewMonetaryInt(1000), // Should trigger an insufficient fund error but the conflict error has precedence over it }, }, Reference: "ref1", @@ -242,7 +242,7 @@ func TestTransactionBatchWithConflictingReference(t *testing.T) { Source: "world", Destination: "player", Asset: "GEM", - Amount: int64(100), + Amount: core.NewMonetaryInt(100), }, }, Reference: "ref1", @@ -266,7 +266,7 @@ func TestTransactionExpectedVolumes(t *testing.T) { Source: "world", Destination: "player", Asset: "USD", - Amount: int64(100), + Amount: core.NewMonetaryInt(100), }, }, }, @@ -276,7 +276,7 @@ func TestTransactionExpectedVolumes(t *testing.T) { Source: "world", Destination: "player", Asset: "EUR", - Amount: int64(100), + Amount: core.NewMonetaryInt(100), }, }, }, @@ -286,7 +286,7 @@ func TestTransactionExpectedVolumes(t *testing.T) { Source: "world", Destination: "player2", Asset: "EUR", - Amount: int64(100), + Amount: core.NewMonetaryInt(100), }, }, }, @@ -296,7 +296,7 @@ func TestTransactionExpectedVolumes(t *testing.T) { Source: "player", Destination: "player2", Asset: "EUR", - Amount: int64(50), + Amount: core.NewMonetaryInt(50), }, }, }, @@ -308,24 +308,28 @@ func TestTransactionExpectedVolumes(t *testing.T) { assert.EqualValues(t, core.AccountsAssetsVolumes{ "world": core.AssetsVolumes{ "USD": { - Output: 100, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(100), }, "EUR": { - Output: 200, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(200), }, }, "player": core.AssetsVolumes{ "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, "EUR": { - Input: 100, - Output: 50, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(50), }, }, "player2": core.AssetsVolumes{ "EUR": { - Input: 150, + Input: core.NewMonetaryInt(150), + Output: core.NewMonetaryInt(0), }, }, }, res.PostCommitVolumes) @@ -340,7 +344,7 @@ func TestBalance(t *testing.T) { { Source: "empty_wallet", Destination: "world", - Amount: 1, + Amount: core.NewMonetaryInt(1), Asset: "COIN", }, }, @@ -359,7 +363,7 @@ func TestReference(t *testing.T) { { Source: "world", Destination: "payments:001", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "COIN", }, }, @@ -404,7 +408,7 @@ func TestAccountMetadata(t *testing.T) { Postings: core.Postings{ { Source: "world", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", Destination: "users:001", }, @@ -432,7 +436,7 @@ func TestTransactionMetadata(t *testing.T) { { Source: "world", Destination: "payments:001", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "COIN", }, }, @@ -470,7 +474,7 @@ func TestSaveTransactionMetadata(t *testing.T) { { Source: "world", Destination: "payments:001", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "COIN", }, }, @@ -499,7 +503,7 @@ func TestGetTransaction(t *testing.T) { { Source: "world", Destination: "payments:001", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "COIN", }, }, @@ -523,7 +527,7 @@ func TestGetTransactions(t *testing.T) { { Source: "world", Destination: "test_get_transactions", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "COIN", }, }, @@ -541,7 +545,7 @@ func TestGetTransactions(t *testing.T) { func TestRevertTransaction(t *testing.T) { runOnLedger(func(l *Ledger) { - revertAmt := int64(100) + revertAmt := core.NewMonetaryInt(100) res, err := l.Commit(context.Background(), []core.TransactionData{{ Reference: "foo", @@ -568,7 +572,7 @@ func TestRevertTransaction(t *testing.T) { { Source: "payments:001", Destination: "world", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "COIN", }, }, revertTx.TransactionData.Postings) @@ -589,7 +593,7 @@ func TestRevertTransaction(t *testing.T) { require.NoError(t, err) newBal := world.Balances["COIN"] - expectedBal := originalBal + revertAmt + expectedBal := originalBal.Add(revertAmt) require.Equalf(t, expectedBal, newBal, "COIN world balances expected %d, got %d", expectedBal, newBal) }) @@ -606,7 +610,7 @@ func BenchmarkTransaction1(b *testing.B) { Source: "world", Destination: "benchmark", Asset: "COIN", - Amount: 10, + Amount: core.NewMonetaryInt(10), }, }, }) @@ -630,7 +634,7 @@ func BenchmarkTransaction_20_1k(b *testing.B) { Source: "world", Destination: "benchmark", Asset: "COIN", - Amount: 10, + Amount: core.NewMonetaryInt(10), }, }, }) diff --git a/pkg/ledger/process.go b/pkg/ledger/process.go index 6d2e8cf21..261eb55d9 100644 --- a/pkg/ledger/process.go +++ b/pkg/ledger/process.go @@ -69,7 +69,7 @@ func (l *Ledger) processTx(ctx context.Context, ts []core.TransactionData) (*Com txVolumeAggregator := volumeAggregator.nextTx() for _, p := range t.Postings { - if p.Amount < 0 { + if p.Amount.Ltz() { return nil, NewTransactionCommitError(i, NewValidationError("negative amount")) } if !core.ValidateAddress(p.Source) { @@ -81,7 +81,7 @@ func (l *Ledger) processTx(ctx context.Context, ts []core.TransactionData) (*Com 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)) + err := txVolumeAggregator.transfer(ctx, p.Source, p.Destination, p.Asset, p.Amount) if err != nil { return nil, NewTransactionCommitError(i, err) } @@ -96,22 +96,14 @@ func (l *Ledger) processTx(ctx context.Context, ts []core.TransactionData) (*Com 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 _, ok := accounts[addr]; !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 { + if !expectedBalance.Gte(core.NewMonetaryInt(0)) { return nil, NewTransactionCommitError(i, NewInsufficientFundError(asset)) } break diff --git a/pkg/ledger/process_test.go b/pkg/ledger/process_test.go index eca50e0f9..2336d0f35 100644 --- a/pkg/ledger/process_test.go +++ b/pkg/ledger/process_test.go @@ -13,14 +13,12 @@ import ( 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 - ) + worldTotoUSD := core.NewMonetaryInt(43) + worldAliceUSD := core.NewMonetaryInt(98) + aliceTotoUSD := core.NewMonetaryInt(45) + worldTotoEUR := core.NewMonetaryInt(15) + worldAliceEUR := core.NewMonetaryInt(10) + totoAliceEUR := core.NewMonetaryInt(5) postings := []core.Posting{ { @@ -63,16 +61,34 @@ func TestLedger_processTx(t *testing.T) { expectedPreCommitVol := core.AccountsAssetsVolumes{ "alice": core.AssetsVolumes{ - "USD": {}, - "EUR": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, + "EUR": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, "toto": core.AssetsVolumes{ - "USD": {}, - "EUR": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, + "EUR": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, "world": core.AssetsVolumes{ - "USD": {}, - "EUR": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, + "EUR": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, } @@ -83,12 +99,14 @@ func TestLedger_processTx(t *testing.T) { Output: aliceTotoUSD, }, "EUR": { - Input: worldAliceEUR + totoAliceEUR, + Input: worldAliceEUR.Add(totoAliceEUR), + Output: core.NewMonetaryInt(0), }, }, "toto": core.AssetsVolumes{ "USD": { - Input: worldTotoUSD + aliceTotoUSD, + Input: worldTotoUSD.Add(aliceTotoUSD), + Output: core.NewMonetaryInt(0), }, "EUR": { Input: worldTotoEUR, @@ -97,10 +115,12 @@ func TestLedger_processTx(t *testing.T) { }, "world": core.AssetsVolumes{ "USD": { - Output: worldTotoUSD + worldAliceUSD, + Input: core.NewMonetaryInt(0), + Output: worldTotoUSD.Add(worldAliceUSD), }, "EUR": { - Output: worldTotoEUR + worldAliceEUR, + Input: core.NewMonetaryInt(0), + Output: worldTotoEUR.Add(worldAliceEUR), }, }, } @@ -175,11 +195,11 @@ func TestLedger_processTx(t *testing.T) { ID: 0, }, PreCommitVolumes: core.AccountsAssetsVolumes{ - "toto": core.AssetsVolumes{"USD": core.Volumes{Input: 0, Output: 0}}, - "world": core.AssetsVolumes{"USD": core.Volumes{Input: 0, Output: 0}}}, + "toto": core.AssetsVolumes{"USD": core.Volumes{Input: core.NewMonetaryInt(0), Output: core.NewMonetaryInt(0)}}, + "world": core.AssetsVolumes{"USD": core.Volumes{Input: core.NewMonetaryInt(0), Output: core.NewMonetaryInt(0)}}}, PostCommitVolumes: core.AccountsAssetsVolumes{ - "toto": core.AssetsVolumes{"USD": core.Volumes{Input: worldTotoUSD, Output: 0}}, - "world": core.AssetsVolumes{"USD": core.Volumes{Input: 0, Output: worldTotoUSD}}}, + "toto": core.AssetsVolumes{"USD": core.Volumes{Input: worldTotoUSD, Output: core.NewMonetaryInt(0)}}, + "world": core.AssetsVolumes{"USD": core.Volumes{Input: core.NewMonetaryInt(0), Output: worldTotoUSD}}}, }, { Transaction: core.Transaction{ @@ -190,12 +210,12 @@ func TestLedger_processTx(t *testing.T) { ID: 1, }, PreCommitVolumes: core.AccountsAssetsVolumes{ - "world": core.AssetsVolumes{"USD": core.Volumes{Input: 0, Output: worldTotoUSD}}, - "alice": core.AssetsVolumes{"USD": core.Volumes{Input: 0, Output: 0}}, + "world": core.AssetsVolumes{"USD": core.Volumes{Input: core.NewMonetaryInt(0), Output: worldTotoUSD}}, + "alice": core.AssetsVolumes{"USD": core.Volumes{Input: core.NewMonetaryInt(0), Output: core.NewMonetaryInt(0)}}, }, PostCommitVolumes: core.AccountsAssetsVolumes{ - "world": core.AssetsVolumes{"USD": core.Volumes{Input: 0, Output: worldTotoUSD + worldAliceUSD}}, - "alice": core.AssetsVolumes{"USD": core.Volumes{Input: worldAliceUSD, Output: 0}}, + "world": core.AssetsVolumes{"USD": core.Volumes{Input: core.NewMonetaryInt(0), Output: worldTotoUSD.Add(worldAliceUSD)}}, + "alice": core.AssetsVolumes{"USD": core.Volumes{Input: worldAliceUSD, Output: core.NewMonetaryInt(0)}}, }, }, { @@ -207,12 +227,12 @@ func TestLedger_processTx(t *testing.T) { ID: 2, }, PreCommitVolumes: core.AccountsAssetsVolumes{ - "alice": core.AssetsVolumes{"USD": core.Volumes{Input: worldAliceUSD, Output: 0}}, - "toto": core.AssetsVolumes{"USD": core.Volumes{Input: worldTotoUSD, Output: 0}}, + "alice": core.AssetsVolumes{"USD": core.Volumes{Input: worldAliceUSD, Output: core.NewMonetaryInt(0)}}, + "toto": core.AssetsVolumes{"USD": core.Volumes{Input: worldTotoUSD, Output: core.NewMonetaryInt(0)}}, }, PostCommitVolumes: core.AccountsAssetsVolumes{ "alice": core.AssetsVolumes{"USD": core.Volumes{Input: worldAliceUSD, Output: aliceTotoUSD}}, - "toto": core.AssetsVolumes{"USD": core.Volumes{Input: worldTotoUSD + aliceTotoUSD, Output: 0}}, + "toto": core.AssetsVolumes{"USD": core.Volumes{Input: worldTotoUSD.Add(aliceTotoUSD), Output: core.NewMonetaryInt(0)}}, }, }, { @@ -224,12 +244,12 @@ func TestLedger_processTx(t *testing.T) { ID: 3, }, PreCommitVolumes: core.AccountsAssetsVolumes{ - "world": core.AssetsVolumes{"EUR": core.Volumes{Input: 0, Output: 0}}, - "toto": core.AssetsVolumes{"EUR": core.Volumes{Input: 0, Output: 0}}, + "world": core.AssetsVolumes{"EUR": core.Volumes{Input: core.NewMonetaryInt(0), Output: core.NewMonetaryInt(0)}}, + "toto": core.AssetsVolumes{"EUR": core.Volumes{Input: core.NewMonetaryInt(0), Output: core.NewMonetaryInt(0)}}, }, PostCommitVolumes: core.AccountsAssetsVolumes{ - "world": core.AssetsVolumes{"EUR": core.Volumes{Input: 0, Output: worldTotoEUR}}, - "toto": core.AssetsVolumes{"EUR": core.Volumes{Input: worldTotoEUR, Output: 0}}, + "world": core.AssetsVolumes{"EUR": core.Volumes{Input: core.NewMonetaryInt(0), Output: worldTotoEUR}}, + "toto": core.AssetsVolumes{"EUR": core.Volumes{Input: worldTotoEUR, Output: core.NewMonetaryInt(0)}}, }, }, { @@ -241,12 +261,12 @@ func TestLedger_processTx(t *testing.T) { ID: 4, }, PreCommitVolumes: core.AccountsAssetsVolumes{ - "world": core.AssetsVolumes{"EUR": core.Volumes{Input: 0, Output: worldTotoEUR}}, - "alice": core.AssetsVolumes{"EUR": core.Volumes{Input: 0, Output: 0}}, + "world": core.AssetsVolumes{"EUR": core.Volumes{Input: core.NewMonetaryInt(0), Output: worldTotoEUR}}, + "alice": core.AssetsVolumes{"EUR": core.Volumes{Input: core.NewMonetaryInt(0), Output: core.NewMonetaryInt(0)}}, }, PostCommitVolumes: core.AccountsAssetsVolumes{ - "world": core.AssetsVolumes{"EUR": core.Volumes{Input: 0, Output: worldTotoEUR + worldAliceEUR}}, - "alice": core.AssetsVolumes{"EUR": core.Volumes{Input: worldAliceEUR, Output: 0}}, + "world": core.AssetsVolumes{"EUR": core.Volumes{Input: core.NewMonetaryInt(0), Output: worldTotoEUR.Add(worldAliceEUR)}}, + "alice": core.AssetsVolumes{"EUR": core.Volumes{Input: worldAliceEUR, Output: core.NewMonetaryInt(0)}}, }, }, { @@ -258,18 +278,17 @@ func TestLedger_processTx(t *testing.T) { ID: 5, }, PreCommitVolumes: core.AccountsAssetsVolumes{ - "toto": core.AssetsVolumes{"EUR": core.Volumes{Input: worldTotoEUR, Output: 0}}, - "alice": core.AssetsVolumes{"EUR": core.Volumes{Input: worldAliceEUR, Output: 0}}, + "toto": core.AssetsVolumes{"EUR": core.Volumes{Input: worldTotoEUR, Output: core.NewMonetaryInt(0)}}, + "alice": core.AssetsVolumes{"EUR": core.Volumes{Input: worldAliceEUR, Output: core.NewMonetaryInt(0)}}, }, PostCommitVolumes: core.AccountsAssetsVolumes{ "toto": core.AssetsVolumes{"EUR": core.Volumes{Input: worldTotoEUR, Output: totoAliceEUR}}, - "alice": core.AssetsVolumes{"EUR": core.Volumes{Input: worldAliceEUR + totoAliceEUR, Output: 0}}, + "alice": core.AssetsVolumes{"EUR": core.Volumes{Input: worldAliceEUR.Add(totoAliceEUR), Output: core.NewMonetaryInt(0)}}, }, }, } assert.Equal(t, expectedTxs, res.GeneratedTransactions) - }) }) @@ -299,7 +318,7 @@ func TestLedger_processTx(t *testing.T) { Postings: []core.Posting{{ Source: "world", Destination: "bank", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }}, Timestamp: now.Add(-time.Second), @@ -327,7 +346,7 @@ func TestLedger_processTx(t *testing.T) { Postings: []core.Posting{{ Source: "world", Destination: "bank", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }}, Timestamp: now.Add(-time.Second), diff --git a/pkg/ledger/volume_agg.go b/pkg/ledger/volume_agg.go index b4df5f54e..2abb04e5f 100644 --- a/pkg/ledger/volume_agg.go +++ b/pkg/ledger/volume_agg.go @@ -22,7 +22,11 @@ func (tva *transactionVolumeAggregator) preCommitVolumes() core.AccountsAssetsVo return tva.preVolumes } -func (tva *transactionVolumeAggregator) transfer(ctx context.Context, from, to, asset string, amount uint64) error { +func (tva *transactionVolumeAggregator) transfer( + ctx context.Context, + from, to, asset string, + amount *core.MonetaryInt, +) error { if tva.preVolumes == nil { tva.preVolumes = core.AccountsAssetsVolumes{} } @@ -38,7 +42,10 @@ func (tva *transactionVolumeAggregator) transfer(ctx context.Context, from, to, } for current != nil { if v, ok := current.postVolumes[addr][asset]; ok { - tva.preVolumes[addr][asset] = v + tva.preVolumes[addr][asset] = core.Volumes{ + Input: v.Input.OrZero(), + Output: v.Output.OrZero(), + } found = true break } @@ -49,7 +56,10 @@ func (tva *transactionVolumeAggregator) transfer(ctx context.Context, from, to, if err != nil { return err } - tva.preVolumes[addr][asset] = v + tva.preVolumes[addr][asset] = core.Volumes{ + Input: v.Input.OrZero(), + Output: v.Output.OrZero(), + } } } if _, ok := tva.postVolumes[addr][asset]; !ok { @@ -60,11 +70,11 @@ func (tva *transactionVolumeAggregator) transfer(ctx context.Context, from, to, } } v := tva.postVolumes[from][asset] - v.Output += int64(amount) + v.Output = v.Output.Add(amount) tva.postVolumes[from][asset] = v v = tva.postVolumes[to][asset] - v.Input += int64(amount) + v.Input = v.Input.Add(amount) tva.postVolumes[to][asset] = v return nil diff --git a/pkg/ledger/volume_agg_test.go b/pkg/ledger/volume_agg_test.go index 9486fca73..5cd32b1d0 100644 --- a/pkg/ledger/volume_agg_test.go +++ b/pkg/ledger/volume_agg_test.go @@ -35,7 +35,7 @@ func TestVolumeAggregator(t *testing.T) { { Source: "bob", Destination: "zozo", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -43,21 +43,29 @@ func TestVolumeAggregator(t *testing.T) { }, PreCommitVolumes: map[string]core.AssetsVolumes{ "bob": { - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, "zozo": { - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, }, PostCommitVolumes: map[string]core.AssetsVolumes{ "bob": { "USD": { - Output: 100, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(100), }, }, "zozo": { "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, }, }, @@ -71,7 +79,7 @@ func TestVolumeAggregator(t *testing.T) { { Source: "zozo", Destination: "alice", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -80,23 +88,28 @@ func TestVolumeAggregator(t *testing.T) { PostCommitVolumes: map[string]core.AssetsVolumes{ "alice": { "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, }, "zozo": { "USD": { - Input: 100, - Output: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(100), }, }, }, PreCommitVolumes: map[string]core.AssetsVolumes{ "alice": { - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, "zozo": { "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, }, }, @@ -105,78 +118,91 @@ func TestVolumeAggregator(t *testing.T) { volumeAggregator := newVolumeAggregator(store) firstTx := volumeAggregator.nextTx() - require.NoError(t, firstTx.transfer(context.Background(), "bob", "alice", "USD", 100)) - require.NoError(t, firstTx.transfer(context.Background(), "bob", "zoro", "USD", 50)) + require.NoError(t, firstTx.transfer(context.Background(), "bob", "alice", "USD", core.NewMonetaryInt(100))) + require.NoError(t, firstTx.transfer(context.Background(), "bob", "zoro", "USD", core.NewMonetaryInt(50))) require.Equal(t, core.AccountsAssetsVolumes{ "bob": core.AssetsVolumes{ "USD": { - Output: 250, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(250), }, }, "alice": core.AssetsVolumes{ "USD": { - Input: 200, + Input: core.NewMonetaryInt(200), + Output: core.NewMonetaryInt(0), }, }, "zoro": { "USD": { - Input: 50, + Input: core.NewMonetaryInt(50), + Output: core.NewMonetaryInt(0), }, }, }, firstTx.postCommitVolumes()) require.Equal(t, core.AccountsAssetsVolumes{ "bob": core.AssetsVolumes{ "USD": { - Output: 100, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(100), }, }, "alice": core.AssetsVolumes{ "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, }, "zoro": core.AssetsVolumes{ "USD": { - Input: 0, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), }, }, }, firstTx.preCommitVolumes()) secondTx := volumeAggregator.nextTx() - require.NoError(t, secondTx.transfer(context.Background(), "alice", "fred", "USD", 50)) - require.NoError(t, secondTx.transfer(context.Background(), "bob", "fred", "USD", 25)) + require.NoError(t, secondTx.transfer(context.Background(), "alice", "fred", "USD", core.NewMonetaryInt(50))) + require.NoError(t, secondTx.transfer(context.Background(), "bob", "fred", "USD", core.NewMonetaryInt(25))) require.Equal(t, core.AccountsAssetsVolumes{ "bob": core.AssetsVolumes{ "USD": { - Output: 275, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(275), }, }, "alice": core.AssetsVolumes{ "USD": { - Input: 200, - Output: 50, + Input: core.NewMonetaryInt(200), + Output: core.NewMonetaryInt(50), }, }, "fred": core.AssetsVolumes{ "USD": { - Input: 75, + Input: core.NewMonetaryInt(75), + Output: core.NewMonetaryInt(0), }, }, }, secondTx.postCommitVolumes()) require.Equal(t, core.AccountsAssetsVolumes{ "bob": core.AssetsVolumes{ "USD": { - Output: 250, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(250), }, }, "alice": core.AssetsVolumes{ "USD": { - Input: 200, + Input: core.NewMonetaryInt(200), + Output: core.NewMonetaryInt(0), }, }, "fred": core.AssetsVolumes{ - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, }, secondTx.preCommitVolumes()) @@ -184,24 +210,26 @@ func TestVolumeAggregator(t *testing.T) { require.Equal(t, core.AccountsAssetsVolumes{ "bob": core.AssetsVolumes{ "USD": { - Output: 275, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(275), }, }, "alice": core.AssetsVolumes{ "USD": { - Input: 200, - Output: 50, + Input: core.NewMonetaryInt(200), + Output: core.NewMonetaryInt(50), }, }, "fred": core.AssetsVolumes{ "USD": { - Input: 75, + Input: core.NewMonetaryInt(75), + Output: core.NewMonetaryInt(0), }, }, "zoro": core.AssetsVolumes{ "USD": { - Input: 50, - Output: 0, + Input: core.NewMonetaryInt(50), + Output: core.NewMonetaryInt(0), }, }, }, aggregatedPostVolumes) @@ -210,19 +238,27 @@ func TestVolumeAggregator(t *testing.T) { require.Equal(t, core.AccountsAssetsVolumes{ "bob": core.AssetsVolumes{ "USD": { - Output: 100, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(100), }, }, "alice": core.AssetsVolumes{ "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, }, "fred": core.AssetsVolumes{ - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, "zoro": core.AssetsVolumes{ - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, }, aggregatedPreVolumes) diff --git a/pkg/storage/sqlstorage/aggregations.go b/pkg/storage/sqlstorage/aggregations.go index dc1e720a6..eb3f2b9e8 100644 --- a/pkg/storage/sqlstorage/aggregations.go +++ b/pkg/storage/sqlstorage/aggregations.go @@ -52,14 +52,27 @@ func (s *API) GetAssetsVolumes(ctx context.Context, accountAddress string) (core volumes := core.AssetsVolumes{} for rows.Next() { var ( - asset string - input int64 - output int64 + asset string + inputStr string + outputStr string ) - err = rows.Scan(&asset, &input, &output) + err = rows.Scan(&asset, &inputStr, &outputStr) if err != nil { return nil, s.error(err) } + + input, err := core.ParseMonetaryInt(inputStr) + + if err != nil { + return nil, s.error(err) + } + + output, err := core.ParseMonetaryInt(outputStr) + + if err != nil { + return nil, s.error(err) + } + volumes[asset] = core.Volumes{ Input: input, Output: output, @@ -84,14 +97,27 @@ func (s *API) GetVolumes(ctx context.Context, accountAddress, asset string) (cor return core.Volumes{}, s.error(row.Err()) } - var input, output int64 - if err := row.Scan(&input, &output); err != nil { + var inputStr, outputStr string + + if err := row.Scan(&inputStr, &outputStr); err != nil { if err == sql.ErrNoRows { return core.Volumes{}, nil } return core.Volumes{}, s.error(err) } + input, err := core.ParseMonetaryInt(inputStr) + + if err != nil { + return core.Volumes{}, s.error(err) + } + + output, err := core.ParseMonetaryInt(outputStr) + + if err != nil { + return core.Volumes{}, s.error(err) + } + return core.Volumes{ Input: input, Output: output, diff --git a/pkg/storage/sqlstorage/balances.go b/pkg/storage/sqlstorage/balances.go index a11742a6c..7290c6dd1 100644 --- a/pkg/storage/sqlstorage/balances.go +++ b/pkg/storage/sqlstorage/balances.go @@ -47,10 +47,16 @@ func (s *API) GetBalancesAggregated(ctx context.Context, q storage.BalancesQuery for rows.Next() { var ( - asset string - balances int64 + asset string + balancesStr string ) - if err = rows.Scan(&asset, &balances); err != nil { + if err = rows.Scan(&asset, &balancesStr); err != nil { + return nil, s.error(err) + } + + balances, err := core.ParseMonetaryInt(balancesStr) + + if err != nil { return nil, s.error(err) } @@ -136,7 +142,7 @@ func (s *API) GetBalances(ctx context.Context, q storage.BalancesQuery) (shareda if err != nil { return sharedapi.Cursor[core.AccountsBalances]{}, s.error(err) } - accountsBalances[currentAccount][asset] = balances + accountsBalances[currentAccount][asset] = core.NewMonetaryInt(balances) } accounts = append(accounts, accountsBalances) diff --git a/pkg/storage/sqlstorage/balances_test.go b/pkg/storage/sqlstorage/balances_test.go index c07dae835..cb79c1df7 100644 --- a/pkg/storage/sqlstorage/balances_test.go +++ b/pkg/storage/sqlstorage/balances_test.go @@ -29,17 +29,17 @@ func testGetBalances(t *testing.T, store *sqlstorage.Store) { assert.Equal(t, []core.AccountsBalances{ { "world": core.AssetsBalances{ - "USD": -200, + "USD": core.NewMonetaryInt(-200), }, }, { "users:1": core.AssetsBalances{ - "USD": 1, + "USD": core.NewMonetaryInt(1), }, }, { "central_bank": core.AssetsBalances{ - "USD": 199, + "USD": core.NewMonetaryInt(199), }, }, }, cursor.Data) @@ -58,7 +58,7 @@ func testGetBalances(t *testing.T, store *sqlstorage.Store) { assert.Equal(t, []core.AccountsBalances{ { "world": core.AssetsBalances{ - "USD": -200, + "USD": core.NewMonetaryInt(-200), }, }, }, cursor.Data) @@ -78,7 +78,7 @@ func testGetBalances(t *testing.T, store *sqlstorage.Store) { assert.Equal(t, []core.AccountsBalances{ { "users:1": core.AssetsBalances{ - "USD": 1, + "USD": core.NewMonetaryInt(1), }, }, }, cursor.Data) @@ -98,12 +98,12 @@ func testGetBalances(t *testing.T, store *sqlstorage.Store) { assert.Equal(t, []core.AccountsBalances{ { "users:1": core.AssetsBalances{ - "USD": 1, + "USD": core.NewMonetaryInt(1), }, }, { "central_bank": core.AssetsBalances{ - "USD": 199, + "USD": core.NewMonetaryInt(199), }, }, }, cursor.Data) @@ -124,7 +124,7 @@ func testGetBalances(t *testing.T, store *sqlstorage.Store) { assert.Equal(t, []core.AccountsBalances{ { "users:1": core.AssetsBalances{ - "USD": 1, + "USD": core.NewMonetaryInt(1), }, }, }, cursor.Data) @@ -141,6 +141,6 @@ func testGetBalancesAggregated(t *testing.T, store *sqlstorage.Store) { cursor, err := store.GetBalancesAggregated(context.Background(), q) assert.NoError(t, err) assert.Equal(t, core.AssetsBalances{ - "USD": 0, + "USD": core.NewMonetaryInt(0), }, cursor) } diff --git a/pkg/storage/sqlstorage/log.go b/pkg/storage/sqlstorage/log.go index 15629c18b..0cc40bff8 100644 --- a/pkg/storage/sqlstorage/log.go +++ b/pkg/storage/sqlstorage/log.go @@ -3,10 +3,10 @@ package sqlstorage import ( "context" "database/sql" + "encoding/json" "fmt" "time" - json "github.com/gibson042/canonicaljson-go" "github.com/huandu/go-sqlbuilder" "github.com/numary/go-libs/sharedlogging" "github.com/numary/ledger/pkg/core" diff --git a/pkg/storage/sqlstorage/migrates/13-amounts-numeric/postgres.sql b/pkg/storage/sqlstorage/migrates/13-amounts-numeric/postgres.sql new file mode 100644 index 000000000..a5da171f8 --- /dev/null +++ b/pkg/storage/sqlstorage/migrates/13-amounts-numeric/postgres.sql @@ -0,0 +1,4 @@ +--statement +ALTER TABLE "VAR_LEDGER_NAME".volumes +ALTER COLUMN input TYPE numeric, +ALTER COLUMN output TYPE numeric; diff --git a/pkg/storage/sqlstorage/migrates/9-add-pre-post-volumes/any.go b/pkg/storage/sqlstorage/migrates/9-add-pre-post-volumes/any.go index 0208e8ddb..f485a964f 100644 --- a/pkg/storage/sqlstorage/migrates/9-add-pre-post-volumes/any.go +++ b/pkg/storage/sqlstorage/migrates/9-add-pre-post-volumes/any.go @@ -56,27 +56,53 @@ func Upgrade(ctx context.Context, schema sqlstorage.Schema, sqlTx *sql.Tx) error postCommitVolumes := core.AccountsAssetsVolumes{} for _, posting := range tx.Postings { - preCommitVolumes.SetVolumes(posting.Source, posting.Asset, - aggregatedVolumes.GetVolumes(posting.Source, posting.Asset)) - preCommitVolumes.SetVolumes(posting.Destination, posting.Asset, - aggregatedVolumes.GetVolumes(posting.Destination, posting.Asset)) + preCommitVolumes.SetVolumes( + posting.Source, + posting.Asset, + aggregatedVolumes.GetVolumes(posting.Source, posting.Asset), + ) + + preCommitVolumes.SetVolumes( + posting.Destination, + posting.Asset, + aggregatedVolumes.GetVolumes(posting.Destination, posting.Asset), + ) if !postCommitVolumes.HasAccount(posting.Source) { - postCommitVolumes.SetVolumes(posting.Source, posting.Asset, - preCommitVolumes.GetVolumes(posting.Source, posting.Asset)) + postCommitVolumes.SetVolumes( + posting.Source, + posting.Asset, + preCommitVolumes.GetVolumes(posting.Source, posting.Asset), + ) } + if !postCommitVolumes.HasAccount(posting.Destination) { - postCommitVolumes.SetVolumes(posting.Destination, posting.Asset, - preCommitVolumes.GetVolumes(posting.Destination, posting.Asset)) + postCommitVolumes.SetVolumes( + posting.Destination, + posting.Asset, + preCommitVolumes.GetVolumes(posting.Destination, posting.Asset), + ) } - postCommitVolumes.AddOutput(posting.Source, posting.Asset, posting.Amount) - postCommitVolumes.AddInput(posting.Destination, posting.Asset, posting.Amount) + postCommitVolumes.AddOutput( + posting.Source, + posting.Asset, + posting.Amount, + ) + + postCommitVolumes.AddInput( + posting.Destination, + posting.Asset, + posting.Amount, + ) } for account, accountVolumes := range postCommitVolumes { for asset, volumes := range accountVolumes { - aggregatedVolumes.SetVolumes(account, asset, volumes) + aggregatedVolumes.SetVolumes(account, asset, core.Volumes{ + Input: volumes.Input.OrZero(), + Output: volumes.Output.OrZero(), + }) } } diff --git a/pkg/storage/sqlstorage/migrates/9-add-pre-post-volumes/any_test.go b/pkg/storage/sqlstorage/migrates/9-add-pre-post-volumes/any_test.go index e37ac7b98..8eec2ae4e 100644 --- a/pkg/storage/sqlstorage/migrates/9-add-pre-post-volumes/any_test.go +++ b/pkg/storage/sqlstorage/migrates/9-add-pre-post-volumes/any_test.go @@ -29,27 +29,35 @@ var testCases = []testCase{ { Source: "world", Destination: "bank", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, expectedPreCommitVolumes: core.AccountsAssetsVolumes{ "world": { - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, "bank": { - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, }, expectedPostCommitVolumes: core.AccountsAssetsVolumes{ "world": { "USD": { - Output: 100, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(100), }, }, "bank": { "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, }, }, @@ -59,29 +67,35 @@ var testCases = []testCase{ { Source: "world", Destination: "bank2", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, expectedPreCommitVolumes: core.AccountsAssetsVolumes{ "world": { "USD": { - Output: 100, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(100), }, }, "bank2": { - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, }, expectedPostCommitVolumes: core.AccountsAssetsVolumes{ "world": { "USD": { - Output: 200, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(200), }, }, "bank2": { "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, }, }, @@ -91,47 +105,53 @@ var testCases = []testCase{ { Source: "world", Destination: "bank", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, { Source: "world", Destination: "bank2", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, expectedPreCommitVolumes: core.AccountsAssetsVolumes{ "world": { "USD": { - Output: 200, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(200), }, }, "bank": { "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, }, "bank2": { "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, }, }, expectedPostCommitVolumes: core.AccountsAssetsVolumes{ "world": { "USD": { - Output: 400, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(400), }, }, "bank2": { "USD": { - Input: 200, + Input: core.NewMonetaryInt(200), + Output: core.NewMonetaryInt(0), }, }, "bank": { "USD": { - Input: 200, + Input: core.NewMonetaryInt(200), + Output: core.NewMonetaryInt(0), }, }, }, @@ -141,48 +161,53 @@ var testCases = []testCase{ { Source: "bank", Destination: "user:1", - Amount: 10, + Amount: core.NewMonetaryInt(10), Asset: "USD", }, { Source: "bank", Destination: "user:2", - Amount: 90, - Asset: "USDT", + Amount: core.NewMonetaryInt(90), + Asset: "USD", }, }, expectedPreCommitVolumes: core.AccountsAssetsVolumes{ "bank": { "USD": { - Input: 200, + Input: core.NewMonetaryInt(200), + Output: core.NewMonetaryInt(0), }, - "USDT": {}, }, "user:1": { - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, "user:2": { - "USDT": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, }, expectedPostCommitVolumes: core.AccountsAssetsVolumes{ "bank": { "USD": { - Input: 200, - Output: 10, - }, - "USDT": { - Output: 90, + Input: core.NewMonetaryInt(200), + Output: core.NewMonetaryInt(100), }, }, "user:1": { "USD": { - Input: 10, + Input: core.NewMonetaryInt(10), + Output: core.NewMonetaryInt(0), }, }, "user:2": { - "USDT": { - Input: 90, + "USD": { + Input: core.NewMonetaryInt(90), + Output: core.NewMonetaryInt(0), }, }, }, diff --git a/pkg/storage/sqlstorage/store_test.go b/pkg/storage/sqlstorage/store_test.go index 8ec096e6a..013d28fc1 100644 --- a/pkg/storage/sqlstorage/store_test.go +++ b/pkg/storage/sqlstorage/store_test.go @@ -104,7 +104,7 @@ var tx1 = core.ExpandedTransaction{ { Source: "world", Destination: "central_bank", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -115,21 +115,29 @@ var tx1 = core.ExpandedTransaction{ PostCommitVolumes: core.AccountsAssetsVolumes{ "world": { "USD": { - Output: 100, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(100), }, }, "central_bank": { "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, }, }, PreCommitVolumes: core.AccountsAssetsVolumes{ "world": { - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, "central_bank": { - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, }, } @@ -141,7 +149,7 @@ var tx2 = core.ExpandedTransaction{ { Source: "world", Destination: "central_bank", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -152,24 +160,28 @@ var tx2 = core.ExpandedTransaction{ PostCommitVolumes: core.AccountsAssetsVolumes{ "world": { "USD": { - Output: 200, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(200), }, }, "central_bank": { "USD": { - Input: 200, + Input: core.NewMonetaryInt(200), + Output: core.NewMonetaryInt(0), }, }, }, PreCommitVolumes: core.AccountsAssetsVolumes{ "world": { "USD": { - Output: 100, + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(100), }, }, "central_bank": { "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, }, }, @@ -182,7 +194,7 @@ var tx3 = core.ExpandedTransaction{ { Source: "central_bank", Destination: "users:1", - Amount: 1, + Amount: core.NewMonetaryInt(1), Asset: "USD", }, }, @@ -196,23 +208,28 @@ var tx3 = core.ExpandedTransaction{ PreCommitVolumes: core.AccountsAssetsVolumes{ "central_bank": { "USD": { - Input: 200, + Input: core.NewMonetaryInt(200), + Output: core.NewMonetaryInt(0), }, }, "users:1": { - "USD": {}, + "USD": { + Input: core.NewMonetaryInt(0), + Output: core.NewMonetaryInt(0), + }, }, }, PostCommitVolumes: core.AccountsAssetsVolumes{ "central_bank": { "USD": { - Input: 200, - Output: 1, + Input: core.NewMonetaryInt(200), + Output: core.NewMonetaryInt(1), }, }, "users:1": { "USD": { - Input: 1, + Input: core.NewMonetaryInt(1), + Output: core.NewMonetaryInt(0), }, }, }, @@ -227,7 +244,7 @@ func testCommit(t *testing.T, store *sqlstorage.Store) { { Source: "world", Destination: "central_bank", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -257,7 +274,7 @@ func testUpdateTransactionMetadata(t *testing.T, store *sqlstorage.Store) { { Source: "world", Destination: "central_bank", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -292,7 +309,7 @@ func testUpdateAccountMetadata(t *testing.T, store *sqlstorage.Store) { { Source: "world", Destination: "central_bank", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -327,7 +344,7 @@ func testCountAccounts(t *testing.T, store *sqlstorage.Store) { { Source: "world", Destination: "central_bank", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -351,7 +368,7 @@ func testGetAssetsVolumes(t *testing.T, store *sqlstorage.Store) { { Source: "world", Destination: "central_bank", - Amount: 100, + Amount: core.NewMonetaryInt(100), Asset: "USD", }, }, @@ -361,14 +378,16 @@ func testGetAssetsVolumes(t *testing.T, store *sqlstorage.Store) { PostCommitVolumes: core.AccountsAssetsVolumes{ "central_bank": core.AssetsVolumes{ "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, }, }, PreCommitVolumes: core.AccountsAssetsVolumes{ "central_bank": core.AssetsVolumes{ "USD": { - Input: 100, + Input: core.NewMonetaryInt(100), + Output: core.NewMonetaryInt(0), }, }, }, @@ -379,8 +398,8 @@ func testGetAssetsVolumes(t *testing.T, store *sqlstorage.Store) { volumes, err := store.GetAssetsVolumes(context.Background(), "central_bank") require.NoError(t, err) require.Len(t, volumes, 1) - require.EqualValues(t, 100, volumes["USD"].Input) - require.EqualValues(t, 0, volumes["USD"].Output) + require.EqualValues(t, core.NewMonetaryInt(100), volumes["USD"].Input) + require.EqualValues(t, core.NewMonetaryInt(0), volumes["USD"].Output) } func testGetAccounts(t *testing.T, store *sqlstorage.Store) { @@ -633,10 +652,7 @@ func testMapping(t *testing.T, store *sqlstorage.Store) { m := core.Mapping{ Contracts: []core.Contract{ { - Expr: &core.ExprGt{ - Op1: core.VariableExpr{Name: "balance"}, - Op2: core.ConstantExpr{Value: float64(0)}, - }, + Name: "contract", Account: "orders:*", }, }, diff --git a/pkg/storage/sqlstorage/volumes.go b/pkg/storage/sqlstorage/volumes.go index 60905fb3d..440328044 100644 --- a/pkg/storage/sqlstorage/volumes.go +++ b/pkg/storage/sqlstorage/volumes.go @@ -8,16 +8,15 @@ import ( ) func (s *API) updateVolumes(ctx context.Context, volumes core.AccountsAssetsVolumes) error { - for account, accountVolumes := range volumes { for asset, volumes := range accountVolumes { ib := sqlbuilder.NewInsertBuilder() - inputArg := ib.Var(volumes.Input) - outputArg := ib.Var(volumes.Output) + inputArg := ib.Var(volumes.Input.String()) + outputArg := ib.Var(volumes.Output.String()) ib. InsertInto(s.schema.Table("volumes")). Cols("account", "asset", "input", "output"). - Values(account, asset, volumes.Input, volumes.Output). + Values(account, asset, volumes.Input.String(), volumes.Output.String()). SQL("ON CONFLICT (account, asset) DO UPDATE SET input = " + inputArg + ", output = " + outputArg) sqlq, args := ib.BuildWithFlavor(s.schema.Flavor())