Skip to content

Commit 9f241c0

Browse files
authored
feat(tendermint): support unsigned txs for ledger's keplr extension (#2148)
This commit enables Tendermint transaction functionality for Ledger HW wallet using Keplr. However, HTLC transactions and swap operations are not yet supported, as Keplr has not implemented SIGN_MODE_TEXTUAL yet.
1 parent 6db5b9f commit 9f241c0

File tree

4 files changed

+151
-27
lines changed

4 files changed

+151
-27
lines changed

mm2src/coins/tendermint/htlc/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ pub(crate) struct TendermintHtlc {
8585
pub(crate) id: String,
8686

8787
/// Message payload to be sent.
88-
pub(crate) msg_payload: cosmrs::Any,
88+
pub(crate) msg_payload: Any,
8989
}
9090

9191
#[derive(prost::Message)]

mm2src/coins/tendermint/tendermint_coin.rs

+139-26
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ const MIN_TIME_LOCK: i64 = 50;
107107

108108
const ACCOUNT_SEQUENCE_ERR: &str = "incorrect account sequence";
109109

110+
pub struct SerializedUnsignedTx {
111+
tx_json: Json,
112+
body_bytes: Vec<u8>,
113+
}
114+
110115
type TendermintPrivKeyPolicy = PrivKeyPolicy<TendermintKeyPair>;
111116

