Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

multi: Add accountless ticket purchasing #22

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 68 additions & 24 deletions pydecred/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
support.
"""

import time
from tinydecred.wallet.accounts import Account
from tinydecred.util import tinyjson, helpers
from tinydecred.crypto.crypto import AddressSecpPubKey, CrazyKeyError
Expand Down Expand Up @@ -115,6 +116,10 @@ def __fromjson__(obj):
acct = Account.__fromjson__(obj, cls=DecredAccount)
acct.tickets = obj["tickets"]
acct.stakePools = obj["stakePools"]
# Temp fix for buck, as there will be no ID yet
for i in range(len(acct.stakePools)):
if acct.stakePools[i].ID < 0:
acct.stakePools[i].ID = i
acct.updateStakeStats()
return acct
def open(self, pw):
Expand Down Expand Up @@ -225,15 +230,31 @@ def votingAddress(self):
AddressSecpPubkey: The address object.
"""
return AddressSecpPubKey(self.votingKey().pub.serializeCompressed(), self.net).string()

def addPool(self, pool):
"""
Add the specified pool to the list of stakepools we can use.

Args:
pool (vsp.VotingServiceProvider): The stake pool object.
"""
assert isinstance(pool, VotingServiceProvider)
# If this a new pool, give it an ID one more than the highest.
if pool.ID < 0:
pool.ID = 0
if len(self.stakePools) > 0:
pool.ID = max([p.ID for p in self.stakePools]) + 1
self.stakePools = [pool] + [p for p in self.stakePools if p.ID !=
pool.ID]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separated add and set because accountless should have different data to set each time we buy a ticket whenever we start to derive a new address for every ticket.


def setPool(self, pool):
"""
Set the specified pool as the default.
Set the specified pool for use.

Args:
pool (vsp.VotingServiceProvider): The stake pool object.
"""
assert isinstance(pool, VotingServiceProvider)
self.stakePools = [pool] + [p for p in self.stakePools if p.apiKey != pool.apiKey]
bc = self.blockchain
addr = pool.purchaseInfo.ticketAddress
for txid in bc.txsForAddr(addr):
Expand Down Expand Up @@ -349,33 +370,56 @@ def purchaseTickets(self, qty, price):
prepare the TicketRequest and KeySource and gather some other account-
related information.
"""
pool = self.stakePool()
allTxs = [[], []]

# If accountless, purchase tickets one at a time.
if pool.isAccountless:
for i in range(qty):
# TODO use a new voting address every time.
addr = self.votingAddress()
pool.authorize(addr, self.net)
self.setPool(pool)
self._purchaseTickets(pool, allTxs, 1, price)
# dcrdata needs some time inbetween requests. This should
# probably be randomized to increase privacy anyway.
if qty > 1 and i < qty:
time.sleep(2)
else:
self._purchaseTickets(pool, allTxs, qty, price)
if allTxs[0]:
for tx in allTxs[0]:
# Add the split transactions
self.addMempoolTx(tx)
for txs in allTxs[1]:
# Add all tickets
for tx in txs:
self.addMempoolTx(tx)
# Store the txids.
self.tickets.extend([tx.txid() for tx in txs])
return allTxs[1]

def _purchaseTickets(self, pool, allTxs, qty, price):
keysource = KeySource(
priv = self.getPrivKeyForAddress,
internal = self.nextInternalAddress,
priv=self.getPrivKeyForAddress,
internal=self.nextInternalAddress,
)
pool = self.stakePool()
pi = pool.purchaseInfo
req = TicketRequest(
minConf = 0,
expiry = 0,
spendLimit = int(round(price*qty*1.1*1e8)), # convert to atoms here
poolAddress = pi.poolAddress,
votingAddress = pi.ticketAddress,
ticketFee = 0, # use network default
poolFees = pi.poolFees,
count = qty,
txFee = 0, # use network default
minConf=0,
expiry=0,
spendLimit=int(round(price*qty*1.1*1e8)), # convert to atoms here
poolAddress=pi.poolAddress,
votingAddress=pi.ticketAddress,
ticketFee=0, # use network default
poolFees=pi.poolFees,
count=qty,
txFee=0, # use network default
)
txs, spentUTXOs, newUTXOs = self.blockchain.purchaseTickets(keysource, self.getUTXOs, req)
if txs:
# Add the split transactions
self.addMempoolTx(txs[0])
# Add all tickets
for tx in txs[1]:
self.addMempoolTx(tx)
# Store the txids.
self.tickets.extend([tx.txid() for tx in txs[1]])
return txs[1]
allTxs[0].append(txs[0])
allTxs[1].append(txs[1])

def sync(self, blockchain, signals):
"""
Synchronize the UTXO set with the server. This should be the first
Expand Down Expand Up @@ -424,4 +468,4 @@ def sync(self, blockchain, signals):

