From 7d52867e3c94cfe4e4e70ef7db96e16c6d91ed73 Mon Sep 17 00:00:00 2001 From: TimothyBanks Date: Tue, 11 Aug 2026 09:44:00 -0400 Subject: [PATCH] feat: Hook up nft_uri, nft_issuer, nft_taxon, nft_flags, nft_xfer_fee, nft_serial host functions --- crates/xrpl-host-functions/src/lib.rs | 36 +++++ .../tests/generated_abi.rs | 72 ++++++++++ crates/xrpl-wasm-vm-ffi/src/lib.rs | 48 +++++++ crates/xrpl-wasm-vm/src/abi.rs | 18 +++ crates/xrpl-wasm-vm/src/register.rs | 103 +++++++++++++++ crates/xrpl-wasm-vm/tests/budgets.rs | 30 +++++ crates/xrpl-wasm-vm/tests/host_calls.rs | 99 ++++++++++++++ crates/xrpl-wasm-vm/tests/preflight.rs | 8 +- crates/xrpl-wasm-vm/tests/support/mod.rs | 125 ++++++++++++++++++ include/xrpl/tx/wasm/HostContext.h | 36 +++++ src/libxrpl/tx/wasm/HostContext.cpp | 97 ++++++++++++++ 11 files changed, 671 insertions(+), 1 deletion(-) diff --git a/crates/xrpl-host-functions/src/lib.rs b/crates/xrpl-host-functions/src/lib.rs index c44b4add21..36fe13b7ca 100644 --- a/crates/xrpl-host-functions/src/lib.rs +++ b/crates/xrpl-host-functions/src/lib.rs @@ -427,4 +427,40 @@ host_functions! { #[gas = 1000] #[wasm_name = "set_data"] fn update_data(&self, data: &[u8]) -> HostResult; + + /// The URI of the `NFToken` with id `nft_id` (32 bytes) held by the 20-byte + /// `account`. Reads both regions and writes the URI bytes. + #[gas = 5000] + #[wasm_name = "nft_uri"] + fn get_nft(&self, account: &[u8], nft_id: &[u8], out: &mut [u8]) -> HostResult; + + /// The 20-byte issuer account encoded in the `NFToken` id `nft_id` (32 bytes). + /// Reads the id region and writes the issuer bytes. + #[gas = 70] + #[wasm_name = "nft_issuer"] + fn get_nft_issuer(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult; + + /// The taxon encoded in the `NFToken` id `nft_id` (32 bytes). Reads the id region + /// and writes the taxon as its four little-endian bytes. + #[gas = 60] + #[wasm_name = "nft_taxon"] + fn get_nft_taxon(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult; + + /// The flags encoded in the `NFToken` id `nft_id` (32 bytes). Reads the id region + /// and returns the flags as the call's scalar result. + #[gas = 60] + #[wasm_name = "nft_flags"] + fn get_nft_flags(&self, nft_id: &[u8]) -> HostResult; + + /// The transfer fee encoded in the `NFToken` id `nft_id` (32 bytes). Reads the id + /// region and returns the fee as the call's scalar result. + #[gas = 60] + #[wasm_name = "nft_xfer_fee"] + fn get_nft_transfer_fee(&self, nft_id: &[u8]) -> HostResult; + + /// The sequence number encoded in the `NFToken` id `nft_id` (32 bytes). Reads the + /// id region and writes the sequence as its four little-endian bytes. + #[gas = 60] + #[wasm_name = "nft_serial"] + fn get_nft_sequence(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult; } diff --git a/crates/xrpl-host-functions/tests/generated_abi.rs b/crates/xrpl-host-functions/tests/generated_abi.rs index db520485c3..543857139d 100644 --- a/crates/xrpl-host-functions/tests/generated_abi.rs +++ b/crates/xrpl-host-functions/tests/generated_abi.rs @@ -406,6 +406,55 @@ impl HostFunctions for FakeHost { fn update_data(&self, data: &[u8]) -> HostResult { Ok(data.len() as i32) } + + /// Reads an account and an nft id, writes a byte value; `InvalidParams` if either + /// is empty. + fn get_nft(&self, account: &[u8], nft_id: &[u8], out: &mut [u8]) -> HostResult { + if account.is_empty() || nft_id.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[account[0]; HASH_LEN]) + } + + /// Reads an nft id, writes a byte value; `InvalidParams` on an empty id. + fn get_nft_issuer(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + if nft_id.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &[nft_id[0]; HASH_LEN]) + } + + /// The same, for the taxon. + fn get_nft_taxon(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + if nft_id.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &nft_id[0].to_le_bytes()) + } + + /// Reads an nft id and returns a scalar; `InvalidParams` on an empty id. + fn get_nft_flags(&self, nft_id: &[u8]) -> HostResult { + if nft_id.is_empty() { + return Err(HostError::InvalidParams); + } + Ok(i32::from(nft_id[0])) + } + + /// The same, for the transfer fee. + fn get_nft_transfer_fee(&self, nft_id: &[u8]) -> HostResult { + if nft_id.is_empty() { + return Err(HostError::InvalidParams); + } + Ok(i32::from(nft_id[0])) + } + + /// The same byte-output shape, for the sequence number. + fn get_nft_sequence(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + if nft_id.is_empty() { + return Err(HostError::InvalidParams); + } + put(out, &nft_id[0].to_le_bytes()) + } } #[test] @@ -621,6 +670,23 @@ fn the_trait_is_implementable() { assert_eq!(host.trace("hello", b"xy", true), Ok(())); assert_eq!(host.trace_num("count", -1), Ok(())); assert_eq!(host.update_data(b"abcd"), Ok(4)); + assert_eq!(host.get_nft(&[7; 20], &[9; 32], &mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 7); + assert_eq!( + host.get_nft(&[], &[9; 32], &mut out), + Err(HostError::InvalidParams) + ); + assert_eq!(host.get_nft_issuer(&[9; 32], &mut out), Ok(HASH_LEN)); + assert_eq!(out[0], 9); + assert_eq!( + host.get_nft_issuer(&[], &mut out), + Err(HostError::InvalidParams) + ); + assert_eq!(host.get_nft_taxon(&[9; 32], &mut out), Ok(1)); + assert_eq!(host.get_nft_flags(&[9; 32]), Ok(9)); + assert_eq!(host.get_nft_flags(&[]), Err(HostError::InvalidParams)); + assert_eq!(host.get_nft_transfer_fee(&[9; 32]), Ok(9)); + assert_eq!(host.get_nft_sequence(&[9; 32], &mut out), Ok(1)); assert_eq!(*host.traced.borrow(), ["hello/2/true", "count=-1"]); } @@ -726,6 +792,12 @@ fn the_spec_table_matches_the_declarations() { ("trace", 500), ("trace_num", 500), ("set_data", 1000), + ("nft_uri", 5000), + ("nft_issuer", 70), + ("nft_taxon", 60), + ("nft_flags", 60), + ("nft_xfer_fee", 60), + ("nft_serial", 60), ] ); } diff --git a/crates/xrpl-wasm-vm-ffi/src/lib.rs b/crates/xrpl-wasm-vm-ffi/src/lib.rs index 3e1a766816..0ed1ad1053 100644 --- a/crates/xrpl-wasm-vm-ffi/src/lib.rs +++ b/crates/xrpl-wasm-vm-ffi/src/lib.rs @@ -395,6 +395,30 @@ mod ffi { #[namespace = "xrpl"] #[cxx_name = "updateData"] fn update_data(self: &HostContext, data: &[u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "getNFT"] + fn get_nft(self: &HostContext, account: &[u8], nft_id: &[u8], out: &mut [u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "getNFTIssuer"] + fn get_nft_issuer(self: &HostContext, nft_id: &[u8], out: &mut [u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "getNFTTaxon"] + fn get_nft_taxon(self: &HostContext, nft_id: &[u8], out: &mut [u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "getNFTFlags"] + fn get_nft_flags(self: &HostContext, nft_id: &[u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "getNFTTransferFee"] + fn get_nft_transfer_fee(self: &HostContext, nft_id: &[u8]) -> i32; + + #[namespace = "xrpl"] + #[cxx_name = "getNFTSequence"] + fn get_nft_sequence(self: &HostContext, nft_id: &[u8], out: &mut [u8]) -> i32; } } @@ -665,6 +689,30 @@ impl HostFunctions for CxxHost<'_> { fn update_data(&self, data: &[u8]) -> HostResult { scalar(self.ctx.update_data(data)) } + + fn get_nft(&self, account: &[u8], nft_id: &[u8], out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.get_nft(account, nft_id, out)) + } + + fn get_nft_issuer(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.get_nft_issuer(nft_id, out)) + } + + fn get_nft_taxon(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.get_nft_taxon(nft_id, out)) + } + + fn get_nft_flags(&self, nft_id: &[u8]) -> HostResult { + scalar(self.ctx.get_nft_flags(nft_id)) + } + + fn get_nft_transfer_fee(&self, nft_id: &[u8]) -> HostResult { + scalar(self.ctx.get_nft_transfer_fee(nft_id)) + } + + fn get_nft_sequence(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + bytes_written(self.ctx.get_nft_sequence(nft_id, out)) + } } fn run_escrow( diff --git a/crates/xrpl-wasm-vm/src/abi.rs b/crates/xrpl-wasm-vm/src/abi.rs index f009278ba5..85a83d3e32 100644 --- a/crates/xrpl-wasm-vm/src/abi.rs +++ b/crates/xrpl-wasm-vm/src/abi.rs @@ -387,6 +387,24 @@ mod tests { fn update_data(&self, _data: &[u8]) -> HostResult { unreachable!("no unit test in this module calls the host") } + fn get_nft(&self, _account: &[u8], _nft_id: &[u8], _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn get_nft_issuer(&self, _nft_id: &[u8], _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn get_nft_taxon(&self, _nft_id: &[u8], _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn get_nft_flags(&self, _nft_id: &[u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn get_nft_transfer_fee(&self, _nft_id: &[u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } + fn get_nft_sequence(&self, _nft_id: &[u8], _out: &mut [u8]) -> HostResult { + unreachable!("no unit test in this module calls the host") + } } fn state(budget: u64) -> VmState<'static> { diff --git a/crates/xrpl-wasm-vm/src/register.rs b/crates/xrpl-wasm-vm/src/register.rs index 1919117b20..e40a47e7bc 100644 --- a/crates/xrpl-wasm-vm/src/register.rs +++ b/crates/xrpl-wasm-vm/src/register.rs @@ -795,6 +795,109 @@ pub(crate) fn register_host_functions( }) }, ), + HostFunctionSpec::GetNft => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + acc_ptr: i32, + acc_len: i32, + nft_ptr: i32, + nft_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetNft, |c| { + let out = Region::new(out_ptr, out_len); + let account = Region::new(acc_ptr, acc_len); + let nft_id = Region::new(nft_ptr, nft_len); + write_buffered(c, out, |host, data, buf| { + host.get_nft(account.read(data)?, nft_id.read(data)?, buf) + }) + }) + }, + ), + HostFunctionSpec::GetNftIssuer => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + nft_ptr: i32, + nft_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetNftIssuer, |c| { + let out = Region::new(out_ptr, out_len); + let nft_id = Region::new(nft_ptr, nft_len); + write_buffered(c, out, |host, data, buf| { + host.get_nft_issuer(nft_id.read(data)?, buf) + }) + }) + }, + ), + HostFunctionSpec::GetNftTaxon => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + nft_ptr: i32, + nft_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetNftTaxon, |c| { + let out = Region::new(out_ptr, out_len); + let nft_id = Region::new(nft_ptr, nft_len); + write_buffered(c, out, |host, data, buf| { + host.get_nft_taxon(nft_id.read(data)?, buf) + }) + }) + }, + ), + HostFunctionSpec::GetNftFlags => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + nft_ptr: i32, + nft_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetNftFlags, |c| { + let host = c.data().host; + let nft_id = read_borrowed(c, Region::new(nft_ptr, nft_len))?; + host.get_nft_flags(nft_id) + }) + }, + ), + HostFunctionSpec::GetNftTransferFee => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + nft_ptr: i32, + nft_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetNftTransferFee, |c| { + let host = c.data().host; + let nft_id = read_borrowed(c, Region::new(nft_ptr, nft_len))?; + host.get_nft_transfer_fee(nft_id) + }) + }, + ), + HostFunctionSpec::GetNftSequence => linker.func_wrap( + HOST_MODULE, + op.wasm_name(), + |mut caller: Caller<'_, VmState<'_>>, + nft_ptr: i32, + nft_len: i32, + out_ptr: i32, + out_len: i32| + -> Result { + charged(&mut caller, HostFunctionSpec::GetNftSequence, |c| { + let out = Region::new(out_ptr, out_len); + let nft_id = Region::new(nft_ptr, nft_len); + write_buffered(c, out, |host, data, buf| { + host.get_nft_sequence(nft_id.read(data)?, buf) + }) + }) + }, + ), }?; } Ok(()) diff --git a/crates/xrpl-wasm-vm/tests/budgets.rs b/crates/xrpl-wasm-vm/tests/budgets.rs index 5faf35a512..78908b904f 100644 --- a/crates/xrpl-wasm-vm/tests/budgets.rs +++ b/crates/xrpl-wasm-vm/tests/budgets.rs @@ -259,6 +259,36 @@ fn call_for(op: HostFunctionSpec) -> Call { "(call $set_data (i32.const 0) (i32.const 8))", 2, ), + HostFunctionSpec::GetNft => ( + import::NFT_URI, + "(call $nft_uri (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 32) (i32.const 52) (i32.const 12))", + 6, + ), + HostFunctionSpec::GetNftIssuer => ( + import::NFT_ISSUER, + "(call $nft_issuer (i32.const 0) (i32.const 32) (i32.const 32) (i32.const 20))", + 4, + ), + HostFunctionSpec::GetNftTaxon => ( + import::NFT_TAXON, + "(call $nft_taxon (i32.const 0) (i32.const 32) (i32.const 32) (i32.const 4))", + 4, + ), + HostFunctionSpec::GetNftFlags => ( + import::NFT_FLAGS, + "(call $nft_flags (i32.const 0) (i32.const 32))", + 2, + ), + HostFunctionSpec::GetNftTransferFee => ( + import::NFT_XFER_FEE, + "(call $nft_xfer_fee (i32.const 0) (i32.const 32))", + 2, + ), + HostFunctionSpec::GetNftSequence => ( + import::NFT_SERIAL, + "(call $nft_serial (i32.const 0) (i32.const 32) (i32.const 32) (i32.const 4))", + 4, + ), }; Call { import, diff --git a/crates/xrpl-wasm-vm/tests/host_calls.rs b/crates/xrpl-wasm-vm/tests/host_calls.rs index 3d9415d87e..a138aea120 100644 --- a/crates/xrpl-wasm-vm/tests/host_calls.rs +++ b/crates/xrpl-wasm-vm/tests/host_calls.rs @@ -767,6 +767,105 @@ fn set_data_passes_the_data_through_and_returns_the_count() { ); } +/// A getter that reads two input regions — an account and an nft id — and writes the +/// answer to a third: both inputs reach the host, keyed together, and the bytes it +/// answers land where the guest asked. +#[test] +fn nft_uri_reads_the_account_and_id_and_writes_the_uri() { + let account = vec![0u8; 20]; + let nft_id = vec![0u8; 32]; + let host = FakeHost::new().answering_get_nft( + account.clone(), + nft_id.clone(), + support::Answer::bytes([0xab, 0xcd, 0xef]), + ); + + let wat = module( + &[import::NFT_URI, ONE_PAGE], + "(call $nft_uri (i32.const 0) (i32.const 20) (i32.const 20) (i32.const 32) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 3, "the uri length"); + assert_eq!(*host.nfts_asked.borrow(), vec![(account, nft_id)]); +} + +/// A single-input byte getter: the nft id reaches the host and the issuer bytes it +/// answers land where the guest asked. +#[test] +fn nft_issuer_reads_the_id_and_writes_the_issuer() { + let nft_id = vec![0u8; 32]; + let host = FakeHost::new().answering_nft_issuer(nft_id.clone(), support::Answer::filler(20)); + + let wat = module( + &[import::NFT_ISSUER, ONE_PAGE], + "(call $nft_issuer (i32.const 0) (i32.const 32) (i32.const 64) (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 20, "the issuer length"); + assert_eq!(*host.nft_issuers_asked.borrow(), vec![nft_id]); +} + +/// A u32-valued getter whose four bytes the host writes to the output region: the id +/// reaches the host, and the little-endian bytes land where the guest asked. +#[test] +fn nft_taxon_reads_the_id_and_writes_four_bytes() { + let nft_id = vec![0u8; 32]; + let host = + FakeHost::new().answering_nft_taxon(nft_id.clone(), support::Answer::bytes([7, 0, 0, 0])); + + let wat = module( + &[import::NFT_TAXON, ONE_PAGE], + "(drop (call $nft_taxon (i32.const 0) (i32.const 32) (i32.const 64) (i32.const 4))) + (i32.load (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 7, "the taxon the host wrote"); + assert_eq!(*host.nft_taxons_asked.borrow(), vec![nft_id]); +} + +/// A single-input scalar getter: the nft id reaches the host and the flags it reports +/// come back as the call's status, no output region involved. +#[test] +fn nft_flags_reads_the_id_and_returns_the_flags() { + let nft_id = vec![0u8; 32]; + let host = FakeHost::new().answering_nft_flags(Ok(11)); + + let wat = module( + &[import::NFT_FLAGS, ONE_PAGE], + "(call $nft_flags (i32.const 0) (i32.const 32))", + ); + assert_eq!(status(&wat, &host), 11, "the flags the host reported"); + assert_eq!(*host.nft_flags_asked.borrow(), vec![nft_id]); +} + +/// A second scalar getter, to pin the pattern: the transfer fee comes back as the +/// status. +#[test] +fn nft_xfer_fee_reads_the_id_and_returns_the_fee() { + let nft_id = vec![0u8; 32]; + let host = FakeHost::new().answering_nft_transfer_fee(Ok(314)); + + let wat = module( + &[import::NFT_XFER_FEE, ONE_PAGE], + "(call $nft_xfer_fee (i32.const 0) (i32.const 32))", + ); + assert_eq!(status(&wat, &host), 314, "the fee the host reported"); + assert_eq!(*host.nft_fee_asked.borrow(), vec![nft_id]); +} + +/// The last NFT getter, a u32 sequence written to the output region. +#[test] +fn nft_serial_reads_the_id_and_writes_four_bytes() { + let nft_id = vec![0u8; 32]; + let host = FakeHost::new() + .answering_nft_sequence(nft_id.clone(), support::Answer::bytes([42, 0, 0, 0])); + + let wat = module( + &[import::NFT_SERIAL, ONE_PAGE], + "(drop (call $nft_serial (i32.const 0) (i32.const 32) (i32.const 64) (i32.const 4))) + (i32.load (i32.const 64))", + ); + assert_eq!(status(&wat, &host), 42, "the sequence the host wrote"); + assert_eq!(*host.nft_sequences_asked.borrow(), vec![nft_id]); +} + /// A leading scalar parameter reaches the host as declared. #[test] fn home_le_field_passes_the_field_selector_through() { diff --git a/crates/xrpl-wasm-vm/tests/preflight.rs b/crates/xrpl-wasm-vm/tests/preflight.rs index 056f8066a6..a3932e434d 100644 --- a/crates/xrpl-wasm-vm/tests/preflight.rs +++ b/crates/xrpl-wasm-vm/tests/preflight.rs @@ -98,7 +98,7 @@ fn a_disabled_feature_does_not_pass() { /// Every host function the ABI declares, spelled as a guest imports it. The count /// is asserted against the ABI so a function added to it cannot be left out here. -const ALL_IMPORTS: [&str; 42] = [ +const ALL_IMPORTS: [&str; 48] = [ import::LDGR_INDEX, import::PARENT_LDGR_TIME, import::PARENT_LDGR_HASH, @@ -141,6 +141,12 @@ const ALL_IMPORTS: [&str; 42] = [ import::TRACE, import::TRACE_NUM, import::SET_DATA, + import::NFT_URI, + import::NFT_ISSUER, + import::NFT_TAXON, + import::NFT_FLAGS, + import::NFT_XFER_FEE, + import::NFT_SERIAL, ]; #[test] diff --git a/crates/xrpl-wasm-vm/tests/support/mod.rs b/crates/xrpl-wasm-vm/tests/support/mod.rs index 09f81e2b35..73de5a4d61 100644 --- a/crates/xrpl-wasm-vm/tests/support/mod.rs +++ b/crates/xrpl-wasm-vm/tests/support/mod.rs @@ -286,6 +286,32 @@ pub struct FakeHost { pub update_data_answer: HostResult, /// Every data blob `update_data` was given. pub update_data_asked: RefCell>>, + /// What `get_nft` answers, by (account, nft id) bytes. An unlisted key answers + /// `InvalidParams`. + pub nfts: HashMap<(Vec, Vec), Answer>, + /// Every (account, nft id) `get_nft` was asked for. + pub nfts_asked: RefCell, Vec)>>, + /// What `get_nft_issuer` answers, by nft id. An unlisted id answers `InvalidParams`. + pub nft_issuers: HashMap, Answer>, + /// Every nft id `get_nft_issuer` was asked for. + pub nft_issuers_asked: RefCell>>, + /// What `get_nft_taxon` answers, by nft id. An unlisted id answers `InvalidParams`. + pub nft_taxons: HashMap, Answer>, + /// Every nft id `get_nft_taxon` was asked for. + pub nft_taxons_asked: RefCell>>, + /// What `get_nft_flags` answers, whatever nft id it is given. + pub nft_flags_answer: HostResult, + /// Every nft id `get_nft_flags` was asked for. + pub nft_flags_asked: RefCell>>, + /// What `get_nft_transfer_fee` answers, whatever nft id it is given. + pub nft_fee_answer: HostResult, + /// Every nft id `get_nft_transfer_fee` was asked for. + pub nft_fee_asked: RefCell>>, + /// What `get_nft_sequence` answers, by nft id. An unlisted id answers + /// `InvalidParams`. + pub nft_sequences: HashMap, Answer>, + /// Every nft id `get_nft_sequence` was asked for. + pub nft_sequences_asked: RefCell>>, } impl Default for FakeHost { @@ -376,6 +402,18 @@ impl Default for FakeHost { traces: RefCell::new(Vec::new()), update_data_answer: Ok(0), update_data_asked: RefCell::new(Vec::new()), + nfts: HashMap::new(), + nfts_asked: RefCell::new(Vec::new()), + nft_issuers: HashMap::new(), + nft_issuers_asked: RefCell::new(Vec::new()), + nft_taxons: HashMap::new(), + nft_taxons_asked: RefCell::new(Vec::new()), + nft_flags_answer: Ok(0), + nft_flags_asked: RefCell::new(Vec::new()), + nft_fee_answer: Ok(0), + nft_fee_asked: RefCell::new(Vec::new()), + nft_sequences: HashMap::new(), + nft_sequences_asked: RefCell::new(Vec::new()), } } } @@ -682,6 +720,41 @@ impl FakeHost { self } + pub fn answering_get_nft( + mut self, + account: Vec, + nft_id: Vec, + answer: Answer, + ) -> FakeHost { + self.nfts.insert((account, nft_id), answer); + self + } + + pub fn answering_nft_issuer(mut self, nft_id: Vec, answer: Answer) -> FakeHost { + self.nft_issuers.insert(nft_id, answer); + self + } + + pub fn answering_nft_taxon(mut self, nft_id: Vec, answer: Answer) -> FakeHost { + self.nft_taxons.insert(nft_id, answer); + self + } + + pub fn answering_nft_flags(mut self, answer: HostResult) -> FakeHost { + self.nft_flags_answer = answer; + self + } + + pub fn answering_nft_transfer_fee(mut self, answer: HostResult) -> FakeHost { + self.nft_fee_answer = answer; + self + } + + pub fn answering_nft_sequence(mut self, nft_id: Vec, answer: Answer) -> FakeHost { + self.nft_sequences.insert(nft_id, answer); + self + } + pub fn traces(&self) -> Vec { self.traces.borrow().clone() } @@ -1085,6 +1158,49 @@ impl HostFunctions for FakeHost { self.update_data_asked.borrow_mut().push(data.to_vec()); self.update_data_answer } + + fn get_nft(&self, account: &[u8], nft_id: &[u8], out: &mut [u8]) -> HostResult { + let key = (account.to_vec(), nft_id.to_vec()); + self.nfts_asked.borrow_mut().push(key.clone()); + match self.nfts.get(&key) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidParams), + } + } + + fn get_nft_issuer(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + self.nft_issuers_asked.borrow_mut().push(nft_id.to_vec()); + match self.nft_issuers.get(nft_id) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidParams), + } + } + + fn get_nft_taxon(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + self.nft_taxons_asked.borrow_mut().push(nft_id.to_vec()); + match self.nft_taxons.get(nft_id) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidParams), + } + } + + fn get_nft_flags(&self, nft_id: &[u8]) -> HostResult { + self.nft_flags_asked.borrow_mut().push(nft_id.to_vec()); + self.nft_flags_answer + } + + fn get_nft_transfer_fee(&self, nft_id: &[u8]) -> HostResult { + self.nft_fee_asked.borrow_mut().push(nft_id.to_vec()); + self.nft_fee_answer + } + + fn get_nft_sequence(&self, nft_id: &[u8], out: &mut [u8]) -> HostResult { + self.nft_sequences_asked.borrow_mut().push(nft_id.to_vec()); + match self.nft_sequences.get(nft_id) { + Some(answer) => answer.fill(out), + None => Err(HostError::InvalidParams), + } + } } // --------------------------------------------------------------------------- @@ -1150,6 +1266,15 @@ pub mod import { r#"(import "host_lib" "trace_num" (func $trace_num (param i32 i32 i64) (result i32)))"#; pub const SET_DATA: &str = r#"(import "host_lib" "set_data" (func $set_data (param i32 i32) (result i32)))"#; + pub const NFT_URI: &str = r#"(import "host_lib" "nft_uri" (func $nft_uri (param i32 i32 i32 i32 i32 i32) (result i32)))"#; + pub const NFT_ISSUER: &str = r#"(import "host_lib" "nft_issuer" (func $nft_issuer (param i32 i32 i32 i32) (result i32)))"#; + pub const NFT_TAXON: &str = + r#"(import "host_lib" "nft_taxon" (func $nft_taxon (param i32 i32 i32 i32) (result i32)))"#; + pub const NFT_FLAGS: &str = + r#"(import "host_lib" "nft_flags" (func $nft_flags (param i32 i32) (result i32)))"#; + pub const NFT_XFER_FEE: &str = + r#"(import "host_lib" "nft_xfer_fee" (func $nft_xfer_fee (param i32 i32) (result i32)))"#; + pub const NFT_SERIAL: &str = r#"(import "host_lib" "nft_serial" (func $nft_serial (param i32 i32 i32 i32) (result i32)))"#; } /// One page of linear memory, exported under the name the engine looks for. diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h index 14e7838bd1..fa44678ee0 100644 --- a/include/xrpl/tx/wasm/HostContext.h +++ b/include/xrpl/tx/wasm/HostContext.h @@ -278,6 +278,42 @@ public: // stored, or a negative `HostFunctionError` code. [[nodiscard]] std::int32_t updateData(rust::Slice data) const noexcept; + + // The account id must be 20 bytes and the nft id 32 bytes, else `InvalidParams`. + // Writes the token's URI bytes. + [[nodiscard]] std::int32_t + getNFT( + rust::Slice account, + rust::Slice nftId, + rust::Slice out) const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Writes the 20-byte issuer + // account encoded in the id. + [[nodiscard]] std::int32_t + getNFTIssuer(rust::Slice nftId, rust::Slice out) + const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Writes the taxon as its four + // little-endian bytes. + [[nodiscard]] std::int32_t + getNFTTaxon(rust::Slice nftId, rust::Slice out) + const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Returns the flags, or a + // negative `HostFunctionError` code. + [[nodiscard]] std::int32_t + getNFTFlags(rust::Slice nftId) const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Returns the transfer fee, or a + // negative `HostFunctionError` code. + [[nodiscard]] std::int32_t + getNFTTransferFee(rust::Slice nftId) const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Writes the sequence number as + // its four little-endian bytes. + [[nodiscard]] std::int32_t + getNFTSequence(rust::Slice nftId, rust::Slice out) + const noexcept; }; } // namespace xrpl diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp index 2aea22d671..3af6aa1071 100644 --- a/src/libxrpl/tx/wasm/HostContext.cpp +++ b/src/libxrpl/tx/wasm/HostContext.cpp @@ -874,4 +874,101 @@ HostContext::updateData(rust::Slice data) const noexcept }); } +std::int32_t +HostContext::getNFT( + rust::Slice account, + rust::Slice nftId, + rust::Slice out) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (account.size() != AccountID::size() || nftId.size() != uint256::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.getNFT( + AccountID::fromVoid(account.data()), uint256::fromVoid(nftId.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + +std::int32_t +HostContext::getNFTIssuer(rust::Slice nftId, rust::Slice out) + const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (nftId.size() != uint256::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.getNFTIssuer(uint256::fromVoid(nftId.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answer(out, value->data(), value->size()); + }); +} + +std::int32_t +HostContext::getNFTTaxon(rust::Slice nftId, rust::Slice out) + const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (nftId.size() != uint256::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.getNFTTaxon(uint256::fromVoid(nftId.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answerScalar(out, *value); + }); +} + +std::int32_t +HostContext::getNFTFlags(rust::Slice nftId) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (nftId.size() != uint256::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.getNFTFlags(uint256::fromVoid(nftId.data())); + if (!value) + return hfErrorToInt(value.error()); + + return *value; + }); +} + +std::int32_t +HostContext::getNFTTransferFee(rust::Slice nftId) const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (nftId.size() != uint256::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.getNFTTransferFee(uint256::fromVoid(nftId.data())); + if (!value) + return hfErrorToInt(value.error()); + + return *value; + }); +} + +std::int32_t +HostContext::getNFTSequence(rust::Slice nftId, rust::Slice out) + const noexcept +{ + return guarded(hostFunctions_.getJournal(), kHostInternal, [&] { + if (nftId.size() != uint256::size()) + return hfErrorToInt(HostFunctionError::InvalidParams); + + auto const value = hostFunctions_.getNFTSequence(uint256::fromVoid(nftId.data())); + if (!value) + return hfErrorToInt(value.error()); + + return answerScalar(out, *value); + }); +} + } // namespace xrpl