Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat v2/get all balances #228

Merged
merged 9 commits into from
Oct 30, 2023
Merged
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"axios": "^1.5.0",
"cosmjs-types": "^0.8.0",
"ethers": "6.7.1",
"ethers-multicall-provider": "^5.0.0",
"lodash": "^4.17.21"
},
"resolutions": {
Expand Down
25 changes: 25 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,28 @@ export const uint256MaxValue =
"115792089237316195423570985008687907853269984665640564039457584007913129639935";

export const nativeTokenConstant = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
export const NATIVE_EVM_TOKEN_ADDRESS =
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
export const MULTICALL_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
export const multicallAbi = [
{
inputs: [
{
internalType: "address",
name: "account",
type: "address"
}
],
name: "getEthBalance",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256"
}
],
stateMutability: "view",
type: "function"
}
];
export const CHAINS_WITHOUT_MULTICALL = [314, 3141]; // Filecoin, & Filecoin testnet
64 changes: 61 additions & 3 deletions src/handlers/cosmos/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { toUtf8 } from "@cosmjs/encoding";
import { calculateFee, Coin, GasPrice } from "@cosmjs/stargate";
import { fromBech32, toBech32, toUtf8 } from "@cosmjs/encoding";
import { calculateFee, Coin, GasPrice, StargateClient } from "@cosmjs/stargate";
import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx";

import {
Expand All @@ -9,7 +9,11 @@ import {
CosmosMsg,
IBC_TRANSFER_TYPE,
WasmHookMsg,
WASM_TYPE
WASM_TYPE,
CosmosBalance,
CosmosChain,
CosmosAddress,
ChainType
} from "../../types";
import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx";

Expand Down Expand Up @@ -115,4 +119,58 @@ export class CosmosHandler {
""
);
}

async getBalances({
addresses,
cosmosChains
}: {
addresses: CosmosAddress[];
cosmosChains: CosmosChain[];
}): Promise<CosmosBalance[]> {
const cosmosBalances: CosmosBalance[] = [];

for (const chain of cosmosChains) {
if (chain.chainType !== ChainType.COSMOS) continue;

const addressData = addresses.find(
address => address.coinType === chain.coinType
);

if (!addressData) continue;

const cosmosAddress = this.deriveCosmosAddress(
chain.bech32Config.bech32PrefixAccAddr,
addressData.address
);

try {
const client = await StargateClient.connect(chain.rpc);

const balances = (await client.getAllBalances(cosmosAddress)) ?? [];

if (balances.length === 0) continue;

balances.forEach(balance => {
const { amount, denom } = balance;

cosmosBalances.push({
balance: amount,
denom,
chainId: String(chain.chainId),
decimals:
chain.currencies.find(currency => currency.coinDenom === denom)
?.coinDecimals ?? 6
});
});
} catch (error) {
//
}
}

return cosmosBalances;
}

deriveCosmosAddress(chainPrefix: string, address: string): string {
return toBech32(chainPrefix, fromBech32(address).data);
}
}
71 changes: 70 additions & 1 deletion src/handlers/evm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {
EvmWallet,
ExecuteRoute,
RouteParamsPopulated,
Token,
TokenBalance,
TransactionRequest,
TransactionResponse,
WalletV6
} from "../../types";

import { uint256MaxValue } from "../../constants";
import { CHAINS_WITHOUT_MULTICALL, uint256MaxValue } from "../../constants";
import { Utils } from "./utils";

const ethersAdapter = new EthersAdapter();
Expand Down Expand Up @@ -238,4 +240,71 @@ export class EvmHandler extends Utils {
...gasData
});
}

