chia-blockchain/chia/wallet/did_wallet/did_wallet.py

891 lines
36 KiB
Python

import logging
import time
import json
from typing import Dict, Optional, List, Any, Set, Tuple, Union
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.types.blockchain_format.coin import Coin
from chia.types.coin_solution import CoinSolution
from chia.types.blockchain_format.program import Program
from chia.types.spend_bundle import SpendBundle
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.wallet.util.transaction_type import TransactionType
from chia.util.ints import uint64, uint32, uint8
from chia.wallet.did_wallet.did_info import DIDInfo
from chia.wallet.cc_wallet.ccparent import CCParent
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.wallet_types import WalletType
from chia.wallet.wallet import Wallet
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
class DIDWallet:
wallet_state_manager: Any
log: logging.Logger
wallet_info: WalletInfo
did_info: DIDInfo
standard_wallet: Wallet
base_puzzle_program: Optional[bytes]
base_inner_puzzle_hash: Optional[bytes32]
wallet_id: int
@staticmethod
async def create_new_did_wallet(
wallet_state_manager: Any,
wallet: Wallet,
amount: int,
backups_ids: List = [],
num_of_backup_ids_needed: uint64 = None,
name: str = None,
):
"""
This must be called under the wallet state manager lock
"""
self = DIDWallet()
self.base_puzzle_program = None
self.base_inner_puzzle_hash = None
self.standard_wallet = wallet
if name:
self.log = logging.getLogger(name)
else:
self.log = logging.getLogger(__name__)
if amount & 1 == 0:
raise ValueError("DID amount must be odd number")
self.wallet_state_manager = wallet_state_manager
if num_of_backup_ids_needed is None:
num_of_backup_ids_needed = uint64(len(backups_ids))
if num_of_backup_ids_needed > len(backups_ids):
raise ValueError("Cannot require more IDs than are known.")
self.did_info = DIDInfo(None, backups_ids, num_of_backup_ids_needed, [], None, None, None, None)
info_as_string = json.dumps(self.did_info.to_json_dict())
self.wallet_info = await wallet_state_manager.user_store.create_wallet(
"DID Wallet", WalletType.DISTRIBUTED_ID.value, info_as_string
)
if self.wallet_info is None:
raise ValueError("Internal Error")
self.wallet_id = self.wallet_info.id
bal = await self.standard_wallet.get_confirmed_balance()
if amount > bal:
raise ValueError("Not enough balance")
spend_bundle = await self.generate_new_decentralised_id(uint64(amount))
if spend_bundle is None:
raise ValueError("failed to generate ID for wallet")
await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id)
assert self.did_info.origin_coin is not None
did_puzzle_hash = did_wallet_puzzles.create_fullpuz(
self.did_info.current_inner, self.did_info.origin_coin.puzzle_hash
).get_tree_hash()
did_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=did_puzzle_hash,
amount=uint64(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(),
)
regular_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=did_puzzle_hash,
amount=uint64(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(),
)
await self.standard_wallet.push_transaction(regular_record)
await self.standard_wallet.push_transaction(did_record)
return self
@staticmethod
async def create_new_did_wallet_from_recovery(
wallet_state_manager: Any,
wallet: Wallet,
filename: str,
name: str = None,
):
self = DIDWallet()
self.base_puzzle_program = None
self.base_inner_puzzle_hash = None
self.standard_wallet = wallet
if name:
self.log = logging.getLogger(name)
else:
self.log = logging.getLogger(__name__)
self.wallet_state_manager = wallet_state_manager
self.did_info = DIDInfo(None, [], uint64(0), [], None, None, None, None)
info_as_string = json.dumps(self.did_info.to_json_dict())
self.wallet_info = await wallet_state_manager.user_store.create_wallet(
"DID Wallet", WalletType.DISTRIBUTED_ID.value, info_as_string
)
await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id)
# load backup will also set our DIDInfo
await self.load_backup(filename)
if self.wallet_info is None:
raise ValueError("Internal Error")
self.wallet_id = self.wallet_info.id
return self
@staticmethod
async def create(
wallet_state_manager: Any,
wallet: Wallet,
wallet_info: WalletInfo,
name: str = None,
):
self = DIDWallet()
if name:
self.log = logging.getLogger(name)
else:
self.log = logging.getLogger(__name__)
self.wallet_state_manager = wallet_state_manager
self.wallet_info = wallet_info
self.wallet_id = wallet_info.id
self.standard_wallet = wallet
self.wallet_info = wallet_info
self.did_info = DIDInfo.from_json_dict(json.loads(wallet_info.data))
self.base_puzzle_program = None
self.base_inner_puzzle_hash = None
return self
@classmethod
def type(cls) -> uint8:
return uint8(WalletType.DISTRIBUTED_ID)
def id(self):
return self.wallet_info.id
async def get_confirmed_balance(self, record_list=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:
parent = await self.get_parent_for_coin(record.coin)
if parent is not None:
amount = uint64(amount + record.coin.amount)
self.log.info(f"Confirmed balance for did wallet is {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:
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_unconfirmed_balance(self, record_list=None) -> uint64:
confirmed = await self.get_confirmed_balance(record_list)
unconfirmed_tx: List[TransactionRecord] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(
self.wallet_info.id
)
addition_amount = 0
removal_amount = 0
for record in unconfirmed_tx:
if record.type == 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 did wallet is {result}")
return uint64(result)
async def select_coins(self, amount, exclude: List[Coin] = None) -> Optional[Set[Coin]]:
"""Returns a set of coins that can be used for generating a new transaction."""
if exclude is None:
exclude = []
spendable_amount = await self.get_spendable_balance()
if amount > spendable_amount:
self.log.warning(f"Can't select {amount}, from spendable {spendable_amount} for wallet id {self.id()}")
return None
self.log.info(f"About to select coins for amount {amount}")
unspent: List[WalletCoinRecord] = list(
await self.wallet_state_manager.get_spendable_coins_for_wallet(self.wallet_info.id)
)
sum_value = 0
used_coins: Set = set()
# Use older coins first
unspent.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.wallet_info.id
)
for coinrecord in unspent:
if sum_value >= amount and len(used_coins) > 0:
break
if coinrecord.coin.name() in unconfirmed_removals:
continue
if coinrecord.coin in exclude:
continue
sum_value += coinrecord.coin.amount
used_coins.add(coinrecord.coin)
# 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_value < 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
# This will be used in the recovery case where we don't have the parent info already
async def coin_added(self, coin: Coin, header_hash: bytes32, removals: List[Coin], height: int):
"""Notification from wallet state manager that wallet has been received."""
self.log.info("DID wallet has been notified that coin was added")
inner_puzzle = await self.inner_puzzle_for_did_puzzle(coin.puzzle_hash)
new_info = DIDInfo(
self.did_info.origin_coin,
self.did_info.backup_ids,
self.did_info.num_of_backup_ids_needed,
self.did_info.parent_info,
inner_puzzle,
None,
None,
None,
)
await self.save_info(new_info, True)
future_parent = CCParent(
coin.parent_coin_info,
inner_puzzle.get_tree_hash(),
coin.amount,
)
await self.add_parent(coin.name(), future_parent, True)
def create_backup(self, filename: str):
assert self.did_info.current_inner is not None
assert self.did_info.origin_coin is not None
try:
f = open(filename, "w")
output_str = f"{self.did_info.origin_coin.parent_coin_info}:"
output_str += f"{self.did_info.origin_coin.puzzle_hash}:"
output_str += f"{self.did_info.origin_coin.amount}:"
for did in self.did_info.backup_ids:
output_str = output_str + did.hex() + ","
output_str = output_str[:-1]
output_str = (
output_str + f":{bytes(self.did_info.current_inner).hex()}:{self.did_info.num_of_backup_ids_needed}"
)
f.write(output_str)
f.close()
except Exception as e:
raise e
return None
async def load_backup(self, filename: str):
try:
f = open(filename, "r")
details = f.readline().split(":")
f.close()
origin = Coin(bytes.fromhex(details[0]), bytes.fromhex(details[1]), uint64(int(details[2])))
backup_ids = []
for d in details[3].split(","):
backup_ids.append(bytes.fromhex(d))
num_of_backup_ids_needed = uint64(int(details[5]))
if num_of_backup_ids_needed > len(backup_ids):
raise Exception
innerpuz = Program.from_bytes(bytes.fromhex(details[4]))
did_info = DIDInfo(
origin,
backup_ids,
num_of_backup_ids_needed,
self.did_info.parent_info,
innerpuz,
None,
None,
None,
)
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.puzzle_hash)
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)
# All additions in this block here:
new_puzhash = (await self.get_new_puzzle()).get_tree_hash()
new_pubkey = bytes(
(await self.wallet_state_manager.get_unused_derivation_record(self.wallet_info.id)).pubkey
)
all_parents: 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)
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 = CCParent(
coin.parent_coin_info,
innerpuz.get_tree_hash(),
coin.amount,
)
await self.add_parent(coin.name(), future_parent, False)
if coin.name() in all_parents:
continue
did_info = DIDInfo(
origin,
backup_ids,
num_of_backup_ids_needed,
self.did_info.parent_info,
innerpuz,
coin,
new_puzhash,
new_pubkey,
)
await self.save_info(did_info, False)
return None
except Exception as e:
raise e
def puzzle_for_pk(self, pubkey: bytes) -> Program:
innerpuz = did_wallet_puzzles.create_innerpuz(
pubkey, self.did_info.backup_ids, self.did_info.num_of_backup_ids_needed
)
if self.did_info.origin_coin is not None:
return did_wallet_puzzles.create_fullpuz(innerpuz, self.did_info.origin_coin.puzzle_hash)
else:
return did_wallet_puzzles.create_fullpuz(innerpuz, 0x00)
async def get_new_puzzle(self) -> Program:
return self.puzzle_for_pk(
bytes((await self.wallet_state_manager.get_unused_derivation_record(self.wallet_info.id)).pubkey)
)
def get_my_DID(self) -> str:
assert self.did_info.origin_coin is not None
core = self.did_info.origin_coin.puzzle_hash
assert core is not None
return core.hex()
# This is used to cash out, or update the id_list
async def create_spend(self, puzhash: bytes32):
assert self.did_info.current_inner is not None
assert self.did_info.origin_coin is not None
coins = await self.select_coins(1)
assert coins is not None
coin = coins.pop()
# innerpuz solution is (mode amount new_puz identity my_puz)
innersol: Program = Program.to([0, coin.amount, puzhash, coin.name(), coin.puzzle_hash])
# full solution is (corehash parent_info my_amount innerpuz_reveal solution)
innerpuz: Program = self.did_info.current_inner
full_puzzle: Program = did_wallet_puzzles.create_fullpuz(
innerpuz,
self.did_info.origin_coin.puzzle_hash,
)
parent_info = await self.get_parent_for_coin(coin)
assert parent_info is not None
fullsol = Program.to(
[
[self.did_info.origin_coin.parent_coin_info, self.did_info.origin_coin.amount],
[
parent_info.parent_name,
parent_info.inner_puzzle_hash,
parent_info.amount,
],
coin.amount,
innersol,
]
)
list_of_solutions = [CoinSolution(coin, full_puzzle, fullsol)]
# sign for AGG_SIG_ME
message = (
Program.to([coin.amount, puzhash]).get_tree_hash()
+ 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)
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)
did_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=puzhash,
amount=uint64(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_info.id,
sent_to=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=token_bytes(),
)
await self.standard_wallet.push_transaction(did_record)
return spend_bundle
# Pushes the a SpendBundle to create a message coin on the blockchain
# Returns a SpendBundle for the recoverer to spend the message coin
async def create_attestment(
self, recovering_coin_name: bytes32, newpuz: bytes32, pubkey: G1Element, filename=None
) -> SpendBundle:
assert self.did_info.current_inner is not None
assert self.did_info.origin_coin is not None
coins = await self.select_coins(1)
assert coins is not None and coins != set()
coin = coins.pop()
message = did_wallet_puzzles.create_recovery_message_puzzle(recovering_coin_name, newpuz, pubkey)
innermessage = message.get_tree_hash()
# innerpuz solution is (mode amount new_puz identity my_puz)
innersol = Program.to([1, coin.amount, innermessage, recovering_coin_name, coin.puzzle_hash])
# full solution is (corehash parent_info my_amount innerpuz_reveal solution)
innerpuz: Program = self.did_info.current_inner
full_puzzle: Program = did_wallet_puzzles.create_fullpuz(
innerpuz,
self.did_info.origin_coin.puzzle_hash,
)
parent_info = await self.get_parent_for_coin(coin)
assert parent_info is not None
fullsol = Program.to(
[
[self.did_info.origin_coin.parent_coin_info, self.did_info.origin_coin.amount],
[
parent_info.parent_name,
parent_info.inner_puzzle_hash,
parent_info.amount,
],
coin.amount,
innersol,
]
)
list_of_solutions = [CoinSolution(coin, full_puzzle, fullsol)]
message_spend = did_wallet_puzzles.create_spend_for_message(coin.name(), recovering_coin_name, newpuz, pubkey)
message_spend_bundle = SpendBundle([message_spend], AugSchemeMPL.aggregate([]))
# sign for AGG_SIG_ME
to_sign = Program.to([coin.puzzle_hash, coin.amount, innermessage]).get_tree_hash()
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)
signature = AugSchemeMPL.sign(private, message)
# assert signature.validate([signature.PkMessagePair(pubkey, message)])
spend_bundle = SpendBundle(list_of_solutions, signature)
did_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=coin.puzzle_hash,
amount=uint64(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_info.id,
sent_to=[],
trade_id=None,
type=uint32(TransactionType.INCOMING_TX.value),
name=token_bytes(),
)
await self.standard_wallet.push_transaction(did_record)
if filename is not None:
f = open(filename, "w")
f.write(self.get_my_DID())
f.write(":")
f.write(bytes(message_spend_bundle).hex())
f.write(":")
parent = coin.parent_coin_info.hex()
innerpuzhash = self.did_info.current_inner.get_tree_hash().hex()
amount = coin.amount
f.write(parent)
f.write(":")
f.write(innerpuzhash)
f.write(":")
f.write(str(amount))
f.close()
return message_spend_bundle
# this is just for testing purposes, API should use create_attestment_now
async def get_info_for_recovery(self):
coins = await self.select_coins(1)
coin = coins.pop()
parent = coin.parent_coin_info
innerpuzhash = self.did_info.current_inner.get_tree_hash()
amount = coin.amount
return [parent, innerpuzhash, amount]
async def load_attest_files_for_recovery_spend(self, filenames):
spend_bundle_list = []
info_dict = {}
try:
for i in filenames:
f = open(i)
info = f.read().split(":")
info_dict[info[0]] = [
bytes.fromhex(info[2]),
bytes.fromhex(info[3]),
uint64(info[4]),
]
new_sb = SpendBundle.from_bytes(bytes.fromhex(info[1]))
spend_bundle_list.append(new_sb)
f.close()
# info_dict {0xidentity: "(0xparent_info 0xinnerpuz amount)"}
my_recovery_list: List[bytes] = self.did_info.backup_ids
# convert info dict into recovery list - same order as wallet
info_list = []
for entry in my_recovery_list:
if entry.hex() in info_dict:
info_list.append(
[
info_dict[entry.hex()][0],
info_dict[entry.hex()][1],
info_dict[entry.hex()][2],
]
)
else:
info_list.append([])
message_spend_bundle = SpendBundle.aggregate(spend_bundle_list)
return info_list, message_spend_bundle
except Exception:
raise
async def recovery_spend(
self,
coin: Coin,
puzhash: bytes,
parent_innerpuzhash_amounts_for_recovery_ids: List[Tuple[bytes, bytes, int]],
pubkey: G1Element,
spend_bundle: SpendBundle,
) -> SpendBundle:
assert self.did_info.origin_coin is not None
# innerpuz solution is (mode amount new_puz identity my_puz parent_innerpuzhash_amounts_for_recovery_ids)
innersol = Program.to(
[
2,
coin.amount,
puzhash,
coin.name(),
coin.puzzle_hash,
parent_innerpuzhash_amounts_for_recovery_ids,
bytes(pubkey),
self.did_info.backup_ids,
self.did_info.num_of_backup_ids_needed,
]
)
# full solution is (parent_info my_amount solution)
innerpuz = self.did_info.current_inner
full_puzzle: Program = did_wallet_puzzles.create_fullpuz(
innerpuz,
self.did_info.origin_coin.puzzle_hash,
)
parent_info = await self.get_parent_for_coin(coin)
assert parent_info is not None
fullsol = Program.to(
[
[self.did_info.origin_coin.parent_coin_info, self.did_info.origin_coin.amount],
[
parent_info.parent_name,
parent_info.inner_puzzle_hash,
parent_info.amount,
],
coin.amount,
innersol,
]
)
list_of_solutions = [CoinSolution(coin, full_puzzle, fullsol)]
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)
message = bytes(puzhash)
sigs = [AugSchemeMPL.sign(private, message)]
for _ in spend_bundle.coin_solutions:
sigs.append(AugSchemeMPL.sign(private, message))
aggsig = AugSchemeMPL.aggregate(sigs)
# assert AugSchemeMPL.verify(pubkey, message, aggsig)
if spend_bundle is None:
spend_bundle = SpendBundle(list_of_solutions, aggsig)
else:
spend_bundle = spend_bundle.aggregate([spend_bundle, SpendBundle(list_of_solutions, aggsig)])
did_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=puzhash,
amount=uint64(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_info.id,
sent_to=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=token_bytes(),
)
await self.standard_wallet.push_transaction(did_record)
return spend_bundle
async def get_new_innerpuz(self) -> Program:
devrec = await self.wallet_state_manager.get_unused_derivation_record(self.standard_wallet.id())
pubkey = bytes(devrec.pubkey)
innerpuz = did_wallet_puzzles.create_innerpuz(
pubkey,
self.did_info.backup_ids,
uint64(self.did_info.num_of_backup_ids_needed),
)
return innerpuz
async def get_new_inner_hash(self) -> bytes32:
innerpuz = await self.get_new_innerpuz()
return innerpuz.get_tree_hash()
async def get_innerhash_for_pubkey(self, pubkey: bytes):
innerpuz = did_wallet_puzzles.create_innerpuz(
pubkey,
self.did_info.backup_ids,
uint64(self.did_info.num_of_backup_ids_needed),
)
return innerpuz.get_tree_hash()
async def inner_puzzle_for_did_puzzle(self, did_hash: bytes32) -> Program:
record: DerivationRecord = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(
did_hash.hex()
)
inner_puzzle: Program = did_wallet_puzzles.create_innerpuz(
bytes(record.pubkey),
self.did_info.backup_ids,
self.did_info.num_of_backup_ids_needed,
)
return inner_puzzle
async def get_parent_for_coin(self, coin) -> Optional[CCParent]:
parent_info = None
for name, ccparent in self.did_info.parent_info:
if name == coin.parent_coin_info:
parent_info = ccparent
return parent_info
async def generate_new_decentralised_id(self, amount: uint64) -> Optional[SpendBundle]:
"""
This must be called under the wallet state manager lock
"""
coins = await self.standard_wallet.select_coins(amount)
if coins is None:
return None
origin = coins.copy().pop()
did_inner: Program = await self.get_new_innerpuz()
did_inner_hash = did_inner.get_tree_hash()
did_puz = did_wallet_puzzles.create_fullpuz(did_inner, origin.puzzle_hash)
did_puzzle_hash = did_puz.get_tree_hash()
tx_record: Optional[TransactionRecord] = await self.standard_wallet.generate_signed_transaction(
amount, did_puzzle_hash, uint64(0), origin.name(), coins
)
eve_coin = Coin(origin.name(), did_puzzle_hash, amount)
future_parent = CCParent(
eve_coin.parent_coin_info,
did_inner_hash,
eve_coin.amount,
)
eve_parent = CCParent(
origin.parent_coin_info,
origin.puzzle_hash,
origin.amount,
)
await self.add_parent(eve_coin.parent_coin_info, eve_parent, False)
await self.add_parent(eve_coin.name(), future_parent, False)
if tx_record is None or tx_record.spend_bundle is None:
return None
# Only want to save this information if the transaction is valid
did_info: DIDInfo = DIDInfo(
origin,
self.did_info.backup_ids,
self.did_info.num_of_backup_ids_needed,
self.did_info.parent_info,
did_inner,
None,
None,
None,
)
await self.save_info(did_info, False)
eve_spend = await self.generate_eve_spend(eve_coin, did_puz, did_inner)
full_spend = SpendBundle.aggregate([tx_record.spend_bundle, eve_spend])
return full_spend
async def generate_eve_spend(self, coin: Coin, full_puzzle: Program, innerpuz: Program):
assert self.did_info.origin_coin is not None
# innerpuz solution is (mode amount message my_id my_puzhash parent_innerpuzhash_amounts_for_recovery_ids)
innersol = Program.to([0, coin.amount, coin.puzzle_hash, coin.name(), coin.puzzle_hash, []])
# full solution is (parent_info my_amount innersolution)
fullsol = Program.to(
[
[self.did_info.origin_coin.parent_coin_info, self.did_info.origin_coin.amount],
coin.parent_coin_info,
coin.amount,
innersol,
]
)
list_of_solutions = [CoinSolution(coin, full_puzzle, fullsol)]
# sign for AGG_SIG_ME
message = (
Program.to([coin.amount, coin.puzzle_hash]).get_tree_hash()
+ 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)
signature = AugSchemeMPL.sign(private, message)
sigs = [signature]
aggsig = AugSchemeMPL.aggregate(sigs)
spend_bundle = SpendBundle(list_of_solutions, aggsig)
return spend_bundle
async def get_frozen_amount(self) -> uint64:
return await self.wallet_state_manager.get_frozen_balance(self.wallet_info.id)
async def get_spendable_balance(self, unspent_records=None) -> uint64:
spendable_am = await self.wallet_state_manager.get_confirmed_spendable_balance_for_wallet(
self.wallet_info.id, unspent_records
)
return spendable_am
async def get_max_send_amount(self, records=None):
max_send_amount = await self.get_confirmed_balance()
return max_send_amount
async def add_parent(self, name: bytes32, parent: Optional[CCParent], in_transaction: bool):
self.log.info(f"Adding parent {name}: {parent}")
current_list = self.did_info.parent_info.copy()
current_list.append((name, parent))
did_info: DIDInfo = DIDInfo(
self.did_info.origin_coin,
self.did_info.backup_ids,
self.did_info.num_of_backup_ids_needed,
current_list,
self.did_info.current_inner,
self.did_info.temp_coin,
self.did_info.temp_puzhash,
self.did_info.temp_pubkey,
)
await self.save_info(did_info, in_transaction)
async def update_recovery_list(self, recover_list: List[bytes], num_of_backup_ids_needed: uint64):
if num_of_backup_ids_needed > len(recover_list):
return False
did_info: DIDInfo = DIDInfo(
self.did_info.origin_coin,
recover_list,
num_of_backup_ids_needed,
self.did_info.parent_info,
self.did_info.current_inner,
self.did_info.temp_coin,
self.did_info.temp_puzhash,
self.did_info.temp_pubkey,
)
await self.save_info(did_info, False)
await self.wallet_state_manager.update_wallet_puzzle_hashes(self.wallet_info.id)
return True
async def save_info(self, did_info: DIDInfo, in_transaction: bool):
self.did_info = did_info
current_info = self.wallet_info
data_str = json.dumps(did_info.to_json_dict())
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)