Skip to content
Draft
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
20 changes: 20 additions & 0 deletions crates/nexum/rpc/src/namespaces/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,26 @@ where
}
})?;

// personal_sign has reversed parameter order compared to eth_sign: (message, address)
eth_module.register_async_method(
"personal_sign",
async |params, ctx, _| -> RpcResult<Bytes> {
let (message, signer_addr) = params.parse::<(Bytes, Address)>()?;
let (sender, receiver) = oneshot::channel::<InteractiveResponse>();
ctx.sender
.send((InteractiveRequest::PersonalSign(signer_addr, message), sender))
.await
.map_err(json_rpc_internal_error)?;
let res = receiver.await.map_err(json_rpc_internal_error)?;
match res {
InteractiveResponse::PersonalSign(signature) => Ok(signature
.map(|s| s.as_bytes().into())
.map_err(json_rpc_internal_error)?),
_ => Err(ErrorObject::from(ErrorCode::InternalError)),
}
},
)?;

eth_module.register_async_method(
"eth_signTypedData_v4",
async |params, ctx, _| -> RpcResult<Bytes> {
Expand Down
21 changes: 14 additions & 7 deletions crates/nexum/rpc/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ pub enum InteractiveRequest {
SignTransaction(Box<EthereumTypedTransaction<TxEip4844Variant>>),
EthSign(Address, Bytes),
EthSignTypedData(Address, Box<TypedData>),
PersonalSign(Address, Bytes),
}

/// Responses for the interactive requests
Expand All @@ -111,6 +112,7 @@ pub enum InteractiveResponse {
SignTransaction(Result<Signature, Box<dyn std::error::Error + Send + Sync>>),
EthSign(Result<Signature, Box<dyn std::error::Error + Send + Sync>>),
EthSignTypedData(Result<Signature, Box<dyn std::error::Error + Send + Sync>>),
PersonalSign(Result<Signature, Box<dyn std::error::Error + Send + Sync>>),
}

pub async fn make_interactive_request(
Expand Down Expand Up @@ -181,13 +183,18 @@ impl RpcServerBuilder {
}

pub fn chain_id_or_name_to_named_chain(chain: &str) -> eyre::Result<NamedChain> {
let chain = chain.parse::<NamedChain>().map(Some).unwrap_or_else(|_| {
chain
.parse::<u64>()
.map(|chainid| NamedChain::try_from(chainid).ok())
.ok()
.flatten()
});
// NamedChain uses strum's kebab-case serialization, so convert to lowercase for name matching
let chain = chain
.to_lowercase()
.parse::<NamedChain>()
.map(Some)
.unwrap_or_else(|_| {
chain
.parse::<u64>()
.map(|chainid| NamedChain::try_from(chainid).ok())
.ok()
.flatten()
});
chain.ok_or_eyre("failed to parse chain")
}

Expand Down
2 changes: 2 additions & 0 deletions crates/nexum/tui/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ impl Config {
self.rpcs
.iter()
.filter_map(|(chain_name, url)| {
// NamedChain uses strum's kebab-case serialization, so convert to lowercase
chain_name
.to_lowercase()
.parse::<NamedChain>()
.map(|chain| (chain, url.clone()))
.inspect_err(|e| {
Expand Down
74 changes: 74 additions & 0 deletions crates/nexum/tui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,24 @@ impl App {
),
);
}
Prompt::PersonalSign(_, message, _) => {
let block = Block::bordered()
.padding(Padding::uniform(1))
.title(" Personal Sign ")
.title_alignment(HorizontalAlignment::Center)
.title_bottom("[A]ccept ───── [R]eject");
// Try to decode the message as UTF-8 for display
let text = String::from_utf8(message.to_vec())
.unwrap_or_else(|_| message.to_string());
let n_lines = text.lines().count().max(1);
frame.render_widget(
Paragraph::new(text).block(block),
frame.area().centered(
Constraint::Length(80),
Constraint::Length((n_lines as u16) + 4),
),
);
}
}
}
}
Expand Down Expand Up @@ -532,6 +550,27 @@ impl App {
}
_ => {}
},
Prompt::PersonalSign(_, _, _) => match key.code {
KeyCode::Esc | KeyCode::Char('r') | KeyCode::Char('R') => {
if let Some(Prompt::PersonalSign(signer_addr, message, sender)) =
self.prompt.take()
{
sender
.send((signer_addr, message, false))
.expect("failed to send personal_sign prompt response");
}
}
KeyCode::Char('a') | KeyCode::Char('A') => {
if let Some(Prompt::PersonalSign(signer_addr, message, sender)) =
self.prompt.take()
{
sender
.send((signer_addr, message, true))
.expect("failed to send personal_sign prompt response");
}
}
_ => {}
},
},
None => match (&self.active_tab, key.code) {
// global keybinds
Expand Down Expand Up @@ -699,6 +738,40 @@ impl App {
}
});
}
InteractiveRequest::PersonalSign(signer, message) => {
let (sender, receiver) = oneshot::channel::<(Address, Bytes, bool)>();
self.prompt_sender
.send(Prompt::PersonalSign(signer, message, sender))
.expect("failed to send personal_sign prompt");
let wallet = self.wallet_pane.clone();
tokio::spawn(async move {
let (signer, message, should_sign) =
receiver.await.expect("failed to receive personal_sign response");
if should_sign {
tracing::debug!("signing personal message now");
response_sender
.send(InteractiveResponse::PersonalSign(
wallet
.sign_message(Some(signer), &message)
.await
.map_err(|e| {
tracing::error!(?e, "failed to sign personal message");
let boxed_error: Box<dyn std::error::Error + Send + Sync> =
Box::new(e);
boxed_error
}),
))
.expect("failed to send personal_sign response");
} else {
tracing::debug!("personal_sign rejected");
response_sender
.send(InteractiveResponse::PersonalSign(Err(Box::new(
NexumTuiError::UserRejectedSigning,
))))
.expect("failed to send personal_sign response");
}
});
}
}
}
}
Expand All @@ -720,6 +793,7 @@ enum Prompt {
Box<TypedData>,
oneshot::Sender<(Address, Box<TypedData>, bool)>,
),
PersonalSign(Address, Bytes, oneshot::Sender<(Address, Bytes, bool)>),
}

#[derive(Debug)]
Expand Down
Loading