async getBalances(
evmTokens: Token[],
userAddress: string,
chainRpcUrls: {
[chainId: string]: string;
}
): Promise<TokenBalance[]> {
try {
// Some tokens don't support multicall, so we need to fetch them with Promise.all
// TODO: Once we support multicall on all chains, we can remove this split
const splittedTokensByMultiCallSupport = evmTokens.reduce(
(acc, token) => {
if (CHAINS_WITHOUT_MULTICALL.includes(Number(token.chainId))) {
acc[0].push(token);
} else {
acc[1].push(token);
}
return acc;
},
[[], []] as Token[][]
);

const tokensNotSupportingMulticall = splittedTokensByMultiCallSupport[0];
const tokensSupportingMulticall = splittedTokensByMultiCallSupport[1];

const tokensByChainId = tokensSupportingMulticall.reduce(
(groupedTokens, token) => {
if (!groupedTokens[token.chainId]) {
groupedTokens[token.chainId] = [];
}

groupedTokens[token.chainId].push(token);

return groupedTokens;
},
{} as Record<string, Token[]>
);

const tokensMulticall: TokenBalance[] = [];

for (const chainId in tokensByChainId) {
const tokens = tokensByChainId[chainId];
const rpcUrl = chainRpcUrls[chainId];

if (!rpcUrl) continue;

const tokensBalances = await this.getTokensBalanceSupportingMultiCall(
tokens,
rpcUrl,
userAddress
);

tokensMulticall.push(...tokensBalances);
}

const tokensNotMultiCall = await this.getTokensBalanceWithoutMultiCall(
tokensNotSupportingMulticall,
userAddress,
chainRpcUrls
);

return [...tokensMulticall, ...tokensNotMultiCall];
} catch (error) {
return [];
}
}
}
147 changes: 145 additions & 2 deletions src/handlers/evm/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { ChainData, SquidData } from "@0xsquid/squid-types";
import { ChainData, SquidData, Token } from "@0xsquid/squid-types";

import { OverrideParams, Contract, GasData, RpcProvider } from "../../types";
import {
OverrideParams,
Contract,
GasData,
RpcProvider,
TokenBalance
} from "../../types";
import { MulticallWrapper } from "ethers-multicall-provider";
import { Provider, ethers } from "ethers";
import {
multicallAbi,
MULTICALL_ADDRESS,
NATIVE_EVM_TOKEN_ADDRESS
} from "../../constants";

export class Utils {
async validateNativeBalance({
Expand Down Expand Up @@ -108,4 +121,134 @@ export class Utils {

return overrides ? { ...gasParams, ...overrides } : (gasParams as GasData);
};

async getTokensBalanceSupportingMultiCall(
tokens: Token[],
chainRpcUrl: string,
userAddress?: string
): Promise<TokenBalance[]> {
if (!userAddress) return [];

const multicallProvider = MulticallWrapper.wrap(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
new ethers.JsonRpcProvider(chainRpcUrl)
);

const tokenBalances: Promise<TokenBalance>[] = tokens.map(token => {
const isNativeToken =
token.address.toLowerCase() === NATIVE_EVM_TOKEN_ADDRESS.toLowerCase();

const contract = new ethers.Contract(
isNativeToken ? MULTICALL_ADDRESS : token.address,
isNativeToken
? multicallAbi
: [
{
name: "balanceOf",
type: "function",
inputs: [{ name: "_owner", type: "address" }],
outputs: [{ name: "balance", type: "uint256" }],
stateMutability: "view"
}
],
multicallProvider as unknown as Provider
);

const getTokenData = async () => {
const balanceInWei = await contract[
isNativeToken ? "getEthBalance" : "balanceOf"
](userAddress);

return {
balance: balanceInWei.toString(),
symbol: token.symbol,
address: token.address,
decimals: token.decimals,
chainId: token.chainId
};
};

return getTokenData();
});

try {
return Promise.all(tokenBalances);
} catch (error) {
return [];
}
}

async getTokensBalanceWithoutMultiCall(
tokens: Token[],
userAddress: string,
rpcUrlsPerChain: {
[chainId: string]: string;
}
): Promise<TokenBalance[]> {
const balances: (TokenBalance | null)[] = await Promise.all(
tokens.map(async t => {
let balance: TokenBalance | null;
try {
if (t.address === NATIVE_EVM_TOKEN_ADDRESS) {
balance = await this.fetchBalance({
token: t,
userAddress,
rpcUrl: rpcUrlsPerChain[t.chainId]
});
} else {
balance = await this.fetchBalance({
token: t,
userAddress,
rpcUrl: rpcUrlsPerChain[t.chainId]
});
}

return balance;
} catch (error) {
return null;
}
})
);

// filter out null values
return balances.filter(Boolean) as TokenBalance[];
}

async fetchBalance({
token,
userAddress,
rpcUrl
}: {
token: Token;
userAddress: string;
rpcUrl: string;
}): Promise<TokenBalance | null> {
try {
const provider = new ethers.JsonRpcProvider(rpcUrl);

const tokenAbi = ["function balanceOf(address) view returns (uint256)"];
const tokenContract = new ethers.Contract(
token.address ?? "",
tokenAbi,
provider
);

const balance = (await tokenContract.balanceOf(userAddress)) ?? "0";

if (!token) return null;

const { decimals, symbol, address } = token;

return {
address,
// balance in wei
balance: parseInt(balance, 16).toString(),
decimals,
symbol
};
} catch (error) {
return null;
}
}
}
Loading
Loading