2020-03-27 17:30:07 +03:00
|
|
|
from typing import Dict, Optional, List, Tuple, Set, Any
|
2020-02-12 04:01:39 +03:00
|
|
|
import clvm
|
|
|
|
from blspy import ExtendedPrivateKey, PublicKey
|
2020-02-12 00:00:41 +03:00
|
|
|
import logging
|
2020-02-13 22:57:40 +03:00
|
|
|
from src.types.hashable.BLSSignature import BLSSignature
|
2020-03-06 02:53:36 +03:00
|
|
|
from src.types.hashable.coin import Coin
|
2020-02-27 05:23:03 +03:00
|
|
|
from src.types.hashable.coin_solution import CoinSolution
|
|
|
|
from src.types.hashable.program import Program
|
|
|
|
from src.types.hashable.spend_bundle import SpendBundle
|
2020-02-12 04:01:39 +03:00
|
|
|
from src.types.sized_bytes import bytes32
|
2020-02-13 22:57:40 +03:00
|
|
|
from src.util.condition_tools import (
|
|
|
|
conditions_for_solution,
|
|
|
|
conditions_by_opcode,
|
|
|
|
hash_key_pairs_for_conditions_dict,
|
|
|
|
)
|
2020-03-27 17:30:07 +03:00
|
|
|
from src.types.mempool_inclusion_status import MempoolInclusionStatus
|
|
|
|
from src.util.ints import uint64, uint32
|
2020-02-12 04:01:39 +03:00
|
|
|
from src.wallet.BLSPrivateKey import BLSPrivateKey
|
|
|
|
from src.wallet.puzzles.p2_conditions import puzzle_for_conditions
|
|
|
|
from src.wallet.puzzles.p2_delegated_puzzle import puzzle_for_pk
|
2020-02-13 22:57:40 +03:00
|
|
|
from src.wallet.puzzles.puzzle_utils import (
|
|
|
|
make_assert_my_coin_id_condition,
|
|
|
|
make_assert_time_exceeds_condition,
|
|
|
|
make_assert_coin_consumed_condition,
|
|
|
|
make_create_coin_condition,
|
|
|
|
)
|
2020-02-27 23:37:32 +03:00
|
|
|
from src.wallet.util.wallet_types import WalletType
|
2020-03-19 11:26:51 +03:00
|
|
|
from src.wallet.wallet_coin_record import WalletCoinRecord
|
2020-03-27 17:30:07 +03:00
|
|
|
from src.wallet.transaction_record import TransactionRecord
|
2020-03-19 11:26:51 +03:00
|
|
|
from src.wallet.wallet_info import WalletInfo
|
2020-02-24 20:16:47 +03:00
|
|
|
|
|
|
|
from src.wallet.wallet_state_manager import WalletStateManager
|
2020-02-12 00:00:41 +03:00
|
|
|
|
|
|
|
|
|
|
|
class Wallet:
|
|
|
|
private_key: ExtendedPrivateKey
|
|
|
|
key_config: Dict
|
|
|
|
config: Dict
|
2020-02-24 20:16:47 +03:00
|
|
|
wallet_state_manager: WalletStateManager
|
2020-02-12 00:00:41 +03:00
|
|
|
|
2020-02-13 22:57:40 +03:00
|
|
|
log: logging.Logger
|
|
|
|
|
2020-03-19 11:26:51 +03:00
|
|
|
wallet_info: WalletInfo
|
2020-02-13 23:59:03 +03:00
|
|
|
|
2020-02-13 22:57:40 +03:00
|
|
|
@staticmethod
|
2020-02-24 21:41:54 +03:00
|
|
|
async def create(
|
|
|
|
config: Dict,
|
|
|
|
key_config: Dict,
|
|
|
|
wallet_state_manager: WalletStateManager,
|
2020-03-19 11:26:51 +03:00
|
|
|
info: WalletInfo,
|
2020-02-24 21:41:54 +03:00
|
|
|
name: str = None,
|
|
|
|
):
|
2020-02-13 22:57:40 +03:00
|
|
|
self = Wallet()
|
2020-02-12 00:00:41 +03:00
|
|
|
self.config = config
|
|
|
|
self.key_config = key_config
|
|
|
|
sk_hex = self.key_config["wallet_sk"]
|
|
|
|
self.private_key = ExtendedPrivateKey.from_bytes(bytes.fromhex(sk_hex))
|
|
|
|
if name:
|
|
|
|
self.log = logging.getLogger(name)
|
|
|
|
else:
|
|
|
|
self.log = logging.getLogger(__name__)
|
|
|
|
|
2020-02-24 20:16:47 +03:00
|
|
|
self.wallet_state_manager = wallet_state_manager
|
2020-02-13 23:59:03 +03:00
|
|
|
|
2020-03-19 11:26:51 +03:00
|
|
|
self.wallet_info = info
|
|
|
|
|
2020-02-13 22:57:40 +03:00
|
|
|
return self
|
2020-02-12 04:01:39 +03:00
|
|
|
|
2020-03-27 17:30:07 +03:00
|
|
|
def get_public_key(self, index: uint32) -> PublicKey:
|
2020-02-27 23:37:32 +03:00
|
|
|
pubkey = self.private_key.public_child(index).get_public_key()
|
2020-02-12 04:01:39 +03:00
|
|
|
return pubkey
|
|
|
|
|
2020-02-13 23:59:03 +03:00
|
|
|
async def get_confirmed_balance(self) -> uint64:
|
2020-03-19 22:11:58 +03:00
|
|
|
return await self.wallet_state_manager.get_confirmed_balance_for_wallet(
|
|
|
|
self.wallet_info.id
|
|
|
|
)
|
2020-02-13 22:57:40 +03:00
|
|
|
|
2020-02-13 23:59:03 +03:00
|
|
|
async def get_unconfirmed_balance(self) -> uint64:
|
2020-03-19 22:11:58 +03:00
|
|
|
return await self.wallet_state_manager.get_unconfirmed_balance(
|
|
|
|
self.wallet_info.id
|
|
|
|
)
|
2020-02-13 23:59:03 +03:00
|
|
|
|
2020-02-27 23:37:32 +03:00
|
|
|
async def can_generate_puzzle_hash(self, hash: bytes32) -> bool:
|
2020-03-05 02:22:36 +03:00
|
|
|
return await self.wallet_state_manager.puzzle_store.puzzle_hash_exists(hash)
|
2020-02-12 04:01:39 +03:00
|
|
|
|
2020-03-27 17:30:07 +03:00
|
|
|
def puzzle_for_pk(self, pubkey: bytes) -> Program:
|
2020-02-12 04:01:39 +03:00
|
|
|
return puzzle_for_pk(pubkey)
|
|
|
|
|
2020-02-27 23:37:32 +03:00
|
|
|
async def get_new_puzzlehash(self) -> bytes32:
|
2020-03-05 02:22:36 +03:00
|
|
|
index = await self.wallet_state_manager.puzzle_store.get_max_derivation_path()
|
2020-02-27 23:37:32 +03:00
|
|
|
index += 1
|
2020-03-27 17:30:07 +03:00
|
|
|
pubkey: bytes = bytes(self.get_public_key(index))
|
2020-02-27 23:37:32 +03:00
|
|
|
puzzle: Program = self.puzzle_for_pk(pubkey)
|
2020-02-12 04:01:39 +03:00
|
|
|
puzzlehash: bytes32 = puzzle.get_hash()
|
2020-02-27 23:37:32 +03:00
|
|
|
|
2020-03-05 02:22:36 +03:00
|
|
|
await self.wallet_state_manager.puzzle_store.add_derivation_path_of_interest(
|
2020-03-19 11:26:51 +03:00
|
|
|
index, puzzlehash, pubkey, WalletType.STANDARD_WALLET, self.wallet_info.id
|
2020-02-27 23:39:59 +03:00
|
|
|
)
|
2020-02-27 23:37:32 +03:00
|
|
|
|
2020-02-12 04:01:39 +03:00
|
|
|
return puzzlehash
|
|
|
|
|
2020-02-13 22:57:40 +03:00
|
|
|
def make_solution(self, primaries=None, min_time=0, me=None, consumed=None):
|
2020-03-06 02:53:36 +03:00
|
|
|
condition_list = []
|
2020-02-13 22:57:40 +03:00
|
|
|
if primaries:
|
|
|
|
for primary in primaries:
|
2020-03-06 02:53:36 +03:00
|
|
|
condition_list.append(
|
2020-02-13 22:57:40 +03:00
|
|
|
make_create_coin_condition(primary["puzzlehash"], primary["amount"])
|
|
|
|
)
|
|
|
|
if consumed:
|
|
|
|
for coin in consumed:
|
2020-03-06 02:53:36 +03:00
|
|
|
condition_list.append(make_assert_coin_consumed_condition(coin))
|
2020-02-12 04:01:39 +03:00
|
|
|
if min_time > 0:
|
2020-03-06 02:53:36 +03:00
|
|
|
condition_list.append(make_assert_time_exceeds_condition(min_time))
|
2020-02-12 04:01:39 +03:00
|
|
|
if me:
|
2020-03-06 02:53:36 +03:00
|
|
|
condition_list.append(make_assert_my_coin_id_condition(me["id"]))
|
|
|
|
return clvm.to_sexp_f([puzzle_for_conditions(condition_list), []])
|
2020-02-12 04:01:39 +03:00
|
|
|
|
2020-02-27 23:39:59 +03:00
|
|
|
async def get_keys(
|
|
|
|
self, hash: bytes32
|
|
|
|
) -> Optional[Tuple[PublicKey, ExtendedPrivateKey]]:
|
2020-03-05 02:22:36 +03:00
|
|
|
index_for_puzzlehash = await self.wallet_state_manager.puzzle_store.index_for_puzzle_hash(
|
2020-02-27 23:39:59 +03:00
|
|
|
hash
|
|
|
|
)
|
2020-02-27 23:37:32 +03:00
|
|
|
if index_for_puzzlehash == -1:
|
|
|
|
raise
|
|
|
|
pubkey = self.private_key.public_child(index_for_puzzlehash).get_public_key()
|
|
|
|
private = self.private_key.private_child(index_for_puzzlehash).get_private_key()
|
|
|
|
return pubkey, private
|
2020-02-12 04:01:39 +03:00
|
|
|
|
2020-03-19 11:26:51 +03:00
|
|
|
async def select_coins(self, amount) -> Optional[Set[Coin]]:
|
|
|
|
""" Returns a set of coins that can be used for generating a new transaction. """
|
|
|
|
|
2020-03-19 22:11:58 +03:00
|
|
|
if (
|
|
|
|
amount
|
|
|
|
> await self.wallet_state_manager.get_unconfirmed_spendable_for_wallet(
|
2020-03-28 00:03:48 +03:00
|
|
|
self.wallet_info.id
|
2020-03-19 22:11:58 +03:00
|
|
|
)
|
|
|
|
):
|
2020-03-19 11:26:51 +03:00
|
|
|
return None
|
|
|
|
|
2020-03-19 22:11:58 +03:00
|
|
|
unspent: Set[
|
|
|
|
WalletCoinRecord
|
2020-03-28 00:03:48 +03:00
|
|
|
] = await self.wallet_state_manager.get_spendable_coins_for_wallet(
|
|
|
|
self.wallet_info.id
|
2020-03-19 11:26:51 +03:00
|
|
|
)
|
|
|
|
sum = 0
|
|
|
|
used_coins: Set = set()
|
|
|
|
|
|
|
|
# Try to use coins from the store, if there isn't enough of "unused"
|
|
|
|
# coins use change coins that are not confirmed yet
|
2020-03-28 00:03:48 +03:00
|
|
|
unconfirmed_removals: Dict[
|
|
|
|
bytes32, Coin
|
|
|
|
] = await self.wallet_state_manager.unconfirmed_removals_for_wallet(
|
2020-03-19 22:11:58 +03:00
|
|
|
self.wallet_info.id
|
|
|
|
)
|
2020-03-19 11:26:51 +03:00
|
|
|
for coinrecord in unspent:
|
|
|
|
if sum >= amount:
|
|
|
|
break
|
|
|
|
if coinrecord.coin.name() in unconfirmed_removals:
|
|
|
|
continue
|
|
|
|
sum += 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 < amount:
|
2020-03-28 00:03:48 +03:00
|
|
|
unconfirmed_additions = await self.wallet_state_manager.unconfirmed_additions_for_wallet(
|
|
|
|
self.wallet_info.id
|
|
|
|
)
|
|
|
|
for coin in unconfirmed_additions.values():
|
2020-03-19 11:26:51 +03:00
|
|
|
if sum > amount:
|
|
|
|
break
|
2020-03-28 00:03:48 +03:00
|
|
|
if coin.name() in unconfirmed_removals:
|
2020-03-19 11:26:51 +03:00
|
|
|
continue
|
2020-03-28 00:03:48 +03:00
|
|
|
|
2020-03-19 11:26:51 +03:00
|
|
|
sum += coin.amount
|
|
|
|
used_coins.add(coin)
|
|
|
|
|
|
|
|
if sum >= amount:
|
|
|
|
return used_coins
|
|
|
|
else:
|
|
|
|
# This shouldn't happen because of: if amount > self.get_unconfirmed_balance_spendable():
|
|
|
|
return None
|
|
|
|
|
2020-02-13 22:57:40 +03:00
|
|
|
async def generate_unsigned_transaction(
|
2020-03-23 22:59:53 +03:00
|
|
|
self,
|
2020-03-27 17:30:07 +03:00
|
|
|
amount: uint64,
|
2020-03-23 22:59:53 +03:00
|
|
|
newpuzzlehash: bytes32,
|
2020-03-27 17:30:07 +03:00
|
|
|
fee: uint64 = uint64(0),
|
2020-03-23 22:59:53 +03:00
|
|
|
origin_id: bytes32 = None,
|
|
|
|
coins: Set[Coin] = None,
|
2020-02-13 22:57:40 +03:00
|
|
|
) -> List[Tuple[Program, CoinSolution]]:
|
2020-03-06 02:53:36 +03:00
|
|
|
"""
|
|
|
|
Generates a unsigned transaction in form of List(Puzzle, Solutions)
|
|
|
|
"""
|
2020-03-23 22:46:17 +03:00
|
|
|
if coins is None:
|
|
|
|
coins = await self.select_coins(amount + fee)
|
|
|
|
if coins is None:
|
2020-02-13 22:57:40 +03:00
|
|
|
return []
|
2020-03-06 02:53:36 +03:00
|
|
|
|
2020-03-23 22:46:17 +03:00
|
|
|
spend_value = sum([coin.amount for coin in coins])
|
2020-02-12 04:01:39 +03:00
|
|
|
change = spend_value - amount - fee
|
2020-03-06 02:53:36 +03:00
|
|
|
|
|
|
|
spends: List[Tuple[Program, CoinSolution]] = []
|
|
|
|
output_created = False
|
|
|
|
|
2020-03-23 22:46:17 +03:00
|
|
|
for coin in coins:
|
2020-03-06 02:53:36 +03:00
|
|
|
# Get keys for puzzle_hash
|
2020-02-12 04:01:39 +03:00
|
|
|
puzzle_hash = coin.puzzle_hash
|
2020-02-27 23:37:32 +03:00
|
|
|
maybe = await self.get_keys(puzzle_hash)
|
2020-02-13 22:57:40 +03:00
|
|
|
if not maybe:
|
|
|
|
return []
|
2020-03-06 02:53:36 +03:00
|
|
|
|
|
|
|
# Get puzzle for pubkey
|
2020-02-13 22:57:40 +03:00
|
|
|
pubkey, secretkey = maybe
|
2020-03-27 17:30:07 +03:00
|
|
|
puzzle: Program = puzzle_for_pk(bytes(pubkey))
|
2020-03-06 02:53:36 +03:00
|
|
|
|
|
|
|
# Only one coin creates outputs
|
2020-03-23 22:46:17 +03:00
|
|
|
if output_created is False and origin_id is None:
|
|
|
|
primaries = [{"puzzlehash": newpuzzlehash, "amount": amount}]
|
|
|
|
if change > 0:
|
|
|
|
changepuzzlehash = await self.get_new_puzzlehash()
|
|
|
|
primaries.append({"puzzlehash": changepuzzlehash, "amount": change})
|
|
|
|
|
|
|
|
solution = self.make_solution(primaries=primaries)
|
|
|
|
output_created = True
|
|
|
|
elif output_created is False and origin_id == coin.name():
|
2020-02-13 22:57:40 +03:00
|
|
|
primaries = [{"puzzlehash": newpuzzlehash, "amount": amount}]
|
2020-02-12 04:01:39 +03:00
|
|
|
if change > 0:
|
2020-02-27 23:37:32 +03:00
|
|
|
changepuzzlehash = await self.get_new_puzzlehash()
|
2020-02-13 22:57:40 +03:00
|
|
|
primaries.append({"puzzlehash": changepuzzlehash, "amount": change})
|
2020-02-24 20:16:47 +03:00
|
|
|
|
2020-02-12 04:01:39 +03:00
|
|
|
solution = self.make_solution(primaries=primaries)
|
|
|
|
output_created = True
|
|
|
|
else:
|
2020-03-06 02:53:36 +03:00
|
|
|
# TODO coin consumed condition should be removed
|
2020-02-12 04:01:39 +03:00
|
|
|
solution = self.make_solution(consumed=[coin.name()])
|
2020-03-06 02:53:36 +03:00
|
|
|
|
2020-02-12 04:01:39 +03:00
|
|
|
spends.append((puzzle, CoinSolution(coin, solution)))
|
|
|
|
return spends
|
|
|
|
|
2020-03-27 17:30:07 +03:00
|
|
|
async def sign_transaction(
|
|
|
|
self, spends: List[Tuple[Program, CoinSolution]]
|
|
|
|
) -> Optional[SpendBundle]:
|
2020-03-06 02:53:36 +03:00
|
|
|
signatures = []
|
2020-02-13 22:57:40 +03:00
|
|
|
for puzzle, solution in spends:
|
2020-03-06 02:53:36 +03:00
|
|
|
# Get keys
|
2020-02-27 23:37:32 +03:00
|
|
|
keys = await self.get_keys(solution.coin.puzzle_hash)
|
2020-02-13 22:57:40 +03:00
|
|
|
if not keys:
|
|
|
|
return None
|
|
|
|
pubkey, secretkey = keys
|
|
|
|
secretkey = BLSPrivateKey(secretkey)
|
2020-03-06 02:53:36 +03:00
|
|
|
|
2020-02-13 22:57:40 +03:00
|
|
|
code_ = [puzzle, solution.solution]
|
|
|
|
sexp = clvm.to_sexp_f(code_)
|
2020-03-06 02:53:36 +03:00
|
|
|
|
|
|
|
# Get AGGSIG conditions
|
2020-02-27 04:33:25 +03:00
|
|
|
err, con, cost = conditions_for_solution(sexp)
|
2020-02-13 22:57:40 +03:00
|
|
|
if err or not con:
|
|
|
|
return None
|
|
|
|
conditions_dict = conditions_by_opcode(con)
|
|
|
|
|
2020-03-06 02:53:36 +03:00
|
|
|
# Create signature
|
2020-03-26 00:36:32 +03:00
|
|
|
for pk_message in hash_key_pairs_for_conditions_dict(
|
|
|
|
conditions_dict, bytes(solution.coin)
|
|
|
|
):
|
2020-03-06 02:53:36 +03:00
|
|
|
signature = secretkey.sign(pk_message.message_hash)
|
|
|
|
signatures.append(signature)
|
|
|
|
|
|
|
|
# Aggregate signatures
|
|
|
|
aggsig = BLSSignature.aggregate(signatures)
|
2020-02-13 22:57:40 +03:00
|
|
|
solution_list: List[CoinSolution] = [
|
|
|
|
CoinSolution(
|
|
|
|
coin_solution.coin, clvm.to_sexp_f([puzzle, coin_solution.solution])
|
|
|
|
)
|
|
|
|
for (puzzle, coin_solution) in spends
|
|
|
|
]
|
|
|
|
spend_bundle = SpendBundle(solution_list, aggsig)
|
2020-03-06 02:53:36 +03:00
|
|
|
|
2020-02-13 22:57:40 +03:00
|
|
|
return spend_bundle
|
|
|
|
|
2020-03-19 11:26:51 +03:00
|
|
|
async def generate_signed_transaction_dict(
|
2020-03-27 17:30:07 +03:00
|
|
|
self, data: Dict[str, Any]
|
2020-03-19 11:26:51 +03:00
|
|
|
) -> Optional[SpendBundle]:
|
|
|
|
""" Use this to generate transaction. """
|
2020-03-27 17:30:07 +03:00
|
|
|
# Check that both are integers
|
|
|
|
if not isinstance(data["amount"], int) or not isinstance(data["amount"], int):
|
|
|
|
raise ValueError("An integer amount or fee is required (too many decimals)")
|
|
|
|
amount = uint64(data["amount"])
|
2020-03-19 11:26:51 +03:00
|
|
|
|
|
|
|
if "fee" in data:
|
2020-03-27 17:30:07 +03:00
|
|
|
fee = uint64(data["fee"])
|
2020-03-19 11:26:51 +03:00
|
|
|
else:
|
2020-03-27 17:30:07 +03:00
|
|
|
fee = uint64(0)
|
2020-03-19 11:26:51 +03:00
|
|
|
|
2020-03-27 17:30:07 +03:00
|
|
|
puzzle_hash = bytes32(bytes.fromhex(data["puzzle_hash"]))
|
2020-03-19 11:26:51 +03:00
|
|
|
|
|
|
|
return await self.generate_signed_transaction(amount, puzzle_hash, fee)
|
|
|
|
|
2020-02-13 22:57:40 +03:00
|
|
|
async def generate_signed_transaction(
|
2020-03-23 22:59:53 +03:00
|
|
|
self,
|
2020-03-27 17:30:07 +03:00
|
|
|
amount: uint64,
|
|
|
|
puzzle_hash: bytes32,
|
|
|
|
fee: uint64 = uint64(0),
|
2020-03-23 22:59:53 +03:00
|
|
|
origin_id: bytes32 = None,
|
|
|
|
coins: Set[Coin] = None,
|
2020-02-13 22:57:40 +03:00
|
|
|
) -> Optional[SpendBundle]:
|
2020-02-25 04:19:50 +03:00
|
|
|
""" Use this to generate transaction. """
|
2020-03-19 11:26:51 +03:00
|
|
|
|
2020-03-23 22:59:53 +03:00
|
|
|
transaction = await self.generate_unsigned_transaction(
|
|
|
|
amount, puzzle_hash, fee, origin_id, coins
|
|
|
|
)
|
2020-02-13 22:57:40 +03:00
|
|
|
if len(transaction) == 0:
|
|
|
|
return None
|
2020-02-27 23:37:32 +03:00
|
|
|
return await self.sign_transaction(transaction)
|
2020-02-13 22:57:40 +03:00
|
|
|
|
2020-03-27 17:30:07 +03:00
|
|
|
async def get_transaction_status(
|
|
|
|
self, tx_id: SpendBundle
|
|
|
|
) -> List[Tuple[str, MempoolInclusionStatus, Optional[str]]]:
|
|
|
|
tr: Optional[
|
|
|
|
TransactionRecord
|
|
|
|
] = await self.wallet_state_manager.get_transaction(tx_id)
|
|
|
|
ret_list = []
|
|
|
|
if tr is not None:
|
|
|
|
for (name, ss, err) in tr.sent_to:
|
|
|
|
ret_list.append((name, MempoolInclusionStatus(ss), err))
|
|
|
|
return ret_list
|
|
|
|
|
|
|
|
async def push_transaction(self, spend_bundle: SpendBundle) -> None:
|
2020-02-25 04:19:50 +03:00
|
|
|
""" Use this API to send transactions. """
|
2020-03-19 22:11:58 +03:00
|
|
|
await self.wallet_state_manager.add_pending_transaction(
|
|
|
|
spend_bundle, self.wallet_info.id
|
|
|
|
)
|