The x/marketmap
module encapsulates a system for creating and updating a unified configuration that is stored on-chain
and consumed by a set of oracle service providers (Slinky oracle, etc.).
The core goal of the system is to collect off-chain market updates and to post them on chain, informing oracle service providers to fetch prices for new markets.
The data is stored in a MarketMap
data structure which can be queried and consumed by oracle services.
When integrating x/marketmap
into your Cosmos SDK application, some considerations must be made:
Integrating modules can use the hooks exposed by x/marketmap
to update their state whenever
changes are made to the marketmap.
An example of this can be seen in x/oracle
's implementation of the AfterMarketCreated
hook. This hook
triggers the creation of a CurrencyPairState
that corresponds to the new Ticker
that was created in the marketmap.
This allows for a unified flow where updates to the market map prepare the x/oracle
module for new price feeds.
Any modules that integrate with x/marketmap
must set their InitGenesis
to occur before the x/marketmap
module's
InitGenesis
. This is so that logic any consuming modules may want to implement in AfterMarketGenesis
will be
run properly.
The market map data is as follows:
// Market encapsulates a Ticker and its provider-specific configuration.
message Market {
option (gogoproto.goproto_stringer) = false;
option (gogoproto.stringer) = false;
// Ticker represents a price feed for a given asset pair i.e. BTC/USD. The
// price feed is scaled to a number of decimal places and has a minimum number
// of providers required to consider the ticker valid.
Ticker ticker = 1 [ (gogoproto.nullable) = false ];
// ProviderConfigs is the list of provider-specific configs for this Market.
repeated ProviderConfig provider_configs = 2 [ (gogoproto.nullable) = false ];
}
// Ticker represents a price feed for a given asset pair i.e. BTC/USD. The price
// feed is scaled to a number of decimal places and has a minimum number of
// providers required to consider the ticker valid.
message Ticker {
option (gogoproto.goproto_stringer) = false;
option (gogoproto.stringer) = false;
// CurrencyPair is the currency pair for this ticker.
slinky.types.v1.CurrencyPair currency_pair = 1
[ (gogoproto.nullable) = false ];
// Decimals is the number of decimal places for the ticker. The number of
// decimal places is used to convert the price to a human-readable format.
uint64 decimals = 2;
// MinProviderCount is the minimum number of providers required to consider
// the ticker valid.
uint64 min_provider_count = 3;
// Enabled is the flag that denotes if the Ticker is enabled for price
// fetching by an oracle.
bool enabled = 14;
// MetadataJSON is a string of JSON that encodes any extra configuration
// for the given ticker.
string metadata_JSON = 15;
}
message ProviderConfig {
// Name corresponds to the name of the provider for which the configuration is
// being set.
string name = 1;
// OffChainTicker is the off-chain representation of the ticker i.e. BTC/USD.
// The off-chain ticker is unique to a given provider and is used to fetch the
// price of the ticker from the provider.
string off_chain_ticker = 2;
// NormalizeByPair is the currency pair for this ticker to be normalized by.
// For example, if the desired Ticker is BTC/USD, this market could be reached
// using: OffChainTicker = BTC/USDT NormalizeByPair = USDT/USD This field is
// optional and nullable.
slinky.types.v1.CurrencyPair normalize_by_pair = 3;
// Invert is a boolean indicating if the BASE and QUOTE of the market should
// be inverted. i.e. BASE -> QUOTE, QUOTE -> BASE
bool invert = 4;
// MetadataJSON is a string of JSON that encodes any extra configuration
// for the given provider config.
string metadata_JSON = 15;
}
// MarketMap maps ticker strings to their Markets.
message MarketMap {
option (gogoproto.goproto_stringer) = false;
option (gogoproto.stringer) = false;
// Markets is the full list of tickers and their associated configurations
// to be stored on-chain.
map<string, Market> markets = 1 [ (gogoproto.nullable) = false ];
}
The MarketMap
message itself is not stored in state. Rather, ticker strings are used as key prefixes
so that the data can be stored in a map-like structure, while retaining determinism.
The x/marketmap
module stores its params in the keeper state. The params can be updated with governance or the
keeper authority address.
The x/marketmap
module contains the following parameters:
| Key | Type | Example | | MarketAuthorities | []string | "cosmos1vq93x443c0fznuf6...q4jd28ke6r46p999s0" |
A MarketAuthority is the bech32 address that is permitted to submit market updates to the chain.
The marketmap module emits the following events:
Attribute Key | Attribute Value |
---|---|
currency_pair | {CurrencyPair} |
decimals | {uint64} |
min_provider_count | {uint64} |
metadata | {json string} |
Other modules can register routines to execute after a certain event has occurred in x/marketmap
.
The following hooks can be registered:
AfterMarketCreated(ctx sdk.Context, ticker marketmaptypes.Market) error
- Called after a new market is created in
CreateMarket
message server.
- Called after a new market is created in
AfterMarketUpdated(ctx sdk.Context, ticker marketmaptypes.Market) error
- Called after a new market is updated in
UpdateMarket
message server.
- Called after a new market is updated in
AfterMarketGenesis(ctx sdk.Context, tickers map[string]marketmaptypes.Market) error
- Called at the end of
InitGenesis
for thex/marketmap
keeper.
- Called at the end of
A user can query the marketmap
module using gRPC endpoints.
The MarketMap
endpoint queries the full state of the market map as well as associated information such as
LastUpdated
.
Example:
grpcurl -plaintext localhost:9090 slinky.marketmap.v1.Query/MarketMap
Example response:
{
"marketMap": {
"tickers": {
"BITCOIN/USD": {
"currencyPair": {
"Base": "BITCOIN",
"Quote": "USD"
},
"decimals": "8",
"minProviderCount": "3"
}
},
"paths": {
"BITCOIN/USD": {
"paths": [
{
"operations": [
{
"currencyPair": {
"Base": "BITCOIN",
"Quote": "USD"
}
}
]
}
]
}
},
"providers": {
"BITCOIN/USD": {
"providers": [
{
"name": "kucoin",
"offChainTicker": "btc_usd"
},
{
"name": "mexc",
"offChainTicker": "btc-usd"
},
{
"name": "binance",
"offChainTicker": "BTCUSD"
}
]
}
}
},
"lastUpdated": "1"
}
The LastUpdated
endpoint queries the last block height that the market map was updated.
This can be consumed by oracle service providers to recognize when their local configurations
must be updated using the heavier MarketMap
query.
Example:
grpcurl -plaintext localhost:9090 slinky.marketmap.v1.Query/LastUpdated
Example response:
{
"lastUpdated": "1"
}
The params query allows users to query values set as marketmap parameters.
Example:
grpcurl -plaintext localhost:9090 slinky.marketmap.v1.Query/Params
Example response:
{
"params": {
"marketAuthorities": "[cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn]"
}
}
A user can query the marketmap
module using the CLI.
The MarketMap
endpoint queries the full state of the market map as well as associated information such as
LastUpdated
and Version
.
Example:
slinkyd q marketmap market-map
The LastUpdated
query queries the last block height that the market map was updated.
This can be consumed by oracle service providers to recognize when their local configurations
must be updated using the heavier MarketMap
query.
Example:
slinkyd q marketmap last-updated
The params query allows users to query values set as marketmap parameters.
Example:
slinkyd q marketmap params