Skip to content

Commit

Permalink
feat: adds support for ipfs to orbit template
Browse files Browse the repository at this point in the history
  • Loading branch information
douglance committed Oct 21, 2024
1 parent 7251f61 commit b8a12a7
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 52 deletions.
3 changes: 3 additions & 0 deletions packages/scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@
"axios": "^1.7.7",
"commander": "^12.1.0",
"ethers": "^5.7.2",
"file-type": "^19.6.0",
"mime-types": "^2.1.35",
"sharp": "0.32.6",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/mime-types": "^2.1.4",
"@types/node": "^22.7.1",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
Expand Down
28 changes: 28 additions & 0 deletions packages/scripts/src/addOrbitChain/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,31 @@ const createPullRequest = async (
};

export { commitChanges, createBranch, createPullRequest, getContent, getIssue };

export const saveImageToGitHub = async (
branchName: string,
imageSavePath: string,
imageBuffer: Buffer
): Promise<void> => {
try {
// Check if the file already exists in the repository
let sha: string | undefined;
try {
const { data } = await getContent(branchName, imageSavePath);

if ("sha" in data) {
sha = data.sha;
}
} catch (error) {
// File doesn't exist, which is fine
console.log(`File ${imageSavePath} doesn't exist in the repository yet.`);
}

// Update or create the file in the repository
await updateContent(branchName, imageSavePath, imageBuffer, sha);
console.log(`Successfully saved image to GitHub at ${imageSavePath}`);
} catch (error) {
console.error("Error saving image to GitHub:", error);
throw error;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ Test Chain
### Chain description
A test chain.
### Chain logo
https://example.com/logo.png
### Chain logo
https://ipfs.io/ipfs/QmYAX3R4LhoFenKsMEq6nPBZzmNx9mNkQW1PUwqYfxK3Ym
### Brand color
#FF0000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`Transforms > extractRawChainData > should extract raw chain data from t
{
"bridge": "0x0000000000000000000000000000000000000002",
"chainId": "42161",
"chainLogo": "https://example.com/logo.png",
"chainLogo": "https://ipfs.io/ipfs/QmYAX3R4LhoFenKsMEq6nPBZzmNx9mNkQW1PUwqYfxK3Ym",
"childCustomGateway": "0x000000000000000000000000000000000000000E",
"childErc20Gateway": "0x000000000000000000000000000000000000000F",
"childGatewayRouter": "0x0000000000000000000000000000000000000010",
Expand Down
103 changes: 102 additions & 1 deletion packages/scripts/src/addOrbitChain/tests/transforms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import fs from "fs";
import path from "path";
import sharp from "sharp";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest";
import { IncomingChainData } from "../schemas";
import {
extractRawChainData,
fetchAndProcessImage,
nameToSlug,
resizeImage,
stripWhitespace,
Expand All @@ -18,6 +19,7 @@ import {
mockIncomingChainData,
mockOrbitChain,
} from "./__mocks__/chainDataMocks";
import { warning } from "@actions/core";

describe("Transforms", () => {
describe("extractRawChainData", () => {
Expand Down Expand Up @@ -193,4 +195,103 @@ describe("Transforms", () => {
expect(resizedAspectRatio).toBeCloseTo(originalAspectRatio, 2);
});
});
describe("Image Download and Processing", () => {
const downloadedImagePath = path.join(
process.cwd(),
"..",
"..",
"arb-token-bridge-ui",
"public",
"images",
"downloaded_chain_logo.png"
);

// Clean up downloaded image after tests
// Comment out the following 'after' block if you want to inspect the downloaded image
afterAll(() => {
if (fs.existsSync(downloadedImagePath)) {
fs.unlinkSync(downloadedImagePath);
console.log("Cleaned up downloaded image");
}
});

it("should download, process, and save the chain logo image from fullMockIssue", async () => {
const rawChainData = extractRawChainData(fullMockIssue);
const imageUrl = rawChainData.chainLogo as string;

expect(imageUrl).toBeTruthy();
expect(imageUrl.startsWith("https://")).toBe(true);

const { buffer, fileExtension } = await fetchAndProcessImage(imageUrl);

expect(buffer).toBeTruthy();
expect(buffer.length).toBeGreaterThan(0);
expect(fileExtension).toBeTruthy();

const fileName = "downloaded_chain_logo";
const savedImagePath = saveImageLocally(buffer, fileName, fileExtension);

expect(savedImagePath).toBeTruthy();
console.log(`Image downloaded and saved to: ${savedImagePath}`);

const fullSavePath = path.join(
process.cwd(),
"..",
"..",
"arb-token-bridge-ui",
"public",
savedImagePath
);
expect(fs.existsSync(fullSavePath)).toBe(true);

const stats = fs.statSync(fullSavePath);
expect(stats.size).toBeGreaterThan(0);
});

it("should download, process, and save the chain logo image from IPFS URL", async () => {
const ipfsUrl = "ipfs://QmYAX3R4LhoFenKsMEq6nPBZzmNx9mNkQW1PUwqYfxK3Ym";
const { buffer, fileExtension } = await fetchAndProcessImage(ipfsUrl);

expect(buffer).toBeTruthy();
expect(buffer.length).toBeGreaterThan(0);
expect(fileExtension).toBe(".png");
});

it("should throw an error if the image fetch fails", async () => {
const invalidUrl = "https://example.com/nonexistent-image.png";
await expect(fetchAndProcessImage(invalidUrl)).rejects.toThrow();
});
});
});

const saveImageLocally = (
imageBuffer: Buffer,
fileName: string,
fileExtension: string
): string => {
const imageSavePath = `images/${fileName}${fileExtension}`;
const fullSavePath = path.join(
process.cwd(),
"..",
"..",
"arb-token-bridge-ui",
"public",
imageSavePath
);

// Create directories if they don't exist
const dirs = path.dirname(fullSavePath);
if (!fs.existsSync(dirs)) {
fs.mkdirSync(dirs, { recursive: true });
}

if (fs.existsSync(fullSavePath)) {
warning(
`${fileName} already exists at '${imageSavePath}'. Overwriting the existing image.`
);
}

fs.writeFileSync(fullSavePath, imageBuffer);

return `/${imageSavePath}`;
};
114 changes: 67 additions & 47 deletions packages/scripts/src/addOrbitChain/transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */

import * as core from "@actions/core";
import { warning } from "@actions/core";
import axios from "axios";
import { fileTypeFromBuffer } from "file-type";
import * as fs from "fs";
import path from "path";
import sharp from "sharp";

import { lookup } from "mime-types";
import {
commitChanges,
createBranch,
createPullRequest,
getContent,
getIssue,
updateContent,
saveImageToGitHub,
} from "./github";
import {
chainDataLabelToKey,
Expand All @@ -30,6 +31,11 @@ import {
const SUPPORTED_IMAGE_EXTENSIONS = ["png", "svg", "jpg", "jpeg", "webp"];
const MAX_IMAGE_SIZE_KB = 100;

export const getFileExtension = (mimeType: string): string => {
const extension = lookup(mimeType);
return extension ? `.${extension}` : "";
};

export const initializeAndFetchData = async (): Promise<void> => {
core.startGroup("Initialization and Data Fetching");
console.log("Starting initialization and data fetching...");
Expand Down Expand Up @@ -262,49 +268,66 @@ export const resizeImage = async (
return resizedImage;
};

export const fetchAndSaveImage = async (
urlOrPath: string,
fileName: string,
branchName: string
): Promise<string> => {
const isLocalPath = !urlOrPath.startsWith("http");
if (isLocalPath) {
export const fetchAndProcessImage = async (
urlOrPath: string
): Promise<{ buffer: Buffer; fileExtension: string }> => {
let imageBuffer: Buffer;
let fileExtension: string;

// Check if the URL is an IPFS URL or starts with http/https
if (urlOrPath.startsWith("ipfs://") || urlOrPath.startsWith("http")) {
// Handle remote URLs (including IPFS)
if (urlOrPath.startsWith("ipfs://")) {
urlOrPath = `https://ipfs.io/ipfs/${urlOrPath.slice(7)}`;
}

console.log("Fetching image from:", urlOrPath);

const response = await axios.get(urlOrPath, {
responseType: "arraybuffer",
timeout: 10000, // 10 seconds timeout
});

if (response.status !== 200) {
throw new Error(`Failed to fetch image. Status: ${response.status}`);
}

imageBuffer = Buffer.from(response.data);

// Try to determine file extension from response headers
fileExtension = lookup(response.headers["content-type"] as string) || "";
if (!fileExtension) {
// If not found in headers, try to determine from the image data
const detectedType = await fileTypeFromBuffer(imageBuffer);
fileExtension = detectedType ? `.${detectedType.ext}` : "";
}
} else {
// Handle local paths
const localPath = `../../arb-token-bridge-ui/public/${urlOrPath}`;
if (!fs.existsSync(localPath)) {
throw new Error(
`Provided local path '${localPath}' did not match any existing images.`
);
}
return urlOrPath;
imageBuffer = fs.readFileSync(localPath);
fileExtension = path.extname(localPath);
}

const fileExtension = urlOrPath.split(".").pop()?.split("?")[0] || "";
if (!SUPPORTED_IMAGE_EXTENSIONS.includes(fileExtension)) {
throw new Error(
`Invalid image extension '${fileExtension}'. Expected: ${SUPPORTED_IMAGE_EXTENSIONS.join(
", "
)}.`
);
// If still not found, default to .png
if (!fileExtension) {
console.warn("Could not determine file type, defaulting to .png");
fileExtension = ".png";
}

const imageSavePath = `images/${fileName}.${fileExtension}`;
const fullSavePath = `../../arb-token-bridge-ui/public/${imageSavePath}`;

// Create directories if they don't exist
const dirs = fullSavePath.split("/").slice(0, -1).join("/");
if (!fs.existsSync(dirs)) {
fs.mkdirSync(dirs, { recursive: true });
}

if (fs.existsSync(fullSavePath)) {
warning(
`${fileName} already exists at '${imageSavePath}'. Using the existing image.`
if (!SUPPORTED_IMAGE_EXTENSIONS.includes(fileExtension.replace(".", ""))) {
console.warn(
`Unsupported image extension '${fileExtension}'. Converting to PNG.`
);
return `/${imageSavePath}`;
}

const response = await axios.get(urlOrPath, { responseType: "arraybuffer" });
let imageBuffer = Buffer.from(response.data);
// Convert the image to PNG using sharp
imageBuffer = await sharp(imageBuffer).png().toBuffer();
fileExtension = ".png";
}

// Resize the image
try {
Expand All @@ -317,20 +340,17 @@ export const fetchAndSaveImage = async (
}
}

// Check if the file already exists in the repository
let sha: string | undefined;
try {
const { data } = await getContent(branchName, imageSavePath);

if ("sha" in data) {
sha = data.sha;
}
} catch (error) {
// File doesn't exist, which is fine
}

await updateContent(branchName, imageSavePath, imageBuffer, sha);
return { buffer: imageBuffer, fileExtension };
};

export const fetchAndSaveImage = async (
urlOrPath: string,
fileName: string,
branchName: string
): Promise<string> => {
const { buffer, fileExtension } = await fetchAndProcessImage(urlOrPath);
const imageSavePath = `images/${fileName}${fileExtension}`;
await saveImageToGitHub(branchName, imageSavePath, buffer);
return `/${imageSavePath}`;
};

Expand Down
Loading

0 comments on commit b8a12a7

Please sign in to comment.