Skip to content

Commit 793b938

Browse files
committed
fix: return signable_payload from to_bytes for unsigned transactions
Make to_bytes() for unsigned DOT transactions return signable_payload() instead of a standard V4 extrinsic. The signing payload format (call_data | era | nonce | tip | extensions | additional_signed) matches what legacy BitGoJS produces via construct.signingPayload(), preserving nonce readback in bgms. Key changes: - Transaction::new() constructor for builder-created transactions (no raw extrinsic bytes stored) - to_bytes() unsigned path calls signable_payload() instead of returning raw_bytes - parse_extrinsic() detects signing payload format (no compact length prefix or version byte) and parses call_data using metadata type registry, then extracts era/nonce/tip from signed extensions - Removed build_unsigned_extrinsic() from builder (no longer needed) BTC-3161
1 parent aaff21f commit 793b938

2 files changed

Lines changed: 176 additions & 231 deletions

File tree

packages/wasm-dot/src/builder/mod.rs

Lines changed: 5 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ mod calls;
88
pub mod types;
99

1010
use crate::error::WasmDotError;
11-
use crate::transaction::{encode_era, Transaction};
11+
use crate::transaction::Transaction;
1212
use crate::types::{Era, Validity};
1313
use calls::encode_intent;
14-
use parity_scale_codec::{Compact, Encode};
15-
use subxt_core::metadata::Metadata;
1614
use types::{BuildContext, TransactionIntent};
1715

1816
/// Build a transaction from a business-level intent and context.
@@ -33,17 +31,10 @@ pub fn build_transaction(
3331
// Calculate era from validity
3432
let era = compute_era(&context.validity);
3533

36-
// Build unsigned extrinsic with signed extensions encoded per the chain's metadata
37-
let unsigned_bytes = build_unsigned_extrinsic(
38-
&call_data,
39-
&era,
40-
context.nonce,
41-
context.tip as u128,
42-
&metadata,
43-
)?;
44-
45-
// Create transaction from bytes — pass metadata so parser uses metadata-aware decoding
46-
let mut tx = Transaction::from_bytes(&unsigned_bytes, None, Some(&metadata))?;
34+
// Create transaction directly from components (no extrinsic encoding needed).
35+
// to_bytes() on unsigned transactions returns signable_payload(), which is the
36+
// signing payload format: call_data | era | nonce | tip | extensions | additional_signed.
37+
let mut tx = Transaction::new(call_data, era, context.nonce, context.tip as u128);
4738
tx.set_context(context.material, context.validity, &context.reference_block)?;
4839

4940
Ok(tx)
@@ -63,138 +54,6 @@ fn compute_era(validity: &Validity) -> Era {
6354
}
6455
}
6556

66-
/// Build unsigned extrinsic bytes with metadata-driven signed extension encoding.
67-
///
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
76-
fn build_unsigned_extrinsic(
77-
call_data: &[u8],
78-
era: &Era,
79-
nonce: u32,
80-
tip: u128,
81-
metadata: &Metadata,
82-
) -> Result<Vec<u8>, WasmDotError> {
83-
let mut body = Vec::new();
84-
85-
// Version byte: 0x04 = unsigned, version 4
86-
body.push(0x04);
87-
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
125-
body.extend_from_slice(call_data);
126-
127-
// Length prefix (compact encoded)
128-
let mut result = Compact(body.len() as u32).encode();
129-
result.extend_from_slice(&body);
130-
131-
Ok(result)
132-
}
133-
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-
19857
#[cfg(test)]
19958
mod tests {
20059
// Tests require real metadata - will be added with test fixtures

0 commit comments

Comments
 (0)