Skip to content

Commit

Permalink
Add basic fee support for XCH transfers
Browse files Browse the repository at this point in the history
  • Loading branch information
trgarrett committed Nov 24, 2022
1 parent a38dbe5 commit b329d35
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 13 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
venv
.DS_Store
main.sym
chialisp.code-workspace
133 changes: 120 additions & 13 deletions royalty_share/royalty_share_spend.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
import time

from chia.consensus.default_constants import DEFAULT_CONSTANTS
AGG_SIG_ME_ADDITIONAL_DATA = DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA
MAX_BLOCK_COST_CLVM = DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM

from chia.rpc.full_node_rpc_client import FullNodeRpcClient
from chia.rpc.wallet_rpc_client import WalletRpcClient

from chia.types.announcement import Announcement
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program, SerializedProgram
from chia.types.blockchain_format.sized_bytes import bytes32
Expand All @@ -21,13 +25,22 @@
from chia.util.config import load_config
from chia.util.default_root import DEFAULT_ROOT_PATH
from chia.util.ints import uint16, uint64
from chia.util.keychain import Keychain
from chia.wallet.cat_wallet.cat_utils import (
CAT_MOD,
SpendableCAT,
construct_cat_puzzle,
unsigned_spend_bundle_for_spendable_cats
)
from chia.wallet.derive_keys import master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened
from chia.wallet.lineage_proof import LineageProof
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
DEFAULT_HIDDEN_PUZZLE_HASH,
calculate_synthetic_secret_key,
puzzle_for_pk
)
from chia.wallet.puzzles.puzzle_utils import make_assert_coin_announcement
from chia.wallet.sign_coin_spends import sign_coin_spends
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.wallet import Wallet
from clvm_tools.clvmc import compile_clvm
Expand All @@ -40,15 +53,21 @@
wallet_rpc_port = config["wallet"]["rpc_port"] # 9256
prefix = "xch"

global wallet_keys
wallet_keys = []

MIN_FEE = 50
DERIVATIONS = 100

def print_json(dict):
print(json.dumps(dict, sort_keys=True, indent=4))

async def spend_unspent_coins(royalty_address, royalty_puzzle_hash, royalty_puzzle: Program, cat_asset_id=None):
async def spend_unspent_coins(royalty_address, royalty_puzzle_hash, royalty_puzzle: Program, cat_asset_id=None, add_fees=False):
try:
if cat_asset_id:
print(f"\tTrying address {royalty_address} as CAT with TAIL hash {cat_asset_id}")
node_client = await FullNodeRpcClient.create(self_hostname, uint16(full_node_rpc_port), DEFAULT_ROOT_PATH, config)
wallet_client = await WalletRpcClient.create(self_hostname, uint16(wallet_rpc_port), DEFAULT_ROOT_PATH, config)
all_royalty_coins = await node_client.get_coin_records_by_puzzle_hash(royalty_puzzle_hash, False, 0)
for coin_record in all_royalty_coins:
try:
Expand Down Expand Up @@ -88,14 +107,12 @@ async def spend_unspent_coins(royalty_address, royalty_puzzle_hash, royalty_puzz
signature,
)

# TODO: coin selection and fee calculation
# general approach:
# add fee coin(s) to coin_spend with puzzlehash=*my wallet* with amount=(coin amount - max fees) -> allow remainder to go to farmer
# update spend bundle aggregated signature
# if max_fees > 0:
# spend_bundle = add_fees_to_spend_bundle(spend_bundle, max_fees)
if add_fees is True:
print(f'Adding fees: {MIN_FEE}')
spend_bundle = await add_fees_and_sign_spend_bundle(spend_bundle, node_client, wallet_client)

print(f'{spend_bundle}')