112117
pub struct TendermintKeyPair {
@@ -346,6 +351,7 @@ pub struct TendermintCoinImpl {
346351
client: TendermintRpcClient,
347352
pub(crate) chain_registry_name: Option<String>,
348353
pub(crate) ctx: MmWeak,
354+
pub(crate) is_keplr_from_ledger: bool,
349355
}
350356

351357
#[derive(Clone)]
@@ -599,6 +605,7 @@ impl TendermintCommons for TendermintCoin {
599605
}
600606

601607
impl TendermintCoin {
608+
#[allow(clippy::too_many_arguments)]
602609
pub async fn init(
603610
ctx: &MmArc,
604611
ticker: String,
@@ -607,6 +614,7 @@ impl TendermintCoin {
607614
rpc_urls: Vec<String>,
608615
tx_history: bool,
609616
activation_policy: TendermintActivationPolicy,
617+
is_keplr_from_ledger: bool,
610618
) -> MmResult<Self, TendermintInitError> {
611619
if rpc_urls.is_empty() {
612620
return MmError::err(TendermintInitError {
@@ -671,6 +679,7 @@ impl TendermintCoin {
671679
client: TendermintRpcClient(AsyncMutex::new(client_impl)),
672680
chain_registry_name: protocol_info.chain_registry_name,
673681
ctx: ctx.weak(),
682+
is_keplr_from_ledger,
674683
})))
675684
}
676685

@@ -868,19 +877,14 @@ impl TendermintCoin {
868877
let ctx = try_tx_s!(MmArc::from_weak(&self.ctx).ok_or(ERRL!("ctx must be initialized already")));
869878

870879
let account_info = try_tx_s!(self.account_info(&self.account_id).await);
871-
let sign_doc = try_tx_s!(self.any_to_sign_doc(account_info, tx_payload, fee, timeout_height, memo));
872-
873-
let unsigned_tx = json!({
874-
"sign_doc": {
875-
"body_bytes": sign_doc.body_bytes,
876-
"auth_info_bytes": sign_doc.auth_info_bytes,
877-
"chain_id": sign_doc.chain_id,
878-
"account_number": sign_doc.account_number,
879-
}
880-
});
880+
let SerializedUnsignedTx { tx_json, body_bytes } = if self.is_keplr_from_ledger {
881+
try_tx_s!(self.any_to_legacy_amino_json(account_info, tx_payload, fee, timeout_height, memo))
882+
} else {
883+
try_tx_s!(self.any_to_serialized_sign_doc(account_info, tx_payload, fee, timeout_height, memo))
884+
};
881885

882886
let data: TxHashData = try_tx_s!(ctx
883-
.ask_for_data(&format!("TX_HASH:{}", self.ticker()), unsigned_tx, timeout)
887+
.ask_for_data(&format!("TX_HASH:{}", self.ticker()), tx_json, timeout)
884888
.await
885889
.map_err(|e| ERRL!("{}", e)));
886890

@@ -892,7 +896,7 @@ impl TendermintCoin {
892896
signatures: tx.signatures,
893897
};
894898

895-
if sign_doc.body_bytes != tx_raw_inner.body_bytes {
899+
if body_bytes != tx_raw_inner.body_bytes {
896900
return Err(crate::TransactionErr::Plain(ERRL!(
897901
"Unsigned transaction don't match with the externally provided transaction."
898902
)));
@@ -1166,18 +1170,13 @@ impl TendermintCoin {
11661170
hex::encode_upper(hash.as_slice()),
11671171
))
11681172
} else {
1169-
let sign_doc = self.any_to_sign_doc(account_info, message, fee, timeout_height, memo)?;
1170-
1171-
let tx = json!({
1172-
"sign_doc": {
1173-
"body_bytes": sign_doc.body_bytes,
1174-
"auth_info_bytes": sign_doc.auth_info_bytes,
1175-
"chain_id": sign_doc.chain_id,
1176-
"account_number": sign_doc.account_number,
1177-
}
1178-
});
1173+
let SerializedUnsignedTx { tx_json, .. } = if self.is_keplr_from_ledger {
1174+
self.any_to_legacy_amino_json(account_info, message, fee, timeout_height, memo)
1175+
} else {
1176+
self.any_to_serialized_sign_doc(account_info, message, fee, timeout_height, memo)
1177+
}?;
11791178

1180-
Ok(TransactionData::Unsigned(tx))
1179+
Ok(TransactionData::Unsigned(tx_json))
11811180
}
11821181
}
11831182

@@ -1253,18 +1252,116 @@ impl TendermintCoin {
12531252
sign_doc.sign(&signkey)
12541253
}
12551254

1256-
pub(super) fn any_to_sign_doc(
1255+
pub(super) fn any_to_serialized_sign_doc(
12571256
&self,
12581257
account_info: BaseAccount,
12591258
tx_payload: Any,
12601259
fee: Fee,
12611260
timeout_height: u64,
12621261
memo: String,
1263-
) -> cosmrs::Result<SignDoc> {
1262+
) -> cosmrs::Result<SerializedUnsignedTx> {
12641263
let tx_body = tx::Body::new(vec![tx_payload], memo, timeout_height as u32);
12651264
let pubkey = self.activation_policy.public_key()?.into();
12661265
let auth_info = SignerInfo::single_direct(Some(pubkey), account_info.sequence).auth_info(fee);
1267-
SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)
1266+
let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account_info.account_number)?;
1267+
1268+
let tx_json = json!({
1269+
"sign_doc": {
1270+
"body_bytes": sign_doc.body_bytes,
1271+
"auth_info_bytes": sign_doc.auth_info_bytes,
1272+
"chain_id": sign_doc.chain_id,
1273+
"account_number": sign_doc.account_number,
1274+
}
1275+
});
1276+
1277+
Ok(SerializedUnsignedTx {
1278+
tx_json,
1279+
body_bytes: sign_doc.body_bytes,
1280+
})
1281+
}
1282+
1283+
/// This should only be used for Keplr from Ledger!
1284+
/// When using Keplr from Ledger, they don't accept `SING_MODE_DIRECT` transactions.
1285+
///
1286+
/// Visit https://docs.cosmos.network/main/build/architecture/adr-050-sign-mode-textual#context for more context.
1287+
pub(super) fn any_to_legacy_amino_json(
1288+
&self,
1289+
account_info: BaseAccount,
1290+
tx_payload: Any,
1291+
fee: Fee,
1292+
timeout_height: u64,
1293+
memo: String,
1294+
) -> cosmrs::Result<SerializedUnsignedTx> {
1295+
const MSG_SEND_TYPE_URL: &str = "/cosmos.bank.v1beta1.MsgSend";
1296+
const LEDGER_MSG_SEND_TYPE_URL: &str = "cosmos-sdk/MsgSend";
1297+
1298+
// Ledger's keplr works as wallet-only, so `MsgSend` support is enough for now.
1299+
if tx_payload.type_url != MSG_SEND_TYPE_URL {
1300+
return Err(ErrorReport::new(io::Error::new(
1301+
io::ErrorKind::Unsupported,
1302+
format!(
1303+
"Signing mode `SIGN_MODE_LEGACY_AMINO_JSON` is not supported for '{}' transaction type.",
1304+
tx_payload.type_url
1305+
),
1306+
)));
1307+
}
1308+
1309+
let msg_send = MsgSend::from_any(&tx_payload)?;
1310+
let timeout_height = u32::try_from(timeout_height)?;
1311+
let original_tx_type_url = tx_payload.type_url.clone();
1312+
let body_bytes = tx::Body::new(vec![tx_payload], &memo, timeout_height).into_bytes()?;
1313+
1314+
let amount: Vec<Json> = msg_send
1315+
.amount
1316+
.into_iter()
1317+
.map(|t| {
1318+
json!( {
1319+
"denom": t.denom,
1320+
// Numbers needs to be converted into string type.
1321+
// Ref: https://github.com/cosmos/ledger-cosmos/blob/c707129e59f6e0f07ad67161a6b75e8951af063c/docs/TXSPEC.md#json-format
1322+
"amount": t.amount.to_string(),
1323+
})
1324+
})
1325+
.collect();
1326+
1327+
let msg = json!({
1328+
"type": LEDGER_MSG_SEND_TYPE_URL,
1329+
"value": json!({
1330+
"from_address": msg_send.from_address.to_string(),
1331+
"to_address": msg_send.to_address.to_string(),
1332+
"amount": amount,
1333+
})
1334+
});
1335+
1336+
let fee_amount: Vec<Json> = fee
1337+
.amount
1338+
.into_iter()
1339+
.map(|t| {
1340+
json!( {
1341+
"denom": t.denom,
1342+
// Numbers needs to be converted into string type.
1343+
// Ref: https://github.com/cosmos/ledger-cosmos/blob/c707129e59f6e0f07ad67161a6b75e8951af063c/docs/TXSPEC.md#json-format
1344+
"amount": t.amount.to_string(),
1345+
})
1346+
})
1347+
.collect();
1348+
1349+
let tx_json = serde_json::json!({
1350+
"legacy_amino_json": {
1351+
"account_number": account_info.account_number.to_string(),
1352+
"chain_id": self.chain_id.to_string(),
1353+
"fee": {
1354+
"amount": fee_amount,
1355+
"gas": fee.gas_limit.to_string()
1356+
},
1357+
"memo": memo,
1358+
"msgs": [msg],
1359+
"sequence": account_info.sequence.to_string(),
1360+
},
1361+
"original_tx_type_url": original_tx_type_url,
1362+
});
1363+
1364+
Ok(SerializedUnsignedTx { tx_json, body_bytes })
12681365
}
12691366

12701367
pub fn add_activated_token_info(&self, ticker: String, decimals: u8, denom: Denom) {
@@ -2024,6 +2121,13 @@ pub async fn get_ibc_chain_list() -> IBCChainRegistriesResult {
20242121
impl MmCoin for TendermintCoin {
20252122
fn is_asset_chain(&self) -> bool { false }
20262123

2124+
fn wallet_only(&self, ctx: &MmArc) -> bool {
2125+
let coin_conf = crate::coin_conf(ctx, self.ticker());
2126+
let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false);
2127+
2128+
wallet_only_conf || self.is_keplr_from_ledger
2129+
}
2130+
20272131
fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) }
20282132

20292133
fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut {
@@ -3214,6 +3318,7 @@ pub mod tendermint_coin_tests {
32143318
rpc_urls,
32153319
false,
32163320
activation_policy,
3321+
false,
32173322
))
32183323
.unwrap();
32193324

@@ -3339,6 +3444,7 @@ pub mod tendermint_coin_tests {
33393444
rpc_urls,
33403445
false,
33413446
activation_policy,
3447+
false,
33423448
))
33433449
.unwrap();
33443450

@@ -3401,6 +3507,7 @@ pub mod tendermint_coin_tests {
34013507
rpc_urls,
34023508
false,
34033509
activation_policy,
3510+
false,
34043511
))
34053512
.unwrap();
34063513

