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 } else if len <= 0xFFFF {
86 self.data.push(0x0D); self.data.extend_from_slice(&(len as u16).to_le_bytes());
88 } else {
89 self.data.push(0x0E); self.data.extend_from_slice(&(len as u32).to_le_bytes());
91 }
92 self.data.extend_from_slice(data);
93 self
94 }
95
96 pub fn emit_push_hash160(&mut self, hash: &[u8; 20]) -> &mut Self {
98 self.emit_push_bytes(hash)
99 }
100
101 pub fn emit_syscall(&mut self, method_hash: u32) -> &mut Self {
103 self.data.push(opcode::SYSCALL);
104 self.data.extend_from_slice(&method_hash.to_le_bytes());
105 self
106 }
107
108 pub fn emit_contract_call(
112 &mut self,
113 contract_hash: &[u8; 20],
114 method: &str,
115 args_count: usize,
116 ) -> &mut Self {
117 self.emit_push_integer(args_count as i64);
119 self.emit(opcode::PACK);
120 self.emit_push_bytes(method.as_bytes());
122 self.emit_push_hash160(contract_hash);
124 self.emit_syscall(0x627d5b52);
126 self
127 }
128
129 #[must_use]
131 pub fn to_bytes(&self) -> Vec<u8> {
132 self.data.clone()
133 }
134}
135
136fn int_to_bytes(value: i64) -> Vec<u8> {
137 if value == 0 {
138 return vec![0];
139 }
140 let mut val = value;
141 let mut bytes = Vec::new();
142 let negative = val < 0;
143 while val != 0 && val != -1 {
144 bytes.push(val as u8);
145 val >>= 8;
146 }
147 if !negative && (bytes.last().is_some_and(|b| b & 0x80 != 0)) {
149 bytes.push(0);
150 }
151 if negative && (bytes.last().is_some_and(|b| b & 0x80 == 0)) {
152 bytes.push(0xFF);
153 }
154 bytes
155}
156
157pub mod contracts {
163 pub const NEO_TOKEN: [u8; 20] = [
165 0xf5, 0x63, 0xea, 0x40, 0xbc, 0x28, 0x3d, 0x4d, 0x0e, 0x05, 0xc4, 0x8e, 0xa3, 0x05, 0xb3,
166 0xf2, 0xa0, 0x73, 0x40, 0xef,
167 ];
168
169 pub const GAS_TOKEN: [u8; 20] = [
171 0xcf, 0x76, 0xe2, 0x8b, 0xd0, 0x06, 0x2c, 0x4a, 0x47, 0x8e, 0xe3, 0x55, 0x61, 0x01, 0x13,
172 0x19, 0xf3, 0xcf, 0xa4, 0xd2,
173 ];
174}
175
176pub fn nep17_transfer(
184 token_hash: &[u8; 20],
185 from: &[u8; 20],
186 to: &[u8; 20],
187 amount: i64,
188) -> Vec<u8> {
189 let mut sb = ScriptBuilder::new();
190 sb.emit(opcode::PUSH0); sb.emit_push_integer(amount);
193 sb.emit_push_hash160(to);
194 sb.emit_push_hash160(from);
195 sb.emit_contract_call(token_hash, "transfer", 4);
196 sb.to_bytes()
197}
198
199pub fn nep17_balance_of(token_hash: &[u8; 20], account: &[u8; 20]) -> Vec<u8> {
201 let mut sb = ScriptBuilder::new();
202 sb.emit_push_hash160(account);
203 sb.emit_contract_call(token_hash, "balanceOf", 1);
204 sb.to_bytes()
205}
206
207pub fn nep17_symbol(token_hash: &[u8; 20]) -> Vec<u8> {
209 let mut sb = ScriptBuilder::new();
210 sb.emit_contract_call(token_hash, "symbol", 0);
211 sb.to_bytes()
212}
213
214pub fn nep17_decimals(token_hash: &[u8; 20]) -> Vec<u8> {
216 let mut sb = ScriptBuilder::new();
217 sb.emit_contract_call(token_hash, "decimals", 0);
218 sb.to_bytes()
219}
220
221pub fn nep17_total_supply(token_hash: &[u8; 20]) -> Vec<u8> {
223 let mut sb = ScriptBuilder::new();
224 sb.emit_contract_call(token_hash, "totalSupply", 0);
225 sb.to_bytes()
226}
227
228#[derive(Debug, Clone)]
234pub struct NeoTransaction {
235 pub version: u8,
237 pub nonce: u32,
239 pub system_fee: i64,
241 pub network_fee: i64,
243 pub valid_until_block: u32,
245 pub signers: Vec<TransactionSigner>,
247 pub attributes: Vec<TransactionAttribute>,
249 pub script: Vec<u8>,
251}
252
253#[derive(Debug, Clone)]
255pub struct TransactionSigner {
256 pub account: [u8; 20],
258 pub scope: WitnessScope,
260 pub allowed_contracts: Vec<[u8; 20]>,
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266#[repr(u8)]
267pub enum WitnessScope {
268 None = 0x00,
270 CalledByEntry = 0x01,
272 CustomContracts = 0x10,
274 Global = 0x80,
276}
277
278#[derive(Debug, Clone)]
280pub struct TransactionAttribute {
281 pub attr_type: u8,
283 pub data: Vec<u8>,
285}
286
287impl NeoTransaction {
288 #[must_use]
290 pub fn new(script: Vec<u8>) -> Self {
291 Self {
292 version: 0,
293 nonce: 0,
294 system_fee: 0,
295 network_fee: 0,
296 valid_until_block: 0,
297 signers: vec![],
298 attributes: vec![],
299 script,
300 }
301 }
302
303 #[must_use]
305 pub fn serialize_unsigned(&self) -> Vec<u8> {
306 let mut buf = Vec::new();
307 buf.push(self.version);
308 buf.extend_from_slice(&self.nonce.to_le_bytes());
309 buf.extend_from_slice(&self.system_fee.to_le_bytes());
310 buf.extend_from_slice(&self.network_fee.to_le_bytes());
311 buf.extend_from_slice(&self.valid_until_block.to_le_bytes());
312
313 write_var_int(&mut buf, self.signers.len() as u64);
315 for signer in &self.signers {
316 buf.extend_from_slice(&signer.account);
317 buf.push(signer.scope as u8);
318 if signer.scope == WitnessScope::CustomContracts {
319 write_var_int(&mut buf, signer.allowed_contracts.len() as u64);
320 for c in &signer.allowed_contracts {
321 buf.extend_from_slice(c);
322 }
323 }
324 }
325
326 write_var_int(&mut buf, self.attributes.len() as u64);
328 for attr in &self.attributes {
329 buf.push(attr.attr_type);
330 write_var_int(&mut buf, attr.data.len() as u64);
331 buf.extend_from_slice(&attr.data);
332 }
333
334 write_var_int(&mut buf, self.script.len() as u64);
336 buf.extend_from_slice(&self.script);
337
338 buf
339 }
340
341 #[must_use]
343 pub fn hash(&self) -> [u8; 32] {
344 use sha2::{Digest, Sha256};
345 let data = self.serialize_unsigned();
346 let mut out = [0u8; 32];
347 out.copy_from_slice(&Sha256::digest(data));
348 out
349 }
350
351 pub fn sign(&self, signer: &super::NeoSigner) -> Result<super::NeoSignature, SignerError> {
353 let hash = self.hash();
354 signer.sign_digest(&hash)
355 }
356}
357
358fn write_var_int(buf: &mut Vec<u8>, val: u64) {
359 if val < 0xFD {
360 buf.push(val as u8);
361 } else if val <= 0xFFFF {
362 buf.push(0xFD);
363 buf.extend_from_slice(&(val as u16).to_le_bytes());
364 } else if val <= 0xFFFF_FFFF {
365 buf.push(0xFE);
366 buf.extend_from_slice(&(val as u32).to_le_bytes());
367 } else {
368 buf.push(0xFF);
369 buf.extend_from_slice(&val.to_le_bytes());
370 }
371}
372
373pub fn read_var_int(data: &[u8]) -> Result<(u64, usize), SignerError> {
381 if data.is_empty() {
382 return Err(SignerError::ParseError("read_var_int: empty".into()));
383 }
384 match data[0] {
385 0..=0xFC => Ok((u64::from(data[0]), 1)),
386 0xFD => {
387 if data.len() < 3 {
388 return Err(SignerError::ParseError(
389 "read_var_int: truncated u16".into(),
390 ));
391 }
392 let val = u16::from_le_bytes([data[1], data[2]]);
393 if val < 0xFD {
394 return Err(SignerError::ParseError(
395 "read_var_int: non-canonical u16 encoding".into(),
396 ));
397 }
398 Ok((u64::from(val), 3))
399 }
400 0xFE => {
401 if data.len() < 5 {
402 return Err(SignerError::ParseError(
403 "read_var_int: truncated u32".into(),
404 ));
405 }
406 let val = u32::from_le_bytes([data[1], data[2], data[3], data[4]]);
407 if val <= 0xFFFF {
408 return Err(SignerError::ParseError(
409 "read_var_int: non-canonical u32 encoding".into(),
410 ));
411 }
412 Ok((u64::from(val), 5))
413 }
414 0xFF => {
415 if data.len() < 9 {
416 return Err(SignerError::ParseError(
417 "read_var_int: truncated u64".into(),
418 ));
419 }
420 let val = u64::from_le_bytes([
421 data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8],
422 ]);
423 if val <= 0xFFFF_FFFF {
424 return Err(SignerError::ParseError(
425 "read_var_int: non-canonical u64 encoding".into(),
426 ));
427 }
428 Ok((val, 9))
429 }
430 }
431}
432
433impl NeoTransaction {
434 pub fn deserialize(data: &[u8]) -> Result<Self, SignerError> {
438 if data.len() < 25 {
439 return Err(SignerError::ParseError("neo tx: too short".into()));
440 }
441 let mut pos = 0;
442
443 let version = data[pos];
444 pos += 1;
445
446 let nonce = u32::from_le_bytes(
447 data[pos..pos + 4]
448 .try_into()
449 .map_err(|_| SignerError::ParseError("neo tx: nonce".into()))?,
450 );
451 pos += 4;
452
453 let system_fee = i64::from_le_bytes(
454 data[pos..pos + 8]
455 .try_into()
456 .map_err(|_| SignerError::ParseError("neo tx: system_fee".into()))?,
457 );
458 pos += 8;
459
460 let network_fee = i64::from_le_bytes(
461 data[pos..pos + 8]
462 .try_into()
463 .map_err(|_| SignerError::ParseError("neo tx: network_fee".into()))?,
464 );
465 pos += 8;
466
467 let valid_until_block = u32::from_le_bytes(
468 data[pos..pos + 4]
469 .try_into()
470 .map_err(|_| SignerError::ParseError("neo tx: valid_until".into()))?,
471 );
472 pos += 4;
473
474 let (num_signers, consumed) = read_var_int(&data[pos..])?;
476 pos += consumed;
477 let mut signers = Vec::new();
478 for _ in 0..num_signers {
479 if pos + 21 > data.len() {
480 return Err(SignerError::ParseError("neo tx: truncated signer".into()));
481 }
482 let mut account = [0u8; 20];
483 account.copy_from_slice(&data[pos..pos + 20]);
484 pos += 20;
485 let scope_byte = data[pos];
486 pos += 1;
487 let scope = match scope_byte {
488 0x00 => WitnessScope::None,
489 0x01 => WitnessScope::CalledByEntry,
490 0x10 => WitnessScope::CustomContracts,
491 0x80 => WitnessScope::Global,
492 _ => {
493 return Err(SignerError::ParseError(format!(
494 "neo tx: unknown witness scope 0x{scope_byte:02x}"
495 )))
496 }
497 };
498 let mut allowed_contracts = Vec::new();
499 if scope == WitnessScope::CustomContracts {
500 let (num_contracts, c) = read_var_int(&data[pos..])?;
501 pos += c;
502 for _ in 0..num_contracts {
503 if pos + 20 > data.len() {
504 return Err(SignerError::ParseError(
505 "neo tx: truncated allowed contract".into(),
506 ));
507 }
508 let mut contract = [0u8; 20];
509 contract.copy_from_slice(&data[pos..pos + 20]);
510 pos += 20;
511 allowed_contracts.push(contract);
512 }
513 }
514 signers.push(TransactionSigner {
515 account,
516 scope,
517 allowed_contracts,
518 });
519 }
520
521 let (num_attrs, consumed) = read_var_int(&data[pos..])?;
523 pos += consumed;
524 let mut attributes = Vec::new();
525 for _ in 0..num_attrs {
526 if pos >= data.len() {
527 return Err(SignerError::ParseError("neo tx: truncated attr".into()));
528 }
529 let attr_type = data[pos];
530 pos += 1;
531 let (attr_len, c) = read_var_int(&data[pos..])?;
532 pos += c;
533 let attr_len_usize = usize::try_from(attr_len)
534 .map_err(|_| SignerError::ParseError("neo tx: attr length exceeds usize".into()))?;
535 let attr_end = pos
536 .checked_add(attr_len_usize)
537 .ok_or_else(|| SignerError::ParseError("neo tx: attr length overflow".into()))?;
538 if attr_end > data.len() {
539 return Err(SignerError::ParseError(
540 "neo tx: truncated attr data".into(),
541 ));
542 }
543 let attr_data = data[pos..attr_end].to_vec();
544 pos = attr_end;
545 attributes.push(TransactionAttribute {
546 attr_type,
547 data: attr_data,
548 });
549 }
550
551 let (script_len, consumed) = read_var_int(&data[pos..])?;
553 pos += consumed;
554 let script_len_usize = usize::try_from(script_len)
555 .map_err(|_| SignerError::ParseError("neo tx: script length exceeds usize".into()))?;
556 let script_end = pos
557 .checked_add(script_len_usize)
558 .ok_or_else(|| SignerError::ParseError("neo tx: script length overflow".into()))?;
559 if script_end > data.len() {
560 return Err(SignerError::ParseError("neo tx: truncated script".into()));
561 }
562 let script = data[pos..script_end].to_vec();
563 pos = script_end;
564
565 if pos != data.len() {
567 return Err(SignerError::ParseError(format!(
568 "neo tx: {} trailing bytes",
569 data.len() - pos
570 )));
571 }
572
573 Ok(Self {
574 version,
575 nonce,
576 system_fee,
577 network_fee,
578 valid_until_block,
579 signers,
580 attributes,
581 script,
582 })
583 }
584}
585
586pub fn nep17_approve(
595 token_hash: &[u8; 20],
596 owner: &[u8; 20],
597 spender: &[u8; 20],
598 amount: i64,
599) -> Vec<u8> {
600 let mut sb = ScriptBuilder::new();
601 sb.emit_push_integer(amount)
602 .emit_push_hash160(spender)
603 .emit_push_hash160(owner)
604 .emit_contract_call(token_hash, "approve", 3);
605 sb.to_bytes()
606}
607
608pub fn nep17_allowance(token_hash: &[u8; 20], owner: &[u8; 20], spender: &[u8; 20]) -> Vec<u8> {
612 let mut sb = ScriptBuilder::new();
613 sb.emit_push_hash160(spender)
614 .emit_push_hash160(owner)
615 .emit_contract_call(token_hash, "allowance", 2);
616 sb.to_bytes()
617}
618
619pub fn nep17_transfer_from(
623 token_hash: &[u8; 20],
624 spender: &[u8; 20],
625 from: &[u8; 20],
626 to: &[u8; 20],
627 amount: i64,
628) -> Vec<u8> {
629 let mut sb = ScriptBuilder::new();
630 sb.emit_push_integer(amount)
631 .emit_push_hash160(to)
632 .emit_push_hash160(from)
633 .emit_push_hash160(spender)
634 .emit_contract_call(token_hash, "transferFrom", 4);
635 sb.to_bytes()
636}
637
638pub const CONTRACT_MANAGEMENT_HASH: [u8; 20] = [
644 0xfd, 0xa3, 0xfa, 0x43, 0x34, 0x6b, 0x9d, 0x6b, 0x51, 0xd3, 0x3c, 0x64, 0xb2, 0x1c, 0x68, 0x24,
645 0x38, 0x97, 0x28, 0xe6,
646];
647
648pub fn contract_deploy(nef_bytes: &[u8], manifest_json: &str) -> Vec<u8> {
654 let mut sb = ScriptBuilder::new();
655 sb.emit_push_bytes(manifest_json.as_bytes())
656 .emit_push_bytes(nef_bytes)
657 .emit_contract_call(&CONTRACT_MANAGEMENT_HASH, "deploy", 2);
658 sb.to_bytes()
659}
660
661pub fn contract_update(nef_bytes: &[u8], manifest_json: &str) -> Vec<u8> {
663 let mut sb = ScriptBuilder::new();
664 sb.emit_push_bytes(manifest_json.as_bytes())
665 .emit_push_bytes(nef_bytes)
666 .emit_contract_call(&CONTRACT_MANAGEMENT_HASH, "update", 2);
667 sb.to_bytes()
668}
669
670pub fn contract_destroy() -> Vec<u8> {
672 let mut sb = ScriptBuilder::new();
673 sb.emit_contract_call(&CONTRACT_MANAGEMENT_HASH, "destroy", 0);
674 sb.to_bytes()
675}
676
677pub fn neo_vote(voter: &[u8; 20], candidate_pubkey: Option<&[u8; 33]>) -> Vec<u8> {
689 let mut sb = ScriptBuilder::new();
690 match candidate_pubkey {
691 Some(pk) => sb.emit_push_bytes(pk),
692 None => sb.emit(opcode::PUSH0), };
694 sb.emit_push_hash160(voter)
695 .emit_contract_call(&contracts::NEO_TOKEN, "vote", 2);
696 sb.to_bytes()
697}
698
699pub fn neo_unclaimed_gas(account: &[u8; 20], end_height: u32) -> Vec<u8> {
701 let mut sb = ScriptBuilder::new();
702 sb.emit_push_integer(i64::from(end_height))
703 .emit_push_hash160(account)
704 .emit_contract_call(&contracts::NEO_TOKEN, "unclaimedGas", 2);
705 sb.to_bytes()
706}
707
708pub fn neo_register_candidate(pubkey: &[u8; 33]) -> Vec<u8> {
710 let mut sb = ScriptBuilder::new();
711 sb.emit_push_bytes(pubkey)
712 .emit_contract_call(&contracts::NEO_TOKEN, "registerCandidate", 1);
713 sb.to_bytes()
714}
715
716pub fn neo_get_candidates() -> Vec<u8> {
718 let mut sb = ScriptBuilder::new();
719 sb.emit_contract_call(&contracts::NEO_TOKEN, "getCandidates", 0);
720 sb.to_bytes()
721}
722
723pub fn neo_get_committee() -> Vec<u8> {
725 let mut sb = ScriptBuilder::new();
726 sb.emit_contract_call(&contracts::NEO_TOKEN, "getCommittee", 0);
727 sb.to_bytes()
728}
729
730#[cfg(test)]
735#[allow(clippy::unwrap_used, clippy::expect_used)]
736mod tests {
737 use super::*;
738 use crate::traits::KeyPair;
739
740 #[test]
743 fn test_script_builder_push_integer() {
744 let mut sb = ScriptBuilder::new();
745 sb.emit_push_integer(0);
746 assert_eq!(sb.to_bytes(), vec![opcode::PUSH0]);
747 }
748
749 #[test]
750 fn test_script_builder_push_integer_range() {
751 for i in 1..=16 {
752 let mut sb = ScriptBuilder::new();
753 sb.emit_push_integer(i);
754 let bytes = sb.to_bytes();
755 assert_eq!(bytes.len(), 1);
756 assert_eq!(bytes[0], opcode::PUSH1 + (i as u8 - 1));
757 }
758 }
759
760 #[test]
761 fn test_script_builder_push_bytes() {
762 let mut sb = ScriptBuilder::new();
763 sb.emit_push_bytes(b"hello");
764 let bytes = sb.to_bytes();
765 assert_eq!(bytes[0], opcode::PUSHDATA1);
766 assert_eq!(bytes[1], 5);
767 assert_eq!(&bytes[2..], b"hello");
768 }
769
770 #[test]
771 fn test_script_builder_syscall() {
772 let mut sb = ScriptBuilder::new();
773 sb.emit_syscall(0x627d5b52);
774 let bytes = sb.to_bytes();
775 assert_eq!(bytes[0], opcode::SYSCALL);
776 assert_eq!(&bytes[1..5], &0x627d5b52u32.to_le_bytes());
777 }
778
779 #[test]
782 fn test_nep17_transfer_script() {
783 let from = [0xAA; 20];
784 let to = [0xBB; 20];
785 let script = nep17_transfer(&contracts::NEO_TOKEN, &from, &to, 10);
786 assert!(!script.is_empty());
787 let s = String::from_utf8_lossy(&script);
789 assert!(s.contains("transfer"));
790 }
791
792 #[test]
793 fn test_nep17_balance_of_script() {
794 let account = [0xCC; 20];
795 let script = nep17_balance_of(&contracts::GAS_TOKEN, &account);
796 assert!(!script.is_empty());
797 let s = String::from_utf8_lossy(&script);
798 assert!(s.contains("balanceOf"));
799 }
800
801 #[test]
802 fn test_nep17_symbol() {
803 let script = nep17_symbol(&contracts::NEO_TOKEN);
804 assert!(!script.is_empty());
805 let s = String::from_utf8_lossy(&script);
806 assert!(s.contains("symbol"));
807 }
808
809 #[test]
810 fn test_nep17_decimals() {
811 let script = nep17_decimals(&contracts::GAS_TOKEN);
812 let s = String::from_utf8_lossy(&script);
813 assert!(s.contains("decimals"));
814 }
815
816 #[test]
817 fn test_nep17_total_supply() {
818 let script = nep17_total_supply(&contracts::NEO_TOKEN);
819 let s = String::from_utf8_lossy(&script);
820 assert!(s.contains("totalSupply"));
821 }
822
823 #[test]
826 fn test_neo_transaction_serialization() {
827 let script = nep17_transfer(&contracts::NEO_TOKEN, &[0xAA; 20], &[0xBB; 20], 1);
828 let tx = NeoTransaction {
829 version: 0,
830 nonce: 12345,
831 system_fee: 100_000,
832 network_fee: 50_000,
833 valid_until_block: 1000,
834 signers: vec![TransactionSigner {
835 account: [0xAA; 20],
836 scope: WitnessScope::CalledByEntry,
837 allowed_contracts: vec![],
838 }],
839 attributes: vec![],
840 script,
841 };
842 let serialized = tx.serialize_unsigned();
843 assert!(!serialized.is_empty());
844 assert_eq!(serialized[0], 0); }
846
847 #[test]
848 fn test_neo_transaction_hash_deterministic() {
849 let script = nep17_transfer(&contracts::GAS_TOKEN, &[0xAA; 20], &[0xBB; 20], 100);
850 let tx = NeoTransaction::new(script);
851 assert_eq!(tx.hash(), tx.hash());
852 }
853
854 #[test]
855 fn test_neo_transaction_sign() {
856 let signer = super::super::NeoSigner::generate().unwrap();
857 let script_hash = signer.script_hash();
858 let script = nep17_transfer(&contracts::NEO_TOKEN, &script_hash, &[0xBB; 20], 1);
859 let tx = NeoTransaction::new(script);
860 let sig = tx.sign(&signer).unwrap();
861 assert_eq!(sig.to_bytes().len(), 64);
862 }
863
864 #[test]
865 fn test_neo_transaction_different_nonce_different_hash() {
866 let script = vec![0x00];
867 let mut tx1 = NeoTransaction::new(script.clone());
868 tx1.nonce = 1;
869 let mut tx2 = NeoTransaction::new(script);
870 tx2.nonce = 2;
871 assert_ne!(tx1.hash(), tx2.hash());
872 }
873
874 #[test]
877 fn test_neo_tx_serialize_deserialize_roundtrip() {
878 let script = nep17_transfer(&contracts::NEO_TOKEN, &[0xAA; 20], &[0xBB; 20], 10);
879 let tx = NeoTransaction {
880 version: 0,
881 nonce: 42,
882 system_fee: 100_000,
883 network_fee: 50_000,
884 valid_until_block: 999,
885 signers: vec![TransactionSigner {
886 account: [0xAA; 20],
887 scope: WitnessScope::CalledByEntry,
888 allowed_contracts: vec![],
889 }],
890 attributes: vec![],
891 script,
892 };
893 let bytes = tx.serialize_unsigned();
894 let restored = NeoTransaction::deserialize(&bytes).unwrap();
895 assert_eq!(restored.version, tx.version);
896 assert_eq!(restored.nonce, tx.nonce);
897 assert_eq!(restored.system_fee, tx.system_fee);
898 assert_eq!(restored.network_fee, tx.network_fee);
899 assert_eq!(restored.valid_until_block, tx.valid_until_block);
900 assert_eq!(restored.signers.len(), 1);
901 assert_eq!(restored.signers[0].account, [0xAA; 20]);
902 assert_eq!(restored.script, tx.script);
903 }
904
905 #[test]
906 fn test_neo_tx_deserialize_empty_fails() {
907 assert!(NeoTransaction::deserialize(&[]).is_err());
908 assert!(NeoTransaction::deserialize(&[0u8; 10]).is_err());
909 }
910
911 #[test]
912 fn test_read_var_int() {
913 assert_eq!(read_var_int(&[0x00]).unwrap(), (0, 1));
914 assert_eq!(read_var_int(&[0xFC]).unwrap(), (252, 1));
915 assert_eq!(read_var_int(&[0xFD, 0xFD, 0x00]).unwrap(), (253, 3));
916 assert_eq!(
917 read_var_int(&[0xFE, 0x00, 0x00, 0x01, 0x00]).unwrap(),
918 (65_536, 5)
919 );
920 assert!(read_var_int(&[0xFD, 0x01, 0x00]).is_err()); assert!(read_var_int(&[0xFE, 0x01, 0x00, 0x00, 0x00]).is_err()); assert!(read_var_int(&[0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]).is_err());
923 }
924
925 #[test]
928 fn test_nep17_approve_script() {
929 let script = nep17_approve(&contracts::GAS_TOKEN, &[0xAA; 20], &[0xBB; 20], 1000);
930 assert!(!script.is_empty());
931 let s = String::from_utf8_lossy(&script);
932 assert!(s.contains("approve"));
933 }
934
935 #[test]
936 fn test_nep17_allowance_script() {
937 let script = nep17_allowance(&contracts::GAS_TOKEN, &[0xAA; 20], &[0xBB; 20]);
938 assert!(!script.is_empty());
939 let s = String::from_utf8_lossy(&script);
940 assert!(s.contains("allowance"));
941 }
942
943 #[test]
944 fn test_nep17_transfer_from_script() {
945 let script = nep17_transfer_from(
946 &contracts::GAS_TOKEN,
947 &[0xAA; 20],
948 &[0xBB; 20],
949 &[0xCC; 20],
950 500,
951 );
952 let s = String::from_utf8_lossy(&script);
953 assert!(s.contains("transferFrom"));
954 }
955
956 #[test]
959 fn test_contract_deploy_script() {
960 let nef = b"\x4e\x45\x46\x33"; let manifest = r#"{"name":"test"}"#;
962 let script = contract_deploy(nef, manifest);
963 assert!(!script.is_empty());
964 let s = String::from_utf8_lossy(&script);
965 assert!(s.contains("deploy"));
966 }
967
968 #[test]
969 fn test_contract_update_script() {
970 let script = contract_update(b"\x00", r#"{}"#);
971 let s = String::from_utf8_lossy(&script);
972 assert!(s.contains("update"));
973 }
974
975 #[test]
976 fn test_contract_destroy_script() {
977 let script = contract_destroy();
978 let s = String::from_utf8_lossy(&script);
979 assert!(s.contains("destroy"));
980 }
981
982 #[test]
985 fn test_neo_vote_script() {
986 let voter = [0xAA; 20];
987 let pubkey = [0x02; 33];
988 let script = neo_vote(&voter, Some(&pubkey));
989 let s = String::from_utf8_lossy(&script);
990 assert!(s.contains("vote"));
991 }
992
993 #[test]
994 fn test_neo_vote_cancel() {
995 let voter = [0xAA; 20];
996 let script = neo_vote(&voter, None);
997 let s = String::from_utf8_lossy(&script);
998 assert!(s.contains("vote"));
999 }
1000
1001 #[test]
1002 fn test_neo_unclaimed_gas() {
1003 let script = neo_unclaimed_gas(&[0xAA; 20], 100_000);
1004 let s = String::from_utf8_lossy(&script);
1005 assert!(s.contains("unclaimedGas"));
1006 }
1007
1008 #[test]
1009 fn test_neo_register_candidate() {
1010 let script = neo_register_candidate(&[0x02; 33]);
1011 let s = String::from_utf8_lossy(&script);
1012 assert!(s.contains("registerCandidate"));
1013 }
1014
1015 #[test]
1016 fn test_neo_get_candidates() {
1017 let script = neo_get_candidates();
1018 let s = String::from_utf8_lossy(&script);
1019 assert!(s.contains("getCandidates"));
1020 }
1021
1022 #[test]
1023 fn test_neo_get_committee() {
1024 let script = neo_get_committee();
1025 let s = String::from_utf8_lossy(&script);
1026 assert!(s.contains("getCommittee"));
1027 }
1028}