Files
chia-blockchain/chia/_tests/wallet/test_signer_protocol.py
T
Matt HauffandGitHub d90001e3ee [LABS-170] Add automatic reorg testing to WalletTestFramework (#21211)
* [LABS-170] Add automatic reorg testing to `WalletTestFramework`

* Actually exempt reorgs when specified

* Comments by @cursor

* Fix CAT wallet tests

* Reorg exempt offer tests

* Fix DID tests

* Fix NFTWallet tests

* Fix RPC tests

* Fix VC tests

* Exempt pool tests

* Add reorg_exempt to top level conftest

* Fix tests (nd maybe break them?)
2026-08-10 16:49:16 -05:00

959 lines
36 KiB
Python

from __future__ import annotations
import dataclasses
import click
import pytest
from chia_rs import AugSchemeMPL, CoinSpend, G1Element, G2Element, PrivateKey
from chia_rs.sized_bytes import bytes32
from chia_rs.sized_ints import uint64
from click.testing import CliRunner
from chia._tests.cmds.test_cmd_framework import check_click_parsing
from chia._tests.cmds.wallet.test_consts import STD_TX
from chia._tests.conftest import ConsensusMode
from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework
from chia.cmds.cmd_classes import chia_command
from chia.cmds.cmd_helpers import NeedsWalletRPC, TransactionsIn, TransactionsOut, WalletClientInfo
from chia.cmds.cmds_util import TransactionBundle
from chia.cmds.signer import (
ApplySignaturesCMD,
ExecuteSigningInstructionsCMD,
GatherSigningInfoCMD,
PushTransactionsCMD,
QrCodeDisplay,
SPIn,
SPOut,
)
from chia.rpc.util import ALL_TRANSLATION_LAYERS
from chia.types.blockchain_format.coin import Coin as ConsensusCoin
from chia.types.blockchain_format.program import Program
from chia.types.coin_spend import make_spend
from chia.util.hash import std_hash
from chia.util.streamable import Streamable
from chia.wallet.conditions import AggSigMe
from chia.wallet.derivation_record import DerivationRecord
from chia.wallet.derive_keys import _derive_path_unhardened
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
DEFAULT_HIDDEN_PUZZLE_HASH,
calculate_synthetic_offset,
)
from chia.wallet.signer_protocol import (
Coin,
KeyHints,
PathHint,
SignedTransaction,
SigningInstructions,
SigningResponse,
SigningTarget,
Spend,
SumHint,
TransactionInfo,
UnsignedTransaction,
)
from chia.wallet.util.blind_signer_tl import (
BLIND_SIGNER_TRANSLATION,
BSTLPathHint,
BSTLSigningInstructions,
BSTLSigningResponse,
BSTLSigningTarget,
BSTLSumHint,
BSTLUnsignedTransaction,
)
from chia.wallet.util.clvm_streamable import (
TranslationLayer,
TranslationLayerMapping,
byte_serialize_clvm_streamable,
clvm_streamable,
json_deserialize_with_clvm_streamable,
json_serialize_with_clvm_streamable,
)
from chia.wallet.wallet import Wallet
from chia.wallet.wallet_request_types import (
ApplySignatures,
ExecuteSigningInstructions,
GatherSigningInfo,
GatherSigningInfoResponse,
SubmitTransactions,
)
from chia.wallet.wallet_rpc_client import WalletRpcClient
from chia.wallet.wallet_spend_bundle import WalletSpendBundle
from chia.wallet.wallet_state_manager import WalletStateManager
def test_unsigned_transaction_type() -> None:
pubkey: G1Element = G1Element()
message: bytes = b"message"
coin: ConsensusCoin = ConsensusCoin(bytes32.zeros, bytes32.zeros, uint64(0))
puzzle: Program = Program.to(1)
solution: Program = Program.to([AggSigMe(pubkey, message).to_program()])
coin_spend: CoinSpend = make_spend(coin, puzzle, solution)
assert Spend.from_coin_spend(coin_spend).as_coin_spend() == coin_spend
tx: UnsignedTransaction = UnsignedTransaction(
TransactionInfo([Spend.from_coin_spend(coin_spend)]),
SigningInstructions(
KeyHints([], []),
[SigningTarget(pubkey.get_fingerprint().to_bytes(4, "big"), message, bytes32([1] * 32))],
),
)
assert tx == json_deserialize_with_clvm_streamable(json_serialize_with_clvm_streamable(tx), UnsignedTransaction)
as_json_dict = {
"coin": {
"parent_coin_id": "0x" + tx.transaction_info.spends[0].coin.parent_coin_id.hex(),
"puzzle_hash": "0x" + tx.transaction_info.spends[0].coin.puzzle_hash.hex(),
"amount": tx.transaction_info.spends[0].coin.amount,
},
"puzzle": "0x" + bytes(tx.transaction_info.spends[0].puzzle).hex(),
"solution": "0x" + bytes(tx.transaction_info.spends[0].solution).hex(),
}
assert tx.transaction_info.spends[0].to_json_dict() == as_json_dict
@pytest.mark.parametrize(
"wallet_environments",
[
{
"num_environments": 1,
"blocks_needed": [1],
"trusted": True,
"reuse_puzhash": True,
}
],
indirect=True,
)
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0])
@pytest.mark.anyio
async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFramework) -> None:
wallet: Wallet = wallet_environments.environments[0].xch_wallet
wallet_state_manager: WalletStateManager = wallet_environments.environments[0].wallet_state_manager
wallet_rpc: WalletRpcClient = wallet_environments.environments[0].rpc_client
# Test first that we can properly examine and sign a regular transaction
async with wallet.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=False) as action_scope:
[coin] = await wallet.select_coins(uint64(0), action_scope)
puzzle: Program = await wallet.puzzle_for_puzzle_hash(coin.puzzle_hash)
delegated_puzzle: Program = Program.NIL
delegated_puzzle_hash: bytes32 = delegated_puzzle.get_tree_hash()
solution: Program = Program.to([None, None, None])
coin_spend: CoinSpend = make_spend(
coin,
puzzle,
solution,
)
derivation_record: (
DerivationRecord | None
) = await wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(coin.puzzle_hash)
assert derivation_record is not None
pubkey: G1Element = derivation_record.pubkey
atom = puzzle.uncurry()[1].at("f").atom
assert atom is not None
synthetic_pubkey: G1Element = G1Element.from_bytes(atom)
message: bytes = delegated_puzzle_hash + coin.name() + wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA
utx: UnsignedTransaction = UnsignedTransaction(
TransactionInfo([Spend.from_coin_spend(coin_spend)]),
(
await wallet_rpc.gather_signing_info(GatherSigningInfo(spends=[Spend.from_coin_spend(coin_spend)]))
).signing_instructions,
)
assert utx.signing_instructions.key_hints.sum_hints == [
SumHint(
[pubkey.get_fingerprint().to_bytes(4, "big")],
calculate_synthetic_offset(pubkey, DEFAULT_HIDDEN_PUZZLE_HASH).to_bytes(32, "big"),
wallet_state_manager.main_wallet.puzzle_for_pk(pubkey).uncurry()[1].at("f").as_atom(),
)
]
assert utx.signing_instructions.key_hints.path_hints == [
PathHint(
wallet_state_manager.root_pubkey.get_fingerprint().to_bytes(4, "big"),
[uint64(12381), uint64(8444), uint64(2), uint64(derivation_record.index)],
)
]
assert len(utx.signing_instructions.targets) == 1
assert utx.signing_instructions.targets[0].fingerprint == synthetic_pubkey.get_fingerprint().to_bytes(4, "big")
assert utx.signing_instructions.targets[0].message == message
signing_responses: list[SigningResponse] = (
await wallet_rpc.execute_signing_instructions(
ExecuteSigningInstructions(signing_instructions=utx.signing_instructions)
)
).signing_responses
assert len(signing_responses) == 1
assert signing_responses[0].hook == utx.signing_instructions.targets[0].hook
assert AugSchemeMPL.verify(synthetic_pubkey, message, G2Element.from_bytes(signing_responses[0].signature))
# Now test that we can partially sign a transaction
ACS: Program = Program.to(1)
ACS_PH = Program.to(1).get_tree_hash()
not_our_private_key: PrivateKey = PrivateKey.from_bytes(bytes([1] * 32))
not_our_pubkey: G1Element = not_our_private_key.get_g1()
not_our_message: bytes = b"not our message"
not_our_coin: ConsensusCoin = ConsensusCoin(
bytes32.zeros,
ACS_PH,
uint64(0),
)
not_our_coin_spend: CoinSpend = make_spend(not_our_coin, ACS, Program.to([[49, not_our_pubkey, not_our_message]]))
not_our_utx: UnsignedTransaction = UnsignedTransaction(
TransactionInfo([Spend.from_coin_spend(coin_spend), Spend.from_coin_spend(not_our_coin_spend)]),
(
await wallet_rpc.gather_signing_info(
GatherSigningInfo(spends=[Spend.from_coin_spend(coin_spend), Spend.from_coin_spend(not_our_coin_spend)])
)
).signing_instructions,
)
assert not_our_utx.signing_instructions.key_hints == utx.signing_instructions.key_hints
assert len(not_our_utx.signing_instructions.targets) == 2
assert not_our_utx.signing_instructions.targets[0].fingerprint == synthetic_pubkey.get_fingerprint().to_bytes(
4, "big"
)
assert not_our_utx.signing_instructions.targets[0].message == bytes(message)
assert not_our_utx.signing_instructions.targets[1].fingerprint == not_our_pubkey.get_fingerprint().to_bytes(
4, "big"
)
assert not_our_utx.signing_instructions.targets[1].message == bytes(not_our_message)
not_our_signing_instructions: SigningInstructions = not_our_utx.signing_instructions
with pytest.raises(ValueError, match=r"not found \(or path/sum hinted to\)"):
await wallet_state_manager.signer.execute_signing_instructions(not_our_signing_instructions)
with pytest.raises(ValueError, match=r"No pubkey found \(or path hinted to\) for fingerprint"):
await wallet_state_manager.signer.execute_signing_instructions(
dataclasses.replace(
not_our_signing_instructions,
key_hints=dataclasses.replace(
not_our_signing_instructions.key_hints,
sum_hints=[
*not_our_signing_instructions.key_hints.sum_hints,
SumHint([bytes(not_our_pubkey)], std_hash(b"sum hint only"), bytes(G1Element())),
],
),
)
)
with pytest.raises(ValueError, match="No root pubkey for fingerprint"):
await wallet_state_manager.signer.execute_signing_instructions(
dataclasses.replace(
not_our_signing_instructions,
key_hints=dataclasses.replace(
not_our_signing_instructions.key_hints,
path_hints=[
*not_our_signing_instructions.key_hints.path_hints,
PathHint(bytes(not_our_pubkey), [uint64(0)]),
],
),
)
)
signing_responses_2 = await wallet_state_manager.signer.execute_signing_instructions(
not_our_signing_instructions, partial_allowed=True
)
assert len(signing_responses_2) == 2
assert (
bytes(AugSchemeMPL.aggregate([G2Element.from_bytes(sig.signature) for sig in signing_responses_2]))
== signing_responses[0].signature
)
signed_txs: list[SignedTransaction] = (
await wallet_rpc.apply_signatures(
ApplySignatures(spends=[Spend.from_coin_spend(coin_spend)], signing_responses=signing_responses)
)
).signed_transactions
await wallet_rpc.submit_transactions(SubmitTransactions(signed_transactions=signed_txs))
bundle = WalletSpendBundle(
[spend.as_coin_spend() for tx in signed_txs for spend in tx.transaction_info.spends],
G2Element.from_bytes(signing_responses[0].signature),
)
await wallet_environments.full_node.wait_bundle_ids_in_mempool([bundle.name()])
await wallet_environments.process_pending_states(
[
WalletStateTransition(
# We haven't submitted a TransactionRecord so the wallet won't know about this until confirmed
pre_block_balance_updates={},
post_block_balance_updates={
1: {
"confirmed_wallet_balance": -1 * coin.amount,
"unconfirmed_wallet_balance": -1 * coin.amount,
"spendable_balance": -1 * coin.amount,
"max_send_amount": -1 * coin.amount,
"unspent_coin_count": -1,
},
},
),
],
bundles_to_repush=[bundle],
)
# And test that we can get compressed versions if we want
request = GatherSigningInfo(
spends=[Spend.from_coin_spend(coin_spend), Spend.from_coin_spend(not_our_coin_spend)]
).to_json_dict()
response_dict = await wallet_rpc.fetch("gather_signing_info", {"translation": "chip-0029", **request})
response: GatherSigningInfoResponse = json_deserialize_with_clvm_streamable(
response_dict, GatherSigningInfoResponse, translation_layer=BLIND_SIGNER_TRANSLATION
)
assert response.signing_instructions == not_our_utx.signing_instructions
@pytest.mark.parametrize(
"wallet_environments",
[
{
"num_environments": 1,
"blocks_needed": [1],
"trusted": True,
"reuse_puzhash": True,
}
],
indirect=True,
)
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0])
@pytest.mark.anyio
async def test_p2blsdohp_execute_signing_instructions(wallet_environments: WalletTestFramework) -> None:
wallet: Wallet = wallet_environments.environments[0].xch_wallet
root_sk: PrivateKey = wallet.wallet_state_manager.get_master_private_key()
root_pk: G1Element = root_sk.get_g1()
root_fingerprint: bytes = root_pk.get_fingerprint().to_bytes(4, "big")
# Test just a path hint
test_name: bytes32 = std_hash(b"path hint only")
child_sk: PrivateKey = _derive_path_unhardened(root_sk, [uint64(1), uint64(2), uint64(3), uint64(4)])
signing_responses: list[SigningResponse] = await wallet.wallet_state_manager.signer.execute_signing_instructions(
SigningInstructions(
KeyHints(
[],
[PathHint(root_fingerprint, [uint64(1), uint64(2), uint64(3), uint64(4)])],
),
[SigningTarget(child_sk.get_g1().get_fingerprint().to_bytes(4, "big"), test_name, test_name)],
)
)
assert signing_responses == [SigningResponse(bytes(AugSchemeMPL.sign(child_sk, test_name)), test_name)]
# Test just a sum hint
test_name = std_hash(b"sum hint only")
other_sk: PrivateKey = PrivateKey.from_bytes(test_name)
sum_pk: G1Element = other_sk.get_g1() + root_pk
signing_instructions: SigningInstructions = SigningInstructions(
KeyHints(
[SumHint([root_fingerprint], test_name, bytes(sum_pk))],
[],
),
[SigningTarget(sum_pk.get_fingerprint().to_bytes(4, "big"), test_name, test_name)],
)
for partial_allowed in (True, False):
signing_responses = await wallet.wallet_state_manager.signer.execute_signing_instructions(
signing_instructions, partial_allowed
)
assert signing_responses == [
SigningResponse(
bytes(
AugSchemeMPL.aggregate(
[
AugSchemeMPL.sign(other_sk, test_name, sum_pk),
AugSchemeMPL.sign(root_sk, test_name, sum_pk),
]
)
),
test_name,
),
]
# Toss in a random SigningTarget to see that the responses split up
signing_instructions = dataclasses.replace(
signing_instructions,
targets=[
SigningTarget(sum_pk.get_fingerprint().to_bytes(4, "big"), test_name, test_name),
SigningTarget(b"random fingerprint", test_name, test_name),
],
)
signing_responses = await wallet.wallet_state_manager.signer.execute_signing_instructions(
signing_instructions, partial_allowed=True
)
assert signing_responses == [
SigningResponse(
bytes(
AugSchemeMPL.sign(root_sk, test_name, sum_pk),
),
test_name,
),
SigningResponse(
bytes(
AugSchemeMPL.sign(other_sk, test_name, sum_pk),
),
test_name,
),
]
# Test both path and sum hint
test_name = std_hash(b"path and sum hint")
child_sk = _derive_path_unhardened(root_sk, [uint64(1), uint64(2), uint64(3), uint64(4)])
other_sk = PrivateKey.from_bytes(test_name)
sum_pk = child_sk.get_g1() + other_sk.get_g1()
signing_instructions = SigningInstructions(
KeyHints(
[SumHint([child_sk.get_g1().get_fingerprint().to_bytes(4, "big")], test_name, bytes(sum_pk))],
[PathHint(root_fingerprint, [uint64(1), uint64(2), uint64(3), uint64(4)])],
),
[SigningTarget(sum_pk.get_fingerprint().to_bytes(4, "big"), test_name, test_name)],
)
for partial_allowed in (True, False):
signing_responses = await wallet.wallet_state_manager.signer.execute_signing_instructions(
signing_instructions, partial_allowed
)
assert signing_responses == [
SigningResponse(
bytes(
AugSchemeMPL.aggregate(
[
AugSchemeMPL.sign(other_sk, test_name, sum_pk),
AugSchemeMPL.sign(child_sk, test_name, sum_pk),
]
)
),
test_name,
),
]
# Test partial signing
test_name = std_hash(b"path and sum hint partial")
test_name_2 = std_hash(test_name)
root_sk_2 = PrivateKey.from_bytes(std_hash(b"a key we do not have"))
child_sk = _derive_path_unhardened(root_sk, [uint64(1), uint64(2), uint64(3), uint64(4)])
child_sk_2 = _derive_path_unhardened(root_sk_2, [uint64(1), uint64(2), uint64(3), uint64(4)])
other_sk = PrivateKey.from_bytes(test_name)
other_sk_2 = PrivateKey.from_bytes(test_name_2)
sum_pk = child_sk.get_g1() + other_sk.get_g1()
sum_pk_2 = child_sk_2.get_g1() + other_sk_2.get_g1()
signing_responses = await wallet.wallet_state_manager.signer.execute_signing_instructions(
SigningInstructions(
KeyHints(
[
SumHint([child_sk.get_g1().get_fingerprint().to_bytes(4, "big")], test_name, bytes(sum_pk)),
SumHint([child_sk_2.get_g1().get_fingerprint().to_bytes(4, "big")], test_name_2, bytes(sum_pk_2)),
],
[
PathHint(root_fingerprint, [uint64(1), uint64(2), uint64(3), uint64(4)]),
PathHint(
root_sk_2.get_g1().get_fingerprint().to_bytes(4, "big"),
[uint64(1), uint64(2), uint64(3), uint64(4)],
),
],
),
[
SigningTarget(sum_pk.get_fingerprint().to_bytes(4, "big"), test_name, test_name),
SigningTarget(sum_pk_2.get_fingerprint().to_bytes(4, "big"), test_name_2, test_name_2),
],
),
partial_allowed=True,
)
assert signing_responses == [
SigningResponse(
bytes(
AugSchemeMPL.sign(child_sk, test_name, sum_pk),
),
test_name,
),
SigningResponse(
bytes(AugSchemeMPL.sign(other_sk, test_name, sum_pk)),
test_name,
),
SigningResponse(bytes(AugSchemeMPL.sign(other_sk_2, test_name_2, sum_pk_2)), test_name_2),
]
# Test errors
unknown_path_hint = SigningInstructions(
KeyHints(
[],
[PathHint(b"unknown fingerprint", [uint64(1), uint64(2), uint64(3), uint64(4)])],
),
[],
)
unknown_sum_hint = SigningInstructions(
KeyHints(
[SumHint([b"unknown fingerprint"], b"", bytes(G1Element()))],
[],
),
[],
)
unknown_target = SigningInstructions(
KeyHints(
[],
[],
),
[SigningTarget(b"unknown fingerprint", b"", std_hash(b"some hook"))],
)
with pytest.raises(ValueError, match="No root pubkey for fingerprint"):
await wallet.wallet_state_manager.signer.execute_signing_instructions(unknown_path_hint)
with pytest.raises(ValueError, match="No pubkey found"):
await wallet.wallet_state_manager.signer.execute_signing_instructions(unknown_sum_hint)
with pytest.raises(ValueError, match="not found"):
await wallet.wallet_state_manager.signer.execute_signing_instructions(unknown_target)
# Test no private key partial sign sum hint
wallet.wallet_state_manager.signer = dataclasses.replace(wallet.wallet_state_manager.signer, root_private_key=None)
test_name = std_hash(b"sum hint partial no private key")
other_sk = PrivateKey.from_bytes(test_name)
sum_pk = other_sk.get_g1() + root_pk
signing_responses = await wallet.wallet_state_manager.signer.execute_signing_instructions(
SigningInstructions(
KeyHints(
[SumHint([root_fingerprint], test_name, bytes(sum_pk))],
[],
),
[SigningTarget(sum_pk.get_fingerprint().to_bytes(4, "big"), test_name, test_name)],
),
partial_allowed=True,
)
assert signing_responses == [SigningResponse(bytes(AugSchemeMPL.sign(other_sk, test_name, sum_pk)), test_name)]
def test_blind_signer_translation_layer() -> None:
sum_hints: list[SumHint] = [
SumHint([b"a", b"b", b"c"], b"offset", b"final"),
SumHint([b"c", b"b", b"a"], b"offset2", b"final"),
]
path_hints: list[PathHint] = [
PathHint(b"root1", [uint64(1), uint64(2), uint64(3)]),
PathHint(b"root2", [uint64(4), uint64(5), uint64(6)]),
]
signing_targets: list[SigningTarget] = [
SigningTarget(b"pubkey", b"message", bytes32.zeros),
SigningTarget(b"pubkey2", b"message2", bytes32([1] * 32)),
]
instructions: SigningInstructions = SigningInstructions(
KeyHints(sum_hints, path_hints),
signing_targets,
)
transaction: UnsignedTransaction = UnsignedTransaction(
TransactionInfo([]),
instructions,
)
signing_response: SigningResponse = SigningResponse(
b"signature",
bytes32([1] * 32),
)
bstl_sum_hints: list[BSTLSumHint] = [
BSTLSumHint([b"a", b"b", b"c"], b"offset", b"final"),
BSTLSumHint([b"c", b"b", b"a"], b"offset2", b"final"),
]
bstl_path_hints: list[BSTLPathHint] = [
BSTLPathHint(b"root1", [uint64(1), uint64(2), uint64(3)]),
BSTLPathHint(b"root2", [uint64(4), uint64(5), uint64(6)]),
]
bstl_signing_targets: list[BSTLSigningTarget] = [
BSTLSigningTarget(b"pubkey", b"message", bytes32.zeros),
BSTLSigningTarget(b"pubkey2", b"message2", bytes32([1] * 32)),
]
bstl_instructions: BSTLSigningInstructions = BSTLSigningInstructions(
bstl_sum_hints,
bstl_path_hints,
bstl_signing_targets,
)
bstl_transaction: BSTLUnsignedTransaction = BSTLUnsignedTransaction(
bstl_sum_hints,
bstl_path_hints,
bstl_signing_targets,
)
bstl_signing_response: BSTLSigningResponse = BSTLSigningResponse(
b"signature",
bytes32([1] * 32),
)
bstl_instructions_json = json_serialize_with_clvm_streamable(bstl_instructions)
bstl_transaction_json = json_serialize_with_clvm_streamable(bstl_transaction)
bstl_signing_response_json = json_serialize_with_clvm_streamable(bstl_signing_response)
assert bstl_instructions_json == json_serialize_with_clvm_streamable(
instructions, translation_layer=BLIND_SIGNER_TRANSLATION
)
assert bstl_transaction_json == json_serialize_with_clvm_streamable(
transaction, translation_layer=BLIND_SIGNER_TRANSLATION
)
assert bstl_signing_response_json == json_serialize_with_clvm_streamable(
signing_response, translation_layer=BLIND_SIGNER_TRANSLATION
)
assert (
json_deserialize_with_clvm_streamable(
bstl_instructions_json, SigningInstructions, translation_layer=BLIND_SIGNER_TRANSLATION
)
== instructions
)
assert (
json_deserialize_with_clvm_streamable(
bstl_transaction_json, UnsignedTransaction, translation_layer=BLIND_SIGNER_TRANSLATION
)
== transaction
)
assert (
json_deserialize_with_clvm_streamable(
bstl_signing_response_json, SigningResponse, translation_layer=BLIND_SIGNER_TRANSLATION
)
== signing_response
)
@pytest.mark.parametrize(
"wallet_environments",
[
{
"num_environments": 1,
"blocks_needed": [1],
"trusted": True,
"reuse_puzhash": True,
}
],
indirect=True,
)
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0])
@pytest.mark.anyio
async def test_signer_commands(wallet_environments: WalletTestFramework) -> None:
wallet: Wallet = wallet_environments.environments[0].xch_wallet
wallet_state_manager: WalletStateManager = wallet_environments.environments[0].wallet_state_manager
wallet_rpc: WalletRpcClient = wallet_environments.environments[0].rpc_client
client_info: WalletClientInfo = WalletClientInfo(
wallet_rpc,
wallet_state_manager.root_pubkey.get_fingerprint(),
wallet_state_manager.config,
)
AMOUNT = uint64(1)
async with wallet_state_manager.new_action_scope(
wallet_environments.tx_config, sign=False, push=False
) as action_scope:
await wallet.generate_signed_transaction([AMOUNT], [bytes32.zeros], action_scope)
[tx] = action_scope.side_effects.transactions
runner = CliRunner()
with runner.isolated_filesystem():
with open("./temp-tb", "wb") as file:
file.write(bytes(TransactionBundle([tx])))
await GatherSigningInfoCMD(
rpc_info=NeedsWalletRPC(client_info=client_info),
sp_out=SPOut(
translation="CHIP-0028",
output_format="file",
output_file=["./temp-si"],
),
txs_in=TransactionsIn(transaction_file_in="./temp-tb"),
).run()
await ExecuteSigningInstructionsCMD(
rpc_info=NeedsWalletRPC(client_info=client_info),
sp_in=SPIn(
translation="CHIP-0028",
signer_protocol_input=["./temp-si"],
),
sp_out=SPOut(
translation="CHIP-0028",
output_format="file",
output_file=["./temp-sr"],
),
).run()
await ApplySignaturesCMD(
rpc_info=NeedsWalletRPC(client_info=client_info),
txs_in=TransactionsIn(transaction_file_in="./temp-tb"),
sp_in=SPIn(
translation="CHIP-0028",
signer_protocol_input=["./temp-sr"],
),
txs_out=TransactionsOut(transaction_file_out="./temp-stb"),
).run()
await PushTransactionsCMD(
rpc_info=NeedsWalletRPC(client_info=client_info),
txs_in=TransactionsIn(transaction_file_in="./temp-stb"),
).run()
await wallet_environments.process_pending_states(
[
WalletStateTransition(
pre_block_balance_updates={
1: {
"unconfirmed_wallet_balance": -1 * AMOUNT,
"<=#spendable_balance": -1 * AMOUNT,
"<=#max_send_amount": -1 * AMOUNT,
"pending_change": sum(c.amount for c in tx.removals) - AMOUNT,
"pending_coin_removal_count": 1,
}
},
post_block_balance_updates={
1: {
"confirmed_wallet_balance": -1 * AMOUNT,
"pending_change": -1 * (sum(c.amount for c in tx.removals) - AMOUNT),
"pending_coin_removal_count": -1,
"set_remainder": True,
},
},
),
]
)
@pytest.mark.filterwarnings("ignore:The parameter .* is used more than once:UserWarning")
def test_signer_command_default_parsing() -> None:
check_click_parsing(
GatherSigningInfoCMD(
rpc_info=NeedsWalletRPC(client_info=None, wallet_rpc_port=None, fingerprint=None),
sp_out=SPOut(
translation="none",
output_format="hex",
output_file=tuple(),
),
txs_in=TransactionsIn(transaction_file_in="in"),
),
"--transaction-file-in",
"in",
)
check_click_parsing(
ExecuteSigningInstructionsCMD(
rpc_info=NeedsWalletRPC(client_info=None, wallet_rpc_port=None, fingerprint=None),
sp_in=SPIn(
translation="none",
signer_protocol_input=("sp-in",),
),
sp_out=SPOut(
translation="none",
output_format="hex",
output_file=tuple(),
),
),
"-p",
"sp-in",
)
check_click_parsing(
ApplySignaturesCMD(
rpc_info=NeedsWalletRPC(client_info=None, wallet_rpc_port=None, fingerprint=None),
txs_in=TransactionsIn(transaction_file_in="in"),
sp_in=SPIn(
translation="none",
signer_protocol_input=("sp-in",),
),
txs_out=TransactionsOut(transaction_file_out="out"),
),
"--transaction-file-in",
"in",
"--transaction-file-out",
"out",
"-p",
"sp-in",
)
check_click_parsing(
PushTransactionsCMD(
rpc_info=NeedsWalletRPC(client_info=None, wallet_rpc_port=None, fingerprint=None),
txs_in=TransactionsIn(transaction_file_in="in"),
),
"--transaction-file-in",
"in",
)
def test_transactions_in() -> None:
@click.group()
def cmd() -> None:
pass
@chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a")
class TempCMD(TransactionsIn):
def run(self) -> None:
assert self.transaction_bundle == TransactionBundle([STD_TX])
runner = CliRunner()
with runner.isolated_filesystem():
with open("some file", "wb") as file:
file.write(bytes(TransactionBundle([STD_TX])))
result = runner.invoke(cmd, ["temp_cmd", "--transaction-file-in", "some file"], catch_exceptions=False)
assert result.output == ""
def test_transactions_out() -> None:
@click.group()
def cmd() -> None:
pass
@chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a")
class TempCMD(TransactionsOut):
def run(self) -> None:
self.handle_transaction_output([STD_TX])
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(cmd, ["temp_cmd", "--transaction-file-out", "some file"], catch_exceptions=False)
assert result.output == ""
with open("some file", "rb") as file:
file.read() == bytes(TransactionBundle([STD_TX]))
@clvm_streamable
@dataclasses.dataclass(frozen=True)
class FooCoin(Streamable):
amount: uint64
@staticmethod
def from_wallet_api(_from: Coin) -> FooCoin:
return FooCoin(_from.amount)
@staticmethod
def to_wallet_api(_from: FooCoin) -> Coin:
return Coin(
bytes32.zeros,
bytes32.zeros,
_from.amount,
)
FOO_COIN_TRANSLATION = TranslationLayer(
[
TranslationLayerMapping(
Coin,
FooCoin,
FooCoin.from_wallet_api,
FooCoin.to_wallet_api,
)
]
)
def test_signer_protocol_in(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setitem(ALL_TRANSLATION_LAYERS, "CHIP-0028", FOO_COIN_TRANSLATION)
@click.group()
def cmd() -> None:
pass
coin = Coin(bytes32.zeros, bytes32.zeros, uint64(13))
@chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a")
class TempCMD(SPIn):
def run(self) -> None:
assert self.read_sp_input(Coin) == [coin, coin]
runner = CliRunner()
with runner.isolated_filesystem():
with open("some file", "wb") as file:
file.write(byte_serialize_clvm_streamable(coin))
with open("some file2", "wb") as file:
file.write(byte_serialize_clvm_streamable(coin))
result = runner.invoke(
cmd,
["temp_cmd", "--signer-protocol-input", "some file", "--signer-protocol-input", "some file2"],
catch_exceptions=False,
)
assert result.output == ""
with runner.isolated_filesystem():
with open("some file", "wb") as file:
file.write(byte_serialize_clvm_streamable(coin, translation_layer=FOO_COIN_TRANSLATION))
with open("some file2", "wb") as file2:
file2.write(byte_serialize_clvm_streamable(coin, translation_layer=FOO_COIN_TRANSLATION))
result = runner.invoke(
cmd, ["temp_cmd", "--signer-protocol-input", "some file", "--signer-protocol-input", "some file2"]
)
assert result.exception is not None
result = runner.invoke(
cmd,
[
"temp_cmd",
"--signer-protocol-input",
"some file",
"--signer-protocol-input",
"some file2",
"--translation",
"CHIP-0028",
],
catch_exceptions=False,
)
assert result.output == ""
def test_signer_protocol_out(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setitem(ALL_TRANSLATION_LAYERS, "CHIP-0028", FOO_COIN_TRANSLATION)
@click.group()
def cmd() -> None:
pass
coin = Coin(bytes32.zeros, bytes32.zeros, uint64(0))
coin_bytes = byte_serialize_clvm_streamable(coin)
@chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a")
class TempCMD(SPOut):
def run(self) -> None:
self.handle_clvm_output([coin, coin])
runner = CliRunner()
with runner.isolated_filesystem():
result = runner.invoke(cmd, ["temp_cmd", "--output-format", "hex"], catch_exceptions=False)
assert result.output.strip() == coin_bytes.hex() + "\n" + coin_bytes.hex()
result = runner.invoke(cmd, ["temp_cmd", "--output-format", "file"], catch_exceptions=False)
assert result.output == "--output-format=file specified without any --output-file\n"
result = runner.invoke(
cmd, ["temp_cmd", "--output-format", "file", "--output-file", "some file"], catch_exceptions=False
)
assert "Incorrect number of file outputs specified" in result.output
result = runner.invoke(
cmd,
["temp_cmd", "--output-format", "file", "--output-file", "some file", "--output-file", "some file2"],
catch_exceptions=False,
)
assert result.output == ""
with open("some file", "rb") as file:
file.read() == coin_bytes
with open("some file2", "rb") as file:
file.read() == coin_bytes
result = runner.invoke(cmd, ["temp_cmd", "--output-format", "qr"], catch_exceptions=False)
assert result.output != "" # separate test for QrCodeDisplay
result = runner.invoke(
cmd, ["temp_cmd", "--output-format", "hex", "--translation", "CHIP-0028"], catch_exceptions=False
)
assert result.output.strip() != coin_bytes.hex()
coin_hex = byte_serialize_clvm_streamable(coin, translation_layer=ALL_TRANSLATION_LAYERS["CHIP-0028"]).hex()
assert result.output.strip() == coin_hex + "\n" + coin_hex
def test_qr_code_display() -> None:
@click.group()
def cmd() -> None:
pass
bytes_to_encode = b"foo bar qat qux bam bat"
@chia_command(group=cmd, name="temp_cmd", short_help="blah", help="n/a")
class TempCMD(QrCodeDisplay):
def run(self) -> None:
self.display_qr_codes([bytes_to_encode, bytes_to_encode])
runner = CliRunner()
result = runner.invoke(
cmd,
["temp_cmd"],
input="\n",
catch_exceptions=False,
)
# Would be good to check eventually that the QR codes are valid but segno doesn't seem to provide that ATM
assert result.output.count("Displaying QR Codes (1/2)") == 1
assert result.output.count("Displaying QR Codes (2/2)") == 1