feat: set up project
This commit is contained in:
parent
fac61db3f1
commit
b426a36570
|
@ -1,4 +1,5 @@
|
||||||
.env
|
.env
|
||||||
|
.test.env
|
||||||
.vscode
|
.vscode
|
||||||
coverage
|
coverage
|
||||||
.notes
|
.notes
|
||||||
|
|
6
LICENSE
6
LICENSE
|
@ -1,12 +1,10 @@
|
||||||
---- Definitions ----
|
---- Definitions ----
|
||||||
license means right to use
|
license means right to use
|
||||||
author means who had initial idea, who research about jurisprudence and the who who started, in this cas : Urko: Bein.
|
author means who had initial idea, who did research and who started this project, in this case : Urko: Bein.
|
||||||
contributors means every man who has helped to improve this software
|
contributors means every man who has helped to improve this software
|
||||||
|
|
||||||
|
|
||||||
Everybody is invited to contribute to improve this project and the main idea.
|
Everybody is invited to contribute to improve this project and the main idea.
|
||||||
This idea which is to help the community to present a prima facie, https://www.law.cornell.edu/wex/prima_facie,
|
This idea which is to help the community to have a bitcoin pay checker for their ecommerce platforms
|
||||||
proof of evidence from unmodified documents based on hash algorythm
|
|
||||||
published in the blockchain to be a public constructive notice: See https://www.law.cornell.edu/wex/constructive_notice
|
|
||||||
|
|
||||||
For the benefit of all men and women by the grace of YAHWEH!
|
For the benefit of all men and women by the grace of YAHWEH!
|
|
@ -0,0 +1,13 @@
|
||||||
|
COVERAGE_DIR=coverage
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golangci-lint run ./...
|
||||||
|
goreportcard:
|
||||||
|
goreportcard-cli -v
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
test-coverage:
|
||||||
|
rm -rf ${COVERAGE_DIR}
|
||||||
|
mkdir ${COVERAGE_DIR}
|
||||||
|
go test -v -coverprofile ${COVERAGE_DIR}/cover.out ./...
|
||||||
|
go tool cover -html ${COVERAGE_DIR}/cover.out -o ${COVERAGE_DIR}/cover.html
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Bitcoin Payment Checker
|
||||||
|
|
||||||
|
Bitcoin Payment Checker is a Go application that allows clients to pay for orders using Bitcoin. The application calculates the Bitcoin equivalent of the order amount, provides a wallet address for payment, and tracks the payment status.
|
||||||
|
|
||||||
|
## Application Structure
|
||||||
|
|
||||||
|
The application is structured into several packages:
|
||||||
|
|
||||||
|
- `handler`: Contains the HTTP handlers for the application. The `OrderHandler` is responsible for processing new orders and calculating the Bitcoin price.
|
||||||
|
- `services`: Contains various services used by the application, such as the `Order` service for managing orders and the `PriceConversor` service for converting USD to Bitcoin.
|
||||||
|
- `main`: The entry point of the application. It sets up the database connection, initializes the services, and starts the HTTP server.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- `handler/order.go`: Contains the `OrderHandler` which processes new orders. It calculates the Bitcoin price of the order, stores the order in the database, and renders an HTML page with the order details.
|
||||||
|
- `main.go`: The entry point of the application. It sets up the database connection, initializes the services, and starts the HTTP server.
|
||||||
|
|
||||||
|
## Running the Application
|
||||||
|
|
||||||
|
To run the application, you need to have Go installed. Then, you can run the application using the `go run` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start the application and listen for HTTP requests on the configured port.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The application uses the following environment variables:
|
||||||
|
```
|
||||||
|
PAY_CHECKER_ENV: The environment the application is running in. If set to "dev", the application will load configuration from a .env file.
|
||||||
|
DB_ADDRESS: The address of the MongoDB database.
|
||||||
|
DB_NAME: The name of the MongoDB database.
|
||||||
|
ORDERS_COLLECTION: The name of the MongoDB collection for orders.
|
||||||
|
CONVERSOR_API: The API used for converting USD to Bitcoin.
|
||||||
|
RPC_HOST, RPC_AUTH, RPC_ZMQ, WALLET_ADDRESS: Configuration for the Bitcoin service.
|
||||||
|
MAIL_USER, MAIL_PASSWORD, MAIL_HOST, MAIL_PORT, MAIL_FROM, MAIL_TEMPLATES_DIR: Configuration for the mail service.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
The application uses several external packages:
|
||||||
|
|
||||||
|
gofiber/fiber/v2: For building the HTTP server and handling requests.
|
||||||
|
go.mongodb.org/mongo-driver/mongo: For connecting to and interacting with MongoDB.
|
||||||
|
net/smtp: For sending emails.
|
|
@ -0,0 +1,108 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/smtp"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.urkob.com/urko/btc-pay-checker/internal/api"
|
||||||
|
"gitea.urkob.com/urko/btc-pay-checker/internal/platform/mongodb/order"
|
||||||
|
"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"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
envFile := ""
|
||||||
|
if os.Getenv("PAY_CHECKER_ENV") == "dev" {
|
||||||
|
envFile = ".env"
|
||||||
|
}
|
||||||
|
config := cfg.NewConfig(envFile)
|
||||||
|
log.SetFlags(log.Lmicroseconds)
|
||||||
|
if config.LogFile {
|
||||||
|
logFileName := fmt.Sprintf("%s.txt", time.Now().Format(strings.ReplaceAll(time.RFC1123Z, ":", "_")))
|
||||||
|
f, err := os.OpenFile(logFileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
log.SetOutput(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbOpts := options.Client()
|
||||||
|
dbOpts.ApplyURI(config.DbAddress)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(signalContext(context.Background()))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := mongo.Connect(ctx, dbOpts)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("mongo.NewClient: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("mongodb client is connected")
|
||||||
|
|
||||||
|
ordersCollection := client.
|
||||||
|
Database(config.DbName).
|
||||||
|
Collection(config.OrdersCollection)
|
||||||
|
orderRepo := order.NewRepo(ordersCollection)
|
||||||
|
orderSrv := services.NewOrder(orderRepo)
|
||||||
|
|
||||||
|
priceSrv := price.NewPriceConversor(config.ConversorApi, config.ConversorApi)
|
||||||
|
btcSrv := btc.NewBitcoinService(config.RpcHost, config.RpcAuth, config.RpcZmq, config.WalletAddress).WithTestnet()
|
||||||
|
mailSrv := mail.NewMailService(
|
||||||
|
mail.MailServiceConfig{
|
||||||
|
Auth: smtp.PlainAuth("", config.MailUser, config.MailPassword, config.MailHost),
|
||||||
|
Host: config.MailHost,
|
||||||
|
Port: config.MailPort,
|
||||||
|
From: config.MailFrom,
|
||||||
|
TemplatesDir: config.MailTemplatesDir,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
restServer := api.NewRestServer(config, orderSrv, btcSrv, priceSrv, mailSrv)
|
||||||
|
go func() {
|
||||||
|
if err = restServer.Start(ctx, config.ApiPort, config.Views); err != nil {
|
||||||
|
panic(fmt.Errorf("restServer.Start: %w", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
log.Println("on shutdown")
|
||||||
|
if restServer != nil {
|
||||||
|
if err := restServer.Shutdown(); err != nil {
|
||||||
|
panic(fmt.Errorf("restServer.Shutdown: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Println("gracefully shutdown")
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalContext(ctx context.Context) context.Context {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
sigs := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigs, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Println("listening for shutdown signal")
|
||||||
|
<-sigs
|
||||||
|
log.Println("shutdown signal received")
|
||||||
|
signal.Stop(sigs)
|
||||||
|
close(sigs)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9edbe;
|
||||||
|
color: #856404;
|
||||||
|
border: 1px solid #ffeeba;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="alert">
|
||||||
|
<p><strong>Unexpected error: {{message}}</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,104 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9edbe;
|
||||||
|
color: #856404;
|
||||||
|
border: 1px solid #ffeeba;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="alert">
|
||||||
|
<h1>Bitcoin Payment</h1>
|
||||||
|
<p>Please send the exact amount of Bitcoin to the provided wallet address. Your order will be processed once the
|
||||||
|
transaction is confirmed.</p>
|
||||||
|
</div>
|
||||||
|
<p id="countdown"><strong>Time Remaining:</strong></p>
|
||||||
|
<div class="details">
|
||||||
|
<h2>Order Details</h2>
|
||||||
|
<p><strong>Order ID:</strong> {{order_id}}</p>
|
||||||
|
<p><strong>Amount:</strong> {{amount}} BTC</p>
|
||||||
|
<p><strong>Wallet Address:</strong> {{wallet_address}}</p>
|
||||||
|
<p id="dueDate"><strong>Payment Due:</strong></p>
|
||||||
|
<p id="countdown"><strong>Time Remaining:</strong> </p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// Get the expires_at time from your server and convert it to a JavaScript Date object
|
||||||
|
const dueDate = new Date("{{expires_at}}");
|
||||||
|
|
||||||
|
// Format the date and time
|
||||||
|
const formattedDate = dueDate.toLocaleDateString() + " " + dueDate.toLocaleTimeString();
|
||||||
|
|
||||||
|
// Display the result in the element with id="dueDate"
|
||||||
|
document.getElementById("dueDate").innerHTML = "<strong>Payment Due:</strong> " + formattedDate;
|
||||||
|
|
||||||
|
// Get the expires_at time from your server and convert it to a JavaScript Date object
|
||||||
|
const countDownDate = new Date("{{expires_at}}").getTime();
|
||||||
|
|
||||||
|
// Update the countdown every 1 second
|
||||||
|
const countdownInterval = setInterval(function () {
|
||||||
|
|
||||||
|
// Get today's date and time
|
||||||
|
const now = new Date().getTime();
|
||||||
|
|
||||||
|
// Find the distance between now and the count down date
|
||||||
|
const distance = countDownDate - now;
|
||||||
|
|
||||||
|
// Time calculations for days, hours, minutes and seconds
|
||||||
|
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
|
||||||
|
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
||||||
|
|
||||||
|
// Display the result in the element with id="countdown"
|
||||||
|
// document.getElementById("countdown").innerHTML = "<strong>Time Remaining:</strong> " + days + "d " + hours + "h " + minutes + "m " + seconds + "s ";
|
||||||
|
document.getElementById("countdown").innerHTML = "<strong>Time Remaining:</strong> " + minutes + "m " + seconds + "s ";
|
||||||
|
|
||||||
|
// If the countdown is finished, write some text
|
||||||
|
if (distance < 0) {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
document.getElementById("countdown").innerHTML = "EXPIRED";
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,47 @@
|
||||||
|
module gitea.urkob.com/urko/btc-pay-checker
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
gitea.urkob.com/urko/go-root-dir v0.0.0-20230311113851-2f6d4355888a
|
||||||
|
github.com/docker/go-units v0.5.0
|
||||||
|
github.com/gofiber/fiber/v2 v2.48.0
|
||||||
|
github.com/gofiber/template/handlebars/v2 v2.1.4
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0
|
||||||
|
github.com/pebbe/zmq4 v1.2.10
|
||||||
|
github.com/stretchr/testify v1.8.4
|
||||||
|
go.mongodb.org/mongo-driver v1.12.0
|
||||||
|
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||||
|
github.com/aymerick/raymond v2.0.2+incompatible // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/gofiber/template v1.8.2 // indirect
|
||||||
|
github.com/gofiber/utils v1.1.0 // indirect
|
||||||
|
github.com/golang/snappy v0.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.16.5 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
|
||||||
|
github.com/philhofer/fwd v1.1.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/tinylib/msgp v1.1.8 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasthttp v1.48.0 // indirect
|
||||||
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||||
|
golang.org/x/crypto v0.7.0 // indirect
|
||||||
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
|
golang.org/x/sys v0.10.0 // indirect
|
||||||
|
golang.org/x/text v0.8.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,121 @@
|
||||||
|
gitea.urkob.com/urko/go-root-dir v0.0.0-20230311113851-2f6d4355888a h1:s73cd3bRR6v0LGiBei841iIolbBJN2tbkUwN54X9vVg=
|
||||||
|
gitea.urkob.com/urko/go-root-dir v0.0.0-20230311113851-2f6d4355888a/go.mod h1:mU9nRHl70tBhJFbgKotpoXMV+s0wx+1uJ988p4oEpSo=
|
||||||
|
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||||
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0=
|
||||||
|
github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/gofiber/fiber/v2 v2.48.0 h1:cRVMCb9aUJDsyHxGFLwz/sGzDggdailZZyptU9F9cU0=
|
||||||
|
github.com/gofiber/fiber/v2 v2.48.0/go.mod h1:xqJgfqrc23FJuqGOW6DVgi3HyZEm2Mn9pRqUb2kHSX8=
|
||||||
|
github.com/gofiber/template v1.8.2 h1:PIv9s/7Uq6m+Fm2MDNd20pAFFKt5wWs7ZBd8iV9pWwk=
|
||||||
|
github.com/gofiber/template v1.8.2/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
|
||||||
|
github.com/gofiber/template/handlebars/v2 v2.1.4 h1:m/GwEnzv5bpifOg9BrpCIzkhD2GU10E5oZjI8NdDbYY=
|
||||||
|
github.com/gofiber/template/handlebars/v2 v2.1.4/go.mod h1:bb6ip6ZEgBqKSdZcbnFLlfL8PcCRslnX6WgpcxVBiTE=
|
||||||
|
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
|
||||||
|
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
|
||||||
|
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||||
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||||
|
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
|
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
||||||
|
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
|
||||||
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||||
|
github.com/pebbe/zmq4 v1.2.10 h1:wQkqRZ3CZeABIeidr3e8uQZMMH5YAykA/WN0L5zkd1c=
|
||||||
|
github.com/pebbe/zmq4 v1.2.10/go.mod h1:nqnPueOapVhE2wItZ0uOErngczsJdLOGkebMxaO8r48=
|
||||||
|
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||||
|
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||||
|
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc=
|
||||||
|
github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||||
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
|
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||||
|
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE=
|
||||||
|
go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||||
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
|
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
|
||||||
|
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||||
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||||
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -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)
|
||||||
|
}
|
|
@ -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),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
type Coin string
|
||||||
|
|
||||||
|
const CoinBTC Coin = "coingecko:bitcoin"
|
||||||
|
|
||||||
|
type FiatCurrency string
|
||||||
|
|
||||||
|
const FiatCurrencyDollar = "USD"
|
||||||
|
const FiatCurrencyEuro = "EUR"
|
|
@ -0,0 +1,10 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Notification struct {
|
||||||
|
BlockHash string
|
||||||
|
Tx string
|
||||||
|
Amount float64
|
||||||
|
DoneAt time.Time
|
||||||
|
}
|
|
@ -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"`
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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"`
|
||||||
|
}
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
TODO:
|
|
@ -0,0 +1 @@
|
||||||
|
TODO:
|
|
@ -0,0 +1 @@
|
||||||
|
TODO:
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package cfg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/kelseyhightower/envconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
LogFile bool `required:"false" split_words:"true"`
|
||||||
|
Host string `required:"true" split_words:"true"`
|
||||||
|
DbAddress string `required:"true" split_words:"true"`
|
||||||
|
DbName string `required:"true" split_words:"true"`
|
||||||
|
OrdersCollection string `required:"true" split_words:"true"`
|
||||||
|
RpcZmq string `required:"true" split_words:"true"`
|
||||||
|
RpcAuth string `required:"true" split_words:"true"`
|
||||||
|
RpcHost string `required:"true" split_words:"true"`
|
||||||
|
WalletAddress string `required:"true" split_words:"true"`
|
||||||
|
ApiPort string `required:"true" split_words:"true"`
|
||||||
|
Views string `required:"true" split_words:"true"`
|
||||||
|
ConversorApi string `required:"true" split_words:"true"`
|
||||||
|
DollarRateApi string `required:"true" split_words:"true"`
|
||||||
|
MailHost string `required:"true" split_words:"true"`
|
||||||
|
MailPort string `required:"true" split_words:"true"`
|
||||||
|
MailUser string `required:"true" split_words:"true"`
|
||||||
|
MailPassword string `required:"true" split_words:"true"`
|
||||||
|
MailFrom string `required:"true" split_words:"true"`
|
||||||
|
MailTemplatesDir string `required:"true" split_words:"true"`
|
||||||
|
MailTo string `required:"false" split_words:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig(envFile string) *Config {
|
||||||
|
if envFile != "" {
|
||||||
|
err := godotenv.Load(envFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("godotenv.Load:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
err := envconfig.Process("", cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("envconfig.Process: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package kit
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
const JsonHeader = "application/json"
|
||||||
|
|
||||||
|
func WithJSONHeaders(r *http.Request) {
|
||||||
|
r.Header.Add("Accept", JsonHeader)
|
||||||
|
r.Header.Add("Content-Type", JsonHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHeaders(r *http.Request, headers map[string]string) {
|
||||||
|
for k, v := range headers {
|
||||||
|
r.Header.Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package kit
|
||||||
|
|
||||||
|
import (
|
||||||
|
root_dir "gitea.urkob.com/urko/go-root-dir"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RootDir() string {
|
||||||
|
return root_dir.RootDir("btc-pay-checker")
|
||||||
|
}
|
Loading…
Reference in New Issue