print_json(spend_bundle.to_json_dict())
status = await node_client.push_tx(spend_bundle)
print_json(status)
except Exception as e:
Expand All @@ -104,10 +121,78 @@ async def spend_unspent_coins(royalty_address, royalty_puzzle_hash, royalty_puzz
print('\r\n...Continuing to next coin')
finally:
node_client.close()
wallet_client.close()
await node_client.await_closed()

def wallet_keyf(pk):
print(f'Looking for wallet keys to sign spend, PK: {pk}')
for wallet_key in wallet_keys:
synth_key = calculate_synthetic_secret_key(wallet_key, DEFAULT_HIDDEN_PUZZLE_HASH)
if synth_key.get_g1() == pk:
return synth_key
raise Exception("Evaluated all keys without finding PK match!")

async def add_fees_and_sign_spend_bundle(spend_bundle: SpendBundle, node_client: FullNodeRpcClient, wallet_client: WalletRpcClient):
updated_coin_spends = []
updated_coin_spends.extend(spend_bundle.coin_spends)

fees_remaining: uint64 = uint64(MIN_FEE)
print(f'Signing spend bundle with non-empty signature, applying {fees_remaining} mojos fee.')

fee_coins = await wallet_client.select_coins(amount=uint64(MIN_FEE), wallet_id=1)

print(f'evaluating {len(fee_coins)} coin(s) for fees')
for fee_coin in fee_coins:
print(f'{fee_coin}')
if fees_remaining <= 0:
break
if fee_coin.amount > fees_remaining:
change_spend = await calculate_change_spend(node_client, fee_coin, fees_remaining, spend_bundle.coin_spends)
updated_coin_spends.append(change_spend)
fees_remaining = 0
# FIXME - missing case where we can only get PART of the fees from one coin

spend_bundle = await sign_coin_spends(updated_coin_spends, wallet_keyf, AGG_SIG_ME_ADDITIONAL_DATA, MAX_BLOCK_COST_CLVM)

return spend_bundle

async def calculate_change_spend(node_client: FullNodeRpcClient, fee_coin: Coin, fee_amount: uint64, peer_coin_spends: List[CoinSpend]):

puzzle_reveal = None
for wallet_key in wallet_keys:
pk = wallet_key.get_g1()
candidate_puzzle_reveal = puzzle_for_pk(pk)
if candidate_puzzle_reveal.get_tree_hash() == fee_coin.puzzle_hash:
puzzle_reveal = candidate_puzzle_reveal
break
if puzzle_reveal is None:
raise Exception("Checked all known keys for valid puzzle reveal. Failed to find any.")

change_amount = fee_coin.amount - fee_amount
destination_puzzlehash = fee_coin.puzzle_hash
primaries = [{"puzzlehash": destination_puzzlehash, "amount": change_amount, "memos": [destination_puzzlehash]}]

# FIXME - if we further aggregate spend bundles, we need to assert ALL spends
peer_coin_id = peer_coin_spends[0].coin.name()
assert_coin_announcement = Announcement(peer_coin_id, b'').name()


solution = Wallet().make_solution(
primaries=primaries,
fee=fee_amount,
# make sure our bundles aren't separated
coin_announcements_to_assert = [assert_coin_announcement]
)

print(f'solution: {solution}')

print(f'Prepping change spend of amount {change_amount} mojos')

return CoinSpend(fee_coin, puzzle_reveal, solution)


def usage():
print(f"Usage: python royalty_share_spend.py <ROYALTY_ADDRESS> <PATH_TO_CURRIED_ROYALTY_PUZZLE_AS_HEX> \r\n")
print(f"Usage: python royalty_share_spend.py <ROYALTY_ADDRESS> <PATH_TO_CURRIED_ROYALTY_PUZZLE_AS_HEX> <optional:WALLET_FINGERPRINT_FOR_SIGNED_SPENDS> \r\n")
exit(-1)

async def calculate_cat_spend_bundle(coin_record: CoinRecord, node_client: FullNodeRpcClient, cat_asset_id: str, royalty_address: str, royalty_puzzle: Program) -> SpendBundle:
Expand Down Expand Up @@ -159,32 +244,54 @@ def calculate_cat_royalty_address(royalty_address, asset_id):
return (encode_puzzle_hash(outer_puzzlehash, prefix), outer_puzzlehash)

async def main():

if(len(sys.argv) != 3):
arg_count = len(sys.argv)

if(arg_count != 3 and arg_count != 4):
usage()
exit(1)

royalty_address = sys.argv[1]
royalty_puzzle_hash = decode_puzzle_hash(sys.argv[1])
royalty_puzzle = None
wallet_fingerprint = None
text: str = None

with open(sys.argv[2], 'r') as f:
text = f.readlines()[0]

if arg_count == 4:
wallet_fingerprint = sys.argv[3]

clvm_blob = bytes.fromhex(text)
sp = SerializedProgram.from_bytes(clvm_blob)
royalty_puzzle = Program.from_bytes(bytes(sp))

if wallet_fingerprint is not None:
print('Loading private key for spend bundle signing (fee support)')
print(f'Is keychain locked? {Keychain.is_keyring_locked()}')
keychain = Keychain()

keychain.get_private_key_by_fingerprint

all_sks = keychain.get_all_private_keys()

print(f'Deriving {DERIVATIONS} synthetic private keys')
for i in range(DERIVATIONS):
for sk in all_sks:
wallet_keys.append(master_sk_to_wallet_sk_unhardened(sk[0], i))

print('Checking XCH spends...')
await spend_unspent_coins(royalty_address, royalty_puzzle_hash, royalty_puzzle)

add_fees = wallet_fingerprint is not None
await spend_unspent_coins(royalty_address, royalty_puzzle_hash, royalty_puzzle, cat_asset_id=None, add_fees=add_fees)

# It's highly likely you will want to support a different set of these
# TODO: look at API or CSV import of all/as many as you care about
cats = {
"LKY8": "e5a8af7124c2737283838e6797b0f0a5293fc81aca1ffd2720f8506c23f2ad88",
"SBX": "a628c1c2c6fcb74d53746157e438e108eab5c0bb3e5c80ff9b1910b3e4832913",
"TEST": "2267357bf318926f9ccaa5b68e1d4527df89b00c4aed41d6d590d75aa6fa0ff4"
"TEST": "2267357bf318926f9ccaa5b68e1d4527df89b00c4aed41d6d590d75aa6fa0ff4",
"USDS": "6d95dae356e32a71db5ddcb42224754a02524c615c5fc35f568c2af04774e589"
}

print('Checking CAT spends...')
Expand Down

0 comments on commit b329d35

Please sign in to comment.