Skip to content

Commit 1a5e7cc

Browse files
committed
feat(swap): add swap info endpoint and improve BTC handling
1 parent cff6e7d commit 1a5e7cc

File tree

12 files changed

+126
-43
lines changed

12 files changed

+126
-43
lines changed

internal/btcrpc/blockstream/blockstream.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/pkg/errors"
1414

15+
"github.com/dwarvesf/icy-backend/internal/consts"
1516
"github.com/dwarvesf/icy-backend/internal/model"
1617
"github.com/dwarvesf/icy-backend/internal/utils/config"
1718
"github.com/dwarvesf/icy-backend/internal/utils/logger"
@@ -179,7 +180,7 @@ func (c *blockstream) GetBTCBalance(address string) (*model.Web3BigInt, error) {
179180
balanceSats := response.ChainStats.FundedTxoSum - response.ChainStats.SpentTxoSum
180181
return &model.Web3BigInt{
181182
Value: strconv.FormatInt(int64(balanceSats), 10),
182-
Decimal: 8, // BTC has 8 decimal places
183+
Decimal: consts.BTC_DECIMALS, // BTC has 8 decimal places
183184
}, nil
184185
}
185186

internal/btcrpc/helper.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ func (b *BtcRpc) selectUTXOs(address string, amountToSend int64) (selected []blo
302302
return nil, 0, fmt.Errorf("fee exceeds amount to send: fee %d, amountToSend %d", fee, amountToSend)
303303
}
304304

305-
satoshiRate, err := b.getSatoshiUSDPrice()
305+
satoshiRate, err := b.GetSatoshiUSDPrice()
306306
if err != nil {
307307
return nil, 0, err
308308
}
@@ -339,10 +339,10 @@ type CoinGeckoResponse struct {
339339
} `json:"bitcoin"`
340340
}
341341

