Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mintlist-cli: Adding a bump comand that diffs mintlists and overrides #36

Merged
merged 9 commits into from
Apr 27, 2023
4 changes: 2 additions & 2 deletions packages/mintlist-cli/package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "@orca-so/mintlist-cli",
"version": "0.0.1-alpha.0",
"version": "0.0.1",
"license": "Apache-2.0",
"main": "dist/index.js",
"dependencies": {
"@orca-so/token-sdk": "^0.1.5-alpha.0",
"@orca-so/token-sdk": "^0.1.5",
"@solana/web3.js": "^1.73.3",
"commander": "^10.0.0",
"mz": "^2.7.0"
Expand Down
11 changes: 7 additions & 4 deletions packages/mintlist-cli/src/cmd/add-mint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MintlistFileUtil } from "../util/mintlist-file-util";
export function addMint(mintlistPath: string, addMints: string[]) {
let mintlist = MintlistFileUtil.readMintlistSync(mintlistPath);
const mints = mintlist.mints;
let numAdded = 0;
const addedMints = [];
for (const mint of addMints) {
// Check mint is valid pubkey
try {
Expand All @@ -17,17 +17,20 @@ export function addMint(mintlistPath: string, addMints: string[]) {
// Check mint doesn't already exist
const exists = mints.indexOf(mint) !== -1;
if (exists) {
console.log(`Mint ${mint} already exists in ${mintlist.name} (${mintlistPath})`);
continue;
}

mints.push(mint);
numAdded++;
addedMints.push(mint);
}

mints.sort();
mintlist.mints = mints;
MintlistFileUtil.writeJsonSync(mintlistPath, mintlist);

console.log(`Added ${numAdded} mints to ${mintlist.name} (${mintlistPath})`);
if (addedMints.length === 0) {
console.log("No mints added");
} else {
console.log(`Added ${addedMints.length} mints to ${mintlist.name}:\n${addedMints.join("\n")}`);
}
}
163 changes: 163 additions & 0 deletions packages/mintlist-cli/src/cmd/bump.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { Mintlist } from "@orca-so/token-sdk";
import { execSync } from "child_process";
import { MetadataOverrides, MintlistFileUtil } from "../util/mintlist-file-util";

interface BumpOptions {
before: string;
after: string;
}

type VersionChange = "major" | "minor" | "patch";

/**
* Bumps the version of the package based on the changes in the mintlists.
* If a mintlist is added or removed, a major version bump is performed.
* If the only change is removed mints, a minor version bump is performed.
* If the only change is added mints or updated overrides, a patch version bump is performed.
*/
export function bump({ before, after }: BumpOptions) {
if (!hasDeps()) {
console.log("npm and git must be installed and in the PATH");
process.exit(1);
}

if (hasUncommittedChanges()) {
console.log("Must have clear working directory");
process.exit(1);
}

const versionChange = getVersionChange(before, after);
if (!versionChange) {
return;
}

const version = execSync(`npm version ${versionChange}`, { encoding: "utf-8" }).trim();
console.log(`${version}`);
}

function getVersionChange(before: string, after: string): VersionChange | undefined {
if (hasMintlistChanges(before, after)) {
return "major";
}
const { hasAdded, hasRemoved } = diffMintlists(before, after);
if (hasRemoved) {
return "minor";
}
if (hasAdded || diffOverrides(before, after)) {
return "patch";
}
}

function diffOverrides(before: string, after: string): boolean {
const beforeOverrides = MintlistFileUtil.fromString<MetadataOverrides>(
execSync(`git show ${before}:src/overrides.json`, {
terranmoccasin marked this conversation as resolved.
Show resolved Hide resolved
encoding: "utf-8",
})
);
const afterOverrides = MintlistFileUtil.fromString<MetadataOverrides>(
execSync(`git show ${after}:src/overrides.json`, {
encoding: "utf-8",
})
);

if (Object.keys(beforeOverrides).length !== Object.keys(afterOverrides).length) {
return true;
}

for (const [mint, metadata] of Object.entries(beforeOverrides)) {
if (
!afterOverrides[mint] ||
metadata.image !== afterOverrides[mint].image ||
metadata.name !== afterOverrides[mint].name ||
metadata.symbol !== afterOverrides[mint].symbol
) {
return true;
}
}

return false;
}

function diffMintlists(before: string, after: string): { hasAdded: boolean; hasRemoved: boolean } {
const files = MintlistFileUtil.toValidFilePaths(
execSync(`git diff --name-only --diff-filter=ad ${before} ${after}`, {
encoding: "utf-8",
})
);

let hasAdded = false;
let hasRemoved = false;
for (const filePath of files) {
if (!exists(before, filePath) || !exists(after, filePath)) {
continue;
}

const beforeFile = MintlistFileUtil.fromString<Mintlist>(
execSync(`git show ${before}:${filePath}`, { encoding: "utf-8" })
);
const afterFile = MintlistFileUtil.fromString<Mintlist>(
execSync(`git show ${after}:${filePath}`, { encoding: "utf-8" })
);
if (hasAddedMints(beforeFile, afterFile)) {
hasAdded = true;
}
if (hasAddedMints(afterFile, beforeFile)) {
hasRemoved = true;
}
}

return { hasAdded, hasRemoved };
}

// Returns true if the right mintlist has mints that are not in the left mintlist
function hasAddedMints(left: Mintlist, right: Mintlist): boolean {
const beforeSet = new Set(left.mints);
for (const mint of right.mints) {
if (!beforeSet.has(mint)) {
return true;
}
}
return false;
}

function exists(hash: string, filePath: string) {
const flag = execSync(`git ls-tree ${hash} ${filePath} | wc -l`, { encoding: "utf-8" })
.trim()
.split("\n")[0];
return flag === "1";
}

// Checks if a mintlist.json file was added or removed from the src/mintlists directory
// Returns true if a mintlist.json file was added or removed, false otherwise
function hasMintlistChanges(before: string, after: string) {
const beforeMintlists = MintlistFileUtil.toValidFilePaths(
execSync(`git ls-tree --name-only ${before} src/**/**`, {
encoding: "utf-8",
})
);
const afterMintlists = MintlistFileUtil.toValidFilePaths(
execSync(`git ls-tree --name-only ${after} src/**/**`, {
encoding: "utf-8",
})
);
return beforeMintlists.sort().toString() !== afterMintlists.sort().toString();
}

// Checks if npm and git are installed and in the PATH
// Returns true if both are installed, false otherwise
function hasDeps() {
try {
execSync("git --version", { stdio: "ignore" });
execSync("npm --version", { stdio: "ignore" });
return true;
} catch (e) {
return false;
}
}

// Checks if there are uncommitted changes in the working tree
// Returns true if there are uncommitted changes, false otherwise
function hasUncommittedChanges() {
const diff = execSync("git diff --name-only", { encoding: "utf-8" });
return diff.length > 0;
}
9 changes: 6 additions & 3 deletions packages/mintlist-cli/src/cmd/gen-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export function genIndex(opts: any) {
if (!isOpts(opts)) {
throw new Error("Invalid options");
}
const { outDir, inDir } = opts;
const { outDir, inDir, outFile } = opts;
const inPath = resolve(inDir);
const outPath = resolve(outDir);
const files = getFilePaths(inPath);
Expand All @@ -28,7 +28,7 @@ export function genIndex(opts: any) {
mkdirSync(outPath);
}

writeFileSync(`${outPath}/index.ts`, contents);
writeFileSync(`${outPath}/${outFile}`, contents);
}

function toExportLine(filePath: string): string {
Expand Down Expand Up @@ -99,8 +99,11 @@ import { Mintlist, Tokenlist } from "@orca-so/token-sdk";`;
interface Opts {
outDir: string;
inDir: string;
outFile: string;
}

function isOpts(opts: any): opts is Opts {
return !!opts && typeof opts === "object" && "outDir" in opts && "inDir" in opts;
return (
!!opts && typeof opts === "object" && "outDir" in opts && "inDir" in opts && "outFile" in opts
);
}
26 changes: 10 additions & 16 deletions packages/mintlist-cli/src/cmd/gen-tokenlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { Connection } from "@solana/web3.js";
import { existsSync, mkdirSync } from "mz/fs";
import { MintlistFileUtil } from "../util/mintlist-file-util";
import { resolve } from "path";
import path from "node:path";

export async function genTokenlist(paths: string[], opts: any) {
if (!isOpts(opts)) {
Expand All @@ -24,10 +23,10 @@ export async function genTokenlist(paths: string[], opts: any) {
}

const params = paths
.filter((path) => MintlistFileUtil.validMintlistName(getFileName(path)))
.filter((path) => MintlistFileUtil.validMintlistName(MintlistFileUtil.getFileName(path)))
.map((path) => ({
path: resolve(path),
tokenlistPath: `${outPath}/${toTokenlistFileName(getFileName(path))}`,
tokenlistPath: `${outPath}/${toTokenlistFileName(MintlistFileUtil.getFileName(path))}`,
mintlist: MintlistFileUtil.readMintlistSync(path),
}));

Expand All @@ -41,12 +40,16 @@ async function createFetcher(opts: Opts): Promise<TokenFetcher> {
if (!process.env.SOLANA_NETWORK) {
throw new Error("SOLANA_NETWORK must be set");
}
const connection = new Connection(process.env.SOLANA_NETWORK);
const fetcher = TokenFetcher.from(connection);
if (opts.overrides) {
fetcher.addProvider(new FileSystemProvider(MintlistFileUtil.readOverridesSync(opts.overrides)));
let overrides;
try {
overrides = MintlistFileUtil.readOverridesSync("./src/overrides.json");
} catch (e) {
throw new Error("overrides.json file not found");
}
const connection = new Connection(process.env.SOLANA_NETWORK);
const fetcher = new TokenFetcher(connection);
fetcher
.addProvider(new FileSystemProvider(overrides))
.addProvider(new MetaplexProvider(connection, { concurrency: 10, intervalMs: 1000 }))
.addProvider(new SolanaFmProvider({ concurrency: 5, intervalMs: 1000 }))
.addProvider(
Expand All @@ -55,14 +58,6 @@ async function createFetcher(opts: Opts): Promise<TokenFetcher> {
return fetcher;
}

function getFileName(filePath: string): string {
const name = filePath.split(path.sep).pop();
if (!name) {
throw new Error("Invalid path");
}
return name;
}

function toTokenlistFileName(mintlistName: string): string {
return mintlistName.replace(".mintlist", ".tokenlist");
}
Expand All @@ -78,7 +73,6 @@ async function fetchTokenlist(fetcher: TokenFetcher, mintlist: Mintlist): Promis

interface Opts {
outDir: string;
overrides?: string;
}

function isOpts(opts: any): opts is Opts {
Expand Down
10 changes: 9 additions & 1 deletion packages/mintlist-cli/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { genTokenlist } from "./cmd/gen-tokenlist";
import { genIndex } from "./cmd/gen-index";
import { addMint } from "./cmd/add-mint";
import { removeMint } from "./cmd/remove-mint";
import { bump } from "./cmd/bump";

const program = new Command();

Expand Down Expand Up @@ -32,7 +33,6 @@ program
"Output directory for generated tokenlists",
"./src/tokenlists"
)
.option("--overrides <file>", "Path to overrides file")
.action(genTokenlist);

program
Expand All @@ -44,6 +44,14 @@ program
"./src"
)
.requiredOption("--outDir <string>", "Output directory for generated index.ts", "./src")
.requiredOption("--outFile <string>", "Output file name", "index.ts")
.action(genIndex);

program
.command("bump")
.description("Bump package version")
.requiredOption("--before <string>", "Commit hash before bump", "HEAD~1")
.requiredOption("--after <string>", "Commit hash after bump", "HEAD")
.action(bump);

export default program;
Loading