@@ -3474,6 +3581,7 @@ pub mod tendermint_coin_tests {
34743581
rpc_urls,
34753582
false,
34763583
activation_policy,
3584+
false,
34773585
))
34783586
.unwrap();
34793587

@@ -3670,6 +3778,7 @@ pub mod tendermint_coin_tests {
36703778
rpc_urls,
36713779
false,
36723780
activation_policy,
3781+
false,
36733782
))
36743783
.unwrap();
36753784

@@ -3752,6 +3861,7 @@ pub mod tendermint_coin_tests {
37523861
rpc_urls,
37533862
false,
37543863
activation_policy,
3864+
false,
37553865
))
37563866
.unwrap();
37573867

@@ -3827,6 +3937,7 @@ pub mod tendermint_coin_tests {
38273937
rpc_urls,
38283938
false,
38293939
activation_policy,
3940+
false,
38303941
))
38313942
.unwrap();
38323943

@@ -3898,6 +4009,7 @@ pub mod tendermint_coin_tests {
38984009
rpc_urls,
38994010
false,
39004011
activation_policy,
4012+
false,
39014013
))
39024014
.unwrap();
39034015

@@ -3952,6 +4064,7 @@ pub mod tendermint_coin_tests {
39524064
rpc_urls,
39534065
false,
39544066
activation_policy,
4067+
false,
39554068
))
39564069
.unwrap();
39574070

