Skip to content

Commit b8c4c0e

Browse files
committed
add ability to swap native tokens in v2 and v3
1 parent 04ff616 commit b8c4c0e

11 files changed

Lines changed: 200 additions & 112 deletions

File tree

frontend/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ log = "0.4"
1616
console_log = "1.0"
1717
alloy = {version = "1.1", default-features = false }
1818
dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false }
19+
enum-table = "2.1"
1920

2021
[profile]
2122

frontend/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod app;
2+
mod config;
23
mod components;
34
mod metamask;
45
mod vanillaswap;

frontend/src/metamask.js

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ const wrapperRouterAbi = [
2727
// Uniswap V2
2828
const uniswapV2RouterAbi = [
2929
"function factory() view returns (address)",
30-
"function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) returns (uint256[])"
30+
"function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) returns (uint256[])",
31+
"function swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline)",
32+
"function swapExactETHForTokensSupportingFeeOnTransferTokens(uint256 amountOutMin, address[] calldata path, address to, uint256 deadline) payable"
3133
];
3234
const uniswapV2FactoryAbi = [
3335
"function allPairsLength() view returns (uint256)",
@@ -43,10 +45,12 @@ const uniswapV2PairAbi = [
4345
const uniswapV3PairAbi = [ "function slot0() view returns (uint160 sqrtPriceX96, int24, uint16, uint16, uint16, uint8, bool)", "function liquidity() view returns (uint128)" ];
4446

4547
const uniswapV3RouterAbi = [
46-
// exactInputSingle params in struct form
47-
//"function exactInputSingle((address,address,uint24,address,uint256,uint256,uint160)) external payable returns (uint256 amountOut)",
4848
"function exactInput((bytes path,address recipient,uint256 amountIn,uint256 amountOutMinimum)) external payable returns (uint256 amountOut)"
4949
];
50+
const wethAbi = [
51+
"function withdraw(uint256 wad) external",
52+
"function balanceOf(address owner) view returns (uint256)"
53+
];
5054

5155
/**
5256
* helper JSON stringify that converts BigInt to string
@@ -102,14 +106,20 @@ export function js_on_accounts_changed(callback) {
102106
}
103107

104108
// ERC20
105-
export async function js_get_token_balance(user, token) {
109+
export async function js_get_token_balance(user, token, isNative) {
106110
try {
107111
if (!window.ethereum) throw new Error("MetaMask not installed");
108112
await window.ethereum.request({ method: 'eth_requestAccounts' });
109113
let provider = new ethers.BrowserProvider(window.ethereum);
110114

111-
const erc20 = new ethers.Contract(token, erc20Abi, provider);
112-
const [bal, decimals] = await Promise.all([erc20.balanceOf(user), 18]);
115+
let bal, decimals;
116+
117+
if (isNative) {
118+
[bal, decimals] = await Promise.all([provider.getBalance(user), 18]);
119+
}else{
120+
const erc20 = new ethers.Contract(token, erc20Abi, provider);
121+
[bal, decimals] = await Promise.all([erc20.balanceOf(user), erc20.decimals()]);
122+
}
113123
return {
114124
ok: true,
115125
value: JSON.stringify(ethers.formatUnits(bal, decimals))
@@ -337,37 +347,61 @@ export async function js_get_uniswap_v2_pairs(routerAddr) {
337347
}
338348
}
339349

340-
export async function js_uniswap_v2_swap_tokens(tokenIn, tokenOut, amountIn, amountOutMin, routerAddress) {
350+
export async function js_uniswap_v2_swap_tokens(tokenIn, tokenOut, amountIn, amountOutMin, routerAddress, isNativeIn, isNativeOut) {
341351
try {
342352
if (!window.ethereum) throw new Error("MetaMask not installed");
343353
await window.ethereum.request({ method: 'eth_requestAccounts' });
344354
let provider = new ethers.BrowserProvider(window.ethereum);
345355
let signer = await provider.getSigner();
346356

347-
348357
const router = new ethers.Contract(routerAddress, uniswapV2RouterAbi, signer);
349358

350359
const tokenInContract = new ethers.Contract(tokenIn, erc20Abi, signer);
351360
const decimals = await tokenInContract.decimals();
352-
const amount_in_u256 = amountIn;//thers.parseUnits(amountIn,decimals);
353-
354-
// Approve
355-
const allowance = await tokenInContract.allowance(signer, routerAddress);
356-
if (allowance < amount_in_u256){
357-
const approve_tx = await tokenInContract.approve(routerAddress, amount_in_u256);
358-
await approve_tx.wait();
359-
}
361+
const amount_in_u256 = amountIn;
360362

361363
const path = [tokenIn, tokenOut];
362364
const deadline = Math.floor(Date.now() / 1000) + 60 * 10;
363365

364-
const tx = await router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
365-
amountIn,
366-
amountOutMin,
367-
path,
368-
await signer.getAddress(),
369-
deadline,
370-
);
366+
let tx;
367+
if (isNativeIn){
368+
tx = await router.swapExactETHForTokensSupportingFeeOnTransferTokens(
369+
amountOutMin,
370+
path,
371+
await signer.getAddress(),
372+
deadline,
373+
{
374+
value: amountIn
375+
}
376+
);
377+
}else{
378+
// Approve
379+
const allowance = await tokenInContract.allowance(signer, routerAddress);
380+
if (allowance < amount_in_u256){
381+
const approve_tx = await tokenInContract.approve(routerAddress, amount_in_u256);
382+
await approve_tx.wait();
383+
}
384+
385+
386+
if (isNativeOut){
387+
tx = await router.swapExactTokensForETHSupportingFeeOnTransferTokens(
388+
amountIn,
389+
amountOutMin,
390+
path,
391+
await signer.getAddress(),
392+
deadline,
393+
);
394+
}else{
395+
tx = await router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
396+
amountIn,
397+
amountOutMin,
398+
path,
399+
await signer.getAddress(),
400+
deadline,
401+
);
402+
}
403+
404+
}
371405
const receipt = await tx.wait();
372406

373407
return {
@@ -428,7 +462,7 @@ function encodePath(tokenIn, fee, tokenOut) {
428462
]);
429463
}
430464

431-
export async function js_uniswap_v3_swap_tokens(tokenIn, tokenOut, amountIn, amountOutMin, fee, routerAddress) {
465+
export async function js_uniswap_v3_swap_tokens(tokenIn, tokenOut, amountIn, amountOutMin, fee, routerAddress, isNativeIn, isNativeOut) {
432466
try {
433467
if (!window.ethereum) throw new Error("MetaMask not installed");
434468
await window.ethereum.request({ method: 'eth_requestAccounts' });
@@ -450,17 +484,23 @@ export async function js_uniswap_v3_swap_tokens(tokenIn, tokenOut, amountIn, amo
450484
}
451485

452486
const path = encodePath(tokenIn,fee,tokenOut);
453-
console.log("Path len ",path.length);
454487
const recipient = await signer.getAddress();
455488
const params = {
456489
path:path,
457490
recipient:recipient,
458491
amountIn:amount_in_u256,
459492
amountOutMinimum: amountOutMin
460493
};
461-
const tx = await router.exactInput(params,{value: 0n});
494+
const tx = await router.exactInput(params,isNativeIn ? { value: amountIn } : {});
462495
const receipt = await tx.wait();
463-
496+
if (isNativeOut) {
497+
const wethContract = new ethers.Contract(tokenOut, wethAbi, signer);
498+
const balance = await wethContract.balanceOf(recipient);
499+
if (balance > 0n) {
500+
const unwrapTx = await wethContract.withdraw(balance);
501+
await unwrapTx.wait();
502+
}
503+
}
464504
return {
465505
ok: true,
466506
value: JSON.stringify(`${receipt.hash}`)

frontend/src/metamask.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ extern "C" {
1515
async fn js_connect_metamask() -> JsValue;
1616
pub fn js_on_chain_changed(callback: &Closure<dyn FnMut(u32)>);
1717
pub fn js_on_accounts_changed(callback: &Closure<dyn FnMut(Vec<JsValue>)>);
18-
async fn js_get_token_balance(user: &str, token: &str) -> JsValue;
18+
async fn js_get_token_balance(user: &str, token: &str, is_native: bool) -> JsValue;
1919
}
2020

2121
pub fn js_parse<T: DeserializeOwned>(js: JsValue) -> Result<T, String> {
@@ -53,6 +53,6 @@ pub async fn connect_metamask() -> Result<MetamaskInfo, Box<dyn Error>>{
5353
js_try!(js_connect_metamask() => MetamaskInfo)
5454
}
5555

56-
pub async fn get_token_balance(user: &str, token: &str) -> Result<String,Box<dyn Error>>{
57-
js_try!(js_get_token_balance(user, token) => String)
56+
pub async fn get_token_balance(user: &str, token: &str, is_native: bool) -> Result<String,Box<dyn Error>>{
57+
js_try!(js_get_token_balance(user, token, is_native) => String)
5858
}

frontend/src/metamask/uniswap_v2.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ extern "C" {
1313
// uniswap v2
1414
async fn js_get_uniswap_v2_pairs(router_address: &str) -> JsValue;
1515
async fn js_uniswap_v2_swap_tokens(token_in: &str, token_out: &str, amount_in: &str,
16-
amount_out_min: &str, router_address: &str) -> JsValue;
16+
amount_out_min: &str, router_address: &str, is_native_in: bool, is_native_out: bool) -> JsValue;
1717
}
1818

1919

@@ -43,6 +43,8 @@ pub async fn uniswap_v2_swap_tokens(
4343
amount_in: &str,
4444
amount_out_min: &str,
4545
router_address: &str,
46+
is_native_in: bool,
47+
is_native_out: bool,
4648
) -> Result<String, String> {
47-
js_try!(js_uniswap_v2_swap_tokens(token_in,token_out,amount_in,amount_out_min,router_address) => String)
49+
js_try!(js_uniswap_v2_swap_tokens(token_in,token_out,amount_in,amount_out_min,router_address, is_native_in, is_native_out) => String)
4850
}

frontend/src/metamask/uniswap_v3.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ extern "C" {
1313
//uniswap v3
1414
async fn js_get_uniswap_v3_pool_states(pools: Vec<JsValue>) -> JsValue;
1515
async fn js_uniswap_v3_swap_tokens(token_in: &str, token_out: &str, amount_in: &str,
16-
amount_out_min: &str, fee: &str, router_address: &str) -> JsValue;
16+
amount_out_min: &str, fee: &str, router_address: &str, is_native_in: bool, is_native_out: bool) -> JsValue;
1717
}
1818

1919

@@ -40,6 +40,8 @@ pub async fn uniswap_v3_swap_tokens(
4040
amount_out_min: &str,
4141
fee: &str,
4242
router_address: &str,
43+
is_native_in: bool,
44+
is_native_out: bool,
4345
) -> Result<String, String> {
44-
js_try!(js_uniswap_v3_swap_tokens(token_in,token_out,amount_in,amount_out_min,fee,router_address) => String)
46+
js_try!(js_uniswap_v3_swap_tokens(token_in,token_out,amount_in,amount_out_min,fee,router_address, is_native_in, is_native_out) => String)
4547
}

frontend/src/vanillaswap/swap_v2.rs

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ use crate::metamask::{
66
get_token_balance,
77
uniswap_v2::uniswap_v2_swap_tokens
88
};
9-
use crate::wrapper::TokenInfo;
9+
use crate::wrapper::{TokenInfo, TokenType};
1010
use crate::wallet_context::use_wallet;
1111
use super::v2::{approx_amount_out, unique_pool_tokens, use_v2_pools};
1212

1313
#[component]
1414
pub fn PoolV2Swap() -> Element {
15+
let wallet = use_wallet();
1516
let pools = use_v2_pools();
1617

1718
let mut show_zero_liq = use_signal(|| false);
@@ -33,7 +34,6 @@ pub fn PoolV2Swap() -> Element {
3334
spawn_local(async move {
3435
calculating.set(true);
3536
let new_amount = approx_amount_out(&amount_in.read(),&token_a.read(), &token_b.read(), &pools.pairs.read());
36-
log::debug!("New Amount out : {}", new_amount);
3737
amount_out.set(new_amount);
3838
calculating.set(false);
3939
});
@@ -47,7 +47,7 @@ pub fn PoolV2Swap() -> Element {
4747

4848
spawn_local(async move {
4949
if let Some(from_sel) = from_sel
50-
&& let Ok(bal) = get_token_balance(&(wallet.info)().address, &from_sel.address).await {
50+
&& let Ok(bal) = get_token_balance(&(wallet.info)().address, &from_sel.address, matches!(from_sel.token_type, TokenType::Native)).await {
5151
log::debug!("GetTokenBalance of address {} for token address {} :{:?}",(wallet.info)().address, from_sel.address, bal);
5252
balance.set(bal);
5353
}
@@ -105,6 +105,8 @@ pub fn PoolV2Swap() -> Element {
105105
&amount_in.get_absolute().to_string(),
106106
&amount_out_min.to_string(),
107107
&(pools.router_address)(),
108+
matches!(a.token_type, TokenType::Native),
109+
matches!(b.token_type, TokenType::Native),
108110
).await {
109111
Ok(jsval) => {
110112
log::info!("swap ok: {}", jsval);
@@ -123,10 +125,10 @@ pub fn PoolV2Swap() -> Element {
123125
}
124126
};
125127

126-
let from_options = unique_pool_tokens(&None, &pools.pairs.read(), &show_zero_liq.read());
127-
let to_options = unique_pool_tokens(&token_a.read(), &pools.pairs.read(), &show_zero_liq.read());
128-
let from_selected = token_a.read().as_ref().map(|t| t.address.clone()).unwrap_or_default();
129-
let to_selected = token_b.read().as_ref().map(|t| t.address.clone()).unwrap_or_default();
128+
let from_options = unique_pool_tokens(&None, &pools.pairs.read(), &show_zero_liq.read(), (wallet.info)().chain_id);
129+
let to_options = unique_pool_tokens(&token_a.read(), &pools.pairs.read(), &show_zero_liq.read(), (wallet.info)().chain_id);
130+
let from_selected = token_a.read().as_ref().map(|t| serde_json::to_string(&t).unwrap()).unwrap_or_default();
131+
let to_selected = token_b.read().as_ref().map(|t| serde_json::to_string(&t).unwrap()).unwrap_or_default();
130132
// UI rendering
131133
rsx! {
132134
div { class: "p-8 mt-12 glass w-full max-w-4xl flex flex-col gap-6 items-stretch flex-col-sm",
@@ -156,16 +158,17 @@ pub fn PoolV2Swap() -> Element {
156158
class: "flex-1 bg-transparent text-white text-xl font-semibold focus:outline-none",
157159
value: "{from_selected}",
158160
onchange: move |e| {
159-
log::debug!("Value {}", e.value());
160-
if let Some(tok) = from_options.iter().find(|t| t.address == e.value()) {
161-
token_a.set(Some(tok.clone()));
162-
// Reset token B, because A changed
163-
token_b.set(None);
161+
if let Ok(sel) = serde_json::from_str::<TokenInfo>(&e.value()) {
162+
if let Some(tok) = from_options.iter().find(|t| **t == sel) {
163+
token_a.set(Some(tok.clone()));
164+
// Reset token B, because A changed
165+
token_b.set(None);
166+
}
164167
}
165168
},
166169
option { value: "", "Select token A" }
167170
{ from_options.iter().map(|t| rsx!(
168-
option { value: "{t.address}", "{t.symbol}" }
171+
option { value: "{serde_json::to_string(&t).unwrap()}", "{t.symbol}" }
169172
)) }
170173
}
171174
}
@@ -204,14 +207,15 @@ pub fn PoolV2Swap() -> Element {
204207
class: "flex-1 bg-transparent text-white text-xl font-semibold focus:outline-none",
205208
value: "{to_selected}",
206209
onchange: move |e| {
207-
let sym = e.value();
208-
if let Some(tok) = to_options.iter().find(|t| t.address == e.value()) {
209-
token_b.set(Some(tok.clone()));
210+
if let Ok(sel) = serde_json::from_str::<TokenInfo>(&e.value()) {
211+
if let Some(tok) = to_options.iter().find(|t| **t == sel) {
212+
token_b.set(Some(tok.clone()));
213+
}
210214
}
211215
},
212216
option { value: "", "Select token B" }
213217
{ to_options.iter().map(|t| rsx!(
214-
option { value: "{t.address}", "{t.symbol}" }
218+
option { value: "{serde_json::to_string(&t).unwrap()}", "{t.symbol}" }
215219
)) }
216220
}
217221
}

0 commit comments

Comments
 (0)