diff --git a/internal/rpchelp/helpdescs_en_US.go b/internal/rpchelp/helpdescs_en_US.go index 2a7a38261..007f4494f 100644 --- a/internal/rpchelp/helpdescs_en_US.go +++ b/internal/rpchelp/helpdescs_en_US.go @@ -541,6 +541,15 @@ var helpDescsEnUS = map[string]string{ "verifymessage-message": "The message to verify", "verifymessage--result0": "Whether the message was signed with the private key of 'address'", + // VerifySeedCmd help. + "verifyseed--synopsis": "Verifes if the inputted seed derived account key is associated with the running wallet account key", + "verifyseed-seed": "Seed to be checked against the running wallets", + "verifyseed-account": "Used to check if a watching only wallets public key is the same as the running wallets", + + // VerifySeedResult help. + "verifyseedresult-keyresult": "The result of comparing an inputted seed with the running wallets. Useful to check if a watching only wallet's public key is the same as the running wallet", + "verifyseedresult-cointype": "Outputs the current cointype of the running wallet", + // Version help "version--synopsis": "Returns application and API versions (semver) keyed by their names", "version--result0--desc": "Version objects keyed by the program or API name", @@ -548,7 +557,7 @@ var helpDescsEnUS = map[string]string{ "version--result0--value": "Object containing the semantic version", // WalletLockCmd help. - "walletlock--synopsis": "Lock the wallet.", + "walletlock--synopsis": "Lock the wallet", // WalletPassphraseCmd help. "walletpassphrase--synopsis": "Unlock the wallet.", diff --git a/internal/rpchelp/methods.go b/internal/rpchelp/methods.go index 010b48258..7ca947b07 100644 --- a/internal/rpchelp/methods.go +++ b/internal/rpchelp/methods.go @@ -93,6 +93,7 @@ var Methods = []struct { {"ticketsforaddress", returnsBool}, {"validateaddress", []interface{}{(*dcrjson.ValidateAddressWalletResult)(nil)}}, {"verifymessage", returnsBool}, + {"verifyseed", []interface{}{(*dcrjson.VerifySeedResult)(nil)}}, {"version", []interface{}{(*map[string]dcrjson.VersionResult)(nil)}}, {"walletinfo", []interface{}{(*dcrjson.WalletInfoResult)(nil)}}, {"walletislocked", returnsBool}, diff --git a/rpc/legacyrpc/methods.go b/rpc/legacyrpc/methods.go index a39b8724a..ba5ca613a 100644 --- a/rpc/legacyrpc/methods.go +++ b/rpc/legacyrpc/methods.go @@ -36,6 +36,7 @@ import ( "github.com/decred/dcrwallet/wallet" "github.com/decred/dcrwallet/wallet/txrules" "github.com/decred/dcrwallet/wallet/udb" + "github.com/decred/dcrwallet/walletseed" ) // API version constants @@ -123,6 +124,7 @@ var handlers = map[string]handler{ "ticketsforaddress": {fn: ticketsForAddress}, "validateaddress": {fn: validateAddress}, "verifymessage": {fn: verifyMessage}, + "verifyseed": {fn: verifySeed}, "version": {fn: version}, "walletinfo": {fn: walletInfo}, "walletlock": {fn: walletLock}, @@ -3423,6 +3425,88 @@ WrongAddrKind: return nil, rpcErrorf(dcrjson.ErrRPCInvalidParameter, "address must be secp256k1 P2PK or P2PKH") } +func deriveCoinTypeKey(seed []byte, coinType uint32, params *chaincfg.Params) (*hdkeychain.ExtendedKey, error) { + // Create new root from the inputted seed and the current net + root, err := hdkeychain.NewMaster(seed[:], params) + if err != nil { + return nil, err + } + + // BIP0032 hierarchy: m/'/ + // Where purpose = 44 and the ' indicates hardening with the HardenedKeyStart 0x80000000 + purpose, err := root.Child(44 + hdkeychain.HardenedKeyStart) + if err != nil { + return nil, err + } + defer purpose.Zero() + + // BIP0044 hierarchy: m/'/' + // Where coin type is either the legacy coin type, 20, or the coin type described in SLIP0044, 44. Note these parameters + // are only appropraite for main net. + coinTypePrivKey, err := purpose.Child(coinType + hdkeychain.HardenedKeyStart) + if err != nil { + return nil, err + } + + return coinTypePrivKey, nil +} + +// verifySeed checks if a user inputted seed is equivelent to the running wallets. +// Returns a a JSON object, with the dscribed bool and coin type. +func verifySeed(s *Server, icmd interface{}) (interface{}, error) { + cmd := icmd.(*dcrjson.VerifySeedCmd) + w, ok := s.walletLoader.LoadedWallet() + if !ok { + return nil, errUnloadedWallet + } + + // obtain the wallet public key to check agaisnt the wallet derived seed + account := 0 + if cmd.Account != nil { + account = int(*cmd.Account) + } + + coinType, err := w.CoinType() + if err != nil { + return nil, err + } + + decodedSeed, err := walletseed.DecodeUserInput(cmd.Seed) + if err != nil { + return nil, err + } + + coinTypePrivKey, err := deriveCoinTypeKey(decodedSeed, coinType, w.ChainParams()) + if err != nil { + return nil, err + } + defer coinTypePrivKey.Zero() + + // Both derivedAccountKey and walletDerivedAccountKey use the BIP044 hierachy: m/44'/'/' + accountKey, err := coinTypePrivKey.Child(uint32(account) + hdkeychain.HardenedKeyStart) + if err != nil { + return nil, err + } + defer accountKey.Zero() + + // To be matched with walletxPubKey. + seedxPubKey, err := accountKey.Neuter() + if err != nil { + return nil, err + } + + // need to get walletsPubKey + walletxPubKey, err := w.MasterPubKey(uint32(account)) + if err != nil { + return nil, err + } + + return &dcrjson.VerifySeedResult{ + Result: walletxPubKey.String() == seedxPubKey.String(), + CoinType: coinType, + }, nil +} + // version handles the version command by returning the RPC API versions of the // wallet and, optionally, the consensus RPC server as well if it is associated // with the server. The chainClient is optional, and this is simply a helper diff --git a/rpc/legacyrpc/rpcserverhelp.go b/rpc/legacyrpc/rpcserverhelp.go index 01bd65105..01e6c8e7b 100644 --- a/rpc/legacyrpc/rpcserverhelp.go +++ b/rpc/legacyrpc/rpcserverhelp.go @@ -73,6 +73,7 @@ func helpDescsEnUS() map[string]string { "ticketsforaddress": "ticketsforaddress \"address\"\n\nRequest all the tickets for an address.\n\nArguments:\n1. address (string, required) Address to look for.\n\nResult:\ntrue|false (boolean) Tickets owned by the specified address.\n", "validateaddress": "validateaddress \"address\"\n\nVerify that an address is valid.\nExtra details are returned if the address is controlled by this wallet.\nThe following fields are valid only when the address is controlled by this wallet (ismine=true): isscript, pubkey, iscompressed, account, addresses, hex, script, and sigsrequired.\nThe following fields are only valid when address has an associated public key: pubkey, iscompressed.\nThe following fields are only valid when address is a pay-to-script-hash address: addresses, hex, and script.\nIf the address is a multisig address controlled by this wallet, the multisig fields will be left unset if the wallet is locked since the redeem script cannot be decrypted.\n\nArguments:\n1. address (string, required) Address to validate\n\nResult:\n{\n \"isvalid\": true|false, (boolean) Whether or not the address is valid\n \"address\": \"value\", (string) The payment address (only when isvalid is true)\n \"ismine\": true|false, (boolean) Whether this address is controlled by the wallet (only when isvalid is true)\n \"iswatchonly\": true|false, (boolean) Unset\n \"isscript\": true|false, (boolean) Whether the payment address is a pay-to-script-hash address (only when isvalid is true)\n \"pubkeyaddr\": \"value\", (string) The pubkey for this payment address (only when isvalid is true)\n \"pubkey\": \"value\", (string) The associated public key of the payment address, if any (only when isvalid is true)\n \"iscompressed\": true|false, (boolean) Whether the address was created by hashing a compressed public key, if any (only when isvalid is true)\n \"account\": \"value\", (string) The account this payment address belongs to (only when isvalid is true)\n \"addresses\": [\"value\",...], (array of string) All associated payment addresses of the script if address is a multisig address (only when isvalid is true)\n \"hex\": \"value\", (string) The redeem script \n \"script\": \"value\", (string) The class of redeem script for a multisig address\n \"sigsrequired\": n, (numeric) The number of required signatures to redeem outputs to the multisig address\n} \n", "verifymessage": "verifymessage \"address\" \"signature\" \"message\"\n\nVerify a message was signed with the associated private key of some address.\n\nArguments:\n1. address (string, required) Address used to sign message\n2. signature (string, required) The signature to verify\n3. message (string, required) The message to verify\n\nResult:\ntrue|false (boolean) Whether the message was signed with the private key of 'address'\n", + "verifyseed": "verifyseed \"seed\" (account)\n\nVerifes if the inputted seed derived account key is associated with the running wallet account key\n\nArguments:\n1. seed (string, required) Seed to be checked against the running wallets\n2. account (numeric, optional) Used to check if a watching only wallets public key is the same as the running wallets\n\nResult:\n{\n \"keyresult\": true|false, (boolean) The result of comparing an inputted seed with the running wallets. Useful to check if a watching only wallet's public key is the same as the running wallet\n \"cointype\": n, (numeric) Outputs the current cointype of the running wallet\n} \n", "version": "version\n\nReturns application and API versions (semver) keyed by their names\n\nArguments:\nNone\n\nResult:\n{\n \"Program or API name\": Object containing the semantic version, (object) Version objects keyed by the program or API name\n ...\n}\n", "walletinfo": "walletinfo\n\nReturns global information about the wallet\n\nArguments:\nNone\n\nResult:\n{\n \"daemonconnected\": true|false, (boolean) Whether or not the wallet is currently connected to the daemon RPC\n \"unlocked\": true|false, (boolean) Whether or not the wallet is unlocked\n \"txfee\": n.nnn, (numeric) Transaction fee per kB of the serialized tx size in coins\n \"ticketfee\": n.nnn, (numeric) Ticket fee per kB of the serialized tx size in coins\n \"ticketpurchasing\": true|false, (boolean) Whether or not the wallet is currently purchasing tickets\n \"votebits\": n, (numeric) Vote bits setting\n \"votebitsextended\": \"value\", (string) Extended vote bits setting\n \"voteversion\": n, (numeric) Version of votes that will be generated\n \"voting\": true|false, (boolean) Whether or not the wallet is currently voting tickets\n} \n", "walletislocked": "walletislocked\n\nReturns whether or not the wallet is locked.\n\nArguments:\nNone\n\nResult:\ntrue|false (boolean) Whether the wallet is locked\n", @@ -86,4 +87,4 @@ var localeHelpDescs = map[string]func() map[string]string{ "en_US": helpDescsEnUS, } -var requestUsages = "accountaddressindex \"account\" branch\naccountsyncaddressindex \"account\" branch index\naddmultisigaddress nrequired [\"key\",...] (\"account\")\naddticket \"tickethex\"\nconsolidate inputs (\"account\" \"address\")\ncreatemultisig nrequired [\"key\",...]\ncreatenewaccount \"account\"\ndumpprivkey \"address\"\nexportwatchingwallet (\"account\" download=false)\ngeneratevote \"blockhash\" height \"tickethash\" votebits \"votebitsext\"\ngetaccountaddress \"account\"\ngetaccount \"address\"\ngetaddressesbyaccount \"account\"\ngetbalance (\"account\" minconf=1)\ngetbestblockhash\ngetbestblock\ngetblockcount\ngetinfo\ngetmasterpubkey (\"account\")\ngetmultisigoutinfo \"hash\" index\ngetnewaddress (\"account\" \"gappolicy\")\ngetrawchangeaddress (\"account\")\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngetstakeinfo\ngetticketfee\ngettickets includeimmature\ngettransaction \"txid\" (includewatchonly=false)\ngetunconfirmedbalance (\"account\")\ngetvotechoices\ngetwalletfee\nhelp (\"command\")\nimportprivkey \"privkey\" (\"label\" rescan=true scanfrom)\nimportscript \"hex\" (rescan=true scanfrom)\nkeypoolrefill (newsize=100)\nlistaccounts (minconf=1)\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nlistlockunspent\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistscripts\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...])\nlockunspent unlock [{\"amount\":n.nnn,\"txid\":\"value\",\"vout\":n,\"tree\":n},...]\npurchaseticket \"fromaccount\" spendlimit (minconf=1 \"ticketaddress\" numtickets \"pooladdress\" poolfees expiry \"comment\" ticketfee)\nredeemmultisigout \"hash\" index tree (\"address\")\nredeemmultisigouts \"fromscraddress\" (\"toaddress\" number)\nrenameaccount \"oldaccount\" \"newaccount\"\nrescanwallet (beginheight=0)\nrevoketickets\nsendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\nsendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\nsendtoaddress \"address\" amount (\"comment\" \"commentto\")\nsendtomultisig \"fromaccount\" amount [\"pubkey\",...] (nrequired=1 minconf=1 \"comment\")\nsetticketfee fee\nsettxfee amount\nsetvotechoice \"agendaid\" \"choiceid\"\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nsignrawtransactions [\"rawtx\",...] (send=true)\nstakepooluserinfo \"user\"\nstartautobuyer \"account\" \"passphrase\" (balancetomaintain maxfeeperkb maxpricerelative maxpriceabsolute \"votingaddress\" \"pooladdress\" poolfees maxperblock)\nstopautobuyer\nsweepaccount \"sourceaccount\" \"destinationaddress\" (requiredconfirmations feeperkb)\nticketsforaddress \"address\"\nvalidateaddress \"address\"\nverifymessage \"address\" \"signature\" \"message\"\nversion\nwalletinfo\nwalletislocked\nwalletlock\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\nwalletpassphrase \"passphrase\" timeout" +var requestUsages = "accountaddressindex \"account\" branch\naccountsyncaddressindex \"account\" branch index\naddmultisigaddress nrequired [\"key\",...] (\"account\")\nconsolidate inputs (\"account\" \"address\")\ncreatemultisig nrequired [\"key\",...]\ndumpprivkey \"address\"\ngetaccount \"address\"\ngetaccountaddress \"account\"\ngetaddressesbyaccount \"account\"\ngetbalance (\"account\" minconf=1)\ngetbestblockhash\ngetblockcount\ngetinfo\ngetmasterpubkey (\"account\")\ngetmultisigoutinfo \"hash\" index\ngetnewaddress (\"account\" \"gappolicy\")\ngetrawchangeaddress (\"account\")\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngettickets includeimmature\ngettransaction \"txid\" (includewatchonly=false)\ngetvotechoices\nhelp (\"command\")\nimportprivkey \"privkey\" (\"label\" rescan=true scanfrom)\nimportscript \"hex\" (rescan=true scanfrom)\nkeypoolrefill (newsize=100)\nlistaccounts (minconf=1)\nlistlockunspent\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...])\nlockunspent unlock [{\"amount\":n.nnn,\"txid\":\"value\",\"vout\":n,\"tree\":n},...]\nredeemmultisigout \"hash\" index tree (\"address\")\nredeemmultisigouts \"fromscraddress\" (\"toaddress\" number)\nrescanwallet (beginheight=0)\nrevoketickets\nsendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\nsendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\nsendtoaddress \"address\" amount (\"comment\" \"commentto\")\nsendtomultisig \"fromaccount\" amount [\"pubkey\",...] (nrequired=1 minconf=1 \"comment\")\nsettxfee amount\nsetvotechoice \"agendaid\" \"choiceid\"\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nsignrawtransactions [\"rawtx\",...] (send=true)\nstartautobuyer \"account\" \"passphrase\" (balancetomaintain maxfeeperkb maxpricerelative maxpriceabsolute \"votingaddress\" \"pooladdress\" poolfees maxperblock)\nstopautobuyer\nsweepaccount \"sourceaccount\" \"destinationaddress\" (requiredconfirmations feeperkb)\nvalidateaddress \"address\"\nverifymessage \"address\" \"signature\" \"message\"\nverifyseed \"seed\" (account)\nversion\nwalletlock\nwalletpassphrase \"passphrase\" timeout\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\ncreatenewaccount \"account\"\nexportwatchingwallet (\"account\" download=false)\ngetbestblock\ngetunconfirmedbalance (\"account\")\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nrenameaccount \"oldaccount\" \"newaccount\"\nwalletislocked\nwalletinfo\npurchaseticket \"fromaccount\" spendlimit (minconf=1 \"ticketaddress\" numtickets \"pooladdress\" poolfees expiry \"comment\" ticketfee)\ngeneratevote \"blockhash\" height \"tickethash\" votebits \"votebitsext\"\ngetstakeinfo\ngetticketfee\nsetticketfee fee\ngetwalletfee\naddticket \"tickethex\"\nlistscripts\nstakepooluserinfo \"user\"\nticketsforaddress \"address\"" diff --git a/wallet/wallet.go b/wallet/wallet.go index 58eeab753..a955533e7 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -1887,35 +1887,6 @@ func (w *Wallet) MasterPubKey(account uint32) (*hdkeychain.ExtendedKey, error) { return extKey, nil } -// GetTransactionsByHashes returns all known transactions identified by a slice -// of transaction hashes. It is possible that not all transactions are found, -// and in this case the known results will be returned along with an inventory -// vector of all missing transactions and an error with code -// NotExist. -func (w *Wallet) GetTransactionsByHashes(txHashes []*chainhash.Hash) (txs []*wire.MsgTx, notFound []*wire.InvVect, err error) { - err = walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { - ns := dbtx.ReadBucket(wtxmgrNamespaceKey) - for _, hash := range txHashes { - tx, err := w.TxStore.Tx(ns, hash) - if err != nil { - return err - } - if tx == nil { - notFound = append(notFound, wire.NewInvVect(wire.InvTypeTx, hash)) - } else { - txs = append(txs, tx) - } - } - return nil - }) - if err != nil { - return - } - if len(notFound) != 0 { - err = errors.E(errors.NotExist, "transaction(s) not found") - } - return -======= // CoinTypeKey returns the BIP0044 coin type private key for the passed account. func (w *Wallet) CoinTypeKey() (*hdkeychain.ExtendedKey, error) { const op errors.Op = "wallet.CoinTypeKey" @@ -1944,7 +1915,36 @@ func (w *Wallet) CoinType() (uint32, error) { return 0, errors.E(op, err) } return coinType, nil ->>>>>>> wallet: Add CoinTypeKey and CoinType functions +} + +// GetTransactionsByHashes returns all known transactions identified by a slice +// of transaction hashes. It is possible that not all transactions are found, +// and in this case the known results will be returned along with an inventory +// vector of all missing transactions and an error with code +// NotExist. +func (w *Wallet) GetTransactionsByHashes(txHashes []*chainhash.Hash) (txs []*wire.MsgTx, notFound []*wire.InvVect, err error) { + err = walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { + ns := dbtx.ReadBucket(wtxmgrNamespaceKey) + for _, hash := range txHashes { + tx, err := w.TxStore.Tx(ns, hash) + if err != nil { + return err + } + if tx == nil { + notFound = append(notFound, wire.NewInvVect(wire.InvTypeTx, hash)) + } else { + txs = append(txs, tx) + } + } + return nil + }) + if err != nil { + return + } + if len(notFound) != 0 { + err = errors.E(errors.NotExist, "transaction(s) not found") + } + return } // CreditCategory describes the type of wallet transaction output. The category