passing tests

This commit is contained in:
Lex Berezhny 2018-06-13 20:57:57 -04:00
parent 99ae39012f
commit e9fbc875e0
18 changed files with 231 additions and 293 deletions

View file

@ -4,7 +4,11 @@ python:
- "2.7"
- "3.6"
install: pip install tox-travis coverage
install:
- pip install tox-travis coverage
- pushd .. && git clone https://github.com/lbryio/electrumx.git --branch packages && popd
- pushd .. && git clone https://github.com/lbryio/orchstr8.git && popd
script: tox
after_success:

View file

View file

@ -17,22 +17,23 @@ class BasicTransactionTests(IntegrationTestCase):
address = await account1.receiving.get_or_create_usable_address().asFuture(asyncio.get_event_loop())
sendtxid = await self.blockchain.send_to_address(address.decode(), 5.5)
await self.on_transaction(sendtxid)
await self.on_transaction(sendtxid) #mempool
await self.blockchain.generate(1)
await asyncio.sleep(5)
await self.on_transaction(sendtxid) #confirmed
self.assertEqual(await self.get_balance(account1), int(5.5*COIN))
self.assertEqual(await self.get_balance(account2), 0)
address = await account2.receiving.get_or_create_usable_address().asFuture(asyncio.get_event_loop())
tx = await self.ledger.transaction_class.pay(
[self.ledger.transaction_class.output_class.pay_pubkey_hash(2, self.ledger.address_to_hash160(address))],
[self.ledger.transaction_class.output_class.pay_pubkey_hash(2*COIN, self.ledger.address_to_hash160(address))],
[account1], account1
).asFuture(asyncio.get_event_loop())
await self.blockchain.decode_raw_transaction(tx)
await self.broadcast(tx)
await self.on_transaction(tx.id.decode())
await self.lbrycrd.generate(1)
self.assertEqual(await self.get_balance(account1), int(3.5*COIN))
self.assertEqual(await self.get_balance(account2), int(2.0*COIN))
await self.on_transaction(tx.hex_id.decode()) #mempool
await self.blockchain.generate(1)
await self.on_transaction(tx.hex_id.decode()) #confirmed
self.assertEqual(round(await self.get_balance(account1)/COIN, 1), 3.5)
self.assertEqual(round(await self.get_balance(account2)/COIN, 1), 2.0)

View file

@ -41,13 +41,13 @@ class TestKeyChain(unittest.TestCase):
# case #2: only one new addressed needed
keys = yield account.receiving.get_addresses(None, True)
yield self.ledger.db.set_address_history(keys[19]['address'], 'a:1:')
yield self.ledger.db.set_address_history(keys[19]['address'], b'a:1:')
new_keys = yield account.receiving.ensure_address_gap()
self.assertEqual(len(new_keys), 1)
# case #3: 20 addresses needed
keys = yield account.receiving.get_addresses(None, True)
yield self.ledger.db.set_address_history(keys[0]['address'], 'a:1:')
yield self.ledger.db.set_address_history(keys[0]['address'], b'a:1:')
new_keys = yield account.receiving.ensure_address_gap()
self.assertEqual(len(new_keys), 20)

View file

