Skip to content

Commit

Permalink
Add CLI for Clawback (#15483)
Browse files Browse the repository at this point in the history
* Add CLI for Clawback

* Fix coverage & Disable auto claim

* Display Clawback TX in CLI

* Fix coverage

* Fix get transaction amount
  • Loading branch information
ytx1991 authored Jun 14, 2023
1 parent d978896 commit 33fa3f4
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 20 deletions.
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}")
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

0 comments on commit 33fa3f4

Please sign in to comment.