mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2024-09-19 23:21:46 +03:00
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 at71da0487b9
* Remove unused ignores * more unused ignores * Fix bad merge at3b143e7050
* 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:
parent
0ba838b7a8
commit
89f15f591c
@ -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
|
||||
#
|
@ -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
|
||||
|
||||
|
||||
#
|
@ -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"]
|
||||
|
@ -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]]:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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():
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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}
|
||||
|
@ -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})
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
30
chia/wallet/cat_wallet/cat_constants.py
Normal file
30
chia/wallet/cat_wallet/cat_constants.py
Normal 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,
|
||||
}
|
@ -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}
|
135
chia/wallet/cat_wallet/cat_utils.py
Normal file
135
chia/wallet/cat_wallet/cat_utils.py
Normal 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)
|
776
chia/wallet/cat_wallet/cat_wallet.py
Normal file
776
chia/wallet/cat_wallet/cat_wallet.py
Normal 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)
|
@ -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
|
@ -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)
|
@ -19,3 +19,4 @@ class DerivationRecord:
|
||||
pubkey: G1Element
|
||||
wallet_type: WalletType
|
||||
wallet_id: uint32
|
||||
hardened: bool
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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
33
chia/wallet/payment.py
Normal 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)
|
417
chia/wallet/puzzles/cat.clvm
Normal file
417
chia/wallet/puzzles/cat.clvm
Normal 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
|
||||
)
|
||||
)
|
1
chia/wallet/puzzles/cat.clvm.hex
Normal file
1
chia/wallet/puzzles/cat.clvm.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff01ff02ff5effff04ff02ffff04ffff04ff05ffff04ffff0bff2cff0580ffff04ff0bff80808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ffff02ff2effff04ff02ffff04ff17ff80808080ffff04ffff0bff82027fff82057fff820b7f80ffff04ff81bfffff04ff82017fffff04ff8202ffffff04ff8205ffffff04ff820bffff80808080808080808080808080ffff04ffff01ffffffff81ca3dff46ff0233ffff3c04ff01ff0181cbffffff02ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff22ffff0bff2cff3480ffff0bff22ffff0bff22ffff0bff2cff5c80ff0980ffff0bff22ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ffff02ff2effff04ff02ffff04ff13ff80808080ff820b9f80ffff01ff02ff26ffff04ff02ffff04ffff02ff13ffff04ff5fffff04ff17ffff04ff2fffff04ff81bfffff04ff82017fffff04ff1bff8080808080808080ffff04ff82017fff8080808080ffff01ff088080ff0180ffff01ff02ffff03ff17ffff01ff02ffff03ffff20ff81bf80ffff0182017fffff01ff088080ff0180ffff01ff088080ff018080ff0180ffff04ffff04ff05ff2780ffff04ffff10ff0bff5780ff778080ff02ffff03ff05ffff01ff02ffff03ffff09ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff01818f80ffff01ff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ffff04ff81b9ff82017980ff808080808080ffff01ff02ff5affff04ff02ffff04ffff02ffff03ffff09ff11ff7880ffff01ff04ff78ffff04ffff02ff36ffff04ff02ffff04ff13ffff04ff29ffff04ffff0bff2cff5b80ffff04ff2bff80808080808080ff398080ffff01ff02ffff03ffff09ff11ff2480ffff01ff04ff24ffff04ffff0bff20ff2980ff398080ffff010980ff018080ff0180ffff04ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff04ffff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ff17ff808080808080ff80808080808080ff0180ffff01ff04ff80ffff04ff80ff17808080ff0180ffffff02ffff03ff05ffff01ff04ff09ffff02ff26ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff0bff22ffff0bff2cff5880ffff0bff22ffff0bff22ffff0bff2cff5c80ff0580ffff0bff22ffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bff2cff058080ff0180ffff04ffff04ff28ffff04ff5fff808080ffff02ff7effff04ff02ffff04ffff04ffff04ff2fff0580ffff04ff5fff82017f8080ffff04ffff02ff7affff04ff02ffff04ff0bffff04ff05ffff01ff808080808080ffff04ff17ffff04ff81bfffff04ff82017fffff04ffff0bff8204ffffff02ff36ffff04ff02ffff04ff09ffff04ff820affffff04ffff0bff2cff2d80ffff04ff15ff80808080808080ff8216ff80ffff04ff8205ffffff04ff820bffff808080808080808080808080ff02ff2affff04ff02ffff04ff5fffff04ff3bffff04ffff02ffff03ff17ffff01ff09ff2dffff0bff27ffff02ff36ffff04ff02ffff04ff29ffff04ff57ffff04ffff0bff2cff81b980ffff04ff59ff80808080808080ff81b78080ff8080ff0180ffff04ff17ffff04ff05ffff04ff8202ffffff04ffff04ffff04ff24ffff04ffff0bff7cff2fff82017f80ff808080ffff04ffff04ff30ffff04ffff0bff81bfffff0bff7cff15ffff10ff82017fffff11ff8202dfff2b80ff8202ff808080ff808080ff138080ff80808080808080808080ff018080
|
1
chia/wallet/puzzles/cat.clvm.hex.sha256tree
Normal file
1
chia/wallet/puzzles/cat.clvm.hex.sha256tree
Normal file
@ -0,0 +1 @@
|
||||
72dec062874cd4d3aab892a0906688a1ae412b0109982e1797a170add88bdcdc
|
@ -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__)
|
31
chia/wallet/puzzles/cat_truths.clib
Normal file
31
chia/wallet/puzzles/cat_truths.clib
Normal 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)))))
|
||||
)
|
@ -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
|
||||
)
|
||||
)
|
@ -1 +0,0 @@
|
||||
ff02ffff01ff02ff7affff04ff02ffff04ffff04ff05ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ffff04ff0bffff04ffff02ff2effff04ff02ffff04ff0bff80808080ff8080808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffff808080808080808080ffff04ffff01ffffffff3d46ff333cffffff02ff5effff04ff02ffff04ffff02ff2cffff04ff02ffff04ff09ffff04ff15ffff04ff5dffff04ff0bff80808080808080ffff04ff09ffff04ff15ffff04ff5dffff04ff0bff8080808080808080ff0bff09ff15ff2d80ffff02ff5cffff04ff02ffff04ff05ffff04ff07ff8080808080ffff04ffff0102ffff04ffff04ffff0101ff0580ffff04ffff02ff7cffff04ff02ffff04ff0bffff01ff0180808080ff80808080ff02ffff03ff05ffff01ff04ffff0104ffff04ffff04ffff0101ff0980ffff04ffff02ff7cffff04ff02ffff04ff0dffff04ff0bff8080808080ff80808080ffff010b80ff0180ffffff2dff02ffff03ff15ffff01ff02ff5affff04ff02ffff04ff0bffff04ff09ffff04ff1dff808080808080ffff01ff02ffff02ff22ffff04ff02ffff04ff0bff80808080ffff04ff0bffff04ff09ffff04ff1dff808080808080ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ff05ff1380ffff01ff0101ffff01ff02ff2affff04ff02ffff04ff05ffff04ff1bff808080808080ff0180ff8080ff0180ffff09ff13ffff0bff27ffff02ff24ffff04ff02ffff04ff05ffff04ff57ff8080808080ff81b78080ff02ffff03ffff02ff32ffff04ff02ffff04ff17ffff04ff05ff8080808080ffff01ff02ffff03ffff02ff32ffff04ff02ffff04ff2fffff04ff05ff8080808080ffff01ff02ffff03ffff02ff32ffff04ff02ffff04ff5fffff04ff05ff8080808080ffff01ff04ffff04ff30ffff04ffff02ff34ffff04ff02ffff04ff4fff80808080ff808080ffff04ffff04ff38ffff04ffff02ff2effff04ff02ffff04ffff04ff27ffff04ff81bfff808080ff80808080ff808080ffff04ffff04ff20ffff04ffff0bffff02ff34ffff04ff02ffff04ff819fff80808080ffff02ff2effff04ff02ffff04ffff04ff4fffff04ffff10ff81bfffff11ff8202cfffff02ff36ffff04ff02ffff04ff0bff808080808080ff808080ff8080808080ff808080ffff02ff26ffff04ff02ffff04ff0bffff04ff05ff8080808080808080ffff01ff088080ff0180ffff01ff088080ff0180ffff01ff088080ff0180ffffff02ffff03ff05ffff01ff04ffff02ffff03ffff09ff11ff2880ffff01ff04ff28ffff04ffff02ff24ffff04ff02ffff04ff0bffff04ff29ff8080808080ffff04ff59ff80808080ffff01ff02ffff03ffff09ff11ff3880ffff01ff0880ffff010980ff018080ff0180ffff02ff26ffff04ff02ffff04ff0dffff04ff0bff808080808080ff8080ff0180ff02ffff03ff05ffff01ff10ffff02ffff03ffff09ff11ff2880ffff0159ff8080ff0180ffff02ff36ffff04ff02ffff04ff0dff8080808080ff8080ff0180ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ff7effff04ff02ffff04ff05ffff04ff07ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff7effff04ff02ffff04ff09ffff04ff0bff8080808080ffff02ff7effff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff02ffff03ffff02ff2affff04ff02ffff04ff05ffff04ff0bff8080808080ffff0105ffff01ff0bffff0101ff058080ff018080ff0180ff018080
|
@ -1 +0,0 @@
|
||||
d4596fa7aa6eaa267ebce8d527546827de083d58fb4e14f4137c2448f7252e5c
|
1
chia/wallet/puzzles/condition_codes.clvm.hex
Normal file
1
chia/wallet/puzzles/condition_codes.clvm.hex
Normal file
@ -0,0 +1 @@
|
||||
can't compile ("defconstant" "AGG_SIG_UNSAFE" 49), unknown operator
|
1
chia/wallet/puzzles/create-lock-puzzlehash.clvm.hex
Normal file
1
chia/wallet/puzzles/create-lock-puzzlehash.clvm.hex
Normal file
@ -0,0 +1 @@
|
||||
can't compile ("my-id"), unknown operator
|
25
chia/wallet/puzzles/delegated_genesis_checker.clvm
Normal file
25
chia/wallet/puzzles/delegated_genesis_checker.clvm
Normal 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))))))
|
||||
)
|
||||
)
|
1
chia/wallet/puzzles/delegated_genesis_checker.clvm.hex
Normal file
1
chia/wallet/puzzles/delegated_genesis_checker.clvm.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff82027fff80808080ff80808080ffff02ff82027fffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfff82057f80808080808080ffff04ffff01ff31ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080
|
@ -0,0 +1 @@
|
||||
999c3696e167f8a79d938adc11feba3a3dcb39ccff69a426d570706e7b8ec399
|
25
chia/wallet/puzzles/delegated_tail.clvm
Normal file
25
chia/wallet/puzzles/delegated_tail.clvm
Normal 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))))))
|
||||
)
|
||||
)
|
1
chia/wallet/puzzles/delegated_tail.clvm.hex
Normal file
1
chia/wallet/puzzles/delegated_tail.clvm.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff82027fff80808080ff80808080ffff02ff82027fffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfff82057f80808080808080ffff04ffff01ff31ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080
|
1
chia/wallet/puzzles/delegated_tail.clvm.hex.sha256tree
Normal file
1
chia/wallet/puzzles/delegated_tail.clvm.hex.sha256tree
Normal file
@ -0,0 +1 @@
|
||||
999c3696e167f8a79d938adc11feba3a3dcb39ccff69a426d570706e7b8ec399
|
15
chia/wallet/puzzles/everything_with_signature.clvm
Normal file
15
chia/wallet/puzzles/everything_with_signature.clvm
Normal 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
|
||||
)
|
1
chia/wallet/puzzles/everything_with_signature.clvm.hex
Normal file
1
chia/wallet/puzzles/everything_with_signature.clvm.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff01ff04ffff04ff02ffff04ff05ffff04ff5fff80808080ff8080ffff04ffff0132ff018080
|
@ -0,0 +1 @@
|
||||
1720d13250a7c16988eaf530331cefa9dd57a76b2c82236bec8bbbff91499b89
|
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -1 +1 @@
|
||||
ff02ffff03ffff09ff5bff8080ffff01ff0101ffff01ff02ffff03ffff09ff13ff0280ffff01ff0101ff8080ff018080ff0180
|
||||
ff02ffff03ff2fffff01ff0880ffff01ff02ffff03ffff09ff2dff0280ff80ffff01ff088080ff018080ff0180
|
@ -1 +1 @@
|
||||
258008f81f21c270f4b58488b108a46a35e5df43ca5b0313ac83e900a5e44a5f
|
||||
493afb89eed93ab86741b2aa61b8f5de495d33ff9b781dfc8919e602b2afa150
|
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -1 +1 @@
|
||||
ff02ffff03ffff09ff5bff8080ffff01ff0101ffff01ff02ffff03ffff02ffff03ffff09ffff0bff47ff81a7ff82016780ff1380ffff01ff02ffff03ffff09ff81a7ff0280ffff01ff0101ff8080ff0180ff8080ff0180ffff01ff0101ff8080ff018080ff0180
|
||||
ff02ffff03ff2fffff01ff0880ffff01ff02ffff03ffff09ffff0bff82013fff02ff8202bf80ff2d80ff80ffff01ff088080ff018080ff0180
|
@ -1 +1 @@
|
||||
795964e0324fbc08e8383d67659194a70455956ad1ebd2329ccf20008da00936
|
||||
de5a6e06d41518be97ff6365694f4f89475dda773dede267caa33da63b434e36
|
26
chia/wallet/puzzles/genesis_by_coin_id.clvm
Normal file
26
chia/wallet/puzzles/genesis_by_coin_id.clvm
Normal 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)
|
||||
)
|
||||
)
|
||||
|
||||
)
|
1
chia/wallet/puzzles/genesis_by_coin_id.clvm.hex
Normal file
1
chia/wallet/puzzles/genesis_by_coin_id.clvm.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff03ff2fffff01ff0880ffff01ff02ffff03ffff09ff2dff0280ff80ffff01ff088080ff018080ff0180
|
@ -0,0 +1 @@
|
||||
493afb89eed93ab86741b2aa61b8f5de495d33ff9b781dfc8919e602b2afa150
|
@ -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)
|
24
chia/wallet/puzzles/genesis_by_puzzle_hash.clvm
Normal file
24
chia/wallet/puzzles/genesis_by_puzzle_hash.clvm
Normal 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)
|
||||
)
|
||||
)
|
||||
)
|
1
chia/wallet/puzzles/genesis_by_puzzle_hash.clvm.hex
Normal file
1
chia/wallet/puzzles/genesis_by_puzzle_hash.clvm.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff03ff2fffff01ff0880ffff01ff02ffff03ffff09ffff0bff82013fff02ff8202bf80ff2d80ff80ffff01ff088080ff018080ff0180
|
@ -0,0 +1 @@
|
||||
de5a6e06d41518be97ff6365694f4f89475dda773dede267caa33da63b434e36
|
@ -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)
|
208
chia/wallet/puzzles/genesis_checkers.py
Normal file
208
chia/wallet/puzzles/genesis_checkers.py
Normal 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, []
|
@ -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
|
||||
|
@ -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]
|
||||
|
||||
|
||||
|
45
chia/wallet/puzzles/settlement_payments.clvm
Normal file
45
chia/wallet/puzzles/settlement_payments.clvm
Normal 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)
|
||||
)
|
1
chia/wallet/puzzles/settlement_payments.clvm.hex
Normal file
1
chia/wallet/puzzles/settlement_payments.clvm.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080
|
@ -0,0 +1 @@
|
||||
bae24162efbd568f89bc7a340798a6118df0189eb9e3f8697bcea27af99f8f79
|
@ -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
|
||||
|
@ -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,
|
||||
|
206
chia/wallet/puzzles/tails.py
Normal file
206
chia/wallet/puzzles/tails.py
Normal 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, []
|
@ -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()
|
@ -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
|
||||
|
@ -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__]
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
398
chia/wallet/trading/offer.py
Normal file
398
chia/wallet/trading/offer.py
Normal 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)
|
@ -5,6 +5,6 @@ class TradeStatus(Enum):
|
||||
PENDING_ACCEPT = 0
|
||||
PENDING_CONFIRM = 1
|
||||
PENDING_CANCEL = 2
|
||||
CANCELED = 3
|
||||
CANCELLED = 3
|
||||
CONFIRMED = 4
|
||||
FAILED = 5
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
208
chia/wallet/util/wallet_sync_utils.py
Normal file
208
chia/wallet/util/wallet_sync_utils.py
Normal 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
|
@ -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]
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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]
|
||||
|
@ -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
@ -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
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user