Skip to content

Commit

Permalink
feat: sendZRC20 helper, refactored getBalances, fixed pools and C…
Browse files Browse the repository at this point in the history
…CTX tracking for Bitcoin, omnichain ERC-20 compatibility (#79)
  • Loading branch information
fadeev authored Dec 19, 2023
1 parent 6de3c9a commit a360309
Show file tree
Hide file tree
Showing 15 changed files with 447 additions and 187 deletions.
39 changes: 39 additions & 0 deletions contracts/SwapHelperLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,43 @@ library SwapHelperLib {
);
return amounts[path.length - 1];
}

function swapTokensForExactTokens(
address zetaToken,
address uniswapV2Factory,
address uniswapV2Router,
address zrc20,
uint256 amount,
address targetZRC20,
uint256 amountInMax
) internal returns (uint256) {
bool existsPairPool = _existsPairPool(
uniswapV2Factory,
zrc20,
targetZRC20
);

address[] memory path;
if (existsPairPool) {
path = new address[](2);
path[0] = zrc20;
path[1] = targetZRC20;
} else {
path = new address[](3);
path[0] = zrc20;
path[1] = zetaToken;
path[2] = targetZRC20;
}

IZRC20(zrc20).approve(address(uniswapV2Router), amountInMax);
uint256[] memory amounts = IUniswapV2Router01(uniswapV2Router)
.swapTokensForExactTokens(
amount,
amountInMax,
path,
address(this),
block.timestamp + MAX_DEADLINE
);
return amounts[0];
}
}
228 changes: 140 additions & 88 deletions helpers/balances.ts
Original file line number Diff line number Diff line change
@@ -1,101 +1,153 @@
import ERC20_ABI from "@openzeppelin/contracts/build/contracts/ERC20.json";
import { getEndpoints } from "@zetachain/networks/dist/src/getEndpoints";
import networks from "@zetachain/networks/dist/src/networks";
import { getAddress } from "@zetachain/protocol-contracts";
import ZetaEth from "@zetachain/protocol-contracts/abi/evm/Zeta.eth.sol/ZetaEth.json";
import ZRC20 from "@zetachain/protocol-contracts/abi/zevm/ZRC20.sol/ZRC20.json";
import { ethers } from "ethers";
import { formatEther } from "ethers/lib/utils";
import { formatUnits } from "ethers/lib/utils";
import fetch from "isomorphic-fetch";

