Skip to content

Commit

Permalink
New oracle plugin forex_wise (#46)
Browse files Browse the repository at this point in the history
* Oracle Plugin Forex Wise

* apply comments

* apply comments
  • Loading branch information
metatarz authored Feb 13, 2025
1 parent 84e554b commit 9405279
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 0 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,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:
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down
4 changes: 4 additions & 0 deletions config/oracle_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,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.
Expand Down
5 changes: 5 additions & 0 deletions plugins/common/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down Expand Up @@ -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
Expand Down
209 changes: 209 additions & 0 deletions plugins/forex_wise/forex_wise.go
Original file line number Diff line number Diff line change
@@ -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)
}
19 changes: 19 additions & 0 deletions plugins/forex_wise/forex_wise_test.go
Original file line number Diff line number Diff line change
@@ -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))
}

0 comments on commit 9405279

Please sign in to comment.