diff --git a/Cargo.lock b/Cargo.lock index 76888771..0511b99b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -587,6 +587,7 @@ version = "0.0.0" dependencies = [ "chia", "chia-wallet-sdk", + "clvmr", "napi", "napi-build", "napi-derive", diff --git a/napi/Cargo.toml b/napi/Cargo.toml index c31936a6..d92f9b35 100644 --- a/napi/Cargo.toml +++ b/napi/Cargo.toml @@ -22,6 +22,7 @@ napi = { workspace = true, features = ["napi6"] } napi-derive = { workspace = true } chia-wallet-sdk = { workspace = true } chia = { workspace = true } +clvmr = { workspace = true } [build-dependencies] napi-build = "2.0.1" diff --git a/napi/index.d.ts b/napi/index.d.ts index c5fcf3db..90ae3e56 100644 --- a/napi/index.d.ts +++ b/napi/index.d.ts @@ -14,3 +14,38 @@ export interface CoinSpend { puzzleReveal: Uint8Array solution: Uint8Array } +export interface LineageProof { + parentParentCoinInfo: Uint8Array + parentInnerPuzzleHash?: Uint8Array + parentAmount: bigint +} +export interface Nft { + coin: Coin + lineageProof: LineageProof + info: NftInfo +} +export interface NftInfo { + launcherId: Uint8Array + metadata: NftMetadata + metadataUpdaterPuzzleHash: Uint8Array + currentOwner?: Uint8Array + royaltyPuzzleHash: Uint8Array + royaltyTenThousandths: number + p2PuzzleHash: Uint8Array +} +export interface NftMetadata { + editionNumber: bigint + editionTotal: bigint + dataUris: Array + dataHash?: Uint8Array + metadataUris: Array + metadataHash?: Uint8Array + licenseUris: Array + licenseHash?: Uint8Array +} +export interface ParsedNft { + nftInfo: NftInfo + innerPuzzle: Uint8Array +} +export declare function parseNftInfo(puzzleReveal: Uint8Array): ParsedNft | null +export declare function parseUnspentNft(parentCoin: Coin, parentPuzzleReveal: Uint8Array, parentSolution: Uint8Array, coin: Coin): Nft | null diff --git a/napi/index.js b/napi/index.js index 188c001c..171c4891 100644 --- a/napi/index.js +++ b/napi/index.js @@ -310,6 +310,8 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { toCoinId } = nativeBinding +const { toCoinId, parseNftInfo, parseUnspentNft } = nativeBinding module.exports.toCoinId = toCoinId +module.exports.parseNftInfo = parseNftInfo +module.exports.parseUnspentNft = parseUnspentNft diff --git a/napi/src/lib.rs b/napi/src/lib.rs index e890e967..fd6788b8 100644 --- a/napi/src/lib.rs +++ b/napi/src/lib.rs @@ -1,11 +1,17 @@ #![allow(missing_debug_implementations)] +#![allow(missing_copy_implementations)] +#![allow(clippy::needless_pass_by_value)] #[macro_use] extern crate napi_derive; mod coin; mod coin_spend; +mod lineage_proof; +mod nft; mod traits; pub use coin::*; pub use coin_spend::*; +pub use lineage_proof::*; +pub use nft::*; diff --git a/napi/src/lineage_proof.rs b/napi/src/lineage_proof.rs new file mode 100644 index 00000000..bff06c65 --- /dev/null +++ b/napi/src/lineage_proof.rs @@ -0,0 +1,45 @@ +use chia::puzzles; +use napi::bindgen_prelude::*; + +use crate::traits::{FromJs, IntoJs, IntoRust}; + +#[napi(object)] +pub struct LineageProof { + pub parent_parent_coin_info: Uint8Array, + pub parent_inner_puzzle_hash: Option, + pub parent_amount: BigInt, +} + +impl FromJs for puzzles::Proof { + fn from_js(value: LineageProof) -> Result { + if let Some(parent_inner_puzzle_hash) = value.parent_inner_puzzle_hash { + Ok(Self::Lineage(puzzles::LineageProof { + parent_parent_coin_info: value.parent_parent_coin_info.into_rust()?, + parent_inner_puzzle_hash: parent_inner_puzzle_hash.into_rust()?, + parent_amount: value.parent_amount.into_rust()?, + })) + } else { + Ok(Self::Eve(puzzles::EveProof { + parent_parent_coin_info: value.parent_parent_coin_info.into_rust()?, + parent_amount: value.parent_amount.into_rust()?, + })) + } + } +} + +impl IntoJs for puzzles::Proof { + fn into_js(self) -> Result { + match self { + Self::Lineage(proof) => Ok(LineageProof { + parent_parent_coin_info: proof.parent_parent_coin_info.into_js()?, + parent_inner_puzzle_hash: Some(proof.parent_inner_puzzle_hash.into_js()?), + parent_amount: proof.parent_amount.into_js()?, + }), + Self::Eve(proof) => Ok(LineageProof { + parent_parent_coin_info: proof.parent_parent_coin_info.into_js()?, + parent_inner_puzzle_hash: None, + parent_amount: proof.parent_amount.into_js()?, + }), + } + } +} diff --git a/napi/src/nft.rs b/napi/src/nft.rs new file mode 100644 index 00000000..c6a4c295 --- /dev/null +++ b/napi/src/nft.rs @@ -0,0 +1,133 @@ +use chia::puzzles::nft; +use chia_wallet_sdk::{self as sdk, Primitive}; +use clvmr::{ + serde::{node_from_bytes, node_to_bytes}, + Allocator, +}; +use napi::bindgen_prelude::*; + +use crate::{ + traits::{IntoJs, IntoRust}, + Coin, LineageProof, +}; + +#[napi(object)] +pub struct Nft { + pub coin: Coin, + pub lineage_proof: LineageProof, + pub info: NftInfo, +} + +impl IntoJs for sdk::Nft { + fn into_js(self) -> Result { + Ok(Nft { + coin: self.coin.into_js()?, + lineage_proof: self.proof.into_js()?, + info: self.info.into_js()?, + }) + } +} + +#[napi(object)] +pub struct NftInfo { + pub launcher_id: Uint8Array, + pub metadata: NftMetadata, + pub metadata_updater_puzzle_hash: Uint8Array, + pub current_owner: Option, + pub royalty_puzzle_hash: Uint8Array, + pub royalty_ten_thousandths: u16, + pub p2_puzzle_hash: Uint8Array, +} + +impl IntoJs for sdk::NftInfo { + fn into_js(self) -> Result { + Ok(NftInfo { + launcher_id: self.launcher_id.into_js()?, + metadata: self.metadata.into_js()?, + metadata_updater_puzzle_hash: self.metadata_updater_puzzle_hash.into_js()?, + current_owner: self.current_owner.map(IntoJs::into_js).transpose()?, + royalty_puzzle_hash: self.royalty_puzzle_hash.into_js()?, + royalty_ten_thousandths: self.royalty_ten_thousandths, + p2_puzzle_hash: self.p2_puzzle_hash.into_js()?, + }) + } +} + +#[napi(object)] +pub struct NftMetadata { + pub edition_number: BigInt, + pub edition_total: BigInt, + pub data_uris: Vec, + pub data_hash: Option, + pub metadata_uris: Vec, + pub metadata_hash: Option, + pub license_uris: Vec, + pub license_hash: Option, +} + +impl IntoJs for nft::NftMetadata { + fn into_js(self) -> Result { + Ok(NftMetadata { + edition_number: self.edition_number.into_js()?, + edition_total: self.edition_total.into_js()?, + data_uris: self.data_uris, + data_hash: self.data_hash.map(IntoJs::into_js).transpose()?, + metadata_uris: self.metadata_uris, + metadata_hash: self.metadata_hash.map(IntoJs::into_js).transpose()?, + license_uris: self.license_uris, + license_hash: self.license_hash.map(IntoJs::into_js).transpose()?, + }) + } +} + +#[napi(object)] +pub struct ParsedNft { + pub nft_info: NftInfo, + pub inner_puzzle: Uint8Array, +} + +#[napi] +pub fn parse_nft_info(puzzle_reveal: Uint8Array) -> Result> { + let mut allocator = Allocator::new(); + let ptr = node_from_bytes(&mut allocator, puzzle_reveal.as_ref())?; + let puzzle = sdk::Puzzle::parse(&allocator, ptr); + + let Some((nft_info, inner_puzzle)) = + sdk::NftInfo::::parse(&allocator, puzzle) + .map_err(|error| Error::from_reason(error.to_string()))? + else { + return Ok(None); + }; + + Ok(Some(ParsedNft { + nft_info: nft_info.into_js()?, + inner_puzzle: node_to_bytes(&allocator, inner_puzzle.ptr())?.into(), + })) +} + +#[napi] +pub fn parse_unspent_nft( + parent_coin: Coin, + parent_puzzle_reveal: Uint8Array, + parent_solution: Uint8Array, + coin: Coin, +) -> Result> { + let mut allocator = Allocator::new(); + let parent_ptr = node_from_bytes(&mut allocator, parent_puzzle_reveal.as_ref())?; + let parent_puzzle = sdk::Puzzle::parse(&allocator, parent_ptr); + let parent_solution = node_from_bytes(&mut allocator, parent_solution.as_ref())?; + + let Some(nft) = sdk::Nft::::from_parent_spend( + &mut allocator, + parent_coin.into_rust()?, + parent_puzzle, + parent_solution, + coin.into_rust()?, + ) + .map_err(|error| Error::from_reason(error.to_string()))? + else { + return Ok(None); + }; + + Ok(Some(nft.into_js()?)) +} diff --git a/napi/src/traits.rs b/napi/src/traits.rs index da0502c7..548815c3 100644 --- a/napi/src/traits.rs +++ b/napi/src/traits.rs @@ -15,8 +15,6 @@ pub(crate) trait IntoRust { fn into_rust(self) -> Result; } -// Implement ToRust for every type that implements FromJs - impl IntoRust for T where U: FromJs,