mm2src/coins/tendermint/tendermint_token.rs

+7
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,13 @@ impl MarketCoinOps for TendermintToken {
475475
impl MmCoin for TendermintToken {
476476
fn is_asset_chain(&self) -> bool { false }
477477

478+
fn wallet_only(&self, ctx: &MmArc) -> bool {
479+
let coin_conf = crate::coin_conf(ctx, self.ticker());
480+
let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false);
481+
482+
wallet_only_conf || self.platform_coin.is_keplr_from_ledger
483+
}
484+
478485
fn spawner(&self) -> CoinFutSpawner { CoinFutSpawner::new(&self.abortable_system) }
479486

480487
fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut {

mm2src/coins_activation/src/tendermint_with_assets_activation.rs

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ pub struct TendermintActivationParams {
5252
#[serde(default)]
5353
#[serde(deserialize_with = "deserialize_account_public_key")]
5454
with_pubkey: Option<TendermintPublicKey>,
55+
#[serde(default)]
56+
is_keplr_from_ledger: bool,
5557
}
5658

5759
fn deserialize_account_public_key<'de, D>(deserializer: D) -> Result<Option<TendermintPublicKey>, D::Error>
@@ -234,6 +236,7 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin {
234236
protocol_conf: Self::PlatformProtocolInfo,
235237
) -> Result<Self, MmError<Self::ActivationError>> {
236238
let conf = TendermintConf::try_from_json(&ticker, coin_conf)?;
239+
let is_keplr_from_ledger = activation_request.is_keplr_from_ledger && activation_request.with_pubkey.is_some();
237240

238241
let activation_policy = if let Some(pubkey) = activation_request.with_pubkey {
239242
if ctx.is_watcher() || ctx.use_watchers() {
@@ -265,6 +268,7 @@ impl PlatformCoinWithTokensActivationOps for TendermintCoin {
265268
activation_request.rpc_urls,
266269
activation_request.tx_history,
267270
activation_policy,
271+
is_keplr_from_ledger,
268272
)
269273
.await
270274
}

0 commit comments

Comments
 (0)