1use core::slice;
2use neo_devpack::prelude::*;
3
4const TOTAL_SUPPLY_KEY: &[u8] = b"token:total_supply";
5const BALANCE_PREFIX: &[u8] = b"token:balance:";
6
7neo_manifest_overlay!(
8 r#"{
9 "name": "SampleNEP17",
10 "supportedstandards": ["NEP-17"],
11 "features": { "storage": true },
12 "abi": {
13 "methods": [
14 {
15 "name": "init",
16 "parameters": [
17 {"name": "owner", "type": "Hash160"},
18 {"name": "amount", "type": "Integer"}
19 ],
20 "returntype": "Boolean"
21 },
22 {
23 "name": "transfer",
24 "parameters": [
25 {"name": "from", "type": "Hash160"},
26 {"name": "to", "type": "Hash160"},
27 {"name": "amount", "type": "Integer"},
28 {"name": "data", "type": "Any"}
29 ],
30 "returntype": "Boolean"
31 },
32 {
33 "name": "balanceOf",
34 "parameters": [
35 {"name": "account", "type": "Hash160"}
36 ],
37 "returntype": "Integer"
38 },
39 {
40 "name": "totalSupply",
41 "parameters": [],
42 "returntype": "Integer"
43 }
44 ],
45 "events": [
46 {
47 "name": "Transfer",
48 "parameters": [
49 {"name": "from", "type": "Hash160"},
50 {"name": "to", "type": "Hash160"},
51 {"name": "amount", "type": "Integer"}
52 ]
53 }
54 ]
55 }
56}"#
57);
58
59#[neo_event]
60pub struct TransferEvent {
61 pub from: NeoByteString,
62 pub to: NeoByteString,
63 pub amount: NeoInteger,
64}
65
66fn storage_context() -> Option<NeoStorageContext> {
67 NeoStorage::get_context().ok()
68}
69
70fn balance_key(account: &NeoByteString) -> Vec<u8> {
71 let mut key = BALANCE_PREFIX.to_vec();
72 key.extend_from_slice(account.as_slice());
73 key
74}
75
76fn read_i64(bytes: &NeoByteString) -> i64 {
77 let raw = bytes.as_slice();
78 if raw.is_empty() {
79 0
80 } else {
81 let mut buffer = [0u8; 8];
82 let copy_len = raw.len().min(8);
83 buffer[..copy_len].copy_from_slice(&raw[..copy_len]);
84 i64::from_le_bytes(buffer)
85 }
86}
87
88fn write_i64(value: i64) -> NeoByteString {
89 NeoByteString::from_slice(&value.to_le_bytes())
90}
91
92fn load_total_supply(ctx: &NeoStorageContext) -> NeoResult<i64> {
93 let key = NeoByteString::from_slice(TOTAL_SUPPLY_KEY);
94 let data = NeoStorage::get(ctx, &key)?;
95 Ok(read_i64(&data))
96}
97
98fn store_total_supply(ctx: &NeoStorageContext, value: i64) -> NeoResult<()> {
99 let key = NeoByteString::from_slice(TOTAL_SUPPLY_KEY);
100 let encoded = write_i64(value);
101 NeoStorage::put(ctx, &key, &encoded)
102}
103
104fn load_balance(ctx: &NeoStorageContext, account: &NeoByteString) -> NeoResult<i64> {
105 let key = NeoByteString::from_slice(&balance_key(account));
106 let data = NeoStorage::get(ctx, &key)?;
107 Ok(read_i64(&data))
108}
109
110fn store_balance(ctx: &NeoStorageContext, account: &NeoByteString, value: i64) -> NeoResult<()> {
111 let key = NeoByteString::from_slice(&balance_key(account));
112 if value == 0 {
113 NeoStorage::delete(ctx, &key)
114 } else {
115 let encoded = write_i64(value);
116 NeoStorage::put(ctx, &key, &encoded)
117 }
118}
119
120#[allow(improper_ctypes_definitions)]
121#[no_mangle]
122pub extern "C" fn init(owner_ptr: i64, owner_len: i64, amount: i64) -> i64 {
123 if amount <= 0 {
124 return 0;
125 }
126
127 let Some(ctx) = storage_context() else {
128 return 0;
129 };
130
131 if load_total_supply(&ctx).unwrap_or(0) != 0 {
132 return 0;
133 }
134
135 let Some(owner) = read_address(owner_ptr, owner_len) else {
136 return 0;
137 };
138
139 match store_total_supply(&ctx, amount) {
140 Ok(_) => {}
141 Err(_) => return 0,
142 }
143
144 match store_balance(&ctx, &owner, amount) {
145 Ok(_) => {}
146 Err(_) => return 0,
147 }
148
149 TransferEvent {
150 from: NeoByteString::new(Vec::new()),
151 to: owner,
152 amount: NeoInteger::new(amount),
153 }
154 .emit()
155 .ok();
156
157 1
158}
159
160#[no_mangle]
161#[neo_safe]
162pub extern "C" fn totalSupply() -> i64 {
163 storage_context()
164 .and_then(|ctx| load_total_supply(&ctx).ok())
165 .unwrap_or(0)
166}
167
168#[allow(improper_ctypes_definitions)]
169#[no_mangle]
170#[neo_safe]
171pub extern "C" fn balanceOf(account_ptr: i64, account_len: i64) -> i64 {
172 let Some(account) = read_address(account_ptr, account_len) else {
173 return 0;
174 };
175 storage_context()
176 .and_then(|ctx| load_balance(&ctx, &account).ok())
177 .unwrap_or(0)
178}
179
180#[allow(improper_ctypes_definitions)]
181#[no_mangle]
182pub extern "C" fn transfer(
183 from_ptr: i64,
184 from_len: i64,
185 to_ptr: i64,
186 to_len: i64,
187 amount: i64,
188 _data_ptr: i64,
189 _data_len: i64,
190) -> i64 {
191 if amount <= 0 {
192 return 0;
193 }
194
195 let Some(ctx) = storage_context() else {
196 return 0;
197 };
198
199 let Some(from) = read_address(from_ptr, from_len) else {
200 return 0;
201 };
202 let Some(to) = read_address(to_ptr, to_len) else {
203 return 0;
204 };
205
206 if from.as_slice() == to.as_slice() {
207 return 0;
208 }
209
210 if !ensure_witness(&from) {
211 return 0;
212 }
213
214 let from_balance = match load_balance(&ctx, &from) {
215 Ok(value) => value,
216 Err(_) => return 0,
217 };
218
219 if from_balance < amount {
220 return 0;
221 }
222
223 let to_balance = match load_balance(&ctx, &to) {
224 Ok(value) => value,
225 Err(_) => 0,
226 };
227
228 let new_from_balance = match from_balance.checked_sub(amount) {
229 Some(value) => value,
230 None => return 0,
231 };
232
233 let new_to_balance = match to_balance.checked_add(amount) {
234 Some(value) => value,
235 None => return 0,
236 };
237
238 if store_balance(&ctx, &from, new_from_balance).is_err() {
239 return 0;
240 }
241 if store_balance(&ctx, &to, new_to_balance).is_err() {
242 return 0;
243 }
244
245 TransferEvent {
246 from,
247 to,
248 amount: NeoInteger::new(amount),
249 }
250 .emit()
251 .ok();
252
253 1
254}
255
256#[allow(improper_ctypes_definitions)]
257#[no_mangle]
258pub extern "C" fn onNEP17Payment(from: NeoByteString, amount: i64, _data: NeoByteString) {
259 if amount <= 0 {
260 return;
261 }
262
263 let Some(ctx) = storage_context() else {
264 return;
265 };
266
267 let current_balance = match load_balance(&ctx, &from) {
268 Ok(value) => value,
269 Err(_) => 0,
270 };
271
272 let new_balance = match current_balance.checked_add(amount) {
273 Some(value) => value,
274 None => return,
275 };
276
277 if store_balance(&ctx, &from, new_balance).is_err() {
278 return;
279 }
280
281 TransferEvent {
282 from: NeoByteString::new(Vec::new()),
283 to: from,
284 amount: NeoInteger::new(amount),
285 }
286 .emit()
287 .ok();
288}
289
290fn read_address(ptr: i64, len: i64) -> Option<NeoByteString> {
291 let bytes = read_bytes(ptr, len)?;
292 if bytes.len() != 20 {
293 return None;
294 }
295 Some(NeoByteString::from_slice(&bytes))
296}
297
298fn read_bytes(ptr: i64, len: i64) -> Option<Vec<u8>> {
299 if ptr == 0 || len <= 0 {
300 return None;
301 }
302 let len = len as usize;
303 let slice = unsafe { slice::from_raw_parts(ptr as *const u8, len) };
304 Some(slice.to_vec())
305}
306
307fn ensure_witness(account: &NeoByteString) -> bool {
308 NeoRuntime::check_witness(account)
309 .ok()
310 .map(|b| b.as_bool())
311 .unwrap_or(false)
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use std::sync::{Mutex, OnceLock};
318
319 fn test_lock() -> &'static Mutex<()> {
320 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
321 LOCK.get_or_init(|| Mutex::new(()))
322 }
323
324 fn address(byte: u8) -> Vec<u8> {
325 vec![byte; 20]
326 }
327
328 fn reset_state() {
329 let ctx = storage_context().unwrap();
330 NeoStorage::delete(&ctx, &NeoByteString::from_slice(TOTAL_SUPPLY_KEY)).ok();
331 if let Ok(iter) = NeoStorage::find(&ctx, &NeoByteString::from_slice(BALANCE_PREFIX)) {
332 let mut iterator = iter;
333 while iterator.has_next() {
334 if let Some(entry) = iterator.next() {
335 if let Some(key) = entry
336 .as_struct()
337 .and_then(|st| st.get_field("key"))
338 .and_then(NeoValue::as_byte_string)
339 {
340 NeoStorage::delete(&ctx, &key).ok();
341 }
342 }
343 }
344 }
345 }
346
347 fn configure_sample(amount: i64) {
348 reset_state();
349 let owner = address(0x44);
350 assert_eq!(init(owner.as_ptr() as i64, owner.len() as i64, amount), 1);
351 }
352
353 #[test]
354 fn init_and_supply() {
355 let _guard = test_lock().lock().unwrap();
356 configure_sample(1_000_000);
357 assert_eq!(totalSupply(), 1_000_000);
358 }
359
360 #[test]
361 fn balance_reflects_supply() {
362 let _guard = test_lock().lock().unwrap();
363 let owner = address(0x55);
364 configure_sample(500_000);
365 let balance = balanceOf(owner.as_ptr() as i64, owner.len() as i64);
366 assert_eq!(balance, 500_000);
367 }
368}