diff --git a/.gitignore b/.gitignore index 66fd13c..bd3957d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,38 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a *.so -*.dylib -# Test binary, built with `go test -c` +# Folders +_obj +_test +__pycache__ + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe *.test +*.prof +.idea/ +*.iml +.gx/ +dist -# Output of the go coverage tool, specifically when used with LiteIDE -*.out +# Development environment files +.ackrc +.tags* +*.sw? -# Dependency directories (remove the comment below to include it) -# vendor/ +# macOS +.DS_Store +vendor/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..e386637 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,52 @@ +run: + skip-dirs: + - vendor + +issues: + max-per-linter: 99999 + max-same: 99999 + exclude: + - composite literal uses unkeyed fields + +linters-settings: + errcheck: + check-type-assertions: false + check-blank: false + unparam: + algo: cha + check-exported: false + nakedret: + max-func-lines: 0 + misspell: + locale: US + dupl: + threshold: 600 + gocyclo: + min-complexity: 180 + lll: + line-length: 750 + +linters: + enable: + - unconvert + - gofmt + - ineffassign + - staticcheck + - structcheck + - unused + - varcheck + - deadcode + - gosimple + - gocyclo + - lll + - goconst + - govet + - megacheck + disable: + - dupl + - nakedret + - unparam + - goimports + - errcheck + - golint + - prealloc diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cf81ad5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: go +go: + - "1.11" +sudo: required +services: + - docker +env: + - "PATH=/home/travis/gopath/bin:$PATH" +before_install: + - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh + - go get github.com/tcnksm/ghr + - go get github.com/axw/gocov/gocov + - go get github.com/mattn/goveralls + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.15.0 +install: + - dep ensure +script: + - $GOPATH/bin/golangci-lint run --deadline 10m --new + - cd $TRAVIS_BUILD_DIR && chmod a+x test_compile.sh && ./test_compile.sh + - goveralls -coverprofile=coverage.out -service travis-ci diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..b160c04 --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,460 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + branch = "master" + digest = "1:139894af9fdf6ed3836003686ddc34d0e2cc018f34d2beb26107841ea410b2b5" + name = "github.com/OpenBazaar/golang-socketio" + packages = [ + ".", + "protocol", + "transport", + ] + pruneopts = "UT" + revision = "909b73d947ae79609bc2c5b03cb480f35180c564" + +[[projects]] + branch = "master" + digest = "1:3861bd7ece0e78ea465eb4c2ee12391a2759755220c89729a7033b4af98b42e3" + name = "github.com/OpenBazaar/spvwallet" + packages = [ + ".", + "exchangerates", + ] + pruneopts = "UT" + revision = "a32d41681bf3a75473498711710d0123f1625ec8" + +[[projects]] + branch = "master" + digest = "1:41045b2cbf58bf6007d65103b1fb17cbc092777af415546eb40fe4e005da3780" + name = "github.com/OpenBazaar/wallet-interface" + packages = ["."] + pruneopts = "UT" + revision = "ba95db86ca2f75d561554d3270623db4d5ed0467" + +[[projects]] + digest = "1:0f98f59e9a2f4070d66f0c9c39561f68fcd1dc837b22a852d28d0003aebd1b1e" + name = "github.com/boltdb/bolt" + packages = ["."] + pruneopts = "UT" + revision = "2f1ce7a837dcb8da3ec595b1dac9d0632f0f99e8" + version = "v1.3.1" + +[[projects]] + branch = "master" + digest = "1:4f1545a9a908c3f3d7c5d6c93baa76c3bb2f6369eb08a58f06d91487946625cc" + name = "github.com/btcsuite/btcd" + packages = [ + "addrmgr", + "blockchain", + "btcec", + "chaincfg", + "chaincfg/chainhash", + "connmgr", + "database", + "peer", + "txscript", + "wire", + ] + pruneopts = "UT" + revision = "5bda5314ca9549a589e63d7b2e6104492a0d5328" + +[[projects]] + branch = "master" + digest = "1:30d4a548e09bca4a0c77317c58e7407e2a65c15325e944f9c08a7b7992f8a59e" + name = "github.com/btcsuite/btclog" + packages = ["."] + pruneopts = "UT" + revision = "84c8d2346e9fc8c7b947e243b9c24e6df9fd206a" + +[[projects]] + branch = "master" + digest = "1:43190b35675926c93f392feed190b2056efd1f4f1b247b559f2a63cc164fa104" + name = "github.com/btcsuite/btcutil" + packages = [ + ".", + "base58", + "bech32", + "bloom", + "coinset", + "hdkeychain", + "txsort", + ] + pruneopts = "UT" + revision = "ab6388e0c60ae4834a1f57511e20c17b5f78be4b" + +[[projects]] + branch = "master" + digest = "1:69e5357e344f35f1d3da320bf85271b5d5d758a826a52f132a3da29d6112fcc2" + name = "github.com/btcsuite/btcwallet" + packages = [ + "internal/helpers", + "wallet/internal/txsizes", + "wallet/txauthor", + "wallet/txrules", + ] + pruneopts = "UT" + revision = "177e31c0b3273c5f7422102cc65be55cbcfde957" + +[[projects]] + branch = "master" + digest = "1:1e6b2f7aa98b082c30a1303c29a702c369b2ec6d86b74a599bc8bbe2333db299" + name = "github.com/btcsuite/go-socks" + packages = ["socks"] + pruneopts = "UT" + revision = "4720035b7bfd2a9bb130b1c184f8bbe41b6f0d0f" + +[[projects]] + branch = "master" + digest = "1:49ad1acb33bb5b40c0d197321d3cf9ee9a29eb02f4765ab7c316e08983eb7559" + name = "github.com/btcsuite/golangcrypto" + packages = ["ripemd160"] + pruneopts = "UT" + revision = "53f62d9b43e87a6c56975cf862af7edf33a8d0df" + +[[projects]] + digest = "1:91fc3f4d1842584d1342364193106e80d1d532bbd1668fbd7c61627f01d0111f" + name = "github.com/btcsuite/goleveldb" + packages = [ + "leveldb/errors", + "leveldb/storage", + "leveldb/util", + ] + pruneopts = "UT" + revision = "3fd0373267b6461dbefe91cef614278064d05465" + version = "v1.0.0" + +[[projects]] + branch = "master" + digest = "1:78097abc20f73ec968b3f67bf74deda55009caa4b801d0d04f36145c869ab3c3" + name = "github.com/cevaris/ordered_map" + packages = ["."] + pruneopts = "UT" + revision = "0efaee1733e3399a3cb88fc7d2ce340bf2e863d7" + +[[projects]] + branch = "master" + digest = "1:36236e7063db3314f32b60885ef7ddf8abab65cb055f3a21d118f7e19148cfa3" + name = "github.com/cpacia/bchutil" + packages = ["."] + pruneopts = "UT" + revision = "b126f6a35b6c2968c0877cb4d2ac5dcf67682d27" + +[[projects]] + digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" + name = "github.com/davecgh/go-spew" + packages = ["spew"] + pruneopts = "UT" + revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" + version = "v1.1.1" + +[[projects]] + digest = "1:1e9a0ec4f7e852123fefad9aadd7647eed1e9fd3716118e99a4b3dc463705c82" + name = "github.com/dchest/siphash" + packages = ["."] + pruneopts = "UT" + revision = "34f201214d993633bb24f418ba11736ab8b55aa7" + version = "v1.2.1" + +[[projects]] + digest = "1:700f82416846a964010b86fddeada0e1ceb1c96fa65acc4f234811a7d3e4fded" + name = "github.com/gcash/bchd" + packages = [ + "bchec", + "chaincfg", + "chaincfg/chainhash", + "txscript", + "wire", + ] + pruneopts = "UT" + revision = "34d8b67e58c8487e08cc2b8130398ec4f4bc6df5" + version = "v0.14.6" + +[[projects]] + branch = "master" + digest = "1:b1053b781e9090dab5d3e916eb04c8d85b63a7f6911007c2cd1dd82fb22f7f6a" + name = "github.com/gcash/bchlog" + packages = ["."] + pruneopts = "UT" + revision = "b4f036f92fa66c88eec458f4531ff14ff87704d6" + +[[projects]] + branch = "master" + digest = "1:2538b5efd7d3d7ac9efdfef955f5bcda82cbec4d6f8aef4d388f1c70e6318fc5" + name = "github.com/gcash/bchutil" + packages = [ + ".", + "base58", + ] + pruneopts = "UT" + revision = "800e62fe9aff291db8909a5dbf35c23cff8d1a62" + +[[projects]] + branch = "master" + digest = "1:a54f931f516df9f3b2401e3cfa47482be79397d20fcbe838b7da6c63d5b8e615" + name = "github.com/golang/protobuf" + packages = [ + "proto", + "protoc-gen-go/descriptor", + "ptypes", + "ptypes/any", + "ptypes/duration", + "ptypes/timestamp", + ] + pruneopts = "UT" + revision = "347cf4a86c1cb8d262994d8ef5924d4576c5b331" + +[[projects]] + branch = "master" + digest = "1:521455a43b348afa7e4deb79c471f3ed8ff04a71d7d9b88ab0d149b9eb905fe2" + name = "github.com/gorilla/websocket" + packages = ["."] + pruneopts = "UT" + revision = "95ba29eb981bbb27d92e1f70bf8a1949452d926b" + +[[projects]] + branch = "master" + digest = "1:459271b8268fe541549b299f65160b1df5abe9ffef0426cc38607f771dbc6bb4" + name = "github.com/jessevdk/go-flags" + packages = ["."] + pruneopts = "UT" + revision = "c0795c8afcf41dd1d786bebce68636c199b3bb45" + +[[projects]] + branch = "master" + digest = "1:1d509f3d4933044d1416d2537d98a7caf11c4ddbbff2c466d5ebe8a7cea80b54" + name = "github.com/ltcsuite/ltcd" + packages = [ + "btcec", + "chaincfg", + "chaincfg/chainhash", + "txscript", + "wire", + ] + pruneopts = "UT" + revision = "f37f8bf35796325487af28e0e01c2528dd97ea95" + +[[projects]] + branch = "master" + digest = "1:61c97cb69387bafe67d376d09eb4059096f45343f5ba87a70ed217bfabb81a56" + name = "github.com/ltcsuite/ltcutil" + packages = [ + ".", + "base58", + "bech32", + ] + pruneopts = "UT" + revision = "17f3b04680b6521349067fb99d9f7495a9e96d60" + +[[projects]] + branch = "master" + digest = "1:a302d142a103687a0dc12e2c1fffc4128011b6ed27dbc969c549799b23f57b8d" + name = "github.com/ltcsuite/ltcwallet" + packages = ["wallet/txrules"] + pruneopts = "UT" + revision = "3fa612e326e5f0f1cfa460711fd563d79bc2ef49" + +[[projects]] + branch = "master" + digest = "1:130cefe87d7eeefc824978dcb78e35672d4c49a11f25c153fbf0cfd952756fa3" + name = "github.com/minio/blake2b-simd" + packages = ["."] + pruneopts = "UT" + revision = "3f5f724cb5b182a5c278d6d3d55b40e7f8c2efb4" + +[[projects]] + digest = "1:78bbb1ba5b7c3f2ed0ea1eab57bdd3859aec7e177811563edc41198a760b06af" + name = "github.com/mitchellh/go-homedir" + packages = ["."] + pruneopts = "UT" + revision = "ae18d6b8b3205b561c79e8e5f69bff09736185f4" + version = "v1.0.0" + +[[projects]] + branch = "master" + digest = "1:41f3b549dbb8216e490c27926c09dc055aa509b3476e7cbc3317103990b13aeb" + name = "github.com/op/go-logging" + packages = ["."] + pruneopts = "UT" + revision = "970db520ece77730c7e4724c61121037378659d9" + +[[projects]] + branch = "master" + digest = "1:6bae001f7c4cddc9ebf2b47bd5ff1bd3978a84b422c468d6037dad284c8c8e9d" + name = "github.com/tyler-smith/go-bip39" + packages = [ + ".", + "wordlists", + ] + pruneopts = "UT" + revision = "dbb3b84ba2ef14e894f5e33d6c6e43641e665738" + +[[projects]] + branch = "master" + digest = "1:734c27157144367f37acd13536569700f61f01576c4a5e9665ccb76f79966d97" + name = "golang.org/x/crypto" + packages = [ + "pbkdf2", + "ripemd160", + "scrypt", + ] + pruneopts = "UT" + revision = "ff983b9c42bc9fbf91556e191cc8efb585c16908" + +[[projects]] + branch = "master" + digest = "1:f38eb85ef192e7499628544cc186b5e5c42ca7e967f87e18730bcdbbf424f191" + name = "golang.org/x/net" + packages = [ + "context", + "http/httpguts", + "http2", + "http2/hpack", + "idna", + "internal/socks", + "internal/timeseries", + "proxy", + "trace", + ] + pruneopts = "UT" + revision = "1e06a53dbb7e2ed46e91183f219db23c6943c532" + +[[projects]] + branch = "master" + digest = "1:91137b48dc3eb34409f731b49f63a5ebf73218168a065e1a93af24eb5b2f99e8" + name = "golang.org/x/sys" + packages = ["unix"] + pruneopts = "UT" + revision = "48ac38b7c8cbedd50b1613c0fccacfc7d88dfcdf" + +[[projects]] + digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" + name = "golang.org/x/text" + packages = [ + "collate", + "collate/build", + "internal/colltab", + "internal/gen", + "internal/tag", + "internal/triegen", + "internal/ucd", + "language", + "secure/bidirule", + "transform", + "unicode/bidi", + "unicode/cldr", + "unicode/norm", + "unicode/rangetable", + ] + pruneopts = "UT" + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" + +[[projects]] + branch = "master" + digest = "1:077c1c599507b3b3e9156d17d36e1e61928ee9b53a5b420f10f28ebd4a0b275c" + name = "google.golang.org/genproto" + packages = ["googleapis/rpc/status"] + pruneopts = "UT" + revision = "ae2f86662275e140f395167f1dab7081a5bd5fa8" + +[[projects]] + branch = "master" + digest = "1:574a2035852ef91df91535fe0eba7d0f7ab01d49bdf6f0398f70153093c047a2" + name = "google.golang.org/grpc" + packages = [ + ".", + "balancer", + "balancer/base", + "balancer/roundrobin", + "binarylog/grpc_binarylog_v1", + "codes", + "connectivity", + "credentials", + "credentials/internal", + "encoding", + "encoding/proto", + "grpclog", + "internal", + "internal/backoff", + "internal/binarylog", + "internal/channelz", + "internal/envconfig", + "internal/grpcrand", + "internal/grpcsync", + "internal/syscall", + "internal/transport", + "keepalive", + "metadata", + "naming", + "peer", + "reflection", + "reflection/grpc_reflection_v1alpha", + "resolver", + "resolver/dns", + "resolver/passthrough", + "stats", + "status", + "tap", + ] + pruneopts = "UT" + revision = "b6f0a0f3fc650f4977c0b6869f5e4e717c1c0f24" + +[[projects]] + branch = "v1" + digest = "1:6c802dcce0c717e7fc42d788ee1e2c1393c64b60339d908c0585c32dc5dec994" + name = "gopkg.in/jarcoal/httpmock.v1" + packages = ["."] + pruneopts = "UT" + revision = "275e9df93516fc27a998eca24c8f1cca277ce5bf" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + input-imports = [ + "github.com/OpenBazaar/golang-socketio", + "github.com/OpenBazaar/golang-socketio/protocol", + "github.com/OpenBazaar/golang-socketio/transport", + "github.com/OpenBazaar/spvwallet", + "github.com/OpenBazaar/spvwallet/exchangerates", + "github.com/OpenBazaar/wallet-interface", + "github.com/btcsuite/btcd/blockchain", + "github.com/btcsuite/btcd/btcec", + "github.com/btcsuite/btcd/chaincfg", + "github.com/btcsuite/btcd/chaincfg/chainhash", + "github.com/btcsuite/btcd/txscript", + "github.com/btcsuite/btcd/wire", + "github.com/btcsuite/btcutil", + "github.com/btcsuite/btcutil/base58", + "github.com/btcsuite/btcutil/bech32", + "github.com/btcsuite/btcutil/coinset", + "github.com/btcsuite/btcutil/hdkeychain", + "github.com/btcsuite/btcutil/txsort", + "github.com/btcsuite/btcwallet/wallet/txauthor", + "github.com/btcsuite/btcwallet/wallet/txrules", + "github.com/btcsuite/golangcrypto/ripemd160", + "github.com/cpacia/bchutil", + "github.com/gcash/bchd/chaincfg/chainhash", + "github.com/gcash/bchd/txscript", + "github.com/gcash/bchd/wire", + "github.com/golang/protobuf/proto", + "github.com/golang/protobuf/ptypes/timestamp", + "github.com/gorilla/websocket", + "github.com/jessevdk/go-flags", + "github.com/ltcsuite/ltcd/chaincfg", + "github.com/ltcsuite/ltcd/chaincfg/chainhash", + "github.com/ltcsuite/ltcutil", + "github.com/ltcsuite/ltcutil/base58", + "github.com/ltcsuite/ltcwallet/wallet/txrules", + "github.com/minio/blake2b-simd", + "github.com/op/go-logging", + "github.com/tyler-smith/go-bip39", + "golang.org/x/crypto/ripemd160", + "golang.org/x/net/context", + "golang.org/x/net/proxy", + "google.golang.org/grpc", + "google.golang.org/grpc/reflection", + "gopkg.in/jarcoal/httpmock.v1", + ] + solver-name = "gps-cdcl" + solver-version = 1 \ No newline at end of file diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..68f89ca --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,114 @@ +# Gopkg.toml example +# +# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + branch = "master" + name = "github.com/OpenBazaar/golang-socketio" + +[[constraint]] + branch = "master" + name = "github.com/OpenBazaar/spvwallet" + +[[constraint]] + branch = "master" + name = "github.com/OpenBazaar/wallet-interface" + +[[constraint]] + branch = "master" + name = "github.com/btcsuite/btcd" + +[[constraint]] + branch = "master" + name = "github.com/btcsuite/btcutil" + +[[constraint]] + branch = "master" + name = "github.com/btcsuite/btcwallet" + +[[constraint]] + branch = "master" + name = "github.com/btcsuite/golangcrypto" + +[[constraint]] + branch = "master" + name = "github.com/cpacia/bchutil" + +[[constraint]] + branch = "master" + name = "github.com/golang/protobuf" + +[[constraint]] + branch = "master" + name = "github.com/gorilla/websocket" + +[[constraint]] + branch = "master" + name = "github.com/jessevdk/go-flags" + +[[constraint]] + branch = "master" + name = "github.com/ltcsuite/ltcd" + +[[constraint]] + branch = "master" + name = "github.com/ltcsuite/ltcutil" + +[[constraint]] + branch = "master" + name = "github.com/ltcsuite/ltcwallet" + +[[constraint]] + branch = "master" + name = "github.com/minio/blake2b-simd" + +[[constraint]] + branch = "master" + name = "github.com/op/go-logging" + +[[constraint]] + branch = "master" + name = "github.com/tyler-smith/go-bip39" + +[[constraint]] + branch = "master" + name = "golang.org/x/crypto" + +[[constraint]] + branch = "master" + name = "golang.org/x/net" + +[[constraint]] + branch = "master" + name = "google.golang.org/grpc" + +[[constraint]] + branch = "v1" + name = "gopkg.in/jarcoal/httpmock.v1" + +[prune] + go-tests = true + unused-packages = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..54c9625 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 OpenBazaar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 30ef423..ea9e6f0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,38 @@ -# Gleec - Chain&Wallet +[![Build Status](https://travis-ci.org/OpenBazaar/multiwallet.svg?branch=master)](https://travis-ci.org/OpenBazaar/multiwallet) +[![Coverage Status](https://coveralls.io/repos/github/OpenBazaar/multiwallet/badge.svg?branch=master)](https://coveralls.io/github/OpenBazaar/multiwallet?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/OpenBazaar/multiwallet)](https://goreportcard.com/report/github.com/OpenBazaar/multiwallet) + +# multiwallet +Insight API based multi-cryptocurrency wallet + +## Usage + +Once your go environment is configured (https://golang.org/doc/install), you should be able to run the multiwallet like this: + +``` +go get -u github.com/OpenBazaar/multiwallet +cd $GOPATH/src/github.com/OpenBazaar/multiwallet + +go run cmd/multiwallet/main.go -h +``` + +That last command will give you some subcommands you can then add to the end (in place of the `-h`): +``` +Usage: + main [OPTIONS] + +Help Options: + -h, --help Show this help message + +Available commands: + balance get the wallet's balances + chaintip return the height of the chain + currentaddress get the current bitcoin address + dumptables print out the database tables + newaddress get a new bitcoin address + spend send bitcoins + start start the wallet + stop stop the wallet + version print the version number +``` + diff --git a/api/pb/api.pb.go b/api/pb/api.pb.go new file mode 100644 index 0000000..c903b33 --- /dev/null +++ b/api/pb/api.pb.go @@ -0,0 +1,2602 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: api.proto + +package pb + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" +import timestamp "github.com/golang/protobuf/ptypes/timestamp" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type CoinType int32 + +const ( + CoinType_BITCOIN CoinType = 0 + CoinType_BITCOIN_CASH CoinType = 1 + CoinType_ZCASH CoinType = 2 + CoinType_LITECOIN CoinType = 3 + CoinType_ETHEREUM CoinType = 4 +) + +var CoinType_name = map[int32]string{ + 0: "BITCOIN", + 1: "BITCOIN_CASH", + 2: "ZCASH", + 3: "LITECOIN", + 4: "ETHEREUM", +} +var CoinType_value = map[string]int32{ + "BITCOIN": 0, + "BITCOIN_CASH": 1, + "ZCASH": 2, + "LITECOIN": 3, + "ETHEREUM": 4, +} + +func (x CoinType) String() string { + return proto.EnumName(CoinType_name, int32(x)) +} +func (CoinType) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{0} +} + +type KeyPurpose int32 + +const ( + KeyPurpose_INTERNAL KeyPurpose = 0 + KeyPurpose_EXTERNAL KeyPurpose = 1 +) + +var KeyPurpose_name = map[int32]string{ + 0: "INTERNAL", + 1: "EXTERNAL", +} +var KeyPurpose_value = map[string]int32{ + "INTERNAL": 0, + "EXTERNAL": 1, +} + +func (x KeyPurpose) String() string { + return proto.EnumName(KeyPurpose_name, int32(x)) +} +func (KeyPurpose) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{1} +} + +type FeeLevel int32 + +const ( + FeeLevel_ECONOMIC FeeLevel = 0 + FeeLevel_NORMAL FeeLevel = 1 + FeeLevel_PRIORITY FeeLevel = 2 +) + +var FeeLevel_name = map[int32]string{ + 0: "ECONOMIC", + 1: "NORMAL", + 2: "PRIORITY", +} +var FeeLevel_value = map[string]int32{ + "ECONOMIC": 0, + "NORMAL": 1, + "PRIORITY": 2, +} + +func (x FeeLevel) String() string { + return proto.EnumName(FeeLevel_name, int32(x)) +} +func (FeeLevel) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{2} +} + +type Empty struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Empty) Reset() { *m = Empty{} } +func (m *Empty) String() string { return proto.CompactTextString(m) } +func (*Empty) ProtoMessage() {} +func (*Empty) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{0} +} +func (m *Empty) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Empty.Unmarshal(m, b) +} +func (m *Empty) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Empty.Marshal(b, m, deterministic) +} +func (dst *Empty) XXX_Merge(src proto.Message) { + xxx_messageInfo_Empty.Merge(dst, src) +} +func (m *Empty) XXX_Size() int { + return xxx_messageInfo_Empty.Size(m) +} +func (m *Empty) XXX_DiscardUnknown() { + xxx_messageInfo_Empty.DiscardUnknown(m) +} + +var xxx_messageInfo_Empty proto.InternalMessageInfo + +type CoinSelection struct { + Coin CoinType `protobuf:"varint,1,opt,name=coin,proto3,enum=pb.CoinType" json:"coin,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CoinSelection) Reset() { *m = CoinSelection{} } +func (m *CoinSelection) String() string { return proto.CompactTextString(m) } +func (*CoinSelection) ProtoMessage() {} +func (*CoinSelection) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{1} +} +func (m *CoinSelection) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CoinSelection.Unmarshal(m, b) +} +func (m *CoinSelection) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CoinSelection.Marshal(b, m, deterministic) +} +func (dst *CoinSelection) XXX_Merge(src proto.Message) { + xxx_messageInfo_CoinSelection.Merge(dst, src) +} +func (m *CoinSelection) XXX_Size() int { + return xxx_messageInfo_CoinSelection.Size(m) +} +func (m *CoinSelection) XXX_DiscardUnknown() { + xxx_messageInfo_CoinSelection.DiscardUnknown(m) +} + +var xxx_messageInfo_CoinSelection proto.InternalMessageInfo + +func (m *CoinSelection) GetCoin() CoinType { + if m != nil { + return m.Coin + } + return CoinType_BITCOIN +} + +type Row struct { + Data string `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Row) Reset() { *m = Row{} } +func (m *Row) String() string { return proto.CompactTextString(m) } +func (*Row) ProtoMessage() {} +func (*Row) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{2} +} +func (m *Row) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Row.Unmarshal(m, b) +} +func (m *Row) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Row.Marshal(b, m, deterministic) +} +func (dst *Row) XXX_Merge(src proto.Message) { + xxx_messageInfo_Row.Merge(dst, src) +} +func (m *Row) XXX_Size() int { + return xxx_messageInfo_Row.Size(m) +} +func (m *Row) XXX_DiscardUnknown() { + xxx_messageInfo_Row.DiscardUnknown(m) +} + +var xxx_messageInfo_Row proto.InternalMessageInfo + +func (m *Row) GetData() string { + if m != nil { + return m.Data + } + return "" +} + +type KeySelection struct { + Coin CoinType `protobuf:"varint,1,opt,name=coin,proto3,enum=pb.CoinType" json:"coin,omitempty"` + Purpose KeyPurpose `protobuf:"varint,2,opt,name=purpose,proto3,enum=pb.KeyPurpose" json:"purpose,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *KeySelection) Reset() { *m = KeySelection{} } +func (m *KeySelection) String() string { return proto.CompactTextString(m) } +func (*KeySelection) ProtoMessage() {} +func (*KeySelection) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{3} +} +func (m *KeySelection) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_KeySelection.Unmarshal(m, b) +} +func (m *KeySelection) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_KeySelection.Marshal(b, m, deterministic) +} +func (dst *KeySelection) XXX_Merge(src proto.Message) { + xxx_messageInfo_KeySelection.Merge(dst, src) +} +func (m *KeySelection) XXX_Size() int { + return xxx_messageInfo_KeySelection.Size(m) +} +func (m *KeySelection) XXX_DiscardUnknown() { + xxx_messageInfo_KeySelection.DiscardUnknown(m) +} + +var xxx_messageInfo_KeySelection proto.InternalMessageInfo + +func (m *KeySelection) GetCoin() CoinType { + if m != nil { + return m.Coin + } + return CoinType_BITCOIN +} + +func (m *KeySelection) GetPurpose() KeyPurpose { + if m != nil { + return m.Purpose + } + return KeyPurpose_INTERNAL +} + +type Address struct { + Coin CoinType `protobuf:"varint,1,opt,name=coin,proto3,enum=pb.CoinType" json:"coin,omitempty"` + Addr string `protobuf:"bytes,2,opt,name=addr,proto3" json:"addr,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Address) Reset() { *m = Address{} } +func (m *Address) String() string { return proto.CompactTextString(m) } +func (*Address) ProtoMessage() {} +func (*Address) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{4} +} +func (m *Address) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Address.Unmarshal(m, b) +} +func (m *Address) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Address.Marshal(b, m, deterministic) +} +func (dst *Address) XXX_Merge(src proto.Message) { + xxx_messageInfo_Address.Merge(dst, src) +} +func (m *Address) XXX_Size() int { + return xxx_messageInfo_Address.Size(m) +} +func (m *Address) XXX_DiscardUnknown() { + xxx_messageInfo_Address.DiscardUnknown(m) +} + +var xxx_messageInfo_Address proto.InternalMessageInfo + +func (m *Address) GetCoin() CoinType { + if m != nil { + return m.Coin + } + return CoinType_BITCOIN +} + +func (m *Address) GetAddr() string { + if m != nil { + return m.Addr + } + return "" +} + +type Height struct { + Height uint32 `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Height) Reset() { *m = Height{} } +func (m *Height) String() string { return proto.CompactTextString(m) } +func (*Height) ProtoMessage() {} +func (*Height) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{5} +} +func (m *Height) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Height.Unmarshal(m, b) +} +func (m *Height) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Height.Marshal(b, m, deterministic) +} +func (dst *Height) XXX_Merge(src proto.Message) { + xxx_messageInfo_Height.Merge(dst, src) +} +func (m *Height) XXX_Size() int { + return xxx_messageInfo_Height.Size(m) +} +func (m *Height) XXX_DiscardUnknown() { + xxx_messageInfo_Height.DiscardUnknown(m) +} + +var xxx_messageInfo_Height proto.InternalMessageInfo + +func (m *Height) GetHeight() uint32 { + if m != nil { + return m.Height + } + return 0 +} + +type Balances struct { + Confirmed uint64 `protobuf:"varint,1,opt,name=confirmed,proto3" json:"confirmed,omitempty"` + Unconfirmed uint64 `protobuf:"varint,2,opt,name=unconfirmed,proto3" json:"unconfirmed,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Balances) Reset() { *m = Balances{} } +func (m *Balances) String() string { return proto.CompactTextString(m) } +func (*Balances) ProtoMessage() {} +func (*Balances) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{6} +} +func (m *Balances) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Balances.Unmarshal(m, b) +} +func (m *Balances) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Balances.Marshal(b, m, deterministic) +} +func (dst *Balances) XXX_Merge(src proto.Message) { + xxx_messageInfo_Balances.Merge(dst, src) +} +func (m *Balances) XXX_Size() int { + return xxx_messageInfo_Balances.Size(m) +} +func (m *Balances) XXX_DiscardUnknown() { + xxx_messageInfo_Balances.DiscardUnknown(m) +} + +var xxx_messageInfo_Balances proto.InternalMessageInfo + +func (m *Balances) GetConfirmed() uint64 { + if m != nil { + return m.Confirmed + } + return 0 +} + +func (m *Balances) GetUnconfirmed() uint64 { + if m != nil { + return m.Unconfirmed + } + return 0 +} + +type Key struct { + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Key) Reset() { *m = Key{} } +func (m *Key) String() string { return proto.CompactTextString(m) } +func (*Key) ProtoMessage() {} +func (*Key) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{7} +} +func (m *Key) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Key.Unmarshal(m, b) +} +func (m *Key) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Key.Marshal(b, m, deterministic) +} +func (dst *Key) XXX_Merge(src proto.Message) { + xxx_messageInfo_Key.Merge(dst, src) +} +func (m *Key) XXX_Size() int { + return xxx_messageInfo_Key.Size(m) +} +func (m *Key) XXX_DiscardUnknown() { + xxx_messageInfo_Key.DiscardUnknown(m) +} + +var xxx_messageInfo_Key proto.InternalMessageInfo + +func (m *Key) GetKey() string { + if m != nil { + return m.Key + } + return "" +} + +type Keys struct { + Keys []*Key `protobuf:"bytes,1,rep,name=keys,proto3" json:"keys,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Keys) Reset() { *m = Keys{} } +func (m *Keys) String() string { return proto.CompactTextString(m) } +func (*Keys) ProtoMessage() {} +func (*Keys) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{8} +} +func (m *Keys) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Keys.Unmarshal(m, b) +} +func (m *Keys) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Keys.Marshal(b, m, deterministic) +} +func (dst *Keys) XXX_Merge(src proto.Message) { + xxx_messageInfo_Keys.Merge(dst, src) +} +func (m *Keys) XXX_Size() int { + return xxx_messageInfo_Keys.Size(m) +} +func (m *Keys) XXX_DiscardUnknown() { + xxx_messageInfo_Keys.DiscardUnknown(m) +} + +var xxx_messageInfo_Keys proto.InternalMessageInfo + +func (m *Keys) GetKeys() []*Key { + if m != nil { + return m.Keys + } + return nil +} + +type Addresses struct { + Addresses []*Address `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Addresses) Reset() { *m = Addresses{} } +func (m *Addresses) String() string { return proto.CompactTextString(m) } +func (*Addresses) ProtoMessage() {} +func (*Addresses) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{9} +} +func (m *Addresses) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Addresses.Unmarshal(m, b) +} +func (m *Addresses) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Addresses.Marshal(b, m, deterministic) +} +func (dst *Addresses) XXX_Merge(src proto.Message) { + xxx_messageInfo_Addresses.Merge(dst, src) +} +func (m *Addresses) XXX_Size() int { + return xxx_messageInfo_Addresses.Size(m) +} +func (m *Addresses) XXX_DiscardUnknown() { + xxx_messageInfo_Addresses.DiscardUnknown(m) +} + +var xxx_messageInfo_Addresses proto.InternalMessageInfo + +func (m *Addresses) GetAddresses() []*Address { + if m != nil { + return m.Addresses + } + return nil +} + +type BoolResponse struct { + Bool bool `protobuf:"varint,1,opt,name=bool,proto3" json:"bool,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *BoolResponse) Reset() { *m = BoolResponse{} } +func (m *BoolResponse) String() string { return proto.CompactTextString(m) } +func (*BoolResponse) ProtoMessage() {} +func (*BoolResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{10} +} +func (m *BoolResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_BoolResponse.Unmarshal(m, b) +} +func (m *BoolResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_BoolResponse.Marshal(b, m, deterministic) +} +func (dst *BoolResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_BoolResponse.Merge(dst, src) +} +func (m *BoolResponse) XXX_Size() int { + return xxx_messageInfo_BoolResponse.Size(m) +} +func (m *BoolResponse) XXX_DiscardUnknown() { + xxx_messageInfo_BoolResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_BoolResponse proto.InternalMessageInfo + +func (m *BoolResponse) GetBool() bool { + if m != nil { + return m.Bool + } + return false +} + +type NetParams struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *NetParams) Reset() { *m = NetParams{} } +func (m *NetParams) String() string { return proto.CompactTextString(m) } +func (*NetParams) ProtoMessage() {} +func (*NetParams) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{11} +} +func (m *NetParams) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_NetParams.Unmarshal(m, b) +} +func (m *NetParams) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_NetParams.Marshal(b, m, deterministic) +} +func (dst *NetParams) XXX_Merge(src proto.Message) { + xxx_messageInfo_NetParams.Merge(dst, src) +} +func (m *NetParams) XXX_Size() int { + return xxx_messageInfo_NetParams.Size(m) +} +func (m *NetParams) XXX_DiscardUnknown() { + xxx_messageInfo_NetParams.DiscardUnknown(m) +} + +var xxx_messageInfo_NetParams proto.InternalMessageInfo + +func (m *NetParams) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +type TransactionList struct { + Transactions []*Tx `protobuf:"bytes,1,rep,name=transactions,proto3" json:"transactions,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *TransactionList) Reset() { *m = TransactionList{} } +func (m *TransactionList) String() string { return proto.CompactTextString(m) } +func (*TransactionList) ProtoMessage() {} +func (*TransactionList) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{12} +} +func (m *TransactionList) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_TransactionList.Unmarshal(m, b) +} +func (m *TransactionList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_TransactionList.Marshal(b, m, deterministic) +} +func (dst *TransactionList) XXX_Merge(src proto.Message) { + xxx_messageInfo_TransactionList.Merge(dst, src) +} +func (m *TransactionList) XXX_Size() int { + return xxx_messageInfo_TransactionList.Size(m) +} +func (m *TransactionList) XXX_DiscardUnknown() { + xxx_messageInfo_TransactionList.DiscardUnknown(m) +} + +var xxx_messageInfo_TransactionList proto.InternalMessageInfo + +func (m *TransactionList) GetTransactions() []*Tx { + if m != nil { + return m.Transactions + } + return nil +} + +type Tx struct { + Txid string `protobuf:"bytes,1,opt,name=txid,proto3" json:"txid,omitempty"` + Value int64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"` + Height int32 `protobuf:"varint,3,opt,name=height,proto3" json:"height,omitempty"` + Timestamp *timestamp.Timestamp `protobuf:"bytes,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + WatchOnly bool `protobuf:"varint,5,opt,name=watchOnly,proto3" json:"watchOnly,omitempty"` + Raw []byte `protobuf:"bytes,6,opt,name=raw,proto3" json:"raw,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Tx) Reset() { *m = Tx{} } +func (m *Tx) String() string { return proto.CompactTextString(m) } +func (*Tx) ProtoMessage() {} +func (*Tx) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{13} +} +func (m *Tx) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Tx.Unmarshal(m, b) +} +func (m *Tx) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Tx.Marshal(b, m, deterministic) +} +func (dst *Tx) XXX_Merge(src proto.Message) { + xxx_messageInfo_Tx.Merge(dst, src) +} +func (m *Tx) XXX_Size() int { + return xxx_messageInfo_Tx.Size(m) +} +func (m *Tx) XXX_DiscardUnknown() { + xxx_messageInfo_Tx.DiscardUnknown(m) +} + +var xxx_messageInfo_Tx proto.InternalMessageInfo + +func (m *Tx) GetTxid() string { + if m != nil { + return m.Txid + } + return "" +} + +func (m *Tx) GetValue() int64 { + if m != nil { + return m.Value + } + return 0 +} + +func (m *Tx) GetHeight() int32 { + if m != nil { + return m.Height + } + return 0 +} + +func (m *Tx) GetTimestamp() *timestamp.Timestamp { + if m != nil { + return m.Timestamp + } + return nil +} + +func (m *Tx) GetWatchOnly() bool { + if m != nil { + return m.WatchOnly + } + return false +} + +func (m *Tx) GetRaw() []byte { + if m != nil { + return m.Raw + } + return nil +} + +type Txid struct { + Coin CoinType `protobuf:"varint,1,opt,name=coin,proto3,enum=pb.CoinType" json:"coin,omitempty"` + Hash string `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Txid) Reset() { *m = Txid{} } +func (m *Txid) String() string { return proto.CompactTextString(m) } +func (*Txid) ProtoMessage() {} +func (*Txid) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{14} +} +func (m *Txid) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Txid.Unmarshal(m, b) +} +func (m *Txid) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Txid.Marshal(b, m, deterministic) +} +func (dst *Txid) XXX_Merge(src proto.Message) { + xxx_messageInfo_Txid.Merge(dst, src) +} +func (m *Txid) XXX_Size() int { + return xxx_messageInfo_Txid.Size(m) +} +func (m *Txid) XXX_DiscardUnknown() { + xxx_messageInfo_Txid.DiscardUnknown(m) +} + +var xxx_messageInfo_Txid proto.InternalMessageInfo + +func (m *Txid) GetCoin() CoinType { + if m != nil { + return m.Coin + } + return CoinType_BITCOIN +} + +func (m *Txid) GetHash() string { + if m != nil { + return m.Hash + } + return "" +} + +type FeeLevelSelection struct { + Coin CoinType `protobuf:"varint,1,opt,name=coin,proto3,enum=pb.CoinType" json:"coin,omitempty"` + FeeLevel FeeLevel `protobuf:"varint,2,opt,name=feeLevel,proto3,enum=pb.FeeLevel" json:"feeLevel,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *FeeLevelSelection) Reset() { *m = FeeLevelSelection{} } +func (m *FeeLevelSelection) String() string { return proto.CompactTextString(m) } +func (*FeeLevelSelection) ProtoMessage() {} +func (*FeeLevelSelection) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{15} +} +func (m *FeeLevelSelection) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_FeeLevelSelection.Unmarshal(m, b) +} +func (m *FeeLevelSelection) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_FeeLevelSelection.Marshal(b, m, deterministic) +} +func (dst *FeeLevelSelection) XXX_Merge(src proto.Message) { + xxx_messageInfo_FeeLevelSelection.Merge(dst, src) +} +func (m *FeeLevelSelection) XXX_Size() int { + return xxx_messageInfo_FeeLevelSelection.Size(m) +} +func (m *FeeLevelSelection) XXX_DiscardUnknown() { + xxx_messageInfo_FeeLevelSelection.DiscardUnknown(m) +} + +var xxx_messageInfo_FeeLevelSelection proto.InternalMessageInfo + +func (m *FeeLevelSelection) GetCoin() CoinType { + if m != nil { + return m.Coin + } + return CoinType_BITCOIN +} + +func (m *FeeLevelSelection) GetFeeLevel() FeeLevel { + if m != nil { + return m.FeeLevel + } + return FeeLevel_ECONOMIC +} + +type FeePerByte struct { + Fee uint64 `protobuf:"varint,1,opt,name=fee,proto3" json:"fee,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *FeePerByte) Reset() { *m = FeePerByte{} } +func (m *FeePerByte) String() string { return proto.CompactTextString(m) } +func (*FeePerByte) ProtoMessage() {} +func (*FeePerByte) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{16} +} +func (m *FeePerByte) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_FeePerByte.Unmarshal(m, b) +} +func (m *FeePerByte) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_FeePerByte.Marshal(b, m, deterministic) +} +func (dst *FeePerByte) XXX_Merge(src proto.Message) { + xxx_messageInfo_FeePerByte.Merge(dst, src) +} +func (m *FeePerByte) XXX_Size() int { + return xxx_messageInfo_FeePerByte.Size(m) +} +func (m *FeePerByte) XXX_DiscardUnknown() { + xxx_messageInfo_FeePerByte.DiscardUnknown(m) +} + +var xxx_messageInfo_FeePerByte proto.InternalMessageInfo + +func (m *FeePerByte) GetFee() uint64 { + if m != nil { + return m.Fee + } + return 0 +} + +type Fee struct { + Fee uint64 `protobuf:"varint,1,opt,name=fee,proto3" json:"fee,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Fee) Reset() { *m = Fee{} } +func (m *Fee) String() string { return proto.CompactTextString(m) } +func (*Fee) ProtoMessage() {} +func (*Fee) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{17} +} +func (m *Fee) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Fee.Unmarshal(m, b) +} +func (m *Fee) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Fee.Marshal(b, m, deterministic) +} +func (dst *Fee) XXX_Merge(src proto.Message) { + xxx_messageInfo_Fee.Merge(dst, src) +} +func (m *Fee) XXX_Size() int { + return xxx_messageInfo_Fee.Size(m) +} +func (m *Fee) XXX_DiscardUnknown() { + xxx_messageInfo_Fee.DiscardUnknown(m) +} + +var xxx_messageInfo_Fee proto.InternalMessageInfo + +func (m *Fee) GetFee() uint64 { + if m != nil { + return m.Fee + } + return 0 +} + +type SpendInfo struct { + Coin CoinType `protobuf:"varint,1,opt,name=coin,proto3,enum=pb.CoinType" json:"coin,omitempty"` + Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` + Amount uint64 `protobuf:"varint,3,opt,name=amount,proto3" json:"amount,omitempty"` + FeeLevel FeeLevel `protobuf:"varint,4,opt,name=feeLevel,proto3,enum=pb.FeeLevel" json:"feeLevel,omitempty"` + Memo string `protobuf:"bytes,5,opt,name=memo,proto3" json:"memo,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SpendInfo) Reset() { *m = SpendInfo{} } +func (m *SpendInfo) String() string { return proto.CompactTextString(m) } +func (*SpendInfo) ProtoMessage() {} +func (*SpendInfo) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{18} +} +func (m *SpendInfo) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SpendInfo.Unmarshal(m, b) +} +func (m *SpendInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SpendInfo.Marshal(b, m, deterministic) +} +func (dst *SpendInfo) XXX_Merge(src proto.Message) { + xxx_messageInfo_SpendInfo.Merge(dst, src) +} +func (m *SpendInfo) XXX_Size() int { + return xxx_messageInfo_SpendInfo.Size(m) +} +func (m *SpendInfo) XXX_DiscardUnknown() { + xxx_messageInfo_SpendInfo.DiscardUnknown(m) +} + +var xxx_messageInfo_SpendInfo proto.InternalMessageInfo + +func (m *SpendInfo) GetCoin() CoinType { + if m != nil { + return m.Coin + } + return CoinType_BITCOIN +} + +func (m *SpendInfo) GetAddress() string { + if m != nil { + return m.Address + } + return "" +} + +func (m *SpendInfo) GetAmount() uint64 { + if m != nil { + return m.Amount + } + return 0 +} + +func (m *SpendInfo) GetFeeLevel() FeeLevel { + if m != nil { + return m.FeeLevel + } + return FeeLevel_ECONOMIC +} + +func (m *SpendInfo) GetMemo() string { + if m != nil { + return m.Memo + } + return "" +} + +type Confirmations struct { + Confirmations uint32 `protobuf:"varint,1,opt,name=confirmations,proto3" json:"confirmations,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Confirmations) Reset() { *m = Confirmations{} } +func (m *Confirmations) String() string { return proto.CompactTextString(m) } +func (*Confirmations) ProtoMessage() {} +func (*Confirmations) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{19} +} +func (m *Confirmations) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Confirmations.Unmarshal(m, b) +} +func (m *Confirmations) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Confirmations.Marshal(b, m, deterministic) +} +func (dst *Confirmations) XXX_Merge(src proto.Message) { + xxx_messageInfo_Confirmations.Merge(dst, src) +} +func (m *Confirmations) XXX_Size() int { + return xxx_messageInfo_Confirmations.Size(m) +} +func (m *Confirmations) XXX_DiscardUnknown() { + xxx_messageInfo_Confirmations.DiscardUnknown(m) +} + +var xxx_messageInfo_Confirmations proto.InternalMessageInfo + +func (m *Confirmations) GetConfirmations() uint32 { + if m != nil { + return m.Confirmations + } + return 0 +} + +type Utxo struct { + Txid string `protobuf:"bytes,1,opt,name=txid,proto3" json:"txid,omitempty"` + Index uint32 `protobuf:"varint,2,opt,name=index,proto3" json:"index,omitempty"` + Value uint64 `protobuf:"varint,3,opt,name=value,proto3" json:"value,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Utxo) Reset() { *m = Utxo{} } +func (m *Utxo) String() string { return proto.CompactTextString(m) } +func (*Utxo) ProtoMessage() {} +func (*Utxo) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{20} +} +func (m *Utxo) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Utxo.Unmarshal(m, b) +} +func (m *Utxo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Utxo.Marshal(b, m, deterministic) +} +func (dst *Utxo) XXX_Merge(src proto.Message) { + xxx_messageInfo_Utxo.Merge(dst, src) +} +func (m *Utxo) XXX_Size() int { + return xxx_messageInfo_Utxo.Size(m) +} +func (m *Utxo) XXX_DiscardUnknown() { + xxx_messageInfo_Utxo.DiscardUnknown(m) +} + +var xxx_messageInfo_Utxo proto.InternalMessageInfo + +func (m *Utxo) GetTxid() string { + if m != nil { + return m.Txid + } + return "" +} + +func (m *Utxo) GetIndex() uint32 { + if m != nil { + return m.Index + } + return 0 +} + +func (m *Utxo) GetValue() uint64 { + if m != nil { + return m.Value + } + return 0 +} + +type SweepInfo struct { + Coin CoinType `protobuf:"varint,1,opt,name=coin,proto3,enum=pb.CoinType" json:"coin,omitempty"` + Utxos []*Utxo `protobuf:"bytes,2,rep,name=utxos,proto3" json:"utxos,omitempty"` + Address string `protobuf:"bytes,3,opt,name=address,proto3" json:"address,omitempty"` + Key string `protobuf:"bytes,4,opt,name=key,proto3" json:"key,omitempty"` + RedeemScript []byte `protobuf:"bytes,5,opt,name=redeemScript,proto3" json:"redeemScript,omitempty"` + FeeLevel FeeLevel `protobuf:"varint,6,opt,name=feeLevel,proto3,enum=pb.FeeLevel" json:"feeLevel,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SweepInfo) Reset() { *m = SweepInfo{} } +func (m *SweepInfo) String() string { return proto.CompactTextString(m) } +func (*SweepInfo) ProtoMessage() {} +func (*SweepInfo) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{21} +} +func (m *SweepInfo) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SweepInfo.Unmarshal(m, b) +} +func (m *SweepInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SweepInfo.Marshal(b, m, deterministic) +} +func (dst *SweepInfo) XXX_Merge(src proto.Message) { + xxx_messageInfo_SweepInfo.Merge(dst, src) +} +func (m *SweepInfo) XXX_Size() int { + return xxx_messageInfo_SweepInfo.Size(m) +} +func (m *SweepInfo) XXX_DiscardUnknown() { + xxx_messageInfo_SweepInfo.DiscardUnknown(m) +} + +var xxx_messageInfo_SweepInfo proto.InternalMessageInfo + +func (m *SweepInfo) GetCoin() CoinType { + if m != nil { + return m.Coin + } + return CoinType_BITCOIN +} + +func (m *SweepInfo) GetUtxos() []*Utxo { + if m != nil { + return m.Utxos + } + return nil +} + +func (m *SweepInfo) GetAddress() string { + if m != nil { + return m.Address + } + return "" +} + +func (m *SweepInfo) GetKey() string { + if m != nil { + return m.Key + } + return "" +} + +func (m *SweepInfo) GetRedeemScript() []byte { + if m != nil { + return m.RedeemScript + } + return nil +} + +func (m *SweepInfo) GetFeeLevel() FeeLevel { + if m != nil { + return m.FeeLevel + } + return FeeLevel_ECONOMIC +} + +type Input struct { + Txid string `protobuf:"bytes,1,opt,name=txid,proto3" json:"txid,omitempty"` + Index uint32 `protobuf:"varint,2,opt,name=index,proto3" json:"index,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Input) Reset() { *m = Input{} } +func (m *Input) String() string { return proto.CompactTextString(m) } +func (*Input) ProtoMessage() {} +func (*Input) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{22} +} +func (m *Input) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Input.Unmarshal(m, b) +} +func (m *Input) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Input.Marshal(b, m, deterministic) +} +func (dst *Input) XXX_Merge(src proto.Message) { + xxx_messageInfo_Input.Merge(dst, src) +} +func (m *Input) XXX_Size() int { + return xxx_messageInfo_Input.Size(m) +} +func (m *Input) XXX_DiscardUnknown() { + xxx_messageInfo_Input.DiscardUnknown(m) +} + +var xxx_messageInfo_Input proto.InternalMessageInfo + +func (m *Input) GetTxid() string { + if m != nil { + return m.Txid + } + return "" +} + +func (m *Input) GetIndex() uint32 { + if m != nil { + return m.Index + } + return 0 +} + +type Output struct { + ScriptPubKey []byte `protobuf:"bytes,1,opt,name=scriptPubKey,proto3" json:"scriptPubKey,omitempty"` + Value uint64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Output) Reset() { *m = Output{} } +func (m *Output) String() string { return proto.CompactTextString(m) } +func (*Output) ProtoMessage() {} +func (*Output) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{23} +} +func (m *Output) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Output.Unmarshal(m, b) +} +func (m *Output) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Output.Marshal(b, m, deterministic) +} +func (dst *Output) XXX_Merge(src proto.Message) { + xxx_messageInfo_Output.Merge(dst, src) +} +func (m *Output) XXX_Size() int { + return xxx_messageInfo_Output.Size(m) +} +func (m *Output) XXX_DiscardUnknown() { + xxx_messageInfo_Output.DiscardUnknown(m) +} + +var xxx_messageInfo_Output proto.InternalMessageInfo + +func (m *Output) GetScriptPubKey() []byte { + if m != nil { + return m.ScriptPubKey + } + return nil +} + +func (m *Output) GetValue() uint64 { + if m != nil { + return m.Value + } + return 0 +} + +type Signature struct { + Index uint32 `protobuf:"varint,1,opt,name=index,proto3" json:"index,omitempty"` + Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Signature) Reset() { *m = Signature{} } +func (m *Signature) String() string { return proto.CompactTextString(m) } +func (*Signature) ProtoMessage() {} +func (*Signature) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{24} +} +func (m *Signature) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Signature.Unmarshal(m, b) +} +func (m *Signature) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Signature.Marshal(b, m, deterministic) +} +func (dst *Signature) XXX_Merge(src proto.Message) { + xxx_messageInfo_Signature.Merge(dst, src) +} +func (m *Signature) XXX_Size() int { + return xxx_messageInfo_Signature.Size(m) +} +func (m *Signature) XXX_DiscardUnknown() { + xxx_messageInfo_Signature.DiscardUnknown(m) +} + +var xxx_messageInfo_Signature proto.InternalMessageInfo + +func (m *Signature) GetIndex() uint32 { + if m != nil { + return m.Index + } + return 0 +} + +func (m *Signature) GetSignature() []byte { + if m != nil { + return m.Signature + } + return nil +} + +type CreateMultisigInfo struct { + Coin CoinType `protobuf:"varint,1,opt,name=coin,proto3,enum=pb.CoinType" json:"coin,omitempty"` + Inputs []*Input `protobuf:"bytes,2,rep,name=inputs,proto3" json:"inputs,omitempty"` + Outputs []*Output `protobuf:"bytes,3,rep,name=outputs,proto3" json:"outputs,omitempty"` + Key string `protobuf:"bytes,4,opt,name=key,proto3" json:"key,omitempty"` + RedeemScript []byte `protobuf:"bytes,5,opt,name=redeemScript,proto3" json:"redeemScript,omitempty"` + FeePerByte uint64 `protobuf:"varint,6,opt,name=feePerByte,proto3" json:"feePerByte,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CreateMultisigInfo) Reset() { *m = CreateMultisigInfo{} } +func (m *CreateMultisigInfo) String() string { return proto.CompactTextString(m) } +func (*CreateMultisigInfo) ProtoMessage() {} +func (*CreateMultisigInfo) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{25} +} +func (m *CreateMultisigInfo) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CreateMultisigInfo.Unmarshal(m, b) +} +func (m *CreateMultisigInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CreateMultisigInfo.Marshal(b, m, deterministic) +} +func (dst *CreateMultisigInfo) XXX_Merge(src proto.Message) { + xxx_messageInfo_CreateMultisigInfo.Merge(dst, src) +} +func (m *CreateMultisigInfo) XXX_Size() int { + return xxx_messageInfo_CreateMultisigInfo.Size(m) +} +func (m *CreateMultisigInfo) XXX_DiscardUnknown() { + xxx_messageInfo_CreateMultisigInfo.DiscardUnknown(m) +} + +var xxx_messageInfo_CreateMultisigInfo proto.InternalMessageInfo + +func (m *CreateMultisigInfo) GetCoin() CoinType { + if m != nil { + return m.Coin + } + return CoinType_BITCOIN +} + +func (m *CreateMultisigInfo) GetInputs() []*Input { + if m != nil { + return m.Inputs + } + return nil +} + +func (m *CreateMultisigInfo) GetOutputs() []*Output { + if m != nil { + return m.Outputs + } + return nil +} + +func (m *CreateMultisigInfo) GetKey() string { + if m != nil { + return m.Key + } + return "" +} + +func (m *CreateMultisigInfo) GetRedeemScript() []byte { + if m != nil { + return m.RedeemScript + } + return nil +} + +func (m *CreateMultisigInfo) GetFeePerByte() uint64 { + if m != nil { + return m.FeePerByte + } + return 0 +} + +type SignatureList struct { + Sigs []*Signature `protobuf:"bytes,1,rep,name=sigs,proto3" json:"sigs,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SignatureList) Reset() { *m = SignatureList{} } +func (m *SignatureList) String() string { return proto.CompactTextString(m) } +func (*SignatureList) ProtoMessage() {} +func (*SignatureList) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{26} +} +func (m *SignatureList) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SignatureList.Unmarshal(m, b) +} +func (m *SignatureList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SignatureList.Marshal(b, m, deterministic) +} +func (dst *SignatureList) XXX_Merge(src proto.Message) { + xxx_messageInfo_SignatureList.Merge(dst, src) +} +func (m *SignatureList) XXX_Size() int { + return xxx_messageInfo_SignatureList.Size(m) +} +func (m *SignatureList) XXX_DiscardUnknown() { + xxx_messageInfo_SignatureList.DiscardUnknown(m) +} + +var xxx_messageInfo_SignatureList proto.InternalMessageInfo + +func (m *SignatureList) GetSigs() []*Signature { + if m != nil { + return m.Sigs + } + return nil +} + +type MultisignInfo struct { + Coin CoinType `protobuf:"varint,1,opt,name=coin,proto3,enum=pb.CoinType" json:"coin,omitempty"` + Inputs []*Input `protobuf:"bytes,2,rep,name=inputs,proto3" json:"inputs,omitempty"` + Outputs []*Output `protobuf:"bytes,3,rep,name=outputs,proto3" json:"outputs,omitempty"` + Sig1 []*Signature `protobuf:"bytes,4,rep,name=sig1,proto3" json:"sig1,omitempty"` + Sig2 []*Signature `protobuf:"bytes,5,rep,name=sig2,proto3" json:"sig2,omitempty"` + RedeemScript []byte `protobuf:"bytes,6,opt,name=redeemScript,proto3" json:"redeemScript,omitempty"` + FeePerByte uint64 `protobuf:"varint,7,opt,name=feePerByte,proto3" json:"feePerByte,omitempty"` + Broadcast bool `protobuf:"varint,8,opt,name=broadcast,proto3" json:"broadcast,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *MultisignInfo) Reset() { *m = MultisignInfo{} } +func (m *MultisignInfo) String() string { return proto.CompactTextString(m) } +func (*MultisignInfo) ProtoMessage() {} +func (*MultisignInfo) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{27} +} +func (m *MultisignInfo) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_MultisignInfo.Unmarshal(m, b) +} +func (m *MultisignInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_MultisignInfo.Marshal(b, m, deterministic) +} +func (dst *MultisignInfo) XXX_Merge(src proto.Message) { + xxx_messageInfo_MultisignInfo.Merge(dst, src) +} +func (m *MultisignInfo) XXX_Size() int { + return xxx_messageInfo_MultisignInfo.Size(m) +} +func (m *MultisignInfo) XXX_DiscardUnknown() { + xxx_messageInfo_MultisignInfo.DiscardUnknown(m) +} + +var xxx_messageInfo_MultisignInfo proto.InternalMessageInfo + +func (m *MultisignInfo) GetCoin() CoinType { + if m != nil { + return m.Coin + } + return CoinType_BITCOIN +} + +func (m *MultisignInfo) GetInputs() []*Input { + if m != nil { + return m.Inputs + } + return nil +} + +func (m *MultisignInfo) GetOutputs() []*Output { + if m != nil { + return m.Outputs + } + return nil +} + +func (m *MultisignInfo) GetSig1() []*Signature { + if m != nil { + return m.Sig1 + } + return nil +} + +func (m *MultisignInfo) GetSig2() []*Signature { + if m != nil { + return m.Sig2 + } + return nil +} + +func (m *MultisignInfo) GetRedeemScript() []byte { + if m != nil { + return m.RedeemScript + } + return nil +} + +func (m *MultisignInfo) GetFeePerByte() uint64 { + if m != nil { + return m.FeePerByte + } + return 0 +} + +func (m *MultisignInfo) GetBroadcast() bool { + if m != nil { + return m.Broadcast + } + return false +} + +type RawTx struct { + Tx []byte `protobuf:"bytes,1,opt,name=tx,proto3" json:"tx,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RawTx) Reset() { *m = RawTx{} } +func (m *RawTx) String() string { return proto.CompactTextString(m) } +func (*RawTx) ProtoMessage() {} +func (*RawTx) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{28} +} +func (m *RawTx) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RawTx.Unmarshal(m, b) +} +func (m *RawTx) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RawTx.Marshal(b, m, deterministic) +} +func (dst *RawTx) XXX_Merge(src proto.Message) { + xxx_messageInfo_RawTx.Merge(dst, src) +} +func (m *RawTx) XXX_Size() int { + return xxx_messageInfo_RawTx.Size(m) +} +func (m *RawTx) XXX_DiscardUnknown() { + xxx_messageInfo_RawTx.DiscardUnknown(m) +} + +var xxx_messageInfo_RawTx proto.InternalMessageInfo + +func (m *RawTx) GetTx() []byte { + if m != nil { + return m.Tx + } + return nil +} + +type EstimateFeeData struct { + Coin CoinType `protobuf:"varint,1,opt,name=coin,proto3,enum=pb.CoinType" json:"coin,omitempty"` + Inputs []*Input `protobuf:"bytes,2,rep,name=inputs,proto3" json:"inputs,omitempty"` + Outputs []*Output `protobuf:"bytes,3,rep,name=outputs,proto3" json:"outputs,omitempty"` + FeePerByte uint64 `protobuf:"varint,4,opt,name=feePerByte,proto3" json:"feePerByte,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *EstimateFeeData) Reset() { *m = EstimateFeeData{} } +func (m *EstimateFeeData) String() string { return proto.CompactTextString(m) } +func (*EstimateFeeData) ProtoMessage() {} +func (*EstimateFeeData) Descriptor() ([]byte, []int) { + return fileDescriptor_api_2ff753dddd9b028a, []int{29} +} +func (m *EstimateFeeData) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_EstimateFeeData.Unmarshal(m, b) +} +func (m *EstimateFeeData) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_EstimateFeeData.Marshal(b, m, deterministic) +} +func (dst *EstimateFeeData) XXX_Merge(src proto.Message) { + xxx_messageInfo_EstimateFeeData.Merge(dst, src) +} +func (m *EstimateFeeData) XXX_Size() int { + return xxx_messageInfo_EstimateFeeData.Size(m) +} +func (m *EstimateFeeData) XXX_DiscardUnknown() { + xxx_messageInfo_EstimateFeeData.DiscardUnknown(m) +} + +var xxx_messageInfo_EstimateFeeData proto.InternalMessageInfo + +func (m *EstimateFeeData) GetCoin() CoinType { + if m != nil { + return m.Coin + } + return CoinType_BITCOIN +} + +func (m *EstimateFeeData) GetInputs() []*Input { + if m != nil { + return m.Inputs + } + return nil +} + +func (m *EstimateFeeData) GetOutputs() []*Output { + if m != nil { + return m.Outputs + } + return nil +} + +func (m *EstimateFeeData) GetFeePerByte() uint64 { + if m != nil { + return m.FeePerByte + } + return 0 +} + +func init() { + proto.RegisterType((*Empty)(nil), "pb.Empty") + proto.RegisterType((*CoinSelection)(nil), "pb.CoinSelection") + proto.RegisterType((*Row)(nil), "pb.Row") + proto.RegisterType((*KeySelection)(nil), "pb.KeySelection") + proto.RegisterType((*Address)(nil), "pb.Address") + proto.RegisterType((*Height)(nil), "pb.Height") + proto.RegisterType((*Balances)(nil), "pb.Balances") + proto.RegisterType((*Key)(nil), "pb.Key") + proto.RegisterType((*Keys)(nil), "pb.Keys") + proto.RegisterType((*Addresses)(nil), "pb.Addresses") + proto.RegisterType((*BoolResponse)(nil), "pb.BoolResponse") + proto.RegisterType((*NetParams)(nil), "pb.NetParams") + proto.RegisterType((*TransactionList)(nil), "pb.TransactionList") + proto.RegisterType((*Tx)(nil), "pb.Tx") + proto.RegisterType((*Txid)(nil), "pb.Txid") + proto.RegisterType((*FeeLevelSelection)(nil), "pb.FeeLevelSelection") + proto.RegisterType((*FeePerByte)(nil), "pb.FeePerByte") + proto.RegisterType((*Fee)(nil), "pb.Fee") + proto.RegisterType((*SpendInfo)(nil), "pb.SpendInfo") + proto.RegisterType((*Confirmations)(nil), "pb.Confirmations") + proto.RegisterType((*Utxo)(nil), "pb.Utxo") + proto.RegisterType((*SweepInfo)(nil), "pb.SweepInfo") + proto.RegisterType((*Input)(nil), "pb.Input") + proto.RegisterType((*Output)(nil), "pb.Output") + proto.RegisterType((*Signature)(nil), "pb.Signature") + proto.RegisterType((*CreateMultisigInfo)(nil), "pb.CreateMultisigInfo") + proto.RegisterType((*SignatureList)(nil), "pb.SignatureList") + proto.RegisterType((*MultisignInfo)(nil), "pb.MultisignInfo") + proto.RegisterType((*RawTx)(nil), "pb.RawTx") + proto.RegisterType((*EstimateFeeData)(nil), "pb.EstimateFeeData") + proto.RegisterEnum("pb.CoinType", CoinType_name, CoinType_value) + proto.RegisterEnum("pb.KeyPurpose", KeyPurpose_name, KeyPurpose_value) + proto.RegisterEnum("pb.FeeLevel", FeeLevel_name, FeeLevel_value) +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// APIClient is the client API for API service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type APIClient interface { + Stop(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) + CurrentAddress(ctx context.Context, in *KeySelection, opts ...grpc.CallOption) (*Address, error) + NewAddress(ctx context.Context, in *KeySelection, opts ...grpc.CallOption) (*Address, error) + ChainTip(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (*Height, error) + Balance(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (*Balances, error) + MasterPrivateKey(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (*Key, error) + MasterPublicKey(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (*Key, error) + HasKey(ctx context.Context, in *Address, opts ...grpc.CallOption) (*BoolResponse, error) + Params(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*NetParams, error) + Transactions(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (*TransactionList, error) + GetTransaction(ctx context.Context, in *Txid, opts ...grpc.CallOption) (*Tx, error) + GetFeePerByte(ctx context.Context, in *FeeLevelSelection, opts ...grpc.CallOption) (*FeePerByte, error) + Spend(ctx context.Context, in *SpendInfo, opts ...grpc.CallOption) (*Txid, error) + BumpFee(ctx context.Context, in *Txid, opts ...grpc.CallOption) (*Txid, error) + AddWatchedScript(ctx context.Context, in *Address, opts ...grpc.CallOption) (*Empty, error) + GetConfirmations(ctx context.Context, in *Txid, opts ...grpc.CallOption) (*Confirmations, error) + SweepAddress(ctx context.Context, in *SweepInfo, opts ...grpc.CallOption) (*Txid, error) + CreateMultisigSignature(ctx context.Context, in *CreateMultisigInfo, opts ...grpc.CallOption) (*SignatureList, error) + Multisign(ctx context.Context, in *MultisignInfo, opts ...grpc.CallOption) (*RawTx, error) + EstimateFee(ctx context.Context, in *EstimateFeeData, opts ...grpc.CallOption) (*Fee, error) + GetKey(ctx context.Context, in *Address, opts ...grpc.CallOption) (*Key, error) + ListKeys(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (*Keys, error) + ListAddresses(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (*Addresses, error) + WalletNotify(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (API_WalletNotifyClient, error) + DumpTables(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (API_DumpTablesClient, error) +} + +type aPIClient struct { + cc *grpc.ClientConn +} + +func NewAPIClient(cc *grpc.ClientConn) APIClient { + return &aPIClient{cc} +} + +func (c *aPIClient) Stop(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/pb.API/Stop", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) CurrentAddress(ctx context.Context, in *KeySelection, opts ...grpc.CallOption) (*Address, error) { + out := new(Address) + err := c.cc.Invoke(ctx, "/pb.API/CurrentAddress", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) NewAddress(ctx context.Context, in *KeySelection, opts ...grpc.CallOption) (*Address, error) { + out := new(Address) + err := c.cc.Invoke(ctx, "/pb.API/NewAddress", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) ChainTip(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (*Height, error) { + out := new(Height) + err := c.cc.Invoke(ctx, "/pb.API/ChainTip", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) Balance(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (*Balances, error) { + out := new(Balances) + err := c.cc.Invoke(ctx, "/pb.API/Balance", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) MasterPrivateKey(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (*Key, error) { + out := new(Key) + err := c.cc.Invoke(ctx, "/pb.API/MasterPrivateKey", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) MasterPublicKey(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (*Key, error) { + out := new(Key) + err := c.cc.Invoke(ctx, "/pb.API/MasterPublicKey", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) HasKey(ctx context.Context, in *Address, opts ...grpc.CallOption) (*BoolResponse, error) { + out := new(BoolResponse) + err := c.cc.Invoke(ctx, "/pb.API/HasKey", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) Params(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*NetParams, error) { + out := new(NetParams) + err := c.cc.Invoke(ctx, "/pb.API/Params", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) Transactions(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (*TransactionList, error) { + out := new(TransactionList) + err := c.cc.Invoke(ctx, "/pb.API/Transactions", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) GetTransaction(ctx context.Context, in *Txid, opts ...grpc.CallOption) (*Tx, error) { + out := new(Tx) + err := c.cc.Invoke(ctx, "/pb.API/GetTransaction", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) GetFeePerByte(ctx context.Context, in *FeeLevelSelection, opts ...grpc.CallOption) (*FeePerByte, error) { + out := new(FeePerByte) + err := c.cc.Invoke(ctx, "/pb.API/GetFeePerByte", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) Spend(ctx context.Context, in *SpendInfo, opts ...grpc.CallOption) (*Txid, error) { + out := new(Txid) + err := c.cc.Invoke(ctx, "/pb.API/Spend", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) BumpFee(ctx context.Context, in *Txid, opts ...grpc.CallOption) (*Txid, error) { + out := new(Txid) + err := c.cc.Invoke(ctx, "/pb.API/BumpFee", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) AddWatchedScript(ctx context.Context, in *Address, opts ...grpc.CallOption) (*Empty, error) { + out := new(Empty) + err := c.cc.Invoke(ctx, "/pb.API/AddWatchedScript", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) GetConfirmations(ctx context.Context, in *Txid, opts ...grpc.CallOption) (*Confirmations, error) { + out := new(Confirmations) + err := c.cc.Invoke(ctx, "/pb.API/GetConfirmations", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) SweepAddress(ctx context.Context, in *SweepInfo, opts ...grpc.CallOption) (*Txid, error) { + out := new(Txid) + err := c.cc.Invoke(ctx, "/pb.API/SweepAddress", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) CreateMultisigSignature(ctx context.Context, in *CreateMultisigInfo, opts ...grpc.CallOption) (*SignatureList, error) { + out := new(SignatureList) + err := c.cc.Invoke(ctx, "/pb.API/CreateMultisigSignature", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) Multisign(ctx context.Context, in *MultisignInfo, opts ...grpc.CallOption) (*RawTx, error) { + out := new(RawTx) + err := c.cc.Invoke(ctx, "/pb.API/Multisign", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) EstimateFee(ctx context.Context, in *EstimateFeeData, opts ...grpc.CallOption) (*Fee, error) { + out := new(Fee) + err := c.cc.Invoke(ctx, "/pb.API/EstimateFee", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) GetKey(ctx context.Context, in *Address, opts ...grpc.CallOption) (*Key, error) { + out := new(Key) + err := c.cc.Invoke(ctx, "/pb.API/GetKey", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) ListKeys(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (*Keys, error) { + out := new(Keys) + err := c.cc.Invoke(ctx, "/pb.API/ListKeys", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) ListAddresses(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (*Addresses, error) { + out := new(Addresses) + err := c.cc.Invoke(ctx, "/pb.API/ListAddresses", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) WalletNotify(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (API_WalletNotifyClient, error) { + stream, err := c.cc.NewStream(ctx, &_API_serviceDesc.Streams[0], "/pb.API/WalletNotify", opts...) + if err != nil { + return nil, err + } + x := &aPIWalletNotifyClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type API_WalletNotifyClient interface { + Recv() (*Tx, error) + grpc.ClientStream +} + +type aPIWalletNotifyClient struct { + grpc.ClientStream +} + +func (x *aPIWalletNotifyClient) Recv() (*Tx, error) { + m := new(Tx) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *aPIClient) DumpTables(ctx context.Context, in *CoinSelection, opts ...grpc.CallOption) (API_DumpTablesClient, error) { + stream, err := c.cc.NewStream(ctx, &_API_serviceDesc.Streams[1], "/pb.API/DumpTables", opts...) + if err != nil { + return nil, err + } + x := &aPIDumpTablesClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type API_DumpTablesClient interface { + Recv() (*Row, error) + grpc.ClientStream +} + +type aPIDumpTablesClient struct { + grpc.ClientStream +} + +func (x *aPIDumpTablesClient) Recv() (*Row, error) { + m := new(Row) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// APIServer is the server API for API service. +type APIServer interface { + Stop(context.Context, *Empty) (*Empty, error) + CurrentAddress(context.Context, *KeySelection) (*Address, error) + NewAddress(context.Context, *KeySelection) (*Address, error) + ChainTip(context.Context, *CoinSelection) (*Height, error) + Balance(context.Context, *CoinSelection) (*Balances, error) + MasterPrivateKey(context.Context, *CoinSelection) (*Key, error) + MasterPublicKey(context.Context, *CoinSelection) (*Key, error) + HasKey(context.Context, *Address) (*BoolResponse, error) + Params(context.Context, *Empty) (*NetParams, error) + Transactions(context.Context, *CoinSelection) (*TransactionList, error) + GetTransaction(context.Context, *Txid) (*Tx, error) + GetFeePerByte(context.Context, *FeeLevelSelection) (*FeePerByte, error) + Spend(context.Context, *SpendInfo) (*Txid, error) + BumpFee(context.Context, *Txid) (*Txid, error) + AddWatchedScript(context.Context, *Address) (*Empty, error) + GetConfirmations(context.Context, *Txid) (*Confirmations, error) + SweepAddress(context.Context, *SweepInfo) (*Txid, error) + CreateMultisigSignature(context.Context, *CreateMultisigInfo) (*SignatureList, error) + Multisign(context.Context, *MultisignInfo) (*RawTx, error) + EstimateFee(context.Context, *EstimateFeeData) (*Fee, error) + GetKey(context.Context, *Address) (*Key, error) + ListKeys(context.Context, *CoinSelection) (*Keys, error) + ListAddresses(context.Context, *CoinSelection) (*Addresses, error) + WalletNotify(*CoinSelection, API_WalletNotifyServer) error + DumpTables(*CoinSelection, API_DumpTablesServer) error +} + +func RegisterAPIServer(s *grpc.Server, srv APIServer) { + s.RegisterService(&_API_serviceDesc, srv) +} + +func _API_Stop_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).Stop(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/Stop", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).Stop(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_CurrentAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(KeySelection) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).CurrentAddress(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/CurrentAddress", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).CurrentAddress(ctx, req.(*KeySelection)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_NewAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(KeySelection) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).NewAddress(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/NewAddress", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).NewAddress(ctx, req.(*KeySelection)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_ChainTip_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CoinSelection) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).ChainTip(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/ChainTip", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).ChainTip(ctx, req.(*CoinSelection)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_Balance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CoinSelection) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).Balance(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/Balance", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).Balance(ctx, req.(*CoinSelection)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_MasterPrivateKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CoinSelection) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).MasterPrivateKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/MasterPrivateKey", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).MasterPrivateKey(ctx, req.(*CoinSelection)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_MasterPublicKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CoinSelection) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).MasterPublicKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/MasterPublicKey", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).MasterPublicKey(ctx, req.(*CoinSelection)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_HasKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Address) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).HasKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/HasKey", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).HasKey(ctx, req.(*Address)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_Params_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).Params(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/Params", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).Params(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_Transactions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CoinSelection) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).Transactions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/Transactions", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).Transactions(ctx, req.(*CoinSelection)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_GetTransaction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Txid) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).GetTransaction(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/GetTransaction", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).GetTransaction(ctx, req.(*Txid)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_GetFeePerByte_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FeeLevelSelection) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).GetFeePerByte(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/GetFeePerByte", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).GetFeePerByte(ctx, req.(*FeeLevelSelection)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_Spend_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SpendInfo) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).Spend(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/Spend", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).Spend(ctx, req.(*SpendInfo)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_BumpFee_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Txid) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).BumpFee(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/BumpFee", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).BumpFee(ctx, req.(*Txid)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_AddWatchedScript_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Address) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).AddWatchedScript(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/AddWatchedScript", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).AddWatchedScript(ctx, req.(*Address)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_GetConfirmations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Txid) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).GetConfirmations(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/GetConfirmations", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).GetConfirmations(ctx, req.(*Txid)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_SweepAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SweepInfo) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).SweepAddress(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/SweepAddress", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).SweepAddress(ctx, req.(*SweepInfo)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_CreateMultisigSignature_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateMultisigInfo) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).CreateMultisigSignature(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/CreateMultisigSignature", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).CreateMultisigSignature(ctx, req.(*CreateMultisigInfo)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_Multisign_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MultisignInfo) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).Multisign(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/Multisign", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).Multisign(ctx, req.(*MultisignInfo)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_EstimateFee_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EstimateFeeData) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).EstimateFee(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/EstimateFee", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).EstimateFee(ctx, req.(*EstimateFeeData)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_GetKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Address) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).GetKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/GetKey", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).GetKey(ctx, req.(*Address)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_ListKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CoinSelection) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).ListKeys(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/ListKeys", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).ListKeys(ctx, req.(*CoinSelection)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_ListAddresses_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CoinSelection) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).ListAddresses(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/pb.API/ListAddresses", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).ListAddresses(ctx, req.(*CoinSelection)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_WalletNotify_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(CoinSelection) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(APIServer).WalletNotify(m, &aPIWalletNotifyServer{stream}) +} + +type API_WalletNotifyServer interface { + Send(*Tx) error + grpc.ServerStream +} + +type aPIWalletNotifyServer struct { + grpc.ServerStream +} + +func (x *aPIWalletNotifyServer) Send(m *Tx) error { + return x.ServerStream.SendMsg(m) +} + +func _API_DumpTables_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(CoinSelection) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(APIServer).DumpTables(m, &aPIDumpTablesServer{stream}) +} + +type API_DumpTablesServer interface { + Send(*Row) error + grpc.ServerStream +} + +type aPIDumpTablesServer struct { + grpc.ServerStream +} + +func (x *aPIDumpTablesServer) Send(m *Row) error { + return x.ServerStream.SendMsg(m) +} + +var _API_serviceDesc = grpc.ServiceDesc{ + ServiceName: "pb.API", + HandlerType: (*APIServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Stop", + Handler: _API_Stop_Handler, + }, + { + MethodName: "CurrentAddress", + Handler: _API_CurrentAddress_Handler, + }, + { + MethodName: "NewAddress", + Handler: _API_NewAddress_Handler, + }, + { + MethodName: "ChainTip", + Handler: _API_ChainTip_Handler, + }, + { + MethodName: "Balance", + Handler: _API_Balance_Handler, + }, + { + MethodName: "MasterPrivateKey", + Handler: _API_MasterPrivateKey_Handler, + }, + { + MethodName: "MasterPublicKey", + Handler: _API_MasterPublicKey_Handler, + }, + { + MethodName: "HasKey", + Handler: _API_HasKey_Handler, + }, + { + MethodName: "Params", + Handler: _API_Params_Handler, + }, + { + MethodName: "Transactions", + Handler: _API_Transactions_Handler, + }, + { + MethodName: "GetTransaction", + Handler: _API_GetTransaction_Handler, + }, + { + MethodName: "GetFeePerByte", + Handler: _API_GetFeePerByte_Handler, + }, + { + MethodName: "Spend", + Handler: _API_Spend_Handler, + }, + { + MethodName: "BumpFee", + Handler: _API_BumpFee_Handler, + }, + { + MethodName: "AddWatchedScript", + Handler: _API_AddWatchedScript_Handler, + }, + { + MethodName: "GetConfirmations", + Handler: _API_GetConfirmations_Handler, + }, + { + MethodName: "SweepAddress", + Handler: _API_SweepAddress_Handler, + }, + { + MethodName: "CreateMultisigSignature", + Handler: _API_CreateMultisigSignature_Handler, + }, + { + MethodName: "Multisign", + Handler: _API_Multisign_Handler, + }, + { + MethodName: "EstimateFee", + Handler: _API_EstimateFee_Handler, + }, + { + MethodName: "GetKey", + Handler: _API_GetKey_Handler, + }, + { + MethodName: "ListKeys", + Handler: _API_ListKeys_Handler, + }, + { + MethodName: "ListAddresses", + Handler: _API_ListAddresses_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "WalletNotify", + Handler: _API_WalletNotify_Handler, + ServerStreams: true, + }, + { + StreamName: "DumpTables", + Handler: _API_DumpTables_Handler, + ServerStreams: true, + }, + }, + Metadata: "api.proto", +} + +func init() { proto.RegisterFile("api.proto", fileDescriptor_api_2ff753dddd9b028a) } + +var fileDescriptor_api_2ff753dddd9b028a = []byte{ + // 1449 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x57, 0xcd, 0x72, 0xdb, 0x46, + 0x12, 0xe6, 0x0f, 0xf8, 0x83, 0x16, 0x28, 0xd3, 0xb3, 0xbb, 0x36, 0x57, 0xeb, 0x92, 0xe9, 0x59, + 0x1f, 0x64, 0xad, 0x57, 0xb6, 0xe8, 0x4a, 0xca, 0x87, 0xa4, 0x5c, 0x12, 0xad, 0x1f, 0x46, 0x12, + 0xc5, 0x1a, 0xd1, 0xe5, 0xc4, 0x17, 0xd7, 0x90, 0x68, 0x49, 0x28, 0x93, 0x00, 0x0a, 0x18, 0x58, + 0xe4, 0x3d, 0x8f, 0x91, 0x5c, 0xf2, 0x04, 0x79, 0x8b, 0x3c, 0x41, 0xde, 0x27, 0x35, 0x83, 0x01, + 0x01, 0xc8, 0xb4, 0x2d, 0xe7, 0xe0, 0x5b, 0x4f, 0xf7, 0x87, 0x99, 0xee, 0xaf, 0x7b, 0x7a, 0x1a, + 0x60, 0x72, 0xdf, 0xd9, 0xf2, 0x03, 0x4f, 0x78, 0xa4, 0xe4, 0x8f, 0xd6, 0xee, 0x5f, 0x78, 0xde, + 0xc5, 0x04, 0x9f, 0x28, 0xcd, 0x28, 0x3a, 0x7f, 0x22, 0x9c, 0x29, 0x86, 0x82, 0x4f, 0xfd, 0x18, + 0x44, 0x6b, 0x50, 0xd9, 0x9b, 0xfa, 0x62, 0x4e, 0xb7, 0xa1, 0xd1, 0xf5, 0x1c, 0xf7, 0x0c, 0x27, + 0x38, 0x16, 0x8e, 0xe7, 0x92, 0x36, 0x18, 0x63, 0xcf, 0x71, 0x5b, 0xc5, 0x76, 0x71, 0x63, 0xb5, + 0x63, 0x6d, 0xf9, 0xa3, 0x2d, 0x09, 0x18, 0xce, 0x7d, 0x64, 0xca, 0x42, 0xff, 0x0d, 0x65, 0xe6, + 0x5d, 0x11, 0x02, 0x86, 0xcd, 0x05, 0x57, 0x40, 0x93, 0x29, 0x99, 0xbe, 0x01, 0xeb, 0x08, 0xe7, + 0x5f, 0xb0, 0x19, 0xd9, 0x80, 0x9a, 0x1f, 0x05, 0xbe, 0x17, 0x62, 0xab, 0xa4, 0x40, 0xab, 0x12, + 0x74, 0x84, 0xf3, 0x41, 0xac, 0x65, 0x89, 0x99, 0xbe, 0x80, 0xda, 0x8e, 0x6d, 0x07, 0x18, 0x86, + 0x37, 0xd8, 0x96, 0x80, 0xc1, 0x6d, 0x3b, 0x50, 0x7b, 0x9a, 0x4c, 0xc9, 0xb4, 0x0d, 0xd5, 0x43, + 0x74, 0x2e, 0x2e, 0x05, 0xb9, 0x03, 0xd5, 0x4b, 0x25, 0xa9, 0x1d, 0x1a, 0x4c, 0xaf, 0xe8, 0x0f, + 0x50, 0xdf, 0xe5, 0x13, 0xee, 0x8e, 0x31, 0x24, 0xf7, 0xc0, 0x1c, 0x7b, 0xee, 0xb9, 0x13, 0x4c, + 0xd1, 0x56, 0x30, 0x83, 0xa5, 0x0a, 0xd2, 0x86, 0x95, 0xc8, 0x4d, 0xed, 0x25, 0x65, 0xcf, 0xaa, + 0xe8, 0x5d, 0x28, 0x1f, 0xe1, 0x9c, 0x34, 0xa1, 0xfc, 0x0e, 0xe7, 0x9a, 0x24, 0x29, 0xd2, 0xff, + 0x82, 0x71, 0x84, 0xf3, 0x90, 0xfc, 0x07, 0x8c, 0x77, 0x38, 0x0f, 0x5b, 0xc5, 0x76, 0x79, 0x63, + 0xa5, 0x53, 0xd3, 0x61, 0x33, 0xa5, 0xa4, 0xdf, 0x82, 0xa9, 0x83, 0xc5, 0x90, 0x3c, 0x02, 0x93, + 0x27, 0x0b, 0x0d, 0x5f, 0x91, 0x70, 0x8d, 0x60, 0xa9, 0x95, 0x52, 0xb0, 0x76, 0x3d, 0x6f, 0xc2, + 0x30, 0xf4, 0x3d, 0x37, 0x44, 0xc9, 0xc3, 0xc8, 0xf3, 0x26, 0xea, 0xfc, 0x3a, 0x53, 0x32, 0xbd, + 0x0f, 0x66, 0x1f, 0xc5, 0x80, 0x07, 0x7c, 0x1a, 0x4a, 0x80, 0xcb, 0xa7, 0x98, 0x64, 0x51, 0xca, + 0xf4, 0x7b, 0xb8, 0x35, 0x0c, 0xb8, 0x1b, 0x72, 0x95, 0xc4, 0x63, 0x27, 0x14, 0x64, 0x13, 0x2c, + 0x91, 0xaa, 0x12, 0x2f, 0xaa, 0xd2, 0x8b, 0xe1, 0x8c, 0xe5, 0x6c, 0xf4, 0xf7, 0x22, 0x94, 0x86, + 0x33, 0xb9, 0xb3, 0x98, 0x39, 0x76, 0xb2, 0xb3, 0x94, 0xc9, 0x3f, 0xa1, 0xf2, 0x9e, 0x4f, 0xa2, + 0x38, 0xd7, 0x65, 0x16, 0x2f, 0x32, 0xe9, 0x28, 0xb7, 0x8b, 0x1b, 0x95, 0x24, 0x1d, 0xe4, 0x39, + 0x98, 0x8b, 0xba, 0x6d, 0x19, 0xed, 0xe2, 0xc6, 0x4a, 0x67, 0x6d, 0x2b, 0xae, 0xec, 0xad, 0xa4, + 0xb2, 0xb7, 0x86, 0x09, 0x82, 0xa5, 0x60, 0x99, 0xbc, 0x2b, 0x2e, 0xc6, 0x97, 0xa7, 0xee, 0x64, + 0xde, 0xaa, 0xa8, 0xd8, 0x53, 0x85, 0xcc, 0x49, 0xc0, 0xaf, 0x5a, 0xd5, 0x76, 0x71, 0xc3, 0x62, + 0x52, 0xa4, 0xdf, 0x81, 0x31, 0x94, 0xfe, 0xdd, 0xa8, 0xb0, 0x2e, 0x79, 0x78, 0x99, 0x14, 0x96, + 0x94, 0xe9, 0x5b, 0xb8, 0xbd, 0x8f, 0x78, 0x8c, 0xef, 0x71, 0xf2, 0x65, 0xa5, 0x5f, 0x3f, 0xd7, + 0x9f, 0xe9, 0xda, 0x57, 0xa8, 0x64, 0x2b, 0xb6, 0xb0, 0xd2, 0x75, 0x80, 0x7d, 0xc4, 0x01, 0x06, + 0xbb, 0x73, 0x81, 0xd2, 0xfd, 0x73, 0x44, 0x5d, 0x93, 0x52, 0x94, 0xb5, 0xb6, 0x8f, 0xcb, 0x0c, + 0xbf, 0x16, 0xc1, 0x3c, 0xf3, 0xd1, 0xb5, 0x7b, 0xee, 0xb9, 0x77, 0x03, 0x97, 0x5a, 0x50, 0xd3, + 0xb5, 0xa4, 0x03, 0x4c, 0x96, 0x32, 0x47, 0x7c, 0xea, 0x45, 0x6e, 0x9c, 0x23, 0x83, 0xe9, 0x55, + 0x2e, 0x08, 0xe3, 0x53, 0x41, 0x48, 0xe6, 0xa6, 0x38, 0xf5, 0x54, 0x3a, 0x4c, 0xa6, 0x64, 0xfa, + 0x8d, 0xec, 0x3e, 0xea, 0xc6, 0x70, 0x55, 0x3b, 0xe4, 0x21, 0x34, 0xc6, 0x59, 0x85, 0xbe, 0xa0, + 0x79, 0x25, 0xdd, 0x07, 0xe3, 0x95, 0x98, 0x79, 0x1f, 0x2b, 0x31, 0xc7, 0xb5, 0x71, 0xa6, 0x02, + 0x68, 0xb0, 0x78, 0x91, 0x16, 0x5e, 0xec, 0x7d, 0xbc, 0xa0, 0x7f, 0x48, 0x7a, 0xae, 0x10, 0xfd, + 0x1b, 0xd2, 0xb3, 0x0e, 0x95, 0x48, 0xcc, 0x3c, 0x49, 0x8e, 0x2c, 0xff, 0xba, 0x84, 0x48, 0x47, + 0x58, 0xac, 0xce, 0xd2, 0x57, 0xce, 0xd3, 0xa7, 0xdb, 0x80, 0xb1, 0x68, 0x03, 0x84, 0x82, 0x15, + 0xa0, 0x8d, 0x38, 0x3d, 0x1b, 0x07, 0x8e, 0x2f, 0x14, 0x2d, 0x16, 0xcb, 0xe9, 0x72, 0xe4, 0x56, + 0x3f, 0x59, 0x21, 0xdb, 0x50, 0xe9, 0xb9, 0x7e, 0x24, 0x6e, 0x4e, 0x09, 0xdd, 0x85, 0xea, 0x69, + 0x24, 0xe4, 0x37, 0x14, 0xac, 0x50, 0x1d, 0x38, 0x88, 0x46, 0x47, 0xba, 0x59, 0x59, 0x2c, 0xa7, + 0xcb, 0xdf, 0xdc, 0x05, 0x81, 0x2f, 0xc0, 0x3c, 0x73, 0x2e, 0x5c, 0x2e, 0xa2, 0x00, 0xd3, 0x63, + 0x8a, 0x59, 0xe6, 0xef, 0x81, 0x19, 0x26, 0x10, 0xf5, 0xb1, 0xc5, 0x52, 0x05, 0xfd, 0xb3, 0x08, + 0xa4, 0x1b, 0x20, 0x17, 0x78, 0x12, 0x4d, 0x84, 0x13, 0x3a, 0x17, 0x37, 0x4c, 0xc5, 0x03, 0xa8, + 0x3a, 0x32, 0xe0, 0x24, 0x17, 0xa6, 0xc4, 0x28, 0x0a, 0x98, 0x36, 0x90, 0x87, 0x50, 0xf3, 0x54, + 0x80, 0x32, 0x1b, 0x12, 0x03, 0x12, 0x13, 0xc7, 0xcc, 0x12, 0xd3, 0xdf, 0xcc, 0xcc, 0x3a, 0xc0, + 0xf9, 0xe2, 0x46, 0xaa, 0xdc, 0x18, 0x2c, 0xa3, 0xa1, 0x1d, 0x68, 0x2c, 0x88, 0x51, 0x0d, 0xf4, + 0x01, 0x18, 0xa1, 0x73, 0x91, 0x34, 0xce, 0x86, 0xf4, 0x64, 0x01, 0x60, 0xca, 0x44, 0x7f, 0x2b, + 0x41, 0x23, 0x61, 0xc1, 0xfd, 0xda, 0x34, 0xc4, 0xfe, 0x6d, 0xb7, 0x8c, 0x8f, 0xf9, 0xb7, 0xad, + 0x21, 0x9d, 0x56, 0xe5, 0x63, 0x90, 0xce, 0x07, 0xd4, 0x55, 0x3f, 0x4b, 0x5d, 0xed, 0x3a, 0x75, + 0xb2, 0x60, 0x46, 0x81, 0xc7, 0xed, 0x31, 0x0f, 0x45, 0xab, 0x1e, 0xf7, 0xee, 0x85, 0x82, 0xde, + 0x85, 0x0a, 0xe3, 0x57, 0xc3, 0x19, 0x59, 0x85, 0x92, 0x98, 0xe9, 0x52, 0x2d, 0x89, 0x19, 0xfd, + 0xa5, 0x08, 0xb7, 0xf6, 0x42, 0xe1, 0x4c, 0xb9, 0xc0, 0x7d, 0xc4, 0x97, 0x5c, 0xf0, 0xaf, 0xc9, + 0x5f, 0x3e, 0x2a, 0xe3, 0x7a, 0x54, 0x9b, 0x03, 0xa8, 0x27, 0x47, 0x93, 0x15, 0xa8, 0xed, 0xf6, + 0x86, 0xdd, 0xd3, 0x5e, 0xbf, 0x59, 0x20, 0x4d, 0xb0, 0xf4, 0xe2, 0x6d, 0x77, 0xe7, 0xec, 0xb0, + 0x59, 0x24, 0x26, 0x54, 0xde, 0x28, 0xb1, 0x44, 0x2c, 0xa8, 0x1f, 0xf7, 0x86, 0x7b, 0x0a, 0x5a, + 0x96, 0xab, 0xbd, 0xe1, 0xe1, 0x1e, 0xdb, 0x7b, 0x75, 0xd2, 0x34, 0x36, 0x37, 0x00, 0xd2, 0x31, + 0x49, 0xda, 0x7a, 0xfd, 0xe1, 0x1e, 0xeb, 0xef, 0x1c, 0x37, 0x0b, 0x0a, 0xf9, 0xa3, 0x5e, 0x15, + 0x37, 0x3b, 0x50, 0x4f, 0x5a, 0x86, 0xb2, 0x74, 0x4f, 0xfb, 0xa7, 0x27, 0xbd, 0x6e, 0xb3, 0x40, + 0x00, 0xaa, 0xfd, 0x53, 0x76, 0x22, 0x51, 0xd2, 0x32, 0x60, 0xbd, 0x53, 0xd6, 0x1b, 0xfe, 0xd4, + 0x2c, 0x75, 0x7e, 0x36, 0xa1, 0xbc, 0x33, 0xe8, 0x91, 0x75, 0x30, 0xce, 0x84, 0xe7, 0x13, 0x45, + 0x8c, 0x1a, 0x19, 0xd7, 0x52, 0x91, 0x16, 0xc8, 0x36, 0xac, 0x76, 0xa3, 0x20, 0x40, 0x57, 0x24, + 0xc3, 0x59, 0x53, 0x4f, 0x32, 0x8b, 0xa7, 0x70, 0x2d, 0x3b, 0xac, 0xd0, 0x02, 0xf9, 0x3f, 0x40, + 0x1f, 0xaf, 0x6e, 0x0c, 0xff, 0x1f, 0xd4, 0xbb, 0x97, 0xdc, 0x71, 0x87, 0x8e, 0x4f, 0x6e, 0x27, + 0x29, 0x4c, 0xd1, 0x2a, 0x1b, 0xf1, 0x5c, 0x47, 0x0b, 0xe4, 0x31, 0xd4, 0xf4, 0x04, 0xb7, 0x0c, + 0xab, 0x2a, 0x20, 0x99, 0xf0, 0x68, 0x81, 0x3c, 0x85, 0xe6, 0x09, 0x0f, 0x05, 0x06, 0x83, 0xc0, + 0x79, 0xcf, 0x05, 0xca, 0x46, 0xb7, 0xe4, 0xb3, 0x64, 0x36, 0xa3, 0x05, 0xf2, 0x04, 0x6e, 0xe9, + 0x2f, 0xa2, 0xd1, 0xc4, 0x19, 0x7f, 0xfe, 0x83, 0x47, 0x50, 0x3d, 0xe4, 0xa1, 0xc4, 0x65, 0xc3, + 0x5a, 0x53, 0x51, 0x67, 0x27, 0x35, 0x5a, 0x20, 0x0f, 0xa1, 0xaa, 0x87, 0xb2, 0x0c, 0xd9, 0xea, + 0x9a, 0x2d, 0xc6, 0x35, 0x5a, 0x20, 0xcf, 0xc1, 0xca, 0x0c, 0x67, 0xe1, 0xb2, 0xe3, 0xff, 0xa1, + 0xc6, 0xb2, 0xfc, 0x04, 0xa7, 0xf6, 0x5f, 0x3d, 0x40, 0x91, 0xd1, 0x93, 0x7a, 0x3c, 0xbf, 0x39, + 0xf6, 0x9a, 0x9e, 0xe4, 0xd4, 0xfe, 0x8d, 0x03, 0x14, 0x99, 0x71, 0xe3, 0x5f, 0xd9, 0x27, 0x27, + 0x3d, 0x64, 0x55, 0xab, 0x93, 0x8e, 0x57, 0x20, 0x14, 0x2a, 0x6a, 0xd6, 0x20, 0x71, 0x6b, 0x48, + 0xc6, 0x8e, 0xb5, 0xc5, 0x29, 0xb4, 0x40, 0xee, 0x43, 0x6d, 0x37, 0x9a, 0xfa, 0x72, 0x5a, 0x49, + 0x0f, 0xcf, 0x02, 0x1e, 0x43, 0x73, 0xc7, 0xb6, 0x5f, 0xcb, 0x59, 0x0d, 0x6d, 0xdd, 0x31, 0x72, + 0xcc, 0x5d, 0xab, 0xbe, 0xe6, 0x01, 0x8a, 0xfc, 0x08, 0x91, 0xee, 0xab, 0xa9, 0xc9, 0x4e, 0x0e, + 0x32, 0x21, 0x96, 0x7a, 0xf2, 0x93, 0xfa, 0x8b, 0x9d, 0x4d, 0x86, 0x80, 0x9c, 0x2f, 0xfb, 0x70, + 0x37, 0xff, 0x36, 0xa5, 0x6f, 0xdd, 0x1d, 0xb5, 0xf5, 0x07, 0x0f, 0x57, 0x7c, 0x64, 0xae, 0xf3, + 0xab, 0x0a, 0x36, 0x17, 0x7d, 0x3d, 0xce, 0x57, 0xae, 0xcd, 0xc7, 0x21, 0xa9, 0xae, 0xa6, 0x6e, + 0xc7, 0x4a, 0xa6, 0x8d, 0x11, 0x95, 0xcb, 0x6b, 0x7d, 0x2d, 0xae, 0xaf, 0x7d, 0x94, 0xa4, 0xb7, + 0xa1, 0x7a, 0x80, 0xe2, 0x83, 0xfa, 0xca, 0x55, 0x60, 0x5d, 0xfa, 0xa1, 0xfe, 0x39, 0x96, 0x14, + 0x4b, 0x5d, 0x23, 0x25, 0x37, 0xcf, 0xa0, 0x21, 0xa1, 0xe9, 0x9f, 0xc7, 0x12, 0x7c, 0x23, 0x73, + 0x0c, 0xc6, 0xd7, 0xd9, 0x7a, 0xcd, 0x27, 0x13, 0x14, 0x7d, 0x4f, 0x38, 0xe7, 0x4b, 0xef, 0xc3, + 0xa2, 0xba, 0x9e, 0x16, 0xc9, 0x63, 0x80, 0x97, 0xd1, 0xd4, 0x1f, 0xf2, 0xd1, 0x64, 0xf9, 0x01, + 0xca, 0x75, 0xe6, 0x5d, 0x49, 0xf4, 0xa8, 0xaa, 0xe6, 0xfc, 0x67, 0x7f, 0x05, 0x00, 0x00, 0xff, + 0xff, 0x87, 0x69, 0x86, 0xca, 0xe0, 0x0e, 0x00, 0x00, +} diff --git a/api/pb/api.proto b/api/pb/api.proto new file mode 100644 index 0000000..791b442 --- /dev/null +++ b/api/pb/api.proto @@ -0,0 +1,209 @@ +syntax = "proto3"; + +package pb; + +import "google/protobuf/timestamp.proto"; + +service API { + rpc Stop (Empty) returns (Empty) {} + rpc CurrentAddress (KeySelection) returns (Address) {} + rpc NewAddress (KeySelection) returns (Address) {} + rpc ChainTip (CoinSelection) returns (Height) {} + rpc Balance (CoinSelection) returns (Balances) {} + rpc MasterPrivateKey (CoinSelection) returns (Key) {} + rpc MasterPublicKey (CoinSelection) returns (Key) {} + rpc HasKey (Address) returns (BoolResponse) {} + rpc Params (Empty) returns (NetParams) {} + rpc Transactions (CoinSelection) returns (TransactionList) {} + rpc GetTransaction (Txid) returns (Tx) {} + rpc GetFeePerByte (FeeLevelSelection) returns (FeePerByte) {} + rpc Spend (SpendInfo) returns (Txid) {} + rpc BumpFee (Txid) returns (Txid) {} + rpc AddWatchedScript (Address) returns (Empty) {} + rpc GetConfirmations (Txid) returns (Confirmations) {} + rpc SweepAddress (SweepInfo) returns (Txid) {} + rpc CreateMultisigSignature (CreateMultisigInfo) returns (SignatureList) {} + rpc Multisign (MultisignInfo) returns (RawTx) {} + rpc EstimateFee (EstimateFeeData) returns (Fee) {} + rpc GetKey (Address) returns (Key) {} + rpc ListKeys (CoinSelection) returns (Keys) {} + rpc ListAddresses (CoinSelection) returns (Addresses) {} + rpc WalletNotify (CoinSelection) returns (stream Tx) {} + rpc DumpTables (CoinSelection) returns (stream Row) {} +} + +enum CoinType { + BITCOIN = 0; + BITCOIN_CASH = 1; + ZCASH = 2; + LITECOIN = 3; + ETHEREUM = 4; +} + +message Empty {} + +message CoinSelection { + CoinType coin = 1; +} + +enum KeyPurpose { + INTERNAL = 0; + EXTERNAL = 1; +} + +message Row { + string data = 1; +} + +message KeySelection { + CoinType coin = 1; + KeyPurpose purpose = 2; +} + +message Address { + CoinType coin = 1; + string addr = 2; +} + +message Height { + uint32 height = 1; +} + +message Balances { + uint64 confirmed = 1; + uint64 unconfirmed = 2; +} + +message Key { + string key = 1; +} + +message Keys { + repeated Key keys = 1; +} + +message Addresses { + repeated Address addresses = 1; +} + +message BoolResponse { + bool bool = 1; +} + +message NetParams { + string name = 1; +} + +message TransactionList { + repeated Tx transactions = 1; +} + +message Tx { + string txid = 1; + int64 value = 2; + int32 height = 3; + google.protobuf.Timestamp timestamp = 4; + bool watchOnly = 5; + bytes raw = 6; +} + +message Txid { + CoinType coin = 1; + string hash = 2; +} + +enum FeeLevel { + ECONOMIC = 0; + NORMAL = 1; + PRIORITY = 2; +} + +message FeeLevelSelection { + CoinType coin = 1; + FeeLevel feeLevel = 2; +} + +message FeePerByte { + uint64 fee = 1; +} + +message Fee { + uint64 fee = 1; +} + +message SpendInfo { + CoinType coin = 1; + string address = 2; + uint64 amount = 3; + FeeLevel feeLevel = 4; + string memo = 5; +} + +message Confirmations { + uint32 confirmations = 1; +} + +message Utxo { + string txid = 1; + uint32 index = 2; + uint64 value = 3; +} + +message SweepInfo { + CoinType coin = 1; + repeated Utxo utxos = 2; + string address = 3; + string key = 4; + bytes redeemScript = 5; + FeeLevel feeLevel = 6; +} + +message Input { + string txid = 1; + uint32 index = 2; +} + +message Output { + bytes scriptPubKey = 1; + uint64 value = 2; +} + +message Signature { + uint32 index = 1; + bytes signature = 2; +} + +message CreateMultisigInfo { + CoinType coin = 1; + repeated Input inputs = 2; + repeated Output outputs = 3; + string key = 4; + bytes redeemScript = 5; + uint64 feePerByte = 6; +} + +message SignatureList { + repeated Signature sigs = 1; +} + +message MultisignInfo { + CoinType coin = 1; + repeated Input inputs = 2; + repeated Output outputs = 3; + repeated Signature sig1 = 4; + repeated Signature sig2 = 5; + bytes redeemScript = 6; + uint64 feePerByte = 7; + bool broadcast = 8; +} + +message RawTx { + bytes tx = 1; +} + +message EstimateFeeData { + CoinType coin = 1; + repeated Input inputs = 2; + repeated Output outputs = 3; + uint64 feePerByte = 4; +} \ No newline at end of file diff --git a/api/rpc.go b/api/rpc.go new file mode 100644 index 0000000..02586cb --- /dev/null +++ b/api/rpc.go @@ -0,0 +1,281 @@ +package api + +import ( + "errors" + "net" + + "github.com/OpenBazaar/multiwallet" + "github.com/OpenBazaar/multiwallet/api/pb" + "github.com/OpenBazaar/multiwallet/bitcoin" + "github.com/OpenBazaar/multiwallet/bitcoincash" + "github.com/OpenBazaar/multiwallet/litecoin" + "github.com/OpenBazaar/multiwallet/zcash" + "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcutil" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +const Addr = "127.0.0.1:8234" + +type server struct { + w multiwallet.MultiWallet +} + +func ServeAPI(w multiwallet.MultiWallet) error { + lis, err := net.Listen("tcp", Addr) + if err != nil { + return err + } + s := grpc.NewServer() + pb.RegisterAPIServer(s, &server{w}) + reflection.Register(s) + if err := s.Serve(lis); err != nil { + return err + } + return nil +} + +func coinType(coinType pb.CoinType) wallet.CoinType { + switch coinType { + case pb.CoinType_BITCOIN: + return wallet.Bitcoin + case pb.CoinType_BITCOIN_CASH: + return wallet.BitcoinCash + case pb.CoinType_ZCASH: + return wallet.Zcash + case pb.CoinType_LITECOIN: + return wallet.Litecoin + default: + return wallet.Bitcoin + } +} + +func (s *server) Stop(ctx context.Context, in *pb.Empty) (*pb.Empty, error) { + // Stub + return &pb.Empty{}, nil +} + +func (s *server) CurrentAddress(ctx context.Context, in *pb.KeySelection) (*pb.Address, error) { + var purpose wallet.KeyPurpose + if in.Purpose == pb.KeyPurpose_INTERNAL { + purpose = wallet.INTERNAL + } else if in.Purpose == pb.KeyPurpose_EXTERNAL { + purpose = wallet.EXTERNAL + } else { + return nil, errors.New("Unknown key purpose") + } + ct := coinType(in.Coin) + wal, err := s.w.WalletForCurrencyCode(ct.CurrencyCode()) + if err != nil { + return nil, err + } + addr := wal.CurrentAddress(purpose) + return &pb.Address{Coin: in.Coin, Addr: addr.String()}, nil +} + +func (s *server) NewAddress(ctx context.Context, in *pb.KeySelection) (*pb.Address, error) { + var purpose wallet.KeyPurpose + if in.Purpose == pb.KeyPurpose_INTERNAL { + purpose = wallet.INTERNAL + } else if in.Purpose == pb.KeyPurpose_EXTERNAL { + purpose = wallet.EXTERNAL + } else { + return nil, errors.New("Unknown key purpose") + } + ct := coinType(in.Coin) + wal, err := s.w.WalletForCurrencyCode(ct.CurrencyCode()) + if err != nil { + return nil, err + } + addr := wal.NewAddress(purpose) + return &pb.Address{Coin: in.Coin, Addr: addr.String()}, nil +} + +func (s *server) ChainTip(ctx context.Context, in *pb.CoinSelection) (*pb.Height, error) { + ct := coinType(in.Coin) + wal, err := s.w.WalletForCurrencyCode(ct.CurrencyCode()) + if err != nil { + return nil, err + } + h, _ := wal.ChainTip() + return &pb.Height{Height: h}, nil +} + +func (s *server) Balance(ctx context.Context, in *pb.CoinSelection) (*pb.Balances, error) { + ct := coinType(in.Coin) + wal, err := s.w.WalletForCurrencyCode(ct.CurrencyCode()) + if err != nil { + return nil, err + } + c, u := wal.Balance() + return &pb.Balances{Confirmed: uint64(c), Unconfirmed: uint64(u)}, nil +} + +func (s *server) MasterPrivateKey(ctx context.Context, in *pb.CoinSelection) (*pb.Key, error) { + // Stub + return &pb.Key{Key: ""}, nil +} + +func (s *server) MasterPublicKey(ctx context.Context, in *pb.CoinSelection) (*pb.Key, error) { + // Stub + return &pb.Key{Key: ""}, nil +} + +func (s *server) Params(ctx context.Context, in *pb.Empty) (*pb.NetParams, error) { + // Stub + return &pb.NetParams{Name: ""}, nil +} + +func (s *server) HasKey(ctx context.Context, in *pb.Address) (*pb.BoolResponse, error) { + // Stub + return &pb.BoolResponse{Bool: false}, nil +} + +func (s *server) Transactions(ctx context.Context, in *pb.CoinSelection) (*pb.TransactionList, error) { + // Stub + var list []*pb.Tx + return &pb.TransactionList{Transactions: list}, nil +} + +func (s *server) GetTransaction(ctx context.Context, in *pb.Txid) (*pb.Tx, error) { + // Stub + respTx := &pb.Tx{} + return respTx, nil +} + +func (s *server) GetFeePerByte(ctx context.Context, in *pb.FeeLevelSelection) (*pb.FeePerByte, error) { + // Stub + return &pb.FeePerByte{Fee: 0}, nil +} + +func (s *server) Spend(ctx context.Context, in *pb.SpendInfo) (*pb.Txid, error) { + var addr btcutil.Address + var err error + + ct := coinType(in.Coin) + wal, err := s.w.WalletForCurrencyCode(ct.CurrencyCode()) + if err != nil { + return nil, err + } + addr, err = wal.DecodeAddress(in.Address) + if err != nil { + return nil, err + } + + var feeLevel wallet.FeeLevel + switch in.FeeLevel { + case pb.FeeLevel_PRIORITY: + feeLevel = wallet.PRIOIRTY + case pb.FeeLevel_NORMAL: + feeLevel = wallet.NORMAL + case pb.FeeLevel_ECONOMIC: + feeLevel = wallet.ECONOMIC + default: + feeLevel = wallet.NORMAL + } + txid, err := wal.Spend(int64(in.Amount), addr, feeLevel, "", false) + if err != nil { + return nil, err + } + return &pb.Txid{Coin: in.Coin, Hash: txid.String()}, nil +} + +func (s *server) BumpFee(ctx context.Context, in *pb.Txid) (*pb.Txid, error) { + // Stub + return &pb.Txid{Coin: in.Coin, Hash: ""}, nil +} + +func (s *server) AddWatchedScript(ctx context.Context, in *pb.Address) (*pb.Empty, error) { + return nil, nil +} + +func (s *server) GetConfirmations(ctx context.Context, in *pb.Txid) (*pb.Confirmations, error) { + // Stub + return &pb.Confirmations{Confirmations: 0}, nil +} + +func (s *server) SweepAddress(ctx context.Context, in *pb.SweepInfo) (*pb.Txid, error) { + // Stub + return &pb.Txid{Coin: in.Coin, Hash: ""}, nil +} + +func (s *server) CreateMultisigSignature(ctx context.Context, in *pb.CreateMultisigInfo) (*pb.SignatureList, error) { + var retSigs []*pb.Signature + return &pb.SignatureList{Sigs: retSigs}, nil +} + +func (s *server) Multisign(ctx context.Context, in *pb.MultisignInfo) (*pb.RawTx, error) { + // Stub + return &pb.RawTx{Tx: []byte{}}, nil +} + +func (s *server) EstimateFee(ctx context.Context, in *pb.EstimateFeeData) (*pb.Fee, error) { + // Stub + return &pb.Fee{Fee: 0}, nil +} + +func (s *server) WalletNotify(in *pb.CoinSelection, stream pb.API_WalletNotifyServer) error { + // Stub + return nil +} + +func (s *server) GetKey(ctx context.Context, in *pb.Address) (*pb.Key, error) { + // Stub + return &pb.Key{Key: ""}, nil +} + +func (s *server) ListAddresses(ctx context.Context, in *pb.CoinSelection) (*pb.Addresses, error) { + // Stub + var list []*pb.Address + return &pb.Addresses{Addresses: list}, nil +} + +func (s *server) ListKeys(ctx context.Context, in *pb.CoinSelection) (*pb.Keys, error) { + // Stub + var list []*pb.Key + return &pb.Keys{Keys: list}, nil +} + +type HeaderWriter struct { + stream pb.API_DumpTablesServer +} + +func (h *HeaderWriter) Write(p []byte) (n int, err error) { + hdr := &pb.Row{Data: string(p)} + if err := h.stream.Send(hdr); err != nil { + return 0, err + } + return 0, nil +} + +func (s *server) DumpTables(in *pb.CoinSelection, stream pb.API_DumpTablesServer) error { + writer := HeaderWriter{stream} + ct := coinType(in.Coin) + wal, err := s.w.WalletForCurrencyCode(ct.CurrencyCode()) + if err != nil { + return err + } + bitcoinWallet, ok := wal.(*bitcoin.BitcoinWallet) + if ok { + bitcoinWallet.DumpTables(&writer) + return nil + } + bitcoincashWallet, ok := wal.(*bitcoincash.BitcoinCashWallet) + if ok { + bitcoincashWallet.DumpTables(&writer) + return nil + } + litecoinWallet, ok := wal.(*litecoin.LitecoinWallet) + if ok { + litecoinWallet.DumpTables(&writer) + return nil + } + zcashWallet, ok := wal.(*zcash.ZCashWallet) + if ok { + zcashWallet.DumpTables(&writer) + return nil + } + return nil +} diff --git a/bitcoin/sign.go b/bitcoin/sign.go new file mode 100644 index 0000000..361714b --- /dev/null +++ b/bitcoin/sign.go @@ -0,0 +1,670 @@ +package bitcoin + +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/btcsuite/btcwallet/wallet/txrules" + + "github.com/OpenBazaar/multiwallet/util" +) + +func (w *BitcoinWallet) buildTx(amount int64, addr btc.Address, feeLevel wi.FeeLevel, optionalOutput *wire.TxOut) (*wire.MsgTx, error) { + // Check for dust + script, _ := txscript.PayToAddrScript(addr) + if txrules.IsDustAmount(btc.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 := key.Address(w.params) + 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 := txscript.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) { + addrStr := addr.EncodeAddress() + wif := additionalKeysByAddress[addrStr] + 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 *BitcoinWallet) 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 := txscript.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(btc.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(feePerKb, estimatedSize) + + for { + inputAmount, inputs, _, scripts, err := fetchInputs(targetAmount + targetFee) + if err != nil { + return nil, err + } + if inputAmount < targetAmount+targetFee { + return nil, errors.New("insufficient funds available to construct transaction") + } + + maxSignedSize := EstimateSerializeSize(len(inputs), outputs, true, P2PKH) + maxRequiredFee := txrules.FeeForSerializeSize(feePerKb, maxSignedSize) + remainingAmount := inputAmount - targetAmount + if remainingAmount < maxRequiredFee { + targetFee = maxRequiredFee + continue + } + + unsignedTransaction := &wire.MsgTx{ + Version: wire.TxVersion, + TxIn: inputs, + TxOut: outputs, + LockTime: 0, + } + changeIndex := -1 + changeAmount := inputAmount - targetAmount - maxRequiredFee + if changeAmount != 0 && !txrules.IsDustAmount(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 *BitcoinWallet) 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 *BitcoinWallet) 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 := txscript.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 := txscript.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 *BitcoinWallet) 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 := txscript.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 *BitcoinWallet) 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 := txscript.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 *BitcoinWallet) 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 = btc.NewAddressWitnessScriptHash(witnessProgram[:], w.params) + if err != nil { + return nil, nil, err + } + return addr, redeemScript, nil +} + +func (w *BitcoinWallet) 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 := btc.DecodeAddress("bc1qxtq7ha2l5qg70atpwp3fus84fx3w0v2w4r2my7gt89ll3w0vnlgspu349h", &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/bitcoin/sign_test.go b/bitcoin/sign_test.go new file mode 100644 index 0000000..394bb14 --- /dev/null +++ b/bitcoin/sign_test.go @@ -0,0 +1,692 @@ +package bitcoin + +import ( + "bytes" + "encoding/hex" + "github.com/OpenBazaar/multiwallet/util" + "testing" + "time" + + "github.com/OpenBazaar/multiwallet/cache" + "github.com/OpenBazaar/multiwallet/datastore" + "github.com/OpenBazaar/multiwallet/keys" + "github.com/OpenBazaar/multiwallet/model/mock" + "github.com/OpenBazaar/multiwallet/service" + "github.com/OpenBazaar/spvwallet" + "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" +) + +type FeeResponse struct { + Priority int `json:"priority"` + Normal int `json:"normal"` + Economic int `json:"economic"` +} + +func newMockWallet() (*BitcoinWallet, error) { + mockDb := datastore.NewMockMultiwalletDatastore() + + db, err := mockDb.GetDatastoreForWallet(wallet.Bitcoin) + if err != nil { + return nil, err + } + + params := &chaincfg.MainNetParams + + seed, err := hex.DecodeString("16c034c59522326867593487c03a8f9615fb248406dd0d4ffb3a6b976a248403") + if err != nil { + return nil, err + } + master, err := hdkeychain.NewMaster(seed, params) + if err != nil { + return nil, err + } + km, err := keys.NewKeyManager(db.Keys(), params, master, wallet.Bitcoin, keyToAddress) + if err != nil { + return nil, err + } + + fp := spvwallet.NewFeeProvider(2000, 300, 200, 100, "", nil) + + bw := &BitcoinWallet{ + params: params, + km: km, + db: db, + fp: fp, + } + cli := mock.NewMockApiClient(bw.AddressToScript) + ws, err := service.NewWalletService(db, km, cli, params, wallet.Bitcoin, cache.NewMockCacher()) + if err != nil { + return nil, err + } + + bw.client = cli + bw.ws = ws + return bw, nil +} + +func TestWalletService_VerifyWatchScriptFilter(t *testing.T) { + // Verify that AddWatchedAddress should never add a script which already represents a key from its own wallet + w, err := newMockWallet() + if err != nil { + t.Fatal(err) + } + keys := w.km.GetKeys() + + addr, err := w.km.KeyToAddress(keys[0]) + if err != nil { + t.Fatal(err) + } + err = w.AddWatchedAddresses(addr) + if err != nil { + t.Fatal(err) + } + + watchScripts, err := w.db.WatchedScripts().GetAll() + if err != nil { + t.Fatal(err) + } + + if len(watchScripts) != 0 { + t.Error("Put watched scripts fails on key manager owned key") + } +} + +func TestWalletService_VerifyWatchScriptPut(t *testing.T) { + // Verify that AddWatchedAddress should add a script which does not represent a key from its own wallet + w, err := newMockWallet() + if err != nil { + t.Fatal(err) + } + + addr, err := w.DecodeAddress("16E4rWXEDcDRfmuMmJ6tTvL2uwHNgWF4yR") + if err != nil { + t.Fatal(err) + } + + err = w.AddWatchedAddresses(addr) + if err != nil { + t.Fatal(err) + } + + watchScripts, err := w.db.WatchedScripts().GetAll() + if err != nil { + t.Fatal(err) + } + + if len(watchScripts) == 0 { + t.Error("Put watched scripts fails on non-key manager owned key") + } +} + +func waitForTxnSync(t *testing.T, txnStore wallet.Txns) { + // Look for a known txn, this sucks a bit. It would be better to check if the + // number of stored txns matched the expected, but not all the mock + // transactions are relevant, so the numbers don't add up. + // Even better would be for the wallet to signal that the initial sync was + // done. + lastTxn := mock.MockTransactions[len(mock.MockTransactions)-2] + txHash, err := chainhash.NewHashFromStr(lastTxn.Txid) + if err != nil { + t.Fatal(err) + } + + for i := 0; i < 100; i++ { + if _, err := txnStore.Get(*txHash); err == nil { + return + } + time.Sleep(100 * time.Millisecond) + } + t.Fatal("timeout waiting for wallet to sync transactions") +} + +func TestBitcoinWallet_buildTx(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + time.Sleep(time.Second / 2) + + waitForTxnSync(t, w.db.Txns()) + addr, err := w.DecodeAddress("1AhsMpyyyVyPZ9KDUgwsX3zTDJWWSsRo4f") + if err != nil { + t.Error(err) + } + + // Test build normal tx + tx, err := w.buildTx(1500000, addr, wallet.NORMAL, nil) + if err != nil { + t.Error(err) + } + if !containsOutput(tx, addr) { + t.Error("Built tx does not contain the requested output") + } + if !validInputs(tx, w.db) { + t.Error("Built tx does not contain valid inputs") + } + if !validChangeAddress(tx, w.db, w.params) { + t.Error("Built tx does not contain a valid change output") + } + + // Insuffient funds + _, err = w.buildTx(1000000000, addr, wallet.NORMAL, nil) + if err != wallet.ErrorInsuffientFunds { + t.Error("Failed to throw insuffient funds error") + } + + // Dust + _, err = w.buildTx(1, addr, wallet.NORMAL, nil) + if err != wallet.ErrorDustAmount { + t.Error("Failed to throw dust error") + } +} + +func TestBitcoinWallet_buildSpendAllTx(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + time.Sleep(time.Second / 2) + + waitForTxnSync(t, w.db.Txns()) + addr, err := w.DecodeAddress("1AhsMpyyyVyPZ9KDUgwsX3zTDJWWSsRo4f") + if err != nil { + t.Error(err) + } + + // Test build spendAll tx + tx, err := w.buildSpendAllTx(addr, wallet.NORMAL) + if err != nil { + t.Error(err) + } + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Fatal(err) + } + spendableUtxos := 0 + for _, u := range utxos { + if !u.WatchOnly { + spendableUtxos++ + } + } + if len(tx.TxIn) != spendableUtxos { + t.Error("Built tx does not spend all available utxos") + } + if !containsOutput(tx, addr) { + t.Error("Built tx does not contain the requested output") + } + if !validInputs(tx, w.db) { + t.Error("Built tx does not contain valid inputs") + } + if len(tx.TxOut) != 1 { + t.Error("Built tx should only have one output") + } + + // Verify the signatures on each input using the scripting engine + for i, in := range tx.TxIn { + var prevScript []byte + for _, u := range utxos { + if util.OutPointsEqual(u.Op, in.PreviousOutPoint) { + prevScript = u.ScriptPubkey + break + } + } + vm, err := txscript.NewEngine(prevScript, tx, i, txscript.StandardVerifyFlags, nil, nil, 0) + if err != nil { + t.Fatal(err) + } + if err := vm.Execute(); err != nil { + t.Error(err) + } + } +} + +func containsOutput(tx *wire.MsgTx, addr btcutil.Address) bool { + for _, o := range tx.TxOut { + script, _ := txscript.PayToAddrScript(addr) + if bytes.Equal(script, o.PkScript) { + return true + } + } + return false +} + +func validInputs(tx *wire.MsgTx, db wallet.Datastore) bool { + utxos, _ := db.Utxos().GetAll() + uMap := make(map[wire.OutPoint]bool) + for _, u := range utxos { + uMap[u.Op] = true + } + for _, in := range tx.TxIn { + if !uMap[in.PreviousOutPoint] { + return false + } + } + return true +} + +func validChangeAddress(tx *wire.MsgTx, db wallet.Datastore, params *chaincfg.Params) bool { + for _, out := range tx.TxOut { + _, addrs, _, err := txscript.ExtractPkScriptAddrs(out.PkScript, params) + if err != nil { + continue + } + if len(addrs) == 0 { + continue + } + _, err = db.Keys().GetPathForKey(addrs[0].ScriptAddress()) + if err == nil { + return true + } + } + return false +} + +func TestBitcoinWallet_GenerateMultisigScript(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + key2, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + key3, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + keys := []hdkeychain.ExtendedKey{*key1, *key2, *key3} + + // test without timeout + addr, redeemScript, err := w.generateMultisigScript(keys, 2, 0, nil) + if err != nil { + t.Error(err) + } + if addr.String() != "bc1q7ckk79my7g0jltxtae34yk7e6nzth40dy6j6a67c96mhh6ue0hyqtmf66p" { + t.Error("Returned invalid address") + } + + rs := "52" + // OP_2 + "21" + // OP_PUSHDATA(33) + "03c157f2a7c178430972263232c9306110090c50b44d4e906ecd6d377eec89a53c" + // pubkey1 + "21" + // OP_PUSHDATA(33) + "0205b02b9dbe570f36d1c12e3100e55586b2b9dc61d6778c1d24a8eaca03625e7e" + // pubkey2 + "21" + // OP_PUSHDATA(33) + "030c83b025cd6bdd8c06e93a2b953b821b4a8c29da211335048d7dc3389706d7e8" + // pubkey3 + "53" + // OP_3 + "ae" // OP_CHECKMULTISIG + rsBytes, err := hex.DecodeString(rs) + if err != nil { + t.Error(err) + } + if !bytes.Equal(rsBytes, redeemScript) { + t.Error("Returned invalid redeem script") + } + + // test with timeout + key4, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + addr, redeemScript, err = w.generateMultisigScript(keys, 2, time.Hour*10, key4) + if err != nil { + t.Error(err) + } + if addr.String() != "bc1qlx7djex36u6ttf7kvqk0uzhvyu0ug3t695r4xjqz0s7pl4kkyzmqwxp2mc" { + t.Error("Returned invalid address") + } + + rs = "63" + // OP_IF + "52" + // OP_2 + "21" + // OP_PUSHDATA(33) + "03c157f2a7c178430972263232c9306110090c50b44d4e906ecd6d377eec89a53c" + // pubkey1 + "21" + // OP_PUSHDATA(33) + "0205b02b9dbe570f36d1c12e3100e55586b2b9dc61d6778c1d24a8eaca03625e7e" + // pubkey2 + "21" + // OP_PUSHDATA(33) + "030c83b025cd6bdd8c06e93a2b953b821b4a8c29da211335048d7dc3389706d7e8" + // pubkey3 + "53" + // OP_3 + "ae" + // OP_CHECKMULTISIG + "67" + // OP_ELSE + "01" + // OP_PUSHDATA(1) + "3c" + // 60 blocks + "b2" + // OP_CHECKSEQUENCEVERIFY + "75" + // OP_DROP + "21" + // OP_PUSHDATA(33) + "02c2902e25457d7780471890b957fbbc3d80af94e3bba9a6b89fd28f618bf4147e" + // timeout pubkey + "ac" + // OP_CHECKSIG + "68" // OP_ENDIF + rsBytes, err = hex.DecodeString(rs) + if err != nil { + t.Error(err) + } + if !bytes.Equal(rsBytes, redeemScript) { + t.Error("Returned invalid redeem script") + } +} + +func TestBitcoinWallet_newUnsignedTransaction(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + waitForTxnSync(t, w.db.Txns()) + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + addr, err := w.DecodeAddress("1AhsMpyyyVyPZ9KDUgwsX3zTDJWWSsRo4f") + if err != nil { + t.Error(err) + } + + script, err := txscript.PayToAddrScript(addr) + if err != nil { + t.Error(err) + } + out := wire.NewTxOut(10000, script) + outputs := []*wire.TxOut{out} + + changeSource := func() ([]byte, error) { + addr := w.CurrentAddress(wallet.INTERNAL) + script, err := txscript.PayToAddrScript(addr) + if err != nil { + return []byte{}, err + } + return script, nil + } + + inputSource := func(target btcutil.Amount) (total btcutil.Amount, inputs []*wire.TxIn, inputValues []btcutil.Amount, scripts [][]byte, err error) { + total += btcutil.Amount(utxos[0].Value) + in := wire.NewTxIn(&utxos[0].Op, []byte{}, [][]byte{}) + in.Sequence = 0 // Opt-in RBF so we can bump fees + inputs = append(inputs, in) + return total, inputs, inputValues, scripts, nil + } + + // Regular transaction + authoredTx, err := newUnsignedTransaction(outputs, btcutil.Amount(1000), inputSource, changeSource) + if err != nil { + t.Error(err) + } + if len(authoredTx.Tx.TxOut) != 2 { + t.Error("Returned incorrect number of outputs") + } + if len(authoredTx.Tx.TxIn) != 1 { + t.Error("Returned incorrect number of inputs") + } + + // Insufficient funds + outputs[0].Value = 1000000000 + _, err = newUnsignedTransaction(outputs, btcutil.Amount(1000), inputSource, changeSource) + if err == nil { + t.Error("Failed to return insuffient funds error") + } +} + +func TestBitcoinWallet_CreateMultisigSignature(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + ins, outs, redeemScript, err := buildTxData(w) + if err != nil { + t.Error(err) + } + + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + sigs, err := w.CreateMultisigSignature(ins, outs, key1, redeemScript, 50) + if err != nil { + t.Error(err) + } + if len(sigs) != 2 { + t.Error(err) + } + for _, sig := range sigs { + if len(sig.Signature) == 0 { + t.Error("Returned empty signature") + } + } +} + +func buildTxData(w *BitcoinWallet) ([]wallet.TransactionInput, []wallet.TransactionOutput, []byte, error) { + redeemScript := "522103c157f2a7c178430972263232c9306110090c50b44d4e906ecd6d377eec89a53c210205b02b9dbe570f36d1c12e3100e55586b2b9dc61d6778c1d24a8eaca03625e7e21030c83b025cd6bdd8c06e93a2b953b821b4a8c29da211335048d7dc3389706d7e853ae" + redeemScriptBytes, err := hex.DecodeString(redeemScript) + if err != nil { + return nil, nil, nil, err + } + h1, err := hex.DecodeString("1a20f4299b4fa1f209428dace31ebf4f23f13abd8ed669cebede118343a6ae05") + if err != nil { + return nil, nil, nil, err + } + in1 := wallet.TransactionInput{ + OutpointHash: h1, + OutpointIndex: 1, + } + h2, err := hex.DecodeString("458d88b4ae9eb4a347f2e7f5592f1da3b9ddf7d40f307f6e5d7bc107a9b3e90e") + if err != nil { + return nil, nil, nil, err + } + in2 := wallet.TransactionInput{ + OutpointHash: h2, + OutpointIndex: 0, + } + addr, err := w.DecodeAddress("1AhsMpyyyVyPZ9KDUgwsX3zTDJWWSsRo4f") + if err != nil { + return nil, nil, nil, err + } + + out := wallet.TransactionOutput{ + Value: 20000, + Address: addr, + } + return []wallet.TransactionInput{in1, in2}, []wallet.TransactionOutput{out}, redeemScriptBytes, nil +} + +func TestBitcoinWallet_Multisign(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + ins, outs, redeemScript, err := buildTxData(w) + if err != nil { + t.Error(err) + } + + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + key2, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + sigs1, err := w.CreateMultisigSignature(ins, outs, key1, redeemScript, 50) + if err != nil { + t.Error(err) + } + if len(sigs1) != 2 { + t.Error(err) + } + sigs2, err := w.CreateMultisigSignature(ins, outs, key2, redeemScript, 50) + if err != nil { + t.Error(err) + } + if len(sigs2) != 2 { + t.Error(err) + } + txBytes, err := w.Multisign(ins, outs, sigs1, sigs2, redeemScript, 50, false) + if err != nil { + t.Error(err) + } + + tx := wire.NewMsgTx(0) + tx.BtcDecode(bytes.NewReader(txBytes), wire.ProtocolVersion, wire.WitnessEncoding) + if len(tx.TxIn) != 2 { + t.Error("Transactions has incorrect number of inputs") + } + if len(tx.TxOut) != 1 { + t.Error("Transactions has incorrect number of outputs") + } + for _, in := range tx.TxIn { + if len(in.Witness) == 0 { + t.Error("Input witness has zero length") + } + } +} + +func TestBitcoinWallet_bumpFee(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + waitForTxnSync(t, w.db.Txns()) + ch, err := chainhash.NewHashFromStr("ff2b865c3b73439912eebf4cce9a15b12c7d7bcdd14ae1110a90541426c4e7c5") + if err != nil { + t.Error(err) + } + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + for _, u := range utxos { + if u.Op.Hash.IsEqual(ch) { + u.AtHeight = 0 + w.db.Utxos().Put(u) + } + } + + w.db.Txns().UpdateHeight(*ch, 0, time.Now()) + + // Test unconfirmed + _, err = w.bumpFee(*ch) + if err != nil { + t.Error(err) + } + + err = w.db.Txns().UpdateHeight(*ch, 1289597, time.Now()) + if err != nil { + t.Error(err) + } + + // Test confirmed + _, err = w.bumpFee(*ch) + if err == nil { + t.Error("Should not be able to bump fee of confirmed txs") + } +} + +func TestBitcoinWallet_sweepAddress(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + waitForTxnSync(t, w.db.Txns()) + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + + var in wallet.TransactionInput + var key *hdkeychain.ExtendedKey + for _, ut := range utxos { + if ut.Value > 0 && !ut.WatchOnly { + addr, err := w.ScriptToAddress(ut.ScriptPubkey) + if err != nil { + t.Error(err) + } + key, err = w.km.GetKeyForScript(addr.ScriptAddress()) + if err != nil { + t.Error(err) + } + h, err := hex.DecodeString(ut.Op.Hash.String()) + if err != nil { + t.Error(err) + } + in = wallet.TransactionInput{ + LinkedAddress: addr, + Value: ut.Value, + OutpointIndex: ut.Op.Index, + OutpointHash: h, + } + } + } + // P2PKH addr + _, err = w.sweepAddress([]wallet.TransactionInput{in}, nil, key, nil, wallet.NORMAL) + if err != nil { + t.Error(err) + return + } + + // 1 of 2 P2WSH + for _, ut := range utxos { + if ut.Value > 0 && ut.WatchOnly { + addr, err := w.ScriptToAddress(ut.ScriptPubkey) + if err != nil { + t.Error(err) + } + h, err := hex.DecodeString(ut.Op.Hash.String()) + if err != nil { + t.Error(err) + } + in = wallet.TransactionInput{ + LinkedAddress: addr, + Value: ut.Value, + OutpointIndex: ut.Op.Index, + OutpointHash: h, + } + } + } + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + key2, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + _, redeemScript, err := w.GenerateMultisigScript([]hdkeychain.ExtendedKey{*key1, *key2}, 1, 0, nil) + if err != nil { + t.Error(err) + } + _, err = w.sweepAddress([]wallet.TransactionInput{in}, nil, key1, &redeemScript, wallet.NORMAL) + if err != nil { + t.Error(err) + } +} + +func TestBitcoinWallet_estimateSpendFee(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + waitForTxnSync(t, w.db.Txns()) + fee, err := w.estimateSpendFee(1000, wallet.NORMAL) + if err != nil { + t.Error(err) + } + if fee == 0 { + t.Error("Returned incorrect fee") + } +} diff --git a/bitcoin/txsizes.go b/bitcoin/txsizes.go new file mode 100644 index 0000000..8d53b33 --- /dev/null +++ b/bitcoin/txsizes.go @@ -0,0 +1,249 @@ +package bitcoin + +// 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/bitcoin/txsizes_test.go b/bitcoin/txsizes_test.go new file mode 100644 index 0000000..7555b7f --- /dev/null +++ b/bitcoin/txsizes_test.go @@ -0,0 +1,84 @@ +package bitcoin + +// 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 ( + "bytes" + "encoding/hex" + "github.com/btcsuite/btcd/wire" + "testing" +) + +const ( + p2pkhScriptSize = P2PKHPkScriptSize + p2shScriptSize = 23 +) + +func makeInts(value int, n int) []int { + v := make([]int, n) + for i := range v { + v[i] = value + } + return v +} + +func TestEstimateSerializeSize(t *testing.T) { + tests := []struct { + InputCount int + OutputScriptLengths []int + AddChangeOutput bool + ExpectedSizeEstimate int + }{ + 0: {1, []int{}, false, 161}, + 1: {1, []int{p2pkhScriptSize}, false, 195}, + 2: {1, []int{}, true, 195}, + 3: {1, []int{p2pkhScriptSize}, true, 229}, + 4: {1, []int{p2shScriptSize}, false, 193}, + 5: {1, []int{p2shScriptSize}, true, 227}, + + 6: {2, []int{}, false, 310}, + 7: {2, []int{p2pkhScriptSize}, false, 344}, + 8: {2, []int{}, true, 344}, + 9: {2, []int{p2pkhScriptSize}, true, 378}, + 10: {2, []int{p2shScriptSize}, false, 342}, + 11: {2, []int{p2shScriptSize}, true, 376}, + + // 0xfd is discriminant for 16-bit compact ints, compact int + // total size increases from 1 byte to 3. + 12: {1, makeInts(p2pkhScriptSize, 0xfc), false, 8729}, + 13: {1, makeInts(p2pkhScriptSize, 0xfd), false, 8729 + P2PKHOutputSize + 2}, + 14: {1, makeInts(p2pkhScriptSize, 0xfc), true, 8729 + P2PKHOutputSize + 2}, + 15: {0xfc, []int{}, false, 37560}, + 16: {0xfd, []int{}, false, 37560 + RedeemP2PKHInputSize + 2}, + } + for i, test := range tests { + outputs := make([]*wire.TxOut, 0, len(test.OutputScriptLengths)) + for _, l := range test.OutputScriptLengths { + outputs = append(outputs, &wire.TxOut{PkScript: make([]byte, l)}) + } + actualEstimate := EstimateSerializeSize(test.InputCount, outputs, test.AddChangeOutput, P2PKH) + if actualEstimate != test.ExpectedSizeEstimate { + t.Errorf("Test %d: Got %v: Expected %v", i, actualEstimate, test.ExpectedSizeEstimate) + } + } +} + +func TestSumOutputSerializeSizes(t *testing.T) { + testTx := "0100000001066b78efa7d66d271cae6d6eb799e1d10953fb1a4a760226cc93186d52b55613010000006a47304402204e6c32cc214c496546c3277191ca734494fe49fed0af1d800db92fed2021e61802206a14d063b67f2f1c8fc18f9e9a5963fe33e18c549e56e3045e88b4fc6219be11012103f72d0a11727219bff66b8838c3c5e1c74a5257a325b0c84247bd10bdb9069e88ffffffff0200c2eb0b000000001976a914426e80ad778792e3e19c20977fb93ec0591e1a3988ac35b7cb59000000001976a914e5b6dc0b297acdd99d1a89937474df77db5743c788ac00000000" + txBytes, err := hex.DecodeString(testTx) + if err != nil { + t.Error(err) + return + } + r := bytes.NewReader(txBytes) + msgTx := wire.NewMsgTx(1) + msgTx.BtcDecode(r, 1, wire.WitnessEncoding) + if SumOutputSerializeSizes(msgTx.TxOut) != 68 { + t.Error("SumOutputSerializeSizes returned incorrect value") + } + +} diff --git a/bitcoin/wallet.go b/bitcoin/wallet.go new file mode 100644 index 0000000..7ac92c5 --- /dev/null +++ b/bitcoin/wallet.go @@ -0,0 +1,477 @@ +package bitcoin + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "io" + "time" + + "github.com/OpenBazaar/multiwallet/cache" + "github.com/OpenBazaar/multiwallet/client" + "github.com/OpenBazaar/multiwallet/config" + "github.com/OpenBazaar/multiwallet/keys" + "github.com/OpenBazaar/multiwallet/model" + "github.com/OpenBazaar/multiwallet/service" + "github.com/OpenBazaar/multiwallet/util" + "github.com/OpenBazaar/spvwallet" + "github.com/OpenBazaar/spvwallet/exchangerates" + wi "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + btc "github.com/btcsuite/btcutil" + hd "github.com/btcsuite/btcutil/hdkeychain" + "github.com/btcsuite/btcwallet/wallet/txrules" + logging "github.com/op/go-logging" + bip39 "github.com/tyler-smith/go-bip39" + "golang.org/x/net/proxy" +) + +type BitcoinWallet struct { + db wi.Datastore + km *keys.KeyManager + params *chaincfg.Params + client model.APIClient + ws *service.WalletService + fp *spvwallet.FeeProvider + + mPrivKey *hd.ExtendedKey + mPubKey *hd.ExtendedKey + + exchangeRates wi.ExchangeRates + log *logging.Logger +} + +var _ = wi.Wallet(&BitcoinWallet{}) + +func NewBitcoinWallet(cfg config.CoinConfig, mnemonic string, params *chaincfg.Params, proxy proxy.Dialer, cache cache.Cacher, disableExchangeRates bool) (*BitcoinWallet, 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.Bitcoin, keyToAddress) + if err != nil { + return nil, err + } + + c, err := client.NewClientPool(cfg.ClientAPIs, proxy) + if err != nil { + return nil, err + } + er := exchangerates.NewBitcoinPriceFetcher(proxy) + if !disableExchangeRates { + go er.Run() + } + + wm, err := service.NewWalletService(cfg.DB, km, c, params, wi.Bitcoin, cache) + if err != nil { + return nil, err + } + + fp := spvwallet.NewFeeProvider(cfg.MaxFee, cfg.HighFee, cfg.MediumFee, cfg.LowFee, cfg.FeeAPI, proxy) + + return &BitcoinWallet{ + db: cfg.DB, + km: km, + params: params, + client: c, + ws: wm, + fp: fp, + mPrivKey: mPrivKey, + mPubKey: mPubKey, + exchangeRates: er, + log: logging.MustGetLogger("bitcoin-wallet"), + }, nil +} + +func keyToAddress(key *hd.ExtendedKey, params *chaincfg.Params) (btc.Address, error) { + return key.Address(params) +} + +func (w *BitcoinWallet) Start() { + w.client.Start() + w.ws.Start() +} + +func (w *BitcoinWallet) Params() *chaincfg.Params { + return w.params +} + +func (w *BitcoinWallet) CurrencyCode() string { + if w.params.Name == chaincfg.MainNetParams.Name { + return "btc" + } else { + return "tbtc" + } +} + +func (w *BitcoinWallet) IsDust(amount int64) bool { + return txrules.IsDustAmount(btc.Amount(amount), 25, txrules.DefaultRelayFeePerKb) +} + +func (w *BitcoinWallet) MasterPrivateKey() *hd.ExtendedKey { + return w.mPrivKey +} + +func (w *BitcoinWallet) MasterPublicKey() *hd.ExtendedKey { + return w.mPubKey +} + +func (w *BitcoinWallet) 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 *BitcoinWallet) CurrentAddress(purpose wi.KeyPurpose) btc.Address { + 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) + } + return addr +} + +func (w *BitcoinWallet) NewAddress(purpose wi.KeyPurpose) btc.Address { + 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) + } + return addr +} + +func (w *BitcoinWallet) DecodeAddress(addr string) (btc.Address, error) { + return btc.DecodeAddress(addr, w.params) +} + +func (w *BitcoinWallet) ScriptToAddress(script []byte) (btc.Address, error) { + _, addrs, _, err := txscript.ExtractPkScriptAddrs(script, w.params) + if err != nil { + return nil, err + } + if len(addrs) == 0 { + return nil, errors.New("unknown script") + } + return addrs[0], nil +} + +func (w *BitcoinWallet) AddressToScript(addr btc.Address) ([]byte, error) { + return txscript.PayToAddrScript(addr) +} + +func (w *BitcoinWallet) HasKey(addr btc.Address) bool { + _, err := w.km.GetKeyForScript(addr.ScriptAddress()) + if err != nil { + return false + } + return true +} + +func (w *BitcoinWallet) Balance() (confirmed, unconfirmed int64) { + utxos, _ := w.db.Utxos().GetAll() + txns, _ := w.db.Txns().GetAll(false) + return util.CalcBalance(utxos, txns) +} + +func (w *BitcoinWallet) 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 < 6: + status = wi.StatusPending + confirmations = confs + case confs > 5: + status = wi.StatusConfirmed + confirmations = confs + } + tx.Confirmations = int64(confirmations) + tx.Status = status + txns[i] = tx + } + return txns, nil +} + +func (w *BitcoinWallet) 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 { + var addr btc.Address + _, addrs, _, err := txscript.ExtractPkScriptAddrs(out.PkScript, w.params) + if err != nil { + w.log.Errorf("error extracting address from txn pkscript: %v\n", err) + } + if len(addrs) == 0 { + addr = nil + } else { + addr = addrs[0] + } + tout := wi.TransactionOutput{ + Address: addr, + Value: out.Value, + Index: uint32(i), + } + outs = append(outs, tout) + } + txn.Outputs = outs + } + return txn, err +} + +func (w *BitcoinWallet) ChainTip() (uint32, chainhash.Hash) { + return w.ws.ChainTip() +} + +func (w *BitcoinWallet) GetFeePerByte(feeLevel wi.FeeLevel) uint64 { + return w.fp.GetFeePerByte(feeLevel) +} + +func (w *BitcoinWallet) Spend(amount int64, addr btc.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 + } + } + + if err := w.Broadcast(tx); err != nil { + return nil, err + } + ch := tx.TxHash() + return &ch, nil +} + +func (w *BitcoinWallet) BumpFee(txid chainhash.Hash) (*chainhash.Hash, error) { + return w.bumpFee(txid) +} + +func (w *BitcoinWallet) EstimateFee(ins []wi.TransactionInput, outs []wi.TransactionOutput, feePerByte uint64) uint64 { + tx := new(wire.MsgTx) + for _, out := range outs { + scriptPubKey, _ := txscript.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 *BitcoinWallet) EstimateSpendFee(amount int64, feeLevel wi.FeeLevel) (uint64, error) { + return w.estimateSpendFee(amount, feeLevel) +} + +func (w *BitcoinWallet) SweepAddress(ins []wi.TransactionInput, address *btc.Address, key *hd.ExtendedKey, redeemScript *[]byte, feeLevel wi.FeeLevel) (*chainhash.Hash, error) { + return w.sweepAddress(ins, address, key, redeemScript, feeLevel) +} + +func (w *BitcoinWallet) 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 *BitcoinWallet) 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 *BitcoinWallet) GenerateMultisigScript(keys []hd.ExtendedKey, threshold int, timeout time.Duration, timeoutKey *hd.ExtendedKey) (addr btc.Address, redeemScript []byte, err error) { + return w.generateMultisigScript(keys, threshold, timeout, timeoutKey) +} + +func (w *BitcoinWallet) AddWatchedAddresses(addrs ...btc.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 *BitcoinWallet) AddTransactionListener(callback func(wi.TransactionCallback)) { + w.ws.AddTransactionListener(callback) +} + +func (w *BitcoinWallet) ReSyncBlockchain(fromTime time.Time) { + go w.ws.UpdateState() +} + +func (w *BitcoinWallet) 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 *BitcoinWallet) Close() { + w.ws.Stop() + w.client.Close() +} + +func (w *BitcoinWallet) ExchangeRates() wi.ExchangeRates { + return w.exchangeRates +} + +func (w *BitcoinWallet) 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) + } +} + +// Build a client.Transaction so we can ingest it into the wallet service then broadcast +func (w *BitcoinWallet) 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.Bitcoin), + } + 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 *BitcoinWallet) AssociateTransactionWithOrder(cb wi.TransactionCallback) { + w.ws.InvokeTransactionListeners(cb) +} diff --git a/bitcoincash/exchange_rates.go b/bitcoincash/exchange_rates.go new file mode 100644 index 0000000..c036a70 --- /dev/null +++ b/bitcoincash/exchange_rates.go @@ -0,0 +1,165 @@ +package bitcoincash + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/OpenBazaar/multiwallet/util" + "golang.org/x/net/proxy" + "net/http" + "reflect" + "sync" + "time" +) + +type ExchangeRateProvider struct { + fetchUrl string + cache map[string]float64 + client *http.Client + decoder ExchangeRateDecoder +} + +type ExchangeRateDecoder interface { + decode(dat interface{}, cache map[string]float64) (err error) +} + +type OpenBazaarDecoder struct{} + +type BitcoinCashPriceFetcher struct { + sync.Mutex + cache map[string]float64 + providers []*ExchangeRateProvider +} + +func NewBitcoinCashPriceFetcher(dialer proxy.Dialer) *BitcoinCashPriceFetcher { + b := BitcoinCashPriceFetcher{ + 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} + } + + + b.providers = []*ExchangeRateProvider{ + {"https://ticker.openbazaar.org/api", b.cache, client, OpenBazaarDecoder{}}, + } + return &b +} + +func (b *BitcoinCashPriceFetcher) GetExchangeRate(currencyCode string) (float64, error) { + b.Lock() + defer b.Unlock() + + currencyCode = util.NormalizeCurrencyCode(currencyCode) + price, ok := b.cache[currencyCode] + if !ok { + return 0, errors.New("Currency not tracked") + } + return price, nil +} + +func (b *BitcoinCashPriceFetcher) GetLatestRate(currencyCode string) (float64, error) { + b.fetchCurrentRates() + b.Lock() + defer b.Unlock() + + currencyCode = util.NormalizeCurrencyCode(currencyCode) + price, ok := b.cache[currencyCode] + if !ok { + return 0, errors.New("Currency not tracked") + } + return price, nil +} + +func (b *BitcoinCashPriceFetcher) GetAllRates(cacheOK bool) (map[string]float64, error) { + if !cacheOK { + err := b.fetchCurrentRates() + if err != nil { + return nil, err + } + } + b.Lock() + defer b.Unlock() + return b.cache, nil +} + +func (b *BitcoinCashPriceFetcher) UnitsPerCoin() int { + return 100000000 +} + +func (b *BitcoinCashPriceFetcher) fetchCurrentRates() error { + b.Lock() + defer b.Unlock() + for _, provider := range b.providers { + err := provider.fetch() + if err == nil { + return nil + } + fmt.Println(err) + } + return errors.New("All exchange rate API queries failed") +} + +func (b *BitcoinCashPriceFetcher) Run() { + b.fetchCurrentRates() + ticker := time.NewTicker(time.Minute * 15) + for range ticker.C { + b.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) +} + +func (b OpenBazaarDecoder) decode(dat interface{}, cache map[string]float64) (err error) { + data, ok := dat.(map[string]interface{}) + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed") + } + bch, ok := data["BCH"] + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed, missing 'BCH' field") + } + val, ok := bch.(map[string]interface{}) + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed") + } + bchRate, 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 / bchRate) + } + } + return nil +} diff --git a/bitcoincash/exchange_rates_test.go b/bitcoincash/exchange_rates_test.go new file mode 100644 index 0000000..9e18fc7 --- /dev/null +++ b/bitcoincash/exchange_rates_test.go @@ -0,0 +1,219 @@ +package bitcoincash + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "testing" + + "gopkg.in/jarcoal/httpmock.v1" +) + +func setupBitcoinPriceFetcher() (BitcoinCashPriceFetcher, func()) { + var ( + url = "https://ticker.openbazaar.org/api" + mockResponse = `{ + "BCH": { + "last": 20.00000, + "type": "crypto" + }, + "USD": { + "last": 10000.00, + "type": "fiat" + } + }` + exchangeCache = make(map[string]float64) + ) + + httpmock.Activate() + httpmock.RegisterResponder("GET", url, + httpmock.NewStringResponder(200, mockResponse)) + + return BitcoinCashPriceFetcher{ + cache: exchangeCache, + providers: []*ExchangeRateProvider{{url, exchangeCache, &http.Client{}, OpenBazaarDecoder{}}}, + }, httpmock.DeactivateAndReset +} + +func TestFetchCurrentRates(t *testing.T) { + b, teardown := setupBitcoinPriceFetcher() + defer teardown() + + err := b.fetchCurrentRates() + if err != nil { + t.Error("Failed to fetch bitcoin exchange rates") + } +} + +func TestGetLatestRate(t *testing.T) { + b, teardown := setupBitcoinPriceFetcher() + defer teardown() + + price, ok := b.cache["USD"] + if !ok && price == 500.00 { + t.Errorf("incorrect cache value, expected (%f) but got (%f)", 500.00, price) + } + price, err := b.GetLatestRate("USD") + if err != nil && price == 500.00 { + t.Error("Incorrect return at GetLatestRate (price, err)", price, err) + } +} + +func TestGetAllRates(t *testing.T) { + b, teardown := setupBitcoinPriceFetcher() + defer teardown() + + b.cache["USD"] = 650.00 + b.cache["EUR"] = 600.00 + priceMap, err := b.GetAllRates(true) + if err != nil { + t.Error(err) + } + usd, ok := priceMap["USD"] + if !ok || usd != 650.00 { + t.Error("Failed to fetch exchange rates from cache") + } + eur, ok := priceMap["EUR"] + if !ok || eur != 600.00 { + t.Error("Failed to fetch exchange rates from cache") + } +} + +func TestGetExchangeRate(t *testing.T) { + b, teardown := setupBitcoinPriceFetcher() + defer teardown() + + b.cache["USD"] = 650.00 + r, err := b.GetExchangeRate("USD") + if err != nil { + t.Error("Failed to fetch exchange rate") + } + if r != 650.00 { + t.Error("Returned exchange rate incorrect") + } + r, err = b.GetExchangeRate("EUR") + if r != 0 || err == nil { + t.Error("Return erroneous exchange rate") + } + + // Test that currency symbols are normalized correctly + r, err = b.GetExchangeRate("usd") + if err != nil { + t.Error("Failed to fetch exchange rate") + } + if r != 650.00 { + t.Error("Returned exchange rate incorrect") + } +} + +type req struct { + io.Reader +} + +func (r *req) Close() error { + return nil +} + +func TestDecodeOpenBazaar(t *testing.T) { + cache := make(map[string]float64) + openbazaarDecoder := OpenBazaarDecoder{} + var dataMap interface{} + + response := `{ + "AED": { + "ask": 2242.19, + "bid": 2236.61, + "last": 2239.99, + "timestamp": "Tue, 02 Aug 2016 00:20:45 -0000", + "volume_btc": 0.0, + "volume_percent": 0.0 + }, + "AFN": { + "ask": 41849.95, + "bid": 41745.86, + "last": 41808.85, + "timestamp": "Tue, 02 Aug 2016 00:20:45 -0000", + "volume_btc": 0.0, + "volume_percent": 0.0 + }, + "ALL": { + "ask": 74758.44, + "bid": 74572.49, + "last": 74685.02, + "timestamp": "Tue, 02 Aug 2016 00:20:45 -0000", + "volume_btc": 0.0, + "volume_percent": 0.0 + }, + "BCH": { + "ask":32.089016, + "bid":32.089016, + "last":32.089016, + "timestamp": "Tue, 02 Aug 2016 00:20:45 -0000" + }, + "timestamp": "Tue, 02 Aug 2016 00:20:45 -0000" + }` + // Test valid response + r := &req{bytes.NewReader([]byte(response))} + decoder := json.NewDecoder(r) + err := decoder.Decode(&dataMap) + if err != nil { + t.Error(err) + } + err = openbazaarDecoder.decode(dataMap, cache) + if err != nil { + t.Error(err) + } + // Make sure it saved to cache + if len(cache) == 0 { + t.Error("Failed to response to cache") + } + resp := `{"ZWL": { + "ask": 196806.48, + "bid": 196316.95, + "timestamp": "Tue, 02 Aug 2016 00:20:45 -0000", + "volume_btc": 0.0, + "volume_percent": 0.0 + }}` + + // Test missing JSON element + r = &req{bytes.NewReader([]byte(resp))} + decoder = json.NewDecoder(r) + err = decoder.Decode(&dataMap) + if err != nil { + t.Error(err) + } + err = openbazaarDecoder.decode(dataMap, cache) + if err == nil { + t.Error(err) + } + resp = `{ + "ask": 196806.48, + "bid": 196316.95, + "last": 196613.2, + "timestamp": "Tue, 02 Aug 2016 00:20:45 -0000", + "volume_btc": 0.0, + "volume_percent": 0.0 + }` + + // Test invalid JSON + r = &req{bytes.NewReader([]byte(resp))} + decoder = json.NewDecoder(r) + err = decoder.Decode(&dataMap) + if err != nil { + t.Error(err) + } + err = openbazaarDecoder.decode(dataMap, cache) + if err == nil { + t.Error(err) + } + + // Test decode error + r = &req{bytes.NewReader([]byte(""))} + decoder = json.NewDecoder(r) + decoder.Decode(&dataMap) + err = openbazaarDecoder.decode(dataMap, cache) + if err == nil { + t.Error(err) + } +} diff --git a/bitcoincash/sign.go b/bitcoincash/sign.go new file mode 100644 index 0000000..dd3dc5b --- /dev/null +++ b/bitcoincash/sign.go @@ -0,0 +1,678 @@ +package bitcoincash + +import ( + "bytes" + "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" + "github.com/btcsuite/btcutil" + 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/btcsuite/btcwallet/wallet/txrules" + "github.com/cpacia/bchutil" + + "github.com/OpenBazaar/multiwallet/util" +) + +func (w *BitcoinCashWallet) buildTx(amount int64, addr btc.Address, feeLevel wi.FeeLevel, optionalOutput *wire.TxOut) (*wire.MsgTx, error) { + // Check for dust + script, _ := bchutil.PayToAddrScript(addr) + if txrules.IsDustAmount(btc.Amount(amount), len(script), txrules.DefaultRelayFeePerKb) { + return nil, wi.ErrorDustAmount + } + + var additionalPrevScripts map[wire.OutPoint][]byte + var additionalKeysByAddress map[string]*btc.WIF + var inVals map[wire.OutPoint]int64 + + // 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 []btcutil.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) + inVals = make(map[wire.OutPoint]int64) + 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 := key.Address(w.params) + if err != nil { + continue + } + privKey, err := key.ECPrivKey() + if err != nil { + continue + } + wif, _ := btc.NewWIF(privKey, w.params, true) + additionalKeysByAddress[addr.EncodeAddress()] = wif + val := c.Value() + sat := val.ToUnit(btc.AmountSatoshi) + inVals[*outpoint] = int64(sat) + } + 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 := bchutil.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) { + addrStr := addr.EncodeAddress() + wif := additionalKeysByAddress[addrStr] + 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 := bchutil.SignTxOutput(w.params, + authoredTx.Tx, i, prevOutScript, txscript.SigHashAll, getKey, + getScript, txIn.SignatureScript, inVals[txIn.PreviousOutPoint]) + if err != nil { + return nil, errors.New("Failed to sign transaction") + } + txIn.SignatureScript = script + } + return authoredTx.Tx, nil +} + +func (w *BitcoinCashWallet) 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, inVals, additionalPrevScripts, additionalKeysByAddress := util.LoadAllInputs(tx, coinMap, w.params) + + // outputs + script, err := bchutil.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(btc.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 := bchutil.SignTxOutput(w.params, + tx, i, prevOutScript, txscript.SigHashAll, getKey, + getScript, txIn.SignatureScript, inVals[txIn.PreviousOutPoint]) + 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(feePerKb, estimatedSize) + + for { + inputAmount, inputs, _, scripts, err := fetchInputs(targetAmount + targetFee) + if err != nil { + return nil, err + } + if inputAmount < targetAmount+targetFee { + return nil, errors.New("insufficient funds available to construct transaction") + } + + maxSignedSize := EstimateSerializeSize(len(inputs), outputs, true, P2PKH) + maxRequiredFee := txrules.FeeForSerializeSize(feePerKb, maxSignedSize) + remainingAmount := inputAmount - targetAmount + if remainingAmount < maxRequiredFee { + targetFee = maxRequiredFee + continue + } + + unsignedTransaction := &wire.MsgTx{ + Version: wire.TxVersion, + TxIn: inputs, + TxOut: outputs, + LockTime: 0, + } + changeIndex := -1 + changeAmount := inputAmount - targetAmount - maxRequiredFee + if changeAmount != 0 && !txrules.IsDustAmount(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 *BitcoinCashWallet) 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: 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 *BitcoinCashWallet) 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 := bchutil.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 := bchutil.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 + } + } + } + + for i, txIn := range tx.TxIn { + if !timeLocked { + prevOutScript := additionalPrevScripts[txIn.PreviousOutPoint] + script, err := bchutil.SignTxOutput(w.params, + tx, i, prevOutScript, txscript.SigHashAll, getKey, + getScript, txIn.SignatureScript, ins[i].Value) + if err != nil { + return nil, errors.New("Failed to sign transaction") + } + txIn.SignatureScript = script + } else { + priv, err := key.ECPrivKey() + if err != nil { + return nil, err + } + script, err := bchutil.RawTxInSignature(tx, i, *redeemScript, txscript.SigHashAll, priv, ins[i].Value) + if err != nil { + return nil, err + } + builder := txscript.NewScriptBuilder(). + AddData(script). + AddOp(txscript.OP_0). + AddData(*redeemScript) + scriptSig, _ := builder.Script() + txIn.SignatureScript = scriptSig + } + } + + // broadcast + if err := w.Broadcast(tx); err != nil { + return nil, err + } + txid := tx.TxHash() + return &txid, nil +} + +func (w *BitcoinCashWallet) 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 := bchutil.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) + + signingKey, err := key.ECPrivKey() + if err != nil { + return sigs, err + } + + for i := range tx.TxIn { + sig, err := bchutil.RawTxInSignature(tx, i, redeemScript, txscript.SigHashAll, signingKey, ins[i].Value) + if err != nil { + continue + } + bs := wi.Signature{InputIndex: uint32(i), Signature: sig} + sigs = append(sigs, bs) + } + return sigs, nil +} + +func (w *BitcoinCashWallet) 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 := bchutil.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 + } + } + for _, sig := range sigs2 { + if int(sig.InputIndex) == i { + sig2 = sig.Signature + } + } + builder := txscript.NewScriptBuilder() + builder.AddOp(txscript.OP_0) + builder.AddData(sig1) + builder.AddData(sig2) + + if timeLocked { + builder.AddOp(txscript.OP_1) + } + + builder.AddData(redeemScript) + scriptSig, err := builder.Script() + if err != nil { + return nil, err + } + input.SignatureScript = scriptSig + } + // broadcast + var buf bytes.Buffer + tx.BtcEncode(&buf, wire.ProtocolVersion, wire.BaseEncoding) + if broadcast { + if err := w.Broadcast(tx); err != nil { + return nil, err + } + } + return buf.Bytes(), nil +} + +func (w *BitcoinCashWallet) 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 + } + addr, err = bchutil.NewCashAddressScriptHash(redeemScript, w.params) + if err != nil { + return nil, nil, err + } + return addr, redeemScript, nil +} + +func (w *BitcoinCashWallet) 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 := bchutil.DecodeAddress("qpf464w2g36kyklq9shvyjk9lvuf6ph7jv3k8qpq0m", &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/bitcoincash/sign_test.go b/bitcoincash/sign_test.go new file mode 100644 index 0000000..dea1e01 --- /dev/null +++ b/bitcoincash/sign_test.go @@ -0,0 +1,732 @@ +package bitcoincash + +import ( + "bytes" + "encoding/hex" + "github.com/OpenBazaar/multiwallet/util" + "github.com/gcash/bchd/txscript" + "os" + "testing" + "time" + + "github.com/OpenBazaar/multiwallet/cache" + "github.com/OpenBazaar/multiwallet/datastore" + "github.com/OpenBazaar/multiwallet/keys" + "github.com/OpenBazaar/multiwallet/model/mock" + "github.com/OpenBazaar/multiwallet/service" + "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" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/cpacia/bchutil" + bchhash "github.com/gcash/bchd/chaincfg/chainhash" + bchwire "github.com/gcash/bchd/wire" +) + +type FeeResponse struct { + Priority int `json:"priority"` + Normal int `json:"normal"` + Economic int `json:"economic"` +} + +func newMockWallet() (*BitcoinCashWallet, error) { + mockDb := datastore.NewMockMultiwalletDatastore() + + db, err := mockDb.GetDatastoreForWallet(wallet.BitcoinCash) + if err != nil { + return nil, err + } + params := &chaincfg.MainNetParams + + seed, err := hex.DecodeString("16c034c59522326867593487c03a8f9615fb248406dd0d4ffb3a6b976a248403") + if err != nil { + return nil, err + } + master, err := hdkeychain.NewMaster(seed, params) + if err != nil { + return nil, err + } + km, err := keys.NewKeyManager(db.Keys(), params, master, wallet.BitcoinCash, bitcoinCashAddress) + if err != nil { + return nil, err + } + + fp := util.NewFeeProvider(2000, 300, 200, 100, nil) + + bw := &BitcoinCashWallet{ + params: params, + km: km, + db: db, + fp: fp, + } + cli := mock.NewMockApiClient(bw.AddressToScript) + ws, err := service.NewWalletService(db, km, cli, params, wallet.BitcoinCash, cache.NewMockCacher()) + if err != nil { + return nil, err + } + bw.client = cli + bw.ws = ws + return bw, nil +} + +func TestWalletService_VerifyWatchScriptFilter(t *testing.T) { + // Verify that AddWatchedAddress should never add a script which already represents a key from its own wallet + w, err := newMockWallet() + if err != nil { + t.Fatal(err) + } + keys := w.km.GetKeys() + + addr, err := w.km.KeyToAddress(keys[0]) + if err != nil { + t.Fatal(err) + } + err = w.AddWatchedAddresses(addr) + if err != nil { + t.Fatal(err) + } + + watchScripts, err := w.db.WatchedScripts().GetAll() + if err != nil { + t.Fatal(err) + } + + if len(watchScripts) != 0 { + t.Error("Put watched scripts fails on key manager owned key") + } +} + +func TestWalletService_VerifyWatchScriptPut(t *testing.T) { + // Verify that AddWatchedAddress should add a script which does not represent a key from its own wallet + w, err := newMockWallet() + if err != nil { + t.Fatal(err) + } + + addr, err := w.DecodeAddress("qqx0p0ja3xddkvwldaqwcvrkkgrzx6rjwuzla4ca90") + if err != nil { + t.Fatal(err) + } + + err = w.AddWatchedAddresses(addr) + if err != nil { + t.Fatal(err) + } + + watchScripts, err := w.db.WatchedScripts().GetAll() + if err != nil { + t.Fatal(err) + } + + if len(watchScripts) == 0 { + t.Error("Put watched scripts fails on non-key manager owned key") + } + +} + +func waitForTxnSync(t *testing.T, txnStore wallet.Txns) { + // Look for a known txn, this sucks a bit. It would be better to check if the + // number of stored txns matched the expected, but not all the mock + // transactions are relevant, so the numbers don't add up. + // Even better would be for the wallet to signal that the initial sync was + // done. + lastTxn := mock.MockTransactions[len(mock.MockTransactions)-2] + txHash, err := chainhash.NewHashFromStr(lastTxn.Txid) + if err != nil { + t.Fatal(err) + } + + for i := 0; i < 100; i++ { + if _, err := txnStore.Get(*txHash); err == nil { + return + } + time.Sleep(100 * time.Millisecond) + } + t.Fatal("timeout waiting for wallet to sync transactions") +} + +func TestBitcoinCashWallet_buildTx(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + time.Sleep(time.Second / 2) + + addr, err := w.DecodeAddress("qpf464w2g36kyklq9shvyjk9lvuf6ph7jv3k8qpq0m") + if err != nil { + t.Error(err) + } + // Test build normal tx + tx, err := w.buildTx(1500000, addr, wallet.NORMAL, nil) + if err != nil { + w.DumpTables(os.Stdout) + t.Error(err) + return + } + if !containsOutput(tx, addr) { + t.Error("Built tx does not contain the requested output") + } + if !validInputs(tx, w.db) { + t.Error("Built tx does not contain valid inputs") + } + if !validChangeAddress(tx, w.db, w.params) { + t.Error("Built tx does not contain a valid change output") + } + + // Insuffient funds + _, err = w.buildTx(1000000000, addr, wallet.NORMAL, nil) + if err != wallet.ErrorInsuffientFunds { + t.Error("Failed to throw insuffient funds error") + } + + // Dust + _, err = w.buildTx(1, addr, wallet.NORMAL, nil) + if err != wallet.ErrorDustAmount { + t.Error("Failed to throw dust error") + } +} + +func TestBitcoinCashWallet_buildSpendAllTx(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + time.Sleep(time.Second / 2) + + waitForTxnSync(t, w.db.Txns()) + addr, err := w.DecodeAddress("qpyafty5hf6uwjtd8y5tvgzeawfeyfhj55ke8l2dy7") + if err != nil { + t.Error(err) + } + + // Test build spendAll tx + tx, err := w.buildSpendAllTx(addr, wallet.NORMAL) + if err != nil { + t.Error(err) + } + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Fatal(err) + } + spendableUtxos := 0 + for _, u := range utxos { + if !u.WatchOnly { + spendableUtxos++ + } + } + if len(tx.TxIn) != spendableUtxos { + t.Error("Built tx does not spend all available utxos") + } + if !containsOutput(tx, addr) { + t.Error("Built tx does not contain the requested output") + } + if !validInputs(tx, w.db) { + t.Error("Built tx does not contain valid inputs") + } + if len(tx.TxOut) != 1 { + t.Error("Built tx should only have one output") + } + + bchTx := bchwire.MsgTx{ + Version: tx.Version, + LockTime: tx.LockTime, + } + for _, in := range tx.TxIn { + hash := bchhash.Hash(in.PreviousOutPoint.Hash) + op := bchwire.NewOutPoint(&hash, in.PreviousOutPoint.Index) + newIn := bchwire.TxIn{ + PreviousOutPoint: *op, + Sequence: in.Sequence, + SignatureScript: in.SignatureScript, + } + bchTx.TxIn = append(bchTx.TxIn, &newIn) + } + for _, out := range tx.TxOut { + newOut := bchwire.TxOut{ + Value: out.Value, + PkScript: out.PkScript, + } + bchTx.TxOut = append(bchTx.TxOut, &newOut) + } + + // Verify the signatures on each input using the scripting engine + for i, in := range tx.TxIn { + var prevScript []byte + var amt int64 + for _, u := range utxos { + if util.OutPointsEqual(u.Op, in.PreviousOutPoint) { + prevScript = u.ScriptPubkey + amt = u.Value + break + } + } + vm, err := txscript.NewEngine(prevScript, &bchTx, i, txscript.StandardVerifyFlags, nil, nil, amt) + if err != nil { + t.Fatal(err) + } + if err := vm.Execute(); err != nil { + t.Error(err) + } + } +} + +func containsOutput(tx *wire.MsgTx, addr btcutil.Address) bool { + for _, o := range tx.TxOut { + script, _ := bchutil.PayToAddrScript(addr) + if bytes.Equal(script, o.PkScript) { + return true + } + } + return false +} + +func validInputs(tx *wire.MsgTx, db wallet.Datastore) bool { + utxos, _ := db.Utxos().GetAll() + uMap := make(map[wire.OutPoint]bool) + for _, u := range utxos { + uMap[u.Op] = true + } + for _, in := range tx.TxIn { + if !uMap[in.PreviousOutPoint] { + return false + } + } + return true +} + +func validChangeAddress(tx *wire.MsgTx, db wallet.Datastore, params *chaincfg.Params) bool { + for _, out := range tx.TxOut { + addr, err := bchutil.ExtractPkScriptAddrs(out.PkScript, params) + if err != nil { + continue + } + _, err = db.Keys().GetPathForKey(addr.ScriptAddress()) + if err == nil { + return true + } + } + return false +} + +func TestBitcoinCashWallet_GenerateMultisigScript(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + pubkey1, err := key1.ECPubKey() + if err != nil { + t.Error(err) + } + key2, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + pubkey2, err := key2.ECPubKey() + if err != nil { + t.Error(err) + } + key3, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + pubkey3, err := key3.ECPubKey() + if err != nil { + t.Error(err) + } + keys := []hdkeychain.ExtendedKey{*key1, *key2, *key3} + + // test without timeout + addr, redeemScript, err := w.generateMultisigScript(keys, 2, 0, nil) + if err != nil { + t.Error(err) + } + if addr.String() != "pzjfg2pg2q6uz445vx7hvmuw6rp0ay5f9q9vnhwqfl" { + t.Error("Returned invalid address") + } + + rs := "52" + // OP_2 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey1.SerializeCompressed()) + // pubkey1 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey2.SerializeCompressed()) + // pubkey2 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey3.SerializeCompressed()) + // pubkey3 + "53" + // OP_3 + "ae" // OP_CHECKMULTISIG + rsBytes, err := hex.DecodeString(rs) + if err != nil { + t.Error(err) + } + if !bytes.Equal(rsBytes, redeemScript) { + t.Error("Returned invalid redeem script") + } + + // test with timeout + key4, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + pubkey4, err := key4.ECPubKey() + if err != nil { + t.Error(err) + } + addr, redeemScript, err = w.generateMultisigScript(keys, 2, time.Hour*10, key4) + if err != nil { + t.Error(err) + } + if addr.String() != "ppx5mmammxfs42m0p6ypvf6znnkq3llskvlz0texus" { + t.Error("Returned invalid address") + } + + rs = "63" + // OP_IF + "52" + // OP_2 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey1.SerializeCompressed()) + // pubkey1 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey2.SerializeCompressed()) + // pubkey2 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey3.SerializeCompressed()) + // pubkey3 + "53" + // OP_3 + "ae" + // OP_CHECKMULTISIG + "67" + // OP_ELSE + "01" + // OP_PUSHDATA(1) + "3c" + // 60 blocks + "b2" + // OP_CHECKSEQUENCEVERIFY + "75" + // OP_DROP + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey4.SerializeCompressed()) + // timeout pubkey + "ac" + // OP_CHECKSIG + "68" // OP_ENDIF + rsBytes, err = hex.DecodeString(rs) + if !bytes.Equal(rsBytes, redeemScript) { + t.Error("Returned invalid redeem script") + } +} + +func TestBitcoinCashWallet_newUnsignedTransaction(t *testing.T) { + w, err := newMockWallet() + w.ws.Start() + time.Sleep(time.Second / 2) + if err != nil { + t.Error(err) + } + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + addr, err := w.DecodeAddress("ppx5mmammxfs42m0p6ypvf6znnkq3llskvlz0texus") + if err != nil { + t.Error(err) + } + + script, err := bchutil.PayToAddrScript(addr) + if err != nil { + t.Error(err) + } + out := wire.NewTxOut(10000, script) + outputs := []*wire.TxOut{out} + + changeSource := func() ([]byte, error) { + addr := w.CurrentAddress(wallet.INTERNAL) + script, err := bchutil.PayToAddrScript(addr) + if err != nil { + return []byte{}, err + } + return script, nil + } + + inputSource := func(target btcutil.Amount) (total btcutil.Amount, inputs []*wire.TxIn, inputValues []btcutil.Amount, scripts [][]byte, err error) { + total += btcutil.Amount(utxos[0].Value) + in := wire.NewTxIn(&utxos[0].Op, []byte{}, [][]byte{}) + in.Sequence = 0 // Opt-in RBF so we can bump fees + inputs = append(inputs, in) + return total, inputs, inputValues, scripts, nil + } + + // Regular transaction + authoredTx, err := newUnsignedTransaction(outputs, btcutil.Amount(1000), inputSource, changeSource) + if err != nil { + t.Error(err) + } + if len(authoredTx.Tx.TxOut) != 2 { + t.Error("Returned incorrect number of outputs") + } + if len(authoredTx.Tx.TxIn) != 1 { + t.Error("Returned incorrect number of inputs") + } + + // Insufficient funds + outputs[0].Value = 1000000000 + _, err = newUnsignedTransaction(outputs, btcutil.Amount(1000), inputSource, changeSource) + if err == nil { + t.Error("Failed to return insuffient funds error") + } +} + +func TestBitcoinCashWallet_CreateMultisigSignature(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + ins, outs, redeemScript, err := buildTxData(w) + if err != nil { + t.Error(err) + } + + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + sigs, err := w.CreateMultisigSignature(ins, outs, key1, redeemScript, 50) + if err != nil { + t.Error(err) + } + if len(sigs) != 2 { + t.Error(err) + } + for _, sig := range sigs { + if len(sig.Signature) == 0 { + t.Error("Returned empty signature") + } + } +} + +func buildTxData(w *BitcoinCashWallet) ([]wallet.TransactionInput, []wallet.TransactionOutput, []byte, error) { + redeemScript := "522103c157f2a7c178430972263232c9306110090c50b44d4e906ecd6d377eec89a53c210205b02b9dbe570f36d1c12e3100e55586b2b9dc61d6778c1d24a8eaca03625e7e21030c83b025cd6bdd8c06e93a2b953b821b4a8c29da211335048d7dc3389706d7e853ae" + redeemScriptBytes, err := hex.DecodeString(redeemScript) + if err != nil { + return nil, nil, nil, err + } + h1, err := hex.DecodeString("1a20f4299b4fa1f209428dace31ebf4f23f13abd8ed669cebede118343a6ae05") + if err != nil { + return nil, nil, nil, err + } + in1 := wallet.TransactionInput{ + OutpointHash: h1, + OutpointIndex: 1, + } + h2, err := hex.DecodeString("458d88b4ae9eb4a347f2e7f5592f1da3b9ddf7d40f307f6e5d7bc107a9b3e90e") + if err != nil { + return nil, nil, nil, err + } + in2 := wallet.TransactionInput{ + OutpointHash: h2, + OutpointIndex: 0, + } + addr, err := w.DecodeAddress("ppx5mmammxfs42m0p6ypvf6znnkq3llskvlz0texus") + if err != nil { + return nil, nil, nil, err + } + + out := wallet.TransactionOutput{ + Value: 20000, + Address: addr, + } + return []wallet.TransactionInput{in1, in2}, []wallet.TransactionOutput{out}, redeemScriptBytes, nil +} + +func TestBitcoinCashWallet_Multisign(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + ins, outs, redeemScript, err := buildTxData(w) + if err != nil { + t.Error(err) + } + + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + key2, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + sigs1, err := w.CreateMultisigSignature(ins, outs, key1, redeemScript, 50) + if err != nil { + t.Error(err) + } + if len(sigs1) != 2 { + t.Error(err) + } + sigs2, err := w.CreateMultisigSignature(ins, outs, key2, redeemScript, 50) + if err != nil { + t.Error(err) + } + if len(sigs2) != 2 { + t.Error(err) + } + txBytes, err := w.Multisign(ins, outs, sigs1, sigs2, redeemScript, 50, false) + if err != nil { + t.Error(err) + } + + tx := wire.NewMsgTx(0) + tx.BtcDecode(bytes.NewReader(txBytes), wire.ProtocolVersion, wire.WitnessEncoding) + if len(tx.TxIn) != 2 { + t.Error("Transactions has incorrect number of inputs") + } + if len(tx.TxOut) != 1 { + t.Error("Transactions has incorrect number of outputs") + } + for _, in := range tx.TxIn { + if len(in.SignatureScript) == 0 { + t.Error("Input script has zero length") + } + } +} + +func TestBitcoinCashWallet_bumpFee(t *testing.T) { + w, err := newMockWallet() + w.ws.Start() + time.Sleep(time.Second / 2) + if err != nil { + t.Error(err) + } + txns, err := w.db.Txns().GetAll(false) + if err != nil { + t.Error(err) + } + ch, err := chainhash.NewHashFromStr(txns[2].Txid) + if err != nil { + t.Error(err) + } + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + for _, u := range utxos { + if u.Op.Hash.IsEqual(ch) { + u.AtHeight = 0 + w.db.Utxos().Put(u) + } + } + + w.db.Txns().UpdateHeight(*ch, 0, time.Now()) + + // Test unconfirmed + _, err = w.bumpFee(*ch) + if err != nil { + t.Error(err) + } + + err = w.db.Txns().UpdateHeight(*ch, 1289597, time.Now()) + if err != nil { + t.Error(err) + } + + // Test confirmed + _, err = w.bumpFee(*ch) + if err == nil { + t.Error("Should not be able to bump fee of confirmed txs") + } +} + +func TestBitcoinCashWallet_sweepAddress(t *testing.T) { + w, err := newMockWallet() + w.ws.Start() + time.Sleep(time.Second / 2) + if err != nil { + t.Error(err) + } + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + var in wallet.TransactionInput + var key *hdkeychain.ExtendedKey + for _, ut := range utxos { + if ut.Value > 0 && !ut.WatchOnly { + addr, err := w.ScriptToAddress(ut.ScriptPubkey) + if err != nil { + t.Error(err) + } + key, err = w.km.GetKeyForScript(addr.ScriptAddress()) + if err != nil { + t.Error(err) + } + h, err := hex.DecodeString(ut.Op.Hash.String()) + if err != nil { + t.Error(err) + } + in = wallet.TransactionInput{ + LinkedAddress: addr, + Value: ut.Value, + OutpointIndex: ut.Op.Index, + OutpointHash: h, + } + break + } + } + // P2PKH addr + _, err = w.sweepAddress([]wallet.TransactionInput{in}, nil, key, nil, wallet.NORMAL) + if err != nil { + t.Error(err) + return + } + + // 1 of 2 P2WSH + for _, ut := range utxos { + if ut.Value > 0 && ut.WatchOnly { + h, err := hex.DecodeString(ut.Op.Hash.String()) + if err != nil { + t.Error(err) + } + addr, err := w.ScriptToAddress(ut.ScriptPubkey) + if err != nil { + t.Error(err) + } + in = wallet.TransactionInput{ + LinkedAddress: addr, + Value: ut.Value, + OutpointIndex: ut.Op.Index, + OutpointHash: h, + } + } + } + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + key2, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + _, redeemScript, err := w.GenerateMultisigScript([]hdkeychain.ExtendedKey{*key1, *key2}, 1, 0, nil) + if err != nil { + t.Error(err) + } + _, err = w.sweepAddress([]wallet.TransactionInput{in}, nil, key1, &redeemScript, wallet.NORMAL) + if err != nil { + t.Error(err) + } +} + +func TestBitcoinCashWallet_estimateSpendFee(t *testing.T) { + w, err := newMockWallet() + w.ws.Start() + time.Sleep(time.Second / 2) + if err != nil { + t.Error(err) + } + fee, err := w.estimateSpendFee(1000, wallet.NORMAL) + if err != nil { + t.Error(err) + } + if fee <= 0 { + t.Error("Returned incorrect fee") + } +} diff --git a/bitcoincash/txsizes.go b/bitcoincash/txsizes.go new file mode 100644 index 0000000..0db2af0 --- /dev/null +++ b/bitcoincash/txsizes.go @@ -0,0 +1,249 @@ +package bitcoincash + +// 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/bitcoincash/txsizes_test.go b/bitcoincash/txsizes_test.go new file mode 100644 index 0000000..5243f13 --- /dev/null +++ b/bitcoincash/txsizes_test.go @@ -0,0 +1,84 @@ +package bitcoincash + +// 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 ( + "bytes" + "encoding/hex" + "github.com/btcsuite/btcd/wire" + "testing" +) + +const ( + p2pkhScriptSize = P2PKHPkScriptSize + p2shScriptSize = 23 +) + +func makeInts(value int, n int) []int { + v := make([]int, n) + for i := range v { + v[i] = value + } + return v +} + +func TestEstimateSerializeSize(t *testing.T) { + tests := []struct { + InputCount int + OutputScriptLengths []int + AddChangeOutput bool + ExpectedSizeEstimate int + }{ + 0: {1, []int{}, false, 161}, + 1: {1, []int{p2pkhScriptSize}, false, 195}, + 2: {1, []int{}, true, 195}, + 3: {1, []int{p2pkhScriptSize}, true, 229}, + 4: {1, []int{p2shScriptSize}, false, 193}, + 5: {1, []int{p2shScriptSize}, true, 227}, + + 6: {2, []int{}, false, 310}, + 7: {2, []int{p2pkhScriptSize}, false, 344}, + 8: {2, []int{}, true, 344}, + 9: {2, []int{p2pkhScriptSize}, true, 378}, + 10: {2, []int{p2shScriptSize}, false, 342}, + 11: {2, []int{p2shScriptSize}, true, 376}, + + // 0xfd is discriminant for 16-bit compact ints, compact int + // total size increases from 1 byte to 3. + 12: {1, makeInts(p2pkhScriptSize, 0xfc), false, 8729}, + 13: {1, makeInts(p2pkhScriptSize, 0xfd), false, 8729 + P2PKHOutputSize + 2}, + 14: {1, makeInts(p2pkhScriptSize, 0xfc), true, 8729 + P2PKHOutputSize + 2}, + 15: {0xfc, []int{}, false, 37560}, + 16: {0xfd, []int{}, false, 37560 + RedeemP2PKHInputSize + 2}, + } + for i, test := range tests { + outputs := make([]*wire.TxOut, 0, len(test.OutputScriptLengths)) + for _, l := range test.OutputScriptLengths { + outputs = append(outputs, &wire.TxOut{PkScript: make([]byte, l)}) + } + actualEstimate := EstimateSerializeSize(test.InputCount, outputs, test.AddChangeOutput, P2PKH) + if actualEstimate != test.ExpectedSizeEstimate { + t.Errorf("Test %d: Got %v: Expected %v", i, actualEstimate, test.ExpectedSizeEstimate) + } + } +} + +func TestSumOutputSerializeSizes(t *testing.T) { + testTx := "0100000001066b78efa7d66d271cae6d6eb799e1d10953fb1a4a760226cc93186d52b55613010000006a47304402204e6c32cc214c496546c3277191ca734494fe49fed0af1d800db92fed2021e61802206a14d063b67f2f1c8fc18f9e9a5963fe33e18c549e56e3045e88b4fc6219be11012103f72d0a11727219bff66b8838c3c5e1c74a5257a325b0c84247bd10bdb9069e88ffffffff0200c2eb0b000000001976a914426e80ad778792e3e19c20977fb93ec0591e1a3988ac35b7cb59000000001976a914e5b6dc0b297acdd99d1a89937474df77db5743c788ac00000000" + txBytes, err := hex.DecodeString(testTx) + if err != nil { + t.Error(err) + return + } + r := bytes.NewReader(txBytes) + msgTx := wire.NewMsgTx(1) + msgTx.BtcDecode(r, 1, wire.WitnessEncoding) + if SumOutputSerializeSizes(msgTx.TxOut) != 68 { + t.Error("SumOutputSerializeSizes returned incorrect value") + } + +} diff --git a/bitcoincash/wallet.go b/bitcoincash/wallet.go new file mode 100644 index 0000000..3b69435 --- /dev/null +++ b/bitcoincash/wallet.go @@ -0,0 +1,482 @@ +package bitcoincash + +import ( + "bytes" + "encoding/hex" + "fmt" + "github.com/op/go-logging" + "io" + "log" + "time" + + 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/btcsuite/btcwallet/wallet/txrules" + "github.com/cpacia/bchutil" + "github.com/tyler-smith/go-bip39" + "golang.org/x/net/proxy" + + "github.com/OpenBazaar/multiwallet/cache" + "github.com/OpenBazaar/multiwallet/client" + "github.com/OpenBazaar/multiwallet/config" + "github.com/OpenBazaar/multiwallet/keys" + "github.com/OpenBazaar/multiwallet/model" + "github.com/OpenBazaar/multiwallet/service" + "github.com/OpenBazaar/multiwallet/util" +) + +type BitcoinCashWallet 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(&BitcoinCashWallet{}) + +func NewBitcoinCashWallet(cfg config.CoinConfig, mnemonic string, params *chaincfg.Params, proxy proxy.Dialer, cache cache.Cacher, disableExchangeRates bool) (*BitcoinCashWallet, 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.BitcoinCash, bitcoinCashAddress) + 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.BitcoinCash, cache) + if err != nil { + return nil, err + } + exchangeRates := NewBitcoinCashPriceFetcher(proxy) + if !disableExchangeRates { + go exchangeRates.Run() + } + + fp := util.NewFeeProvider(cfg.MaxFee, cfg.HighFee, cfg.MediumFee, cfg.LowFee, exchangeRates) + + return &BitcoinCashWallet{ + db: cfg.DB, + km: km, + params: params, + client: c, + ws: wm, + fp: fp, + mPrivKey: mPrivKey, + mPubKey: mPubKey, + exchangeRates: exchangeRates, + log: logging.MustGetLogger("bitcoin-cash-wallet"), + }, nil +} + +func bitcoinCashAddress(key *hd.ExtendedKey, params *chaincfg.Params) (btcutil.Address, error) { + addr, err := key.Address(params) + if err != nil { + return nil, err + } + return bchutil.NewCashAddressPubKeyHash(addr.ScriptAddress(), params) +} + +func (w *BitcoinCashWallet) Start() { + w.client.Start() + w.ws.Start() +} + +func (w *BitcoinCashWallet) Params() *chaincfg.Params { + return w.params +} + +func (w *BitcoinCashWallet) CurrencyCode() string { + if w.params.Name == chaincfg.MainNetParams.Name { + return "bch" + } else { + return "tbch" + } +} + +func (w *BitcoinCashWallet) IsDust(amount int64) bool { + return txrules.IsDustAmount(btcutil.Amount(amount), 25, txrules.DefaultRelayFeePerKb) +} + +func (w *BitcoinCashWallet) MasterPrivateKey() *hd.ExtendedKey { + return w.mPrivKey +} + +func (w *BitcoinCashWallet) MasterPublicKey() *hd.ExtendedKey { + return w.mPubKey +} + +func (w *BitcoinCashWallet) 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 *BitcoinCashWallet) CurrentAddress(purpose wi.KeyPurpose) btcutil.Address { + 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) + } + return addr +} + +func (w *BitcoinCashWallet) NewAddress(purpose wi.KeyPurpose) btcutil.Address { + 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) + } + return addr +} + +func (w *BitcoinCashWallet) DecodeAddress(addr string) (btcutil.Address, error) { + return bchutil.DecodeAddress(addr, w.params) +} + +func (w *BitcoinCashWallet) ScriptToAddress(script []byte) (btcutil.Address, error) { + return bchutil.ExtractPkScriptAddrs(script, w.params) +} + +func (w *BitcoinCashWallet) AddressToScript(addr btcutil.Address) ([]byte, error) { + return bchutil.PayToAddrScript(addr) +} + +func (w *BitcoinCashWallet) HasKey(addr btcutil.Address) bool { + _, err := w.km.GetKeyForScript(addr.ScriptAddress()) + if err != nil { + return false + } + return true +} + +func (w *BitcoinCashWallet) Balance() (confirmed, unconfirmed int64) { + utxos, _ := w.db.Utxos().GetAll() + txns, _ := w.db.Txns().GetAll(false) + return util.CalcBalance(utxos, txns) +} + +func (w *BitcoinCashWallet) 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 < 6: + status = wi.StatusPending + confirmations = confs + case confs > 5: + status = wi.StatusConfirmed + confirmations = confs + } + tx.Confirmations = int64(confirmations) + tx.Status = status + txns[i] = tx + } + return txns, nil +} + +func (w *BitcoinCashWallet) 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 := bchutil.ExtractPkScriptAddrs(out.PkScript, w.params) + if err != nil { + log.Printf("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 *BitcoinCashWallet) ChainTip() (uint32, chainhash.Hash) { + return w.ws.ChainTip() +} + +func (w *BitcoinCashWallet) GetFeePerByte(feeLevel wi.FeeLevel) uint64 { + return w.fp.GetFeePerByte(feeLevel) +} + +func (w *BitcoinCashWallet) 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 *BitcoinCashWallet) BumpFee(txid chainhash.Hash) (*chainhash.Hash, error) { + return w.bumpFee(txid) +} + +func (w *BitcoinCashWallet) EstimateFee(ins []wi.TransactionInput, outs []wi.TransactionOutput, feePerByte uint64) uint64 { + tx := new(wire.MsgTx) + for _, out := range outs { + scriptPubKey, _ := bchutil.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 *BitcoinCashWallet) EstimateSpendFee(amount int64, feeLevel wi.FeeLevel) (uint64, error) { + return w.estimateSpendFee(amount, feeLevel) +} + +func (w *BitcoinCashWallet) 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 *BitcoinCashWallet) 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 *BitcoinCashWallet) 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 *BitcoinCashWallet) 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 *BitcoinCashWallet) 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 *BitcoinCashWallet) 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 *BitcoinCashWallet) AddTransactionListener(callback func(wi.TransactionCallback)) { + w.ws.AddTransactionListener(callback) +} + +func (w *BitcoinCashWallet) ReSyncBlockchain(fromTime time.Time) { + go w.ws.UpdateState() +} + +func (w *BitcoinCashWallet) 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 *BitcoinCashWallet) Close() { + w.ws.Stop() + w.client.Close() +} + +func (w *BitcoinCashWallet) ExchangeRates() wi.ExchangeRates { + return w.exchangeRates +} + +func (w *BitcoinCashWallet) 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) + } +} + +// Build a client.Transaction so we can ingest it into the wallet service then broadcast +func (w *BitcoinCashWallet) Broadcast(tx *wire.MsgTx) error { + var buf bytes.Buffer + tx.BtcEncode(&buf, wire.ProtocolVersion, wire.BaseEncoding) + 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.BitcoinCash), + } + 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 *BitcoinCashWallet) AssociateTransactionWithOrder(cb wi.TransactionCallback) { + w.ws.InvokeTransactionListeners(cb) +} diff --git a/cache/cacher.go b/cache/cacher.go new file mode 100644 index 0000000..84581df --- /dev/null +++ b/cache/cacher.go @@ -0,0 +1,39 @@ +package cache + +import ( + "fmt" + "sync" +) + +type Cacher interface { + Set(string, []byte) error + Get(string) ([]byte, error) +} + +func NewMockCacher() Cacher { + return &exampleWithNoPersistence{ + kv: make(map[string][]byte), + } +} + +type exampleWithNoPersistence struct { + lock sync.RWMutex + kv map[string][]byte +} + +func (e *exampleWithNoPersistence) Set(key string, value []byte) error { + e.lock.Lock() + e.kv[key] = value + e.lock.Unlock() + return nil +} + +func (e *exampleWithNoPersistence) Get(key string) ([]byte, error) { + e.lock.RLock() + value, ok := e.kv[key] + e.lock.RUnlock() + if !ok { + return nil, fmt.Errorf("cached key not found") + } + return value, nil +} diff --git a/cache/cacher_example_test.go b/cache/cacher_example_test.go new file mode 100644 index 0000000..6de320a --- /dev/null +++ b/cache/cacher_example_test.go @@ -0,0 +1,55 @@ +package cache_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/OpenBazaar/multiwallet/cache" +) + +type testStructSubject struct { + StringType string + IntType int + TimeType time.Time +} + +func TestSettingGettingStructs(t *testing.T) { + var ( + subject = testStructSubject{ + StringType: "teststring", + IntType: 123456, + TimeType: time.Now(), + } + cacher = cache.NewMockCacher() + ) + marshalledSubject, err := json.Marshal(subject) + if err != nil { + t.Fatal(err) + } + err = cacher.Set("thing1", marshalledSubject) + if err != nil { + t.Fatal(err) + } + + marshalledThing, err := cacher.Get("thing1") + if err != nil { + t.Fatal(err) + } + + var actual testStructSubject + err = json.Unmarshal(marshalledThing, &actual) + if err != nil { + t.Fatal(err) + } + + if subject.StringType != actual.StringType { + t.Error("expected StringType to match but did not") + } + if subject.IntType != actual.IntType { + t.Error("expected IntType to match but did not") + } + if !subject.TimeType.Equal(actual.TimeType) { + t.Error("expected TimeType to match but did not") + } +} diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 0000000..fcd61dc --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,323 @@ +package cli + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "github.com/OpenBazaar/multiwallet/api" + "github.com/OpenBazaar/multiwallet/api/pb" + "github.com/jessevdk/go-flags" + "golang.org/x/net/context" + "google.golang.org/grpc" +) + +func SetupCli(parser *flags.Parser) { + // Add commands to parser + parser.AddCommand("stop", + "stop the wallet", + "The stop command disconnects from peers and shuts down the wallet", + &stop) + parser.AddCommand("currentaddress", + "get the current bitcoin address", + "Returns the first unused address in the keychain\n\n"+ + "Args:\n"+ + "1. coinType (string)\n"+ + "2. purpose (string default=external) The purpose for the address. Can be external for receiving from outside parties or internal for example, for change.\n\n"+ + "Examples:\n"+ + "> multiwallet currentaddress bitcoin\n"+ + "1DxGWC22a46VPEjq8YKoeVXSLzB7BA8sJS\n"+ + "> multiwallet currentaddress bitcoin internal\n"+ + "18zAxgfKx4NuTUGUEuB8p7FKgCYPM15DfS\n", + ¤tAddress) + parser.AddCommand("newaddress", + "get a new bitcoin address", + "Returns a new unused address in the keychain. Use caution when using this function as generating too many new addresses may cause the keychain to extend further than the wallet's lookahead window, meaning it might fail to recover all transactions when restoring from seed. CurrentAddress is safer as it never extends past the lookahead window.\n\n"+ + "Args:\n"+ + "1. coinType (string)\n"+ + "2. purpose (string default=external) The purpose for the address. Can be external for receiving from outside parties or internal for example, for change.\n\n"+ + "Examples:\n"+ + "> multiwallet newaddress bitcoin\n"+ + "1DxGWC22a46VPEjq8YKoeVXSLzB7BA8sJS\n"+ + "> multiwallet newaddress bitcoin internal\n"+ + "18zAxgfKx4NuTUGUEuB8p7FKgCYPM15DfS\n", + &newAddress) + parser.AddCommand("chaintip", + "return the height of the chain", + "Returns the height of the best chain of blocks", + &chainTip) + parser.AddCommand("dumptables", + "print out the database tables", + "Prints each row in the database tables", + &dumpTables) + parser.AddCommand("spend", + "send bitcoins", + "Send bitcoins to the given address\n\n"+ + "Args:\n"+ + "1. coinType (string)\n"+ + "2. address (string) The recipient's bitcoin address\n"+ + "3. amount (integer) The amount to send in satoshi"+ + "4. feelevel (string default=normal) The fee level: economic, normal, priority\n\n"+ + "5. memo (string) The orderID\n"+ + "Examples:\n"+ + "> multiwallet spend bitcoin 1DxGWC22a46VPEjq8YKoeVXSLzB7BA8sJS 1000000\n"+ + "82bfd45f3564e0b5166ab9ca072200a237f78499576e9658b20b0ccd10ff325c 1a3w"+ + "> multiwallet spend bitcoin 1DxGWC22a46VPEjq8YKoeVXSLzB7BA8sJS 3000000000 priority\n"+ + "82bfd45f3564e0b5166ab9ca072200a237f78499576e9658b20b0ccd10ff325c 4wq2", + &spend) + parser.AddCommand("balance", + "get the wallet's balances", + "Returns the confirmed and unconfirmed balances for the specified coin", + &balance) +} + +func coinType(args []string) pb.CoinType { + if len(args) == 0 { + return pb.CoinType_BITCOIN + } + switch strings.ToLower(args[0]) { + case "bitcoin": + return pb.CoinType_BITCOIN + case "bitcoincash": + return pb.CoinType_BITCOIN_CASH + case "zcash": + return pb.CoinType_ZCASH + case "litecoin": + return pb.CoinType_LITECOIN + case "ethereum": + return pb.CoinType_ETHEREUM + default: + return pb.CoinType_BITCOIN + } +} + +func newGRPCClient() (pb.APIClient, *grpc.ClientConn, error) { + // Set up a connection to the server. + conn, err := grpc.Dial(api.Addr, grpc.WithInsecure()) + if err != nil { + return nil, nil, err + } + client := pb.NewAPIClient(conn) + return client, conn, nil +} + +type Stop struct{} + +var stop Stop + +func (x *Stop) Execute(args []string) error { + client, conn, err := newGRPCClient() + if err != nil { + return err + } + defer conn.Close() + client.Stop(context.Background(), &pb.Empty{}) + return nil +} + +type CurrentAddress struct{} + +var currentAddress CurrentAddress + +func (x *CurrentAddress) Execute(args []string) error { + client, conn, err := newGRPCClient() + if err != nil { + return err + } + defer conn.Close() + var purpose pb.KeyPurpose + userSelection := "" + + t := coinType(args) + if len(args) == 1 { + userSelection = args[0] + } else if len(args) == 2 { + userSelection = args[1] + } + switch strings.ToLower(userSelection) { + case "internal": + purpose = pb.KeyPurpose_INTERNAL + case "external": + purpose = pb.KeyPurpose_EXTERNAL + default: + purpose = pb.KeyPurpose_EXTERNAL + } + + resp, err := client.CurrentAddress(context.Background(), &pb.KeySelection{Coin: t, Purpose: purpose}) + if err != nil { + return err + } + fmt.Println(resp.Addr) + return nil +} + +type NewAddress struct{} + +var newAddress NewAddress + +func (x *NewAddress) Execute(args []string) error { + client, conn, err := newGRPCClient() + if err != nil { + return err + } + defer conn.Close() + if len(args) == 0 { + return errors.New("Must select coin type") + } + t := coinType(args) + var purpose pb.KeyPurpose + userSelection := "" + if len(args) == 1 { + userSelection = args[0] + } else if len(args) == 2 { + userSelection = args[1] + } + switch strings.ToLower(userSelection) { + case "internal": + purpose = pb.KeyPurpose_INTERNAL + case "external": + purpose = pb.KeyPurpose_EXTERNAL + default: + purpose = pb.KeyPurpose_EXTERNAL + } + resp, err := client.NewAddress(context.Background(), &pb.KeySelection{Coin: t, Purpose: purpose}) + if err != nil { + return err + } + fmt.Println(resp.Addr) + return nil +} + +type ChainTip struct{} + +var chainTip ChainTip + +func (x *ChainTip) Execute(args []string) error { + client, conn, err := newGRPCClient() + if err != nil { + return err + } + defer conn.Close() + if len(args) == 0 { + return errors.New("Must select coin type") + } + t := coinType(args) + resp, err := client.ChainTip(context.Background(), &pb.CoinSelection{Coin: t}) + if err != nil { + return err + } + fmt.Println(resp.Height) + return nil +} + +type DumpTables struct{} + +var dumpTables DumpTables + +func (x *DumpTables) Execute(args []string) error { + client, conn, err := newGRPCClient() + if err != nil { + return err + } + defer conn.Close() + if len(args) == 0 { + return errors.New("Must select coin type") + } + t := coinType(args) + resp, err := client.DumpTables(context.Background(), &pb.CoinSelection{Coin: t}) + if err != nil { + return err + } + for { + row, err := resp.Recv() + if err != nil { + // errors when no more rows and exits + return err + } + fmt.Println(row.Data) + } +} + +type Spend struct{} + +var spend Spend + +func (x *Spend) Execute(args []string) error { + var ( + address string + feeLevel pb.FeeLevel + referenceID string + userSelection string + + client, conn, err = newGRPCClient() + ) + if err != nil { + return err + } + defer conn.Close() + + if len(args) == 0 { + return errors.New("Must select coin type") + } + if len(args) > 4 { + address = args[1] + userSelection = args[3] + referenceID = args[4] + } + if len(args) < 4 { + return errors.New("Address and amount are required") + } + + switch strings.ToLower(userSelection) { + case "economic": + feeLevel = pb.FeeLevel_ECONOMIC + case "normal": + feeLevel = pb.FeeLevel_NORMAL + case "priority": + feeLevel = pb.FeeLevel_PRIORITY + default: + feeLevel = pb.FeeLevel_NORMAL + } + + amt, err := strconv.Atoi(args[2]) + if err != nil { + return err + } + + resp, err := client.Spend(context.Background(), &pb.SpendInfo{ + Coin: coinType(args), + Address: address, + Amount: uint64(amt), + FeeLevel: feeLevel, + Memo: referenceID, + }) + if err != nil { + return err + } + + fmt.Println(resp.Hash) + return nil +} + +type Balance struct{} + +var balance Balance + +func (x *Balance) Execute(args []string) error { + client, conn, err := newGRPCClient() + if err != nil { + return err + } + defer conn.Close() + if len(args) == 0 { + return errors.New("Must select coin type") + } + t := coinType(args) + resp, err := client.Balance(context.Background(), &pb.CoinSelection{Coin: t}) + if err != nil { + return err + } + fmt.Printf("Confirmed: %d, Unconfirmed: %d\n", resp.Confirmed, resp.Unconfirmed) + return nil +} diff --git a/client/blockbook/client.go b/client/blockbook/client.go new file mode 100644 index 0000000..491871e --- /dev/null +++ b/client/blockbook/client.go @@ -0,0 +1,774 @@ +package blockbook + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "path" + "strconv" + "strings" + "sync" + "time" + + gosocketio "github.com/OpenBazaar/golang-socketio" + "github.com/OpenBazaar/golang-socketio/protocol" + clientErr "github.com/OpenBazaar/multiwallet/client/errors" + "github.com/OpenBazaar/multiwallet/client/transport" + "github.com/OpenBazaar/multiwallet/model" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcutil" + "github.com/cpacia/bchutil" + "github.com/op/go-logging" + "golang.org/x/net/proxy" +) + +const maxInfightQueries = 25 + +var Log = logging.MustGetLogger("client") + +type wsWatchdog struct { + client *BlockBookClient + done chan struct{} + wsStopped chan struct{} +} + +func newWebsocketWatchdog(client *BlockBookClient) *wsWatchdog { + return &wsWatchdog{ + client: client, + done: make(chan struct{}, 0), + wsStopped: make(chan struct{}, 1), + } +} + +func (w *wsWatchdog) guardWebsocket() { + var t = time.NewTicker(1 * time.Second) + defer t.Stop() + for range t.C { + select { + case <-w.wsStopped: + Log.Warningf("reconnecting stopped websocket (%s)", w.client.String()) + w.client.socketMutex.Lock() + if w.client.SocketClient != nil { + w.client.SocketClient.Close() + w.client.SocketClient = nil + } + w.drainAndRollover() + if err := w.client.setupListeners(); err != nil { + Log.Warningf("failed reconnecting websocket (%s)", w.client.String()) + w.client.socketMutex.Unlock() + w.client.sendAndDiscardCloseChan(fmt.Errorf("websocket unavailable (%s): %s", w.client.String(), err.Error())) + go w.putAway() + return + } + w.client.socketMutex.Unlock() + case <-w.done: + return + } + } +} + +func (w *wsWatchdog) drainAndRollover() { + for { + select { + case <-w.wsStopped: + default: + return + } + } +} + +func (w *wsWatchdog) bark() { + select { + case w.wsStopped <- struct{}{}: + default: + } +} + +func (w *wsWatchdog) putAway() { + select { + case w.done <- struct{}{}: + default: + } +} + +type BlockBookClient struct { + apiUrl *url.URL + blockNotifyChan chan model.Block + closeChan chan<- error + listenLock sync.Mutex + listenQueue []string + proxyDialer proxy.Dialer + txNotifyChan chan model.Transaction + websocketWatchdog *wsWatchdog + + HTTPClient http.Client + RequestFunc func(endpoint, method string, body []byte, query url.Values) (*http.Response, error) + SocketClient model.SocketClient + socketMutex sync.RWMutex +} + +func NewBlockBookClient(apiUrl string, proxyDialer proxy.Dialer) (*BlockBookClient, error) { + u, err := url.Parse(apiUrl) + if err != nil { + return nil, err + } + + if err := validateScheme(u); err != nil { + return nil, err + } + + var customClient http.Client + if proxyDialer != nil { + dial := proxyDialer.Dial + tbTransport := &http.Transport{Dial: dial} + customClient = http.Client{Timeout: time.Second * 30, Transport: tbTransport} + } else { + customClient = http.Client{Timeout: time.Second * 30} + } + + bch := make(chan model.Block) + tch := make(chan model.Transaction) + + ic := &BlockBookClient{ + HTTPClient: customClient, + apiUrl: u, + proxyDialer: proxyDialer, + blockNotifyChan: bch, + txNotifyChan: tch, + listenLock: sync.Mutex{}, + } + ic.websocketWatchdog = newWebsocketWatchdog(ic) + ic.RequestFunc = ic.doRequest + return ic, nil +} + +func (i *BlockBookClient) String() string { + return i.apiUrl.Host +} + +func (i *BlockBookClient) BlockChannel() chan model.Block { + return i.blockNotifyChan +} + +func (i *BlockBookClient) TxChannel() chan model.Transaction { + return i.txNotifyChan +} + +func (i *BlockBookClient) EndpointURL() *url.URL { + var u = *i.apiUrl + return &u +} + +func (i *BlockBookClient) Start(closeChan chan<- error) error { + i.socketMutex.Lock() + defer i.socketMutex.Unlock() + if err := i.setupListeners(); err != nil { + return err + } + i.closeChan = closeChan + return nil +} + +func (i *BlockBookClient) Close() { + Log.Infof("closing client (%s)...", i.String()) + i.socketMutex.Lock() + defer i.socketMutex.Unlock() + if i.SocketClient != nil { + if i.websocketWatchdog != nil { + go i.websocketWatchdog.putAway() + } + i.SocketClient.Close() + i.SocketClient = nil + } + i.sendAndDiscardCloseChan(nil) +} + +func (i *BlockBookClient) sendAndDiscardCloseChan(err error) { + if i.closeChan != nil { + i.closeChan <- err + i.closeChan = nil + } +} + +func validateScheme(target *url.URL) error { + switch target.Scheme { + case "https", "http": + return nil + } + return fmt.Errorf("unsupported scheme: %s", target.Scheme) +} + +func (i *BlockBookClient) doRequest(endpoint, method string, body []byte, query url.Values) (*http.Response, error) { + requestUrl := i.EndpointURL() + requestUrl.Path = path.Join(i.EndpointURL().Path, endpoint) + req, err := http.NewRequest(method, requestUrl.String(), bytes.NewReader(body)) + if query != nil { + req.URL.RawQuery = query.Encode() + } + if err != nil { + Log.Errorf("creating: %s", err.Error()) + return nil, fmt.Errorf("creating: %s", err.Error()) + } + req.Header.Add("Content-Type", "application/json") + + resp, err := i.HTTPClient.Do(req) + if err != nil { + if urlErr, ok := err.(*url.Error); ok && urlErr.Timeout() { + Log.Errorf("timed out executing: %s", err.Error()) + return nil, clientErr.MakeRetryable(err) + } + Log.Errorf("executing: %s", err.Error()) + return nil, fmt.Errorf("executing: %s", err.Error()) + } + if resp.StatusCode != http.StatusOK { + errStr := fmt.Sprintf("not ok (%s %s): responded %s", method, requestUrl.String(), resp.Status) + Log.Errorf(errStr) + + // log body + if body, err := ioutil.ReadAll(resp.Body); err != nil { + Log.Warningf("reading body (%s %s): %s", method, requestUrl.String(), err.Error()) + } else { + if len(body) > 0 { + Log.Debugf("not ok response body (%s %s):\n\tstring: %s\n\thexencoded: %s", method, requestUrl.String(), string(body), hex.EncodeToString(body)) + } + } + + // mark 500 errors as fatal + if resp.StatusCode >= 500 { + err := fmt.Errorf("wallet server internal error (%s %s)", method, requestUrl.String()) + return nil, clientErr.MakeRetryable(clientErr.MakeFatal(err)) + } + return nil, fmt.Errorf("status not ok: %s", resp.Status) + } + return resp, nil +} + +// GetInfo is unused for now so we will not implement it yet +func (i *BlockBookClient) GetInfo() (*model.Info, error) { + return nil, nil +} + +func (i *BlockBookClient) GetTransaction(txid string) (*model.Transaction, error) { + type resIn struct { + model.Input + Addresses []string `json:"addresses"` + } + type resOut struct { + model.Output + Spent bool `json:"spent"` + } + type resTx struct { + model.Transaction + Hex string `json:"hex"` + Vin []resIn `json:"vin"` + Vout []resOut `json:"vout"` + } + resp, err := i.RequestFunc("tx/"+txid, http.MethodGet, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + tx := new(resTx) + decoder := json.NewDecoder(resp.Body) + if err = decoder.Decode(tx); err != nil { + return nil, fmt.Errorf("error decoding transactions: %s", err) + } + for n, in := range tx.Vin { + f, err := model.ToFloat(in.ValueIface) + if err != nil { + return nil, err + } + tx.Vin[n].Value = f + } + for n, out := range tx.Vout { + f, err := model.ToFloat(out.ValueIface) + if err != nil { + return nil, err + } + tx.Vout[n].Value = f + } + raw, err := hex.DecodeString(tx.Hex) + if err != nil { + return nil, err + } + ctx := model.Transaction{ + BlockHash: tx.BlockHash, + BlockHeight: tx.BlockHeight, + BlockTime: tx.BlockTime, + Confirmations: tx.Confirmations, + Locktime: tx.Locktime, + RawBytes: raw, + Time: tx.Time, + Txid: tx.Txid, + Version: tx.Version, + } + for n, i := range tx.Vin { + newIn := model.Input{ + Addr: i.Addr, + N: i.N, + Satoshis: i.Satoshis, + ScriptSig: i.ScriptSig, + Sequence: i.Sequence, + Txid: i.Txid, + Value: tx.Vin[n].Value, + Vout: i.Vout, + } + if len(i.Addresses) > 0 { + newIn.Addr = maybeTrimCashAddrPrefix(i.Addresses[0]) + } + ctx.Inputs = append(ctx.Inputs, newIn) + } + for n, o := range tx.Vout { + newOut := model.Output{ + Value: tx.Vout[n].Value, + N: o.N, + ScriptPubKey: o.ScriptPubKey, + } + for i, addr := range newOut.ScriptPubKey.Addresses { + newOut.ScriptPubKey.Addresses[i] = maybeTrimCashAddrPrefix(addr) + } + ctx.Outputs = append(ctx.Outputs, newOut) + } + return &ctx, nil +} + +// GetRawTransaction is unused for now so we will not implement it yet +func (i *BlockBookClient) GetRawTransaction(txid string) ([]byte, error) { + return nil, nil +} + +// GetTransactions returns the transactions for a given address. If a single address +// query fails this method will not return an error. Instead it will log the error +// and returns the transactions for the other addresses. +func (i *BlockBookClient) GetTransactions(addrs []btcutil.Address) ([]model.Transaction, error) { + var txs []model.Transaction + type txsOrError struct { + Txs []model.Transaction + Err error + } + var ( + txChan = make(chan txsOrError) + queryChan = make(chan struct{}, maxInfightQueries) + wg sync.WaitGroup + ) + wg.Add(len(addrs)) + go func() { + for _, addr := range addrs { + queryChan <- struct{}{} + go func(a btcutil.Address) { + txs, err := i.getTransactions(maybeConvertCashAddress(a)) + txChan <- txsOrError{txs, err} + <-queryChan + wg.Done() + }(addr) + } + wg.Wait() + close(txChan) + }() + for toe := range txChan { + if toe.Err != nil { + Log.Errorf("Error querying address from blockbook: %s", toe.Err.Error()) + return nil, toe.Err + } + txs = append(txs, toe.Txs...) + } + return txs, nil +} + +func (i *BlockBookClient) getTransactions(addr string) ([]model.Transaction, error) { + var ret []model.Transaction + type resAddr struct { + TotalPages int `json:"totalPages"` + Transactions []string `json:"transactions"` + } + type txOrError struct { + Tx *model.Transaction + Err error + } + page := 1 + for { + q, err := url.ParseQuery("?page=" + strconv.Itoa(page)) + if err != nil { + return nil, err + } + resp, err := i.RequestFunc("/address/"+addr, http.MethodGet, nil, q) + if err != nil { + return nil, err + } + defer resp.Body.Close() + res := new(resAddr) + decoder := json.NewDecoder(resp.Body) + if err = decoder.Decode(res); err != nil { + return nil, fmt.Errorf("error decoding addrs response: %s", err) + } + txChan := make(chan txOrError) + go func() { + var wg sync.WaitGroup + wg.Add(len(res.Transactions)) + for _, txid := range res.Transactions { + go func(id string) { + tx, err := i.GetTransaction(id) + txChan <- txOrError{tx, err} + wg.Done() + }(txid) + } + wg.Wait() + close(txChan) + }() + for toe := range txChan { + if toe.Err != nil { + return nil, err + } + if toe.Tx != nil { + ret = append(ret, *toe.Tx) + } + } + if res.TotalPages <= page { + break + } + page++ + } + return ret, nil +} + +// GetUtxos returns the utxos for a given address. If a single address +// query fails this method will not return an error. Instead it will log the error +// and returns the transactions for the other addresses. +func (i *BlockBookClient) GetUtxos(addrs []btcutil.Address) ([]model.Utxo, error) { + var ret []model.Utxo + type utxoOrError struct { + Utxo *model.Utxo + Err error + } + var ( + wg sync.WaitGroup + queryChan = make(chan struct{}, maxInfightQueries) + utxoChan = make(chan utxoOrError) + ) + wg.Add(len(addrs)) + go func() { + for _, addr := range addrs { + queryChan <- struct{}{} + go func(addr btcutil.Address) { + defer wg.Done() + defer func() { + <-queryChan + }() + resp, err := i.RequestFunc("/utxo/"+maybeConvertCashAddress(addr), http.MethodGet, nil, nil) + if err != nil { + utxoChan <- utxoOrError{nil, err} + return + } + defer resp.Body.Close() + var utxos []model.Utxo + decoder := json.NewDecoder(resp.Body) + if err = decoder.Decode(&utxos); err != nil { + utxoChan <- utxoOrError{nil, err} + return + } + for z, u := range utxos { + f, err := model.ToFloat(u.AmountIface) + if err != nil { + utxoChan <- utxoOrError{nil, err} + return + } + utxos[z].Amount = f + } + var wg2 sync.WaitGroup + wg2.Add(len(utxos)) + for _, u := range utxos { + go func(ut model.Utxo) { + defer wg2.Done() + + tx, err := i.GetTransaction(ut.Txid) + if err != nil { + utxoChan <- utxoOrError{nil, err} + return + } + if len(tx.Outputs)-1 < ut.Vout { + utxoChan <- utxoOrError{nil, errors.New("transaction has invalid number of outputs")} + return + } + ut.ScriptPubKey = tx.Outputs[ut.Vout].ScriptPubKey.Hex + if len(tx.Outputs[ut.Vout].ScriptPubKey.Addresses[0]) > 0 { + ut.Address = maybeTrimCashAddrPrefix(tx.Outputs[ut.Vout].ScriptPubKey.Addresses[0]) + } + utxoChan <- utxoOrError{&ut, nil} + }(u) + } + wg2.Wait() + }(addr) + } + wg.Wait() + close(utxoChan) + }() + for toe := range utxoChan { + if toe.Err != nil { + Log.Errorf("Error querying utxos from blockbook: %s", toe.Err.Error()) + return nil, toe.Err + } + if toe.Utxo != nil { + ret = append(ret, *toe.Utxo) + } + } + return ret, nil +} + +func (i *BlockBookClient) BlockNotify() <-chan model.Block { + return i.blockNotifyChan +} + +func (i *BlockBookClient) TransactionNotify() <-chan model.Transaction { + return i.txNotifyChan +} + +func (i *BlockBookClient) ListenAddresses(addrs ...btcutil.Address) { + if len(addrs) == 0 { + return + } + + i.listenLock.Lock() + defer i.listenLock.Unlock() + i.socketMutex.RLock() + defer i.socketMutex.RUnlock() + + var convertedAddrs []string + for _, addr := range addrs { + convertedAddrs = append(convertedAddrs, maybeConvertCashAddress(addr)) + } + + if i.SocketClient != nil { + i.SocketClient.Emit("subscribe", []interface{}{"bitcoind/addresstxid", convertedAddrs}) + } else { + i.listenQueue = append(i.listenQueue, convertedAddrs...) + } +} + +func connectSocket(u *url.URL, proxyDialer proxy.Dialer) (model.SocketClient, error) { + socketClient, err := gosocketio.Dial( + gosocketio.GetUrl(u.Hostname(), model.DefaultPort(u), model.HasImpliedURLSecurity(u)), + transport.GetDefaultWebsocketTransport(proxyDialer), + ) + if err != nil { + return nil, err + } + + // Signal readyness on connection + socketReady := make(chan struct{}) + socketClient.On(gosocketio.OnConnection, func(h *gosocketio.Channel, args interface{}) { + close(socketReady) + }) + + // Wait for socket to be ready or timeout + select { + case <-time.After(10 * time.Second): + return nil, fmt.Errorf("websocket connection timeout (%s)", u.Host) + case <-socketReady: + break + } + return socketClient, nil +} + +func (i *BlockBookClient) setupListeners() error { + i.listenLock.Lock() + defer i.listenLock.Unlock() + + if i.SocketClient != nil { + return nil + } + + client, err := connectSocket(i.apiUrl, i.proxyDialer) + if err != nil { + Log.Errorf("reconnect websocket (%s): %s", i.String(), err.Error()) + var ( + setupTimeoutAt = time.Now().Add(10 * time.Second) + t = time.NewTicker(2 * time.Second) + ) + defer t.Stop() + for range t.C { + if time.Now().After(setupTimeoutAt) { + return fmt.Errorf("websocket reconnection timeout (%s)", i.String()) + } + client, err = connectSocket(i.apiUrl, i.proxyDialer) + if err != nil { + Log.Errorf("reconnect websocket (%s): %s", i.String(), err.Error()) + continue + } + break + } + } + i.SocketClient = client + go i.websocketWatchdog.guardWebsocket() + + i.SocketClient.On(gosocketio.OnError, func(c *gosocketio.Channel, args interface{}) { + Log.Warningf("websocket error (%s): %+v", i.String(), "-", args) + i.websocketWatchdog.bark() + }) + i.SocketClient.On(gosocketio.OnDisconnection, func(c *gosocketio.Channel) { + Log.Warningf("websocket disconnected (%s)", i.String()) + i.websocketWatchdog.bark() + }) + + i.SocketClient.On("bitcoind/hashblock", func(h *gosocketio.Channel, arg interface{}) { + best, err := i.GetBestBlock() + if err != nil { + Log.Errorf("error downloading best block: %s", err.Error()) + return + } + i.blockNotifyChan <- *best + }) + i.SocketClient.Emit("subscribe", protocol.ToArgArray("bitcoind/hashblock")) + + i.SocketClient.On("bitcoind/addresstxid", func(h *gosocketio.Channel, arg interface{}) { + m, ok := arg.(map[string]interface{}) + if !ok { + Log.Errorf("error checking type after socket notification: %T", arg) + return + } + for _, v := range m { + txid, ok := v.(string) + if !ok { + Log.Errorf("error checking type after socket notification: %T", arg) + return + } + _, err := chainhash.NewHashFromStr(txid) // Check is 256 bit hash. Might also be address + if err == nil { + tx, err := i.GetTransaction(txid) + if err != nil { + Log.Errorf("error downloading tx after socket notification: %s", err.Error()) + return + } + tx.Time = time.Now().Unix() + i.txNotifyChan <- *tx + } + } + }) + + // Subscribe to queued addresses + if len(i.listenQueue) != 0 { + i.SocketClient.Emit("subscribe", []interface{}{"bitcoind/addresstxid", i.listenQueue}) + i.listenQueue = []string{} + } + + Log.Infof("websocket connected (%s)", i.String()) + return nil +} + +func (i *BlockBookClient) Broadcast(tx []byte) (string, error) { + txHex := hex.EncodeToString(tx) + resp, err := i.RequestFunc("sendtx/"+txHex, http.MethodGet, nil, nil) + if err != nil { + return "", fmt.Errorf("error broadcasting tx: %s", err) + } + defer resp.Body.Close() + + type Response struct { + Txid string `json:"result"` + } + rs := new(Response) + if err = json.NewDecoder(resp.Body).Decode(rs); err != nil { + return "", fmt.Errorf("error decoding txid: %s", err) + } + return rs.Txid, nil +} + +func (i *BlockBookClient) GetBestBlock() (*model.Block, error) { + type backend struct { + Blocks int `json:"blocks"` + BestBlockHash string `json:"bestBlockHash"` + } + type resIndex struct { + Backend backend `json:"backend"` + } + + type resBlockHash struct { + BlockHash string `json:"blockHash"` + } + + resp, err := i.RequestFunc("", http.MethodGet, nil, nil) + if err != nil { + return nil, fmt.Errorf("getting block index: %s", err.Error()) + } + defer resp.Body.Close() + + decoder := json.NewDecoder(resp.Body) + bi := new(resIndex) + if err = decoder.Decode(bi); err != nil { + return nil, fmt.Errorf("decoding block index: %s", err) + } + blockIndexPath := "/block-index/" + strconv.Itoa(bi.Backend.Blocks-1) + + resp2, err := i.RequestFunc(blockIndexPath, http.MethodGet, nil, nil) + if err != nil { + return nil, fmt.Errorf("getting block detail (%s): %s", blockIndexPath, err.Error()) + } + defer resp2.Body.Close() + + decoder2 := json.NewDecoder(resp2.Body) + bh := new(resBlockHash) + if err = decoder2.Decode(bh); err != nil { + return nil, fmt.Errorf("decoding block detail: %s", err) + } + + return &model.Block{ + Hash: bi.Backend.BestBlockHash, + Height: bi.Backend.Blocks, + PreviousBlockhash: bh.BlockHash, + }, nil +} + +func (i *BlockBookClient) GetBlocksBefore(to time.Time, limit int) (*model.BlockList, error) { + resp, err := i.RequestFunc("blocks", http.MethodGet, nil, url.Values{ + "blockDate": {to.Format("2006-01-02")}, + "startTimestamp": {fmt.Sprint(to.Unix())}, + "limit": {fmt.Sprint(limit)}, + }) + if err != nil { + return nil, err + } + defer resp.Body.Close() + list := new(model.BlockList) + decoder := json.NewDecoder(resp.Body) + if err = decoder.Decode(list); err != nil { + return nil, fmt.Errorf("error decoding block list: %s", err) + } + return list, nil +} + +func (i *BlockBookClient) EstimateFee(nbBlocks int) (int, error) { + resp, err := i.RequestFunc("utils/estimatefee", http.MethodGet, nil, url.Values{"nbBlocks": {fmt.Sprint(nbBlocks)}}) + if err != nil { + return 0, err + } + defer resp.Body.Close() + data := map[int]float64{} + if err = json.NewDecoder(resp.Body).Decode(&data); err != nil { + return 0, fmt.Errorf("error decoding fee estimate: %s", err) + } + return int(data[nbBlocks] * 1e8), nil +} + +// maybeConvertCashAddress adds the Bitcoin Cash URI prefix to the address +// to make blockbook happy if this is a cashaddr. +func maybeConvertCashAddress(addr btcutil.Address) string { + _, isP2PKHCashAddr := addr.(*bchutil.CashAddressPubKeyHash) + _, isP2SHCashAddr := addr.(*bchutil.CashAddressScriptHash) + if isP2PKHCashAddr || isP2SHCashAddr { + if addr.IsForNet(&chaincfg.MainNetParams) { + return "bitcoincash:" + addr.String() + } else { + return "bchtest:" + addr.String() + } + } + return addr.String() +} + +func maybeTrimCashAddrPrefix(addr string) string { + return strings.TrimPrefix(strings.TrimPrefix(addr, "bchtest:"), "bitcoincash:") +} diff --git a/client/errors/fatal_server_error.go b/client/errors/fatal_server_error.go new file mode 100644 index 0000000..ca5624d --- /dev/null +++ b/client/errors/fatal_server_error.go @@ -0,0 +1,48 @@ +package errors + +import ( + "errors" + "fmt" +) + +// FatalServerError is a special error interface which is understood by +// client/pool for the purposes of indicating the error should conditionally +// trigger a non-recoverable failure for that request, which is indicated +// by IsFatal returning true. +type FatalServerError interface { + wrappedError + IsFatal() bool +} + +// FatalServerErrorInstance is a simple type which implements +// FatalServerError interface. This pattern can be extended to create +// other complex error types which can also conditionally respond as +// non-recoverable. +type FatalServerErrorInstance struct { + err error +} + +// NewFatalError is a helper that produces a FatalServerError +func NewFatalError(errReason string) FatalServerError { + return FatalServerErrorInstance{err: errors.New(errReason)} +} + +// NewFatalErrorf is a helper that produces a FatalServerError +func NewFatalErrorf(format string, args ...interface{}) FatalServerError { + return FatalServerErrorInstance{err: fmt.Errorf(format, args...)} +} + +// MakeFatal wraps an existing error into a FatalServerError. MakeFatal is +// composable with other wrappedError types. +func MakeFatal(err error) FatalServerError { + return FatalServerErrorInstance{err: err} +} + +// Error returns the error message +func (e FatalServerErrorInstance) Error() string { return e.err.Error() } + +func (e FatalServerErrorInstance) internalError() error { return e.err } + +// IsFatal indicates whether the error should be considered non-recoverable +// for that request +func (e FatalServerErrorInstance) IsFatal() bool { return true } diff --git a/client/errors/fatal_server_error_test.go b/client/errors/fatal_server_error_test.go new file mode 100644 index 0000000..bc5fe00 --- /dev/null +++ b/client/errors/fatal_server_error_test.go @@ -0,0 +1,22 @@ +package errors_test + +import ( + "errors" + "testing" + + clientErrs "github.com/OpenBazaar/multiwallet/client/errors" +) + +func TestIsFatal(t *testing.T) { + var ( + nonFatal = errors.New("nonfatal error") + fatal = clientErrs.NewFatalError("fatal error") + ) + + if clientErrs.IsFatal(nonFatal) { + t.Error("expected non-fatal error to not indicate fatal, but did") + } + if !clientErrs.IsFatal(fatal) { + t.Error("expected fatal error to indicate fatal, but did not") + } +} diff --git a/client/errors/retryable_server_error.go b/client/errors/retryable_server_error.go new file mode 100644 index 0000000..cd78204 --- /dev/null +++ b/client/errors/retryable_server_error.go @@ -0,0 +1,47 @@ +package errors + +import ( + "errors" + "fmt" +) + +// RetryableServerError is a special error interface which is understood by +// client/pool for the purposes of indicating whether the error should +// conditionally retry the request, which is indicated by IsRetryable +// returning true. +type RetryableServerError interface { + wrappedError + IsRetryable() bool +} + +// RetryableErrorInstance is a simple type which implements the +// RetryableServerError interface. This pattern can be extended to create +// other complex error types which can also conditionally respond as +// retryable. +type RetryableErrorInstance struct { + err error +} + +// NewRetryableError is a helper that produces a RetryableServerError +func NewRetryableError(errReason string) RetryableServerError { + return RetryableErrorInstance{err: errors.New(errReason)} +} + +// NewRetryableErrorf is a helper that produces a RetryableServerError +func NewRetryableErrorf(format string, args ...interface{}) RetryableServerError { + return RetryableErrorInstance{err: fmt.Errorf(format, args...)} +} + +// MakeRetryable wraps an existing error into a RetryableServerError. +// This method can be used to wrap other already wrappedError types. +func MakeRetryable(err error) RetryableServerError { + return RetryableErrorInstance{err: err} +} + +// Error returns the error message +func (e RetryableErrorInstance) Error() string { return e.err.Error() } + +func (e RetryableErrorInstance) internalError() error { return e.err } + +// IsRetryable indicates whether the error should be attempted again +func (e RetryableErrorInstance) IsRetryable() bool { return true } diff --git a/client/errors/retryable_server_error_test.go b/client/errors/retryable_server_error_test.go new file mode 100644 index 0000000..203c876 --- /dev/null +++ b/client/errors/retryable_server_error_test.go @@ -0,0 +1,22 @@ +package errors_test + +import ( + "errors" + "testing" + + clientErrs "github.com/OpenBazaar/multiwallet/client/errors" +) + +func TestIsRetryable(t *testing.T) { + var ( + nonRetryable = errors.New("nonretryable error") + retryable = clientErrs.NewRetryableError("retryable error") + ) + + if clientErrs.IsRetryable(nonRetryable) { + t.Error("expected non-retryable error to not indicate retryable, but did") + } + if !clientErrs.IsRetryable(retryable) { + t.Error("expected retryable error to indicate retryable, but did not") + } +} diff --git a/client/errors/wrapped_error.go b/client/errors/wrapped_error.go new file mode 100644 index 0000000..fe91c92 --- /dev/null +++ b/client/errors/wrapped_error.go @@ -0,0 +1,30 @@ +package errors + +type wrappedError interface { + error + internalError() error +} + +// IsFatal can detect whether a wrappedError is Fatal or not. This +// method does not work on a regular error and will assume all regular +// error instances are not fatal +func IsFatal(err error) bool { + var ok, iOK bool + _, ok = err.(FatalServerError) + if wErr, wOK := err.(wrappedError); wOK { + iOK = IsFatal(wErr.internalError()) + } + return ok || iOK +} + +// IsRetryable can detect whether a wrappedError is Retryable or not. This +// method does not work on a regular error and will assume all regular +// error instances are not retryable +func IsRetryable(err error) bool { + var ok, iOK bool + _, ok = err.(RetryableServerError) + if wErr, wOK := err.(wrappedError); wOK { + iOK = IsRetryable(wErr.internalError()) + } + return ok || iOK +} diff --git a/client/errors/wrapped_error_test.go b/client/errors/wrapped_error_test.go new file mode 100644 index 0000000..b7c6d73 --- /dev/null +++ b/client/errors/wrapped_error_test.go @@ -0,0 +1,33 @@ +package errors_test + +import ( + "errors" + "testing" + + clientErr "github.com/OpenBazaar/multiwallet/client/errors" +) + +func TestWrappedErrorsAreComposable(t *testing.T) { + var ( + baseErr = errors.New("base") + fatalErr = clientErr.MakeFatal(baseErr) + retryableErr = clientErr.MakeRetryable(baseErr) + + fatalRetryableErr = clientErr.MakeFatal(retryableErr) + retryableFatalErr = clientErr.MakeRetryable(fatalErr) + ) + + if !clientErr.IsRetryable(fatalRetryableErr) { + t.Errorf("expected fatal(retryable(err)) to be retryable but was not") + } + if !clientErr.IsFatal(fatalRetryableErr) { + t.Errorf("expected fatal(retryable(err)) to be fatal but was not") + } + + if !clientErr.IsRetryable(retryableFatalErr) { + t.Errorf("expected retryable(fatal(err)) to be retryable but was not") + } + if !clientErr.IsFatal(retryableFatalErr) { + t.Errorf("expected retryable(fatal(err)) to be fatal but was not") + } +} diff --git a/client/pool.go b/client/pool.go new file mode 100644 index 0000000..af584ac --- /dev/null +++ b/client/pool.go @@ -0,0 +1,393 @@ +package client + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "sync" + + "github.com/OpenBazaar/multiwallet/client/blockbook" + clientErr "github.com/OpenBazaar/multiwallet/client/errors" + "github.com/OpenBazaar/multiwallet/model" + "github.com/btcsuite/btcutil" + "github.com/op/go-logging" + "golang.org/x/net/proxy" +) + +var Log = logging.MustGetLogger("pool") + +// ClientPool is an implementation of the APIClient interface which will handle +// server failure, rotate servers, and retry API requests. +type ClientPool struct { + blockChan chan model.Block + cancelListenChan context.CancelFunc + listenAddrs []btcutil.Address + listenAddrsLock sync.Mutex + poolManager *rotationManager + proxyDialer proxy.Dialer + txChan chan model.Transaction + unblockStart chan struct{} + + HTTPClient http.Client +} + +func (p *ClientPool) newMaximumTryEnumerator() *maxTryEnum { + return &maxTryEnum{max: 3, attempts: 0} +} + +type maxTryEnum struct{ max, attempts int } + +func (m *maxTryEnum) next() bool { + var now = m.attempts + m.attempts++ + return now < m.max +} + +func (m *maxTryEnum) isFinal() bool { + return m.attempts == m.max +} + +// NewClientPool instantiates a new ClientPool object with the given server APIs +func NewClientPool(endpoints []string, proxyDialer proxy.Dialer) (*ClientPool, error) { + if len(endpoints) == 0 { + return nil, errors.New("no client endpoints provided") + } + + var ( + pool = &ClientPool{ + blockChan: make(chan model.Block), + poolManager: &rotationManager{}, + listenAddrs: make([]btcutil.Address, 0), + txChan: make(chan model.Transaction), + unblockStart: make(chan struct{}, 1), + } + manager, err = newRotationManager(endpoints, proxyDialer) + ) + if err != nil { + return nil, err + } + pool.poolManager = manager + return pool, nil +} + +// Start will attempt to connect to the first available server. If it fails to +// connect it will rotate through the servers to try to find one that works. +func (p *ClientPool) Start() error { + go p.run() + return nil +} + +func (p *ClientPool) Clients() []*blockbook.BlockBookClient { + var clients []*blockbook.BlockBookClient + for _, c := range p.poolManager.clientCache { + clients = append(clients, c) + } + return clients +} + +func (p *ClientPool) run() { + for { + select { + case <-p.unblockStart: + return + default: + p.runLoop() + } + } +} + +func (p *ClientPool) runLoop() error { + p.poolManager.SelectNext() + var closeChan = make(chan error, 0) + defer close(closeChan) + if err := p.poolManager.StartCurrent(closeChan); err != nil { + Log.Errorf("error starting %s: %s", p.poolManager.currentTarget, err.Error()) + p.poolManager.FailCurrent() + p.poolManager.CloseCurrent() + return err + } + var ctx context.Context + ctx, p.cancelListenChan = context.WithCancel(context.Background()) + go p.listenChans(ctx) + defer p.stopWebsocketListening() + p.replayListenAddresses() + if err := <-closeChan; err != nil { + p.poolManager.FailCurrent() + p.poolManager.CloseCurrent() + } + return nil +} + +// Close proxies the same request to the active client +func (p *ClientPool) Close() { + p.stopWebsocketListening() + p.unblockStart <- struct{}{} + p.poolManager.CloseCurrent() +} + +// PoolManager returns the pool manager object +func (p *ClientPool) PoolManager() *rotationManager { + return p.poolManager +} + +// FailAndCloseCurrentClient cleans up the active client's connections, and +// signals to the rotation manager that it is unhealthy. The internal runLoop +// will detect the client's closing and attempt to start the next available. +func (p *ClientPool) FailAndCloseCurrentClient() { + p.stopWebsocketListening() + p.poolManager.FailCurrent() + p.poolManager.CloseCurrent() +} + +func (p *ClientPool) stopWebsocketListening() { + if p.cancelListenChan != nil { + p.cancelListenChan() + p.cancelListenChan = nil + } +} + +// listenChans proxies the block and tx chans from the client to the ClientPool's channels +func (p *ClientPool) listenChans(ctx context.Context) { + var ( + client = p.poolManager.AcquireCurrent() + blockChan = client.BlockChannel() + txChan = client.TxChannel() + ) + defer p.poolManager.ReleaseCurrent() + go func() { + for { + select { + case block := <-blockChan: + p.blockChan <- block + case tx := <-txChan: + p.txChan <- tx + case <-ctx.Done(): + return + } + } + }() +} + +// executeRequest handles making the HTTP request and responding with rotating the +// pool and/or retrying requests. As all requests travel through this method, without +// middleware intercepting error responses, all returned errors are checked to see if +// they are Retryable and/or Fatal as defined by client/errors. These error properties +// can be composed like client/errors.MakeFatal(client/errors.MakeRetryable(err)). +// This approach should allow individual requests to define how the resulting error +// should be handled upstream of the request. +func (p *ClientPool) executeRequest(queryFunc func(c *blockbook.BlockBookClient) error) error { + var err error + for e := p.newMaximumTryEnumerator(); e.next(); { + var client = p.poolManager.AcquireCurrentWhenReady() + if err = queryFunc(client); err != nil { + p.poolManager.ReleaseCurrent() + if clientErr.IsFatal(err) || e.isFinal() { + Log.Warningf("rotating server due to fatal or exhausted attempts") + p.FailAndCloseCurrentClient() + } + if !clientErr.IsRetryable(err) { + Log.Errorf("unretryable error: %s", err.Error()) + return err + } + if !e.isFinal() { + Log.Warningf("retrying due to error: %s", err.Error()) + } + continue + } else { + p.poolManager.ReleaseCurrent() + return nil + } + } + Log.Errorf("exhausted retry attempts, last error: %s", err.Error()) + return fmt.Errorf("request failed: %s", err.Error()) +} + +// BlockNofity proxies the active client's block channel +func (p *ClientPool) BlockNotify() <-chan model.Block { + return p.blockChan +} + +// Broadcast proxies the same request to the active client +func (p *ClientPool) Broadcast(tx []byte) (string, error) { + var ( + txid string + queryFunc = func(c *blockbook.BlockBookClient) error { + Log.Debugf("(%s) broadcasting transaction", c.EndpointURL().String()) + r, err := c.Broadcast(tx) + if err != nil { + return err + } + txid = r + return nil + } + ) + + err := p.executeRequest(queryFunc) + return txid, err +} + +// EstimateFee proxies the same request to the active client +func (p *ClientPool) EstimateFee(nBlocks int) (int, error) { + var ( + fee int + queryFunc = func(c *blockbook.BlockBookClient) error { + Log.Debugf("(%s) requesting fee estimate", c.EndpointURL().String()) + r, err := c.EstimateFee(nBlocks) + if err != nil { + return clientErr.MakeRetryable(err) + } + fee = r + return nil + } + ) + + err := p.executeRequest(queryFunc) + return fee, err +} + +// GetBestBlock proxies the same request to the active client +func (p *ClientPool) GetBestBlock() (*model.Block, error) { + var ( + block *model.Block + queryFunc = func(c *blockbook.BlockBookClient) error { + Log.Debugf("(%s) request best block info", c.EndpointURL().String()) + r, err := c.GetBestBlock() + if err != nil { + return clientErr.MakeRetryable(err) + } + block = r + return err + } + ) + + err := p.executeRequest(queryFunc) + return block, err +} + +// GetInfo proxies the same request to the active client +func (p *ClientPool) GetInfo() (*model.Info, error) { + var ( + info *model.Info + queryFunc = func(c *blockbook.BlockBookClient) error { + Log.Debugf("(%s) request backend info", c.EndpointURL().String()) + r, err := c.GetInfo() + if err != nil { + return clientErr.MakeRetryable(err) + } + info = r + return nil + } + ) + + err := p.executeRequest(queryFunc) + return info, err +} + +// GetRawTransaction proxies the same request to the active client +func (p *ClientPool) GetRawTransaction(txid string) ([]byte, error) { + var ( + tx []byte + queryFunc = func(c *blockbook.BlockBookClient) error { + Log.Debugf("(%s) request transaction info, txid: %s", c.EndpointURL().String(), txid) + r, err := c.GetRawTransaction(txid) + if err != nil { + return err + } + tx = r + return nil + } + ) + err := p.executeRequest(queryFunc) + return tx, err +} + +// GetTransactions proxies the same request to the active client +func (p *ClientPool) GetTransactions(addrs []btcutil.Address) ([]model.Transaction, error) { + var ( + txs []model.Transaction + queryFunc = func(c *blockbook.BlockBookClient) error { + var addrStrings []string + for _, a := range addrs { + addrStrings = append(addrStrings, a.String()) + } + Log.Debugf("(%s) request transactions for (%d) addrs", c.EndpointURL().String(), len(addrs)) + Log.Debugf("\taddrs requested: %s", strings.Join(addrStrings, ",")) + r, err := c.GetTransactions(addrs) + if err != nil { + return err + } + txs = r + return nil + } + ) + + err := p.executeRequest(queryFunc) + return txs, err +} + +// GetTransaction proxies the same request to the active client +func (p *ClientPool) GetTransaction(txid string) (*model.Transaction, error) { + var ( + tx *model.Transaction + queryFunc = func(c *blockbook.BlockBookClient) error { + Log.Debugf("(%s) request transaction data, txid: %s", c.EndpointURL().String(), txid) + r, err := c.GetTransaction(txid) + if err != nil { + return err + } + tx = r + return nil + } + ) + + err := p.executeRequest(queryFunc) + return tx, err +} + +// GetUtxos proxies the same request to the active client +func (p *ClientPool) GetUtxos(addrs []btcutil.Address) ([]model.Utxo, error) { + var ( + utxos []model.Utxo + queryFunc = func(c *blockbook.BlockBookClient) error { + var addrStrings []string + for _, a := range addrs { + addrStrings = append(addrStrings, a.String()) + } + Log.Debugf("(%s) request utxos for (%d) addrs", c.EndpointURL().String(), len(addrs)) + Log.Debugf("\taddrs requested: %s", strings.Join(addrStrings, ",")) + r, err := c.GetUtxos(addrs) + if err != nil { + return err + } + utxos = r + return nil + } + ) + + err := p.executeRequest(queryFunc) + return utxos, err +} + +// ListenAddresses proxies the same request to the active client +func (p *ClientPool) ListenAddresses(addrs ...btcutil.Address) { + p.listenAddrsLock.Lock() + defer p.listenAddrsLock.Unlock() + var client = p.poolManager.AcquireCurrentWhenReady() + defer p.poolManager.ReleaseCurrent() + + p.listenAddrs = append(p.listenAddrs, addrs...) + client.ListenAddresses(addrs...) +} + +func (p *ClientPool) replayListenAddresses() { + p.listenAddrsLock.Lock() + defer p.listenAddrsLock.Unlock() + var client = p.poolManager.AcquireCurrent() + defer p.poolManager.ReleaseCurrent() + client.ListenAddresses(p.listenAddrs...) +} + +// TransactionNotify proxies the active client's tx channel +func (p *ClientPool) TransactionNotify() <-chan model.Transaction { return p.txChan } diff --git a/client/pool_test.go b/client/pool_test.go new file mode 100644 index 0000000..15306a6 --- /dev/null +++ b/client/pool_test.go @@ -0,0 +1,254 @@ +package client_test + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/OpenBazaar/multiwallet/client" + "github.com/OpenBazaar/multiwallet/model" + "github.com/OpenBazaar/multiwallet/model/mock" + "github.com/OpenBazaar/multiwallet/test/factory" + "gopkg.in/jarcoal/httpmock.v1" +) + +func replaceHTTPClientOnClientPool(p *client.ClientPool, c http.Client) { + for _, cp := range p.Clients() { + cp.HTTPClient = c + } + p.HTTPClient = c +} + +func mustPrepareClientPool(endpoints []string) (*client.ClientPool, func()) { + var p, err = client.NewClientPool(endpoints, nil) + if err != nil { + panic(err.Error()) + } + + mockedHTTPClient := http.Client{} + httpmock.ActivateNonDefault(&mockedHTTPClient) + replaceHTTPClientOnClientPool(p, mockedHTTPClient) + + mock.MockWebsocketClientOnClientPool(p) + err = p.Start() + if err != nil { + panic(err.Error()) + } + + return p, func() { + httpmock.DeactivateAndReset() + p.Close() + } +} + +func TestRequestRotatesServersOn500(t *testing.T) { + var ( + endpointOne = "http://localhost:8332" + endpointTwo = "http://localhost:8336" + p, cleanup = mustPrepareClientPool([]string{endpointOne, endpointTwo}) + expectedTx = factory.NewTransaction() + txid = "1be612e4f2b79af279e0b307337924072b819b3aca09fcb20370dd9492b83428" + ) + defer cleanup() + + httpmock.RegisterResponder(http.MethodGet, fmt.Sprintf("%s/tx/%s", endpointOne, txid), + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(http.StatusInternalServerError, expectedTx) + }, + ) + httpmock.RegisterResponder(http.MethodGet, fmt.Sprintf("%s/tx/%s", endpointTwo, txid), + func(req *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(http.StatusOK, expectedTx) + }, + ) + + _, err := p.GetTransaction(txid) + if err != nil { + t.Errorf("expected successful transaction, but got error: %s", err.Error()) + } +} + +func TestRequestRetriesTimeoutsToExhaustionThenRotates(t *testing.T) { + var ( + endpointOne = "http://localhost:8332" + endpointTwo = "http://localhost:8336" + fastTimeoutClient = http.Client{Timeout: 500000 * time.Nanosecond} + p, err = client.NewClientPool([]string{endpointOne, endpointTwo}, nil) + ) + if err != nil { + t.Fatal(err) + } + + httpmock.DeactivateAndReset() + httpmock.ActivateNonDefault(&fastTimeoutClient) + replaceHTTPClientOnClientPool(p, fastTimeoutClient) + mock.MockWebsocketClientOnClientPool(p) + if err = p.Start(); err != nil { + t.Fatal(err) + } + defer func() { + httpmock.DeactivateAndReset() + p.Close() + }() + + var ( + txid = "1be612e4f2b79af279e0b307337924072b819b3aca09fcb20370dd9492b83428" + expectedAttempts = uint(3) + requestAttempts uint + laggyResponse = func(req *http.Request) (*http.Response, error) { + if requestAttempts < expectedAttempts { + requestAttempts++ + time.Sleep(1 * time.Second) + return nil, fmt.Errorf("timeout") + } + return httpmock.NewJsonResponse(http.StatusOK, factory.NewTransaction()) + } + ) + httpmock.RegisterResponder(http.MethodGet, fmt.Sprintf("%s/tx/%s", endpointOne, txid), laggyResponse) + httpmock.RegisterResponder(http.MethodGet, fmt.Sprintf("%s/tx/%s", endpointTwo, txid), laggyResponse) + + _, err = p.GetTransaction(txid) + if err == nil { + t.Errorf("expected getTransaction to respond with timeout error, but did not") + return + } + if requestAttempts != expectedAttempts { + t.Errorf("expected initial server to be attempted %d times, but was attempted only %d", expectedAttempts, requestAttempts) + } + _, err = p.GetTransaction(txid) + if err != nil { + t.Errorf("expected getTransaction to rotate to the next server and succeed, but returned error: %s", err.Error()) + } +} + +func TestPoolBlockNotifyWorksAfterRotation(t *testing.T) { + var ( + endpointOne = "http://localhost:8332" + endpointTwo = "http://localhost:8336" + testHash = "0000000000000000003f1fb88ac3dab0e607e87def0e9031f7bea02cb464a04f" + txid = "1be612e4f2b79af279e0b307337924072b819b3aca09fcb20370dd9492b83428" + testPath = func(host string) string { return fmt.Sprintf("%s/tx/%s", host, txid) } + p, cleanup = mustPrepareClientPool([]string{endpointOne, endpointTwo}) + ) + defer cleanup() + + // GetTransaction should fail for endpoint one and succeed for endpoint two + var ( + beenBad bool + badThenGood = func(req *http.Request) (*http.Response, error) { + if beenBad { + return httpmock.NewJsonResponse(http.StatusOK, factory.NewTransaction()) + } + beenBad = true + return httpmock.NewJsonResponse(http.StatusInternalServerError, nil) + } + ) + httpmock.RegisterResponder(http.MethodGet, testPath(endpointOne), badThenGood) + httpmock.RegisterResponder(http.MethodGet, testPath(endpointTwo), badThenGood) + + go func() { + c := p.PoolManager().AcquireCurrentWhenReady() + c.BlockChannel() <- model.Block{Hash: testHash} + p.PoolManager().ReleaseCurrent() + }() + + ticker := time.NewTicker(time.Second * 2) + select { + case <-ticker.C: + t.Error("Timed out waiting for block") + case b := <-p.BlockNotify(): + if b.Hash != testHash { + t.Error("Returned incorrect block hash") + } + } + ticker.Stop() + + // request transaction triggers rotation + if _, err := p.GetTransaction(txid); err != nil { + t.Fatal(err) + } + + go func() { + c := p.PoolManager().AcquireCurrentWhenReady() + c.BlockChannel() <- model.Block{Hash: testHash} + p.PoolManager().ReleaseCurrent() + }() + + ticker = time.NewTicker(time.Second * 2) + select { + case <-ticker.C: + t.Error("Timed out waiting for block") + case b := <-p.BlockNotify(): + if b.Hash != testHash { + t.Error("Returned incorrect block hash") + } + } + ticker.Stop() +} + +func TestTransactionNotifyWorksAfterRotation(t *testing.T) { + var ( + endpointOne = "http://localhost:8332" + endpointTwo = "http://localhost:8336" + expectedTx = factory.NewTransaction() + expectedTxid = "500000e4f2b79af279e0b307337924072b819b3aca09fcb20370dd9492b83428" + testPath = func(host string) string { return fmt.Sprintf("%s/tx/%s", host, expectedTxid) } + p, cleanup = mustPrepareClientPool([]string{endpointOne, endpointTwo}) + ) + defer cleanup() + expectedTx.Txid = expectedTxid + + // GetTransaction should fail for endpoint one and succeed for endpoint two + var ( + beenBad bool + badThenGood = func(req *http.Request) (*http.Response, error) { + if beenBad { + return httpmock.NewJsonResponse(http.StatusOK, expectedTx) + } + beenBad = true + return httpmock.NewJsonResponse(http.StatusInternalServerError, nil) + } + ) + httpmock.RegisterResponder(http.MethodGet, testPath(endpointOne), badThenGood) + httpmock.RegisterResponder(http.MethodGet, testPath(endpointTwo), badThenGood) + + go func() { + c := p.PoolManager().AcquireCurrentWhenReady() + c.TxChannel() <- expectedTx + p.PoolManager().ReleaseCurrent() + }() + + ticker := time.NewTicker(time.Second * 2) + select { + case <-ticker.C: + t.Error("Timed out waiting for tx") + case b := <-p.TransactionNotify(): + if b.Txid != expectedTx.Txid { + t.Error("Returned incorrect tx hash") + } + } + ticker.Stop() + + // request transaction triggers rotation + if _, err := p.GetTransaction(expectedTxid); err != nil { + t.Fatal(err) + } + + go func() { + c := p.PoolManager().AcquireCurrentWhenReady() + c.TxChannel() <- expectedTx + p.PoolManager().ReleaseCurrent() + }() + + ticker = time.NewTicker(time.Second * 2) + select { + case <-ticker.C: + t.Error("Timed out waiting for tx") + case b := <-p.TransactionNotify(): + if b.Txid != expectedTx.Txid { + t.Error("Returned incorrect tx hash") + } + } + ticker.Stop() +} diff --git a/client/rotation_manager.go b/client/rotation_manager.go new file mode 100644 index 0000000..b1eff90 --- /dev/null +++ b/client/rotation_manager.go @@ -0,0 +1,205 @@ +package client + +import ( + "errors" + "sync" + "time" + + "github.com/OpenBazaar/multiwallet/client/blockbook" + "golang.org/x/net/proxy" +) + +var maximumBackoff = 60 * time.Second + +type healthState struct { + lastFailedAt time.Time + backoffDuration time.Duration +} + +func (h *healthState) markUnhealthy() { + var now = time.Now() + if now.Before(h.nextAvailable()) { + // can't be unhealthy before it's available + return + } + if now.Before(h.lastFailedAt.Add(5 * time.Minute)) { + h.backoffDuration *= 2 + if h.backoffDuration > maximumBackoff { + h.backoffDuration = maximumBackoff + } + } else { + h.backoffDuration = 2 * time.Second + } + h.lastFailedAt = now +} + +func (h *healthState) isHealthy() bool { + return time.Now().After(h.nextAvailable()) +} + +func (h *healthState) nextAvailable() time.Time { + return h.lastFailedAt.Add(h.backoffDuration) +} + +const nilTarget = RotationTarget("") + +type ( + RotationTarget string + rotationManager struct { + clientCache map[RotationTarget]*blockbook.BlockBookClient + currentTarget RotationTarget + targetHealth map[RotationTarget]*healthState + rotateLock sync.RWMutex + started bool + } +) + +func newRotationManager(targets []string, proxyDialer proxy.Dialer) (*rotationManager, error) { + var ( + targetHealth = make(map[RotationTarget]*healthState) + clients = make(map[RotationTarget]*blockbook.BlockBookClient) + ) + for _, apiUrl := range targets { + c, err := blockbook.NewBlockBookClient(apiUrl, proxyDialer) + if err != nil { + return nil, err + } + clients[RotationTarget(apiUrl)] = c + targetHealth[RotationTarget(apiUrl)] = &healthState{} + } + m := &rotationManager{ + clientCache: clients, + currentTarget: nilTarget, + targetHealth: targetHealth, + } + return m, nil +} + +// AcquireCurrent locks the current client for reading and returns a pointer to the current +// client. ReleaseCurrent is required at the end of using the active client to ensure rotation +// does not lock indefinitely. +func (r *rotationManager) AcquireCurrent() *blockbook.BlockBookClient { + for { + r.rLock() + if client, ok := r.clientCache[r.currentTarget]; !ok { + r.rUnlock() + r.SelectNext() + continue + } else { + return client + } + } +} + +// AcquireCurrentWhenReady will block until the current client is ready for use. This method +// should always be used before the AcquireCurrent variety to minimize time within a read lock. +func (r *rotationManager) AcquireCurrentWhenReady() *blockbook.BlockBookClient { + if r.started { + return r.AcquireCurrent() + } + var t = time.NewTicker(1 * time.Second) + defer t.Stop() + for range t.C { + if r.started { + break + } + } + return r.AcquireCurrent() +} + +// ReleaseCurrent unlocks the current client for reading and cleans up outstanding resources as +// needed. +func (r *rotationManager) ReleaseCurrent() { + r.rUnlock() +} + +// CloseCurrent locks the client for changing which prevents further read locks from being accessed. +func (r *rotationManager) CloseCurrent() { + r.lock() + defer r.unlock() + + if r.currentTarget != nilTarget { + if r.started { + r.clientCache[r.currentTarget].Close() + } + r.started = false + r.currentTarget = nilTarget + } +} + +// StartCurrent unlocks the client for use after successfully starting the active client. +func (r *rotationManager) StartCurrent(done chan<- error) error { + r.lock() + defer r.unlock() + + client, ok := r.clientCache[r.currentTarget] + if !ok { + // ensure this isn't the result of r.currentTarget being nilTarget + r.unlock() + r.SelectNext() + r.lock() + client, ok = r.clientCache[r.currentTarget] + if !ok { + return errors.New("current client unavailable") + } + } + + if err := client.Start(done); err != nil { + return err + } + + r.started = true + return nil +} + +// FailCurrent marks the current client as having failed and ensures it is not rotated into too soon. +func (r *rotationManager) FailCurrent() { + r.lock() + defer r.unlock() + + hs, ok := r.targetHealth[r.currentTarget] + if ok { + hs.markUnhealthy() + } +} + +// SelectNext finds the next healthy and available server to activate with StartCurrent. This call will +// block until a server is healthy and available. +func (r *rotationManager) SelectNext() { + r.lock() + defer r.unlock() + + if r.currentTarget == nilTarget { + var nextAvailableAt time.Time + for { + if time.Now().Before(nextAvailableAt) { + continue + } + for target, health := range r.targetHealth { + if health.isHealthy() { + r.currentTarget = target + return + } + if health.nextAvailable().After(nextAvailableAt) { + nextAvailableAt = health.nextAvailable() + } + } + } + } +} + +func (r *rotationManager) lock() { + r.rotateLock.Lock() +} + +func (r *rotationManager) unlock() { + r.rotateLock.Unlock() +} + +func (r *rotationManager) rLock() { + r.rotateLock.RLock() +} + +func (r *rotationManager) rUnlock() { + r.rotateLock.RUnlock() +} diff --git a/client/transport/transport.go b/client/transport/transport.go new file mode 100644 index 0000000..5a8940d --- /dev/null +++ b/client/transport/transport.go @@ -0,0 +1,148 @@ +package transport + +import ( + "errors" + tp "github.com/OpenBazaar/golang-socketio/transport" + "github.com/gorilla/websocket" + "golang.org/x/net/proxy" + "io/ioutil" + "net" + "net/http" + "time" +) + +const ( + upgradeFailed = "Upgrade failed: " + + WsDefaultPingInterval = 30 * time.Second + WsDefaultPingTimeout = 60 * time.Second + WsDefaultReceiveTimeout = 60 * time.Second + WsDefaultSendTimeout = 60 * time.Second + WsDefaultBufferSize = 1024 * 32 +) + +var ( + ErrorBinaryMessage = errors.New("Binary messages are not supported") + ErrorBadBuffer = errors.New("Buffer error") + ErrorPacketWrong = errors.New("Wrong packet type error") + ErrorMethodNotAllowed = errors.New("Method not allowed") + ErrorHttpUpgradeFailed = errors.New("Http upgrade failed") +) + +type WebsocketConnection struct { + socket *websocket.Conn + transport *WebsocketTransport +} + +func (wsc *WebsocketConnection) GetMessage() (message string, err error) { + wsc.socket.SetReadDeadline(time.Now().Add(wsc.transport.ReceiveTimeout)) + msgType, reader, err := wsc.socket.NextReader() + if err != nil { + return "", err + } + + //support only text messages exchange + if msgType != websocket.TextMessage { + return "", ErrorBinaryMessage + } + + data, err := ioutil.ReadAll(reader) + if err != nil { + return "", ErrorBadBuffer + } + text := string(data) + + //empty messages are not allowed + if len(text) == 0 { + return "", ErrorPacketWrong + } + + return text, nil +} + +func (wsc *WebsocketConnection) WriteMessage(message string) error { + wsc.socket.SetWriteDeadline(time.Now().Add(wsc.transport.SendTimeout)) + writer, err := wsc.socket.NextWriter(websocket.TextMessage) + if err != nil { + return err + } + + if _, err := writer.Write([]byte(message)); err != nil { + return err + } + if err := writer.Close(); err != nil { + return err + } + return nil +} + +func (wsc *WebsocketConnection) Close() { + wsc.socket.Close() +} + +func (wsc *WebsocketConnection) PingParams() (interval, timeout time.Duration) { + return wsc.transport.PingInterval, wsc.transport.PingTimeout +} + +type WebsocketTransport struct { + PingInterval time.Duration + PingTimeout time.Duration + ReceiveTimeout time.Duration + SendTimeout time.Duration + + BufferSize int + + RequestHeader http.Header + + proxyDialer proxy.Dialer +} + +func (wst *WebsocketTransport) Connect(url string) (conn tp.Connection, err error) { + dial := net.Dial + if wst.proxyDialer != nil { + dial = wst.proxyDialer.Dial + } + dialer := websocket.Dialer{NetDial: dial} + socket, _, err := dialer.Dial(url, wst.RequestHeader) + if err != nil { + return nil, err + } + + return &WebsocketConnection{socket, wst}, nil +} + +func (wst *WebsocketTransport) HandleConnection( + w http.ResponseWriter, r *http.Request) (conn tp.Connection, err error) { + + if r.Method != "GET" { + http.Error(w, upgradeFailed+ErrorMethodNotAllowed.Error(), 503) + return nil, ErrorMethodNotAllowed + } + + socket, err := websocket.Upgrade(w, r, nil, wst.BufferSize, wst.BufferSize) + if err != nil { + http.Error(w, upgradeFailed+err.Error(), 503) + return nil, ErrorHttpUpgradeFailed + } + + return &WebsocketConnection{socket, wst}, nil +} + +/** +Websocket connection do not require any additional processing +*/ +func (wst *WebsocketTransport) Serve(w http.ResponseWriter, r *http.Request) {} + +/** +Returns websocket connection with default params +*/ +func GetDefaultWebsocketTransport(proxyDialer proxy.Dialer) *WebsocketTransport { + return &WebsocketTransport{ + PingInterval: WsDefaultPingInterval, + PingTimeout: WsDefaultPingTimeout, + ReceiveTimeout: WsDefaultReceiveTimeout, + SendTimeout: WsDefaultSendTimeout, + BufferSize: WsDefaultBufferSize, + proxyDialer: proxyDialer, + } +} diff --git a/cmd/multiwallet/main.go b/cmd/multiwallet/main.go new file mode 100644 index 0000000..b021013 --- /dev/null +++ b/cmd/multiwallet/main.go @@ -0,0 +1,83 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "sync" + + "github.com/OpenBazaar/multiwallet" + "github.com/OpenBazaar/multiwallet/api" + "github.com/OpenBazaar/multiwallet/cli" + "github.com/OpenBazaar/multiwallet/config" + wi "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/chaincfg" + "github.com/jessevdk/go-flags" +) + +const WALLET_VERSION = "0.1.0" + +var parser = flags.NewParser(nil, flags.Default) + +type Start struct { + Testnet bool `short:"t" long:"testnet" description:"use the test network"` +} +type Version struct{} + +var start Start +var version Version +var mw multiwallet.MultiWallet + +func main() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + for range c { + fmt.Println("Multiwallet shutting down...") + os.Exit(1) + } + }() + parser.AddCommand("start", + "start the wallet", + "The start command starts the wallet daemon", + &start) + parser.AddCommand("version", + "print the version number", + "Print the version number and exit", + &version) + cli.SetupCli(parser) + if _, err := parser.Parse(); err != nil { + os.Exit(1) + } +} + +func (x *Version) Execute(args []string) error { + fmt.Println(WALLET_VERSION) + return nil +} + +func (x *Start) Execute(args []string) error { + m := make(map[wi.CoinType]bool) + m[wi.Bitcoin] = true + m[wi.BitcoinCash] = true + m[wi.Zcash] = true + m[wi.Litecoin] = true + m[wi.Ethereum] = true + params := &chaincfg.MainNetParams + if x.Testnet { + params = &chaincfg.TestNet3Params + } + cfg := config.NewDefaultConfig(m, params) + cfg.Mnemonic = "bottle author ability expose illegal saddle antique setup pledge wife innocent treat" + var err error + mw, err = multiwallet.NewMultiWallet(cfg) + if err != nil { + return err + } + go api.ServeAPI(mw) + var wg sync.WaitGroup + wg.Add(1) + mw.Start() + wg.Wait() + return nil +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..3101ef9 --- /dev/null +++ b/config/config.go @@ -0,0 +1,233 @@ +package config + +import ( + "os" + "time" + + "github.com/OpenBazaar/multiwallet/cache" + "github.com/OpenBazaar/multiwallet/datastore" + "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/chaincfg" + "github.com/op/go-logging" + "golang.org/x/net/proxy" +) + +const ( + EthereumRegistryAddressMainnet = "0x403d907982474cdd51687b09a8968346159378f3" + EthereumRegistryAddressRinkeby = "0x403d907982474cdd51687b09a8968346159378f3" + EthereumRegistryAddressRopsten = "0x403d907982474cdd51687b09a8968346159378f3" +) + +type Config struct { + // Network parameters. Set mainnet, testnet, or regtest using this. + Params *chaincfg.Params + + // Bip39 mnemonic string. If empty a new mnemonic will be created. + Mnemonic string + + // The date the wallet was created. + // If before the earliest checkpoint the chain will be synced using the earliest checkpoint. + CreationDate time.Time + + // A Tor proxy can be set here causing the wallet will use Tor + Proxy proxy.Dialer + + // A logger. You can write the logs to file or stdout or however else you want. + Logger logging.Backend + + // Cache is a persistable storage provided by the consumer where the wallet can + // keep state between runtime executions + Cache cache.Cacher + + // A list of coin configs. One config should be included for each coin to be used. + Coins []CoinConfig + + // Disable the exchange rate functionality in each wallet + DisableExchangeRates bool +} + +type CoinConfig struct { + // The type of coin to configure + CoinType wallet.CoinType + + // The default fee-per-byte for each level + LowFee uint64 + MediumFee uint64 + HighFee uint64 + + // The highest allowable fee-per-byte + MaxFee uint64 + + // External API to query to look up fees. If this field is nil then the default fees will be used. + // If the API is unreachable then the default fees will likewise be used. If the API returns a fee + // greater than MaxFee then the MaxFee will be used in place. The API response must be formatted as + // { "fastestFee": 40, "halfHourFee": 20, "hourFee": 10 } + FeeAPI string + + // The trusted APIs to use for querying for balances and listening to blockchain events. + ClientAPIs []string + + // An implementation of the Datastore interface for each desired coin + DB wallet.Datastore + + // Custom options for wallet to use + Options map[string]interface{} +} + +func NewDefaultConfig(coinTypes map[wallet.CoinType]bool, params *chaincfg.Params) *Config { + cfg := &Config{ + Cache: cache.NewMockCacher(), + Params: params, + Logger: logging.NewLogBackend(os.Stdout, "", 0), + } + var testnet bool + if params.Name == chaincfg.TestNet3Params.Name { + testnet = true + } + mockDB := datastore.NewMockMultiwalletDatastore() + if coinTypes[wallet.Bitcoin] { + var apiEndpoints []string + if !testnet { + apiEndpoints = []string{ + "https://btc.blockbook.api.openbazaar.org/api", + // temporarily deprecated Insight endpoints + //"https://btc.bloqapi.net/insight-api", + //"https://btc.insight.openbazaar.org/insight-api", + } + } else { + apiEndpoints = []string{ + "https://tbtc.blockbook.api.openbazaar.org/api", + // temporarily deprecated Insight endpoints + //"https://test-insight.bitpay.com/api", + } + } + feeApi := "https://btc.fees.openbazaar.org" + db, _ := mockDB.GetDatastoreForWallet(wallet.Bitcoin) + btcCfg := CoinConfig{ + CoinType: wallet.Bitcoin, + FeeAPI: feeApi, + LowFee: 140, + MediumFee: 160, + HighFee: 180, + MaxFee: 2000, + ClientAPIs: apiEndpoints, + DB: db, + } + cfg.Coins = append(cfg.Coins, btcCfg) + } + if coinTypes[wallet.BitcoinCash] { + var apiEndpoints []string + if !testnet { + apiEndpoints = []string{ + "https://bch.blockbook.api.openbazaar.org/api", + // temporarily deprecated Insight endpoints + //"https://bitcoincash.blockexplorer.com/api", + } + } else { + apiEndpoints = []string{ + "https://tbch.blockbook.api.openbazaar.org/api", + // temporarily deprecated Insight endpoints + //"https://test-bch-insight.bitpay.com/api", + } + } + db, _ := mockDB.GetDatastoreForWallet(wallet.BitcoinCash) + bchCfg := CoinConfig{ + CoinType: wallet.BitcoinCash, + FeeAPI: "", + LowFee: 140, + MediumFee: 160, + HighFee: 180, + MaxFee: 2000, + ClientAPIs: apiEndpoints, + DB: db, + } + cfg.Coins = append(cfg.Coins, bchCfg) + } + if coinTypes[wallet.Zcash] { + var apiEndpoints []string + if !testnet { + apiEndpoints = []string{ + "https://zec.blockbook.api.openbazaar.org/api", + // temporarily deprecated Insight endpoints + //"https://zcashnetwork.info/api", + } + } else { + apiEndpoints = []string{ + "https://tzec.blockbook.api.openbazaar.org/api", + // temporarily deprecated Insight endpoints + //"https://explorer.testnet.z.cash/api", + } + } + db, _ := mockDB.GetDatastoreForWallet(wallet.Zcash) + zecCfg := CoinConfig{ + CoinType: wallet.Zcash, + FeeAPI: "", + LowFee: 140, + MediumFee: 160, + HighFee: 180, + MaxFee: 2000, + ClientAPIs: apiEndpoints, + DB: db, + } + cfg.Coins = append(cfg.Coins, zecCfg) + } + if coinTypes[wallet.Litecoin] { + var apiEndpoints []string + if !testnet { + apiEndpoints = []string{ + "https://ltc.blockbook.api.openbazaar.org/api", + // temporarily deprecated Insight endpoints + //"https://ltc.coin.space/api", + //"https://ltc.insight.openbazaar.org/insight-lite-api", + } + } else { + apiEndpoints = []string{ + "https://tltc.blockbook.api.openbazaar.org/api", + // temporarily deprecated Insight endpoints + //"https://testnet.litecore.io/api", + } + } + db, _ := mockDB.GetDatastoreForWallet(wallet.Litecoin) + ltcCfg := CoinConfig{ + CoinType: wallet.Litecoin, + FeeAPI: "", + LowFee: 140, + MediumFee: 160, + HighFee: 180, + MaxFee: 2000, + ClientAPIs: apiEndpoints, + DB: db, + } + cfg.Coins = append(cfg.Coins, ltcCfg) + } + if coinTypes[wallet.Ethereum] { + var apiEndpoints []string + if !testnet { + apiEndpoints = []string{ + "https://rinkeby.infura.io", + } + } else { + apiEndpoints = []string{ + "https://rinkeby.infura.io", + } + } + db, _ := mockDB.GetDatastoreForWallet(wallet.Ethereum) + ethCfg := CoinConfig{ + CoinType: wallet.Ethereum, + FeeAPI: "", + LowFee: 140, + MediumFee: 160, + HighFee: 180, + MaxFee: 2000, + ClientAPIs: apiEndpoints, + DB: db, + Options: map[string]interface{}{ + "RegistryAddress": EthereumRegistryAddressMainnet, + "RinkebyRegistryAddress": EthereumRegistryAddressRinkeby, + "RopstenRegistryAddress": EthereumRegistryAddressRopsten, + }, + } + cfg.Coins = append(cfg.Coins, ethCfg) + } + return cfg +} diff --git a/datastore/mock.go b/datastore/mock.go new file mode 100644 index 0000000..cc34cba --- /dev/null +++ b/datastore/mock.go @@ -0,0 +1,446 @@ +package datastore + +import ( + "bytes" + "encoding/hex" + "errors" + "sort" + "strconv" + "sync" + "time" + + "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg/chainhash" +) + +type MockDatastore struct { + keys wallet.Keys + utxos wallet.Utxos + stxos wallet.Stxos + txns wallet.Txns + watchedScripts wallet.WatchedScripts +} + +type MockMultiwalletDatastore struct { + db map[wallet.CoinType]wallet.Datastore + sync.Mutex +} + +func (m *MockMultiwalletDatastore) GetDatastoreForWallet(coinType wallet.CoinType) (wallet.Datastore, error) { + m.Lock() + defer m.Unlock() + db, ok := m.db[coinType] + if !ok { + return nil, errors.New("Cointype not supported") + } + return db, nil +} + +func NewMockMultiwalletDatastore() *MockMultiwalletDatastore { + db := make(map[wallet.CoinType]wallet.Datastore) + db[wallet.Bitcoin] = wallet.Datastore(&MockDatastore{ + &MockKeyStore{Keys: make(map[string]*KeyStoreEntry)}, + &MockUtxoStore{utxos: make(map[string]*wallet.Utxo)}, + &MockStxoStore{stxos: make(map[string]*wallet.Stxo)}, + &MockTxnStore{txns: make(map[string]*txnStoreEntry)}, + &MockWatchedScriptsStore{scripts: make(map[string][]byte)}, + }) + db[wallet.BitcoinCash] = wallet.Datastore(&MockDatastore{ + &MockKeyStore{Keys: make(map[string]*KeyStoreEntry)}, + &MockUtxoStore{utxos: make(map[string]*wallet.Utxo)}, + &MockStxoStore{stxos: make(map[string]*wallet.Stxo)}, + &MockTxnStore{txns: make(map[string]*txnStoreEntry)}, + &MockWatchedScriptsStore{scripts: make(map[string][]byte)}, + }) + db[wallet.Zcash] = wallet.Datastore(&MockDatastore{ + &MockKeyStore{Keys: make(map[string]*KeyStoreEntry)}, + &MockUtxoStore{utxos: make(map[string]*wallet.Utxo)}, + &MockStxoStore{stxos: make(map[string]*wallet.Stxo)}, + &MockTxnStore{txns: make(map[string]*txnStoreEntry)}, + &MockWatchedScriptsStore{scripts: make(map[string][]byte)}, + }) + db[wallet.Litecoin] = wallet.Datastore(&MockDatastore{ + &MockKeyStore{Keys: make(map[string]*KeyStoreEntry)}, + &MockUtxoStore{utxos: make(map[string]*wallet.Utxo)}, + &MockStxoStore{stxos: make(map[string]*wallet.Stxo)}, + &MockTxnStore{txns: make(map[string]*txnStoreEntry)}, + &MockWatchedScriptsStore{scripts: make(map[string][]byte)}, + }) + db[wallet.Ethereum] = wallet.Datastore(&MockDatastore{ + &MockKeyStore{Keys: make(map[string]*KeyStoreEntry)}, + &MockUtxoStore{utxos: make(map[string]*wallet.Utxo)}, + &MockStxoStore{stxos: make(map[string]*wallet.Stxo)}, + &MockTxnStore{txns: make(map[string]*txnStoreEntry)}, + &MockWatchedScriptsStore{scripts: make(map[string][]byte)}, + }) + return &MockMultiwalletDatastore{db: db} +} + +func (m *MockDatastore) Keys() wallet.Keys { + return m.keys +} + +func (m *MockDatastore) Utxos() wallet.Utxos { + return m.utxos +} + +func (m *MockDatastore) Stxos() wallet.Stxos { + return m.stxos +} + +func (m *MockDatastore) Txns() wallet.Txns { + return m.txns +} + +func (m *MockDatastore) WatchedScripts() wallet.WatchedScripts { + return m.watchedScripts +} + +type KeyStoreEntry struct { + ScriptAddress []byte + Path wallet.KeyPath + Used bool + Key *btcec.PrivateKey +} + +type MockKeyStore struct { + Keys map[string]*KeyStoreEntry + sync.Mutex +} + +func (m *MockKeyStore) Put(scriptAddress []byte, keyPath wallet.KeyPath) error { + m.Lock() + defer m.Unlock() + m.Keys[hex.EncodeToString(scriptAddress)] = &KeyStoreEntry{scriptAddress, keyPath, false, nil} + return nil +} + +func (m *MockKeyStore) ImportKey(scriptAddress []byte, key *btcec.PrivateKey) error { + m.Lock() + defer m.Unlock() + kp := wallet.KeyPath{Purpose: wallet.EXTERNAL, Index: -1} + m.Keys[hex.EncodeToString(scriptAddress)] = &KeyStoreEntry{scriptAddress, kp, false, key} + return nil +} + +func (m *MockKeyStore) MarkKeyAsUsed(scriptAddress []byte) error { + m.Lock() + defer m.Unlock() + key, ok := m.Keys[hex.EncodeToString(scriptAddress)] + if !ok { + return errors.New("key does not exist") + } + key.Used = true + return nil +} + +func (m *MockKeyStore) GetLastKeyIndex(purpose wallet.KeyPurpose) (int, bool, error) { + m.Lock() + defer m.Unlock() + i := -1 + used := false + for _, key := range m.Keys { + if key.Path.Purpose == purpose && key.Path.Index > i { + i = key.Path.Index + used = key.Used + } + } + if i == -1 { + return i, used, errors.New("No saved keys") + } + return i, used, nil +} + +func (m *MockKeyStore) GetPathForKey(scriptAddress []byte) (wallet.KeyPath, error) { + m.Lock() + defer m.Unlock() + key, ok := m.Keys[hex.EncodeToString(scriptAddress)] + if !ok || key.Path.Index == -1 { + return wallet.KeyPath{}, errors.New("key does not exist") + } + return key.Path, nil +} + +func (m *MockKeyStore) GetKey(scriptAddress []byte) (*btcec.PrivateKey, error) { + m.Lock() + defer m.Unlock() + for _, k := range m.Keys { + if k.Path.Index == -1 && bytes.Equal(scriptAddress, k.ScriptAddress) { + return k.Key, nil + } + } + return nil, errors.New("Not found") +} + +func (m *MockKeyStore) GetImported() ([]*btcec.PrivateKey, error) { + m.Lock() + defer m.Unlock() + var keys []*btcec.PrivateKey + for _, k := range m.Keys { + if k.Path.Index == -1 { + keys = append(keys, k.Key) + } + } + return keys, nil +} + +func (m *MockKeyStore) GetUnused(purpose wallet.KeyPurpose) ([]int, error) { + m.Lock() + defer m.Unlock() + var i []int + for _, key := range m.Keys { + if !key.Used && key.Path.Purpose == purpose { + i = append(i, key.Path.Index) + } + } + sort.Ints(i) + return i, nil +} + +func (m *MockKeyStore) GetAll() ([]wallet.KeyPath, error) { + m.Lock() + defer m.Unlock() + var kp []wallet.KeyPath + for _, key := range m.Keys { + kp = append(kp, key.Path) + } + return kp, nil +} + +func (m *MockKeyStore) GetLookaheadWindows() map[wallet.KeyPurpose]int { + m.Lock() + defer m.Unlock() + internalLastUsed := -1 + externalLastUsed := -1 + for _, key := range m.Keys { + if key.Path.Purpose == wallet.INTERNAL && key.Used && key.Path.Index > internalLastUsed { + internalLastUsed = key.Path.Index + } + if key.Path.Purpose == wallet.EXTERNAL && key.Used && key.Path.Index > externalLastUsed { + externalLastUsed = key.Path.Index + } + } + internalUnused := 0 + externalUnused := 0 + for _, key := range m.Keys { + if key.Path.Purpose == wallet.INTERNAL && !key.Used && key.Path.Index > internalLastUsed { + internalUnused++ + } + if key.Path.Purpose == wallet.EXTERNAL && !key.Used && key.Path.Index > externalLastUsed { + externalUnused++ + } + } + mp := make(map[wallet.KeyPurpose]int) + mp[wallet.INTERNAL] = internalUnused + mp[wallet.EXTERNAL] = externalUnused + return mp +} + +type MockUtxoStore struct { + utxos map[string]*wallet.Utxo + sync.Mutex +} + +func (m *MockUtxoStore) Put(utxo wallet.Utxo) error { + m.Lock() + defer m.Unlock() + key := utxo.Op.Hash.String() + ":" + strconv.Itoa(int(utxo.Op.Index)) + m.utxos[key] = &utxo + return nil +} + +func (m *MockUtxoStore) GetAll() ([]wallet.Utxo, error) { + m.Lock() + defer m.Unlock() + var utxos []wallet.Utxo + for _, v := range m.utxos { + utxos = append(utxos, *v) + } + return utxos, nil +} + +func (m *MockUtxoStore) SetWatchOnly(utxo wallet.Utxo) error { + m.Lock() + defer m.Unlock() + key := utxo.Op.Hash.String() + ":" + strconv.Itoa(int(utxo.Op.Index)) + u, ok := m.utxos[key] + if !ok { + return errors.New("Not found") + } + u.WatchOnly = true + return nil +} + +func (m *MockUtxoStore) Delete(utxo wallet.Utxo) error { + m.Lock() + defer m.Unlock() + key := utxo.Op.Hash.String() + ":" + strconv.Itoa(int(utxo.Op.Index)) + _, ok := m.utxos[key] + if !ok { + return errors.New("Not found") + } + delete(m.utxos, key) + return nil +} + +type MockStxoStore struct { + stxos map[string]*wallet.Stxo + sync.Mutex +} + +func (m *MockStxoStore) Put(stxo wallet.Stxo) error { + m.Lock() + defer m.Unlock() + m.stxos[stxo.SpendTxid.String()] = &stxo + return nil +} + +func (m *MockStxoStore) GetAll() ([]wallet.Stxo, error) { + m.Lock() + defer m.Unlock() + var stxos []wallet.Stxo + for _, v := range m.stxos { + stxos = append(stxos, *v) + } + return stxos, nil +} + +func (m *MockStxoStore) Delete(stxo wallet.Stxo) error { + m.Lock() + defer m.Unlock() + _, ok := m.stxos[stxo.SpendTxid.String()] + if !ok { + return errors.New("Not found") + } + delete(m.stxos, stxo.SpendTxid.String()) + return nil +} + +type txnStoreEntry struct { + txn []byte + value int + height int + timestamp time.Time + watchOnly bool +} + +type MockTxnStore struct { + txns map[string]*txnStoreEntry + sync.Mutex +} + +func (m *MockTxnStore) Put(tx []byte, txid string, value, height int, timestamp time.Time, watchOnly bool) error { + m.Lock() + defer m.Unlock() + m.txns[txid] = &txnStoreEntry{ + txn: tx, + value: value, + height: height, + timestamp: timestamp, + watchOnly: watchOnly, + } + return nil +} + +func (m *MockTxnStore) Get(txid chainhash.Hash) (wallet.Txn, error) { + m.Lock() + defer m.Unlock() + t, ok := m.txns[txid.String()] + if !ok { + return wallet.Txn{}, errors.New("Not found") + } + return wallet.Txn{ + Txid: txid.String(), + Value: int64(t.value), + Height: int32(t.height), + Timestamp: t.timestamp, + WatchOnly: t.watchOnly, + Bytes: t.txn, + }, nil +} + +func (m *MockTxnStore) GetAll(includeWatchOnly bool) ([]wallet.Txn, error) { + m.Lock() + defer m.Unlock() + var txns []wallet.Txn + for txid, t := range m.txns { + txn := wallet.Txn{ + Txid: txid, + Value: int64(t.value), + Height: int32(t.height), + Timestamp: t.timestamp, + WatchOnly: t.watchOnly, + Bytes: t.txn, + } + txns = append(txns, txn) + } + return txns, nil +} + +func (m *MockTxnStore) UpdateHeight(txid chainhash.Hash, height int, timestamp time.Time) error { + m.Lock() + defer m.Unlock() + txn, ok := m.txns[txid.String()] + if !ok { + return errors.New("Not found") + } + txn.height = height + txn.timestamp = timestamp + m.txns[txid.String()] = txn + return nil +} + +func (m *MockTxnStore) Delete(txid *chainhash.Hash) error { + m.Lock() + defer m.Unlock() + _, ok := m.txns[txid.String()] + if !ok { + return errors.New("Not found") + } + delete(m.txns, txid.String()) + return nil +} + +type MockWatchedScriptsStore struct { + scripts map[string][]byte + sync.Mutex +} + +func (m *MockWatchedScriptsStore) PutAll(scriptPubKeys [][]byte) error { + m.Lock() + defer m.Unlock() + for _, scriptPubKey := range scriptPubKeys { + m.scripts[hex.EncodeToString(scriptPubKey)] = scriptPubKey + } + return nil +} + +func (m *MockWatchedScriptsStore) Put(scriptPubKey []byte) error { + m.Lock() + defer m.Unlock() + m.scripts[hex.EncodeToString(scriptPubKey)] = scriptPubKey + return nil +} + +func (m *MockWatchedScriptsStore) GetAll() ([][]byte, error) { + m.Lock() + defer m.Unlock() + var ret [][]byte + for _, b := range m.scripts { + ret = append(ret, b) + } + return ret, nil +} + +func (m *MockWatchedScriptsStore) Delete(scriptPubKey []byte) error { + m.Lock() + defer m.Unlock() + enc := hex.EncodeToString(scriptPubKey) + _, ok := m.scripts[enc] + if !ok { + return errors.New("Not found") + } + delete(m.scripts, enc) + return nil +} 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/keys/keys.go b/keys/keys.go new file mode 100644 index 0000000..e155b5e --- /dev/null +++ b/keys/keys.go @@ -0,0 +1,201 @@ +package keys + +import ( + "errors" + "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + hd "github.com/btcsuite/btcutil/hdkeychain" +) + +const LOOKAHEADWINDOW = 20 + +type KeyManager struct { + datastore wallet.Keys + params *chaincfg.Params + + internalKey *hd.ExtendedKey + externalKey *hd.ExtendedKey + + coinType wallet.CoinType + getAddr AddrFunc +} + +type AddrFunc func(k *hd.ExtendedKey, net *chaincfg.Params) (btcutil.Address, error) + +func NewKeyManager(db wallet.Keys, params *chaincfg.Params, masterPrivKey *hd.ExtendedKey, coinType wallet.CoinType, getAddr AddrFunc) (*KeyManager, error) { + internal, external, err := Bip44Derivation(masterPrivKey, coinType) + if err != nil { + return nil, err + } + km := &KeyManager{ + datastore: db, + params: params, + internalKey: internal, + externalKey: external, + coinType: coinType, + getAddr: getAddr, + } + if err := km.lookahead(); err != nil { + return nil, err + } + return km, nil +} + +// m / purpose' / coin_type' / account' / change / address_index +func Bip44Derivation(masterPrivKey *hd.ExtendedKey, coinType wallet.CoinType) (internal, external *hd.ExtendedKey, err error) { + // Purpose = bip44 + fourtyFour, err := masterPrivKey.Child(hd.HardenedKeyStart + 44) + if err != nil { + return nil, nil, err + } + // Cointype + bitcoin, err := fourtyFour.Child(hd.HardenedKeyStart + uint32(coinType)) + if err != nil { + return nil, nil, err + } + // Account = 0 + account, err := bitcoin.Child(hd.HardenedKeyStart + 0) + if err != nil { + return nil, nil, err + } + // Change(0) = external + external, err = account.Child(0) + if err != nil { + return nil, nil, err + } + // Change(1) = internal + internal, err = account.Child(1) + if err != nil { + return nil, nil, err + } + return internal, external, nil +} + +func (km *KeyManager) GetCurrentKey(purpose wallet.KeyPurpose) (*hd.ExtendedKey, error) { + i, err := km.datastore.GetUnused(purpose) + if err != nil { + return nil, err + } + if len(i) == 0 { + return nil, errors.New("no unused keys in database") + } + return km.GenerateChildKey(purpose, uint32(i[0])) +} + +func (km *KeyManager) GetFreshKey(purpose wallet.KeyPurpose) (*hd.ExtendedKey, error) { + index, _, err := km.datastore.GetLastKeyIndex(purpose) + var childKey *hd.ExtendedKey + if err != nil { + index = 0 + } else { + index += 1 + } + for { + // There is a small possibility bip32 keys can be invalid. The procedure in such cases + // is to discard the key and derive the next one. This loop will continue until a valid key + // is derived. + childKey, err = km.GenerateChildKey(purpose, uint32(index)) + if err == nil { + break + } + index += 1 + } + addr, err := km.KeyToAddress(childKey) + if err != nil { + return nil, err + } + p := wallet.KeyPath{Purpose: purpose, Index: index} + err = km.datastore.Put(addr.ScriptAddress(), p) + if err != nil { + return nil, err + } + return childKey, nil +} + +func (km *KeyManager) GetNextUnused(purpose wallet.KeyPurpose) (*hd.ExtendedKey, error) { + if err := km.lookahead(); err != nil { + return nil, err + } + i, err := km.datastore.GetUnused(purpose) + if err != nil { + return nil, err + } + key, err := km.GenerateChildKey(purpose, uint32(i[1])) + if err != nil { + return nil, err + } + return key, nil +} + +func (km *KeyManager) GetKeys() []*hd.ExtendedKey { + var keys []*hd.ExtendedKey + keyPaths, err := km.datastore.GetAll() + if err != nil { + return keys + } + for _, path := range keyPaths { + k, err := km.GenerateChildKey(path.Purpose, uint32(path.Index)) + if err != nil { + continue + } + keys = append(keys, k) + } + return keys +} + +func (km *KeyManager) GetKeyForScript(scriptAddress []byte) (*hd.ExtendedKey, error) { + keyPath, err := km.datastore.GetPathForKey(scriptAddress) + if err != nil { + key, err := km.datastore.GetKey(scriptAddress) + if err != nil { + return nil, err + } + hdKey := hd.NewExtendedKey( + km.params.HDPrivateKeyID[:], + key.Serialize(), + make([]byte, 32), + []byte{0x00, 0x00, 0x00, 0x00}, + 0, + 0, + true) + return hdKey, nil + } + return km.GenerateChildKey(keyPath.Purpose, uint32(keyPath.Index)) +} + +// Mark the given key as used and extend the lookahead window +func (km *KeyManager) MarkKeyAsUsed(scriptAddress []byte) error { + if err := km.datastore.MarkKeyAsUsed(scriptAddress); err != nil { + return err + } + return km.lookahead() +} + +func (km *KeyManager) GenerateChildKey(purpose wallet.KeyPurpose, index uint32) (*hd.ExtendedKey, error) { + if purpose == wallet.EXTERNAL { + return km.externalKey.Child(index) + } else if purpose == wallet.INTERNAL { + return km.internalKey.Child(index) + } + return nil, errors.New("unknown key purpose") +} + +func (km *KeyManager) lookahead() error { + lookaheadWindows := km.datastore.GetLookaheadWindows() + for purpose, size := range lookaheadWindows { + if size < LOOKAHEADWINDOW { + for i := 0; i < (LOOKAHEADWINDOW - size); i++ { + _, err := km.GetFreshKey(purpose) + if err != nil { + return err + } + } + } + } + return nil +} + +func (km *KeyManager) KeyToAddress(key *hd.ExtendedKey) (btcutil.Address, error) { + return km.getAddr(key, km.params) +} diff --git a/keys/keys_test.go b/keys/keys_test.go new file mode 100644 index 0000000..d16fd51 --- /dev/null +++ b/keys/keys_test.go @@ -0,0 +1,342 @@ +package keys + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/OpenBazaar/multiwallet/datastore" + "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" +) + +func createKeyManager() (*KeyManager, error) { + masterPrivKey, err := hdkeychain.NewKeyFromString("xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6") + if err != nil { + return nil, err + } + return NewKeyManager(&datastore.MockKeyStore{Keys: make(map[string]*datastore.KeyStoreEntry)}, &chaincfg.MainNetParams, masterPrivKey, wallet.Bitcoin, bitcoinAddress) +} + +func bitcoinAddress(key *hdkeychain.ExtendedKey, params *chaincfg.Params) (btcutil.Address, error) { + return key.Address(params) +} + +func TestNewKeyManager(t *testing.T) { + km, err := createKeyManager() + if err != nil { + t.Error(err) + } + keys, err := km.datastore.GetAll() + if err != nil { + t.Error(err) + } + if len(keys) != LOOKAHEADWINDOW*2 { + t.Error("Failed to generate lookahead windows when creating a new KeyManager") + } +} + +func TestBip44Derivation(t *testing.T) { + masterPrivKey, err := hdkeychain.NewKeyFromString("xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6") + if err != nil { + t.Error(err) + } + internal, external, err := Bip44Derivation(masterPrivKey, wallet.Bitcoin) + if err != nil { + t.Error(err) + } + externalKey, err := external.Child(0) + if err != nil { + t.Error(err) + } + externalAddr, err := externalKey.Address(&chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + if externalAddr.String() != "17rxURoF96VhmkcEGCj5LNQkmN9HVhWb7F" { + t.Error("Incorrect Bip44 key derivation") + } + + internalKey, err := internal.Child(0) + if err != nil { + t.Error(err) + } + internalAddr, err := internalKey.Address(&chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + if internalAddr.String() != "16wbbYdecq9QzXdxa58q2dYXJRc8sfkE4J" { + t.Error("Incorrect Bip44 key derivation") + } +} + +func TestKeys_generateChildKey(t *testing.T) { + km, err := createKeyManager() + if err != nil { + t.Error(err) + } + internalKey, err := km.GenerateChildKey(wallet.INTERNAL, 0) + internalAddr, err := internalKey.Address(&chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + if internalAddr.String() != "16wbbYdecq9QzXdxa58q2dYXJRc8sfkE4J" { + t.Error("generateChildKey returned incorrect key") + } + externalKey, err := km.GenerateChildKey(wallet.EXTERNAL, 0) + externalAddr, err := externalKey.Address(&chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + if externalAddr.String() != "17rxURoF96VhmkcEGCj5LNQkmN9HVhWb7F" { + t.Error("generateChildKey returned incorrect key") + } +} + +func TestKeyManager_lookahead(t *testing.T) { + masterPrivKey, err := hdkeychain.NewKeyFromString("xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6") + if err != nil { + t.Error(err) + } + mock := &datastore.MockKeyStore{Keys: make(map[string]*datastore.KeyStoreEntry)} + km, err := NewKeyManager(mock, &chaincfg.MainNetParams, masterPrivKey, wallet.Bitcoin, bitcoinAddress) + if err != nil { + t.Error(err) + } + for _, key := range mock.Keys { + key.Used = true + } + n := len(mock.Keys) + err = km.lookahead() + if err != nil { + t.Error(err) + } + if len(mock.Keys) != n+(LOOKAHEADWINDOW*2) { + t.Error("Failed to generated a correct lookahead window") + } + unused := 0 + for _, k := range mock.Keys { + if !k.Used { + unused++ + } + } + if unused != LOOKAHEADWINDOW*2 { + t.Error("Failed to generated unused keys in lookahead window") + } +} + +func TestKeyManager_MarkKeyAsUsed(t *testing.T) { + km, err := createKeyManager() + if err != nil { + t.Error(err) + } + i, err := km.datastore.GetUnused(wallet.EXTERNAL) + if err != nil { + t.Error(err) + } + if len(i) == 0 { + t.Error("No unused keys in database") + } + key, err := km.GenerateChildKey(wallet.EXTERNAL, uint32(i[0])) + if err != nil { + t.Error(err) + } + addr, err := key.Address(&chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + err = km.MarkKeyAsUsed(addr.ScriptAddress()) + if err != nil { + t.Error(err) + } + if len(km.GetKeys()) != (LOOKAHEADWINDOW*2)+1 { + t.Error("Failed to extend lookahead window when marking as read") + } + unused, err := km.datastore.GetUnused(wallet.EXTERNAL) + if err != nil { + t.Error(err) + } + for _, i := range unused { + if i == 0 { + t.Error("Failed to mark key as used") + } + } +} + +func TestKeyManager_GetCurrentKey(t *testing.T) { + masterPrivKey, err := hdkeychain.NewKeyFromString("xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6") + if err != nil { + t.Error(err) + } + mock := &datastore.MockKeyStore{Keys: make(map[string]*datastore.KeyStoreEntry)} + km, err := NewKeyManager(mock, &chaincfg.MainNetParams, masterPrivKey, wallet.Bitcoin, bitcoinAddress) + if err != nil { + t.Error(err) + } + var scriptAddress string + for script, key := range mock.Keys { + if key.Path.Purpose == wallet.EXTERNAL && key.Path.Index == 0 { + scriptAddress = script + break + } + } + key, err := km.GetCurrentKey(wallet.EXTERNAL) + if err != nil { + t.Error(err) + } + addr, err := key.Address(&chaincfg.Params{}) + if err != nil { + t.Error(err) + } + if hex.EncodeToString(addr.ScriptAddress()) != scriptAddress { + t.Error("CurrentKey returned wrong key") + } +} + +func TestKeyManager_GetFreshKey(t *testing.T) { + km, err := createKeyManager() + if err != nil { + t.Error(err) + } + key, err := km.GetFreshKey(wallet.EXTERNAL) + if err != nil { + t.Error(err) + } + if len(km.GetKeys()) != LOOKAHEADWINDOW*2+1 { + t.Error("Failed to create additional key") + } + key2, err := km.GenerateChildKey(wallet.EXTERNAL, 20) + if err != nil { + t.Error(err) + } + if key.String() != key2.String() { + t.Error("GetFreshKey returned incorrect key") + } +} + +func TestKeyManager_GetNextUnused(t *testing.T) { + km, err := createKeyManager() + if err != nil { + t.Fatal(err) + } + + // Since the lookahead window has already been generated, GetNextUnused + // should return the key with index 1. + key, err := km.GetNextUnused(wallet.EXTERNAL) + if err != nil { + t.Fatal(err) + } + + nextUnused, err := km.GenerateChildKey(wallet.EXTERNAL, uint32(1)) + if err != nil { + t.Fatal(err) + } + + if key.String() != nextUnused.String() { + t.Errorf("Derived incorrect key. Expected %s got %s", nextUnused.String(), key.String()) + } + + // Next let's mark all the keys as used and make sure GetNextUnused still + // generates a lookahead window and returns the next unused key. + allKeys := km.GetKeys() + for _, key := range allKeys { + addr, err := key.Address(&chaincfg.MainNetParams) + if err != nil { + t.Fatal(err) + } + if err := km.datastore.MarkKeyAsUsed(addr.ScriptAddress()); err != nil { + t.Fatal(err) + } + } + + key, err = km.GetNextUnused(wallet.EXTERNAL) + if err != nil { + t.Fatal(err) + } + + nextUnused, err = km.GenerateChildKey(wallet.EXTERNAL, uint32(21)) + if err != nil { + t.Fatal(err) + } + + if key.String() != nextUnused.String() { + t.Errorf("Derived incorrect key. Expected %s got %s", nextUnused.String(), key.String()) + } +} + +func TestKeyManager_GetKeys(t *testing.T) { + km, err := createKeyManager() + if err != nil { + t.Error(err) + } + keys := km.GetKeys() + if len(keys) != LOOKAHEADWINDOW*2 { + t.Error("Returned incorrect number of keys") + } + for _, key := range keys { + if key == nil { + t.Error("Incorrectly returned nil key") + } + } +} + +func TestKeyManager_GetKeyForScript(t *testing.T) { + masterPrivKey, err := hdkeychain.NewKeyFromString("xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6") + if err != nil { + t.Error(err) + } + mock := &datastore.MockKeyStore{Keys: make(map[string]*datastore.KeyStoreEntry)} + km, err := NewKeyManager(mock, &chaincfg.MainNetParams, masterPrivKey, wallet.Bitcoin, bitcoinAddress) + if err != nil { + t.Error(err) + } + addr, err := btcutil.DecodeAddress("17rxURoF96VhmkcEGCj5LNQkmN9HVhWb7F", &chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + key, err := km.GetKeyForScript(addr.ScriptAddress()) + if err != nil { + t.Error(err) + } + if key == nil { + t.Error("Returned key is nil") + } + testAddr, err := key.Address(&chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + if testAddr.String() != addr.String() { + t.Error("Returned incorrect key") + } + importKey, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + t.Error(err) + } + importAddr, err := key.Address(&chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + importScript, err := txscript.PayToAddrScript(importAddr) + if err != nil { + t.Error(err) + } + err = km.datastore.ImportKey(importScript, importKey) + if err != nil { + t.Error(err) + } + retKey, err := km.GetKeyForScript(importScript) + if err != nil { + t.Error(err) + } + retECKey, err := retKey.ECPrivKey() + if err != nil { + t.Error(err) + } + if !bytes.Equal(retECKey.Serialize(), importKey.Serialize()) { + t.Error("Failed to return imported key") + } +} diff --git a/litecoin/address/address.go b/litecoin/address/address.go new file mode 100644 index 0000000..2626667 --- /dev/null +++ b/litecoin/address/address.go @@ -0,0 +1,792 @@ +package address + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "strings" + + lparams "github.com/OpenBazaar/multiwallet/litecoin/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 = 0x32 + NetIDTestnetP2SH2 = 0x3A +) + +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 litecoin 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 litecoin 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 litecoin 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 litecoin 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 litecoin 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 litecoin 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/litecoin/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 litecoin 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/litecoin/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 litecoin 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/litecoin/address/address_test.go b/litecoin/address/address_test.go new file mode 100644 index 0000000..c392021 --- /dev/null +++ b/litecoin/address/address_test.go @@ -0,0 +1,104 @@ +package address + +import ( + "github.com/btcsuite/btcd/chaincfg" + "testing" +) + +func TestDecodeLitecoinAddress(t *testing.T) { + // Mainnet + addr, err := DecodeAddress("ltc1qj065d66h5943s357vfd9kltn6k4atn3qwqy8frycnfcf4ycwhrtqr6496q", &chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + if addr.String() != "ltc1qj065d66h5943s357vfd9kltn6k4atn3qwqy8frycnfcf4ycwhrtqr6496q" { + t.Error("Address decoding error") + } + addr1, err := DecodeAddress("LKxmT8iooGt2d9xQn1y8PU6KwW3J8EDQ9a", &chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + if addr1.String() != "LKxmT8iooGt2d9xQn1y8PU6KwW3J8EDQ9a" { + t.Error("Address decoding error") + } + // Testnet + addr, err = DecodeAddress("mjFBdzsYNBCeabLNwyYYCt8epG7GhzYeTw", &chaincfg.TestNet3Params) + if err != nil { + t.Error(err) + } + if addr.String() != "mjFBdzsYNBCeabLNwyYYCt8epG7GhzYeTw" { + t.Error("Address decoding error") + } + + // Testnet witness + addr, err = DecodeAddress("tltc1qxjqda2dlef5250yqgdhyscj2n2sv98yt6f9ewzvrmt0v86xuefxs9xya9u", &chaincfg.TestNet3Params) + if err != nil { + t.Error(err) + } + if addr.String() != "tltc1qxjqda2dlef5250yqgdhyscj2n2sv98yt6f9ewzvrmt0v86xuefxs9xya9u" { + t.Error("Address decoding error") + } + +} + +var dataElement = []byte{203, 72, 18, 50, 41, 156, 213, 116, 49, 81, 172, 75, 45, 99, 174, 25, 142, 123, 176, 169} + +// Second address of https://github.com/Bitcoin-UAHF/spec/blob/master/cashaddr.md#examples-of-address-translation +func TestAddressPubKeyHash_EncodeAddress(t *testing.T) { + // Mainnet + addr, err := NewAddressPubKeyHash(dataElement, &chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + if addr.String() != "LdkomjvYVsoY5DdZx3LJVd1dXRhpKc18Xa" { + t.Error("Address decoding error") + } + // Testnet + addr, err = NewAddressPubKeyHash(dataElement, &chaincfg.TestNet3Params) + if err != nil { + t.Error(err) + } + if addr.String() != "mz3ooahhEEzjbXR2VUKP3XACBCwF5zhQBy" { + t.Error("Address decoding error") + } +} + +var dataElement2 = []byte{118, 160, 64, 83, 189, 160, 168, 139, 218, 81, 119, 184, 106, 21, 195, 178, 159, 85, 152, 115, 118, 160, 64, 83, 189, 160, 168, 139, 218, 81, 119, 184} + +// 4th address of https://github.com/Bitcoin-UAHF/spec/blob/master/cashaddr.md#examples-of-address-translation +func TestWitnessScriptHash_EncodeAddress(t *testing.T) { + // Mainnet + addr, err := NewAddressWitnessScriptHash(dataElement2, &chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + if addr.String() != "ltc1qw6syq5aa5z5ghkj3w7ux59wrk204txrnw6syq5aa5z5ghkj3w7uqdjs2cd" { + t.Error("Address decoding error") + } + // Testnet + addr, err = NewAddressWitnessScriptHash(dataElement2, &chaincfg.TestNet3Params) + if err != nil { + t.Error(err) + } + if addr.String() != "tltc1qw6syq5aa5z5ghkj3w7ux59wrk204txrnw6syq5aa5z5ghkj3w7uqxa558c" { + t.Error("Address decoding error") + } +} + +func TestScriptParsing(t *testing.T) { + addr, err := DecodeAddress("ltc1qj065d66h5943s357vfd9kltn6k4atn3qwqy8frycnfcf4ycwhrtqr6496q", &chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + script, err := PayToAddrScript(addr) + if err != nil { + t.Error(err) + } + addr2, err := ExtractPkScriptAddrs(script, &chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + if addr.String() != addr2.String() { + t.Error("Failed to convert script back into address") + } +} diff --git a/litecoin/exchange_rates.go b/litecoin/exchange_rates.go new file mode 100644 index 0000000..47f08f0 --- /dev/null +++ b/litecoin/exchange_rates.go @@ -0,0 +1,339 @@ +package litecoin + +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 LitecoinPriceFetcher struct { + sync.Mutex + cache map[string]float64 + providers []*ExchangeRateProvider +} + +func NewLitecoinPriceFetcher(dialer proxy.Dialer) *LitecoinPriceFetcher { + bp := exchange.NewBitcoinPriceFetcher(dialer) + z := LitecoinPriceFetcher{ + 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 *LitecoinPriceFetcher) 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 *LitecoinPriceFetcher) 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 *LitecoinPriceFetcher) 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 *LitecoinPriceFetcher) UnitsPerCoin() int { + return exchange.SatoshiPerBTC +} + +func (z *LitecoinPriceFetcher) 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 *LitecoinPriceFetcher) 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-litecoin 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-litecoin 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-litecoin 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-litecoin 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/litecoin/params/params.go b/litecoin/params/params.go new file mode 100644 index 0000000..ce4752b --- /dev/null +++ b/litecoin/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 = 0x05 +} + +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/litecoin/sign.go b/litecoin/sign.go new file mode 100644 index 0000000..bf4af00 --- /dev/null +++ b/litecoin/sign.go @@ -0,0 +1,675 @@ +package litecoin + +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/litecoin/address" + "github.com/OpenBazaar/multiwallet/util" +) + +func (w *LitecoinWallet) 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 *LitecoinWallet) 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 *LitecoinWallet) 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 *LitecoinWallet) 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 *LitecoinWallet) 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 *LitecoinWallet) 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 *LitecoinWallet) 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 *LitecoinWallet) 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/litecoin/sign_test.go b/litecoin/sign_test.go new file mode 100644 index 0000000..8a95c6e --- /dev/null +++ b/litecoin/sign_test.go @@ -0,0 +1,703 @@ +package litecoin + +import ( + "bytes" + "encoding/hex" + "testing" + "time" + + "github.com/OpenBazaar/multiwallet/cache" + "github.com/OpenBazaar/multiwallet/datastore" + "github.com/OpenBazaar/multiwallet/keys" + laddr "github.com/OpenBazaar/multiwallet/litecoin/address" + "github.com/OpenBazaar/multiwallet/model/mock" + "github.com/OpenBazaar/multiwallet/service" + "github.com/OpenBazaar/multiwallet/util" + "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" +) + +type FeeResponse struct { + Priority int `json:"priority"` + Normal int `json:"normal"` + Economic int `json:"economic"` +} + +func newMockWallet() (*LitecoinWallet, error) { + mockDb := datastore.NewMockMultiwalletDatastore() + + db, err := mockDb.GetDatastoreForWallet(wallet.Litecoin) + if err != nil { + return nil, err + } + + params := &chaincfg.MainNetParams + + seed, err := hex.DecodeString("16c034c59522326867593487c03a8f9615fb248406dd0d4ffb3a6b976a248403") + if err != nil { + return nil, err + } + master, err := hdkeychain.NewMaster(seed, params) + if err != nil { + return nil, err + } + km, err := keys.NewKeyManager(db.Keys(), params, master, wallet.Litecoin, litecoinAddress) + if err != nil { + return nil, err + } + + fp := util.NewFeeProvider(2000, 300, 200, 100, nil) + + bw := &LitecoinWallet{ + params: params, + km: km, + db: db, + fp: fp, + } + cli := mock.NewMockApiClient(bw.AddressToScript) + ws, err := service.NewWalletService(db, km, cli, params, wallet.Litecoin, cache.NewMockCacher()) + if err != nil { + return nil, err + } + bw.client = cli + bw.ws = ws + return bw, nil +} + +func TestWalletService_VerifyWatchScriptFilter(t *testing.T) { + // Verify that AddWatchedAddress should never add a script which already represents a key from its own wallet + w, err := newMockWallet() + if err != nil { + t.Fatal(err) + } + keys := w.km.GetKeys() + + addr, err := w.km.KeyToAddress(keys[0]) + if err != nil { + t.Fatal(err) + } + err = w.AddWatchedAddresses(addr) + if err != nil { + t.Fatal(err) + } + + watchScripts, err := w.db.WatchedScripts().GetAll() + if err != nil { + t.Fatal(err) + } + + if len(watchScripts) != 0 { + t.Error("Put watched scripts fails on key manager owned key") + } +} + +func TestWalletService_VerifyWatchScriptPut(t *testing.T) { + // Verify that AddWatchedAddress should add a script which does not represent a key from its own wallet + w, err := newMockWallet() + if err != nil { + t.Fatal(err) + } + + addr, err := w.DecodeAddress("LhyLNfBkoKshT7R8Pce6vkB9T2cP2o84hx") + if err != nil { + t.Fatal(err) + } + + err = w.AddWatchedAddresses(addr) + if err != nil { + t.Fatal(err) + } + + watchScripts, err := w.db.WatchedScripts().GetAll() + if err != nil { + t.Fatal(err) + } + + if len(watchScripts) == 0 { + t.Error("Put watched scripts fails on non-key manager owned key") + } +} + +func waitForTxnSync(t *testing.T, txnStore wallet.Txns) { + // Look for a known txn, this sucks a bit. It would be better to check if the + // number of stored txns matched the expected, but not all the mock + // transactions are relevant, so the numbers don't add up. + // Even better would be for the wallet to signal that the initial sync was + // done. + lastTxn := mock.MockTransactions[len(mock.MockTransactions)-2] + txHash, err := chainhash.NewHashFromStr(lastTxn.Txid) + if err != nil { + t.Fatal(err) + } + + for i := 0; i < 100; i++ { + if _, err := txnStore.Get(*txHash); err == nil { + return + } + time.Sleep(100 * time.Millisecond) + } + t.Fatal("timeout waiting for wallet to sync transactions") +} + +func TestLitecoinWallet_buildTx(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + waitForTxnSync(t, w.db.Txns()) + addr, err := w.DecodeAddress("Lep9b95MtHofxS72Hjdg4Wfmr43sHetrZT") + if err != nil { + t.Error(err) + } + + // Test build normal tx + tx, err := w.buildTx(1500000, addr, wallet.NORMAL, nil) + if err != nil { + t.Error(err) + } + if !containsOutput(tx, addr) { + t.Error("Built tx does not contain the requested output") + } + if !validInputs(tx, w.db) { + t.Error("Built tx does not contain valid inputs") + } + if !validChangeAddress(tx, w.db, w.params) { + t.Error("Built tx does not contain a valid change output") + } + + // Insuffient funds + _, err = w.buildTx(1000000000, addr, wallet.NORMAL, nil) + if err != wallet.ErrorInsuffientFunds { + t.Error("Failed to throw insuffient funds error") + } + + // Dust + _, err = w.buildTx(1, addr, wallet.NORMAL, nil) + if err != wallet.ErrorDustAmount { + t.Error("Failed to throw dust error") + } +} + +func TestLitecoinWallet_buildSpendAllTx(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + time.Sleep(time.Second / 2) + + waitForTxnSync(t, w.db.Txns()) + addr, err := w.DecodeAddress("Lep9b95MtHofxS72Hjdg4Wfmr43sHetrZT") + if err != nil { + t.Error(err) + } + + // Test build spendAll tx + tx, err := w.buildSpendAllTx(addr, wallet.NORMAL) + if err != nil { + t.Error(err) + } + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Fatal(err) + } + spendableUtxos := 0 + for _, u := range utxos { + if !u.WatchOnly { + spendableUtxos++ + } + } + if len(tx.TxIn) != spendableUtxos { + t.Error("Built tx does not spend all available utxos") + } + if !containsOutput(tx, addr) { + t.Error("Built tx does not contain the requested output") + } + if !validInputs(tx, w.db) { + t.Error("Built tx does not contain valid inputs") + } + if len(tx.TxOut) != 1 { + t.Error("Built tx should only have one output") + } + + // Verify the signatures on each input using the scripting engine + for i, in := range tx.TxIn { + var prevScript []byte + for _, u := range utxos { + if util.OutPointsEqual(u.Op, in.PreviousOutPoint) { + prevScript = u.ScriptPubkey + break + } + } + vm, err := txscript.NewEngine(prevScript, tx, i, txscript.StandardVerifyFlags, nil, nil, 0) + if err != nil { + t.Fatal(err) + } + if err := vm.Execute(); err != nil { + t.Error(err) + } + } +} + +func containsOutput(tx *wire.MsgTx, addr btcutil.Address) bool { + for _, o := range tx.TxOut { + script, _ := laddr.PayToAddrScript(addr) + if bytes.Equal(script, o.PkScript) { + return true + } + } + return false +} + +func validInputs(tx *wire.MsgTx, db wallet.Datastore) bool { + utxos, _ := db.Utxos().GetAll() + uMap := make(map[wire.OutPoint]bool) + for _, u := range utxos { + uMap[u.Op] = true + } + for _, in := range tx.TxIn { + if !uMap[in.PreviousOutPoint] { + return false + } + } + return true +} + +func validChangeAddress(tx *wire.MsgTx, db wallet.Datastore, params *chaincfg.Params) bool { + for _, out := range tx.TxOut { + _, addrs, _, err := txscript.ExtractPkScriptAddrs(out.PkScript, params) + if err != nil { + continue + } + if len(addrs) == 0 { + continue + } + _, err = db.Keys().GetPathForKey(addrs[0].ScriptAddress()) + if err == nil { + return true + } + } + return false +} + +func TestLitecoinWallet_GenerateMultisigScript(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + pubkey1, err := key1.ECPubKey() + if err != nil { + t.Error(err) + } + key2, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + pubkey2, err := key2.ECPubKey() + if err != nil { + t.Error(err) + } + key3, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + pubkey3, err := key3.ECPubKey() + if err != nil { + t.Error(err) + } + keys := []hdkeychain.ExtendedKey{*key1, *key2, *key3} + + // test without timeout + addr, redeemScript, err := w.generateMultisigScript(keys, 2, 0, nil) + if err != nil { + t.Error(err) + } + if addr.String() != "ltc1qrrsyr5ktgfl3w8aahzzdz5g87yplaze7ump2vht7lj7g5fg34ruspm8n44" { + t.Error("Returned invalid address") + } + + rs := "52" + // OP_2 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey1.SerializeCompressed()) + // pubkey1 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey2.SerializeCompressed()) + // pubkey2 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey3.SerializeCompressed()) + // pubkey3 + "53" + // OP_3 + "ae" // OP_CHECKMULTISIG + rsBytes, err := hex.DecodeString(rs) + if !bytes.Equal(rsBytes, redeemScript) { + t.Error("Returned invalid redeem script") + } + + // test with timeout + key4, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + pubkey4, err := key4.ECPubKey() + if err != nil { + t.Error(err) + } + addr, redeemScript, err = w.generateMultisigScript(keys, 2, time.Hour*10, key4) + if err != nil { + t.Error(err) + } + if addr.String() != "ltc1qf5x040t6thd6mkcjsde2zxpxq2lmzp3grwau055e085vqs00qa3qscdl2k" { + t.Error("Returned invalid address") + } + + rs = "63" + // OP_IF + "52" + // OP_2 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey1.SerializeCompressed()) + // pubkey1 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey2.SerializeCompressed()) + // pubkey2 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey3.SerializeCompressed()) + // pubkey3 + "53" + // OP_3 + "ae" + // OP_CHECKMULTISIG + "67" + // OP_ELSE + "01" + // OP_PUSHDATA(1) + "3c" + // 60 blocks + "b2" + // OP_CHECKSEQUENCEVERIFY + "75" + // OP_DROP + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey4.SerializeCompressed()) + // timeout pubkey + "ac" + // OP_CHECKSIG + "68" // OP_ENDIF + rsBytes, err = hex.DecodeString(rs) + if !bytes.Equal(rsBytes, redeemScript) { + t.Error("Returned invalid redeem script") + } +} + +func TestLitecoinWallet_newUnsignedTransaction(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + waitForTxnSync(t, w.db.Txns()) + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + addr, err := w.DecodeAddress("Lep9b95MtHofxS72Hjdg4Wfmr43sHetrZT") + if err != nil { + t.Error(err) + } + + script, err := laddr.PayToAddrScript(addr) + if err != nil { + t.Error(err) + } + out := wire.NewTxOut(10000, script) + outputs := []*wire.TxOut{out} + + changeSource := func() ([]byte, error) { + addr := w.CurrentAddress(wallet.INTERNAL) + script, err := laddr.PayToAddrScript(addr) + if err != nil { + return []byte{}, err + } + return script, nil + } + + inputSource := func(target btcutil.Amount) (total btcutil.Amount, inputs []*wire.TxIn, inputValues []btcutil.Amount, scripts [][]byte, err error) { + total += btcutil.Amount(utxos[0].Value) + in := wire.NewTxIn(&utxos[0].Op, []byte{}, [][]byte{}) + in.Sequence = 0 // Opt-in RBF so we can bump fees + inputs = append(inputs, in) + return total, inputs, inputValues, scripts, nil + } + + // Regular transaction + authoredTx, err := newUnsignedTransaction(outputs, btcutil.Amount(1000), inputSource, changeSource) + if err != nil { + t.Error(err) + } + if len(authoredTx.Tx.TxOut) != 2 { + t.Error("Returned incorrect number of outputs") + } + if len(authoredTx.Tx.TxIn) != 1 { + t.Error("Returned incorrect number of inputs") + } + + // Insufficient funds + outputs[0].Value = 1000000000 + _, err = newUnsignedTransaction(outputs, btcutil.Amount(1000), inputSource, changeSource) + if err == nil { + t.Error("Failed to return insuffient funds error") + } +} + +func TestLitecoinWallet_CreateMultisigSignature(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + ins, outs, redeemScript, err := buildTxData(w) + if err != nil { + t.Error(err) + } + + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + sigs, err := w.CreateMultisigSignature(ins, outs, key1, redeemScript, 50) + if err != nil { + t.Error(err) + } + if len(sigs) != 2 { + t.Error(err) + } + for _, sig := range sigs { + if len(sig.Signature) == 0 { + t.Error("Returned empty signature") + } + } +} + +func buildTxData(w *LitecoinWallet) ([]wallet.TransactionInput, []wallet.TransactionOutput, []byte, error) { + redeemScript := "522103c157f2a7c178430972263232c9306110090c50b44d4e906ecd6d377eec89a53c210205b02b9dbe570f36d1c12e3100e55586b2b9dc61d6778c1d24a8eaca03625e7e21030c83b025cd6bdd8c06e93a2b953b821b4a8c29da211335048d7dc3389706d7e853ae" + redeemScriptBytes, err := hex.DecodeString(redeemScript) + if err != nil { + return nil, nil, nil, err + } + h1, err := hex.DecodeString("1a20f4299b4fa1f209428dace31ebf4f23f13abd8ed669cebede118343a6ae05") + if err != nil { + return nil, nil, nil, err + } + in1 := wallet.TransactionInput{ + OutpointHash: h1, + OutpointIndex: 1, + } + h2, err := hex.DecodeString("458d88b4ae9eb4a347f2e7f5592f1da3b9ddf7d40f307f6e5d7bc107a9b3e90e") + if err != nil { + return nil, nil, nil, err + } + in2 := wallet.TransactionInput{ + OutpointHash: h2, + OutpointIndex: 0, + } + addr, err := w.DecodeAddress("Lep9b95MtHofxS72Hjdg4Wfmr43sHetrZT") + if err != nil { + return nil, nil, nil, err + } + + out := wallet.TransactionOutput{ + Value: 20000, + Address: addr, + } + return []wallet.TransactionInput{in1, in2}, []wallet.TransactionOutput{out}, redeemScriptBytes, nil +} + +func TestLitecoinWallet_Multisign(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + ins, outs, redeemScript, err := buildTxData(w) + if err != nil { + t.Error(err) + } + + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + key2, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + sigs1, err := w.CreateMultisigSignature(ins, outs, key1, redeemScript, 50) + if err != nil { + t.Error(err) + } + if len(sigs1) != 2 { + t.Error(err) + } + sigs2, err := w.CreateMultisigSignature(ins, outs, key2, redeemScript, 50) + if err != nil { + t.Error(err) + } + if len(sigs2) != 2 { + t.Error(err) + } + txBytes, err := w.Multisign(ins, outs, sigs1, sigs2, redeemScript, 50, false) + if err != nil { + t.Error(err) + } + + tx := wire.NewMsgTx(0) + tx.BtcDecode(bytes.NewReader(txBytes), wire.ProtocolVersion, wire.WitnessEncoding) + if len(tx.TxIn) != 2 { + t.Error("Transactions has incorrect number of inputs") + } + if len(tx.TxOut) != 1 { + t.Error("Transactions has incorrect number of outputs") + } + for _, in := range tx.TxIn { + if len(in.Witness) == 0 { + t.Error("Input witness has zero length") + } + } +} + +func TestLitecoinWallet_bumpFee(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + waitForTxnSync(t, w.db.Txns()) + txns, err := w.db.Txns().GetAll(false) + if err != nil { + t.Error(err) + } + ch, err := chainhash.NewHashFromStr(txns[2].Txid) + if err != nil { + t.Error(err) + } + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + for _, u := range utxos { + if u.Op.Hash.IsEqual(ch) { + u.AtHeight = 0 + w.db.Utxos().Put(u) + } + } + + w.db.Txns().UpdateHeight(*ch, 0, time.Now()) + + // Test unconfirmed + _, err = w.bumpFee(*ch) + if err != nil { + t.Error(err) + } + + err = w.db.Txns().UpdateHeight(*ch, 1289597, time.Now()) + if err != nil { + t.Error(err) + } + + // Test confirmed + _, err = w.bumpFee(*ch) + if err == nil { + t.Error("Should not be able to bump fee of confirmed txs") + } +} + +func TestLitecoinWallet_sweepAddress(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + waitForTxnSync(t, w.db.Txns()) + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + + var in wallet.TransactionInput + var key *hdkeychain.ExtendedKey + for _, ut := range utxos { + if ut.Value > 0 && !ut.WatchOnly { + addr, err := w.ScriptToAddress(ut.ScriptPubkey) + if err != nil { + t.Error(err) + } + key, err = w.km.GetKeyForScript(addr.ScriptAddress()) + if err != nil { + t.Error(err) + } + h, err := hex.DecodeString(ut.Op.Hash.String()) + if err != nil { + t.Error(err) + } + in = wallet.TransactionInput{ + LinkedAddress: addr, + Value: ut.Value, + OutpointIndex: ut.Op.Index, + OutpointHash: h, + } + } + } + // P2PKH addr + _, err = w.sweepAddress([]wallet.TransactionInput{in}, nil, key, nil, wallet.NORMAL) + if err != nil { + t.Error(err) + return + } + + // 1 of 2 P2WSH + for _, ut := range utxos { + if ut.Value > 0 && ut.WatchOnly { + addr, err := w.ScriptToAddress(ut.ScriptPubkey) + if err != nil { + t.Error(err) + } + h, err := hex.DecodeString(ut.Op.Hash.String()) + if err != nil { + t.Error(err) + } + in = wallet.TransactionInput{ + LinkedAddress: addr, + Value: ut.Value, + OutpointIndex: ut.Op.Index, + OutpointHash: h, + } + } + } + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + key2, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + _, redeemScript, err := w.GenerateMultisigScript([]hdkeychain.ExtendedKey{*key1, *key2}, 1, 0, nil) + if err != nil { + t.Error(err) + } + _, err = w.sweepAddress([]wallet.TransactionInput{in}, nil, key1, &redeemScript, wallet.NORMAL) + if err != nil { + t.Error(err) + } +} + +func TestLitecoinWallet_estimateSpendFee(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + waitForTxnSync(t, w.db.Txns()) + fee, err := w.estimateSpendFee(1000, wallet.NORMAL) + if err != nil { + t.Error(err) + } + if fee <= 0 { + t.Error("Returned incorrect fee") + } +} diff --git a/litecoin/txsizes.go b/litecoin/txsizes.go new file mode 100644 index 0000000..e19726c --- /dev/null +++ b/litecoin/txsizes.go @@ -0,0 +1,249 @@ +package litecoin + +// 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/litecoin/txsizes_test.go b/litecoin/txsizes_test.go new file mode 100644 index 0000000..f0776d6 --- /dev/null +++ b/litecoin/txsizes_test.go @@ -0,0 +1,84 @@ +package litecoin + +// 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 ( + "bytes" + "encoding/hex" + "github.com/btcsuite/btcd/wire" + "testing" +) + +const ( + p2pkhScriptSize = P2PKHPkScriptSize + p2shScriptSize = 23 +) + +func makeInts(value int, n int) []int { + v := make([]int, n) + for i := range v { + v[i] = value + } + return v +} + +func TestEstimateSerializeSize(t *testing.T) { + tests := []struct { + InputCount int + OutputScriptLengths []int + AddChangeOutput bool + ExpectedSizeEstimate int + }{ + 0: {1, []int{}, false, 161}, + 1: {1, []int{p2pkhScriptSize}, false, 195}, + 2: {1, []int{}, true, 195}, + 3: {1, []int{p2pkhScriptSize}, true, 229}, + 4: {1, []int{p2shScriptSize}, false, 193}, + 5: {1, []int{p2shScriptSize}, true, 227}, + + 6: {2, []int{}, false, 310}, + 7: {2, []int{p2pkhScriptSize}, false, 344}, + 8: {2, []int{}, true, 344}, + 9: {2, []int{p2pkhScriptSize}, true, 378}, + 10: {2, []int{p2shScriptSize}, false, 342}, + 11: {2, []int{p2shScriptSize}, true, 376}, + + // 0xfd is discriminant for 16-bit compact ints, compact int + // total size increases from 1 byte to 3. + 12: {1, makeInts(p2pkhScriptSize, 0xfc), false, 8729}, + 13: {1, makeInts(p2pkhScriptSize, 0xfd), false, 8729 + P2PKHOutputSize + 2}, + 14: {1, makeInts(p2pkhScriptSize, 0xfc), true, 8729 + P2PKHOutputSize + 2}, + 15: {0xfc, []int{}, false, 37560}, + 16: {0xfd, []int{}, false, 37560 + RedeemP2PKHInputSize + 2}, + } + for i, test := range tests { + outputs := make([]*wire.TxOut, 0, len(test.OutputScriptLengths)) + for _, l := range test.OutputScriptLengths { + outputs = append(outputs, &wire.TxOut{PkScript: make([]byte, l)}) + } + actualEstimate := EstimateSerializeSize(test.InputCount, outputs, test.AddChangeOutput, P2PKH) + if actualEstimate != test.ExpectedSizeEstimate { + t.Errorf("Test %d: Got %v: Expected %v", i, actualEstimate, test.ExpectedSizeEstimate) + } + } +} + +func TestSumOutputSerializeSizes(t *testing.T) { + testTx := "0100000001066b78efa7d66d271cae6d6eb799e1d10953fb1a4a760226cc93186d52b55613010000006a47304402204e6c32cc214c496546c3277191ca734494fe49fed0af1d800db92fed2021e61802206a14d063b67f2f1c8fc18f9e9a5963fe33e18c549e56e3045e88b4fc6219be11012103f72d0a11727219bff66b8838c3c5e1c74a5257a325b0c84247bd10bdb9069e88ffffffff0200c2eb0b000000001976a914426e80ad778792e3e19c20977fb93ec0591e1a3988ac35b7cb59000000001976a914e5b6dc0b297acdd99d1a89937474df77db5743c788ac00000000" + txBytes, err := hex.DecodeString(testTx) + if err != nil { + t.Error(err) + return + } + r := bytes.NewReader(txBytes) + msgTx := wire.NewMsgTx(1) + msgTx.BtcDecode(r, 1, wire.WitnessEncoding) + if SumOutputSerializeSizes(msgTx.TxOut) != 68 { + t.Error("SumOutputSerializeSizes returned incorrect value") + } + +} diff --git a/litecoin/wallet.go b/litecoin/wallet.go new file mode 100644 index 0000000..d5de776 --- /dev/null +++ b/litecoin/wallet.go @@ -0,0 +1,516 @@ +package litecoin + +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/litecoin/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 LitecoinWallet 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(&LitecoinWallet{}) + +func NewLitecoinWallet(cfg config.CoinConfig, mnemonic string, params *chaincfg.Params, proxy proxy.Dialer, cache cache.Cacher, disableExchangeRates bool) (*LitecoinWallet, 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.Litecoin, litecoinAddress) + 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.Litecoin, cache) + if err != nil { + return nil, err + } + var er wi.ExchangeRates + if !disableExchangeRates { + er = NewLitecoinPriceFetcher(proxy) + } + + fp := util.NewFeeProvider(cfg.MaxFee, cfg.HighFee, cfg.MediumFee, cfg.LowFee, er) + + return &LitecoinWallet{ + db: cfg.DB, + km: km, + params: params, + client: c, + ws: wm, + fp: fp, + mPrivKey: mPrivKey, + mPubKey: mPubKey, + exchangeRates: er, + log: logging.MustGetLogger("litecoin-wallet"), + }, nil +} + +func litecoinAddress(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 *LitecoinWallet) Start() { + w.client.Start() + w.ws.Start() +} + +func (w *LitecoinWallet) Params() *chaincfg.Params { + return w.params +} + +func (w *LitecoinWallet) CurrencyCode() string { + if w.params.Name == chaincfg.MainNetParams.Name { + return "ltc" + } else { + return "tltc" + } +} + +func (w *LitecoinWallet) IsDust(amount int64) bool { + return txrules.IsDustAmount(ltcutil.Amount(amount), 25, txrules.DefaultRelayFeePerKb) +} + +func (w *LitecoinWallet) MasterPrivateKey() *hd.ExtendedKey { + return w.mPrivKey +} + +func (w *LitecoinWallet) MasterPublicKey() *hd.ExtendedKey { + return w.mPubKey +} + +func (w *LitecoinWallet) 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 *LitecoinWallet) 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 *LitecoinWallet) 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 *LitecoinWallet) DecodeAddress(addr string) (btcutil.Address, error) { + return laddr.DecodeAddress(addr, w.params) +} + +func (w *LitecoinWallet) ScriptToAddress(script []byte) (btcutil.Address, error) { + return laddr.ExtractPkScriptAddrs(script, w.params) +} + +func (w *LitecoinWallet) AddressToScript(addr btcutil.Address) ([]byte, error) { + return laddr.PayToAddrScript(addr) +} + +func (w *LitecoinWallet) HasKey(addr btcutil.Address) bool { + _, err := w.km.GetKeyForScript(addr.ScriptAddress()) + return err == nil +} + +func (w *LitecoinWallet) Balance() (confirmed, unconfirmed int64) { + utxos, _ := w.db.Utxos().GetAll() + txns, _ := w.db.Txns().GetAll(false) + return util.CalcBalance(utxos, txns) +} + +func (w *LitecoinWallet) 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 *LitecoinWallet) 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 *LitecoinWallet) ChainTip() (uint32, chainhash.Hash) { + return w.ws.ChainTip() +} + +func (w *LitecoinWallet) GetFeePerByte(feeLevel wi.FeeLevel) uint64 { + return w.fp.GetFeePerByte(feeLevel) +} + +func (w *LitecoinWallet) 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 *LitecoinWallet) BumpFee(txid chainhash.Hash) (*chainhash.Hash, error) { + return w.bumpFee(txid) +} + +func (w *LitecoinWallet) 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 *LitecoinWallet) EstimateSpendFee(amount int64, feeLevel wi.FeeLevel) (uint64, error) { + return w.estimateSpendFee(amount, feeLevel) +} + +func (w *LitecoinWallet) 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 *LitecoinWallet) 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 *LitecoinWallet) 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 *LitecoinWallet) 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 *LitecoinWallet) 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 *LitecoinWallet) 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 *LitecoinWallet) AddTransactionListener(callback func(wi.TransactionCallback)) { + w.ws.AddTransactionListener(callback) +} + +func (w *LitecoinWallet) ReSyncBlockchain(fromTime time.Time) { + go w.ws.UpdateState() +} + +func (w *LitecoinWallet) 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 *LitecoinWallet) Close() { + w.ws.Stop() + w.client.Close() +} + +func (w *LitecoinWallet) ExchangeRates() wi.ExchangeRates { + return w.exchangeRates +} + +func (w *LitecoinWallet) 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 *LitecoinWallet) 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.Litecoin), + } + 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 *LitecoinWallet) AssociateTransactionWithOrder(cb wi.TransactionCallback) { + w.ws.InvokeTransactionListeners(cb) +} diff --git a/litecoin/wallet_test.go b/litecoin/wallet_test.go new file mode 100644 index 0000000..429d00f --- /dev/null +++ b/litecoin/wallet_test.go @@ -0,0 +1,71 @@ +package litecoin + +import ( + "crypto/rand" + "github.com/OpenBazaar/multiwallet/datastore" + "github.com/OpenBazaar/multiwallet/keys" + "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil/hdkeychain" + "strings" + "testing" +) + +func TestLitecoinWallet_CurrentAddress(t *testing.T) { + w, seed, err := createWalletAndSeed() + if err != nil { + t.Fatal(err) + } + + for i := 0; i < 10; i++ { + addr := w.CurrentAddress(wallet.EXTERNAL) + if strings.HasPrefix(strings.ToLower(addr.String()), "ltc1") { + t.Errorf("Address %s hash ltc1 prefix: seed %x", addr, seed) + } + if err := w.db.Keys().MarkKeyAsUsed(addr.ScriptAddress()); err != nil { + t.Fatal(err) + } + } +} + +func TestLitecoinWallet_NewAddress(t *testing.T) { + w, seed, err := createWalletAndSeed() + if err != nil { + t.Fatal(err) + } + + for i := 0; i < 10; i++ { + addr := w.NewAddress(wallet.EXTERNAL) + if strings.HasPrefix(strings.ToLower(addr.String()), "ltc1") { + t.Errorf("Address %s hash ltc1 prefix: %x", addr, seed) + } + } +} + +func createWalletAndSeed() (*LitecoinWallet, []byte, error) { + ds := datastore.NewMockMultiwalletDatastore() + db, err := ds.GetDatastoreForWallet(wallet.Litecoin) + if err != nil { + return nil, nil, err + } + + seed := make([]byte, 32) + if _, err := rand.Read(seed); err != nil { + return nil, nil, err + } + + masterPrivKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) + if err != nil { + return nil, nil, err + } + km, err := keys.NewKeyManager(db.Keys(), &chaincfg.MainNetParams, masterPrivKey, wallet.Litecoin, litecoinAddress) + if err != nil { + return nil, nil, err + } + + return &LitecoinWallet{ + db: db, + km: km, + params: &chaincfg.MainNetParams, + }, seed, nil +} diff --git a/makefile b/makefile new file mode 100644 index 0000000..fe026e1 --- /dev/null +++ b/makefile @@ -0,0 +1,5 @@ +install: + cd cmd/multiwallet && go install + +protos: + cd api/pb && protoc --go_out=plugins=grpc:. api.proto \ No newline at end of file diff --git a/model/helper.go b/model/helper.go new file mode 100644 index 0000000..1d1780f --- /dev/null +++ b/model/helper.go @@ -0,0 +1,46 @@ +package model + +import ( + "errors" + "fmt" + "net/url" + "strconv" +) + +// ToFloat ensures that any value returned by an insight or blockbook response is case to a float64 or errors +func ToFloat(i interface{}) (float64, error) { + _, fok := i.(float64) + _, sok := i.(string) + if fok { + return i.(float64), nil + } else if sok { + s := i.(string) + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, fmt.Errorf("error parsing value float: %s", err) + } + return f, nil + } else { + return 0, errors.New("Unknown value type in response") + } +} + +// DefaultPort returns the default port for the given connection scheme unless +// otherwise indicated in the url.URL provided +func DefaultPort(u *url.URL) int { + var port int + if parsedPort, err := strconv.ParseInt(u.Port(), 10, 32); err == nil { + port = int(parsedPort) + } + if port == 0 { + if HasImpliedURLSecurity(u) { + port = 443 + } else { + port = 80 + } + } + return port +} + +// HasImpliedURLSecurity returns true if the scheme is https +func HasImpliedURLSecurity(u *url.URL) bool { return u.Scheme == "https" } diff --git a/model/helper_test.go b/model/helper_test.go new file mode 100644 index 0000000..007f693 --- /dev/null +++ b/model/helper_test.go @@ -0,0 +1,24 @@ +package model_test + +import ( + "testing" + + "github.com/OpenBazaar/multiwallet/model" +) + +func Test_toFloat64(t *testing.T) { + f, err := model.ToFloat(12.345) + if err != nil { + t.Error(err) + } + if f != 12.345 { + t.Error("Returned incorrect float") + } + f, err = model.ToFloat("456.789") + if err != nil { + t.Error(err) + } + if f != 456.789 { + t.Error("Returned incorrect float") + } +} diff --git a/model/interfaces.go b/model/interfaces.go new file mode 100644 index 0000000..0ec9ec1 --- /dev/null +++ b/model/interfaces.go @@ -0,0 +1,58 @@ +package model + +import "github.com/btcsuite/btcutil" + +type APIClient interface { + + // Start up the API service + Start() error + + // Get info about the server + GetInfo() (*Info, error) + + // For a given txid get back the transaction metadata + GetTransaction(txid string) (*Transaction, error) + + // For a given txid get back the full transaction bytes + GetRawTransaction(txid string) ([]byte, error) + + // Get back all the transactions for the given list of addresses + GetTransactions(addrs []btcutil.Address) ([]Transaction, error) + + // Get back all spendable UTXOs for the given list of addresses + GetUtxos(addrs []btcutil.Address) ([]Utxo, error) + + // Returns a chan which fires on each new block + BlockNotify() <-chan Block + + // Returns a chan which fires whenever a new transaction is received or + // when an existing transaction confirms for all addresses the API is listening on. + TransactionNotify() <-chan Transaction + + // Listen for events on these addresses. Results are returned to TransactionNotify() + ListenAddresses(addrs ...btcutil.Address) + + // Broadcast a transaction to the network + Broadcast(tx []byte) (string, error) + + // Get info on the current chain tip + GetBestBlock() (*Block, error) + + // Estimate the fee required for a transaction + EstimateFee(nBlocks int) (int, error) + + // Close all connections and shutdown + Close() +} + +type SocketClient interface { + + // Set callback for method + On(method string, callback interface{}) error + + // Listen on method + Emit(method string, args []interface{}) error + + // Close the socket connection + Close() +} diff --git a/model/mock/interfaces.go b/model/mock/interfaces.go new file mode 100644 index 0000000..8872a5b --- /dev/null +++ b/model/mock/interfaces.go @@ -0,0 +1,188 @@ +package mock + +import ( + "encoding/hex" + "errors" + "fmt" + "sync" + + gosocketio "github.com/OpenBazaar/golang-socketio" + "github.com/OpenBazaar/multiwallet/client" + "github.com/OpenBazaar/multiwallet/model" + "github.com/btcsuite/btcutil" +) + +type MockAPIClient struct { + blockChan chan model.Block + txChan chan model.Transaction + + listeningAddrs []btcutil.Address + chainTip int + feePerBlock int + info *model.Info + addrToScript func(btcutil.Address) ([]byte, error) +} + +func NewMockApiClient(addrToScript func(btcutil.Address) ([]byte, error)) model.APIClient { + return &MockAPIClient{ + blockChan: make(chan model.Block), + txChan: make(chan model.Transaction), + chainTip: 0, + addrToScript: addrToScript, + feePerBlock: 1, + info: &MockInfo, + } +} + +func (m *MockAPIClient) Start() error { + return nil +} + +func (m *MockAPIClient) GetInfo() (*model.Info, error) { + return m.info, nil +} + +func (m *MockAPIClient) GetTransaction(txid string) (*model.Transaction, error) { + for _, tx := range MockTransactions { + if tx.Txid == txid { + return &tx, nil + } + } + return nil, errors.New("Not found") +} + +func (m *MockAPIClient) GetRawTransaction(txid string) ([]byte, error) { + if raw, ok := MockRawTransactions[txid]; ok { + return raw, nil + } + return nil, errors.New("Not found") +} + +func (m *MockAPIClient) GetTransactions(addrs []btcutil.Address) ([]model.Transaction, error) { + txs := make([]model.Transaction, len(MockTransactions)) + copy(txs, MockTransactions) + txs[0].Outputs[1].ScriptPubKey.Addresses = []string{addrs[0].String()} + txs[1].Inputs[0].Addr = addrs[0].String() + txs[1].Outputs[1].ScriptPubKey.Addresses = []string{addrs[1].String()} + txs[2].Outputs[1].ScriptPubKey.Addresses = []string{addrs[2].String()} + return txs, nil +} + +func (m *MockAPIClient) GetUtxos(addrs []btcutil.Address) ([]model.Utxo, error) { + utxos := make([]model.Utxo, len(MockUtxos)) + copy(utxos, MockUtxos) + utxos[0].Address = addrs[1].String() + script, _ := m.addrToScript(addrs[1]) + utxos[0].ScriptPubKey = hex.EncodeToString(script) + utxos[1].Address = addrs[2].String() + script, _ = m.addrToScript(addrs[2]) + utxos[1].ScriptPubKey = hex.EncodeToString(script) + return utxos, nil +} + +func (m *MockAPIClient) BlockNotify() <-chan model.Block { + return m.blockChan +} + +func (m *MockAPIClient) TransactionNotify() <-chan model.Transaction { + return m.txChan +} + +func (m *MockAPIClient) ListenAddresses(addrs ...btcutil.Address) { + m.listeningAddrs = append(m.listeningAddrs, addrs...) +} + +func (m *MockAPIClient) Broadcast(tx []byte) (string, error) { + return "a8c685478265f4c14dada651969c45a65e1aeb8cd6791f2f5bb6a1d9952104d9", nil +} + +func (m *MockAPIClient) GetBestBlock() (*model.Block, error) { + return &MockBlocks[m.chainTip], nil +} + +func (m *MockAPIClient) EstimateFee(nBlocks int) (int, error) { + return m.feePerBlock * nBlocks, nil +} + +func (m *MockAPIClient) Close() {} + +func MockWebsocketClientOnClientPool(p *client.ClientPool) *MockSocketClient { + var ( + callbacksMap = make(map[string]func(*gosocketio.Channel, interface{})) + mockSocketClient = &MockSocketClient{ + callbacks: callbacksMap, + listeningAddresses: []string{}, + } + ) + for _, c := range p.Clients() { + c.SocketClient = mockSocketClient + } + return mockSocketClient +} + +func NewMockWebsocketClient() *MockSocketClient { + var ( + callbacksMap = make(map[string]func(*gosocketio.Channel, interface{})) + mockSocketClient = &MockSocketClient{ + callbacks: callbacksMap, + listeningAddresses: []string{}, + } + ) + return mockSocketClient +} + +type MockSocketClient struct { + callbackMutex sync.Mutex + callbacks map[string]func(*gosocketio.Channel, interface{}) + listeningAddresses []string +} + +func (m *MockSocketClient) SendCallback(method string, args ...interface{}) { + if gosocketChan, ok := args[0].(*gosocketio.Channel); ok { + m.callbacks[method](gosocketChan, args[1]) + } else { + m.callbacks[method](nil, args[1]) + } +} + +func (m *MockSocketClient) IsListeningForAddress(addr string) bool { + for _, a := range m.listeningAddresses { + if a == addr { + return true + } + } + return false +} + +func (m *MockSocketClient) On(method string, callback interface{}) error { + c, ok := callback.(func(h *gosocketio.Channel, args interface{})) + if !ok { + return fmt.Errorf("failed casting mock callback: %+v", callback) + } + + m.callbackMutex.Lock() + defer m.callbackMutex.Unlock() + if method == "bitcoind/addresstxid" { + m.callbacks[method] = c + } else if method == "bitcoind/hashblock" { + m.callbacks[method] = c + } + return nil +} + +func (m *MockSocketClient) Emit(method string, args []interface{}) error { + if method == "subscribe" { + subscribeTo, ok := args[0].(string) + if !ok || subscribeTo != "bitcoind/addresstxid" { + return fmt.Errorf("first emit arg is not bitcoind/addresstxid, was: %+v", args[0]) + } + addrs, ok := args[1].([]string) + if !ok { + return fmt.Errorf("second emit arg is not address value, was %+v", args[1]) + } + m.listeningAddresses = append(m.listeningAddresses, addrs...) + } + return nil +} + +func (m *MockSocketClient) Close() {} diff --git a/model/mock/models.go b/model/mock/models.go new file mode 100644 index 0000000..101d7e7 --- /dev/null +++ b/model/mock/models.go @@ -0,0 +1,283 @@ +package mock + +import "github.com/OpenBazaar/multiwallet/model" + +var MockInfo = model.Info{ + Version: 1, + ProtocolVersion: 9005, + Blocks: 1289596, + TimeOffset: 0, + Connections: 1024, + DifficultyIface: "1.23", + Difficulty: 1.23, + Testnet: true, + RelayFeeIface: "1.00", + RelayFee: 1.00, + Errors: "", + Network: "testnet", +} + +var MockBlocks = []model.Block{ + { + Hash: "000000000000004c68a477283a8db18c1d1c2155b03d9bc23d587ac5e1c4d1af", + Height: 1289594, + PreviousBlockhash: "00000000000003df72ec254d787b216ae913cb82c6ab601c4b3f19fd5d1cf9aa", + Tx: make([]string, 21), + Size: 4705, + Time: 1522349145, + }, + { + Hash: "0000000000000142ffae87224cb67206e93bf934f9fdeba75d02a7050acc6136", + Height: 1289595, + PreviousBlockhash: "000000000000004c68a477283a8db18c1d1c2155b03d9bc23d587ac5e1c4d1af", + Tx: make([]string, 30), + Size: 6623, + Time: 1522349136, + }, + { + Hash: "000000000000033ef24180d5d282d0e6d03b1185e29421fda97e1ba0ffd7c918", + Height: 1289596, + PreviousBlockhash: "0000000000000142ffae87224cb67206e93bf934f9fdeba75d02a7050acc6136", + Tx: make([]string, 5), + Size: 1186, + Time: 1522349156, + }, +} + +var MockTransactions = []model.Transaction{ + { + Txid: "54ebaa07c42216393b9d5816e40dd608593b92c42e2d6525f45bdd36bce8fe4d", + Version: 2, + Locktime: 512378, + Inputs: []model.Input{ + { + Txid: "6d892f04fc097f430d58ab06229c9b6344a130fc1842da5b990e857daed42194", + Vout: 1, + Sequence: 1, + ValueIface: "0.04294455", + Value: 0.04294455, + N: 0, + ScriptSig: model.Script{ + Hex: "4830450221008665481674067564ef562cfd8d1ca8f1506133fb26a2319e4b8dfba3cedfd5de022038f27121c44e6c64b93b94d72620e11b9de35fd864730175db9176ca98f1ec610121022023e49335a0dddb864ff673468a6cc04e282571b1227933fcf3ff9babbcc662", + }, + Addr: "1C74Gbij8Q5h61W58aSKGvXK4rk82T2A3y", + Satoshis: 4294455, + }, + }, + Outputs: []model.Output{ + { + ScriptPubKey: model.OutScript{ + Script: model.Script{ + Hex: "76a914ff3f7d402fbd6d116ba4a02af9784f3ae9b7108a88ac", + }, + Type: "pay-to-pubkey-hash", + Addresses: []string{"1QGdNEDjWnghrjfTBCTDAPZZ3ffoKvGc9B"}, + }, + ValueIface: "0.01398175", + Value: 0.01398175, + N: 0, + }, + { + ScriptPubKey: model.OutScript{ + Script: model.Script{ + Hex: "76a914f99b84270843bdab59a71ce9af15b89bef5087a388ac", + }, + Type: "pay-to-pubkey-hash", + Addresses: []string{"1PkoZDtXT63BnYGd429Vy4DoyGhdDcjQiN"}, // var + }, + ValueIface: "0.02717080", + Value: 0.02717080, + N: 1, + }, + }, + Time: 1520449061, + BlockHash: "0000000000000000003f1fb88ac3dab0e607e87def0e9031f7bea02cb464a04f", + BlockHeight: 1289475, + Confirmations: 15, + }, + { + Txid: "ff2b865c3b73439912eebf4cce9a15b12c7d7bcdd14ae1110a90541426c4e7c5", + Version: 2, + Locktime: 0, + Inputs: []model.Input{ + { + Txid: "54ebaa07c42216393b9d5816e40dd608593b92c42e2d6525f45bdd36bce8fe4d", + Vout: 1, + Sequence: 1, + ValueIface: "0.02717080", + Value: 0.02717080, + N: 0, + ScriptSig: model.Script{ + Hex: "4830450221008665481674067564ef562cfd8d1ca8f1506133fb26a2319e4b8dfba3cedfd5de022038f27121c44e6c64b93b94d72620e11b9de35fd864730175db9176ca98f1ec610121022023e49335a0dddb864ff673468a6cc04e282571b1227933fcf3ff9babbcc662", + }, + Addr: "1PkoZDtXT63BnYGd429Vy4DoyGhdDcjQiN", // var tx0:1 + Satoshis: 2717080, + }, + }, + Outputs: []model.Output{ + { + ScriptPubKey: model.OutScript{ + Script: model.Script{ + Hex: "a9144b18dadba74ad5ef4dbbfea47f9d5aaefe766c6387", + }, + Type: "pay-to-script-hash", + Addresses: []string{"38Y6Nt35hQcEDxyCfCEi62QLGPnr4mhANc"}, + }, + ValueIface: "0.01398175", + Value: 0.01617080, + N: 0, + }, + { + ScriptPubKey: model.OutScript{ + Script: model.Script{ + Hex: "76a914f821d6db9376dc60124de46a8683110877e1f13188ac", + }, + Type: "pay-to-pubkey-hash", + Addresses: []string{"1Pd17mbYsVPcCKLtNdPkngtizTj7zjzqeK"}, // var change + }, + ValueIface: "0.01", + Value: 0.01, + N: 1, + }, + }, + Time: 1520449061, + BlockHash: "0000000000000000003f1fb88ac3dab0e607e87def0e9031f7bea02cb464a04f", + BlockHeight: 1289475, + Confirmations: 10, + }, + { + Txid: "1d4288fa682fa376fbae73dbd74ea04b9ea33011d63315ca9d2d50d081e671d5", + Version: 2, + Locktime: 0, + Inputs: []model.Input{ + { + Txid: "bffb894c27dac82525c1f00a085150be94c70834e8d05ea5e7bb3bd1278d3138", + Vout: 1, + Sequence: 1, + ValueIface: "0.3", + Value: 0.3, + N: 0, + ScriptSig: model.Script{ + Hex: "4830450221008665481674067564ef562cfd8d1ca8f1506133fb26a2319e4b8dfba3cedfd5de022038f27121c44e6c64b93b94d72620e11b9de35fd864730175db9176ca98f1ec610121022023e49335a0dddb864ff673468a6cc04e282571b1227933fcf3ff9babbcc662", + }, + Addr: "1H2ZS69jUZz6CuCtiRCTWXr4AhAWfXc4YT", + Satoshis: 2717080, + }, + }, + Outputs: []model.Output{ + { + ScriptPubKey: model.OutScript{ + Script: model.Script{ + Hex: "76a914e20c0ca5875b1fb0d057e23d032ba88b9dda6f3888ac", + }, + Type: "pay-to-pubkey-hash", + Addresses: []string{"1McE9ZXFhWkFeAqR1hyAm1XaDK8zvyrFPr"}, + }, + ValueIface: "0.2", + Value: 0.2, + N: 0, + }, + { + ScriptPubKey: model.OutScript{ + Script: model.Script{ + Hex: "76a914594963287fe6684872340e9078a78d0accbec26288ac", + }, + Type: "pay-to-pubkey-hash", + Addresses: []string{"199747e2arXMBPiWfTqpBTXz3eFbeJPMqS"}, // var + }, + ValueIface: "0.1", + Value: 0.1, + N: 1, + }, + }, + Time: 1520449061, + BlockHash: "0000000000000000003f1fb88ac3dab0e607e87def0e9031f7bea02cb464a04f", + BlockHeight: 1289475, + Confirmations: 2, + }, + { + Txid: "830bf683ab8eec1a75d891689e2989f846508bc7d500cb026ef671c2d1dce20c", + Version: 2, + Locktime: 516299, + Inputs: []model.Input{ + { + Txid: "b466d034076ab53f4b019d573b6c68cf68c5b9a8cfbf07c8d46208d0fcf37762", + Vout: 0, + Sequence: 4294967294, + ValueIface: "0.01983741", + Value: 0.01983741, + N: 0, + ScriptSig: model.Script{ + Hex: "483045022100baa2b3653d48ccf2838caa549d96a40540c838c4f4a8e7048dbe158ec180b3f602206f1bb8c6d055103ce635db562c31ebd8c30565c5d415458affb9f99407ec06d10121039fea462cb64296e01384cffc16af4b86ab14b6027094399bf5a4b52e5c9ffef3", + }, + Addr: "1LUv9VNMZQR4VknWj1TBa1oDgPq53wP7BK", + Satoshis: 1983741, + }, + }, + Outputs: []model.Output{ + { + ScriptPubKey: model.OutScript{ + Script: model.Script{ + Hex: "76a91491a8a9e0375f10b721743782162a0b4f9fae69a888ac", + }, + Type: "pay-to-pubkey-hash", + Addresses: []string{"1EHB2mSaUXzkM6r6XgVHcutFDZoB9e2mZH"}, + }, + ValueIface: "0.01181823", + Value: 0.01181823, + N: 0, + }, + { + ScriptPubKey: model.OutScript{ + Script: model.Script{ + Hex: "a91457fc729da2a83dc8cd3c1835351c4a813c2ae8ba87", + }, + Type: "pay-to-script-hash", + Addresses: []string{"39iF8cDMhctrPVoPbi2Vb1NnErg6CEB7BZ"}, + }, + ValueIface: "0.00751918", + Value: 0.00751918, + N: 1, + }, + }, + Time: 1520449061, + BlockHash: "0000000000000000003f1fb88ac3dab0e607e87def0e9031f7bea02cb464a04f", + BlockHeight: 1289475, + Confirmations: 2, + }, +} + +var MockRawTransactions = map[string][]byte{} + +var MockUtxos = []model.Utxo{ + { + Address: "1Pd17mbYsVPcCKLtNdPkngtizTj7zjzqeK", // tx1:1 + ScriptPubKey: "76a914f821d6db9376dc60124de46a8683110877e1f13188ac", + Vout: 1, + Satoshis: 1000000, + Confirmations: 10, + Txid: "ff2b865c3b73439912eebf4cce9a15b12c7d7bcdd14ae1110a90541426c4e7c5", + AmountIface: "0.01", + Amount: 0.01, + }, + { + Address: "199747e2arXMBPiWfTqpBTXz3eFbeJPMqS", //tx2:1 + ScriptPubKey: "76a914594963287fe6684872340e9078a78d0accbec26288ac", + Vout: 1, + Satoshis: 10000000, + Confirmations: 2, + Txid: "1d4288fa682fa376fbae73dbd74ea04b9ea33011d63315ca9d2d50d081e671d5", + AmountIface: "0.1", + Amount: 0.1, + }, + { + Address: "39iF8cDMhctrPVoPbi2Vb1NnErg6CEB7BZ", + ScriptPubKey: "a91457fc729da2a83dc8cd3c1835351c4a813c2ae8ba87", + Vout: 1, + Satoshis: 751918, + Confirmations: 2, + Txid: "830bf683ab8eec1a75d891689e2989f846508bc7d500cb026ef671c2d1dce20c", + AmountIface: "0.00751918", + Amount: 0.00751918, + }, +} diff --git a/model/models.go b/model/models.go new file mode 100644 index 0000000..6a86c93 --- /dev/null +++ b/model/models.go @@ -0,0 +1,171 @@ +package model + +type Status struct { + Info Info `json:"info"` +} + +type Info struct { + Version int `json:"version"` + ProtocolVersion int `json:"protocolversion"` + Blocks int `json:"blocks"` + TimeOffset int `json:"timeoffset"` + Connections int `json:"connections"` + DifficultyIface interface{} `json:"difficulty"` + Difficulty float64 `json:"-"` + Testnet bool `json:"testnet"` + RelayFeeIface interface{} `json:"relayfee"` + RelayFee float64 `json:"-"` + Errors string `json:"errors"` + Network string `json:"network"` +} + +func (i Info) IsEqual(other Info) bool { + if i.Version != other.Version { + return false + } + if i.ProtocolVersion != other.ProtocolVersion { + return false + } + if i.Blocks != other.Blocks { + return false + } + if i.TimeOffset != other.TimeOffset { + return false + } + if i.Connections != other.Connections { + return false + } + if i.Difficulty != other.Difficulty { + return false + } + if i.Testnet != other.Testnet { + return false + } + if i.RelayFee != other.RelayFee { + return false + } + if i.Errors != other.Errors { + return false + } + if i.Network != other.Network { + return false + } + return true +} + +type BlockList struct { + Blocks []Block `json:"blocks"` + Length int `json:"length"` + Pagination Pagination `json:"pagination"` +} + +type Pagination struct { + Next string `json:"next"` + Prev string `json:"prev"` + CurrentTs int `json:"currentTs"` + Current string `json:"current"` + IsToday bool `json:"isToday"` + More bool `json:"more"` + MoreTs int `json:"moreTs"` +} + +type Block struct { + Hash string `json:"hash"` + Size int `json:"size"` + Height int `json:"height"` + Version int `json:"version"` + MerkleRoot string `json:"merkleroot"` + Tx []string `json:"tx"` + Time int64 `json:"time"` + Nonce string `json:"nonce"` + Solution string `json:"solution"` + Bits string `json:"bits"` + Difficulty float64 `json:"difficulty"` + Chainwork string `json:"chainwork"` + Confirmations int `json:"confirmations"` + PreviousBlockhash string `json:"previousblockhash"` + NextBlockhash string `json:"nextblockhash"` + Reward float64 `json:"reward"` + IsMainChain bool `json:"isMainChain"` + PoolInfo *PoolInfo `json:"poolinfo"` +} + +type PoolInfo struct { + PoolName string `json:"poolName"` + URL string `json:"url"` +} + +type Utxo struct { + Address string `json:"address"` + Txid string `json:"txid"` + Vout int `json:"vout"` + ScriptPubKey string `json:"scriptPubKey"` + AmountIface interface{} `json:"amount"` + Amount float64 + Satoshis int64 `json:"satoshis"` + Confirmations int `json:"confirmations"` +} + +type TransactionList struct { + TotalItems int `json:"totalItems"` + From int `json:"from"` + To int `json:"to"` + Items []Transaction `json:"items"` +} + +type Transaction struct { + Txid string `json:"txid"` + Version int `json:"version"` + Locktime int `json:"locktime"` + Inputs []Input `json:"vin"` + Outputs []Output `json:"vout"` + BlockHash string `json:"blockhash"` + BlockHeight int `json:"blockheight"` + Confirmations int `json:"confirmations"` + Time int64 `json:"time"` + BlockTime int64 `json:"blocktime"` + RawBytes []byte `json:"rawbytes"` +} + +type RawTxResponse struct { + RawTx string `json:"rawtx"` +} + +type Input struct { + Txid string `json:"txid"` + Vout int `json:"vout"` + Sequence uint32 `json:"sequence"` + N int `json:"n"` + ScriptSig Script `json:"scriptSig"` + Addr string `json:"addr"` + Satoshis int64 `json:"valueSat"` + ValueIface interface{} `json:"value"` + Value float64 + DoubleSpentTxid string `json:"doubleSpentTxID"` +} + +type Output struct { + ValueIface interface{} `json:"value"` + Value float64 + N int `json:"n"` + ScriptPubKey OutScript `json:"scriptPubKey"` + SpentTxid string `json:"spentTxId"` + SpentIndex int `json:"spentIndex"` + SpentHeight int `json:"spentHeight"` +} + +type Script struct { + Hex string `json:"hex"` + Asm string `json:"asm"` +} + +type OutScript struct { + Script + Addresses []string `json:"addresses"` + Type string `json:"type"` +} + +type AddressTxid struct { + Address string `json:"address"` + Txid string `json:"txid"` +} diff --git a/multiwallet.go b/multiwallet.go new file mode 100644 index 0000000..ea7b886 --- /dev/null +++ b/multiwallet.go @@ -0,0 +1,138 @@ +package multiwallet + +import ( + "errors" + "strings" + "time" + + "github.com/OpenBazaar/multiwallet/bitcoin" + "github.com/OpenBazaar/multiwallet/bitcoincash" + "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" + "github.com/btcsuite/btcd/chaincfg" + "github.com/op/go-logging" + "github.com/tyler-smith/go-bip39" +) + +var log = logging.MustGetLogger("multiwallet") + +var UnsuppertedCoinError = errors.New("multiwallet does not contain an implementation for the given coin") + +type MultiWallet map[wallet.CoinType]wallet.Wallet + +func NewMultiWallet(cfg *config.Config) (MultiWallet, error) { + log.SetBackend(logging.AddModuleLevel(cfg.Logger)) + service.Log = log + blockbook.Log = log + + if cfg.Mnemonic == "" { + ent, err := bip39.NewEntropy(128) + if err != nil { + return nil, err + } + mnemonic, err := bip39.NewMnemonic(ent) + if err != nil { + return nil, err + } + cfg.Mnemonic = mnemonic + cfg.CreationDate = time.Now() + } + + multiwallet := make(MultiWallet) + var err error + for _, coin := range cfg.Coins { + var w wallet.Wallet + switch coin.CoinType { + case wallet.Bitcoin: + w, err = bitcoin.NewBitcoinWallet(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.Bitcoin] = w + } else { + multiwallet[wallet.TestnetBitcoin] = w + } + case wallet.BitcoinCash: + w, err = bitcoincash.NewBitcoinCashWallet(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.BitcoinCash] = w + } else { + multiwallet[wallet.TestnetBitcoinCash] = w + } + case wallet.Zcash: + w, err = zcash.NewZCashWallet(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.Zcash] = w + } else { + multiwallet[wallet.TestnetZcash] = w + } + case wallet.Litecoin: + w, err = litecoin.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.Litecoin] = 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 + } + 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 +} + +func (w *MultiWallet) Start() { + for _, wallet := range *w { + wallet.Start() + } +} + +func (w *MultiWallet) Close() { + for _, wallet := range *w { + wallet.Close() + } +} + +func (w *MultiWallet) WalletForCurrencyCode(currencyCode string) (wallet.Wallet, error) { + for _, wl := range *w { + if strings.EqualFold(wl.CurrencyCode(), currencyCode) || strings.EqualFold(wl.CurrencyCode(), "T"+currencyCode) { + return wl, nil + } + } + return nil, UnsuppertedCoinError +} diff --git a/service/wallet_service.go b/service/wallet_service.go new file mode 100644 index 0000000..a1e586a --- /dev/null +++ b/service/wallet_service.go @@ -0,0 +1,650 @@ +package service + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "math" + "strconv" + "sync" + "time" + + "github.com/OpenBazaar/multiwallet/cache" + "github.com/OpenBazaar/multiwallet/keys" + laddr "github.com/OpenBazaar/multiwallet/litecoin/address" + "github.com/OpenBazaar/multiwallet/model" + "github.com/OpenBazaar/multiwallet/util" + zaddr "github.com/OpenBazaar/multiwallet/zcash/address" + "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/cpacia/bchutil" + "github.com/op/go-logging" +) + +var Log = logging.MustGetLogger("WalletService") + +type WalletService struct { + db wallet.Datastore + km *keys.KeyManager + client model.APIClient + params *chaincfg.Params + coinType wallet.CoinType + + chainHeight uint32 + bestBlock string + cache cache.Cacher + + listeners []func(wallet.TransactionCallback) + + lock sync.RWMutex + + doneChan chan struct{} +} + +type HashAndHeight struct { + Height uint32 `json:"height"` + Hash string `json:"string"` + Timestamp time.Time `json:"timestamp"` +} + +const nullHash = "0000000000000000000000000000000000000000000000000000000000000000" + +func NewWalletService(db wallet.Datastore, km *keys.KeyManager, client model.APIClient, params *chaincfg.Params, coinType wallet.CoinType, cache cache.Cacher) (*WalletService, error) { + var ( + ws = &WalletService{ + db: db, + km: km, + client: client, + params: params, + coinType: coinType, + chainHeight: 0, + bestBlock: nullHash, + + cache: cache, + listeners: []func(wallet.TransactionCallback){}, + lock: sync.RWMutex{}, + doneChan: make(chan struct{}), + } + marshaledHeight, err = cache.Get(ws.bestHeightKey()) + ) + + if err != nil { + Log.Info("cached block height missing: using default") + } else { + var hh HashAndHeight + if err := json.Unmarshal(marshaledHeight, &hh); err != nil { + Log.Error("failed unmarshaling cached block height") + return ws, nil + } + ws.bestBlock = hh.Hash + ws.chainHeight = hh.Height + } + return ws, nil +} + +func (ws *WalletService) Start() { + Log.Noticef("starting %s WalletService", ws.coinType.String()) + go ws.UpdateState() + go ws.listen() +} + +func (ws *WalletService) Stop() { + ws.doneChan <- struct{}{} +} + +func (ws *WalletService) ChainTip() (uint32, chainhash.Hash) { + ws.lock.RLock() + defer ws.lock.RUnlock() + ch, err := chainhash.NewHashFromStr(ws.bestBlock) + if err != nil { + Log.Errorf("producing BestBlock hash: %s", err.Error()) + } + return ws.chainHeight, *ch +} + +func (ws *WalletService) AddTransactionListener(callback func(callback wallet.TransactionCallback)) { + ws.listeners = append(ws.listeners, callback) +} + +// InvokeTransactionListeners will invoke the transaction listeners for the updation of order state +func (ws *WalletService) InvokeTransactionListeners(callback wallet.TransactionCallback) { + for _, l := range ws.listeners { + go l(callback) + } +} + +func (ws *WalletService) listen() { + var ( + addrs = ws.getStoredAddresses() + txChan = ws.client.TransactionNotify() + blockChan = ws.client.BlockNotify() + ) + + var listenAddrs []btcutil.Address + for _, sa := range addrs { + listenAddrs = append(listenAddrs, sa.Addr) + } + ws.client.ListenAddresses(listenAddrs...) + + for { + select { + case <-ws.doneChan: + return + case tx := <-txChan: + go ws.ProcessIncomingTransaction(tx) + case block := <-blockChan: + go ws.processIncomingBlock(block) + } + } +} + +// This is a transaction fresh off the wire. Let's save it to the db. +func (ws *WalletService) ProcessIncomingTransaction(tx model.Transaction) { + Log.Debugf("new incoming %s transaction: %s", ws.coinType.String(), tx.Txid) + addrs := ws.getStoredAddresses() + ws.lock.RLock() + chainHeight := int32(ws.chainHeight) + ws.lock.RUnlock() + ws.saveSingleTxToDB(tx, chainHeight, addrs) + utxos, err := ws.db.Utxos().GetAll() + if err != nil { + Log.Errorf("error loading %s utxos: %s", ws.coinType.String(), err.Error()) + } + + for _, sa := range addrs { + for _, out := range tx.Outputs { + for _, addr := range out.ScriptPubKey.Addresses { + if addr == sa.Addr.String() { + utxo := model.Utxo{ + Txid: tx.Txid, + ScriptPubKey: out.ScriptPubKey.Hex, + Satoshis: int64(math.Round(out.Value * util.SatoshisPerCoin(ws.coinType))), + Vout: out.N, + Address: addr, + Confirmations: 0, + Amount: out.Value, + } + ws.saveSingleUtxoToDB(utxo, addrs, chainHeight) + break + } + } + } + // If spending a utxo, delete it + for _, in := range tx.Inputs { + for _, u := range utxos { + if in.Txid == u.Op.Hash.String() && in.Vout == int(u.Op.Index) { + err := ws.db.Utxos().Delete(u) + if err != nil { + Log.Errorf("deleting spent utxo: %s", err.Error()) + } + break + } + } + } + } +} + +// A new block was found let's update our chain height and best hash and check for a reorg +func (ws *WalletService) processIncomingBlock(block model.Block) { + Log.Infof("received new %s block at height %d: %s", ws.coinType.String(), block.Height, block.Hash) + ws.lock.RLock() + currentBest := ws.bestBlock + ws.lock.RUnlock() + + ws.lock.Lock() + err := ws.saveHashAndHeight(block.Hash, uint32(block.Height)) + if err != nil { + Log.Errorf("update %s blockchain height: %s", ws.coinType.String(), err.Error()) + } + ws.lock.Unlock() + + // REORG! Rescan all transactions and utxos to see if anything changed + if currentBest != block.PreviousBlockhash && currentBest != block.Hash { + Log.Warningf("%s chain reorg detected: rescanning wallet", ws.coinType.String()) + ws.UpdateState() + return + } + + // Query db for unconfirmed txs and utxos then query API to get current height + txs, err := ws.db.Txns().GetAll(true) + if err != nil { + Log.Errorf("error loading %s txs from db: %s", ws.coinType.String(), err.Error()) + return + } + utxos, err := ws.db.Utxos().GetAll() + if err != nil { + Log.Errorf("error loading %s txs from db: %s", ws.coinType.String(), err.Error()) + return + } + addrs := ws.getStoredAddresses() + for _, tx := range txs { + if tx.Height == 0 { + Log.Debugf("broadcasting unconfirmed txid %s", tx.Txid) + go func(txn wallet.Txn) { + ret, err := ws.client.GetTransaction(txn.Txid) + if err != nil { + Log.Errorf("error fetching unconfirmed %s tx: %s", ws.coinType.String(), err.Error()) + return + } + if ret.Confirmations > 0 { + h := int32(block.Height) - int32(ret.Confirmations-1) + ws.saveSingleTxToDB(*ret, int32(block.Height), addrs) + for _, u := range utxos { + if u.Op.Hash.String() == txn.Txid { + u.AtHeight = h + if err := ws.db.Utxos().Put(u); err != nil { + Log.Errorf("updating utxo confirmation to %d: %s", h, err.Error()) + } + continue + } + } + return + } + // Rebroadcast unconfirmed transactions + _, err = ws.client.Broadcast(tx.Bytes) + if err != nil { + Log.Errorf("broadcasting unconfirmed utxo: %s", err.Error()) + } + }(tx) + } + } +} + +// updateState will query the API for both UTXOs and TXs relevant to our wallet and then update +// the db state to match the API responses. +func (ws *WalletService) UpdateState() { + // Start by fetching the chain height from the API + Log.Debugf("updating %s chain state", ws.coinType.String()) + best, err := ws.client.GetBestBlock() + if err == nil { + Log.Debugf("%s chain height: %d", ws.coinType.String(), best.Height) + ws.lock.Lock() + err = ws.saveHashAndHeight(best.Hash, uint32(best.Height)) + if err != nil { + Log.Errorf("updating %s blockchain height: %s", ws.coinType.String(), err.Error()) + } + ws.lock.Unlock() + } else { + Log.Errorf("error querying API for %s chain height: %s", ws.coinType.String(), err.Error()) + } + + // Load wallet addresses and watch only addresses from the db + addrs := ws.getStoredAddresses() + + go ws.syncUtxos(addrs) + go ws.syncTxs(addrs) + +} + +// Query API for UTXOs and synchronize db state +func (ws *WalletService) syncUtxos(addrs map[string]storedAddress) { + Log.Debugf("querying for %s utxos", ws.coinType.String()) + var query []btcutil.Address + for _, sa := range addrs { + query = append(query, sa.Addr) + } + utxos, err := ws.client.GetUtxos(query) + if err != nil { + Log.Errorf("error downloading utxos for %s: %s", ws.coinType.String(), err.Error()) + } else { + Log.Debugf("downloaded %d %s utxos", len(utxos), ws.coinType.String()) + ws.saveUtxosToDB(utxos, addrs) + } +} + +// For each API response we will have to figure out height at which the UTXO has confirmed (if it has) and +// build a UTXO object suitable for saving to the database. If the database contains any UTXOs not returned +// by the API we will delete them. +func (ws *WalletService) saveUtxosToDB(utxos []model.Utxo, addrs map[string]storedAddress) { + // Get current utxos + currentUtxos, err := ws.db.Utxos().GetAll() + if err != nil { + Log.Error("error loading utxos for %s: %s", ws.coinType.String(), err.Error()) + return + } + + ws.lock.RLock() + chainHeight := int32(ws.chainHeight) + ws.lock.RUnlock() + + newUtxos := make(map[string]wallet.Utxo) + // Iterate over new utxos and put them to the db + for _, u := range utxos { + ch, err := chainhash.NewHashFromStr(u.Txid) + if err != nil { + Log.Error("error converting to chainhash for %s: %s", ws.coinType.String(), err.Error()) + continue + } + newU := wallet.Utxo{ + Op: *wire.NewOutPoint(ch, uint32(u.Vout)), + } + newUtxos[serializeUtxo(newU)] = newU + ws.saveSingleUtxoToDB(u, addrs, chainHeight) + } + // If any old utxos were not returned by the API, delete them. + for _, cur := range currentUtxos { + _, ok := newUtxos[serializeUtxo(cur)] + if !ok { + if err := ws.db.Utxos().Delete(cur); err != nil { + Log.Errorf("deleting utxo (%s): %s", cur.Op.Hash.String(), err.Error()) + } + } + } +} + +func (ws *WalletService) saveSingleUtxoToDB(u model.Utxo, addrs map[string]storedAddress, chainHeight int32) { + ch, err := chainhash.NewHashFromStr(u.Txid) + if err != nil { + Log.Error("error converting to chainhash for %s: %s", ws.coinType.String(), err.Error()) + return + } + scriptBytes, err := hex.DecodeString(u.ScriptPubKey) + if err != nil { + Log.Error("error converting to script bytes for %s: %s", ws.coinType.String(), err.Error()) + return + } + + var watchOnly bool + sa, ok := addrs[u.Address] + if sa.WatchOnly || !ok { + watchOnly = true + } + + height := int32(0) + if u.Confirmations > 0 { + height = chainHeight - (int32(u.Confirmations) - 1) + } + + newU := wallet.Utxo{ + Op: *wire.NewOutPoint(ch, uint32(u.Vout)), + Value: u.Satoshis, + WatchOnly: watchOnly, + ScriptPubkey: scriptBytes, + AtHeight: height, + } + + if err := ws.db.Utxos().Put(newU); err != nil { + Log.Errorf("putting utxo (%s): %s", u.Txid, err.Error()) + return + } +} + +// For use as a map key +func serializeUtxo(u wallet.Utxo) string { + ser := u.Op.Hash.String() + ser += strconv.Itoa(int(u.Op.Index)) + return ser +} + +// Query API for TXs and synchronize db state +func (ws *WalletService) syncTxs(addrs map[string]storedAddress) { + Log.Debugf("querying for %s transactions", ws.coinType.String()) + var query []btcutil.Address + for _, sa := range addrs { + query = append(query, sa.Addr) + } + txs, err := ws.client.GetTransactions(query) + if err != nil { + Log.Errorf("error downloading txs for %s: %s", ws.coinType.String(), err.Error()) + } else { + Log.Debugf("downloaded %d %s transactions", len(txs), ws.coinType.String()) + ws.saveTxsToDB(txs, addrs) + } +} + +// For each API response we will need to determine the net coins leaving/entering the wallet as well as determine +// if the transaction was exclusively for our `watch only` addresses. We will also build a Tx object suitable +// for saving to the db and delete any existing txs not returned by the API. Finally, for any output matching a key +// in our wallet we need to mark that key as used in the db +func (ws *WalletService) saveTxsToDB(txns []model.Transaction, addrs map[string]storedAddress) { + ws.lock.RLock() + chainHeight := int32(ws.chainHeight) + ws.lock.RUnlock() + + // Iterate over new txs and put them to the db + for _, u := range txns { + ws.saveSingleTxToDB(u, chainHeight, addrs) + } +} + +func (ws *WalletService) saveSingleTxToDB(u model.Transaction, chainHeight int32, addrs map[string]storedAddress) { + msgTx := wire.NewMsgTx(int32(u.Version)) + msgTx.LockTime = uint32(u.Locktime) + hits := 0 + value := int64(0) + + height := int32(0) + if u.Confirmations > 0 { + height = chainHeight - (int32(u.Confirmations) - 1) + } + + txHash, err := chainhash.NewHashFromStr(u.Txid) + if err != nil { + Log.Errorf("error converting to txHash for %s: %s", ws.coinType.String(), err.Error()) + return + } + var relevant bool + cb := wallet.TransactionCallback{Txid: txHash.String(), Height: height, Timestamp: time.Unix(u.Time, 0)} + for _, in := range u.Inputs { + ch, err := chainhash.NewHashFromStr(in.Txid) + if err != nil { + Log.Errorf("error converting to chainhash for %s: %s", ws.coinType.String(), err.Error()) + continue + } + script, err := hex.DecodeString(in.ScriptSig.Hex) + if err != nil { + Log.Errorf("error converting to scriptsig for %s: %s", ws.coinType.String(), err.Error()) + continue + } + op := wire.NewOutPoint(ch, uint32(in.Vout)) + addr, err := util.DecodeAddress(in.Addr, ws.params) + if err != nil { + // Some addresses may not decode and we can still process them normally + addr = nil + } + + txin := wire.NewTxIn(op, script, [][]byte{}) + txin.Sequence = uint32(in.Sequence) + msgTx.TxIn = append(msgTx.TxIn, txin) + h, err := hex.DecodeString(op.Hash.String()) + if err != nil { + Log.Errorf("error converting outpoint hash for %s: %s", ws.coinType.String(), err.Error()) + return + } + v := int64(math.Round(in.Value * float64(util.SatoshisPerCoin(ws.coinType)))) + cbin := wallet.TransactionInput{ + OutpointHash: h, + OutpointIndex: op.Index, + LinkedAddress: addr, + Value: v, + } + cb.Inputs = append(cb.Inputs, cbin) + + sa, ok := addrs[in.Addr] + if !ok { + continue + } + if !sa.WatchOnly { + value -= v + hits++ + } + relevant = true + } + for i, out := range u.Outputs { + script, err := hex.DecodeString(out.ScriptPubKey.Hex) + if err != nil { + Log.Errorf("error converting to scriptPubkey for %s: %s", ws.coinType.String(), err.Error()) + continue + } + var addr btcutil.Address + if len(out.ScriptPubKey.Addresses) > 0 && out.ScriptPubKey.Addresses[0] != "" { + addr, err = util.DecodeAddress(out.ScriptPubKey.Addresses[0], ws.params) + if err != nil { + // Some addresses may not decode and we can still process them normally + addr = nil + } + } + + if len(out.ScriptPubKey.Addresses) == 0 { + continue + } + + v := int64(math.Round(out.Value * float64(util.SatoshisPerCoin(ws.coinType)))) + + txout := wire.NewTxOut(v, script) + msgTx.TxOut = append(msgTx.TxOut, txout) + cbout := wallet.TransactionOutput{Address: addr, Value: v, Index: uint32(i)} + cb.Outputs = append(cb.Outputs, cbout) + + sa, ok := addrs[out.ScriptPubKey.Addresses[0]] + if !ok { + continue + } + if !sa.WatchOnly { + value += v + hits++ + // Mark the key we received coins to as used + err = ws.km.MarkKeyAsUsed(sa.Addr.ScriptAddress()) + if err != nil { + Log.Errorf("marking address (%s) key used: %s", sa.Addr.String(), err.Error()) + } + } + relevant = true + } + + if !relevant { + Log.Warningf("abort saving irrelevant txid (%s) to db", u.Txid) + return + } + + cb.Value = value + cb.WatchOnly = (hits == 0) + saved, err := ws.db.Txns().Get(*txHash) + if err != nil || saved.WatchOnly != cb.WatchOnly { + ts := time.Now() + if u.Confirmations > 0 { + ts = time.Unix(u.BlockTime, 0) + } + var txBytes []byte + if len(u.RawBytes) > 0 { + txBytes = u.RawBytes + } else { + var buf bytes.Buffer + msgTx.BtcEncode(&buf, wire.ProtocolVersion, wire.BaseEncoding) + txBytes = buf.Bytes() + } + err = ws.db.Txns().Put(txBytes, txHash.String(), int(value), int(height), ts, hits == 0) + if err != nil { + Log.Errorf("putting txid (%s): %s", txHash.String(), err.Error()) + return + } + cb.Timestamp = ts + ws.callbackListeners(cb) + } else if height > 0 { + err := ws.db.Txns().UpdateHeight(*txHash, int(height), time.Unix(u.BlockTime, 0)) + if err != nil { + Log.Errorf("updating height for tx (%s): %s", txHash.String(), err.Error()) + return + } + if saved.Height != height { + cb.Timestamp = saved.Timestamp + ws.callbackListeners(cb) + } + } +} + +func (ws *WalletService) callbackListeners(cb wallet.TransactionCallback) { + for _, callback := range ws.listeners { + callback(cb) + } +} + +type storedAddress struct { + Addr btcutil.Address + WatchOnly bool +} + +func (ws *WalletService) getStoredAddresses() map[string]storedAddress { + keys := ws.km.GetKeys() + addrs := make(map[string]storedAddress) + for _, key := range keys { + addr, err := ws.km.KeyToAddress(key) + if err != nil { + Log.Warningf("error getting %s address for key: %s", ws.coinType.String(), err.Error()) + continue + } + addrs[addr.String()] = storedAddress{addr, false} + } + watchScripts, err := ws.db.WatchedScripts().GetAll() + if err != nil { + Log.Errorf("error loading %s watch scripts: %s", ws.coinType.String(), err.Error()) + return addrs + } + + for _, script := range watchScripts { + var addr btcutil.Address + switch ws.coinType { + case wallet.Bitcoin: + _, addrSlice, _, err := txscript.ExtractPkScriptAddrs(script, ws.params) + if err != nil { + Log.Warningf("error serializing %s script: %s", ws.coinType.String(), err.Error()) + continue + } + if len(addrs) == 0 { + Log.Warningf("error serializing %s script: %s", ws.coinType.String(), "Unknown script") + continue + } + addr = addrSlice[0] + case wallet.BitcoinCash: + cashAddr, err := bchutil.ExtractPkScriptAddrs(script, ws.params) + if err != nil { + Log.Warningf("error serializing %s script: %s", ws.coinType.String(), err.Error()) + continue + } + addr = cashAddr + case wallet.Zcash: + zAddr, err := zaddr.ExtractPkScriptAddrs(script, ws.params) + if err != nil { + Log.Warningf("error serializing %s script: %s", ws.coinType.String(), err.Error()) + continue + } + addr = zAddr + case wallet.Litecoin: + ltcAddr, err := laddr.ExtractPkScriptAddrs(script, ws.params) + if err != nil { + Log.Warningf("error serializing %s script: %s", ws.coinType.String(), err.Error()) + continue + } + addr = ltcAddr + } + if _, ok := addrs[addr.String()]; !ok { + addrs[addr.String()] = storedAddress{addr, true} + } + } + + return addrs +} + +func (ws *WalletService) saveHashAndHeight(hash string, height uint32) error { + hh := HashAndHeight{ + Height: height, + Hash: hash, + Timestamp: time.Now(), + } + b, err := json.MarshalIndent(&hh, "", " ") + if err != nil { + return err + } + ws.chainHeight = height + ws.bestBlock = hash + return ws.cache.Set(ws.bestHeightKey(), b) +} + +func (ws *WalletService) bestHeightKey() string { + return fmt.Sprintf("best-height-%s", ws.coinType.String()) +} diff --git a/service/wallet_service_test.go b/service/wallet_service_test.go new file mode 100644 index 0000000..84c0311 --- /dev/null +++ b/service/wallet_service_test.go @@ -0,0 +1,494 @@ +package service + +import ( + "encoding/hex" + "strconv" + "testing" + "time" + + "github.com/OpenBazaar/multiwallet/cache" + "github.com/OpenBazaar/multiwallet/datastore" + "github.com/OpenBazaar/multiwallet/keys" + "github.com/OpenBazaar/multiwallet/model/mock" + "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/ltcsuite/ltcd/chaincfg/chainhash" +) + +func mockWalletService() (*WalletService, error) { + datastore := datastore.NewMockMultiwalletDatastore() + + db, err := datastore.GetDatastoreForWallet(wallet.Bitcoin) + if err != nil { + return nil, err + } + params := &chaincfg.MainNetParams + + seed, err := hex.DecodeString("16c034c59522326867593487c03a8f9615fb248406dd0d4ffb3a6b976a248403") + if err != nil { + return nil, err + } + master, err := hdkeychain.NewMaster(seed, params) + if err != nil { + return nil, err + } + km, err := keys.NewKeyManager(db.Keys(), params, master, wallet.Bitcoin, bitcoinAddress) + if err != nil { + return nil, err + } + cli := mock.NewMockApiClient(func(addr btcutil.Address) ([]byte, error) { + return txscript.PayToAddrScript(addr) + }) + return NewWalletService(db, km, cli, params, wallet.Bitcoin, cache.NewMockCacher()) +} + +func bitcoinAddress(key *hdkeychain.ExtendedKey, params *chaincfg.Params) (btcutil.Address, error) { + return key.Address(params) +} + +func TestWalletService_ChainTip(t *testing.T) { + ws, err := mockWalletService() + if err != nil { + t.Fatal(err) + } + ws.UpdateState() + height, hash := ws.ChainTip() + if height != 1289594 { + t.Error("returned incorrect height") + } + if hash.String() != "000000000000004c68a477283a8db18c1d1c2155b03d9bc23d587ac5e1c4d1af" { + t.Error("returned incorrect best hash") + } +} + +func TestWalletService_syncTxs(t *testing.T) { + ws, err := mockWalletService() + if err != nil { + t.Fatal(err) + } + ws.syncTxs(ws.getStoredAddresses()) + + txns, err := ws.db.Txns().GetAll(true) + if err != nil { + t.Error(err) + } + if len(txns) != 3 { + t.Error("failed to update state correctly") + } + txMap := make(map[string]wallet.Txn) + for _, tx := range txns { + txMap[tx.Txid] = tx + } + + tx, ok := txMap["54ebaa07c42216393b9d5816e40dd608593b92c42e2d6525f45bdd36bce8fe4d"] + if !ok { + t.Error("failed to return tx") + } + if tx.Value != 2717080 || tx.WatchOnly { + t.Error("failed to return incorrect value for tx") + } + tx, ok = txMap["ff2b865c3b73439912eebf4cce9a15b12c7d7bcdd14ae1110a90541426c4e7c5"] + if !ok { + t.Error("failed to return tx") + } + if tx.Value != -1717080 || tx.WatchOnly { + t.Error("failed to return incorrect value for tx") + } + tx, ok = txMap["1d4288fa682fa376fbae73dbd74ea04b9ea33011d63315ca9d2d50d081e671d5"] + if !ok { + t.Error("failed to return tx") + } + if tx.Value != 10000000 || tx.WatchOnly { + t.Error("failed to return incorrect value for tx") + } +} + +func TestWalletService_syncUtxos(t *testing.T) { + ws, err := mockWalletService() + if err != nil { + t.Fatal(err) + } + script, err := hex.DecodeString("a91457fc729da2a83dc8cd3c1835351c4a813c2ae8ba87") + if err != nil { + t.Fatal(err) + } + if err := ws.db.WatchedScripts().Put(script); err != nil { + t.Fatal(err) + } + ws.syncUtxos(ws.getStoredAddresses()) + + utxos, err := ws.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + if len(utxos) != 3 { + t.Error("failed to update state correctly") + } + + utxoMap := make(map[string]wallet.Utxo) + for _, u := range utxos { + utxoMap[u.Op.Hash.String()+":"+strconv.Itoa(int(u.Op.Index))] = u + } + + u, ok := utxoMap["ff2b865c3b73439912eebf4cce9a15b12c7d7bcdd14ae1110a90541426c4e7c5:1"] + if !ok { + t.Error("failed to return correct utxo") + } + if u.Value != 1000000 || u.WatchOnly { + t.Error("returned incorrect value") + } + u, ok = utxoMap["1d4288fa682fa376fbae73dbd74ea04b9ea33011d63315ca9d2d50d081e671d5:1"] + if !ok { + t.Error("failed to return correct utxo") + } + if u.Value != 10000000 || u.WatchOnly { + t.Error("returned incorrect value") + } + u, ok = utxoMap["830bf683ab8eec1a75d891689e2989f846508bc7d500cb026ef671c2d1dce20c:1"] + if !ok { + t.Error("failed to return correct utxo") + } + if u.Value != 751918 || !u.WatchOnly { + t.Error("returned incorrect value") + } +} + +func TestWalletService_TestSyncWatchOnly(t *testing.T) { + ws, err := mockWalletService() + if err != nil { + t.Fatal(err) + } + script, err := hex.DecodeString("a91457fc729da2a83dc8cd3c1835351c4a813c2ae8ba87") + if err != nil { + t.Fatal(err) + } + if err := ws.db.WatchedScripts().Put(script); err != nil { + t.Fatal(err) + } + ws.syncTxs(ws.getStoredAddresses()) + ws.syncUtxos(ws.getStoredAddresses()) + + txns, err := ws.db.Txns().GetAll(true) + if err != nil { + t.Error(err) + } + if len(txns) != 4 { + t.Error("failed to update state correctly") + } + txMap := make(map[string]wallet.Txn) + for _, tx := range txns { + txMap[tx.Txid] = tx + } + + tx, ok := txMap["830bf683ab8eec1a75d891689e2989f846508bc7d500cb026ef671c2d1dce20c"] + if !ok { + t.Fatal("Failed to return correct transaction") + } + if !tx.WatchOnly { + t.Error("failed to return correct value for tx") + } + + utxos, err := ws.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + if len(utxos) != 3 { + t.Error("failed to update state correctly") + } + utxoMap := make(map[string]wallet.Utxo) + for _, u := range utxos { + utxoMap[u.Op.String()] = u + } + + utxo, ok := utxoMap["830bf683ab8eec1a75d891689e2989f846508bc7d500cb026ef671c2d1dce20c:1"] + if !ok { + t.Fatal("Failed to return correct utxo") + } + if !utxo.WatchOnly { + t.Error("failed to return correct value for utxo") + } +} + +func TestWalletService_ProcessIncomingTransaction(t *testing.T) { + ws, err := mockWalletService() + if err != nil { + t.Fatal(err) + } + + // Process an incoming transaction + ws.ProcessIncomingTransaction(mock.MockTransactions[0]) + txns, err := ws.db.Txns().GetAll(true) + if err != nil { + t.Error(err) + } + if len(txns) != 1 { + t.Error("failed to update state correctly") + } + if txns[0].Txid != mock.MockTransactions[0].Txid { + t.Error("saved incorrect transaction") + } + if txns[0].Value != 2717080 { + t.Error("saved incorrect value") + } + if txns[0].WatchOnly { + t.Error("saved incorrect watch only") + } + + utxos, err := ws.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + if len(utxos) != 1 { + t.Error("failed to update state correctly") + } + if utxos[0].WatchOnly { + t.Error("saved incorrect watch only") + } + if utxos[0].Op.Hash.String() != mock.MockTransactions[0].Txid { + t.Error("saved incorrect transaction ID") + } + if utxos[0].Op.Index != 1 { + t.Error("saved incorrect outpoint index") + } + if utxos[0].Value != 2717080 { + t.Error("saved incorrect value") + } + + // Process an outgoing transaction. Make sure it deletes the utxo + ws.ProcessIncomingTransaction(mock.MockTransactions[1]) + txns, err = ws.db.Txns().GetAll(true) + if err != nil { + t.Error(err) + } + if len(txns) != 2 { + t.Error("failed to update state correctly") + } + + utxos, err = ws.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + if len(utxos) != 1 { + t.Error("failed to update state correctly") + } + if utxos[0].Op.Hash.String() != mock.MockTransactions[1].Txid { + t.Error("failed to save correct utxo") + } + if utxos[0].Op.Index != 1 { + t.Error("failed to save correct utxo") + } +} + +func TestWalletService_processIncomingBlock(t *testing.T) { + ws, err := mockWalletService() + if err != nil { + t.Fatal(err) + } + ws.chainHeight = uint32(mock.MockBlocks[0].Height) + ws.bestBlock = mock.MockBlocks[0].Hash + + // Check update height + ws.processIncomingBlock(mock.MockBlocks[1]) + height, hash := ws.ChainTip() + if height != uint32(mock.MockBlocks[1].Height) { + t.Error("failed to update height") + } + if hash.String() != mock.MockBlocks[1].Hash { + t.Error("failed to update hash") + } + + // Check update height of unconfirmed txs and utxos + tx := mock.MockTransactions[0] + tx.Confirmations = 0 + ws.ProcessIncomingTransaction(tx) + + ws.processIncomingBlock(mock.MockBlocks[2]) + time.Sleep(time.Second / 2) + + txns, err := ws.db.Txns().GetAll(true) + if err != nil { + t.Fatal(err) + } + if len(txns) != 1 { + t.Fatal("Returned incorrect number of txs") + } + if txns[0].Height != int32(mock.MockBlocks[2].Height-14) { + t.Error("returned incorrect transaction height") + } + + utxos, err := ws.db.Utxos().GetAll() + if err != nil { + t.Fatal(err) + } + if len(utxos) != 1 { + t.Fatal("Returned incorrect number of utxos") + } + if utxos[0].AtHeight != int32(mock.MockBlocks[2].Height-14) { + t.Error("returned incorrect utxo height") + } + + // Test updateState() is called during reorg + block := mock.MockBlocks[1] + block.Hash = "0000000000000000003c4b7f56e45567980f02012ea00d8e384267a2d825fcf9" + ws.processIncomingBlock(block) + + time.Sleep(time.Second / 2) + + txns, err = ws.db.Txns().GetAll(true) + if err != nil { + t.Fatal(err) + } + if len(txns) != 3 { + t.Fatal("Returned incorrect number of txs") + } + + utxos, err = ws.db.Utxos().GetAll() + if err != nil { + t.Fatal(err) + } + + if len(utxos) != 3 { + t.Fatal("Returned incorrect number of utxos") + } +} + +func TestWalletService_listenersFired(t *testing.T) { + nCallbacks := 0 + var response wallet.TransactionCallback + cb := func(callback wallet.TransactionCallback) { + nCallbacks++ + response = callback + } + ws, err := mockWalletService() + if err != nil { + t.Fatal(err) + } + ws.AddTransactionListener(cb) + tx := mock.MockTransactions[0] + tx.Confirmations = 0 + ws.saveSingleTxToDB(tx, int32(mock.MockBlocks[0].Height), ws.getStoredAddresses()) + if nCallbacks != 1 { + t.Errorf("expected 1 callback but had %d", nCallbacks) + } + ch, err := chainhash.NewHashFromStr(response.Txid) + if err != nil { + t.Errorf("failed getting hash from %s: %s", response.Txid, err) + } + if ch.String() != mock.MockTransactions[0].Txid { + t.Errorf("expected hash to be %s, but was %s", mock.MockTransactions[0].Txid, ch.String()) + } + if response.Value != 2717080 { + t.Errorf("expected tx value to be 2717080, but was %d", response.Value) + } + if response.Height != 0 { + t.Error("returned incorrect height") + } + if response.WatchOnly { + t.Error("returned incorrect watch only") + } + + // Test watch only + script, err := hex.DecodeString("a91457fc729da2a83dc8cd3c1835351c4a813c2ae8ba87") + if err != nil { + t.Fatal(err) + } + if err := ws.db.WatchedScripts().Put(script); err != nil { + t.Fatal(err) + } + ws.saveSingleTxToDB(mock.MockTransactions[3], int32(mock.MockBlocks[0].Height), ws.getStoredAddresses()) + if nCallbacks != 2 { + t.Error("failed to fire transaction callback") + } + ch, err = chainhash.NewHashFromStr(response.Txid) + if err != nil { + t.Error(err) + } + if ch.String() != mock.MockTransactions[3].Txid { + t.Error("returned incorrect txid") + } + if response.Height != 1289594-1 { + t.Error("returned incorrect height") + } + if !response.WatchOnly { + t.Error("returned incorrect watch only") + } + + // Test fired when height is updated + tx = mock.MockTransactions[0] + tx.Confirmations = 1 + ws.saveSingleTxToDB(tx, int32(mock.MockBlocks[0].Height), ws.getStoredAddresses()) + if nCallbacks != 3 { + t.Error("failed to fire transaction callback") + } + ch, err = chainhash.NewHashFromStr(response.Txid) + if err != nil { + t.Error(err) + } + if ch.String() != mock.MockTransactions[0].Txid { + t.Error("returned incorrect txid") + } + if response.Value != 2717080 { + t.Error("returned incorrect value") + } + if response.Height != int32(mock.MockBlocks[0].Height) { + t.Error("returned incorrect height") + } + if response.WatchOnly { + t.Error("returned incorrect watch only") + } +} + +func TestWalletService_getStoredAddresses(t *testing.T) { + ws, err := mockWalletService() + if err != nil { + t.Fatal(err) + } + + types := []wallet.CoinType{ + wallet.Bitcoin, + wallet.BitcoinCash, + wallet.Zcash, + wallet.Litecoin, + } + + script, err := hex.DecodeString("a91457fc729da2a83dc8cd3c1835351c4a813c2ae8ba87") + if err != nil { + t.Fatal(err) + } + if err := ws.db.WatchedScripts().Put(script); err != nil { + t.Fatal(err) + } + + for _, ty := range types { + ws.coinType = ty + addrs := ws.getStoredAddresses() + if len(addrs) != 41 { + t.Error("returned incorrect number of addresses") + } + switch ty { + case wallet.Bitcoin: + sa, ok := addrs["39iF8cDMhctrPVoPbi2Vb1NnErg6CEB7BZ"] + if !sa.WatchOnly || !ok { + t.Error("returned incorrect watch only address") + } + case wallet.BitcoinCash: + sa, ok := addrs["pptlcu5a525rmjxd8svr2dguf2qnc2hghgln5xu4l7"] + if !sa.WatchOnly || !ok { + t.Error("returned incorrect watch only address") + } + case wallet.Zcash: + sa, ok := addrs["t3Sar8wdVfwgSz8rHY8qcipUhVWsB2x2xxa"] + if !sa.WatchOnly || !ok { + t.Error("returned incorrect watch only address") + } + case wallet.Litecoin: + sa, ok := addrs["39iF8cDMhctrPVoPbi2Vb1NnErg6CEB7BZ"] + if !sa.WatchOnly || !ok { + t.Error("returned incorrect watch only address") + } + } + } +} diff --git a/test/factory/transaction.go b/test/factory/transaction.go new file mode 100644 index 0000000..77304ec --- /dev/null +++ b/test/factory/transaction.go @@ -0,0 +1,50 @@ +package factory + +import "github.com/OpenBazaar/multiwallet/model" + +func NewTransaction() model.Transaction { + return model.Transaction{ + Txid: "1be612e4f2b79af279e0b307337924072b819b3aca09fcb20370dd9492b83428", + Version: 2, + Locktime: 512378, + Inputs: []model.Input{ + { + Txid: "6d892f04fc097f430d58ab06229c9b6344a130fc1842da5b990e857daed42194", + Vout: 1, + Sequence: 1, + ValueIface: "0.04294455", + ScriptSig: model.Script{ + Hex: "4830450221008665481674067564ef562cfd8d1ca8f1506133fb26a2319e4b8dfba3cedfd5de022038f27121c44e6c64b93b94d72620e11b9de35fd864730175db9176ca98f1ec610121022023e49335a0dddb864ff673468a6cc04e282571b1227933fcf3ff9babbcc662", + }, + Addr: "1C74Gbij8Q5h61W58aSKGvXK4rk82T2A3y", + Satoshis: 4294455, + }, + }, + Outputs: []model.Output{ + { + ScriptPubKey: model.OutScript{ + Script: model.Script{ + Hex: "76a914ff3f7d402fbd6d116ba4a02af9784f3ae9b7108a88ac", + }, + Type: "pay-to-pubkey-hash", + Addresses: []string{"1QGdNEDjWnghrjfTBCTDAPZZ3ffoKvGc9B"}, + }, + ValueIface: "0.01398175", + }, + { + ScriptPubKey: model.OutScript{ + Script: model.Script{ + Hex: "a9148a62462d08a977fa89226a56fca7eb01b6fef67c87", + }, + Type: "pay-to-script-hashh", + Addresses: []string{"3EJiuDqsHuAtFqiLGWKVyCfvqoGpWVCCRs"}, + }, + ValueIface: "0.02717080", + }, + }, + Time: 1520449061, + BlockHash: "0000000000000000003f1fb88ac3dab0e607e87def0e9031f7bea02cb464a04f", + BlockHeight: 512476, + Confirmations: 1, + } +} diff --git a/test/helper.go b/test/helper.go new file mode 100644 index 0000000..1038695 --- /dev/null +++ b/test/helper.go @@ -0,0 +1,85 @@ +package test + +import ( + "testing" + + "github.com/OpenBazaar/multiwallet/model" +) + +func ValidateTransaction(tx, expectedTx model.Transaction, t *testing.T) { + if tx.Txid != expectedTx.Txid { + t.Error("Returned invalid transaction") + } + if tx.Version != expectedTx.Version { + t.Error("Returned invalid transaction") + } + if tx.Locktime != expectedTx.Locktime { + t.Error("Returned invalid transaction") + } + if tx.Time != expectedTx.Time { + t.Error("Returned invalid transaction") + } + if tx.BlockHash != expectedTx.BlockHash { + t.Error("Returned invalid transaction") + } + if tx.BlockHeight != expectedTx.BlockHeight { + t.Error("Returned invalid transaction") + } + if tx.Confirmations != expectedTx.Confirmations { + t.Error("Returned invalid transaction") + } + if len(tx.Inputs) != 1 { + t.Error("Returned incorrect number of inputs") + return + } + if tx.Inputs[0].Txid != expectedTx.Inputs[0].Txid { + t.Error("Returned invalid transaction") + } + if tx.Inputs[0].Value != 0.04294455 { + t.Error("Returned invalid transaction") + } + if tx.Inputs[0].Satoshis != expectedTx.Inputs[0].Satoshis { + t.Error("Returned invalid transaction") + } + if tx.Inputs[0].Addr != expectedTx.Inputs[0].Addr { + t.Error("Returned invalid transaction") + } + if tx.Inputs[0].Sequence != expectedTx.Inputs[0].Sequence { + t.Error("Returned invalid transaction") + } + if tx.Inputs[0].Vout != expectedTx.Inputs[0].Vout { + t.Error("Returned invalid transaction") + } + if tx.Inputs[0].ScriptSig.Hex != expectedTx.Inputs[0].ScriptSig.Hex { + t.Error("Returned invalid transaction") + } + + if len(tx.Outputs) != 2 { + t.Error("Returned incorrect number of outputs") + return + } + if tx.Outputs[0].Value != 0.01398175 { + t.Error("Returned invalid transaction") + } + if tx.Outputs[0].ScriptPubKey.Hex != expectedTx.Outputs[0].ScriptPubKey.Hex { + t.Error("Returned invalid transaction") + } + if tx.Outputs[0].ScriptPubKey.Type != expectedTx.Outputs[0].ScriptPubKey.Type { + t.Error("Returned invalid transaction") + } + if tx.Outputs[0].ScriptPubKey.Addresses[0] != expectedTx.Outputs[0].ScriptPubKey.Addresses[0] { + t.Error("Returned invalid transaction") + } + if tx.Outputs[1].Value != 0.02717080 { + t.Error("Returned invalid transaction") + } + if tx.Outputs[1].ScriptPubKey.Hex != expectedTx.Outputs[1].ScriptPubKey.Hex { + t.Error("Returned invalid transaction") + } + if tx.Outputs[1].ScriptPubKey.Type != expectedTx.Outputs[1].ScriptPubKey.Type { + t.Error("Returned invalid transaction") + } + if tx.Outputs[1].ScriptPubKey.Addresses[0] != expectedTx.Outputs[1].ScriptPubKey.Addresses[0] { + t.Error("Returned invalid transaction") + } +} diff --git a/test_compile.sh b/test_compile.sh new file mode 100644 index 0000000..6170a91 --- /dev/null +++ b/test_compile.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e +pwd +go get gopkg.in/jarcoal/httpmock.v1 +go get github.com/OpenBazaar/multiwallet +go get github.com/mattn/go-sqlite3 +go test -coverprofile=bitcoin.cover.out ./bitcoin +go test -coverprofile=client.cover.out ./client +go test -coverprofile=config.cover.out ./config +go test -coverprofile=keys.cover.out ./keys +go test -coverprofile=litecoin.cover.out ./litecoin +go test -coverprofile=litecoin.address.cover.out ./litecoin/address +go test -coverprofile=service.cover.out ./service +go test -coverprofile=util.cover.out ./util +go test -coverprofile=zcash.cover.out ./zcash +go test -coverprofile=zcash.address.cover.out ./zcash/address +go test -coverprofile=multiwallet.cover.out ./ +echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \ +awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out +rm -rf *.cover.out diff --git a/util/address.go b/util/address.go new file mode 100644 index 0000000..9bd2a12 --- /dev/null +++ b/util/address.go @@ -0,0 +1,30 @@ +package util + +import ( + liteaddr "github.com/OpenBazaar/multiwallet/litecoin/address" + zaddr "github.com/OpenBazaar/multiwallet/zcash/address" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/cpacia/bchutil" + + "errors" +) + +func DecodeAddress(address string, params *chaincfg.Params) (btcutil.Address, error) { + if len(address) == 0 { + return nil, errors.New("unknown address") + } + if addr, err := btcutil.DecodeAddress(address, params); err == nil { + return addr, nil + } + if addr, err := bchutil.DecodeAddress(address, params); err == nil { + return addr, nil + } + if addr, err := liteaddr.DecodeAddress(address, params); err == nil { + return addr, nil + } + if addr, err := zaddr.DecodeAddress(address, params); err == nil { + return addr, nil + } + return nil, errors.New("unknown address") +} diff --git a/util/balance.go b/util/balance.go new file mode 100644 index 0000000..ca40b74 --- /dev/null +++ b/util/balance.go @@ -0,0 +1,59 @@ +package util + +import ( + "bytes" + wi "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/wire" +) + +func CalcBalance(utxos []wi.Utxo, txns []wi.Txn) (confirmed, unconfirmed int64) { + var txmap = make(map[string]wi.Txn) + for _, tx := range txns { + txmap[tx.Txid] = tx + } + + for _, utxo := range utxos { + if !utxo.WatchOnly { + if utxo.AtHeight > 0 { + confirmed += utxo.Value + } else { + if checkIfStxoIsConfirmed(utxo.Op.Hash.String(), txmap) { + confirmed += utxo.Value + } else { + unconfirmed += utxo.Value + } + } + } + } + return confirmed, unconfirmed +} + +func checkIfStxoIsConfirmed(txid string, txmap map[string]wi.Txn) bool { + // First look up tx and derserialize + txn, ok := txmap[txid] + if !ok { + return false + } + tx := wire.NewMsgTx(1) + rbuf := bytes.NewReader(txn.Bytes) + err := tx.BtcDecode(rbuf, wire.ProtocolVersion, wire.WitnessEncoding) + if err != nil { + return false + } + + // For each input, recursively check if confirmed + inputsConfirmed := true + for _, in := range tx.TxIn { + checkTx, ok := txmap[in.PreviousOutPoint.Hash.String()] + if ok { // Is an stxo. If confirmed we can return true. If no, we need to check the dependency. + if checkTx.Height == 0 { + if !checkIfStxoIsConfirmed(in.PreviousOutPoint.Hash.String(), txmap) { + inputsConfirmed = false + } + } + } else { // We don't have the tx in our db so it can't be an stxo. Return false. + return false + } + } + return inputsConfirmed +} diff --git a/util/balance_test.go b/util/balance_test.go new file mode 100644 index 0000000..9d1b056 --- /dev/null +++ b/util/balance_test.go @@ -0,0 +1,112 @@ +package util + +import ( + "bytes" + "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "testing" +) + +func TestCalcBalance(t *testing.T) { + ch1, err := chainhash.NewHashFromStr("ccfd8d91b38e065a4d0f655fffabbdbf61666d1fdf1b54b7432c5d0ad453b76d") + if err != nil { + t.Error(err) + } + ch2, err := chainhash.NewHashFromStr("37aface44f82f6f319957b501030da2595b35d8bbc953bbe237f378c5f715bdd") + if err != nil { + t.Error(err) + } + ch3, err := chainhash.NewHashFromStr("2d08e0e877ff9d034ca272666d01626e96a0cf9e17004aafb4ae9d5aa109dd20") + if err != nil { + t.Error(err) + } + ch4, err := chainhash.NewHashFromStr("c803c8e21a464f0425fda75fb43f5a40bb6188bab9f3bfe0c597b46899e30045") + if err != nil { + t.Error(err) + } + + var utxos []wallet.Utxo + var txns []wallet.Txn + + // Test confirmed and unconfirmed + utxos = append(utxos, wallet.Utxo{ + AtHeight: 500, + Value: 1000, + Op: *wire.NewOutPoint(ch1, 0), + }) + utxos = append(utxos, wallet.Utxo{ + AtHeight: 0, + Value: 2000, + Op: *wire.NewOutPoint(ch2, 0), + }) + + confirmed, unconfirmed := CalcBalance(utxos, txns) + if confirmed != 1000 || unconfirmed != 2000 { + t.Error("Returned incorrect balance") + } + + // Test confirmed stxo + tx := wire.NewMsgTx(1) + op := wire.NewOutPoint(ch3, 1) + in := wire.NewTxIn(op, []byte{}, [][]byte{}) + out := wire.NewTxOut(500, []byte{0x00}) + tx.TxIn = append(tx.TxIn, in) + tx.TxOut = append(tx.TxOut, out) + var buf bytes.Buffer + err = tx.BtcEncode(&buf, wire.ProtocolVersion, wire.WitnessEncoding) + if err != nil { + t.Error(err) + } + txns = append(txns, wallet.Txn{ + Bytes: buf.Bytes(), + Txid: "37aface44f82f6f319957b501030da2595b35d8bbc953bbe237f378c5f715bdd", + }) + tx = wire.NewMsgTx(1) + op = wire.NewOutPoint(ch4, 1) + in = wire.NewTxIn(op, []byte{}, [][]byte{}) + out = wire.NewTxOut(500, []byte{0x00}) + tx.TxIn = append(tx.TxIn, in) + tx.TxOut = append(tx.TxOut, out) + var buf2 bytes.Buffer + err = tx.BtcEncode(&buf, wire.ProtocolVersion, wire.WitnessEncoding) + if err != nil { + t.Error(err) + } + txns = append(txns, wallet.Txn{ + Bytes: buf2.Bytes(), + Txid: "2d08e0e877ff9d034ca272666d01626e96a0cf9e17004aafb4ae9d5aa109dd20", + Height: 1999, + }) + confirmed, unconfirmed = CalcBalance(utxos, txns) + if confirmed != 3000 || unconfirmed != 0 { + t.Error("Returned incorrect balance") + } + + // Test unconfirmed stxo + txns = []wallet.Txn{} + txns = append(txns, wallet.Txn{ + Bytes: buf.Bytes(), + Txid: "37aface44f82f6f319957b501030da2595b35d8bbc953bbe237f378c5f715bdd", + }) + txns = append(txns, wallet.Txn{ + Bytes: buf2.Bytes(), + Txid: "2d08e0e877ff9d034ca272666d01626e96a0cf9e17004aafb4ae9d5aa109dd20", + Height: 0, + }) + confirmed, unconfirmed = CalcBalance(utxos, txns) + if confirmed != 1000 || unconfirmed != 2000 { + t.Error("Returned incorrect balance") + } + + // Test without stxo in db + txns = []wallet.Txn{} + txns = append(txns, wallet.Txn{ + Bytes: buf.Bytes(), + Txid: "37aface44f82f6f319957b501030da2595b35d8bbc953bbe237f378c5f715bdd", + }) + confirmed, unconfirmed = CalcBalance(utxos, txns) + if confirmed != 1000 || unconfirmed != 2000 { + t.Error("Returned incorrect balance") + } +} diff --git a/util/coin.go b/util/coin.go new file mode 100644 index 0000000..57ec5b7 --- /dev/null +++ b/util/coin.go @@ -0,0 +1,94 @@ +package util + +import ( + "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" + "github.com/btcsuite/btcutil/coinset" + hd "github.com/btcsuite/btcutil/hdkeychain" +) + +type Coin struct { + TxHash *chainhash.Hash + TxIndex uint32 + TxValue btcutil.Amount + TxNumConfs int64 + ScriptPubKey []byte +} + +func (c *Coin) Hash() *chainhash.Hash { return c.TxHash } +func (c *Coin) Index() uint32 { return c.TxIndex } +func (c *Coin) Value() btcutil.Amount { return c.TxValue } +func (c *Coin) PkScript() []byte { return c.ScriptPubKey } +func (c *Coin) NumConfs() int64 { return c.TxNumConfs } +func (c *Coin) ValueAge() int64 { return int64(c.TxValue) * c.TxNumConfs } + +func NewCoin(txid chainhash.Hash, index uint32, value btcutil.Amount, numConfs int64, scriptPubKey []byte) (coinset.Coin, error) { + c := &Coin{ + TxHash: &txid, + TxIndex: index, + TxValue: value, + TxNumConfs: numConfs, + ScriptPubKey: scriptPubKey, + } + return coinset.Coin(c), nil +} + +func GatherCoins(height uint32, utxos []wallet.Utxo, scriptToAddress func(script []byte) (btcutil.Address, error), getKeyForScript func(scriptAddress []byte) (*hd.ExtendedKey, error)) map[coinset.Coin]*hd.ExtendedKey { + m := make(map[coinset.Coin]*hd.ExtendedKey) + for _, u := range utxos { + if u.WatchOnly { + continue + } + var confirmations int32 + if u.AtHeight > 0 { + confirmations = int32(height) - u.AtHeight + } + c, err := NewCoin(u.Op.Hash, u.Op.Index, btcutil.Amount(u.Value), int64(confirmations), u.ScriptPubkey) + if err != nil { + continue + } + + addr, err := scriptToAddress(u.ScriptPubkey) + if err != nil { + continue + } + key, err := getKeyForScript(addr.ScriptAddress()) + if err != nil { + continue + } + m[c] = key + } + return m +} + +func LoadAllInputs(tx *wire.MsgTx, coinMap map[coinset.Coin]*hd.ExtendedKey, params *chaincfg.Params) (int64, map[wire.OutPoint]int64, map[wire.OutPoint][]byte, map[string]*btcutil.WIF) { + inVals := make(map[wire.OutPoint]int64) + totalIn := int64(0) + additionalPrevScripts := make(map[wire.OutPoint][]byte) + additionalKeysByAddress := make(map[string]*btcutil.WIF) + + for coin, key := range coinMap { + outpoint := wire.NewOutPoint(coin.Hash(), coin.Index()) + in := wire.NewTxIn(outpoint, nil, nil) + additionalPrevScripts[*outpoint] = coin.PkScript() + tx.TxIn = append(tx.TxIn, in) + val := int64(coin.Value().ToUnit(btcutil.AmountSatoshi)) + totalIn += val + inVals[*outpoint] = val + + addr, err := key.Address(params) + if err != nil { + continue + } + privKey, err := key.ECPrivKey() + if err != nil { + continue + } + wif, _ := btcutil.NewWIF(privKey, params, true) + additionalKeysByAddress[addr.EncodeAddress()] = wif + } + return totalIn, inVals, additionalPrevScripts, additionalKeysByAddress +} diff --git a/util/coin_test.go b/util/coin_test.go new file mode 100644 index 0000000..3d2caf2 --- /dev/null +++ b/util/coin_test.go @@ -0,0 +1,251 @@ +package util + +import ( + "bytes" + "encoding/hex" + "errors" + "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + hd "github.com/btcsuite/btcutil/hdkeychain" + "testing" +) + +func TestNewCoin(t *testing.T) { + txid := "7eae21cc2709a58a8795f9b0239b6b8ed974a3c4ce10f8919deae527995dd744" + ch, err := chainhash.NewHashFromStr(txid) + if err != nil { + t.Error(err) + return + } + scriptPubkey := "76a914ab8c06d1c22f575b30c3afc66bde8b3aa2de99bc88ac" + scriptBytes, err := hex.DecodeString(scriptPubkey) + if err != nil { + t.Error(err) + return + } + c, err := NewCoin(*ch, 0, btcutil.Amount(100000), 10, scriptBytes) + if err != nil { + t.Error(err) + return + } + if c.Hash().String() != ch.String() { + t.Error("Returned incorrect txid") + } + if c.Index() != 0 { + t.Error("Returned incorrect index") + } + if c.Value() != btcutil.Amount(100000) { + t.Error("Returned incorrect value") + } + if !bytes.Equal(c.PkScript(), scriptBytes) { + t.Error("Returned incorrect pk script") + } + if c.NumConfs() != 10 { + t.Error("Returned incorrect num confs") + } + if c.ValueAge() != 1000000 { + t.Error("Returned incorrect value age") + } +} + +func buildTestData() (uint32, []wallet.Utxo, func(script []byte) (btcutil.Address, error), + func(scriptAddress []byte) (*hd.ExtendedKey, error), + map[string]wallet.Utxo, map[string]*hd.ExtendedKey, error) { + + scriptPubkey1 := "76a914ab8c06d1c22f575b30c3afc66bde8b3aa2de99bc88ac" + scriptBytes1, err := hex.DecodeString(scriptPubkey1) + if err != nil { + return 0, nil, nil, nil, nil, nil, err + } + scriptPubkey2 := "76a914281032bc033f41a33ded636bc2f7c2d67bb2871f88ac" + scriptBytes2, err := hex.DecodeString(scriptPubkey2) + if err != nil { + return 0, nil, nil, nil, nil, nil, err + } + scriptPubkey3 := "76a91450033f99ce3ed61dc428a0ac481e9bdab646664c88ac" + scriptBytes3, err := hex.DecodeString(scriptPubkey3) + if err != nil { + return 0, nil, nil, nil, nil, nil, err + } + ch1, err := chainhash.NewHashFromStr("8cf466484a741850b63482133b6f7d506297c624290db2bb74214e4f9932f93e") + if err != nil { + return 0, nil, nil, nil, nil, nil, err + } + op1 := wire.NewOutPoint(ch1, 0) + ch2, err := chainhash.NewHashFromStr("8fc073d5452cc2765a24baf5d434fedc1d16b7f74f9dabce209a6b416d4fb91f") + if err != nil { + return 0, nil, nil, nil, nil, nil, err + } + op2 := wire.NewOutPoint(ch2, 1) + ch3, err := chainhash.NewHashFromStr("d7144e933f4a03ff194e373331d5a4ef8c5e4ce8df666c66b882145e686834b1") + if err != nil { + return 0, nil, nil, nil, nil, nil, err + } + op3 := wire.NewOutPoint(ch3, 2) + utxos := []wallet.Utxo{ + { + Value: 100000, + WatchOnly: false, + AtHeight: 300000, + ScriptPubkey: scriptBytes1, + Op: *op1, + }, + { + Value: 50000, + WatchOnly: false, + AtHeight: 350000, + ScriptPubkey: scriptBytes2, + Op: *op2, + }, + { + Value: 99000, + WatchOnly: true, + AtHeight: 250000, + ScriptPubkey: scriptBytes3, + Op: *op3, + }, + } + + utxoMap := make(map[string]wallet.Utxo) + utxoMap[utxos[0].Op.Hash.String()] = utxos[0] + utxoMap[utxos[1].Op.Hash.String()] = utxos[1] + utxoMap[utxos[2].Op.Hash.String()] = utxos[2] + + master, err := hd.NewMaster([]byte("8cf466484a741850b63482133b6f7d506297c624290db2bb74214e4f9932f93e"), &chaincfg.MainNetParams) + if err != nil { + return 0, nil, nil, nil, nil, nil, err + } + key0, err := master.Child(0) + if err != nil { + return 0, nil, nil, nil, nil, nil, err + } + key1, err := master.Child(1) + if err != nil { + return 0, nil, nil, nil, nil, nil, err + } + key2, err := master.Child(2) + if err != nil { + return 0, nil, nil, nil, nil, nil, err + } + + keyMap := make(map[string]*hd.ExtendedKey) + keyMap["ab8c06d1c22f575b30c3afc66bde8b3aa2de99bc"] = key0 + keyMap["281032bc033f41a33ded636bc2f7c2d67bb2871f"] = key1 + keyMap["50033f99ce3ed61dc428a0ac481e9bdab646664c"] = key2 + + height := uint32(351000) + + scriptToAddress := func(script []byte) (btcutil.Address, error) { + _, addrs, _, err := txscript.ExtractPkScriptAddrs(script, &chaincfg.MainNetParams) + if err != nil { + return nil, err + } + return addrs[0], nil + } + getKeyForScript := func(scriptAddress []byte) (*hd.ExtendedKey, error) { + key, ok := keyMap[hex.EncodeToString(scriptAddress)] + if !ok { + return nil, errors.New("key not found") + } + return key, nil + } + return height, utxos, scriptToAddress, getKeyForScript, utxoMap, keyMap, nil +} + +func TestGatherCoins(t *testing.T) { + + height, utxos, scriptToAddress, getKeyForScript, utxoMap, keyMap, err := buildTestData() + if err != nil { + t.Fatal(err) + } + + coins := GatherCoins(height, utxos, scriptToAddress, getKeyForScript) + if len(coins) != 2 { + t.Error("Returned incorrect number of coins") + } + for coin, key := range coins { + u := utxoMap[coin.Hash().String()] + addr, err := scriptToAddress(coin.PkScript()) + if err != nil { + t.Error(err) + } + k := keyMap[hex.EncodeToString(addr.ScriptAddress())] + if coin.Value() != btcutil.Amount(u.Value) { + t.Error("Returned incorrect value") + } + if coin.Hash().String() != u.Op.Hash.String() { + t.Error("Returned incorrect outpoint hash") + } + if coin.Index() != u.Op.Index { + t.Error("Returned incorrect outpoint index") + } + if !bytes.Equal(coin.PkScript(), u.ScriptPubkey) { + t.Error("Returned incorrect script pubkey") + } + if key.String() != k.String() { + t.Error("Returned incorrect key") + } + } +} + +func TestLoadAllInputs(t *testing.T) { + height, utxos, scriptToAddress, getKeyForScript, _, keyMap, err := buildTestData() + if err != nil { + t.Fatal(err) + } + coins := GatherCoins(height, utxos, scriptToAddress, getKeyForScript) + + tx := wire.NewMsgTx(1) + totalIn, inputValMap, additionalPrevScripts, additionalKeysByAddress := LoadAllInputs(tx, coins, &chaincfg.MainNetParams) + + if totalIn != 150000 { + t.Errorf("Failed to return correct total input value: expected 150000 got %d", totalIn) + } + + for _, u := range utxos { + val, ok := inputValMap[u.Op] + if !u.WatchOnly && !ok { + t.Errorf("Missing outpoint %s in input value map", u.Op) + } + if u.WatchOnly && ok { + t.Error("Watch only output found in input values map") + } + + if !u.WatchOnly && val != u.Value { + t.Errorf("Returned incorrect input value for outpoint %s. Expected %d, got %d", u.Op, u.Value, val) + } + + prevScript, ok := additionalPrevScripts[u.Op] + if !u.WatchOnly && !ok { + t.Errorf("Missing outpoint %s in additionalPrevScripts map", u.Op) + } + if u.WatchOnly && ok { + t.Error("Watch only output found in additionalPrevScripts map") + } + + if !u.WatchOnly && !bytes.Equal(prevScript, u.ScriptPubkey) { + t.Errorf("Returned incorrect script for script %s. Expected %x, got %x", u.Op, u.ScriptPubkey, prevScript) + } + } + + for _, key := range additionalKeysByAddress { + found := false + for _, k := range keyMap { + priv, err := k.ECPrivKey() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(key.PrivKey.Serialize(), priv.Serialize()) { + found = true + break + } + } + if !found { + t.Errorf("Key %s not in additionalKeysByAddress map", key.String()) + } + } +} diff --git a/util/currency.go b/util/currency.go new file mode 100644 index 0000000..d8ad96b --- /dev/null +++ b/util/currency.go @@ -0,0 +1,8 @@ +package util + +import "strings" + +// NormalizeCurrencyCode standardizes the format for the given currency code +func NormalizeCurrencyCode(currencyCode string) string { + return strings.ToUpper(currencyCode) +} diff --git a/util/fees.go b/util/fees.go new file mode 100644 index 0000000..ffaffbf --- /dev/null +++ b/util/fees.go @@ -0,0 +1,101 @@ +package util + +import ( + "net/http" + + "github.com/OpenBazaar/wallet-interface" +) + +type httpClient interface { + Get(string) (*http.Response, error) +} + +// Fees describe +type Fees struct { + FastestFee uint64 + HalfHourFee uint64 + HourFee uint64 +} + +type FeeProvider struct { + maxFee uint64 + priorityFee uint64 + normalFee uint64 + economicFee uint64 + + exchangeRates wallet.ExchangeRates +} + +// We will target a fee per byte such that it would equal +// 0.1 USD cent for economic, 1 USD cents for normal and +// 5 USD cents for priority for a median (226 byte) transaction. +type FeeTargetInUSDCents float64 + +const ( + EconomicTarget FeeTargetInUSDCents = 0.1 + NormalTarget FeeTargetInUSDCents = 1 + PriorityTarget FeeTargetInUSDCents = 5 + + AverageTransactionSize = 226 +) + +func NewFeeProvider(maxFee, priorityFee, normalFee, economicFee uint64, exchangeRates wallet.ExchangeRates) *FeeProvider { + return &FeeProvider{ + maxFee: maxFee, + priorityFee: priorityFee, + normalFee: normalFee, + economicFee: economicFee, + exchangeRates: exchangeRates, + } +} + +func (fp *FeeProvider) GetFeePerByte(feeLevel wallet.FeeLevel) uint64 { + defaultFee := func() uint64 { + switch feeLevel { + case wallet.PRIOIRTY: + return fp.priorityFee + case wallet.NORMAL: + return fp.normalFee + case wallet.ECONOMIC: + return fp.economicFee + case wallet.FEE_BUMP: + return fp.priorityFee * 2 + default: + return fp.normalFee + } + } + if fp.exchangeRates == nil { + return defaultFee() + } + + rate, err := fp.exchangeRates.GetLatestRate("USD") + if err != nil || rate == 0 { + return defaultFee() + } + + var target FeeTargetInUSDCents + switch feeLevel { + case wallet.PRIOIRTY: + target = PriorityTarget + case wallet.NORMAL: + target = NormalTarget + case wallet.ECONOMIC: + target = EconomicTarget + case wallet.FEE_BUMP: + target = PriorityTarget * 2 + default: + target = NormalTarget + } + + feePerByte := (((float64(target) / 100) / rate) * 100000000) / AverageTransactionSize + + if uint64(feePerByte) > fp.maxFee { + return fp.maxFee + } + + if uint64(feePerByte) == 0 { + return 1 + } + + return uint64(feePerByte) +} diff --git a/util/fees_test.go b/util/fees_test.go new file mode 100644 index 0000000..7b36bfd --- /dev/null +++ b/util/fees_test.go @@ -0,0 +1,85 @@ +package util + +import ( + "bytes" + "testing" + + "github.com/OpenBazaar/wallet-interface" +) + +type ClosingBuffer struct { + *bytes.Buffer +} + +func (cb *ClosingBuffer) Close() (err error) { + return +} + +type mockExchangeRate struct { + rate float64 +} + +func (m *mockExchangeRate) GetExchangeRate(currencyCode string) (float64, error) { + return 0, nil +} + +func (m *mockExchangeRate) GetLatestRate(currencyCode string) (float64, error) { + return m.rate, nil +} + +func (m *mockExchangeRate) GetAllRates(usecache bool) (map[string]float64, error) { + return make(map[string]float64), nil +} + +func (m *mockExchangeRate) UnitsPerCoin() int { + return 0 +} + +func TestFeeProvider_GetFeePerByte(t *testing.T) { + er := &mockExchangeRate{438} + fp := NewFeeProvider(2000, 360, 320, 280, er) + + // Test using exchange rates + if fp.GetFeePerByte(wallet.PRIOIRTY) != 50 { + t.Error("Returned incorrect fee per byte") + } + if fp.GetFeePerByte(wallet.NORMAL) != 10 { + t.Error("Returned incorrect fee per byte") + } + if fp.GetFeePerByte(wallet.ECONOMIC) != 1 { + t.Error("Returned incorrect fee per byte") + } + if fp.GetFeePerByte(wallet.FEE_BUMP) != 101 { + t.Error("Returned incorrect fee per byte") + } + + // Test exchange rate is limited at max if bad exchange rate is returned + er.rate = 0.1 + if fp.GetFeePerByte(wallet.PRIOIRTY) != 2000 { + t.Error("Returned incorrect fee per byte") + } + if fp.GetFeePerByte(wallet.NORMAL) != 2000 { + t.Error("Returned incorrect fee per byte") + } + if fp.GetFeePerByte(wallet.ECONOMIC) != 2000 { + t.Error("Returned incorrect fee per byte") + } + if fp.GetFeePerByte(wallet.FEE_BUMP) != 2000 { + t.Error("Returned incorrect fee per byte") + } + + // Test no Exchange rate provided + fp.exchangeRates = nil + if fp.GetFeePerByte(wallet.PRIOIRTY) != 360 { + t.Error("Returned incorrect fee per byte") + } + if fp.GetFeePerByte(wallet.NORMAL) != 320 { + t.Error("Returned incorrect fee per byte") + } + if fp.GetFeePerByte(wallet.ECONOMIC) != 280 { + t.Error("Returned incorrect fee per byte") + } + if fp.GetFeePerByte(wallet.FEE_BUMP) != 720 { + t.Error("Returned incorrect fee per byte") + } +} diff --git a/util/outpoints.go b/util/outpoints.go new file mode 100644 index 0000000..90562ab --- /dev/null +++ b/util/outpoints.go @@ -0,0 +1,10 @@ +package util + +import "github.com/btcsuite/btcd/wire" + +func OutPointsEqual(a, b wire.OutPoint) bool { + if !a.Hash.IsEqual(&b.Hash) { + return false + } + return a.Index == b.Index +} diff --git a/util/outpoints_test.go b/util/outpoints_test.go new file mode 100644 index 0000000..043c7c1 --- /dev/null +++ b/util/outpoints_test.go @@ -0,0 +1,26 @@ +package util + +import ( + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "testing" +) + +func TestOutPointsEqual(t *testing.T) { + h1, err := chainhash.NewHashFromStr("6f7a58ad92702601fcbaac0e039943a384f5274a205c16bb8bbab54f9ea2fbad") + if err != nil { + t.Error(err) + } + op := wire.NewOutPoint(h1, 0) + h2, err := chainhash.NewHashFromStr("a0d4cbcd8d0694e1132400b5e114b31bc3e0d8a2ac26e054f78727c95485b528") + op2 := wire.NewOutPoint(h2, 0) + if err != nil { + t.Error(err) + } + if !OutPointsEqual(*op, *op) { + t.Error("Failed to detect equal outpoints") + } + if OutPointsEqual(*op, *op2) { + t.Error("Incorrectly returned equal outpoints") + } +} diff --git a/util/satoshis.go b/util/satoshis.go new file mode 100644 index 0000000..dc246c6 --- /dev/null +++ b/util/satoshis.go @@ -0,0 +1,8 @@ +package util + +import "github.com/OpenBazaar/wallet-interface" + +// All implemented coins currently have 100m satoshis per coin +func SatoshisPerCoin(coinType wallet.CoinType) float64 { + return 100000000 +} diff --git a/zcash/address/address.go b/zcash/address/address.go new file mode 100644 index 0000000..f9f77f4 --- /dev/null +++ b/zcash/address/address.go @@ -0,0 +1,335 @@ +package address + +import ( + "errors" + + "bytes" + "crypto/sha256" + "fmt" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/base58" + "golang.org/x/crypto/ripemd160" +) + +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") + + // ErrInvalidFormat describes an error where decoding failed due to invalid version + ErrInvalidFormat = errors.New("invalid format: version and/or checksum bytes missing") + + NetIDs map[string]NetID +) + +type NetID struct { + AddressPubKeyHash []byte + AddressScriptHash []byte + ZAddress []byte +} + +func init() { + NetIDs = make(map[string]NetID) + NetIDs[chaincfg.MainNetParams.Name] = NetID{[]byte{0x1c, 0xb8}, []byte{0x1c, 0xbd}, []byte{0x16, 0x9a}} + NetIDs[chaincfg.TestNet3Params.Name] = NetID{[]byte{0x1d, 0x25}, []byte{0x1c, 0xba}, []byte{0x16, 0xb6}} + NetIDs[chaincfg.RegressionNetParams.Name] = NetID{[]byte{0x1d, 0x25}, []byte{0x1c, 0xba}, []byte{0x16, 0xb6}} +} + +// checksum: first four bytes of sha256^2 +func checksum(input []byte) (cksum [4]byte) { + h := sha256.Sum256(input) + h2 := sha256.Sum256(h[:]) + copy(cksum[:], h2[:4]) + return +} + +// CheckEncode prepends a version byte and appends a four byte checksum. +func CheckEncode(input []byte, version []byte) string { + b := make([]byte, 0, 2+len(input)+4) + b = append(b, version...) + b = append(b, input[:]...) + cksum := checksum(b) + b = append(b, cksum[:]...) + return base58.Encode(b) +} + +// CheckDecode decodes a string that was encoded with CheckEncode and verifies the checksum. +func CheckDecode(input string) (result []byte, version []byte, err error) { + decoded := base58.Decode(input) + if len(decoded) < 5 { + return nil, nil, ErrInvalidFormat + } + version = append(version, decoded[0:2]...) + var cksum [4]byte + copy(cksum[:], decoded[len(decoded)-4:]) + if checksum(decoded[:len(decoded)-4]) != cksum { + return nil, nil, ErrChecksumMismatch + } + payload := decoded[2 : len(decoded)-4] + result = append(result, payload...) + return +} + +// encodeAddress returns a human-readable payment address given a ripemd160 hash +// and netID which encodes the zcash 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 2 bytes for a network and address class (i.e. P2PKH vs + // P2SH), 20 bytes for a RIPEMD160 hash, and 4 bytes of checksum. + return CheckEncode(hash160[:ripemd160.Size], netID) +} + +// DecodeAddress decodes the string encoding of an address and returns +// the Address if addr is a valid encoding for a known address type. +// +// The zcash network the address is associated with is extracted if possible. +func DecodeAddress(addr string, defaultNet *chaincfg.Params) (btcutil.Address, error) { + + checkID, ok := NetIDs[defaultNet.Name] + if !ok { + return nil, errors.New("unknown network parameters") + } + + // Switch on decoded length to determine the type. + decoded, netID, err := 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 := bytes.Equal(netID, checkID.AddressPubKeyHash) + isP2SH := bytes.Equal(netID, checkID.AddressScriptHash) + switch hash160 := decoded; { + case isP2PKH && isP2SH: + return nil, ErrAddressCollision + case isP2PKH: + return newAddressPubKeyHash(hash160, defaultNet) + case isP2SH: + return newAddressScriptHashFromHash(hash160, defaultNet) + default: + return nil, ErrUnknownAddressType + } + + default: + return nil, errors.New("decoded address is of unknown size") + } +} + +// 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) { + return newAddressPubKeyHash(pkHash, net) +} + +// 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, net *chaincfg.Params) (*AddressPubKeyHash, error) { + // Check for a valid pubkey hash length. + if len(pkHash) != ripemd160.Size { + return nil, errors.New("pkHash must be 20 bytes") + } + + netID, ok := NetIDs[net.Name] + if !ok { + return nil, errors.New("unknown network parameters") + } + + addr := &AddressPubKeyHash{netID: netID.AddressPubKeyHash} + 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 zcash network. +func (a *AddressPubKeyHash) IsForNet(net *chaincfg.Params) bool { + checkID, ok := NetIDs[net.Name] + if !ok { + return false + } + return bytes.Equal(a.netID, checkID.AddressPubKeyHash) +} + +// 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) + return newAddressScriptHashFromHash(scriptHash, net) +} + +// NewAddressScriptHashFromHash returns a new AddressScriptHash. scriptHash +// must be 20 bytes. +func NewAddressScriptHashFromHash(scriptHash []byte, net *chaincfg.Params) (*AddressScriptHash, error) { + return newAddressScriptHashFromHash(scriptHash, net) +} + +// 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, net *chaincfg.Params) (*AddressScriptHash, error) { + // Check for a valid script hash length. + if len(scriptHash) != ripemd160.Size { + return nil, errors.New("scriptHash must be 20 bytes") + } + + netID, ok := NetIDs[net.Name] + if !ok { + return nil, errors.New("unknown network parameters") + } + + addr := &AddressScriptHash{netID: netID.AddressScriptHash} + 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 zcash network. +func (a *AddressScriptHash) IsForNet(net *chaincfg.Params) bool { + checkID, ok := NetIDs[net.Name] + if !ok { + return false + } + return bytes.Equal(a.netID, checkID.AddressScriptHash) +} + +// 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 +} + +// 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 *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() +} + +// ExtractPkScriptAddrs returns the type of script, addresses and required +// signatures associated with the passed PkScript. Note that it only works for +// 'standard' transaction script types. Any data such as public keys which are +// invalid are omitted from the results. +func ExtractPkScriptAddrs(pkScript []byte, chainParams *chaincfg.Params) (btcutil.Address, error) { + // No valid addresses or required signatures if the script doesn't + // parse. + 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) + } + return nil, errors.New("unknown script type") +} diff --git a/zcash/address/address_test.go b/zcash/address/address_test.go new file mode 100644 index 0000000..1f70a9f --- /dev/null +++ b/zcash/address/address_test.go @@ -0,0 +1,87 @@ +package address + +import ( + "github.com/btcsuite/btcd/chaincfg" + "testing" +) + +func TestDecodeZcashAddress(t *testing.T) { + // Mainnet + addr, err := DecodeAddress("t1cQTWs2rPYM5R3zJiLA8MR3nZsXd1p2U6Q", &chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + if addr.String() != "t1cQTWs2rPYM5R3zJiLA8MR3nZsXd1p2U6Q" { + t.Error("Address decoding error") + } + // Testnet + addr, err = DecodeAddress("tmUFCqhXFnCraZJBkP4TsD5iYArcSWSmgkT", &chaincfg.TestNet3Params) + if err != nil { + t.Error(err) + } + if addr.String() != "tmUFCqhXFnCraZJBkP4TsD5iYArcSWSmgkT" { + t.Error("Address decoding error") + } +} + +var dataElement = []byte{203, 72, 18, 50, 41, 156, 213, 116, 49, 81, 172, 75, 45, 99, 174, 25, 142, 123, 176, 169} + +// Second address of https://github.com/Bitcoin-UAHF/spec/blob/master/cashaddr.md#examples-of-address-translation +func TestAddressPubKeyHash_EncodeAddress(t *testing.T) { + // Mainnet + addr, err := NewAddressPubKeyHash(dataElement, &chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + if addr.String() != "t1cQTWs2rPYM5R3zJiLA8MR3nZsXd1p2U6Q" { + t.Error("Address decoding error") + } + // Testnet + addr, err = NewAddressPubKeyHash(dataElement, &chaincfg.TestNet3Params) + if err != nil { + t.Error(err) + } + if addr.String() != "tmUFCqhXFnCraZJBkP4TsD5iYArcSWSmgkT" { + t.Error("Address decoding error") + } +} + +var dataElement2 = []byte{118, 160, 64, 83, 189, 160, 168, 139, 218, 81, 119, 184, 106, 21, 195, 178, 159, 85, 152, 115} + +// 4th address of https://github.com/Bitcoin-UAHF/spec/blob/master/cashaddr.md#examples-of-address-translation +func TestCashWitnessScriptHash_EncodeAddress(t *testing.T) { + // Mainnet + addr, err := NewAddressScriptHashFromHash(dataElement2, &chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + if addr.String() != "t3VNrdy8EjPaEJv2DnRN414eVwVQR9M8iS3" { + t.Error("Address decoding error") + } + // Testnet + addr, err = NewAddressScriptHashFromHash(dataElement2, &chaincfg.TestNet3Params) + if err != nil { + t.Error(err) + } + if addr.String() != "t2HN3geENbrBbrcbxiAN6Ygq93ydayzuTqB" { + t.Error("Address decoding error") + } +} + +func TestScriptParsing(t *testing.T) { + addr, err := DecodeAddress("t3VNrdy8EjPaEJv2DnRN414eVwVQR9M8iS3", &chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + script, err := PayToAddrScript(addr) + if err != nil { + t.Error(err) + } + addr2, err := ExtractPkScriptAddrs(script, &chaincfg.MainNetParams) + if err != nil { + t.Error(err) + } + if addr.String() != addr2.String() { + t.Error("Failed to convert script back into address") + } +} diff --git a/zcash/exchange_rates.go b/zcash/exchange_rates.go new file mode 100644 index 0000000..cd19504 --- /dev/null +++ b/zcash/exchange_rates.go @@ -0,0 +1,334 @@ +package zcash + +import ( + "encoding/json" + "errors" + "github.com/OpenBazaar/multiwallet/util" + "net/http" + "reflect" + "strconv" + "sync" + "time" + + exchange "github.com/OpenBazaar/spvwallet/exchangerates" + "golang.org/x/net/proxy" +) + +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 ZcashPriceFetcher struct { + sync.Mutex + cache map[string]float64 + providers []*ExchangeRateProvider +} + +func NewZcashPriceFetcher(dialer proxy.Dialer) *ZcashPriceFetcher { + bp := exchange.NewBitcoinPriceFetcher(dialer) + z := ZcashPriceFetcher{ + 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-zec", z.cache, client, BittrexDecoder{}, bp}, + {"https://api.bitfinex.com/v1/pubticker/zecbtc", z.cache, client, BitfinexDecoder{}, bp}, + {"https://poloniex.com/public?command=returnTicker", z.cache, client, PoloniexDecoder{}, bp}, + {"https://api.kraken.com/0/public/Ticker?pair=ZECXBT", z.cache, client, KrakenDecoder{}, bp}, + } + go z.run() + return &z +} + +func (z *ZcashPriceFetcher) GetExchangeRate(currencyCode string) (float64, error) { + currencyCode = util.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 *ZcashPriceFetcher) GetLatestRate(currencyCode string) (float64, error) { + currencyCode = util.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 *ZcashPriceFetcher) GetAllRates(cacheOK bool) (map[string]float64, error) { + if !cacheOK { + err := z.fetchCurrentRates() + if err != nil { + return nil, err + } + } + z.Lock() + defer z.Unlock() + return z.cache, nil +} + +func (z *ZcashPriceFetcher) UnitsPerCoin() int { + return exchange.SatoshiPerBTC +} + +func (z *ZcashPriceFetcher) 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 *ZcashPriceFetcher) 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") + } + + zec, ok := data["ZEC"] + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed, missing 'ZEC' field") + } + val, ok := zec.(map[string]interface{}) + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed") + } + zecRate, 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 / zecRate) + } + } + 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["BCHXBT"] + 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-ZCash 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-ZCash 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-ZCash 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, ok := data["BTC_ZEC"] + if !ok { + return errors.New(reflect.TypeOf(b).Name() + ".decode: Type assertion failed") + } + 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-Zcash price data not available") + } + for k, v := range rates { + cache[k] = v * rate + } + return nil +} diff --git a/zcash/sign.go b/zcash/sign.go new file mode 100644 index 0000000..2bd11ca --- /dev/null +++ b/zcash/sign.go @@ -0,0 +1,932 @@ +package zcash + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "github.com/btcsuite/btcd/chaincfg" + "github.com/minio/blake2b-simd" + "time" + + "github.com/OpenBazaar/spvwallet" + wi "github.com/OpenBazaar/wallet-interface" + "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/btcsuite/btcwallet/wallet/txrules" + + "github.com/OpenBazaar/multiwallet/util" + zaddr "github.com/OpenBazaar/multiwallet/zcash/address" +) + +var ( + txHeaderBytes = []byte{0x04, 0x00, 0x00, 0x80} + txNVersionGroupIDBytes = []byte{0x85, 0x20, 0x2f, 0x89} + + hashPrevOutPersonalization = []byte("ZcashPrevoutHash") + hashSequencePersonalization = []byte("ZcashSequencHash") + hashOutputsPersonalization = []byte("ZcashOutputsHash") + sigHashPersonalization = []byte("ZcashSigHash") +) + +const ( + sigHashMask = 0x1f + branchID = 0x2BB40E60 +) + +func (w *ZCashWallet) buildTx(amount int64, addr btc.Address, feeLevel wi.FeeLevel, optionalOutput *wire.TxOut) (*wire.MsgTx, error) { + // Check for dust + script, err := zaddr.PayToAddrScript(addr) + if err != nil { + return nil, err + } + if txrules.IsDustAmount(btc.Amount(amount), len(script), txrules.DefaultRelayFeePerKb) { + return nil, wi.ErrorDustAmount + } + + var ( + additionalPrevScripts map[wire.OutPoint][]byte + additionalKeysByAddress map[string]*btc.WIF + inVals map[wire.OutPoint]btc.Amount + ) + + // 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) + inVals = make(map[wire.OutPoint]btc.Amount) + 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 := key.Address(w.params) + if err != nil { + continue + } + privKey, err := key.ECPrivKey() + if err != nil { + continue + } + wif, _ := btc.NewWIF(privKey, w.params, true) + additionalKeysByAddress[addr.EncodeAddress()] = wif + inVals[*outpoint] = c.Value() + } + 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 := zaddr.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) { + addrStr := addr.EncodeAddress() + wif := additionalKeysByAddress[addrStr] + return wif.PrivKey, wif.CompressPubKey, nil + }) + for i, txIn := range authoredTx.Tx.TxIn { + prevOutScript := additionalPrevScripts[txIn.PreviousOutPoint] + _, addrs, _, err := txscript.ExtractPkScriptAddrs(prevOutScript, w.params) + if err != nil { + return nil, err + } + key, _, err := getKey(addrs[0]) + if err != nil { + return nil, err + } + val := int64(inVals[txIn.PreviousOutPoint].ToUnit(btc.AmountSatoshi)) + sig, err := rawTxInSignature(authoredTx.Tx, i, prevOutScript, txscript.SigHashAll, key, val) + if err != nil { + return nil, errors.New("failed to sign transaction") + } + builder := txscript.NewScriptBuilder() + builder.AddData(sig) + builder.AddData(key.PubKey().SerializeCompressed()) + script, err := builder.Script() + if err != nil { + return nil, err + } + txIn.SignatureScript = script + } + return authoredTx.Tx, nil +} + +func (w *ZCashWallet) 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, inVals, additionalPrevScripts, additionalKeysByAddress := util.LoadAllInputs(tx, coinMap, w.params) + + // outputs + script, err := zaddr.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(btc.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 + }) + for i, txIn := range tx.TxIn { + prevOutScript := additionalPrevScripts[txIn.PreviousOutPoint] + _, addrs, _, err := txscript.ExtractPkScriptAddrs(prevOutScript, w.params) + if err != nil { + return nil, err + } + key, _, err := getKey(addrs[0]) + if err != nil { + return nil, err + } + sig, err := rawTxInSignature(tx, i, prevOutScript, txscript.SigHashAll, key, inVals[txIn.PreviousOutPoint]) + if err != nil { + return nil, errors.New("failed to sign transaction") + } + builder := txscript.NewScriptBuilder() + builder.AddData(sig) + builder.AddData(key.PubKey().SerializeCompressed()) + script, err := builder.Script() + if err != nil { + return nil, err + } + 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(feePerKb, estimatedSize) + for { + inputAmount, inputs, _, scripts, err := fetchInputs(targetAmount + targetFee) + if err != nil { + return nil, err + } + if inputAmount < targetAmount+targetFee { + return nil, errors.New("insufficient funds available to construct transaction") + } + + maxSignedSize := EstimateSerializeSize(len(inputs), outputs, true, P2PKH) + maxRequiredFee := txrules.FeeForSerializeSize(feePerKb, maxSignedSize) + remainingAmount := inputAmount - targetAmount + if remainingAmount < maxRequiredFee { + targetFee = maxRequiredFee + continue + } + + unsignedTransaction := &wire.MsgTx{ + Version: wire.TxVersion, + TxIn: inputs, + TxOut: outputs, + LockTime: 0, + } + changeIndex := -1 + changeAmount := inputAmount - targetAmount - maxRequiredFee + if changeAmount != 0 && !txrules.IsDustAmount(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 *ZCashWallet) 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 *ZCashWallet) 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 := zaddr.PayToAddrScript(internalAddr) + if err != nil { + return nil, err + } + + var val int64 + var inputs []*wire.TxIn + additionalPrevScripts := make(map[wire.OutPoint][]byte) + var values []int64 + for _, in := range ins { + val += in.Value + values = append(values, in.Value) + ch, err := chainhash.NewHashFromStr(hex.EncodeToString(in.OutpointHash)) + if err != nil { + return nil, err + } + script, err := zaddr.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 + } + } else { + redeemScript = &[]byte{} + } + 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, err + } + + for i, txIn := range tx.TxIn { + sig, err := rawTxInSignature(tx, i, *redeemScript, txscript.SigHashAll, privKey, values[i]) + if err != nil { + return nil, errors.New("failed to sign transaction") + } + builder := txscript.NewScriptBuilder() + builder.AddOp(txscript.OP_0) + builder.AddData(sig) + if redeemScript != nil { + builder.AddData(*redeemScript) + } + script, err := builder.Script() + if err != nil { + return nil, err + } + txIn.SignatureScript = script + } + + // broadcast + txid, err := w.Broadcast(tx) + if err != nil { + return nil, err + } + return chainhash.NewHashFromStr(txid) +} + +func (w *ZCashWallet) 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) + var values []int64 + for _, in := range ins { + ch, err := chainhash.NewHashFromStr(hex.EncodeToString(in.OutpointHash)) + if err != nil { + return sigs, err + } + values = append(values, in.Value) + outpoint := wire.NewOutPoint(ch, in.OutpointIndex) + input := wire.NewTxIn(outpoint, []byte{}, [][]byte{}) + tx.TxIn = append(tx.TxIn, input) + } + for _, out := range outs { + scriptPubkey, err := zaddr.PayToAddrScript(out.Address) + if err != nil { + return sigs, err + } + output := wire.NewTxOut(out.Value, scriptPubkey) + tx.TxOut = append(tx.TxOut, output) + } + + // Subtract fee + estimatedSize := EstimateSerializeSize(len(ins), tx.TxOut, false, P2SH_2of3_Multisig) + 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 + } + + for i := range tx.TxIn { + sig, err := rawTxInSignature(tx, i, redeemScript, txscript.SigHashAll, signingKey, values[i]) + if err != nil { + continue + } + bs := wi.Signature{InputIndex: uint32(i), Signature: sig} + sigs = append(sigs, bs) + } + return sigs, nil +} + +func (w *ZCashWallet) 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 := zaddr.PayToAddrScript(out.Address) + if err != nil { + return nil, err + } + output := wire.NewTxOut(out.Value, scriptPubkey) + tx.TxOut = append(tx.TxOut, output) + } + + // Subtract fee + estimatedSize := EstimateSerializeSize(len(ins), tx.TxOut, false, P2SH_2of3_Multisig) + 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) + + 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 + } + } + + builder := txscript.NewScriptBuilder() + builder.AddOp(txscript.OP_0) + builder.AddData(sig1) + builder.AddData(sig2) + builder.AddData(redeemScript) + scriptSig, err := builder.Script() + if err != nil { + return nil, err + } + input.SignatureScript = scriptSig + } + // broadcast + if broadcast { + if _, err := w.Broadcast(tx); err != nil { + return nil, err + } + } + return serializeVersion4Transaction(tx, 0) +} + +func (w *ZCashWallet) 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() + builder.AddInt64(int64(threshold)) + for _, key := range ecKeys { + builder.AddData(key.SerializeCompressed()) + } + builder.AddInt64(int64(len(ecKeys))) + builder.AddOp(txscript.OP_CHECKMULTISIG) + + redeemScript, err = builder.Script() + if err != nil { + return nil, nil, err + } + + addr, err = zaddr.NewAddressScriptHash(redeemScript, w.params) + if err != nil { + return nil, nil, err + } + return addr, redeemScript, nil +} + +func (w *ZCashWallet) 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 := zaddr.DecodeAddress("t1hASvMj8e6TXWryuB3L5TKXJB7XfNioZP3", &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 +} + +// rawTxInSignature returns the serialized ECDSA signature for the input idx of +// the given transaction, with hashType appended to it. +func rawTxInSignature(tx *wire.MsgTx, idx int, prevScriptBytes []byte, + hashType txscript.SigHashType, key *btcec.PrivateKey, amt int64) ([]byte, error) { + + hash, err := calcSignatureHash(prevScriptBytes, hashType, tx, idx, amt, 0) + if err != nil { + return nil, err + } + signature, err := key.Sign(hash) + if err != nil { + return nil, fmt.Errorf("cannot sign tx input: %s", err) + } + + return append(signature.Serialize(), byte(hashType)), nil +} + +func calcSignatureHash(prevScriptBytes []byte, hashType txscript.SigHashType, tx *wire.MsgTx, idx int, amt int64, expiry uint32) ([]byte, error) { + + // As a sanity check, ensure the passed input index for the transaction + // is valid. + if idx > len(tx.TxIn)-1 { + return nil, fmt.Errorf("idx %d but %d txins", idx, len(tx.TxIn)) + } + + // We'll utilize this buffer throughout to incrementally calculate + // the signature hash for this transaction. + var sigHash bytes.Buffer + + // Write header + _, err := sigHash.Write(txHeaderBytes) + if err != nil { + return nil, err + } + + // Write group ID + _, err = sigHash.Write(txNVersionGroupIDBytes) + if err != nil { + return nil, err + } + + // Next write out the possibly pre-calculated hashes for the sequence + // numbers of all inputs, and the hashes of the previous outs for all + // outputs. + var zeroHash chainhash.Hash + + // If anyone can pay isn't active, then we can use the cached + // hashPrevOuts, otherwise we just write zeroes for the prev outs. + if hashType&txscript.SigHashAnyOneCanPay == 0 { + sigHash.Write(calcHashPrevOuts(tx)) + } else { + sigHash.Write(zeroHash[:]) + } + + // If the sighash isn't anyone can pay, single, or none, the use the + // cached hash sequences, otherwise write all zeroes for the + // hashSequence. + if hashType&txscript.SigHashAnyOneCanPay == 0 && + hashType&sigHashMask != txscript.SigHashSingle && + hashType&sigHashMask != txscript.SigHashNone { + sigHash.Write(calcHashSequence(tx)) + } else { + sigHash.Write(zeroHash[:]) + } + + // If the current signature mode isn't single, or none, then we can + // re-use the pre-generated hashoutputs sighash fragment. Otherwise, + // we'll serialize and add only the target output index to the signature + // pre-image. + if hashType&sigHashMask != txscript.SigHashSingle && + hashType&sigHashMask != txscript.SigHashNone { + sigHash.Write(calcHashOutputs(tx)) + } else if hashType&sigHashMask == txscript.SigHashSingle && idx < len(tx.TxOut) { + var b bytes.Buffer + wire.WriteTxOut(&b, 0, 0, tx.TxOut[idx]) + sigHash.Write(chainhash.DoubleHashB(b.Bytes())) + } else { + sigHash.Write(zeroHash[:]) + } + + // Write hash JoinSplits + sigHash.Write(make([]byte, 32)) + + // Write hash ShieldedSpends + sigHash.Write(make([]byte, 32)) + + // Write hash ShieldedOutputs + sigHash.Write(make([]byte, 32)) + + // Write out the transaction's locktime, and the sig hash + // type. + var bLockTime [4]byte + binary.LittleEndian.PutUint32(bLockTime[:], tx.LockTime) + sigHash.Write(bLockTime[:]) + + // Write expiry + var bExpiryTime [4]byte + binary.LittleEndian.PutUint32(bExpiryTime[:], expiry) + sigHash.Write(bExpiryTime[:]) + + // Write valueblance + sigHash.Write(make([]byte, 8)) + + // Write the hash type + var bHashType [4]byte + binary.LittleEndian.PutUint32(bHashType[:], uint32(hashType)) + sigHash.Write(bHashType[:]) + + // Next, write the outpoint being spent. + sigHash.Write(tx.TxIn[idx].PreviousOutPoint.Hash[:]) + var bIndex [4]byte + binary.LittleEndian.PutUint32(bIndex[:], tx.TxIn[idx].PreviousOutPoint.Index) + sigHash.Write(bIndex[:]) + + // Write the previous script bytes + wire.WriteVarBytes(&sigHash, 0, prevScriptBytes) + + // Next, add the input amount, and sequence number of the input being + // signed. + var bAmount [8]byte + binary.LittleEndian.PutUint64(bAmount[:], uint64(amt)) + sigHash.Write(bAmount[:]) + var bSequence [4]byte + binary.LittleEndian.PutUint32(bSequence[:], tx.TxIn[idx].Sequence) + sigHash.Write(bSequence[:]) + + leBranchID := make([]byte, 4) + binary.LittleEndian.PutUint32(leBranchID, branchID) + bl, _ := blake2b.New(&blake2b.Config{ + Size: 32, + Person: append(sigHashPersonalization, leBranchID...), + }) + bl.Write(sigHash.Bytes()) + h := bl.Sum(nil) + return h[:], nil +} + +// serializeVersion4Transaction serializes a wire.MsgTx into the zcash version four +// wire transaction format. +func serializeVersion4Transaction(tx *wire.MsgTx, expiryHeight uint32) ([]byte, error) { + var buf bytes.Buffer + + // Write header + _, err := buf.Write(txHeaderBytes) + if err != nil { + return nil, err + } + + // Write group ID + _, err = buf.Write(txNVersionGroupIDBytes) + if err != nil { + return nil, err + } + + // Write varint input count + count := uint64(len(tx.TxIn)) + err = wire.WriteVarInt(&buf, wire.ProtocolVersion, count) + if err != nil { + return nil, err + } + + // Write inputs + for _, ti := range tx.TxIn { + // Write outpoint hash + _, err := buf.Write(ti.PreviousOutPoint.Hash[:]) + if err != nil { + return nil, err + } + // Write outpoint index + index := make([]byte, 4) + binary.LittleEndian.PutUint32(index, ti.PreviousOutPoint.Index) + _, err = buf.Write(index) + if err != nil { + return nil, err + } + // Write sigscript + err = wire.WriteVarBytes(&buf, wire.ProtocolVersion, ti.SignatureScript) + if err != nil { + return nil, err + } + // Write sequence + sequence := make([]byte, 4) + binary.LittleEndian.PutUint32(sequence, ti.Sequence) + _, err = buf.Write(sequence) + if err != nil { + return nil, err + } + } + // Write varint output count + count = uint64(len(tx.TxOut)) + err = wire.WriteVarInt(&buf, wire.ProtocolVersion, count) + if err != nil { + return nil, err + } + // Write outputs + for _, to := range tx.TxOut { + // Write value + val := make([]byte, 8) + binary.LittleEndian.PutUint64(val, uint64(to.Value)) + _, err = buf.Write(val) + if err != nil { + return nil, err + } + // Write pkScript + err = wire.WriteVarBytes(&buf, wire.ProtocolVersion, to.PkScript) + if err != nil { + return nil, err + } + } + // Write nLocktime + nLockTime := make([]byte, 4) + binary.LittleEndian.PutUint32(nLockTime, tx.LockTime) + _, err = buf.Write(nLockTime) + if err != nil { + return nil, err + } + + // Write nExpiryHeight + expiry := make([]byte, 4) + binary.LittleEndian.PutUint32(expiry, expiryHeight) + _, err = buf.Write(expiry) + if err != nil { + return nil, err + } + + // Write nil value balance + _, err = buf.Write(make([]byte, 8)) + if err != nil { + return nil, err + } + + // Write nil value vShieldedSpend + _, err = buf.Write(make([]byte, 1)) + if err != nil { + return nil, err + } + + // Write nil value vShieldedOutput + _, err = buf.Write(make([]byte, 1)) + if err != nil { + return nil, err + } + + // Write nil value vJoinSplit + _, err = buf.Write(make([]byte, 1)) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func calcHashPrevOuts(tx *wire.MsgTx) []byte { + var b bytes.Buffer + for _, in := range tx.TxIn { + // First write out the 32-byte transaction ID one of whose + // outputs are being referenced by this input. + b.Write(in.PreviousOutPoint.Hash[:]) + + // Next, we'll encode the index of the referenced output as a + // little endian integer. + var buf [4]byte + binary.LittleEndian.PutUint32(buf[:], in.PreviousOutPoint.Index) + b.Write(buf[:]) + } + bl, _ := blake2b.New(&blake2b.Config{ + Size: 32, + Person: hashPrevOutPersonalization, + }) + bl.Write(b.Bytes()) + h := bl.Sum(nil) + return h[:] +} + +func calcHashSequence(tx *wire.MsgTx) []byte { + var b bytes.Buffer + for _, in := range tx.TxIn { + var buf [4]byte + binary.LittleEndian.PutUint32(buf[:], in.Sequence) + b.Write(buf[:]) + } + bl, _ := blake2b.New(&blake2b.Config{ + Size: 32, + Person: hashSequencePersonalization, + }) + bl.Write(b.Bytes()) + h := bl.Sum(nil) + return h[:] +} + +func calcHashOutputs(tx *wire.MsgTx) []byte { + var b bytes.Buffer + for _, out := range tx.TxOut { + wire.WriteTxOut(&b, 0, 0, out) + } + bl, _ := blake2b.New(&blake2b.Config{ + Size: 32, + Person: hashOutputsPersonalization, + }) + bl.Write(b.Bytes()) + h := bl.Sum(nil) + return h[:] +} diff --git a/zcash/sign_test.go b/zcash/sign_test.go new file mode 100644 index 0000000..ffc573e --- /dev/null +++ b/zcash/sign_test.go @@ -0,0 +1,717 @@ +package zcash + +import ( + "bytes" + "encoding/hex" + "os" + "testing" + "time" + + "github.com/OpenBazaar/multiwallet/cache" + "github.com/OpenBazaar/multiwallet/datastore" + "github.com/OpenBazaar/multiwallet/keys" + "github.com/OpenBazaar/multiwallet/model/mock" + "github.com/OpenBazaar/multiwallet/service" + "github.com/OpenBazaar/multiwallet/util" + zaddr "github.com/OpenBazaar/multiwallet/zcash/address" + "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" +) + +type FeeResponse struct { + Priority int `json:"priority"` + Normal int `json:"normal"` + Economic int `json:"economic"` +} + +func newMockWallet() (*ZCashWallet, error) { + mockDb := datastore.NewMockMultiwalletDatastore() + + db, err := mockDb.GetDatastoreForWallet(wallet.Zcash) + if err != nil { + return nil, err + } + params := &chaincfg.MainNetParams + + seed, err := hex.DecodeString("16c034c59522326867593487c03a8f9615fb248406dd0d4ffb3a6b976a248403") + if err != nil { + return nil, err + } + master, err := hdkeychain.NewMaster(seed, params) + if err != nil { + return nil, err + } + km, err := keys.NewKeyManager(db.Keys(), params, master, wallet.Zcash, zcashAddress) + if err != nil { + return nil, err + } + + fp := util.NewFeeProvider(2000, 300, 200, 100, nil) + + bw := &ZCashWallet{ + params: params, + km: km, + db: db, + fp: fp, + } + cli := mock.NewMockApiClient(bw.AddressToScript) + ws, err := service.NewWalletService(db, km, cli, params, wallet.Zcash, cache.NewMockCacher()) + if err != nil { + return nil, err + } + bw.client = cli + bw.ws = ws + return bw, nil +} + +func TestWalletService_VerifyWatchScriptFilter(t *testing.T) { + // Verify that AddWatchedAddress should never add a script which already represents a key from its own wallet + w, err := newMockWallet() + if err != nil { + t.Fatal(err) + } + keys := w.km.GetKeys() + + addr, err := w.km.KeyToAddress(keys[0]) + if err != nil { + t.Fatal(err) + } + err = w.AddWatchedAddresses(addr) + if err != nil { + t.Fatal(err) + } + + watchScripts, err := w.db.WatchedScripts().GetAll() + if err != nil { + t.Fatal(err) + } + + if len(watchScripts) != 0 { + t.Error("Put watched scripts fails on key manager owned key") + } +} + +func TestWalletService_VerifyWatchScriptPut(t *testing.T) { + // Verify that AddWatchedAddress should add a script which does not represent a key from its own wallet + w, err := newMockWallet() + if err != nil { + t.Fatal(err) + } + + addr, err := w.DecodeAddress("t1aZvxRLCGVeMPFXvqfnBgHVEbi4c6g8MVa") + if err != nil { + t.Fatal(err) + } + + err = w.AddWatchedAddresses(addr) + if err != nil { + t.Fatal(err) + } + + watchScripts, err := w.db.WatchedScripts().GetAll() + if err != nil { + t.Fatal(err) + } + + if len(watchScripts) == 0 { + t.Error("Put watched scripts fails on non-key manager owned key") + } +} + +func waitForTxnSync(t *testing.T, txnStore wallet.Txns) { + // Look for a known txn, this sucks a bit. It would be better to check if the + // number of stored txns matched the expected, but not all the mock + // transactions are relevant, so the numbers don't add up. + // Even better would be for the wallet to signal that the initial sync was + // done. + lastTxn := mock.MockTransactions[len(mock.MockTransactions)-2] + txHash, err := chainhash.NewHashFromStr(lastTxn.Txid) + if err != nil { + t.Fatal(err) + } + + for i := 0; i < 100; i++ { + if _, err := txnStore.Get(*txHash); err == nil { + return + } + time.Sleep(100 * time.Millisecond) + } + t.Fatal("timeout waiting for wallet to sync transactions") +} + +func TestZCashWallet_buildTx(t *testing.T) { + w, err := newMockWallet() + w.ws.Start() + time.Sleep(time.Second / 2) + if err != nil { + t.Error(err) + } + addr, err := w.DecodeAddress("t1hASvMj8e6TXWryuB3L5TKXJB7XfNioZP3") + if err != nil { + t.Error(err) + } + // Test build normal tx + tx, err := w.buildTx(1500000, addr, wallet.NORMAL, nil) + if err != nil { + w.DumpTables(os.Stdout) + t.Error(err) + return + } + if !containsOutput(tx, addr) { + t.Error("Built tx does not contain the requested output") + } + if !validInputs(tx, w.db) { + t.Error("Built tx does not contain valid inputs") + } + if !validChangeAddress(tx, w.db, w.params) { + t.Error("Built tx does not contain a valid change output") + } + + // Insuffient funds + _, err = w.buildTx(1000000000, addr, wallet.NORMAL, nil) + if err != wallet.ErrorInsuffientFunds { + t.Error("Failed to throw insuffient funds error") + } + + // Dust + _, err = w.buildTx(1, addr, wallet.NORMAL, nil) + if err != wallet.ErrorDustAmount { + t.Error("Failed to throw dust error") + } +} + +func TestZCashWallet_buildSpendAllTx(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + w.ws.Start() + time.Sleep(time.Second / 2) + + waitForTxnSync(t, w.db.Txns()) + addr, err := w.DecodeAddress("t1hASvMj8e6TXWryuB3L5TKXJB7XfNioZP3") + if err != nil { + t.Error(err) + } + + // Test build spendAll tx + tx, err := w.buildSpendAllTx(addr, wallet.NORMAL) + if err != nil { + t.Error(err) + } + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Fatal(err) + } + spendableUtxos := 0 + for _, u := range utxos { + if !u.WatchOnly { + spendableUtxos++ + } + } + if len(tx.TxIn) != spendableUtxos { + t.Error("Built tx does not spend all available utxos") + } + if !containsOutput(tx, addr) { + t.Error("Built tx does not contain the requested output") + } + if !validInputs(tx, w.db) { + t.Error("Built tx does not contain valid inputs") + } + if len(tx.TxOut) != 1 { + t.Error("Built tx should only have one output") + } +} + +func containsOutput(tx *wire.MsgTx, addr btcutil.Address) bool { + for _, o := range tx.TxOut { + script, _ := zaddr.PayToAddrScript(addr) + if bytes.Equal(script, o.PkScript) { + return true + } + } + return false +} + +func validInputs(tx *wire.MsgTx, db wallet.Datastore) bool { + utxos, _ := db.Utxos().GetAll() + uMap := make(map[wire.OutPoint]bool) + for _, u := range utxos { + uMap[u.Op] = true + } + for _, in := range tx.TxIn { + if !uMap[in.PreviousOutPoint] { + return false + } + } + return true +} + +func validChangeAddress(tx *wire.MsgTx, db wallet.Datastore, params *chaincfg.Params) bool { + for _, out := range tx.TxOut { + _, addrs, _, err := txscript.ExtractPkScriptAddrs(out.PkScript, params) + if err != nil { + continue + } + if len(addrs) == 0 { + continue + } + _, err = db.Keys().GetPathForKey(addrs[0].ScriptAddress()) + if err == nil { + return true + } + } + return false +} + +func TestZCashWallet_GenerateMultisigScript(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + pubkey1, err := key1.ECPubKey() + if err != nil { + t.Error(err) + } + key2, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + pubkey2, err := key2.ECPubKey() + if err != nil { + t.Error(err) + } + key3, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + pubkey3, err := key3.ECPubKey() + if err != nil { + t.Error(err) + } + keys := []hdkeychain.ExtendedKey{*key1, *key2, *key3} + + // test without timeout + addr, redeemScript, err := w.generateMultisigScript(keys, 2, 0, nil) + if err != nil { + t.Error(err) + } + if addr.String() != "t3S5yuHPzajqHcaJ6WDTGAwTuK9VDvWYj7r" { + t.Error("Returned invalid address") + } + + rs := "52" + // OP_2 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey1.SerializeCompressed()) + // pubkey1 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey2.SerializeCompressed()) + // pubkey2 + "21" + // OP_PUSHDATA(33) + hex.EncodeToString(pubkey3.SerializeCompressed()) + // pubkey3 + "53" + // OP_3 + "ae" // OP_CHECKMULTISIG + rsBytes, err := hex.DecodeString(rs) + if !bytes.Equal(rsBytes, redeemScript) { + t.Error("Returned invalid redeem script") + } +} + +func TestZCashWallet_newUnsignedTransaction(t *testing.T) { + w, err := newMockWallet() + w.ws.Start() + time.Sleep(time.Second / 2) + if err != nil { + t.Error(err) + } + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + addr, err := w.DecodeAddress("t3ZZqETXWTohq3xXHxD9yzfq4UxpcACLkVc") + if err != nil { + t.Error(err) + } + + script, err := zaddr.PayToAddrScript(addr) + if err != nil { + t.Error(err) + } + out := wire.NewTxOut(10000, script) + outputs := []*wire.TxOut{out} + + changeSource := func() ([]byte, error) { + addr := w.CurrentAddress(wallet.INTERNAL) + script, err := zaddr.PayToAddrScript(addr) + if err != nil { + return []byte{}, err + } + return script, nil + } + + inputSource := func(target btcutil.Amount) (total btcutil.Amount, inputs []*wire.TxIn, inputValues []btcutil.Amount, scripts [][]byte, err error) { + total += btcutil.Amount(utxos[0].Value) + in := wire.NewTxIn(&utxos[0].Op, []byte{}, [][]byte{}) + in.Sequence = 0 // Opt-in RBF so we can bump fees + inputs = append(inputs, in) + return total, inputs, inputValues, scripts, nil + } + + // Regular transaction + authoredTx, err := newUnsignedTransaction(outputs, btcutil.Amount(1000), inputSource, changeSource) + if err != nil { + t.Error(err) + } + if len(authoredTx.Tx.TxOut) != 2 { + t.Error("Returned incorrect number of outputs") + } + if len(authoredTx.Tx.TxIn) != 1 { + t.Error("Returned incorrect number of inputs") + } + + // Insufficient funds + outputs[0].Value = 1000000000 + _, err = newUnsignedTransaction(outputs, btcutil.Amount(1000), inputSource, changeSource) + if err == nil { + t.Error("Failed to return insuffient funds error") + } +} + +func TestZCashWallet_CreateMultisigSignature(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + ins, outs, redeemScript, err := buildTxData(w) + if err != nil { + t.Error(err) + } + + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + sigs, err := w.CreateMultisigSignature(ins, outs, key1, redeemScript, 50) + if err != nil { + t.Error(err) + } + if len(sigs) != 2 { + t.Error(err) + } + for _, sig := range sigs { + if len(sig.Signature) == 0 { + t.Error("Returned empty signature") + } + } +} + +func buildTxData(w *ZCashWallet) ([]wallet.TransactionInput, []wallet.TransactionOutput, []byte, error) { + redeemScript := "522103c157f2a7c178430972263232c9306110090c50b44d4e906ecd6d377eec89a53c210205b02b9dbe570f36d1c12e3100e55586b2b9dc61d6778c1d24a8eaca03625e7e21030c83b025cd6bdd8c06e93a2b953b821b4a8c29da211335048d7dc3389706d7e853ae" + redeemScriptBytes, err := hex.DecodeString(redeemScript) + if err != nil { + return nil, nil, nil, err + } + h1, err := hex.DecodeString("1a20f4299b4fa1f209428dace31ebf4f23f13abd8ed669cebede118343a6ae05") + if err != nil { + return nil, nil, nil, err + } + in1 := wallet.TransactionInput{ + OutpointHash: h1, + OutpointIndex: 1, + } + h2, err := hex.DecodeString("458d88b4ae9eb4a347f2e7f5592f1da3b9ddf7d40f307f6e5d7bc107a9b3e90e") + if err != nil { + return nil, nil, nil, err + } + in2 := wallet.TransactionInput{ + OutpointHash: h2, + OutpointIndex: 0, + } + addr, err := w.DecodeAddress("t3ZZqETXWTohq3xXHxD9yzfq4UxpcACLkVc") + if err != nil { + return nil, nil, nil, err + } + + out := wallet.TransactionOutput{ + Value: 20000, + Address: addr, + } + return []wallet.TransactionInput{in1, in2}, []wallet.TransactionOutput{out}, redeemScriptBytes, nil +} + +func TestZCashWallet_Multisign(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Error(err) + } + ins, outs, redeemScript, err := buildTxData(w) + if err != nil { + t.Error(err) + } + + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + key2, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + sigs1, err := w.CreateMultisigSignature(ins, outs, key1, redeemScript, 50) + if err != nil { + t.Error(err) + } + if len(sigs1) != 2 { + t.Error(err) + } + sigs2, err := w.CreateMultisigSignature(ins, outs, key2, redeemScript, 50) + if err != nil { + t.Error(err) + } + if len(sigs2) != 2 { + t.Error(err) + } + _, err = w.Multisign(ins, outs, sigs1, sigs2, redeemScript, 50, false) + if err != nil { + t.Error(err) + } +} + +func TestZCashWallet_bumpFee(t *testing.T) { + w, err := newMockWallet() + if err != nil { + t.Fatal(err) + } + + w.ws.Start() + time.Sleep(time.Second / 2) + txns, err := w.db.Txns().GetAll(false) + if err != nil { + t.Fatal(err) + } + + ch, err := chainhash.NewHashFromStr(txns[2].Txid) + if err != nil { + t.Fatal(err) + } + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Fatal(err) + } + for _, u := range utxos { + if u.Op.Hash.IsEqual(ch) { + u.AtHeight = 0 + if err := w.db.Utxos().Put(u); err != nil { + t.Fatal(err) + } + } + } + + w.db.Txns().UpdateHeight(*ch, 0, time.Now()) + + // Test unconfirmed + _, err = w.bumpFee(*ch) + if err != nil { + t.Error(err) + } + + err = w.db.Txns().UpdateHeight(*ch, 1289597, time.Now()) + if err != nil { + t.Error(err) + } + + // Test confirmed + _, err = w.bumpFee(*ch) + if err == nil { + t.Error("Should not be able to bump fee of confirmed txs") + } +} + +func TestZCashWallet_sweepAddress(t *testing.T) { + w, err := newMockWallet() + w.ws.Start() + time.Sleep(time.Second / 2) + if err != nil { + t.Error(err) + } + utxos, err := w.db.Utxos().GetAll() + if err != nil { + t.Error(err) + } + var in wallet.TransactionInput + var key *hdkeychain.ExtendedKey + for _, ut := range utxos { + if ut.Value > 0 && !ut.WatchOnly { + addr, err := w.ScriptToAddress(ut.ScriptPubkey) + if err != nil { + t.Error(err) + } + key, err = w.km.GetKeyForScript(addr.ScriptAddress()) + if err != nil { + t.Error(err) + } + h, err := hex.DecodeString(ut.Op.Hash.String()) + if err != nil { + t.Error(err) + } + in = wallet.TransactionInput{ + LinkedAddress: addr, + Value: ut.Value, + OutpointIndex: ut.Op.Index, + OutpointHash: h, + } + } + } + // P2PKH addr + _, err = w.sweepAddress([]wallet.TransactionInput{in}, nil, key, nil, wallet.NORMAL) + if err != nil { + t.Error(err) + return + } + + // 1 of 2 P2WSH + for _, ut := range utxos { + if ut.Value > 0 && ut.WatchOnly { + addr, err := w.ScriptToAddress(ut.ScriptPubkey) + if err != nil { + t.Error(err) + } + h, err := hex.DecodeString(ut.Op.Hash.String()) + if err != nil { + t.Error(err) + } + in = wallet.TransactionInput{ + LinkedAddress: addr, + Value: ut.Value, + OutpointIndex: ut.Op.Index, + OutpointHash: h, + } + } + } + key1, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + + key2, err := w.km.GetFreshKey(wallet.INTERNAL) + if err != nil { + t.Error(err) + } + _, redeemScript, err := w.GenerateMultisigScript([]hdkeychain.ExtendedKey{*key1, *key2}, 1, 0, nil) + if err != nil { + t.Error(err) + } + _, err = w.sweepAddress([]wallet.TransactionInput{in}, nil, key1, &redeemScript, wallet.NORMAL) + if err != nil { + t.Error(err) + } +} + +func TestZCashWallet_estimateSpendFee(t *testing.T) { + w, err := newMockWallet() + w.ws.Start() + time.Sleep(time.Second / 2) + if err != nil { + t.Error(err) + } + fee, err := w.estimateSpendFee(1000, wallet.NORMAL) + if err != nil { + t.Error(err) + } + if fee <= 0 { + t.Error("Returned incorrect fee") + } +} + +func buildTestTx() (*wire.MsgTx, []byte, error) { + expected, err := hex.DecodeString(`0400008085202f8901a8c685478265f4c14dada651969c45a65e1aeb8cd6791f2f5bb6a1d9952104d9010000006b483045022100a61e5d557568c2ddc1d9b03a7173c6ce7c996c4daecab007ac8f34bee01e6b9702204d38fdc0bcf2728a69fde78462a10fb45a9baa27873e6a5fc45fb5c76764202a01210365ffea3efa3908918a8b8627724af852fc9b86d7375b103ab0543cf418bcaa7ffeffffff02005a6202000000001976a9148132712c3ff19f3a151234616777420a6d7ef22688ac8b959800000000001976a9145453e4698f02a38abdaa521cd1ff2dee6fac187188ac29b0040048b004000000000000000000000000`) + if err != nil { + return nil, nil, err + } + + tx := wire.NewMsgTx(1) + + inHash, err := hex.DecodeString("a8c685478265f4c14dada651969c45a65e1aeb8cd6791f2f5bb6a1d9952104d9") + if err != nil { + return nil, nil, err + } + prevHash, err := chainhash.NewHash(inHash) + if err != nil { + return nil, nil, err + } + op := wire.NewOutPoint(prevHash, 1) + + scriptSig, err := hex.DecodeString("483045022100a61e5d557568c2ddc1d9b03a7173c6ce7c996c4daecab007ac8f34bee01e6b9702204d38fdc0bcf2728a69fde78462a10fb45a9baa27873e6a5fc45fb5c76764202a01210365ffea3efa3908918a8b8627724af852fc9b86d7375b103ab0543cf418bcaa7f") + if err != nil { + return nil, nil, err + } + txIn := wire.NewTxIn(op, scriptSig, nil) + txIn.Sequence = 4294967294 + + tx.TxIn = []*wire.TxIn{txIn} + + pkScirpt0, err := hex.DecodeString("76a9148132712c3ff19f3a151234616777420a6d7ef22688ac") + if err != nil { + return nil, nil, err + } + out0 := wire.NewTxOut(40000000, pkScirpt0) + + pkScirpt1, err := hex.DecodeString("76a9145453e4698f02a38abdaa521cd1ff2dee6fac187188ac") + if err != nil { + return nil, nil, err + } + out1 := wire.NewTxOut(9999755, pkScirpt1) + tx.TxOut = []*wire.TxOut{out0, out1} + + tx.LockTime = 307241 + return tx, expected, nil +} + +func TestSerializeVersion4Transaction(t *testing.T) { + tx, expected, err := buildTestTx() + if err != nil { + t.Fatal(err) + } + + serialized, err := serializeVersion4Transaction(tx, 307272) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(serialized, expected) { + t.Fatal("Failed to serialize transaction correctly") + } +} + +func TestCalcSignatureHash(t *testing.T) { + tx, _, err := buildTestTx() + if err != nil { + t.Fatal(err) + } + + prevScript, err := hex.DecodeString("76a914507173527b4c3318a2aecd793bf1cfed705950cf88ac") + if err != nil { + t.Fatal(err) + } + sigHash, err := calcSignatureHash(prevScript, txscript.SigHashAll, tx, 0, 50000000, 307272) + if err != nil { + t.Fatal(err) + } + expected, err := hex.DecodeString("8df91420215909927be677a978c36b528e1e7b4ba343acefdd259fe57f3f1f85") + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(sigHash, expected) { + t.Fatal("Failed to calculate correct sig hash") + } +} diff --git a/zcash/txsizes.go b/zcash/txsizes.go new file mode 100644 index 0000000..c66fe0b --- /dev/null +++ b/zcash/txsizes.go @@ -0,0 +1,249 @@ +package zcash + +// 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/zcash/txsizes_test.go b/zcash/txsizes_test.go new file mode 100644 index 0000000..56843c0 --- /dev/null +++ b/zcash/txsizes_test.go @@ -0,0 +1,84 @@ +package zcash + +// 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 ( + "bytes" + "encoding/hex" + "github.com/btcsuite/btcd/wire" + "testing" +) + +const ( + p2pkhScriptSize = P2PKHPkScriptSize + p2shScriptSize = 23 +) + +func makeInts(value int, n int) []int { + v := make([]int, n) + for i := range v { + v[i] = value + } + return v +} + +func TestEstimateSerializeSize(t *testing.T) { + tests := []struct { + InputCount int + OutputScriptLengths []int + AddChangeOutput bool + ExpectedSizeEstimate int + }{ + 0: {1, []int{}, false, 161}, + 1: {1, []int{p2pkhScriptSize}, false, 195}, + 2: {1, []int{}, true, 195}, + 3: {1, []int{p2pkhScriptSize}, true, 229}, + 4: {1, []int{p2shScriptSize}, false, 193}, + 5: {1, []int{p2shScriptSize}, true, 227}, + + 6: {2, []int{}, false, 310}, + 7: {2, []int{p2pkhScriptSize}, false, 344}, + 8: {2, []int{}, true, 344}, + 9: {2, []int{p2pkhScriptSize}, true, 378}, + 10: {2, []int{p2shScriptSize}, false, 342}, + 11: {2, []int{p2shScriptSize}, true, 376}, + + // 0xfd is discriminant for 16-bit compact ints, compact int + // total size increases from 1 byte to 3. + 12: {1, makeInts(p2pkhScriptSize, 0xfc), false, 8729}, + 13: {1, makeInts(p2pkhScriptSize, 0xfd), false, 8729 + P2PKHOutputSize + 2}, + 14: {1, makeInts(p2pkhScriptSize, 0xfc), true, 8729 + P2PKHOutputSize + 2}, + 15: {0xfc, []int{}, false, 37560}, + 16: {0xfd, []int{}, false, 37560 + RedeemP2PKHInputSize + 2}, + } + for i, test := range tests { + outputs := make([]*wire.TxOut, 0, len(test.OutputScriptLengths)) + for _, l := range test.OutputScriptLengths { + outputs = append(outputs, &wire.TxOut{PkScript: make([]byte, l)}) + } + actualEstimate := EstimateSerializeSize(test.InputCount, outputs, test.AddChangeOutput, P2PKH) + if actualEstimate != test.ExpectedSizeEstimate { + t.Errorf("Test %d: Got %v: Expected %v", i, actualEstimate, test.ExpectedSizeEstimate) + } + } +} + +func TestSumOutputSerializeSizes(t *testing.T) { + testTx := "0100000001066b78efa7d66d271cae6d6eb799e1d10953fb1a4a760226cc93186d52b55613010000006a47304402204e6c32cc214c496546c3277191ca734494fe49fed0af1d800db92fed2021e61802206a14d063b67f2f1c8fc18f9e9a5963fe33e18c549e56e3045e88b4fc6219be11012103f72d0a11727219bff66b8838c3c5e1c74a5257a325b0c84247bd10bdb9069e88ffffffff0200c2eb0b000000001976a914426e80ad778792e3e19c20977fb93ec0591e1a3988ac35b7cb59000000001976a914e5b6dc0b297acdd99d1a89937474df77db5743c788ac00000000" + txBytes, err := hex.DecodeString(testTx) + if err != nil { + t.Error(err) + return + } + r := bytes.NewReader(txBytes) + msgTx := wire.NewMsgTx(1) + msgTx.BtcDecode(r, 1, wire.WitnessEncoding) + if SumOutputSerializeSizes(msgTx.TxOut) != 68 { + t.Error("SumOutputSerializeSizes returned incorrect value") + } + +} diff --git a/zcash/wallet.go b/zcash/wallet.go new file mode 100644 index 0000000..73af2fb --- /dev/null +++ b/zcash/wallet.go @@ -0,0 +1,498 @@ +package zcash + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "time" + + "github.com/OpenBazaar/multiwallet/cache" + "github.com/OpenBazaar/multiwallet/client" + "github.com/OpenBazaar/multiwallet/config" + "github.com/OpenBazaar/multiwallet/keys" + "github.com/OpenBazaar/multiwallet/model" + "github.com/OpenBazaar/multiwallet/service" + "github.com/OpenBazaar/multiwallet/util" + zaddr "github.com/OpenBazaar/multiwallet/zcash/address" + 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/btcsuite/btcwallet/wallet/txrules" + logging "github.com/op/go-logging" + "github.com/tyler-smith/go-bip39" + "golang.org/x/net/proxy" +) + +type ZCashWallet 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(&ZCashWallet{}) + +func NewZCashWallet(cfg config.CoinConfig, mnemonic string, params *chaincfg.Params, proxy proxy.Dialer, cache cache.Cacher, disableExchangeRates bool) (*ZCashWallet, 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.Zcash, zcashAddress) + 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.Zcash, cache) + if err != nil { + return nil, err + } + + var er wi.ExchangeRates + if !disableExchangeRates { + er = NewZcashPriceFetcher(proxy) + } + + fp := util.NewFeeProvider(cfg.MaxFee, cfg.HighFee, cfg.MediumFee, cfg.LowFee, er) + + return &ZCashWallet{ + db: cfg.DB, + km: km, + params: params, + client: c, + ws: wm, + fp: fp, + mPrivKey: mPrivKey, + mPubKey: mPubKey, + exchangeRates: er, + log: logging.MustGetLogger("zcash-wallet"), + }, nil +} + +func zcashAddress(key *hd.ExtendedKey, params *chaincfg.Params) (btcutil.Address, error) { + addr, err := key.Address(params) + if err != nil { + return nil, err + } + return zaddr.NewAddressPubKeyHash(addr.ScriptAddress(), params) +} + +func (w *ZCashWallet) Start() { + w.client.Start() + w.ws.Start() +} + +func (w *ZCashWallet) Params() *chaincfg.Params { + return w.params +} + +func (w *ZCashWallet) CurrencyCode() string { + if w.params.Name == chaincfg.MainNetParams.Name { + return "zec" + } else { + return "tzec" + } +} + +func (w *ZCashWallet) IsDust(amount int64) bool { + return txrules.IsDustAmount(btcutil.Amount(amount), 25, txrules.DefaultRelayFeePerKb) +} + +func (w *ZCashWallet) MasterPrivateKey() *hd.ExtendedKey { + return w.mPrivKey +} + +func (w *ZCashWallet) MasterPublicKey() *hd.ExtendedKey { + return w.mPubKey +} + +func (w *ZCashWallet) 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 *ZCashWallet) CurrentAddress(purpose wi.KeyPurpose) btcutil.Address { + 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) + } + return addr +} + +func (w *ZCashWallet) NewAddress(purpose wi.KeyPurpose) btcutil.Address { + 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) + } + return addr +} + +func (w *ZCashWallet) DecodeAddress(addr string) (btcutil.Address, error) { + return zaddr.DecodeAddress(addr, w.params) +} + +func (w *ZCashWallet) ScriptToAddress(script []byte) (btcutil.Address, error) { + addr, err := zaddr.ExtractPkScriptAddrs(script, w.params) + if err != nil { + return nil, err + } + return addr, nil +} + +func (w *ZCashWallet) AddressToScript(addr btcutil.Address) ([]byte, error) { + return zaddr.PayToAddrScript(addr) +} + +func (w *ZCashWallet) HasKey(addr btcutil.Address) bool { + _, err := w.km.GetKeyForScript(addr.ScriptAddress()) + if err != nil { + return false + } + return true +} + +func (w *ZCashWallet) Balance() (confirmed, unconfirmed int64) { + utxos, _ := w.db.Utxos().GetAll() + txns, _ := w.db.Txns().GetAll(false) + // Zcash transactions have additional data embedded in them + // that is not expected by the BtcDecode deserialize function. + // We strip off the extra data here so the derserialize function + // does not error. This will have no affect on the balance calculation + // as the metadata is not used in the calculation. + for i, tx := range txns { + txns[i].Bytes = trimTxForDeserialization(tx.Bytes) + } + return util.CalcBalance(utxos, txns) +} + +func (w *ZCashWallet) 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 *ZCashWallet) GetTransaction(txid chainhash.Hash) (wi.Txn, error) { + txn, err := w.db.Txns().Get(txid) + if err == nil { + tx := wire.NewMsgTx(1) + rbuf := bytes.NewReader(trimTxForDeserialization(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 := zaddr.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 *ZCashWallet) ChainTip() (uint32, chainhash.Hash) { + return w.ws.ChainTip() +} + +func (w *ZCashWallet) GetFeePerByte(feeLevel wi.FeeLevel) uint64 { + return w.fp.GetFeePerByte(feeLevel) +} + +func (w *ZCashWallet) 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 + txid, err := w.Broadcast(tx) + if err != nil { + return nil, err + } + + return chainhash.NewHashFromStr(txid) +} + +func (w *ZCashWallet) BumpFee(txid chainhash.Hash) (*chainhash.Hash, error) { + return w.bumpFee(txid) +} + +func (w *ZCashWallet) EstimateFee(ins []wi.TransactionInput, outs []wi.TransactionOutput, feePerByte uint64) uint64 { + tx := new(wire.MsgTx) + for _, out := range outs { + scriptPubKey, _ := zaddr.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 *ZCashWallet) EstimateSpendFee(amount int64, feeLevel wi.FeeLevel) (uint64, error) { + return w.estimateSpendFee(amount, feeLevel) +} + +func (w *ZCashWallet) 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 *ZCashWallet) 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 *ZCashWallet) 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 *ZCashWallet) 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 *ZCashWallet) 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 *ZCashWallet) 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 *ZCashWallet) AddTransactionListener(callback func(wi.TransactionCallback)) { + w.ws.AddTransactionListener(callback) +} + +func (w *ZCashWallet) ReSyncBlockchain(fromTime time.Time) { + go w.ws.UpdateState() +} + +func (w *ZCashWallet) 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 *ZCashWallet) Close() { + w.ws.Stop() + w.client.Close() +} + +func (w *ZCashWallet) ExchangeRates() wi.ExchangeRates { + return w.exchangeRates +} + +func (w *ZCashWallet) 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) + } +} + +// Build a client.Transaction so we can ingest it into the wallet service then broadcast +func (w *ZCashWallet) Broadcast(tx *wire.MsgTx) (string, error) { + txBytes, err := serializeVersion4Transaction(tx, 0) + if err != nil { + return "", err + } + cTxn := model.Transaction{ + Txid: tx.TxHash().String(), + Locktime: int(tx.LockTime), + Version: int(tx.Version), + Confirmations: 0, + Time: time.Now().Unix(), + RawBytes: txBytes, + } + 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.Zcash), + } + 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) + } + cTxn.Txid, err = w.client.Broadcast(txBytes) + if err != nil { + return "", err + } + w.ws.ProcessIncomingTransaction(cTxn) + return cTxn.Txid, nil +} + +// AssociateTransactionWithOrder used for ORDER_PAYMENT message +func (w *ZCashWallet) AssociateTransactionWithOrder(cb wi.TransactionCallback) { + w.ws.InvokeTransactionListeners(cb) +} + +func trimTxForDeserialization(txBytes []byte) []byte { + return txBytes[4 : len(txBytes)-15] +} diff --git a/zcash/wallet_test.go b/zcash/wallet_test.go new file mode 100644 index 0000000..7cb9c4f --- /dev/null +++ b/zcash/wallet_test.go @@ -0,0 +1,95 @@ +package zcash + +import ( + "github.com/OpenBazaar/multiwallet/datastore" + "github.com/OpenBazaar/wallet-interface" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "testing" + "time" +) + +func TestZCashWallet_Balance(t *testing.T) { + ds := datastore.NewMockMultiwalletDatastore() + db, err := ds.GetDatastoreForWallet(wallet.Zcash) + if err != nil { + t.Fatal(err) + } + + w := ZCashWallet{ + db: db, + } + + ch1, err := chainhash.NewHashFromStr("ccfd8d91b38e065a4d0f655fffabbdbf61666d1fdf1b54b7432c5d0ad453b76d") + if err != nil { + t.Error(err) + } + ch2, err := chainhash.NewHashFromStr("37aface44f82f6f319957b501030da2595b35d8bbc953bbe237f378c5f715bdd") + if err != nil { + t.Error(err) + } + ch3, err := chainhash.NewHashFromStr("2d08e0e877ff9d034ca272666d01626e96a0cf9e17004aafb4ae9d5aa109dd20") + if err != nil { + t.Error(err) + } + ch4, err := chainhash.NewHashFromStr("c803c8e21a464f0425fda75fb43f5a40bb6188bab9f3bfe0c597b46899e30045") + if err != nil { + t.Error(err) + } + + err = db.Utxos().Put(wallet.Utxo{ + AtHeight: 500, + Value: 1000, + Op: *wire.NewOutPoint(ch1, 0), + }) + if err != nil { + t.Fatal(err) + } + err = db.Utxos().Put(wallet.Utxo{ + AtHeight: 0, + Value: 2000, + Op: *wire.NewOutPoint(ch2, 0), + }) + if err != nil { + t.Fatal(err) + } + + // Test unconfirmed + confirmed, unconfirmed := w.Balance() + if confirmed != 1000 || unconfirmed != 2000 { + t.Error("Returned incorrect balance") + } + + // Test confirmed stxo + tx := wire.NewMsgTx(1) + op := wire.NewOutPoint(ch3, 1) + in := wire.NewTxIn(op, []byte{}, [][]byte{}) + out := wire.NewTxOut(500, []byte{0x00}) + tx.TxIn = append(tx.TxIn, in) + tx.TxOut = append(tx.TxOut, out) + buf, err := serializeVersion4Transaction(tx, 0) + if err != nil { + t.Fatal(err) + } + if err := db.Txns().Put(buf, "37aface44f82f6f319957b501030da2595b35d8bbc953bbe237f378c5f715bdd", 0, 0, time.Now(), false); err != nil { + t.Fatal(err) + } + + tx = wire.NewMsgTx(1) + op = wire.NewOutPoint(ch4, 1) + in = wire.NewTxIn(op, []byte{}, [][]byte{}) + out = wire.NewTxOut(500, []byte{0x00}) + tx.TxIn = append(tx.TxIn, in) + tx.TxOut = append(tx.TxOut, out) + buf2, err := serializeVersion4Transaction(tx, 0) + if err != nil { + t.Fatal(err) + } + if err := db.Txns().Put(buf2, "2d08e0e877ff9d034ca272666d01626e96a0cf9e17004aafb4ae9d5aa109dd20", 0, 1999, time.Now(), false); err != nil { + t.Fatal(err) + } + confirmed, unconfirmed = w.Balance() + if confirmed != 3000 || unconfirmed != 0 { + t.Error("Returned incorrect balance") + } +}