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

Add CLI for Clawback #15483

Merged
merged 5 commits into from
Jun 14, 2023
Merged
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
55 changes: 53 additions & 2 deletions chia/cmds/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ def get_transaction_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: in
default=False,
help="Reverse the transaction ordering",
)
@click.option(
"--clawback",
is_flag=True,
default=False,
help="Only show clawback transactions",
)
def get_transactions_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
Expand All @@ -106,7 +112,8 @@ def get_transactions_cmd(
paginate: Optional[bool],
sort_key: SortKey,
reverse: bool,
) -> None:
clawback: bool,
) -> None: # pragma: no cover
extra_params = {
"id": id,
"verbose": verbose,
Expand All @@ -115,6 +122,7 @@ def get_transactions_cmd(
"limit": limit,
"sort_key": sort_key,
"reverse": reverse,
"clawback": clawback,
}

import asyncio
Expand Down Expand Up @@ -185,6 +193,13 @@ def get_transactions_cmd(
is_flag=True,
default=False,
)
@click.option(
"--clawback_time",
help="The seconds that the recipient needs to wait to claim the fund."
" A positive number will enable the Clawback features.",
type=int,
default=0,
)
def send_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
Expand All @@ -198,7 +213,8 @@ def send_cmd(
max_coin_amount: str,
coins_to_exclude: Tuple[str],
reuse: bool,
) -> None:
clawback_time: int,
) -> None: # pragma: no cover
extra_params = {
"id": id,
"amount": amount,
Expand All @@ -210,6 +226,7 @@ def send_cmd(
"max_coin_amount": max_coin_amount,
"exclude_coin_ids": list(coins_to_exclude),
"reuse_puzhash": True if reuse else None,
"clawback_time": clawback_time,
}
import asyncio

Expand Down Expand Up @@ -274,6 +291,40 @@ def get_address_cmd(wallet_rpc_port: Optional[int], id, fingerprint: int, new_ad
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, get_address))


@wallet_cmd.command(
"clawback",
help="Claim or revert a Clawback transaction."
" The wallet will automatically detect if you are able to revert or claim.",
)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-i", "--id", help="Id of the wallet to use", type=int, default=1, show_default=True, required=True)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option(
"-ids",
"--tx_ids",
help="IDs of the Clawback transactions you want to revert or claim. Separate multiple IDs by comma (,).",
type=str,
default="",
required=True,
)
@click.option(
"-m", "--fee", help="A fee to add to the offer when it gets taken, in XCH", default="0", show_default=True
)
def clawback(wallet_rpc_port: Optional[int], id, fingerprint: int, tx_ids: str, fee: str) -> None: # pragma: no cover
extra_params = {"id": id, "tx_ids": tx_ids, "fee": fee}
import asyncio

from .wallet_funcs import spend_clawback

asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, spend_clawback))


@wallet_cmd.command("delete_unconfirmed_transactions", help="Deletes all unconfirmed transactions for this wallet ID")
@click.option(
"-wp",
Expand Down
71 changes: 64 additions & 7 deletions chia/cmds/wallet_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@
from chia.wallet.trading.trade_status import TradeStatus
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.address_type import AddressType, ensure_valid_address
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.puzzle_decorator_type import PuzzleDecoratorType
from chia.wallet.util.query_filter import HashFilter, TransactionTypeFilter
from chia.wallet.util.transaction_type import CLAWBACK_TRANSACTION_TYPES, TransactionType
from chia.wallet.util.wallet_types import WalletType
from chia.wallet.vc_wallet.vc_store import VCProofs
from chia.wallet.wallet_coin_store import GetCoinRecords

CATNameResolver = Callable[[bytes32], Awaitable[Optional[Tuple[Optional[uint32], str]]]]

Expand All @@ -41,14 +44,24 @@
TransactionType.FEE_REWARD: "rewarded",
TransactionType.INCOMING_TRADE: "received in trade",
TransactionType.OUTGOING_TRADE: "sent in trade",
TransactionType.INCOMING_CLAWBACK_RECEIVE: "received in clawback as recipient",
TransactionType.INCOMING_CLAWBACK_SEND: "received in clawback as sender",
TransactionType.OUTGOING_CLAWBACK: "claim/clawback",
}


def transaction_description_from_type(tx: TransactionRecord) -> str:
return transaction_type_descriptions.get(TransactionType(tx.type), "(unknown reason)")


