diff --git a/packages/widget/.DS_Store b/packages/widget/.DS_Store new file mode 100644 index 0000000000..5172429f26 Binary files /dev/null and b/packages/widget/.DS_Store differ diff --git a/packages/widget/.eslintrc.js b/packages/widget/.eslintrc.js new file mode 100644 index 0000000000..f1730e77ad --- /dev/null +++ b/packages/widget/.eslintrc.js @@ -0,0 +1,196 @@ +module.exports = { + root: true, + env: { + browser: true, + es6: true, + }, + ignorePatterns: [ + 'dist', + 'examples', + 'rollup.config.js', + 'src/hooks/useBridgeTxStatus.tsx', + 'src/utils/generateTheme.ts', + ], + extends: ['plugin:prettier/recommended'], + parser: '@babel/eslint-parser', + parserOptions: { + es6: true, + ecmaVersion: 6, + sourceType: 'module', + requireConfigFile: false, + }, + plugins: [ + 'eslint-plugin-import', + 'eslint-plugin-unicorn', + 'eslint-plugin-jsdoc', + 'eslint-plugin-prefer-arrow', + 'eslint-plugin-react', + '@typescript-eslint', + ], + overrides: [ + { + files: ['**/*.ts', '**/*.tsx'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: './packages/**/tsconfig.json', + sourceType: 'module', + allowAutomaticSingleRunInference: true, + }, + rules: { + '@typescript-eslint/comma-dangle': 'off', + '@typescript-eslint/adjacent-overload-signatures': 'error', + '@typescript-eslint/array-type': 'off', + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/consistent-type-assertions': 'error', + '@typescript-eslint/dot-notation': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/indent': 'off', + '@typescript-eslint/member-delimiter-style': [ + 'off', + { + multiline: { + delimiter: 'none', + requireLast: true, + }, + singleline: { + delimiter: 'semi', + requireLast: false, + }, + }, + ], + '@typescript-eslint/member-ordering': 'off', + '@typescript-eslint/naming-convention': 'off', + '@typescript-eslint/no-empty-function': 'error', + '@typescript-eslint/no-empty-interface': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-misused-new': 'error', + '@typescript-eslint/no-namespace': 'error', + '@typescript-eslint/no-parameter-properties': 'off', + '@typescript-eslint/no-shadow': [ + 'error', + { + hoist: 'all', + }, + ], + '@typescript-eslint/no-this-alias': 'error', + '@typescript-eslint/no-unused-expressions': 'off', + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/no-var-requires': 'error', + '@typescript-eslint/prefer-for-of': 'error', + '@typescript-eslint/prefer-function-type': 'error', + '@typescript-eslint/prefer-namespace-keyword': 'error', + '@typescript-eslint/quotes': 'off', + '@typescript-eslint/semi': ['off', null], + '@typescript-eslint/triple-slash-reference': [ + 'error', + { + path: 'always', + types: 'prefer-import', + lib: 'always', + }, + ], + '@typescript-eslint/type-annotation-spacing': 'off', + '@typescript-eslint/unified-signatures': 'error', + '@typescript-eslint/no-unused-vars': 'off', + }, + }, + ], + rules: { + 'prettier/prettier': 'warn', + 'arrow-parens': ['off', 'always'], + 'brace-style': ['off', 'off'], + 'comma-dangle': 'off', + complexity: 'off', + 'constructor-super': 'error', + curly: 'off', + 'dot-notation': 'off', + 'eol-last': 'off', + eqeqeq: ['error', 'smart'], + 'guard-for-in': 'off', + 'id-blacklist': 'off', + 'id-match': 'off', + 'import/no-extraneous-dependencies': ['error'], + 'import/no-internal-modules': 'off', + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal'], + 'newlines-between': 'always', + }, + ], + indent: 'off', + 'jsdoc/check-alignment': 'error', + 'jsdoc/check-indentation': 'error', + 'jsdoc/newline-after-description': 'error', + 'linebreak-style': 'off', + 'max-classes-per-file': 'off', + 'max-len': 'off', + 'new-parens': 'off', + 'newline-per-chained-call': 'off', + 'no-bitwise': 'off', + 'no-caller': 'error', + 'no-cond-assign': 'error', + 'no-console': 'off', + 'no-debugger': 'error', + 'no-duplicate-case': 'error', + 'no-duplicate-imports': 'error', + 'no-empty': 'error', + 'no-eval': 'error', + 'no-extra-bind': 'error', + 'no-extra-semi': 'off', + 'no-fallthrough': 'off', + 'no-invalid-this': 'off', + 'no-irregular-whitespace': 'off', + 'no-multiple-empty-lines': 'off', + 'no-new-func': 'error', + 'no-new-wrappers': 'error', + 'no-redeclare': 'error', + 'no-return-await': 'error', + 'no-sequences': 'error', + 'no-sparse-arrays': 'error', + 'no-template-curly-in-string': 'error', + 'no-throw-literal': 'error', + 'no-trailing-spaces': 'off', + 'no-undef-init': 'error', + 'no-underscore-dangle': 'off', + 'no-unsafe-finally': 'error', + 'no-unused-expressions': 'off', + 'no-unused-labels': 'error', + 'no-use-before-define': 'off', + 'no-var': 'error', + 'object-shorthand': 'error', + 'one-var': ['error', 'never'], + 'padded-blocks': [ + 'off', + { + blocks: 'never', + }, + { + allowSingleLineBlocks: true, + }, + ], + 'prefer-arrow/prefer-arrow-functions': 'warn', + 'prefer-const': 'error', + 'prefer-object-spread': 'error', + 'quote-props': 'off', + quotes: 'off', + radix: 'error', + 'react/jsx-curly-spacing': 'off', + 'react/jsx-equals-spacing': 'off', + 'react/jsx-tag-spacing': [ + 'off', + { + afterOpening: 'allow', + closingSlash: 'allow', + }, + ], + 'react/jsx-wrap-multilines': 'off', + semi: 'off', + 'space-before-blocks': 'error', + 'space-before-function-paren': 'off', + 'space-in-parens': ['off', 'never'], + 'unicorn/prefer-ternary': 'off', + 'use-isnan': 'error', + 'valid-typeof': 'off', + }, +} diff --git a/packages/widget/.gitignore b/packages/widget/.gitignore new file mode 100644 index 0000000000..b2d59d1f75 --- /dev/null +++ b/packages/widget/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/dist \ No newline at end of file diff --git a/packages/widget/.nvmrc b/packages/widget/.nvmrc new file mode 100644 index 0000000000..e2838c8b88 --- /dev/null +++ b/packages/widget/.nvmrc @@ -0,0 +1 @@ +v16.14.0 \ No newline at end of file diff --git a/packages/widget/.prettierrc.json b/packages/widget/.prettierrc.json new file mode 100644 index 0000000000..54eb1fc7c6 --- /dev/null +++ b/packages/widget/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/prettierrc", + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true, + "arrowParens": "always" + } \ No newline at end of file diff --git a/packages/widget/.vscode/settings.json b/packages/widget/.vscode/settings.json new file mode 100644 index 0000000000..3e4336e4b3 --- /dev/null +++ b/packages/widget/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "editor.formatOnSave": true, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + } \ No newline at end of file diff --git a/packages/widget/README.md b/packages/widget/README.md new file mode 100644 index 0000000000..fd336ddc62 --- /dev/null +++ b/packages/widget/README.md @@ -0,0 +1,143 @@ +This explains how to integrate the bridge widget into your dApp in just a few minutes. This widget enables users to bridge tokens directly on your site, utilizing the Synapse Protocol. + +Dark Theme + +Live version of the widget: + +/_ link to landing page to come _/ + +Example use cases include: + +- Building a custom frontend for the Synapse Protocol +- Bridging assets in a DeFi application +- Acquiring a token to participate in a web3 game + +This guide shows how to customize the widget to seamlessly blend with your app's theme by altering colors, fonts, and the token list. Learn to make the widget appear as an integral part of your application. + +## Installation + +The widget is available on npm or yarn. + +npm: + +```bash +npm install @synapsecns/widget +``` + +yarn: + +```bash +yarn add @synapsecns/widget +``` + +Note: The widget's `peerDependencies` require the consumer app to use `react` and `react-dom` (`>=17.0.1`) + +## Installation + +To get started, import the `Widget` React component into your App. You will need a `web3Provider` parameter to pass to the widget. The demo landing page app, for example, defines this provider from the `ethers` library. However, the component supports any similar provider: + +```tsx +import { Bridge } from ‘@synapsecns/widget’ + +const MyApp = () => { + const web3Provider = new ethers.BrowserProvider(window.ethereum) + + +} + +``` + +Your site should now display a fully operational bridge widget integrating the routes and tokens supported by the Synapse protocol. By utilizing Synapse's multiple routers, you will be able to find the best quotes to support your bridging use case. + +## Recommended Parameters + +The bridge widget is a React component designed for straightforward integration into any React-based project. Engineered for immediate functionality, and apart from a `web3Provider`, it requires no initial parameters or web3 setup to begin operation. The widget facilitates bridging across all networks where the Synapse Protocol is active. + +While the widget is primed for immediate use without configuration as it provides some basic primary and fallback JSON-RPC endpoints, we encourage developers to specify their own for enhancd performance. This can be done by including a `customRpcs` parameter in the format of an object with chain ids as keys and their associated RPC endpoints as values. + +```tsx +import { Bridge, CustomRpcs } from ‘@synapsecns/widget’ + +const customRpcs: CustomRpcs = { + 1: 'https://ethereum.my-custom-rpc.com', + 10: 'https://optimism.my-custom-rpc.com', + 42161: 'https://arbitrum.my-custom-rpc.com', +} + +const MyApp = () => { + const web3Provider = new ethers.BrowserProvider(window.ethereum) + + +} +``` + +## Token and Chain Customization + +To further tailor the bridge widget to meet the specific demands of your project, additional optional `targetTokens` and `targetChainIds` parameters are provided. These allow for customizing which chain and tokens your consuming application will support bridging to. This is effectively a way to filter for specific tokens on destination chain your application's users bridge. + +```tsx +import { Bridge, CustomRpcs, ETH, USDC, USDT } from ‘@synapsecns/widget’ + +const MyApp = () => { + const web3Provider = new ethers.BrowserProvider(window.ethereum) + + +} +``` + +Note: Token naming convention is based on the tokens provided by `@synapsecns/widget`. For example, USDC on Metis is `METISUSDC` instead of simply `USDC`. The package's `src/constants/bridgeable.ts` file contains a detailed list of supported tokens and the chains they live on. Additionally, to see a detailed list of Synapse Protocol supported chains, please see `src/constants/chains.ts`. + +## Theme Customization + +The widget is designed to be easily customized to match your app's theme. The widget accepts an optional `customTheme` configurable `bgColor` parameter for `'dark'` and `'light'` modes: + +```tsx + +``` + +Additionally, the widget supports more complex custom themes with the `customTheme` property. This allows for more fine-grained control over the widget's colors and fonts. + +```tsx +const customTheme = { + '--synapse-text': 'white', + '--synapse-secondary': '#ffffffb3', + '--synapse-root': '#16182e', + '--synapse-surface': 'linear-gradient(90deg, #1e223de6, #262b47e6)', + '--synapse-border': 'transparent', + '--synapse-select-bg': 'hsl(231.5deg 32% 19.5%', + '--synapse-select-border': 'hsl(233deg 34% 34%)', + '--synapse-button-bg': '#2d42fc', +} + + +``` + +Please see the `examples/landing-page` folder for more examples. + +## Container Customization + +The widget additionally supports a `container` property of `true` or `false` to adjust its width to the container it's in. + +```tsx + +``` + +## Example Apps + +Within the repository's `/examples` folder, there are three example apps. The `landing-page` folder contains a fully functional demo with customizations of the widget. The `with-react` and `with-next` folders contain a simple implementation of the widget using React and Next.js, respectively. + +Dark Theme +Light Theme +Blue Theme diff --git a/packages/widget/examples/landing-page/.gitignore b/packages/widget/examples/landing-page/.gitignore new file mode 100644 index 0000000000..8f322f0d8f --- /dev/null +++ b/packages/widget/examples/landing-page/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/packages/widget/examples/landing-page/README.md b/packages/widget/examples/landing-page/README.md new file mode 100644 index 0000000000..222bc8c55b --- /dev/null +++ b/packages/widget/examples/landing-page/README.md @@ -0,0 +1,77 @@ +# Installing for local development and testing + +## Widget + +1. Clone repo + +``` +git clone git@github.com:synapsecns/sanguine.git +``` + +2. Enter widget directory + +``` +cd sanguine/packages/widget +``` + +3. Use right node version + +``` +nvm use +``` + +4. Install dependencies for widget + +``` +yarn +``` + +5. Ensure local singular build works + +``` +yarn build +``` + +5. Create a link to the widget package to allow user in consumer app + +``` +yarn link +``` + +6. To use live reload for changes made in widget, we can use through Rollup's `watch` flag, we'll need a live builder, which updates the build based on changes made in the `widget` package + +``` +yarn watch +``` + +## Example Landing Page Next.js consumer + +1. Open up another terminal window + +2. Enter Landing Page app directory + +``` +cd examples/landing-page +``` + +3. Install dependencies for app + +``` +yarn +``` + +4. Link widget to allow use in consumer app + +``` +yarn link @synapsecns/widget +``` + +5. Start consumer app + +``` +yarn dev +``` + +### Deploying + +1. When ready to deploy, make sure to add the `@synapsecns/widget` dependency to the `package.json` file of the Landing page app. diff --git a/packages/widget/examples/landing-page/next.config.js b/packages/widget/examples/landing-page/next.config.js new file mode 100644 index 0000000000..eefe6470b1 --- /dev/null +++ b/packages/widget/examples/landing-page/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + productionBrowserSourceMaps: true, +} + +module.exports = nextConfig diff --git a/packages/widget/examples/landing-page/package.json b/packages/widget/examples/landing-page/package.json new file mode 100644 index 0000000000..fd519d88af --- /dev/null +++ b/packages/widget/examples/landing-page/package.json @@ -0,0 +1,24 @@ +{ + "name": "landing-page", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "ethers": "^6.9.0", + "next": "13.5.6", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "null-loader": "^4.0.1", + "typescript": "^5" + } +} diff --git a/packages/widget/examples/landing-page/postcss.config.js b/packages/widget/examples/landing-page/postcss.config.js new file mode 100644 index 0000000000..90d9fffcb1 --- /dev/null +++ b/packages/widget/examples/landing-page/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + autoprefixer: {}, + }, +} diff --git a/packages/widget/examples/landing-page/public/favicon.ico b/packages/widget/examples/landing-page/public/favicon.ico new file mode 100644 index 0000000000..a73399407a Binary files /dev/null and b/packages/widget/examples/landing-page/public/favicon.ico differ diff --git a/packages/widget/examples/landing-page/public/synapse-logo-onDark.svg b/packages/widget/examples/landing-page/public/synapse-logo-onDark.svg new file mode 100644 index 0000000000..6e2e4f4105 --- /dev/null +++ b/packages/widget/examples/landing-page/public/synapse-logo-onDark.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/widget/examples/landing-page/public/synapse-logo-onLight.svg b/packages/widget/examples/landing-page/public/synapse-logo-onLight.svg new file mode 100644 index 0000000000..d3f3bdc12a --- /dev/null +++ b/packages/widget/examples/landing-page/public/synapse-logo-onLight.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/widget/examples/landing-page/src/app/layout.tsx b/packages/widget/examples/landing-page/src/app/layout.tsx new file mode 100644 index 0000000000..8a31b72d3d --- /dev/null +++ b/packages/widget/examples/landing-page/src/app/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from 'next' +import '@/styles/globals.css' + +export const metadata: Metadata = { + title: 'Synapse Widget Landing Page', + description: 'Example Synapse Bridge widget', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/packages/widget/examples/landing-page/src/app/page.tsx b/packages/widget/examples/landing-page/src/app/page.tsx new file mode 100644 index 0000000000..7a6fb7aaf0 --- /dev/null +++ b/packages/widget/examples/landing-page/src/app/page.tsx @@ -0,0 +1,175 @@ +'use client' + +import { + Bridge, + USDC, + USDT, + DAI, + ETH, + METISUSDC, + CustomRpcs, +} from '@synapsecns/widget' +import { useEthereumWallet } from '@/hooks/useEthereumWallet' +import { useState } from 'react' +import { Header } from '@/components/Header' +import { Footer } from '@/components/Footer' +import { Instructions } from '@/components/Instructions' +import { PackageInstall } from '@/components/PackageInstall' + +const initialConfig = { + customTheme: { + bgColor: 'dark', + }, + targetTokens: [], + targetChainIds: [], +} + +const consumerExamples = { + dark: { + customTheme: { + bgColor: 'dark', + }, + }, + light: { + customTheme: { + bgColor: 'light', + }, + }, + gmx: { + customTheme: { + '--synapse-text': 'white', + '--synapse-secondary': '#ffffffb3', + '--synapse-root': '#16182e', + '--synapse-surface': 'linear-gradient(90deg, #1e223de6, #262b47e6)', + '--synapse-border': 'transparent', + '--synapse-select-bg': 'hsl(231.5deg 32% 19.5%', + '--synapse-select-border': 'hsl(233deg 34% 34%)', + '--synapse-button-bg': '#2d42fc', + }, + targetTokens: [ETH, USDC, USDT], + targetChainIds: [42161, 43114], + }, + hercules: { + customTheme: { + bgColor: 'dark', + '--synapse-button-bg': + 'linear-gradient(90deg, hsl(43deg 79% 74%), hsl(21deg 76% 60%))', + '--synapse-button-text': 'black', + '--synapse-focus': 'hsl(32deg 77.5% 67%)', + }, + targetTokens: [METISUSDC], + targetChainIds: [1088], + }, + dfk: { + customTheme: { + bgColor: 'light', + '--synapse-text': 'hsl(12deg 85% 13%)', + '--synapse-secondary': 'hsl(12deg 85% 20%)', + '--synapse-select-bg': 'hsl(35deg 100% 87%)', + '--synapse-surface': 'hsl(32deg 69% 78%)', + '--synapse-root': 'hsl(35deg 100% 87%)', + '--synapse-border': 'hsl(29deg 53% 68%)', + '--synapse-focus': 'hsl(12deg 85% 15%)', + '--synapse-accent': 'hsl(12deg 85% 15%)', + }, + targetTokens: [ETH, USDC], + targetChainIds: [42161, 43114], + }, +} + +const customRpcs: CustomRpcs = { + 1: 'https://eth.llamarpc.com', + 42161: 'https://arbitrum.llamarpc.com', +} + +export default function Home() { + const [config, setConfig] = useState(initialConfig) + const [container, setContainer] = useState(true) + + const { web3Provider } = useEthereumWallet() + + const inputChangeHandler = ( + e: React.ChangeEvent + ): void => { + const selection = e.target.value + + const newConfig = + consumerExamples[selection as keyof typeof consumerExamples] || + initialConfig + setConfig(newConfig) + } + + const createCustomTheme = () => { + const colorPicker = document.getElementById( + 'color-picker' + ) as HTMLInputElement | null + setConfig((prevConfig: any) => ({ + ...prevConfig, + customTheme: { bgColor: colorPicker?.value }, + })) + } + + const toggleContainer = (e: React.ChangeEvent) => + setContainer(e.target.checked) + + const bridgeContainerDisplayProperty = container ? 'grid' : 'block' + + return ( + <> +
+
+
+

Install the Synapse Bridge

+ +

+ Easily onboard new users by adding a custom instance of the Synapse + Bridge to your React project. +

+
+
+ +
+
+
+ +
+ {' '} + +
+ +
+ Drag to resize +
+
+
+ +
+