diff --git a/.gitignore b/.gitignore
index 627e865..61e074e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.env
+.test.env
.vscode
coverage
.notes
diff --git a/LICENSE b/LICENSE
index ca5a576..8e445b1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,12 +1,10 @@
---- Definitions ----
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
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,
-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
+This idea which is to help the community to have a bitcoin pay checker for their ecommerce platforms
For the benefit of all men and women by the grace of YAHWEH!
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..152717e
--- /dev/null
+++ b/Makefile
@@ -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
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..51452c9
--- /dev/null
+++ b/README.md
@@ -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.
\ No newline at end of file
diff --git a/cmd/http/server/main.go b/cmd/http/server/main.go
new file mode 100644
index 0000000..883e318
--- /dev/null
+++ b/cmd/http/server/main.go
@@ -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
+}
diff --git a/cmd/http/views/error.hbs b/cmd/http/views/error.hbs
new file mode 100644
index 0000000..ac82f99
--- /dev/null
+++ b/cmd/http/views/error.hbs
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
Unexpected error: {{message}}
+
+
+
+
\ No newline at end of file
diff --git a/cmd/http/views/order.hbs b/cmd/http/views/order.hbs
new file mode 100644
index 0000000..2d6b0ed
--- /dev/null
+++ b/cmd/http/views/order.hbs
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
Bitcoin Payment
+
Please send the exact amount of Bitcoin to the provided wallet address. Your order will be processed once the
+ transaction is confirmed.
+
+
Time Remaining:
+
+
Order Details
+
Order ID: {{order_id}}
+
Amount: {{amount}} BTC
+
Wallet Address: {{wallet_address}}
+
Payment Due:
+
Time Remaining:
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..f84f782
--- /dev/null
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..5bee8f6
--- /dev/null
+++ b/go.sum
@@ -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=
diff --git a/internal/api/handler/helper.go b/internal/api/handler/helper.go
new file mode 100644
index 0000000..5e23aaa
--- /dev/null
+++ b/internal/api/handler/helper.go
@@ -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)
+}
diff --git a/internal/api/handler/payment.go b/internal/api/handler/payment.go
new file mode 100644
index 0000000..0516dec
--- /dev/null
+++ b/internal/api/handler/payment.go
@@ -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),
+ },
+ )
+}
diff --git a/internal/api/server.go b/internal/api/server.go
new file mode 100644
index 0000000..092cde6
--- /dev/null
+++ b/internal/api/server.go
@@ -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
+}
diff --git a/internal/domain/coin.go b/internal/domain/coin.go
new file mode 100644
index 0000000..d61e6d5
--- /dev/null
+++ b/internal/domain/coin.go
@@ -0,0 +1,10 @@
+package domain
+
+type Coin string
+
+const CoinBTC Coin = "coingecko:bitcoin"
+
+type FiatCurrency string
+
+const FiatCurrencyDollar = "USD"
+const FiatCurrencyEuro = "EUR"
diff --git a/internal/domain/notification.go b/internal/domain/notification.go
new file mode 100644
index 0000000..f5ec8ec
--- /dev/null
+++ b/internal/domain/notification.go
@@ -0,0 +1,10 @@
+package domain
+
+import "time"
+
+type Notification struct {
+ BlockHash string
+ Tx string
+ Amount float64
+ DoneAt time.Time
+}
diff --git a/internal/domain/order.go b/internal/domain/order.go
new file mode 100644
index 0000000..afc38db
--- /dev/null
+++ b/internal/domain/order.go
@@ -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"`
+}
diff --git a/internal/platform/mongodb/order/repository.go b/internal/platform/mongodb/order/repository.go
new file mode 100644
index 0000000..d9a2a55
--- /dev/null
+++ b/internal/platform/mongodb/order/repository.go
@@ -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
+}
diff --git a/internal/services/btc/btc.go b/internal/services/btc/btc.go
new file mode 100644
index 0000000..2c58d77
--- /dev/null
+++ b/internal/services/btc/btc.go
@@ -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
+}
diff --git a/internal/services/btc/decode_raw_transaction.go b/internal/services/btc/decode_raw_transaction.go
new file mode 100644
index 0000000..5328a16
--- /dev/null
+++ b/internal/services/btc/decode_raw_transaction.go
@@ -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
+}
diff --git a/internal/services/btc/get_balance.go b/internal/services/btc/get_balance.go
new file mode 100644
index 0000000..2c12402
--- /dev/null
+++ b/internal/services/btc/get_balance.go
@@ -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
+}
diff --git a/internal/services/btc/get_balance_test.go b/internal/services/btc/get_balance_test.go
new file mode 100644
index 0000000..f1a001f
--- /dev/null
+++ b/internal/services/btc/get_balance_test.go
@@ -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)
+}
diff --git a/internal/services/btc/get_block.go b/internal/services/btc/get_block.go
new file mode 100644
index 0000000..65ed7dc
--- /dev/null
+++ b/internal/services/btc/get_block.go
@@ -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
+}
diff --git a/internal/services/btc/get_block_test.go b/internal/services/btc/get_block_test.go
new file mode 100644
index 0000000..cbffad5
--- /dev/null
+++ b/internal/services/btc/get_block_test.go
@@ -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)
+}
diff --git a/internal/services/btc/get_raw_transaction.go b/internal/services/btc/get_raw_transaction.go
new file mode 100644
index 0000000..1b4b5b7
--- /dev/null
+++ b/internal/services/btc/get_raw_transaction.go
@@ -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
+}
diff --git a/internal/services/btc/get_raw_transaction_test.go b/internal/services/btc/get_raw_transaction_test.go
new file mode 100644
index 0000000..c51a3d3
--- /dev/null
+++ b/internal/services/btc/get_raw_transaction_test.go
@@ -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))
+}
diff --git a/internal/services/btc/get_transaction.go b/internal/services/btc/get_transaction.go
new file mode 100644
index 0000000..2435f43
--- /dev/null
+++ b/internal/services/btc/get_transaction.go
@@ -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
+}
diff --git a/internal/services/btc/get_transaction_test.go b/internal/services/btc/get_transaction_test.go
new file mode 100644
index 0000000..6b7599b
--- /dev/null
+++ b/internal/services/btc/get_transaction_test.go
@@ -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)
+}
diff --git a/internal/services/btc/listaddressgroupings.go b/internal/services/btc/listaddressgroupings.go
new file mode 100644
index 0000000..490e0eb
--- /dev/null
+++ b/internal/services/btc/listaddressgroupings.go
@@ -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
+}
diff --git a/internal/services/btc/listaddressgroupings_test.go b/internal/services/btc/listaddressgroupings_test.go
new file mode 100644
index 0000000..8bc550d
--- /dev/null
+++ b/internal/services/btc/listaddressgroupings_test.go
@@ -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)
+
+}
diff --git a/internal/services/btc/notification.go b/internal/services/btc/notification.go
new file mode 100644
index 0000000..96cafef
--- /dev/null
+++ b/internal/services/btc/notification.go
@@ -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)
+ }
+}
diff --git a/internal/services/btc/rpc_request.go b/internal/services/btc/rpc_request.go
new file mode 100644
index 0000000..26c84a2
--- /dev/null
+++ b/internal/services/btc/rpc_request.go
@@ -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
+}
diff --git a/internal/services/btc/rpc_request_test.go b/internal/services/btc/rpc_request_test.go
new file mode 100644
index 0000000..f7e7bb0
--- /dev/null
+++ b/internal/services/btc/rpc_request_test.go
@@ -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)
+}
diff --git a/internal/services/btc/rpc_response.go b/internal/services/btc/rpc_response.go
new file mode 100644
index 0000000..21fc668
--- /dev/null
+++ b/internal/services/btc/rpc_response.go
@@ -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"`
+}
diff --git a/internal/services/btc/test_helper.go b/internal/services/btc/test_helper.go
new file mode 100644
index 0000000..c6ed19f
--- /dev/null
+++ b/internal/services/btc/test_helper.go
@@ -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),
+ }
+}
diff --git a/internal/services/btc/zmq.go b/internal/services/btc/zmq.go
new file mode 100644
index 0000000..55bad9d
--- /dev/null
+++ b/internal/services/btc/zmq.go
@@ -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
+ }
+ }
+}
diff --git a/internal/services/mail/mail.go b/internal/services/mail/mail.go
new file mode 100644
index 0000000..03f05dc
--- /dev/null
+++ b/internal/services/mail/mail.go
@@ -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
+}
diff --git a/internal/services/mail/mail_test.go b/internal/services/mail/mail_test.go
new file mode 100644
index 0000000..2fd0294
--- /dev/null
+++ b/internal/services/mail/mail_test.go
@@ -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)
+}
diff --git a/internal/services/mail/templates/client_confirm.html b/internal/services/mail/templates/client_confirm.html
new file mode 100644
index 0000000..17498c8
--- /dev/null
+++ b/internal/services/mail/templates/client_confirm.html
@@ -0,0 +1 @@
+TODO:
\ No newline at end of file
diff --git a/internal/services/mail/templates/error.html b/internal/services/mail/templates/error.html
new file mode 100644
index 0000000..17498c8
--- /dev/null
+++ b/internal/services/mail/templates/error.html
@@ -0,0 +1 @@
+TODO:
\ No newline at end of file
diff --git a/internal/services/mail/templates/provider_confirm.html b/internal/services/mail/templates/provider_confirm.html
new file mode 100644
index 0000000..17498c8
--- /dev/null
+++ b/internal/services/mail/templates/provider_confirm.html
@@ -0,0 +1 @@
+TODO:
\ No newline at end of file
diff --git a/internal/services/order.go b/internal/services/order.go
new file mode 100644
index 0000000..2ea86a1
--- /dev/null
+++ b/internal/services/order.go
@@ -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)
+}
diff --git a/internal/services/price/conversor.go b/internal/services/price/conversor.go
new file mode 100644
index 0000000..8d59e43
--- /dev/null
+++ b/internal/services/price/conversor.go
@@ -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
+}
diff --git a/internal/services/price/conversor_test.go b/internal/services/price/conversor_test.go
new file mode 100644
index 0000000..5e2c4d9
--- /dev/null
+++ b/internal/services/price/conversor_test.go
@@ -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)
+}
diff --git a/kit/cfg/config.go b/kit/cfg/config.go
new file mode 100644
index 0000000..1baaca6
--- /dev/null
+++ b/kit/cfg/config.go
@@ -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
+}
diff --git a/kit/http.go b/kit/http.go
new file mode 100644
index 0000000..a47b199
--- /dev/null
+++ b/kit/http.go
@@ -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)
+ }
+}
diff --git a/kit/path.go b/kit/path.go
new file mode 100644
index 0000000..7494c4d
--- /dev/null
+++ b/kit/path.go
@@ -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")
+}