diff --git a/crate/src/sdk/args.rs b/crate/src/sdk/args.rs index 5fedf0b..47531c5 100644 --- a/crate/src/sdk/args.rs +++ b/crate/src/sdk/args.rs @@ -5,8 +5,9 @@ use namada_sdk::address::DecodeError; use namada_sdk::args::{ TxShieldedSource, TxShieldedTarget, TxTransparentSource, TxTransparentTarget, }; -use namada_sdk::borsh::{BorshDeserialize, BorshSerialize}; +use namada_sdk::borsh::{BorshDeserialize, BorshSerialize, BorshSerializeExt}; use namada_sdk::collections::HashMap; +use namada_sdk::dec::Dec; use namada_sdk::ibc::core::host::types::identifiers::{ChannelId, PortId}; use namada_sdk::ibc::IbcShieldingData; use namada_sdk::masp::partial_deauthorize; @@ -22,6 +23,7 @@ use namada_sdk::masp_primitives::zip32; use namada_sdk::signing::SigningTxData; use namada_sdk::time::DateTimeUtc; use namada_sdk::tx::data::GasLimit; +use namada_sdk::tx::either::Either; use namada_sdk::tx::{Section, Tx}; use namada_sdk::{ address::Address, @@ -1105,6 +1107,135 @@ fn tx_msg_into_args(tx_msg: &[u8]) -> Result { Ok(args) } +#[derive(BorshSerialize, BorshDeserialize, Debug)] +#[borsh(crate = "namada_sdk::borsh")] +pub struct OsmosisPoolHop { + /// The id of the pool to use on Osmosis. + pub pool_id: String, + /// The output denomination expected from the + /// pool on Osmosis. + pub token_out_denom: String, +} + +impl From for args::OsmosisPoolHop { + fn from(hop: OsmosisPoolHop) -> Self { + args::OsmosisPoolHop { + pool_id: hop.pool_id, + token_out_denom: hop.token_out_denom, + } + } +} + +#[derive(BorshSerialize, BorshDeserialize, Debug)] +#[borsh(crate = "namada_sdk::borsh")] +pub enum Slippage { + /// Specifies the minimum amount to be received + MinOutputAmount(String), + /// A time-weighted average price + Twap { + /// The maximum percentage difference allowed between the estimated and + /// actual trade price. This must be a decimal number in the range + /// `[0, 100]`. + slippage_percentage: String, + }, +} + +impl From for args::Slippage { + fn from(slippage: Slippage) -> Self { + match slippage { + Slippage::MinOutputAmount(min_output_amount) => { + let min_output_amount = + Amount::from_str(&min_output_amount, 0u8).expect("Amount to be valid."); + args::Slippage::MinOutputAmount(min_output_amount) + } + Slippage::Twap { + slippage_percentage, + } => { + let slippage_percentage = Dec::from_str(&slippage_percentage) + .expect("Slippage percentage to be valid decimal."); + + args::Slippage::Twap { + slippage_percentage, + } + } + } + } +} + +#[derive(BorshSerialize, BorshDeserialize, Debug)] +#[borsh(crate = "namada_sdk::borsh")] +pub struct OsmosisSwapMsg { + pub transfer: IbcTransferMsg, + /// The token we wish to receive (on Namada) + pub output_denom: String, + /// Address of the recipient on Namada + pub recipient: String, + /// Address to receive funds exceeding the minimum amount, + /// in case of IBC shieldings + pub overflow: String, + /// Constraints on the osmosis swap + pub slippage: Slippage, + /// Recovery address (on Osmosis) in case of failure + pub local_recovery_addr: String, + /// The route to take through Osmosis pools + pub route: Option>, + /// The route to take through Osmosis pools + /// A REST rpc endpoint to Osmosis + pub osmosis_lcd_rpc: String, +} + +pub fn osmosis_swap_tx_args( + osmosis_swap_msg: &[u8], + tx_msg: &[u8], + native_token: Address, +) -> Result<(args::TxOsmosisSwap, Option), JsError> { + let osmosis_swap_msg = OsmosisSwapMsg::try_from_slice(osmosis_swap_msg)?; + + let OsmosisSwapMsg { + transfer, + output_denom, + recipient, + overflow, + slippage, + local_recovery_addr, + route, + osmosis_lcd_rpc, + } = osmosis_swap_msg; + + let (ibc_transfer_args, bparams) = + ibc_transfer_tx_args(&transfer.serialize_to_vec(), tx_msg, native_token)?; + + let recipient = match Address::from_str(&recipient) { + Ok(address) => Ok(Either::Left(address)), + Err(_) => match PaymentAddress::from_str(&recipient) { + Ok(pa) => Ok(Either::Right(pa)), + Err(_) => Err(JsError::new("Invalid recipient address")), + }, + }?; + + let overflow = Address::from_str(&overflow)?; + let route = route.map(|r| { + r.into_iter() + .map(args::OsmosisPoolHop::from) + .collect::>() + }); + + let tx_osmosis_swap_args = args::TxOsmosisSwap { + transfer: ibc_transfer_args, + output_denom, + recipient, + overflow: Some(overflow), + slippage: Some(slippage.into()), + local_recovery_addr, + route, + osmosis_lcd_rpc: Some(osmosis_lcd_rpc), + // TODO: not sure if needed + osmosis_sqs_rpc: None, + }; + + Ok((tx_osmosis_swap_args, bparams)) +} + pub enum BuildParams { RngBuildParams(RngBuildParams), StoredBuildParams(StoredBuildParams), diff --git a/crate/src/sdk/mod.rs b/crate/src/sdk/mod.rs index 9e2dae7..ec6b780 100644 --- a/crate/src/sdk/mod.rs +++ b/crate/src/sdk/mod.rs @@ -1030,6 +1030,72 @@ impl Sdk { } } + pub async fn build_osmosis_swap( + &self, + osmosis_swap_msg: &[u8], + wrapper_tx_msg: &[u8], + ) -> Result { + let (args, bparams) = args::osmosis_swap_tx_args( + osmosis_swap_msg, + wrapper_tx_msg, + self.namada.native_token(), + )?; + + web_sys::console::log_1(&format!( + "Building osmosis swap with args: {:?}", + args + ).into()); + + let _ = &self + .namada + .shielded_mut() + .await + .try_load(async |_| {}) + .await; + + // TODO: check if returning true is correct here + let tx = args + .into_ibc_transfer(&self.namada, |_route, _min_amount, _quote_amount| true) + .await?; + web_sys::console::log_1(&format!("Built osmosis swap tx: {:?}", tx).into()); + + let bparams = if let Some(bparams) = bparams { + BuildParams::StoredBuildParams(bparams) + } else { + generate_rng_build_params() + }; + + let xfvks = match tx.source { + TransferSource::Address(_) => vec![], + TransferSource::ExtendedKey(pek) => vec![pek.to_viewing_key()], + }; + + let ((tx, signing_data, _), masp_signing_data) = match bparams { + BuildParams::RngBuildParams(mut bparams) => { + // TODO: replace flase + let tx = build_ibc_transfer(&self.namada, &tx, &mut bparams, false).await?; + let masp_signing_data = MaspSigningData::new( + bparams + .to_stored() + .ok_or_err_msg("Cannot convert bparams to stored")?, + xfvks, + ); + + (tx, masp_signing_data) + } + BuildParams::StoredBuildParams(mut bparams) => { + // TODO: replace flase + let tx = build_ibc_transfer(&self.namada, &tx, &mut bparams, false).await?; + let masp_signing_data = MaspSigningData::new(bparams, xfvks); + + (tx, masp_signing_data) + } + }; + + self.serialize_tx_result(tx, wrapper_tx_msg, signing_data, Some(masp_signing_data)) + } + + // This should be a part of query.rs but we have to pass whole "namada" into estimate_next_epoch_rewards pub async fn shielded_rewards( &self, diff --git a/packages/lib/src/tx/tx.ts b/packages/lib/src/tx/tx.ts index 71bc329..bdaf4f2 100644 --- a/packages/lib/src/tx/tx.ts +++ b/packages/lib/src/tx/tx.ts @@ -15,6 +15,8 @@ import { IbcTransferMsgValue, IbcTransferProps, Message, + OsmosisSwapProps, + OsmosisSwapMsgValue, RedelegateMsgValue, RedelegateProps, RevealPkMsgValue, @@ -523,6 +525,31 @@ export class Tx { ); } + /** + * Build Osmosis Swap Tx + * `osmosisSwapProps.transfer.amountInBaseDenom` is the amount in the **base** denom + * e.g. the value of 1 NAM should be BigNumber(1_000_000), not BigNumber(1). + * @async + * @param wrapperTxProps - properties of the transaction + * @param osmosisSwapProps - properties of the osmosis swap tx + * @returns promise that resolves to an TxMsgValue + */ + async buildOsmosisSwap( + wrapperTxProps: WrapperTxProps, + osmosisSwapProps: OsmosisSwapProps, + ): Promise { + const ibcTransferMsg = new Message(); + const encodedWrapperArgs = this.encodeTxArgs(wrapperTxProps); + const encodedIbcTransfer = ibcTransferMsg.encode( + new OsmosisSwapMsgValue(osmosisSwapProps), + ); + const serializedTx = await this.sdk.build_osmosis_swap( + encodedIbcTransfer, + encodedWrapperArgs, + ); + return deserialize(Buffer.from(serializedTx), TxMsgValue); + } + /** * Return the inner tx hashes from the provided tx bytes * @param bytes - Uint8Array diff --git a/packages/lib/src/types/schema/index.ts b/packages/lib/src/types/schema/index.ts index edacc6f..6a968bf 100644 --- a/packages/lib/src/types/schema/index.ts +++ b/packages/lib/src/types/schema/index.ts @@ -16,12 +16,14 @@ export * from "./utils"; export * from "./voteProposal"; export * from "./withdraw"; export * from "./wrapperTx"; +export * from "./osmosisSwap"; import { BatchTxResultMsgValue } from "./batchTxResult"; import { BondMsgValue } from "./bond"; import { ClaimRewardsMsgValue } from "./claimRewards"; import { EthBridgeTransferMsgValue } from "./ethBridgeTransfer"; import { IbcTransferMsgValue } from "./ibcTransfer"; +import { OsmosisSwapMsgValue } from "./osmosisSwap"; import { RedelegateMsgValue } from "./redelegate"; import { RevealPkMsgValue } from "./revealPk"; import { SignatureMsgValue } from "./signature"; @@ -74,4 +76,5 @@ export type Schema = | RedelegateMsgValue | CommitmentMsgValue | TxDetailsMsgValue - | RevealPkMsgValue; + | RevealPkMsgValue + | OsmosisSwapMsgValue; diff --git a/packages/lib/src/types/schema/osmosisSwap.ts b/packages/lib/src/types/schema/osmosisSwap.ts new file mode 100644 index 0000000..1b54f6b --- /dev/null +++ b/packages/lib/src/types/schema/osmosisSwap.ts @@ -0,0 +1,101 @@ +import { field, option, variant, vec } from "@dao-xyz/borsh"; +import { OsmosisSwapProps } from "../types"; +import { IbcTransferMsgValue } from "./ibcTransfer"; + +abstract class SlippageMsgValue {} + +@variant(0) +export class MinOutputAmount extends SlippageMsgValue { + // 0 to follow rust convention, the amount is in the **base** denom + @field({ type: "string" }) + public 0!: string; + + constructor(data: MinOutputAmount) { + super(); + Object.assign(this, data); + } +} +export const isMinOutputAmount = ( + data: SlippageMsgValue, +): data is MinOutputAmount => { + return typeof (data as MinOutputAmount)[0] === "string"; +}; + +@variant(1) +export class Twap extends SlippageMsgValue { + @field({ type: "string" }) + public slippagePercentage!: string; + + @field({ type: "u64" }) + public windowSeconds!: bigint; + + constructor(data: SlippageMsgValue) { + super(); + Object.assign(this, data); + } +} + +export const isTwap = (data: SlippageMsgValue): data is Twap => { + return ( + typeof (data as Twap).slippagePercentage === "string" && + typeof (data as Twap).windowSeconds === "bigint" + ); +}; + +export class OsmosisPoolHop { + @field({ type: "string" }) + poolId!: string; + + @field({ type: "string" }) + tokenOutDenom!: string; + + constructor(data: OsmosisPoolHop) { + Object.assign(this, data); + } +} + +export class OsmosisSwapMsgValue { + @field({ type: IbcTransferMsgValue }) + transfer!: IbcTransferMsgValue; + + @field({ type: "string" }) + outputDenom!: string; + + @field({ type: "string" }) + recipient!: string; + + @field({ type: "string" }) + overflow!: string; + + @field({ type: SlippageMsgValue }) + slippage!: SlippageMsgValue; + + @field({ type: "string" }) + localRecoveryAddr!: string; + + @field({ type: option(vec(OsmosisPoolHop)) }) + route?: OsmosisPoolHop[]; + + @field({ type: "string" }) + osmosisRestRpc!: string; + + constructor(data: OsmosisSwapProps) { + let slippage; + if (isMinOutputAmount(data.slippage)) { + slippage = new MinOutputAmount(data.slippage); + } else if (isTwap(data.slippage)) { + slippage = new Twap(data.slippage); + } else { + throw new Error("Invalid slippage type"); + } + + Object.assign(this, { + ...data, + transfer: new IbcTransferMsgValue(data.transfer), + slippage, + route: data.route?.map((hop) => { + return new OsmosisPoolHop(hop); + }), + }); + } +} diff --git a/packages/lib/src/types/types.ts b/packages/lib/src/types/types.ts index 12b6c00..7090f8d 100644 --- a/packages/lib/src/types/types.ts +++ b/packages/lib/src/types/types.ts @@ -26,6 +26,7 @@ import { WithdrawMsgValue, WrapperTxMsgValue, } from "./schema"; +import { OsmosisSwapMsgValue } from "./schema/osmosisSwap"; import { RevealPkMsgValue } from "./schema/revealPk"; export type BatchTxResultProps = BatchTxResultMsgValue; @@ -55,6 +56,7 @@ export type ClaimRewardsProps = ClaimRewardsMsgValue; export type WithdrawProps = WithdrawMsgValue; export type WrapperTxProps = WrapperTxMsgValue; export type RevealPkProps = RevealPkMsgValue; +export type OsmosisSwapProps = OsmosisSwapMsgValue; export type SupportedTxProps = | BondProps @@ -67,7 +69,8 @@ export type SupportedTxProps = | ClaimRewardsProps | TransferProps | TransferDetailsProps - | RevealPkProps; + | RevealPkProps + | OsmosisSwapProps; export type CommitmentDetailProps< T extends SupportedTxProps | unknown = unknown,