CLI send transaction and get transction (#849)

* CLI send transaction and get transction

* get_transaction in help

* Add fingerprint to suggestion

* Remove bad file checked in

* Add serialized program to unhashable types

* Print size of mempool and min fee, optimize excessive name computation

* Fix log

* lint

Co-authored-by: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com>
This commit is contained in:
Mariano Sorgente 2021-02-08 15:14:46 +09:00 committed by GitHub
parent d9396d4308
commit e55844f847
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 245 additions and 97 deletions

View File

@ -1,13 +1,29 @@
import time
from typing import Tuple, Optional, Callable
import aiohttp
import asyncio
from src.rpc.wallet_rpc_client import WalletRpcClient
from src.util.byte_types import hexstr_to_bytes
from src.util.config import load_config
from src.util.default_root import DEFAULT_ROOT_PATH
from src.wallet.util.wallet_types import WalletType
from src.cmds.units import units
command_list = ["send", "show", "get_transaction"]
def help_message():
print("usage: chia wallet command")
print(f"command can be any of {command_list}")
print("")
print("chia wallet send -f [optional fingerprint] -i [optional wallet_id] -a [amount] -f [fee] -t [target address]")
print("chia wallet show -f [optional fingerprint] -i [optional wallet_id]")
print("chia wallet get_transaction -f [optional fingerprint] -i [optional wallet_id] -tx [transaction id]")
def make_parser(parser):
parser.add_argument(
"-wp",
@ -18,103 +34,209 @@ def make_parser(parser):
type=int,
default=9256,
)
parser.set_defaults(function=show)
parser.add_argument(
"-f",
"--fingerprint",
help="Set the fingerprint to specify which wallet to use.",
type=int,
)
parser.add_argument("-i", "--id", help="Id of the wallet to use.", type=int, default=1)
parser.add_argument(
"-a",
"--amount",
help="How much chia to send, in TXCH/XCH",
type=int,
)
parser.add_argument("-m", "--fee", help="Set the fees for the transaction.", type=int, default=0)
parser.add_argument(
"-t",
"--address",
help="Address to send the TXCH/XCH",
type=str,
)
parser.add_argument(
"-tx",
"--tx_id",
help="transaction id to search for",
type=str,
)
parser.add_argument(
"command",
help=f"Command can be any one of {command_list}",
type=str,
nargs="?",
)
parser.set_defaults(function=handler)
parser.print_help = lambda self=parser: help_message()
async def print_balances(wallet_client):
async def get_transaction(args, wallet_client, fingerprint: int):
if args.id is None:
print("Please specify a wallet id with -i")
return
else:
wallet_id = args.id
if args.tx_id is None:
print("Please specify a transaction id -tx")
return
else:
transaction_id = hexstr_to_bytes(args.tx_id)
tx = await wallet_client.get_transaction(wallet_id, transaction_id=transaction_id)
print(tx)
async def send(args, wallet_client, fingerprint: int):
if args.id is None:
print("Please specify a wallet id with -i")
return
else:
wallet_id = args.id
if args.amount is None:
print("Please specify an amount with -a")
return
else:
amount = args.amount
if args.amount is None:
print("Please specify the transaction fees with -m")
return
else:
fee = args.fee
if args.address is None:
print("Please specify a target address with -t")
return
else:
address = args.address
print("Submitting transaction...")
res = await wallet_client.send_transaction(wallet_id, amount, address, fee)
tx_id = res.name
start = time.time()
while time.time() - start < 10:
await asyncio.sleep(0.1)
tx = await wallet_client.get_transaction(wallet_id, tx_id)
if len(tx.sent_to) > 0:
print(f"Transaction submitted to nodes: {tx.sent_to}")
print(f"Do chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id} to get status")
return
print("Transaction not yet submitted to nodes.")
print(f"Do chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id} to get status")
async def print_balances(args, wallet_client, fingerprint: int):
summaries_response = await wallet_client.get_wallets()
print("Balances")
print(f"Balances, fingerprint: {fingerprint}")
for summary in summaries_response:
wallet_id = summary["id"]
balances = await wallet_client.get_wallet_balance(wallet_id)
typ = WalletType(int(summary["type"])).name
if typ != "STANDARD_WALLET":
print(f"Wallet ID {wallet_id} type {typ} {summary['name']}")
print(f" -Confirmed: {balances['confirmed_wallet_balance']/units['colouredcoin']}")
print(
f" -Confirmed: balances['confirmed_wallet_balance']"
f"{balances['confirmed_wallet_balance']/units['colouredcoin']}"
)
print(f" -Unconfirmed: {balances['unconfirmed_wallet_balance']/units['colouredcoin']}")
print(f" -Spendable: {balances['spendable_balance']/units['colouredcoin']}")
print(f" -Frozen: {balances['frozen_balance']/units['colouredcoin']}")
print(f" -Pending change: {balances['pending_change']/units['colouredcoin']}")
else:
print(f"Wallet ID {wallet_id} type {typ}")
print(f" -Confirmed: {balances['confirmed_wallet_balance']/units['chia']} TXCH")
print(f" -Unconfirmed: {balances['unconfirmed_wallet_balance']/units['chia']} TXCH")
print(f" -Spendable: {balances['spendable_balance']/units['chia']} TXCH")
print(f" -Frozen: {balances['frozen_balance']/units['chia']} TXCH")
print(f" -Pending change: {balances['pending_change']/units['chia']} TXCH")
print(
f" -Confirmed: {balances['confirmed_wallet_balance']} mojo "
f"({balances['confirmed_wallet_balance']/units['chia']} TXCH)"
)
print(
f" -Unconfirmed: {balances['unconfirmed_wallet_balance']} mojo "
f"({balances['unconfirmed_wallet_balance']/units['chia']} TXCH)"
)
print(
f" -Spendable: {balances['spendable_balance']} mojo "
f"({balances['spendable_balance']/units['chia']} TXCH)"
)
print(
f" -Pending change: {balances['pending_change']} mojo "
f"({balances['pending_change']/units['chia']} TXCH)"
)
async def wallet_loop(wallet_client):
fingerprint = None
while True:
# if fingerprint is None:
fingerprints = await wallet_client.get_public_keys()
if len(fingerprints) == 0:
print("No keys loaded. Run 'chia keys generate' or import a key.")
return
if len(fingerprints) == 1:
fingerprint = fingerprints[0]
log_in_response = await wallet_client.log_in(fingerprint)
else:
print("Choose wallet key:")
for i, fp in enumerate(fingerprints):
print(f"{i+1}) {fp}")
val = None
while val is None:
val = input("Enter a number to pick or q to quit: ")
if val == "q":
return
if not val.isdigit():
async def get_wallet(wallet_client, fingerprint=None) -> Optional[Tuple[WalletRpcClient, int]]:
fingerprints = await wallet_client.get_public_keys()
if len(fingerprints) == 0:
print("No keys loaded. Run 'chia keys generate' or import a key.")
return None
if fingerprint is not None:
if fingerprint not in fingerprints:
print(f"Fingerprint {fingerprint} does not exist")
return None
if len(fingerprints) == 1:
fingerprint = fingerprints[0]
if fingerprint is not None:
log_in_response = await wallet_client.log_in(fingerprint)
else:
print("Choose wallet key:")
for i, fp in enumerate(fingerprints):
print(f"{i+1}) {fp}")
val = None
while val is None:
val = input("Enter a number to pick or q to quit: ")
if val == "q":
return None
if not val.isdigit():
val = None
else:
index = int(val) - 1
if index >= len(fingerprints):
print("Invalid value")
val = None
continue
else:
index = int(val) - 1
if index >= len(fingerprints):
print("Invalid value")
val = None
continue
else:
fingerprint = fingerprints[index]
log_in_response = await wallet_client.log_in(fingerprint)
if log_in_response["success"] is False:
if log_in_response["error"] == "not_initialized":
use_cloud = True
if "backup_path" in log_in_response:
path = log_in_response["backup_path"]
print(f"Backup file from backup.chia.net downloaded and written to: {path}")
val = input("Do you want to use this file to restore from backup? (Y/N) ")
if val.lower() == "y":
log_in_response = await wallet_client.log_in_and_restore(fingerprint, path)
else:
use_cloud = False
fingerprint = fingerprints[index]
log_in_response = await wallet_client.log_in(fingerprint)
if log_in_response["success"] is False:
if log_in_response["error"] == "not_initialized":
use_cloud = True
if "backup_path" in log_in_response:
path = log_in_response["backup_path"]
print(f"Backup file from backup.chia.net downloaded and written to: {path}")
val = input("Do you want to use this file to restore from backup? (Y/N) ")
if val.lower() == "y":
log_in_response = await wallet_client.log_in_and_restore(fingerprint, path)
else:
use_cloud = False
if "backup_path" not in log_in_response or use_cloud is False:
if use_cloud is True:
val = input(
"No online backup file found, \n Press S to skip restore from backup"
" \n Press F to use your own backup file: "
)
else:
val = input(
"Cloud backup declined, \n Press S to skip restore from backup"
" \n Press F to use your own backup file: "
)
if "backup_path" not in log_in_response or use_cloud is False:
if use_cloud is True:
val = input(
"No online backup file found, \n Press S to skip restore from backup"
" \n Press F to use your own backup file: "
)
else:
val = input(
"Cloud backup declined, \n Press S to skip restore from backup"
" \n Press F to use your own backup file: "
)
if val.lower() == "s":
log_in_response = await wallet_client.log_in_and_skip(fingerprint)
elif val.lower() == "f":
val = input("Please provide the full path to your backup file: ")
log_in_response = await wallet_client.log_in_and_restore(fingerprint, val)
if val.lower() == "s":
log_in_response = await wallet_client.log_in_and_skip(fingerprint)
elif val.lower() == "f":
val = input("Please provide the full path to your backup file: ")
log_in_response = await wallet_client.log_in_and_restore(fingerprint, val)
if "success" not in log_in_response or log_in_response["success"] is False:
if "error" in log_in_response:
error = log_in_response["error"]
print(f"Error: {log_in_response[error]}")
return
await print_balances(wallet_client)
break
if "success" not in log_in_response or log_in_response["success"] is False:
if "error" in log_in_response:
error = log_in_response["error"]
print(f"Error: {log_in_response[error]}")
return None
return wallet_client, fingerprint
async def show_async(args, parser):
async def execute_with_wallet(args, parser, function: Callable):
if args.fingerprint is None:
fingerprint = None
else:
fingerprint = args.fingerprint
try:
config = load_config(DEFAULT_ROOT_PATH, "config.yaml")
self_hostname = config["self_hostname"]
@ -123,7 +245,13 @@ async def show_async(args, parser):
else:
wallet_rpc_port = args.wallet_rpc_port
wallet_client = await WalletRpcClient.create(self_hostname, wallet_rpc_port, DEFAULT_ROOT_PATH, config)
await wallet_loop(wallet_client)
wallet_client_f = await get_wallet(wallet_client, fingerprint=fingerprint)
if wallet_client_f is None:
wallet_client.close()
await wallet_client.await_closed()
return
wallet_client, fingerprint = wallet_client_f
await function(args, wallet_client, fingerprint)
except Exception as e:
if isinstance(e, aiohttp.client_exceptions.ClientConnectorError):
@ -135,5 +263,18 @@ async def show_async(args, parser):
await wallet_client.await_closed()
def show(args, parser):
return asyncio.run(show_async(args, parser))
def handler(args, parser):
if args.command is None or len(args.command) < 1:
help_message()
parser.exit(1)
command = args.command
if command not in command_list:
help_message()
parser.exit(1)
if command == "get_transaction":
return asyncio.run(execute_with_wallet(args, parser, get_transaction))
if command == "send":
return asyncio.run(execute_with_wallet(args, parser, send))
elif command == "show":
return asyncio.run(execute_with_wallet(args, parser, print_balances))

View File

@ -143,7 +143,6 @@ class FullNodeAPI:
transaction = full_node_protocol.RespondTransaction(spend_bundle)
msg = Message("respond_transaction", transaction)
self.log.debug(f"sending transaction (tx_id: {spend_bundle.name()}) to peer")
return msg
@peer_required
@ -167,26 +166,35 @@ class FullNodeAPI:
return None
# Ignore if we have already added this transaction
if self.full_node.mempool_manager.get_spendbundle(tx.transaction.name()) is not None:
return None
spend_name = tx.transaction.name()
if self.full_node.mempool_manager.get_spendbundle(spend_name) is not None:
return None
# This is important because we might not have added the spend bundle, but we are about to add it, so it
# prevents double processing
if self.full_node.mempool_manager.seen(spend_name):
return None
# Adds it to seen so we don't double process
self.full_node.mempool_manager.add_and_maybe_pop_seen(spend_name)
# Do the expensive pre-validation outside the lock
cost_result = await self.full_node.mempool_manager.pre_validate_spendbundle(tx.transaction)
async with self.full_node.blockchain.lock:
if self.full_node.mempool_manager.get_spendbundle(tx.transaction.name()) is not None:
# Check for unnecessary addition
if self.full_node.mempool_manager.get_spendbundle(spend_name) is not None:
return None
# Performs DB operations within the lock
cost, status, error = await self.full_node.mempool_manager.add_spendbundle(
tx.transaction, cost_result, spend_name
)
if status == MempoolInclusionStatus.SUCCESS:
self.log.debug(f"Added transaction to mempool: {tx.transaction.name()}")
fees = tx.transaction.fees()
self.log.debug(f"Added transaction to mempool: {spend_name} cost: {cost} fees {fees}")
assert fees >= 0
assert cost is not None
new_tx = full_node_protocol.NewTransaction(
tx.transaction.name(),
spend_name,
cost,
uint64(tx.transaction.fees()),
)
@ -194,9 +202,7 @@ class FullNodeAPI:
await self.server.send_to_all_except([message], NodeType.FULL_NODE, peer.peer_node_id)
else:
self.full_node.mempool_manager.remove_seen(spend_name)
self.log.warning(
f"Was not able to add transaction with id {tx.transaction.name()}, {status} error: {error}"
)
self.log.warning(f"Was not able to add transaction with id {spend_name}, {status} error: {error}")
return None
@api_request
@ -1002,7 +1008,7 @@ class FullNodeAPI:
if self.full_node.mempool_manager.seen(spend_name):
return None
self.full_node.mempool_manager.add_and_maybe_pop_seen(spend_name)
self.log.debug(f"Processing transaction: {request.transaction.name()}")
self.log.debug(f"Processing transaction: {spend_name}")
# Ignore if syncing
if self.full_node.sync_store.get_sync_mode():
status = MempoolInclusionStatus.FAILED
@ -1014,14 +1020,14 @@ class FullNodeAPI:
request.transaction, cost_result, spend_name
)
if status == MempoolInclusionStatus.SUCCESS:
self.log.debug(f"Added transaction to mempool: {request.transaction.name()}")
self.log.debug(f"Added transaction to mempool: {spend_name}")
# Only broadcast successful transactions, not pending ones. Otherwise it's a DOS
# vector.
fees = request.transaction.fees()
assert fees >= 0
assert cost is not None
new_tx = full_node_protocol.NewTransaction(
request.transaction.name(),
spend_name,
cost,
uint64(request.transaction.fees()),
)
@ -1029,22 +1035,19 @@ class FullNodeAPI:
await self.full_node.server.send_to_all([msg], NodeType.FULL_NODE)
else:
self.log.warning(
f"Wasn't able to add transaction with id {request.transaction.name()}, "
f"status {status} error: {error}"
f"Wasn't able to add transaction with id {spend_name}, " f"status {status} error: {error}"
)
error_name = error.name if error is not None else None
if status == MempoolInclusionStatus.SUCCESS:
response = wallet_protocol.TransactionAck(request.transaction.name(), status, error_name)
response = wallet_protocol.TransactionAck(spend_name, status, error_name)
else:
self.full_node.mempool_manager.remove_seen(spend_name)
# If if failed/pending, but it previously succeeded (in mempool), this is idempotence, return SUCCESS
if self.full_node.mempool_manager.get_spendbundle(request.transaction.name()) is not None:
response = wallet_protocol.TransactionAck(
request.transaction.name(), MempoolInclusionStatus.SUCCESS, None
)
if self.full_node.mempool_manager.get_spendbundle(spend_name) is not None:
response = wallet_protocol.TransactionAck(spend_name, MempoolInclusionStatus.SUCCESS, None)
else:
response = wallet_protocol.TransactionAck(request.transaction.name(), status, error_name)
response = wallet_protocol.TransactionAck(spend_name, status, error_name)
msg = Message("transaction_ack", response)
return msg

View File

@ -408,6 +408,9 @@ class MempoolManager:
self.potential_txs = {}
for tx, cached_result, cached_name in potential_txs_copy.values():
await self.add_spendbundle(tx, cached_result, cached_name)
log.debug(
f"Size of mempool: {len(self.mempool.spends)}, minimum fee to get in: {self.mempool.get_min_fee_rate()}"
)
async def get_items_not_in_filter(self, mempool_filter: PyBIP158) -> List[MempoolItem]:
items: List[MempoolItem] = []

View File

@ -8,7 +8,7 @@ import pprint
from enum import Enum
from typing import Any, BinaryIO, List, Type, get_type_hints, Dict, Tuple
from src.util.byte_types import hexstr_to_bytes
from src.types.program import Program
from src.types.program import Program, SerializedProgram
from src.util.hash import std_hash
from blspy import PrivateKey, G1Element, G2Element
@ -48,6 +48,7 @@ unhashable_types = [
G1Element,
G2Element,
Program,
SerializedProgram,
]
# JSON does not support big ints, so these types must be serialized differently in JSON
big_ints = [uint64, int64, uint128, int512]