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