@ -1,11 +1,11 @@
import unittest
from twisted.trial import unittest
from types import GeneratorType
from torba.coin.bitcoinsegwit import MainNetLedger
from torba.coinselection import CoinSelector, MAXIMUM_TRIES
from torba.constants import CENT
from torba.manager import WalletManager
from .test_transaction import Output, get_output as utxo
from .test_transaction import get_output as utxo
NULL_HASH = b'\x00'*32
@ -13,7 +13,7 @@ NULL_HASH = b'\x00'*32
def search(*args, **kwargs):
selection = CoinSelector(*args, **kwargs).branch_and_bound()
return [o.output.amount for o in selection] if selection else selection
return [o.txo.amount for o in selection] if selection else selection
class BaseSelectionTestCase(unittest.TestCase):
@ -23,7 +23,7 @@ class BaseSelectionTestCase(unittest.TestCase):
return self.ledger.db.start()
def estimates(self, *args):
txos = args if isinstance(args[0], Output) else args[0]
txos = args[0] if isinstance(args[0], (GeneratorType, list)) else args
return [txo.get_estimator(self.ledger) for txo in txos]
@ -44,7 +44,7 @@ class TestCoinSelectionTests(BaseSelectionTestCase):
self.assertEqual(selector.tries, 201)
def test_exact_match(self):
fee = utxo(CENT).get_estimator(self.coin).fee
fee = utxo(CENT).get_estimator(self.ledger).fee
utxo_pool = self.estimates(
utxo(CENT + fee),
utxo(CENT),
@ -52,7 +52,7 @@ class TestCoinSelectionTests(BaseSelectionTestCase):
)
selector = CoinSelector(utxo_pool, CENT, 0)
match = selector.select()
self.assertEqual([CENT + fee], [c.output.amount for c in match])
self.assertEqual([CENT + fee], [c.txo.amount for c in match])
self.assertTrue(selector.exact_match)
def test_random_draw(self):
@ -63,7 +63,7 @@ class TestCoinSelectionTests(BaseSelectionTestCase):
)
selector = CoinSelector(utxo_pool, CENT, 0, '\x00')
match = selector.select()
self.assertEqual([2 * CENT], [c.output.amount for c in match])
self.assertEqual([2 * CENT], [c.txo.amount for c in match])
self.assertFalse(selector.exact_match)
@ -78,10 +78,6 @@ class TestOfficialBitcoinCoinSelectionTests(BaseSelectionTestCase):
# Branch and Bound coin selection white paper:
# https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf
def setUp(self):
ledger = WalletManager().get_or_create_ledger(BTC.get_id())
self.coin = BTC(ledger, 0)
def make_hard_case(self, utxos):
target = 0
utxo_pool = []
@ -93,6 +89,8 @@ class TestOfficialBitcoinCoinSelectionTests(BaseSelectionTestCase):
return self.estimates(utxo_pool), target
def test_branch_and_bound_coin_selection(self):
self.ledger.fee_per_byte = 0
utxo_pool = self.estimates(
utxo(1 * CENT),
utxo(2 * CENT),

View file

@ -1,3 +1,4 @@
import six
from binascii import hexlify
from twisted.trial import unittest
from twisted.internet import defer
@ -6,6 +7,9 @@ from torba.coin.bitcoinsegwit import MainNetLedger
from .test_transaction import get_transaction
if six.PY3:
buffer = memoryview
class MockNetwork:
@ -53,7 +57,7 @@ class TestSynchronization(unittest.TestCase):
self.assertEqual(self.ledger.network.get_transaction_called, [b'abc', b'def', b'ghi'])
address_details = yield self.ledger.db.get_address(address)
self.assertEqual(address_details['history'], b'abc:1:def:2:ghi:3:')
self.assertEqual(address_details['history'], buffer(b'abc:1:def:2:ghi:3:'))
self.ledger.network.get_history_called = []
self.ledger.network.get_transaction_called = []
@ -69,4 +73,4 @@ class TestSynchronization(unittest.TestCase):
self.assertEqual(self.ledger.network.get_history_called, [address])
self.assertEqual(self.ledger.network.get_transaction_called, [b'jkl'])
address_details = yield self.ledger.db.get_address(address)
self.assertEqual(address_details['history'], b'abc:1:def:2:ghi:3:jkl:4:')
self.assertEqual(address_details['history'], buffer(b'abc:1:def:2:ghi:3:jkl:4:'))

View file

@ -1,8 +1,8 @@
from binascii import hexlify, unhexlify
from twisted.trial import unittest
from twisted.internet import defer
from torba.basetransaction import BaseTransaction, BaseInput, BaseOutput
from torba.coin.bitcoinsegwit import MainNetLedger
from torba.coin.bitcoinsegwit import MainNetLedger as ledger_class
from torba.constants import CENT, COIN
from torba.manager import WalletManager
from torba.wallet import Wallet
@ -14,30 +14,26 @@ FEE_PER_CHAR = 200000
def get_output(amount=CENT, pubkey_hash=NULL_HASH):
return BaseTransaction() \
.add_outputs([BaseTransaction.output_class.pay_pubkey_hash(amount, pubkey_hash)]) \
return ledger_class.transaction_class() \
.add_outputs([ledger_class.transaction_class.output_class.pay_pubkey_hash(amount, pubkey_hash)]) \
.outputs[0]
def get_input():
return BaseInput.spend(get_output())
return ledger_class.transaction_class.input_class.spend(get_output())
def get_transaction(txo=None):
return BaseTransaction() \
return ledger_class.transaction_class() \
.add_inputs([get_input()]) \
.add_outputs([txo or BaseOutput.pay_pubkey_hash(CENT, NULL_HASH)])
def get_wallet_and_ledger():
ledger = WalletManager().get_or_create_ledger(MainNetLedger.get_id())
return Wallet('Main', [ledger], [ledger.account_class.generate(ledger, u'torba')]), ledger
.add_outputs([txo or ledger_class.transaction_class.output_class.pay_pubkey_hash(CENT, NULL_HASH)])
class TestSizeAndFeeEstimation(unittest.TestCase):
def setUp(self):
self.wallet, self.ledger = get_wallet_and_ledger()
self.ledger = ledger_class(db=':memory:')
return self.ledger.db.start()
def io_fee(self, io):
return self.ledger.get_input_output_fee(io)
@ -70,20 +66,20 @@ class TestTransactionSerialization(unittest.TestCase):
'000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4c'
'ef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000'
)
tx = BaseTransaction(raw)
tx = ledger_class.transaction_class(raw)
self.assertEqual(tx.version, 1)
self.assertEqual(tx.locktime, 0)
self.assertEqual(len(tx.inputs), 1)
self.assertEqual(len(tx.outputs), 1)
ledgerbase = tx.inputs[0]
self.assertEqual(ledgerbase.output_txid, NULL_HASH)
self.assertEqual(ledgerbase.output_index, 0xFFFFFFFF)
self.assertEqual(ledgerbase.sequence, 4294967295)
self.assertTrue(ledgerbase.is_ledgerbase)
self.assertEqual(ledgerbase.script, None)
coinbase = tx.inputs[0]
self.assertEqual(coinbase.output_txhash, NULL_HASH)
self.assertEqual(coinbase.output_index, 0xFFFFFFFF)
self.assertEqual(coinbase.sequence, 4294967295)
self.assertTrue(coinbase.is_coinbase)
self.assertEqual(coinbase.script, None)
self.assertEqual(
ledgerbase.ledgerbase[8:],
coinbase.coinbase[8:],
b'The Times 03/Jan/2009 Chancellor on brink of second bailout for banks'
)
@ -97,7 +93,7 @@ class TestTransactionSerialization(unittest.TestCase):
tx._reset()
self.assertEqual(tx.raw, raw)
def test_ledgerbase_transaction(self):
def test_coinbase_transaction(self):
raw = unhexlify(
'01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4e03'
'1f5a070473319e592f4254432e434f4d2f4e59412ffabe6d6dcceb2a9d0444c51cabc4ee97a1a000036ca0'
@ -105,20 +101,20 @@ class TestTransactionSerialization(unittest.TestCase):
'0000000017a914e083685a1097ce1ea9e91987ab9e94eae33d8a13870000000000000000266a24aa21a9ed'
'e6c99265a6b9e1d36c962fda0516b35709c49dc3b8176fa7e5d5f1f6197884b400000000'
)
tx = BaseTransaction(raw)
tx = ledger_class.transaction_class(raw)
self.assertEqual(tx.version, 1)
self.assertEqual(tx.locktime, 0)
self.assertEqual(len(tx.inputs), 1)
self.assertEqual(len(tx.outputs), 2)
ledgerbase = tx.inputs[0]
self.assertEqual(ledgerbase.output_txid, NULL_HASH)
self.assertEqual(ledgerbase.output_index, 0xFFFFFFFF)
self.assertEqual(ledgerbase.sequence, 4294967295)
self.assertTrue(ledgerbase.is_ledgerbase)
self.assertEqual(ledgerbase.script, None)
coinbase = tx.inputs[0]
self.assertEqual(coinbase.output_txhash, NULL_HASH)
self.assertEqual(coinbase.output_index, 0xFFFFFFFF)
self.assertEqual(coinbase.sequence, 4294967295)
self.assertTrue(coinbase.is_coinbase)
self.assertEqual(coinbase.script, None)
self.assertEqual(
ledgerbase.ledgerbase[9:22],
coinbase.coinbase[9:22],
b'/BTC.COM/NYA/'
)
@ -148,26 +144,32 @@ class TestTransactionSerialization(unittest.TestCase):
class TestTransactionSigning(unittest.TestCase):
def setUp(self):
self.ledger = ledger_class(db=':memory:')
return self.ledger.db.start()
@defer.inlineCallbacks
def test_sign(self):
ledger = WalletManager().get_or_create_ledger(BTC.get_id())
ledger = BTC(ledger)
wallet = Wallet('Main', [ledger], [Account.from_seed(
ledger, u'carbon smart garage balance margin twelve chest sword toast envelope bottom stom'
u'ach absent', u'torba'
)])
account = wallet.default_account
account = self.ledger.account_class.from_seed(
self.ledger,
u"carbon smart garage balance margin twelve chest sword toast envelope bottom stomach ab"
u"sent", u"torba"
)
address1 = account.receiving_keys.generate_next_address()
address2 = account.receiving_keys.generate_next_address()
pubkey_hash1 = account.ledger.address_to_hash160(address1)
pubkey_hash2 = account.ledger.address_to_hash160(address2)
yield account.ensure_address_gap()
address1 = (yield account.receiving.get_addresses())[0]
address2 = (yield account.receiving.get_addresses())[0]
pubkey_hash1 = self.ledger.address_to_hash160(address1)
pubkey_hash2 = self.ledger.address_to_hash160(address2)
tx = Transaction() \
.add_inputs([Input.spend(get_output(2*COIN, pubkey_hash1))]) \
.add_outputs([Output.pay_pubkey_hash(int(1.9*COIN), pubkey_hash2)]) \
.sign(account)
tx = ledger_class.transaction_class() \
.add_inputs([ledger_class.transaction_class.input_class.spend(get_output(2*COIN, pubkey_hash1))]) \
.add_outputs([ledger_class.transaction_class.output_class.pay_pubkey_hash(int(1.9*COIN), pubkey_hash2)]) \
yield tx.sign([account])
self.assertEqual(
hexlify(tx.inputs[0].script.values['signature']),
b'304402203d463519290d06891e461ea5256c56097ccdad53379b1bb4e51ec5abc6e9fd02022034ed15b9d7c678716c4aa7c0fd26c688e8f9db8075838f2839ab55d551b62c0a01'
b'3044022064cd6b95c9e0084253c10dd56bcec2bfd816c29aad05fbea490511d79540462b02201aa9d6f73'
b'48bb0c76b28d1ad87cf4ffd51cf4de0b299af8bf0ecad70e3369ef201'
)

