diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index d818f0979..c9e372106 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -1477,12 +1477,21 @@ class Daemon(metaclass=JSONRPCServerType): outputs = [] for address in addresses: - self.valid_address_or_error(address) - outputs.append( - Output.pay_pubkey_hash( - amount, self.ledger.address_to_hash160(address) + self.valid_address_or_error(address, allow_script_address=True) + if self.ledger.is_pubkey_address(address): + outputs.append( + Output.pay_pubkey_hash( + amount, self.ledger.address_to_hash160(address) + ) ) - ) + elif self.ledger.is_script_address(address): + outputs.append( + Output.pay_script_hash( + amount, self.ledger.address_to_hash160(address) + ) + ) + else: + raise ValueError(f"Unsupported address: '{address}'") tx = await Transaction.create( [], outputs, accounts, account @@ -5525,9 +5534,11 @@ class Daemon(metaclass=JSONRPCServerType): async def broadcast_or_release(self, tx, blocking=False): await self.wallet_manager.broadcast_or_release(tx, blocking) - def valid_address_or_error(self, address): + def valid_address_or_error(self, address, allow_script_address=False): try: - assert self.ledger.is_valid_address(address) + assert self.ledger.is_pubkey_address(address) or ( + allow_script_address and self.ledger.is_script_address(address) + ) except: raise Exception(f"'{address}' is not a valid address") diff --git a/lbry/testcase.py b/lbry/testcase.py index 88448ad60..5b5d5b6e4 100644 --- a/lbry/testcase.py +++ b/lbry/testcase.py @@ -577,6 +577,11 @@ class CommandTestCase(IntegrationTestCase): self.daemon.jsonrpc_support_abandon(*args, **kwargs), confirm ) + async def account_send(self, *args, confirm=True, **kwargs): + return await self.confirm_and_render( + self.daemon.jsonrpc_account_send(*args, **kwargs), confirm + ) + async def wallet_send(self, *args, confirm=True, **kwargs): return await self.confirm_and_render( self.daemon.jsonrpc_wallet_send(*args, **kwargs), confirm diff --git a/lbry/wallet/database.py b/lbry/wallet/database.py index fc40b72bf..c12ea0b8e 100644 --- a/lbry/wallet/database.py +++ b/lbry/wallet/database.py @@ -769,9 +769,10 @@ class Database(SQLiteMixin): conn.execute(*self._insert_sql( "txo", self.txo_to_row(tx, txo), ignore_duplicate=True )).fetchall() - elif txo.script.is_pay_script_hash: - # TODO: implement script hash payments - log.warning('Database.save_transaction_io: pay script hash is not implemented!') + elif txo.script.is_pay_script_hash and is_my_input: + conn.execute(*self._insert_sql( + "txo", self.txo_to_row(tx, txo), ignore_duplicate=True + )).fetchall() def save_transaction_io(self, tx: Transaction, address, txhash, history): return self.save_transaction_io_batch([tx], address, txhash, history) diff --git a/lbry/wallet/ledger.py b/lbry/wallet/ledger.py index fc8decf57..18b0d46f9 100644 --- a/lbry/wallet/ledger.py +++ b/lbry/wallet/ledger.py @@ -178,17 +178,24 @@ class Ledger(metaclass=LedgerRegistry): raw_address = cls.pubkey_address_prefix + h160 return Base58.encode(bytearray(raw_address + double_sha256(raw_address)[0:4])) + @classmethod + def hash160_to_script_address(cls, h160): + raw_address = cls.script_address_prefix + h160 + return Base58.encode(bytearray(raw_address + double_sha256(raw_address)[0:4])) + @staticmethod def address_to_hash160(address): return Base58.decode(address)[1:21] @classmethod - def is_valid_address(cls, address): + def is_pubkey_address(cls, address): decoded = Base58.decode_check(address) - return ( - decoded[0] == cls.pubkey_address_prefix[0] or - decoded[0] == cls.script_address_prefix[0] - ) + return decoded[0] == cls.pubkey_address_prefix[0] + + @classmethod + def is_script_address(cls, address): + decoded = Base58.decode_check(address) + return decoded[0] == cls.script_address_prefix[0] @classmethod def public_key_to_address(cls, public_key): @@ -1192,6 +1199,7 @@ class TestNetLedger(Ledger): extended_private_key_prefix = unhexlify('04358394') checkpoints = {} + class RegTestLedger(Ledger): network_name = 'regtest' headers_class = UnvalidatedHeaders diff --git a/lbry/wallet/orchstr8/node.py b/lbry/wallet/orchstr8/node.py index be4a4fc7a..916edde6e 100644 --- a/lbry/wallet/orchstr8/node.py +++ b/lbry/wallet/orchstr8/node.py @@ -410,7 +410,7 @@ class BlockchainNode: return self._cli_cmnd('getnewaddress', "", address_type) async def get_balance(self): - return float(await self._cli_cmnd('getbalance')) + return await self._cli_cmnd('getbalance') def send_to_address(self, address, amount): return self._cli_cmnd('sendtoaddress', address, str(amount)) diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py index 9856d274a..6b0fef370 100644 --- a/lbry/wallet/transaction.py +++ b/lbry/wallet/transaction.py @@ -276,12 +276,19 @@ class Output(InputOutput): def pubkey_hash(self): return self.script.values['pubkey_hash'] + @property + def script_hash(self): + return self.script.values['script_hash'] + @property def has_address(self): return 'pubkey_hash' in self.script.values def get_address(self, ledger): - return ledger.hash160_to_address(self.pubkey_hash) + if self.script.is_pay_pubkey_hash: + return ledger.hash160_to_address(self.pubkey_hash) + elif self.script.is_pay_script_hash: + return ledger.hash160_to_script_address(self.script_hash) def get_estimator(self, ledger): return OutputEffectiveAmountEstimator(ledger, self) @@ -290,6 +297,10 @@ class Output(InputOutput): def pay_pubkey_hash(cls, amount, pubkey_hash): return cls(amount, OutputScript.pay_pubkey_hash(pubkey_hash)) + @classmethod + def pay_script_hash(cls, amount, pubkey_hash): + return cls(amount, OutputScript.pay_script_hash(pubkey_hash)) + @classmethod def deserialize_from(cls, stream): return cls( diff --git a/lbry/wallet/usage_payment.py b/lbry/wallet/usage_payment.py index 67c740260..c930a996c 100644 --- a/lbry/wallet/usage_payment.py +++ b/lbry/wallet/usage_payment.py @@ -35,7 +35,7 @@ class WalletServerPayer: if not address or not amount: continue - if not self.ledger.is_valid_address(address): + if not self.ledger.is_pubkey_address(address): self._on_payment_controller.add_error(ServerPaymentInvalidAddressError(address)) continue diff --git a/tests/integration/blockchain/test_purchase_command.py b/tests/integration/blockchain/test_purchase_command.py index 9c110b91c..329873531 100644 --- a/tests/integration/blockchain/test_purchase_command.py +++ b/tests/integration/blockchain/test_purchase_command.py @@ -44,7 +44,7 @@ class PurchaseCommandTests(CommandTestCase): await self.account.release_all_outputs() buyer_balance = await self.account.get_balance() - merchant_balance = lbc_to_dewies(str(await self.blockchain.get_balance())) + merchant_balance = lbc_to_dewies(await self.blockchain.get_balance()) pre_purchase_count = (await self.daemon.jsonrpc_purchase_list())['total_items'] purchase = await operation() stream_txo, purchase_txo = stream.outputs[0], purchase.outputs[0] @@ -60,7 +60,9 @@ class PurchaseCommandTests(CommandTestCase): self.assertEqual( await self.account.get_balance(), buyer_balance - (purchase.input_sum-purchase.outputs[2].amount)) self.assertEqual( - str(await self.blockchain.get_balance()), dewies_to_lbc(merchant_balance + purchase_txo.amount)) + await self.blockchain.get_balance(), + dewies_to_lbc(merchant_balance + purchase_txo.amount) + ) purchases = await self.daemon.jsonrpc_purchase_list() self.assertEqual(purchases['total_items'], pre_purchase_count+1) diff --git a/tests/integration/blockchain/test_wallet_commands.py b/tests/integration/blockchain/test_wallet_commands.py index 0ec97fa19..f2b59ebd6 100644 --- a/tests/integration/blockchain/test_wallet_commands.py +++ b/tests/integration/blockchain/test_wallet_commands.py @@ -57,6 +57,17 @@ class WalletCommands(CommandTestCase): self.assertEqual(len(status['wallet']['servers']), 1) self.assertEqual(status['wallet']['servers'][0]['port'], 54320) + async def test_sending_to_scripthash_address(self): + self.assertEqual(await self.blockchain.get_balance(), '95.99973580') + await self.assertBalance(self.account, '10.0') + p2sh_address1 = await self.blockchain.get_new_address(self.blockchain.P2SH_SEGWIT_ADDRESS) + await self.account_send('2.0', p2sh_address1) + self.assertEqual(await self.blockchain.get_balance(), '98.99973580') # +1 lbc for confirm block + await self.assertBalance(self.account, '7.999877') + await self.wallet_send('3.0', p2sh_address1) + self.assertEqual(await self.blockchain.get_balance(), '102.99973580') # +1 lbc for confirm block + await self.assertBalance(self.account, '4.999754') + async def test_balance_caching(self): account2 = await self.daemon.jsonrpc_account_create("Tip-er") address2 = await self.daemon.jsonrpc_address_unused(account2.id) diff --git a/tests/integration/datanetwork/test_file_commands.py b/tests/integration/datanetwork/test_file_commands.py index 0bf9160bc..b4e9c535a 100644 --- a/tests/integration/datanetwork/test_file_commands.py +++ b/tests/integration/datanetwork/test_file_commands.py @@ -410,11 +410,12 @@ class FileCommands(CommandTestCase): await asyncio.wait_for(self.wait_files_to_complete(), timeout=1) # check that the fee was received - starting_balance = await self.blockchain.get_balance() + starting_balance = float(await self.blockchain.get_balance()) await self.generate(1) block_reward_and_claim_fee = 2.0 self.assertEqual( - await self.blockchain.get_balance(), starting_balance + block_reward_and_claim_fee + float(await self.blockchain.get_balance()), + starting_balance + block_reward_and_claim_fee ) # restart the daemon and make sure the fee is still there @@ -463,11 +464,11 @@ class FileCommands(CommandTestCase): self.assertItemCount(await self.daemon.jsonrpc_file_list(), 1) # Assert the transaction is recorded to the blockchain - starting_balance = await self.blockchain.get_balance() + starting_balance = float(await self.blockchain.get_balance()) await self.generate(1) block_reward_and_claim_fee = 2.0 self.assertEqual( - await self.blockchain.get_balance(), starting_balance + block_reward_and_claim_fee + float(await self.blockchain.get_balance()), starting_balance + block_reward_and_claim_fee ) async def test_null_fee(self): diff --git a/tests/unit/wallet/test_ledger.py b/tests/unit/wallet/test_ledger.py index 67f78f0aa..5e81649b9 100644 --- a/tests/unit/wallet/test_ledger.py +++ b/tests/unit/wallet/test_ledger.py @@ -89,7 +89,7 @@ class LedgerTestCase(AsyncioTestCase): class TestUtils(TestCase): def test_valid_address(self): - self.assertTrue(Ledger.is_valid_address("rCz6yb1p33oYHToGZDzTjX7nFKaU3kNgBd")) + self.assertTrue(Ledger.is_script_address("rCz6yb1p33oYHToGZDzTjX7nFKaU3kNgBd")) class TestSynchronization(LedgerTestCase):