github.com/piotrnar/gocoin@v0.0.0-20240512203912-faa0448c5e96/lib/others/utils/unspent.go (about)

     1  package utils
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"os"
    10  	"time"
    11  
    12  	"github.com/piotrnar/gocoin/lib/btc"
    13  	"github.com/piotrnar/gocoin/lib/utxo"
    14  )
    15  
    16  func GetUnspentFromExplorer(addr *btc.BtcAddr, testnet bool) (res utxo.AllUnspentTx, er error) {
    17  	var r *http.Response
    18  	if testnet {
    19  		r, er = http.Get("https://testnet.blockexplorer.com/api/addr/" + addr.String() + "/utxo")
    20  	} else {
    21  		r, er = http.Get("https://blockexplorer.com/api/addr/" + addr.String() + "/utxo")
    22  	}
    23  	if er != nil {
    24  		return
    25  	}
    26  	if r.StatusCode != 200 {
    27  		er = errors.New(fmt.Sprint("HTTP StatusCode ", r.StatusCode))
    28  		return
    29  	}
    30  
    31  	c, _ := io.ReadAll(r.Body)
    32  	r.Body.Close()
    33  
    34  	var result []struct {
    35  		Addr   string `json:"address"`
    36  		TxID   string `json:"txid"`
    37  		Vout   uint32 `json:"vout"`
    38  		Value  uint64 `json:"satoshis"`
    39  		Height uint32 `json:"height"`
    40  	}
    41  
    42  	er = json.Unmarshal(c, &result)
    43  	if er != nil {
    44  		return
    45  	}
    46  
    47  	for _, r := range result {
    48  		ur := new(utxo.OneUnspentTx)
    49  		id := btc.NewUint256FromString(r.TxID)
    50  		if id == nil {
    51  			er = errors.New(fmt.Sprint("Bad TXID:", r.TxID))
    52  			return
    53  		}
    54  		copy(ur.TxPrevOut.Hash[:], id.Hash[:])
    55  		ur.TxPrevOut.Vout = r.Vout
    56  		ur.Value = r.Value
    57  		ur.MinedAt = r.Height
    58  		ur.BtcAddr = addr
    59  		res = append(res, ur)
    60  	}
    61  
    62  	return
    63  }
    64  
    65  func GetUnspentFromBlockchainInfo(addr *btc.BtcAddr) (res utxo.AllUnspentTx, er error) {
    66  	var r *http.Response
    67  	r, er = http.Get("https://blockchain.info/unspent?active=" + addr.String())
    68  	if er != nil {
    69  		return
    70  	}
    71  	if r.StatusCode != 200 {
    72  		er = errors.New(fmt.Sprint("HTTP StatusCode ", r.StatusCode))
    73  		return
    74  	}
    75  
    76  	c, _ := io.ReadAll(r.Body)
    77  	r.Body.Close()
    78  
    79  	var result struct {
    80  		U []struct {
    81  			TxID  string `json:"tx_hash_big_endian"`
    82  			Vout  uint32 `json:"tx_output_n"`
    83  			Value uint64 `json:"value"`
    84  		} `json:"unspent_outputs"`
    85  	}
    86  
    87  	er = json.Unmarshal(c, &result)
    88  	if er != nil {
    89  		return
    90  	}
    91  
    92  	for _, r := range result.U {
    93  		ur := new(utxo.OneUnspentTx)
    94  		id := btc.NewUint256FromString(r.TxID)
    95  		if id == nil {
    96  			er = errors.New(fmt.Sprint("Bad TXID:", r.TxID))
    97  			return
    98  		}
    99  		copy(ur.TxPrevOut.Hash[:], id.Hash[:])
   100  		ur.TxPrevOut.Vout = r.Vout
   101  		ur.Value = r.Value
   102  		//ur.MinedAt = r.Height
   103  		ur.BtcAddr = addr
   104  		res = append(res, ur)
   105  	}
   106  
   107  	return
   108  }
   109  
   110  func GetUnspentFromBlockcypher(addr *btc.BtcAddr, currency string) (res utxo.AllUnspentTx, er error) {
   111  	var r *http.Response
   112  	var try_cnt int
   113  
   114  	token := os.Getenv("BLOCKCYPHER_TOKEN")
   115  	if token == "" {
   116  		println("WARNING: BLOCKCYPHER_TOKEN envirionment variable not set (get it from blockcypher.com)")
   117  	} else {
   118  		token = "&token=" + token
   119  	}
   120  
   121  	for {
   122  		r, er = http.Get("https://api.blockcypher.com/v1/" + currency + "/main/addrs/" + addr.String() + "?unspentOnly=true" + token)
   123  
   124  		if er != nil {
   125  			return
   126  		}
   127  		if r.StatusCode == 429 && try_cnt < 5 {
   128  			try_cnt++
   129  			println("Retry blockcypher.com in", try_cnt, "seconds...")
   130  			time.Sleep(time.Duration(try_cnt) * time.Second)
   131  			continue
   132  		}
   133  
   134  		if r.StatusCode != 200 {
   135  			er = errors.New(fmt.Sprint("HTTP StatusCode ", r.StatusCode))
   136  			return
   137  		}
   138  
   139  		break
   140  	}
   141  
   142  	c, _ := io.ReadAll(r.Body)
   143  	r.Body.Close()
   144  
   145  	var result struct {
   146  		Addr string `json:"address"`
   147  		Outs []struct {
   148  			TxID   string `json:"tx_hash"`
   149  			Vout   uint32 `json:"tx_output_n"`
   150  			Value  uint64 `json:"value"`
   151  			Height uint32 `json:"block_height"`
   152  		} `json:"txrefs"`
   153  	}
   154  
   155  	er = json.Unmarshal(c, &result)
   156  	if er != nil {
   157  		return
   158  	}
   159  
   160  	for _, r := range result.Outs {
   161  		ur := new(utxo.OneUnspentTx)
   162  		id := btc.NewUint256FromString(r.TxID)
   163  		if id == nil {
   164  			er = errors.New(fmt.Sprint("Bad TXID:", r.TxID))
   165  			return
   166  		}
   167  		copy(ur.TxPrevOut.Hash[:], id.Hash[:])
   168  		ur.TxPrevOut.Vout = r.Vout
   169  		ur.Value = r.Value
   170  		ur.MinedAt = r.Height
   171  		ur.BtcAddr = addr
   172  		res = append(res, ur)
   173  	}
   174  
   175  	return
   176  }
   177  
   178  // currency is either "bitcoin" or "bitcoin-cash"
   179  func GetUnspentFromBlockchair(addr *btc.BtcAddr, currency string) (res utxo.AllUnspentTx, er error) {
   180  	var r *http.Response
   181  	var try_cnt int
   182  
   183  	for {
   184  		// https://api.blockchair.com/bitcoin/outputs?q=is_spent(false),recipient(bc1qdvpxmyvyu9urhadl6sk69gcjsfqsvrjsqfk5aq)
   185  		r, er = http.Get("https://api.blockchair.com/" + currency + "/outputs?q=is_spent(false),recipient(" + addr.String() + ")")
   186  
   187  		if er != nil {
   188  			return
   189  		}
   190  		if (r.StatusCode == 402 || r.StatusCode == 429) && try_cnt < 5 {
   191  			try_cnt++
   192  			println("Retry blockchair.com in", try_cnt, "seconds...")
   193  			time.Sleep(time.Duration(try_cnt) * time.Second)
   194  			continue
   195  		}
   196  		if r.StatusCode != 200 {
   197  			er = errors.New(fmt.Sprint("HTTP StatusCode ", r.StatusCode))
   198  			return
   199  		}
   200  		break
   201  	}
   202  
   203  	c, _ := io.ReadAll(r.Body)
   204  	r.Body.Close()
   205  
   206  	var result struct {
   207  		Outs []struct {
   208  			TxID   string `json:"transaction_hash"`
   209  			Vout   uint32 `json:"index"`
   210  			Value  uint64 `json:"value"`
   211  			Height uint32 `json:"block_id"`
   212  			Spent  bool   `json:"is_spent"`
   213  		} `json:"data"`
   214  	}
   215  
   216  	er = json.Unmarshal(c, &result)
   217  	if er != nil {
   218  		return
   219  	}
   220  
   221  	for _, r := range result.Outs {
   222  		if r.Spent {
   223  			continue
   224  		}
   225  		ur := new(utxo.OneUnspentTx)
   226  		id := btc.NewUint256FromString(r.TxID)
   227  		if id == nil {
   228  			er = errors.New(fmt.Sprint("Bad TXID:", r.TxID))
   229  			return
   230  		}
   231  		copy(ur.TxPrevOut.Hash[:], id.Hash[:])
   232  		ur.TxPrevOut.Vout = r.Vout
   233  		ur.Value = r.Value
   234  		ur.MinedAt = r.Height
   235  		ur.BtcAddr = addr
   236  		res = append(res, ur)
   237  	}
   238  
   239  	return
   240  }
   241  
   242  func GetUnspentFromBlockstream(addr *btc.BtcAddr, api_url string) (res utxo.AllUnspentTx, er error) {
   243  	var r *http.Response
   244  
   245  	r, er = http.Get(api_url + addr.String() + "/utxo")
   246  
   247  	if er != nil {
   248  		return
   249  	}
   250  	if r.StatusCode != 200 {
   251  		er = errors.New(fmt.Sprint("HTTP StatusCode ", r.StatusCode))
   252  		return
   253  	}
   254  
   255  	c, _ := io.ReadAll(r.Body)
   256  	r.Body.Close()
   257  
   258  	var result []struct {
   259  		TxID   string `json:"txid"`
   260  		Vout   uint32 `json:"vout"`
   261  		Status struct {
   262  			Confirmed bool   `json:"confirmed"`
   263  			Height    uint32 `json:"block_height"`
   264  		} `json:"status"`
   265  		Value uint64 `json:"value"`
   266  	}
   267  
   268  	er = json.Unmarshal(c, &result)
   269  	if er != nil {
   270  		return
   271  	}
   272  
   273  	for _, r := range result {
   274  		if !r.Status.Confirmed {
   275  			continue
   276  		}
   277  		ur := new(utxo.OneUnspentTx)
   278  		id := btc.NewUint256FromString(r.TxID)
   279  		if id == nil {
   280  			er = errors.New(fmt.Sprint("Bad TXID:", r.TxID))
   281  			return
   282  		}
   283  		copy(ur.TxPrevOut.Hash[:], id.Hash[:])
   284  		ur.TxPrevOut.Vout = r.Vout
   285  		ur.Value = r.Value
   286  		ur.MinedAt = r.Status.Height
   287  		ur.BtcAddr = addr
   288  		res = append(res, ur)
   289  	}
   290  
   291  	return
   292  }
   293  
   294  func GetUnspent(addr *btc.BtcAddr) (res utxo.AllUnspentTx) {
   295  	var er error
   296  
   297  	res, er = GetUnspentFromBlockstream(addr, "https://blockstream.info/api/address/")
   298  	if er == nil {
   299  		return
   300  	}
   301  	println("GetUnspentFromBlockstream:", er.Error())
   302  
   303  	res, er = GetUnspentFromBlockchair(addr, "bitcoin")
   304  	if er == nil {
   305  		return
   306  	}
   307  	println("GetUnspentFromBlockchair:", er.Error())
   308  
   309  	res, er = GetUnspentFromBlockcypher(addr, "btc")
   310  	if er == nil {
   311  		return
   312  	}
   313  	println("GetUnspentFromBlockcypher:", er.Error())
   314  
   315  	res, er = GetUnspentFromBlockchainInfo(addr)
   316  	if er == nil {
   317  		return
   318  	}
   319  	println("GetUnspentFromBlockchainInfo:", er.Error())
   320  
   321  	res, er = GetUnspentFromExplorer(addr, false)
   322  	if er == nil {
   323  		return
   324  	}
   325  	println("GetUnspentFromExplorer:", er.Error())
   326  	return
   327  }
   328  
   329  func GetUnspentTestnet(addr *btc.BtcAddr) (res utxo.AllUnspentTx) {
   330  	var er error
   331  
   332  	res, er = GetUnspentFromBlockstream(addr, "https://blockstream.info/testnet/api/address/")
   333  	if er == nil {
   334  		return
   335  	}
   336  	println("GetUnspentFromBlockstream:", er.Error())
   337  
   338  	res, er = GetUnspentFromExplorer(addr, true)
   339  	if er == nil {
   340  		return
   341  	}
   342  	println("GetUnspentFromExplorer:", er.Error())
   343  
   344  	res, er = GetUnspentFromBlockcypher(addr, "btc-testnet")
   345  	if er == nil {
   346  		return
   347  	}
   348  	println("GetUnspentFromBlockcypher:", er.Error())
   349  
   350  	return
   351  }