342-
func (b *BtcRpc) getSatoshiUSDPrice() (float64, error) {
342+
func (b *BtcRpc) GetSatoshiUSDPrice() (float64, error) {
343343
// call from cache
344344
if x, found := b.cch.Get("satoshiPerUSD"); found {
345-
b.logger.Info("[getSatoshiUSDPrice] cache hit", map[string]string{
345+
b.logger.Info("[GetSatoshiUSDPrice] cache hit", map[string]string{
346346
"satoshiPerUSD": fmt.Sprintf("%0.1f", x),
347347
})
348348
rate := x.(float64)

internal/btcrpc/interface.go

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ type IBtcRpc interface {
77
CurrentBalance() (*model.Web3BigInt, error)
88
GetTransactionsByAddress(address string, fromTxId string) ([]model.OnchainBtcTransaction, error)
99
EstimateFees() (map[string]float64, error)
10+
GetSatoshiUSDPrice() (float64, error)
1011
}

internal/handler/handler.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"gorm.io/gorm"
55

66
"github.com/dwarvesf/icy-backend/internal/baserpc"
7+
"github.com/dwarvesf/icy-backend/internal/btcrpc"
78
"github.com/dwarvesf/icy-backend/internal/handler/oracle"
89
"github.com/dwarvesf/icy-backend/internal/handler/swap"
910
"github.com/dwarvesf/icy-backend/internal/handler/transaction"
@@ -19,10 +20,14 @@ type Handler struct {
1920
TransactionHandler transaction.IHandler
2021
}
2122

22-
func New(appConfig *config.AppConfig, logger *logger.Logger, oracleSvc oracleService.IOracle, baseRPC baserpc.IBaseRPC, db *gorm.DB) *Handler {
23+
func New(appConfig *config.AppConfig, logger *logger.Logger,
24+
oracleSvc oracleService.IOracle,
25+
baseRPC baserpc.IBaseRPC,
26+
btcRPC btcrpc.IBtcRpc,
27+
db *gorm.DB) *Handler {
2328
return &Handler{
2429
OracleHandler: oracle.New(oracleSvc, logger, appConfig),
25-
SwapHandler: swap.New(logger, appConfig, oracleSvc, baseRPC, db),
30+
SwapHandler: swap.New(logger, appConfig, oracleSvc, baseRPC, btcRPC, db),
2631
TransactionHandler: transaction.NewTransactionHandler(db, onchainbtcprocessedtransaction.New()),
2732
}
2833
}

internal/handler/swap/interface.go

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ import "github.com/gin-gonic/gin"
55
type IHandler interface {
66
CreateSwapRequest(c *gin.Context)
77
GenerateSignature(c *gin.Context)
8+
Info(c *gin.Context)
89
}

internal/handler/swap/swap.go

+69-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package swap
33
import (
44
"errors"
55
"fmt"
6+
"math"
67
"math/big"
78
"net/http"
89
"strconv"
@@ -13,6 +14,7 @@ import (
1314
"gorm.io/gorm"
1415

1516
"github.com/dwarvesf/icy-backend/internal/baserpc"
17+
"github.com/dwarvesf/icy-backend/internal/btcrpc"
1618
"github.com/dwarvesf/icy-backend/internal/consts"
1719
"github.com/dwarvesf/icy-backend/internal/model"
1820
"github.com/dwarvesf/icy-backend/internal/oracle"
@@ -40,6 +42,7 @@ type handler struct {
4042
appConfig *config.AppConfig
4143
oracle oracle.IOracle
4244
baseRPC baserpc.IBaseRPC
45+
btcRPC btcrpc.IBtcRpc
4346
db *gorm.DB
4447
btcProcessedTxStore onchainbtcprocessedtransaction.IStore
4548
swapRequestStore swaprequest.IStore
@@ -50,13 +53,15 @@ func New(
5053
appConfig *config.AppConfig,
5154
oracle oracle.IOracle,
5255
baseRPC baserpc.IBaseRPC,
56+
btcRPC btcrpc.IBtcRpc,
5357
db *gorm.DB,
5458
) IHandler {
5559
return &handler{
5660
logger: logger,
5761
appConfig: appConfig,
5862
oracle: oracle,
5963
baseRPC: baseRPC,
64+
btcRPC: btcRPC,
6065
db: db,
6166
btcProcessedTxStore: onchainbtcprocessedtransaction.New(),
6267
swapRequestStore: swaprequest.New(),
@@ -149,15 +154,15 @@ func (h *handler) CreateSwapRequest(c *gin.Context) {
149154
return
150155
}
151156

152-
icyAmountFloat, err := strconv.ParseFloat(req.ICYAmount, 64)
157+
icyAmountInt, err := strconv.ParseInt(req.ICYAmount, 10, 64)
153158
if err != nil {
154159
h.logger.Error("[CreateSwapRequest][ParseFloat]", map[string]string{
155160
"error": err.Error(),
156161
})
157162
c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, err, req, "invalid ICY amount"))
158163
return
159164
}
160-
if icyAmountFloat < h.appConfig.MinIcySwapAmount {
165+
if icyAmountInt < h.appConfig.MinIcySwapAmount {
161166
c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, fmt.Errorf("minimum ICY amount is %v", h.appConfig.MinIcySwapAmount), nil, "invalid ICY amount"))
162167
return
163168
}
@@ -217,3 +222,65 @@ func (h *handler) CreateSwapRequest(c *gin.Context) {
217222

218223
c.JSON(http.StatusOK, view.CreateResponse[any]("success", nil, nil, "swap request created successfully"))
219224
}
225+
226+
func (h *handler) Info(c *gin.Context) {
227+
// Get minimum ICY to swap from config
228+
minIcySwap := model.Web3BigInt{
229+
Value: fmt.Sprintf("%d", h.appConfig.MinIcySwapAmount),
230+
Decimal: 18,
231+
}
232+
233+
// Get ICY/BTC rate from oracle (using cached realtime rate), n ICY per 100M satoshi
234+
rate, err := h.oracle.GetRealtimeICYBTC()
235+
if err != nil {
236+
h.logger.Error("[Info][GetCachedRealtimeICYBTC]", map[string]string{
237+
"error": err.Error(),
238+
})
239+
c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, err, nil, "failed to get ICY/BTC rate"))
240+
return
241+
}
242+
243+
satPerUSD, err := h.btcRPC.GetSatoshiUSDPrice()
244+
if err != nil {
245+
h.logger.Error("[Info][GetSatoshiUSDPrice]", map[string]string{
246+
"error": err.Error(),
247+
})
248+
c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, err, nil, "failed to get satoshi price"))
249+
return
250+
}
251+
252+
// Get circulated ICY balance
253+
circulatedIcyBalance, err := h.oracle.GetCirculatedICY()
254+
if err != nil {
255+
h.logger.Error("[Info][GetCirculatedICY]", map[string]string{
256+
"error": err.Error(),
257+
})
258+
c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, err, nil, "failed to get circulated ICY balance"))
259+
return
260+
}
261+
262+
// Get BTC supply
263+
satBalance, err := h.oracle.GetBTCSupply()
264+
if err != nil {
265+
h.logger.Error("[Info][GetBTCSupply]", map[string]string{
266+
"error": err.Error(),
267+
})
268+
c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, err, nil, "failed to get BTC balance"))
269+
return
270+
}
271+
272+
// <rate> (x) icy = 100M satoshi
273+
// 1 icy = 100M / <rate> satoshi
274+
satPerIcy := new(big.Float).Quo(new(big.Float).SetFloat64(1e8), new(big.Float).SetFloat64(rate.ToFloat()))
275+
icyPerSat := new(big.Float).Quo(new(big.Float).SetFloat64(1), satPerIcy)
276+
icyPerUSD := new(big.Float).Quo(icyPerSat, new(big.Float).SetFloat64(satPerUSD))
277+
278+
c.JSON(http.StatusOK, view.CreateResponse[any](map[string]interface{}{
279+
"circulated_icy_balance": circulatedIcyBalance.Value,
280+
"satoshi_balance": satBalance.Value,
281+
"satoshi_per_usd": math.Ceil(satPerUSD*10) / 10,
282+
"icy_satoshi_rate": rate.Value,
283+
"icy_per_usd": icyPerUSD.String(),
284+
"min_icy_to_swap": minIcySwap.Value,
285+
}, nil, nil, "swap info retrieved successfully"))
286+
}

