Skip to main content

sendfun_sdk/utils/
creator_hash.rs

1use solana_address::Address;
2
3/// SHA-256 of the byte length of `creator_platform`, `creator_platform`, the
4/// byte length of `creator_id`, and `creator_id`. Each length is a
5/// little-endian `u32`. Seeds the `CreatorFeeConfig` PDA.
6#[must_use]
7pub fn creator_hash_from_id(
8	creator_platform: &str,
9	creator_id: &str,
10) -> Address {
11	let platform_len = (creator_platform.len() as u32).to_le_bytes();
12	let id_len = (creator_id.len() as u32).to_le_bytes();
13	Address::new_from_array(
14		solana_sha256_hasher::hashv(&[
15			&platform_len,
16			creator_platform.as_bytes(),
17			&id_len,
18			creator_id.as_bytes(),
19		])
20		.to_bytes(),
21	)
22}
23
24/// Copies `text` into `[u8; N]` and pads it with NUL bytes. `None` if `text` is
25/// longer than `N` bytes.
26#[must_use]
27pub fn encode_creator_id<const N: usize>(text: &str) -> Option<[u8; N]> {
28	let bytes = text.as_bytes();
29	let mut out = [0u8; N];
30	out.get_mut(..bytes.len())?.copy_from_slice(bytes);
31	Some(out)
32}
33
34/// Removes the trailing NUL bytes. `None` if the rest is not UTF-8. Pass the
35/// result to [`creator_hash_from_id`]. The padded array gives a different hash.
36#[must_use]
37pub fn decode_creator_id(bytes: &[u8]) -> Option<&str> {
38	let end = match bytes.iter().rposition(|&b| b != 0) {
39		Some(last) => last.checked_add(1)?,
40		None => 0,
41	};
42	core::str::from_utf8(bytes.get(..end)?).ok()
43}
44
45#[cfg(test)]
46mod tests {
47	use solana_address::{Address, address};
48
49	use super::creator_hash_from_id;
50
51	#[test]
52	fn creator_hash_wallet_platform() {
53		let hash =
54			creator_hash_from_id("wallet", "11111111111111111111111111111111");
55		let hash2 =
56			creator_hash_from_id("wallet", "11111111111111111111111111111111");
57		assert_eq!(hash, hash2);
58		let hash3 =
59			creator_hash_from_id("wallet", "22222222222222222222222222222222");
60		assert_ne!(hash, hash3);
61	}
62
63	#[test]
64	fn creator_hash_different_platforms() {
65		let hash_wallet = creator_hash_from_id("wallet", "test_id");
66		let hash_twitter = creator_hash_from_id("twitter", "test_id");
67		assert_ne!(hash_wallet, hash_twitter);
68	}
69
70	#[test]
71	fn creator_hash_matches_sha256() {
72		use sha2::{Digest, Sha256};
73
74		let platform = "wallet";
75		let id = "TestWalletAddress123456789012345";
76		let hash = creator_hash_from_id(platform, id);
77
78		let mut hasher = Sha256::new();
79		hasher.update((platform.len() as u32).to_le_bytes());
80		hasher.update(platform.as_bytes());
81		hasher.update((id.len() as u32).to_le_bytes());
82		hasher.update(id.as_bytes());
83		let expected = Address::new_from_array(hasher.finalize().into());
84		assert_eq!(hash, expected);
85	}
86
87	#[test]
88	fn creator_hash_matches_the_typescript_sdk() {
89		assert_eq!(
90			creator_hash_from_id("twitter", "alice123"),
91			address!("oiZL4KcM2jYpYBHDtTUm9QbGUeWvEfPQN35ufpDzxTL"),
92		);
93	}
94}