diff --git a/crates/chia-sdk-driver/src/layers/p2_singleton.rs b/crates/chia-sdk-driver/src/layers/p2_singleton.rs index e3da7e21..ac8da523 100644 --- a/crates/chia-sdk-driver/src/layers/p2_singleton.rs +++ b/crates/chia-sdk-driver/src/layers/p2_singleton.rs @@ -5,7 +5,7 @@ use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; use clvmr::{Allocator, NodePtr}; use hex_literal::hex; -use crate::{DriverError, Layer, Puzzle, SpendContext}; +use crate::{DriverError, Layer, Puzzle, Spend, SpendContext}; /// The p2 singleton [`Layer`] allows for requiring that a /// singleton be spent alongside this coin to authorize it. @@ -20,6 +20,23 @@ impl P2Singleton { } pub fn spend( + &self, + ctx: &mut SpendContext, + coin_id: Bytes32, + singleton_inner_puzzle_hash: Bytes32, + ) -> Result { + let puzzle = self.construct_puzzle(ctx)?; + let solution = self.construct_solution( + ctx, + P2SingletonSolution { + singleton_inner_puzzle_hash, + my_id: coin_id, + }, + )?; + Ok(Spend { puzzle, solution }) + } + + pub fn spend_coin( &self, ctx: &mut SpendContext, coin: Coin, @@ -187,7 +204,7 @@ mod tests { )?; let p2_coin = Coin::new(coin.coin_id(), p2_singleton_hash, 1); - p2_singleton.spend(ctx, p2_coin, puzzle_hash)?; + p2_singleton.spend_coin(ctx, p2_coin, puzzle_hash)?; let inner_solution = p2 .spend_with_conditions( diff --git a/napi/index.d.ts b/napi/index.d.ts index 95382ed9..85561671 100644 --- a/napi/index.d.ts +++ b/napi/index.d.ts @@ -49,6 +49,7 @@ export interface ParsedNft { } export declare function parseNftInfo(puzzleReveal: Uint8Array): ParsedNft | null export declare function parseUnspentNft(parentCoin: Coin, parentPuzzleReveal: Uint8Array, parentSolution: Uint8Array, coin: Coin): Nft | null +export declare function spendNft(nft: Nft, innerSpend: Spend): Array export interface NftMint { metadata: NftMetadata p2PuzzleHash: Uint8Array @@ -61,3 +62,9 @@ export interface MintedNfts { parentConditions: Array } export declare function mintNfts(parentCoinId: Uint8Array, nftMints: Array): MintedNfts +export interface Spend { + puzzle: Uint8Array + solution: Uint8Array +} +export declare function spendP2Standard(syntheticKey: Uint8Array, conditions: Array): Spend +export declare function spendP2Singleton(launcherId: Uint8Array, coinId: Uint8Array, singletonInnerPuzzleHash: Uint8Array): Spend diff --git a/napi/index.js b/napi/index.js index 168d4d0e..b247e8e1 100644 --- a/napi/index.js +++ b/napi/index.js @@ -310,9 +310,12 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { toCoinId, parseNftInfo, parseUnspentNft, mintNfts } = nativeBinding +const { toCoinId, parseNftInfo, parseUnspentNft, spendNft, mintNfts, spendP2Standard, spendP2Singleton } = nativeBinding module.exports.toCoinId = toCoinId module.exports.parseNftInfo = parseNftInfo module.exports.parseUnspentNft = parseUnspentNft +module.exports.spendNft = spendNft module.exports.mintNfts = mintNfts +module.exports.spendP2Standard = spendP2Standard +module.exports.spendP2Singleton = spendP2Singleton diff --git a/napi/src/lib.rs b/napi/src/lib.rs index 66181044..5299d824 100644 --- a/napi/src/lib.rs +++ b/napi/src/lib.rs @@ -10,6 +10,7 @@ mod coin_spend; mod lineage_proof; mod nft; mod nft_mint; +mod spend; mod traits; pub use coin::*; @@ -17,3 +18,4 @@ pub use coin_spend::*; pub use lineage_proof::*; pub use nft::*; pub use nft_mint::*; +pub use spend::*; diff --git a/napi/src/nft.rs b/napi/src/nft.rs index 787cc437..977c6ed3 100644 --- a/napi/src/nft.rs +++ b/napi/src/nft.rs @@ -1,5 +1,5 @@ use chia::puzzles::nft; -use chia_wallet_sdk::{self as sdk, Primitive}; +use chia_wallet_sdk::{self as sdk, Primitive, SpendContext}; use clvmr::{ serde::{node_from_bytes, node_to_bytes}, Allocator, @@ -8,7 +8,7 @@ use napi::bindgen_prelude::*; use crate::{ traits::{FromJs, IntoJs, IntoRust}, - Coin, LineageProof, + Coin, CoinSpend, LineageProof, Spend, }; #[napi(object)] @@ -28,6 +28,16 @@ impl IntoJs for sdk::Nft { } } +impl FromJs for sdk::Nft { + fn from_js(nft: Nft) -> Result { + Ok(sdk::Nft { + coin: nft.coin.into_rust()?, + proof: nft.lineage_proof.into_rust()?, + info: nft.info.into_rust()?, + }) + } +} + #[napi(object)] pub struct NftInfo { pub launcher_id: Uint8Array, @@ -53,6 +63,20 @@ impl IntoJs for sdk::NftInfo { } } +impl FromJs for sdk::NftInfo { + fn from_js(info: NftInfo) -> Result { + Ok(sdk::NftInfo { + launcher_id: info.launcher_id.into_rust()?, + metadata: info.metadata.into_rust()?, + metadata_updater_puzzle_hash: info.metadata_updater_puzzle_hash.into_rust()?, + current_owner: info.current_owner.map(IntoRust::into_rust).transpose()?, + royalty_puzzle_hash: info.royalty_puzzle_hash.into_rust()?, + royalty_ten_thousandths: info.royalty_ten_thousandths, + p2_puzzle_hash: info.p2_puzzle_hash.into_rust()?, + }) + } +} + #[napi(object)] pub struct NftMetadata { pub edition_number: BigInt, @@ -149,3 +173,17 @@ pub fn parse_unspent_nft( Ok(Some(nft.into_js()?)) } + +#[napi] +pub fn spend_nft(nft: Nft, inner_spend: Spend) -> Result> { + let mut ctx = SpendContext::new(); + + let nft = sdk::Nft::::from_js(nft)?; + let puzzle = node_from_bytes(&mut ctx.allocator, &inner_spend.puzzle)?; + let solution = node_from_bytes(&mut ctx.allocator, &inner_spend.solution)?; + + nft.spend(&mut ctx, sdk::Spend { puzzle, solution }) + .map_err(|error| Error::from_reason(error.to_string()))?; + + ctx.take().into_iter().map(IntoJs::into_js).collect() +} diff --git a/napi/src/spend.rs b/napi/src/spend.rs new file mode 100644 index 00000000..de0f4c6e --- /dev/null +++ b/napi/src/spend.rs @@ -0,0 +1,68 @@ +use chia::{bls::PublicKey, clvm_traits::FromClvm}; +use chia_wallet_sdk::{ + Condition, Conditions, P2Singleton, SpendContext, SpendWithConditions, StandardLayer, +}; +use clvmr::{ + serde::{node_from_bytes, node_to_bytes}, + NodePtr, +}; +use napi::bindgen_prelude::*; + +use crate::traits::{FromJs, IntoRust}; + +#[napi(object)] +pub struct Spend { + pub puzzle: Uint8Array, + pub solution: Uint8Array, +} + +#[napi] +pub fn spend_p2_standard(synthetic_key: Uint8Array, conditions: Vec) -> Result { + let mut ctx = SpendContext::new(); + + let synthetic_key = PublicKey::from_js(synthetic_key)?; + let p2 = StandardLayer::new(synthetic_key); + + let mut spend_conditions = Conditions::new(); + + for condition in conditions { + let condition = node_from_bytes(&mut ctx.allocator, &condition)?; + spend_conditions = spend_conditions.with( + Condition::::from_clvm(&ctx.allocator, condition) + .map_err(|error| Error::from_reason(error.to_string()))?, + ); + } + + let spend = p2 + .spend_with_conditions(&mut ctx, spend_conditions) + .map_err(|error| Error::from_reason(error.to_string()))?; + + Ok(Spend { + puzzle: node_to_bytes(&ctx.allocator, spend.puzzle)?.into(), + solution: node_to_bytes(&ctx.allocator, spend.solution)?.into(), + }) +} + +#[napi] +pub fn spend_p2_singleton( + launcher_id: Uint8Array, + coin_id: Uint8Array, + singleton_inner_puzzle_hash: Uint8Array, +) -> Result { + let mut ctx = SpendContext::new(); + + let p2 = P2Singleton::new(launcher_id.into_rust()?); + + let spend = p2 + .spend( + &mut ctx, + coin_id.into_rust()?, + singleton_inner_puzzle_hash.into_rust()?, + ) + .map_err(|error| Error::from_reason(error.to_string()))?; + + Ok(Spend { + puzzle: node_to_bytes(&ctx.allocator, spend.puzzle)?.into(), + solution: node_to_bytes(&ctx.allocator, spend.solution)?.into(), + }) +} diff --git a/napi/src/traits.rs b/napi/src/traits.rs index ed27de80..b4ed1258 100644 --- a/napi/src/traits.rs +++ b/napi/src/traits.rs @@ -1,4 +1,7 @@ -use chia::protocol::{BytesImpl, Program}; +use chia::{ + bls::PublicKey, + protocol::{BytesImpl, Program}, +}; use napi::bindgen_prelude::*; pub(crate) trait IntoJs { @@ -40,6 +43,36 @@ impl FromJs for BytesImpl { } } +impl IntoJs for [u8; N] { + fn into_js(self) -> Result { + Ok(Uint8Array::new(self.to_vec())) + } +} + +impl FromJs for [u8; N] { + fn from_js(js_value: Uint8Array) -> Result { + js_value.to_vec().try_into().map_err(|bytes: Vec| { + Error::from_reason(format!("Expected length {N}, found {}", bytes.len())) + }) + } +} + +impl IntoJs for PublicKey { + fn into_js(self) -> Result { + Ok(Uint8Array::new(self.to_bytes().to_vec())) + } +} + +impl FromJs for PublicKey { + fn from_js(js_value: Uint8Array) -> Result + where + Self: Sized, + { + PublicKey::from_bytes(&js_value.into_rust()?) + .map_err(|error| Error::from_reason(error.to_string())) + } +} + impl IntoJs for Program { fn into_js(self) -> Result { Ok(Uint8Array::new(self.to_vec()))