feat(vm): implement NftTransfer and CollectionClaim execution
- NftTransfer: validates ownership, NFT not in collection, deducts fee, updates owner field + nft_ids on sender/recipient accounts - CollectionClaim: validates lineage with validate_lineage(), all NFTs owned by sender and unclaimed; sets collection_id on all NFTs in path - Engine: wire both variants; NftMint remains internal-only (returns error) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3711bc450c
commit
ec355b40d2
2 changed files with 114 additions and 7 deletions
|
|
@ -2,8 +2,8 @@ use nu_block::types::{Block, RawTransaction, TxPayload, TxReceipt};
|
||||||
use nu_state::StateAccessor;
|
use nu_state::StateAccessor;
|
||||||
|
|
||||||
use crate::executor::{
|
use crate::executor::{
|
||||||
execute_node_approve, execute_node_reject, execute_node_submit,
|
execute_collection_claim, execute_node_approve, execute_node_reject, execute_node_submit,
|
||||||
execute_stake_op, execute_token_transfer, execute_validator_register,
|
execute_nft_transfer, execute_stake_op, execute_token_transfer, execute_validator_register,
|
||||||
execute_vote_cast, execute_vote_register, execute_voting_open,
|
execute_vote_cast, execute_vote_register, execute_voting_open,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
};
|
};
|
||||||
|
|
@ -68,11 +68,15 @@ fn execute_tx(
|
||||||
TxPayload::NodeReject { node_id } => {
|
TxPayload::NodeReject { node_id } => {
|
||||||
execute_node_reject(&ctx, node_id)
|
execute_node_reject(&ctx, node_id)
|
||||||
}
|
}
|
||||||
// Faz 2 later
|
TxPayload::NftTransfer { nft_id, to } => {
|
||||||
TxPayload::NftMint { .. }
|
execute_nft_transfer(&ctx, &tx.sender, tx.nonce, tx.fee, nft_id, to)
|
||||||
| TxPayload::NftTransfer { .. }
|
}
|
||||||
| TxPayload::CollectionClaim { .. } => {
|
TxPayload::CollectionClaim { nft_ids } => {
|
||||||
Err(crate::errors::VmError::Unknown(format!("{} not yet implemented", payload_name(&tx.payload))))
|
execute_collection_claim(&ctx, &tx.sender, tx.nonce, tx.fee, nft_ids)
|
||||||
|
}
|
||||||
|
// Minted automatically via NodeApprove — not user-submitted
|
||||||
|
TxPayload::NftMint { .. } => {
|
||||||
|
Err(crate::errors::VmError::Unknown("NftMint not a user tx".into()))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,109 @@ pub fn execute_node_approve(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Transfer an NFT from sender to recipient.
|
||||||
|
pub fn execute_nft_transfer(
|
||||||
|
ctx: &ExecutionContext,
|
||||||
|
sender: &str,
|
||||||
|
nonce: u64,
|
||||||
|
fee: u64,
|
||||||
|
nft_id: &str,
|
||||||
|
to: &str,
|
||||||
|
) -> Result<(), VmError> {
|
||||||
|
let expected_nonce = ctx.state.get_nonce(sender);
|
||||||
|
if nonce != expected_nonce {
|
||||||
|
return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce });
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut nft = ctx.state.get_nft(nft_id)
|
||||||
|
.ok_or_else(|| VmError::Unknown(format!("NFT {nft_id} not found")))?;
|
||||||
|
|
||||||
|
if nft.owner != sender {
|
||||||
|
return Err(VmError::Unknown(format!("sender does not own NFT {nft_id}")));
|
||||||
|
}
|
||||||
|
if !nft.collection_id.is_empty() {
|
||||||
|
return Err(VmError::Unknown("NFT is part of a collection — cannot transfer".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let balance = ctx.state.get_balance(sender);
|
||||||
|
if balance < fee {
|
||||||
|
return Err(VmError::InsufficientBalance { need: fee, have: balance });
|
||||||
|
}
|
||||||
|
|
||||||
|
nft.owner = to.to_string();
|
||||||
|
ctx.state.set_nft(&nft);
|
||||||
|
|
||||||
|
// Update NFT id lists on both accounts
|
||||||
|
let mut sender_acct = ctx.state.get_account(sender);
|
||||||
|
sender_acct.nft_ids.retain(|id| id != nft_id);
|
||||||
|
sender_acct.balance -= fee;
|
||||||
|
sender_acct.nonce += 1;
|
||||||
|
ctx.state.set_account(&sender_acct);
|
||||||
|
|
||||||
|
let mut recipient_acct = ctx.state.get_account(to);
|
||||||
|
recipient_acct.nft_ids.push(nft_id.to_string());
|
||||||
|
ctx.state.set_account(&recipient_acct);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Claim an exclusive collection from a valid lineage of NFTs.
|
||||||
|
/// All NFTs must be owned by sender and form a valid prefix-extension path.
|
||||||
|
pub fn execute_collection_claim(
|
||||||
|
ctx: &ExecutionContext,
|
||||||
|
sender: &str,
|
||||||
|
nonce: u64,
|
||||||
|
fee: u64,
|
||||||
|
nft_ids: &[String],
|
||||||
|
) -> Result<(), VmError> {
|
||||||
|
use nu_state::nft::validate_lineage;
|
||||||
|
|
||||||
|
let expected_nonce = ctx.state.get_nonce(sender);
|
||||||
|
if nonce != expected_nonce {
|
||||||
|
return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce });
|
||||||
|
}
|
||||||
|
if nft_ids.is_empty() {
|
||||||
|
return Err(VmError::Unknown("nft_ids cannot be empty".into()));
|
||||||
|
}
|
||||||
|
if !validate_lineage(nft_ids) {
|
||||||
|
return Err(VmError::Unknown("NFT ids do not form a valid lineage".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let balance = ctx.state.get_balance(sender);
|
||||||
|
if balance < fee {
|
||||||
|
return Err(VmError::InsufficientBalance { need: fee, have: balance });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ownership and free status
|
||||||
|
let mut nfts = Vec::with_capacity(nft_ids.len());
|
||||||
|
for id in nft_ids {
|
||||||
|
let nft = ctx.state.get_nft(id)
|
||||||
|
.ok_or_else(|| VmError::Unknown(format!("NFT {id} not found")))?;
|
||||||
|
if nft.owner != sender {
|
||||||
|
return Err(VmError::Unknown(format!("sender does not own NFT {id}")));
|
||||||
|
}
|
||||||
|
if !nft.collection_id.is_empty() {
|
||||||
|
return Err(VmError::Unknown(format!("NFT {id} already in a collection")));
|
||||||
|
}
|
||||||
|
nfts.push(nft);
|
||||||
|
}
|
||||||
|
|
||||||
|
// collection_id = last nft_id in path (the deepest node)
|
||||||
|
let collection_id = nft_ids.last().unwrap().clone();
|
||||||
|
|
||||||
|
for mut nft in nfts {
|
||||||
|
nft.collection_id = collection_id.clone();
|
||||||
|
ctx.state.set_nft(&nft);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut account = ctx.state.get_account(sender);
|
||||||
|
account.balance -= fee;
|
||||||
|
account.nonce += 1;
|
||||||
|
ctx.state.set_account(&account);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Auto-tx: reject node, burn entry fee, unlock stakes.
|
/// Auto-tx: reject node, burn entry fee, unlock stakes.
|
||||||
pub fn execute_node_reject(
|
pub fn execute_node_reject(
|
||||||
ctx: &ExecutionContext,
|
ctx: &ExecutionContext,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue