Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
133 changes: 132 additions & 1 deletion crate/src/sdk/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -1105,6 +1107,135 @@ fn tx_msg_into_args(tx_msg: &[u8]) -> Result<args::Tx, JsError> {
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<OsmosisPoolHop> 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<Slippage> 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<Vec<OsmosisPoolHop>>,
/// 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<StoredBuildParams>), 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::<Vec<_>>()
});

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<OsRng>),
StoredBuildParams(StoredBuildParams),
Expand Down
66 changes: 66 additions & 0 deletions crate/src/sdk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,72 @@ impl Sdk {
}
}

pub async fn build_osmosis_swap(
&self,
osmosis_swap_msg: &[u8],
wrapper_tx_msg: &[u8],
) -> Result<JsValue, JsError> {
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,
Expand Down
27 changes: 27 additions & 0 deletions packages/lib/src/tx/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
IbcTransferMsgValue,
IbcTransferProps,
Message,
OsmosisSwapProps,
OsmosisSwapMsgValue,
RedelegateMsgValue,
RedelegateProps,
RevealPkMsgValue,
Expand Down Expand Up @@ -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<TxMsgValue> {
const ibcTransferMsg = new Message<OsmosisSwapProps>();
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
Expand Down
5 changes: 4 additions & 1 deletion packages/lib/src/types/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -74,4 +76,5 @@ export type Schema =
| RedelegateMsgValue
| CommitmentMsgValue
| TxDetailsMsgValue
| RevealPkMsgValue;
| RevealPkMsgValue
| OsmosisSwapMsgValue;
101 changes: 101 additions & 0 deletions packages/lib/src/types/schema/osmosisSwap.ts
Original file line number Diff line number Diff line change
@@ -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);
}),
});
}
}
Loading