Skip to content

Commit

Permalink
multi: Add accountless ticket purchases
Browse files Browse the repository at this point in the history
If a vsp has APIVersionsSupported 3 allow accountless ticket
purchasing for that pool.
  • Loading branch information
JoeGruffins committed Nov 28, 2019
1 parent 43542ad commit 349da8c
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 36 deletions.
72 changes: 57 additions & 15 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,29 @@ 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]
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,11 +368,40 @@ 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,
Expand All @@ -367,15 +415,9 @@ def purchaseTickets(self, qty, price):
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 +466,4 @@ def sync(self, blockchain, signals):

return True

tinyjson.register(DecredAccount, "DecredAccount")
tinyjson.register(DecredAccount, "DecredAccount")
55 changes: 47 additions & 8 deletions pydecred/vsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
from tinydecred.crypto import crypto
from tinydecred.crypto.bytearray import ByteArray


# joe's test stakepool
# TODO: remove
dcrstakedinner = {'APIEnabled': True, 'APIVersionsSupported': [1, 2, 3],
'Network': 'testnet', 'URL': 'https://www.dcrstakedinner.com',
'Launched': 1543421580, 'LastUpdated': 1574655889,
'Immature': 0, 'Live': 0, 'Voted': 0, 'Missed': 0,
'PoolFees': 0.5, 'ProportionLive': 0, 'ProportionMissed': 0,
'UserCount': 0, 'UserCountActive': 0, 'Version': '1.5.0-pre+dev'}

def resultIsSuccess(res):
"""
JSON-decoded stake pool responses have a common base structure that enables
Expand Down Expand Up @@ -125,7 +135,7 @@ class VotingServiceProvider(object):
the VSP API. VotingServiceProvider is JSON-serializable if used with
tinyjson, so can be stored as part of an Account in the wallet.
"""
def __init__(self, url, apiKey):
def __init__(self, url, apiKey, isAccountless):
"""
Args:
url (string): The stake pool URL.
Expand All @@ -140,6 +150,8 @@ def __init__(self, url, apiKey):
# The signingAddress (also called a votingAddress in other contexts) is
# the P2SH 1-of-2 multi-sig address that spends SSTX outputs.
self.signingAddress = None
self.isAccountless = isAccountless
self.ID = -1
self.apiKey = apiKey
self.lastConnection = 0
self.purchaseInfo = None
Expand All @@ -151,10 +163,21 @@ def __tojson__(self):
"apiKey": self.apiKey,
"purchaseInfo": self.purchaseInfo,
"stats": self.stats,
"isAccountless": self.isAccountless,
"ID": self.ID,
}
@staticmethod
def __fromjson__(obj):
sp = VotingServiceProvider(obj["url"], obj["apiKey"])
sp = VotingServiceProvider(obj["url"], obj["apiKey"],
False)
# TODO: These if's can be removed. They are here in case these keys do
# not exist yet.
if "isAccountless" in obj:
sp.isAccountless = obj["isAccountless"]
if "ID" in obj:
sp.ID = obj["ID"]
else:
sp.ID = -1
sp.purchaseInfo = obj["purchaseInfo"]
sp.stats = obj["stats"]
return sp
Expand All @@ -170,6 +193,8 @@ def providers(net):
list(object): The vsp list.
"""
vsps = tinyhttp.get("https://api.decred.org/?c=gsd")
# TODO remove adding dcrstakedinner
vsps["stakedinner"] = dcrstakedinner
network = "testnet" if net.Name == "testnet3" else net.Name
return [vsp for vsp in vsps.values() if vsp["Network"] == network]
def apiPath(self, command):
Expand All @@ -191,6 +216,14 @@ def headers(self):
object: The headers as a Python object.
"""
return {"Authorization": "Bearer %s" % self.apiKey}
def accountlessData(self, addr):
"""
Make the API request headers.
Returns:
object: The headers as a Python object.
"""
return {"UserPubKeyAddr": "%s" % addr}
def validate(self, addr):
"""
Validate performs some checks that the PurchaseInfo provided by the
Expand Down Expand Up @@ -233,7 +266,7 @@ def authorize(self, address, net):
# First try to get the purchase info directly.
self.net = net
try:
self.getPurchaseInfo()
self.getPurchaseInfo(address)
self.validate(address)
except Exception as e:
alreadyRegistered = isinstance(self.err, dict) and "code" in self.err and self.err["code"] == 9
Expand All @@ -244,20 +277,26 @@ def authorize(self, address, net):
data = { "UserPubKeyAddr": address }
res = tinyhttp.post(self.apiPath("address"), data, headers=self.headers(), urlEncode=True)
if resultIsSuccess(res):
self.getPurchaseInfo()
self.getPurchaseInfo(address)
self.validate(address)
else:
raise Exception("unexpected response from 'address': %s" % repr(res))
def getPurchaseInfo(self):
def getPurchaseInfo(self, addr):
"""
Get the purchase info from the stake pool API.
Returns:
PurchaseInfo: The PurchaseInfo object.
"""
# An error is returned if the address isn't yet set
# {'status': 'error', 'code': 9, 'message': 'purchaseinfo error - no address submitted', 'data': None}
res = tinyhttp.get(self.apiPath("getpurchaseinfo"), headers=self.headers())
if self.isAccountless:
# Accountless vsp gets purchaseinfo from api/purchaseticket
# endpoint.
res = tinyhttp.post(self.apiPath("purchaseticket"),
self.accountlessData(addr), urlEncode=True)
else:
# An error is returned if the address isn't yet set
# {'status': 'error', 'code': 9, 'message': 'purchaseinfo error - no address submitted', 'data': None}
res = tinyhttp.get(self.apiPath("getpurchaseinfo"), headers=self.headers())
if resultIsSuccess(res):
pi = PurchaseInfo(res["data"])
# check the script hash
Expand Down
Loading

0 comments on commit 349da8c

Please sign in to comment.