diff --git a/gleecbtc/address/address.go b/gleecbtc/address/address.go new file mode 100644 index 0000000..c23e07a --- /dev/null +++ b/gleecbtc/address/address.go @@ -0,0 +1,792 @@ +package address + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "strings" + + lparams "github.com/OpenBazaar/multiwallet/gleecbtc/params" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/bech32" + "github.com/btcsuite/golangcrypto/ripemd160" + ltcparams "github.com/ltcsuite/ltcd/chaincfg" + "github.com/ltcsuite/ltcutil/base58" +) + +// UnsupportedWitnessVerError describes an error where a segwit address being +// decoded has an unsupported witness version. +type UnsupportedWitnessVerError byte + +func (e UnsupportedWitnessVerError) Error() string { + return "unsupported witness version: " + string(e) +} + +// UnsupportedWitnessProgLenError describes an error where a segwit address +// being decoded has an unsupported witness program length. +type UnsupportedWitnessProgLenError int + +func (e UnsupportedWitnessProgLenError) Error() string { + return "unsupported witness program length: " + string(e) +} + +var ( + // ErrChecksumMismatch describes an error where decoding failed due + // to a bad checksum. + ErrChecksumMismatch = errors.New("checksum mismatch") + + // ErrUnknownAddressType describes an error where an address can not + // decoded as a specific address type due to the string encoding + // begining with an identifier byte unknown to any standard or + // registered (via chaincfg.Register) network. + ErrUnknownAddressType = errors.New("unknown address type") + + // ErrAddressCollision describes an error where an address can not + // be uniquely determined as either a pay-to-pubkey-hash or + // pay-to-script-hash address since the leading identifier is used for + // describing both address kinds, but for different networks. Rather + // than assuming or defaulting to one or the other, this error is + // returned and the caller must decide how to decode the address. + ErrAddressCollision = errors.New("address collision") + + scriptHashAddrIDs map[byte]struct{} +) + +const ( + NetIDMainnetP2S2 = 0x26 + NetIDTestnetP2SH2 = 0xC4 +) + +func init() { + scriptHashAddrIDs = make(map[byte]struct{}) + scriptHashAddrIDs[chaincfg.MainNetParams.ScriptHashAddrID] = struct{}{} + scriptHashAddrIDs[chaincfg.TestNet3Params.ScriptHashAddrID] = struct{}{} + scriptHashAddrIDs[chaincfg.RegressionNetParams.ScriptHashAddrID] = struct{}{} + scriptHashAddrIDs[NetIDMainnetP2S2] = struct{}{} + scriptHashAddrIDs[NetIDTestnetP2SH2] = struct{}{} +} + +// encodeAddress returns a human-readable payment address given a ripemd160 hash +// and netID which encodes the gleecbtc network and address type. It is used +// in both pay-to-pubkey-hash (P2PKH) and pay-to-script-hash (P2SH) address +// encoding. +func encodeAddress(hash160 []byte, netID byte) string { + // Format is 1 byte for a network and address class (i.e. P2PKH vs + // P2SH), 20 bytes for a RIPEMD160 hash, and 4 bytes of checksum. + return base58.CheckEncode(hash160[:ripemd160.Size], netID) +} + +// encodeSegWitAddress creates a bech32 encoded address string representation +// from witness version and witness program. +func encodeSegWitAddress(hrp string, witnessVersion byte, witnessProgram []byte) (string, error) { + // Group the address bytes into 5 bit groups, as this is what is used to + // encode each character in the address string. + converted, err := bech32.ConvertBits(witnessProgram, 8, 5, true) + if err != nil { + return "", err + } + + // Concatenate the witness version and program, and encode the resulting + // bytes using bech32 encoding. + combined := make([]byte, len(converted)+1) + combined[0] = witnessVersion + copy(combined[1:], converted) + bech, err := bech32.Encode(hrp, combined) + if err != nil { + return "", err + } + + // Check validity by decoding the created address. + version, program, err := decodeSegWitAddress(bech) + if err != nil { + return "", fmt.Errorf("invalid segwit address: %v", err) + } + + if version != witnessVersion || !bytes.Equal(program, witnessProgram) { + return "", fmt.Errorf("invalid segwit address") + } + + return bech, nil +} + +// Address is an interface type for any type of destination a transaction +// output may spend to. This includes pay-to-pubkey (P2PK), pay-to-pubkey-hash +// (P2PKH), and pay-to-script-hash (P2SH). Address is designed to be generic +// enough that other kinds of addresses may be added in the future without +// changing the decoding and encoding API. +type Address interface { + // String returns the string encoding of the transaction output + // destination. + // + // Please note that String differs subtly from EncodeAddress: String + // will return the value as a string without any conversion, while + // EncodeAddress may convert destination types (for example, + // converting pubkeys to P2PKH addresses) before encoding as a + // payment address string. + String() string + + // EncodeAddress returns the string encoding of the payment address + // associated with the Address value. See the comment on String + // for how this method differs from String. + EncodeAddress() string + + // ScriptAddress returns the raw bytes of the address to be used + // when inserting the address into a txout's script. + ScriptAddress() []byte + + // IsForNet returns whether or not the address is associated with the + // passed gleecbtc network. + IsForNet(*chaincfg.Params) bool +} + +// DecodeAddress decodes the string encoding of an address and returns +// the Address if addr is a valid encoding for a known address type. +// +// The gleecbtc network the address is associated with is extracted if possible. +// When the address does not encode the network, such as in the case of a raw +// public key, the address will be associated with the passed defaultNet. +func DecodeAddress(addr string, defaultNet *chaincfg.Params) (Address, error) { + // Bech32 encoded segwit addresses start with a human-readable part + // (hrp) followed by '1'. For Bitcoin mainnet the hrp is "bc", and for + // testnet it is "tb". If the address string has a prefix that matches + // one of the prefixes for the known networks, we try to decode it as + // a segwit address. + oneIndex := strings.LastIndexByte(addr, '1') + if oneIndex > 1 { + prefix := addr[:oneIndex+1] + if IsBech32SegwitPrefix(prefix) { + witnessVer, witnessProg, err := decodeSegWitAddress(addr) + if err == nil { + // We currently only support P2WPKH and P2WSH, which is + // witness version 0. + if witnessVer != 0 { + return nil, UnsupportedWitnessVerError(witnessVer) + } + + // The HRP is everything before the found '1'. + hrp := prefix[:len(prefix)-1] + + switch len(witnessProg) { + case 20: + return newAddressWitnessPubKeyHash(hrp, witnessProg) + case 32: + return newAddressWitnessScriptHash(hrp, witnessProg) + default: + return nil, UnsupportedWitnessProgLenError(len(witnessProg)) + } + } + } + } + + // Serialized public keys are either 65 bytes (130 hex chars) if + // uncompressed/hybrid or 33 bytes (66 hex chars) if compressed. + if len(addr) == 130 || len(addr) == 66 { + serializedPubKey, err := hex.DecodeString(addr) + if err != nil { + return nil, err + } + return NewAddressPubKey(serializedPubKey, defaultNet) + } + + // Switch on decoded length to determine the type. + decoded, netID, err := base58.CheckDecode(addr) + if err != nil { + if err == base58.ErrChecksum { + return nil, ErrChecksumMismatch + } + return nil, errors.New("decoded address is of unknown format") + } + switch len(decoded) { + case ripemd160.Size: // P2PKH or P2SH + isP2PKH := ltcparams.IsPubKeyHashAddrID(netID) + isP2SH := IsScriptHashAddrID(netID) + switch hash160 := decoded; { + case isP2PKH && isP2SH: + return nil, ErrAddressCollision + case isP2PKH: + return newAddressPubKeyHash(hash160, netID) + case isP2SH: + return newAddressScriptHashFromHash(hash160, netID) + default: + return nil, ErrUnknownAddressType + } + + default: + return nil, errors.New("decoded address is of unknown size") + } +} + +// IsBech32SegwitPrefix returns whether the prefix is a known prefix for segwit +// addresses on any default or registered network. This is used when decoding +// an address string into a specific address type. +func IsBech32SegwitPrefix(prefix string) bool { + prefix = strings.ToLower(prefix) + if prefix == "ltc1" || prefix == "tltc1" { + return true + } + return false +} + +// decodeSegWitAddress parses a bech32 encoded segwit address string and +// returns the witness version and witness program byte representation. +func decodeSegWitAddress(address string) (byte, []byte, error) { + // Decode the bech32 encoded address. + _, data, err := bech32.Decode(address) + if err != nil { + return 0, nil, err + } + + // The first byte of the decoded address is the witness version, it must + // exist. + if len(data) < 1 { + return 0, nil, fmt.Errorf("no witness version") + } + + // ...and be <= 16. + version := data[0] + if version > 16 { + return 0, nil, fmt.Errorf("invalid witness version: %v", version) + } + + // The remaining characters of the address returned are grouped into + // words of 5 bits. In order to restore the original witness program + // bytes, we'll need to regroup into 8 bit words. + regrouped, err := bech32.ConvertBits(data[1:], 5, 8, false) + if err != nil { + return 0, nil, err + } + + // The regrouped data must be between 2 and 40 bytes. + if len(regrouped) < 2 || len(regrouped) > 40 { + return 0, nil, fmt.Errorf("invalid data length") + } + + // For witness version 0, address MUST be exactly 20 or 32 bytes. + if version == 0 && len(regrouped) != 20 && len(regrouped) != 32 { + return 0, nil, fmt.Errorf("invalid data length for witness "+ + "version 0: %v", len(regrouped)) + } + + return version, regrouped, nil +} + +// AddressPubKeyHash is an Address for a pay-to-pubkey-hash (P2PKH) +// transaction. +type AddressPubKeyHash struct { + hash [ripemd160.Size]byte + netID byte +} + +// NewAddressPubKeyHash returns a new AddressPubKeyHash. pkHash mustbe 20 +// bytes. +func NewAddressPubKeyHash(pkHash []byte, net *chaincfg.Params) (*AddressPubKeyHash, error) { + params := lparams.ConvertParams(net) + return newAddressPubKeyHash(pkHash, params.PubKeyHashAddrID) +} + +// newAddressPubKeyHash is the internal API to create a pubkey hash address +// with a known leading identifier byte for a network, rather than looking +// it up through its parameters. This is useful when creating a new address +// structure from a string encoding where the identifer byte is already +// known. +func newAddressPubKeyHash(pkHash []byte, netID byte) (*AddressPubKeyHash, error) { + // Check for a valid pubkey hash length. + if len(pkHash) != ripemd160.Size { + return nil, errors.New("pkHash must be 20 bytes") + } + + addr := &AddressPubKeyHash{netID: netID} + copy(addr.hash[:], pkHash) + return addr, nil +} + +// EncodeAddress returns the string encoding of a pay-to-pubkey-hash +// address. Part of the Address interface. +func (a *AddressPubKeyHash) EncodeAddress() string { + return encodeAddress(a.hash[:], a.netID) +} + +// ScriptAddress returns the bytes to be included in a txout script to pay +// to a pubkey hash. Part of the Address interface. +func (a *AddressPubKeyHash) ScriptAddress() []byte { + return a.hash[:] +} + +// IsForNet returns whether or not the pay-to-pubkey-hash address is associated +// with the passed gleecbtc network. +func (a *AddressPubKeyHash) IsForNet(net *chaincfg.Params) bool { + params := lparams.ConvertParams(net) + return a.netID == params.PubKeyHashAddrID +} + +// String returns a human-readable string for the pay-to-pubkey-hash address. +// This is equivalent to calling EncodeAddress, but is provided so the type can +// be used as a fmt.Stringer. +func (a *AddressPubKeyHash) String() string { + return a.EncodeAddress() +} + +// Hash160 returns the underlying array of the pubkey hash. This can be useful +// when an array is more appropiate than a slice (for example, when used as map +// keys). +func (a *AddressPubKeyHash) Hash160() *[ripemd160.Size]byte { + return &a.hash +} + +// AddressScriptHash is an Address for a pay-to-script-hash (P2SH) +// transaction. +type AddressScriptHash struct { + hash [ripemd160.Size]byte + netID byte +} + +// NewAddressScriptHash returns a new AddressScriptHash. +func NewAddressScriptHash(serializedScript []byte, net *chaincfg.Params) (*AddressScriptHash, error) { + scriptHash := btcutil.Hash160(serializedScript) + params := lparams.ConvertParams(net) + return newAddressScriptHashFromHash(scriptHash, params.ScriptHashAddrID) +} + +// NewAddressScriptHashFromHash returns a new AddressScriptHash. scriptHash +// must be 20 bytes. +func NewAddressScriptHashFromHash(scriptHash []byte, net *chaincfg.Params) (*AddressScriptHash, error) { + params := lparams.ConvertParams(net) + return newAddressScriptHashFromHash(scriptHash, params.ScriptHashAddrID) +} + +// newAddressScriptHashFromHash is the internal API to create a script hash +// address with a known leading identifier byte for a network, rather than +// looking it up through its parameters. This is useful when creating a new +// address structure from a string encoding where the identifer byte is already +// known. +func newAddressScriptHashFromHash(scriptHash []byte, netID byte) (*AddressScriptHash, error) { + // Check for a valid script hash length. + if len(scriptHash) != ripemd160.Size { + return nil, errors.New("scriptHash must be 20 bytes") + } + + addr := &AddressScriptHash{netID: netID} + copy(addr.hash[:], scriptHash) + return addr, nil +} + +// EncodeAddress returns the string encoding of a pay-to-script-hash +// address. Part of the Address interface. +func (a *AddressScriptHash) EncodeAddress() string { + return encodeAddress(a.hash[:], a.netID) +} + +// ScriptAddress returns the bytes to be included in a txout script to pay +// to a script hash. Part of the Address interface. +func (a *AddressScriptHash) ScriptAddress() []byte { + return a.hash[:] +} + +// IsForNet returns whether or not the pay-to-script-hash address is associated +// with the passed gleecbtc network. +func (a *AddressScriptHash) IsForNet(net *chaincfg.Params) bool { + params := lparams.ConvertParams(net) + return a.netID == params.ScriptHashAddrID +} + +// String returns a human-readable string for the pay-to-script-hash address. +// This is equivalent to calling EncodeAddress, but is provided so the type can +// be used as a fmt.Stringer. +func (a *AddressScriptHash) String() string { + return a.EncodeAddress() +} + +// Hash160 returns the underlying array of the script hash. This can be useful +// when an array is more appropiate than a slice (for example, when used as map +// keys). +func (a *AddressScriptHash) Hash160() *[ripemd160.Size]byte { + return &a.hash +} + +// PubKeyFormat describes what format to use for a pay-to-pubkey address. +type PubKeyFormat int + +const ( + // PKFUncompressed indicates the pay-to-pubkey address format is an + // uncompressed public key. + PKFUncompressed PubKeyFormat = iota + + // PKFCompressed indicates the pay-to-pubkey address format is a + // compressed public key. + PKFCompressed + + // PKFHybrid indicates the pay-to-pubkey address format is a hybrid + // public key. + PKFHybrid +) + +// AddressPubKey is an Address for a pay-to-pubkey transaction. +type AddressPubKey struct { + pubKeyFormat PubKeyFormat + pubKey *btcec.PublicKey + pubKeyHashID byte +} + +// NewAddressPubKey returns a new AddressPubKey which represents a pay-to-pubkey +// address. The serializedPubKey parameter must be a valid pubkey and can be +// uncompressed, compressed, or hybrid. +func NewAddressPubKey(serializedPubKey []byte, net *chaincfg.Params) (*AddressPubKey, error) { + pubKey, err := btcec.ParsePubKey(serializedPubKey, btcec.S256()) + if err != nil { + return nil, err + } + + // Set the format of the pubkey. This probably should be returned + // from btcec, but do it here to avoid API churn. We already know the + // pubkey is valid since it parsed above, so it's safe to simply examine + // the leading byte to get the format. + pkFormat := PKFUncompressed + switch serializedPubKey[0] { + case 0x02, 0x03: + pkFormat = PKFCompressed + case 0x06, 0x07: + pkFormat = PKFHybrid + } + params := lparams.ConvertParams(net) + + return &AddressPubKey{ + pubKeyFormat: pkFormat, + pubKey: pubKey, + pubKeyHashID: params.PubKeyHashAddrID, + }, nil +} + +// serialize returns the serialization of the public key according to the +// format associated with the address. +func (a *AddressPubKey) serialize() []byte { + switch a.pubKeyFormat { + default: + fallthrough + case PKFUncompressed: + return a.pubKey.SerializeUncompressed() + + case PKFCompressed: + return a.pubKey.SerializeCompressed() + + case PKFHybrid: + return a.pubKey.SerializeHybrid() + } +} + +// EncodeAddress returns the string encoding of the public key as a +// pay-to-pubkey-hash. Note that the public key format (uncompressed, +// compressed, etc) will change the resulting address. This is expected since +// pay-to-pubkey-hash is a hash of the serialized public key which obviously +// differs with the format. At the time of this writing, most Bitcoin addresses +// are pay-to-pubkey-hash constructed from the uncompressed public key. +// +// Part of the Address interface. +func (a *AddressPubKey) EncodeAddress() string { + return encodeAddress(btcutil.Hash160(a.serialize()), a.pubKeyHashID) +} + +// ScriptAddress returns the bytes to be included in a txout script to pay +// to a public key. Setting the public key format will affect the output of +// this function accordingly. Part of the Address interface. +func (a *AddressPubKey) ScriptAddress() []byte { + return a.serialize() +} + +// IsForNet returns whether or not the pay-to-pubkey address is associated +// with the passed gleecbtc network. +func (a *AddressPubKey) IsForNet(net *chaincfg.Params) bool { + params := lparams.ConvertParams(net) + return a.pubKeyHashID == params.PubKeyHashAddrID +} + +// String returns the hex-encoded human-readable string for the pay-to-pubkey +// address. This is not the same as calling EncodeAddress. +func (a *AddressPubKey) String() string { + return hex.EncodeToString(a.serialize()) +} + +// Format returns the format (uncompressed, compressed, etc) of the +// pay-to-pubkey address. +func (a *AddressPubKey) Format() PubKeyFormat { + return a.pubKeyFormat +} + +// SetFormat sets the format (uncompressed, compressed, etc) of the +// pay-to-pubkey address. +func (a *AddressPubKey) SetFormat(pkFormat PubKeyFormat) { + a.pubKeyFormat = pkFormat +} + +// AddressPubKeyHash returns the pay-to-pubkey address converted to a +// pay-to-pubkey-hash address. Note that the public key format (uncompressed, +// compressed, etc) will change the resulting address. This is expected since +// pay-to-pubkey-hash is a hash of the serialized public key which obviously +// differs with the format. At the time of this writing, most Bitcoin addresses +// are pay-to-pubkey-hash constructed from the uncompressed public key. +func (a *AddressPubKey) AddressPubKeyHash() *AddressPubKeyHash { + addr := &AddressPubKeyHash{netID: a.pubKeyHashID} + copy(addr.hash[:], btcutil.Hash160(a.serialize())) + return addr +} + +// PubKey returns the underlying public key for the address. +func (a *AddressPubKey) PubKey() *btcec.PublicKey { + return a.pubKey +} + +// AddressWitnessPubKeyHash is an Address for a pay-to-witness-pubkey-hash +// (P2WPKH) output. See BIP 173 for further details regarding native segregated +// witness address encoding: +// https://github.com/gleecbtc/bips/blob/master/bip-0173.mediawiki +type AddressWitnessPubKeyHash struct { + hrp string + witnessVersion byte + witnessProgram [20]byte +} + +// NewAddressWitnessPubKeyHash returns a new AddressWitnessPubKeyHash. +func NewAddressWitnessPubKeyHash(witnessProg []byte, net *chaincfg.Params) (*AddressWitnessPubKeyHash, error) { + params := lparams.ConvertParams(net) + return newAddressWitnessPubKeyHash(params.Bech32HRPSegwit, witnessProg) +} + +// newAddressWitnessPubKeyHash is an internal helper function to create an +// AddressWitnessPubKeyHash with a known human-readable part, rather than +// looking it up through its parameters. +func newAddressWitnessPubKeyHash(hrp string, witnessProg []byte) (*AddressWitnessPubKeyHash, error) { + // Check for valid program length for witness version 0, which is 20 + // for P2WPKH. + if len(witnessProg) != 20 { + return nil, errors.New("witness program must be 20 " + + "bytes for p2wpkh") + } + + addr := &AddressWitnessPubKeyHash{ + hrp: strings.ToLower(hrp), + witnessVersion: 0x00, + } + + copy(addr.witnessProgram[:], witnessProg) + + return addr, nil +} + +// EncodeAddress returns the bech32 string encoding of an +// AddressWitnessPubKeyHash. +// Part of the Address interface. +func (a *AddressWitnessPubKeyHash) EncodeAddress() string { + str, err := encodeSegWitAddress(a.hrp, a.witnessVersion, + a.witnessProgram[:]) + if err != nil { + return "" + } + return str +} + +// ScriptAddress returns the witness program for this address. +// Part of the Address interface. +func (a *AddressWitnessPubKeyHash) ScriptAddress() []byte { + return a.witnessProgram[:] +} + +// IsForNet returns whether or not the AddressWitnessPubKeyHash is associated +// with the passed gleecbtc network. +// Part of the Address interface. +func (a *AddressWitnessPubKeyHash) IsForNet(net *chaincfg.Params) bool { + params := lparams.ConvertParams(net) + return a.hrp == params.Bech32HRPSegwit +} + +// String returns a human-readable string for the AddressWitnessPubKeyHash. +// This is equivalent to calling EncodeAddress, but is provided so the type +// can be used as a fmt.Stringer. +// Part of the Address interface. +func (a *AddressWitnessPubKeyHash) String() string { + return a.EncodeAddress() +} + +// Hrp returns the human-readable part of the bech32 encoded +// AddressWitnessPubKeyHash. +func (a *AddressWitnessPubKeyHash) Hrp() string { + return a.hrp +} + +// WitnessVersion returns the witness version of the AddressWitnessPubKeyHash. +func (a *AddressWitnessPubKeyHash) WitnessVersion() byte { + return a.witnessVersion +} + +// WitnessProgram returns the witness program of the AddressWitnessPubKeyHash. +func (a *AddressWitnessPubKeyHash) WitnessProgram() []byte { + return a.witnessProgram[:] +} + +// Hash160 returns the witness program of the AddressWitnessPubKeyHash as a +// byte array. +func (a *AddressWitnessPubKeyHash) Hash160() *[20]byte { + return &a.witnessProgram +} + +// AddressWitnessScriptHash is an Address for a pay-to-witness-script-hash +// (P2WSH) output. See BIP 173 for further details regarding native segregated +// witness address encoding: +// https://github.com/gleecbtc/bips/blob/master/bip-0173.mediawiki +type AddressWitnessScriptHash struct { + hrp string + witnessVersion byte + witnessProgram [32]byte +} + +// NewAddressWitnessScriptHash returns a new AddressWitnessPubKeyHash. +func NewAddressWitnessScriptHash(witnessProg []byte, net *chaincfg.Params) (*AddressWitnessScriptHash, error) { + params := lparams.ConvertParams(net) + return newAddressWitnessScriptHash(params.Bech32HRPSegwit, witnessProg) +} + +// newAddressWitnessScriptHash is an internal helper function to create an +// AddressWitnessScriptHash with a known human-readable part, rather than +// looking it up through its parameters. +func newAddressWitnessScriptHash(hrp string, witnessProg []byte) (*AddressWitnessScriptHash, error) { + // Check for valid program length for witness version 0, which is 32 + // for P2WSH. + if len(witnessProg) != 32 { + return nil, errors.New("witness program must be 32 " + + "bytes for p2wsh") + } + + addr := &AddressWitnessScriptHash{ + hrp: strings.ToLower(hrp), + witnessVersion: 0x00, + } + + copy(addr.witnessProgram[:], witnessProg) + + return addr, nil +} + +// EncodeAddress returns the bech32 string encoding of an +// AddressWitnessScriptHash. +// Part of the Address interface. +func (a *AddressWitnessScriptHash) EncodeAddress() string { + str, err := encodeSegWitAddress(a.hrp, a.witnessVersion, + a.witnessProgram[:]) + if err != nil { + return "" + } + return str +} + +// ScriptAddress returns the witness program for this address. +// Part of the Address interface. +func (a *AddressWitnessScriptHash) ScriptAddress() []byte { + return a.witnessProgram[:] +} + +// IsForNet returns whether or not the AddressWitnessScriptHash is associated +// with the passed gleecbtc network. +// Part of the Address interface. +func (a *AddressWitnessScriptHash) IsForNet(net *chaincfg.Params) bool { + params := lparams.ConvertParams(net) + return a.hrp == params.Bech32HRPSegwit +} + +// String returns a human-readable string for the AddressWitnessScriptHash. +// This is equivalent to calling EncodeAddress, but is provided so the type +// can be used as a fmt.Stringer. +// Part of the Address interface. +func (a *AddressWitnessScriptHash) String() string { + return a.EncodeAddress() +} + +// Hrp returns the human-readable part of the bech32 encoded +// AddressWitnessScriptHash. +func (a *AddressWitnessScriptHash) Hrp() string { + return a.hrp +} + +// WitnessVersion returns the witness version of the AddressWitnessScriptHash. +func (a *AddressWitnessScriptHash) WitnessVersion() byte { + return a.witnessVersion +} + +// WitnessProgram returns the witness program of the AddressWitnessScriptHash. +func (a *AddressWitnessScriptHash) WitnessProgram() []byte { + return a.witnessProgram[:] +} + +// PayToAddrScript creates a new script to pay a transaction output to a the +// specified address. +func PayToAddrScript(addr btcutil.Address) ([]byte, error) { + const nilAddrErrStr = "unable to generate payment script for nil address" + + switch addr := addr.(type) { + case *AddressPubKeyHash: + if addr == nil { + return nil, errors.New(nilAddrErrStr) + } + return payToPubKeyHashScript(addr.ScriptAddress()) + case *AddressWitnessScriptHash: + if addr == nil { + return nil, errors.New(nilAddrErrStr) + } + return payToWitnessScriptHashScript(addr.ScriptAddress()) + case *AddressScriptHash: + if addr == nil { + return nil, errors.New(nilAddrErrStr) + } + return payToScriptHashScript(addr.ScriptAddress()) + } + return nil, fmt.Errorf("unable to generate payment script for unsupported "+ + "address type %T", addr) +} + +// payToPubKeyHashScript creates a new script to pay a transaction +// output to a 20-byte pubkey hash. It is expected that the input is a valid +// hash. +func payToPubKeyHashScript(pubKeyHash []byte) ([]byte, error) { + return txscript.NewScriptBuilder().AddOp(txscript.OP_DUP).AddOp(txscript.OP_HASH160). + AddData(pubKeyHash).AddOp(txscript.OP_EQUALVERIFY).AddOp(txscript.OP_CHECKSIG). + Script() +} + +// payToScriptHashScript creates a new script to pay a transaction output to a +// script hash. It is expected that the input is a valid hash. +func payToScriptHashScript(scriptHash []byte) ([]byte, error) { + return txscript.NewScriptBuilder().AddOp(txscript.OP_HASH160).AddData(scriptHash). + AddOp(txscript.OP_EQUAL).Script() +} + +// payToWitnessPubKeyHashScript creates a new script to pay to a version 0 +// script hash witness program. The passed hash is expected to be valid. +func payToWitnessScriptHashScript(scriptHash []byte) ([]byte, error) { + return txscript.NewScriptBuilder().AddOp(txscript.OP_0).AddData(scriptHash).Script() +} + +func ExtractPkScriptAddrs(pkScript []byte, chainParams *chaincfg.Params) (btcutil.Address, error) { + // No valid addresses or required signatures if the script doesn't + if len(pkScript) == 1+1+20+1 && pkScript[0] == 0xa9 && pkScript[1] == 0x14 && pkScript[22] == 0x87 { + return NewAddressScriptHashFromHash(pkScript[2:22], chainParams) + } else if len(pkScript) == 1+1+1+20+1+1 && pkScript[0] == 0x76 && pkScript[1] == 0xa9 && pkScript[2] == 0x14 && pkScript[23] == 0x88 && pkScript[24] == 0xac { + return NewAddressPubKeyHash(pkScript[3:23], chainParams) + } else if len(pkScript) == 1+1+32 && pkScript[0] == 0x00 && pkScript[1] == 0x20 { + return NewAddressWitnessScriptHash(pkScript[2:], chainParams) + } else if len(pkScript) == 1+1+20 && pkScript[0] == 0x00 && pkScript[1] == 0x14 { + return NewAddressWitnessPubKeyHash(pkScript[2:], chainParams) + } + return nil, errors.New("unknown script type") +} + +// IsScriptHashAddrID returns whether the id is an identifier known to prefix a +// pay-to-script-hash address on any default or registered network. This is +// used when decoding an address string into a specific address type. It is up +// to the caller to check both this and IsPubKeyHashAddrID and decide whether an +// address is a pubkey hash address, script hash address, neither, or +// undeterminable (if both return true). +func IsScriptHashAddrID(id byte) bool { + _, ok := scriptHashAddrIDs[id] + return ok +} diff --git a/gleecbtc/exchange_rates.go b/gleecbtc/exchange_rates.go new file mode 100644 index 0000000..0a9ffb2 --- /dev/null +++ b/gleecbtc/exchange_rates.go @@ -0,0 +1,339 @@ +package gleecbtc + +import ( + "encoding/json" + "errors" + "net/http" + "reflect" + "strconv" + "sync" + "time" + + exchange "github.com/OpenBazaar/spvwallet/exchangerates" + "golang.org/x/net/proxy" + "strings" +) + +type ExchangeRateProvider struct { + fetchUrl string + cache map[string]float64 + client *http.Client + decoder ExchangeRateDecoder + bitcoinProvider *exchange.BitcoinPriceFetcher +} + +type ExchangeRateDecoder interface { + decode(dat interface{}, cache map[string]float64, bp *exchange.BitcoinPriceFetcher) (err error) +} + +type OpenBazaarDecoder struct{} +type KrakenDecoder struct{} +type PoloniexDecoder struct{} +type BitfinexDecoder struct{} +type BittrexDecoder struct{} + +type GleecbtcPriceFetcher struct { + sync.Mutex + cache map[string]float64 + providers []*ExchangeRateProvider +} + +func NewGleecbtcPriceFetcher(dialer proxy.Dialer) *GleecbtcPriceFetcher { + bp := exchange.NewBitcoinPriceFetcher(dialer) + z := GleecbtcPriceFetcher{ + cache: make(map[string]float64), + } + + var client *http.Client + if dialer != nil { + dial := dialer.Dial + tbTransport := &http.Transport{Dial: dial} + client = &http.Client{Transport: tbTransport, Timeout: time.Minute} + } else { + client = &http.Client{Timeout: time.Minute} + } + + z.providers = []*ExchangeRateProvider{ + {"https://ticker.openbazaar.org/api", z.cache, client, OpenBazaarDecoder{}, nil}, + {"https://bittrex.com/api/v1.1/public/getticker?market=btc-ltc", z.cache, client, BittrexDecoder{}, bp}, + {"https://api.bitfinex.com/v1/pubticker/ltcbtc", z.cache, client, BitfinexDecoder{}, bp}, + {"https://poloniex.com/public?command=returnTicker", z.cache, client, PoloniexDecoder{}, bp}, + {"https://api.kraken.com/0/public/Ticker?pair=LTCXBT", z.cache, client, KrakenDecoder{}, bp}, + } + go z.run() + return &z +} + +func (z *GleecbtcPriceFetcher) GetExchangeRate(currencyCode string) (float64, error) { + currencyCode = NormalizeCurrencyCode(currencyCode) + + z.Lock() + defer z.Unlock() + price, ok := z.cache[currencyCode] + if !ok { + return 0, errors.New("Currency not tracked") + } + return price, nil +} + +func (z *GleecbtcPriceFetcher) GetLatestRate(currencyCode string) (float64, error) { + currencyCode = NormalizeCurrencyCode(currencyCode) + + z.fetchCurrentRates() + z.Lock() + defer z.Unlock() + price, ok := z.cache[currencyCode] + if !ok { + return 0, errors.New("Currency not tracked") + } + return price, nil +} + +func (z *GleecbtcPriceFetcher) GetAllRates(cacheOK bool) (map[string]float64, error) { + if !cacheOK { + err := z.fetchCurrentRates() + if err != nil { + return nil, err + } + } + z.Lock() + defer z.Unlock() + copy := make(map[string]float64, len(z.cache)) + for k, v := range z.cache { + copy[k] = v + } + return copy, nil +} + +func (z *GleecbtcPriceFetcher) UnitsPerCoin() int { + return exchange.SatoshiPerBTC +} + +func (z *GleecbtcPriceFetcher) fetchCurrentRates() error { + z.Lock() + defer z.Unlock() + for _, provider := range z.providers { + err := provider.fetch() + if err == nil { + return nil + } + } + return errors.New("all exchange rate API queries failed") +} + +func (z *GleecbtcPriceFetcher) run() { + z.fetchCurrentRates() + ticker := time.NewTicker(time.Minute * 15) + for range ticker.C { + z.fetchCurrentRates() + } +} + +func (provider *ExchangeRateProvider) fetch() (err error) { + if len(provider.fetchUrl) == 0 { + err = errors.New("provider has no fetchUrl") + return err + } + resp, err := provider.client.Get(provider.fetchUrl) + if err != nil { + return err + } + decoder := json.NewDecoder(resp.Body) + var dataMap interface{} + err = decoder.Decode(&dataMap) + if err != nil { + return err + } + return provider.decoder.decode(dataMap, provider.cache, provider.bitcoinProvider) +} + +func (b OpenBazaarDecoder) decode(dat interface{}, cache map[string]float64, bp *exchange.BitcoinPriceFetcher) (err error) { + data, ok := dat.(map[string]interface{}) + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed invalid json") + } + + ltc, ok := data["LTC"] + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed, missing 'ZEC' field") + } + val, ok := ltc.(map[string]interface{}) + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed") + } + ltcRate, ok := val["last"].(float64) + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed, missing 'last' (float) field") + } + for k, v := range data { + if k != "timestamp" { + val, ok := v.(map[string]interface{}) + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed") + } + price, ok := val["last"].(float64) + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed, missing 'last' (float) field") + } + cache[k] = price * (1 / ltcRate) + } + } + return nil +} + +func (b KrakenDecoder) decode(dat interface{}, cache map[string]float64, bp *exchange.BitcoinPriceFetcher) (err error) { + rates, err := bp.GetAllRates(false) + if err != nil { + return err + } + obj, ok := dat.(map[string]interface{}) + if !ok { + return errors.New("KrackenDecoder type assertion failure") + } + result, ok := obj["result"] + if !ok { + return errors.New("KrakenDecoder: field `result` not found") + } + resultMap, ok := result.(map[string]interface{}) + if !ok { + return errors.New("KrackenDecoder type assertion failure") + } + pair, ok := resultMap["XLTCXXBT"] + if !ok { + return errors.New("KrakenDecoder: field `BCHXBT` not found") + } + pairMap, ok := pair.(map[string]interface{}) + if !ok { + return errors.New("KrackenDecoder type assertion failure") + } + c, ok := pairMap["c"] + if !ok { + return errors.New("KrakenDecoder: field `c` not found") + } + cList, ok := c.([]interface{}) + if !ok { + return errors.New("KrackenDecoder type assertion failure") + } + rateStr, ok := cList[0].(string) + if !ok { + return errors.New("KrackenDecoder type assertion failure") + } + price, err := strconv.ParseFloat(rateStr, 64) + if err != nil { + return err + } + rate := price + + if rate == 0 { + return errors.New("Bitcoin-gleecbtc price data not available") + } + for k, v := range rates { + cache[k] = v * rate + } + return nil +} + +func (b BitfinexDecoder) decode(dat interface{}, cache map[string]float64, bp *exchange.BitcoinPriceFetcher) (err error) { + rates, err := bp.GetAllRates(false) + if err != nil { + return err + } + obj, ok := dat.(map[string]interface{}) + if !ok { + return errors.New("BitfinexDecoder type assertion failure") + } + r, ok := obj["last_price"] + if !ok { + return errors.New("BitfinexDecoder: field `last_price` not found") + } + rateStr, ok := r.(string) + if !ok { + return errors.New("BitfinexDecoder type assertion failure") + } + price, err := strconv.ParseFloat(rateStr, 64) + if err != nil { + return err + } + rate := price + + if rate == 0 { + return errors.New("Bitcoin-gleecbtc price data not available") + } + for k, v := range rates { + cache[k] = v * rate + } + return nil +} + +func (b BittrexDecoder) decode(dat interface{}, cache map[string]float64, bp *exchange.BitcoinPriceFetcher) (err error) { + rates, err := bp.GetAllRates(false) + if err != nil { + return err + } + obj, ok := dat.(map[string]interface{}) + if !ok { + return errors.New("BittrexDecoder type assertion failure") + } + result, ok := obj["result"] + if !ok { + return errors.New("BittrexDecoder: field `result` not found") + } + resultMap, ok := result.(map[string]interface{}) + if !ok { + return errors.New("BittrexDecoder type assertion failure") + } + exRate, ok := resultMap["Last"] + if !ok { + return errors.New("BittrexDecoder: field `Last` not found") + } + rate, ok := exRate.(float64) + if !ok { + return errors.New("BittrexDecoder type assertion failure") + } + + if rate == 0 { + return errors.New("Bitcoin-gleecbtc price data not available") + } + for k, v := range rates { + cache[k] = v * rate + } + return nil +} + +func (b PoloniexDecoder) decode(dat interface{}, cache map[string]float64, bp *exchange.BitcoinPriceFetcher) (err error) { + rates, err := bp.GetAllRates(false) + if err != nil { + return err + } + data, ok := dat.(map[string]interface{}) + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed") + } + var rate float64 + v := data["BTC_LTC"] + val, ok := v.(map[string]interface{}) + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed") + } + s, ok := val["last"].(string) + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed, missing 'last' (string) field") + } + price, err := strconv.ParseFloat(s, 64) + if err != nil { + return err + } + rate = price + if rate == 0 { + return errors.New("Bitcoin-gleecbtc price data not available") + } + for k, v := range rates { + cache[k] = v * rate + } + return nil +} + +// NormalizeCurrencyCode standardizes the format for the given currency code +func NormalizeCurrencyCode(currencyCode string) string { + return strings.ToUpper(currencyCode) +} diff --git a/gleecbtc/params/params.go b/gleecbtc/params/params.go new file mode 100644 index 0000000..48d496d --- /dev/null +++ b/gleecbtc/params/params.go @@ -0,0 +1,21 @@ +package params + +import ( + "github.com/btcsuite/btcd/chaincfg" + l "github.com/ltcsuite/ltcd/chaincfg" +) + +func init() { + l.MainNetParams.ScriptHashAddrID = 0x26 +} + +func ConvertParams(params *chaincfg.Params) l.Params { + switch params.Name { + case chaincfg.MainNetParams.Name: + return l.MainNetParams + case chaincfg.TestNet3Params.Name: + return l.TestNet4Params + default: + return l.RegressionNetParams + } +} diff --git a/gleecbtc/sign.go b/gleecbtc/sign.go new file mode 100644 index 0000000..270213d --- /dev/null +++ b/gleecbtc/sign.go @@ -0,0 +1,675 @@ +package gleecbtc + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "time" + + "github.com/btcsuite/btcd/chaincfg" + + "github.com/OpenBazaar/spvwallet" + wi "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + btc "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/coinset" + hd "github.com/btcsuite/btcutil/hdkeychain" + "github.com/btcsuite/btcutil/txsort" + "github.com/btcsuite/btcwallet/wallet/txauthor" + "github.com/ltcsuite/ltcutil" + "github.com/ltcsuite/ltcwallet/wallet/txrules" + + laddr "github.com/OpenBazaar/multiwallet/gleecbtc/address" + "github.com/OpenBazaar/multiwallet/util" +) + +func (w *GleecbtcWallet) buildTx(amount int64, addr btc.Address, feeLevel wi.FeeLevel, optionalOutput *wire.TxOut) (*wire.MsgTx, error) { + // Check for dust + script, _ := laddr.PayToAddrScript(addr) + if txrules.IsDustAmount(ltcutil.Amount(amount), len(script), txrules.DefaultRelayFeePerKb) { + return nil, wi.ErrorDustAmount + } + + var additionalPrevScripts map[wire.OutPoint][]byte + var additionalKeysByAddress map[string]*btc.WIF + + // Create input source + height, _ := w.ws.ChainTip() + utxos, err := w.db.Utxos().GetAll() + if err != nil { + return nil, err + } + coinMap := util.GatherCoins(height, utxos, w.ScriptToAddress, w.km.GetKeyForScript) + + coins := make([]coinset.Coin, 0, len(coinMap)) + for k := range coinMap { + coins = append(coins, k) + } + inputSource := func(target btc.Amount) (total btc.Amount, inputs []*wire.TxIn, inputValues []btc.Amount, scripts [][]byte, err error) { + coinSelector := coinset.MaxValueAgeCoinSelector{MaxInputs: 10000, MinChangeAmount: btc.Amount(0)} + coins, err := coinSelector.CoinSelect(target, coins) + if err != nil { + return total, inputs, inputValues, scripts, wi.ErrorInsuffientFunds + } + additionalPrevScripts = make(map[wire.OutPoint][]byte) + additionalKeysByAddress = make(map[string]*btc.WIF) + for _, c := range coins.Coins() { + total += c.Value() + outpoint := wire.NewOutPoint(c.Hash(), c.Index()) + in := wire.NewTxIn(outpoint, []byte{}, [][]byte{}) + in.Sequence = 0 // Opt-in RBF so we can bump fees + inputs = append(inputs, in) + additionalPrevScripts[*outpoint] = c.PkScript() + key := coinMap[c] + addr, err := w.km.KeyToAddress(key) + if err != nil { + continue + } + privKey, err := key.ECPrivKey() + if err != nil { + continue + } + wif, _ := btc.NewWIF(privKey, w.params, true) + additionalKeysByAddress[addr.EncodeAddress()] = wif + } + return total, inputs, inputValues, scripts, nil + } + + // Get the fee per kilobyte + feePerKB := int64(w.GetFeePerByte(feeLevel)) * 1000 + + // outputs + out := wire.NewTxOut(amount, script) + + // Create change source + changeSource := func() ([]byte, error) { + addr := w.CurrentAddress(wi.INTERNAL) + script, err := laddr.PayToAddrScript(addr) + if err != nil { + return []byte{}, err + } + return script, nil + } + + outputs := []*wire.TxOut{out} + if optionalOutput != nil { + outputs = append(outputs, optionalOutput) + } + authoredTx, err := newUnsignedTransaction(outputs, btc.Amount(feePerKB), inputSource, changeSource) + if err != nil { + return nil, err + } + + // BIP 69 sorting + txsort.InPlaceSort(authoredTx.Tx) + + // Sign tx + getKey := txscript.KeyClosure(func(addr btc.Address) (*btcec.PrivateKey, bool, error) { + a, err := laddr.NewAddressPubKeyHash(addr.ScriptAddress(), w.params) + if err != nil { + return nil, false, err + } + wif := additionalKeysByAddress[a.EncodeAddress()] + return wif.PrivKey, wif.CompressPubKey, nil + }) + getScript := txscript.ScriptClosure(func( + addr btc.Address) ([]byte, error) { + return []byte{}, nil + }) + for i, txIn := range authoredTx.Tx.TxIn { + prevOutScript := additionalPrevScripts[txIn.PreviousOutPoint] + script, err := txscript.SignTxOutput(w.params, + authoredTx.Tx, i, prevOutScript, txscript.SigHashAll, getKey, + getScript, txIn.SignatureScript) + if err != nil { + return nil, errors.New("Failed to sign transaction") + } + txIn.SignatureScript = script + } + return authoredTx.Tx, nil +} + +func (w *GleecbtcWallet) buildSpendAllTx(addr btc.Address, feeLevel wi.FeeLevel) (*wire.MsgTx, error) { + tx := wire.NewMsgTx(1) + + height, _ := w.ws.ChainTip() + utxos, err := w.db.Utxos().GetAll() + if err != nil { + return nil, err + } + coinMap := util.GatherCoins(height, utxos, w.ScriptToAddress, w.km.GetKeyForScript) + + totalIn, _, additionalPrevScripts, additionalKeysByAddress := util.LoadAllInputs(tx, coinMap, w.params) + + // outputs + script, err := laddr.PayToAddrScript(addr) + if err != nil { + return nil, err + } + + // Get the fee + feePerByte := int64(w.GetFeePerByte(feeLevel)) + estimatedSize := EstimateSerializeSize(1, []*wire.TxOut{wire.NewTxOut(0, script)}, false, P2PKH) + fee := int64(estimatedSize) * feePerByte + + // Check for dust output + if txrules.IsDustAmount(ltcutil.Amount(totalIn-fee), len(script), txrules.DefaultRelayFeePerKb) { + return nil, wi.ErrorDustAmount + } + + // Build the output + out := wire.NewTxOut(totalIn-fee, script) + tx.TxOut = append(tx.TxOut, out) + + // BIP 69 sorting + txsort.InPlaceSort(tx) + + // Sign + getKey := txscript.KeyClosure(func(addr btc.Address) (*btcec.PrivateKey, bool, error) { + addrStr := addr.EncodeAddress() + wif, ok := additionalKeysByAddress[addrStr] + if !ok { + return nil, false, errors.New("key not found") + } + return wif.PrivKey, wif.CompressPubKey, nil + }) + getScript := txscript.ScriptClosure(func( + addr btc.Address) ([]byte, error) { + return []byte{}, nil + }) + for i, txIn := range tx.TxIn { + prevOutScript := additionalPrevScripts[txIn.PreviousOutPoint] + script, err := txscript.SignTxOutput(w.params, + tx, i, prevOutScript, txscript.SigHashAll, getKey, + getScript, txIn.SignatureScript) + if err != nil { + return nil, errors.New("failed to sign transaction") + } + txIn.SignatureScript = script + } + return tx, nil +} + +func newUnsignedTransaction(outputs []*wire.TxOut, feePerKb btc.Amount, fetchInputs txauthor.InputSource, fetchChange txauthor.ChangeSource) (*txauthor.AuthoredTx, error) { + + var targetAmount btc.Amount + for _, txOut := range outputs { + targetAmount += btc.Amount(txOut.Value) + } + + estimatedSize := EstimateSerializeSize(1, outputs, true, P2PKH) + targetFee := txrules.FeeForSerializeSize(ltcutil.Amount(feePerKb), estimatedSize) + + for { + inputAmount, inputs, _, scripts, err := fetchInputs(targetAmount + btc.Amount(targetFee)) + if err != nil { + return nil, err + } + if inputAmount < targetAmount+btc.Amount(targetFee) { + return nil, errors.New("insufficient funds available to construct transaction") + } + + maxSignedSize := EstimateSerializeSize(len(inputs), outputs, true, P2PKH) + maxRequiredFee := txrules.FeeForSerializeSize(ltcutil.Amount(feePerKb), maxSignedSize) + remainingAmount := inputAmount - targetAmount + if remainingAmount < btc.Amount(maxRequiredFee) { + targetFee = maxRequiredFee + continue + } + + unsignedTransaction := &wire.MsgTx{ + Version: wire.TxVersion, + TxIn: inputs, + TxOut: outputs, + LockTime: 0, + } + changeIndex := -1 + changeAmount := inputAmount - targetAmount - btc.Amount(maxRequiredFee) + if changeAmount != 0 && !txrules.IsDustAmount(ltcutil.Amount(changeAmount), + P2PKHOutputSize, txrules.DefaultRelayFeePerKb) { + changeScript, err := fetchChange() + if err != nil { + return nil, err + } + if len(changeScript) > P2PKHPkScriptSize { + return nil, errors.New("fee estimation requires change " + + "scripts no larger than P2PKH output scripts") + } + change := wire.NewTxOut(int64(changeAmount), changeScript) + l := len(outputs) + unsignedTransaction.TxOut = append(outputs[:l:l], change) + changeIndex = l + } + + return &txauthor.AuthoredTx{ + Tx: unsignedTransaction, + PrevScripts: scripts, + TotalInput: inputAmount, + ChangeIndex: changeIndex, + }, nil + } +} + +func (w *GleecbtcWallet) bumpFee(txid chainhash.Hash) (*chainhash.Hash, error) { + txn, err := w.db.Txns().Get(txid) + if err != nil { + return nil, err + } + if txn.Height > 0 { + return nil, spvwallet.BumpFeeAlreadyConfirmedError + } + if txn.Height < 0 { + return nil, spvwallet.BumpFeeTransactionDeadError + } + // Check utxos for CPFP + utxos, _ := w.db.Utxos().GetAll() + for _, u := range utxos { + if u.Op.Hash.IsEqual(&txid) && u.AtHeight == 0 { + addr, err := w.ScriptToAddress(u.ScriptPubkey) + if err != nil { + return nil, err + } + key, err := w.km.GetKeyForScript(addr.ScriptAddress()) + if err != nil { + return nil, err + } + h, err := hex.DecodeString(u.Op.Hash.String()) + if err != nil { + return nil, err + } + in := wi.TransactionInput{ + LinkedAddress: addr, + OutpointIndex: u.Op.Index, + OutpointHash: h, + Value: int64(u.Value), + } + transactionID, err := w.sweepAddress([]wi.TransactionInput{in}, nil, key, nil, wi.FEE_BUMP) + if err != nil { + return nil, err + } + return transactionID, nil + } + } + return nil, spvwallet.BumpFeeNotFoundError +} + +func (w *GleecbtcWallet) sweepAddress(ins []wi.TransactionInput, address *btc.Address, key *hd.ExtendedKey, redeemScript *[]byte, feeLevel wi.FeeLevel) (*chainhash.Hash, error) { + var internalAddr btc.Address + if address != nil { + internalAddr = *address + } else { + internalAddr = w.CurrentAddress(wi.INTERNAL) + } + script, err := laddr.PayToAddrScript(internalAddr) + if err != nil { + return nil, err + } + + var val int64 + var inputs []*wire.TxIn + additionalPrevScripts := make(map[wire.OutPoint][]byte) + for _, in := range ins { + val += in.Value + ch, err := chainhash.NewHashFromStr(hex.EncodeToString(in.OutpointHash)) + if err != nil { + return nil, err + } + script, err := laddr.PayToAddrScript(in.LinkedAddress) + if err != nil { + return nil, err + } + outpoint := wire.NewOutPoint(ch, in.OutpointIndex) + input := wire.NewTxIn(outpoint, []byte{}, [][]byte{}) + inputs = append(inputs, input) + additionalPrevScripts[*outpoint] = script + } + out := wire.NewTxOut(val, script) + + txType := P2PKH + if redeemScript != nil { + txType = P2SH_1of2_Multisig + _, err := spvwallet.LockTimeFromRedeemScript(*redeemScript) + if err == nil { + txType = P2SH_Multisig_Timelock_1Sig + } + } + estimatedSize := EstimateSerializeSize(len(ins), []*wire.TxOut{out}, false, txType) + + // Calculate the fee + feePerByte := int(w.GetFeePerByte(feeLevel)) + fee := estimatedSize * feePerByte + + outVal := val - int64(fee) + if outVal < 0 { + outVal = 0 + } + out.Value = outVal + + tx := &wire.MsgTx{ + Version: wire.TxVersion, + TxIn: inputs, + TxOut: []*wire.TxOut{out}, + LockTime: 0, + } + + // BIP 69 sorting + txsort.InPlaceSort(tx) + + // Sign tx + privKey, err := key.ECPrivKey() + if err != nil { + return nil, fmt.Errorf("retrieving private key: %s", err.Error()) + } + pk := privKey.PubKey().SerializeCompressed() + addressPub, err := btc.NewAddressPubKey(pk, w.params) + if err != nil { + return nil, fmt.Errorf("generating address pub key: %s", err.Error()) + } + + getKey := txscript.KeyClosure(func(addr btc.Address) (*btcec.PrivateKey, bool, error) { + if addressPub.EncodeAddress() == addr.EncodeAddress() { + wif, err := btc.NewWIF(privKey, w.params, true) + if err != nil { + return nil, false, err + } + return wif.PrivKey, wif.CompressPubKey, nil + } + return nil, false, errors.New("Not found") + }) + getScript := txscript.ScriptClosure(func(addr btc.Address) ([]byte, error) { + if redeemScript == nil { + return []byte{}, nil + } + return *redeemScript, nil + }) + + // Check if time locked + var timeLocked bool + if redeemScript != nil { + rs := *redeemScript + if rs[0] == txscript.OP_IF { + timeLocked = true + tx.Version = 2 + for _, txIn := range tx.TxIn { + locktime, err := spvwallet.LockTimeFromRedeemScript(*redeemScript) + if err != nil { + return nil, err + } + txIn.Sequence = locktime + } + } + } + + hashes := txscript.NewTxSigHashes(tx) + for i, txIn := range tx.TxIn { + if redeemScript == nil { + prevOutScript := additionalPrevScripts[txIn.PreviousOutPoint] + script, err := txscript.SignTxOutput(w.params, + tx, i, prevOutScript, txscript.SigHashAll, getKey, + getScript, txIn.SignatureScript) + if err != nil { + return nil, errors.New("Failed to sign transaction") + } + txIn.SignatureScript = script + } else { + sig, err := txscript.RawTxInWitnessSignature(tx, hashes, i, ins[i].Value, *redeemScript, txscript.SigHashAll, privKey) + if err != nil { + return nil, err + } + var witness wire.TxWitness + if timeLocked { + witness = wire.TxWitness{sig, []byte{}} + } else { + witness = wire.TxWitness{[]byte{}, sig} + } + witness = append(witness, *redeemScript) + txIn.Witness = witness + } + } + + // broadcast + if err := w.Broadcast(tx); err != nil { + return nil, err + } + txid := tx.TxHash() + return &txid, nil +} + +func (w *GleecbtcWallet) createMultisigSignature(ins []wi.TransactionInput, outs []wi.TransactionOutput, key *hd.ExtendedKey, redeemScript []byte, feePerByte uint64) ([]wi.Signature, error) { + var sigs []wi.Signature + tx := wire.NewMsgTx(1) + for _, in := range ins { + ch, err := chainhash.NewHashFromStr(hex.EncodeToString(in.OutpointHash)) + if err != nil { + return sigs, err + } + outpoint := wire.NewOutPoint(ch, in.OutpointIndex) + input := wire.NewTxIn(outpoint, []byte{}, [][]byte{}) + tx.TxIn = append(tx.TxIn, input) + } + for _, out := range outs { + scriptPubkey, err := laddr.PayToAddrScript(out.Address) + if err != nil { + return sigs, err + } + output := wire.NewTxOut(out.Value, scriptPubkey) + tx.TxOut = append(tx.TxOut, output) + } + + // Subtract fee + txType := P2SH_2of3_Multisig + _, err := spvwallet.LockTimeFromRedeemScript(redeemScript) + if err == nil { + txType = P2SH_Multisig_Timelock_2Sigs + } + estimatedSize := EstimateSerializeSize(len(ins), tx.TxOut, false, txType) + fee := estimatedSize * int(feePerByte) + if len(tx.TxOut) > 0 { + feePerOutput := fee / len(tx.TxOut) + for _, output := range tx.TxOut { + output.Value -= int64(feePerOutput) + } + } + + // BIP 69 sorting + txsort.InPlaceSort(tx) + + signingKey, err := key.ECPrivKey() + if err != nil { + return sigs, err + } + + hashes := txscript.NewTxSigHashes(tx) + for i := range tx.TxIn { + sig, err := txscript.RawTxInWitnessSignature(tx, hashes, i, ins[i].Value, redeemScript, txscript.SigHashAll, signingKey) + if err != nil { + continue + } + bs := wi.Signature{InputIndex: uint32(i), Signature: sig} + sigs = append(sigs, bs) + } + return sigs, nil +} + +func (w *GleecbtcWallet) multisign(ins []wi.TransactionInput, outs []wi.TransactionOutput, sigs1 []wi.Signature, sigs2 []wi.Signature, redeemScript []byte, feePerByte uint64, broadcast bool) ([]byte, error) { + tx := wire.NewMsgTx(1) + for _, in := range ins { + ch, err := chainhash.NewHashFromStr(hex.EncodeToString(in.OutpointHash)) + if err != nil { + return nil, err + } + outpoint := wire.NewOutPoint(ch, in.OutpointIndex) + input := wire.NewTxIn(outpoint, []byte{}, [][]byte{}) + tx.TxIn = append(tx.TxIn, input) + } + for _, out := range outs { + scriptPubkey, err := laddr.PayToAddrScript(out.Address) + if err != nil { + return nil, err + } + output := wire.NewTxOut(out.Value, scriptPubkey) + tx.TxOut = append(tx.TxOut, output) + } + + // Subtract fee + txType := P2SH_2of3_Multisig + _, err := spvwallet.LockTimeFromRedeemScript(redeemScript) + if err == nil { + txType = P2SH_Multisig_Timelock_2Sigs + } + estimatedSize := EstimateSerializeSize(len(ins), tx.TxOut, false, txType) + fee := estimatedSize * int(feePerByte) + if len(tx.TxOut) > 0 { + feePerOutput := fee / len(tx.TxOut) + for _, output := range tx.TxOut { + output.Value -= int64(feePerOutput) + } + } + + // BIP 69 sorting + txsort.InPlaceSort(tx) + + // Check if time locked + var timeLocked bool + if redeemScript[0] == txscript.OP_IF { + timeLocked = true + } + + for i, input := range tx.TxIn { + var sig1 []byte + var sig2 []byte + for _, sig := range sigs1 { + if int(sig.InputIndex) == i { + sig1 = sig.Signature + break + } + } + for _, sig := range sigs2 { + if int(sig.InputIndex) == i { + sig2 = sig.Signature + break + } + } + + witness := wire.TxWitness{[]byte{}, sig1, sig2} + + if timeLocked { + witness = append(witness, []byte{0x01}) + } + witness = append(witness, redeemScript) + input.Witness = witness + } + // broadcast + if broadcast { + if err := w.Broadcast(tx); err != nil { + return nil, err + } + } + var buf bytes.Buffer + tx.BtcEncode(&buf, wire.ProtocolVersion, wire.WitnessEncoding) + return buf.Bytes(), nil +} + +func (w *GleecbtcWallet) generateMultisigScript(keys []hd.ExtendedKey, threshold int, timeout time.Duration, timeoutKey *hd.ExtendedKey) (addr btc.Address, redeemScript []byte, err error) { + if uint32(timeout.Hours()) > 0 && timeoutKey == nil { + return nil, nil, errors.New("Timeout key must be non nil when using an escrow timeout") + } + + if len(keys) < threshold { + return nil, nil, fmt.Errorf("unable to generate multisig script with "+ + "%d required signatures when there are only %d public "+ + "keys available", threshold, len(keys)) + } + + var ecKeys []*btcec.PublicKey + for _, key := range keys { + ecKey, err := key.ECPubKey() + if err != nil { + return nil, nil, err + } + ecKeys = append(ecKeys, ecKey) + } + + builder := txscript.NewScriptBuilder() + if uint32(timeout.Hours()) == 0 { + + builder.AddInt64(int64(threshold)) + for _, key := range ecKeys { + builder.AddData(key.SerializeCompressed()) + } + builder.AddInt64(int64(len(ecKeys))) + builder.AddOp(txscript.OP_CHECKMULTISIG) + + } else { + ecKey, err := timeoutKey.ECPubKey() + if err != nil { + return nil, nil, err + } + sequenceLock := blockchain.LockTimeToSequence(false, uint32(timeout.Hours()*6)) + builder.AddOp(txscript.OP_IF) + builder.AddInt64(int64(threshold)) + for _, key := range ecKeys { + builder.AddData(key.SerializeCompressed()) + } + builder.AddInt64(int64(len(ecKeys))) + builder.AddOp(txscript.OP_CHECKMULTISIG) + builder.AddOp(txscript.OP_ELSE). + AddInt64(int64(sequenceLock)). + AddOp(txscript.OP_CHECKSEQUENCEVERIFY). + AddOp(txscript.OP_DROP). + AddData(ecKey.SerializeCompressed()). + AddOp(txscript.OP_CHECKSIG). + AddOp(txscript.OP_ENDIF) + } + redeemScript, err = builder.Script() + if err != nil { + return nil, nil, err + } + + witnessProgram := sha256.Sum256(redeemScript) + + addr, err = laddr.NewAddressWitnessScriptHash(witnessProgram[:], w.params) + if err != nil { + return nil, nil, err + } + return addr, redeemScript, nil +} + +func (w *GleecbtcWallet) estimateSpendFee(amount int64, feeLevel wi.FeeLevel) (uint64, error) { + // Since this is an estimate we can use a dummy output address. Let's use a long one so we don't under estimate. + addr, err := laddr.DecodeAddress("ltc1q65n2p3r4pwz4qppflml65en4xpdp6srjwultrun6hnddpzct5unsyyq4sf", &chaincfg.MainNetParams) + if err != nil { + return 0, err + } + tx, err := w.buildTx(amount, addr, feeLevel, nil) + if err != nil { + return 0, err + } + var outval int64 + for _, output := range tx.TxOut { + outval += output.Value + } + var inval int64 + utxos, err := w.db.Utxos().GetAll() + if err != nil { + return 0, err + } + for _, input := range tx.TxIn { + for _, utxo := range utxos { + if utxo.Op.Hash.IsEqual(&input.PreviousOutPoint.Hash) && utxo.Op.Index == input.PreviousOutPoint.Index { + inval += utxo.Value + break + } + } + } + if inval < outval { + return 0, errors.New("Error building transaction: inputs less than outputs") + } + return uint64(inval - outval), err +} diff --git a/gleecbtc/txsizes.go b/gleecbtc/txsizes.go new file mode 100644 index 0000000..848aa3a --- /dev/null +++ b/gleecbtc/txsizes.go @@ -0,0 +1,249 @@ +package gleecbtc + +// Copyright (c) 2016 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +/* Copied here from a btcd internal package*/ + +import ( + "github.com/btcsuite/btcd/wire" +) + +// Worst case script and input/output size estimates. +const ( + // RedeemP2PKHSigScriptSize is the worst case (largest) serialize size + // of a transaction input script that redeems a compressed P2PKH output. + // It is calculated as: + // + // - OP_DATA_73 + // - 72 bytes DER signature + 1 byte sighash + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + RedeemP2PKHSigScriptSize = 1 + 73 + 1 + 33 + + // RedeemP2SHMultisigSigScriptSize is the worst case (largest) serialize size + // of a transaction input script that redeems a 2 of 3 P2SH multisig output with compressed keys. + // It is calculated as: + // + // - OP_0 + // - OP_DATA_72 + // - 72 bytes DER signature + // - OP_DATA_72 + // - 72 bytes DER signature + // - OP_PUSHDATA + // - OP_2 + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP3 + // - OP_CHECKMULTISIG + RedeemP2SH2of3MultisigSigScriptSize = 1 + 1 + 72 + 1 + 72 + 1 + 1 + 1 + 33 + 1 + 33 + 1 + 33 + 1 + 1 + + // RedeemP2SH1of2MultisigSigScriptSize is the worst case (largest) serialize size + // of a transaction input script that redeems a 1 of 2 P2SH multisig output with compressed keys. + // It is calculated as: + // + // - OP_0 + // - OP_DATA_72 + // - 72 bytes DER signature + // - OP_PUSHDATA + // - OP_1 + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP2 + // - OP_CHECKMULTISIG + RedeemP2SH1of2MultisigSigScriptSize = 1 + 1 + 72 + 1 + 1 + 1 + 33 + 1 + 33 + 1 + 1 + + // RedeemP2SHMultisigTimelock1SigScriptSize is the worst case (largest) serialize size + // of a transaction input script that redeems a compressed P2SH timelocked multisig using the timeout. + // It is calculated as: + // + // - OP_DATA_72 + // - 72 bytes DER signature + // - OP_0 + // - OP_PUSHDATA + // - OP_IF + // - OP_2 + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP3 + // - OP_CHECKMULTISIG + // - OP_ELSE + // - OP_PUSHDATA + // - 2 byte block height + // - OP_CHECKSEQUENCEVERIFY + // - OP_DROP + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_CHECKSIG + // - OP_ENDIF + RedeemP2SHMultisigTimelock1SigScriptSize = 1 + 72 + 1 + 1 + 1 + 1 + 1 + 33 + 1 + 33 + 1 + 33 + 1 + 1 + 1 + 1 + 2 + 1 + 1 + 1 + 33 + 1 + 1 + + // RedeemP2SHMultisigTimelock2SigScriptSize is the worst case (largest) serialize size + // of a transaction input script that redeems a compressed P2SH timelocked multisig without using the timeout. + // It is calculated as: + // + // - OP_0 + // - OP_DATA_72 + // - 72 bytes DER signature + // - OP_DATA_72 + // - 72 bytes DER signature + // - OP_1 + // - OP_PUSHDATA + // - OP_IF + // - OP_2 + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP3 + // - OP_CHECKMULTISIG + // - OP_ELSE + // - OP_PUSHDATA + // - 2 byte block height + // - OP_CHECKSEQUENCEVERIFY + // - OP_DROP + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_CHECKSIG + // - OP_ENDIF + RedeemP2SHMultisigTimelock2SigScriptSize = 1 + 1 + 72 + +1 + 72 + 1 + 1 + 1 + 1 + 1 + 33 + 1 + 33 + 1 + 33 + 1 + 1 + 1 + 1 + 2 + 1 + 1 + 1 + 33 + 1 + 1 + + // P2PKHPkScriptSize is the size of a transaction output script that + // pays to a compressed pubkey hash. It is calculated as: + // + // - OP_DUP + // - OP_HASH160 + // - OP_DATA_20 + // - 20 bytes pubkey hash + // - OP_EQUALVERIFY + // - OP_CHECKSIG + P2PKHPkScriptSize = 1 + 1 + 1 + 20 + 1 + 1 + + // RedeemP2PKHInputSize is the worst case (largest) serialize size of a + // transaction input redeeming a compressed P2PKH output. It is + // calculated as: + // + // - 32 bytes previous tx + // - 4 bytes output index + // - 1 byte script len + // - signature script + // - 4 bytes sequence + RedeemP2PKHInputSize = 32 + 4 + 1 + RedeemP2PKHSigScriptSize + 4 + + // RedeemP2SH2of3MultisigInputSize is the worst case (largest) serialize size of a + // transaction input redeeming a compressed P2SH 2 of 3 multisig output. It is + // calculated as: + // + // - 32 bytes previous tx + // - 4 bytes output index + // - 1 byte script len + // - 4 bytes sequence + /// - witness discounted signature script + RedeemP2SH2of3MultisigInputSize = 32 + 4 + 1 + 4 + (RedeemP2SH2of3MultisigSigScriptSize / 4) + + // RedeemP2SH1of2MultisigInputSize is the worst case (largest) serialize size of a + // transaction input redeeming a compressed P2SH 2 of 3 multisig output. It is + // calculated as: + // + // - 32 bytes previous tx + // - 4 bytes output index + // - 1 byte script len + // - 4 bytes sequence + /// - witness discounted signature script + RedeemP2SH1of2MultisigInputSize = 32 + 4 + 1 + 4 + (RedeemP2SH1of2MultisigSigScriptSize / 4) + + // RedeemP2SHMultisigTimelock1InputSize is the worst case (largest) serialize size of a + // transaction input redeeming a compressed p2sh timelocked multig output with using the timeout. It is + // calculated as: + // + // - 32 bytes previous tx + // - 4 bytes output index + // - 1 byte script len + // - 4 bytes sequence + /// - witness discounted signature script + RedeemP2SHMultisigTimelock1InputSize = 32 + 4 + 1 + 4 + (RedeemP2SHMultisigTimelock1SigScriptSize / 4) + + // RedeemP2SHMultisigTimelock2InputSize is the worst case (largest) serialize size of a + // transaction input redeeming a compressed P2SH timelocked multisig output without using the timeout. It is + // calculated as: + // + // - 32 bytes previous tx + // - 4 bytes output index + // - 1 byte script len + // - 4 bytes sequence + /// - witness discounted signature script + RedeemP2SHMultisigTimelock2InputSize = 32 + 4 + 1 + 4 + (RedeemP2SHMultisigTimelock2SigScriptSize / 4) + + // P2PKHOutputSize is the serialize size of a transaction output with a + // P2PKH output script. It is calculated as: + // + // - 8 bytes output value + // - 1 byte compact int encoding value 25 + // - 25 bytes P2PKH output script + P2PKHOutputSize = 8 + 1 + P2PKHPkScriptSize +) + +type InputType int + +const ( + P2PKH InputType = iota + P2SH_1of2_Multisig + P2SH_2of3_Multisig + P2SH_Multisig_Timelock_1Sig + P2SH_Multisig_Timelock_2Sigs +) + +// EstimateSerializeSize returns a worst case serialize size estimate for a +// signed transaction that spends inputCount number of compressed P2PKH outputs +// and contains each transaction output from txOuts. The estimated size is +// incremented for an additional P2PKH change output if addChangeOutput is true. +func EstimateSerializeSize(inputCount int, txOuts []*wire.TxOut, addChangeOutput bool, inputType InputType) int { + changeSize := 0 + outputCount := len(txOuts) + if addChangeOutput { + changeSize = P2PKHOutputSize + outputCount++ + } + + var redeemScriptSize int + switch inputType { + case P2PKH: + redeemScriptSize = RedeemP2PKHInputSize + case P2SH_1of2_Multisig: + redeemScriptSize = RedeemP2SH1of2MultisigInputSize + case P2SH_2of3_Multisig: + redeemScriptSize = RedeemP2SH2of3MultisigInputSize + case P2SH_Multisig_Timelock_1Sig: + redeemScriptSize = RedeemP2SHMultisigTimelock1InputSize + case P2SH_Multisig_Timelock_2Sigs: + redeemScriptSize = RedeemP2SHMultisigTimelock2InputSize + } + + // 10 additional bytes are for version, locktime, and segwit flags + return 10 + wire.VarIntSerializeSize(uint64(inputCount)) + + wire.VarIntSerializeSize(uint64(outputCount)) + + inputCount*redeemScriptSize + + SumOutputSerializeSizes(txOuts) + + changeSize +} + +// SumOutputSerializeSizes sums up the serialized size of the supplied outputs. +func SumOutputSerializeSizes(outputs []*wire.TxOut) (serializeSize int) { + for _, txOut := range outputs { + serializeSize += txOut.SerializeSize() + } + return serializeSize +} diff --git a/gleecbtc/wallet.go b/gleecbtc/wallet.go new file mode 100644 index 0000000..c7ef362 --- /dev/null +++ b/gleecbtc/wallet.go @@ -0,0 +1,516 @@ +package gleecbtc + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "strings" + "time" + + "github.com/OpenBazaar/multiwallet/cache" + "github.com/OpenBazaar/multiwallet/client" + "github.com/OpenBazaar/multiwallet/config" + "github.com/OpenBazaar/multiwallet/keys" + laddr "github.com/OpenBazaar/multiwallet/gleecbtc/address" + "github.com/OpenBazaar/multiwallet/model" + "github.com/OpenBazaar/multiwallet/service" + "github.com/OpenBazaar/multiwallet/util" + wi "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + hd "github.com/btcsuite/btcutil/hdkeychain" + "github.com/ltcsuite/ltcutil" + "github.com/ltcsuite/ltcwallet/wallet/txrules" + logging "github.com/op/go-logging" + "github.com/tyler-smith/go-bip39" + "golang.org/x/net/proxy" +) + +type GleecbtcWallet struct { + db wi.Datastore + km *keys.KeyManager + params *chaincfg.Params + client model.APIClient + ws *service.WalletService + fp *util.FeeProvider + + mPrivKey *hd.ExtendedKey + mPubKey *hd.ExtendedKey + + exchangeRates wi.ExchangeRates + log *logging.Logger +} + +var _ = wi.Wallet(&GleecbtcWallet{}) + +func NewGleecbtcWallet(cfg config.CoinConfig, mnemonic string, params *chaincfg.Params, proxy proxy.Dialer, cache cache.Cacher, disableExchangeRates bool) (*GleecbtcWallet, error) { + seed := bip39.NewSeed(mnemonic, "") + + mPrivKey, err := hd.NewMaster(seed, params) + if err != nil { + return nil, err + } + mPubKey, err := mPrivKey.Neuter() + if err != nil { + return nil, err + } + km, err := keys.NewKeyManager(cfg.DB.Keys(), params, mPrivKey, wi.Gleecbtc, gleecbtcAddress) + if err != nil { + return nil, err + } + + c, err := client.NewClientPool(cfg.ClientAPIs, proxy) + if err != nil { + return nil, err + } + + wm, err := service.NewWalletService(cfg.DB, km, c, params, wi.Gleecbtc, cache) + if err != nil { + return nil, err + } + var er wi.ExchangeRates + if !disableExchangeRates { + er = NewGleecbtcPriceFetcher(proxy) + } + + fp := util.NewFeeProvider(cfg.MaxFee, cfg.HighFee, cfg.MediumFee, cfg.LowFee, er) + + return &GleecbtcWallet{ + db: cfg.DB, + km: km, + params: params, + client: c, + ws: wm, + fp: fp, + mPrivKey: mPrivKey, + mPubKey: mPubKey, + exchangeRates: er, + log: logging.MustGetLogger("gleecbtc-wallet"), + }, nil +} + +func gleecbtcAddress(key *hd.ExtendedKey, params *chaincfg.Params) (btcutil.Address, error) { + addr, err := key.Address(params) + if err != nil { + return nil, err + } + return laddr.NewAddressPubKeyHash(addr.ScriptAddress(), params) +} +func (w *GleecbtcWallet) Start() { + w.client.Start() + w.ws.Start() +} + +func (w *GleecbtcWallet) Params() *chaincfg.Params { + return w.params +} + +func (w *GleecbtcWallet) CurrencyCode() string { + if w.params.Name == chaincfg.MainNetParams.Name { + return "gleec" + } else { + return "tgleec" + } +} + +func (w *GleecbtcWallet) IsDust(amount int64) bool { + return txrules.IsDustAmount(ltcutil.Amount(amount), 25, txrules.DefaultRelayFeePerKb) +} + +func (w *GleecbtcWallet) MasterPrivateKey() *hd.ExtendedKey { + return w.mPrivKey +} + +func (w *GleecbtcWallet) MasterPublicKey() *hd.ExtendedKey { + return w.mPubKey +} + +func (w *GleecbtcWallet) ChildKey(keyBytes []byte, chaincode []byte, isPrivateKey bool) (*hd.ExtendedKey, error) { + parentFP := []byte{0x00, 0x00, 0x00, 0x00} + var id []byte + if isPrivateKey { + id = w.params.HDPrivateKeyID[:] + } else { + id = w.params.HDPublicKeyID[:] + } + hdKey := hd.NewExtendedKey( + id, + keyBytes, + chaincode, + parentFP, + 0, + 0, + isPrivateKey) + return hdKey.Child(0) +} + +func (w *GleecbtcWallet) CurrentAddress(purpose wi.KeyPurpose) btcutil.Address { + var addr btcutil.Address + for { + key, err := w.km.GetCurrentKey(purpose) + if err != nil { + w.log.Errorf("Error generating current key: %s", err) + } + addr, err = w.km.KeyToAddress(key) + if err != nil { + w.log.Errorf("Error converting key to address: %s", err) + } + + if !strings.HasPrefix(strings.ToLower(addr.String()), "ltc1") { + break + } + if err := w.db.Keys().MarkKeyAsUsed(addr.ScriptAddress()); err != nil { + w.log.Errorf("Error marking key as used: %s", err) + } + } + return addr +} + +func (w *GleecbtcWallet) NewAddress(purpose wi.KeyPurpose) btcutil.Address { + var addr btcutil.Address + for { + key, err := w.km.GetNextUnused(purpose) + if err != nil { + w.log.Errorf("Error generating next unused key: %s", err) + } + addr, err = w.km.KeyToAddress(key) + if err != nil { + w.log.Errorf("Error converting key to address: %s", err) + } + if err := w.db.Keys().MarkKeyAsUsed(addr.ScriptAddress()); err != nil { + w.log.Errorf("Error marking key as used: %s", err) + } + if !strings.HasPrefix(strings.ToLower(addr.String()), "ltc1") { + break + } + } + return addr +} + +func (w *GleecbtcWallet) DecodeAddress(addr string) (btcutil.Address, error) { + return laddr.DecodeAddress(addr, w.params) +} + +func (w *GleecbtcWallet) ScriptToAddress(script []byte) (btcutil.Address, error) { + return laddr.ExtractPkScriptAddrs(script, w.params) +} + +func (w *GleecbtcWallet) AddressToScript(addr btcutil.Address) ([]byte, error) { + return laddr.PayToAddrScript(addr) +} + +func (w *GleecbtcWallet) HasKey(addr btcutil.Address) bool { + _, err := w.km.GetKeyForScript(addr.ScriptAddress()) + return err == nil +} + +func (w *GleecbtcWallet) Balance() (confirmed, unconfirmed int64) { + utxos, _ := w.db.Utxos().GetAll() + txns, _ := w.db.Txns().GetAll(false) + return util.CalcBalance(utxos, txns) +} + +func (w *GleecbtcWallet) Transactions() ([]wi.Txn, error) { + height, _ := w.ChainTip() + txns, err := w.db.Txns().GetAll(false) + if err != nil { + return txns, err + } + for i, tx := range txns { + var confirmations int32 + var status wi.StatusCode + confs := int32(height) - tx.Height + 1 + if tx.Height <= 0 { + confs = tx.Height + } + switch { + case confs < 0: + status = wi.StatusDead + case confs == 0 && time.Since(tx.Timestamp) <= time.Hour*6: + status = wi.StatusUnconfirmed + case confs == 0 && time.Since(tx.Timestamp) > time.Hour*6: + status = wi.StatusDead + case confs > 0 && confs < 24: + status = wi.StatusPending + confirmations = confs + case confs > 23: + status = wi.StatusConfirmed + confirmations = confs + } + tx.Confirmations = int64(confirmations) + tx.Status = status + txns[i] = tx + } + return txns, nil +} + +func (w *GleecbtcWallet) GetTransaction(txid chainhash.Hash) (wi.Txn, error) { + txn, err := w.db.Txns().Get(txid) + if err == nil { + tx := wire.NewMsgTx(1) + rbuf := bytes.NewReader(txn.Bytes) + err := tx.BtcDecode(rbuf, wire.ProtocolVersion, wire.WitnessEncoding) + if err != nil { + return txn, err + } + outs := []wi.TransactionOutput{} + for i, out := range tx.TxOut { + addr, err := laddr.ExtractPkScriptAddrs(out.PkScript, w.params) + if err != nil { + w.log.Errorf("error extracting address from txn pkscript: %v\n", err) + } + tout := wi.TransactionOutput{ + Address: addr, + Value: out.Value, + Index: uint32(i), + } + outs = append(outs, tout) + } + txn.Outputs = outs + } + return txn, err +} + +func (w *GleecbtcWallet) ChainTip() (uint32, chainhash.Hash) { + return w.ws.ChainTip() +} + +func (w *GleecbtcWallet) GetFeePerByte(feeLevel wi.FeeLevel) uint64 { + return w.fp.GetFeePerByte(feeLevel) +} + +func (w *GleecbtcWallet) Spend(amount int64, addr btcutil.Address, feeLevel wi.FeeLevel, referenceID string, spendAll bool) (*chainhash.Hash, error) { + var ( + tx *wire.MsgTx + err error + ) + if spendAll { + tx, err = w.buildSpendAllTx(addr, feeLevel) + if err != nil { + return nil, err + } + } else { + tx, err = w.buildTx(amount, addr, feeLevel, nil) + if err != nil { + return nil, err + } + } + + // Broadcast + if err := w.Broadcast(tx); err != nil { + return nil, err + } + + ch := tx.TxHash() + return &ch, nil +} + +func (w *GleecbtcWallet) BumpFee(txid chainhash.Hash) (*chainhash.Hash, error) { + return w.bumpFee(txid) +} + +func (w *GleecbtcWallet) EstimateFee(ins []wi.TransactionInput, outs []wi.TransactionOutput, feePerByte uint64) uint64 { + tx := new(wire.MsgTx) + for _, out := range outs { + scriptPubKey, _ := laddr.PayToAddrScript(out.Address) + output := wire.NewTxOut(out.Value, scriptPubKey) + tx.TxOut = append(tx.TxOut, output) + } + estimatedSize := EstimateSerializeSize(len(ins), tx.TxOut, false, P2PKH) + fee := estimatedSize * int(feePerByte) + return uint64(fee) +} + +func (w *GleecbtcWallet) EstimateSpendFee(amount int64, feeLevel wi.FeeLevel) (uint64, error) { + return w.estimateSpendFee(amount, feeLevel) +} + +func (w *GleecbtcWallet) SweepAddress(ins []wi.TransactionInput, address *btcutil.Address, key *hd.ExtendedKey, redeemScript *[]byte, feeLevel wi.FeeLevel) (*chainhash.Hash, error) { + return w.sweepAddress(ins, address, key, redeemScript, feeLevel) +} + +func (w *GleecbtcWallet) CreateMultisigSignature(ins []wi.TransactionInput, outs []wi.TransactionOutput, key *hd.ExtendedKey, redeemScript []byte, feePerByte uint64) ([]wi.Signature, error) { + return w.createMultisigSignature(ins, outs, key, redeemScript, feePerByte) +} + +func (w *GleecbtcWallet) Multisign(ins []wi.TransactionInput, outs []wi.TransactionOutput, sigs1 []wi.Signature, sigs2 []wi.Signature, redeemScript []byte, feePerByte uint64, broadcast bool) ([]byte, error) { + return w.multisign(ins, outs, sigs1, sigs2, redeemScript, feePerByte, broadcast) +} + +func (w *GleecbtcWallet) GenerateMultisigScript(keys []hd.ExtendedKey, threshold int, timeout time.Duration, timeoutKey *hd.ExtendedKey) (addr btcutil.Address, redeemScript []byte, err error) { + return w.generateMultisigScript(keys, threshold, timeout, timeoutKey) +} + +func (w *GleecbtcWallet) AddWatchedAddresses(addrs ...btcutil.Address) error { + + var watchedScripts [][]byte + for _, addr := range addrs { + if !w.HasKey(addr) { + script, err := w.AddressToScript(addr) + if err != nil { + return err + } + watchedScripts = append(watchedScripts, script) + } + } + + err := w.db.WatchedScripts().PutAll(watchedScripts) + if err != nil { + return err + } + + w.client.ListenAddresses(addrs...) + return nil +} + +func (w *GleecbtcWallet) AddWatchedScript(script []byte) error { + err := w.db.WatchedScripts().Put(script) + if err != nil { + return err + } + addr, err := w.ScriptToAddress(script) + if err != nil { + return err + } + w.client.ListenAddresses(addr) + return nil +} + +func (w *GleecbtcWallet) AddTransactionListener(callback func(wi.TransactionCallback)) { + w.ws.AddTransactionListener(callback) +} + +func (w *GleecbtcWallet) ReSyncBlockchain(fromTime time.Time) { + go w.ws.UpdateState() +} + +func (w *GleecbtcWallet) GetConfirmations(txid chainhash.Hash) (uint32, uint32, error) { + txn, err := w.db.Txns().Get(txid) + if err != nil { + return 0, 0, err + } + if txn.Height == 0 { + return 0, 0, nil + } + chainTip, _ := w.ChainTip() + return chainTip - uint32(txn.Height) + 1, uint32(txn.Height), nil +} + +func (w *GleecbtcWallet) Close() { + w.ws.Stop() + w.client.Close() +} + +func (w *GleecbtcWallet) ExchangeRates() wi.ExchangeRates { + return w.exchangeRates +} + +func (w *GleecbtcWallet) DumpTables(wr io.Writer) { + fmt.Fprintln(wr, "Transactions-----") + txns, _ := w.db.Txns().GetAll(true) + for _, tx := range txns { + fmt.Fprintf(wr, "Hash: %s, Height: %d, Value: %d, WatchOnly: %t\n", tx.Txid, int(tx.Height), int(tx.Value), tx.WatchOnly) + } + fmt.Fprintln(wr, "\nUtxos-----") + utxos, _ := w.db.Utxos().GetAll() + for _, u := range utxos { + fmt.Fprintf(wr, "Hash: %s, Index: %d, Height: %d, Value: %d, WatchOnly: %t\n", u.Op.Hash.String(), int(u.Op.Index), int(u.AtHeight), int(u.Value), u.WatchOnly) + } + fmt.Fprintln(wr, "\nKeys-----") + keys, _ := w.db.Keys().GetAll() + unusedInternal, _ := w.db.Keys().GetUnused(wi.INTERNAL) + unusedExternal, _ := w.db.Keys().GetUnused(wi.EXTERNAL) + internalMap := make(map[int]bool) + externalMap := make(map[int]bool) + for _, k := range unusedInternal { + internalMap[k] = true + } + for _, k := range unusedExternal { + externalMap[k] = true + } + + for _, k := range keys { + var used bool + if k.Purpose == wi.INTERNAL { + used = internalMap[k.Index] + } else { + used = externalMap[k.Index] + } + fmt.Fprintf(wr, "KeyIndex: %d, Purpose: %d, Used: %t\n", k.Index, k.Purpose, used) + } +} + +// Build a client.Transaction so we can ingest it into the wallet service then broadcast +func (w *GleecbtcWallet) Broadcast(tx *wire.MsgTx) error { + var buf bytes.Buffer + tx.BtcEncode(&buf, wire.ProtocolVersion, wire.WitnessEncoding) + cTxn := model.Transaction{ + Txid: tx.TxHash().String(), + Locktime: int(tx.LockTime), + Version: int(tx.Version), + Confirmations: 0, + Time: time.Now().Unix(), + RawBytes: buf.Bytes(), + } + utxos, err := w.db.Utxos().GetAll() + if err != nil { + return err + } + for n, in := range tx.TxIn { + var u wi.Utxo + for _, ut := range utxos { + if util.OutPointsEqual(ut.Op, in.PreviousOutPoint) { + u = ut + break + } + } + addr, err := w.ScriptToAddress(u.ScriptPubkey) + if err != nil { + return err + } + input := model.Input{ + Txid: in.PreviousOutPoint.Hash.String(), + Vout: int(in.PreviousOutPoint.Index), + ScriptSig: model.Script{ + Hex: hex.EncodeToString(in.SignatureScript), + }, + Sequence: uint32(in.Sequence), + N: n, + Addr: addr.String(), + Satoshis: u.Value, + Value: float64(u.Value) / util.SatoshisPerCoin(wi.Gleecbtc), + } + cTxn.Inputs = append(cTxn.Inputs, input) + } + for n, out := range tx.TxOut { + addr, err := w.ScriptToAddress(out.PkScript) + if err != nil { + return err + } + output := model.Output{ + N: n, + ScriptPubKey: model.OutScript{ + Script: model.Script{ + Hex: hex.EncodeToString(out.PkScript), + }, + Addresses: []string{addr.String()}, + }, + Value: float64(float64(out.Value) / util.SatoshisPerCoin(wi.Bitcoin)), + } + cTxn.Outputs = append(cTxn.Outputs, output) + } + _, err = w.client.Broadcast(buf.Bytes()) + if err != nil { + return err + } + w.ws.ProcessIncomingTransaction(cTxn) + return nil +} + +// AssociateTransactionWithOrder used for ORDER_PAYMENT message +func (w *GleecbtcWallet) AssociateTransactionWithOrder(cb wi.TransactionCallback) { + w.ws.InvokeTransactionListeners(cb) +} diff --git a/multiwallet.go b/multiwallet.go index e9e10ce..ea7b886 100644 --- a/multiwallet.go +++ b/multiwallet.go @@ -10,6 +10,7 @@ import ( "github.com/OpenBazaar/multiwallet/client/blockbook" "github.com/OpenBazaar/multiwallet/config" "github.com/OpenBazaar/multiwallet/litecoin" + "github.com/developertask/multiwallet/gleecbtc" "github.com/OpenBazaar/multiwallet/service" "github.com/OpenBazaar/multiwallet/zcash" "github.com/OpenBazaar/wallet-interface" @@ -94,6 +95,23 @@ func NewMultiWallet(cfg *config.Config) (MultiWallet, error) { //} //multiwallet[coin.CoinType] = w } + case wallet.Gleecbtc: + w, err = gleecbtc.NewLitecoinWallet(coin, cfg.Mnemonic, cfg.Params, cfg.Proxy, cfg.Cache, cfg.DisableExchangeRates) + if err != nil { + return nil, err + } + if cfg.Params.Name == chaincfg.MainNetParams.Name { + multiwallet[wallet.Gleecbtc] = w + } else { + multiwallet[wallet.TestnetLitecoin] = w + } + //case wallet.Ethereum: + //w, err = eth.NewEthereumWallet(coin, cfg.Mnemonic, cfg.Proxy) + //if err != nil { + //return nil, err + //} + //multiwallet[coin.CoinType] = w + } } return multiwallet, nil }