Пример #1
0
def test_discover(monkeypatch, prepareLogger):
    cryptoKey = rando.newKey()
    db = database.KeyValueDatabase(":memory:").child("tmp")
    acctMgr = accounts.createNewAccountManager(tRoot, cryptoKey, "dcr",
                                               nets.mainnet, db)
    txs = {}

    class Blockchain:
        params = nets.mainnet
        addrsHaveTxs = lambda addrs: any(a in txs for a in addrs)

    # Set up globals for test.
    origChain = chains.chain("dcr")
    chains.registerChain("dcr", Blockchain)
    # Set up globals for test.
    monkeypatch.setattr(accounts, "ACCOUNT_GAP_LIMIT", 2)
    monkeypatch.setattr(account, "DefaultGapLimit", 4)

    acctMgr.discover(cryptoKey)
    assert len(acctMgr.accounts) == 1

    coinExtKey = acctMgr.coinKey(cryptoKey)
    acct2ExtKey = coinExtKey.deriveAccountKey(2).neuter().child(0)
    acct2Addr3 = addrlib.deriveChildAddress(acct2ExtKey, 3, nets.mainnet)
    txs[acct2Addr3] = ["tx"]
    acctMgr.discover(cryptoKey)
    assert len(acctMgr.accounts) == 3

    # Restore globals.
    chains.registerChain("dcr", origChain)
Пример #2
0
def test_discover(prepareLogger):
    cryptoKey = rando.newKey()
    db = database.KeyValueDatabase(":memory:").child("tmp")
    acctMgr = accounts.createNewAccountManager(tRoot, cryptoKey, "dcr",
                                               nets.mainnet, db)
    txs = {}

    class Blockchain:
        params = nets.mainnet
        addrsHaveTxs = lambda addrs: any(a in txs for a in addrs)

    ogChain = chains.chain("dcr")
    chains.registerChain("dcr", Blockchain)
    ogLimit = accounts.ACCOUNT_GAP_LIMIT
    accounts.ACCOUNT_GAP_LIMIT = 2

    acctMgr.discover(cryptoKey)
    assert len(acctMgr.accounts) == 1

    coinExtKey = acctMgr.coinKey(cryptoKey)
    acct2ExtKey = coinExtKey.deriveAccountKey(2).neuter().child(0)
    acct2Addr5 = addrlib.deriveChildAddress(acct2ExtKey, 5, nets.mainnet)
    txs[acct2Addr5] = ["tx"]
    acctMgr.discover(cryptoKey)
    assert len(acctMgr.accounts) == 3

    chains.registerChain("dcr", ogChain)
    accounts.ACCOUNT_GAP_LIMIT = ogLimit
Пример #3
0
def test_change_addresses(prepareLogger):
    """
    Test internal branch address derivation.
    """
    cryptoKey = rando.newKey()
    db = database.KeyValueDatabase(":memory:").child("tmp")
    # ticker for coin type is ok. Case insensitive.
    acctMgr = accounts.createNewAccountManager(tRoot, cryptoKey, "DcR",
                                               nets.mainnet, db)
    acct = acctMgr.openAccount(0, cryptoKey)
    for i in range(10):
        acct.nextInternalAddress()
Пример #4
0
def test_account_manager(monkeypatch, prepareLogger):
    # Set up globals for test.
    monkeypatch.setattr(account, "DefaultGapLimit", 2)

    cryptoKey = rando.newKey()
    db = database.KeyValueDatabase(":memory:").child("tmp")
    # 42 = Decred
    acctMgr = accounts.createNewAccountManager(tRoot, cryptoKey, 42,
                                               nets.mainnet, db)

    acct = acctMgr.openAccount(0, cryptoKey)
    tempAcct = acctMgr.addAccount(cryptoKey, "temp")
    assert acctMgr.account(1) == tempAcct
    assert acctMgr.listAccounts() == [acct, tempAcct]

    acctMgr.setNode("node")
    assert acctMgr.node == "node"
    assert tempAcct.node == "node"

    acctMgr.accounts[3] = tempAcct
    del acctMgr.accounts[1]
    with pytest.raises(DecredError):
        acctMgr.listAccounts()
    del acctMgr.accounts[3]

    with pytest.raises(DecredError):
        accounts.AccountManager.unblob(encode.BuildyBytes(0))

    zeroth = acct.currentAddress()
    b = acctMgr.serialize()
    reAM = accounts.AccountManager.unblob(b.b)
    assert acctMgr.coinType == reAM.coinType
    assert acctMgr.netName == reAM.netName
    assert acctMgr.netName == reAM.netName

    reAM.load(db, None)
    reAcct = reAM.openAccount(0, cryptoKey)
    reZeroth = reAcct.currentAddress()
    assert zeroth == reZeroth

    acctMgr.saveAccount(0)
    db = acctMgr.dbForAcctIdx(0)
    assert db.name == "tmp$accts$0"
Пример #5
0
    def initialize(self, seed, pw, netParams):
        """
        Initialize the wallet.

        Args:
            seed (bytes-like): The wallet seed.
            pw   (bytes-like): The wallet password, UTF-8 encoded.
            netParams (module): Network parameters.
        """
        pwKey = crypto.SecretKey(pw)
        cryptoKey = rando.newKey()
        root = crypto.ExtendedKey.new(seed)
        self.masterDB[DBKeys.cryptoKey] = pwKey.encrypt(cryptoKey)
        self.masterDB[DBKeys.root] = root.serialize()
        self.masterDB[DBKeys.keyParams] = crypto.ByteArray(
            pwKey.params().serialize())
        db = self.coinDB.child(str(BipIDs.decred), table=False)
        acctManager = accounts.createNewAccountManager(root, cryptoKey, "dcr",
                                                       netParams, db)
        self.coinDB[BipIDs.decred] = acctManager
