Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
577c14b
add canvas dep
gsteenkamp89 Jun 19, 2025
1262125
draw base layer image, serve with page & necessary tags
gsteenkamp89 Jun 19, 2025
34297ad
add unstyled component. build URL
gsteenkamp89 Jun 19, 2025
47f4f6c
style banner
gsteenkamp89 Jun 24, 2025
dad022d
fixup
gsteenkamp89 Jun 26, 2025
0612b9b
fix
gsteenkamp89 Jun 26, 2025
1e2bab7
fixes
gsteenkamp89 Jun 26, 2025
2c8fb3e
add transitive system deps for canvas
gsteenkamp89 Jun 26, 2025
6d31ea4
add canvas as a dev dependency
gsteenkamp89 Jun 26, 2025
d2b7406
test new dimensions
gsteenkamp89 Jun 26, 2025
e9cda1a
square image
gsteenkamp89 Jun 26, 2025
47550f7
serve better html
gsteenkamp89 Jun 27, 2025
52307bb
draw chain logos, cache, add redirect link
gsteenkamp89 Jun 30, 2025
7451509
fixup
gsteenkamp89 Jun 30, 2025
9b248a3
serve content based on user agent
gsteenkamp89 Jul 3, 2025
d3a101c
remove some duplicate icons
gsteenkamp89 Jul 3, 2025
a5cf7e2
use even 20px padding
gsteenkamp89 Jul 3, 2025
211a146
refactor
gsteenkamp89 Jul 3, 2025
a2a5d4e
update copy
gsteenkamp89 Jul 7, 2025
f5859cc
handle 0 seconds bridge
gsteenkamp89 Jul 15, 2025
9d59695
fixup
gsteenkamp89 Jul 15, 2025
52b1b5d
update UI copy
gsteenkamp89 Jul 18, 2025
4b3f186
add utm params to redirect
gsteenkamp89 Jul 18, 2025
d816bee
Merge branch 'master' into feat/twitter-share
gsteenkamp89 Jul 18, 2025
f543e56
fix tweet text
gsteenkamp89 Aug 4, 2025
eed9e29
fix aspect ratio
gsteenkamp89 Aug 4, 2025
96ab4c8
Merge branch 'master' into feat/twitter-share
gsteenkamp89 Aug 25, 2025
c5af293
update lock file
gsteenkamp89 Aug 25, 2025
a750b91
fixup
gsteenkamp89 Aug 27, 2025
672e9eb
update card styles, create base modal
gsteenkamp89 Aug 29, 2025
d0dd834
style modal
gsteenkamp89 Aug 29, 2025
a33ba86
update images
gsteenkamp89 Aug 29, 2025
c791255
add copy and download functionality
gsteenkamp89 Aug 29, 2025
42ffed3
fixup
gsteenkamp89 Aug 29, 2025
6b3494f
add animated copy button
gsteenkamp89 Aug 30, 2025
e2d07e4
Merge branch 'master' into feat/twitter-share
gsteenkamp89 Aug 31, 2025
73a0a91
create distinct frontend and api test environments
gsteenkamp89 Sep 1, 2025
92dc255
Merge branch 'master' into feat/twitter-share
gsteenkamp89 Sep 1, 2025
c31d838
test: rebuild canvas
james-a-morris Sep 1, 2025
439e2ff
use ready as default state
gsteenkamp89 Sep 1, 2025
1e86580
Merge branch 'master' into feat/twitter-share
gsteenkamp89 Sep 2, 2025
ee831c5
increase image scale
gsteenkamp89 Sep 2, 2025
1e5f0e1
fixup
gsteenkamp89 Sep 2, 2025
1a17be7
dynamic copy based on env var
gsteenkamp89 Sep 3, 2025
4571cca
edit copy
gsteenkamp89 Sep 3, 2025
f58fd1e
add solana icon
gsteenkamp89 Sep 5, 2025
938532b
update solana icon
gsteenkamp89 Sep 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/actions/setup/action.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
name: Setup Node.js and Dependencies
description: Setup Node.js environment and other system dependencies

runs:
using: composite
steps:
- uses: actions/checkout@v4

