mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
dexd is now the ONLY command in the repo. One binary, two run modes:
dexd run as a luxd rpcchainvm PLUGIN. luxd execs the plugin from
dexd plugin its PluginDir with NO positional args (engine address arrives
via the runtime.EngineAddressKey env var) and probes the
version with `dexd version`. No-subcommand == plugin is
REQUIRED to preserve that contract — byte-for-byte the old
cmd/dchain behavior (rpc.Serve over the plugin transport).
dexd run run the STANDALONE D-Chain venue: open the on-disk zapdb,
dexd standalone serve the frozen dex_* surface over ZAP, and drive the
consensus sealer loop (WaitForEvent->BuildBlock->Verify->
Accept). -addr/-db flags preserved from the old cmd/dvenue.
Relocated logic (engine code NOT duplicated — pkg/dchain + pkg/lx untouched):
cmd/dchain/{main,runner} -> internal/plugin (plugin.Run)
cmd/dvenue/{main,engine_*} -> internal/standalone (standalone.Run + engineName)
cmd/dexd/main.go = thin run-mode dispatcher
Removed cmd dirs (12): dchain, dvenue, demo, dex-api-server, dex-server,
dexfillproof, dexseed, fix-gateway, gateway, monitor, persistent-server,
price-feed. Removed committed build artifacts: gateway, dex-server-linux.
Build/ship ONLY dexd:
- Dockerfile: canonical CPU image (CGO=0, ghcr.io/luxfi/dex) running
`dexd run`; no GPU/MLX required.
- Dockerfile.dvenue: GPU operator bundle repointed cmd/dvenue -> cmd/dexd.
- Deleted Dockerfile.{gateway,devnet,minimal,dvenue-cpu} and
docker/{Dockerfile,Dockerfile.cuda,backend/Dockerfile,compose-simple.yml}.
- Makefile rewritten to build/run/ship dexd only.
- docker.yml: dropped the dex-gateway image; dex-venue-image.yml: dvenue->dexd.
- Deleted legacy scripts that built removed/long-gone cmds; fixed deploy.sh +
dev-setup.sh; .gitignore ignores root build binaries.
Also included (coupled, environment-applied, settled): the in-progress
clob -> dex/orderbook terminology refactor across pkg/* + sdk/* + tests/*
(identifier + file renames, e.g. RegisterCLOB->RegisterDEX,
source_clob.go->source_orderbook.go). The relocated standalone venue calls the
renamed API, so it lands in the same consistent snapshot.
Verified (GOWORK=off CGO_ENABLED=0): `go build ./cmd/dexd`, `go build ./...`,
`go vet ./...` all exit 0; `go test ./pkg/dchain` (renamed RegisterDEX socket
path) green. GPU/cgo path not compiled locally (no CUDA); its rename mirrors
the verified nocgo twins and preserves the C ABI symbol names.
609 lines
15 KiB
Go
609 lines
15 KiB
Go
// Package adapters provides venue adapter implementations.
|
|
package adapters
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/luxfi/trading"
|
|
"github.com/shopspring/decimal"
|
|
)
|
|
|
|
// CcxtAdapter implements VenueAdapter for CCXT exchanges via HTTP gateway.
|
|
// CCXT runs as a separate service and this adapter communicates via REST.
|
|
type CcxtAdapter struct {
|
|
trading.BaseAdapter
|
|
config *trading.CcxtConfig
|
|
client *http.Client
|
|
name string
|
|
}
|
|
|
|
// NewCcxtAdapter creates a new CCXT adapter.
|
|
func NewCcxtAdapter(name string, config *trading.CcxtConfig) *CcxtAdapter {
|
|
return &CcxtAdapter{
|
|
name: name,
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
// Name returns the venue name.
|
|
func (a *CcxtAdapter) Name() string {
|
|
return a.name
|
|
}
|
|
|
|
// VenueType returns the venue type.
|
|
func (a *CcxtAdapter) VenueType() trading.VenueType {
|
|
return trading.VenueTypeCCXT
|
|
}
|
|
|
|
// Capabilities returns venue capabilities.
|
|
func (a *CcxtAdapter) Capabilities() trading.VenueCapabilities {
|
|
caps := trading.OrderBookCapabilities()
|
|
caps.BatchOrders = false // CCXT doesn't have unified batch
|
|
return caps
|
|
}
|
|
|
|
// Connect establishes connection to the CCXT gateway.
|
|
func (a *CcxtAdapter) Connect(ctx context.Context) error {
|
|
a.client = &http.Client{Timeout: 30 * time.Second}
|
|
|
|
// Load markets to verify connection
|
|
start := time.Now()
|
|
_, err := a.doRequest(ctx, http.MethodPost, "/load_markets", map[string]interface{}{
|
|
"exchange": a.config.ExchangeID,
|
|
"apiKey": a.config.APIKey,
|
|
"secret": a.config.APISecret,
|
|
"password": a.config.Password,
|
|
"sandbox": a.config.Sandbox,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect to CCXT gateway: %w", err)
|
|
}
|
|
|
|
a.SetLatency(time.Since(start).Milliseconds())
|
|
a.SetConnected(true)
|
|
return nil
|
|
}
|
|
|
|
// Disconnect closes the connection.
|
|
func (a *CcxtAdapter) Disconnect(ctx context.Context) error {
|
|
a.client = nil
|
|
a.SetConnected(false)
|
|
return nil
|
|
}
|
|
|
|
func (a *CcxtAdapter) doRequest(ctx context.Context, method, path string, body interface{}) (map[string]interface{}, error) {
|
|
if a.client == nil {
|
|
return nil, trading.ErrNotConnected
|
|
}
|
|
|
|
url := a.config.BaseURL + path
|
|
|
|
var reqBody io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reqBody = bytes.NewReader(data)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
start := time.Now()
|
|
resp, err := a.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
a.SetLatency(time.Since(start).Milliseconds())
|
|
|
|
if resp.StatusCode >= 400 {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, trading.NewTradingError("HTTP_ERROR", fmt.Sprintf("status %d: %s", resp.StatusCode, string(body)))
|
|
}
|
|
|
|
var result map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (a *CcxtAdapter) doRequestArray(ctx context.Context, method, path string, body interface{}) ([]map[string]interface{}, error) {
|
|
if a.client == nil {
|
|
return nil, trading.ErrNotConnected
|
|
}
|
|
|
|
url := a.config.BaseURL + path
|
|
|
|
var reqBody io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reqBody = bytes.NewReader(data)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
start := time.Now()
|
|
resp, err := a.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
a.SetLatency(time.Since(start).Milliseconds())
|
|
|
|
if resp.StatusCode >= 400 {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, trading.NewTradingError("HTTP_ERROR", fmt.Sprintf("status %d: %s", resp.StatusCode, string(body)))
|
|
}
|
|
|
|
var result []map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (a *CcxtAdapter) exchangeParams() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"exchange": a.config.ExchangeID,
|
|
"apiKey": a.config.APIKey,
|
|
"secret": a.config.APISecret,
|
|
"password": a.config.Password,
|
|
"sandbox": a.config.Sandbox,
|
|
}
|
|
}
|
|
|
|
// GetMarkets returns available markets.
|
|
func (a *CcxtAdapter) GetMarkets(ctx context.Context) ([]trading.MarketInfo, error) {
|
|
data, err := a.doRequest(ctx, http.MethodPost, "/markets", a.exchangeParams())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var markets []trading.MarketInfo
|
|
for symbol, mkt := range data {
|
|
m, ok := mkt.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
precision := getMap(m, "precision")
|
|
limits := getMap(m, "limits")
|
|
amountLimits := getMap(limits, "amount")
|
|
costLimits := getMap(limits, "cost")
|
|
|
|
minQty := decimal.NewFromFloat(getFloat(amountLimits, "min"))
|
|
maxQty := decimal.NewFromFloat(getFloat(amountLimits, "max"))
|
|
minNotional := decimal.NewFromFloat(getFloat(costLimits, "min"))
|
|
|
|
var maxQtyPtr, minNotPtr *decimal.Decimal
|
|
if !maxQty.IsZero() {
|
|
maxQtyPtr = &maxQty
|
|
}
|
|
if !minNotional.IsZero() {
|
|
minNotPtr = &minNotional
|
|
}
|
|
|
|
markets = append(markets, trading.MarketInfo{
|
|
Symbol: symbol,
|
|
Base: getString(m, "base"),
|
|
Quote: getString(m, "quote"),
|
|
PricePrecision: getInt(precision, "price"),
|
|
QuantityPrecision: getInt(precision, "amount"),
|
|
MinQuantity: minQty,
|
|
MaxQuantity: maxQtyPtr,
|
|
MinNotional: minNotPtr,
|
|
TickSize: decimal.NewFromFloat(0.00000001),
|
|
LotSize: decimal.NewFromFloat(0.00000001),
|
|
})
|
|
}
|
|
|
|
return markets, nil
|
|
}
|
|
|
|
// GetTicker returns ticker for a symbol.
|
|
func (a *CcxtAdapter) GetTicker(ctx context.Context, symbol string) (trading.Ticker, error) {
|
|
params := a.exchangeParams()
|
|
params["symbol"] = symbol
|
|
|
|
data, err := a.doRequest(ctx, http.MethodPost, "/fetch_ticker", params)
|
|
if err != nil {
|
|
return trading.Ticker{}, err
|
|
}
|
|
|
|
ticker := trading.Ticker{
|
|
Symbol: symbol,
|
|
Venue: a.name,
|
|
Timestamp: time.UnixMilli(int64(getFloat(data, "timestamp"))),
|
|
}
|
|
|
|
if bid := getFloat(data, "bid"); bid > 0 {
|
|
d := decimal.NewFromFloat(bid)
|
|
ticker.Bid = &d
|
|
}
|
|
if ask := getFloat(data, "ask"); ask > 0 {
|
|
d := decimal.NewFromFloat(ask)
|
|
ticker.Ask = &d
|
|
}
|
|
if last := getFloat(data, "last"); last > 0 {
|
|
d := decimal.NewFromFloat(last)
|
|
ticker.Last = &d
|
|
}
|
|
if vol := getFloat(data, "baseVolume"); vol > 0 {
|
|
d := decimal.NewFromFloat(vol)
|
|
ticker.Volume24H = &d
|
|
}
|
|
if high := getFloat(data, "high"); high > 0 {
|
|
d := decimal.NewFromFloat(high)
|
|
ticker.High24H = &d
|
|
}
|
|
if low := getFloat(data, "low"); low > 0 {
|
|
d := decimal.NewFromFloat(low)
|
|
ticker.Low24H = &d
|
|
}
|
|
if pct := getFloat(data, "percentage"); pct != 0 {
|
|
d := decimal.NewFromFloat(pct)
|
|
ticker.Change24H = &d
|
|
}
|
|
|
|
return ticker, nil
|
|
}
|
|
|
|
// GetOrderbook returns the orderbook for a symbol.
|
|
func (a *CcxtAdapter) GetOrderbook(ctx context.Context, symbol string, depth int) (*trading.Orderbook, error) {
|
|
params := a.exchangeParams()
|
|
params["symbol"] = symbol
|
|
if depth > 0 {
|
|
params["limit"] = depth
|
|
}
|
|
|
|
data, err := a.doRequest(ctx, http.MethodPost, "/fetch_order_book", params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
book := trading.NewOrderbook(symbol, a.name)
|
|
|
|
if bids, ok := data["bids"].([]interface{}); ok {
|
|
for _, b := range bids {
|
|
if bid, ok := b.([]interface{}); ok && len(bid) >= 2 {
|
|
price := decimal.NewFromFloat(toFloat(bid[0]))
|
|
qty := decimal.NewFromFloat(toFloat(bid[1]))
|
|
book.AddBid(price, qty)
|
|
}
|
|
}
|
|
}
|
|
|
|
if asks, ok := data["asks"].([]interface{}); ok {
|
|
for _, a := range asks {
|
|
if ask, ok := a.([]interface{}); ok && len(ask) >= 2 {
|
|
price := decimal.NewFromFloat(toFloat(ask[0]))
|
|
qty := decimal.NewFromFloat(toFloat(ask[1]))
|
|
book.AddAsk(price, qty)
|
|
}
|
|
}
|
|
}
|
|
|
|
book.Sort()
|
|
return book, nil
|
|
}
|
|
|
|
// GetTrades returns recent trades.
|
|
func (a *CcxtAdapter) GetTrades(ctx context.Context, symbol string, limit int) ([]trading.Trade, error) {
|
|
params := a.exchangeParams()
|
|
params["symbol"] = symbol
|
|
if limit > 0 {
|
|
params["limit"] = limit
|
|
}
|
|
|
|
data, err := a.doRequestArray(ctx, http.MethodPost, "/fetch_trades", params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
trades := make([]trading.Trade, len(data))
|
|
for i, t := range data {
|
|
side := trading.SideBuy
|
|
if getString(t, "side") == "sell" {
|
|
side = trading.SideSell
|
|
}
|
|
|
|
fee := trading.Fee{}
|
|
if feeData := getMap(t, "fee"); feeData != nil {
|
|
fee.Asset = getString(feeData, "currency")
|
|
fee.Amount = decimal.NewFromFloat(getFloat(feeData, "cost"))
|
|
}
|
|
|
|
trades[i] = trading.Trade{
|
|
TradeID: getString(t, "id"),
|
|
OrderID: getString(t, "order"),
|
|
Symbol: symbol,
|
|
Venue: a.name,
|
|
Side: side,
|
|
Price: decimal.NewFromFloat(getFloat(t, "price")),
|
|
Quantity: decimal.NewFromFloat(getFloat(t, "amount")),
|
|
Fee: fee,
|
|
Timestamp: time.UnixMilli(int64(getFloat(t, "timestamp"))),
|
|
IsMaker: getString(t, "takerOrMaker") == "maker",
|
|
}
|
|
}
|
|
|
|
return trades, nil
|
|
}
|
|
|
|
// GetBalances returns all balances.
|
|
func (a *CcxtAdapter) GetBalances(ctx context.Context) ([]trading.Balance, error) {
|
|
data, err := a.doRequest(ctx, http.MethodPost, "/fetch_balance", a.exchangeParams())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var balances []trading.Balance
|
|
|
|
total := getMap(data, "total")
|
|
free := getMap(data, "free")
|
|
used := getMap(data, "used")
|
|
|
|
for asset, amount := range total {
|
|
amt := toFloat(amount)
|
|
if amt > 0 {
|
|
balances = append(balances, trading.Balance{
|
|
Asset: asset,
|
|
Venue: a.name,
|
|
Free: decimal.NewFromFloat(getFloat(free, asset)),
|
|
Locked: decimal.NewFromFloat(getFloat(used, asset)),
|
|
})
|
|
}
|
|
}
|
|
|
|
return balances, nil
|
|
}
|
|
|
|
// GetBalance returns balance for a specific asset.
|
|
func (a *CcxtAdapter) GetBalance(ctx context.Context, asset string) (trading.Balance, error) {
|
|
balances, err := a.GetBalances(ctx)
|
|
if err != nil {
|
|
return trading.Balance{}, err
|
|
}
|
|
|
|
for _, b := range balances {
|
|
if b.Asset == asset {
|
|
return b, nil
|
|
}
|
|
}
|
|
|
|
return trading.Balance{
|
|
Asset: asset,
|
|
Venue: a.name,
|
|
Free: decimal.Zero,
|
|
Locked: decimal.Zero,
|
|
}, nil
|
|
}
|
|
|
|
// GetOpenOrders returns open orders.
|
|
func (a *CcxtAdapter) GetOpenOrders(ctx context.Context, symbol string) ([]trading.Order, error) {
|
|
params := a.exchangeParams()
|
|
if symbol != "" {
|
|
params["symbol"] = symbol
|
|
}
|
|
|
|
data, err := a.doRequestArray(ctx, http.MethodPost, "/fetch_open_orders", params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
orders := make([]trading.Order, len(data))
|
|
for i, o := range data {
|
|
orders[i] = a.convertOrder(o)
|
|
}
|
|
|
|
return orders, nil
|
|
}
|
|
|
|
// PlaceOrder places a new order.
|
|
func (a *CcxtAdapter) PlaceOrder(ctx context.Context, request trading.OrderRequest) (trading.Order, error) {
|
|
params := a.exchangeParams()
|
|
params["symbol"] = request.Symbol
|
|
params["type"] = "limit"
|
|
if request.OrderType == trading.OrderTypeMarket {
|
|
params["type"] = "market"
|
|
}
|
|
params["side"] = "buy"
|
|
if request.Side == trading.SideSell {
|
|
params["side"] = "sell"
|
|
}
|
|
params["amount"] = request.Quantity.InexactFloat64()
|
|
if request.Price != nil {
|
|
params["price"] = request.Price.InexactFloat64()
|
|
}
|
|
params["params"] = map[string]interface{}{
|
|
"clientOrderId": request.ClientOrderID,
|
|
}
|
|
|
|
data, err := a.doRequest(ctx, http.MethodPost, "/create_order", params)
|
|
if err != nil {
|
|
return trading.Order{}, err
|
|
}
|
|
|
|
return a.convertOrder(data), nil
|
|
}
|
|
|
|
// CancelOrder cancels an order.
|
|
func (a *CcxtAdapter) CancelOrder(ctx context.Context, orderID, symbol string) (trading.Order, error) {
|
|
params := a.exchangeParams()
|
|
params["id"] = orderID
|
|
params["symbol"] = symbol
|
|
|
|
data, err := a.doRequest(ctx, http.MethodPost, "/cancel_order", params)
|
|
if err != nil {
|
|
return trading.Order{}, err
|
|
}
|
|
|
|
return a.convertOrder(data), nil
|
|
}
|
|
|
|
// CancelAllOrders cancels all orders.
|
|
func (a *CcxtAdapter) CancelAllOrders(ctx context.Context, symbol string) ([]trading.Order, error) {
|
|
params := a.exchangeParams()
|
|
if symbol != "" {
|
|
params["symbol"] = symbol
|
|
}
|
|
|
|
data, err := a.doRequestArray(ctx, http.MethodPost, "/cancel_all_orders", params)
|
|
if err != nil {
|
|
// Fallback: cancel one by one
|
|
openOrders, err := a.GetOpenOrders(ctx, symbol)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var cancelled []trading.Order
|
|
for _, order := range openOrders {
|
|
if o, err := a.CancelOrder(ctx, order.OrderID, order.Symbol); err == nil {
|
|
cancelled = append(cancelled, o)
|
|
}
|
|
}
|
|
return cancelled, nil
|
|
}
|
|
|
|
orders := make([]trading.Order, len(data))
|
|
for i, o := range data {
|
|
orders[i] = a.convertOrder(o)
|
|
}
|
|
|
|
return orders, nil
|
|
}
|
|
|
|
func (a *CcxtAdapter) convertOrder(o map[string]interface{}) trading.Order {
|
|
quantity := decimal.NewFromFloat(getFloat(o, "amount"))
|
|
filled := decimal.NewFromFloat(getFloat(o, "filled"))
|
|
|
|
side := trading.SideBuy
|
|
if getString(o, "side") == "sell" {
|
|
side = trading.SideSell
|
|
}
|
|
|
|
orderType := trading.OrderTypeLimit
|
|
switch getString(o, "type") {
|
|
case "market":
|
|
orderType = trading.OrderTypeMarket
|
|
case "stop":
|
|
orderType = trading.OrderTypeStopLoss
|
|
case "stop_limit":
|
|
orderType = trading.OrderTypeStopLossLimit
|
|
}
|
|
|
|
status := trading.OrderStatusOpen
|
|
switch getString(o, "status") {
|
|
case "closed":
|
|
status = trading.OrderStatusFilled
|
|
case "canceled", "cancelled":
|
|
status = trading.OrderStatusCancelled
|
|
case "expired":
|
|
status = trading.OrderStatusExpired
|
|
case "rejected":
|
|
status = trading.OrderStatusRejected
|
|
}
|
|
|
|
order := trading.Order{
|
|
OrderID: getString(o, "id"),
|
|
ClientOrderID: getString(o, "clientOrderId"),
|
|
Symbol: getString(o, "symbol"),
|
|
Venue: a.name,
|
|
Side: side,
|
|
OrderType: orderType,
|
|
Status: status,
|
|
Quantity: quantity,
|
|
FilledQuantity: filled,
|
|
RemainingQuantity: quantity.Sub(filled),
|
|
CreatedAt: time.UnixMilli(int64(getFloat(o, "timestamp"))),
|
|
UpdatedAt: time.UnixMilli(int64(getFloat(o, "lastTradeTimestamp"))),
|
|
}
|
|
|
|
if price := getFloat(o, "price"); price > 0 {
|
|
d := decimal.NewFromFloat(price)
|
|
order.Price = &d
|
|
}
|
|
if avg := getFloat(o, "average"); avg > 0 {
|
|
d := decimal.NewFromFloat(avg)
|
|
order.AveragePrice = &d
|
|
}
|
|
|
|
return order
|
|
}
|
|
|
|
// Helper functions for type-safe map access
|
|
func getMap(m map[string]interface{}, key string) map[string]interface{} {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
if v, ok := m[key].(map[string]interface{}); ok {
|
|
return v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getString(m map[string]interface{}, key string) string {
|
|
if m == nil {
|
|
return ""
|
|
}
|
|
if v, ok := m[key].(string); ok {
|
|
return v
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getFloat(m map[string]interface{}, key string) float64 {
|
|
if m == nil {
|
|
return 0
|
|
}
|
|
return toFloat(m[key])
|
|
}
|
|
|
|
func getInt(m map[string]interface{}, key string) int {
|
|
if m == nil {
|
|
return 0
|
|
}
|
|
return int(toFloat(m[key]))
|
|
}
|
|
|
|
func toFloat(v interface{}) float64 {
|
|
switch n := v.(type) {
|
|
case float64:
|
|
return n
|
|
case float32:
|
|
return float64(n)
|
|
case int:
|
|
return float64(n)
|
|
case int64:
|
|
return float64(n)
|
|
case string:
|
|
d, _ := decimal.NewFromString(n)
|
|
f, _ := d.Float64()
|
|
return f
|
|
}
|
|
return 0
|
|
}
|