@@ -8,7 +8,7 @@ mod calls;
88pub mod types;
99
1010use crate :: error:: WasmDotError ;
11- use crate :: transaction:: { encode_era , Transaction } ;
11+ use crate :: transaction:: Transaction ;
1212use crate :: types:: { Era , Validity } ;
1313use calls:: encode_intent;
1414use parity_scale_codec:: { Compact , Encode } ;
@@ -33,7 +33,7 @@ pub fn build_transaction(
3333 // Calculate era from validity
3434 let era = compute_era ( & context. validity ) ;
3535
36- // Build unsigned extrinsic with signed extensions encoded per the chain's metadata
36+ // Build unsigned extrinsic: compact(length) | 0x04 | call_data
3737 let unsigned_bytes = build_unsigned_extrinsic (
3838 & call_data,
3939 & era,
@@ -46,6 +46,12 @@ pub fn build_transaction(
4646 let mut tx = Transaction :: from_bytes ( & unsigned_bytes, None , Some ( & metadata) ) ?;
4747 tx. set_context ( context. material , context. validity , & context. reference_block ) ?;
4848
49+ // Set era/nonce/tip from build context (not parsed from unsigned extrinsic body,
50+ // since standard format doesn't include signed extensions in the body)
51+ tx. set_era ( era) ;
52+ tx. set_nonce ( context. nonce ) ;
53+ tx. set_tip ( context. tip as u128 ) ;
54+
4955 Ok ( tx)
5056}
5157
@@ -63,65 +69,29 @@ fn compute_era(validity: &Validity) -> Era {
6369 }
6470}
6571
66- /// Build unsigned extrinsic bytes with metadata-driven signed extension encoding.
72+ /// Build unsigned extrinsic bytes in standard Substrate V4 format.
73+ ///
74+ /// Format: `compact(length) | 0x04 | call_data`
75+ ///
76+ /// Signed extensions (era, nonce, tip) are NOT included in the unsigned
77+ /// extrinsic body. They belong only in the signing payload, which is
78+ /// computed separately by `signable_payload()` via subxt-core.
6779///
68- /// Iterates the chain's signed extension list from metadata and encodes each:
69- /// - Empty types (0-size composites/tuples): skip
70- /// - CheckMortality: era bytes
71- /// - CheckNonce: Compact<u32>
72- /// - ChargeTransactionPayment: Compact<u128> tip
73- /// - ChargeAssetTxPayment: Compact<u128> tip + 0x00 (None asset_id)
74- /// - CheckMetadataHash: 0x00 (Disabled mode)
75- /// - Other non-empty types: encode default bytes using scale_decode to determine size
80+ /// This matches the format that polkadot-js, txwrapper, and all standard
81+ /// Substrate tools expect for unsigned extrinsics.
7682fn build_unsigned_extrinsic (
7783 call_data : & [ u8 ] ,
78- era : & Era ,
79- nonce : u32 ,
80- tip : u128 ,
81- metadata : & Metadata ,
84+ _era : & Era ,
85+ _nonce : u32 ,
86+ _tip : u128 ,
87+ _metadata : & Metadata ,
8288) -> Result < Vec < u8 > , WasmDotError > {
8389 let mut body = Vec :: new ( ) ;
8490
8591 // Version byte: 0x04 = unsigned, version 4
8692 body. push ( 0x04 ) ;
8793
88- // Encode signed extensions per metadata
89- for ext in metadata. extrinsic ( ) . signed_extensions ( ) {
90- let id = ext. identifier ( ) ;
91- let ty_id = ext. extra_ty ( ) ;
92-
93- if is_empty_type ( metadata, ty_id) {
94- continue ;
95- }
96-
97- match id {
98- "CheckMortality" | "CheckEra" => {
99- body. extend_from_slice ( & encode_era ( era) ) ;
100- }
101- "CheckNonce" => {
102- Compact ( nonce) . encode_to ( & mut body) ;
103- }
104- "ChargeTransactionPayment" => {
105- Compact ( tip) . encode_to ( & mut body) ;
106- }
107- "ChargeAssetTxPayment" => {
108- // Struct: { tip: Compact<u128>, asset_id: Option<T> }
109- Compact ( tip) . encode_to ( & mut body) ;
110- body. push ( 0x00 ) ; // None — no asset_id
111- }
112- "CheckMetadataHash" => {
113- // Mode enum: 0x00 = Disabled
114- body. push ( 0x00 ) ;
115- }
116- _ => {
117- // Unknown non-empty extension — encode zero bytes.
118- // This shouldn't happen for known chains but is a safety fallback.
119- encode_zero_value ( & mut body, ty_id, metadata) ?;
120- }
121- }
122- }
123-
124- // Call data
94+ // Call data immediately after version byte
12595 body. extend_from_slice ( call_data) ;
12696
12797 // Length prefix (compact encoded)
@@ -131,70 +101,6 @@ fn build_unsigned_extrinsic(
131101 Ok ( result)
132102}
133103
134- /// Check if a type ID resolves to an empty (zero-size) type.
135- fn is_empty_type ( metadata : & Metadata , ty_id : u32 ) -> bool {
136- let Some ( ty) = metadata. types ( ) . resolve ( ty_id) else {
137- return false ;
138- } ;
139- match & ty. type_def {
140- scale_info:: TypeDef :: Tuple ( t) => t. fields . is_empty ( ) ,
141- scale_info:: TypeDef :: Composite ( c) => c. fields . is_empty ( ) ,
142- _ => false ,
143- }
144- }
145-
146- /// Encode the zero/default value for a type. Used for unknown signed extensions
147- /// where we don't know the semantic meaning but need to produce valid SCALE bytes.
148- fn encode_zero_value (
149- buf : & mut Vec < u8 > ,
150- ty_id : u32 ,
151- metadata : & Metadata ,
152- ) -> Result < ( ) , WasmDotError > {
153- let Some ( ty) = metadata. types ( ) . resolve ( ty_id) else {
154- return Ok ( ( ) ) ; // Unknown type — skip
155- } ;
156- match & ty. type_def {
157- scale_info:: TypeDef :: Primitive ( p) => {
158- use scale_info:: TypeDefPrimitive ;
159- let zeros: usize = match p {
160- TypeDefPrimitive :: Bool | TypeDefPrimitive :: U8 | TypeDefPrimitive :: I8 => 1 ,
161- TypeDefPrimitive :: U16 | TypeDefPrimitive :: I16 => 2 ,
162- TypeDefPrimitive :: U32 | TypeDefPrimitive :: I32 => 4 ,
163- TypeDefPrimitive :: U64 | TypeDefPrimitive :: I64 => 8 ,
164- TypeDefPrimitive :: U128 | TypeDefPrimitive :: I128 => 16 ,
165- TypeDefPrimitive :: U256 | TypeDefPrimitive :: I256 => 32 ,
166- TypeDefPrimitive :: Str | TypeDefPrimitive :: Char => {
167- buf. push ( 0x00 ) ; // empty compact-encoded string/char
168- return Ok ( ( ) ) ;
169- }
170- } ;
171- buf. extend_from_slice ( & vec ! [ 0u8 ; zeros] ) ;
172- }
173- scale_info:: TypeDef :: Compact ( _) => {
174- buf. push ( 0x00 ) ; // Compact(0)
175- }
176- scale_info:: TypeDef :: Variant ( v) => {
177- // Use first variant (index 0 or lowest)
178- if let Some ( variant) = v. variants . first ( ) {
179- buf. push ( variant. index ) ;
180- for field in & variant. fields {
181- encode_zero_value ( buf, field. ty . id , metadata) ?;
182- }
183- }
184- }
185- scale_info:: TypeDef :: Composite ( c) => {
186- for field in & c. fields {
187- encode_zero_value ( buf, field. ty . id , metadata) ?;
188- }
189- }
190- scale_info:: TypeDef :: Sequence ( _) | scale_info:: TypeDef :: Array ( _) => {
191- buf. push ( 0x00 ) ; // empty sequence
192- }
193- _ => { } // BitSequence, etc. — skip
194- }
195- Ok ( ( ) )
196- }
197-
198104#[ cfg( test) ]
199105mod tests {
200106 // Tests require real metadata - will be added with test fixtures
0 commit comments