Skip to content

Commit

Permalink
feat: deterministic query oracle (#893)
Browse files Browse the repository at this point in the history
  • Loading branch information
aljo242 authored Jan 16, 2025
1 parent d2dfc66 commit bbd36fe
Show file tree
Hide file tree
Showing 11 changed files with 2,573 additions and 127 deletions.
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 @@ func (q queryServer) GetPrices(ctx context.Context, req *types.GetPricesRequest)
}, 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
}

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 @@ func (k *Keeper) GetAllCurrencyPairs(ctx context.Context) []connecttypes.Currenc
}

// 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 @@ func (k *Keeper) GetCurrencyPairMapping(ctx context.Context) (map[uint64]connect
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
}

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

0 comments on commit bbd36fe

Please sign in to comment.