├── .github └── workflows │ └── build.yml ├── .gitignore ├── README.md ├── account_mixer.go ├── accounts.go ├── address.go ├── addresshelper └── helper.go ├── badgerdb ├── bucket.go ├── db.go └── driver.go ├── consensus.go ├── dcrlibwallet_suite_test.go ├── decodetx.go ├── dexclient.go ├── dexdcr ├── config.go ├── go.mod ├── go.sum ├── spvsyncer.go └── wallet.go ├── errors.go ├── ext ├── ext_test.go ├── httpclient.go ├── log.go ├── service.go └── types.go ├── go.mod ├── go.sum ├── internal ├── certs │ └── cspp.go ├── loader │ ├── loader.go │ └── log.go ├── politeia │ ├── errors.go │ ├── log.go │ ├── politeia.go │ ├── politeia_client.go │ ├── politeia_sync.go │ └── types.go ├── uniformprng │ └── prng.go └── vsp │ ├── client.go │ ├── errors.go │ ├── feepayment.go │ ├── log.go │ └── vsp.go ├── log.go ├── message.go ├── multiwallet.go ├── multiwallet_config.go ├── multiwallet_utils.go ├── multiwallet_utils_test.go ├── rescan.go ├── run_tests.sh ├── spv ├── backend.go ├── log.go ├── rescan.go └── sync.go ├── sync.go ├── syncnotification.go ├── ticket.go ├── transactions.go ├── treasury.go ├── txandblocknotifications.go ├── txauthor.go ├── txhelper ├── changesource.go ├── helper.go ├── outputs.go └── types.go ├── txindex.go ├── txparser.go ├── types.go ├── utils.go ├── utils └── netparams.go ├── utxo.go ├── vsp.go ├── wallet.go ├── wallet_config.go ├── walletdata ├── db.go ├── filter.go ├── read.go └── save.go ├── wallets.go └── wordlist.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: [push, pull_request] 3 | permissions: 4 | contents: read 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Set up Go 1.17 11 | uses: actions/setup-go@v2 12 | with: 13 | go-version: 1.17 14 | 15 | - name: Check out code into the Go module directory 16 | uses: actions/checkout@v2 17 | 18 | - name: Cache (dependencies) 19 | uses: actions/cache@v1 20 | id: cache 21 | with: 22 | path: ~/go/pkg/mod 23 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 24 | restore-keys: | 25 | ${{ runner.os }}-go- 26 | 27 | - name: Install dependencies 28 | if: steps.cache.outputs.cache-hit != 'true' 29 | env: 30 | GO111MODULE: "on" 31 | run: go mod download 32 | 33 | - name: Build 34 | env: 35 | GO111MODULE: "on" 36 | run: go build 37 | 38 | - name: Install linter 39 | run: "curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.37.0" 40 | 41 | - name: Test and Lint 42 | env: 43 | GO111MODULE: "on" 44 | run: | 45 | export PATH=${PATH}:$(go env GOPATH)/bin 46 | sh ./run_tests.sh 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | vendor 4 | Dcrlibwallet.framework 5 | dcrlibwallet.aar 6 | dcrlibwallet-sources.jar 7 | main/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dcrlibwallet 2 | 3 | [![Build Status](https://github.com/planetdecred/dcrlibwallet/workflows/Build/badge.svg)](https://github.com/planetdecred/dcrlibwallet/actions) 4 | 5 | A Decred wallet library written in golang for [dcrwallet](https://github.com/decred/dcrwallet) 6 | 7 | ## Build Dependencies 8 | 9 | [Go( >= 1.11 )](http://golang.org/doc/install) 10 | [Gomobile](https://github.com/golang/go/wiki/Mobile#tools) (correctly init'd with gomobile init) 11 | 12 | ## Build Instructions using Gomobile 13 | 14 | To build this libary, clone the project 15 | 16 | ```bash 17 | go get -t github.com/planetdecred/dcrlibwallet 18 | cd $GOPATH/src/github.com/planetdecred/dcrlibwallet/ 19 | ``` 20 | 21 | and run the following commands in dcrlibwallet directory. 22 | 23 | ```bash 24 | export GO111MODULE=on 25 | go mod download 26 | go mod vendor 27 | export GO111MODULE=off 28 | gomobile bind -target=android # -target=ios for iOS 29 | ``` 30 | 31 | dcrlibwallet can be built targeting different architectures of android which can be configured using the `-target` command line argument Ex. `gomobile bind -target=android/arm`, `gomobile bind -target=android/386`... 32 | 33 | Copy the generated library (dcrlibwallet.aar for android or dcrlibwallet.framewok in the case of iOS) into `libs` directory(`Frameworks` for iOS) 34 | -------------------------------------------------------------------------------- /accounts.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "decred.org/dcrwallet/v2/errors" 10 | w "decred.org/dcrwallet/v2/wallet" 11 | "decred.org/dcrwallet/v2/wallet/udb" 12 | "github.com/decred/dcrd/chaincfg/v3" 13 | "github.com/decred/dcrd/dcrutil/v4" 14 | "github.com/planetdecred/dcrlibwallet/addresshelper" 15 | ) 16 | 17 | const ( 18 | AddressGapLimit uint32 = 20 19 | ImportedAccountNumber = udb.ImportedAddrAccount 20 | DefaultAccountNum = udb.DefaultAccountNum 21 | ) 22 | 23 | func (wallet *Wallet) GetAccounts() (string, error) { 24 | accountsResponse, err := wallet.GetAccountsRaw() 25 | if err != nil { 26 | return "", nil 27 | } 28 | 29 | result, _ := json.Marshal(accountsResponse) 30 | return string(result), nil 31 | } 32 | 33 | func (wallet *Wallet) GetAccountsRaw() (*Accounts, error) { 34 | resp, err := wallet.Internal().Accounts(wallet.shutdownContext()) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | accounts := make([]*Account, len(resp.Accounts)) 40 | for i, a := range resp.Accounts { 41 | balance, err := wallet.GetAccountBalance(int32(a.AccountNumber)) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | accounts[i] = &Account{ 47 | WalletID: wallet.ID, 48 | Number: int32(a.AccountNumber), 49 | Name: a.AccountName, 50 | Balance: balance, 51 | TotalBalance: balance.Total, 52 | ExternalKeyCount: int32(a.LastUsedExternalIndex + AddressGapLimit), // Add gap limit 53 | InternalKeyCount: int32(a.LastUsedInternalIndex + AddressGapLimit), 54 | ImportedKeyCount: int32(a.ImportedKeyCount), 55 | } 56 | } 57 | 58 | return &Accounts{ 59 | Count: len(resp.Accounts), 60 | CurrentBlockHash: resp.CurrentBlockHash[:], 61 | CurrentBlockHeight: resp.CurrentBlockHeight, 62 | Acc: accounts, 63 | }, nil 64 | } 65 | 66 | func (wallet *Wallet) AccountsIterator() (*AccountsIterator, error) { 67 | accounts, err := wallet.GetAccountsRaw() 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return &AccountsIterator{ 73 | currentIndex: 0, 74 | accounts: accounts.Acc, 75 | }, nil 76 | } 77 | 78 | func (accountsInterator *AccountsIterator) Next() *Account { 79 | if accountsInterator.currentIndex < len(accountsInterator.accounts) { 80 | account := accountsInterator.accounts[accountsInterator.currentIndex] 81 | accountsInterator.currentIndex++ 82 | return account 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func (accountsInterator *AccountsIterator) Reset() { 89 | accountsInterator.currentIndex = 0 90 | } 91 | 92 | func (wallet *Wallet) GetAccount(accountNumber int32) (*Account, error) { 93 | accounts, err := wallet.GetAccountsRaw() 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | for _, account := range accounts.Acc { 99 | if account.Number == accountNumber { 100 | return account, nil 101 | } 102 | } 103 | 104 | return nil, errors.New(ErrNotExist) 105 | } 106 | 107 | func (wallet *Wallet) GetAccountBalance(accountNumber int32) (*Balance, error) { 108 | balance, err := wallet.Internal().AccountBalance(wallet.shutdownContext(), uint32(accountNumber), wallet.RequiredConfirmations()) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | return &Balance{ 114 | Total: int64(balance.Total), 115 | Spendable: int64(balance.Spendable), 116 | ImmatureReward: int64(balance.ImmatureCoinbaseRewards), 117 | ImmatureStakeGeneration: int64(balance.ImmatureStakeGeneration), 118 | LockedByTickets: int64(balance.LockedByTickets), 119 | VotingAuthority: int64(balance.VotingAuthority), 120 | UnConfirmed: int64(balance.Unconfirmed), 121 | }, nil 122 | } 123 | 124 | func (wallet *Wallet) SpendableForAccount(account int32) (int64, error) { 125 | bals, err := wallet.Internal().AccountBalance(wallet.shutdownContext(), uint32(account), wallet.RequiredConfirmations()) 126 | if err != nil { 127 | log.Error(err) 128 | return 0, translateError(err) 129 | } 130 | return int64(bals.Spendable), nil 131 | } 132 | 133 | func (wallet *Wallet) UnspentOutputs(account int32) ([]*UnspentOutput, error) { 134 | policy := w.OutputSelectionPolicy{ 135 | Account: uint32(account), 136 | RequiredConfirmations: wallet.RequiredConfirmations(), 137 | } 138 | 139 | // fetch all utxos in account to extract details for the utxos selected by user 140 | // use targetAmount = 0 to fetch ALL utxos in account 141 | inputDetail, err := wallet.Internal().SelectInputs(wallet.shutdownContext(), dcrutil.Amount(0), policy) 142 | 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | unspentOutputs := make([]*UnspentOutput, len(inputDetail.Inputs)) 148 | 149 | for i, input := range inputDetail.Inputs { 150 | outputInfo, err := wallet.Internal().OutputInfo(wallet.shutdownContext(), &input.PreviousOutPoint) 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | // unique key to identify utxo 156 | outputKey := fmt.Sprintf("%s:%d", input.PreviousOutPoint.Hash, input.PreviousOutPoint.Index) 157 | 158 | addresses := addresshelper.PkScriptAddresses(wallet.chainParams, inputDetail.Scripts[i]) 159 | 160 | var confirmations int32 161 | inputBlockHeight := int32(input.BlockHeight) 162 | if inputBlockHeight != -1 { 163 | confirmations = wallet.GetBestBlock() - inputBlockHeight + 1 164 | } 165 | 166 | unspentOutputs[i] = &UnspentOutput{ 167 | TransactionHash: input.PreviousOutPoint.Hash[:], 168 | OutputIndex: input.PreviousOutPoint.Index, 169 | OutputKey: outputKey, 170 | Tree: int32(input.PreviousOutPoint.Tree), 171 | Amount: int64(outputInfo.Amount), 172 | PkScript: inputDetail.Scripts[i], 173 | ReceiveTime: outputInfo.Received.Unix(), 174 | FromCoinbase: outputInfo.FromCoinbase, 175 | Addresses: strings.Join(addresses, ", "), 176 | Confirmations: confirmations, 177 | } 178 | } 179 | 180 | return unspentOutputs, nil 181 | } 182 | 183 | func (wallet *Wallet) CreateNewAccount(accountName string, privPass []byte) (int32, error) { 184 | err := wallet.UnlockWallet(privPass) 185 | if err != nil { 186 | return -1, err 187 | } 188 | 189 | defer wallet.LockWallet() 190 | 191 | return wallet.NextAccount(accountName) 192 | } 193 | 194 | func (wallet *Wallet) NextAccount(accountName string) (int32, error) { 195 | 196 | if wallet.IsLocked() { 197 | return -1, errors.New(ErrWalletLocked) 198 | } 199 | 200 | ctx := wallet.shutdownContext() 201 | 202 | accountNumber, err := wallet.Internal().NextAccount(ctx, accountName) 203 | if err != nil { 204 | return -1, err 205 | } 206 | 207 | return int32(accountNumber), nil 208 | } 209 | 210 | func (wallet *Wallet) RenameAccount(accountNumber int32, newName string) error { 211 | err := wallet.Internal().RenameAccount(wallet.shutdownContext(), uint32(accountNumber), newName) 212 | if err != nil { 213 | return translateError(err) 214 | } 215 | 216 | return nil 217 | } 218 | 219 | func (wallet *Wallet) AccountName(accountNumber int32) (string, error) { 220 | name, err := wallet.AccountNameRaw(uint32(accountNumber)) 221 | if err != nil { 222 | return "", translateError(err) 223 | } 224 | return name, nil 225 | } 226 | 227 | func (wallet *Wallet) AccountNameRaw(accountNumber uint32) (string, error) { 228 | return wallet.Internal().AccountName(wallet.shutdownContext(), accountNumber) 229 | } 230 | 231 | func (wallet *Wallet) AccountNumber(accountName string) (int32, error) { 232 | accountNumber, err := wallet.Internal().AccountNumber(wallet.shutdownContext(), accountName) 233 | return int32(accountNumber), translateError(err) 234 | } 235 | 236 | func (wallet *Wallet) HasAccount(accountName string) bool { 237 | _, err := wallet.Internal().AccountNumber(wallet.shutdownContext(), accountName) 238 | return err == nil 239 | } 240 | 241 | func (wallet *Wallet) HDPathForAccount(accountNumber int32) (string, error) { 242 | cointype, err := wallet.Internal().CoinType(wallet.shutdownContext()) 243 | if err != nil { 244 | return "", translateError(err) 245 | } 246 | 247 | var hdPath string 248 | isLegacyCoinType := cointype == wallet.chainParams.LegacyCoinType 249 | if wallet.chainParams.Name == chaincfg.MainNetParams().Name { 250 | if isLegacyCoinType { 251 | hdPath = LegacyMainnetHDPath 252 | } else { 253 | hdPath = MainnetHDPath 254 | } 255 | } else { 256 | if isLegacyCoinType { 257 | hdPath = LegacyTestnetHDPath 258 | } else { 259 | hdPath = TestnetHDPath 260 | } 261 | } 262 | 263 | return hdPath + strconv.Itoa(int(accountNumber)), nil 264 | } 265 | -------------------------------------------------------------------------------- /address.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "fmt" 5 | 6 | "decred.org/dcrwallet/v2/errors" 7 | w "decred.org/dcrwallet/v2/wallet" 8 | "github.com/decred/dcrd/txscript/v4/stdaddr" 9 | ) 10 | 11 | // AddressInfo holds information about an address 12 | // If the address belongs to the querying wallet, IsMine will be true and the AccountNumber and AccountName values will be populated 13 | type AddressInfo struct { 14 | Address string 15 | IsMine bool 16 | AccountNumber uint32 17 | AccountName string 18 | } 19 | 20 | func (mw *MultiWallet) IsAddressValid(address string) bool { 21 | _, err := stdaddr.DecodeAddress(address, mw.chainParams) 22 | return err == nil 23 | } 24 | 25 | func (wallet *Wallet) HaveAddress(address string) bool { 26 | addr, err := stdaddr.DecodeAddress(address, wallet.chainParams) 27 | if err != nil { 28 | return false 29 | } 30 | 31 | have, err := wallet.Internal().HaveAddress(wallet.shutdownContext(), addr) 32 | if err != nil { 33 | return false 34 | } 35 | 36 | return have 37 | } 38 | 39 | func (wallet *Wallet) AccountOfAddress(address string) (string, error) { 40 | addr, err := stdaddr.DecodeAddress(address, wallet.chainParams) 41 | if err != nil { 42 | return "", translateError(err) 43 | } 44 | 45 | a, err := wallet.Internal().KnownAddress(wallet.shutdownContext(), addr) 46 | if err != nil { 47 | return "", translateError(err) 48 | } 49 | 50 | return a.AccountName(), nil 51 | } 52 | 53 | func (wallet *Wallet) AddressInfo(address string) (*AddressInfo, error) { 54 | addr, err := stdaddr.DecodeAddress(address, wallet.chainParams) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | addressInfo := &AddressInfo{ 60 | Address: address, 61 | } 62 | 63 | known, _ := wallet.Internal().KnownAddress(wallet.shutdownContext(), addr) 64 | if known != nil { 65 | addressInfo.IsMine = true 66 | addressInfo.AccountName = known.AccountName() 67 | 68 | accountNumber, err := wallet.AccountNumber(known.AccountName()) 69 | if err != nil { 70 | return nil, err 71 | } 72 | addressInfo.AccountNumber = uint32(accountNumber) 73 | } 74 | 75 | return addressInfo, nil 76 | } 77 | 78 | // CurrentAddress gets the most recently requested payment address from the 79 | // wallet. If that address has already been used to receive funds, the next 80 | // chained address is returned. 81 | func (wallet *Wallet) CurrentAddress(account int32) (string, error) { 82 | if wallet.IsRestored && !wallet.HasDiscoveredAccounts { 83 | return "", errors.E(ErrAddressDiscoveryNotDone) 84 | } 85 | 86 | addr, err := wallet.Internal().CurrentAddress(uint32(account)) 87 | if err != nil { 88 | log.Errorf("CurrentAddress error: %w", err) 89 | return "", err 90 | } 91 | return addr.String(), nil 92 | } 93 | 94 | // NextAddress returns the address immediately following the last requested 95 | // payment address. If that address has already been used to receive funds, 96 | // the next chained address is returned. 97 | func (wallet *Wallet) NextAddress(account int32) (string, error) { 98 | if wallet.IsRestored && !wallet.HasDiscoveredAccounts { 99 | return "", errors.E(ErrAddressDiscoveryNotDone) 100 | } 101 | 102 | // NewExternalAddress increments the lastReturnedAddressIndex but does 103 | // not return the address at the new index. The actual new address (at 104 | // the newly incremented index) is returned below by CurrentAddress. 105 | // NOTE: This workaround will be unnecessary once this anomaly is corrected 106 | // upstream. 107 | _, err := wallet.Internal().NewExternalAddress(wallet.shutdownContext(), uint32(account), w.WithGapPolicyWrap()) 108 | if err != nil { 109 | log.Errorf("NewExternalAddress error: %w", err) 110 | return "", err 111 | } 112 | 113 | return wallet.CurrentAddress(account) 114 | } 115 | 116 | func (wallet *Wallet) AddressPubKey(address string) (string, error) { 117 | addr, err := stdaddr.DecodeAddress(address, wallet.chainParams) 118 | if err != nil { 119 | return "", err 120 | } 121 | 122 | known, err := wallet.Internal().KnownAddress(wallet.shutdownContext(), addr) 123 | if err != nil { 124 | return "", err 125 | } 126 | 127 | switch known := known.(type) { 128 | case w.PubKeyHashAddress: 129 | pubKeyAddr, err := stdaddr.NewAddressPubKeyEcdsaSecp256k1V0Raw(known.PubKey(), wallet.chainParams) 130 | if err != nil { 131 | return "", err 132 | } 133 | return pubKeyAddr.String(), nil 134 | 135 | default: 136 | return "", fmt.Errorf("address is not a managed pub key address") 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /addresshelper/helper.go: -------------------------------------------------------------------------------- 1 | package addresshelper 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/decred/dcrd/chaincfg/v3" 7 | "github.com/decred/dcrd/dcrutil/v4" 8 | "github.com/decred/dcrd/txscript/v4/stdaddr" 9 | "github.com/decred/dcrd/txscript/v4/stdscript" 10 | ) 11 | 12 | const scriptVersion = 0 13 | 14 | func PkScript(address string, net dcrutil.AddressParams) ([]byte, error) { 15 | addr, err := stdaddr.DecodeAddress(address, net) 16 | if err != nil { 17 | return nil, fmt.Errorf("error decoding address '%s': %s", address, err.Error()) 18 | } 19 | 20 | _, pkScript := addr.PaymentScript() 21 | return pkScript, nil 22 | } 23 | 24 | func PkScriptAddresses(params *chaincfg.Params, pkScript []byte) []string { 25 | _, addresses := stdscript.ExtractAddrs(scriptVersion, pkScript, params) 26 | encodedAddresses := make([]string, len(addresses)) 27 | for i, address := range addresses { 28 | encodedAddresses[i] = address.String() 29 | } 30 | return encodedAddresses 31 | } 32 | -------------------------------------------------------------------------------- /badgerdb/bucket.go: -------------------------------------------------------------------------------- 1 | package badgerdb 2 | 3 | import ( 4 | "bytes" 5 | 6 | "decred.org/dcrwallet/v2/errors" 7 | "github.com/dgraph-io/badger" 8 | ) 9 | 10 | const ( 11 | // Maximum length of a key, in bytes. 12 | maxKeySize = 65378 13 | 14 | // Holds an identifier for a bucket 15 | metaBucket = 5 16 | ) 17 | 18 | // Bucket is an internal type used to represent a collection of key/value pairs 19 | // and implements the walletdb Bucket interfaces. 20 | type Bucket struct { 21 | prefix []byte 22 | buckets []*Bucket 23 | txn *badger.Txn 24 | dbTransaction *transaction 25 | } 26 | 27 | // Cursor represents a cursor over key/value pairs and nested buckets of a 28 | // bucket. 29 | // 30 | // Note that open cursors are not tracked on bucket changes and any 31 | // modifications to the bucket, with the exception of cursor.Delete, invalidate 32 | // the cursor. After invalidation, the cursor must be repositioned, or the keys 33 | // and values returned may be unpredictable. 34 | type Cursor struct { 35 | iterator *badger.Iterator 36 | reverseIterator *badger.Iterator 37 | txn *badger.Txn 38 | prefix []byte 39 | ck []byte 40 | dbTransaction *transaction 41 | } 42 | 43 | func newBucket(tx *badger.Txn, badgerKey []byte, dbTx *transaction) (*Bucket, error) { 44 | prefix := make([]byte, len(badgerKey)) 45 | copy(prefix, badgerKey) 46 | item, err := tx.Get(prefix) 47 | if err != nil { 48 | //Not Found 49 | if err == badger.ErrKeyNotFound { 50 | entry := badger.NewEntry(prefix, insertPrefixLength([]byte{}, len(prefix))).WithMeta(metaBucket) 51 | err = tx.SetEntry(entry) 52 | if err != nil { 53 | return nil, convertErr(err) 54 | } 55 | return &Bucket{txn: tx, prefix: prefix, dbTransaction: dbTx}, nil 56 | } 57 | return nil, convertErr(err) 58 | } 59 | if item.UserMeta() != metaBucket { 60 | errors.E(errors.Invalid, "key is not associated with a bucket") 61 | } 62 | return &Bucket{txn: tx, prefix: prefix, dbTransaction: dbTx}, nil 63 | } 64 | 65 | func insertPrefixLength(val []byte, length int) []byte { 66 | result := make([]byte, 0) 67 | prefixBytes := byte(length) 68 | result = append(result, prefixBytes) 69 | result = append(result, val...) 70 | return result 71 | } 72 | 73 | func addPrefix(prefix []byte, key []byte) ([]byte, error) { 74 | if len(key) > maxKeySize { 75 | return nil, errors.E(errors.Invalid, "key too long") 76 | } 77 | return append(prefix, key...), nil 78 | } 79 | 80 | // SetTx changes the transaction for bucket and sub buckets 81 | func (b *Bucket) setTx(tx *badger.Txn) { 82 | b.txn = tx 83 | for _, bkt := range b.buckets { 84 | bkt.setTx(tx) 85 | } 86 | } 87 | 88 | func (b *Bucket) iterator() *badger.Iterator { 89 | opts := badger.DefaultIteratorOptions 90 | opts.PrefetchSize = 100 91 | it := b.txn.NewIterator(opts) 92 | return it 93 | } 94 | 95 | func (b *Bucket) badgerCursor() *Cursor { 96 | reverseOptions := badger.DefaultIteratorOptions 97 | //Key-only iteration for faster search. Value gets fetched when item.Value() is called. 98 | reverseOptions.PrefetchValues = false 99 | reverseOptions.Reverse = true 100 | txn := b.dbTransaction.db.NewTransaction(false) 101 | reverseIterator := txn.NewIterator(reverseOptions) 102 | cursor := &Cursor{iterator: b.iterator(), reverseIterator: reverseIterator, txn: b.txn, prefix: b.prefix, dbTransaction: b.dbTransaction} 103 | return cursor 104 | } 105 | 106 | // Bucket returns a nested bucket which is created from the passed key 107 | func (b *Bucket) bucket(key []byte, errorIfExists bool) (*Bucket, error) { 108 | if len(key) == 0 { 109 | //Empty Key 110 | return nil, errors.E(errors.Invalid, "key is empty") 111 | } 112 | keyPrefix, err := addPrefix(b.prefix, key) 113 | if err != nil { 114 | return nil, err 115 | } 116 | copiedKey := make([]byte, len(keyPrefix)) 117 | copy(copiedKey, keyPrefix) 118 | item, err := b.txn.Get(copiedKey) 119 | if err != nil { 120 | //Key Not Found 121 | entry := badger.NewEntry(copiedKey, insertPrefixLength([]byte{}, len(b.prefix))).WithMeta(metaBucket) 122 | err = b.txn.SetEntry(entry) 123 | if err != nil { 124 | return nil, convertErr(err) 125 | } 126 | bucket := &Bucket{txn: b.txn, prefix: copiedKey, dbTransaction: b.dbTransaction} 127 | b.buckets = append(b.buckets, bucket) 128 | return bucket, nil 129 | } 130 | 131 | if item.UserMeta() == metaBucket { 132 | if errorIfExists { 133 | return nil, errors.E(errors.Exist, "bucket already exists") 134 | } 135 | 136 | bucket := &Bucket{txn: b.txn, prefix: copiedKey, dbTransaction: b.dbTransaction} 137 | b.buckets = append(b.buckets, bucket) 138 | return bucket, nil 139 | } 140 | 141 | return nil, errors.E(errors.Invalid, "key is not associated with a bucket") 142 | } 143 | 144 | // DropBucket deletes a bucket and all it's data 145 | // from the database. Return nil if bucket does 146 | // not exist, transaction is not writable or given 147 | // key does not point to a bucket 148 | func (b *Bucket) dropBucket(key []byte) error { 149 | if !b.dbTransaction.writable { 150 | return errors.E(errors.Invalid, "cannot delete nested bucket in a read-only transaction") 151 | } 152 | 153 | prefix, err := addPrefix(b.prefix, key) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | item, err := b.txn.Get(prefix) 159 | if err != nil { 160 | return convertErr(err) 161 | } 162 | 163 | if item.UserMeta() != metaBucket { 164 | return errors.E(errors.Invalid, "key is not associated with a bucket") 165 | } 166 | 167 | iteratorTxn := b.dbTransaction.db.NewTransaction(true) 168 | it := iteratorTxn.NewIterator(badger.DefaultIteratorOptions) 169 | 170 | it.Rewind() 171 | for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { 172 | item := it.Item() 173 | if bytes.Equal(item.Key(), prefix) { 174 | continue 175 | } 176 | 177 | v, err := item.ValueCopy(nil) 178 | if err != nil { 179 | return convertErr(err) 180 | } 181 | 182 | prefixLength := int(v[0]) 183 | if bytes.Equal(item.Key()[:prefixLength], prefix) { 184 | retryDelete: 185 | err = b.txn.Delete(item.KeyCopy(nil)) 186 | if err != nil { 187 | if err == badger.ErrTxnTooBig { 188 | err = b.txn.Commit() 189 | if err != nil { 190 | return err 191 | } 192 | *b.txn = *b.dbTransaction.db.NewTransaction(true) 193 | goto retryDelete 194 | } 195 | return err 196 | } 197 | } 198 | } 199 | it.Close() 200 | iteratorTxn.Discard() 201 | 202 | err = b.txn.Commit() 203 | if err != nil { 204 | return convertErr(err) 205 | } 206 | 207 | *b.txn = *b.dbTransaction.db.NewTransaction(true) 208 | 209 | err = b.txn.Delete(item.Key()[:]) 210 | if err != nil { 211 | return convertErr(err) 212 | } 213 | 214 | return nil 215 | } 216 | 217 | func (b *Bucket) get(key []byte) []byte { 218 | if len(key) == 0 { 219 | return nil 220 | } 221 | k, err := addPrefix(b.prefix, key) 222 | if err != nil { 223 | return nil 224 | } 225 | item, err := b.txn.Get(k) 226 | if err != nil { 227 | //Not found 228 | return nil 229 | } 230 | val, err := item.ValueCopy(nil) 231 | if err != nil { 232 | return nil 233 | } 234 | return val[1:] 235 | } 236 | 237 | func (b *Bucket) put(key []byte, value []byte) error { 238 | if len(key) == 0 { 239 | return errors.E(errors.Invalid, "key is empty") 240 | } else if len(key) > maxKeySize { 241 | return errors.E(errors.Invalid, "key is too large") 242 | } 243 | copiedKey := make([]byte, len(key)) 244 | copy(copiedKey, key[:]) 245 | 246 | k, err := addPrefix(b.prefix, copiedKey) 247 | if err != nil { 248 | return err 249 | } 250 | err = b.txn.Set(k, insertPrefixLength(value[:], len(b.prefix))) 251 | 252 | return err 253 | } 254 | 255 | func (b *Bucket) delete(key []byte) error { 256 | if len(key) == 0 { 257 | return nil 258 | } 259 | 260 | k, err := addPrefix(b.prefix, key) 261 | if err != nil { 262 | return err 263 | } 264 | err = b.txn.Delete(k) 265 | if err == badger.ErrKeyNotFound { 266 | return nil 267 | } 268 | return err 269 | } 270 | 271 | func (b *Bucket) forEach(fn func(k, v []byte) error) error { 272 | txn := b.txn 273 | it := txn.NewIterator(badger.DefaultIteratorOptions) 274 | defer func() { 275 | it.Close() 276 | }() 277 | prefix := b.prefix 278 | it.Rewind() 279 | for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { 280 | item := it.Item() 281 | k := item.Key() 282 | if bytes.Equal(item.Key(), prefix) { 283 | continue 284 | } 285 | 286 | v, err := item.ValueCopy(nil) 287 | if err != nil { 288 | return convertErr(err) 289 | } 290 | 291 | prefixLength := int(v[0]) 292 | if bytes.Equal(item.Key()[:prefixLength], prefix) { 293 | if item.UserMeta() == metaBucket { 294 | if err := fn(k[prefixLength:], nil); err != nil { 295 | return err 296 | } 297 | } else { 298 | if err := fn(k[prefixLength:], v[1:]); err != nil { 299 | return err 300 | } 301 | } 302 | } 303 | } 304 | return nil 305 | } 306 | -------------------------------------------------------------------------------- /badgerdb/driver.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The btcsuite developers 2 | // Copyright (c) 2015 The Decred developers 3 | // Use of this source code is governed by an ISC 4 | // license that can be found in the LICENSE file. 5 | 6 | package badgerdb 7 | 8 | import ( 9 | "fmt" 10 | 11 | "decred.org/dcrwallet/v2/errors" 12 | "decred.org/dcrwallet/v2/wallet/walletdb" 13 | ) 14 | 15 | const ( 16 | dbType = "badgerdb" 17 | ) 18 | 19 | // parseArgs parses the arguments from the walletdb Open/Create methods. 20 | func parseArgs(funcName string, args ...interface{}) (string, error) { 21 | if len(args) != 1 { 22 | return "", errors.Errorf("invalid arguments to %s.%s -- "+ 23 | "expected database path", dbType, funcName) 24 | } 25 | 26 | dbPath, ok := args[0].(string) 27 | if !ok { 28 | return "", errors.Errorf("first argument to %s.%s is invalid -- "+ 29 | "expected database path string", dbType, funcName) 30 | } 31 | 32 | return dbPath, nil 33 | } 34 | 35 | // openDBDriver is the callback provided during driver registration that opens 36 | // an existing database for use. 37 | func openDBDriver(args ...interface{}) (walletdb.DB, error) { 38 | dbPath, err := parseArgs("Open", args...) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return openDB(dbPath, false) 44 | } 45 | 46 | // createDBDriver is the callback provided during driver registration that 47 | // creates, initializes, and opens a database for use. 48 | func createDBDriver(args ...interface{}) (walletdb.DB, error) { 49 | dbPath, err := parseArgs("Create", args...) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return openDB(dbPath, true) 55 | } 56 | 57 | func init() { 58 | // Register the driver. 59 | driver := walletdb.Driver{ 60 | DbType: dbType, 61 | Create: createDBDriver, 62 | Open: openDBDriver, 63 | } 64 | if err := walletdb.RegisterDriver(driver); err != nil { 65 | panic(fmt.Sprintf("Failed to register database driver '%s': %v", 66 | dbType, err)) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /consensus.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | 8 | "decred.org/dcrwallet/v2/errors" 9 | w "decred.org/dcrwallet/v2/wallet" 10 | 11 | "github.com/decred/dcrd/chaincfg/chainhash" 12 | "github.com/decred/dcrd/chaincfg/v3" 13 | "github.com/decred/dcrd/wire" 14 | ) 15 | 16 | // AgendaStatusType defines the various agenda statuses. 17 | type AgendaStatusType string 18 | 19 | const ( 20 | dcrdataAgendasAPIMainnetUrl = "https://dcrdata.decred.org/api/agendas" 21 | dcrdataAgendasAPITestnetUrl = "https://testnet.decred.org/api/agendas" 22 | 23 | // AgendaStatusUpcoming used to define an agenda yet to vote. 24 | AgendaStatusUpcoming AgendaStatusType = "upcoming" 25 | 26 | // AgendaStatusInProgress used to define an agenda with voting ongoing. 27 | AgendaStatusInProgress AgendaStatusType = "in progress" 28 | 29 | // AgendaStatusFailed used to define an agenda when the votes tally does not 30 | // attain the minimum threshold set. Activation height is not set for such an 31 | // agenda. 32 | AgendaStatusFailed AgendaStatusType = "failed" 33 | 34 | // AgendaStatusLockedIn used to define an agenda that has passed after attaining 35 | // the minimum set threshold. 36 | AgendaStatusLockedIn AgendaStatusType = "locked in" 37 | 38 | // AgendaStatusFinished used to define an agenda that has finished voting. 39 | AgendaStatusFinished AgendaStatusType = "finished" 40 | 41 | // UnknownStatus is used when a status string is not recognized. 42 | UnknownStatus AgendaStatusType = "unknown" 43 | ) 44 | 45 | func (a AgendaStatusType) String() string { 46 | switch a { 47 | case AgendaStatusUpcoming: 48 | return "upcoming" 49 | case AgendaStatusInProgress: 50 | return "in progress" 51 | case AgendaStatusLockedIn: 52 | return "locked in" 53 | case AgendaStatusFailed: 54 | return "failed" 55 | case AgendaStatusFinished: 56 | return "finished" 57 | default: 58 | return "unknown" 59 | } 60 | } 61 | 62 | // AgendaStatusFromStr creates an agenda status from a string. If "UnknownStatus" 63 | // is returned then an invalid status string has been passed. 64 | func AgendaStatusFromStr(status string) AgendaStatusType { 65 | switch strings.ToLower(status) { 66 | case "defined", "upcoming": 67 | return AgendaStatusUpcoming 68 | case "started", "in progress": 69 | return AgendaStatusInProgress 70 | case "failed": 71 | return AgendaStatusFailed 72 | case "lockedin", "locked in": 73 | return AgendaStatusLockedIn 74 | case "active", "finished": 75 | return AgendaStatusFinished 76 | default: 77 | return UnknownStatus 78 | } 79 | } 80 | 81 | // SetVoteChoice sets a voting choice for the specified agenda. If a ticket 82 | // hash is provided, the voting choice is also updated with the VSP controlling 83 | // the ticket. If a ticket hash isn't provided, the vote choice is saved to the 84 | // local wallet database and the VSPs controlling all unspent, unexpired tickets 85 | // are updated to use the specified vote choice. 86 | func (wallet *Wallet) SetVoteChoice(agendaID, choiceID, hash string, passphrase []byte) error { 87 | var ticketHash *chainhash.Hash 88 | if hash != "" { 89 | hash, err := chainhash.NewHashFromStr(hash) 90 | if err != nil { 91 | return fmt.Errorf("inavlid hash: %w", err) 92 | } 93 | ticketHash = hash 94 | } 95 | 96 | // The wallet will need to be unlocked to sign the API 97 | // request(s) for setting this vote choice with the VSP. 98 | err := wallet.UnlockWallet(passphrase) 99 | if err != nil { 100 | return translateError(err) 101 | } 102 | defer wallet.LockWallet() 103 | 104 | ctx := wallet.shutdownContext() 105 | 106 | // get choices 107 | choices, _, err := wallet.Internal().AgendaChoices(ctx, ticketHash) // returns saved prefs for current agendas 108 | if err != nil { 109 | return err 110 | } 111 | 112 | currentChoice := w.AgendaChoice{ 113 | AgendaID: agendaID, 114 | ChoiceID: "abstain", // default to abstain as current choice if not found in wallet 115 | } 116 | 117 | for i := range choices { 118 | if choices[i].AgendaID == agendaID { 119 | currentChoice.ChoiceID = choices[i].ChoiceID 120 | break 121 | } 122 | } 123 | 124 | newChoice := w.AgendaChoice{ 125 | AgendaID: agendaID, 126 | ChoiceID: choiceID, 127 | } 128 | 129 | _, err = wallet.Internal().SetAgendaChoices(ctx, ticketHash, newChoice) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | var vspPreferenceUpdateSuccess bool 135 | defer func() { 136 | if !vspPreferenceUpdateSuccess { 137 | // Updating the agenda voting preference with the vsp failed, 138 | // revert the locally saved voting preference for the agenda. 139 | _, revertError := wallet.Internal().SetAgendaChoices(ctx, ticketHash, currentChoice) 140 | if revertError != nil { 141 | log.Errorf("unable to revert locally saved voting preference: %v", revertError) 142 | } 143 | } 144 | }() 145 | 146 | // If a ticket hash is provided, set the specified vote choice with 147 | // the VSP associated with the provided ticket. Otherwise, set the 148 | // vote choice with the VSPs associated with all "votable" tickets. 149 | ticketHashes := make([]*chainhash.Hash, 0) 150 | if ticketHash != nil { 151 | ticketHashes = append(ticketHashes, ticketHash) 152 | } else { 153 | err = wallet.Internal().ForUnspentUnexpiredTickets(ctx, func(hash *chainhash.Hash) error { 154 | ticketHashes = append(ticketHashes, hash) 155 | return nil 156 | }) 157 | if err != nil { 158 | return fmt.Errorf("unable to fetch hashes for all unspent, unexpired tickets: %v", err) 159 | } 160 | } 161 | 162 | // Never return errors from this for loop, so all tickets are tried. 163 | // The first error will be returned to the caller. 164 | var firstErr error 165 | for _, tHash := range ticketHashes { 166 | vspTicketInfo, err := wallet.Internal().VSPTicketInfo(ctx, tHash) 167 | if err != nil { 168 | // Ignore NotExist error, just means the ticket is not 169 | // registered with a VSP, nothing more to do here. 170 | if firstErr == nil && !errors.Is(err, errors.NotExist) { 171 | firstErr = err 172 | } 173 | continue // try next tHash 174 | } 175 | 176 | // Update the vote choice for the ticket with the associated VSP. 177 | vspClient, err := wallet.VSPClient(vspTicketInfo.Host, vspTicketInfo.PubKey) 178 | if err != nil && firstErr == nil { 179 | firstErr = err 180 | continue // try next tHash 181 | } 182 | err = vspClient.SetVoteChoice(ctx, tHash, []w.AgendaChoice{newChoice}, nil, nil) 183 | if err != nil && firstErr == nil { 184 | firstErr = err 185 | continue // try next tHash 186 | } 187 | } 188 | 189 | vspPreferenceUpdateSuccess = firstErr == nil 190 | return firstErr 191 | } 192 | 193 | // AllVoteAgendas returns all agendas of all stake versions for the active 194 | // network and this version of the software. Also returns any saved vote 195 | // preferences for the agendas of the current stake version. Vote preferences 196 | // for older agendas cannot currently be retrieved. 197 | func (wallet *Wallet) AllVoteAgendas(hash string, newestFirst bool) ([]*Agenda, error) { 198 | if wallet.chainParams.Deployments == nil { 199 | return nil, nil // no agendas to return 200 | } 201 | 202 | var ticketHash *chainhash.Hash 203 | if hash != "" { 204 | hash, err := chainhash.NewHashFromStr(hash) 205 | if err != nil { 206 | return nil, fmt.Errorf("inavlid hash: %w", err) 207 | } 208 | ticketHash = hash 209 | } 210 | 211 | ctx := wallet.shutdownContext() 212 | choices, _, err := wallet.Internal().AgendaChoices(ctx, ticketHash) // returns saved prefs for current agendas 213 | if err != nil { 214 | return nil, err 215 | } 216 | 217 | // Check for all agendas from the intital stake version to the 218 | // current stake version, in order to fetch legacy agendas. 219 | deployments := make([]chaincfg.ConsensusDeployment, 0) 220 | var i uint32 221 | for i = 1; i <= voteVersion(wallet.chainParams); i++ { 222 | deployments = append(deployments, wallet.chainParams.Deployments[i]...) 223 | } 224 | 225 | // Fetch high level agenda detail form dcrdata api. 226 | var dcrdataAgenda []DcrdataAgenda 227 | host := dcrdataAgendasAPIMainnetUrl 228 | if wallet.chainParams.Net == wire.TestNet3 { 229 | host = dcrdataAgendasAPITestnetUrl 230 | } 231 | _, _, err = HttpGet(host, &dcrdataAgenda) 232 | if err != nil { 233 | return nil, err 234 | } 235 | 236 | agendas := make([]*Agenda, len(deployments)) 237 | var status string 238 | for i := range deployments { 239 | d := &deployments[i] 240 | 241 | votingPreference := "abstain" // assume abstain, if we have the saved pref, it'll be updated below 242 | for c := range choices { 243 | if choices[c].AgendaID == d.Vote.Id { 244 | votingPreference = choices[c].ChoiceID 245 | break 246 | } 247 | } 248 | 249 | for j := range dcrdataAgenda { 250 | if dcrdataAgenda[j].Name == d.Vote.Id { 251 | status = AgendaStatusFromStr(dcrdataAgenda[j].Status).String() 252 | } 253 | } 254 | 255 | agendas[i] = &Agenda{ 256 | AgendaID: d.Vote.Id, 257 | Description: d.Vote.Description, 258 | Mask: uint32(d.Vote.Mask), 259 | Choices: d.Vote.Choices, 260 | VotingPreference: votingPreference, 261 | StartTime: int64(d.StartTime), 262 | ExpireTime: int64(d.ExpireTime), 263 | Status: status, 264 | } 265 | } 266 | 267 | if newestFirst { 268 | sort.Slice(agendas, func(i, j int) bool { 269 | return agendas[i].StartTime > agendas[j].StartTime 270 | }) 271 | } 272 | return agendas, nil 273 | } 274 | -------------------------------------------------------------------------------- /dcrlibwallet_suite_test.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet_test 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestDcrlibwallet(t *testing.T) { 12 | RegisterFailHandler(Fail) 13 | rand.Seed(GinkgoRandomSeed()) 14 | RunSpecs(t, "Dcrlibwallet Suite") 15 | } 16 | -------------------------------------------------------------------------------- /decodetx.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "fmt" 5 | 6 | "decred.org/dcrwallet/v2/wallet" 7 | "github.com/decred/dcrd/blockchain/stake/v4" 8 | "github.com/decred/dcrd/chaincfg/v3" 9 | "github.com/decred/dcrd/dcrutil/v4" 10 | "github.com/decred/dcrd/txscript/v4/stdscript" 11 | "github.com/decred/dcrd/wire" 12 | "github.com/decred/dcrdata/v7/txhelpers" 13 | "github.com/planetdecred/dcrlibwallet/txhelper" 14 | ) 15 | 16 | const BlockValid = 1 << 0 17 | 18 | // DecodeTransaction uses `walletTx.Hex` to retrieve detailed information for a transaction. 19 | func (w *Wallet) DecodeTransaction(walletTx *TxInfoFromWallet, netParams *chaincfg.Params) (*Transaction, error) { 20 | msgTx, txFee, txSize, txFeeRate, err := txhelper.MsgTxFeeSizeRate(walletTx.Hex) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | inputs, totalWalletInput, totalWalletUnmixedInputs := w.decodeTxInputs(msgTx, walletTx.Inputs) 26 | outputs, totalWalletOutput, totalWalletMixedOutputs, mixedOutputsCount := w.decodeTxOutputs(msgTx, netParams, walletTx.Outputs) 27 | 28 | amount, direction := txhelper.TransactionAmountAndDirection(totalWalletInput, totalWalletOutput, int64(txFee)) 29 | 30 | ssGenVersion, lastBlockValid, voteBits, ticketSpentHash := voteInfo(msgTx) 31 | 32 | // ticketSpentHash will be empty if this isn't a vote tx 33 | if txhelpers.IsSSRtx(msgTx) { 34 | ticketSpentHash = msgTx.TxIn[0].PreviousOutPoint.Hash.String() 35 | // set first tx input as amount for revoked txs 36 | amount = msgTx.TxIn[0].ValueIn 37 | } else if stake.IsSStx(msgTx) { 38 | // set first tx output as amount for ticket txs 39 | amount = msgTx.TxOut[0].Value 40 | } 41 | 42 | isMixedTx, mixDenom, _ := txhelpers.IsMixTx(msgTx) 43 | 44 | txType := txhelper.FormatTransactionType(wallet.TxTransactionType(msgTx)) 45 | if isMixedTx { 46 | txType = txhelper.TxTypeMixed 47 | 48 | mixChange := totalWalletOutput - totalWalletMixedOutputs 49 | txFee = dcrutil.Amount(totalWalletUnmixedInputs - (totalWalletMixedOutputs + mixChange)) 50 | } 51 | 52 | return &Transaction{ 53 | WalletID: walletTx.WalletID, 54 | Hash: msgTx.TxHash().String(), 55 | Type: txType, 56 | Hex: walletTx.Hex, 57 | Timestamp: walletTx.Timestamp, 58 | BlockHeight: walletTx.BlockHeight, 59 | 60 | MixDenomination: mixDenom, 61 | MixCount: mixedOutputsCount, 62 | 63 | Version: int32(msgTx.Version), 64 | LockTime: int32(msgTx.LockTime), 65 | Expiry: int32(msgTx.Expiry), 66 | Fee: int64(txFee), 67 | FeeRate: int64(txFeeRate), 68 | Size: txSize, 69 | 70 | Direction: direction, 71 | Amount: amount, 72 | Inputs: inputs, 73 | Outputs: outputs, 74 | 75 | VoteVersion: int32(ssGenVersion), 76 | LastBlockValid: lastBlockValid, 77 | VoteBits: voteBits, 78 | TicketSpentHash: ticketSpentHash, 79 | }, nil 80 | } 81 | 82 | func (wallet *Wallet) decodeTxInputs(mtx *wire.MsgTx, walletInputs []*WalletInput) (inputs []*TxInput, totalWalletInputs, totalWalletUnmixedInputs int64) { 83 | inputs = make([]*TxInput, len(mtx.TxIn)) 84 | unmixedAccountNumber := wallet.ReadInt32ConfigValueForKey(AccountMixerUnmixedAccount, -1) 85 | 86 | for i, txIn := range mtx.TxIn { 87 | input := &TxInput{ 88 | PreviousTransactionHash: txIn.PreviousOutPoint.Hash.String(), 89 | PreviousTransactionIndex: int32(txIn.PreviousOutPoint.Index), 90 | PreviousOutpoint: txIn.PreviousOutPoint.String(), 91 | Amount: txIn.ValueIn, 92 | AccountNumber: -1, // correct account number is set below if this is a wallet output 93 | } 94 | 95 | // override account details if this is wallet input 96 | for _, walletInput := range walletInputs { 97 | if walletInput.Index == int32(i) { 98 | input.AccountNumber = walletInput.AccountNumber 99 | break 100 | } 101 | } 102 | 103 | if input.AccountNumber != -1 { 104 | totalWalletInputs += input.Amount 105 | if input.AccountNumber == unmixedAccountNumber { 106 | totalWalletUnmixedInputs += input.Amount 107 | } 108 | } 109 | 110 | inputs[i] = input 111 | } 112 | 113 | return 114 | } 115 | 116 | func (wallet *Wallet) decodeTxOutputs(mtx *wire.MsgTx, netParams *chaincfg.Params, 117 | walletOutputs []*WalletOutput) (outputs []*TxOutput, totalWalletOutput, totalWalletMixedOutputs int64, mixedOutputsCount int32) { 118 | outputs = make([]*TxOutput, len(mtx.TxOut)) 119 | txType := txhelpers.DetermineTxType(mtx, true) 120 | mixedAccountNumber := wallet.ReadInt32ConfigValueForKey(AccountMixerMixedAccount, -1) 121 | 122 | for i, txOut := range mtx.TxOut { 123 | // get address and script type for output 124 | var address, scriptType string 125 | if (txType == stake.TxTypeSStx) && (stake.IsStakeCommitmentTxOut(i)) { 126 | addr, err := stake.AddrFromSStxPkScrCommitment(txOut.PkScript, netParams) 127 | if err == nil { 128 | address = addr.String() 129 | } 130 | scriptType = stdscript.STStakeSubmissionPubKeyHash.String() 131 | } else { 132 | // Ignore the error here since an error means the script 133 | // couldn't parse and there is no additional information 134 | // about it anyways. 135 | scriptClass, addrs := stdscript.ExtractAddrs(txOut.Version, txOut.PkScript, netParams) 136 | if len(addrs) > 0 { 137 | address = addrs[0].String() 138 | } 139 | scriptType = scriptClass.String() 140 | } 141 | 142 | output := &TxOutput{ 143 | Index: int32(i), 144 | Amount: txOut.Value, 145 | Version: int32(txOut.Version), 146 | ScriptType: scriptType, 147 | Address: address, // correct address, account name and number set below if this is a wallet output 148 | AccountNumber: -1, 149 | } 150 | 151 | // override address and account details if this is wallet output 152 | for _, walletOutput := range walletOutputs { 153 | if walletOutput.Index == output.Index { 154 | output.Internal = walletOutput.Internal 155 | output.Address = walletOutput.Address 156 | output.AccountNumber = walletOutput.AccountNumber 157 | break 158 | } 159 | } 160 | 161 | if output.AccountNumber != -1 { 162 | totalWalletOutput += output.Amount 163 | if output.AccountNumber == mixedAccountNumber { 164 | totalWalletMixedOutputs += output.Amount 165 | mixedOutputsCount++ 166 | } 167 | } 168 | 169 | outputs[i] = output 170 | } 171 | 172 | return 173 | } 174 | 175 | func voteInfo(msgTx *wire.MsgTx) (ssGenVersion uint32, lastBlockValid bool, voteBits string, ticketSpentHash string) { 176 | if stake.IsSSGen(msgTx, true) { 177 | ssGenVersion = stake.SSGenVersion(msgTx) 178 | bits := stake.SSGenVoteBits(msgTx) 179 | voteBits = fmt.Sprintf("%#04x", bits) 180 | lastBlockValid = bits&uint16(BlockValid) != 0 181 | ticketSpentHash = msgTx.TxIn[1].PreviousOutPoint.Hash.String() 182 | } 183 | return 184 | } 185 | -------------------------------------------------------------------------------- /dexdcr/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 The Decred developers 2 | // Use of this source code is governed by an ISC 3 | // license that can be found in the LICENSE file. 4 | 5 | package dexdcr 6 | 7 | import ( 8 | "decred.org/dcrdex/client/asset" 9 | ) 10 | 11 | const ( 12 | // defaultFee is the default value for the fallbackfee. 13 | defaultFee = 20 14 | // defaultFeeRateLimit is the default value for the feeratelimit. 15 | defaultFeeRateLimit = 100 16 | // defaultRedeemConfTarget is the default redeem transaction confirmation 17 | // target in blocks used by estimatesmartfee to get the optimal fee for a 18 | // redeem transaction. 19 | defaultRedeemConfTarget = 1 20 | ) 21 | 22 | // DefaultConfigOpts are the general, non-rpc-specific configuration options 23 | // defined in the decred.org/dcrdex/client/asset/dcr package. 24 | var DefaultConfigOpts = []*asset.ConfigOption{ 25 | { 26 | Key: "account", 27 | DisplayName: "Account Name", 28 | Description: "dcrwallet account name", 29 | }, 30 | { 31 | Key: "fallbackfee", 32 | DisplayName: "Fallback fee rate", 33 | Description: "The fee rate to use for fee payment and withdrawals when " + 34 | "estimatesmartfee is not available. Units: DCR/kB", 35 | DefaultValue: defaultFee * 1000 / 1e8, 36 | }, 37 | { 38 | Key: "feeratelimit", 39 | DisplayName: "Highest acceptable fee rate", 40 | Description: "This is the highest network fee rate you are willing to " + 41 | "pay on swap transactions. If feeratelimit is lower than a market's " + 42 | "maxfeerate, you will not be able to trade on that market with this " + 43 | "wallet. Units: DCR/kB", 44 | DefaultValue: defaultFeeRateLimit * 1000 / 1e8, 45 | }, 46 | { 47 | Key: "redeemconftarget", 48 | DisplayName: "Redeem confirmation target", 49 | Description: "The target number of blocks for the redeem transaction " + 50 | "to get a confirmation. Used to set the transaction's fee rate." + 51 | " (default: 1 block)", 52 | DefaultValue: defaultRedeemConfTarget, 53 | }, 54 | { 55 | Key: "txsplit", 56 | DisplayName: "Pre-size funding inputs", 57 | Description: "When placing an order, create a \"split\" transaction to " + 58 | "fund the order without locking more of the wallet balance than " + 59 | "necessary. Otherwise, excess funds may be reserved to fund the order " + 60 | "until the first swap contract is broadcast during match settlement, or " + 61 | "the order is canceled. This an extra transaction for which network " + 62 | "mining fees are paid. Used only for standing-type orders, e.g. " + 63 | "limit orders without immediate time-in-force.", 64 | IsBoolean: true, 65 | }, 66 | } 67 | -------------------------------------------------------------------------------- /dexdcr/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/planetdecred/dcrlibwallet/dexdcr 2 | 3 | require ( 4 | decred.org/dcrdex v0.4.1 5 | decred.org/dcrwallet/v2 v2.0.1 6 | github.com/decred/dcrd/blockchain/stake/v4 v4.0.0 7 | github.com/decred/dcrd/blockchain/standalone/v2 v2.1.0 8 | github.com/decred/dcrd/chaincfg/chainhash v1.0.3 9 | github.com/decred/dcrd/chaincfg/v3 v3.1.1 10 | github.com/decred/dcrd/dcrjson/v4 v4.0.0 11 | github.com/decred/dcrd/dcrutil/v4 v4.0.0 12 | github.com/decred/dcrd/rpc/jsonrpc/types/v3 v3.0.0 13 | github.com/decred/dcrd/txscript/v4 v4.0.0 14 | github.com/decred/dcrd/wire v1.5.0 15 | github.com/decred/slog v1.2.0 16 | ) 17 | 18 | go 1.16 19 | -------------------------------------------------------------------------------- /dexdcr/spvsyncer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 The Decred developers 2 | // Use of this source code is governed by an ISC 3 | // license that can be found in the LICENSE file. 4 | 5 | package dexdcr 6 | 7 | import ( 8 | "fmt" 9 | 10 | "decred.org/dcrwallet/v2/p2p" 11 | ) 12 | 13 | // SpvSyncer defines methods we expect to find in an spv wallet backend. 14 | type SpvSyncer interface { 15 | Synced() bool 16 | EstimateMainChainTip() int32 17 | GetRemotePeers() map[string]*p2p.RemotePeer 18 | } 19 | 20 | // spvSyncer returns the spv syncer connected to the wallet or returns an error 21 | // if the wallet isn't connected to an spv syncer backend. 22 | func (spvw *SpvWallet) spvSyncer() (SpvSyncer, error) { 23 | n, err := spvw.wallet.NetworkBackend() 24 | if err != nil { 25 | return nil, fmt.Errorf("wallet network backend error: %w", err) 26 | } 27 | if spvSyncer, ok := n.(SpvSyncer); ok { 28 | return spvSyncer, nil 29 | } 30 | return nil, fmt.Errorf("wallet is not connected to a supported spv backend") 31 | } 32 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "decred.org/dcrwallet/v2/errors" 5 | "github.com/asdine/storm" 6 | ) 7 | 8 | const ( 9 | // Error Codes 10 | ErrInsufficientBalance = "insufficient_balance" 11 | ErrInvalid = "invalid" 12 | ErrWalletLocked = "wallet_locked" 13 | ErrWalletDatabaseInUse = "wallet_db_in_use" 14 | ErrWalletNotLoaded = "wallet_not_loaded" 15 | ErrWalletNotFound = "wallet_not_found" 16 | ErrWalletNameExist = "wallet_name_exists" 17 | ErrReservedWalletName = "wallet_name_reserved" 18 | ErrWalletIsRestored = "wallet_is_restored" 19 | ErrWalletIsWatchOnly = "watch_only_wallet" 20 | ErrUnusableSeed = "unusable_seed" 21 | ErrPassphraseRequired = "passphrase_required" 22 | ErrInvalidPassphrase = "invalid_passphrase" 23 | ErrNotConnected = "not_connected" 24 | ErrExist = "exists" 25 | ErrNotExist = "not_exists" 26 | ErrEmptySeed = "empty_seed" 27 | ErrInvalidAddress = "invalid_address" 28 | ErrInvalidAuth = "invalid_auth" 29 | ErrUnavailable = "unavailable" 30 | ErrContextCanceled = "context_canceled" 31 | ErrFailedPrecondition = "failed_precondition" 32 | ErrSyncAlreadyInProgress = "sync_already_in_progress" 33 | ErrNoPeers = "no_peers" 34 | ErrInvalidPeers = "invalid_peers" 35 | ErrListenerAlreadyExist = "listener_already_exist" 36 | ErrLoggerAlreadyRegistered = "logger_already_registered" 37 | ErrLogRotatorAlreadyInitialized = "log_rotator_already_initialized" 38 | ErrAddressDiscoveryNotDone = "address_discovery_not_done" 39 | ErrChangingPassphrase = "err_changing_passphrase" 40 | ErrSavingWallet = "err_saving_wallet" 41 | ErrIndexOutOfRange = "err_index_out_of_range" 42 | ErrNoMixableOutput = "err_no_mixable_output" 43 | ErrInvalidVoteBit = "err_invalid_vote_bit" 44 | ) 45 | 46 | // todo, should update this method to translate more error kinds. 47 | func translateError(err error) error { 48 | if err, ok := err.(*errors.Error); ok { 49 | switch err.Kind { 50 | case errors.InsufficientBalance: 51 | return errors.New(ErrInsufficientBalance) 52 | case errors.NotExist, storm.ErrNotFound: 53 | return errors.New(ErrNotExist) 54 | case errors.Passphrase: 55 | return errors.New(ErrInvalidPassphrase) 56 | case errors.NoPeers: 57 | return errors.New(ErrNoPeers) 58 | } 59 | } 60 | return err 61 | } 62 | -------------------------------------------------------------------------------- /ext/httpclient.go: -------------------------------------------------------------------------------- 1 | package ext 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httputil" 10 | "reflect" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | const ( 16 | // Default http client timeout in secs. 17 | defaultHttpClientTimeout = 10 * time.Second 18 | ) 19 | 20 | type ( 21 | // Client is the base for http/https calls 22 | Client struct { 23 | httpClient *http.Client 24 | RequestFilter func(reqConfig *ReqConfig) (req *http.Request, err error) 25 | } 26 | 27 | // ReqConfig models the configuration options for requests. 28 | ReqConfig struct { 29 | payload []byte 30 | method string 31 | url string 32 | retByte bool // if set to true, client.Do will delegate response processing to caller. 33 | } 34 | ) 35 | 36 | // NewClient configures and return a new client 37 | func NewClient() (c *Client) { 38 | t := http.DefaultTransport.(*http.Transport).Clone() 39 | client := &http.Client{ 40 | Timeout: defaultHttpClientTimeout, 41 | Transport: t, 42 | } 43 | 44 | return &Client{ 45 | httpClient: client, 46 | RequestFilter: nil, 47 | } 48 | } 49 | 50 | // Do prepare and process HTTP request to backend resources. 51 | func (c *Client) Do(backend, net string, reqConfig *ReqConfig, response interface{}) (err error) { 52 | c.setBackend(backend, net, reqConfig) 53 | if c.RequestFilter == nil { 54 | return errors.New("Request Filter was not set") 55 | } 56 | 57 | var req *http.Request 58 | req, err = c.RequestFilter(reqConfig) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | if req == nil { 64 | return errors.New("error: nil request") 65 | } 66 | 67 | c.dumpRequest(req) 68 | resp, err := c.httpClient.Do(req) 69 | if err != nil { 70 | return err 71 | } 72 | c.dumpResponse(resp) 73 | 74 | defer resp.Body.Close() 75 | body, err := ioutil.ReadAll(resp.Body) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | if resp.StatusCode != http.StatusOK { 81 | return fmt.Errorf("Error: status: %v resp: %s", resp.Status, body) 82 | } 83 | 84 | // if retByte is option is true. Response from the resource queried 85 | // is not in json format, don't unmarshal return response byte slice to the caller for further processing. 86 | if reqConfig.retByte { 87 | r := reflect.Indirect(reflect.ValueOf(response)) 88 | r.Set(reflect.AppendSlice(r.Slice(0, 0), reflect.ValueOf(body))) 89 | return nil 90 | } 91 | 92 | err = json.Unmarshal(body, response) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (c *Client) dumpRequest(r *http.Request) { 101 | if r == nil { 102 | log.Debug("dumpReq ok: ") 103 | return 104 | } 105 | dump, err := httputil.DumpRequest(r, true) 106 | if err != nil { 107 | log.Debug("dumpReq err: %v", err) 108 | } else { 109 | log.Debug("dumpReq ok: %v", string(dump)) 110 | } 111 | } 112 | 113 | func (c *Client) dumpResponse(r *http.Response) { 114 | if r == nil { 115 | log.Debug("dumpResponse ok: ") 116 | return 117 | } 118 | dump, err := httputil.DumpResponse(r, true) 119 | if err != nil { 120 | log.Debug("dumpResponse err: %v", err) 121 | } else { 122 | log.Debug("dumpResponse ok: %v", string(dump)) 123 | } 124 | } 125 | 126 | // Setbackend sets the appropriate URL scheme and authority for the backend resource. 127 | func (c *Client) setBackend(backend string, net string, reqConfig *ReqConfig) { 128 | // Check if URL scheme and authority is already set. 129 | if strings.HasPrefix(reqConfig.url, "http") { 130 | return 131 | } 132 | 133 | // Prepend URL sheme and authority to the URL. 134 | if authority, ok := backendUrl[net][backend]; ok { 135 | reqConfig.url = fmt.Sprintf("%s%s", authority, reqConfig.url) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /ext/log.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013-2021 The btcsuite developers 2 | // Use of this source code is governed by an ISC 3 | // license that can be found in the LICENSE file. 4 | 5 | package ext 6 | 7 | import "github.com/decred/slog" 8 | 9 | var log = slog.Disabled 10 | 11 | // UseLogger uses a specified Logger to output package logging info. 12 | func UseLogger(logger slog.Logger) { 13 | log = logger 14 | } 15 | -------------------------------------------------------------------------------- /ext/types.go: -------------------------------------------------------------------------------- 1 | package ext 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/decred/dcrdata/v7/api/types" 7 | ) 8 | 9 | type ( 10 | // BlockDataBasic models primary information about a block. 11 | BlockDataBasic struct { 12 | Height uint32 `json:"height"` 13 | Size uint32 `json:"size"` 14 | Hash string `json:"hash"` 15 | Difficulty float64 `json:"diff"` 16 | StakeDiff float64 `json:"sdiff"` 17 | Time types.TimeAPI `json:"time"` 18 | NumTx uint32 `json:"txlength"` 19 | MiningFee *int64 `json:"fees,omitempty"` 20 | TotalSent *int64 `json:"total_sent,omitempty"` 21 | // TicketPoolInfo may be nil for side chain blocks. 22 | PoolInfo *types.TicketPoolInfo `json:"ticket_pool,omitempty"` 23 | } 24 | 25 | // TreasuryDetails is the current balance, spent amount, and tx count for the 26 | // treasury. 27 | TreasuryDetails struct { 28 | Height int64 `json:"height"` 29 | MaturityHeight int64 `json:"maturity_height"` 30 | Balance int64 `json:"balance"` 31 | TxCount int64 `json:"output_count"` 32 | AddCount int64 `json:"add_count"` 33 | Added int64 `json:"added"` 34 | SpendCount int64 `json:"spend_count"` 35 | Spent int64 `json:"spent"` 36 | TBaseCount int64 `json:"tbase_count"` 37 | TBase int64 `json:"tbase"` 38 | ImmatureCount int64 `json:"immature_count"` 39 | Immature int64 `json:"immature"` 40 | } 41 | 42 | // BaseState are the non-iterable fields of the ExchangeState, which embeds 43 | // BaseState. 44 | BaseState struct { 45 | Price float64 `json:"price"` 46 | // BaseVolume is poorly named. This is the volume in terms of (usually) BTC, 47 | // not the base asset of any particular market. 48 | BaseVolume float64 `json:"base_volume,omitempty"` 49 | Volume float64 `json:"volume,omitempty"` 50 | Change float64 `json:"change,omitempty"` 51 | Stamp int64 `json:"timestamp,omitempty"` 52 | } 53 | 54 | // ExchangeRates is the dcr and btc prices converted to fiat. 55 | ExchangeRates struct { 56 | BtcIndex string `json:"btcIndex"` 57 | DcrPrice float64 `json:"dcrPrice"` 58 | BtcPrice float64 `json:"btcPrice"` 59 | Exchanges map[string]BaseState `json:"exchanges"` 60 | } 61 | // ExchangeState models the dcrdata supported exchanges state. 62 | ExchangeState struct { 63 | BtcIndex string `json:"btc_index"` 64 | BtcPrice float64 `json:"btc_fiat_price"` 65 | Price float64 `json:"price"` 66 | Volume float64 `json:"volume"` 67 | DcrBtc map[string]*ExchangeState `json:"dcr_btc_exchanges"` 68 | FiatIndices map[string]*ExchangeState `json:"btc_indices"` 69 | } 70 | 71 | // AddressState models the adddress balances and transactions. 72 | AddressState struct { 73 | Address string `json:"address"` 74 | Balance int64 `json:"balance,string"` 75 | TotalReceived int64 `json:"totalReceived,string"` 76 | TotalSent int64 `json:"totalSent,string"` 77 | UnconfirmedBalance int64 `json:"unconfirmedBalance,string"` 78 | UnconfirmedTxs int64 `json:"unconfirmedTxs"` 79 | Txs int32 `json:"txs"` 80 | TxIds []string `json:"txids"` 81 | } 82 | 83 | // XpubAddress models data about a specific xpub token. 84 | XpubAddress struct { 85 | Address string `json:"name"` 86 | Path string `json:"path"` 87 | Transfers int32 `json:"transfers"` 88 | Decimals int32 `json:"decimals"` 89 | Balance int64 `json:"balance,string"` 90 | TotalReceived int64 `json:"totalReceived,string"` 91 | TotalSent int64 `json:"totalSent,string"` 92 | } 93 | 94 | // XpubBalAndTxs models xpub transactions and balance. 95 | XpubBalAndTxs struct { 96 | Xpub string `json:"address"` 97 | Balance int64 `json:"balance,string"` 98 | TotalReceived int64 `json:"totalReceived,string"` 99 | TotalSent int64 `json:"totalSent,string"` 100 | UnconfirmedBalance int64 `json:"unconfirmedBalance,string"` 101 | UnconfirmedTxs int64 `json:"unconfirmedTxs"` 102 | Txs int32 `json:"txs"` 103 | TxIds []string `json:"txids"` 104 | UsedTokens int32 `json:"usedTokens"` 105 | XpubAddress []XpubAddress `json:"tokens"` 106 | } 107 | // Ticker is the generic ticker information that is returned 108 | // to a caller of GetTiker function. 109 | Ticker struct { 110 | Exchange string 111 | Symbol string 112 | LastTradePrice float64 113 | BidPrice float64 114 | AskPrice float64 115 | } 116 | // BittrexTicker models bittrex specific ticker information. 117 | BittrexTicker struct { 118 | Symbol string `json:"symbol"` 119 | LastTradeRate float64 `json:"lastTradeRate,string"` 120 | Bid float64 `json:"bidRate,string"` 121 | Ask float64 `json:"askRate,string"` 122 | } 123 | // BinanceTicker models binance specific ticker information. 124 | BinanceTicker struct { 125 | AskPrice float64 `json:"askPrice,string"` 126 | AskQty float64 `json:"askQty,string"` 127 | BidPrice float64 `json:"bidPrice,string"` 128 | BidQty float64 `json:"bidQty,string"` 129 | CloseTime int `json:"closeTime"` 130 | Count int `json:"count"` 131 | FirstID int `json:"firstId"` 132 | HighPrice float64 `json:"highPrice,string"` 133 | LastID int `json:"lastId"` 134 | LastPrice float64 `json:"lastPrice,string"` 135 | LastQty float64 `json:"lastQty,string"` 136 | LowPrice float64 `json:"lowPrice,string"` 137 | OpenPrice float64 `json:"openPrice,string"` 138 | OpenTime int `json:"openTime"` 139 | PrevClosePrice float64 `json:"prevClosePrice,string"` 140 | PriceChange float64 `json:"priceChange,string"` 141 | PriceChangePercent float64 `json:"priceChangePercent,string"` 142 | QuoteVolume float64 `json:"quoteVolume,string"` 143 | Symbol string `json:"symbol"` 144 | Volume float64 `json:"volume,string"` 145 | WeightedAvgPrice float64 `json:"weightedAvgPrice,string"` 146 | } 147 | // KuCoinTicker models Kucoin's specific ticker information. 148 | KuCoinTicker struct { 149 | Code int `json:"code,string"` 150 | Data struct { 151 | Time int64 `json:"time"` 152 | Sequence int64 `json:"sequence,string"` 153 | Price float64 `json:"price,string"` 154 | Size float64 `json:"size,string"` 155 | BestBid float64 `json:"bestBid,string"` 156 | BestBidSize float64 `json:"bestBidSize,string"` 157 | BestAsk float64 `json:"bestAsk,string"` 158 | BestAskSize float64 `json:"bestAskSize,string"` 159 | } `json:"data"` 160 | } 161 | 162 | // AgendaAPIResponse holds two sets of AgendaVoteChoices charts data. 163 | AgendaAPIResponse struct { 164 | ByHeight *AgendaVoteChoices `json:"by_height"` 165 | ByTime *AgendaVoteChoices `json:"by_time"` 166 | } 167 | 168 | // AgendaVoteChoices contains the vote counts on multiple intervals of time. The 169 | // interval length may be either a single block, in which case Height contains 170 | // the block heights, or a day, in which case Time contains the time stamps of 171 | // each interval. Total is always the sum of Yes, No, and Abstain. 172 | AgendaVoteChoices struct { 173 | Abstain []uint64 `json:"abstain"` 174 | Yes []uint64 `json:"yes"` 175 | No []uint64 `json:"no"` 176 | Total []uint64 `json:"total"` 177 | Height []uint64 `json:"height,omitempty"` 178 | Time []time.Time `json:"time,omitempty"` 179 | } 180 | ) 181 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/planetdecred/dcrlibwallet 2 | 3 | require ( 4 | decred.org/dcrdex v0.4.1 5 | decred.org/dcrwallet/v2 v2.0.3-0.20220808190744-3b3e9e04b3c2 6 | github.com/asdine/storm v0.0.0-20190216191021-fe89819f6282 7 | github.com/decred/dcrd/addrmgr/v2 v2.0.0 8 | github.com/decred/dcrd/blockchain/stake/v4 v4.0.0 9 | github.com/decred/dcrd/chaincfg/chainhash v1.0.3 10 | github.com/decred/dcrd/chaincfg/v3 v3.1.1 11 | github.com/decred/dcrd/connmgr/v3 v3.1.0 12 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 13 | github.com/decred/dcrd/dcrutil/v4 v4.0.0 14 | github.com/decred/dcrd/gcs/v3 v3.0.0 15 | github.com/decred/dcrd/hdkeychain/v3 v3.1.0 16 | github.com/decred/dcrd/rpc/jsonrpc/types/v3 v3.0.0 17 | github.com/decred/dcrd/txscript/v4 v4.0.0 18 | github.com/decred/dcrd/wire v1.5.0 19 | github.com/decred/dcrdata/v7 v7.0.0-20211216152310-365c9dc820eb 20 | github.com/decred/politeia v1.3.1 21 | github.com/decred/slog v1.2.0 22 | github.com/dgraph-io/badger v1.6.2 23 | github.com/jrick/logrotate v1.0.0 24 | github.com/kevinburke/nacl v0.0.0-20190829012316-f3ed23dbd7f8 25 | github.com/onsi/ginkgo v1.14.0 26 | github.com/onsi/gomega v1.10.1 27 | github.com/planetdecred/dcrlibwallet/dexdcr v0.0.0-20220223161805-c736f970653d 28 | go.etcd.io/bbolt v1.3.6 29 | golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 30 | golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde 31 | ) 32 | 33 | require ( 34 | decred.org/cspp/v2 v2.0.0 // indirect 35 | decred.org/dcrwallet v1.7.0 // indirect 36 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect 37 | github.com/DataDog/zstd v1.4.8 // indirect 38 | github.com/aead/siphash v1.0.1 // indirect 39 | github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect 40 | github.com/btcsuite/btcd v0.22.0-beta.0.20211026140004-31791ba4dc6e // indirect 41 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect 42 | github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890 // indirect 43 | github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce // indirect 44 | github.com/btcsuite/btcwallet v0.12.0 // indirect 45 | github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0 // indirect 46 | github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 // indirect 47 | github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 // indirect 48 | github.com/btcsuite/btcwallet/walletdb v1.4.0 // indirect 49 | github.com/btcsuite/btcwallet/wtxmgr v1.3.0 // indirect 50 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect 51 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect 52 | github.com/cespare/xxhash v1.1.0 // indirect 53 | github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a // indirect 54 | github.com/davecgh/go-spew v1.1.1 // indirect 55 | github.com/dchest/siphash v1.2.3 // indirect 56 | github.com/decred/base58 v1.0.4 // indirect 57 | github.com/decred/dcrd/blockchain/stake/v3 v3.0.0 // indirect 58 | github.com/decred/dcrd/blockchain/standalone/v2 v2.1.0 // indirect 59 | github.com/decred/dcrd/blockchain/v4 v4.0.2 // indirect 60 | github.com/decred/dcrd/certgen v1.1.1 // indirect 61 | github.com/decred/dcrd/crypto/blake256 v1.0.1-0.20200921185235-6d75c7ec1199 // indirect 62 | github.com/decred/dcrd/crypto/ripemd160 v1.0.1 // indirect 63 | github.com/decred/dcrd/database/v2 v2.0.2 // indirect 64 | github.com/decred/dcrd/database/v3 v3.0.0 // indirect 65 | github.com/decred/dcrd/dcrec v1.0.1-0.20200921185235-6d75c7ec1199 // indirect 66 | github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2 // indirect 67 | github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 // indirect 68 | github.com/decred/dcrd/dcrjson/v4 v4.0.0 // indirect 69 | github.com/decred/dcrd/dcrutil/v3 v3.0.0 // indirect 70 | github.com/decred/dcrd/gcs/v2 v2.1.0 // indirect 71 | github.com/decred/dcrd/lru v1.1.1 // indirect 72 | github.com/decred/dcrd/rpcclient/v7 v7.0.0 // indirect 73 | github.com/decred/dcrd/txscript/v3 v3.0.0 // indirect 74 | github.com/decred/dcrtime v0.0.0-20191018193024-8d8b4ef0458e // indirect 75 | github.com/decred/go-socks v1.1.0 // indirect 76 | github.com/dgraph-io/ristretto v0.0.2 // indirect 77 | github.com/dustin/go-humanize v1.0.0 // indirect 78 | github.com/fsnotify/fsnotify v1.4.9 // indirect 79 | github.com/go-chi/chi/v5 v5.0.1 // indirect 80 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect 81 | github.com/golang/protobuf v1.5.2 // indirect 82 | github.com/golang/snappy v0.0.4 // indirect 83 | github.com/google/trillian v1.3.13 // indirect 84 | github.com/google/uuid v1.1.5 // indirect 85 | github.com/gorilla/schema v1.1.0 // indirect 86 | github.com/gorilla/websocket v1.5.0 // indirect 87 | github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c // indirect 88 | github.com/jrick/bitset v1.0.0 // indirect 89 | github.com/jrick/wsrpc/v2 v2.3.5 // indirect 90 | github.com/kkdai/bstream v1.0.0 // indirect 91 | github.com/lib/pq v1.10.3 // indirect 92 | github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect 93 | github.com/lightninglabs/neutrino v0.13.1-0.20211214231330-53b628ce1756 // indirect 94 | github.com/lightningnetwork/lnd/clock v1.0.1 // indirect 95 | github.com/lightningnetwork/lnd/queue v1.0.1 // indirect 96 | github.com/lightningnetwork/lnd/ticker v1.0.0 // indirect 97 | github.com/marcopeereboom/sbox v1.1.0 // indirect 98 | github.com/nxadm/tail v1.4.4 // indirect 99 | github.com/pkg/errors v0.9.1 // indirect 100 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect 101 | golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c // indirect 102 | golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect 103 | golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect 104 | golang.org/x/text v0.3.7 // indirect 105 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect 106 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 107 | google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect 108 | google.golang.org/grpc v1.48.0 // indirect 109 | google.golang.org/protobuf v1.28.1 // indirect 110 | gopkg.in/ini.v1 v1.55.0 // indirect 111 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 112 | gopkg.in/yaml.v2 v2.4.0 // indirect 113 | ) 114 | 115 | // Older versions of github.com/lib/pq are required by politeia (v1.9.0) 116 | // and dcrdex (v1.10.3) but only v1.10.4 and above can be compiled for 117 | // the android OS using gomobile. This replace can be removed once any 118 | // of those projects update their github.com/lib/pq dependency. 119 | replace github.com/lib/pq => github.com/lib/pq v1.10.4 120 | 121 | go 1.17 122 | -------------------------------------------------------------------------------- /internal/certs/cspp.go: -------------------------------------------------------------------------------- 1 | package certs 2 | 3 | const CSPP = `-----BEGIN CERTIFICATE----- 4 | MIICFjCCAbugAwIBAgIRALybxefeCAo7bRNPQCShItowCgYIKoZIzj0EAwIwKjEP 5 | MA0GA1UEChMGRGVjcmVkMRcwFQYDVQQDEw5taXguZGVjcmVkLm9yZzAeFw0yMTEy 6 | MTMyMjI2NTRaFw0zMTEyMTIyMjI2NTRaMCoxDzANBgNVBAoTBkRlY3JlZDEXMBUG 7 | A1UEAxMObWl4LmRlY3JlZC5vcmcwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQK 8 | hX0D+UJnK51uIgJ3Fay5YPza8W0DY589srMuB4Gy6xSC/oUz1xbnMaUH+IEuxF1q 9 | le1wNMHLnuBhXOUuj8U3o4HBMIG+MA4GA1UdDwEB/wQEAwIChDAPBgNVHRMBAf8E 10 | BTADAQH/MB0GA1UdDgQWBBRG+HHZ+ksuzroDQ7rbcY+JMLsa5jB8BgNVHREEdTBz 11 | gg5taXguZGVjcmVkLm9yZ4I+cnRsaHBobmNwcXZ5eGJ3eHp0Z28ybDc2cjduZHo3 12 | b3puajV2dWhhdjRpd3dvajJ3azRib3E1eWQub25pb26CCWxvY2FsaG9zdIcEfwAA 13 | AYcQAAAAAAAAAAAAAAAAAAAAATAKBggqhkjOPQQDAgNJADBGAiEAmt6TPKT+frCw 14 | i05K3CCZTIoiZJSlfYDwfi4uHDV3T8MCIQCoK73gLGdqMdZGPTfi8JhlGtE7sHea 15 | RE9/S0ootEuw9g== 16 | -----END CERTIFICATE----- 17 | ` 18 | -------------------------------------------------------------------------------- /internal/loader/log.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 The btcsuite developers 2 | // Copyright (c) 2017-2018 The Decred developers 3 | // Use of this source code is governed by an ISC 4 | // license that can be found in the LICENSE file. 5 | 6 | package loader 7 | 8 | import "github.com/decred/slog" 9 | 10 | // log is a logger that is initialized with no output filters. This 11 | // means the package will not perform any logging by default until the caller 12 | // requests it. 13 | var log = slog.Disabled 14 | 15 | // UseLogger uses a specified Logger to output package logging info. 16 | // This should be used in preference to SetLogWriter if the caller is also 17 | // using slog. 18 | func UseLogger(logger slog.Logger) { 19 | log = logger 20 | } 21 | -------------------------------------------------------------------------------- /internal/politeia/errors.go: -------------------------------------------------------------------------------- 1 | package politeia 2 | 3 | import ( 4 | "decred.org/dcrwallet/v2/errors" 5 | "github.com/asdine/storm" 6 | ) 7 | 8 | const ( 9 | ErrInsufficientBalance = "insufficient_balance" 10 | ErrSyncAlreadyInProgress = "sync_already_in_progress" 11 | ErrNotExist = "not_exists" 12 | ErrInvalid = "invalid" 13 | ErrListenerAlreadyExist = "listener_already_exist" 14 | ErrInvalidAddress = "invalid_address" 15 | ErrInvalidPassphrase = "invalid_passphrase" 16 | ErrNoPeers = "no_peers" 17 | ) 18 | 19 | func translateError(err error) error { 20 | if err, ok := err.(*errors.Error); ok { 21 | switch err.Kind { 22 | case errors.InsufficientBalance: 23 | return errors.New(ErrInsufficientBalance) 24 | case errors.NotExist, storm.ErrNotFound: 25 | return errors.New(ErrNotExist) 26 | case errors.Passphrase: 27 | return errors.New(ErrInvalidPassphrase) 28 | case errors.NoPeers: 29 | return errors.New(ErrNoPeers) 30 | } 31 | } 32 | return err 33 | } 34 | -------------------------------------------------------------------------------- /internal/politeia/log.go: -------------------------------------------------------------------------------- 1 | package politeia 2 | 3 | import "github.com/decred/slog" 4 | 5 | // log is a logger that is initialized with no output filters. This 6 | // means the package will not perform any logging by default until the caller 7 | // requests it. 8 | var log = slog.Disabled 9 | 10 | // UseLogger uses a specified Logger to output package logging info. 11 | // This should be used in preference to SetLogWriter if the caller is also 12 | // using slog. 13 | func UseLogger(logger slog.Logger) { 14 | log = logger 15 | } 16 | -------------------------------------------------------------------------------- /internal/politeia/politeia.go: -------------------------------------------------------------------------------- 1 | package politeia 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "sync" 8 | 9 | "decred.org/dcrwallet/v2/errors" 10 | "github.com/asdine/storm" 11 | "github.com/asdine/storm/q" 12 | ) 13 | 14 | const ( 15 | PoliteiaMainnetHost = "https://proposals.decred.org/api" 16 | PoliteiaTestnetHost = "https://test-proposals.decred.org/api" 17 | 18 | configDBBkt = "politeia_config" 19 | LastSyncedTimestampConfigKey = "politeia_last_synced_timestamp" 20 | ) 21 | 22 | type Politeia struct { 23 | host string 24 | db *storm.DB 25 | 26 | // TODO: Check usages of mu, seems not to always be unlocked. 27 | mu sync.RWMutex 28 | ctx context.Context 29 | cancelSync context.CancelFunc 30 | client *politeiaClient 31 | 32 | notificationListenersMu sync.RWMutex 33 | notificationListeners map[string]ProposalNotificationListener 34 | } 35 | 36 | const ( 37 | ProposalCategoryAll int32 = iota + 1 38 | ProposalCategoryPre 39 | ProposalCategoryActive 40 | ProposalCategoryApproved 41 | ProposalCategoryRejected 42 | ProposalCategoryAbandoned 43 | ) 44 | 45 | func New(host string, db *storm.DB) (*Politeia, error) { 46 | if err := db.Init(&Proposal{}); err != nil { 47 | log.Errorf("Error initializing politeia database: %s", err.Error()) 48 | return nil, err 49 | } 50 | 51 | return &Politeia{ 52 | host: host, 53 | db: db, 54 | notificationListeners: make(map[string]ProposalNotificationListener), 55 | }, nil 56 | } 57 | 58 | func (p *Politeia) saveLastSyncedTimestamp(lastSyncedTimestamp int64) { 59 | err := p.db.Set(configDBBkt, LastSyncedTimestampConfigKey, lastSyncedTimestamp) 60 | if err != nil { 61 | log.Errorf("error setting config value for key: %s, error: %v", LastSyncedTimestampConfigKey, err) 62 | } 63 | } 64 | 65 | func (p *Politeia) getLastSyncedTimestamp() (lastSyncedTimestamp int64) { 66 | err := p.db.Get(configDBBkt, LastSyncedTimestampConfigKey, lastSyncedTimestamp) 67 | if err != nil && err != storm.ErrNotFound { 68 | log.Errorf("error reading config value for key: %s, error: %v", LastSyncedTimestampConfigKey, err) 69 | } 70 | return lastSyncedTimestamp 71 | } 72 | 73 | func (p *Politeia) saveOrOverwiteProposal(proposal *Proposal) error { 74 | var oldProposal Proposal 75 | err := p.db.One("Token", proposal.Token, &oldProposal) 76 | if err != nil && err != storm.ErrNotFound { 77 | return errors.Errorf("error checking if proposal was already indexed: %s", err.Error()) 78 | } 79 | 80 | if oldProposal.Token != "" { 81 | // delete old record before saving new (if it exists) 82 | p.db.DeleteStruct(oldProposal) 83 | } 84 | 85 | return p.db.Save(proposal) 86 | } 87 | 88 | // GetProposalsRaw fetches and returns a proposals from the db 89 | func (p *Politeia) GetProposalsRaw(category int32, offset, limit int32, newestFirst bool) ([]Proposal, error) { 90 | return p.getProposalsRaw(category, offset, limit, newestFirst, false) 91 | } 92 | 93 | func (p *Politeia) getProposalsRaw(category int32, offset, limit int32, newestFirst bool, skipAbandoned bool) ([]Proposal, error) { 94 | 95 | var query storm.Query 96 | switch category { 97 | case ProposalCategoryAll: 98 | 99 | if skipAbandoned { 100 | query = p.db.Select( 101 | q.Not(q.Eq("Category", ProposalCategoryAbandoned)), 102 | ) 103 | } else { 104 | query = p.db.Select( 105 | q.True(), 106 | ) 107 | } 108 | default: 109 | query = p.db.Select( 110 | q.Eq("Category", category), 111 | ) 112 | } 113 | 114 | if offset > 0 { 115 | query = query.Skip(int(offset)) 116 | } 117 | 118 | if limit > 0 { 119 | query = query.Limit(int(limit)) 120 | } 121 | 122 | if newestFirst { 123 | query = query.OrderBy("PublishedAt").Reverse() 124 | } else { 125 | query = query.OrderBy("PublishedAt") 126 | } 127 | 128 | var proposals []Proposal 129 | err := query.Find(&proposals) 130 | if err != nil && err != storm.ErrNotFound { 131 | return nil, fmt.Errorf("error fetching proposals: %s", err.Error()) 132 | } 133 | 134 | return proposals, nil 135 | } 136 | 137 | // GetProposals returns the result of GetProposalsRaw as a JSON string 138 | func (p *Politeia) GetProposals(category int32, offset, limit int32, newestFirst bool) (string, error) { 139 | 140 | result, err := p.GetProposalsRaw(category, offset, limit, newestFirst) 141 | if err != nil { 142 | return "", err 143 | } 144 | 145 | if len(result) == 0 { 146 | return "[]", nil 147 | } 148 | 149 | response, err := json.Marshal(result) 150 | if err != nil { 151 | return "", fmt.Errorf("error marshalling result: %s", err.Error()) 152 | } 153 | 154 | return string(response), nil 155 | } 156 | 157 | // GetProposalRaw fetches and returns a single proposal specified by it's censorship record token 158 | func (p *Politeia) GetProposalRaw(censorshipToken string) (*Proposal, error) { 159 | var proposal Proposal 160 | err := p.db.One("Token", censorshipToken, &proposal) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | return &proposal, nil 166 | } 167 | 168 | // GetProposal returns the result of GetProposalRaw as a JSON string 169 | func (p *Politeia) GetProposal(censorshipToken string) (string, error) { 170 | return p.marshalResult(p.GetProposalRaw(censorshipToken)) 171 | } 172 | 173 | // GetProposalByIDRaw fetches and returns a single proposal specified by it's ID 174 | func (p *Politeia) GetProposalByIDRaw(proposalID int) (*Proposal, error) { 175 | var proposal Proposal 176 | err := p.db.One("ID", proposalID, &proposal) 177 | if err != nil { 178 | return nil, err 179 | } 180 | 181 | return &proposal, nil 182 | } 183 | 184 | // GetProposalByID returns the result of GetProposalByIDRaw as a JSON string 185 | func (p *Politeia) GetProposalByID(proposalID int) (string, error) { 186 | return p.marshalResult(p.GetProposalByIDRaw(proposalID)) 187 | } 188 | 189 | // Count returns the number of proposals of a specified category 190 | func (p *Politeia) Count(category int32) (int32, error) { 191 | var matcher q.Matcher 192 | 193 | if category == ProposalCategoryAll { 194 | matcher = q.True() 195 | } else { 196 | matcher = q.Eq("Category", category) 197 | } 198 | 199 | count, err := p.db.Select(matcher).Count(&Proposal{}) 200 | if err != nil { 201 | return 0, err 202 | } 203 | 204 | return int32(count), nil 205 | } 206 | 207 | func (p *Politeia) Overview() (*ProposalOverview, error) { 208 | 209 | pre, err := p.Count(ProposalCategoryPre) 210 | if err != nil { 211 | return nil, err 212 | } 213 | 214 | active, err := p.Count(ProposalCategoryActive) 215 | if err != nil { 216 | return nil, err 217 | } 218 | 219 | approved, err := p.Count(ProposalCategoryApproved) 220 | if err != nil { 221 | return nil, err 222 | } 223 | 224 | rejected, err := p.Count(ProposalCategoryRejected) 225 | if err != nil { 226 | return nil, err 227 | } 228 | 229 | abandoned, err := p.Count(ProposalCategoryApproved) 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | return &ProposalOverview{ 235 | All: pre + active + approved + rejected + abandoned, 236 | Discussion: pre, 237 | Voting: active, 238 | Approved: approved, 239 | Rejected: rejected, 240 | Abandoned: abandoned, 241 | }, nil 242 | } 243 | 244 | func (p *Politeia) ClearSavedProposals() error { 245 | err := p.db.Drop(&Proposal{}) 246 | if err != nil { 247 | return translateError(err) 248 | } 249 | 250 | return p.db.Init(&Proposal{}) 251 | } 252 | 253 | func (p *Politeia) marshalResult(result interface{}, err error) (string, error) { 254 | 255 | if err != nil { 256 | return "", translateError(err) 257 | } 258 | 259 | response, err := json.Marshal(result) 260 | if err != nil { 261 | return "", fmt.Errorf("error marshalling result: %s", err.Error()) 262 | } 263 | 264 | return string(response), nil 265 | } 266 | -------------------------------------------------------------------------------- /internal/politeia/politeia_client.go: -------------------------------------------------------------------------------- 1 | package politeia 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | "time" 13 | 14 | tkv1 "github.com/decred/politeia/politeiawww/api/ticketvote/v1" 15 | www "github.com/decred/politeia/politeiawww/api/www/v1" 16 | "github.com/decred/politeia/politeiawww/client" 17 | ) 18 | 19 | type politeiaClient struct { 20 | host string 21 | httpClient *http.Client 22 | 23 | version *www.VersionReply 24 | policy *www.PolicyReply 25 | cookies []*http.Cookie 26 | } 27 | 28 | const ( 29 | ticketVoteApi = tkv1.APIRoute 30 | proposalDetailsPath = "/proposals/" 31 | ) 32 | 33 | var apiPath = www.PoliteiaWWWAPIRoute 34 | 35 | func newPoliteiaClient(host string) *politeiaClient { 36 | tr := &http.Transport{ 37 | TLSClientConfig: &tls.Config{ 38 | InsecureSkipVerify: true, 39 | }, 40 | } 41 | 42 | httpClient := &http.Client{ 43 | Transport: tr, 44 | Timeout: time.Second * 60, 45 | } 46 | 47 | return &politeiaClient{ 48 | host: host, 49 | httpClient: httpClient, 50 | } 51 | } 52 | 53 | func (p *Politeia) getClient() (*politeiaClient, error) { 54 | p.mu.Lock() 55 | defer p.mu.Unlock() 56 | client := p.client 57 | if client == nil { 58 | client = newPoliteiaClient(p.host) 59 | version, err := client.serverVersion() 60 | if err != nil { 61 | return nil, err 62 | } 63 | client.version = &version 64 | 65 | err = client.loadServerPolicy() 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | p.client = client 71 | } 72 | 73 | return client, nil 74 | } 75 | 76 | func (c *politeiaClient) getRequestBody(method string, body interface{}) ([]byte, error) { 77 | if body == nil { 78 | return nil, nil 79 | } 80 | 81 | if method == http.MethodPost { 82 | if requestBody, ok := body.([]byte); ok { 83 | return requestBody, nil 84 | } 85 | } else if method == http.MethodGet { 86 | if requestBody, ok := body.(map[string]string); ok { 87 | params := url.Values{} 88 | for key, val := range requestBody { 89 | params.Add(key, val) 90 | } 91 | return []byte(params.Encode()), nil 92 | } 93 | } 94 | 95 | return nil, errors.New("invalid request body") 96 | } 97 | 98 | func (c *politeiaClient) makeRequest(method, apiRoute, path string, body interface{}, dest interface{}) error { 99 | var err error 100 | var requestBody []byte 101 | 102 | route := c.host + apiRoute + path 103 | if body != nil { 104 | requestBody, err = c.getRequestBody(method, body) 105 | if err != nil { 106 | return err 107 | } 108 | } 109 | 110 | if method == http.MethodGet && requestBody != nil { 111 | route += string(requestBody) 112 | } 113 | 114 | // Create http request 115 | req, err := http.NewRequest(method, route, nil) 116 | if err != nil { 117 | return fmt.Errorf("error creating http request: %s", err.Error()) 118 | } 119 | if method == http.MethodPost && requestBody != nil { 120 | req.Body = ioutil.NopCloser(bytes.NewReader(requestBody)) 121 | } 122 | 123 | for _, cookie := range c.cookies { 124 | req.AddCookie(cookie) 125 | } 126 | 127 | // Send request 128 | r, err := c.httpClient.Do(req) 129 | if err != nil { 130 | return err 131 | } 132 | defer func() { 133 | r.Body.Close() 134 | }() 135 | 136 | responseBody, err := ioutil.ReadAll(r.Body) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | if r.StatusCode != http.StatusOK { 142 | return c.handleError(r.StatusCode, responseBody) 143 | } 144 | 145 | err = json.Unmarshal(responseBody, dest) 146 | if err != nil { 147 | return fmt.Errorf("error unmarshaling response: %s", err.Error()) 148 | } 149 | 150 | return nil 151 | } 152 | 153 | func (c *politeiaClient) handleError(statusCode int, responseBody []byte) error { 154 | switch statusCode { 155 | case http.StatusNotFound: 156 | return errors.New("resource not found") 157 | case http.StatusInternalServerError: 158 | return errors.New("internal server error") 159 | case http.StatusForbidden: 160 | return errors.New(string(responseBody)) 161 | case http.StatusUnauthorized: 162 | var errResp www.ErrorReply 163 | if err := json.Unmarshal(responseBody, &errResp); err != nil { 164 | return err 165 | } 166 | return fmt.Errorf("unauthorized: %d", errResp.ErrorCode) 167 | case http.StatusBadRequest: 168 | var errResp www.ErrorReply 169 | if err := json.Unmarshal(responseBody, &errResp); err != nil { 170 | return err 171 | } 172 | return fmt.Errorf("bad request: %d", errResp.ErrorCode) 173 | } 174 | 175 | return errors.New("unknown error") 176 | } 177 | 178 | func (c *politeiaClient) loadServerPolicy() error { 179 | serverPolicy, err := c.serverPolicy() 180 | if err != nil { 181 | return err 182 | } 183 | 184 | c.policy = &serverPolicy 185 | 186 | return nil 187 | } 188 | 189 | func (c *politeiaClient) serverPolicy() (www.PolicyReply, error) { 190 | var policyReply www.PolicyReply 191 | err := c.makeRequest(http.MethodGet, apiPath, www.RoutePolicy, nil, &policyReply) 192 | return policyReply, err 193 | } 194 | 195 | func (c *politeiaClient) serverVersion() (www.VersionReply, error) { 196 | var versionReply www.VersionReply 197 | err := c.makeRequest(http.MethodGet, apiPath, www.RouteVersion, nil, &versionReply) 198 | return versionReply, err 199 | } 200 | 201 | func (c *politeiaClient) batchProposals(tokens []string) ([]Proposal, error) { 202 | b, err := json.Marshal(&www.BatchProposals{Tokens: tokens}) 203 | if err != nil { 204 | return nil, err 205 | } 206 | 207 | var batchProposalsReply www.BatchProposalsReply 208 | 209 | err = c.makeRequest(http.MethodPost, apiPath, www.RouteBatchProposals, b, &batchProposalsReply) 210 | if err != nil { 211 | return nil, err 212 | } 213 | 214 | proposals := make([]Proposal, len(batchProposalsReply.Proposals)) 215 | for i, proposalRecord := range batchProposalsReply.Proposals { 216 | proposal := Proposal{ 217 | Token: proposalRecord.CensorshipRecord.Token, 218 | Name: proposalRecord.Name, 219 | State: int32(proposalRecord.State), 220 | Status: int32(proposalRecord.Status), 221 | Timestamp: proposalRecord.Timestamp, 222 | UserID: proposalRecord.UserId, 223 | Username: proposalRecord.Username, 224 | NumComments: int32(proposalRecord.NumComments), 225 | Version: proposalRecord.Version, 226 | PublishedAt: proposalRecord.PublishedAt, 227 | } 228 | 229 | for _, file := range proposalRecord.Files { 230 | if file.Name == "index.md" { 231 | proposal.IndexFile = file.Payload 232 | break 233 | } 234 | } 235 | 236 | proposals[i] = proposal 237 | } 238 | 239 | return proposals, nil 240 | } 241 | 242 | func (c *politeiaClient) proposalDetails(token string) (*www.ProposalDetailsReply, error) { 243 | 244 | route := proposalDetailsPath + token 245 | 246 | var proposalDetailsReply www.ProposalDetailsReply 247 | err := c.makeRequest(http.MethodGet, apiPath, route, nil, &proposalDetailsReply) 248 | if err != nil { 249 | return nil, err 250 | } 251 | 252 | return &proposalDetailsReply, nil 253 | } 254 | 255 | func (c *politeiaClient) tokenInventory() (*www.TokenInventoryReply, error) { 256 | var tokenInventoryReply www.TokenInventoryReply 257 | 258 | err := c.makeRequest(http.MethodGet, apiPath, www.RouteTokenInventory, nil, &tokenInventoryReply) 259 | if err != nil { 260 | return nil, err 261 | } 262 | 263 | return &tokenInventoryReply, nil 264 | } 265 | 266 | func (c *politeiaClient) voteDetails(token string) (*tkv1.DetailsReply, error) { 267 | 268 | requestBody, err := json.Marshal(&tkv1.Details{Token: token}) 269 | if err != nil { 270 | return nil, err 271 | } 272 | 273 | var dr tkv1.DetailsReply 274 | err = c.makeRequest(http.MethodPost, ticketVoteApi, tkv1.RouteDetails, requestBody, &dr) 275 | if err != nil { 276 | return nil, err 277 | } 278 | 279 | // Verify VoteDetails. 280 | err = client.VoteDetailsVerify(*dr.Vote, c.version.PubKey) 281 | if err != nil { 282 | return nil, err 283 | } 284 | 285 | return &dr, nil 286 | } 287 | 288 | func (c *politeiaClient) voteResults(token string) (*tkv1.ResultsReply, error) { 289 | 290 | requestBody, err := json.Marshal(&tkv1.Results{Token: token}) 291 | if err != nil { 292 | return nil, err 293 | } 294 | 295 | var resultReply tkv1.ResultsReply 296 | err = c.makeRequest(http.MethodPost, ticketVoteApi, tkv1.RouteResults, requestBody, &resultReply) 297 | if err != nil { 298 | return nil, err 299 | } 300 | 301 | // Verify CastVoteDetails. 302 | for _, cvd := range resultReply.Votes { 303 | err = client.CastVoteDetailsVerify(cvd, c.version.PubKey) 304 | if err != nil { 305 | return nil, err 306 | } 307 | } 308 | 309 | return &resultReply, nil 310 | } 311 | 312 | func (c *politeiaClient) batchVoteSummary(tokens []string) (map[string]www.VoteSummary, error) { 313 | b, err := json.Marshal(&www.BatchVoteSummary{Tokens: tokens}) 314 | if err != nil { 315 | return nil, err 316 | } 317 | 318 | var batchVoteSummaryReply www.BatchVoteSummaryReply 319 | 320 | err = c.makeRequest(http.MethodPost, apiPath, www.RouteBatchVoteSummary, b, &batchVoteSummaryReply) 321 | if err != nil { 322 | return nil, err 323 | } 324 | 325 | return batchVoteSummaryReply.Summaries, nil 326 | } 327 | 328 | func (c *politeiaClient) sendVotes(votes []tkv1.CastVote) error { 329 | b, err := json.Marshal(&tkv1.CastBallot{Votes: votes}) 330 | if err != nil { 331 | return err 332 | } 333 | 334 | var reply tkv1.CastBallotReply 335 | err = c.makeRequest(http.MethodPost, ticketVoteApi, tkv1.RouteCastBallot, b, &reply) 336 | if err != nil { 337 | return err 338 | } 339 | 340 | for _, receipt := range reply.Receipts { 341 | if receipt.ErrorContext != "" { 342 | return fmt.Errorf(receipt.ErrorContext) 343 | } 344 | } 345 | return nil 346 | } 347 | -------------------------------------------------------------------------------- /internal/politeia/types.go: -------------------------------------------------------------------------------- 1 | package politeia 2 | 3 | type Proposal struct { 4 | ID int `storm:"id,increment"` 5 | Token string `json:"token" storm:"unique"` 6 | Category int32 `json:"category" storm:"index"` 7 | Name string `json:"name"` 8 | State int32 `json:"state"` 9 | Status int32 `json:"status"` 10 | Timestamp int64 `json:"timestamp"` 11 | UserID string `json:"userid"` 12 | Username string `json:"username"` 13 | NumComments int32 `json:"numcomments"` 14 | Version string `json:"version"` 15 | PublishedAt int64 `json:"publishedat"` 16 | IndexFile string `json:"indexfile"` 17 | IndexFileVersion string `json:"fileversion"` 18 | VoteStatus int32 `json:"votestatus"` 19 | VoteApproved bool `json:"voteapproved"` 20 | YesVotes int32 `json:"yesvotes"` 21 | NoVotes int32 `json:"novotes"` 22 | EligibleTickets int32 `json:"eligibletickets"` 23 | QuorumPercentage int32 `json:"quorumpercentage"` 24 | PassPercentage int32 `json:"passpercentage"` 25 | } 26 | 27 | type ProposalOverview struct { 28 | All int32 29 | Discussion int32 30 | Voting int32 31 | Approved int32 32 | Rejected int32 33 | Abandoned int32 34 | } 35 | 36 | type ProposalVoteDetails struct { 37 | EligibleTickets []*EligibleTicket 38 | Votes []*ProposalVote 39 | YesVotes int32 40 | NoVotes int32 41 | } 42 | 43 | type EligibleTicket struct { 44 | Hash string 45 | Address string 46 | } 47 | 48 | type ProposalVote struct { 49 | Ticket *EligibleTicket 50 | Bit string 51 | } 52 | 53 | type ProposalNotificationListener interface { 54 | OnProposalsSynced() 55 | OnNewProposal(proposal *Proposal) 56 | OnProposalVoteStarted(proposal *Proposal) 57 | OnProposalVoteFinished(proposal *Proposal) 58 | } 59 | -------------------------------------------------------------------------------- /internal/uniformprng/prng.go: -------------------------------------------------------------------------------- 1 | // Package uniformprng implements a uniform, cryptographically secure 2 | // pseudo-random number generator. 3 | package uniformprng 4 | 5 | import ( 6 | "encoding/binary" 7 | "io" 8 | "math/bits" 9 | 10 | "golang.org/x/crypto/chacha20" 11 | ) 12 | 13 | // Source returns cryptographically-secure pseudorandom numbers with uniform 14 | // distribution. 15 | type Source struct { 16 | buf [8]byte 17 | cipher *chacha20.Cipher 18 | } 19 | 20 | var nonce = make([]byte, chacha20.NonceSize) 21 | 22 | // NewSource seeds a Source from a 32-byte key. 23 | func NewSource(seed *[32]byte) *Source { 24 | cipher, _ := chacha20.NewUnauthenticatedCipher(seed[:], nonce) 25 | return &Source{cipher: cipher} 26 | } 27 | 28 | // RandSource creates a Source with seed randomness read from rand. 29 | func RandSource(rand io.Reader) (*Source, error) { 30 | seed := new([32]byte) 31 | _, err := io.ReadFull(rand, seed[:]) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return NewSource(seed), nil 36 | } 37 | 38 | // Uint32 returns a pseudo-random uint32. 39 | func (s *Source) Uint32() uint32 { 40 | b := s.buf[:4] 41 | for i := range b { 42 | b[i] = 0 43 | } 44 | s.cipher.XORKeyStream(b, b) 45 | return binary.LittleEndian.Uint32(b) 46 | } 47 | 48 | // Uint32n returns a pseudo-random uint32 in range [0,n) without modulo bias. 49 | func (s *Source) Uint32n(n uint32) uint32 { 50 | if n < 2 { 51 | return 0 52 | } 53 | n-- 54 | mask := ^uint32(0) >> bits.LeadingZeros32(n) 55 | for { 56 | u := s.Uint32() & mask 57 | if u <= n { 58 | return u 59 | } 60 | } 61 | } 62 | 63 | // Int63 returns a pseudo-random 63-bit positive integer as an int64 without 64 | // modulo bias. 65 | func (s *Source) Int63() int64 { 66 | b := s.buf[:] 67 | for i := range b { 68 | b[i] = 0 69 | } 70 | s.cipher.XORKeyStream(b, b) 71 | return int64(binary.LittleEndian.Uint64(b) &^ (1 << 63)) 72 | } 73 | 74 | // Int63n returns, as an int64, a pseudo-random 63-bit positive integer in [0,n) 75 | // without modulo bias. 76 | // It panics if n <= 0. 77 | func (s *Source) Int63n(n int64) int64 { 78 | if n <= 0 { 79 | panic("invalid argument to Int63n") 80 | } 81 | n-- 82 | mask := int64(^uint64(0) >> bits.LeadingZeros64(uint64(n))) 83 | for { 84 | i := s.Int63() & mask 85 | if i <= n { 86 | return i 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /internal/vsp/client.go: -------------------------------------------------------------------------------- 1 | package vsp 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/ed25519" 7 | "encoding/base64" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | 13 | "github.com/decred/dcrd/txscript/v4/stdaddr" 14 | ) 15 | 16 | type client struct { 17 | http.Client 18 | url string 19 | pub []byte 20 | sign func(context.Context, string, stdaddr.Address) ([]byte, error) 21 | } 22 | 23 | type signer interface { 24 | SignMessage(ctx context.Context, message string, address stdaddr.Address) ([]byte, error) 25 | } 26 | 27 | func newClient(url string, pub []byte, s signer) *client { 28 | return &client{url: url, pub: pub, sign: s.SignMessage} 29 | } 30 | 31 | type BadRequestError struct { 32 | HTTPStatus int `json:"-"` 33 | Code int `json:"code"` 34 | Message string `json:"message"` 35 | } 36 | 37 | func (e *BadRequestError) Error() string { return e.Message } 38 | 39 | func (c *client) post(ctx context.Context, path string, addr stdaddr.Address, resp, req interface{}) error { 40 | return c.do(ctx, "POST", path, addr, resp, req) 41 | } 42 | 43 | func (c *client) get(ctx context.Context, path string, resp interface{}) error { 44 | return c.do(ctx, "GET", path, nil, resp, nil) 45 | } 46 | 47 | func (c *client) do(ctx context.Context, method, path string, addr stdaddr.Address, resp, req interface{}) error { 48 | var reqBody io.Reader 49 | var sig []byte 50 | if method == "POST" { 51 | body, err := json.Marshal(req) 52 | if err != nil { 53 | return fmt.Errorf("marshal request: %w", err) 54 | } 55 | sig, err = c.sign(ctx, string(body), addr) 56 | if err != nil { 57 | return fmt.Errorf("sign request: %w", err) 58 | } 59 | reqBody = bytes.NewReader(body) 60 | } 61 | httpReq, err := http.NewRequestWithContext(ctx, method, c.url+path, reqBody) 62 | if err != nil { 63 | return fmt.Errorf("new request: %w", err) 64 | } 65 | if sig != nil { 66 | httpReq.Header.Set("VSP-Client-Signature", base64.StdEncoding.EncodeToString(sig)) 67 | } 68 | reply, err := c.Do(httpReq) 69 | if err != nil { 70 | return fmt.Errorf("%s %s: %w", method, httpReq.URL.String(), err) 71 | } 72 | defer reply.Body.Close() 73 | 74 | status := reply.StatusCode 75 | is200 := status == 200 76 | is4xx := status >= 400 && status <= 499 77 | if !(is200 || is4xx) { 78 | return fmt.Errorf("%s %s: http %v %s", method, httpReq.URL.String(), 79 | status, http.StatusText(status)) 80 | } 81 | sigBase64 := reply.Header.Get("VSP-Server-Signature") 82 | if sigBase64 == "" { 83 | return fmt.Errorf("cannot authenticate server: no signature") 84 | } 85 | sig, err = base64.StdEncoding.DecodeString(sigBase64) 86 | if err != nil { 87 | return fmt.Errorf("cannot authenticate server: %w", err) 88 | } 89 | respBody, err := io.ReadAll(reply.Body) 90 | if err != nil { 91 | return fmt.Errorf("read response body: %w", err) 92 | } 93 | if !ed25519.Verify(c.pub, respBody, sig) { 94 | return fmt.Errorf("cannot authenticate server: invalid signature") 95 | } 96 | var apiError *BadRequestError 97 | if is4xx { 98 | apiError = new(BadRequestError) 99 | resp = apiError 100 | } 101 | if resp != nil { 102 | err = json.Unmarshal(respBody, resp) 103 | if err != nil { 104 | return fmt.Errorf("unmarshal respose body: %w", err) 105 | } 106 | } 107 | if apiError != nil { 108 | apiError.HTTPStatus = status 109 | return apiError 110 | } 111 | return nil 112 | } 113 | -------------------------------------------------------------------------------- /internal/vsp/errors.go: -------------------------------------------------------------------------------- 1 | package vsp 2 | 3 | const ( 4 | codeBadRequest = iota 5 | codeInternalErr 6 | codeVspClosed 7 | codeFeeAlreadyReceived 8 | codeInvalidFeeTx 9 | codeFeeTooSmall 10 | codeUnknownTicket 11 | codeTicketCannotVote 12 | codeFeeExpired 13 | codeInvalidVoteChoices 14 | codeBadSignature 15 | codeInvalidPrivKey 16 | codeFeeNotReceived 17 | codeInvalidTicket 18 | codeCannotBroadcastTicket 19 | codeCannotBroadcastFee 20 | codeCannotBroadcastFeeUnknownOutputs 21 | ) 22 | -------------------------------------------------------------------------------- /internal/vsp/log.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2018 The Decred developers 2 | // Use of this source code is governed by an ISC 3 | // license that can be found in the LICENSE file. 4 | 5 | package vsp 6 | 7 | import "github.com/decred/slog" 8 | 9 | // log is a logger that is initialized with no output filters. This 10 | // means the package will not perform any logging by default until the caller 11 | // requests it. 12 | var log = slog.Disabled 13 | 14 | // UseLogger uses a specified Logger to output package logging info. 15 | // This should be used in preference to SetLogWriter if the caller is also 16 | // using slog. 17 | func UseLogger(logger slog.Logger) { 18 | log = logger 19 | } 20 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013-2017 The btcsuite developers 2 | // Copyright (c) 2015-2018 The Decred developers 3 | // Use of this source code is governed by an ISC 4 | // license that can be found in the LICENSE file. 5 | 6 | package dcrlibwallet 7 | 8 | import ( 9 | "os" 10 | 11 | "decred.org/dcrwallet/v2/errors" 12 | "decred.org/dcrwallet/v2/p2p" 13 | "decred.org/dcrwallet/v2/ticketbuyer" 14 | "decred.org/dcrwallet/v2/wallet" 15 | "decred.org/dcrwallet/v2/wallet/udb" 16 | "github.com/decred/dcrd/addrmgr/v2" 17 | "github.com/decred/dcrd/connmgr/v3" 18 | "github.com/decred/slog" 19 | "github.com/jrick/logrotate/rotator" 20 | "github.com/planetdecred/dcrlibwallet/ext" 21 | "github.com/planetdecred/dcrlibwallet/internal/loader" 22 | "github.com/planetdecred/dcrlibwallet/internal/politeia" 23 | "github.com/planetdecred/dcrlibwallet/internal/vsp" 24 | "github.com/planetdecred/dcrlibwallet/spv" 25 | ) 26 | 27 | // logWriter implements an io.Writer that outputs to both standard output and 28 | // the write-end pipe of an initialized log rotator. 29 | type logWriter struct{} 30 | 31 | func (logWriter) Write(p []byte) (n int, err error) { 32 | os.Stdout.Write(p) 33 | logRotator.Write(p) 34 | return len(p), nil 35 | } 36 | 37 | // Loggers per subsystem. A single backend logger is created and all subsytem 38 | // loggers created from it will write to the backend. When adding new 39 | // subsystems, add the subsystem logger variable here and to the 40 | // subsystemLoggers map. 41 | // 42 | // Loggers can not be used before the log rotator has been initialized with a 43 | // log file. This must be performed early during application startup by calling 44 | // initLogRotator. 45 | var ( 46 | // backendLog is the logging backend used to create all subsystem loggers. 47 | // The backend must not be used before the log rotator has been initialized, 48 | // or data races and/or nil pointer dereferences will occur. 49 | backendLog = slog.NewBackend(logWriter{}) 50 | 51 | // logRotator is one of the logging outputs. It should be closed on 52 | // application shutdown. 53 | logRotator *rotator.Rotator 54 | 55 | log = backendLog.Logger("DLWL") 56 | loaderLog = backendLog.Logger("LODR") 57 | walletLog = backendLog.Logger("WLLT") 58 | tkbyLog = backendLog.Logger("TKBY") 59 | syncLog = backendLog.Logger("SYNC") 60 | grpcLog = backendLog.Logger("GRPC") 61 | legacyRPCLog = backendLog.Logger("RPCS") 62 | cmgrLog = backendLog.Logger("CMGR") 63 | amgrLog = backendLog.Logger("AMGR") 64 | vspcLog = backendLog.Logger("VSPC") 65 | politeiaLog = backendLog.Logger("POLT") 66 | extLog = backendLog.Logger("EXT") 67 | ) 68 | 69 | // Initialize package-global logger variables. 70 | func init() { 71 | loader.UseLogger(loaderLog) 72 | wallet.UseLogger(walletLog) 73 | udb.UseLogger(walletLog) 74 | ticketbuyer.UseLogger(tkbyLog) 75 | spv.UseLogger(syncLog) 76 | p2p.UseLogger(syncLog) 77 | connmgr.UseLogger(cmgrLog) 78 | addrmgr.UseLogger(amgrLog) 79 | vsp.UseLogger(vspcLog) 80 | politeia.UseLogger(politeiaLog) 81 | ext.UseLogger(extLog) 82 | } 83 | 84 | // subsystemLoggers maps each subsystem identifier to its associated logger. 85 | var subsystemLoggers = map[string]slog.Logger{ 86 | "DLWL": log, 87 | "LODR": loaderLog, 88 | "WLLT": walletLog, 89 | "TKBY": tkbyLog, 90 | "SYNC": syncLog, 91 | "GRPC": grpcLog, 92 | "RPCS": legacyRPCLog, 93 | "CMGR": cmgrLog, 94 | "AMGR": amgrLog, 95 | "VSPC": vspcLog, 96 | "POLT": politeiaLog, 97 | "EXT": extLog, 98 | } 99 | 100 | // initLogRotator initializes the logging rotater to write logs to logFile and 101 | // create roll files in the same directory. It must be called before the 102 | // package-global log rotater variables are used. 103 | func initLogRotator(logFile string) error { 104 | r, err := rotator.New(logFile, 10*1024, false, 3) 105 | if err != nil { 106 | return errors.Errorf("failed to create file rotator: %v", err) 107 | } 108 | 109 | logRotator = r 110 | return nil 111 | } 112 | 113 | // RegisterLogger should be called before logRotator is initialized. 114 | func RegisterLogger(tag string) (slog.Logger, error) { 115 | if logRotator != nil { 116 | return nil, errors.E(ErrLogRotatorAlreadyInitialized) 117 | } 118 | 119 | if _, exists := subsystemLoggers[tag]; exists { 120 | return nil, errors.E(ErrLoggerAlreadyRegistered) 121 | } 122 | 123 | logger := backendLog.Logger(tag) 124 | subsystemLoggers[tag] = logger 125 | 126 | return logger, nil 127 | } 128 | 129 | func SetLogLevels(logLevel string) { 130 | _, ok := slog.LevelFromString(logLevel) 131 | if !ok { 132 | return 133 | } 134 | 135 | // Configure all sub-systems with the new logging level. Dynamically 136 | // create loggers as needed. 137 | for subsystemID := range subsystemLoggers { 138 | setLogLevel(subsystemID, logLevel) 139 | } 140 | } 141 | 142 | // setLogLevel sets the logging level for provided subsystem. Invalid 143 | // subsystems are ignored. Uninitialized subsystems are dynamically created as 144 | // needed. 145 | func setLogLevel(subsystemID string, logLevel string) { 146 | // Ignore invalid subsystems. 147 | logger, ok := subsystemLoggers[subsystemID] 148 | if !ok { 149 | return 150 | } 151 | 152 | // Defaults to info if the log level is invalid. 153 | level, _ := slog.LevelFromString(logLevel) 154 | logger.SetLevel(level) 155 | } 156 | 157 | // Log writes a message to the log using LevelInfo. 158 | func Log(m string) { 159 | log.Info(m) 160 | } 161 | 162 | // LogT writes a tagged message to the log using LevelInfo. 163 | func LogT(tag, m string) { 164 | log.Infof("%s: %s", tag, m) 165 | } 166 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "decred.org/dcrwallet/v2/errors" 5 | w "decred.org/dcrwallet/v2/wallet" 6 | "github.com/decred/dcrd/txscript/v4/stdaddr" 7 | ) 8 | 9 | func (wallet *Wallet) SignMessage(passphrase []byte, address string, message string) ([]byte, error) { 10 | err := wallet.UnlockWallet(passphrase) 11 | if err != nil { 12 | return nil, translateError(err) 13 | } 14 | defer wallet.LockWallet() 15 | 16 | return wallet.signMessage(address, message) 17 | } 18 | 19 | func (wallet *Wallet) signMessage(address string, message string) ([]byte, error) { 20 | addr, err := stdaddr.DecodeAddress(address, wallet.chainParams) 21 | if err != nil { 22 | return nil, translateError(err) 23 | } 24 | 25 | // Addresses must have an associated secp256k1 private key and therefore 26 | // must be P2PK or P2PKH (P2SH is not allowed). 27 | switch addr.(type) { 28 | case *stdaddr.AddressPubKeyEcdsaSecp256k1V0: 29 | case *stdaddr.AddressPubKeyHashEcdsaSecp256k1V0: 30 | default: 31 | return nil, errors.New(ErrInvalidAddress) 32 | } 33 | 34 | sig, err := wallet.Internal().SignMessage(wallet.shutdownContext(), message, addr) 35 | if err != nil { 36 | return nil, translateError(err) 37 | } 38 | 39 | return sig, nil 40 | } 41 | 42 | func (mw *MultiWallet) VerifyMessage(address string, message string, signatureBase64 string) (bool, error) { 43 | var valid bool 44 | 45 | addr, err := stdaddr.DecodeAddress(address, mw.chainParams) 46 | if err != nil { 47 | return false, translateError(err) 48 | } 49 | 50 | signature, err := DecodeBase64(signatureBase64) 51 | if err != nil { 52 | return false, err 53 | } 54 | 55 | // Addresses must have an associated secp256k1 private key and therefore 56 | // must be P2PK or P2PKH (P2SH is not allowed). 57 | switch addr.(type) { 58 | case *stdaddr.AddressPubKeyEcdsaSecp256k1V0: 59 | case *stdaddr.AddressPubKeyHashEcdsaSecp256k1V0: 60 | default: 61 | return false, errors.New(ErrInvalidAddress) 62 | } 63 | 64 | valid, err = w.VerifyMessage(message, addr, signature, mw.chainParams) 65 | if err != nil { 66 | return false, translateError(err) 67 | } 68 | 69 | return valid, nil 70 | } 71 | -------------------------------------------------------------------------------- /multiwallet_config.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "github.com/asdine/storm" 5 | ) 6 | 7 | const ( 8 | userConfigBucketName = "user_config" 9 | 10 | LogLevelConfigKey = "log_level" 11 | 12 | SpendUnconfirmedConfigKey = "spend_unconfirmed" 13 | CurrencyConversionConfigKey = "currency_conversion_option" 14 | 15 | IsStartupSecuritySetConfigKey = "startup_security_set" 16 | StartupSecurityTypeConfigKey = "startup_security_type" 17 | UseBiometricConfigKey = "use_biometric" 18 | 19 | IncomingTxNotificationsConfigKey = "tx_notification_enabled" 20 | BeepNewBlocksConfigKey = "beep_new_blocks" 21 | 22 | SyncOnCellularConfigKey = "always_sync" 23 | NetworkModeConfigKey = "network_mode" 24 | SpvPersistentPeerAddressesConfigKey = "spv_peer_addresses" 25 | UserAgentConfigKey = "user_agent" 26 | 27 | PoliteiaNotificationConfigKey = "politeia_notification" 28 | 29 | LastTxHashConfigKey = "last_tx_hash" 30 | 31 | KnownVSPsConfigKey = "known_vsps" 32 | 33 | TicketBuyerVSPHostConfigKey = "tb_vsp_host" 34 | TicketBuyerWalletConfigKey = "tb_wallet_id" 35 | TicketBuyerAccountConfigKey = "tb_account_number" 36 | TicketBuyerATMConfigKey = "tb_amount_to_maintain" 37 | 38 | PassphraseTypePin int32 = 0 39 | PassphraseTypePass int32 = 1 40 | ) 41 | 42 | type configSaveFn = func(key string, value interface{}) error 43 | type configReadFn = func(multiwallet bool, key string, valueOut interface{}) error 44 | 45 | func (mw *MultiWallet) walletConfigSetFn(walletID int) configSaveFn { 46 | return func(key string, value interface{}) error { 47 | walletUniqueKey := WalletUniqueConfigKey(walletID, key) 48 | return mw.db.Set(userConfigBucketName, walletUniqueKey, value) 49 | } 50 | } 51 | 52 | func (mw *MultiWallet) walletConfigReadFn(walletID int) configReadFn { 53 | return func(multiwallet bool, key string, valueOut interface{}) error { 54 | if !multiwallet { 55 | key = WalletUniqueConfigKey(walletID, key) 56 | } 57 | return mw.db.Get(userConfigBucketName, key, valueOut) 58 | } 59 | } 60 | 61 | func (mw *MultiWallet) SaveUserConfigValue(key string, value interface{}) { 62 | err := mw.db.Set(userConfigBucketName, key, value) 63 | if err != nil { 64 | log.Errorf("error setting config value for key: %s, error: %v", key, err) 65 | } 66 | } 67 | 68 | func (mw *MultiWallet) ReadUserConfigValue(key string, valueOut interface{}) error { 69 | err := mw.db.Get(userConfigBucketName, key, valueOut) 70 | if err != nil && err != storm.ErrNotFound { 71 | log.Errorf("error reading config value for key: %s, error: %v", key, err) 72 | } 73 | return err 74 | } 75 | 76 | func (mw *MultiWallet) DeleteUserConfigValueForKey(key string) { 77 | err := mw.db.Delete(userConfigBucketName, key) 78 | if err != nil { 79 | log.Errorf("error deleting config value for key: %s, error: %v", key, err) 80 | } 81 | } 82 | 83 | func (mw *MultiWallet) ClearConfig() { 84 | err := mw.db.Drop(userConfigBucketName) 85 | if err != nil { 86 | log.Errorf("error deleting config bucket: %v", err) 87 | } 88 | } 89 | 90 | func (mw *MultiWallet) SetBoolConfigValueForKey(key string, value bool) { 91 | mw.SaveUserConfigValue(key, value) 92 | } 93 | 94 | func (mw *MultiWallet) SetDoubleConfigValueForKey(key string, value float64) { 95 | mw.SaveUserConfigValue(key, value) 96 | } 97 | 98 | func (mw *MultiWallet) SetIntConfigValueForKey(key string, value int) { 99 | mw.SaveUserConfigValue(key, value) 100 | } 101 | 102 | func (mw *MultiWallet) SetInt32ConfigValueForKey(key string, value int32) { 103 | mw.SaveUserConfigValue(key, value) 104 | } 105 | 106 | func (mw *MultiWallet) SetLongConfigValueForKey(key string, value int64) { 107 | mw.SaveUserConfigValue(key, value) 108 | } 109 | 110 | func (mw *MultiWallet) SetStringConfigValueForKey(key, value string) { 111 | mw.SaveUserConfigValue(key, value) 112 | } 113 | 114 | func (mw *MultiWallet) ReadBoolConfigValueForKey(key string, defaultValue bool) (valueOut bool) { 115 | if err := mw.ReadUserConfigValue(key, &valueOut); err == storm.ErrNotFound { 116 | valueOut = defaultValue 117 | } 118 | return 119 | } 120 | 121 | func (mw *MultiWallet) ReadDoubleConfigValueForKey(key string, defaultValue float64) (valueOut float64) { 122 | if err := mw.ReadUserConfigValue(key, &valueOut); err == storm.ErrNotFound { 123 | valueOut = defaultValue 124 | } 125 | return 126 | } 127 | 128 | func (mw *MultiWallet) ReadIntConfigValueForKey(key string, defaultValue int) (valueOut int) { 129 | if err := mw.ReadUserConfigValue(key, &valueOut); err == storm.ErrNotFound { 130 | valueOut = defaultValue 131 | } 132 | return 133 | } 134 | 135 | func (mw *MultiWallet) ReadInt32ConfigValueForKey(key string, defaultValue int32) (valueOut int32) { 136 | if err := mw.ReadUserConfigValue(key, &valueOut); err == storm.ErrNotFound { 137 | valueOut = defaultValue 138 | } 139 | return 140 | } 141 | 142 | func (mw *MultiWallet) ReadLongConfigValueForKey(key string, defaultValue int64) (valueOut int64) { 143 | if err := mw.ReadUserConfigValue(key, &valueOut); err == storm.ErrNotFound { 144 | valueOut = defaultValue 145 | } 146 | return 147 | } 148 | 149 | func (mw *MultiWallet) ReadStringConfigValueForKey(key string) (valueOut string) { 150 | mw.ReadUserConfigValue(key, &valueOut) 151 | return 152 | } 153 | -------------------------------------------------------------------------------- /multiwallet_utils.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "decred.org/dcrwallet/v2/deployments" 10 | "decred.org/dcrwallet/v2/errors" 11 | w "decred.org/dcrwallet/v2/wallet" 12 | "decred.org/dcrwallet/v2/walletseed" 13 | "github.com/asdine/storm" 14 | "github.com/decred/dcrd/chaincfg/v3" 15 | "github.com/decred/dcrd/hdkeychain/v3" 16 | "github.com/kevinburke/nacl" 17 | "github.com/kevinburke/nacl/secretbox" 18 | "golang.org/x/crypto/scrypt" 19 | ) 20 | 21 | const ( 22 | logFileName = "dcrlibwallet.log" 23 | walletsDbName = "wallets.db" 24 | 25 | walletsMetadataBucketName = "metadata" 26 | walletstartupPassphraseField = "startup-passphrase" 27 | ) 28 | 29 | var ( 30 | Mainnet = chaincfg.MainNetParams().Name 31 | Testnet3 = chaincfg.TestNet3Params().Name 32 | ) 33 | 34 | func (mw *MultiWallet) batchDbTransaction(dbOp func(node storm.Node) error) (err error) { 35 | dbTx, err := mw.db.Begin(true) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | // Commit or rollback the transaction after f returns or panics. Do not 41 | // recover from the panic to keep the original stack trace intact. 42 | panicked := true 43 | defer func() { 44 | if panicked || err != nil { 45 | dbTx.Rollback() 46 | return 47 | } 48 | 49 | err = dbTx.Commit() 50 | }() 51 | 52 | err = dbOp(dbTx) 53 | panicked = false 54 | return err 55 | } 56 | 57 | func (mw *MultiWallet) loadWalletTemporarily(ctx context.Context, walletDataDir, walletPublicPass string, 58 | onLoaded func(*w.Wallet) error) error { 59 | 60 | if walletPublicPass == "" { 61 | walletPublicPass = w.InsecurePubPassphrase 62 | } 63 | 64 | // initialize the wallet loader 65 | walletLoader := initWalletLoader(mw.chainParams, walletDataDir, mw.dbDriver) 66 | 67 | // open the wallet to get ready for temporary use 68 | wallet, err := walletLoader.OpenExistingWallet(ctx, []byte(walletPublicPass)) 69 | if err != nil { 70 | return translateError(err) 71 | } 72 | 73 | // unload wallet after temporary use 74 | defer walletLoader.UnloadWallet() 75 | 76 | if onLoaded != nil { 77 | return onLoaded(wallet) 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func (mw *MultiWallet) markWalletAsDiscoveredAccounts(walletID int) error { 84 | wallet := mw.WalletWithID(walletID) 85 | if wallet == nil { 86 | return errors.New(ErrNotExist) 87 | } 88 | 89 | log.Infof("Set discovered accounts = true for wallet %d", wallet.ID) 90 | wallet.HasDiscoveredAccounts = true 91 | err := mw.db.Save(wallet) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | return nil 97 | } 98 | 99 | // RootDirFileSizeInBytes returns the total directory size of 100 | // multiwallet's root directory in bytes. 101 | func (mw *MultiWallet) RootDirFileSizeInBytes() (int64, error) { 102 | var size int64 103 | err := filepath.Walk(mw.rootDir, func(_ string, info os.FileInfo, err error) error { 104 | if err != nil { 105 | return err 106 | } 107 | if !info.IsDir() { 108 | size += info.Size() 109 | } 110 | return err 111 | }) 112 | return size, err 113 | } 114 | 115 | // DCP0001ActivationBlockHeight returns the hardcoded block height that 116 | // the DCP0001 deployment activates at. DCP0001 specifies hard forking 117 | // changes to the stake difficulty algorithm. 118 | func (mw *MultiWallet) DCP0001ActivationBlockHeight() int32 { 119 | var activationHeight int32 = -1 120 | switch strings.ToLower(mw.chainParams.Name) { 121 | case strings.ToLower(Mainnet): 122 | activationHeight = deployments.DCP0001.MainNetActivationHeight 123 | case strings.ToLower(Testnet3): 124 | activationHeight = deployments.DCP0001.TestNet3ActivationHeight 125 | default: 126 | } 127 | 128 | return activationHeight 129 | } 130 | 131 | // WalletWithXPub returns the ID of the wallet that has an account with the 132 | // provided xpub. Returns -1 if there is no such wallet. 133 | func (mw *MultiWallet) WalletWithXPub(xpub string) (int, error) { 134 | ctx, cancel := mw.contextWithShutdownCancel() 135 | defer cancel() 136 | 137 | for _, w := range mw.wallets { 138 | if !w.WalletOpened() { 139 | return -1, errors.Errorf("wallet %d is not open and cannot be checked", w.ID) 140 | } 141 | accounts, err := w.Internal().Accounts(ctx) 142 | if err != nil { 143 | return -1, err 144 | } 145 | for _, account := range accounts.Accounts { 146 | if account.AccountNumber == ImportedAccountNumber { 147 | continue 148 | } 149 | acctXPub, err := w.Internal().AccountXpub(ctx, account.AccountNumber) 150 | if err != nil { 151 | return -1, err 152 | } 153 | if acctXPub.String() == xpub { 154 | return w.ID, nil 155 | } 156 | } 157 | } 158 | return -1, nil 159 | } 160 | 161 | // WalletWithSeed returns the ID of the wallet that was created or restored 162 | // using the same seed as the one provided. Returns -1 if no wallet uses the 163 | // provided seed. 164 | func (mw *MultiWallet) WalletWithSeed(seedMnemonic string) (int, error) { 165 | if len(seedMnemonic) == 0 { 166 | return -1, errors.New(ErrEmptySeed) 167 | } 168 | 169 | newSeedLegacyXPUb, newSeedSLIP0044XPUb, err := deriveBIP44AccountXPubs(seedMnemonic, DefaultAccountNum, mw.chainParams) 170 | if err != nil { 171 | return -1, err 172 | } 173 | 174 | for _, wallet := range mw.wallets { 175 | if !wallet.WalletOpened() { 176 | return -1, errors.Errorf("cannot check if seed matches unloaded wallet %d", wallet.ID) 177 | } 178 | // NOTE: Existing watch-only wallets may have been created using the 179 | // xpub of an account that is NOT the default account and may return 180 | // incorrect result from the check below. But this would return true 181 | // if the watch-only wallet was created using the xpub of the default 182 | // account of the provided seed. 183 | usesSameSeed, err := wallet.AccountXPubMatches(DefaultAccountNum, newSeedLegacyXPUb, newSeedSLIP0044XPUb) 184 | if err != nil { 185 | return -1, err 186 | } 187 | if usesSameSeed { 188 | return wallet.ID, nil 189 | } 190 | } 191 | 192 | return -1, nil 193 | } 194 | 195 | // deriveBIP44AccountXPub derives and returns the legacy and SLIP0044 account 196 | // xpubs using the BIP44 HD path for accounts: m/44'/'/'. 197 | func deriveBIP44AccountXPubs(seedMnemonic string, account uint32, params *chaincfg.Params) (string, string, error) { 198 | seed, err := walletseed.DecodeUserInput(seedMnemonic) 199 | if err != nil { 200 | return "", "", err 201 | } 202 | defer func() { 203 | for i := range seed { 204 | seed[i] = 0 205 | } 206 | }() 207 | 208 | // Derive the master extended key from the provided seed. 209 | masterNode, err := hdkeychain.NewMaster(seed, params) 210 | if err != nil { 211 | return "", "", err 212 | } 213 | defer masterNode.Zero() 214 | 215 | // Derive the purpose key as a child of the master node. 216 | purpose, err := masterNode.Child(44 + hdkeychain.HardenedKeyStart) 217 | if err != nil { 218 | return "", "", err 219 | } 220 | defer purpose.Zero() 221 | 222 | accountXPub := func(coinType uint32) (string, error) { 223 | coinTypePrivKey, err := purpose.Child(coinType + hdkeychain.HardenedKeyStart) 224 | if err != nil { 225 | return "", err 226 | } 227 | defer coinTypePrivKey.Zero() 228 | acctPrivKey, err := coinTypePrivKey.Child(account + hdkeychain.HardenedKeyStart) 229 | if err != nil { 230 | return "", err 231 | } 232 | defer acctPrivKey.Zero() 233 | return acctPrivKey.Neuter().String(), nil 234 | } 235 | 236 | legacyXPUb, err := accountXPub(params.LegacyCoinType) 237 | if err != nil { 238 | return "", "", err 239 | } 240 | slip0044XPUb, err := accountXPub(params.SLIP0044CoinType) 241 | if err != nil { 242 | return "", "", err 243 | } 244 | 245 | return legacyXPUb, slip0044XPUb, nil 246 | } 247 | 248 | // naclLoadFromPass derives a nacl.Key from pass using scrypt.Key. 249 | func naclLoadFromPass(pass []byte) (nacl.Key, error) { 250 | 251 | const N, r, p = 1 << 15, 8, 1 252 | 253 | hash, err := scrypt.Key(pass, nil, N, r, p, 32) 254 | if err != nil { 255 | return nil, err 256 | } 257 | return nacl.Load(EncodeHex(hash)) 258 | } 259 | 260 | // encryptWalletSeed encrypts the seed with secretbox.EasySeal using pass. 261 | func encryptWalletSeed(pass []byte, seed string) ([]byte, error) { 262 | key, err := naclLoadFromPass(pass) 263 | if err != nil { 264 | return nil, err 265 | } 266 | return secretbox.EasySeal([]byte(seed), key), nil 267 | } 268 | 269 | // decryptWalletSeed decrypts the encryptedSeed with secretbox.EasyOpen using pass. 270 | func decryptWalletSeed(pass []byte, encryptedSeed []byte) (string, error) { 271 | key, err := naclLoadFromPass(pass) 272 | if err != nil { 273 | return "", err 274 | } 275 | 276 | decryptedSeed, err := secretbox.EasyOpen(encryptedSeed, key) 277 | if err != nil { 278 | return "", errors.New(ErrInvalidPassphrase) 279 | } 280 | 281 | return string(decryptedSeed), nil 282 | } 283 | -------------------------------------------------------------------------------- /multiwallet_utils_test.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "bytes" 5 | "math/rand" 6 | 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | // genPass generates a random []byte 12 | func genPass() []byte { 13 | pass := make([]byte, rand.Intn(32)) 14 | _, err := rand.Read(pass) 15 | Expect(err).To(BeNil()) 16 | return pass 17 | } 18 | 19 | var _ = Describe("MultiwalletUtils", func() { 20 | Describe("Wallet Seed Encryption", func() { 21 | Context("encryptWalletSeed and decryptWalletSeed", func() { 22 | It("encrypts and decrypts the wallet seed properly", func() { 23 | pass := genPass() 24 | fakePass := genPass() 25 | for bytes.Equal(pass, fakePass) { 26 | fakePass = genPass() 27 | } 28 | 29 | seed, err := GenerateSeed() 30 | Expect(err).To(BeNil()) 31 | 32 | By("Encrypting the seed with the password") 33 | encrypted, err := encryptWalletSeed(pass, seed) 34 | Expect(err).To(BeNil()) 35 | 36 | By("Failing decryption of the encrypted seed using the wrong password") 37 | _, err = decryptWalletSeed(fakePass, encrypted) 38 | Expect(err).ToNot(BeNil()) 39 | 40 | By("Decrypting the encrypted seed using the correct password") 41 | decrypted, err := decryptWalletSeed(pass, encrypted) 42 | Expect(err).To(BeNil()) 43 | 44 | By("Comparing the decrypted and original seeds") 45 | Expect(seed).To(Equal(decrypted)) 46 | }) 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /rescan.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "time" 7 | 8 | "decred.org/dcrwallet/v2/errors" 9 | w "decred.org/dcrwallet/v2/wallet" 10 | ) 11 | 12 | func (mw *MultiWallet) RescanBlocks(walletID int) error { 13 | return mw.RescanBlocksFromHeight(walletID, 0) 14 | } 15 | 16 | func (mw *MultiWallet) RescanBlocksFromHeight(walletID int, startHeight int32) error { 17 | 18 | wallet := mw.WalletWithID(walletID) 19 | if wallet == nil { 20 | return errors.E(ErrNotExist) 21 | } 22 | 23 | netBackend, err := wallet.Internal().NetworkBackend() 24 | if err != nil { 25 | return errors.E(ErrNotConnected) 26 | } 27 | 28 | if mw.IsRescanning() || !mw.IsSynced() { 29 | return errors.E(ErrInvalid) 30 | } 31 | 32 | go func() { 33 | defer func() { 34 | mw.syncData.mu.Lock() 35 | mw.syncData.rescanning = false 36 | mw.syncData.cancelRescan = nil 37 | mw.syncData.mu.Unlock() 38 | }() 39 | 40 | ctx, cancel := wallet.shutdownContextWithCancel() 41 | 42 | mw.syncData.mu.Lock() 43 | mw.syncData.rescanning = true 44 | mw.syncData.cancelRescan = cancel 45 | mw.syncData.mu.Unlock() 46 | 47 | if mw.blocksRescanProgressListener != nil { 48 | mw.blocksRescanProgressListener.OnBlocksRescanStarted(walletID) 49 | } 50 | 51 | progress := make(chan w.RescanProgress, 1) 52 | go wallet.Internal().RescanProgressFromHeight(ctx, netBackend, startHeight, progress) 53 | 54 | rescanStartTime := time.Now().Unix() 55 | 56 | for p := range progress { 57 | if p.Err != nil { 58 | log.Error(p.Err) 59 | if mw.blocksRescanProgressListener != nil { 60 | mw.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, p.Err) 61 | } 62 | return 63 | } 64 | 65 | rescanProgressReport := &HeadersRescanProgressReport{ 66 | CurrentRescanHeight: p.ScannedThrough, 67 | TotalHeadersToScan: wallet.GetBestBlock(), 68 | WalletID: walletID, 69 | } 70 | 71 | elapsedRescanTime := time.Now().Unix() - rescanStartTime 72 | rescanRate := float64(p.ScannedThrough) / float64(rescanProgressReport.TotalHeadersToScan) 73 | 74 | rescanProgressReport.RescanProgress = int32(math.Round(rescanRate * 100)) 75 | estimatedTotalRescanTime := int64(math.Round(float64(elapsedRescanTime) / rescanRate)) 76 | rescanProgressReport.RescanTimeRemaining = estimatedTotalRescanTime - elapsedRescanTime 77 | 78 | rescanProgressReport.GeneralSyncProgress = &GeneralSyncProgress{ 79 | TotalSyncProgress: rescanProgressReport.RescanProgress, 80 | TotalTimeRemainingSeconds: rescanProgressReport.RescanTimeRemaining, 81 | } 82 | 83 | if mw.blocksRescanProgressListener != nil { 84 | mw.blocksRescanProgressListener.OnBlocksRescanProgress(rescanProgressReport) 85 | } 86 | 87 | select { 88 | case <-ctx.Done(): 89 | log.Info("Rescan canceled through context") 90 | 91 | if mw.blocksRescanProgressListener != nil { 92 | if ctx.Err() != nil && ctx.Err() != context.Canceled { 93 | mw.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, ctx.Err()) 94 | } else { 95 | mw.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, nil) 96 | } 97 | } 98 | 99 | return 100 | default: 101 | continue 102 | } 103 | } 104 | 105 | var err error 106 | if startHeight == 0 { 107 | err = wallet.reindexTransactions() 108 | } else { 109 | err = wallet.walletDataDB.SaveLastIndexPoint(startHeight) 110 | if err != nil { 111 | if mw.blocksRescanProgressListener != nil { 112 | mw.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, err) 113 | } 114 | return 115 | } 116 | 117 | err = wallet.IndexTransactions() 118 | } 119 | if mw.blocksRescanProgressListener != nil { 120 | mw.blocksRescanProgressListener.OnBlocksRescanEnded(walletID, err) 121 | } 122 | }() 123 | 124 | return nil 125 | } 126 | 127 | func (mw *MultiWallet) CancelRescan() { 128 | mw.syncData.mu.Lock() 129 | defer mw.syncData.mu.Unlock() 130 | if mw.syncData.cancelRescan != nil { 131 | mw.syncData.cancelRescan() 132 | mw.syncData.cancelRescan = nil 133 | 134 | log.Info("Rescan canceled.") 135 | } 136 | } 137 | 138 | func (mw *MultiWallet) IsRescanning() bool { 139 | mw.syncData.mu.RLock() 140 | defer mw.syncData.mu.RUnlock() 141 | return mw.syncData.rescanning 142 | } 143 | 144 | func (mw *MultiWallet) SetBlocksRescanProgressListener(blocksRescanProgressListener BlocksRescanProgressListener) { 145 | mw.blocksRescanProgressListener = blocksRescanProgressListener 146 | } 147 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # run tests on all modules 6 | for i in $(find . -name go.mod -type f -print); do 7 | module=$(dirname ${i}) 8 | echo "running tests and lint on ${module}" 9 | (cd ${module} && \ 10 | go test && \ 11 | golangci-lint run --deadline=10m \ 12 | --disable-all \ 13 | --enable govet \ 14 | --enable staticcheck \ 15 | --enable gosimple \ 16 | --enable unconvert \ 17 | --enable ineffassign \ 18 | --enable goimports \ 19 | --enable misspell \ 20 | ) 21 | done 22 | -------------------------------------------------------------------------------- /spv/backend.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018-2021 The Decred developers 2 | // Use of this source code is governed by an ISC 3 | // license that can be found in the LICENSE file. 4 | 5 | package spv 6 | 7 | import ( 8 | "context" 9 | "runtime" 10 | "sync" 11 | 12 | "decred.org/dcrwallet/v2/errors" 13 | "decred.org/dcrwallet/v2/p2p" 14 | "decred.org/dcrwallet/v2/validate" 15 | "decred.org/dcrwallet/v2/wallet" 16 | "github.com/decred/dcrd/chaincfg/chainhash" 17 | "github.com/decred/dcrd/dcrutil/v4" 18 | "github.com/decred/dcrd/gcs/v3" 19 | "github.com/decred/dcrd/gcs/v3/blockcf2" 20 | "github.com/decred/dcrd/txscript/v4/stdaddr" 21 | "github.com/decred/dcrd/wire" 22 | ) 23 | 24 | var _ wallet.NetworkBackend = (*WalletBackend)(nil) 25 | 26 | type WalletBackend struct { 27 | *Syncer 28 | WalletID int 29 | } 30 | 31 | // TODO: When using the Syncer as a NetworkBackend, keep track of in-flight 32 | // blocks and cfilters. If one is already incoming, wait on that response. If 33 | // that peer is lost, try a different peer. Optionally keep a cache of fetched 34 | // data so it can be immediately returned without another call. 35 | 36 | func pickAny(*p2p.RemotePeer) bool { return true } 37 | 38 | // Blocks implements the Blocks method of the wallet.Peer interface. 39 | func (wb *WalletBackend) Blocks(ctx context.Context, blockHashes []*chainhash.Hash) ([]*wire.MsgBlock, error) { 40 | for { 41 | if err := ctx.Err(); err != nil { 42 | return nil, err 43 | } 44 | rp, err := wb.pickRemote(pickAny) 45 | if err != nil { 46 | return nil, err 47 | } 48 | blocks, err := rp.Blocks(ctx, blockHashes) 49 | if err != nil { 50 | continue 51 | } 52 | return blocks, nil 53 | } 54 | } 55 | 56 | // filterProof is an alias to the same anonymous struct as wallet package's 57 | // FilterProof struct. 58 | type filterProof = struct { 59 | Filter *gcs.FilterV2 60 | ProofIndex uint32 61 | Proof []chainhash.Hash 62 | } 63 | 64 | // CFiltersV2 implements the CFiltersV2 method of the wallet.Peer interface. 65 | func (wb *WalletBackend) CFiltersV2(ctx context.Context, blockHashes []*chainhash.Hash) ([]filterProof, error) { 66 | for { 67 | if err := ctx.Err(); err != nil { 68 | return nil, err 69 | } 70 | rp, err := wb.pickRemote(pickAny) 71 | if err != nil { 72 | return nil, err 73 | } 74 | fs, err := rp.CFiltersV2(ctx, blockHashes) 75 | if err != nil { 76 | continue 77 | } 78 | return fs, nil 79 | } 80 | } 81 | 82 | // Headers implements the Headers method of the wallet.Peer interface. 83 | func (wb *WalletBackend) Headers(ctx context.Context, blockLocators []*chainhash.Hash, hashStop *chainhash.Hash) ([]*wire.BlockHeader, error) { 84 | for { 85 | if err := ctx.Err(); err != nil { 86 | return nil, err 87 | } 88 | rp, err := wb.pickRemote(pickAny) 89 | if err != nil { 90 | return nil, err 91 | } 92 | hs, err := rp.Headers(ctx, blockLocators, hashStop) 93 | if err != nil { 94 | continue 95 | } 96 | return hs, nil 97 | } 98 | } 99 | 100 | func (wb *WalletBackend) String() string { 101 | // This method is part of the wallet.Peer interface and will typically 102 | // specify the remote address of the peer. Since the syncer can encompass 103 | // multiple peers, just use the qualified type as the string. 104 | return "spv.Syncer" 105 | } 106 | 107 | // LoadTxFilter implements the LoadTxFilter method of the wallet.NetworkBackend 108 | // interface. 109 | // 110 | // NOTE: due to blockcf2 *not* including the spent outpoints in the block, the 111 | // addrs[] slice MUST include the addresses corresponding to the respective 112 | // outpoints, otherwise they will not be returned during the rescan. 113 | func (wb *WalletBackend) LoadTxFilter(ctx context.Context, reload bool, addrs []stdaddr.Address, outpoints []wire.OutPoint) error { 114 | wb.filterMu.Lock() 115 | if reload || wb.rescanFilter[wb.WalletID] == nil { 116 | wb.rescanFilter[wb.WalletID] = wallet.NewRescanFilter(nil, nil) 117 | wb.filterData[wb.WalletID] = &blockcf2.Entries{} 118 | } 119 | for _, addr := range addrs { 120 | _, pkScript := addr.PaymentScript() 121 | wb.rescanFilter[wb.WalletID].AddAddress(addr) 122 | wb.filterData[wb.WalletID].AddRegularPkScript(pkScript) 123 | } 124 | for i := range outpoints { 125 | wb.rescanFilter[wb.WalletID].AddUnspentOutPoint(&outpoints[i]) 126 | } 127 | wb.filterMu.Unlock() 128 | return nil 129 | } 130 | 131 | // PublishTransactions implements the PublishTransaction method of the 132 | // wallet.Peer interface. 133 | func (wb *WalletBackend) PublishTransactions(ctx context.Context, txs ...*wire.MsgTx) error { 134 | // Figure out transactions that are not stored by the wallet and create 135 | // an aux map so we can choose which need to be stored in the syncer's 136 | // mempool. 137 | walletBacked := make(map[chainhash.Hash]bool, len(txs)) 138 | for _, w := range wb.wallets { 139 | relevant, _, err := w.DetermineRelevantTxs(ctx, txs...) 140 | if err != nil { 141 | return err 142 | } 143 | for _, tx := range relevant { 144 | walletBacked[tx.TxHash()] = true 145 | } 146 | } 147 | 148 | msg := wire.NewMsgInvSizeHint(uint(len(txs))) 149 | for _, tx := range txs { 150 | txHash := tx.TxHash() 151 | if !walletBacked[txHash] { 152 | // Load into the mempool and let the mempool handler 153 | // know of it. 154 | if _, loaded := wb.mempool.LoadOrStore(txHash, tx); !loaded { 155 | select { 156 | case wb.mempoolAdds <- &txHash: 157 | case <-ctx.Done(): 158 | return ctx.Err() 159 | } 160 | } 161 | } 162 | err := msg.AddInvVect(wire.NewInvVect(wire.InvTypeTx, &txHash)) 163 | if err != nil { 164 | return errors.E(errors.Protocol, err) 165 | } 166 | } 167 | return wb.forRemotes(func(rp *p2p.RemotePeer) error { 168 | for _, inv := range msg.InvList { 169 | rp.InvsSent().Add(inv.Hash) 170 | } 171 | return rp.SendMessage(ctx, msg) 172 | }) 173 | } 174 | 175 | // Rescan implements the Rescan method of the wallet.NetworkBackend interface. 176 | func (wb *WalletBackend) Rescan(ctx context.Context, blockHashes []chainhash.Hash, save func(*chainhash.Hash, []*wire.MsgTx) error) error { 177 | const op errors.Op = "spv.Rescan" 178 | 179 | w, ok := wb.wallets[wb.WalletID] 180 | if !ok { 181 | return errors.E(op, errors.Invalid) 182 | } 183 | 184 | cfilters := make([]*gcs.FilterV2, 0, len(blockHashes)) 185 | cfilterKeys := make([][gcs.KeySize]byte, 0, len(blockHashes)) 186 | for i := 0; i < len(blockHashes); i++ { 187 | k, f, err := w.CFilterV2(ctx, &blockHashes[i]) 188 | if err != nil { 189 | return err 190 | } 191 | cfilters = append(cfilters, f) 192 | cfilterKeys = append(cfilterKeys, k) 193 | } 194 | 195 | blockMatches := make([]*wire.MsgBlock, len(blockHashes)) // Block assigned to slice once fetched 196 | 197 | // Read current filter data. filterData is reassinged to new data matches 198 | // for subsequent filter checks, which improves filter matching performance 199 | // by checking for less data. 200 | wb.filterMu.Lock() 201 | filterData := *wb.filterData[wb.WalletID] 202 | wb.filterMu.Unlock() 203 | 204 | idx := 0 205 | FilterLoop: 206 | for idx < len(blockHashes) { 207 | var fmatches []*chainhash.Hash 208 | var fmatchidx []int 209 | var fmatchMu sync.Mutex 210 | 211 | // Spawn ncpu workers to check filter matches 212 | ncpu := runtime.NumCPU() 213 | c := make(chan int, ncpu) 214 | var wg sync.WaitGroup 215 | wg.Add(ncpu) 216 | for i := 0; i < ncpu; i++ { 217 | go func() { 218 | for i := range c { 219 | blockHash := &blockHashes[i] 220 | key := cfilterKeys[i] 221 | f := cfilters[i] 222 | if f.MatchAny(key, filterData) { 223 | fmatchMu.Lock() 224 | fmatches = append(fmatches, blockHash) 225 | fmatchidx = append(fmatchidx, i) 226 | fmatchMu.Unlock() 227 | } 228 | } 229 | wg.Done() 230 | }() 231 | } 232 | for i := idx; i < len(blockHashes); i++ { 233 | if blockMatches[i] != nil { 234 | // Already fetched this block 235 | continue 236 | } 237 | c <- i 238 | } 239 | close(c) 240 | wg.Wait() 241 | 242 | if len(fmatches) != 0 { 243 | var rp *p2p.RemotePeer 244 | PickPeer: 245 | for { 246 | if err := ctx.Err(); err != nil { 247 | return err 248 | } 249 | if rp == nil { 250 | var err error 251 | rp, err = wb.pickRemote(pickAny) 252 | if err != nil { 253 | return err 254 | } 255 | } 256 | 257 | blocks, err := rp.Blocks(ctx, fmatches) 258 | if err != nil { 259 | rp = nil 260 | continue PickPeer 261 | } 262 | 263 | for j, b := range blocks { 264 | // Validate fetched blocks before rescanning transactions. PoW 265 | // and PoS difficulties have already been validated since the 266 | // header is saved by the wallet, and modifications to these in 267 | // the downloaded block would result in a different block hash 268 | // and failure to fetch the block. 269 | // 270 | // Block filters were also validated 271 | // against the header (assuming dcp0005 272 | // was activated). 273 | err = validate.MerkleRoots(b) 274 | if err != nil { 275 | err = validate.DCP0005MerkleRoot(b) 276 | } 277 | if err != nil { 278 | err := errors.E(op, err) 279 | rp.Disconnect(err) 280 | rp = nil 281 | continue PickPeer 282 | } 283 | 284 | i := fmatchidx[j] 285 | blockMatches[i] = b 286 | } 287 | break 288 | } 289 | } 290 | 291 | for i := idx; i < len(blockMatches); i++ { 292 | b := blockMatches[i] 293 | if b == nil { 294 | // No filter match, skip block 295 | continue 296 | } 297 | 298 | if err := ctx.Err(); err != nil { 299 | return err 300 | } 301 | 302 | matchedTxs, fadded := wb.rescanBlock(b, wb.WalletID) 303 | if len(matchedTxs) != 0 { 304 | err := save(&blockHashes[i], matchedTxs) 305 | if err != nil { 306 | return err 307 | } 308 | 309 | // Check for more matched blocks using updated filters, 310 | // starting at the next block. 311 | if len(fadded) != 0 { 312 | idx = i + 1 313 | filterData = fadded 314 | continue FilterLoop 315 | } 316 | } 317 | } 318 | return nil 319 | } 320 | 321 | return nil 322 | } 323 | 324 | // StakeDifficulty implements the StakeDifficulty method of the 325 | // wallet.NetworkBackend interface. 326 | // 327 | // This implementation of the method will always error as the stake difficulty 328 | // is not queryable over wire protocol, and when the next stake difficulty is 329 | // available in a header commitment, the wallet will be able to determine this 330 | // itself without requiring the NetworkBackend. 331 | func (wb *WalletBackend) StakeDifficulty(ctx context.Context) (dcrutil.Amount, error) { 332 | return 0, errors.E(errors.Invalid, "stake difficulty is not queryable over wire protocol") 333 | } 334 | -------------------------------------------------------------------------------- /spv/log.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013-2021 The btcsuite developers 2 | // Use of this source code is governed by an ISC 3 | // license that can be found in the LICENSE file. 4 | 5 | package spv 6 | 7 | import "github.com/decred/slog" 8 | 9 | var log = slog.Disabled 10 | 11 | // UseLogger uses a specified Logger to output package logging info. 12 | func UseLogger(logger slog.Logger) { 13 | log = logger 14 | } 15 | -------------------------------------------------------------------------------- /spv/rescan.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013-2014 The btcsuite developers 2 | // Copyright (c) 2018-2021 The Decred developers 3 | // Use of this source code is governed by an ISC 4 | // license that can be found in the LICENSE file. 5 | 6 | package spv 7 | 8 | import ( 9 | "github.com/decred/dcrd/blockchain/stake/v4" 10 | "github.com/decred/dcrd/gcs/v3/blockcf2" 11 | "github.com/decred/dcrd/txscript/v4/stdscript" 12 | "github.com/decred/dcrd/wire" 13 | ) 14 | 15 | // rescanCheckTransaction is a helper function to rescan both stake and regular 16 | // transactions in a block. It appends transactions that match the filters to 17 | // *matches, while updating the filters to add outpoints for new UTXOs 18 | // controlled by this wallet. New data added to the Syncer's filters is also 19 | // added to fadded. 20 | // 21 | // This function may only be called with the filter mutex held. 22 | func (s *Syncer) rescanCheckTransactions(matches *[]*wire.MsgTx, fadded *blockcf2.Entries, txs []*wire.MsgTx, tree int8, walletID int) { 23 | for i, tx := range txs { 24 | // Keep track of whether the transaction has already been added 25 | // to the result. It shouldn't be added twice. 26 | added := false 27 | 28 | txty := stake.TxTypeRegular 29 | if tree == wire.TxTreeStake { 30 | txty = stake.DetermineTxType(tx, true, false) 31 | } 32 | 33 | // Coinbases and stakebases are handled specially: all inputs of a 34 | // coinbase and the first (stakebase) input of a vote are skipped over 35 | // as they generate coins and do not reference any previous outputs. 36 | inputs := tx.TxIn 37 | if i == 0 && txty == stake.TxTypeRegular { 38 | goto LoopOutputs 39 | } 40 | if txty == stake.TxTypeSSGen { 41 | inputs = inputs[1:] 42 | } 43 | 44 | for _, input := range inputs { 45 | if !s.rescanFilter[walletID].ExistsUnspentOutPoint(&input.PreviousOutPoint) { 46 | continue 47 | } 48 | if !added { 49 | *matches = append(*matches, tx) 50 | added = true 51 | } 52 | } 53 | 54 | LoopOutputs: 55 | for i, output := range tx.TxOut { 56 | _, addrs := stdscript.ExtractAddrs(output.Version, output.PkScript, s.wallets[walletID].ChainParams()) 57 | for _, a := range addrs { 58 | if !s.rescanFilter[walletID].ExistsAddress(a) { 59 | continue 60 | } 61 | 62 | op := wire.OutPoint{ 63 | Hash: tx.TxHash(), 64 | Index: uint32(i), 65 | Tree: tree, 66 | } 67 | if !s.rescanFilter[walletID].ExistsUnspentOutPoint(&op) { 68 | s.rescanFilter[walletID].AddUnspentOutPoint(&op) 69 | } 70 | 71 | if !added { 72 | *matches = append(*matches, tx) 73 | added = true 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | // rescanBlock rescans a block for any relevant transactions for the passed 81 | // lookup keys. Returns any discovered transactions and any new data added to 82 | // the filter. 83 | func (s *Syncer) rescanBlock(block *wire.MsgBlock, walletID int) (matches []*wire.MsgTx, fadded blockcf2.Entries) { 84 | s.filterMu.Lock() 85 | s.rescanCheckTransactions(&matches, &fadded, block.STransactions, wire.TxTreeStake, walletID) 86 | s.rescanCheckTransactions(&matches, &fadded, block.Transactions, wire.TxTreeRegular, walletID) 87 | s.filterMu.Unlock() 88 | return matches, fadded 89 | } 90 | 91 | // filterRelevant filters out all transactions considered irrelevant 92 | // without updating filters. 93 | func (s *Syncer) filterRelevant(txs []*wire.MsgTx, walletID int) []*wire.MsgTx { 94 | defer s.filterMu.Unlock() 95 | s.filterMu.Lock() 96 | 97 | matches := txs[:0] 98 | Txs: 99 | for _, tx := range txs { 100 | for _, in := range tx.TxIn { 101 | if s.rescanFilter[walletID].ExistsUnspentOutPoint(&in.PreviousOutPoint) { 102 | matches = append(matches, tx) 103 | continue Txs 104 | } 105 | } 106 | for _, out := range tx.TxOut { 107 | _, addrs := stdscript.ExtractAddrs(out.Version, out.PkScript, s.wallets[walletID].ChainParams()) 108 | for _, a := range addrs { 109 | if s.rescanFilter[walletID].ExistsAddress(a) { 110 | matches = append(matches, tx) 111 | continue Txs 112 | } 113 | } 114 | } 115 | } 116 | 117 | return matches 118 | } 119 | -------------------------------------------------------------------------------- /transactions.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "encoding/json" 5 | "sort" 6 | 7 | "github.com/asdine/storm" 8 | "github.com/decred/dcrd/chaincfg/chainhash" 9 | "github.com/planetdecred/dcrlibwallet/txhelper" 10 | "github.com/planetdecred/dcrlibwallet/walletdata" 11 | ) 12 | 13 | const ( 14 | // Export constants for use in mobile apps 15 | // since gomobile excludes fields from sub packages. 16 | TxFilterAll = walletdata.TxFilterAll 17 | TxFilterSent = walletdata.TxFilterSent 18 | TxFilterReceived = walletdata.TxFilterReceived 19 | TxFilterTransferred = walletdata.TxFilterTransferred 20 | TxFilterStaking = walletdata.TxFilterStaking 21 | TxFilterCoinBase = walletdata.TxFilterCoinBase 22 | TxFilterRegular = walletdata.TxFilterRegular 23 | TxFilterMixed = walletdata.TxFilterMixed 24 | TxFilterVoted = walletdata.TxFilterVoted 25 | TxFilterRevoked = walletdata.TxFilterRevoked 26 | TxFilterImmature = walletdata.TxFilterImmature 27 | TxFilterLive = walletdata.TxFilterLive 28 | TxFilterUnmined = walletdata.TxFilterUnmined 29 | TxFilterExpired = walletdata.TxFilterExpired 30 | TxFilterTickets = walletdata.TxFilterTickets 31 | 32 | TxDirectionInvalid = txhelper.TxDirectionInvalid 33 | TxDirectionSent = txhelper.TxDirectionSent 34 | TxDirectionReceived = txhelper.TxDirectionReceived 35 | TxDirectionTransferred = txhelper.TxDirectionTransferred 36 | 37 | TxTypeRegular = txhelper.TxTypeRegular 38 | TxTypeCoinBase = txhelper.TxTypeCoinBase 39 | TxTypeTicketPurchase = txhelper.TxTypeTicketPurchase 40 | TxTypeVote = txhelper.TxTypeVote 41 | TxTypeRevocation = txhelper.TxTypeRevocation 42 | TxTypeMixed = txhelper.TxTypeMixed 43 | 44 | TicketStatusUnmined = "unmined" 45 | TicketStatusImmature = "immature" 46 | TicketStatusLive = "live" 47 | TicketStatusVotedOrRevoked = "votedrevoked" 48 | TicketStatusExpired = "expired" 49 | ) 50 | 51 | func (wallet *Wallet) PublishUnminedTransactions() error { 52 | n, err := wallet.Internal().NetworkBackend() 53 | if err != nil { 54 | log.Error(err) 55 | return err 56 | } 57 | 58 | return wallet.Internal().PublishUnminedTransactions(wallet.shutdownContext(), n) 59 | } 60 | 61 | func (wallet *Wallet) GetTransaction(txHash string) (string, error) { 62 | transaction, err := wallet.GetTransactionRaw(txHash) 63 | if err != nil { 64 | log.Error(err) 65 | return "", err 66 | } 67 | 68 | result, err := json.Marshal(transaction) 69 | if err != nil { 70 | return "", err 71 | } 72 | 73 | return string(result), nil 74 | } 75 | 76 | func (wallet *Wallet) GetTransactionRaw(txHash string) (*Transaction, error) { 77 | hash, err := chainhash.NewHashFromStr(txHash) 78 | if err != nil { 79 | log.Error(err) 80 | return nil, err 81 | } 82 | 83 | txSummary, _, blockHash, err := wallet.Internal().TransactionSummary(wallet.shutdownContext(), hash) 84 | if err != nil { 85 | log.Error(err) 86 | return nil, err 87 | } 88 | 89 | return wallet.decodeTransactionWithTxSummary(txSummary, blockHash) 90 | } 91 | 92 | func (wallet *Wallet) GetTransactions(offset, limit, txFilter int32, newestFirst bool) (string, error) { 93 | transactions, err := wallet.GetTransactionsRaw(offset, limit, txFilter, newestFirst) 94 | if err != nil { 95 | return "", err 96 | } 97 | 98 | jsonEncodedTransactions, err := json.Marshal(&transactions) 99 | if err != nil { 100 | return "", err 101 | } 102 | 103 | return string(jsonEncodedTransactions), nil 104 | } 105 | 106 | func (wallet *Wallet) GetTransactionsRaw(offset, limit, txFilter int32, newestFirst bool) (transactions []Transaction, err error) { 107 | err = wallet.walletDataDB.Read(offset, limit, txFilter, newestFirst, wallet.RequiredConfirmations(), wallet.GetBestBlock(), &transactions) 108 | return 109 | } 110 | 111 | func (mw *MultiWallet) GetTransactions(offset, limit, txFilter int32, newestFirst bool) (string, error) { 112 | 113 | transactions, err := mw.GetTransactionsRaw(offset, limit, txFilter, newestFirst) 114 | if err != nil { 115 | return "", err 116 | } 117 | 118 | jsonEncodedTransactions, err := json.Marshal(&transactions) 119 | if err != nil { 120 | return "", err 121 | } 122 | 123 | return string(jsonEncodedTransactions), nil 124 | } 125 | 126 | func (mw *MultiWallet) GetTransactionsRaw(offset, limit, txFilter int32, newestFirst bool) ([]Transaction, error) { 127 | transactions := make([]Transaction, 0) 128 | for _, wallet := range mw.wallets { 129 | walletTransactions, err := wallet.GetTransactionsRaw(offset, limit, txFilter, newestFirst) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | transactions = append(transactions, walletTransactions...) 135 | } 136 | 137 | // sort transaction by timestamp in descending order 138 | sort.Slice(transactions[:], func(i, j int) bool { 139 | if newestFirst { 140 | return transactions[i].Timestamp > transactions[j].Timestamp 141 | } 142 | return transactions[i].Timestamp < transactions[j].Timestamp 143 | }) 144 | 145 | if len(transactions) > int(limit) && limit > 0 { 146 | transactions = transactions[:limit] 147 | } 148 | 149 | return transactions, nil 150 | } 151 | 152 | func (wallet *Wallet) CountTransactions(txFilter int32) (int, error) { 153 | return wallet.walletDataDB.Count(txFilter, wallet.RequiredConfirmations(), wallet.GetBestBlock(), &Transaction{}) 154 | } 155 | 156 | func (wallet *Wallet) TicketHasVotedOrRevoked(ticketHash string) (bool, error) { 157 | err := wallet.walletDataDB.FindOne("TicketSpentHash", ticketHash, &Transaction{}) 158 | if err != nil { 159 | if err == storm.ErrNotFound { 160 | return false, nil 161 | } 162 | return false, err 163 | } 164 | 165 | return true, nil 166 | } 167 | 168 | func (wallet *Wallet) TicketSpender(ticketHash string) (*Transaction, error) { 169 | var spender Transaction 170 | err := wallet.walletDataDB.FindOne("TicketSpentHash", ticketHash, &spender) 171 | if err != nil { 172 | if err == storm.ErrNotFound { 173 | return nil, nil 174 | } 175 | return nil, err 176 | } 177 | 178 | return &spender, nil 179 | } 180 | 181 | func (wallet *Wallet) TransactionOverview() (txOverview *TransactionOverview, err error) { 182 | 183 | txOverview = &TransactionOverview{} 184 | 185 | txOverview.Sent, err = wallet.CountTransactions(TxFilterSent) 186 | if err != nil { 187 | return 188 | } 189 | 190 | txOverview.Received, err = wallet.CountTransactions(TxFilterReceived) 191 | if err != nil { 192 | return 193 | } 194 | 195 | txOverview.Transferred, err = wallet.CountTransactions(TxFilterTransferred) 196 | if err != nil { 197 | return 198 | } 199 | 200 | txOverview.Mixed, err = wallet.CountTransactions(TxFilterMixed) 201 | if err != nil { 202 | return 203 | } 204 | 205 | txOverview.Staking, err = wallet.CountTransactions(TxFilterStaking) 206 | if err != nil { 207 | return 208 | } 209 | 210 | txOverview.Coinbase, err = wallet.CountTransactions(TxFilterCoinBase) 211 | if err != nil { 212 | return 213 | } 214 | 215 | txOverview.All = txOverview.Sent + txOverview.Received + txOverview.Transferred + txOverview.Mixed + 216 | txOverview.Staking + txOverview.Coinbase 217 | 218 | return txOverview, nil 219 | } 220 | 221 | func (wallet *Wallet) TxMatchesFilter(tx *Transaction, txFilter int32) bool { 222 | bestBlock := wallet.GetBestBlock() 223 | 224 | // tickets with block height less than this are matured. 225 | maturityBlock := bestBlock - int32(wallet.chainParams.TicketMaturity) 226 | 227 | // tickets with block height less than this are expired. 228 | expiryBlock := bestBlock - int32(wallet.chainParams.TicketMaturity+uint16(wallet.chainParams.TicketExpiry)) 229 | 230 | switch txFilter { 231 | case TxFilterSent: 232 | return tx.Type == TxTypeRegular && tx.Direction == TxDirectionSent 233 | case TxFilterReceived: 234 | return tx.Type == TxTypeRegular && tx.Direction == TxDirectionReceived 235 | case TxFilterTransferred: 236 | return tx.Type == TxTypeRegular && tx.Direction == TxDirectionTransferred 237 | case TxFilterStaking: 238 | switch tx.Type { 239 | case TxTypeTicketPurchase: 240 | fallthrough 241 | case TxTypeVote: 242 | fallthrough 243 | case TxTypeRevocation: 244 | return true 245 | } 246 | 247 | return false 248 | case TxFilterCoinBase: 249 | return tx.Type == TxTypeCoinBase 250 | case TxFilterRegular: 251 | return tx.Type == TxTypeRegular 252 | case TxFilterMixed: 253 | return tx.Type == TxTypeMixed 254 | case TxFilterVoted: 255 | return tx.Type == TxTypeVote 256 | case TxFilterRevoked: 257 | return tx.Type == TxTypeRevocation 258 | case walletdata.TxFilterImmature: 259 | return tx.Type == TxTypeTicketPurchase && 260 | (tx.BlockHeight > maturityBlock) // not matured 261 | case TxFilterLive: 262 | // ticket is live if we don't have the spender hash and it hasn't expired. 263 | // we cannot detect missed tickets over spv. 264 | return tx.Type == TxTypeTicketPurchase && 265 | tx.TicketSpender == "" && 266 | tx.BlockHeight > 0 && 267 | tx.BlockHeight <= maturityBlock && 268 | tx.BlockHeight > expiryBlock // not expired 269 | case TxFilterUnmined: 270 | return tx.Type == TxTypeTicketPurchase && tx.BlockHeight == -1 271 | case TxFilterExpired: 272 | return tx.Type == TxTypeTicketPurchase && 273 | tx.TicketSpender == "" && 274 | tx.BlockHeight > 0 && 275 | tx.BlockHeight <= expiryBlock 276 | case TxFilterTickets: 277 | return tx.Type == TxTypeTicketPurchase 278 | case TxFilterAll: 279 | return true 280 | } 281 | 282 | return false 283 | } 284 | 285 | func (wallet *Wallet) TxMatchesFilter2(direction, blockHeight int32, txType, ticketSpender string, txFilter int32) bool { 286 | tx := Transaction{ 287 | Type: txType, 288 | Direction: direction, 289 | BlockHeight: blockHeight, 290 | TicketSpender: ticketSpender, 291 | } 292 | return wallet.TxMatchesFilter(&tx, txFilter) 293 | } 294 | 295 | func (tx Transaction) Confirmations(bestBlock int32) int32 { 296 | if tx.BlockHeight == BlockHeightInvalid { 297 | return 0 298 | } 299 | 300 | return (bestBlock - tx.BlockHeight) + 1 301 | } 302 | 303 | func (tx Transaction) TicketStatus(ticketMaturity, ticketExpiry, bestBlock int32) string { 304 | if tx.Type != TxTypeTicketPurchase { 305 | return "" 306 | } 307 | 308 | confirmations := tx.Confirmations(bestBlock) 309 | if confirmations == 0 { 310 | return TicketStatusUnmined 311 | } else if confirmations <= ticketMaturity { 312 | return TicketStatusImmature 313 | } else if confirmations > (ticketMaturity + ticketExpiry) { 314 | return TicketStatusExpired 315 | } else if tx.TicketSpender != "" { 316 | return TicketStatusVotedOrRevoked 317 | } 318 | 319 | return TicketStatusLive 320 | } 321 | -------------------------------------------------------------------------------- /treasury.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | 7 | "decred.org/dcrwallet/v2/errors" 8 | 9 | "github.com/decred/dcrd/blockchain/stake/v4" 10 | "github.com/decred/dcrd/chaincfg/chainhash" 11 | "github.com/decred/dcrd/dcrec/secp256k1/v4" 12 | ) 13 | 14 | // SetTreasuryPolicy saves the voting policy for treasury spends by a particular 15 | // PI key. 16 | // If a ticket hash is provided, the voting policy is also updated with the VSP 17 | // controlling the ticket. If a ticket hash isn't provided, the vote choice is 18 | // saved to the local wallet database and the VSPs controlling all unspent, 19 | // unexpired tickets are updated to use the specified vote policy. 20 | func (wallet *Wallet) SetTreasuryPolicy(PiKey, newVotingPolicy, tixHash string, passphrase []byte) error { 21 | var ticketHash *chainhash.Hash 22 | if tixHash != "" { 23 | tixHash, err := chainhash.NewHashFromStr(tixHash) 24 | if err != nil { 25 | return fmt.Errorf("invalid ticket hash: %w", err) 26 | } 27 | ticketHash = tixHash 28 | } 29 | 30 | pikey, err := hex.DecodeString(PiKey) 31 | if err != nil { 32 | return fmt.Errorf("invalid pikey: %w", err) 33 | } 34 | if len(pikey) != secp256k1.PubKeyBytesLenCompressed { 35 | return fmt.Errorf("treasury pikey must be %d bytes", secp256k1.PubKeyBytesLenCompressed) 36 | } 37 | 38 | var policy stake.TreasuryVoteT 39 | switch newVotingPolicy { 40 | case "abstain", "invalid", "": 41 | policy = stake.TreasuryVoteInvalid 42 | case "yes": 43 | policy = stake.TreasuryVoteYes 44 | case "no": 45 | policy = stake.TreasuryVoteNo 46 | default: 47 | return fmt.Errorf("invalid policy: unknown policy %q", newVotingPolicy) 48 | } 49 | 50 | // The wallet will need to be unlocked to sign the API 51 | // request(s) for setting this voting policy with the VSP. 52 | err = wallet.UnlockWallet(passphrase) 53 | if err != nil { 54 | return translateError(err) 55 | } 56 | defer wallet.LockWallet() 57 | 58 | currentVotingPolicy := wallet.Internal().TreasuryKeyPolicy(pikey, ticketHash) 59 | 60 | ctx := wallet.shutdownContext() 61 | 62 | err = wallet.Internal().SetTreasuryKeyPolicy(ctx, pikey, policy, ticketHash) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | var vspPreferenceUpdateSuccess bool 68 | defer func() { 69 | if !vspPreferenceUpdateSuccess { 70 | // Updating the treasury spend voting preference with the vsp failed, 71 | // revert the locally saved voting preference for the treasury spend. 72 | revertError := wallet.Internal().SetTreasuryKeyPolicy(ctx, pikey, currentVotingPolicy, ticketHash) 73 | if revertError != nil { 74 | log.Errorf("unable to revert locally saved voting preference: %v", revertError) 75 | } 76 | } 77 | }() 78 | 79 | // If a ticket hash is provided, set the specified vote policy with 80 | // the VSP associated with the provided ticket. Otherwise, set the 81 | // vote policy with the VSPs associated with all "votable" tickets. 82 | ticketHashes := make([]*chainhash.Hash, 0) 83 | if ticketHash != nil { 84 | ticketHashes = append(ticketHashes, ticketHash) 85 | } else { 86 | err = wallet.Internal().ForUnspentUnexpiredTickets(ctx, func(hash *chainhash.Hash) error { 87 | ticketHashes = append(ticketHashes, hash) 88 | return nil 89 | }) 90 | if err != nil { 91 | return fmt.Errorf("unable to fetch hashes for all unspent, unexpired tickets: %v", err) 92 | } 93 | } 94 | 95 | // Never return errors from this for loop, so all tickets are tried. 96 | // The first error will be returned to the caller. 97 | var firstErr error 98 | // Update voting preferences on VSPs if required. 99 | policyMap := map[string]string{ 100 | PiKey: newVotingPolicy, 101 | } 102 | for _, tHash := range ticketHashes { 103 | vspTicketInfo, err := wallet.Internal().VSPTicketInfo(ctx, tHash) 104 | if err != nil { 105 | // Ignore NotExist error, just means the ticket is not 106 | // registered with a VSP, nothing more to do here. 107 | if firstErr == nil && !errors.Is(err, errors.NotExist) { 108 | firstErr = err 109 | } 110 | continue // try next tHash 111 | } 112 | 113 | // Update the vote policy for the ticket with the associated VSP. 114 | vspClient, err := wallet.VSPClient(vspTicketInfo.Host, vspTicketInfo.PubKey) 115 | if err != nil && firstErr == nil { 116 | firstErr = err 117 | continue // try next tHash 118 | } 119 | err = vspClient.SetVoteChoice(ctx, tHash, nil, nil, policyMap) 120 | if err != nil && firstErr == nil { 121 | firstErr = err 122 | continue // try next tHash 123 | } 124 | } 125 | 126 | vspPreferenceUpdateSuccess = firstErr == nil 127 | return firstErr 128 | } 129 | 130 | // TreasuryPolicies returns saved voting policies for treasury spends 131 | // per pi key. If a pi key is specified, the policy for that pi key 132 | // is returned; otherwise the policies for all pi keys are returned. 133 | // If a ticket hash is provided, the policy(ies) for that ticket 134 | // is/are returned. 135 | func (wallet *Wallet) TreasuryPolicies(PiKey, tixHash string) ([]*TreasuryKeyPolicy, error) { 136 | var ticketHash *chainhash.Hash 137 | if tixHash != "" { 138 | tixHash, err := chainhash.NewHashFromStr(tixHash) 139 | if err != nil { 140 | return nil, fmt.Errorf("inavlid hash: %w", err) 141 | } 142 | ticketHash = tixHash 143 | } 144 | 145 | if PiKey != "" { 146 | pikey, err := hex.DecodeString(PiKey) 147 | if err != nil { 148 | return nil, fmt.Errorf("invalid pikey: %w", err) 149 | } 150 | var policy string 151 | switch wallet.Internal().TreasuryKeyPolicy(pikey, ticketHash) { 152 | case stake.TreasuryVoteYes: 153 | policy = "yes" 154 | case stake.TreasuryVoteNo: 155 | policy = "no" 156 | default: 157 | policy = "abstain" 158 | } 159 | res := []*TreasuryKeyPolicy{ 160 | { 161 | TicketHash: tixHash, 162 | PiKey: PiKey, 163 | Policy: policy, 164 | }, 165 | } 166 | return res, nil 167 | } 168 | 169 | policies := wallet.Internal().TreasuryKeyPolicies() 170 | res := make([]*TreasuryKeyPolicy, len(policies)) 171 | for i := range policies { 172 | var policy string 173 | switch policies[i].Policy { 174 | case stake.TreasuryVoteYes: 175 | policy = "yes" 176 | case stake.TreasuryVoteNo: 177 | policy = "no" 178 | } 179 | r := &TreasuryKeyPolicy{ 180 | PiKey: hex.EncodeToString(policies[i].PiKey), 181 | Policy: policy, 182 | } 183 | if policies[i].Ticket != nil { 184 | r.TicketHash = policies[i].Ticket.String() 185 | } 186 | res[i] = r 187 | } 188 | return res, nil 189 | } 190 | 191 | // PiKeys returns the sanctioned Politeia keys for the current network. 192 | func (mw *MultiWallet) PiKeys() [][]byte { 193 | return mw.chainParams.PiKeys 194 | } 195 | -------------------------------------------------------------------------------- /txandblocknotifications.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "decred.org/dcrwallet/v2/errors" 7 | ) 8 | 9 | func (mw *MultiWallet) listenForTransactions(walletID int) { 10 | go func() { 11 | 12 | wallet := mw.wallets[walletID] 13 | n := wallet.Internal().NtfnServer.TransactionNotifications() 14 | 15 | for { 16 | select { 17 | case v := <-n.C: 18 | if v == nil { 19 | return 20 | } 21 | for _, transaction := range v.UnminedTransactions { 22 | tempTransaction, err := wallet.decodeTransactionWithTxSummary(&transaction, nil) 23 | if err != nil { 24 | log.Errorf("[%d] Error ntfn parse tx: %v", wallet.ID, err) 25 | return 26 | } 27 | 28 | overwritten, err := wallet.walletDataDB.SaveOrUpdate(&Transaction{}, tempTransaction) 29 | if err != nil { 30 | log.Errorf("[%d] New Tx save err: %v", wallet.ID, err) 31 | return 32 | } 33 | 34 | if !overwritten { 35 | log.Infof("[%d] New Transaction %s", wallet.ID, tempTransaction.Hash) 36 | 37 | result, err := json.Marshal(tempTransaction) 38 | if err != nil { 39 | log.Error(err) 40 | } else { 41 | mw.mempoolTransactionNotification(string(result)) 42 | } 43 | } 44 | } 45 | 46 | for _, block := range v.AttachedBlocks { 47 | blockHash := block.Header.BlockHash() 48 | for _, transaction := range block.Transactions { 49 | tempTransaction, err := wallet.decodeTransactionWithTxSummary(&transaction, &blockHash) 50 | if err != nil { 51 | log.Errorf("[%d] Error ntfn parse tx: %v", wallet.ID, err) 52 | return 53 | } 54 | 55 | _, err = wallet.walletDataDB.SaveOrUpdate(&Transaction{}, tempTransaction) 56 | if err != nil { 57 | log.Errorf("[%d] Incoming block replace tx error :%v", wallet.ID, err) 58 | return 59 | } 60 | mw.publishTransactionConfirmed(wallet.ID, transaction.Hash.String(), int32(block.Header.Height)) 61 | } 62 | 63 | mw.publishBlockAttached(wallet.ID, int32(block.Header.Height)) 64 | } 65 | 66 | if len(v.AttachedBlocks) > 0 { 67 | mw.checkWalletMixers() 68 | } 69 | 70 | case <-mw.syncData.syncCanceled: 71 | n.Done() 72 | } 73 | } 74 | }() 75 | } 76 | 77 | // AddTxAndBlockNotificationListener registers a set of functions to be invoked 78 | // when a transaction or block update is processed by the wallet. If async is 79 | // true, the provided callback methods will be called from separate goroutines, 80 | // allowing notification senders to continue their operation without waiting 81 | // for the listener to complete processing the notification. This asyncrhonous 82 | // handling is especially important for cases where the wallet process that 83 | // sends the notification temporarily prevents access to other wallet features 84 | // until all notification handlers finish processing the notification. If a 85 | // notification handler were to try to access such features, it would result 86 | // in a deadlock. 87 | func (mw *MultiWallet) AddTxAndBlockNotificationListener(txAndBlockNotificationListener TxAndBlockNotificationListener, async bool, uniqueIdentifier string) error { 88 | mw.notificationListenersMu.Lock() 89 | defer mw.notificationListenersMu.Unlock() 90 | 91 | _, ok := mw.txAndBlockNotificationListeners[uniqueIdentifier] 92 | if ok { 93 | return errors.New(ErrListenerAlreadyExist) 94 | } 95 | 96 | if async { 97 | mw.txAndBlockNotificationListeners[uniqueIdentifier] = &asyncTxAndBlockNotificationListener{ 98 | l: txAndBlockNotificationListener, 99 | } 100 | } else { 101 | mw.txAndBlockNotificationListeners[uniqueIdentifier] = txAndBlockNotificationListener 102 | } 103 | 104 | return nil 105 | } 106 | 107 | func (mw *MultiWallet) RemoveTxAndBlockNotificationListener(uniqueIdentifier string) { 108 | mw.notificationListenersMu.Lock() 109 | defer mw.notificationListenersMu.Unlock() 110 | 111 | delete(mw.txAndBlockNotificationListeners, uniqueIdentifier) 112 | } 113 | 114 | func (mw *MultiWallet) checkWalletMixers() { 115 | for _, wallet := range mw.wallets { 116 | if wallet.IsAccountMixerActive() { 117 | unmixedAccount := wallet.ReadInt32ConfigValueForKey(AccountMixerUnmixedAccount, -1) 118 | hasMixableOutput, err := wallet.accountHasMixableOutput(unmixedAccount) 119 | if err != nil { 120 | log.Errorf("Error checking for mixable outputs: %v", err) 121 | } 122 | 123 | if !hasMixableOutput { 124 | log.Infof("[%d] unmixed account does not have a mixable output, stopping account mixer", wallet.ID) 125 | err = mw.StopAccountMixer(wallet.ID) 126 | if err != nil { 127 | log.Errorf("Error stopping account mixer: %v", err) 128 | } 129 | } 130 | } 131 | } 132 | } 133 | 134 | func (mw *MultiWallet) mempoolTransactionNotification(transaction string) { 135 | mw.notificationListenersMu.RLock() 136 | defer mw.notificationListenersMu.RUnlock() 137 | 138 | for _, txAndBlockNotifcationListener := range mw.txAndBlockNotificationListeners { 139 | txAndBlockNotifcationListener.OnTransaction(transaction) 140 | } 141 | } 142 | 143 | func (mw *MultiWallet) publishTransactionConfirmed(walletID int, transactionHash string, blockHeight int32) { 144 | mw.notificationListenersMu.RLock() 145 | defer mw.notificationListenersMu.RUnlock() 146 | 147 | for _, txAndBlockNotifcationListener := range mw.txAndBlockNotificationListeners { 148 | txAndBlockNotifcationListener.OnTransactionConfirmed(walletID, transactionHash, blockHeight) 149 | } 150 | } 151 | 152 | func (mw *MultiWallet) publishBlockAttached(walletID int, blockHeight int32) { 153 | mw.notificationListenersMu.RLock() 154 | defer mw.notificationListenersMu.RUnlock() 155 | 156 | for _, txAndBlockNotifcationListener := range mw.txAndBlockNotificationListeners { 157 | txAndBlockNotifcationListener.OnBlockAttached(walletID, blockHeight) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /txhelper/changesource.go: -------------------------------------------------------------------------------- 1 | package txhelper 2 | 3 | import ( 4 | "github.com/decred/dcrd/dcrutil/v4" 5 | "github.com/planetdecred/dcrlibwallet/addresshelper" 6 | ) 7 | 8 | const scriptVersion = 0 9 | 10 | // implements Script() and ScriptSize() functions of txauthor.ChangeSource 11 | type txChangeSource struct { 12 | version uint16 13 | script []byte 14 | } 15 | 16 | func (src *txChangeSource) Script() ([]byte, uint16, error) { 17 | return src.script, src.version, nil 18 | } 19 | 20 | func (src *txChangeSource) ScriptSize() int { 21 | return len(src.script) 22 | } 23 | 24 | func MakeTxChangeSource(destAddr string, net dcrutil.AddressParams) (*txChangeSource, error) { 25 | pkScript, err := addresshelper.PkScript(destAddr, net) 26 | if err != nil { 27 | return nil, err 28 | } 29 | changeSource := &txChangeSource{ 30 | script: pkScript, 31 | version: scriptVersion, 32 | } 33 | return changeSource, nil 34 | } 35 | -------------------------------------------------------------------------------- /txhelper/helper.go: -------------------------------------------------------------------------------- 1 | package txhelper 2 | 3 | import ( 4 | "math" 5 | 6 | "decred.org/dcrwallet/v2/wallet" 7 | "github.com/decred/dcrd/dcrutil/v4" 8 | "github.com/decred/dcrd/wire" 9 | "github.com/decred/dcrdata/v7/txhelpers" 10 | ) 11 | 12 | func MsgTxFeeSizeRate(transactionHex string) (msgTx *wire.MsgTx, fee dcrutil.Amount, size int, feeRate dcrutil.Amount, err error) { 13 | msgTx, err = txhelpers.MsgTxFromHex(transactionHex) 14 | if err != nil { 15 | return 16 | } 17 | 18 | size = msgTx.SerializeSize() 19 | fee, feeRate = txhelpers.TxFeeRate(msgTx) 20 | return 21 | } 22 | 23 | func TransactionAmountAndDirection(inputTotal, outputTotal, fee int64) (amount int64, direction int32) { 24 | amountDifference := outputTotal - inputTotal 25 | 26 | if amountDifference < 0 && float64(fee) == math.Abs(float64(amountDifference)) { 27 | // transferred internally, the only real amount spent was transaction fee 28 | direction = TxDirectionTransferred 29 | amount = fee 30 | } else if amountDifference > 0 { 31 | // received 32 | direction = TxDirectionReceived 33 | amount = outputTotal 34 | } else { 35 | // sent 36 | direction = TxDirectionSent 37 | amount = inputTotal - outputTotal - fee 38 | } 39 | 40 | return 41 | } 42 | 43 | func FormatTransactionType(txType wallet.TransactionType) string { 44 | switch txType { 45 | case wallet.TransactionTypeCoinbase: 46 | return TxTypeCoinBase 47 | case wallet.TransactionTypeTicketPurchase: 48 | return TxTypeTicketPurchase 49 | case wallet.TransactionTypeVote: 50 | return TxTypeVote 51 | case wallet.TransactionTypeRevocation: 52 | return TxTypeRevocation 53 | default: 54 | return TxTypeRegular 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /txhelper/outputs.go: -------------------------------------------------------------------------------- 1 | package txhelper 2 | 3 | import ( 4 | dcrutil "github.com/decred/dcrd/dcrutil/v4" 5 | "github.com/decred/dcrd/wire" 6 | "github.com/planetdecred/dcrlibwallet/addresshelper" 7 | ) 8 | 9 | func MakeTxOutput(address string, amountInAtom int64, net dcrutil.AddressParams) (output *wire.TxOut, err error) { 10 | pkScript, err := addresshelper.PkScript(address, net) 11 | if err != nil { 12 | return 13 | } 14 | 15 | output = &wire.TxOut{ 16 | Value: amountInAtom, 17 | Version: scriptVersion, 18 | PkScript: pkScript, 19 | } 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /txhelper/types.go: -------------------------------------------------------------------------------- 1 | package txhelper 2 | 3 | const ( 4 | TxDirectionInvalid int32 = -1 5 | TxDirectionSent int32 = 0 6 | TxDirectionReceived int32 = 1 7 | TxDirectionTransferred int32 = 2 8 | 9 | TxTypeRegular = "Regular" 10 | TxTypeMixed = "Mixed" 11 | TxTypeCoinBase = "Coinbase" 12 | TxTypeTicketPurchase = "Ticket" 13 | TxTypeVote = "Vote" 14 | TxTypeRevocation = "Revocation" 15 | ) 16 | -------------------------------------------------------------------------------- /txindex.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | w "decred.org/dcrwallet/v2/wallet" 5 | "github.com/decred/dcrd/chaincfg/chainhash" 6 | "github.com/planetdecred/dcrlibwallet/walletdata" 7 | ) 8 | 9 | func (wallet *Wallet) IndexTransactions() error { 10 | ctx := wallet.shutdownContext() 11 | 12 | var totalIndex int32 13 | var txEndHeight uint32 14 | rangeFn := func(block *w.Block) (bool, error) { 15 | for _, transaction := range block.Transactions { 16 | 17 | var blockHash *chainhash.Hash 18 | if block.Header != nil { 19 | hash := block.Header.BlockHash() 20 | blockHash = &hash 21 | } else { 22 | blockHash = nil 23 | } 24 | 25 | tx, err := wallet.decodeTransactionWithTxSummary(&transaction, blockHash) 26 | if err != nil { 27 | return false, err 28 | } 29 | 30 | _, err = wallet.walletDataDB.SaveOrUpdate(&Transaction{}, tx) 31 | if err != nil { 32 | log.Errorf("[%d] Index tx replace tx err : %v", wallet.ID, err) 33 | return false, err 34 | } 35 | 36 | totalIndex++ 37 | } 38 | 39 | if block.Header != nil { 40 | txEndHeight = block.Header.Height 41 | err := wallet.walletDataDB.SaveLastIndexPoint(int32(txEndHeight)) 42 | if err != nil { 43 | log.Errorf("[%d] Set tx index end block height error: ", wallet.ID, err) 44 | return false, err 45 | } 46 | 47 | log.Debugf("[%d] Index saved for transactions in block %d", wallet.ID, txEndHeight) 48 | } 49 | 50 | select { 51 | case <-ctx.Done(): 52 | return true, ctx.Err() 53 | default: 54 | return false, nil 55 | } 56 | } 57 | 58 | beginHeight, err := wallet.walletDataDB.ReadIndexingStartBlock() 59 | if err != nil { 60 | log.Errorf("[%d] Get tx indexing start point error: %v", wallet.ID, err) 61 | return err 62 | } 63 | 64 | endHeight := wallet.GetBestBlock() 65 | 66 | startBlock := w.NewBlockIdentifierFromHeight(beginHeight) 67 | endBlock := w.NewBlockIdentifierFromHeight(endHeight) 68 | 69 | defer func() { 70 | count, err := wallet.walletDataDB.Count(walletdata.TxFilterAll, wallet.RequiredConfirmations(), endHeight, &Transaction{}) 71 | if err != nil { 72 | log.Errorf("[%d] Post-indexing tx count error :%v", wallet.ID, err) 73 | } else if count > 0 { 74 | log.Infof("[%d] Transaction index finished at %d, %d transaction(s) indexed in total", wallet.ID, endHeight, count) 75 | } 76 | 77 | err = wallet.walletDataDB.SaveLastIndexPoint(endHeight) 78 | if err != nil { 79 | log.Errorf("[%d] Set tx index end block height error: ", wallet.ID, err) 80 | } 81 | }() 82 | 83 | log.Infof("[%d] Indexing transactions start height: %d, end height: %d", wallet.ID, beginHeight, endHeight) 84 | return wallet.Internal().GetTransactions(ctx, rangeFn, startBlock, endBlock) 85 | } 86 | 87 | func (wallet *Wallet) reindexTransactions() error { 88 | err := wallet.walletDataDB.ClearSavedTransactions(&Transaction{}) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | return wallet.IndexTransactions() 94 | } 95 | -------------------------------------------------------------------------------- /txparser.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "fmt" 5 | 6 | w "decred.org/dcrwallet/v2/wallet" 7 | "github.com/decred/dcrd/chaincfg/chainhash" 8 | ) 9 | 10 | const BlockHeightInvalid int32 = -1 11 | 12 | func (wallet *Wallet) decodeTransactionWithTxSummary(txSummary *w.TransactionSummary, 13 | blockHash *chainhash.Hash) (*Transaction, error) { 14 | 15 | var blockHeight int32 = BlockHeightInvalid 16 | if blockHash != nil { 17 | blockIdentifier := w.NewBlockIdentifierFromHash(blockHash) 18 | blockInfo, err := wallet.Internal().BlockInfo(wallet.shutdownContext(), blockIdentifier) 19 | if err != nil { 20 | log.Error(err) 21 | } else { 22 | blockHeight = blockInfo.Height 23 | } 24 | } 25 | 26 | walletInputs := make([]*WalletInput, len(txSummary.MyInputs)) 27 | for i, input := range txSummary.MyInputs { 28 | accountNumber := int32(input.PreviousAccount) 29 | accountName, err := wallet.AccountName(accountNumber) 30 | if err != nil { 31 | log.Error(err) 32 | } 33 | 34 | walletInputs[i] = &WalletInput{ 35 | Index: int32(input.Index), 36 | AmountIn: int64(input.PreviousAmount), 37 | WalletAccount: &WalletAccount{ 38 | AccountNumber: accountNumber, 39 | AccountName: accountName, 40 | }, 41 | } 42 | } 43 | 44 | walletOutputs := make([]*WalletOutput, len(txSummary.MyOutputs)) 45 | for i, output := range txSummary.MyOutputs { 46 | accountNumber := int32(output.Account) 47 | accountName, err := wallet.AccountName(accountNumber) 48 | if err != nil { 49 | log.Error(err) 50 | } 51 | 52 | walletOutputs[i] = &WalletOutput{ 53 | Index: int32(output.Index), 54 | AmountOut: int64(output.Amount), 55 | Internal: output.Internal, 56 | Address: output.Address.String(), 57 | WalletAccount: &WalletAccount{ 58 | AccountNumber: accountNumber, 59 | AccountName: accountName, 60 | }, 61 | } 62 | } 63 | 64 | walletTx := &TxInfoFromWallet{ 65 | WalletID: wallet.ID, 66 | BlockHeight: blockHeight, 67 | Timestamp: txSummary.Timestamp, 68 | Hex: fmt.Sprintf("%x", txSummary.Transaction), 69 | Inputs: walletInputs, 70 | Outputs: walletOutputs, 71 | } 72 | 73 | decodedTx, err := wallet.DecodeTransaction(walletTx, wallet.chainParams) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | if decodedTx.TicketSpentHash != "" { 79 | ticketPurchaseTx, err := wallet.GetTransactionRaw(decodedTx.TicketSpentHash) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | timeDifferenceInSeconds := decodedTx.Timestamp - ticketPurchaseTx.Timestamp 85 | decodedTx.DaysToVoteOrRevoke = int32(timeDifferenceInSeconds / 86400) // seconds to days conversion 86 | 87 | // calculate reward 88 | var ticketInvestment int64 89 | for _, input := range ticketPurchaseTx.Inputs { 90 | if input.AccountNumber > -1 { 91 | ticketInvestment += input.Amount 92 | } 93 | } 94 | 95 | var ticketOutput int64 96 | for _, output := range walletTx.Outputs { 97 | if output.AccountNumber > -1 { 98 | ticketOutput += output.AmountOut 99 | } 100 | } 101 | 102 | reward := ticketOutput - ticketInvestment 103 | decodedTx.VoteReward = reward 104 | 105 | // update ticket with spender hash 106 | ticketPurchaseTx.TicketSpender = decodedTx.Hash 107 | wallet.walletDataDB.SaveOrUpdate(&Transaction{}, ticketPurchaseTx) 108 | } 109 | 110 | return decodedTx, nil 111 | } 112 | -------------------------------------------------------------------------------- /utils/netparams.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | 6 | "decred.org/dcrwallet/v2/errors" 7 | "github.com/decred/dcrd/chaincfg/v3" 8 | ) 9 | 10 | var ( 11 | mainnetParams = chaincfg.MainNetParams() 12 | testnetParams = chaincfg.TestNet3Params() 13 | ) 14 | 15 | func ChainParams(netType string) (*chaincfg.Params, error) { 16 | switch strings.ToLower(netType) { 17 | case strings.ToLower(mainnetParams.Name): 18 | return mainnetParams, nil 19 | case strings.ToLower(testnetParams.Name): 20 | return testnetParams, nil 21 | default: 22 | return nil, errors.New("invalid net type") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /utxo.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "fmt" 5 | 6 | "decred.org/dcrwallet/v2/errors" 7 | "decred.org/dcrwallet/v2/wallet/txauthor" 8 | "decred.org/dcrwallet/v2/wallet/txrules" 9 | "decred.org/dcrwallet/v2/wallet/txsizes" 10 | "github.com/decred/dcrd/chaincfg/v3" 11 | "github.com/decred/dcrd/dcrutil/v4" 12 | "github.com/decred/dcrd/txscript/v4" 13 | "github.com/decred/dcrd/wire" 14 | "github.com/planetdecred/dcrlibwallet/txhelper" 15 | ) 16 | 17 | type nextAddressFunc func() (address string, err error) 18 | 19 | func calculateChangeScriptSize(changeAddress string, chainParams *chaincfg.Params) (int, error) { 20 | changeSource, err := txhelper.MakeTxChangeSource(changeAddress, chainParams) 21 | if err != nil { 22 | return 0, fmt.Errorf("change address error: %v", err) 23 | } 24 | return changeSource.ScriptSize(), nil 25 | } 26 | 27 | // ParseOutputsAndChangeDestination generates and returns TxOuts 28 | // using the provided slice of transaction destinations. 29 | // Any destination set to receive max amount is not included in the TxOuts returned, 30 | // but is instead returned as a change destination. 31 | // Returns an error if more than 1 max amount recipients identified or 32 | // if any other error is encountered while processing the addresses and amounts. 33 | func (tx *TxAuthor) ParseOutputsAndChangeDestination(txDestinations []TransactionDestination) ([]*wire.TxOut, int64, string, error) { 34 | var outputs = make([]*wire.TxOut, 0) 35 | var totalSendAmount int64 36 | var maxAmountRecipientAddress string 37 | 38 | for _, destination := range txDestinations { 39 | if err := tx.validateSendAmount(destination.SendMax, destination.AtomAmount); err != nil { 40 | return nil, 0, "", err 41 | } 42 | 43 | // check if multiple destinations are set to receive max amount 44 | if destination.SendMax && maxAmountRecipientAddress != "" { 45 | return nil, 0, "", fmt.Errorf("cannot send max amount to multiple recipients") 46 | } 47 | 48 | if destination.SendMax { 49 | maxAmountRecipientAddress = destination.Address 50 | continue // do not prepare a tx output for this destination 51 | } 52 | 53 | output, err := txhelper.MakeTxOutput(destination.Address, destination.AtomAmount, tx.sourceWallet.chainParams) 54 | if err != nil { 55 | return nil, 0, "", fmt.Errorf("make tx output error: %v", err) 56 | } 57 | 58 | totalSendAmount += output.Value 59 | outputs = append(outputs, output) 60 | } 61 | 62 | return outputs, totalSendAmount, maxAmountRecipientAddress, nil 63 | } 64 | 65 | func (tx *TxAuthor) constructCustomTransaction() (*txauthor.AuthoredTx, error) { 66 | // Used to generate an internal address for change, 67 | // if no change destination is provided and 68 | // no recipient is set to receive max amount. 69 | nextInternalAddress := func() (string, error) { 70 | ctx := tx.sourceWallet.shutdownContext() 71 | addr, err := tx.sourceWallet.Internal().NewChangeAddress(ctx, tx.sourceAccountNumber) 72 | if err != nil { 73 | return "", err 74 | } 75 | return addr.String(), nil 76 | } 77 | 78 | return tx.newUnsignedTxUTXO(tx.inputs, tx.destinations, tx.changeDestination, nextInternalAddress) 79 | } 80 | 81 | func (tx *TxAuthor) newUnsignedTxUTXO(inputs []*wire.TxIn, sendDestinations []TransactionDestination, changeDestination *TransactionDestination, 82 | nextInternalAddress nextAddressFunc) (*txauthor.AuthoredTx, error) { 83 | outputs, totalSendAmount, maxAmountRecipientAddress, err := tx.ParseOutputsAndChangeDestination(sendDestinations) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | if maxAmountRecipientAddress != "" && changeDestination != nil { 89 | return nil, errors.E(errors.Invalid, "no change is generated when sending max amount,"+ 90 | " change destinations must not be provided") 91 | } 92 | 93 | if maxAmountRecipientAddress == "" && changeDestination == nil { 94 | // no change specified, generate new internal address to use as change (max amount recipient) 95 | maxAmountRecipientAddress, err = nextInternalAddress() 96 | if err != nil { 97 | return nil, fmt.Errorf("error generating internal address to use as change: %s", err.Error()) 98 | } 99 | } 100 | 101 | var totalInputAmount int64 102 | inputScriptSizes := make([]int, len(inputs)) 103 | inputScripts := make([][]byte, len(inputs)) 104 | for i, input := range inputs { 105 | totalInputAmount += input.ValueIn 106 | inputScriptSizes[i] = txsizes.RedeemP2PKHSigScriptSize 107 | inputScripts[i] = input.SignatureScript 108 | } 109 | 110 | var changeScriptSize int 111 | if maxAmountRecipientAddress != "" { 112 | changeScriptSize, err = calculateChangeScriptSize(maxAmountRecipientAddress, tx.sourceWallet.chainParams) 113 | } else { 114 | changeScriptSize, err = calculateChangeScriptSize(changeDestination.Address, tx.sourceWallet.chainParams) 115 | } 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | maxSignedSize := txsizes.EstimateSerializeSize(inputScriptSizes, outputs, changeScriptSize) 121 | maxRequiredFee := txrules.FeeForSerializeSize(txrules.DefaultRelayFeePerKb, maxSignedSize) 122 | changeAmount := totalInputAmount - totalSendAmount - int64(maxRequiredFee) 123 | 124 | if changeAmount < 0 { 125 | return nil, errors.New(ErrInsufficientBalance) 126 | } 127 | 128 | if changeAmount != 0 && !txrules.IsDustAmount(dcrutil.Amount(changeAmount), changeScriptSize, txrules.DefaultRelayFeePerKb) { 129 | if changeScriptSize > txscript.MaxScriptElementSize { 130 | return nil, fmt.Errorf("script size exceed maximum bytes pushable to the stack") 131 | } 132 | if maxAmountRecipientAddress != "" { 133 | outputs, err = tx.changeOutput(changeAmount, maxAmountRecipientAddress, outputs) 134 | } else if changeDestination != nil { 135 | outputs, err = tx.changeOutput(changeAmount, changeDestination.Address, outputs) 136 | } 137 | if err != nil { 138 | return nil, fmt.Errorf("change address error: %v", err) 139 | } 140 | } 141 | 142 | return &txauthor.AuthoredTx{ 143 | TotalInput: dcrutil.Amount(totalInputAmount), 144 | EstimatedSignedSerializeSize: maxSignedSize, 145 | Tx: &wire.MsgTx{ 146 | SerType: wire.TxSerializeFull, 147 | Version: wire.TxVersion, 148 | TxIn: inputs, 149 | TxOut: outputs, 150 | LockTime: 0, 151 | Expiry: 0, 152 | }, 153 | }, nil 154 | } 155 | 156 | func (tx *TxAuthor) changeOutput(changeAmount int64, maxAmountRecipientAddress string, outputs []*wire.TxOut) ([]*wire.TxOut, error) { 157 | changeOutput, err := txhelper.MakeTxOutput(maxAmountRecipientAddress, changeAmount, tx.sourceWallet.chainParams) 158 | if err != nil { 159 | return nil, err 160 | } 161 | outputs = append(outputs, changeOutput) 162 | txauthor.RandomizeOutputPosition(outputs, len(outputs)-1) 163 | return outputs, nil 164 | } 165 | -------------------------------------------------------------------------------- /vsp.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "context" 5 | "crypto/ed25519" 6 | "encoding/base64" 7 | "fmt" 8 | "strings" 9 | 10 | "decred.org/dcrwallet/v2/errors" 11 | "github.com/planetdecred/dcrlibwallet/internal/vsp" 12 | ) 13 | 14 | // VSPClient loads or creates a VSP client instance for the specified host. 15 | func (wallet *Wallet) VSPClient(host string, pubKey []byte) (*vsp.Client, error) { 16 | wallet.vspClientsMu.Lock() 17 | defer wallet.vspClientsMu.Unlock() 18 | client, ok := wallet.vspClients[host] 19 | if ok { 20 | return client, nil 21 | } 22 | 23 | cfg := vsp.Config{ 24 | URL: host, 25 | PubKey: base64.StdEncoding.EncodeToString(pubKey), 26 | Dialer: nil, // optional, but consider providing a value 27 | Wallet: wallet.Internal(), 28 | } 29 | client, err := vsp.New(cfg) 30 | if err != nil { 31 | return nil, err 32 | } 33 | wallet.vspClients[host] = client 34 | return client, nil 35 | } 36 | 37 | // KnownVSPs returns a list of known VSPs. This list may be updated by calling 38 | // ReloadVSPList. This method is safe for concurrent access. 39 | func (mw *MultiWallet) KnownVSPs() []*VSP { 40 | mw.vspMu.RLock() 41 | defer mw.vspMu.RUnlock() 42 | return mw.vsps // TODO: Return a copy. 43 | } 44 | 45 | // SaveVSP marks a VSP as known and will be susbequently included as part of 46 | // known VSPs. 47 | func (mw *MultiWallet) SaveVSP(host string) (err error) { 48 | // check if host already exists 49 | vspDbData := mw.getVSPDBData() 50 | for _, savedHost := range vspDbData.SavedHosts { 51 | if savedHost == host { 52 | return fmt.Errorf("duplicate host %s", host) 53 | } 54 | } 55 | 56 | // validate host network 57 | info, err := vspInfo(host) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | // TODO: defaultVSPs() uses strings.Contains(network, vspInfo.Network). 63 | if info.Network != mw.NetType() { 64 | return fmt.Errorf("invalid net %s", info.Network) 65 | } 66 | 67 | vspDbData.SavedHosts = append(vspDbData.SavedHosts, host) 68 | mw.updateVSPDBData(vspDbData) 69 | 70 | mw.vspMu.Lock() 71 | mw.vsps = append(mw.vsps, &VSP{Host: host, VspInfoResponse: info}) 72 | mw.vspMu.Unlock() 73 | 74 | return 75 | } 76 | 77 | // LastUsedVSP returns the host of the last used VSP, as saved by the 78 | // SaveLastUsedVSP() method. 79 | func (mw *MultiWallet) LastUsedVSP() string { 80 | return mw.getVSPDBData().LastUsedVSP 81 | } 82 | 83 | // SaveLastUsedVSP saves the host of the last used VSP. 84 | func (mw *MultiWallet) SaveLastUsedVSP(host string) { 85 | vspDbData := mw.getVSPDBData() 86 | vspDbData.LastUsedVSP = host 87 | mw.updateVSPDBData(vspDbData) 88 | } 89 | 90 | type vspDbData struct { 91 | SavedHosts []string 92 | LastUsedVSP string 93 | } 94 | 95 | func (mw *MultiWallet) getVSPDBData() *vspDbData { 96 | vspDbData := new(vspDbData) 97 | mw.ReadUserConfigValue(KnownVSPsConfigKey, vspDbData) 98 | return vspDbData 99 | } 100 | 101 | func (mw *MultiWallet) updateVSPDBData(data *vspDbData) { 102 | mw.SaveUserConfigValue(KnownVSPsConfigKey, data) 103 | } 104 | 105 | // ReloadVSPList reloads the list of known VSPs. 106 | // This method makes multiple network calls; should be called in a goroutine 107 | // to prevent blocking the UI thread. 108 | func (mw *MultiWallet) ReloadVSPList(ctx context.Context) { 109 | log.Debugf("Reloading list of known VSPs") 110 | defer log.Debugf("Reloaded list of known VSPs") 111 | 112 | vspDbData := mw.getVSPDBData() 113 | vspList := make(map[string]*VspInfoResponse) 114 | for _, host := range vspDbData.SavedHosts { 115 | vspInfo, err := vspInfo(host) 116 | if err != nil { 117 | // User saved this VSP. Log an error message. 118 | log.Errorf("get vsp info error for %s: %v", host, err) 119 | } else { 120 | vspList[host] = vspInfo 121 | } 122 | if ctx.Err() != nil { 123 | return // context canceled, abort 124 | } 125 | } 126 | 127 | otherVSPHosts, err := defaultVSPs(mw.NetType()) 128 | if err != nil { 129 | log.Debugf("get default vsp list error: %v", err) 130 | } 131 | for _, host := range otherVSPHosts { 132 | if _, wasAdded := vspList[host]; wasAdded { 133 | continue 134 | } 135 | vspInfo, err := vspInfo(host) 136 | if err != nil { 137 | log.Debugf("vsp info error for %s: %v\n", host, err) // debug only, user didn't request this VSP 138 | } else { 139 | vspList[host] = vspInfo 140 | } 141 | if ctx.Err() != nil { 142 | return // context canceled, abort 143 | } 144 | } 145 | 146 | mw.vspMu.Lock() 147 | mw.vsps = make([]*VSP, 0, len(vspList)) 148 | for host, info := range vspList { 149 | mw.vsps = append(mw.vsps, &VSP{Host: host, VspInfoResponse: info}) 150 | } 151 | mw.vspMu.Unlock() 152 | } 153 | 154 | func vspInfo(vspHost string) (*VspInfoResponse, error) { 155 | vspInfoResponse := new(VspInfoResponse) 156 | resp, respBytes, err := HttpGet(vspHost+"/api/v3/vspinfo", vspInfoResponse) 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | // Validate server response. 162 | sigStr := resp.Header.Get("VSP-Server-Signature") 163 | sig, err := base64.StdEncoding.DecodeString(sigStr) 164 | if err != nil { 165 | return nil, fmt.Errorf("error validating VSP signature: %v", err) 166 | } 167 | if !ed25519.Verify(vspInfoResponse.PubKey, respBytes, sig) { 168 | return nil, errors.New("bad signature from VSP") 169 | } 170 | 171 | return vspInfoResponse, nil 172 | } 173 | 174 | // defaultVSPs returns a list of known VSPs. 175 | func defaultVSPs(network string) ([]string, error) { 176 | var vspInfoResponse map[string]*VspInfoResponse 177 | _, _, err := HttpGet("https://api.decred.org/?c=vsp", &vspInfoResponse) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | // The above API does not return the pubKeys for the 183 | // VSPs. Only return the host since we'll still need 184 | // to make another API call to get the VSP pubKeys. 185 | vsps := make([]string, 0) 186 | for url, vspInfo := range vspInfoResponse { 187 | if strings.Contains(network, vspInfo.Network) { 188 | vsps = append(vsps, "https://"+url) 189 | } 190 | } 191 | return vsps, nil 192 | } 193 | -------------------------------------------------------------------------------- /wallet.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | "sync" 10 | "time" 11 | 12 | "decred.org/dcrwallet/v2/errors" 13 | w "decred.org/dcrwallet/v2/wallet" 14 | "decred.org/dcrwallet/v2/walletseed" 15 | "github.com/decred/dcrd/chaincfg/v3" 16 | "github.com/planetdecred/dcrlibwallet/internal/loader" 17 | "github.com/planetdecred/dcrlibwallet/internal/vsp" 18 | "github.com/planetdecred/dcrlibwallet/walletdata" 19 | ) 20 | 21 | type Wallet struct { 22 | ID int `storm:"id,increment"` 23 | Name string `storm:"unique"` 24 | CreatedAt time.Time `storm:"index"` 25 | DbDriver string 26 | EncryptedSeed []byte 27 | IsRestored bool 28 | HasDiscoveredAccounts bool 29 | PrivatePassphraseType int32 30 | 31 | chainParams *chaincfg.Params 32 | dataDir string 33 | loader *loader.Loader 34 | walletDataDB *walletdata.DB 35 | 36 | synced bool 37 | syncing bool 38 | waitingForHeaders bool 39 | 40 | shuttingDown chan bool 41 | cancelFuncs []context.CancelFunc 42 | cancelAccountMixer context.CancelFunc 43 | 44 | cancelAutoTicketBuyerMu sync.Mutex 45 | cancelAutoTicketBuyer context.CancelFunc 46 | 47 | vspClientsMu sync.Mutex 48 | vspClients map[string]*vsp.Client 49 | 50 | // setUserConfigValue saves the provided key-value pair to a config database. 51 | // This function is ideally assigned when the `wallet.prepare` method is 52 | // called from a MultiWallet instance. 53 | setUserConfigValue configSaveFn 54 | 55 | // readUserConfigValue returns the previously saved value for the provided 56 | // key from a config database. Returns nil if the key wasn't previously set. 57 | // This function is ideally assigned when the `wallet.prepare` method is 58 | // called from a MultiWallet instance. 59 | readUserConfigValue configReadFn 60 | } 61 | 62 | // prepare gets a wallet ready for use by opening the transactions index database 63 | // and initializing the wallet loader which can be used subsequently to create, 64 | // load and unload the wallet. 65 | func (wallet *Wallet) prepare(rootDir string, chainParams *chaincfg.Params, 66 | setUserConfigValueFn configSaveFn, readUserConfigValueFn configReadFn) (err error) { 67 | 68 | wallet.chainParams = chainParams 69 | wallet.dataDir = filepath.Join(rootDir, strconv.Itoa(wallet.ID)) 70 | wallet.vspClients = make(map[string]*vsp.Client) 71 | wallet.setUserConfigValue = setUserConfigValueFn 72 | wallet.readUserConfigValue = readUserConfigValueFn 73 | 74 | // open database for indexing transactions for faster loading 75 | walletDataDBPath := filepath.Join(wallet.dataDir, walletdata.DbName) 76 | oldTxDBPath := filepath.Join(wallet.dataDir, walletdata.OldDbName) 77 | if exists, _ := fileExists(oldTxDBPath); exists { 78 | moveFile(oldTxDBPath, walletDataDBPath) 79 | } 80 | wallet.walletDataDB, err = walletdata.Initialize(walletDataDBPath, chainParams, &Transaction{}) 81 | if err != nil { 82 | log.Error(err.Error()) 83 | return err 84 | } 85 | 86 | // init loader 87 | wallet.loader = initWalletLoader(wallet.chainParams, wallet.dataDir, wallet.DbDriver) 88 | 89 | // init cancelFuncs slice to hold cancel functions for long running 90 | // operations and start go routine to listen for shutdown signal 91 | wallet.cancelFuncs = make([]context.CancelFunc, 0) 92 | wallet.shuttingDown = make(chan bool) 93 | go func() { 94 | <-wallet.shuttingDown 95 | for _, cancel := range wallet.cancelFuncs { 96 | cancel() 97 | } 98 | }() 99 | 100 | return nil 101 | } 102 | 103 | func (wallet *Wallet) Shutdown() { 104 | // Trigger shuttingDown signal to cancel all contexts created with 105 | // `wallet.shutdownContext()` or `wallet.shutdownContextWithCancel()`. 106 | wallet.shuttingDown <- true 107 | 108 | if _, loaded := wallet.loader.LoadedWallet(); loaded { 109 | err := wallet.loader.UnloadWallet() 110 | if err != nil { 111 | log.Errorf("Failed to close wallet: %v", err) 112 | } else { 113 | log.Info("Closed wallet") 114 | } 115 | } 116 | 117 | if wallet.walletDataDB != nil { 118 | err := wallet.walletDataDB.Close() 119 | if err != nil { 120 | log.Errorf("tx db closed with error: %v", err) 121 | } else { 122 | log.Info("tx db closed successfully") 123 | } 124 | } 125 | } 126 | 127 | // WalletCreationTimeInMillis returns the wallet creation time for new 128 | // wallets. Restored wallets would return an error. 129 | func (wallet *Wallet) WalletCreationTimeInMillis() (int64, error) { 130 | if wallet.IsRestored { 131 | return 0, errors.New(ErrWalletIsRestored) 132 | } 133 | 134 | return wallet.CreatedAt.UnixNano() / int64(time.Millisecond), nil 135 | } 136 | 137 | func (wallet *Wallet) NetType() string { 138 | return wallet.chainParams.Name 139 | } 140 | 141 | func (wallet *Wallet) Internal() *w.Wallet { 142 | lw, _ := wallet.loader.LoadedWallet() 143 | return lw 144 | } 145 | 146 | func (wallet *Wallet) WalletExists() (bool, error) { 147 | return wallet.loader.WalletExists() 148 | } 149 | 150 | func (wallet *Wallet) createWallet(privatePassphrase, seedMnemonic string) error { 151 | log.Info("Creating Wallet") 152 | if len(seedMnemonic) == 0 { 153 | return errors.New(ErrEmptySeed) 154 | } 155 | 156 | pubPass := []byte(w.InsecurePubPassphrase) 157 | privPass := []byte(privatePassphrase) 158 | seed, err := walletseed.DecodeUserInput(seedMnemonic) 159 | if err != nil { 160 | log.Error(err) 161 | return err 162 | } 163 | 164 | _, err = wallet.loader.CreateNewWallet(wallet.shutdownContext(), pubPass, privPass, seed) 165 | if err != nil { 166 | log.Error(err) 167 | return err 168 | } 169 | 170 | log.Info("Created Wallet") 171 | return nil 172 | } 173 | 174 | func (wallet *Wallet) createWatchingOnlyWallet(extendedPublicKey string) error { 175 | pubPass := []byte(w.InsecurePubPassphrase) 176 | 177 | _, err := wallet.loader.CreateWatchingOnlyWallet(wallet.shutdownContext(), extendedPublicKey, pubPass) 178 | if err != nil { 179 | log.Error(err) 180 | return err 181 | } 182 | 183 | log.Info("Created Watching Only Wallet") 184 | return nil 185 | } 186 | 187 | func (wallet *Wallet) IsWatchingOnlyWallet() bool { 188 | if w, ok := wallet.loader.LoadedWallet(); ok { 189 | return w.WatchingOnly() 190 | } 191 | 192 | return false 193 | } 194 | 195 | func (wallet *Wallet) openWallet() error { 196 | pubPass := []byte(w.InsecurePubPassphrase) 197 | 198 | _, err := wallet.loader.OpenExistingWallet(wallet.shutdownContext(), pubPass) 199 | if err != nil { 200 | log.Error(err) 201 | return translateError(err) 202 | } 203 | 204 | return nil 205 | } 206 | 207 | func (wallet *Wallet) WalletOpened() bool { 208 | return wallet.Internal() != nil 209 | } 210 | 211 | func (wallet *Wallet) UnlockWallet(privPass []byte) error { 212 | loadedWallet, ok := wallet.loader.LoadedWallet() 213 | if !ok { 214 | return fmt.Errorf("wallet has not been loaded") 215 | } 216 | 217 | ctx, _ := wallet.shutdownContextWithCancel() 218 | err := loadedWallet.Unlock(ctx, privPass, nil) 219 | if err != nil { 220 | return translateError(err) 221 | } 222 | 223 | return nil 224 | } 225 | 226 | func (wallet *Wallet) LockWallet() { 227 | if wallet.IsAccountMixerActive() { 228 | log.Error("LockWallet ignored due to active account mixer") 229 | return 230 | } 231 | 232 | if !wallet.Internal().Locked() { 233 | wallet.Internal().Lock() 234 | } 235 | } 236 | 237 | func (wallet *Wallet) IsLocked() bool { 238 | return wallet.Internal().Locked() 239 | } 240 | 241 | func (wallet *Wallet) changePrivatePassphrase(oldPass []byte, newPass []byte) error { 242 | defer func() { 243 | for i := range oldPass { 244 | oldPass[i] = 0 245 | } 246 | 247 | for i := range newPass { 248 | newPass[i] = 0 249 | } 250 | }() 251 | 252 | err := wallet.Internal().ChangePrivatePassphrase(wallet.shutdownContext(), oldPass, newPass) 253 | if err != nil { 254 | return translateError(err) 255 | } 256 | return nil 257 | } 258 | 259 | func (wallet *Wallet) deleteWallet(privatePassphrase []byte) error { 260 | defer func() { 261 | for i := range privatePassphrase { 262 | privatePassphrase[i] = 0 263 | } 264 | }() 265 | 266 | if _, loaded := wallet.loader.LoadedWallet(); !loaded { 267 | return errors.New(ErrWalletNotLoaded) 268 | } 269 | 270 | if !wallet.IsWatchingOnlyWallet() { 271 | err := wallet.Internal().Unlock(wallet.shutdownContext(), privatePassphrase, nil) 272 | if err != nil { 273 | return translateError(err) 274 | } 275 | wallet.Internal().Lock() 276 | } 277 | 278 | wallet.Shutdown() 279 | 280 | log.Info("Deleting Wallet") 281 | return os.RemoveAll(wallet.dataDir) 282 | } 283 | 284 | // DecryptSeed decrypts wallet.EncryptedSeed using privatePassphrase 285 | func (wallet *Wallet) DecryptSeed(privatePassphrase []byte) (string, error) { 286 | if wallet.EncryptedSeed == nil { 287 | return "", errors.New(ErrInvalid) 288 | } 289 | 290 | return decryptWalletSeed(privatePassphrase, wallet.EncryptedSeed) 291 | } 292 | 293 | // AccountXPubMatches checks if the xpub of the provided account matches the 294 | // provided legacy or SLIP0044 xpub. While both the legacy and SLIP0044 xpubs 295 | // will be checked for watch-only wallets, other wallets will only check the 296 | // xpub that matches the coin type key used by the wallet. 297 | func (wallet *Wallet) AccountXPubMatches(account uint32, legacyXPub, slip044XPub string) (bool, error) { 298 | ctx := wallet.shutdownContext() 299 | 300 | acctXPubKey, err := wallet.Internal().AccountXpub(ctx, account) 301 | if err != nil { 302 | return false, err 303 | } 304 | acctXPub := acctXPubKey.String() 305 | 306 | if wallet.IsWatchingOnlyWallet() { 307 | // Coin type info isn't saved for watch-only wallets, so check 308 | // against both legacy and SLIP0044 coin types. 309 | return acctXPub == legacyXPub || acctXPub == slip044XPub, nil 310 | } 311 | 312 | cointype, err := wallet.Internal().CoinType(ctx) 313 | if err != nil { 314 | return false, err 315 | } 316 | 317 | if cointype == wallet.chainParams.LegacyCoinType { 318 | return acctXPub == legacyXPub, nil 319 | } else { 320 | return acctXPub == slip044XPub, nil 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /wallet_config.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | import ( 4 | "decred.org/dcrwallet/v2/errors" 5 | "github.com/asdine/storm" 6 | ) 7 | 8 | const ( 9 | AccountMixerConfigSet = "account_mixer_config_set" 10 | AccountMixerMixedAccount = "account_mixer_mixed_account" 11 | AccountMixerUnmixedAccount = "account_mixer_unmixed_account" 12 | AccountMixerMixTxChange = "account_mixer_mix_tx_change" 13 | ) 14 | 15 | func (wallet *Wallet) SaveUserConfigValue(key string, value interface{}) { 16 | if wallet.setUserConfigValue == nil { 17 | log.Errorf("call wallet.prepare before setting wallet config values") 18 | return 19 | } 20 | 21 | err := wallet.setUserConfigValue(key, value) 22 | if err != nil { 23 | log.Errorf("error setting config value for key: %s, error: %v", key, err) 24 | } 25 | } 26 | 27 | func (wallet *Wallet) ReadUserConfigValue(key string, valueOut interface{}) error { 28 | if wallet.setUserConfigValue == nil { 29 | log.Errorf("call wallet.prepare before reading wallet config values") 30 | return errors.New(ErrFailedPrecondition) 31 | } 32 | 33 | err := wallet.readUserConfigValue(false, key, valueOut) 34 | if err != nil && err != storm.ErrNotFound { 35 | log.Errorf("error reading config value for key: %s, error: %v", key, err) 36 | } 37 | return err 38 | } 39 | 40 | func (wallet *Wallet) SetBoolConfigValueForKey(key string, value bool) { 41 | wallet.SaveUserConfigValue(key, value) 42 | } 43 | 44 | func (wallet *Wallet) SetDoubleConfigValueForKey(key string, value float64) { 45 | wallet.SaveUserConfigValue(key, value) 46 | } 47 | 48 | func (wallet *Wallet) SetIntConfigValueForKey(key string, value int) { 49 | wallet.SaveUserConfigValue(key, value) 50 | } 51 | 52 | func (wallet *Wallet) SetInt32ConfigValueForKey(key string, value int32) { 53 | wallet.SaveUserConfigValue(key, value) 54 | } 55 | 56 | func (wallet *Wallet) SetLongConfigValueForKey(key string, value int64) { 57 | wallet.SaveUserConfigValue(key, value) 58 | } 59 | 60 | func (wallet *Wallet) SetStringConfigValueForKey(key, value string) { 61 | wallet.SaveUserConfigValue(key, value) 62 | } 63 | 64 | func (wallet *Wallet) ReadBoolConfigValueForKey(key string, defaultValue bool) (valueOut bool) { 65 | if err := wallet.ReadUserConfigValue(key, &valueOut); err == storm.ErrNotFound { 66 | valueOut = defaultValue 67 | } 68 | return 69 | } 70 | 71 | func (wallet *Wallet) ReadDoubleConfigValueForKey(key string, defaultValue float64) (valueOut float64) { 72 | if err := wallet.ReadUserConfigValue(key, &valueOut); err == storm.ErrNotFound { 73 | valueOut = defaultValue 74 | } 75 | return 76 | } 77 | 78 | func (wallet *Wallet) ReadIntConfigValueForKey(key string, defaultValue int) (valueOut int) { 79 | if err := wallet.ReadUserConfigValue(key, &valueOut); err == storm.ErrNotFound { 80 | valueOut = defaultValue 81 | } 82 | return 83 | } 84 | 85 | func (wallet *Wallet) ReadInt32ConfigValueForKey(key string, defaultValue int32) (valueOut int32) { 86 | if err := wallet.ReadUserConfigValue(key, &valueOut); err == storm.ErrNotFound { 87 | valueOut = defaultValue 88 | } 89 | return 90 | } 91 | 92 | func (wallet *Wallet) ReadLongConfigValueForKey(key string, defaultValue int64) (valueOut int64) { 93 | if err := wallet.ReadUserConfigValue(key, &valueOut); err == storm.ErrNotFound { 94 | valueOut = defaultValue 95 | } 96 | return 97 | } 98 | 99 | func (wallet *Wallet) ReadStringConfigValueForKey(key string, defaultValue string) (valueOut string) { 100 | if err := wallet.ReadUserConfigValue(key, &valueOut); err == storm.ErrNotFound { 101 | valueOut = defaultValue 102 | } 103 | return 104 | } 105 | -------------------------------------------------------------------------------- /walletdata/db.go: -------------------------------------------------------------------------------- 1 | package walletdata 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/asdine/storm" 8 | "github.com/decred/dcrd/chaincfg/v3" 9 | bolt "go.etcd.io/bbolt" 10 | ) 11 | 12 | const ( 13 | DbName = "walletData.db" 14 | OldDbName = "tx.db" 15 | 16 | TxBucketName = "TxIndexInfo" 17 | KeyDbVersion = "DbVersion" 18 | 19 | // TxDbVersion is necessary to force re-indexing if changes are made to the structure of data being stored. 20 | // Increment this version number if db structure changes such that client apps need to re-index. 21 | TxDbVersion uint32 = 3 22 | ) 23 | 24 | type DB struct { 25 | walletDataDB *storm.DB 26 | chainParams *chaincfg.Params 27 | Close func() error 28 | } 29 | 30 | // Initialize opens the existing storm db at `dbPath` 31 | // and checks the database version for compatibility. 32 | // If there is a version mismatch or the db does not exist at `dbPath`, 33 | // a new db is created and the current db version number saved to the db. 34 | func Initialize(dbPath string, chainParams *chaincfg.Params, txData interface{}) (*DB, error) { 35 | walletDataDB, err := openOrCreateDB(dbPath) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | walletDataDB, err = ensureTxDatabaseVersion(walletDataDB, dbPath, txData) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | // init bucket for saving/reading transaction objects 46 | err = walletDataDB.Init(txData) 47 | if err != nil { 48 | return nil, fmt.Errorf("error initializing tx bucket for wallet: %s", err.Error()) 49 | } 50 | 51 | return &DB{ 52 | walletDataDB, 53 | chainParams, 54 | walletDataDB.Close, 55 | }, nil 56 | } 57 | 58 | func openOrCreateDB(dbPath string) (*storm.DB, error) { 59 | var isNewDbFile bool 60 | 61 | // first check if db file exists at dbPath, if not we'll need to create it and set the db version 62 | if _, err := os.Stat(dbPath); err != nil { 63 | if os.IsNotExist(err) { 64 | isNewDbFile = true 65 | } else { 66 | return nil, fmt.Errorf("error checking tx index database file: %s", err.Error()) 67 | } 68 | } 69 | 70 | walletDataDB, err := storm.Open(dbPath) 71 | if err != nil { 72 | switch err { 73 | case bolt.ErrTimeout: 74 | // timeout error occurs if storm fails to acquire a lock on the database file 75 | return nil, fmt.Errorf("wallet data database is in use by another process") 76 | default: 77 | return nil, fmt.Errorf("error opening wallet data database: %s", err.Error()) 78 | } 79 | } 80 | 81 | if isNewDbFile { 82 | err = walletDataDB.Set(TxBucketName, KeyDbVersion, TxDbVersion) 83 | if err != nil { 84 | os.RemoveAll(dbPath) 85 | return nil, fmt.Errorf("error initializing wallet data db: %s", err.Error()) 86 | } 87 | } 88 | 89 | return walletDataDB, nil 90 | } 91 | 92 | // ensureTxDatabaseVersion checks the version of the existing db against `TxDbVersion`. 93 | // If there's a difference, the current wallet data db file is deleted and a new one created. 94 | func ensureTxDatabaseVersion(walletDataDB *storm.DB, dbPath string, txData interface{}) (*storm.DB, error) { 95 | var currentDbVersion uint32 96 | err := walletDataDB.Get(TxBucketName, KeyDbVersion, ¤tDbVersion) 97 | if err != nil && err != storm.ErrNotFound { 98 | // ignore key not found errors as earlier db versions did not set a version number in the db. 99 | return nil, fmt.Errorf("error checking wallet data database version: %s", err.Error()) 100 | } 101 | 102 | if currentDbVersion != TxDbVersion { 103 | if err = walletDataDB.Drop(txData); err != nil { 104 | return nil, fmt.Errorf("error deleting outdated wallet data database: %s", err.Error()) 105 | } 106 | 107 | if err = walletDataDB.Set(TxBucketName, KeyDbVersion, TxDbVersion); err != nil { 108 | return nil, fmt.Errorf("error updating tx db version: %s", err.Error()) 109 | } 110 | 111 | return walletDataDB, walletDataDB.Set(TxBucketName, KeyEndBlock, 0) // reset tx index 112 | } 113 | 114 | return walletDataDB, nil 115 | } 116 | -------------------------------------------------------------------------------- /walletdata/filter.go: -------------------------------------------------------------------------------- 1 | package walletdata 2 | 3 | import ( 4 | "github.com/asdine/storm" 5 | "github.com/asdine/storm/q" 6 | "github.com/planetdecred/dcrlibwallet/txhelper" 7 | ) 8 | 9 | const ( 10 | TxFilterAll int32 = 0 11 | TxFilterSent int32 = 1 12 | TxFilterReceived int32 = 2 13 | TxFilterTransferred int32 = 3 14 | TxFilterStaking int32 = 4 15 | TxFilterCoinBase int32 = 5 16 | TxFilterRegular int32 = 6 17 | TxFilterMixed int32 = 7 18 | TxFilterVoted int32 = 8 19 | TxFilterRevoked int32 = 9 20 | TxFilterImmature int32 = 10 21 | TxFilterLive int32 = 11 22 | TxFilterUnmined int32 = 12 23 | TxFilterExpired int32 = 13 24 | TxFilterTickets int32 = 14 25 | ) 26 | 27 | func (db *DB) prepareTxQuery(txFilter, requiredConfirmations, bestBlock int32) (query storm.Query) { 28 | // tickets with block height less than this are matured. 29 | maturityBlock := bestBlock - int32(db.chainParams.TicketMaturity) 30 | 31 | // tickets with block height less than this are expired. 32 | expiryBlock := bestBlock - int32(db.chainParams.TicketMaturity+uint16(db.chainParams.TicketExpiry)) 33 | 34 | switch txFilter { 35 | case TxFilterSent: 36 | query = db.walletDataDB.Select( 37 | q.Eq("Type", txhelper.TxTypeRegular), 38 | q.Eq("Direction", txhelper.TxDirectionSent), 39 | ) 40 | case TxFilterReceived: 41 | query = db.walletDataDB.Select( 42 | q.Eq("Type", txhelper.TxTypeRegular), 43 | q.Eq("Direction", txhelper.TxDirectionReceived), 44 | ) 45 | case TxFilterTransferred: 46 | query = db.walletDataDB.Select( 47 | q.Eq("Type", txhelper.TxTypeRegular), 48 | q.Eq("Direction", txhelper.TxDirectionTransferred), 49 | ) 50 | case TxFilterStaking: 51 | query = db.walletDataDB.Select( 52 | q.Or( 53 | q.Eq("Type", txhelper.TxTypeTicketPurchase), 54 | q.Eq("Type", txhelper.TxTypeVote), 55 | q.Eq("Type", txhelper.TxTypeRevocation), 56 | ), 57 | ) 58 | case TxFilterCoinBase: 59 | query = db.walletDataDB.Select( 60 | q.Eq("Type", txhelper.TxTypeCoinBase), 61 | ) 62 | case TxFilterRegular: 63 | query = db.walletDataDB.Select( 64 | q.Eq("Type", txhelper.TxTypeRegular), 65 | ) 66 | case TxFilterMixed: 67 | query = db.walletDataDB.Select( 68 | q.Eq("Type", txhelper.TxTypeMixed), 69 | ) 70 | case TxFilterVoted: 71 | query = db.walletDataDB.Select( 72 | q.Eq("Type", txhelper.TxTypeVote), 73 | ) 74 | case TxFilterRevoked: 75 | query = db.walletDataDB.Select( 76 | q.Eq("Type", txhelper.TxTypeRevocation), 77 | ) 78 | case TxFilterImmature: 79 | query = db.walletDataDB.Select( 80 | q.Eq("Type", txhelper.TxTypeTicketPurchase), 81 | q.And( 82 | q.Gt("BlockHeight", maturityBlock), 83 | ), 84 | ) 85 | case TxFilterLive: 86 | query = db.walletDataDB.Select( 87 | q.Eq("Type", txhelper.TxTypeTicketPurchase), 88 | q.Eq("TicketSpender", ""), // not spent by a vote or revoke 89 | q.Gt("BlockHeight", 0), // mined 90 | q.Lte("BlockHeight", maturityBlock), // must be matured 91 | q.Gt("BlockHeight", expiryBlock), // not expired 92 | ) 93 | case TxFilterUnmined: 94 | query = db.walletDataDB.Select( 95 | q.Eq("Type", txhelper.TxTypeTicketPurchase), 96 | q.Or( 97 | q.Eq("BlockHeight", -1), 98 | ), 99 | ) 100 | case TxFilterExpired: 101 | query = db.walletDataDB.Select( 102 | q.Eq("Type", txhelper.TxTypeTicketPurchase), 103 | q.Eq("TicketSpender", ""), // not spent by a vote or revoke 104 | q.Gt("BlockHeight", 0), // mined 105 | q.Lte("BlockHeight", expiryBlock), 106 | ) 107 | case TxFilterTickets: 108 | query = db.walletDataDB.Select( 109 | q.Eq("Type", txhelper.TxTypeTicketPurchase), 110 | ) 111 | default: 112 | query = db.walletDataDB.Select( 113 | q.True(), 114 | ) 115 | } 116 | 117 | return 118 | } 119 | -------------------------------------------------------------------------------- /walletdata/read.go: -------------------------------------------------------------------------------- 1 | package walletdata 2 | 3 | import ( 4 | "github.com/asdine/storm" 5 | "github.com/asdine/storm/q" 6 | ) 7 | 8 | const MaxReOrgBlocks = 6 9 | 10 | // ReadIndexingStartBlock checks if the end block height was saved from last indexing operation. 11 | // If so, the end block height - MaxReOrgBlocks is returned. 12 | // Otherwise, 0 is returned to begin indexing from height 0. 13 | func (db *DB) ReadIndexingStartBlock() (int32, error) { 14 | var startBlockHeight int32 15 | err := db.walletDataDB.Get(TxBucketName, KeyEndBlock, &startBlockHeight) 16 | if err != nil && err != storm.ErrNotFound { 17 | return 0, err 18 | } 19 | 20 | startBlockHeight -= MaxReOrgBlocks 21 | if startBlockHeight < 0 { 22 | startBlockHeight = 0 23 | } 24 | return startBlockHeight, nil 25 | } 26 | 27 | // Read queries the db for `limit` count transactions that match the specified `txFilter` 28 | // starting from the specified `offset`; and saves the transactions found to the received `transactions` object. 29 | // `transactions` should be a pointer to a slice of Transaction objects. 30 | func (db *DB) Read(offset, limit, txFilter int32, newestFirst bool, requiredConfirmations, bestBlock int32, transactions interface{}) error { 31 | query := db.prepareTxQuery(txFilter, requiredConfirmations, bestBlock) 32 | if offset > 0 { 33 | query = query.Skip(int(offset)) 34 | } 35 | if limit > 0 { 36 | query = query.Limit(int(limit)) 37 | } 38 | if newestFirst { 39 | query = query.OrderBy("Timestamp").Reverse() 40 | } else { 41 | query = query.OrderBy("Timestamp") 42 | } 43 | 44 | err := query.Find(transactions) 45 | if err != nil && err != storm.ErrNotFound { 46 | return err 47 | } 48 | return nil 49 | } 50 | 51 | // Count queries the db for transactions of the `txObj` type 52 | // to return the number of records matching the specified `txFilter`. 53 | func (db *DB) Count(txFilter int32, requiredConfirmations, bestBlock int32, txObj interface{}) (int, error) { 54 | query := db.prepareTxQuery(txFilter, requiredConfirmations, bestBlock) 55 | 56 | count, err := query.Count(txObj) 57 | if err != nil { 58 | return -1, err 59 | } 60 | 61 | return count, nil 62 | } 63 | 64 | func (db *DB) Find(matcher q.Matcher, transactions interface{}) error { 65 | query := db.walletDataDB.Select(matcher) 66 | 67 | err := query.Find(transactions) 68 | if err != nil && err != storm.ErrNotFound { 69 | return err 70 | } 71 | return nil 72 | } 73 | 74 | func (db *DB) FindOne(fieldName string, value interface{}, obj interface{}) error { 75 | return db.walletDataDB.One(fieldName, value, obj) 76 | } 77 | 78 | func (db *DB) FindLast(fieldName string, value interface{}, txObj interface{}) error { 79 | query := db.walletDataDB.Select(q.Eq(fieldName, value)).OrderBy("Timestamp").Reverse() 80 | return query.First(txObj) 81 | } 82 | 83 | func (db *DB) FindAll(fieldName string, value interface{}, txObj interface{}) error { 84 | return db.walletDataDB.Find(fieldName, value, txObj) 85 | } 86 | -------------------------------------------------------------------------------- /walletdata/save.go: -------------------------------------------------------------------------------- 1 | package walletdata 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "decred.org/dcrwallet/v2/errors" 8 | "github.com/asdine/storm" 9 | ) 10 | 11 | const KeyEndBlock = "EndBlock" 12 | 13 | // SaveOrUpdate saves a transaction to the database and would overwrite 14 | // if a transaction with same hash exists 15 | func (db *DB) SaveOrUpdate(emptyTxPointer, record interface{}) (overwritten bool, err error) { 16 | v := reflect.ValueOf(record) 17 | txHash := reflect.Indirect(v).FieldByName("Hash").String() 18 | err = db.walletDataDB.One("Hash", txHash, emptyTxPointer) 19 | if err != nil && err != storm.ErrNotFound { 20 | err = errors.Errorf("error checking if record was already indexed: %s", err.Error()) 21 | return 22 | } 23 | 24 | v2 := reflect.ValueOf(emptyTxPointer) 25 | timestamp := reflect.Indirect(v2).FieldByName("Timestamp").Int() 26 | if timestamp > 0 { 27 | overwritten = true 28 | // delete old record before saving new (if it exists) 29 | db.walletDataDB.DeleteStruct(emptyTxPointer) 30 | } 31 | 32 | err = db.walletDataDB.Save(record) 33 | return 34 | } 35 | 36 | func (db *DB) SaveOrUpdateVspdRecord(emptyTxPointer, record interface{}) (updated bool, err error) { 37 | v := reflect.ValueOf(record) 38 | txHash := reflect.Indirect(v).FieldByName("Hash").String() 39 | err = db.walletDataDB.One("Hash", txHash, emptyTxPointer) 40 | if err != nil && err != storm.ErrNotFound { 41 | err = errors.Errorf("error checking if record was already indexed: %s", err.Error()) 42 | return 43 | } 44 | if err == storm.ErrNotFound { 45 | err = db.walletDataDB.Save(record) 46 | return 47 | } 48 | 49 | updated = true 50 | err = db.walletDataDB.Update(record) 51 | return 52 | } 53 | 54 | func (db *DB) LastIndexPoint() (int32, error) { 55 | var endBlockHeight int32 56 | err := db.walletDataDB.Get(TxBucketName, KeyEndBlock, &endBlockHeight) 57 | if err != nil && err != storm.ErrNotFound { 58 | return 0, err 59 | } 60 | 61 | return endBlockHeight, nil 62 | } 63 | 64 | func (db *DB) SaveLastIndexPoint(endBlockHeight int32) error { 65 | err := db.walletDataDB.Set(TxBucketName, KeyEndBlock, &endBlockHeight) 66 | if err != nil { 67 | return fmt.Errorf("error setting block height for last indexed tx: %s", err.Error()) 68 | } 69 | return nil 70 | } 71 | 72 | func (db *DB) ClearSavedTransactions(emptyTxPointer interface{}) error { 73 | err := db.walletDataDB.Drop(emptyTxPointer) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | return db.SaveLastIndexPoint(0) 79 | } 80 | -------------------------------------------------------------------------------- /wallets.go: -------------------------------------------------------------------------------- 1 | package dcrlibwallet 2 | 3 | func (mw *MultiWallet) AllWallets() (wallets []*Wallet) { 4 | for _, wallet := range mw.wallets { 5 | wallets = append(wallets, wallet) 6 | } 7 | return wallets 8 | } 9 | 10 | func (mw *MultiWallet) WalletsIterator() *WalletsIterator { 11 | return &WalletsIterator{ 12 | currentIndex: 0, 13 | wallets: mw.AllWallets(), 14 | } 15 | } 16 | 17 | func (walletsIterator *WalletsIterator) Next() *Wallet { 18 | if walletsIterator.currentIndex < len(walletsIterator.wallets) { 19 | wallet := walletsIterator.wallets[walletsIterator.currentIndex] 20 | walletsIterator.currentIndex++ 21 | return wallet 22 | } 23 | 24 | return nil 25 | } 26 | 27 | func (walletsIterator *WalletsIterator) Reset() { 28 | walletsIterator.currentIndex = 0 29 | } 30 | -------------------------------------------------------------------------------- /wordlist.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-2019 The Decred developers 3 | * 4 | * Permission to use, copy, modify, and distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | package dcrlibwallet 18 | 19 | import "strings" 20 | 21 | func PGPWordList() []string { 22 | return strings.Split(AlternatingWords, "\n") 23 | } 24 | 25 | const AlternatingWords = `aardvark 26 | adroitness 27 | absurd 28 | adviser 29 | accrue 30 | aftermath 31 | acme 32 | aggregate 33 | adrift 34 | alkali 35 | adult 36 | almighty 37 | afflict 38 | amulet 39 | ahead 40 | amusement 41 | aimless 42 | antenna 43 | Algol 44 | applicant 45 | allow 46 | Apollo 47 | alone 48 | armistice 49 | ammo 50 | article 51 | ancient 52 | asteroid 53 | apple 54 | Atlantic 55 | artist 56 | atmosphere 57 | assume 58 | autopsy 59 | Athens 60 | Babylon 61 | atlas 62 | backwater 63 | Aztec 64 | barbecue 65 | baboon 66 | belowground 67 | backfield 68 | bifocals 69 | backward 70 | bodyguard 71 | banjo 72 | bookseller 73 | beaming 74 | borderline 75 | bedlamp 76 | bottomless 77 | beehive 78 | Bradbury 79 | beeswax 80 | bravado 81 | befriend 82 | Brazilian 83 | Belfast 84 | breakaway 85 | berserk 86 | Burlington 87 | billiard 88 | businessman 89 | bison 90 | butterfat 91 | blackjack 92 | Camelot 93 | blockade 94 | candidate 95 | blowtorch 96 | cannonball 97 | bluebird 98 | Capricorn 99 | bombast 100 | caravan 101 | bookshelf 102 | caretaker 103 | brackish 104 | celebrate 105 | breadline 106 | cellulose 107 | breakup 108 | certify 109 | brickyard 110 | chambermaid 111 | briefcase 112 | Cherokee 113 | Burbank 114 | Chicago 115 | button 116 | clergyman 117 | buzzard 118 | coherence 119 | cement 120 | combustion 121 | chairlift 122 | commando 123 | chatter 124 | company 125 | checkup 126 | component 127 | chisel 128 | concurrent 129 | choking 130 | confidence 131 | chopper 132 | conformist 133 | Christmas 134 | congregate 135 | clamshell 136 | consensus 137 | classic 138 | consulting 139 | classroom 140 | corporate 141 | cleanup 142 | corrosion 143 | clockwork 144 | councilman 145 | cobra 146 | crossover 147 | commence 148 | crucifix 149 | concert 150 | cumbersome 151 | cowbell 152 | customer 153 | crackdown 154 | Dakota 155 | cranky 156 | decadence 157 | crowfoot 158 | December 159 | crucial 160 | decimal 161 | crumpled 162 | designing 163 | crusade 164 | detector 165 | cubic 166 | detergent 167 | dashboard 168 | determine 169 | deadbolt 170 | dictator 171 | deckhand 172 | dinosaur 173 | dogsled 174 | direction 175 | dragnet 176 | disable 177 | drainage 178 | disbelief 179 | dreadful 180 | disruptive 181 | drifter 182 | distortion 183 | dropper 184 | document 185 | drumbeat 186 | embezzle 187 | drunken 188 | enchanting 189 | Dupont 190 | enrollment 191 | dwelling 192 | enterprise 193 | eating 194 | equation 195 | edict 196 | equipment 197 | egghead 198 | escapade 199 | eightball 200 | Eskimo 201 | endorse 202 | everyday 203 | endow 204 | examine 205 | enlist 206 | existence 207 | erase 208 | exodus 209 | escape 210 | fascinate 211 | exceed 212 | filament 213 | eyeglass 214 | finicky 215 | eyetooth 216 | forever 217 | facial 218 | fortitude 219 | fallout 220 | frequency 221 | flagpole 222 | gadgetry 223 | flatfoot 224 | Galveston 225 | flytrap 226 | getaway 227 | fracture 228 | glossary 229 | framework 230 | gossamer 231 | freedom 232 | graduate 233 | frighten 234 | gravity 235 | gazelle 236 | guitarist 237 | Geiger 238 | hamburger 239 | glitter 240 | Hamilton 241 | glucose 242 | handiwork 243 | goggles 244 | hazardous 245 | goldfish 246 | headwaters 247 | gremlin 248 | hemisphere 249 | guidance 250 | hesitate 251 | hamlet 252 | hideaway 253 | highchair 254 | holiness 255 | hockey 256 | hurricane 257 | indoors 258 | hydraulic 259 | indulge 260 | impartial 261 | inverse 262 | impetus 263 | involve 264 | inception 265 | island 266 | indigo 267 | jawbone 268 | inertia 269 | keyboard 270 | infancy 271 | kickoff 272 | inferno 273 | kiwi 274 | informant 275 | klaxon 276 | insincere 277 | locale 278 | insurgent 279 | lockup 280 | integrate 281 | merit 282 | intention 283 | minnow 284 | inventive 285 | miser 286 | Istanbul 287 | Mohawk 288 | Jamaica 289 | mural 290 | Jupiter 291 | music 292 | leprosy 293 | necklace 294 | letterhead 295 | Neptune 296 | liberty 297 | newborn 298 | maritime 299 | nightbird 300 | matchmaker 301 | Oakland 302 | maverick 303 | obtuse 304 | Medusa 305 | offload 306 | megaton 307 | optic 308 | microscope 309 | orca 310 | microwave 311 | payday 312 | midsummer 313 | peachy 314 | millionaire 315 | pheasant 316 | miracle 317 | physique 318 | misnomer 319 | playhouse 320 | molasses 321 | Pluto 322 | molecule 323 | preclude 324 | Montana 325 | prefer 326 | monument 327 | preshrunk 328 | mosquito 329 | printer 330 | narrative 331 | prowler 332 | nebula 333 | pupil 334 | newsletter 335 | puppy 336 | Norwegian 337 | python 338 | October 339 | quadrant 340 | Ohio 341 | quiver 342 | onlooker 343 | quota 344 | opulent 345 | ragtime 346 | Orlando 347 | ratchet 348 | outfielder 349 | rebirth 350 | Pacific 351 | reform 352 | pandemic 353 | regain 354 | Pandora 355 | reindeer 356 | paperweight 357 | rematch 358 | paragon 359 | repay 360 | paragraph 361 | retouch 362 | paramount 363 | revenge 364 | passenger 365 | reward 366 | pedigree 367 | rhythm 368 | Pegasus 369 | ribcage 370 | penetrate 371 | ringbolt 372 | perceptive 373 | robust 374 | performance 375 | rocker 376 | pharmacy 377 | ruffled 378 | phonetic 379 | sailboat 380 | photograph 381 | sawdust 382 | pioneer 383 | scallion 384 | pocketful 385 | scenic 386 | politeness 387 | scorecard 388 | positive 389 | Scotland 390 | potato 391 | seabird 392 | processor 393 | select 394 | provincial 395 | sentence 396 | proximate 397 | shadow 398 | puberty 399 | shamrock 400 | publisher 401 | showgirl 402 | pyramid 403 | skullcap 404 | quantity 405 | skydive 406 | racketeer 407 | slingshot 408 | rebellion 409 | slowdown 410 | recipe 411 | snapline 412 | recover 413 | snapshot 414 | repellent 415 | snowcap 416 | replica 417 | snowslide 418 | reproduce 419 | solo 420 | resistor 421 | southward 422 | responsive 423 | soybean 424 | retraction 425 | spaniel 426 | retrieval 427 | spearhead 428 | retrospect 429 | spellbind 430 | revenue 431 | spheroid 432 | revival 433 | spigot 434 | revolver 435 | spindle 436 | sandalwood 437 | spyglass 438 | sardonic 439 | stagehand 440 | Saturday 441 | stagnate 442 | savagery 443 | stairway 444 | scavenger 445 | standard 446 | sensation 447 | stapler 448 | sociable 449 | steamship 450 | souvenir 451 | sterling 452 | specialist 453 | stockman 454 | speculate 455 | stopwatch 456 | stethoscope 457 | stormy 458 | stupendous 459 | sugar 460 | supportive 461 | surmount 462 | surrender 463 | suspense 464 | suspicious 465 | sweatband 466 | sympathy 467 | swelter 468 | tambourine 469 | tactics 470 | telephone 471 | talon 472 | therapist 473 | tapeworm 474 | tobacco 475 | tempest 476 | tolerance 477 | tiger 478 | tomorrow 479 | tissue 480 | torpedo 481 | tonic 482 | tradition 483 | topmost 484 | travesty 485 | tracker 486 | trombonist 487 | transit 488 | truncated 489 | trauma 490 | typewriter 491 | treadmill 492 | ultimate 493 | Trojan 494 | undaunted 495 | trouble 496 | underfoot 497 | tumor 498 | unicorn 499 | tunnel 500 | unify 501 | tycoon 502 | universe 503 | uncut 504 | unravel 505 | unearth 506 | upcoming 507 | unwind 508 | vacancy 509 | uproot 510 | vagabond 511 | upset 512 | vertigo 513 | upshot 514 | Virginia 515 | vapor 516 | visitor 517 | village 518 | vocalist 519 | virus 520 | voyager 521 | Vulcan 522 | warranty 523 | waffle 524 | Waterloo 525 | wallet 526 | whimsical 527 | watchword 528 | Wichita 529 | wayside 530 | Wilmington 531 | willow 532 | Wyoming 533 | woodlark 534 | yesteryear 535 | Zulu 536 | Yucatan` 537 | --------------------------------------------------------------------------------