Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions .spellcheck/signer.dic
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
120
161
Expand Down Expand Up @@ -131,4 +131,32 @@ yarn
zeroization
zeroize
zeroizeable
zeroized
zeroized
BananaSplit
Cmd
CocoaPods
H256
PRs
QA
RuntimeMetadata
SDK
TODOs
TestFlight
UIs
UI's
V14
apk
decodings
emoji
env
multisignatures
opencv
qr_transfers
rustc
transaction_parsing
v6
x86
x86_64
yml
support@novawallet
☝️
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ Try launching Studio from the shell (command assumes you are in the "Application
./Android\ Studio.app/Contents/MacOS/studio
```

#### "build fails when runnning particular test"
#### "build fails when running particular test"

Try to enable features for the dependencies. For example,

Expand Down
6 changes: 3 additions & 3 deletions docs/src/tutorials/Start.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ When you first launch Vault, it prompts you to read and accept terms and conditi

### Create keys

Open key manager by tapping bottom left symbol. On fresh start you will be prompted to create seed (otherwise you could always create more seeds by tapping `New seed` button in Key Manager). Enter any convenient seed name (it does not matter anything and is not used anywhere except for this particulat Vault device) and - if you would like to use custom seed phrase - switch to recovery mode and type the seed phrase. Custom seed phrase should be used only to recover or import existing key(s), do not input custom seed phrase unless it is properly random! **Security of your accounts relies on randomness of seed phrase**. If you are generating new seed phrase, use built-in random generator and do not input a custom seed phrase.
Open key manager by tapping bottom left symbol. On fresh start you will be prompted to create seed (otherwise you could always create more seeds by tapping `New seed` button in Key Manager). Enter any convenient seed name (it does not matter anything and is not used anywhere except for this particular Vault device) and - if you would like to use custom seed phrase - switch to recovery mode and type the seed phrase. Custom seed phrase should be used only to recover or import existing key(s), do not input custom seed phrase unless it is properly random! **Security of your accounts relies on randomness of seed phrase**. If you are generating new seed phrase, use built-in random generator and do not input a custom seed phrase.

Once you click `create` button, you will be prompted to authenticate yourself. This will happen every time cruptographic engine of the phone is used to handle seeds - on all creations, backups, derivations and signatures and in some OS versions on starting the Vault.
Once you click `create` button, you will be prompted to authenticate yourself. This will happen every time cryptographic engine of the phone is used to handle seeds - on all creations, backups, derivations and signatures and in some OS versions on starting the Vault.

You will see the created secret seed. Please back it up on paper and store it in safe place. If you lose your Vault device or it will become non-functional, you will be able to recover your keys using this seed phrase. Anyone could recover your keys with knowledge of this phrase. If you lose this seed phrase, though, **it will be impossible to recover your keys**. You can check the seed phrase anytime in Settings menu, but make sure that it is backed up at all times.

Expand All @@ -44,4 +44,4 @@ To learn more on key generation, read [subkey specifications](https://substrate.

Once you have a keypair you would like to use, you should first export it to hot wallet. Tap the key and select `Export` button. You will see the export QR code you can use with hot wallet.

Details on [signing with Pokadot.js Apps](./Kusama-tutorial.md)
Details on [signing with Polkadot.js Apps](./Kusama-tutorial.md)
2 changes: 1 addition & 1 deletion ios/PolkadotVault.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2558,7 +2558,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [ $ACTION != \"indexbuild\" ]; then\n if which swiftformat >/dev/null; then\n brew upgrade swiftformat\n swiftformat .\n else\n echo \"warning: Swiftformat not installed. Attempting to install via Homebrew...\"\n\n # Install Homebrew if not already installed\n if ! which brew >/dev/null; then\n echo \"Installing Homebrew...\"\n /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n fi\n\n # Install SwiftFormat\n echo \"Installing SwiftFormat...\"\n brew install swiftformat\n\n # Check for M1 chipset and create symlink if necessary\n if [[ `uname -m` == 'arm64' ]]; then\n echo \"M1 chipset detected. Creating necessary symlink for SwiftFormat...\"\n ln -s /opt/homebrew/bin/swiftformat /usr/local/bin/swiftformat\n fi\n\n # Verify installation\n if which swiftformat >/dev/null; then\n echo \"SwiftFormat installed successfully.\"\n swiftformat .\n else\n echo \"error: Failed to install SwiftFormat. Please install manually from https://github.com/nicklockwood/SwiftFormat\"\n fi\n fi\nfi\n";
shellScript = "if [ \"$ACTION\" = \"indexbuild\" ]; then\n exit 0\nfi\n\n# Xcode runs script phases with a minimal PATH that omits Homebrew\n# (/opt/homebrew/bin on Apple Silicon, /usr/local/bin on Intel), so an\n# already-installed swiftformat would not be found. Add them explicitly.\nexport PATH=\"/opt/homebrew/bin:/usr/local/bin:$PATH\"\n\nif ! which swiftformat >/dev/null; then\n if which brew >/dev/null; then\n echo \"SwiftFormat not found. Installing via Homebrew...\"\n brew install swiftformat\n fi\nfi\n\nif which swiftformat >/dev/null; then\n swiftformat .\nelse\n echo \"warning: SwiftFormat not installed. Install it with 'brew install swiftformat' (https://github.com/nicklockwood/SwiftFormat)\"\nfi\n";
};
6DDEF13528AE65D7004CA2FD /* Build libsigner.a */ = {
isa = PBXShellScriptBuildPhase;
Expand Down
6 changes: 6 additions & 0 deletions rust/deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ ignore = [
"RUSTSEC-2025-0009", # ring AES panic (via rustls; upgrade when jsonrpsee/rustls allow)
"RUSTSEC-2025-0134", # rustls-pemfile unmaintained (via rustls-native-certs)
"RUSTSEC-2025-0055", # tracing-subscriber ANSI injection (via substrate sp-tracing)
"RUSTSEC-2026-0007", # bytes BytesMut::reserve integer overflow (via substrate/subxt deps)
"RUSTSEC-2025-0161", # libsecp256k1 unmaintained (via substrate sp-io)
"RUSTSEC-2026-0098", # rustls-webpki name constraints URI (via subxt/jsonrpsee tls)
"RUSTSEC-2026-0099", # rustls-webpki name constraints wildcard (via subxt/jsonrpsee tls)
"RUSTSEC-2026-0104", # rustls-webpki CRL parsing panic (via subxt/jsonrpsee tls)
"RUSTSEC-2026-0009", # time DoS via stack exhaustion (via transitive deps)
]

[licenses]
Expand Down
2 changes: 1 addition & 1 deletion rust/generate_message/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -901,7 +901,7 @@
//! `$ cargo run make-cold-release <optional path>`
//!
//! Removes old cold release database and generates new one with default values
//! (unitiniated) at user-provided path or, if no valid path is given, at
//! (uninitiated) at user-provided path or, if no valid path is given, at
//! default path [`COLD_DB_NAME_RELEASE`](constants::COLD_DB_NAME_RELEASE).
//!
//! By default, the uninitiated cold release database contains:
Expand Down
35 changes: 33 additions & 2 deletions rust/parser/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ impl StateOutput {
pub trait State: Send + Sync {
fn clone_box(&self) -> Box<dyn State>;

/// Whether an upcoming byte sequence (`Vec<u8>`) holds human-readable text
/// and should be rendered as a single text card. The byte-sequence fast
/// path in `state_machine.rs` consults this flag.
fn expects_text(&self) -> bool {
false
}

fn get_default_output(&self, input: String, indent: u32) -> StateOutput {
let card = OutputCard {
card: ParserCard::Default(input),
Expand Down Expand Up @@ -316,8 +323,16 @@ pub trait State: Send + Sync {
}
};

let next_state: Box<dyn State> =
self.get_special_state_or_default(&input.type_name, &input.extra_info);
// Same field-name rule as the legacy decoder (`decoding_sci.rs`): the
// `remark` of `system.remark`/`system.remarkWithEvent` is user text.
let next_state: Box<dyn State> = if matches!(
input.name.as_deref(),
Some("remark") | Some("remark_with_event")
) {
Box::new(TextState)
} else {
self.get_special_state_or_default(&input.type_name, &input.extra_info)
};

Ok(StateOutput {
next_state,
Expand Down Expand Up @@ -421,3 +436,19 @@ impl State for DefaultState {
Box::new(self.clone())
}
}

/// Entered for fields whose bytes are human-readable text (e.g. `remark`).
/// Behaves as [`DefaultState`] for every type except byte sequences, which the
/// state machine renders as a single text card instead of per-byte cards.
#[derive(Debug, Clone, Default)]
pub struct TextState;

impl State for TextState {
fn clone_box(&self) -> Box<dyn State> {
Box::new(self.clone())
}

fn expects_text(&self) -> bool {
true
}
}
56 changes: 55 additions & 1 deletion rust/parser/src/state_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ use merkleized_metadata::{
};

use crate::{
cards::ParserCard,
decoding_commons::OutputCard,
state::{State, StateError, StateInputCompound, StateInputCompoundItem, StateOutput},
state::{
DefaultState, State, StateError, StateInputCompound, StateInputCompoundItem, StateOutput,
},
};

/// Maximum allowed recursion depth to prevent stack overflow from deeply nested types.
Expand Down Expand Up @@ -146,6 +149,24 @@ impl StateMachineParser<'_> {
}
}

/// Minimal visitor that extracts a single `u8` — used to drain byte sequences
/// without producing per-byte cards.
struct U8Collector;

impl Visitor for U8Collector {
type TypeResolver = TypeResolver;
type Value<'scale, 'resolver> = u8;
type Error = StateError;

fn visit_u8<'scale, 'resolver>(
self,
value: u8,
_type_id: TypeRef,
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
Ok(value)
}
}

impl Visitor for StateMachineParser<'_> {
type TypeResolver = TypeResolver;
type Value<'scale, 'resolver> = Self;
Expand Down Expand Up @@ -417,6 +438,39 @@ impl Visitor for StateMachineParser<'_> {

let items_count = value.remaining();

// Byte sequences (`Vec<u8>`) collapse into a single card: UTF-8 text when
// the current state expects text (`remark`-like fields), hex otherwise.
// Per-byte numeric cards would make the value unreviewable on the signing
// screen.
if matches!(item_type_id, Some(TypeRef::U8)) {
let mut bytes = Vec::with_capacity(items_count);
for _ in 0..items_count {
let byte = value
.decode_item(U8Collector)
.ok_or_else(|| StateError::BadInput("Unexpected end of sequence".into()))??;
bytes.push(byte);
}

let card = if visitor.state.expects_text() {
match String::from_utf8(bytes) {
Ok(text) => ParserCard::Text(text),
Err(error) => {
ParserCard::Default(format!("0x{}", hex::encode(error.into_bytes())))
}
}
} else {
ParserCard::Default(format!("0x{}", hex::encode(&bytes)))
};

visitor.cards.push(OutputCard {
card,
indent: visitor.indent,
});
visitor.state = Box::new(DefaultState);

return Ok(visitor);
}

let input = StateInputCompound {
name: None,
path: &path,
Expand Down
133 changes: 133 additions & 0 deletions rust/parser/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -685,3 +685,136 @@ fn parse_extrinsic_with_deeply_nested_calls() {
"Deeply nested calls should be rejected to prevent stack overflow"
);
}

/// Builds a minimal `MetadataProof` describing a single-pallet runtime whose
/// only call is `System::<method_name> { <field_name>: Vec<u8> }`.
fn byte_sequence_metadata_proof(method_name: &str, field_name: &str) -> MetadataProof {
use merkleized_metadata::types::{
EnumerationVariant, ExtrinsicMetadata, Field, Type, TypeDef, TypeRef,
};
use merkleized_metadata::{ExtraInfo, Proof};

let call_enum = Type {
path: vec!["test_runtime".to_string(), "RuntimeCall".to_string()],
type_def: TypeDef::Enumeration(EnumerationVariant {
name: "System".to_string(),
fields: vec![Field {
name: None,
ty: TypeRef::ById(1u32.into()),
type_name: None,
}],
index: 0u32.into(),
}),
type_id: 0u32.into(),
};

let pallet_call_enum = Type {
path: vec![
"frame_system".to_string(),
"pallet".to_string(),
"Call".to_string(),
],
type_def: TypeDef::Enumeration(EnumerationVariant {
name: method_name.to_string(),
fields: vec![Field {
name: Some(field_name.to_string()),
ty: TypeRef::ById(2u32.into()),
type_name: Some("Vec<u8>".to_string()),
}],
index: 7u32.into(),
}),
type_id: 1u32.into(),
};

let byte_sequence = Type {
path: vec![],
type_def: TypeDef::Sequence(TypeRef::U8),
type_id: 2u32.into(),
};

MetadataProof {
proof: Proof {
leaves: vec![call_enum, pallet_call_enum, byte_sequence],
leaf_indices: vec![0, 1, 2],
nodes: vec![],
},
extrinsic: ExtrinsicMetadata {
version: 4,
address_ty: TypeRef::Void,
call_ty: TypeRef::ById(0u32.into()),
signature_ty: TypeRef::Void,
signed_extensions: vec![],
},
extra_info: ExtraInfo {
spec_version: 1,
spec_name: "test".to_string(),
base58_prefix: 42,
decimals: 12,
token_symbol: "UNIT".to_string(),
},
}
}

fn encode_byte_sequence_call(payload: &[u8]) -> Vec<u8> {
// pallet variant 0 (System), method variant 7, then SCALE-encoded Vec<u8>
let mut call_data = vec![0u8, 7u8];
call_data.extend(payload.to_vec().encode());
call_data
}

fn collect_text_cards(cards: &[crate::decoding_commons::OutputCard]) -> Vec<String> {
cards
.iter()
.filter_map(|c| match &c.card {
crate::cards::ParserCard::Text(text) => Some(text.clone()),
_ => None,
})
.collect()
}

fn collect_default_cards(cards: &[crate::decoding_commons::OutputCard]) -> Vec<String> {
cards
.iter()
.filter_map(|c| match &c.card {
crate::cards::ParserCard::Default(value) => Some(value.clone()),
_ => None,
})
.collect()
}

#[test]
fn parse_remark_with_event_bytes_as_text() {
let metadata = byte_sequence_metadata_proof("remark_with_event", "remark");
let call_data = encode_byte_sequence_call(b"verify proxy ping");

let cards = decode_call(&mut &call_data[..], &metadata).unwrap();

assert_eq!(collect_text_cards(&cards), vec!["verify proxy ping"]);
assert_eq!(
collect_default_cards(&cards),
Vec::<String>::new(),
"remark bytes must not be rendered as per-byte cards"
);
}

#[test]
fn parse_remark_with_invalid_utf8_as_hex() {
let metadata = byte_sequence_metadata_proof("remark_with_event", "remark");
let call_data = encode_byte_sequence_call(&[0xff, 0xfe, 0x00]);

let cards = decode_call(&mut &call_data[..], &metadata).unwrap();

assert_eq!(collect_text_cards(&cards), Vec::<String>::new());
assert_eq!(collect_default_cards(&cards), vec!["0xfffe00"]);
}

#[test]
fn parse_non_remark_byte_sequence_as_single_hex_card() {
let metadata = byte_sequence_metadata_proof("set_code", "code");
let call_data = encode_byte_sequence_call(&[0xde, 0xad, 0xbe, 0xef]);

let cards = decode_call(&mut &call_data[..], &metadata).unwrap();

assert_eq!(collect_text_cards(&cards), Vec::<String>::new());
assert_eq!(collect_default_cards(&cards), vec!["0xdeadbeef"]);
}
Loading