View file

@ -1,75 +1,50 @@
from twisted.trial import unittest
from torba.coin.bitcoinsegwit import BTC
from torba.coin.bitcoinsegwit import MainNetLedger as BTCLedger
from torba.coin.bitcoincash import MainNetLedger as BCHLedger
from torba.manager import WalletManager
from torba.wallet import Account, Wallet, WalletStorage
from .ftc import FTC
from torba.wallet import Wallet, WalletStorage
class TestWalletCreation(unittest.TestCase):
def setUp(self):
self.manager = WalletManager()
self.btc_ledger = self.manager.get_or_create_ledger(BTC.get_id())
self.ftc_ledger = self.manager.get_or_create_ledger(FTC.get_id())
config = {'wallet_path': '/tmp/wallet'}
self.btc_ledger = self.manager.get_or_create_ledger(BTCLedger.get_id(), config)
self.bch_ledger = self.manager.get_or_create_ledger(BCHLedger.get_id(), config)
def test_create_wallet_and_accounts(self):
wallet = Wallet()
self.assertEqual(wallet.name, 'Wallet')
self.assertEqual(wallet.coins, [])
self.assertEqual(wallet.accounts, [])
account1 = wallet.generate_account(self.btc_ledger)
account2 = wallet.generate_account(self.btc_ledger)
account3 = wallet.generate_account(self.ftc_ledger)
wallet.generate_account(self.btc_ledger)
wallet.generate_account(self.bch_ledger)
self.assertEqual(wallet.default_account, account1)
self.assertEqual(len(wallet.coins), 2)
self.assertEqual(len(wallet.accounts), 3)
self.assertIsInstance(wallet.coins[0], BTC)
self.assertIsInstance(wallet.coins[1], FTC)
self.assertEqual(len(account1.receiving_keys.addresses), 0)
self.assertEqual(len(account1.change_keys.addresses), 0)
self.assertEqual(len(account2.receiving_keys.addresses), 0)
self.assertEqual(len(account2.change_keys.addresses), 0)
self.assertEqual(len(account3.receiving_keys.addresses), 0)
self.assertEqual(len(account3.change_keys.addresses), 0)
account1.ensure_enough_addresses()
account2.ensure_enough_addresses()
account3.ensure_enough_addresses()
self.assertEqual(len(account1.receiving_keys.addresses), 20)
self.assertEqual(len(account1.change_keys.addresses), 6)
self.assertEqual(len(account2.receiving_keys.addresses), 20)
self.assertEqual(len(account2.change_keys.addresses), 6)
self.assertEqual(len(account3.receiving_keys.addresses), 20)
self.assertEqual(len(account3.change_keys.addresses), 6)
def test_load_and_save_wallet(self):
wallet_dict = {
'name': 'Main Wallet',
'accounts': [
{
'coin': 'btc_mainnet',
'ledger': 'btc_mainnet',
'seed':
"carbon smart garage balance margin twelve chest sword toast envelope bottom stomac"
"h absent",
'encrypted': False,
'private_key':
'xprv9s21ZrQH143K2dyhK7SevfRG72bYDRNv25yKPWWm6dqApNxm1Zb1m5gGcBWYfbsPjTr2v5joit8Af2Zp5P'
'6yz3jMbycrLrRMpeAJxR8qDg8',
b'xprv9s21ZrQH143K2dyhK7SevfRG72bYDRNv25yKPWWm6dqApNxm1Zb1m5gGcBWYfbsPjTr2v5joit8Af2Zp5P'
b'6yz3jMbycrLrRMpeAJxR8qDg8',
'public_key':
'xpub661MyMwAqRbcF84AR8yfHoMzf4S2ct6mPJtvBtvNeyN9hBHuZ6uGJszkTSn5fQUCdz3XU17eBzFeAUwV6f'
'iW44g14WF52fYC5J483wqQ5ZP',
b'xpub661MyMwAqRbcF84AR8yfHoMzf4S2ct6mPJtvBtvNeyN9hBHuZ6uGJszkTSn5fQUCdz3XU17eBzFeAUwV6f'
b'iW44g14WF52fYC5J483wqQ5ZP',
'receiving_gap': 10,
'receiving_keys': [
'0222345947a59dca4a3363ffa81ac87dd907d2b2feff57383eaeddbab266ca5f2d',
'03fdc9826d5d00a484188cba8eb7dba5877c0323acb77905b7bcbbab35d94be9f6'
],
'receiving_maximum_use_per_address': 2,
'change_gap': 10,
'change_keys': [
'038836be4147836ed6b4df6a89e0d9f1b1c11cec529b7ff5407de57f2e5b032c83'
]
'change_maximum_use_per_address': 2,
}
]
}
@ -77,21 +52,8 @@ class TestWalletCreation(unittest.TestCase):
storage = WalletStorage(default=wallet_dict)
wallet = Wallet.from_storage(storage, self.manager)
self.assertEqual(wallet.name, 'Main Wallet')
self.assertEqual(len(wallet.coins), 1)
self.assertIsInstance(wallet.coins[0], BTC)
self.assertEqual(len(wallet.accounts), 1)
account = wallet.default_account
self.assertIsInstance(account, Account)
self.assertEqual(len(account.receiving_keys.addresses), 2)
self.assertEqual(
account.receiving_keys.addresses[0],
b'1PmX9T3sCiDysNtWszJa44SkKcpGc2NaXP'
)
self.assertEqual(len(account.change_keys.addresses), 1)
self.assertEqual(
account.change_keys.addresses[0],
b'1PUbu1D1f3c244JPRSJKBCxRqui5NT6geR'
)
wallet_dict['coins'] = {'btc_mainnet': {'fee_per_byte': 50}}
self.assertIsInstance(account, BTCLedger.account_class)
self.maxDiff = None
self.assertDictEqual(wallet_dict, wallet.to_dict())