- name: Install canvas dependencies
run: sudo apt-get update && sudo apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
shell: bash
Comment on lines +9 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this be an issue on vercel?


- uses: actions/setup-node@v4
with:
node-version: 20
Expand All @@ -11,3 +18,7 @@ runs:

- run: yarn install --frozen-lockfile --ignore-scripts
shell: bash

- name: Build canvas package from source
run: npm rebuild canvas --build-from-source
shell: bash
8 changes: 0 additions & 8 deletions api/_dexes/lifi/utils/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const SOURCES = {
{ key: "1inch", names: ["1inch"] },
{ key: "openocean", names: ["openocean"] },
{ key: "kyberswap", names: ["kyberswap"] },
{ key: "lifidexaggregator", names: ["lifidexaggregator"] },
{ key: "sushiswap", names: ["sushiswap"] },
{ key: "okx", names: ["okx"] },
{ key: "merkle", names: ["merkle"] },
Expand All @@ -23,7 +22,6 @@ export const SOURCES = {
{ key: "1inch", names: ["1inch"] },
{ key: "openocean", names: ["openocean"] },
{ key: "kyberswap", names: ["kyberswap"] },
{ key: "lifidexaggregator", names: ["lifidexaggregator"] },
{ key: "sushiswap", names: ["sushiswap"] },
{ key: "okx", names: ["okx"] },
],
Expand All @@ -50,7 +48,6 @@ export const SOURCES = {
{ key: "1inch", names: ["1inch"] },
{ key: "openocean", names: ["openocean"] },
{ key: "kyberswap", names: ["kyberswap"] },
{ key: "lifidexaggregator", names: ["lifidexaggregator"] },
{ key: "sushiswap", names: ["sushiswap"] },
{ key: "okx", names: ["okx"] },
],
Expand All @@ -64,7 +61,6 @@ export const SOURCES = {
{ key: "odos", names: ["odos"] },
{ key: "1inch", names: ["1inch"] },
{ key: "openocean", names: ["openocean"] },
{ key: "lifidexaggregator", names: ["lifidexaggregator"] },
{ key: "sushiswap", names: ["sushiswap"] },
{ key: "okx", names: ["okx"] },
],
Expand All @@ -80,7 +76,6 @@ export const SOURCES = {
{ key: "1inch", names: ["1inch"] },
{ key: "openocean", names: ["openocean"] },
{ key: "kyberswap", names: ["kyberswap"] },
{ key: "lifidexaggregator", names: ["lifidexaggregator"] },
{ key: "sushiswap", names: ["sushiswap"] },
{ key: "okx", names: ["okx"] },
],
Expand Down Expand Up @@ -109,23 +104,20 @@ export const SOURCES = {
{ key: "odos", names: ["odos"] },
{ key: "openocean", names: ["openocean"] },
{ key: "kyberswap", names: ["kyberswap"] },
{ key: "lifidexaggregator", names: ["lifidexaggregator"] },
{ key: "sushiswap", names: ["sushiswap"] },
{ key: "okx", names: ["okx"] },
],
"60808": [{ key: "lifidexaggregator", names: ["lifidexaggregator"] }],
"81457": [
{ key: "openocean", names: ["openocean"] },
{ key: "kyberswap", names: ["kyberswap"] },
{ key: "lifidexaggregator", names: ["lifidexaggregator"] },
{ key: "sushiswap", names: ["sushiswap"] },
{ key: "okx", names: ["okx"] },
],
"534352": [
{ key: "odos", names: ["odos"] },
{ key: "openocean", names: ["openocean"] },
{ key: "kyberswap", names: ["kyberswap"] },
{ key: "lifidexaggregator", names: ["lifidexaggregator"] },
{ key: "sushiswap", names: ["sushiswap"] },
{ key: "okx", names: ["okx"] },
],
Expand Down
10 changes: 10 additions & 0 deletions api/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1563,6 +1563,16 @@ export function positiveIntStr() {
});
}

export function intStringInRange(from: number, to: number) {
return define<string>("intStringInRange", (value) => {
return (
Number.isInteger(Number(value)) &&
Number(value) >= from &&
Number(value) <= to
);
});
}