return True

tinyjson.register(DecredAccount, "DecredAccount")
tinyjson.register(DecredAccount, "DecredAccount")
101 changes: 100 additions & 1 deletion pydecred/dcrdata.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""
Copyright (c) 2019, Brian Stafford
Copyright (c) 2019, the Decred developers
See LICENSE for details

DcrdataClient.endpointList() for available enpoints.
Expand Down Expand Up @@ -167,7 +168,7 @@ def endpointList(self):
return [entry[1] for entry in self.listEntries]
def endpointGuide(self):
"""
Print on endpoint per line.
Print one endpoint per line.
Each line shows a translation from Python notation to a URL.
"""
print("\n".join(["%s -> %s" % entry for entry in self.listEntries]))
Expand Down Expand Up @@ -386,6 +387,93 @@ def __tojson__(self):

tinyjson.register(TicketInfo, "TicketInfo")


class AgendaChoices:
"""
Agenda choices such as abstain, yes, no.
"""
def __init__(self, ID, description, bits, isabstain,
isno, count, progress):
self.id = ID
self.description = description
self.bits = bits
self.isabstain = isabstain
self.isno = isno
self.count = count
self.progress = progress

@staticmethod
def parse(obj):
return AgendaChoices(
ID=obj["id"],
description=obj["description"],
bits=obj["bits"],
isabstain=obj["isabstain"],
isno=obj["isno"],
count=obj["count"],
progress=obj["progress"],
)


class Agenda:
"""
An agenda with name, description, and AgendaChoices.
"""
def __init__(self, ID, description, mask, starttime, expiretime,
status, quorumprogress, choices):
self.id = ID
self.description = description
self.mask = mask
self.starttime = starttime
self.expiretime = expiretime
self.status = status
self.quorumprogress = quorumprogress
self.choices = choices

@staticmethod
def parse(obj):
return Agenda(
ID=obj["id"],
description=obj["description"],
mask=obj["mask"],
starttime=obj["starttime"],
expiretime=obj["expiretime"],
status=obj["status"],
quorumprogress=obj["quorumprogress"],
choices=[AgendaChoices.parse(choice) for choice in obj["choices"]],
)


class AgendasInfo:
"""
All current agenda information for the current network. agendas contains
a list of Agenda.
"""
def __init__(self, currentheight, startheight, endheight, HASH,
voteversion, quorum, totalvotes, agendas):
self.currentheight = currentheight
self.startheight = startheight
self.endheight = endheight
self.hash = HASH
self.voteversion = voteversion
self.quorum = quorum
self.totalvotes = totalvotes
self.agendas = agendas

@staticmethod
def parse(obj):
return AgendasInfo(
currentheight=obj["currentheight"],
startheight=obj["startheight"],
endheight=obj["endheight"],
HASH=obj["hash"],
voteversion=obj["voteversion"],
quorum=obj["quorum"],
totalvotes=obj["totalvotes"],
agendas=[Agenda.parse(agenda) for agenda in obj["agendas"]],
)


class UTXO(object):
"""
The UTXO is part of the wallet API. BlockChains create and parse UTXO
Expand Down Expand Up @@ -650,6 +738,16 @@ def subscribeBlocks(self, receiver):
"""
self.blockReceiver = receiver
self.dcrdata.subscribeBlocks()

def getAgendasInfo(self):
"""
The agendas info that is used for voting.

Returns:
AgendasInfo: the current agendas.
"""
return AgendasInfo.parse(self.dcrdata.stake.vote.info())

def subscribeAddresses(self, addrs, receiver=None):
"""
Subscribe to notifications for the provided addresses.
Expand All @@ -665,6 +763,7 @@ def subscribeAddresses(self, addrs, receiver=None):
elif self.addressReceiver == None:
raise Exception("must set receiver to subscribe to addresses")
self.dcrdata.subscribeAddresses(addrs)

def processNewUTXO(self, utxo):
"""
Processes an as-received blockchain utxo.
Expand Down
Loading