Skip to content

Commit

Permalink
feat: add support for IBC deposits to shielded (#1163)
Browse files Browse the repository at this point in the history
  • Loading branch information
emccorson authored and mateuszjasiuk committed Oct 30, 2024
1 parent 2a0b2b2 commit eb1c685
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 27 deletions.
32 changes: 28 additions & 4 deletions apps/namadillo/src/App/Ibc/IbcTransfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,15 @@ 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);
const [registry, setRegistry] = useState<ChainRegistryEntry>();
const [sourceAddress, setSourceAddress] = useState<string | undefined>();
const [shielded, setShielded] = useState<boolean>(true);
const [selectedAsset, setSelectedAsset] = useState<Asset>();
const [sourceChannelId, setSourceChannelId] = useState<string>("");
const [destinationChannelId, setDestinationChannelId] = useState<string>("");
const performIbcTransfer = useAtomValue(ibcTransferAtom);
const defaultAccounts = useAtomValue(allDefaultAccountsAtom);

Expand Down Expand Up @@ -140,7 +139,15 @@ export const IbcTransfer: React.FC = () => {
destinationAddress,
amount,
token: selectedAsset.base,
channelId: namadaChannelId,
sourceChannelId,
...(shielded ?
{
isShielded: true,
destinationChannelId,
}
: {
isShielded: false,
}),
},
});
};
Expand All @@ -161,6 +168,23 @@ export const IbcTransfer: React.FC = () => {
<header className="text-center mb-4">
<h2>IBC Transfer to Namada</h2>
</header>
<div className="flex flex-col gap-2">
<input
className="text-black"
type="text"
placeholder="source channel id"
value={sourceChannelId}
onChange={(e) => setSourceChannelId(e.target.value)}
/>

<input
className="text-black"
type="text"
placeholder="destination channel id"
value={destinationChannelId}
onChange={(e) => setDestinationChannelId(e.target.value)}
/>
</div>
<TransferModule
source={{
isLoadingAssets: isLoadingBalances,
Expand Down
4 changes: 2 additions & 2 deletions apps/namadillo/src/atoms/integrations/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { atomWithMutation, atomWithQuery } from "jotai-tanstack-query";
import { atomFamily, atomWithStorage } from "jotai/utils";
import { getKnownChains, mapCoinsToAssets } from "./functions";
import {
IBCTransferParams,
IbcTransferParams,
queryAndStoreRpc,
queryAssetBalances,
submitIbcTransfer,
} from "./services";

type IBCTransferAtomParams = {
transferParams: IBCTransferParams;
transferParams: IbcTransferParams;
chain: Chain;
};

Expand Down
74 changes: 58 additions & 16 deletions apps/namadillo/src/atoms/integrations/services.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,53 @@
import { Asset, Chain } from "@chain-registry/types";
import { Coin, OfflineSigner } from "@cosmjs/launchpad";
import { coin, coins } from "@cosmjs/proto-signing";
import { SigningStargateClient, StargateClient } from "@cosmjs/stargate";
import {
MsgTransferEncodeObject,
SigningStargateClient,
StargateClient,
} from "@cosmjs/stargate";
import BigNumber from "bignumber.js";
import { getDefaultStore } from "jotai";
import Long from "long";
import { getSdkInstance } from "utils/sdk";
import { workingRpcsAtom } from "./atoms";
import { getRpcByIndex } from "./functions";

export type IBCTransferParams = {
type CommonParams = {
signer: OfflineSigner;
sourceAddress: string;
destinationAddress: string;
amount: BigNumber;
token: string;
channelId: string;
memo?: string;
sourceChannelId: string;
};

type TransparentParams = CommonParams & { isShielded: false };
type ShieldedParams = CommonParams & {
isShielded: true;
destinationChannelId: string;
};

export type IbcTransferParams = TransparentParams | ShieldedParams;

const getShieldedArgs = async (
target: string,
token: string,
amount: BigNumber,
destinationChannelId: string
): Promise<{ receiver: string; memo: string }> => {
const sdk = await getSdkInstance();

const memo = await sdk.tx.generateIbcShieldingMemo(
target,
token,
amount,
destinationChannelId
);

return {
receiver: sdk.masp.maspAddress(),
memo,
};
};

export type AssetWithBalance = {
Expand All @@ -35,16 +67,16 @@ export const queryAssetBalances = async (
};

export const submitIbcTransfer =
(transferParams: IBCTransferParams) =>
(transferParams: IbcTransferParams) =>
async (rpc: string): Promise<void> => {
const {
signer,
sourceAddress,
destinationAddress,
amount,
token,
channelId,
memo,
sourceChannelId,
isShielded,
} = transferParams;

const client = await SigningStargateClient.connectWithSigner(rpc, signer, {
Expand All @@ -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) {
Expand Down
9 changes: 9 additions & 0 deletions packages/sdk/src/masp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,13 @@ export class Masp {
async addDefaultPaymentAddress(xvk: string, alias: string): Promise<void> {
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();
}
}
25 changes: 25 additions & 0 deletions packages/sdk/src/tx/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
WrapperTxProps,
} from "@namada/types";
import { ResponseSign } from "@zondax/ledger-namada";
import BigNumber from "bignumber.js";
import { WasmHash } from "../rpc";

/**
Expand Down Expand Up @@ -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<string> {
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
Expand Down
59 changes: 54 additions & 5 deletions packages/shared/lib/src/sdk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};

Expand All @@ -55,6 +61,7 @@ const MAX_HW_OUTPUT: usize = 15;
#[wasm_bindgen]
pub struct Sdk {
namada: NamadaImpl<HttpClient, wallet::JSWalletUtils, masp::JSShieldedUtils, WebIo>,
rpc_url: String
}

#[wasm_bindgen]
Expand All @@ -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::JSWalletUtils> = Wallet::new(
wallet::JSWalletUtils::new_utils(&path_or_db_name),
Store::default(),
Expand All @@ -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<JsValue, JsValue> {
Expand Down Expand Up @@ -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<JsValue, JsError> {
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,
Expand Down

0 comments on commit eb1c685

Please sign in to comment.