mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2024-09-11 10:56:14 +03:00
Add did_message_spend API (#13890)
* Add did_message_spend API * Add get DID info API * Resolve comments * Add hints
This commit is contained in:
parent
46126d47b3
commit
df519c8a5d
@ -42,6 +42,7 @@ from chia.wallet.derive_keys import (
|
||||
match_address_to_sk,
|
||||
)
|
||||
from chia.wallet.did_wallet.did_wallet import DIDWallet
|
||||
from chia.wallet.did_wallet.did_wallet_puzzles import match_did_puzzle, program_to_metadata
|
||||
from chia.wallet.nft_wallet import nft_puzzles
|
||||
from chia.wallet.nft_wallet.nft_info import NFTInfo, NFTCoinInfo
|
||||
from chia.wallet.nft_wallet.nft_puzzles import get_metadata_and_phs
|
||||
@ -54,7 +55,9 @@ from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash
|
||||
from chia.wallet.trade_record import TradeRecord
|
||||
from chia.wallet.trading.offer import Offer
|
||||
from chia.wallet.transaction_record import TransactionRecord
|
||||
from chia.wallet.uncurried_puzzle import uncurry_puzzle
|
||||
from chia.wallet.util.address_type import AddressType, is_valid_address
|
||||
from chia.wallet.util.compute_memos import compute_memos
|
||||
from chia.wallet.util.transaction_type import TransactionType
|
||||
from chia.wallet.util.wallet_types import AmountWithPuzzlehash, WalletType
|
||||
from chia.wallet.wallet_coin_record import WalletCoinRecord
|
||||
@ -156,6 +159,8 @@ class WalletRpcApi:
|
||||
"/did_get_current_coin_info": self.did_get_current_coin_info,
|
||||
"/did_create_backup_file": self.did_create_backup_file,
|
||||
"/did_transfer_did": self.did_transfer_did,
|
||||
"/did_message_spend": self.did_message_spend,
|
||||
"/did_get_info": self.did_get_info,
|
||||
# NFT Wallet
|
||||
"/nft_mint_nft": self.nft_mint_nft,
|
||||
"/nft_get_nfts": self.nft_get_nfts,
|
||||
@ -242,6 +247,44 @@ class WalletRpcApi:
|
||||
),
|
||||
)
|
||||
|
||||
async def get_latest_coin_spend(
|
||||
self, peer: Optional[WSChiaConnection], coin_id: bytes32, latest: bool = True
|
||||
) -> Tuple[CoinSpend, CoinState]:
|
||||
if peer is None:
|
||||
raise ValueError("No peers to get info from")
|
||||
coin_state_list: List[CoinState] = await self.service.wallet_state_manager.wallet_node.get_coin_state(
|
||||
[coin_id], peer=peer
|
||||
)
|
||||
if coin_state_list is None or len(coin_state_list) < 1:
|
||||
raise ValueError(f"Coin record 0x{coin_id.hex()} not found")
|
||||
coin_state: CoinState = coin_state_list[0]
|
||||
if latest:
|
||||
# Find the unspent coin
|
||||
while coin_state.spent_height is not None:
|
||||
coin_state_list = await self.service.wallet_state_manager.wallet_node.fetch_children(
|
||||
coin_state.coin.name(), peer=peer
|
||||
)
|
||||
odd_coin = 0
|
||||
for coin in coin_state_list:
|
||||
if coin.coin.amount % 2 == 1:
|
||||
odd_coin += 1
|
||||
if odd_coin > 1:
|
||||
raise ValueError("This is not a singleton, multiple children coins found.")
|
||||
if odd_coin == 0:
|
||||
raise ValueError("Cannot find child coin, please wait then retry.")
|
||||
coin_state = coin_state_list[0]
|
||||
# Get parent coin
|
||||
parent_coin_state_list: List[CoinState] = await self.service.wallet_state_manager.wallet_node.get_coin_state(
|
||||
[coin_state.coin.parent_coin_info], peer=peer
|
||||
)
|
||||
if parent_coin_state_list is None or len(parent_coin_state_list) < 1:
|
||||
raise ValueError(f"Parent coin record 0x{coin_state.coin.parent_coin_info.hex()} not found")
|
||||
parent_coin_state: CoinState = parent_coin_state_list[0]
|
||||
coin_spend: CoinSpend = await self.service.wallet_state_manager.wallet_node.fetch_puzzle_solution(
|
||||
parent_coin_state.spent_height, parent_coin_state.coin, peer
|
||||
)
|
||||
return coin_spend, coin_state
|
||||
|
||||
##########################################################################################
|
||||
# Key management
|
||||
##########################################################################################
|
||||
@ -1227,6 +1270,10 @@ class WalletRpcApi:
|
||||
return {"success": False, "error": f"DID for {entity_id.hex()} doesn't exist."}
|
||||
assert isinstance(selected_wallet, DIDWallet)
|
||||
pubkey, signature = await selected_wallet.sign_message(request["message"])
|
||||
latest_coin: Set[Coin] = await selected_wallet.select_coins(uint64(1))
|
||||
latest_coin_id = None
|
||||
if len(latest_coin) > 0:
|
||||
latest_coin_id = latest_coin.pop().name()
|
||||
elif is_valid_address(request["id"], {AddressType.NFT}, self.service.config):
|
||||
target_nft: Optional[NFTCoinInfo] = None
|
||||
for wallet in self.service.wallet_state_manager.wallets.values():
|
||||
@ -1242,10 +1289,17 @@ class WalletRpcApi:
|
||||
|
||||
assert isinstance(selected_wallet, NFTWallet)
|
||||
pubkey, signature = await selected_wallet.sign_message(request["message"], target_nft)
|
||||
latest_coin_id = target_nft.coin.name()
|
||||
else:
|
||||
return {"success": False, "error": f'Unknown ID type, {request["id"]}'}
|
||||
|
||||
return {"success": True, "pubkey": str(pubkey), "signature": str(signature)}
|
||||
assert latest_coin_id is not None
|
||||
return {
|
||||
"success": True,
|
||||
"pubkey": str(pubkey),
|
||||
"signature": str(signature),
|
||||
"latest_coin_id": latest_coin_id.hex(),
|
||||
}
|
||||
|
||||
##########################################################################################
|
||||
# CATs and Trading
|
||||
@ -1658,6 +1712,58 @@ class WalletRpcApi:
|
||||
success = True
|
||||
return {"success": success}
|
||||
|
||||
async def did_message_spend(self, request) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
wallet = self.service.wallet_state_manager.wallets[wallet_id]
|
||||
if wallet.type() != WalletType.DECENTRALIZED_ID.value:
|
||||
return {"success": False, "error": f"Wallet with id {wallet_id} is not a DID wallet"}
|
||||
assert isinstance(wallet, DIDWallet)
|
||||
coin_announcements: Set[bytes] = set([])
|
||||
for ca in request.get("coin_announcements", []):
|
||||
coin_announcements.add(bytes.fromhex(ca))
|
||||
puzzle_announcements: Set[bytes] = set([])
|
||||
for pa in request.get("puzzle_announcements", []):
|
||||
puzzle_announcements.add(bytes.fromhex(pa))
|
||||
spend_bundle = await wallet.create_message_spend(coin_announcements, puzzle_announcements)
|
||||
return {"success": True, "spend_bundle": spend_bundle}
|
||||
|
||||
async def did_get_info(self, request) -> EndpointResult:
|
||||
if "coin_id" not in request:
|
||||
return {"success": False, "error": "Coin ID is required."}
|
||||
coin_id = request["coin_id"]
|
||||
if coin_id.startswith(AddressType.DID.hrp(self.service.config)):
|
||||
coin_id = decode_puzzle_hash(coin_id)
|
||||
else:
|
||||
coin_id = bytes32.from_hexstr(coin_id)
|
||||
# Get coin state
|
||||
peer: Optional[WSChiaConnection] = self.service.get_full_node_peer()
|
||||
coin_spend, coin_state = await self.get_latest_coin_spend(peer, coin_id, request.get("latest", True))
|
||||
full_puzzle: Program = Program.from_bytes(bytes(coin_spend.puzzle_reveal))
|
||||
uncurried = uncurry_puzzle(full_puzzle)
|
||||
curried_args = match_did_puzzle(uncurried.mod, uncurried.args)
|
||||
if curried_args is None:
|
||||
return {"success": False, "error": "The coin is not a DID."}
|
||||
p2_puzzle, recovery_list_hash, num_verification, singleton_struct, metadata = curried_args
|
||||
uncurried_p2 = uncurry_puzzle(p2_puzzle)
|
||||
(public_key,) = uncurried_p2.args.as_iter()
|
||||
memos = compute_memos(SpendBundle([coin_spend], G2Element()))
|
||||
hints = []
|
||||
if coin_state.coin.name() in memos:
|
||||
for memo in memos[coin_state.coin.name()]:
|
||||
hints.append(memo.hex())
|
||||
return {
|
||||
"success": True,
|
||||
"latest_coin": coin_state.coin.name().hex(),
|
||||
"p2_address": encode_puzzle_hash(p2_puzzle.get_tree_hash(), AddressType.XCH.hrp(self.service.config)),
|
||||
"public_key": public_key.as_python().hex(),
|
||||
"recovery_list_hash": recovery_list_hash.as_python().hex(),
|
||||
"num_verification": num_verification.as_int(),
|
||||
"metadata": program_to_metadata(metadata),
|
||||
"launcher_id": singleton_struct.rest().first().as_python().hex(),
|
||||
"full_puzzle": full_puzzle,
|
||||
"hints": hints,
|
||||
}
|
||||
|
||||
async def did_update_metadata(self, request) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
wallet = self.service.wallet_state_manager.wallets[wallet_id]
|
||||
@ -2069,42 +2175,8 @@ class WalletRpcApi:
|
||||
coin_id = bytes32.from_hexstr(coin_id)
|
||||
# Get coin state
|
||||
peer: Optional[WSChiaConnection] = self.service.get_full_node_peer()
|
||||
if peer is None:
|
||||
raise ValueError("No peers to get info from")
|
||||
coin_state_list: List[CoinState] = await self.service.wallet_state_manager.wallet_node.get_coin_state(
|
||||
[coin_id], peer=peer
|
||||
)
|
||||
if coin_state_list is None or len(coin_state_list) < 1:
|
||||
return {"success": False, "error": f"Coin record 0x{coin_id.hex()} not found"}
|
||||
coin_state: CoinState = coin_state_list[0]
|
||||
if request.get("latest", True):
|
||||
# Find the unspent coin
|
||||
while coin_state.spent_height is not None:
|
||||
coin_state_list = await self.service.wallet_state_manager.wallet_node.fetch_children(
|
||||
coin_state.coin.name(), peer=peer
|
||||
)
|
||||
odd_coin = 0
|
||||
for coin in coin_state_list:
|
||||
if coin.coin.amount % 2 == 1:
|
||||
odd_coin += 1
|
||||
if odd_coin > 1:
|
||||
return {"success": False, "error": "This is not a singleton, multiple children coins found."}
|
||||
if odd_coin == 0:
|
||||
return {"success": False, "error": "Cannot find child coin, please wait then retry."}
|
||||
coin_state = coin_state_list[0]
|
||||
# Get parent coin
|
||||
parent_coin_state_list: List[CoinState] = await self.service.wallet_state_manager.wallet_node.get_coin_state(
|
||||
[coin_state.coin.parent_coin_info], peer=peer
|
||||
)
|
||||
if parent_coin_state_list is None or len(parent_coin_state_list) < 1:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Parent coin record 0x{coin_state.coin.parent_coin_info.hex()} not found",
|
||||
}
|
||||
parent_coin_state: CoinState = parent_coin_state_list[0]
|
||||
coin_spend: CoinSpend = await self.service.wallet_state_manager.wallet_node.fetch_puzzle_solution(
|
||||
parent_coin_state.spent_height, parent_coin_state.coin, peer
|
||||
)
|
||||
assert peer is not None
|
||||
coin_spend, coin_state = await self.get_latest_coin_spend(peer, coin_id, request.get("latest", True))
|
||||
# convert to NFTInfo
|
||||
# Check if the metadata is updated
|
||||
full_puzzle: Program = Program.from_bytes(bytes(coin_spend.puzzle_reveal))
|
||||
|
@ -8,10 +8,13 @@ from chia.consensus.block_rewards import calculate_pool_reward, calculate_base_f
|
||||
from chia.rpc.wallet_rpc_api import WalletRpcApi
|
||||
from chia.simulator.simulator_protocol import FarmNewBlockProtocol
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.condition_opcodes import ConditionOpcode
|
||||
from chia.types.peer_info import PeerInfo
|
||||
from chia.types.spend_bundle import SpendBundle
|
||||
from chia.util.bech32m import encode_puzzle_hash
|
||||
from chia.util.bech32m import encode_puzzle_hash, decode_puzzle_hash
|
||||
from chia.util.condition_tools import conditions_dict_for_solution
|
||||
from chia.util.ints import uint16, uint32, uint64
|
||||
from chia.wallet.did_wallet.did_wallet_puzzles import create_fullpuz
|
||||
from chia.wallet.util.address_type import AddressType
|
||||
|
||||
from chia.wallet.util.wallet_types import WalletType
|
||||
@ -766,13 +769,153 @@ class TestDIDWallet:
|
||||
assert did_wallet_1.did_info.backup_ids[0] == bytes(ph)
|
||||
assert did_wallet_1.did_info.num_of_backup_ids_needed == 1
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trusted",
|
||||
[True, False],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info(self, two_wallet_nodes, trusted):
|
||||
num_blocks = 3
|
||||
fee = uint64(1000)
|
||||
full_nodes, wallets, _ = two_wallet_nodes
|
||||
full_node_api = full_nodes[0]
|
||||
server_1 = full_node_api.server
|
||||
wallet_node, server_2 = wallets[0]
|
||||
wallet_node_2, server_3 = wallets[1]
|
||||
wallet = wallet_node.wallet_state_manager.main_wallet
|
||||
wallet1 = wallet_node_2.wallet_state_manager.main_wallet
|
||||
ph = await wallet.get_new_puzzlehash()
|
||||
ph1 = await wallet1.get_new_puzzlehash()
|
||||
api_0 = WalletRpcApi(wallet_node)
|
||||
if trusted:
|
||||
wallet_node.config["trusted_peers"] = {
|
||||
full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex()
|
||||
}
|
||||
wallet_node_2.config["trusted_peers"] = {
|
||||
full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex()
|
||||
}
|
||||
else:
|
||||
wallet_node.config["trusted_peers"] = {}
|
||||
wallet_node_2.config["trusted_peers"] = {}
|
||||
|
||||
await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
await server_3.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
|
||||
|
||||
funds = sum(
|
||||
[
|
||||
calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i))
|
||||
for i in range(1, num_blocks - 1)
|
||||
]
|
||||
)
|
||||
|
||||
await time_out_assert(15, wallet.get_confirmed_balance, funds)
|
||||
|
||||
async with wallet_node.wallet_state_manager.lock:
|
||||
did_wallet_1: DIDWallet = await DIDWallet.create_new_did_wallet(
|
||||
wallet_node.wallet_state_manager, wallet, uint64(101), [], metadata={"twitter": "twitter"}, fee=fee
|
||||
)
|
||||
spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(
|
||||
did_wallet_1.id()
|
||||
)
|
||||
spend_bundle = spend_bundle_list[0].spend_bundle
|
||||
await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name())
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1))
|
||||
await time_out_assert(15, did_wallet_1.get_confirmed_balance, 101)
|
||||
await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101)
|
||||
response = await api_0.did_get_info({"coin_id": did_wallet_1.did_info.origin_coin.name().hex()})
|
||||
|
||||
assert response["launcher_id"] == did_wallet_1.did_info.origin_coin.name().hex()
|
||||
assert response["full_puzzle"] == create_fullpuz(
|
||||
did_wallet_1.did_info.current_inner, did_wallet_1.did_info.origin_coin.name()
|
||||
)
|
||||
assert response["metadata"]["twitter"] == "twitter"
|
||||
assert response["latest_coin"] == (await did_wallet_1.select_coins(uint64(1))).pop().name().hex()
|
||||
assert response["num_verification"] == 0
|
||||
assert response["recovery_list_hash"] == Program(Program.to([])).get_tree_hash().hex()
|
||||
assert decode_puzzle_hash(response["p2_address"]).hex() == response["hints"][0]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trusted",
|
||||
[True, False],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_message_spend(self, two_wallet_nodes, trusted):
|
||||
num_blocks = 3
|
||||
fee = uint64(1000)
|
||||
full_nodes, wallets, _ = two_wallet_nodes
|
||||
full_node_api = full_nodes[0]
|
||||
server_1 = full_node_api.server
|
||||
wallet_node, server_2 = wallets[0]
|
||||
wallet_node_2, server_3 = wallets[1]
|
||||
wallet = wallet_node.wallet_state_manager.main_wallet
|
||||
wallet1 = wallet_node_2.wallet_state_manager.main_wallet
|
||||
ph = await wallet.get_new_puzzlehash()
|
||||
ph1 = await wallet1.get_new_puzzlehash()
|
||||
api_0 = WalletRpcApi(wallet_node)
|
||||
if trusted:
|
||||
wallet_node.config["trusted_peers"] = {
|
||||
full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex()
|
||||
}
|
||||
wallet_node_2.config["trusted_peers"] = {
|
||||
full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex()
|
||||
}
|
||||
else:
|
||||
wallet_node.config["trusted_peers"] = {}
|
||||
wallet_node_2.config["trusted_peers"] = {}
|
||||
|
||||
await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
await server_3.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
|
||||
|
||||
funds = sum(
|
||||
[
|
||||
calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i))
|
||||
for i in range(1, num_blocks - 1)
|
||||
]
|
||||
)
|
||||
|
||||
await time_out_assert(15, wallet.get_confirmed_balance, funds)
|
||||
|
||||
async with wallet_node.wallet_state_manager.lock:
|
||||
did_wallet_1: DIDWallet = await DIDWallet.create_new_did_wallet(
|
||||
wallet_node.wallet_state_manager, wallet, uint64(101), [], fee=fee
|
||||
)
|
||||
spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(
|
||||
did_wallet_1.id()
|
||||
)
|
||||
spend_bundle = spend_bundle_list[0].spend_bundle
|
||||
await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name())
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1))
|
||||
await time_out_assert(15, did_wallet_1.get_confirmed_balance, 101)
|
||||
await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101)
|
||||
response = await api_0.did_message_spend(
|
||||
{"wallet_id": did_wallet_1.wallet_id, "coin_announcements": ["0abc"], "puzzle_announcements": ["0def"]}
|
||||
)
|
||||
assert "spend_bundle" in response
|
||||
spend = response["spend_bundle"].coin_spends[0]
|
||||
error, conditions, cost = conditions_dict_for_solution(
|
||||
spend.puzzle_reveal.to_program(),
|
||||
spend.solution.to_program(),
|
||||
wallet.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM,
|
||||
)
|
||||
|
||||
assert len(conditions[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT]) == 1
|
||||
assert conditions[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT][0].vars[0].hex() == "0abc"
|
||||
assert len(conditions[ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT]) == 1
|
||||
assert conditions[ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT][0].vars[0].hex() == "0def"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trusted",
|
||||
[True, False],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_metadata(self, two_wallet_nodes, trusted):
|
||||
num_blocks = 5
|
||||
num_blocks = 3
|
||||
fee = uint64(1000)
|
||||
full_nodes, wallets, _ = two_wallet_nodes
|
||||
full_node_api = full_nodes[0]
|
||||
@ -821,8 +964,8 @@ class TestDIDWallet:
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1))
|
||||
await time_out_assert(15, did_wallet_1.get_confirmed_balance, 101)
|
||||
await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101)
|
||||
await time_out_assert(15, wallet.get_confirmed_balance, 7999999998899)
|
||||
await time_out_assert(15, wallet.get_unconfirmed_balance, 7999999998899)
|
||||
await time_out_assert(15, wallet.get_confirmed_balance, 3999999998899)
|
||||
await time_out_assert(15, wallet.get_unconfirmed_balance, 3999999998899)
|
||||
metadata = {}
|
||||
metadata["Twitter"] = "http://www.twitter.com"
|
||||
await did_wallet_1.update_metadata(metadata)
|
||||
@ -832,8 +975,8 @@ class TestDIDWallet:
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1))
|
||||
await time_out_assert(15, did_wallet_1.get_confirmed_balance, 101)
|
||||
await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101)
|
||||
await time_out_assert(15, wallet.get_confirmed_balance, 7999999997899)
|
||||
await time_out_assert(15, wallet.get_unconfirmed_balance, 7999999997899)
|
||||
await time_out_assert(15, wallet.get_confirmed_balance, 3999999997899)
|
||||
await time_out_assert(15, wallet.get_unconfirmed_balance, 3999999997899)
|
||||
assert did_wallet_1.did_info.metadata.find("Twitter") > 0
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
Loading…
Reference in New Issue
Block a user