diff --git a/internal/numscript.go b/internal/api/common/numscript.go similarity index 85% rename from internal/numscript.go rename to internal/api/common/numscript.go index 9936f86bc..61e440ca7 100644 --- a/internal/numscript.go +++ b/internal/api/common/numscript.go @@ -1,7 +1,9 @@ -package ledger +package common import ( "fmt" + "github.com/formancehq/ledger/internal" + ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "sort" "strings" @@ -13,7 +15,7 @@ type variable struct { value string } -func TxToScriptData(txData TransactionData, allowUnboundedOverdrafts bool) RunScript { +func TxToScriptData(txData ledger.TransactionData, allowUnboundedOverdrafts bool) ledgercontroller.RunScript { sb := strings.Builder{} monetaryToVars := map[string]variable{} accountsToVars := map[string]variable{} @@ -21,7 +23,7 @@ func TxToScriptData(txData TransactionData, allowUnboundedOverdrafts bool) RunSc j := 0 for _, p := range txData.Postings { if _, ok := accountsToVars[p.Source]; !ok { - if p.Source != WORLD { + if p.Source != ledger.WORLD { accountsToVars[p.Source] = variable{ name: fmt.Sprintf("va%d", i), value: p.Source, @@ -30,7 +32,7 @@ func TxToScriptData(txData TransactionData, allowUnboundedOverdrafts bool) RunSc } } if _, ok := accountsToVars[p.Destination]; !ok { - if p.Destination != WORLD { + if p.Destination != ledger.WORLD { accountsToVars[p.Destination] = variable{ name: fmt.Sprintf("va%d", i), value: p.Destination, @@ -74,7 +76,7 @@ func TxToScriptData(txData TransactionData, allowUnboundedOverdrafts bool) RunSc panic(fmt.Sprintf("monetary %s not found", m)) } sb.WriteString(fmt.Sprintf("send $%s (\n", mon.name)) - if p.Source == WORLD { + if p.Source == ledger.WORLD { sb.WriteString("\tsource = @world\n") } else { src, ok := accountsToVars[p.Source] @@ -87,7 +89,7 @@ func TxToScriptData(txData TransactionData, allowUnboundedOverdrafts bool) RunSc } sb.WriteString("\n") } - if p.Destination == WORLD { + if p.Destination == ledger.WORLD { sb.WriteString("\tdestination = @world\n") } else { dest, ok := accountsToVars[p.Destination] @@ -111,8 +113,8 @@ func TxToScriptData(txData TransactionData, allowUnboundedOverdrafts bool) RunSc txData.Metadata = metadata.Metadata{} } - return RunScript{ - Script: Script{ + return ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: sb.String(), Vars: vars, }, diff --git a/internal/api/v1/controllers_accounts_add_metadata.go b/internal/api/v1/controllers_accounts_add_metadata.go index 3d98e9ef5..028365949 100644 --- a/internal/api/v1/controllers_accounts_add_metadata.go +++ b/internal/api/v1/controllers_accounts_add_metadata.go @@ -2,6 +2,7 @@ package v1 import ( "encoding/json" + "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/ledger/pkg/accounts" "net/http" "net/url" @@ -15,13 +16,13 @@ import ( func addAccountMetadata(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) - param, err := url.PathUnescape(chi.URLParam(r, "address")) + address, err := url.PathUnescape(chi.URLParam(r, "address")) if err != nil { api.BadRequestWithDetails(w, ErrValidation, err, err.Error()) return } - if !accounts.ValidateAddress(param) { + if !accounts.ValidateAddress(address) { api.BadRequest(w, ErrValidation, errors.New("invalid account address format")) return } @@ -32,7 +33,10 @@ func addAccountMetadata(w http.ResponseWriter, r *http.Request) { return } - err = l.SaveAccountMetadata(r.Context(), getCommandParameters(r), param, m) + err = l.SaveAccountMetadata(r.Context(), getCommandParameters(r, ledger.SaveAccountMetadata{ + Address: address, + Metadata: m, + })) if err != nil { api.InternalServerError(w, r, err) return diff --git a/internal/api/v1/controllers_accounts_add_metadata_test.go b/internal/api/v1/controllers_accounts_add_metadata_test.go index 9e7432c75..91adccd88 100644 --- a/internal/api/v1/controllers_accounts_add_metadata_test.go +++ b/internal/api/v1/controllers_accounts_add_metadata_test.go @@ -87,7 +87,12 @@ func TestAccountsAddMetadata(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) if testCase.expectStatusCode == http.StatusNoContent { ledgerController.EXPECT(). - SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters{}, testCase.account, testCase.body). + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: testCase.account, + Metadata: testCase.body.(metadata.Metadata), + }, + }). Return(nil) } diff --git a/internal/api/v1/controllers_accounts_delete_metadata.go b/internal/api/v1/controllers_accounts_delete_metadata.go index cda9fd0f4..5de701536 100644 --- a/internal/api/v1/controllers_accounts_delete_metadata.go +++ b/internal/api/v1/controllers_accounts_delete_metadata.go @@ -1,6 +1,7 @@ package v1 import ( + "github.com/formancehq/ledger/internal/controller/ledger" "net/http" "net/url" @@ -10,7 +11,7 @@ import ( ) func deleteAccountMetadata(w http.ResponseWriter, r *http.Request) { - param, err := url.PathUnescape(chi.URLParam(r, "address")) + address, err := url.PathUnescape(chi.URLParam(r, "address")) if err != nil { api.BadRequestWithDetails(w, ErrValidation, err, err.Error()) return @@ -19,9 +20,10 @@ func deleteAccountMetadata(w http.ResponseWriter, r *http.Request) { if err := common.LedgerFromContext(r.Context()). DeleteAccountMetadata( r.Context(), - getCommandParameters(r), - param, - chi.URLParam(r, "key"), + getCommandParameters(r, ledger.DeleteAccountMetadata{ + Address: address, + Key: chi.URLParam(r, "key"), + }), ); err != nil { api.InternalServerError(w, r, err) return diff --git a/internal/api/v1/controllers_accounts_delete_metadata_test.go b/internal/api/v1/controllers_accounts_delete_metadata_test.go index 6f4e2fc7c..815d3caa1 100644 --- a/internal/api/v1/controllers_accounts_delete_metadata_test.go +++ b/internal/api/v1/controllers_accounts_delete_metadata_test.go @@ -60,7 +60,15 @@ func TestAccountsDeleteMetadata(t *testing.T) { if tc.expectBackendCall { ledgerController.EXPECT(). - DeleteAccountMetadata(gomock.Any(), ledgercontroller.Parameters{}, tc.account, "foo"). + DeleteAccountMetadata( + gomock.Any(), + ledgercontroller.Parameters[ledgercontroller.DeleteAccountMetadata]{ + Input: ledgercontroller.DeleteAccountMetadata{ + Address: tc.account, + Key: "foo", + }, + }, + ). Return(tc.returnErr) } diff --git a/internal/api/v1/controllers_transactions_add_metadata.go b/internal/api/v1/controllers_transactions_add_metadata.go index bfbf6ac3d..35dcc4f12 100644 --- a/internal/api/v1/controllers_transactions_add_metadata.go +++ b/internal/api/v1/controllers_transactions_add_metadata.go @@ -29,7 +29,10 @@ func addTransactionMetadata(w http.ResponseWriter, r *http.Request) { return } - if err := l.SaveTransactionMetadata(r.Context(), getCommandParameters(r), int(txID), m); err != nil { + if err := l.SaveTransactionMetadata(r.Context(), getCommandParameters(r, ledgercontroller.SaveTransactionMetadata{ + TransactionID: int(txID), + Metadata: m, + })); err != nil { switch { case errors.Is(err, ledgercontroller.ErrNotFound): api.NotFound(w, err) diff --git a/internal/api/v1/controllers_transactions_add_metadata_test.go b/internal/api/v1/controllers_transactions_add_metadata_test.go index d5eced9c1..4baa50779 100644 --- a/internal/api/v1/controllers_transactions_add_metadata_test.go +++ b/internal/api/v1/controllers_transactions_add_metadata_test.go @@ -50,7 +50,12 @@ func TestTransactionsAddMetadata(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) if testCase.expectStatusCode == http.StatusNoContent { ledgerController.EXPECT(). - SaveTransactionMetadata(gomock.Any(), ledgercontroller.Parameters{}, 0, testCase.body). + SaveTransactionMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveTransactionMetadata]{ + Input: ledgercontroller.SaveTransactionMetadata{ + TransactionID: 0, + Metadata: testCase.body.(metadata.Metadata), + }, + }). Return(nil) } diff --git a/internal/api/v1/controllers_transactions_create.go b/internal/api/v1/controllers_transactions_create.go index 3dd0373a0..6b0c2dde9 100644 --- a/internal/api/v1/controllers_transactions_create.go +++ b/internal/api/v1/controllers_transactions_create.go @@ -17,11 +17,11 @@ import ( ) type Script struct { - ledger.Script + ledgercontroller.Script Vars map[string]json.RawMessage `json:"vars"` } -func (s Script) ToCore() (*ledger.Script, error) { +func (s Script) ToCore() (*ledgercontroller.Script, error) { s.Script.Vars = map[string]string{} for k, v := range s.Vars { @@ -83,7 +83,7 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { Metadata: payload.Metadata, } - res, err := l.CreateTransaction(r.Context(), getCommandParameters(r), ledger.TxToScriptData(txData, false)) + res, err := l.CreateTransaction(r.Context(), getCommandParameters(r, common.TxToScriptData(txData, false))) if err != nil { switch { case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}): @@ -92,9 +92,10 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { api.BadRequest(w, ErrScriptCompilationFailed, err) case errors.Is(err, &ledgercontroller.ErrMetadataOverride{}): api.BadRequest(w, ErrScriptMetadataOverride, err) - case errors.Is(err, ledgercontroller.ErrNoPostings): + case errors.Is(err, ledgercontroller.ErrNoPostings) || + errors.Is(err, ledgercontroller.ErrInvalidIdempotencyInput{}): api.BadRequest(w, ErrValidation, err) - case errors.Is(err, ledgercontroller.ErrReferenceConflict{}): + case errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{}): api.WriteErrorResponse(w, http.StatusConflict, ErrConflict, err) default: api.InternalServerError(w, r, err) @@ -111,14 +112,14 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { return } - runScript := ledger.RunScript{ + runScript := ledgercontroller.RunScript{ Script: *script, Timestamp: payload.Timestamp, Reference: payload.Reference, Metadata: payload.Metadata, } - res, err := l.CreateTransaction(r.Context(), getCommandParameters(r), runScript) + res, err := l.CreateTransaction(r.Context(), getCommandParameters(r, runScript)) if err != nil { switch { case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}): @@ -126,9 +127,10 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { case errors.Is(err, &ledgercontroller.ErrInvalidVars{}) || errors.Is(err, ledgercontroller.ErrCompilationFailed{}) || errors.Is(err, &ledgercontroller.ErrMetadataOverride{}) || + errors.Is(err, ledgercontroller.ErrInvalidIdempotencyInput{}) || errors.Is(err, ledgercontroller.ErrNoPostings): api.BadRequest(w, ErrValidation, err) - case errors.Is(err, ledgercontroller.ErrReferenceConflict{}): + case errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{}): api.WriteErrorResponse(w, http.StatusConflict, ErrConflict, err) default: api.InternalServerError(w, r, err) diff --git a/internal/api/v1/controllers_transactions_create_test.go b/internal/api/v1/controllers_transactions_create_test.go index e1a82f8f6..8e27b3359 100644 --- a/internal/api/v1/controllers_transactions_create_test.go +++ b/internal/api/v1/controllers_transactions_create_test.go @@ -2,6 +2,7 @@ package v1 import ( "encoding/json" + "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" "net/http/httptest" @@ -23,7 +24,7 @@ func TestTransactionsCreate(t *testing.T) { type testCase struct { name string expectedPreview bool - expectedRunScript ledger.RunScript + expectedRunScript ledgercontroller.RunScript payload any expectedStatusCode int expectedErrorCode string @@ -35,13 +36,13 @@ func TestTransactionsCreate(t *testing.T) { name: "using plain numscript", payload: CreateTransactionRequest{ Script: Script{ - Script: ledger.Script{ + Script: ledgercontroller.Script{ Plain: `XXX`, }, }, }, - expectedRunScript: ledger.RunScript{ - Script: ledger.Script{ + expectedRunScript: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: `XXX`, Vars: map[string]string{}, }, @@ -51,7 +52,7 @@ func TestTransactionsCreate(t *testing.T) { name: "using plain numscript with variables", payload: CreateTransactionRequest{ Script: Script{ - Script: ledger.Script{ + Script: ledgercontroller.Script{ Plain: `vars { monetary $val } @@ -66,8 +67,8 @@ func TestTransactionsCreate(t *testing.T) { }, }, }, - expectedRunScript: ledger.RunScript{ - Script: ledger.Script{ + expectedRunScript: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: `vars { monetary $val } @@ -86,7 +87,7 @@ func TestTransactionsCreate(t *testing.T) { name: "using plain numscript with variables (legacy format)", payload: CreateTransactionRequest{ Script: Script{ - Script: ledger.Script{ + Script: ledgercontroller.Script{ Plain: `vars { monetary $val } @@ -104,8 +105,8 @@ func TestTransactionsCreate(t *testing.T) { }, }, }, - expectedRunScript: ledger.RunScript{ - Script: ledger.Script{ + expectedRunScript: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: `vars { monetary $val } @@ -124,7 +125,7 @@ func TestTransactionsCreate(t *testing.T) { name: "using plain numscript and dry run", payload: CreateTransactionRequest{ Script: Script{ - Script: ledger.Script{ + Script: ledgercontroller.Script{ Plain: `send ( source = @world destination = @bank @@ -132,8 +133,8 @@ func TestTransactionsCreate(t *testing.T) { }, }, }, - expectedRunScript: ledger.RunScript{ - Script: ledger.Script{ + expectedRunScript: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: `send ( source = @world destination = @bank @@ -153,7 +154,7 @@ func TestTransactionsCreate(t *testing.T) { ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), }, }, - expectedRunScript: ledger.TxToScriptData(ledger.NewTransactionData().WithPostings( + expectedRunScript: common.TxToScriptData(ledger.NewTransactionData().WithPostings( ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), ), false), }, @@ -168,7 +169,7 @@ func TestTransactionsCreate(t *testing.T) { }, }, expectedPreview: true, - expectedRunScript: ledger.TxToScriptData(ledger.NewTransactionData().WithPostings( + expectedRunScript: common.TxToScriptData(ledger.NewTransactionData().WithPostings( ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), ), false), }, @@ -190,7 +191,7 @@ func TestTransactionsCreate(t *testing.T) { }, }, Script: Script{ - Script: ledger.Script{ + Script: ledgercontroller.Script{ Plain: ` send [COIN 100] ( source = @world @@ -225,9 +226,10 @@ func TestTransactionsCreate(t *testing.T) { if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { testCase.expectedRunScript.Timestamp = time.Time{} ledgerController.EXPECT(). - CreateTransaction(gomock.Any(), ledgercontroller.Parameters{ + CreateTransaction(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.RunScript]{ DryRun: tc.expectedPreview, - }, testCase.expectedRunScript). + Input: testCase.expectedRunScript, + }). Return(pointer.For(expectedTx), nil) } diff --git a/internal/api/v1/controllers_transactions_delete_metadata.go b/internal/api/v1/controllers_transactions_delete_metadata.go index ae10ba3df..79a4f2993 100644 --- a/internal/api/v1/controllers_transactions_delete_metadata.go +++ b/internal/api/v1/controllers_transactions_delete_metadata.go @@ -23,7 +23,10 @@ func deleteTransactionMetadata(w http.ResponseWriter, r *http.Request) { metadataKey := chi.URLParam(r, "key") - if err := l.DeleteTransactionMetadata(r.Context(), getCommandParameters(r), int(transactionID), metadataKey); err != nil { + if err := l.DeleteTransactionMetadata(r.Context(), getCommandParameters(r, ledgercontroller.DeleteTransactionMetadata{ + TransactionID: int(transactionID), + Key: metadataKey, + })); err != nil { switch { case errors.Is(err, ledgercontroller.ErrNotFound): api.NotFound(w, err) diff --git a/internal/api/v1/controllers_transactions_delete_metadata_test.go b/internal/api/v1/controllers_transactions_delete_metadata_test.go index de18a1ca6..20134e87d 100644 --- a/internal/api/v1/controllers_transactions_delete_metadata_test.go +++ b/internal/api/v1/controllers_transactions_delete_metadata_test.go @@ -57,7 +57,12 @@ func TestTransactionsDeleteMetadata(t *testing.T) { if tc.expectBackendCall { ledgerController.EXPECT(). - DeleteTransactionMetadata(gomock.Any(), ledgercontroller.Parameters{}, 1, "foo"). + DeleteTransactionMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.DeleteTransactionMetadata]{ + Input: ledgercontroller.DeleteTransactionMetadata{ + TransactionID: 1, + Key: "foo", + }, + }). Return(tc.returnErr) } diff --git a/internal/api/v1/controllers_transactions_revert.go b/internal/api/v1/controllers_transactions_revert.go index 71aa4f68e..aa7348596 100644 --- a/internal/api/v1/controllers_transactions_revert.go +++ b/internal/api/v1/controllers_transactions_revert.go @@ -21,8 +21,14 @@ func revertTransaction(w http.ResponseWriter, r *http.Request) { return } - tx, err := l.RevertTransaction(r.Context(), getCommandParameters(r), int(txId), - api.QueryParamBool(r, "disableChecks"), false) + tx, err := l.RevertTransaction( + r.Context(), + getCommandParameters(r, ledgercontroller.RevertTransaction{ + Force: api.QueryParamBool(r, "disableChecks"), + AtEffectiveDate: false, + TransactionID: int(txId), + }), + ) if err != nil { switch { case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}): diff --git a/internal/api/v1/controllers_transactions_revert_test.go b/internal/api/v1/controllers_transactions_revert_test.go index 8daf217ba..b6c525b05 100644 --- a/internal/api/v1/controllers_transactions_revert_test.go +++ b/internal/api/v1/controllers_transactions_revert_test.go @@ -71,7 +71,11 @@ func TestTransactionsRevert(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) ledgerController. EXPECT(). - RevertTransaction(gomock.Any(), ledgercontroller.Parameters{}, 0, tc.expectForce, false). + RevertTransaction(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.RevertTransaction]{ + Input: ledgercontroller.RevertTransaction{ + Force: tc.expectForce, + }, + }). Return(pointer.For(tc.returnTx), tc.returnErr) router := NewRouter(systemController, auth.NewNoAuth(), "develop", testing.Verbose()) diff --git a/internal/api/v1/utils.go b/internal/api/v1/utils.go index 151e2c0cf..e1fd0541d 100644 --- a/internal/api/v1/utils.go +++ b/internal/api/v1/utils.go @@ -66,14 +66,15 @@ func getPaginatedQueryOptionsOfPITFilterWithVolumes(r *http.Request) (*ledgercon WithPageSize(pageSize)), nil } -func getCommandParameters(r *http.Request) ledgercontroller.Parameters { +func getCommandParameters[INPUT any](r *http.Request, input INPUT) ledgercontroller.Parameters[INPUT] { dryRunAsString := r.URL.Query().Get("preview") dryRun := strings.ToUpper(dryRunAsString) == "YES" || strings.ToUpper(dryRunAsString) == "TRUE" || dryRunAsString == "1" idempotencyKey := r.Header.Get("Idempotency-Key") - return ledgercontroller.Parameters{ + return ledgercontroller.Parameters[INPUT]{ DryRun: dryRun, IdempotencyKey: idempotencyKey, + Input: input, } } diff --git a/internal/api/v2/common.go b/internal/api/v2/common.go index 8cceeea13..a9641c296 100644 --- a/internal/api/v2/common.go +++ b/internal/api/v2/common.go @@ -1,6 +1,9 @@ package v2 import ( + "github.com/formancehq/go-libs/metadata" + "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/api/common" "io" "net/http" "slices" @@ -169,3 +172,32 @@ func getPaginatedQueryOptionsOfFiltersForVolumes(r *http.Request) (*ledgercontro WithPageSize(pageSize). WithQueryBuilder(qb)), nil } + +type TransactionRequest struct { + Postings ledger.Postings `json:"postings"` + Script ledgercontroller.ScriptV1 `json:"script"` + Timestamp time.Time `json:"timestamp"` + Reference string `json:"reference"` + Metadata metadata.Metadata `json:"metadata" swaggertype:"object"` +} + +func (req *TransactionRequest) ToRunScript(allowUnboundedOverdrafts bool) *ledgercontroller.RunScript { + + if len(req.Postings) > 0 { + txData := ledger.TransactionData{ + Postings: req.Postings, + Timestamp: req.Timestamp, + Reference: req.Reference, + Metadata: req.Metadata, + } + + return pointer.For(common.TxToScriptData(txData, allowUnboundedOverdrafts)) + } + + return &ledgercontroller.RunScript{ + Script: req.Script.ToCore(), + Timestamp: req.Timestamp, + Reference: req.Reference, + Metadata: req.Metadata, + } +} diff --git a/internal/api/v2/controllers_accounts_add_metadata.go b/internal/api/v2/controllers_accounts_add_metadata.go index 8b0faf058..2db572523 100644 --- a/internal/api/v2/controllers_accounts_add_metadata.go +++ b/internal/api/v2/controllers_accounts_add_metadata.go @@ -2,6 +2,7 @@ package v2 import ( "encoding/json" + "github.com/formancehq/ledger/internal/controller/ledger" "net/http" "net/url" @@ -27,7 +28,10 @@ func addAccountMetadata(w http.ResponseWriter, r *http.Request) { return } - err = l.SaveAccountMetadata(r.Context(), getCommandParameters(r), address, m) + err = l.SaveAccountMetadata(r.Context(), getCommandParameters(r, ledger.SaveAccountMetadata{ + Address: address, + Metadata: m, + })) if err != nil { api.InternalServerError(w, r, err) return diff --git a/internal/api/v2/controllers_accounts_add_metadata_test.go b/internal/api/v2/controllers_accounts_add_metadata_test.go index 25f07f3de..c1676de23 100644 --- a/internal/api/v2/controllers_accounts_add_metadata_test.go +++ b/internal/api/v2/controllers_accounts_add_metadata_test.go @@ -60,7 +60,12 @@ func TestAccountsAddMetadata(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) if testCase.expectStatusCode == http.StatusNoContent { ledgerController.EXPECT(). - SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters{}, testCase.account, testCase.body). + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: testCase.account, + Metadata: testCase.body.(metadata.Metadata), + }, + }). Return(nil) } diff --git a/internal/api/v2/controllers_accounts_delete_metadata.go b/internal/api/v2/controllers_accounts_delete_metadata.go index 4a5922783..0d8d65300 100644 --- a/internal/api/v2/controllers_accounts_delete_metadata.go +++ b/internal/api/v2/controllers_accounts_delete_metadata.go @@ -1,6 +1,7 @@ package v2 import ( + "github.com/formancehq/ledger/internal/controller/ledger" "net/http" "net/url" @@ -11,7 +12,7 @@ import ( ) func deleteAccountMetadata(w http.ResponseWriter, r *http.Request) { - param, err := url.PathUnescape(chi.URLParam(r, "address")) + address, err := url.PathUnescape(chi.URLParam(r, "address")) if err != nil { api.BadRequestWithDetails(w, ErrValidation, err, err.Error()) return @@ -20,9 +21,10 @@ func deleteAccountMetadata(w http.ResponseWriter, r *http.Request) { if err := common.LedgerFromContext(r.Context()). DeleteAccountMetadata( r.Context(), - getCommandParameters(r), - param, - chi.URLParam(r, "key"), + getCommandParameters(r, ledger.DeleteAccountMetadata{ + Address: address, + Key: chi.URLParam(r, "key"), + }), ); err != nil { api.InternalServerError(w, r, err) return diff --git a/internal/api/v2/controllers_accounts_delete_metadata_test.go b/internal/api/v2/controllers_accounts_delete_metadata_test.go index 1538f220a..c0ce238dd 100644 --- a/internal/api/v2/controllers_accounts_delete_metadata_test.go +++ b/internal/api/v2/controllers_accounts_delete_metadata_test.go @@ -60,7 +60,12 @@ func TestAccountsDeleteMetadata(t *testing.T) { if tc.expectBackendCall { ledgerController.EXPECT(). - DeleteAccountMetadata(gomock.Any(), ledgercontroller.Parameters{}, tc.account, "foo"). + DeleteAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.DeleteAccountMetadata]{ + Input: ledgercontroller.DeleteAccountMetadata{ + Address: tc.account, + Key: "foo", + }, + }). Return(tc.returnErr) } diff --git a/internal/api/v2/controllers_bulk.go b/internal/api/v2/controllers_bulk.go index ce65c55bd..a97f311b7 100644 --- a/internal/api/v2/controllers_bulk.go +++ b/internal/api/v2/controllers_bulk.go @@ -78,20 +78,19 @@ func ProcessBulk(ctx context.Context, l ledgercontroller.Controller, bulk Bulk, } for i, element := range bulk { - parameters := ledgercontroller.Parameters{ - DryRun: false, - IdempotencyKey: element.IdempotencyKey, - } - switch element.Action { case ActionCreateTransaction: - req := &ledger.TransactionRequest{} + req := &TransactionRequest{} if err := json.Unmarshal(element.Data, req); err != nil { return nil, errorsInBulk, fmt.Errorf("error parsing element %d: %s", i, err) } rs := req.ToRunScript(false) - tx, err := l.CreateTransaction(ctx, parameters, *rs) + tx, err := l.CreateTransaction(ctx, ledgercontroller.Parameters[ledgercontroller.RunScript]{ + DryRun: false, + IdempotencyKey: element.IdempotencyKey, + Input: *rs, + }) if err != nil { var code string @@ -104,7 +103,7 @@ func ProcessBulk(ctx context.Context, l ledgercontroller.Controller, bulk Bulk, code = ErrMetadataOverride case errors.Is(err, ledgercontroller.ErrNoPostings): code = ErrNoPostings - case errors.Is(err, ledgercontroller.ErrReferenceConflict{}): + case errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{}): code = ErrConflict default: code = api.ErrorInternal @@ -134,17 +133,31 @@ func ProcessBulk(ctx context.Context, l ledgercontroller.Controller, bulk Bulk, var err error switch req.TargetType { case ledger.MetaTargetTypeAccount: - targetID := "" - if err := json.Unmarshal(req.TargetID, &targetID); err != nil { + address := "" + if err := json.Unmarshal(req.TargetID, &address); err != nil { return nil, errorsInBulk, err } - err = l.SaveAccountMetadata(ctx, parameters, targetID, req.Metadata) + err = l.SaveAccountMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + DryRun: false, + IdempotencyKey: element.IdempotencyKey, + Input: ledgercontroller.SaveAccountMetadata{ + Address: address, + Metadata: req.Metadata, + }, + }) case ledger.MetaTargetTypeTransaction: - targetID := 0 - if err := json.Unmarshal(req.TargetID, &targetID); err != nil { + transactionID := 0 + if err := json.Unmarshal(req.TargetID, &transactionID); err != nil { return nil, errorsInBulk, err } - err = l.SaveTransactionMetadata(ctx, parameters, targetID, req.Metadata) + err = l.SaveTransactionMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.SaveTransactionMetadata]{ + DryRun: false, + IdempotencyKey: element.IdempotencyKey, + Input: ledgercontroller.SaveTransactionMetadata{ + TransactionID: transactionID, + Metadata: req.Metadata, + }, + }) } if err != nil { var code string @@ -174,7 +187,15 @@ func ProcessBulk(ctx context.Context, l ledgercontroller.Controller, bulk Bulk, return nil, errorsInBulk, fmt.Errorf("error parsing element %d: %s", i, err) } - tx, err := l.RevertTransaction(ctx, parameters, req.ID, req.Force, req.AtEffectiveDate) + tx, err := l.RevertTransaction(ctx, ledgercontroller.Parameters[ledgercontroller.RevertTransaction]{ + DryRun: false, + IdempotencyKey: element.IdempotencyKey, + Input: ledgercontroller.RevertTransaction{ + Force: req.Force, + AtEffectiveDate: req.AtEffectiveDate, + TransactionID: req.ID, + }, + }) if err != nil { var code string switch { @@ -207,17 +228,31 @@ func ProcessBulk(ctx context.Context, l ledgercontroller.Controller, bulk Bulk, var err error switch req.TargetType { case ledger.MetaTargetTypeAccount: - targetID := "" - if err := json.Unmarshal(req.TargetID, &targetID); err != nil { + address := "" + if err := json.Unmarshal(req.TargetID, &address); err != nil { return nil, errorsInBulk, err } - err = l.DeleteAccountMetadata(ctx, parameters, targetID, req.Key) + err = l.DeleteAccountMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.DeleteAccountMetadata]{ + DryRun: false, + IdempotencyKey: element.IdempotencyKey, + Input: ledgercontroller.DeleteAccountMetadata{ + Address: address, + Key: req.Key, + }, + }) case ledger.MetaTargetTypeTransaction: - targetID := 0 - if err := json.Unmarshal(req.TargetID, &targetID); err != nil { + transactionID := 0 + if err := json.Unmarshal(req.TargetID, &transactionID); err != nil { return nil, errorsInBulk, err } - err = l.DeleteTransactionMetadata(ctx, parameters, targetID, req.Key) + err = l.DeleteTransactionMetadata(ctx, ledgercontroller.Parameters[ledgercontroller.DeleteTransactionMetadata]{ + DryRun: false, + IdempotencyKey: element.IdempotencyKey, + Input: ledgercontroller.DeleteTransactionMetadata{ + TransactionID: transactionID, + Key: req.Key, + }, + }) } if err != nil { var code string diff --git a/internal/api/v2/controllers_bulk_test.go b/internal/api/v2/controllers_bulk_test.go index 4bc0b0248..1f63fc92b 100644 --- a/internal/api/v2/controllers_bulk_test.go +++ b/internal/api/v2/controllers_bulk_test.go @@ -3,6 +3,7 @@ package v2 import ( "bytes" "fmt" + "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" "net/http/httptest" @@ -60,10 +61,12 @@ func TestBulk(t *testing.T) { Asset: "USD/2", }} mockLedger.EXPECT(). - CreateTransaction(gomock.Any(), ledgercontroller.Parameters{}, ledger.TxToScriptData(ledger.TransactionData{ - Postings: postings, - Timestamp: now, - }, false)). + CreateTransaction(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.RunScript]{ + Input: common.TxToScriptData(ledger.TransactionData{ + Postings: postings, + Timestamp: now, + }, false), + }). Return(&ledger.Transaction{ TransactionData: ledger.TransactionData{ Postings: postings, @@ -104,8 +107,13 @@ func TestBulk(t *testing.T) { }]`, expectations: func(mockLedger *ledgercontroller.MockController) { mockLedger.EXPECT(). - SaveTransactionMetadata(gomock.Any(), ledgercontroller.Parameters{}, 1, metadata.Metadata{ - "foo": "bar", + SaveTransactionMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveTransactionMetadata]{ + Input: ledgercontroller.SaveTransactionMetadata{ + TransactionID: 1, + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, }). Return(nil) }, @@ -127,8 +135,13 @@ func TestBulk(t *testing.T) { }]`, expectations: func(mockLedger *ledgercontroller.MockController) { mockLedger.EXPECT(). - SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters{}, "world", metadata.Metadata{ - "foo": "bar", + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, }). Return(nil) }, @@ -146,7 +159,11 @@ func TestBulk(t *testing.T) { }]`, expectations: func(mockLedger *ledgercontroller.MockController) { mockLedger.EXPECT(). - RevertTransaction(gomock.Any(), ledgercontroller.Parameters{}, 1, false, false). + RevertTransaction(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.RevertTransaction]{ + Input: ledgercontroller.RevertTransaction{ + TransactionID: 1, + }, + }). Return(&ledger.Transaction{}, nil) }, expectResults: []Result{{ @@ -172,7 +189,12 @@ func TestBulk(t *testing.T) { }]`, expectations: func(mockLedger *ledgercontroller.MockController) { mockLedger.EXPECT(). - DeleteTransactionMetadata(gomock.Any(), ledgercontroller.Parameters{}, 1, "foo"). + DeleteTransactionMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.DeleteTransactionMetadata]{ + Input: ledgercontroller.DeleteTransactionMetadata{ + TransactionID: 1, + Key: "foo", + }, + }). Return(nil) }, expectResults: []Result{{ @@ -215,13 +237,23 @@ func TestBulk(t *testing.T) { ]`, expectations: func(mockLedger *ledgercontroller.MockController) { mockLedger.EXPECT(). - SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters{}, "world", metadata.Metadata{ - "foo": "bar", + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, }). Return(nil) mockLedger.EXPECT(). - SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters{}, "world", metadata.Metadata{ - "foo2": "bar2", + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo2": "bar2", + }, + }, }). Return(errors.New("unexpected error")) }, @@ -273,18 +305,33 @@ func TestBulk(t *testing.T) { }, expectations: func(mockLedger *ledgercontroller.MockController) { mockLedger.EXPECT(). - SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters{}, "world", metadata.Metadata{ - "foo": "bar", + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo": "bar", + }, + }, }). Return(nil) mockLedger.EXPECT(). - SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters{}, "world", metadata.Metadata{ - "foo2": "bar2", + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo2": "bar2", + }, + }, }). Return(errors.New("unexpected error")) mockLedger.EXPECT(). - SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters{}, "world", metadata.Metadata{ - "foo3": "bar3", + SaveAccountMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveAccountMetadata]{ + Input: ledgercontroller.SaveAccountMetadata{ + Address: "world", + Metadata: metadata.Metadata{ + "foo3": "bar3", + }, + }, }). Return(nil) }, diff --git a/internal/api/v2/controllers_transactions_add_metadata.go b/internal/api/v2/controllers_transactions_add_metadata.go index b667a79e1..2cae7eef6 100644 --- a/internal/api/v2/controllers_transactions_add_metadata.go +++ b/internal/api/v2/controllers_transactions_add_metadata.go @@ -29,7 +29,10 @@ func addTransactionMetadata(w http.ResponseWriter, r *http.Request) { return } - if err := l.SaveTransactionMetadata(r.Context(), getCommandParameters(r), int(txID), m); err != nil { + if err := l.SaveTransactionMetadata(r.Context(), getCommandParameters(r, ledgercontroller.SaveTransactionMetadata{ + TransactionID: int(txID), + Metadata: m, + })); err != nil { switch { case errors.Is(err, ledgercontroller.ErrNotFound): api.NotFound(w, err) diff --git a/internal/api/v2/controllers_transactions_add_metadata_test.go b/internal/api/v2/controllers_transactions_add_metadata_test.go index dc5838e86..443dcd229 100644 --- a/internal/api/v2/controllers_transactions_add_metadata_test.go +++ b/internal/api/v2/controllers_transactions_add_metadata_test.go @@ -88,7 +88,12 @@ func TestTransactionsAddMetadata(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) if testCase.expectBackendCall { ledgerController.EXPECT(). - SaveTransactionMetadata(gomock.Any(), ledgercontroller.Parameters{}, 1, testCase.body). + SaveTransactionMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.SaveTransactionMetadata]{ + Input: ledgercontroller.SaveTransactionMetadata{ + TransactionID: 1, + Metadata: testCase.body.(metadata.Metadata), + }, + }). Return(testCase.returnErr) } diff --git a/internal/api/v2/controllers_transactions_create.go b/internal/api/v2/controllers_transactions_create.go index f59ed136c..a036f6168 100644 --- a/internal/api/v2/controllers_transactions_create.go +++ b/internal/api/v2/controllers_transactions_create.go @@ -7,7 +7,6 @@ import ( ledgercontroller "github.com/formancehq/ledger/internal/controller/ledger" "github.com/formancehq/go-libs/api" - ledger "github.com/formancehq/ledger/internal" "github.com/formancehq/ledger/internal/api/common" "github.com/pkg/errors" ) @@ -15,7 +14,7 @@ import ( func createTransaction(w http.ResponseWriter, r *http.Request) { l := common.LedgerFromContext(r.Context()) - payload := ledger.TransactionRequest{} + payload := TransactionRequest{} if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { api.BadRequest(w, ErrValidation, errors.New("invalid transaction format")) return @@ -31,7 +30,7 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { return } - res, err := l.CreateTransaction(r.Context(), getCommandParameters(r), *payload.ToRunScript(api.QueryParamBool(r, "force"))) + res, err := l.CreateTransaction(r.Context(), getCommandParameters(r, *payload.ToRunScript(api.QueryParamBool(r, "force")))) if err != nil { switch { case errors.Is(err, &ledgercontroller.ErrInsufficientFunds{}): @@ -42,8 +41,10 @@ func createTransaction(w http.ResponseWriter, r *http.Request) { api.BadRequest(w, ErrMetadataOverride, err) case errors.Is(err, ledgercontroller.ErrNoPostings): api.BadRequest(w, ErrNoPostings, err) - case errors.Is(err, ledgercontroller.ErrReferenceConflict{}): + case errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{}): api.WriteErrorResponse(w, http.StatusConflict, ErrConflict, err) + case errors.Is(err, ledgercontroller.ErrInvalidIdempotencyInput{}): + api.BadRequest(w, ErrValidation, err) default: api.InternalServerError(w, r, err) } diff --git a/internal/api/v2/controllers_transactions_create_test.go b/internal/api/v2/controllers_transactions_create_test.go index 8772b091d..906c16da3 100644 --- a/internal/api/v2/controllers_transactions_create_test.go +++ b/internal/api/v2/controllers_transactions_create_test.go @@ -1,6 +1,7 @@ package v2 import ( + "github.com/formancehq/ledger/internal/api/common" "math/big" "net/http" "net/http/httptest" @@ -23,7 +24,7 @@ func TestTransactionCreate(t *testing.T) { type testCase struct { name string expectedDryRun bool - expectedRunScript ledger.RunScript + expectedRunScript ledgercontroller.RunScript returnError error payload any expectedStatusCode int @@ -35,15 +36,15 @@ func TestTransactionCreate(t *testing.T) { testCases := []testCase{ { name: "using plain numscript", - payload: ledger.TransactionRequest{ - Script: ledger.ScriptV1{ - Script: ledger.Script{ + payload: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ Plain: `XXX`, }, }, }, - expectedRunScript: ledger.RunScript{ - Script: ledger.Script{ + expectedRunScript: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: `XXX`, Vars: map[string]string{}, }, @@ -52,9 +53,9 @@ func TestTransactionCreate(t *testing.T) { }, { name: "using plain numscript with variables", - payload: ledger.TransactionRequest{ - Script: ledger.ScriptV1{ - Script: ledger.Script{ + payload: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ Plain: `vars { monetary $val } @@ -70,8 +71,8 @@ func TestTransactionCreate(t *testing.T) { }, }, expectControllerCall: true, - expectedRunScript: ledger.RunScript{ - Script: ledger.Script{ + expectedRunScript: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: `vars { monetary $val } @@ -89,9 +90,9 @@ func TestTransactionCreate(t *testing.T) { { name: "using plain numscript with variables (legacy format)", expectControllerCall: true, - payload: ledger.TransactionRequest{ - Script: ledger.ScriptV1{ - Script: ledger.Script{ + payload: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ Plain: `vars { monetary $val } @@ -109,8 +110,8 @@ func TestTransactionCreate(t *testing.T) { }, }, }, - expectedRunScript: ledger.RunScript{ - Script: ledger.Script{ + expectedRunScript: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: `vars { monetary $val } @@ -128,9 +129,9 @@ func TestTransactionCreate(t *testing.T) { { name: "using plain numscript and dry run", expectControllerCall: true, - payload: ledger.TransactionRequest{ - Script: ledger.ScriptV1{ - Script: ledger.Script{ + payload: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ Plain: `send ( source = @world destination = @bank @@ -138,8 +139,8 @@ func TestTransactionCreate(t *testing.T) { }, }, }, - expectedRunScript: ledger.RunScript{ - Script: ledger.Script{ + expectedRunScript: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: `send ( source = @world destination = @bank @@ -155,12 +156,12 @@ func TestTransactionCreate(t *testing.T) { { name: "using JSON postings", expectControllerCall: true, - payload: ledger.TransactionRequest{ + payload: TransactionRequest{ Postings: []ledger.Posting{ ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), }, }, - expectedRunScript: ledger.TxToScriptData(ledger.NewTransactionData().WithPostings( + expectedRunScript: common.TxToScriptData(ledger.NewTransactionData().WithPostings( ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), ), false), }, @@ -170,19 +171,19 @@ func TestTransactionCreate(t *testing.T) { queryParams: url.Values{ "dryRun": []string{"true"}, }, - payload: ledger.TransactionRequest{ + payload: TransactionRequest{ Postings: []ledger.Posting{ ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), }, }, expectedDryRun: true, - expectedRunScript: ledger.TxToScriptData(ledger.NewTransactionData().WithPostings( + expectedRunScript: common.TxToScriptData(ledger.NewTransactionData().WithPostings( ledger.NewPosting("world", "bank", "USD", big.NewInt(100)), ), false), }, { name: "no postings or script", - payload: ledger.TransactionRequest{ + payload: TransactionRequest{ Metadata: map[string]string{}, }, expectedStatusCode: http.StatusBadRequest, @@ -191,7 +192,7 @@ func TestTransactionCreate(t *testing.T) { }, { name: "postings and script", - payload: ledger.TransactionRequest{ + payload: TransactionRequest{ Postings: ledger.Postings{ { Source: "world", @@ -200,8 +201,8 @@ func TestTransactionCreate(t *testing.T) { Asset: "COIN", }, }, - Script: ledger.ScriptV1{ - Script: ledger.Script{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ Plain: ` send [COIN 100] ( source = @world @@ -222,15 +223,15 @@ func TestTransactionCreate(t *testing.T) { { name: "with insufficient funds", expectControllerCall: true, - payload: ledger.TransactionRequest{ - Script: ledger.ScriptV1{ - Script: ledger.Script{ + payload: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ Plain: `XXX`, }, }, }, - expectedRunScript: ledger.RunScript{ - Script: ledger.Script{ + expectedRunScript: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: `XXX`, Vars: map[string]string{}, }, @@ -241,7 +242,7 @@ func TestTransactionCreate(t *testing.T) { }, { name: "using JSON postings and negative amount", - payload: ledger.TransactionRequest{ + payload: TransactionRequest{ Postings: []ledger.Posting{ ledger.NewPosting("world", "bank", "USD", big.NewInt(-100)), }, @@ -249,7 +250,7 @@ func TestTransactionCreate(t *testing.T) { expectControllerCall: true, expectedStatusCode: http.StatusBadRequest, expectedErrorCode: ErrCompilationFailed, - expectedRunScript: ledger.TxToScriptData(ledger.NewTransactionData().WithPostings( + expectedRunScript: common.TxToScriptData(ledger.NewTransactionData().WithPostings( ledger.NewPosting("world", "bank", "USD", big.NewInt(-100)), ), false), returnError: &ledgercontroller.ErrInvalidVars{}, @@ -257,9 +258,9 @@ func TestTransactionCreate(t *testing.T) { { expectControllerCall: true, name: "numscript and negative amount", - payload: ledger.TransactionRequest{ - Script: ledger.ScriptV1{ - Script: ledger.Script{ + payload: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ Plain: `send [COIN -100] ( source = @world destination = @bob @@ -269,8 +270,8 @@ func TestTransactionCreate(t *testing.T) { }, expectedStatusCode: http.StatusBadRequest, expectedErrorCode: ErrCompilationFailed, - expectedRunScript: ledger.RunScript{ - Script: ledger.Script{ + expectedRunScript: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: `send [COIN -100] ( source = @world destination = @bob @@ -283,9 +284,9 @@ func TestTransactionCreate(t *testing.T) { { name: "numscript and compilation failed", expectControllerCall: true, - payload: ledger.TransactionRequest{ - Script: ledger.ScriptV1{ - Script: ledger.Script{ + payload: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ Plain: `send [COIN XXX] ( source = @world destination = @bob @@ -295,8 +296,8 @@ func TestTransactionCreate(t *testing.T) { }, expectedStatusCode: http.StatusBadRequest, expectedErrorCode: ErrCompilationFailed, - expectedRunScript: ledger.RunScript{ - Script: ledger.Script{ + expectedRunScript: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: `send [COIN XXX] ( source = @world destination = @bob @@ -309,17 +310,17 @@ func TestTransactionCreate(t *testing.T) { { name: "numscript and no postings", expectControllerCall: true, - payload: ledger.TransactionRequest{ - Script: ledger.ScriptV1{ - Script: ledger.Script{ + payload: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ Plain: `vars {}`, }, }, }, expectedStatusCode: http.StatusBadRequest, expectedErrorCode: ErrNoPostings, - expectedRunScript: ledger.RunScript{ - Script: ledger.Script{ + expectedRunScript: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: `vars {}`, Vars: map[string]string{}, }, @@ -329,9 +330,9 @@ func TestTransactionCreate(t *testing.T) { { name: "numscript and metadata override", expectControllerCall: true, - payload: ledger.TransactionRequest{ - Script: ledger.ScriptV1{ - Script: ledger.Script{ + payload: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ Plain: `send [COIN 100] ( source = @world destination = @bob @@ -346,8 +347,8 @@ func TestTransactionCreate(t *testing.T) { }, expectedStatusCode: http.StatusBadRequest, expectedErrorCode: ErrMetadataOverride, - expectedRunScript: ledger.RunScript{ - Script: ledger.Script{ + expectedRunScript: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: `send [COIN 100] ( source = @world destination = @bob @@ -365,9 +366,9 @@ func TestTransactionCreate(t *testing.T) { { name: "unexpected error", expectControllerCall: true, - payload: ledger.TransactionRequest{ - Script: ledger.ScriptV1{ - Script: ledger.Script{ + payload: TransactionRequest{ + Script: ledgercontroller.ScriptV1{ + Script: ledgercontroller.Script{ Plain: `send [COIN 100] ( source = @world destination = @bob @@ -377,8 +378,8 @@ func TestTransactionCreate(t *testing.T) { }, expectedStatusCode: http.StatusInternalServerError, expectedErrorCode: api.ErrorInternal, - expectedRunScript: ledger.RunScript{ - Script: ledger.Script{ + expectedRunScript: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ Plain: `send [COIN 100] ( source = @world destination = @bob @@ -405,9 +406,10 @@ func TestTransactionCreate(t *testing.T) { if testCase.expectControllerCall { testCase.expectedRunScript.Timestamp = time.Time{} expect := ledgerController.EXPECT(). - CreateTransaction(gomock.Any(), ledgercontroller.Parameters{ + CreateTransaction(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.RunScript]{ DryRun: tc.expectedDryRun, - }, testCase.expectedRunScript) + Input: testCase.expectedRunScript, + }) if tc.returnError == nil { expect.Return(pointer.For(expectedTx), nil) diff --git a/internal/api/v2/controllers_transactions_delete_metadata.go b/internal/api/v2/controllers_transactions_delete_metadata.go index 379f988c4..0b4a26f70 100644 --- a/internal/api/v2/controllers_transactions_delete_metadata.go +++ b/internal/api/v2/controllers_transactions_delete_metadata.go @@ -25,7 +25,10 @@ func deleteTransactionMetadata(w http.ResponseWriter, r *http.Request) { metadataKey := chi.URLParam(r, "key") - if err := l.DeleteTransactionMetadata(r.Context(), getCommandParameters(r), int(txID), metadataKey); err != nil { + if err := l.DeleteTransactionMetadata(r.Context(), getCommandParameters(r, ledgercontroller.DeleteTransactionMetadata{ + TransactionID: int(txID), + Key: metadataKey, + })); err != nil { switch { case errors.Is(err, ledgercontroller.ErrNotFound): api.NotFound(w, err) diff --git a/internal/api/v2/controllers_transactions_delete_metadata_test.go b/internal/api/v2/controllers_transactions_delete_metadata_test.go index bfbde9087..78a5e1779 100644 --- a/internal/api/v2/controllers_transactions_delete_metadata_test.go +++ b/internal/api/v2/controllers_transactions_delete_metadata_test.go @@ -57,7 +57,12 @@ func TestTransactionsDeleteMetadata(t *testing.T) { if tc.expectBackendCall { ledgerController.EXPECT(). - DeleteTransactionMetadata(gomock.Any(), ledgercontroller.Parameters{}, 1, "foo"). + DeleteTransactionMetadata(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.DeleteTransactionMetadata]{ + Input: ledgercontroller.DeleteTransactionMetadata{ + TransactionID: 1, + Key: "foo", + }, + }). Return(tc.returnErr) } diff --git a/internal/api/v2/controllers_transactions_revert.go b/internal/api/v2/controllers_transactions_revert.go index dd9aee86a..36c7185c8 100644 --- a/internal/api/v2/controllers_transactions_revert.go +++ b/internal/api/v2/controllers_transactions_revert.go @@ -21,9 +21,13 @@ func revertTransaction(w http.ResponseWriter, r *http.Request) { return } - tx, err := l.RevertTransaction(r.Context(), getCommandParameters(r), int(txId), - api.QueryParamBool(r, "force"), - api.QueryParamBool(r, "atEffectiveDate"), + tx, err := l.RevertTransaction( + r.Context(), + getCommandParameters(r, ledgercontroller.RevertTransaction{ + Force: api.QueryParamBool(r, "force"), + AtEffectiveDate: api.QueryParamBool(r, "atEffectiveDate"), + TransactionID: int(txId), + }), ) if err != nil { switch { diff --git a/internal/api/v2/controllers_transactions_revert_test.go b/internal/api/v2/controllers_transactions_revert_test.go index 17d676610..9adbdd343 100644 --- a/internal/api/v2/controllers_transactions_revert_test.go +++ b/internal/api/v2/controllers_transactions_revert_test.go @@ -71,7 +71,11 @@ func TestTransactionsRevert(t *testing.T) { systemController, ledgerController := newTestingSystemController(t, true) ledgerController. EXPECT(). - RevertTransaction(gomock.Any(), ledgercontroller.Parameters{}, 0, tc.expectForce, false). + RevertTransaction(gomock.Any(), ledgercontroller.Parameters[ledgercontroller.RevertTransaction]{ + Input: ledgercontroller.RevertTransaction{ + Force: tc.expectForce, + }, + }). Return(pointer.For(tc.returnTx), tc.returnErr) router := NewRouter(systemController, auth.NewNoAuth(), "develop", testing.Verbose()) diff --git a/internal/api/v2/query.go b/internal/api/v2/query.go index c55922f91..184a36ad1 100644 --- a/internal/api/v2/query.go +++ b/internal/api/v2/query.go @@ -16,9 +16,10 @@ const ( QueryKeyCursor = "cursor" ) -func getCommandParameters(r *http.Request) ledger.Parameters { - return ledger.Parameters{ +func getCommandParameters[INPUT any](r *http.Request, input INPUT) ledger.Parameters[INPUT] { + return ledger.Parameters[INPUT]{ DryRun: api.QueryParamBool(r, "dryRun"), IdempotencyKey: api.IdempotencyKeyFromRequest(r), + Input: input, } } diff --git a/internal/controller/ledger/controller.go b/internal/controller/ledger/controller.go index 851a20ab7..25d745ed4 100644 --- a/internal/controller/ledger/controller.go +++ b/internal/controller/ledger/controller.go @@ -4,7 +4,6 @@ import ( "context" "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/metadata" "github.com/formancehq/go-libs/migrations" ledger "github.com/formancehq/ledger/internal" ) @@ -33,10 +32,10 @@ type Controller interface { // * ErrCompilationFailed // * ErrMetadataOverride // * ErrInvalidVars - // * ErrReferenceConflict + // * ErrTransactionReferenceConflict // * ErrIdempotencyKeyConflict // * ErrInsufficientFunds - CreateTransaction(ctx context.Context, parameters Parameters, data ledger.RunScript) (*ledger.Transaction, error) + CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Transaction, error) // RevertTransaction allow to revert a transaction. // It can return following errors: // * ErrInsufficientFunds @@ -44,22 +43,22 @@ type Controller interface { // * ErrNotFound // Parameter force indicate we want to force revert the transaction even if the accounts does not have funds // Parameter atEffectiveDate indicate we want to set the timestamp of the newly created transaction on the timestamp of the reverted transaction - RevertTransaction(ctx context.Context, parameters Parameters, id int, force, atEffectiveDate bool) (*ledger.Transaction, error) + RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Transaction, error) // SaveTransactionMetadata allow to add metadata to an existing transaction // It can return following errors: // * ErrNotFound - SaveTransactionMetadata(ctx context.Context, parameters Parameters, id int, m metadata.Metadata) error + SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) error // SaveAccountMetadata allow to add metadata to an account // If the account does not exist, it is created - SaveAccountMetadata(ctx context.Context, parameters Parameters, id string, m metadata.Metadata) error + SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) error // DeleteTransactionMetadata allow to remove metadata of a transaction // It can return following errors: // * ErrNotFound : indicate the transaction was not found OR the metadata does not exist on the transaction - DeleteTransactionMetadata(ctx context.Context, parameters Parameters, id int, key string) error + DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) error // DeleteAccountMetadata allow to remove metadata of an account // It can return following errors: // * ErrNotFound : indicate the account was not found OR the metadata does not exist on the account - DeleteAccountMetadata(ctx context.Context, parameters Parameters, targetID string, key string) error + DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) error // Import allow to import the logs of an existing ledger // It can return following errors: // * ErrImport diff --git a/internal/controller/ledger/controller_default.go b/internal/controller/ledger/controller_default.go index 9586a8924..cf2d187dc 100644 --- a/internal/controller/ledger/controller_default.go +++ b/internal/controller/ledger/controller_default.go @@ -53,74 +53,6 @@ func (ctrl *DefaultController) GetMigrationsInfo(ctx context.Context) ([]migrati return ctrl.store.GetMigrationsInfo(ctx) } -func (ctrl *DefaultController) runTx(ctx context.Context, parameters Parameters, fn func(sqlTX TX) (*ledger.Log, error)) (*ledger.Log, error) { - var log *ledger.Log - err := ctrl.store.WithTX(ctx, nil, func(tx TX) (commit bool, err error) { - log, err = fn(tx) - if err != nil { - return false, err - } - log.IdempotencyKey = parameters.IdempotencyKey - - _, err = tracing.TraceWithLatency(ctx, "InsertLog", func(ctx context.Context) (*struct{}, error) { - return nil, tx.InsertLog(ctx, log) - }) - if err != nil { - return false, errors.Wrap(err, "failed to insert log") - } - logging.FromContext(ctx).Debugf("log inserted with id %d", log.ID) - - if parameters.DryRun { - return false, nil - } - - return true, nil - }) - return log, err -} - -// todo: handle too many clients error -func (ctrl *DefaultController) forgeLog(ctx context.Context, parameters Parameters, fn func(sqlTX TX) (*ledger.Log, error)) (*ledger.Log, error) { - - if parameters.IdempotencyKey != "" { - log, err := ctrl.store.ReadLogWithIdempotencyKey(ctx, parameters.IdempotencyKey) - if err != nil && !errors.Is(err, postgres.ErrNotFound) { - return nil, err - } - if err == nil { - // todo: prevent errors by checking than the original parameters match the actual parameters. - return log, nil - } - } - - for { - log, err := ctrl.runTx(ctx, parameters, fn) - if err != nil { - switch { - case errors.Is(err, postgres.ErrDeadlockDetected): - logging.FromContext(ctx).Info("deadlock detected, retrying...") - continue - // A log with the IK could have been inserted in the meantime, read again the database to retrieve it - case errors.Is(err, ErrIdempotencyKeyConflict{}): - log, err := ctrl.store.ReadLogWithIdempotencyKey(ctx, parameters.IdempotencyKey) - if err != nil && !errors.Is(err, postgres.ErrNotFound) { - return nil, err - } - if errors.Is(err, postgres.ErrNotFound) { - logging.FromContext(ctx).Errorf("incoherent error, received duplicate IK but log not found in database") - return nil, err - } - - return log, nil - default: - return nil, errors.Wrap(err, "unexpected error while forging log") - } - } - - return log, nil - } -} - func (ctrl *DefaultController) ListTransactions(ctx context.Context, q ListTransactionsQuery) (*bunpaginate.Cursor[ledger.Transaction], error) { return tracing.Trace(ctx, "ListTransactions", func(ctx context.Context) (*bunpaginate.Cursor[ledger.Transaction], error) { txs, err := ctrl.store.ListTransactions(ctx, q) @@ -310,21 +242,20 @@ func (ctrl *DefaultController) GetVolumesWithBalances(ctx context.Context, q Get }) } -func (ctrl *DefaultController) CreateTransaction(ctx context.Context, parameters Parameters, runScript ledger.RunScript) (*ledger.Transaction, error) { +func (ctrl *DefaultController) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Transaction, error) { log, err := tracing.TraceWithLatency(ctx, "CreateTransaction", func(ctx context.Context) (*ledger.Log, error) { logger := logging.FromContext(ctx).WithField("req", uuid.NewString()[:8]) ctx = logging.ContextWithLogger(ctx, logger) - m, err := ctrl.machineFactory.Make(runScript.Plain) + m, err := ctrl.machineFactory.Make(parameters.Input.Plain) if err != nil { return nil, errors.Wrap(err, "failed to compile script") } - return ctrl.forgeLog(ctx, parameters, func(sqlTX TX) (*ledger.Log, error) { - + return forgeLog(ctx, ctrl.store, parameters, func(ctx context.Context, sqlTX TX, input RunScript) (*ledger.Log, error) { result, err := tracing.TraceWithLatency(ctx, "ExecuteMachine", func(ctx context.Context) (*MachineResult, error) { - return m.Execute(ctx, newVmStoreAdapter(sqlTX), runScript.Vars) + return m.Execute(ctx, newVmStoreAdapter(sqlTX), input.Vars) }) if err != nil { return nil, errors.Wrap(err, "failed to execute program") @@ -338,7 +269,7 @@ func (ctrl *DefaultController) CreateTransaction(ctx context.Context, parameters if finalMetadata == nil { finalMetadata = metadata.Metadata{} } - for k, v := range runScript.Metadata { + for k, v := range input.Metadata { if finalMetadata[k] != "" { return nil, newErrMetadataOverride(k) } @@ -346,7 +277,7 @@ func (ctrl *DefaultController) CreateTransaction(ctx context.Context, parameters } now := time.Now() - ts := runScript.Timestamp + ts := input.Timestamp if ts.IsZero() { ts = now } @@ -356,7 +287,7 @@ func (ctrl *DefaultController) CreateTransaction(ctx context.Context, parameters WithMetadata(finalMetadata). WithTimestamp(ts). WithInsertedAt(now). - WithReference(runScript.Reference) + WithReference(input.Reference) err = sqlTX.CommitTransaction(ctx, &transaction) if err != nil { return nil, err @@ -383,21 +314,21 @@ func (ctrl *DefaultController) CreateTransaction(ctx context.Context, parameters return &transaction, nil } -func (ctrl *DefaultController) RevertTransaction(ctx context.Context, parameters Parameters, id int, force, atEffectiveDate bool) (*ledger.Transaction, error) { +func (ctrl *DefaultController) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Transaction, error) { var originalTransaction *ledger.Transaction ret, err := tracing.Trace(ctx, "RevertTransaction", func(ctx context.Context) (*ledger.Transaction, error) { - log, err := ctrl.forgeLog(ctx, parameters, func(sqlTX TX) (*ledger.Log, error) { + log, err := forgeLog(ctx, ctrl.store, parameters, func(ctx context.Context, sqlTX TX, input RevertTransaction) (*ledger.Log, error) { var ( hasBeenReverted bool err error ) - originalTransaction, hasBeenReverted, err = sqlTX.RevertTransaction(ctx, id) + originalTransaction, hasBeenReverted, err = sqlTX.RevertTransaction(ctx, input.TransactionID) if err != nil { return nil, err } if !hasBeenReverted { - return nil, newErrAlreadyReverted(id) + return nil, newErrAlreadyReverted(input.TransactionID) } bq := originalTransaction.InvolvedAccountAndAssets() @@ -408,14 +339,14 @@ func (ctrl *DefaultController) RevertTransaction(ctx context.Context, parameters } reversedTx := originalTransaction.Reverse() - if atEffectiveDate { + if input.AtEffectiveDate { reversedTx = reversedTx.WithTimestamp(originalTransaction.Timestamp) } else { reversedTx = reversedTx.WithTimestamp(*originalTransaction.RevertedAt) } // Check balances after the revert, all balances must be greater than 0 - if !force { + if !input.Force { for _, posting := range reversedTx.Postings { balances[posting.Source][posting.Asset] = balances[posting.Source][posting.Asset].Add( balances[posting.Source][posting.Asset], @@ -443,7 +374,7 @@ func (ctrl *DefaultController) RevertTransaction(ctx context.Context, parameters return nil, errors.Wrap(err, "failed to insert transaction") } - return pointer.For(ledger.NewRevertedTransactionLog(id, reversedTx)), nil + return pointer.For(ledger.NewRevertedTransactionLog(input.TransactionID, reversedTx)), nil }) if err != nil { return nil, err @@ -462,34 +393,39 @@ func (ctrl *DefaultController) RevertTransaction(ctx context.Context, parameters return ret, nil } -func (ctrl *DefaultController) SaveTransactionMetadata(ctx context.Context, parameters Parameters, id int, m metadata.Metadata) error { - if err := tracing.SkipResult(tracing.Trace(ctx, "SaveTransactionMetadata", tracing.NoResult(func(ctx context.Context) error { - _, err := ctrl.forgeLog(ctx, parameters, func(sqlTX TX) (*ledger.Log, error) { - if _, _, err := sqlTX.UpdateTransactionMetadata(ctx, id, m); err != nil { +func (ctrl *DefaultController) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) error { + if err := tracing.SkipResult(tracing.Trace(ctx, "SaveTransactionMetadata", func(ctx context.Context) (*ledger.Log, error) { + return forgeLog(ctx, ctrl.store, parameters, func(ctx context.Context, sqlTX TX, input SaveTransactionMetadata) (*ledger.Log, error) { + if _, _, err := sqlTX.UpdateTransactionMetadata(ctx, input.TransactionID, input.Metadata); err != nil { return nil, err } - return pointer.For(ledger.NewSetMetadataOnTransactionLog(id, m)), nil + return pointer.For(ledger.NewSetMetadataOnTransactionLog(input.TransactionID, input.Metadata)), nil }) - return err - }))); err != nil { + })); err != nil { return err } if ctrl.listener != nil { - ctrl.listener.SavedMetadata(ctx, ctrl.ledger.Name, ledger.MetaTargetTypeTransaction, fmt.Sprint(id), m) + ctrl.listener.SavedMetadata( + ctx, + ctrl.ledger.Name, + ledger.MetaTargetTypeTransaction, + fmt.Sprint(parameters.Input.TransactionID), + parameters.Input.Metadata, + ) } return nil } -func (ctrl *DefaultController) SaveAccountMetadata(ctx context.Context, parameters Parameters, address string, m metadata.Metadata) error { - if err := tracing.SkipResult(tracing.Trace(ctx, "SaveAccountMetadata", tracing.NoResult(func(ctx context.Context) error { +func (ctrl *DefaultController) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) error { + if err := tracing.SkipResult(tracing.Trace(ctx, "SaveAccountMetadata", func(ctx context.Context) (*ledger.Log, error) { now := time.Now() - _, err := ctrl.forgeLog(ctx, parameters, func(sqlTX TX) (*ledger.Log, error) { + return forgeLog(ctx, ctrl.store, parameters, func(ctx context.Context, sqlTX TX, input SaveAccountMetadata) (*ledger.Log, error) { if _, err := sqlTX.UpsertAccount(ctx, &ledger.Account{ - Address: address, - Metadata: m, + Address: input.Address, + Metadata: input.Metadata, FirstUsage: now, InsertionDate: now, UpdatedAt: now, @@ -497,24 +433,29 @@ func (ctrl *DefaultController) SaveAccountMetadata(ctx context.Context, paramete return nil, err } - return pointer.For(ledger.NewSetMetadataOnAccountLog(address, m)), nil + return pointer.For(ledger.NewSetMetadataOnAccountLog(input.Address, input.Metadata)), nil }) - return err - }))); err != nil { + })); err != nil { return err } if ctrl.listener != nil { - ctrl.listener.SavedMetadata(ctx, ctrl.ledger.Name, ledger.MetaTargetTypeAccount, address, m) + ctrl.listener.SavedMetadata( + ctx, + ctrl.ledger.Name, + ledger.MetaTargetTypeAccount, + parameters.Input.Address, + parameters.Input.Metadata, + ) } return nil } -func (ctrl *DefaultController) DeleteTransactionMetadata(ctx context.Context, parameters Parameters, targetID int, key string) error { +func (ctrl *DefaultController) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) error { if err := tracing.SkipResult(tracing.Trace(ctx, "DeleteTransactionMetadata", func(ctx context.Context) (*ledger.Log, error) { - return ctrl.forgeLog(ctx, parameters, func(sqlTX TX) (*ledger.Log, error) { - _, modified, err := sqlTX.DeleteTransactionMetadata(ctx, targetID, key) + return forgeLog(ctx, ctrl.store, parameters, func(ctx context.Context, sqlTX TX, input DeleteTransactionMetadata) (*ledger.Log, error) { + _, modified, err := sqlTX.DeleteTransactionMetadata(ctx, input.TransactionID, input.Key) if err != nil { return nil, err } @@ -523,35 +464,48 @@ func (ctrl *DefaultController) DeleteTransactionMetadata(ctx context.Context, pa return nil, postgres.ErrNotFound } - return pointer.For(ledger.NewDeleteTransactionMetadataLog(targetID, key)), nil + return pointer.For(ledger.NewDeleteTransactionMetadataLog(input.TransactionID, input.Key)), nil }) })); err != nil { return err } + // todo: events should not be sent in dry run! if ctrl.listener != nil { - ctrl.listener.DeletedMetadata(ctx, ctrl.ledger.Name, ledger.MetaTargetTypeTransaction, fmt.Sprint(targetID), key) + ctrl.listener.DeletedMetadata( + ctx, + ctrl.ledger.Name, + ledger.MetaTargetTypeTransaction, + fmt.Sprint(parameters.Input.TransactionID), + parameters.Input.Key, + ) } return nil } -func (ctrl *DefaultController) DeleteAccountMetadata(ctx context.Context, parameters Parameters, targetID string, key string) error { +func (ctrl *DefaultController) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) error { if err := tracing.SkipResult(tracing.Trace(ctx, "DeleteAccountMetadata", func(ctx context.Context) (*ledger.Log, error) { - return ctrl.forgeLog(ctx, parameters, func(sqlTX TX) (*ledger.Log, error) { - err := sqlTX.DeleteAccountMetadata(ctx, targetID, key) + return forgeLog(ctx, ctrl.store, parameters, func(ctx context.Context, sqlTX TX, input DeleteAccountMetadata) (*ledger.Log, error) { + err := sqlTX.DeleteAccountMetadata(ctx, input.Address, input.Key) if err != nil { return nil, err } - return pointer.For(ledger.NewDeleteAccountMetadataLog(targetID, key)), nil + return pointer.For(ledger.NewDeleteAccountMetadataLog(input.Address, input.Key)), nil }) })); err != nil { return err } if ctrl.listener != nil { - ctrl.listener.DeletedMetadata(ctx, ctrl.ledger.Name, ledger.MetaTargetTypeAccount, targetID, key) + ctrl.listener.DeletedMetadata( + ctx, + ctrl.ledger.Name, + ledger.MetaTargetTypeAccount, + parameters.Input.Address, + parameters.Input.Key, + ) } return nil diff --git a/internal/controller/ledger/controller_default_test.go b/internal/controller/ledger/controller_default_test.go index 50b042178..de8fb0ceb 100644 --- a/internal/controller/ledger/controller_default_test.go +++ b/internal/controller/ledger/controller_default_test.go @@ -29,7 +29,7 @@ func TestCreateTransaction(t *testing.T) { l := NewDefaultController(ledger.Ledger{}, store, listener, machineFactory) - runScript := ledger.RunScript{} + runScript := RunScript{} machineFactory.EXPECT(). Make(runScript.Plain). @@ -64,7 +64,9 @@ func TestCreateTransaction(t *testing.T) { listener.EXPECT(). CommittedTransactions(gomock.Any(), "", gomock.Any(), ledger.AccountMetadata{}) - _, err := l.CreateTransaction(context.Background(), Parameters{}, runScript) + _, err := l.CreateTransaction(context.Background(), Parameters[RunScript]{ + Input: runScript, + }) require.NoError(t, err) } @@ -112,7 +114,11 @@ func TestRevertTransaction(t *testing.T) { })). Return(nil) - _, err := l.RevertTransaction(ctx, Parameters{}, 1, false, false) + _, err := l.RevertTransaction(ctx, Parameters[RevertTransaction]{ + Input: RevertTransaction{ + TransactionID: 1, + }, + }) require.NoError(t, err) } @@ -150,7 +156,12 @@ func TestSaveTransactionMetadata(t *testing.T) { listener.EXPECT(). SavedMetadata(gomock.Any(), "", ledger.MetaTargetTypeTransaction, "1", m) - err := l.SaveTransactionMetadata(ctx, Parameters{}, 1, m) + err := l.SaveTransactionMetadata(ctx, Parameters[SaveTransactionMetadata]{ + Input: SaveTransactionMetadata{ + Metadata: m, + TransactionID: 1, + }, + }) require.NoError(t, err) } @@ -185,7 +196,12 @@ func TestDeleteTransactionMetadata(t *testing.T) { listener.EXPECT(). DeletedMetadata(gomock.Any(), "", ledger.MetaTargetTypeTransaction, "1", "foo") - err := l.DeleteTransactionMetadata(ctx, Parameters{}, 1, "foo") + err := l.DeleteTransactionMetadata(ctx, Parameters[DeleteTransactionMetadata]{ + Input: DeleteTransactionMetadata{ + TransactionID: 1, + Key: "foo", + }, + }) require.NoError(t, err) } diff --git a/internal/controller/ledger/controller_generated.go b/internal/controller/ledger/controller_generated.go index 633a5c7c1..a17ca83d0 100644 --- a/internal/controller/ledger/controller_generated.go +++ b/internal/controller/ledger/controller_generated.go @@ -14,7 +14,6 @@ import ( reflect "reflect" bunpaginate "github.com/formancehq/go-libs/bun/bunpaginate" - metadata "github.com/formancehq/go-libs/metadata" migrations "github.com/formancehq/go-libs/migrations" ledger "github.com/formancehq/ledger/internal" gomock "go.uber.org/mock/gomock" @@ -74,46 +73,46 @@ func (mr *MockControllerMockRecorder) CountTransactions(ctx, query any) *gomock. } // CreateTransaction mocks base method. -func (m *MockController) CreateTransaction(ctx context.Context, parameters Parameters, data ledger.RunScript) (*ledger.Transaction, error) { +func (m *MockController) CreateTransaction(ctx context.Context, parameters Parameters[RunScript]) (*ledger.Transaction, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateTransaction", ctx, parameters, data) + ret := m.ctrl.Call(m, "CreateTransaction", ctx, parameters) ret0, _ := ret[0].(*ledger.Transaction) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateTransaction indicates an expected call of CreateTransaction. -func (mr *MockControllerMockRecorder) CreateTransaction(ctx, parameters, data any) *gomock.Call { +func (mr *MockControllerMockRecorder) CreateTransaction(ctx, parameters any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTransaction", reflect.TypeOf((*MockController)(nil).CreateTransaction), ctx, parameters, data) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTransaction", reflect.TypeOf((*MockController)(nil).CreateTransaction), ctx, parameters) } // DeleteAccountMetadata mocks base method. -func (m *MockController) DeleteAccountMetadata(ctx context.Context, parameters Parameters, targetID, key string) error { +func (m *MockController) DeleteAccountMetadata(ctx context.Context, parameters Parameters[DeleteAccountMetadata]) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteAccountMetadata", ctx, parameters, targetID, key) + ret := m.ctrl.Call(m, "DeleteAccountMetadata", ctx, parameters) ret0, _ := ret[0].(error) return ret0 } // DeleteAccountMetadata indicates an expected call of DeleteAccountMetadata. -func (mr *MockControllerMockRecorder) DeleteAccountMetadata(ctx, parameters, targetID, key any) *gomock.Call { +func (mr *MockControllerMockRecorder) DeleteAccountMetadata(ctx, parameters any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccountMetadata", reflect.TypeOf((*MockController)(nil).DeleteAccountMetadata), ctx, parameters, targetID, key) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccountMetadata", reflect.TypeOf((*MockController)(nil).DeleteAccountMetadata), ctx, parameters) } // DeleteTransactionMetadata mocks base method. -func (m *MockController) DeleteTransactionMetadata(ctx context.Context, parameters Parameters, id int, key string) error { +func (m *MockController) DeleteTransactionMetadata(ctx context.Context, parameters Parameters[DeleteTransactionMetadata]) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteTransactionMetadata", ctx, parameters, id, key) + ret := m.ctrl.Call(m, "DeleteTransactionMetadata", ctx, parameters) ret0, _ := ret[0].(error) return ret0 } // DeleteTransactionMetadata indicates an expected call of DeleteTransactionMetadata. -func (mr *MockControllerMockRecorder) DeleteTransactionMetadata(ctx, parameters, id, key any) *gomock.Call { +func (mr *MockControllerMockRecorder) DeleteTransactionMetadata(ctx, parameters any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTransactionMetadata", reflect.TypeOf((*MockController)(nil).DeleteTransactionMetadata), ctx, parameters, id, key) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTransactionMetadata", reflect.TypeOf((*MockController)(nil).DeleteTransactionMetadata), ctx, parameters) } // Export mocks base method. @@ -295,44 +294,44 @@ func (mr *MockControllerMockRecorder) ListTransactions(ctx, query any) *gomock.C } // RevertTransaction mocks base method. -func (m *MockController) RevertTransaction(ctx context.Context, parameters Parameters, id int, force, atEffectiveDate bool) (*ledger.Transaction, error) { +func (m *MockController) RevertTransaction(ctx context.Context, parameters Parameters[RevertTransaction]) (*ledger.Transaction, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RevertTransaction", ctx, parameters, id, force, atEffectiveDate) + ret := m.ctrl.Call(m, "RevertTransaction", ctx, parameters) ret0, _ := ret[0].(*ledger.Transaction) ret1, _ := ret[1].(error) return ret0, ret1 } // RevertTransaction indicates an expected call of RevertTransaction. -func (mr *MockControllerMockRecorder) RevertTransaction(ctx, parameters, id, force, atEffectiveDate any) *gomock.Call { +func (mr *MockControllerMockRecorder) RevertTransaction(ctx, parameters any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevertTransaction", reflect.TypeOf((*MockController)(nil).RevertTransaction), ctx, parameters, id, force, atEffectiveDate) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevertTransaction", reflect.TypeOf((*MockController)(nil).RevertTransaction), ctx, parameters) } // SaveAccountMetadata mocks base method. -func (m_2 *MockController) SaveAccountMetadata(ctx context.Context, parameters Parameters, id string, m metadata.Metadata) error { - m_2.ctrl.T.Helper() - ret := m_2.ctrl.Call(m_2, "SaveAccountMetadata", ctx, parameters, id, m) +func (m *MockController) SaveAccountMetadata(ctx context.Context, parameters Parameters[SaveAccountMetadata]) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveAccountMetadata", ctx, parameters) ret0, _ := ret[0].(error) return ret0 } // SaveAccountMetadata indicates an expected call of SaveAccountMetadata. -func (mr *MockControllerMockRecorder) SaveAccountMetadata(ctx, parameters, id, m any) *gomock.Call { +func (mr *MockControllerMockRecorder) SaveAccountMetadata(ctx, parameters any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAccountMetadata", reflect.TypeOf((*MockController)(nil).SaveAccountMetadata), ctx, parameters, id, m) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAccountMetadata", reflect.TypeOf((*MockController)(nil).SaveAccountMetadata), ctx, parameters) } // SaveTransactionMetadata mocks base method. -func (m_2 *MockController) SaveTransactionMetadata(ctx context.Context, parameters Parameters, id int, m metadata.Metadata) error { - m_2.ctrl.T.Helper() - ret := m_2.ctrl.Call(m_2, "SaveTransactionMetadata", ctx, parameters, id, m) +func (m *MockController) SaveTransactionMetadata(ctx context.Context, parameters Parameters[SaveTransactionMetadata]) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveTransactionMetadata", ctx, parameters) ret0, _ := ret[0].(error) return ret0 } // SaveTransactionMetadata indicates an expected call of SaveTransactionMetadata. -func (mr *MockControllerMockRecorder) SaveTransactionMetadata(ctx, parameters, id, m any) *gomock.Call { +func (mr *MockControllerMockRecorder) SaveTransactionMetadata(ctx, parameters any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTransactionMetadata", reflect.TypeOf((*MockController)(nil).SaveTransactionMetadata), ctx, parameters, id, m) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTransactionMetadata", reflect.TypeOf((*MockController)(nil).SaveTransactionMetadata), ctx, parameters) } diff --git a/internal/controller/ledger/errors.go b/internal/controller/ledger/errors.go index ee85a2781..13ad00d62 100644 --- a/internal/controller/ledger/errors.go +++ b/internal/controller/ledger/errors.go @@ -145,27 +145,28 @@ func NewErrIdempotencyKeyConflict(ik string) ErrIdempotencyKeyConflict { } } -type ErrReferenceConflict struct { +type ErrTransactionReferenceConflict struct { reference string } -func (e ErrReferenceConflict) Error() string { +func (e ErrTransactionReferenceConflict) Error() string { return fmt.Sprintf("duplicate reference %q", e.reference) } -func (e ErrReferenceConflict) Is(err error) bool { - _, ok := err.(ErrReferenceConflict) +func (e ErrTransactionReferenceConflict) Is(err error) bool { + _, ok := err.(ErrTransactionReferenceConflict) return ok } -func NewErrReferenceConflict(reference string) ErrReferenceConflict { - return ErrReferenceConflict{ +func NewErrTransactionReferenceConflict(reference string) ErrTransactionReferenceConflict { + return ErrTransactionReferenceConflict{ reference: reference, } } type ErrInvalidVars = machine.ErrInvalidVars +// ErrCompilationFailed is used for any errors returned by the numscript interpreter type ErrCompilationFailed struct { err error } @@ -185,6 +186,7 @@ func newErrCompilationFailed(err error) ErrCompilationFailed { } } +// ErrMetadataOverride is used when a metadata is defined at numscript level AND at the input level type ErrMetadataOverride struct { key string } @@ -203,3 +205,33 @@ func newErrMetadataOverride(key string) *ErrMetadataOverride { key: key, } } + +// ErrInvalidIdempotencyInput is used when a IK is used with an inputs different from the original one. +// For example, try to use the same IK with a different numscript script will result with that error. +type ErrInvalidIdempotencyInput struct { + idempotencyKey string + expectedIdempotencyHash string + computedIdempotencyHash string +} + +func (e ErrInvalidIdempotencyInput) Error() string { + return fmt.Sprintf( + "invalid idempotency hash when using idempotency key '%s', has computed '%s' but '%s' is stored", + e.idempotencyKey, + e.computedIdempotencyHash, + e.expectedIdempotencyHash, + ) +} + +func (e ErrInvalidIdempotencyInput) Is(err error) bool { + _, ok := err.(ErrInvalidIdempotencyInput) + return ok +} + +func newErrInvalidIdempotencyInputs(idempotencyKey, expectedIdempotencyHash, gotIdempotencyHash string) ErrInvalidIdempotencyInput { + return ErrInvalidIdempotencyInput{ + idempotencyKey: idempotencyKey, + expectedIdempotencyHash: expectedIdempotencyHash, + computedIdempotencyHash: gotIdempotencyHash, + } +} diff --git a/internal/controller/ledger/inputs.go b/internal/controller/ledger/inputs.go new file mode 100644 index 000000000..7f2d8de03 --- /dev/null +++ b/internal/controller/ledger/inputs.go @@ -0,0 +1,37 @@ +package ledger + +import ( + "github.com/formancehq/ledger/internal/machine/vm" + + "github.com/formancehq/go-libs/metadata" +) + +type RunScript = vm.RunScript +type Script = vm.Script +type ScriptV1 = vm.ScriptV1 + +type RevertTransaction struct { + Force bool + AtEffectiveDate bool + TransactionID int +} + +type SaveTransactionMetadata struct { + TransactionID int + Metadata metadata.Metadata +} + +type SaveAccountMetadata struct { + Address string + Metadata metadata.Metadata +} + +type DeleteTransactionMetadata struct { + TransactionID int + Key string +} + +type DeleteAccountMetadata struct { + Address string + Key string +} diff --git a/internal/controller/ledger/log_process.go b/internal/controller/ledger/log_process.go new file mode 100644 index 000000000..c9f06e39f --- /dev/null +++ b/internal/controller/ledger/log_process.go @@ -0,0 +1,81 @@ +package ledger + +import ( + "context" + "github.com/formancehq/go-libs/logging" + "github.com/formancehq/go-libs/platform/postgres" + ledger "github.com/formancehq/ledger/internal" + "github.com/formancehq/ledger/internal/tracing" + "github.com/pkg/errors" +) + +func runTx[INPUT any](ctx context.Context, store Store, parameters Parameters[INPUT], fn func(ctx context.Context, sqlTX TX, input INPUT) (*ledger.Log, error)) (*ledger.Log, error) { + var log *ledger.Log + err := store.WithTX(ctx, nil, func(tx TX) (commit bool, err error) { + log, err = fn(ctx, tx, parameters.Input) + if err != nil { + return false, err + } + log.IdempotencyKey = parameters.IdempotencyKey + log.IdempotencyHash = ledger.ComputeIdempotencyHash(parameters.Input) + + _, err = tracing.TraceWithLatency(ctx, "InsertLog", func(ctx context.Context) (*struct{}, error) { + return nil, tx.InsertLog(ctx, log) + }) + if err != nil { + return false, errors.Wrap(err, "failed to insert log") + } + logging.FromContext(ctx).Debugf("log inserted with id %d", log.ID) + + if parameters.DryRun { + return false, nil + } + + return true, nil + }) + return log, err +} + +// todo: handle too many clients error +func forgeLog[INPUT any](ctx context.Context, store Store, parameters Parameters[INPUT], fn func(ctx context.Context, sqlTX TX, input INPUT) (*ledger.Log, error)) (*ledger.Log, error) { + if parameters.IdempotencyKey != "" { + log, err := store.ReadLogWithIdempotencyKey(ctx, parameters.IdempotencyKey) + if err != nil && !errors.Is(err, postgres.ErrNotFound) { + return nil, err + } + if err == nil { + if computedHash := ledger.ComputeIdempotencyHash(parameters.Input); log.IdempotencyHash != computedHash { + return nil, newErrInvalidIdempotencyInputs(log.IdempotencyKey, log.IdempotencyHash, computedHash) + } + + return log, nil + } + } + + for { + log, err := runTx(ctx, store, parameters, fn) + if err != nil { + switch { + case errors.Is(err, postgres.ErrDeadlockDetected): + logging.FromContext(ctx).Info("deadlock detected, retrying...") + continue + // A log with the IK could have been inserted in the meantime, read again the database to retrieve it + case errors.Is(err, ErrIdempotencyKeyConflict{}): + log, err := store.ReadLogWithIdempotencyKey(ctx, parameters.IdempotencyKey) + if err != nil && !errors.Is(err, postgres.ErrNotFound) { + return nil, err + } + if errors.Is(err, postgres.ErrNotFound) { + logging.FromContext(ctx).Errorf("incoherent error, received duplicate IK but log not found in database") + return nil, err + } + + return log, nil + default: + return nil, errors.Wrap(err, "unexpected error while forging log") + } + } + + return log, nil + } +} diff --git a/internal/controller/ledger/machine.go b/internal/controller/ledger/machine.go index fbb9c6024..af1647340 100644 --- a/internal/controller/ledger/machine.go +++ b/internal/controller/ledger/machine.go @@ -32,7 +32,14 @@ type DefaultMachineAdapter struct { func (d *DefaultMachineAdapter) Execute(ctx context.Context, store vm.Store, vars map[string]string) (*MachineResult, error) { d.machine = vm.NewMachine(d.program) - if err := d.machine.SetVarsFromJSON(vars); err != nil { + + // notes(gfyrag): machines modify the map, copy it to keep our original parameters unchanged + varsCopy := make(map[string]string) + for k, v := range vars { + varsCopy[k] = v + } + + if err := d.machine.SetVarsFromJSON(varsCopy); err != nil { return nil, errors.Wrap(err, "failed to set vars from JSON") } err := d.machine.ResolveResources(ctx, store) diff --git a/internal/controller/ledger/parameters.go b/internal/controller/ledger/parameters.go index 01b779b68..1a97236f0 100644 --- a/internal/controller/ledger/parameters.go +++ b/internal/controller/ledger/parameters.go @@ -1,6 +1,7 @@ package ledger -type Parameters struct { +type Parameters[INPUT any] struct { DryRun bool IdempotencyKey string + Input INPUT } diff --git a/internal/controller/ledger/store_generated.go b/internal/controller/ledger/store_generated.go index 6a054aa47..f69d92883 100644 --- a/internal/controller/ledger/store_generated.go +++ b/internal/controller/ledger/store_generated.go @@ -178,20 +178,6 @@ func (mr *MockTXMockRecorder) RevertTransaction(ctx, id any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevertTransaction", reflect.TypeOf((*MockTX)(nil).RevertTransaction), ctx, id) } -// SwitchLedgerState mocks base method. -func (m *MockTX) SwitchLedgerState(ctx context.Context, name, state string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SwitchLedgerState", ctx, name, state) - ret0, _ := ret[0].(error) - return ret0 -} - -// SwitchLedgerState indicates an expected call of SwitchLedgerState. -func (mr *MockTXMockRecorder) SwitchLedgerState(ctx, name, state any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwitchLedgerState", reflect.TypeOf((*MockTX)(nil).SwitchLedgerState), ctx, name, state) -} - // UpdateAccountsMetadata mocks base method. func (m_2 *MockTX) UpdateAccountsMetadata(ctx context.Context, m map[string]metadata.Metadata) error { m_2.ctrl.T.Helper() diff --git a/internal/errors.go b/internal/errors.go index aa6d9e777..f51e711c4 100644 --- a/internal/errors.go +++ b/internal/errors.go @@ -36,4 +36,4 @@ func (e ErrInvalidBucketName) Is(err error) bool { func newErrInvalidBucketName(bucket string, err error) ErrInvalidBucketName { return ErrInvalidBucketName{err: err, bucket: bucket} -} +} \ No newline at end of file diff --git a/internal/log.go b/internal/log.go index 3f29f9219..7a92ca035 100644 --- a/internal/log.go +++ b/internal/log.go @@ -3,6 +3,7 @@ package ledger import ( "crypto/sha256" "database/sql/driver" + "encoding/base64" "encoding/json" "fmt" "github.com/uptrace/bun" @@ -356,3 +357,14 @@ func HydrateLog(_type LogType, data []byte) (any, error) { return reflect.ValueOf(payload).Elem().Interface(), nil } + +func ComputeIdempotencyHash(inputs any) string { + digest := sha256.New() + enc := json.NewEncoder(digest) + + if err := enc.Encode(inputs); err != nil { + panic(err) + } + + return base64.URLEncoding.EncodeToString(digest.Sum(nil)) +} \ No newline at end of file diff --git a/internal/machine/vm/run.go b/internal/machine/vm/run.go index ae2cf0da4..b64f39cad 100644 --- a/internal/machine/vm/run.go +++ b/internal/machine/vm/run.go @@ -1,6 +1,8 @@ package vm import ( + "fmt" + "github.com/formancehq/go-libs/time" "math/big" "github.com/formancehq/ledger/internal/machine" @@ -10,13 +12,45 @@ import ( "github.com/pkg/errors" ) +type RunScript struct { + Script + Timestamp time.Time `json:"timestamp"` + Metadata metadata.Metadata `json:"metadata"` + Reference string `json:"reference"` +} + +type Script struct { + Plain string `json:"plain"` + Vars map[string]string `json:"vars" swaggertype:"object"` +} + +type ScriptV1 struct { + Script + Vars map[string]any `json:"vars"` +} + +func (s ScriptV1) ToCore() Script { + s.Script.Vars = map[string]string{} + for k, v := range s.Vars { + switch v := v.(type) { + case string: + s.Script.Vars[k] = v + case map[string]any: + s.Script.Vars[k] = fmt.Sprintf("%s %v", v["asset"], v["amount"]) + default: + s.Script.Vars[k] = fmt.Sprint(v) + } + } + return s.Script +} + type Result struct { Postings ledger.Postings Metadata metadata.Metadata AccountMetadata map[string]metadata.Metadata } -func Run(m *Machine, script ledger.RunScript) (*Result, error) { +func Run(m *Machine, script RunScript) (*Result, error) { err := m.Execute() if err != nil { return nil, errors.Wrap(err, "script execution failed") diff --git a/internal/machine/vm/run_test.go b/internal/machine/vm/run_test.go index 4fd5ca747..a42e0669b 100644 --- a/internal/machine/vm/run_test.go +++ b/internal/machine/vm/run_test.go @@ -434,8 +434,8 @@ func TestRun(t *testing.T) { require.NoError(t, err) require.NoError(t, m.ResolveBalances(context.Background(), tc.store)) - result, err := Run(m, ledger.RunScript{ - Script: ledger.Script{ + result, err := Run(m, RunScript{ + Script: Script{ Plain: tc.script, Vars: tc.vars, }, diff --git a/internal/script.go b/internal/script.go deleted file mode 100644 index 40322a394..000000000 --- a/internal/script.go +++ /dev/null @@ -1,41 +0,0 @@ -package ledger - -import ( - "fmt" - - "github.com/formancehq/go-libs/time" - - "github.com/formancehq/go-libs/metadata" -) - -type RunScript struct { - Script - Timestamp time.Time `json:"timestamp"` - Metadata metadata.Metadata `json:"metadata"` - Reference string `json:"reference"` -} - -type Script struct { - Plain string `json:"plain"` - Vars map[string]string `json:"vars" swaggertype:"object"` -} - -type ScriptV1 struct { - Script - Vars map[string]any `json:"vars"` -} - -func (s ScriptV1) ToCore() Script { - s.Script.Vars = map[string]string{} - for k, v := range s.Vars { - switch v := v.(type) { - case string: - s.Script.Vars[k] = v - case map[string]any: - s.Script.Vars[k] = fmt.Sprintf("%s %v", v["asset"], v["amount"]) - default: - s.Script.Vars[k] = fmt.Sprint(v) - } - } - return s.Script -} diff --git a/internal/storage/ledger/transactions.go b/internal/storage/ledger/transactions.go index 2e3a83289..b2f3fab3a 100644 --- a/internal/storage/ledger/transactions.go +++ b/internal/storage/ledger/transactions.go @@ -384,7 +384,7 @@ func (s *Store) InsertTransaction(ctx context.Context, tx *ledger.Transaction) e switch { case errors.Is(err, postgres.ErrConstraintsFailed{}): if err.(postgres.ErrConstraintsFailed).GetConstraint() == "transactions_reference" { - return nil, ledgercontroller.NewErrReferenceConflict(tx.Reference) + return nil, ledgercontroller.NewErrTransactionReferenceConflict(tx.Reference) } default: return nil, err diff --git a/internal/storage/ledger/transactions_test.go b/internal/storage/ledger/transactions_test.go index a402a3470..67b3a4844 100644 --- a/internal/storage/ledger/transactions_test.go +++ b/internal/storage/ledger/transactions_test.go @@ -515,7 +515,7 @@ func TestTransactionsInsert(t *testing.T) { } err = store.InsertTransaction(ctx, &tx2) require.Error(t, err) - require.True(t, errors.Is(err, ledgercontroller.ErrReferenceConflict{})) + require.True(t, errors.Is(err, ledgercontroller.ErrTransactionReferenceConflict{})) }) t.Run("create a tx with no timestamp", func(t *testing.T) { t.Parallel() diff --git a/internal/transaction.go b/internal/transaction.go index 96d850575..4aa4d65cc 100644 --- a/internal/transaction.go +++ b/internal/transaction.go @@ -9,8 +9,6 @@ import ( "slices" "sort" - "github.com/formancehq/go-libs/pointer" - "github.com/formancehq/go-libs/metadata" ) @@ -221,32 +219,3 @@ func NewTransaction() Transaction { TransactionData: NewTransactionData(), } } - -type TransactionRequest struct { - Postings Postings `json:"postings"` - Script ScriptV1 `json:"script"` - Timestamp time.Time `json:"timestamp"` - Reference string `json:"reference"` - Metadata metadata.Metadata `json:"metadata" swaggertype:"object"` -} - -func (req *TransactionRequest) ToRunScript(allowUnboundedOverdrafts bool) *RunScript { - - if len(req.Postings) > 0 { - txData := TransactionData{ - Postings: req.Postings, - Timestamp: req.Timestamp, - Reference: req.Reference, - Metadata: req.Metadata, - } - - return pointer.For(TxToScriptData(txData, allowUnboundedOverdrafts)) - } - - return &RunScript{ - Script: req.Script.ToCore(), - Timestamp: req.Timestamp, - Reference: req.Reference, - Metadata: req.Metadata, - } -} diff --git a/test/e2e/api_transactions_create.go b/test/e2e/api_transactions_create.go index 9d18a9229..ec11fbc5a 100644 --- a/test/e2e/api_transactions_create.go +++ b/test/e2e/api_transactions_create.go @@ -293,29 +293,29 @@ var _ = Context("Ledger accounts list API tests", func() { var ( err error response *components.V2Transaction + req operations.V2CreateTransactionRequest ) createTransaction := func() { - response, err = CreateTransaction( - ctx, - testServer.GetValue(), - operations.V2CreateTransactionRequest{ - IdempotencyKey: pointer.For("testing"), - V2PostTransaction: components.V2PostTransaction{ - Metadata: map[string]string{}, - Postings: []components.V2Posting{ - { - Amount: big.NewInt(100), - Asset: "USD", - Source: "world", - Destination: "alice", - }, + response, err = CreateTransaction(ctx, testServer.GetValue(), req) + } + BeforeEach(func() { + req = operations.V2CreateTransactionRequest{ + IdempotencyKey: pointer.For("testing"), + V2PostTransaction: components.V2PostTransaction{ + Metadata: map[string]string{}, + Postings: []components.V2Posting{ + { + Amount: big.NewInt(100), + Asset: "USD", + Source: "world", + Destination: "alice", }, }, - Ledger: "default", }, - ) - } - BeforeEach(createTransaction) + Ledger: "default", + } + }) + JustBeforeEach(createTransaction) It("should be ok", func() { Expect(err).To(Succeed()) Expect(response.ID).To(Equal(big.NewInt(1))) @@ -327,6 +327,18 @@ var _ = Context("Ledger accounts list API tests", func() { Expect(response.ID).To(Equal(big.NewInt(1))) }) }) + When("creating another tx with the same IK but different input", func() { + JustBeforeEach(func() { + req.V2PostTransaction.Metadata = metadata.Metadata{ + "foo": "bar", + } + createTransaction() + }) + It("should fail", func() { + Expect(err).NotTo(Succeed()) + Expect(err).To(HaveErrorCode(string(components.V2ErrorsEnumValidation))) + }) + }) }) // TODO(gfyrag): test negative amount with a variable When("creating a transaction on a ledger with a negative amount in the script", func() { diff --git a/test/performance/env_core_test.go b/test/performance/env_core_test.go index e32aaf347..e09e9a940 100644 --- a/test/performance/env_core_test.go +++ b/test/performance/env_core_test.go @@ -32,10 +32,12 @@ func (e *CoreEnv) Stop(_ context.Context) error { func (e *CoreEnv) Executor() TransactionExecutor { return TransactionExecutorFn(func(ctx context.Context, plain string, vars map[string]string) (*ledger.Transaction, error) { - ret, err := e.writer.CreateTransaction(ctx, ledgercontroller.Parameters{}, ledger.RunScript{ - Script: ledger.Script{ - Plain: plain, - Vars: vars, + ret, err := e.writer.CreateTransaction(ctx, ledgercontroller.Parameters[ledgercontroller.RunScript]{ + Input: ledgercontroller.RunScript{ + Script: ledgercontroller.Script{ + Plain: plain, + Vars: vars, + }, }, }) if err != nil {