Skip to content

Instantly share code, notes, and snippets.

@afsardo
Last active February 13, 2024 13:06
Show Gist options
  • Save afsardo/44aa74664c3a322aba452b23541ce6f5 to your computer and use it in GitHub Desktop.
Save afsardo/44aa74664c3a322aba452b23541ce6f5 to your computer and use it in GitHub Desktop.
Generate asset list
export type Token = {
protocol: string;
symbol: string;
token: string;
icon?: string;
chain?: string;
decimals?: number;
chainId?: string;
priceUsd?: number;
isBlocked?: boolean;
isHidden?: boolean;
coingeckoId?: string;
originDenom?: string;
originChainId?: string;
};
export type TokenBaseList = Token[];
import fs from "fs";
import path from "path";
import { config as dotenvConfig } from "dotenv";
import prettier from "prettier";
import { TokenBaseList } from "~/types/local";
import { logger } from "~/utils/logger";
import { endTimer, startTimer } from "~/utils/timer";
import { downloadAndSaveImage, tokensDir } from "./utils";
dotenvConfig();
const repo = "astroport-fi/astroport-token-lists";
const GITHUB_RAW_DEFAULT_BASEURL = "https://raw.githubusercontent.com";
const BRANCH_NAME = "main";
function getFilePath({
chainName,
mainnet,
}: {
chainName: string;
mainnet: boolean;
}) {
return `tokenLists${mainnet ? "" : "/testnets"}/${chainName}.json`;
}
async function generateAssetLists({
chainAssets,
}: {
chainAssets: {
chainId: string;
assets: TokenBaseList;
}[];
}) {
let content = "";
content += `
import { Token } from "~/types/local";
export const AssetLists: { chainId: string; assets: Token[] }[] = ${JSON.stringify(
chainAssets,
null,
2,
// replace all icon paths with the generated path
).replaceAll("/img", tokensDir)};
`;
const prettierConfig = await prettier.resolveConfig("./");
const formatted = await prettier.format(content, {
...prettierConfig,
parser: "typescript",
});
const dirPath = "src/config/generated";
const fileName = "asset-lists.ts";
try {
const filePath = path.join(dirPath, fileName);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
fs.writeFileSync(filePath, formatted, {
encoding: "utf8",
flag: "w",
});
} catch (e) {
logger.error(`Error writing ${fileName}: ${e}`);
}
logger.debug("Successfully generated asset lists.");
}
async function generateAssetImages({ imageUrls }: { imageUrls: string[] }) {
startTimer("downloadImages");
logger.debug("Successfully downloaded images.");
for await (const url of imageUrls) {
await downloadAndSaveImage(url);
}
const endTime = endTimer("downloadImages");
logger.debug(
`Successfully downloaded images. (Execution time: ${endTime[0]}s ${endTime[1]}ms)`,
);
}
async function queryGithubFile<T>({
repo,
filePath,
commitHash,
baseUrl = GITHUB_RAW_DEFAULT_BASEURL,
}: {
repo: string;
filePath: string;
commitHash?: string;
baseUrl?: string;
}): Promise<T> {
const url = new URL(
`/${repo}/${commitHash ? commitHash : BRANCH_NAME}/${filePath}`,
baseUrl,
);
try {
const res = await fetch(url.toString());
return res.json();
} catch (e) {
logger.error(`Error fetching ${url.toString()}: ${e}`);
return Promise.reject(e);
}
}
const chains = [
{
name: "terra2",
chainId: "phoenix-1",
mainnet: true,
},
{
name: "neutron",
chainId: "neutron-1",
mainnet: true,
},
{
name: "injective",
chainId: "injective-1",
mainnet: true,
},
{
name: "sei",
chainId: "pacific-1",
mainnet: true,
},
{
name: "terra2",
chainId: "pisco-1",
mainnet: false,
},
{
name: "neutron",
chainId: "pion-1",
mainnet: false,
},
{
name: "injective",
chainId: "injective-888",
mainnet: false,
},
{
name: "sei",
chainId: "atlantic-2",
mainnet: false,
},
];
async function main() {
const responses = await Promise.all(
chains.map(async ({ chainId, name, mainnet }) => {
const filePath = getFilePath({ chainName: name, mainnet: mainnet });
const githubFile = await queryGithubFile<TokenBaseList>({
repo,
filePath,
});
return {
chainId: chainId,
assets: githubFile,
};
}),
);
let assetList: TokenBaseList = [];
responses.forEach((chain) => {
assetList = [...assetList, ...chain.assets];
});
await generateAssetLists({ chainAssets: responses });
await generateAssetImages({
imageUrls: assetList.map(
(asset) =>
`${GITHUB_RAW_DEFAULT_BASEURL}/${repo}/${BRANCH_NAME}${asset.icon}`,
),
});
}
main().catch((e) => {
logger.error(e);
process.exit(1);
});
import fs from "fs";
import path from "path";
import { Readable } from "stream";
import { finished } from "stream/promises";
export const tokensDir = "/tokens/generated";
export function getNodeImageRelativeFilePath(imageUrl: string) {
const urlParts = imageUrl.split("/");
const fileName = urlParts[urlParts.length - 1];
return path.join("/public", tokensDir, `${fileName}`);
}
/**
* Download an image from the provided URL and save it to the local file system.
* @param {string} imageUrl The URL of the image to download.
* @returns {Promise<string>} The filename of the saved image.
*/
export async function downloadAndSaveImage(imageUrl: string) {
// Ensure the tokens directory exists.
if (!fs.existsSync(path.resolve() + "/public" + tokensDir)) {
fs.mkdirSync(path.resolve() + "/public" + tokensDir, { recursive: true });
}
const filePath = path.resolve() + getNodeImageRelativeFilePath(imageUrl);
if (fs.existsSync(filePath)) {
return null;
}
// Fetch the image from the URL.
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(
`Failed to fetch image from ${imageUrl}: ${response.statusText}`,
);
}
// Save the image to the file system.
const fileStream = fs.createWriteStream(filePath, { flags: "w" });
await finished(
Readable.fromWeb(
response.body as import("stream/web").ReadableStream<any>,
).pipe(fileStream),
);
const splitPath = filePath.split("/");
return splitPath[splitPath.length - 1];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment