diff --git a/btc.go b/btc.go new file mode 100644 index 000000000..12f9c7d9e --- /dev/null +++ b/btc.go @@ -0,0 +1,51 @@ +package dcrlibwallet + +import ( + // "context" + // "fmt" + "os" + "path/filepath" + + "decred.org/dcrwallet/v2/errors" + + "github.com/asdine/storm" + // "github.com/asdine/storm/q" + + bolt "go.etcd.io/bbolt" + + "github.com/planetdecred/dcrlibwallet/wallets/btc" +) + +func initializeBTCWallet(rootDir, dbDriver, netType string) (*storm.DB, string, error) { + var btcDB *storm.DB + + rootDir = filepath.Join(rootDir, netType, "btc") + err := os.MkdirAll(rootDir, os.ModePerm) + if err != nil { + return btcDB, "", errors.Errorf("failed to create btc rootDir: %v", err) + } + + err = initLogRotator(filepath.Join(rootDir, logFileName)) + if err != nil { + return btcDB, "", errors.Errorf("failed to init btc logRotator: %v", err.Error()) + } + + btcDB, err = storm.Open(filepath.Join(rootDir, walletsDbName)) + if err != nil { + log.Errorf("Error opening btc wallets database: %s", err.Error()) + if err == bolt.ErrTimeout { + // timeout error occurs if storm fails to acquire a lock on the database file + return btcDB, "", errors.E(ErrWalletDatabaseInUse) + } + return btcDB, "", errors.Errorf("error opening btc wallets database: %s", err.Error()) + } + + // init database for saving/reading wallet objects + err = btcDB.Init(&btc.Wallet{}) + if err != nil { + log.Errorf("Error initializing btc wallets database: %s", err.Error()) + return btcDB, "", err + } + + return btcDB, rootDir, nil +} diff --git a/btcwallet/go.mod b/btcwallet/go.mod new file mode 100644 index 000000000..b27ab0f4d --- /dev/null +++ b/btcwallet/go.mod @@ -0,0 +1,24 @@ +module github.com/planetdecred/dcrlibwallet/btcwallet + +require ( + github.com/btcsuite/btcd v0.22.0-beta.0.20211026140004-31791ba4dc6e + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f + github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890 // note: hoists btcd's own require of btcutil + github.com/btcsuite/btcwallet v0.12.0 + github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0 // indirect + github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 // indirect + github.com/btcsuite/btcwallet/walletdb v1.4.0 + github.com/btcsuite/btcwallet/wtxmgr v1.3.0 + github.com/decred/dcrd/lru v1.1.1 // indirect + github.com/decred/slog v1.2.0 + github.com/jrick/logrotate v1.0.0 + github.com/kkdai/bstream v1.0.0 // indirect + github.com/lightninglabs/neutrino v0.13.1-0.20211214231330-53b628ce1756 + github.com/stretchr/testify v1.7.0 // indirect + go.etcd.io/bbolt v1.3.5 // indirect + golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect + golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912 // indirect + golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect +) + +go 1.16 diff --git a/btcwallet/go.sum b/btcwallet/go.sum new file mode 100644 index 000000000..a9daf456e --- /dev/null +++ b/btcwallet/go.sum @@ -0,0 +1,168 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.21.0-beta.0.20201208033208-6bd4c64a54fa/go.mod h1:Sv4JPQ3/M+teHz9Bo5jBpkNcP0x6r7rdihlNL/7tTAs= +github.com/btcsuite/btcd v0.21.0-beta.0.20210426180113-7eba688b65e5/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= +github.com/btcsuite/btcd v0.22.0-beta.0.20210803133449-f5a1fb9965e4/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= +github.com/btcsuite/btcd v0.22.0-beta.0.20211026140004-31791ba4dc6e h1:d0NkvbJGQThTkhypOdtf5Orxe2+dUHLB86pQ4EXwrTo= +github.com/btcsuite/btcd v0.22.0-beta.0.20211026140004-31791ba4dc6e/go.mod h1:3PH+KbvLFfzBTCevQenPiDedjGQGt6aa70dVjJDWGTA= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= +github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890 h1:9aGy5p7oXRUB4MCTmWm0+jzuh79GpjPIfv1leA5POD4= +github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= +github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce h1:3PRwz+js0AMMV1fHRrCdQ55akoomx4Q3ulozHC3BDDY= +github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= +github.com/btcsuite/btcwallet v0.12.0 h1:0kT0rDN8vNcAvuHp2qUS/hLsfa0VUn2dNQ2GvM9ozBA= +github.com/btcsuite/btcwallet v0.12.0/go.mod h1:f1HuBGov5+OTp40Gh1vA+tvF6d7bbuLFTceJMRB7fXw= +github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU= +github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0 h1:8pO0pvPX1rFRfRiol4oV6kX7dY5y4chPwhfVwUfvwtk= +github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0/go.mod h1:ktYuJyumYtwG+QQ832Q+kqvxWJRAei3Nqs5qhSn4nww= +github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 h1:2VsfS0sBedcM5KmDzRMT3+b6xobqWveZGvjb+jFez5w= +github.com/btcsuite/btcwallet/wallet/txrules v1.0.0/go.mod h1:UwQE78yCerZ313EXZwEiu3jNAtfXj2n2+c8RWiE/WNA= +github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs= +github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 h1:wZnOolEAeNOHzHTnznw/wQv+j35ftCIokNrnOTOU5o8= +github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs= +github.com/btcsuite/btcwallet/walletdb v1.3.4/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= +github.com/btcsuite/btcwallet/walletdb v1.3.5/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= +github.com/btcsuite/btcwallet/walletdb v1.4.0 h1:/C5JRF+dTuE2CNMCO/or5N8epsrhmSM4710uBQoYPTQ= +github.com/btcsuite/btcwallet/walletdb v1.4.0/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= +github.com/btcsuite/btcwallet/wtxmgr v1.3.0 h1:lrZaZXGJjDedYTV7s5UgU9xBe8+N+cIDW7BYwI/B8Fs= +github.com/btcsuite/btcwallet/wtxmgr v1.3.0/go.mod h1:awQsh1n/0ZrEQ+JZgWvHeo153ubzEisf/FyNtwI0dDk= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/decred/dcrd/lru v1.1.1 h1:kWFDaW0OWx6AD6Ki342c+JPmHbiVdE6rK81pT3fuo/Y= +github.com/decred/dcrd/lru v1.1.1/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/decred/slog v1.2.0 h1:soHAxV52B54Di3WtKLfPum9OFfWqwtf/ygf9njdfnPM= +github.com/decred/slog v1.2.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8= +github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= +github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= +github.com/lightninglabs/neutrino v0.12.1/go.mod h1:GlKninWpRBbL7b8G0oQ36/8downfnFwKsr0hbRA6E/E= +github.com/lightninglabs/neutrino v0.13.1-0.20211214231330-53b628ce1756 h1:lf3i1CNI5j2XhKMvQNmnr2o1DFoyoE02mynRdAt+ss0= +github.com/lightninglabs/neutrino v0.13.1-0.20211214231330-53b628ce1756/go.mod h1:GlKninWpRBbL7b8G0oQ36/8downfnFwKsr0hbRA6E/E= +github.com/lightningnetwork/lnd/clock v1.0.1 h1:QQod8+m3KgqHdvVMV+2DRNNZS1GRFir8mHZYA+Z2hFo= +github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg= +github.com/lightningnetwork/lnd/queue v1.0.1 h1:jzJKcTy3Nj5lQrooJ3aaw9Lau3I0IwvQR5sqtjdv2R0= +github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms= +github.com/lightningnetwork/lnd/ticker v1.0.0 h1:S1b60TEGoTtCe2A0yeB+ecoj/kkS4qpwh6l+AkQEZwU= +github.com/lightningnetwork/lnd/ticker v1.0.0/go.mod h1:iaLXJiVgI1sPANIF2qYYUJXjoksPNvGNYowB8aRbpX0= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912 h1:uCLL3g5wH2xjxVREVuAbP9JM5PPKjRbXKRa6IBjkzmU= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/btcwallet/wallet.go b/btcwallet/wallet.go new file mode 100644 index 000000000..54ac5d252 --- /dev/null +++ b/btcwallet/wallet.go @@ -0,0 +1,420 @@ +package btcwallet + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btclog" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/gcs" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/walletdb" + _ "github.com/btcsuite/btcwallet/walletdb/bdb" // bdb init() registers a driver + "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/decred/slog" + "github.com/jrick/logrotate/rotator" + "github.com/lightninglabs/neutrino" + "github.com/lightninglabs/neutrino/headerfs" +) + +type Wallet struct { + ID int `storm:"id,increment"` + Name string `storm:"unique"` + CreatedAt time.Time `storm:"index"` + EncryptedSeed []byte + + cl neutrinoService + neutrinoDB walletdb.DB + chainClient *chain.NeutrinoClient + dataDir string + chainParams *chaincfg.Params + loader *wallet.Loader + log slog.Logger + birthday time.Time +} + +// neutrinoService is satisfied by *neutrino.ChainService. +type neutrinoService interface { + GetBlockHash(int64) (*chainhash.Hash, error) + BestBlock() (*headerfs.BlockStamp, error) + Peers() []*neutrino.ServerPeer + GetBlockHeight(hash *chainhash.Hash) (int32, error) + GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, error) + GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, options ...neutrino.QueryOption) (*gcs.Filter, error) + GetBlock(blockHash chainhash.Hash, options ...neutrino.QueryOption) (*btcutil.Block, error) + Stop() error +} + +var _ neutrinoService = (*neutrino.ChainService)(nil) + +var ( + walletBirthday time.Time + loggingInited uint32 +) + +const ( + neutrinoDBName = "neutrino.db" + logDirName = "logs" + logFileName = "neutrino.log" +) + +func NewSpvWallet(walletName string, encryptedSeed []byte, net string, log slog.Logger) (*Wallet, error) { + chainParams, err := parseChainParams(net) + if err != nil { + return nil, err + } + + return &Wallet{ + Name: walletName, + chainParams: chainParams, + CreatedAt: time.Now(), + EncryptedSeed: encryptedSeed, + log: log, + }, nil +} + +func parseChainParams(net string) (*chaincfg.Params, error) { + switch net { + case "mainnet": + return &chaincfg.MainNetParams, nil + case "testnet3": + return &chaincfg.TestNet3Params, nil + case "regtest", "regnet", "simnet": + return &chaincfg.RegressionNetParams, nil + } + return nil, fmt.Errorf("unknown network ID %v", net) +} + +// logWriter implements an io.Writer that outputs to a rotating log file. +type logWriter struct { + *rotator.Rotator +} + +// logNeutrino initializes logging in the neutrino + wallet packages. Logging +// only has to be initialized once, so an atomic flag is used internally to +// return early on subsequent invocations. +// +// In theory, the the rotating file logger must be Close'd at some point, but +// there are concurrency issues with that since btcd and btcwallet have +// unsupervised goroutines still running after shutdown. So we leave the rotator +// running at the risk of losing some logs. +func logNeutrino(walletDir string) error { + if !atomic.CompareAndSwapUint32(&loggingInited, 0, 1) { + return nil + } + + logSpinner, err := logRotator(walletDir) + if err != nil { + return fmt.Errorf("error initializing log rotator: %w", err) + } + + backendLog := btclog.NewBackend(logWriter{logSpinner}) + + logger := func(name string, lvl btclog.Level) btclog.Logger { + l := backendLog.Logger(name) + l.SetLevel(lvl) + return l + } + + neutrino.UseLogger(logger("NTRNO", btclog.LevelDebug)) + wallet.UseLogger(logger("BTCW", btclog.LevelInfo)) + wtxmgr.UseLogger(logger("TXMGR", btclog.LevelInfo)) + chain.UseLogger(logger("CHAIN", btclog.LevelInfo)) + + return nil +} + +// logRotator initializes a rotating file logger. +func logRotator(netDir string) (*rotator.Rotator, error) { + const maxLogRolls = 8 + logDir := filepath.Join(netDir, logDirName) + if err := os.MkdirAll(logDir, 0744); err != nil { + return nil, fmt.Errorf("error creating log directory: %w", err) + } + + logFilename := filepath.Join(logDir, logFileName) + return rotator.New(logFilename, 32*1024, false, maxLogRolls) +} +func (w *Wallet) RawRequest(method string, params []json.RawMessage) (json.RawMessage, error) { + // Not needed for spv wallet. + return nil, errors.New("RawRequest not available on spv") +} + +// createSPVWallet creates a new SPV wallet. +func (w *Wallet) CreateSPVWallet(privPass []byte, seed []byte, dbDir string) error { + net := w.chainParams + w.dataDir = filepath.Join(dbDir, strconv.Itoa(w.ID)) + + if err := logNeutrino(w.dataDir); err != nil { + return fmt.Errorf("error initializing btcwallet+neutrino logging: %v", err) + } + + logDir := filepath.Join(w.dataDir, logDirName) + err := os.MkdirAll(logDir, 0744) + if err != nil { + return fmt.Errorf("error creating wallet directories: %v", err) + } + + loader := wallet.NewLoader(net, w.dataDir, true, 60*time.Second, 250) + pubPass := []byte(wallet.InsecurePubPassphrase) + + _, err = loader.CreateNewWallet(pubPass, privPass, seed, walletBirthday) + if err != nil { + return fmt.Errorf("CreateNewWallet error: %w", err) + } + + bailOnWallet := func() { + if err := loader.UnloadWallet(); err != nil { + w.log.Errorf("Error unloading wallet after createSPVWallet error: %v", err) + } + } + + neutrinoDBPath := filepath.Join(w.dataDir, neutrinoDBName) + db, err := walletdb.Create("bdb", neutrinoDBPath, true, 5*time.Second) + if err != nil { + bailOnWallet() + return fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err) + } + if err = db.Close(); err != nil { + bailOnWallet() + return fmt.Errorf("error closing newly created wallet database: %w", err) + } + + if err := loader.UnloadWallet(); err != nil { + return fmt.Errorf("error unloading wallet: %w", err) + } + + return nil +} + +func (wallet *Wallet) DataDir() string { + return wallet.dataDir +} + +// prepare gets a wallet ready for use by opening the transactions index database +// and initializing the wallet loader which can be used subsequently to create, +// load and unload the wallet. +func (w *Wallet) Prepare(rootDir string, net string, log slog.Logger) (err error) { + chainParams, err := parseChainParams(net) + if err != nil { + return err + } + + w.chainParams = chainParams + w.dataDir = filepath.Join(rootDir, strconv.Itoa(w.ID)) + w.log = log + w.loader = wallet.NewLoader(w.chainParams, w.dataDir, true, 60*time.Second, 250) + return nil +} + +func (w *Wallet) ConnectSPVWallet(ctx context.Context, wg *sync.WaitGroup) (err error) { + return w.connect(ctx, wg) +} + +// connect will start the wallet and begin syncing. +func (w *Wallet) connect(ctx context.Context, wg *sync.WaitGroup) error { + if err := logNeutrino(w.dataDir); err != nil { + return fmt.Errorf("error initializing btcwallet+neutrino logging: %v", err) + } + + err := w.startWallet() + if err != nil { + return err + } + + // txNotes := w.wallet.txNotifications() + + // Nanny for the caches checkpoints and txBlocks caches. + wg.Add(1) + // go func() { + // defer wg.Done() + // defer w.stop() + // defer txNotes.Done() + + // ticker := time.NewTicker(time.Minute * 20) + // defer ticker.Stop() + // expiration := time.Hour * 2 + // for { + // select { + // case <-ticker.C: + // w.txBlocksMtx.Lock() + // for txHash, entry := range w.txBlocks { + // if time.Since(entry.lastAccess) > expiration { + // delete(w.txBlocks, txHash) + // } + // } + // w.txBlocksMtx.Unlock() + + // w.checkpointMtx.Lock() + // for outPt, check := range w.checkpoints { + // if time.Since(check.lastAccess) > expiration { + // delete(w.checkpoints, outPt) + // } + // } + // w.checkpointMtx.Unlock() + + // case note := <-txNotes.C: + // if len(note.AttachedBlocks) > 0 { + // lastBlock := note.AttachedBlocks[len(note.AttachedBlocks)-1] + // syncTarget := atomic.LoadInt32(&w.syncTarget) + + // for ib := range note.AttachedBlocks { + // for _, nt := range note.AttachedBlocks[ib].Transactions { + // w.log.Debugf("Block %d contains wallet transaction %v", note.AttachedBlocks[ib].Height, nt.Hash) + // } + // } + + // if syncTarget == 0 || (lastBlock.Height < syncTarget && lastBlock.Height%10_000 != 0) { + // continue + // } + + // select { + // case w.tipChan <- &block{ + // hash: *lastBlock.Hash, + // height: int64(lastBlock.Height), + // }: + // default: + // w.log.Warnf("tip report channel was blocking") + // } + // } + + // case <-ctx.Done(): + // return + // } + // } + // }() + + return nil +} + +// startWallet initializes the *btcwallet.Wallet and its supporting players and +// starts syncing. +func (w *Wallet) startWallet() error { + // timeout and recoverWindow arguments borrowed from btcwallet directly. + w.loader = wallet.NewLoader(w.chainParams, w.dataDir, true, 60*time.Second, 250) + + exists, err := w.loader.WalletExists() + if err != nil { + return fmt.Errorf("error verifying wallet existence: %v", err) + } + if !exists { + return errors.New("wallet not found") + } + + w.log.Debug("Starting native BTC wallet...") + btcw, err := w.loader.OpenExistingWallet([]byte(wallet.InsecurePubPassphrase), false) + if err != nil { + return fmt.Errorf("couldn't load wallet: %w", err) + } + + bailOnWallet := func() { + if err := w.loader.UnloadWallet(); err != nil { + w.log.Errorf("Error unloading wallet: %v", err) + } + } + + neutrinoDBPath := filepath.Join(w.dataDir, neutrinoDBName) + w.neutrinoDB, err = walletdb.Create("bdb", neutrinoDBPath, true, wallet.DefaultDBTimeout) + if err != nil { + bailOnWallet() + return fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err) + } + + bailOnWalletAndDB := func() { + if err := w.neutrinoDB.Close(); err != nil { + w.log.Errorf("Error closing neutrino database: %v", err) + } + bailOnWallet() + } + + // Depending on the network, we add some addpeers or a connect peer. On + // regtest, if the peers haven't been explicitly set, add the simnet harness + // alpha node as an additional peer so we don't have to type it in. On + // mainet and testnet3, add a known reliable persistent peer to be used in + // addition to normal DNS seed-based peer discovery. + var addPeers []string + var connectPeers []string + switch w.chainParams.Net { + case wire.MainNet: + addPeers = []string{"cfilters.ssgen.io"} + case wire.TestNet3: + addPeers = []string{"dex-test.ssgen.io"} + case wire.TestNet, wire.SimNet: // plain "wire.TestNet" is regnet! + connectPeers = []string{"localhost:20575"} + } + w.log.Debug("Starting neutrino chain service...") + chainService, err := neutrino.NewChainService(neutrino.Config{ + DataDir: w.dataDir, + Database: w.neutrinoDB, + ChainParams: *w.chainParams, + PersistToDisk: true, // keep cfilter headers on disk for efficient rescanning + AddPeers: addPeers, + ConnectPeers: connectPeers, + // WARNING: PublishTransaction currently uses the entire duration + // because if an external bug, but even if the resolved, a typical + // inv/getdata round trip is ~4 seconds, so we set this so neutrino does + // not cancel queries too readily. + BroadcastTimeout: 6 * time.Second, + }) + if err != nil { + bailOnWalletAndDB() + return fmt.Errorf("couldn't create Neutrino ChainService: %v", err) + } + + bailOnEverything := func() { + if err := chainService.Stop(); err != nil { + w.log.Errorf("Error closing neutrino chain service: %v", err) + } + bailOnWalletAndDB() + } + + w.cl = chainService + w.chainClient = chain.NewNeutrinoClient(w.chainParams, chainService) + // w.wallet = &walletExtender{btcw, w.chainParams} + + // oldBday := btcw.Manager.Birthday() + // wdb := btcw.Database() + + // performRescan := w.birthday.Before(oldBday) + // if performRescan && !w.allowAutomaticRescan { + // bailOnWalletAndDB() + // return errors.New("cannot set earlier birthday while there are active deals") + // } + + // if !oldBday.Equal(w.birthday) { + // err = walletdb.Update(wdb, func(dbtx walletdb.ReadWriteTx) error { + // ns := dbtx.ReadWriteBucket(wAddrMgrBkt) + // return btcw.Manager.SetBirthday(ns, w.birthday) + // }) + // if err != nil { + // w.log.Errorf("Failed to reset wallet manager birthday: %v", err) + // performRescan = false + // } + // } + + // if performRescan { + // w.forceRescan() + // } + + if err = w.chainClient.Start(); err != nil { // lazily starts connmgr + bailOnEverything() + return fmt.Errorf("couldn't start Neutrino client: %v", err) + } + + w.log.Info("Synchronizing wallet with network...") + btcw.SynchronizeRPC(w.chainClient) + + return nil +} diff --git a/dcr.go b/dcr.go new file mode 100644 index 000000000..31d1f4c4c --- /dev/null +++ b/dcr.go @@ -0,0 +1,58 @@ +package dcrlibwallet + +import ( + // "context" + // "fmt" + "os" + "path/filepath" + + "decred.org/dcrwallet/v2/errors" + + "github.com/asdine/storm" + // "github.com/asdine/storm/q" + + bolt "go.etcd.io/bbolt" + + "github.com/planetdecred/dcrlibwallet/wallets/dcr" +) + +func initializeDCRWallet(rootDir, dbDriver, netType string) (*storm.DB, string, error) { + var mwDB *storm.DB + + rootDir = filepath.Join(rootDir, netType, "dcr") + err := os.MkdirAll(rootDir, os.ModePerm) + if err != nil { + return mwDB, "", errors.Errorf("failed to create dcr rootDir: %v", err) + } + + err = initLogRotator(filepath.Join(rootDir, logFileName)) + if err != nil { + return mwDB, "", errors.Errorf("failed to init dcr logRotator: %v", err.Error()) + } + + mwDB, err = storm.Open(filepath.Join(rootDir, walletsDbName)) + if err != nil { + log.Errorf("Error opening dcr wallets database: %s", err.Error()) + if err == bolt.ErrTimeout { + // timeout error occurs if storm fails to acquire a lock on the database file + return mwDB, "", errors.E(ErrWalletDatabaseInUse) + } + return mwDB, "", errors.Errorf("error opening dcr wallets database: %s", err.Error()) + } + + // init database for saving/reading wallet objects + err = mwDB.Init(&dcr.Wallet{}) + if err != nil { + log.Errorf("Error initializing wallets database: %s", err.Error()) + return mwDB, "", err + } + + // init database for saving/reading proposal objects + err = mwDB.Init(&dcr.Proposal{}) + if err != nil { + log.Errorf("Error initializing wallets database: %s", err.Error()) + return mwDB, "", err + } + + return mwDB, rootDir, nil +} diff --git a/dexclient.go b/dexclient.go index 087679138..6c3c11dee 100644 --- a/dexclient.go +++ b/dexclient.go @@ -89,7 +89,7 @@ func (mw *MultiWallet) prepareDexSupportForDcrWalletLibrary() error { return nil, fmt.Errorf("invalid wallet ID %q in settings", walletIDStr) } - wallet := mw.WalletWithID(walletID) + wallet, _ := mw.WalletWithID(walletID, "DCR") if wallet == nil { return nil, fmt.Errorf("no wallet exists with ID %q", walletIDStr) } @@ -105,7 +105,7 @@ func (mw *MultiWallet) prepareDexSupportForDcrWalletLibrary() error { return nil, fmt.Errorf("account error: %v", err) } - walletDesc := fmt.Sprintf("%q in %s", wallet.Name, wallet.dataDir) + walletDesc := fmt.Sprintf("%q in %s", wallet.Name, wallet.DataDir) return dexdcr.NewSpvWallet(wallet.Internal(), walletDesc, chainParams, logger.SubLogger("DLWL")), nil } diff --git a/go.mod b/go.mod index 8dcbb46bc..096067637 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,15 @@ module github.com/planetdecred/dcrlibwallet require ( decred.org/dcrdex v0.4.1 decred.org/dcrwallet/v2 v2.0.2-0.20220505152146-ece5da349895 - github.com/DataDog/zstd v1.4.8 // indirect github.com/asdine/storm v0.0.0-20190216191021-fe89819f6282 - github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a // indirect - github.com/dchest/siphash v1.2.3 // indirect - github.com/decred/base58 v1.0.4 // indirect + github.com/btcsuite/btcd v0.22.1 + github.com/btcsuite/btcd/btcutil v1.1.1 + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f + github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890 // note: hoists btcd's own require of btcutil + github.com/btcsuite/btcwallet v0.12.0 + github.com/btcsuite/btcwallet/walletdb v1.4.0 + github.com/btcsuite/btcwallet/wtxmgr v1.3.0 github.com/decred/dcrd/addrmgr/v2 v2.0.0 github.com/decred/dcrd/blockchain/stake/v4 v4.0.0 github.com/decred/dcrd/chaincfg/chainhash v1.0.3 @@ -24,19 +28,15 @@ require ( github.com/decred/politeia v1.3.1 github.com/decred/slog v1.2.0 github.com/dgraph-io/badger v1.6.2 - github.com/gorilla/websocket v1.5.0 // indirect github.com/jrick/logrotate v1.0.0 github.com/kevinburke/nacl v0.0.0-20190829012316-f3ed23dbd7f8 + github.com/lightninglabs/neutrino v0.13.1-0.20211214231330-53b628ce1756 github.com/onsi/ginkgo v1.14.0 github.com/onsi/gomega v1.10.1 github.com/planetdecred/dcrlibwallet/dexdcr v0.0.0-20220223161805-c736f970653d go.etcd.io/bbolt v1.3.6 golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f - golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect - golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect - google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3 // indirect ) // Older versions of github.com/lib/pq are required by politeia (v1.9.0) diff --git a/go.sum b/go.sum index 30fcd2956..13aebe8e5 100644 --- a/go.sum +++ b/go.sum @@ -140,8 +140,26 @@ github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13P github.com/btcsuite/btcd v0.21.0-beta.0.20201208033208-6bd4c64a54fa/go.mod h1:Sv4JPQ3/M+teHz9Bo5jBpkNcP0x6r7rdihlNL/7tTAs= github.com/btcsuite/btcd v0.21.0-beta.0.20210426180113-7eba688b65e5/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= github.com/btcsuite/btcd v0.22.0-beta.0.20210803133449-f5a1fb9965e4/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= -github.com/btcsuite/btcd v0.22.0-beta.0.20211026140004-31791ba4dc6e h1:d0NkvbJGQThTkhypOdtf5Orxe2+dUHLB86pQ4EXwrTo= github.com/btcsuite/btcd v0.22.0-beta.0.20211026140004-31791ba4dc6e/go.mod h1:3PH+KbvLFfzBTCevQenPiDedjGQGt6aa70dVjJDWGTA= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c= +github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= +github.com/btcsuite/btcd v0.23.0 h1:V2/ZgjfDFIygAX3ZapeigkVBoVUtOJKSwrhZdlpSvaA= +github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.1 h1:xxivBG6pU3wwxx9qPNZP+2K0PXO9VmFLaSrwOFr24Hw= +github.com/btcsuite/btcd/btcec/v2 v2.1.1/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.1 h1:hDcDaXiP0uEzR8Biqo2weECKqEw0uHDZ9ixIWevVQqY= +github.com/btcsuite/btcd/btcutil v1.1.1/go.mod h1:nbKlBMNm9FGsdvKvu0essceubPiAcI57pYBNnsLAa34= +github.com/btcsuite/btcd/btcutil v1.1.2 h1:XLMbX8JQEiwMcYft2EGi8zPUkoa0abKIU6/BJSRsjzQ= +github.com/btcsuite/btcd/btcutil v1.1.2/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= @@ -200,6 +218,7 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/companyzero/sntrup4591761 v0.0.0-20200131011700-2b0d299dbd22 h1:vfqLMkB1UqwJliW0I/34oscQawInrVfL1uPjGEEt2YY= github.com/companyzero/sntrup4591761 v0.0.0-20200131011700-2b0d299dbd22/go.mod h1:LoZJNGDWmVPqMEHmeJzj4Weq4Stjc6FKY6FVpY3Hem0= github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a h1:clYxJ3Os0EQUKDDVU8M0oipllX0EkuFNBfhVQuIfyF0= github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a/go.mod h1:z/9Ck1EDixEbBbZ2KH2qNHekEmDLTOZ+FyoIPWWSVOI= @@ -229,12 +248,14 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dchest/blake256 v1.0.0/go.mod h1:xXNWCE1jsAP8DAjP+rKw2MbeqLczjI3TRx2VK+9OEYY= github.com/dchest/siphash v1.2.0/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= +github.com/dchest/siphash v1.2.2 h1:9DFz8tQwl9pTVt5iok/9zKyzA1Q6bRGiF3HPiEEVr9I= github.com/dchest/siphash v1.2.2/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= github.com/decred/base58 v1.0.0/go.mod h1:LLY1p5e3g91byL/UO1eiZaYd+uRoVRarybgcoymu9Ks= github.com/decred/base58 v1.0.1/go.mod h1:H2ENcsJjye1G7CbRa67kV9OFaui0LGr56ntKKoY5g9c= +github.com/decred/base58 v1.0.3 h1:KGZuh8d1WEMIrK0leQRM47W85KqCAdl2N+uagbctdDI= github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E= github.com/decred/base58 v1.0.4 h1:QJC6B0E0rXOPA8U/kw2rP+qiRJsUaE2Er+pYb3siUeA= github.com/decred/base58 v1.0.4/go.mod h1:jJswKPEdvpFpvf7dsDvFZyLT22xZ9lWqEByX38oGd9E= @@ -568,6 +589,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= @@ -630,6 +652,7 @@ github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -696,6 +719,7 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M github.com/jessevdk/go-flags v0.0.0-20181221193153-c0795c8afcf4/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.1-0.20200711081900-c17162fe8fd7/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSnpuG7toUTG4= github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= @@ -891,6 +915,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= +github.com/planetdecred/dcrlibwallet v1.7.0/go.mod h1:QTADevJvo+ugI0LZkwVVwCMiqtVf/W0DKlODTYwbnCs= github.com/planetdecred/dcrlibwallet/dexdcr v0.0.0-20220223161805-c736f970653d h1:egC6nx+qP3QMwKQhA+JdJReKV9NhG17Ag+3oCVCsj3c= github.com/planetdecred/dcrlibwallet/dexdcr v0.0.0-20220223161805-c736f970653d/go.mod h1:jO4RP2rgqom8CLgl3rMwZ4cGzmalJqBkKjHgVS812lM= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -1155,6 +1180,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -1239,18 +1265,21 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912 h1:uCLL3g5wH2xjxVREVuAbP9JM5PPKjRbXKRa6IBjkzmU= golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= @@ -1262,6 +1291,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -1397,6 +1427,7 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200707001353-8e8330bf89df/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201022181438-0ff5f38871d5 h1:YejJbGvoWsTXHab4OKNrzk27Dr7s4lPLnewbHue1+gM= google.golang.org/genproto v0.0.0-20201022181438-0ff5f38871d5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3 h1:q1kiSVscqoDeqTF27eQ2NnLLDmqF0I373qQNXYMy0fo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= @@ -1423,6 +1454,7 @@ google.golang.org/grpc v1.29.0/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8= @@ -1436,6 +1468,7 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= diff --git a/multiwallet.go b/multiwallet.go index efbaa26fd..62d50b21b 100644 --- a/multiwallet.go +++ b/multiwallet.go @@ -6,46 +6,55 @@ import ( "fmt" "os" "path/filepath" - "strconv" "strings" - "sync" - "time" "decred.org/dcrwallet/v2/errors" - w "decred.org/dcrwallet/v2/wallet" "github.com/asdine/storm" "github.com/asdine/storm/q" + btccfg "github.com/btcsuite/btcd/chaincfg" "github.com/decred/dcrd/chaincfg/v3" + "github.com/planetdecred/dcrlibwallet/utils" - "github.com/planetdecred/dcrlibwallet/walletdata" - bolt "go.etcd.io/bbolt" + + "github.com/planetdecred/dcrlibwallet/wallets/btc" + "github.com/planetdecred/dcrlibwallet/wallets/dcr" + "golang.org/x/crypto/bcrypt" ) +type Assets struct { + DCR struct { + Wallets map[int]*dcr.Wallet + BadWallets map[int]*dcr.Wallet + DBDriver string + RootDir string + DB *storm.DB + ChainParams *chaincfg.Params + } + BTC struct { + Wallets map[int]*btc.Wallet + BadWallets map[int]*btc.Wallet + DBDriver string + RootDir string + DB *storm.DB + ChainParams *btccfg.Params + } +} + type MultiWallet struct { dbDriver string rootDir string db *storm.DB chainParams *chaincfg.Params - wallets map[int]*Wallet - badWallets map[int]*Wallet - syncData *syncData - - notificationListenersMu sync.RWMutex - txAndBlockNotificationListeners map[string]TxAndBlockNotificationListener - - blocksRescanProgressListener BlocksRescanProgressListener - accountMixerNotificationListener map[string]AccountMixerNotificationListener + Assets *Assets + wallets map[int]*dcr.Wallet + badWallets map[int]*dcr.Wallet shuttingDown chan bool cancelFuncs []context.CancelFunc - Politeia *Politeia dexClient *DexClient - - vspMu sync.RWMutex - vsps []*VSP } func NewMultiWallet(rootDir, dbDriver, netType, politeiaHost string) (*MultiWallet, error) { @@ -56,94 +65,123 @@ func NewMultiWallet(rootDir, dbDriver, netType, politeiaHost string) (*MultiWall return nil, err } - rootDir = filepath.Join(rootDir, netType) - err = os.MkdirAll(rootDir, os.ModePerm) - if err != nil { - return nil, errors.Errorf("failed to create rootDir: %v", err) - } - - err = initLogRotator(filepath.Join(rootDir, logFileName)) - if err != nil { - return nil, errors.Errorf("failed to init logRotator: %v", err.Error()) - } - - mwDB, err := storm.Open(filepath.Join(rootDir, walletsDbName)) - if err != nil { - log.Errorf("Error opening wallets database: %s", err.Error()) - if err == bolt.ErrTimeout { - // timeout error occurs if storm fails to acquire a lock on the database file - return nil, errors.E(ErrWalletDatabaseInUse) - } - return nil, errors.Errorf("error opening wallets database: %s", err.Error()) - } - - // init database for saving/reading wallet objects - err = mwDB.Init(&Wallet{}) + dcrDB, dcrRootDir, err := initializeDCRWallet(rootDir, dbDriver, netType) if err != nil { - log.Errorf("Error initializing wallets database: %s", err.Error()) - return nil, err + log.Errorf("error initializing DCRWallet: %s", err.Error()) + return nil, errors.Errorf("error initializing DCRWallet: %s", err.Error()) } - // init database for saving/reading proposal objects - err = mwDB.Init(&Proposal{}) + btcDB, btcRootDir, err := initializeBTCWallet(rootDir, dbDriver, netType) if err != nil { - log.Errorf("Error initializing wallets database: %s", err.Error()) - return nil, err + log.Errorf("error initializing BTCWallet: %s", err.Error()) + return nil, errors.Errorf("error initializing BTCWallet: %s", err.Error()) } mw := &MultiWallet{ dbDriver: dbDriver, - rootDir: rootDir, - db: mwDB, + rootDir: dcrRootDir, + db: dcrDB, chainParams: chainParams, - wallets: make(map[int]*Wallet), - badWallets: make(map[int]*Wallet), - syncData: &syncData{ - syncProgressListeners: make(map[string]SyncProgressListener), + Assets: &Assets{ + DCR: struct { + Wallets map[int]*dcr.Wallet + BadWallets map[int]*dcr.Wallet + DBDriver string + RootDir string + DB *storm.DB + ChainParams *chaincfg.Params + }{ + Wallets: make(map[int]*dcr.Wallet), + BadWallets: make(map[int]*dcr.Wallet), + DBDriver: dbDriver, + RootDir: dcrRootDir, + DB: dcrDB, + ChainParams: chainParams, + }, + BTC: struct { + Wallets map[int]*btc.Wallet + BadWallets map[int]*btc.Wallet + DBDriver string + RootDir string + DB *storm.DB + ChainParams *btccfg.Params + }{ + Wallets: make(map[int]*btc.Wallet), + BadWallets: make(map[int]*btc.Wallet), + DBDriver: dbDriver, + RootDir: btcRootDir, + DB: btcDB, + ChainParams: &btccfg.TestNet3Params, + }, }, - txAndBlockNotificationListeners: make(map[string]TxAndBlockNotificationListener), - accountMixerNotificationListener: make(map[string]AccountMixerNotificationListener), } - mw.Politeia, err = newPoliteia(mw, politeiaHost) - if err != nil { - return nil, err + mw.prepareWallets() + + mw.listenForShutdown() + + logLevel := mw.ReadStringConfigValueForKey(LogLevelConfigKey) + SetLogLevels(logLevel) + + log.Infof("Loaded %d wallets", mw.LoadedWalletsCount()) + + if err = mw.initDexClient(); err != nil { + log.Errorf("DEX client set up error: %v", err) } + return mw, nil +} + +func (mw *MultiWallet) prepareWallets() error { // read saved wallets info from db and initialize wallets query := mw.db.Select(q.True()).OrderBy("ID") - var wallets []*Wallet - err = query.Find(&wallets) + var dcrWallets []*dcr.Wallet + err := query.Find(&dcrWallets) if err != nil && err != storm.ErrNotFound { - return nil, err + return err } // prepare the wallets loaded from db for use - for _, wallet := range wallets { - err = wallet.prepare(rootDir, chainParams, mw.walletConfigSetFn(wallet.ID), mw.walletConfigReadFn(wallet.ID)) - if err == nil && !WalletExistsAt(wallet.dataDir) { + for _, wallet := range dcrWallets { + err = wallet.Prepare(mw.rootDir, mw.chainParams, mw.walletConfigSetFn(wallet.ID), mw.walletConfigReadFn(wallet.ID)) + if err == nil && !WalletExistsAt(wallet.DataDir) { err = fmt.Errorf("missing wallet database file") } if err != nil { - mw.badWallets[wallet.ID] = wallet + mw.Assets.DCR.BadWallets[wallet.ID] = wallet log.Warnf("Ignored wallet load error for wallet %d (%s)", wallet.ID, wallet.Name) } else { - mw.wallets[wallet.ID] = wallet + mw.Assets.DCR.Wallets[wallet.ID] = wallet } - } - mw.listenForShutdown() + // initialize Politeia. + wallet.NewPoliteia("") + } - logLevel := mw.ReadStringConfigValueForKey(LogLevelConfigKey) - SetLogLevels(logLevel) + // read saved wallets info from db and initialize wallets + query = mw.Assets.BTC.DB.Select(q.True()).OrderBy("ID") + var btcWallets []*btc.Wallet + err = query.Find(&btcWallets) + if err != nil && err != storm.ErrNotFound { + return err + } - log.Infof("Loaded %d wallets", mw.LoadedWalletsCount()) + // prepare the wallets loaded from db for use + for _, wallet := range btcWallets { + err = wallet.Prepare(mw.Assets.BTC.RootDir, mw.NetType(), log) + if err == nil && !WalletExistsAt(wallet.DataDir()) { + err = fmt.Errorf("missing wallet database file") + } + if err != nil { + mw.Assets.BTC.BadWallets[wallet.ID] = wallet - if err = mw.initDexClient(); err != nil { - log.Errorf("DEX client set up error: %v", err) + log.Warnf("Ignored wallet load error for wallet %d (%s)", wallet.ID, wallet.Name) + } else { + mw.Assets.BTC.Wallets[wallet.ID] = wallet + } } - return mw, nil + return nil } func (mw *MultiWallet) Shutdown() { @@ -152,25 +190,46 @@ func (mw *MultiWallet) Shutdown() { // Trigger shuttingDown signal to cancel all contexts created with `shutdownContextWithCancel`. mw.shuttingDown <- true - mw.CancelRescan() - mw.CancelSync() + mw.shuttingDownDCR() + + mw.shuttingDownBTC() + + if logRotator != nil { + log.Info("Shutting down log rotator") + logRotator.Close() + log.Info("Shutdown log rotator successfully") + } +} - for _, wallet := range mw.wallets { +func (mw *MultiWallet) shuttingDownDCR() { + for _, wallet := range mw.Assets.DCR.Wallets { + wallet.CancelRescan() + } + + for _, wallet := range mw.Assets.DCR.Wallets { + wallet.CancelSync() + } + + for _, wallet := range mw.Assets.DCR.Wallets { wallet.Shutdown() } - if mw.db != nil { - if err := mw.db.Close(); err != nil { - log.Errorf("db closed with error: %v", err) + if mw.Assets.DCR.DB != nil { + if err := mw.Assets.DCR.DB.Close(); err != nil { + log.Errorf("dcr db closed with error: %v", err) } else { - log.Info("db closed successfully") + log.Info("dcr db closed successfully") } } +} - if logRotator != nil { - log.Info("Shutting down log rotator") - logRotator.Close() - log.Info("Shutdown log rotator successfully") +func (mw *MultiWallet) shuttingDownBTC() { + if mw.Assets.BTC.DB != nil { + if err := mw.Assets.BTC.DB.Close(); err != nil { + log.Errorf("btc db closed with error: %v", err) + } else { + log.Info("btc db closed successfully") + } } } @@ -266,17 +325,25 @@ func (mw *MultiWallet) StartupSecurityType() int32 { } func (mw *MultiWallet) OpenWallets(startupPassphrase []byte) error { - if mw.IsSyncing() { - return errors.New(ErrSyncAlreadyInProgress) - } - err := mw.VerifyStartupPassphrase(startupPassphrase) if err != nil { return err } - for _, wallet := range mw.wallets { - err = wallet.openWallet() + // Open DCR wallets + for _, wallet := range mw.Assets.DCR.Wallets { + if wallet.IsSyncing() { + return errors.New(ErrSyncAlreadyInProgress) + } + err = wallet.OpenWallet() + if err != nil { + return err + } + } + + // Open BTC wallets + for _, wallet := range mw.Assets.BTC.Wallets { + err = wallet.OpenWallet() if err != nil { return err } @@ -286,326 +353,139 @@ func (mw *MultiWallet) OpenWallets(startupPassphrase []byte) error { } func (mw *MultiWallet) AllWalletsAreWatchOnly() (bool, error) { - if len(mw.wallets) == 0 { + if len(mw.Assets.DCR.Wallets) == 0 || len(mw.Assets.BTC.Wallets) == 0 { return false, errors.New(ErrInvalid) } - for _, w := range mw.wallets { + for _, w := range mw.Assets.DCR.Wallets { if !w.IsWatchingOnlyWallet() { return false, nil } } - return true, nil -} - -func (mw *MultiWallet) CreateWatchOnlyWallet(walletName, extendedPublicKey string) (*Wallet, error) { - wallet := &Wallet{ - Name: walletName, - IsRestored: true, - HasDiscoveredAccounts: true, - } - - return mw.saveNewWallet(wallet, func() error { - err := wallet.prepare(mw.rootDir, mw.chainParams, mw.walletConfigSetFn(wallet.ID), mw.walletConfigReadFn(wallet.ID)) - if err != nil { - return err + for _, w := range mw.Assets.BTC.Wallets { + if !w.IsWatchingOnlyWallet() { + return false, nil } - - return wallet.createWatchingOnlyWallet(extendedPublicKey) - }) -} - -func (mw *MultiWallet) CreateNewWallet(walletName, privatePassphrase string, privatePassphraseType int32) (*Wallet, error) { - seed, err := GenerateSeed() - if err != nil { - return nil, err } - encryptedSeed, err := encryptWalletSeed([]byte(privatePassphrase), seed) - if err != nil { - return nil, err - } - wallet := &Wallet{ - Name: walletName, - CreatedAt: time.Now(), - EncryptedSeed: encryptedSeed, - PrivatePassphraseType: privatePassphraseType, - HasDiscoveredAccounts: true, - } - - return mw.saveNewWallet(wallet, func() error { - err := wallet.prepare(mw.rootDir, mw.chainParams, mw.walletConfigSetFn(wallet.ID), mw.walletConfigReadFn(wallet.ID)) - if err != nil { - return err - } - - return wallet.createWallet(privatePassphrase, seed) - }) + return true, nil } -func (mw *MultiWallet) RestoreWallet(walletName, seedMnemonic, privatePassphrase string, privatePassphraseType int32) (*Wallet, error) { - - wallet := &Wallet{ - Name: walletName, - PrivatePassphraseType: privatePassphraseType, - IsRestored: true, - HasDiscoveredAccounts: false, - } - - return mw.saveNewWallet(wallet, func() error { - err := wallet.prepare(mw.rootDir, mw.chainParams, mw.walletConfigSetFn(wallet.ID), mw.walletConfigReadFn(wallet.ID)) - if err != nil { - return err - } - - return wallet.createWallet(privatePassphrase, seedMnemonic) - }) +func (mw *MultiWallet) BadWallets() map[int]*dcr.Wallet { + return mw.badWallets } -func (mw *MultiWallet) LinkExistingWallet(walletName, walletDataDir, originalPubPass string, privatePassphraseType int32) (*Wallet, error) { - // check if `walletDataDir` contains wallet.db - if !WalletExistsAt(walletDataDir) { - return nil, errors.New(ErrNotExist) +func (mw *MultiWallet) DeleteBadWallet(walletID int) error { + wallet := mw.badWallets[walletID] + if wallet == nil { + return errors.New(ErrNotExist) } - ctx, _ := mw.contextWithShutdownCancel() + log.Info("Deleting bad wallet") - // verify the public passphrase for the wallet being linked before proceeding - if err := mw.loadWalletTemporarily(ctx, walletDataDir, originalPubPass, nil); err != nil { - return nil, err + err := mw.db.DeleteStruct(wallet) + if err != nil { + return translateError(err) } - wallet := &Wallet{ - Name: walletName, - PrivatePassphraseType: privatePassphraseType, - IsRestored: true, - HasDiscoveredAccounts: false, // assume that account discovery hasn't been done - } + os.RemoveAll(wallet.DataDir) + delete(mw.badWallets, walletID) - return mw.saveNewWallet(wallet, func() error { - // move wallet.db and tx.db files to newly created dir for the wallet - currentWalletDbFilePath := filepath.Join(walletDataDir, walletDbName) - newWalletDbFilePath := filepath.Join(wallet.dataDir, walletDbName) - if err := moveFile(currentWalletDbFilePath, newWalletDbFilePath); err != nil { - return err - } + return nil +} - currentTxDbFilePath := filepath.Join(walletDataDir, walletdata.OldDbName) - newTxDbFilePath := filepath.Join(wallet.dataDir, walletdata.DbName) - if err := moveFile(currentTxDbFilePath, newTxDbFilePath); err != nil { - return err +func (mw *MultiWallet) DeleteWallet(walletID int, privPass []byte, Asset string) error { + switch Asset { + case "DCR", "dcr": + wallet, _ := mw.WalletWithID(walletID, "dcr") + if wallet == nil { + return errors.New(ErrNotExist) } - // prepare the wallet for use and open it - err := (func() error { - err := wallet.prepare(mw.rootDir, mw.chainParams, mw.walletConfigSetFn(wallet.ID), mw.walletConfigReadFn(wallet.ID)) - if err != nil { - return err - } - - if originalPubPass == "" || originalPubPass == w.InsecurePubPassphrase { - return wallet.openWallet() - } - - err = mw.loadWalletTemporarily(ctx, wallet.dataDir, originalPubPass, func(tempWallet *w.Wallet) error { - return tempWallet.ChangePublicPassphrase(ctx, []byte(originalPubPass), []byte(w.InsecurePubPassphrase)) - }) - if err != nil { - return err - } - - return wallet.openWallet() - })() - - // restore db files to their original location if there was an error - // in the wallet setup process above - if err != nil { - moveFile(newWalletDbFilePath, currentWalletDbFilePath) - moveFile(newTxDbFilePath, currentTxDbFilePath) + if wallet.IsConnectedToDecredNetwork() { + wallet.CancelSync() + defer func() { + if mw.OpenedWalletsCount() > 0 { + wallet.SpvSync() + } + }() } - return err - }) -} + // err := wallet.deleteWallet(privPass) + // if err != nil { + // return translateError(err) + // } -// saveNewWallet performs the following tasks using a db batch operation to ensure -// that db changes are rolled back if any of the steps below return an error. -// -// - saves the initial wallet info to mw.walletsDb to get a wallet id -// - creates a data directory for the wallet using the auto-generated wallet id -// - updates the initial wallet info with name, dataDir (created above), db driver -// and saves the updated info to mw.walletsDb -// - calls the provided `setupWallet` function to perform any necessary creation, -// restoration or linking of the just saved wallet -// -// IFF all the above operations succeed, the wallet info will be persisted to db -// and the wallet will be added to `mw.wallets`. -func (mw *MultiWallet) saveNewWallet(wallet *Wallet, setupWallet func() error) (*Wallet, error) { - exists, err := mw.WalletNameExists(wallet.Name) - if err != nil { - return nil, err - } else if exists { - return nil, errors.New(ErrExist) - } - - if mw.IsConnectedToDecredNetwork() { - mw.CancelSync() - defer mw.SpvSync() - } - // Perform database save operations in batch transaction - // for automatic rollback if error occurs at any point. - err = mw.batchDbTransaction(func(db storm.Node) error { - // saving struct to update ID property with an auto-generated value - err := db.Save(wallet) + err := mw.Assets.DCR.DB.DeleteStruct(wallet) if err != nil { - return err + return translateError(err) } - walletDataDir := filepath.Join(mw.rootDir, strconv.Itoa(wallet.ID)) + delete(mw.wallets, walletID) - dirExists, err := fileExists(walletDataDir) - if err != nil { - return err - } else if dirExists { - newDirName, err := backupFile(walletDataDir, 1) - if err != nil { - return err - } - - log.Infof("Undocumented file at %s moved to %s", walletDataDir, newDirName) + return nil + case "BTC", "btc": + _, wallet := mw.WalletWithID(walletID, "btc") + if wallet == nil { + return errors.New(ErrNotExist) } - os.MkdirAll(walletDataDir, os.ModePerm) // create wallet dir + // if wallet.IsConnectedToDecredNetwork() { + // wallet.CancelSync() + // defer func() { + // if mw.OpenedWalletsCount() > 0 { + // wallet.SpvSync() + // } + // }() + // } - if wallet.Name == "" { - wallet.Name = "wallet-" + strconv.Itoa(wallet.ID) // wallet-# + err := wallet.DeleteWallet(privPass) + if err != nil { + return translateError(err) } - wallet.dataDir = walletDataDir - wallet.DbDriver = mw.dbDriver - err = db.Save(wallet) // update database with complete wallet information + err = mw.Assets.BTC.DB.DeleteStruct(wallet) if err != nil { - return err + return translateError(err) } - return setupWallet() - }) - - if err != nil { - return nil, translateError(err) - } - - mw.wallets[wallet.ID] = wallet - - return wallet, nil -} - -func (mw *MultiWallet) RenameWallet(walletID int, newName string) error { - if strings.HasPrefix(newName, "wallet-") { - return errors.E(ErrReservedWalletName) - } - - if exists, err := mw.WalletNameExists(newName); err != nil { - return translateError(err) - } else if exists { - return errors.New(ErrExist) - } + delete(mw.Assets.BTC.Wallets, walletID) - wallet := mw.WalletWithID(walletID) - if wallet == nil { - return errors.New(ErrInvalid) - } - - wallet.Name = newName - return mw.db.Save(wallet) // update WalletName field -} - -func (mw *MultiWallet) DeleteWallet(walletID int, privPass []byte) error { - - wallet := mw.WalletWithID(walletID) - if wallet == nil { - return errors.New(ErrNotExist) - } - - if mw.IsConnectedToDecredNetwork() { - mw.CancelSync() - defer func() { - if mw.OpenedWalletsCount() > 0 { - mw.SpvSync() - } - }() - } - - err := wallet.deleteWallet(privPass) - if err != nil { - return translateError(err) - } - - err = mw.db.DeleteStruct(wallet) - if err != nil { - return translateError(err) - } - - delete(mw.wallets, walletID) - - return nil -} - -func (mw *MultiWallet) BadWallets() map[int]*Wallet { - return mw.badWallets -} - -func (mw *MultiWallet) DeleteBadWallet(walletID int) error { - wallet := mw.badWallets[walletID] - if wallet == nil { - return errors.New(ErrNotExist) - } - - log.Info("Deleting bad wallet") - - err := mw.db.DeleteStruct(wallet) - if err != nil { - return translateError(err) + return nil + default: + return nil } - os.RemoveAll(wallet.dataDir) - delete(mw.badWallets, walletID) - - return nil } -func (mw *MultiWallet) WalletWithID(walletID int) *Wallet { - if wallet, ok := mw.wallets[walletID]; ok { - return wallet - } - return nil -} - -// VerifySeedForWallet compares seedMnemonic with the decrypted wallet.EncryptedSeed and clears wallet.EncryptedSeed if they match. -func (mw *MultiWallet) VerifySeedForWallet(walletID int, seedMnemonic string, privpass []byte) (bool, error) { - wallet := mw.WalletWithID(walletID) - if wallet == nil { - return false, errors.New(ErrNotExist) - } - - decryptedSeed, err := decryptWalletSeed(privpass, wallet.EncryptedSeed) - if err != nil { - return false, err - } - - if decryptedSeed == seedMnemonic { - wallet.EncryptedSeed = nil - return true, translateError(mw.db.Save(wallet)) +func (mw *MultiWallet) WalletWithID(walletID int, Asset string) (*dcr.Wallet, *btc.Wallet) { + switch Asset { + case "DCR", "dcr": + if wallet, ok := mw.Assets.DCR.Wallets[walletID]; ok { + return wallet, nil + } + return nil, nil + case "BTC", "btc": + if wallet, ok := mw.Assets.BTC.Wallets[walletID]; ok { + return nil, wallet + } + return nil, nil + default: + return nil, nil } - - return false, errors.New(ErrInvalid) } // NumWalletsNeedingSeedBackup returns the number of opened wallets whose seed haven't been verified. func (mw *MultiWallet) NumWalletsNeedingSeedBackup() int32 { var backupsNeeded int32 - for _, wallet := range mw.wallets { + for _, wallet := range mw.Assets.DCR.Wallets { + if wallet.WalletOpened() && wallet.EncryptedSeed != nil { + backupsNeeded++ + } + } + + for _, wallet := range mw.Assets.BTC.Wallets { if wallet.WalletOpened() && wallet.EncryptedSeed != nil { backupsNeeded++ } @@ -614,12 +494,18 @@ func (mw *MultiWallet) NumWalletsNeedingSeedBackup() int32 { } func (mw *MultiWallet) LoadedWalletsCount() int32 { - return int32(len(mw.wallets)) + return int32(len(mw.Assets.DCR.Wallets)) + int32(len(mw.Assets.BTC.Wallets)) } func (mw *MultiWallet) OpenedWalletIDsRaw() []int { walletIDs := make([]int, 0) - for _, wallet := range mw.wallets { + for _, wallet := range mw.Assets.DCR.Wallets { + if wallet.WalletOpened() { + walletIDs = append(walletIDs, wallet.ID) + } + } + + for _, wallet := range mw.Assets.BTC.Wallets { if wallet.WalletOpened() { walletIDs = append(walletIDs, wallet.ID) } @@ -639,8 +525,14 @@ func (mw *MultiWallet) OpenedWalletsCount() int32 { func (mw *MultiWallet) SyncedWalletsCount() int32 { var syncedWallets int32 - for _, wallet := range mw.wallets { - if wallet.WalletOpened() && wallet.synced { + for _, wallet := range mw.Assets.DCR.Wallets { + if wallet.WalletOpened() && wallet.Synced { + syncedWallets++ + } + } + + for _, wallet := range mw.Assets.BTC.Wallets { + if wallet.WalletOpened() && wallet.Synced { syncedWallets++ } } @@ -653,69 +545,19 @@ func (mw *MultiWallet) WalletNameExists(walletName string) (bool, error) { return false, errors.E(ErrReservedWalletName) } - err := mw.db.One("Name", walletName, &Wallet{}) + err := mw.Assets.DCR.DB.One("Name", walletName, &dcr.Wallet{}) if err == nil { return true, nil } else if err != storm.ErrNotFound { return false, err } - return false, nil -} - -func (mw *MultiWallet) UnlockWallet(walletID int, privPass []byte) error { - wallet := mw.WalletWithID(walletID) - if wallet == nil { - return errors.New(ErrNotExist) - } - - return wallet.UnlockWallet(privPass) -} - -// ChangePrivatePassphraseForWallet attempts to change the wallet's passphrase and re-encrypts the seed with the new passphrase. -func (mw *MultiWallet) ChangePrivatePassphraseForWallet(walletID int, oldPrivatePassphrase, newPrivatePassphrase []byte, privatePassphraseType int32) error { - if privatePassphraseType != PassphraseTypePin && privatePassphraseType != PassphraseTypePass { - return errors.New(ErrInvalid) - } - - wallet := mw.WalletWithID(walletID) - if wallet == nil { - return errors.New(ErrInvalid) - } - - encryptedSeed := wallet.EncryptedSeed - if encryptedSeed != nil { - decryptedSeed, err := decryptWalletSeed(oldPrivatePassphrase, encryptedSeed) - if err != nil { - return err - } - - encryptedSeed, err = encryptWalletSeed(newPrivatePassphrase, decryptedSeed) - if err != nil { - return err - } - } - - err := wallet.changePrivatePassphrase(oldPrivatePassphrase, newPrivatePassphrase) - if err != nil { - return translateError(err) - } - - wallet.EncryptedSeed = encryptedSeed - wallet.PrivatePassphraseType = privatePassphraseType - err = mw.db.Save(wallet) - if err != nil { - log.Errorf("error saving wallet-[%d] to database after passphrase change: %v", wallet.ID, err) - - err2 := wallet.changePrivatePassphrase(newPrivatePassphrase, oldPrivatePassphrase) - if err2 != nil { - log.Errorf("error undoing wallet passphrase change: %v", err2) - log.Errorf("error wallet passphrase was changed but passphrase type and newly encrypted seed could not be saved: %v", err) - return errors.New(ErrSavingWallet) - } - - return errors.New(ErrChangingPassphrase) + err = mw.Assets.BTC.DB.One("Name", walletName, &btc.Wallet{}) + if err == nil { + return true, nil + } else if err != storm.ErrNotFound { + return false, err } - return nil + return false, nil } diff --git a/multiwallet_utils.go b/multiwallet_utils.go index 9208c2be4..5caaf96aa 100644 --- a/multiwallet_utils.go +++ b/multiwallet_utils.go @@ -16,6 +16,8 @@ import ( "github.com/kevinburke/nacl" "github.com/kevinburke/nacl/secretbox" "golang.org/x/crypto/scrypt" + + "github.com/planetdecred/dcrlibwallet/wallets/dcr" ) const ( @@ -81,7 +83,7 @@ func (mw *MultiWallet) loadWalletTemporarily(ctx context.Context, walletDataDir, } func (mw *MultiWallet) markWalletAsDiscoveredAccounts(walletID int) error { - wallet := mw.WalletWithID(walletID) + wallet, _ := mw.WalletWithID(walletID, "DCR") if wallet == nil { return errors.New(ErrNotExist) } @@ -143,7 +145,7 @@ func (mw *MultiWallet) WalletWithXPub(xpub string) (int, error) { return -1, err } for _, account := range accounts.Accounts { - if account.AccountNumber == ImportedAccountNumber { + if account.AccountNumber == dcr.ImportedAccountNumber { continue } acctXPub, err := w.Internal().AccountXpub(ctx, account.AccountNumber) @@ -166,7 +168,7 @@ func (mw *MultiWallet) WalletWithSeed(seedMnemonic string) (int, error) { return -1, errors.New(ErrEmptySeed) } - newSeedLegacyXPUb, newSeedSLIP0044XPUb, err := deriveBIP44AccountXPubs(seedMnemonic, DefaultAccountNum, mw.chainParams) + newSeedLegacyXPUb, newSeedSLIP0044XPUb, err := deriveBIP44AccountXPubs(seedMnemonic, dcr.DefaultAccountNum, mw.chainParams) if err != nil { return -1, err } @@ -180,7 +182,7 @@ func (mw *MultiWallet) WalletWithSeed(seedMnemonic string) (int, error) { // incorrect result from the check below. But this would return true // if the watch-only wallet was created using the xpub of the default // account of the provided seed. - usesSameSeed, err := wallet.AccountXPubMatches(DefaultAccountNum, newSeedLegacyXPUb, newSeedSLIP0044XPUb) + usesSameSeed, err := wallet.AccountXPubMatches(dcr.DefaultAccountNum, newSeedLegacyXPUb, newSeedSLIP0044XPUb) if err != nil { return -1, err } diff --git a/rescan.go b/rescan.go deleted file mode 100644 index d6babc19f..000000000 --- a/rescan.go +++ /dev/null @@ -1,146 +0,0 @@ -package dcrlibwallet - -import ( - "context" - "math" - "time" - - "decred.org/dcrwallet/v2/errors" - w "decred.org/dcrwallet/v2/wallet" -) - -func (mw *MultiWallet) RescanBlocks(walletID int) error { - return mw.RescanBlocksFromHeight(walletID, 0) -} - -func (mw *MultiWallet) RescanBlocksFromHeight(walletID int, startHeight int32) error { - - wallet := mw.WalletWithID(walletID) - if wallet == nil { - return errors.E(ErrNotExist) - } - - netBackend, err := wallet.Internal().NetworkBackend() - if err != nil { - return errors.E(ErrNotConnected) - } - - if mw.IsRescanning() || !mw.IsSynced() { - return errors.E(ErrInvalid) - } - - go func() { - defer func() { - mw.syncData.mu.Lock() - mw.syncData.rescanning = false - mw.syncData.cancelRescan = nil - mw.syncData.mu.Unlock() - }() - - ctx, cancel := wallet.shutdownContextWithCancel() - - mw.syncData.mu.Lock() - mw.syncData.rescanning = true - mw.syncData.cancelRescan = cancel - mw.syncData.mu.Unlock() - - if mw.blocksRescanProgressListener != nil { - mw.blocksRescanProgressListener.OnBlocksRescanStarted(walletID) - } - - progress := make(chan w.RescanProgress, 1) - go wallet.Internal().RescanProgressFromHeight(ctx, netBackend, startHeight, progress) - - rescanStartTime := time.Now().Unix() - - for p := range progress { - if p.Err != nil { - log.Error(p.Err) - if mw.blocksRescanProgressListener != nil { - mw.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, p.Err) - } - return - } - - rescanProgressReport := &HeadersRescanProgressReport{ - CurrentRescanHeight: p.ScannedThrough, - TotalHeadersToScan: wallet.GetBestBlock(), - WalletID: walletID, - } - - elapsedRescanTime := time.Now().Unix() - rescanStartTime - rescanRate := float64(p.ScannedThrough) / float64(rescanProgressReport.TotalHeadersToScan) - - rescanProgressReport.RescanProgress = int32(math.Round(rescanRate * 100)) - estimatedTotalRescanTime := int64(math.Round(float64(elapsedRescanTime) / rescanRate)) - rescanProgressReport.RescanTimeRemaining = estimatedTotalRescanTime - elapsedRescanTime - - rescanProgressReport.GeneralSyncProgress = &GeneralSyncProgress{ - TotalSyncProgress: rescanProgressReport.RescanProgress, - TotalTimeRemainingSeconds: rescanProgressReport.RescanTimeRemaining, - } - - if mw.blocksRescanProgressListener != nil { - mw.blocksRescanProgressListener.OnBlocksRescanProgress(rescanProgressReport) - } - - select { - case <-ctx.Done(): - log.Info("Rescan canceled through context") - - if mw.blocksRescanProgressListener != nil { - if ctx.Err() != nil && ctx.Err() != context.Canceled { - mw.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, ctx.Err()) - } else { - mw.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, nil) - } - } - - return - default: - continue - } - } - - var err error - if startHeight == 0 { - err = wallet.reindexTransactions() - } else { - err = wallet.walletDataDB.SaveLastIndexPoint(startHeight) - if err != nil { - if mw.blocksRescanProgressListener != nil { - mw.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, err) - } - return - } - - err = wallet.IndexTransactions() - } - if mw.blocksRescanProgressListener != nil { - mw.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, err) - } - }() - - return nil -} - -func (mw *MultiWallet) CancelRescan() { - mw.syncData.mu.Lock() - defer mw.syncData.mu.Unlock() - if mw.syncData.cancelRescan != nil { - mw.syncData.cancelRescan() - mw.syncData.cancelRescan = nil - - log.Info("Rescan canceled.") - } -} - -func (mw *MultiWallet) IsRescanning() bool { - mw.syncData.mu.RLock() - defer mw.syncData.mu.RUnlock() - return mw.syncData.rescanning -} - -func (mw *MultiWallet) SetBlocksRescanProgressListener(blocksRescanProgressListener BlocksRescanProgressListener) { - mw.blocksRescanProgressListener = blocksRescanProgressListener -} diff --git a/sync.go b/sync.go deleted file mode 100644 index 18cfcd09a..000000000 --- a/sync.go +++ /dev/null @@ -1,493 +0,0 @@ -package dcrlibwallet - -import ( - "context" - "encoding/json" - "fmt" - "net" - "sort" - "strings" - "sync" - - "decred.org/dcrwallet/v2/errors" - "decred.org/dcrwallet/v2/p2p" - w "decred.org/dcrwallet/v2/wallet" - "github.com/decred/dcrd/addrmgr/v2" - "github.com/planetdecred/dcrlibwallet/spv" -) - -// reading/writing of properties of this struct are protected by mutex.x -type syncData struct { - mu sync.RWMutex - - syncProgressListeners map[string]SyncProgressListener - showLogs bool - - synced bool - syncing bool - cancelSync context.CancelFunc - cancelRescan context.CancelFunc - syncCanceled chan struct{} - - // Flag to notify syncCanceled callback if the sync was canceled so as to be restarted. - restartSyncRequested bool - - rescanning bool - connectedPeers int32 - - *activeSyncData -} - -// reading/writing of properties of this struct are protected by syncData.mu. -type activeSyncData struct { - syncer *spv.Syncer - - syncStage int32 - - cfiltersFetchProgress CFiltersFetchProgressReport - headersFetchProgress HeadersFetchProgressReport - addressDiscoveryProgress AddressDiscoveryProgressReport - headersRescanProgress HeadersRescanProgressReport - - addressDiscoveryCompletedOrCanceled chan bool - - rescanStartTime int64 - - totalInactiveSeconds int64 -} - -const ( - InvalidSyncStage = -1 - CFiltersFetchSyncStage = 0 - HeadersFetchSyncStage = 1 - AddressDiscoverySyncStage = 2 - HeadersRescanSyncStage = 3 -) - -func (mw *MultiWallet) initActiveSyncData() { - - cfiltersFetchProgress := CFiltersFetchProgressReport{ - GeneralSyncProgress: &GeneralSyncProgress{}, - beginFetchCFiltersTimeStamp: 0, - startCFiltersHeight: -1, - cfiltersFetchTimeSpent: 0, - totalFetchedCFiltersCount: 0, - } - - headersFetchProgress := HeadersFetchProgressReport{ - GeneralSyncProgress: &GeneralSyncProgress{}, - beginFetchTimeStamp: -1, - headersFetchTimeSpent: -1, - totalFetchedHeadersCount: 0, - } - - addressDiscoveryProgress := AddressDiscoveryProgressReport{ - GeneralSyncProgress: &GeneralSyncProgress{}, - addressDiscoveryStartTime: -1, - totalDiscoveryTimeSpent: -1, - } - - headersRescanProgress := HeadersRescanProgressReport{} - headersRescanProgress.GeneralSyncProgress = &GeneralSyncProgress{} - - mw.syncData.mu.Lock() - mw.syncData.activeSyncData = &activeSyncData{ - syncStage: InvalidSyncStage, - - cfiltersFetchProgress: cfiltersFetchProgress, - headersFetchProgress: headersFetchProgress, - addressDiscoveryProgress: addressDiscoveryProgress, - headersRescanProgress: headersRescanProgress, - } - mw.syncData.mu.Unlock() -} - -func (mw *MultiWallet) IsSyncProgressListenerRegisteredFor(uniqueIdentifier string) bool { - mw.syncData.mu.RLock() - _, exists := mw.syncData.syncProgressListeners[uniqueIdentifier] - mw.syncData.mu.RUnlock() - return exists -} - -func (mw *MultiWallet) AddSyncProgressListener(syncProgressListener SyncProgressListener, uniqueIdentifier string) error { - if mw.IsSyncProgressListenerRegisteredFor(uniqueIdentifier) { - return errors.New(ErrListenerAlreadyExist) - } - - mw.syncData.mu.Lock() - mw.syncData.syncProgressListeners[uniqueIdentifier] = syncProgressListener - mw.syncData.mu.Unlock() - - // If sync is already on, notify this newly added listener of the current progress report. - return mw.PublishLastSyncProgress(uniqueIdentifier) -} - -func (mw *MultiWallet) RemoveSyncProgressListener(uniqueIdentifier string) { - mw.syncData.mu.Lock() - delete(mw.syncData.syncProgressListeners, uniqueIdentifier) - mw.syncData.mu.Unlock() -} - -func (mw *MultiWallet) syncProgressListeners() []SyncProgressListener { - mw.syncData.mu.RLock() - defer mw.syncData.mu.RUnlock() - - listeners := make([]SyncProgressListener, 0, len(mw.syncData.syncProgressListeners)) - for _, listener := range mw.syncData.syncProgressListeners { - listeners = append(listeners, listener) - } - - return listeners -} - -func (mw *MultiWallet) PublishLastSyncProgress(uniqueIdentifier string) error { - mw.syncData.mu.RLock() - defer mw.syncData.mu.RUnlock() - - syncProgressListener, exists := mw.syncData.syncProgressListeners[uniqueIdentifier] - if !exists { - return errors.New(ErrInvalid) - } - - if mw.syncData.syncing && mw.syncData.activeSyncData != nil { - switch mw.syncData.activeSyncData.syncStage { - case HeadersFetchSyncStage: - syncProgressListener.OnHeadersFetchProgress(&mw.syncData.headersFetchProgress) - case AddressDiscoverySyncStage: - syncProgressListener.OnAddressDiscoveryProgress(&mw.syncData.addressDiscoveryProgress) - case HeadersRescanSyncStage: - syncProgressListener.OnHeadersRescanProgress(&mw.syncData.headersRescanProgress) - } - } - - return nil -} - -func (mw *MultiWallet) EnableSyncLogs() { - mw.syncData.mu.Lock() - mw.syncData.showLogs = true - mw.syncData.mu.Unlock() -} - -func (mw *MultiWallet) SyncInactiveForPeriod(totalInactiveSeconds int64) { - mw.syncData.mu.Lock() - defer mw.syncData.mu.Unlock() - - if !mw.syncData.syncing || mw.syncData.activeSyncData == nil { - log.Debug("Not accounting for inactive time, wallet is not syncing.") - return - } - - mw.syncData.totalInactiveSeconds += totalInactiveSeconds - if mw.syncData.connectedPeers == 0 { - // assume it would take another 60 seconds to reconnect to peers - mw.syncData.totalInactiveSeconds += 60 - } -} - -func (mw *MultiWallet) SpvSync() error { - // prevent an attempt to sync when the previous syncing has not been canceled - if mw.IsSyncing() || mw.IsSynced() { - return errors.New(ErrSyncAlreadyInProgress) - } - - addr := &net.TCPAddr{IP: net.ParseIP("::1"), Port: 0} - addrManager := addrmgr.New(mw.rootDir, net.LookupIP) // TODO: be mindful of tor - lp := p2p.NewLocalPeer(mw.chainParams, addr, addrManager) - - var validPeerAddresses []string - peerAddresses := mw.ReadStringConfigValueForKey(SpvPersistentPeerAddressesConfigKey) - if peerAddresses != "" { - addresses := strings.Split(peerAddresses, ";") - for _, address := range addresses { - peerAddress, err := NormalizeAddress(address, mw.chainParams.DefaultPort) - if err != nil { - log.Errorf("SPV peer address(%s) is invalid: %v", peerAddress, err) - } else { - validPeerAddresses = append(validPeerAddresses, peerAddress) - } - } - - if len(validPeerAddresses) == 0 { - return errors.New(ErrInvalidPeers) - } - } - - // init activeSyncData to be used to hold data used - // to calculate sync estimates only during sync - mw.initActiveSyncData() - - wallets := make(map[int]*w.Wallet) - for id, wallet := range mw.wallets { - wallets[id] = wallet.Internal() - wallet.waitingForHeaders = true - wallet.syncing = true - } - - syncer := spv.NewSyncer(wallets, lp) - syncer.SetNotifications(mw.spvSyncNotificationCallbacks()) - if len(validPeerAddresses) > 0 { - syncer.SetPersistentPeers(validPeerAddresses) - } - - ctx, cancel := mw.contextWithShutdownCancel() - - var restartSyncRequested bool - - mw.syncData.mu.Lock() - restartSyncRequested = mw.syncData.restartSyncRequested - mw.syncData.restartSyncRequested = false - mw.syncData.syncing = true - mw.syncData.cancelSync = cancel - mw.syncData.syncCanceled = make(chan struct{}) - mw.syncData.syncer = syncer - mw.syncData.mu.Unlock() - - for _, listener := range mw.syncProgressListeners() { - listener.OnSyncStarted(restartSyncRequested) - } - - // syncer.Run uses a wait group to block the thread until the sync context - // expires or is canceled or some other error occurs such as - // losing connection to all persistent peers. - go func() { - syncError := syncer.Run(ctx) - //sync has ended or errored - if syncError != nil { - if syncError == context.DeadlineExceeded { - mw.notifySyncError(errors.Errorf("SPV synchronization deadline exceeded: %v", syncError)) - } else if syncError == context.Canceled { - close(mw.syncData.syncCanceled) - mw.notifySyncCanceled() - } else { - mw.notifySyncError(syncError) - } - } - - //reset sync variables - mw.resetSyncData() - }() - return nil -} - -func (mw *MultiWallet) RestartSpvSync() error { - mw.syncData.mu.Lock() - mw.syncData.restartSyncRequested = true - mw.syncData.mu.Unlock() - - mw.CancelSync() // necessary to unset the network backend. - return mw.SpvSync() -} - -func (mw *MultiWallet) CancelSync() { - mw.syncData.mu.RLock() - cancelSync := mw.syncData.cancelSync - mw.syncData.mu.RUnlock() - - if cancelSync != nil { - log.Info("Canceling sync. May take a while for sync to fully cancel.") - - // Stop running cspp mixers - for _, wallet := range mw.wallets { - if wallet.IsAccountMixerActive() { - log.Infof("[%d] Stopping cspp mixer", wallet.ID) - err := mw.StopAccountMixer(wallet.ID) - if err != nil { - log.Errorf("[%d] Error stopping cspp mixer: %v", wallet.ID, err) - } - } - } - - // Cancel the context used for syncer.Run in spvSync(). - // This may not immediately cause the sync process to terminate, - // but when it eventually terminates, syncer.Run will return `err == context.Canceled`. - cancelSync() - - // When sync terminates and syncer.Run returns `err == context.Canceled`, - // we will get notified on this channel. - <-mw.syncData.syncCanceled - - log.Info("Sync fully canceled.") - } -} - -func (wallet *Wallet) IsWaiting() bool { - return wallet.waitingForHeaders -} - -func (wallet *Wallet) IsSynced() bool { - return wallet.synced -} - -func (wallet *Wallet) IsSyncing() bool { - return wallet.syncing -} - -func (mw *MultiWallet) IsConnectedToDecredNetwork() bool { - mw.syncData.mu.RLock() - defer mw.syncData.mu.RUnlock() - return mw.syncData.syncing || mw.syncData.synced -} - -func (mw *MultiWallet) IsSynced() bool { - mw.syncData.mu.RLock() - defer mw.syncData.mu.RUnlock() - return mw.syncData.synced -} - -func (mw *MultiWallet) IsSyncing() bool { - mw.syncData.mu.RLock() - defer mw.syncData.mu.RUnlock() - return mw.syncData.syncing -} - -func (mw *MultiWallet) CurrentSyncStage() int32 { - mw.syncData.mu.RLock() - defer mw.syncData.mu.RUnlock() - - if mw.syncData != nil && mw.syncData.syncing { - return mw.syncData.syncStage - } - return InvalidSyncStage -} - -func (mw *MultiWallet) GeneralSyncProgress() *GeneralSyncProgress { - mw.syncData.mu.RLock() - defer mw.syncData.mu.RUnlock() - - if mw.syncData != nil && mw.syncData.syncing { - switch mw.syncData.syncStage { - case HeadersFetchSyncStage: - return mw.syncData.headersFetchProgress.GeneralSyncProgress - case AddressDiscoverySyncStage: - return mw.syncData.addressDiscoveryProgress.GeneralSyncProgress - case HeadersRescanSyncStage: - return mw.syncData.headersRescanProgress.GeneralSyncProgress - case CFiltersFetchSyncStage: - return mw.syncData.cfiltersFetchProgress.GeneralSyncProgress - } - } - - return nil -} - -func (mw *MultiWallet) ConnectedPeers() int32 { - mw.syncData.mu.RLock() - defer mw.syncData.mu.RUnlock() - return mw.syncData.connectedPeers -} - -func (mw *MultiWallet) PeerInfoRaw() ([]PeerInfo, error) { - if !mw.IsConnectedToDecredNetwork() { - return nil, errors.New(ErrNotConnected) - } - - syncer := mw.syncData.syncer - - infos := make([]PeerInfo, 0, len(syncer.GetRemotePeers())) - for _, rp := range syncer.GetRemotePeers() { - info := PeerInfo{ - ID: int32(rp.ID()), - Addr: rp.RemoteAddr().String(), - AddrLocal: rp.LocalAddr().String(), - Services: fmt.Sprintf("%08d", uint64(rp.Services())), - Version: rp.Pver(), - SubVer: rp.UA(), - StartingHeight: int64(rp.InitialHeight()), - BanScore: int32(rp.BanScore()), - } - - infos = append(infos, info) - } - - sort.Slice(infos, func(i, j int) bool { - return infos[i].ID < infos[j].ID - }) - - return infos, nil -} - -func (mw *MultiWallet) PeerInfo() (string, error) { - infos, err := mw.PeerInfoRaw() - if err != nil { - return "", err - } - - result, _ := json.Marshal(infos) - return string(result), nil -} - -func (mw *MultiWallet) GetBestBlock() *BlockInfo { - var bestBlock int32 = -1 - var blockInfo *BlockInfo - for _, wallet := range mw.wallets { - if !wallet.WalletOpened() { - continue - } - - walletBestBLock := wallet.GetBestBlock() - if walletBestBLock > bestBlock || bestBlock == -1 { - bestBlock = walletBestBLock - blockInfo = &BlockInfo{Height: bestBlock, Timestamp: wallet.GetBestBlockTimeStamp()} - } - } - - return blockInfo -} - -func (mw *MultiWallet) GetLowestBlock() *BlockInfo { - var lowestBlock int32 = -1 - var blockInfo *BlockInfo - for _, wallet := range mw.wallets { - if !wallet.WalletOpened() { - continue - } - walletBestBLock := wallet.GetBestBlock() - if walletBestBLock < lowestBlock || lowestBlock == -1 { - lowestBlock = walletBestBLock - blockInfo = &BlockInfo{Height: lowestBlock, Timestamp: wallet.GetBestBlockTimeStamp()} - } - } - - return blockInfo -} - -func (wallet *Wallet) GetBestBlock() int32 { - if wallet.Internal() == nil { - // This method is sometimes called after a wallet is deleted and causes crash. - log.Error("Attempting to read best block height without a loaded wallet.") - return 0 - } - - _, height := wallet.Internal().MainChainTip(wallet.shutdownContext()) - return height -} - -func (wallet *Wallet) GetBestBlockTimeStamp() int64 { - if wallet.Internal() == nil { - // This method is sometimes called after a wallet is deleted and causes crash. - log.Error("Attempting to read best block timestamp without a loaded wallet.") - return 0 - } - - ctx := wallet.shutdownContext() - _, height := wallet.Internal().MainChainTip(ctx) - identifier := w.NewBlockIdentifierFromHeight(height) - info, err := wallet.Internal().BlockInfo(ctx, identifier) - if err != nil { - log.Error(err) - return 0 - } - return info.Timestamp -} - -func (mw *MultiWallet) GetLowestBlockTimestamp() int64 { - var timestamp int64 = -1 - for _, wallet := range mw.wallets { - bestBlockTimestamp := wallet.GetBestBlockTimeStamp() - if bestBlockTimestamp < timestamp || timestamp == -1 { - timestamp = bestBlockTimestamp - } - } - return timestamp -} diff --git a/syncnotification.go b/syncnotification.go deleted file mode 100644 index 4e4444376..000000000 --- a/syncnotification.go +++ /dev/null @@ -1,686 +0,0 @@ -package dcrlibwallet - -import ( - "math" - "time" - - "github.com/planetdecred/dcrlibwallet/spv" - "golang.org/x/sync/errgroup" -) - -func (mw *MultiWallet) spvSyncNotificationCallbacks() *spv.Notifications { - return &spv.Notifications{ - PeerConnected: func(peerCount int32, addr string) { - mw.handlePeerCountUpdate(peerCount) - }, - PeerDisconnected: func(peerCount int32, addr string) { - mw.handlePeerCountUpdate(peerCount) - }, - Synced: mw.synced, - FetchHeadersStarted: mw.fetchHeadersStarted, - FetchHeadersProgress: mw.fetchHeadersProgress, - FetchHeadersFinished: mw.fetchHeadersFinished, - FetchMissingCFiltersStarted: mw.fetchCFiltersStarted, - FetchMissingCFiltersProgress: mw.fetchCFiltersProgress, - FetchMissingCFiltersFinished: mw.fetchCFiltersEnded, - DiscoverAddressesStarted: mw.discoverAddressesStarted, - DiscoverAddressesFinished: mw.discoverAddressesFinished, - RescanStarted: mw.rescanStarted, - RescanProgress: mw.rescanProgress, - RescanFinished: mw.rescanFinished, - } -} - -func (mw *MultiWallet) handlePeerCountUpdate(peerCount int32) { - mw.syncData.mu.Lock() - mw.syncData.connectedPeers = peerCount - shouldLog := mw.syncData.showLogs && mw.syncData.syncing - mw.syncData.mu.Unlock() - - for _, syncProgressListener := range mw.syncProgressListeners() { - syncProgressListener.OnPeerConnectedOrDisconnected(peerCount) - } - - if shouldLog { - if peerCount == 1 { - log.Infof("Connected to %d peer on %s.", peerCount, mw.chainParams.Name) - } else { - log.Infof("Connected to %d peers on %s.", peerCount, mw.chainParams.Name) - } - } -} - -// Fetch CFilters Callbacks - -func (mw *MultiWallet) fetchCFiltersStarted(walletID int) { - mw.syncData.mu.Lock() - mw.syncData.activeSyncData.syncStage = CFiltersFetchSyncStage - mw.syncData.activeSyncData.cfiltersFetchProgress.beginFetchCFiltersTimeStamp = time.Now().Unix() - mw.syncData.activeSyncData.cfiltersFetchProgress.totalFetchedCFiltersCount = 0 - showLogs := mw.syncData.showLogs - mw.syncData.mu.Unlock() - - if showLogs { - log.Infof("Step 1 of 3 - fetching %d block headers.") - } -} - -func (mw *MultiWallet) fetchCFiltersProgress(walletID int, startCFiltersHeight, endCFiltersHeight int32) { - - // lock the mutex before reading and writing to mw.syncData.* - mw.syncData.mu.Lock() - - if mw.syncData.activeSyncData.cfiltersFetchProgress.startCFiltersHeight == -1 { - mw.syncData.activeSyncData.cfiltersFetchProgress.startCFiltersHeight = startCFiltersHeight - } - - wallet := mw.WalletWithID(walletID) - mw.syncData.activeSyncData.cfiltersFetchProgress.totalFetchedCFiltersCount += endCFiltersHeight - startCFiltersHeight - - totalCFiltersToFetch := wallet.GetBestBlock() - mw.syncData.activeSyncData.cfiltersFetchProgress.startCFiltersHeight - // cfiltersLeftToFetch := totalCFiltersToFetch - mw.syncData.activeSyncData.cfiltersFetchProgress.totalFetchedCFiltersCount - - cfiltersFetchProgress := float64(mw.syncData.activeSyncData.cfiltersFetchProgress.totalFetchedCFiltersCount) / float64(totalCFiltersToFetch) - - // If there was some period of inactivity, - // assume that this process started at some point in the future, - // thereby accounting for the total reported time of inactivity. - mw.syncData.activeSyncData.cfiltersFetchProgress.beginFetchCFiltersTimeStamp += mw.syncData.activeSyncData.totalInactiveSeconds - mw.syncData.activeSyncData.totalInactiveSeconds = 0 - - timeTakenSoFar := time.Now().Unix() - mw.syncData.activeSyncData.cfiltersFetchProgress.beginFetchCFiltersTimeStamp - if timeTakenSoFar < 1 { - timeTakenSoFar = 1 - } - estimatedTotalCFiltersFetchTime := float64(timeTakenSoFar) / cfiltersFetchProgress - - // Use CFilters fetch rate to estimate headers fetch time. - cfiltersFetchRate := float64(mw.syncData.activeSyncData.cfiltersFetchProgress.totalFetchedCFiltersCount) / float64(timeTakenSoFar) - estimatedHeadersLeftToFetch := mw.estimateBlockHeadersCountAfter(wallet.GetBestBlockTimeStamp()) - estimatedTotalHeadersFetchTime := float64(estimatedHeadersLeftToFetch) / cfiltersFetchRate - // increase estimated value by FetchPercentage - estimatedTotalHeadersFetchTime /= FetchPercentage - - estimatedDiscoveryTime := estimatedTotalHeadersFetchTime * DiscoveryPercentage - estimatedRescanTime := estimatedTotalHeadersFetchTime * RescanPercentage - estimatedTotalSyncTime := estimatedTotalCFiltersFetchTime + estimatedTotalHeadersFetchTime + estimatedDiscoveryTime + estimatedRescanTime - - totalSyncProgress := float64(timeTakenSoFar) / estimatedTotalSyncTime - totalTimeRemainingSeconds := int64(math.Round(estimatedTotalSyncTime)) - timeTakenSoFar - - // update headers fetching progress report including total progress percentage and total time remaining - mw.syncData.activeSyncData.cfiltersFetchProgress.TotalCFiltersToFetch = totalCFiltersToFetch - mw.syncData.activeSyncData.cfiltersFetchProgress.CurrentCFilterHeight = startCFiltersHeight - mw.syncData.activeSyncData.cfiltersFetchProgress.CFiltersFetchProgress = roundUp(cfiltersFetchProgress * 100.0) - mw.syncData.activeSyncData.cfiltersFetchProgress.TotalSyncProgress = roundUp(totalSyncProgress * 100.0) - mw.syncData.activeSyncData.cfiltersFetchProgress.TotalTimeRemainingSeconds = totalTimeRemainingSeconds - - mw.syncData.mu.Unlock() - - // notify progress listener of estimated progress report - mw.publishFetchCFiltersProgress() - - cfiltersFetchTimeRemaining := estimatedTotalCFiltersFetchTime - float64(timeTakenSoFar) - debugInfo := &DebugInfo{ - timeTakenSoFar, - totalTimeRemainingSeconds, - timeTakenSoFar, - int64(math.Round(cfiltersFetchTimeRemaining)), - } - mw.publishDebugInfo(debugInfo) -} - -func (mw *MultiWallet) publishFetchCFiltersProgress() { - for _, syncProgressListener := range mw.syncProgressListeners() { - syncProgressListener.OnCFiltersFetchProgress(&mw.syncData.cfiltersFetchProgress) - } -} - -func (mw *MultiWallet) fetchCFiltersEnded(walletID int) { - mw.syncData.mu.Lock() - defer mw.syncData.mu.Unlock() - - mw.syncData.activeSyncData.cfiltersFetchProgress.cfiltersFetchTimeSpent = time.Now().Unix() - mw.syncData.cfiltersFetchProgress.beginFetchCFiltersTimeStamp - - // If there is some period of inactivity reported at this stage, - // subtract it from the total stage time. - mw.syncData.activeSyncData.cfiltersFetchProgress.cfiltersFetchTimeSpent -= mw.syncData.totalInactiveSeconds - mw.syncData.activeSyncData.totalInactiveSeconds = 0 -} - -// Fetch Headers Callbacks - -func (mw *MultiWallet) fetchHeadersStarted(peerInitialHeight int32) { - if !mw.IsSyncing() { - return - } - - mw.syncData.mu.RLock() - headersFetchingStarted := mw.syncData.headersFetchProgress.beginFetchTimeStamp != -1 - showLogs := mw.syncData.showLogs - mw.syncData.mu.RUnlock() - - if headersFetchingStarted { - // This function gets called for each newly connected peer so - // ignore if headers fetching was already started. - return - } - - for _, wallet := range mw.wallets { - wallet.waitingForHeaders = true - } - - lowestBlockHeight := mw.GetLowestBlock().Height - - mw.syncData.mu.Lock() - mw.syncData.activeSyncData.syncStage = HeadersFetchSyncStage - mw.syncData.activeSyncData.headersFetchProgress.beginFetchTimeStamp = time.Now().Unix() - mw.syncData.activeSyncData.headersFetchProgress.startHeaderHeight = lowestBlockHeight - mw.syncData.headersFetchProgress.totalFetchedHeadersCount = 0 - mw.syncData.activeSyncData.totalInactiveSeconds = 0 - mw.syncData.mu.Unlock() - - if showLogs { - log.Infof("Step 1 of 3 - fetching %d block headers.", peerInitialHeight-lowestBlockHeight) - } -} - -func (mw *MultiWallet) fetchHeadersProgress(lastFetchedHeaderHeight int32, lastFetchedHeaderTime int64) { - if !mw.IsSyncing() { - return - } - - mw.syncData.mu.RLock() - headersFetchingCompleted := mw.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent != -1 - mw.syncData.mu.RUnlock() - - if headersFetchingCompleted { - // This function gets called for each newly connected peer so ignore - // this call if the headers fetching phase was previously completed. - return - } - - for _, wallet := range mw.wallets { - if wallet.waitingForHeaders { - wallet.waitingForHeaders = wallet.GetBestBlock() > lastFetchedHeaderHeight - } - } - - // lock the mutex before reading and writing to mw.syncData.* - mw.syncData.mu.Lock() - - if lastFetchedHeaderHeight > mw.syncData.activeSyncData.headersFetchProgress.startHeaderHeight { - mw.syncData.activeSyncData.headersFetchProgress.totalFetchedHeadersCount = lastFetchedHeaderHeight - mw.syncData.activeSyncData.headersFetchProgress.startHeaderHeight - } - - headersLeftToFetch := mw.estimateBlockHeadersCountAfter(lastFetchedHeaderTime) - totalHeadersToFetch := lastFetchedHeaderHeight + headersLeftToFetch - headersFetchProgress := float64(mw.syncData.activeSyncData.headersFetchProgress.totalFetchedHeadersCount) / float64(totalHeadersToFetch) - - // If there was some period of inactivity, - // assume that this process started at some point in the future, - // thereby accounting for the total reported time of inactivity. - mw.syncData.activeSyncData.headersFetchProgress.beginFetchTimeStamp += mw.syncData.activeSyncData.totalInactiveSeconds - mw.syncData.activeSyncData.totalInactiveSeconds = 0 - - fetchTimeTakenSoFar := time.Now().Unix() - mw.syncData.activeSyncData.headersFetchProgress.beginFetchTimeStamp - if fetchTimeTakenSoFar < 1 { - fetchTimeTakenSoFar = 1 - } - estimatedTotalHeadersFetchTime := float64(fetchTimeTakenSoFar) / headersFetchProgress - - // For some reason, the actual total headers fetch time is more than the predicted/estimated time. - // Account for this difference by multiplying the estimatedTotalHeadersFetchTime by an incrementing factor. - // The incrementing factor is inversely proportional to the headers fetch progress, - // ranging from 0.5 to 0 as headers fetching progress increases from 0 to 1. - // todo, the above noted (mal)calculation may explain this difference. - // TODO: is this adjustment still needed since the calculation has been corrected. - adjustmentFactor := 0.5 * (1 - headersFetchProgress) - estimatedTotalHeadersFetchTime += estimatedTotalHeadersFetchTime * adjustmentFactor - - estimatedDiscoveryTime := estimatedTotalHeadersFetchTime * DiscoveryPercentage - estimatedRescanTime := estimatedTotalHeadersFetchTime * RescanPercentage - estimatedTotalSyncTime := float64(mw.syncData.activeSyncData.cfiltersFetchProgress.cfiltersFetchTimeSpent) + - estimatedTotalHeadersFetchTime + estimatedDiscoveryTime + estimatedRescanTime - - totalSyncProgress := float64(fetchTimeTakenSoFar) / estimatedTotalSyncTime - totalTimeRemainingSeconds := int64(math.Round(estimatedTotalSyncTime)) - fetchTimeTakenSoFar - - // update headers fetching progress report including total progress percentage and total time remaining - mw.syncData.activeSyncData.headersFetchProgress.TotalHeadersToFetch = totalHeadersToFetch - mw.syncData.activeSyncData.headersFetchProgress.CurrentHeaderHeight = lastFetchedHeaderHeight - mw.syncData.activeSyncData.headersFetchProgress.CurrentHeaderTimestamp = lastFetchedHeaderTime - mw.syncData.activeSyncData.headersFetchProgress.HeadersFetchProgress = roundUp(headersFetchProgress * 100.0) - mw.syncData.activeSyncData.headersFetchProgress.TotalSyncProgress = roundUp(totalSyncProgress * 100.0) - mw.syncData.activeSyncData.headersFetchProgress.TotalTimeRemainingSeconds = totalTimeRemainingSeconds - - // unlock the mutex before issuing notification callbacks to prevent potential deadlock - // if any invoked callback takes a considerable amount of time to execute. - mw.syncData.mu.Unlock() - - // notify progress listener of estimated progress report - mw.publishFetchHeadersProgress() - - // todo: also log report if showLog == true - timeTakenSoFar := mw.syncData.activeSyncData.cfiltersFetchProgress.cfiltersFetchTimeSpent + fetchTimeTakenSoFar - headersFetchTimeRemaining := estimatedTotalHeadersFetchTime - float64(fetchTimeTakenSoFar) - debugInfo := &DebugInfo{ - timeTakenSoFar, - totalTimeRemainingSeconds, - fetchTimeTakenSoFar, - int64(math.Round(headersFetchTimeRemaining)), - } - mw.publishDebugInfo(debugInfo) -} - -func (mw *MultiWallet) publishFetchHeadersProgress() { - for _, syncProgressListener := range mw.syncProgressListeners() { - syncProgressListener.OnHeadersFetchProgress(&mw.syncData.headersFetchProgress) - } -} - -func (mw *MultiWallet) fetchHeadersFinished() { - mw.syncData.mu.Lock() - defer mw.syncData.mu.Unlock() - - if !mw.syncData.syncing { - // ignore if sync is not in progress - return - } - - mw.syncData.activeSyncData.headersFetchProgress.startHeaderHeight = -1 - mw.syncData.headersFetchProgress.totalFetchedHeadersCount = 0 - mw.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent = time.Now().Unix() - mw.syncData.headersFetchProgress.beginFetchTimeStamp - - // If there is some period of inactivity reported at this stage, - // subtract it from the total stage time. - mw.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent -= mw.syncData.totalInactiveSeconds - mw.syncData.activeSyncData.totalInactiveSeconds = 0 - - if mw.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent < 150 { - // This ensures that minimum ETA used for stage 2 (address discovery) is 120 seconds (80% of 150 seconds). - mw.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent = 150 - } - - if mw.syncData.showLogs && mw.syncData.syncing { - log.Info("Fetch headers completed.") - } -} - -// Address/Account Discovery Callbacks - -func (mw *MultiWallet) discoverAddressesStarted(walletID int) { - if !mw.IsSyncing() { - return - } - - mw.syncData.mu.RLock() - addressDiscoveryAlreadyStarted := mw.syncData.activeSyncData.addressDiscoveryProgress.addressDiscoveryStartTime != -1 - totalHeadersFetchTime := float64(mw.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent) - mw.syncData.mu.RUnlock() - - if addressDiscoveryAlreadyStarted { - return - } - - mw.syncData.mu.Lock() - mw.syncData.activeSyncData.syncStage = AddressDiscoverySyncStage - mw.syncData.activeSyncData.addressDiscoveryProgress.addressDiscoveryStartTime = time.Now().Unix() - mw.syncData.activeSyncData.addressDiscoveryProgress.WalletID = walletID - mw.syncData.addressDiscoveryCompletedOrCanceled = make(chan bool) - mw.syncData.mu.Unlock() - - go mw.updateAddressDiscoveryProgress(totalHeadersFetchTime) - - if mw.syncData.showLogs { - log.Info("Step 2 of 3 - discovering used addresses.") - } -} - -func (mw *MultiWallet) updateAddressDiscoveryProgress(totalHeadersFetchTime float64) { - // use ticker to calculate and broadcast address discovery progress every second - everySecondTicker := time.NewTicker(1 * time.Second) - - // these values will be used every second to calculate the total sync progress - estimatedDiscoveryTime := totalHeadersFetchTime * DiscoveryPercentage - estimatedRescanTime := totalHeadersFetchTime * RescanPercentage - - // track last logged time remaining and total percent to avoid re-logging same message - var lastTimeRemaining int64 - var lastTotalPercent int32 = -1 - - for { - if !mw.IsSyncing() { - return - } - - // If there was some period of inactivity, - // assume that this process started at some point in the future, - // thereby accounting for the total reported time of inactivity. - mw.syncData.mu.Lock() - mw.syncData.addressDiscoveryProgress.addressDiscoveryStartTime += mw.syncData.totalInactiveSeconds - mw.syncData.totalInactiveSeconds = 0 - addressDiscoveryStartTime := mw.syncData.addressDiscoveryProgress.addressDiscoveryStartTime - totalCfiltersFetchTime := float64(mw.syncData.cfiltersFetchProgress.cfiltersFetchTimeSpent) - showLogs := mw.syncData.showLogs - mw.syncData.mu.Unlock() - - select { - case <-mw.syncData.addressDiscoveryCompletedOrCanceled: - // stop calculating and broadcasting address discovery progress - everySecondTicker.Stop() - if showLogs { - log.Info("Address discovery complete.") - } - return - - case <-everySecondTicker.C: - // calculate address discovery progress - elapsedDiscoveryTime := float64(time.Now().Unix() - addressDiscoveryStartTime) - discoveryProgress := (elapsedDiscoveryTime / estimatedDiscoveryTime) * 100 - - var totalSyncTime float64 - if elapsedDiscoveryTime > estimatedDiscoveryTime { - totalSyncTime = totalCfiltersFetchTime + totalHeadersFetchTime + elapsedDiscoveryTime + estimatedRescanTime - } else { - totalSyncTime = totalCfiltersFetchTime + totalHeadersFetchTime + estimatedDiscoveryTime + estimatedRescanTime - } - - totalElapsedTime := totalCfiltersFetchTime + totalHeadersFetchTime + elapsedDiscoveryTime - totalProgress := (totalElapsedTime / totalSyncTime) * 100 - - remainingAccountDiscoveryTime := math.Round(estimatedDiscoveryTime - elapsedDiscoveryTime) - if remainingAccountDiscoveryTime < 0 { - remainingAccountDiscoveryTime = 0 - } - - totalProgressPercent := int32(math.Round(totalProgress)) - totalTimeRemainingSeconds := int64(math.Round(remainingAccountDiscoveryTime + estimatedRescanTime)) - - // update address discovery progress, total progress and total time remaining - mw.syncData.mu.Lock() - mw.syncData.addressDiscoveryProgress.AddressDiscoveryProgress = int32(math.Round(discoveryProgress)) - mw.syncData.addressDiscoveryProgress.TotalSyncProgress = totalProgressPercent - mw.syncData.addressDiscoveryProgress.TotalTimeRemainingSeconds = totalTimeRemainingSeconds - mw.syncData.mu.Unlock() - - mw.publishAddressDiscoveryProgress() - - debugInfo := &DebugInfo{ - int64(math.Round(totalElapsedTime)), - totalTimeRemainingSeconds, - int64(math.Round(elapsedDiscoveryTime)), - int64(math.Round(remainingAccountDiscoveryTime)), - } - mw.publishDebugInfo(debugInfo) - - if showLogs { - // avoid logging same message multiple times - if totalProgressPercent != lastTotalPercent || totalTimeRemainingSeconds != lastTimeRemaining { - log.Infof("Syncing %d%%, %s remaining, discovering used addresses.", - totalProgressPercent, CalculateTotalTimeRemaining(totalTimeRemainingSeconds)) - - lastTotalPercent = totalProgressPercent - lastTimeRemaining = totalTimeRemainingSeconds - } - } - } - } -} - -func (mw *MultiWallet) publishAddressDiscoveryProgress() { - for _, syncProgressListener := range mw.syncProgressListeners() { - syncProgressListener.OnAddressDiscoveryProgress(&mw.syncData.activeSyncData.addressDiscoveryProgress) - } -} - -func (mw *MultiWallet) discoverAddressesFinished(walletID int) { - if !mw.IsSyncing() { - return - } - - mw.stopUpdatingAddressDiscoveryProgress() -} - -func (mw *MultiWallet) stopUpdatingAddressDiscoveryProgress() { - mw.syncData.mu.Lock() - if mw.syncData.activeSyncData != nil && mw.syncData.activeSyncData.addressDiscoveryCompletedOrCanceled != nil { - close(mw.syncData.activeSyncData.addressDiscoveryCompletedOrCanceled) - mw.syncData.activeSyncData.addressDiscoveryCompletedOrCanceled = nil - mw.syncData.activeSyncData.addressDiscoveryProgress.totalDiscoveryTimeSpent = time.Now().Unix() - mw.syncData.addressDiscoveryProgress.addressDiscoveryStartTime - } - mw.syncData.mu.Unlock() -} - -// Blocks Scan Callbacks - -func (mw *MultiWallet) rescanStarted(walletID int) { - mw.stopUpdatingAddressDiscoveryProgress() - - mw.syncData.mu.Lock() - defer mw.syncData.mu.Unlock() - - if !mw.syncData.syncing { - // ignore if sync is not in progress - return - } - - mw.syncData.activeSyncData.syncStage = HeadersRescanSyncStage - mw.syncData.activeSyncData.rescanStartTime = time.Now().Unix() - - // retain last total progress report from address discovery phase - mw.syncData.activeSyncData.headersRescanProgress.TotalTimeRemainingSeconds = mw.syncData.activeSyncData.addressDiscoveryProgress.TotalTimeRemainingSeconds - mw.syncData.activeSyncData.headersRescanProgress.TotalSyncProgress = mw.syncData.activeSyncData.addressDiscoveryProgress.TotalSyncProgress - mw.syncData.activeSyncData.headersRescanProgress.WalletID = walletID - - if mw.syncData.showLogs && mw.syncData.syncing { - log.Info("Step 3 of 3 - Scanning block headers.") - } -} - -func (mw *MultiWallet) rescanProgress(walletID int, rescannedThrough int32) { - if !mw.IsSyncing() { - // ignore if sync is not in progress - return - } - - wallet := mw.wallets[walletID] - totalHeadersToScan := wallet.GetBestBlock() - - rescanRate := float64(rescannedThrough) / float64(totalHeadersToScan) - - mw.syncData.mu.Lock() - - // If there was some period of inactivity, - // assume that this process started at some point in the future, - // thereby accounting for the total reported time of inactivity. - mw.syncData.activeSyncData.rescanStartTime += mw.syncData.activeSyncData.totalInactiveSeconds - mw.syncData.activeSyncData.totalInactiveSeconds = 0 - - elapsedRescanTime := time.Now().Unix() - mw.syncData.activeSyncData.rescanStartTime - estimatedTotalRescanTime := int64(math.Round(float64(elapsedRescanTime) / rescanRate)) - totalTimeRemainingSeconds := estimatedTotalRescanTime - elapsedRescanTime - totalElapsedTime := mw.syncData.activeSyncData.cfiltersFetchProgress.cfiltersFetchTimeSpent + mw.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent + - mw.syncData.activeSyncData.addressDiscoveryProgress.totalDiscoveryTimeSpent + elapsedRescanTime - - mw.syncData.activeSyncData.headersRescanProgress.WalletID = walletID - mw.syncData.activeSyncData.headersRescanProgress.TotalHeadersToScan = totalHeadersToScan - mw.syncData.activeSyncData.headersRescanProgress.RescanProgress = int32(math.Round(rescanRate * 100)) - mw.syncData.activeSyncData.headersRescanProgress.CurrentRescanHeight = rescannedThrough - mw.syncData.activeSyncData.headersRescanProgress.RescanTimeRemaining = totalTimeRemainingSeconds - - // do not update total time taken and total progress percent if elapsedRescanTime is 0 - // because the estimatedTotalRescanTime will be inaccurate (also 0) - // which will make the estimatedTotalSyncTime equal to totalElapsedTime - // giving the wrong impression that the process is complete - if elapsedRescanTime > 0 { - estimatedTotalSyncTime := mw.syncData.activeSyncData.cfiltersFetchProgress.cfiltersFetchTimeSpent + mw.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent + - mw.syncData.activeSyncData.addressDiscoveryProgress.totalDiscoveryTimeSpent + estimatedTotalRescanTime - totalProgress := (float64(totalElapsedTime) / float64(estimatedTotalSyncTime)) * 100 - - mw.syncData.activeSyncData.headersRescanProgress.TotalTimeRemainingSeconds = totalTimeRemainingSeconds - mw.syncData.activeSyncData.headersRescanProgress.TotalSyncProgress = int32(math.Round(totalProgress)) - } - - mw.syncData.mu.Unlock() - - mw.publishHeadersRescanProgress() - - debugInfo := &DebugInfo{ - totalElapsedTime, - totalTimeRemainingSeconds, - elapsedRescanTime, - totalTimeRemainingSeconds, - } - mw.publishDebugInfo(debugInfo) - - mw.syncData.mu.RLock() - if mw.syncData.showLogs { - log.Infof("Syncing %d%%, %s remaining, scanning %d of %d block headers.", - mw.syncData.activeSyncData.headersRescanProgress.TotalSyncProgress, - CalculateTotalTimeRemaining(mw.syncData.activeSyncData.headersRescanProgress.TotalTimeRemainingSeconds), - mw.syncData.activeSyncData.headersRescanProgress.CurrentRescanHeight, - mw.syncData.activeSyncData.headersRescanProgress.TotalHeadersToScan, - ) - } - mw.syncData.mu.RUnlock() -} - -func (mw *MultiWallet) publishHeadersRescanProgress() { - for _, syncProgressListener := range mw.syncProgressListeners() { - syncProgressListener.OnHeadersRescanProgress(&mw.syncData.activeSyncData.headersRescanProgress) - } -} - -func (mw *MultiWallet) rescanFinished(walletID int) { - if !mw.IsSyncing() { - // ignore if sync is not in progress - return - } - - mw.syncData.mu.Lock() - mw.syncData.activeSyncData.headersRescanProgress.WalletID = walletID - mw.syncData.activeSyncData.headersRescanProgress.TotalTimeRemainingSeconds = 0 - mw.syncData.activeSyncData.headersRescanProgress.TotalSyncProgress = 100 - - // Reset these value so that address discovery would - // not be skipped for the next wallet. - mw.syncData.activeSyncData.addressDiscoveryProgress.addressDiscoveryStartTime = -1 - mw.syncData.activeSyncData.addressDiscoveryProgress.totalDiscoveryTimeSpent = -1 - mw.syncData.mu.Unlock() - - mw.publishHeadersRescanProgress() -} - -func (mw *MultiWallet) publishDebugInfo(debugInfo *DebugInfo) { - for _, syncProgressListener := range mw.syncProgressListeners() { - syncProgressListener.Debug(debugInfo) - } -} - -/** Helper functions start here */ - -func (mw *MultiWallet) estimateBlockHeadersCountAfter(lastHeaderTime int64) int32 { - // Use the difference between current time (now) and last reported block time, - // to estimate total headers to fetch. - timeDifferenceInSeconds := float64(time.Now().Unix() - lastHeaderTime) - targetTimePerBlockInSeconds := mw.chainParams.TargetTimePerBlock.Seconds() - estimatedHeadersDifference := timeDifferenceInSeconds / targetTimePerBlockInSeconds - - // return next integer value (upper limit) if estimatedHeadersDifference is a fraction - return int32(math.Ceil(estimatedHeadersDifference)) -} - -func (mw *MultiWallet) notifySyncError(err error) { - for _, syncProgressListener := range mw.syncProgressListeners() { - syncProgressListener.OnSyncEndedWithError(err) - } -} - -func (mw *MultiWallet) notifySyncCanceled() { - mw.syncData.mu.RLock() - restartSyncRequested := mw.syncData.restartSyncRequested - mw.syncData.mu.RUnlock() - - for _, syncProgressListener := range mw.syncProgressListeners() { - syncProgressListener.OnSyncCanceled(restartSyncRequested) - } -} - -func (mw *MultiWallet) resetSyncData() { - // It's possible that sync ends or errors while address discovery is ongoing. - // If this happens, it's important to stop the address discovery process before - // resetting sync data. - mw.stopUpdatingAddressDiscoveryProgress() - - mw.syncData.mu.Lock() - mw.syncData.syncing = false - mw.syncData.synced = false - mw.syncData.cancelSync = nil - mw.syncData.syncCanceled = nil - mw.syncData.activeSyncData = nil - mw.syncData.mu.Unlock() - - for _, wallet := range mw.wallets { - wallet.waitingForHeaders = true - wallet.LockWallet() // lock wallet if previously unlocked to perform account discovery. - } -} - -func (mw *MultiWallet) synced(walletID int, synced bool) { - - indexTransactions := func() { - // begin indexing transactions after sync is completed, - // syncProgressListeners.OnSynced() will be invoked after transactions are indexed - var txIndexing errgroup.Group - for _, wallet := range mw.wallets { - txIndexing.Go(wallet.IndexTransactions) - } - - go func() { - err := txIndexing.Wait() - if err != nil { - log.Errorf("Tx Index Error: %v", err) - } - - for _, syncProgressListener := range mw.syncProgressListeners() { - if synced { - syncProgressListener.OnSyncCompleted() - } else { - syncProgressListener.OnSyncCanceled(false) - } - } - }() - } - - mw.syncData.mu.RLock() - allWalletsSynced := mw.syncData.synced - mw.syncData.mu.RUnlock() - - if allWalletsSynced && synced { - indexTransactions() - return - } - - wallet := mw.wallets[walletID] - wallet.synced = synced - wallet.syncing = false - mw.listenForTransactions(wallet.ID) - - if !wallet.Internal().Locked() { - wallet.LockWallet() // lock wallet if previously unlocked to perform account discovery. - err := mw.markWalletAsDiscoveredAccounts(walletID) - if err != nil { - log.Error(err) - } - } - - if mw.OpenedWalletsCount() == mw.SyncedWalletsCount() { - mw.syncData.mu.Lock() - mw.syncData.syncing = false - mw.syncData.synced = true - mw.syncData.mu.Unlock() - - indexTransactions() - } -} diff --git a/treasury.go b/treasury.go deleted file mode 100644 index cd78ec46b..000000000 --- a/treasury.go +++ /dev/null @@ -1,194 +0,0 @@ -package dcrlibwallet - -import ( - "encoding/hex" - "fmt" - - "decred.org/dcrwallet/v2/errors" - - "github.com/decred/dcrd/blockchain/stake/v4" - "github.com/decred/dcrd/chaincfg/chainhash" - "github.com/decred/dcrd/dcrec/secp256k1/v4" -) - -// SetTreasuryPolicy saves the voting policy for treasury spends by a particular -// PI key. -// If a ticket hash is provided, the voting policy is also updated with the VSP -// controlling the ticket. If a ticket hash isn't provided, the vote choice is -// saved to the local wallet database and the VSPs controlling all unspent, -// unexpired tickets are updated to use the specified vote policy. -func (wallet *Wallet) SetTreasuryPolicy(PiKey, newVotingPolicy, tixHash string, passphrase []byte) error { - var ticketHash *chainhash.Hash - if tixHash != "" { - tixHash, err := chainhash.NewHashFromStr(tixHash) - if err != nil { - return fmt.Errorf("invalid ticket hash: %w", err) - } - ticketHash = tixHash - } - - pikey, err := hex.DecodeString(PiKey) - if err != nil { - return fmt.Errorf("invalid pikey: %w", err) - } - if len(pikey) != secp256k1.PubKeyBytesLenCompressed { - return fmt.Errorf("treasury pikey must be %d bytes", secp256k1.PubKeyBytesLenCompressed) - } - - var policy stake.TreasuryVoteT - switch newVotingPolicy { - case "abstain", "invalid", "": - policy = stake.TreasuryVoteInvalid - case "yes": - policy = stake.TreasuryVoteYes - case "no": - policy = stake.TreasuryVoteNo - default: - return fmt.Errorf("invalid policy: unknown policy %q", newVotingPolicy) - } - - // The wallet will need to be unlocked to sign the API - // request(s) for setting this voting policy with the VSP. - err = wallet.UnlockWallet(passphrase) - if err != nil { - return translateError(err) - } - defer wallet.LockWallet() - - currentVotingPolicy := wallet.Internal().TreasuryKeyPolicy(pikey, ticketHash) - - ctx := wallet.shutdownContext() - - err = wallet.Internal().SetTreasuryKeyPolicy(ctx, pikey, policy, ticketHash) - if err != nil { - return err - } - - var vspPreferenceUpdateSuccess bool - defer func() { - if !vspPreferenceUpdateSuccess { - // Updating the treasury spend voting preference with the vsp failed, - // revert the locally saved voting preference for the treasury spend. - revertError := wallet.Internal().SetTreasuryKeyPolicy(ctx, pikey, currentVotingPolicy, ticketHash) - if revertError != nil { - log.Errorf("unable to revert locally saved voting preference: %v", revertError) - } - } - }() - - // If a ticket hash is provided, set the specified vote policy with - // the VSP associated with the provided ticket. Otherwise, set the - // vote policy with the VSPs associated with all "votable" tickets. - ticketHashes := make([]*chainhash.Hash, 0) - if ticketHash != nil { - ticketHashes = append(ticketHashes, ticketHash) - } else { - err = wallet.Internal().ForUnspentUnexpiredTickets(ctx, func(hash *chainhash.Hash) error { - ticketHashes = append(ticketHashes, hash) - return nil - }) - if err != nil { - return fmt.Errorf("unable to fetch hashes for all unspent, unexpired tickets: %v", err) - } - } - - // Never return errors from this for loop, so all tickets are tried. - // The first error will be returned to the caller. - var firstErr error - // Update voting preferences on VSPs if required. - policyMap := map[string]string{ - PiKey: newVotingPolicy, - } - for _, tHash := range ticketHashes { - vspTicketInfo, err := wallet.Internal().VSPTicketInfo(ctx, tHash) - if err != nil { - // Ignore NotExist error, just means the ticket is not - // registered with a VSP, nothing more to do here. - if firstErr == nil && !errors.Is(err, errors.NotExist) { - firstErr = err - } - continue // try next tHash - } - - // Update the vote policy for the ticket with the associated VSP. - vspClient, err := wallet.VSPClient(vspTicketInfo.Host, vspTicketInfo.PubKey) - if err != nil && firstErr == nil { - firstErr = err - continue // try next tHash - } - err = vspClient.SetVoteChoice(ctx, tHash, nil, nil, policyMap) - if err != nil && firstErr == nil { - firstErr = err - continue // try next tHash - } - } - - vspPreferenceUpdateSuccess = firstErr == nil - return firstErr -} - -// TreasuryPolicies returns saved voting policies for treasury spends -// per pi key. If a pi key is specified, the policy for that pi key -// is returned; otherwise the policies for all pi keys are returned. -// If a ticket hash is provided, the policy(ies) for that ticket -// is/are returned. -func (wallet *Wallet) TreasuryPolicies(PiKey, tixHash string) ([]*TreasuryKeyPolicy, error) { - var ticketHash *chainhash.Hash - if tixHash != "" { - tixHash, err := chainhash.NewHashFromStr(tixHash) - if err != nil { - return nil, fmt.Errorf("inavlid hash: %w", err) - } - ticketHash = tixHash - } - - if PiKey != "" { - pikey, err := hex.DecodeString(PiKey) - if err != nil { - return nil, fmt.Errorf("invalid pikey: %w", err) - } - var policy string - switch wallet.Internal().TreasuryKeyPolicy(pikey, ticketHash) { - case stake.TreasuryVoteYes: - policy = "yes" - case stake.TreasuryVoteNo: - policy = "no" - default: - policy = "abstain" - } - res := []*TreasuryKeyPolicy{ - { - TicketHash: tixHash, - PiKey: PiKey, - Policy: policy, - }, - } - return res, nil - } - - policies := wallet.Internal().TreasuryKeyPolicies() - res := make([]*TreasuryKeyPolicy, len(policies)) - for i := range policies { - var policy string - switch policies[i].Policy { - case stake.TreasuryVoteYes: - policy = "yes" - case stake.TreasuryVoteNo: - policy = "no" - } - r := &TreasuryKeyPolicy{ - PiKey: hex.EncodeToString(policies[i].PiKey), - Policy: policy, - } - if policies[i].Ticket != nil { - r.TicketHash = policies[i].Ticket.String() - } - res[i] = r - } - return res, nil -} - -// PiKeys returns the sanctioned Politeia keys for the current network. -func (mw *MultiWallet) PiKeys() [][]byte { - return mw.chainParams.PiKeys -} diff --git a/txandblocknotifications.go b/txandblocknotifications.go deleted file mode 100644 index dd55166a9..000000000 --- a/txandblocknotifications.go +++ /dev/null @@ -1,159 +0,0 @@ -package dcrlibwallet - -import ( - "encoding/json" - - "decred.org/dcrwallet/v2/errors" -) - -func (mw *MultiWallet) listenForTransactions(walletID int) { - go func() { - - wallet := mw.wallets[walletID] - n := wallet.Internal().NtfnServer.TransactionNotifications() - - for { - select { - case v := <-n.C: - if v == nil { - return - } - for _, transaction := range v.UnminedTransactions { - tempTransaction, err := wallet.decodeTransactionWithTxSummary(&transaction, nil) - if err != nil { - log.Errorf("[%d] Error ntfn parse tx: %v", wallet.ID, err) - return - } - - overwritten, err := wallet.walletDataDB.SaveOrUpdate(&Transaction{}, tempTransaction) - if err != nil { - log.Errorf("[%d] New Tx save err: %v", wallet.ID, err) - return - } - - if !overwritten { - log.Infof("[%d] New Transaction %s", wallet.ID, tempTransaction.Hash) - - result, err := json.Marshal(tempTransaction) - if err != nil { - log.Error(err) - } else { - mw.mempoolTransactionNotification(string(result)) - } - } - } - - for _, block := range v.AttachedBlocks { - blockHash := block.Header.BlockHash() - for _, transaction := range block.Transactions { - tempTransaction, err := wallet.decodeTransactionWithTxSummary(&transaction, &blockHash) - if err != nil { - log.Errorf("[%d] Error ntfn parse tx: %v", wallet.ID, err) - return - } - - _, err = wallet.walletDataDB.SaveOrUpdate(&Transaction{}, tempTransaction) - if err != nil { - log.Errorf("[%d] Incoming block replace tx error :%v", wallet.ID, err) - return - } - mw.publishTransactionConfirmed(wallet.ID, transaction.Hash.String(), int32(block.Header.Height)) - } - - mw.publishBlockAttached(wallet.ID, int32(block.Header.Height)) - } - - if len(v.AttachedBlocks) > 0 { - mw.checkWalletMixers() - } - - case <-mw.syncData.syncCanceled: - n.Done() - } - } - }() -} - -// AddTxAndBlockNotificationListener registers a set of functions to be invoked -// when a transaction or block update is processed by the wallet. If async is -// true, the provided callback methods will be called from separate goroutines, -// allowing notification senders to continue their operation without waiting -// for the listener to complete processing the notification. This asyncrhonous -// handling is especially important for cases where the wallet process that -// sends the notification temporarily prevents access to other wallet features -// until all notification handlers finish processing the notification. If a -// notification handler were to try to access such features, it would result -// in a deadlock. -func (mw *MultiWallet) AddTxAndBlockNotificationListener(txAndBlockNotificationListener TxAndBlockNotificationListener, async bool, uniqueIdentifier string) error { - mw.notificationListenersMu.Lock() - defer mw.notificationListenersMu.Unlock() - - _, ok := mw.txAndBlockNotificationListeners[uniqueIdentifier] - if ok { - return errors.New(ErrListenerAlreadyExist) - } - - if async { - mw.txAndBlockNotificationListeners[uniqueIdentifier] = &asyncTxAndBlockNotificationListener{ - l: txAndBlockNotificationListener, - } - } else { - mw.txAndBlockNotificationListeners[uniqueIdentifier] = txAndBlockNotificationListener - } - - return nil -} - -func (mw *MultiWallet) RemoveTxAndBlockNotificationListener(uniqueIdentifier string) { - mw.notificationListenersMu.Lock() - defer mw.notificationListenersMu.Unlock() - - delete(mw.txAndBlockNotificationListeners, uniqueIdentifier) -} - -func (mw *MultiWallet) checkWalletMixers() { - for _, wallet := range mw.wallets { - if wallet.IsAccountMixerActive() { - unmixedAccount := wallet.ReadInt32ConfigValueForKey(AccountMixerUnmixedAccount, -1) - hasMixableOutput, err := wallet.accountHasMixableOutput(unmixedAccount) - if err != nil { - log.Errorf("Error checking for mixable outputs: %v", err) - } - - if !hasMixableOutput { - log.Infof("[%d] unmixed account does not have a mixable output, stopping account mixer", wallet.ID) - err = mw.StopAccountMixer(wallet.ID) - if err != nil { - log.Errorf("Error stopping account mixer: %v", err) - } - } - } - } -} - -func (mw *MultiWallet) mempoolTransactionNotification(transaction string) { - mw.notificationListenersMu.RLock() - defer mw.notificationListenersMu.RUnlock() - - for _, txAndBlockNotifcationListener := range mw.txAndBlockNotificationListeners { - txAndBlockNotifcationListener.OnTransaction(transaction) - } -} - -func (mw *MultiWallet) publishTransactionConfirmed(walletID int, transactionHash string, blockHeight int32) { - mw.notificationListenersMu.RLock() - defer mw.notificationListenersMu.RUnlock() - - for _, txAndBlockNotifcationListener := range mw.txAndBlockNotificationListeners { - txAndBlockNotifcationListener.OnTransactionConfirmed(walletID, transactionHash, blockHeight) - } -} - -func (mw *MultiWallet) publishBlockAttached(walletID int, blockHeight int32) { - mw.notificationListenersMu.RLock() - defer mw.notificationListenersMu.RUnlock() - - for _, txAndBlockNotifcationListener := range mw.txAndBlockNotificationListeners { - txAndBlockNotifcationListener.OnBlockAttached(walletID, blockHeight) - } -} diff --git a/types.go b/types.go index 899ff1015..bfaecbb9a 100644 --- a/types.go +++ b/types.go @@ -36,10 +36,10 @@ type CSPPConfig struct { ChangeAccount uint32 } -type WalletsIterator struct { - currentIndex int - wallets []*Wallet -} +// type WalletsIterator struct { +// currentIndex int +// wallets []*Wallet +// } type BlockInfo struct { Height int32 diff --git a/utils.go b/utils.go index 9e4068919..5f968244a 100644 --- a/utils.go +++ b/utils.go @@ -26,6 +26,7 @@ import ( "github.com/decred/dcrd/hdkeychain/v3" "github.com/decred/dcrd/wire" "github.com/planetdecred/dcrlibwallet/internal/loader" + "github.com/planetdecred/dcrlibwallet/wallets/dcr" ) const ( @@ -63,15 +64,6 @@ func (mw *MultiWallet) RequiredConfirmations() int32 { return DefaultRequiredConfirmations } -func (wallet *Wallet) RequiredConfirmations() int32 { - var spendUnconfirmed bool - wallet.readUserConfigValue(true, SpendUnconfirmedConfigKey, &spendUnconfirmed) - if spendUnconfirmed { - return 0 - } - return DefaultRequiredConfirmations -} - func (mw *MultiWallet) listenForShutdown() { mw.cancelFuncs = make([]context.CancelFunc, 0) @@ -84,17 +76,6 @@ func (mw *MultiWallet) listenForShutdown() { }() } -func (wallet *Wallet) shutdownContextWithCancel() (context.Context, context.CancelFunc) { - ctx, cancel := context.WithCancel(context.Background()) - wallet.cancelFuncs = append(wallet.cancelFuncs, cancel) - return ctx, cancel -} - -func (wallet *Wallet) shutdownContext() (ctx context.Context) { - ctx, _ = wallet.shutdownContextWithCancel() - return -} - func (mw *MultiWallet) contextWithShutdownCancel() (context.Context, context.CancelFunc) { ctx, cancel := context.WithCancel(context.Background()) mw.cancelFuncs = append(mw.cancelFuncs, cancel) @@ -207,12 +188,12 @@ func ShannonEntropy(text string) (entropy float64) { func TransactionDirectionName(direction int32) string { switch direction { - case TxDirectionSent: - return "Sent" - case TxDirectionReceived: - return "Received" - case TxDirectionTransferred: - return "Yourself" + // case TxDirectionSent: + // return "Sent" + // case TxDirectionReceived: + // return "Received" + // case TxDirectionTransferred: + // return "Yourself" default: return "invalid" } @@ -315,7 +296,7 @@ func backupFile(fileName string, suffix int) (newName string, err error) { func initWalletLoader(chainParams *chaincfg.Params, walletDataDir, walletDbDriver string) *loader.Loader { // TODO: Allow users provide values to override these defaults. - cfg := &WalletConfig{ + cfg := &dcr.WalletConfig{ GapLimit: 20, AllowHighFees: false, RelayFee: txrules.DefaultRelayFeePerKb, diff --git a/wallet.go b/wallet.go deleted file mode 100644 index 593b92a1e..000000000 --- a/wallet.go +++ /dev/null @@ -1,322 +0,0 @@ -package dcrlibwallet - -import ( - "context" - "fmt" - "os" - "path/filepath" - "strconv" - "sync" - "time" - - "decred.org/dcrwallet/v2/errors" - w "decred.org/dcrwallet/v2/wallet" - "decred.org/dcrwallet/v2/walletseed" - "github.com/decred/dcrd/chaincfg/v3" - "github.com/planetdecred/dcrlibwallet/internal/loader" - "github.com/planetdecred/dcrlibwallet/internal/vsp" - "github.com/planetdecred/dcrlibwallet/walletdata" -) - -type Wallet struct { - ID int `storm:"id,increment"` - Name string `storm:"unique"` - CreatedAt time.Time `storm:"index"` - DbDriver string - EncryptedSeed []byte - IsRestored bool - HasDiscoveredAccounts bool - PrivatePassphraseType int32 - - chainParams *chaincfg.Params - dataDir string - loader *loader.Loader - walletDataDB *walletdata.DB - - synced bool - syncing bool - waitingForHeaders bool - - shuttingDown chan bool - cancelFuncs []context.CancelFunc - cancelAccountMixer context.CancelFunc - - cancelAutoTicketBuyerMu sync.Mutex - cancelAutoTicketBuyer context.CancelFunc - - vspClientsMu sync.Mutex - vspClients map[string]*vsp.Client - - // setUserConfigValue saves the provided key-value pair to a config database. - // This function is ideally assigned when the `wallet.prepare` method is - // called from a MultiWallet instance. - setUserConfigValue configSaveFn - - // readUserConfigValue returns the previously saved value for the provided - // key from a config database. Returns nil if the key wasn't previously set. - // This function is ideally assigned when the `wallet.prepare` method is - // called from a MultiWallet instance. - readUserConfigValue configReadFn -} - -// prepare gets a wallet ready for use by opening the transactions index database -// and initializing the wallet loader which can be used subsequently to create, -// load and unload the wallet. -func (wallet *Wallet) prepare(rootDir string, chainParams *chaincfg.Params, - setUserConfigValueFn configSaveFn, readUserConfigValueFn configReadFn) (err error) { - - wallet.chainParams = chainParams - wallet.dataDir = filepath.Join(rootDir, strconv.Itoa(wallet.ID)) - wallet.vspClients = make(map[string]*vsp.Client) - wallet.setUserConfigValue = setUserConfigValueFn - wallet.readUserConfigValue = readUserConfigValueFn - - // open database for indexing transactions for faster loading - walletDataDBPath := filepath.Join(wallet.dataDir, walletdata.DbName) - oldTxDBPath := filepath.Join(wallet.dataDir, walletdata.OldDbName) - if exists, _ := fileExists(oldTxDBPath); exists { - moveFile(oldTxDBPath, walletDataDBPath) - } - wallet.walletDataDB, err = walletdata.Initialize(walletDataDBPath, chainParams, &Transaction{}) - if err != nil { - log.Error(err.Error()) - return err - } - - // init loader - wallet.loader = initWalletLoader(wallet.chainParams, wallet.dataDir, wallet.DbDriver) - - // init cancelFuncs slice to hold cancel functions for long running - // operations and start go routine to listen for shutdown signal - wallet.cancelFuncs = make([]context.CancelFunc, 0) - wallet.shuttingDown = make(chan bool) - go func() { - <-wallet.shuttingDown - for _, cancel := range wallet.cancelFuncs { - cancel() - } - }() - - return nil -} - -func (wallet *Wallet) Shutdown() { - // Trigger shuttingDown signal to cancel all contexts created with - // `wallet.shutdownContext()` or `wallet.shutdownContextWithCancel()`. - wallet.shuttingDown <- true - - if _, loaded := wallet.loader.LoadedWallet(); loaded { - err := wallet.loader.UnloadWallet() - if err != nil { - log.Errorf("Failed to close wallet: %v", err) - } else { - log.Info("Closed wallet") - } - } - - if wallet.walletDataDB != nil { - err := wallet.walletDataDB.Close() - if err != nil { - log.Errorf("tx db closed with error: %v", err) - } else { - log.Info("tx db closed successfully") - } - } -} - -// WalletCreationTimeInMillis returns the wallet creation time for new -// wallets. Restored wallets would return an error. -func (wallet *Wallet) WalletCreationTimeInMillis() (int64, error) { - if wallet.IsRestored { - return 0, errors.New(ErrWalletIsRestored) - } - - return wallet.CreatedAt.UnixNano() / int64(time.Millisecond), nil -} - -func (wallet *Wallet) NetType() string { - return wallet.chainParams.Name -} - -func (wallet *Wallet) Internal() *w.Wallet { - lw, _ := wallet.loader.LoadedWallet() - return lw -} - -func (wallet *Wallet) WalletExists() (bool, error) { - return wallet.loader.WalletExists() -} - -func (wallet *Wallet) createWallet(privatePassphrase, seedMnemonic string) error { - log.Info("Creating Wallet") - if len(seedMnemonic) == 0 { - return errors.New(ErrEmptySeed) - } - - pubPass := []byte(w.InsecurePubPassphrase) - privPass := []byte(privatePassphrase) - seed, err := walletseed.DecodeUserInput(seedMnemonic) - if err != nil { - log.Error(err) - return err - } - - _, err = wallet.loader.CreateNewWallet(wallet.shutdownContext(), pubPass, privPass, seed) - if err != nil { - log.Error(err) - return err - } - - log.Info("Created Wallet") - return nil -} - -func (wallet *Wallet) createWatchingOnlyWallet(extendedPublicKey string) error { - pubPass := []byte(w.InsecurePubPassphrase) - - _, err := wallet.loader.CreateWatchingOnlyWallet(wallet.shutdownContext(), extendedPublicKey, pubPass) - if err != nil { - log.Error(err) - return err - } - - log.Info("Created Watching Only Wallet") - return nil -} - -func (wallet *Wallet) IsWatchingOnlyWallet() bool { - if w, ok := wallet.loader.LoadedWallet(); ok { - return w.WatchingOnly() - } - - return false -} - -func (wallet *Wallet) openWallet() error { - pubPass := []byte(w.InsecurePubPassphrase) - - _, err := wallet.loader.OpenExistingWallet(wallet.shutdownContext(), pubPass) - if err != nil { - log.Error(err) - return translateError(err) - } - - return nil -} - -func (wallet *Wallet) WalletOpened() bool { - return wallet.Internal() != nil -} - -func (wallet *Wallet) UnlockWallet(privPass []byte) error { - loadedWallet, ok := wallet.loader.LoadedWallet() - if !ok { - return fmt.Errorf("wallet has not been loaded") - } - - ctx, _ := wallet.shutdownContextWithCancel() - err := loadedWallet.Unlock(ctx, privPass, nil) - if err != nil { - return translateError(err) - } - - return nil -} - -func (wallet *Wallet) LockWallet() { - if wallet.IsAccountMixerActive() { - log.Error("LockWallet ignored due to active account mixer") - return - } - - if !wallet.Internal().Locked() { - wallet.Internal().Lock() - } -} - -func (wallet *Wallet) IsLocked() bool { - return wallet.Internal().Locked() -} - -func (wallet *Wallet) changePrivatePassphrase(oldPass []byte, newPass []byte) error { - defer func() { - for i := range oldPass { - oldPass[i] = 0 - } - - for i := range newPass { - newPass[i] = 0 - } - }() - - err := wallet.Internal().ChangePrivatePassphrase(wallet.shutdownContext(), oldPass, newPass) - if err != nil { - return translateError(err) - } - return nil -} - -func (wallet *Wallet) deleteWallet(privatePassphrase []byte) error { - defer func() { - for i := range privatePassphrase { - privatePassphrase[i] = 0 - } - }() - - if _, loaded := wallet.loader.LoadedWallet(); !loaded { - return errors.New(ErrWalletNotLoaded) - } - - if !wallet.IsWatchingOnlyWallet() { - err := wallet.Internal().Unlock(wallet.shutdownContext(), privatePassphrase, nil) - if err != nil { - return translateError(err) - } - wallet.Internal().Lock() - } - - wallet.Shutdown() - - log.Info("Deleting Wallet") - return os.RemoveAll(wallet.dataDir) -} - -// DecryptSeed decrypts wallet.EncryptedSeed using privatePassphrase -func (wallet *Wallet) DecryptSeed(privatePassphrase []byte) (string, error) { - if wallet.EncryptedSeed == nil { - return "", errors.New(ErrInvalid) - } - - return decryptWalletSeed(privatePassphrase, wallet.EncryptedSeed) -} - -// AccountXPubMatches checks if the xpub of the provided account matches the -// provided legacy or SLIP0044 xpub. While both the legacy and SLIP0044 xpubs -// will be checked for watch-only wallets, other wallets will only check the -// xpub that matches the coin type key used by the wallet. -func (wallet *Wallet) AccountXPubMatches(account uint32, legacyXPub, slip044XPub string) (bool, error) { - ctx := wallet.shutdownContext() - - acctXPubKey, err := wallet.Internal().AccountXpub(ctx, account) - if err != nil { - return false, err - } - acctXPub := acctXPubKey.String() - - if wallet.IsWatchingOnlyWallet() { - // Coin type info isn't saved for watch-only wallets, so check - // against both legacy and SLIP0044 coin types. - return acctXPub == legacyXPub || acctXPub == slip044XPub, nil - } - - cointype, err := wallet.Internal().CoinType(ctx) - if err != nil { - return false, err - } - - if cointype == wallet.chainParams.LegacyCoinType { - return acctXPub == legacyXPub, nil - } else { - return acctXPub == slip044XPub, nil - } -} diff --git a/wallets.go b/wallets.go index c5f97cc7b..7bb87906a 100644 --- a/wallets.go +++ b/wallets.go @@ -1,29 +1,29 @@ package dcrlibwallet -func (mw *MultiWallet) AllWallets() (wallets []*Wallet) { - for _, wallet := range mw.wallets { - wallets = append(wallets, wallet) - } - return wallets -} +// func (mw *MultiWallet) AllWallets() (wallets []*Wallet) { +// for _, wallet := range mw.wallets { +// wallets = append(wallets, wallet) +// } +// return wallets +// } -func (mw *MultiWallet) WalletsIterator() *WalletsIterator { - return &WalletsIterator{ - currentIndex: 0, - wallets: mw.AllWallets(), - } -} +// func (mw *MultiWallet) WalletsIterator() *WalletsIterator { +// return &WalletsIterator{ +// currentIndex: 0, +// wallets: mw.AllWallets(), +// } +// } -func (walletsIterator *WalletsIterator) Next() *Wallet { - if walletsIterator.currentIndex < len(walletsIterator.wallets) { - wallet := walletsIterator.wallets[walletsIterator.currentIndex] - walletsIterator.currentIndex++ - return wallet - } +// func (walletsIterator *WalletsIterator) Next() *Wallet { +// if walletsIterator.currentIndex < len(walletsIterator.wallets) { +// wallet := walletsIterator.wallets[walletsIterator.currentIndex] +// walletsIterator.currentIndex++ +// return wallet +// } - return nil -} +// return nil +// } -func (walletsIterator *WalletsIterator) Reset() { - walletsIterator.currentIndex = 0 -} +// func (walletsIterator *WalletsIterator) Reset() { +// walletsIterator.currentIndex = 0 +// } diff --git a/wallets/btc/errors.go b/wallets/btc/errors.go new file mode 100644 index 000000000..c054ef2e8 --- /dev/null +++ b/wallets/btc/errors.go @@ -0,0 +1,61 @@ +package btc + +import ( + "decred.org/dcrwallet/v2/errors" + "github.com/asdine/storm" +) + +const ( + // Error Codes + ErrInsufficientBalance = "insufficient_balance" + ErrInvalid = "invalid" + ErrWalletLocked = "wallet_locked" + ErrWalletDatabaseInUse = "wallet_db_in_use" + ErrWalletNotLoaded = "wallet_not_loaded" + ErrWalletNotFound = "wallet_not_found" + ErrWalletNameExist = "wallet_name_exists" + ErrReservedWalletName = "wallet_name_reserved" + ErrWalletIsRestored = "wallet_is_restored" + ErrWalletIsWatchOnly = "watch_only_wallet" + ErrUnusableSeed = "unusable_seed" + ErrPassphraseRequired = "passphrase_required" + ErrInvalidPassphrase = "invalid_passphrase" + ErrNotConnected = "not_connected" + ErrExist = "exists" + ErrNotExist = "not_exists" + ErrEmptySeed = "empty_seed" + ErrInvalidAddress = "invalid_address" + ErrInvalidAuth = "invalid_auth" + ErrUnavailable = "unavailable" + ErrContextCanceled = "context_canceled" + ErrFailedPrecondition = "failed_precondition" + ErrSyncAlreadyInProgress = "sync_already_in_progress" + ErrNoPeers = "no_peers" + ErrInvalidPeers = "invalid_peers" + ErrListenerAlreadyExist = "listener_already_exist" + ErrLoggerAlreadyRegistered = "logger_already_registered" + ErrLogRotatorAlreadyInitialized = "log_rotator_already_initialized" + ErrAddressDiscoveryNotDone = "address_discovery_not_done" + ErrChangingPassphrase = "err_changing_passphrase" + ErrSavingWallet = "err_saving_wallet" + ErrIndexOutOfRange = "err_index_out_of_range" + ErrNoMixableOutput = "err_no_mixable_output" + ErrInvalidVoteBit = "err_invalid_vote_bit" +) + +// todo, should update this method to translate more error kinds. +func translateError(err error) error { + if err, ok := err.(*errors.Error); ok { + switch err.Kind { + case errors.InsufficientBalance: + return errors.New(ErrInsufficientBalance) + case errors.NotExist, storm.ErrNotFound: + return errors.New(ErrNotExist) + case errors.Passphrase: + return errors.New(ErrInvalidPassphrase) + case errors.NoPeers: + return errors.New(ErrNoPeers) + } + } + return err +} diff --git a/wallets/btc/wallet.go b/wallets/btc/wallet.go new file mode 100644 index 000000000..e57d2b9fd --- /dev/null +++ b/wallets/btc/wallet.go @@ -0,0 +1,707 @@ +package btc + +import ( + "context" + "encoding/json" + // "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btclog" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/gcs" + "github.com/btcsuite/btcwallet/chain" + // "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcd/btcutil/hdkeychain" + w "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/walletdb" + // "github.com/planetdecred/dcrlibwallet" + "decred.org/dcrwallet/v2/errors" + _ "github.com/btcsuite/btcwallet/walletdb/bdb" // bdb init() registers a driver + "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/decred/slog" + "github.com/jrick/logrotate/rotator" + "github.com/lightninglabs/neutrino" + "github.com/lightninglabs/neutrino/headerfs" + + "github.com/asdine/storm" + // "github.com/asdine/storm/q" +) + +type Wallet struct { + ID int `storm:"id,increment"` + Name string `storm:"unique"` + CreatedAt time.Time `storm:"index"` + dbDriver string + rootDir string + db *storm.DB + EncryptedSeed []byte + IsRestored bool + + cl neutrinoService + neutrinoDB walletdb.DB + chainClient *chain.NeutrinoClient + + dataDir string + cancelFuncs []context.CancelFunc + + Synced bool + + chainParams *chaincfg.Params + loader *w.Loader + log slog.Logger + birthday time.Time + // mw dcrlibwallet.MultiWallet +} + +// neutrinoService is satisfied by *neutrino.ChainService. +type neutrinoService interface { + GetBlockHash(int64) (*chainhash.Hash, error) + BestBlock() (*headerfs.BlockStamp, error) + Peers() []*neutrino.ServerPeer + GetBlockHeight(hash *chainhash.Hash) (int32, error) + GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, error) + GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, options ...neutrino.QueryOption) (*gcs.Filter, error) + GetBlock(blockHash chainhash.Hash, options ...neutrino.QueryOption) (*btcutil.Block, error) + Stop() error +} + +var _ neutrinoService = (*neutrino.ChainService)(nil) + +var ( + walletBirthday time.Time + loggingInited uint32 +) + +const ( + neutrinoDBName = "neutrino.db" + logDirName = "logs" + logFileName = "neutrino.log" +) + +func parseChainParams(net string) (*chaincfg.Params, error) { + switch net { + case "mainnet": + return &chaincfg.MainNetParams, nil + case "testnet3": + return &chaincfg.TestNet3Params, nil + case "regtest", "regnet", "simnet": + return &chaincfg.RegressionNetParams, nil + } + return nil, fmt.Errorf("unknown network ID %v", net) +} + +// logWriter implements an io.Writer that outputs to a rotating log file. +type logWriter struct { + *rotator.Rotator +} + +// logNeutrino initializes logging in the neutrino + wallet packages. Logging +// only has to be initialized once, so an atomic flag is used internally to +// return early on subsequent invocations. +// +// In theory, the the rotating file logger must be Close'd at some point, but +// there are concurrency issues with that since btcd and btcwallet have +// unsupervised goroutines still running after shutdown. So we leave the rotator +// running at the risk of losing some logs. +func logNeutrino(walletDir string) error { + if !atomic.CompareAndSwapUint32(&loggingInited, 0, 1) { + return nil + } + + logSpinner, err := logRotator(walletDir) + if err != nil { + return fmt.Errorf("error initializing log rotator: %w", err) + } + + backendLog := btclog.NewBackend(logWriter{logSpinner}) + + logger := func(name string, lvl btclog.Level) btclog.Logger { + l := backendLog.Logger(name) + l.SetLevel(lvl) + return l + } + + neutrino.UseLogger(logger("NTRNO", btclog.LevelDebug)) + w.UseLogger(logger("BTCW", btclog.LevelInfo)) + wtxmgr.UseLogger(logger("TXMGR", btclog.LevelInfo)) + chain.UseLogger(logger("CHAIN", btclog.LevelInfo)) + + return nil +} + +// logRotator initializes a rotating file logger. +func logRotator(netDir string) (*rotator.Rotator, error) { + const maxLogRolls = 8 + logDir := filepath.Join(netDir, logDirName) + if err := os.MkdirAll(logDir, 0744); err != nil { + return nil, fmt.Errorf("error creating log directory: %w", err) + } + + logFilename := filepath.Join(logDir, logFileName) + return rotator.New(logFilename, 32*1024, false, maxLogRolls) +} +func (wallet *Wallet) RawRequest(method string, params []json.RawMessage) (json.RawMessage, error) { + // Not needed for spv wallet. + return nil, errors.New("RawRequest not available on spv") +} + +// prepare gets a wallet ready for use by opening the transactions index database +// and initializing the wallet loader which can be used subsequently to create, +// load and unload the wallet. +func (wallet *Wallet) Prepare(rootDir string, net string, log slog.Logger) (err error) { + chainParams, err := parseChainParams(net) + if err != nil { + return err + } + + wallet.chainParams = chainParams + wallet.dataDir = filepath.Join(rootDir, strconv.Itoa(wallet.ID)) + wallet.log = log + wallet.loader = w.NewLoader(wallet.chainParams, wallet.dataDir, true, 60*time.Second, 250) + return nil +} + +func (wallet *Wallet) Shutdown(walletDBRef *storm.DB) { + // Trigger shuttingDown signal to cancel all contexts created with + // `wallet.shutdownContext()` or `wallet.shutdownContextWithCancel()`. + // wallet.shuttingDown <- true + + if _, loaded := wallet.loader.LoadedWallet(); loaded { + err := wallet.loader.UnloadWallet() + if err != nil { + // log.Errorf("Failed to close wallet: %v", err) + } else { + // log.Info("Closed wallet") + } + } + + if walletDBRef != nil { + err := walletDBRef.Close() + if err != nil { + // log.Errorf("tx db closed with error: %v", err) + } else { + // log.Info("tx db closed successfully") + } + } +} + +// WalletCreationTimeInMillis returns the wallet creation time for new +// wallets. Restored wallets would return an error. +func (wallet *Wallet) WalletCreationTimeInMillis() (int64, error) { + if wallet.IsRestored { + return 0, errors.New(ErrWalletIsRestored) + } + + return wallet.CreatedAt.UnixNano() / int64(time.Millisecond), nil +} + +func (wallet *Wallet) NetType() string { + return wallet.chainParams.Name +} + +func (wallet *Wallet) Internal() *w.Wallet { + lw, _ := wallet.loader.LoadedWallet() + return lw +} + +func (wallet *Wallet) WalletExists() (bool, error) { + return wallet.loader.WalletExists() +} + +func CreateNewWallet(walletName, privatePassphrase string, privatePassphraseType int32, db *storm.DB, rootDir, dbDriver string, chainParams *chaincfg.Params) (*Wallet, error) { + // seed := "witch collapse practice feed shame open despair" + // encryptedSeed := []byte(seed) + encryptedSeed, err := hdkeychain.GenerateSeed( + hdkeychain.RecommendedSeedLen, + ) + if err != nil { + return nil, err + } + + wallet := &Wallet{ + Name: walletName, + db: db, + dbDriver: dbDriver, + rootDir: rootDir, + chainParams: chainParams, + CreatedAt: time.Now(), + EncryptedSeed: encryptedSeed, + // log: log, + } + + return wallet.saveNewWallet(func() error { + err := wallet.Prepare(wallet.rootDir, "testnet3", wallet.log) + if err != nil { + return err + } + + return wallet.createWallet(privatePassphrase, encryptedSeed) + }) +} + +func (wallet *Wallet) createWallet(privatePassphrase string, seedMnemonic []byte) error { + // log.Info("Creating Wallet") + if len(seedMnemonic) == 0 { + return errors.New("ErrEmptySeed") + } + + pubPass := []byte(w.InsecurePubPassphrase) + privPass := []byte(privatePassphrase) + // seed, err := walletseed.DecodeUserInput(seedMnemonic) + // if err != nil { + // // log.Error(err) + // return err + // } + + _, err := wallet.loader.CreateNewWallet(pubPass, privPass, seedMnemonic, wallet.CreatedAt) + if err != nil { + // log.Error(err) + return err + } + + bailOnWallet := func() { + if err := wallet.loader.UnloadWallet(); err != nil { + fmt.Errorf("Error unloading wallet after createSPVWallet error: %v", err) + } + } + + neutrinoDBPath := filepath.Join(wallet.dataDir, neutrinoDBName) + db, err := walletdb.Create("bdb", neutrinoDBPath, true, 5*time.Second) + if err != nil { + bailOnWallet() + return fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err) + } + if err = db.Close(); err != nil { + bailOnWallet() + return fmt.Errorf("error closing newly created wallet database: %w", err) + } + + if err := wallet.loader.UnloadWallet(); err != nil { + return fmt.Errorf("error unloading wallet: %w", err) + } + + // log.Info("Created Wallet") + return nil +} + +func CreateNewWatchOnlyWallet(walletName string, chainParams *chaincfg.Params) (*Wallet, error) { + wallet := &Wallet{ + Name: walletName, + IsRestored: true, + } + + return wallet.saveNewWallet(func() error { + err := wallet.Prepare(wallet.rootDir, "testnet3", wallet.log) + if err != nil { + return err + } + + return wallet.createWatchingOnlyWallet() + }) +} + +func (wallet *Wallet) createWatchingOnlyWallet() error { + pubPass := []byte(w.InsecurePubPassphrase) + + _, err := wallet.loader.CreateNewWatchingOnlyWallet(pubPass, time.Now()) + if err != nil { + // log.Error(err) + return err + } + + // log.Info("Created Watching Only Wallet") + return nil +} + +func (wallet *Wallet) IsWatchingOnlyWallet() bool { + if _, ok := wallet.loader.LoadedWallet(); ok { + // return w.WatchingOnly() + return false + } + + return false +} + +func (wallet *Wallet) RenameWallet(newName string, walledDbRef *storm.DB) error { + if strings.HasPrefix(newName, "wallet-") { + return errors.E(ErrReservedWalletName) + } + + if exists, err := WalletNameExists(newName, walledDbRef); err != nil { + return translateError(err) + } else if exists { + return errors.New(ErrExist) + } + + wallet.Name = newName + return walledDbRef.Save(wallet) // update WalletName field +} + +func (wallet *Wallet) OpenWallet() error { + pubPass := []byte(w.InsecurePubPassphrase) + + _, err := wallet.loader.OpenExistingWallet(pubPass, false) + if err != nil { + // log.Error(err) + return translateError(err) + } + + return nil +} + +func (wallet *Wallet) WalletOpened() bool { + return wallet.Internal() != nil +} + +func (wallet *Wallet) UnlockWallet(privPass []byte) error { + loadedWallet, ok := wallet.loader.LoadedWallet() + if !ok { + return fmt.Errorf("wallet has not been loaded") + } + + err := loadedWallet.Unlock(privPass, nil) + if err != nil { + return translateError(err) + } + + return nil +} + +func (wallet *Wallet) LockWallet() { + if !wallet.Internal().Locked() { + wallet.Internal().Lock() + } +} + +func (wallet *Wallet) IsLocked() bool { + return wallet.Internal().Locked() +} + +func (wallet *Wallet) ChangePrivatePassphrase(oldPass []byte, newPass []byte) error { + defer func() { + for i := range oldPass { + oldPass[i] = 0 + } + + for i := range newPass { + newPass[i] = 0 + } + }() + + err := wallet.Internal().ChangePrivatePassphrase(oldPass, newPass) + if err != nil { + return translateError(err) + } + return nil +} + +func (wallet *Wallet) DeleteWallet(privatePassphrase []byte) error { + defer func() { + for i := range privatePassphrase { + privatePassphrase[i] = 0 + } + }() + + if _, loaded := wallet.loader.LoadedWallet(); !loaded { + return errors.New(ErrWalletNotLoaded) + } + + if !wallet.IsWatchingOnlyWallet() { + err := wallet.Internal().Unlock(privatePassphrase, nil) + if err != nil { + return translateError(err) + } + wallet.Internal().Lock() + } + + // wallet.Shutdown() + + // log.Info("Deleting Wallet") + return os.RemoveAll(wallet.dataDir) +} + +func (wallet *Wallet) ConnectSPVWallet(ctx context.Context, wg *sync.WaitGroup) (err error) { + return wallet.connect(ctx, wg) +} + +// connect will start the wallet and begin syncing. +func (wallet *Wallet) connect(ctx context.Context, wg *sync.WaitGroup) error { + if err := logNeutrino(wallet.dataDir); err != nil { + return fmt.Errorf("error initializing btcwallet+neutrino logging: %v", err) + } + + err := wallet.startWallet() + if err != nil { + return err + } + + // txNotes := wallet.wallet.txNotifications() + + // Nanny for the caches checkpoints and txBlocks caches. + wg.Add(1) + // go func() { + // defer wg.Done() + // defer wallet.stop() + // defer txNotes.Done() + + // ticker := time.NewTicker(time.Minute * 20) + // defer ticker.Stop() + // expiration := time.Hour * 2 + // for { + // select { + // case <-ticker.C: + // wallet.txBlocksMtx.Lock() + // for txHash, entry := range wallet.txBlocks { + // if time.Since(entry.lastAccess) > expiration { + // delete(wallet.txBlocks, txHash) + // } + // } + // wallet.txBlocksMtx.Unlock() + + // wallet.checkpointMtx.Lock() + // for outPt, check := range wallet.checkpoints { + // if time.Since(check.lastAccess) > expiration { + // delete(wallet.checkpoints, outPt) + // } + // } + // wallet.checkpointMtx.Unlock() + + // case note := <-txNotes.C: + // if len(note.AttachedBlocks) > 0 { + // lastBlock := note.AttachedBlocks[len(note.AttachedBlocks)-1] + // syncTarget := atomic.LoadInt32(&wallet.syncTarget) + + // for ib := range note.AttachedBlocks { + // for _, nt := range note.AttachedBlocks[ib].Transactions { + // wallet.log.Debugf("Block %d contains wallet transaction %v", note.AttachedBlocks[ib].Height, nt.Hash) + // } + // } + + // if syncTarget == 0 || (lastBlock.Height < syncTarget && lastBlock.Height%10_000 != 0) { + // continue + // } + + // select { + // case wallet.tipChan <- &block{ + // hash: *lastBlock.Hash, + // height: int64(lastBlock.Height), + // }: + // default: + // wallet.log.Warnf("tip report channel was blocking") + // } + // } + + // case <-ctx.Done(): + // return + // } + // } + // }() + + return nil +} + +// startWallet initializes the *btcwallet.Wallet and its supporting players and +// starts syncing. +func (wallet *Wallet) startWallet() error { + // timeout and recoverWindow arguments borrowed from btcwallet directly. + wallet.loader = w.NewLoader(wallet.chainParams, wallet.dataDir, true, 60*time.Second, 250) + + exists, err := wallet.loader.WalletExists() + if err != nil { + return fmt.Errorf("error verifying wallet existence: %v", err) + } + if !exists { + return errors.New("wallet not found") + } + + wallet.log.Debug("Starting native BTC wallet...") + btcw, err := wallet.loader.OpenExistingWallet([]byte(w.InsecurePubPassphrase), false) + if err != nil { + return fmt.Errorf("couldn't load wallet: %w", err) + } + + bailOnWallet := func() { + if err := wallet.loader.UnloadWallet(); err != nil { + wallet.log.Errorf("Error unloading wallet: %v", err) + } + } + + neutrinoDBPath := filepath.Join(wallet.dataDir, neutrinoDBName) + wallet.neutrinoDB, err = walletdb.Create("bdb", neutrinoDBPath, true, w.DefaultDBTimeout) + if err != nil { + bailOnWallet() + return fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err) + } + + bailOnWalletAndDB := func() { + if err := wallet.neutrinoDB.Close(); err != nil { + wallet.log.Errorf("Error closing neutrino database: %v", err) + } + bailOnWallet() + } + + // Depending on the network, we add some addpeers or a connect peer. On + // regtest, if the peers haven't been explicitly set, add the simnet harness + // alpha node as an additional peer so we don't have to type it in. On + // mainet and testnet3, add a known reliable persistent peer to be used in + // addition to normal DNS seed-based peer discovery. + var addPeers []string + var connectPeers []string + switch wallet.chainParams.Net { + case wire.MainNet: + addPeers = []string{"cfilters.ssgen.io"} + case wire.TestNet3: + addPeers = []string{"dex-test.ssgen.io"} + case wire.TestNet, wire.SimNet: // plain "wire.TestNet" is regnet! + connectPeers = []string{"localhost:20575"} + } + wallet.log.Debug("Starting neutrino chain service...") + chainService, err := neutrino.NewChainService(neutrino.Config{ + DataDir: wallet.dataDir, + Database: wallet.neutrinoDB, + ChainParams: *wallet.chainParams, + PersistToDisk: true, // keep cfilter headers on disk for efficient rescanning + AddPeers: addPeers, + ConnectPeers: connectPeers, + // WARNING: PublishTransaction currently uses the entire duration + // because if an external bug, but even if the resolved, a typical + // inv/getdata round trip is ~4 seconds, so we set this so neutrino does + // not cancel queries too readily. + BroadcastTimeout: 6 * time.Second, + }) + if err != nil { + bailOnWalletAndDB() + return fmt.Errorf("couldn't create Neutrino ChainService: %v", err) + } + + bailOnEverything := func() { + if err := chainService.Stop(); err != nil { + wallet.log.Errorf("Error closing neutrino chain service: %v", err) + } + bailOnWalletAndDB() + } + + wallet.cl = chainService + wallet.chainClient = chain.NewNeutrinoClient(wallet.chainParams, chainService) + // wallet.wallet = &walletExtender{btcw, wallet.chainParams} + + // oldBday := btcw.Manager.Birthday() + // wdb := btcw.Database() + + // performRescan := wallet.birthday.Before(oldBday) + // if performRescan && !wallet.allowAutomaticRescan { + // bailOnWalletAndDB() + // return errors.New("cannot set earlier birthday while there are active deals") + // } + + // if !oldBday.Equal(wallet.birthday) { + // err = walletdb.Update(wdb, func(dbtx walletdb.ReadWriteTx) error { + // ns := dbtx.ReadWriteBucket(wAddrMgrBkt) + // return btcw.Manager.SetBirthday(ns, wallet.birthday) + // }) + // if err != nil { + // wallet.log.Errorf("Failed to reset wallet manager birthday: %v", err) + // performRescan = false + // } + // } + + // if performRescan { + // wallet.forceRescan() + // } + + if err = wallet.chainClient.Start(); err != nil { // lazily starts connmgr + bailOnEverything() + return fmt.Errorf("couldn't start Neutrino client: %v", err) + } + + wallet.log.Info("Synchronizing wallet with network...") + btcw.SynchronizeRPC(wallet.chainClient) + + return nil +} + +// saveNewWallet performs the following tasks using a db batch operation to ensure +// that db changes are rolled back if any of the steps below return an error. +// +// - saves the initial wallet info to btcWallet.walletsDb to get a wallet id +// - creates a data directory for the wallet using the auto-generated wallet id +// - updates the initial wallet info with name, dataDir (created above), db driver +// and saves the updated info to btcWallet.walletsDb +// - calls the provided `setupWallet` function to perform any necessary creation, +// restoration or linking of the just saved wallet +// +// IFF all the above operations succeed, the wallet info will be persisted to db +// and the wallet will be added to `btcWallet.wallets`. +func (wallet *Wallet) saveNewWallet(setupWallet func() error) (*Wallet, error) { + exists, err := WalletNameExists(wallet.Name, wallet.db) + if err != nil { + return nil, err + } else if exists { + return nil, errors.New(ErrExist) + } + + // if btcWallet.IsConnectedToDecredNetwork() { + // btcWallet.CancelSync() + // defer btcWallet.SpvSync() + // } + + // Perform database save operations in batch transaction + // for automatic rollback if error occurs at any point. + err = wallet.batchDbTransaction(func(db storm.Node) error { + // saving struct to update ID property with an auto-generated value + err := db.Save(wallet) + if err != nil { + return err + } + + walletDataDir := filepath.Join(wallet.dataDir, strconv.Itoa(wallet.ID)) + + dirExists, err := fileExists(walletDataDir) + if err != nil { + return err + } else if dirExists { + _, err := backupFile(walletDataDir, 1) + if err != nil { + return err + } + + // log.Infof("Undocumented file at %s moved to %s", walletDataDir, newDirName) + } + + os.MkdirAll(walletDataDir, os.ModePerm) // create wallet dir + + if wallet.Name == "" { + wallet.Name = "wallet-" + strconv.Itoa(wallet.ID) // wallet-# + } + + wallet.dataDir = walletDataDir + + err = db.Save(wallet) // update database with complete wallet information + if err != nil { + return err + } + + return setupWallet() + + }) + + if err != nil { + return nil, translateError(err) + } + + return wallet, nil +} + +func (wallet *Wallet) DataDir() string { + return wallet.dataDir +} diff --git a/wallets/btc/wallet_utils.go b/wallets/btc/wallet_utils.go new file mode 100644 index 000000000..a7489c992 --- /dev/null +++ b/wallets/btc/wallet_utils.go @@ -0,0 +1,178 @@ +package btc + +import ( + // "context" + "os" + "strconv" + + "decred.org/dcrwallet/v2/errors" + "github.com/asdine/storm" + // "github.com/kevinburke/nacl" + // "github.com/kevinburke/nacl/secretbox" + // "golang.org/x/crypto/scrypt" + + // w "decred.org/dcrwallet/v2/wallet" + + "strings" +) + +// func (wallet *Wallet) markWalletAsDiscoveredAccounts() error { +// if wallet == nil { +// return errors.New(ErrNotExist) +// } + +// log.Infof("Set discovered accounts = true for wallet %d", wallet.ID) +// wallet.HasDiscoveredAccounts = true +// err := wallet.DB.Save(wallet) +// if err != nil { +// return err +// } + +// return nil +// } + +func (wallet *Wallet) batchDbTransaction(dbOp func(node storm.Node) error) (err error) { + dbTx, err := wallet.db.Begin(true) + if err != nil { + return err + } + + // Commit or rollback the transaction after f returns or panics. Do not + // recover from the panic to keep the original stack trace intact. + panicked := true + defer func() { + if panicked || err != nil { + dbTx.Rollback() + return + } + + err = dbTx.Commit() + }() + + err = dbOp(dbTx) + panicked = false + return err +} + +func WalletNameExists(walletName string, walledDbRef *storm.DB) (bool, error) { + if strings.HasPrefix(walletName, "wallet-") { + return false, errors.E(ErrReservedWalletName) + } + + err := walledDbRef.One("Name", walletName, &Wallet{}) + if err == nil { + return true, nil + } else if err != storm.ErrNotFound { + return false, err + } + + return false, nil +} + +// // naclLoadFromPass derives a nacl.Key from pass using scrypt.Key. +// func naclLoadFromPass(pass []byte) (nacl.Key, error) { + +// const N, r, p = 1 << 15, 8, 1 + +// hash, err := scrypt.Key(pass, nil, N, r, p, 32) +// if err != nil { +// return nil, err +// } +// return nacl.Load(EncodeHex(hash)) +// } + +// encryptWalletSeed encrypts the seed with secretbox.EasySeal using pass. +// func encryptWalletSeed(pass []byte, seed string) ([]byte, error) { +// key, err := naclLoadFromPass(pass) +// if err != nil { +// return nil, err +// } +// return secretbox.EasySeal([]byte(seed), key), nil +// } + +// decryptWalletSeed decrypts the encryptedSeed with secretbox.EasyOpen using pass. +// func decryptWalletSeed(pass []byte, encryptedSeed []byte) (string, error) { +// key, err := naclLoadFromPass(pass) +// if err != nil { +// return "", err +// } + +// decryptedSeed, err := secretbox.EasyOpen(encryptedSeed, key) +// if err != nil { +// return "", errors.New(ErrInvalidPassphrase) +// } + +// return string(decryptedSeed), nil +// } + +// func (wallet *Wallet) loadWalletTemporarily(ctx context.Context, walletDataDir, walletPublicPass string, +// onLoaded func(*w.Wallet) error) error { + +// if walletPublicPass == "" { +// walletPublicPass = w.InsecurePubPassphrase +// } + +// // initialize the wallet loader +// walletLoader := initWalletLoader(wallet.chainParams, walletDataDir, wallet.dbDriver) + +// // open the wallet to get ready for temporary use +// wal, err := walletLoader.OpenExistingWallet(ctx, []byte(walletPublicPass)) +// if err != nil { +// return translateError(err) +// } + +// // unload wallet after temporary use +// defer walletLoader.UnloadWallet() + +// if onLoaded != nil { +// return onLoaded(wal) +// } + +// return nil +// } + +func fileExists(filePath string) (bool, error) { + _, err := os.Stat(filePath) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + return true, nil +} + +func backupFile(fileName string, suffix int) (newName string, err error) { + newName = fileName + ".bak" + strconv.Itoa(suffix) + exists, err := fileExists(newName) + if err != nil { + return "", err + } else if exists { + return backupFile(fileName, suffix+1) + } + + err = moveFile(fileName, newName) + if err != nil { + return "", err + } + + return newName, nil +} + +func moveFile(sourcePath, destinationPath string) error { + if exists, _ := fileExists(sourcePath); exists { + return os.Rename(sourcePath, destinationPath) + } + return nil +} + +// func (wallet *Wallet) shutdownContextWithCancel() (context.Context, context.CancelFunc) { +// ctx, cancel := context.WithCancel(context.Background()) +// wallet.cancelFuncs = append(wallet.cancelFuncs, cancel) +// return ctx, cancel +// } + +// func (wallet *Wallet) shutdownContext() (ctx context.Context) { +// ctx, _ = wallet.shutdownContextWithCancel() +// return +// } diff --git a/account_mixer.go b/wallets/dcr/account_mixer.go similarity index 79% rename from account_mixer.go rename to wallets/dcr/account_mixer.go index 957fc5807..196c495e6 100644 --- a/account_mixer.go +++ b/wallets/dcr/account_mixer.go @@ -1,4 +1,4 @@ -package dcrlibwallet +package dcr import ( "context" @@ -23,23 +23,23 @@ const ( MixedAccountBranch = int32(udb.ExternalBranch) ) -func (mw *MultiWallet) AddAccountMixerNotificationListener(accountMixerNotificationListener AccountMixerNotificationListener, uniqueIdentifier string) error { - mw.notificationListenersMu.Lock() - defer mw.notificationListenersMu.Unlock() +func (wallet *Wallet) AddAccountMixerNotificationListener(accountMixerNotificationListener AccountMixerNotificationListener, uniqueIdentifier string) error { + wallet.notificationListenersMu.Lock() + defer wallet.notificationListenersMu.Unlock() - if _, ok := mw.accountMixerNotificationListener[uniqueIdentifier]; ok { + if _, ok := wallet.accountMixerNotificationListener[uniqueIdentifier]; ok { return errors.New(ErrListenerAlreadyExist) } - mw.accountMixerNotificationListener[uniqueIdentifier] = accountMixerNotificationListener + wallet.accountMixerNotificationListener[uniqueIdentifier] = accountMixerNotificationListener return nil } -func (mw *MultiWallet) RemoveAccountMixerNotificationListener(uniqueIdentifier string) { - mw.notificationListenersMu.Lock() - defer mw.notificationListenersMu.Unlock() +func (wallet *Wallet) RemoveAccountMixerNotificationListener(uniqueIdentifier string) { + wallet.notificationListenersMu.Lock() + defer wallet.notificationListenersMu.Unlock() - delete(mw.accountMixerNotificationListener, uniqueIdentifier) + delete(wallet.accountMixerNotificationListener, uniqueIdentifier) } // CreateMixerAccounts creates the two accounts needed for the account mixer. This function @@ -133,8 +133,7 @@ func (wallet *Wallet) ClearMixerConfig() { wallet.SetBoolConfigValueForKey(AccountMixerConfigSet, false) } -func (mw *MultiWallet) ReadyToMix(walletID int) (bool, error) { - wallet := mw.WalletWithID(walletID) +func (wallet *Wallet) ReadyToMix(walletID int) (bool, error) { if wallet == nil { return false, errors.New(ErrNotExist) } @@ -150,12 +149,11 @@ func (mw *MultiWallet) ReadyToMix(walletID int) (bool, error) { } // StartAccountMixer starts the automatic account mixer -func (mw *MultiWallet) StartAccountMixer(walletID int, walletPassphrase string) error { - if !mw.IsConnectedToDecredNetwork() { +func (wallet *Wallet) StartAccountMixer(walletID int, walletPassphrase string) error { + if !wallet.IsConnectedToDecredNetwork() { return errors.New(ErrNotConnected) } - wallet := mw.WalletWithID(walletID) if wallet == nil { return errors.New(ErrNotExist) } @@ -192,20 +190,20 @@ func (mw *MultiWallet) StartAccountMixer(walletID int, walletPassphrase string) go func() { log.Info("Running account mixer") - if mw.accountMixerNotificationListener != nil { - mw.publishAccountMixerStarted(walletID) + if wallet.accountMixerNotificationListener != nil { + wallet.publishAccountMixerStarted(walletID) } - ctx, cancel := mw.contextWithShutdownCancel() - wallet.cancelAccountMixer = cancel + ctx, cancel := wallet.contextWithShutdownCancel() + wallet.CancelAccountMixer = cancel err = tb.Run(ctx, []byte(walletPassphrase)) if err != nil { log.Errorf("AccountMixer instance errored: %v", err) } - wallet.cancelAccountMixer = nil - if mw.accountMixerNotificationListener != nil { - mw.publishAccountMixerEnded(walletID) + wallet.CancelAccountMixer = nil + if wallet.accountMixerNotificationListener != nil { + wallet.publishAccountMixerEnded(walletID) } }() @@ -256,19 +254,17 @@ func (wallet *Wallet) readCSPPConfig() *CSPPConfig { } // StopAccountMixer stops the active account mixer -func (mw *MultiWallet) StopAccountMixer(walletID int) error { - - wallet := mw.WalletWithID(walletID) +func (wallet *Wallet) StopAccountMixer() error { if wallet == nil { return errors.New(ErrNotExist) } - if wallet.cancelAccountMixer == nil { + if wallet.CancelAccountMixer == nil { return errors.New(ErrInvalid) } - wallet.cancelAccountMixer() - wallet.cancelAccountMixer = nil + wallet.CancelAccountMixer() + wallet.CancelAccountMixer = nil return nil } @@ -281,7 +277,7 @@ func (wallet *Wallet) accountHasMixableOutput(accountNumber int32) (bool, error) // fetch all utxos in account to extract details for the utxos selected by user // use targetAmount = 0 to fetch ALL utxos in account - inputDetail, err := wallet.Internal().SelectInputs(wallet.shutdownContext(), dcrutil.Amount(0), policy) + inputDetail, err := wallet.Internal().SelectInputs(wallet.ShutdownContext(), dcrutil.Amount(0), policy) if err != nil { return false, nil } @@ -300,7 +296,7 @@ func (wallet *Wallet) accountHasMixableOutput(accountNumber int32) (bool, error) return hasMixableOutput, nil } - lockedOutpoints, err := wallet.Internal().LockedOutpoints(wallet.shutdownContext(), accountName) + lockedOutpoints, err := wallet.Internal().LockedOutpoints(wallet.ShutdownContext(), accountName) if err != nil { return hasMixableOutput, nil } @@ -312,23 +308,23 @@ func (wallet *Wallet) accountHasMixableOutput(accountNumber int32) (bool, error) // IsAccountMixerActive returns true if account mixer is active func (wallet *Wallet) IsAccountMixerActive() bool { - return wallet.cancelAccountMixer != nil + return wallet.CancelAccountMixer != nil } -func (mw *MultiWallet) publishAccountMixerStarted(walletID int) { - mw.notificationListenersMu.RLock() - defer mw.notificationListenersMu.RUnlock() +func (wallet *Wallet) publishAccountMixerStarted(walletID int) { + wallet.notificationListenersMu.RLock() + defer wallet.notificationListenersMu.RUnlock() - for _, accountMixerNotificationListener := range mw.accountMixerNotificationListener { + for _, accountMixerNotificationListener := range wallet.accountMixerNotificationListener { accountMixerNotificationListener.OnAccountMixerStarted(walletID) } } -func (mw *MultiWallet) publishAccountMixerEnded(walletID int) { - mw.notificationListenersMu.RLock() - defer mw.notificationListenersMu.RUnlock() +func (wallet *Wallet) publishAccountMixerEnded(walletID int) { + wallet.notificationListenersMu.RLock() + defer wallet.notificationListenersMu.RUnlock() - for _, accountMixerNotificationListener := range mw.accountMixerNotificationListener { + for _, accountMixerNotificationListener := range wallet.accountMixerNotificationListener { accountMixerNotificationListener.OnAccountMixerEnded(walletID) } } diff --git a/accounts.go b/wallets/dcr/accounts.go similarity index 89% rename from accounts.go rename to wallets/dcr/accounts.go index 4fe3e2624..129dc19d1 100644 --- a/accounts.go +++ b/wallets/dcr/accounts.go @@ -1,4 +1,4 @@ -package dcrlibwallet +package dcr import ( "encoding/json" @@ -31,7 +31,7 @@ func (wallet *Wallet) GetAccounts() (string, error) { } func (wallet *Wallet) GetAccountsRaw() (*Accounts, error) { - resp, err := wallet.Internal().Accounts(wallet.shutdownContext()) + resp, err := wallet.Internal().Accounts(wallet.ShutdownContext()) if err != nil { return nil, err } @@ -105,7 +105,7 @@ func (wallet *Wallet) GetAccount(accountNumber int32) (*Account, error) { } func (wallet *Wallet) GetAccountBalance(accountNumber int32) (*Balance, error) { - balance, err := wallet.Internal().AccountBalance(wallet.shutdownContext(), uint32(accountNumber), wallet.RequiredConfirmations()) + balance, err := wallet.Internal().AccountBalance(wallet.ShutdownContext(), uint32(accountNumber), wallet.RequiredConfirmations()) if err != nil { return nil, err } @@ -122,7 +122,7 @@ func (wallet *Wallet) GetAccountBalance(accountNumber int32) (*Balance, error) { } func (wallet *Wallet) SpendableForAccount(account int32) (int64, error) { - bals, err := wallet.Internal().AccountBalance(wallet.shutdownContext(), uint32(account), wallet.RequiredConfirmations()) + bals, err := wallet.Internal().AccountBalance(wallet.ShutdownContext(), uint32(account), wallet.RequiredConfirmations()) if err != nil { log.Error(err) return 0, translateError(err) @@ -138,7 +138,7 @@ func (wallet *Wallet) UnspentOutputs(account int32) ([]*UnspentOutput, error) { // fetch all utxos in account to extract details for the utxos selected by user // use targetAmount = 0 to fetch ALL utxos in account - inputDetail, err := wallet.Internal().SelectInputs(wallet.shutdownContext(), dcrutil.Amount(0), policy) + inputDetail, err := wallet.Internal().SelectInputs(wallet.ShutdownContext(), dcrutil.Amount(0), policy) if err != nil { return nil, err @@ -147,7 +147,7 @@ func (wallet *Wallet) UnspentOutputs(account int32) ([]*UnspentOutput, error) { unspentOutputs := make([]*UnspentOutput, len(inputDetail.Inputs)) for i, input := range inputDetail.Inputs { - outputInfo, err := wallet.Internal().OutputInfo(wallet.shutdownContext(), &input.PreviousOutPoint) + outputInfo, err := wallet.Internal().OutputInfo(wallet.ShutdownContext(), &input.PreviousOutPoint) if err != nil { return nil, err } @@ -160,7 +160,7 @@ func (wallet *Wallet) UnspentOutputs(account int32) ([]*UnspentOutput, error) { var confirmations int32 inputBlockHeight := int32(input.BlockHeight) if inputBlockHeight != -1 { - confirmations = wallet.GetBestBlock() - inputBlockHeight + 1 + confirmations = wallet.getBestBlock() - inputBlockHeight + 1 } unspentOutputs[i] = &UnspentOutput{ @@ -197,7 +197,7 @@ func (wallet *Wallet) NextAccount(accountName string) (int32, error) { return -1, errors.New(ErrWalletLocked) } - ctx := wallet.shutdownContext() + ctx := wallet.ShutdownContext() accountNumber, err := wallet.Internal().NextAccount(ctx, accountName) if err != nil { @@ -208,7 +208,7 @@ func (wallet *Wallet) NextAccount(accountName string) (int32, error) { } func (wallet *Wallet) RenameAccount(accountNumber int32, newName string) error { - err := wallet.Internal().RenameAccount(wallet.shutdownContext(), uint32(accountNumber), newName) + err := wallet.Internal().RenameAccount(wallet.ShutdownContext(), uint32(accountNumber), newName) if err != nil { return translateError(err) } @@ -225,21 +225,21 @@ func (wallet *Wallet) AccountName(accountNumber int32) (string, error) { } func (wallet *Wallet) AccountNameRaw(accountNumber uint32) (string, error) { - return wallet.Internal().AccountName(wallet.shutdownContext(), accountNumber) + return wallet.Internal().AccountName(wallet.ShutdownContext(), accountNumber) } func (wallet *Wallet) AccountNumber(accountName string) (int32, error) { - accountNumber, err := wallet.Internal().AccountNumber(wallet.shutdownContext(), accountName) + accountNumber, err := wallet.Internal().AccountNumber(wallet.ShutdownContext(), accountName) return int32(accountNumber), translateError(err) } func (wallet *Wallet) HasAccount(accountName string) bool { - _, err := wallet.Internal().AccountNumber(wallet.shutdownContext(), accountName) + _, err := wallet.Internal().AccountNumber(wallet.ShutdownContext(), accountName) return err == nil } func (wallet *Wallet) HDPathForAccount(accountNumber int32) (string, error) { - cointype, err := wallet.Internal().CoinType(wallet.shutdownContext()) + cointype, err := wallet.Internal().CoinType(wallet.ShutdownContext()) if err != nil { return "", translateError(err) } diff --git a/address.go b/wallets/dcr/address.go similarity index 86% rename from address.go rename to wallets/dcr/address.go index c42b6cf8b..9a69620c6 100644 --- a/address.go +++ b/wallets/dcr/address.go @@ -1,10 +1,11 @@ -package dcrlibwallet +package dcr import ( "fmt" "decred.org/dcrwallet/v2/errors" w "decred.org/dcrwallet/v2/wallet" + "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/txscript/v4/stdaddr" ) @@ -17,8 +18,8 @@ type AddressInfo struct { AccountName string } -func (mw *MultiWallet) IsAddressValid(address string) bool { - _, err := stdaddr.DecodeAddress(address, mw.chainParams) +func (wallet *Wallet) IsAddressValid(address string, chainParams *chaincfg.Params) bool { + _, err := stdaddr.DecodeAddress(address, chainParams) return err == nil } @@ -28,7 +29,7 @@ func (wallet *Wallet) HaveAddress(address string) bool { return false } - have, err := wallet.Internal().HaveAddress(wallet.shutdownContext(), addr) + have, err := wallet.Internal().HaveAddress(wallet.ShutdownContext(), addr) if err != nil { return false } @@ -42,7 +43,7 @@ func (wallet *Wallet) AccountOfAddress(address string) (string, error) { return "", translateError(err) } - a, err := wallet.Internal().KnownAddress(wallet.shutdownContext(), addr) + a, err := wallet.Internal().KnownAddress(wallet.ShutdownContext(), addr) if err != nil { return "", translateError(err) } @@ -60,7 +61,7 @@ func (wallet *Wallet) AddressInfo(address string) (*AddressInfo, error) { Address: address, } - known, _ := wallet.Internal().KnownAddress(wallet.shutdownContext(), addr) + known, _ := wallet.Internal().KnownAddress(wallet.ShutdownContext(), addr) if known != nil { addressInfo.IsMine = true addressInfo.AccountName = known.AccountName() @@ -104,7 +105,7 @@ func (wallet *Wallet) NextAddress(account int32) (string, error) { // the newly incremented index) is returned below by CurrentAddress. // NOTE: This workaround will be unnecessary once this anomaly is corrected // upstream. - _, err := wallet.Internal().NewExternalAddress(wallet.shutdownContext(), uint32(account), w.WithGapPolicyWrap()) + _, err := wallet.Internal().NewExternalAddress(wallet.ShutdownContext(), uint32(account), w.WithGapPolicyWrap()) if err != nil { log.Errorf("NewExternalAddress error: %w", err) return "", err @@ -119,7 +120,7 @@ func (wallet *Wallet) AddressPubKey(address string) (string, error) { return "", err } - known, err := wallet.Internal().KnownAddress(wallet.shutdownContext(), addr) + known, err := wallet.Internal().KnownAddress(wallet.ShutdownContext(), addr) if err != nil { return "", err } diff --git a/consensus.go b/wallets/dcr/consensus.go similarity index 98% rename from consensus.go rename to wallets/dcr/consensus.go index baae75db5..9a353614b 100644 --- a/consensus.go +++ b/wallets/dcr/consensus.go @@ -1,4 +1,4 @@ -package dcrlibwallet +package dcr import ( "fmt" @@ -101,7 +101,7 @@ func (wallet *Wallet) SetVoteChoice(agendaID, choiceID, hash string, passphrase } defer wallet.LockWallet() - ctx := wallet.shutdownContext() + ctx := wallet.ShutdownContext() // get choices choices, _, err := wallet.Internal().AgendaChoices(ctx, ticketHash) // returns saved prefs for current agendas @@ -208,7 +208,7 @@ func (wallet *Wallet) AllVoteAgendas(hash string, newestFirst bool) ([]*Agenda, ticketHash = hash } - ctx := wallet.shutdownContext() + ctx := wallet.ShutdownContext() choices, _, err := wallet.Internal().AgendaChoices(ctx, ticketHash) // returns saved prefs for current agendas if err != nil { return nil, err diff --git a/decodetx.go b/wallets/dcr/decodetx.go similarity index 99% rename from decodetx.go rename to wallets/dcr/decodetx.go index effea74c9..20b4b60c3 100644 --- a/decodetx.go +++ b/wallets/dcr/decodetx.go @@ -1,4 +1,4 @@ -package dcrlibwallet +package dcr import ( "fmt" diff --git a/wallets/dcr/errors.go b/wallets/dcr/errors.go new file mode 100644 index 000000000..7bec27cfb --- /dev/null +++ b/wallets/dcr/errors.go @@ -0,0 +1,61 @@ +package dcr + +import ( + "decred.org/dcrwallet/v2/errors" + "github.com/asdine/storm" +) + +const ( + // Error Codes + ErrInsufficientBalance = "insufficient_balance" + ErrInvalid = "invalid" + ErrWalletLocked = "wallet_locked" + ErrWalletDatabaseInUse = "wallet_db_in_use" + ErrWalletNotLoaded = "wallet_not_loaded" + ErrWalletNotFound = "wallet_not_found" + ErrWalletNameExist = "wallet_name_exists" + ErrReservedWalletName = "wallet_name_reserved" + ErrWalletIsRestored = "wallet_is_restored" + ErrWalletIsWatchOnly = "watch_only_wallet" + ErrUnusableSeed = "unusable_seed" + ErrPassphraseRequired = "passphrase_required" + ErrInvalidPassphrase = "invalid_passphrase" + ErrNotConnected = "not_connected" + ErrExist = "exists" + ErrNotExist = "not_exists" + ErrEmptySeed = "empty_seed" + ErrInvalidAddress = "invalid_address" + ErrInvalidAuth = "invalid_auth" + ErrUnavailable = "unavailable" + ErrContextCanceled = "context_canceled" + ErrFailedPrecondition = "failed_precondition" + ErrSyncAlreadyInProgress = "sync_already_in_progress" + ErrNoPeers = "no_peers" + ErrInvalidPeers = "invalid_peers" + ErrListenerAlreadyExist = "listener_already_exist" + ErrLoggerAlreadyRegistered = "logger_already_registered" + ErrLogRotatorAlreadyInitialized = "log_rotator_already_initialized" + ErrAddressDiscoveryNotDone = "address_discovery_not_done" + ErrChangingPassphrase = "err_changing_passphrase" + ErrSavingWallet = "err_saving_wallet" + ErrIndexOutOfRange = "err_index_out_of_range" + ErrNoMixableOutput = "err_no_mixable_output" + ErrInvalidVoteBit = "err_invalid_vote_bit" +) + +// todo, should update this method to translate more error kinds. +func translateError(err error) error { + if err, ok := err.(*errors.Error); ok { + switch err.Kind { + case errors.InsufficientBalance: + return errors.New(ErrInsufficientBalance) + case errors.NotExist, storm.ErrNotFound: + return errors.New(ErrNotExist) + case errors.Passphrase: + return errors.New(ErrInvalidPassphrase) + case errors.NoPeers: + return errors.New(ErrNoPeers) + } + } + return err +} diff --git a/wallets/dcr/log.go b/wallets/dcr/log.go new file mode 100644 index 000000000..5b13d7839 --- /dev/null +++ b/wallets/dcr/log.go @@ -0,0 +1,176 @@ +// Copyright (c) 2013-2017 The btcsuite developers +// Copyright (c) 2015-2018 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package dcr + +import ( + "os" + + "decred.org/dcrwallet/v2/errors" + "decred.org/dcrwallet/v2/p2p" + "decred.org/dcrwallet/v2/ticketbuyer" + "decred.org/dcrwallet/v2/wallet" + "decred.org/dcrwallet/v2/wallet/udb" + "github.com/decred/dcrd/addrmgr/v2" + "github.com/decred/dcrd/connmgr/v3" + "github.com/decred/slog" + "github.com/jrick/logrotate/rotator" + "github.com/planetdecred/dcrlibwallet/internal/loader" + "github.com/planetdecred/dcrlibwallet/internal/vsp" + "github.com/planetdecred/dcrlibwallet/spv" +) + +// logWriter implements an io.Writer that outputs to both standard output and +// the write-end pipe of an initialized log rotator. +type logWriter struct{} + +func (logWriter) Write(p []byte) (n int, err error) { + os.Stdout.Write(p) + logRotator.Write(p) + return len(p), nil +} + +// Loggers per subsystem. A single backend logger is created and all subsytem +// loggers created from it will write to the backend. When adding new +// subsystems, add the subsystem logger variable here and to the +// subsystemLoggers map. +// +// Loggers can not be used before the log rotator has been initialized with a +// log file. This must be performed early during application startup by calling +// initLogRotator. +var ( + // backendLog is the logging backend used to create all subsystem loggers. + // The backend must not be used before the log rotator has been initialized, + // or data races and/or nil pointer dereferences will occur. + backendLog = slog.NewBackend(logWriter{}) + + // logRotator is one of the logging outputs. It should be closed on + // application shutdown. + logRotator *rotator.Rotator + + log = backendLog.Logger("DLWL") + loaderLog = backendLog.Logger("LODR") + walletLog = backendLog.Logger("WLLT") + tkbyLog = backendLog.Logger("TKBY") + syncLog = backendLog.Logger("SYNC") + grpcLog = backendLog.Logger("GRPC") + legacyRPCLog = backendLog.Logger("RPCS") + cmgrLog = backendLog.Logger("CMGR") + amgrLog = backendLog.Logger("AMGR") + vspcLog = backendLog.Logger("VSPC") +) + +// Initialize package-global logger variables. +func init() { + loader.UseLogger(loaderLog) + wallet.UseLogger(walletLog) + udb.UseLogger(walletLog) + ticketbuyer.UseLogger(tkbyLog) + spv.UseLogger(syncLog) + p2p.UseLogger(syncLog) + connmgr.UseLogger(cmgrLog) + addrmgr.UseLogger(amgrLog) + vsp.UseLogger(vspcLog) +} + +// subsystemLoggers maps each subsystem identifier to its associated logger. +var subsystemLoggers = map[string]slog.Logger{ + "DLWL": log, + "LODR": loaderLog, + "WLLT": walletLog, + "TKBY": tkbyLog, + "SYNC": syncLog, + "GRPC": grpcLog, + "RPCS": legacyRPCLog, + "CMGR": cmgrLog, + "AMGR": amgrLog, + "VSPC": vspcLog, +} + +// initLogRotator initializes the logging rotater to write logs to logFile and +// create roll files in the same directory. It must be called before the +// package-global log rotater variables are used. +func initLogRotator(logFile string) error { + r, err := rotator.New(logFile, 10*1024, false, 3) + if err != nil { + return errors.Errorf("failed to create file rotator: %v", err) + } + + logRotator = r + return nil +} + +// UseLoggers sets the subsystem logs to use the provided loggers. +func UseLoggers(main, loaderLog, walletLog, tkbyLog, + syncLog, cmgrLog, amgrLog slog.Logger) { + log = main + loader.UseLogger(loaderLog) + wallet.UseLogger(walletLog) + udb.UseLogger(walletLog) + ticketbuyer.UseLogger(tkbyLog) + spv.UseLogger(syncLog) + p2p.UseLogger(syncLog) + connmgr.UseLogger(cmgrLog) + addrmgr.UseLogger(amgrLog) +} + +// UseLogger sets the subsystem logs to use the provided logger. +func UseLogger(logger slog.Logger) { + UseLoggers(logger, logger, logger, logger, logger, logger, logger) +} + +// RegisterLogger should be called before logRotator is initialized. +func RegisterLogger(tag string) (slog.Logger, error) { + if logRotator != nil { + return nil, errors.E(ErrLogRotatorAlreadyInitialized) + } + + if _, exists := subsystemLoggers[tag]; exists { + return nil, errors.E(ErrLoggerAlreadyRegistered) + } + + logger := backendLog.Logger(tag) + subsystemLoggers[tag] = logger + + return logger, nil +} + +func SetLogLevels(logLevel string) { + _, ok := slog.LevelFromString(logLevel) + if !ok { + return + } + + // Configure all sub-systems with the new logging level. Dynamically + // create loggers as needed. + for subsystemID := range subsystemLoggers { + setLogLevel(subsystemID, logLevel) + } +} + +// setLogLevel sets the logging level for provided subsystem. Invalid +// subsystems are ignored. Uninitialized subsystems are dynamically created as +// needed. +func setLogLevel(subsystemID string, logLevel string) { + // Ignore invalid subsystems. + logger, ok := subsystemLoggers[subsystemID] + if !ok { + return + } + + // Defaults to info if the log level is invalid. + level, _ := slog.LevelFromString(logLevel) + logger.SetLevel(level) +} + +// Log writes a message to the log using LevelInfo. +func Log(m string) { + log.Info(m) +} + +// LogT writes a tagged message to the log using LevelInfo. +func LogT(tag, m string) { + log.Infof("%s: %s", tag, m) +} diff --git a/message.go b/wallets/dcr/message.go similarity index 73% rename from message.go rename to wallets/dcr/message.go index 989a1c045..3d99e2c49 100644 --- a/message.go +++ b/wallets/dcr/message.go @@ -1,8 +1,9 @@ -package dcrlibwallet +package dcr import ( "decred.org/dcrwallet/v2/errors" w "decred.org/dcrwallet/v2/wallet" + "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/txscript/v4/stdaddr" ) @@ -13,10 +14,10 @@ func (wallet *Wallet) SignMessage(passphrase []byte, address string, message str } defer wallet.LockWallet() - return wallet.signMessage(address, message) + return wallet.SignMessageDirect(address, message) } -func (wallet *Wallet) signMessage(address string, message string) ([]byte, error) { +func (wallet *Wallet) SignMessageDirect(address string, message string) ([]byte, error) { addr, err := stdaddr.DecodeAddress(address, wallet.chainParams) if err != nil { return nil, translateError(err) @@ -31,7 +32,7 @@ func (wallet *Wallet) signMessage(address string, message string) ([]byte, error return nil, errors.New(ErrInvalidAddress) } - sig, err := wallet.Internal().SignMessage(wallet.shutdownContext(), message, addr) + sig, err := wallet.Internal().SignMessage(wallet.ShutdownContext(), message, addr) if err != nil { return nil, translateError(err) } @@ -39,10 +40,10 @@ func (wallet *Wallet) signMessage(address string, message string) ([]byte, error return sig, nil } -func (mw *MultiWallet) VerifyMessage(address string, message string, signatureBase64 string) (bool, error) { +func (wallet *Wallet) VerifyMessage(address string, message string, signatureBase64 string, chainParams *chaincfg.Params) (bool, error) { var valid bool - addr, err := stdaddr.DecodeAddress(address, mw.chainParams) + addr, err := stdaddr.DecodeAddress(address, chainParams) if err != nil { return false, translateError(err) } @@ -61,7 +62,7 @@ func (mw *MultiWallet) VerifyMessage(address string, message string, signatureBa return false, errors.New(ErrInvalidAddress) } - valid, err = w.VerifyMessage(message, addr, signature, mw.chainParams) + valid, err = w.VerifyMessage(message, addr, signature, chainParams) if err != nil { return false, translateError(err) } diff --git a/politeia.go b/wallets/dcr/politeia.go similarity index 71% rename from politeia.go rename to wallets/dcr/politeia.go index 45b836082..078377faf 100644 --- a/politeia.go +++ b/wallets/dcr/politeia.go @@ -1,27 +1,14 @@ -package dcrlibwallet +package dcr import ( - "context" "encoding/json" "fmt" - "sync" "decred.org/dcrwallet/v2/errors" "github.com/asdine/storm" "github.com/asdine/storm/q" ) -type Politeia struct { - mwRef *MultiWallet - host string - mu sync.RWMutex - ctx context.Context - cancelSync context.CancelFunc - client *politeiaClient - notificationListenersMu sync.RWMutex - notificationListeners map[string]ProposalNotificationListener -} - const ( ProposalCategoryAll int32 = iota + 1 ProposalCategoryPre @@ -31,38 +18,38 @@ const ( ProposalCategoryAbandoned ) -func newPoliteia(mwRef *MultiWallet, host string) (*Politeia, error) { +func (wallet *Wallet) NewPoliteia(host string) (*Politeia, error) { p := &Politeia{ - mwRef: mwRef, - host: host, - client: nil, - notificationListeners: make(map[string]ProposalNotificationListener), + WalletRef: wallet, // Holds a reference to the wallet initializing Politeia. + Host: host, + Client: nil, + NotificationListeners: make(map[string]ProposalNotificationListener), } return p, nil } func (p *Politeia) saveLastSyncedTimestamp(lastSyncedTimestamp int64) { - p.mwRef.SetLongConfigValueForKey(PoliteiaLastSyncedTimestampConfigKey, lastSyncedTimestamp) + p.WalletRef.SetLongConfigValueForKey(PoliteiaLastSyncedTimestampConfigKey, lastSyncedTimestamp) } func (p *Politeia) getLastSyncedTimestamp() int64 { - return p.mwRef.ReadLongConfigValueForKey(PoliteiaLastSyncedTimestampConfigKey, 0) + return p.WalletRef.ReadLongConfigValueForKey(PoliteiaLastSyncedTimestampConfigKey, 0) } func (p *Politeia) saveOrOverwiteProposal(proposal *Proposal) error { var oldProposal Proposal - err := p.mwRef.db.One("Token", proposal.Token, &oldProposal) + err := p.WalletRef.DB.One("Token", proposal.Token, &oldProposal) if err != nil && err != storm.ErrNotFound { return errors.Errorf("error checking if proposal was already indexed: %s", err.Error()) } if oldProposal.Token != "" { // delete old record before saving new (if it exists) - p.mwRef.db.DeleteStruct(oldProposal) + p.WalletRef.DB.DeleteStruct(oldProposal) } - return p.mwRef.db.Save(proposal) + return p.WalletRef.DB.Save(proposal) } // GetProposalsRaw fetches and returns a proposals from the db @@ -77,16 +64,16 @@ func (p *Politeia) getProposalsRaw(category int32, offset, limit int32, newestFi case ProposalCategoryAll: if skipAbandoned { - query = p.mwRef.db.Select( + query = p.WalletRef.DB.Select( q.Not(q.Eq("Category", ProposalCategoryAbandoned)), ) } else { - query = p.mwRef.db.Select( + query = p.WalletRef.DB.Select( q.True(), ) } default: - query = p.mwRef.db.Select( + query = p.WalletRef.DB.Select( q.Eq("Category", category), ) } @@ -137,7 +124,7 @@ func (p *Politeia) GetProposals(category int32, offset, limit int32, newestFirst // GetProposalRaw fetches and returns a single proposal specified by it's censorship record token func (p *Politeia) GetProposalRaw(censorshipToken string) (*Proposal, error) { var proposal Proposal - err := p.mwRef.db.One("Token", censorshipToken, &proposal) + err := p.WalletRef.DB.One("Token", censorshipToken, &proposal) if err != nil { return nil, err } @@ -147,13 +134,13 @@ func (p *Politeia) GetProposalRaw(censorshipToken string) (*Proposal, error) { // GetProposal returns the result of GetProposalRaw as a JSON string func (p *Politeia) GetProposal(censorshipToken string) (string, error) { - return p.marshalResult(p.GetProposalRaw(censorshipToken)) + return marshalResult(p.GetProposalRaw(censorshipToken)) } // GetProposalByIDRaw fetches and returns a single proposal specified by it's ID func (p *Politeia) GetProposalByIDRaw(proposalID int) (*Proposal, error) { var proposal Proposal - err := p.mwRef.db.One("ID", proposalID, &proposal) + err := p.WalletRef.DB.One("ID", proposalID, &proposal) if err != nil { return nil, err } @@ -163,7 +150,7 @@ func (p *Politeia) GetProposalByIDRaw(proposalID int) (*Proposal, error) { // GetProposalByID returns the result of GetProposalByIDRaw as a JSON string func (p *Politeia) GetProposalByID(proposalID int) (string, error) { - return p.marshalResult(p.GetProposalByIDRaw(proposalID)) + return marshalResult(p.GetProposalByIDRaw(proposalID)) } // Count returns the number of proposals of a specified category @@ -176,7 +163,7 @@ func (p *Politeia) Count(category int32) (int32, error) { matcher = q.Eq("Category", category) } - count, err := p.mwRef.db.Select(matcher).Count(&Proposal{}) + count, err := p.WalletRef.DB.Select(matcher).Count(&Proposal{}) if err != nil { return 0, err } @@ -222,24 +209,10 @@ func (p *Politeia) Overview() (*ProposalOverview, error) { } func (p *Politeia) ClearSavedProposals() error { - err := p.mwRef.db.Drop(&Proposal{}) + err := p.WalletRef.DB.Drop(&Proposal{}) if err != nil { return translateError(err) } - return p.mwRef.db.Init(&Proposal{}) -} - -func (p *Politeia) marshalResult(result interface{}, err error) (string, error) { - - if err != nil { - return "", translateError(err) - } - - response, err := json.Marshal(result) - if err != nil { - return "", fmt.Errorf("error marshalling result: %s", err.Error()) - } - - return string(response), nil + return p.WalletRef.DB.Init(&Proposal{}) } diff --git a/politeia_client.go b/wallets/dcr/politeia_client.go similarity index 96% rename from politeia_client.go rename to wallets/dcr/politeia_client.go index cd5981fe9..fb33d9ab3 100644 --- a/politeia_client.go +++ b/wallets/dcr/politeia_client.go @@ -1,4 +1,4 @@ -package dcrlibwallet +package dcr import ( "bytes" @@ -16,15 +16,6 @@ import ( "github.com/decred/politeia/politeiawww/client" ) -type politeiaClient struct { - host string - httpClient *http.Client - - version *www.VersionReply - policy *www.PolicyReply - cookies []*http.Cookie -} - const ( PoliteiaMainnetHost = "https://proposals.decred.org/api" PoliteiaTestnetHost = "https://test-proposals.decred.org/api" @@ -58,9 +49,9 @@ func newPoliteiaClient(host string) *politeiaClient { func (p *Politeia) getClient() (*politeiaClient, error) { p.mu.Lock() defer p.mu.Unlock() - client := p.client + client := p.Client if client == nil { - client = newPoliteiaClient(p.host) + client = newPoliteiaClient(p.Host) version, err := client.serverVersion() if err != nil { return nil, err @@ -72,7 +63,7 @@ func (p *Politeia) getClient() (*politeiaClient, error) { return nil, err } - p.client = client + p.Client = client } return client, nil diff --git a/politeia_sync.go b/wallets/dcr/politeia_sync.go similarity index 90% rename from politeia_sync.go rename to wallets/dcr/politeia_sync.go index 8832111c1..30f644285 100644 --- a/politeia_sync.go +++ b/wallets/dcr/politeia_sync.go @@ -1,14 +1,15 @@ -package dcrlibwallet +package dcr import ( "encoding/hex" "encoding/json" - "errors" "fmt" "reflect" "strconv" "time" + "decred.org/dcrwallet/v2/errors" + "github.com/asdine/storm" tkv1 "github.com/decred/politeia/politeiawww/api/ticketvote/v1" www "github.com/decred/politeia/politeiawww/api/www/v1" @@ -35,7 +36,7 @@ func (p *Politeia) Sync() error { log.Info("Politeia sync: started") - p.ctx, p.cancelSync = p.mwRef.contextWithShutdownCancel() + p.ctx, p.cancelSync = p.WalletRef.contextWithShutdownCancel() defer p.resetSyncData() p.mu.Unlock() @@ -96,7 +97,7 @@ func (p *Politeia) StopSync() { func (p *Politeia) checkForUpdates() error { offset := 0 p.mu.RLock() - limit := int32(p.client.policy.ProposalListPageSize) + limit := int32(p.Client.policy.ProposalListPageSize) p.mu.RUnlock() for { @@ -138,7 +139,7 @@ func (p *Politeia) handleNewProposals(proposals []Proposal) error { } p.mu.RLock() - tokenInventory, err := p.client.tokenInventory() + tokenInventory, err := p.Client.tokenInventory() p.mu.RUnlock() if err != nil { return err @@ -156,12 +157,12 @@ func (p *Politeia) handleProposalsUpdate(proposals []Proposal) error { p.mu.RLock() defer p.mu.RUnlock() - batchProposals, err := p.client.batchProposals(tokens) + batchProposals, err := p.Client.batchProposals(tokens) if err != nil { return err } - batchVotesSummaries, err := p.client.batchVoteSummary(tokens) + batchVotesSummaries, err := p.Client.batchVoteSummary(tokens) if err != nil { return err } @@ -220,7 +221,7 @@ func (p *Politeia) updateProposalDetails(oldProposal, updatedProposal Proposal) } } - err := p.mwRef.db.Update(&updatedProposal) + err := p.WalletRef.DB.Update(&updatedProposal) if err != nil { return fmt.Errorf("error saving updated proposal: %s", err.Error()) } @@ -287,7 +288,7 @@ func (p *Politeia) fetchBatchProposals(category int32, tokens []string, broadcas return errors.New(ErrContextCanceled) } - limit := int(p.client.policy.ProposalListPageSize) + limit := int(p.Client.policy.ProposalListPageSize) if len(tokens) <= limit { limit = len(tokens) } @@ -297,7 +298,7 @@ func (p *Politeia) fetchBatchProposals(category int32, tokens []string, broadcas var tokenBatch []string tokenBatch, tokens = tokens[:limit], tokens[limit:] - proposals, err := p.client.batchProposals(tokenBatch) + proposals, err := p.Client.batchProposals(tokenBatch) if err != nil { return err } @@ -306,7 +307,7 @@ func (p *Politeia) fetchBatchProposals(category int32, tokens []string, broadcas return errors.New(ErrContextCanceled) } - votesSummaries, err := p.client.batchVoteSummary(tokenBatch) + votesSummaries, err := p.Client.batchVoteSummary(tokenBatch) if err != nil { return err } @@ -351,12 +352,12 @@ func (p *Politeia) FetchProposalDescription(token string) (string, error) { return "", err } - client, err := p.getClient() + Client, err := p.getClient() if err != nil { return "", err } - proposalDetailsReply, err := client.proposalDetails(token) + proposalDetailsReply, err := Client.proposalDetails(token) if err != nil { return "", err } @@ -386,22 +387,22 @@ func (p *Politeia) FetchProposalDescription(token string) (string, error) { } func (p *Politeia) ProposalVoteDetailsRaw(walletID int, token string) (*ProposalVoteDetails, error) { - wal := p.mwRef.WalletWithID(walletID) + wal := p.WalletRef if wal == nil { return nil, fmt.Errorf(ErrWalletNotFound) } - client, err := p.getClient() + Client, err := p.getClient() if err != nil { return nil, err } - detailsReply, err := client.voteDetails(token) + detailsReply, err := Client.voteDetails(token) if err != nil { return nil, err } - votesResults, err := client.voteResults(token) + votesResults, err := Client.voteResults(token) if err != nil { return nil, err } @@ -411,7 +412,7 @@ func (p *Politeia) ProposalVoteDetailsRaw(walletID int, token string) (*Proposal return nil, err } - ticketHashes, addresses, err := wal.Internal().CommittedTickets(wal.shutdownContext(), hashes) + ticketHashes, addresses, err := wal.Internal().CommittedTickets(wal.ShutdownContext(), hashes) if err != nil { return nil, err } @@ -482,17 +483,17 @@ func (p *Politeia) ProposalVoteDetails(walletID int, token string) (string, erro } func (p *Politeia) CastVotes(walletID int, eligibleTickets []*ProposalVote, token, passphrase string) error { - wal := p.mwRef.WalletWithID(walletID) + wal := p.WalletRef if wal == nil { return fmt.Errorf(ErrWalletNotFound) } - client, err := p.getClient() + Client, err := p.getClient() if err != nil { return err } - detailsReply, err := client.voteDetails(token) + detailsReply, err := Client.voteDetails(token) if err != nil { return err } @@ -522,7 +523,7 @@ func (p *Politeia) CastVotes(walletID int, eligibleTickets []*ProposalVote, toke msg := token + ticket.Hash + voteBitHex - signature, err := wal.signMessage(ticket.Address, msg) + signature, err := wal.SignMessageDirect(ticket.Address, msg) if err != nil { return err } @@ -538,18 +539,18 @@ func (p *Politeia) CastVotes(walletID int, eligibleTickets []*ProposalVote, toke votes = append(votes, singleVote) } - return client.sendVotes(votes) + return Client.sendVotes(votes) } func (p *Politeia) AddNotificationListener(notificationListener ProposalNotificationListener, uniqueIdentifier string) error { p.notificationListenersMu.Lock() defer p.notificationListenersMu.Unlock() - if _, ok := p.notificationListeners[uniqueIdentifier]; ok { + if _, ok := p.NotificationListeners[uniqueIdentifier]; ok { return errors.New(ErrListenerAlreadyExist) } - p.notificationListeners[uniqueIdentifier] = notificationListener + p.NotificationListeners[uniqueIdentifier] = notificationListener return nil } @@ -557,14 +558,14 @@ func (p *Politeia) RemoveNotificationListener(uniqueIdentifier string) { p.notificationListenersMu.Lock() defer p.notificationListenersMu.Unlock() - delete(p.notificationListeners, uniqueIdentifier) + delete(p.NotificationListeners, uniqueIdentifier) } func (p *Politeia) publishSynced() { p.notificationListenersMu.Lock() defer p.notificationListenersMu.Unlock() - for _, notificationListener := range p.notificationListeners { + for _, notificationListener := range p.NotificationListeners { notificationListener.OnProposalsSynced() } } @@ -573,7 +574,7 @@ func (p *Politeia) publishNewProposal(proposal *Proposal) { p.notificationListenersMu.Lock() defer p.notificationListenersMu.Unlock() - for _, notificationListener := range p.notificationListeners { + for _, notificationListener := range p.NotificationListeners { notificationListener.OnNewProposal(proposal) } } @@ -582,7 +583,7 @@ func (p *Politeia) publishVoteStarted(proposal *Proposal) { p.notificationListenersMu.Lock() defer p.notificationListenersMu.Unlock() - for _, notificationListener := range p.notificationListeners { + for _, notificationListener := range p.NotificationListeners { notificationListener.OnProposalVoteStarted(proposal) } } @@ -591,7 +592,7 @@ func (p *Politeia) publishVoteFinished(proposal *Proposal) { p.notificationListenersMu.Lock() defer p.notificationListenersMu.Unlock() - for _, notificationListener := range p.notificationListeners { + for _, notificationListener := range p.NotificationListeners { notificationListener.OnProposalVoteFinished(proposal) } } diff --git a/wallets/dcr/rescan.go b/wallets/dcr/rescan.go new file mode 100644 index 000000000..fedce9881 --- /dev/null +++ b/wallets/dcr/rescan.go @@ -0,0 +1,141 @@ +package dcr + +import ( + "context" + "math" + "time" + + "decred.org/dcrwallet/v2/errors" + w "decred.org/dcrwallet/v2/wallet" +) + +func (wallet *Wallet) RescanBlocks(walletID int) error { + return wallet.RescanBlocksFromHeight(walletID, 0) +} + +func (wallet *Wallet) RescanBlocksFromHeight(walletID int, startHeight int32) error { + + netBackend, err := wallet.Internal().NetworkBackend() + if err != nil { + return errors.E(ErrNotConnected) + } + + if wallet.IsRescanning() || !wallet.IsSynced() { + return errors.E(ErrInvalid) + } + + go func() { + defer func() { + wallet.syncData.mu.Lock() + wallet.syncData.rescanning = false + wallet.syncData.cancelRescan = nil + wallet.syncData.mu.Unlock() + }() + + ctx, cancel := wallet.ShutdownContextWithCancel() + + wallet.syncData.mu.Lock() + wallet.syncData.rescanning = true + wallet.syncData.cancelRescan = cancel + wallet.syncData.mu.Unlock() + + if wallet.blocksRescanProgressListener != nil { + wallet.blocksRescanProgressListener.OnBlocksRescanStarted(walletID) + } + + progress := make(chan w.RescanProgress, 1) + go wallet.Internal().RescanProgressFromHeight(ctx, netBackend, startHeight, progress) + + rescanStartTime := time.Now().Unix() + + for p := range progress { + if p.Err != nil { + log.Error(p.Err) + if wallet.blocksRescanProgressListener != nil { + wallet.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, p.Err) + } + return + } + + rescanProgressReport := &HeadersRescanProgressReport{ + CurrentRescanHeight: p.ScannedThrough, + TotalHeadersToScan: wallet.getBestBlock(), + WalletID: walletID, + } + + elapsedRescanTime := time.Now().Unix() - rescanStartTime + rescanRate := float64(p.ScannedThrough) / float64(rescanProgressReport.TotalHeadersToScan) + + rescanProgressReport.RescanProgress = int32(math.Round(rescanRate * 100)) + estimatedTotalRescanTime := int64(math.Round(float64(elapsedRescanTime) / rescanRate)) + rescanProgressReport.RescanTimeRemaining = estimatedTotalRescanTime - elapsedRescanTime + + rescanProgressReport.GeneralSyncProgress = &GeneralSyncProgress{ + TotalSyncProgress: rescanProgressReport.RescanProgress, + TotalTimeRemainingSeconds: rescanProgressReport.RescanTimeRemaining, + } + + if wallet.blocksRescanProgressListener != nil { + wallet.blocksRescanProgressListener.OnBlocksRescanProgress(rescanProgressReport) + } + + select { + case <-ctx.Done(): + log.Info("Rescan canceled through context") + + if wallet.blocksRescanProgressListener != nil { + if ctx.Err() != nil && ctx.Err() != context.Canceled { + wallet.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, ctx.Err()) + } else { + wallet.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, nil) + } + } + + return + default: + continue + } + } + + var err error + if startHeight == 0 { + err = wallet.ReindexTransactions() + } else { + err = wallet.WalletDataDB.SaveLastIndexPoint(startHeight) + if err != nil { + if wallet.blocksRescanProgressListener != nil { + wallet.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, err) + } + return + } + + err = wallet.IndexTransactions() + } + if wallet.blocksRescanProgressListener != nil { + wallet.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, err) + } + }() + + return nil +} + +func (wallet *Wallet) CancelRescan() { + wallet.syncData.mu.Lock() + defer wallet.syncData.mu.Unlock() + if wallet.syncData.cancelRescan != nil { + wallet.syncData.cancelRescan() + wallet.syncData.cancelRescan = nil + + log.Info("Rescan canceled.") + } +} + +func (wallet *Wallet) IsRescanning() bool { + wallet.syncData.mu.RLock() + defer wallet.syncData.mu.RUnlock() + return wallet.syncData.rescanning +} + +func (wallet *Wallet) SetBlocksRescanProgressListener(blocksRescanProgressListener BlocksRescanProgressListener) { + wallet.blocksRescanProgressListener = blocksRescanProgressListener +} diff --git a/wallets/dcr/sync.go b/wallets/dcr/sync.go new file mode 100644 index 000000000..1b8f676e7 --- /dev/null +++ b/wallets/dcr/sync.go @@ -0,0 +1,484 @@ +package dcr + +import ( + "context" + "encoding/json" + "fmt" + "net" + "sort" + "strings" + "sync" + + "decred.org/dcrwallet/v2/errors" + "decred.org/dcrwallet/v2/p2p" + w "decred.org/dcrwallet/v2/wallet" + "github.com/decred/dcrd/addrmgr/v2" + "github.com/planetdecred/dcrlibwallet/spv" +) + +// reading/writing of properties of this struct are protected by mutex.x +type SyncData struct { + mu sync.RWMutex + + SyncProgressListeners map[string]SyncProgressListener + showLogs bool + + synced bool + syncing bool + cancelSync context.CancelFunc + cancelRescan context.CancelFunc + syncCanceled chan struct{} + + // Flag to notify syncCanceled callback if the sync was canceled so as to be restarted. + restartSyncRequested bool + + rescanning bool + connectedPeers int32 + + *activeSyncData +} + +// reading/writing of properties of this struct are protected by syncData.mu. +type activeSyncData struct { + syncer *spv.Syncer + + syncStage int32 + + cfiltersFetchProgress CFiltersFetchProgressReport + headersFetchProgress HeadersFetchProgressReport + addressDiscoveryProgress AddressDiscoveryProgressReport + headersRescanProgress HeadersRescanProgressReport + + addressDiscoveryCompletedOrCanceled chan bool + + rescanStartTime int64 + + totalInactiveSeconds int64 +} + +const ( + InvalidSyncStage = -1 + CFiltersFetchSyncStage = 0 + HeadersFetchSyncStage = 1 + AddressDiscoverySyncStage = 2 + HeadersRescanSyncStage = 3 +) + +func (wallet *Wallet) initActiveSyncData() { + + cfiltersFetchProgress := CFiltersFetchProgressReport{ + GeneralSyncProgress: &GeneralSyncProgress{}, + beginFetchCFiltersTimeStamp: 0, + startCFiltersHeight: -1, + cfiltersFetchTimeSpent: 0, + totalFetchedCFiltersCount: 0, + } + + headersFetchProgress := HeadersFetchProgressReport{ + GeneralSyncProgress: &GeneralSyncProgress{}, + beginFetchTimeStamp: -1, + headersFetchTimeSpent: -1, + totalFetchedHeadersCount: 0, + } + + addressDiscoveryProgress := AddressDiscoveryProgressReport{ + GeneralSyncProgress: &GeneralSyncProgress{}, + addressDiscoveryStartTime: -1, + totalDiscoveryTimeSpent: -1, + } + + headersRescanProgress := HeadersRescanProgressReport{} + headersRescanProgress.GeneralSyncProgress = &GeneralSyncProgress{} + + wallet.syncData.mu.Lock() + wallet.syncData.activeSyncData = &activeSyncData{ + syncStage: InvalidSyncStage, + + cfiltersFetchProgress: cfiltersFetchProgress, + headersFetchProgress: headersFetchProgress, + addressDiscoveryProgress: addressDiscoveryProgress, + headersRescanProgress: headersRescanProgress, + } + wallet.syncData.mu.Unlock() +} + +func (wallet *Wallet) IsSyncProgressListenerRegisteredFor(uniqueIdentifier string) bool { + wallet.syncData.mu.RLock() + _, exists := wallet.syncData.SyncProgressListeners[uniqueIdentifier] + wallet.syncData.mu.RUnlock() + return exists +} + +func (wallet *Wallet) AddSyncProgressListener(syncProgressListener SyncProgressListener, uniqueIdentifier string) error { + if wallet.IsSyncProgressListenerRegisteredFor(uniqueIdentifier) { + return errors.New(ErrListenerAlreadyExist) + } + + wallet.syncData.mu.Lock() + wallet.syncData.SyncProgressListeners[uniqueIdentifier] = syncProgressListener + wallet.syncData.mu.Unlock() + + // If sync is already on, notify this newly added listener of the current progress report. + return wallet.PublishLastSyncProgress(uniqueIdentifier) +} + +func (wallet *Wallet) RemoveSyncProgressListener(uniqueIdentifier string) { + wallet.syncData.mu.Lock() + delete(wallet.syncData.SyncProgressListeners, uniqueIdentifier) + wallet.syncData.mu.Unlock() +} + +func (wallet *Wallet) syncProgressListeners() []SyncProgressListener { + wallet.syncData.mu.RLock() + defer wallet.syncData.mu.RUnlock() + + listeners := make([]SyncProgressListener, 0, len(wallet.syncData.SyncProgressListeners)) + for _, listener := range wallet.syncData.SyncProgressListeners { + listeners = append(listeners, listener) + } + + return listeners +} + +func (wallet *Wallet) PublishLastSyncProgress(uniqueIdentifier string) error { + wallet.syncData.mu.RLock() + defer wallet.syncData.mu.RUnlock() + + syncProgressListener, exists := wallet.syncData.SyncProgressListeners[uniqueIdentifier] + if !exists { + return errors.New(ErrInvalid) + } + + if wallet.syncData.syncing && wallet.syncData.activeSyncData != nil { + switch wallet.syncData.activeSyncData.syncStage { + case HeadersFetchSyncStage: + syncProgressListener.OnHeadersFetchProgress(&wallet.syncData.headersFetchProgress) + case AddressDiscoverySyncStage: + syncProgressListener.OnAddressDiscoveryProgress(&wallet.syncData.addressDiscoveryProgress) + case HeadersRescanSyncStage: + syncProgressListener.OnHeadersRescanProgress(&wallet.syncData.headersRescanProgress) + } + } + + return nil +} + +func (wallet *Wallet) EnableSyncLogs() { + wallet.syncData.mu.Lock() + wallet.syncData.showLogs = true + wallet.syncData.mu.Unlock() +} + +func (wallet *Wallet) SyncInactiveForPeriod(totalInactiveSeconds int64) { + wallet.syncData.mu.Lock() + defer wallet.syncData.mu.Unlock() + + if !wallet.syncData.syncing || wallet.syncData.activeSyncData == nil { + log.Debug("Not accounting for inactive time, wallet is not syncing.") + return + } + + wallet.syncData.totalInactiveSeconds += totalInactiveSeconds + if wallet.syncData.connectedPeers == 0 { + // assume it would take another 60 seconds to reconnect to peers + wallet.syncData.totalInactiveSeconds += 60 + } +} + +func (wallet *Wallet) SpvSync() error { + // prevent an attempt to sync when the previous syncing has not been canceled + if wallet.IsSyncing() || wallet.IsSynced() { + return errors.New(ErrSyncAlreadyInProgress) + } + + addr := &net.TCPAddr{IP: net.ParseIP("::1"), Port: 0} + addrManager := addrmgr.New(wallet.rootDir, net.LookupIP) // TODO: be mindful of tor + lp := p2p.NewLocalPeer(wallet.chainParams, addr, addrManager) + + var validPeerAddresses []string + peerAddresses := wallet.ReadStringConfigValueForKey(SpvPersistentPeerAddressesConfigKey, "") + if peerAddresses != "" { + addresses := strings.Split(peerAddresses, ";") + for _, address := range addresses { + peerAddress, err := NormalizeAddress(address, wallet.chainParams.DefaultPort) + if err != nil { + log.Errorf("SPV peer address(%s) is invalid: %v", peerAddress, err) + } else { + validPeerAddresses = append(validPeerAddresses, peerAddress) + } + } + + if len(validPeerAddresses) == 0 { + return errors.New(ErrInvalidPeers) + } + } + + // init activeSyncData to be used to hold data used + // to calculate sync estimates only during sync + wallet.initActiveSyncData() + + wallets := make(map[int]*w.Wallet) + wallets[wallet.ID] = wallet.Internal() + wallet.WaitingForHeaders = true + wallet.Syncing = true + + syncer := spv.NewSyncer(wallets, lp) + syncer.SetNotifications(wallet.spvSyncNotificationCallbacks()) + if len(validPeerAddresses) > 0 { + syncer.SetPersistentPeers(validPeerAddresses) + } + + ctx, cancel := wallet.contextWithShutdownCancel() + + var restartSyncRequested bool + + wallet.syncData.mu.Lock() + restartSyncRequested = wallet.syncData.restartSyncRequested + wallet.syncData.restartSyncRequested = false + wallet.syncData.syncing = true + wallet.syncData.cancelSync = cancel + wallet.syncData.syncCanceled = make(chan struct{}) + wallet.syncData.syncer = syncer + wallet.syncData.mu.Unlock() + + for _, listener := range wallet.syncProgressListeners() { + listener.OnSyncStarted(restartSyncRequested) + } + + // syncer.Run uses a wait group to block the thread until the sync context + // expires or is canceled or some other error occurs such as + // losing connection to all persistent peers. + go func() { + syncError := syncer.Run(ctx) + //sync has ended or errored + if syncError != nil { + if syncError == context.DeadlineExceeded { + wallet.notifySyncError(errors.Errorf("SPV synchronization deadline exceeded: %v", syncError)) + } else if syncError == context.Canceled { + close(wallet.syncData.syncCanceled) + wallet.notifySyncCanceled() + } else { + wallet.notifySyncError(syncError) + } + } + + //reset sync variables + wallet.resetSyncData() + }() + return nil +} + +func (wallet *Wallet) RestartSpvSync() error { + wallet.syncData.mu.Lock() + wallet.syncData.restartSyncRequested = true + wallet.syncData.mu.Unlock() + + wallet.CancelSync() // necessary to unset the network backend. + return wallet.SpvSync() +} + +func (wallet *Wallet) CancelSync() { + wallet.syncData.mu.RLock() + cancelSync := wallet.syncData.cancelSync + wallet.syncData.mu.RUnlock() + + if cancelSync != nil { + log.Info("Canceling sync. May take a while for sync to fully cancel.") + + // Stop running cspp mixers + if wallet.IsAccountMixerActive() { + log.Infof("[%d] Stopping cspp mixer", wallet.ID) + err := wallet.StopAccountMixer() + if err != nil { + log.Errorf("[%d] Error stopping cspp mixer: %v", wallet.ID, err) + } + } + + // Cancel the context used for syncer.Run in spvSync(). + // This may not immediately cause the sync process to terminate, + // but when it eventually terminates, syncer.Run will return `err == context.Canceled`. + cancelSync() + + // When sync terminates and syncer.Run returns `err == context.Canceled`, + // we will get notified on this channel. + <-wallet.syncData.syncCanceled + + log.Info("Sync fully canceled.") + } +} + +func (wallet *Wallet) IsWaiting() bool { + return wallet.WaitingForHeaders +} + +func (wallet *Wallet) IsSynced() bool { + return wallet.Synced +} + +func (wallet *Wallet) IsSyncing() bool { + return wallet.Syncing +} + +func (wallet *Wallet) IsConnectedToDecredNetwork() bool { + wallet.syncData.mu.RLock() + defer wallet.syncData.mu.RUnlock() + return wallet.syncData.syncing || wallet.syncData.synced +} + +func (wallet *Wallet) isSynced() bool { + wallet.syncData.mu.RLock() + defer wallet.syncData.mu.RUnlock() + return wallet.syncData.synced +} + +func (wallet *Wallet) isSyncing() bool { + wallet.syncData.mu.RLock() + defer wallet.syncData.mu.RUnlock() + return wallet.syncData.syncing +} + +func (wallet *Wallet) CurrentSyncStage() int32 { + wallet.syncData.mu.RLock() + defer wallet.syncData.mu.RUnlock() + + if wallet.syncData != nil && wallet.syncData.syncing { + return wallet.syncData.syncStage + } + return InvalidSyncStage +} + +func (wallet *Wallet) GeneralSyncProgress() *GeneralSyncProgress { + wallet.syncData.mu.RLock() + defer wallet.syncData.mu.RUnlock() + + if wallet.syncData != nil && wallet.syncData.syncing { + switch wallet.syncData.syncStage { + case HeadersFetchSyncStage: + return wallet.syncData.headersFetchProgress.GeneralSyncProgress + case AddressDiscoverySyncStage: + return wallet.syncData.addressDiscoveryProgress.GeneralSyncProgress + case HeadersRescanSyncStage: + return wallet.syncData.headersRescanProgress.GeneralSyncProgress + case CFiltersFetchSyncStage: + return wallet.syncData.cfiltersFetchProgress.GeneralSyncProgress + } + } + + return nil +} + +func (wallet *Wallet) ConnectedPeers() int32 { + wallet.syncData.mu.RLock() + defer wallet.syncData.mu.RUnlock() + return wallet.syncData.connectedPeers +} + +func (wallet *Wallet) PeerInfoRaw() ([]PeerInfo, error) { + if !wallet.IsConnectedToDecredNetwork() { + return nil, errors.New(ErrNotConnected) + } + + syncer := wallet.syncData.syncer + + infos := make([]PeerInfo, 0, len(syncer.GetRemotePeers())) + for _, rp := range syncer.GetRemotePeers() { + info := PeerInfo{ + ID: int32(rp.ID()), + Addr: rp.RemoteAddr().String(), + AddrLocal: rp.LocalAddr().String(), + Services: fmt.Sprintf("%08d", uint64(rp.Services())), + Version: rp.Pver(), + SubVer: rp.UA(), + StartingHeight: int64(rp.InitialHeight()), + BanScore: int32(rp.BanScore()), + } + + infos = append(infos, info) + } + + sort.Slice(infos, func(i, j int) bool { + return infos[i].ID < infos[j].ID + }) + + return infos, nil +} + +func (wallet *Wallet) PeerInfo() (string, error) { + infos, err := wallet.PeerInfoRaw() + if err != nil { + return "", err + } + + result, _ := json.Marshal(infos) + return string(result), nil +} + +func (wallet *Wallet) GetBestBlock() *BlockInfo { + var bestBlock int32 = -1 + var blockInfo *BlockInfo + if !wallet.WalletOpened() { + return nil + } + + walletBestBLock := wallet.getBestBlock() + if walletBestBLock > bestBlock || bestBlock == -1 { + bestBlock = walletBestBLock + blockInfo = &BlockInfo{Height: bestBlock, Timestamp: wallet.GetBestBlockTimeStamp()} + } + + return blockInfo +} + +func (wallet *Wallet) GetLowestBlock() *BlockInfo { + var lowestBlock int32 = -1 + var blockInfo *BlockInfo + if !wallet.WalletOpened() { + return nil + } + walletBestBLock := wallet.getBestBlock() + if walletBestBLock < lowestBlock || lowestBlock == -1 { + lowestBlock = walletBestBLock + blockInfo = &BlockInfo{Height: lowestBlock, Timestamp: wallet.GetBestBlockTimeStamp()} + } + + return blockInfo +} + +func (wallet *Wallet) getBestBlock() int32 { + if wallet.Internal() == nil { + // This method is sometimes called after a wallet is deleted and causes crash. + log.Error("Attempting to read best block height without a loaded wallet.") + return 0 + } + + _, height := wallet.Internal().MainChainTip(wallet.ShutdownContext()) + return height +} + +func (wallet *Wallet) GetBestBlockTimeStamp() int64 { + if wallet.Internal() == nil { + // This method is sometimes called after a wallet is deleted and causes crash. + log.Error("Attempting to read best block timestamp without a loaded wallet.") + return 0 + } + + ctx := wallet.ShutdownContext() + _, height := wallet.Internal().MainChainTip(ctx) + identifier := w.NewBlockIdentifierFromHeight(height) + info, err := wallet.Internal().BlockInfo(ctx, identifier) + if err != nil { + log.Error(err) + return 0 + } + return info.Timestamp +} + +func (wallet *Wallet) GetLowestBlockTimestamp() int64 { + var timestamp int64 = -1 + bestBlockTimestamp := wallet.GetBestBlockTimeStamp() + if bestBlockTimestamp < timestamp || timestamp == -1 { + timestamp = bestBlockTimestamp + } + + return timestamp +} diff --git a/wallets/dcr/syncnotification.go b/wallets/dcr/syncnotification.go new file mode 100644 index 000000000..e06b36cdf --- /dev/null +++ b/wallets/dcr/syncnotification.go @@ -0,0 +1,678 @@ +package dcr + +import ( + "math" + "time" + + "github.com/planetdecred/dcrlibwallet/spv" + "golang.org/x/sync/errgroup" +) + +func (w *Wallet) spvSyncNotificationCallbacks() *spv.Notifications { + return &spv.Notifications{ + PeerConnected: func(peerCount int32, addr string) { + w.handlePeerCountUpdate(peerCount) + }, + PeerDisconnected: func(peerCount int32, addr string) { + w.handlePeerCountUpdate(peerCount) + }, + Synced: w.synced, + FetchHeadersStarted: w.fetchHeadersStarted, + FetchHeadersProgress: w.fetchHeadersProgress, + FetchHeadersFinished: w.fetchHeadersFinished, + FetchMissingCFiltersStarted: w.fetchCFiltersStarted, + FetchMissingCFiltersProgress: w.fetchCFiltersProgress, + FetchMissingCFiltersFinished: w.fetchCFiltersEnded, + DiscoverAddressesStarted: w.discoverAddressesStarted, + DiscoverAddressesFinished: w.discoverAddressesFinished, + RescanStarted: w.rescanStarted, + RescanProgress: w.rescanProgress, + RescanFinished: w.rescanFinished, + } +} + +func (w *Wallet) handlePeerCountUpdate(peerCount int32) { + w.syncData.mu.Lock() + w.syncData.connectedPeers = peerCount + shouldLog := w.syncData.showLogs && w.syncData.syncing + w.syncData.mu.Unlock() + + for _, syncProgressListener := range w.syncProgressListeners() { + syncProgressListener.OnPeerConnectedOrDisconnected(peerCount) + } + + if shouldLog { + if peerCount == 1 { + log.Infof("Connected to %d peer on %s.", peerCount, w.chainParams.Name) + } else { + log.Infof("Connected to %d peers on %s.", peerCount, w.chainParams.Name) + } + } +} + +// Fetch CFilters Callbacks + +func (w *Wallet) fetchCFiltersStarted(walletID int) { + w.syncData.mu.Lock() + w.syncData.activeSyncData.syncStage = CFiltersFetchSyncStage + w.syncData.activeSyncData.cfiltersFetchProgress.beginFetchCFiltersTimeStamp = time.Now().Unix() + w.syncData.activeSyncData.cfiltersFetchProgress.totalFetchedCFiltersCount = 0 + showLogs := w.syncData.showLogs + w.syncData.mu.Unlock() + + if showLogs { + log.Infof("Step 1 of 3 - fetching %d block headers.") + } +} + +func (w *Wallet) fetchCFiltersProgress(walletID int, startCFiltersHeight, endCFiltersHeight int32) { + + // lock the mutex before reading and writing to w.syncData.* + w.syncData.mu.Lock() + + if w.syncData.activeSyncData.cfiltersFetchProgress.startCFiltersHeight == -1 { + w.syncData.activeSyncData.cfiltersFetchProgress.startCFiltersHeight = startCFiltersHeight + } + + // wallet := w.WalletWithID(walletID) + w.syncData.activeSyncData.cfiltersFetchProgress.totalFetchedCFiltersCount += endCFiltersHeight - startCFiltersHeight + + totalCFiltersToFetch := w.getBestBlock() - w.syncData.activeSyncData.cfiltersFetchProgress.startCFiltersHeight + // cfiltersLeftToFetch := totalCFiltersToFetch - w.syncData.activeSyncData.cfiltersFetchProgress.totalFetchedCFiltersCount + + cfiltersFetchProgress := float64(w.syncData.activeSyncData.cfiltersFetchProgress.totalFetchedCFiltersCount) / float64(totalCFiltersToFetch) + + // If there was some period of inactivity, + // assume that this process started at some point in the future, + // thereby accounting for the total reported time of inactivity. + w.syncData.activeSyncData.cfiltersFetchProgress.beginFetchCFiltersTimeStamp += w.syncData.activeSyncData.totalInactiveSeconds + w.syncData.activeSyncData.totalInactiveSeconds = 0 + + timeTakenSoFar := time.Now().Unix() - w.syncData.activeSyncData.cfiltersFetchProgress.beginFetchCFiltersTimeStamp + if timeTakenSoFar < 1 { + timeTakenSoFar = 1 + } + estimatedTotalCFiltersFetchTime := float64(timeTakenSoFar) / cfiltersFetchProgress + + // Use CFilters fetch rate to estimate headers fetch time. + cfiltersFetchRate := float64(w.syncData.activeSyncData.cfiltersFetchProgress.totalFetchedCFiltersCount) / float64(timeTakenSoFar) + estimatedHeadersLeftToFetch := w.estimateBlockHeadersCountAfter(w.GetBestBlockTimeStamp()) + estimatedTotalHeadersFetchTime := float64(estimatedHeadersLeftToFetch) / cfiltersFetchRate + // increase estimated value by FetchPercentage + estimatedTotalHeadersFetchTime /= FetchPercentage + + estimatedDiscoveryTime := estimatedTotalHeadersFetchTime * DiscoveryPercentage + estimatedRescanTime := estimatedTotalHeadersFetchTime * RescanPercentage + estimatedTotalSyncTime := estimatedTotalCFiltersFetchTime + estimatedTotalHeadersFetchTime + estimatedDiscoveryTime + estimatedRescanTime + + totalSyncProgress := float64(timeTakenSoFar) / estimatedTotalSyncTime + totalTimeRemainingSeconds := int64(math.Round(estimatedTotalSyncTime)) - timeTakenSoFar + + // update headers fetching progress report including total progress percentage and total time remaining + w.syncData.activeSyncData.cfiltersFetchProgress.TotalCFiltersToFetch = totalCFiltersToFetch + w.syncData.activeSyncData.cfiltersFetchProgress.CurrentCFilterHeight = startCFiltersHeight + w.syncData.activeSyncData.cfiltersFetchProgress.CFiltersFetchProgress = roundUp(cfiltersFetchProgress * 100.0) + w.syncData.activeSyncData.cfiltersFetchProgress.TotalSyncProgress = roundUp(totalSyncProgress * 100.0) + w.syncData.activeSyncData.cfiltersFetchProgress.TotalTimeRemainingSeconds = totalTimeRemainingSeconds + + w.syncData.mu.Unlock() + + // notify progress listener of estimated progress report + w.publishFetchCFiltersProgress() + + cfiltersFetchTimeRemaining := estimatedTotalCFiltersFetchTime - float64(timeTakenSoFar) + debugInfo := &DebugInfo{ + timeTakenSoFar, + totalTimeRemainingSeconds, + timeTakenSoFar, + int64(math.Round(cfiltersFetchTimeRemaining)), + } + w.publishDebugInfo(debugInfo) +} + +func (w *Wallet) publishFetchCFiltersProgress() { + for _, syncProgressListener := range w.syncProgressListeners() { + syncProgressListener.OnCFiltersFetchProgress(&w.syncData.cfiltersFetchProgress) + } +} + +func (w *Wallet) fetchCFiltersEnded(walletID int) { + w.syncData.mu.Lock() + defer w.syncData.mu.Unlock() + + w.syncData.activeSyncData.cfiltersFetchProgress.cfiltersFetchTimeSpent = time.Now().Unix() - w.syncData.cfiltersFetchProgress.beginFetchCFiltersTimeStamp + + // If there is some period of inactivity reported at this stage, + // subtract it from the total stage time. + w.syncData.activeSyncData.cfiltersFetchProgress.cfiltersFetchTimeSpent -= w.syncData.totalInactiveSeconds + w.syncData.activeSyncData.totalInactiveSeconds = 0 +} + +// Fetch Headers Callbacks + +func (w *Wallet) fetchHeadersStarted(peerInitialHeight int32) { + if !w.IsSyncing() { + return + } + + w.syncData.mu.RLock() + headersFetchingStarted := w.syncData.headersFetchProgress.beginFetchTimeStamp != -1 + showLogs := w.syncData.showLogs + w.syncData.mu.RUnlock() + + if headersFetchingStarted { + // This function gets called for each newly connected peer so + // ignore if headers fetching was already started. + return + } + + w.WaitingForHeaders = true + + lowestBlockHeight := w.GetLowestBlock().Height + + w.syncData.mu.Lock() + w.syncData.activeSyncData.syncStage = HeadersFetchSyncStage + w.syncData.activeSyncData.headersFetchProgress.beginFetchTimeStamp = time.Now().Unix() + w.syncData.activeSyncData.headersFetchProgress.startHeaderHeight = lowestBlockHeight + w.syncData.headersFetchProgress.totalFetchedHeadersCount = 0 + w.syncData.activeSyncData.totalInactiveSeconds = 0 + w.syncData.mu.Unlock() + + if showLogs { + log.Infof("Step 1 of 3 - fetching %d block headers.", peerInitialHeight-lowestBlockHeight) + } +} + +func (w *Wallet) fetchHeadersProgress(lastFetchedHeaderHeight int32, lastFetchedHeaderTime int64) { + if !w.IsSyncing() { + return + } + + w.syncData.mu.RLock() + headersFetchingCompleted := w.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent != -1 + w.syncData.mu.RUnlock() + + if headersFetchingCompleted { + // This function gets called for each newly connected peer so ignore + // this call if the headers fetching phase was previously completed. + return + } + + // for _, wallet := range w.wallets { + if w.WaitingForHeaders { + w.WaitingForHeaders = w.getBestBlock() > lastFetchedHeaderHeight + } + // } + + // lock the mutex before reading and writing to w.syncData.* + w.syncData.mu.Lock() + + if lastFetchedHeaderHeight > w.syncData.activeSyncData.headersFetchProgress.startHeaderHeight { + w.syncData.activeSyncData.headersFetchProgress.totalFetchedHeadersCount = lastFetchedHeaderHeight - w.syncData.activeSyncData.headersFetchProgress.startHeaderHeight + } + + headersLeftToFetch := w.estimateBlockHeadersCountAfter(lastFetchedHeaderTime) + totalHeadersToFetch := lastFetchedHeaderHeight + headersLeftToFetch + headersFetchProgress := float64(w.syncData.activeSyncData.headersFetchProgress.totalFetchedHeadersCount) / float64(totalHeadersToFetch) + + // If there was some period of inactivity, + // assume that this process started at some point in the future, + // thereby accounting for the total reported time of inactivity. + w.syncData.activeSyncData.headersFetchProgress.beginFetchTimeStamp += w.syncData.activeSyncData.totalInactiveSeconds + w.syncData.activeSyncData.totalInactiveSeconds = 0 + + fetchTimeTakenSoFar := time.Now().Unix() - w.syncData.activeSyncData.headersFetchProgress.beginFetchTimeStamp + if fetchTimeTakenSoFar < 1 { + fetchTimeTakenSoFar = 1 + } + estimatedTotalHeadersFetchTime := float64(fetchTimeTakenSoFar) / headersFetchProgress + + // For some reason, the actual total headers fetch time is more than the predicted/estimated time. + // Account for this difference by multiplying the estimatedTotalHeadersFetchTime by an incrementing factor. + // The incrementing factor is inversely proportional to the headers fetch progress, + // ranging from 0.5 to 0 as headers fetching progress increases from 0 to 1. + // todo, the above noted (mal)calculation may explain this difference. + // TODO: is this adjustment still needed since the calculation has been corrected. + adjustmentFactor := 0.5 * (1 - headersFetchProgress) + estimatedTotalHeadersFetchTime += estimatedTotalHeadersFetchTime * adjustmentFactor + + estimatedDiscoveryTime := estimatedTotalHeadersFetchTime * DiscoveryPercentage + estimatedRescanTime := estimatedTotalHeadersFetchTime * RescanPercentage + estimatedTotalSyncTime := float64(w.syncData.activeSyncData.cfiltersFetchProgress.cfiltersFetchTimeSpent) + + estimatedTotalHeadersFetchTime + estimatedDiscoveryTime + estimatedRescanTime + + totalSyncProgress := float64(fetchTimeTakenSoFar) / estimatedTotalSyncTime + totalTimeRemainingSeconds := int64(math.Round(estimatedTotalSyncTime)) - fetchTimeTakenSoFar + + // update headers fetching progress report including total progress percentage and total time remaining + w.syncData.activeSyncData.headersFetchProgress.TotalHeadersToFetch = totalHeadersToFetch + w.syncData.activeSyncData.headersFetchProgress.CurrentHeaderHeight = lastFetchedHeaderHeight + w.syncData.activeSyncData.headersFetchProgress.CurrentHeaderTimestamp = lastFetchedHeaderTime + w.syncData.activeSyncData.headersFetchProgress.HeadersFetchProgress = roundUp(headersFetchProgress * 100.0) + w.syncData.activeSyncData.headersFetchProgress.TotalSyncProgress = roundUp(totalSyncProgress * 100.0) + w.syncData.activeSyncData.headersFetchProgress.TotalTimeRemainingSeconds = totalTimeRemainingSeconds + + // unlock the mutex before issuing notification callbacks to prevent potential deadlock + // if any invoked callback takes a considerable amount of time to execute. + w.syncData.mu.Unlock() + + // notify progress listener of estimated progress report + w.publishFetchHeadersProgress() + + // todo: also log report if showLog == true + timeTakenSoFar := w.syncData.activeSyncData.cfiltersFetchProgress.cfiltersFetchTimeSpent + fetchTimeTakenSoFar + headersFetchTimeRemaining := estimatedTotalHeadersFetchTime - float64(fetchTimeTakenSoFar) + debugInfo := &DebugInfo{ + timeTakenSoFar, + totalTimeRemainingSeconds, + fetchTimeTakenSoFar, + int64(math.Round(headersFetchTimeRemaining)), + } + w.publishDebugInfo(debugInfo) +} + +func (w *Wallet) publishFetchHeadersProgress() { + for _, syncProgressListener := range w.syncProgressListeners() { + syncProgressListener.OnHeadersFetchProgress(&w.syncData.headersFetchProgress) + } +} + +func (w *Wallet) fetchHeadersFinished() { + w.syncData.mu.Lock() + defer w.syncData.mu.Unlock() + + if !w.syncData.syncing { + // ignore if sync is not in progress + return + } + + w.syncData.activeSyncData.headersFetchProgress.startHeaderHeight = -1 + w.syncData.headersFetchProgress.totalFetchedHeadersCount = 0 + w.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent = time.Now().Unix() - w.syncData.headersFetchProgress.beginFetchTimeStamp + + // If there is some period of inactivity reported at this stage, + // subtract it from the total stage time. + w.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent -= w.syncData.totalInactiveSeconds + w.syncData.activeSyncData.totalInactiveSeconds = 0 + + if w.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent < 150 { + // This ensures that minimum ETA used for stage 2 (address discovery) is 120 seconds (80% of 150 seconds). + w.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent = 150 + } + + if w.syncData.showLogs && w.syncData.syncing { + log.Info("Fetch headers completed.") + } +} + +// Address/Account Discovery Callbacks + +func (w *Wallet) discoverAddressesStarted(walletID int) { + if !w.IsSyncing() { + return + } + + w.syncData.mu.RLock() + addressDiscoveryAlreadyStarted := w.syncData.activeSyncData.addressDiscoveryProgress.addressDiscoveryStartTime != -1 + totalHeadersFetchTime := float64(w.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent) + w.syncData.mu.RUnlock() + + if addressDiscoveryAlreadyStarted { + return + } + + w.syncData.mu.Lock() + w.syncData.activeSyncData.syncStage = AddressDiscoverySyncStage + w.syncData.activeSyncData.addressDiscoveryProgress.addressDiscoveryStartTime = time.Now().Unix() + w.syncData.activeSyncData.addressDiscoveryProgress.WalletID = walletID + w.syncData.addressDiscoveryCompletedOrCanceled = make(chan bool) + w.syncData.mu.Unlock() + + go w.updateAddressDiscoveryProgress(totalHeadersFetchTime) + + if w.syncData.showLogs { + log.Info("Step 2 of 3 - discovering used addresses.") + } +} + +func (w *Wallet) updateAddressDiscoveryProgress(totalHeadersFetchTime float64) { + // use ticker to calculate and broadcast address discovery progress every second + everySecondTicker := time.NewTicker(1 * time.Second) + + // these values will be used every second to calculate the total sync progress + estimatedDiscoveryTime := totalHeadersFetchTime * DiscoveryPercentage + estimatedRescanTime := totalHeadersFetchTime * RescanPercentage + + // track last logged time remaining and total percent to avoid re-logging same message + var lastTimeRemaining int64 + var lastTotalPercent int32 = -1 + + for { + if !w.IsSyncing() { + return + } + + // If there was some period of inactivity, + // assume that this process started at some point in the future, + // thereby accounting for the total reported time of inactivity. + w.syncData.mu.Lock() + w.syncData.addressDiscoveryProgress.addressDiscoveryStartTime += w.syncData.totalInactiveSeconds + w.syncData.totalInactiveSeconds = 0 + addressDiscoveryStartTime := w.syncData.addressDiscoveryProgress.addressDiscoveryStartTime + totalCfiltersFetchTime := float64(w.syncData.cfiltersFetchProgress.cfiltersFetchTimeSpent) + showLogs := w.syncData.showLogs + w.syncData.mu.Unlock() + + select { + case <-w.syncData.addressDiscoveryCompletedOrCanceled: + // stop calculating and broadcasting address discovery progress + everySecondTicker.Stop() + if showLogs { + log.Info("Address discovery complete.") + } + return + + case <-everySecondTicker.C: + // calculate address discovery progress + elapsedDiscoveryTime := float64(time.Now().Unix() - addressDiscoveryStartTime) + discoveryProgress := (elapsedDiscoveryTime / estimatedDiscoveryTime) * 100 + + var totalSyncTime float64 + if elapsedDiscoveryTime > estimatedDiscoveryTime { + totalSyncTime = totalCfiltersFetchTime + totalHeadersFetchTime + elapsedDiscoveryTime + estimatedRescanTime + } else { + totalSyncTime = totalCfiltersFetchTime + totalHeadersFetchTime + estimatedDiscoveryTime + estimatedRescanTime + } + + totalElapsedTime := totalCfiltersFetchTime + totalHeadersFetchTime + elapsedDiscoveryTime + totalProgress := (totalElapsedTime / totalSyncTime) * 100 + + remainingAccountDiscoveryTime := math.Round(estimatedDiscoveryTime - elapsedDiscoveryTime) + if remainingAccountDiscoveryTime < 0 { + remainingAccountDiscoveryTime = 0 + } + + totalProgressPercent := int32(math.Round(totalProgress)) + totalTimeRemainingSeconds := int64(math.Round(remainingAccountDiscoveryTime + estimatedRescanTime)) + + // update address discovery progress, total progress and total time remaining + w.syncData.mu.Lock() + w.syncData.addressDiscoveryProgress.AddressDiscoveryProgress = int32(math.Round(discoveryProgress)) + w.syncData.addressDiscoveryProgress.TotalSyncProgress = totalProgressPercent + w.syncData.addressDiscoveryProgress.TotalTimeRemainingSeconds = totalTimeRemainingSeconds + w.syncData.mu.Unlock() + + w.publishAddressDiscoveryProgress() + + debugInfo := &DebugInfo{ + int64(math.Round(totalElapsedTime)), + totalTimeRemainingSeconds, + int64(math.Round(elapsedDiscoveryTime)), + int64(math.Round(remainingAccountDiscoveryTime)), + } + w.publishDebugInfo(debugInfo) + + if showLogs { + // avoid logging same message multiple times + if totalProgressPercent != lastTotalPercent || totalTimeRemainingSeconds != lastTimeRemaining { + log.Infof("Syncing %d%%, %s remaining, discovering used addresses.", + totalProgressPercent, CalculateTotalTimeRemaining(totalTimeRemainingSeconds)) + + lastTotalPercent = totalProgressPercent + lastTimeRemaining = totalTimeRemainingSeconds + } + } + } + } +} + +func (w *Wallet) publishAddressDiscoveryProgress() { + for _, syncProgressListener := range w.syncProgressListeners() { + syncProgressListener.OnAddressDiscoveryProgress(&w.syncData.activeSyncData.addressDiscoveryProgress) + } +} + +func (w *Wallet) discoverAddressesFinished(walletID int) { + if !w.IsSyncing() { + return + } + + w.stopUpdatingAddressDiscoveryProgress() +} + +func (w *Wallet) stopUpdatingAddressDiscoveryProgress() { + w.syncData.mu.Lock() + if w.syncData.activeSyncData != nil && w.syncData.activeSyncData.addressDiscoveryCompletedOrCanceled != nil { + close(w.syncData.activeSyncData.addressDiscoveryCompletedOrCanceled) + w.syncData.activeSyncData.addressDiscoveryCompletedOrCanceled = nil + w.syncData.activeSyncData.addressDiscoveryProgress.totalDiscoveryTimeSpent = time.Now().Unix() - w.syncData.addressDiscoveryProgress.addressDiscoveryStartTime + } + w.syncData.mu.Unlock() +} + +// Blocks Scan Callbacks + +func (w *Wallet) rescanStarted(walletID int) { + w.stopUpdatingAddressDiscoveryProgress() + + w.syncData.mu.Lock() + defer w.syncData.mu.Unlock() + + if !w.syncData.syncing { + // ignore if sync is not in progress + return + } + + w.syncData.activeSyncData.syncStage = HeadersRescanSyncStage + w.syncData.activeSyncData.rescanStartTime = time.Now().Unix() + + // retain last total progress report from address discovery phase + w.syncData.activeSyncData.headersRescanProgress.TotalTimeRemainingSeconds = w.syncData.activeSyncData.addressDiscoveryProgress.TotalTimeRemainingSeconds + w.syncData.activeSyncData.headersRescanProgress.TotalSyncProgress = w.syncData.activeSyncData.addressDiscoveryProgress.TotalSyncProgress + w.syncData.activeSyncData.headersRescanProgress.WalletID = walletID + + if w.syncData.showLogs && w.syncData.syncing { + log.Info("Step 3 of 3 - Scanning block headers.") + } +} + +func (w *Wallet) rescanProgress(walletID int, rescannedThrough int32) { + if !w.IsSyncing() { + // ignore if sync is not in progress + return + } + + totalHeadersToScan := w.getBestBlock() + + rescanRate := float64(rescannedThrough) / float64(totalHeadersToScan) + + w.syncData.mu.Lock() + + // If there was some period of inactivity, + // assume that this process started at some point in the future, + // thereby accounting for the total reported time of inactivity. + w.syncData.activeSyncData.rescanStartTime += w.syncData.activeSyncData.totalInactiveSeconds + w.syncData.activeSyncData.totalInactiveSeconds = 0 + + elapsedRescanTime := time.Now().Unix() - w.syncData.activeSyncData.rescanStartTime + estimatedTotalRescanTime := int64(math.Round(float64(elapsedRescanTime) / rescanRate)) + totalTimeRemainingSeconds := estimatedTotalRescanTime - elapsedRescanTime + totalElapsedTime := w.syncData.activeSyncData.cfiltersFetchProgress.cfiltersFetchTimeSpent + w.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent + + w.syncData.activeSyncData.addressDiscoveryProgress.totalDiscoveryTimeSpent + elapsedRescanTime + + w.syncData.activeSyncData.headersRescanProgress.WalletID = walletID + w.syncData.activeSyncData.headersRescanProgress.TotalHeadersToScan = totalHeadersToScan + w.syncData.activeSyncData.headersRescanProgress.RescanProgress = int32(math.Round(rescanRate * 100)) + w.syncData.activeSyncData.headersRescanProgress.CurrentRescanHeight = rescannedThrough + w.syncData.activeSyncData.headersRescanProgress.RescanTimeRemaining = totalTimeRemainingSeconds + + // do not update total time taken and total progress percent if elapsedRescanTime is 0 + // because the estimatedTotalRescanTime will be inaccurate (also 0) + // which will make the estimatedTotalSyncTime equal to totalElapsedTime + // giving the wrong impression that the process is complete + if elapsedRescanTime > 0 { + estimatedTotalSyncTime := w.syncData.activeSyncData.cfiltersFetchProgress.cfiltersFetchTimeSpent + w.syncData.activeSyncData.headersFetchProgress.headersFetchTimeSpent + + w.syncData.activeSyncData.addressDiscoveryProgress.totalDiscoveryTimeSpent + estimatedTotalRescanTime + totalProgress := (float64(totalElapsedTime) / float64(estimatedTotalSyncTime)) * 100 + + w.syncData.activeSyncData.headersRescanProgress.TotalTimeRemainingSeconds = totalTimeRemainingSeconds + w.syncData.activeSyncData.headersRescanProgress.TotalSyncProgress = int32(math.Round(totalProgress)) + } + + w.syncData.mu.Unlock() + + w.publishHeadersRescanProgress() + + debugInfo := &DebugInfo{ + totalElapsedTime, + totalTimeRemainingSeconds, + elapsedRescanTime, + totalTimeRemainingSeconds, + } + w.publishDebugInfo(debugInfo) + + w.syncData.mu.RLock() + if w.syncData.showLogs { + log.Infof("Syncing %d%%, %s remaining, scanning %d of %d block headers.", + w.syncData.activeSyncData.headersRescanProgress.TotalSyncProgress, + CalculateTotalTimeRemaining(w.syncData.activeSyncData.headersRescanProgress.TotalTimeRemainingSeconds), + w.syncData.activeSyncData.headersRescanProgress.CurrentRescanHeight, + w.syncData.activeSyncData.headersRescanProgress.TotalHeadersToScan, + ) + } + w.syncData.mu.RUnlock() +} + +func (w *Wallet) publishHeadersRescanProgress() { + for _, syncProgressListener := range w.syncProgressListeners() { + syncProgressListener.OnHeadersRescanProgress(&w.syncData.activeSyncData.headersRescanProgress) + } +} + +func (w *Wallet) rescanFinished(walletID int) { + if !w.IsSyncing() { + // ignore if sync is not in progress + return + } + + w.syncData.mu.Lock() + w.syncData.activeSyncData.headersRescanProgress.WalletID = walletID + w.syncData.activeSyncData.headersRescanProgress.TotalTimeRemainingSeconds = 0 + w.syncData.activeSyncData.headersRescanProgress.TotalSyncProgress = 100 + + // Reset these value so that address discovery would + // not be skipped for the next wallet. + w.syncData.activeSyncData.addressDiscoveryProgress.addressDiscoveryStartTime = -1 + w.syncData.activeSyncData.addressDiscoveryProgress.totalDiscoveryTimeSpent = -1 + w.syncData.mu.Unlock() + + w.publishHeadersRescanProgress() +} + +func (w *Wallet) publishDebugInfo(debugInfo *DebugInfo) { + for _, syncProgressListener := range w.syncProgressListeners() { + syncProgressListener.Debug(debugInfo) + } +} + +/** Helper functions start here */ + +func (w *Wallet) estimateBlockHeadersCountAfter(lastHeaderTime int64) int32 { + // Use the difference between current time (now) and last reported block time, + // to estimate total headers to fetch. + timeDifferenceInSeconds := float64(time.Now().Unix() - lastHeaderTime) + targetTimePerBlockInSeconds := w.chainParams.TargetTimePerBlock.Seconds() + estimatedHeadersDifference := timeDifferenceInSeconds / targetTimePerBlockInSeconds + + // return next integer value (upper limit) if estimatedHeadersDifference is a fraction + return int32(math.Ceil(estimatedHeadersDifference)) +} + +func (w *Wallet) notifySyncError(err error) { + for _, syncProgressListener := range w.syncProgressListeners() { + syncProgressListener.OnSyncEndedWithError(err) + } +} + +func (w *Wallet) notifySyncCanceled() { + w.syncData.mu.RLock() + restartSyncRequested := w.syncData.restartSyncRequested + w.syncData.mu.RUnlock() + + for _, syncProgressListener := range w.syncProgressListeners() { + syncProgressListener.OnSyncCanceled(restartSyncRequested) + } +} + +func (w *Wallet) resetSyncData() { + // It's possible that sync ends or errors while address discovery is ongoing. + // If this happens, it's important to stop the address discovery process before + // resetting sync data. + w.stopUpdatingAddressDiscoveryProgress() + + w.syncData.mu.Lock() + w.syncData.syncing = false + w.syncData.synced = false + w.syncData.cancelSync = nil + w.syncData.syncCanceled = nil + w.syncData.activeSyncData = nil + w.syncData.mu.Unlock() + + w.WaitingForHeaders = true + w.LockWallet() // lock wallet if previously unlocked to perform account discovery. +} + +func (w *Wallet) synced(walletID int, synced bool) { + + indexTransactions := func() { + // begin indexing transactions after sync is completed, + // syncProgressListeners.OnSynced() will be invoked after transactions are indexed + var txIndexing errgroup.Group + txIndexing.Go(w.IndexTransactions) + + go func() { + err := txIndexing.Wait() + if err != nil { + log.Errorf("Tx Index Error: %v", err) + } + + for _, syncProgressListener := range w.syncProgressListeners() { + if synced { + syncProgressListener.OnSyncCompleted() + } else { + syncProgressListener.OnSyncCanceled(false) + } + } + }() + } + + w.syncData.mu.RLock() + allWalletsSynced := w.syncData.synced + w.syncData.mu.RUnlock() + + if allWalletsSynced && synced { + indexTransactions() + return + } + + w.Synced = synced + w.Syncing = false + w.listenForTransactions() + + if !w.Internal().Locked() { + w.LockWallet() // lock wallet if previously unlocked to perform account discovery. + err := w.markWalletAsDiscoveredAccounts() + if err != nil { + log.Error(err) + } + } + + // if w.OpenedWalletsCount() == w.SyncedWalletsCount() { + w.syncData.mu.Lock() + w.syncData.syncing = false + w.syncData.synced = true + w.syncData.mu.Unlock() + + indexTransactions() + // } +} diff --git a/ticket.go b/wallets/dcr/ticket.go similarity index 87% rename from ticket.go rename to wallets/dcr/ticket.go index e1b2de3a9..63b36313b 100644 --- a/ticket.go +++ b/wallets/dcr/ticket.go @@ -1,4 +1,4 @@ -package dcrlibwallet +package dcr import ( "context" @@ -30,26 +30,12 @@ func (wallet *Wallet) TotalStakingRewards() (int64, error) { return totalRewards, nil } -func (mw *MultiWallet) TotalStakingRewards() (int64, error) { - var totalRewards int64 - for _, wal := range mw.wallets { - walletTotalRewards, err := wal.TotalStakingRewards() - if err != nil { - return 0, err - } - - totalRewards += walletTotalRewards - } - - return totalRewards, nil -} - -func (mw *MultiWallet) TicketMaturity() int32 { - return int32(mw.chainParams.TicketMaturity) +func (wallet *Wallet) TicketMaturity() int32 { + return int32(wallet.chainParams.TicketMaturity) } -func (mw *MultiWallet) TicketExpiry() int32 { - return int32(mw.chainParams.TicketExpiry) +func (wallet *Wallet) TicketExpiry() int32 { + return int32(wallet.chainParams.TicketExpiry) } func (wallet *Wallet) StakingOverview() (stOverview *StakingOverview, err error) { @@ -91,34 +77,11 @@ func (wallet *Wallet) StakingOverview() (stOverview *StakingOverview, err error) return stOverview, nil } -func (mw *MultiWallet) StakingOverview() (stOverview *StakingOverview, err error) { - stOverview = &StakingOverview{} - - for _, wallet := range mw.wallets { - st, err := wallet.StakingOverview() - if err != nil { - return nil, err - } - - stOverview.Unmined += st.Unmined - stOverview.Immature += st.Immature - stOverview.Live += st.Live - stOverview.Voted += st.Voted - stOverview.Revoked += st.Revoked - stOverview.Expired += st.Expired - } - - stOverview.All = stOverview.Unmined + stOverview.Immature + stOverview.Live + stOverview.Voted + - stOverview.Revoked + stOverview.Expired - - return stOverview, nil -} - // TicketPrice returns the price of a ticket for the next block, also known as // the stake difficulty. May be incorrect if blockchain sync is ongoing or if // blockchain is not up-to-date. func (wallet *Wallet) TicketPrice() (*TicketPriceResponse, error) { - ctx := wallet.shutdownContext() + ctx := wallet.ShutdownContext() sdiff, err := wallet.Internal().NextStakeDifficulty(ctx) if err != nil { return nil, err @@ -132,22 +95,6 @@ func (wallet *Wallet) TicketPrice() (*TicketPriceResponse, error) { return resp, nil } -func (mw *MultiWallet) TicketPrice() (*TicketPriceResponse, error) { - bestBlock := mw.GetBestBlock() - for _, wal := range mw.wallets { - resp, err := wal.TicketPrice() - if err != nil { - return nil, err - } - - if resp.Height == bestBlock.Height { - return resp, nil - } - } - - return nil, errors.New(ErrWalletNotFound) -} - // PurchaseTickets purchases tickets from the wallet. // Returns a slice of hashes for tickets purchased. func (wallet *Wallet) PurchaseTickets(account, numTickets int32, vspHost string, vspPubKey []byte, passphrase []byte) ([]*chainhash.Hash, error) { @@ -194,7 +141,7 @@ func (wallet *Wallet) PurchaseTickets(account, numTickets int32, vspHost string, request.MixedSplitAccount = csppCfg.TicketSplitAccount } - ctx := wallet.shutdownContext() + ctx := wallet.ShutdownContext() ticketsResponse, err := wallet.Internal().PurchaseTickets(ctx, networkBackend, request) if err != nil { return nil, err @@ -205,11 +152,7 @@ func (wallet *Wallet) PurchaseTickets(account, numTickets int32, vspHost string, // VSPTicketInfo returns vsp-related info for a given ticket. Returns an error // if the ticket is not yet assigned to a VSP. -func (mw *MultiWallet) VSPTicketInfo(walletID int, hash string) (*VSPTicketInfo, error) { - wallet := mw.WalletWithID(walletID) - if wallet == nil { - return nil, fmt.Errorf("no wallet with ID %d", walletID) - } +func (wallet *Wallet) VSPTicketInfo(walletID int, hash string) (*VSPTicketInfo, error) { ticketHash, err := chainhash.NewHashFromStr(hash) if err != nil { @@ -217,7 +160,7 @@ func (mw *MultiWallet) VSPTicketInfo(walletID int, hash string) (*VSPTicketInfo, } // Read the VSP info for this ticket from the wallet db. - ctx := wallet.shutdownContext() + ctx := wallet.ShutdownContext() walletTicketInfo, err := wallet.Internal().VSPTicketInfo(ctx, ticketHash) if err != nil { return nil, err @@ -295,7 +238,7 @@ func (wallet *Wallet) StartTicketBuyer(passphrase []byte) error { return errors.New("Ticket buyer already running") } - ctx, cancel := wallet.shutdownContextWithCancel() + ctx, cancel := wallet.ShutdownContextWithCancel() wallet.cancelAutoTicketBuyer = cancel wallet.cancelAutoTicketBuyerMu.Unlock() @@ -545,11 +488,7 @@ func (wallet *Wallet) IsAutoTicketsPurchaseActive() bool { } // StopAutoTicketsPurchase stops the automatic ticket buyer. -func (mw *MultiWallet) StopAutoTicketsPurchase(walletID int) error { - wallet := mw.WalletWithID(walletID) - if wallet == nil { - return errors.New(ErrNotExist) - } +func (wallet *Wallet) StopAutoTicketsPurchase(walletID int) error { wallet.cancelAutoTicketBuyerMu.Lock() defer wallet.cancelAutoTicketBuyerMu.Unlock() @@ -590,28 +529,24 @@ func (wallet *Wallet) TicketBuyerConfigIsSet() bool { } // ClearTicketBuyerConfig clears the wallet's ticket buyer config. -func (mw *MultiWallet) ClearTicketBuyerConfig(walletID int) error { - wallet := mw.WalletWithID(walletID) - if wallet == nil { - return errors.New(ErrNotExist) - } +func (wallet *Wallet) ClearTicketBuyerConfig(walletID int) error { - mw.SetLongConfigValueForKey(TicketBuyerATMConfigKey, -1) - mw.SetInt32ConfigValueForKey(TicketBuyerAccountConfigKey, -1) - mw.SetStringConfigValueForKey(TicketBuyerVSPHostConfigKey, "") + wallet.SetLongConfigValueForKey(TicketBuyerATMConfigKey, -1) + wallet.SetInt32ConfigValueForKey(TicketBuyerAccountConfigKey, -1) + wallet.SetStringConfigValueForKey(TicketBuyerVSPHostConfigKey, "") return nil } // NextTicketPriceRemaining returns the remaning time in seconds of a ticket for the next block, // if secs equal 0 is imminent -func (mw *MultiWallet) NextTicketPriceRemaining() (secs int64, err error) { - params, er := utils.ChainParams(mw.chainParams.Name) +func (wallet *Wallet) NextTicketPriceRemaining() (secs int64, err error) { + params, er := utils.ChainParams(wallet.chainParams.Name) if er != nil { secs, err = -1, er return } - bestBestBlock := mw.GetBestBlock() + bestBestBlock := wallet.GetBestBlock() idxBlockInWindow := int(int64(bestBestBlock.Height)%params.StakeDiffWindowSize) + 1 blockTime := params.TargetTimePerBlock.Nanoseconds() windowSize := params.StakeDiffWindowSize diff --git a/transactions.go b/wallets/dcr/transactions.go similarity index 81% rename from transactions.go rename to wallets/dcr/transactions.go index 9bfb76844..f2d2d3a2a 100644 --- a/transactions.go +++ b/wallets/dcr/transactions.go @@ -1,13 +1,12 @@ -package dcrlibwallet +package dcr import ( "encoding/json" - "sort" "github.com/asdine/storm" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/planetdecred/dcrlibwallet/txhelper" - "github.com/planetdecred/dcrlibwallet/walletdata" + "github.com/planetdecred/dcrlibwallet/wallets/dcr/walletdata" ) const ( @@ -55,7 +54,7 @@ func (wallet *Wallet) PublishUnminedTransactions() error { return err } - return wallet.Internal().PublishUnminedTransactions(wallet.shutdownContext(), n) + return wallet.Internal().PublishUnminedTransactions(wallet.ShutdownContext(), n) } func (wallet *Wallet) GetTransaction(txHash string) (string, error) { @@ -80,7 +79,7 @@ func (wallet *Wallet) GetTransactionRaw(txHash string) (*Transaction, error) { return nil, err } - txSummary, _, blockHash, err := wallet.Internal().TransactionSummary(wallet.shutdownContext(), hash) + txSummary, _, blockHash, err := wallet.Internal().TransactionSummary(wallet.ShutdownContext(), hash) if err != nil { log.Error(err) return nil, err @@ -104,57 +103,16 @@ func (wallet *Wallet) GetTransactions(offset, limit, txFilter int32, newestFirst } func (wallet *Wallet) GetTransactionsRaw(offset, limit, txFilter int32, newestFirst bool) (transactions []Transaction, err error) { - err = wallet.walletDataDB.Read(offset, limit, txFilter, newestFirst, wallet.RequiredConfirmations(), wallet.GetBestBlock(), &transactions) + err = wallet.WalletDataDB.Read(offset, limit, txFilter, newestFirst, wallet.RequiredConfirmations(), wallet.getBestBlock(), &transactions) return } -func (mw *MultiWallet) GetTransactions(offset, limit, txFilter int32, newestFirst bool) (string, error) { - - transactions, err := mw.GetTransactionsRaw(offset, limit, txFilter, newestFirst) - if err != nil { - return "", err - } - - jsonEncodedTransactions, err := json.Marshal(&transactions) - if err != nil { - return "", err - } - - return string(jsonEncodedTransactions), nil -} - -func (mw *MultiWallet) GetTransactionsRaw(offset, limit, txFilter int32, newestFirst bool) ([]Transaction, error) { - transactions := make([]Transaction, 0) - for _, wallet := range mw.wallets { - walletTransactions, err := wallet.GetTransactionsRaw(offset, limit, txFilter, newestFirst) - if err != nil { - return nil, err - } - - transactions = append(transactions, walletTransactions...) - } - - // sort transaction by timestamp in descending order - sort.Slice(transactions[:], func(i, j int) bool { - if newestFirst { - return transactions[i].Timestamp > transactions[j].Timestamp - } - return transactions[i].Timestamp < transactions[j].Timestamp - }) - - if len(transactions) > int(limit) && limit > 0 { - transactions = transactions[:limit] - } - - return transactions, nil -} - func (wallet *Wallet) CountTransactions(txFilter int32) (int, error) { - return wallet.walletDataDB.Count(txFilter, wallet.RequiredConfirmations(), wallet.GetBestBlock(), &Transaction{}) + return wallet.WalletDataDB.Count(txFilter, wallet.RequiredConfirmations(), wallet.getBestBlock(), &Transaction{}) } func (wallet *Wallet) TicketHasVotedOrRevoked(ticketHash string) (bool, error) { - err := wallet.walletDataDB.FindOne("TicketSpentHash", ticketHash, &Transaction{}) + err := wallet.WalletDataDB.FindOne("TicketSpentHash", ticketHash, &Transaction{}) if err != nil { if err == storm.ErrNotFound { return false, nil @@ -167,7 +125,7 @@ func (wallet *Wallet) TicketHasVotedOrRevoked(ticketHash string) (bool, error) { func (wallet *Wallet) TicketSpender(ticketHash string) (*Transaction, error) { var spender Transaction - err := wallet.walletDataDB.FindOne("TicketSpentHash", ticketHash, &spender) + err := wallet.WalletDataDB.FindOne("TicketSpentHash", ticketHash, &spender) if err != nil { if err == storm.ErrNotFound { return nil, nil @@ -219,7 +177,7 @@ func (wallet *Wallet) TransactionOverview() (txOverview *TransactionOverview, er } func (wallet *Wallet) TxMatchesFilter(tx *Transaction, txFilter int32) bool { - bestBlock := wallet.GetBestBlock() + bestBlock := wallet.getBestBlock() // tickets with block height less than this are matured. maturityBlock := bestBlock - int32(wallet.chainParams.TicketMaturity) diff --git a/wallets/dcr/txandblocknotifications.go b/wallets/dcr/txandblocknotifications.go new file mode 100644 index 000000000..4152ca7ca --- /dev/null +++ b/wallets/dcr/txandblocknotifications.go @@ -0,0 +1,156 @@ +package dcr + +import ( + "encoding/json" + + "decred.org/dcrwallet/v2/errors" +) + +func (wallet *Wallet) listenForTransactions() { + go func() { + + n := wallet.Internal().NtfnServer.TransactionNotifications() + + for { + select { + case v := <-n.C: + if v == nil { + return + } + for _, transaction := range v.UnminedTransactions { + tempTransaction, err := wallet.decodeTransactionWithTxSummary(&transaction, nil) + if err != nil { + log.Errorf("[%d] Error ntfn parse tx: %v", wallet.ID, err) + return + } + + overwritten, err := wallet.WalletDataDB.SaveOrUpdate(&Transaction{}, tempTransaction) + if err != nil { + log.Errorf("[%d] New Tx save err: %v", wallet.ID, err) + return + } + + if !overwritten { + log.Infof("[%d] New Transaction %s", wallet.ID, tempTransaction.Hash) + + result, err := json.Marshal(tempTransaction) + if err != nil { + log.Error(err) + } else { + wallet.mempoolTransactionNotification(string(result)) + } + } + } + + for _, block := range v.AttachedBlocks { + blockHash := block.Header.BlockHash() + for _, transaction := range block.Transactions { + tempTransaction, err := wallet.decodeTransactionWithTxSummary(&transaction, &blockHash) + if err != nil { + log.Errorf("[%d] Error ntfn parse tx: %v", wallet.ID, err) + return + } + + _, err = wallet.WalletDataDB.SaveOrUpdate(&Transaction{}, tempTransaction) + if err != nil { + log.Errorf("[%d] Incoming block replace tx error :%v", wallet.ID, err) + return + } + wallet.publishTransactionConfirmed(transaction.Hash.String(), int32(block.Header.Height)) + } + + wallet.publishBlockAttached(int32(block.Header.Height)) + } + + if len(v.AttachedBlocks) > 0 { + wallet.checkWalletMixers() + } + + case <-wallet.syncData.syncCanceled: + n.Done() + } + } + }() +} + +// AddTxAndBlockNotificationListener registers a set of functions to be invoked +// when a transaction or block update is processed by the wallet. If async is +// true, the provided callback methods will be called from separate goroutines, +// allowing notification senders to continue their operation without waiting +// for the listener to complete processing the notification. This asyncrhonous +// handling is especially important for cases where the wallet process that +// sends the notification temporarily prevents access to other wallet features +// until all notification handlers finish processing the notification. If a +// notification handler were to try to access such features, it would result +// in a deadlock. +func (wallet *Wallet) AddTxAndBlockNotificationListener(txAndBlockNotificationListener TxAndBlockNotificationListener, async bool, uniqueIdentifier string) error { + wallet.notificationListenersMu.Lock() + defer wallet.notificationListenersMu.Unlock() + + _, ok := wallet.txAndBlockNotificationListeners[uniqueIdentifier] + if ok { + return errors.New(ErrListenerAlreadyExist) + } + + if async { + wallet.txAndBlockNotificationListeners[uniqueIdentifier] = &asyncTxAndBlockNotificationListener{ + l: txAndBlockNotificationListener, + } + } else { + wallet.txAndBlockNotificationListeners[uniqueIdentifier] = txAndBlockNotificationListener + } + + return nil +} + +func (wallet *Wallet) RemoveTxAndBlockNotificationListener(uniqueIdentifier string) { + wallet.notificationListenersMu.Lock() + defer wallet.notificationListenersMu.Unlock() + + delete(wallet.txAndBlockNotificationListeners, uniqueIdentifier) +} + +func (wallet *Wallet) checkWalletMixers() { + if wallet.IsAccountMixerActive() { + unmixedAccount := wallet.ReadInt32ConfigValueForKey(AccountMixerUnmixedAccount, -1) + hasMixableOutput, err := wallet.accountHasMixableOutput(unmixedAccount) + if err != nil { + log.Errorf("Error checking for mixable outputs: %v", err) + } + + if !hasMixableOutput { + log.Infof("[%d] unmixed account does not have a mixable output, stopping account mixer", wallet.ID) + err = wallet.StopAccountMixer() + if err != nil { + log.Errorf("Error stopping account mixer: %v", err) + } + } + } +} + +func (wallet *Wallet) mempoolTransactionNotification(transaction string) { + wallet.notificationListenersMu.RLock() + defer wallet.notificationListenersMu.RUnlock() + + for _, txAndBlockNotifcationListener := range wallet.txAndBlockNotificationListeners { + txAndBlockNotifcationListener.OnTransaction(transaction) + } +} + +func (wallet *Wallet) publishTransactionConfirmed(transactionHash string, blockHeight int32) { + wallet.notificationListenersMu.RLock() + defer wallet.notificationListenersMu.RUnlock() + + for _, txAndBlockNotifcationListener := range wallet.txAndBlockNotificationListeners { + txAndBlockNotifcationListener.OnTransactionConfirmed(wallet.ID, transactionHash, blockHeight) + } +} + +func (wallet *Wallet) publishBlockAttached(blockHeight int32) { + wallet.notificationListenersMu.RLock() + defer wallet.notificationListenersMu.RUnlock() + + for _, txAndBlockNotifcationListener := range wallet.txAndBlockNotificationListeners { + txAndBlockNotifcationListener.OnBlockAttached(wallet.ID, blockHeight) + } +} diff --git a/txauthor.go b/wallets/dcr/txauthor.go similarity index 96% rename from txauthor.go rename to wallets/dcr/txauthor.go index 878889983..1d574bb04 100644 --- a/txauthor.go +++ b/wallets/dcr/txauthor.go @@ -1,4 +1,4 @@ -package dcrlibwallet +package dcr import ( "bytes" @@ -32,8 +32,8 @@ type TxAuthor struct { needsConstruct bool } -func (mw *MultiWallet) NewUnsignedTx(walletID int, sourceAccountNumber int32) (*TxAuthor, error) { - sourceWallet := mw.WalletWithID(walletID) +func (wallet *Wallet) NewUnsignedTx(walletID int, sourceAccountNumber int32) (*TxAuthor, error) { + sourceWallet := wallet if sourceWallet == nil { return nil, fmt.Errorf(ErrWalletNotFound) } @@ -194,7 +194,7 @@ func (tx *TxAuthor) UseInputs(utxoKeys []string) error { Hash: *txHash, Index: uint32(index), } - outputInfo, err := tx.sourceWallet.Internal().OutputInfo(tx.sourceWallet.shutdownContext(), op) + outputInfo, err := tx.sourceWallet.Internal().OutputInfo(tx.sourceWallet.ShutdownContext(), op) if err != nil { return fmt.Errorf("no valid utxo found for '%s' in the source account", utxoKey) } @@ -251,7 +251,7 @@ func (tx *TxAuthor) Broadcast(privatePassphrase []byte) ([]byte, error) { lock <- time.Time{} }() - ctx := tx.sourceWallet.shutdownContext() + ctx := tx.sourceWallet.ShutdownContext() err = tx.sourceWallet.Internal().Unlock(ctx, privatePassphrase, lock) if err != nil { log.Error(err) @@ -308,16 +308,16 @@ func (tx *TxAuthor) unsignedTransaction() (*txauthor.AuthoredTx, error) { } func (tx *TxAuthor) constructTransaction() (*txauthor.AuthoredTx, error) { - if len(tx.inputs) != 0 { - return tx.constructCustomTransaction() - } + // if len(tx.inputs) != 0 { + // return tx.constructCustomTransaction() + // } var err error var outputs = make([]*wire.TxOut, 0) var outputSelectionAlgorithm w.OutputSelectionAlgorithm = w.OutputSelectionAlgorithmDefault var changeSource txauthor.ChangeSource - ctx := tx.sourceWallet.shutdownContext() + ctx := tx.sourceWallet.ShutdownContext() for _, destination := range tx.destinations { if err := tx.validateSendAmount(destination.SendMax, destination.AtomAmount); err != nil { diff --git a/txindex.go b/wallets/dcr/txindex.go similarity index 77% rename from txindex.go rename to wallets/dcr/txindex.go index ffc29f3b4..764012c63 100644 --- a/txindex.go +++ b/wallets/dcr/txindex.go @@ -1,13 +1,13 @@ -package dcrlibwallet +package dcr import ( w "decred.org/dcrwallet/v2/wallet" "github.com/decred/dcrd/chaincfg/chainhash" - "github.com/planetdecred/dcrlibwallet/walletdata" + "github.com/planetdecred/dcrlibwallet/wallets/dcr/walletdata" ) func (wallet *Wallet) IndexTransactions() error { - ctx := wallet.shutdownContext() + ctx := wallet.ShutdownContext() var totalIndex int32 var txEndHeight uint32 @@ -27,7 +27,7 @@ func (wallet *Wallet) IndexTransactions() error { return false, err } - _, err = wallet.walletDataDB.SaveOrUpdate(&Transaction{}, tx) + _, err = wallet.WalletDataDB.SaveOrUpdate(&Transaction{}, tx) if err != nil { log.Errorf("[%d] Index tx replace tx err : %v", wallet.ID, err) return false, err @@ -38,7 +38,7 @@ func (wallet *Wallet) IndexTransactions() error { if block.Header != nil { txEndHeight = block.Header.Height - err := wallet.walletDataDB.SaveLastIndexPoint(int32(txEndHeight)) + err := wallet.WalletDataDB.SaveLastIndexPoint(int32(txEndHeight)) if err != nil { log.Errorf("[%d] Set tx index end block height error: ", wallet.ID, err) return false, err @@ -55,26 +55,26 @@ func (wallet *Wallet) IndexTransactions() error { } } - beginHeight, err := wallet.walletDataDB.ReadIndexingStartBlock() + beginHeight, err := wallet.WalletDataDB.ReadIndexingStartBlock() if err != nil { log.Errorf("[%d] Get tx indexing start point error: %v", wallet.ID, err) return err } - endHeight := wallet.GetBestBlock() + endHeight := wallet.getBestBlock() startBlock := w.NewBlockIdentifierFromHeight(beginHeight) endBlock := w.NewBlockIdentifierFromHeight(endHeight) defer func() { - count, err := wallet.walletDataDB.Count(walletdata.TxFilterAll, wallet.RequiredConfirmations(), endHeight, &Transaction{}) + count, err := wallet.WalletDataDB.Count(walletdata.TxFilterAll, wallet.RequiredConfirmations(), endHeight, &Transaction{}) if err != nil { log.Errorf("[%d] Post-indexing tx count error :%v", wallet.ID, err) } else if count > 0 { log.Infof("[%d] Transaction index finished at %d, %d transaction(s) indexed in total", wallet.ID, endHeight, count) } - err = wallet.walletDataDB.SaveLastIndexPoint(endHeight) + err = wallet.WalletDataDB.SaveLastIndexPoint(endHeight) if err != nil { log.Errorf("[%d] Set tx index end block height error: ", wallet.ID, err) } @@ -84,8 +84,8 @@ func (wallet *Wallet) IndexTransactions() error { return wallet.Internal().GetTransactions(ctx, rangeFn, startBlock, endBlock) } -func (wallet *Wallet) reindexTransactions() error { - err := wallet.walletDataDB.ClearSavedTransactions(&Transaction{}) +func (wallet *Wallet) ReindexTransactions() error { + err := wallet.WalletDataDB.ClearSavedTransactions(&Transaction{}) if err != nil { return err } diff --git a/txparser.go b/wallets/dcr/txparser.go similarity index 94% rename from txparser.go rename to wallets/dcr/txparser.go index 126c8c985..30826bb9c 100644 --- a/txparser.go +++ b/wallets/dcr/txparser.go @@ -1,4 +1,4 @@ -package dcrlibwallet +package dcr import ( "fmt" @@ -15,7 +15,7 @@ func (wallet *Wallet) decodeTransactionWithTxSummary(txSummary *w.TransactionSum var blockHeight int32 = BlockHeightInvalid if blockHash != nil { blockIdentifier := w.NewBlockIdentifierFromHash(blockHash) - blockInfo, err := wallet.Internal().BlockInfo(wallet.shutdownContext(), blockIdentifier) + blockInfo, err := wallet.Internal().BlockInfo(wallet.ShutdownContext(), blockIdentifier) if err != nil { log.Error(err) } else { @@ -104,7 +104,7 @@ func (wallet *Wallet) decodeTransactionWithTxSummary(txSummary *w.TransactionSum // update ticket with spender hash ticketPurchaseTx.TicketSpender = decodedTx.Hash - wallet.walletDataDB.SaveOrUpdate(&Transaction{}, ticketPurchaseTx) + wallet.WalletDataDB.SaveOrUpdate(&Transaction{}, ticketPurchaseTx) } return decodedTx, nil diff --git a/wallets/dcr/types.go b/wallets/dcr/types.go new file mode 100644 index 000000000..6485281fc --- /dev/null +++ b/wallets/dcr/types.go @@ -0,0 +1,553 @@ +package dcr + +import ( + "context" + "fmt" + "net" + "net/http" + "sync" + + "decred.org/dcrwallet/v2/wallet/udb" + + "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/dcrutil/v4" + www "github.com/decred/politeia/politeiawww/api/www/v1" + "github.com/planetdecred/dcrlibwallet/internal/vsp" +) + +// WalletConfig defines options for configuring wallet behaviour. +// This is a subset of the config used by dcrwallet. +type WalletConfig struct { + // General + GapLimit uint32 // Allowed unused address gap between used addresses of accounts + ManualTickets bool // Do not discover new tickets through network synchronization + AllowHighFees bool // Do not perform high fee checks + RelayFee dcrutil.Amount // Transaction fee per kilobyte + AccountGapLimit int // Allowed gap of unused accounts + DisableCoinTypeUpgrades bool // Never upgrade from legacy to SLIP0044 coin type keys + + // CSPP + MixSplitLimit int // Connection limit to CoinShuffle++ server per change amount +} + +type CSPPConfig struct { + CSPPServer string + DialCSPPServer func(ctx context.Context, network, addr string) (net.Conn, error) + MixedAccount uint32 + MixedAccountBranch uint32 + TicketSplitAccount uint32 + ChangeAccount uint32 +} + +type WalletsIterator struct { + currentIndex int + wallets []*Wallet +} + +type BlockInfo struct { + Height int32 + Timestamp int64 +} + +type Amount struct { + AtomValue int64 + DcrValue float64 +} + +type TxFeeAndSize struct { + Fee *Amount + Change *Amount + EstimatedSignedSize int +} + +type UnsignedTransaction struct { + UnsignedTransaction []byte + EstimatedSignedSize int + ChangeIndex int + TotalOutputAmount int64 + TotalPreviousOutputAmount int64 +} + +type Balance struct { + Total int64 + Spendable int64 + ImmatureReward int64 + ImmatureStakeGeneration int64 + LockedByTickets int64 + VotingAuthority int64 + UnConfirmed int64 +} + +type Account struct { + WalletID int + Number int32 + Name string + Balance *Balance + TotalBalance int64 + ExternalKeyCount int32 + InternalKeyCount int32 + ImportedKeyCount int32 +} + +type AccountsIterator struct { + currentIndex int + accounts []*Account +} + +type Accounts struct { + Count int + Acc []*Account + CurrentBlockHash []byte + CurrentBlockHeight int32 +} + +type PeerInfo struct { + ID int32 `json:"id"` + Addr string `json:"addr"` + AddrLocal string `json:"addr_local"` + Services string `json:"services"` + Version uint32 `json:"version"` + SubVer string `json:"sub_ver"` + StartingHeight int64 `json:"starting_height"` + BanScore int32 `json:"ban_score"` +} + +type AccountMixerNotificationListener interface { + OnAccountMixerStarted(walletID int) + OnAccountMixerEnded(walletID int) +} + +/** begin sync-related types */ + +type SyncProgressListener interface { + OnSyncStarted(wasRestarted bool) + OnPeerConnectedOrDisconnected(numberOfConnectedPeers int32) + OnCFiltersFetchProgress(cfiltersFetchProgress *CFiltersFetchProgressReport) + OnHeadersFetchProgress(headersFetchProgress *HeadersFetchProgressReport) + OnAddressDiscoveryProgress(addressDiscoveryProgress *AddressDiscoveryProgressReport) + OnHeadersRescanProgress(headersRescanProgress *HeadersRescanProgressReport) + OnSyncCompleted() + OnSyncCanceled(willRestart bool) + OnSyncEndedWithError(err error) + Debug(debugInfo *DebugInfo) +} + +type GeneralSyncProgress struct { + TotalSyncProgress int32 `json:"totalSyncProgress"` + TotalTimeRemainingSeconds int64 `json:"totalTimeRemainingSeconds"` +} + +type CFiltersFetchProgressReport struct { + *GeneralSyncProgress + beginFetchCFiltersTimeStamp int64 + startCFiltersHeight int32 + cfiltersFetchTimeSpent int64 + totalFetchedCFiltersCount int32 + TotalCFiltersToFetch int32 `json:"totalCFiltersToFetch"` + CurrentCFilterHeight int32 `json:"currentCFilterHeight"` + CFiltersFetchProgress int32 `json:"headersFetchProgress"` +} + +type HeadersFetchProgressReport struct { + *GeneralSyncProgress + headersFetchTimeSpent int64 + beginFetchTimeStamp int64 + startHeaderHeight int32 + totalFetchedHeadersCount int32 + TotalHeadersToFetch int32 `json:"totalHeadersToFetch"` + CurrentHeaderHeight int32 `json:"currentHeaderHeight"` + CurrentHeaderTimestamp int64 `json:"currentHeaderTimestamp"` + HeadersFetchProgress int32 `json:"headersFetchProgress"` +} + +type AddressDiscoveryProgressReport struct { + *GeneralSyncProgress + addressDiscoveryStartTime int64 + totalDiscoveryTimeSpent int64 + AddressDiscoveryProgress int32 `json:"addressDiscoveryProgress"` + WalletID int `json:"walletID"` +} + +type HeadersRescanProgressReport struct { + *GeneralSyncProgress + TotalHeadersToScan int32 `json:"totalHeadersToScan"` + CurrentRescanHeight int32 `json:"currentRescanHeight"` + RescanProgress int32 `json:"rescanProgress"` + RescanTimeRemaining int64 `json:"rescanTimeRemaining"` + WalletID int `json:"walletID"` +} + +type DebugInfo struct { + TotalTimeElapsed int64 + TotalTimeRemaining int64 + CurrentStageTimeElapsed int64 + CurrentStageTimeRemaining int64 +} + +/** end sync-related types */ + +/** begin tx-related types */ + +type TxAndBlockNotificationListener interface { + OnTransaction(transaction string) + OnBlockAttached(walletID int, blockHeight int32) + OnTransactionConfirmed(walletID int, hash string, blockHeight int32) +} + +// asyncTxAndBlockNotificationListener is a TxAndBlockNotificationListener that +// triggers notifcation callbacks asynchronously. +type asyncTxAndBlockNotificationListener struct { + l TxAndBlockNotificationListener +} + +// OnTransaction satisfies the TxAndBlockNotificationListener interface and +// starts a goroutine to actually handle the notification using the embedded +// listener. +func (asyncTxBlockListener *asyncTxAndBlockNotificationListener) OnTransaction(transaction string) { + go asyncTxBlockListener.l.OnTransaction(transaction) +} + +// OnBlockAttached satisfies the TxAndBlockNotificationListener interface and +// starts a goroutine to actually handle the notification using the embedded +// listener. +func (asyncTxBlockListener *asyncTxAndBlockNotificationListener) OnBlockAttached(walletID int, blockHeight int32) { + go asyncTxBlockListener.l.OnBlockAttached(walletID, blockHeight) +} + +// OnTransactionConfirmed satisfies the TxAndBlockNotificationListener interface +// and starts a goroutine to actually handle the notification using the embedded +// listener. +func (asyncTxBlockListener *asyncTxAndBlockNotificationListener) OnTransactionConfirmed(walletID int, hash string, blockHeight int32) { + go asyncTxBlockListener.l.OnTransactionConfirmed(walletID, hash, blockHeight) +} + +type BlocksRescanProgressListener interface { + OnBlocksRescanStarted(walletID int) + OnBlocksRescanProgress(*HeadersRescanProgressReport) + OnBlocksRescanEnded(walletID int, err error) +} + +// Transaction is used with storm for tx indexing operations. +// For faster queries, the `Hash`, `Type` and `Direction` fields are indexed. +type Transaction struct { + WalletID int `json:"walletID"` + Hash string `storm:"id,unique" json:"hash"` + Type string `storm:"index" json:"type"` + Hex string `json:"hex"` + Timestamp int64 `storm:"index" json:"timestamp"` + BlockHeight int32 `storm:"index" json:"block_height"` + TicketSpender string `storm:"index" json:"ticket_spender"` + + MixDenomination int64 `json:"mix_denom"` + MixCount int32 `json:"mix_count"` + + Version int32 `json:"version"` + LockTime int32 `json:"lock_time"` + Expiry int32 `json:"expiry"` + Fee int64 `json:"fee"` + FeeRate int64 `json:"fee_rate"` + Size int `json:"size"` + + Direction int32 `storm:"index" json:"direction"` + Amount int64 `json:"amount"` + Inputs []*TxInput `json:"inputs"` + Outputs []*TxOutput `json:"outputs"` + + // Vote Info + VoteVersion int32 `json:"vote_version"` + LastBlockValid bool `json:"last_block_valid"` + VoteBits string `json:"vote_bits"` + VoteReward int64 `json:"vote_reward"` + TicketSpentHash string `storm:"unique" json:"ticket_spent_hash"` + DaysToVoteOrRevoke int32 `json:"days_to_vote_revoke"` +} + +type TxInput struct { + PreviousTransactionHash string `json:"previous_transaction_hash"` + PreviousTransactionIndex int32 `json:"previous_transaction_index"` + PreviousOutpoint string `json:"previous_outpoint"` + Amount int64 `json:"amount"` + AccountNumber int32 `json:"account_number"` +} + +type TxOutput struct { + Index int32 `json:"index"` + Amount int64 `json:"amount"` + Version int32 `json:"version"` + ScriptType string `json:"script_type"` + Address string `json:"address"` + Internal bool `json:"internal"` + AccountNumber int32 `json:"account_number"` +} + +// TxInfoFromWallet contains tx data that relates to the querying wallet. +// This info is used with `DecodeTransaction` to compose the entire details of a transaction. +type TxInfoFromWallet struct { + WalletID int + Hex string + Timestamp int64 + BlockHeight int32 + Inputs []*WalletInput + Outputs []*WalletOutput +} + +type WalletInput struct { + Index int32 `json:"index"` + AmountIn int64 `json:"amount_in"` + *WalletAccount +} + +type WalletOutput struct { + Index int32 `json:"index"` + AmountOut int64 `json:"amount_out"` + Internal bool `json:"internal"` + Address string `json:"address"` + *WalletAccount +} + +type WalletAccount struct { + AccountNumber int32 `json:"account_number"` + AccountName string `json:"account_name"` +} + +type TransactionDestination struct { + Address string + AtomAmount int64 + SendMax bool +} + +type TransactionOverview struct { + All int + Sent int + Received int + Transferred int + Mixed int + Staking int + Coinbase int +} + +/** end tx-related types */ + +/** begin ticket-related types */ + +type TicketPriceResponse struct { + TicketPrice int64 + Height int32 +} + +type StakingOverview struct { + All int + Unmined int + Immature int + Live int + Voted int + Revoked int + Expired int +} + +// TicketBuyerConfig defines configuration parameters for running +// an automated ticket buyer. +type TicketBuyerConfig struct { + VspHost string + PurchaseAccount int32 + BalanceToMaintain int64 + + vspClient *vsp.Client +} + +// VSPFeeStatus represents the current fee status of a ticket. +type VSPFeeStatus uint8 + +const ( + // VSPFeeProcessStarted represents the state which process has being + // called but fee still not paid. + VSPFeeProcessStarted VSPFeeStatus = iota + // VSPFeeProcessPaid represents the state where the process has being + // paid, but not published. + VSPFeeProcessPaid + VSPFeeProcessErrored + // VSPFeeProcessConfirmed represents the state where the fee has been + // confirmed by the VSP. + VSPFeeProcessConfirmed +) + +// String returns a human-readable interpretation of the vsp fee status. +func (status VSPFeeStatus) String() string { + switch udb.FeeStatus(status) { + case udb.VSPFeeProcessStarted: + return "fee process started" + case udb.VSPFeeProcessPaid: + return "fee paid" + case udb.VSPFeeProcessErrored: + return "fee payment errored" + case udb.VSPFeeProcessConfirmed: + return "fee confirmed by vsp" + default: + return fmt.Sprintf("invalid fee status %d", status) + } +} + +// VSPTicketInfo is information about a ticket that is assigned to a VSP. +type VSPTicketInfo struct { + VSP string + FeeTxHash string + FeeTxStatus VSPFeeStatus + // ConfirmedByVSP is nil if the ticket status could not be obtained + // from the VSP, false if the VSP hasn't confirmed the fee and true + // if the VSP has fully registered the ticket. + ConfirmedByVSP *bool + // VoteChoices is only set if the ticket status was obtained from the + // VSP. + VoteChoices map[string]string +} + +/** end ticket-related types */ + +/** begin politeia types */ +type Politeia struct { + WalletRef *Wallet + Host string + mu sync.RWMutex + ctx context.Context + cancelSync context.CancelFunc + Client *politeiaClient + notificationListenersMu sync.RWMutex + NotificationListeners map[string]ProposalNotificationListener +} + +type politeiaClient struct { + host string + httpClient *http.Client + + version *www.VersionReply + policy *www.PolicyReply + cookies []*http.Cookie +} + +type Proposal struct { + ID int `storm:"id,increment"` + Token string `json:"token" storm:"unique"` + Category int32 `json:"category" storm:"index"` + Name string `json:"name"` + State int32 `json:"state"` + Status int32 `json:"status"` + Timestamp int64 `json:"timestamp"` + UserID string `json:"userid"` + Username string `json:"username"` + NumComments int32 `json:"numcomments"` + Version string `json:"version"` + PublishedAt int64 `json:"publishedat"` + IndexFile string `json:"indexfile"` + IndexFileVersion string `json:"fileversion"` + VoteStatus int32 `json:"votestatus"` + VoteApproved bool `json:"voteapproved"` + YesVotes int32 `json:"yesvotes"` + NoVotes int32 `json:"novotes"` + EligibleTickets int32 `json:"eligibletickets"` + QuorumPercentage int32 `json:"quorumpercentage"` + PassPercentage int32 `json:"passpercentage"` +} + +type ProposalOverview struct { + All int32 + Discussion int32 + Voting int32 + Approved int32 + Rejected int32 + Abandoned int32 +} + +type ProposalVoteDetails struct { + EligibleTickets []*EligibleTicket + Votes []*ProposalVote + YesVotes int32 + NoVotes int32 +} + +type EligibleTicket struct { + Hash string + Address string +} + +type ProposalVote struct { + Ticket *EligibleTicket + Bit string +} + +type ProposalNotificationListener interface { + OnProposalsSynced() + OnNewProposal(proposal *Proposal) + OnProposalVoteStarted(proposal *Proposal) + OnProposalVoteFinished(proposal *Proposal) +} + +/** end politea proposal types */ + +type UnspentOutput struct { + TransactionHash []byte + OutputIndex uint32 + OutputKey string + ReceiveTime int64 + Amount int64 + FromCoinbase bool + Tree int32 + PkScript []byte + Addresses string // separated by commas + Confirmations int32 +} + +/** end politea proposal types */ + +/** begin vspd-related types */ +type VspInfoResponse struct { + APIVersions []int64 `json:"apiversions"` + Timestamp int64 `json:"timestamp"` + PubKey []byte `json:"pubkey"` + FeePercentage float64 `json:"feepercentage"` + VspClosed bool `json:"vspclosed"` + Network string `json:"network"` + VspdVersion string `json:"vspdversion"` + Voting int64 `json:"voting"` + Voted int64 `json:"voted"` + Revoked int64 `json:"revoked"` +} + +type VSP struct { + Host string + *VspInfoResponse +} + +/** end vspd-related types */ + +/** begin agenda types */ + +// Agenda contains information about a consensus deployment +type Agenda struct { + AgendaID string `json:"agenda_id"` + Description string `json:"description"` + Mask uint32 `json:"mask"` + Choices []chaincfg.Choice `json:"choices"` + VotingPreference string `json:"voting_preference"` + StartTime int64 `json:"start_time"` + ExpireTime int64 `json:"expire_time"` + Status string `json:"status"` +} + +// DcrdataAgenda models agenda information for the active network from the +// dcrdata api https://dcrdata.decred.org/api/agendas for mainnet or +// https://testnet.decred.org/api/agendas for testnet. +type DcrdataAgenda struct { + Name string `json:"name"` + Description string `json:"-"` + Status string `json:"status"` + VotingStarted int64 `json:"-"` + VotingDone int64 `json:"-"` + Activated int64 `json:"-"` + HardForked int64 `json:"-"` + StartTime string `json:"-"` + ExpireTime string `json:"-"` + VoteVersion uint32 `json:"-"` + Mask uint16 `json:"-"` +} + +/** end agenda types */ diff --git a/wallets/dcr/utils.go b/wallets/dcr/utils.go new file mode 100644 index 000000000..854fa7a12 --- /dev/null +++ b/wallets/dcr/utils.go @@ -0,0 +1,508 @@ +package dcr + +import ( + "context" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "math" + "net" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "decred.org/dcrwallet/v2/errors" + "decred.org/dcrwallet/v2/wallet" + "decred.org/dcrwallet/v2/wallet/txrules" + "decred.org/dcrwallet/v2/walletseed" + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/dcrutil/v4" + "github.com/decred/dcrd/hdkeychain/v3" + "github.com/decred/dcrd/wire" + "github.com/planetdecred/dcrlibwallet/internal/loader" +) + +const ( + walletDbName = "wallet.db" + + // FetchPercentage is used to increase the initial estimate gotten during cfilters stage + FetchPercentage = 0.38 + + // Use 10% of estimated total headers fetch time to estimate rescan time + RescanPercentage = 0.1 + + // Use 80% of estimated total headers fetch time to estimate address discovery time + DiscoveryPercentage = 0.8 + + MaxAmountAtom = dcrutil.MaxAmount + MaxAmountDcr = dcrutil.MaxAmount / dcrutil.AtomsPerCoin + + TestnetHDPath = "m / 44' / 1' / " + LegacyTestnetHDPath = "m / 44’ / 11’ / " + MainnetHDPath = "m / 44' / 42' / " + LegacyMainnetHDPath = "m / 44’ / 20’ / " + + DefaultRequiredConfirmations = 2 + + LongAbbreviationFormat = "long" + ShortAbbreviationFormat = "short" + ShortestAbbreviationFormat = "shortest" +) + +func (wallet *Wallet) RequiredConfirmations() int32 { + var spendUnconfirmed bool + wallet.readUserConfigValue(true, SpendUnconfirmedConfigKey, &spendUnconfirmed) + if spendUnconfirmed { + return 0 + } + return DefaultRequiredConfirmations +} + +func (wallet *Wallet) listenForShutdown() { + + wallet.cancelFuncs = make([]context.CancelFunc, 0) + wallet.shuttingDown = make(chan bool) + go func() { + <-wallet.shuttingDown + for _, cancel := range wallet.cancelFuncs { + cancel() + } + }() +} + +func (wallet *Wallet) ShutdownContextWithCancel() (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(context.Background()) + wallet.cancelFuncs = append(wallet.cancelFuncs, cancel) + return ctx, cancel +} + +func (wallet *Wallet) ShutdownContext() (ctx context.Context) { + ctx, _ = wallet.ShutdownContextWithCancel() + return +} + +func (wallet *Wallet) contextWithShutdownCancel() (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(context.Background()) + wallet.cancelFuncs = append(wallet.cancelFuncs, cancel) + return ctx, cancel +} + +func (wallet *Wallet) ValidateExtPubKey(extendedPubKey string) error { + _, err := hdkeychain.NewKeyFromString(extendedPubKey, wallet.chainParams) + if err != nil { + if err == hdkeychain.ErrInvalidChild { + return errors.New(ErrUnusableSeed) + } + + return errors.New(ErrInvalid) + } + + return nil +} + +func NormalizeAddress(addr string, defaultPort string) (string, error) { + // If the first SplitHostPort errors because of a missing port and not + // for an invalid host, add the port. If the second SplitHostPort + // fails, then a port is not missing and the original error should be + // returned. + host, port, origErr := net.SplitHostPort(addr) + if origErr == nil { + return net.JoinHostPort(host, port), nil + } + addr = net.JoinHostPort(addr, defaultPort) + _, _, err := net.SplitHostPort(addr) + if err != nil { + return "", origErr + } + return addr, nil +} + +// For use with gomobile bind, +// doesn't support the alternative `GenerateSeed` function because it returns more than 2 types. +func GenerateSeed() (string, error) { + seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen) + if err != nil { + return "", err + } + + return walletseed.EncodeMnemonic(seed), nil +} + +func VerifySeed(seedMnemonic string) bool { + _, err := walletseed.DecodeUserInput(seedMnemonic) + return err == nil +} + +// ExtractDateOrTime returns the date represented by the timestamp as a date string if the timestamp is over 24 hours ago. +// Otherwise, the time alone is returned as a string. +func ExtractDateOrTime(timestamp int64) string { + utcTime := time.Unix(timestamp, 0).UTC() + if time.Now().UTC().Sub(utcTime).Hours() > 24 { + return utcTime.Format("2006-01-02") + } else { + return utcTime.Format("15:04:05") + } +} + +func FormatUTCTime(timestamp int64) string { + return time.Unix(timestamp, 0).UTC().Format("2006-01-02 15:04:05") +} + +func AmountCoin(amount int64) float64 { + return dcrutil.Amount(amount).ToCoin() +} + +func AmountAtom(f float64) int64 { + amount, err := dcrutil.NewAmount(f) + if err != nil { + log.Error(err) + return -1 + } + return int64(amount) +} + +func EncodeHex(hexBytes []byte) string { + return hex.EncodeToString(hexBytes) +} + +func EncodeBase64(text []byte) string { + return base64.StdEncoding.EncodeToString(text) +} + +func DecodeBase64(base64Text string) ([]byte, error) { + b, err := base64.StdEncoding.DecodeString(base64Text) + if err != nil { + return nil, err + } + + return b, nil +} + +func ShannonEntropy(text string) (entropy float64) { + if text == "" { + return 0 + } + for i := 0; i < 256; i++ { + px := float64(strings.Count(text, string(byte(i)))) / float64(len(text)) + if px > 0 { + entropy += -px * math.Log2(px) + } + } + return entropy +} + +func TransactionDirectionName(direction int32) string { + switch direction { + case TxDirectionSent: + return "Sent" + case TxDirectionReceived: + return "Received" + case TxDirectionTransferred: + return "Yourself" + default: + return "invalid" + } +} + +func CalculateTotalTimeRemaining(timeRemainingInSeconds int64) string { + minutes := timeRemainingInSeconds / 60 + if minutes > 0 { + return fmt.Sprintf("%d min", minutes) + } + return fmt.Sprintf("%d sec", timeRemainingInSeconds) +} + +func CalculateDaysBehind(lastHeaderTime int64) string { + diff := time.Since(time.Unix(lastHeaderTime, 0)) + daysBehind := int(math.Round(diff.Hours() / 24)) + if daysBehind == 0 { + return "<1 day" + } else if daysBehind == 1 { + return "1 day" + } else { + return fmt.Sprintf("%d days", daysBehind) + } +} + +func StringsToHashes(h []string) ([]*chainhash.Hash, error) { + hashes := make([]*chainhash.Hash, 0, len(h)) + for _, v := range h { + hash, err := chainhash.NewHashFromStr(v) + if err != nil { + return nil, err + } + hashes = append(hashes, hash) + } + return hashes, nil +} + +func roundUp(n float64) int32 { + return int32(math.Round(n)) +} + +func WalletUniqueConfigKey(walletID int, key string) string { + return fmt.Sprintf("%d%s", walletID, key) +} + +func WalletExistsAt(directory string) bool { + walletDbFilePath := filepath.Join(directory, walletDbName) + exists, err := fileExists(walletDbFilePath) + if err != nil { + log.Errorf("wallet exists check error: %v", err) + } + return exists +} + +func fileExists(filePath string) (bool, error) { + _, err := os.Stat(filePath) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + return true, nil +} + +func moveFile(sourcePath, destinationPath string) error { + if exists, _ := fileExists(sourcePath); exists { + return os.Rename(sourcePath, destinationPath) + } + return nil +} + +// done returns whether the context's Done channel was closed due to +// cancellation or exceeded deadline. +func done(ctx context.Context) bool { + select { + case <-ctx.Done(): + return true + default: + return false + } +} + +func backupFile(fileName string, suffix int) (newName string, err error) { + newName = fileName + ".bak" + strconv.Itoa(suffix) + exists, err := fileExists(newName) + if err != nil { + return "", err + } else if exists { + return backupFile(fileName, suffix+1) + } + + err = moveFile(fileName, newName) + if err != nil { + return "", err + } + + return newName, nil +} + +func initWalletLoader(chainParams *chaincfg.Params, walletDataDir, walletDbDriver string) *loader.Loader { + // TODO: Allow users provide values to override these defaults. + cfg := &WalletConfig{ + GapLimit: 20, + AllowHighFees: false, + RelayFee: txrules.DefaultRelayFeePerKb, + AccountGapLimit: wallet.DefaultAccountGapLimit, + DisableCoinTypeUpgrades: false, + ManualTickets: false, + MixSplitLimit: 10, + } + + stakeOptions := &loader.StakeOptions{ + VotingEnabled: false, + AddressReuse: false, + VotingAddress: nil, + } + walletLoader := loader.NewLoader(chainParams, walletDataDir, stakeOptions, + cfg.GapLimit, cfg.AllowHighFees, cfg.RelayFee, cfg.AccountGapLimit, + cfg.DisableCoinTypeUpgrades, cfg.ManualTickets, cfg.MixSplitLimit) + + if walletDbDriver != "" { + walletLoader.SetDatabaseDriver(walletDbDriver) + } + + return walletLoader +} + +// makePlural is used with the TimeElapsed function. makePlural checks if the arguments passed is > 1, +// if true, it adds "s" after the given time to make it plural +func makePlural(x float64) string { + if int(x) == 1 { + return "" + } + return "s" +} + +// TimeElapsed returns the formatted time diffrence between two times as a string. +// If the argument `fullTime` is set to true, then the full time available is returned e.g 3 hours, 2 minutes, 20 seconds ago, +// as opposed to 3 hours ago. +// If the argument `abbreviationFormat` is set to `long` the time format is e.g 2 minutes +// If the argument `abbreviationFormat` is set to `short` the time format is e.g 2 mins +// If the argument `abbreviationFormat` is set to `shortest` the time format is e.g 2 m +func TimeElapsed(now, then time.Time, abbreviationFormat string, fullTime bool) string { + var parts []string + var text string + + year2, month2, day2 := now.Date() + hour2, minute2, second2 := now.Clock() + + year1, month1, day1 := then.Date() + hour1, minute1, second1 := then.Clock() + + year := math.Abs(float64(year2 - year1)) + month := math.Abs(float64(month2 - month1)) + day := math.Abs(float64(day2 - day1)) + hour := math.Abs(float64(hour2 - hour1)) + minute := math.Abs(float64(minute2 - minute1)) + second := math.Abs(float64(second2 - second1)) + + week := math.Floor(day / 7) + + if year > 0 { + if abbreviationFormat == LongAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(year))+" year"+makePlural(year)) + } else if abbreviationFormat == ShortAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(year))+" yr"+makePlural(year)) + } else if abbreviationFormat == ShortestAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(year))+" y") + } + } + + if month > 0 { + if abbreviationFormat == LongAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(month))+" month"+makePlural(month)) + } else if abbreviationFormat == ShortAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(month))+" mon"+makePlural(month)) + } else if abbreviationFormat == ShortestAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(month))+" m") + } + } + + if week > 0 { + if abbreviationFormat == LongAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(week))+" week"+makePlural(week)) + } else if abbreviationFormat == ShortAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(week))+" wk"+makePlural(week)) + } else if abbreviationFormat == ShortestAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(week))+" w") + } + } + + if day > 0 { + if abbreviationFormat == LongAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(day))+" day"+makePlural(day)) + } else if abbreviationFormat == ShortAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(day))+" dy"+makePlural(day)) + } else if abbreviationFormat == ShortestAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(day))+" d") + } + } + + if hour > 0 { + if abbreviationFormat == LongAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(hour))+" hour"+makePlural(hour)) + } else if abbreviationFormat == ShortAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(hour))+" hr"+makePlural(hour)) + } else if abbreviationFormat == ShortestAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(hour))+" h") + } + } + + if minute > 0 { + if abbreviationFormat == LongAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(minute))+" minute"+makePlural(minute)) + } else if abbreviationFormat == ShortAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(minute))+" min"+makePlural(minute)) + } else if abbreviationFormat == ShortestAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(minute))+" mi") + } + } + + if second > 0 { + if abbreviationFormat == LongAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(second))+" second"+makePlural(second)) + } else if abbreviationFormat == ShortAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(second))+" sec"+makePlural(second)) + } else if abbreviationFormat == ShortestAbbreviationFormat { + parts = append(parts, strconv.Itoa(int(second))+" s") + } + } + + if now.After(then) { + text = " ago" + } else { + text = " after" + } + + if len(parts) == 0 { + return "just now" + } + + if fullTime { + return strings.Join(parts, ", ") + text + } + return parts[0] + text +} + +// voteVersion was borrowed from upstream, and needs to always be in +// sync with the upstream method. This is the LOC to the upstream version: +// https://github.com/decred/dcrwallet/blob/master/wallet/wallet.go#L266 +func voteVersion(params *chaincfg.Params) uint32 { + switch params.Net { + case wire.MainNet: + return 9 + case 0x48e7a065: // TestNet2 + return 6 + case wire.TestNet3: + return 10 + case wire.SimNet: + return 10 + default: + return 1 + } +} + +// HttpGet helps to convert json(Byte data) into a struct object. +func HttpGet(url string, respObj interface{}) (*http.Response, []byte, error) { + rq := new(http.Client) + resp, err := rq.Get((url)) + if err != nil { + return nil, nil, err + } + + respBytes, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return nil, nil, err + } + + if resp.StatusCode != http.StatusOK { + return resp, respBytes, fmt.Errorf("%d response from server: %v", resp.StatusCode, string(respBytes)) + } + + err = json.Unmarshal(respBytes, respObj) + return resp, respBytes, err +} + +func marshalResult(result interface{}, err error) (string, error) { + + if err != nil { + return "", translateError(err) + } + + response, err := json.Marshal(result) + if err != nil { + return "", fmt.Errorf("error marshalling result: %s", err.Error()) + } + + return string(response), nil +} diff --git a/utxo.go b/wallets/dcr/utxo.go similarity index 98% rename from utxo.go rename to wallets/dcr/utxo.go index 5e8775c49..d0de847e1 100644 --- a/utxo.go +++ b/wallets/dcr/utxo.go @@ -1,4 +1,4 @@ -package dcrlibwallet +package dcr import ( "fmt" @@ -67,7 +67,7 @@ func (tx *TxAuthor) constructCustomTransaction() (*txauthor.AuthoredTx, error) { // if no change destination is provided and // no recipient is set to receive max amount. nextInternalAddress := func() (string, error) { - ctx := tx.sourceWallet.shutdownContext() + ctx := tx.sourceWallet.ShutdownContext() addr, err := tx.sourceWallet.Internal().NewChangeAddress(ctx, tx.sourceAccountNumber) if err != nil { return "", err diff --git a/vsp.go b/wallets/dcr/vsp.go similarity index 77% rename from vsp.go rename to wallets/dcr/vsp.go index 95eb37224..9f7050ecf 100644 --- a/vsp.go +++ b/wallets/dcr/vsp.go @@ -1,4 +1,4 @@ -package dcrlibwallet +package dcr import ( "context" @@ -36,17 +36,17 @@ func (wallet *Wallet) VSPClient(host string, pubKey []byte) (*vsp.Client, error) // KnownVSPs returns a list of known VSPs. This list may be updated by calling // ReloadVSPList. This method is safe for concurrent access. -func (mw *MultiWallet) KnownVSPs() []*VSP { - mw.vspMu.RLock() - defer mw.vspMu.RUnlock() - return mw.vsps // TODO: Return a copy. +func (wallet *Wallet) KnownVSPs() []*VSP { + wallet.vspMu.RLock() + defer wallet.vspMu.RUnlock() + return wallet.vsps // TODO: Return a copy. } // SaveVSP marks a VSP as known and will be susbequently included as part of // known VSPs. -func (mw *MultiWallet) SaveVSP(host string) (err error) { +func (wallet *Wallet) SaveVSP(host string) (err error) { // check if host already exists - vspDbData := mw.getVSPDBData() + vspDbData := wallet.getVSPDBData() for _, savedHost := range vspDbData.SavedHosts { if savedHost == host { return fmt.Errorf("duplicate host %s", host) @@ -60,31 +60,31 @@ func (mw *MultiWallet) SaveVSP(host string) (err error) { } // TODO: defaultVSPs() uses strings.Contains(network, vspInfo.Network). - if info.Network != mw.NetType() { + if info.Network != wallet.NetType() { return fmt.Errorf("invalid net %s", info.Network) } vspDbData.SavedHosts = append(vspDbData.SavedHosts, host) - mw.updateVSPDBData(vspDbData) + wallet.updateVSPDBData(vspDbData) - mw.vspMu.Lock() - mw.vsps = append(mw.vsps, &VSP{Host: host, VspInfoResponse: info}) - mw.vspMu.Unlock() + wallet.vspMu.Lock() + wallet.vsps = append(wallet.vsps, &VSP{Host: host, VspInfoResponse: info}) + wallet.vspMu.Unlock() return } // LastUsedVSP returns the host of the last used VSP, as saved by the // SaveLastUsedVSP() method. -func (mw *MultiWallet) LastUsedVSP() string { - return mw.getVSPDBData().LastUsedVSP +func (wallet *Wallet) LastUsedVSP() string { + return wallet.getVSPDBData().LastUsedVSP } // SaveLastUsedVSP saves the host of the last used VSP. -func (mw *MultiWallet) SaveLastUsedVSP(host string) { - vspDbData := mw.getVSPDBData() +func (wallet *Wallet) SaveLastUsedVSP(host string) { + vspDbData := wallet.getVSPDBData() vspDbData.LastUsedVSP = host - mw.updateVSPDBData(vspDbData) + wallet.updateVSPDBData(vspDbData) } type vspDbData struct { @@ -92,24 +92,24 @@ type vspDbData struct { LastUsedVSP string } -func (mw *MultiWallet) getVSPDBData() *vspDbData { +func (wallet *Wallet) getVSPDBData() *vspDbData { vspDbData := new(vspDbData) - mw.ReadUserConfigValue(KnownVSPsConfigKey, vspDbData) + wallet.ReadUserConfigValue(KnownVSPsConfigKey, vspDbData) return vspDbData } -func (mw *MultiWallet) updateVSPDBData(data *vspDbData) { - mw.SaveUserConfigValue(KnownVSPsConfigKey, data) +func (wallet *Wallet) updateVSPDBData(data *vspDbData) { + wallet.SaveUserConfigValue(KnownVSPsConfigKey, data) } // ReloadVSPList reloads the list of known VSPs. // This method makes multiple network calls; should be called in a goroutine // to prevent blocking the UI thread. -func (mw *MultiWallet) ReloadVSPList(ctx context.Context) { +func (wallet *Wallet) ReloadVSPList(ctx context.Context) { log.Debugf("Reloading list of known VSPs") defer log.Debugf("Reloaded list of known VSPs") - vspDbData := mw.getVSPDBData() + vspDbData := wallet.getVSPDBData() vspList := make(map[string]*VspInfoResponse) for _, host := range vspDbData.SavedHosts { vspInfo, err := vspInfo(host) @@ -124,7 +124,7 @@ func (mw *MultiWallet) ReloadVSPList(ctx context.Context) { } } - otherVSPHosts, err := defaultVSPs(mw.NetType()) + otherVSPHosts, err := defaultVSPs(wallet.NetType()) if err != nil { log.Debugf("get default vsp list error: %v", err) } @@ -143,12 +143,12 @@ func (mw *MultiWallet) ReloadVSPList(ctx context.Context) { } } - mw.vspMu.Lock() - mw.vsps = make([]*VSP, 0, len(vspList)) + wallet.vspMu.Lock() + wallet.vsps = make([]*VSP, 0, len(vspList)) for host, info := range vspList { - mw.vsps = append(mw.vsps, &VSP{Host: host, VspInfoResponse: info}) + wallet.vsps = append(wallet.vsps, &VSP{Host: host, VspInfoResponse: info}) } - mw.vspMu.Unlock() + wallet.vspMu.Unlock() } func vspInfo(vspHost string) (*VspInfoResponse, error) { diff --git a/wallets/dcr/wallet.go b/wallets/dcr/wallet.go new file mode 100644 index 000000000..24e4288b5 --- /dev/null +++ b/wallets/dcr/wallet.go @@ -0,0 +1,632 @@ +package dcr + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "time" + + "decred.org/dcrwallet/v2/errors" + w "decred.org/dcrwallet/v2/wallet" + "decred.org/dcrwallet/v2/walletseed" + "github.com/asdine/storm" + "github.com/decred/dcrd/chaincfg/v3" + "github.com/planetdecred/dcrlibwallet/internal/loader" + "github.com/planetdecred/dcrlibwallet/internal/vsp" + "github.com/planetdecred/dcrlibwallet/wallets/dcr/walletdata" +) + +type Wallet struct { + ID int `storm:"id,increment"` + Name string `storm:"unique"` + CreatedAt time.Time `storm:"index"` + dbDriver string + rootDir string + DB *storm.DB + + EncryptedSeed []byte + IsRestored bool + HasDiscoveredAccounts bool + PrivatePassphraseType int32 + + chainParams *chaincfg.Params + DataDir string + loader *loader.Loader + WalletDataDB *walletdata.DB + + Synced bool + Syncing bool + WaitingForHeaders bool + + shuttingDown chan bool + cancelFuncs []context.CancelFunc + CancelAccountMixer context.CancelFunc + + cancelAutoTicketBuyerMu sync.Mutex + cancelAutoTicketBuyer context.CancelFunc + + vspClientsMu sync.Mutex + vspClients map[string]*vsp.Client + + // setUserConfigValue saves the provided key-value pair to a config database. + // This function is ideally assigned when the `wallet.prepare` method is + // called from a MultiWallet instance. + setUserConfigValue configSaveFn + + // readUserConfigValue returns the previously saved value for the provided + // key from a config database. Returns nil if the key wasn't previously set. + // This function is ideally assigned when the `wallet.prepare` method is + // called from a MultiWallet instance. + readUserConfigValue configReadFn + + notificationListenersMu sync.RWMutex + syncData *SyncData + accountMixerNotificationListener map[string]AccountMixerNotificationListener + txAndBlockNotificationListeners map[string]TxAndBlockNotificationListener + blocksRescanProgressListener BlocksRescanProgressListener + + vspMu sync.RWMutex + vsps []*VSP +} + +// prepare gets a wallet ready for use by opening the transactions index database +// and initializing the wallet loader which can be used subsequently to create, +// load and unload the wallet. +func (wallet *Wallet) Prepare(rootDir string, chainParams *chaincfg.Params, + setUserConfigValueFn configSaveFn, readUserConfigValueFn configReadFn) (err error) { + + wallet.chainParams = chainParams + wallet.DataDir = filepath.Join(rootDir, strconv.Itoa(wallet.ID)) + wallet.vspClients = make(map[string]*vsp.Client) + wallet.setUserConfigValue = setUserConfigValueFn + wallet.readUserConfigValue = readUserConfigValueFn + + // open database for indexing transactions for faster loading + walletDataDBPath := filepath.Join(wallet.DataDir, walletdata.DbName) + oldTxDBPath := filepath.Join(wallet.DataDir, walletdata.OldDbName) + if exists, _ := fileExists(oldTxDBPath); exists { + moveFile(oldTxDBPath, walletDataDBPath) + } + wallet.WalletDataDB, err = walletdata.Initialize(walletDataDBPath, chainParams, &Transaction{}) + if err != nil { + log.Error(err.Error()) + return err + } + + wallet.syncData = &SyncData{ + SyncProgressListeners: make(map[string]SyncProgressListener), + } + + // init loader + wallet.loader = initWalletLoader(wallet.chainParams, wallet.DataDir, wallet.dbDriver) + + // init cancelFuncs slice to hold cancel functions for long running + // operations and start go routine to listen for shutdown signal + wallet.cancelFuncs = make([]context.CancelFunc, 0) + wallet.shuttingDown = make(chan bool) + go func() { + <-wallet.shuttingDown + for _, cancel := range wallet.cancelFuncs { + cancel() + } + }() + + return nil +} + +func (wallet *Wallet) Shutdown() { + // Trigger shuttingDown signal to cancel all contexts created with + // `wallet.ShutdownContext()` or `wallet.shutdownContextWithCancel()`. + wallet.shuttingDown <- true + + if _, loaded := wallet.loader.LoadedWallet(); loaded { + err := wallet.loader.UnloadWallet() + if err != nil { + log.Errorf("Failed to close wallet: %v", err) + } else { + log.Info("Closed wallet") + } + } + + if wallet.WalletDataDB != nil { + err := wallet.WalletDataDB.Close() + if err != nil { + log.Errorf("tx db closed with error: %v", err) + } else { + log.Info("tx db closed successfully") + } + } +} + +// WalletCreationTimeInMillis returns the wallet creation time for new +// wallets. Restored wallets would return an error. +func (wallet *Wallet) WalletCreationTimeInMillis() (int64, error) { + if wallet.IsRestored { + return 0, errors.New(ErrWalletIsRestored) + } + + return wallet.CreatedAt.UnixNano() / int64(time.Millisecond), nil +} + +func (wallet *Wallet) NetType() string { + return wallet.chainParams.Name +} + +func (wallet *Wallet) Internal() *w.Wallet { + lw, _ := wallet.loader.LoadedWallet() + return lw +} + +func (wallet *Wallet) WalletExists() (bool, error) { + return wallet.loader.WalletExists() +} + +func (wallet *Wallet) CreateNewWallet(walletName, privatePassphrase string, privatePassphraseType int32) (*Wallet, error) { + seed, err := GenerateSeed() + if err != nil { + return nil, err + } + + encryptedSeed, err := encryptWalletSeed([]byte(privatePassphrase), seed) + if err != nil { + return nil, err + } + + wal := &Wallet{ + Name: walletName, + CreatedAt: time.Now(), + EncryptedSeed: encryptedSeed, + PrivatePassphraseType: privatePassphraseType, + HasDiscoveredAccounts: true, + } + + return wal.saveNewWallet(func() error { + err := wal.Prepare(wal.rootDir, wal.chainParams, wal.walletConfigSetFn(wal.ID), wal.walletConfigReadFn(wal.ID)) + if err != nil { + return err + } + + return wal.CreateWallet(privatePassphrase, seed) + }) +} + +func (wallet *Wallet) CreateWallet(privatePassphrase, seedMnemonic string) error { + log.Info("Creating Wallet") + if len(seedMnemonic) == 0 { + return errors.New(ErrEmptySeed) + } + + pubPass := []byte(w.InsecurePubPassphrase) + privPass := []byte(privatePassphrase) + seed, err := walletseed.DecodeUserInput(seedMnemonic) + if err != nil { + log.Error(err) + return err + } + + _, err = wallet.loader.CreateNewWallet(wallet.ShutdownContext(), pubPass, privPass, seed) + if err != nil { + log.Error(err) + return err + } + + log.Info("Created Wallet") + return nil +} + +func (wallet *Wallet) CreateWatchOnlyWallet(walletName, extendedPublicKey string) (*Wallet, error) { + wal := &Wallet{ + Name: walletName, + IsRestored: true, + HasDiscoveredAccounts: true, + } + + return wallet.saveNewWallet(func() error { + err := wallet.Prepare(wallet.rootDir, wallet.chainParams, wallet.walletConfigSetFn(wal.ID), wallet.walletConfigReadFn(wal.ID)) + if err != nil { + return err + } + + return wallet.createWatchingOnlyWallet(extendedPublicKey) + }) +} + +func (wallet *Wallet) createWatchingOnlyWallet(extendedPublicKey string) error { + pubPass := []byte(w.InsecurePubPassphrase) + + _, err := wallet.loader.CreateWatchingOnlyWallet(wallet.ShutdownContext(), extendedPublicKey, pubPass) + if err != nil { + log.Error(err) + return err + } + + log.Info("Created Watching Only Wallet") + return nil +} + +func (wallet *Wallet) RenameWallet(newName string) error { + if strings.HasPrefix(newName, "wallet-") { + return errors.E(ErrReservedWalletName) + } + + if exists, err := wallet.WalletNameExists(newName); err != nil { + return translateError(err) + } else if exists { + return errors.New(ErrExist) + } + + wallet.Name = newName + return wallet.DB.Save(wallet) // update WalletName field +} + +func (wallet *Wallet) RestoreWallet(walletName, seedMnemonic, privatePassphrase string, privatePassphraseType int32) (*Wallet, error) { + + wal := &Wallet{ + Name: walletName, + PrivatePassphraseType: privatePassphraseType, + IsRestored: true, + HasDiscoveredAccounts: false, + } + + return wallet.saveNewWallet(func() error { + err := wallet.Prepare(wallet.rootDir, wallet.chainParams, wallet.walletConfigSetFn(wal.ID), wallet.walletConfigReadFn(wal.ID)) + if err != nil { + return err + } + + return wallet.CreateWallet(privatePassphrase, seedMnemonic) + }) +} + +func (wallet *Wallet) DeleteWallet(privPass []byte) error { + + if wallet.IsConnectedToDecredNetwork() { + wallet.CancelSync() + defer func() { + // if wallet.OpenedWalletsCount() > 0 { + wallet.SpvSync() + // } + }() + } + + err := wallet.deleteWallet(privPass) + if err != nil { + return translateError(err) + } + + err = wallet.DB.DeleteStruct(wallet) + if err != nil { + return translateError(err) + } + + // delete(wallet.ID) + + return nil +} + +func (wallet *Wallet) saveNewWallet(setupWallet func() error) (*Wallet, error) { + exists, err := wallet.WalletNameExists(wallet.Name) + if err != nil { + return nil, err + } else if exists { + return nil, errors.New(ErrExist) + } + + if wallet.IsConnectedToDecredNetwork() { + wallet.CancelSync() + defer wallet.SpvSync() + } + // Perform database save operations in batch transaction + // for automatic rollback if error occurs at any point. + err = wallet.batchDbTransaction(func(db storm.Node) error { + // saving struct to update ID property with an auto-generated value + err := db.Save(wallet) + if err != nil { + return err + } + + walletDataDir := filepath.Join(wallet.rootDir, strconv.Itoa(wallet.ID)) + + dirExists, err := fileExists(walletDataDir) + if err != nil { + return err + } else if dirExists { + newDirName, err := backupFile(walletDataDir, 1) + if err != nil { + return err + } + + log.Infof("Undocumented file at %s moved to %s", walletDataDir, newDirName) + } + + os.MkdirAll(walletDataDir, os.ModePerm) // create wallet dir + + if wallet.Name == "" { + wallet.Name = "wallet-" + strconv.Itoa(wallet.ID) // wallet-# + } + wallet.DataDir = walletDataDir + + err = db.Save(wallet) // update database with complete wallet information + if err != nil { + return err + } + + return setupWallet() + }) + + if err != nil { + return nil, translateError(err) + } + + // wallet.wallets[wallet.ID] = wallet + + return wallet, nil +} + +func (wallet *Wallet) LinkExistingWallet(walletName, walletDataDir, originalPubPass string, privatePassphraseType int32) (*Wallet, error) { + // check if `walletDataDir` contains wallet.DB + if !WalletExistsAt(walletDataDir) { + return nil, errors.New(ErrNotExist) + } + + ctx, _ := wallet.contextWithShutdownCancel() + + // verify the public passphrase for the wallet being linked before proceeding + if err := wallet.loadWalletTemporarily(ctx, walletDataDir, originalPubPass, nil); err != nil { + return nil, err + } + + wal := &Wallet{ + Name: walletName, + PrivatePassphraseType: privatePassphraseType, + IsRestored: true, + HasDiscoveredAccounts: false, // assume that account discovery hasn't been done + } + + return wallet.saveNewWallet(func() error { + // move wallet.DB and tx.DB files to newly created dir for the wallet + currentWalletDbFilePath := filepath.Join(walletDataDir, walletDbName) + newWalletDbFilePath := filepath.Join(wal.DataDir, walletDbName) + if err := moveFile(currentWalletDbFilePath, newWalletDbFilePath); err != nil { + return err + } + + currentTxDbFilePath := filepath.Join(walletDataDir, walletdata.OldDbName) + newTxDbFilePath := filepath.Join(wallet.DataDir, walletdata.DbName) + if err := moveFile(currentTxDbFilePath, newTxDbFilePath); err != nil { + return err + } + + // prepare the wallet for use and open it + err := (func() error { + err := wallet.Prepare(wallet.rootDir, wallet.chainParams, wallet.walletConfigSetFn(wallet.ID), wallet.walletConfigReadFn(wallet.ID)) + if err != nil { + return err + } + + if originalPubPass == "" || originalPubPass == w.InsecurePubPassphrase { + return wallet.OpenWallet() + } + + err = wallet.loadWalletTemporarily(ctx, wallet.DataDir, originalPubPass, func(tempWallet *w.Wallet) error { + return tempWallet.ChangePublicPassphrase(ctx, []byte(originalPubPass), []byte(w.InsecurePubPassphrase)) + }) + if err != nil { + return err + } + + return wallet.OpenWallet() + })() + + // restore db files to their original location if there was an error + // in the wallet setup process above + if err != nil { + moveFile(newWalletDbFilePath, currentWalletDbFilePath) + moveFile(newTxDbFilePath, currentTxDbFilePath) + } + + return err + }) +} + +func (wallet *Wallet) IsWatchingOnlyWallet() bool { + if w, ok := wallet.loader.LoadedWallet(); ok { + return w.WatchingOnly() + } + + return false +} + +func (wallet *Wallet) OpenWallet() error { + pubPass := []byte(w.InsecurePubPassphrase) + + _, err := wallet.loader.OpenExistingWallet(wallet.ShutdownContext(), pubPass) + if err != nil { + log.Error(err) + return translateError(err) + } + + return nil +} + +func (wallet *Wallet) WalletOpened() bool { + return wallet.Internal() != nil +} + +func (wallet *Wallet) UnlockWallet(privPass []byte) error { + return wallet.unlockWallet(privPass) +} + +func (wallet *Wallet) unlockWallet(privPass []byte) error { + loadedWallet, ok := wallet.loader.LoadedWallet() + if !ok { + return fmt.Errorf("wallet has not been loaded") + } + + ctx, _ := wallet.ShutdownContextWithCancel() + err := loadedWallet.Unlock(ctx, privPass, nil) + if err != nil { + return translateError(err) + } + + return nil +} + +func (wallet *Wallet) LockWallet() { + if wallet.IsAccountMixerActive() { + log.Error("LockWallet ignored due to active account mixer") + return + } + + if !wallet.Internal().Locked() { + wallet.Internal().Lock() + } +} + +func (wallet *Wallet) IsLocked() bool { + return wallet.Internal().Locked() +} + +// ChangePrivatePassphraseForWallet attempts to change the wallet's passphrase and re-encrypts the seed with the new passphrase. +func (wallet *Wallet) ChangePrivatePassphraseForWallet(oldPrivatePassphrase, newPrivatePassphrase []byte, privatePassphraseType int32) error { + if privatePassphraseType != PassphraseTypePin && privatePassphraseType != PassphraseTypePass { + return errors.New(ErrInvalid) + } + encryptedSeed := wallet.EncryptedSeed + if encryptedSeed != nil { + decryptedSeed, err := decryptWalletSeed(oldPrivatePassphrase, encryptedSeed) + if err != nil { + return err + } + + encryptedSeed, err = encryptWalletSeed(newPrivatePassphrase, decryptedSeed) + if err != nil { + return err + } + } + + err := wallet.changePrivatePassphrase(oldPrivatePassphrase, newPrivatePassphrase) + if err != nil { + return translateError(err) + } + + wallet.EncryptedSeed = encryptedSeed + wallet.PrivatePassphraseType = privatePassphraseType + err = wallet.DB.Save(wallet) + if err != nil { + log.Errorf("error saving wallet-[%d] to database after passphrase change: %v", wallet.ID, err) + + err2 := wallet.changePrivatePassphrase(newPrivatePassphrase, oldPrivatePassphrase) + if err2 != nil { + log.Errorf("error undoing wallet passphrase change: %v", err2) + log.Errorf("error wallet passphrase was changed but passphrase type and newly encrypted seed could not be saved: %v", err) + return errors.New(ErrSavingWallet) + } + + return errors.New(ErrChangingPassphrase) + } + + return nil +} + +func (wallet *Wallet) changePrivatePassphrase(oldPass []byte, newPass []byte) error { + defer func() { + for i := range oldPass { + oldPass[i] = 0 + } + + for i := range newPass { + newPass[i] = 0 + } + }() + + err := wallet.Internal().ChangePrivatePassphrase(wallet.ShutdownContext(), oldPass, newPass) + if err != nil { + return translateError(err) + } + return nil +} + +func (wallet *Wallet) deleteWallet(privatePassphrase []byte) error { + defer func() { + for i := range privatePassphrase { + privatePassphrase[i] = 0 + } + }() + + if _, loaded := wallet.loader.LoadedWallet(); !loaded { + return errors.New(ErrWalletNotLoaded) + } + + if !wallet.IsWatchingOnlyWallet() { + err := wallet.Internal().Unlock(wallet.ShutdownContext(), privatePassphrase, nil) + if err != nil { + return translateError(err) + } + wallet.Internal().Lock() + } + + wallet.Shutdown() + + log.Info("Deleting Wallet") + return os.RemoveAll(wallet.DataDir) +} + +// DecryptSeed decrypts wallet.EncryptedSeed using privatePassphrase +// func (wallet *Wallet) DecryptSeed(privatePassphrase []byte) (string, error) { +// if wallet.EncryptedSeed == nil { +// return "", errors.New(ErrInvalid) +// } + +// return decryptWalletSeed(privatePassphrase, wallet.EncryptedSeed) +// } + +// AccountXPubMatches checks if the xpub of the provided account matches the +// provided legacy or SLIP0044 xpub. While both the legacy and SLIP0044 xpubs +// will be checked for watch-only wallets, other wallets will only check the +// xpub that matches the coin type key used by the wallet. +func (wallet *Wallet) AccountXPubMatches(account uint32, legacyXPub, slip044XPub string) (bool, error) { + ctx := wallet.ShutdownContext() + + acctXPubKey, err := wallet.Internal().AccountXpub(ctx, account) + if err != nil { + return false, err + } + acctXPub := acctXPubKey.String() + + if wallet.IsWatchingOnlyWallet() { + // Coin type info isn't saved for watch-only wallets, so check + // against both legacy and SLIP0044 coin types. + return acctXPub == legacyXPub || acctXPub == slip044XPub, nil + } + + cointype, err := wallet.Internal().CoinType(ctx) + if err != nil { + return false, err + } + + if cointype == wallet.chainParams.LegacyCoinType { + return acctXPub == legacyXPub, nil + } else { + return acctXPub == slip044XPub, nil + } +} + +// VerifySeedForWallet compares seedMnemonic with the decrypted wallet.EncryptedSeed and clears wallet.EncryptedSeed if they match. +func (wallet *Wallet) VerifySeedForWallet(seedMnemonic string, privpass []byte) (bool, error) { + decryptedSeed, err := decryptWalletSeed(privpass, wallet.EncryptedSeed) + if err != nil { + return false, err + } + + if decryptedSeed == seedMnemonic { + wallet.EncryptedSeed = nil + return true, translateError(wallet.DB.Save(wallet)) + } + + return false, errors.New(ErrInvalid) +} diff --git a/wallet_config.go b/wallets/dcr/wallet_config.go similarity index 63% rename from wallet_config.go rename to wallets/dcr/wallet_config.go index 8f6ba8ab8..29abd6c71 100644 --- a/wallet_config.go +++ b/wallets/dcr/wallet_config.go @@ -1,4 +1,4 @@ -package dcrlibwallet +package dcr import ( "decred.org/dcrwallet/v2/errors" @@ -10,8 +10,60 @@ const ( AccountMixerMixedAccount = "account_mixer_mixed_account" AccountMixerUnmixedAccount = "account_mixer_unmixed_account" AccountMixerMixTxChange = "account_mixer_mix_tx_change" + + userConfigBucketName = "user_config" + + LogLevelConfigKey = "log_level" + + SpendUnconfirmedConfigKey = "spend_unconfirmed" + CurrencyConversionConfigKey = "currency_conversion_option" + + IsStartupSecuritySetConfigKey = "startup_security_set" + StartupSecurityTypeConfigKey = "startup_security_type" + UseBiometricConfigKey = "use_biometric" + + IncomingTxNotificationsConfigKey = "tx_notification_enabled" + BeepNewBlocksConfigKey = "beep_new_blocks" + + SyncOnCellularConfigKey = "always_sync" + NetworkModeConfigKey = "network_mode" + SpvPersistentPeerAddressesConfigKey = "spv_peer_addresses" + UserAgentConfigKey = "user_agent" + + PoliteiaNotificationConfigKey = "politeia_notification" + + LastTxHashConfigKey = "last_tx_hash" + + KnownVSPsConfigKey = "known_vsps" + + TicketBuyerVSPHostConfigKey = "tb_vsp_host" + TicketBuyerWalletConfigKey = "tb_wallet_id" + TicketBuyerAccountConfigKey = "tb_account_number" + TicketBuyerATMConfigKey = "tb_amount_to_maintain" + + PassphraseTypePin int32 = 0 + PassphraseTypePass int32 = 1 ) +type configSaveFn = func(key string, value interface{}) error +type configReadFn = func(multiwallet bool, key string, valueOut interface{}) error + +func (wallet *Wallet) walletConfigSetFn(walletID int) configSaveFn { + return func(key string, value interface{}) error { + walletUniqueKey := WalletUniqueConfigKey(walletID, key) + return wallet.DB.Set(userConfigBucketName, walletUniqueKey, value) + } +} + +func (wallet *Wallet) walletConfigReadFn(walletID int) configReadFn { + return func(multiwallet bool, key string, valueOut interface{}) error { + if !multiwallet { + key = WalletUniqueConfigKey(walletID, key) + } + return wallet.DB.Get(userConfigBucketName, key, valueOut) + } +} + func (wallet *Wallet) SaveUserConfigValue(key string, value interface{}) { if wallet.setUserConfigValue == nil { log.Errorf("call wallet.prepare before setting wallet config values") diff --git a/wallets/dcr/wallet_utils.go b/wallets/dcr/wallet_utils.go new file mode 100644 index 000000000..474bc32a0 --- /dev/null +++ b/wallets/dcr/wallet_utils.go @@ -0,0 +1,130 @@ +package dcr + +import ( + "context" + + "decred.org/dcrwallet/v2/errors" + "github.com/asdine/storm" + "github.com/kevinburke/nacl" + "github.com/kevinburke/nacl/secretbox" + "golang.org/x/crypto/scrypt" + + w "decred.org/dcrwallet/v2/wallet" + + "strings" +) + +func (wallet *Wallet) markWalletAsDiscoveredAccounts() error { + if wallet == nil { + return errors.New(ErrNotExist) + } + + log.Infof("Set discovered accounts = true for wallet %d", wallet.ID) + wallet.HasDiscoveredAccounts = true + err := wallet.DB.Save(wallet) + if err != nil { + return err + } + + return nil +} + +func (wallet *Wallet) batchDbTransaction(dbOp func(node storm.Node) error) (err error) { + dbTx, err := wallet.DB.Begin(true) + if err != nil { + return err + } + + // Commit or rollback the transaction after f returns or panics. Do not + // recover from the panic to keep the original stack trace intact. + panicked := true + defer func() { + if panicked || err != nil { + dbTx.Rollback() + return + } + + err = dbTx.Commit() + }() + + err = dbOp(dbTx) + panicked = false + return err +} + +func (wallet *Wallet) WalletNameExists(walletName string) (bool, error) { + if strings.HasPrefix(walletName, "wallet-") { + return false, errors.E(ErrReservedWalletName) + } + + err := wallet.DB.One("Name", walletName, &Wallet{}) + if err == nil { + return true, nil + } else if err != storm.ErrNotFound { + return false, err + } + + return false, nil +} + +// naclLoadFromPass derives a nacl.Key from pass using scrypt.Key. +func naclLoadFromPass(pass []byte) (nacl.Key, error) { + + const N, r, p = 1 << 15, 8, 1 + + hash, err := scrypt.Key(pass, nil, N, r, p, 32) + if err != nil { + return nil, err + } + return nacl.Load(EncodeHex(hash)) +} + +// encryptWalletSeed encrypts the seed with secretbox.EasySeal using pass. +func encryptWalletSeed(pass []byte, seed string) ([]byte, error) { + key, err := naclLoadFromPass(pass) + if err != nil { + return nil, err + } + return secretbox.EasySeal([]byte(seed), key), nil +} + +// decryptWalletSeed decrypts the encryptedSeed with secretbox.EasyOpen using pass. +func decryptWalletSeed(pass []byte, encryptedSeed []byte) (string, error) { + key, err := naclLoadFromPass(pass) + if err != nil { + return "", err + } + + decryptedSeed, err := secretbox.EasyOpen(encryptedSeed, key) + if err != nil { + return "", errors.New(ErrInvalidPassphrase) + } + + return string(decryptedSeed), nil +} + +func (wallet *Wallet) loadWalletTemporarily(ctx context.Context, walletDataDir, walletPublicPass string, + onLoaded func(*w.Wallet) error) error { + + if walletPublicPass == "" { + walletPublicPass = w.InsecurePubPassphrase + } + + // initialize the wallet loader + walletLoader := initWalletLoader(wallet.chainParams, walletDataDir, wallet.dbDriver) + + // open the wallet to get ready for temporary use + wal, err := walletLoader.OpenExistingWallet(ctx, []byte(walletPublicPass)) + if err != nil { + return translateError(err) + } + + // unload wallet after temporary use + defer walletLoader.UnloadWallet() + + if onLoaded != nil { + return onLoaded(wal) + } + + return nil +} diff --git a/walletdata/db.go b/wallets/dcr/walletdata/db.go similarity index 100% rename from walletdata/db.go rename to wallets/dcr/walletdata/db.go diff --git a/walletdata/filter.go b/wallets/dcr/walletdata/filter.go similarity index 100% rename from walletdata/filter.go rename to wallets/dcr/walletdata/filter.go diff --git a/walletdata/read.go b/wallets/dcr/walletdata/read.go similarity index 100% rename from walletdata/read.go rename to wallets/dcr/walletdata/read.go diff --git a/walletdata/save.go b/wallets/dcr/walletdata/save.go similarity index 100% rename from walletdata/save.go rename to wallets/dcr/walletdata/save.go diff --git a/wordlist.go b/wallets/dcr/wordlist.go similarity index 99% rename from wordlist.go rename to wallets/dcr/wordlist.go index 91a819dda..64d30862a 100644 --- a/wordlist.go +++ b/wallets/dcr/wordlist.go @@ -14,7 +14,7 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -package dcrlibwallet +package dcr import "strings"