Skip to content

Commit

Permalink
Add route to get all pools held by a token account
Browse files Browse the repository at this point in the history
Signed-off-by: Andrew Richardson <andrew.richardson@kaleido.io>
  • Loading branch information
awrichar committed Nov 3, 2021
1 parent 6f38314 commit 9ec59d6
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 8 deletions.
107 changes: 107 additions & 0 deletions docs/swagger/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5515,6 +5515,113 @@ paths:
description: Success
default:
description: ""
/namespaces/{ns}/tokens/accounts/{key}:
get:
description: 'TODO: Description'
operationId: getTokenAccountPools
parameters:
- description: 'TODO: Description'
in: path
name: ns
required: true
schema:
example: default
type: string
- description: 'TODO: Description'
in: path
name: key
required: true
schema:
type: string
- description: Server-side request timeout (millseconds, or set a custom suffix
like 10s)
in: header
name: Request-Timeout
schema:
default: 120s
type: string
- description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
in: query
name: balance
schema:
type: string
- description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
in: query
name: connector
schema:
type: string
- description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
in: query
name: key
schema:
type: string
- description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
in: query
name: namespace
schema:
type: string
- description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
in: query
name: pool
schema:
type: string
- description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
in: query
name: tokenindex
schema:
type: string
- description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^'
in: query
name: updated
schema:
type: string
- description: Sort field. For multi-field sort use comma separated values (or
multiple query values) with '-' prefix for descending
in: query
name: sort
schema:
type: string
- description: Ascending sort order (overrides all fields in a multi-field sort)
in: query
name: ascending
schema:
type: string
- description: Descending sort order (overrides all fields in a multi-field
sort)
in: query
name: descending
schema:
type: string
- description: 'The number of records to skip (max: 1,000). Unsuitable for bulk
operations'
in: query
name: skip
schema:
type: string
- description: 'The maximum number of records to return (max: 1,000)'
in: query
name: limit
schema:
example: "25"
type: string
- description: Return a total count as well as items (adds extra database processing)
in: query
name: count
schema:
type: string
responses:
"200":
content:
application/json:
schema:
items:
properties:
pool: {}
type: object
type: array
description: Success
default:
description: ""
/namespaces/{ns}/tokens/balances:
get:
description: 'TODO: Description'
Expand Down
46 changes: 46 additions & 0 deletions internal/apiserver/route_get_token_account_pools.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright © 2021 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package apiserver

import (
"net/http"

"github.com/hyperledger/firefly/internal/config"
"github.com/hyperledger/firefly/internal/i18n"
"github.com/hyperledger/firefly/internal/oapispec"
"github.com/hyperledger/firefly/pkg/database"
"github.com/hyperledger/firefly/pkg/fftypes"
)

var getTokenAccountPools = &oapispec.Route{
Name: "getTokenAccountPools",
Path: "namespaces/{ns}/tokens/accounts/{key}",
Method: http.MethodGet,
PathParams: []*oapispec.PathParam{
{Name: "ns", ExampleFromConf: config.NamespacesDefault, Description: i18n.MsgTBD},
{Name: "key", Description: i18n.MsgTBD},
},
QueryParams: nil,
FilterFactory: database.TokenBalanceQueryFactory,
Description: i18n.MsgTBD,
JSONInputValue: nil,
JSONOutputValue: func() interface{} { return []*fftypes.TokenAccountPool{} },
JSONOutputCodes: []int{http.StatusOK},
JSONHandler: func(r *oapispec.APIRequest) (output interface{}, err error) {
return filterResult(r.Or.Assets().GetTokenAccountPools(r.Ctx, r.PP["ns"], r.PP["key"], r.Filter))
},
}
42 changes: 42 additions & 0 deletions internal/apiserver/route_get_token_account_pools_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright © 2021 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package apiserver

