diff --git a/Makefile b/Makefile index 3d8acab..1f4af3a 100644 --- a/Makefile +++ b/Makefile @@ -108,6 +108,7 @@ forex-plugins: go build -o $(PLUGIN_DIR)/forex_currencylayer $(PLUGIN_SRC_DIR)/forex_currencylayer/forex_currencylayer.go go build -o $(PLUGIN_DIR)/forex_exchangerate $(PLUGIN_SRC_DIR)/forex_exchangerate/forex_exchangerate.go go build -o $(PLUGIN_DIR)/forex_openexchange $(PLUGIN_SRC_DIR)/forex_openexchange/forex_openexchange.go + go build -o $(PLUGIN_DIR)/forex_wise $(PLUGIN_SRC_DIR)/forex_wise/forex_wise.go chmod +x $(PLUGIN_DIR)/* cex-plugins: diff --git a/README.md b/README.md index d0eb490..012177f 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,10 @@ confidenceStrategy: 0 # 0: linear, 1: fixed # - name: forex_exchangerate # required, it is the plugin file name in the plugin directory. # key: 111f04e4775bb86c20296530 # required, visit https://www.exchangerate-api.com to get your key, and replace it. # refresh: 3600 # optional, buffered data within 3600s, recommended for API rate limited data source. + +# - name: forex_wise # required, it is the plugin file name in the plugin directory. +# key: 1234 # required, visit https://www.wise.com to get your key, and replace it. +# refresh: 30 # optional, buffered data within 30s, recommended for API rate limited data source. # Un-comment below lines to config the RPC endpoint of a Piccadilly Network Full Node for your AMM plugin which sources ATN & NTN market data from an on-chain AMM. # - name: crypto_uniswap # scheme: "wss" # Available values are: "http", "https", "ws" or "wss", default value is "wss". diff --git a/config/oracle_config.yml b/config/oracle_config.yml index 68d1800..ea211f6 100644 --- a/config/oracle_config.yml +++ b/config/oracle_config.yml @@ -86,6 +86,10 @@ confidenceStrategy: 0 # 0: linear, 1: fixed # - name: forex_exchangerate # required, it is the plugin file name in the plugin directory. # key: 111f04e4775bb86c20296530 # required, visit https://www.exchangerate-api.com to get your key, and replace it. # refresh: 3600 # optional, buffered data within 3600s, recommended for API rate limited data source. + +# - name: forex_wise # required, it is the plugin file name in the plugin directory. +# key: 1234 # required, visit https://www.wise.com to get your key, and replace it. +# refresh: 30 # optional, buffered data within 30s, recommended for API rate limited data source. # Un-comment below lines to config the RPC endpoint of a Piccadilly Network Full Node for your AMM plugin which sources ATN & NTN market data from an on-chain AMM. # - name: crypto_uniswap # scheme: "wss" # Only websocket please, available values are: "ws" or "wss", default value is "wss" for uniswap plugins. diff --git a/plugins/common/client.go b/plugins/common/client.go index c05994a..10e4c84 100644 --- a/plugins/common/client.go +++ b/plugins/common/client.go @@ -8,6 +8,7 @@ import ( type Connection interface { Request(scheme string, endpoint *url.URL) (*http.Response, error) + Do(req *http.Request) (*http.Response, error) Close() } @@ -45,6 +46,10 @@ func (conn *connection) Request(scheme string, endpoint *url.URL) (*http.Respons return conn.client.Get(targetUrl) } +func (conn *connection) Do(req *http.Request) (*http.Response, error) { + return conn.client.Do(req) +} + type Client struct { Conn Connection ApiKey string diff --git a/plugins/forex_wise/forex_wise.go b/plugins/forex_wise/forex_wise.go new file mode 100644 index 0000000..1a145df --- /dev/null +++ b/plugins/forex_wise/forex_wise.go @@ -0,0 +1,209 @@ +package main + +import ( + "autonity-oracle/config" + "autonity-oracle/plugins/common" + "autonity-oracle/types" + "encoding/json" + "fmt" + "github.com/hashicorp/go-hclog" + "github.com/shopspring/decimal" + "io" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +const ( + version = "v0.2.0" + apiPath = "/v1/rates" +) + +var defaultConfig = config.PluginConfig{ + Name: "forex_wise", + Key: "0x123", + Scheme: "https", + Endpoint: "api.transferwise.com", + Timeout: 10, // Timeout in seconds + DataUpdateInterval: 30, +} + +type Price struct { + Symbol string `json:"symbol"` + Price string `json:"price"` +} + +type WiseClient struct { + conf *config.PluginConfig + client *common.Client + logger hclog.Logger +} + +type WRResult struct { + Rate decimal.Decimal `json:"rate"` + Source string `json:"source"` + Target string `json:"target"` + Time string `json:"time"` +} + +func (wc *WiseClient) buildURL(source, target string) *url.URL { + endpoint := &url.URL{ + Scheme: "https", + Host: wc.conf.Endpoint, + Path: "/v1/rates", + } + + query := endpoint.Query() + query.Set("source", source) + query.Set("target", target) + + endpoint.RawQuery = query.Encode() + + return endpoint +} + +func NewWiseClient(conf *config.PluginConfig) *WiseClient { + client := common.NewClient(conf.Key, time.Second*time.Duration(conf.Timeout), conf.Endpoint) + logger := hclog.New(&hclog.LoggerOptions{ + Name: conf.Name, + Level: hclog.Info, + Output: os.Stdout, + }) + + return &WiseClient{ + conf: conf, + client: client, + logger: logger, + } +} + +func (wc *WiseClient) KeyRequired() bool { + return true +} + +// FetchPrice fetches forex prices for given symbols. +func (wc *WiseClient) FetchPrice(symbols []string) (common.Prices, error) { + var prices common.Prices + + for _, symbol := range symbols { + parts := strings.Split(symbol, "-") + if len(parts) != 2 { + wc.logger.Warn("Invalid symbol format, expected SOURCE-TARGET", "symbol", symbol) + continue + } + + source := parts[0] + target := parts[1] + + reqURL := wc.buildURL(target, source) + + req, err := http.NewRequest("GET", reqURL.String(), nil) + if err != nil { + wc.logger.Error("Failed to create request", "error", err) + continue + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", wc.conf.Key)) + resp, err := wc.client.Conn.Do(req) + if err != nil { + wc.logger.Error("Request to Wise API failed", "error", err) + continue + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + wc.logger.Error("API response returned non-200 status code", "status", resp.Status, "symbol", symbol) + continue + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + wc.logger.Error("Failed to read response body", "error", err) + continue + } + + var result []WRResult + if err := json.Unmarshal(body, &result); err != nil { + wc.logger.Error("Failed to parse JSON response", "error", err) + continue + } + + for i := range result { + + p, err := wc.symbolsToPrice(symbol, &result[i]) + if err != nil { + wc.logger.Error("symbol to price", "error", err.Error()) + continue + } + prices = append(prices, p) + + } + } + + return prices, nil +} + +func (wl *WiseClient) symbolsToPrice(s string, res *WRResult) (common.Price, error) { + var price common.Price + sep := common.ResolveSeparator(s) + codes := strings.Split(s, sep) + if len(codes) != 2 { + return price, fmt.Errorf("invalid symbol %s", s) + } + + from := codes[0] + to := codes[1] + if to != res.Source { + return price, fmt.Errorf("wrong base %s", to) + } + + price.Symbol = s + price.Volume = types.DefaultVolume.String() + switch from { + case "EUR": + price.Price = decimal.NewFromInt(1).Div(res.Rate).String() + case "JPY": + price.Price = decimal.NewFromInt(1).Div(res.Rate).String() + case "GBP": + price.Price = decimal.NewFromInt(1).Div(res.Rate).String() + case "AUD": + price.Price = decimal.NewFromInt(1).Div(res.Rate).String() + case "CAD": + price.Price = decimal.NewFromInt(1).Div(res.Rate).String() + case "SEK": + price.Price = decimal.NewFromInt(1).Div(res.Rate).String() + default: + return price, fmt.Errorf("unknown symbol %s", from) + } + return price, nil +} + +// AvailableSymbols returns the supported symbols. +func (wc *WiseClient) AvailableSymbols() ([]string, error) { + return []string{ + "EUR-USD", + "JPY-USD", + "GBP-USD", + "AUD-USD", + "CAD-USD", + "SEK-USD", + }, nil +} + +func (wc *WiseClient) Close() { + wc.client.Conn.Close() +} + +func main() { + // Plugin configuration + + conf := common.ResolveConf(os.Args[0], &defaultConfig) + + adapter := common.NewPlugin(conf, NewWiseClient(conf), version, types.SrcCEX, nil) + defer adapter.Close() + + common.PluginServe(adapter) +} diff --git a/plugins/forex_wise/forex_wise_test.go b/plugins/forex_wise/forex_wise_test.go new file mode 100644 index 0000000..d882389 --- /dev/null +++ b/plugins/forex_wise/forex_wise_test.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + "github.com/stretchr/testify/require" + "testing" +) + +func TestNewWiseClient(t *testing.T) { + // this key is only used by testing + defaultConfig.Key = "0x123" + client := NewWiseClient(&defaultConfig) + defer client.Close() + prices, _ := client.FetchPrice([]string{"EUR-USD", "JPY-USD", "GBP-USD", "AUD-USD", "CAD-USD", "SEK-USD"}) + fmt.Print(prices) + //require.NoError(t, err) + //require.Equal(t, 6, len(prices)) + require.Equal(t, 0, len(prices)) +}