feat: set up project
This commit is contained in:
parent
fac61db3f1
commit
b426a36570
|
@ -1,4 +1,5 @@
|
|||
.env
|
||||
.test.env
|
||||
.vscode
|
||||
coverage
|
||||
.notes
|
||||
|
|
6
LICENSE
6
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!
|
|
@ -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)
|
||||