@xswap-link/sdk
Version:
JavaScript SDK for XSwap platform
194 lines (166 loc) • 5.39 kB
text/typescript
import { BALANCES_CHUNK_SIZE } from "@src/constants";
import { ADDRESSES, BatchQueryAbi, ERC20Abi } from "@src/contracts";
import { Chain, TokenBalances } from "@src/models";
import { chunkArray } from "@src/utils";
import { BigNumber, ethers } from "ethers";
export const IERC20 = new ethers.utils.Interface(ERC20Abi);
export const getBalanceOf = async (
wallet: string,
token: string,
rpcUrl: string,
): Promise<string> => {
const provider = ethers.getDefaultProvider(rpcUrl);
if (token === ethers.constants.AddressZero) {
return ethers.utils.formatEther(await provider.getBalance(wallet));
}
const contract = new ethers.Contract(token, ERC20Abi, provider);
return ethers.utils.formatUnits(
await contract.balanceOf(wallet),
await contract.decimals(),
);
};
export const getAllowanceOf = async (
token: string,
wallet: string,
spender: string,
rpcUrl: string,
): Promise<string> => {
const provider = ethers.getDefaultProvider(rpcUrl);
if (token === ethers.constants.AddressZero) {
return ethers.constants.MaxUint256.toString();
}
const contract = new ethers.Contract(token, ERC20Abi, provider);
return ethers.utils.formatUnits(
await contract.allowance(wallet, spender),
await contract.decimals(),
);
};
export const getBalances = async (
chain: Chain,
wallet: string,
): Promise<TokenBalances> => {
return {
...(await getNativeBalance(chain, wallet)),
...(await getErc20Balances(chain, wallet)),
};
};
const getNativeBalance = async (
chain: Chain,
wallet: string,
): Promise<TokenBalances> => {
const native = chain.tokens.find(
({ address }) =>
address.toLowerCase() === ethers.constants.AddressZero.toLowerCase(),
);
if (native) {
const provider = ethers.getDefaultProvider(chain.publicRpcUrls[0]);
const balance = await provider.getBalance(wallet);
return {
[native.address]: balance,
};
}
return {};
};
const getErc20Balances = async (
chain: Chain,
wallet: string,
): Promise<TokenBalances> => {
const erc20Tokens = chain.tokens.filter(
({ address }) => address !== ethers.constants.AddressZero,
);
const tokenChunks = chunkArray(erc20Tokens, BALANCES_CHUNK_SIZE);
const promises = tokenChunks.map(async (tokenChunk) => {
const tokenAddresses = tokenChunk.map((token) => token.address);
const calldatas = tokenChunk.map(() =>
IERC20.encodeFunctionData("balanceOf", [wallet]),
);
const contractAddress = ADDRESSES[chain.chainId]?.BatchQuery;
const rpcUrl = chain.publicRpcUrls[0];
if (contractAddress && rpcUrl) {
const contract = new ethers.Contract(
contractAddress,
BatchQueryAbi,
ethers.getDefaultProvider(rpcUrl),
);
return await contract["batchQuery"](tokenAddresses, calldatas);
}
return Promise.resolve();
});
const tokenBalances: BigNumber[] = (await Promise.all(promises))
.flat()
.map(
(encodedBalance) =>
ethers.utils.defaultAbiCoder.decode(["uint256"], encodedBalance)[0],
);
const balances: TokenBalances = {};
erc20Tokens.forEach((token, index) => {
balances[token.address] = tokenBalances[index];
});
return balances;
};
/**
* Calculate param's value offset within the encoded function's bytecode.
* @param abi
* @param funName
* @param funParams
* @param placeholderValue
*/
export const findPlaceholderIndex = (
abi: string,
funName: string,
// eslint-disable-next-line
funParams: any[],
placeholderValue: BigNumber,
) => {
if (
funParams.flat(Infinity).filter((param) => param === placeholderValue)
.length !== 1
) {
throw new Error("Random placeholder value must be provided and unique.");
}
const iface = new ethers.utils.Interface(abi);
const functionFragment = iface.getFunction(funName);
if (!functionFragment) {
throw new Error(`Can't find function "${funName}" in provided ABI.`);
}
const encodedFunData = iface.encodeFunctionData(functionFragment, funParams);
// Remove the 4-byte selector
const paramData = encodedFunData.slice(10);
// Split into 32-byte (64 hex characters) chunks
const chunks: string[] = [];
for (let i = 0; i < paramData.length; i += 64) {
chunks.push(paramData.slice(i, i + 64));
}
// Find the index of the chunk containing the random value
const searchValue = placeholderValue.toHexString().slice(2).padStart(64, "0");
const result = chunks.findIndex((chunk) => chunk === searchValue);
if (result === -1) {
throw new Error("Randomized parameter not found in the encoded data.");
}
return result;
};
export const getEventsFromReceipt = async (
receipt: ethers.providers.TransactionReceipt,
abi: ethers.ContractInterface,
rpcUrl: string,
eventName: string,
) => {
const contract = new ethers.Contract(
receipt.to,
abi,
ethers.getDefaultProvider(rpcUrl),
);
const eventSignature = contract.interface.getEvent(eventName).format();
return receipt.logs
.filter((log) => log.topics[0] === ethers.utils.id(eventSignature))
.map((log) => contract.interface.parseLog(log));
};
export const trimTxHash = (hash: string, beginAndEndCharacters: number = 5) => {
if (!hash) {
return "";
}
return `${hash?.substring(0, beginAndEndCharacters)}...${hash.substring(
hash.length - beginAndEndCharacters,
hash.length,
)}`;
};