diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index b8fcbf774..8bcc521bf 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -17,7 +17,6 @@ adadfc adb adec adf -adk aec aed afa @@ -182,7 +181,6 @@ codecmock codecov codectypes codeowners -codespell coinbaseapi coinbasebtcusd coinbaseethusd @@ -232,7 +230,6 @@ datacenters datadog datasources davecgh -davidterpay dbddb dbf dbm @@ -256,6 +253,7 @@ depinject desertbit Devation dfa +dfb dfd dfe dgraph @@ -281,6 +279,7 @@ dyno EAB eaf ece +ecfc eci eda eeba @@ -314,7 +313,6 @@ ETHUSDTAMMID EUR eux evm -exportloopref extendee fabf Factom @@ -339,6 +337,7 @@ FHK findstring Finex flatbuffers +fpmm fromjson fsnotify fznuf @@ -539,7 +538,6 @@ mmmocks mms mmservicetypes mmtypes -moby mocd mockmetrics mockstrategies @@ -578,7 +576,6 @@ neuton nfalling nhooyr Nilf -nivasan nocapongodskiptoonicewititshiiiiiiiii nolint nolintlint @@ -618,9 +615,6 @@ orderedcode otel otelgrpc otelhttp -otlp -otlptrace -otlptracehttp outpkg outstruct pamock @@ -755,6 +749,7 @@ SMOLE solana solanago solusd +sortkeys sortme sourcegraph soz @@ -885,8 +880,8 @@ Warehime wastedassign wbs websockets -wethusdc wesl +wethusdc WFys Whyy wincred @@ -959,4 +954,3 @@ zerolog ZFvy zondax zstd -sortkeys diff --git a/.golangci.yml b/.golangci.yml index 8401ec1bc..b9d35d515 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -16,7 +16,6 @@ linters: - dogsled # Check for two durations multiplied together. - durationcheck - - exportloopref - goconst - gocritic - gofumpt @@ -28,8 +27,6 @@ linters: - misspell - nakedret - errorlint - # Checks for pointers to enclosing loop variables. - - exportloopref - nolintlint # Finds shadowing of Go's predeclared identifiers. # I hear a lot of complaints from junior developers. diff --git a/cmd/constants/marketmaps/markets.go b/cmd/constants/marketmaps/markets.go index 57d52b463..0991ca76c 100644 --- a/cmd/constants/marketmaps/markets.go +++ b/cmd/constants/marketmaps/markets.go @@ -9724,48 +9724,14 @@ var ( "Base":"WILL_BERNIE_SANDERS_WIN_THE_2024_US_PRESIDENTIAL_ELECTION?YES", "Quote":"USD" }, - "decimals":3, + "decimals":4, "min_provider_count":1, "enabled":true }, "provider_configs":[ { "name":"polymarket_api", - "off_chain_ticker":"95128817762909535143571435260705470642391662537976312011260538371392879420759" - } - ] - }, - "WILL_ROBERT_F_KENNEDY_JR_WIN_THE_2024_US_PRESIDENTIAL_ELECTION?NO/USD":{ - "ticker":{ - "currency_pair":{ - "Base":"WILL_ROBERT_F_KENNEDY_JR_WIN_THE_2024_US_PRESIDENTIAL_ELECTION?NO", - "Quote":"USD" - }, - "decimals":3, - "min_provider_count":1, - "enabled":true - }, - "provider_configs":[ - { - "name":"polymarket_api", - "off_chain_ticker":"56404905393055211239795086916790918063008904529043139446524120756836481670648" - } - ] - }, - "USA_WINS_THE_MOST_GOLD_MEDALS_IN_2024_PARIS_OLYMPICS?YES/USD":{ - "ticker":{ - "currency_pair":{ - "Base":"USA_WINS_THE_MOST_GOLD_MEDALS_IN_2024_PARIS_OLYMPICS?YES", - "Quote":"USD" - }, - "decimals":3, - "min_provider_count":1, - "enabled":true - }, - "provider_configs":[ - { - "name":"polymarket_api", - "off_chain_ticker":"21948917496837354367910826573765617012647201536430148892502780921686496760749" + "off_chain_ticker":"0x08f5fe8d0d29c08a96f0bc3dfb52f50e0caf470d94d133d95d38fa6c847e0925/95128817762909535143571435260705470642391662537976312011260538371392879420759" } ] }, @@ -9775,14 +9741,14 @@ var ( "Base":"WILL_INSIDE_OUT_2_GROSS_MOST_IN_2024?YES", "Quote":"USD" }, - "decimals":3, + "decimals":4, "min_provider_count":1, "enabled":true }, "provider_configs":[ { "name":"polymarket_api", - "off_chain_ticker":"50107902083284751016545440401692219408556171231461347396738260657226842527986" + "off_chain_ticker":"0x1ab07117f9f698f28490f57754d6fe5309374230c95867a7eba572892a11d710/50107902083284751016545440401692219408556171231461347396738260657226842527986" } ] } diff --git a/providers/apis/polymarket/README.md b/providers/apis/polymarket/README.md index 64c8b8a63..d4d2063ce 100644 --- a/providers/apis/polymarket/README.md +++ b/providers/apis/polymarket/README.md @@ -8,25 +8,21 @@ Polymarket is a web3 based prediction market. This provider uses the Polymarket Polymarket uses [conditional outcome tokens](https://docs.gnosis.io/conditionaltokens/), a token that represents an outcome of a specific event. All tokens in Polymarket are denominated in terms of USD. -Tickers take the form of the `?/USD`: +We suggest tickers take the form of the `?/USD`. However, tickers are ignored by the polymarket provider, and they can be whatever arbitrary data that suits your use case. The ONLY required text is your ticker must end in `/USD`. Example: `WILL_BERNIE_SANDERS_WIN_THE_2024_US_PRESIDENTIAL_ELECTION?YES/USD` +Example2: `BernieBecomesPresident/USD` -The offchain ticker is expected to be _just_ the token_id. +The offchain ticker **must** be / -example: `95128817762909535143571435260705470642391662537976312011260538371392879420759` +example: `0x08f5fe8d0d29c08a96f0bc3dfb52f50e0caf470d94d133d95d38fa6c847e0925/95128817762909535143571435260705470642391662537976312011260538371392879420759` -The Provider can handle both the midpoint and the price endpoints. However, passing in multiple endpoints to the same provider will not yield additional data, as only the first endpoint is considered for the provider. +The Provider queries the `/markets` endpoint, and looks for the token_id in the response. The provider will throw an error if the token_id in the offchain ticker is not present in the response data. Example: -Midpoint: +`https://clob.polymarket.com/markets/0xc6485bb7ea46d7bb89beb9c91e7572ecfc72a6273789496f78bc5e989e4d1638` -`https://clob.polymarket.com/midpoint?token_id=95128817762909535143571435260705470642391662537976312011260538371392879420759` - -Price: - -`https://clob.polymarket.com/price?token_id=95128817762909535143571435260705470642391662537976312011260538371392879420759&side=BUY` ## Market Config @@ -48,7 +44,7 @@ Below is an example of a market config for a single Polymarket token. "provider_configs": [ { "name": "polymarket_api", - "off_chain_ticker": "95128817762909535143571435260705470642391662537976312011260538371392879420759" + "off_chain_ticker": "0x08f5fe8d0d29c08a96f0bc3dfb52f50e0caf470d94d133d95d38fa6c847e0925/95128817762909535143571435260705470642391662537976312011260538371392879420759" } ] } @@ -73,10 +69,10 @@ Below is an example of an oracle config with a Polymarket provider. "interval": 500000000, "reconnectTimeout": 2000000000, "maxQueries": 1, - "atomic": true, + "atomic": false, "endpoints": [ { - "url": "https://clob.polymarket.com/midpoint?token_id=%s", + "url": "https://clob.polymarket.com/markets/%s", "authentication": { "apiKey": "", "apiKeyHeader": "" diff --git a/providers/apis/polymarket/api_handler.go b/providers/apis/polymarket/api_handler.go index 22f9778e3..71b4708f4 100644 --- a/providers/apis/polymarket/api_handler.go +++ b/providers/apis/polymarket/api_handler.go @@ -3,15 +3,11 @@ package polymarket import ( "encoding/json" "fmt" - "io" "math/big" "net/http" - "net/url" "strings" "time" - "golang.org/x/exp/maps" - "github.com/skip-mev/slinky/oracle/config" "github.com/skip-mev/slinky/oracle/types" providertypes "github.com/skip-mev/slinky/providers/types" @@ -21,27 +17,17 @@ const ( // Name is the name of the Polymarket provider. Name = "polymarket_api" - host = "clob.polymarket.com" - // URL is the default base URL of the Polymarket CLOB API. It uses the midpoint endpoint with a given token ID. - URL = "https://clob.polymarket.com/midpoint?token_id=%s" + // URL is the default base URL of the Polymarket CLOB API. It uses the `markets` endpoint with a given market ID. + URL = "https://clob.polymarket.com/markets/%s" - // priceAdjustmentMax is the value the price gets set to in the event of price == 1.00. - priceAdjustmentMax = .9999 - priceAdjustmentMin = .00001 + // priceAdjustmentMin is the value the price gets set to in the event of price == 0. + priceAdjustmentMin = 0.00001 ) -var ( - _ types.PriceAPIDataHandler = (*APIHandler)(nil) - - // valueExtractorFromEndpoint maps a URL path to a function that can extract the returned data from the response of that endpoint. - valueExtractorFromEndpoint = map[string]valueExtractor{ - "/midpoint": dataFromMidpoint, - "/price": dataFromPrice, - } -) +var _ types.PriceAPIDataHandler = (*APIHandler)(nil) // APIHandler implements the PriceAPIDataHandler interface for Polymarket, which can be used -// by a base provider. The handler fetches data from either the `/midpoint` or `/price` endpoint. +// by a base provider. The handler fetches data from the `markets` endpoint. type APIHandler struct { api config.APIConfig } @@ -64,117 +50,101 @@ func NewAPIHandler(api config.APIConfig) (types.PriceAPIDataHandler, error) { return nil, fmt.Errorf("invalid polymarket endpoint config: expected 1 endpoint got %d", len(api.Endpoints)) } - u, err := url.Parse(api.Endpoints[0].URL) - if err != nil { - return nil, fmt.Errorf("invalid polymarket endpoint url %q: %w", api.Endpoints[0].URL, err) - } - - if u.Host != host { - return nil, fmt.Errorf("invalid polymarket URL: expected %q got %q", host, u.Host) - } - - if _, exists := valueExtractorFromEndpoint[u.Path]; !exists { - return nil, fmt.Errorf("invalid polymarket endpoint url path %s. endpoint must be one of: %s", u.Path, strings.Join(maps.Keys(valueExtractorFromEndpoint), ",")) - } - return &APIHandler{ api: api, }, nil } // CreateURL returns the URL that is used to fetch data from the Polymarket API for the -// given ticker. Since the midpoint endpoint is automatically denominated in USD, only one ID is expected to be passed +// given ticker. Since the markets endpoint's price data is automatically denominated in USD, only one ID is expected to be passed // into this method. func (h APIHandler) CreateURL(ids []types.ProviderTicker) (string, error) { if len(ids) != 1 { return "", fmt.Errorf("expected 1 ticker, got %d", len(ids)) } - return fmt.Sprintf(h.api.Endpoints[0].URL, ids[0].GetOffChainTicker()), nil -} - -// midpointResponseBody is the response structure for the `/midpoint` endpoint of the Polymarket API. -type midpointResponseBody struct { - Mid string `json:"mid"` -} - -// priceResponseBody is the response structure for the `/price` endpoint of the Polymarket API. -type priceResponseBody struct { - Price string `json:"price"` -} - -// valueExtractor is a function that can extract (price, midpoint) from a http response body. -// This function is expected to return a sting representation of a float. -type valueExtractor func(io.ReadCloser) (string, error) - -// dataFromPrice unmarshalls data from the /price endpoint. -func dataFromPrice(reader io.ReadCloser) (string, error) { - var result priceResponseBody - err := json.NewDecoder(reader).Decode(&result) + marketID, _, err := getMarketAndTokenFromTicker(ids[0]) if err != nil { return "", err } - return result.Price, nil + return fmt.Sprintf(h.api.Endpoints[0].URL, marketID), nil } -// dataFromMidpoint unmarshalls data from the /midpoint endpoint. -func dataFromMidpoint(reader io.ReadCloser) (string, error) { - var result midpointResponseBody - err := json.NewDecoder(reader).Decode(&result) - if err != nil { - return "", err - } - return result.Mid, nil +type TokenData struct { + TokenID string `json:"token_id"` + Outcome string `json:"outcome"` + Price float64 `json:"price"` +} + +type MarketsResponse struct { + EnableOrderBook bool `json:"enable_order_book"` + Active bool `json:"active"` + Closed bool `json:"closed"` + Archived bool `json:"archived"` + AcceptingOrders bool `json:"accepting_orders"` + AcceptingOrderTimestamp time.Time `json:"accepting_order_timestamp"` + MinimumOrderSize int `json:"minimum_order_size"` + MinimumTickSize float64 `json:"minimum_tick_size"` + ConditionID string `json:"condition_id"` + QuestionID string `json:"question_id"` + Question string `json:"question"` + Description string `json:"description"` + MarketSlug string `json:"market_slug"` + EndDateIso time.Time `json:"end_date_iso"` + GameStartTime any `json:"game_start_time"` + SecondsDelay int `json:"seconds_delay"` + Fpmm string `json:"fpmm"` + MakerBaseFee int `json:"maker_base_fee"` + TakerBaseFee int `json:"taker_base_fee"` + NotificationsEnabled bool `json:"notifications_enabled"` + NegRisk bool `json:"neg_risk"` + NegRiskMarketID string `json:"neg_risk_market_id"` + NegRiskRequestID string `json:"neg_risk_request_id"` + Icon string `json:"icon"` + Image string `json:"image"` + Rewards struct { + Rates []struct { + AssetAddress string `json:"asset_address"` + RewardsDailyRate int `json:"rewards_daily_rate"` + } `json:"rates"` + MinSize int `json:"min_size"` + MaxSpread float64 `json:"max_spread"` + } `json:"rewards"` + Is5050Outcome bool `json:"is_50_50_outcome"` + Tokens []TokenData `json:"tokens"` + Tags []string `json:"tags"` } -// ParseResponse parses the HTTP response from either the `/price` or `/midpoint` endpoint of the Polymarket API endpoint and returns +// ParseResponse parses the HTTP response from the markets endpoint of the Polymarket API endpoint and returns // the resulting data. func (h APIHandler) ParseResponse(ids []types.ProviderTicker, response *http.Response) types.PriceResponse { if len(ids) != 1 { - return types.NewPriceResponseWithErr( - ids, - providertypes.NewErrorWithCode( - fmt.Errorf("expected 1 ticker, got %d", len(ids)), - providertypes.ErrorInvalidResponse, - ), - ) + return priceResponseError(ids, fmt.Errorf("expected 1 ticker, got %d", len(ids)), providertypes.ErrorInvalidResponse) } - // get the extractor function for this endpoint. - extractor, ok := valueExtractorFromEndpoint[response.Request.URL.Path] - if !ok { - return types.NewPriceResponseWithErr( - ids, - providertypes.NewErrorWithCode(fmt.Errorf("unknown request path %q", response.Request.URL.Path), providertypes.ErrorFailedToDecode), - ) + var result MarketsResponse + if err := json.NewDecoder(response.Body).Decode(&result); err != nil { + return priceResponseError(ids, fmt.Errorf("failed to decode market response: %w", err), providertypes.ErrorFailedToDecode) } - // extract the value. it should be a string representation of a float. - val, err := extractor(response.Body) + _, tokenID, err := getMarketAndTokenFromTicker(ids[0]) if err != nil { - return types.NewPriceResponseWithErr( - ids, - providertypes.NewErrorWithCode(err, providertypes.ErrorFailedToDecode), - ) + return priceResponseError(ids, err, providertypes.ErrorAPIGeneral) } - price, ok := new(big.Float).SetString(val) - if !ok { - return types.NewPriceResponseWithErr( - ids, - providertypes.NewErrorWithCode(fmt.Errorf("failed to convert %q to float", val), providertypes.ErrorFailedToDecode), - ) - } - if err := validatePrice(price); err != nil { - return types.NewPriceResponseWithErr( - ids, - providertypes.NewErrorWithCode(err, providertypes.ErrorInvalidResponse), - ) + var tokenData *TokenData + for _, token := range result.Tokens { + if token.TokenID == tokenID { + tokenData = &token + break + } } - // set price to priceAdjustmentMax if its 1.00 - if big.NewFloat(1.00).Cmp(price) == 0 { - price = new(big.Float).SetFloat64(priceAdjustmentMax) + if tokenData == nil { + return priceResponseError(ids, fmt.Errorf("token ID %s not found in response", tokenID), providertypes.ErrorInvalidResponse) } + + price := new(big.Float).SetFloat64(tokenData.Price) + // switch price to priceAdjustmentMin if its 0.00. if big.NewFloat(0.00).Cmp(price) == 0 { price = new(big.Float).SetFloat64(priceAdjustmentMin) @@ -187,18 +157,17 @@ func (h APIHandler) ParseResponse(ids []types.ProviderTicker, response *http.Res return types.NewPriceResponse(resolved, nil) } -// validatePrice ensures the price is between [1.00 and 0.00]. -func validatePrice(price *big.Float) error { - if sign := price.Sign(); sign == -1 { - return fmt.Errorf("price must be greater than 0.00") - } +func priceResponseError(ids []types.ProviderTicker, err error, code providertypes.ErrorCode) providertypes.GetResponse[types.ProviderTicker, *big.Float] { + return types.NewPriceResponseWithErr( + ids, + providertypes.NewErrorWithCode(err, code), + ) +} - maxPriceFloat := 1.00 - maxPrice := big.NewFloat(maxPriceFloat) - diff := new(big.Float).Sub(maxPrice, price) - if diff.Sign() == -1 { - return fmt.Errorf("price exceeded %.2f", maxPriceFloat) +func getMarketAndTokenFromTicker(t types.ProviderTicker) (marketID string, tokenID string, err error) { + split := strings.Split(t.GetOffChainTicker(), "/") + if len(split) != 2 { + return "", "", fmt.Errorf("expected ticker format market_id/token_id, got: %s", t.GetOffChainTicker()) } - - return nil + return split[0], split[1], nil } diff --git a/providers/apis/polymarket/api_handler_test.go b/providers/apis/polymarket/api_handler_test.go index 1c72e173a..eb06c7b4f 100644 --- a/providers/apis/polymarket/api_handler_test.go +++ b/providers/apis/polymarket/api_handler_test.go @@ -6,19 +6,16 @@ import ( "io" "math/big" "net/http" - "net/url" "testing" - "time" "github.com/stretchr/testify/require" "github.com/skip-mev/slinky/oracle/config" "github.com/skip-mev/slinky/oracle/types" - providertypes "github.com/skip-mev/slinky/providers/types" ) var candidateWinsElectionToken = types.DefaultProviderTicker{ - OffChainTicker: "95128817762909535143571435260705470642391662537976312011260538371392879420759", + OffChainTicker: "0xc6485bb7ea46d7bb89beb9c91e7572ecfc72a6273789496f78bc5e989e4d1638/95128817762909535143571435260705470642391662537976312011260538371392879420759", } func TestNewAPIHandler(t *testing.T) { @@ -62,24 +59,6 @@ func TestNewAPIHandler(t *testing.T) { expectError: true, errorMsg: "api config for polymarket_api is not enabled", }, - { - name: "Invalid host", - modifyConfig: func(cfg config.APIConfig) config.APIConfig { - cfg.Endpoints[0].URL = "https://foobar.com/price" - return cfg - }, - expectError: true, - errorMsg: "invalid polymarket URL: expected", - }, - { - name: "Invalid endpoint path", - modifyConfig: func(cfg config.APIConfig) config.APIConfig { - cfg.Endpoints[0].URL = "https://" + host + "/foo" - return cfg - }, - expectError: true, - errorMsg: `invalid polymarket endpoint url path /foo`, - }, } for _, tt := range tests { @@ -89,7 +68,6 @@ func TestNewAPIHandler(t *testing.T) { modifiedConfig := tt.modifyConfig(cfg) _, err := NewAPIHandler(modifiedConfig) if tt.expectError { - fmt.Println(err.Error()) require.Error(t, err) require.ErrorContains(t, err, tt.errorMsg) } else { @@ -124,7 +102,7 @@ func TestCreateURL(t *testing.T) { pts: []types.ProviderTicker{ candidateWinsElectionToken, }, - expectedURL: fmt.Sprintf(URL, candidateWinsElectionToken), + expectedURL: fmt.Sprintf(URL, "0xc6485bb7ea46d7bb89beb9c91e7572ecfc72a6273789496f78bc5e989e4d1638"), }, } h, err := NewAPIHandler(DefaultAPIConfig) @@ -143,139 +121,74 @@ func TestCreateURL(t *testing.T) { } func TestParseResponse(t *testing.T) { - id := candidateWinsElectionToken handler, err := NewAPIHandler(DefaultAPIConfig) require.NoError(t, err) - testCases := []struct { - name string - path string - noError bool - ids []types.ProviderTicker - responseBody string - expectedResponse types.PriceResponse + testCases := map[string]struct { + data string + ticker []types.ProviderTicker + expectedErr string + expectedPrice *big.Float }{ - { - name: "happy case from midpoint", - path: "/midpoint", - ids: []types.ProviderTicker{candidateWinsElectionToken}, - noError: true, - responseBody: `{ "mid": "0.45" }`, - expectedResponse: types.NewPriceResponse( - types.ResolvedPrices{ - id: types.NewPriceResult(big.NewFloat(0.45), time.Now().UTC()), - }, - nil, - ), - }, - { - name: "happy case from price", - path: "/price", - ids: []types.ProviderTicker{candidateWinsElectionToken}, - noError: true, - responseBody: `{ "price": "0.45" }`, - expectedResponse: types.NewPriceResponse( - types.ResolvedPrices{ - id: types.NewPriceResult(big.NewFloat(0.45), time.Now().UTC()), - }, - nil, - ), - }, - { - name: "bad path", - path: "/foobar", - ids: []types.ProviderTicker{candidateWinsElectionToken}, - responseBody: `{"mid": "234.3"}"`, - expectedResponse: types.NewPriceResponseWithErr( - []types.ProviderTicker{candidateWinsElectionToken}, - providertypes.NewErrorWithCode(fmt.Errorf("unknown request path %q", "/foobar"), providertypes.ErrorFailedToDecode), - ), - }, - { - name: "1.00 should resolve to 0.999...", - path: "/midpoint", - ids: []types.ProviderTicker{candidateWinsElectionToken}, - noError: true, - responseBody: `{ "mid": "1.00" }`, - expectedResponse: types.NewPriceResponse( - types.ResolvedPrices{ - id: types.NewPriceResult(big.NewFloat(priceAdjustmentMax), time.Now().UTC()), - }, - nil, - ), - }, - { - name: "0.00 should resolve to 0.00001", - path: "/midpoint", - ids: []types.ProviderTicker{candidateWinsElectionToken}, - noError: true, - responseBody: `{ "mid": "0.00" }`, - expectedResponse: types.NewPriceResponse( - types.ResolvedPrices{ - id: types.NewPriceResult(big.NewFloat(priceAdjustmentMin), time.Now().UTC()), - }, - nil, - ), - }, - { - name: "too many IDs", - path: "/midpoint", - ids: []types.ProviderTicker{candidateWinsElectionToken, candidateWinsElectionToken}, - responseBody: ``, - expectedResponse: types.NewPriceResponseWithErr( - []types.ProviderTicker{candidateWinsElectionToken, candidateWinsElectionToken}, - providertypes.NewErrorWithCode( - fmt.Errorf("expected 1 ticker, got 2"), - providertypes.ErrorInvalidResponse, - ), - ), - }, - { - name: "invalid JSON", - path: "/midpoint", - ids: []types.ProviderTicker{candidateWinsElectionToken}, - responseBody: `{"mid": "0fa3adk"}"`, - expectedResponse: types.NewPriceResponseWithErr( - []types.ProviderTicker{candidateWinsElectionToken}, - providertypes.NewErrorWithCode(fmt.Errorf("failed to convert %q to float", "0fa3adk"), providertypes.ErrorFailedToDecode), - ), - }, - { - name: "bad price - max", - path: "/midpoint", - ids: []types.ProviderTicker{candidateWinsElectionToken}, - responseBody: `{"mid": "1.0001"}"`, - expectedResponse: types.NewPriceResponseWithErr( - []types.ProviderTicker{candidateWinsElectionToken}, - providertypes.NewErrorWithCode(fmt.Errorf("price exceeded 1.00"), providertypes.ErrorInvalidResponse), - ), - }, - { - name: "bad price - negative", - path: "/midpoint", - ids: []types.ProviderTicker{candidateWinsElectionToken}, - responseBody: `{"mid": "-0.12"}"`, - expectedResponse: types.NewPriceResponseWithErr( - []types.ProviderTicker{candidateWinsElectionToken}, - providertypes.NewErrorWithCode(fmt.Errorf("price must be greater than 0.00"), providertypes.ErrorInvalidResponse), - ), + "happy path": { + data: `{"tokens": [{ + "token_id": "95128817762909535143571435260705470642391662537976312011260538371392879420759", + "outcome": "Yes", + "price": 1}]}]}`, + ticker: []types.ProviderTicker{candidateWinsElectionToken}, + expectedPrice: big.NewFloat(1.00), + }, + "zero resolution": { + data: `{"tokens": [{ + "token_id": "95128817762909535143571435260705470642391662537976312011260538371392879420759", + "outcome": "Yes", + "price": 0}]}]}`, + ticker: []types.ProviderTicker{candidateWinsElectionToken}, + expectedPrice: big.NewFloat(priceAdjustmentMin), + }, + "other values work": { + data: `{"tokens": [{ + "token_id": "95128817762909535143571435260705470642391662537976312011260538371392879420759", + "outcome": "Yes", + "price": 0.325}]}]}`, + ticker: []types.ProviderTicker{candidateWinsElectionToken}, + expectedPrice: big.NewFloat(0.325), + }, + "token not in response": { + data: `{"tokens": [{ + "token_id": "35128817762909535143571435260705470642391662537976312011260538371392879420759", + "outcome": "Yes", + "price": 0.325}]}]}`, + ticker: []types.ProviderTicker{candidateWinsElectionToken}, + expectedErr: "token ID 95128817762909535143571435260705470642391662537976312011260538371392879420759 not found in response", + }, + "bad response data": { + data: `{"tokens": [{ + "token_id":z, + "outcome": "Yes", + "price": 0.325}]}]}`, + ticker: []types.ProviderTicker{candidateWinsElectionToken}, + expectedErr: "failed to decode market response", + }, + "too many tickers": { + data: `{"tokens": []}`, + ticker: []types.ProviderTicker{candidateWinsElectionToken, candidateWinsElectionToken}, + expectedErr: "expected 1 ticker, got 2", }, } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { httpInput := &http.Response{ - Body: io.NopCloser(bytes.NewBufferString(tc.responseBody)), - Request: &http.Request{URL: &url.URL{Path: tc.path}}, + Body: io.NopCloser(bytes.NewBufferString(tc.data)), } - res := handler.ParseResponse(tc.ids, httpInput) - - // timestamps are off, repair here. - if tc.noError { - val := tc.expectedResponse.Resolved[tc.ids[0]] - val.Timestamp = res.Resolved[tc.ids[0]].Timestamp - tc.expectedResponse.Resolved[tc.ids[0]] = val + res := handler.ParseResponse(tc.ticker, httpInput) + if tc.expectedErr != "" { + require.Contains(t, res.UnResolved[tc.ticker[0]].Error(), tc.expectedErr) + } else { + gotPrice := res.Resolved[tc.ticker[0]].Value + require.Equal(t, gotPrice.Cmp(tc.expectedPrice), 0, "expected %v, got %v", tc.expectedPrice, gotPrice) + require.Equal(t, len(res.Resolved), len(tc.ticker)) } - require.Equal(t, tc.expectedResponse.String(), res.String()) }) } }