Skip to content

Commit 74d68a1

Browse files
authored
tokens per quarter (#2749)
1 parent a319753 commit 74d68a1

File tree

2 files changed

+240
-0
lines changed

2 files changed

+240
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"compare-chains-metadata": "tsx --require tsconfig-paths/register src/scripts/chains-metadata.ts",
2020
"fetch-selectors": "npx tsx --require tsconfig-paths/register src/scripts/ccip/fetch-selectors.ts",
2121
"detect-new-tokens": "npx tsx --require tsconfig-paths/register src/scripts/ccip/detect-new-tokens.ts",
22+
"generate-token-report": "npx tsx --require tsconfig-paths/register src/scripts/ccip/generate-token-report.ts",
2223
"detect-new-data": "npx tsx --require tsconfig-paths/register src/scripts/data/detect-new-data.ts",
2324
"detect-trailing-slash-links": "tsx --require tsconfig-paths/register scripts/detect-trailing-slash-links.ts",
2425
"fix-trailing-slash-links": "tsx --require tsconfig-paths/register scripts/fix-trailing-slash-links.ts",
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/**
2+
* @file src/scripts/ccip/generate-token-report.ts
3+
* @description Script to generate a report of CCIP tokens added per quarter.
4+
*
5+
* This script scans the git history of the token configuration files to determine
6+
* when each token was added. It then generates a markdown report grouping
7+
* the tokens by the quarter of their introduction.
8+
*/
9+
10+
import fs from "fs"
11+
import path from "path"
12+
import { execFileSync } from "child_process"
13+
import { pino } from "pino"
14+
import { TokensConfig, LanesConfig, Environment, Version } from "../../config/data/ccip/types.js"
15+
import { loadReferenceData } from "../../config/data/ccip/data.js"
16+
17+
// ==============================
18+
// CONFIGURATION
19+
// ==============================
20+
21+
const SCRIPT_VERSION = "1.0.0"
22+
const ENVIRONMENT = Environment.Mainnet
23+
const VERSION = Version.V1_2_0
24+
const START_DATE = new Date("2024-01-01")
25+
const OUTPUT_DIR = ".tmp"
26+
const OUTPUT_FILE = path.join(OUTPUT_DIR, "tokens-by-quarter.md")
27+
28+
const logger = pino({
29+
level: process.env.LOG_LEVEL || "info",
30+
})
31+
32+
// ==============================
33+
// HELPER FUNCTIONS
34+
// ==============================
35+
36+
/**
37+
* Generate paths for the source files based on environment and version.
38+
*/
39+
function generateSourcePaths(environment: Environment, version: Version): { TOKENS_PATH: string; LANES_PATH: string } {
40+
const formatVersion = (v: string): string => `v${v.replace(/\./g, "_")}`
41+
const VERSION_PATH = formatVersion(version)
42+
const BASE_PATH = `src/config/data/ccip/${VERSION_PATH}/${environment}`
43+
return {
44+
TOKENS_PATH: `${BASE_PATH}/tokens.json`,
45+
LANES_PATH: `${BASE_PATH}/lanes.json`,
46+
}
47+
}
48+
49+
/**
50+
* Get file content from git history for a specific date.
51+
*/
52+
function getFileFromGitHistory(filePath: string, date: string): string | null {
53+
try {
54+
const commitHash = execFileSync(
55+
"git",
56+
["log", `--before=${date}T23:59:59`, "-n", "1", "--format=%H", "--", filePath],
57+
{
58+
encoding: "utf8",
59+
}
60+
).trim()
61+
62+
if (!commitHash) {
63+
logger.warn({ filePath, date }, "No commit found for file before specified date")
64+
return null
65+
}
66+
67+
return execFileSync("git", ["show", `${commitHash}:${filePath}`], { encoding: "utf8" })
68+
} catch (error) {
69+
logger.error(
70+
{ error: error instanceof Error ? error.message : String(error), filePath, date },
71+
`Error getting file ${filePath} from git history`
72+
)
73+
return null
74+
}
75+
}
76+
77+
/**
78+
* Build a map of token support across chains and lanes.
79+
*/
80+
function buildTokenSupportMap(tokensData: TokensConfig, lanesData: LanesConfig): Record<string, { lanes: string[] }> {
81+
const tokenSupport: Record<string, { lanes: string[] }> = {}
82+
83+
Object.keys(tokensData).forEach((tokenSymbol) => {
84+
tokenSupport[tokenSymbol] = { lanes: [] }
85+
})
86+
87+
Object.keys(lanesData).forEach((sourceChain) => {
88+
Object.keys(lanesData[sourceChain]).forEach((destChain) => {
89+
const lane = `${sourceChain}-to-${destChain}`
90+
const supportedTokens = lanesData[sourceChain][destChain].supportedTokens || {}
91+
Object.keys(supportedTokens).forEach((tokenSymbol) => {
92+
if (tokenSupport[tokenSymbol]) {
93+
tokenSupport[tokenSymbol].lanes.push(lane)
94+
}
95+
})
96+
})
97+
})
98+
99+
return tokenSupport
100+
}
101+
102+
/**
103+
* Gets the set of supported token symbols at a specific date.
104+
*/
105+
async function getSupportedTokensAtDate(date: string, tokensPath: string, lanesPath: string): Promise<Set<string>> {
106+
const tokensContent = getFileFromGitHistory(tokensPath, date)
107+
const lanesContent = getFileFromGitHistory(lanesPath, date)
108+
109+
if (!tokensContent || !lanesContent) {
110+
return new Set()
111+
}
112+
113+
try {
114+
const tokensData = JSON.parse(tokensContent) as TokensConfig
115+
const lanesData = JSON.parse(lanesContent) as LanesConfig
116+
const tokenSupportMap = buildTokenSupportMap(tokensData, lanesData)
117+
118+
const supportedTokens = Object.keys(tokenSupportMap).filter(
119+
(token) => tokenSupportMap[token] && tokenSupportMap[token].lanes.length > 0
120+
)
121+
122+
return new Set(supportedTokens)
123+
} catch (error) {
124+
logger.error(
125+
{ error: error instanceof Error ? error.message : String(error), date },
126+
"Failed to parse historical token data."
127+
)
128+
return new Set()
129+
}
130+
}
131+
132+
/**
133+
* Gets the token name from the token symbol.
134+
*/
135+
function getTokenName(tokenSymbol: string, tokensData: TokensConfig): string {
136+
const tokenDetails = tokensData[tokenSymbol]
137+
if (!tokenDetails) return tokenSymbol
138+
const sampleChain = Object.keys(tokenDetails)[0]
139+
return sampleChain && tokenDetails[sampleChain] ? tokenDetails[sampleChain].name || tokenSymbol : tokenSymbol
140+
}
141+
142+
// ==============================
143+
// MAIN SCRIPT LOGIC
144+
// ==============================
145+
146+
/**
147+
* Generates and writes the token report.
148+
*/
149+
async function generateReport() {
150+
logger.info(`Starting token report generation (v${SCRIPT_VERSION})`)
151+
logger.info(`Environment: ${ENVIRONMENT}, Version: ${VERSION}`)
152+
153+
if (!fs.existsSync(OUTPUT_DIR)) {
154+
fs.mkdirSync(OUTPUT_DIR)
155+
}
156+
157+
const { TOKENS_PATH, LANES_PATH } = generateSourcePaths(ENVIRONMENT, VERSION)
158+
const { tokensReferenceData: currentTokensData } = loadReferenceData({ environment: ENVIRONMENT, version: VERSION })
159+
160+
const reportData: Record<
161+
string,
162+
{
163+
tokens: string[]
164+
startDate: string
165+
endDate: string
166+
}
167+
> = {}
168+
const allTokensFound = new Set<string>()
169+
170+
// Use UTC dates to avoid timezone issues.
171+
let currentDate = new Date(START_DATE.toISOString().split("T")[0] + "T00:00:00Z")
172+
const now = new Date()
173+
174+
while (currentDate <= now) {
175+
const year = currentDate.getUTCFullYear()
176+
// getUTCMonth() is 0-indexed (0-11), so we can calculate the quarter directly.
177+
const quarter = Math.floor(currentDate.getUTCMonth() / 3) + 1
178+
const quarterKey = `Q${quarter}-${year}`
179+
180+
const startOfQuarter = new Date(Date.UTC(year, (quarter - 1) * 3, 1))
181+
const endOfQuarter = new Date(Date.UTC(year, quarter * 3, 0)) // Day 0 of next month is last day of current month
182+
183+
const dateForGit = endOfQuarter > now ? now : endOfQuarter
184+
const dateStringForGit = dateForGit.toISOString().split("T")[0]
185+
186+
logger.info(`Processing ${quarterKey} (up to ${dateStringForGit})...`)
187+
188+
const tokensAtEndOfQuarter = await getSupportedTokensAtDate(dateStringForGit, TOKENS_PATH, LANES_PATH)
189+
const newTokens = new Set([...tokensAtEndOfQuarter].filter((x) => !allTokensFound.has(x)))
190+
191+
if (newTokens.size > 0) {
192+
const newSortedTokens = Array.from(newTokens).sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }))
193+
reportData[quarterKey] = {
194+
tokens: newSortedTokens,
195+
startDate: startOfQuarter.toISOString().split("T")[0],
196+
endDate: endOfQuarter.toISOString().split("T")[0],
197+
}
198+
newTokens.forEach((token) => allTokensFound.add(token))
199+
}
200+
201+
// Move to the first day of the next quarter in UTC
202+
currentDate = new Date(Date.UTC(year, quarter * 3, 1))
203+
}
204+
205+
// Generate Markdown report
206+
let reportContent = `# CCIP Tokens Added by Quarter\n\n`
207+
reportContent += `*Report generated on: ${new Date().toUTCString()}*\n`
208+
reportContent += `*Environment: ${ENVIRONMENT}, Version: ${VERSION}*\n\n`
209+
210+
const sortedQuarters = Object.keys(reportData).sort((a, b) => {
211+
const [aQ, aY] = a.split("-")
212+
const [bQ, bY] = b.split("-")
213+
if (aY !== bY) return Number(aY) - Number(bY)
214+
return Number(aQ.substring(1)) - Number(bQ.substring(1))
215+
})
216+
217+
for (const quarter of sortedQuarters) {
218+
const { tokens, startDate, endDate } = reportData[quarter]
219+
reportContent += `## ${quarter} (${tokens.length} tokens)\n\n`
220+
reportContent += `*Period: ${startDate} to ${endDate}*\n\n`
221+
for (const tokenSymbol of tokens) {
222+
const tokenName = getTokenName(tokenSymbol, currentTokensData)
223+
reportContent += `- **${tokenSymbol}**: ${tokenName}\n`
224+
}
225+
reportContent += `\n`
226+
}
227+
228+
fs.writeFileSync(OUTPUT_FILE, reportContent)
229+
logger.info(`Report successfully generated at ${OUTPUT_FILE}`)
230+
}
231+
232+
// ==============================
233+
// EXECUTION
234+
// ==============================
235+
236+
generateReport().catch((err) => {
237+
logger.error(err, "An unexpected error occurred while generating the report.")
238+
process.exit(1)
239+
})

0 commit comments

Comments
 (0)