View file

@ -59,9 +59,9 @@ class KeyChain:
def get_or_create_usable_address(self):
addresses = yield self.get_usable_addresses(1)
if addresses:
return addresses[0]
defer.returnValue(addresses[0])
addresses = yield self.ensure_address_gap()
return addresses[0]
defer.returnValue(addresses[0])
class BaseAccount:
@ -83,7 +83,7 @@ class BaseAccount:
KeyChain(self, public_key, 0, receiving_gap, receiving_maximum_use_per_address),
KeyChain(self, public_key, 1, change_gap, change_maximum_use_per_address)
)
ledger.account_created(self)
ledger.add_account(self)
@classmethod
def generate(cls, ledger, password): # type: (torba.baseledger.BaseLedger, str) -> BaseAccount

View file

@ -1,5 +1,6 @@
import logging
from typing import List, Union
from operator import itemgetter
import sqlite3
from twisted.internet import defer
@ -97,11 +98,10 @@ class BaseDatabase(SQLiteMixin):
CREATE_TX_TABLE = """
create table if not exists tx (
txid blob primary key,
txhash blob primary key,
raw blob not null,
height integer not null,
is_confirmed boolean not null,
is_verified boolean not null
is_verified boolean not null default false
);
"""
@ -113,14 +113,14 @@ class BaseDatabase(SQLiteMixin):
position integer not null,
pubkey blob not null,
history text,
used_times integer default 0
used_times integer not null default 0
);
"""
CREATE_TXO_TABLE = """
create table if not exists txo (
txoid integer primary key,
txid blob references tx,
txhash blob references tx,
address blob references pubkey_address,
position integer not null,
amount integer not null,
@ -130,7 +130,7 @@ class BaseDatabase(SQLiteMixin):
CREATE_TXI_TABLE = """
create table if not exists txi (
txid blob references tx,
txhash blob references tx,
address blob references pubkey_address,
txoid integer references txo
);
@ -143,20 +143,33 @@ class BaseDatabase(SQLiteMixin):
CREATE_TXI_TABLE
)
def add_transaction(self, address, hash, tx, height, is_confirmed, is_verified):
def add_transaction(self, address, hash, tx, height, is_verified):
def _steps(t):
if not t.execute("SELECT 1 FROM tx WHERE txid=?", (sqlite3.Binary(tx.id),)).fetchone():
current_height = t.execute("SELECT height FROM tx WHERE txhash=?", (sqlite3.Binary(tx.hash),)).fetchone()
if current_height is None:
t.execute(*self._insert_sql('tx', {
'txid': sqlite3.Binary(tx.id),
'txhash': sqlite3.Binary(tx.hash),
'raw': sqlite3.Binary(tx.raw),
'height': height,
'is_confirmed': is_confirmed,
'is_verified': is_verified
}))
elif current_height[0] != height:
t.execute("UPDATE tx SET height = :height WHERE txhash = :txhash", {
'txhash': sqlite3.Binary(tx.hash),
'height': height,
})
existing_txos = list(map(itemgetter(0), t.execute(
"SELECT position FROM txo WHERE txhash = ?",
(sqlite3.Binary(tx.hash),)
).fetchall()))
for txo in tx.outputs:
if txo.index in existing_txos:
continue
if txo.script.is_pay_pubkey_hash and txo.script.values['pubkey_hash'] == hash:
t.execute(*self._insert_sql("txo", {
'txid': sqlite3.Binary(tx.id),
'txhash': sqlite3.Binary(tx.hash),
'address': sqlite3.Binary(address),
'position': txo.index,
'amount': txo.amount,
@ -166,24 +179,33 @@ class BaseDatabase(SQLiteMixin):
# TODO: implement script hash payments
print('Database.add_transaction pay script hash is not implemented!')
existing_txis = [txi[0] for txi in t.execute(
"SELECT txoid FROM txi WHERE txhash = ? AND address = ?",
(sqlite3.Binary(tx.hash), sqlite3.Binary(address))).fetchall()]
for txi in tx.inputs:
txoid = t.execute(
"SELECT txoid, address FROM txo WHERE txid = ? AND position = ?",
(sqlite3.Binary(txi.output_txid), txi.output_index)
"SELECT txoid FROM txo WHERE txhash = ? AND position = ?",
(sqlite3.Binary(txi.output_txhash), txi.output_index)
).fetchone()
if txoid:
if txoid is not None and txoid[0] not in existing_txis:
t.execute(*self._insert_sql("txi", {
'txid': sqlite3.Binary(tx.id),
'txhash': sqlite3.Binary(tx.hash),
'address': sqlite3.Binary(address),
'txoid': txoid,
'txoid': txoid[0],
}))
return self.db.runInteraction(_steps)
@defer.inlineCallbacks
def get_balance_for_account(self, account):
result = yield self.db.runQuery(
"SELECT SUM(amount) FROM txo NATURAL JOIN pubkey_address WHERE account=:account AND "
"txoid NOT IN (SELECT txoid FROM txi)",
"""
SELECT SUM(amount) FROM txo
JOIN pubkey_address ON pubkey_address.address=txo.address
WHERE account=:account AND
txoid NOT IN (SELECT txoid FROM txi)
""",
{'account': sqlite3.Binary(account.public_key.address)}
)
if result:
@ -195,8 +217,8 @@ class BaseDatabase(SQLiteMixin):
def get_utxos(self, account, output_class):
utxos = yield self.db.runQuery(
"""
SELECT amount, script, txid, position
FROM txo NATURAL JOIN pubkey_address
SELECT amount, script, txhash, txo.position
FROM txo JOIN pubkey_address ON pubkey_address.address=txo.address
WHERE account=:account AND txoid NOT IN (SELECT txoid FROM txi)
""",
{'account': sqlite3.Binary(account.public_key.address)}

View file

@ -76,9 +76,6 @@ class BaseLedger(six.with_metaclass(LedgerRegistry)):
raw_address = self.pubkey_address_prefix + h160
return Base58.encode(bytearray(raw_address + double_sha256(raw_address)[0:4]))
def account_created(self, account): # type: (baseaccount.BaseAccount) -> None
self.accounts.add(account)
@staticmethod
def address_to_hash160(address):
bytes = Base58.decode(address)
@ -108,10 +105,16 @@ class BaseLedger(six.with_metaclass(LedgerRegistry)):
def add_transaction(self, address, transaction, height):
# type: (bytes, basetransaction.BaseTransaction, int) -> None
yield self.db.add_transaction(
address, self.address_to_hash160(address), transaction, height, False, False
address, self.address_to_hash160(address), transaction, height, False
)
self._on_transaction_controller.add(transaction)
@defer.inlineCallbacks
def add_account(self, account): # type: (baseaccount.BaseAccount) -> None
self.accounts.add(account)
if self.network.is_connected:
yield self.update_account(account)
@defer.inlineCallbacks
def get_private_key_for_address(self, address):
match = yield self.db.get_address(address)
@ -123,19 +126,6 @@ class BaseLedger(six.with_metaclass(LedgerRegistry)):
def get_unspent_outputs(self, account):
return self.db.get_utxos(account, self.transaction_class.output_class)
# def get_unspent_outputs(self, account):
# inputs, outputs, utxos = set(), set(), set()
# for address in self.addresses.values():
# for tx in address:
# for txi in tx.inputs:
# inputs.add((hexlify(txi.output_txid), txi.output_index))
# for txo in tx.outputs:
# if txo.script.is_pay_pubkey_hash and txo.script.values['pubkey_hash'] == address.pubkey_hash:
# outputs.add((txo, txo.transaction.id, txo.index))
# for output in outputs:
# if output[1:] not in inputs:
# yield output[0]
@defer.inlineCallbacks
def get_local_status(self, address):
address_details = yield self.db.get_address(address)
@ -146,6 +136,8 @@ class BaseLedger(six.with_metaclass(LedgerRegistry)):
def get_local_history(self, address):
address_details = yield self.db.get_address(address)
history = address_details['history'] or b''
if six.PY2:
history = str(history)
parts = history.split(b':')[:-1]
defer.returnValue(list(zip(parts[0::2], map(int, parts[1::2]))))

View file

@ -95,7 +95,7 @@ class StratumClientProtocol(LineOnlyReceiver):
try:
d = self.lookup_table.pop(message['id'])
if message.get('error'):
d.errback(RuntimeError(*message['error']))
d.errback(RuntimeError(message['error']))
else:
d.callback(message.get('result'))
except KeyError:

View file

@ -5,12 +5,13 @@ from binascii import hexlify
from twisted.internet import defer
import torba.baseaccount
import torba.baseledger
from torba.basescript import BaseInputScript, BaseOutputScript
from torba.coinselection import CoinSelector
from torba.constants import COIN
from torba.bcd_data_stream import BCDataStream
from torba.hash import sha256
from torba.baseaccount import BaseAccount
from torba.util import ReadOnlyList
@ -22,16 +23,16 @@ NULL_HASH = b'\x00'*32
class InputOutput(object):
def __init__(self, txid, index=None):
self._txid = txid # type: bytes
def __init__(self, txhash, index=None):
self._txhash = txhash # type: bytes
self.transaction = None # type: BaseTransaction
self.index = index # type: int
@property
def txid(self):
if self._txid is None:
self._txid = self.transaction.id
return self._txid
def txhash(self):
if self._txhash is None:
self._txhash = self.transaction.hash
return self._txhash
@property
def size(self):
@ -51,26 +52,20 @@ class BaseInput(InputOutput):
NULL_SIGNATURE = b'\x00'*72
NULL_PUBLIC_KEY = b'\x00'*33
def __init__(self, output_or_txid_index, script, sequence=0xFFFFFFFF, txid=None):
super(BaseInput, self).__init__(txid)
if isinstance(output_or_txid_index, BaseOutput):
self.output = output_or_txid_index # type: BaseOutput
self.output_txid = self.output.txid
def __init__(self, output_or_txhash_index, script, sequence=0xFFFFFFFF, txhash=None):
super(BaseInput, self).__init__(txhash)
if isinstance(output_or_txhash_index, BaseOutput):
self.output = output_or_txhash_index # type: BaseOutput
self.output_txhash = self.output.txhash
self.output_index = self.output.index
else:
self.output = None # type: BaseOutput
self.output_txid, self.output_index = output_or_txid_index
self.output_txhash, self.output_index = output_or_txhash_index
self.sequence = sequence
self.is_coinbase = self.output_txid == NULL_HASH
self.is_coinbase = self.output_txhash == NULL_HASH
self.coinbase = script if self.is_coinbase else None
self.script = script if not self.is_coinbase else None # type: BaseInputScript
def link_output(self, output):
assert self.output is None
assert self.output_txid == output.transaction.id
assert self.output_index == output.index
self.output = output
@classmethod
def spend(cls, output):
""" Create an input to spend the output."""
@ -87,18 +82,18 @@ class BaseInput(InputOutput):
@classmethod
def deserialize_from(cls, stream):
txid = stream.read(32)
txhash = stream.read(32)
index = stream.read_uint32()
script = stream.read_string()
sequence = stream.read_uint32()
return cls(
(txid, index),
cls.script_class(script) if not txid == NULL_HASH else script,
(txhash, index),
cls.script_class(script) if not txhash == NULL_HASH else script,
sequence
)
def serialize_to(self, stream, alternate_script=None):
stream.write(self.output_txid)
stream.write(self.output_txhash)
stream.write_uint32(self.output_index)
if alternate_script is not None:
stream.write_string(alternate_script)
@ -114,7 +109,7 @@ class BaseOutputEffectiveAmountEstimator(object):
__slots__ = 'coin', 'txi', 'txo', 'fee', 'effective_amount'
def __init__(self, ledger, txo): # type: (BaseLedger, BaseOutput) -> None
def __init__(self, ledger, txo): # type: (torba.baseledger.BaseLedger, BaseOutput) -> None
self.txo = txo
self.txi = ledger.transaction_class.input_class.spend(txo)
self.fee = ledger.get_input_output_fee(self.txi)
@ -129,8 +124,8 @@ class BaseOutput(InputOutput):
script_class = BaseOutputScript
estimator_class = BaseOutputEffectiveAmountEstimator
def __init__(self, amount, script, txid=None, index=None):
super(BaseOutput, self).__init__(txid, index)
def __init__(self, amount, script, txhash=None, index=None):
super(BaseOutput, self).__init__(txhash, index)
self.amount = amount # type: int
self.script = script # type: BaseOutputScript
@ -259,7 +254,7 @@ class BaseTransaction:
for txout in self._outputs:
txout.serialize_to(stream)
stream.write_uint32(self.locktime)
stream.write_uint32(1) # signature hash type: SIGHASH_ALL
stream.write_uint32(self.signature_hash_type(1)) # signature hash type: SIGHASH_ALL
return stream.get_bytes()
def _deserialize(self):
@ -279,7 +274,7 @@ class BaseTransaction:
@classmethod
@defer.inlineCallbacks
def get_effective_amount_estimators(cls, funding_accounts):
# type: (Iterable[BaseAccount]) -> Generator[BaseOutputEffectiveAmountEstimator]
# type: (Iterable[torba.baseaccount.BaseAccount]) -> Generator[BaseOutputEffectiveAmountEstimator]
estimators = []
for account in funding_accounts:
utxos = yield account.ledger.get_unspent_outputs(account)
@ -289,7 +284,7 @@ class BaseTransaction:
@classmethod
def ensure_all_have_same_ledger(cls, funding_accounts, change_account=None):
# type: (Iterable[BaseAccount], BaseAccount) -> baseledger.BaseLedger
# type: (Iterable[torba.baseaccount.BaseAccount], torba.baseaccount.BaseAccount) -> torba.baseledger.BaseLedger
ledger = None
for account in funding_accounts:
if ledger is None:
@ -305,12 +300,12 @@ class BaseTransaction:
@classmethod
@defer.inlineCallbacks
def pay(cls, outputs, funding_accounts, change_account):
# type: (List[BaseOutput], List[BaseAccount], BaseAccount) -> BaseTransaction
# type: (List[BaseOutput], List[torba.baseaccount.BaseAccount], torba.baseaccount.BaseAccount) -> BaseTransaction
""" Efficiently spend utxos from funding_accounts to cover the new outputs. """
tx = cls().add_outputs(outputs)
ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account)
amount = ledger.get_transaction_base_fee(tx)
amount = tx.output_sum + ledger.get_transaction_base_fee(tx)
txos = yield cls.get_effective_amount_estimators(funding_accounts)
selector = CoinSelector(
txos, amount,
@ -325,34 +320,38 @@ class BaseTransaction:
spent_sum = sum(s.effective_amount for s in spendables)
if spent_sum > amount:
change_address = change_account.change.get_or_create_usable_address()
change_address = yield change_account.change.get_or_create_usable_address()
change_hash160 = change_account.ledger.address_to_hash160(change_address)
change_amount = spent_sum - amount
tx.add_outputs([cls.output_class.pay_pubkey_hash(change_amount, change_hash160)])
tx.add_inputs([s.txi for s in spendables])
tx.sign(funding_accounts)
yield tx.sign(funding_accounts)
defer.returnValue(tx)
@classmethod
def liquidate(cls, assets, funding_accounts, change_account):
""" Spend assets (utxos) supplementing with funding_accounts if fee is higher than asset value. """
def sign(self, funding_accounts): # type: (Iterable[BaseAccount]) -> BaseTransaction
def signature_hash_type(self, hash_type):
return hash_type
@defer.inlineCallbacks
def sign(self, funding_accounts): # type: (Iterable[torba.baseaccount.BaseAccount]) -> BaseTransaction
ledger = self.ensure_all_have_same_ledger(funding_accounts)
for i, txi in enumerate(self._inputs):
txo_script = txi.output.script
if txo_script.is_pay_pubkey_hash:
address = ledger.hash160_to_address(txo_script.values['pubkey_hash'])
private_key = ledger.get_private_key_for_address(address)
private_key = yield ledger.get_private_key_for_address(address)
tx = self._serialize_for_signature(i)
txi.script.values['signature'] = private_key.sign(tx)+six.int2byte(1)
txi.script.values['signature'] = \
private_key.sign(tx) + six.int2byte(self.signature_hash_type(1))
txi.script.values['pubkey'] = private_key.public_key.pubkey_bytes
txi.script.generate()
else:
raise NotImplementedError("Don't know how to spend this output.")
self._reset()
return self
def sort(self):
# See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki
@ -361,8 +360,8 @@ class BaseTransaction:
@property
def input_sum(self):
return sum(i.amount for i in self._inputs)
return sum(i.amount for i in self.inputs)
@property
def output_sum(self):
return sum(o.amount for o in self._outputs)
return sum(o.amount for o in self.outputs)

View file

@ -4,63 +4,26 @@ __node_bin__ = 'bitcoin-abc-0.17.2/bin'
__node_url__ = (
'https://download.bitcoinabc.org/0.17.2/linux/bitcoin-abc-0.17.2-x86_64-linux-gnu.tar.gz'
)
__electrumx__ = 'electrumx.lib.coins.BitcoinCashRegtest'
from six import int2byte
from binascii import unhexlify
from torba.baseledger import BaseLedger, BaseHeaders
from torba.basenetwork import BaseNetwork
from torba.basescript import BaseInputScript, BaseOutputScript
from torba.basetransaction import BaseTransaction, BaseInput, BaseOutput
from torba.basedatabase import BaseSQLiteWalletStorage
from torba.manager import BaseWalletManager
class WalletManager(BaseWalletManager):
pass
class Input(BaseInput):
script_class = BaseInputScript
class Output(BaseOutput):
script_class = BaseOutputScript
from torba.baseledger import BaseLedger
from torba.baseheader import BaseHeaders
from torba.basetransaction import BaseTransaction
class Transaction(BaseTransaction):
input_class = Input
output_class = Output
def signature_hash_type(self, hash_type):
return hash_type | 0x40
class BitcoinCashLedger(BaseLedger):
network_class = BaseNetwork
headers_class = BaseHeaders
database_class = BaseSQLiteWalletStorage
class MainNetLedger(BitcoinCashLedger):
pass
class UnverifiedHeaders(BaseHeaders):
verify_bits_to_target = False
class RegTestLedger(BitcoinCashLedger):
headers_class = UnverifiedHeaders
max_target = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
genesis_hash = '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206'
genesis_bits = 0x207fffff
target_timespan = 1
verify_bits_to_target = False
class BitcoinCash(BaseCoin):
class MainNetLedger(BaseLedger):
name = 'BitcoinCash'
symbol = 'BCH'
network = 'mainnet'
network_name = 'mainnet'
ledger_class = MainNetLedger
transaction_class = Transaction
pubkey_address_prefix = int2byte(0x00)
@ -70,15 +33,21 @@ class BitcoinCash(BaseCoin):
default_fee_per_byte = 50
def __init__(self, ledger, fee_per_byte=default_fee_per_byte):
super(BitcoinCash, self).__init__(ledger, fee_per_byte)
class UnverifiedHeaders(BaseHeaders):
verify_bits_to_target = False
class BitcoinCashRegtest(BitcoinCash):
network = 'regtest'
ledger_class = RegTestLedger
class RegTestLedger(MainNetLedger):
headers_class = UnverifiedHeaders
network_name = 'regtest'
pubkey_address_prefix = int2byte(111)
script_address_prefix = int2byte(196)
extended_public_key_prefix = unhexlify('043587cf')
extended_private_key_prefix = unhexlify('04358394')
max_target = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
genesis_hash = '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206'
genesis_bits = 0x207fffff
target_timespan = 1

View file

@ -4,6 +4,7 @@ __node_bin__ = 'bitcoin-0.16.0/bin'
__node_url__ = (
'https://bitcoin.org/bin/bitcoin-core-0.16.0/bitcoin-0.16.0-x86_64-linux-gnu.tar.gz'
)
__electrumx__ = 'electrumx.lib.coins.BitcoinSegwitRegtest'
from six import int2byte
from binascii import unhexlify

View file

@ -120,6 +120,9 @@ class Base58(object):
@classmethod
def decode(cls, txt):
""" Decodes txt into a big-endian bytearray. """
if six.PY2 and isinstance(txt, buffer):
txt = str(txt)
if isinstance(txt, six.binary_type):
txt = txt.decode()

View file

@ -1,17 +1,10 @@
import stat
import json
import os
from typing import List, Dict
from typing import List
from torba.baseaccount import BaseAccount
from torba.baseledger import LedgerRegistry, BaseLedger
def inflate_ledger(manager, ledger_id, ledger_dict):
# type: ('WalletManager', str, Dict) -> BaseLedger
ledger_class = LedgerRegistry.get_ledger_class(ledger_id)
ledger = manager.get_or_create_ledger(ledger_id)
return ledger_class(ledger, **ledger_dict)
import torba.baseaccount
import torba.baseledger
class Wallet:
@ -21,13 +14,14 @@ class Wallet:
by physical files on the filesystem.
"""
def __init__(self, name='Wallet', ledgers=None, accounts=None, storage=None):
def __init__(self, name='Wallet', accounts=None, storage=None):
# type: (str, List[torba.baseaccount.BaseAccount], WalletStorage) -> None
self.name = name
self.ledgers = ledgers or [] # type: List[BaseLedger]
self.accounts = accounts or [] # type: List[BaseAccount]
self.accounts = accounts or []
self.storage = storage or WalletStorage()
def generate_account(self, ledger): # type: (BaseLedger) -> Account
def generate_account(self, ledger):
# type: (torba.baseledger.BaseLedger) -> torba.baseaccount.BaseAccount
account = ledger.account_class.generate(ledger, u'torba')
self.accounts.append(account)
return account
@ -36,22 +30,14 @@ class Wallet:
def from_storage(cls, storage, manager): # type: (WalletStorage, 'WalletManager') -> Wallet
json_dict = storage.read()
ledgers = {}
for ledger_id, ledger_dict in json_dict.get('ledgers', {}).items():
ledgers[ledger_id] = inflate_ledger(manager, ledger_id, ledger_dict)
accounts = []
for account_dict in json_dict.get('accounts', []):
ledger_id = account_dict['ledger']
ledger = ledgers.get(ledger_id)
if ledger is None:
ledger = ledgers[ledger_id] = inflate_ledger(manager, ledger_id, {})
ledger = manager.get_or_create_ledger(account_dict['ledger'])
account = ledger.account_class.from_dict(ledger, account_dict)
accounts.append(account)
return cls(
name=json_dict.get('name', 'Wallet'),
ledgers=list(ledgers.values()),
accounts=accounts,
storage=storage
)
@ -59,7 +45,6 @@ class Wallet:
def to_dict(self):
return {
'name': self.name,
'ledgers': {c.get_id(): {} for c in self.ledgers},
'accounts': [a.to_dict() for a in self.accounts]
}
@ -71,12 +56,6 @@ class Wallet:
for account in self.accounts:
return account
def get_account_private_key_for_address(self, address):
for account in self.accounts:
private_key = account.get_private_key_for_address(address)
if private_key is not None:
return account, private_key
class WalletStorage:

12
tox.ini
View file

@ -1,12 +1,14 @@
[tox]
envlist = py{27,36}
envlist = py27,py36-{torba.coin.bitcoincash,torba.coin.bitcoinsegwit}
[testenv]
deps =
coverage
mock
py36: ../orchstr8
py36: ../electrumx
extras = test
changedir = {toxinidir}/tests
setenv = py36: LEDGER={envname}
commands =
coverage run -p --source={envsitepackagesdir}/torba -m unittest discover -v
coverage run -p --source={envsitepackagesdir}/torba -m twisted.trial unit
py36: coverage run -p --source={envsitepackagesdir}/torba -m twisted.trial --reactor=asyncio integration