Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: deterministic query oracle #893

Merged
merged 7 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,673 changes: 1,594 additions & 79 deletions api/connect/oracle/v2/query.pulsar.go

Large diffs are not rendered by default.

52 changes: 48 additions & 4 deletions api/connect/oracle/v2/query_grpc.pb.go

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

29 changes: 29 additions & 0 deletions proto/connect/oracle/v2/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ service Query {
additional_bindings : []
};
}

// Get the mapping of currency pair ID <-> currency pair as a list. This is
// useful for indexers that have access to the ID of a currency pair, but no
// way to get the underlying currency pair from it.
rpc GetCurrencyPairMappingList(GetCurrencyPairMappingListRequest)
returns (GetCurrencyPairMappingListResponse) {
option (google.api.http) = {
get : "/connect/oracle/v2/get_currency_pair_mapping_list"
additional_bindings : []
};
}
}

message GetAllCurrencyPairsRequest {}
Expand Down Expand Up @@ -94,3 +105,21 @@ message GetCurrencyPairMappingResponse {
map<uint64, connect.types.v2.CurrencyPair> currency_pair_mapping = 1
[ (gogoproto.nullable) = false ];
}

// GetCurrencyPairMappingRequest is the GetCurrencyPairMapping request type.
message GetCurrencyPairMappingListRequest {}

message CurrencyPairMapping {
// ID is the unique identifier for this currency pair string.
uint64 id = 1;
// CurrencyPair is the human-readable representation of the currency pair.
connect.types.v2.CurrencyPair currency_pair = 2
[ (gogoproto.nullable) = false ];
}

// GetCurrencyPairMappingResponse is the GetCurrencyPairMapping response type.
message GetCurrencyPairMappingListResponse {
// mappings is a list of the id representing the currency pair
// to the currency pair itself.
repeated CurrencyPairMapping mappings = 1 [ (gogoproto.nullable) = false ];
}
76 changes: 75 additions & 1 deletion tests/integration/connect_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,81 @@ func QueryCurrencyPairs(chain *cosmos.CosmosChain) (*oracletypes.GetAllCurrencyP
client := oracletypes.NewQueryClient(cc)

// query the currency pairs
return client.GetAllCurrencyPairs(context.Background(), &oracletypes.GetAllCurrencyPairsRequest{})
resp, err := client.GetAllCurrencyPairs(context.Background(), &oracletypes.GetAllCurrencyPairsRequest{})

// check that there is a correspondence between mappings and the raw response
mappingResp, err := QueryCurrencyPairMappings(chain)
if err != nil {
return nil, err
}

if len(resp.CurrencyPairs) != len(mappingResp.CurrencyPairMapping) {
return nil, fmt.Errorf("list and map responses should be the same length: got %d list, %d map",
len(resp.CurrencyPairs),
len(mappingResp.CurrencyPairMapping),
)
}
for _, v := range mappingResp.CurrencyPairMapping {
found := false
for _, cp := range resp.CurrencyPairs {
if v.Equal(cp) {
found = true
}
}

if !found {
return nil, fmt.Errorf("currency pair %v was found in mapping response but not in currency pair list", v)
}
}

return resp, err
}

// QueryCurrencyPairMappings queries the chain for the given currency pair mappings
func QueryCurrencyPairMappings(chain *cosmos.CosmosChain) (*oracletypes.GetCurrencyPairMappingResponse, error) {
// get grpc address
grpcAddr := chain.GetHostGRPCAddress()

// create the client
cc, err := grpc.Dial(grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, err
}
defer cc.Close()

// create the oracle client
client := oracletypes.NewQueryClient(cc)

// query the currency pairs map
mapRes, err := client.GetCurrencyPairMapping(context.Background(), &oracletypes.GetCurrencyPairMappingRequest{})
if err != nil {
return nil, err
}

// query the currency pairs list
listRes, err := client.GetCurrencyPairMappingList(context.Background(), &oracletypes.GetCurrencyPairMappingListRequest{})
if err != nil {
return nil, err
}

if len(listRes.Mappings) != len(mapRes.CurrencyPairMapping) {
return nil, fmt.Errorf("map and list responses should be the same length: got %d list, %d map",
len(listRes.Mappings),
len(mapRes.CurrencyPairMapping),
)
}
for _, m := range listRes.Mappings {
cp, found := mapRes.CurrencyPairMapping[m.Id]
if !found {
return nil, fmt.Errorf("mapping for %d not found", m.Id)
}

if !m.CurrencyPair.Equal(cp) {
return nil, fmt.Errorf("market %s is not equal to %s", m.CurrencyPair.String(), cp.String())
}
}

return mapRes, nil
}

// QueryCurrencyPair queries the price for the given currency-pair given a desired height to query from
Expand Down
2 changes: 2 additions & 0 deletions x/marketmap/keeper/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ func NewQueryServer(k *Keeper) types.QueryServer {
}