import (
"net/http/httptest"
"testing"

"github.com/hyperledger/firefly/mocks/assetmocks"
"github.com/hyperledger/firefly/pkg/fftypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func TestGetTokenAccountPools(t *testing.T) {
o, r := newTestAPIServer()
mam := &assetmocks.Manager{}
o.On("Assets").Return(mam)
req := httptest.NewRequest("GET", "/api/v1/namespaces/ns1/tokens/accounts/0x1", nil)
req.Header.Set("Content-Type", "application/json; charset=utf-8")
res := httptest.NewRecorder()

mam.On("GetTokenAccountPools", mock.Anything, "ns1", "0x1", mock.Anything).
Return([]*fftypes.TokenAccountPool{}, nil, nil)
r.ServeHTTP(res, req)

assert.Equal(t, 200, res.Result().StatusCode)
}
1 change: 1 addition & 0 deletions internal/apiserver/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ var routes = []*oapispec.Route{
getTokenBalances,
getTokenAccounts,
getTokenAccountsByPool,
getTokenAccountPools,
getTokenTransfers,
getTokenTransfersByPool,
getTokenTransferByID,
Expand Down
5 changes: 5 additions & 0 deletions internal/assets/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Manager interface {

GetTokenBalances(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.TokenBalance, *database.FilterResult, error)
GetTokenAccounts(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.TokenAccount, *database.FilterResult, error)
GetTokenAccountPools(ctx context.Context, ns, key string, filter database.AndFilter) ([]*fftypes.TokenAccountPool, *database.FilterResult, error)

GetTokenTransfers(ctx context.Context, ns string, filter database.AndFilter) ([]*fftypes.TokenTransfer, *database.FilterResult, error)
GetTokenTransferByID(ctx context.Context, ns, id string) (*fftypes.TokenTransfer, error)
Expand Down Expand Up @@ -135,6 +136,10 @@ func (am *assetManager) GetTokenAccounts(ctx context.Context, ns string, filter
return am.database.GetTokenAccounts(ctx, am.scopeNS(ns, filter))
}

func (am *assetManager) GetTokenAccountPools(ctx context.Context, ns, key string, filter database.AndFilter) ([]*fftypes.TokenAccountPool, *database.FilterResult, error) {
return am.database.GetTokenAccountPools(ctx, key, am.scopeNS(ns, filter))
}

func (am *assetManager) GetTokenConnectors(ctx context.Context, ns string) ([]*fftypes.TokenConnector, error) {
if err := fftypes.ValidateFFNameField(ctx, ns, "namespace"); err != nil {
return nil, err
Expand Down
12 changes: 12 additions & 0 deletions internal/assets/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,18 @@ func TestGetTokenAccounts(t *testing.T) {
assert.NoError(t, err)
}

func TestGetTokenAccountPools(t *testing.T) {
am, cancel := newTestAssets(t)
defer cancel()

mdi := am.database.(*databasemocks.Plugin)
fb := database.TokenBalanceQueryFactory.NewFilter(context.Background())
f := fb.And()
mdi.On("GetTokenAccountPools", context.Background(), "0x1", f).Return([]*fftypes.TokenAccountPool{}, nil, nil)
_, _, err := am.GetTokenAccountPools(context.Background(), "ns1", "0x1", f)
assert.NoError(t, err)
}

func TestGetTokenConnectors(t *testing.T) {
am, cancel := newTestAssets(t)
defer cancel()
Expand Down
33 changes: 30 additions & 3 deletions internal/database/sqlcommon/tokenbalance_sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,9 @@ func (s *SQLCommon) GetTokenBalances(ctx context.Context, filter database.Filter
}

func (s *SQLCommon) GetTokenAccounts(ctx context.Context, filter database.Filter) ([]*fftypes.TokenAccount, *database.FilterResult, error) {
query, fop, fi, err := s.filterSelect(ctx, "", sq.Select("key").Distinct().From("tokenbalance"), filter, tokenBalanceFilterFieldMap, []interface{}{"seq"})
query, fop, fi, err := s.filterSelect(ctx, "",
sq.Select("key").Distinct().From("tokenbalance"),
filter, tokenBalanceFilterFieldMap, []interface{}{"seq"})
if err != nil {
return nil, nil, err
}
Expand All @@ -207,12 +209,37 @@ func (s *SQLCommon) GetTokenAccounts(ctx context.Context, filter database.Filter
var accounts []*fftypes.TokenAccount
for rows.Next() {
var account fftypes.TokenAccount
err := rows.Scan(&account.Key)
if err != nil {
if err := rows.Scan(&account.Key); err != nil {
return nil, nil, i18n.WrapError(ctx, err, i18n.MsgDBReadErr, "tokenbalance")
}
accounts = append(accounts, &account)
}

return accounts, s.queryRes(ctx, tx, "tokenbalance", fop, fi), err
}

func (s *SQLCommon) GetTokenAccountPools(ctx context.Context, key string, filter database.Filter) ([]*fftypes.TokenAccountPool, *database.FilterResult, error) {
query, fop, fi, err := s.filterSelect(ctx, "",
sq.Select("pool_id").Distinct().From("tokenbalance").Where(sq.Eq{"key": key}),
filter, tokenBalanceFilterFieldMap, []interface{}{"seq"})
if err != nil {
return nil, nil, err
}

rows, tx, err := s.query(ctx, query)
if err != nil {
return nil, nil, err
}
defer rows.Close()

var pools []*fftypes.TokenAccountPool
for rows.Next() {
var pool fftypes.TokenAccountPool
if err := rows.Scan(&pool.Pool); err != nil {
return nil, nil, i18n.WrapError(ctx, err, i18n.MsgDBReadErr, "tokenbalance")
}
pools = append(pools, &pool)
}

return pools, s.queryRes(ctx, tx, "tokenbalance", fop, fi), err
}
39 changes: 36 additions & 3 deletions internal/database/sqlcommon/tokenbalance_sql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,21 @@ func TestTokenBalanceE2EWithDB(t *testing.T) {
assert.Equal(t, string(balanceJson), string(balanceReadJson))

// Query the list of unique accounts
fb2 := database.TokenBalanceQueryFactory.NewFilter(ctx)
accounts, fr, err := s.GetTokenAccounts(ctx, fb2.And().Count(true))
accounts, _, err := s.GetTokenAccounts(ctx, fb.And())
assert.NoError(t, err)
assert.Equal(t, int64(2), *fr.TotalCount)
assert.Equal(t, 2, len(accounts))
assert.Equal(t, "0x1", accounts[0].Key)
assert.Equal(t, "0x0", accounts[1].Key)

// Query the pools for each account
pools, _, err := s.GetTokenAccountPools(ctx, "0x0", fb.And())
assert.NoError(t, err)
assert.Equal(t, 1, len(pools))
assert.Equal(t, *transfer.Pool, *pools[0].Pool)
pools, _, err = s.GetTokenAccountPools(ctx, "0x1", fb.And())
assert.NoError(t, err)
assert.Equal(t, 1, len(pools))
assert.Equal(t, *transfer.Pool, *pools[0].Pool)
}

func TestUpdateTokenBalancesFailBegin(t *testing.T) {
Expand Down Expand Up @@ -248,3 +256,28 @@ func TestGetTokenAccountsScanFail(t *testing.T) {
assert.Regexp(t, "FF10121", err)
assert.NoError(t, mock.ExpectationsWereMet())
}

func TestGetTokenAccountPoolsQueryFail(t *testing.T) {
s, mock := newMockProvider().init()
mock.ExpectQuery("SELECT .*").WillReturnError(fmt.Errorf("pop"))
f := database.TokenBalanceQueryFactory.NewFilter(context.Background()).And()
_, _, err := s.GetTokenAccountPools(context.Background(), "0x1", f)
assert.Regexp(t, "FF10115", err)
assert.NoError(t, mock.ExpectationsWereMet())
}

func TestGetTokenAccountPoolsBuildQueryFail(t *testing.T) {
s, _ := newMockProvider().init()
f := database.TokenBalanceQueryFactory.NewFilter(context.Background()).Eq("pool", map[bool]bool{true: false})
_, _, err := s.GetTokenAccountPools(context.Background(), "0x1", f)
assert.Regexp(t, "FF10149.*pool", err)
}

func TestGetTokenAccountPoolsScanFail(t *testing.T) {
s, mock := newMockProvider().init()
mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows([]string{"key", "bad"}).AddRow("too many", "columns"))
f := database.TokenBalanceQueryFactory.NewFilter(context.Background()).And()
_, _, err := s.GetTokenAccountPools(context.Background(), "0x1", f)
assert.Regexp(t, "FF10121", err)
assert.NoError(t, mock.ExpectationsWereMet())
}
32 changes: 32 additions & 0 deletions mocks/assetmocks/manager.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 9ec59d6

Please sign in to comment.