Merge standalone wallet into main (#9793)

* wallet changes from pac

* cat changes

* pool tests

* pooling tests passing

* offers

* lint

* mempool_mode

* black

* linting

* workflow files

* flake8

* more cleanup

* renamed

* remove obsolete test, don't cast announcement

* memos are not only bytes32

* trade renames

* fix rpcs, block_record

* wallet rpc, recompile settlement clvm

* key derivation

* clvm tests

* lgtm issues and wallet peers

* stash

* rename

* mypy linting

* flake8

* bad initializer

* flaky tests

* Make CAT wallets only create on verified hints (#9651)

* fix clvm tests

* return to log lvl warn

* check puzzle unhardened

* public key, not bytes. api caching change

* precommit changes

* remove unused import

* mypy ci file, tests

* ensure balance before creating a tx

* Remove CAT logic from full node test (#9741)

* Add confirmations and sleeps for wallet (#9742)

* use pool executor

* rever merge mistakes/cleanup

* Fix trade test flakiness (#9751)

* remove precommit

* older version of black

* lint only in super linter

* Make announcements in RPC be objects instead of bytes (#9752)

* Make announcements in RPC be objects instead of bytes

* Lint

* misc hint'ish cleanup (#9753)

* misc hint'ish cleanup

* unremove some ci bits

* Use main cached_bls.py

* Fix bad merge in main_pac (#9774)

* Fix bad merge at 71da0487b9

* Remove unused ignores

* more unused ignores

* Fix bad merge at 3b143e7050

* One more byte32.from_hexstr

* Remove obsolete test

* remove commented out

* remove duplicate payment object

* remove long sync

* remove unused test, noise

* memos type

* bytes32

* make it clear it's a single state at a time

* copy over asset ids from pacr

* file endl linter

* Update chia/server/ws_connection.py

Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com>

Co-authored-by: Matt Hauff <quexington@gmail.com>
Co-authored-by: Kyle Altendorf <sda@fstab.net>
Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com>
This commit is contained in:
Yostra 2022-01-13 15:08:32 -05:00 committed by GitHub
parent 0ba838b7a8
commit 89f15f591c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
128 changed files with 9548 additions and 6466 deletions

View File

@ -1,7 +1,7 @@
#
# THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme
#
name: MacOS wallet-cc_wallet Tests
name: MacOS wallet-cat_wallet Tests
on:
push:
@ -15,7 +15,7 @@ on:
jobs:
build:
name: MacOS wallet-cc_wallet Tests
name: MacOS wallet-cat_wallet Tests
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
@ -92,10 +92,10 @@ jobs:
sh install-timelord.sh
./vdf_bench square_asm 400000
- name: Test wallet-cc_wallet code with pytest
- name: Test wallet-cat_wallet code with pytest
run: |
. ./activate
./venv/bin/py.test tests/wallet/cc_wallet/test_*.py -s -v --durations 0
./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0
#
# THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme
#

View File

@ -1,7 +1,7 @@
#
# THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme
#
name: Ubuntu wallet-cc_wallet Test
name: Ubuntu wallet-cat_wallet Test
on:
push:
@ -15,7 +15,7 @@ on:
jobs:
build:
name: Ubuntu wallet-cc_wallet Test
name: Ubuntu wallet-cat_wallet Test
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
@ -97,10 +97,10 @@ jobs:
sh install-timelord.sh
./vdf_bench square_asm 400000
- name: Test wallet-cc_wallet code with pytest
- name: Test wallet-cat_wallet code with pytest
run: |
. ./activate
./venv/bin/py.test tests/wallet/cc_wallet/test_*.py -s -v --durations 0
./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0
#

View File

@ -119,7 +119,7 @@ async def delete_unconfirmed_transactions(args: dict, wallet_client: WalletRpcCl
def wallet_coin_unit(typ: WalletType, address_prefix: str) -> Tuple[str, int]:
if typ == WalletType.COLOURED_COIN:
if typ == WalletType.CAT:
return "", units["colouredcoin"]
if typ in [WalletType.STANDARD_WALLET, WalletType.POOLING_WALLET, WalletType.MULTI_SIG, WalletType.RATE_LIMITED]:
return address_prefix, units["chia"]

View File

@ -848,22 +848,12 @@ class Blockchain(BlockchainInterface):
self.__heights_in_cache[block_record.height] = set()
self.__heights_in_cache[block_record.height].add(block_record.header_hash)
# TODO: address hint error and remove ignore
# error: Argument 1 of "persist_sub_epoch_challenge_segments" is incompatible with supertype
# "BlockchainInterface"; supertype defines the argument type as "uint32" [override]
# note: This violates the Liskov substitution principle
# note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
async def persist_sub_epoch_challenge_segments( # type: ignore[override]
async def persist_sub_epoch_challenge_segments(
self, ses_block_hash: bytes32, segments: List[SubEpochChallengeSegment]
):
return await self.block_store.persist_sub_epoch_challenge_segments(ses_block_hash, segments)
# TODO: address hint error and remove ignore
# error: Argument 1 of "get_sub_epoch_challenge_segments" is incompatible with supertype
# "BlockchainInterface"; supertype defines the argument type as "uint32" [override]
# note: This violates the Liskov substitution principle
# note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
async def get_sub_epoch_challenge_segments( # type: ignore[override]
async def get_sub_epoch_challenge_segments(
self,
ses_block_hash: bytes32,
) -> Optional[List[SubEpochChallengeSegment]]:

View File

@ -71,13 +71,13 @@ class BlockchainInterface:
return None
async def persist_sub_epoch_challenge_segments(
self, sub_epoch_summary_height: uint32, segments: List[SubEpochChallengeSegment]
self, sub_epoch_summary_height: bytes32, segments: List[SubEpochChallengeSegment]
):
pass
async def get_sub_epoch_challenge_segments(
self,
sub_epoch_summary_height: uint32,
sub_epoch_summary_hash: bytes32,
) -> Optional[List[SubEpochChallengeSegment]]:
pass

View File

@ -22,6 +22,7 @@ def block_to_block_record(
required_iters: uint64,
full_block: Optional[Union[FullBlock, HeaderBlock]],
header_block: Optional[HeaderBlock],
sub_slot_iters: Optional[uint64] = None,
) -> BlockRecord:
if full_block is None:
@ -32,9 +33,10 @@ def block_to_block_record(
prev_b = blocks.try_block_record(block.prev_header_hash)
if block.height > 0:
assert prev_b is not None
sub_slot_iters, _ = get_next_sub_slot_iters_and_difficulty(
constants, len(block.finished_sub_slots) > 0, prev_b, blocks
)
if sub_slot_iters is None:
sub_slot_iters, _ = get_next_sub_slot_iters_and_difficulty(
constants, len(block.finished_sub_slots) > 0, prev_b, blocks
)
overflow = is_overflow_block(constants, block.reward_chain_block.signage_point_index)
deficit = calculate_deficit(
constants,

View File

@ -58,11 +58,8 @@ def validate_clvm_and_signature(
return Err(result.error), b"", {}
pks: List[G1Element] = []
msgs: List[bytes32] = []
# TODO: address hint error and remove ignore
# error: Incompatible types in assignment (expression has type "List[bytes]", variable has type
# "List[bytes32]") [assignment]
pks, msgs = pkm_pairs(result.npc_list, additional_data) # type: ignore[assignment]
msgs: List[bytes] = []
pks, msgs = pkm_pairs(result.npc_list, additional_data)
# Verify aggregated signature
cache: LRUCache = LRUCache(10000)
@ -249,6 +246,7 @@ class MempoolManager:
start_time = time.time()
if new_spend_bytes is None:
new_spend_bytes = bytes(new_spend)
err, cached_result_bytes, new_cache_entries = await asyncio.get_running_loop().run_in_executor(
self.pool,
validate_clvm_and_signature,
@ -257,6 +255,7 @@ class MempoolManager:
self.constants.COST_PER_BYTE,
self.constants.AGG_SIG_ME_ADDITIONAL_DATA,
)
if err is not None:
raise ValidationError(err)
for cache_entry_key, cached_entry_value in new_cache_entries.items():

View File

@ -2,6 +2,7 @@ import asyncio
import dataclasses
import logging
import math
import pathlib
import random
from concurrent.futures.process import ProcessPoolExecutor
from typing import Dict, List, Optional, Tuple
@ -23,7 +24,7 @@ from chia.types.blockchain_format.classgroup import ClassgroupElement
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.blockchain_format.slots import ChallengeChainSubSlot, RewardChainSubSlot
from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary
from chia.types.blockchain_format.vdf import VDFInfo
from chia.types.blockchain_format.vdf import VDFInfo, VDFProof
from chia.types.end_of_slot_bundle import EndOfSubSlotBundle
from chia.types.header_block import HeaderBlock
from chia.types.weight_proof import (
@ -58,6 +59,7 @@ class WeightProofHandler:
self.constants = constants
self.blockchain = blockchain
self.lock = asyncio.Lock()
self._num_processes = 4
async def get_proof_of_weight(self, tip: bytes32) -> Optional[WeightProof]:
@ -107,10 +109,9 @@ class WeightProofHandler:
return None
summary_heights = self.blockchain.get_ses_heights()
# TODO: address hint error and remove ignore
# error: Argument 1 to "get_block_record_from_db" of "BlockchainInterface" has incompatible type
# "Optional[bytes32]"; expected "bytes32" [arg-type]
prev_ses_block = await self.blockchain.get_block_record_from_db(self.blockchain.height_to_hash(uint32(0))) # type: ignore[arg-type] # noqa: E501
zero_hash = self.blockchain.height_to_hash(uint32(0))
assert zero_hash is not None
prev_ses_block = await self.blockchain.get_block_record_from_db(zero_hash)
if prev_ses_block is None:
return None
sub_epoch_data = self.get_sub_epoch_data(tip_rec.height, summary_heights)
@ -140,10 +141,7 @@ class WeightProofHandler:
if _sample_sub_epoch(prev_ses_block.weight, ses_block.weight, weight_to_check): # type: ignore
sample_n += 1
# TODO: address hint error and remove ignore
# error: Argument 1 to "get_sub_epoch_challenge_segments" of "BlockchainInterface" has
# incompatible type "bytes32"; expected "uint32" [arg-type]
segments = await self.blockchain.get_sub_epoch_challenge_segments(ses_block.header_hash) # type: ignore[arg-type] # noqa: E501
segments = await self.blockchain.get_sub_epoch_challenge_segments(ses_block.header_hash)
if segments is None:
segments = await self.__create_sub_epoch_segments(ses_block, prev_ses_block, uint32(sub_epoch_n))
if segments is None:
@ -151,11 +149,7 @@ class WeightProofHandler:
f"failed while building segments for sub epoch {sub_epoch_n}, ses height {ses_height} "
)
return None
# TODO: address hint error and remove ignore
# error: Argument 1 to "persist_sub_epoch_challenge_segments" of "BlockchainInterface" has
# incompatible type "bytes32"; expected "uint32" [arg-type]
await self.blockchain.persist_sub_epoch_challenge_segments(ses_block.header_hash, segments) # type: ignore[arg-type] # noqa: E501
log.debug(f"sub epoch {sub_epoch_n} has {len(segments)} segments")
await self.blockchain.persist_sub_epoch_challenge_segments(ses_block.header_hash, segments)
sub_epoch_segments.extend(segments)
prev_ses_block = ses_block
log.debug(f"sub_epochs: {len(sub_epoch_data)}")
@ -195,10 +189,9 @@ class WeightProofHandler:
if curr_height == 0:
break
# add to needed reward chain recent blocks
# TODO: address hint error and remove ignore
# error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, HeaderBlock]"; expected type
# "bytes32" [index]
header_block = headers[self.blockchain.height_to_hash(curr_height)] # type: ignore[index]
header_hash = self.blockchain.height_to_hash(curr_height)
assert header_hash is not None
header_block = headers[header_hash]
block_rec = blocks[header_block.header_hash]
if header_block is None:
log.error("creating recent chain failed")
@ -209,10 +202,9 @@ class WeightProofHandler:
curr_height = uint32(curr_height - 1)
blocks_n += 1
# TODO: address hint error and remove ignore
# error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, HeaderBlock]"; expected type "bytes32"
# [index]
header_block = headers[self.blockchain.height_to_hash(curr_height)] # type: ignore[index]
header_hash = self.blockchain.height_to_hash(curr_height)
assert header_hash is not None
header_block = headers[header_hash]
recent_chain.insert(0, header_block)
log.info(
@ -309,10 +301,9 @@ class WeightProofHandler:
first = False
else:
height = height + uint32(1) # type: ignore
# TODO: address hint error and remove ignore
# error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, HeaderBlock]"; expected type
# "bytes32" [index]
curr = header_blocks[self.blockchain.height_to_hash(height)] # type: ignore[index]
header_hash = self.blockchain.height_to_hash(height)
assert header_hash is not None
curr = header_blocks[header_hash]
if curr is None:
return None
log.debug(f"next sub epoch starts at {height}")
@ -331,10 +322,9 @@ class WeightProofHandler:
if end - curr_rec.height == batch_size - 1:
blocks = await self.blockchain.get_block_records_in_range(curr_rec.height - batch_size, curr_rec.height)
end = curr_rec.height
# TODO: address hint error and remove ignore
# error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, BlockRecord]"; expected type
# "bytes32" [index]
curr_rec = blocks[self.blockchain.height_to_hash(uint32(curr_rec.height - 1))] # type: ignore[index]
header_hash = self.blockchain.height_to_hash(uint32(curr_rec.height - 1))
assert header_hash is not None
curr_rec = blocks[header_hash]
return curr_rec.height
async def _create_challenge_segment(
@ -447,10 +437,9 @@ class WeightProofHandler:
curr.total_iters,
)
tmp_sub_slots_data.append(ssd)
# TODO: address hint error and remove ignore
# error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, HeaderBlock]"; expected type
# "bytes32" [index]
curr = header_blocks[self.blockchain.height_to_hash(uint32(curr.height + 1))] # type: ignore[index]
header_hash = self.blockchain.height_to_hash(uint32(curr.height + 1))
assert header_hash is not None
curr = header_blocks[header_hash]
if len(tmp_sub_slots_data) > 0:
sub_slots_data.extend(tmp_sub_slots_data)
@ -479,10 +468,9 @@ class WeightProofHandler:
) -> Tuple[Optional[List[SubSlotData]], uint32]:
# gets all vdfs first sub slot after challenge block to last sub slot
log.debug(f"slot end vdf start height {start_height}")
# TODO: address hint error and remove ignore
# error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, HeaderBlock]"; expected type "bytes32"
# [index]
curr = header_blocks[self.blockchain.height_to_hash(start_height)] # type: ignore[index]
header_hash = self.blockchain.height_to_hash(start_height)
assert header_hash is not None
curr = header_blocks[header_hash]
curr_header_hash = curr.header_hash
sub_slots_data: List[SubSlotData] = []
tmp_sub_slots_data: List[SubSlotData] = []
@ -500,11 +488,9 @@ class WeightProofHandler:
sub_slots_data.append(handle_end_of_slot(sub_slot, eos_vdf_iters))
tmp_sub_slots_data = []
tmp_sub_slots_data.append(self.handle_block_vdfs(curr, blocks))
# TODO: address hint error and remove ignore
# error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, HeaderBlock]"; expected type
# "bytes32" [index]
curr = header_blocks[self.blockchain.height_to_hash(uint32(curr.height + 1))] # type: ignore[index]
header_hash = self.blockchain.height_to_hash(uint32(curr.height + 1))
assert header_hash is not None
curr = header_blocks[header_hash]
curr_header_hash = curr.header_hash
if len(tmp_sub_slots_data) > 0:
@ -619,30 +605,42 @@ class WeightProofHandler:
log.error("failed weight proof sub epoch sample validation")
return False, uint32(0), []
executor = ProcessPoolExecutor(1)
executor = ProcessPoolExecutor(4)
constants, summary_bytes, wp_segment_bytes, wp_recent_chain_bytes = vars_to_bytes(
self.constants, summaries, weight_proof
)
segment_validation_task = asyncio.get_running_loop().run_in_executor(
executor, _validate_sub_epoch_segments, constants, rng, wp_segment_bytes, summary_bytes
)
recent_blocks_validation_task = asyncio.get_running_loop().run_in_executor(
executor, _validate_recent_blocks, constants, wp_recent_chain_bytes, summary_bytes
)
valid_segment_task = segment_validation_task
segments_validated, vdfs_to_validate = _validate_sub_epoch_segments(
constants, rng, wp_segment_bytes, summary_bytes
)
if not segments_validated:
return False, uint32(0), []
vdf_chunks = chunks(vdfs_to_validate, self._num_processes)
vdf_tasks = []
for chunk in vdf_chunks:
byte_chunks = []
for vdf_proof, classgroup, vdf_info in chunk:
byte_chunks.append((bytes(vdf_proof), bytes(classgroup), bytes(vdf_info)))
vdf_task = asyncio.get_running_loop().run_in_executor(executor, _validate_vdf_batch, constants, byte_chunks)
vdf_tasks.append(vdf_task)
for vdf_task in vdf_tasks:
validated = await vdf_task
if not validated:
return False, uint32(0), []
valid_recent_blocks_task = recent_blocks_validation_task
valid_recent_blocks = await valid_recent_blocks_task
if not valid_recent_blocks:
log.error("failed validating weight proof recent blocks")
return False, uint32(0), []
valid_segments = await valid_segment_task
if not valid_segments:
log.error("failed validating weight proof sub epoch segments")
return False, uint32(0), []
return True, self.get_fork_point(summaries), summaries
def get_fork_point(self, received_summaries: List[SubEpochSummary]) -> uint32:
@ -837,6 +835,11 @@ def handle_end_of_slot(
)
def chunks(some_list, chunk_size):
chunk_size = max(1, chunk_size)
return (some_list[i : i + chunk_size] for i in range(0, len(some_list), chunk_size))
def compress_segments(full_segment_index, segments: List[SubEpochChallengeSegment]) -> List[SubEpochChallengeSegment]:
compressed_segments = []
compressed_segments.append(segments[0])
@ -961,6 +964,7 @@ def _validate_sub_epoch_segments(
prev_ses: Optional[SubEpochSummary] = None
segments_by_sub_epoch = map_segments_by_sub_epoch(sub_epoch_segments.challenge_segments)
curr_ssi = constants.SUB_SLOT_ITERS_STARTING
vdfs_to_validate = []
for sub_epoch_n, segments in segments_by_sub_epoch.items():
prev_ssi = curr_ssi
curr_difficulty, curr_ssi = _get_curr_diff_ssi(constants, sub_epoch_n, summaries)
@ -975,9 +979,10 @@ def _validate_sub_epoch_segments(
log.error(f"failed reward_chain_hash validation sub_epoch {sub_epoch_n}")
return False
for idx, segment in enumerate(segments):
valid_segment, ip_iters, slot_iters, slots = _validate_segment(
valid_segment, ip_iters, slot_iters, slots, vdf_list = _validate_segment(
constants, segment, curr_ssi, prev_ssi, curr_difficulty, prev_ses, idx == 0, sampled_seg_index == idx
)
vdfs_to_validate.extend(vdf_list)
if not valid_segment:
log.error(f"failed to validate sub_epoch {segment.sub_epoch_n} segment {idx} slots")
return False
@ -986,7 +991,7 @@ def _validate_sub_epoch_segments(
total_slot_iters += slot_iters
total_slots += slots
total_ip_iters += ip_iters
return True
return True, vdfs_to_validate
def _validate_segment(
@ -998,37 +1003,40 @@ def _validate_segment(
ses: Optional[SubEpochSummary],
first_segment_in_se: bool,
sampled: bool,
) -> Tuple[bool, int, int, int]:
) -> Tuple[bool, int, int, int, List[Tuple[VDFProof, ClassgroupElement, VDFInfo]]]:
ip_iters, slot_iters, slots = 0, 0, 0
after_challenge = False
to_validate = []
for idx, sub_slot_data in enumerate(segment.sub_slots):
if sampled and sub_slot_data.is_challenge():
after_challenge = True
required_iters = __validate_pospace(constants, segment, idx, curr_difficulty, ses, first_segment_in_se)
if required_iters is None:
return False, uint64(0), uint64(0), uint64(0)
return False, uint64(0), uint64(0), uint64(0), []
assert sub_slot_data.signage_point_index is not None
ip_iters = ip_iters + calculate_ip_iters(
constants, curr_ssi, sub_slot_data.signage_point_index, required_iters
)
if not _validate_challenge_block_vdfs(constants, idx, segment.sub_slots, curr_ssi):
log.error(f"failed to validate challenge slot {idx} vdfs")
return False, uint64(0), uint64(0), uint64(0)
vdf_list = _get_challenge_block_vdfs(constants, idx, segment.sub_slots, curr_ssi)
to_validate.extend(vdf_list)
elif sampled and after_challenge:
if not _validate_sub_slot_data(constants, idx, segment.sub_slots, curr_ssi):
validated, vdf_list = _validate_sub_slot_data(constants, idx, segment.sub_slots, curr_ssi)
if not validated:
log.error(f"failed to validate sub slot data {idx} vdfs")
return False, uint64(0), uint64(0), uint64(0)
return False, uint64(0), uint64(0), uint64(0), []
to_validate.extend(vdf_list)
slot_iters = slot_iters + curr_ssi
slots = slots + uint64(1)
return True, ip_iters, slot_iters, slots
return True, ip_iters, slot_iters, slots, to_validate
def _validate_challenge_block_vdfs(
def _get_challenge_block_vdfs(
constants: ConsensusConstants,
sub_slot_idx: int,
sub_slots: List[SubSlotData],
ssi: uint64,
) -> bool:
) -> List[Tuple[VDFProof, ClassgroupElement, VDFInfo]]:
to_validate = []
sub_slot_data = sub_slots[sub_slot_idx]
if sub_slot_data.cc_signage_point is not None and sub_slot_data.cc_sp_vdf_info:
assert sub_slot_data.signage_point_index
@ -1039,9 +1047,8 @@ def _validate_challenge_block_vdfs(
sp_input = sub_slot_data_vdf_input(
constants, sub_slot_data, sub_slot_idx, sub_slots, is_overflow, prev_ssd.is_end_of_slot(), ssi
)
if not sub_slot_data.cc_signage_point.is_valid(constants, sp_input, sub_slot_data.cc_sp_vdf_info):
log.error(f"failed to validate challenge chain signage point 2 {sub_slot_data.cc_sp_vdf_info}")
return False
to_validate.append((sub_slot_data.cc_signage_point, sp_input, sub_slot_data.cc_sp_vdf_info))
assert sub_slot_data.cc_infusion_point
assert sub_slot_data.cc_ip_vdf_info
ip_input = ClassgroupElement.get_default_element()
@ -1057,10 +1064,9 @@ def _validate_challenge_block_vdfs(
cc_ip_vdf_info = VDFInfo(
sub_slot_data.cc_ip_vdf_info.challenge, ip_vdf_iters, sub_slot_data.cc_ip_vdf_info.output
)
if not sub_slot_data.cc_infusion_point.is_valid(constants, ip_input, cc_ip_vdf_info):
log.error(f"failed to validate challenge chain infusion point {sub_slot_data.cc_ip_vdf_info}")
return False
return True
to_validate.append((sub_slot_data.cc_infusion_point, ip_input, cc_ip_vdf_info))
return to_validate
def _validate_sub_slot_data(
@ -1068,10 +1074,12 @@ def _validate_sub_slot_data(
sub_slot_idx: int,
sub_slots: List[SubSlotData],
ssi: uint64,
) -> bool:
) -> Tuple[bool, List[Tuple[VDFProof, ClassgroupElement, VDFInfo]]]:
sub_slot_data = sub_slots[sub_slot_idx]
assert sub_slot_idx > 0
prev_ssd = sub_slots[sub_slot_idx - 1]
to_validate = []
if sub_slot_data.is_end_of_slot():
if sub_slot_data.icc_slot_end is not None:
input = ClassgroupElement.get_default_element()
@ -1079,9 +1087,7 @@ def _validate_sub_slot_data(
assert prev_ssd.icc_ip_vdf_info
input = prev_ssd.icc_ip_vdf_info.output
assert sub_slot_data.icc_slot_end_info
if not sub_slot_data.icc_slot_end.is_valid(constants, input, sub_slot_data.icc_slot_end_info, None):
log.error(f"failed icc slot end validation {sub_slot_data.icc_slot_end_info} ")
return False
to_validate.append((sub_slot_data.icc_slot_end, input, sub_slot_data.icc_slot_end_info))
assert sub_slot_data.cc_slot_end_info
assert sub_slot_data.cc_slot_end
input = ClassgroupElement.get_default_element()
@ -1090,7 +1096,7 @@ def _validate_sub_slot_data(
input = prev_ssd.cc_ip_vdf_info.output
if not sub_slot_data.cc_slot_end.is_valid(constants, input, sub_slot_data.cc_slot_end_info):
log.error(f"failed cc slot end validation {sub_slot_data.cc_slot_end_info}")
return False
return False, []
else:
# find end of slot
idx = sub_slot_idx
@ -1101,7 +1107,7 @@ def _validate_sub_slot_data(
assert curr_slot.cc_slot_end
if curr_slot.cc_slot_end.normalized_to_identity is True:
log.debug(f"skip intermediate vdfs slot {sub_slot_idx}")
return True
return True, to_validate
else:
break
idx += 1
@ -1109,10 +1115,7 @@ def _validate_sub_slot_data(
input = ClassgroupElement.get_default_element()
if not prev_ssd.is_challenge() and prev_ssd.icc_ip_vdf_info is not None:
input = prev_ssd.icc_ip_vdf_info.output
if not sub_slot_data.icc_infusion_point.is_valid(constants, input, sub_slot_data.icc_ip_vdf_info, None):
log.error(f"failed icc infusion point vdf validation {sub_slot_data.icc_slot_end_info} ")
return False
to_validate.append((sub_slot_data.icc_infusion_point, input, sub_slot_data.icc_ip_vdf_info))
assert sub_slot_data.signage_point_index is not None
if sub_slot_data.cc_signage_point:
assert sub_slot_data.cc_sp_vdf_info
@ -1122,10 +1125,8 @@ def _validate_sub_slot_data(
input = sub_slot_data_vdf_input(
constants, sub_slot_data, sub_slot_idx, sub_slots, is_overflow, prev_ssd.is_end_of_slot(), ssi
)
to_validate.append((sub_slot_data.cc_signage_point, input, sub_slot_data.cc_sp_vdf_info))
if not sub_slot_data.cc_signage_point.is_valid(constants, input, sub_slot_data.cc_sp_vdf_info):
log.error(f"failed cc signage point vdf validation {sub_slot_data.cc_sp_vdf_info}")
return False
input = ClassgroupElement.get_default_element()
assert sub_slot_data.cc_ip_vdf_info
assert sub_slot_data.cc_infusion_point
@ -1139,10 +1140,9 @@ def _validate_sub_slot_data(
cc_ip_vdf_info = VDFInfo(
sub_slot_data.cc_ip_vdf_info.challenge, ip_vdf_iters, sub_slot_data.cc_ip_vdf_info.output
)
if not sub_slot_data.cc_infusion_point.is_valid(constants, input, cc_ip_vdf_info):
log.error(f"failed cc infusion point vdf validation {sub_slot_data.cc_slot_end_info}")
return False
return True
to_validate.append((sub_slot_data.cc_infusion_point, input, cc_ip_vdf_info))
return True, to_validate
def sub_slot_data_vdf_input(
@ -1203,14 +1203,17 @@ def sub_slot_data_vdf_input(
return cc_input
def _validate_recent_blocks(constants_dict: Dict, recent_chain_bytes: bytes, summaries_bytes: List[bytes]) -> bool:
constants, summaries = bytes_to_vars(constants_dict, summaries_bytes)
recent_chain: RecentChainData = RecentChainData.from_bytes(recent_chain_bytes)
def validate_recent_blocks(
constants: ConsensusConstants,
recent_chain: RecentChainData,
summaries: List[SubEpochSummary],
shutdown_file_path: Optional[pathlib.Path] = None,
) -> Tuple[bool, List[bytes]]:
sub_blocks = BlockCache({})
first_ses_idx = _get_ses_idx(recent_chain.recent_chain_data)
ses_idx = len(summaries) - len(first_ses_idx)
ssi: uint64 = constants.SUB_SLOT_ITERS_STARTING
diff: Optional[uint64] = constants.DIFFICULTY_STARTING
diff: uint64 = constants.DIFFICULTY_STARTING
last_blocks_to_validate = 100 # todo remove cap after benchmarks
for summary in summaries[:ses_idx]:
if summary.new_sub_slot_iters is not None:
@ -1219,10 +1222,11 @@ def _validate_recent_blocks(constants_dict: Dict, recent_chain_bytes: bytes, sum
diff = summary.new_difficulty
ses_blocks, sub_slots, transaction_blocks = 0, 0, 0
challenge, prev_challenge = None, None
challenge, prev_challenge = recent_chain.recent_chain_data[0].reward_chain_block.pos_ss_cc_challenge_hash, None
tip_height = recent_chain.recent_chain_data[-1].height
prev_block_record = None
deficit = uint8(0)
adjusted = False
for idx, block in enumerate(recent_chain.recent_chain_data):
required_iters = uint64(0)
overflow = False
@ -1243,21 +1247,30 @@ def _validate_recent_blocks(constants_dict: Dict, recent_chain_bytes: bytes, sum
if (challenge is not None) and (prev_challenge is not None):
overflow = is_overflow_block(constants, block.reward_chain_block.signage_point_index)
if not adjusted:
prev_block_record = dataclasses.replace(
prev_block_record, deficit=deficit % constants.MIN_BLOCKS_PER_CHALLENGE_BLOCK
)
assert prev_block_record is not None
sub_blocks.add_block_record(prev_block_record)
adjusted = True
deficit = get_deficit(constants, deficit, prev_block_record, overflow, len(block.finished_sub_slots))
log.debug(f"wp, validate block {block.height}")
if sub_slots > 2 and transaction_blocks > 11 and (tip_height - block.height < last_blocks_to_validate):
required_iters, error = validate_finished_header_block(
caluclated_required_iters, error = validate_finished_header_block(
constants, sub_blocks, block, False, diff, ssi, ses_blocks > 2
)
if error is not None:
log.error(f"block {block.header_hash} failed validation {error}")
return False
return False, []
assert caluclated_required_iters is not None
required_iters = caluclated_required_iters
else:
required_iters = _validate_pospace_recent_chain(
constants, block, challenge, diff, overflow, prev_challenge
)
if required_iters is None:
return False
return False, []
curr_block_ses = None if not ses else summaries[ses_idx - 1]
block_record = header_block_to_sub_block_record(
@ -1274,7 +1287,29 @@ def _validate_recent_blocks(constants_dict: Dict, recent_chain_bytes: bytes, sum
ses_blocks += 1
prev_block_record = block_record
return True
if shutdown_file_path is not None and not shutdown_file_path.is_file():
log.info(f"cancelling block {block.header_hash} validation, shutdown requested")
return False, []
return True, [bytes(sub) for sub in sub_blocks._block_records.values()]
def _validate_recent_blocks(constants_dict: Dict, recent_chain_bytes: bytes, summaries_bytes: List[bytes]) -> bool:
constants, summaries = bytes_to_vars(constants_dict, summaries_bytes)
recent_chain: RecentChainData = RecentChainData.from_bytes(recent_chain_bytes)
success, records = validate_recent_blocks(constants, recent_chain, summaries)
return success
def _validate_recent_blocks_and_get_records(
constants_dict: Dict,
recent_chain_bytes: bytes,
summaries_bytes: List[bytes],
shutdown_file_path: Optional[pathlib.Path] = None,
) -> Tuple[bool, List[bytes]]:
constants, summaries = bytes_to_vars(constants_dict, summaries_bytes)
recent_chain: RecentChainData = RecentChainData.from_bytes(recent_chain_bytes)
return validate_recent_blocks(constants, recent_chain, summaries, shutdown_file_path)
def _validate_pospace_recent_chain(
@ -1473,7 +1508,7 @@ def _get_curr_diff_ssi(constants: ConsensusConstants, idx, summaries):
return curr_difficulty, curr_ssi
def vars_to_bytes(constants, summaries, weight_proof):
def vars_to_bytes(constants: ConsensusConstants, summaries: List[SubEpochSummary], weight_proof: WeightProof):
constants_dict = recurse_jsonify(dataclasses.asdict(constants))
wp_recent_chain_bytes = bytes(RecentChainData(weight_proof.recent_chain_data))
wp_segment_bytes = bytes(SubEpochSegments(weight_proof.sub_epoch_segments))
@ -1524,13 +1559,13 @@ def _get_ses_idx(recent_reward_chain: List[HeaderBlock]) -> List[int]:
def get_deficit(
constants: ConsensusConstants,
curr_deficit: uint8,
prev_block: BlockRecord,
prev_block: Optional[BlockRecord],
overflow: bool,
num_finished_sub_slots: int,
) -> uint8:
if prev_block is None:
if curr_deficit >= 1 and not (overflow and curr_deficit == constants.MIN_BLOCKS_PER_CHALLENGE_BLOCK):
curr_deficit -= 1
curr_deficit = uint8(curr_deficit - 1)
return curr_deficit
return calculate_deficit(constants, uint32(prev_block.height + 1), prev_block, overflow, num_finished_sub_slots)
@ -1617,3 +1652,22 @@ def validate_total_iters(
total_iters = uint128(prev_b.total_iters - prev_b.cc_ip_vdf_info.number_of_iterations)
total_iters = uint128(total_iters + sub_slot_data.cc_ip_vdf_info.number_of_iterations)
return total_iters == sub_slot_data.total_iters
def _validate_vdf_batch(
constants_dict, vdf_list: List[Tuple[bytes, bytes, bytes]], shutdown_file_path: Optional[pathlib.Path] = None
):
constants: ConsensusConstants = dataclass_from_dict(ConsensusConstants, constants_dict)
for vdf_proof_bytes, class_group_bytes, info in vdf_list:
vdf = VDFProof.from_bytes(vdf_proof_bytes)
class_group = ClassgroupElement.from_bytes(class_group_bytes)
vdf_info = VDFInfo.from_bytes(info)
if not vdf.is_valid(constants, class_group, vdf_info):
return False
if shutdown_file_path is not None and not shutdown_file_path.is_file():
log.info("cancelling VDF validation, shutdown requested")
return False
return True

View File

@ -4,7 +4,6 @@ from typing import Any, Optional, Set, Tuple, List, Dict
from blspy import PrivateKey, G2Element, G1Element
from chia.consensus.block_record import BlockRecord
from chia.pools.pool_config import PoolWalletConfig, load_pool_config, update_pool_config
from chia.pools.pool_wallet_info import (
PoolWalletInfo,
@ -265,39 +264,35 @@ class PoolWallet:
return [coin.name()]
return []
async def apply_state_transitions(self, block_spends: List[CoinSpend], block_height: uint32):
async def apply_state_transitions(self, new_state: CoinSpend, block_height: uint32):
"""
Updates the Pool state (including DB) with new singleton spends. The block spends can contain many spends
that we are not interested in, and can contain many ephemeral spends. They must all be in the same block.
The DB must be committed after calling this method. All validation should be done here.
"""
coin_name_to_spend: Dict[bytes32, CoinSpend] = {cs.coin.name(): cs for cs in block_spends}
tip: Tuple[uint32, CoinSpend] = await self.get_tip()
tip_height = tip[0]
tip_spend = tip[1]
assert block_height >= tip_height # We should not have a spend with a lesser block height
while True:
tip_coin: Optional[Coin] = get_most_recent_singleton_coin_from_coin_spend(tip_spend)
assert tip_coin is not None
spent_coin_name: bytes32 = tip_coin.name()
if spent_coin_name not in coin_name_to_spend:
tip_coin: Optional[Coin] = get_most_recent_singleton_coin_from_coin_spend(tip_spend)
assert tip_coin is not None
spent_coin_name: bytes32 = tip_coin.name()
if spent_coin_name != new_state.coin.name():
self.log.warning(f"Failed to apply state transition. tip: {tip_coin} new_state: {new_state} ")
return
await self.wallet_state_manager.pool_store.add_spend(self.wallet_id, new_state, block_height)
tip_spend = (await self.get_tip())[1]
self.log.info(f"New PoolWallet singleton tip_coin: {tip_spend}")
# If we have reached the target state, resets it to None. Loops back to get current state
for _, added_spend in reversed(self.wallet_state_manager.pool_store.get_spends_for_wallet(self.wallet_id)):
latest_state: Optional[PoolState] = solution_to_pool_state(added_spend)
if latest_state is not None:
if self.target_state == latest_state:
self.target_state = None
self.next_transaction_fee = uint64(0)
break
spend: CoinSpend = coin_name_to_spend[spent_coin_name]
await self.wallet_state_manager.pool_store.add_spend(self.wallet_id, spend, block_height)
tip_spend = (await self.get_tip())[1]
self.log.info(f"New PoolWallet singleton tip_coin: {tip_spend}")
coin_name_to_spend.pop(spent_coin_name)
# If we have reached the target state, resets it to None. Loops back to get current state
for _, added_spend in reversed(self.wallet_state_manager.pool_store.get_spends_for_wallet(self.wallet_id)):
latest_state: Optional[PoolState] = solution_to_pool_state(added_spend)
if latest_state is not None:
if self.target_state == latest_state:
self.target_state = None
self.next_transaction_fee = uint64(0)
break
await self.update_pool_config(False)
async def rewind(self, block_height: int) -> bool:
@ -313,11 +308,6 @@ class PoolWallet:
await self.wallet_state_manager.pool_store.rollback(block_height, self.wallet_id)
if len(history) > 0 and history[0][0] > block_height:
# If we have no entries in the DB, we have no singleton, so we should not have a wallet either
# The PoolWallet object becomes invalid after this.
await self.wallet_state_manager.interested_store.remove_interested_puzzle_hash(
prev_state.p2_singleton_puzzle_hash, in_transaction=True
)
return True
else:
if await self.get_current_state() != prev_state:
@ -362,12 +352,8 @@ class PoolWallet:
await self.update_pool_config(True)
p2_puzzle_hash: bytes32 = (await self.get_current_state()).p2_singleton_puzzle_hash
await self.wallet_state_manager.interested_store.add_interested_puzzle_hash(
p2_puzzle_hash, self.wallet_id, True
)
await self.wallet_state_manager.add_interested_puzzle_hash(p2_puzzle_hash, self.wallet_id, False)
await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id, create_puzzle_hashes=False)
self.wallet_state_manager.set_new_peak_callback(self.wallet_id, self.new_peak)
return self
@staticmethod
@ -388,7 +374,6 @@ class PoolWallet:
self.wallet_info = wallet_info
self.target_state = None
self.log = logging.getLogger(name if name else __name__)
self.wallet_state_manager.set_new_peak_callback(self.wallet_id, self.new_peak)
return self
@staticmethod
@ -452,6 +437,7 @@ class PoolWallet:
removals=spend_bundle.removals(),
wallet_id=wallet_state_manager.main_wallet.id(),
sent_to=[],
memos=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=spend_bundle.name(),
@ -567,6 +553,7 @@ class PoolWallet:
wallet_id=self.id(),
sent_to=[],
trade_id=None,
memos=[],
type=uint32(TransactionType.OUTGOING_TX.value),
name=signed_spend_bundle.name(),
)
@ -585,7 +572,6 @@ class PoolWallet:
Creates the initial singleton, which includes spending an origin coin, the launcher, and creating a singleton
with the "pooling" inner state, which can be either self pooling or using a pool
"""
coins: Set[Coin] = await standard_wallet.select_coins(amount)
if coins is None:
raise ValueError("Not enough coins to create pool wallet")
@ -627,9 +613,9 @@ class PoolWallet:
puzzle_hash: bytes32 = full_pooling_puzzle.get_tree_hash()
pool_state_bytes = Program.to([("p", bytes(initial_target_state)), ("t", delay_time), ("h", delay_ph)])
announcement_set: Set[bytes32] = set()
announcement_set: Set[Announcement] = set()
announcement_message = Program.to([puzzle_hash, amount, pool_state_bytes]).get_tree_hash()
announcement_set.add(Announcement(launcher_coin.name(), announcement_message).name())
announcement_set.add(Announcement(launcher_coin.name(), announcement_message))
create_launcher_tx_record: Optional[TransactionRecord] = await standard_wallet.generate_signed_transaction(
amount,
@ -803,6 +789,7 @@ class PoolWallet:
removals=spend_bundle.removals(),
wallet_id=uint32(self.wallet_id),
sent_to=[],
memos=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=spend_bundle.name(),
@ -810,7 +797,7 @@ class PoolWallet:
await self.wallet_state_manager.add_pending_transaction(absorb_transaction)
return absorb_transaction
async def new_peak(self, peak: BlockRecord) -> None:
async def new_peak(self, peak_height: uint64) -> None:
# This gets called from the WalletStateManager whenever there is a new peak
pool_wallet_info: PoolWalletInfo = await self.get_current_state()
@ -828,14 +815,8 @@ class PoolWallet:
):
leave_height = tip_height + pool_wallet_info.current.relative_lock_height
curr: BlockRecord = peak
while not curr.is_transaction_block:
curr = self.wallet_state_manager.blockchain.block_record(curr.prev_hash)
self.log.info(f"Last transaction block height: {curr.height} OK to leave at height {leave_height}")
# Add some buffer (+2) to reduce chances of a reorg
if curr.height > leave_height + 2:
if peak_height > leave_height + 2:
unconfirmed: List[
TransactionRecord
] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.wallet_id)

View File

@ -556,10 +556,8 @@ class FullNodeRpcApi:
raise ValueError(f"Invalid height {height}. coin record {coin_record}")
header_hash = self.service.blockchain.height_to_hash(height)
# TODO: address hint error and remove ignore
# error: Argument 1 to "get_full_block" of "BlockStore" has incompatible type "Optional[bytes32]";
# expected "bytes32" [arg-type]
block: Optional[FullBlock] = await self.service.block_store.get_full_block(header_hash) # type: ignore[arg-type] # noqa: E501
assert header_hash is not None
block: Optional[FullBlock] = await self.service.block_store.get_full_block(header_hash)
if block is None or block.transactions_generator is None:
raise ValueError("Invalid block or block generator")

View File

@ -1,10 +1,10 @@
import asyncio
import logging
import time
from pathlib import Path
from typing import Callable, Dict, List, Optional, Tuple, Set
from typing import Callable, Dict, List, Optional, Tuple, Set, Any
from blspy import PrivateKey, G1Element
from clvm_tools import binutils
from chia.consensus.block_rewards import calculate_base_farmer_reward
from chia.pools.pool_wallet import PoolWallet
@ -12,6 +12,8 @@ from chia.pools.pool_wallet_info import create_pool_state, FARMING_TO_POOL, Pool
from chia.protocols.protocol_message_types import ProtocolMessageTypes
from chia.server.outbound_message import NodeType, make_msg
from chia.simulator.simulator_protocol import FarmNewBlockProtocol
from chia.types.announcement import Announcement
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash
@ -20,15 +22,15 @@ from chia.util.ints import uint32, uint64
from chia.util.keychain import KeyringIsLocked, bytes_to_mnemonic, generate_mnemonic
from chia.util.path import path_from_root
from chia.util.ws_message import WsRpcMessage, create_payload_dict
from chia.wallet.cc_wallet.cc_wallet import CCWallet
from chia.wallet.derive_keys import master_sk_to_singleton_owner_sk
from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS
from chia.wallet.cat_wallet.cat_wallet import CATWallet
from chia.wallet.derive_keys import master_sk_to_singleton_owner_sk, master_sk_to_wallet_sk_unhardened
from chia.wallet.rl_wallet.rl_wallet import RLWallet
from chia.wallet.derive_keys import master_sk_to_farmer_sk, master_sk_to_pool_sk, master_sk_to_wallet_sk
from chia.wallet.did_wallet.did_wallet import DIDWallet
from chia.wallet.trade_record import TradeRecord
from chia.wallet.trading.offer import Offer
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.backup_utils import download_backup, get_backup_info, upload_backup
from chia.wallet.util.trade_utils import trade_record_to_dict
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.wallet_types import AmountWithPuzzlehash, WalletType
from chia.wallet.wallet_info import WalletInfo
@ -47,6 +49,7 @@ class WalletRpcApi:
assert wallet_node is not None
self.service = wallet_node
self.service_name = "chia_wallet"
self.balance_cache: Dict[int, Any] = {}
def get_routes(self) -> Dict[str, Callable]:
return {
@ -75,25 +78,27 @@ class WalletRpcApi:
"/get_wallet_balance": self.get_wallet_balance,
"/get_transaction": self.get_transaction,
"/get_transactions": self.get_transactions,
"/get_transaction_count": self.get_transaction_count,
"/get_next_address": self.get_next_address,
"/send_transaction": self.send_transaction,
"/send_transaction_multi": self.send_transaction_multi,
"/create_backup": self.create_backup,
"/get_transaction_count": self.get_transaction_count,
"/get_farmed_amount": self.get_farmed_amount,
"/create_signed_transaction": self.create_signed_transaction,
"/delete_unconfirmed_transactions": self.delete_unconfirmed_transactions,
# Coloured coins and trading
"/cc_set_name": self.cc_set_name,
"/cc_get_name": self.cc_get_name,
"/cc_spend": self.cc_spend,
"/cc_get_colour": self.cc_get_colour,
"/cat_set_name": self.cat_set_name,
"/cat_asset_id_to_name": self.cat_asset_id_to_name,
"/cat_get_name": self.cat_get_name,
"/cat_spend": self.cat_spend,
"/cat_get_asset_id": self.cat_get_asset_id,
"/create_offer_for_ids": self.create_offer_for_ids,
"/get_discrepancies_for_offer": self.get_discrepancies_for_offer,
"/respond_to_offer": self.respond_to_offer,
"/get_trade": self.get_trade,
"/get_all_trades": self.get_all_trades,
"/cancel_trade": self.cancel_trade,
"/get_offer_summary": self.get_offer_summary,
"/check_offer_validity": self.check_offer_validity,
"/take_offer": self.take_offer,
"/get_offer": self.get_offer,
"/get_all_offers": self.get_all_offers,
"/cancel_offer": self.cancel_offer,
"/get_cat_list": self.get_cat_list,
# DID Wallet
"/did_update_recovery_ids": self.did_update_recovery_ids,
"/did_get_pubkey": self.did_get_pubkey,
@ -138,7 +143,9 @@ class WalletRpcApi:
"""
if self.service is not None:
self.service._close()
await self.service._await_closed()
peers_close_task: Optional[asyncio.Task] = await self.service._await_closed()
if peers_close_task is not None:
await peers_close_task
##########################################################################################
# Key management
@ -154,45 +161,9 @@ class WalletRpcApi:
return {"fingerprint": fingerprint}
await self._stop_wallet()
log_in_type = request["type"]
recovery_host = request["host"]
testing = False
if "testing" in self.service.config and self.service.config["testing"] is True:
testing = True
if log_in_type == "skip":
started = await self.service._start(fingerprint=fingerprint, skip_backup_import=True)
elif log_in_type == "restore_backup":
file_path = Path(request["file_path"])
started = await self.service._start(fingerprint=fingerprint, backup_file=file_path)
else:
started = await self.service._start(fingerprint)
started = await self.service._start(fingerprint)
if started is True:
return {"fingerprint": fingerprint}
elif testing is True and self.service.backup_initialized is False:
response = {"success": False, "error": "not_initialized"}
return response
elif self.service.backup_initialized is False:
backup_info = None
backup_path = None
try:
private_key = await self.service.get_key_for_fingerprint(fingerprint)
last_recovery = await download_backup(recovery_host, private_key)
backup_path = path_from_root(self.service.root_path, "last_recovery")
if backup_path.exists():
backup_path.unlink()
backup_path.write_text(last_recovery)
backup_info = get_backup_info(backup_path, private_key)
backup_info["backup_host"] = recovery_host
backup_info["downloaded"] = True
except Exception as e:
log.error(f"error {e}")
response = {"success": False, "error": "not_initialized"}
if backup_info is not None:
response["backup_info"] = backup_info
response["backup_path"] = f"{backup_path}"
return response
return {"success": False, "error": "Unknown Error"}
@ -270,15 +241,7 @@ class WalletRpcApi:
await self.service.keychain_proxy.check_keys(self.service.root_path)
except Exception as e:
log.error(f"Failed to check_keys after adding a new key: {e}")
request_type = request["type"]
if request_type == "new_wallet":
started = await self.service._start(fingerprint=fingerprint, new_wallet=True)
elif request_type == "skip":
started = await self.service._start(fingerprint=fingerprint, skip_backup_import=True)
elif request_type == "restore_backup":
file_path = Path(request["file_path"])
started = await self.service._start(fingerprint=fingerprint, backup_file=file_path)
started = await self.service._start(fingerprint=fingerprint)
if started is True:
return {"fingerprint": fingerprint}
raise ValueError("Failed to start")
@ -322,12 +285,17 @@ class WalletRpcApi:
if found_farmer and found_pool:
break
ph = encode_puzzle_hash(create_puzzlehash_for_pk(master_sk_to_wallet_sk(sk, uint32(i)).get_g1()), prefix)
if ph == farmer_target:
found_farmer = True
if ph == pool_target:
found_pool = True
phs = [
encode_puzzle_hash(create_puzzlehash_for_pk(master_sk_to_wallet_sk(sk, uint32(i)).get_g1()), prefix),
encode_puzzle_hash(
create_puzzlehash_for_pk(master_sk_to_wallet_sk_unhardened(sk, uint32(i)).get_g1()), prefix
),
]
for ph in phs:
if ph == farmer_target:
found_farmer = True
if ph == pool_target:
found_pool = True
return found_farmer, found_pool
@ -347,19 +315,18 @@ class WalletRpcApi:
if self.service.logged_in_fingerprint != fingerprint:
await self._stop_wallet()
await self.service._start(fingerprint=fingerprint, skip_backup_import=True)
await self.service._start(fingerprint=fingerprint)
async with self.service.wallet_state_manager.lock:
wallets: List[WalletInfo] = await self.service.wallet_state_manager.get_all_wallet_info_entries()
for w in wallets:
wallet = self.service.wallet_state_manager.wallets[w.id]
unspent = await self.service.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(w.id)
balance = await wallet.get_confirmed_balance(unspent)
pending_balance = await wallet.get_unconfirmed_balance(unspent)
wallets: List[WalletInfo] = await self.service.wallet_state_manager.get_all_wallet_info_entries()
for w in wallets:
wallet = self.service.wallet_state_manager.wallets[w.id]
unspent = await self.service.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(w.id)
balance = await wallet.get_confirmed_balance(unspent)
pending_balance = await wallet.get_unconfirmed_balance(unspent)
if (balance + pending_balance) > 0:
walletBalance = True
break
if (balance + pending_balance) > 0:
walletBalance = True
break
return {
"fingerprint": fingerprint,
@ -393,11 +360,8 @@ class WalletRpcApi:
async def get_height_info(self, request: Dict):
assert self.service.wallet_state_manager is not None
peak = self.service.wallet_state_manager.peak
if peak is None:
return {"height": 0}
else:
return {"height": peak.height}
height = self.service.wallet_state_manager.blockchain.get_peak_height()
return {"height": height}
async def get_network_info(self, request: Dict):
assert self.service.wallet_state_manager is not None
@ -424,53 +388,37 @@ class WalletRpcApi:
return {"wallets": wallets}
async def _create_backup_and_upload(self, host) -> None:
assert self.service.wallet_state_manager is not None
try:
if "testing" in self.service.config and self.service.config["testing"] is True:
return None
now = time.time()
file_name = f"backup_{now}"
path = path_from_root(self.service.root_path, file_name)
await self.service.wallet_state_manager.create_wallet_backup(path)
backup_text = path.read_text()
response = await upload_backup(host, backup_text)
success = response["success"]
if success is False:
log.error("Failed to upload backup to wallet backup service")
elif success is True:
log.info("Finished upload of the backup file")
except Exception as e:
log.error(f"Exception in upload backup. Error: {e}")
async def create_new_wallet(self, request: Dict):
assert self.service.wallet_state_manager is not None
wallet_state_manager = self.service.wallet_state_manager
if await self.service.wallet_state_manager.synced() is False:
raise ValueError("Wallet needs to be fully synced.")
main_wallet = wallet_state_manager.main_wallet
host = request["host"]
fee = uint64(request.get("fee", 0))
if request["wallet_type"] == "cc_wallet":
if request["wallet_type"] == "cat_wallet":
name = request.get("name", "CAT Wallet")
if request["mode"] == "new":
async with self.service.wallet_state_manager.lock:
cc_wallet: CCWallet = await CCWallet.create_new_cc(
wallet_state_manager, main_wallet, uint64(request["amount"])
cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet(
wallet_state_manager,
main_wallet,
{"identifier": "genesis_by_id"},
uint64(request["amount"]),
name,
)
colour = cc_wallet.get_colour()
asyncio.create_task(self._create_backup_and_upload(host))
return {
"type": cc_wallet.type(),
"colour": colour,
"wallet_id": cc_wallet.id(),
}
asset_id = cat_wallet.get_asset_id()
self.service.wallet_state_manager.state_changed("wallet_created")
return {"type": cat_wallet.type(), "asset_id": asset_id, "wallet_id": cat_wallet.id()}
elif request["mode"] == "existing":
async with self.service.wallet_state_manager.lock:
cc_wallet = await CCWallet.create_wallet_for_cc(
wallet_state_manager, main_wallet, request["colour"]
cat_wallet = await CATWallet.create_wallet_for_cat(
wallet_state_manager, main_wallet, request["asset_id"]
)
asyncio.create_task(self._create_backup_and_upload(host))
return {"type": cc_wallet.type()}
self.service.wallet_state_manager.state_changed("wallet_created")
return {"type": cat_wallet.type(), "asset_id": request["asset_id"], "wallet_id": cat_wallet.id()}
else: # undefined mode
pass
@ -487,7 +435,6 @@ class WalletRpcApi:
uint64(int(request["amount"])),
uint64(int(request["fee"])) if "fee" in request else uint64(0),
)
asyncio.create_task(self._create_backup_and_upload(host))
assert rl_admin.rl_info.admin_pubkey is not None
return {
"success": success,
@ -501,7 +448,6 @@ class WalletRpcApi:
log.info("Create rl user wallet")
async with self.service.wallet_state_manager.lock:
rl_user: RLWallet = await RLWallet.create_rl_user(wallet_state_manager)
asyncio.create_task(self._create_backup_and_upload(host))
assert rl_user.rl_info.user_pubkey is not None
return {
"id": rl_user.id(),
@ -623,28 +569,48 @@ class WalletRpcApi:
assert self.service.wallet_state_manager is not None
wallet_id = uint32(int(request["wallet_id"]))
wallet = self.service.wallet_state_manager.wallets[wallet_id]
async with self.service.wallet_state_manager.lock:
unspent_records = await self.service.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(wallet_id)
balance = await wallet.get_confirmed_balance(unspent_records)
pending_balance = await wallet.get_unconfirmed_balance(unspent_records)
spendable_balance = await wallet.get_spendable_balance(unspent_records)
pending_change = await wallet.get_pending_change_balance()
max_send_amount = await wallet.get_max_send_amount(unspent_records)
unconfirmed_removals: Dict[
bytes32, Coin
] = await wallet.wallet_state_manager.unconfirmed_removals_for_wallet(wallet_id)
# If syncing return the last available info or 0s
syncing = self.service.wallet_state_manager.sync_mode
if syncing:
if wallet_id in self.balance_cache:
wallet_balance = self.balance_cache[wallet_id]
else:
wallet_balance = {
"wallet_id": wallet_id,
"confirmed_wallet_balance": 0,
"unconfirmed_wallet_balance": 0,
"spendable_balance": 0,
"pending_change": 0,
"max_send_amount": 0,
"unspent_coin_count": 0,
"pending_coin_removal_count": 0,
}
else:
async with self.service.wallet_state_manager.lock:
unspent_records = await self.service.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(
wallet_id
)
balance = await wallet.get_confirmed_balance(unspent_records)
pending_balance = await wallet.get_unconfirmed_balance(unspent_records)
spendable_balance = await wallet.get_spendable_balance(unspent_records)
pending_change = await wallet.get_pending_change_balance()
max_send_amount = await wallet.get_max_send_amount(unspent_records)
wallet_balance = {
"wallet_id": wallet_id,
"confirmed_wallet_balance": balance,
"unconfirmed_wallet_balance": pending_balance,
"spendable_balance": spendable_balance,
"pending_change": pending_change,
"max_send_amount": max_send_amount,
"unspent_coin_count": len(unspent_records),
"pending_coin_removal_count": len(unconfirmed_removals),
}
unconfirmed_removals: Dict[
bytes32, Coin
] = await wallet.wallet_state_manager.unconfirmed_removals_for_wallet(wallet_id)
wallet_balance = {
"wallet_id": wallet_id,
"confirmed_wallet_balance": balance,
"unconfirmed_wallet_balance": pending_balance,
"spendable_balance": spendable_balance,
"pending_change": pending_change,
"max_send_amount": max_send_amount,
"unspent_coin_count": len(unspent_records),
"pending_coin_removal_count": len(unconfirmed_removals),
}
self.balance_cache[wallet_id] = wallet_balance
return {"wallet_balance": wallet_balance}
@ -656,7 +622,7 @@ class WalletRpcApi:
raise ValueError(f"Transaction 0x{transaction_id.hex()} not found")
return {
"transaction": tr,
"transaction": tr.to_json_dict_convenience(self.service.config),
"transaction_id": tr.name,
}
@ -673,15 +639,18 @@ class WalletRpcApi:
transactions = await self.service.wallet_state_manager.tx_store.get_transactions_between(
wallet_id, start, end, sort_key=sort_key, reverse=reverse
)
formatted_transactions = []
selected = self.service.config["selected_network"]
prefix = self.service.config["network_overrides"]["config"][selected]["address_prefix"]
for tx in transactions:
formatted = tx.to_json_dict()
formatted["to_address"] = encode_puzzle_hash(tx.to_puzzle_hash, prefix)
formatted_transactions.append(formatted)
return {
"transactions": formatted_transactions,
"transactions": [tr.to_json_dict_convenience(self.service.config) for tr in transactions],
"wallet_id": wallet_id,
}
async def get_transaction_count(self, request: Dict) -> Dict:
assert self.service.wallet_state_manager is not None
wallet_id = int(request["wallet_id"])
count = await self.service.wallet_state_manager.tx_store.get_transaction_count_for_wallet(wallet_id)
return {
"count": count,
"wallet_id": wallet_id,
}
@ -708,8 +677,8 @@ class WalletRpcApi:
if wallet.type() == WalletType.STANDARD_WALLET:
raw_puzzle_hash = await wallet.get_puzzle_hash(create_new)
address = encode_puzzle_hash(raw_puzzle_hash, prefix)
elif wallet.type() == WalletType.COLOURED_COIN:
raw_puzzle_hash = await wallet.get_puzzle_hash(create_new)
elif wallet.type() == WalletType.CAT:
raw_puzzle_hash = await wallet.standard_wallet.get_puzzle_hash(create_new)
address = encode_puzzle_hash(raw_puzzle_hash, prefix)
else:
raise ValueError(f"Wallet type {wallet.type()} cannot create puzzle hashes")
@ -728,25 +697,33 @@ class WalletRpcApi:
wallet_id = int(request["wallet_id"])
wallet = self.service.wallet_state_manager.wallets[wallet_id]
if wallet.type() == WalletType.CAT:
raise ValueError("send_transaction does not work for CAT wallets")
if not isinstance(request["amount"], int) or not isinstance(request["fee"], int):
raise ValueError("An integer amount or fee is required (too many decimals)")
amount: uint64 = uint64(request["amount"])
puzzle_hash: bytes32 = decode_puzzle_hash(request["address"])
memos: List[bytes] = []
if "memos" in request:
memos = [mem.encode("utf-8") for mem in request["memos"]]
if "fee" in request:
fee = uint64(request["fee"])
else:
fee = uint64(0)
async with self.service.wallet_state_manager.lock:
tx: TransactionRecord = await wallet.generate_signed_transaction(amount, puzzle_hash, fee)
tx: TransactionRecord = await wallet.generate_signed_transaction(amount, puzzle_hash, fee, memos=memos)
await wallet.push_transaction(tx)
# Transaction may not have been included in the mempool yet. Use get_transaction to check.
return {
"transaction": tx,
"transaction": tx.to_json_dict_convenience(self.service.config),
"transaction_id": tx.name,
}
async def send_transaction_multi(self, request):
async def send_transaction_multi(self, request) -> Dict:
assert self.service.wallet_state_manager is not None
if await self.service.wallet_state_manager.synced() is False:
@ -756,21 +733,20 @@ class WalletRpcApi:
wallet = self.service.wallet_state_manager.wallets[wallet_id]
async with self.service.wallet_state_manager.lock:
transaction: TransactionRecord = (await self.create_signed_transaction(request, hold_lock=False))[
"signed_tx"
]
await wallet.push_transaction(transaction)
transaction: Dict = (await self.create_signed_transaction(request, hold_lock=False))["signed_tx"]
tr: TransactionRecord = TransactionRecord.from_json_dict_convenience(transaction)
await wallet.push_transaction(tr)
# Transaction may not have been included in the mempool yet. Use get_transaction to check.
return {
"transaction": transaction,
"transaction_id": transaction.name,
}
return {"transaction": transaction, "transaction_id": tr.name}
async def delete_unconfirmed_transactions(self, request):
wallet_id = uint32(request["wallet_id"])
if wallet_id not in self.service.wallet_state_manager.wallets:
raise ValueError(f"Wallet id {wallet_id} does not exist")
if await self.service.wallet_state_manager.synced() is False:
raise ValueError("Wallet needs to be fully synced.")
async with self.service.wallet_state_manager.lock:
async with self.service.wallet_state_manager.tx_store.db_wrapper.lock:
await self.service.wallet_state_manager.tx_store.db_wrapper.begin_transaction()
@ -782,41 +758,48 @@ class WalletRpcApi:
await self.service.wallet_state_manager.tx_store.rebuild_tx_cache()
return {}
async def get_transaction_count(self, request):
wallet_id = int(request["wallet_id"])
count = await self.service.wallet_state_manager.tx_store.get_transaction_count_for_wallet(wallet_id)
return {"wallet_id": wallet_id, "count": count}
async def create_backup(self, request):
assert self.service.wallet_state_manager is not None
file_path = Path(request["file_path"])
await self.service.wallet_state_manager.create_wallet_backup(file_path)
return {}
##########################################################################################
# Coloured Coins and Trading
##########################################################################################
async def cc_set_name(self, request):
async def get_cat_list(self, request):
return {"cat_list": list(DEFAULT_CATS.values())}
async def cat_set_name(self, request):
assert self.service.wallet_state_manager is not None
wallet_id = int(request["wallet_id"])
wallet: CCWallet = self.service.wallet_state_manager.wallets[wallet_id]
wallet: CATWallet = self.service.wallet_state_manager.wallets[wallet_id]
await wallet.set_name(str(request["name"]))
return {"wallet_id": wallet_id}
async def cc_get_name(self, request):
async def cat_get_name(self, request):
assert self.service.wallet_state_manager is not None
wallet_id = int(request["wallet_id"])
wallet: CCWallet = self.service.wallet_state_manager.wallets[wallet_id]
wallet: CATWallet = self.service.wallet_state_manager.wallets[wallet_id]
name: str = await wallet.get_name()
return {"wallet_id": wallet_id, "name": name}
async def cc_spend(self, request):
async def cat_asset_id_to_name(self, request):
assert self.service.wallet_state_manager is not None
wallet = await self.service.wallet_state_manager.get_wallet_for_asset_id(request["asset_id"])
if wallet is None:
raise ValueError("The asset ID specified does not belong to a wallet")
else:
return {"wallet_id": wallet.id(), "name": (await wallet.get_name())}
async def cat_spend(self, request):
assert self.service.wallet_state_manager is not None
if await self.service.wallet_state_manager.synced() is False:
raise ValueError("Wallet needs to be fully synced.")
wallet_id = int(request["wallet_id"])
wallet: CCWallet = self.service.wallet_state_manager.wallets[wallet_id]
wallet: CATWallet = self.service.wallet_state_manager.wallets[wallet_id]
puzzle_hash: bytes32 = decode_puzzle_hash(request["inner_address"])
memos: List[bytes] = []
if "memos" in request:
memos = [mem.encode("utf-8") for mem in request["memos"]]
if not isinstance(request["amount"], int) or not isinstance(request["amount"], int):
raise ValueError("An integer amount or fee is required (too many decimals)")
amount: uint64 = uint64(request["amount"])
@ -825,129 +808,133 @@ class WalletRpcApi:
else:
fee = uint64(0)
async with self.service.wallet_state_manager.lock:
tx: TransactionRecord = await wallet.generate_signed_transaction([amount], [puzzle_hash], fee)
await wallet.push_transaction(tx)
txs: TransactionRecord = await wallet.generate_signed_transaction(
[amount], [puzzle_hash], fee, memos=[memos]
)
for tx in txs:
await wallet.standard_wallet.push_transaction(tx)
return {
"transaction": tx,
"transaction": tx.to_json_dict_convenience(self.service.config),
"transaction_id": tx.name,
}
async def cc_get_colour(self, request):
async def cat_get_asset_id(self, request):
assert self.service.wallet_state_manager is not None
wallet_id = int(request["wallet_id"])
wallet: CCWallet = self.service.wallet_state_manager.wallets[wallet_id]
colour: str = wallet.get_colour()
return {"colour": colour, "wallet_id": wallet_id}
wallet: CATWallet = self.service.wallet_state_manager.wallets[wallet_id]
asset_id: str = wallet.get_asset_id()
return {"asset_id": asset_id, "wallet_id": wallet_id}
async def get_offer_summary(self, request):
assert self.service.wallet_state_manager is not None
offer_hex: str = request["offer"]
offer = Offer.from_bytes(hexstr_to_bytes(offer_hex))
offered, requested = offer.summary()
return {"summary": {"offered": offered, "requested": requested}}
async def create_offer_for_ids(self, request):
assert self.service.wallet_state_manager is not None
offer = request["ids"]
file_name = request["filename"]
async with self.service.wallet_state_manager.lock:
(
success,
spend_bundle,
error,
) = await self.service.wallet_state_manager.trade_manager.create_offer_for_ids(offer, file_name)
if success:
self.service.wallet_state_manager.trade_manager.write_offer_to_disk(Path(file_name), spend_bundle)
return {}
raise ValueError(error)
offer: Dict[str, int] = request["offer"]
fee: uint64 = uint64(request.get("fee", 0))
validate_only: bool = request.get("validate_only", False)
async def get_discrepancies_for_offer(self, request):
assert self.service.wallet_state_manager is not None
file_name = request["filename"]
file_path = Path(file_name)
async with self.service.wallet_state_manager.lock:
(
success,
discrepancies,
error,
) = await self.service.wallet_state_manager.trade_manager.get_discrepancies_for_offer(file_path)
modified_offer = {}
for key in offer:
modified_offer[int(key)] = offer[key]
if success:
return {"discrepancies": discrepancies}
raise ValueError(error)
async def respond_to_offer(self, request):
assert self.service.wallet_state_manager is not None
file_path = Path(request["filename"])
async with self.service.wallet_state_manager.lock:
(
success,
trade_record,
error,
) = await self.service.wallet_state_manager.trade_manager.respond_to_offer(file_path)
) = await self.service.wallet_state_manager.trade_manager.create_offer_for_ids(
modified_offer, fee=fee, validate_only=validate_only
)
if success:
return {
"offer": trade_record.offer.hex(),
"trade_record": trade_record.to_json_dict_convenience(),
}
raise ValueError(error)
async def check_offer_validity(self, request):
assert self.service.wallet_state_manager is not None
offer_hex: str = request["offer"]
offer = Offer.from_bytes(hexstr_to_bytes(offer_hex))
return {"valid": (await self.service.wallet_state_manager.trade_manager.check_offer_validity(offer))}
async def take_offer(self, request):
assert self.service.wallet_state_manager is not None
offer_hex = request["offer"]
offer = Offer.from_bytes(hexstr_to_bytes(offer_hex))
fee: uint64 = uint64(request.get("fee", 0))
async with self.service.wallet_state_manager.lock:
(
success,
trade_record,
error,
) = await self.service.wallet_state_manager.trade_manager.respond_to_offer(offer, fee=fee)
if not success:
raise ValueError(error)
return {}
return {"trade_record": trade_record.to_json_dict_convenience()}
async def get_trade(self, request: Dict):
async def get_offer(self, request: Dict):
assert self.service.wallet_state_manager is not None
trade_mgr = self.service.wallet_state_manager.trade_manager
trade_id = bytes32.from_hexstr(request["trade_id"])
trade: Optional[TradeRecord] = await trade_mgr.get_trade_by_id(trade_id)
if trade is None:
file_contents: bool = request.get("file_contents", False)
trade_record: Optional[TradeRecord] = await trade_mgr.get_trade_by_id(bytes32(trade_id))
if trade_record is None:
raise ValueError(f"No trade with trade id: {trade_id.hex()}")
result = trade_record_to_dict(trade)
return {"trade": result}
offer_to_return: bytes = trade_record.offer if trade_record.taken_offer is None else trade_record.taken_offer
offer_value: Optional[str] = offer_to_return.hex() if file_contents else None
return {"trade_record": trade_record.to_json_dict_convenience(), "offer": offer_value}
async def get_all_trades(self, request: Dict):
async def get_all_offers(self, request: Dict):
assert self.service.wallet_state_manager is not None
trade_mgr = self.service.wallet_state_manager.trade_manager
all_trades = await trade_mgr.get_all_trades()
start: int = request.get("start", 0)
end: int = request.get("end", 50)
sort_key: Optional[str] = request.get("sort_key", None)
reverse: bool = request.get("reverse", False)
file_contents: bool = request.get("file_contents", False)
all_trades = await trade_mgr.trade_store.get_trades_between(start, end, sort_key=sort_key, reverse=reverse)
result = []
offer_values: Optional[List[str]] = [] if file_contents else None
for trade in all_trades:
result.append(trade_record_to_dict(trade))
result.append(trade.to_json_dict_convenience())
if file_contents and offer_values is not None:
offer_to_return: bytes = trade.offer if trade.taken_offer is None else trade.taken_offer
offer_values.append(offer_to_return.hex())
return {"trades": result}
return {"trade_records": result, "offers": offer_values}
async def cancel_trade(self, request: Dict):
async def cancel_offer(self, request: Dict):
assert self.service.wallet_state_manager is not None
wsm = self.service.wallet_state_manager
secure = request["secure"]
trade_id = bytes32.from_hexstr(request["trade_id"])
fee: uint64 = uint64(request.get("fee", 0))
async with self.service.wallet_state_manager.lock:
if secure:
await wsm.trade_manager.cancel_pending_offer_safely(trade_id)
await wsm.trade_manager.cancel_pending_offer_safely(bytes32(trade_id), fee=fee)
else:
await wsm.trade_manager.cancel_pending_offer(trade_id)
await wsm.trade_manager.cancel_pending_offer(bytes32(trade_id))
return {}
async def get_backup_info(self, request: Dict):
file_path = Path(request["file_path"])
sk = None
if "words" in request:
mnemonic = request["words"]
passphrase = ""
try:
assert self.service.keychain_proxy is not None # An offering to the mypy gods
sk = await self.service.keychain_proxy.add_private_key(" ".join(mnemonic), passphrase)
except KeyError as e:
return {
"success": False,
"error": f"The word '{e.args[0]}' is incorrect.'",
"word": e.args[0],
}
except Exception as e:
return {"success": False, "error": str(e)}
elif "fingerprint" in request:
sk, seed = await self._get_private_key(request["fingerprint"])
if sk is None:
raise ValueError("Unable to decrypt the backup file.")
backup_info = get_backup_info(file_path, sk)
return {"backup_info": backup_info}
##########################################################################################
# Distributed Identities
##########################################################################################
@ -1156,26 +1143,30 @@ class WalletRpcApi:
"last_height_farmed": last_height_farmed,
}
async def create_signed_transaction(self, request, hold_lock=True):
async def create_signed_transaction(self, request, hold_lock=True) -> Dict:
assert self.service.wallet_state_manager is not None
if "additions" not in request or len(request["additions"]) < 1:
raise ValueError("Specify additions list")
additions: List[Dict] = request["additions"]
amount_0: uint64 = uint64(additions[0]["amount"])
assert amount_0 <= self.service.constants.MAX_COIN_AMOUNT
puzzle_hash_0 = hexstr_to_bytes(additions[0]["puzzle_hash"])
puzzle_hash_0 = bytes32.from_hexstr(additions[0]["puzzle_hash"])
if len(puzzle_hash_0) != 32:
raise ValueError(f"Address must be 32 bytes. {puzzle_hash_0}")
raise ValueError(f"Address must be 32 bytes. {puzzle_hash_0.hex()}")
memos_0 = None if "memos" not in additions[0] else [mem.encode("utf-8") for mem in additions[0]["memos"]]
additional_outputs: List[AmountWithPuzzlehash] = []
for addition in additions[1:]:
receiver_ph = hexstr_to_bytes(addition["puzzle_hash"])
receiver_ph = bytes32.from_hexstr(addition["puzzle_hash"])
if len(receiver_ph) != 32:
raise ValueError(f"Address must be 32 bytes. {receiver_ph}")
raise ValueError(f"Address must be 32 bytes. {receiver_ph.hex()}")
amount = uint64(addition["amount"])
if amount > self.service.constants.MAX_COIN_AMOUNT:
raise ValueError(f"Coin amount cannot exceed {self.service.constants.MAX_COIN_AMOUNT}")
additional_outputs.append({"puzzlehash": receiver_ph, "amount": amount})
memos = [] if "memos" not in addition else [mem.encode("utf-8") for mem in addition["memos"]]
additional_outputs.append({"puzzlehash": receiver_ph, "amount": amount, "memos": memos})
fee = uint64(0)
if "fee" in request:
@ -1185,36 +1176,62 @@ class WalletRpcApi:
if "coins" in request and len(request["coins"]) > 0:
coins = set([Coin.from_json_dict(coin_json) for coin_json in request["coins"]])
coin_announcements: Optional[Set[bytes32]] = None
coin_announcements: Optional[Set[Announcement]] = None
if (
"coin_announcements" in request
and request["coin_announcements"] is not None
and len(request["coin_announcements"]) > 0
):
coin_announcements = set([hexstr_to_bytes(announcement) for announcement in request["coin_announcements"]])
coin_announcements = {
Announcement(
bytes32.from_hexstr(announcement["coin_id"]),
bytes(Program.to(binutils.assemble(announcement["message"]))),
hexstr_to_bytes(announcement["morph_bytes"]) if "morph_bytes" in announcement else None,
)
for announcement in request["coin_announcements"]
}
puzzle_announcements: Optional[Set[Announcement]] = None
if (
"puzzle_announcements" in request
and request["puzzle_announcements"] is not None
and len(request["puzzle_announcements"]) > 0
):
puzzle_announcements = {
Announcement(
bytes32.from_hexstr(announcement["puzzle_hash"]),
bytes(Program.to(binutils.assemble(announcement["message"]))),
hexstr_to_bytes(announcement["morph_bytes"]) if "morph_bytes" in announcement else None,
)
for announcement in request["puzzle_announcements"]
}
if hold_lock:
async with self.service.wallet_state_manager.lock:
signed_tx = await self.service.wallet_state_manager.main_wallet.generate_signed_transaction(
amount_0,
puzzle_hash_0,
bytes32(puzzle_hash_0),
fee,
coins=coins,
ignore_max_send_amount=True,
primaries=additional_outputs,
announcements_to_consume=coin_announcements,
memos=memos_0,
coin_announcements_to_consume=coin_announcements,
puzzle_announcements_to_consume=puzzle_announcements,
)
else:
signed_tx = await self.service.wallet_state_manager.main_wallet.generate_signed_transaction(
amount_0,
puzzle_hash_0,
bytes32(puzzle_hash_0),
fee,
coins=coins,
ignore_max_send_amount=True,
primaries=additional_outputs,
announcements_to_consume=coin_announcements,
memos=memos_0,
coin_announcements_to_consume=coin_announcements,
puzzle_announcements_to_consume=puzzle_announcements,
)
return {"signed_tx": signed_tx}
return {"signed_tx": signed_tx.to_json_dict_convenience(self.service.config)}
##########################################################################################
# Pool Wallet
@ -1228,14 +1245,16 @@ class WalletRpcApi:
pool_wallet_info: PoolWalletInfo = await wallet.get_current_state()
owner_pubkey = pool_wallet_info.current.owner_pubkey
target_puzzlehash = None
if await self.service.wallet_state_manager.synced() is False:
raise ValueError("Wallet needs to be fully synced.")
if "target_puzzlehash" in request:
target_puzzlehash = bytes32(hexstr_to_bytes(request["target_puzzlehash"]))
# TODO: address hint error and remove ignore
# error: Argument 2 to "create_pool_state" has incompatible type "Optional[bytes32]"; expected "bytes32"
# [arg-type]
assert target_puzzlehash is not None
new_target_state: PoolState = create_pool_state(
FARMING_TO_POOL,
target_puzzlehash, # type: ignore[arg-type]
target_puzzlehash,
owner_pubkey,
request["pool_url"],
uint32(request["relative_lock_height"]),
@ -1254,6 +1273,9 @@ class WalletRpcApi:
wallet_id = uint32(request["wallet_id"])
wallet: PoolWallet = self.service.wallet_state_manager.wallets[wallet_id]
if await self.service.wallet_state_manager.synced() is False:
raise ValueError("Wallet needs to be fully synced.")
async with self.service.wallet_state_manager.lock:
total_fee, tx = await wallet.self_pool(fee) # total_fee: uint64, tx: TransactionRecord
return {"total_fee": total_fee, "transaction": tx}

View File

@ -3,10 +3,13 @@ from typing import Dict, List, Optional, Any, Tuple
from chia.pools.pool_wallet_info import PoolWalletInfo
from chia.rpc.rpc_client import RpcClient
from chia.types.announcement import Announcement
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.bech32m import decode_puzzle_hash
from chia.util.byte_types import hexstr_to_bytes
from chia.util.ints import uint32, uint64
from chia.wallet.trade_record import TradeRecord
from chia.wallet.trading.offer import Offer
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.transaction_sorting import SortKey
@ -104,7 +107,7 @@ class WalletRpcClient(RpcClient):
"get_transaction",
{"walled_id": wallet_id, "transaction_id": transaction_id.hex()},
)
return TransactionRecord.from_json_dict(res["transaction"])
return TransactionRecord.from_json_dict_convenience(res["transaction"])
async def get_transactions(
self,
@ -128,32 +131,46 @@ class WalletRpcClient(RpcClient):
"get_transactions",
request,
)
reverted_tx: List[TransactionRecord] = []
for modified_tx in res["transactions"]:
# Server returns address instead of ph, but TransactionRecord requires ph
modified_tx["to_puzzle_hash"] = decode_puzzle_hash(modified_tx["to_address"]).hex()
del modified_tx["to_address"]
reverted_tx.append(TransactionRecord.from_json_dict(modified_tx))
return reverted_tx
return [TransactionRecord.from_json_dict_convenience(tx) for tx in res["transactions"]]
async def get_transaction_count(
self,
wallet_id: str,
) -> List[TransactionRecord]:
res = await self.fetch(
"get_transaction_count",
{"wallet_id": wallet_id},
)
return res["count"]
async def get_next_address(self, wallet_id: str, new_address: bool) -> str:
return (await self.fetch("get_next_address", {"wallet_id": wallet_id, "new_address": new_address}))["address"]
async def send_transaction(
self, wallet_id: str, amount: uint64, address: str, fee: uint64 = uint64(0)
self, wallet_id: str, amount: uint64, address: str, fee: uint64 = uint64(0), memos: Optional[List[str]] = None
) -> TransactionRecord:
res = await self.fetch(
"send_transaction",
{"wallet_id": wallet_id, "amount": amount, "address": address, "fee": fee},
)
return TransactionRecord.from_json_dict(res["transaction"])
if memos is None:
send_dict: Dict = {"wallet_id": wallet_id, "amount": amount, "address": address, "fee": fee}
else:
send_dict = {
"wallet_id": wallet_id,
"amount": amount,
"address": address,
"fee": fee,
"memos": memos,
}
res = await self.fetch("send_transaction", send_dict)
return TransactionRecord.from_json_dict_convenience(res["transaction"])
async def send_transaction_multi(
self, wallet_id: str, additions: List[Dict], coins: List[Coin] = None, fee: uint64 = uint64(0)
) -> TransactionRecord:
# Converts bytes to hex for puzzle hashes
additions_hex = [{"amount": ad["amount"], "puzzle_hash": ad["puzzle_hash"].hex()} for ad in additions]
additions_hex = []
for ad in additions:
additions_hex.append({"amount": ad["amount"], "puzzle_hash": ad["puzzle_hash"].hex()})
if "memos" in ad:
additions_hex[-1]["memos"] = ad["memos"]
if coins is not None and len(coins) > 0:
coins_json = [c.to_json_dict() for c in coins]
response: Dict = await self.fetch(
@ -164,7 +181,8 @@ class WalletRpcClient(RpcClient):
response = await self.fetch(
"send_transaction_multi", {"wallet_id": wallet_id, "additions": additions_hex, "fee": fee}
)
return TransactionRecord.from_json_dict(response["transaction"])
return TransactionRecord.from_json_dict_convenience(response["transaction"])
async def delete_unconfirmed_transactions(self, wallet_id: str) -> None:
await self.fetch(
@ -184,35 +202,47 @@ class WalletRpcClient(RpcClient):
additions: List[Dict],
coins: List[Coin] = None,
fee: uint64 = uint64(0),
coin_announcements: List[bytes32] = None,
coin_announcements: Optional[List[Announcement]] = None,
puzzle_announcements: Optional[List[Announcement]] = None,
) -> TransactionRecord:
# Converts bytes to hex for puzzle hashes
additions_hex = [{"amount": ad["amount"], "puzzle_hash": ad["puzzle_hash"].hex()} for ad in additions]
# Converts bytes to hex for coin announcements and does not if it is none.
coin_announcements_hex: Optional[List[str]] = None
additions_hex = []
for ad in additions:
additions_hex.append({"amount": ad["amount"], "puzzle_hash": ad["puzzle_hash"].hex()})
if "memos" in ad:
additions_hex[-1]["memos"] = ad["memos"]
request: Dict[str, Any] = {
"additions": additions_hex,
"fee": fee,
}
if coin_announcements is not None and len(coin_announcements) > 0:
coin_announcements_hex = [announcement.hex() for announcement in coin_announcements]
request["coin_announcements"] = [
{
"coin_id": ann.origin_info.hex(),
"message": ann.message.hex(),
"morph_bytes": ann.morph_bytes.hex() if ann.morph_bytes is not None else b"".hex(),
}
for ann in coin_announcements
]
if puzzle_announcements is not None and len(puzzle_announcements) > 0:
request["puzzle_announcements"] = [
{
"puzzle_hash": ann.origin_info.hex(),
"message": ann.message.hex(),
"morph_bytes": ann.morph_bytes.hex() if ann.morph_bytes is not None else b"".hex(),
}
for ann in puzzle_announcements
]
if coins is not None and len(coins) > 0:
coins_json = [c.to_json_dict() for c in coins]
response: Dict = await self.fetch(
"create_signed_transaction",
{
"additions": additions_hex,
"coins": coins_json,
"fee": fee,
"coin_announcements": coin_announcements_hex,
},
)
else:
response = await self.fetch(
"create_signed_transaction",
{
"additions": additions_hex,
"fee": fee,
"coin_announcements": coin_announcements_hex,
},
)
return TransactionRecord.from_json_dict(response["signed_tx"])
request["coins"] = coins_json
response: Dict = await self.fetch("create_signed_transaction", request)
return TransactionRecord.from_json_dict_convenience(response["signed_tx"])
async def create_new_did_wallet(self, amount):
request: Dict[str, Any] = {
@ -318,3 +348,122 @@ class WalletRpcClient(RpcClient):
PoolWalletInfo.from_json_dict(json_dict["state"]),
[TransactionRecord.from_json_dict(tr) for tr in json_dict["unconfirmed_transactions"]],
)
# CATS
async def create_new_cat_and_wallet(self, amount: uint64) -> Dict:
request: Dict[str, Any] = {
"wallet_type": "cat_wallet",
"mode": "new",
"amount": amount,
"host": f"{self.hostname}:{self.port}",
}
return await self.fetch("create_new_wallet", request)
async def create_wallet_for_existing_cat(self, asset_id: bytes) -> Dict:
request: Dict[str, Any] = {
"wallet_type": "cat_wallet",
"asset_id": asset_id.hex(),
"mode": "existing",
"host": f"{self.hostname}:{self.port}",
}
return await self.fetch("create_new_wallet", request)
async def get_cat_asset_id(self, wallet_id: str) -> bytes:
request: Dict[str, Any] = {
"wallet_id": wallet_id,
}
return bytes.fromhex((await self.fetch("cat_get_asset_id", request))["asset_id"])
async def cat_asset_id_to_name(self, asset_id: bytes32) -> Optional[Tuple[uint32, str]]:
request: Dict[str, Any] = {
"asset_id": asset_id.hex(),
}
try:
res = await self.fetch("cat_asset_id_to_name", request)
return uint32(int(res["wallet_id"])), res["name"]
except Exception:
return None
async def get_cat_name(self, wallet_id: str) -> str:
request: Dict[str, Any] = {
"wallet_id": wallet_id,
}
return (await self.fetch("cat_get_name", request))["name"]
async def set_cat_name(self, wallet_id: str, name: str) -> None:
request: Dict[str, Any] = {
"wallet_id": wallet_id,
"name": name,
}
await self.fetch("cat_set_name", request)
async def cat_spend(
self,
wallet_id: str,
amount: uint64,
inner_address: str,
fee: uint64 = uint64(0),
memos: Optional[List[str]] = None,
) -> TransactionRecord:
send_dict = {
"wallet_id": wallet_id,
"amount": amount,
"inner_address": inner_address,
"fee": fee,
"memos": memos if memos else [],
}
res = await self.fetch("cat_spend", send_dict)
return TransactionRecord.from_json_dict_convenience(res["transaction"])
# Offers
async def create_offer_for_ids(
self, offer_dict: Dict[uint32, int], fee=uint64(0), validate_only: bool = False
) -> Tuple[Optional[Offer], TradeRecord]:
send_dict: Dict[str, int] = {}
for key in offer_dict:
send_dict[str(key)] = offer_dict[key]
res = await self.fetch("create_offer_for_ids", {"offer": send_dict, "validate_only": validate_only, "fee": fee})
offer: Optional[Offer] = None if validate_only else Offer.from_bytes(hexstr_to_bytes(res["offer"]))
return offer, TradeRecord.from_json_dict_convenience(res["trade_record"], res["offer"])
async def get_offer_summary(self, offer: Offer) -> Dict[str, Dict[str, int]]:
res = await self.fetch("get_offer_summary", {"offer": bytes(offer).hex()})
return res["summary"]
async def check_offer_validity(self, offer: Offer) -> bool:
res = await self.fetch("check_offer_validity", {"offer": bytes(offer).hex()})
return res["valid"]
async def take_offer(self, offer: Offer, fee=uint64(0)) -> TradeRecord:
res = await self.fetch("take_offer", {"offer": bytes(offer).hex(), "fee": fee})
return TradeRecord.from_json_dict_convenience(res["trade_record"])
async def get_offer(self, trade_id: bytes32, file_contents: bool = False) -> TradeRecord:
res = await self.fetch("get_offer", {"trade_id": trade_id.hex(), "file_contents": file_contents})
offer_str = res["offer"] if file_contents else ""
return TradeRecord.from_json_dict_convenience(res["trade_record"], offer_str)
async def get_all_offers(
self, start: int = 0, end: int = 50, sort_key: str = None, reverse: bool = False, file_contents: bool = False
) -> List[TradeRecord]:
res = await self.fetch(
"get_all_offers",
{
"start": start,
"end": end,
"sort_key": sort_key,
"reverse": reverse,
"file_contents": file_contents,
},
)
records = []
optional_offers = res["offers"] if file_contents else ([""] * len(res["trade_records"]))
for record, offer in zip(res["trade_records"], optional_offers):
records.append(TradeRecord.from_json_dict_convenience(record, offer))
return records
async def cancel_offer(self, trade_id: bytes32, fee=uint64(0), secure: bool = True):
await self.fetch("cancel_offer", {"trade_id": trade_id.hex(), "secure": secure, "fee": fee})

View File

@ -707,7 +707,7 @@ class WalletPeers(FullNodeDiscovery):
)
async def start(self) -> None:
self.initial_wait = 60
self.initial_wait = 1
await self.migrate_address_manager_if_necessary()
await self.initialize_address_manager()
await self.start_tasks()

View File

@ -803,13 +803,9 @@ class ChiaServer:
def is_trusted_peer(self, peer: WSChiaConnection, trusted_peers: Dict) -> bool:
if trusted_peers is None:
return False
for trusted_peer in trusted_peers:
cert = self.root_path / trusted_peers[trusted_peer]
pem_cert = x509.load_pem_x509_certificate(cert.read_bytes())
cert_bytes = pem_cert.public_bytes(encoding=serialization.Encoding.DER)
der_cert = x509.load_der_x509_certificate(cert_bytes)
peer_id = bytes32(der_cert.fingerprint(hashes.SHA256()))
if peer_id == peer.peer_node_id:
self.log.debug(f"trusted node {peer.peer_node_id} {peer.peer_host}")
return True
return False
if not self.config["testing"] and peer.peer_host == "127.0.0.1":
return True
if peer.peer_node_id.hex() not in trusted_peers:
return False
return True

View File

@ -13,7 +13,6 @@ from chia.protocols.protocol_timing import INTERNAL_PROTOCOL_ERROR_BAN_SECONDS
from chia.protocols.shared_protocol import Capability, Handshake
from chia.server.outbound_message import Message, NodeType, make_msg
from chia.server.rate_limits import RateLimiter
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.peer_info import PeerInfo
from chia.util.errors import Err, ProtocolError
from chia.util.ints import uint8, uint16
@ -89,9 +88,9 @@ class WSChiaConnection:
self.session = session
self.close_callback = close_callback
self.pending_requests: Dict[bytes32, asyncio.Event] = {}
self.pending_timeouts: Dict[bytes32, asyncio.Task] = {}
self.request_results: Dict[bytes32, Message] = {}
self.pending_requests: Dict[uint16, asyncio.Event] = {}
self.pending_timeouts: Dict[uint16, asyncio.Task] = {}
self.request_results: Dict[uint16, Message] = {}
self.closed = False
self.connection_type: Optional[NodeType] = None
if is_outbound:
@ -108,6 +107,7 @@ class WSChiaConnection:
# Used by the Chia Seeder.
self.version = None
self.protocol_version = ""
async def perform_handshake(self, network_id: str, protocol_version: str, server_port: int, local_type: NodeType):
if self.is_outbound:
@ -142,7 +142,7 @@ class WSChiaConnection:
raise ProtocolError(Err.INCOMPATIBLE_NETWORK_ID)
self.version = inbound_handshake.software_version
self.protocol_version = inbound_handshake.protocol_version
self.peer_server_port = inbound_handshake.server_port
self.connection_type = NodeType(inbound_handshake.node_type)
@ -342,11 +342,8 @@ class WSChiaConnection:
)
message = Message(message_no_id.type, request_id, message_no_id.data)
# TODO: address hint error and remove ignore
# error: Invalid index type "Optional[uint16]" for "Dict[bytes32, Event]"; expected type "bytes32"
# [index]
self.pending_requests[message.id] = event # type: ignore[index]
assert message.id is not None
self.pending_requests[message.id] = event
await self.outgoing_queue.put(message)
# If the timeout passes, we set the event
@ -361,34 +358,16 @@ class WSChiaConnection:
raise
timeout_task = asyncio.create_task(time_out(message.id, timeout))
# TODO: address hint error and remove ignore
# error: Invalid index type "Optional[uint16]" for "Dict[bytes32, Task[Any]]"; expected type "bytes32"
# [index]
self.pending_timeouts[message.id] = timeout_task # type: ignore[index]
self.pending_timeouts[message.id] = timeout_task
await event.wait()
# TODO: address hint error and remove ignore
# error: No overload variant of "pop" of "MutableMapping" matches argument type "Optional[uint16]"
# [call-overload]
# note: Possible overload variants:
# note: def pop(self, key: bytes32) -> Event
# note: def [_T] pop(self, key: bytes32, default: Union[Event, _T] = ...) -> Union[Event, _T]
self.pending_requests.pop(message.id) # type: ignore[call-overload]
self.pending_requests.pop(message.id)
result: Optional[Message] = None
if message.id in self.request_results:
# TODO: address hint error and remove ignore
# error: Invalid index type "Optional[uint16]" for "Dict[bytes32, Message]"; expected type "bytes32"
# [index]
result = self.request_results[message.id] # type: ignore[index]
result = self.request_results[message.id]
assert result is not None
self.log.debug(f"<- {ProtocolMessageTypes(result.type).name} from: {self.peer_host}:{self.peer_port}")
# TODO: address hint error and remove ignore
# error: No overload variant of "pop" of "MutableMapping" matches argument type "Optional[uint16]"
# [call-overload]
# note: Possible overload variants:
# note: def pop(self, key: bytes32) -> Message
# note: def [_T] pop(self, key: bytes32, default: Union[Message, _T] = ...) -> Union[Message, _T]
self.request_results.pop(result.id) # type: ignore[call-overload]
self.request_results.pop(message.id)
return result

View File

@ -1,4 +1,5 @@
from dataclasses import dataclass
from typing import Optional
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.hash import std_hash
@ -8,9 +9,14 @@ from chia.util.hash import std_hash
class Announcement:
origin_info: bytes32
message: bytes
morph_bytes: Optional[bytes] = None # CATs morph their announcements and other puzzles may choose to do so too
def name(self) -> bytes32:
return std_hash(bytes(self.origin_info + self.message))
if self.morph_bytes is not None:
message: bytes = std_hash(self.morph_bytes + self.message)
else:
message = self.message
return std_hash(bytes(self.origin_info + message))
def __str__(self):
return self.name().decode("utf-8")

View File

@ -1,8 +1,10 @@
from dataclasses import dataclass
from typing import List
from blspy import G2Element
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import SerializedProgram, INFINITE_COST
from chia.types.condition_opcodes import ConditionOpcode
from chia.util.chain_utils import additions_for_solution, fee_for_solution
from chia.util.streamable import Streamable, streamable
@ -25,3 +27,26 @@ class CoinSpend(Streamable):
def reserved_fee(self) -> int:
return fee_for_solution(self.puzzle_reveal, self.solution, INFINITE_COST)
def hints(self) -> List[bytes]:
# import above was causing circular import issue
from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions
from chia.consensus.default_constants import DEFAULT_CONSTANTS
from chia.types.spend_bundle import SpendBundle
from chia.full_node.bundle_tools import simple_solution_generator
bundle = SpendBundle([self], G2Element())
generator = simple_solution_generator(bundle)
npc_result = get_name_puzzle_conditions(
generator, INFINITE_COST, cost_per_byte=DEFAULT_CONSTANTS.COST_PER_BYTE, mempool_mode=False
)
h_list = []
for npc in npc_result.npc_list:
for opcode, conditions in npc.conditions:
if opcode == ConditionOpcode.CREATE_COIN:
for condition in conditions:
if len(condition.vars) > 2 and condition.vars[2] != b"":
h_list.append(condition.vars[2])
return h_list

View File

@ -2,17 +2,20 @@ import dataclasses
import warnings
from dataclasses import dataclass
from typing import List
from typing import List, Dict
from blspy import AugSchemeMPL, G2Element
from clvm.casts import int_from_bytes
from chia.consensus.default_constants import DEFAULT_CONSTANTS
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.streamable import Streamable, dataclass_from_dict, recurse_jsonify, streamable
from chia.wallet.util.debug_spend_bundle import debug_spend_bundle
from .blockchain_format.program import Program
from .coin_spend import CoinSpend
from .condition_opcodes import ConditionOpcode
@dataclass(frozen=True)
@ -77,6 +80,27 @@ class SpendBundle(Streamable):
return result
def get_memos(self) -> Dict[bytes32, List[bytes]]:
"""
Retrieves the memos for additions in this spend_bundle, which are formatted as a list in the 3rd parameter of
CREATE_COIN. If there are no memos, the addition coin_id is not included. If they are not formatted as a list
of bytes, they are not included. This is expensive to call, it should not be used in full node code.
"""
memos: Dict[bytes32, List[bytes]] = {}
for coin_spend in self.coin_spends:
result = Program.from_bytes(bytes(coin_spend.puzzle_reveal)).run(
Program.from_bytes(bytes(coin_spend.solution))
)
for condition in result.as_python():
if condition[0] == ConditionOpcode.CREATE_COIN and len(condition) >= 4:
# If only 3 elements (opcode + 2 args), there is no memo, this is ph, amount
coin_added = Coin(coin_spend.coin.name(), bytes32(condition[1]), int_from_bytes(condition[2]))
if type(condition[3]) != list:
# If it's not a list, it's not the correct format
continue
memos[coin_added.name()] = condition[3]
return memos
# Note that `coin_spends` used to have the bad name `coin_solutions`.
# Some API still expects this name. For now, we accept both names.
#

View File

@ -28,7 +28,7 @@ class BlockCache(BlockchainInterface):
self._headers = headers
self._height_to_hash = height_to_hash
self._sub_epoch_summaries = sub_epoch_summaries
self._sub_epoch_segments: Dict[uint32, SubEpochSegments] = {}
self._sub_epoch_segments: Dict[bytes32, SubEpochSegments] = {}
self.log = logging.getLogger(__name__)
def block_record(self, header_hash: bytes32) -> BlockRecord:
@ -83,15 +83,15 @@ class BlockCache(BlockchainInterface):
return self._headers
async def persist_sub_epoch_challenge_segments(
self, sub_epoch_summary_height: uint32, segments: List[SubEpochChallengeSegment]
self, sub_epoch_summary_hash: bytes32, segments: List[SubEpochChallengeSegment]
):
self._sub_epoch_segments[sub_epoch_summary_height] = SubEpochSegments(segments)
self._sub_epoch_segments[sub_epoch_summary_hash] = SubEpochSegments(segments)
async def get_sub_epoch_challenge_segments(
self,
sub_epoch_summary_height: uint32,
sub_epoch_summary_hash: bytes32,
) -> Optional[List[SubEpochChallengeSegment]]:
segments = self._sub_epoch_segments.get(sub_epoch_summary_height)
segments = self._sub_epoch_segments.get(sub_epoch_summary_hash)
if segments is None:
return None
return segments.challenge_segments

View File

@ -114,9 +114,7 @@ def created_outputs_for_conditions_dict(
for cvp in conditions_dict.get(ConditionOpcode.CREATE_COIN, []):
puzzle_hash, amount_bin = cvp.vars[0], cvp.vars[1]
amount = int_from_bytes(amount_bin)
# TODO: address hint error and remove ignore
# error: Argument 2 to "Coin" has incompatible type "bytes"; expected "bytes32" [arg-type]
coin = Coin(input_coin_name, puzzle_hash, uint64(amount)) # type: ignore[arg-type]
coin = Coin(input_coin_name, bytes32(puzzle_hash), uint64(amount))
output_coins.append(coin)
return output_coins

View File

@ -73,6 +73,7 @@ network_overrides: &network_overrides
default_full_node_port: 8444
testnet0:
address_prefix: "txch"
default_full_node_port: 58444
testnet1:
address_prefix: "txch"
testnet2:

View File

@ -353,7 +353,7 @@ def confirm_included(root: Node, val: bytes, proof: bytes32) -> bool:
return confirm_not_included_already_hashed(root, sha256(val).digest(), proof)
def confirm_included_already_hashed(root: Node, val: bytes, proof: bytes32) -> bool:
def confirm_included_already_hashed(root: Node, val: bytes, proof: bytes) -> bool:
return _confirm(root, val, proof, True)
@ -361,11 +361,11 @@ def confirm_not_included(root: Node, val: bytes, proof: bytes32) -> bool:
return confirm_not_included_already_hashed(root, sha256(val).digest(), proof)
def confirm_not_included_already_hashed(root: Node, val: bytes, proof: bytes32) -> bool:
def confirm_not_included_already_hashed(root: Node, val: bytes, proof: bytes) -> bool:
return _confirm(root, val, proof, False)
def _confirm(root: Node, val: bytes, proof: bytes32, expected: bool) -> bool:
def _confirm(root: Node, val: bytes, proof: bytes, expected: bool) -> bool:
try:
p = deserialize_proof(proof)
if p.get_root() != root:
@ -376,7 +376,7 @@ def _confirm(root: Node, val: bytes, proof: bytes32, expected: bool) -> bool:
return False
def deserialize_proof(proof: bytes32) -> MerkleSet:
def deserialize_proof(proof: bytes) -> MerkleSet:
try:
r, pos = _deserialize(proof, 0, [])
if pos != len(proof):
@ -386,7 +386,7 @@ def deserialize_proof(proof: bytes32) -> MerkleSet:
raise SetError()
def _deserialize(proof: bytes32, pos: int, bits: List[int]) -> Tuple[Node, int]:
def _deserialize(proof: bytes, pos: int, bits: List[int]) -> Tuple[Node, int]:
t = proof[pos : pos + 1] # flake8: noqa
if t == EMPTY:
return _empty, pos + 1

View File

@ -0,0 +1,30 @@
SPACEBUCKS = {
"asset_id": "78ad32a8c9ea70f27d73e9306fc467bab2a6b15b30289791e37ab6e8612212b1",
"name": "Spacebucks",
"symbol": "SBX",
}
MARMOT = {
"asset_id": "8ebf855de6eb146db5602f0456d2f0cbe750d57f821b6f91a8592ee9f1d4cf31",
"name": "Marmot",
"symbol": "MRMT",
}
DUCK_SAUCE = {
"asset_id": "6d95dae356e32a71db5ddcb42224754a02524c615c5fc35f568c2af04774e589",
"name": "Duck Sauce",
"symbol": "DSC",
}
CHIA_HOLIDAY_TOKEN = {
"asset_id": "509deafe3cd8bbfbb9ccce1d930e3d7b57b40c964fa33379b18d628175eb7a8f",
"name": "Chia Holiday 2021 Token",
"symbol": "CH21",
}
DEFAULT_CATS = {
SPACEBUCKS["asset_id"]: SPACEBUCKS,
MARMOT["asset_id"]: MARMOT,
DUCK_SAUCE["asset_id"]: DUCK_SAUCE,
CHIA_HOLIDAY_TOKEN["asset_id"]: CHIA_HOLIDAY_TOKEN,
}

View File

@ -3,11 +3,13 @@ from typing import List, Optional, Tuple
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.wallet.lineage_proof import LineageProof
from chia.util.streamable import Streamable, streamable
@dataclass(frozen=True)
@streamable
class CCInfo(Streamable):
my_genesis_checker: Optional[Program] # this is the program
lineage_proofs: List[Tuple[bytes32, Optional[Program]]] # {coin.name(): lineage_proof}
class CATInfo(Streamable):
limitations_program_hash: bytes32
my_tail: Optional[Program] # this is the program
lineage_proofs: List[Tuple[bytes32, Optional[LineageProof]]] # {coin.name(): lineage_proof}

View File

@ -0,0 +1,135 @@
import dataclasses
from typing import List, Tuple, Iterator
from blspy import G2Element
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program, INFINITE_COST
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.condition_opcodes import ConditionOpcode
from chia.types.spend_bundle import CoinSpend, SpendBundle
from chia.util.condition_tools import conditions_dict_for_solution
from chia.wallet.lineage_proof import LineageProof
from chia.wallet.puzzles.cat_loader import CAT_MOD
NULL_SIGNATURE = G2Element()
ANYONE_CAN_SPEND_PUZZLE = Program.to(1) # simply return the conditions
# information needed to spend a cc
@dataclasses.dataclass
class SpendableCAT:
coin: Coin
limitations_program_hash: bytes32
inner_puzzle: Program
inner_solution: Program
limitations_solution: Program = Program.to([])
lineage_proof: LineageProof = LineageProof()
extra_delta: int = 0
limitations_program_reveal: Program = Program.to([])
def match_cat_puzzle(puzzle: Program) -> Tuple[bool, Iterator[Program]]:
"""
Given a puzzle test if it's a CAT and, if it is, return the curried arguments
"""
mod, curried_args = puzzle.uncurry()
if mod == CAT_MOD:
return True, curried_args.as_iter()
else:
return False, iter(())
def construct_cat_puzzle(mod_code: Program, limitations_program_hash: bytes32, inner_puzzle: Program) -> Program:
"""
Given an inner puzzle hash and tail hash calculate a puzzle program for a specific cc.
"""
return mod_code.curry(mod_code.get_tree_hash(), limitations_program_hash, inner_puzzle)
def subtotals_for_deltas(deltas) -> List[int]:
"""
Given a list of deltas corresponding to input coins, create the "subtotals" list
needed in solutions spending those coins.
"""
subtotals = []
subtotal = 0
for delta in deltas:
subtotals.append(subtotal)
subtotal += delta
# tweak the subtotals so the smallest value is 0
subtotal_offset = min(subtotals)
subtotals = [_ - subtotal_offset for _ in subtotals]
return subtotals
def next_info_for_spendable_cat(spendable_cat: SpendableCAT) -> Program:
c = spendable_cat.coin
list = [c.parent_coin_info, spendable_cat.inner_puzzle.get_tree_hash(), c.amount]
return Program.to(list)
# This should probably return UnsignedSpendBundle if that type ever exists
def unsigned_spend_bundle_for_spendable_cats(mod_code: Program, spendable_cat_list: List[SpendableCAT]) -> SpendBundle:
"""
Given a list of `SpendableCAT` objects, create a `SpendBundle` that spends all those coins.
Note that no signing is done here, so it falls on the caller to sign the resultant bundle.
"""
N = len(spendable_cat_list)
# figure out what the deltas are by running the inner puzzles & solutions
deltas = []
for spend_info in spendable_cat_list:
error, conditions, cost = conditions_dict_for_solution(
spend_info.inner_puzzle, spend_info.inner_solution, INFINITE_COST
)
total = spend_info.extra_delta * -1
if conditions:
for _ in conditions.get(ConditionOpcode.CREATE_COIN, []):
if _.vars[1] != b"\x8f": # -113 in bytes
total += Program.to(_.vars[1]).as_int()
deltas.append(spend_info.coin.amount - total)
if sum(deltas) != 0:
raise ValueError("input and output amounts don't match")
subtotals = subtotals_for_deltas(deltas)
infos_for_next = []
infos_for_me = []
ids = []
for _ in spendable_cat_list:
infos_for_next.append(next_info_for_spendable_cat(_))
infos_for_me.append(Program.to(_.coin.as_list()))
ids.append(_.coin.name())
coin_spends = []
for index in range(N):
spend_info = spendable_cat_list[index]
puzzle_reveal = construct_cat_puzzle(mod_code, spend_info.limitations_program_hash, spend_info.inner_puzzle)
prev_index = (index - 1) % N
next_index = (index + 1) % N
prev_id = ids[prev_index]
my_info = infos_for_me[index]
next_info = infos_for_next[next_index]
solution = [
spend_info.inner_solution,
spend_info.lineage_proof.to_program(),
prev_id,
my_info,
next_info,
subtotals[index],
spend_info.extra_delta,
]
coin_spend = CoinSpend(spend_info.coin, puzzle_reveal, Program.to(solution))
coin_spends.append(coin_spend)
return SpendBundle(coin_spends, NULL_SIGNATURE)

View File

@ -0,0 +1,776 @@
from __future__ import annotations
import dataclasses
import logging
import time
from secrets import token_bytes
from typing import Any, Dict, List, Optional, Set, Tuple
from blspy import AugSchemeMPL, G2Element
from chia.consensus.cost_calculator import calculate_cost_of_program, NPCResult
from chia.full_node.bundle_tools import simple_solution_generator
from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions
from chia.protocols.wallet_protocol import PuzzleSolutionResponse, CoinState
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.announcement import Announcement
from chia.types.generator_types import BlockGenerator
from chia.types.spend_bundle import SpendBundle
from chia.types.condition_opcodes import ConditionOpcode
from chia.util.byte_types import hexstr_to_bytes
from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict
from chia.util.ints import uint8, uint32, uint64, uint128
from chia.util.json_util import dict_to_json_str
from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS
from chia.wallet.cat_wallet.cat_info import CATInfo
from chia.wallet.cat_wallet.cat_utils import (
CAT_MOD,
SpendableCAT,
construct_cat_puzzle,
unsigned_spend_bundle_for_spendable_cats,
match_cat_puzzle,
)
from chia.wallet.derivation_record import DerivationRecord
from chia.wallet.lineage_proof import LineageProof
from chia.wallet.payment import Payment
from chia.wallet.puzzles.genesis_checkers import ALL_LIMITATIONS_PROGRAMS
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
DEFAULT_HIDDEN_PUZZLE_HASH,
calculate_synthetic_secret_key,
)
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.wallet_types import WalletType, AmountWithPuzzlehash
from chia.wallet.wallet import Wallet
from chia.wallet.wallet_coin_record import WalletCoinRecord
from chia.wallet.wallet_info import WalletInfo
# This should probably not live in this file but it's for experimental right now
class CATWallet:
wallet_state_manager: Any
log: logging.Logger
wallet_info: WalletInfo
cat_info: CATInfo
standard_wallet: Wallet
cost_of_single_tx: Optional[int]
@staticmethod
async def create_new_cat_wallet(
wallet_state_manager: Any, wallet: Wallet, cat_tail_info: Dict[str, Any], amount: uint64, name="CAT WALLET"
):
self = CATWallet()
self.cost_of_single_tx = None
self.standard_wallet = wallet
self.log = logging.getLogger(__name__)
std_wallet_id = self.standard_wallet.wallet_id
bal = await wallet_state_manager.get_confirmed_balance_for_wallet_already_locked(std_wallet_id)
if amount > bal:
raise ValueError("Not enough balance")
self.wallet_state_manager = wallet_state_manager
# We use 00 bytes because it's not optional. We must check this is overidden during issuance.
empty_bytes = bytes32(32 * b"\0")
self.cat_info = CATInfo(empty_bytes, None, [])
info_as_string = bytes(self.cat_info).hex()
self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, WalletType.CAT, info_as_string)
if self.wallet_info is None:
raise ValueError("Internal Error")
try:
chia_tx, spend_bundle = await ALL_LIMITATIONS_PROGRAMS[
cat_tail_info["identifier"]
].generate_issuance_bundle(
self,
cat_tail_info,
amount,
)
assert self.cat_info.limitations_program_hash != empty_bytes
assert self.cat_info.lineage_proofs != []
except Exception:
await wallet_state_manager.user_store.delete_wallet(self.id(), False)
raise
if spend_bundle is None:
await wallet_state_manager.user_store.delete_wallet(self.id())
raise ValueError("Failed to create spend.")
await self.wallet_state_manager.add_new_wallet(self, self.id())
# Change and actual CAT coin
non_ephemeral_coins: List[Coin] = spend_bundle.not_ephemeral_additions()
cc_coin = None
puzzle_store = self.wallet_state_manager.puzzle_store
for c in non_ephemeral_coins:
info = await puzzle_store.wallet_info_for_puzzle_hash(c.puzzle_hash)
if info is None:
raise ValueError("Internal Error")
id, wallet_type = info
if id == self.id():
cc_coin = c
if cc_coin is None:
raise ValueError("Internal Error, unable to generate new CAT coin")
cc_pid: bytes32 = cc_coin.parent_coin_info
cc_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=cc_coin.puzzle_hash,
amount=uint64(cc_coin.amount),
fee_amount=uint64(0),
confirmed=False,
sent=uint32(10),
spend_bundle=None,
additions=[cc_coin],
removals=list(filter(lambda rem: rem.name() == cc_pid, spend_bundle.removals())),
wallet_id=self.id(),
sent_to=[],
trade_id=None,
type=uint32(TransactionType.INCOMING_TX.value),
name=bytes32(token_bytes()),
memos=[],
)
chia_tx = dataclasses.replace(chia_tx, spend_bundle=spend_bundle)
await self.standard_wallet.push_transaction(chia_tx)
await self.standard_wallet.push_transaction(cc_record)
return self
@staticmethod
async def create_wallet_for_cat(
wallet_state_manager: Any, wallet: Wallet, limitations_program_hash_hex: str, name="CAT WALLET"
) -> CATWallet:
self = CATWallet()
self.cost_of_single_tx = None
self.standard_wallet = wallet
self.log = logging.getLogger(__name__)
for id, wallet in wallet_state_manager.wallets.items():
if wallet.type() == CATWallet.type():
if wallet.get_asset_id() == limitations_program_hash_hex: # type: ignore
self.log.warning("Not creating wallet for already existing CAT wallet")
raise ValueError("Wallet already exists")
self.wallet_state_manager = wallet_state_manager
if limitations_program_hash_hex in DEFAULT_CATS:
cat_info = DEFAULT_CATS[limitations_program_hash_hex]
name = cat_info["name"]
limitations_program_hash = bytes32(hexstr_to_bytes(limitations_program_hash_hex))
self.cat_info = CATInfo(limitations_program_hash, None, [])
info_as_string = bytes(self.cat_info).hex()
self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, WalletType.CAT, info_as_string)
if self.wallet_info is None:
raise Exception("wallet_info is None")
await self.wallet_state_manager.add_new_wallet(self, self.id())
return self
@staticmethod
async def create(
wallet_state_manager: Any,
wallet: Wallet,
wallet_info: WalletInfo,
) -> CATWallet:
self = CATWallet()
self.log = logging.getLogger(__name__)
self.cost_of_single_tx = None
self.wallet_state_manager = wallet_state_manager
self.wallet_info = wallet_info
self.standard_wallet = wallet
self.cat_info = CATInfo.from_bytes(hexstr_to_bytes(self.wallet_info.data))
return self
@classmethod
def type(cls) -> uint8:
return uint8(WalletType.CAT)
def id(self) -> uint32:
return self.wallet_info.id
async def get_confirmed_balance(self, record_list: Optional[Set[WalletCoinRecord]] = None) -> uint64:
if record_list is None:
record_list = await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(self.id())
amount: uint64 = uint64(0)
for record in record_list:
lineage = await self.get_lineage_proof_for_coin(record.coin)
if lineage is not None:
amount = uint64(amount + record.coin.amount)
self.log.info(f"Confirmed balance for cc wallet {self.id()} is {amount}")
return uint64(amount)
async def get_unconfirmed_balance(self, unspent_records=None) -> uint128:
return await self.wallet_state_manager.get_unconfirmed_balance(self.id(), unspent_records)
async def get_max_send_amount(self, records=None):
spendable: List[WalletCoinRecord] = list(await self.get_cat_spendable_coins())
if len(spendable) == 0:
return 0
spendable.sort(reverse=True, key=lambda record: record.coin.amount)
if self.cost_of_single_tx is None:
coin = spendable[0].coin
txs = await self.generate_signed_transaction(
[coin.amount], [coin.puzzle_hash], coins={coin}, ignore_max_send_amount=True
)
program: BlockGenerator = simple_solution_generator(txs[0].spend_bundle)
# npc contains names of the coins removed, puzzle_hashes and their spend conditions
result: NPCResult = get_name_puzzle_conditions(
program,
self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM,
cost_per_byte=self.wallet_state_manager.constants.COST_PER_BYTE,
mempool_mode=True,
)
cost_result: uint64 = calculate_cost_of_program(
program.program, result, self.wallet_state_manager.constants.COST_PER_BYTE
)
self.cost_of_single_tx = cost_result
self.log.info(f"Cost of a single tx for CAT wallet: {self.cost_of_single_tx}")
max_cost = self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM / 2 # avoid full block TXs
current_cost = 0
total_amount = 0
total_coin_count = 0
for record in spendable:
current_cost += self.cost_of_single_tx
total_amount += record.coin.amount
total_coin_count += 1
if current_cost + self.cost_of_single_tx > max_cost:
break
return total_amount
async def get_name(self):
return self.wallet_info.name
async def set_name(self, new_name: str):
new_info = dataclasses.replace(self.wallet_info, name=new_name)
self.wallet_info = new_info
await self.wallet_state_manager.user_store.update_wallet(self.wallet_info, False)
def get_asset_id(self) -> str:
return bytes(self.cat_info.limitations_program_hash).hex()
async def set_tail_program(self, tail_program: str):
assert Program.fromhex(tail_program).get_tree_hash() == self.cat_info.limitations_program_hash
await self.save_info(
CATInfo(
self.cat_info.limitations_program_hash, Program.fromhex(tail_program), self.cat_info.lineage_proofs
),
False,
)
async def coin_added(self, coin: Coin, height: uint32):
"""Notification from wallet state manager that wallet has been received."""
self.log.info(f"CC wallet has been notified that {coin} was added")
search_for_parent: bool = True
inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash)
lineage_proof = LineageProof(coin.parent_coin_info, inner_puzzle.get_tree_hash(), coin.amount)
await self.add_lineage(coin.name(), lineage_proof, True)
for name, lineage_proofs in self.cat_info.lineage_proofs:
if coin.parent_coin_info == name:
search_for_parent = False
break
if search_for_parent:
data: Dict[str, Any] = {
"data": {
"action_data": {
"api_name": "request_puzzle_solution",
"height": height,
"coin_name": coin.parent_coin_info,
"received_coin": coin.name(),
}
}
}
data_str = dict_to_json_str(data)
await self.wallet_state_manager.create_action(
name="request_puzzle_solution",
wallet_id=self.id(),
wallet_type=self.type(),
callback="puzzle_solution_received",
done=False,
data=data_str,
in_transaction=True,
)
async def puzzle_solution_received(self, response: PuzzleSolutionResponse, action_id: int):
coin_name = response.coin_name
puzzle: Program = response.puzzle
matched, curried_args = match_cat_puzzle(puzzle)
if matched:
mod_hash, genesis_coin_checker_hash, inner_puzzle = curried_args
self.log.info(f"parent: {coin_name} inner_puzzle for parent is {inner_puzzle}")
parent_coin = None
coin_record = await self.wallet_state_manager.coin_store.get_coin_record(coin_name)
if coin_record is None:
coin_states: Optional[List[CoinState]] = await self.wallet_state_manager.get_coin_state([coin_name])
if coin_states is not None:
parent_coin = coin_states[0].coin
if coin_record is not None:
parent_coin = coin_record.coin
if parent_coin is None:
raise ValueError("Error in finding parent")
await self.add_lineage(
coin_name, LineageProof(parent_coin.parent_coin_info, inner_puzzle.get_tree_hash(), parent_coin.amount)
)
await self.wallet_state_manager.action_store.action_done(action_id)
else:
# The parent is not a CAT which means we need to scrub all of its children from our DB
child_coin_records = await self.wallet_state_manager.coin_store.get_coin_records_by_parent_id(coin_name)
if len(child_coin_records) > 0:
for record in child_coin_records:
if record.wallet_id == self.id():
await self.wallet_state_manager.coin_store.delete_coin_record(record.coin.name())
await self.remove_lineage(record.coin.name())
# We also need to make sure there's no record of the transaction
await self.wallet_state_manager.tx_store.delete_transaction_record(record.coin.name())
async def get_new_inner_hash(self) -> bytes32:
puzzle = await self.get_new_inner_puzzle()
return puzzle.get_tree_hash()
async def get_new_inner_puzzle(self) -> Program:
return await self.standard_wallet.get_new_puzzle()
async def get_new_puzzlehash(self) -> bytes32:
return await self.standard_wallet.get_new_puzzlehash()
def puzzle_for_pk(self, pubkey) -> Program:
inner_puzzle = self.standard_wallet.puzzle_for_pk(bytes(pubkey))
cc_puzzle: Program = construct_cat_puzzle(CAT_MOD, self.cat_info.limitations_program_hash, inner_puzzle)
return cc_puzzle
async def get_new_cat_puzzle_hash(self):
return (await self.wallet_state_manager.get_unused_derivation_record(self.id())).puzzle_hash
async def get_spendable_balance(self, records=None) -> uint64:
coins = await self.get_cat_spendable_coins(records)
amount = 0
for record in coins:
amount += record.coin.amount
return uint64(amount)
async def get_pending_change_balance(self) -> uint64:
unconfirmed_tx = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.id())
addition_amount = 0
for record in unconfirmed_tx:
if not record.is_in_mempool():
continue
our_spend = False
for coin in record.removals:
if await self.wallet_state_manager.does_coin_belong_to_wallet(coin, self.id()):
our_spend = True
break
if our_spend is not True:
continue
for coin in record.additions:
if await self.wallet_state_manager.does_coin_belong_to_wallet(coin, self.id()):
addition_amount += coin.amount
return uint64(addition_amount)
async def get_cat_spendable_coins(self, records=None) -> List[WalletCoinRecord]:
result: List[WalletCoinRecord] = []
record_list: Set[WalletCoinRecord] = await self.wallet_state_manager.get_spendable_coins_for_wallet(
self.id(), records
)
for record in record_list:
lineage = await self.get_lineage_proof_for_coin(record.coin)
if lineage is not None and not lineage.is_none():
result.append(record)
return result
async def select_coins(self, amount: uint64) -> Set[Coin]:
"""
Returns a set of coins that can be used for generating a new transaction.
Note: Must be called under wallet state manager lock
"""
spendable_am = await self.get_confirmed_balance()
if amount > spendable_am:
error_msg = f"Can't select amount higher than our spendable balance {amount}, spendable {spendable_am}"
self.log.warning(error_msg)
raise ValueError(error_msg)
self.log.info(f"About to select coins for amount {amount}")
spendable: List[WalletCoinRecord] = await self.get_cat_spendable_coins()
sum = 0
used_coins: Set = set()
# Use older coins first
spendable.sort(key=lambda r: r.confirmed_block_height)
# Try to use coins from the store, if there isn't enough of "unused"
# coins use change coins that are not confirmed yet
unconfirmed_removals: Dict[bytes32, Coin] = await self.wallet_state_manager.unconfirmed_removals_for_wallet(
self.id()
)
for coinrecord in spendable:
if sum >= amount and len(used_coins) > 0:
break
if coinrecord.coin.name() in unconfirmed_removals:
continue
sum += coinrecord.coin.amount
used_coins.add(coinrecord.coin)
self.log.info(f"Selected coin: {coinrecord.coin.name()} at height {coinrecord.confirmed_block_height}!")
# This happens when we couldn't use one of the coins because it's already used
# but unconfirmed, and we are waiting for the change. (unconfirmed_additions)
if sum < amount:
raise ValueError(
"Can't make this transaction at the moment. Waiting for the change from the previous transaction."
)
self.log.info(f"Successfully selected coins: {used_coins}")
return used_coins
async def sign(self, spend_bundle: SpendBundle) -> SpendBundle:
sigs: List[G2Element] = []
for spend in spend_bundle.coin_spends:
matched, puzzle_args = match_cat_puzzle(spend.puzzle_reveal.to_program())
if matched:
_, _, inner_puzzle = puzzle_args
puzzle_hash = inner_puzzle.get_tree_hash()
pubkey, private = await self.wallet_state_manager.get_keys(puzzle_hash)
synthetic_secret_key = calculate_synthetic_secret_key(private, DEFAULT_HIDDEN_PUZZLE_HASH)
error, conditions, cost = conditions_dict_for_solution(
spend.puzzle_reveal.to_program(),
spend.solution.to_program(),
self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM,
)
if conditions is not None:
synthetic_pk = synthetic_secret_key.get_g1()
for pk, msg in pkm_pairs_for_conditions_dict(
conditions, spend.coin.name(), self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA
):
try:
assert bytes(synthetic_pk) == pk
sigs.append(AugSchemeMPL.sign(synthetic_secret_key, msg))
except AssertionError:
raise ValueError("This spend bundle cannot be signed by the CAT wallet")
agg_sig = AugSchemeMPL.aggregate(sigs)
return SpendBundle.aggregate([spend_bundle, SpendBundle([], agg_sig)])
async def inner_puzzle_for_cc_puzhash(self, cc_hash: bytes32) -> Program:
record: DerivationRecord = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(
cc_hash
)
inner_puzzle: Program = self.standard_wallet.puzzle_for_pk(bytes(record.pubkey))
return inner_puzzle
async def convert_puzzle_hash(self, puzzle_hash: bytes32) -> bytes32:
record: DerivationRecord = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(
puzzle_hash
)
if record is None:
return puzzle_hash
else:
return (await self.inner_puzzle_for_cc_puzhash(puzzle_hash)).get_tree_hash()
async def get_lineage_proof_for_coin(self, coin) -> Optional[LineageProof]:
for name, proof in self.cat_info.lineage_proofs:
if name == coin.parent_coin_info:
return proof
return None
async def create_tandem_xch_tx(
self,
fee: uint64,
amount_to_claim: uint64,
announcement_to_assert: Optional[Announcement] = None,
) -> Tuple[TransactionRecord, Optional[Announcement]]:
"""
This function creates a non-CAT transaction to pay fees, contribute funds for issuance, and absorb melt value.
It is meant to be called in `generate_unsigned_spendbundle` and as such should be called under the
wallet_state_manager lock
"""
announcement = None
if fee > amount_to_claim:
chia_coins = await self.standard_wallet.select_coins(fee)
origin_id = list(chia_coins)[0].name()
chia_tx = await self.standard_wallet.generate_signed_transaction(
uint64(0),
(await self.standard_wallet.get_new_puzzlehash()),
fee=uint64(fee - amount_to_claim),
coins=chia_coins,
origin_id=origin_id, # We specify this so that we know the coin that is making the announcement
negative_change_allowed=False,
coin_announcements_to_consume={announcement_to_assert} if announcement_to_assert is not None else None,
)
assert chia_tx.spend_bundle is not None
message = None
for spend in chia_tx.spend_bundle.coin_spends:
if spend.coin.name() == origin_id:
conditions = spend.puzzle_reveal.to_program().run(spend.solution.to_program()).as_python()
for condition in conditions:
if condition[0] == ConditionOpcode.CREATE_COIN_ANNOUNCEMENT:
message = condition[1]
assert message is not None
announcement = Announcement(origin_id, message)
else:
chia_coins = await self.standard_wallet.select_coins(fee)
selected_amount = sum([c.amount for c in chia_coins])
chia_tx = await self.standard_wallet.generate_signed_transaction(
uint64(selected_amount + amount_to_claim - fee),
(await self.standard_wallet.get_new_puzzlehash()),
coins=chia_coins,
negative_change_allowed=True,
coin_announcements_to_consume={announcement_to_assert} if announcement_to_assert is not None else None,
)
assert chia_tx.spend_bundle is not None
return chia_tx, announcement
async def generate_unsigned_spendbundle(
self,
payments: List[Payment],
fee: uint64 = uint64(0),
cat_discrepancy: Optional[Tuple[int, Program]] = None, # (extra_delta, limitations_solution)
coins: Set[Coin] = None,
coin_announcements_to_consume: Optional[Set[Announcement]] = None,
puzzle_announcements_to_consume: Optional[Set[Announcement]] = None,
) -> Tuple[SpendBundle, Optional[TransactionRecord]]:
if coin_announcements_to_consume is not None:
coin_announcements_bytes: Optional[Set[bytes32]] = {a.name() for a in coin_announcements_to_consume}
else:
coin_announcements_bytes = None
if puzzle_announcements_to_consume is not None:
puzzle_announcements_bytes: Optional[Set[bytes32]] = {a.name() for a in puzzle_announcements_to_consume}
else:
puzzle_announcements_bytes = None
if cat_discrepancy is not None:
extra_delta, limitations_solution = cat_discrepancy
else:
extra_delta, limitations_solution = 0, Program.to([])
payment_amount: int = sum([p.amount for p in payments])
starting_amount: int = payment_amount - extra_delta
if coins is None:
cat_coins = await self.select_coins(uint64(starting_amount))
else:
cat_coins = coins
selected_cat_amount = sum([c.amount for c in cat_coins])
assert selected_cat_amount >= starting_amount
# Figure out if we need to absorb/melt some XCH as part of this
regular_chia_to_claim: int = 0
if payment_amount > starting_amount:
fee = uint64(fee + payment_amount - starting_amount)
elif payment_amount < starting_amount:
regular_chia_to_claim = payment_amount
need_chia_transaction = (fee > 0 or regular_chia_to_claim > 0) and (fee - regular_chia_to_claim != 0)
# Calculate standard puzzle solutions
change = selected_cat_amount - starting_amount
primaries: List[AmountWithPuzzlehash] = []
for payment in payments:
primaries.append({"puzzlehash": payment.puzzle_hash, "amount": payment.amount, "memos": payment.memos})
if change > 0:
changepuzzlehash = await self.get_new_inner_hash()
primaries.append({"puzzlehash": changepuzzlehash, "amount": uint64(change), "memos": []})
limitations_program_reveal = Program.to([])
if self.cat_info.my_tail is None:
assert cat_discrepancy is None
elif cat_discrepancy is not None:
limitations_program_reveal = self.cat_info.my_tail
# Loop through the coins we've selected and gather the information we need to spend them
spendable_cc_list = []
chia_tx = None
first = True
for coin in cat_coins:
if first:
first = False
if need_chia_transaction:
if fee > regular_chia_to_claim:
announcement = Announcement(coin.name(), b"$", b"\xca")
chia_tx, _ = await self.create_tandem_xch_tx(
fee, uint64(regular_chia_to_claim), announcement_to_assert=announcement
)
innersol = self.standard_wallet.make_solution(
primaries=primaries,
coin_announcements={announcement.message},
coin_announcements_to_assert=coin_announcements_bytes,
puzzle_announcements_to_assert=puzzle_announcements_bytes,
)
elif regular_chia_to_claim > fee:
chia_tx, _ = await self.create_tandem_xch_tx(fee, uint64(regular_chia_to_claim))
innersol = self.standard_wallet.make_solution(
primaries=primaries, coin_announcements_to_assert={announcement.name()}
)
else:
innersol = self.standard_wallet.make_solution(
primaries=primaries,
coin_announcements_to_assert=coin_announcements_bytes,
puzzle_announcements_to_assert=puzzle_announcements_bytes,
)
else:
innersol = self.standard_wallet.make_solution(primaries=[])
inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash)
lineage_proof = await self.get_lineage_proof_for_coin(coin)
assert lineage_proof is not None
new_spendable_cc = SpendableCAT(
coin,
self.cat_info.limitations_program_hash,
inner_puzzle,
innersol,
limitations_solution=limitations_solution,
extra_delta=extra_delta,
lineage_proof=lineage_proof,
limitations_program_reveal=limitations_program_reveal,
)
spendable_cc_list.append(new_spendable_cc)
cat_spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cc_list)
chia_spend_bundle = SpendBundle([], G2Element())
if chia_tx is not None and chia_tx.spend_bundle is not None:
chia_spend_bundle = chia_tx.spend_bundle
return (
SpendBundle.aggregate(
[
cat_spend_bundle,
chia_spend_bundle,
]
),
chia_tx,
)
async def generate_signed_transaction(
self,
amounts: List[uint64],
puzzle_hashes: List[bytes32],
fee: uint64 = uint64(0),
coins: Set[Coin] = None,
ignore_max_send_amount: bool = False,
memos: Optional[List[List[bytes]]] = None,
coin_announcements_to_consume: Optional[Set[Announcement]] = None,
puzzle_announcements_to_consume: Optional[Set[Announcement]] = None,
) -> List[TransactionRecord]:
if memos is None:
memos = [[] for _ in range(len(puzzle_hashes))]
if not (len(memos) == len(puzzle_hashes) == len(amounts)):
raise ValueError("Memos, puzzle_hashes, and amounts must have the same length")
payments = []
for amount, puzhash, memo_list in zip(amounts, puzzle_hashes, memos):
memos_with_hint: List[bytes] = [puzhash]
memos_with_hint.extend(memo_list)
payments.append(Payment(puzhash, amount, memos_with_hint))
payment_sum = sum([p.amount for p in payments])
if not ignore_max_send_amount:
max_send = await self.get_max_send_amount()
if payment_sum > max_send:
raise ValueError(f"Can't send more than {max_send} in a single transaction")
unsigned_spend_bundle, chia_tx = await self.generate_unsigned_spendbundle(
payments,
fee,
coins=coins,
coin_announcements_to_consume=coin_announcements_to_consume,
puzzle_announcements_to_consume=puzzle_announcements_to_consume,
)
spend_bundle = await self.sign(unsigned_spend_bundle)
# TODO add support for array in stored records
tx_list = [
TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=puzzle_hashes[0],
amount=uint64(payment_sum),
fee_amount=fee,
confirmed=False,
sent=uint32(0),
spend_bundle=spend_bundle,
additions=spend_bundle.additions(),
removals=spend_bundle.removals(),
wallet_id=self.id(),
sent_to=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=spend_bundle.name(),
memos=list(spend_bundle.get_memos().items()),
)
]
if chia_tx is not None:
tx_list.append(
TransactionRecord(
confirmed_at_height=chia_tx.confirmed_at_height,
created_at_time=chia_tx.created_at_time,
to_puzzle_hash=chia_tx.to_puzzle_hash,
amount=chia_tx.amount,
fee_amount=chia_tx.fee_amount,
confirmed=chia_tx.confirmed,
sent=chia_tx.sent,
spend_bundle=None,
additions=chia_tx.additions,
removals=chia_tx.removals,
wallet_id=chia_tx.wallet_id,
sent_to=chia_tx.sent_to,
trade_id=chia_tx.trade_id,
type=chia_tx.type,
name=chia_tx.name,
memos=[],
)
)
return tx_list
async def add_lineage(self, name: bytes32, lineage: Optional[LineageProof], in_transaction=False):
"""
Lineage proofs are stored as a list of parent coins and the lineage proof you will need if they are the
parent of the coin you are trying to spend. 'If I'm your parent, here's the info you need to spend yourself'
"""
self.log.info(f"Adding parent {name}: {lineage}")
current_list = self.cat_info.lineage_proofs.copy()
if (name, lineage) not in current_list:
current_list.append((name, lineage))
cat_info: CATInfo = CATInfo(self.cat_info.limitations_program_hash, self.cat_info.my_tail, current_list)
await self.save_info(cat_info, in_transaction)
async def remove_lineage(self, name: bytes32, in_transaction=False):
self.log.info(f"Removing parent {name} (probably had a non-CAT parent)")
current_list = self.cat_info.lineage_proofs.copy()
current_list = list(filter(lambda tup: tup[0] != name, current_list))
cat_info: CATInfo = CATInfo(self.cat_info.limitations_program_hash, self.cat_info.my_tail, current_list)
await self.save_info(cat_info, in_transaction)
async def save_info(self, cat_info: CATInfo, in_transaction):
self.cat_info = cat_info
current_info = self.wallet_info
data_str = bytes(cat_info).hex()
wallet_info = WalletInfo(current_info.id, current_info.name, current_info.type, data_str)
self.wallet_info = wallet_info
await self.wallet_state_manager.user_store.update_wallet(wallet_info, in_transaction)

View File

@ -1,255 +0,0 @@
import dataclasses
from typing import List, Optional, Tuple
from blspy import AugSchemeMPL, G2Element
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program, INFINITE_COST
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.condition_opcodes import ConditionOpcode
from chia.types.spend_bundle import CoinSpend, SpendBundle
from chia.util.condition_tools import conditions_dict_for_solution
from chia.util.ints import uint64
from chia.wallet.puzzles.cc_loader import CC_MOD, LOCK_INNER_PUZZLE
from chia.wallet.puzzles.genesis_by_coin_id_with_0 import (
genesis_coin_id_for_genesis_coin_checker,
lineage_proof_for_coin,
lineage_proof_for_genesis,
lineage_proof_for_zero,
)
NULL_SIGNATURE = G2Element()
ANYONE_CAN_SPEND_PUZZLE = Program.to(1) # simply return the conditions
# information needed to spend a cc
# if we ever support more genesis conditions, like a re-issuable coin,
# we may need also to save the `genesis_coin_mod` or its hash
@dataclasses.dataclass
class SpendableCC:
coin: Coin
genesis_coin_id: bytes32
inner_puzzle: Program
lineage_proof: Program
def cc_puzzle_for_inner_puzzle(mod_code, genesis_coin_checker, inner_puzzle) -> Program:
"""
Given an inner puzzle, generate a puzzle program for a specific cc.
"""
return mod_code.curry(mod_code.get_tree_hash(), genesis_coin_checker, inner_puzzle)
# return mod_code.curry([mod_code.get_tree_hash(), genesis_coin_checker, inner_puzzle])
def cc_puzzle_hash_for_inner_puzzle_hash(mod_code, genesis_coin_checker, inner_puzzle_hash) -> bytes32:
"""
Given an inner puzzle hash, calculate a puzzle program hash for a specific cc.
"""
gcc_hash = genesis_coin_checker.get_tree_hash()
return mod_code.curry(mod_code.get_tree_hash(), gcc_hash, inner_puzzle_hash).get_tree_hash(
gcc_hash, inner_puzzle_hash
)
def lineage_proof_for_cc_parent(parent_coin: Coin, parent_inner_puzzle_hash: bytes32) -> Program:
return Program.to(
(
1,
[parent_coin.parent_coin_info, parent_inner_puzzle_hash, parent_coin.amount],
)
)
def subtotals_for_deltas(deltas) -> List[int]:
"""
Given a list of deltas corresponding to input coins, create the "subtotals" list
needed in solutions spending those coins.
"""
subtotals = []
subtotal = 0
for delta in deltas:
subtotals.append(subtotal)
subtotal += delta
# tweak the subtotals so the smallest value is 0
subtotal_offset = min(subtotals)
subtotals = [_ - subtotal_offset for _ in subtotals]
return subtotals
def coin_spend_for_lock_coin(
prev_coin: Coin,
subtotal: int,
coin: Coin,
) -> CoinSpend:
puzzle_reveal = LOCK_INNER_PUZZLE.curry(prev_coin.as_list(), subtotal)
coin = Coin(coin.name(), puzzle_reveal.get_tree_hash(), uint64(0))
coin_spend = CoinSpend(coin, puzzle_reveal, Program.to(0))
return coin_spend
def bundle_for_spendable_cc_list(spendable_cc: SpendableCC) -> Program:
pair = (spendable_cc.coin.as_list(), spendable_cc.lineage_proof)
return Program.to(pair)
def spend_bundle_for_spendable_ccs(
mod_code: Program,
genesis_coin_checker: Program,
spendable_cc_list: List[SpendableCC],
inner_solutions: List[Program],
sigs: Optional[List[G2Element]] = [],
) -> SpendBundle:
"""
Given a list of `SpendableCC` objects and inner solutions for those objects, create a `SpendBundle`
that spends all those coins. Note that it the signature is not calculated it, so the caller is responsible
for fixing it.
"""
N = len(spendable_cc_list)
if len(inner_solutions) != N:
raise ValueError("spendable_cc_list and inner_solutions are different lengths")
input_coins = [_.coin for _ in spendable_cc_list]
# figure out what the output amounts are by running the inner puzzles & solutions
output_amounts = []
for cc_spend_info, inner_solution in zip(spendable_cc_list, inner_solutions):
error, conditions, cost = conditions_dict_for_solution(
cc_spend_info.inner_puzzle, inner_solution, INFINITE_COST
)
total = 0
if conditions:
for _ in conditions.get(ConditionOpcode.CREATE_COIN, []):
total += Program.to(_.vars[1]).as_int()
output_amounts.append(total)
coin_spends = []
deltas = [input_coins[_].amount - output_amounts[_] for _ in range(N)]
subtotals = subtotals_for_deltas(deltas)
if sum(deltas) != 0:
raise ValueError("input and output amounts don't match")
bundles = [bundle_for_spendable_cc_list(_) for _ in spendable_cc_list]
for index in range(N):
cc_spend_info = spendable_cc_list[index]
puzzle_reveal = cc_puzzle_for_inner_puzzle(mod_code, genesis_coin_checker, cc_spend_info.inner_puzzle)
prev_index = (index - 1) % N
next_index = (index + 1) % N
prev_bundle = bundles[prev_index]
my_bundle = bundles[index]
next_bundle = bundles[next_index]
solution = [
inner_solutions[index],
prev_bundle,
my_bundle,
next_bundle,
subtotals[index],
]
coin_spend = CoinSpend(input_coins[index], puzzle_reveal, Program.to(solution))
coin_spends.append(coin_spend)
if sigs is None or sigs == []:
return SpendBundle(coin_spends, NULL_SIGNATURE)
else:
return SpendBundle(coin_spends, AugSchemeMPL.aggregate(sigs))
def is_cc_mod(inner_f: Program):
"""
You may want to generalize this if different `CC_MOD` templates are supported.
"""
return inner_f == CC_MOD
def check_is_cc_puzzle(puzzle: Program):
r = puzzle.uncurry()
if r is None:
return False
inner_f, args = r
return is_cc_mod(inner_f)
def uncurry_cc(puzzle: Program) -> Optional[Tuple[Program, Program, Program]]:
"""
Take a puzzle and return `None` if it's not a `CC_MOD` cc, or
a triple of `mod_hash, genesis_coin_checker, inner_puzzle` if it is.
"""
r = puzzle.uncurry()
if r is None:
return r
inner_f, args = r
if not is_cc_mod(inner_f):
return None
mod_hash, genesis_coin_checker, inner_puzzle = list(args.as_iter())
return mod_hash, genesis_coin_checker, inner_puzzle
def get_lineage_proof_from_coin_and_puz(parent_coin, parent_puzzle):
r = uncurry_cc(parent_puzzle)
if r:
mod_hash, genesis_checker, inner_puzzle = r
lineage_proof = lineage_proof_for_cc_parent(parent_coin, inner_puzzle.get_tree_hash())
else:
if parent_coin.amount == 0:
lineage_proof = lineage_proof_for_zero(parent_coin)
else:
lineage_proof = lineage_proof_for_genesis(parent_coin)
return lineage_proof
def spendable_cc_list_from_coin_spend(coin_spend: CoinSpend, hash_to_puzzle_f) -> List[SpendableCC]:
"""
Given a `CoinSpend`, extract out a list of `SpendableCC` objects.
Since `SpendableCC` needs to track the inner puzzles and a `Coin` only includes
puzzle hash, we also need a `hash_to_puzzle_f` function that turns puzzle hashes into
the corresponding puzzles. This is generally either a `dict` or some kind of DB
(if it's large or persistent).
"""
spendable_cc_list = []
coin = coin_spend.coin
puzzle = Program.from_bytes(bytes(coin_spend.puzzle_reveal))
r = uncurry_cc(puzzle)
if r:
mod_hash, genesis_coin_checker, inner_puzzle = r
lineage_proof = lineage_proof_for_cc_parent(coin, inner_puzzle.get_tree_hash())
else:
lineage_proof = lineage_proof_for_coin(coin)
for new_coin in coin_spend.additions():
puzzle = hash_to_puzzle_f(new_coin.puzzle_hash)
if puzzle is None:
# we don't recognize this puzzle hash, skip it
continue
r = uncurry_cc(puzzle)
if r is None:
# this isn't a cc puzzle
continue
mod_hash, genesis_coin_checker, inner_puzzle = r
genesis_coin_id = genesis_coin_id_for_genesis_coin_checker(genesis_coin_checker)
# TODO: address hint error and remove ignore
# error: Argument 2 to "SpendableCC" has incompatible type "Optional[bytes32]"; expected "bytes32"
# [arg-type]
cc_spend_info = SpendableCC(new_coin, genesis_coin_id, inner_puzzle, lineage_proof) # type: ignore[arg-type]
spendable_cc_list.append(cc_spend_info)
return spendable_cc_list

View File

@ -1,765 +0,0 @@
from __future__ import annotations
import logging
import time
from dataclasses import replace
from secrets import token_bytes
from typing import Any, Dict, List, Optional, Set
from blspy import AugSchemeMPL, G2Element
from chia.consensus.cost_calculator import calculate_cost_of_program, NPCResult
from chia.full_node.bundle_tools import simple_solution_generator
from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions
from chia.protocols.wallet_protocol import PuzzleSolutionResponse
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_spend import CoinSpend
from chia.types.generator_types import BlockGenerator
from chia.types.spend_bundle import SpendBundle
from chia.util.byte_types import hexstr_to_bytes
from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict
from chia.util.ints import uint8, uint32, uint64, uint128
from chia.util.json_util import dict_to_json_str
from chia.wallet.block_record import HeaderBlockRecord
from chia.wallet.cc_wallet.cc_info import CCInfo
from chia.wallet.cc_wallet.cc_utils import (
CC_MOD,
SpendableCC,
cc_puzzle_for_inner_puzzle,
cc_puzzle_hash_for_inner_puzzle_hash,
get_lineage_proof_from_coin_and_puz,
spend_bundle_for_spendable_ccs,
uncurry_cc,
)
from chia.wallet.derivation_record import DerivationRecord
from chia.wallet.puzzles.genesis_by_coin_id_with_0 import (
create_genesis_or_zero_coin_checker,
genesis_coin_id_for_genesis_coin_checker,
lineage_proof_for_genesis,
)
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
DEFAULT_HIDDEN_PUZZLE_HASH,
calculate_synthetic_secret_key,
)
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.wallet_types import AmountWithPuzzlehash, WalletType
from chia.wallet.wallet import Wallet
from chia.wallet.wallet_coin_record import WalletCoinRecord
from chia.wallet.wallet_info import WalletInfo
class CCWallet:
wallet_state_manager: Any
log: logging.Logger
wallet_info: WalletInfo
cc_coin_record: WalletCoinRecord
cc_info: CCInfo
standard_wallet: Wallet
base_puzzle_program: Optional[bytes]
base_inner_puzzle_hash: Optional[bytes32]
cost_of_single_tx: Optional[int]
@staticmethod
async def create_new_cc(
wallet_state_manager: Any,
wallet: Wallet,
amount: uint64,
):
self = CCWallet()
self.cost_of_single_tx = None
self.base_puzzle_program = None
self.base_inner_puzzle_hash = None
self.standard_wallet = wallet
self.log = logging.getLogger(__name__)
std_wallet_id = self.standard_wallet.wallet_id
bal = await wallet_state_manager.get_confirmed_balance_for_wallet(std_wallet_id, None)
if amount > bal:
raise ValueError("Not enough balance")
self.wallet_state_manager = wallet_state_manager
self.cc_info = CCInfo(None, [])
info_as_string = bytes(self.cc_info).hex()
self.wallet_info = await wallet_state_manager.user_store.create_wallet(
"CC Wallet", WalletType.COLOURED_COIN, info_as_string
)
if self.wallet_info is None:
raise ValueError("Internal Error")
try:
spend_bundle = await self.generate_new_coloured_coin(amount)
except Exception:
await wallet_state_manager.user_store.delete_wallet(self.id())
raise
if spend_bundle is None:
await wallet_state_manager.user_store.delete_wallet(self.id())
raise ValueError("Failed to create spend.")
await self.wallet_state_manager.add_new_wallet(self, self.id())
# Change and actual coloured coin
non_ephemeral_spends: List[Coin] = spend_bundle.not_ephemeral_additions()
cc_coin = None
puzzle_store = self.wallet_state_manager.puzzle_store
for c in non_ephemeral_spends:
info = await puzzle_store.wallet_info_for_puzzle_hash(c.puzzle_hash)
if info is None:
raise ValueError("Internal Error")
id, wallet_type = info
if id == self.id():
cc_coin = c
if cc_coin is None:
raise ValueError("Internal Error, unable to generate new coloured coin")
# TODO: address hint error and remove ignore
# error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32"
# [arg-type]
regular_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=cc_coin.puzzle_hash,
amount=uint64(cc_coin.amount),
fee_amount=uint64(0),
confirmed=False,
sent=uint32(0),
spend_bundle=spend_bundle,
additions=spend_bundle.additions(),
removals=spend_bundle.removals(),
wallet_id=self.wallet_state_manager.main_wallet.id(),
sent_to=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=token_bytes(), # type: ignore[arg-type]
)
# TODO: address hint error and remove ignore
# error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32"
# [arg-type]
cc_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=cc_coin.puzzle_hash,
amount=uint64(cc_coin.amount),
fee_amount=uint64(0),
confirmed=False,
sent=uint32(10),
spend_bundle=None,
additions=spend_bundle.additions(),
removals=spend_bundle.removals(),
wallet_id=self.id(),
sent_to=[],
trade_id=None,
type=uint32(TransactionType.INCOMING_TX.value),
name=token_bytes(), # type: ignore[arg-type]
)
await self.standard_wallet.push_transaction(regular_record)
await self.standard_wallet.push_transaction(cc_record)
return self
@staticmethod
async def create_wallet_for_cc(
wallet_state_manager: Any,
wallet: Wallet,
genesis_checker_hex: str,
) -> CCWallet:
self = CCWallet()
self.cost_of_single_tx = None
self.base_puzzle_program = None
self.base_inner_puzzle_hash = None
self.standard_wallet = wallet
self.log = logging.getLogger(__name__)
self.wallet_state_manager = wallet_state_manager
self.cc_info = CCInfo(Program.from_bytes(bytes.fromhex(genesis_checker_hex)), [])
info_as_string = bytes(self.cc_info).hex()
self.wallet_info = await wallet_state_manager.user_store.create_wallet(
"CC Wallet", WalletType.COLOURED_COIN, info_as_string
)
if self.wallet_info is None:
raise Exception("wallet_info is None")
await self.wallet_state_manager.add_new_wallet(self, self.id())
return self
@staticmethod
async def create(
wallet_state_manager: Any,
wallet: Wallet,
wallet_info: WalletInfo,
) -> CCWallet:
self = CCWallet()
self.log = logging.getLogger(__name__)
self.cost_of_single_tx = None
self.wallet_state_manager = wallet_state_manager
self.wallet_info = wallet_info
self.standard_wallet = wallet
self.cc_info = CCInfo.from_bytes(hexstr_to_bytes(self.wallet_info.data))
self.base_puzzle_program = None
self.base_inner_puzzle_hash = None
return self
@classmethod
def type(cls) -> uint8:
return uint8(WalletType.COLOURED_COIN)
def id(self) -> uint32:
return self.wallet_info.id
async def get_confirmed_balance(self, record_list: Optional[Set[WalletCoinRecord]] = None) -> uint64:
if record_list is None:
record_list = await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(self.id())
amount: uint64 = uint64(0)
for record in record_list:
lineage = await self.get_lineage_proof_for_coin(record.coin)
if lineage is not None:
amount = uint64(amount + record.coin.amount)
self.log.info(f"Confirmed balance for cc wallet {self.id()} is {amount}")
return uint64(amount)
async def get_unconfirmed_balance(self, unspent_records=None) -> uint128:
confirmed = await self.get_confirmed_balance(unspent_records)
unconfirmed_tx: List[TransactionRecord] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(
self.id()
)
addition_amount = 0
removal_amount = 0
for record in unconfirmed_tx:
if TransactionType(record.type) is TransactionType.INCOMING_TX:
addition_amount += record.amount
else:
removal_amount += record.amount
result = confirmed - removal_amount + addition_amount
self.log.info(f"Unconfirmed balance for cc wallet {self.id()} is {result}")
return uint128(result)
async def get_max_send_amount(self, records=None):
spendable: List[WalletCoinRecord] = list(
await self.wallet_state_manager.get_spendable_coins_for_wallet(self.id(), records)
)
if len(spendable) == 0:
return 0
spendable.sort(reverse=True, key=lambda record: record.coin.amount)
if self.cost_of_single_tx is None:
coin = spendable[0].coin
tx = await self.generate_signed_transaction(
[coin.amount], [coin.puzzle_hash], coins={coin}, ignore_max_send_amount=True
)
program: BlockGenerator = simple_solution_generator(tx.spend_bundle)
# npc contains names of the coins removed, puzzle_hashes and their spend conditions
result: NPCResult = get_name_puzzle_conditions(
program,
self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM,
cost_per_byte=self.wallet_state_manager.constants.COST_PER_BYTE,
mempool_mode=True,
)
cost_result: uint64 = calculate_cost_of_program(
program.program, result, self.wallet_state_manager.constants.COST_PER_BYTE
)
self.cost_of_single_tx = cost_result
self.log.info(f"Cost of a single tx for standard wallet: {self.cost_of_single_tx}")
max_cost = self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM / 2 # avoid full block TXs
current_cost = 0
total_amount = 0
total_coin_count = 0
for record in spendable:
current_cost += self.cost_of_single_tx
total_amount += record.coin.amount
total_coin_count += 1
if current_cost + self.cost_of_single_tx > max_cost:
break
return total_amount
async def get_name(self):
return self.wallet_info.name
async def set_name(self, new_name: str):
new_info = replace(self.wallet_info, name=new_name)
self.wallet_info = new_info
await self.wallet_state_manager.user_store.update_wallet(self.wallet_info, False)
def get_colour(self) -> str:
assert self.cc_info.my_genesis_checker is not None
return bytes(self.cc_info.my_genesis_checker).hex()
async def coin_added(self, coin: Coin, height: uint32):
"""Notification from wallet state manager that wallet has been received."""
self.log.info(f"CC wallet has been notified that {coin} was added")
search_for_parent: bool = True
inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash)
lineage_proof = Program.to((1, [coin.parent_coin_info, inner_puzzle.get_tree_hash(), coin.amount]))
await self.add_lineage(coin.name(), lineage_proof, True)
for name, lineage_proofs in self.cc_info.lineage_proofs:
if coin.parent_coin_info == name:
search_for_parent = False
break
if search_for_parent:
data: Dict[str, Any] = {
"data": {
"action_data": {
"api_name": "request_puzzle_solution",
"height": height,
"coin_name": coin.parent_coin_info,
"received_coin": coin.name(),
}
}
}
data_str = dict_to_json_str(data)
await self.wallet_state_manager.create_action(
name="request_puzzle_solution",
wallet_id=self.id(),
wallet_type=self.type(),
callback="puzzle_solution_received",
done=False,
data=data_str,
in_transaction=True,
)
async def puzzle_solution_received(self, response: PuzzleSolutionResponse, action_id: int):
coin_name = response.coin_name
height = response.height
puzzle: Program = response.puzzle
r = uncurry_cc(puzzle)
header_hash = self.wallet_state_manager.blockchain.height_to_hash(height)
block: Optional[
HeaderBlockRecord
] = await self.wallet_state_manager.blockchain.block_store.get_header_block_record(header_hash)
if block is None:
return None
removals = block.removals
if r is not None:
mod_hash, genesis_coin_checker, inner_puzzle = r
self.log.info(f"parent: {coin_name} inner_puzzle for parent is {inner_puzzle}")
parent_coin = None
for coin in removals:
if coin.name() == coin_name:
parent_coin = coin
if parent_coin is None:
raise ValueError("Error in finding parent")
lineage_proof = get_lineage_proof_from_coin_and_puz(parent_coin, puzzle)
await self.add_lineage(coin_name, lineage_proof)
await self.wallet_state_manager.action_store.action_done(action_id)
async def get_new_inner_hash(self) -> bytes32:
return await self.standard_wallet.get_new_puzzlehash()
async def get_new_inner_puzzle(self) -> Program:
return await self.standard_wallet.get_new_puzzle()
async def get_puzzle_hash(self, new: bool):
return await self.standard_wallet.get_puzzle_hash(new)
async def get_new_puzzlehash(self) -> bytes32:
return await self.standard_wallet.get_new_puzzlehash()
def puzzle_for_pk(self, pubkey) -> Program:
inner_puzzle = self.standard_wallet.puzzle_for_pk(bytes(pubkey))
cc_puzzle: Program = cc_puzzle_for_inner_puzzle(CC_MOD, self.cc_info.my_genesis_checker, inner_puzzle)
self.base_puzzle_program = bytes(cc_puzzle)
self.base_inner_puzzle_hash = inner_puzzle.get_tree_hash()
return cc_puzzle
async def get_new_cc_puzzle_hash(self):
return (await self.wallet_state_manager.get_unused_derivation_record(self.id())).puzzle_hash
# Create a new coin of value 0 with a given colour
async def generate_zero_val_coin(self, send=True, exclude: List[Coin] = None) -> SpendBundle:
if self.cc_info.my_genesis_checker is None:
raise ValueError("My genesis checker is None")
if exclude is None:
exclude = []
coins = await self.standard_wallet.select_coins(0, exclude)
assert coins != set()
origin = coins.copy().pop()
origin_id = origin.name()
cc_inner = await self.get_new_inner_hash()
cc_puzzle_hash: bytes32 = cc_puzzle_hash_for_inner_puzzle_hash(
CC_MOD, self.cc_info.my_genesis_checker, cc_inner
)
tx: TransactionRecord = await self.standard_wallet.generate_signed_transaction(
uint64(0), cc_puzzle_hash, uint64(0), origin_id, coins
)
assert tx.spend_bundle is not None
full_spend: SpendBundle = tx.spend_bundle
self.log.info(f"Generate zero val coin: cc_puzzle_hash is {cc_puzzle_hash}")
# generate eve coin so we can add future lineage_proofs even if we don't eve spend
eve_coin = Coin(origin_id, cc_puzzle_hash, uint64(0))
await self.add_lineage(
eve_coin.name(),
Program.to(
(
1,
[eve_coin.parent_coin_info, cc_inner, eve_coin.amount],
)
),
)
await self.add_lineage(eve_coin.parent_coin_info, Program.to((0, [origin.as_list(), 1])))
if send:
# TODO: address hint error and remove ignore
# error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32"
# [arg-type]
regular_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=cc_puzzle_hash,
amount=uint64(0),
fee_amount=uint64(0),
confirmed=False,
sent=uint32(10),
spend_bundle=full_spend,
additions=full_spend.additions(),
removals=full_spend.removals(),
wallet_id=uint32(1),
sent_to=[],
trade_id=None,
type=uint32(TransactionType.INCOMING_TX.value),
name=token_bytes(), # type: ignore[arg-type]
)
cc_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=cc_puzzle_hash,
amount=uint64(0),
fee_amount=uint64(0),
confirmed=False,
sent=uint32(0),
spend_bundle=full_spend,
additions=full_spend.additions(),
removals=full_spend.removals(),
wallet_id=self.id(),
sent_to=[],
trade_id=None,
type=uint32(TransactionType.INCOMING_TX.value),
name=full_spend.name(),
)
await self.wallet_state_manager.add_transaction(regular_record)
await self.wallet_state_manager.add_pending_transaction(cc_record)
return full_spend
async def get_spendable_balance(self, records=None) -> uint64:
coins = await self.get_cc_spendable_coins(records)
amount = 0
for record in coins:
amount += record.coin.amount
return uint64(amount)
async def get_pending_change_balance(self) -> uint64:
unconfirmed_tx = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.id())
addition_amount = 0
for record in unconfirmed_tx:
if not record.is_in_mempool():
continue
our_spend = False
for coin in record.removals:
# Don't count eve spend as change
if coin.parent_coin_info.hex() == self.get_colour():
continue
if await self.wallet_state_manager.does_coin_belong_to_wallet(coin, self.id()):
our_spend = True
break
if our_spend is not True:
continue
for coin in record.additions:
if await self.wallet_state_manager.does_coin_belong_to_wallet(coin, self.id()):
addition_amount += coin.amount
return uint64(addition_amount)
async def get_cc_spendable_coins(self, records=None) -> List[WalletCoinRecord]:
result: List[WalletCoinRecord] = []
record_list: Set[WalletCoinRecord] = await self.wallet_state_manager.get_spendable_coins_for_wallet(
self.id(), records
)
for record in record_list:
lineage = await self.get_lineage_proof_for_coin(record.coin)
if lineage is not None:
result.append(record)
return result
async def select_coins(self, amount: uint64) -> Set[Coin]:
"""
Returns a set of coins that can be used for generating a new transaction.
Note: Must be called under wallet state manager lock
"""
spendable_am = await self.get_confirmed_balance()
if amount > spendable_am:
error_msg = f"Can't select amount higher than our spendable balance {amount}, spendable {spendable_am}"
self.log.warning(error_msg)
raise ValueError(error_msg)
self.log.info(f"About to select coins for amount {amount}")
spendable: List[WalletCoinRecord] = await self.get_cc_spendable_coins()
sum = 0
used_coins: Set = set()
# Use older coins first
spendable.sort(key=lambda r: r.confirmed_block_height)
# Try to use coins from the store, if there isn't enough of "unused"
# coins use change coins that are not confirmed yet
unconfirmed_removals: Dict[bytes32, Coin] = await self.wallet_state_manager.unconfirmed_removals_for_wallet(
self.id()
)
for coinrecord in spendable:
if sum >= amount and len(used_coins) > 0:
break
if coinrecord.coin.name() in unconfirmed_removals:
continue
sum += coinrecord.coin.amount
used_coins.add(coinrecord.coin)
self.log.info(f"Selected coin: {coinrecord.coin.name()} at height {coinrecord.confirmed_block_height}!")
# This happens when we couldn't use one of the coins because it's already used
# but unconfirmed, and we are waiting for the change. (unconfirmed_additions)
if sum < amount:
raise ValueError(
"Can't make this transaction at the moment. Waiting for the change from the previous transaction."
)
self.log.info(f"Successfully selected coins: {used_coins}")
return used_coins
async def get_sigs(self, innerpuz: Program, innersol: Program, coin_name: bytes32) -> List[G2Element]:
puzzle_hash = innerpuz.get_tree_hash()
pubkey, private = await self.wallet_state_manager.get_keys(puzzle_hash)
synthetic_secret_key = calculate_synthetic_secret_key(private, DEFAULT_HIDDEN_PUZZLE_HASH)
sigs: List[G2Element] = []
error, conditions, cost = conditions_dict_for_solution(
innerpuz, innersol, self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM
)
if conditions is not None:
for _, msg in pkm_pairs_for_conditions_dict(
conditions, coin_name, self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA
):
signature = AugSchemeMPL.sign(synthetic_secret_key, msg)
sigs.append(signature)
return sigs
async def inner_puzzle_for_cc_puzhash(self, cc_hash: bytes32) -> Program:
record: DerivationRecord = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(
cc_hash
)
inner_puzzle: Program = self.standard_wallet.puzzle_for_pk(bytes(record.pubkey))
return inner_puzzle
async def get_lineage_proof_for_coin(self, coin) -> Optional[Program]:
for name, proof in self.cc_info.lineage_proofs:
if name == coin.parent_coin_info:
return proof
return None
async def generate_signed_transaction(
self,
amounts: List[uint64],
puzzle_hashes: List[bytes32],
fee: uint64 = uint64(0),
origin_id: bytes32 = None,
coins: Set[Coin] = None,
ignore_max_send_amount: bool = False,
) -> TransactionRecord:
# Get coins and calculate amount of change required
outgoing_amount = uint64(sum(amounts))
total_outgoing = outgoing_amount + fee
if not ignore_max_send_amount:
max_send = await self.get_max_send_amount()
if total_outgoing > max_send:
raise ValueError(f"Can't send more than {max_send} in a single transaction")
if coins is None:
selected_coins: Set[Coin] = await self.select_coins(uint64(total_outgoing))
else:
selected_coins = coins
total_amount = sum([x.amount for x in selected_coins])
change = total_amount - total_outgoing
primaries: List[AmountWithPuzzlehash] = []
for amount, puzzle_hash in zip(amounts, puzzle_hashes):
primaries.append({"puzzlehash": puzzle_hash, "amount": amount})
if change > 0:
changepuzzlehash = await self.get_new_inner_hash()
primaries.append({"puzzlehash": changepuzzlehash, "amount": uint64(change)})
coin = list(selected_coins)[0]
inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash)
if self.cc_info.my_genesis_checker is None:
raise ValueError("My genesis checker is None")
genesis_id = genesis_coin_id_for_genesis_coin_checker(self.cc_info.my_genesis_checker)
spendable_cc_list = []
innersol_list = []
sigs: List[G2Element] = []
first = True
for coin in selected_coins:
coin_inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash)
if first:
first = False
if fee > 0:
innersol = self.standard_wallet.make_solution(primaries=primaries, fee=fee)
else:
innersol = self.standard_wallet.make_solution(primaries=primaries)
else:
innersol = self.standard_wallet.make_solution()
innersol_list.append(innersol)
lineage_proof = await self.get_lineage_proof_for_coin(coin)
assert lineage_proof is not None
# TODO: address hint error and remove ignore
# error: Argument 2 to "SpendableCC" has incompatible type "Optional[bytes32]"; expected "bytes32"
# [arg-type]
spendable_cc_list.append(SpendableCC(coin, genesis_id, inner_puzzle, lineage_proof)) # type: ignore[arg-type] # noqa: E501
sigs = sigs + await self.get_sigs(coin_inner_puzzle, innersol, coin.name())
spend_bundle = spend_bundle_for_spendable_ccs(
CC_MOD,
self.cc_info.my_genesis_checker,
spendable_cc_list,
innersol_list,
sigs,
)
# TODO add support for array in stored records
return TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=puzzle_hashes[0],
amount=uint64(outgoing_amount),
fee_amount=uint64(0),
confirmed=False,
sent=uint32(0),
spend_bundle=spend_bundle,
additions=spend_bundle.additions(),
removals=spend_bundle.removals(),
wallet_id=self.id(),
sent_to=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=spend_bundle.name(),
)
async def add_lineage(self, name: bytes32, lineage: Optional[Program], in_transaction=False):
self.log.info(f"Adding parent {name}: {lineage}")
current_list = self.cc_info.lineage_proofs.copy()
current_list.append((name, lineage))
cc_info: CCInfo = CCInfo(self.cc_info.my_genesis_checker, current_list)
await self.save_info(cc_info, in_transaction)
async def save_info(self, cc_info: CCInfo, in_transaction):
self.cc_info = cc_info
current_info = self.wallet_info
data_str = bytes(cc_info).hex()
wallet_info = WalletInfo(current_info.id, current_info.name, current_info.type, data_str)
self.wallet_info = wallet_info
await self.wallet_state_manager.user_store.update_wallet(wallet_info, in_transaction)
async def generate_new_coloured_coin(self, amount: uint64) -> SpendBundle:
coins = await self.standard_wallet.select_coins(amount)
origin = coins.copy().pop()
origin_id = origin.name()
cc_inner_hash = await self.get_new_inner_hash()
await self.add_lineage(origin_id, Program.to((0, [origin.as_list(), 0])))
genesis_coin_checker = create_genesis_or_zero_coin_checker(origin_id)
minted_cc_puzzle_hash = cc_puzzle_hash_for_inner_puzzle_hash(CC_MOD, genesis_coin_checker, cc_inner_hash)
tx_record: TransactionRecord = await self.standard_wallet.generate_signed_transaction(
amount, minted_cc_puzzle_hash, uint64(0), origin_id, coins
)
assert tx_record.spend_bundle is not None
lineage_proof: Optional[Program] = lineage_proof_for_genesis(origin)
lineage_proofs = [(origin_id, lineage_proof)]
cc_info: CCInfo = CCInfo(genesis_coin_checker, lineage_proofs)
await self.save_info(cc_info, False)
return tx_record.spend_bundle
async def create_spend_bundle_relative_amount(self, cc_amount, zero_coin: Coin = None) -> Optional[SpendBundle]:
# If we're losing value then get coloured coins with at least that much value
# If we're gaining value then our amount doesn't matter
if cc_amount < 0:
cc_spends = await self.select_coins(abs(cc_amount))
else:
if zero_coin is None:
return None
cc_spends = set()
cc_spends.add(zero_coin)
if cc_spends is None:
return None
# Calculate output amount given relative difference and sum of actual values
spend_value = sum([coin.amount for coin in cc_spends])
cc_amount = spend_value + cc_amount
# Loop through coins and create solution for innerpuzzle
list_of_solutions = []
output_created = None
sigs: List[G2Element] = []
for coin in cc_spends:
if output_created is None:
newinnerpuzhash = await self.get_new_inner_hash()
innersol = self.standard_wallet.make_solution(
primaries=[{"puzzlehash": newinnerpuzhash, "amount": cc_amount}]
)
output_created = coin
else:
innersol = self.standard_wallet.make_solution(consumed=[output_created.name()])
innerpuz: Program = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash)
sigs = sigs + await self.get_sigs(innerpuz, innersol, coin.name())
lineage_proof = await self.get_lineage_proof_for_coin(coin)
puzzle_reveal = cc_puzzle_for_inner_puzzle(CC_MOD, self.cc_info.my_genesis_checker, innerpuz)
# Use coin info to create solution and add coin and solution to list of CoinSpends
solution = [
innersol,
coin.as_list(),
lineage_proof,
None,
None,
None,
None,
None,
]
list_of_solutions.append(CoinSpend(coin, puzzle_reveal, Program.to(solution)))
aggsig = AugSchemeMPL.aggregate(sigs)
return SpendBundle(list_of_solutions, aggsig)

View File

@ -19,3 +19,4 @@ class DerivationRecord:
pubkey: G1Element
wallet_type: WalletType
wallet_id: uint32
hardened: bool

View File

@ -17,6 +17,12 @@ def _derive_path(sk: PrivateKey, path: List[int]) -> PrivateKey:
return sk
def _derive_path_unhardened(sk: PrivateKey, path: List[int]) -> PrivateKey:
for index in path:
sk = AugSchemeMPL.derive_child_sk_unhardened(sk, index)
return sk
def master_sk_to_farmer_sk(master: PrivateKey) -> PrivateKey:
return _derive_path(master, [12381, 8444, 0, 0])
@ -25,8 +31,22 @@ def master_sk_to_pool_sk(master: PrivateKey) -> PrivateKey:
return _derive_path(master, [12381, 8444, 1, 0])
def master_sk_to_wallet_sk_intermediate(master: PrivateKey) -> PrivateKey:
return _derive_path(master, [12381, 8444, 2])
def master_sk_to_wallet_sk(master: PrivateKey, index: uint32) -> PrivateKey:
return _derive_path(master, [12381, 8444, 2, index])
intermediate = master_sk_to_wallet_sk_intermediate(master)
return _derive_path(intermediate, [index])
def master_sk_to_wallet_sk_unhardened_intermediate(master: PrivateKey) -> PrivateKey:
return _derive_path_unhardened(master, [12381, 8444, 2])
def master_sk_to_wallet_sk_unhardened(master: PrivateKey, index: uint32) -> PrivateKey:
intermediate = master_sk_to_wallet_sk_unhardened_intermediate(master)
return _derive_path_unhardened(intermediate, [index])
def master_sk_to_local_sk(master: PrivateKey) -> PrivateKey:

View File

@ -2,13 +2,11 @@ import logging
import time
import json
from typing import Dict, Optional, List, Any, Set, Tuple, Union
from typing import Dict, Optional, List, Any, Set, Tuple
from blspy import AugSchemeMPL, G1Element
from secrets import token_bytes
from chia.protocols import wallet_protocol
from chia.protocols.wallet_protocol import RespondAdditions, RejectAdditionsRequest
from chia.server.outbound_message import NodeType
from chia.protocols.wallet_protocol import CoinState
from chia.types.announcement import Announcement
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
@ -27,7 +25,7 @@ from chia.wallet.wallet_coin_record import WalletCoinRecord
from chia.wallet.wallet_info import WalletInfo
from chia.wallet.derivation_record import DerivationRecord
from chia.wallet.did_wallet import did_wallet_puzzles
from chia.wallet.derive_keys import master_sk_to_wallet_sk
from chia.wallet.derive_keys import master_sk_to_wallet_sk_unhardened
class DIDWallet:
@ -97,9 +95,6 @@ class DIDWallet:
self.did_info.current_inner, self.did_info.origin_coin.name()
).get_tree_hash()
# TODO: address hint error and remove ignore
# error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32"
# [arg-type]
did_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
@ -115,11 +110,9 @@ class DIDWallet:
sent_to=[],
trade_id=None,
type=uint32(TransactionType.INCOMING_TX.value),
name=token_bytes(), # type: ignore[arg-type]
name=bytes32(token_bytes()),
memos=[],
)
# TODO: address hint error and remove ignore
# error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32"
# [arg-type]
regular_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
@ -135,7 +128,8 @@ class DIDWallet:
sent_to=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=token_bytes(), # type: ignore[arg-type]
name=bytes32(token_bytes()),
memos=list(spend_bundle.get_memos().items()),
)
await self.standard_wallet.push_transaction(regular_record)
await self.standard_wallet.push_transaction(did_record)
@ -200,7 +194,7 @@ class DIDWallet:
amount: uint64 = uint64(0)
for record in record_list:
parent = await self.get_parent_for_coin(record.coin)
parent = self.get_parent_for_coin(record.coin)
if parent is not None:
amount = uint64(amount + record.coin.amount)
@ -279,7 +273,7 @@ class DIDWallet:
# This will be used in the recovery case where we don't have the parent info already
async def coin_added(self, coin: Coin, _: uint32):
"""Notification from wallet state manager that wallet has been received."""
self.log.info("DID wallet has been notified that coin was added")
self.log.info(f"DID wallet has been notified that coin was added: {coin.name()}:{coin}")
inner_puzzle = await self.inner_puzzle_for_did_puzzle(coin.puzzle_hash)
if self.did_info.temp_coin is not None:
self.wallet_state_manager.state_changed("did_coin_added", self.wallet_info.id)
@ -303,6 +297,27 @@ class DIDWallet:
)
await self.add_parent(coin.name(), future_parent, True)
parent = self.get_parent_for_coin(coin)
if parent is None:
parent_state: CoinState = (
await self.wallet_state_manager.wallet_node.get_coin_state([coin.parent_coin_info])
)[0]
node = self.wallet_state_manager.wallet_node.get_full_node_peer()
assert parent_state.spent_height is not None
puzzle_solution_request = wallet_protocol.RequestPuzzleSolution(
coin.parent_coin_info, parent_state.spent_height
)
response = await node.request_puzzle_solution(puzzle_solution_request)
req_puz_sol = response.response
assert req_puz_sol.puzzle is not None
parent_innerpuz = did_wallet_puzzles.get_innerpuzzle_from_puzzle(req_puz_sol.puzzle)
assert parent_innerpuz is not None
parent_info = LineageProof(
parent_state.coin.parent_coin_info,
parent_innerpuz.get_tree_hash(),
parent_state.coin.amount,
)
await self.add_parent(coin.parent_coin_info, parent_info, False)
def create_backup(self, filename: str):
assert self.did_info.current_inner is not None
@ -355,82 +370,61 @@ class DIDWallet:
await self.save_info(did_info, False)
await self.wallet_state_manager.update_wallet_puzzle_hashes(self.wallet_info.id)
full_puz = did_wallet_puzzles.create_fullpuz(innerpuz, origin.name())
full_puzzle_hash = full_puz.get_tree_hash()
(
sub_height,
header_hash,
) = await self.wallet_state_manager.search_blockrecords_for_puzzlehash(full_puzzle_hash)
assert sub_height is not None
assert header_hash is not None
full_nodes = self.wallet_state_manager.server.connection_by_type[NodeType.FULL_NODE]
additions: Union[RespondAdditions, RejectAdditionsRequest, None] = None
for id, node in full_nodes.items():
request = wallet_protocol.RequestAdditions(sub_height, header_hash, None)
additions = await node.request_additions(request)
if additions is not None:
break
if isinstance(additions, RejectAdditionsRequest):
continue
assert additions is not None
assert isinstance(additions, RespondAdditions)
# full_puz = did_wallet_puzzles.create_fullpuz(innerpuz, origin.name())
# All additions in this block here:
new_puzhash = await self.get_new_inner_hash()
new_pubkey = bytes(
(await self.wallet_state_manager.get_unused_derivation_record(self.wallet_info.id)).pubkey
)
all_parents: Set[bytes32] = set()
for puzzle_list_coin in additions.coins:
puzzle_hash, coins = puzzle_list_coin
for coin in coins:
all_parents.add(coin.parent_coin_info)
parent_info = None
for puzzle_list_coin in additions.coins:
puzzle_hash, coins = puzzle_list_coin
if puzzle_hash == full_puzzle_hash:
# our coin
for coin in coins:
future_parent = LineageProof(
coin.parent_coin_info,
innerpuz.get_tree_hash(),
coin.amount,
)
await self.add_parent(coin.name(), future_parent, False)
if coin.name() not in all_parents:
did_info = DIDInfo(
origin,
backup_ids,
num_of_backup_ids_needed,
self.did_info.parent_info,
innerpuz,
coin,
new_puzhash,
new_pubkey,
False,
)
await self.save_info(did_info, False)
removal_request = wallet_protocol.RequestRemovals(sub_height, header_hash, None)
removals_response = await node.request_removals(removal_request)
for coin_tuple in removals_response.coins:
if coin_tuple[0] == coin.parent_coin_info:
puzzle_solution_request = wallet_protocol.RequestPuzzleSolution(
coin.parent_coin_info, sub_height
)
response = await node.request_puzzle_solution(puzzle_solution_request)
req_puz_sol = response.response
assert req_puz_sol.puzzle is not None
parent_innerpuz = did_wallet_puzzles.get_innerpuzzle_from_puzzle(req_puz_sol.puzzle)
assert parent_innerpuz is not None
parent_info = LineageProof(
coin_tuple[1].parent_coin_info,
parent_innerpuz.get_tree_hash(),
coin_tuple[1].amount,
)
await self.add_parent(coin.parent_coin_info, parent_info, False)
break
node = self.wallet_state_manager.wallet_node.get_full_node_peer()
children = await self.wallet_state_manager.wallet_node.fetch_children(node, origin.name())
while True:
if len(children) == 0:
break
children_state: CoinState = children[0]
coin = children_state.coin
name = coin.name()
children = await self.wallet_state_manager.wallet_node.fetch_children(node, name)
future_parent = LineageProof(
coin.parent_coin_info,
innerpuz.get_tree_hash(),
coin.amount,
)
await self.add_parent(coin.name(), future_parent, False)
if children_state.spent_height != children_state.created_height:
did_info = DIDInfo(
origin,
backup_ids,
num_of_backup_ids_needed,
self.did_info.parent_info,
innerpuz,
coin,
new_puzhash,
new_pubkey,
False,
)
await self.save_info(did_info, False)
assert children_state.created_height
puzzle_solution_request = wallet_protocol.RequestPuzzleSolution(
coin.parent_coin_info, children_state.created_height
)
parent_state: CoinState = (
await self.wallet_state_manager.wallet_node.get_coin_state([coin.parent_coin_info])
)[0]
response = await node.request_puzzle_solution(puzzle_solution_request)
req_puz_sol = response.response
assert req_puz_sol.puzzle is not None
parent_innerpuz = did_wallet_puzzles.get_innerpuzzle_from_puzzle(req_puz_sol.puzzle)
assert parent_innerpuz is not None
parent_info = LineageProof(
parent_state.coin.parent_coin_info,
parent_innerpuz.get_tree_hash(),
parent_state.coin.amount,
)
await self.add_parent(coin.parent_coin_info, parent_info, False)
assert parent_info is not None
return None
except Exception as e:
@ -474,7 +468,7 @@ class DIDWallet:
innerpuz,
self.did_info.origin_coin.name(),
)
parent_info = await self.get_parent_for_coin(coin)
parent_info = self.get_parent_for_coin(coin)
assert parent_info is not None
fullsol = Program.to(
[
@ -496,7 +490,7 @@ class DIDWallet:
)
pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz)
index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey)
private = master_sk_to_wallet_sk(self.wallet_state_manager.private_key, index)
private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index)
signature = AugSchemeMPL.sign(private, message)
# assert signature.validate([signature.PkMessagePair(pubkey, message)])
sigs = [signature]
@ -519,6 +513,7 @@ class DIDWallet:
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=token_bytes(),
memos=list(spend_bundle.get_memos().items()),
)
await self.standard_wallet.push_transaction(did_record)
return spend_bundle
@ -541,7 +536,7 @@ class DIDWallet:
innerpuz,
self.did_info.origin_coin.name(),
)
parent_info = await self.get_parent_for_coin(coin)
parent_info = self.get_parent_for_coin(coin)
assert parent_info is not None
fullsol = Program.to(
[
@ -564,16 +559,13 @@ class DIDWallet:
)
pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz)
index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey)
private = master_sk_to_wallet_sk(self.wallet_state_manager.private_key, index)
private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index)
signature = AugSchemeMPL.sign(private, message)
# assert signature.validate([signature.PkMessagePair(pubkey, message)])
sigs = [signature]
aggsig = AugSchemeMPL.aggregate(sigs)
spend_bundle = SpendBundle(list_of_solutions, aggsig)
# TODO: address hint error and remove ignore
# error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32"
# [arg-type]
did_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
@ -589,7 +581,8 @@ class DIDWallet:
sent_to=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=token_bytes(), # type: ignore[arg-type]
name=bytes32(token_bytes()),
memos=list(spend_bundle.get_memos().items()),
)
await self.standard_wallet.push_transaction(did_record)
return spend_bundle
@ -611,7 +604,7 @@ class DIDWallet:
innerpuz,
self.did_info.origin_coin.name(),
)
parent_info = await self.get_parent_for_coin(coin)
parent_info = self.get_parent_for_coin(coin)
assert parent_info is not None
fullsol = Program.to(
[
@ -633,16 +626,13 @@ class DIDWallet:
)
pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz)
index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey)
private = master_sk_to_wallet_sk(self.wallet_state_manager.private_key, index)
private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index)
signature = AugSchemeMPL.sign(private, message)
# assert signature.validate([signature.PkMessagePair(pubkey, message)])
sigs = [signature]
aggsig = AugSchemeMPL.aggregate(sigs)
spend_bundle = SpendBundle(list_of_solutions, aggsig)
# TODO: address hint error and remove ignore
# error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32"
# [arg-type]
did_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
@ -658,7 +648,8 @@ class DIDWallet:
sent_to=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=token_bytes(), # type: ignore[arg-type]
name=bytes32(token_bytes()),
memos=list(spend_bundle.get_memos().items()),
)
await self.standard_wallet.push_transaction(did_record)
return spend_bundle
@ -685,7 +676,7 @@ class DIDWallet:
innerpuz,
self.did_info.origin_coin.name(),
)
parent_info = await self.get_parent_for_coin(coin)
parent_info = self.get_parent_for_coin(coin)
assert parent_info is not None
fullsol = Program.to(
@ -707,13 +698,10 @@ class DIDWallet:
message = to_sign + coin.name() + self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA
pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz)
index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey)
private = master_sk_to_wallet_sk(self.wallet_state_manager.private_key, index)
private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index)
signature = AugSchemeMPL.sign(private, message)
# assert signature.validate([signature.PkMessagePair(pubkey, message)])
spend_bundle = SpendBundle(list_of_solutions, signature)
# TODO: address hint error and remove ignore
# error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32"
# [arg-type]
did_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
@ -729,7 +717,8 @@ class DIDWallet:
sent_to=[],
trade_id=None,
type=uint32(TransactionType.INCOMING_TX.value),
name=token_bytes(), # type: ignore[arg-type]
name=bytes32(token_bytes()),
memos=list(spend_bundle.get_memos().items()),
)
await self.standard_wallet.push_transaction(did_record)
if filename is not None:
@ -824,7 +813,7 @@ class DIDWallet:
innerpuz,
self.did_info.origin_coin.name(),
)
parent_info = await self.get_parent_for_coin(coin)
parent_info = self.get_parent_for_coin(coin)
assert parent_info is not None
fullsol = Program.to(
[
@ -842,7 +831,7 @@ class DIDWallet:
index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey)
if index is None:
raise ValueError("Unknown pubkey.")
private = master_sk_to_wallet_sk(self.wallet_state_manager.private_key, index)
private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index)
message = bytes(puzhash)
sigs = [AugSchemeMPL.sign(private, message)]
for _ in spend_bundle.coin_spends:
@ -854,9 +843,6 @@ class DIDWallet:
else:
spend_bundle = spend_bundle.aggregate([spend_bundle, SpendBundle(list_of_solutions, aggsig)])
# TODO: address hint error and remove ignore
# error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32"
# [arg-type]
did_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
@ -872,7 +858,8 @@ class DIDWallet:
sent_to=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=token_bytes(), # type: ignore[arg-type]
name=bytes32(token_bytes()),
memos=list(spend_bundle.get_memos().items()),
)
await self.standard_wallet.push_transaction(did_record)
new_did_info = DIDInfo(
@ -923,7 +910,7 @@ class DIDWallet:
)
return inner_puzzle
async def get_parent_for_coin(self, coin) -> Optional[LineageProof]:
def get_parent_for_coin(self, coin) -> Optional[LineageProof]:
parent_info = None
for name, ccparent in self.did_info.parent_info:
if name == coin.parent_coin_info:
@ -949,9 +936,9 @@ class DIDWallet:
did_full_puz = did_wallet_puzzles.create_fullpuz(did_inner, launcher_coin.name())
did_puzzle_hash = did_full_puz.get_tree_hash()
announcement_set: Set[bytes32] = set()
announcement_set: Set[Announcement] = set()
announcement_message = Program.to([did_puzzle_hash, amount, bytes(0x80)]).get_tree_hash()
announcement_set.add(Announcement(launcher_coin.name(), announcement_message).name())
announcement_set.add(Announcement(launcher_coin.name(), announcement_message))
tx_record: Optional[TransactionRecord] = await self.standard_wallet.generate_signed_transaction(
amount, genesis_launcher_puz.get_tree_hash(), uint64(0), origin.name(), coins, None, False, announcement_set
@ -1015,8 +1002,9 @@ class DIDWallet:
+ self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA
)
pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz)
index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey)
private = master_sk_to_wallet_sk(self.wallet_state_manager.private_key, index)
record: Optional[DerivationRecord] = await self.wallet_state_manager.puzzle_store.record_for_pubkey(pubkey)
assert record is not None
private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, record.index)
signature = AugSchemeMPL.sign(private, message)
sigs = [signature]
aggsig = AugSchemeMPL.aggregate(sigs)

View File

@ -41,7 +41,7 @@ def get_pubkey_from_innerpuz(innerpuz: Program) -> G1Element:
def is_did_innerpuz(inner_f: Program):
"""
You may want to generalize this if different `CC_MOD` templates are supported.
You may want to generalize this if different `CAT_MOD` templates are supported.
"""
return inner_f == DID_INNERPUZ_MOD
@ -52,7 +52,7 @@ def is_did_core(inner_f: Program):
def uncurry_innerpuz(puzzle: Program) -> Optional[Tuple[Program, Program]]:
"""
Take a puzzle and return `None` if it's not a `CC_MOD` cc, or
Take a puzzle and return `None` if it's not a `CAT_MOD` cc, or
a triple of `mod_hash, genesis_coin_checker, inner_puzzle` if it is.
"""
r = puzzle.uncurry()

View File

@ -2,7 +2,6 @@ from typing import Any
import aiosqlite
from chia.util.byte_types import hexstr_to_bytes
from chia.util.db_wrapper import DBWrapper
from chia.util.streamable import Streamable
@ -21,7 +20,7 @@ class KeyValStore:
self.db_wrapper = db_wrapper
self.db_connection = db_wrapper.db
await self.db_connection.execute(
("CREATE TABLE IF NOT EXISTS key_val_store(" " key text PRIMARY KEY," " value text)")
"CREATE TABLE IF NOT EXISTS key_val_store(" " key text PRIMARY KEY," " value blob)"
)
await self.db_connection.execute("CREATE INDEX IF NOT EXISTS name on key_val_store(key)")
@ -34,7 +33,7 @@ class KeyValStore:
await cursor.close()
await self.db_connection.commit()
async def get_object(self, key: str, type: Any) -> Any:
async def get_object(self, key: str, object_type: Any) -> Any:
"""
Return bytes representation of stored object
"""
@ -46,7 +45,7 @@ class KeyValStore:
if row is None:
return None
return type.from_bytes(hexstr_to_bytes(row[1]))
return object_type.from_bytes(row[1])
async def set_object(self, key: str, obj: Streamable):
"""
@ -55,7 +54,12 @@ class KeyValStore:
async with self.db_wrapper.lock:
cursor = await self.db_connection.execute(
"INSERT OR REPLACE INTO key_val_store VALUES(?, ?)",
(key, bytes(obj).hex()),
(key, bytes(obj)),
)
await cursor.close()
await self.db_connection.commit()
async def remove_object(self, key: str):
cursor = await self.db_connection.execute("DELETE FROM key_val_store where key=?", (key,))
await cursor.close()
await self.db_connection.commit()

View File

@ -1,7 +1,8 @@
from dataclasses import dataclass
from typing import Optional
from typing import Optional, Any, List
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.blockchain_format.program import Program
from chia.util.ints import uint64
from chia.util.streamable import Streamable, streamable
@ -9,6 +10,19 @@ from chia.util.streamable import Streamable, streamable
@dataclass(frozen=True)
@streamable
class LineageProof(Streamable):
parent_name: bytes32
inner_puzzle_hash: Optional[bytes32]
amount: uint64
parent_name: Optional[bytes32] = None
inner_puzzle_hash: Optional[bytes32] = None
amount: Optional[uint64] = None
def to_program(self) -> Program:
final_list: List[Any] = []
if self.parent_name is not None:
final_list.append(self.parent_name)
if self.inner_puzzle_hash is not None:
final_list.append(self.inner_puzzle_hash)
if self.amount is not None:
final_list.append(self.amount)
return Program.to(final_list)
def is_none(self) -> bool:
return all([self.parent_name is None, self.inner_puzzle_hash is None, self.amount is None])

33
chia/wallet/payment.py Normal file
View File

@ -0,0 +1,33 @@
from dataclasses import dataclass
from typing import List
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.blockchain_format.program import Program
from chia.util.ints import uint64
# This class is supposed to correspond to a CREATE_COIN condition
@dataclass(frozen=True)
class Payment:
puzzle_hash: bytes32
amount: uint64
memos: List[bytes]
def as_condition_args(self) -> List:
return [self.puzzle_hash, self.amount, self.memos]
def as_condition(self) -> Program:
return Program.to([51, *self.as_condition_args()])
def name(self) -> bytes32:
return self.as_condition().get_tree_hash()
@classmethod
def from_condition(cls, condition: Program) -> "Payment":
python_condition: List = condition.as_python()
puzzle_hash, amount = python_condition[1:3]
memos: List[bytes] = []
if len(python_condition) > 3:
memos = python_condition[3]
return cls(bytes32(puzzle_hash), uint64(int.from_bytes(amount, "big")), memos)

View File

@ -0,0 +1,417 @@
; Coins locked with this puzzle are spendable cats.
;
; Choose a list of n inputs (n>=1), I_1, ... I_n with amounts A_1, ... A_n.
;
; We put them in a ring, so "previous" and "next" have intuitive k-1 and k+1 semantics,
; wrapping so {n} and 0 are the same, ie. all indices are mod n.
;
; Each coin creates 0 or more coins with total output value O_k.
; Let D_k = the "debt" O_k - A_k contribution of coin I_k, ie. how much debt this input accumulates.
; Some coins may spend more than they contribute and some may spend less, ie. D_k need
; not be zero. That's okay. It's enough for the total of all D_k in the ring to be 0.
;
; A coin can calculate its own D_k since it can verify A_k (it's hashed into the coin id)
; and it can sum up `CREATE_COIN` conditions for O_k.
;
; Defines a "subtotal of debts" S_k for each coin as follows:
;
; S_1 = 0
; S_k = S_{k-1} + D_{k-1}
;
; Here's the main trick that shows the ring sums to 0.
; You can prove by induction that S_{k+1} = D_1 + D_2 + ... + D_k.
; But it's a ring, so S_{n+1} is also S_1, which is 0. So D_1 + D_2 + ... + D_k = 0.
; So the total debts must be 0, ie. no coins are created or destroyed.
;
; Each coin's solution includes I_{k-1}, I_k, and I_{k+1} along with proofs that I_{k}, and I_{k+1} are CATs of the same type.
; Each coin's solution includes S_{k-1}. It calculates D_k = O_k - A_k, and then S_k = S_{k-1} + D_{k-1}
;
; Announcements are used to ensure that each S_k follows the pattern is valid.
; Announcements automatically commit to their own coin id.
; Coin I_k creates an announcement that further commits to I_{k-1} and S_{k-1}.
;
; Coin I_k gets a proof that I_{k+1} is a cat, so it knows it must also create an announcement
; when spent. It checks that I_{k+1} creates an announcement committing to I_k and S_k.
;
; So S_{k+1} is correct iff S_k is correct.
;
; Coins also receive proofs that their neighbours are CATs, ensuring the announcements aren't forgeries.
; Inner puzzles and the CAT layer prepend `CREATE_COIN_ANNOUNCEMENT` with different prefixes to avoid forgeries.
; Ring announcements use 0xcb, and inner puzzles are given 0xca
;
; In summary, I_k generates a coin_announcement Y_k ("Y" for "yell") as follows:
;
; Y_k: hash of I_k (automatically), I_{k-1}, S_k
;
; Each coin creates an assert_coin_announcement to ensure that the next coin's announcement is as expected:
; Y_{k+1} : hash of I_{k+1}, I_k, S_{k+1}
;
; TLDR:
; I_k : coins
; A_k : amount coin k contributes
; O_k : amount coin k spend
; D_k : difference/delta that coin k incurs (A - O)
; S_k : subtotal of debts D_1 + D_2 ... + D_k
; Y_k : announcements created by coin k commiting to I_{k-1}, I_k, S_k
;
; All conditions go through a "transformer" that looks for CREATE_COIN conditions
; generated by the inner solution, and wraps the puzzle hash ensuring the output is a cat.
;
; Three output conditions are prepended to the list of conditions for each I_k:
; (ASSERT_MY_ID I_k) to ensure that the passed in value for I_k is correct
; (CREATE_COIN_ANNOUNCEMENT I_{k-1} S_k) to create this coin's announcement
; (ASSERT_COIN_ANNOUNCEMENT hashed_announcement(Y_{k+1})) to ensure the next coin really is next and
; the relative values of S_k and S_{k+1} are correct
;
; This is all we need to do to ensure cats exactly balance in the inputs and outputs.
;
; Proof:
; Consider n, k, I_k values, O_k values, S_k and A_k as above.
; For the (CREATE_COIN_ANNOUNCEMENT Y_{k+1}) (created by the next coin)
; and (ASSERT_COIN_ANNOUNCEMENT hashed(Y_{k+1})) to match,
; we see that I_k can ensure that is has the correct value for S_{k+1}.
;
; By induction, we see that S_{m+1} = sum(i, 1, m) [O_i - A_i] = sum(i, 1, m) O_i - sum(i, 1, m) A_i
; So S_{n+1} = sum(i, 1, n) O_i - sum(i, 1, n) A_i. But S_{n+1} is actually S_1 = 0,
; so thus sum(i, 1, n) O_i = sum (i, 1, n) A_i, ie. output total equals input total.
;; GLOSSARY:
;; MOD_HASH: this code's sha256 tree hash
;; TAIL_PROGRAM_HASH: the program that determines if a coin can mint new cats, burn cats, and check if its lineage is valid if its parent is not a CAT
;; INNER_PUZZLE: an independent puzzle protecting the coins. Solutions to this puzzle are expected to generate `AGG_SIG` conditions and possibly `CREATE_COIN` conditions.
;; ---- items above are curried into the puzzle hash ----
;; inner_puzzle_solution: the solution to the inner puzzle
;; prev_coin_id: the id for the previous coin
;; tail_program_reveal: reveal of TAIL_PROGRAM_HASH required to run the program if desired
;; tail_solution: optional solution passed into tail_program
;; lineage_proof: optional proof that our coin's parent is a CAT
;; this_coin_info: (parent_id puzzle_hash amount)
;; next_coin_proof: (parent_id inner_puzzle_hash amount)
;; prev_subtotal: the subtotal between prev-coin and this-coin
;; extra_delta: an amount that is added to our delta and checked by the TAIL program
;;
(mod (
MOD_HASH ;; curried into puzzle
TAIL_PROGRAM_HASH ;; curried into puzzle
INNER_PUZZLE ;; curried into puzzle
inner_puzzle_solution ;; if invalid, INNER_PUZZLE will fail
lineage_proof ;; This is the parent's coin info, used to check if the parent was a CAT. Optional if using tail_program.
prev_coin_id ;; used in this coin's announcement, prev_coin ASSERT_COIN_ANNOUNCEMENT will fail if wrong
this_coin_info ;; verified with ASSERT_MY_COIN_ID
next_coin_proof ;; used to generate ASSERT_COIN_ANNOUNCEMENT
prev_subtotal ;; included in announcement, prev_coin ASSERT_COIN_ANNOUNCEMENT will fail if wrong
extra_delta ;; this is the "legal discrepancy" between your real delta and what you're announcing your delta is
)
;;;;; start library code
(include condition_codes.clvm)
(include curry-and-treehash.clinc)
(include cat_truths.clib)
(defconstant ANNOUNCEMENT_MORPH_BYTE 0xca)
(defconstant RING_MORPH_BYTE 0xcb)
(defmacro assert items
(if (r items)
(list if (f items) (c assert (r items)) (q . (x)))
(f items)
)
)
(defmacro and ARGS
(if ARGS
(qq (if (unquote (f ARGS))
(unquote (c and (r ARGS)))
()
))
1)
)
; takes a lisp tree and returns the hash of it
(defun sha256tree1 (TREE)
(if (l TREE)
(sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE)))
(sha256 ONE TREE)))
; take two lists and merge them into one
(defun merge_list (list_a list_b)
(if list_a
(c (f list_a) (merge_list (r list_a) list_b))
list_b
)
)
; cat_mod_struct = (MOD_HASH MOD_HASH_hash GENESIS_COIN_CHECKER GENESIS_COIN_CHECKER_hash)
(defun-inline mod_hash_from_cat_mod_struct (cat_mod_struct) (f cat_mod_struct))
(defun-inline mod_hash_hash_from_cat_mod_struct (cat_mod_struct) (f (r cat_mod_struct)))
(defun-inline tail_program_hash_from_cat_mod_struct (cat_mod_struct) (f (r (r cat_mod_struct))))
;;;;; end library code
;; return the puzzle hash for a cat with the given `GENESIS_COIN_CHECKER_hash` & `INNER_PUZZLE`
(defun-inline cat_puzzle_hash (cat_mod_struct inner_puzzle_hash)
(puzzle-hash-of-curried-function (mod_hash_from_cat_mod_struct cat_mod_struct)
inner_puzzle_hash
(sha256 ONE (tail_program_hash_from_cat_mod_struct cat_mod_struct))
(mod_hash_hash_from_cat_mod_struct cat_mod_struct)
)
)
;; tweak `CREATE_COIN` condition by wrapping the puzzle hash, forcing it to be a cat
;; prepend `CREATE_COIN_ANNOUNCEMENT` with 0xca as bytes so it cannot be used to cheat the coin ring
(defun-inline morph_condition (condition cat_mod_struct)
(if (= (f condition) CREATE_COIN)
(c CREATE_COIN
(c (cat_puzzle_hash cat_mod_struct (f (r condition)))
(r (r condition)))
)
(if (= (f condition) CREATE_COIN_ANNOUNCEMENT)
(c CREATE_COIN_ANNOUNCEMENT
(c (sha256 ANNOUNCEMENT_MORPH_BYTE (f (r condition)))
(r (r condition))
)
)
condition
)
)
)
;; given a coin's parent, inner_puzzle and amount, and the cat_mod_struct, calculate the id of the coin
(defun-inline coin_id_for_proof (coin cat_mod_struct)
(sha256 (f coin) (cat_puzzle_hash cat_mod_struct (f (r coin))) (f (r (r coin))))
)
;; utility to fetch coin amount from coin
(defun-inline input_amount_for_coin (coin)
(f (r (r coin)))
)
;; calculate the hash of an announcement
;; we add 0xcb so ring announcements exist in a different namespace to announcements from inner_puzzles
(defun-inline calculate_annoucement_id (this_coin_id this_subtotal next_coin_id cat_mod_struct)
(sha256 next_coin_id (sha256 RING_MORPH_BYTE this_coin_id this_subtotal))
)
;; create the `ASSERT_COIN_ANNOUNCEMENT` condition that ensures the next coin's announcement is correct
(defun-inline create_assert_next_announcement_condition (this_coin_id this_subtotal next_coin_id cat_mod_struct)
(list ASSERT_COIN_ANNOUNCEMENT
(calculate_annoucement_id this_coin_id
this_subtotal
next_coin_id
cat_mod_struct
)
)
)
;; here we commit to I_{k-1} and S_k
;; we add 0xcb so ring announcements exist in a different namespace to announcements from inner_puzzles
(defun-inline create_announcement_condition (prev_coin_id prev_subtotal)
(list CREATE_COIN_ANNOUNCEMENT
(sha256 RING_MORPH_BYTE prev_coin_id prev_subtotal)
)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; this function takes a condition and returns an integer indicating
;; the value of all output coins created with CREATE_COIN. If it's not
;; a CREATE_COIN condition, it returns 0.
(defun-inline output_value_for_condition (condition)
(if (= (f condition) CREATE_COIN)
(f (r (r condition)))
0
)
)
;; add two conditions to the list of morphed conditions:
;; CREATE_COIN_ANNOUNCEMENT for my announcement
;; ASSERT_COIN_ANNOUNCEMENT for the next coin's announcement
(defun-inline generate_final_output_conditions
(
prev_subtotal
this_subtotal
morphed_conditions
prev_coin_id
this_coin_id
next_coin_id
cat_mod_struct
)
(c (create_announcement_condition prev_coin_id prev_subtotal)
(c (create_assert_next_announcement_condition this_coin_id this_subtotal next_coin_id cat_mod_struct)
morphed_conditions)
)
)
;; This next section of code loops through all of the conditions to do three things:
;; 1) Look for a "magic" value of -113 and, if one exists, filter it, and take note of the tail reveal and solution
;; 2) Morph any CREATE_COIN or CREATE_COIN_ANNOUNCEMENT conditions
;; 3) Sum the total output amount of all of the CREATE_COINs that are output by the inner puzzle
;;
;; After everything return a struct in the format (morphed_conditions . (output_sum . tail_reveal_and_solution))
;; If multiple magic conditions are specified, the later one will take precedence
(defun-inline condition_tail_reveal (condition) (f (r (r (r condition)))))
(defun-inline condition_tail_solution (condition) (f (r (r (r (r condition))))))
(defun cons_onto_first_and_add_to_second (morphed_condition output_value struct)
(c (c morphed_condition (f struct)) (c (+ output_value (f (r struct))) (r (r struct))))
)
(defun find_and_strip_tail_info (inner_conditions cat_mod_struct tail_reveal_and_solution)
(if inner_conditions
(if (= (output_value_for_condition (f inner_conditions)) -113) ; Checks this is a CREATE_COIN of value -113
(find_and_strip_tail_info
(r inner_conditions)
cat_mod_struct
(c (condition_tail_reveal (f inner_conditions)) (condition_tail_solution (f inner_conditions)))
)
(cons_onto_first_and_add_to_second
(morph_condition (f inner_conditions) cat_mod_struct)
(output_value_for_condition (f inner_conditions))
(find_and_strip_tail_info
(r inner_conditions)
cat_mod_struct
tail_reveal_and_solution
)
)
)
(c () (c 0 tail_reveal_and_solution))
)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;; lineage checking
;; return true iff parent of `this_coin_info` is provably a cat
;; A 'lineage proof' consists of (parent_parent_id parent_INNER_puzzle_hash parent_amount)
;; We use this information to construct a coin who's puzzle has been wrapped in this MOD and verify that,
;; once wrapped, it matches our parent coin's ID.
(defun-inline is_parent_cat (
cat_mod_struct
parent_id
lineage_proof
)
(= parent_id
(sha256 (f lineage_proof)
(cat_puzzle_hash cat_mod_struct (f (r lineage_proof)))
(f (r (r lineage_proof)))
)
)
)
(defun check_lineage_or_run_tail_program
(
this_coin_info
tail_reveal_and_solution
parent_is_cat ; flag which says whether or not the parent CAT check ran and passed
lineage_proof
Truths
extra_delta
inner_conditions
)
(if tail_reveal_and_solution
(assert (= (sha256tree1 (f tail_reveal_and_solution)) (cat_tail_program_hash_truth Truths))
(merge_list
(a (f tail_reveal_and_solution)
(list
Truths
parent_is_cat
lineage_proof ; Lineage proof is only guaranteed to be true if parent_is_cat
extra_delta
inner_conditions
(r tail_reveal_and_solution)
)
)
inner_conditions
)
)
(assert parent_is_cat (not extra_delta)
inner_conditions
)
)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun stager_two (
Truths
(inner_conditions . (output_sum . tail_reveal_and_solution))
lineage_proof
prev_coin_id
this_coin_info
next_coin_id
prev_subtotal
extra_delta
)
(check_lineage_or_run_tail_program
this_coin_info
tail_reveal_and_solution
(if lineage_proof (is_parent_cat (cat_struct_truth Truths) (my_parent_cat_truth Truths) lineage_proof) ())
lineage_proof
Truths
extra_delta
(generate_final_output_conditions
prev_subtotal
; the expression on the next line calculates `this_subtotal` by adding the delta to `prev_subtotal`
(+ prev_subtotal (- (input_amount_for_coin this_coin_info) output_sum) extra_delta)
inner_conditions
prev_coin_id
(my_id_cat_truth Truths)
next_coin_id
(cat_struct_truth Truths)
)
)
)
; CAT TRUTHS struct is: ; CAT Truths is: ((Inner puzzle hash . (MOD hash . (MOD hash hash . TAIL hash))) . (my_id . (my_parent_info my_puzhash my_amount)))
; create truths - this_coin_info verified true because we calculated my ID from it!
; lineage proof is verified later by cat parent check or tail_program
(defun stager (
cat_mod_struct
inner_conditions
lineage_proof
inner_puzzle_hash
my_id
prev_coin_id
this_coin_info
next_coin_proof
prev_subtotal
extra_delta
)
(c (list ASSERT_MY_COIN_ID my_id) (stager_two
(cat_truth_data_to_truth_struct
inner_puzzle_hash
cat_mod_struct
my_id
this_coin_info
)
(find_and_strip_tail_info inner_conditions cat_mod_struct ())
lineage_proof
prev_coin_id
this_coin_info
(coin_id_for_proof next_coin_proof cat_mod_struct)
prev_subtotal
extra_delta
))
)
(stager
;; calculate cat_mod_struct, inner_puzzle_hash, coin_id
(list MOD_HASH (sha256 ONE MOD_HASH) TAIL_PROGRAM_HASH)
(a INNER_PUZZLE inner_puzzle_solution)
lineage_proof
(sha256tree1 INNER_PUZZLE)
(sha256 (f this_coin_info) (f (r this_coin_info)) (f (r (r this_coin_info))))
prev_coin_id ; ID
this_coin_info ; (parent_id puzzle_hash amount)
next_coin_proof ; (parent_id innerpuzhash amount)
prev_subtotal
extra_delta
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff02ff5effff04ff02ffff04ffff04ff05ffff04ffff0bff2cff0580ffff04ff0bff80808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ffff02ff2effff04ff02ffff04ff17ff80808080ffff04ffff0bff82027fff82057fff820b7f80ffff04ff81bfffff04ff82017fffff04ff8202ffffff04ff8205ffffff04ff820bffff80808080808080808080808080ffff04ffff01ffffffff81ca3dff46ff0233ffff3c04ff01ff0181cbffffff02ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff22ffff0bff2cff3480ffff0bff22ffff0bff22ffff0bff2cff5c80ff0980ffff0bff22ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ffff02ff2effff04ff02ffff04ff13ff80808080ff820b9f80ffff01ff02ff26ffff04ff02ffff04ffff02ff13ffff04ff5fffff04ff17ffff04ff2fffff04ff81bfffff04ff82017fffff04ff1bff8080808080808080ffff04ff82017fff8080808080ffff01ff088080ff0180ffff01ff02ffff03ff17ffff01ff02ffff03ffff20ff81bf80ffff0182017fffff01ff088080ff0180ffff01ff088080ff018080ff0180ffff04ffff04ff05ff2780ffff04ffff10ff0bff5780ff778080ff02ffff03ff05ffff01ff02ffff03ffff09ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff01818f80ffff01ff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ffff04ff81b9ff82017980ff808080808080ffff01ff02ff5affff04ff02ffff04ffff02ffff03ffff09ff11ff7880ffff01ff04ff78ffff04ffff02ff36ffff04ff02ffff04ff13ffff04ff29ffff04ffff0bff2cff5b80ffff04ff2bff80808080808080ff398080ffff01ff02ffff03ffff09ff11ff2480ffff01ff04ff24ffff04ffff0bff20ff2980ff398080ffff010980ff018080ff0180ffff04ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff04ffff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ff17ff808080808080ff80808080808080ff0180ffff01ff04ff80ffff04ff80ff17808080ff0180ffffff02ffff03ff05ffff01ff04ff09ffff02ff26ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff0bff22ffff0bff2cff5880ffff0bff22ffff0bff22ffff0bff2cff5c80ff0580ffff0bff22ffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bff2cff058080ff0180ffff04ffff04ff28ffff04ff5fff808080ffff02ff7effff04ff02ffff04ffff04ffff04ff2fff0580ffff04ff5fff82017f8080ffff04ffff02ff7affff04ff02ffff04ff0bffff04ff05ffff01ff808080808080ffff04ff17ffff04ff81bfffff04ff82017fffff04ffff0bff8204ffffff02ff36ffff04ff02ffff04ff09ffff04ff820affffff04ffff0bff2cff2d80ffff04ff15ff80808080808080ff8216ff80ffff04ff8205ffffff04ff820bffff808080808080808080808080ff02ff2affff04ff02ffff04ff5fffff04ff3bffff04ffff02ffff03ff17ffff01ff09ff2dffff0bff27ffff02ff36ffff04ff02ffff04ff29ffff04ff57ffff04ffff0bff2cff81b980ffff04ff59ff80808080808080ff81b78080ff8080ff0180ffff04ff17ffff04ff05ffff04ff8202ffffff04ffff04ffff04ff24ffff04ffff0bff7cff2fff82017f80ff808080ffff04ffff04ff30ffff04ffff0bff81bfffff0bff7cff15ffff10ff82017fffff11ff8202dfff2b80ff8202ff808080ff808080ff138080ff80808080808080808080ff018080

View File

@ -0,0 +1 @@
72dec062874cd4d3aab892a0906688a1ae412b0109982e1797a170add88bdcdc

View File

@ -1,4 +1,4 @@
from chia.wallet.puzzles.load_clvm import load_clvm
CC_MOD = load_clvm("cc.clvm", package_or_requirement=__name__)
CAT_MOD = load_clvm("cat.clvm", package_or_requirement=__name__)
LOCK_INNER_PUZZLE = load_clvm("lock.inner.puzzle.clvm", package_or_requirement=__name__)

View File

@ -0,0 +1,31 @@
(
(defun-inline cat_truth_data_to_truth_struct (innerpuzhash cat_struct my_id this_coin_info)
(c
(c
innerpuzhash
cat_struct
)
(c
my_id
this_coin_info
)
)
)
; CAT Truths is: ((Inner puzzle hash . (MOD hash . (MOD hash hash . TAIL hash))) . (my_id . (my_parent_info my_puzhash my_amount)))
(defun-inline my_inner_puzzle_hash_cat_truth (Truths) (f (f Truths)))
(defun-inline cat_struct_truth (Truths) (r (f Truths)))
(defun-inline my_id_cat_truth (Truths) (f (r Truths)))
(defun-inline my_coin_info_truth (Truths) (r (r Truths)))
(defun-inline my_amount_cat_truth (Truths) (f (r (r (my_coin_info_truth Truths)))))
(defun-inline my_full_puzzle_hash_cat_truth (Truths) (f (r (my_coin_info_truth Truths))))
(defun-inline my_parent_cat_truth (Truths) (f (my_coin_info_truth Truths)))
; CAT mod_struct is: (MOD_HASH MOD_HASH_hash TAIL_PROGRAM TAIL_PROGRAM_hash)
(defun-inline cat_mod_hash_truth (Truths) (f (cat_struct_truth Truths)))
(defun-inline cat_mod_hash_hash_truth (Truths) (f (r (cat_struct_truth Truths))))
(defun-inline cat_tail_program_hash_truth (Truths) (f (r (r (cat_struct_truth Truths)))))
)

View File

@ -1,377 +0,0 @@
; Coins locked with this puzzle are spendable ccs.
;
; Choose a list of n inputs (n>=1), I_1, ... I_n with amounts A_1, ... A_n.
;
; We put them in a ring, so "previous" and "next" have intuitive k-1 and k+1 semantics,
; wrapping so {n} and 0 are the same, ie. all indices are mod n.
;
; Each coin creates 0 or more coins with total output value O_k.
; Let D_k = the "debt" O_k - A_k contribution of coin I_k, ie. how much debt this input accumulates.
; Some coins may spend more than they contribute and some may spend less, ie. D_k need
; not be zero. That's okay. It's enough for the total of all D_k in the ring to be 0.
;
; A coin can calculate its own D_k since it can verify A_k (it's hashed into the coin id)
; and it can sum up `CREATE_COIN` conditions for O_k.
;
; Defines a "subtotal of debts" S_k for each coin as follows:
;
; S_1 = 0
; S_k = S_{k-1} + D_{k-1}
;
; Here's the main trick that shows the ring sums to 0.
; You can prove by induction that S_{k+1} = D_1 + D_2 + ... + D_k.
; But it's a ring, so S_{n+1} is also S_1, which is 0. So D_1 + D_2 + ... + D_k = 0.
; So the total debts must be 0, ie. no coins are created or destroyed.
;
; Each coin's solution includes I_{k-1}, I_k, and I_{k+1} along with proofs that each is a CC.
; Each coin's solution includes S_{k-1}. It calculates D_k = O_k - A_k, and then S_k = S_{k-1} + D_{k-1}
;
; Announcements are used to ensure that each S_k follows the pattern is valid.
; Announcements automatically commit to their own coin id.
; Coin I_k creates an announcement that further commits to I_{k-1} and S_{k-1}.
;
; Coin I_k gets a proof that I_{k+1} is a CC, so it knows it must also create an announcement
; when spent. It checks that I_{k+1} creates an announcement committing to I_k and S_k.
;
; So S_{k+1} is correct iff S_k is correct.
;
; Coins also receive proofs that their neighbors are ccs, ensuring the announcements aren't forgeries, as
; inner puzzles are not allowed to use `CREATE_COIN_ANNOUNCEMENT`.
;
; In summary, I_k generates an announcement Y_k (for "yell") as follows:
;
; Y_k: hash of I_k (automatically), I_{k-1}, S_k
;
; Each coin ensures that the next coin's announcement is as expected:
; Y_{k+1} : hash of I_{k+1}, I_k, S_{k+1}
;
; TLDR:
; I_k : coins
; A_k : amount coin k contributes
; O_k : amount coin k spend
; D_k : difference/delta that coin k incurs (A - O)
; S_k : subtotal of debts D_1 + D_2 ... + D_k
; Y_k : announcements created by coin k commiting to I_{k-1}, I_k, S_k
;
; All conditions go through a "transformer" that looks for CREATE_COIN conditions
; generated by the inner solution, and wraps the puzzle hash ensuring the output is a cc.
;
; Three output conditions are prepended to the list of conditions for each I_k:
; (ASSERT_MY_ID I_k) to ensure that the passed in value for I_k is correct
; (CREATE_COIN_ANNOUNCEMENT I_{k-1} S_k) to create this coin's announcement
; (ASSERT_COIN_ANNOUNCEMENT hashed_announcement(Y_{k+1})) to ensure the next coin really is next and
; the relative values of S_k and S_{k+1} are correct
;
; This is all we need to do to ensure ccs exactly balance in the inputs and outputs.
;
; Proof:
; Consider n, k, I_k values, O_k values, S_k and A_k as above.
; For the (CREATE_COIN_ANNOUNCEMENT Y_{k+1}) (created by the next coin)
; and (ASSERT_COIN_ANNOUNCEMENT hashed(Y_{k+1})) to match,
; we see that I_k can ensure that is has the correct value for S_{k+1}.
;
; By induction, we see that S_{m+1} = sum(i, 1, m) [O_i - A_i] = sum(i, 1, m) O_i - sum(i, 1, m) A_i
; So S_{n+1} = sum(i, 1, n) O_i - sum(i, 1, n) A_i. But S_{n+1} is actually S_1 = 0,
; so thus sum(i, 1, n) O_i = sum (i, 1, n) A_i, ie. output total equals input total.
;
; QUESTION: do we want a secondary puzzle that allows for coins to be spent? This could be good for
; bleaching coins (sendable to any address), or reclaiming them by a central authority.
;
;; GLOSSARY:
;; mod-hash: this code's sha256 tree hash
;; genesis-coin-checker: the function that determines if a coin can mint new ccs
;; inner-puzzle: an independent puzzle protecting the coins. Solutions to this puzzle are expected to
;; generate `AGG_SIG` conditions and possibly `CREATE_COIN` conditions.
;; ---- items above are curried into the puzzle hash ----
;; inner-puzzle-solution: the solution to the inner puzzle
;; prev-coin-bundle: the bundle for previous coin
;; this-coin-bundle: the bundle for this coin
;; next-coin-bundle: the bundle for next coin
;; prev-subtotal: the subtotal between prev-coin and this-coin
;;
;; coin-info: `(parent_id puzzle_hash amount)`. This defines the coin id used with ASSERT_MY_COIN_ID
;; coin-bundle: the cons box `(coin-info . lineage_proof)`
;;
;; and automatically hashed in to the announcement generated with CREATE_COIN_ANNOUNCEMENT.
;;
(mod (mod-hash ;; curried into puzzle
genesis-coin-checker ;; curried into puzzle
inner-puzzle ;; curried into puzzle
inner-puzzle-solution ;; if invalid, inner-puzzle will fail
prev-coin-bundle ;; used in this coin's announcement, prev-coin ASSERT_COIN_ANNOUNCEMENT will fail if wrong
this-coin-bundle ;; verified with ASSERT_MY_COIN_ID
next-coin-bundle ;; used to generate ASSERT_COIN_ANNOUNCEMENT
prev-subtotal ;; included in announcement, prev-coin ASSERT_COIN_ANNOUNCEMENT will fail if wrong
)
;;;;; start library code
(include condition_codes.clvm)
(defmacro assert items
(if (r items)
(list if (f items) (c assert (r items)) (q . (x)))
(f items)
)
)
;; utility function used by `curry_args`
(defun fix_curry_args (items core)
(if items
(qq (c (q . (unquote (f items))) (unquote (fix_curry_args (r items) core))))
core
)
)
; (curry_args sum (list 50 60)) => returns a function that is like (sum 50 60 ...)
(defun curry_args (func list_of_args) (qq (a (q . (unquote func)) (unquote (fix_curry_args list_of_args (q . 1))))))
;; (curry sum 50 60) => returns a function that is like (sum 50 60 ...)
(defun curry (func . args) (curry_args func args))
(defun is-in-list (atom items)
;; returns 1 iff `atom` is in the list of `items`
(if items
(if (= atom (f items))
1
(is-in-list atom (r items))
)
0
)
)
;; hash a tree with escape values representing already-hashed subtrees
;; This optimization can be useful if you know the puzzle hash of a sub-expression.
;; You probably actually want to use `curry_and_hash` though.
(defun sha256tree_esc_list
(TREE LITERALS)
(if (l TREE)
(sha256 2 (sha256tree_esc_list (f TREE) LITERALS) (sha256tree_esc_list (r TREE) LITERALS))
(if (is-in-list TREE LITERALS)
TREE
(sha256 1 TREE)
)
)
)
;; hash a tree with escape values representing already-hashed subtrees
;; This optimization can be useful if you know the tree hash of a sub-expression.
(defun sha256tree_esc
(TREE . LITERAL)
(sha256tree_esc_list TREE LITERAL)
)
; takes a lisp tree and returns the hash of it
(defun sha256tree1 (TREE)
(if (l TREE)
(sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE)))
(sha256 1 TREE)))
;;;;; end library code
;; return the puzzle hash for a cc with the given `genesis-coin-checker-hash` & `inner-puzzle`
(defun cc-puzzle-hash ((mod-hash mod-hash-hash genesis-coin-checker genesis-coin-checker-hash) inner-puzzle-hash)
(sha256tree_esc (curry mod-hash mod-hash-hash genesis-coin-checker-hash inner-puzzle-hash)
mod-hash
mod-hash-hash
genesis-coin-checker-hash
inner-puzzle-hash)
)
;; tweak `CREATE_COIN` condition by wrapping the puzzle hash, forcing it to be a cc
;; prohibit CREATE_COIN_ANNOUNCEMENT
(defun-inline morph-condition (condition lineage-proof-parameters)
(if (= (f condition) CREATE_COIN)
(list CREATE_COIN
(cc-puzzle-hash lineage-proof-parameters (f (r condition)))
(f (r (r condition)))
)
(if (= (f condition) CREATE_COIN_ANNOUNCEMENT)
(x)
condition
)
)
)
;; tweak all `CREATE_COIN` conditions, enforcing created coins to be ccs
(defun morph-conditions (conditions lineage-proof-parameters)
(if conditions
(c
(morph-condition (f conditions) lineage-proof-parameters)
(morph-conditions (r conditions) lineage-proof-parameters)
)
()
)
)
;; given a coin triplet, return the id of the coin
(defun coin-id-for-coin ((parent-id puzzle-hash amount))
(sha256 parent-id puzzle-hash amount)
)
;; utility to fetch coin amount from coin
(defun-inline input-amount-for-coin (coin)
(f (r (r coin)))
)
;; calculate the hash of an announcement
(defun-inline calculate-annoucement-id (this-coin-info this-subtotal next-coin-info)
; NOTE: the next line containts a bug, as sha256tree1 ignores `this-subtotal`
(sha256 (coin-id-for-coin next-coin-info) (sha256tree1 (list this-coin-info this-subtotal)))
)
;; create the `ASSERT_COIN_ANNOUNCEMENT` condition that ensures the next coin's announcement is correct
(defun-inline create-assert-next-announcement-condition (this-coin-info this-subtotal next-coin-info)
(list ASSERT_COIN_ANNOUNCEMENT
(calculate-annoucement-id this-coin-info
this-subtotal
next-coin-info
)
)
)
;; here we commit to I_{k-1} and S_k
(defun-inline create-announcement-condition (prev-coin-info prev-subtotal)
(list CREATE_COIN_ANNOUNCEMENT
(sha256tree1 (list prev-coin-info prev-subtotal))
)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; this function takes a condition and returns an integer indicating
;; the value of all output coins created with CREATE_COIN. If it's not
;; a CREATE_COIN condition, it returns 0.
(defun-inline output-value-for-condition (condition)
(if (= (f condition) CREATE_COIN)
(f (r (r condition)))
0
)
)
;; this function takes a list of conditions and returns an integer indicating
;; the value of all output coins created with CREATE_COIN
(defun output-totals (conditions)
(if conditions
(+ (output-value-for-condition (f conditions)) (output-totals (r conditions)))
0
)
)
;; ensure `this-coin-info` is correct by creating the `ASSERT_MY_COIN_ID` condition
(defun-inline create-assert-my-id (this-coin-info)
(list ASSERT_MY_COIN_ID (coin-id-for-coin this-coin-info))
)
;; add three conditions to the list of morphed conditions:
;; ASSERT_MY_COIN_ID for `this-coin-info`
;; CREATE_COIN_ANNOUNCEMENT for my announcement
;; ASSERT_COIN_ANNOUNCEMENT for the next coin's announcement
(defun-inline generate-final-output-conditions
(
prev-subtotal
this-subtotal
morphed-conditions
prev-coin-info
this-coin-info
next-coin-info
)
(c (create-assert-my-id this-coin-info)
(c (create-announcement-condition prev-coin-info prev-subtotal)
(c (create-assert-next-announcement-condition this-coin-info this-subtotal next-coin-info)
morphed-conditions)
)
)
)
(defun-inline coin-info-for-coin-bundle (coin-bundle)
(f coin-bundle)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;; lineage checking
;; return true iff parent of `this-coin-info` is provably a cc
(defun is-parent-cc (
lineage-proof-parameters
this-coin-info
(parent-parent-coin-id parent-inner-puzzle-hash parent-amount)
)
(= (f this-coin-info)
(sha256 parent-parent-coin-id
(cc-puzzle-hash lineage-proof-parameters parent-inner-puzzle-hash)
parent-amount
)
)
)
;; return true iff the lineage proof is valid
;; lineage-proof is of one of two forms:
;; (1 . (parent-parent-coin-id parent-inner-puzzle-hash parent-amount))
;; (0 . some-opaque-proof-passed-to-genesis-coin-checker)
;; so the `f` value determines what kind of proof it is, and the `r` value is the proof
(defun genesis-coin-checker-for-lpp ((mod_hash mod_hash_hash genesis-coin-checker genesis-coin-checker-hash))
genesis-coin-checker
)
(defun-inline is-lineage-proof-valid (
lineage-proof-parameters coin-info lineage-proof)
(if
(f lineage-proof)
(is-parent-cc lineage-proof-parameters coin-info (r lineage-proof))
(a (genesis-coin-checker-for-lpp lineage-proof-parameters)
(list lineage-proof-parameters coin-info (r lineage-proof)))
)
)
(defun is-bundle-valid ((coin . lineage-proof) lineage-proof-parameters)
(is-lineage-proof-valid lineage-proof-parameters coin lineage-proof)
)
;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun main (
lineage-proof-parameters
inner-conditions
prev-coin-bundle
this-coin-bundle
next-coin-bundle
prev-subtotal
)
(assert
; ensure prev is a cc (is this really necessary?)
(is-bundle-valid prev-coin-bundle lineage-proof-parameters)
; ensure this is a cc (to ensure parent wasn't counterfeit)
(is-bundle-valid this-coin-bundle lineage-proof-parameters)
; ensure next is a cc (to ensure its announcements can be trusted)
(is-bundle-valid next-coin-bundle lineage-proof-parameters)
(generate-final-output-conditions
prev-subtotal
; the expression on the next line calculates `this-subtotal` by adding the delta to `prev-subtotal`
(+ prev-subtotal (- (input-amount-for-coin (coin-info-for-coin-bundle this-coin-bundle)) (output-totals inner-conditions)))
(morph-conditions inner-conditions lineage-proof-parameters)
(coin-info-for-coin-bundle prev-coin-bundle)
(coin-info-for-coin-bundle this-coin-bundle)
(coin-info-for-coin-bundle next-coin-bundle)
)
)
)
(main
;; cache some stuff: output conditions, and lineage-proof-parameters
(list mod-hash (sha256tree1 mod-hash) genesis-coin-checker (sha256tree1 genesis-coin-checker))
(a inner-puzzle inner-puzzle-solution)
prev-coin-bundle
this-coin-bundle
next-coin-bundle
prev-subtotal
)
)

View File

@ -1 +0,0 @@
ff02ffff01ff02ff7affff04ff02ffff04ffff04ff05ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ffff04ff0bffff04ffff02ff2effff04ff02ffff04ff0bff80808080ff8080808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffff808080808080808080ffff04ffff01ffffffff3d46ff333cffffff02ff5effff04ff02ffff04ffff02ff2cffff04ff02ffff04ff09ffff04ff15ffff04ff5dffff04ff0bff80808080808080ffff04ff09ffff04ff15ffff04ff5dffff04ff0bff8080808080808080ff0bff09ff15ff2d80ffff02ff5cffff04ff02ffff04ff05ffff04ff07ff8080808080ffff04ffff0102ffff04ffff04ffff0101ff0580ffff04ffff02ff7cffff04ff02ffff04ff0bffff01ff0180808080ff80808080ff02ffff03ff05ffff01ff04ffff0104ffff04ffff04ffff0101ff0980ffff04ffff02ff7cffff04ff02ffff04ff0dffff04ff0bff8080808080ff80808080ffff010b80ff0180ffffff2dff02ffff03ff15ffff01ff02ff5affff04ff02ffff04ff0bffff04ff09ffff04ff1dff808080808080ffff01ff02ffff02ff22ffff04ff02ffff04ff0bff80808080ffff04ff0bffff04ff09ffff04ff1dff808080808080ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ff05ff1380ffff01ff0101ffff01ff02ff2affff04ff02ffff04ff05ffff04ff1bff808080808080ff0180ff8080ff0180ffff09ff13ffff0bff27ffff02ff24ffff04ff02ffff04ff05ffff04ff57ff8080808080ff81b78080ff02ffff03ffff02ff32ffff04ff02ffff04ff17ffff04ff05ff8080808080ffff01ff02ffff03ffff02ff32ffff04ff02ffff04ff2fffff04ff05ff8080808080ffff01ff02ffff03ffff02ff32ffff04ff02ffff04ff5fffff04ff05ff8080808080ffff01ff04ffff04ff30ffff04ffff02ff34ffff04ff02ffff04ff4fff80808080ff808080ffff04ffff04ff38ffff04ffff02ff2effff04ff02ffff04ffff04ff27ffff04ff81bfff808080ff80808080ff808080ffff04ffff04ff20ffff04ffff0bffff02ff34ffff04ff02ffff04ff819fff80808080ffff02ff2effff04ff02ffff04ffff04ff4fffff04ffff10ff81bfffff11ff8202cfffff02ff36ffff04ff02ffff04ff0bff808080808080ff808080ff8080808080ff808080ffff02ff26ffff04ff02ffff04ff0bffff04ff05ff8080808080808080ffff01ff088080ff0180ffff01ff088080ff0180ffff01ff088080ff0180ffffff02ffff03ff05ffff01ff04ffff02ffff03ffff09ff11ff2880ffff01ff04ff28ffff04ffff02ff24ffff04ff02ffff04ff0bffff04ff29ff8080808080ffff04ff59ff80808080ffff01ff02ffff03ffff09ff11ff3880ffff01ff0880ffff010980ff018080ff0180ffff02ff26ffff04ff02ffff04ff0dffff04ff0bff808080808080ff8080ff0180ff02ffff03ff05ffff01ff10ffff02ffff03ffff09ff11ff2880ffff0159ff8080ff0180ffff02ff36ffff04ff02ffff04ff0dff8080808080ff8080ff0180ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ff7effff04ff02ffff04ff05ffff04ff07ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff7effff04ff02ffff04ff09ffff04ff0bff8080808080ffff02ff7effff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff02ffff03ffff02ff2affff04ff02ffff04ff05ffff04ff0bff8080808080ffff0105ffff01ff0bffff0101ff058080ff018080ff0180ff018080

View File

@ -1 +0,0 @@
d4596fa7aa6eaa267ebce8d527546827de083d58fb4e14f4137c2448f7252e5c

View File

@ -0,0 +1 @@
can't compile ("defconstant" "AGG_SIG_UNSAFE" 49), unknown operator

View File

@ -0,0 +1 @@
can't compile ("my-id"), unknown operator

View File

@ -0,0 +1,25 @@
; This is a "limitations_program" for use with cat.clvm.
(mod (
PUBKEY
Truths
parent_is_cat
lineage_proof
delta
inner_conditions
(
delegated_puzzle
delegated_solution
)
)
(include condition_codes.clvm)
(defun sha256tree1 (TREE)
(if (l TREE)
(sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE)))
(sha256 1 TREE)))
(c (list AGG_SIG_UNSAFE PUBKEY (sha256tree1 delegated_puzzle))
(a delegated_puzzle (c Truths (c parent_is_cat (c lineage_proof (c delta (c inner_conditions delegated_solution))))))
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff82027fff80808080ff80808080ffff02ff82027fffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfff82057f80808080808080ffff04ffff01ff31ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080

View File

@ -0,0 +1 @@
999c3696e167f8a79d938adc11feba3a3dcb39ccff69a426d570706e7b8ec399

View File

@ -0,0 +1,25 @@
; This is a "limitations_program" for use with cat.clvm.
(mod (
PUBKEY
Truths
parent_is_cat
lineage_proof
delta
inner_conditions
(
delegated_puzzle
delegated_solution
)
)
(include condition_codes.clvm)
(defun sha256tree1 (TREE)
(if (l TREE)
(sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE)))
(sha256 1 TREE)))
(c (list AGG_SIG_UNSAFE PUBKEY (sha256tree1 delegated_puzzle))
(a delegated_puzzle (c Truths (c parent_is_cat (c lineage_proof (c delta (c inner_conditions delegated_solution))))))
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff82027fff80808080ff80808080ffff02ff82027fffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfff82057f80808080808080ffff04ffff01ff31ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080

View File

@ -0,0 +1 @@
999c3696e167f8a79d938adc11feba3a3dcb39ccff69a426d570706e7b8ec399

View File

@ -0,0 +1,15 @@
; This is a "limitations_program" for use with cat.clvm.
(mod (
PUBKEY
Truths
parent_is_cat
lineage_proof
delta
inner_conditions
_
)
(include condition_codes.clvm)
(list (list AGG_SIG_ME PUBKEY delta)) ; Careful with a delta of zero, the bytecode is 80 not 00
)

View File

@ -0,0 +1 @@
ff02ffff01ff04ffff04ff02ffff04ff05ffff04ff5fff80808080ff8080ffff04ffff0132ff018080

View File

@ -0,0 +1 @@
1720d13250a7c16988eaf530331cefa9dd57a76b2c82236bec8bbbff91499b89

View File

@ -1,39 +1,26 @@
; This is a "genesis checker" for use with cc.clvm.
;
; This checker allows new ccs to be created if they have a particular
; coin id as parent; or created by anyone if their value is 0.
; This checker allows new CATs to be created if they have a particular coin id as parent
;
; The genesis_id is curried in, making this lineage_check program unique and giving the CAT it's uniqueness
(mod (
genesis-id
lineage-proof-parameters
my-coin-info
(parent-coin zero-parent-inner-puzzle-hash)
GENESIS_ID
Truths
parent_is_cat
lineage_proof
delta
inner_conditions
_
)
;; boolean or macro
;; This lets you write something like (if (or COND1 COND2 COND3) (do-something) (do-something-else))
(defmacro or ARGS
(if ARGS
(qq (if (unquote (f ARGS))
1
(unquote (c or (r ARGS)))
))
0)
(include cat_truths.clib)
(if delta
(x)
(if (= (my_parent_cat_truth Truths) GENESIS_ID)
()
(x)
)
)
(defun-inline main (
genesis-id
my-coin-info
)
(or
(= (f (r (r my-coin-info))) 0)
(= (f my-coin-info) genesis-id)
)
)
(main
genesis-id
my-coin-info
)
)
)

View File

@ -1 +1 @@
ff02ffff03ffff09ff5bff8080ffff01ff0101ffff01ff02ffff03ffff09ff13ff0280ffff01ff0101ff8080ff018080ff0180
ff02ffff03ff2fffff01ff0880ffff01ff02ffff03ffff09ff2dff0280ff80ffff01ff088080ff018080ff0180

View File

@ -1 +1 @@
258008f81f21c270f4b58488b108a46a35e5df43ca5b0313ac83e900a5e44a5f
493afb89eed93ab86741b2aa61b8f5de495d33ff9b781dfc8919e602b2afa150

View File

@ -1,55 +1,24 @@
; This is a "genesis checker" for use with cc.clvm.
; This is a "limitations_program" for use with cat.clvm.
;
; This checker allows new ccs to be created if their parent has a particular
; puzzle hash; or created by anyone if their value is 0.
; This checker allows new CATs to be created if their parent has a particular puzzle hash
(mod (
genesis-puzzle-hash
lineage-proof-parameters
my-coin-info
(parent-coin zero-parent-inner-puzzle-hash)
GENESIS_PUZZLE_HASH
Truths
parent_is_cat
lineage_proof
delta
inner_conditions
(parent_parent_id parent_amount)
)
;; boolean and macro
;; This lets you write something like (if (and COND1 COND2 COND3) (do-something) (do-something-else))
(defmacro and ARGS
(if ARGS
(qq (if (unquote (f ARGS))
(unquote (c and (r ARGS)))
()
))
1)
)
(include cat_truths.clib)
;; boolean or macro
;; This lets you write something like (if (or COND1 COND2 COND3) (do-something) (do-something-else))
(defmacro or ARGS
(if ARGS
(qq (if (unquote (f ARGS))
1
(unquote (c or (r ARGS)))
))
0)
)
(defun-inline main (
genesis-puzzle-hash
my-coin-info
parent-coin
)
(or
(= (f (r (r my-coin-info))) 0)
(and
(= (sha256 (f parent-coin) (f (r parent-coin)) (f (r (r parent-coin)))) (f my-coin-info))
(= (f (r parent-coin)) genesis-puzzle-hash)
; Returns nil since we don't need to add any conditions
(if delta
(x)
(if (= (sha256 parent_parent_id GENESIS_PUZZLE_HASH parent_amount) (my_parent_cat_truth Truths))
()
(x)
)
)
)
(main
genesis-puzzle-hash
my-coin-info
parent-coin
)
)
)

View File

@ -1 +1 @@
ff02ffff03ffff09ff5bff8080ffff01ff0101ffff01ff02ffff03ffff02ffff03ffff09ffff0bff47ff81a7ff82016780ff1380ffff01ff02ffff03ffff09ff81a7ff0280ffff01ff0101ff8080ff0180ff8080ff0180ffff01ff0101ff8080ff018080ff0180
ff02ffff03ff2fffff01ff0880ffff01ff02ffff03ffff09ffff0bff82013fff02ff8202bf80ff2d80ff80ffff01ff088080ff018080ff0180

View File

@ -1 +1 @@
795964e0324fbc08e8383d67659194a70455956ad1ebd2329ccf20008da00936
de5a6e06d41518be97ff6365694f4f89475dda773dede267caa33da63b434e36

View File

@ -0,0 +1,26 @@
; This is a TAIL for use with cat.clvm.
;
; This checker allows new CATs to be created if they have a particular coin id as parent
;
; The genesis_id is curried in, making this lineage_check program unique and giving the CAT it's uniqueness
(mod (
GENESIS_ID
Truths
parent_is_cat
lineage_proof
delta
inner_conditions
_
)
(include cat_truths.clib)
(if delta
(x)
(if (= (my_parent_cat_truth Truths) GENESIS_ID)
()
(x)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff03ff2fffff01ff0880ffff01ff02ffff03ffff09ff2dff0280ff80ffff01ff088080ff018080ff0180

View File

@ -0,0 +1 @@
493afb89eed93ab86741b2aa61b8f5de495d33ff9b781dfc8919e602b2afa150

View File

@ -1,46 +0,0 @@
from typing import Optional
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.wallet.puzzles.load_clvm import load_clvm
MOD = load_clvm("genesis-by-coin-id-with-0.clvm", package_or_requirement=__name__)
def create_genesis_or_zero_coin_checker(genesis_coin_id: bytes32) -> Program:
"""
Given a specific genesis coin id, create a `genesis_coin_mod` that allows
both that coin id to issue a cc, or anyone to create a cc with amount 0.
"""
genesis_coin_mod = MOD
return genesis_coin_mod.curry(genesis_coin_id)
def genesis_coin_id_for_genesis_coin_checker(
genesis_coin_checker: Program,
) -> Optional[bytes32]:
"""
Given a `genesis_coin_checker` program, pull out the genesis coin id.
"""
r = genesis_coin_checker.uncurry()
if r is None:
return r
f, args = r
if f != MOD:
return None
return args.first().as_atom()
def lineage_proof_for_genesis(parent_coin: Coin) -> Program:
return Program.to((0, [parent_coin.as_list(), 0]))
def lineage_proof_for_zero(parent_coin: Coin) -> Program:
return Program.to((0, [parent_coin.as_list(), 1]))
def lineage_proof_for_coin(parent_coin: Coin) -> Program:
if parent_coin.amount == 0:
return lineage_proof_for_zero(parent_coin)
return lineage_proof_for_genesis(parent_coin)

View File

@ -0,0 +1,24 @@
; This is a "limitations_program" for use with cat.clvm.
;
; This checker allows new CATs to be created if their parent has a particular puzzle hash
(mod (
GENESIS_PUZZLE_HASH
Truths
parent_is_cat
lineage_proof
delta
inner_conditions
(parent_parent_id parent_amount)
)
(include cat_truths.clib)
; Returns nil since we don't need to add any conditions
(if delta
(x)
(if (= (sha256 parent_parent_id GENESIS_PUZZLE_HASH parent_amount) (my_parent_cat_truth Truths))
()
(x)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff03ff2fffff01ff0880ffff01ff02ffff03ffff09ffff0bff82013fff02ff8202bf80ff2d80ff80ffff01ff088080ff018080ff0180

View File

@ -0,0 +1 @@
de5a6e06d41518be97ff6365694f4f89475dda773dede267caa33da63b434e36

View File

@ -1,46 +0,0 @@
from typing import Optional
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.wallet.puzzles.load_clvm import load_clvm
MOD = load_clvm("genesis-by-puzzle-hash-with-0.clvm", package_or_requirement=__name__)
def create_genesis_puzzle_or_zero_coin_checker(genesis_puzzle_hash: bytes32) -> Program:
"""
Given a specific genesis coin id, create a `genesis_coin_mod` that allows
both that coin id to issue a cc, or anyone to create a cc with amount 0.
"""
genesis_coin_mod = MOD
return genesis_coin_mod.curry(genesis_puzzle_hash)
def genesis_puzzle_hash_for_genesis_coin_checker(
genesis_coin_checker: Program,
) -> Optional[bytes32]:
"""
Given a `genesis_coin_checker` program, pull out the genesis puzzle hash.
"""
r = genesis_coin_checker.uncurry()
if r is None:
return r
f, args = r
if f != MOD:
return None
return args.first().as_atom()
def lineage_proof_for_genesis_puzzle(parent_coin: Coin) -> Program:
return Program.to((0, [parent_coin.as_list(), 0]))
def lineage_proof_for_zero(parent_coin: Coin) -> Program:
return Program.to((0, [parent_coin.as_list(), 1]))
def lineage_proof_for_coin(parent_coin: Coin) -> Program:
if parent_coin.amount == 0:
return lineage_proof_for_zero(parent_coin)
return lineage_proof_for_genesis_puzzle(parent_coin)

View File

@ -0,0 +1,208 @@
from typing import Tuple, Dict, List, Optional, Any
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.spend_bundle import SpendBundle
from chia.util.ints import uint64
from chia.util.byte_types import hexstr_to_bytes
from chia.wallet.lineage_proof import LineageProof
from chia.wallet.puzzles.load_clvm import load_clvm
from chia.wallet.cat_wallet.cat_utils import (
CAT_MOD,
construct_cat_puzzle,
unsigned_spend_bundle_for_spendable_cats,
SpendableCAT,
)
from chia.wallet.cat_wallet.cat_info import CATInfo
from chia.wallet.transaction_record import TransactionRecord
GENESIS_BY_ID_MOD = load_clvm("genesis-by-coin-id-with-0.clvm")
GENESIS_BY_PUZHASH_MOD = load_clvm("genesis-by-puzzle-hash-with-0.clvm")
EVERYTHING_WITH_SIG_MOD = load_clvm("everything_with_signature.clvm")
DELEGATED_LIMITATIONS_MOD = load_clvm("delegated_genesis_checker.clvm")
class LimitationsProgram:
@staticmethod
def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]:
raise NotImplementedError("Need to implement 'match' on limitations programs")
@staticmethod
def construct(args: List[Program]) -> Program:
raise NotImplementedError("Need to implement 'construct' on limitations programs")
@staticmethod
def solve(args: List[Program], solution_dict: Dict) -> Program:
raise NotImplementedError("Need to implement 'solve' on limitations programs")
@classmethod
async def generate_issuance_bundle(
cls, wallet, cat_tail_info: Dict, amount: uint64
) -> Tuple[TransactionRecord, SpendBundle]:
raise NotImplementedError("Need to implement 'generate_issuance_bundle' on limitations programs")
class GenesisById(LimitationsProgram):
"""
This TAIL allows for coins to be issued only by a specific "genesis" coin ID.
There can therefore only be one issuance. There is no minting or melting allowed.
"""
@staticmethod
def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]:
if uncurried_mod == GENESIS_BY_ID_MOD:
genesis_id = curried_args.first()
return True, [genesis_id]
else:
return False, []
@staticmethod
def construct(args: List[Program]) -> Program:
return GENESIS_BY_ID_MOD.curry(args[0])
@staticmethod
def solve(args: List[Program], solution_dict: Dict) -> Program:
return Program.to([])
@classmethod
async def generate_issuance_bundle(cls, wallet, _: Dict, amount: uint64) -> Tuple[TransactionRecord, SpendBundle]:
coins = await wallet.standard_wallet.select_coins(amount)
origin = coins.copy().pop()
origin_id = origin.name()
cc_inner: Program = await wallet.get_new_inner_puzzle()
await wallet.add_lineage(origin_id, LineageProof())
genesis_coin_checker: Program = cls.construct([Program.to(origin_id)])
minted_cc_puzzle_hash: bytes32 = construct_cat_puzzle(
CAT_MOD, genesis_coin_checker.get_tree_hash(), cc_inner
).get_tree_hash()
tx_record: TransactionRecord = await wallet.standard_wallet.generate_signed_transaction(
amount, minted_cc_puzzle_hash, uint64(0), origin_id, coins
)
assert tx_record.spend_bundle is not None
inner_solution = wallet.standard_wallet.add_condition_to_solution(
Program.to([51, 0, -113, genesis_coin_checker, []]),
wallet.standard_wallet.make_solution(
primaries=[{"puzzlehash": cc_inner.get_tree_hash(), "amount": amount}],
),
)
eve_spend = unsigned_spend_bundle_for_spendable_cats(
CAT_MOD,
[
SpendableCAT(
list(filter(lambda a: a.amount == amount, tx_record.additions))[0],
genesis_coin_checker.get_tree_hash(),
cc_inner,
inner_solution,
limitations_program_reveal=genesis_coin_checker,
)
],
)
signed_eve_spend = await wallet.sign(eve_spend)
if wallet.cat_info.my_tail is None:
await wallet.save_info(
CATInfo(genesis_coin_checker.get_tree_hash(), genesis_coin_checker, wallet.cat_info.lineage_proofs),
False,
)
return tx_record, SpendBundle.aggregate([tx_record.spend_bundle, signed_eve_spend])
class GenesisByPuzhash(LimitationsProgram):
"""
This TAIL allows for issuance of a certain coin only by a specific puzzle hash.
There is no minting or melting allowed.
"""
@staticmethod
def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]:
if uncurried_mod == GENESIS_BY_PUZHASH_MOD:
genesis_puzhash = curried_args.first()
return True, [genesis_puzhash]
else:
return False, []
@staticmethod
def construct(args: List[Program]) -> Program:
return GENESIS_BY_PUZHASH_MOD.curry(args[0])
@staticmethod
def solve(args: List[Program], solution_dict: Dict) -> Program:
pid = hexstr_to_bytes(solution_dict["parent_coin_info"])
return Program.to([pid, solution_dict["amount"]])
class EverythingWithSig(LimitationsProgram):
"""
This TAIL allows for issuance, minting, and melting as long as you provide a signature with the spend.
"""
@staticmethod
def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]:
if uncurried_mod == EVERYTHING_WITH_SIG_MOD:
pubkey = curried_args.first()
return True, [pubkey]
else:
return False, []
@staticmethod
def construct(args: List[Program]) -> Program:
return EVERYTHING_WITH_SIG_MOD.curry(args[0])
@staticmethod
def solve(args: List[Program], solution_dict: Dict) -> Program:
return Program.to([])
class DelegatedLimitations(LimitationsProgram):
"""
This TAIL allows for another TAIL to be used, as long as a signature of that TAIL's puzzlehash is included.
"""
@staticmethod
def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]:
if uncurried_mod == DELEGATED_LIMITATIONS_MOD:
pubkey = curried_args.first()
return True, [pubkey]
else:
return False, []
@staticmethod
def construct(args: List[Program]) -> Program:
return DELEGATED_LIMITATIONS_MOD.curry(args[0])
@staticmethod
def solve(args: List[Program], solution_dict: Dict) -> Program:
signed_program = ALL_LIMITATIONS_PROGRAMS[solution_dict["signed_program"]["identifier"]]
inner_program_args = [Program.fromhex(item) for item in solution_dict["signed_program"]["args"]]
inner_solution_dict = solution_dict["program_arguments"]
return Program.to(
[
signed_program.construct(inner_program_args),
signed_program.solve(inner_program_args, inner_solution_dict),
]
)
# This should probably be much more elegant than just a dictionary with strings as identifiers
# Right now this is small and experimental so it can stay like this
ALL_LIMITATIONS_PROGRAMS: Dict[str, Any] = {
"genesis_by_id": GenesisById,
"genesis_by_puzhash": GenesisByPuzhash,
"everything_with_signature": EverythingWithSig,
"delegated_limitations": DelegatedLimitations,
}
def match_limitations_program(limitations_program: Program) -> Tuple[Optional[LimitationsProgram], List[Program]]:
uncurried_mod, curried_args = limitations_program.uncurry()
for key, lp in ALL_LIMITATIONS_PROGRAMS.items():
matched, args = lp.match(uncurried_mod, curried_args)
if matched:
return lp, args
return None, []

View File

@ -7,6 +7,7 @@ from chia.types.condition_opcodes import ConditionOpcode
from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash
from chia.util.condition_tools import parse_sexp_to_conditions
from chia.util.ints import uint32
from chia.types.blockchain_format.sized_bytes import bytes32
address1 = "txch15gx26ndmacfaqlq8m0yajeggzceu7cvmaz4df0hahkukes695rss6lej7h" # Gene wallet (m/12381/8444/2/42):
address2 = "txch1c2cguswhvmdyz9hr3q6hak2h6p9dw4rz82g4707k2xy2sarv705qcce4pn" # Mariano address (m/12381/8444/2/0)
@ -45,11 +46,8 @@ def make_puzzle(amount: int) -> int:
for cvp in result_human:
assert len(cvp.vars) == 2
total_chia += int_from_bytes(cvp.vars[1])
# TODO: address hint error and remove ignore
# error: Argument 1 to "encode_puzzle_hash" has incompatible type "bytes"; expected "bytes32"
# [arg-type]
print(
f"{ConditionOpcode(cvp.opcode).name}: {encode_puzzle_hash(cvp.vars[0], prefix)}," # type: ignore[arg-type] # noqa E501
f"{ConditionOpcode(cvp.opcode).name}: {encode_puzzle_hash(bytes32(cvp.vars[0]), prefix)},"
f" amount: {int_from_bytes(cvp.vars[1])}"
)
return total_chia

View File

@ -1,7 +1,11 @@
from typing import Optional, List
from chia.util.condition_tools import ConditionOpcode
def make_create_coin_condition(puzzle_hash, amount):
def make_create_coin_condition(puzzle_hash, amount, memos: Optional[List[bytes]]) -> List:
if memos is not None:
return [ConditionOpcode.CREATE_COIN, puzzle_hash, amount, memos]
return [ConditionOpcode.CREATE_COIN, puzzle_hash, amount]

View File

@ -0,0 +1,45 @@
(mod notarized_payments
;; `notarized_payments` is a list of notarized coin payments
;; a notarized coin payment is `(nonce . ((puzzle_hash amount ...) (puzzle_hash amount ...) ...))`
;; Each notarized coin payment creates some `(CREATE_COIN puzzle_hash amount ...)` payments
;; and a `(CREATE_PUZZLE_ANNOUNCEMENT (sha256tree notarized_coin_payment))` announcement
;; The idea is the other side of this trade requires observing the announcement from a
;; `settlement_payments` puzzle hash as a condition of one or more coin spends.
(include condition_codes.clvm)
(defun sha256tree (TREE)
(if (l TREE)
(sha256 2 (sha256tree (f TREE)) (sha256tree (r TREE)))
(sha256 1 TREE)
)
)
(defun create_coins_for_payment (payment_params so_far)
(if payment_params
(c (c CREATE_COIN (f payment_params)) (create_coins_for_payment (r payment_params) so_far))
so_far
)
)
(defun-inline create_announcement_for_payment (notarized_payment)
(list CREATE_PUZZLE_ANNOUNCEMENT
(sha256tree notarized_payment))
)
(defun-inline augment_condition_list (notarized_payment so_far)
(c
(create_announcement_for_payment notarized_payment)
(create_coins_for_payment (r notarized_payment) so_far)
)
)
(defun construct_condition_list (notarized_payments)
(if notarized_payments
(augment_condition_list (f notarized_payments) (construct_condition_list (r notarized_payments)))
()
)
)
(construct_condition_list notarized_payments)
)

View File

@ -0,0 +1 @@
ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080

View File

@ -0,0 +1 @@
bae24162efbd568f89bc7a340798a6118df0189eb9e3f8697bcea27af99f8f79

View File

@ -30,13 +30,13 @@
(defmacro assert items
(if (r items)
(list if (f items) (c assert (r items)) (q . (x)))
(f items)
)
(f items)
)
)
(defun-inline mod_hash_for_singleton_struct (SINGLETON_STRUCT) (f SINGLETON_STRUCT))
(defun-inline launcher_id_for_singleton_struct (SINGLETON_STRUCT) (f (r SINGLETON_STRUCT)))
(defun-inline launcher_puzzle_hash_for_singleton_struct (SINGLETON_STRUCT) (r (r SINGLETON_STRUCT)))
(defun-inline mod_hash_for_singleton_struct (SINGLETON_STRUCT) (f SINGLETON_STRUCT))
(defun-inline launcher_id_for_singleton_struct (SINGLETON_STRUCT) (f (r SINGLETON_STRUCT)))
(defun-inline launcher_puzzle_hash_for_singleton_struct (SINGLETON_STRUCT) (r (r SINGLETON_STRUCT)))
;; return the full puzzlehash for a singleton with the innerpuzzle curried in
; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc

View File

@ -31,6 +31,16 @@ def adapt_inner_to_singleton(inner_puzzle: Program) -> Program:
return Program.to([2, (1, inner_puzzle), [6, 1]])
def adapt_inner_puzzle_hash_to_singleton(inner_puzzle_hash: bytes32) -> bytes32:
puzzle = adapt_inner_to_singleton(Program.to(inner_puzzle_hash))
return puzzle.get_tree_hash(inner_puzzle_hash)
def remove_singleton_truth_wrapper(puzzle: Program) -> Program:
inner_puzzle = puzzle.rest().first().rest()
return inner_puzzle
# Take standard coin and amount -> launch conditions & launcher coin solution
def launch_conditions_and_coinsol(
coin: Coin,

View File

@ -0,0 +1,206 @@
from typing import Tuple, Dict, List, Optional, Any
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.spend_bundle import SpendBundle
from chia.util.ints import uint64
from chia.util.byte_types import hexstr_to_bytes
from chia.wallet.lineage_proof import LineageProof
from chia.wallet.puzzles.load_clvm import load_clvm
from chia.wallet.cat_wallet.cat_utils import (
CAT_MOD,
construct_cat_puzzle,
unsigned_spend_bundle_for_spendable_cats,
SpendableCAT,
)
from chia.wallet.cat_wallet.cat_info import CATInfo
from chia.wallet.transaction_record import TransactionRecord
GENESIS_BY_ID_MOD = load_clvm("genesis_by_coin_id.clvm")
GENESIS_BY_PUZHASH_MOD = load_clvm("genesis_by_puzzle_hash.clvm")
EVERYTHING_WITH_SIG_MOD = load_clvm("everything_with_signature.clvm")
DELEGATED_LIMITATIONS_MOD = load_clvm("delegated_tail.clvm")
class LimitationsProgram:
@staticmethod
def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]:
raise NotImplementedError("Need to implement 'match' on limitations programs")
@staticmethod
def construct(args: List[Program]) -> Program:
raise NotImplementedError("Need to implement 'construct' on limitations programs")
@staticmethod
def solve(args: List[Program], solution_dict: Dict) -> Program:
raise NotImplementedError("Need to implement 'solve' on limitations programs")
@classmethod
async def generate_issuance_bundle(
cls, wallet, cat_tail_info: Dict, amount: uint64
) -> Tuple[TransactionRecord, SpendBundle]:
raise NotImplementedError("Need to implement 'generate_issuance_bundle' on limitations programs")
class GenesisById(LimitationsProgram):
"""
This TAIL allows for coins to be issued only by a specific "genesis" coin ID.
There can therefore only be one issuance. There is no minting or melting allowed.
"""
@staticmethod
def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]:
if uncurried_mod == GENESIS_BY_ID_MOD:
genesis_id = curried_args.first()
return True, [genesis_id]
else:
return False, []
@staticmethod
def construct(args: List[Program]) -> Program:
return GENESIS_BY_ID_MOD.curry(args[0])
@staticmethod
def solve(args: List[Program], solution_dict: Dict) -> Program:
return Program.to([])
@classmethod
async def generate_issuance_bundle(cls, wallet, _: Dict, amount: uint64) -> Tuple[TransactionRecord, SpendBundle]:
coins = await wallet.standard_wallet.select_coins(amount)
origin = coins.copy().pop()
origin_id = origin.name()
cat_inner: Program = await wallet.get_new_inner_puzzle()
await wallet.add_lineage(origin_id, LineageProof())
tail: Program = cls.construct([Program.to(origin_id)])
minted_cat_puzzle_hash: bytes32 = construct_cat_puzzle(CAT_MOD, tail.get_tree_hash(), cat_inner).get_tree_hash()
tx_record: TransactionRecord = await wallet.standard_wallet.generate_signed_transaction(
amount, minted_cat_puzzle_hash, uint64(0), origin_id, coins
)
assert tx_record.spend_bundle is not None
inner_solution = wallet.standard_wallet.add_condition_to_solution(
Program.to([51, 0, -113, tail, []]),
wallet.standard_wallet.make_solution(
primaries=[{"puzzlehash": cat_inner.get_tree_hash(), "amount": amount}],
),
)
eve_spend = unsigned_spend_bundle_for_spendable_cats(
CAT_MOD,
[
SpendableCAT(
list(filter(lambda a: a.amount == amount, tx_record.additions))[0],
tail.get_tree_hash(),
cat_inner,
inner_solution,
limitations_program_reveal=tail,
)
],
)
signed_eve_spend = await wallet.sign(eve_spend)
if wallet.cat_info.my_tail is None:
await wallet.save_info(
CATInfo(tail.get_tree_hash(), tail, wallet.cat_info.lineage_proofs),
False,
)
return tx_record, SpendBundle.aggregate([tx_record.spend_bundle, signed_eve_spend])
class GenesisByPuzhash(LimitationsProgram):
"""
This TAIL allows for issuance of a certain coin only by a specific puzzle hash.
There is no minting or melting allowed.
"""
@staticmethod
def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]:
if uncurried_mod == GENESIS_BY_PUZHASH_MOD:
genesis_puzhash = curried_args.first()
return True, [genesis_puzhash]
else:
return False, []
@staticmethod
def construct(args: List[Program]) -> Program:
return GENESIS_BY_PUZHASH_MOD.curry(args[0])
@staticmethod
def solve(args: List[Program], solution_dict: Dict) -> Program:
pid = hexstr_to_bytes(solution_dict["parent_coin_info"])
return Program.to([pid, solution_dict["amount"]])
class EverythingWithSig(LimitationsProgram):
"""
This TAIL allows for issuance, minting, and melting as long as you provide a signature with the spend.
"""
@staticmethod
def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]:
if uncurried_mod == EVERYTHING_WITH_SIG_MOD:
pubkey = curried_args.first()
return True, [pubkey]
else:
return False, []
@staticmethod
def construct(args: List[Program]) -> Program:
return EVERYTHING_WITH_SIG_MOD.curry(args[0])
@staticmethod
def solve(args: List[Program], solution_dict: Dict) -> Program:
return Program.to([])
class DelegatedLimitations(LimitationsProgram):
"""
This TAIL allows for another TAIL to be used, as long as a signature of that TAIL's puzzlehash is included.
"""
@staticmethod
def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]:
if uncurried_mod == DELEGATED_LIMITATIONS_MOD:
pubkey = curried_args.first()
return True, [pubkey]
else:
return False, []
@staticmethod
def construct(args: List[Program]) -> Program:
return DELEGATED_LIMITATIONS_MOD.curry(args[0])
@staticmethod
def solve(args: List[Program], solution_dict: Dict) -> Program:
signed_program = ALL_LIMITATIONS_PROGRAMS[solution_dict["signed_program"]["identifier"]]
inner_program_args = [Program.fromhex(item) for item in solution_dict["signed_program"]["args"]]
inner_solution_dict = solution_dict["program_arguments"]
return Program.to(
[
signed_program.construct(inner_program_args),
signed_program.solve(inner_program_args, inner_solution_dict),
]
)
# This should probably be much more elegant than just a dictionary with strings as identifiers
# Right now this is small and experimental so it can stay like this
ALL_LIMITATIONS_PROGRAMS: Dict[str, Any] = {
"genesis_by_id": GenesisById,
"genesis_by_puzhash": GenesisByPuzhash,
"everything_with_signature": EverythingWithSig,
"delegated_limitations": DelegatedLimitations,
}
def match_limitations_program(limitations_program: Program) -> Tuple[Optional[LimitationsProgram], List[Program]]:
uncurried_mod, curried_args = limitations_program.uncurry()
for key, lp in ALL_LIMITATIONS_PROGRAMS.items():
matched, args = lp.match(uncurried_mod, curried_args)
if matched:
return lp, args
return None, []

View File

@ -1,242 +0,0 @@
# this is used to iterate on `cc.clvm` to ensure that it's producing the sort
# of output that we expect
from typing import Dict, List, Optional, Tuple
from blspy import G2Element
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.condition_opcodes import ConditionOpcode
from chia.types.spend_bundle import CoinSpend, SpendBundle
from chia.util.ints import uint64
from chia.wallet.cc_wallet.cc_utils import (
CC_MOD,
cc_puzzle_for_inner_puzzle,
cc_puzzle_hash_for_inner_puzzle_hash,
spend_bundle_for_spendable_ccs,
spendable_cc_list_from_coin_spend,
)
from chia.wallet.puzzles.genesis_by_coin_id_with_0 import create_genesis_or_zero_coin_checker
from chia.wallet.puzzles.genesis_by_puzzle_hash_with_0 import create_genesis_puzzle_or_zero_coin_checker
CONDITIONS = dict((k, bytes(v)[0]) for k, v in ConditionOpcode.__members__.items()) # pylint: disable=E1101
NULL_SIGNATURE = G2Element()
ANYONE_CAN_SPEND_PUZZLE = Program.to(1) # simply return the conditions
PUZZLE_TABLE: Dict[bytes32, Program] = dict((_.get_tree_hash(), _) for _ in [ANYONE_CAN_SPEND_PUZZLE])
def hash_to_puzzle_f(puzzle_hash: bytes32) -> Optional[Program]:
return PUZZLE_TABLE.get(puzzle_hash)
def add_puzzles_to_puzzle_preimage_db(puzzles: List[Program]) -> None:
for _ in puzzles:
PUZZLE_TABLE[_.get_tree_hash()] = _
def int_as_bytes32(v: int) -> bytes32:
return bytes32(v.to_bytes(32, byteorder="big"))
def generate_farmed_coin(
block_index: int,
puzzle_hash: bytes32,
amount: int,
) -> Coin:
"""
Generate a (fake) coin which can be used as a starting point for a chain
of coin tests.
"""
return Coin(int_as_bytes32(block_index), puzzle_hash, uint64(amount))
def issue_cc_from_farmed_coin(
mod_code: Program,
coin_checker_for_farmed_coin,
block_id: int,
inner_puzzle_hash: bytes32,
amount: int,
) -> Tuple[Program, SpendBundle]:
"""
This is an example of how to issue a cc.
"""
# get a farmed coin
farmed_puzzle = ANYONE_CAN_SPEND_PUZZLE
farmed_puzzle_hash = farmed_puzzle.get_tree_hash()
# mint a cc
farmed_coin = generate_farmed_coin(block_id, farmed_puzzle_hash, amount=uint64(amount))
genesis_coin_checker = coin_checker_for_farmed_coin(farmed_coin)
minted_cc_puzzle_hash = cc_puzzle_hash_for_inner_puzzle_hash(mod_code, genesis_coin_checker, inner_puzzle_hash)
output_conditions = [[ConditionOpcode.CREATE_COIN, minted_cc_puzzle_hash, farmed_coin.amount]]
# for this very simple puzzle, the solution is simply the output conditions
# this is just a coincidence... for more complicated puzzles, you'll likely have to do some real work
solution = Program.to(output_conditions)
coin_spend = CoinSpend(farmed_coin, farmed_puzzle, solution)
spend_bundle = SpendBundle([coin_spend], NULL_SIGNATURE)
return genesis_coin_checker, spend_bundle
def solution_for_pay_to_any(puzzle_hash_amount_pairs: List[Tuple[bytes32, int]]) -> Program:
output_conditions = [
[ConditionOpcode.CREATE_COIN, puzzle_hash, amount] for puzzle_hash, amount in puzzle_hash_amount_pairs
]
return Program.to(output_conditions)
def test_spend_through_n(mod_code, coin_checker_for_farmed_coin, n):
"""
Test to spend ccs from a farmed coin to a cc genesis coin, then to N outputs,
then joining back down to two outputs.
"""
################################
# spend from a farmed coin to a cc genesis coin
# get a farmed coin
eve_inner_puzzle = ANYONE_CAN_SPEND_PUZZLE
eve_inner_puzzle_hash = eve_inner_puzzle.get_tree_hash()
# generate output values [0x100, 0x200, ...]
output_values = [0x100 + 0x100 * _ for _ in range(n)]
total_minted = sum(output_values)
genesis_coin_checker, spend_bundle = issue_cc_from_farmed_coin(
mod_code, coin_checker_for_farmed_coin, 1, eve_inner_puzzle_hash, total_minted
)
# hack the wrapped puzzles into the PUZZLE_TABLE DB
puzzles_for_db = [cc_puzzle_for_inner_puzzle(mod_code, genesis_coin_checker, eve_inner_puzzle)]
add_puzzles_to_puzzle_preimage_db(puzzles_for_db)
spend_bundle.debug()
################################
# collect up the spendable coins
spendable_cc_list = []
for coin_spend in spend_bundle.coin_spends:
spendable_cc_list.extend(spendable_cc_list_from_coin_spend(coin_spend, hash_to_puzzle_f))
# now spend the genesis coin cc to N outputs
output_conditions = solution_for_pay_to_any([(eve_inner_puzzle_hash, _) for _ in output_values])
inner_puzzle_solution = Program.to(output_conditions)
spend_bundle = spend_bundle_for_spendable_ccs(
mod_code,
genesis_coin_checker,
spendable_cc_list,
[inner_puzzle_solution],
)
spend_bundle.debug()
################################
# collect up the spendable coins
spendable_cc_list = []
for coin_spend in spend_bundle.coin_spends:
spendable_cc_list.extend(spendable_cc_list_from_coin_spend(coin_spend, hash_to_puzzle_f))
# now spend N inputs to two outputs
output_amounts = ([0] * (n - 2)) + [0x1, total_minted - 1]
inner_solutions = [
solution_for_pay_to_any([(eve_inner_puzzle_hash, amount)] if amount else []) for amount in output_amounts
]
spend_bundle = spend_bundle_for_spendable_ccs(
mod_code,
genesis_coin_checker,
spendable_cc_list,
inner_solutions,
)
spend_bundle.debug()
def test_spend_zero_coin(mod_code: Program, coin_checker_for_farmed_coin):
"""
Test to spend ccs from a farmed coin to a cc genesis coin, then to N outputs,
then joining back down to two outputs.
"""
eve_inner_puzzle = ANYONE_CAN_SPEND_PUZZLE
eve_inner_puzzle_hash = eve_inner_puzzle.get_tree_hash()
total_minted = 0x111
genesis_coin_checker, spend_bundle = issue_cc_from_farmed_coin(
mod_code, coin_checker_for_farmed_coin, 1, eve_inner_puzzle_hash, total_minted
)
puzzles_for_db = [cc_puzzle_for_inner_puzzle(mod_code, genesis_coin_checker, eve_inner_puzzle)]
add_puzzles_to_puzzle_preimage_db(puzzles_for_db)
eve_cc_list = []
for _ in spend_bundle.coin_spends:
eve_cc_list.extend(spendable_cc_list_from_coin_spend(_, hash_to_puzzle_f))
assert len(eve_cc_list) == 1
eve_cc_spendable = eve_cc_list[0]
# farm regular chia
farmed_coin = generate_farmed_coin(2, eve_inner_puzzle_hash, amount=500)
# create a zero cc from this farmed coin
wrapped_cc_puzzle_hash = cc_puzzle_hash_for_inner_puzzle_hash(mod_code, genesis_coin_checker, eve_inner_puzzle_hash)
solution = solution_for_pay_to_any([(wrapped_cc_puzzle_hash, 0)])
coin_spend = CoinSpend(farmed_coin, ANYONE_CAN_SPEND_PUZZLE, solution)
spendable_cc_list = spendable_cc_list_from_coin_spend(coin_spend, hash_to_puzzle_f)
assert len(spendable_cc_list) == 1
zero_cc_spendable = spendable_cc_list[0]
# we have our zero coin
# now try to spend it
spendable_cc_list = [eve_cc_spendable, zero_cc_spendable]
inner_solutions = [
solution_for_pay_to_any([]),
solution_for_pay_to_any([(wrapped_cc_puzzle_hash, eve_cc_spendable.coin.amount)]),
]
spend_bundle = spend_bundle_for_spendable_ccs(mod_code, genesis_coin_checker, spendable_cc_list, inner_solutions)
spend_bundle.debug()
def main():
mod_code = CC_MOD
def coin_checker_for_farmed_coin_by_coin_id(coin: Coin):
return create_genesis_or_zero_coin_checker(coin.name())
test_spend_through_n(mod_code, coin_checker_for_farmed_coin_by_coin_id, 12)
test_spend_zero_coin(mod_code, coin_checker_for_farmed_coin_by_coin_id)
def coin_checker_for_farmed_coin_by_puzzle_hash(coin: Coin):
return create_genesis_puzzle_or_zero_coin_checker(coin.puzzle_hash)
test_spend_through_n(mod_code, coin_checker_for_farmed_coin_by_puzzle_hash, 10)
if __name__ == "__main__":
main()

View File

@ -85,6 +85,7 @@ class RLWallet:
pubkey,
WalletType.RATE_LIMITED,
wallet_info.id,
False,
)
]
)
@ -121,11 +122,7 @@ class RLWallet:
await wallet_state_manager.puzzle_store.add_derivation_paths(
[
DerivationRecord(
unused,
bytes32(token_bytes(32)),
pubkey,
WalletType.RATE_LIMITED,
wallet_info.id,
unused, bytes32(token_bytes(32)), pubkey, WalletType.RATE_LIMITED, wallet_info.id, False
)
]
)
@ -193,6 +190,7 @@ class RLWallet:
G1Element.from_bytes(self.rl_info.admin_pubkey),
WalletType.RATE_LIMITED,
self.id(),
False,
)
await self.wallet_state_manager.puzzle_store.add_derivation_paths([record])
@ -235,8 +233,8 @@ class RLWallet:
assert self.rl_info.user_pubkey is not None
origin = Coin(
bytes32.from_hexstr(origin_parent_id),
bytes32.from_hexstr(origin_puzzle_hash),
bytes32(hexstr_to_bytes(origin_parent_id)),
bytes32(hexstr_to_bytes(origin_puzzle_hash)),
origin_amount,
)
rl_puzzle = rl_puzzle_for_pk(
@ -274,6 +272,7 @@ class RLWallet:
user_pubkey,
WalletType.RATE_LIMITED,
self.id(),
False,
)
aggregation_puzzlehash = self.rl_get_aggregation_puzzlehash(new_rl_info.rl_puzzle_hash)
@ -283,6 +282,7 @@ class RLWallet:
user_pubkey,
WalletType.RATE_LIMITED,
self.id(),
False,
)
await self.wallet_state_manager.puzzle_store.add_derivation_paths([record, record2])
self.wallet_state_manager.set_coin_with_puzzlehash_created_callback(
@ -303,13 +303,11 @@ class RLWallet:
rl_coin = await self._get_rl_coin()
puzzle_hash = rl_coin.puzzle_hash if rl_coin is not None else None
# TODO: address hint error and remove ignore
# error: Argument "to_puzzle_hash" to "TransactionRecord" has incompatible type "Optional[bytes32]";
# expected "bytes32" [arg-type]
assert puzzle_hash is not None
tx_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=puzzle_hash, # type: ignore[arg-type]
to_puzzle_hash=puzzle_hash,
amount=uint64(0),
fee_amount=uint64(0),
confirmed=False,
@ -322,6 +320,7 @@ class RLWallet:
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=spend_bundle.name(),
memos=list(spend_bundle.get_memos().items()),
)
asyncio.create_task(self.push_transaction(tx_record))
@ -520,7 +519,9 @@ class RLWallet:
spends.append(CoinSpend(coin, puzzle, solution))
return spends
async def generate_signed_transaction(self, amount, to_puzzle_hash, fee: uint64 = uint64(0)) -> TransactionRecord:
async def generate_signed_transaction(
self, amount, to_puzzle_hash, fee: uint64 = uint64(0), memo: Optional[List[bytes]] = None
) -> TransactionRecord:
self.rl_coin_record = await self._get_rl_coin_record()
if not self.rl_coin_record:
raise ValueError("No unspent coin (zero balance)")
@ -545,6 +546,7 @@ class RLWallet:
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=spend_bundle.name(),
memos=list(spend_bundle.get_memos().items()),
)
async def rl_sign_transaction(self, spends: List[CoinSpend]) -> SpendBundle:
@ -621,6 +623,7 @@ class RLWallet:
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=spend_bundle.name(),
memos=list(spend_bundle.get_memos().items()),
)
# This is for using the AC locked coin and aggregating it into wallet - must happen in same block as RL Mode 2

View File

@ -39,36 +39,3 @@ class UserSettings:
name = setting.__class__.__name__
await self.basic_store.set_object(name, setting)
self.settings[name] = setting
async def user_skipped_backup_import(self):
new = BackupInitialized(
user_initialized=True,
user_skipped=True,
backup_info_imported=False,
new_wallet=False,
)
await self.setting_updated(new)
return new
async def user_imported_backup(self):
new = BackupInitialized(
user_initialized=True,
user_skipped=False,
backup_info_imported=True,
new_wallet=False,
)
await self.setting_updated(new)
return new
async def user_created_new_wallet(self):
new = BackupInitialized(
user_initialized=True,
user_skipped=False,
backup_info_imported=False,
new_wallet=True,
)
await self.setting_updated(new)
return new
def get_backup_settings(self) -> BackupInitialized:
return self.settings[BackupInitialized.__name__]

View File

@ -1,34 +1,23 @@
import logging
import time
import traceback
from pathlib import Path
from secrets import token_bytes
from typing import Any, Dict, List, Optional, Tuple
from blspy import AugSchemeMPL
from typing import Any, Dict, List, Optional, Tuple, Union, Set
from chia.protocols.wallet_protocol import CoinState
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.spend_bundle import SpendBundle
from chia.types.coin_spend import CoinSpend
from chia.util.byte_types import hexstr_to_bytes
from chia.util.db_wrapper import DBWrapper
from chia.util.hash import std_hash
from chia.util.ints import uint32, uint64
from chia.wallet.cc_wallet import cc_utils
from chia.wallet.cc_wallet.cc_utils import CC_MOD, SpendableCC, spend_bundle_for_spendable_ccs, uncurry_cc
from chia.wallet.cc_wallet.cc_wallet import CCWallet
from chia.wallet.puzzles.genesis_by_coin_id_with_0 import genesis_coin_id_for_genesis_coin_checker
from chia.wallet.cat_wallet.cat_wallet import CATWallet
from chia.wallet.payment import Payment
from chia.wallet.trade_record import TradeRecord
from chia.wallet.trading.offer import Offer, NotarizedPayment
from chia.wallet.trading.trade_status import TradeStatus
from chia.wallet.trading.trade_store import TradeStore
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.trade_utils import (
get_discrepancies_for_spend_bundle,
get_output_amount_for_puzzle_and_solution,
get_output_discrepancy_for_puzzle_and_solution,
)
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.wallet_types import WalletType
from chia.wallet.wallet import Wallet
@ -62,7 +51,7 @@ class TradeManager:
async def get_coins_of_interest(
self,
) -> Tuple[Dict[bytes32, Coin], Dict[bytes32, Coin]]:
) -> Dict[bytes32, Coin]:
"""
Returns list of coins we want to check if they are included in filter,
These will include coins that belong to us and coins that that on other side of treade
@ -74,81 +63,69 @@ class TradeManager:
all_pending.extend(pending_accept)
all_pending.extend(pending_confirm)
all_pending.extend(pending_cancel)
removals = {}
additions = {}
interested_dict = {}
for trade in all_pending:
for coin in trade.removals:
removals[coin.name()] = coin
for coin in trade.additions:
additions[coin.name()] = coin
for coin in trade.coins_of_interest:
interested_dict[coin.name()] = coin
return removals, additions
return interested_dict
async def get_trade_by_coin(self, coin: Coin) -> Optional[TradeRecord]:
all_trades = await self.get_all_trades()
for trade in all_trades:
if trade.status == TradeStatus.CANCELED.value:
if trade.status == TradeStatus.CANCELLED.value:
continue
if coin in trade.removals:
return trade
if coin in trade.additions:
if coin in trade.coins_of_interest:
return trade
return None
async def coins_of_interest_farmed(self, removals: List[Coin], additions: List[Coin], height: uint32):
async def coins_of_interest_farmed(self, coin_state: CoinState):
"""
If both our coins and other coins in trade got removed that means that trade was successfully executed
If coins from other side of trade got farmed without ours, that means that trade failed because either someone
else completed trade or other side of trade canceled the trade by doing a spend.
If our coins got farmed but coins from other side didn't, we successfully canceled trade by spending inputs.
"""
removal_dict = {}
addition_dict = {}
checked: Dict[bytes32, Coin] = {}
for coin in removals:
removal_dict[coin.name()] = coin
for coin in additions:
addition_dict[coin.name()] = coin
trade = await self.get_trade_by_coin(coin_state.coin)
if trade is None:
self.log.error(f"Coin: {coin_state.coin}, not in any trade")
return
if coin_state.spent_height is None:
self.log.error(f"Coin: {coin_state.coin}, has not been spent so trade can remain valid")
all_coins = []
all_coins.extend(removals)
all_coins.extend(additions)
# Then let's filter the offer into coins that WE offered
offer = Offer.from_bytes(trade.offer)
primary_coin_ids = [c.name() for c in offer.get_primary_coins()]
our_coin_records: List[WalletCoinRecord] = await self.wallet_state_manager.coin_store.get_multiple_coin_records(
primary_coin_ids
)
our_primary_coins: List[bytes32] = [cr.coin.name() for cr in our_coin_records]
all_settlement_payments: List[Coin] = [c for coins in offer.get_offered_coins().values() for c in coins]
our_settlement_payments: List[Coin] = list(
filter(lambda c: offer.get_root_removal(c).name() in our_primary_coins, all_settlement_payments)
)
our_settlement_ids: List[bytes32] = [c.name() for c in our_settlement_payments]
for coin in all_coins:
if coin.name() in checked:
continue
trade = await self.get_trade_by_coin(coin)
if trade is None:
self.log.error(f"Coin: {Coin}, not in any trade")
continue
# And get all relevant coin states
coin_states = await self.wallet_state_manager.get_coin_state(our_settlement_ids)
assert coin_states is not None
coin_state_names: List[bytes32] = [cs.coin.name() for cs in coin_states]
# Check if all coins that are part of the trade got farmed
# If coin is missing, trade failed
failed = False
for removed_coin in trade.removals:
if removed_coin.name() not in removal_dict:
self.log.error(f"{removed_coin} from trade not removed")
failed = True
checked[removed_coin.name()] = removed_coin
for added_coin in trade.additions:
if added_coin.name() not in addition_dict:
self.log.error(f"{added_coin} from trade not added")
failed = True
checked[coin.name()] = coin
if failed is False:
# Mark this trade as successful
await self.trade_store.set_status(trade.trade_id, TradeStatus.CONFIRMED, True, height)
self.log.info(f"Trade with id: {trade.trade_id} confirmed at height: {height}")
else:
# Either we canceled this trade or this trade failed
if trade.status == TradeStatus.PENDING_CANCEL.value:
await self.trade_store.set_status(trade.trade_id, TradeStatus.CANCELED, True)
self.log.info(f"Trade with id: {trade.trade_id} canceled at height: {height}")
elif trade.status == TradeStatus.PENDING_CONFIRM.value:
await self.trade_store.set_status(trade.trade_id, TradeStatus.FAILED, True)
self.log.warning(f"Trade with id: {trade.trade_id} failed at height: {height}")
# If any of our settlement_payments were spent, this offer was a success!
if set(our_settlement_ids) & set(coin_state_names):
height = coin_states[0].spent_height
await self.trade_store.set_status(trade.trade_id, TradeStatus.CONFIRMED, True, height)
self.log.info(f"Trade with id: {trade.trade_id} confirmed at height: {height}")
else:
# In any other scenario this trade failed
await self.wallet_state_manager.delete_trade_transactions(trade.trade_id)
if trade.status == TradeStatus.PENDING_CANCEL.value:
await self.trade_store.set_status(trade.trade_id, TradeStatus.CANCELLED, True)
self.log.info(f"Trade with id: {trade.trade_id} canceled")
elif trade.status == TradeStatus.PENDING_CONFIRM.value:
await self.trade_store.set_status(trade.trade_id, TradeStatus.FAILED, True)
self.log.warning(f"Trade with id: {trade.trade_id} failed")
async def get_locked_coins(self, wallet_id: int = None) -> Dict[bytes32, WalletCoinRecord]:
"""Returns a dictionary of confirmed coins that are locked by a trade."""
@ -159,18 +136,16 @@ class TradeManager:
all_pending.extend(pending_accept)
all_pending.extend(pending_confirm)
all_pending.extend(pending_cancel)
if len(all_pending) == 0:
return {}
coins_of_interest = []
for trade_offer in all_pending:
coins_of_interest.extend([c.name() for c in Offer.from_bytes(trade_offer.offer).get_involved_coins()])
result = {}
for trade_offer in all_pending:
if trade_offer.tx_spend_bundle is None:
locked = await self.get_locked_coins_in_spend_bundle(trade_offer.spend_bundle)
else:
locked = await self.get_locked_coins_in_spend_bundle(trade_offer.tx_spend_bundle)
for name, record in locked.items():
if wallet_id is None or record.wallet_id == wallet_id:
result[name] = record
coin_records = await self.wallet_state_manager.coin_store.get_multiple_coin_records(coins_of_interest)
for record in coin_records:
if wallet_id is None or record.wallet_id == wallet_id:
result[record.name()] = record
return result
@ -182,496 +157,341 @@ class TradeManager:
record = await self.trade_store.get_trade_record(trade_id)
return record
async def get_locked_coins_in_spend_bundle(self, bundle: SpendBundle) -> Dict[bytes32, WalletCoinRecord]:
"""Returns a list of coin records that are used in this SpendBundle"""
result = {}
removals = bundle.removals()
for coin in removals:
coin_record = await self.wallet_state_manager.coin_store.get_coin_record(coin.name())
if coin_record is None:
continue
result[coin_record.name()] = coin_record
return result
async def cancel_pending_offer(self, trade_id: bytes32):
await self.trade_store.set_status(trade_id, TradeStatus.CANCELED, False)
await self.trade_store.set_status(trade_id, TradeStatus.CANCELLED, False)
async def cancel_pending_offer_safely(self, trade_id: bytes32):
async def cancel_pending_offer_safely(
self, trade_id: bytes32, fee: uint64 = uint64(0)
) -> Optional[List[TransactionRecord]]:
"""This will create a transaction that includes coins that were offered"""
self.log.info(f"Secure-Cancel pending offer with id trade_id {trade_id.hex()}")
trade = await self.trade_store.get_trade_record(trade_id)
if trade is None:
return None
all_coins = trade.removals
for coin in all_coins:
all_txs: List[TransactionRecord] = []
fee_to_pay: uint64 = fee
for coin in Offer.from_bytes(trade.offer).get_primary_coins():
wallet = await self.wallet_state_manager.get_wallet_for_coin(coin.name())
if wallet is None:
continue
new_ph = await wallet.get_new_puzzlehash()
if wallet.type() == WalletType.COLOURED_COIN.value:
tx = await wallet.generate_signed_transaction(
[coin.amount], [new_ph], 0, coins={coin}, ignore_max_send_amount=True
# This should probably not switch on whether or not we're spending a CAT but it has to for now
if wallet.type() == WalletType.CAT:
txs = await wallet.generate_signed_transaction(
[coin.amount], [new_ph], fee=fee_to_pay, coins={coin}, ignore_max_send_amount=True
)
all_txs.extend(txs)
else:
if fee_to_pay > coin.amount:
selected_coins: Set[Coin] = await wallet.select_coins(
uint64(fee_to_pay - coin.amount),
exclude=[coin],
)
selected_coins.add(coin)
else:
selected_coins = {coin}
tx = await wallet.generate_signed_transaction(
coin.amount, new_ph, 0, coins={coin}, ignore_max_send_amount=True
uint64(sum([c.amount for c in selected_coins]) - fee_to_pay),
new_ph,
fee=fee_to_pay,
coins=selected_coins,
ignore_max_send_amount=True,
)
all_txs.append(tx)
fee_to_pay = uint64(0)
for tx in all_txs:
await self.wallet_state_manager.add_pending_transaction(tx_record=tx)
await self.trade_store.set_status(trade_id, TradeStatus.PENDING_CANCEL, False)
return None
return all_txs
async def save_trade(self, trade: TradeRecord):
await self.trade_store.add_trade_record(trade, False)
async def create_offer_for_ids(
self, offer: Dict[int, int], file_name: str
self, offer: Dict[Union[int, bytes32], int], fee: uint64 = uint64(0), validate_only: bool = False
) -> Tuple[bool, Optional[TradeRecord], Optional[str]]:
success, trade_offer, error = await self._create_offer_for_ids(offer)
success, created_offer, error = await self._create_offer_for_ids(offer, fee=fee)
if not success or created_offer is None:
raise Exception(f"Error creating offer: {error}")
if success is True and trade_offer is not None:
self.write_offer_to_disk(Path(file_name), trade_offer)
now = uint64(int(time.time()))
trade_offer: TradeRecord = TradeRecord(
confirmed_at_index=uint32(0),
accepted_at_time=None,
created_at_time=now,
is_my_offer=True,
sent=uint32(0),
offer=bytes(created_offer),
taken_offer=None,
coins_of_interest=created_offer.get_involved_coins(),
trade_id=created_offer.name(),
status=uint32(TradeStatus.PENDING_ACCEPT.value),
sent_to=[],
)
if success is True and trade_offer is not None and not validate_only:
await self.save_trade(trade_offer)
return success, trade_offer, error
async def _create_offer_for_ids(self, offer: Dict[int, int]) -> Tuple[bool, Optional[TradeRecord], Optional[str]]:
async def _create_offer_for_ids(
self, offer_dict: Dict[Union[int, bytes32], int], fee: uint64 = uint64(0)
) -> Tuple[bool, Optional[Offer], Optional[str]]:
"""
Offer is dictionary of wallet ids and amount
"""
spend_bundle = None
try:
for id in offer.keys():
amount = offer[id]
wallet_id = uint32(int(id))
wallet = self.wallet_state_manager.wallets[wallet_id]
if isinstance(wallet, CCWallet):
coins_to_offer: Dict[uint32, List[Coin]] = {}
requested_payments: Dict[Optional[bytes32], List[Payment]] = {}
for id, amount in offer_dict.items():
if amount > 0:
if isinstance(id, int):
wallet_id = uint32(id)
wallet = self.wallet_state_manager.wallets[wallet_id]
p2_ph: bytes32 = await wallet.get_new_puzzlehash()
if wallet.type() == WalletType.STANDARD_WALLET:
key: Optional[bytes32] = None
memos: List[bytes] = []
elif wallet.type() == WalletType.CAT:
key = bytes32(bytes.fromhex(wallet.get_asset_id()))
memos = [p2_ph]
else:
raise ValueError(f"Offers are not implemented for {wallet.type()}")
else:
p2_ph = await self.wallet_state_manager.main_wallet.get_new_puzzlehash()
key = id
memos = [p2_ph]
requested_payments[key] = [Payment(p2_ph, uint64(amount), memos)]
elif amount < 0:
assert isinstance(id, int)
wallet_id = uint32(id)
wallet = self.wallet_state_manager.wallets[wallet_id]
balance = await wallet.get_confirmed_balance()
if balance < abs(amount) and amount < 0:
if balance < abs(amount):
raise Exception(f"insufficient funds in wallet {wallet_id}")
if amount > 0:
if spend_bundle is None:
to_exclude: List[Coin] = []
else:
to_exclude = spend_bundle.removals()
zero_spend_bundle: SpendBundle = await wallet.generate_zero_val_coin(False, to_exclude)
coins_to_offer[wallet_id] = await wallet.select_coins(uint64(abs(amount)))
elif amount == 0:
raise ValueError("You cannot offer nor request 0 amount of something")
if spend_bundle is None:
spend_bundle = zero_spend_bundle
else:
spend_bundle = SpendBundle.aggregate([spend_bundle, zero_spend_bundle])
additions = zero_spend_bundle.additions()
removals = zero_spend_bundle.removals()
zero_val_coin: Optional[Coin] = None
for add in additions:
if add not in removals and add.amount == 0:
zero_val_coin = add
new_spend_bundle = await wallet.create_spend_bundle_relative_amount(amount, zero_val_coin)
else:
new_spend_bundle = await wallet.create_spend_bundle_relative_amount(amount)
elif isinstance(wallet, Wallet):
if spend_bundle is None:
to_exclude = []
else:
to_exclude = spend_bundle.removals()
new_spend_bundle = await wallet.create_spend_bundle_relative_chia(amount, to_exclude)
else:
return False, None, "unsupported wallet type"
if new_spend_bundle is None or new_spend_bundle.removals() == []:
raise Exception(f"Wallet {id} was unable to create offer.")
if spend_bundle is None:
spend_bundle = new_spend_bundle
else:
spend_bundle = SpendBundle.aggregate([spend_bundle, new_spend_bundle])
if spend_bundle is None:
return False, None, None
now = uint64(int(time.time()))
trade_offer: TradeRecord = TradeRecord(
confirmed_at_index=uint32(0),
accepted_at_time=None,
created_at_time=now,
my_offer=True,
sent=uint32(0),
spend_bundle=spend_bundle,
tx_spend_bundle=None,
additions=spend_bundle.additions(),
removals=spend_bundle.removals(),
trade_id=std_hash(spend_bundle.name() + bytes(now)),
status=uint32(TradeStatus.PENDING_ACCEPT.value),
sent_to=[],
all_coins: List[Coin] = [c for coins in coins_to_offer.values() for c in coins]
notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments(
requested_payments, all_coins
)
return True, trade_offer, None
announcements_to_assert = Offer.calculate_announcements(notarized_payments)
all_transactions: List[TransactionRecord] = []
fee_left_to_pay: uint64 = fee
for wallet_id, selected_coins in coins_to_offer.items():
wallet = self.wallet_state_manager.wallets[wallet_id]
# This should probably not switch on whether or not we're spending a CAT but it has to for now
if wallet.type() == WalletType.CAT:
txs = await wallet.generate_signed_transaction(
[abs(offer_dict[int(wallet_id)])],
[Offer.ph()],
fee=fee_left_to_pay,
coins=set(selected_coins),
puzzle_announcements_to_consume=announcements_to_assert,
)
all_transactions.extend(txs)
else:
tx = await wallet.generate_signed_transaction(
abs(offer_dict[int(wallet_id)]),
Offer.ph(),
fee=fee_left_to_pay,
coins=set(selected_coins),
puzzle_announcements_to_consume=announcements_to_assert,
)
all_transactions.append(tx)
fee_left_to_pay = uint64(0)
transaction_bundles: List[Optional[SpendBundle]] = [tx.spend_bundle for tx in all_transactions]
total_spend_bundle = SpendBundle.aggregate(list(filter(lambda b: b is not None, transaction_bundles)))
offer = Offer(notarized_payments, total_spend_bundle)
return True, offer, None
except Exception as e:
tb = traceback.format_exc()
self.log.error(f"Error with creating trade offer: {type(e)}{tb}")
return False, None, str(e)
def write_offer_to_disk(self, file_path: Path, offer: TradeRecord):
if offer is not None:
file_path.write_text(bytes(offer).hex())
async def maybe_create_wallets_for_offer(self, offer: Offer):
async def get_discrepancies_for_offer(self, file_path: Path) -> Tuple[bool, Optional[Dict], Optional[Exception]]:
self.log.info(f"trade offer: {file_path}")
trade_offer_hex = file_path.read_text()
trade_offer = TradeRecord.from_bytes(bytes.fromhex(trade_offer_hex))
return get_discrepancies_for_spend_bundle(trade_offer.spend_bundle)
async def get_inner_puzzle_for_puzzle_hash(self, puzzle_hash) -> Program:
info = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(puzzle_hash)
assert info is not None
puzzle = self.wallet_state_manager.main_wallet.puzzle_for_pk(bytes(info.pubkey))
return puzzle
async def maybe_create_wallets_for_offer(self, file_path: Path) -> bool:
success, result, error = await self.get_discrepancies_for_offer(file_path)
if not success or result is None:
return False
for key, value in result.items():
for key in offer.arbitrage():
wsm = self.wallet_state_manager
wallet: Wallet = wsm.main_wallet
if key == "chia":
continue
self.log.info(f"value is {key}")
exists = await wsm.get_wallet_for_colour(key)
if exists is not None:
if key is None:
continue
exists: Optional[Wallet] = await wsm.get_wallet_for_asset_id(key.hex())
if exists is None:
self.log.info(f"Creating wallet for asset ID: {key}")
await CATWallet.create_wallet_for_cat(wsm, wallet, key.hex())
await CCWallet.create_wallet_for_cc(wsm, wallet, key)
return True
async def respond_to_offer(self, file_path: Path) -> Tuple[bool, Optional[TradeRecord], Optional[str]]:
has_wallets = await self.maybe_create_wallets_for_offer(file_path)
if not has_wallets:
return False, None, "Unknown Error"
trade_offer = None
try:
trade_offer_hex = file_path.read_text()
trade_offer = TradeRecord.from_bytes(hexstr_to_bytes(trade_offer_hex))
except Exception as e:
return False, None, f"Error: {e}"
if trade_offer is not None:
offer_spend_bundle: SpendBundle = trade_offer.spend_bundle
coinsols: List[CoinSpend] = [] # [] of CoinSpends
cc_coinsol_outamounts: Dict[str, List[Tuple[CoinSpend, int]]] = dict()
aggsig = offer_spend_bundle.aggregated_signature
cc_discrepancies: Dict[str, int] = dict()
chia_discrepancy = None
wallets: Dict[str, Any] = dict() # colour to wallet dict
for coinsol in offer_spend_bundle.coin_spends:
puzzle: Program = Program.from_bytes(bytes(coinsol.puzzle_reveal))
solution: Program = Program.from_bytes(bytes(coinsol.solution))
# work out the deficits between coin amount and expected output for each
r = cc_utils.uncurry_cc(puzzle)
if r:
# Calculate output amounts
mod_hash, genesis_checker, inner_puzzle = r
colour = bytes(genesis_checker).hex()
if colour not in wallets:
wallets[colour] = await self.wallet_state_manager.get_wallet_for_colour(colour)
unspent = await self.wallet_state_manager.get_spendable_coins_for_wallet(wallets[colour].id())
if coinsol.coin in [record.coin for record in unspent]:
return False, None, "can't respond to own offer"
innersol = solution.first()
total = get_output_amount_for_puzzle_and_solution(inner_puzzle, innersol)
if colour in cc_discrepancies:
cc_discrepancies[colour] += coinsol.coin.amount - total
else:
cc_discrepancies[colour] = coinsol.coin.amount - total
# Store coinsol and output amount for later
if colour in cc_coinsol_outamounts:
cc_coinsol_outamounts[colour].append((coinsol, total))
else:
cc_coinsol_outamounts[colour] = [(coinsol, total)]
else:
# standard chia coin
unspent = await self.wallet_state_manager.get_spendable_coins_for_wallet(1)
if coinsol.coin in [record.coin for record in unspent]:
return False, None, "can't respond to own offer"
if chia_discrepancy is None:
chia_discrepancy = get_output_discrepancy_for_puzzle_and_solution(coinsol.coin, puzzle, solution)
else:
chia_discrepancy += get_output_discrepancy_for_puzzle_and_solution(coinsol.coin, puzzle, solution)
coinsols.append(coinsol)
chia_spend_bundle: Optional[SpendBundle] = None
if chia_discrepancy is not None:
chia_spend_bundle = await self.wallet_state_manager.main_wallet.create_spend_bundle_relative_chia(
chia_discrepancy, []
)
if chia_spend_bundle is not None:
for coinsol in coinsols:
chia_spend_bundle.coin_spends.append(coinsol)
zero_spend_list: List[SpendBundle] = []
spend_bundle = None
# create coloured coin
self.log.info(cc_discrepancies)
for colour in cc_discrepancies.keys():
if cc_discrepancies[colour] < 0:
my_cc_spends = await wallets[colour].select_coins(abs(cc_discrepancies[colour]))
else:
if chia_spend_bundle is None:
to_exclude: List = []
else:
to_exclude = chia_spend_bundle.removals()
my_cc_spends = await wallets[colour].select_coins(0)
if my_cc_spends is None or my_cc_spends == set():
zero_spend_bundle: SpendBundle = await wallets[colour].generate_zero_val_coin(False, to_exclude)
if zero_spend_bundle is None:
return (
False,
None,
"Unable to generate zero value coin. Confirm that you have chia available",
)
zero_spend_list.append(zero_spend_bundle)
additions = zero_spend_bundle.additions()
removals = zero_spend_bundle.removals()
my_cc_spends = set()
for add in additions:
if add not in removals and add.amount == 0:
my_cc_spends.add(add)
if my_cc_spends == set() or my_cc_spends is None:
return False, None, "insufficient funds"
# Create SpendableCC list and innersol_list with both my coins and the offered coins
# Firstly get the output coin
my_output_coin = my_cc_spends.pop()
spendable_cc_list = []
innersol_list = []
genesis_id = genesis_coin_id_for_genesis_coin_checker(Program.from_bytes(bytes.fromhex(colour)))
# Make the rest of the coins assert the output coin is consumed
for coloured_coin in my_cc_spends:
inner_solution = self.wallet_state_manager.main_wallet.make_solution(consumed=[my_output_coin.name()])
inner_puzzle = await self.get_inner_puzzle_for_puzzle_hash(coloured_coin.puzzle_hash)
assert inner_puzzle is not None
sigs = await wallets[colour].get_sigs(inner_puzzle, inner_solution, coloured_coin.name())
sigs.append(aggsig)
aggsig = AugSchemeMPL.aggregate(sigs)
lineage_proof = await wallets[colour].get_lineage_proof_for_coin(coloured_coin)
# TODO: address hint error and remove ignore
# error: Argument 2 to "SpendableCC" has incompatible type "Optional[bytes32]"; expected "bytes32"
# [arg-type]
spendable_cc_list.append(SpendableCC(coloured_coin, genesis_id, inner_puzzle, lineage_proof)) # type: ignore[arg-type] # noqa: E501
innersol_list.append(inner_solution)
# Create SpendableCC for each of the coloured coins received
for cc_coinsol_out in cc_coinsol_outamounts[colour]:
cc_coinsol = cc_coinsol_out[0]
puzzle = Program.from_bytes(bytes(cc_coinsol.puzzle_reveal))
solution = Program.from_bytes(bytes(cc_coinsol.solution))
r = uncurry_cc(puzzle)
if r:
mod_hash, genesis_coin_checker, inner_puzzle = r
inner_solution = solution.first()
lineage_proof = solution.rest().rest().first()
# TODO: address hint error and remove ignore
# error: Argument 2 to "SpendableCC" has incompatible type "Optional[bytes32]"; expected
# "bytes32" [arg-type]
spendable_cc_list.append(SpendableCC(cc_coinsol.coin, genesis_id, inner_puzzle, lineage_proof)) # type: ignore[arg-type] # noqa: E501
innersol_list.append(inner_solution)
# Finish the output coin SpendableCC with new information
newinnerpuzhash = await wallets[colour].get_new_inner_hash()
outputamount = sum([c.amount for c in my_cc_spends]) + cc_discrepancies[colour] + my_output_coin.amount
inner_solution = self.wallet_state_manager.main_wallet.make_solution(
primaries=[{"puzzlehash": newinnerpuzhash, "amount": outputamount}]
)
inner_puzzle = await self.get_inner_puzzle_for_puzzle_hash(my_output_coin.puzzle_hash)
assert inner_puzzle is not None
lineage_proof = await wallets[colour].get_lineage_proof_for_coin(my_output_coin)
# TODO: address hint error and remove ignore
# error: Argument 2 to "SpendableCC" has incompatible type "Optional[bytes32]"; expected "bytes32"
# [arg-type]
spendable_cc_list.append(SpendableCC(my_output_coin, genesis_id, inner_puzzle, lineage_proof)) # type: ignore[arg-type] # noqa: E501
innersol_list.append(inner_solution)
sigs = await wallets[colour].get_sigs(inner_puzzle, inner_solution, my_output_coin.name())
sigs.append(aggsig)
aggsig = AugSchemeMPL.aggregate(sigs)
if spend_bundle is None:
spend_bundle = spend_bundle_for_spendable_ccs(
CC_MOD,
Program.from_bytes(bytes.fromhex(colour)),
spendable_cc_list,
innersol_list,
[aggsig],
)
else:
new_spend_bundle = spend_bundle_for_spendable_ccs(
CC_MOD,
Program.from_bytes(bytes.fromhex(colour)),
spendable_cc_list,
innersol_list,
[aggsig],
)
spend_bundle = SpendBundle.aggregate([spend_bundle, new_spend_bundle])
# reset sigs and aggsig so that they aren't included next time around
sigs = []
aggsig = AugSchemeMPL.aggregate(sigs)
my_tx_records = []
if zero_spend_list is not None and spend_bundle is not None:
zero_spend_list.append(spend_bundle)
spend_bundle = SpendBundle.aggregate(zero_spend_list)
if spend_bundle is None:
return False, None, "spend_bundle missing"
# Add transaction history for this trade
now = uint64(int(time.time()))
if chia_spend_bundle is not None:
spend_bundle = SpendBundle.aggregate([spend_bundle, chia_spend_bundle])
if chia_discrepancy < 0:
# TODO: address hint error and remove ignore
# error: Argument "to_puzzle_hash" to "TransactionRecord" has incompatible type "bytes"; expected
# "bytes32" [arg-type]
tx_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=now,
to_puzzle_hash=token_bytes(), # type: ignore[arg-type]
amount=uint64(abs(chia_discrepancy)),
fee_amount=uint64(0),
confirmed=False,
sent=uint32(10),
spend_bundle=chia_spend_bundle,
additions=chia_spend_bundle.additions(),
removals=chia_spend_bundle.removals(),
wallet_id=uint32(1),
sent_to=[],
trade_id=std_hash(spend_bundle.name() + bytes(now)),
type=uint32(TransactionType.OUTGOING_TRADE.value),
name=chia_spend_bundle.name(),
)
else:
# TODO: address hint error and remove ignore
# error: Argument "to_puzzle_hash" to "TransactionRecord" has incompatible type "bytes"; expected
# "bytes32" [arg-type]
tx_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=token_bytes(), # type: ignore[arg-type]
amount=uint64(abs(chia_discrepancy)),
fee_amount=uint64(0),
confirmed=False,
sent=uint32(10),
spend_bundle=chia_spend_bundle,
additions=chia_spend_bundle.additions(),
removals=chia_spend_bundle.removals(),
wallet_id=uint32(1),
sent_to=[],
trade_id=std_hash(spend_bundle.name() + bytes(now)),
type=uint32(TransactionType.INCOMING_TRADE.value),
name=chia_spend_bundle.name(),
)
my_tx_records.append(tx_record)
for colour, amount in cc_discrepancies.items():
wallet = wallets[colour]
if chia_discrepancy > 0:
# TODO: address hint error and remove ignore
# error: Argument "to_puzzle_hash" to "TransactionRecord" has incompatible type "bytes"; expected
# "bytes32" [arg-type]
tx_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=token_bytes(), # type: ignore[arg-type]
amount=uint64(abs(amount)),
fee_amount=uint64(0),
confirmed=False,
sent=uint32(10),
spend_bundle=spend_bundle,
additions=spend_bundle.additions(),
removals=spend_bundle.removals(),
wallet_id=wallet.id(),
sent_to=[],
trade_id=std_hash(spend_bundle.name() + bytes(now)),
type=uint32(TransactionType.OUTGOING_TRADE.value),
name=spend_bundle.name(),
)
else:
# TODO: address hint errors and remove ignores
# error: Argument "to_puzzle_hash" to "TransactionRecord" has incompatible type "bytes"; expected
# "bytes32" [arg-type]
# error: Argument "name" to "TransactionRecord" has incompatible type "bytes"; expected "bytes32"
# [arg-type]
tx_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=token_bytes(), # type: ignore[arg-type]
amount=uint64(abs(amount)),
fee_amount=uint64(0),
confirmed=False,
sent=uint32(10),
spend_bundle=spend_bundle,
additions=spend_bundle.additions(),
removals=spend_bundle.removals(),
wallet_id=wallet.id(),
sent_to=[],
trade_id=std_hash(spend_bundle.name() + bytes(now)),
type=uint32(TransactionType.INCOMING_TRADE.value),
name=token_bytes(), # type: ignore[arg-type]
)
my_tx_records.append(tx_record)
# TODO: address hint error and remove ignore
# error: Argument "to_puzzle_hash" to "TransactionRecord" has incompatible type "bytes"; expected
# "bytes32" [arg-type]
tx_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=token_bytes(), # type: ignore[arg-type]
amount=uint64(0),
fee_amount=uint64(0),
confirmed=False,
sent=uint32(0),
spend_bundle=spend_bundle,
additions=spend_bundle.additions(),
removals=spend_bundle.removals(),
wallet_id=uint32(0),
sent_to=[],
trade_id=std_hash(spend_bundle.name() + bytes(now)),
type=uint32(TransactionType.OUTGOING_TRADE.value),
name=spend_bundle.name(),
async def check_offer_validity(self, offer: Offer) -> bool:
all_removals: List[Coin] = offer.bundle.removals()
all_removal_names: List[bytes32] = [c.name() for c in all_removals]
non_ephemeral_removals: List[Coin] = list(
filter(lambda c: c.parent_coin_info not in all_removal_names, all_removals)
)
coin_states = await self.wallet_state_manager.get_coin_state([c.name() for c in non_ephemeral_removals])
assert coin_states is not None
return not any([cs.spent_height is not None for cs in coin_states])
async def respond_to_offer(self, offer: Offer, fee=uint64(0)) -> Tuple[bool, Optional[TradeRecord], Optional[str]]:
take_offer_dict: Dict[Union[bytes32, int], int] = {}
arbitrage: Dict[Optional[bytes32], int] = offer.arbitrage()
for asset_id, amount in arbitrage.items():
if asset_id is None:
wallet = self.wallet_state_manager.main_wallet
key: Union[bytes32, int] = int(wallet.id())
else:
wallet = await self.wallet_state_manager.get_wallet_for_asset_id(asset_id.hex())
if wallet is None and amount < 0:
return False, None, f"Do not have a CAT of asset ID: {asset_id} to fulfill offer"
elif wallet is None:
key = asset_id
else:
key = int(wallet.id())
take_offer_dict[key] = amount
# First we validate that all of the coins in this offer exist
valid: bool = await self.check_offer_validity(offer)
if not valid:
return False, None, "This offer is no longer valid"
success, take_offer, error = await self._create_offer_for_ids(take_offer_dict, fee=fee)
if not success or take_offer is None:
return False, None, error
complete_offer = Offer.aggregate([offer, take_offer])
assert complete_offer.is_valid()
final_spend_bundle: SpendBundle = complete_offer.to_valid_spend()
await self.maybe_create_wallets_for_offer(complete_offer)
# Now to deal with transaction history before pushing the spend
settlement_coins: List[Coin] = [c for coins in complete_offer.get_offered_coins().values() for c in coins]
settlement_coin_ids: List[bytes32] = [c.name() for c in settlement_coins]
additions: List[Coin] = final_spend_bundle.not_ephemeral_additions()
removals: List[Coin] = final_spend_bundle.removals()
all_fees = uint64(final_spend_bundle.fees())
txs = []
addition_dict: Dict[uint32, List[Coin]] = {}
for addition in additions:
wallet_info = await self.wallet_state_manager.get_wallet_id_for_puzzle_hash(addition.puzzle_hash)
if wallet_info is not None:
wallet_id, _ = wallet_info
if addition.parent_coin_info in settlement_coin_ids:
wallet = self.wallet_state_manager.wallets[wallet_id]
to_puzzle_hash = await wallet.convert_puzzle_hash(addition.puzzle_hash)
txs.append(
TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=to_puzzle_hash,
amount=addition.amount,
fee_amount=uint64(0),
confirmed=False,
sent=uint32(10),
spend_bundle=None,
additions=[addition],
removals=[],
wallet_id=wallet_id,
sent_to=[],
trade_id=complete_offer.name(),
type=uint32(TransactionType.INCOMING_TRADE.value),
name=std_hash(final_spend_bundle.name() + addition.name()),
memos=[],
)
)
else: # This is change
addition_dict.setdefault(wallet_id, [])
addition_dict[wallet_id].append(addition)
# While we want additions to show up as separate records, removals of the same wallet should show as one
removal_dict: Dict[uint32, List[Coin]] = {}
for removal in removals:
wallet_info = await self.wallet_state_manager.get_wallet_id_for_puzzle_hash(removal.puzzle_hash)
if wallet_info is not None:
wallet_id, _ = wallet_info
removal_dict.setdefault(wallet_id, [])
removal_dict[wallet_id].append(removal)
for wid, grouped_removals in removal_dict.items():
wallet = self.wallet_state_manager.wallets[wid]
to_puzzle_hash = bytes32([1] * 32) # We use all zeros to be clear not to send here
removal_tree_hash = Program.to([rem.as_list() for rem in grouped_removals]).get_tree_hash()
# We also need to calculate the sent amount
removed: int = sum(c.amount for c in grouped_removals)
change_coins: List[Coin] = addition_dict[wid] if wid in addition_dict else []
change_amount: int = sum(c.amount for c in change_coins)
sent_amount: int = removed - change_amount
txs.append(
TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=to_puzzle_hash,
amount=uint64(sent_amount),
fee_amount=all_fees,
confirmed=False,
sent=uint32(10),
spend_bundle=None,
additions=change_coins,
removals=grouped_removals,
wallet_id=wallet.id(),
sent_to=[],
trade_id=complete_offer.name(),
type=uint32(TransactionType.OUTGOING_TRADE.value),
name=std_hash(final_spend_bundle.name() + removal_tree_hash),
memos=[],
)
)
now = uint64(int(time.time()))
trade_record: TradeRecord = TradeRecord(
confirmed_at_index=uint32(0),
accepted_at_time=now,
created_at_time=now,
my_offer=False,
accepted_at_time=uint64(int(time.time())),
created_at_time=uint64(int(time.time())),
is_my_offer=False,
sent=uint32(0),
spend_bundle=offer_spend_bundle,
tx_spend_bundle=spend_bundle,
additions=spend_bundle.additions(),
removals=spend_bundle.removals(),
trade_id=std_hash(spend_bundle.name() + bytes(now)),
offer=bytes(complete_offer),
taken_offer=bytes(offer),
coins_of_interest=complete_offer.get_involved_coins(),
trade_id=complete_offer.name(),
status=uint32(TradeStatus.PENDING_CONFIRM.value),
sent_to=[],
)
await self.save_trade(trade_record)
await self.wallet_state_manager.add_pending_transaction(tx_record)
for tx in my_tx_records:
# Dummy transaction for the sake of the wallet push
push_tx = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=bytes32([1] * 32),
amount=uint64(0),
fee_amount=uint64(0),
confirmed=False,
sent=uint32(0),
spend_bundle=final_spend_bundle,
additions=final_spend_bundle.additions(),
removals=final_spend_bundle.removals(),
wallet_id=uint32(0),
sent_to=[],
trade_id=complete_offer.name(),
type=uint32(TransactionType.OUTGOING_TRADE.value),
name=final_spend_bundle.name(),
memos=list(final_spend_bundle.get_memos().items()),
)
await self.wallet_state_manager.add_pending_transaction(push_tx)
for tx in txs:
await self.wallet_state_manager.add_transaction(tx)
return True, trade_record, None

View File

@ -1,11 +1,12 @@
from dataclasses import dataclass
from typing import List, Optional, Tuple
from typing import List, Optional, Tuple, Dict, Any
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.spend_bundle import SpendBundle
from chia.util.ints import uint8, uint32, uint64
from chia.util.streamable import Streamable, streamable
from chia.wallet.trading.offer import Offer
from chia.wallet.trading.trade_status import TradeStatus
@dataclass(frozen=True)
@ -18,12 +19,34 @@ class TradeRecord(Streamable):
confirmed_at_index: uint32
accepted_at_time: Optional[uint64]
created_at_time: uint64
my_offer: bool
is_my_offer: bool
sent: uint32
spend_bundle: SpendBundle # This in not complete spendbundle
tx_spend_bundle: Optional[SpendBundle] # this is full trade
additions: List[Coin]
removals: List[Coin]
offer: bytes
taken_offer: Optional[bytes]
coins_of_interest: List[Coin]
trade_id: bytes32
status: uint32 # TradeStatus, enum not streamable
sent_to: List[Tuple[str, uint8, Optional[str]]]
def to_json_dict_convenience(self) -> Dict[str, Any]:
formatted = self.to_json_dict()
formatted["status"] = TradeStatus(self.status).name
offer_to_summarize: bytes = self.offer if self.taken_offer is None else self.taken_offer
offer = Offer.from_bytes(offer_to_summarize)
offered, requested = offer.summary()
formatted["summary"] = {
"offered": offered,
"requested": requested,
}
formatted["pending"] = offer.get_pending_amounts()
del formatted["offer"]
return formatted
@classmethod
def from_json_dict_convenience(cls, record: Dict[str, Any], offer: str = "") -> "TradeRecord":
new_record = record.copy()
new_record["status"] = TradeStatus[record["status"]].value
del new_record["summary"]
del new_record["pending"]
new_record["offer"] = offer
return cls.from_json_dict(new_record)

View File

@ -0,0 +1,398 @@
from dataclasses import dataclass
from typing import List, Optional, Dict, Set, Tuple
from blspy import G2Element
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
from chia.types.announcement import Announcement
from chia.types.coin_spend import CoinSpend
from chia.types.spend_bundle import SpendBundle
from chia.util.ints import uint64
from chia.wallet.cat_wallet.cat_utils import (
CAT_MOD,
SpendableCAT,
construct_cat_puzzle,
match_cat_puzzle,
unsigned_spend_bundle_for_spendable_cats,
)
from chia.wallet.lineage_proof import LineageProof
from chia.wallet.puzzles.load_clvm import load_clvm
from chia.wallet.payment import Payment
OFFER_MOD = load_clvm("settlement_payments.clvm")
ZERO_32 = bytes32([0] * 32)
@dataclass(frozen=True)
class NotarizedPayment(Payment):
nonce: bytes32 = ZERO_32
@classmethod
def from_condition_and_nonce(cls, condition: Program, nonce: bytes32) -> "NotarizedPayment":
with_opcode: Program = Program.to((51, condition)) # Gotta do this because the super class is expecting it
p = Payment.from_condition(with_opcode)
puzzle_hash, amount, memos = tuple(p.as_condition_args())
return cls(puzzle_hash, amount, memos, nonce)
@dataclass(frozen=True)
class Offer:
requested_payments: Dict[
Optional[bytes32], List[NotarizedPayment]
] # The key is the asset id of the asset being requested
bundle: SpendBundle
@staticmethod
def ph():
return OFFER_MOD.get_tree_hash()
@staticmethod
def notarize_payments(
requested_payments: Dict[Optional[bytes32], List[Payment]], # `None` means you are requesting XCH
coins: List[Coin],
) -> Dict[Optional[bytes32], List[NotarizedPayment]]:
# This sort should be reproducible in CLVM with `>s`
sorted_coins: List[Coin] = sorted(coins, key=Coin.name)
sorted_coin_list: List[List] = [c.as_list() for c in sorted_coins]
nonce: bytes32 = Program.to(sorted_coin_list).get_tree_hash()
notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = {}
for tail_hash, payments in requested_payments.items():
notarized_payments[tail_hash] = []
for p in payments:
puzzle_hash, amount, memos = tuple(p.as_condition_args())
notarized_payments[tail_hash].append(NotarizedPayment(puzzle_hash, amount, memos, nonce))
return notarized_payments
# The announcements returned from this function must be asserted in whatever spend bundle is created by the wallet
@staticmethod
def calculate_announcements(
notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]],
) -> List[Announcement]:
announcements: List[Announcement] = []
for tail, payments in notarized_payments.items():
if tail is not None:
settlement_ph: bytes32 = construct_cat_puzzle(CAT_MOD, tail, OFFER_MOD).get_tree_hash()
else:
settlement_ph = OFFER_MOD.get_tree_hash()
msg: bytes32 = Program.to((payments[0].nonce, [p.as_condition_args() for p in payments])).get_tree_hash()
announcements.append(Announcement(settlement_ph, msg))
return announcements
def __post_init__(self):
# Verify that there is at least something being offered
offered_coins: Dict[bytes32, List[Coin]] = self.get_offered_coins()
if offered_coins == {}:
raise ValueError("Bundle is not offering anything")
# Verify that there are no duplicate payments
for payments in self.requested_payments.values():
payment_programs: List[bytes32] = [p.name() for p in payments]
if len(set(payment_programs)) != len(payment_programs):
raise ValueError("Bundle has duplicate requested payments")
# This method does not get every coin that is being offered, only the `settlement_payment` children
def get_offered_coins(self) -> Dict[Optional[bytes32], List[Coin]]:
offered_coins: Dict[Optional[bytes32], List[Coin]] = {}
for addition in self.bundle.additions():
# Get the parent puzzle
parent_puzzle: Program = list(
filter(lambda cs: cs.coin.name() == addition.parent_coin_info, self.bundle.coin_spends)
)[0].puzzle_reveal.to_program()
# Determine it's TAIL (or lack of)
matched, curried_args = match_cat_puzzle(parent_puzzle)
tail_hash: Optional[bytes32] = None
if matched:
_, tail_hash_program, _ = curried_args
tail_hash = bytes32(tail_hash_program.as_python())
offer_ph: bytes32 = construct_cat_puzzle(CAT_MOD, tail_hash, OFFER_MOD).get_tree_hash()
else:
tail_hash = None
offer_ph = OFFER_MOD.get_tree_hash()
# Check if the puzzle_hash matches the hypothetical `settlement_payments` puzzle hash
if addition.puzzle_hash == offer_ph:
if tail_hash in offered_coins:
offered_coins[tail_hash].append(addition)
else:
offered_coins[tail_hash] = [addition]
return offered_coins
def get_offered_amounts(self) -> Dict[Optional[bytes32], int]:
offered_coins: Dict[Optional[bytes32], List[Coin]] = self.get_offered_coins()
offered_amounts: Dict[Optional[bytes32], int] = {}
for asset_id, coins in offered_coins.items():
offered_amounts[asset_id] = uint64(sum([c.amount for c in coins]))
return offered_amounts
def get_requested_payments(self) -> Dict[Optional[bytes32], List[NotarizedPayment]]:
return self.requested_payments
def get_requested_amounts(self) -> Dict[Optional[bytes32], int]:
requested_amounts: Dict[Optional[bytes32], int] = {}
for asset_id, coins in self.get_requested_payments().items():
requested_amounts[asset_id] = uint64(sum([c.amount for c in coins]))
return requested_amounts
def arbitrage(self) -> Dict[Optional[bytes32], int]:
offered_amounts: Dict[Optional[bytes32], int] = self.get_offered_amounts()
requested_amounts: Dict[Optional[bytes32], int] = self.get_requested_amounts()
arbitrage_dict: Dict[Optional[bytes32], int] = {}
for asset_id in [*requested_amounts.keys(), *offered_amounts.keys()]:
arbitrage_dict[asset_id] = offered_amounts.get(asset_id, 0) - requested_amounts.get(asset_id, 0)
return arbitrage_dict
# This is a method mostly for the UI that creates a JSON summary of the offer
def summary(self) -> Tuple[Dict[str, int], Dict[str, int]]:
offered_amounts: Dict[Optional[bytes32], int] = self.get_offered_amounts()
requested_amounts: Dict[Optional[bytes32], int] = self.get_requested_amounts()
def keys_to_strings(dic: Dict[Optional[bytes32], int]) -> Dict[str, int]:
new_dic: Dict[str, int] = {}
for key in dic:
if key is None:
new_dic["xch"] = dic[key]
else:
new_dic[key.hex()] = dic[key]
return new_dic
return keys_to_strings(offered_amounts), keys_to_strings(requested_amounts)
# Also mostly for the UI, returns a dictionary of assets and how much of them is pended for this offer
# This method is also imperfect for sufficiently complex spends
def get_pending_amounts(self) -> Dict[str, int]:
all_additions: List[Coin] = self.bundle.additions()
all_removals: List[Coin] = self.bundle.removals()
non_ephemeral_removals: List[Coin] = list(filter(lambda c: c not in all_additions, all_removals))
pending_dict: Dict[str, int] = {}
# First we add up the amounts of all coins that share an ancestor with the offered coins (i.e. a primary coin)
for asset_id, coins in self.get_offered_coins().items():
name = "xch" if asset_id is None else asset_id.hex()
pending_dict[name] = 0
for coin in coins:
root_removal: Coin = self.get_root_removal(coin)
for addition in filter(lambda c: c.parent_coin_info == root_removal.name(), all_additions):
pending_dict[name] += addition.amount
# Then we add a potential fee as pending XCH
fee: int = sum(c.amount for c in all_removals) - sum(c.amount for c in all_additions)
if fee > 0:
pending_dict.setdefault("xch", 0)
pending_dict["xch"] += fee
# Then we gather anything else as unknown
sum_of_additions_so_far: int = sum(pending_dict.values())
unknown: int = sum([c.amount for c in non_ephemeral_removals]) - sum_of_additions_so_far
if unknown > 0:
pending_dict["unknown"] = unknown
return pending_dict
# This method returns all of the coins that are being used in the offer (without which it would be invalid)
def get_involved_coins(self) -> List[Coin]:
additions = self.bundle.additions()
return list(filter(lambda c: c not in additions, self.bundle.removals()))
# This returns the non-ephemeral removal that is an ancestor of the specified coin
# This should maybe move to the SpendBundle object at some point
def get_root_removal(self, coin: Coin) -> Coin:
all_removals: Set[Coin] = set(self.bundle.removals())
all_removal_ids: Set[bytes32] = {c.name() for c in all_removals}
non_ephemeral_removals: Set[Coin] = {
c for c in all_removals if c.parent_coin_info not in {r.name() for r in all_removals}
}
if coin.name() not in all_removal_ids and coin.parent_coin_info not in all_removal_ids:
raise ValueError("The specified coin is not a coin in this bundle")
while coin not in non_ephemeral_removals:
coin = next(c for c in all_removals if c.name() == coin.parent_coin_info)
return coin
# This will only return coins that are ancestors of settlement payments
def get_primary_coins(self) -> List[Coin]:
primary_coins: Set[Coin] = set()
for _, coins in self.get_offered_coins().items():
for coin in coins:
primary_coins.add(self.get_root_removal(coin))
return list(primary_coins)
@classmethod
def aggregate(cls, offers: List["Offer"]) -> "Offer":
total_requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = {}
total_bundle = SpendBundle([], G2Element())
for offer in offers:
# First check for any overlap in inputs
total_inputs: Set[Coin] = {cs.coin for cs in total_bundle.coin_spends}
offer_inputs: Set[Coin] = {cs.coin for cs in offer.bundle.coin_spends}
if total_inputs & offer_inputs:
raise ValueError("The aggregated offers overlap inputs")
# Next, do the aggregation
for tail, payments in offer.requested_payments.items():
if tail in total_requested_payments:
total_requested_payments[tail].extend(payments)
else:
total_requested_payments[tail] = payments
total_bundle = SpendBundle.aggregate([total_bundle, offer.bundle])
return cls(total_requested_payments, total_bundle)
# Validity is defined by having enough funds within the offer to satisfy both sides
def is_valid(self) -> bool:
return all([value >= 0 for value in self.arbitrage().values()])
# A "valid" spend means that this bundle can be pushed to the network and will succeed
# This differs from the `to_spend_bundle` method which deliberately creates an invalid SpendBundle
def to_valid_spend(self, arbitrage_ph: Optional[bytes32] = None) -> SpendBundle:
if not self.is_valid():
raise ValueError("Offer is currently incomplete")
completion_spends: List[CoinSpend] = []
for tail_hash, payments in self.requested_payments.items():
offered_coins: List[Coin] = self.get_offered_coins()[tail_hash]
# Because of CAT supply laws, we must specify a place for the leftovers to go
arbitrage_amount: int = self.arbitrage()[tail_hash]
all_payments: List[NotarizedPayment] = payments.copy()
if arbitrage_amount > 0:
assert arbitrage_amount is not None
assert arbitrage_ph is not None
all_payments.append(NotarizedPayment(arbitrage_ph, uint64(arbitrage_amount), []))
for coin in offered_coins:
inner_solutions = []
if coin == offered_coins[0]:
nonces: List[bytes32] = [p.nonce for p in all_payments]
for nonce in list(dict.fromkeys(nonces)): # dedup without messing with order
nonce_payments: List[NotarizedPayment] = list(filter(lambda p: p.nonce == nonce, all_payments))
inner_solutions.append((nonce, [np.as_condition_args() for np in nonce_payments]))
if tail_hash:
# CATs have a special way to be solved so we have to do some calculation before getting the solution
parent_spend: CoinSpend = list(
filter(lambda cs: cs.coin.name() == coin.parent_coin_info, self.bundle.coin_spends)
)[0]
parent_coin: Coin = parent_spend.coin
matched, curried_args = match_cat_puzzle(parent_spend.puzzle_reveal.to_program())
assert matched
_, _, inner_puzzle = curried_args
spendable_cat = SpendableCAT(
coin,
tail_hash,
OFFER_MOD,
Program.to(inner_solutions),
lineage_proof=LineageProof(
parent_coin.parent_coin_info, inner_puzzle.get_tree_hash(), parent_coin.amount
),
)
solution: Program = (
unsigned_spend_bundle_for_spendable_cats(CAT_MOD, [spendable_cat])
.coin_spends[0]
.solution.to_program()
)
else:
solution = Program.to(inner_solutions)
completion_spends.append(
CoinSpend(
coin,
construct_cat_puzzle(CAT_MOD, tail_hash, OFFER_MOD) if tail_hash else OFFER_MOD,
solution,
)
)
return SpendBundle.aggregate([SpendBundle(completion_spends, G2Element()), self.bundle])
def to_spend_bundle(self) -> SpendBundle:
# Before we serialze this as a SpendBundle, we need to serialze the `requested_payments` as dummy CoinSpends
additional_coin_spends: List[CoinSpend] = []
for tail_hash, payments in self.requested_payments.items():
puzzle_reveal: Program = construct_cat_puzzle(CAT_MOD, tail_hash, OFFER_MOD) if tail_hash else OFFER_MOD
inner_solutions = []
nonces: List[bytes32] = [p.nonce for p in payments]
for nonce in list(dict.fromkeys(nonces)): # dedup without messing with order
nonce_payments: List[NotarizedPayment] = list(filter(lambda p: p.nonce == nonce, payments))
inner_solutions.append((nonce, [np.as_condition_args() for np in nonce_payments]))
additional_coin_spends.append(
CoinSpend(
Coin(
ZERO_32,
puzzle_reveal.get_tree_hash(),
uint64(0),
),
puzzle_reveal,
Program.to(inner_solutions),
)
)
return SpendBundle.aggregate(
[
SpendBundle(additional_coin_spends, G2Element()),
self.bundle,
]
)
@classmethod
def from_spend_bundle(cls, bundle: SpendBundle) -> "Offer":
# Because of the `to_spend_bundle` method, we need to parse the dummy CoinSpends as `requested_payments`
requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = {}
leftover_coin_spends: List[CoinSpend] = []
for coin_spend in bundle.coin_spends:
if coin_spend.coin.parent_coin_info == ZERO_32:
matched, curried_args = match_cat_puzzle(coin_spend.puzzle_reveal.to_program())
if matched:
_, tail_hash_program, _ = curried_args
tail_hash: Optional[bytes32] = bytes32(tail_hash_program.as_python())
else:
tail_hash = None
notarized_payments: List[NotarizedPayment] = []
for payment_group in coin_spend.solution.to_program().as_iter():
nonce = bytes32(payment_group.first().as_python())
payment_args_list: List[Program] = payment_group.rest().as_iter()
notarized_payments.extend(
[NotarizedPayment.from_condition_and_nonce(condition, nonce) for condition in payment_args_list]
)
requested_payments[tail_hash] = notarized_payments
else:
leftover_coin_spends.append(coin_spend)
return cls(requested_payments, SpendBundle(leftover_coin_spends, bundle.aggregated_signature))
def name(self) -> bytes32:
return self.to_spend_bundle().name()
# Methods to make this a valid Streamable member
# We basically hijack the SpendBundle versions for most of it
@classmethod
def parse(cls, f) -> "Offer":
parsed_bundle = SpendBundle.parse(f)
return cls.from_bytes(bytes(parsed_bundle))
def stream(self, f):
as_spend_bundle = SpendBundle.from_bytes(bytes(self))
as_spend_bundle.stream(f)
def __bytes__(self) -> bytes:
return bytes(self.to_spend_bundle())
@classmethod
def from_bytes(cls, as_bytes: bytes) -> "Offer":
# Because of the __bytes__ method, we need to parse the dummy CoinSpends as `requested_payments`
bundle = SpendBundle.from_bytes(as_bytes)
return cls.from_spend_bundle(bundle)

View File

@ -5,6 +5,6 @@ class TradeStatus(Enum):
PENDING_ACCEPT = 0
PENDING_CONFIRM = 1
PENDING_CANCEL = 2
CANCELED = 3
CANCELLED = 3
CONFIRMED = 4
FAILED = 5

View File

@ -1,4 +1,5 @@
from typing import List, Optional
from operator import attrgetter
import aiosqlite
@ -91,12 +92,11 @@ class TradeStore:
confirmed_at_index=confirmed_at_index,
accepted_at_time=current.accepted_at_time,
created_at_time=current.created_at_time,
my_offer=current.my_offer,
is_my_offer=current.is_my_offer,
sent=current.sent,
spend_bundle=current.spend_bundle,
tx_spend_bundle=current.tx_spend_bundle,
additions=current.additions,
removals=current.removals,
offer=current.offer,
taken_offer=current.taken_offer,
coins_of_interest=current.coins_of_interest,
trade_id=current.trade_id,
status=uint32(status.value),
sent_to=current.sent_to,
@ -133,12 +133,11 @@ class TradeStore:
confirmed_at_index=current.confirmed_at_index,
accepted_at_time=current.accepted_at_time,
created_at_time=current.created_at_time,
my_offer=current.my_offer,
is_my_offer=current.is_my_offer,
sent=uint32(current.sent + 1),
spend_bundle=current.spend_bundle,
tx_spend_bundle=current.tx_spend_bundle,
additions=current.additions,
removals=current.removals,
offer=current.offer,
taken_offer=current.taken_offer,
coins_of_interest=current.coins_of_interest,
trade_id=current.trade_id,
status=current.status,
sent_to=sent_to,
@ -160,12 +159,11 @@ class TradeStore:
confirmed_at_index=current.confirmed_at_index,
accepted_at_time=current.accepted_at_time,
created_at_time=current.created_at_time,
my_offer=current.my_offer,
is_my_offer=current.is_my_offer,
sent=uint32(0),
spend_bundle=current.spend_bundle,
tx_spend_bundle=current.tx_spend_bundle,
additions=current.additions,
removals=current.removals,
offer=current.offer,
taken_offer=current.taken_offer,
coins_of_interest=current.coins_of_interest,
trade_id=current.trade_id,
status=uint32(TradeStatus.PENDING_CONFIRM.value),
sent_to=[],
@ -252,6 +250,40 @@ class TradeStore:
return records
async def get_trades_between(
self, start: int, end: int, sort_key: Optional[str] = None, reverse: bool = False
) -> List[TradeRecord]:
"""
Return a list of trades sorted by a key and between a start and end index.
"""
records = await self.get_all_trades()
# Sort
records = sorted(records, key=attrgetter("trade_id")) # For determinism
if sort_key is None or sort_key == "CONFIRMED_AT_HEIGHT":
records = sorted(records, key=attrgetter("confirmed_at_index"), reverse=(not reverse))
elif sort_key == "RELEVANCE":
sorted_records = sorted(records, key=attrgetter("created_at_time"), reverse=(not reverse))
sorted_records = sorted(sorted_records, key=attrgetter("confirmed_at_index"), reverse=(not reverse))
# custom sort of the statuses here
records = []
statuses = ["PENDING", "CONFIRMED", "CANCELLED", "FAILED"]
if reverse:
statuses.reverse()
statuses.append("") # This is a catch all for any statuses we have not explicitly designated
for status in statuses:
for record in sorted_records:
if status in TradeStatus(record.status).name and record not in records:
records.append(record)
else:
raise ValueError(f"No known sort {sort_key}")
# Paginate
if start > len(records) - 1:
return []
else:
return records[max(start, 0) : min(end, len(records))]
async def get_trades_above(self, height: uint32) -> List[TradeRecord]:
cursor = await self.db_connection.execute("SELECT * from trade_records WHERE confirmed_at_index>?", (height,))
rows = await cursor.fetchall()

View File

@ -1,11 +1,12 @@
from dataclasses import dataclass
from typing import List, Optional, Tuple
from typing import List, Optional, Tuple, Dict
from chia.consensus.coinbase import pool_parent_id, farmer_parent_id
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.mempool_inclusion_status import MempoolInclusionStatus
from chia.types.spend_bundle import SpendBundle
from chia.util.bech32m import encode_puzzle_hash, decode_puzzle_hash
from chia.util.ints import uint8, uint32, uint64
from chia.util.streamable import Streamable, streamable
from chia.wallet.util.transaction_type import TransactionType
@ -36,6 +37,7 @@ class TransactionRecord(Streamable):
trade_id: Optional[bytes32]
type: uint32 # TransactionType
name: bytes32
memos: List[Tuple[bytes32, List[bytes]]]
def is_in_mempool(self) -> bool:
# If one of the nodes we sent it to responded with success, we set it to success
@ -59,3 +61,39 @@ class TransactionRecord(Streamable):
if farmer_parent == self.additions[0].parent_coin_info:
return uint32(block_index)
return None
def get_memos(self) -> Dict[bytes32, List[bytes]]:
return {coin_id: ms for coin_id, ms in self.memos}
@classmethod
def from_json_dict_convenience(cls, modified_tx_input: Dict):
modified_tx = modified_tx_input.copy()
if "to_address" in modified_tx:
modified_tx["to_puzzle_hash"] = decode_puzzle_hash(modified_tx["to_address"]).hex()
if "to_address" in modified_tx:
del modified_tx["to_address"]
# Converts memos from a flat dict into a nested list
memos_dict: Dict[str, List[str]] = {}
memos_list: List = []
if "memos" in modified_tx:
for coin_id, memo in modified_tx["memos"].items():
if coin_id not in memos_dict:
memos_dict[coin_id] = []
memos_dict[coin_id].append(memo)
for coin_id, memos in memos_dict.items():
memos_list.append((coin_id, memos))
modified_tx["memos"] = memos_list
return cls.from_json_dict(modified_tx)
def to_json_dict_convenience(self, config: Dict) -> Dict:
selected = config["selected_network"]
prefix = config["network_overrides"]["config"][selected]["address_prefix"]
formatted = self.to_json_dict()
formatted["to_address"] = encode_puzzle_hash(self.to_puzzle_hash, prefix)
formatted["memos"] = {
coin_id.hex(): memo.hex()
for coin_id, memos in self.get_memos().items()
for memo in memos
if memo is not None
}
return formatted

View File

@ -1,4 +1,4 @@
from typing import Iterable, List, Tuple
from typing import List
from blspy import AugSchemeMPL, G1Element
from clvm import KEYWORD_FROM_ATOM
@ -6,7 +6,6 @@ from clvm_tools.binutils import disassemble as bu_disassemble
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program, INFINITE_COST
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.consensus.default_constants import DEFAULT_CONSTANTS
from chia.types.condition_opcodes import ConditionOpcode
from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict
@ -76,8 +75,8 @@ def debug_spend_bundle(spend_bundle, agg_sig_additional_data=DEFAULT_CONSTANTS.A
if error:
print(f"*** error {error}")
elif conditions is not None:
for pk, m in pkm_pairs_for_conditions_dict(conditions, coin_name, agg_sig_additional_data):
pks.append(G1Element.from_bytes(pk))
for pk_bytes, m in pkm_pairs_for_conditions_dict(conditions, coin_name, agg_sig_additional_data):
pks.append(G1Element.from_bytes(pk_bytes))
msgs.append(m)
print()
cost, r = puzzle_reveal.run_with_cost(INFINITE_COST, solution) # type: ignore
@ -189,10 +188,3 @@ def debug_spend_bundle(spend_bundle, agg_sig_additional_data=DEFAULT_CONSTANTS.A
print(f" coin_ids: {[msg.hex()[-128:-64] for msg in msgs]}")
print(f" add_data: {[msg.hex()[-64:] for msg in msgs]}")
print(f"signature: {spend_bundle.aggregated_signature}")
def solution_for_pay_to_any(puzzle_hash_amount_pairs: Iterable[Tuple[bytes32, int]]) -> Program:
output_conditions = [
[ConditionOpcode.CREATE_COIN, puzzle_hash, amount] for puzzle_hash, amount in puzzle_hash_amount_pairs
]
return Program.to(output_conditions)

View File

@ -1,93 +0,0 @@
from typing import Any, Dict, Optional, Tuple
from chia.types.blockchain_format.program import Program, INFINITE_COST
from chia.types.condition_opcodes import ConditionOpcode
from chia.types.spend_bundle import SpendBundle
from chia.util.condition_tools import conditions_dict_for_solution
from chia.wallet.cc_wallet import cc_utils
from chia.wallet.trade_record import TradeRecord
from chia.wallet.trading.trade_status import TradeStatus
def trade_status_ui_string(status: TradeStatus):
if status is TradeStatus.PENDING_CONFIRM:
return "Pending Confirmation"
elif status is TradeStatus.CANCELED:
return "Canceled"
elif status is TradeStatus.CONFIRMED:
return "Confirmed"
elif status is TradeStatus.PENDING_CANCEL:
return "Pending Cancellation"
elif status is TradeStatus.FAILED:
return "Failed"
elif status is TradeStatus.PENDING_ACCEPT:
return "Pending"
def trade_record_to_dict(record: TradeRecord) -> Dict[str, Any]:
"""Convenience function to return only part of trade record we care about and show correct status to the ui"""
result: Dict[str, Any] = {}
result["trade_id"] = record.trade_id.hex()
result["sent"] = record.sent
result["my_offer"] = record.my_offer
result["created_at_time"] = record.created_at_time
result["accepted_at_time"] = record.accepted_at_time
result["confirmed_at_index"] = record.confirmed_at_index
result["status"] = trade_status_ui_string(TradeStatus(record.status))
success, offer_dict, error = get_discrepancies_for_spend_bundle(record.spend_bundle)
if success is False or offer_dict is None:
raise ValueError(error)
result["offer_dict"] = offer_dict
return result
# Returns the relative difference in value between the amount outputted by a puzzle and solution and a coin's amount
def get_output_discrepancy_for_puzzle_and_solution(coin, puzzle, solution):
discrepancy = coin.amount - get_output_amount_for_puzzle_and_solution(puzzle, solution)
return discrepancy
# Returns the amount of value outputted by a puzzle and solution
def get_output_amount_for_puzzle_and_solution(puzzle: Program, solution: Program) -> int:
error, conditions, cost = conditions_dict_for_solution(puzzle, solution, INFINITE_COST)
total = 0
if conditions:
for _ in conditions.get(ConditionOpcode.CREATE_COIN, []):
total += Program.to(_.vars[1]).as_int()
return total
def get_discrepancies_for_spend_bundle(
trade_offer: SpendBundle,
) -> Tuple[bool, Optional[Dict], Optional[Exception]]:
try:
cc_discrepancies: Dict[str, int] = dict()
for coinsol in trade_offer.coin_spends:
puzzle: Program = Program.from_bytes(bytes(coinsol.puzzle_reveal))
solution: Program = Program.from_bytes(bytes(coinsol.solution))
# work out the deficits between coin amount and expected output for each
r = cc_utils.uncurry_cc(puzzle)
if r:
# Calculate output amounts
mod_hash, genesis_checker, inner_puzzle = r
innersol = solution.first()
total = get_output_amount_for_puzzle_and_solution(inner_puzzle, innersol)
colour = bytes(genesis_checker).hex()
if colour in cc_discrepancies:
cc_discrepancies[colour] += coinsol.coin.amount - total
else:
cc_discrepancies[colour] = coinsol.coin.amount - total
else:
coin_amount = coinsol.coin.amount
out_amount = get_output_amount_for_puzzle_and_solution(puzzle, solution)
diff = coin_amount - out_amount
if "chia" in cc_discrepancies:
cc_discrepancies["chia"] = cc_discrepancies["chia"] + diff
else:
cc_discrepancies["chia"] = diff
return True, cc_discrepancies, None
except Exception as e:
return False, None, e

View File

@ -0,0 +1,208 @@
from typing import List, Optional, Tuple, Union, Dict
from chia.consensus.constants import ConsensusConstants
from chia.protocols.wallet_protocol import (
RequestAdditions,
RespondAdditions,
RejectAdditionsRequest,
RejectRemovalsRequest,
RespondRemovals,
RequestRemovals,
)
from chia.types.blockchain_format.coin import hash_coin_list, Coin
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.full_block import FullBlock
from chia.util.merkle_set import confirm_not_included_already_hashed, confirm_included_already_hashed, MerkleSet
def validate_additions(
coins: List[Tuple[bytes32, List[Coin]]],
proofs: Optional[List[Tuple[bytes32, bytes, Optional[bytes]]]],
root,
):
if proofs is None:
# Verify root
additions_merkle_set = MerkleSet()
# Addition Merkle set contains puzzlehash and hash of all coins with that puzzlehash
for puzzle_hash, coins_l in coins:
additions_merkle_set.add_already_hashed(puzzle_hash)
additions_merkle_set.add_already_hashed(hash_coin_list(coins_l))
additions_root = additions_merkle_set.get_root()
if root != additions_root:
return False
else:
for i in range(len(coins)):
assert coins[i][0] == proofs[i][0]
coin_list_1: List[Coin] = coins[i][1]
puzzle_hash_proof: Optional[bytes] = proofs[i][1]
coin_list_proof: Optional[bytes] = proofs[i][2]
if len(coin_list_1) == 0:
# Verify exclusion proof for puzzle hash
assert puzzle_hash_proof is not None
not_included = confirm_not_included_already_hashed(
root,
coins[i][0],
puzzle_hash_proof,
)
if not_included is False:
return False
else:
try:
# Verify inclusion proof for coin list
assert coin_list_proof is not None
included = confirm_included_already_hashed(
root,
hash_coin_list(coin_list_1),
coin_list_proof,
)
if included is False:
return False
except AssertionError:
return False
try:
# Verify inclusion proof for puzzle hash
assert puzzle_hash_proof is not None
included = confirm_included_already_hashed(
root,
coins[i][0],
puzzle_hash_proof,
)
if included is False:
return False
except AssertionError:
return False
return True
def validate_removals(coins, proofs, root):
if proofs is None:
# If there are no proofs, it means all removals were returned in the response.
# we must find the ones relevant to our wallets.
# Verify removals root
removals_merkle_set = MerkleSet()
for name_coin in coins:
name, coin = name_coin
if coin is not None:
removals_merkle_set.add_already_hashed(coin.name())
removals_root = removals_merkle_set.get_root()
if root != removals_root:
return False
else:
# This means the full node has responded only with the relevant removals
# for our wallet. Each merkle proof must be verified.
if len(coins) != len(proofs):
return False
for i in range(len(coins)):
# Coins are in the same order as proofs
if coins[i][0] != proofs[i][0]:
return False
coin = coins[i][1]
if coin is None:
# Verifies merkle proof of exclusion
not_included = confirm_not_included_already_hashed(
root,
coins[i][0],
proofs[i][1],
)
if not_included is False:
return False
else:
# Verifies merkle proof of inclusion of coin name
if coins[i][0] != coin.name():
return False
included = confirm_included_already_hashed(
root,
coin.name(),
proofs[i][1],
)
if included is False:
return False
return True
async def request_and_validate_removals(peer, height, header_hash, coin_name, removals_root) -> bool:
removals_request = RequestRemovals(height, header_hash, [coin_name])
removals_res: Optional[Union[RespondRemovals, RejectRemovalsRequest]] = await peer.request_removals(
removals_request
)
if removals_res is None or isinstance(removals_res, RejectRemovalsRequest):
return False
return validate_removals(removals_res.coins, removals_res.proofs, removals_root)
async def request_and_validate_additions(peer, height, header_hash, puzzle_hash, additions_root):
additions_request = RequestAdditions(height, header_hash, [puzzle_hash])
additions_res: Optional[Union[RespondAdditions, RejectAdditionsRequest]] = await peer.request_additions(
additions_request
)
if additions_res is None or isinstance(additions_res, RejectAdditionsRequest):
return False
validated = validate_additions(
additions_res.coins,
additions_res.proofs,
additions_root,
)
return validated
def get_block_challenge(
constants: ConsensusConstants,
header_block: FullBlock,
all_blocks: Dict[bytes32, FullBlock],
genesis_block: bool,
overflow: bool,
skip_overflow_last_ss_validation: bool,
) -> Optional[bytes32]:
if len(header_block.finished_sub_slots) > 0:
if overflow:
# New sub-slot with overflow block
if skip_overflow_last_ss_validation:
# In this case, we are missing the final sub-slot bundle (it's not finished yet), however
# There is a whole empty slot before this block is infused
challenge: bytes32 = header_block.finished_sub_slots[-1].challenge_chain.get_hash()
else:
challenge = header_block.finished_sub_slots[
-1
].challenge_chain.challenge_chain_end_of_slot_vdf.challenge
else:
# No overflow, new slot with a new challenge
challenge = header_block.finished_sub_slots[-1].challenge_chain.get_hash()
else:
if genesis_block:
challenge = constants.GENESIS_CHALLENGE
else:
if overflow:
if skip_overflow_last_ss_validation:
# Overflow infusion without the new slot, so get the last challenge
challenges_to_look_for = 1
else:
# Overflow infusion, so get the second to last challenge. skip_overflow_last_ss_validation is False,
# Which means no sub slots are omitted
challenges_to_look_for = 2
else:
challenges_to_look_for = 1
reversed_challenge_hashes: List[bytes32] = []
if header_block.height == 0:
return constants.GENESIS_CHALLENGE
if header_block.prev_header_hash not in all_blocks:
return None
curr: Optional[FullBlock] = all_blocks[header_block.prev_header_hash]
while len(reversed_challenge_hashes) < challenges_to_look_for:
if curr is None:
return None
if len(curr.finished_sub_slots) > 0:
reversed_challenge_hashes += reversed(
[slot.challenge_chain.get_hash() for slot in curr.finished_sub_slots]
)
if curr.height == 0:
return constants.GENESIS_CHALLENGE
curr = all_blocks.get(curr.prev_header_hash, None)
challenge = reversed_challenge_hashes[challenges_to_look_for - 1]
return challenge

View File

@ -1,4 +1,5 @@
from enum import IntEnum
from typing import List
from typing_extensions import TypedDict
@ -14,7 +15,7 @@ class WalletType(IntEnum):
AUTHORIZED_PAYEE = 3
MULTI_SIG = 4
CUSTODY = 5
COLOURED_COIN = 6
CAT = 6
RECOVERABLE = 7
DISTRIBUTED_ID = 8
POOLING_WALLET = 9
@ -23,3 +24,4 @@ class WalletType(IntEnum):
class AmountWithPuzzlehash(TypedDict):
amount: uint64
puzzlehash: bytes32
memos: List[bytes]

View File

@ -37,7 +37,7 @@ from chia.wallet.secret_key_store import SecretKeyStore
from chia.wallet.sign_coin_spends import sign_coin_spends
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.wallet_types import AmountWithPuzzlehash, WalletType
from chia.wallet.util.wallet_types import WalletType, AmountWithPuzzlehash
from chia.wallet.wallet_coin_record import WalletCoinRecord
from chia.wallet.wallet_info import WalletInfo
@ -127,7 +127,7 @@ class Wallet:
for record in unconfirmed_tx:
if not record.is_in_mempool():
self.log.warning(f"Record: {record} not in mempool")
self.log.warning(f"Record: {record} not in mempool, {record.sent_to}")
continue
our_spend = False
for coin in record.removals:
@ -147,6 +147,9 @@ class Wallet:
def puzzle_for_pk(self, pubkey: bytes) -> Program:
return puzzle_for_pk(pubkey)
async def convert_puzzle_hash(self, puzzle_hash: bytes32) -> bytes32:
return puzzle_hash # Looks unimpressive, but it's more complicated in other wallets
async def hack_populate_secret_key_for_puzzle_hash(self, puzzle_hash: bytes32) -> G1Element:
maybe = await self.wallet_state_manager.get_keys(puzzle_hash)
if maybe is None:
@ -195,10 +198,10 @@ class Wallet:
def make_solution(
self,
primaries: Optional[List[AmountWithPuzzlehash]] = None,
primaries: List[AmountWithPuzzlehash],
min_time=0,
me=None,
coin_announcements: Optional[Set[bytes32]] = None,
coin_announcements: Optional[Set[bytes]] = None,
coin_announcements_to_assert: Optional[Set[bytes32]] = None,
puzzle_announcements: Optional[Set[bytes32]] = None,
puzzle_announcements_to_assert: Optional[Set[bytes32]] = None,
@ -206,9 +209,15 @@ class Wallet:
) -> Program:
assert fee >= 0
condition_list = []
if primaries:
if len(primaries) > 0:
for primary in primaries:
condition_list.append(make_create_coin_condition(primary["puzzlehash"], primary["amount"]))
if "memos" in primary:
memos: Optional[List[bytes]] = primary["memos"]
if memos is not None and len(memos) == 0:
memos = None
else:
memos = None
condition_list.append(make_create_coin_condition(primary["puzzlehash"], primary["amount"], memos))
if min_time > 0:
condition_list.append(make_assert_absolute_seconds_exceeds_condition(min_time))
if me:
@ -229,6 +238,11 @@ class Wallet:
condition_list.append(make_assert_puzzle_announcement(announcement_hash))
return solution_for_conditions(condition_list)
def add_condition_to_solution(self, condition: Program, solution: Program) -> Program:
python_program = solution.as_python()
python_program[1].append(condition)
return Program.to(python_program)
async def select_coins(self, amount, exclude: List[Coin] = None) -> Set[Coin]:
"""
Returns a set of coins that can be used for generating a new transaction.
@ -292,15 +306,17 @@ class Wallet:
coins: Set[Coin] = None,
primaries_input: Optional[List[AmountWithPuzzlehash]] = None,
ignore_max_send_amount: bool = False,
announcements_to_consume: Set[Announcement] = None,
coin_announcements_to_consume: Set[Announcement] = None,
puzzle_announcements_to_consume: Set[Announcement] = None,
memos: Optional[List[bytes]] = None,
negative_change_allowed: bool = False,
) -> List[CoinSpend]:
"""
Generates a unsigned transaction in form of List(Puzzle, Solutions)
Note: this must be called under a wallet state manager lock
"""
primaries: Optional[List[AmountWithPuzzlehash]]
if primaries_input is None:
primaries = None
primaries: Optional[List[AmountWithPuzzlehash]] = None
total_amount = amount + fee
else:
primaries = primaries_input.copy()
@ -317,12 +333,24 @@ class Wallet:
if coins is None:
coins = await self.select_coins(total_amount)
assert len(coins) > 0
self.log.info(f"coins is not None {coins}")
spend_value = sum([coin.amount for coin in coins])
change = spend_value - total_amount
if negative_change_allowed:
change = max(0, change)
assert change >= 0
if coin_announcements_to_consume is not None:
coin_announcements_bytes: Optional[Set[bytes32]] = {a.name() for a in coin_announcements_to_consume}
else:
coin_announcements_bytes = None
if puzzle_announcements_to_consume is not None:
puzzle_announcements_bytes: Optional[Set[bytes32]] = {a.name() for a in puzzle_announcements_to_consume}
else:
puzzle_announcements_bytes = None
spends: List[CoinSpend] = []
primary_announcement_hash: Optional[bytes32] = None
@ -331,39 +359,39 @@ class Wallet:
all_primaries_list = [(p["puzzlehash"], p["amount"]) for p in primaries] + [(newpuzzlehash, amount)]
if len(set(all_primaries_list)) != len(all_primaries_list):
raise ValueError("Cannot create two identical coins")
if memos is None:
memos = []
assert memos is not None
for coin in coins:
self.log.info(f"coin from coins {coin}")
self.log.info(f"coin from coins: {coin.name()} {coin}")
puzzle: Program = await self.puzzle_for_puzzle_hash(coin.puzzle_hash)
# Only one coin creates outputs
if primary_announcement_hash is None and origin_id in (None, coin.name()):
if primaries is None:
primaries = [{"puzzlehash": newpuzzlehash, "amount": amount}]
if amount > 0:
primaries = [{"puzzlehash": newpuzzlehash, "amount": uint64(amount), "memos": memos}]
else:
primaries = []
else:
primaries.append({"puzzlehash": newpuzzlehash, "amount": amount})
primaries.append({"puzzlehash": newpuzzlehash, "amount": uint64(amount), "memos": memos})
if change > 0:
change_puzzle_hash: bytes32 = await self.get_new_puzzlehash()
primaries.append({"puzzlehash": change_puzzle_hash, "amount": uint64(change)})
primaries.append({"puzzlehash": change_puzzle_hash, "amount": uint64(change), "memos": []})
message_list: List[bytes32] = [c.name() for c in coins]
for primary in primaries:
message_list.append(Coin(coin.name(), primary["puzzlehash"], primary["amount"]).name())
message: bytes32 = std_hash(b"".join(message_list))
# TODO: address hint error and remove ignore
# error: Argument "coin_announcements_to_assert" to "make_solution" of "Wallet" has incompatible
# type "Optional[Set[Announcement]]"; expected "Optional[Set[bytes32]]" [arg-type]
solution: Program = self.make_solution(
primaries=primaries,
fee=fee,
coin_announcements={message},
coin_announcements_to_assert=announcements_to_consume, # type: ignore[arg-type]
coin_announcements_to_assert=coin_announcements_bytes,
puzzle_announcements_to_assert=puzzle_announcements_bytes,
)
primary_announcement_hash = Announcement(coin.name(), message).name()
else:
# TODO: address hint error and remove ignore
# error: Argument 1 to <set> has incompatible type "Optional[bytes32]"; expected "bytes32"
# [arg-type]
solution = self.make_solution(coin_announcements_to_assert={primary_announcement_hash}) # type: ignore[arg-type] # noqa: E501
assert primary_announcement_hash is not None
solution = self.make_solution(coin_announcements_to_assert={primary_announcement_hash}, primaries=[])
spends.append(
CoinSpend(
@ -391,22 +419,33 @@ class Wallet:
coins: Set[Coin] = None,
primaries: Optional[List[AmountWithPuzzlehash]] = None,
ignore_max_send_amount: bool = False,
announcements_to_consume: Set[bytes32] = None,
coin_announcements_to_consume: Set[Announcement] = None,
puzzle_announcements_to_consume: Set[Announcement] = None,
memos: Optional[List[bytes]] = None,
negative_change_allowed: bool = False,
) -> TransactionRecord:
"""
Use this to generate transaction.
Note: this must be called under a wallet state manager lock
The first output is (amount, puzzle_hash, memos), and the rest of the outputs are in primaries.
"""
if primaries is None:
non_change_amount = amount
else:
non_change_amount = uint64(amount + sum(p["amount"] for p in primaries))
# TODO: address hint error and remove ignore
# error: Argument 8 to "_generate_unsigned_transaction" of "Wallet" has incompatible type
# "Optional[Set[bytes32]]"; expected "Optional[Set[Announcement]]" [arg-type]
transaction = await self._generate_unsigned_transaction(
amount, puzzle_hash, fee, origin_id, coins, primaries, ignore_max_send_amount, announcements_to_consume # type: ignore[arg-type] # noqa: E501
amount,
puzzle_hash,
fee,
origin_id,
coins,
primaries,
ignore_max_send_amount,
coin_announcements_to_consume,
puzzle_announcements_to_consume,
memos,
negative_change_allowed,
)
assert len(transaction) > 0
@ -422,7 +461,13 @@ class Wallet:
now = uint64(int(time.time()))
add_list: List[Coin] = list(spend_bundle.additions())
rem_list: List[Coin] = list(spend_bundle.removals())
assert sum(a.amount for a in add_list) + fee == sum(r.amount for r in rem_list)
output_amount = sum(a.amount for a in add_list) + fee
input_amount = sum(r.amount for r in rem_list)
if negative_change_allowed:
assert output_amount >= input_amount
else:
assert output_amount == input_amount
return TransactionRecord(
confirmed_at_height=uint32(0),
@ -440,11 +485,13 @@ class Wallet:
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=spend_bundle.name(),
memos=list(spend_bundle.get_memos().items()),
)
async def push_transaction(self, tx: TransactionRecord) -> None:
"""Use this API to send transactions."""
await self.wallet_state_manager.add_pending_transaction(tx)
await self.wallet_state_manager.wallet_node.update_ui()
# This is to be aggregated together with a coloured coin offer to ensure that the trade happens
async def create_spend_bundle_relative_chia(self, chia_amount: int, exclude: List[Coin]) -> SpendBundle:
@ -470,7 +517,9 @@ class Wallet:
puzzle = await self.puzzle_for_puzzle_hash(coin.puzzle_hash)
if output_created is None:
newpuzhash = await self.get_new_puzzlehash()
primaries: List[AmountWithPuzzlehash] = [{"puzzlehash": newpuzhash, "amount": uint64(chia_amount)}]
primaries: List[AmountWithPuzzlehash] = [
{"puzzlehash": newpuzhash, "amount": uint64(chia_amount), "memos": []}
]
solution = self.make_solution(primaries=primaries)
output_created = coin
list_of_solutions.append(CoinSpend(coin, puzzle, solution))

View File

@ -12,7 +12,7 @@ class WalletAction:
Purpose:
Some wallets require wallet node to perform a certain action when event happens.
For Example, coloured coin wallet needs to fetch solutions once it receives a coin.
For Example, CAT wallet needs to fetch solutions once it receives a coin.
In order to be safe from losing connection, closing the app, etc, those actions need to be persisted.
id: auto-incremented for every added action

View File

@ -1,321 +0,0 @@
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple
import aiosqlite
from chia.consensus.block_record import BlockRecord
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary
from chia.types.coin_spend import CoinSpend
from chia.types.header_block import HeaderBlock
from chia.util.db_wrapper import DBWrapper
from chia.util.ints import uint32, uint64
from chia.util.lru_cache import LRUCache
from chia.util.streamable import Streamable, streamable
from chia.wallet.block_record import HeaderBlockRecord
@dataclass(frozen=True)
@streamable
class AdditionalCoinSpends(Streamable):
coin_spends_list: List[CoinSpend]
class WalletBlockStore:
"""
This object handles HeaderBlocks and Blocks stored in DB used by wallet.
"""
db: aiosqlite.Connection
db_wrapper: DBWrapper
block_cache: LRUCache
@classmethod
async def create(cls, db_wrapper: DBWrapper):
self = cls()
self.db_wrapper = db_wrapper
self.db = db_wrapper.db
await self.db.execute(
"CREATE TABLE IF NOT EXISTS header_blocks(header_hash text PRIMARY KEY, height int,"
" timestamp int, block blob)"
)
await self.db.execute("CREATE INDEX IF NOT EXISTS header_hash on header_blocks(header_hash)")
await self.db.execute("CREATE INDEX IF NOT EXISTS timestamp on header_blocks(timestamp)")
await self.db.execute("CREATE INDEX IF NOT EXISTS height on header_blocks(height)")
# Block records
await self.db.execute(
"CREATE TABLE IF NOT EXISTS block_records(header_hash "
"text PRIMARY KEY, prev_hash text, height bigint, weight bigint, total_iters text,"
"block blob, sub_epoch_summary blob, is_peak tinyint)"
)
await self.db.execute(
"CREATE TABLE IF NOT EXISTS additional_coin_spends(header_hash text PRIMARY KEY, spends_list_blob blob)"
)
# Height index so we can look up in order of height for sync purposes
await self.db.execute("CREATE INDEX IF NOT EXISTS height on block_records(height)")
await self.db.execute("CREATE INDEX IF NOT EXISTS hh on block_records(header_hash)")
await self.db.execute("CREATE INDEX IF NOT EXISTS peak on block_records(is_peak)")
await self.db.commit()
self.block_cache = LRUCache(1000)
return self
async def _clear_database(self):
cursor_2 = await self.db.execute("DELETE FROM header_blocks")
await cursor_2.close()
await self.db.commit()
async def add_block_record(
self,
header_block_record: HeaderBlockRecord,
block_record: BlockRecord,
additional_coin_spends: List[CoinSpend],
):
"""
Adds a block record to the database. This block record is assumed to be connected
to the chain, but it may or may not be in the LCA path.
"""
cached = self.block_cache.get(header_block_record.header_hash)
if cached is not None:
# Since write to db can fail, we remove from cache here to avoid potential inconsistency
# Adding to cache only from reading
self.block_cache.put(header_block_record.header_hash, None)
if header_block_record.header.foliage_transaction_block is not None:
timestamp = header_block_record.header.foliage_transaction_block.timestamp
else:
timestamp = uint64(0)
cursor = await self.db.execute(
"INSERT OR REPLACE INTO header_blocks VALUES(?, ?, ?, ?)",
(
header_block_record.header_hash.hex(),
header_block_record.height,
timestamp,
bytes(header_block_record),
),
)
await cursor.close()
cursor_2 = await self.db.execute(
"INSERT OR REPLACE INTO block_records VALUES(?, ?, ?, ?, ?, ?, ?,?)",
(
header_block_record.header.header_hash.hex(),
header_block_record.header.prev_header_hash.hex(),
header_block_record.header.height,
header_block_record.header.weight.to_bytes(128 // 8, "big", signed=False).hex(),
header_block_record.header.total_iters.to_bytes(128 // 8, "big", signed=False).hex(),
bytes(block_record),
None
if block_record.sub_epoch_summary_included is None
else bytes(block_record.sub_epoch_summary_included),
False,
),
)
await cursor_2.close()
if len(additional_coin_spends) > 0:
blob: bytes = bytes(AdditionalCoinSpends(additional_coin_spends))
cursor_3 = await self.db.execute(
"INSERT OR REPLACE INTO additional_coin_spends VALUES(?, ?)",
(header_block_record.header_hash.hex(), blob),
)
await cursor_3.close()
async def get_header_block_at(self, heights: List[uint32]) -> List[HeaderBlock]:
if len(heights) == 0:
return []
heights_db = tuple(heights)
formatted_str = f'SELECT block from header_blocks WHERE height in ({"?," * (len(heights_db) - 1)}?)'
cursor = await self.db.execute(formatted_str, heights_db)
rows = await cursor.fetchall()
await cursor.close()
return [HeaderBlock.from_bytes(row[0]) for row in rows]
async def get_header_block_record(self, header_hash: bytes32) -> Optional[HeaderBlockRecord]:
"""Gets a block record from the database, if present"""
cached = self.block_cache.get(header_hash)
if cached is not None:
return cached
cursor = await self.db.execute("SELECT block from header_blocks WHERE header_hash=?", (header_hash.hex(),))
row = await cursor.fetchone()
await cursor.close()
if row is not None:
hbr: HeaderBlockRecord = HeaderBlockRecord.from_bytes(row[0])
self.block_cache.put(hbr.header_hash, hbr)
return hbr
else:
return None
async def get_additional_coin_spends(self, header_hash: bytes32) -> Optional[List[CoinSpend]]:
cursor = await self.db.execute(
"SELECT spends_list_blob from additional_coin_spends WHERE header_hash=?", (header_hash.hex(),)
)
row = await cursor.fetchone()
await cursor.close()
if row is not None:
coin_spends: AdditionalCoinSpends = AdditionalCoinSpends.from_bytes(row[0])
return coin_spends.coin_spends_list
else:
return None
async def get_block_record(self, header_hash: bytes32) -> Optional[BlockRecord]:
cursor = await self.db.execute(
"SELECT block from block_records WHERE header_hash=?",
(header_hash.hex(),),
)
row = await cursor.fetchone()
await cursor.close()
if row is not None:
return BlockRecord.from_bytes(row[0])
return None
async def get_block_records(
self,
) -> Tuple[Dict[bytes32, BlockRecord], Optional[bytes32]]:
"""
Returns a dictionary with all blocks, as well as the header hash of the peak,
if present.
"""
cursor = await self.db.execute("SELECT header_hash, block, is_peak from block_records")
rows = await cursor.fetchall()
await cursor.close()
ret: Dict[bytes32, BlockRecord] = {}
peak: Optional[bytes32] = None
for row in rows:
header_hash_bytes, block_record_bytes, is_peak = row
header_hash = bytes32.fromhex(header_hash_bytes)
ret[header_hash] = BlockRecord.from_bytes(block_record_bytes)
if is_peak:
assert peak is None # Sanity check, only one peak
peak = header_hash
return ret, peak
def rollback_cache_block(self, header_hash: bytes32):
self.block_cache.remove(header_hash)
async def set_peak(self, header_hash: bytes32) -> None:
cursor_1 = await self.db.execute("UPDATE block_records SET is_peak=0 WHERE is_peak=1")
await cursor_1.close()
cursor_2 = await self.db.execute(
"UPDATE block_records SET is_peak=1 WHERE header_hash=?",
(header_hash.hex(),),
)
await cursor_2.close()
async def get_block_records_close_to_peak(
self, blocks_n: int
) -> Tuple[Dict[bytes32, BlockRecord], Optional[bytes32]]:
"""
Returns a dictionary with all blocks, as well as the header hash of the peak,
if present.
"""
res = await self.db.execute("SELECT header_hash, height from block_records WHERE is_peak = 1")
row = await res.fetchone()
await res.close()
if row is None:
return {}, None
header_hash_bytes, peak_height = row
peak: bytes32 = bytes32(bytes.fromhex(header_hash_bytes))
formatted_str = f"SELECT header_hash, block from block_records WHERE height >= {peak_height - blocks_n}"
cursor = await self.db.execute(formatted_str)
rows = await cursor.fetchall()
await cursor.close()
ret: Dict[bytes32, BlockRecord] = {}
for row in rows:
header_hash_bytes, block_record_bytes = row
header_hash = bytes32.fromhex(header_hash_bytes)
ret[header_hash] = BlockRecord.from_bytes(block_record_bytes)
return ret, peak
async def get_header_blocks_in_range(
self,
start: int,
stop: int,
) -> Dict[bytes32, HeaderBlock]:
formatted_str = f"SELECT header_hash, block from header_blocks WHERE height >= {start} and height <= {stop}"
cursor = await self.db.execute(formatted_str)
rows = await cursor.fetchall()
await cursor.close()
ret: Dict[bytes32, HeaderBlock] = {}
for row in rows:
header_hash_bytes, block_record_bytes = row
header_hash = bytes32.fromhex(header_hash_bytes)
ret[header_hash] = HeaderBlock.from_bytes(block_record_bytes)
return ret
async def get_block_records_in_range(
self,
start: int,
stop: int,
) -> Dict[bytes32, BlockRecord]:
"""
Returns a dictionary with all blocks, as well as the header hash of the peak,
if present.
"""
formatted_str = f"SELECT header_hash, block from block_records WHERE height >= {start} and height <= {stop}"
cursor = await self.db.execute(formatted_str)
rows = await cursor.fetchall()
await cursor.close()
ret: Dict[bytes32, BlockRecord] = {}
for row in rows:
header_hash_bytes, block_record_bytes = row
header_hash = bytes32.fromhex(header_hash_bytes)
ret[header_hash] = BlockRecord.from_bytes(block_record_bytes)
return ret
async def get_peak_heights_dicts(self) -> Tuple[Dict[uint32, bytes32], Dict[uint32, SubEpochSummary]]:
"""
Returns a dictionary with all blocks, as well as the header hash of the peak,
if present.
"""
res = await self.db.execute("SELECT header_hash from block_records WHERE is_peak = 1")
row = await res.fetchone()
await res.close()
if row is None:
return {}, {}
peak: bytes32 = bytes32.fromhex(row[0])
cursor = await self.db.execute("SELECT header_hash,prev_hash,height,sub_epoch_summary from block_records")
rows = await cursor.fetchall()
await cursor.close()
hash_to_prev_hash: Dict[bytes32, bytes32] = {}
hash_to_height: Dict[bytes32, uint32] = {}
hash_to_summary: Dict[bytes32, SubEpochSummary] = {}
for row in rows:
hash_to_prev_hash[bytes32.fromhex(row[0])] = bytes32.fromhex(row[1])
hash_to_height[bytes32.fromhex(row[0])] = row[2]
if row[3] is not None:
hash_to_summary[bytes32.fromhex(row[0])] = SubEpochSummary.from_bytes(row[3])
height_to_hash: Dict[uint32, bytes32] = {}
sub_epoch_summaries: Dict[uint32, SubEpochSummary] = {}
curr_header_hash = peak
curr_height = hash_to_height[curr_header_hash]
while True:
height_to_hash[curr_height] = curr_header_hash
if curr_header_hash in hash_to_summary:
sub_epoch_summaries[curr_height] = hash_to_summary[curr_header_hash]
if curr_height == 0:
break
curr_header_hash = hash_to_prev_hash[curr_header_hash]
curr_height = hash_to_height[curr_header_hash]
return height_to_hash, sub_epoch_summaries

View File

@ -1,94 +1,41 @@
import asyncio
import dataclasses
import logging
import multiprocessing
from concurrent.futures.process import ProcessPoolExecutor
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
from chia.consensus.block_header_validation import validate_finished_header_block, validate_unfinished_header_block
from typing import Dict, Optional, Tuple, List
from chia.consensus.block_header_validation import validate_finished_header_block
from chia.consensus.block_record import BlockRecord
from chia.consensus.blockchain import ReceiveBlockResult
from chia.consensus.blockchain_interface import BlockchainInterface
from chia.consensus.constants import ConsensusConstants
from chia.consensus.difficulty_adjustment import get_next_sub_slot_iters_and_difficulty
from chia.consensus.find_fork_point import find_fork_point_in_chain
from chia.consensus.full_block_to_block_record import block_to_block_record
from chia.consensus.multiprocess_validation import PreValidationResult, pre_validate_blocks_multiprocessing
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary
from chia.types.coin_spend import CoinSpend
from chia.types.header_block import HeaderBlock
from chia.types.unfinished_header_block import UnfinishedHeaderBlock
from chia.util.errors import Err, ValidationError
from chia.types.weight_proof import WeightProof
from chia.util.errors import Err
from chia.util.ints import uint32, uint64
from chia.util.streamable import recurse_jsonify
from chia.wallet.block_record import HeaderBlockRecord
from chia.wallet.wallet_block_store import WalletBlockStore
from chia.wallet.wallet_coin_store import WalletCoinStore
from chia.wallet.wallet_pool_store import WalletPoolStore
from chia.wallet.wallet_transaction_store import WalletTransactionStore
from chia.wallet.key_val_store import KeyValStore
from chia.wallet.wallet_weight_proof_handler import WalletWeightProofHandler
log = logging.getLogger(__name__)
class ReceiveBlockResult(Enum):
"""
When Blockchain.receive_block(b) is called, one of these results is returned,
showing whether the block was added to the chain (extending the peak),
and if not, why it was not added.
"""
NEW_PEAK = 1 # Added to the peak of the blockchain
ADDED_AS_ORPHAN = 2 # Added as an orphan/stale block (not a new peak of the chain)
INVALID_BLOCK = 3 # Block was not added because it was invalid
ALREADY_HAVE_BLOCK = 4 # Block is already present in this blockchain
DISCONNECTED_BLOCK = 5 # Block's parent (previous pointer) is not in this blockchain
class WalletBlockchain(BlockchainInterface):
constants: ConsensusConstants
constants_json: Dict
# peak of the blockchain
_peak_height: Optional[uint32]
# All blocks in peak path are guaranteed to be included, can include orphan blocks
__block_records: Dict[bytes32, BlockRecord]
# Defines the path from genesis to the peak, no orphan blocks
__height_to_hash: Dict[uint32, bytes32]
# all hashes of blocks in block_record by height, used for garbage collection
__heights_in_cache: Dict[uint32, Set[bytes32]]
# All sub-epoch summaries that have been included in the blockchain from the beginning until and including the peak
# (height_included, SubEpochSummary). Note: ONLY for the blocks in the path to the peak
__sub_epoch_summaries: Dict[uint32, SubEpochSummary] = {}
# Stores
coin_store: WalletCoinStore
tx_store: WalletTransactionStore
pool_store: WalletPoolStore
block_store: WalletBlockStore
# Used to verify blocks in parallel
pool: ProcessPoolExecutor
_basic_store: KeyValStore
_weight_proof_handler: WalletWeightProofHandler
new_transaction_block_callback: Any
reorg_rollback: Any
wallet_state_manager_lock: asyncio.Lock
synced_weight_proof: Optional[WeightProof]
# Whether blockchain is shut down or not
_shut_down: bool
# Lock to prevent simultaneous reads and writes
lock: asyncio.Lock
log: logging.Logger
_peak: Optional[HeaderBlock]
_height_to_hash: Dict[uint32, bytes32]
_block_records: Dict[bytes32, BlockRecord]
_latest_timestamp: uint64
_sub_slot_iters: uint64
_difficulty: uint64
CACHE_SIZE: int
@staticmethod
async def create(
block_store: WalletBlockStore,
coin_store: WalletCoinStore,
tx_store: WalletTransactionStore,
pool_store: WalletPoolStore,
consensus_constants: ConsensusConstants,
new_transaction_block_callback: Callable, # f(removals: List[Coin], additions: List[Coin], height: uint32)
reorg_rollback: Callable,
lock: asyncio.Lock,
reserved_cores: int,
_basic_store: KeyValStore, constants: ConsensusConstants, weight_proof_handler: WalletWeightProofHandler
):
"""
Initializes a blockchain with the BlockRecords from disk, assuming they have all been
@ -96,401 +43,173 @@ class WalletBlockchain(BlockchainInterface):
in the consensus constants config.
"""
self = WalletBlockchain()
self.lock = asyncio.Lock()
self.coin_store = coin_store
self.tx_store = tx_store
self.pool_store = pool_store
cpu_count = multiprocessing.cpu_count()
if cpu_count > 61:
cpu_count = 61 # Windows Server 2016 has an issue https://bugs.python.org/issue26903
num_workers = max(cpu_count - reserved_cores, 1)
self.pool = ProcessPoolExecutor(max_workers=num_workers)
log.info(f"Started {num_workers} processes for block validation")
self.constants = consensus_constants
self.constants_json = recurse_jsonify(dataclasses.asdict(self.constants))
self.block_store = block_store
self._shut_down = False
self.new_transaction_block_callback = new_transaction_block_callback
self.reorg_rollback = reorg_rollback
self.log = logging.getLogger(__name__)
self.wallet_state_manager_lock = lock
await self._load_chain_from_store()
self._basic_store = _basic_store
self.constants = constants
self.CACHE_SIZE = constants.SUB_EPOCH_BLOCKS + 100
self._weight_proof_handler = weight_proof_handler
self.synced_weight_proof = await self._basic_store.get_object("SYNCED_WEIGHT_PROOF", WeightProof)
self._peak = None
self._peak = await self.get_peak_block()
self._latest_timestamp = uint64(0)
self._height_to_hash = {}
self._block_records = {}
if self.synced_weight_proof is not None:
await self.new_weight_proof(self.synced_weight_proof)
else:
self._sub_slot_iters = constants.SUB_SLOT_ITERS_STARTING
self._difficulty = constants.DIFFICULTY_STARTING
return self
def shut_down(self):
self._shut_down = True
self.pool.shutdown(wait=True)
async def new_weight_proof(self, weight_proof: WeightProof, records: Optional[List[BlockRecord]] = None) -> None:
peak: Optional[HeaderBlock] = await self.get_peak_block()
async def _load_chain_from_store(self) -> None:
"""
Initializes the state of the Blockchain class from the database.
"""
height_to_hash, sub_epoch_summaries = await self.block_store.get_peak_heights_dicts()
self.__height_to_hash = height_to_hash
self.__sub_epoch_summaries = sub_epoch_summaries
self.__block_records = {}
self.__heights_in_cache = {}
blocks, peak = await self.block_store.get_block_records_close_to_peak(self.constants.BLOCKS_CACHE_SIZE)
for block_record in blocks.values():
self.add_block_record(block_record)
if len(blocks) == 0:
assert peak is None
self._peak_height = None
if peak is not None and weight_proof.recent_chain_data[-1].weight <= peak.weight:
# No update, don't change anything
return None
self.synced_weight_proof = weight_proof
await self._basic_store.set_object("SYNCED_WEIGHT_PROOF", weight_proof)
assert peak is not None
self._peak_height = self.block_record(peak).height
assert len(self.__height_to_hash) == self._peak_height + 1
latest_timestamp = self._latest_timestamp
def get_peak(self) -> Optional[BlockRecord]:
"""
Return the peak of the blockchain
"""
if self._peak_height is None:
return None
return self.height_to_block_record(self._peak_height)
if records is None:
success, _, _, records = await self._weight_proof_handler.validate_weight_proof(weight_proof, True)
assert success
assert records is not None and len(records) > 1
async def receive_block(
self,
header_block_record: HeaderBlockRecord,
pre_validation_result: Optional[PreValidationResult] = None,
trusted: bool = False,
fork_point_with_peak: Optional[uint32] = None,
additional_coin_spends: List[CoinSpend] = None,
) -> Tuple[ReceiveBlockResult, Optional[Err], Optional[uint32]]:
"""
Adds a new block into the blockchain, if it's valid and connected to the current
blockchain, regardless of whether it is the child of a head, or another block.
Returns a header if block is added to head. Returns an error if the block is
invalid. Also returns the fork height, in the case of a new peak.
"""
for record in records:
self._height_to_hash[record.height] = record.header_hash
self.add_block_record(record)
if record.is_transaction_block:
assert record.timestamp is not None
if record.timestamp > latest_timestamp:
latest_timestamp = record.timestamp
if additional_coin_spends is None:
additional_coin_spends = []
block = header_block_record.header
genesis: bool = block.height == 0
self._sub_slot_iters = records[-1].sub_slot_iters
self._difficulty = uint64(records[-1].weight - records[-2].weight)
await self.set_peak_block(weight_proof.recent_chain_data[-1], latest_timestamp)
self.clean_block_records()
async def receive_block(self, block: HeaderBlock) -> Tuple[ReceiveBlockResult, Optional[Err]]:
if self.contains_block(block.header_hash):
return ReceiveBlockResult.ALREADY_HAVE_BLOCK, None, None
if not self.contains_block(block.prev_header_hash) and not genesis:
return (
ReceiveBlockResult.DISCONNECTED_BLOCK,
Err.INVALID_PREV_BLOCK_HASH,
None,
)
if block.height == 0:
prev_b: Optional[BlockRecord] = None
return ReceiveBlockResult.ALREADY_HAVE_BLOCK, None
if not self.contains_block(block.prev_header_hash) and block.height > 0:
return ReceiveBlockResult.DISCONNECTED_BLOCK, None
if (
len(block.finished_sub_slots) > 0
and block.finished_sub_slots[0].challenge_chain.new_sub_slot_iters is not None
):
assert block.finished_sub_slots[0].challenge_chain.new_difficulty is not None # They both change together
sub_slot_iters: uint64 = block.finished_sub_slots[0].challenge_chain.new_sub_slot_iters
difficulty: uint64 = block.finished_sub_slots[0].challenge_chain.new_difficulty
else:
prev_b = self.block_record(block.prev_header_hash)
sub_slot_iters, difficulty = get_next_sub_slot_iters_and_difficulty(
self.constants, len(block.finished_sub_slots) > 0, prev_b, self
sub_slot_iters = self._sub_slot_iters
difficulty = self._difficulty
required_iters, error = validate_finished_header_block(
self.constants, self, block, False, difficulty, sub_slot_iters, False
)
if trusted is False and pre_validation_result is None:
required_iters, error = validate_finished_header_block(
self.constants, self, block, False, difficulty, sub_slot_iters
)
elif trusted:
unfinished_header_block = UnfinishedHeaderBlock(
block.finished_sub_slots,
block.reward_chain_block.get_unfinished(),
block.challenge_chain_sp_proof,
block.reward_chain_sp_proof,
block.foliage,
block.foliage_transaction_block,
block.transactions_filter,
)
required_iters, val_error = validate_unfinished_header_block(
self.constants, self, unfinished_header_block, False, difficulty, sub_slot_iters, False, True
)
error = val_error if val_error is not None else None
else:
assert pre_validation_result is not None
required_iters = pre_validation_result.required_iters
error = (
ValidationError(Err(pre_validation_result.error)) if pre_validation_result.error is not None else None
)
if error is not None:
return ReceiveBlockResult.INVALID_BLOCK, error.code, None
assert required_iters is not None
return ReceiveBlockResult.INVALID_BLOCK, error.code
if required_iters is None:
return ReceiveBlockResult.INVALID_BLOCK, Err.INVALID_POSPACE
block_record = block_to_block_record(
self.constants,
self,
required_iters,
None,
block,
block_record: BlockRecord = block_to_block_record(
self.constants, self, required_iters, None, block, sub_slot_iters
)
heights_changed: Set[Tuple[uint32, Optional[bytes32]]] = set()
# Always add the block to the database
async with self.wallet_state_manager_lock:
async with self.block_store.db_wrapper.lock:
try:
await self.block_store.db_wrapper.begin_transaction()
await self.block_store.add_block_record(header_block_record, block_record, additional_coin_spends)
self.add_block_record(block_record)
self.clean_block_record(block_record.height - self.constants.BLOCKS_CACHE_SIZE)
fork_height, records_to_add = await self._reconsider_peak(
block_record, genesis, fork_point_with_peak, additional_coin_spends, heights_changed
)
for record in records_to_add:
if record.sub_epoch_summary_included is not None:
self.__sub_epoch_summaries[record.height] = record.sub_epoch_summary_included
await self.block_store.db_wrapper.commit_transaction()
except BaseException as e:
self.log.error(f"Error during db transaction: {e}")
if self.block_store.db_wrapper.db._connection is not None:
await self.block_store.db_wrapper.rollback_transaction()
self.remove_block_record(block_record.header_hash)
self.block_store.rollback_cache_block(block_record.header_hash)
await self.coin_store.rebuild_wallet_cache()
await self.tx_store.rebuild_tx_cache()
await self.pool_store.rebuild_cache()
for height, replaced in heights_changed:
# If it was replaced change back to the previous value otherwise pop the change
if replaced is not None:
self.__height_to_hash[height] = replaced
else:
self.__height_to_hash.pop(height)
raise
if fork_height is not None:
self.log.info(f"💰 Updated wallet peak to height {block_record.height}, weight {block_record.weight}, ")
return ReceiveBlockResult.NEW_PEAK, None, fork_height
self.add_block_record(block_record)
if self._peak is None:
if block_record.is_transaction_block:
latest_timestamp = block_record.timestamp
else:
return ReceiveBlockResult.ADDED_AS_ORPHAN, None, None
async def _reconsider_peak(
self,
block_record: BlockRecord,
genesis: bool,
fork_point_with_peak: Optional[uint32],
additional_coin_spends_from_wallet: Optional[List[CoinSpend]],
heights_changed: Set[Tuple[uint32, Optional[bytes32]]],
) -> Tuple[Optional[uint32], List[BlockRecord]]:
"""
When a new block is added, this is called, to check if the new block is the new peak of the chain.
This also handles reorgs by reverting blocks which are not in the heaviest chain.
It returns the height of the fork between the previous chain and the new chain, or returns
None if there was no update to the heaviest chain.
"""
peak = self.get_peak()
if genesis:
if peak is None:
block: Optional[HeaderBlockRecord] = await self.block_store.get_header_block_record(
block_record.header_hash
)
assert block is not None
replaced = None
if uint32(0) in self.__height_to_hash:
replaced = self.__height_to_hash[uint32(0)]
self.__height_to_hash[uint32(0)] = block.header_hash
heights_changed.add((uint32(0), replaced))
assert len(block.additions) == 0 and len(block.removals) == 0
await self.new_transaction_block_callback(block.removals, block.additions, block_record, [])
self._peak_height = uint32(0)
return uint32(0), [block_record]
return None, []
assert peak is not None
if block_record.weight > peak.weight:
# Find the fork. if the block is just being appended, it will return the peak
# If no blocks in common, returns -1, and reverts all blocks
if fork_point_with_peak is not None:
fork_h: int = fork_point_with_peak
latest_timestamp = None
self._height_to_hash[block_record.height] = block_record.header_hash
await self.set_peak_block(block, latest_timestamp)
return ReceiveBlockResult.NEW_PEAK, None
elif block_record.weight > self._peak.weight:
if block_record.prev_hash == self._peak.header_hash:
fork_height: int = self._peak.height
else:
fork_h = find_fork_point_in_chain(self, block_record, peak)
# Rollback to fork
self.log.debug(f"fork_h: {fork_h}, SB: {block_record.height}, peak: {peak.height}")
if block_record.prev_hash != peak.header_hash:
await self.reorg_rollback(fork_h)
# Rollback sub_epoch_summaries
heights_to_delete = []
for ses_included_height in self.__sub_epoch_summaries.keys():
if ses_included_height > fork_h:
heights_to_delete.append(ses_included_height)
for height in heights_to_delete:
del self.__sub_epoch_summaries[height]
# Collect all blocks from fork point to new peak
blocks_to_add: List[Tuple[HeaderBlockRecord, BlockRecord, List[CoinSpend]]] = []
curr = block_record.header_hash
while fork_h < 0 or curr != self.height_to_hash(uint32(fork_h)):
fetched_header_block: Optional[HeaderBlockRecord] = await self.block_store.get_header_block_record(curr)
fetched_block_record: Optional[BlockRecord] = await self.block_store.get_block_record(curr)
if curr == block_record.header_hash:
additional_coin_spends = additional_coin_spends_from_wallet
else:
additional_coin_spends = await self.block_store.get_additional_coin_spends(curr)
if additional_coin_spends is None:
additional_coin_spends = []
assert fetched_header_block is not None
assert fetched_block_record is not None
blocks_to_add.append((fetched_header_block, fetched_block_record, additional_coin_spends))
if fetched_header_block.height == 0:
# Doing a full reorg, starting at height 0
fork_height = find_fork_point_in_chain(self, block_record, self._peak)
await self._rollback_to_height(fork_height)
curr_record: BlockRecord = block_record
latest_timestamp = self._latest_timestamp
while curr_record.height > fork_height:
self._height_to_hash[curr_record.height] = curr_record.header_hash
if curr_record.timestamp is not None and curr_record.timestamp > latest_timestamp:
latest_timestamp = curr_record.timestamp
if curr_record.height == 0:
break
curr = fetched_block_record.prev_hash
curr_record = self.block_record(curr_record.prev_hash)
self._sub_slot_iters = block_record.sub_slot_iters
self._difficulty = uint64(block_record.weight - self.block_record(block_record.prev_hash).weight)
await self.set_peak_block(block, latest_timestamp)
self.clean_block_records()
return ReceiveBlockResult.NEW_PEAK, None
return ReceiveBlockResult.ADDED_AS_ORPHAN, None
records_to_add: List[BlockRecord] = []
for fetched_header_block, fetched_block_record, additional_coin_spends in reversed(blocks_to_add):
replaced = None
if fetched_block_record.height in self.__height_to_hash:
replaced = self.__height_to_hash[fetched_block_record.height]
self.__height_to_hash[fetched_block_record.height] = fetched_block_record.header_hash
heights_changed.add((fetched_block_record.height, replaced))
records_to_add.append(fetched_block_record)
if fetched_block_record.is_transaction_block:
await self.new_transaction_block_callback(
fetched_header_block.removals,
fetched_header_block.additions,
fetched_block_record,
additional_coin_spends,
)
async def _rollback_to_height(self, height: int):
if self._peak is None:
return
for h in range(max(0, height + 1), self._peak.height + 1):
del self._height_to_hash[uint32(h)]
# Changes the peak to be the new peak
await self.block_store.set_peak(block_record.header_hash)
self._peak_height = block_record.height
if fork_h < 0:
return None, records_to_add
return uint32(fork_h), records_to_add
await self._basic_store.remove_object("PEAK_BLOCK")
# This is not a heavier block than the heaviest we have seen, so we don't change the coin set
return None, []
def get_peak_height(self) -> uint32:
if self._peak is None:
return uint32(0)
return self._peak.height
def get_next_difficulty(self, header_hash: bytes32, new_slot: bool) -> uint64:
assert self.contains_block(header_hash)
curr = self.block_record(header_hash)
if curr.height <= 2:
return self.constants.DIFFICULTY_STARTING
return get_next_sub_slot_iters_and_difficulty(self.constants, new_slot, curr, self)[1]
async def set_peak_block(self, block: HeaderBlock, timestamp: Optional[uint64] = None):
await self._basic_store.set_object("PEAK_BLOCK", block)
self._peak = block
if timestamp is not None:
self._latest_timestamp = timestamp
elif block.foliage_transaction_block is not None:
self._latest_timestamp = block.foliage_transaction_block.timestamp
log.info(f"Peak set to : {self._peak.height} timestamp: {self._latest_timestamp}")
def get_next_slot_iters(self, header_hash: bytes32, new_slot: bool) -> uint64:
assert self.contains_block(header_hash)
curr = self.block_record(header_hash)
if curr.height <= 2:
return self.constants.SUB_SLOT_ITERS_STARTING
return get_next_sub_slot_iters_and_difficulty(self.constants, new_slot, curr, self)[0]
async def get_peak_block(self) -> Optional[HeaderBlock]:
if self._peak is not None:
return self._peak
return await self._basic_store.get_object("PEAK_BLOCK", HeaderBlock)
async def pre_validate_blocks_multiprocessing(
self, blocks: List[HeaderBlock], batch_size: int = 4
) -> Optional[List[PreValidationResult]]:
return await pre_validate_blocks_multiprocessing(
self.constants, self.constants_json, self, blocks, self.pool, True, {}, None, batch_size
)
def get_latest_timestamp(self) -> uint64:
return self._latest_timestamp
def contains_block(self, header_hash: bytes32) -> bool:
"""
True if we have already added this block to the chain. This may return false for orphan blocks
that we have added but no longer keep in memory.
"""
return header_hash in self.__block_records
def block_record(self, header_hash: bytes32) -> BlockRecord:
return self.__block_records[header_hash]
def height_to_block_record(self, height: uint32, check_db=False) -> BlockRecord:
header_hash = self.height_to_hash(height)
# TODO: address hint error and remove ignore
# error: Argument 1 to "block_record" of "WalletBlockchain" has incompatible type "Optional[bytes32]";
# expected "bytes32" [arg-type]
return self.block_record(header_hash) # type: ignore[arg-type]
def get_ses_heights(self) -> List[uint32]:
return sorted(self.__sub_epoch_summaries.keys())
def get_ses(self, height: uint32) -> SubEpochSummary:
return self.__sub_epoch_summaries[height]
def height_to_hash(self, height: uint32) -> Optional[bytes32]:
return self.__height_to_hash[height]
return header_hash in self._block_records
def contains_height(self, height: uint32) -> bool:
return height in self.__height_to_hash
return height in self._height_to_hash
def get_peak_height(self) -> Optional[uint32]:
return self._peak_height
def height_to_hash(self, height: uint32) -> bytes32:
return self._height_to_hash[height]
async def warmup(self, fork_point: uint32):
"""
Loads blocks into the cache. The blocks loaded include all blocks from
fork point - BLOCKS_CACHE_SIZE up to and including the fork_point.
def try_block_record(self, header_hash: bytes32) -> Optional[BlockRecord]:
if self.contains_block(header_hash):
return self.block_record(header_hash)
return None
Args:
fork_point: the last block height to load in the cache
def block_record(self, header_hash: bytes32) -> BlockRecord:
return self._block_records[header_hash]
"""
if self._peak_height is None:
return None
blocks = await self.block_store.get_block_records_in_range(
fork_point - self.constants.BLOCKS_CACHE_SIZE, self._peak_height
)
for block_record in blocks.values():
self.add_block_record(block_record)
def clean_block_record(self, height: int):
"""
Clears all block records in the cache which have block_record < height.
Args:
height: Minimum height that we need to keep in the cache
"""
if height < 0:
return None
blocks_to_remove = self.__heights_in_cache.get(uint32(height), None)
while blocks_to_remove is not None and height >= 0:
for header_hash in blocks_to_remove:
del self.__block_records[header_hash]
del self.__heights_in_cache[uint32(height)] # remove height from heights in cache
if height == 0:
break
height -= 1
blocks_to_remove = self.__heights_in_cache.get(uint32(height), None)
def add_block_record(self, block_record: BlockRecord):
self._block_records[block_record.header_hash] = block_record
def clean_block_records(self):
"""
Cleans the cache so that we only maintain relevant blocks.
This removes block records that have height < peak - BLOCKS_CACHE_SIZE.
These blocks are necessary for calculating future difficulty adjustments.
Cleans the cache so that we only maintain relevant blocks. This removes
block records that have height < peak - CACHE_SIZE.
"""
if len(self.__block_records) < self.constants.BLOCKS_CACHE_SIZE:
height_limit = max(0, self.get_peak_height() - self.CACHE_SIZE)
if len(self._block_records) < self.CACHE_SIZE:
return None
peak = self.get_peak()
assert peak is not None
if peak.height - self.constants.BLOCKS_CACHE_SIZE < 0:
return None
self.clean_block_record(peak.height - self.constants.BLOCKS_CACHE_SIZE)
to_remove: List[bytes32] = []
for header_hash, block_record in self._block_records.items():
if block_record.height < height_limit:
to_remove.append(header_hash)
async def get_block_records_in_range(self, start: int, stop: int) -> Dict[bytes32, BlockRecord]:
return await self.block_store.get_block_records_in_range(start, stop)
async def get_header_blocks_in_range(
self, start: int, stop: int, tx_filter: bool = True
) -> Dict[bytes32, HeaderBlock]:
return await self.block_store.get_header_blocks_in_range(start, stop)
async def get_block_record_from_db(self, header_hash: bytes32) -> Optional[BlockRecord]:
if header_hash in self.__block_records:
return self.__block_records[header_hash]
return await self.block_store.get_block_record(header_hash)
def remove_block_record(self, header_hash: bytes32):
sbr = self.block_record(header_hash)
del self.__block_records[header_hash]
self.__heights_in_cache[sbr.height].remove(header_hash)
def add_block_record(self, block_record: BlockRecord):
self.__block_records[block_record.header_hash] = block_record
if block_record.height not in self.__heights_in_cache.keys():
self.__heights_in_cache[block_record.height] = set()
self.__heights_in_cache[block_record.height].add(block_record.header_hash)
for header_hash in to_remove:
del self._block_records[header_hash]

View File

@ -82,6 +82,20 @@ class WalletCoinStore:
self.unspent_coin_wallet_cache[coin_record.wallet_id] = {}
self.unspent_coin_wallet_cache[coin_record.wallet_id][name] = coin_record
async def get_multiple_coin_records(self, coin_names: List[bytes32]) -> List[WalletCoinRecord]:
"""Return WalletCoinRecord(s) that have a coin name in the specified list"""
if set(coin_names).issubset(set(self.coin_record_cache.keys())):
return list(filter(lambda cr: cr.coin.name() in coin_names, self.coin_record_cache.values()))
else:
as_hexes = [cn.hex() for cn in coin_names]
cursor = await self.db_connection.execute(
f'SELECT * from coin_record WHERE coin_name in ({"?," * (len(as_hexes) - 1)}?)', tuple(as_hexes)
)
rows = await cursor.fetchall()
await cursor.close()
return [self.coin_record_from_row(row) for row in rows]
# Store CoinRecord in DB and ram cache
async def add_coin_record(self, record: WalletCoinRecord) -> None:
# update wallet cache
@ -114,6 +128,18 @@ class WalletCoinStore:
)
await cursor.close()
# Sometimes we realize that a coin is actually not interesting to us so we need to delete it
async def delete_coin_record(self, coin_name: bytes32) -> None:
if coin_name in self.coin_record_cache:
coin_record = self.coin_record_cache.pop(coin_name)
if coin_record.wallet_id in self.unspent_coin_wallet_cache:
coin_cache = self.unspent_coin_wallet_cache[coin_record.wallet_id]
if coin_name in coin_cache:
coin_cache.pop(coin_record.coin.name())
c = await self.db_connection.execute("DELETE FROM coin_record WHERE coin_name=?", (coin_name.hex(),))
await c.close()
# Update coin_record to be spent in DB
async def set_spent(self, coin_name: bytes32, height: uint32) -> WalletCoinRecord:
current: Optional[WalletCoinRecord] = await self.get_coin_record(coin_name)
@ -200,6 +226,20 @@ class WalletCoinStore:
return set(self.coin_record_from_row(row) for row in rows)
async def get_coins_to_check(self, check_height) -> Set[WalletCoinRecord]:
"""Returns set of all CoinRecords."""
cursor = await self.db_connection.execute(
"SELECT * from coin_record where spent_height=0 or spent_height>? or confirmed_height>?",
(
check_height,
check_height,
),
)
rows = await cursor.fetchall()
await cursor.close()
return set(self.coin_record_from_row(row) for row in rows)
# Checks DB and DiffStores for CoinRecords with puzzle_hash and returns them
async def get_coin_records_by_puzzle_hash(self, puzzle_hash: bytes32) -> List[WalletCoinRecord]:
"""Returns a list of all coin records with the given puzzle hash"""
@ -209,6 +249,17 @@ class WalletCoinStore:
return [self.coin_record_from_row(row) for row in rows]
# Checks DB and DiffStores for CoinRecords with parent_coin_info and returns them
async def get_coin_records_by_parent_id(self, parent_coin_info: bytes32) -> List[WalletCoinRecord]:
"""Returns a list of all coin records with the given parent id"""
cursor = await self.db_connection.execute(
"SELECT * from coin_record WHERE coin_parent=?", (parent_coin_info.hex(),)
)
rows = await cursor.fetchall()
await cursor.close()
return [self.coin_record_from_row(row) for row in rows]
async def rollback_to_block(self, height: int):
"""
Rolls back the blockchain to block_index. All blocks confirmed after this point
@ -229,7 +280,8 @@ class WalletCoinStore:
coin_record.wallet_id,
)
self.coin_record_cache[coin_record.coin.name()] = new_record
self.unspent_coin_wallet_cache[coin_record.wallet_id][coin_record.coin.name()] = new_record
if coin_record.wallet_id in self.unspent_coin_wallet_cache:
self.unspent_coin_wallet_cache[coin_record.wallet_id][coin_record.coin.name()] = new_record
if coin_record.confirmed_block_height > height:
delete_queue.append(coin_record)

File diff suppressed because it is too large Load Diff

View File

@ -78,13 +78,17 @@ class WalletNodeAPI:
assert peer.peer_node_id is not None
name = peer.peer_node_id.hex()
status = MempoolInclusionStatus(ack.status)
if self.wallet_node.wallet_state_manager is None or self.wallet_node.backup_initialized is False:
if self.wallet_node.wallet_state_manager is None:
return None
if status == MempoolInclusionStatus.SUCCESS:
self.wallet_node.log.info(f"SpendBundle has been received and accepted to mempool by the FullNode. {ack}")
elif status == MempoolInclusionStatus.PENDING:
self.wallet_node.log.info(f"SpendBundle has been received (and is pending) by the FullNode. {ack}")
else:
if not self.wallet_node.is_trusted(peer) and ack.error == Err.NO_TRANSACTIONS_WHILE_SYNCING.name:
self.wallet_node.log.info(f"Peer {peer.get_peer_info()} is not synced, closing connection")
await peer.close()
return
self.wallet_node.log.warning(f"SpendBundle has been rejected by the FullNode. {ack}")
if ack.error is not None:
await self.wallet_node.wallet_state_manager.remove_from_queue(ack.txid, name, status, Err[ack.error])
@ -96,10 +100,11 @@ class WalletNodeAPI:
async def respond_peers_introducer(
self, request: introducer_protocol.RespondPeersIntroducer, peer: WSChiaConnection
):
if not self.wallet_node.has_full_node():
await self.wallet_node.wallet_peers.respond_peers(request, peer.get_peer_info(), False)
else:
await self.wallet_node.wallet_peers.ensure_is_closed()
if self.wallet_node.wallet_peers is not None:
if not self.wallet_node.has_full_node():
await self.wallet_node.wallet_peers.respond_peers(request, peer.get_peer_info(), False)
else:
await self.wallet_node.wallet_peers.ensure_is_closed()
if peer is not None and peer.connection_type is NodeType.INTRODUCER:
await peer.close()
@ -107,6 +112,8 @@ class WalletNodeAPI:
@peer_required
@api_request
async def respond_peers(self, request: full_node_protocol.RespondPeers, peer: WSChiaConnection):
if self.wallet_node.wallet_peers is None:
return None
if not self.wallet_node.has_full_node():
self.log.info(f"Wallet received {len(request.peer_list)} peers.")
await self.wallet_node.wallet_peers.respond_peers(request, peer.get_peer_info(), True)
@ -117,7 +124,7 @@ class WalletNodeAPI:
@api_request
async def respond_puzzle_solution(self, request: wallet_protocol.RespondPuzzleSolution):
if self.wallet_node.wallet_state_manager is None or self.wallet_node.backup_initialized is False:
if self.wallet_node.wallet_state_manager is None:
return None
await self.wallet_node.wallet_state_manager.puzzle_solution_received(request)
@ -132,3 +139,28 @@ class WalletNodeAPI:
@api_request
async def reject_header_blocks(self, request: wallet_protocol.RejectHeaderBlocks):
self.log.warning(f"Reject header blocks: {request}")
@peer_required
@api_request
async def coin_state_update(self, request: wallet_protocol.CoinStateUpdate, peer: WSChiaConnection):
await self.wallet_node.state_update_received(request, peer)
@api_request
async def respond_to_ph_update(self, request: wallet_protocol.RespondToPhUpdates):
pass
@api_request
async def respond_to_coin_update(self, request: wallet_protocol.RespondToCoinUpdates):
pass
@api_request
async def respond_children(self, request: wallet_protocol.RespondChildren):
pass
@api_request
async def respond_ses_hashes(self, request: wallet_protocol.RespondSESInfo):
pass
@api_request
async def respond_blocks(self, request: full_node_protocol.RespondBlocks) -> None:
pass

View File

@ -43,7 +43,8 @@ class WalletPuzzleStore:
" puzzle_hash text PRIMARY_KEY,"
" wallet_type int,"
" wallet_id int,"
" used tinyint)"
" used tinyint,"
" hardened tinyint)"
)
)
await self.db_connection.execute(
@ -88,6 +89,10 @@ class WalletPuzzleStore:
sql_records = []
for record in records:
self.all_puzzle_hashes.add(record.puzzle_hash)
if record.hardened:
hardened = 1
else:
hardened = 0
sql_records.append(
(
record.index,
@ -96,11 +101,12 @@ class WalletPuzzleStore:
record.wallet_type,
record.wallet_id,
0,
hardened,
),
)
cursor = await self.db_connection.executemany(
"INSERT OR REPLACE INTO derivation_paths VALUES(?, ?, ?, ?, ?, ?)",
"INSERT OR REPLACE INTO derivation_paths VALUES(?, ?, ?, ?, ?, ?, ?)",
sql_records,
)
@ -110,16 +116,19 @@ class WalletPuzzleStore:
await self.db_connection.commit()
self.db_wrapper.lock.release()
async def get_derivation_record(self, index: uint32, wallet_id: uint32) -> Optional[DerivationRecord]:
async def get_derivation_record(
self, index: uint32, wallet_id: uint32, hardened: bool
) -> Optional[DerivationRecord]:
"""
Returns the derivation record by index and wallet id.
"""
if hardened:
hard = 1
else:
hard = 0
cursor = await self.db_connection.execute(
"SELECT * FROM derivation_paths WHERE derivation_index=? and wallet_id=?;",
(
index,
wallet_id,
),
"SELECT * FROM derivation_paths WHERE derivation_index=? and wallet_id=? and hardened=?;",
(index, wallet_id, hard),
)
row = await cursor.fetchone()
await cursor.close()
@ -131,6 +140,7 @@ class WalletPuzzleStore:
G1Element.from_bytes(bytes.fromhex(row[1])),
WalletType(row[3]),
uint32(row[4]),
bool(row[5]),
)
return None
@ -153,6 +163,7 @@ class WalletPuzzleStore:
G1Element.from_bytes(bytes.fromhex(row[1])),
WalletType(row[3]),
uint32(row[4]),
bool(row[6]),
)
return None
@ -201,6 +212,16 @@ class WalletPuzzleStore:
return False
def row_to_record(self, row) -> DerivationRecord:
return DerivationRecord(
uint32(row[0]),
bytes32.fromhex(row[2]),
G1Element.from_bytes(bytes.fromhex(row[1])),
WalletType(row[3]),
uint32(row[4]),
bool(row[6]),
)
async def index_for_pubkey(self, pubkey: G1Element) -> Optional[uint32]:
"""
Returns derivation paths for the given pubkey.
@ -218,6 +239,23 @@ class WalletPuzzleStore:
return None
async def record_for_pubkey(self, pubkey: G1Element) -> Optional[DerivationRecord]:
"""
Returns derivation record for the given pubkey.
Returns None if not present.
"""
cursor = await self.db_connection.execute(
"SELECT * from derivation_paths WHERE pubkey=?", (bytes(pubkey).hex(),)
)
row = await cursor.fetchone()
await cursor.close()
if row is not None:
return self.row_to_record(row)
return None
async def index_for_puzzle_hash(self, puzzle_hash: bytes32) -> Optional[uint32]:
"""
Returns the derivation path for the puzzle_hash.
@ -234,6 +272,22 @@ class WalletPuzzleStore:
return None
async def record_for_puzzle_hash(self, puzzle_hash: bytes32) -> Optional[DerivationRecord]:
"""
Returns the derivation path for the puzzle_hash.
Returns None if not present.
"""
cursor = await self.db_connection.execute(
"SELECT * from derivation_paths WHERE puzzle_hash=?", (puzzle_hash.hex(),)
)
row = await cursor.fetchone()
await cursor.close()
if row is not None and row[0] is not None:
return self.row_to_record(row)
return None
async def index_for_puzzle_hash_and_wallet(self, puzzle_hash: bytes32, wallet_id: uint32) -> Optional[uint32]:
"""
Returns the derivation path for the puzzle_hash.
@ -322,14 +376,14 @@ class WalletPuzzleStore:
"""
cursor = await self.db_connection.execute(
f"SELECT MAX(derivation_index) FROM derivation_paths WHERE wallet_id={wallet_id} and used=1;"
f"SELECT MAX(derivation_index) FROM derivation_paths WHERE wallet_id={wallet_id} and used=1 and hardened=0;"
)
row = await cursor.fetchone()
await cursor.close()
if row is not None and row[0] is not None:
index = uint32(row[0])
return await self.get_derivation_record(index, wallet_id)
return await self.get_derivation_record(index, wallet_id, False)
return None
@ -337,7 +391,9 @@ class WalletPuzzleStore:
"""
Returns the first unused derivation path by derivation_index.
"""
cursor = await self.db_connection.execute("SELECT MIN(derivation_index) FROM derivation_paths WHERE used=0;")
cursor = await self.db_connection.execute(
"SELECT MIN(derivation_index) FROM derivation_paths WHERE used=0 and hardened=0;"
)
row = await cursor.fetchone()
await cursor.close()

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More