@@ -130,6 +130,24 @@ impl TransactionContext {
130130}
131131
132132impl Transaction {
133+ /// Create a new unsigned transaction from components.
134+ ///
135+ /// Used by the builder when constructing transactions from intents.
136+ /// No raw extrinsic bytes are stored; `to_bytes()` returns `signable_payload()`.
137+ pub fn new ( call_data : Vec < u8 > , era : Era , nonce : u32 , tip : u128 ) -> Self {
138+ Transaction {
139+ raw_bytes : Vec :: new ( ) ,
140+ is_signed : false ,
141+ signer : None ,
142+ signature : None ,
143+ era,
144+ nonce,
145+ tip,
146+ call_data,
147+ context : None ,
148+ }
149+ }
150+
133151 /// Create a transaction from raw bytes
134152 ///
135153 /// # Arguments
@@ -202,8 +220,9 @@ impl Transaction {
202220 // Fall back to manual serialization if no context
203221 self . to_bytes_manual ( )
204222 } else {
205- // Unsigned: return raw bytes (preserves metadata-encoded extensions from builder)
206- Ok ( self . raw_bytes . clone ( ) )
223+ // Unsigned: return the signing payload (call_data + extensions + additional_signed).
224+ // This matches legacy toBroadcastFormat which returns construct.signingPayload().
225+ self . signable_payload ( )
207226 }
208227 }
209228
@@ -263,7 +282,12 @@ impl Transaction {
263282 let bytes = if self . is_signed && self . signature . is_some ( ) {
264283 self . to_bytes ( ) . ok ( ) ?
265284 } else {
266- self . raw_bytes . clone ( )
285+ // For unsigned, use signable_payload if context exists, otherwise raw_bytes
286+ if self . context . is_some ( ) {
287+ self . signable_payload ( ) . ok ( ) ?
288+ } else {
289+ self . raw_bytes . clone ( )
290+ }
267291 } ;
268292
269293 if bytes. is_empty ( ) {
@@ -460,107 +484,165 @@ type ParsedExtrinsic = (
460484 Vec < u8 > ,
461485) ;
462486
463- /// Parse a raw extrinsic
487+ /// Parse a raw extrinsic or signing payload.
488+ ///
489+ /// Detects three formats:
490+ /// - **Signed extrinsic**: compact(length) | 0x84 | signer | signature | extensions | call_data
491+ /// - **Unsigned extrinsic (V4)**: compact(length) | 0x04 | call_data
492+ /// - **Signing payload**: call_data | era | nonce | tip | extensions | additional_signed
493+ /// (no length prefix, no version byte — produced by `signable_payload()`)
464494fn parse_extrinsic (
465495 bytes : & [ u8 ] ,
466496 metadata : Option < & Metadata > ,
467497) -> Result < ParsedExtrinsic , WasmDotError > {
468- use parity_scale_codec:: { Compact , Decode } ;
469-
470- let mut cursor = 0 ;
498+ // Try to detect the format by decoding compact length + version byte.
499+ // If it looks like a standard extrinsic, parse it that way.
500+ // Otherwise, treat as signing payload.
501+ if let Some ( result) = try_parse_extrinsic_format ( bytes, metadata) ? {
502+ return Ok ( result) ;
503+ }
471504
472- // Decode length prefix (compact)
473- let mut input = & bytes [ cursor.. ] ;
474- let length = < Compact < u32 > > :: decode ( & mut input )
475- . map_err ( |e| WasmDotError :: InvalidTransaction ( format ! ( "Invalid length: {}" , e ) ) ) ? ;
476- cursor = bytes . len ( ) - input . len ( ) ;
505+ // Not a standard extrinsic format — try parsing as signing payload.
506+ // Signing payload format: call_data | signed_extensions | additional_signed
507+ // Requires metadata to determine where call_data ends.
508+ parse_signing_payload ( bytes , metadata )
509+ }
477510
478- let _extrinsic_length = length. 0 as usize ;
511+ /// Try to parse bytes as a standard extrinsic (compact length + version byte).
512+ /// Returns None if the bytes don't look like an extrinsic.
513+ fn try_parse_extrinsic_format (
514+ bytes : & [ u8 ] ,
515+ metadata : Option < & Metadata > ,
516+ ) -> Result < Option < ParsedExtrinsic > , WasmDotError > {
517+ use parity_scale_codec:: { Compact , Decode } ;
479518
480- // Version byte
481- if cursor >= bytes. len ( ) {
519+ if bytes. is_empty ( ) {
482520 return Err ( WasmDotError :: InvalidTransaction (
483- "Missing version byte " . to_string ( ) ,
521+ "Empty transaction " . to_string ( ) ,
484522 ) ) ;
485523 }
486- let version = bytes[ cursor] ;
487- cursor += 1 ;
488524
489- let is_signed = ( version & 0x80 ) != 0 ;
490- let _extrinsic_version = version & 0x7f ;
525+ // Try decoding compact length prefix
526+ let mut input = bytes;
527+ let length = match <Compact < u32 > >:: decode ( & mut input) {
528+ Ok ( l) => l,
529+ Err ( _) => return Ok ( None ) , // Can't decode compact — not extrinsic format
530+ } ;
531+ let prefix_size = bytes. len ( ) - input. len ( ) ;
491532
492- if is_signed {
493- // Parse signed extrinsic
533+ // Check if compact length matches remaining bytes
534+ let remaining = bytes. len ( ) - prefix_size;
535+ if length. 0 as usize != remaining {
536+ return Ok ( None ) ; // Length doesn't match — not extrinsic format
537+ }
494538
495- // Signer (MultiAddress)
496- if cursor >= bytes. len ( ) {
497- return Err ( WasmDotError :: InvalidTransaction (
498- "Missing signer" . to_string ( ) ,
499- ) ) ;
500- }
501- let address_type = bytes[ cursor] ;
502- cursor += 1 ;
539+ // Check version byte
540+ if prefix_size >= bytes. len ( ) {
541+ return Ok ( None ) ;
542+ }
543+ let version = bytes[ prefix_size] ;
544+
545+ match version {
546+ 0x84 => {
547+ // Signed extrinsic
548+ let mut cursor = prefix_size + 1 ;
503549
504- let signer = if address_type == 0x00 {
505- // Id variant - 32 byte account id
506- if cursor + 32 > bytes. len ( ) {
550+ // Signer (MultiAddress)
551+ if cursor >= bytes. len ( ) {
507552 return Err ( WasmDotError :: InvalidTransaction (
508- "Truncated signer" . to_string ( ) ,
553+ "Missing signer" . to_string ( ) ,
509554 ) ) ;
510555 }
511- let mut pk = [ 0u8 ; 32 ] ;
512- pk. copy_from_slice ( & bytes[ cursor..cursor + 32 ] ) ;
513- cursor += 32 ;
514- Some ( pk)
515- } else {
516- return Err ( WasmDotError :: InvalidTransaction ( format ! (
517- "Unsupported address type: {}" ,
518- address_type
519- ) ) ) ;
520- } ;
521-
522- // Signature (MultiSignature)
523- if cursor >= bytes. len ( ) {
524- return Err ( WasmDotError :: InvalidTransaction (
525- "Missing signature" . to_string ( ) ,
526- ) ) ;
527- }
528- let sig_type = bytes[ cursor] ;
529- cursor += 1 ;
530-
531- let signature = if sig_type == 0x00 {
532- // Ed25519 - 64 bytes
533- if cursor + 64 > bytes. len ( ) {
556+ let address_type = bytes[ cursor] ;
557+ cursor += 1 ;
558+
559+ let signer = if address_type == 0x00 {
560+ if cursor + 32 > bytes. len ( ) {
561+ return Err ( WasmDotError :: InvalidTransaction (
562+ "Truncated signer" . to_string ( ) ,
563+ ) ) ;
564+ }
565+ let mut pk = [ 0u8 ; 32 ] ;
566+ pk. copy_from_slice ( & bytes[ cursor..cursor + 32 ] ) ;
567+ cursor += 32 ;
568+ Some ( pk)
569+ } else {
570+ return Err ( WasmDotError :: InvalidTransaction ( format ! (
571+ "Unsupported address type: {}" ,
572+ address_type
573+ ) ) ) ;
574+ } ;
575+
576+ // Signature (MultiSignature)
577+ if cursor >= bytes. len ( ) {
534578 return Err ( WasmDotError :: InvalidTransaction (
535- "Truncated signature" . to_string ( ) ,
579+ "Missing signature" . to_string ( ) ,
536580 ) ) ;
537581 }
538- let mut sig = [ 0u8 ; 64 ] ;
539- sig. copy_from_slice ( & bytes[ cursor..cursor + 64 ] ) ;
540- cursor += 64 ;
541- Some ( sig)
542- } else {
543- return Err ( WasmDotError :: InvalidTransaction ( format ! (
544- "Unsupported signature type: {}" ,
545- sig_type
546- ) ) ) ;
547- } ;
582+ let sig_type = bytes[ cursor] ;
583+ cursor += 1 ;
584+
585+ let signature = if sig_type == 0x00 {
586+ if cursor + 64 > bytes. len ( ) {
587+ return Err ( WasmDotError :: InvalidTransaction (
588+ "Truncated signature" . to_string ( ) ,
589+ ) ) ;
590+ }
591+ let mut sig = [ 0u8 ; 64 ] ;
592+ sig. copy_from_slice ( & bytes[ cursor..cursor + 64 ] ) ;
593+ cursor += 64 ;
594+ Some ( sig)
595+ } else {
596+ return Err ( WasmDotError :: InvalidTransaction ( format ! (
597+ "Unsupported signature type: {}" ,
598+ sig_type
599+ ) ) ) ;
600+ } ;
601+
602+ // Parse signed extensions
603+ let ( era, nonce, tip, ext_size) = parse_signed_extensions ( & bytes[ cursor..] , metadata) ?;
604+ cursor += ext_size;
605+
606+ // Remaining bytes are call data
607+ let call_data = bytes[ cursor..] . to_vec ( ) ;
608+
609+ Ok ( Some ( ( true , signer, signature, era, nonce, tip, call_data) ) )
610+ }
611+ _ => {
612+ // Not a signed extrinsic — fall through to signing payload parser
613+ Ok ( None )
614+ }
615+ }
616+ }
548617
549- // Parse signed extensions (era, nonce, tip, and any extras)
550- let ( era, nonce, tip, ext_size) = parse_signed_extensions ( & bytes[ cursor..] , metadata) ?;
551- cursor += ext_size;
618+ /// Parse bytes as a signing payload.
619+ ///
620+ /// Format: call_data | signed_extensions (era, nonce, tip, ...) | additional_signed (spec, tx_ver, genesis, block_hash)
621+ ///
622+ /// Requires metadata to determine where call_data ends (via the RuntimeCall type).
623+ fn parse_signing_payload (
624+ bytes : & [ u8 ] ,
625+ metadata : Option < & Metadata > ,
626+ ) -> Result < ParsedExtrinsic , WasmDotError > {
627+ let md = metadata. ok_or_else ( || {
628+ WasmDotError :: InvalidTransaction (
629+ "Metadata required to parse signing payload format" . to_string ( ) ,
630+ )
631+ } ) ?;
552632
553- // Remaining bytes are call data
554- let call_data = bytes[ cursor..] . to_vec ( ) ;
633+ // Use the RuntimeCall type from metadata to skip over call_data
634+ let call_ty_id = md. outer_enums ( ) . call_enum_ty ( ) ;
635+ let call_data_size = skip_type_bytes ( bytes, call_ty_id, md) ?;
636+ let call_data = bytes[ ..call_data_size] . to_vec ( ) ;
555637
556- Ok ( ( true , signer , signature , era , nonce , tip , call_data) )
557- } else {
558- // Unsigned extrinsic: standard Substrate V4 format has call data
559- // immediately after the version byte (no signed extensions in body).
560- // Era, nonce, and tip are only in the signing payload, not the extrinsic.
561- let call_data = bytes [ cursor.. ] . to_vec ( ) ;
562- Ok ( ( false , None , None , Era :: Immortal , 0 , 0 , call_data ) )
563- }
638+ // Parse signed extensions after call_data
639+ let ext_bytes = & bytes [ call_data_size.. ] ;
640+ let ( era , nonce , tip , _ext_size ) = parse_signed_extensions ( ext_bytes , Some ( md ) ) ? ;
641+
642+ // Remaining bytes after extensions are additional_signed (spec_version, tx_version,
643+ // genesis_hash, block_hash) — we don't need to parse those.
644+
645+ Ok ( ( false , None , None , era , nonce , tip , call_data ) )
564646}
565647
566648/// Parse signed extensions from extrinsic bytes.
@@ -740,25 +822,6 @@ fn decode_era_bytes(bytes: &[u8]) -> Result<(Era, usize), WasmDotError> {
740822mod tests {
741823 use super :: * ;
742824
743- #[ test]
744- fn test_unsigned_tx_id_returns_blake2b_hash ( ) {
745- // Minimal unsigned extrinsic: compact length + version 0x04 + era(immortal) + nonce(0) + tip(0) + call_data
746- // length=6 (compact 0x18), version=0x04, era=0x00, nonce=0x00, tip=0x00, call=0xFF
747- let raw = vec ! [ 0x18 , 0x04 , 0x00 , 0x00 , 0x00 , 0xFF ] ;
748- let tx = Transaction :: from_bytes ( & raw , None , None ) . unwrap ( ) ;
749-
750- assert ! ( !tx. is_signed( ) ) ;
751- let id = tx. id ( ) ;
752- assert ! ( id. is_some( ) , "unsigned tx should have an id" ) ;
753- let id = id. unwrap ( ) ;
754- assert ! ( id. starts_with( "0x" ) , "id should be 0x-prefixed hex" ) ;
755- assert_eq ! ( id. len( ) , 66 , "blake2b-256 hash = 0x + 64 hex chars" ) ;
756-
757- // Same bytes should produce the same hash
758- let tx2 = Transaction :: from_bytes ( & raw , None , None ) . unwrap ( ) ;
759- assert_eq ! ( tx. id( ) , tx2. id( ) , "same bytes should produce same id" ) ;
760- }
761-
762825 #[ test]
763826 fn test_era_encoding_roundtrip ( ) {
764827 let immortal = Era :: Immortal ;
0 commit comments