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

feat: adds support for ipfs to orbit template #2012

Merged
merged 10 commits into from
Oct 29, 2024
3 changes: 3 additions & 0 deletions packages/scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,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
176 changes: 173 additions & 3 deletions packages/scripts/src/addOrbitChain/tests/transforms.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
/* eslint-disable jest/no-mocks-import */

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,
vi,
} from "vitest";
import { IncomingChainData } from "../schemas";
import {
extractRawChainData,
fetchAndProcessImage,
nameToSlug,
resizeImage,
stripWhitespace,
Expand All @@ -18,6 +25,8 @@ import {
mockIncomingChainData,
mockOrbitChain,
} from "./__mocks__/chainDataMocks";
import { warning } from "@actions/core";
import axios from "axios";

describe("Transforms", () => {
describe("extractRawChainData", () => {
Expand Down Expand Up @@ -193,4 +202,165 @@ 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";
fionnachan marked this conversation as resolved.
Show resolved Hide resolved
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();
});

it("should handle IPFS gateway URLs correctly", async () => {
const ipfsGatewayUrl =
"https://ipfs.io/ipfs/QmYAX3R4LhoFenKsMEq6nPBZzmNx9mNkQW1PUwqYfxK3Ym";
const { buffer, fileExtension } = await fetchAndProcessImage(
ipfsGatewayUrl
);

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

// Verify the image can be processed by sharp
const metadata = await sharp(buffer).metadata();
expect(metadata.format).toBeTruthy();
expect(metadata.width).toBeGreaterThan(0);
expect(metadata.height).toBeGreaterThan(0);
});

it("should convert IPFS protocol URLs to gateway URLs", async () => {
const ipfsProtocolUrl =
"ipfs://QmYAX3R4LhoFenKsMEq6nPBZzmNx9mNkQW1PUwqYfxK3Ym";
const { buffer, fileExtension } = await fetchAndProcessImage(
ipfsProtocolUrl
);

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

// Verify the image can be processed by sharp
const metadata = await sharp(buffer).metadata();
expect(metadata.format).toBeTruthy();
expect(metadata.width).toBeGreaterThan(0);
expect(metadata.height).toBeGreaterThan(0);
});

it("should handle invalid IPFS hashes gracefully", async () => {
const invalidIpfsUrl = "ipfs://InvalidHash123";
await expect(fetchAndProcessImage(invalidIpfsUrl)).rejects.toThrow();
});

it("should handle non-image buffers and convert to webp", async () => {
// Create a mock non-image buffer
const nonImageBuffer = Buffer.from("not an image");

// Mock axios to return our non-image buffer
const mockUrl = "https://example.com/unknown-file";
vi.spyOn(axios, "get").mockResolvedValueOnce({
status: 200,
data: nonImageBuffer,
headers: {
"content-type": "application/octet-stream",
},
});

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

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

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}`;
};
Loading