Skip to content

Commit

Permalink
Merge branch 'yuji/ibc-transfer-cmd' (#626)
Browse files Browse the repository at this point in the history
* yuji/ibc-transfer-cmd:
  fix sub-prefix
  replace now func
  fix timeout timestamp
  fix timeout height
  fix timeout
  add ibc-transfer command
  • Loading branch information
tzemanovic authored and juped committed Nov 14, 2022
2 parents fc91d99 + 7008dec commit 4602c1e
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .changelog/unreleased/features/626-ibc-transfer-cmd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Add client command 'ibc-transfer'.
([#626](https://github.com/anoma/namada/pull/626))
3 changes: 3 additions & 0 deletions apps/src/bin/anoma-client/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ pub async fn main() -> Result<()> {
Sub::TxTransfer(TxTransfer(args)) => {
tx::submit_transfer(ctx, args).await;
}
Sub::TxIbcTransfer(TxIbcTransfer(args)) => {
tx::submit_ibc_transfer(ctx, args).await;
}
Sub::TxUpdateVp(TxUpdateVp(args)) => {
tx::submit_update_vp(ctx, args).await;
}
Expand Down
1 change: 1 addition & 0 deletions apps/src/bin/anoma/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ fn handle_command(cmd: cli::cmds::Anoma, raw_sub_cmd: String) -> Result<()> {
cli::cmds::Anoma::Client(_)
| cli::cmds::Anoma::TxCustom(_)
| cli::cmds::Anoma::TxTransfer(_)
| cli::cmds::Anoma::TxIbcTransfer(_)
| cli::cmds::Anoma::TxUpdateVp(_)
| cli::cmds::Anoma::TxInitNft(_)
| cli::cmds::Anoma::TxMintNft(_)
Expand Down
111 changes: 111 additions & 0 deletions apps/src/lib/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub mod cmds {
// Inlined commands from the client.
TxCustom(TxCustom),
TxTransfer(TxTransfer),
TxIbcTransfer(TxIbcTransfer),
TxUpdateVp(TxUpdateVp),
TxInitNft(TxInitNft),
TxMintNft(TxMintNft),
Expand All @@ -61,6 +62,7 @@ pub mod cmds {
.subcommand(Ledger::def())
.subcommand(TxCustom::def())
.subcommand(TxTransfer::def())
.subcommand(TxIbcTransfer::def())
.subcommand(TxUpdateVp::def())
.subcommand(TxInitNft::def())
.subcommand(TxMintNft::def())
Expand All @@ -75,6 +77,8 @@ pub mod cmds {
let ledger = SubCmd::parse(matches).map(Self::Ledger);
let tx_custom = SubCmd::parse(matches).map(Self::TxCustom);
let tx_transfer = SubCmd::parse(matches).map(Self::TxTransfer);
let tx_ibc_transfer =
SubCmd::parse(matches).map(Self::TxIbcTransfer);
let tx_update_vp = SubCmd::parse(matches).map(Self::TxUpdateVp);
let tx_nft_create = SubCmd::parse(matches).map(Self::TxInitNft);
let tx_nft_mint = SubCmd::parse(matches).map(Self::TxMintNft);
Expand All @@ -87,6 +91,7 @@ pub mod cmds {
.or(ledger)
.or(tx_custom)
.or(tx_transfer)
.or(tx_ibc_transfer)
.or(tx_update_vp)
.or(tx_nft_create)
.or(tx_nft_mint)
Expand Down Expand Up @@ -152,6 +157,7 @@ pub mod cmds {
// Simple transactions
.subcommand(TxCustom::def().display_order(1))
.subcommand(TxTransfer::def().display_order(1))
.subcommand(TxIbcTransfer::def().display_order(1))
.subcommand(TxUpdateVp::def().display_order(1))
.subcommand(TxInitAccount::def().display_order(1))
.subcommand(TxInitValidator::def().display_order(1))
Expand Down Expand Up @@ -184,6 +190,7 @@ pub mod cmds {
use AnomaClientWithContext::*;
let tx_custom = Self::parse_with_ctx(matches, TxCustom);
let tx_transfer = Self::parse_with_ctx(matches, TxTransfer);
let tx_ibc_transfer = Self::parse_with_ctx(matches, TxIbcTransfer);
let tx_update_vp = Self::parse_with_ctx(matches, TxUpdateVp);
let tx_init_account = Self::parse_with_ctx(matches, TxInitAccount);
let tx_init_validator =
Expand Down Expand Up @@ -213,6 +220,7 @@ pub mod cmds {
let utils = SubCmd::parse(matches).map(Self::WithoutContext);
tx_custom
.or(tx_transfer)
.or(tx_ibc_transfer)
.or(tx_update_vp)
.or(tx_init_account)
.or(tx_init_validator)
Expand Down Expand Up @@ -271,6 +279,7 @@ pub mod cmds {
// Ledger cmds
TxCustom(TxCustom),
TxTransfer(TxTransfer),
TxIbcTransfer(TxIbcTransfer),
QueryResult(QueryResult),
TxUpdateVp(TxUpdateVp),
TxInitAccount(TxInitAccount),
Expand Down Expand Up @@ -794,6 +803,25 @@ pub mod cmds {
}
}

#[derive(Clone, Debug)]
pub struct TxIbcTransfer(pub args::TxIbcTransfer);

impl SubCmd for TxIbcTransfer {
const CMD: &'static str = "ibc-transfer";

fn parse(matches: &ArgMatches) -> Option<Self> {
matches.subcommand_matches(Self::CMD).map(|matches| {
TxIbcTransfer(args::TxIbcTransfer::parse(matches))
})
}

fn def() -> App {
App::new(Self::CMD)
.about("Send a signed IBC transfer transaction.")
.add_args::<args::TxIbcTransfer>()
}
}

#[derive(Clone, Debug)]
pub struct TxUpdateVp(pub args::TxUpdateVp);

Expand Down Expand Up @@ -1248,6 +1276,7 @@ pub mod args {
use std::path::PathBuf;
use std::str::FromStr;

use namada::ibc::core::ics24_host::identifier::{ChannelId, PortId};
use namada::types::address::Address;
use namada::types::chain::{ChainId, ChainIdPrefix};
use namada::types::governance::ProposalVote;
Expand Down Expand Up @@ -1281,6 +1310,7 @@ pub mod args {
const CHAIN_ID: Arg<ChainId> = arg("chain-id");
const CHAIN_ID_OPT: ArgOpt<ChainId> = CHAIN_ID.opt();
const CHAIN_ID_PREFIX: Arg<ChainIdPrefix> = arg("chain-prefix");
const CHANNEL_ID: Arg<ChannelId> = arg("channel-id");
const CODE_PATH: Arg<PathBuf> = arg("code-path");
const CODE_PATH_OPT: ArgOpt<PathBuf> = CODE_PATH.opt();
const CONSENSUS_TIMEOUT_COMMIT: ArgDefault<Timeout> = arg_default(
Expand Down Expand Up @@ -1318,6 +1348,10 @@ pub mod args {
const NET_ADDRESS: Arg<SocketAddr> = arg("net-address");
const NFT_ADDRESS: Arg<Address> = arg("nft-address");
const OWNER: ArgOpt<WalletAddress> = arg_opt("owner");
const PORT_ID: ArgDefault<PortId> = arg_default(
"port-id",
DefaultFn(|| PortId::from_str("transfer").unwrap()),
);
const PROPOSAL_OFFLINE: ArgFlag = flag("offline");
const PROTOCOL_KEY: ArgOpt<WalletPublicKey> = arg_opt("protocol-key");
const PRE_GENESIS_PATH: ArgOpt<PathBuf> = arg_opt("pre-genesis-path");
Expand All @@ -1328,6 +1362,7 @@ pub mod args {
const RAW_ADDRESS: Arg<Address> = arg("address");
const RAW_ADDRESS_OPT: ArgOpt<Address> = RAW_ADDRESS.opt();
const RAW_PUBLIC_KEY_OPT: ArgOpt<common::PublicKey> = arg_opt("public-key");
const RECEIVER: Arg<String> = arg("receiver");
const REWARDS_CODE_PATH: ArgOpt<PathBuf> = arg_opt("rewards-code-path");
const REWARDS_KEY: ArgOpt<WalletPublicKey> = arg_opt("rewards-key");
const SCHEME: ArgDefault<SchemeType> =
Expand All @@ -1340,6 +1375,8 @@ pub mod args {
const STORAGE_KEY: Arg<storage::Key> = arg("storage-key");
const SUB_PREFIX: ArgOpt<String> = arg_opt("sub-prefix");
const TARGET: Arg<WalletAddress> = arg("target");
const TIMEOUT_HEIGHT: ArgOpt<u64> = arg_opt("timeout-height");
const TIMEOUT_SEC_OFFSET: ArgOpt<u64> = arg_opt("timeout-sec-offset");
const TOKEN_OPT: ArgOpt<WalletAddress> = TOKEN.opt();
const TOKEN: Arg<WalletAddress> = arg("token");
const TX_HASH: Arg<String> = arg("tx-hash");
Expand Down Expand Up @@ -1515,6 +1552,80 @@ pub mod args {
}
}

/// IBC transfer transaction arguments
#[derive(Clone, Debug)]
pub struct TxIbcTransfer {
/// Common tx arguments
pub tx: Tx,
/// Transfer source address
pub source: WalletAddress,
/// Transfer target address
pub receiver: String,
/// Transferred token address
pub token: WalletAddress,
/// Transferred token address
pub sub_prefix: Option<String>,
/// Transferred token amount
pub amount: token::Amount,
/// Port ID
pub port_id: PortId,
/// Channel ID
pub channel_id: ChannelId,
/// Timeout height of the destination chain
pub timeout_height: Option<u64>,
/// Timeout timestamp offset
pub timeout_sec_offset: Option<u64>,
}

impl Args for TxIbcTransfer {
fn parse(matches: &ArgMatches) -> Self {
let tx = Tx::parse(matches);
let source = SOURCE.parse(matches);
let receiver = RECEIVER.parse(matches);
let token = TOKEN.parse(matches);
let sub_prefix = SUB_PREFIX.parse(matches);
let amount = AMOUNT.parse(matches);
let port_id = PORT_ID.parse(matches);
let channel_id = CHANNEL_ID.parse(matches);
let timeout_height = TIMEOUT_HEIGHT.parse(matches);
let timeout_sec_offset = TIMEOUT_SEC_OFFSET.parse(matches);
Self {
tx,
source,
receiver,
token,
sub_prefix,
amount,
port_id,
channel_id,
timeout_height,
timeout_sec_offset,
}
}

fn def(app: App) -> App {
app.add_args::<Tx>()
.arg(SOURCE.def().about(
"The source account address. The source's key is used to \
produce the signature.",
))
.arg(RECEIVER.def().about(
"The receiver address on the destination chain as string.",
))
.arg(TOKEN.def().about("The transfer token."))
.arg(SUB_PREFIX.def().about("The token's sub prefix."))
.arg(AMOUNT.def().about("The amount to transfer in decimal."))
.arg(PORT_ID.def().about("The port ID."))
.arg(CHANNEL_ID.def().about("The channel ID."))
.arg(
TIMEOUT_HEIGHT
.def()
.about("The timeout height of the destination chain."),
)
.arg(TIMEOUT_SEC_OFFSET.def().about("The timeout as seconds."))
}
}

/// Transaction to initialize a new account
#[derive(Clone, Debug)]
pub struct TxInitAccount {
Expand Down
120 changes: 119 additions & 1 deletion apps/src/lib/client/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ use async_std::io::prelude::WriteExt;
use async_std::io::{self};
use borsh::BorshSerialize;
use itertools::Either::*;
use namada::ibc::applications::ics20_fungible_token_transfer::msgs::transfer::MsgTransfer;
use namada::ibc::signer::Signer;
use namada::ibc::timestamp::Timestamp as IbcTimestamp;
use namada::ibc::tx_msg::Msg;
use namada::ibc::Height as IbcHeight;
use namada::ibc_proto::cosmos::base::v1beta1::Coin;
use namada::ledger::governance::storage as gov_storage;
use namada::ledger::pos::{BondId, Bonds, Unbonds};
use namada::proto::Tx;
Expand All @@ -17,7 +23,8 @@ use namada::types::governance::{
};
use namada::types::key::*;
use namada::types::nft::{self, Nft, NftToken};
use namada::types::storage::Epoch;
use namada::types::storage::{Epoch, RESERVED_ADDRESS_PREFIX};
use namada::types::time::DateTimeUtc;
use namada::types::transaction::governance::{
InitProposalData, VoteProposalData,
};
Expand Down Expand Up @@ -49,6 +56,7 @@ const TX_INIT_PROPOSAL: &str = "tx_init_proposal.wasm";
const TX_VOTE_PROPOSAL: &str = "tx_vote_proposal.wasm";
const TX_UPDATE_VP_WASM: &str = "tx_update_vp.wasm";
const TX_TRANSFER_WASM: &str = "tx_transfer.wasm";
const TX_IBC_WASM: &str = "tx_ibc.wasm";
const TX_INIT_NFT: &str = "tx_init_nft.wasm";
const TX_MINT_NFT: &str = "tx_mint_nft.wasm";
const VP_USER_WASM: &str = "vp_user.wasm";
Expand Down Expand Up @@ -465,6 +473,116 @@ pub async fn submit_transfer(ctx: Context, args: args::TxTransfer) {
process_tx(ctx, &args.tx, tx, Some(&args.source)).await;
}

pub async fn submit_ibc_transfer(ctx: Context, args: args::TxIbcTransfer) {
let source = ctx.get(&args.source);
// Check that the source address exists on chain
let source_exists =
rpc::known_address(&source, args.tx.ledger_address.clone()).await;
if !source_exists {
eprintln!("The source address {} doesn't exist on chain.", source);
if !args.tx.force {
safe_exit(1)
}
}

// We cannot check the receiver

let token = ctx.get(&args.token);
// Check that the token address exists on chain
let token_exists =
rpc::known_address(&token, args.tx.ledger_address.clone()).await;
if !token_exists {
eprintln!("The token address {} doesn't exist on chain.", token);
if !args.tx.force {
safe_exit(1)
}
}
// Check source balance
let (sub_prefix, balance_key) = match args.sub_prefix {
Some(sub_prefix) => {
let sub_prefix = storage::Key::parse(sub_prefix).unwrap();
let prefix = token::multitoken_balance_prefix(&token, &sub_prefix);
(
Some(sub_prefix),
token::multitoken_balance_key(&prefix, &source),
)
}
None => (None, token::balance_key(&token, &source)),
};
let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap();
match rpc::query_storage_value::<token::Amount>(&client, &balance_key).await
{
Some(balance) => {
if balance < args.amount {
eprintln!(
"The balance of the source {} of token {} is lower than \
the amount to be transferred. Amount to transfer is {} \
and the balance is {}.",
source, token, args.amount, balance
);
if !args.tx.force {
safe_exit(1)
}
}
}
None => {
eprintln!(
"No balance found for the source {} of token {}",
source, token
);
if !args.tx.force {
safe_exit(1)
}
}
}
let tx_code = ctx.read_wasm(TX_IBC_WASM);

let denom = match sub_prefix {
// To parse IbcToken address, remove the address prefix
Some(sp) => sp.to_string().replace(RESERVED_ADDRESS_PREFIX, ""),
None => token.to_string(),
};
let token = Some(Coin {
denom,
amount: args.amount.to_string(),
});

// this height should be that of the destination chain, not this chain
let timeout_height = match args.timeout_height {
Some(h) => IbcHeight::new(0, h),
None => IbcHeight::zero(),
};

let now: namada::tendermint::Time = DateTimeUtc::now().try_into().unwrap();
let now: IbcTimestamp = now.into();
let timeout_timestamp = if let Some(offset) = args.timeout_sec_offset {
(now + Duration::new(offset, 0)).unwrap()
} else if timeout_height.is_zero() {
// we cannot set 0 to both the height and the timestamp
(now + Duration::new(3600, 0)).unwrap()
} else {
IbcTimestamp::none()
};

let msg = MsgTransfer {
source_port: args.port_id,
source_channel: args.channel_id,
token,
sender: Signer::new(source.to_string()),
receiver: Signer::new(args.receiver),
timeout_height,
timeout_timestamp,
};
tracing::debug!("IBC transfer message {:?}", msg);
let any_msg = msg.to_any();
let mut data = vec![];
prost::Message::encode(&any_msg, &mut data)
.expect("Encoding tx data shouldn't fail");

let tx = Tx::new(tx_code, Some(data));
process_tx(ctx, &args.tx, tx, Some(&args.source)).await;
}

pub async fn submit_init_nft(ctx: Context, args: args::NftCreate) {
let file = File::open(&args.nft_data).expect("File must exist.");
let nft: Nft = serde_json::from_reader(file)
Expand Down

0 comments on commit 4602c1e

Please sign in to comment.