Skip to content

Commit 1d46607

Browse files
authored
feat: hook_hash (#32)
1 parent 768c70e commit 1d46607

File tree

5 files changed

+152
-2
lines changed

5 files changed

+152
-2
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,11 @@ Utilities
190190
Hook context
191191

192192
- [x] `hook_account`
193-
- [ ] `hook_hash`
193+
- [x] `hook_hash`
194194
- [x] `hook_param`
195195
- [ ] `hook_param_set`
196196
- [ ] `hook_skip`
197-
- [ ] `hook_pos`
197+
- [x] `hook_pos`
198198
- [ ] `hook_again`
199199

200200
Serialization

hooks-rs/examples/hook_hash.rs

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#![no_std]
2+
#![no_main]
3+
4+
use hooks_rs::*;
5+
6+
#[no_mangle]
7+
pub extern "C" fn cbak(_: u32) -> i64 {
8+
0
9+
}
10+
11+
#[no_mangle]
12+
pub extern "C" fn hook(_: u32) -> i64 {
13+
max_iter(1);
14+
15+
let hook_hash = hook_hash(HookNumber::CurrentHook).unwrap_line_number();
16+
17+
accept(&hook_hash, 0);
18+
}

hooks-rs/src/api/hook.rs

+42
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ use core::mem::MaybeUninit;
33
use super::*;
44
use crate::c;
55

6+
/// Meant to be used as an argument to `hook_hash` to specify the hook number.
7+
#[derive(Copy, Clone)]
8+
pub enum HookNumber {
9+
/// The currently executing hook
10+
CurrentHook,
11+
/// The position in the hook chain the hook is located at,
12+
Custom(i32),
13+
}
14+
615
/// Retreive the 20 byte Account ID the Hook is executing on
716
///
817
/// # Example
@@ -70,3 +79,36 @@ pub fn hook_param<const HOOK_PARAM_LEN: usize>(
7079
pub fn hook_pos() -> i64 {
7180
unsafe { c::hook_pos() }
7281
}
82+
83+
/// Retreive the 32 byte namespace biased SHA512H of the currently executing Hook
84+
///
85+
/// # Example
86+
/// ```
87+
/// let hook_hash = hook_hash(HookNumber::CurrentHook);
88+
/// ```
89+
#[inline(always)]
90+
pub fn hook_hash(hook_number: HookNumber) -> Result<[u8; HOOK_HASH_LEN]> {
91+
let func = |buffer_mut_ptr: *mut MaybeUninit<u8>| {
92+
let result: Result<u64> = unsafe {
93+
c::hook_hash(
94+
buffer_mut_ptr as u32,
95+
HOOK_HASH_LEN as u32,
96+
hook_number.into(),
97+
)
98+
.into()
99+
};
100+
101+
result
102+
};
103+
104+
init_buffer_mut(func)
105+
}
106+
107+
impl From<HookNumber> for i32 {
108+
fn from(hook_no: HookNumber) -> i32 {
109+
match hook_no {
110+
HookNumber::CurrentHook => -1,
111+
HookNumber::Custom(hook_no) => hook_no,
112+
}
113+
}
114+
}

hooks-rs/src/api/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ pub const STATE_KEY_LEN: usize = 32;
4343
pub const NONCE_LEN: usize = 32;
4444
/// Hash byte length
4545
pub const HASH_LEN: usize = 32;
46+
/// Hook Hash byte length
47+
pub const HOOK_HASH_LEN: usize = 32;
4648
/// Amount byte length
4749
pub const AMOUNT_LEN: usize = 48;
4850
/// Payment simple transaction byte length

hooks-rs/tests/hook_hash.test.ts

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// xrpl
2+
import { Client, Invoke, Transaction, Wallet } from "@transia/xrpl";
3+
import { TestUtils } from "./setup";
4+
import { HookExecution } from "@transia/xrpl/dist/npm/models/transactions/metadata";
5+
6+
const HOOK_NAME = "hook_hash";
7+
8+
describe("hook_hash.rs", () => {
9+
let client: Client;
10+
let alice: Wallet;
11+
let bob: Wallet;
12+
13+
beforeAll(async () => {
14+
const hook = await TestUtils.buildHook(HOOK_NAME);
15+
client = new Client("wss://xahau-test.net", {});
16+
await client.connect();
17+
client.networkID = await client.getNetworkID();
18+
// Because Faucet only allows one account to be created every 60 seconds,
19+
// we will use the following accounts for testing. Change the secrets when
20+
// running out of funds.
21+
// rMXRvoMFnuf2xPK5Pio94D5LhttE7X5H6e
22+
alice = Wallet.fromSecret(`ssJaynfvJVm7wANHyCFAWYV94SXWP`);
23+
// rniKiTrsgGkDFc65pw7zruXJirs4aPweN6
24+
bob = Wallet.fromSecret(`ssz1zWkMuEQN9twEvTTnGZW8KtZjw`);
25+
await TestUtils.setHook(client, alice.seed!, hook);
26+
}, 3 * 60_000);
27+
28+
afterAll(async () => {
29+
await client.disconnect();
30+
}, 10_000);
31+
32+
it(
33+
"accepts with the hook hash of the currently executing hook",
34+
async () => {
35+
const tx: Invoke & Transaction = {
36+
TransactionType: "Invoke",
37+
Account: bob.classicAddress,
38+
Destination: alice.classicAddress,
39+
};
40+
// Autofilling fee does not work with hooks yet
41+
const { Fee, ...rest } = await client.autofill(tx);
42+
const fee = await TestUtils.getTransactionFee(client, rest);
43+
const txResponse = await TestUtils.submitAndWaitWithRetries(
44+
client,
45+
{
46+
...tx,
47+
Fee: fee,
48+
},
49+
{
50+
wallet: bob,
51+
autofill: true,
52+
},
53+
);
54+
if (!txResponse.result.meta) {
55+
throw new Error("No meta in tx response");
56+
}
57+
if (typeof txResponse.result.meta === "string") {
58+
throw new Error("Meta is string, not object");
59+
}
60+
61+
const { meta } = txResponse.result;
62+
if (!(meta.HookExecutions && meta.HookExecutions.length > 0)) {
63+
throw new Error(`Hook execution data is empty`);
64+
}
65+
66+
if (meta.HookExecutions.length > 1) {
67+
throw new Error(`Hook execution happened more than once`);
68+
}
69+
70+
if (txResponse.result.meta.TransactionResult !== "tesSUCCESS") {
71+
console.error(JSON.stringify(txResponse, null, 2));
72+
73+
throw new Error(`Transaction failed`);
74+
}
75+
76+
// safe type: we checked everything
77+
const [hookExecution] = meta.HookExecutions as [HookExecution];
78+
79+
const { HookReturnString, HookReturnCode, HookHash } =
80+
hookExecution.HookExecution;
81+
82+
expect(HookReturnString).toBe(HookHash);
83+
// HookPos should be 0
84+
expect(Number(HookReturnCode)).toBe(0);
85+
},
86+
3 * 60_000,
87+
);
88+
});

0 commit comments

Comments
 (0)