diff --git a/projects/Cargo.lock b/projects/Cargo.lock index 637e7c73..139ba4a2 100644 --- a/projects/Cargo.lock +++ b/projects/Cargo.lock @@ -100,6 +100,14 @@ dependencies = [ "xrpl-wasm-std", ] +[[package]] +name = "keylets" +version = "0.1.0" +dependencies = [ + "xrpl-address-macro", + "xrpl-wasm-std", +] + [[package]] name = "kyc" version = "0.0.1" diff --git a/projects/Cargo.toml b/projects/Cargo.toml index 5c973e33..785f9523 100644 --- a/projects/Cargo.toml +++ b/projects/Cargo.toml @@ -15,6 +15,7 @@ members = [ "examples/smart-escrows/notary", "examples/smart-escrows/notary_macro_example", "examples/smart-escrows/oracle", + "examples/smart-escrows/keylets", ] [profile.release] diff --git a/projects/examples/smart-escrows/keylets/Cargo.toml b/projects/examples/smart-escrows/keylets/Cargo.toml new file mode 100644 index 00000000..5436d7f1 --- /dev/null +++ b/projects/examples/smart-escrows/keylets/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "keylets" +version = "0.1.0" +edition = "2024" +description = "Example: compute keylets, cache ledger objects, and sanity-check fields" +license = "ISC" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +xrpl-wasm-std = { path = "../../../../xrpl-wasm-std" } +xrpl-address-macro = { path = "../../../../xrpl-address-macro" } + diff --git a/projects/examples/smart-escrows/keylets/README.md b/projects/examples/smart-escrows/keylets/README.md new file mode 100644 index 00000000..33694674 --- /dev/null +++ b/projects/examples/smart-escrows/keylets/README.md @@ -0,0 +1,13 @@ +Keylets example + +This example demonstrates: + +- General ledger calls: get_ledger_sqn +- Any-object read path: cache_ledger_obj, get_ledger_obj_field, get_ledger_obj_nested_field, get_ledger_obj_array_len, get_ledger_obj_nested_array_len +- Keylets: account_keylet, signers_keylet, escrow_keylet, check_keylet, ticket_keylet, offer_keylet, nft_offer_keylet, paychan_keylet, permissioned_domain_keylet, vault_keylet, line_keylet, mpt_issuance_keylet, mptoken_keylet + +Run: + +- craft build keylets +- craft test keylets --case success + diff --git a/projects/examples/smart-escrows/keylets/fixtures/success/ledger.json b/projects/examples/smart-escrows/keylets/fixtures/success/ledger.json new file mode 100644 index 00000000..af9cd8b9 --- /dev/null +++ b/projects/examples/smart-escrows/keylets/fixtures/success/ledger.json @@ -0,0 +1,11 @@ +[ + { + "2B6AC232AA4C4BE41BF49D2459FA4A0347E1B543A4C92FCEE0821C0201E2E9A8": { + "LedgerEntryType": "SignerList", + "OwnerCount": 0, + "Signers": [], + "index": "2B6AC232AA4C4BE41BF49D2459FA4A0347E1B543A4C92FCEE0821C0201E2E9A8" + } + } +] + diff --git a/projects/examples/smart-escrows/keylets/fixtures/success/ledger_header.json b/projects/examples/smart-escrows/keylets/fixtures/success/ledger_header.json new file mode 100644 index 00000000..5545dbd3 --- /dev/null +++ b/projects/examples/smart-escrows/keylets/fixtures/success/ledger_header.json @@ -0,0 +1,6 @@ +{ + "ledger_index": 95354542, + "parent_close_time": 797572860, + "parent_hash": "E367C455467EF560515AB024C736359C50D52194BD4C6CA037F3A988984357F3" +} + diff --git a/projects/examples/smart-escrows/keylets/fixtures/success/ledger_object.json b/projects/examples/smart-escrows/keylets/fixtures/success/ledger_object.json new file mode 100644 index 00000000..9e13ff50 --- /dev/null +++ b/projects/examples/smart-escrows/keylets/fixtures/success/ledger_object.json @@ -0,0 +1,7 @@ +{ + "LedgerEntryType": "AccountRoot", + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Balance": "1000000", + "Sequence": 1 +} + diff --git a/projects/examples/smart-escrows/keylets/fixtures/success/nfts.json b/projects/examples/smart-escrows/keylets/fixtures/success/nfts.json new file mode 100644 index 00000000..7dd43875 --- /dev/null +++ b/projects/examples/smart-escrows/keylets/fixtures/success/nfts.json @@ -0,0 +1,2 @@ +[] + diff --git a/projects/examples/smart-escrows/keylets/fixtures/success/tx.json b/projects/examples/smart-escrows/keylets/fixtures/success/tx.json new file mode 100644 index 00000000..d30ff0a4 --- /dev/null +++ b/projects/examples/smart-escrows/keylets/fixtures/success/tx.json @@ -0,0 +1,9 @@ +{ + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "TransactionType": "EscrowFinish", + "Fee": 12, + "Sequence": 1000, + "Memos": [ { "Memo": { "MemoData": "00" } } ], + "Signers": [] +} + diff --git a/projects/examples/smart-escrows/keylets/src/lib.rs b/projects/examples/smart-escrows/keylets/src/lib.rs new file mode 100644 index 00000000..6cadf4b3 --- /dev/null +++ b/projects/examples/smart-escrows/keylets/src/lib.rs @@ -0,0 +1,123 @@ +#![cfg_attr(target_arch = "wasm32", no_std)] + +#[cfg(not(target_arch = "wasm32"))] +extern crate std; + +use xrpl_wasm_std::core::locator::Locator; +use xrpl_wasm_std::decode_hex_32; +use xrpl_wasm_std::host::trace::{trace, trace_data, trace_num, DataRepr::AsHex}; +use xrpl_wasm_std::host::{ + account_keylet, amm_keylet, cache_ledger_obj, check_keylet, escrow_keylet, get_ledger_obj_array_len, + get_ledger_obj_field, get_ledger_obj_nested_array_len, get_ledger_obj_nested_field, get_ledger_sqn, + line_keylet, mpt_issuance_keylet, mptoken_keylet, nft_offer_keylet, offer_keylet, paychan_keylet, + permissioned_domain_keylet, signers_keylet, ticket_keylet, vault_keylet, +}; +use xrpl_wasm_std::sfield; +use xrpl_address_macro::r_address; + +const ACCOUNT: [u8; 20] = r_address!("rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"); + +fn cache_and_check(slot: i32, field: i32, label: &str) -> i32 { + let mut buf = [0u8; 64]; + let wrote = unsafe { get_ledger_obj_field(slot, field, buf.as_mut_ptr(), buf.len()) }; + if wrote <= 0 { + let _ = trace_num("get_ledger_obj_field failed", wrote as i64); + return wrote; + } + let _ = trace_data(label, &buf[..(wrote as usize).min(buf.len())], AsHex); + 0 +} + +fn compute_and_cache_keylet(label: &str, keylet_out: &mut [u8; 32]) -> i32 { + let wrote = unsafe { account_keylet(ACCOUNT.as_ptr(), ACCOUNT.len(), keylet_out.as_mut_ptr(), keylet_out.len()) }; + if wrote <= 0 { return wrote; } + let _ = trace_data(label, keylet_out, AsHex); + unsafe { cache_ledger_obj(keylet_out.as_ptr(), keylet_out.len(), 0) } +} + +#[unsafe(no_mangle)] +pub extern "C" fn finish() -> i32 { + let _ = trace("=== Keylets example ==="); + + // General ledger info + let ls = unsafe { get_ledger_sqn() }; + if ls <= 0 { return ls; } + let _ = trace_num("ledger_sqn", ls as i64); + + // AccountRoot keylet -> cache -> read a few fields + let mut kl = [0u8; 32]; + let slot = compute_and_cache_keylet("account_keylet", &mut kl); + if slot <= 0 { return slot; } + + // Sanity check some fields on AccountRoot + if cache_and_check(slot, sfield::Account, "Account") < 0 { return -1; } + if cache_and_check(slot, sfield::Balance, "Balance") < 0 { return -1; } + + // Nested field example: no nesting on AccountRoot; demonstrate locator against array using fake path + // Instead, show array len for Signers on SignerList (will likely be 0 in default fixtures) + // Derive SignerList keylet and test array length + nested field access patterns + let wrote = unsafe { signers_keylet(ACCOUNT.as_ptr(), ACCOUNT.len(), kl.as_mut_ptr(), kl.len()) }; + if wrote <= 0 { return wrote; } + let _ = trace_data("signers_keylet", &kl, AsHex); + let signer_slot = unsafe { cache_ledger_obj(kl.as_ptr(), kl.len(), 0) }; + if signer_slot > 0 { + let count = unsafe { get_ledger_obj_array_len(signer_slot, sfield::Signers) }; + let _ = trace_num("Signer count", count as i64); + if count > 0 { + let mut loc = Locator::new(); + loc.pack(sfield::Signers); + loc.pack(0); + loc.pack(sfield::Account); + let mut out = [0u8; 32]; + let wrote = unsafe { + get_ledger_obj_nested_field( + signer_slot, + loc.get_addr(), + loc.num_packed_bytes(), + out.as_mut_ptr(), + out.len(), + ) + }; + if wrote > 0 { + let _ = trace_data("First signer Account", &out[..(wrote as usize)], AsHex); + } + } + } + + // Showcase additional keylets (trace only): + let mut out = [0u8; 32]; + let wrote = unsafe { escrow_keylet(ACCOUNT.as_ptr(), ACCOUNT.len(), 1000, out.as_mut_ptr(), out.len()) }; + if wrote > 0 { let _ = trace_data("escrow_keylet", &out, AsHex); } + let wrote = unsafe { check_keylet(ACCOUNT.as_ptr(), ACCOUNT.len(), 1001, out.as_mut_ptr(), out.len()) }; + if wrote > 0 { let _ = trace_data("check_keylet", &out, AsHex); } + let wrote = unsafe { ticket_keylet(ACCOUNT.as_ptr(), ACCOUNT.len(), 1002, out.as_mut_ptr(), out.len()) }; + if wrote > 0 { let _ = trace_data("ticket_keylet", &out, AsHex); } + let wrote = unsafe { offer_keylet(ACCOUNT.as_ptr(), ACCOUNT.len(), 1003, out.as_mut_ptr(), out.len()) }; + if wrote > 0 { let _ = trace_data("offer_keylet", &out, AsHex); } + let wrote = unsafe { nft_offer_keylet(ACCOUNT.as_ptr(), ACCOUNT.len(), 1004, out.as_mut_ptr(), out.len()) }; + if wrote > 0 { let _ = trace_data("nft_offer_keylet", &out, AsHex); } + let wrote = unsafe { paychan_keylet(ACCOUNT.as_ptr(), ACCOUNT.len(), r_address!("rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW").as_ptr(), 20, 1005, out.as_mut_ptr(), out.len()) }; + if wrote > 0 { let _ = trace_data("paychan_keylet", &out, AsHex); } + let wrote = unsafe { permissioned_domain_keylet(ACCOUNT.as_ptr(), ACCOUNT.len(), 1, out.as_mut_ptr(), out.len()) }; + if wrote > 0 { let _ = trace_data("permissioned_domain_keylet", &out, AsHex); } + let wrote = unsafe { vault_keylet(ACCOUNT.as_ptr(), ACCOUNT.len(), 2, out.as_mut_ptr(), out.len()) }; + if wrote > 0 { let _ = trace_data("vault_keylet", &out, AsHex); } + + // AMM and trust line-like keylets require issues and currency data; skip detailed cache check + let issuer = ACCOUNT; + let currency = [0u8; 20]; // placeholder 20 bytes; simulator expects 20-byte currency + let wrote = unsafe { line_keylet(ACCOUNT.as_ptr(), ACCOUNT.len(), issuer.as_ptr(), issuer.len(), currency.as_ptr(), currency.len(), out.as_mut_ptr(), out.len()) }; + if wrote > 0 { let _ = trace_data("line_keylet", &out, AsHex); } + + // mpt issuance and token keylets – provide simple params + let wrote = unsafe { mpt_issuance_keylet(issuer.as_ptr(), issuer.len(), 10, out.as_mut_ptr(), out.len()) }; + if wrote > 0 { + let _ = trace_data("mpt_issuance_keylet", &out, AsHex); + let mut holder = ACCOUNT; + let wrote2 = unsafe { mptoken_keylet(out.as_ptr(), out.len(), holder.as_mut_ptr(), holder.len(), out.as_mut_ptr(), out.len()) }; + if wrote2 > 0 { let _ = trace_data("mptoken_keylet", &out, AsHex); } + } + + 1 +} +