Skip to content

Commit 957551c

Browse files
committed
Merge branch 'yuji/ibc-transfer-cmd' (#626)
* yuji/ibc-transfer-cmd: fix sub-prefix replace now func fix timeout timestamp fix timeout height fix timeout add ibc-transfer command
2 parents daeccae + 40666b1 commit 957551c

File tree

4 files changed

+234
-1
lines changed

4 files changed

+234
-1
lines changed

apps/src/bin/anoma-client/cli.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub async fn main() -> Result<()> {
1818
Sub::TxTransfer(TxTransfer(args)) => {
1919
tx::submit_transfer(ctx, args).await;
2020
}
21+
Sub::TxIbcTransfer(TxIbcTransfer(args)) => {
22+
tx::submit_ibc_transfer(ctx, args).await;
23+
}
2124
Sub::TxUpdateVp(TxUpdateVp(args)) => {
2225
tx::submit_update_vp(ctx, args).await;
2326
}

apps/src/bin/anoma/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ fn handle_command(cmd: cli::cmds::Anoma, raw_sub_cmd: String) -> Result<()> {
4545
cli::cmds::Anoma::Client(_)
4646
| cli::cmds::Anoma::TxCustom(_)
4747
| cli::cmds::Anoma::TxTransfer(_)
48+
| cli::cmds::Anoma::TxIbcTransfer(_)
4849
| cli::cmds::Anoma::TxUpdateVp(_)
4950
| cli::cmds::Anoma::TxInitNft(_)
5051
| cli::cmds::Anoma::TxMintNft(_)

apps/src/lib/cli.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub mod cmds {
4646
// Inlined commands from the client.
4747
TxCustom(TxCustom),
4848
TxTransfer(TxTransfer),
49+
TxIbcTransfer(TxIbcTransfer),
4950
TxUpdateVp(TxUpdateVp),
5051
TxInitNft(TxInitNft),
5152
TxMintNft(TxMintNft),
@@ -61,6 +62,7 @@ pub mod cmds {
6162
.subcommand(Ledger::def())
6263
.subcommand(TxCustom::def())
6364
.subcommand(TxTransfer::def())
65+
.subcommand(TxIbcTransfer::def())
6466
.subcommand(TxUpdateVp::def())
6567
.subcommand(TxInitNft::def())
6668
.subcommand(TxMintNft::def())
@@ -75,6 +77,8 @@ pub mod cmds {
7577
let ledger = SubCmd::parse(matches).map(Self::Ledger);
7678
let tx_custom = SubCmd::parse(matches).map(Self::TxCustom);
7779
let tx_transfer = SubCmd::parse(matches).map(Self::TxTransfer);
80+
let tx_ibc_transfer =
81+
SubCmd::parse(matches).map(Self::TxIbcTransfer);
7882
let tx_update_vp = SubCmd::parse(matches).map(Self::TxUpdateVp);
7983
let tx_nft_create = SubCmd::parse(matches).map(Self::TxInitNft);
8084
let tx_nft_mint = SubCmd::parse(matches).map(Self::TxMintNft);
@@ -87,6 +91,7 @@ pub mod cmds {
8791
.or(ledger)
8892
.or(tx_custom)
8993
.or(tx_transfer)
94+
.or(tx_ibc_transfer)
9095
.or(tx_update_vp)
9196
.or(tx_nft_create)
9297
.or(tx_nft_mint)
@@ -152,6 +157,7 @@ pub mod cmds {
152157
// Simple transactions
153158
.subcommand(TxCustom::def().display_order(1))
154159
.subcommand(TxTransfer::def().display_order(1))
160+
.subcommand(TxIbcTransfer::def().display_order(1))
155161
.subcommand(TxUpdateVp::def().display_order(1))
156162
.subcommand(TxInitAccount::def().display_order(1))
157163
.subcommand(TxInitValidator::def().display_order(1))
@@ -184,6 +190,7 @@ pub mod cmds {
184190
use AnomaClientWithContext::*;
185191
let tx_custom = Self::parse_with_ctx(matches, TxCustom);
186192
let tx_transfer = Self::parse_with_ctx(matches, TxTransfer);
193+
let tx_ibc_transfer = Self::parse_with_ctx(matches, TxIbcTransfer);
187194
let tx_update_vp = Self::parse_with_ctx(matches, TxUpdateVp);
188195
let tx_init_account = Self::parse_with_ctx(matches, TxInitAccount);
189196
let tx_init_validator =
@@ -213,6 +220,7 @@ pub mod cmds {
213220
let utils = SubCmd::parse(matches).map(Self::WithoutContext);
214221
tx_custom
215222
.or(tx_transfer)
223+
.or(tx_ibc_transfer)
216224
.or(tx_update_vp)
217225
.or(tx_init_account)
218226
.or(tx_init_validator)
@@ -271,6 +279,7 @@ pub mod cmds {
271279
// Ledger cmds
272280
TxCustom(TxCustom),
273281
TxTransfer(TxTransfer),
282+
TxIbcTransfer(TxIbcTransfer),
274283
QueryResult(QueryResult),
275284
TxUpdateVp(TxUpdateVp),
276285
TxInitAccount(TxInitAccount),
@@ -794,6 +803,25 @@ pub mod cmds {
794803
}
795804
}
796805

806+
#[derive(Clone, Debug)]
807+
pub struct TxIbcTransfer(pub args::TxIbcTransfer);
808+
809+
impl SubCmd for TxIbcTransfer {
810+
const CMD: &'static str = "ibc-transfer";
811+
812+
fn parse(matches: &ArgMatches) -> Option<Self> {
813+
matches.subcommand_matches(Self::CMD).map(|matches| {
814+
TxIbcTransfer(args::TxIbcTransfer::parse(matches))
815+
})
816+
}
817+
818+
fn def() -> App {
819+
App::new(Self::CMD)
820+
.about("Send a signed IBC transfer transaction.")
821+
.add_args::<args::TxIbcTransfer>()
822+
}
823+
}
824+
797825
#[derive(Clone, Debug)]
798826
pub struct TxUpdateVp(pub args::TxUpdateVp);
799827

@@ -1248,6 +1276,7 @@ pub mod args {
12481276
use std::path::PathBuf;
12491277
use std::str::FromStr;
12501278

1279+
use namada::ibc::core::ics24_host::identifier::{ChannelId, PortId};
12511280
use namada::types::address::Address;
12521281
use namada::types::chain::{ChainId, ChainIdPrefix};
12531282
use namada::types::governance::ProposalVote;
@@ -1281,6 +1310,7 @@ pub mod args {
12811310
const CHAIN_ID: Arg<ChainId> = arg("chain-id");
12821311
const CHAIN_ID_OPT: ArgOpt<ChainId> = CHAIN_ID.opt();
12831312
const CHAIN_ID_PREFIX: Arg<ChainIdPrefix> = arg("chain-prefix");
1313+
const CHANNEL_ID: Arg<ChannelId> = arg("channel-id");
12841314
const CODE_PATH: Arg<PathBuf> = arg("code-path");
12851315
const CODE_PATH_OPT: ArgOpt<PathBuf> = CODE_PATH.opt();
12861316
const CONSENSUS_TIMEOUT_COMMIT: ArgDefault<Timeout> = arg_default(
@@ -1318,6 +1348,10 @@ pub mod args {
13181348
const NET_ADDRESS: Arg<SocketAddr> = arg("net-address");
13191349
const NFT_ADDRESS: Arg<Address> = arg("nft-address");
13201350
const OWNER: ArgOpt<WalletAddress> = arg_opt("owner");
1351+
const PORT_ID: ArgDefault<PortId> = arg_default(
1352+
"port-id",
1353+
DefaultFn(|| PortId::from_str("transfer").unwrap()),
1354+
);
13211355
const PROPOSAL_OFFLINE: ArgFlag = flag("offline");
13221356
const PROTOCOL_KEY: ArgOpt<WalletPublicKey> = arg_opt("protocol-key");
13231357
const PRE_GENESIS_PATH: ArgOpt<PathBuf> = arg_opt("pre-genesis-path");
@@ -1328,6 +1362,7 @@ pub mod args {
13281362
const RAW_ADDRESS: Arg<Address> = arg("address");
13291363
const RAW_ADDRESS_OPT: ArgOpt<Address> = RAW_ADDRESS.opt();
13301364
const RAW_PUBLIC_KEY_OPT: ArgOpt<common::PublicKey> = arg_opt("public-key");
1365+
const RECEIVER: Arg<String> = arg("receiver");
13311366
const REWARDS_CODE_PATH: ArgOpt<PathBuf> = arg_opt("rewards-code-path");
13321367
const REWARDS_KEY: ArgOpt<WalletPublicKey> = arg_opt("rewards-key");
13331368
const SCHEME: ArgDefault<SchemeType> =
@@ -1340,6 +1375,8 @@ pub mod args {
13401375
const STORAGE_KEY: Arg<storage::Key> = arg("storage-key");
13411376
const SUB_PREFIX: ArgOpt<String> = arg_opt("sub-prefix");
13421377
const TARGET: Arg<WalletAddress> = arg("target");
1378+
const TIMEOUT_HEIGHT: ArgOpt<u64> = arg_opt("timeout-height");
1379+
const TIMEOUT_SEC_OFFSET: ArgOpt<u64> = arg_opt("timeout-sec-offset");
13431380
const TOKEN_OPT: ArgOpt<WalletAddress> = TOKEN.opt();
13441381
const TOKEN: Arg<WalletAddress> = arg("token");
13451382
const TX_HASH: Arg<String> = arg("tx-hash");
@@ -1515,6 +1552,80 @@ pub mod args {
15151552
}
15161553
}
15171554

1555+
/// IBC transfer transaction arguments
1556+
#[derive(Clone, Debug)]
1557+
pub struct TxIbcTransfer {
1558+
/// Common tx arguments
1559+
pub tx: Tx,
1560+
/// Transfer source address
1561+
pub source: WalletAddress,
1562+
/// Transfer target address
1563+
pub receiver: String,
1564+
/// Transferred token address
1565+
pub token: WalletAddress,
1566+
/// Transferred token address
1567+
pub sub_prefix: Option<String>,
1568+
/// Transferred token amount
1569+
pub amount: token::Amount,
1570+
/// Port ID
1571+
pub port_id: PortId,
1572+
/// Channel ID
1573+
pub channel_id: ChannelId,
1574+
/// Timeout height of the destination chain
1575+
pub timeout_height: Option<u64>,
1576+
/// Timeout timestamp offset
1577+
pub timeout_sec_offset: Option<u64>,
1578+
}
1579+
1580+
impl Args for TxIbcTransfer {
1581+
fn parse(matches: &ArgMatches) -> Self {
1582+
let tx = Tx::parse(matches);
1583+
let source = SOURCE.parse(matches);
1584+
let receiver = RECEIVER.parse(matches);
1585+
let token = TOKEN.parse(matches);
1586+
let sub_prefix = SUB_PREFIX.parse(matches);
1587+
let amount = AMOUNT.parse(matches);
1588+
let port_id = PORT_ID.parse(matches);
1589+
let channel_id = CHANNEL_ID.parse(matches);
1590+
let timeout_height = TIMEOUT_HEIGHT.parse(matches);
1591+
let timeout_sec_offset = TIMEOUT_SEC_OFFSET.parse(matches);
1592+
Self {
1593+
tx,
1594+
source,
1595+
receiver,
1596+
token,
1597+
sub_prefix,
1598+
amount,
1599+
port_id,
1600+
channel_id,
1601+
timeout_height,
1602+
timeout_sec_offset,
1603+
}
1604+
}
1605+
1606+
fn def(app: App) -> App {
1607+
app.add_args::<Tx>()
1608+
.arg(SOURCE.def().about(
1609+
"The source account address. The source's key is used to \
1610+
produce the signature.",
1611+
))
1612+
.arg(RECEIVER.def().about(
1613+
"The receiver address on the destination chain as string.",
1614+
))
1615+
.arg(TOKEN.def().about("The transfer token."))
1616+
.arg(SUB_PREFIX.def().about("The token's sub prefix."))
1617+
.arg(AMOUNT.def().about("The amount to transfer in decimal."))
1618+
.arg(PORT_ID.def().about("The port ID."))
1619+
.arg(CHANNEL_ID.def().about("The channel ID."))
1620+
.arg(
1621+
TIMEOUT_HEIGHT
1622+
.def()
1623+
.about("The timeout height of the destination chain."),
1624+
)
1625+
.arg(TIMEOUT_SEC_OFFSET.def().about("The timeout as seconds."))
1626+
}
1627+
}
1628+
15181629
/// Transaction to initialize a new account
15191630
#[derive(Clone, Debug)]
15201631
pub struct TxInitAccount {

apps/src/lib/client/tx.rs

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ use async_std::io::prelude::WriteExt;
88
use async_std::io::{self};
99
use borsh::BorshSerialize;
1010
use itertools::Either::*;
11+
use namada::ibc::applications::ics20_fungible_token_transfer::msgs::transfer::MsgTransfer;
12+
use namada::ibc::signer::Signer;
13+
use namada::ibc::timestamp::Timestamp as IbcTimestamp;
14+
use namada::ibc::tx_msg::Msg;
15+
use namada::ibc::Height as IbcHeight;
16+
use namada::ibc_proto::cosmos::base::v1beta1::Coin;
1117
use namada::ledger::governance::storage as gov_storage;
1218
use namada::ledger::pos::{BondId, Bonds, Unbonds};
1319
use namada::proto::Tx;
@@ -17,7 +23,8 @@ use namada::types::governance::{
1723
};
1824
use namada::types::key::*;
1925
use namada::types::nft::{self, Nft, NftToken};
20-
use namada::types::storage::Epoch;
26+
use namada::types::storage::{Epoch, RESERVED_ADDRESS_PREFIX};
27+
use namada::types::time::DateTimeUtc;
2128
use namada::types::transaction::governance::{
2229
InitProposalData, VoteProposalData,
2330
};
@@ -49,6 +56,7 @@ const TX_INIT_PROPOSAL: &str = "tx_init_proposal.wasm";
4956
const TX_VOTE_PROPOSAL: &str = "tx_vote_proposal.wasm";
5057
const TX_UPDATE_VP_WASM: &str = "tx_update_vp.wasm";
5158
const TX_TRANSFER_WASM: &str = "tx_transfer.wasm";
59+
const TX_IBC_WASM: &str = "tx_ibc.wasm";
5260
const TX_INIT_NFT: &str = "tx_init_nft.wasm";
5361
const TX_MINT_NFT: &str = "tx_mint_nft.wasm";
5462
const VP_USER_WASM: &str = "vp_user.wasm";
@@ -465,6 +473,116 @@ pub async fn submit_transfer(ctx: Context, args: args::TxTransfer) {
465473
process_tx(ctx, &args.tx, tx, Some(&args.source)).await;
466474
}
467475

476+
pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) {
477+
let source = ctx.get(&args.source);
478+
// Check that the source address exists on chain
479+
let source_exists =
480+
rpc::known_address(&source, args.tx.ledger_address.clone()).await;
481+
if !source_exists {
482+
eprintln!("The source address {} doesn't exist on chain.", source);
483+
if !args.tx.force {
484+
safe_exit(1)
485+
}
486+
}
487+
488+
// We cannot check the receiver
489+
490+
let token = ctx.get(&args.token);
491+
// Check that the token address exists on chain
492+
let token_exists =
493+
rpc::known_address(&token, args.tx.ledger_address.clone()).await;
494+
if !token_exists {
495+
eprintln!("The token address {} doesn't exist on chain.", token);
496+
if !args.tx.force {
497+
safe_exit(1)
498+
}
499+
}
500+
// Check source balance
501+
let (sub_prefix, balance_key) = match args.sub_prefix {
502+
Some(sub_prefix) => {
503+
let sub_prefix = storage::Key::parse(sub_prefix).unwrap();
504+
let prefix = token::multitoken_balance_prefix(&token, &sub_prefix);
505+
(
506+
Some(sub_prefix),
507+
token::multitoken_balance_key(&prefix, &source),
508+
)
509+
}
510+
None => (None, token::balance_key(&token, &source)),
511+
};
512+
let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap();
513+
match rpc::query_storage_value::<token::Amount>(&client, &balance_key).await
514+
{
515+
Some(balance) => {
516+
if balance < args.amount {
517+
eprintln!(
518+
"The balance of the source {} of token {} is lower than \
519+
the amount to be transferred. Amount to transfer is {} \
520+
and the balance is {}.",
521+
source, token, args.amount, balance
522+
);
523+
if !args.tx.force {
524+
safe_exit(1)
525+
}
526+
}
527+
}
528+
None => {
529+
eprintln!(
530+
"No balance found for the source {} of token {}",
531+
source, token
532+
);
533+
if !args.tx.force {
534+
safe_exit(1)
535+
}
536+
}
537+
}
538+
let tx_code = ctx.read_wasm(TX_IBC_WASM);
539+
540+
let denom = match sub_prefix {
541+
// To parse IbcToken address, remove the address prefix
542+
Some(sp) => sp.to_string().replace(RESERVED_ADDRESS_PREFIX, ""),
543+
None => token.to_string(),
544+
};
545+
let token = Some(Coin {
546+
denom,
547+
amount: args.amount.to_string(),
548+
});
549+
550+
// this height should be that of the destination chain, not this chain
551+
let timeout_height = match args.timeout_height {
552+
Some(h) => IbcHeight::new(0, h),
553+
None => IbcHeight::zero(),
554+
};
555+
556+
let now: namada::tendermint::Time = DateTimeUtc::now().try_into().unwrap();
557+
let now: IbcTimestamp = now.into();
558+
let timeout_timestamp = if let Some(offset) = args.timeout_sec_offset {
559+
(now + Duration::new(offset, 0)).unwrap()
560+
} else if timeout_height.is_zero() {
561+
// we cannot set 0 to both the height and the timestamp
562+
(now + Duration::new(3600, 0)).unwrap()
563+
} else {
564+
IbcTimestamp::none()
565+
};
566+
567+
let msg = MsgTransfer {
568+
source_port: args.port_id,
569+
source_channel: args.channel_id,
570+
token,
571+
sender: Signer::new(source.to_string()),
572+
receiver: Signer::new(args.receiver),
573+
timeout_height,
574+
timeout_timestamp,
575+
};
576+
tracing::debug!("IBC transfer message {:?}", msg);
577+
let any_msg = msg.to_any();
578+
let mut data = vec![];
579+
prost::Message::encode(&any_msg, &mut data)
580+
.expect("Encoding tx data shouldn't fail");
581+
582+
let tx = Tx::new(tx_code, Some(data));
583+
process_tx(ctx, &args.tx, tx, Some(&args.source)).await;
584+
}
585+
468586
pub async fn submit_init_nft(ctx: Context, args: args::NftCreate) {
469587
let file = File::open(&args.nft_data).expect("File must exist.");
470588
let nft: Nft = serde_json::from_reader(file)

0 commit comments

Comments
 (0)