387 lines
10 KiB
Go
387 lines
10 KiB
Go
package lbrycrd
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
|
|
"github.com/lbryio/lbry.go/v2/extras/errors"
|
|
c "github.com/lbryio/lbry.go/v2/schema/stake"
|
|
|
|
"github.com/btcsuite/btcd/btcec"
|
|
"github.com/btcsuite/btcd/btcjson"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/rpcclient"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/go-ini/ini"
|
|
)
|
|
|
|
const DefaultPort = 9245
|
|
|
|
var GenesisHash = chainhash.Hash([chainhash.HashSize]byte{ // Make go vet happy.
|
|
0x9c, 0x89, 0x28, 0x3b, 0xa0, 0xf3, 0x22, 0x7f,
|
|
0x6c, 0x03, 0xb7, 0x02, 0x16, 0xb9, 0xf6, 0x65,
|
|
0xf0, 0x11, 0x8d, 0x5e, 0x0f, 0xa7, 0x29, 0xce,
|
|
0xdf, 0x4f, 0xb3, 0x4d, 0x6a, 0x34, 0xf4, 0x63,
|
|
})
|
|
|
|
// MainNetParams define the lbrycrd network. See https://github.com/lbryio/lbrycrd/blob/master/src/chainparams.cpp
|
|
var MainNetParams = chaincfg.Params{
|
|
PubKeyHashAddrID: 0x55,
|
|
ScriptHashAddrID: 0x7a,
|
|
PrivateKeyID: 0x1c,
|
|
Bech32HRPSegwit: "lbc",
|
|
//WitnessPubKeyHashAddrID: , // i cant find these in bitcoin codebase either
|
|
//WitnessScriptHashAddrID:,
|
|
GenesisHash: &GenesisHash,
|
|
Name: "mainnet",
|
|
Net: wire.BitcoinNet(0xfae4aaf1),
|
|
DefaultPort: "9246",
|
|
BIP0034Height: 1,
|
|
BIP0065Height: 200000,
|
|
BIP0066Height: 200000,
|
|
}
|
|
|
|
const (
|
|
lbrycrdMainPubkeyPrefix = byte(85)
|
|
lbrycrdMainScriptPrefix = byte(122)
|
|
lbrycrdTestnetPubkeyPrefix = byte(111)
|
|
lbrycrdTestnetScriptPrefix = byte(196)
|
|
lbrycrdRegtestPubkeyPrefix = byte(111)
|
|
lbrycrdRegtestScriptPrefix = byte(196)
|
|
|
|
LbrycrdMain = "lbrycrd_main"
|
|
LbrycrdTestnet = "lbrycrd_testnet"
|
|
LbrycrdRegtest = "lbrycrd_regtest"
|
|
)
|
|
|
|
var mainNetParams = chaincfg.Params{
|
|
PubKeyHashAddrID: lbrycrdMainPubkeyPrefix,
|
|
ScriptHashAddrID: lbrycrdMainScriptPrefix,
|
|
PrivateKeyID: 0x1c,
|
|
}
|
|
|
|
var testNetParams = chaincfg.Params{
|
|
PubKeyHashAddrID: lbrycrdTestnetPubkeyPrefix,
|
|
ScriptHashAddrID: lbrycrdTestnetScriptPrefix,
|
|
PrivateKeyID: 0x1c,
|
|
Bech32HRPSegwit: "tlbc",
|
|
}
|
|
|
|
var regTestNetParams = chaincfg.Params{
|
|
PubKeyHashAddrID: lbrycrdRegtestPubkeyPrefix,
|
|
ScriptHashAddrID: lbrycrdRegtestScriptPrefix,
|
|
PrivateKeyID: 0x1c,
|
|
Bech32HRPSegwit: "rlbc",
|
|
}
|
|
|
|
var ChainParamsMap = map[string]chaincfg.Params{LbrycrdMain: mainNetParams, LbrycrdTestnet: testNetParams, LbrycrdRegtest: regTestNetParams}
|
|
|
|
func init() {
|
|
// Register lbrycrd network
|
|
err := chaincfg.Register(&MainNetParams)
|
|
if err != nil {
|
|
panic("failed to register lbrycrd network: " + err.Error())
|
|
}
|
|
}
|
|
|
|
// Client connects to a lbrycrd instance
|
|
type Client struct {
|
|
*rpcclient.Client
|
|
}
|
|
|
|
// New initializes a new Client
|
|
func New(lbrycrdURL string, chainParams *chaincfg.Params) (*Client, error) {
|
|
// Connect to local bitcoin core RPC server using HTTP POST mode.
|
|
|
|
u, err := url.Parse(lbrycrdURL)
|
|
if err != nil {
|
|
return nil, errors.Err(err)
|
|
}
|
|
|
|
if u.User == nil {
|
|
return nil, errors.Err("no userinfo")
|
|
}
|
|
|
|
password, _ := u.User.Password()
|
|
|
|
chain := MainNetParams
|
|
if chainParams != nil {
|
|
chain = *chainParams
|
|
}
|
|
|
|
connCfg := &rpcclient.ConnConfig{
|
|
Host: u.Host,
|
|
User: u.User.Username(),
|
|
Pass: password,
|
|
Params: chain,
|
|
HTTPPostMode: true, // Bitcoin core only supports HTTP POST mode
|
|
DisableTLS: true, // Bitcoin core does not provide TLS by default
|
|
}
|
|
// Notice the notification parameter is nil since notifications are not supported in HTTP POST mode.
|
|
client, err := rpcclient.New(connCfg, nil)
|
|
if err != nil {
|
|
return nil, errors.Err(err)
|
|
}
|
|
|
|
// make sure lbrycrd is running and responsive
|
|
_, err = client.GetBlockChainInfo()
|
|
if err != nil {
|
|
return nil, errors.Err(err)
|
|
}
|
|
|
|
return &Client{client}, nil
|
|
}
|
|
|
|
func NewWithDefaultURL(chainParams *chaincfg.Params) (*Client, error) {
|
|
url, err := getLbrycrdURLFromConfFile()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return New(url, chainParams)
|
|
}
|
|
|
|
var errInsufficientFunds = errors.Base("insufficient funds")
|
|
|
|
// SimpleSend is a convenience function to send credits to an address (0 min confirmations)
|
|
func (c *Client) SimpleSend(toAddress string, amount float64) (*chainhash.Hash, error) {
|
|
decodedAddress, err := DecodeAddress(toAddress, &MainNetParams)
|
|
if err != nil {
|
|
return nil, errors.Err(err)
|
|
}
|
|
|
|
lbcAmount, err := btcutil.NewAmount(amount)
|
|
if err != nil {
|
|
return nil, errors.Err(err)
|
|
}
|
|
|
|
hash, err := c.Client.SendToAddress(decodedAddress, lbcAmount)
|
|
if err != nil {
|
|
if err.Error() == "-6: Insufficient funds" {
|
|
err = errors.Err(errInsufficientFunds)
|
|
}
|
|
return nil, errors.Err(err)
|
|
}
|
|
return hash, nil
|
|
}
|
|
|
|
func getLbrycrdURLFromConfFile() (string, error) {
|
|
if os.Getenv("HOME") == "" {
|
|
return "", errors.Err("no $HOME var found")
|
|
}
|
|
|
|
defaultConfFile := os.Getenv("HOME") + "/.lbrycrd/lbrycrd.conf"
|
|
if os.Getenv("REGTEST") == "true" {
|
|
defaultConfFile = os.Getenv("HOME") + "/.lbrycrd_regtest/lbrycrd.conf"
|
|
}
|
|
if _, err := os.Stat(defaultConfFile); os.IsNotExist(err) {
|
|
return "", errors.Err("default lbrycrd conf file not found")
|
|
}
|
|
|
|
cfg, err := ini.Load(defaultConfFile)
|
|
if err != nil {
|
|
return "", errors.Err(err)
|
|
}
|
|
|
|
section, err := cfg.GetSection("")
|
|
if err != nil {
|
|
return "", errors.Err(err)
|
|
}
|
|
|
|
username := section.Key("rpcuser").String()
|
|
password := section.Key("rpcpassword").String()
|
|
host := section.Key("rpchost").String()
|
|
if host == "" {
|
|
host = "localhost"
|
|
}
|
|
port := section.Key("rpcport").String()
|
|
if port == "" {
|
|
port = strconv.Itoa(DefaultPort)
|
|
}
|
|
|
|
userpass := ""
|
|
if username != "" || password != "" {
|
|
userpass = username + ":" + password + "@"
|
|
}
|
|
|
|
return "rpc://" + userpass + host + ":" + port, nil
|
|
}
|
|
|
|
func (c *Client) CreateBaseRawTx(inputs []btcjson.TransactionInput, change float64) (*wire.MsgTx, error) {
|
|
addresses := make(map[btcutil.Address]btcutil.Amount)
|
|
changeAddress, err := c.GetNewAddress("")
|
|
if err != nil {
|
|
return nil, errors.Err(err)
|
|
}
|
|
changeAmount, err := btcutil.NewAmount(change)
|
|
if err != nil {
|
|
return nil, errors.Err(err)
|
|
}
|
|
addresses[changeAddress] = changeAmount
|
|
lockTime := int64(0)
|
|
return c.CreateRawTransaction(inputs, addresses, &lockTime)
|
|
}
|
|
|
|
func (c *Client) GetEmptyTx(totalOutputSpend float64) (*wire.MsgTx, error) {
|
|
totalFees := 0.1
|
|
unspentResults, err := c.ListUnspentMin(1)
|
|
if err != nil {
|
|
return nil, errors.Err(err)
|
|
}
|
|
finder := newOutputFinder(unspentResults)
|
|
|
|
outputs, err := finder.nextBatch(totalOutputSpend + totalFees)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(outputs) == 0 {
|
|
return nil, errors.Err("Not enough spendable outputs to create transaction")
|
|
}
|
|
inputs := make([]btcjson.TransactionInput, len(outputs))
|
|
var totalInputSpend float64
|
|
for i, output := range outputs {
|
|
inputs[i] = btcjson.TransactionInput{Txid: output.TxID, Vout: output.Vout}
|
|
totalInputSpend = totalInputSpend + output.Amount
|
|
}
|
|
|
|
change := totalInputSpend - totalOutputSpend - totalFees
|
|
return c.CreateBaseRawTx(inputs, change)
|
|
}
|
|
|
|
func (c *Client) SignTxAndSend(rawTx *wire.MsgTx) (*chainhash.Hash, error) {
|
|
signedTx, allInputsSigned, err := c.SignRawTransaction(rawTx)
|
|
if err != nil {
|
|
return nil, errors.Err(err)
|
|
}
|
|
if !allInputsSigned {
|
|
return nil, errors.Err("Not all inputs for the tx could be signed!")
|
|
}
|
|
|
|
return c.SendRawTransaction(signedTx, false)
|
|
}
|
|
|
|
type ScriptType int
|
|
|
|
const (
|
|
ClaimName ScriptType = iota
|
|
ClaimUpdate
|
|
ClaimSupport
|
|
)
|
|
|
|
func (c *Client) AddStakeToTx(rawTx *wire.MsgTx, claim *c.StakeHelper, name string, claimAmount float64, scriptType ScriptType) error {
|
|
|
|
address, err := c.GetNewAddress("")
|
|
if err != nil {
|
|
return errors.Err(err)
|
|
}
|
|
amount, err := btcutil.NewAmount(claimAmount)
|
|
if err != nil {
|
|
return errors.Err(err)
|
|
}
|
|
|
|
value, err := claim.CompileValue()
|
|
if err != nil {
|
|
return errors.Err(err)
|
|
}
|
|
var claimID string
|
|
if len(claim.ClaimID) > 0 {
|
|
claimID = hex.EncodeToString(rev(claim.ClaimID))
|
|
}
|
|
var script []byte
|
|
switch scriptType {
|
|
case ClaimName:
|
|
script, err = getClaimNamePayoutScript(name, value, address)
|
|
if err != nil {
|
|
return errors.Err(err)
|
|
}
|
|
case ClaimUpdate:
|
|
script, err = getUpdateClaimPayoutScript(name, claimID, value, address)
|
|
if err != nil {
|
|
return errors.Err(err)
|
|
}
|
|
case ClaimSupport:
|
|
script, err = getUpdateClaimPayoutScript(name, claimID, value, address)
|
|
if err != nil {
|
|
return errors.Err(err)
|
|
}
|
|
}
|
|
|
|
rawTx.AddTxOut(wire.NewTxOut(int64(amount), script))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) CreateChannel(name string, amount float64) (*c.StakeHelper, *btcec.PrivateKey, error) {
|
|
channel, key, err := NewChannel()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
rawTx, err := c.GetEmptyTx(amount)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
err = c.AddStakeToTx(rawTx, channel, name, amount, ClaimName)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
_, err = c.SignTxAndSend(rawTx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return channel, key, nil
|
|
}
|
|
|
|
func (c *Client) SupportClaim(name, claimID, address, blockchainName string, claimAmount float64) (*chainhash.Hash, error) {
|
|
const DefaultFeePerSupport = float64(0.0001)
|
|
unspentResults, err := c.ListUnspentMin(1)
|
|
if err != nil {
|
|
return nil, errors.Err(err)
|
|
}
|
|
|
|
finder := newOutputFinder(unspentResults)
|
|
outputs, err := finder.nextBatch(claimAmount + DefaultFeePerSupport)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(outputs) == 0 {
|
|
return nil, errors.Err("Not enough spendable outputs to create transaction")
|
|
}
|
|
inputs := make([]btcjson.TransactionInput, len(outputs))
|
|
|
|
var totalInputSpend float64
|
|
for i, output := range outputs {
|
|
inputs[i] = btcjson.TransactionInput{Txid: output.TxID, Vout: output.Vout}
|
|
totalInputSpend = totalInputSpend + output.Amount
|
|
}
|
|
|
|
change := totalInputSpend - claimAmount - DefaultFeePerSupport
|
|
rawTx, err := c.CreateBaseRawTx(inputs, change)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chainParams, ok := ChainParamsMap[blockchainName]
|
|
if !ok {
|
|
return nil, errors.Err("invalid blockchain name %s", blockchainName)
|
|
}
|
|
decodedAddress, err := DecodeAddress(address, &chainParams)
|
|
if err != nil {
|
|
return nil, errors.Err(err)
|
|
}
|
|
amount, err := btcutil.NewAmount(claimAmount)
|
|
if err != nil {
|
|
return nil, errors.Err(err)
|
|
}
|
|
script, err := getClaimSupportPayoutScript(name, claimID, decodedAddress)
|
|
if err != nil {
|
|
return nil, errors.Err(err)
|
|
}
|
|
rawTx.AddTxOut(wire.NewTxOut(int64(amount), script))
|
|
|
|
return c.SignTxAndSend(rawTx)
|
|
}
|