From eb1c685e89845930fd29c67ea67fbeed4e44c6ae Mon Sep 17 00:00:00 2001 From: Eric Corson Date: Fri, 4 Oct 2024 18:54:46 +0100 Subject: [PATCH] feat: add support for IBC deposits to shielded (#1163) --- apps/namadillo/src/App/Ibc/IbcTransfer.tsx | 32 +++++++- .../namadillo/src/atoms/integrations/atoms.ts | 4 +- .../src/atoms/integrations/services.ts | 74 +++++++++++++++---- packages/sdk/src/masp.ts | 9 +++ packages/sdk/src/tx/tx.ts | 25 +++++++ packages/shared/lib/src/sdk/mod.rs | 59 +++++++++++++-- 6 files changed, 176 insertions(+), 27 deletions(-) diff --git a/apps/namadillo/src/App/Ibc/IbcTransfer.tsx b/apps/namadillo/src/App/Ibc/IbcTransfer.tsx index 456032db5..5084cfb61 100644 --- a/apps/namadillo/src/App/Ibc/IbcTransfer.tsx +++ b/apps/namadillo/src/App/Ibc/IbcTransfer.tsx @@ -23,9 +23,6 @@ import { basicConvertToKeplrChain } from "utils/integration"; const keplr = (window as KeplrWindow).keplr!; -//TODO: we need to find a good way to manage IBC channels -const namadaChannelId = "channel-4353"; - export const IbcTransfer: React.FC = () => { const knownChains = useAtomValue(knownChainsAtom); const [chainId, setChainId] = useAtom(selectedIBCChainAtom); @@ -33,6 +30,8 @@ export const IbcTransfer: React.FC = () => { const [sourceAddress, setSourceAddress] = useState(); const [shielded, setShielded] = useState(true); const [selectedAsset, setSelectedAsset] = useState(); + const [sourceChannelId, setSourceChannelId] = useState(""); + const [destinationChannelId, setDestinationChannelId] = useState(""); const performIbcTransfer = useAtomValue(ibcTransferAtom); const defaultAccounts = useAtomValue(allDefaultAccountsAtom); @@ -140,7 +139,15 @@ export const IbcTransfer: React.FC = () => { destinationAddress, amount, token: selectedAsset.base, - channelId: namadaChannelId, + sourceChannelId, + ...(shielded ? + { + isShielded: true, + destinationChannelId, + } + : { + isShielded: false, + }), }, }); }; @@ -161,6 +168,23 @@ export const IbcTransfer: React.FC = () => {

IBC Transfer to Namada

+
+ setSourceChannelId(e.target.value)} + /> + + setDestinationChannelId(e.target.value)} + /> +
=> { + const sdk = await getSdkInstance(); + + const memo = await sdk.tx.generateIbcShieldingMemo( + target, + token, + amount, + destinationChannelId + ); + + return { + receiver: sdk.masp.maspAddress(), + memo, + }; }; export type AssetWithBalance = { @@ -35,7 +67,7 @@ export const queryAssetBalances = async ( }; export const submitIbcTransfer = - (transferParams: IBCTransferParams) => + (transferParams: IbcTransferParams) => async (rpc: string): Promise => { const { signer, @@ -43,8 +75,8 @@ export const submitIbcTransfer = destinationAddress, amount, token, - channelId, - memo, + sourceChannelId, + isShielded, } = transferParams; const client = await SigningStargateClient.connectWithSigner(rpc, signer, { @@ -57,27 +89,37 @@ export const submitIbcTransfer = gas: "222000", }; - const timeoutTimestampNanoseconds = Long.fromNumber( - Math.floor(Date.now() / 1000) + 60 - ).multiply(1_000_000_000); + const timeoutTimestampNanoseconds = + BigInt(Math.floor(Date.now() / 1000) + 60) * BigInt(1_000_000_000); + + const { receiver, memo }: { receiver: string; memo?: string } = + isShielded ? + await getShieldedArgs( + destinationAddress, + token, + amount, + transferParams.destinationChannelId + ) + : { receiver: destinationAddress }; - const transferMsg = { + const transferMsg: MsgTransferEncodeObject = { typeUrl: "/ibc.applications.transfer.v1.MsgTransfer", value: { sourcePort: "transfer", - sourceChannel: channelId, + sourceChannel: sourceChannelId, sender: sourceAddress, - receiver: destinationAddress, + receiver, token: coin(amount.toString(), token), + timeoutHeight: undefined, timeoutTimestamp: timeoutTimestampNanoseconds, + memo, }, }; const response = await client.signAndBroadcast( sourceAddress, [transferMsg], - fee, - memo + fee ); if (response.code !== 0) { diff --git a/packages/sdk/src/masp.ts b/packages/sdk/src/masp.ts index ab5dfae03..6e68b3db3 100644 --- a/packages/sdk/src/masp.ts +++ b/packages/sdk/src/masp.ts @@ -70,4 +70,13 @@ export class Masp { async addDefaultPaymentAddress(xvk: string, alias: string): Promise { return await this.sdk.add_default_payment_address(xvk, alias); } + + /** + * Returns the MASP address used as the receiving address in IBC transfers to + * shielded accounts + * @returns the MASP address + */ + maspAddress(): string { + return this.sdk.masp_address(); + } } diff --git a/packages/sdk/src/tx/tx.ts b/packages/sdk/src/tx/tx.ts index 9e155b13f..1e613cc86 100644 --- a/packages/sdk/src/tx/tx.ts +++ b/packages/sdk/src/tx/tx.ts @@ -43,6 +43,7 @@ import { WrapperTxProps, } from "@namada/types"; import { ResponseSign } from "@zondax/ledger-namada"; +import BigNumber from "bignumber.js"; import { WasmHash } from "../rpc"; /** @@ -473,6 +474,30 @@ export class Tx { }; } + /** + * Generate the memo needed for performing an IBC transfer to a Namada shielded + * address. + * @async + * @param target - the Namada shielded address to send tokens to + * @param token - the token to transfer + * @param amount - the amount to transfer + * @param channelId - the IBC channel ID on the Namada side + * @returns promise that resolves to the shielding memo + */ + generateIbcShieldingMemo( + target: string, + token: string, + amount: BigNumber, + channelId: string + ): Promise { + return this.sdk.generate_ibc_shielding_memo( + target, + token, + amount.toString(), + channelId + ); + } + /** * Return the inner tx hashes from the provided tx bytes * @param bytes - Uint8Array diff --git a/packages/shared/lib/src/sdk/mod.rs b/packages/shared/lib/src/sdk/mod.rs index 3993d590d..3028ae6c9 100644 --- a/packages/shared/lib/src/sdk/mod.rs +++ b/packages/shared/lib/src/sdk/mod.rs @@ -14,7 +14,7 @@ use crate::utils::to_bytes; use crate::utils::to_js_result; use args::generate_masp_build_params; use gloo_utils::format::JsValueSerdeExt; -use namada_sdk::address::Address; +use namada_sdk::address::{Address, MASP}; use namada_sdk::borsh::{self, BorshDeserialize}; use namada_sdk::eth_bridge::bridge_pool::build_bridge_pool_tx; use namada_sdk::hash::Hash; @@ -28,10 +28,16 @@ use namada_sdk::tx::{ build_batch, build_bond, build_claim_rewards, build_ibc_transfer, build_redelegation, build_reveal_pk, build_shielded_transfer, build_shielding_transfer, build_transparent_transfer, build_unbond, build_unshielding_transfer, build_vote_proposal, build_withdraw, - data::compute_inner_tx_hash, either::Either, process_tx, ProcessTxResponse, Tx, + data::compute_inner_tx_hash, either::Either, process_tx, ProcessTxResponse, + Tx, gen_ibc_shielding_transfer }; use namada_sdk::wallet::{Store, Wallet}; -use namada_sdk::{Namada, NamadaImpl}; +use namada_sdk::{Namada, NamadaImpl, PaymentAddress, TransferTarget}; +use namada_sdk::args::{InputAmount, GenIbcShieldingTransfer, Query, TxExpiration}; +use namada_sdk::ibc::core::host::types::identifiers::{ChannelId, PortId}; +use namada_sdk::ibc::convert_masp_tx_to_ibc_memo; +use namada_sdk::token::DenominatedAmount; +use namada_sdk::tendermint_rpc::Url; use std::str::FromStr; use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue}; @@ -55,6 +61,7 @@ const MAX_HW_OUTPUT: usize = 15; #[wasm_bindgen] pub struct Sdk { namada: NamadaImpl, + rpc_url: String } #[wasm_bindgen] @@ -64,7 +71,7 @@ impl Sdk { #[wasm_bindgen(constructor)] pub fn new(url: String, native_token: String, path_or_db_name: String) -> Self { set_panic_hook(); - let client: HttpClient = HttpClient::new(url); + let client: HttpClient = HttpClient::new(url.clone()); let wallet: Wallet = Wallet::new( wallet::JSWalletUtils::new_utils(&path_or_db_name), Store::default(), @@ -80,7 +87,10 @@ impl Sdk { Address::from_str(&native_token).unwrap(), ); - Sdk { namada } + Sdk { + namada, + rpc_url: url + } } pub async fn has_masp_params() -> Result { @@ -507,6 +517,45 @@ impl Sdk { common::SigScheme::verify_signature(&public_key, &signed_hash, &sig).map_err(JsError::from) } + pub async fn generate_ibc_shielding_memo( + &self, + target: &str, + token: String, + amount: &str, + channel_id: &str + ) -> Result { + let ledger_address = Url::from_str(&self.rpc_url).expect("RPC URL is a valid URL"); + let target = TransferTarget::PaymentAddress( + PaymentAddress::from_str(target).expect("target is a valid shielded address") + ); + let amount = InputAmount::Unvalidated( + DenominatedAmount::from_str(amount).expect("amount is valid") + ); + let channel_id = ChannelId::from_str(channel_id).expect("channel ID is valid"); + + let args = GenIbcShieldingTransfer { + query: Query { ledger_address }, + output_folder: None, + target, + token, + amount, + port_id: PortId::transfer(), + channel_id, + expiration: TxExpiration::Default + }; + + if let Some(masp_tx) = gen_ibc_shielding_transfer(&self.namada, args).await? { + let memo = convert_masp_tx_to_ibc_memo(&masp_tx); + to_js_result(memo) + } else { + Err(JsError::new("Generating ibc shielding transfer generated nothing")) + } + } + + pub fn masp_address(&self) -> String { + MASP.to_string() + } + fn serialize_tx_result( &self, tx: Tx,