1use crate::error::SignerError;
9
10pub mod opcode {
16 pub const PUSH0: u8 = 0x00;
18 pub const PUSHDATA1: u8 = 0x0C;
20 pub const PUSH1: u8 = 0x11;
22 pub const PUSH2: u8 = 0x12;
24 pub const PUSH3: u8 = 0x13;
26 pub const PUSH4: u8 = 0x14;
28 pub const PUSH5: u8 = 0x15;
30 pub const PUSH8: u8 = 0x18;
32 pub const PUSH16: u8 = 0x20;
34 pub const NOP: u8 = 0x21;
36 pub const NEWARRAY: u8 = 0xC5;
38 pub const PACK: u8 = 0xC1;
40 pub const SYSCALL: u8 = 0x41;
42}
43
44#[derive(Debug, Clone, Default)]
46pub struct ScriptBuilder {
47 data: Vec<u8>,
48}
49
50impl ScriptBuilder {
51 #[must_use]
53 pub fn new() -> Self {
54 Self { data: Vec::new() }
55 }
56
57 pub fn emit(&mut self, op: u8) -> &mut Self {
59 self.data.push(op);
60 self
61 }
62
63 pub fn emit_push_integer(&mut self, value: i64) -> &mut Self {
65 if value == -1 {
66 self.data.push(0x0F); } else if value == 0 {
68 self.data.push(opcode::PUSH0);
69 } else if (1..=16).contains(&value) {
70 self.data.push(opcode::PUSH1 + (value as u8 - 1));
71 } else {
72 let bytes = int_to_bytes(value);
74 self.emit_push_bytes(&bytes);
75 }
76 self
77 }
78
79 pub fn emit_push_bytes(&mut self, data: &[u8]) -> &mut Self {
81 let len = data.len();
82 if len <= 0xFF {
83 self.data.push(opcode::PUSHDATA1);
84 self.data.push(len as u8);
85 }
86 self.data.extend_from_slice(data);
87 self
88 }
89
90 pub fn emit_push_hash160(&mut self, hash: &[u8; 20]) -> &mut Self {
92 self.emit_push_bytes(hash)
93 }
94
95 pub fn emit_syscall(&mut self, method_hash: u32) -> &mut Self {
97 self.data.push(opcode::SYSCALL);
98 self.data.extend_from_slice(&method_hash.to_le_bytes());
99 self
100 }
101
102 pub fn emit_contract_call(
106 &mut self,
107 contract_hash: &[u8; 20],
108 method: &str,
109 args_count: usize,
110 ) -> &mut Self {
111 self.emit_push_integer(args_count as i64);
113 self.emit(opcode::PACK);
114 self.emit_push_bytes(method.as_bytes());
116 self.emit_push_hash160(contract_hash);
118 self.emit_syscall(0x627d5b52);
120 self
121 }
122
123 #[must_use]
125 pub fn to_bytes(&self) -> Vec<u8> {
126 self.data.clone()
127 }
128}
129
130fn int_to_bytes(value: i64) -> Vec<u8> {
131 if value == 0 {
132 return vec![0];
133 }
134 let mut val = value;
135 let mut bytes = Vec::new();
136 let negative = val < 0;
137 while val != 0 && val != -1 {
138 bytes.push(val as u8);
139 val >>= 8;
140 }
141 if !negative && (bytes.last().is_some_and(|b| b & 0x80 != 0)) {
143 bytes.push(0);
144 }
145 if negative && (bytes.last().is_some_and(|b| b & 0x80 == 0)) {
146 bytes.push(0xFF);
147 }
148 bytes
149}
150
151pub mod contracts {
157 pub const NEO_TOKEN: [u8; 20] = [
159 0xf5, 0x63, 0xea, 0x40, 0xbc, 0x28, 0x3d, 0x4d, 0x0e, 0x05, 0xc4, 0x8e, 0xa3, 0x05, 0xb3,
160 0xf2, 0xa0, 0x73, 0x40, 0xef,
161 ];
162
163 pub const GAS_TOKEN: [u8; 20] = [
165 0xcf, 0x76, 0xe2, 0x8b, 0xd0, 0x06, 0x2c, 0x4a, 0x47, 0x8e, 0xe3, 0x55, 0x61, 0x01, 0x13,
166 0x19, 0xf3, 0xcf, 0xa4, 0xd2,
167 ];
168}
169
170pub fn nep17_transfer(
178 token_hash: &[u8; 20],
179 from: &[u8; 20],
180 to: &[u8; 20],
181 amount: i64,
182) -> Vec<u8> {
183 let mut sb = ScriptBuilder::new();
184 sb.emit(opcode::PUSH0); sb.emit_push_integer(amount);
187 sb.emit_push_hash160(to);
188 sb.emit_push_hash160(from);
189 sb.emit_contract_call(token_hash, "transfer", 4);
190 sb.to_bytes()
191}
192
193pub fn nep17_balance_of(token_hash: &[u8; 20], account: &[u8; 20]) -> Vec<u8> {
195 let mut sb = ScriptBuilder::new();
196 sb.emit_push_hash160(account);
197 sb.emit_contract_call(token_hash, "balanceOf", 1);
198 sb.to_bytes()
199}
200
201pub fn nep17_symbol(token_hash: &[u8; 20]) -> Vec<u8> {
203 let mut sb = ScriptBuilder::new();
204 sb.emit_contract_call(token_hash, "symbol", 0);
205 sb.to_bytes()
206}
207
208pub fn nep17_decimals(token_hash: &[u8; 20]) -> Vec<u8> {
210 let mut sb = ScriptBuilder::new();
211 sb.emit_contract_call(token_hash, "decimals", 0);
212 sb.to_bytes()
213}
214
215pub fn nep17_total_supply(token_hash: &[u8; 20]) -> Vec<u8> {
217 let mut sb = ScriptBuilder::new();
218 sb.emit_contract_call(token_hash, "totalSupply", 0);
219 sb.to_bytes()
220}
221
222#[derive(Debug, Clone)]
228pub struct NeoTransaction {
229 pub version: u8,
231 pub nonce: u32,
233 pub system_fee: i64,
235 pub network_fee: i64,
237 pub valid_until_block: u32,
239 pub signers: Vec<TransactionSigner>,
241 pub attributes: Vec<TransactionAttribute>,
243 pub script: Vec<u8>,
245}
246
247#[derive(Debug, Clone)]
249pub struct TransactionSigner {
250 pub account: [u8; 20],
252 pub scope: WitnessScope,
254 pub allowed_contracts: Vec<[u8; 20]>,
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260#[repr(u8)]
261pub enum WitnessScope {
262 None = 0x00,
264 CalledByEntry = 0x01,
266 CustomContracts = 0x10,
268 Global = 0x80,
270}
271
272#[derive(Debug, Clone)]
274pub struct TransactionAttribute {
275 pub attr_type: u8,
277 pub data: Vec<u8>,
279}
280
281impl NeoTransaction {
282 #[must_use]
284 pub fn new(script: Vec<u8>) -> Self {
285 Self {
286 version: 0,
287 nonce: 0,
288 system_fee: 0,
289 network_fee: 0,
290 valid_until_block: 0,
291 signers: vec![],
292 attributes: vec![],
293 script,
294 }
295 }
296
297 #[must_use]
299 pub fn serialize_unsigned(&self) -> Vec<u8> {
300 let mut buf = Vec::new();
301 buf.push(self.version);
302 buf.extend_from_slice(&self.nonce.to_le_bytes());
303 buf.extend_from_slice(&self.system_fee.to_le_bytes());
304 buf.extend_from_slice(&self.network_fee.to_le_bytes());
305 buf.extend_from_slice(&self.valid_until_block.to_le_bytes());
306
307 write_var_int(&mut buf, self.signers.len() as u64);
309 for signer in &self.signers {
310 buf.extend_from_slice(&signer.account);
311 buf.push(signer.scope as u8);
312 if signer.scope == WitnessScope::CustomContracts {
313 write_var_int(&mut buf, signer.allowed_contracts.len() as u64);
314 for c in &signer.allowed_contracts {
315 buf.extend_from_slice(c);
316 }
317 }
318 }
319
320 write_var_int(&mut buf, self.attributes.len() as u64);
322 for attr in &self.attributes {
323 buf.push(attr.attr_type);
324 write_var_int(&mut buf, attr.data.len() as u64);
325 buf.extend_from_slice(&attr.data);
326 }
327
328 write_var_int(&mut buf, self.script.len() as u64);
330 buf.extend_from_slice(&self.script);
331
332 buf
333 }
334
335 #[must_use]
337 pub fn hash(&self) -> [u8; 32] {
338 use sha2::{Digest, Sha256};
339 let data = self.serialize_unsigned();
340 let mut out = [0u8; 32];
341 out.copy_from_slice(&Sha256::digest(data));
342 out
343 }
344
345 pub fn sign(&self, signer: &super::NeoSigner) -> Result<super::NeoSignature, SignerError> {
347 let hash = self.hash();
348 signer.sign_digest(&hash)
349 }
350}
351
352fn write_var_int(buf: &mut Vec<u8>, val: u64) {
353 if val < 0xFD {
354 buf.push(val as u8);
355 } else if val <= 0xFFFF {
356 buf.push(0xFD);
357 buf.extend_from_slice(&(val as u16).to_le_bytes());
358 } else if val <= 0xFFFF_FFFF {
359 buf.push(0xFE);
360 buf.extend_from_slice(&(val as u32).to_le_bytes());
361 } else {
362 buf.push(0xFF);
363 buf.extend_from_slice(&val.to_le_bytes());
364 }
365}
366
367pub fn read_var_int(data: &[u8]) -> Result<(u64, usize), SignerError> {
375 if data.is_empty() {
376 return Err(SignerError::ParseError("read_var_int: empty".into()));
377 }
378 match data[0] {
379 0..=0xFC => Ok((u64::from(data[0]), 1)),
380 0xFD => {
381 if data.len() < 3 {
382 return Err(SignerError::ParseError(
383 "read_var_int: truncated u16".into(),
384 ));
385 }
386 Ok((u64::from(u16::from_le_bytes([data[1], data[2]])), 3))
387 }
388 0xFE => {
389 if data.len() < 5 {
390 return Err(SignerError::ParseError(
391 "read_var_int: truncated u32".into(),
392 ));
393 }
394 Ok((
395 u64::from(u32::from_le_bytes([data[1], data[2], data[3], data[4]])),
396 5,
397 ))
398 }
399 0xFF => {
400 if data.len() < 9 {
401 return Err(SignerError::ParseError(
402 "read_var_int: truncated u64".into(),
403 ));
404 }
405 let val = u64::from_le_bytes([
406 data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8],
407 ]);
408 Ok((val, 9))
409 }
410 }
411}
412
413impl NeoTransaction {
414 pub fn deserialize(data: &[u8]) -> Result<Self, SignerError> {
418 if data.len() < 25 {
419 return Err(SignerError::ParseError("neo tx: too short".into()));
420 }
421 let mut pos = 0;
422
423 let version = data[pos];
424 pos += 1;
425
426 let nonce = u32::from_le_bytes(
427 data[pos..pos + 4]
428 .try_into()
429 .map_err(|_| SignerError::ParseError("neo tx: nonce".into()))?,
430 );
431 pos += 4;
432
433 let system_fee = i64::from_le_bytes(
434 data[pos..pos + 8]
435 .try_into()
436 .map_err(|_| SignerError::ParseError("neo tx: system_fee".into()))?,
437 );
438 pos += 8;
439
440 let network_fee = i64::from_le_bytes(
441 data[pos..pos + 8]
442 .try_into()
443 .map_err(|_| SignerError::ParseError("neo tx: network_fee".into()))?,
444 );
445 pos += 8;
446
447 let valid_until_block = u32::from_le_bytes(
448 data[pos..pos + 4]
449 .try_into()
450 .map_err(|_| SignerError::ParseError("neo tx: valid_until".into()))?,
451 );
452 pos += 4;
453
454 let (num_signers, consumed) = read_var_int(&data[pos..])?;
456 pos += consumed;
457 let mut signers = Vec::new();
458 for _ in 0..num_signers {
459 if pos + 21 > data.len() {
460 return Err(SignerError::ParseError("neo tx: truncated signer".into()));
461 }
462 let mut account = [0u8; 20];
463 account.copy_from_slice(&data[pos..pos + 20]);
464 pos += 20;
465 let scope_byte = data[pos];
466 pos += 1;
467 let scope = match scope_byte {
468 0x00 => WitnessScope::None,
469 0x01 => WitnessScope::CalledByEntry,
470 0x10 => WitnessScope::CustomContracts,
471 0x80 => WitnessScope::Global,
472 _ => WitnessScope::CalledByEntry,
473 };
474 let mut allowed_contracts = Vec::new();
475 if scope == WitnessScope::CustomContracts {
476 let (num_contracts, c) = read_var_int(&data[pos..])?;
477 pos += c;
478 for _ in 0..num_contracts {
479 if pos + 20 > data.len() {
480 return Err(SignerError::ParseError(
481 "neo tx: truncated allowed contract".into(),
482 ));
483 }
484 let mut contract = [0u8; 20];
485 contract.copy_from_slice(&data[pos..pos + 20]);
486 pos += 20;
487 allowed_contracts.push(contract);
488 }
489 }
490 signers.push(TransactionSigner {
491 account,
492 scope,
493 allowed_contracts,
494 });
495 }
496
497 let (num_attrs, consumed) = read_var_int(&data[pos..])?;
499 pos += consumed;
500 let mut attributes = Vec::new();
501 for _ in 0..num_attrs {
502 if pos >= data.len() {
503 return Err(SignerError::ParseError("neo tx: truncated attr".into()));
504 }
505 let attr_type = data[pos];
506 pos += 1;
507 let (attr_len, c) = read_var_int(&data[pos..])?;
508 pos += c;
509 if pos + attr_len as usize > data.len() {
510 return Err(SignerError::ParseError(
511 "neo tx: truncated attr data".into(),
512 ));
513 }
514 let attr_data = data[pos..pos + attr_len as usize].to_vec();
515 pos += attr_len as usize;
516 attributes.push(TransactionAttribute {
517 attr_type,
518 data: attr_data,
519 });
520 }
521
522 let (script_len, consumed) = read_var_int(&data[pos..])?;
524 pos += consumed;
525 if pos + script_len as usize > data.len() {
526 return Err(SignerError::ParseError("neo tx: truncated script".into()));
527 }
528 let script = data[pos..pos + script_len as usize].to_vec();
529
530 Ok(Self {
531 version,
532 nonce,
533 system_fee,
534 network_fee,
535 valid_until_block,
536 signers,
537 attributes,
538 script,
539 })
540 }
541}
542
543pub fn nep17_approve(
552 token_hash: &[u8; 20],
553 owner: &[u8; 20],
554 spender: &[u8; 20],
555 amount: i64,
556) -> Vec<u8> {
557 let mut sb = ScriptBuilder::new();
558 sb.emit_push_integer(amount)
559 .emit_push_hash160(spender)
560 .emit_push_hash160(owner)
561 .emit(opcode::PUSH3)
562 .emit(opcode::PACK)
563 .emit_contract_call(token_hash, "approve", 3);
564 sb.to_bytes()
565}
566
567pub fn nep17_allowance(token_hash: &[u8; 20], owner: &[u8; 20], spender: &[u8; 20]) -> Vec<u8> {
571 let mut sb = ScriptBuilder::new();
572 sb.emit_push_hash160(spender)
573 .emit_push_hash160(owner)
574 .emit(opcode::PUSH2)
575 .emit(opcode::PACK)
576 .emit_contract_call(token_hash, "allowance", 2);
577 sb.to_bytes()
578}
579
580pub fn nep17_transfer_from(
584 token_hash: &[u8; 20],
585 spender: &[u8; 20],
586 from: &[u8; 20],
587 to: &[u8; 20],
588 amount: i64,
589) -> Vec<u8> {
590 let mut sb = ScriptBuilder::new();
591 sb.emit_push_integer(amount)
592 .emit_push_hash160(to)
593 .emit_push_hash160(from)
594 .emit_push_hash160(spender)
595 .emit(opcode::PUSH4)
596 .emit(opcode::PACK)
597 .emit_contract_call(token_hash, "transferFrom", 4);
598 sb.to_bytes()
599}
600
601pub const CONTRACT_MANAGEMENT_HASH: [u8; 20] = [
607 0xfd, 0xa3, 0xfa, 0x43, 0x34, 0x6b, 0x9d, 0x6b, 0x51, 0xd3, 0x3c, 0x64, 0xb2, 0x1c, 0x68, 0x24,
608 0x38, 0x97, 0x28, 0xe6,
609];
610
611pub fn contract_deploy(nef_bytes: &[u8], manifest_json: &str) -> Vec<u8> {
617 let mut sb = ScriptBuilder::new();
618 sb.emit_push_bytes(manifest_json.as_bytes())
619 .emit_push_bytes(nef_bytes)
620 .emit(opcode::PUSH2)
621 .emit(opcode::PACK)
622 .emit_contract_call(&CONTRACT_MANAGEMENT_HASH, "deploy", 2);
623 sb.to_bytes()
624}
625
626pub fn contract_update(nef_bytes: &[u8], manifest_json: &str) -> Vec<u8> {
628 let mut sb = ScriptBuilder::new();
629 sb.emit_push_bytes(manifest_json.as_bytes())
630 .emit_push_bytes(nef_bytes)
631 .emit(opcode::PUSH2)
632 .emit(opcode::PACK)
633 .emit_contract_call(&CONTRACT_MANAGEMENT_HASH, "update", 2);
634 sb.to_bytes()
635}
636
637pub fn contract_destroy() -> Vec<u8> {
639 let mut sb = ScriptBuilder::new();
640 sb.emit(opcode::PUSH0)
641 .emit(opcode::PACK)
642 .emit_contract_call(&CONTRACT_MANAGEMENT_HASH, "destroy", 0);
643 sb.to_bytes()
644}
645
646pub fn neo_vote(voter: &[u8; 20], candidate_pubkey: Option<&[u8; 33]>) -> Vec<u8> {
658 let mut sb = ScriptBuilder::new();
659 match candidate_pubkey {
660 Some(pk) => sb.emit_push_bytes(pk),
661 None => sb.emit(opcode::PUSH0), };
663 sb.emit_push_hash160(voter)
664 .emit(opcode::PUSH2)
665 .emit(opcode::PACK)
666 .emit_contract_call(&contracts::NEO_TOKEN, "vote", 2);
667 sb.to_bytes()
668}
669
670pub fn neo_unclaimed_gas(account: &[u8; 20], end_height: u32) -> Vec<u8> {
672 let mut sb = ScriptBuilder::new();
673 sb.emit_push_integer(i64::from(end_height))
674 .emit_push_hash160(account)
675 .emit(opcode::PUSH2)
676 .emit(opcode::PACK)
677 .emit_contract_call(&contracts::NEO_TOKEN, "unclaimedGas", 2);
678 sb.to_bytes()
679}
680
681pub fn neo_register_candidate(pubkey: &[u8; 33]) -> Vec<u8> {
683 let mut sb = ScriptBuilder::new();
684 sb.emit_push_bytes(pubkey)
685 .emit(opcode::PUSH1)
686 .emit(opcode::PACK)
687 .emit_contract_call(&contracts::NEO_TOKEN, "registerCandidate", 1);
688 sb.to_bytes()
689}
690
691pub fn neo_get_candidates() -> Vec<u8> {
693 let mut sb = ScriptBuilder::new();
694 sb.emit(opcode::PUSH0)
695 .emit(opcode::PACK)
696 .emit_contract_call(&contracts::NEO_TOKEN, "getCandidates", 0);
697 sb.to_bytes()
698}
699
700pub fn neo_get_committee() -> Vec<u8> {
702 let mut sb = ScriptBuilder::new();
703 sb.emit(opcode::PUSH0)
704 .emit(opcode::PACK)
705 .emit_contract_call(&contracts::NEO_TOKEN, "getCommittee", 0);
706 sb.to_bytes()
707}
708
709#[cfg(test)]
714#[allow(clippy::unwrap_used, clippy::expect_used)]
715mod tests {
716 use super::*;
717 use crate::traits::KeyPair;
718
719 #[test]
722 fn test_script_builder_push_integer() {
723 let mut sb = ScriptBuilder::new();
724 sb.emit_push_integer(0);
725 assert_eq!(sb.to_bytes(), vec![opcode::PUSH0]);
726 }
727
728 #[test]
729 fn test_script_builder_push_integer_range() {
730 for i in 1..=16 {
731 let mut sb = ScriptBuilder::new();
732 sb.emit_push_integer(i);
733 let bytes = sb.to_bytes();
734 assert_eq!(bytes.len(), 1);
735 assert_eq!(bytes[0], opcode::PUSH1 + (i as u8 - 1));
736 }
737 }
738
739 #[test]
740 fn test_script_builder_push_bytes() {
741 let mut sb = ScriptBuilder::new();
742 sb.emit_push_bytes(b"hello");
743 let bytes = sb.to_bytes();
744 assert_eq!(bytes[0], opcode::PUSHDATA1);
745 assert_eq!(bytes[1], 5);
746 assert_eq!(&bytes[2..], b"hello");
747 }
748
749 #[test]
750 fn test_script_builder_syscall() {
751 let mut sb = ScriptBuilder::new();
752 sb.emit_syscall(0x627d5b52);
753 let bytes = sb.to_bytes();
754 assert_eq!(bytes[0], opcode::SYSCALL);
755 assert_eq!(&bytes[1..5], &0x627d5b52u32.to_le_bytes());
756 }
757
758 #[test]
761 fn test_nep17_transfer_script() {
762 let from = [0xAA; 20];
763 let to = [0xBB; 20];
764 let script = nep17_transfer(&contracts::NEO_TOKEN, &from, &to, 10);
765 assert!(!script.is_empty());
766 let s = String::from_utf8_lossy(&script);
768 assert!(s.contains("transfer"));
769 }
770
771 #[test]
772 fn test_nep17_balance_of_script() {
773 let account = [0xCC; 20];
774 let script = nep17_balance_of(&contracts::GAS_TOKEN, &account);
775 assert!(!script.is_empty());
776 let s = String::from_utf8_lossy(&script);
777 assert!(s.contains("balanceOf"));
778 }
779
780 #[test]
781 fn test_nep17_symbol() {
782 let script = nep17_symbol(&contracts::NEO_TOKEN);
783 assert!(!script.is_empty());
784 let s = String::from_utf8_lossy(&script);
785 assert!(s.contains("symbol"));
786 }
787
788 #[test]
789 fn test_nep17_decimals() {
790 let script = nep17_decimals(&contracts::GAS_TOKEN);
791 let s = String::from_utf8_lossy(&script);
792 assert!(s.contains("decimals"));
793 }
794
795 #[test]
796 fn test_nep17_total_supply() {
797 let script = nep17_total_supply(&contracts::NEO_TOKEN);
798 let s = String::from_utf8_lossy(&script);
799 assert!(s.contains("totalSupply"));
800 }
801
802 #[test]
805 fn test_neo_transaction_serialization() {
806 let script = nep17_transfer(&contracts::NEO_TOKEN, &[0xAA; 20], &[0xBB; 20], 1);
807 let tx = NeoTransaction {
808 version: 0,
809 nonce: 12345,
810 system_fee: 100_000,
811 network_fee: 50_000,
812 valid_until_block: 1000,
813 signers: vec![TransactionSigner {
814 account: [0xAA; 20],
815 scope: WitnessScope::CalledByEntry,
816 allowed_contracts: vec![],
817 }],
818 attributes: vec![],
819 script,
820 };
821 let serialized = tx.serialize_unsigned();
822 assert!(!serialized.is_empty());
823 assert_eq!(serialized[0], 0); }
825
826 #[test]
827 fn test_neo_transaction_hash_deterministic() {
828 let script = nep17_transfer(&contracts::GAS_TOKEN, &[0xAA; 20], &[0xBB; 20], 100);
829 let tx = NeoTransaction::new(script);
830 assert_eq!(tx.hash(), tx.hash());
831 }
832
833 #[test]
834 fn test_neo_transaction_sign() {
835 let signer = super::super::NeoSigner::generate().unwrap();
836 let script_hash = signer.script_hash();
837 let script = nep17_transfer(&contracts::NEO_TOKEN, &script_hash, &[0xBB; 20], 1);
838 let tx = NeoTransaction::new(script);
839 let sig = tx.sign(&signer).unwrap();
840 assert_eq!(sig.to_bytes().len(), 64);
841 }
842
843 #[test]
844 fn test_neo_transaction_different_nonce_different_hash() {
845 let script = vec![0x00];
846 let mut tx1 = NeoTransaction::new(script.clone());
847 tx1.nonce = 1;
848 let mut tx2 = NeoTransaction::new(script);
849 tx2.nonce = 2;
850 assert_ne!(tx1.hash(), tx2.hash());
851 }
852
853 #[test]
856 fn test_neo_tx_serialize_deserialize_roundtrip() {
857 let script = nep17_transfer(&contracts::NEO_TOKEN, &[0xAA; 20], &[0xBB; 20], 10);
858 let tx = NeoTransaction {
859 version: 0,
860 nonce: 42,
861 system_fee: 100_000,
862 network_fee: 50_000,
863 valid_until_block: 999,
864 signers: vec![TransactionSigner {
865 account: [0xAA; 20],
866 scope: WitnessScope::CalledByEntry,
867 allowed_contracts: vec![],
868 }],
869 attributes: vec![],
870 script,
871 };
872 let bytes = tx.serialize_unsigned();
873 let restored = NeoTransaction::deserialize(&bytes).unwrap();
874 assert_eq!(restored.version, tx.version);
875 assert_eq!(restored.nonce, tx.nonce);
876 assert_eq!(restored.system_fee, tx.system_fee);
877 assert_eq!(restored.network_fee, tx.network_fee);
878 assert_eq!(restored.valid_until_block, tx.valid_until_block);
879 assert_eq!(restored.signers.len(), 1);
880 assert_eq!(restored.signers[0].account, [0xAA; 20]);
881 assert_eq!(restored.script, tx.script);
882 }
883
884 #[test]
885 fn test_neo_tx_deserialize_empty_fails() {
886 assert!(NeoTransaction::deserialize(&[]).is_err());
887 assert!(NeoTransaction::deserialize(&[0u8; 10]).is_err());
888 }
889
890 #[test]
891 fn test_read_var_int() {
892 assert_eq!(read_var_int(&[0x00]).unwrap(), (0, 1));
893 assert_eq!(read_var_int(&[0xFC]).unwrap(), (252, 1));
894 assert_eq!(read_var_int(&[0xFD, 0x01, 0x00]).unwrap(), (1, 3));
895 assert_eq!(
896 read_var_int(&[0xFE, 0x01, 0x00, 0x00, 0x00]).unwrap(),
897 (1, 5)
898 );
899 }
900
901 #[test]
904 fn test_nep17_approve_script() {
905 let script = nep17_approve(&contracts::GAS_TOKEN, &[0xAA; 20], &[0xBB; 20], 1000);
906 assert!(!script.is_empty());
907 let s = String::from_utf8_lossy(&script);
908 assert!(s.contains("approve"));
909 }
910
911 #[test]
912 fn test_nep17_allowance_script() {
913 let script = nep17_allowance(&contracts::GAS_TOKEN, &[0xAA; 20], &[0xBB; 20]);
914 assert!(!script.is_empty());
915 let s = String::from_utf8_lossy(&script);
916 assert!(s.contains("allowance"));
917 }
918
919 #[test]
920 fn test_nep17_transfer_from_script() {
921 let script = nep17_transfer_from(
922 &contracts::GAS_TOKEN,
923 &[0xAA; 20],
924 &[0xBB; 20],
925 &[0xCC; 20],
926 500,
927 );
928 let s = String::from_utf8_lossy(&script);
929 assert!(s.contains("transferFrom"));
930 }
931
932 #[test]
935 fn test_contract_deploy_script() {
936 let nef = b"\x4e\x45\x46\x33"; let manifest = r#"{"name":"test"}"#;
938 let script = contract_deploy(nef, manifest);
939 assert!(!script.is_empty());
940 let s = String::from_utf8_lossy(&script);
941 assert!(s.contains("deploy"));
942 }
943
944 #[test]
945 fn test_contract_update_script() {
946 let script = contract_update(b"\x00", r#"{}"#);
947 let s = String::from_utf8_lossy(&script);
948 assert!(s.contains("update"));
949 }
950
951 #[test]
952 fn test_contract_destroy_script() {
953 let script = contract_destroy();
954 let s = String::from_utf8_lossy(&script);
955 assert!(s.contains("destroy"));
956 }
957
958 #[test]
961 fn test_neo_vote_script() {
962 let voter = [0xAA; 20];
963 let pubkey = [0x02; 33];
964 let script = neo_vote(&voter, Some(&pubkey));
965 let s = String::from_utf8_lossy(&script);
966 assert!(s.contains("vote"));
967 }
968
969 #[test]
970 fn test_neo_vote_cancel() {
971 let voter = [0xAA; 20];
972 let script = neo_vote(&voter, None);
973 let s = String::from_utf8_lossy(&script);
974 assert!(s.contains("vote"));
975 }
976
977 #[test]
978 fn test_neo_unclaimed_gas() {
979 let script = neo_unclaimed_gas(&[0xAA; 20], 100_000);
980 let s = String::from_utf8_lossy(&script);
981 assert!(s.contains("unclaimedGas"));
982 }
983
984 #[test]
985 fn test_neo_register_candidate() {
986 let script = neo_register_candidate(&[0x02; 33]);
987 let s = String::from_utf8_lossy(&script);
988 assert!(s.contains("registerCandidate"));
989 }
990
991 #[test]
992 fn test_neo_get_candidates() {
993 let script = neo_get_candidates();
994 let s = String::from_utf8_lossy(&script);
995 assert!(s.contains("getCandidates"));
996 }
997
998 #[test]
999 fn test_neo_get_committee() {
1000 let script = neo_get_committee();
1001 let s = String::from_utf8_lossy(&script);
1002 assert!(s.contains("getCommittee"));
1003 }
1004}