Пример #6
0
def test_filter():
    """
    Ensure that the filters and all associated methods work as expected by using
    various known parameters and contents along with random keys for matching
    purposes.
    """
    # Use a random key for each test instance and log it if the tests fail.
    randKey = rando.newKey()[:gcs.KeySize]
    fixedKey = ByteArray(length=16)

    # Test some error paths.
    f = gcs.FilterV2.deserialize(
        ByteArray("1189af70ad5baf9da83c64e99b18e96a06cd7295a58b32"
                  "4e81f09c85d093f1e33dcd6f40f18cfcbe2aeb771d8390"))
    member = ByteArray("Alex".encode())
    with pytest.raises(DecredError):
        f.match(key=ByteArray(length=17), data=ByteArray(0x0A0B))

    # random entry doesn't match.
    assert not f.match(key=fixedKey, data=ByteArray("0a"))
    assert not f.matchAny(key=fixedKey, data=[ByteArray("0a")])
    # Filter of all FF gives encoding error, which returns False.
    f.filterData = ByteArray(0xFF)
    assert not f.match(key=fixedKey, data=member)
    assert not f.matchAny(key=fixedKey, data=[member])

    # fmt: off
    # contents1 defines a set of known elements for use in the tests below.
    contents1 = [
        ByteArray(s.encode()) for s in (
            "Alex",
            "Bob",
            "Charlie",
            "Dick",
            "Ed",
            "Frank",
            "George",
            "Harry",
            "Ilya",
            "John",
            "Kevin",
            "Larry",
            "Michael",
            "Nate",
            "Owen",
            "Paul",
            "Quentin",
        )
    ]

    # contents2 defines a separate set of known elements for use in the tests
    # below.
    contents2 = [
        ByteArray(s.encode()) for s in (
            "Alice",
            "Betty",
            "Charmaine",
            "Donna",
            "Edith",
            "Faina",
            "Georgia",
            "Hannah",
            "Ilsbeth",
            "Jennifer",
            "Kayla",
            "Lena",
            "Michelle",
            "Natalie",
            "Ophelia",
            "Peggy",
            "Queenie",
        )
    ]
    # fmt: on

    tests = [
        dict(
            name="v2 empty filter",
            matchKey=randKey,
            contents=[],
            wantMatches=[],
            fixedKey=fixedKey,
            wantBytes=ByteArray(),
            wantHash=rba(length=32),
        ),
        dict(
            name="v2 filter single nil item produces empty filter",
            matchKey=randKey,
            contents=[ByteArray()],
            wantMatches=[],
            fixedKey=fixedKey,
            wantBytes=bytearray(),
            wantHash=rba(length=32),
        ),
        dict(
            name="v2 filter contents1 with nil item with B=19, M=784931",
            matchKey=randKey,
            contents=[ByteArray()] + contents1,
            wantMatches=contents1,
            fixedKey=fixedKey,
            wantBytes=ByteArray(
                "1189af70ad5baf9da83c64e99b18e96a06cd7295a58b32"
                "4e81f09c85d093f1e33dcd6f40f18cfcbe2aeb771d8390"),
            wantHash=rba(
                "b616838c6090d3e732e775cc2f336ce0b836895f3e0f22d6c3ee4485a6ea5018"
            ),
        ),
        dict(
            name="v2 filter contents1 with B=19, M=784931",
            matchKey=randKey,
            contents=contents1,
            wantMatches=contents1,
            fixedKey=fixedKey,
            wantBytes=ByteArray(
                "1189af70ad5baf9da83c64e99b18e96a06cd7295a58b32"
                "4e81f09c85d093f1e33dcd6f40f18cfcbe2aeb771d8390"),
            wantHash=rba(
                "b616838c6090d3e732e775cc2f336ce0b836895f3e0f22d6c3ee4485a6ea5018"
            ),
        ),
        dict(
            name="v2 filter contents2 with B=19, M=784931",
            matchKey=randKey,
            contents=contents2,
            wantMatches=contents2,
            fixedKey=fixedKey,
            wantBytes=ByteArray(
                "118d4be5372d2f4731c7e1681aefd23028be12306b4d90"
                "701a46b472ee80ad60f9fa86c4d6430cfb495ced604362"),
            wantHash=rba(
                "f3028f42909209120c8bf649fbbc5a70fb907d8997a02c2c1f2eef0e6402cb15"
            ),
        ),
    ]

    for test in tests:
        # Create a filter with the match key for all tests not related to
        # testing serialization.
        f = gcs.FilterV2.deserialize(test["wantBytes"])
        wantN = len(test["contents"]) - sum(
            1 for d in test["contents"] if len(d) == 0)
        assert f.n == wantN, test["name"]

        # Ensure empty data never matches.
        assert not f.match(test["matchKey"], ByteArray())
        assert not f.matchAny(test["matchKey"], []), test["name"]

        assert not f.matchAny(test["matchKey"], [ByteArray()]), test["name"]

        # Ensure empty filter never matches data.
        if len(test["contents"]) == 0:
            wantMiss = "test".encode()
            assert not f.match(test["matchKey"], wantMiss), test["name"]
            assert not f.matchAny(test["matchKey"], [wantMiss]), test["name"]

        # Ensure all of the expected matches occur individually.
        for wantMatch in test["wantMatches"]:
            assert f.match(test["fixedKey"], wantMatch), test["name"]

        # Ensure a subset of the expected matches works in various orders when
        # matching any.
        if len(test["wantMatches"]) > 0:
            # Create set of data to attempt to match such that only the final
            # item is an element in the filter.
            matches = []
            for data in test["wantMatches"]:
                mutated = ByteArray(data)
                mutated[0] ^= 0x55
                matches.append(mutated)

            matches[-1] = test["wantMatches"][-1]

            assert f.matchAny(test["fixedKey"], matches), test["name"]

            # Fisher-Yates shuffle the match set and test for matches again.
            for i in range(len(matches)):
                # Pick a number between current index and the end.
                j = random.randint(0, len(matches) - i - 1) + i
                matches[i], matches[j] = matches[j], matches[i]

            assert f.matchAny(test["fixedKey"], matches), test["name"]

        assert f.hash() == test["wantHash"], test["name"]
