Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ibc-transfer command #626

Merged
merged 7 commits into from
Nov 14, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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
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