Skip to main content

accumulate_client/codec/
signing.rs

1//! Transaction signing utilities for Accumulate protocol
2//!
3//! This module implements proper binary encoding for transaction signing
4//! matching the Go core and Dart SDK implementations.
5
6use super::writer::BinaryWriter;
7use sha2::{Digest, Sha256};
8
9/// Signature type enum values matching Go core
10pub mod signature_types {
11    pub const UNKNOWN: u64 = 0;
12    pub const LEGACY_ED25519: u64 = 1;
13    pub const ED25519: u64 = 2;
14    pub const RCD1: u64 = 3;
15    pub const BTC: u64 = 4;
16    pub const BTC_LEGACY: u64 = 5;
17    pub const ETH: u64 = 6;
18    pub const DELEGATED: u64 = 7;
19    pub const INTERNAL: u64 = 8;
20    pub const RSA_SHA256: u64 = 9;
21    pub const ECDSA_SHA256: u64 = 10;
22    pub const TYPED_DATA: u64 = 11;
23    pub const REMOTE: u64 = 12;
24    pub const RECEIPT: u64 = 13;
25    pub const PARTITION: u64 = 14;
26    pub const SET: u64 = 15;
27    pub const AUTHORITY: u64 = 16;
28}
29
30/// Transaction type enum values matching Go core
31pub mod tx_types {
32    pub const CREATE_IDENTITY: u64 = 0x01;
33    pub const CREATE_TOKEN_ACCOUNT: u64 = 0x02;
34    pub const SEND_TOKENS: u64 = 0x03;
35    pub const CREATE_DATA_ACCOUNT: u64 = 0x04;
36    pub const WRITE_DATA: u64 = 0x05;
37    pub const WRITE_DATA_TO: u64 = 0x06;
38    pub const ACME_FAUCET: u64 = 0x07;
39    pub const CREATE_TOKEN: u64 = 0x08;
40    pub const ISSUE_TOKENS: u64 = 0x09;
41    pub const BURN_TOKENS: u64 = 0x0A;
42    pub const CREATE_LITE_TOKEN_ACCOUNT: u64 = 0x0B;
43    pub const CREATE_KEY_PAGE: u64 = 0x0C;
44    pub const CREATE_KEY_BOOK: u64 = 0x0D;
45    pub const ADD_CREDITS: u64 = 0x0E;
46    pub const UPDATE_KEY_PAGE: u64 = 0x0F;
47    pub const LOCK_ACCOUNT: u64 = 0x10;
48    pub const BURN_CREDITS: u64 = 0x11;
49    pub const TRANSFER_CREDITS: u64 = 0x12;
50    pub const UPDATE_ACCOUNT_AUTH: u64 = 0x15;
51    pub const UPDATE_KEY: u64 = 0x16;
52}
53
54/// KeyPageOperation type enum values matching Go core
55/// From protocol/key_page_operations.yml
56pub mod key_page_op_types {
57    pub const UNKNOWN: u64 = 0;
58    pub const UPDATE: u64 = 1;     // UpdateKeyOperation
59    pub const REMOVE: u64 = 2;     // RemoveKeyOperation
60    pub const ADD: u64 = 3;        // AddKeyOperation
61    pub const SET_THRESHOLD: u64 = 4;  // SetThresholdKeyPageOperation
62    pub const UPDATE_ALLOWED: u64 = 5; // UpdateAllowedKeyPageOperation
63    pub const SET_REJECT_THRESHOLD: u64 = 6;   // SetRejectThresholdKeyPageOperation
64    pub const SET_RESPONSE_THRESHOLD: u64 = 7; // SetResponseThresholdKeyPageOperation
65}
66
67/// Compute signature metadata hash for ED25519 signatures
68///
69/// This computes the SHA256 of the binary-encoded signature metadata,
70/// which is used as both the transaction initiator AND part of the signing preimage.
71///
72/// Field order matches Go: protocol/types_gen.go ED25519Signature.MarshalBinary
73/// - Field 1: Type (enum = 2 for ED25519)
74/// - Field 2: PublicKey (bytes)
75/// - Field 3: Signature (bytes) - OMITTED for metadata
76/// - Field 4: Signer (URL as string)
77/// - Field 5: SignerVersion (uint)
78/// - Field 6: Timestamp (uint)
79/// - Field 7: Vote (enum)
80/// - Field 8: TransactionHash (hash) - OMITTED for metadata (zeros)
81/// - Field 9: Memo (string)
82/// - Field 10: Data (bytes)
83pub fn compute_ed25519_signature_metadata_hash(
84    public_key: &[u8],
85    signer: &str,
86    signer_version: u64,
87    timestamp: u64,
88) -> [u8; 32] {
89    compute_signature_metadata_hash(
90        signature_types::ED25519,
91        public_key,
92        signer,
93        signer_version,
94        timestamp,
95        0, // vote
96        None, // memo
97        None, // data
98    )
99}
100
101/// Compute signature metadata hash for any signature type
102pub fn compute_signature_metadata_hash(
103    signature_type: u64,
104    public_key: &[u8],
105    signer: &str,
106    signer_version: u64,
107    timestamp: u64,
108    vote: u64,
109    memo: Option<&str>,
110    data: Option<&[u8]>,
111) -> [u8; 32] {
112    let mut writer = BinaryWriter::new();
113
114    // Field 1: Type (enum)
115    let _ = writer.write_uvarint(1);
116    let _ = writer.write_uvarint(signature_type);
117
118    // Field 2: PublicKey (bytes with length prefix)
119    if !public_key.is_empty() {
120        let _ = writer.write_uvarint(2);
121        let _ = writer.write_uvarint(public_key.len() as u64);
122        let _ = writer.write_bytes(public_key);
123    }
124
125    // Field 3: Signature - OMITTED for metadata
126
127    // Field 4: Signer URL (string with length prefix)
128    if !signer.is_empty() {
129        let _ = writer.write_uvarint(4);
130        let signer_bytes = signer.as_bytes();
131        let _ = writer.write_uvarint(signer_bytes.len() as u64);
132        let _ = writer.write_bytes(signer_bytes);
133    }
134
135    // Field 5: SignerVersion (uint)
136    if signer_version != 0 {
137        let _ = writer.write_uvarint(5);
138        let _ = writer.write_uvarint(signer_version);
139    }
140
141    // Field 6: Timestamp (uint)
142    if timestamp != 0 {
143        let _ = writer.write_uvarint(6);
144        let _ = writer.write_uvarint(timestamp);
145    }
146
147    // Field 7: Vote (enum)
148    if vote != 0 {
149        let _ = writer.write_uvarint(7);
150        let _ = writer.write_uvarint(vote);
151    }
152
153    // Field 8: TransactionHash - OMITTED for metadata (zeros)
154
155    // Field 9: Memo (string)
156    if let Some(memo_str) = memo {
157        if !memo_str.is_empty() {
158            let _ = writer.write_uvarint(9);
159            let memo_bytes = memo_str.as_bytes();
160            let _ = writer.write_uvarint(memo_bytes.len() as u64);
161            let _ = writer.write_bytes(memo_bytes);
162        }
163    }
164
165    // Field 10: Data (bytes)
166    if let Some(data_bytes) = data {
167        if !data_bytes.is_empty() {
168            let _ = writer.write_uvarint(10);
169            let _ = writer.write_uvarint(data_bytes.len() as u64);
170            let _ = writer.write_bytes(data_bytes);
171        }
172    }
173
174    // Hash the encoded metadata
175    sha256_bytes(writer.bytes())
176}
177
178/// Options for extended transaction header fields (fields 5-7).
179#[derive(Debug, Clone, Default)]
180pub struct HeaderBinaryOptions {
181    /// Expire: at_time as Unix seconds, signed (field 5)
182    pub expire_at_time: Option<i64>,
183    /// HoldUntil: minor_block number (field 6)
184    pub hold_until_minor_block: Option<u64>,
185    /// Additional authority URLs (field 7, repeatable)
186    pub authorities: Option<Vec<String>>,
187}
188
189/// Marshal transaction header to binary format
190///
191/// Field order matches Go: protocol/types_gen.go TransactionHeader.MarshalBinary
192/// - Field 1: Principal (URL as string)
193/// - Field 2: Initiator (hash, 32 bytes, no length prefix)
194/// - Field 3: Memo (string)
195/// - Field 4: Metadata (bytes)
196/// - Field 5: Expire (nested: ExpireOptions with field 1 = atTime uint)
197/// - Field 6: HoldUntil (nested: HoldUntilOptions with field 1 = minorBlock uint)
198/// - Field 7: Authorities (repeatable URL strings)
199pub fn marshal_transaction_header(
200    principal: &str,
201    initiator: &[u8; 32],
202    memo: Option<&str>,
203    metadata: Option<&[u8]>,
204) -> Vec<u8> {
205    marshal_transaction_header_full(principal, initiator, memo, metadata, None)
206}
207
208/// Marshal transaction header with all fields including extended options (fields 5-7).
209pub fn marshal_transaction_header_full(
210    principal: &str,
211    initiator: &[u8; 32],
212    memo: Option<&str>,
213    metadata: Option<&[u8]>,
214    extended: Option<&HeaderBinaryOptions>,
215) -> Vec<u8> {
216    let mut writer = BinaryWriter::new();
217
218    // Field 1: Principal URL
219    let _ = writer.write_uvarint(1);
220    let principal_bytes = principal.as_bytes();
221    let _ = writer.write_uvarint(principal_bytes.len() as u64);
222    let _ = writer.write_bytes(principal_bytes);
223
224    // Field 2: Initiator (32 byte hash, no length prefix)
225    // Only write if not all zeros
226    let is_zero = initiator.iter().all(|&b| b == 0);
227    if !is_zero {
228        let _ = writer.write_uvarint(2);
229        let _ = writer.write_bytes(initiator);
230    }
231
232    // Field 3: Memo
233    if let Some(memo_str) = memo {
234        if !memo_str.is_empty() {
235            let _ = writer.write_uvarint(3);
236            let memo_bytes = memo_str.as_bytes();
237            let _ = writer.write_uvarint(memo_bytes.len() as u64);
238            let _ = writer.write_bytes(memo_bytes);
239        }
240    }
241
242    // Field 4: Metadata
243    if let Some(metadata_bytes) = metadata {
244        if !metadata_bytes.is_empty() {
245            let _ = writer.write_uvarint(4);
246            let _ = writer.write_uvarint(metadata_bytes.len() as u64);
247            let _ = writer.write_bytes(metadata_bytes);
248        }
249    }
250
251    // Fields 5-7: Extended header options
252    if let Some(ext) = extended {
253        // Field 5: Expire (nested ExpireOptions)
254        // Go: writer.WriteValue(5, v.Expire.MarshalBinary)
255        // ExpireOptions.MarshalBinary: writer.WriteTime(1, *v.AtTime)
256        // WriteTime: writeField(n) then writeInt(n, v.UTC().Unix())
257        // writeInt uses binary.PutVarint (signed/zigzag varint)
258        if let Some(at_time) = ext.expire_at_time {
259            let mut expire_writer = BinaryWriter::new();
260            let _ = expire_writer.write_uvarint(1); // ExpireOptions field 1: AtTime
261            let _ = expire_writer.write_varint(at_time); // signed varint (zigzag)
262            let expire_bytes = expire_writer.into_bytes();
263
264            let _ = writer.write_uvarint(5);
265            let _ = writer.write_uvarint(expire_bytes.len() as u64);
266            let _ = writer.write_bytes(&expire_bytes);
267        }
268
269        // Field 6: HoldUntil (nested HoldUntilOptions)
270        if let Some(minor_block) = ext.hold_until_minor_block {
271            let mut hold_writer = BinaryWriter::new();
272            let _ = hold_writer.write_uvarint(1); // HoldUntilOptions field 1: MinorBlock
273            let _ = hold_writer.write_uvarint(minor_block);
274            let hold_bytes = hold_writer.into_bytes();
275
276            let _ = writer.write_uvarint(6);
277            let _ = writer.write_uvarint(hold_bytes.len() as u64);
278            let _ = writer.write_bytes(&hold_bytes);
279        }
280
281        // Field 7: Authorities (repeatable URL)
282        if let Some(ref authorities) = ext.authorities {
283            for auth_url in authorities {
284                let _ = writer.write_uvarint(7);
285                let auth_bytes = auth_url.as_bytes();
286                let _ = writer.write_uvarint(auth_bytes.len() as u64);
287                let _ = writer.write_bytes(auth_bytes);
288            }
289        }
290    }
291
292    writer.into_bytes()
293}
294
295/// Marshal AddCredits transaction body to binary format
296///
297/// Field order matches Go: protocol/types_gen.go AddCredits.MarshalBinary
298/// - Field 1: Type (enum)
299/// - Field 2: Recipient (URL as string)
300/// - Field 3: Amount (BigInt)
301/// - Field 4: Oracle (uint)
302pub fn marshal_add_credits_body(
303    recipient: &str,
304    amount: u64,
305    oracle: u64,
306) -> Vec<u8> {
307    let mut writer = BinaryWriter::new();
308
309    // Field 1: Type (AddCredits = 0x0E)
310    let _ = writer.write_uvarint(1);
311    let _ = writer.write_uvarint(tx_types::ADD_CREDITS);
312
313    // Field 2: Recipient URL
314    let _ = writer.write_uvarint(2);
315    let recipient_bytes = recipient.as_bytes();
316    let _ = writer.write_uvarint(recipient_bytes.len() as u64);
317    let _ = writer.write_bytes(recipient_bytes);
318
319    // Field 3: Amount (BigInt - encode as length-prefixed big-endian bytes)
320    if amount > 0 {
321        let _ = writer.write_uvarint(3);
322        let amount_bytes = amount_to_bigint_bytes(amount);
323        let _ = writer.write_uvarint(amount_bytes.len() as u64);
324        let _ = writer.write_bytes(&amount_bytes);
325    }
326
327    // Field 4: Oracle
328    if oracle > 0 {
329        let _ = writer.write_uvarint(4);
330        let _ = writer.write_uvarint(oracle);
331    }
332
333    writer.into_bytes()
334}
335
336/// Marshal SendTokens transaction body to binary format
337///
338/// Field order matches Go: protocol/types_gen.go SendTokens.MarshalBinary
339/// - Field 1: Type (enum)
340/// - Field 2: Hash (optional) - skipped
341/// - Field 3: Meta (optional) - skipped
342/// - Field 4: To (repeated TokenRecipient)
343pub fn marshal_send_tokens_body(
344    recipients: &[(String, u64)], // (url, amount)
345) -> Vec<u8> {
346    let mut writer = BinaryWriter::new();
347
348    // Field 1: Type (SendTokens = 0x03)
349    let _ = writer.write_uvarint(1);
350    let _ = writer.write_uvarint(tx_types::SEND_TOKENS);
351
352    // Field 4: To (repeated TokenRecipient)
353    for (url, amount) in recipients {
354        let recipient_bytes = marshal_token_recipient(url, *amount);
355        let _ = writer.write_uvarint(4);
356        let _ = writer.write_uvarint(recipient_bytes.len() as u64);
357        let _ = writer.write_bytes(&recipient_bytes);
358    }
359
360    writer.into_bytes()
361}
362
363/// Marshal CreateIdentity transaction body to binary format
364///
365/// Field order matches Go: protocol/types_gen.go CreateIdentity.MarshalBinary
366/// - Field 1: Type (enum)
367/// - Field 2: Url (URL as string)
368/// - Field 3: KeyHash (bytes)
369/// - Field 4: KeyBookUrl (URL as string)
370pub fn marshal_create_identity_body(
371    url: &str,
372    key_hash: &[u8],
373    key_book_url: &str,
374) -> Vec<u8> {
375    let mut writer = BinaryWriter::new();
376
377    // Field 1: Type (CreateIdentity = 0x01)
378    let _ = writer.write_uvarint(1);
379    let _ = writer.write_uvarint(tx_types::CREATE_IDENTITY);
380
381    // Field 2: Url
382    let _ = writer.write_uvarint(2);
383    let url_bytes = url.as_bytes();
384    let _ = writer.write_uvarint(url_bytes.len() as u64);
385    let _ = writer.write_bytes(url_bytes);
386
387    // Field 3: KeyHash (bytes with length prefix)
388    if !key_hash.is_empty() {
389        let _ = writer.write_uvarint(3);
390        let _ = writer.write_uvarint(key_hash.len() as u64);
391        let _ = writer.write_bytes(key_hash);
392    }
393
394    // Field 4: KeyBookUrl
395    let _ = writer.write_uvarint(4);
396    let book_bytes = key_book_url.as_bytes();
397    let _ = writer.write_uvarint(book_bytes.len() as u64);
398    let _ = writer.write_bytes(book_bytes);
399
400    writer.into_bytes()
401}
402
403/// Marshal CreateDataAccount transaction body to binary format
404///
405/// Field order matches Go: protocol/types_gen.go CreateDataAccount.MarshalBinary
406/// - Field 1: Type (enum)
407/// - Field 2: Url (URL as string)
408/// - Field 3: Authorities (repeated URLs)
409pub fn marshal_create_data_account_body(url: &str) -> Vec<u8> {
410    let mut writer = BinaryWriter::new();
411
412    // Field 1: Type (CreateDataAccount = 0x04)
413    let _ = writer.write_uvarint(1);
414    let _ = writer.write_uvarint(tx_types::CREATE_DATA_ACCOUNT);
415
416    // Field 2: Url
417    if !url.is_empty() {
418        let _ = writer.write_uvarint(2);
419        let url_bytes = url.as_bytes();
420        let _ = writer.write_uvarint(url_bytes.len() as u64);
421        let _ = writer.write_bytes(url_bytes);
422    }
423
424    writer.into_bytes()
425}
426
427/// Marshal WriteData transaction body to binary format
428///
429/// Field order matches Go: protocol/types_gen.go WriteData.MarshalBinary
430/// - Field 1: Type (enum)
431/// - Field 2: Entry (nested DataEntry)
432/// - Field 3: Scratch (bool, optional)
433/// - Field 4: WriteToState (bool, optional)
434pub fn marshal_write_data_body(entries_hex: &[String], scratch: bool, write_to_state: bool) -> Vec<u8> {
435    let mut writer = BinaryWriter::new();
436
437    // Field 1: Type (WriteData = 0x05)
438    let _ = writer.write_uvarint(1);
439    let _ = writer.write_uvarint(tx_types::WRITE_DATA);
440
441    // Field 2: Entry (nested DataEntry)
442    if !entries_hex.is_empty() {
443        let entry_bytes = marshal_data_entry(entries_hex);
444        let _ = writer.write_uvarint(2);
445        let _ = writer.write_uvarint(entry_bytes.len() as u64);
446        let _ = writer.write_bytes(&entry_bytes);
447    }
448
449    // Field 3: Scratch (only if true)
450    if scratch {
451        let _ = writer.write_uvarint(3);
452        let _ = writer.write_uvarint(1); // true = 1
453    }
454
455    // Field 4: WriteToState (only if true)
456    if write_to_state {
457        let _ = writer.write_uvarint(4);
458        let _ = writer.write_uvarint(1); // true = 1
459    }
460
461    writer.into_bytes()
462}
463
464/// Marshal a DataEntry (DoubleHash type)
465///
466/// Field order:
467/// - Field 1: Type (enum = 3 for DoubleHash)
468/// - Field 2: Data (repeated bytes)
469fn marshal_data_entry(entries_hex: &[String]) -> Vec<u8> {
470    let mut writer = BinaryWriter::new();
471
472    // Field 1: Type (DoubleHash = 3)
473    let _ = writer.write_uvarint(1);
474    let _ = writer.write_uvarint(3); // DataEntryType.DoubleHash
475
476    // Field 2: Data (repeated bytes)
477    for entry_hex in entries_hex {
478        if let Ok(data) = hex::decode(entry_hex) {
479            let _ = writer.write_uvarint(2);
480            let _ = writer.write_uvarint(data.len() as u64);
481            let _ = writer.write_bytes(&data);
482        }
483    }
484
485    writer.into_bytes()
486}
487
488/// Marshal CreateTokenAccount transaction body to binary format
489///
490/// Field order matches Go: protocol/types_gen.go CreateTokenAccount.MarshalBinary
491/// - Field 1: Type (enum)
492/// - Field 2: Url (URL as string)
493/// - Field 3: TokenUrl (URL as string)
494/// - Field 4: Authorities (repeated URLs)
495pub fn marshal_create_token_account_body(
496    url: &str,
497    token_url: &str,
498) -> Vec<u8> {
499    let mut writer = BinaryWriter::new();
500
501    // Field 1: Type (CreateTokenAccount = 0x02)
502    let _ = writer.write_uvarint(1);
503    let _ = writer.write_uvarint(tx_types::CREATE_TOKEN_ACCOUNT);
504
505    // Field 2: Url
506    if !url.is_empty() {
507        let _ = writer.write_uvarint(2);
508        let url_bytes = url.as_bytes();
509        let _ = writer.write_uvarint(url_bytes.len() as u64);
510        let _ = writer.write_bytes(url_bytes);
511    }
512
513    // Field 3: TokenUrl
514    if !token_url.is_empty() {
515        let _ = writer.write_uvarint(3);
516        let token_bytes = token_url.as_bytes();
517        let _ = writer.write_uvarint(token_bytes.len() as u64);
518        let _ = writer.write_bytes(token_bytes);
519    }
520
521    writer.into_bytes()
522}
523
524/// Marshal a TokenRecipient
525fn marshal_token_recipient(url: &str, amount: u64) -> Vec<u8> {
526    let mut writer = BinaryWriter::new();
527
528    // Field 1: URL
529    let _ = writer.write_uvarint(1);
530    let url_bytes = url.as_bytes();
531    let _ = writer.write_uvarint(url_bytes.len() as u64);
532    let _ = writer.write_bytes(url_bytes);
533
534    // Field 2: Amount
535    if amount > 0 {
536        let _ = writer.write_uvarint(2);
537        let amount_bytes = amount_to_bigint_bytes(amount);
538        let _ = writer.write_uvarint(amount_bytes.len() as u64);
539        let _ = writer.write_bytes(&amount_bytes);
540    }
541
542    writer.into_bytes()
543}
544
545/// Convert u64 amount to big-endian bytes (minimal encoding)
546fn amount_to_bigint_bytes(mut value: u64) -> Vec<u8> {
547    if value == 0 {
548        return vec![];
549    }
550
551    let mut bytes = Vec::new();
552    while value > 0 {
553        bytes.push((value & 0xFF) as u8);
554        value >>= 8;
555    }
556    bytes.reverse(); // Convert to big-endian
557    bytes
558}
559
560/// Marshal CreateToken transaction body to binary format
561///
562/// Field order matches Go: protocol/user_transactions.yml CreateToken
563/// - Field 1: Type (enum 0x08)
564/// - Field 2: Url (URL)
565/// - Field 4: Symbol (string) - note: field 3 is skipped per Go spec
566/// - Field 5: Precision (uint)
567/// - Field 6: Properties (URL, optional)
568/// - Field 7: SupplyLimit (BigInt, optional)
569pub fn marshal_create_token_body(url: &str, symbol: &str, precision: u64, supply_limit: Option<u64>) -> Vec<u8> {
570    let mut writer = BinaryWriter::new();
571
572    // Field 1: Type (CreateToken = 0x08)
573    let _ = writer.write_uvarint(1);
574    let _ = writer.write_uvarint(tx_types::CREATE_TOKEN);
575
576    // Field 2: Url
577    if !url.is_empty() {
578        let _ = writer.write_uvarint(2);
579        let url_bytes = url.as_bytes();
580        let _ = writer.write_uvarint(url_bytes.len() as u64);
581        let _ = writer.write_bytes(url_bytes);
582    }
583
584    // Field 4: Symbol (note: field 3 is not used)
585    if !symbol.is_empty() {
586        let _ = writer.write_uvarint(4);
587        let symbol_bytes = symbol.as_bytes();
588        let _ = writer.write_uvarint(symbol_bytes.len() as u64);
589        let _ = writer.write_bytes(symbol_bytes);
590    }
591
592    // Field 5: Precision (always write, even if 0 - it's a valid value)
593    let _ = writer.write_uvarint(5);
594    let _ = writer.write_uvarint(precision);
595
596    // Field 7: SupplyLimit (optional)
597    if let Some(limit) = supply_limit {
598        if limit > 0 {
599            let _ = writer.write_uvarint(7);
600            let limit_bytes = amount_to_bigint_bytes(limit);
601            let _ = writer.write_uvarint(limit_bytes.len() as u64);
602            let _ = writer.write_bytes(&limit_bytes);
603        }
604    }
605
606    writer.into_bytes()
607}
608
609/// Marshal IssueTokens transaction body to binary format
610///
611/// Field order matches Go: protocol/user_transactions.yml IssueTokens
612/// - Field 1: Type (enum 0x09)
613/// - Field 4: To (repeated TokenRecipient)
614pub fn marshal_issue_tokens_body(recipients: &[(&str, u64)]) -> Vec<u8> {
615    let mut writer = BinaryWriter::new();
616
617    // Field 1: Type (IssueTokens = 0x09)
618    let _ = writer.write_uvarint(1);
619    let _ = writer.write_uvarint(tx_types::ISSUE_TOKENS);
620
621    // Field 4: To (repeated TokenRecipient)
622    for (url, amount) in recipients {
623        let recipient_bytes = marshal_token_recipient(url, *amount);
624        let _ = writer.write_uvarint(4);
625        let _ = writer.write_uvarint(recipient_bytes.len() as u64);
626        let _ = writer.write_bytes(&recipient_bytes);
627    }
628
629    writer.into_bytes()
630}
631
632/// Compute transaction hash using binary encoding
633///
634/// Based on Go: protocol/transaction_hash.go:27-71
635/// Transaction hash = SHA256(SHA256(header_binary) + SHA256(body_binary))
636pub fn compute_transaction_hash(header_bytes: &[u8], body_bytes: &[u8]) -> [u8; 32] {
637    let header_hash = sha256_bytes(header_bytes);
638    let body_hash = sha256_bytes(body_bytes);
639
640    let mut combined = Vec::with_capacity(64);
641    combined.extend_from_slice(&header_hash);
642    combined.extend_from_slice(&body_hash);
643
644    sha256_bytes(&combined)
645}
646
647/// Create signing preimage
648///
649/// Based on Go: protocol/signature_utils.go:50-57
650/// signingHash = SHA256(sigMdHash + txnHash)
651pub fn create_signing_preimage(
652    signature_metadata_hash: &[u8; 32],
653    transaction_hash: &[u8; 32],
654) -> [u8; 32] {
655    let mut combined = Vec::with_capacity(64);
656    combined.extend_from_slice(signature_metadata_hash);
657    combined.extend_from_slice(transaction_hash);
658
659    sha256_bytes(&combined)
660}
661
662/// SHA256 hash helper
663pub fn sha256_bytes(data: &[u8]) -> [u8; 32] {
664    let mut hasher = Sha256::new();
665    hasher.update(data);
666    let result = hasher.finalize();
667    let mut output = [0u8; 32];
668    output.copy_from_slice(&result);
669    output
670}
671
672// =============================================================================
673// WRITEDATA SPECIAL HASH COMPUTATION
674// =============================================================================
675
676/// Compute WriteData body hash using special Merkle algorithm
677///
678/// Based on Go: protocol/transaction_hash.go:91-114
679/// 1. Marshal WriteData body with Entry=nil (only Type, Scratch, WriteToState)
680/// 2. Compute Merkle hash of [SHA256(marshaledBody), entryHash]
681pub fn compute_write_data_body_hash(entries_hex: &[String], scratch: bool, write_to_state: bool) -> [u8; 32] {
682    // Marshal body WITHOUT entry
683    let body_without_entry = marshal_write_data_body_without_entry(scratch, write_to_state);
684    let body_part_hash = sha256_bytes(&body_without_entry);
685
686    // Compute entry hash
687    let entry_hash = if entries_hex.is_empty() {
688        [0u8; 32]
689    } else {
690        compute_data_entry_hash(entries_hex)
691    };
692
693    // Merkle hash of [bodyPartHash, entryHash]
694    merkle_hash(&[body_part_hash, entry_hash])
695}
696
697/// Marshal WriteData body without entry (Entry = nil)
698fn marshal_write_data_body_without_entry(scratch: bool, write_to_state: bool) -> Vec<u8> {
699    let mut writer = BinaryWriter::new();
700
701    // Field 1: Type (WriteData = 0x05)
702    let _ = writer.write_uvarint(1);
703    let _ = writer.write_uvarint(tx_types::WRITE_DATA);
704
705    // Field 2: Entry - OMITTED (nil)
706
707    // Field 3: Scratch
708    if scratch {
709        let _ = writer.write_uvarint(3);
710        let _ = writer.write_uvarint(1);
711    }
712
713    // Field 4: WriteToState
714    if write_to_state {
715        let _ = writer.write_uvarint(4);
716        let _ = writer.write_uvarint(1);
717    }
718
719    writer.into_bytes()
720}
721
722/// Compute WriteDataTo body hash using special Merkle algorithm
723///
724/// Same algorithm as WriteData but with Type=WRITE_DATA_TO and Recipient included
725/// Based on Go: protocol/transaction_hash.go WriteDataTo.GetHash()
726pub fn compute_write_data_to_body_hash(recipient: &str, entries_hex: &[String]) -> [u8; 32] {
727    // Marshal body WITHOUT entry (but WITH recipient)
728    let body_without_entry = marshal_write_data_to_body_without_entry(recipient);
729    let body_part_hash = sha256_bytes(&body_without_entry);
730
731    // Compute entry hash
732    let entry_hash = if entries_hex.is_empty() {
733        [0u8; 32]
734    } else {
735        compute_data_entry_hash(entries_hex)
736    };
737
738    // Merkle hash of [bodyPartHash, entryHash]
739    merkle_hash(&[body_part_hash, entry_hash])
740}
741
742/// Marshal WriteDataTo body without entry (Entry = nil)
743/// Includes Type and Recipient, but NOT Entry
744fn marshal_write_data_to_body_without_entry(recipient: &str) -> Vec<u8> {
745    let mut writer = BinaryWriter::new();
746
747    // Field 1: Type (WriteDataTo = 0x06)
748    let _ = writer.write_uvarint(1);
749    let _ = writer.write_uvarint(tx_types::WRITE_DATA_TO);
750
751    // Field 2: Recipient URL
752    let url_bytes = recipient.as_bytes();
753    let _ = writer.write_uvarint(2);
754    let _ = writer.write_uvarint(url_bytes.len() as u64);
755    let _ = writer.write_bytes(url_bytes);
756
757    // Field 3: Entry - OMITTED (nil)
758
759    writer.into_bytes()
760}
761
762/// Compute hash for a DataEntry
763///
764/// Based on Go: protocol/data_entry.go
765/// DoubleHashDataEntry: SHA256(MerkleHash(SHA256(data1), SHA256(data2), ...))
766fn compute_data_entry_hash(entries_hex: &[String]) -> [u8; 32] {
767    if entries_hex.is_empty() {
768        return [0u8; 32];
769    }
770
771    // Collect SHA256 hashes of each data item
772    let mut data_hashes: Vec<[u8; 32]> = Vec::new();
773    for entry_hex in entries_hex {
774        if let Ok(data) = hex::decode(entry_hex) {
775            data_hashes.push(sha256_bytes(&data));
776        }
777    }
778
779    if data_hashes.is_empty() {
780        return [0u8; 32];
781    }
782
783    // Compute Merkle hash of data hashes
784    let merkle_root = merkle_hash(&data_hashes);
785
786    // For DoubleHash: return SHA256(merkleRoot)
787    sha256_bytes(&merkle_root)
788}
789
790/// Compute Merkle hash of a list of hashes
791///
792/// Based on Go: pkg/database/merkle/hasher.go MerkleHash()
793/// Uses a cascading binary tree algorithm
794fn merkle_hash(hashes: &[[u8; 32]]) -> [u8; 32] {
795    if hashes.is_empty() {
796        return [0u8; 32];
797    }
798
799    if hashes.len() == 1 {
800        return hashes[0];
801    }
802
803    // Use the Merkle cascade algorithm from Go
804    let mut pending: Vec<Option<[u8; 32]>> = Vec::new();
805
806    for hash in hashes {
807        let mut current = *hash;
808        let mut i = 0;
809        loop {
810            // Extend pending if needed
811            if i >= pending.len() {
812                pending.push(Some(current));
813                break;
814            }
815
816            // If slot is empty, put hash there
817            if pending[i].is_none() {
818                pending[i] = Some(current);
819                break;
820            }
821
822            // Combine hashes and carry to next level
823            current = combine_hashes(&pending[i].unwrap(), &current);
824            pending[i] = None;
825            i += 1;
826        }
827    }
828
829    // Combine remaining pending hashes
830    let mut anchor: Option<[u8; 32]> = None;
831    for v in &pending {
832        if anchor.is_none() {
833            anchor = *v;
834        } else if let Some(val) = v {
835            anchor = Some(combine_hashes(val, &anchor.unwrap()));
836        }
837    }
838
839    anchor.unwrap_or([0u8; 32])
840}
841
842/// Combine two hashes: SHA256(left + right)
843fn combine_hashes(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
844    let mut combined = Vec::with_capacity(64);
845    combined.extend_from_slice(left);
846    combined.extend_from_slice(right);
847    sha256_bytes(&combined)
848}
849
850// =============================================================================
851// UPDATE KEY PAGE ENCODING
852// =============================================================================
853
854/// Marshal KeySpecParams to binary format
855///
856/// Field order matches Go: protocol/key_page_operations.yml KeySpecParams
857/// - Field 1: KeyHash (bytes)
858/// - Field 2: Delegate (URL, optional)
859pub fn marshal_key_spec_params(key_hash: &[u8], delegate: Option<&str>) -> Vec<u8> {
860    let mut writer = BinaryWriter::new();
861
862    // Field 1: KeyHash (bytes with length prefix)
863    if !key_hash.is_empty() {
864        let _ = writer.write_uvarint(1);
865        let _ = writer.write_uvarint(key_hash.len() as u64);
866        let _ = writer.write_bytes(key_hash);
867    }
868
869    // Field 2: Delegate URL (optional)
870    if let Some(delegate_url) = delegate {
871        if !delegate_url.is_empty() {
872            let _ = writer.write_uvarint(2);
873            let delegate_bytes = delegate_url.as_bytes();
874            let _ = writer.write_uvarint(delegate_bytes.len() as u64);
875            let _ = writer.write_bytes(delegate_bytes);
876        }
877    }
878
879    writer.into_bytes()
880}
881
882/// Marshal a KeyPageOperation to binary format
883///
884/// Each operation type has different fields after the type enum:
885/// - AddKeyOperation (type=3): Field 2: Entry (KeySpecParams)
886/// - RemoveKeyOperation (type=2): Field 2: Entry (KeySpecParams)
887/// - UpdateKeyOperation (type=1): Field 2: OldEntry, Field 3: NewEntry
888/// - SetThresholdKeyPageOperation (type=4): Field 2: Threshold (uint)
889/// - SetRejectThresholdKeyPageOperation (type=6): Field 2: Threshold (uint)
890/// - SetResponseThresholdKeyPageOperation (type=7): Field 2: Threshold (uint)
891/// - UpdateAllowedKeyPageOperation (type=5): Field 2: Allow[], Field 3: Deny[]
892pub fn marshal_key_page_operation(
893    op_type: &str,
894    key_hash: Option<&[u8]>,
895    delegate: Option<&str>,
896    old_key_hash: Option<&[u8]>,
897    new_key_hash: Option<&[u8]>,
898    threshold: Option<u64>,
899) -> Vec<u8> {
900    let mut writer = BinaryWriter::new();
901
902    // Map operation type string to enum value
903    let op_type_num = match op_type {
904        "add" => key_page_op_types::ADD,
905        "remove" => key_page_op_types::REMOVE,
906        "update" => key_page_op_types::UPDATE,
907        "setThreshold" => key_page_op_types::SET_THRESHOLD,
908        "setRejectThreshold" => key_page_op_types::SET_REJECT_THRESHOLD,
909        "setResponseThreshold" => key_page_op_types::SET_RESPONSE_THRESHOLD,
910        "updateAllowed" => key_page_op_types::UPDATE_ALLOWED,
911        _ => key_page_op_types::UNKNOWN,
912    };
913
914    // Field 1: Type (enum)
915    let _ = writer.write_uvarint(1);
916    let _ = writer.write_uvarint(op_type_num);
917
918    match op_type {
919        "add" | "remove" => {
920            // Field 2: Entry (KeySpecParams)
921            if let Some(hash) = key_hash {
922                let entry_bytes = marshal_key_spec_params(hash, delegate);
923                let _ = writer.write_uvarint(2);
924                let _ = writer.write_uvarint(entry_bytes.len() as u64);
925                let _ = writer.write_bytes(&entry_bytes);
926            }
927        }
928        "update" => {
929            // Field 2: OldEntry (KeySpecParams)
930            if let Some(old_hash) = old_key_hash {
931                let old_entry_bytes = marshal_key_spec_params(old_hash, None);
932                let _ = writer.write_uvarint(2);
933                let _ = writer.write_uvarint(old_entry_bytes.len() as u64);
934                let _ = writer.write_bytes(&old_entry_bytes);
935            }
936            // Field 3: NewEntry (KeySpecParams)
937            if let Some(new_hash) = new_key_hash {
938                let new_entry_bytes = marshal_key_spec_params(new_hash, delegate);
939                let _ = writer.write_uvarint(3);
940                let _ = writer.write_uvarint(new_entry_bytes.len() as u64);
941                let _ = writer.write_bytes(&new_entry_bytes);
942            }
943        }
944        "setThreshold" | "setRejectThreshold" | "setResponseThreshold" => {
945            // Field 2: Threshold (uint)
946            if let Some(thresh) = threshold {
947                let _ = writer.write_uvarint(2);
948                let _ = writer.write_uvarint(thresh);
949            }
950        }
951        "updateAllowed" => {
952            // TODO: Implement Allow/Deny arrays when needed
953            // Field 2: Allow (repeated TransactionType)
954            // Field 3: Deny (repeated TransactionType)
955        }
956        _ => {}
957    }
958
959    writer.into_bytes()
960}
961
962/// Marshal UpdateKeyPage transaction body to binary format
963///
964/// Field order matches Go: protocol/user_transactions.yml UpdateKeyPage
965/// - Field 1: Type (enum 0x0F)
966/// - Field 2: Operation (repeated KeyPageOperation)
967pub fn marshal_update_key_page_body(operations: &[Vec<u8>]) -> Vec<u8> {
968    let mut writer = BinaryWriter::new();
969
970    // Field 1: Type (UpdateKeyPage = 0x0F)
971    let _ = writer.write_uvarint(1);
972    let _ = writer.write_uvarint(tx_types::UPDATE_KEY_PAGE);
973
974    // Field 2: Operation (repeated, each as nested value)
975    for op_bytes in operations {
976        let _ = writer.write_uvarint(2);
977        let _ = writer.write_uvarint(op_bytes.len() as u64);
978        let _ = writer.write_bytes(op_bytes);
979    }
980
981    writer.into_bytes()
982}
983
984/// Marshal CreateKeyPage transaction body to binary format
985///
986/// Field order matches Go: protocol/user_transactions.yml CreateKeyPage
987/// - Field 1: Type (enum, 0x0C)
988/// - Field 2: Keys (repeated KeySpec, each as nested sub-message)
989pub fn marshal_create_key_page_body(key_hashes: &[Vec<u8>]) -> Vec<u8> {
990    let mut writer = BinaryWriter::new();
991
992    // Field 1: Type (CreateKeyPage = 0x0C)
993    let _ = writer.write_uvarint(1);
994    let _ = writer.write_uvarint(tx_types::CREATE_KEY_PAGE);
995
996    // Field 2: Keys (repeated KeySpec, each as nested value)
997    for key_hash in key_hashes {
998        let key_spec_bytes = marshal_key_spec_params(key_hash, None);
999        let _ = writer.write_uvarint(2);
1000        let _ = writer.write_uvarint(key_spec_bytes.len() as u64);
1001        let _ = writer.write_bytes(&key_spec_bytes);
1002    }
1003
1004    writer.into_bytes()
1005}
1006
1007/// Marshal BurnTokens transaction body to binary format
1008///
1009/// Field order matches Go: protocol/user_transactions.yml BurnTokens
1010/// - Field 1: Type (enum, 0x0A)
1011/// - Field 2: Amount (BigInt)
1012pub fn marshal_burn_tokens_body(amount: u64) -> Vec<u8> {
1013    let mut writer = BinaryWriter::new();
1014
1015    // Field 1: Type (BurnTokens = 0x0A)
1016    let _ = writer.write_uvarint(1);
1017    let _ = writer.write_uvarint(tx_types::BURN_TOKENS);
1018
1019    // Field 2: Amount (BigInt, big-endian bytes)
1020    if amount > 0 {
1021        let _ = writer.write_uvarint(2);
1022        let amount_bytes = amount_to_bigint_bytes(amount);
1023        let _ = writer.write_uvarint(amount_bytes.len() as u64);
1024        let _ = writer.write_bytes(&amount_bytes);
1025    }
1026
1027    writer.into_bytes()
1028}
1029
1030/// Marshal CreateKeyBook transaction body to binary format
1031///
1032/// Field order matches Go: protocol/user_transactions.yml CreateKeyBook
1033/// - Field 1: Type (enum, 0x0D)
1034/// - Field 2: Url (URL as string)
1035/// - Field 3: PublicKeyHash (bytes)
1036pub fn marshal_create_key_book_body(url: &str, public_key_hash: &[u8]) -> Vec<u8> {
1037    let mut writer = BinaryWriter::new();
1038
1039    // Field 1: Type (CreateKeyBook = 0x0D)
1040    let _ = writer.write_uvarint(1);
1041    let _ = writer.write_uvarint(tx_types::CREATE_KEY_BOOK);
1042
1043    // Field 2: Url
1044    let url_bytes = url.as_bytes();
1045    let _ = writer.write_uvarint(2);
1046    let _ = writer.write_uvarint(url_bytes.len() as u64);
1047    let _ = writer.write_bytes(url_bytes);
1048
1049    // Field 3: PublicKeyHash (bytes)
1050    if !public_key_hash.is_empty() {
1051        let _ = writer.write_uvarint(3);
1052        let _ = writer.write_uvarint(public_key_hash.len() as u64);
1053        let _ = writer.write_bytes(public_key_hash);
1054    }
1055
1056    writer.into_bytes()
1057}
1058
1059/// Marshal UpdateKey transaction body to binary format
1060///
1061/// Field order matches Go: protocol/user_transactions.yml UpdateKey
1062/// - Field 1: Type (enum, 0x16)
1063/// - Field 2: NewKeyHash (bytes)
1064pub fn marshal_update_key_body(new_key_hash: &[u8]) -> Vec<u8> {
1065    let mut writer = BinaryWriter::new();
1066
1067    // Field 1: Type (UpdateKey = 0x16)
1068    let _ = writer.write_uvarint(1);
1069    let _ = writer.write_uvarint(tx_types::UPDATE_KEY);
1070
1071    // Field 2: NewKeyHash (bytes)
1072    if !new_key_hash.is_empty() {
1073        let _ = writer.write_uvarint(2);
1074        let _ = writer.write_uvarint(new_key_hash.len() as u64);
1075        let _ = writer.write_bytes(new_key_hash);
1076    }
1077
1078    writer.into_bytes()
1079}
1080
1081/// Marshal BurnCredits transaction body to binary format
1082///
1083/// Field order matches Go: protocol/user_transactions.yml BurnCredits
1084/// - Field 1: Type (enum, 0x11)
1085/// - Field 2: Amount (uint64)
1086pub fn marshal_burn_credits_body(amount: u64) -> Vec<u8> {
1087    let mut writer = BinaryWriter::new();
1088
1089    // Field 1: Type (BurnCredits = 0x11)
1090    let _ = writer.write_uvarint(1);
1091    let _ = writer.write_uvarint(tx_types::BURN_CREDITS);
1092
1093    // Field 2: Amount (uint64, NOT BigInt)
1094    if amount > 0 {
1095        let _ = writer.write_uvarint(2);
1096        let _ = writer.write_uvarint(amount);
1097    }
1098
1099    writer.into_bytes()
1100}
1101
1102/// Marshal TransferCredits transaction body to binary format
1103///
1104/// Field order matches Go: protocol/user_transactions.yml TransferCredits
1105/// - Field 1: Type (enum, 0x12)
1106/// - Field 2: To (repeated CreditRecipient)
1107pub fn marshal_transfer_credits_body(recipients: &[(&str, u64)]) -> Vec<u8> {
1108    let mut writer = BinaryWriter::new();
1109
1110    // Field 1: Type (TransferCredits = 0x12)
1111    let _ = writer.write_uvarint(1);
1112    let _ = writer.write_uvarint(tx_types::TRANSFER_CREDITS);
1113
1114    // Field 2: To (repeated CreditRecipient)
1115    for (url, amount) in recipients {
1116        let recipient_bytes = marshal_credit_recipient(url, *amount);
1117        let _ = writer.write_uvarint(2);
1118        let _ = writer.write_uvarint(recipient_bytes.len() as u64);
1119        let _ = writer.write_bytes(&recipient_bytes);
1120    }
1121
1122    writer.into_bytes()
1123}
1124
1125/// Marshal a CreditRecipient to binary
1126/// Go CreditRecipient: Field 1=Url, Field 2=Amount (uint64)
1127fn marshal_credit_recipient(url: &str, amount: u64) -> Vec<u8> {
1128    let mut writer = BinaryWriter::new();
1129
1130    // Field 1: URL
1131    let url_bytes = url.as_bytes();
1132    let _ = writer.write_uvarint(1);
1133    let _ = writer.write_uvarint(url_bytes.len() as u64);
1134    let _ = writer.write_bytes(url_bytes);
1135
1136    // Field 2: Amount (uint64, NOT BigInt)
1137    if amount > 0 {
1138        let _ = writer.write_uvarint(2);
1139        let _ = writer.write_uvarint(amount);
1140    }
1141
1142    writer.into_bytes()
1143}
1144
1145/// Marshal WriteDataTo transaction body to binary format
1146///
1147/// Field order matches Go: protocol/user_transactions.yml WriteDataTo
1148/// - Field 1: Type (enum, 0x06)
1149/// - Field 2: Recipient (URL)
1150/// - Field 3: Entry (nested DataEntry)
1151pub fn marshal_write_data_to_body(recipient: &str, entries_hex: &[String]) -> Vec<u8> {
1152    let mut writer = BinaryWriter::new();
1153
1154    // Field 1: Type (WriteDataTo = 0x06)
1155    let _ = writer.write_uvarint(1);
1156    let _ = writer.write_uvarint(tx_types::WRITE_DATA_TO);
1157
1158    // Field 2: Recipient URL
1159    let url_bytes = recipient.as_bytes();
1160    let _ = writer.write_uvarint(2);
1161    let _ = writer.write_uvarint(url_bytes.len() as u64);
1162    let _ = writer.write_bytes(url_bytes);
1163
1164    // Field 3: Entry (nested DataEntry)
1165    if !entries_hex.is_empty() {
1166        let entry_bytes = marshal_data_entry(entries_hex);
1167        let _ = writer.write_uvarint(3);
1168        let _ = writer.write_uvarint(entry_bytes.len() as u64);
1169        let _ = writer.write_bytes(&entry_bytes);
1170    }
1171
1172    writer.into_bytes()
1173}
1174
1175/// Marshal LockAccount transaction body to binary format
1176///
1177/// Field order matches Go: protocol/user_transactions.yml LockAccount
1178/// - Field 1: Type (enum, 0x10)
1179/// - Field 2: Height (uint64)
1180pub fn marshal_lock_account_body(height: u64) -> Vec<u8> {
1181    let mut writer = BinaryWriter::new();
1182
1183    // Field 1: Type (LockAccount = 0x10)
1184    let _ = writer.write_uvarint(1);
1185    let _ = writer.write_uvarint(tx_types::LOCK_ACCOUNT);
1186
1187    // Field 2: Height (uint64)
1188    if height > 0 {
1189        let _ = writer.write_uvarint(2);
1190        let _ = writer.write_uvarint(height);
1191    }
1192
1193    writer.into_bytes()
1194}
1195
1196/// AccountAuthOperation type constants (matches Go AccountAuthOperationType)
1197pub mod account_auth_op_types {
1198    pub const ENABLE: u64 = 1;
1199    pub const DISABLE: u64 = 2;
1200    pub const ADD_AUTHORITY: u64 = 3;
1201    pub const REMOVE_AUTHORITY: u64 = 4;
1202}
1203
1204/// Marshal UpdateAccountAuth transaction body to binary format
1205///
1206/// Field order matches Go: protocol/user_transactions.yml UpdateAccountAuth
1207/// - Field 1: Type (enum, 0x15)
1208/// - Field 2: Operations (repeated AccountAuthOperation)
1209pub fn marshal_update_account_auth_body(operations: &[(&str, &str)]) -> Vec<u8> {
1210    let mut writer = BinaryWriter::new();
1211
1212    // Field 1: Type (UpdateAccountAuth = 0x15)
1213    let _ = writer.write_uvarint(1);
1214    let _ = writer.write_uvarint(tx_types::UPDATE_ACCOUNT_AUTH);
1215
1216    // Field 2: Operations (repeated)
1217    for (op_type, authority_url) in operations {
1218        let op_bytes = marshal_account_auth_operation(op_type, authority_url);
1219        let _ = writer.write_uvarint(2);
1220        let _ = writer.write_uvarint(op_bytes.len() as u64);
1221        let _ = writer.write_bytes(&op_bytes);
1222    }
1223
1224    writer.into_bytes()
1225}
1226
1227/// Marshal a single AccountAuthOperation to binary
1228/// Go: Field 1=Type (enum), Field 2=Authority (URL)
1229fn marshal_account_auth_operation(op_type: &str, authority_url: &str) -> Vec<u8> {
1230    let mut writer = BinaryWriter::new();
1231
1232    // Field 1: Type (AccountAuthOperationType enum)
1233    let type_num = match op_type {
1234        "enable" => account_auth_op_types::ENABLE,
1235        "disable" => account_auth_op_types::DISABLE,
1236        "add" | "addAuthority" => account_auth_op_types::ADD_AUTHORITY,
1237        "remove" | "removeAuthority" => account_auth_op_types::REMOVE_AUTHORITY,
1238        _ => 0,
1239    };
1240    let _ = writer.write_uvarint(1);
1241    let _ = writer.write_uvarint(type_num);
1242
1243    // Field 2: Authority URL
1244    if !authority_url.is_empty() {
1245        let url_bytes = authority_url.as_bytes();
1246        let _ = writer.write_uvarint(2);
1247        let _ = writer.write_uvarint(url_bytes.len() as u64);
1248        let _ = writer.write_bytes(url_bytes);
1249    }
1250
1251    writer.into_bytes()
1252}
1253
1254#[cfg(test)]
1255mod tests {
1256    use super::*;
1257
1258    #[test]
1259    fn test_signature_metadata_hash() {
1260        let public_key = [1u8; 32];
1261        let signer = "acc://test.acme/book/1";
1262        let signer_version = 1;
1263        let timestamp = 1234567890000u64;
1264
1265        let hash = compute_ed25519_signature_metadata_hash(
1266            &public_key,
1267            signer,
1268            signer_version,
1269            timestamp,
1270        );
1271
1272        // Should produce a 32-byte hash
1273        assert_eq!(hash.len(), 32);
1274
1275        // Same inputs should produce same hash
1276        let hash2 = compute_ed25519_signature_metadata_hash(
1277            &public_key,
1278            signer,
1279            signer_version,
1280            timestamp,
1281        );
1282        assert_eq!(hash, hash2);
1283    }
1284
1285    #[test]
1286    fn test_transaction_hash() {
1287        let header = b"test header";
1288        let body = b"test body";
1289
1290        let hash = compute_transaction_hash(header, body);
1291        assert_eq!(hash.len(), 32);
1292
1293        // Deterministic
1294        let hash2 = compute_transaction_hash(header, body);
1295        assert_eq!(hash, hash2);
1296    }
1297
1298    #[test]
1299    fn test_signing_preimage() {
1300        let sig_hash = [1u8; 32];
1301        let tx_hash = [2u8; 32];
1302
1303        let preimage = create_signing_preimage(&sig_hash, &tx_hash);
1304        assert_eq!(preimage.len(), 32);
1305    }
1306
1307    #[test]
1308    fn test_amount_encoding() {
1309        // Test various amounts
1310        assert_eq!(amount_to_bigint_bytes(0), vec![] as Vec<u8>);
1311        assert_eq!(amount_to_bigint_bytes(1), vec![1]);
1312        assert_eq!(amount_to_bigint_bytes(255), vec![255]);
1313        assert_eq!(amount_to_bigint_bytes(256), vec![1, 0]);
1314        assert_eq!(amount_to_bigint_bytes(0x123456), vec![0x12, 0x34, 0x56]);
1315    }
1316}