Skip to content

Commit

Permalink
feat: add new query for dated prices
Browse files Browse the repository at this point in the history
  • Loading branch information
matthiasmatt committed Oct 29, 2024
1 parent 3aac937 commit 9cf67f2
Show file tree
Hide file tree
Showing 9 changed files with 581 additions and 99 deletions.
21 changes: 20 additions & 1 deletion proto/nibiru/oracle/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ service Query {
option (google.api.http).get = "/nibiru/oracle/v1beta1/exchange_rate_twap";
}

// DatedExchangeRate returns latest price of a pair
rpc DatedExchangeRate(QueryExchangeRateRequest)
returns (QueryDatedExchangeRateResponse) {
option (google.api.http).get = "/nibiru/oracle/v1beta1/dated_exchange_rate";
}

// ExchangeRates returns exchange rates of all pairs
rpc ExchangeRates(QueryExchangeRatesRequest)
returns (QueryExchangeRatesResponse) {
Expand Down Expand Up @@ -112,7 +118,20 @@ message QueryExchangeRateResponse {

// QueryExchangeRatesRequest is the request type for the Query/ExchangeRates RPC
// method.
message QueryExchangeRatesRequest {}
message QueryExchangeRatesRequest {
}

// QueryDatedExchangeRateResponse is the request type for the
// Query/DatedExchangeRate RPC method.
message QueryDatedExchangeRateResponse {
string price = 1 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// milliseconds since unix epoch
int64 timestamp_ms = 2;
}

// QueryExchangeRatesResponse is response type for the
// Query/ExchangeRates RPC method.
Expand Down
15 changes: 9 additions & 6 deletions x/evm/embeds/contracts/IOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ pragma solidity >=0.8.19;

/// @notice Oracle interface for querying exchange rates
interface IOracle {
/// @notice Queries the exchange rate for a given pair
/// @param pair The asset pair to query. For example, "ubtc:uusd" is the
/// USD price of BTC and "unibi:uusd" is the USD price of NIBI.
/// @return The exchange rate (a decimal value) as a string.
/// @dev This function is view-only and does not modify state.
function queryExchangeRate(string memory pair) external view returns (string memory);
/// @notice Queries the dated exchange rate for a given pair
/// @param pair The asset pair to query. For example, "ubtc:uusd" is the
/// USD price of BTC and "unibi:uusd" is the USD price of NIBI.
/// @return The exchange rate (a decimal value) as a string and the block
/// at which it was cerated.
/// @dev This function is view-only and does not modify state.
function queryExchangeRate(
string memory pair
) external view returns (string memory, uint256);
}

address constant ORACLE_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000000801;
Expand Down
2 changes: 1 addition & 1 deletion x/evm/precompile/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func (p precompileOracle) queryExchangeRate(
return nil, err
}

price, err := p.oracleKeeper.GetExchangeRate(ctx, assetPair)
price, err := p.oracleKeeper.GetDatedExchangeRate(ctx, assetPair)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion x/evm/precompile/oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (s *OracleSuite) TestOracle_HappyPath() {
s.NoError(err)

// Check the response
s.Equal("0.067000000000000000", out[0].(string))
s.Equal("exchange_rate:\"67000000000000000\" created_block:1 ", out[0].(string))
}
}

Expand Down
17 changes: 17 additions & 0 deletions x/oracle/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,23 @@ func (k Keeper) GetExchangeRate(ctx sdk.Context, pair asset.Pair) (price sdk.Dec
return
}

func (k Keeper) GetDatedExchangeRate(ctx sdk.Context, pair asset.Pair) (exchangeRate types.PriceSnapshot, err error) {
iterator := k.PriceSnapshots.Iterate(
ctx,
collections.PairRange[asset.Pair, time.Time]{}.
Prefix(pair).
Descending(),
)
defer iterator.Close()

if iterator.Valid() {
exchangeRate = iterator.Value()
return exchangeRate, nil
} else {
return types.PriceSnapshot{}, types.ErrInvalidExchangeRate.Wrapf("no snapshots for pair %s", pair.String())
}
}

// SetPrice sets the price for a pair as well as the price snapshot.
func (k Keeper) SetPrice(ctx sdk.Context, pair asset.Pair, price sdk.Dec) {
k.ExchangeRates.Insert(ctx, pair, types.DatedPrice{ExchangeRate: price, CreatedBlock: uint64(ctx.BlockHeight())})
Expand Down
14 changes: 14 additions & 0 deletions x/oracle/keeper/querier.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@ func (q querier) ExchangeRateTwap(c context.Context, req *types.QueryExchangeRat
return &types.QueryExchangeRateResponse{ExchangeRate: twap}, nil
}

// get the latest price snapshot from the oracle for a pair
func (q querier) DatedExchangeRate(c context.Context, req *types.QueryExchangeRateRequest) (response *types.QueryDatedExchangeRateResponse, err error) {
if _, err = q.ExchangeRate(c, req); err != nil {
return
}

ctx := sdk.UnwrapSDKContext(c)
snapshot, err := q.Keeper.GetDatedExchangeRate(ctx, req.Pair)
if err != nil {
return &types.QueryDatedExchangeRateResponse{}, err
}
return &types.QueryDatedExchangeRateResponse{Price: snapshot.Price, TimestampMs: snapshot.TimestampMs}, nil
}

// ExchangeRates queries exchange rates of all pairs
func (q querier) ExchangeRates(c context.Context, _ *types.QueryExchangeRatesRequest) (*types.QueryExchangeRatesResponse, error) {
ctx := sdk.UnwrapSDKContext(c)
Expand Down
99 changes: 99 additions & 0 deletions x/oracle/keeper/querier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,105 @@ func TestQueryExchangeRateTwap(t *testing.T) {
require.Equal(t, math.LegacyMustNewDecFromStr("1700"), res.ExchangeRate)
}

func TestQueryDatedExchangeRate(t *testing.T) {
input := CreateTestFixture(t)
querier := NewQuerier(input.OracleKeeper)

// Set initial block time and height
initialTime := input.Ctx.BlockTime()
initialHeight := input.Ctx.BlockHeight()

// Pair 1: BTC/NUSD
pairBTC := asset.Registry.Pair(denoms.BTC, denoms.NUSD)
rateBTC1 := math.LegacyNewDec(1700)
rateBTC2 := math.LegacyNewDec(1800)

// Pair 2: ETH/NUSD
pairETH := asset.Registry.Pair(denoms.ETH, denoms.NUSD)
rateETH1 := math.LegacyNewDec(100)
rateETH2 := math.LegacyNewDec(110)

// --- Set first price for BTC/NUSD ---
input.OracleKeeper.SetPrice(input.Ctx, pairBTC, rateBTC1)
testutilevents.RequireContainsTypedEvent(
t,
input.Ctx,
&types.EventPriceUpdate{
Pair: pairBTC.String(),
Price: rateBTC1,
TimestampMs: input.Ctx.BlockTime().UnixMilli(),
},
)

// Advance time and block height
input.Ctx = input.Ctx.WithBlockTime(initialTime.Add(1 * time.Second)).
WithBlockHeight(initialHeight + 1)

// --- Set first price for ETH/NUSD ---
input.OracleKeeper.SetPrice(input.Ctx, pairETH, rateETH1)
testutilevents.RequireContainsTypedEvent(
t,
input.Ctx,
&types.EventPriceUpdate{
Pair: pairETH.String(),
Price: rateETH1,
TimestampMs: input.Ctx.BlockTime().UnixMilli(),
},
)

// Advance time and block height
input.Ctx = input.Ctx.WithBlockTime(initialTime.Add(2 * time.Second)).
WithBlockHeight(initialHeight + 2)

// --- Set second price for BTC/NUSD ---
input.OracleKeeper.SetPrice(input.Ctx, pairBTC, rateBTC2)
testutilevents.RequireContainsTypedEvent(
t,
input.Ctx,
&types.EventPriceUpdate{
Pair: pairBTC.String(),
Price: rateBTC2,
TimestampMs: input.Ctx.BlockTime().UnixMilli(),
},
)

// Advance time and block height
input.Ctx = input.Ctx.WithBlockTime(initialTime.Add(3 * time.Second)).
WithBlockHeight(initialHeight + 3)

// --- Set second price for ETH/NUSD ---
input.OracleKeeper.SetPrice(input.Ctx, pairETH, rateETH2)
testutilevents.RequireContainsTypedEvent(
t,
input.Ctx,
&types.EventPriceUpdate{
Pair: pairETH.String(),
Price: rateETH2,
TimestampMs: input.Ctx.BlockTime().UnixMilli(),
},
)

// Wrap context for querying
ctx := sdk.WrapSDKContext(input.Ctx)

// --- Query latest snapshot for BTC/NUSD ---
resBTC, err := querier.DatedExchangeRate(ctx, &types.QueryExchangeRateRequest{Pair: pairBTC})
require.NoError(t, err)
require.Equal(t, rateBTC2, resBTC.Price)
require.Equal(t, input.Ctx.BlockTime().UnixMilli(), resBTC.TimestampMs)

// --- Query latest snapshot for ETH/NUSD ---
resETH, err := querier.DatedExchangeRate(ctx, &types.QueryExchangeRateRequest{Pair: pairETH})
require.NoError(t, err)
require.Equal(t, rateETH2, resETH.Price)
require.Equal(t, input.Ctx.BlockTime().UnixMilli(), resETH.TimestampMs)

// --- Query a pair with no snapshots (should return an error) ---
pairATOM := asset.Registry.Pair(denoms.ATOM, denoms.NUSD)
_, err = querier.DatedExchangeRate(ctx, &types.QueryExchangeRateRequest{Pair: pairATOM})
require.Error(t, err)
}

func TestCalcTwap(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading

0 comments on commit 9cf67f2

Please sign in to comment.