const fetchBitcoinBalance = async (address: string) => {
const API = getEndpoints("esplora", "btc_testnet")[0].url;
if (API === undefined) throw new Error("fetchBitcoinBalance: API not found");

try {
const response = await fetch(`${API}/address/${address}`);
const data = await response.json();
const { funded_txo_sum, spent_txo_sum } = data.chain_stats;
const balance = funded_txo_sum - spent_txo_sum;
return {
native: `${balance / 100000000}`,
networkName: "btc_testnet",
};
} catch (error) {}
export const getForeignCoins = async () => {
const api = getEndpoints("cosmos-http", "zeta_testnet")[0]?.url;
const endpoint = `${api}/zeta-chain/zetacore/fungible/foreign_coins`;
const response = await fetch(endpoint);
const data = await response.json();
return data.foreignCoins;
};

const fetchNativeBalance = async (address: string, provider: any) => {
const balance = await provider.getBalance(address);
return parseFloat(formatEther(balance)).toFixed(2);
export const getSupportedChains = async () => {
const api = getEndpoints("cosmos-http", "zeta_testnet")[0]?.url;
const endpoint = `${api}/zeta-chain/observer/supportedChains`;
const response = await fetch(endpoint);
const data = await response.json();
return data.chains;
};

const fetchZetaBalance = async (
address: string,
provider: any,
networkName: string
) => {
if (networkName === "zeta_testnet") return "";
const zetaAddress = getAddress("zetaToken", networkName as any);
const contract = new ethers.Contract(zetaAddress, ZetaEth.abi, provider);
const balance = await contract.balanceOf(address);
return parseFloat(formatEther(balance)).toFixed(2);
};

const fetchBalances = async (
address: string,
provider: any,
networkName: string
) => {
try {
const native = await fetchNativeBalance(address, provider);
const zeta = await fetchZetaBalance(address, provider, networkName);
const isZeta = networkName === "zeta_testnet";
const zrc20 = isZeta ? { zrc20: await fetchZRC20Balance(address) } : {};
/* eslint-disable */
return { networkName, native, zeta, ...zrc20 };
/* eslint-enable */
} catch (error) {}
};

const fetchZRC20Balance = async (address: string) => {
const api = getEndpoints("evm", "zeta_testnet");
if (api.length < 1) return;
const rpc = api[0].url;
const provider = new ethers.providers.JsonRpcProvider(rpc);
const promises = Object.keys(networks).map(async (networkName) => {
try {
const zrc20 = getAddress("zrc20", networkName);
const contract = new ethers.Contract(zrc20, ZRC20.abi, provider);
const balance = await contract.balanceOf(address);
const denom = networks[networkName].assets[0].symbol;
if (balance > 0) {
const b = parseFloat(formatEther(balance)).toFixed(2);
return `${b} ${denom}`;
}
} catch (error) {}
export const getBalances = async (evmAddress: any, btcAddress = null) => {
let tokens = [];
const foreignCoins = await getForeignCoins();
const supportedChains = await getSupportedChains();
foreignCoins.forEach((token: any) => {
if (token.coin_type === "Gas") {
tokens.push({
chain_id: token.foreign_chain_id,
coin_type: token.coin_type,
decimals: token.decimals,
symbol: token.symbol,
zrc20: token.zrc20_contract_address,
});
tokens.push({
chain_id: 7001,
coin_type: "ZRC20",
contract: token.zrc20_contract_address,
decimals: token.decimals,
symbol: token.symbol,
});
} else if (token.coin_type === "ERC20") {
tokens.push({
chain_id: token.foreign_chain_id,
coin_type: "ERC20",
contract: token.asset,
decimals: token.decimals,
symbol: token.symbol,
zrc20: token.zrc20_contract_address,
});
tokens.push({
chain_id: 7001,
coin_type: "ZRC20",
contract: token.zrc20_contract_address,
decimals: token.decimals,
symbol: token.name,
});
}
});

const result = await Promise.all(promises);

// tBTC ZRC-20 balance
const btcZRC20 = "0x65a45c57636f9BcCeD4fe193A602008578BcA90b"; // TODO: use getAddress("zrc20", "btc_testnet") when available
const contract = new ethers.Contract(btcZRC20, ZRC20.abi, provider);
const balance = (await contract.balanceOf(address)) / 100000000;
if (balance > 0) {
result.push(`${balance} tBTC`);
}

return result.filter((item) => item !== undefined).join(", ");
};

export const getBalances = async (address, btc_address = null) => {
const balancePromises = Object.keys(networks).map((networkName) => {
const api = getEndpoints("evm", networkName as any);
if (api.length >= 1) {
const rpc = api[0].url;
const provider = new ethers.providers.JsonRpcProvider(rpc);
return fetchBalances(address, provider, networkName);
supportedChains.forEach((chain: any) => {
const contract = getAddress("zetaToken", chain.chain_name as any);
if (contract) {
tokens.push({
chain_id: chain.chain_id,
coin_type: "ERC20",
contract,
decimals: 18,
symbol: "WZETA",
});
}
});
const balances = await Promise.all(balancePromises);
if (btc_address) balances.push(await fetchBitcoinBalance(btc_address));
return balances.filter((balance) => balance != null);
tokens.push({
chain_id: 7001,
coin_type: "Gas",
decimals: 18,
symbol: "ZETA",
});

tokens = tokens.map((token: any) => {
const ticker = token.symbol.split("-")[0];
const chain_name =
token.chain_id === 7001
? "zeta_testnet"
: supportedChains.find((c: any) => c.chain_id === token.chain_id)
?.chain_name;
return {
...token,
chain_name,
id: `${token.chain_id
.toString()
.toLowerCase()}__${token.symbol.toLowerCase()}`,
ticker,
};
});

const balances = await Promise.all(
tokens.map(async (token: any) => {
const isGas = token.coin_type === "Gas";
const isBitcoin = token.chain_name === "btc_testnet";
const isERC = token.coin_type === "ERC20";
const isZRC = token.coin_type === "ZRC20";
if (isGas && !isBitcoin) {
const rpc = getEndpoints("evm", token.chain_name)[0]?.url;
const provider = new ethers.providers.StaticJsonRpcProvider(rpc);
return provider.getBalance(evmAddress).then((balance) => {
return { ...token, balance: formatUnits(balance, token.decimals) };
});
} else if (isGas && isBitcoin && btcAddress) {
const API = getEndpoints("esplora", "btc_testnet")[0]?.url;
return fetch(`${API}/address/${btcAddress}`).then(async (response) => {
const r = await response.json();
const { funded_txo_sum, spent_txo_sum } = r.chain_stats;
const balance = (
(funded_txo_sum - spent_txo_sum) /
100000000
).toString();
return { ...token, balance };
});
} else if (isERC) {
const rpc = getEndpoints("evm", token.chain_name)[0]?.url;
const provider = new ethers.providers.StaticJsonRpcProvider(rpc);
const contract = new ethers.Contract(
token.contract,
ERC20_ABI.abi,
provider
);
return contract.balanceOf(evmAddress).then((balance: any) => {
return { ...token, balance: formatUnits(balance, token.decimals) };
});
} else if (isZRC) {
const rpc = getEndpoints("evm", token.chain_name)[0]?.url;
const provider = new ethers.providers.StaticJsonRpcProvider(rpc);
const contract = new ethers.Contract(
token.contract,
ZRC20.abi,
provider
);
return contract.balanceOf(evmAddress).then((balance: any) => {
return {
...token,
balance: formatUnits(balance, token.decimals),
};
});
} else {
return Promise.resolve(token);
}
})
);
return balances;
};
2 changes: 1 addition & 1 deletion helpers/fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const formatTo18Decimals = (n: any) => parseFloat(formatEther(n)).toFixed(18);
export const fetchZEVMFees = async (network: string) => {
const url = getEndpoints("evm", "zeta_testnet")[0].url;

const provider = new ethers.providers.JsonRpcProvider(url);
const provider = new ethers.providers.StaticJsonRpcProvider(url);
const btcZRC20 = "0x65a45c57636f9BcCeD4fe193A602008578BcA90b"; // TODO: use getAddress("zrc20", "btc_testnet") when available
const zrc20Address =
network === "btc_testnet" ? btcZRC20 : getAddress("zrc20", network as any);
Expand Down
1 change: 1 addition & 0 deletions helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export * from "./fees";
export * from "./pools";
export * from "./prepare";
export * from "./sendZETA";
export * from "./sendZRC20";
export * from "./tx";
50 changes: 27 additions & 23 deletions helpers/pools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,46 @@ export const getPools = async () => {
const data = await response.json();

const rpc = getEndpoints("evm", "zeta_testnet")[0]?.url;
const provider = new ethers.providers.JsonRpcProvider(rpc);
const provider = new ethers.providers.StaticJsonRpcProvider(rpc);

const uniswapV2FactoryAddress = getAddress(
"uniswapv2Factory",
"zeta_testnet"
);
const zetaTokenAddress = getAddress("zetaToken", "zeta_testnet");

const zetaTokenAddress = getAddress(
"zetaToken",
"zeta_testnet"
).toLowerCase();

const UniswapV2FactoryContract = new ethers.Contract(
uniswapV2FactoryAddress,
UniswapV2Factory.abi,
provider
);

const poolPromises = data.foreignCoins.map(async (token: any) => {
const zrc20Address = token.zrc20_contract_address;
const pair = await UniswapV2FactoryContract.getPair(
zrc20Address,
zetaTokenAddress
);

let reservesZRC20 = "0";
let reservesZETA = "0";

if (pair !== ethers.constants.AddressZero) {
const contract = new ethers.Contract(pair, UniswapV2Pair.abi, provider);
const reserves = await contract.getReserves();
reservesZRC20 = ethers.utils.formatEther(reserves[0]);
reservesZETA = ethers.utils.formatEther(reserves[1]);
}
return {
...token,
const totalPairs = await UniswapV2FactoryContract.allPairsLength();
let pairs = [];
for (let i = 0; i < totalPairs; i++) {
pairs.push(await UniswapV2FactoryContract.allPairs(i));
}

const poolPromises = pairs.map(async (pair: any) => {
let pool = {
pair,
reservesZETA,
reservesZRC20,
};
t0: {},
t1: {},
} as any;
const pairContract = new ethers.Contract(pair, UniswapV2Pair.abi, provider);

pool.t0.address = await pairContract.token0();
pool.t1.address = await pairContract.token1();

const reserves = await pairContract.getReserves();
pool.t0.reserve = reserves[0];
pool.t1.reserve = reserves[1];

return pool;
});

const pools = await Promise.all(poolPromises);
Expand Down
5 changes: 3 additions & 2 deletions helpers/sendZETA.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import networks from "@zetachain/networks/dist/src/networks";
import { getAddress } from "@zetachain/protocol-contracts";
import ZetaEthContract from "@zetachain/protocol-contracts/abi/evm/Zeta.eth.sol/ZetaEth.json";
import ZetaConnectorEth from "@zetachain/protocol-contracts/abi/evm/ZetaConnector.eth.sol/ZetaConnectorEth.json";
import ZetaConnectorZEVM from "@zetachain/protocol-contracts/abi/zevm/ConnectorZEVM.sol/ZetaConnectorZEVM.json";
import ZetaConnectorZEVM from "@zetachain/protocol-contracts/abi/zevm/ZetaConnectorZEVM.sol/ZetaConnectorZEVM.json";
import { ethers } from "ethers";

export const sendZETA = async (
Expand All @@ -13,7 +13,8 @@ export const sendZETA = async (
recipient: string
) => {
let connectorContract: any;
const destinationChainId = networks[destination]?.chain_id;
const destinationChainId =
networks[destination as keyof typeof networks]?.chain_id;
if (!destinationChainId) {
throw new Error("Invalid destination chain");
}
Expand Down
Loading

0 comments on commit a360309

Please sign in to comment.