Skip to main content

nep17_token_neo/
lib.rs

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}