export function positiveFloatStr(maxValue?: number) {
return define<string>("positiveFloatStr", (value) => {
return Number(value) >= 0 && (maxValue ? Number(value) <= maxValue : true);
Expand Down
26 changes: 26 additions & 0 deletions api/twitter-share/_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getChainInfo } from "../_utils";
import path from "path";
import fs from "fs";

const assetsDir = path.join(__dirname, "assets");

export function getChainLogoPath(chainId: number): string | undefined {
try {
const chainName = getChainInfo(chainId)
.name.toLowerCase()
.replaceAll(" ", "-");
const logoPath = path.join(assetsDir, "chain-logos", `${chainName}.png`);

if (!fs.existsSync(logoPath)) {
throw new Error(`No chain logo found at path ${logoPath}`);
}

return logoPath;
} catch (e) {
console.warn(`Unable to find logo for chainId ${chainId}.`, {
cause: e,
chainId,
});
return undefined;
}
}
Binary file added api/twitter-share/assets/base/0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/base/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/base/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/base/3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/base/4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/base/5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/chain-logos/base.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/chain-logos/blast.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/chain-logos/bsc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/chain-logos/ink.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/chain-logos/lens.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/chain-logos/linea.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/chain-logos/lisk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/chain-logos/mode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/chain-logos/scroll.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/chain-logos/solana.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added api/twitter-share/assets/chain-logos/zora.png
115 changes: 115 additions & 0 deletions api/twitter-share/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { VercelResponse } from "@vercel/node";
import { createCanvas, loadImage } from "canvas";
import { TypedVercelRequest } from "../_types";
import { handleErrorCondition } from "../_errors";
import { getLogger, intStringInRange, positiveIntStr } from "../_utils";
import path from "path";

import { assert, Infer, type } from "superstruct";
import { getChainLogoPath } from "./_utils";
const assetsDir = path.join(__dirname, "assets");

const TwitterShareParamsSchema = type({
fill_time: intStringInRange(0, 5),
from_chain: positiveIntStr(),
to_chain: positiveIntStr(),
});

export type TwitterShareImageParams = Infer<typeof TwitterShareParamsSchema>;

export const CANVAS = {
width: 800,
height: 800,
};

const chainLogoDimensions = {
x: 89,
y: 89,
};

export default async function handler(
request: TypedVercelRequest<TwitterShareImageParams>,
response: VercelResponse
) {
const logger = getLogger();
try {
logger.debug({
at: "api/twitter-share",
message: "Query data",
query: request.query,
});
assert(request.query, TwitterShareParamsSchema);
const { fill_time, from_chain, to_chain } = request.query;
const canvas = createCanvas(CANVAS.width, CANVAS.height);
const ctx = canvas.getContext("2d");

const baseImage = await loadImage(
path.join(assetsDir, "base", `${fill_time}.png`)
);

// Get chain logo paths and load images only if they exist
const originChainLogoPath = getChainLogoPath(Number(from_chain));
const destinationChainLogoPath = getChainLogoPath(Number(to_chain));

const [originChainLogo, destinationChainLogo] = await Promise.all([
originChainLogoPath ? loadImage(originChainLogoPath) : null,
destinationChainLogoPath ? loadImage(destinationChainLogoPath) : null,
]);

// draw base layer with padding
ctx.drawImage(baseImage, 0, 0, CANVAS.width, CANVAS.height);

// Draw chain logos only if they exist (base layer contains fallback)
if (originChainLogo) {
ctx.drawImage(
originChainLogo,
182,
356,
chainLogoDimensions.x,
chainLogoDimensions.y
);
}
if (destinationChainLogo) {
ctx.drawImage(
destinationChainLogo,
529,
356,
chainLogoDimensions.x,
chainLogoDimensions.y
);
}

response.setHeader(
"Cache-Control",
`public, max-age=${60 * 60 * 24 * 30}, immutable`
); // 1 month
response.setHeader("Content-Type", "image/png");

const stream = canvas.createPNGStream();

// Handle stream errors
stream.on("error", (streamErr) => {
logger.error({
at: "api/twitter-share",
message: "PNG stream error",
error: streamErr,
});
if (!response.headersSent) {
response.status(500).end();
}
});

stream.pipe(response);
} catch (err) {
logger.error({
at: "api/twitter-share",
message: "Failed to generate Twitter share image",
error: err,
});

// Only handle error if response hasn't been sent yet
if (!response.headersSent) {
handleErrorCondition("twitter-share-image", response, logger, err);
}
}
}
8 changes: 3 additions & 5 deletions jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ module.exports = {
"^.+\\.svg$": "jest-transform-stub",
"^.+\\.png$": "jest-transform-stub",
},
// Only include test/api directory for API tests
testMatch: ["<rootDir>/test/api/**/*.test.{ts,tsx}"],
moduleNameMapper: {
"^components/(.*)$": "<rootDir>/src/components/$1",
"^utils/(.*)$": "<rootDir>/src/utils/$1",
"^hooks/(.*)$": "<rootDir>/src/hooks/$1",
"^assets/(.*)$": "<rootDir>/src/assets/$1",
"^data/(.*)$": "<rootDir>/src/data/$1",
// Only keep uuid mapping for API tests - remove frontend path mappings
uuid: require.resolve("uuid"),
},
};
26 changes: 26 additions & 0 deletions jest.frontend.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */

module.exports = {
setupFiles: ["<rootDir>/setup.jest.ts"],
preset: "ts-jest",
testEnvironment: "jsdom",
moduleDirectories: ["node_modules", "<rootDir>"],
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
transform: {
"^.+\\.tsx?$": "ts-jest",
"^.+\\.svg$": "jest-transform-stub",
"^.+\\.png$": "jest-transform-stub",
},
// Only include src directory for frontend tests
testMatch: ["<rootDir>/src/**/*.test.{ts,tsx}"],
// Exclude API modules that require Node.js dependencies
modulePathIgnorePatterns: ["<rootDir>/api/", "<rootDir>/test/api/"],
moduleNameMapper: {
"^components/(.*)$": "<rootDir>/src/components/$1",
"^utils/(.*)$": "<rootDir>/src/utils/$1",
"^hooks/(.*)$": "<rootDir>/src/hooks/$1",
"^assets/(.*)$": "<rootDir>/src/assets/$1",
"^data/(.*)$": "<rootDir>/src/data/$1",
uuid: require.resolve("uuid"),
},
};
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@
"remote-config": "tsx scripts/fetch-remote-config.ts",
"remote-env": "tsx ./scripts/fetch-remote-env.ts",
"analyze": "yarn build && rollup-plugin-visualizer --open ./bundle-size-analysis.json",
"test": "export REACT_APP_GIT_COMMIT_HASH=$(git rev-parse HEAD) && jest --env jsdom src",
"test": "export REACT_APP_GIT_COMMIT_HASH=$(git rev-parse HEAD) && jest --config jest.frontend.config.cjs",
"serve": "vite preview --port 3000",
"test-api": "yarn remote-config && jest test/api",
"test-api": "yarn remote-config && jest --config jest.config.cjs",
"pretest:e2e": "npm pkg set 'type'='module'",
"test:e2e:headful": "yarn pretest:e2e && playwright test",
"test:e2e:headless": "yarn pretest:e2e && HEADLESS=true playwright test",
Expand Down Expand Up @@ -161,6 +161,7 @@
"@vercel/node": "^5.0.2",
"@vitejs/plugin-react": "^4.3.4",
"axios-mock-adapter": "^1.21.2",
"canvas": "^3.1.2",
"buffer": "^6.0.3",
"chalk": "^5.3.0",
"chromatic": "^11.25.1",
Expand Down
Binary file added public/assets/twitter-bg.png
18 changes: 0 additions & 18 deletions src/assets/bg-banners/action-card-aqua-banner.svg
Diff not rendered.
26 changes: 26 additions & 0 deletions src/assets/bg-banners/action-card-banner-a.svg

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions src/assets/bg-banners/action-card-banner-b.svg

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions src/assets/bg-banners/action-card-banner-c.svg

Large diffs are not rendered by default.

14 changes: 0 additions & 14 deletions src/assets/bg-banners/action-card-teal-banner.svg
Diff not rendered.
4 changes: 4 additions & 0 deletions src/assets/icons/arrow-inbox.svg
Loading