From 1620af9e3f0711f5455d4deb689af1c3b7aa83d5 Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 10 Dec 2024 00:50:07 +0300 Subject: [PATCH] Feature: add fees to transactions --- cmd/api/docs/docs.go | 27 ++++++++++++++ cmd/api/docs/swagger.json | 27 ++++++++++++++ cmd/api/docs/swagger.yaml | 19 ++++++++++ cmd/api/handler/responses/fee.go | 12 ++++++ cmd/api/handler/responses/tx.go | 1 + cmd/api/handler/tx.go | 17 ++++++++- cmd/api/handler/tx_test.go | 54 +++++++++++++++++++++++++++ internal/storage/fee.go | 1 + internal/storage/mock/fee.go | 39 +++++++++++++++++++ internal/storage/postgres/fee.go | 10 +++++ internal/storage/postgres/fee_test.go | 13 +++++++ 11 files changed, 219 insertions(+), 1 deletion(-) diff --git a/cmd/api/docs/docs.go b/cmd/api/docs/docs.go index 45c7d98..bb591a6 100644 --- a/cmd/api/docs/docs.go +++ b/cmd/api/docs/docs.go @@ -2328,6 +2328,12 @@ const docTemplate = `{ "name": "hash", "in": "path", "required": true + }, + { + "type": "boolean", + "description": "Flag which indicates need join full transaction fees", + "name": "fee", + "in": "query" } ], "responses": { @@ -3772,6 +3778,12 @@ const docTemplate = `{ "format": "string", "example": "some error text" }, + "fees": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.TxFee" + } + }, "hash": { "type": "string", "format": "binary", @@ -3819,6 +3831,21 @@ const docTemplate = `{ } } }, + "responses.TxFee": { + "type": "object", + "properties": { + "amount": { + "type": "string", + "format": "string", + "example": "1000" + }, + "asset": { + "type": "string", + "format": "string", + "example": "nria" + } + } + }, "responses.Validator": { "type": "object", "properties": { diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index 11a7e2a..e3c9037 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/docs/swagger.json @@ -2318,6 +2318,12 @@ "name": "hash", "in": "path", "required": true + }, + { + "type": "boolean", + "description": "Flag which indicates need join full transaction fees", + "name": "fee", + "in": "query" } ], "responses": { @@ -3762,6 +3768,12 @@ "format": "string", "example": "some error text" }, + "fees": { + "type": "array", + "items": { + "$ref": "#/definitions/responses.TxFee" + } + }, "hash": { "type": "string", "format": "binary", @@ -3809,6 +3821,21 @@ } } }, + "responses.TxFee": { + "type": "object", + "properties": { + "amount": { + "type": "string", + "format": "string", + "example": "1000" + }, + "asset": { + "type": "string", + "format": "string", + "example": "nria" + } + } + }, "responses.Validator": { "type": "object", "properties": { diff --git a/cmd/api/docs/swagger.yaml b/cmd/api/docs/swagger.yaml index 7dc19ee..58c6379 100644 --- a/cmd/api/docs/swagger.yaml +++ b/cmd/api/docs/swagger.yaml @@ -712,6 +712,10 @@ definitions: example: some error text format: string type: string + fees: + items: + $ref: '#/definitions/responses.TxFee' + type: array hash: example: 652452A670018D629CC116E510BA88C1CABE061336661B1F3D206D248BD558AF format: binary @@ -749,6 +753,17 @@ definitions: format: date-time type: string type: object + responses.TxFee: + properties: + amount: + example: "1000" + format: string + type: string + asset: + example: nria + format: string + type: string + type: object responses.Validator: properties: address: @@ -2354,6 +2369,10 @@ paths: name: hash required: true type: string + - description: Flag which indicates need join full transaction fees + in: query + name: fee + type: boolean produces: - application/json responses: diff --git a/cmd/api/handler/responses/fee.go b/cmd/api/handler/responses/fee.go index 18d5718..ba9a343 100644 --- a/cmd/api/handler/responses/fee.go +++ b/cmd/api/handler/responses/fee.go @@ -38,3 +38,15 @@ func NewFullFee(fee storage.Fee) FullFee { return ff } + +type TxFee struct { + Amount string `example:"1000" format:"string" json:"amount" swaggertype:"string"` + Asset string `example:"nria" format:"string" json:"asset" swaggertype:"string"` +} + +func NewTxFee(fee storage.Fee) TxFee { + return TxFee{ + Asset: fee.Asset, + Amount: fee.Amount.String(), + } +} diff --git a/cmd/api/handler/responses/tx.go b/cmd/api/handler/responses/tx.go index 4c2308a..d4f6db1 100644 --- a/cmd/api/handler/responses/tx.go +++ b/cmd/api/handler/responses/tx.go @@ -29,6 +29,7 @@ type Tx struct { ActionTypes []string `example:"rollup_data_submission,transfer" format:"string" json:"action_types" swaggertype:"string"` Actions []Action `json:"actions,omitempty"` + Fees []TxFee `json:"fees,omitempty"` } func NewTx(tx storage.Tx) Tx { diff --git a/cmd/api/handler/tx.go b/cmd/api/handler/tx.go index 05b94a6..e3261ae 100644 --- a/cmd/api/handler/tx.go +++ b/cmd/api/handler/tx.go @@ -43,6 +43,8 @@ func NewTxHandler( type getTxRequest struct { Hash string `param:"hash" validate:"required,hexadecimal,len=64"` + + Fee bool `query:"fee" validate:"omitempty"` } // Get godoc @@ -52,6 +54,7 @@ type getTxRequest struct { // @Tags transactions // @ID get-transaction // @Param hash path string true "Transaction hash in hexadecimal" minlength(64) maxlength(64) +// @Param fee query boolean false "Flag which indicates need join full transaction fees" // @Produce json // @Success 200 {object} responses.Tx // @Success 204 @@ -73,8 +76,20 @@ func (handler *TxHandler) Get(c echo.Context) error { if err != nil { return handleError(c, err, handler.tx) } + response := responses.NewTx(tx) + + if req.Fee { + fees, err := handler.fees.FullTxFee(c.Request().Context(), tx.Id) + if err != nil { + return handleError(c, err, handler.tx) + } + response.Fees = make([]responses.TxFee, len(fees)) + for i := range fees { + response.Fees[i] = responses.NewTxFee(fees[i]) + } + } - return c.JSON(http.StatusOK, responses.NewTx(tx)) + return c.JSON(http.StatusOK, response) } type txListRequest struct { diff --git a/cmd/api/handler/tx_test.go b/cmd/api/handler/tx_test.go index eb96e84..131e303 100644 --- a/cmd/api/handler/tx_test.go +++ b/cmd/api/handler/tx_test.go @@ -89,6 +89,60 @@ func (s *TxTestSuite) TestGet() { s.Require().Equal(types.StatusSuccess, tx.Status) } +func (s *TxTestSuite) TestGetWithFee() { + q := make(url.Values) + q.Add("fee", "true") + + req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) + rec := httptest.NewRecorder() + c := s.echo.NewContext(req, rec) + c.SetPath("/tx/:hash") + c.SetParamNames("hash") + c.SetParamValues(testTxHash) + + s.tx.EXPECT(). + ByHash(gomock.Any(), testTx.Hash). + Return(testTx, nil). + Times(1) + + s.fees.EXPECT(). + FullTxFee(gomock.Any(), testTx.Id). + Return([]storage.Fee{ + { + Amount: decimal.NewFromInt(100), + Asset: "asset_1", + }, { + Amount: decimal.NewFromInt(200), + Asset: "asset_2", + }, + }, nil). + Times(1) + + s.Require().NoError(s.handler.Get(c)) + s.Require().Equal(http.StatusOK, rec.Code) + + var tx responses.Tx + err := json.NewDecoder(rec.Body).Decode(&tx) + s.Require().NoError(err) + + s.Require().EqualValues(1, tx.Id) + s.Require().EqualValues(100, tx.Height) + s.Require().Equal(testTime, tx.Time) + s.Require().Equal(testTxHash, tx.Hash) + s.Require().EqualValues(1, tx.Position) + s.Require().EqualValues(1, tx.ActionsCount) + s.Require().EqualValues(10, tx.Nonce) + s.Require().EqualValues(testAddress.Hash, tx.Signer) + s.Require().Equal("codespace", tx.Codespace) + s.Require().Equal(types.StatusSuccess, tx.Status) + + s.Require().Len(tx.Fees, 2) + s.Require().Equal("100", tx.Fees[0].Amount) + s.Require().Equal("asset_1", tx.Fees[0].Asset) + s.Require().Equal("200", tx.Fees[1].Amount) + s.Require().Equal("asset_2", tx.Fees[1].Asset) +} + func (s *TxTestSuite) TestGetInvalidTx() { req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() diff --git a/internal/storage/fee.go b/internal/storage/fee.go index 2bd82cc..ff4c498 100644 --- a/internal/storage/fee.go +++ b/internal/storage/fee.go @@ -19,6 +19,7 @@ type IFee interface { ByTxId(ctx context.Context, id uint64, limit, offset int) ([]Fee, error) ByPayerId(ctx context.Context, id uint64, limit, offset int, sort storage.SortOrder) ([]Fee, error) + FullTxFee(ctx context.Context, id uint64) ([]Fee, error) } type Fee struct { diff --git a/internal/storage/mock/fee.go b/internal/storage/mock/fee.go index c0db488..3e58c83 100644 --- a/internal/storage/mock/fee.go +++ b/internal/storage/mock/fee.go @@ -161,6 +161,45 @@ func (c *MockIFeeCursorListCall) DoAndReturn(f func(context.Context, uint64, uin return c } +// FullTxFee mocks base method. +func (m *MockIFee) FullTxFee(ctx context.Context, id uint64) ([]storage.Fee, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FullTxFee", ctx, id) + ret0, _ := ret[0].([]storage.Fee) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FullTxFee indicates an expected call of FullTxFee. +func (mr *MockIFeeMockRecorder) FullTxFee(ctx, id any) *MockIFeeFullTxFeeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FullTxFee", reflect.TypeOf((*MockIFee)(nil).FullTxFee), ctx, id) + return &MockIFeeFullTxFeeCall{Call: call} +} + +// MockIFeeFullTxFeeCall wrap *gomock.Call +type MockIFeeFullTxFeeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockIFeeFullTxFeeCall) Return(arg0 []storage.Fee, arg1 error) *MockIFeeFullTxFeeCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockIFeeFullTxFeeCall) Do(f func(context.Context, uint64) ([]storage.Fee, error)) *MockIFeeFullTxFeeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockIFeeFullTxFeeCall) DoAndReturn(f func(context.Context, uint64) ([]storage.Fee, error)) *MockIFeeFullTxFeeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // GetByID mocks base method. func (m *MockIFee) GetByID(ctx context.Context, id uint64) (*storage.Fee, error) { m.ctrl.T.Helper() diff --git a/internal/storage/postgres/fee.go b/internal/storage/postgres/fee.go index c318aa8..5b5770e 100644 --- a/internal/storage/postgres/fee.go +++ b/internal/storage/postgres/fee.go @@ -60,3 +60,13 @@ func (f *Fee) ByPayerId(ctx context.Context, id uint64, limit, offset int, sort return } + +func (f *Fee) FullTxFee(ctx context.Context, id uint64) (fees []storage.Fee, err error) { + err = f.DB().NewSelect(). + Model(&fees). + ColumnExpr("sum(amount) as amount, asset"). + Where("tx_id = ?", id). + Group("asset"). + Scan(ctx) + return +} diff --git a/internal/storage/postgres/fee_test.go b/internal/storage/postgres/fee_test.go index b118add..9c1ddf6 100644 --- a/internal/storage/postgres/fee_test.go +++ b/internal/storage/postgres/fee_test.go @@ -47,3 +47,16 @@ func (s *StorageTestSuite) TestFeeByPayerId() { s.Require().NotNil(fee.Tx) s.Require().NotEmpty(fee.Tx.Hash) } + +func (s *StorageTestSuite) TestFullTxFee() { + ctx, ctxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer ctxCancel() + + fees, err := s.storage.Fee.FullTxFee(ctx, 1) + s.Require().NoError(err) + s.Require().Len(fees, 1) + + fee := fees[0] + s.Require().EqualValues("100", fee.Amount.String()) + s.Require().EqualValues("nria", fee.Asset) +}