Пример #7
0
    def test_main(self, monkeypatch):
        """
        Test account functionality.
        """
        # Set up globals for test.
        monkeypatch.setattr(account, "DefaultGapLimit", 2)

        db = KeyValueDatabase(":memory:").child("tmp")
        acct = self.newAccount(db)
        acct.unlock(self.cryptoKey)
        acct.generateGapAddresses()
        for _ in range(20):
            acct.nextExternalAddress()
        satoshis = int(round(5 * 1e8))
        txHash = rando.newHash()
        txid = reversed(txHash).hex()
        vout = 2
        address = acct.nextExternalAddress()

        utxo = account.UTXO(
            address=address,
            txHash=txHash,
            vout=vout,
            scriptPubKey=ByteArray(0),
            satoshis=satoshis,
            maturity=1,
        )

        # A helper function to get the current utxo count.
        utxocount = lambda: len(list(acct.utxoscan()))

        # Add the utxo
        acct.addUTXO(utxo)
        # Check the count and balance categories.
        assert utxocount() == 1
        assert acct.calcBalance(1).total == satoshis
        assert acct.calcBalance(1).available == satoshis
        assert acct.calcBalance(0).available == 0
        # Helper functions.
        assert acct.caresAboutTxid(txid)
        assert not acct.caresAboutTxid("")
        acct.addUTXO(utxo)
        assert utxocount() == 1
        acct.spendUTXO(utxo)
        assert utxocount() == 0

        b = acct.serialize()
        reAcct = account.Account.unblob(b.b)

        assert acct.pubKeyEncrypted == reAcct.pubKeyEncrypted
        assert acct.privKeyEncrypted == reAcct.privKeyEncrypted
        assert acct.name == reAcct.name
        assert acct.coinID == reAcct.coinID
        assert acct.netID == reAcct.netID
        assert acct.gapLimit == reAcct.gapLimit
        assert acct.cursorExt == reAcct.cursorExt
        assert acct.cursorInt == reAcct.cursorInt
        assert acct.gapLimit == reAcct.gapLimit

        # Create a faux blockchain for the account.
        class Blockchain:
            txidsForAddr = lambda addr: []
            UTXOs = lambda addrs: []
            tipHeight = 5

        acct.blockchain = Blockchain

        class Signals:
            b = None

            @classmethod
            def balance(cls, b):
                cls.b = b

        acct.signals = Signals

        # Add a txid for this first address
        txid = rando.newHash().hex()
        zerothAddr = acct.externalAddresses[0]
        acct.addTxid(zerothAddr, txid)
        # Add a voting service provider
        vspKey = rando.newKey().hex()
        ticketAddr = "ticketAddr"
        pi = PurchaseInfo("addr", 1.0, ByteArray(b"scripthashscript"),
                          ticketAddr, 1, 0, 2)
        vsp = VotingServiceProvider("https://myvsp.com", vspKey,
                                    nets.mainnet.Name, pi)
        with pytest.raises(DecredError):
            acct.setPool(None)
        acct.setPool(vsp)
        acct.setNewPool(vsp)
        # Add UTXOs
        utxos = [account.UTXO.parse(u) for u in self.dcrdataUTXOs]
        acct.resolveUTXOs(utxos)
        ticketStats = acct.ticketStats()
        assert ticketStats.count == 0
        assert ticketStats.value == 0

        # Check that the addresses txid and the vsp load from the database.
        acct.txs.clear()
        acct.stakePools.clear()
        acct.utxos.clear()
        assert acct.stakePool() is None
        acct.load(db, Blockchain, Signals)
        assert zerothAddr in acct.txs
        assert len(acct.txs[zerothAddr]) == 1
        assert acct.txs[zerothAddr][0] == txid
        assert len(acct.stakePools) == 1
        assert acct.stakePool().apiKey == vspKey
        assert acct.calcBalance().available == self.utxoTotal
        assert len(acct.getUTXOs(self.utxoTotal)[0]) == 2
        assert len(acct.getUTXOs(self.utxoTotal, lambda u: False)[0]) == 0
        assert acct.lastSeen(acct.externalAddresses, -1) == 0
        branch, idx = acct.branchAndIndex(zerothAddr)
        assert branch == account.EXTERNAL_BRANCH
        assert idx == 0
        branch, idx = acct.branchAndIndex(acct.internalAddresses[0])
        assert branch == account.INTERNAL_BRANCH
        assert idx == 0
        checkKey = acct.privKey.child(account.EXTERNAL_BRANCH).child(0).key
        with pytest.raises(DecredError):
            acct.privKeyForAddress("")
        assert acct.privKeyForAddress(zerothAddr).key == checkKey
        ticketAddrs = acct.addTicketAddresses([])
        assert len(ticketAddrs) == 1
        assert ticketAddrs[0] == ticketAddr
        assert acct.hasPool()

        # Exercise setNode
        acct.setNode(1)
        assert acct.node == 1

        # Add a coinbase transaction output to the account.
        coinbase = newCoinbaseTx()
        cbUTXO = account.UTXO("addr",
                              ByteArray(b"id"),
                              1,
                              height=5,
                              satoshis=int(1e8))
        coinbase.addTxOut(msgtx.TxOut(int(1e8)))
        coinbaseID = coinbase.id()
        cbUTXO.txid = coinbaseID
        acct.addUTXO(cbUTXO)
        acct.addMempoolTx(coinbase)
        assert coinbaseID in acct.mempool
        # Send a block signal and have the transaction confirmed.
        sig = {"message": {"block": {"Tx": [{"TxID": coinbaseID}]}}}
        acct.blockchain.tipHeight = 5
        acct.blockchain.tx = lambda *a: coinbase
        acct.blockSignal(sig)
        assert coinbaseID not in acct.mempool
        assert cbUTXO.key() in acct.utxos
        maturity = 5 + nets.mainnet.CoinbaseMaturity
        assert acct.utxos[cbUTXO.key()].maturity == maturity
        assert acct.calcBalance(
            maturity).available == self.utxoTotal + cbUTXO.satoshis
        assert acct.calcBalance(0).available == self.utxoTotal
        acct.spendUTXOs([cbUTXO])
        assert acct.calcBalance(maturity).available == self.utxoTotal

        # make a ticket and a vote
        ticket = account.UTXO.parse(dcrdataUTXO)
        utxo.setTicketInfo(dcrdataTinfo)
        utxo.scriptPubKey = ticketScript
        utxo.parseScriptClass()
        acct.addUTXO(ticket)
        expVal = acct.calcBalance().available - ticket.satoshis
        voteTx = msgtx.MsgTx.new()
        voteTx.addTxIn(
            msgtx.TxIn(
                msgtx.OutPoint(reversed(ByteArray(ticket.txid)), ticket.vout,
                               0)))
        acct.blockchain.tx = lambda *a: voteTx
        acct.addressSignal(ticketAddr, "somehash")
        assert Signals.b.available == expVal

        # Detect a new utxo for the account.
        newVal = int(5e8)
        expVal = acct.calcBalance().available + newVal
        addr = acct.externalAddresses[1]
        a = addrlib.decodeAddress(addr, nets.mainnet)
        script = txscript.payToAddrScript(a)
        newTx = msgtx.MsgTx.new()
        op = msgtx.TxOut(newVal, script)
        newTx.addTxOut(op)
        acct.blockchain.tx = lambda *a: newTx
        utxo = account.UTXO(addr, ByteArray(b"txid"), 0, satoshis=newVal)
        acct.blockchain.txVout = lambda *a: utxo
        acct.addressSignal(addr, "somehash")
        assert Signals.b.available == expVal

        # test syncing. add a single output for external address #2
        acct.stakePool().getPurchaseInfo = lambda: None
        acct.stakePool().authorize = lambda a: True
        newVal = int(3e8)
        addr = acct.externalAddresses[2]
        a = addrlib.decodeAddress(addr, nets.mainnet)
        script = txscript.payToAddrScript(a)
        newTx = msgtx.MsgTx.new()
        op = msgtx.TxOut(newVal, script)
        newTx.addTxOut(op)
        utxo = account.UTXO(addr, ByteArray(b"txid"), 0, satoshis=newVal)

        def t4a(*a):
            acct.blockchain.txidsForAddr = lambda *a: []
            return [newTx.id()]

        acct.blockchain.txidsForAddr = t4a

        def utxos4a(*a):
            acct.blockchain.UTXOs = lambda *a: []
            return [utxo]

        acct.blockchain.UTXOs = utxos4a
        acct.blockchain.subscribeAddresses = lambda addrs, receiver: None
        acct.blockchain.subscribeBlocks = lambda a: None
        acct.sync()
        assert Signals.b.available == newVal

        # spend the utxo by sending it. Reusing newTx for convenience.
        changeVal = int(5e7)
        addr = acct.internalAddresses[0]
        change = account.UTXO(addr,
                              ByteArray(b"newtxid"),
                              0,
                              satoshis=changeVal)
        acct.blockchain.sendToAddress = lambda *a: (newTx, [utxo], [change])
        acct.sendToAddress(1, "recipient")
        assert Signals.b.available == changeVal

        # purchase some tickets. Reusing newTx for convenience again.
        ticket = account.UTXO.parse(dcrdataUTXO)
        ticket.setTicketInfo(dcrdataTinfo)
        ticket.scriptPubKey = ticketScript
        ticket.parseScriptClass()
        acct.blockchain.purchaseTickets = lambda *a: ([newTx, []], [],
                                                      [ticket])
        acct.signals.spentTickets = lambda: True
        acct.purchaseTickets(1, 1)
        assert Signals.b.total == changeVal + ticket.satoshis

        # revoke the ticket
        ticketTx = msgtx.MsgTx.new()
        op = msgtx.TxOut(ticket.satoshis, ticket.scriptPubKey)
        ticketTx.addTxOut(op)
        ticket.tinfo.status = "missed"
        redeemHash = addrlib.AddressScriptHash(
            txscript.extractStakeScriptHash(ticketScript, opcode.OP_SSTX),
            nets.mainnet,
        )
        acct.stakePool().purchaseInfo.ticketAddress = redeemHash.string()
        revoked = False

        def rev(*a):
            nonlocal revoked
            revoked = True

        acct.blockchain.tx = lambda *a: ticketTx
        acct.blockchain.revokeTicket = rev
        acct.revokeTickets()
        assert revoked
