693 lines
26 KiB
Python
693 lines
26 KiB
Python
# RLWallet is subclass of Wallet
|
|
import asyncio
|
|
import json
|
|
import time
|
|
from dataclasses import dataclass
|
|
from secrets import token_bytes
|
|
from typing import Any, List, Optional, Tuple
|
|
|
|
from blspy import AugSchemeMPL, G1Element, PrivateKey
|
|
|
|
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_solution import CoinSolution
|
|
from chia.types.spend_bundle import SpendBundle
|
|
from chia.util.byte_types import hexstr_to_bytes
|
|
from chia.util.ints import uint8, uint32, uint64, uint128
|
|
from chia.util.streamable import Streamable, streamable
|
|
from chia.wallet.derivation_record import DerivationRecord
|
|
from chia.wallet.derive_keys import master_sk_to_wallet_sk
|
|
from chia.wallet.rl_wallet.rl_wallet_puzzles import (
|
|
make_clawback_solution,
|
|
rl_make_aggregation_puzzle,
|
|
rl_make_aggregation_solution,
|
|
rl_make_solution_mode_2,
|
|
rl_puzzle_for_pk,
|
|
solution_for_rl,
|
|
)
|
|
from chia.wallet.transaction_record import TransactionRecord
|
|
from chia.wallet.util.transaction_type import TransactionType
|
|
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
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
@streamable
|
|
class RLInfo(Streamable):
|
|
type: str
|
|
admin_pubkey: Optional[bytes]
|
|
user_pubkey: Optional[bytes]
|
|
limit: Optional[uint64]
|
|
interval: Optional[uint64]
|
|
rl_origin: Optional[Coin]
|
|
rl_origin_id: Optional[bytes32]
|
|
rl_puzzle_hash: Optional[bytes32]
|
|
initialized: bool
|
|
|
|
|
|
class RLWallet:
|
|
wallet_state_manager: Any
|
|
wallet_info: WalletInfo
|
|
rl_coin_record: Optional[WalletCoinRecord]
|
|
rl_info: RLInfo
|
|
main_wallet: Wallet
|
|
private_key: PrivateKey
|
|
|
|
@staticmethod
|
|
async def create_rl_admin(
|
|
wallet_state_manager: Any,
|
|
):
|
|
unused: Optional[uint32] = await wallet_state_manager.puzzle_store.get_unused_derivation_path()
|
|
if unused is None:
|
|
await wallet_state_manager.create_more_puzzle_hashes()
|
|
unused = await wallet_state_manager.puzzle_store.get_unused_derivation_path()
|
|
assert unused is not None
|
|
|
|
private_key = master_sk_to_wallet_sk(wallet_state_manager.private_key, unused)
|
|
pubkey: G1Element = private_key.get_g1()
|
|
|
|
rl_info = RLInfo("admin", bytes(pubkey), None, None, None, None, None, None, False)
|
|
info_as_string = json.dumps(rl_info.to_json_dict())
|
|
wallet_info: Optional[WalletInfo] = await wallet_state_manager.user_store.create_wallet(
|
|
"RL Admin", WalletType.RATE_LIMITED, info_as_string
|
|
)
|
|
if wallet_info is None:
|
|
raise Exception("wallet_info is None")
|
|
|
|
await wallet_state_manager.puzzle_store.add_derivation_paths(
|
|
[
|
|
DerivationRecord(
|
|
unused,
|
|
bytes32(token_bytes(32)),
|
|
pubkey,
|
|
WalletType.RATE_LIMITED,
|
|
wallet_info.id,
|
|
)
|
|
]
|
|
)
|
|
await wallet_state_manager.puzzle_store.set_used_up_to(unused)
|
|
|
|
self = await RLWallet.create(wallet_state_manager, wallet_info)
|
|
await wallet_state_manager.add_new_wallet(self, self.id())
|
|
return self
|
|
|
|
@staticmethod
|
|
async def create_rl_user(
|
|
wallet_state_manager: Any,
|
|
):
|
|
async with wallet_state_manager.puzzle_store.lock:
|
|
unused: Optional[uint32] = await wallet_state_manager.puzzle_store.get_unused_derivation_path()
|
|
if unused is None:
|
|
await wallet_state_manager.create_more_puzzle_hashes()
|
|
unused = await wallet_state_manager.puzzle_store.get_unused_derivation_path()
|
|
assert unused is not None
|
|
|
|
private_key = wallet_state_manager.private_key
|
|
|
|
pubkey: G1Element = master_sk_to_wallet_sk(private_key, unused).get_g1()
|
|
|
|
rl_info = RLInfo("user", None, bytes(pubkey), None, None, None, None, None, False)
|
|
info_as_string = json.dumps(rl_info.to_json_dict())
|
|
await wallet_state_manager.user_store.create_wallet("RL User", WalletType.RATE_LIMITED, info_as_string)
|
|
wallet_info = await wallet_state_manager.user_store.get_last_wallet()
|
|
if wallet_info is None:
|
|
raise Exception("wallet_info is None")
|
|
|
|
self = await RLWallet.create(wallet_state_manager, wallet_info)
|
|
|
|
await wallet_state_manager.puzzle_store.add_derivation_paths(
|
|
[
|
|
DerivationRecord(
|
|
unused,
|
|
bytes32(token_bytes(32)),
|
|
pubkey,
|
|
WalletType.RATE_LIMITED,
|
|
wallet_info.id,
|
|
)
|
|
]
|
|
)
|
|
await wallet_state_manager.puzzle_store.set_used_up_to(unused)
|
|
|
|
await wallet_state_manager.add_new_wallet(self, self.id())
|
|
return self
|
|
|
|
@staticmethod
|
|
async def create(wallet_state_manager: Any, info: WalletInfo):
|
|
self = RLWallet()
|
|
|
|
self.private_key = wallet_state_manager.private_key
|
|
|
|
self.wallet_state_manager = wallet_state_manager
|
|
|
|
self.wallet_info = info
|
|
self.rl_info = RLInfo.from_json_dict(json.loads(info.data))
|
|
self.main_wallet = wallet_state_manager.main_wallet
|
|
return self
|
|
|
|
@classmethod
|
|
def type(cls) -> uint8:
|
|
return uint8(WalletType.RATE_LIMITED)
|
|
|
|
def id(self) -> uint32:
|
|
return self.wallet_info.id
|
|
|
|
async def admin_create_coin(
|
|
self,
|
|
interval: uint64,
|
|
limit: uint64,
|
|
user_pubkey: str,
|
|
amount: uint64,
|
|
fee: uint64,
|
|
) -> bool:
|
|
coins = await self.wallet_state_manager.main_wallet.select_coins(amount)
|
|
if coins is None:
|
|
return False
|
|
|
|
origin = coins.copy().pop()
|
|
origin_id = origin.name()
|
|
|
|
user_pubkey_bytes = hexstr_to_bytes(user_pubkey)
|
|
|
|
assert self.rl_info.admin_pubkey is not None
|
|
|
|
rl_puzzle = rl_puzzle_for_pk(
|
|
pubkey=user_pubkey_bytes,
|
|
rate_amount=limit,
|
|
interval_time=interval,
|
|
origin_id=origin_id,
|
|
clawback_pk=self.rl_info.admin_pubkey,
|
|
)
|
|
|
|
rl_puzzle_hash = rl_puzzle.get_tree_hash()
|
|
index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(
|
|
G1Element.from_bytes(self.rl_info.admin_pubkey)
|
|
)
|
|
|
|
assert index is not None
|
|
record = DerivationRecord(
|
|
index,
|
|
rl_puzzle_hash,
|
|
G1Element.from_bytes(self.rl_info.admin_pubkey),
|
|
WalletType.RATE_LIMITED,
|
|
self.id(),
|
|
)
|
|
await self.wallet_state_manager.puzzle_store.add_derivation_paths([record])
|
|
|
|
spend_bundle = await self.main_wallet.generate_signed_transaction(amount, rl_puzzle_hash, fee, origin_id, coins)
|
|
if spend_bundle is None:
|
|
return False
|
|
|
|
await self.main_wallet.push_transaction(spend_bundle)
|
|
new_rl_info = RLInfo(
|
|
"admin",
|
|
self.rl_info.admin_pubkey,
|
|
user_pubkey_bytes,
|
|
limit,
|
|
interval,
|
|
origin,
|
|
origin.name(),
|
|
rl_puzzle_hash,
|
|
True,
|
|
)
|
|
|
|
data_str = json.dumps(new_rl_info.to_json_dict())
|
|
new_wallet_info = WalletInfo(self.id(), self.wallet_info.name, self.type(), data_str)
|
|
await self.wallet_state_manager.user_store.update_wallet(new_wallet_info, False)
|
|
await self.wallet_state_manager.add_new_wallet(self, self.id())
|
|
self.wallet_info = new_wallet_info
|
|
self.rl_info = new_rl_info
|
|
|
|
return True
|
|
|
|
async def set_user_info(
|
|
self,
|
|
interval: uint64,
|
|
limit: uint64,
|
|
origin_parent_id: str,
|
|
origin_puzzle_hash: str,
|
|
origin_amount: uint64,
|
|
admin_pubkey: str,
|
|
) -> None:
|
|
admin_pubkey_bytes = hexstr_to_bytes(admin_pubkey)
|
|
|
|
assert self.rl_info.user_pubkey is not None
|
|
origin = Coin(
|
|
hexstr_to_bytes(origin_parent_id),
|
|
hexstr_to_bytes(origin_puzzle_hash),
|
|
origin_amount,
|
|
)
|
|
rl_puzzle = rl_puzzle_for_pk(
|
|
pubkey=self.rl_info.user_pubkey,
|
|
rate_amount=limit,
|
|
interval_time=interval,
|
|
origin_id=origin.name(),
|
|
clawback_pk=admin_pubkey_bytes,
|
|
)
|
|
|
|
rl_puzzle_hash = rl_puzzle.get_tree_hash()
|
|
|
|
new_rl_info = RLInfo(
|
|
"user",
|
|
admin_pubkey_bytes,
|
|
self.rl_info.user_pubkey,
|
|
limit,
|
|
interval,
|
|
origin,
|
|
origin.name(),
|
|
rl_puzzle_hash,
|
|
True,
|
|
)
|
|
rl_puzzle_hash = rl_puzzle.get_tree_hash()
|
|
if await self.wallet_state_manager.puzzle_store.puzzle_hash_exists(rl_puzzle_hash):
|
|
raise ValueError(
|
|
"Cannot create multiple Rate Limited wallets under the same keys. This will change in a future release."
|
|
)
|
|
user_pubkey: G1Element = G1Element.from_bytes(self.rl_info.user_pubkey)
|
|
index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(user_pubkey)
|
|
assert index is not None
|
|
record = DerivationRecord(
|
|
index,
|
|
rl_puzzle_hash,
|
|
user_pubkey,
|
|
WalletType.RATE_LIMITED,
|
|
self.id(),
|
|
)
|
|
|
|
aggregation_puzzlehash = self.rl_get_aggregation_puzzlehash(new_rl_info.rl_puzzle_hash)
|
|
record2 = DerivationRecord(
|
|
index + 1,
|
|
aggregation_puzzlehash,
|
|
user_pubkey,
|
|
WalletType.RATE_LIMITED,
|
|
self.id(),
|
|
)
|
|
await self.wallet_state_manager.puzzle_store.add_derivation_paths([record, record2])
|
|
self.wallet_state_manager.set_coin_with_puzzlehash_created_callback(
|
|
aggregation_puzzlehash, self.aggregate_this_coin
|
|
)
|
|
|
|
data_str = json.dumps(new_rl_info.to_json_dict())
|
|
new_wallet_info = WalletInfo(self.id(), self.wallet_info.name, self.type(), data_str)
|
|
await self.wallet_state_manager.user_store.update_wallet(new_wallet_info, False)
|
|
await self.wallet_state_manager.add_new_wallet(self, self.id())
|
|
self.wallet_info = new_wallet_info
|
|
self.rl_info = new_rl_info
|
|
|
|
async def aggregate_this_coin(self, coin: Coin):
|
|
spend_bundle = await self.rl_generate_signed_aggregation_transaction(
|
|
self.rl_info, coin, await self._get_rl_parent(), await self._get_rl_coin()
|
|
)
|
|
|
|
rl_coin = await self._get_rl_coin()
|
|
puzzle_hash = rl_coin.puzzle_hash if rl_coin is not None else None
|
|
tx_record = TransactionRecord(
|
|
confirmed_at_height=uint32(0),
|
|
created_at_time=uint64(int(time.time())),
|
|
to_puzzle_hash=puzzle_hash,
|
|
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=self.id(),
|
|
sent_to=[],
|
|
trade_id=None,
|
|
type=uint32(TransactionType.OUTGOING_TX.value),
|
|
name=spend_bundle.name(),
|
|
)
|
|
|
|
asyncio.create_task(self.push_transaction(tx_record))
|
|
|
|
async def rl_available_balance(self) -> uint64:
|
|
self.rl_coin_record = await self._get_rl_coin_record()
|
|
if self.rl_coin_record is None:
|
|
return uint64(0)
|
|
peak = self.wallet_state_manager.blockchain.get_peak()
|
|
height = peak.height if peak else 0
|
|
assert self.rl_info.limit is not None
|
|
unlocked = int(
|
|
((height - self.rl_coin_record.confirmed_block_height) / self.rl_info.interval) * int(self.rl_info.limit)
|
|
)
|
|
total_amount = self.rl_coin_record.coin.amount
|
|
available_amount = min(unlocked, total_amount)
|
|
return uint64(available_amount)
|
|
|
|
async def get_confirmed_balance(self, unspent_records=None) -> uint128:
|
|
return await self.wallet_state_manager.get_confirmed_balance_for_wallet(self.id(), unspent_records)
|
|
|
|
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_spendable_balance(self, unspent_records=None) -> uint128:
|
|
spendable_am = await self.wallet_state_manager.get_confirmed_spendable_balance_for_wallet(self.id())
|
|
return spendable_am
|
|
|
|
async def get_max_send_amount(self, records=None):
|
|
# Rate limited wallet is a singleton, max send is same as spendable
|
|
return await self.get_spendable_balance()
|
|
|
|
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)
|
|
|
|
def get_new_puzzle(self) -> Program:
|
|
if (
|
|
self.rl_info.limit is None
|
|
or self.rl_info.interval is None
|
|
or self.rl_info.user_pubkey is None
|
|
or self.rl_info.admin_pubkey is None
|
|
or self.rl_info.rl_origin_id is None
|
|
):
|
|
raise ValueError("One or more of the RL info fields is None")
|
|
return rl_puzzle_for_pk(
|
|
pubkey=self.rl_info.user_pubkey,
|
|
rate_amount=self.rl_info.limit,
|
|
interval_time=self.rl_info.interval,
|
|
origin_id=self.rl_info.rl_origin_id,
|
|
clawback_pk=self.rl_info.admin_pubkey,
|
|
)
|
|
|
|
def get_new_puzzlehash(self) -> bytes32:
|
|
return self.get_new_puzzle().get_tree_hash()
|
|
|
|
async def can_generate_rl_puzzle_hash(self, hash) -> bool:
|
|
return await self.wallet_state_manager.puzzle_store.puzzle_hash_exists(hash)
|
|
|
|
def puzzle_for_pk(self, pk) -> Optional[Program]:
|
|
if self.rl_info.initialized is False:
|
|
return None
|
|
if (
|
|
self.rl_info.limit is None
|
|
or self.rl_info.interval is None
|
|
or self.rl_info.user_pubkey is None
|
|
or self.rl_info.admin_pubkey is None
|
|
or self.rl_info.rl_origin_id is None
|
|
):
|
|
return None
|
|
return rl_puzzle_for_pk(
|
|
pubkey=self.rl_info.user_pubkey,
|
|
rate_amount=self.rl_info.limit,
|
|
interval_time=self.rl_info.interval,
|
|
origin_id=self.rl_info.rl_origin_id,
|
|
clawback_pk=self.rl_info.admin_pubkey,
|
|
)
|
|
|
|
async def get_keys(self, puzzle_hash: bytes32) -> Tuple[G1Element, PrivateKey]:
|
|
"""
|
|
Returns keys for puzzle_hash.
|
|
"""
|
|
index_for_puzzlehash = await self.wallet_state_manager.puzzle_store.index_for_puzzle_hash_and_wallet(
|
|
puzzle_hash, self.id()
|
|
)
|
|
if index_for_puzzlehash is None:
|
|
raise ValueError(f"index_for_puzzlehash is None ph {puzzle_hash}")
|
|
private = master_sk_to_wallet_sk(self.private_key, index_for_puzzlehash)
|
|
pubkey = private.get_g1()
|
|
return pubkey, private
|
|
|
|
async def get_keys_pk(self, clawback_pubkey: bytes):
|
|
"""
|
|
Return keys for pubkey
|
|
"""
|
|
index_for_pubkey = await self.wallet_state_manager.puzzle_store.index_for_pubkey(
|
|
G1Element.from_bytes(clawback_pubkey)
|
|
)
|
|
if index_for_pubkey is None:
|
|
raise ValueError(f"index_for_pubkey is None pk {clawback_pubkey.hex()}")
|
|
private = master_sk_to_wallet_sk(self.private_key, index_for_pubkey)
|
|
pubkey = private.get_g1()
|
|
|
|
return pubkey, private
|
|
|
|
async def _get_rl_coin(self) -> Optional[Coin]:
|
|
rl_coins = await self.wallet_state_manager.coin_store.get_coin_records_by_puzzle_hash(
|
|
self.rl_info.rl_puzzle_hash
|
|
)
|
|
for coin_record in rl_coins:
|
|
if coin_record.spent is False:
|
|
return coin_record.coin
|
|
|
|
return None
|
|
|
|
async def _get_rl_coin_record(self) -> Optional[WalletCoinRecord]:
|
|
rl_coins = await self.wallet_state_manager.coin_store.get_coin_records_by_puzzle_hash(
|
|
self.rl_info.rl_puzzle_hash
|
|
)
|
|
for coin_record in rl_coins:
|
|
if coin_record.spent is False:
|
|
return coin_record
|
|
|
|
return None
|
|
|
|
async def _get_rl_parent(self) -> Optional[Coin]:
|
|
self.rl_coin_record = await self._get_rl_coin_record()
|
|
if not self.rl_coin_record:
|
|
return None
|
|
rl_parent_id = self.rl_coin_record.coin.parent_coin_info
|
|
if rl_parent_id == self.rl_info.rl_origin_id:
|
|
return self.rl_info.rl_origin
|
|
rl_parent = await self.wallet_state_manager.coin_store.get_coin_record(rl_parent_id)
|
|
if rl_parent is None:
|
|
return None
|
|
|
|
return rl_parent.coin
|
|
|
|
async def rl_generate_unsigned_transaction(self, to_puzzlehash, amount, fee) -> List[CoinSolution]:
|
|
spends = []
|
|
assert self.rl_coin_record is not None
|
|
coin = self.rl_coin_record.coin
|
|
puzzle_hash = coin.puzzle_hash
|
|
pubkey = self.rl_info.user_pubkey
|
|
rl_parent: Optional[Coin] = await self._get_rl_parent()
|
|
if rl_parent is None:
|
|
raise ValueError("No RL parent coin")
|
|
|
|
# these lines make mypy happy
|
|
|
|
assert pubkey is not None
|
|
assert self.rl_info.limit is not None
|
|
assert self.rl_info.interval is not None
|
|
assert self.rl_info.rl_origin_id is not None
|
|
assert self.rl_info.admin_pubkey is not None
|
|
|
|
puzzle = rl_puzzle_for_pk(
|
|
bytes(pubkey),
|
|
self.rl_info.limit,
|
|
self.rl_info.interval,
|
|
self.rl_info.rl_origin_id,
|
|
self.rl_info.admin_pubkey,
|
|
)
|
|
|
|
solution = solution_for_rl(
|
|
coin.parent_coin_info,
|
|
puzzle_hash,
|
|
coin.amount,
|
|
to_puzzlehash,
|
|
amount,
|
|
rl_parent.parent_coin_info,
|
|
rl_parent.amount,
|
|
self.rl_info.interval,
|
|
self.rl_info.limit,
|
|
fee,
|
|
)
|
|
|
|
spends.append(CoinSolution(coin, puzzle, solution))
|
|
return spends
|
|
|
|
async def generate_signed_transaction(self, amount, to_puzzle_hash, fee: uint64 = uint64(0)) -> TransactionRecord:
|
|
self.rl_coin_record = await self._get_rl_coin_record()
|
|
if not self.rl_coin_record:
|
|
raise ValueError("No unspent coin (zero balance)")
|
|
if amount > self.rl_coin_record.coin.amount:
|
|
raise ValueError(f"Coin value not sufficient: {amount} > {self.rl_coin_record.coin.amount}")
|
|
transaction = await self.rl_generate_unsigned_transaction(to_puzzle_hash, amount, fee)
|
|
spend_bundle = await self.rl_sign_transaction(transaction)
|
|
|
|
return TransactionRecord(
|
|
confirmed_at_height=uint32(0),
|
|
created_at_time=uint64(int(time.time())),
|
|
to_puzzle_hash=to_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.id(),
|
|
sent_to=[],
|
|
trade_id=None,
|
|
type=uint32(TransactionType.OUTGOING_TX.value),
|
|
name=spend_bundle.name(),
|
|
)
|
|
|
|
async def rl_sign_transaction(self, spends: List[CoinSolution]) -> SpendBundle:
|
|
sigs = []
|
|
for coin_solution in spends:
|
|
pubkey, secretkey = await self.get_keys(coin_solution.coin.puzzle_hash)
|
|
signature = AugSchemeMPL.sign(secretkey, coin_solution.solution.get_tree_hash())
|
|
sigs.append(signature)
|
|
|
|
aggsig = AugSchemeMPL.aggregate(sigs)
|
|
|
|
return SpendBundle(spends, aggsig)
|
|
|
|
def generate_unsigned_clawback_transaction(self, clawback_coin: Coin, clawback_puzzle_hash: bytes32, fee):
|
|
if (
|
|
self.rl_info.limit is None
|
|
or self.rl_info.interval is None
|
|
or self.rl_info.user_pubkey is None
|
|
or self.rl_info.admin_pubkey is None
|
|
):
|
|
raise ValueError("One ore more of the elements of rl_info is None")
|
|
spends = []
|
|
coin = clawback_coin
|
|
if self.rl_info.rl_origin is None:
|
|
raise ValueError("Origin not initialized")
|
|
puzzle = rl_puzzle_for_pk(
|
|
self.rl_info.user_pubkey,
|
|
self.rl_info.limit,
|
|
self.rl_info.interval,
|
|
self.rl_info.rl_origin.name(),
|
|
self.rl_info.admin_pubkey,
|
|
)
|
|
solution = make_clawback_solution(clawback_puzzle_hash, clawback_coin.amount, fee)
|
|
spends.append((puzzle, CoinSolution(coin, puzzle, solution)))
|
|
return spends
|
|
|
|
async def sign_clawback_transaction(
|
|
self, spends: List[Tuple[Program, CoinSolution]], clawback_pubkey
|
|
) -> SpendBundle:
|
|
sigs = []
|
|
for puzzle, solution in spends:
|
|
pubkey, secretkey = await self.get_keys_pk(clawback_pubkey)
|
|
signature = AugSchemeMPL.sign(secretkey, solution.solution.get_tree_hash())
|
|
sigs.append(signature)
|
|
aggsig = AugSchemeMPL.aggregate(sigs)
|
|
solution_list = []
|
|
for puzzle, coin_solution in spends:
|
|
solution_list.append(coin_solution)
|
|
|
|
return SpendBundle(solution_list, aggsig)
|
|
|
|
async def clawback_rl_coin(self, clawback_puzzle_hash: bytes32, fee) -> SpendBundle:
|
|
rl_coin = await self._get_rl_coin()
|
|
if rl_coin is None:
|
|
raise ValueError("rl_coin is None")
|
|
transaction = self.generate_unsigned_clawback_transaction(rl_coin, clawback_puzzle_hash, fee)
|
|
return await self.sign_clawback_transaction(transaction, self.rl_info.admin_pubkey)
|
|
|
|
async def clawback_rl_coin_transaction(self, fee) -> TransactionRecord:
|
|
to_puzzle_hash = await self.main_wallet.get_new_puzzlehash()
|
|
spend_bundle = await self.clawback_rl_coin(to_puzzle_hash, fee)
|
|
|
|
return TransactionRecord(
|
|
confirmed_at_height=uint32(0),
|
|
created_at_time=uint64(int(time.time())),
|
|
to_puzzle_hash=to_puzzle_hash,
|
|
amount=uint64(0),
|
|
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(),
|
|
)
|
|
|
|
# This is for using the AC locked coin and aggregating it into wallet - must happen in same block as RL Mode 2
|
|
async def rl_generate_signed_aggregation_transaction(self, rl_info, consolidating_coin, rl_parent, rl_coin):
|
|
if (
|
|
rl_info.limit is None
|
|
or rl_info.interval is None
|
|
or rl_info.user_pubkey is None
|
|
or rl_info.admin_pubkey is None
|
|
):
|
|
raise ValueError("One or more of the elements of rl_info is None")
|
|
if self.rl_coin_record is None:
|
|
raise ValueError("Rl coin record is None")
|
|
|
|
list_of_coinsolutions = []
|
|
self.rl_coin_record = await self._get_rl_coin_record()
|
|
pubkey, secretkey = await self.get_keys(self.rl_coin_record.coin.puzzle_hash)
|
|
# Spend wallet coin
|
|
puzzle = rl_puzzle_for_pk(
|
|
rl_info.user_pubkey,
|
|
rl_info.limit,
|
|
rl_info.interval,
|
|
rl_info.rl_origin_id,
|
|
rl_info.admin_pubkey,
|
|
)
|
|
|
|
solution = rl_make_solution_mode_2(
|
|
rl_coin.puzzle_hash,
|
|
consolidating_coin.parent_coin_info,
|
|
consolidating_coin.puzzle_hash,
|
|
consolidating_coin.amount,
|
|
rl_coin.parent_coin_info,
|
|
rl_coin.amount,
|
|
rl_parent.amount,
|
|
rl_parent.parent_coin_info,
|
|
)
|
|
signature = AugSchemeMPL.sign(secretkey, solution.get_tree_hash())
|
|
rl_spend = CoinSolution(self.rl_coin_record.coin, puzzle, solution)
|
|
|
|
list_of_coinsolutions.append(rl_spend)
|
|
|
|
# Spend consolidating coin
|
|
puzzle = rl_make_aggregation_puzzle(self.rl_coin_record.coin.puzzle_hash)
|
|
solution = rl_make_aggregation_solution(
|
|
consolidating_coin.name(),
|
|
self.rl_coin_record.coin.parent_coin_info,
|
|
self.rl_coin_record.coin.amount,
|
|
)
|
|
agg_spend = CoinSolution(consolidating_coin, puzzle, solution)
|
|
|
|
list_of_coinsolutions.append(agg_spend)
|
|
aggsig = AugSchemeMPL.aggregate([signature])
|
|
|
|
return SpendBundle(list_of_coinsolutions, aggsig)
|
|
|
|
def rl_get_aggregation_puzzlehash(self, wallet_puzzle):
|
|
puzzle_hash = rl_make_aggregation_puzzle(wallet_puzzle).get_tree_hash()
|
|
|
|
return puzzle_hash
|
|
|
|
async def rl_add_funds(self, amount, puzzle_hash, fee):
|
|
spend_bundle = await self.main_wallet.generate_signed_transaction(amount, puzzle_hash, fee)
|
|
if spend_bundle is None:
|
|
return False
|
|
|
|
await self.main_wallet.push_transaction(spend_bundle)
|
|
|
|
async def push_transaction(self, tx: TransactionRecord) -> None:
|
|
""" Use this API to send transactions. """
|
|
await self.wallet_state_manager.add_pending_transaction(tx)
|