Skip to content

Commit

Permalink
Support query solana balances (#185)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukema95 authored Sep 27, 2024
1 parent e8bb021 commit d6148b2
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 113 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,13 @@
"@solana/web3.js": "^1.95.3",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@zetachain/faucet-cli": "^4.1.1",
"@zetachain/networks": "^10.0.0",
"@zetachain/networks": "10.0.0-rc1",
"@zetachain/protocol-contracts": "9.0.0",
"axios": "^1.4.0",
"bech32": "^2.0.0",
"bip39": "^3.1.0",
"bitcoinjs-lib": "^6.1.3",
"bs58": "^6.0.0",
"dotenv": "16.0.3",
"ecpair": "^2.1.0",
"envfile": "^6.18.0",
Expand All @@ -118,4 +119,4 @@
"ws": "^8.17.1"
},
"packageManager": "[email protected]+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
}
}
218 changes: 132 additions & 86 deletions packages/client/src/getBalances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ const MULTICALL3_ABI = [
*/
export const getBalances = async function (
this: ZetaChainClient,
{ evmAddress, btcAddress }: { btcAddress?: string; evmAddress: string }
{
evmAddress,
btcAddress,
solanaAddress,
}: { btcAddress?: string; evmAddress?: string; solanaAddress?: string }
): Promise<TokenBalance[]> {
let tokens = [];
const supportedChains = await this.getSupportedChains();
Expand Down Expand Up @@ -138,101 +142,130 @@ export const getBalances = async function (

const multicallContexts: Record<string, any[]> = {};

tokens.forEach((token: any) => {
if (token.coin_type === "ERC20" || token.coin_type === "ZRC20") {
if (!multicallContexts[token.chain_name]) {
multicallContexts[token.chain_name] = [];
}
multicallContexts[token.chain_name].push({
callData: new ethers.utils.Interface(
token.coin_type === "ERC20" ? ERC20_ABI.abi : ZRC20.abi
).encodeFunctionData("balanceOf", [evmAddress]),
target: token.contract,
});
}
});

const balances: TokenBalance[] = [];

await Promise.all(
Object.keys(multicallContexts).map(async (chainName) => {
const rpc = await this.getEndpoint("evm", chainName);
const provider = new ethers.providers.StaticJsonRpcProvider(rpc);
const multicallContract = new ethers.Contract(
multicallAddress,
MULTICALL3_ABI,
provider
);

const calls = multicallContexts[chainName];
if (evmAddress) {
tokens.forEach((token: any) => {
if (token.coin_type === "ERC20" || token.coin_type === "ZRC20") {
if (!multicallContexts[token.chain_name]) {
multicallContexts[token.chain_name] = [];
}
multicallContexts[token.chain_name].push({
callData: new ethers.utils.Interface(
token.coin_type === "ERC20" ? ERC20_ABI.abi : ZRC20.abi
).encodeFunctionData("balanceOf", [evmAddress]),
target: token.contract,
});
}
});

try {
const { returnData } = await multicallContract.callStatic.aggregate(
calls
await Promise.all(
Object.keys(multicallContexts).map(async (chainName) => {
const rpc = await this.getEndpoint("evm", chainName);
const provider = new ethers.providers.StaticJsonRpcProvider(rpc);
const multicallContract = new ethers.Contract(
multicallAddress,
MULTICALL3_ABI,
provider
);

returnData.forEach((data: any, index: number) => {
const token = tokens.find(
const calls = multicallContexts[chainName];

try {
const { returnData } = await multicallContract.callStatic.aggregate(
calls
);

returnData.forEach((data: any, index: number) => {
const token = tokens.find(
(t) =>
t.chain_name === chainName &&
(t.coin_type === "ERC20" || t.coin_type === "ZRC20") &&
t.contract === calls[index].target
);
if (token) {
const balance = ethers.utils.defaultAbiCoder.decode(
["uint256"],
data
)[0];
const formattedBalance = formatUnits(balance, token.decimals);
balances.push({ ...token, balance: formattedBalance });
}
});
} catch (error) {
console.error(`Multicall failed for ${chainName}:`, error);
// Fallback to individual calls if multicall fails
for (const token of tokens.filter(
(t) =>
t.chain_name === chainName &&
(t.coin_type === "ERC20" || t.coin_type === "ZRC20") &&
t.contract === calls[index].target
);
if (token) {
const balance = ethers.utils.defaultAbiCoder.decode(
["uint256"],
data
)[0];
const formattedBalance = formatUnits(balance, token.decimals);
balances.push({ ...token, balance: formattedBalance });
(t.coin_type === "ERC20" || t.coin_type === "ZRC20")
)) {
try {
const contract = new ethers.Contract(
token.contract,
token.coin_type === "ERC20" ? ERC20_ABI.abi : ZRC20.abi,
provider
);
const balance = await contract.balanceOf(evmAddress);
const formattedBalance = formatUnits(balance, token.decimals);
balances.push({ ...token, balance: formattedBalance });
} catch (err) {
console.error(
`Failed to get balance for ${token.symbol} on ${chainName}:`,
err
);
}
}
});
} catch (error) {
console.error(`Multicall failed for ${chainName}:`, error);
// Fallback to individual calls if multicall fails
for (const token of tokens.filter(
(t) =>
t.chain_name === chainName &&
(t.coin_type === "ERC20" || t.coin_type === "ZRC20")
)) {
try {
const contract = new ethers.Contract(
token.contract,
token.coin_type === "ERC20" ? ERC20_ABI.abi : ZRC20.abi,
provider
);
const balance = await contract.balanceOf(evmAddress);
}
})
);

await Promise.all(
tokens
.filter(
(token) =>
token.coin_type === "Gas" &&
![
"btc_testnet",
"btc_mainnet",
"solana_mainnet",
"solana_testnet",
"solana_devnet",
].includes(token.chain_name)
)
.map(async (token) => {
const chainLabel = Object.keys(this.getChains()).find(
(key) => this.getChains()[key].chain_id === parseInt(token.chain_id)
);
if (chainLabel) {
const rpc = await this.getEndpoint("evm", chainLabel);
const provider = new ethers.providers.StaticJsonRpcProvider(rpc);
const balance = await provider.getBalance(evmAddress);
const formattedBalance = formatUnits(balance, token.decimals);
balances.push({ ...token, balance: formattedBalance });
} catch (err) {
console.error(
`Failed to get balance for ${token.symbol} on ${chainName}:`,
err
);
}
}
}
})
);
})
);
}

await Promise.all(
tokens
.filter(
(token) =>
token.coin_type === "Gas" &&
!["btc_testnet", "btc_mainnet"].includes(token.chain_name)
["btc_testnet", "btc_mainnet"].includes(token.chain_name) &&
btcAddress
)
.map(async (token) => {
const chainLabel = Object.keys(this.getChains()).find(
(key) => this.getChains()[key].chain_id === parseInt(token.chain_id)
);
if (chainLabel) {
const rpc = await this.getEndpoint("evm", chainLabel);
const provider = new ethers.providers.StaticJsonRpcProvider(rpc);
const balance = await provider.getBalance(evmAddress);
const formattedBalance = formatUnits(balance, token.decimals);
balances.push({ ...token, balance: formattedBalance });
}
const API = this.getEndpoint("esplora", token.chain_name);
const response = await fetch(`${API}/address/${btcAddress}`);
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();
balances.push({ ...token, balance });
})
);

Expand All @@ -241,18 +274,31 @@ export const getBalances = async function (
.filter(
(token) =>
token.coin_type === "Gas" &&
["btc_testnet", "btc_mainnet"].includes(token.chain_name) &&
btcAddress
["solana_mainnet", "solana_testnet", "solana_devnet"].includes(
token.chain_name
) &&
solanaAddress
)
.map(async (token) => {
const API = this.getEndpoint("esplora", token.chain_name);
const response = await fetch(`${API}/address/${btcAddress}`);
const API = this.getEndpoint("solana", token.chain_name);
const response = await fetch(API, {
body: JSON.stringify({
id: 1,
jsonrpc: "2.0",
method: "getBalance",
params: [solanaAddress],
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
if (!response.ok) {
console.error("Failed to get balance for Solana", response);
return;
}
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();
const balance = r.result.value / 10 ** 9;
balances.push({ ...token, balance });
})
);
Expand Down
Loading

0 comments on commit d6148b2

Please sign in to comment.