// MarketMap returns the full MarketMap and associated information stored in the x/marketmap module.
//
// NOTE: the map type returned by this query is NOT SAFE. Use Markets instead for a safe value.
func (q queryServerImpl) MarketMap(goCtx context.Context, req *types.MarketMapRequest) (*types.MarketMapResponse, error) {
if req == nil {
return nil, fmt.Errorf("request cannot be nil")
Expand Down
12 changes: 12 additions & 0 deletions x/oracle/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,22 @@
}, nil
}

// GetCurrencyPairMapping returns a map of ID -> CurrencyPair.
//
// NOTE: the map type returned by this query is NOT SAFE. Use GetCurrencyPairMappingList instead for a safe value.
func (q queryServer) GetCurrencyPairMapping(ctx context.Context, _ *types.GetCurrencyPairMappingRequest) (*types.GetCurrencyPairMappingResponse, error) {
pairs, err := q.k.GetCurrencyPairMapping(ctx)
if err != nil {
return nil, err
}
return &types.GetCurrencyPairMappingResponse{CurrencyPairMapping: pairs}, nil
}

func (q queryServer) GetCurrencyPairMappingList(ctx context.Context, _ *types.GetCurrencyPairMappingListRequest) (*types.GetCurrencyPairMappingListResponse, error) {
pairs, err := q.k.GetCurrencyPairMappingList(ctx)
if err != nil {
return nil, err
}

Check warning on line 140 in x/oracle/keeper/grpc_query.go

View check run for this annotation

Codecov / codecov/patch

x/oracle/keeper/grpc_query.go#L139-L140

Added lines #L139 - L140 were not covered by tests

return &types.GetCurrencyPairMappingListResponse{Mappings: pairs}, nil
}
40 changes: 40 additions & 0 deletions x/oracle/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,43 @@ func (s *KeeperTestSuite) TestGetCurrencyPairMappingGRPC() {
}
})
}

func (s *KeeperTestSuite) TestGetCurrencyPairMappingListGRPC() {
cp1 := connecttypes.CurrencyPair{Base: "TEST", Quote: "COIN1"}
cp2 := connecttypes.CurrencyPair{Base: "TEST", Quote: "COIN2"}
cp3 := connecttypes.CurrencyPair{Base: "TEST", Quote: "COIN3"}

qs := keeper.NewQueryServer(s.oracleKeeper)
// test that after CurrencyPairs are registered, all of them are returned from the query
s.Run("after CurrencyPairs are registered, all of them are returned from the query", func() {
currencyPairs := []connecttypes.CurrencyPair{
cp1,
cp2,
cp3,
}
for _, cp := range currencyPairs {
s.Require().NoError(s.oracleKeeper.CreateCurrencyPair(s.ctx, cp))
}

// manually insert a new CurrencyPair as well
s.Require().NoError(s.oracleKeeper.SetPriceForCurrencyPair(s.ctx, cp1, types.QuotePrice{Price: sdkmath.NewInt(100)}))

// query for pairs
res, err := qs.GetCurrencyPairMappingList(s.ctx, nil)
s.Require().Nil(err)
s.Require().Equal([]types.CurrencyPairMapping{
{
Id: 0,
CurrencyPair: cp1,
},
{
Id: 1,
CurrencyPair: cp2,
},
{
Id: 2,
CurrencyPair: cp3,
},
}, res.Mappings)
})
}
18 changes: 18 additions & 0 deletions x/oracle/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@
}

// GetCurrencyPairMapping returns a CurrencyPair mapping by ID that have currently been stored to state.
// NOTE: this map[] type should not be used by on-chain code.
func (k *Keeper) GetCurrencyPairMapping(ctx context.Context) (map[uint64]connecttypes.CurrencyPair, error) {
numPairs, err := k.numCPs.Get(ctx)
if err != nil {
Expand All @@ -302,6 +303,23 @@
return pairs, nil
}

// GetCurrencyPairMappingList returns a CurrencyPair mapping by ID that have currently been stored to state as a list.
func (k *Keeper) GetCurrencyPairMappingList(ctx context.Context) ([]types.CurrencyPairMapping, error) {
pairs := make([]types.CurrencyPairMapping, 0)
// aggregate CurrencyPairs stored under KeyPrefixNonce
err := k.IterateCurrencyPairs(ctx, func(cp connecttypes.CurrencyPair, cps types.CurrencyPairState) {
pairs = append(pairs, types.CurrencyPairMapping{
Id: cps.GetId(),
CurrencyPair: cp,
})
})
if err != nil {
return nil, err
}

Check warning on line 318 in x/oracle/keeper/keeper.go

View check run for this annotation

Codecov / codecov/patch

x/oracle/keeper/keeper.go#L317-L318

Added lines #L317 - L318 were not covered by tests

return pairs, nil
}

// IterateCurrencyPairs iterates over all CurrencyPairs in the store, and executes a callback for each CurrencyPair.
func (k *Keeper) IterateCurrencyPairs(ctx context.Context, cb func(cp connecttypes.CurrencyPair, cps types.CurrencyPairState)) error {
it, err := k.currencyPairs.Iterate(ctx, nil)
Expand Down
Loading
Loading