Skip to content

add nft_reward project #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions projects/nft_reward/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "nft_reward"
version = "0.1.0"
edition = "2021"

# This empty workspace definition keeps this project independent
# from the parent workspace
[workspace]

[lib]
crate-type = ["cdylib"]

[profile.release]
lto = true
opt-level = 's'

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
xrpl-std = { path = "../../xrpl-std" }
hex = "0.4.3"
53 changes: 53 additions & 0 deletions projects/nft_reward/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# NFT Reward Escrow

This project implements a Smart Escrow FinishFunction that only returns `true` when the escrow destination owns a specific NFT (NFToken object).

## Overview

The escrow can only be finished if the destination account owns a particular NFT specified by its NFTokenID. This creates a mechanism where the escrow funds can only be claimed when an account has ownership of a specific NFT.

## Implementation Details

The `ready()` function:
1. Gets the escrow destination account
2. For the specified NFTokenID:
- Extracts its low 96 bits
- Constructs the NFTokenPage ID by concatenating the destination account and the low 96 bits
- Checks up to 3 possible pages where the NFT could be located
- For each page:
- Retrieves the NFTokenPage entry from the ledger
- Parses the page to verify if it contains the specific NFT
3. Returns `true` only if the NFT is found in one of the destination's NFTokenPages

## NFTokenPage ID Format

NFTokenPage identifiers are constructed to allow a more efficient paging structure, ideally suited for NFToken objects.

The identifier of an NFTokenPage is derived by concatenating the 160-bit AccountID of the owner of the page, followed by a 96 bit value that indicates whether a particular NFTokenID can be contained in this page.

More specifically, a NFToken with the NFTokenID value A can be included in a page with NFTokenPage ID B if and only if low96(A) >= low96(B).

This uses a function low96(x) which returns the low 96 bits of a 256-bit value. For example, applying the low96 function to the NFTokenID of 000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65 returns the value 42540EE208C3098E00000D65.

https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/nftokenpage

## Finding NFTokens

To find a specific NFToken:
1. Know its NFTokenID and current owner
2. Compute the NFTokenPage ID as described above
3. Search for a ledger entry whose identifier is less than or equal to that value
4. If that entry does not exist or is not an NFTokenPage, that account does not own that NFToken

## Configuration

The NFTokenID that must be owned by the destination is specified in the `REQUIRED_NFT_ID` constant in `src/lib.rs`. You should replace this with the actual NFTokenID you want to use.

## Building

To build the project:
```bash
cargo build --release
```

The compiled WebAssembly module will be available in `target/wasm32-unknown-unknown/release/nft_reward.wasm`
134 changes: 134 additions & 0 deletions projects/nft_reward/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use std::str;
use xrpl_std::get_current_escrow_destination;
use xrpl_std::host_lib;
use serde_json::Value;
use hex;

// The NFTokenID that the destination must own to finish the escrow
// Example NFTokenID from the xrpl.org documentation
// TODO: Read this from the `Data` field of the EscrowCreate transaction instead.
const REQUIRED_NFT_ID: &str = "000B013A95F14B0044F78A264E41713C64B5F89242540EE208C3098E00000D65";

// Helper function to read data from a pointer and length pair
// This is comparable to the `read_data` function from `xrpl_std` but that function is private.
unsafe fn read_data(ptr: i32) -> Vec<u8> {
let int_buf = Vec::from_raw_parts(ptr as *mut u8, 8, 8);
let mut ptr_array: [u8; 4] = [0; 4];
let mut len_array: [u8; 4] = [0; 4];
ptr_array.clone_from_slice(&int_buf[0..4]);
len_array.clone_from_slice(&int_buf[4..8]);
let ptr = i32::from_le_bytes(ptr_array);
let len = i32::from_le_bytes(len_array);
Vec::from_raw_parts(ptr as *mut u8, len as usize, len as usize)
}

// Helper function to extract low 96 bits from a hex string
fn low96(nftoken_id: &str) -> &str {
// The low 96 bits are the last 24 characters of the hex string
&nftoken_id[nftoken_id.len() - 24..]
}

// Helper function to decrement a hex string by 1
fn decrement_hex(hex: &str) -> String {
let mut bytes = hex::decode(hex).unwrap_or_default();
let mut carry = 1;

for byte in bytes.iter_mut().rev() {
if *byte == 0 && carry == 1 {
*byte = 0xFF;
} else {
*byte = byte.wrapping_sub(carry);
carry = 0;
}
}

hex::encode(bytes)
}

// Helper function to get NFTokenPage entry
unsafe fn get_nftoken_page(key: &[u8]) -> Vec<u8> {
let key_ptr = key.as_ptr();
let key_len = key.len();
let mut fname = String::from("NFTokens");
let fname_ptr = fname.as_mut_ptr();
let fname_len = fname.len();
let r_ptr = host_lib::getLedgerEntryField(0x0050, key_ptr as i32, key_len as i32, fname_ptr as i32, fname_len as i32);
read_data(r_ptr)
}

// Helper function to check if an NFT is in a page
fn is_nft_in_page(page_entry: &[u8], nftoken_id: &str) -> bool {
// Parse the page entry as JSON
let page_str = match str::from_utf8(page_entry) {
Ok(s) => s,
Err(_) => return false
};

let page_json: Value = match serde_json::from_str(page_str) {
Ok(v) => v,
Err(_) => return false
};

// Get the NFTokens array from the page
let nftokens = match page_json.get("NFTokens") {
Some(Value::Array(arr)) => arr,
_ => return false
};

// Check each NFToken in the page
for nft in nftokens {
if let Some(Value::Object(nft_obj)) = nft.get("NFToken") {
if let Some(Value::String(nft_id)) = nft_obj.get("NFTokenID") {
if nft_id == nftoken_id {
return true;
}
}
}
}

false
}

#[no_mangle]
pub fn ready() -> bool {
unsafe {
// Get the escrow destination account
let destination = get_current_escrow_destination();
let destination_str = match str::from_utf8(&destination) {
Ok(s) => s,
Err(_) => return false
};

// Get the low 96 bits of the required NFTokenID
let mut current_low96 = low96(REQUIRED_NFT_ID).to_string();

// Check the 3 pages where the NFT could be located
for _ in 0..3 {
// Construct the NFTokenPage ID by concatenating destination account and current low96
let mut page_id = String::with_capacity(destination_str.len() + current_low96.len());
page_id.push_str(destination_str);
page_id.push_str(&current_low96);

// Get the NFTokenPage entry
let page_entry = get_nftoken_page(page_id.as_bytes());

// If we found a valid page, check if it contains our NFT
if !page_entry.is_empty() {
if is_nft_in_page(&page_entry, REQUIRED_NFT_ID) {
return true;
}
}

// Decrement the low96 value to check the previous page
current_low96 = decrement_hex(&current_low96);

// If we've reached all zeros, we've checked all possible pages
if current_low96 == "000000000000000000000000" {
break;
}
}

// If we didn't find the NFT in any of the pages, return false
false
}
}