Skip to content

Commit

Permalink
feat: adds support for ipfs to orbit template (#2012)
Browse files Browse the repository at this point in the history
Co-authored-by: Fionna Chan <[email protected]>
  • Loading branch information
douglance and fionnachan authored Oct 29, 2024
1 parent 15dbe2e commit 0464aa8
Show file tree
Hide file tree
Showing 7 changed files with 339 additions and 53 deletions.
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";
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

0 comments on commit 0464aa8

Please sign in to comment.