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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user