feat: set up project
This commit is contained in:
23
internal/api/handler/helper.go
Normal file
23
internal/api/handler/helper.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func RenderError(c *fiber.Ctx, err error, message string) error {
|
||||
if err != nil {
|
||||
log.Printf("renderError: %s\n", err)
|
||||
}
|
||||
return c.Render("error", fiber.Map{
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
func JSONError(c *fiber.Ctx, status int, err error, message string) error {
|
||||
if err != nil {
|
||||
log.Printf("JSONError: %s\n", err)
|
||||
}
|
||||
return c.Status(status).SendString("error: " + message)
|
||||
}
|
||||
58
internal/api/handler/payment.go
Normal file
58
internal/api/handler/payment.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/domain"
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/services"
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/services/price"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type OrderHandler struct {
|
||||
walletAddress string
|
||||
orderSrv *services.Order
|
||||
conversor *price.PriceConversor
|
||||
}
|
||||
|
||||
func NewOrderHandler(walletAddress string, orderSrv *services.Order, conversor *price.PriceConversor) *OrderHandler {
|
||||
return &OrderHandler{
|
||||
walletAddress: walletAddress,
|
||||
orderSrv: orderSrv,
|
||||
conversor: conversor,
|
||||
}
|
||||
}
|
||||
|
||||
type orderReq struct {
|
||||
OrderID string `json:"order_id"`
|
||||
ClientID string `json:"client_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency domain.FiatCurrency `json:"currency"`
|
||||
}
|
||||
|
||||
func (hdl *OrderHandler) Post(c *fiber.Ctx) error {
|
||||
req := orderReq{}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return RenderError(c, fmt.Errorf("id is empty"), "")
|
||||
}
|
||||
|
||||
btcAmount, err := hdl.conversor.UsdToBtc(req.Amount)
|
||||
if err != nil {
|
||||
return RenderError(c, fmt.Errorf("hdl.conversor.UsdToBtc %w", err), "")
|
||||
}
|
||||
|
||||
order, err := hdl.orderSrv.NewOrder(c.Context(), req.OrderID, req.ClientID, btcAmount)
|
||||
if err != nil {
|
||||
return RenderError(c, fmt.Errorf("hdl.orderSrv.NewOrder %w", err), "")
|
||||
}
|
||||
|
||||
return c.Render("order",
|
||||
fiber.Map{
|
||||
"order_id": order.ID.Hex(),
|
||||
"amount": btcAmount,
|
||||
"wallet_address": hdl.walletAddress,
|
||||
"expires_at": order.ExpiresAt.Format(time.RFC3339),
|
||||
},
|
||||
)
|
||||
}
|
||||
152
internal/api/server.go
Normal file
152
internal/api/server.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/docker/go-units"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||
"github.com/gofiber/template/handlebars/v2"
|
||||
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/api/handler"
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/domain"
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/services"
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/services/btc"
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/services/mail"
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/services/price"
|
||||
"gitea.urkob.com/urko/btc-pay-checker/kit/cfg"
|
||||
)
|
||||
|
||||
const (
|
||||
MAX_FILE_SIZE = 25
|
||||
MAX_FILE_SIZE_MiB = MAX_FILE_SIZE * units.MiB
|
||||
)
|
||||
|
||||
type RestServer struct {
|
||||
app *fiber.App
|
||||
config *cfg.Config
|
||||
btcService *btc.BitcoinService
|
||||
orderSrv *services.Order
|
||||
mailSrv *mail.MailService
|
||||
priceSrv *price.PriceConversor
|
||||
}
|
||||
|
||||
func NewRestServer(
|
||||
config *cfg.Config,
|
||||
orderSrv *services.Order,
|
||||
btcService *btc.BitcoinService,
|
||||
priceSrv *price.PriceConversor,
|
||||
mailSrv *mail.MailService,
|
||||
) *RestServer {
|
||||
return &RestServer{
|
||||
config: config,
|
||||
orderSrv: orderSrv,
|
||||
btcService: btcService,
|
||||
priceSrv: priceSrv,
|
||||
mailSrv: mailSrv,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RestServer) Start(ctx context.Context, apiPort, views string) error {
|
||||
engine := handlebars.New(views, ".hbs")
|
||||
s.app = fiber.New(fiber.Config{
|
||||
Views: engine,
|
||||
BodyLimit: MAX_FILE_SIZE_MiB,
|
||||
})
|
||||
|
||||
s.app.Use(limiter.New(limiter.Config{
|
||||
Max: 5,
|
||||
Expiration: 1 * time.Hour,
|
||||
LimiterMiddleware: limiter.SlidingWindow{},
|
||||
}))
|
||||
|
||||
s.app.Use(cors.New(cors.Config{
|
||||
AllowMethods: "GET,POST,OPTIONS",
|
||||
AllowOrigins: "*",
|
||||
AllowHeaders: "Origin, Accept, Content-Type, X-CSRF-Token, Authorization",
|
||||
ExposeHeaders: "Origin",
|
||||
}))
|
||||
|
||||
s.loadViews(views + "/images")
|
||||
|
||||
orderHandler := handler.NewOrderHandler(s.config.WalletAddress, s.orderSrv, s.priceSrv)
|
||||
|
||||
notifChan := make(chan domain.Notification)
|
||||
go s.btcService.Notify(ctx, notifChan)
|
||||
go s.onNotification(ctx, notifChan)
|
||||
|
||||
s.app.Post("/order", orderHandler.Post)
|
||||
|
||||
if err := s.app.Listen(":" + apiPort); err != nil {
|
||||
log.Fatalln("app.Listen:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// onNotification Step
|
||||
// 1- update the block where tx has been forged
|
||||
// 2- send email to client alerting his block has been forged
|
||||
// 3- to upload file into arweave
|
||||
func (s *RestServer) onNotification(ctx context.Context, notifChan chan domain.Notification) {
|
||||
for notif := range notifChan {
|
||||
order, err := s.orderSrv.FromAmount(ctx, notif.Amount, notif.DoneAt)
|
||||
if err != nil {
|
||||
log.Println("error while retrieve transaction from forged block: ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
order.Block = notif.BlockHash
|
||||
order.Tx = notif.Tx
|
||||
|
||||
if err := s.orderSrv.OrderCompleted(ctx, order); err != nil {
|
||||
log.Println("OrderCompleted:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Send email to client and provider
|
||||
if err := s.mailSrv.SendProviderConfirm(mail.SendOK{
|
||||
TxID: order.Tx,
|
||||
BlockHash: order.Block,
|
||||
DocHash: "",
|
||||
To: "doc.Email",
|
||||
}); err != nil {
|
||||
log.Println("error while send confirm email:", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RestServer) loadViews(imagesDir string) {
|
||||
s.app.Static("/images", imagesDir)
|
||||
|
||||
s.app.Get("/", func(c *fiber.Ctx) error {
|
||||
return c.Render("upload", fiber.Map{})
|
||||
})
|
||||
|
||||
s.app.Get("/error", func(c *fiber.Ctx) error {
|
||||
message := c.Query("message")
|
||||
return renderError(c, nil, message)
|
||||
})
|
||||
}
|
||||
|
||||
func renderError(c *fiber.Ctx, err error, message string) error {
|
||||
if err != nil {
|
||||
log.Printf("renderError: %s\n", err)
|
||||
}
|
||||
return c.Render("error", fiber.Map{
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RestServer) Shutdown() error {
|
||||
if err := s.app.Server().Shutdown(); err != nil {
|
||||
log.Printf("app.Server().Shutdown(): %s\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
10
internal/domain/coin.go
Normal file
10
internal/domain/coin.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package domain
|
||||
|
||||
type Coin string
|
||||
|
||||
const CoinBTC Coin = "coingecko:bitcoin"
|
||||
|
||||
type FiatCurrency string
|
||||
|
||||
const FiatCurrencyDollar = "USD"
|
||||
const FiatCurrencyEuro = "EUR"
|
||||
10
internal/domain/notification.go
Normal file
10
internal/domain/notification.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type Notification struct {
|
||||
BlockHash string
|
||||
Tx string
|
||||
Amount float64
|
||||
DoneAt time.Time
|
||||
}
|
||||
19
internal/domain/order.go
Normal file
19
internal/domain/order.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
ID primitive.ObjectID `bson:"_id" json:"_id"`
|
||||
OrderID string `json:"order_id"`
|
||||
ClientID string `json:"client_id"`
|
||||
Amount float64 `bson:"amount" json:"amount"`
|
||||
Tx string `bson:"tx" json:"tx"`
|
||||
Block string `bson:"block" json:"block"`
|
||||
PaidAt time.Time `bson:"paid_at" json:"paid_at"`
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"`
|
||||
ExpiresAt time.Time `bson:"expires_at" json:"expires_at"`
|
||||
}
|
||||
80
internal/platform/mongodb/order/repository.go
Normal file
80
internal/platform/mongodb/order/repository.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/domain"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
type Repo struct {
|
||||
collection *mongo.Collection
|
||||
}
|
||||
|
||||
func NewRepo(collection *mongo.Collection) *Repo {
|
||||
return &Repo{
|
||||
collection: collection,
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *Repo) Insert(ctx context.Context, order *domain.Order) (primitive.ObjectID, error) {
|
||||
res, err := repo.collection.InsertOne(ctx, order)
|
||||
if err != nil {
|
||||
return primitive.NilObjectID, fmt.Errorf("v.collection.InsertOne: %s", err)
|
||||
}
|
||||
|
||||
documentID, ok := res.InsertedID.(primitive.ObjectID)
|
||||
if !ok {
|
||||
return primitive.NilObjectID, fmt.Errorf("res.InsertedID")
|
||||
}
|
||||
|
||||
return documentID, nil
|
||||
}
|
||||
|
||||
func (repo *Repo) FromAmount(ctx context.Context, amount float64, timestamp time.Time) (*domain.Order, error) {
|
||||
order := &domain.Order{}
|
||||
filter := bson.M{
|
||||
"amount": amount,
|
||||
"expires_at": bson.M{
|
||||
"$gte": timestamp,
|
||||
},
|
||||
"paid_at": time.Time{},
|
||||
}
|
||||
|
||||
count, err := repo.collection.CountDocuments(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("doc.Decode: %s", err)
|
||||
}
|
||||
|
||||
if count <= 0 {
|
||||
return nil, fmt.Errorf("order not found")
|
||||
} else if count > 1 {
|
||||
return nil, fmt.Errorf("multiple orders found: %d", count)
|
||||
}
|
||||
|
||||
doc := repo.collection.FindOne(ctx, filter)
|
||||
if err := doc.Decode(order); err != nil {
|
||||
return nil, fmt.Errorf("doc.Decode: %s", err)
|
||||
}
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func (repo *Repo) OrderCompleted(ctx context.Context, order *domain.Order) error {
|
||||
updateOpts, err := repo.collection.UpdateOne(ctx, bson.M{"_id": order.ID},
|
||||
bson.M{
|
||||
"tx": order.Tx,
|
||||
"block": order.Block,
|
||||
"paid_at": time.Now(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("collection.UpdateOne: %s", err)
|
||||
}
|
||||
log.Printf("OrderCompleted update %+v\n", updateOpts)
|
||||
return nil
|
||||
}
|
||||
53
internal/services/btc/btc.go
Normal file
53
internal/services/btc/btc.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type BitcoinService struct {
|
||||
host string
|
||||
auth string
|
||||
zmqAddress string
|
||||
walletAddress string
|
||||
client *http.Client
|
||||
testNet bool
|
||||
}
|
||||
|
||||
func NewBitcoinService(host, auth, zmqAddress, walletAddress string) *BitcoinService {
|
||||
bs := BitcoinService{
|
||||
host: host,
|
||||
auth: auth,
|
||||
zmqAddress: zmqAddress,
|
||||
walletAddress: walletAddress,
|
||||
client: &http.Client{},
|
||||
}
|
||||
|
||||
from, err := bs.getAddressGroupings(false)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("bs.getAddressGroupings %s", err))
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, a := range from {
|
||||
if a.address == bs.walletAddress {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
return &bs
|
||||
}
|
||||
|
||||
func (bc *BitcoinService) WithTestnet() *BitcoinService {
|
||||
bc.testNet = true
|
||||
return bc
|
||||
}
|
||||
|
||||
func (bc *BitcoinService) Explorer(tx string) string {
|
||||
if bc.testNet {
|
||||
return "https://testnet.bitcoinexplorer.org/tx/" + tx
|
||||
}
|
||||
return "https://btcscan.org/tx/" + tx
|
||||
}
|
||||
61
internal/services/btc/decode_raw_transaction.go
Normal file
61
internal/services/btc/decode_raw_transaction.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type decodeRawTransactionResponseResult struct {
|
||||
Txid string `json:"txid"`
|
||||
Hash string `json:"hash"`
|
||||
Version int `json:"version"`
|
||||
Size int `json:"size"`
|
||||
Vsize int `json:"vsize"`
|
||||
Weight int `json:"weight"`
|
||||
Locktime int `json:"locktime"`
|
||||
Vin []struct {
|
||||
Txid string `json:"txid"`
|
||||
Vout int `json:"vout"`
|
||||
ScriptSig struct {
|
||||
Asm string `json:"asm"`
|
||||
Hex string `json:"hex"`
|
||||
} `json:"scriptSig"`
|
||||
Txinwitness []string `json:"txinwitness"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
} `json:"vin"`
|
||||
Vout []struct {
|
||||
Value float64 `json:"value"`
|
||||
N int `json:"n"`
|
||||
ScriptPubKey struct {
|
||||
Asm string `json:"asm"`
|
||||
Hex string `json:"hex"`
|
||||
Address string `json:"address,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
} `json:"scriptPubKey,omitempty"`
|
||||
} `json:"vout"`
|
||||
}
|
||||
|
||||
// func (d decodeRawTransactionResponseResult) getAddressRestFunds(amountPlusFee float64) string {
|
||||
// address := ""
|
||||
// for _, vout := range d.Vout {
|
||||
// if vout.Value > amountPlusFee {
|
||||
// address = vout.ScriptPubKey.Address
|
||||
|
||||
// }
|
||||
// }
|
||||
// return address
|
||||
// }
|
||||
|
||||
type decodeRawTransactionResponse struct {
|
||||
Result decodeRawTransactionResponseResult `json:"result"`
|
||||
RPCResponse
|
||||
}
|
||||
|
||||
func NewdecodeRawTransactionResponse(bts []byte) (*decodeRawTransactionResponse, error) {
|
||||
resp := new(decodeRawTransactionResponse)
|
||||
err := json.Unmarshal(bts, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
52
internal/services/btc/get_balance.go
Normal file
52
internal/services/btc/get_balance.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func NewGetBalanceParams() []interface{} {
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
type GetBalanceResponse struct {
|
||||
Result float64 `json:"result"`
|
||||
RPCResponse
|
||||
}
|
||||
|
||||
func NewGetBalanceResponse(bts []byte) (*GetBalanceResponse, error) {
|
||||
resp := new(GetBalanceResponse)
|
||||
err := json.Unmarshal(bts, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (b *BitcoinService) GetBalance() (float64, error) {
|
||||
req := b.NewRPCRequest().WithMethod(GET_BALANCE)
|
||||
if req == nil {
|
||||
return 0.0, fmt.Errorf("NewRPCRequest %s is nil", GET_BALANCE)
|
||||
}
|
||||
|
||||
resp, err := req.Call(NewGetBalanceParams()...)
|
||||
if err != nil {
|
||||
return 0.0, fmt.Errorf("req.Call: %s", err)
|
||||
}
|
||||
|
||||
strinResp := string(resp)
|
||||
if strinResp == "" {
|
||||
return 0.0, fmt.Errorf("strinResp: %s", err)
|
||||
}
|
||||
|
||||
trx, err := NewGetBalanceResponse(resp)
|
||||
if err != nil {
|
||||
return 0.0, fmt.Errorf("NewGetBalanceResponse: %s", err)
|
||||
}
|
||||
if trx.Error != nil {
|
||||
return 0.0, fmt.Errorf("raw.Error: %+v", trx.Error)
|
||||
}
|
||||
|
||||
return trx.Result, nil
|
||||
}
|
||||
14
internal/services/btc/get_balance_test.go
Normal file
14
internal/services/btc/get_balance_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_bitcoinService_GetBalance(t *testing.T) {
|
||||
th := newTestHelper()
|
||||
balance, err := th.b.GetBalance()
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, balance, 0.0)
|
||||
}
|
||||
72
internal/services/btc/get_block.go
Normal file
72
internal/services/btc/get_block.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func NewGetBlockParams(blockHash string) []interface{} {
|
||||
return []interface{}{blockHash}
|
||||
}
|
||||
|
||||
type GetBlockResponse struct {
|
||||
Result GetBlockResponseResult `json:"result"`
|
||||
RPCResponse
|
||||
}
|
||||
type GetBlockResponseResult struct {
|
||||
Hash string `json:"hash"`
|
||||
Confirmations int `json:"confirmations"`
|
||||
Height int `json:"height"`
|
||||
Version int `json:"version"`
|
||||
VersionHex string `json:"versionHex"`
|
||||
Merkleroot string `json:"merkleroot"`
|
||||
Time int `json:"time"`
|
||||
Mediantime int `json:"mediantime"`
|
||||
Nonce int `json:"nonce"`
|
||||
Bits string `json:"bits"`
|
||||
Difficulty float64 `json:"difficulty"`
|
||||
Chainwork string `json:"chainwork"`
|
||||
NTx int `json:"nTx"`
|
||||
Previousblockhash string `json:"previousblockhash"`
|
||||
Strippedsize int `json:"strippedsize"`
|
||||
Size int `json:"size"`
|
||||
Weight int `json:"weight"`
|
||||
Tx []string `json:"tx"`
|
||||
}
|
||||
|
||||
func NewGetBlockResponse(bts []byte) (*GetBlockResponse, error) {
|
||||
resp := new(GetBlockResponse)
|
||||
err := json.Unmarshal(bts, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (b *BitcoinService) getBlock(blockHash string) (*GetBlockResponseResult, error) {
|
||||
req := b.NewRPCRequest().WithMethod(GET_BLOCK)
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("NewRPCRequest %s is nil", GET_BLOCK)
|
||||
}
|
||||
|
||||
resp, err := req.Call(NewGetBlockParams(blockHash)...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req.Call: %s", err)
|
||||
}
|
||||
|
||||
strinResp := string(resp)
|
||||
if strinResp == "" {
|
||||
return nil, fmt.Errorf("strinResp: %s", err)
|
||||
}
|
||||
|
||||
block, err := NewGetBlockResponse(resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewGetBlockResponse: %s", err)
|
||||
}
|
||||
if block.Error != nil {
|
||||
return nil, fmt.Errorf("raw.Error: %+v", block.Error)
|
||||
}
|
||||
|
||||
return &block.Result, nil
|
||||
}
|
||||
16
internal/services/btc/get_block_test.go
Normal file
16
internal/services/btc/get_block_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_bitcoinService_GetBlock(t *testing.T) {
|
||||
th := newTestHelper()
|
||||
blockHash := "00000000000000043d385a031abd1f911aae1783810ec5f59a1db9e3ff7eac80"
|
||||
block, err := th.b.getBlock(blockHash)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, block)
|
||||
require.Greater(t, len(block.Tx), 0)
|
||||
}
|
||||
114
internal/services/btc/get_raw_transaction.go
Normal file
114
internal/services/btc/get_raw_transaction.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getRawTransactionParams(trxid, blockchash string) []interface{} {
|
||||
return []interface{}{trxid, true, blockchash}
|
||||
}
|
||||
|
||||
type GetRawTransactionResponseResult struct {
|
||||
InActiveChain bool `json:"in_active_chain"`
|
||||
Txid string `json:"txid"`
|
||||
Hash string `json:"hash"`
|
||||
Version int `json:"version"`
|
||||
Size int `json:"size"`
|
||||
Vsize int `json:"vsize"`
|
||||
Weight int `json:"weight"`
|
||||
Locktime int `json:"locktime"`
|
||||
Vin []struct {
|
||||
Txid string `json:"txid"`
|
||||
Vout int `json:"vout"`
|
||||
ScriptSig struct {
|
||||
Asm string `json:"asm"`
|
||||
Hex string `json:"hex"`
|
||||
} `json:"scriptSig"`
|
||||
Txinwitness []string `json:"txinwitness"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
} `json:"vin"`
|
||||
Vout []GetRawTransactionResponseResultVout `json:"vout"`
|
||||
Hex string `json:"hex"`
|
||||
Blockhash string `json:"blockhash"`
|
||||
Confirmations int `json:"confirmations"`
|
||||
Time int `json:"time"`
|
||||
Blocktime int `json:"blocktime"`
|
||||
}
|
||||
|
||||
type GetRawTransactionResponseResultVout struct {
|
||||
Value float64 `json:"value"`
|
||||
N int `json:"n"`
|
||||
ScriptPubKey struct {
|
||||
Asm string `json:"asm"`
|
||||
Hex string `json:"hex"`
|
||||
Address string `json:"address,omitempty"`
|
||||
Type string `json:"type"`
|
||||
} `json:"scriptPubKey,omitempty"`
|
||||
}
|
||||
|
||||
func (g GetRawTransactionResponseResult) getOpReturn() string {
|
||||
opReturn := ""
|
||||
for _, v := range g.Vout {
|
||||
if strings.HasPrefix(v.ScriptPubKey.Asm, "OP_RETURN") {
|
||||
opReturn = v.ScriptPubKey.Asm
|
||||
break
|
||||
}
|
||||
}
|
||||
if !strings.HasPrefix(opReturn, "OP_RETURN") {
|
||||
return ""
|
||||
}
|
||||
hexMessage := strings.Split(opReturn, " ")[len(strings.Split(opReturn, " "))-1]
|
||||
return hexMessage
|
||||
}
|
||||
|
||||
type GetRawTransactionResponse struct {
|
||||
Result GetRawTransactionResponseResult `json:"result"`
|
||||
RPCResponse
|
||||
}
|
||||
|
||||
func NewGetRawTransactionResponse(bts []byte) (*GetRawTransactionResponse, error) {
|
||||
resp := new(GetRawTransactionResponse)
|
||||
err := json.Unmarshal(bts, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (b *BitcoinService) getRawTransaction(trxid, blockhash string) (*GetRawTransactionResponseResult, error) {
|
||||
req := b.NewRPCRequest().WithMethod(GET_RAW_TRANSACTION)
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("NewRPCRequest %s is nil", GET_RAW_TRANSACTION)
|
||||
}
|
||||
|
||||
bts := getRawTransactionParams(trxid, blockhash)
|
||||
if bts == nil {
|
||||
return nil, fmt.Errorf("getRawTransactionParams is nil")
|
||||
}
|
||||
|
||||
resp, err := req.Call(bts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req.Call: %s", err)
|
||||
}
|
||||
|
||||
strinResp := string(resp)
|
||||
if strinResp == "" {
|
||||
return nil, fmt.Errorf("strinResp: %s", err)
|
||||
}
|
||||
|
||||
trx, err := NewGetRawTransactionResponse(resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewGetRawTransactionResponse: %s", err)
|
||||
}
|
||||
if trx.Error != nil {
|
||||
return nil, fmt.Errorf("raw.Error: %+v", trx.Error)
|
||||
}
|
||||
if trx.Result.Txid != trxid {
|
||||
return nil, fmt.Errorf("trx.Result.Txid: %s != trxid %s", trx.Result.Txid, trxid)
|
||||
}
|
||||
|
||||
return &trx.Result, nil
|
||||
}
|
||||
25
internal/services/btc/get_raw_transaction_test.go
Normal file
25
internal/services/btc/get_raw_transaction_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_bitcoinService_getRawTransaction(t *testing.T) {
|
||||
th := newTestHelper()
|
||||
txid := "873d5516a9cacc065bb30831bf4855b058f59a2a4877e08e0e28c22c51c58e39"
|
||||
tx, err := th.b.getTransaction(txid)
|
||||
require.NoError(t, err)
|
||||
|
||||
rawTrx, err := th.b.getRawTransaction(tx.Txid, tx.Blockhash)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, rawTrx.Txid, txid)
|
||||
|
||||
require.NotEmpty(t, rawTrx.getOpReturn())
|
||||
|
||||
// hexMessage := rawTrx.getOpReturn()
|
||||
// bts, err := hex.DecodeString(hexMessage)
|
||||
// require.NoError(t, err)
|
||||
// require.Equal(t, testMessage, string(bts))
|
||||
}
|
||||
84
internal/services/btc/get_transaction.go
Normal file
84
internal/services/btc/get_transaction.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func getTransactionParams(trxid string) []interface{} {
|
||||
return []interface{}{trxid}
|
||||
}
|
||||
|
||||
type GetTransactionResponseResult struct {
|
||||
Amount float64 `json:"amount"`
|
||||
Fee float64 `json:"fee"`
|
||||
Confirmations int `json:"confirmations"`
|
||||
Blockhash string `json:"blockhash"`
|
||||
Blockheight int `json:"blockheight"`
|
||||
Blockindex int `json:"blockindex"`
|
||||
Blocktime int `json:"blocktime"`
|
||||
Txid string `json:"txid"`
|
||||
Walletconflicts []interface{} `json:"walletconflicts"`
|
||||
Time int `json:"time"`
|
||||
Timereceived int `json:"timereceived"`
|
||||
Bip125Replaceable string `json:"bip125-replaceable"`
|
||||
Details []struct {
|
||||
Address string `json:"address,omitempty"`
|
||||
Category string `json:"category"`
|
||||
Amount float64 `json:"amount"`
|
||||
Vout int `json:"vout"`
|
||||
Fee float64 `json:"fee"`
|
||||
Abandoned bool `json:"abandoned"`
|
||||
} `json:"details"`
|
||||
Hex string `json:"hex"`
|
||||
}
|
||||
|
||||
type GetTransactionResponse struct {
|
||||
Result GetTransactionResponseResult `json:"result"`
|
||||
RPCResponse
|
||||
}
|
||||
|
||||
func NewGetTransactionResponse(bts []byte) (*GetTransactionResponse, error) {
|
||||
resp := new(GetTransactionResponse)
|
||||
err := json.Unmarshal(bts, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (b *BitcoinService) getTransaction(trxid string) (*GetTransactionResponseResult, error) {
|
||||
req := b.NewRPCRequest().WithMethod(GET_TRANSACTION)
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("NewRPCRequest %s is nil", GET_TRANSACTION)
|
||||
}
|
||||
|
||||
bts := getTransactionParams(trxid)
|
||||
if bts == nil {
|
||||
return nil, fmt.Errorf("NewGetTransactionParams is nil")
|
||||
}
|
||||
|
||||
resp, err := req.Call(bts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req.Call: %s", err)
|
||||
}
|
||||
|
||||
strinResp := string(resp)
|
||||
if strinResp == "" {
|
||||
return nil, fmt.Errorf("strinResp: %s", err)
|
||||
}
|
||||
|
||||
trx, err := NewGetTransactionResponse(resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewGetTransactionResponse: %s", err)
|
||||
}
|
||||
if trx.Error != nil {
|
||||
return nil, fmt.Errorf("raw.Error: %+v", trx.Error)
|
||||
}
|
||||
if trx.Result.Txid != trxid {
|
||||
return nil, fmt.Errorf("trx.Result.Txid: %s != trxid %s", trx.Result.Txid, trxid)
|
||||
}
|
||||
|
||||
return &trx.Result, nil
|
||||
}
|
||||
15
internal/services/btc/get_transaction_test.go
Normal file
15
internal/services/btc/get_transaction_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_bitcoinService_GetTransaction(t *testing.T) {
|
||||
th := newTestHelper()
|
||||
txid := "1dd9a1be3dc4feba3031cda110bd043535bc170a34a7664b231ccda3c3928e93"
|
||||
trx, err := th.b.getTransaction(txid)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, trx.Txid, txid)
|
||||
}
|
||||
124
internal/services/btc/listaddressgroupings.go
Normal file
124
internal/services/btc/listaddressgroupings.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
type ListAddressGroupingsResponse struct {
|
||||
Result [][][]any `json:"result"`
|
||||
Error any `json:"error"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func NewListAddressGroupingsResponse(bts []byte) (*ListAddressGroupingsResponse, error) {
|
||||
resp := new(ListAddressGroupingsResponse)
|
||||
err := json.Unmarshal(bts, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// func (b *bitcoinService) listaddressgroupings() ([][]any, error) {
|
||||
// req := b.NewRPCRequest().WithMethod(LIST_ADDRESS_GROUPINGS)
|
||||
// if req == nil {
|
||||
// return nil, fmt.Errorf("NewRPCRequest %s is nil", LIST_ADDRESS_GROUPINGS)
|
||||
// }
|
||||
|
||||
// resp, err := req.Call(nil)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("req.Call: %s", err)
|
||||
// }
|
||||
|
||||
// strinResp := string(resp)
|
||||
// if strinResp == "" {
|
||||
// return nil, fmt.Errorf("strinResp: %s", err)
|
||||
// }
|
||||
|
||||
// log.Println(strinResp)
|
||||
|
||||
// addressResp, err := NewListAddressGroupingsResponse(resp)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("NewFundTransactionResponse: %s", err)
|
||||
// }
|
||||
// if addressResp.Error != nil {
|
||||
// return nil, fmt.Errorf("raw.Error: %+v", addressResp.Error)
|
||||
// }
|
||||
|
||||
// if len(addressResp.Result) != 1 {
|
||||
// return nil, fmt.Errorf("no addresses found")
|
||||
// }
|
||||
// if len(addressResp.Result[0]) <= 0 {
|
||||
// return nil, fmt.Errorf("no addresses found")
|
||||
// }
|
||||
|
||||
// return addressResp.Result[0], nil
|
||||
// }
|
||||
|
||||
type AddressGrouping struct {
|
||||
address string
|
||||
funds float64
|
||||
}
|
||||
|
||||
func (b *BitcoinService) listaddressgroupingsWithFunds() ([]AddressGrouping, error) {
|
||||
return b.getAddressGroupings(true)
|
||||
}
|
||||
|
||||
func (b *BitcoinService) getAddressGroupings(withFunds bool) ([]AddressGrouping, error) {
|
||||
req := b.NewRPCRequest().WithMethod(LIST_ADDRESS_GROUPINGS)
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("NewRPCRequest %s is nil", LIST_ADDRESS_GROUPINGS)
|
||||
}
|
||||
|
||||
resp, err := req.Call()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req.Call: %s", err)
|
||||
}
|
||||
|
||||
strinResp := string(resp)
|
||||
if strinResp == "" {
|
||||
return nil, fmt.Errorf("strinResp: %s", err)
|
||||
}
|
||||
|
||||
addressResp, err := NewListAddressGroupingsResponse(resp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewListAddressGroupingsResponse: %s", err)
|
||||
}
|
||||
if addressResp.Error != nil {
|
||||
return nil, fmt.Errorf("raw.Error: %+v", addressResp.Error)
|
||||
}
|
||||
|
||||
if len(addressResp.Result) != 1 {
|
||||
return nil, fmt.Errorf("no addresses found")
|
||||
}
|
||||
if len(addressResp.Result[0]) <= 0 {
|
||||
return nil, fmt.Errorf("no addresses found")
|
||||
}
|
||||
|
||||
var addressList []AddressGrouping
|
||||
for i := range addressResp.Result[0] {
|
||||
addressRaw, fundsRaw := addressResp.Result[0][i][0], addressResp.Result[0][i][1]
|
||||
address, ok := addressRaw.(string)
|
||||
if !ok {
|
||||
log.Fatalf("Address is not a string: %v", addressRaw)
|
||||
continue
|
||||
}
|
||||
|
||||
funds, ok := fundsRaw.(float64)
|
||||
if !ok {
|
||||
log.Fatalf("Funds is not a float64: %v", fundsRaw)
|
||||
continue
|
||||
}
|
||||
|
||||
if withFunds && funds <= 0.0 {
|
||||
continue
|
||||
}
|
||||
|
||||
addressList = append(addressList, AddressGrouping{address: address, funds: funds})
|
||||
}
|
||||
|
||||
return addressList, nil
|
||||
}
|
||||
17
internal/services/btc/listaddressgroupings_test.go
Normal file
17
internal/services/btc/listaddressgroupings_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_bitcoinService_listaddressgroupingsWithFunds(t *testing.T) {
|
||||
th := newTestHelper()
|
||||
addresslistWithFunds, err := th.b.listaddressgroupingsWithFunds()
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(addresslistWithFunds), 0)
|
||||
log.Println("addresslist", addresslistWithFunds)
|
||||
|
||||
}
|
||||
79
internal/services/btc/notification.go
Normal file
79
internal/services/btc/notification.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/domain"
|
||||
)
|
||||
|
||||
func (b *BitcoinService) Notify(ctx context.Context, notifChan chan<- domain.Notification) {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
q := &Zmq{}
|
||||
if err := q.Connect(b.zmqAddress); err != nil {
|
||||
log.Fatal("Failed to connect to ZeroMQ socket:", err)
|
||||
}
|
||||
|
||||
blockChan := make(chan string)
|
||||
go q.Listen(blockChan)
|
||||
|
||||
go func() {
|
||||
for blockHash := range blockChan {
|
||||
block, err := b.getBlock(blockHash)
|
||||
if err != nil {
|
||||
log.Println("Error b.GetBlock:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Println("block readed", block.Hash)
|
||||
for _, txid := range block.Tx {
|
||||
tx, err := b.getRawTransaction(txid, block.Hash)
|
||||
if err != nil {
|
||||
log.Println("Error b.getRawTransaction:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !tx.InActiveChain {
|
||||
log.Printf("tx is not active on chain | block %s | tx %s \n", blockHash, txid)
|
||||
continue
|
||||
}
|
||||
|
||||
receiverAddress := false
|
||||
amount := 0.0
|
||||
for _, output := range tx.Vout {
|
||||
if output.ScriptPubKey.Address == b.walletAddress {
|
||||
receiverAddress = true
|
||||
amount = output.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !receiverAddress {
|
||||
continue
|
||||
}
|
||||
|
||||
if receiverAddress {
|
||||
log.Println("Transaction has been completed")
|
||||
notifChan <- domain.Notification{
|
||||
BlockHash: blockHash,
|
||||
Tx: txid,
|
||||
Amount: amount,
|
||||
DoneAt: time.Now(),
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-interrupt
|
||||
if err := q.Close(); err != nil {
|
||||
log.Println("Error closing ZeroMQ socket:", err)
|
||||
}
|
||||
}
|
||||
92
internal/services/btc/rpc_request.go
Normal file
92
internal/services/btc/rpc_request.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
GET_BLOCK RPCMethod = "getblock"
|
||||
GET_BALANCE RPCMethod = "getbalance"
|
||||
GET_RAW_TRANSACTION RPCMethod = "getrawtransaction"
|
||||
GET_TRANSACTION RPCMethod = "gettransaction"
|
||||
DECODE_RAW_TRANSACTION RPCMethod = "decoderawtransaction"
|
||||
LIST_ADDRESS_GROUPINGS RPCMethod = "listaddressgroupings"
|
||||
)
|
||||
|
||||
type RPCMethod string
|
||||
|
||||
type RPCRequest struct {
|
||||
host string
|
||||
auth string
|
||||
Jsonrpc string `json:"jsonrpc"`
|
||||
ID string `json:"id"`
|
||||
Method RPCMethod `json:"method"`
|
||||
Params []interface{} `json:"params,omitempty"`
|
||||
debug bool
|
||||
}
|
||||
|
||||
func (b *BitcoinService) NewRPCRequest() *RPCRequest {
|
||||
return &RPCRequest{
|
||||
host: b.host,
|
||||
auth: b.auth,
|
||||
Jsonrpc: "1.0",
|
||||
ID: "curltest",
|
||||
Params: make([]interface{}, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RPCRequest) WithDebug() *RPCRequest {
|
||||
r.debug = true
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *RPCRequest) WithMethod(method RPCMethod) *RPCRequest {
|
||||
r.Method = method
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *RPCRequest) Call(params ...interface{}) ([]byte, error) {
|
||||
if len(params) > 0 && params != nil {
|
||||
r.Params = append(r.Params, params...)
|
||||
}
|
||||
reqBody, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.debug {
|
||||
log.Printf("%s \n body: \n%s \n", r.Method, string(reqBody))
|
||||
}
|
||||
payload := bytes.NewReader(reqBody)
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest(http.MethodPost, r.host, payload)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Accept", "text/plain")
|
||||
req.Header.Add("Authorization", r.auth)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.debug {
|
||||
log.Printf("body response: \n%s \n", body)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
27
internal/services/btc/rpc_request_test.go
Normal file
27
internal/services/btc/rpc_request_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateSendTransaction(t *testing.T) {
|
||||
th := newTestHelper()
|
||||
req := th.b.
|
||||
NewRPCRequest().
|
||||
WithMethod(GET_BALANCE)
|
||||
require.NotNil(t, req)
|
||||
|
||||
resp, err := req.Call()
|
||||
require.NoError(t, err)
|
||||
strinResp := string(resp)
|
||||
require.NotEmpty(t, string(strinResp))
|
||||
log.Println(strinResp)
|
||||
|
||||
// unspent, err := NewListUnspentResponse(resp)
|
||||
// require.NoError(t, err)
|
||||
// require.Nil(t, unspent.Error)
|
||||
// require.Greater(t, len(unspent.Result), 0)
|
||||
}
|
||||
11
internal/services/btc/rpc_response.go
Normal file
11
internal/services/btc/rpc_response.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package btc
|
||||
|
||||
type RPCResponseError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type RPCResponse struct {
|
||||
Id string `json:"id"`
|
||||
Error *RPCResponseError `json:"error"`
|
||||
}
|
||||
22
internal/services/btc/test_helper.go
Normal file
22
internal/services/btc/test_helper.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"gitea.urkob.com/urko/btc-pay-checker/kit"
|
||||
"gitea.urkob.com/urko/btc-pay-checker/kit/cfg"
|
||||
)
|
||||
|
||||
type testHelper struct {
|
||||
testDeliveryAddress string
|
||||
config *cfg.Config
|
||||
b *BitcoinService
|
||||
}
|
||||
|
||||
func newTestHelper() *testHelper {
|
||||
config := cfg.NewConfig(kit.RootDir() + "/.test.env")
|
||||
testDeliveryAddress := config.WalletAddress
|
||||
return &testHelper{
|
||||
config: config,
|
||||
testDeliveryAddress: testDeliveryAddress,
|
||||
b: NewBitcoinService(config.RpcHost, config.RpcAuth, config.RpcZmq, config.WalletAddress),
|
||||
}
|
||||
}
|
||||
71
internal/services/btc/zmq.go
Normal file
71
internal/services/btc/zmq.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package btc
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
const (
|
||||
TOPIC_RAWTX = "rawtx"
|
||||
TOPIC_RAWBLOCK = "rawblock"
|
||||
TOPIC_HASHBLOCK = "hashblock"
|
||||
)
|
||||
|
||||
type Zmq struct {
|
||||
subscriber *zmq.Socket
|
||||
}
|
||||
|
||||
func (q *Zmq) Connect(address string) error {
|
||||
subscriber, err := zmq.NewSocket(zmq.SUB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating ZeroMQ subscriber: %w", err)
|
||||
}
|
||||
|
||||
if err := subscriber.Connect("tcp://" + address); err != nil {
|
||||
return fmt.Errorf("error connecting to ZeroMQ socket: %w", err)
|
||||
}
|
||||
|
||||
if err := subscriber.SetSubscribe(TOPIC_HASHBLOCK); err != nil {
|
||||
return fmt.Errorf("subscriber.SetSubscribe: %w", err)
|
||||
}
|
||||
|
||||
q.subscriber = subscriber
|
||||
log.Println("subsribed to: " + address)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *Zmq) Close() error {
|
||||
return q.subscriber.Close()
|
||||
}
|
||||
func (q *Zmq) Listen(blocksChan chan<- string) {
|
||||
if q.subscriber == nil {
|
||||
log.Fatalln("subscriber cannot be nil")
|
||||
return
|
||||
}
|
||||
log.Println("Listening for new blocks...")
|
||||
for {
|
||||
msgParts, err := q.subscriber.RecvMessageBytes(0)
|
||||
if err != nil {
|
||||
log.Println("Error receiving message:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(msgParts) < 2 {
|
||||
log.Println("Received message part is too short:", msgParts)
|
||||
continue
|
||||
}
|
||||
|
||||
topic := string(msgParts[0])
|
||||
switch topic {
|
||||
case TOPIC_HASHBLOCK:
|
||||
blockHash := hex.EncodeToString(msgParts[1])
|
||||
blocksChan <- blockHash
|
||||
case TOPIC_RAWTX:
|
||||
// TODO: do something with raw tx
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
215
internal/services/mail/mail.go
Normal file
215
internal/services/mail/mail.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
mime = "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
|
||||
okSubject = "Proof-of-Evidence record successful"
|
||||
failSubject = "Proof-of-Evidence record failed"
|
||||
templateError = "errror.html"
|
||||
templateClientConfirm = "client_confirm.html"
|
||||
templateProviderConfirm = "provider_confirm.html"
|
||||
)
|
||||
|
||||
type MailService struct {
|
||||
auth smtp.Auth
|
||||
host string
|
||||
port string
|
||||
from string
|
||||
templatesDir string
|
||||
tlsconfig *tls.Config
|
||||
}
|
||||
|
||||
type SendOK struct {
|
||||
Price float64
|
||||
ExplorerUrl string
|
||||
TxID string
|
||||
BlockHash string
|
||||
DocHash string
|
||||
To string
|
||||
}
|
||||
|
||||
type MailServiceConfig struct {
|
||||
Auth smtp.Auth
|
||||
Host string
|
||||
Port string
|
||||
From string // Sender email address
|
||||
TemplatesDir string // Should end with slash '/'
|
||||
}
|
||||
|
||||
var validCommonNames = []string{"ISRG Root X1", "R3", "DST Root CA X3"}
|
||||
|
||||
func NewMailService(config MailServiceConfig) *MailService {
|
||||
return &MailService{
|
||||
auth: config.Auth,
|
||||
host: config.Host,
|
||||
port: config.Port,
|
||||
from: config.From,
|
||||
templatesDir: config.TemplatesDir,
|
||||
tlsconfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: config.Host,
|
||||
VerifyConnection: func(cs tls.ConnectionState) error {
|
||||
|
||||
// // Check the server's common name
|
||||
// for _, cert := range cs.PeerCertificates {
|
||||
// log.Println("cert.DNSNames", cert.DNSNames)
|
||||
// if err := cert.VerifyHostname(config.Host); err != nil {
|
||||
// return fmt.Errorf("invalid common name: %w", err)
|
||||
// }
|
||||
// }
|
||||
|
||||
// Check the certificate chain
|
||||
opts := x509.VerifyOptions{
|
||||
Intermediates: x509.NewCertPool(),
|
||||
}
|
||||
for _, cert := range cs.PeerCertificates[1:] {
|
||||
opts.Intermediates.AddCert(cert)
|
||||
}
|
||||
_, err := cs.PeerCertificates[0].Verify(opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid certificate chain: %w", err)
|
||||
}
|
||||
|
||||
// Iterate over the certificates again to perform custom checks
|
||||
for _, cert := range cs.PeerCertificates {
|
||||
// Add your own custom checks here...
|
||||
if time.Now().After(cert.NotAfter) {
|
||||
return fmt.Errorf("certificate has expired")
|
||||
}
|
||||
if time.Now().Add(30 * 24 * time.Hour).After(cert.NotAfter) {
|
||||
return fmt.Errorf("certificate will expire within 30 days")
|
||||
}
|
||||
|
||||
if !slices.Contains(validCommonNames, cert.Issuer.CommonName) {
|
||||
return fmt.Errorf("certificate is not issued by a trusted CA")
|
||||
}
|
||||
// log.Println("cert.ExtKeyUsage", cert.ExtKeyUsage)
|
||||
// if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 || len(cert.ExtKeyUsage) == 0 || !slices.Contains(cert.ExtKeyUsage, x509.ExtKeyUsageServerAuth) {
|
||||
// log.Printf("%+v", cert)
|
||||
// return fmt.Errorf("certificate cannot be used for server authentication")
|
||||
// }
|
||||
if cert.PublicKeyAlgorithm != x509.RSA {
|
||||
return fmt.Errorf("unsupported public key algorithm")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
func (m *MailService) SendProviderConfirm(data SendOK) error {
|
||||
bts, err := os.ReadFile(m.templatesDir + templateProviderConfirm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.ReadFile: %s", err)
|
||||
}
|
||||
template := strings.Replace(string(bts), "{{explorer_url}}", data.ExplorerUrl, -1)
|
||||
template = strings.Replace(template, "{{tx_id}}", data.TxID, -1)
|
||||
template = strings.Replace(template, "{{block_hash}}", data.BlockHash, -1)
|
||||
template = strings.Replace(template, "{{doc_hash}}", data.DocHash, -1)
|
||||
template = strings.Replace(template, "{{support_email}}", m.from, -1)
|
||||
msg := []byte(m.messageWithHeaders(okSubject, data.To, template))
|
||||
return m.send(data.To, msg)
|
||||
}
|
||||
|
||||
func (m *MailService) SendClientConfirm(data SendOK) error {
|
||||
bts, err := os.ReadFile(m.templatesDir + templateClientConfirm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.ReadFile: %s", err)
|
||||
}
|
||||
template := strings.Replace(string(bts), "{{explorer_url}}", data.ExplorerUrl, -1)
|
||||
template = strings.Replace(template, "{{tx_id}}", data.TxID, -1)
|
||||
template = strings.Replace(template, "{{block_hash}}", data.BlockHash, -1)
|
||||
template = strings.Replace(template, "{{doc_hash}}", data.DocHash, -1)
|
||||
template = strings.Replace(template, "{{support_email}}", m.from, -1)
|
||||
msg := []byte(m.messageWithHeaders(okSubject, data.To, template))
|
||||
return m.send(data.To, msg)
|
||||
}
|
||||
|
||||
func (m *MailService) SendFail(data SendOK) error {
|
||||
//templateError
|
||||
bts, err := os.ReadFile(m.templatesDir + templateError)
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.ReadFile: %s", err)
|
||||
}
|
||||
template := strings.Replace(string(bts), "{{explorer_url}}", data.ExplorerUrl, -1)
|
||||
template = strings.Replace(template, "{{tx_id}}", data.TxID, -1)
|
||||
template = strings.Replace(template, "{{block_hash}}", data.BlockHash, -1)
|
||||
template = strings.Replace(template, "{{doc_hash}}", data.DocHash, -1)
|
||||
template = strings.Replace(template, "{{support_email}}", m.from, -1)
|
||||
// TODO: Alert client too
|
||||
msg := []byte(m.messageWithHeaders(okSubject, data.To, template))
|
||||
return m.send(data.To, msg)
|
||||
}
|
||||
|
||||
func (m *MailService) send(to string, msg []byte) error {
|
||||
c, err := smtp.Dial(m.host + ":" + m.port)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DIAL: %s", err)
|
||||
}
|
||||
|
||||
if err = c.StartTLS(m.tlsconfig); err != nil {
|
||||
return fmt.Errorf("c.StartTLS: %s", err)
|
||||
}
|
||||
|
||||
// Auth
|
||||
if err = c.Auth(m.auth); err != nil {
|
||||
return fmt.Errorf("c.Auth: %s", err)
|
||||
}
|
||||
|
||||
// To && From
|
||||
if err = c.Mail(m.from); err != nil {
|
||||
return fmt.Errorf("c.Mail: %s", err)
|
||||
}
|
||||
|
||||
if err = c.Rcpt(to); err != nil {
|
||||
return fmt.Errorf("c.Rcpt: %s", err)
|
||||
}
|
||||
|
||||
// Data
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("c.Data: %s", err)
|
||||
}
|
||||
|
||||
_, err = w.Write(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("w.Write: %s", err)
|
||||
}
|
||||
|
||||
if err = w.Close(); err != nil {
|
||||
return fmt.Errorf("w.Close: %s", err)
|
||||
}
|
||||
|
||||
if err = c.Quit(); err != nil {
|
||||
return fmt.Errorf("w.Quit: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MailService) messageWithHeaders(subject, to, body string) string {
|
||||
headers := make(map[string]string)
|
||||
headers["From"] = m.from
|
||||
headers["To"] = to
|
||||
headers["Subject"] = subject
|
||||
headers["MIME-Version"] = "1.0"
|
||||
|
||||
message := ""
|
||||
for k, v := range headers {
|
||||
message += fmt.Sprintf("%s: %s\r\n", k, v)
|
||||
}
|
||||
message += "Content-Type: text/html; charset=utf-8\r\n" + body
|
||||
|
||||
return message
|
||||
}
|
||||
52
internal/services/mail/mail_test.go
Normal file
52
internal/services/mail/mail_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"net/smtp"
|
||||
"testing"
|
||||
|
||||
"gitea.urkob.com/urko/btc-pay-checker/kit"
|
||||
"gitea.urkob.com/urko/btc-pay-checker/kit/cfg"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
mailSrv *MailService
|
||||
config *cfg.Config
|
||||
)
|
||||
|
||||
func init() {
|
||||
config = cfg.NewConfig(kit.RootDir() + "/.test.env")
|
||||
mailSrv = NewMailService(
|
||||
MailServiceConfig{
|
||||
Auth: smtp.PlainAuth("", config.MailUser, config.MailPassword, config.MailHost),
|
||||
Host: config.MailHost,
|
||||
Port: config.MailPort,
|
||||
From: config.MailFrom,
|
||||
TemplatesDir: config.MailTemplatesDir,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func Test_mailService_SendOK(t *testing.T) {
|
||||
dto := SendOK{
|
||||
Price: 10.2,
|
||||
ExplorerUrl: "test",
|
||||
TxID: "test-hash",
|
||||
To: config.MailTo,
|
||||
}
|
||||
|
||||
err := mailSrv.SendClientConfirm(dto)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_mailService_SendConfirm(t *testing.T) {
|
||||
dto := SendOK{
|
||||
ExplorerUrl: "test",
|
||||
TxID: "test-hash",
|
||||
BlockHash: "block-hash",
|
||||
To: config.MailTo,
|
||||
}
|
||||
|
||||
err := mailSrv.SendProviderConfirm(dto)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
1
internal/services/mail/templates/client_confirm.html
Normal file
1
internal/services/mail/templates/client_confirm.html
Normal file
@@ -0,0 +1 @@
|
||||
TODO:
|
||||
1
internal/services/mail/templates/error.html
Normal file
1
internal/services/mail/templates/error.html
Normal file
@@ -0,0 +1 @@
|
||||
TODO:
|
||||
1
internal/services/mail/templates/provider_confirm.html
Normal file
1
internal/services/mail/templates/provider_confirm.html
Normal file
@@ -0,0 +1 @@
|
||||
TODO:
|
||||
54
internal/services/order.go
Normal file
54
internal/services/order.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/domain"
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/platform/mongodb/order"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const defaultExpirationTime = time.Minute * 30
|
||||
|
||||
type Order struct {
|
||||
repo *order.Repo
|
||||
expiration time.Duration
|
||||
}
|
||||
|
||||
func NewOrder(repo *order.Repo) *Order {
|
||||
return &Order{
|
||||
repo: repo,
|
||||
expiration: defaultExpirationTime,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Order) WithExpiration(expiration time.Duration) *Order {
|
||||
o.expiration = expiration
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *Order) NewOrder(ctx context.Context, OrderID string, ClientID string, amount float64) (*domain.Order, error) {
|
||||
order := domain.Order{
|
||||
ID: primitive.NewObjectID(),
|
||||
OrderID: OrderID,
|
||||
ClientID: ClientID,
|
||||
Amount: amount,
|
||||
PaidAt: time.Time{},
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(o.expiration),
|
||||
}
|
||||
_, err := o.repo.Insert(ctx, &order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &order, err
|
||||
}
|
||||
|
||||
func (o *Order) FromAmount(ctx context.Context, amount float64, timestamp time.Time) (*domain.Order, error) {
|
||||
return o.repo.FromAmount(ctx, amount, timestamp)
|
||||
}
|
||||
|
||||
func (o *Order) OrderCompleted(ctx context.Context, order *domain.Order) error {
|
||||
return o.repo.OrderCompleted(ctx, order)
|
||||
}
|
||||
143
internal/services/price/conversor.go
Normal file
143
internal/services/price/conversor.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package price
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/domain"
|
||||
"gitea.urkob.com/urko/btc-pay-checker/kit"
|
||||
)
|
||||
|
||||
type CoinPriceResponse struct {
|
||||
Coins map[string]map[string]interface{} `json:"coins"`
|
||||
}
|
||||
|
||||
type PriceConversor struct {
|
||||
apiUrl string
|
||||
dollarRateApi string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewPriceConversor(apiUrl, dollarRateApi string) *PriceConversor {
|
||||
return &PriceConversor{
|
||||
apiUrl: apiUrl,
|
||||
client: &http.Client{},
|
||||
dollarRateApi: dollarRateApi,
|
||||
}
|
||||
}
|
||||
|
||||
type usdConversorResponse struct {
|
||||
Result string `json:"result"`
|
||||
Provider string `json:"provider"`
|
||||
Documentation string `json:"documentation"`
|
||||
TermsOfUse string `json:"terms_of_use"`
|
||||
TimeLastUpdateUnix int `json:"time_last_update_unix"`
|
||||
TimeLastUpdateUtc string `json:"time_last_update_utc"`
|
||||
TimeNextUpdateUnix int `json:"time_next_update_unix"`
|
||||
TimeNextUpdateUtc string `json:"time_next_update_utc"`
|
||||
TimeEolUnix int `json:"time_eol_unix"`
|
||||
BaseCode string `json:"base_code"`
|
||||
Rates map[string]float64 `json:"rates"`
|
||||
}
|
||||
|
||||
func (p *PriceConversor) USDTo(c domain.Coin) (float64, error) {
|
||||
usd := 0.0
|
||||
reqURL := fmt.Sprintf(p.dollarRateApi)
|
||||
req, err := http.NewRequest("GET", reqURL, nil)
|
||||
if err != nil {
|
||||
return usd, fmt.Errorf("http.NewRequest: %s", err)
|
||||
}
|
||||
|
||||
kit.WithJSONHeaders(req)
|
||||
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return usd, fmt.Errorf("client.Do: %s", err)
|
||||
}
|
||||
|
||||
bts, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return usd, fmt.Errorf("iokit.ReadAll: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return usd, fmt.Errorf("error %d: %s", resp.StatusCode, string(bts))
|
||||
}
|
||||
|
||||
var respBody usdConversorResponse
|
||||
if err = json.Unmarshal(bts, &respBody); err != nil {
|
||||
return usd, fmt.Errorf("json.Unmarshal: %s", err)
|
||||
}
|
||||
|
||||
v, ok := respBody.Rates[string(c)]
|
||||
if !ok {
|
||||
return usd, fmt.Errorf("coin isn't found")
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (p *PriceConversor) USD(c domain.Coin) (float64, error) {
|
||||
usd := 0.0
|
||||
reqURL := fmt.Sprintf(p.apiUrl+"/prices/current/%s", c)
|
||||
req, err := http.NewRequest("GET", reqURL, nil)
|
||||
if err != nil {
|
||||
return usd, fmt.Errorf("http.NewRequest: %s", err)
|
||||
}
|
||||
|
||||
kit.WithJSONHeaders(req)
|
||||
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return usd, fmt.Errorf("client.Do: %s", err)
|
||||
}
|
||||
|
||||
bts, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return usd, fmt.Errorf("iokit.ReadAll: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return usd, fmt.Errorf("error %d: %s", resp.StatusCode, string(bts))
|
||||
}
|
||||
|
||||
var respBody CoinPriceResponse
|
||||
if err = json.Unmarshal(bts, &respBody); err != nil {
|
||||
return usd, fmt.Errorf("json.Unmarshal: %s", err)
|
||||
}
|
||||
|
||||
if _, hasKey := respBody.Coins[string(c)]; !hasKey {
|
||||
return usd, fmt.Errorf("coin isn't found")
|
||||
}
|
||||
|
||||
if _, hasKey := respBody.Coins[string(c)]["price"]; !hasKey {
|
||||
return usd, fmt.Errorf("coin price isn't found")
|
||||
}
|
||||
|
||||
usd, ok := respBody.Coins[string(c)]["price"].(float64)
|
||||
if !ok {
|
||||
return usd, fmt.Errorf("cannot cast price to float64")
|
||||
}
|
||||
|
||||
return usd, nil
|
||||
}
|
||||
|
||||
func (p *PriceConversor) UsdToBtc(usdAmount float64) (float64, error) {
|
||||
usd, err := p.USD(domain.CoinBTC)
|
||||
if err != nil {
|
||||
return usdAmount, fmt.Errorf("USDToBtc: %s", err)
|
||||
}
|
||||
|
||||
return usdAmount / usd, nil
|
||||
}
|
||||
|
||||
func (p *PriceConversor) BtcToUsd(btcAmount float64) (float64, error) {
|
||||
usd, err := p.USD(domain.CoinBTC)
|
||||
if err != nil {
|
||||
return btcAmount, fmt.Errorf("USDToBtc: %s", err)
|
||||
}
|
||||
|
||||
return btcAmount * usd, nil
|
||||
}
|
||||
52
internal/services/price/conversor_test.go
Normal file
52
internal/services/price/conversor_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package price
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"gitea.urkob.com/urko/btc-pay-checker/internal/domain"
|
||||
"gitea.urkob.com/urko/btc-pay-checker/kit"
|
||||
"gitea.urkob.com/urko/btc-pay-checker/kit/cfg"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConversor(t *testing.T) {
|
||||
config := cfg.NewConfig(kit.RootDir() + "/.test.env")
|
||||
p := NewPriceConversor(config.ConversorApi, config.DollarRateApi)
|
||||
|
||||
usd, err := p.USD(domain.CoinBTC)
|
||||
require.NoError(t, err)
|
||||
log.Println("$", usd)
|
||||
require.Greater(t, usd, 0.0)
|
||||
|
||||
usd, err = p.USD(domain.CoinBTC)
|
||||
require.NoError(t, err)
|
||||
log.Println("$", usd)
|
||||
require.Greater(t, usd, 0.0)
|
||||
|
||||
usdAmount := 100.0
|
||||
btcPrice, err := p.UsdToBtc(usdAmount)
|
||||
require.NoError(t, err)
|
||||
log.Println("btcPrice", btcPrice)
|
||||
require.Equal(t, btcPrice, usdAmount/usd)
|
||||
|
||||
usd, err = p.USD(domain.CoinBTC)
|
||||
require.NoError(t, err)
|
||||
log.Println("$", usd)
|
||||
require.Greater(t, usd, 0.0)
|
||||
|
||||
btcAmount := 0.0001
|
||||
usdPrice, err := p.BtcToUsd(btcAmount)
|
||||
require.NoError(t, err)
|
||||
log.Println("usdPrice", usdPrice)
|
||||
require.Equal(t, usdPrice, btcAmount*usd)
|
||||
|
||||
euroRate, err := p.USDTo(domain.FiatCurrencyEuro)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, euroRate, 0.0)
|
||||
log.Println("euroRate", euroRate)
|
||||
usdAmount = 150.0
|
||||
converted := usdAmount * euroRate
|
||||
require.Greater(t, converted, 0.0)
|
||||
require.Equal(t, usdAmount, converted/euroRate)
|
||||
}
|
||||
Reference in New Issue
Block a user