def print_transaction(tx: TransactionRecord, verbose: bool, name, address_prefix: str, mojo_per_unit: int) -> None:
def print_transaction(
tx: TransactionRecord,
verbose: bool,
name,
address_prefix: str,
mojo_per_unit: int,
coin_record: Optional[Dict[str, Any]] = None,
) -> None: # pragma: no cover
if verbose:
print(tx)
else:
Expand All @@ -60,6 +73,13 @@ def print_transaction(tx: TransactionRecord, verbose: bool, name, address_prefix
print(f"Amount {description}: {chia_amount} {name}")
trepca marked this conversation as resolved.
Show resolved Hide resolved
print(f"To address: {to_address}")
print("Created at:", datetime.fromtimestamp(tx.created_at_time).strftime("%Y-%m-%d %H:%M:%S"))
if coin_record is not None:
print(
"Recipient claimable time:",
datetime.fromtimestamp(tx.created_at_time + coin_record["metadata"]["time_lock"]).strftime(
"%Y-%m-%d %H:%M:%S"
),
)
print("")


Expand Down Expand Up @@ -142,7 +162,7 @@ async def get_transaction(args: dict, wallet_client: WalletRpcClient, fingerprin
)


async def get_transactions(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None:
async def get_transactions(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: # pragma: no cover
wallet_id = args["id"]
paginate = args["paginate"]
if paginate is None:
Expand All @@ -151,9 +171,15 @@ async def get_transactions(args: dict, wallet_client: WalletRpcClient, fingerpri
limit = args["limit"]
sort_key = args["sort_key"]
reverse = args["reverse"]

type_filter = (
None
if not args["clawback"]
else TransactionTypeFilter.include(
[TransactionType.INCOMING_CLAWBACK_RECEIVE, TransactionType.INCOMING_CLAWBACK_SEND]
)
)
txs: List[TransactionRecord] = await wallet_client.get_transactions(
wallet_id, start=offset, end=(offset + limit), sort_key=sort_key, reverse=reverse
wallet_id, start=offset, end=(offset + limit), sort_key=sort_key, reverse=reverse, type_filter=type_filter
)

config = load_config(DEFAULT_ROOT_PATH, "config.yaml", SERVICE_NAME)
Expand All @@ -179,12 +205,20 @@ async def get_transactions(args: dict, wallet_client: WalletRpcClient, fingerpri
for j in range(0, num_per_screen):
if i + j >= len(txs):
break
coin_record: Optional[Dict[str, Any]] = None
if txs[i + j].type in CLAWBACK_TRANSACTION_TYPES:
coin_record = (
await wallet_client.get_coin_records(
GetCoinRecords(coin_id_filter=HashFilter.include([txs[i + j].additions[0].name()]))
)
)["coin_records"][0]
print_transaction(
txs[i + j],
verbose=(args["verbose"] > 0),
name=name,
address_prefix=address_prefix,
mojo_per_unit=mojo_per_unit,
coin_record=coin_record,
)
if i + num_per_screen >= len(txs):
return None
Expand All @@ -201,7 +235,7 @@ def check_unusual_transaction(amount: Decimal, fee: Decimal):
return fee >= amount


async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None:
async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: # pragma: no cover
wallet_id: int = args["id"]
amount = Decimal(args["amount"])
fee = Decimal(args["fee"])
Expand All @@ -212,6 +246,7 @@ async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) ->
exclude_coin_ids: List[str] = args["exclude_coin_ids"]
memo = args["memo"]
reuse_puzhash = args["reuse_puzhash"]
clawback_time_lock = args["clawback_time"]
if memo is None:
memos = None
else:
Expand All @@ -226,7 +261,9 @@ async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) ->
if amount == 0:
print("You can not send an empty transaction")
return

if clawback_time_lock < 0:
print("Clawback time lock seconds cannot be negative.")
return
try:
typ = await get_wallet_type(wallet_id=wallet_id, wallet_client=wallet_client)
mojo_per_unit = get_mojo_per_unit(typ)
Expand All @@ -250,6 +287,11 @@ async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) ->
final_max_coin_amount,
exclude_coin_ids=exclude_coin_ids,
reuse_puzhash=reuse_puzhash,
puzzle_decorator_override=[
{"decorator": PuzzleDecoratorType.CLAWBACK.name, "clawback_timelock": clawback_time_lock}
]
if clawback_time_lock > 0
else None,
)
elif typ == WalletType.CAT:
print("Submitting transaction...")
Expand Down Expand Up @@ -1225,6 +1267,21 @@ async def sign_message(args: Dict, wallet_client: WalletRpcClient, fingerprint:
print(f"Signing Mode: {signing_mode}")


async def spend_clawback(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: # pragma: no cover
tx_ids = []
for tid in args["tx_ids"].split(","):
tx_ids.append(bytes32.from_hexstr(tid))
if len(tx_ids) == 0:
print("Transaction ID is required.")
return
fee = Decimal(args["fee"])
if fee < 0:
print("Batch fee cannot be negative.")
return
response = await wallet_client.spend_clawback_coins(tx_ids, int(fee * units["chia"]))
print(str(response))


async def mint_vc(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: # pragma: no cover
config = load_config(DEFAULT_ROOT_PATH, "config.yaml", SERVICE_NAME)
vc_record, txs = await wallet_client.vc_mint(
Expand Down
9 changes: 4 additions & 5 deletions chia/rpc/wallet_rpc_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
from chia.wallet.util.compute_hints import compute_coin_hints
from chia.wallet.util.compute_memos import compute_memos
from chia.wallet.util.query_filter import HashFilter, TransactionTypeFilter
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.transaction_type import CLAWBACK_TRANSACTION_TYPES, TransactionType
from chia.wallet.util.wallet_sync_utils import fetch_coin_spend_for_coin_state
from chia.wallet.util.wallet_types import CoinType, WalletType
from chia.wallet.vc_wallet.vc_store import VCProofs
Expand Down Expand Up @@ -899,12 +899,11 @@ async def get_transactions(self, request: Dict) -> EndpointResult:
)
tx_list = []
# Format for clawback transactions
clawback_types = {TransactionType.INCOMING_CLAWBACK_RECEIVE.value, TransactionType.INCOMING_CLAWBACK_SEND.value}
for tr in transactions:
try:
tx = (await self._convert_tx_puzzle_hash(tr)).to_json_dict_convenience(self.service.config)
tx_list.append(tx)
if tx["type"] not in clawback_types:
if tx["type"] not in CLAWBACK_TRANSACTION_TYPES:
continue
coin: Coin = tr.additions[0]
record: Optional[WalletCoinRecord] = await self.service.wallet_state_manager.coin_store.get_coin_record(
Expand All @@ -914,8 +913,8 @@ async def get_transactions(self, request: Dict) -> EndpointResult:
tx["metadata"] = record.parsed_metadata().to_json_dict()
tx["metadata"]["coin_id"] = coin.name().hex()
tx["metadata"]["spent"] = record.spent
except Exception as e:
log.error(f"Failed to get transaction {tr.name}: {e}")
except Exception:
log.exception(f"Failed to get transaction {tr.name}.")
return {
"transactions": tx_list,
"wallet_id": wallet_id,
Expand Down
2 changes: 1 addition & 1 deletion chia/wallet/puzzles/clawback/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ClawbackVersion(IntEnum):
@streamable
@dataclass(frozen=True)
class AutoClaimSettings(Streamable):
enabled: bool = True
enabled: bool = False
tx_fee: uint64 = uint64(0)
min_amount: uint64 = uint64(0)
batch_size: uint16 = uint16(50)
6 changes: 6 additions & 0 deletions chia/wallet/util/transaction_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ class TransactionType(IntEnum):
INCOMING_CLAWBACK_RECEIVE = 6
INCOMING_CLAWBACK_SEND = 7
OUTGOING_CLAWBACK = 8


CLAWBACK_TRANSACTION_TYPES = {
TransactionType.INCOMING_CLAWBACK_SEND.value,
TransactionType.INCOMING_CLAWBACK_RECEIVE.value,
}
7 changes: 2 additions & 5 deletions chia/wallet/wallet_state_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
from chia.wallet.util.compute_memos import compute_memos
from chia.wallet.util.puzzle_decorator import PuzzleDecoratorManager
from chia.wallet.util.query_filter import HashFilter
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.transaction_type import CLAWBACK_TRANSACTION_TYPES, TransactionType
from chia.wallet.util.wallet_sync_utils import (
PeerRequestException,
fetch_coin_spend_for_coin_state,
Expand Down Expand Up @@ -1442,10 +1442,7 @@ async def _add_coin_states(
await self.interested_store.remove_interested_coin_id(coin_state.coin.name())
confirmed_tx_records: List[TransactionRecord] = []
for tx_record in all_unconfirmed:
if tx_record.type in {
TransactionType.INCOMING_CLAWBACK_SEND.value,
TransactionType.INCOMING_CLAWBACK_RECEIVE.value,
}:
if tx_record.type in CLAWBACK_TRANSACTION_TYPES:
for add_coin in tx_record.additions:
if add_coin == coin_state.coin:
confirmed_tx_records.append(tx_record)
Expand Down
15 changes: 15 additions & 0 deletions tests/wallet/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ async def test_wallet_clawback_claim_manual(
wallet_node_2, server_3 = wallets[1]
wallet = wallet_node.wallet_state_manager.main_wallet
wallet_1 = wallet_node_2.wallet_state_manager.main_wallet
api_0 = WalletRpcApi(wallet_node)
api_1 = WalletRpcApi(wallet_node_2)
if trusted:
wallet_node.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()}
Expand Down Expand Up @@ -486,6 +487,20 @@ async def test_wallet_clawback_claim_manual(
await time_out_assert(10, wallet.get_confirmed_balance, 3999999999500)
await time_out_assert(10, wallet_1.get_confirmed_balance, 4000000000500)

txs = await api_0.get_transactions(
dict(
type_filter={
"values": [
TransactionType.INCOMING_CLAWBACK_SEND.value,
],
"mode": 1,
},
wallet_id=1,
)
)
assert len(txs["transactions"]) == 1
assert txs["transactions"][0]["confirmed"]

@pytest.mark.parametrize(
"trusted",
[True, False],
Expand Down