[LABS-488] Extract signing functionality to its own module (#21159)

* [LABS-480] Remove interested_ph_cache and initialize coin cache in init

* [LABS-481] Remove unnecessary `server` field from WSM

* [LABS-486] Remove useless `add_transaction` from WSM

* [LABS-488] Extract signing functionality to its own module

* Test coverage
This commit is contained in:
Matt Hauff
2026-08-04 11:07:48 -07:00
committed by GitHub
parent 6281e1566f
commit 402236dfdb
12 changed files with 469 additions and 438 deletions
@@ -1454,7 +1454,7 @@ async def test_cat_change_detection(wallet_environments: WalletTestFramework, wa
construct_cat_puzzle(CAT_MOD, Program.NIL.get_tree_hash(), our_puzzle).get_tree_hash(), construct_cat_puzzle(CAT_MOD, Program.NIL.get_tree_hash(), our_puzzle).get_tree_hash(),
cat_amount_0, cat_amount_0,
) )
eve_spend, _ = await wsm.sign_bundle( eve_spend, _ = await wsm.signer.sign_bundle(
[ [
make_spend( make_spend(
cat_coin, cat_coin,
@@ -1670,7 +1670,7 @@ async def test_cat_melt_balance(wallet_environments: WalletTestFramework) -> Non
) )
], ],
) )
signed_spend, _ = await env.wallet_state_manager.sign_bundle(new_spend.coin_spends) signed_spend, _ = await env.wallet_state_manager.signer.sign_bundle(new_spend.coin_spends)
await env.rpc_client.push_tx(PushTX(spend_bundle=signed_spend)) await env.rpc_client.push_tx(PushTX(spend_bundle=signed_spend))
await time_out_assert(10, simulator.tx_id_in_mempool, True, signed_spend.name()) await time_out_assert(10, simulator.tx_id_in_mempool, True, signed_spend.name())
+14 -12
View File
@@ -514,7 +514,7 @@ async def test_cat_trades(
assert trade_make is not None assert trade_make is not None
peer = wallet_node_taker.get_full_node_peer() peer = wallet_node_taker.get_full_node_peer()
[maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
@@ -731,7 +731,7 @@ async def test_cat_trades(
assert success is True assert success is True
assert trade_make is not None assert trade_make is not None
[maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
@@ -860,7 +860,7 @@ async def test_cat_trades(
assert error is None assert error is None
assert success is True assert success is True
assert trade_make is not None assert trade_make is not None
[maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
@@ -1061,7 +1061,7 @@ async def test_cat_trades(
assert success is True assert success is True
assert trade_make is not None assert trade_make is not None
[maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
@@ -1318,7 +1318,7 @@ async def test_cat_trades(
assert error is None assert error is None
assert success is True assert success is True
assert trade_make is not None assert trade_make is not None
[maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
@@ -1451,7 +1451,7 @@ async def test_cat_trades(
assert success is True assert success is True
assert trade_make is not None assert trade_make is not None
[maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
@@ -1733,7 +1733,7 @@ async def test_trade_cancellation(wallet_environments: WalletTestFramework, wall
# Due to current mempool rules, trying to force a take out of the mempool with a cancel will not work. # Due to current mempool rules, trying to force a take out of the mempool with a cancel will not work.
# Uncomment this when/if it does # Uncomment this when/if it does
# [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( # [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.signer.sign_offers(
# [Offer.from_bytes(trade_make.offer)] # [Offer.from_bytes(trade_make.offer)]
# ) # )
# trade_take = await trade_manager_taker.respond_to_offer( # trade_take = await trade_manager_taker.respond_to_offer(
@@ -2037,7 +2037,7 @@ async def test_trade_conflict(wallet_environments: WalletTestFramework, wallet_t
assert trade_make is not None assert trade_make is not None
peer = env_taker.node.get_full_node_peer() peer = env_taker.node.get_full_node_peer()
offer = Offer.from_bytes(trade_make.offer) offer = Offer.from_bytes(trade_make.offer)
[offer], signing_response = await env_maker.wallet_state_manager.sign_offers([offer]) [offer], signing_response = await env_maker.wallet_state_manager.signer.sign_offers([offer])
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
wallet_environments.tx_config, push=True, additional_signing_responses=signing_response wallet_environments.tx_config, push=True, additional_signing_responses=signing_response
) as action_scope: ) as action_scope:
@@ -2055,7 +2055,7 @@ async def test_trade_conflict(wallet_environments: WalletTestFramework, wallet_t
await trade_manager_taker.respond_to_offer(offer, peer, action_scope, fee=fee) await trade_manager_taker.respond_to_offer(offer, peer, action_scope, fee=fee)
await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CONFIRM, trade_manager_taker, tr1) await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CONFIRM, trade_manager_taker, tr1)
# pushing into mempool while already in it should fail # pushing into mempool while already in it should fail
[offer], signing_response = await env_maker.wallet_state_manager.sign_offers([offer]) [offer], signing_response = await env_maker.wallet_state_manager.signer.sign_offers([offer])
async with trade_manager_trader.wallet_state_manager.new_action_scope( async with trade_manager_trader.wallet_state_manager.new_action_scope(
wallet_environments.tx_config, push=True, additional_signing_responses=signing_response wallet_environments.tx_config, push=True, additional_signing_responses=signing_response
) as action_scope: ) as action_scope:
@@ -2352,7 +2352,9 @@ async def test_trade_high_fee(wallet_environments: WalletTestFramework, wallet_t
assert success is True assert success is True
assert trade_make is not None assert trade_make is not None
peer = env_taker.node.get_full_node_peer() peer = env_taker.node.get_full_node_peer()
[offer], signing_response = await env_maker.wallet_state_manager.sign_offers([Offer.from_bytes(trade_make.offer)]) [offer], signing_response = await env_maker.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)]
)
fee = uint64(1_000_000_000_000) fee = uint64(1_000_000_000_000)
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
wallet_environments.tx_config, push=True, additional_signing_responses=signing_response wallet_environments.tx_config, push=True, additional_signing_responses=signing_response
@@ -2515,10 +2517,10 @@ async def test_aggregated_trade_state(wallet_environments: WalletTestFramework,
assert success is True assert success is True
assert trade_make_2 is not None assert trade_make_2 is not None
[offer_1], signing_response_1 = await env_maker.node.wallet_state_manager.sign_offers( [offer_1], signing_response_1 = await env_maker.node.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make_1.offer)] [Offer.from_bytes(trade_make_1.offer)]
) )
[offer_2], signing_response_2 = await env_maker.node.wallet_state_manager.sign_offers( [offer_2], signing_response_2 = await env_maker.node.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make_2.offer)] [Offer.from_bytes(trade_make_2.offer)]
) )
agg_offer = Offer.aggregate([offer_1, offer_2]) agg_offer = Offer.aggregate([offer_1, offer_2])
@@ -195,7 +195,7 @@ async def test_dl_offers(wallet_environments: WalletTestFramework) -> None:
] ]
) )
[_maker_offer], signing_response = await wsm_maker.sign_offers([Offer.from_bytes(offer_maker.offer)]) [_maker_offer], signing_response = await wsm_maker.signer.sign_offers([Offer.from_bytes(offer_maker.offer)])
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
wallet_environments.tx_config, push=True, additional_signing_responses=signing_response wallet_environments.tx_config, push=True, additional_signing_responses=signing_response
) as action_scope: ) as action_scope:
@@ -750,7 +750,7 @@ async def test_multiple_dl_offers(wallet_environments: WalletTestFramework) -> N
assert success is True assert success is True
assert offer_maker is not None assert offer_maker is not None
[_maker_offer], signing_response = await wsm_maker.sign_offers([Offer.from_bytes(offer_maker.offer)]) [_maker_offer], signing_response = await wsm_maker.signer.sign_offers([Offer.from_bytes(offer_maker.offer)])
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
wallet_environments.tx_config, push=True, additional_signing_responses=signing_response wallet_environments.tx_config, push=True, additional_signing_responses=signing_response
) as action_scope: ) as action_scope:
@@ -178,7 +178,7 @@ async def test_nft_offer_sell_nft(wallet_environments: WalletTestFramework, zero
taker_fee = 1 taker_fee = 1
peer = env_taker.node.get_full_node_peer() peer = env_taker.node.get_full_node_peer()
[_maker_offer], signing_response = await env_maker.wallet_state_manager.sign_offers( [_maker_offer], signing_response = await env_maker.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
@@ -410,7 +410,7 @@ async def test_nft_offer_request_nft(wallet_environments: WalletTestFramework, z
taker_fee = 1 taker_fee = 1
peer = env_taker.node.get_full_node_peer() peer = env_taker.node.get_full_node_peer()
[_maker_offer], signing_response = await env_maker.wallet_state_manager.sign_offers( [_maker_offer], signing_response = await env_maker.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
@@ -700,7 +700,7 @@ async def test_nft_offer_sell_did_to_did(wallet_environments: WalletTestFramewor
taker_fee = 1 taker_fee = 1
peer = env_taker.node.get_full_node_peer() peer = env_taker.node.get_full_node_peer()
[_maker_offer], signing_response = await env_maker.wallet_state_manager.sign_offers( [_maker_offer], signing_response = await env_maker.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
@@ -1006,7 +1006,7 @@ async def test_nft_offer_sell_nft_for_cat(
taker_fee = 1 taker_fee = 1
peer = env_taker.node.get_full_node_peer() peer = env_taker.node.get_full_node_peer()
[_maker_offer], signing_response = await env_maker.wallet_state_manager.sign_offers( [_maker_offer], signing_response = await env_maker.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
@@ -1362,7 +1362,7 @@ async def test_nft_offer_request_nft_for_cat(
taker_fee = 1 taker_fee = 1
peer = env_taker.node.get_full_node_peer() peer = env_taker.node.get_full_node_peer()
[_maker_offer], signing_response = await env_maker.wallet_state_manager.sign_offers( [_maker_offer], signing_response = await env_maker.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
@@ -2030,7 +2030,7 @@ async def test_complex_nft_offer(
assert success assert success
assert trade_make is not None assert trade_make is not None
[maker_offer], signing_response = await wsm_maker.sign_offers([Offer.from_bytes(trade_make.offer)]) [maker_offer], signing_response = await wsm_maker.signer.sign_offers([Offer.from_bytes(trade_make.offer)])
if royalty_basis_pts_maker == 10000: if royalty_basis_pts_maker == 10000:
with pytest.raises(ValueError): with pytest.raises(ValueError):
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
@@ -2239,7 +2239,7 @@ async def test_complex_nft_offer(
assert success assert success
assert trade_make is not None assert trade_make is not None
[maker_offer], signing_response = await wsm_maker.sign_offers([Offer.from_bytes(trade_make.offer)]) [maker_offer], signing_response = await wsm_maker.signer.sign_offers([Offer.from_bytes(trade_make.offer)])
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
wallet_environments.tx_config, push=True, additional_signing_responses=signing_response wallet_environments.tx_config, push=True, additional_signing_responses=signing_response
) as action_scope: ) as action_scope:
@@ -136,7 +136,7 @@ async def test_nft_offer_with_fee(wallet_environments: WalletTestFramework) -> N
taker_fee = uint64(1) taker_fee = uint64(1)
[_maker_offer], signing_response = await env_0.wallet_state_manager.sign_offers( [_maker_offer], signing_response = await env_0.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
peer = env_1.node.get_full_node_peer() peer = env_1.node.get_full_node_peer()
@@ -235,7 +235,7 @@ async def test_nft_offer_with_fee(wallet_environments: WalletTestFramework) -> N
taker_fee = uint64(1) taker_fee = uint64(1)
[_maker_offer], signing_response = await env_0.wallet_state_manager.sign_offers( [_maker_offer], signing_response = await env_0.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
@@ -622,7 +622,7 @@ async def test_nft_offer_with_metadata_update(wallet_environments: WalletTestFra
taker_fee = uint64(1) taker_fee = uint64(1)
[_maker_offer], signing_response = await env_0.wallet_state_manager.sign_offers( [_maker_offer], signing_response = await env_0.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
peer = env_1.node.get_full_node_peer() peer = env_1.node.get_full_node_peer()
@@ -850,7 +850,7 @@ async def test_nft_offer_nft_for_cat(wallet_environments: WalletTestFramework, w
taker_fee = uint64(1) taker_fee = uint64(1)
[_maker_offer], signing_response = await env_0.wallet_state_manager.sign_offers( [_maker_offer], signing_response = await env_0.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
peer = env_1.node.get_full_node_peer() peer = env_1.node.get_full_node_peer()
@@ -980,7 +980,7 @@ async def test_nft_offer_nft_for_cat(wallet_environments: WalletTestFramework, w
taker_fee = uint64(1) taker_fee = uint64(1)
[_maker_offer], signing_response = await env_0.wallet_state_manager.sign_offers( [_maker_offer], signing_response = await env_0.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
async with trade_manager_taker.wallet_state_manager.new_action_scope( async with trade_manager_taker.wallet_state_manager.new_action_scope(
@@ -1259,7 +1259,7 @@ async def test_nft_offer_nft_for_nft(wallet_environments: WalletTestFramework) -
taker_fee = uint64(1) taker_fee = uint64(1)
[_maker_offer], signing_response = await env_0.wallet_state_manager.sign_offers( [_maker_offer], signing_response = await env_0.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)] [Offer.from_bytes(trade_make.offer)]
) )
peer = env_1.node.get_full_node_peer() peer = env_1.node.get_full_node_peer()
@@ -1490,7 +1490,9 @@ async def test_nft_offer_nft0_and_xch_for_cat(
assert error is None assert error is None
assert trade_make is not None assert trade_make is not None
[maker_offer], signing_response = await env_0.wallet_state_manager.sign_offers([Offer.from_bytes(trade_make.offer)]) [maker_offer], signing_response = await env_0.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)]
)
taker_fee = uint64(1) taker_fee = uint64(1)
@@ -1619,7 +1621,9 @@ async def test_nft_offer_nft0_and_xch_for_cat(
assert error is None assert error is None
assert trade_make is not None assert trade_make is not None
[maker_offer], signing_response = await env_0.wallet_state_manager.sign_offers([Offer.from_bytes(trade_make.offer)]) [maker_offer], signing_response = await env_0.wallet_state_manager.signer.sign_offers(
[Offer.from_bytes(trade_make.offer)]
)
taker_fee = uint64(1) taker_fee = uint64(1)
+31 -21
View File
@@ -22,10 +22,9 @@ from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
puzzle_hash_for_synthetic_public_key, puzzle_hash_for_synthetic_public_key,
) )
from chia.wallet.util.wallet_types import WalletType from chia.wallet.util.wallet_types import WalletType
from chia.wallet.wallet import Wallet
from chia.wallet.wallet_puzzle_store import WalletPuzzleStore from chia.wallet.wallet_puzzle_store import WalletPuzzleStore
from chia.wallet.wallet_state_manager import WalletStateManager from chia.wallet.wallet_signer import WalletSigner
from chia.wallet.wallet_user_store import WalletUserStore from chia.wallet.wallet_spend_bundle import WalletSpendBundle
top_sk: PrivateKey = PrivateKey.from_bytes(bytes([1] * 32)) top_sk: PrivateKey = PrivateKey.from_bytes(bytes([1] * 32))
sk1_h: PrivateKey = master_sk_to_wallet_sk(top_sk, uint32(1)) sk1_h: PrivateKey = master_sk_to_wallet_sk(top_sk, uint32(1))
@@ -65,28 +64,39 @@ spend_u: CoinSpend = make_spend(
) )
async def _noop_push(_bundle: WalletSpendBundle) -> None: ...
@pytest.mark.anyio @pytest.mark.anyio
async def test_wsm_sign_transaction() -> None: async def test_wallet_signer() -> None:
async with manage_connection("file:temp.db?mode=memory&cache=shared", uri=True, name="writer") as writer_conn: async with manage_connection("file:temp.db?mode=memory&cache=shared", uri=True, name="writer") as writer_conn:
async with manage_connection("file:temp.db?mode=memory&cache=shared", uri=True, name="reader") as reader_conn: async with manage_connection("file:temp.db?mode=memory&cache=shared", uri=True, name="reader") as reader_conn:
wsm = WalletStateManager()
db = DBWrapper2(writer_conn) db = DBWrapper2(writer_conn)
await db.add_connection(reader_conn) await db.add_connection(reader_conn)
wsm.puzzle_store = await WalletPuzzleStore.create(db) with pytest.raises(ValueError, match=re.escape("root_private_key does not match self.root_pubkey")):
wsm.constants = DEFAULT_CONSTANTS WalletSigner(
wsm.private_key = top_sk root_pubkey=G1Element(),
wsm.root_pubkey = top_sk.get_g1() root_private_key=top_sk,
wsm.user_store = await WalletUserStore.create(db) puzzle_store=await WalletPuzzleStore.create(db),
wallet_info = await wsm.user_store.get_wallet_by_id(1) max_block_cost_clvm=DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM,
assert wallet_info is not None agg_sig_me_additional_data=DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA,
wsm.main_wallet = await Wallet.create(wsm, wallet_info) spend_bundle_push=_noop_push,
)
signer = WalletSigner(
root_pubkey=top_sk.get_g1(),
root_private_key=top_sk,
puzzle_store=await WalletPuzzleStore.create(db),
max_block_cost_clvm=DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM,
agg_sig_me_additional_data=DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA,
spend_bundle_push=_noop_push,
)
with pytest.raises( with pytest.raises(
ValueError, match=re.escape(f"Pubkey {pk1_h.get_fingerprint()} not found (or path/sum hinted to)") ValueError, match=re.escape(f"Pubkey {pk1_h.get_fingerprint()} not found (or path/sum hinted to)")
): ):
await wsm.sign_bundle([spend_h]) await signer.sign_bundle([spend_h])
await wsm.puzzle_store.add_derivation_paths( await signer.puzzle_store.add_derivation_paths(
[ [
DerivationRecord( DerivationRecord(
uint32(1), uint32(1),
@@ -99,7 +109,7 @@ async def test_wsm_sign_transaction() -> None:
] ]
) )
await wsm.puzzle_store.add_derivation_paths( await signer.puzzle_store.add_derivation_paths(
[ [
DerivationRecord( DerivationRecord(
uint32(2), uint32(2),
@@ -112,7 +122,7 @@ async def test_wsm_sign_transaction() -> None:
] ]
) )
signature: G2Element = ((await wsm.sign_bundle([spend_h]))[0]).aggregated_signature signature: G2Element = ((await signer.sign_bundle([spend_h]))[0]).aggregated_signature
assert signature == AugSchemeMPL.aggregate( assert signature == AugSchemeMPL.aggregate(
[ [
AugSchemeMPL.sign(sk1_h, msg1), AugSchemeMPL.sign(sk1_h, msg1),
@@ -123,9 +133,9 @@ async def test_wsm_sign_transaction() -> None:
with pytest.raises( with pytest.raises(
ValueError, match=re.escape(f"Pubkey {pk1_u.get_fingerprint()} not found (or path/sum hinted to)") ValueError, match=re.escape(f"Pubkey {pk1_u.get_fingerprint()} not found (or path/sum hinted to)")
): ):
await wsm.sign_bundle([spend_u]) await signer.sign_bundle([spend_u])
await wsm.puzzle_store.add_derivation_paths( await signer.puzzle_store.add_derivation_paths(
[ [
DerivationRecord( DerivationRecord(
uint32(1), uint32(1),
@@ -138,7 +148,7 @@ async def test_wsm_sign_transaction() -> None:
] ]
) )
await wsm.puzzle_store.add_derivation_paths( await signer.puzzle_store.add_derivation_paths(
[ [
DerivationRecord( DerivationRecord(
uint32(2), uint32(2),
@@ -150,7 +160,7 @@ async def test_wsm_sign_transaction() -> None:
) )
] ]
) )
signature2: G2Element = ((await wsm.sign_bundle([spend_u]))[0]).aggregated_signature signature2: G2Element = ((await signer.sign_bundle([spend_u]))[0]).aggregated_signature
assert signature2 == AugSchemeMPL.aggregate( assert signature2 == AugSchemeMPL.aggregate(
[ [
AugSchemeMPL.sign(sk1_u, msg1), AugSchemeMPL.sign(sk1_u, msg1),
+20 -14
View File
@@ -221,9 +221,9 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram
assert not_our_utx.signing_instructions.targets[1].message == bytes(not_our_message) assert not_our_utx.signing_instructions.targets[1].message == bytes(not_our_message)
not_our_signing_instructions: SigningInstructions = not_our_utx.signing_instructions not_our_signing_instructions: SigningInstructions = not_our_utx.signing_instructions
with pytest.raises(ValueError, match=r"not found \(or path/sum hinted to\)"): with pytest.raises(ValueError, match=r"not found \(or path/sum hinted to\)"):
await wallet_state_manager.execute_signing_instructions(not_our_signing_instructions) 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"): with pytest.raises(ValueError, match=r"No pubkey found \(or path hinted to\) for fingerprint"):
await wallet_state_manager.execute_signing_instructions( await wallet_state_manager.signer.execute_signing_instructions(
dataclasses.replace( dataclasses.replace(
not_our_signing_instructions, not_our_signing_instructions,
key_hints=dataclasses.replace( key_hints=dataclasses.replace(
@@ -236,7 +236,7 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram
) )
) )
with pytest.raises(ValueError, match="No root pubkey for fingerprint"): with pytest.raises(ValueError, match="No root pubkey for fingerprint"):
await wallet_state_manager.execute_signing_instructions( await wallet_state_manager.signer.execute_signing_instructions(
dataclasses.replace( dataclasses.replace(
not_our_signing_instructions, not_our_signing_instructions,
key_hints=dataclasses.replace( key_hints=dataclasses.replace(
@@ -248,7 +248,7 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram
), ),
) )
) )
signing_responses_2 = await wallet_state_manager.execute_signing_instructions( signing_responses_2 = await wallet_state_manager.signer.execute_signing_instructions(
not_our_signing_instructions, partial_allowed=True not_our_signing_instructions, partial_allowed=True
) )
assert len(signing_responses_2) == 2 assert len(signing_responses_2) == 2
@@ -324,7 +324,7 @@ async def test_p2blsdohp_execute_signing_instructions(wallet_environments: Walle
# Test just a path hint # Test just a path hint
test_name: bytes32 = std_hash(b"path hint only") 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)]) child_sk: PrivateKey = _derive_path_unhardened(root_sk, [uint64(1), uint64(2), uint64(3), uint64(4)])
signing_responses: list[SigningResponse] = await wallet.execute_signing_instructions( signing_responses: list[SigningResponse] = await wallet.wallet_state_manager.signer.execute_signing_instructions(
SigningInstructions( SigningInstructions(
KeyHints( KeyHints(
[], [],
@@ -347,7 +347,9 @@ async def test_p2blsdohp_execute_signing_instructions(wallet_environments: Walle
[SigningTarget(sum_pk.get_fingerprint().to_bytes(4, "big"), test_name, test_name)], [SigningTarget(sum_pk.get_fingerprint().to_bytes(4, "big"), test_name, test_name)],
) )
for partial_allowed in (True, False): for partial_allowed in (True, False):
signing_responses = await wallet.execute_signing_instructions(signing_instructions, partial_allowed) signing_responses = await wallet.wallet_state_manager.signer.execute_signing_instructions(
signing_instructions, partial_allowed
)
assert signing_responses == [ assert signing_responses == [
SigningResponse( SigningResponse(
bytes( bytes(
@@ -369,7 +371,9 @@ async def test_p2blsdohp_execute_signing_instructions(wallet_environments: Walle
SigningTarget(b"random fingerprint", test_name, test_name), SigningTarget(b"random fingerprint", test_name, test_name),
], ],
) )
signing_responses = await wallet.execute_signing_instructions(signing_instructions, partial_allowed=True) signing_responses = await wallet.wallet_state_manager.signer.execute_signing_instructions(
signing_instructions, partial_allowed=True
)
assert signing_responses == [ assert signing_responses == [
SigningResponse( SigningResponse(
bytes( bytes(
@@ -398,7 +402,9 @@ async def test_p2blsdohp_execute_signing_instructions(wallet_environments: Walle
[SigningTarget(sum_pk.get_fingerprint().to_bytes(4, "big"), test_name, test_name)], [SigningTarget(sum_pk.get_fingerprint().to_bytes(4, "big"), test_name, test_name)],
) )
for partial_allowed in (True, False): for partial_allowed in (True, False):
signing_responses = await wallet.execute_signing_instructions(signing_instructions, partial_allowed) signing_responses = await wallet.wallet_state_manager.signer.execute_signing_instructions(
signing_instructions, partial_allowed
)
assert signing_responses == [ assert signing_responses == [
SigningResponse( SigningResponse(
bytes( bytes(
@@ -423,7 +429,7 @@ async def test_p2blsdohp_execute_signing_instructions(wallet_environments: Walle
other_sk_2 = PrivateKey.from_bytes(test_name_2) other_sk_2 = PrivateKey.from_bytes(test_name_2)
sum_pk = child_sk.get_g1() + other_sk.get_g1() sum_pk = child_sk.get_g1() + other_sk.get_g1()
sum_pk_2 = child_sk_2.get_g1() + other_sk_2.get_g1() sum_pk_2 = child_sk_2.get_g1() + other_sk_2.get_g1()
signing_responses = await wallet.execute_signing_instructions( signing_responses = await wallet.wallet_state_manager.signer.execute_signing_instructions(
SigningInstructions( SigningInstructions(
KeyHints( KeyHints(
[ [
@@ -482,18 +488,18 @@ async def test_p2blsdohp_execute_signing_instructions(wallet_environments: Walle
[SigningTarget(b"unknown fingerprint", b"", std_hash(b"some hook"))], [SigningTarget(b"unknown fingerprint", b"", std_hash(b"some hook"))],
) )
with pytest.raises(ValueError, match="No root pubkey for fingerprint"): with pytest.raises(ValueError, match="No root pubkey for fingerprint"):
await wallet.execute_signing_instructions(unknown_path_hint) await wallet.wallet_state_manager.signer.execute_signing_instructions(unknown_path_hint)
with pytest.raises(ValueError, match="No pubkey found"): with pytest.raises(ValueError, match="No pubkey found"):
await wallet.execute_signing_instructions(unknown_sum_hint) await wallet.wallet_state_manager.signer.execute_signing_instructions(unknown_sum_hint)
with pytest.raises(ValueError, match="not found"): with pytest.raises(ValueError, match="not found"):
await wallet.execute_signing_instructions(unknown_target) await wallet.wallet_state_manager.signer.execute_signing_instructions(unknown_target)
# Test no private key partial sign sum hint # Test no private key partial sign sum hint
wallet.wallet_state_manager.private_key = None 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") test_name = std_hash(b"sum hint partial no private key")
other_sk = PrivateKey.from_bytes(test_name) other_sk = PrivateKey.from_bytes(test_name)
sum_pk = other_sk.get_g1() + root_pk sum_pk = other_sk.get_g1() + root_pk
signing_responses = await wallet.execute_signing_instructions( signing_responses = await wallet.wallet_state_manager.signer.execute_signing_instructions(
SigningInstructions( SigningInstructions(
KeyHints( KeyHints(
[SumHint([root_fingerprint], test_name, bytes(sum_pk))], [SumHint([root_fingerprint], test_name, bytes(sum_pk))],
+1 -1
View File
@@ -1871,7 +1871,7 @@ class TestWalletSimulator:
assert stolen_cs is not None assert stolen_cs is not None
# get a legit signature # get a legit signature
stolen_sb, _ = await wallet.wallet_state_manager.sign_bundle([stolen_cs]) stolen_sb, _ = await wallet.wallet_state_manager.signer.sign_bundle([stolen_cs])
stolen_tx = wallet.wallet_state_manager.new_outgoing_transaction( stolen_tx = wallet.wallet_state_manager.new_outgoing_transaction(
wallet_id=wallet.id(), wallet_id=wallet.id(),
puzzle_hash=bytes32.zeros, puzzle_hash=bytes32.zeros,
+2 -200
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING, Any, ClassVar, cast from typing import TYPE_CHECKING, Any, ClassVar, cast
from chia_rs import AugSchemeMPL, CoinSpend, G1Element, G2Element, PrivateKey from chia_rs import CoinSpend, G1Element, G2Element, PrivateKey
from chia_rs.sized_bytes import bytes32 from chia_rs.sized_bytes import bytes32
from chia_rs.sized_ints import uint32, uint64, uint128 from chia_rs.sized_ints import uint32, uint64, uint128
from typing_extensions import Unpack from typing_extensions import Unpack
@@ -20,34 +20,16 @@ from chia.wallet.conditions import (
CreateCoin, CreateCoin,
CreateCoinAnnouncement, CreateCoinAnnouncement,
) )
from chia.wallet.derivation_record import DerivationRecord from chia.wallet.derive_keys import _derive_path
from chia.wallet.derive_keys import (
MAX_POOL_WALLETS,
_derive_path,
_derive_path_unhardened,
master_sk_to_singleton_owner_sk,
)
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
DEFAULT_HIDDEN_PUZZLE_HASH, DEFAULT_HIDDEN_PUZZLE_HASH,
calculate_synthetic_offset,
calculate_synthetic_public_key, calculate_synthetic_public_key,
calculate_synthetic_secret_key, calculate_synthetic_secret_key,
puzzle_for_pk, puzzle_for_pk,
puzzle_hash_for_pk, puzzle_hash_for_pk,
puzzle_hash_for_synthetic_public_key,
solution_for_conditions, solution_for_conditions,
) )
from chia.wallet.puzzles.puzzle_utils import make_reserve_fee_condition from chia.wallet.puzzles.puzzle_utils import make_reserve_fee_condition
from chia.wallet.signer_protocol import (
PathHint,
Signature,
SignedTransaction,
SigningInstructions,
SigningResponse,
Spend,
SumHint,
TransactionInfo,
)
from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.puzzle_decorator import PuzzleDecoratorManager from chia.wallet.util.puzzle_decorator import PuzzleDecoratorManager
from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES
@@ -498,183 +480,3 @@ class Wallet:
def hardened_pubkey_for_path(self, path: list[int]) -> G1Element: def hardened_pubkey_for_path(self, path: list[int]) -> G1Element:
return _derive_path(self.wallet_state_manager.get_master_private_key(), path).get_g1() return _derive_path(self.wallet_state_manager.get_master_private_key(), path).get_g1()
async def sum_hint_for_pubkey(self, pk: bytes) -> SumHint | None:
pk_parsed: G1Element = G1Element.from_bytes(pk)
dr: DerivationRecord | None = await self.wallet_state_manager.puzzle_store.record_for_puzzle_hash(
puzzle_hash_for_synthetic_public_key(pk_parsed)
)
if dr is None:
return None
return SumHint(
[dr.pubkey.get_fingerprint().to_bytes(4, "big")],
calculate_synthetic_offset(dr.pubkey, DEFAULT_HIDDEN_PUZZLE_HASH).to_bytes(32, "big"),
pk,
)
async def path_hint_for_pubkey(self, pk: bytes) -> PathHint | None:
pk_parsed: G1Element = G1Element.from_bytes(pk)
index: uint32 | None = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pk_parsed)
if index is None:
index = await self.wallet_state_manager.puzzle_store.index_for_puzzle_hash(
puzzle_hash_for_synthetic_public_key(pk_parsed)
)
root_pubkey: bytes = self.wallet_state_manager.root_pubkey.get_fingerprint().to_bytes(4, "big")
if index is None:
# Pool wallet may have a secret key here
if self.wallet_state_manager.private_key is not None:
for pool_wallet_index in range(MAX_POOL_WALLETS):
try_owner_sk = master_sk_to_singleton_owner_sk(
self.wallet_state_manager.private_key, uint32(pool_wallet_index)
)
if try_owner_sk.get_g1() == pk_parsed:
return PathHint(
root_pubkey,
[uint64(12381), uint64(8444), uint64(5), uint64(pool_wallet_index)],
)
return None
return PathHint(
root_pubkey,
[uint64(12381), uint64(8444), uint64(2), uint64(index)],
)
async def execute_signing_instructions(
self, signing_instructions: SigningInstructions, partial_allowed: bool = False
) -> list[SigningResponse]:
root_pubkey: G1Element = self.wallet_state_manager.root_pubkey
pk_lookup: dict[int, G1Element] = (
{root_pubkey.get_fingerprint(): root_pubkey} if self.wallet_state_manager.private_key is not None else {}
)
sk_lookup: dict[int, PrivateKey] = (
{root_pubkey.get_fingerprint(): self.wallet_state_manager.get_master_private_key()}
if self.wallet_state_manager.private_key is not None
else {}
)
aggregate_responses_at_end: bool = True
responses: list[SigningResponse] = []
# TODO: expand path hints and sum hints recursively (a sum hint can give a new key to path hint)
# Next, expand our pubkey set with path hints
if self.wallet_state_manager.private_key is not None:
for path_hint in signing_instructions.key_hints.path_hints:
if int.from_bytes(path_hint.root_fingerprint, "big") != root_pubkey.get_fingerprint():
if not partial_allowed:
raise ValueError(f"No root pubkey for fingerprint {root_pubkey.get_fingerprint()}")
else:
continue
else:
path = [int(step) for step in path_hint.path]
derive_child_sk = _derive_path(self.wallet_state_manager.get_master_private_key(), path)
derive_child_sk_unhardened = _derive_path_unhardened(
self.wallet_state_manager.get_master_private_key(), path
)
derive_child_pk = derive_child_sk.get_g1()
derive_child_pk_unhardened = derive_child_sk_unhardened.get_g1()
pk_lookup[derive_child_pk.get_fingerprint()] = derive_child_pk
pk_lookup[derive_child_pk_unhardened.get_fingerprint()] = derive_child_pk_unhardened
sk_lookup[derive_child_pk.get_fingerprint()] = derive_child_sk
sk_lookup[derive_child_pk_unhardened.get_fingerprint()] = derive_child_sk_unhardened
# Next, expand our pubkey set with sum hints
sum_hint_lookup: dict[int, list[int]] = {}
for sum_hint in signing_instructions.key_hints.sum_hints:
fingerprints_we_have: list[int] = []
for fingerprint in sum_hint.fingerprints:
fingerprint_as_int = int.from_bytes(fingerprint, "big")
if fingerprint_as_int not in pk_lookup:
if not partial_allowed:
raise ValueError(
f"No pubkey found (or path hinted to) for fingerprint {int.from_bytes(fingerprint, 'big')}"
)
else:
aggregate_responses_at_end = False
else:
fingerprints_we_have.append(fingerprint_as_int)
# Add any synthetic offsets as keys we "have"
offset_sk = PrivateKey.from_bytes(sum_hint.synthetic_offset)
offset_pk = offset_sk.get_g1()
pk_lookup[offset_pk.get_fingerprint()] = offset_pk
sk_lookup[offset_pk.get_fingerprint()] = offset_sk
final_pubkey: G1Element = G1Element.from_bytes(sum_hint.final_pubkey)
final_fingerprint: int = final_pubkey.get_fingerprint()
pk_lookup[final_fingerprint] = final_pubkey
sum_hint_lookup[final_fingerprint] = [*fingerprints_we_have, offset_pk.get_fingerprint()]
for target in signing_instructions.targets:
pk_fingerprint: int = int.from_bytes(target.fingerprint, "big")
if pk_fingerprint not in sk_lookup and pk_fingerprint not in sum_hint_lookup:
if not partial_allowed:
raise ValueError(f"Pubkey {pk_fingerprint} not found (or path/sum hinted to)")
else:
aggregate_responses_at_end = False
continue
elif pk_fingerprint in sk_lookup:
responses.append(
SigningResponse(
bytes(AugSchemeMPL.sign(sk_lookup[pk_fingerprint], target.message)),
target.hook,
)
)
else: # Implicit if pk_fingerprint in sum_hint_lookup
signatures: list[G2Element] = []
for partial_fingerprint in sum_hint_lookup[pk_fingerprint]:
signatures.append(
AugSchemeMPL.sign(sk_lookup[partial_fingerprint], target.message, pk_lookup[pk_fingerprint])
)
if partial_allowed:
# In multisig scenarios, we return everything as a component signature
for sig in signatures:
responses.append(
SigningResponse(
bytes(sig),
target.hook,
)
)
else:
# In the scenario where we are the only signer, we can collapse many responses into one
responses.append(
SigningResponse(
bytes(AugSchemeMPL.aggregate(signatures)),
target.hook,
)
)
# If we have the full set of signing responses for the instructions, aggregate them as much as possible
if aggregate_responses_at_end:
new_responses: list[SigningResponse] = []
grouped_responses: dict[bytes32, list[SigningResponse]] = {}
for response in responses:
grouped_responses.setdefault(response.hook, [])
grouped_responses[response.hook].append(response)
for hook, group in grouped_responses.items():
new_responses.append(
SigningResponse(
bytes(AugSchemeMPL.aggregate([G2Element.from_bytes(res.signature) for res in group])),
hook,
)
)
responses = new_responses
return responses
async def apply_signatures(
self, spends: list[Spend], signing_responses: list[SigningResponse]
) -> SignedTransaction:
signing_responses_set = set(signing_responses)
return SignedTransaction(
TransactionInfo(spends),
[
Signature(
"bls_12381_aug_scheme",
bytes(
AugSchemeMPL.aggregate(
[
G2Element.from_bytes(signing_response.signature)
for signing_response in signing_responses_set
]
)
),
)
],
)
+7 -5
View File
@@ -430,7 +430,7 @@ def tx_endpoint(
# deferring to parent action scope # deferring to parent action scope
return response return response
unsigned_txs = await self.service.wallet_state_manager.gather_signing_info_for_txs( unsigned_txs = await self.service.wallet_state_manager.signer.gather_signing_info_for_txs(
action_scope.side_effects.transactions action_scope.side_effects.transactions
) )
@@ -3535,7 +3535,7 @@ class WalletRpcApi:
request: GatherSigningInfo, request: GatherSigningInfo,
) -> GatherSigningInfoResponse: ) -> GatherSigningInfoResponse:
return GatherSigningInfoResponse( return GatherSigningInfoResponse(
signing_instructions=await self.service.wallet_state_manager.gather_signing_info(request.spends) signing_instructions=await self.service.wallet_state_manager.signer.gather_signing_info(request.spends)
) )
async def apply_signatures( async def apply_signatures(
@@ -3544,7 +3544,9 @@ class WalletRpcApi:
) -> ApplySignaturesResponse: ) -> ApplySignaturesResponse:
return ApplySignaturesResponse( return ApplySignaturesResponse(
signed_transactions=[ signed_transactions=[
await self.service.wallet_state_manager.apply_signatures(request.spends, request.signing_responses) await self.service.wallet_state_manager.signer.apply_signatures(
request.spends, request.signing_responses
)
] ]
) )
@@ -3553,7 +3555,7 @@ class WalletRpcApi:
request: SubmitTransactions, request: SubmitTransactions,
) -> SubmitTransactionsResponse: ) -> SubmitTransactionsResponse:
return SubmitTransactionsResponse( return SubmitTransactionsResponse(
mempool_ids=await self.service.wallet_state_manager.submit_transactions(request.signed_transactions) mempool_ids=await self.service.wallet_state_manager.signer.submit_transactions(request.signed_transactions)
) )
async def execute_signing_instructions( async def execute_signing_instructions(
@@ -3561,7 +3563,7 @@ class WalletRpcApi:
request: ExecuteSigningInstructions, request: ExecuteSigningInstructions,
) -> ExecuteSigningInstructionsResponse: ) -> ExecuteSigningInstructionsResponse:
return ExecuteSigningInstructionsResponse( return ExecuteSigningInstructionsResponse(
signing_responses=await self.service.wallet_state_manager.execute_signing_instructions( signing_responses=await self.service.wallet_state_manager.signer.execute_signing_instructions(
request.signing_instructions, request.partial_allowed request.signing_instructions, request.partial_allowed
) )
) )
+357
View File
@@ -0,0 +1,357 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, replace
from chia_rs import AugSchemeMPL, CoinSpend, G1Element, G2Element, PrivateKey
from chia_rs.sized_bytes import bytes32
from chia_rs.sized_ints import uint32, uint64
from chia.consensus.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict
from chia.util.hash import std_hash
from chia.wallet.derive_keys import (
MAX_POOL_WALLETS,
_derive_path,
_derive_path_unhardened,
master_sk_to_singleton_owner_sk,
)
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
DEFAULT_HIDDEN_PUZZLE_HASH,
calculate_synthetic_offset,
puzzle_hash_for_synthetic_public_key,
)
from chia.wallet.signer_protocol import (
KeyHints,
PathHint,
Signature,
SignedTransaction,
SigningInstructions,
SigningResponse,
SigningTarget,
Spend,
SumHint,
TransactionInfo,
UnsignedTransaction,
)
from chia.wallet.trading.offer import Offer
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.wallet_puzzle_store import WalletPuzzleStore
from chia.wallet.wallet_spend_bundle import WalletSpendBundle
@dataclass(frozen=True, kw_only=True)
class WalletSigner:
root_pubkey: G1Element
root_private_key: PrivateKey | None
puzzle_store: WalletPuzzleStore
max_block_cost_clvm: int
agg_sig_me_additional_data: bytes32
spend_bundle_push: Callable[[WalletSpendBundle], Awaitable[None]]
def __post_init__(self) -> None:
if self.root_private_key is not None and self.root_private_key.get_g1() != self.root_pubkey:
raise ValueError("root_private_key does not match self.root_pubkey")
async def sum_hint_for_pubkey(self, pk: bytes) -> SumHint | None:
pk_parsed: G1Element = G1Element.from_bytes(pk)
dr = await self.puzzle_store.record_for_puzzle_hash(puzzle_hash_for_synthetic_public_key(pk_parsed))
if dr is None:
return None
return SumHint(
[dr.pubkey.get_fingerprint().to_bytes(4, "big")],
calculate_synthetic_offset(dr.pubkey, DEFAULT_HIDDEN_PUZZLE_HASH).to_bytes(32, "big"),
pk,
)
async def path_hint_for_pubkey(self, pk: bytes) -> PathHint | None:
pk_parsed = G1Element.from_bytes(pk)
index = await self.puzzle_store.index_for_pubkey(pk_parsed)
if index is None:
index = await self.puzzle_store.index_for_puzzle_hash(puzzle_hash_for_synthetic_public_key(pk_parsed))
root_pubkey: bytes = self.root_pubkey.get_fingerprint().to_bytes(4, "big")
if index is None:
# Pool wallet may have a secret key here
if self.root_private_key is not None:
for pool_wallet_index in range(MAX_POOL_WALLETS):
try_owner_sk = master_sk_to_singleton_owner_sk(self.root_private_key, uint32(pool_wallet_index))
if try_owner_sk.get_g1() == pk_parsed:
return PathHint(
root_pubkey,
[uint64(12381), uint64(8444), uint64(5), uint64(pool_wallet_index)],
)
return None
return PathHint(
root_pubkey,
[uint64(12381), uint64(8444), uint64(2), uint64(index)],
)
async def key_hints_for_pubkeys(self, pks: list[bytes]) -> KeyHints:
return KeyHints(
[sum_hint for pk in pks if (sum_hint := await self.sum_hint_for_pubkey(pk)) is not None],
[path_hint for pk in pks if (path_hint := await self.path_hint_for_pubkey(pk)) is not None],
)
async def gather_signing_info(self, spends: list[Spend]) -> SigningInstructions:
pks: list[bytes] = []
signing_targets: list[SigningTarget] = []
for spend in spends:
coin_spend = spend.as_coin_spend()
# Get AGG_SIG conditions
conditions_dict = conditions_dict_for_solution(
coin_spend.puzzle_reveal,
coin_spend.solution,
self.max_block_cost_clvm,
)
# Create signature
for pk, msg in pkm_pairs_for_conditions_dict(
conditions_dict, coin_spend.coin, self.agg_sig_me_additional_data
):
pk_bytes = bytes(pk)
pks.append(pk_bytes)
fingerprint: bytes = pk.get_fingerprint().to_bytes(4, "big")
signing_targets.append(SigningTarget(fingerprint, msg, std_hash(pk_bytes + msg)))
return SigningInstructions(
await self.key_hints_for_pubkeys(pks),
signing_targets,
)
async def gather_signing_info_for_bundles(self, bundles: list[WalletSpendBundle]) -> list[UnsignedTransaction]:
utxs: list[UnsignedTransaction] = []
for bundle in bundles:
signer_protocol_spends: list[Spend] = [Spend.from_coin_spend(spend) for spend in bundle.coin_spends]
utxs.append(
UnsignedTransaction(
TransactionInfo(signer_protocol_spends),
await self.gather_signing_info(signer_protocol_spends),
)
)
return utxs
async def gather_signing_info_for_txs(self, txs: list[TransactionRecord]) -> list[UnsignedTransaction]:
return await self.gather_signing_info_for_bundles(
[tx.spend_bundle for tx in txs if tx.spend_bundle is not None]
)
async def gather_signing_info_for_trades(self, offers: list[Offer]) -> list[UnsignedTransaction]:
return await self.gather_signing_info_for_bundles([offer._bundle for offer in offers])
async def execute_signing_instructions(
self, signing_instructions: SigningInstructions, partial_allowed: bool = False
) -> list[SigningResponse]:
pk_lookup: dict[int, G1Element] = (
{self.root_pubkey.get_fingerprint(): self.root_pubkey} if self.root_private_key is not None else {}
)
sk_lookup: dict[int, PrivateKey] = (
{self.root_pubkey.get_fingerprint(): self.root_private_key} if self.root_private_key is not None else {}
)
aggregate_responses_at_end: bool = True
responses: list[SigningResponse] = []
# TODO: expand path hints and sum hints recursively (a sum hint can give a new key to path hint)
# Next, expand our pubkey set with path hints
if self.root_private_key is not None:
for path_hint in signing_instructions.key_hints.path_hints:
if int.from_bytes(path_hint.root_fingerprint, "big") != self.root_pubkey.get_fingerprint():
if not partial_allowed:
raise ValueError(f"No root pubkey for fingerprint {self.root_pubkey.get_fingerprint()}")
else:
continue
else:
path = [int(step) for step in path_hint.path]
derive_child_sk = _derive_path(self.root_private_key, path)
derive_child_sk_unhardened = _derive_path_unhardened(self.root_private_key, path)
derive_child_pk = derive_child_sk.get_g1()
derive_child_pk_unhardened = derive_child_sk_unhardened.get_g1()
pk_lookup[derive_child_pk.get_fingerprint()] = derive_child_pk
pk_lookup[derive_child_pk_unhardened.get_fingerprint()] = derive_child_pk_unhardened
sk_lookup[derive_child_pk.get_fingerprint()] = derive_child_sk
sk_lookup[derive_child_pk_unhardened.get_fingerprint()] = derive_child_sk_unhardened
# Next, expand our pubkey set with sum hints
sum_hint_lookup: dict[int, list[int]] = {}
for sum_hint in signing_instructions.key_hints.sum_hints:
fingerprints_we_have: list[int] = []
for fingerprint in sum_hint.fingerprints:
fingerprint_as_int = int.from_bytes(fingerprint, "big")
if fingerprint_as_int not in pk_lookup:
if not partial_allowed:
raise ValueError(
f"No pubkey found (or path hinted to) for fingerprint {int.from_bytes(fingerprint, 'big')}"
)
else:
aggregate_responses_at_end = False
else:
fingerprints_we_have.append(fingerprint_as_int)
# Add any synthetic offsets as keys we "have"
offset_sk = PrivateKey.from_bytes(sum_hint.synthetic_offset)
offset_pk = offset_sk.get_g1()
pk_lookup[offset_pk.get_fingerprint()] = offset_pk
sk_lookup[offset_pk.get_fingerprint()] = offset_sk
final_pubkey: G1Element = G1Element.from_bytes(sum_hint.final_pubkey)
final_fingerprint: int = final_pubkey.get_fingerprint()
pk_lookup[final_fingerprint] = final_pubkey
sum_hint_lookup[final_fingerprint] = [*fingerprints_we_have, offset_pk.get_fingerprint()]
for target in signing_instructions.targets:
pk_fingerprint: int = int.from_bytes(target.fingerprint, "big")
if pk_fingerprint not in sk_lookup and pk_fingerprint not in sum_hint_lookup:
if not partial_allowed:
raise ValueError(f"Pubkey {pk_fingerprint} not found (or path/sum hinted to)")
else:
aggregate_responses_at_end = False
continue
elif pk_fingerprint in sk_lookup:
responses.append(
SigningResponse(
bytes(AugSchemeMPL.sign(sk_lookup[pk_fingerprint], target.message)),
target.hook,
)
)
else: # Implicit if pk_fingerprint in sum_hint_lookup
signatures: list[G2Element] = []
for partial_fingerprint in sum_hint_lookup[pk_fingerprint]:
signatures.append(
AugSchemeMPL.sign(sk_lookup[partial_fingerprint], target.message, pk_lookup[pk_fingerprint])
)
if partial_allowed:
# In multisig scenarios, we return everything as a component signature
for sig in signatures:
responses.append(
SigningResponse(
bytes(sig),
target.hook,
)
)
else:
# In the scenario where we are the only signer, we can collapse many responses into one
responses.append(
SigningResponse(
bytes(AugSchemeMPL.aggregate(signatures)),
target.hook,
)
)
# If we have the full set of signing responses for the instructions, aggregate them as much as possible
if aggregate_responses_at_end:
new_responses: list[SigningResponse] = []
grouped_responses: dict[bytes32, list[SigningResponse]] = {}
for response in responses:
grouped_responses.setdefault(response.hook, [])
grouped_responses[response.hook].append(response)
for hook, group in grouped_responses.items():
new_responses.append(
SigningResponse(
bytes(AugSchemeMPL.aggregate([G2Element.from_bytes(res.signature) for res in group])),
hook,
)
)
responses = new_responses
return responses
async def apply_signatures(
self, spends: list[Spend], signing_responses: list[SigningResponse]
) -> SignedTransaction:
signing_responses_set = set(signing_responses)
return SignedTransaction(
TransactionInfo(spends),
[
Signature(
"bls_12381_aug_scheme",
bytes(
AugSchemeMPL.aggregate(
[
G2Element.from_bytes(signing_response.signature)
for signing_response in signing_responses_set
]
)
),
)
],
)
def signed_tx_to_spendbundle(self, signed_tx: SignedTransaction) -> WalletSpendBundle:
if len([_ for _ in signed_tx.signatures if _.type != "bls_12381_aug_scheme"]) > 0:
raise ValueError("Unable to handle signatures that are not bls_12381_aug_scheme") # pragma: no cover
return WalletSpendBundle(
[spend.as_coin_spend() for spend in signed_tx.transaction_info.spends],
AugSchemeMPL.aggregate([G2Element.from_bytes(sig.signature) for sig in signed_tx.signatures]),
)
async def sign_transactions(
self,
tx_records: list[TransactionRecord],
additional_signing_responses: list[SigningResponse] = [],
partial_allowed: bool = False,
) -> tuple[list[TransactionRecord], list[SigningResponse]]:
unsigned_txs: list[UnsignedTransaction] = await self.gather_signing_info_for_txs(tx_records)
new_txs: list[TransactionRecord] = []
all_signing_responses = additional_signing_responses.copy()
for unsigned_tx, tx in zip(
unsigned_txs, [tx_record for tx_record in tx_records if tx_record.spend_bundle is not None]
):
signing_responses: list[SigningResponse] = await self.execute_signing_instructions(
unsigned_tx.signing_instructions, partial_allowed=partial_allowed
)
all_signing_responses.extend(signing_responses)
new_bundle = self.signed_tx_to_spendbundle(
await self.apply_signatures(
unsigned_tx.transaction_info.spends,
[*additional_signing_responses, *signing_responses],
)
)
new_txs.append(replace(tx, spend_bundle=new_bundle, name=new_bundle.name()))
new_txs.extend([tx_record for tx_record in tx_records if tx_record.spend_bundle is None])
return new_txs, all_signing_responses
async def sign_offers(
self,
offers: list[Offer],
additional_signing_responses: list[SigningResponse] = [],
partial_allowed: bool = False,
) -> tuple[list[Offer], list[SigningResponse]]:
unsigned_txs: list[UnsignedTransaction] = await self.gather_signing_info_for_trades(offers)
new_offers: list[Offer] = []
all_signing_responses = additional_signing_responses.copy()
for unsigned_tx, offer in zip(unsigned_txs, [offer for offer in offers]):
signing_responses: list[SigningResponse] = await self.execute_signing_instructions(
unsigned_tx.signing_instructions, partial_allowed=partial_allowed
)
all_signing_responses.extend(signing_responses)
new_bundle = self.signed_tx_to_spendbundle(
await self.apply_signatures(
unsigned_tx.transaction_info.spends,
[*additional_signing_responses, *signing_responses],
)
)
new_offers.append(Offer(offer.requested_payments, new_bundle, offer.driver_dict))
return new_offers, all_signing_responses
async def sign_bundle(
self,
coin_spends: list[CoinSpend],
additional_signing_responses: list[SigningResponse] = [],
partial_allowed: bool = False,
) -> tuple[WalletSpendBundle, list[SigningResponse]]:
[unsigned_tx] = await self.gather_signing_info_for_bundles([WalletSpendBundle(coin_spends, G2Element())])
signing_responses: list[SigningResponse] = await self.execute_signing_instructions(
unsigned_tx.signing_instructions, partial_allowed=partial_allowed
)
return (
self.signed_tx_to_spendbundle(
await self.apply_signatures(
unsigned_tx.transaction_info.spends,
[*additional_signing_responses, *signing_responses],
)
),
signing_responses,
)
async def submit_transactions(self, signed_txs: list[SignedTransaction]) -> list[bytes32]:
bundles: list[WalletSpendBundle] = [self.signed_tx_to_spendbundle(tx) for tx in signed_txs]
for bundle in bundles:
await self.spend_bundle_push(bundle)
return [bundle.name() for bundle in bundles]
+14 -166
View File
@@ -16,13 +16,12 @@ from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast
import aiosqlite import aiosqlite
from chia_rs import AugSchemeMPL, CoinRecord, CoinSpend, CoinState, ConsensusConstants, G1Element, G2Element, PrivateKey from chia_rs import CoinRecord, CoinSpend, CoinState, ConsensusConstants, G1Element, G2Element, PrivateKey
from chia_rs.sized_bytes import bytes32 from chia_rs.sized_bytes import bytes32
from chia_rs.sized_ints import uint8, uint16, uint32, uint64, uint128 from chia_rs.sized_ints import uint8, uint16, uint32, uint64, uint128
from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward
from chia.consensus.coinbase import farmer_parent_id, pool_parent_id from chia.consensus.coinbase import farmer_parent_id, pool_parent_id
from chia.consensus.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict
from chia.data_layer.data_layer_wallet import DataLayerWallet from chia.data_layer.data_layer_wallet import DataLayerWallet
from chia.data_layer.dl_wallet_store import DataLayerStore from chia.data_layer.dl_wallet_store import DataLayerStore
from chia.data_layer.singleton_record import SingletonRecord from chia.data_layer.singleton_record import SingletonRecord
@@ -93,22 +92,10 @@ from chia.wallet.puzzles.clawback.drivers import generate_clawback_spend_bundle,
from chia.wallet.puzzles.clawback.metadata import ClawbackMetadata, ClawbackVersion from chia.wallet.puzzles.clawback.metadata import ClawbackMetadata, ClawbackVersion
from chia.wallet.remote_wallet.remote_coin_store import RemoteCoinStore from chia.wallet.remote_wallet.remote_coin_store import RemoteCoinStore
from chia.wallet.remote_wallet.remote_wallet import RemoteWallet from chia.wallet.remote_wallet.remote_wallet import RemoteWallet
from chia.wallet.signer_protocol import ( from chia.wallet.signer_protocol import SigningResponse
KeyHints,
PathHint,
SignedTransaction,
SigningInstructions,
SigningResponse,
SigningTarget,
Spend,
SumHint,
TransactionInfo,
UnsignedTransaction,
)
from chia.wallet.singleton import SINGLETON_LAUNCHER_PUZZLE_HASH as SINGLETON_LAUNCHER_HASH from chia.wallet.singleton import SINGLETON_LAUNCHER_PUZZLE_HASH as SINGLETON_LAUNCHER_HASH
from chia.wallet.singleton import create_singleton_puzzle, get_inner_puzzle_from_singleton from chia.wallet.singleton import create_singleton_puzzle, get_inner_puzzle_from_singleton
from chia.wallet.trade_manager import TradeManager from chia.wallet.trade_manager import TradeManager
from chia.wallet.trading.offer import Offer
from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.trading.trade_status import TradeStatus
from chia.wallet.transaction_record import LightTransactionRecord, TransactionRecord from chia.wallet.transaction_record import LightTransactionRecord, TransactionRecord
from chia.wallet.uncurried_puzzle import uncurry_puzzle from chia.wallet.uncurried_puzzle import uncurry_puzzle
@@ -149,6 +136,7 @@ from chia.wallet.wallet_protocol import (
) )
from chia.wallet.wallet_puzzle_store import WalletPuzzleStore from chia.wallet.wallet_puzzle_store import WalletPuzzleStore
from chia.wallet.wallet_retry_store import WalletRetryStore from chia.wallet.wallet_retry_store import WalletRetryStore
from chia.wallet.wallet_signer import WalletSigner
from chia.wallet.wallet_spend_bundle import WalletSpendBundle from chia.wallet.wallet_spend_bundle import WalletSpendBundle
from chia.wallet.wallet_transaction_store import WalletTransactionStore from chia.wallet.wallet_transaction_store import WalletTransactionStore
from chia.wallet.wallet_user_store import WalletUserStore from chia.wallet.wallet_user_store import WalletUserStore
@@ -217,6 +205,7 @@ class WalletStateManager:
asset_to_wallet_map: dict[AssetType, Any] asset_to_wallet_map: dict[AssetType, Any]
initial_num_public_keys: int initial_num_public_keys: int
decorator_manager: PuzzleDecoratorManager decorator_manager: PuzzleDecoratorManager
signer: WalletSigner
@staticmethod @staticmethod
async def create( async def create(
@@ -293,6 +282,15 @@ class WalletStateManager:
assert root_pubkey == calculated_root_public_key assert root_pubkey == calculated_root_public_key
self.root_pubkey = calculated_root_public_key self.root_pubkey = calculated_root_public_key
self.signer = WalletSigner(
root_pubkey=self.root_pubkey,
root_private_key=self.private_key,
puzzle_store=self.puzzle_store,
max_block_cost_clvm=self.constants.MAX_BLOCK_COST_CLVM,
agg_sig_me_additional_data=self.constants.AGG_SIG_ME_ADDITIONAL_DATA,
spend_bundle_push=self.wallet_node.push_tx,
)
fingerprint = self.root_pubkey.get_fingerprint() fingerprint = self.root_pubkey.get_fingerprint()
puzzle_decorators = self.config.get("puzzle_decorators", {}).get(fingerprint, []) puzzle_decorators = self.config.get("puzzle_decorators", {}).get(fingerprint, [])
self.decorator_manager = PuzzleDecoratorManager.create(puzzle_decorators) self.decorator_manager = PuzzleDecoratorManager.create(puzzle_decorators)
@@ -2461,7 +2459,7 @@ class WalletStateManager:
for i, tx in enumerate(tx_records) for i, tx in enumerate(tx_records)
] ]
if sign: if sign:
tx_records, _ = await self.sign_transactions( tx_records, _ = await self.signer.sign_transactions(
tx_records, tx_records,
[] if additional_signing_responses is None else additional_signing_responses, [] if additional_signing_responses is None else additional_signing_responses,
additional_signing_responses != [] and additional_signing_responses is not None, additional_signing_responses != [] and additional_signing_responses is not None,
@@ -2801,156 +2799,6 @@ class WalletStateManager:
return vc_wallet return vc_wallet
async def sum_hint_for_pubkey(self, pk: bytes) -> SumHint | None:
return await self.main_wallet.sum_hint_for_pubkey(pk)
async def path_hint_for_pubkey(self, pk: bytes) -> PathHint | None:
return await self.main_wallet.path_hint_for_pubkey(pk)
async def key_hints_for_pubkeys(self, pks: list[bytes]) -> KeyHints:
return KeyHints(
[sum_hint for pk in pks for sum_hint in (await self.sum_hint_for_pubkey(pk),) if sum_hint is not None],
[path_hint for pk in pks for path_hint in (await self.path_hint_for_pubkey(pk),) if path_hint is not None],
)
async def gather_signing_info(self, spends: list[Spend]) -> SigningInstructions:
pks: list[bytes] = []
signing_targets: list[SigningTarget] = []
for spend in spends:
coin_spend = spend.as_coin_spend()
# Get AGG_SIG conditions
conditions_dict = conditions_dict_for_solution(
Program.from_serialized(coin_spend.puzzle_reveal),
Program.from_serialized(coin_spend.solution),
self.constants.MAX_BLOCK_COST_CLVM,
)
# Create signature
for pk, msg in pkm_pairs_for_conditions_dict(
conditions_dict, coin_spend.coin, self.constants.AGG_SIG_ME_ADDITIONAL_DATA
):
pk_bytes = bytes(pk)
pks.append(pk_bytes)
fingerprint: bytes = pk.get_fingerprint().to_bytes(4, "big")
signing_targets.append(SigningTarget(fingerprint, msg, std_hash(pk_bytes + msg)))
return SigningInstructions(
await self.key_hints_for_pubkeys(pks),
signing_targets,
)
async def gather_signing_info_for_bundles(self, bundles: list[WalletSpendBundle]) -> list[UnsignedTransaction]:
utxs: list[UnsignedTransaction] = []
for bundle in bundles:
signer_protocol_spends: list[Spend] = [Spend.from_coin_spend(spend) for spend in bundle.coin_spends]
utxs.append(
UnsignedTransaction(
TransactionInfo(signer_protocol_spends), await self.gather_signing_info(signer_protocol_spends)
)
)
return utxs
async def gather_signing_info_for_txs(self, txs: list[TransactionRecord]) -> list[UnsignedTransaction]:
return await self.gather_signing_info_for_bundles(
[tx.spend_bundle for tx in txs if tx.spend_bundle is not None]
)
async def gather_signing_info_for_trades(self, offers: list[Offer]) -> list[UnsignedTransaction]:
return await self.gather_signing_info_for_bundles([offer._bundle for offer in offers])
async def execute_signing_instructions(
self, signing_instructions: SigningInstructions, partial_allowed: bool = False
) -> list[SigningResponse]:
return await self.main_wallet.execute_signing_instructions(signing_instructions, partial_allowed)
async def apply_signatures(
self, spends: list[Spend], signing_responses: list[SigningResponse]
) -> SignedTransaction:
return await self.main_wallet.apply_signatures(spends, signing_responses)
def signed_tx_to_spendbundle(self, signed_tx: SignedTransaction) -> WalletSpendBundle:
if len([_ for _ in signed_tx.signatures if _.type != "bls_12381_aug_scheme"]) > 0:
raise ValueError("Unable to handle signatures that are not bls_12381_aug_scheme") # pragma: no cover
return WalletSpendBundle(
[spend.as_coin_spend() for spend in signed_tx.transaction_info.spends],
AugSchemeMPL.aggregate([G2Element.from_bytes(sig.signature) for sig in signed_tx.signatures]),
)
async def sign_transactions(
self,
tx_records: list[TransactionRecord],
additional_signing_responses: list[SigningResponse] = [],
partial_allowed: bool = False,
) -> tuple[list[TransactionRecord], list[SigningResponse]]:
unsigned_txs: list[UnsignedTransaction] = await self.gather_signing_info_for_txs(tx_records)
new_txs: list[TransactionRecord] = []
all_signing_responses = additional_signing_responses.copy()
for unsigned_tx, tx in zip(
unsigned_txs, [tx_record for tx_record in tx_records if tx_record.spend_bundle is not None]
):
signing_responses: list[SigningResponse] = await self.execute_signing_instructions(
unsigned_tx.signing_instructions, partial_allowed=partial_allowed
)
all_signing_responses.extend(signing_responses)
new_bundle = self.signed_tx_to_spendbundle(
await self.apply_signatures(
unsigned_tx.transaction_info.spends,
[*additional_signing_responses, *signing_responses],
)
)
new_txs.append(dataclasses.replace(tx, spend_bundle=new_bundle, name=new_bundle.name()))
new_txs.extend([tx_record for tx_record in tx_records if tx_record.spend_bundle is None])
return new_txs, all_signing_responses
async def sign_offers(
self,
offers: list[Offer],
additional_signing_responses: list[SigningResponse] = [],
partial_allowed: bool = False,
) -> tuple[list[Offer], list[SigningResponse]]:
unsigned_txs: list[UnsignedTransaction] = await self.gather_signing_info_for_trades(offers)
new_offers: list[Offer] = []
all_signing_responses = additional_signing_responses.copy()
for unsigned_tx, offer in zip(unsigned_txs, [offer for offer in offers]):
signing_responses: list[SigningResponse] = await self.execute_signing_instructions(
unsigned_tx.signing_instructions, partial_allowed=partial_allowed
)
all_signing_responses.extend(signing_responses)
new_bundle = self.signed_tx_to_spendbundle(
await self.apply_signatures(
unsigned_tx.transaction_info.spends,
[*additional_signing_responses, *signing_responses],
)
)
new_offers.append(Offer(offer.requested_payments, new_bundle, offer.driver_dict))
return new_offers, all_signing_responses
async def sign_bundle(
self,
coin_spends: list[CoinSpend],
additional_signing_responses: list[SigningResponse] = [],
partial_allowed: bool = False,
) -> tuple[WalletSpendBundle, list[SigningResponse]]:
[unsigned_tx] = await self.gather_signing_info_for_bundles([WalletSpendBundle(coin_spends, G2Element())])
signing_responses: list[SigningResponse] = await self.execute_signing_instructions(
unsigned_tx.signing_instructions, partial_allowed=partial_allowed
)
return (
self.signed_tx_to_spendbundle(
await self.apply_signatures(
unsigned_tx.transaction_info.spends,
[*additional_signing_responses, *signing_responses],
)
),
signing_responses,
)
async def submit_transactions(self, signed_txs: list[SignedTransaction]) -> list[bytes32]:
bundles: list[WalletSpendBundle] = [self.signed_tx_to_spendbundle(tx) for tx in signed_txs]
for bundle in bundles:
await self.wallet_node.push_tx(bundle)
return [bundle.name() for bundle in bundles]
@contextlib.asynccontextmanager @contextlib.asynccontextmanager
async def new_action_scope( async def new_action_scope(
self, self,