Skip to content

Commit a5f47f8

Browse files
committed
feat: osmosis swap messages
1 parent dedbae7 commit a5f47f8

File tree

8 files changed

+290
-0
lines changed

8 files changed

+290
-0
lines changed

apps/namadillo/src/workers/MaspTxMessages.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
Account,
33
IbcTransferMsgValue,
4+
OsmosisSwapMsgValue,
45
ShieldedTransferMsgValue,
56
ShieldingTransferMsgValue,
67
TxResponseMsgValue,
@@ -76,6 +77,19 @@ export type IbcTransferDone = WebWorkerMessage<
7677
EncodedTxData<IbcTransferMsgValue>
7778
>;
7879

80+
type OsmosisSwapPayload = {
81+
account: Account;
82+
gasConfig: GasConfig;
83+
props: OsmosisSwapMsgValue[];
84+
chain: ChainSettings;
85+
memo?: string;
86+
};
87+
export type OsmosisSwap = WebWorkerMessage<"osmosis-swap", OsmosisSwapPayload>;
88+
export type OsmosisSwapDone = WebWorkerMessage<
89+
"osmosis-swap-done",
90+
EncodedTxData<OsmosisSwapMsgValue>
91+
>;
92+
7993
type GenerateIbcShieldingMemoPayload = {
8094
target: string;
8195
token: string;

apps/namadillo/src/workers/MaspTxWorker.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { initMulticore } from "@namada/sdk/inline-init";
22
import { getSdk, Sdk } from "@namada/sdk/web";
33
import {
44
IbcTransferMsgValue,
5+
OsmosisSwapMsgValue,
56
ShieldedTransferMsgValue,
67
ShieldingTransferMsgValue,
78
TxResponseMsgValue,
@@ -18,6 +19,8 @@ import {
1819
IbcTransferDone,
1920
Init,
2021
InitDone,
22+
OsmosisSwap,
23+
OsmosisSwapDone,
2124
Shield,
2225
ShieldDone,
2326
ShieldedTransfer,
@@ -89,6 +92,17 @@ export class Worker {
8992
};
9093
}
9194

95+
async osmosisSwap(m: OsmosisSwap): Promise<OsmosisSwapDone> {
96+
if (!this.sdk) {
97+
throw new Error("SDK is not initialized");
98+
}
99+
100+
return {
101+
type: "osmosis-swap-done",
102+
payload: await osmosisSwap(this.sdk, m.payload),
103+
};
104+
}
105+
92106
async broadcast(m: Broadcast): Promise<BroadcastDone> {
93107
if (!this.sdk) {
94108
throw new Error("SDK is not initialized");
@@ -189,6 +203,27 @@ async function ibcTransfer(
189203
return encodedTxData;
190204
}
191205

206+
async function osmosisSwap(
207+
sdk: Sdk,
208+
payload: OsmosisSwap["payload"]
209+
): Promise<EncodedTxData<OsmosisSwapMsgValue>> {
210+
const { account, gasConfig, chain, props } = payload;
211+
212+
await sdk.masp.loadMaspParams("", chain.chainId);
213+
const encodedTxData = await buildTx<OsmosisSwapMsgValue>(
214+
sdk,
215+
account,
216+
gasConfig,
217+
chain,
218+
props,
219+
sdk.tx.buildOsmosisSwap,
220+
undefined,
221+
false
222+
);
223+
224+
return encodedTxData;
225+
}
226+
192227
async function generateIbcShieldingMemo(
193228
sdk: Sdk,
194229
payload: GenerateIbcShieldingMemo["payload"]
@@ -240,6 +275,8 @@ export const registerTransferHandlers = (): void => {
240275
registerBNTransferHandler<UnshieldDone>("unshield-done");
241276
registerBNTransferHandler<IbcTransfer>("ibc-transfer");
242277
registerBNTransferHandler<IbcTransferDone>("ibc-transfer-done");
278+
registerBNTransferHandler<OsmosisSwap>("osmosis-swap");
279+
registerBNTransferHandler<OsmosisSwapDone>("osmosis-swap-done");
243280
registerBNTransferHandler<Unshield>("unshield");
244281
registerBNTransferHandler<GenerateIbcShieldingMemo>(
245282
"generate-ibc-shielding-memo"

packages/sdk/src/tx/tx.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
IbcTransferMsgValue,
1616
IbcTransferProps,
1717
Message,
18+
OsmosisSwapMsgValue,
19+
OsmosisSwapProps,
1820
RedelegateMsgValue,
1921
RedelegateProps,
2022
RevealPkMsgValue,
@@ -280,6 +282,31 @@ export class Tx {
280282
return deserialize(Buffer.from(serializedTx), TxMsgValue);
281283
}
282284

285+
/**
286+
* Build Osmosis Swap Tx
287+
* `osmosisSwapProps.transfer.amountInBaseDenom` is the amount in the **base** denom
288+
* e.g. the value of 1 NAM should be BigNumber(1_000_000), not BigNumber(1).
289+
* @async
290+
* @param wrapperTxProps - properties of the transaction
291+
* @param osmosisSwapProps - properties of the osmosis swap tx
292+
* @returns promise that resolves to an TxMsgValue
293+
*/
294+
async buildOsmosisSwap(
295+
wrapperTxProps: WrapperTxProps,
296+
osmosisSwapProps: OsmosisSwapProps
297+
): Promise<TxMsgValue> {
298+
const ibcTransferMsg = new Message<OsmosisSwapProps>();
299+
const encodedWrapperArgs = this.encodeTxArgs(wrapperTxProps);
300+
const encodedIbcTransfer = ibcTransferMsg.encode(
301+
new OsmosisSwapMsgValue(osmosisSwapProps)
302+
);
303+
const serializedTx = await this.sdk.build_osmosis_swap(
304+
encodedIbcTransfer,
305+
encodedWrapperArgs
306+
);
307+
return deserialize(Buffer.from(serializedTx), TxMsgValue);
308+
}
309+
283310
/**
284311
* Build Ethereum Bridge Transfer Tx
285312
* @async

packages/shared/lib/src/sdk/args.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use namada_sdk::masp_primitives::zip32;
1919
use namada_sdk::signing::SigningTxData;
2020
use namada_sdk::time::DateTimeUtc;
2121
use namada_sdk::tx::data::GasLimit;
22+
use namada_sdk::tx::either::Either;
2223
use namada_sdk::tx::{Section, Tx};
2324
use namada_sdk::{
2425
address::Address,
@@ -308,6 +309,104 @@ pub fn redelegate_tx_args(
308309
Ok(args)
309310
}
310311

312+
#[derive(BorshSerialize, BorshDeserialize, Debug)]
313+
#[borsh(crate = "namada_sdk::borsh")]
314+
pub enum Slippage {
315+
/// Specifies the minimum amount to be received
316+
MinOutputAmount(String),
317+
/// A time-weighted average price
318+
Twap {
319+
/// The maximum percentage difference allowed between the estimated and
320+
/// actual trade price. This must be a decimal number in the range
321+
/// `[0, 100]`.
322+
slippage_percentage: String,
323+
/// The time period (in seconds) over which the average price is
324+
/// calculated
325+
window_seconds: u64,
326+
},
327+
}
328+
329+
impl From<Slippage> for args::Slippage {
330+
fn from(slippage: Slippage) -> Self {
331+
match slippage {
332+
Slippage::MinOutputAmount(min_output_amount) => {
333+
let min_output_amount =
334+
Amount::from_str(&min_output_amount, 0u8).expect("Amount to be valid.");
335+
args::Slippage::MinOutputAmount(min_output_amount)
336+
}
337+
Slippage::Twap {
338+
slippage_percentage,
339+
window_seconds,
340+
} => args::Slippage::Twap {
341+
slippage_percentage,
342+
window_seconds,
343+
},
344+
}
345+
}
346+
}
347+
348+
#[derive(BorshSerialize, BorshDeserialize, Debug)]
349+
#[borsh(crate = "namada_sdk::borsh")]
350+
pub struct OsmosisSwapMsg {
351+
pub transfer: Vec<u8>,
352+
/// The token we wish to receive (on Namada)
353+
pub output_denom: String,
354+
/// Address of the recipient on Namada
355+
pub recipient: String,
356+
/// Address to receive funds exceeding the minimum amount,
357+
/// in case of IBC shieldings
358+
pub overflow: String,
359+
/// Constraints on the osmosis swap
360+
pub slippage: Slippage,
361+
/// Recovery address (on Osmosis) in case of failure
362+
pub local_recovery_addr: String,
363+
/// The route to take through Osmosis pools
364+
/// A REST rpc endpoint to Osmosis
365+
pub osmosis_rest_rpc: String,
366+
}
367+
368+
pub fn osmosis_swap_tx_args(
369+
osmosis_swap_msg: &[u8],
370+
tx_msg: &[u8],
371+
) -> Result<(args::TxOsmosisSwap, Option<StoredBuildParams>), JsError> {
372+
let osmosis_swap_msg = OsmosisSwapMsg::try_from_slice(osmosis_swap_msg)?;
373+
374+
let OsmosisSwapMsg {
375+
transfer,
376+
output_denom,
377+
recipient,
378+
overflow,
379+
slippage,
380+
local_recovery_addr,
381+
osmosis_rest_rpc,
382+
} = osmosis_swap_msg;
383+
384+
let (ibc_transfer_args, bparams) = ibc_transfer_tx_args(&transfer, tx_msg)?;
385+
386+
let recipient = match Address::from_str(&recipient) {
387+
Ok(address) => Ok(Either::Left(address)),
388+
Err(_) => match PaymentAddress::from_str(&recipient) {
389+
Ok(pa) => Ok(Either::Right(pa)),
390+
Err(_) => Err(JsError::new("Invalid recipient address")),
391+
},
392+
}?;
393+
394+
let overflow = Address::from_str(&overflow)?;
395+
396+
let tx_osmosis_swap_args = args::TxOsmosisSwap {
397+
transfer: ibc_transfer_args,
398+
output_denom,
399+
recipient,
400+
overflow: Some(overflow),
401+
slippage: slippage.into(),
402+
local_recovery_addr,
403+
route: None,
404+
osmosis_rest_rpc,
405+
};
406+
407+
Ok((tx_osmosis_swap_args, bparams))
408+
}
409+
311410
#[derive(BorshSerialize, BorshDeserialize, Debug)]
312411
#[borsh(crate = "namada_sdk::borsh")]
313412
pub struct VoteProposalMsg {

packages/shared/lib/src/sdk/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,49 @@ impl Sdk {
734734
self.serialize_tx_result(tx, wrapper_tx_msg, signing_data, None)
735735
}
736736

737+
pub async fn build_osmosis_swap(
738+
&self,
739+
osmosis_swap_msg: &[u8],
740+
wrapper_tx_msg: &[u8],
741+
) -> Result<JsValue, JsError> {
742+
let (args, bparams) = args::osmosis_swap_tx_args(osmosis_swap_msg, wrapper_tx_msg)?;
743+
let tx = args.into_ibc_transfer(&self.namada).await?;
744+
let bparams = if let Some(bparams) = bparams {
745+
BuildParams::StoredBuildParams(bparams)
746+
} else {
747+
generate_rng_build_params()
748+
};
749+
750+
let _ = &self.namada.shielded_mut().await.load().await?;
751+
752+
let xfvks = match tx.source {
753+
TransferSource::Address(_) => vec![],
754+
TransferSource::ExtendedKey(pek) => vec![pek.to_viewing_key()],
755+
};
756+
757+
let ((tx, signing_data, _), masp_signing_data) = match bparams {
758+
BuildParams::RngBuildParams(mut bparams) => {
759+
let tx = build_ibc_transfer(&self.namada, &tx, &mut bparams).await?;
760+
let masp_signing_data = MaspSigningData::new(
761+
bparams
762+
.to_stored()
763+
.ok_or_err_msg("Cannot convert bparams to stored")?,
764+
xfvks,
765+
);
766+
767+
(tx, masp_signing_data)
768+
}
769+
BuildParams::StoredBuildParams(mut bparams) => {
770+
let tx = build_ibc_transfer(&self.namada, &tx, &mut bparams).await?;
771+
let masp_signing_data = MaspSigningData::new(bparams, xfvks);
772+
773+
(tx, masp_signing_data)
774+
}
775+
};
776+
777+
self.serialize_tx_result(tx, wrapper_tx_msg, signing_data, Some(masp_signing_data))
778+
}
779+
737780
pub async fn build_reveal_pk(&self, wrapper_tx_msg: &[u8]) -> Result<JsValue, JsError> {
738781
let args = args::tx_args_from_slice(wrapper_tx_msg)?;
739782
let public_key = args.signing_keys[0].clone();

packages/types/src/tx/schema/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from "./bparams";
44
export * from "./claimRewards";
55
export * from "./ethBridgeTransfer";
66
export * from "./ibcTransfer";
7+
export * from "./osmosisSwap";
78
export * from "./redelegate";
89
export * from "./revealPk";
910
export * from "./signature";
@@ -22,6 +23,7 @@ import { BondMsgValue } from "./bond";
2223
import { ClaimRewardsMsgValue } from "./claimRewards";
2324
import { EthBridgeTransferMsgValue } from "./ethBridgeTransfer";
2425
import { IbcTransferMsgValue } from "./ibcTransfer";
26+
import { OsmosisSwapMsgValue } from "./osmosisSwap";
2527
import { RedelegateMsgValue } from "./redelegate";
2628
import { RevealPkMsgValue } from "./revealPk";
2729
import { SignatureMsgValue } from "./signature";
@@ -50,6 +52,7 @@ export type Schema =
5052
| BatchTxResultMsgValue
5153
| EthBridgeTransferMsgValue
5254
| IbcTransferMsgValue
55+
| OsmosisSwapMsgValue
5356
| SignatureMsgValue
5457
| BondMsgValue
5558
| UnbondMsgValue
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
import { field, variant } from "@dao-xyz/borsh";
3+
import { OsmosisSwapProps } from "../types";
4+
import { IbcTransferMsgValue } from "./ibcTransfer";
5+
6+
abstract class SlippageMsgValue {}
7+
8+
@variant(0)
9+
class MinOutputAmount extends SlippageMsgValue {
10+
// 0 to follow rust convention, the amount is in the **base** denom
11+
@field({ type: "string" })
12+
public 0!: string;
13+
14+
constructor(data: MinOutputAmount) {
15+
super();
16+
Object.assign(this, data);
17+
}
18+
}
19+
20+
@variant(1)
21+
class Twap extends SlippageMsgValue {
22+
@field({ type: "string" })
23+
public slippagePercentage!: string;
24+
25+
@field({ type: "u64" })
26+
public windowSeconds!: bigint;
27+
28+
constructor(data: SlippageMsgValue) {
29+
super();
30+
Object.assign(this, data);
31+
}
32+
}
33+
34+
export class OsmosisSwapMsgValue {
35+
@field({ type: IbcTransferMsgValue })
36+
transfer!: IbcTransferMsgValue;
37+
38+
@field({ type: "string" })
39+
outputDenom!: string;
40+
41+
@field({ type: "string" })
42+
overflow!: string;
43+
44+
@field({ type: SlippageMsgValue })
45+
slippage!: SlippageMsgValue;
46+
47+
@field({ type: "string" })
48+
localRecoveryAddr!: string;
49+
50+
@field({ type: "string" })
51+
osmosisRestRpc!: string;
52+
53+
constructor(data: OsmosisSwapProps) {
54+
const slippage =
55+
data instanceof MinOutputAmount ?
56+
new MinOutputAmount(data)
57+
: new Twap(data);
58+
59+
Object.assign(this, {
60+
...data,
61+
transfer: new IbcTransferMsgValue(data.transfer),
62+
slippage,
63+
});
64+
}
65+
}

0 commit comments

Comments
 (0)