Пример #8
0
class TestAccount:
    cryptoKey = rando.newKey()
    utxoTotal = 942168886 + 942093929

    # One utxo for each of external and internal branches.
    dcrdataUTXOs = json.loads("""
    [
        {
            "address": "DsmP6rBEou9Qr7jaHnFz8jTfrUxngWqrKBw",
            "txid": "81a09bd29b6ba770f9968ead531435a3ee586ef21c8240bed2e6fde2e93e351c",
            "vout": 2,
            "ts": 1581886222,
            "scriptPubKey": "76a914f14f995d7b8c37c961bdb1baf431a18026f7e31088ac",
            "height": 424213,
            "satoshis": 942168886,
            "confirmations": 1
        },
        {
            "address": "DskHpgbEb6hqkuHchHhtyojpehFToEtjQSo",
            "txid": "b5c63a179c7caad1cb6e438acd7182f27388fbfb0fe2ecf9843163b627018d5f",
            "vout": 2,
            "ts": 1581886006,
            "scriptPubKey": "76a914f14f995d7b8c37c961bdb1baf431a18026f7e31088ac",
            "height": 424212,
            "satoshis": 942093929,
            "confirmations": 2
        }
    ]
    """)

    def newAccount(self, db, blockchain=None):
        # Create an account key
        xk = crypto.ExtendedKey.new(testSeed)
        dcrKey = xk.deriveCoinTypeKey(nets.mainnet)
        acctKey = dcrKey.deriveAccountKey(0)
        acctKeyPub = acctKey.neuter()
        privKeyEncrypted = crypto.encrypt(self.cryptoKey, acctKey.serialize())
        pubKeyEncrypted = crypto.encrypt(self.cryptoKey,
                                         acctKeyPub.serialize())
        return account.Account(0, pubKeyEncrypted, privKeyEncrypted,
                               "acctName", "mainnet", db, blockchain)

    def test_main(self, monkeypatch):
        """
        Test account functionality.
        """
        # Set up globals for test.
        monkeypatch.setattr(account, "DefaultGapLimit", 2)

        db = KeyValueDatabase(":memory:").child("tmp")
        acct = self.newAccount(db)
        acct.unlock(self.cryptoKey)
        acct.generateGapAddresses()
        for _ in range(20):
            acct.nextExternalAddress()
        satoshis = int(round(5 * 1e8))
        txHash = rando.newHash()
        txid = reversed(txHash).hex()
        vout = 2
        address = acct.nextExternalAddress()

        utxo = account.UTXO(
            address=address,
            txHash=txHash,
            vout=vout,
            scriptPubKey=ByteArray(0),
            satoshis=satoshis,
            maturity=1,
        )

        # A helper function to get the current utxo count.
        utxocount = lambda: len(list(acct.utxoscan()))

        # Add the utxo
        acct.addUTXO(utxo)
        # Check the count and balance categories.
        assert utxocount() == 1
        assert acct.calcBalance(1).total == satoshis
        assert acct.calcBalance(1).available == satoshis
        assert acct.calcBalance(0).available == 0
        # Helper functions.
        assert acct.caresAboutTxid(txid)
        assert not acct.caresAboutTxid("")
        acct.addUTXO(utxo)
        assert utxocount() == 1
        acct.spendUTXO(utxo)
        assert utxocount() == 0

        b = acct.serialize()
        reAcct = account.Account.unblob(b.b)

        assert acct.pubKeyEncrypted == reAcct.pubKeyEncrypted
        assert acct.privKeyEncrypted == reAcct.privKeyEncrypted
        assert acct.name == reAcct.name
        assert acct.coinID == reAcct.coinID
        assert acct.netID == reAcct.netID
        assert acct.gapLimit == reAcct.gapLimit
        assert acct.cursorExt == reAcct.cursorExt
        assert acct.cursorInt == reAcct.cursorInt
        assert acct.gapLimit == reAcct.gapLimit

        # Create a faux blockchain for the account.
        class Blockchain:
            txidsForAddr = lambda addr: []
            UTXOs = lambda addrs: []
            tipHeight = 5

        acct.blockchain = Blockchain

        class Signals:
            b = None

            @classmethod
            def balance(cls, b):
                cls.b = b

        acct.signals = Signals

        # Add a txid for this first address
        txid = rando.newHash().hex()
        zerothAddr = acct.externalAddresses[0]
        acct.addTxid(zerothAddr, txid)
        # Add a voting service provider
        vspKey = rando.newKey().hex()
        ticketAddr = "ticketAddr"
        pi = PurchaseInfo("addr", 1.0, ByteArray(b"scripthashscript"),
                          ticketAddr, 1, 0, 2)
        vsp = VotingServiceProvider("https://myvsp.com", vspKey,
                                    nets.mainnet.Name, pi)
        with pytest.raises(DecredError):
            acct.setPool(None)
        acct.setPool(vsp)
        acct.setNewPool(vsp)
        # Add UTXOs
        utxos = [account.UTXO.parse(u) for u in self.dcrdataUTXOs]
        acct.resolveUTXOs(utxos)
        ticketStats = acct.ticketStats()
        assert ticketStats.count == 0
        assert ticketStats.value == 0

        # Check that the addresses txid and the vsp load from the database.
        acct.txs.clear()
        acct.stakePools.clear()
        acct.utxos.clear()
        assert acct.stakePool() is None
        acct.load(db, Blockchain, Signals)
        assert zerothAddr in acct.txs
        assert len(acct.txs[zerothAddr]) == 1
        assert acct.txs[zerothAddr][0] == txid
        assert len(acct.stakePools) == 1
        assert acct.stakePool().apiKey == vspKey
        assert acct.calcBalance().available == self.utxoTotal
        assert len(acct.getUTXOs(self.utxoTotal)[0]) == 2
        assert len(acct.getUTXOs(self.utxoTotal, lambda u: False)[0]) == 0
        assert acct.lastSeen(acct.externalAddresses, -1) == 0
        branch, idx = acct.branchAndIndex(zerothAddr)
        assert branch == account.EXTERNAL_BRANCH
        assert idx == 0
        branch, idx = acct.branchAndIndex(acct.internalAddresses[0])
        assert branch == account.INTERNAL_BRANCH
        assert idx == 0
        checkKey = acct.privKey.child(account.EXTERNAL_BRANCH).child(0).key
        with pytest.raises(DecredError):
            acct.privKeyForAddress("")
        assert acct.privKeyForAddress(zerothAddr).key == checkKey
        ticketAddrs = acct.addTicketAddresses([])
        assert len(ticketAddrs) == 1
        assert ticketAddrs[0] == ticketAddr
        assert acct.hasPool()

        # Exercise setNode
        acct.setNode(1)
        assert acct.node == 1

        # Add a coinbase transaction output to the account.
        coinbase = newCoinbaseTx()
        cbUTXO = account.UTXO("addr",
                              ByteArray(b"id"),
                              1,
                              height=5,
                              satoshis=int(1e8))
        coinbase.addTxOut(msgtx.TxOut(int(1e8)))
        coinbaseID = coinbase.id()
        cbUTXO.txid = coinbaseID
        acct.addUTXO(cbUTXO)
        acct.addMempoolTx(coinbase)
        assert coinbaseID in acct.mempool
        # Send a block signal and have the transaction confirmed.
        sig = {"message": {"block": {"Tx": [{"TxID": coinbaseID}]}}}
        acct.blockchain.tipHeight = 5
        acct.blockchain.tx = lambda *a: coinbase
        acct.blockSignal(sig)
        assert coinbaseID not in acct.mempool
        assert cbUTXO.key() in acct.utxos
        maturity = 5 + nets.mainnet.CoinbaseMaturity
        assert acct.utxos[cbUTXO.key()].maturity == maturity
        assert acct.calcBalance(
            maturity).available == self.utxoTotal + cbUTXO.satoshis
        assert acct.calcBalance(0).available == self.utxoTotal
        acct.spendUTXOs([cbUTXO])
        assert acct.calcBalance(maturity).available == self.utxoTotal

        # make a ticket and a vote
        ticket = account.UTXO.parse(dcrdataUTXO)
        utxo.setTicketInfo(dcrdataTinfo)
        utxo.scriptPubKey = ticketScript
        utxo.parseScriptClass()
        acct.addUTXO(ticket)
        expVal = acct.calcBalance().available - ticket.satoshis
        voteTx = msgtx.MsgTx.new()
        voteTx.addTxIn(
            msgtx.TxIn(
                msgtx.OutPoint(reversed(ByteArray(ticket.txid)), ticket.vout,
                               0)))
        acct.blockchain.tx = lambda *a: voteTx
        acct.addressSignal(ticketAddr, "somehash")
        assert Signals.b.available == expVal

        # Detect a new utxo for the account.
        newVal = int(5e8)
        expVal = acct.calcBalance().available + newVal
        addr = acct.externalAddresses[1]
        a = addrlib.decodeAddress(addr, nets.mainnet)
        script = txscript.payToAddrScript(a)
        newTx = msgtx.MsgTx.new()
        op = msgtx.TxOut(newVal, script)
        newTx.addTxOut(op)
        acct.blockchain.tx = lambda *a: newTx
        utxo = account.UTXO(addr, ByteArray(b"txid"), 0, satoshis=newVal)
        acct.blockchain.txVout = lambda *a: utxo
        acct.addressSignal(addr, "somehash")
        assert Signals.b.available == expVal

        # test syncing. add a single output for external address #2
        acct.stakePool().getPurchaseInfo = lambda: None
        acct.stakePool().authorize = lambda a: True
        newVal = int(3e8)
        addr = acct.externalAddresses[2]
        a = addrlib.decodeAddress(addr, nets.mainnet)
        script = txscript.payToAddrScript(a)
        newTx = msgtx.MsgTx.new()
        op = msgtx.TxOut(newVal, script)
        newTx.addTxOut(op)
        utxo = account.UTXO(addr, ByteArray(b"txid"), 0, satoshis=newVal)

        def t4a(*a):
            acct.blockchain.txidsForAddr = lambda *a: []
            return [newTx.id()]

        acct.blockchain.txidsForAddr = t4a

        def utxos4a(*a):
            acct.blockchain.UTXOs = lambda *a: []
            return [utxo]

        acct.blockchain.UTXOs = utxos4a
        acct.blockchain.subscribeAddresses = lambda addrs, receiver: None
        acct.blockchain.subscribeBlocks = lambda a: None
        acct.sync()
        assert Signals.b.available == newVal

        # spend the utxo by sending it. Reusing newTx for convenience.
        changeVal = int(5e7)
        addr = acct.internalAddresses[0]
        change = account.UTXO(addr,
                              ByteArray(b"newtxid"),
                              0,
                              satoshis=changeVal)
        acct.blockchain.sendToAddress = lambda *a: (newTx, [utxo], [change])
        acct.sendToAddress(1, "recipient")
        assert Signals.b.available == changeVal

        # purchase some tickets. Reusing newTx for convenience again.
        ticket = account.UTXO.parse(dcrdataUTXO)
        ticket.setTicketInfo(dcrdataTinfo)
        ticket.scriptPubKey = ticketScript
        ticket.parseScriptClass()
        acct.blockchain.purchaseTickets = lambda *a: ([newTx, []], [],
                                                      [ticket])
        acct.signals.spentTickets = lambda: True
        acct.purchaseTickets(1, 1)
        assert Signals.b.total == changeVal + ticket.satoshis

        # revoke the ticket
        ticketTx = msgtx.MsgTx.new()
        op = msgtx.TxOut(ticket.satoshis, ticket.scriptPubKey)
        ticketTx.addTxOut(op)
        ticket.tinfo.status = "missed"
        redeemHash = addrlib.AddressScriptHash(
            txscript.extractStakeScriptHash(ticketScript, opcode.OP_SSTX),
            nets.mainnet,
        )
        acct.stakePool().purchaseInfo.ticketAddress = redeemHash.string()
        revoked = False

        def rev(*a):
            nonlocal revoked
            revoked = True

        acct.blockchain.tx = lambda *a: ticketTx
        acct.blockchain.revokeTicket = rev
        acct.revokeTickets()
        assert revoked

    def test_readAddrs(self):
        readAddrs = account.Account.readAddrs
        # We can use dicts instead of database.Buckets because readAddrs
        # only needs the "items" method.
        assert readAddrs({}) == []
        assert readAddrs({0: "zero", 1: "one"}) == ["zero", "one"]
        with pytest.raises(DecredError):
            readAddrs({0: "zero", 2: "two"})

    def test_unlock(self, monkeypatch):
        def mock_privateKey(self):
            raise crypto.CrazyKeyError

        monkeypatch.setattr(crypto.ExtendedKey, "privateKey", mock_privateKey)
        db = KeyValueDatabase(":memory:").child("tmp")
        acct = self.newAccount(db)
        assert not acct.isUnlocked()
        with pytest.raises(DecredError):
            acct.unlock(self.cryptoKey)

    def test_calcTicketProfits(self):
        class FakeAccount(account.Account):
            def __init__(self):
                pass

        db = KeyValueDatabase(":memory:").child("tmp")

        acct = FakeAccount()
        tDB = acct.ticketDB = db.child("tickets",
                                       datatypes=("TEXT", "BLOB"),
                                       blobber=account.UTXO)

        def newTinfo(poolFee, purchaseTxFee, spendTxFee, stakebase):
            return account.TicketInfo(
                status="",
                purchaseBlock=account.TinyBlock(ByteArray(0), 0),
                maturityHeight=0,
                expirationHeight=0,
                lotteryBlock=None,
                vote=None,
                revocation=None,
                poolFee=poolFee,
                purchaseTxFee=purchaseTxFee,
                spendTxFee=spendTxFee,
                stakebase=stakebase,
            )

        utxo = account.UTXO(
            address="",
            txHash=reversed(ByteArray("aa")),
            vout=0,
            scriptPubKey=None,
            satoshis=5,
        )

        tinfo = newTinfo(0, 1, 1, 1)
        utxo.tinfo = tinfo
        tDB["aa"] = utxo
        tinfo = newTinfo(1, 1, 1, 1)
        utxo.tinfo = tinfo
        tDB["ab"] = utxo
        tinfo = newTinfo(0, 1, 0, 0)
        utxo.tinfo = tinfo
        tDB["ac"] = utxo

        stakebases, poolFees, txFees = acct.calcTicketProfits()

        s, p, t = 2, 1, 5

        assert stakebases == s
        assert poolFees == p
        assert txFees == t

    def test_spentTicketInfo(self):
        class BlockchainFull:
            ticketInfo = lambda txid: account.TicketInfo(
                "mempool", None, 0, 0, None, True)

        class BlockchainEmpty:
            ticketInfo = lambda txid: account.TicketInfo("mempool", None, 0, 0)

        db = KeyValueDatabase(":memory:").child("tmp")
        acct = self.newAccount(db, BlockchainFull)
        assert acct.spentTicketInfo("some_txid")
        acct = self.newAccount(db, BlockchainEmpty)
        assert not acct.spentTicketInfo("some_txid")

    def test_spendTicket(self, monkeypatch):
        """
        Test updating spent tickets.
        """
        vote = ("010000000200000000000000000000000000000000000000000000000000"
                "00000000000000ffffffff00ffffffff571ce9fb0c52ae22c3a6480cbf6d"
                "30ff76bdffbf54b6e081eb218aa3a0ca2bc40000000001ffffffff040000"
                "0000000000000000266a2432c0c546b332f7abf51f3fc73f4482185f4c09"
                "61625763a766774237280000007f75050000000000000000000000086a06"
                "050008000000900102000000000000001abb76a914781f472a926da0bb7c"
                "e9eec7f4d434de21015cae88acd9b293890100000000001abb76a9149087"
                "5ba5fee5f35352e9171d40190e796b1b152788ac000000000000000002a6"
                "3895010000000000000000ffffffff020000c47b00880100000049700500"
                "0600000091483045022100c1ec49cb687fa2421e76b534ced49563b3de1e"
                "c6407b1bbfda26752fbdedc88302204988390ea3be77324909781322a46b"
                "463d00dd14718f0964b9536b5eef4e35570147512103af3c24d005ca8b75"
                "5e7167617f3a5b4c60a65f8318a7fcd1b0cacb1abd2a97fc21027b81bc16"
                "954e28adb832248140eb58bedb6078ae5f4dabf21fde5a8ab7135cb652ae")

        revocation = ("010000000139cb023fdcf6fcd18273c7395f51084721d0e4baf0ca"
                      "3ee9590ad63a411a4cb30000000001ffffffff02f0ff0100000000"
                      "0000001abc76a914781f472a926da0bb7ce9eec7f4d434de21015c"
                      "ae88ac8fcc07ba0100000000001abc76a914f90abbb67dc9257efa"
                      "6ab24eb88e2755f34b1f7f88ac0000000000000000018ad609ba01"
                      "000000296505001000000091483045022100bc9a694d0864df030e"
                      "6edea181a2e2385dfbf93396b5792899a65f19c9afd67a02206052"
                      "192b5631e062f4497706276ec514a14cdfa8035ef6c7dca0df7120"
                      "84618e0147512103af3c24d005ca8b755e7167617f3a5b4c60a65f"
                      "8318a7fcd1b0cacb1abd2a97fc21027b81bc16954e28adb8322481"
                      "40eb58bedb6078ae5f4dabf21fde5a8ab7135cb652ae")

        # Tickets can be found on testnet3.
        ticketVotedTxid = (
            "c42bcaa0a38a21eb81e0b654bfffbd76ff306dbf0c48a6c322ae520cfbe91c57")
        ticketRevokedTxid = (
            "b34c1a413ad60a59e93ecaf0bae4d0214708515f39c77382d1fcf6dc3f02cb39")

        voteTxid = "aa19094e404a1ee056760bdb1b7ed1b6c8e5f1d97752335eddbfdfa19e76c262"
        revocationTxid = (
            "d85694ba7aae060667b393558cd96c2df2926426f80db16a18bf4fc9102b0953")

        class Dummy:
            pass

        class FakeAccount(account.Account):
            def __init__(self):
                self.signals = Dummy()
                self.blockchain = Dummy()
                self.netParams = nets.testnet

        db = KeyValueDatabase(":memory:").child("tmp")

        acct = FakeAccount()
        tDB = acct.ticketDB = db.child("tickets",
                                       datatypes=("TEXT", "BLOB"),
                                       blobber=account.UTXO)

        def newTinfo(status):
            return account.TicketInfo(
                status=status,
                purchaseBlock=account.TinyBlock(ByteArray(0), 0),
                maturityHeight=0,
                expirationHeight=0,
            )

        utxo = account.UTXO(
            address="",
            txHash=reversed(ByteArray("aa")),
            vout=0,
            scriptPubKey=None,
            satoshis=5,
        )

        txidToTinfo = {
            voteTxid: newTinfo("vote"),
            revocationTxid: newTinfo("revocation"),
        }
        acct.blockchain.ticketInfoForSpendingTx = lambda txid, netParams: txidToTinfo[
            txid]

        tDB[ticketVotedTxid] = utxo
        tDB[ticketRevokedTxid] = utxo

        v = msgtx.MsgTx.deserialize(ByteArray(vote))

        acct.spendTicket(v)
        tinfo = tDB[ticketVotedTxid].tinfo
        assert tinfo.status == "vote"

        rev = msgtx.MsgTx.deserialize(ByteArray(revocation))

        acct.spendTicket(rev)
        tinfo = tDB[ticketRevokedTxid].tinfo
        assert tinfo.status == "revocation"

        # Test the error case.
        def mock_isSSGen(tx):
            return False

        monkeypatch.setattr(txscript, "isSSGen", mock_isSSGen)
        with pytest.raises(DecredError):
            acct.spendTicket(v)

    def test_updateSpentTickets(self):
        class Dummy:
            pass

        class FakeAccount(account.Account):
            def __init__(self):
                self.signals = Dummy()
                pool = Dummy()
                pool.purchaseInfo = Dummy()
                pool.purchaseInfo.ticketAddress = "ticketaddress"
                self.stakePools = [pool]
                self.blockchain = Dummy()
                self.mempool = {}
                self.txs = {}
                self.utxos = {}
                self.netParams = nets.testnet

        db = KeyValueDatabase(":memory:").child("tmp")

        acct = FakeAccount()
        tDB = acct.ticketDB = db.child("tickets",
                                       datatypes=("TEXT", "BLOB"),
                                       blobber=account.UTXO)

        def fail():
            assert False

        acct.signals.spentTickets = fail

        # tickets and ticketDB empty, noop
        acct.updateSpentTickets()

        stakeSubmission = ByteArray(opcode.OP_SSTX)
        stakeSubmission += opcode.OP_HASH160
        stakeSubmission += opcode.OP_DATA_20
        stakeSubmission += 1 << (8 * 19)
        stakeSubmission += opcode.OP_EQUAL

        def newTinfo(status="live"):
            return account.TicketInfo(
                status=status,
                purchaseBlock=account.TinyBlock(ByteArray(0), 42),
                maturityHeight=0,
                expirationHeight=0,
                lotteryBlock=None,
                vote=None,
                revocation=None,
                poolFee=0,
                purchaseTxFee=1,
                spendTxFee=0,
                stakebase=0,
            )

        txid = "aa"

        def utxoWithTxid(txid):
            return account.UTXO(
                address="ticketaddress",
                txHash=reversed(ByteArray(txid)),
                vout=0,
                scriptPubKey=stakeSubmission,
                satoshis=2,
                tinfo=newTinfo(),
            )

        utxo = utxoWithTxid(txid)

        def txWithTxid(txid):
            txInOne = msgtx.TxIn(msgtx.OutPoint(ByteArray("ff"), 0, 0),
                                 valueIn=1)
            txInTwo = msgtx.TxIn(None, valueIn=3)
            txOutOne = msgtx.TxOut(pkScript=stakeSubmission, value=3)
            txOutTwo = msgtx.TxOut()
            txOutThree = msgtx.TxOut()
            txOutFour = msgtx.TxOut()
            txOutFive = msgtx.TxOut()
            txsIn = [txInOne, txInTwo]
            txsOut = [txOutOne, txOutTwo, txOutThree, txOutFour, txOutFive]
            return msgtx.MsgTx(reversed(ByteArray(txid)), None, None, txsIn,
                               txsOut, None, None)

        tx = txWithTxid(txid)
        utxo.tinfo.status = "mempool"
        tDB[txid] = utxo
        acct.mempool[txid] = tx

        # mempool and ticketDB have the same txid and status, noop
        acct.updateSpentTickets()

        called = False

        def ok():
            nonlocal called
            called = True

        acct.signals.spentTickets = ok

        tinfos = {
            "aa": utxo.tinfo,
            "ab": newTinfo(),
            "ac": newTinfo(),
            "ad": newTinfo(),
            "ae": newTinfo(status="unconfirmed"),
        }

        txs = {k: txWithTxid(k) for k in tinfos.keys() if k != "ab"}

        blockheader = Dummy()
        blockheader.height = 0
        blockheader.timestamp = 0

        vote = "ff"
        revocation = "fe"

        def setVoteOrRevoke(txid):
            if txid == vote:
                ticket = tDB["ac"]
                ticket.tinfo.vote = reversed(ByteArray(vote))
            if txid == revocation:
                ticket = tDB["ad"]
                ticket.tinfo.revocation = reversed(ByteArray(revocation))

        acct.spendTicket = lambda tx: setVoteOrRevoke(
            reversed(tx.cachedH).hex())
        acct.blockchain.tx = lambda txid: txs[txid]
        acct.blockchain.ticketInfo = lambda txid: tinfos[txid]
        acct.blockchain.blockForTx = lambda *args: blockheader
        acct.utxos = {k + "#0": utxoWithTxid(k) for k in txs.keys()}
        acct.mempool = {"ab": txWithTxid("ab")}

        txs[vote] = txWithTxid(vote)
        txs[revocation] = txWithTxid(revocation)

        # Live tickets are now different than database.
        acct.updateSpentTickets()
        assert called

        # The tickets are now stored in the database.
        assert "ab" in tDB and "ac" in tDB and "ad" in tDB
        # They are unspent tickets.
        ut = acct.unspentTickets()
        assert "ab" in ut and "ac" in ut and "ad" in ut

        called = False
        txid = "ac"
        tinfos["ac"].vote = reversed(ByteArray(vote))
        del acct.utxos[txid + "#0"]

        # A ticket has been voted.
        acct.updateSpentTickets()
        assert called
        # It is an voted ticket.
        assert txid in acct.votedTickets() and txid not in acct.unspentTickets(
        )

        called = False
        txid = "ad"
        tinfos["ad"].revocation = reversed(ByteArray(revocation))
        del acct.utxos[txid + "#0"]

        # A ticket has been revoked.
        acct.updateSpentTickets()
        assert called
        # It is a revoked ticket.
        assert txid in acct.revokedTickets(
        ) and txid not in acct.unspentTickets()

        txid = "af"
        called = False
        tDB[txid] = utxo

        # A txid is in the ticketDB but not in utxos or mempool or the
        # blockchain.
        acct.updateSpentTickets()
        assert called
        # It was removed.
        assert txid not in tDB

    def test_nextBranchAddress(self):
        class FakeExtendedKey:
            def child(self, i):
                raise crypto.CrazyKeyError

        db = KeyValueDatabase(":memory:").child("tmp")
        acct = self.newAccount(db)
        with pytest.raises(DecredError):
            acct.nextBranchAddress(FakeExtendedKey(), [], {})

    def test_nextExternalInternalAddress(self):
        class FakeBlockchain:
            subscribeAddresses = lambda addr, receiver: None

        db = KeyValueDatabase(":memory:").child("tmp")
        acct = self.newAccount(db, FakeBlockchain)
        acct.unlock(self.cryptoKey)
        assert acct.nextExternalAddress(
        ) == "DseZQKiWhN3ceBwDJEgGhmwKD3fMbwF4ugf"
        assert acct.nextInternalAddress(
        ) == "DskHpgbEb6hqkuHchHhtyojpehFToEtjQSo"

    def test_gap_handling(self):
        db = KeyValueDatabase(":memory:").child("tmp")
        internalAddrs = [
            "DskHpgbEb6hqkuHchHhtyojpehFToEtjQSo",
            "Dsm4oCLnLraGDedfU5unareezTNT75kPbRb",
            "DsdN6A9bWhKKJ7PAGdcDLxQrYPKEnjnDv2N",
            "Dsifz8eRHvQrfaPXgHHLMDHZopFJq2pBPU9",
            "DsmmzJiTTmpafdt2xx7LGk8ZW8cdAKe53Zx",
            "DsVB47P4N23PK5C1RyaJdqkQDzVuCDKGQbj",
            "DsouVzScdUUswvtCAv6nxRzi2MeufpWnELD",
            "DsSoquT5SiDPfksgnveLv3r524k1v8RamYm",
            "DsbVDrcfhbdLy4YaSmThmWN47xhxT6FC8XB",
            "DsoSrtGYKruQLbvhA92xeJ6eeuAR1GGJVQA",
        ]

        externalAddrs = [
            "DsmP6rBEou9Qr7jaHnFz8jTfrUxngWqrKBw",
            "DseZQKiWhN3ceBwDJEgGhmwKD3fMbwF4ugf",
            "DsVxujP11N72PJ46wgU9sewptWztYUy3L7o",
            "DsYa4UBo379cRMCTpDLxYVfpMvMNBdAGrGS",
            "DsVSEmQozEnsZ9B3D4Xn4H7kEedDyREgc18",
            "DsifDp8p9mRocNj7XNNhGAsYtfWhccc2cry",
            "DsV78j9aF8NBwegbcpPkHYy9cnPM39jWXZm",
            "DsoLa9Rt1L6qAVT9gSNE5F5XSDLGoppMdwC",
            "DsXojqzUTnyRciPDuCFFoKyvQFd6nQMn7Gb",
            "DsWp4nShu8WxefgoPej1rNv4gfwy5AoULfV",
        ]

        ogGapLimit = account.DefaultGapLimit
        account.DefaultGapLimit = gapLimit = 5
        acct = self.newAccount(db)
        with pytest.raises(DecredError):
            acct.generateGapAddresses()
        acct.unlock(self.cryptoKey)
        acct.generateGapAddresses()
        acct.gapLimit = gapLimit

        listsAreEqual = lambda a, b: len(a) == len(b) and all(
            x == y for x, y in zip(a, b))

        watchAddrs = internalAddrs[:gapLimit] + externalAddrs[:gapLimit + 1]
        assert len(acct.watchAddrs()) == len(watchAddrs)
        assert set(acct.watchAddrs()) == set(watchAddrs)
        assert listsAreEqual(acct.internalAddresses, internalAddrs[:gapLimit])

        # The external branch starts with the "last seen" at the zeroth address, so
        # has one additional address to start.
        assert listsAreEqual(acct.externalAddresses,
                             externalAddrs[:gapLimit + 1])

        # Open the account to generate addresses.
        acct.addTxid(internalAddrs[0], "C4fA6958A1847D")
        newAddrs = acct.generateGapAddresses()
        assert len(newAddrs) == 1
        assert newAddrs[0] == internalAddrs[5]

        # The zeroth external address is considered "seen", so this should not
        # change anything.
        acct.addTxid(externalAddrs[0], "C4fA6958A1847D")
        newAddrs = acct.generateGapAddresses()
        assert len(newAddrs) == 0

        # Mark the 1st address as seen.
        acct.addTxid(externalAddrs[1], "C4fA6958A1847D")
        newAddrs = acct.generateGapAddresses()
        assert len(newAddrs) == 1
        assert externalAddrs[1] == acct.currentAddress()

        # cursor should be at index 0, last seen 1, max index 6, so calling
        # nextExternalAddress 5 time should put the cursor at index 6, which is
        # the gap limit.
        for i in range(5):
            acct.nextExternalAddress()
        assert acct.currentAddress() == externalAddrs[6]

        # one more should wrap the cursor back to 1, not zero, so the current
        # address is lastSeenExt(=1) + cursor(=1) = 2
        a1 = acct.nextExternalAddress()
        assert acct.currentAddress() == externalAddrs[2]
        assert a1 == acct.currentAddress()

        # Sanity check that internal addresses are wrapping too.
        for i in range(20):
            acct.nextInternalAddress()
        addrs = acct.internalAddresses
        assert addrs[len(addrs) - 1] == internalAddrs[5]

        account.DefaultGapLimit = ogGapLimit

    def test_publicExtendedKey(self):
        db = KeyValueDatabase(":memory:").child("tmp")
        acct = self.newAccount(db)
        assert acct.publicExtendedKey(self.cryptoKey).string() == (
            "dpubZFbjvDBkxBN5CAeqNXCkNAFizVcdMUPKJYp1vLXzkiq16yMFzREd"
            "eQ1AzaGJ7uBBhZFBruG31MGZ5SkwevLK4PiLEFSEaZm143xgvBgkWhQ")

    def test_txsExistForKey(self):
        has = True

        class Blockchain:
            params = nets.mainnet
            addrsHaveTxs = lambda a: has

        xk = crypto.ExtendedKey.new(testSeed)
        assert account.Account.txsExistForKey(xk, Blockchain)
        has = False
        assert not account.Account.txsExistForKey(xk, Blockchain)