Skip to content

Commit

Permalink
get token details for any address in any chainId (#1272)
Browse files Browse the repository at this point in the history
* resolve arbitrary token details

* fixup

* use wrapped addresses to resolve native tokens

* create error class, refactor using multicall

* Update api/_utils.ts

Co-authored-by: Dong-Ha Kim <[email protected]>

* validate inputs, add cause to error

---------

Co-authored-by: Dong-Ha Kim <[email protected]>
  • Loading branch information
gsteenkamp89 and dohaki authored Nov 15, 2024
1 parent 1e36933 commit 19c0389
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 1 deletion.
9 changes: 9 additions & 0 deletions api/_errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ export class AcrossApiError extends Error {
}
}

export class TokenNotFoundError extends AcrossApiError {
constructor(args: { address: string; chainId: number; opts?: ErrorOptions }) {
super({
message: `Unable to find tokenDetails for address: ${args.address}, on chain with id: ${args.chainId}`,
status: HttpErrorToStatusCode.NOT_FOUND,
});
}
}

export class UnauthorizedError extends AcrossApiError {
constructor(args?: { message: string }, opts?: ErrorOptions) {
super(
Expand Down
106 changes: 105 additions & 1 deletion api/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
maxRelayFeePct,
relayerFeeCapitalCostConfig,
} from "./_constants";
import { PoolStateOfUser, PoolStateResult } from "./_types";
import { PoolStateOfUser, PoolStateResult, TokenInfo } from "./_types";
import {
buildInternalCacheKey,
getCachedValue,
Expand All @@ -64,6 +64,7 @@ import {
MissingParamError,
InvalidParamError,
RouteNotEnabledError,
TokenNotFoundError,
} from "./_errors";
import { Token } from "./_dexes/types";

Expand Down Expand Up @@ -2165,3 +2166,106 @@ export function paramToArray<T extends undefined | string | string[]>(
if (!param) return;
return Array.isArray(param) ? param : [param];
}

export type TokenOptions = {
chainId: number;
address: string;
};

const TTL_TOKEN_INFO = 30 * 24 * 60 * 60; // 30 days

function tokenInfoCache(params: TokenOptions) {
return makeCacheGetterAndSetter(
buildInternalCacheKey("tokenInfo", params.address, params.chainId),
TTL_TOKEN_INFO,
() => getTokenInfo(params),
(tokenDetails) => tokenDetails
);
}

export async function getCachedTokenInfo(params: TokenOptions) {
return tokenInfoCache(params).get();
}

// find decimals and symbol for any token address on any chain we support
export async function getTokenInfo({
chainId,
address,
}: TokenOptions): Promise<
Pick<TokenInfo, "address" | "name" | "symbol" | "decimals">
> {
try {
if (!ethers.utils.isAddress(address)) {
throw new InvalidParamError({
param: "address",
message: '"Address" must be a valid ethereum address',
});
}

if (!Number.isSafeInteger(chainId) || chainId < 0) {
throw new InvalidParamError({
param: "chainId",
message: '"chainId" must be a positive integer',
});
}

// ERC20 resolved statically
const token = Object.values(TOKEN_SYMBOLS_MAP).find((token) =>
Boolean(
token.addresses?.[chainId]?.toLowerCase() === address.toLowerCase()
)
);

if (token) {
return {
decimals: token.decimals,
symbol: token.symbol,
address: token.addresses[chainId],
name: token.name,
};
}

// ERC20 resolved dynamically
const provider = getProvider(chainId);

const erc20 = ERC20__factory.connect(
ethers.utils.getAddress(address),
provider
);

const calls = [
{
contract: erc20,
functionName: "decimals",
},
{
contract: erc20,
functionName: "symbol",
},
{
contract: erc20,
functionName: "name",
},
];

const [[decimals], [symbol], [name]] = await callViaMulticall3(
provider,
calls
);

return {
address,
decimals,
symbol,
name,
};
} catch (error) {
throw new TokenNotFoundError({
chainId,
address,
opts: {
cause: error,
},
});
}
}

0 comments on commit 19c0389

Please sign in to comment.