diff --git a/dcr.go b/dcr.go new file mode 100644 index 000000000..4801c6ffc --- /dev/null +++ b/dcr.go @@ -0,0 +1,55 @@ +package dcrlibwallet + +import ( + "os" + "path/filepath" + + "decred.org/dcrwallet/v2/errors" + + "github.com/asdine/storm" + + 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..3d2503a12 100644 --- a/dexclient.go +++ b/dexclient.go @@ -44,7 +44,7 @@ func (mw *MultiWallet) initDexClient() error { mw.dexClient = &DexClient{ log: dex.NewLogger("DEXC", log.Level(), logWriter{}, true), - dexDataDir: filepath.Join(mw.rootDir, "dex"), + dexDataDir: filepath.Join(mw.RootDir, "dex"), } err := os.MkdirAll(mw.dexClient.dexDataDir, os.ModePerm) @@ -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 5ee2f89f9..d999117bb 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/decred/dcrd/dcrutil/v4 v4.0.0 github.com/decred/dcrd/gcs/v3 v3.0.0 github.com/decred/dcrd/hdkeychain/v3 v3.1.0 + github.com/decred/dcrd/rpc/jsonrpc/types/v3 v3.0.0 github.com/decred/dcrd/txscript/v4 v4.0.0 github.com/decred/dcrd/wire v1.5.0 github.com/decred/dcrdata/v7 v7.0.0-20211216152310-365c9dc820eb @@ -23,7 +24,6 @@ require ( 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/jessevdk/go-flags v1.5.0 // indirect github.com/jrick/logrotate v1.0.0 github.com/kevinburke/nacl v0.0.0-20190829012316-f3ed23dbd7f8 github.com/onsi/ginkgo v1.14.0 diff --git a/go.sum b/go.sum index 0565a88f5..30fcd2956 100644 --- a/go.sum +++ b/go.sum @@ -47,7 +47,6 @@ decred.org/dcrwallet v1.7.0 h1:U/ew00YBdUlx3rJAynt2OdKDgGzBKK4O89FijBq8iVg= decred.org/dcrwallet v1.7.0/go.mod h1:hNOGyvH53gWdgFB601/ubGRzCPfPtWnEVAi9Grs90y4= decred.org/dcrwallet/v2 v2.0.0-20211206163037-9537363becbb/go.mod h1:rbFJaCuXCfDhYoI5ZdeZr8TmF4A4Sb1zE7jQAwtaFMo= decred.org/dcrwallet/v2 v2.0.0-20211207180344-e2bce3d3b877/go.mod h1:nRvFh0CChWgRxXxxCWG2wBpzJnfOhGhdxU7meaMhSfA= -decred.org/dcrwallet/v2 v2.0.1 h1:f4zxCskK6PKUUCifzcXLaq+0UNC1dxxQybS7CKKMP4U= decred.org/dcrwallet/v2 v2.0.1/go.mod h1:lZXgx5OcLDaWyNWFkBekqER1gdqiVwua1w68SFC1/Nk= decred.org/dcrwallet/v2 v2.0.2-0.20220505152146-ece5da349895 h1:qwUXrsjgm6qU7+/1mvaOgIfMeC2v0San3rm1fQVoCaU= decred.org/dcrwallet/v2 v2.0.2-0.20220505152146-ece5da349895/go.mod h1:lZXgx5OcLDaWyNWFkBekqER1gdqiVwua1w68SFC1/Nk= @@ -201,7 +200,6 @@ 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= @@ -231,14 +229,12 @@ 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= @@ -572,7 +568,6 @@ 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= @@ -593,9 +588,9 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -635,7 +630,6 @@ 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= @@ -702,8 +696,6 @@ 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 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= -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= @@ -1021,7 +1013,6 @@ go.etcd.io/bbolt v1.3.0/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= 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= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= @@ -1074,7 +1065,6 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -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/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -1164,7 +1154,6 @@ golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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 h1:w6wWR0H+nyVpbSAQbzVEIACVyr/h8l/BEkY6Sokc7Eg= golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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= @@ -1250,21 +1239,18 @@ 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= @@ -1276,7 +1262,6 @@ 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= @@ -1412,7 +1397,6 @@ 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= @@ -1439,7 +1423,6 @@ 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= @@ -1453,7 +1436,6 @@ 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..b2ff792dd 100644 --- a/multiwallet.go +++ b/multiwallet.go @@ -6,46 +6,42 @@ 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" "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/dcr" + "golang.org/x/crypto/bcrypt" ) -type MultiWallet struct { - dbDriver string - rootDir string - db *storm.DB - - chainParams *chaincfg.Params - wallets map[int]*Wallet - badWallets map[int]*Wallet - syncData *syncData +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 + } +} - notificationListenersMu sync.RWMutex - txAndBlockNotificationListeners map[string]TxAndBlockNotificationListener +type MultiWallet struct { + DbDriver string + RootDir string + DB *storm.DB - blocksRescanProgressListener BlocksRescanProgressListener - accountMixerNotificationListener map[string]AccountMixerNotificationListener + ChainParams *chaincfg.Params + Assets *Assets 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,63 +52,39 @@ 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{}) - if err != nil { - log.Errorf("Error initializing wallets database: %s", err.Error()) - return nil, err - } - - // init database for saving/reading proposal objects - err = mwDB.Init(&Proposal{}) + 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()) } mw := &MultiWallet{ - dbDriver: dbDriver, - rootDir: rootDir, - db: mwDB, - chainParams: chainParams, - wallets: make(map[int]*Wallet), - badWallets: make(map[int]*Wallet), - syncData: &syncData{ - syncProgressListeners: make(map[string]SyncProgressListener), + DbDriver: dbDriver, + RootDir: dcrRootDir, + DB: dcrDB, + ChainParams: chainParams, + 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, + }, }, - txAndBlockNotificationListeners: make(map[string]TxAndBlockNotificationListener), - accountMixerNotificationListener: make(map[string]AccountMixerNotificationListener), - } - - mw.Politeia, err = newPoliteia(mw, politeiaHost) - if err != nil { - return nil, err } // read saved wallets info from db and initialize wallets - query := mw.db.Select(q.True()).OrderBy("ID") - var wallets []*Wallet + query := mw.DB.Select(q.True()).OrderBy("ID") + var wallets []*dcr.Wallet err = query.Find(&wallets) if err != nil && err != storm.ErrNotFound { return nil, err @@ -120,16 +92,22 @@ func NewMultiWallet(rootDir, dbDriver, netType, politeiaHost string) (*MultiWall // 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) { + 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 } + + logLevel := wallet.ReadStringConfigValueForKey(LogLevelConfigKey, "") + SetLogLevels(logLevel) + + // initialize Politeia. + wallet.NewPoliteia(politeiaHost) } mw.listenForShutdown() @@ -152,15 +130,20 @@ func (mw *MultiWallet) Shutdown() { // Trigger shuttingDown signal to cancel all contexts created with `shutdownContextWithCancel`. mw.shuttingDown <- true - mw.CancelRescan() - mw.CancelSync() + for _, wallet := range mw.Assets.DCR.Wallets { + wallet.CancelRescan() + } + + for _, wallet := range mw.Assets.DCR.Wallets { + wallet.CancelSync() + } - for _, wallet := range mw.wallets { + for _, wallet := range mw.Assets.DCR.Wallets { wallet.Shutdown() } - if mw.db != nil { - if err := mw.db.Close(); err != nil { + if mw.DB != nil { + if err := mw.DB.Close(); err != nil { log.Errorf("db closed with error: %v", err) } else { log.Info("db closed successfully") @@ -175,15 +158,15 @@ func (mw *MultiWallet) Shutdown() { } func (mw *MultiWallet) NetType() string { - return mw.chainParams.Name + return mw.ChainParams.Name } func (mw *MultiWallet) LogDir() string { - return filepath.Join(mw.rootDir, logFileName) + return filepath.Join(mw.RootDir, logFileName) } func (mw *MultiWallet) TargetTimePerBlockMinutes() float64 { - return mw.chainParams.TargetTimePerBlock.Minutes() + return mw.ChainParams.TargetTimePerBlock.Minutes() } func (mw *MultiWallet) SetStartupPassphrase(passphrase []byte, passphraseType int32) error { @@ -192,7 +175,7 @@ func (mw *MultiWallet) SetStartupPassphrase(passphrase []byte, passphraseType in func (mw *MultiWallet) VerifyStartupPassphrase(startupPassphrase []byte) error { var startupPassphraseHash []byte - err := mw.db.Get(walletsMetadataBucketName, walletstartupPassphraseField, &startupPassphraseHash) + err := mw.DB.Get(walletsMetadataBucketName, walletstartupPassphraseField, &startupPassphraseHash) if err != nil && err != storm.ErrNotFound { return err } @@ -229,7 +212,7 @@ func (mw *MultiWallet) ChangeStartupPassphrase(oldPassphrase, newPassphrase []by return err } - err = mw.db.Set(walletsMetadataBucketName, walletstartupPassphraseField, startupPassphraseHash) + err = mw.DB.Set(walletsMetadataBucketName, walletstartupPassphraseField, startupPassphraseHash) if err != nil { return err } @@ -246,7 +229,7 @@ func (mw *MultiWallet) RemoveStartupPassphrase(oldPassphrase []byte) error { return err } - err = mw.db.Delete(walletsMetadataBucketName, walletstartupPassphraseField) + err = mw.DB.Delete(walletsMetadataBucketName, walletstartupPassphraseField) if err != nil { return err } @@ -266,17 +249,17 @@ func (mw *MultiWallet) StartupSecurityType() int32 { } func (mw *MultiWallet) OpenWallets(startupPassphrase []byte) error { - if mw.IsSyncing() { - return errors.New(ErrSyncAlreadyInProgress) - } + // if mw.IsSyncing() { + // return errors.New(ErrSyncAlreadyInProgress) + // } err := mw.VerifyStartupPassphrase(startupPassphrase) if err != nil { return err } - for _, wallet := range mw.wallets { - err = wallet.openWallet() + for _, wallet := range mw.Assets.DCR.Wallets { + err = wallet.OpenWallet() if err != nil { return err } @@ -286,11 +269,11 @@ 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 { return false, errors.New(ErrInvalid) } - for _, w := range mw.wallets { + for _, w := range mw.Assets.DCR.Wallets { if !w.IsWatchingOnlyWallet() { return false, nil } @@ -299,313 +282,40 @@ func (mw *MultiWallet) AllWalletsAreWatchOnly() (bool, error) { 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 - } - - 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) - }) -} - -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) LinkExistingWallet(walletName, walletDataDir, originalPubPass string, privatePassphraseType int32) (*Wallet, error) { - // check if `walletDataDir` contains wallet.db - if !WalletExistsAt(walletDataDir) { - return nil, errors.New(ErrNotExist) - } - - ctx, _ := mw.contextWithShutdownCancel() - - // verify the public passphrase for the wallet being linked before proceeding - if err := mw.loadWalletTemporarily(ctx, walletDataDir, originalPubPass, nil); err != nil { - return nil, err - } - - wallet := &Wallet{ - Name: walletName, - PrivatePassphraseType: privatePassphraseType, - IsRestored: true, - HasDiscoveredAccounts: false, // assume that account discovery hasn't been done - } - - 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 - } - - 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(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) - } - - return 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) - if err != nil { - return err - } - - walletDataDir := filepath.Join(mw.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 - wallet.DbDriver = mw.dbDriver - - err = db.Save(wallet) // update database with complete wallet information - if err != nil { - return 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) - } - - 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) BadWallets() map[int]*dcr.Wallet { + return mw.Assets.DCR.BadWallets } func (mw *MultiWallet) DeleteBadWallet(walletID int) error { - wallet := mw.badWallets[walletID] + wallet := mw.Assets.DCR.BadWallets[walletID] if wallet == nil { return errors.New(ErrNotExist) } log.Info("Deleting bad wallet") - err := mw.db.DeleteStruct(wallet) + err := mw.DB.DeleteStruct(wallet) if err != nil { return translateError(err) } - os.RemoveAll(wallet.dataDir) - delete(mw.badWallets, walletID) + os.RemoveAll(wallet.DataDir) + delete(mw.Assets.DCR.BadWallets, walletID) return nil } -func (mw *MultiWallet) WalletWithID(walletID int) *Wallet { - if wallet, ok := mw.wallets[walletID]; ok { +func (mw *MultiWallet) WalletWithID(walletID int) *dcr.Wallet { + if wallet, ok := mw.Assets.DCR.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)) - } - - 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++ } @@ -614,12 +324,12 @@ func (mw *MultiWallet) NumWalletsNeedingSeedBackup() int32 { } func (mw *MultiWallet) LoadedWalletsCount() int32 { - return int32(len(mw.wallets)) + return int32(len(mw.Assets.DCR.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) } @@ -639,8 +349,8 @@ 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++ } } @@ -653,7 +363,7 @@ func (mw *MultiWallet) WalletNameExists(walletName string) (bool, error) { return false, errors.E(ErrReservedWalletName) } - err := mw.db.One("Name", walletName, &Wallet{}) + err := mw.DB.One("Name", walletName, &dcr.Wallet{}) if err == nil { return true, nil } else if err != storm.ErrNotFound { @@ -662,60 +372,3 @@ func (mw *MultiWallet) WalletNameExists(walletName string) (bool, error) { 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) - } - - return nil -} diff --git a/multiwallet_config.go b/multiwallet_config.go index f26408192..22259f31b 100644 --- a/multiwallet_config.go +++ b/multiwallet_config.go @@ -45,7 +45,7 @@ type configReadFn = func(multiwallet bool, key string, valueOut interface{}) err func (mw *MultiWallet) walletConfigSetFn(walletID int) configSaveFn { return func(key string, value interface{}) error { walletUniqueKey := WalletUniqueConfigKey(walletID, key) - return mw.db.Set(userConfigBucketName, walletUniqueKey, value) + return mw.DB.Set(userConfigBucketName, walletUniqueKey, value) } } @@ -54,19 +54,19 @@ func (mw *MultiWallet) walletConfigReadFn(walletID int) configReadFn { if !multiwallet { key = WalletUniqueConfigKey(walletID, key) } - return mw.db.Get(userConfigBucketName, key, valueOut) + return mw.DB.Get(userConfigBucketName, key, valueOut) } } func (mw *MultiWallet) SaveUserConfigValue(key string, value interface{}) { - err := mw.db.Set(userConfigBucketName, key, value) + err := mw.DB.Set(userConfigBucketName, key, value) if err != nil { log.Errorf("error setting config value for key: %s, error: %v", key, err) } } func (mw *MultiWallet) ReadUserConfigValue(key string, valueOut interface{}) error { - err := mw.db.Get(userConfigBucketName, key, valueOut) + err := mw.DB.Get(userConfigBucketName, key, valueOut) if err != nil && err != storm.ErrNotFound { log.Errorf("error reading config value for key: %s, error: %v", key, err) } @@ -74,14 +74,14 @@ func (mw *MultiWallet) ReadUserConfigValue(key string, valueOut interface{}) err } func (mw *MultiWallet) DeleteUserConfigValueForKey(key string) { - err := mw.db.Delete(userConfigBucketName, key) + err := mw.DB.Delete(userConfigBucketName, key) if err != nil { log.Errorf("error deleting config value for key: %s, error: %v", key, err) } } func (mw *MultiWallet) ClearConfig() { - err := mw.db.Drop(userConfigBucketName) + err := mw.DB.Drop(userConfigBucketName) if err != nil { log.Errorf("error deleting config bucket: %v", err) } diff --git a/multiwallet_utils.go b/multiwallet_utils.go index 9208c2be4..8e4f6bf7e 100644 --- a/multiwallet_utils.go +++ b/multiwallet_utils.go @@ -16,11 +16,13 @@ import ( "github.com/kevinburke/nacl" "github.com/kevinburke/nacl/secretbox" "golang.org/x/crypto/scrypt" + + "github.com/planetdecred/dcrlibwallet/wallets/dcr" ) const ( logFileName = "dcrlibwallet.log" - walletsDbName = "wallets.db" + walletsDbName = "wallets.DB" walletsMetadataBucketName = "metadata" walletstartupPassphraseField = "startup-passphrase" @@ -32,7 +34,7 @@ var ( ) func (mw *MultiWallet) batchDbTransaction(dbOp func(node storm.Node) error) (err error) { - dbTx, err := mw.db.Begin(true) + dbTx, err := mw.DB.Begin(true) if err != nil { return err } @@ -62,7 +64,7 @@ func (mw *MultiWallet) loadWalletTemporarily(ctx context.Context, walletDataDir, } // initialize the wallet loader - walletLoader := initWalletLoader(mw.chainParams, walletDataDir, mw.dbDriver) + walletLoader := initWalletLoader(mw.ChainParams, walletDataDir, mw.DbDriver) // open the wallet to get ready for temporary use wallet, err := walletLoader.OpenExistingWallet(ctx, []byte(walletPublicPass)) @@ -88,7 +90,7 @@ func (mw *MultiWallet) markWalletAsDiscoveredAccounts(walletID int) error { log.Infof("Set discovered accounts = true for wallet %d", wallet.ID) wallet.HasDiscoveredAccounts = true - err := mw.db.Save(wallet) + err := mw.DB.Save(wallet) if err != nil { return err } @@ -100,7 +102,7 @@ func (mw *MultiWallet) markWalletAsDiscoveredAccounts(walletID int) error { // multiwallet's root directory in bytes. func (mw *MultiWallet) RootDirFileSizeInBytes() (int64, error) { var size int64 - err := filepath.Walk(mw.rootDir, func(_ string, info os.FileInfo, err error) error { + err := filepath.Walk(mw.RootDir, func(_ string, info os.FileInfo, err error) error { if err != nil { return err } @@ -117,7 +119,7 @@ func (mw *MultiWallet) RootDirFileSizeInBytes() (int64, error) { // changes to the stake difficulty algorithm. func (mw *MultiWallet) DCP0001ActivationBlockHeight() int32 { var activationHeight int32 = -1 - switch strings.ToLower(mw.chainParams.Name) { + switch strings.ToLower(mw.ChainParams.Name) { case strings.ToLower(Mainnet): activationHeight = deployments.DCP0001.MainNetActivationHeight case strings.ToLower(Testnet3): @@ -134,7 +136,7 @@ func (mw *MultiWallet) WalletWithXPub(xpub string) (int, error) { ctx, cancel := mw.contextWithShutdownCancel() defer cancel() - for _, w := range mw.wallets { + for _, w := range mw.Assets.DCR.Wallets { if !w.WalletOpened() { return -1, errors.Errorf("wallet %d is not open and cannot be checked", w.ID) } @@ -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,12 +168,12 @@ 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 } - for _, wallet := range mw.wallets { + for _, wallet := range mw.Assets.DCR.Wallets { if !wallet.WalletOpened() { return -1, errors.Errorf("cannot check if seed matches unloaded wallet %d", wallet.ID) } @@ -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/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/utils.go b/utils.go index 9e4068919..e91e37770 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) @@ -102,7 +83,7 @@ func (mw *MultiWallet) contextWithShutdownCancel() (context.Context, context.Can } func (mw *MultiWallet) ValidateExtPubKey(extendedPubKey string) error { - _, err := hdkeychain.NewKeyFromString(extendedPubKey, mw.chainParams) + _, err := hdkeychain.NewKeyFromString(extendedPubKey, mw.ChainParams) if err != nil { if err == hdkeychain.ErrInvalidChild { return errors.New(ErrUnusableSeed) @@ -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/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..146a127ed 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..f9a3f3473 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..ccfca7c9b --- /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[0] = 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/types.go b/wallets/dcr/types.go similarity index 96% rename from types.go rename to wallets/dcr/types.go index 05d4bccdc..6485281fc 100644 --- a/types.go +++ b/wallets/dcr/types.go @@ -1,14 +1,17 @@ -package dcrlibwallet +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" ) @@ -401,6 +404,26 @@ type VSPTicketInfo struct { /** 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"` 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..ff735595e --- /dev/null +++ b/wallets/dcr/wallet.go @@ -0,0 +1,640 @@ +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 + cancel context.CancelFunc + CancelAccountMixer context.CancelFunc `json:"-"` + + cancelAutoTicketBuyerMu sync.Mutex + cancelAutoTicketBuyer context.CancelFunc `json:"-"` + + 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 CreateNewWallet(walletName, privatePassphrase string, privatePassphraseType int32, db *storm.DB, rootDir, dbDriver string, chainParams *chaincfg.Params) (*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, + db: db, + dbDriver: dbDriver, + rootDir: rootDir, + chainParams: chainParams, + syncData: &SyncData{ + SyncProgressListeners: make(map[string]SyncProgressListener), + }, + txAndBlockNotificationListeners: make(map[string]TxAndBlockNotificationListener), + accountMixerNotificationListener: make(map[string]AccountMixerNotificationListener), + cancelFuncs: make([]context.CancelFunc, 0), + CreatedAt: time.Now(), + EncryptedSeed: encryptedSeed, + PrivatePassphraseType: privatePassphraseType, + HasDiscoveredAccounts: true, + } + + wallet.cancelFuncs = make([]context.CancelFunc, 0) + + return wallet.saveNewWallet(func() error { + err := wallet.Prepare(wallet.rootDir, wallet.chainParams, wallet.walletConfigSetFn(wallet.ID), wallet.walletConfigReadFn(wallet.ID)) + if err != nil { + return err + } + return wallet.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() { + 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) + } + + 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..a185bcc25 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..2ec5deb8d --- /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"