internal/oracle/oracle.go

+30-28
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package oracle
22

33
import (
44
"sync"
5+
"time"
56

7+
"github.com/patrickmn/go-cache"
68
"gorm.io/gorm"
79

810
"github.com/dwarvesf/icy-backend/internal/baserpc"
@@ -14,32 +16,28 @@ import (
1416
)
1517

1618
type IcyOracle struct {
17-
mux *sync.Mutex
18-
19-
cachedICYBTC *model.Web3BigInt
20-
db *gorm.DB
21-
store *store.Store
22-
appConfig *config.AppConfig
23-
logger *logger.Logger
24-
btcRpc btcrpc.IBtcRpc
25-
baseRpc baserpc.IBaseRPC
19+
db *gorm.DB
20+
store *store.Store
21+
appConfig *config.AppConfig
22+
logger *logger.Logger
23+
btcRpc btcrpc.IBtcRpc
24+
baseRpc baserpc.IBaseRPC
25+
cache *cache.Cache
26+
cacheMux *sync.Mutex
2627
}
2728

2829
// TODO: add other smaller packages if needed, e.g btcRPC or baseRPC
2930
func New(db *gorm.DB, store *store.Store, appConfig *config.AppConfig, logger *logger.Logger, btcRpc btcrpc.IBtcRpc, baseRpc baserpc.IBaseRPC) IOracle {
30-
o := &IcyOracle{
31+
return &IcyOracle{
3132
db: db,
3233
store: store,
33-
mux: &sync.Mutex{},
3434
appConfig: appConfig,
3535
logger: logger,
3636
btcRpc: btcRpc,
3737
baseRpc: baseRpc,
38+
cache: cache.New(5*time.Minute, 10*time.Minute),
39+
cacheMux: &sync.Mutex{},
3840
}
39-
40-
// go o.startUpdateCachedRealtimeICYBTC()
41-
42-
return o
4341
}
4442

4543
func (o *IcyOracle) GetCirculatedICY() (*model.Web3BigInt, error) {
@@ -101,6 +99,17 @@ func (o *IcyOracle) GetBTCSupply() (*model.Web3BigInt, error) {
10199
}
102100

103101
func (o *IcyOracle) GetRealtimeICYBTC() (*model.Web3BigInt, error) {
102+
o.cacheMux.Lock()
103+
defer o.cacheMux.Unlock()
104+
105+
// Try to get from cache first
106+
if cachedRate, found := o.cache.Get("icysat_rate"); found {
107+
if rate, ok := cachedRate.(*model.Web3BigInt); ok {
108+
return rate, nil
109+
}
110+
}
111+
112+
// If not in cache, calculate
104113
circulatedICY, err := o.GetCirculatedICY()
105114
if err != nil {
106115
o.logger.Error("[GetRealtimeICYBTC][GetCirculatedICY]", map[string]string{
@@ -117,28 +126,21 @@ func (o *IcyOracle) GetRealtimeICYBTC() (*model.Web3BigInt, error) {
117126
return nil, err
118127
}
119128

120-
icybtcRate, err := getConversionRatio(circulatedICY, btcSupply)
129+
icySatRate, err := getConversionRatio(circulatedICY, btcSupply)
121130
if err != nil {
122131
o.logger.Error("[GetRealtimeICYBTC][getConversionRatio]", map[string]string{
123132
"error": err.Error(),
124133
})
125134
return nil, err
126135
}
127136

128-
o.updateCachedRealtimeICYBTC(icybtcRate)
129-
130-
return icybtcRate, nil
137+
// Cache the new rate
138+
o.cache.Set("icysat_rate", icySatRate, cache.DefaultExpiration)
131139

140+
return icySatRate, nil
132141
}
133142

134143
func (o *IcyOracle) GetCachedRealtimeICYBTC() (*model.Web3BigInt, error) {
135-
o.mux.Lock()
136-
defer o.mux.Unlock()
137-
return o.cachedICYBTC, nil
138-
}
139-
140-
func (o *IcyOracle) updateCachedRealtimeICYBTC(number *model.Web3BigInt) {
141-
o.mux.Lock()
142-
defer o.mux.Unlock()
143-
o.cachedICYBTC = number
144+
// This method now simply calls GetRealtimeICYBTC, which already handles caching
145+
return o.GetRealtimeICYBTC()
144146
}

internal/oracle/util.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"math/big"
66

7+
"github.com/dwarvesf/icy-backend/internal/consts"
78
"github.com/dwarvesf/icy-backend/internal/model"
89
)
910

@@ -22,7 +23,7 @@ func getConversionRatio(circulatedIcy, btcSupply *model.Web3BigInt) (*model.Web3
2223
if btcFloat.Cmp(new(big.Float).SetFloat64(0)) == 0 {
2324
return &model.Web3BigInt{
2425
Value: "0",
25-
Decimal: 6,
26+
Decimal: consts.BTC_DECIMALS,
2627
}, nil
2728
}
2829

@@ -37,6 +38,6 @@ func getConversionRatio(circulatedIcy, btcSupply *model.Web3BigInt) (*model.Web3
3738

3839
return &model.Web3BigInt{
3940
Value: ratioInt.String(),
40-
Decimal: 6,
41+
Decimal: consts.BTC_DECIMALS,
4142
}, nil
4243
}

internal/server/server.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,6 @@ func Init() {
6161
})
6262

6363
c.Start()
64-
httpServer := http.NewHttpServer(appConfig, logger, oracle, baseRpc, db)
64+
httpServer := http.NewHttpServer(appConfig, logger, oracle, baseRpc, btcRpc, db)
6565
httpServer.Run()
6666
}

internal/transport/http/http.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"gorm.io/gorm"
1212

1313
"github.com/dwarvesf/icy-backend/internal/baserpc"
14+
"github.com/dwarvesf/icy-backend/internal/btcrpc"
1415
"github.com/dwarvesf/icy-backend/internal/handler"
1516
"github.com/dwarvesf/icy-backend/internal/oracle"
1617
"github.com/dwarvesf/icy-backend/internal/utils/config"
@@ -44,6 +45,7 @@ func apiKeyMiddleware(appConfig *config.AppConfig) gin.HandlerFunc {
4445
// Skip API key check for health check, swagger routes, and transactions routes
4546
if c.Request.URL.Path == "/healthz" ||
4647
strings.HasPrefix(c.Request.URL.Path, "/swagger") ||
48+
strings.HasPrefix(c.Request.URL.Path, "/api/v1/swap/info") ||
4749
strings.HasPrefix(c.Request.URL.Path, "/api/v1/transactions") ||
4850
strings.HasPrefix(c.Request.URL.Path, "/api/v1/swap/generate-signature") {
4951
c.Next()
@@ -74,7 +76,9 @@ func apiKeyMiddleware(appConfig *config.AppConfig) gin.HandlerFunc {
7476
}
7577
}
7678

77-
func NewHttpServer(appConfig *config.AppConfig, logger *logger.Logger, oracle oracle.IOracle, baseRPC baserpc.IBaseRPC, db *gorm.DB) *gin.Engine {
79+
func NewHttpServer(appConfig *config.AppConfig, logger *logger.Logger,
80+
oracle oracle.IOracle, baseRPC baserpc.IBaseRPC, btcRPC btcrpc.IBtcRpc,
81+
db *gorm.DB) *gin.Engine {
7882
r := gin.New()
7983
r.Use(
8084
gin.LoggerWithWriter(gin.DefaultWriter, "/healthz"),
@@ -85,7 +89,7 @@ func NewHttpServer(appConfig *config.AppConfig, logger *logger.Logger, oracle or
8589
// Add API key middleware
8690
r.Use(apiKeyMiddleware(appConfig))
8791

88-
h := handler.New(appConfig, logger, oracle, baseRPC, db)
92+
h := handler.New(appConfig, logger, oracle, baseRPC, btcRPC, db)
8993

9094
// use ginSwagger middleware to serve the API docs
9195
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

internal/transport/http/v1.go

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func loadV1Routes(r *gin.Engine, h *handler.Handler) {
2323
{
2424
swap.POST("/generate-signature", h.SwapHandler.GenerateSignature)
2525
swap.POST("", h.SwapHandler.CreateSwapRequest)
26+
swap.GET("/info", h.SwapHandler.Info)
2627
}
2728

2829
transactions := v1.Group("/transactions")

0 commit comments

Comments
 (0)