├── .github └── workflows │ └── contracts.yml ├── .gitignore ├── README.md ├── app ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── components.json ├── img │ ├── arch-overview-trade.png │ ├── arch-overview.png │ └── architecture-overview.png ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── arbitrum.svg │ ├── avax.svg │ ├── binance.svg │ ├── caret.svg │ ├── coinbase.svg │ ├── ethereum.svg │ ├── external-link.svg │ ├── github.svg │ ├── honeycomb.svg │ ├── link.svg │ ├── logo.svg │ ├── metamask.svg │ ├── polygon.svg │ ├── sort.svg │ ├── sync-arrows.svg │ ├── usdc.svg │ ├── wallet.svg │ └── walletconnect.svg ├── src │ ├── _types.ts │ ├── app │ │ ├── api │ │ │ ├── feed │ │ │ │ └── [feedId] │ │ │ │ │ └── route.ts │ │ │ └── feeds │ │ │ │ └── [feedIds] │ │ │ │ └── route.ts │ │ ├── datafeed-provider.tsx │ │ ├── error.tsx │ │ ├── favicon.ico │ │ ├── layout.tsx │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── providers.tsx │ │ ├── socket-provider.tsx │ │ └── twitter-image.jpg │ ├── components │ │ ├── connect-wallet.tsx │ │ ├── datafeed-data.tsx │ │ ├── exchange-price.tsx │ │ ├── google-tag.tsx │ │ ├── mobile-connect-wallet.tsx │ │ ├── trade-button.tsx │ │ ├── trade-dialog.tsx │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── form.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── table.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ └── use-toast.ts │ ├── config │ │ ├── contracts.ts │ │ ├── site.tsx │ │ └── trade.ts │ ├── lib │ │ ├── chainlink-sdk.ts │ │ └── utils.ts │ ├── opengraph-image.jpg │ ├── styles │ │ └── globals.css │ └── wagmi.ts ├── tailwind.config.ts └── tsconfig.json ├── contracts ├── .env.example ├── .gitignore ├── contracts │ ├── DataStreamsConsumer.sol │ ├── interfaces │ │ ├── ISwapRouter.sol │ │ └── IVerifierProxy.sol │ └── mocks │ │ └── KeeperRegistryMock.sol ├── hardhat.config.ts ├── package-lock.json ├── package.json ├── scripts │ ├── deploy.ts │ ├── initialize.ts │ ├── trade.ts │ └── upgrade.ts ├── test │ └── DataStreamsConsumer.ts └── tsconfig.json └── img ├── arch-overview-trade.png ├── arch-overview.png └── architecture-overview.png /.github/workflows/contracts.yml: -------------------------------------------------------------------------------- 1 | name: Contracts 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: {} 7 | workflow_dispatch: {} 8 | 9 | jobs: 10 | test: 11 | name: "Contracts: Unit Tests" 12 | runs-on: ubuntu-latest 13 | defaults: 14 | run: 15 | working-directory: ./contracts 16 | env: 17 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 18 | INFURA_KEY: ${{ secrets.INFURA_KEY }} 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: actions/setup-node@v2 22 | with: 23 | node-version: 16 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Run tests 29 | run: npx hardhat test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vercel -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chainlink Data Streams Demo dApp 2 | 3 | > **Note** 4 | > 5 | > _This demo represents an educational example to use a Chainlink system, product, or service and is provided to demonstrate how to interact with Chainlink’s systems, products, and services to integrate them into your own. This template is provided “AS IS” and “AS AVAILABLE” without warranties of any kind, it has not been audited, and it may be missing key checks or error handling to make the usage of the system, product or service more clear. Do not use the code in this example in a production environment without completing your own audits and application of best practices. Neither Chainlink Labs, the Chainlink Foundation, nor Chainlink node operators are responsible for unintended outputs that are generated due to errors in code._ 6 | 7 | This project demonstrates how to use Chainlink Data Streams - part of Chainlink’s family of low-latency, hybrid price feed solutions in a full-stack implementation. 8 | 9 | ## Architecture overview 10 | 11 | ![Architecture Overview](/img/arch-overview.png) 12 | 13 | ## Frontend 14 | 15 | `./app` directory is a Next.js project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app). 16 | 17 | It contains the frontend for the data feeds demo dApp. 18 | 19 | ## Quick Start 20 | 21 | Install all dependencies: 22 | 23 | ```bash 24 | cd app 25 | npm install 26 | ``` 27 | 28 | Set environment variables by copying `.env.example` to `.env` and filling in the values: 29 | 30 | - _NEXT_PUBLIC_ALCHEMY_API_KEY_ for the network you want to use. You can get one from [Alchemy](https://www.alchemy.com/). 31 | - _NEXT_PUBLIC_WALLET_CONNECT_ID_ for the wallet connector. You can get one from [WalletConnect](https://walletconnect.org/). 32 | 33 | For connecting to Chainlink's Low Latency feeds you need to also fill in the following `.env` variables: 34 | 35 | - _CHAINLINK_CLIENT_ID_ - The ID is provided to you by Chainlink. 36 | - _CHAINLINK_CLIENT_SECRET_ - The secret is provided to you by Chainlink. 37 | - _CHAINLINK_API_URL_ api url for consuming the feeds via REST. No `http/https` prefixes should be used. Example: `api.chain.link` 38 | - _CHAINLINK_WEBSOCKET_URL_ optional for consuming feeds via websocket. No `http/https` prefixes should be used. Example: `ws.chain.link` 39 | 40 | You can get those from your Chainlink platform coordinator. 41 | 42 | Run `npm run dev` in your terminal, and then open [localhost:3000](http://localhost:3000) in your browser. 43 | 44 | ## Tech Stack 45 | 46 | - [Next.js](https://nextjs.org/) 47 | - [TypeScript](https://www.typescriptlang.org/) 48 | - [Tailwind CSS](https://tailwindcss.com/) 49 | - [RainbowKit](https://www.rainbowkit.com/) 50 | - [wagmi](https://wagmi.sh/) & [viem](https://viem.sh/) 51 | - [shadcn/ui](https://ui.shadcn.com/) 52 | 53 | ## Backend 54 | 55 | `./contracts` folder is a project that utlizes the low latency on-chain data streams functionality that Chainlink presents. This is used by the `DataStreamsConsumer.sol` contract to trade two ERC20 tokens on UniSwap. The purpose of this is to prevent front-running and automate trade execution. This repository uses the Hardhat environment for development and testing. 56 | 57 | ## Project Details 58 | 59 | - This contract uses Chainlink Automation Log Trigger to require data. This log trigger comes from the `DataStreamsConsumer.sol` contract by the user when he executes the `trade` function which itself emits the `InitiateTrade` event. 60 | 61 | - This event will make the Decentralized Oracle Network call the `checkLog` function which will therefore trigger the `StreamsLookup` error. After that you can model the reports as you wish in the `checkCallback` function. 62 | 63 | - After that Chainlink Data Streams Engine will send the data to your `performUpkeep` function. The data will include a signed report that will be verified in the `performUpkeep` function and the extraData that has been sent which in our contract is the parameters of the `InitiateTrade` event i.e the recipient, the address of the token sent, the address of the token that you will recieve of and the amount that you are going to be sending. The amount that you will receive is calculated by the Data Streams Engine when it sends the report that contains the price of the token received. 64 | 65 | Note: The reports sent to the `DataStreamsConsumer` contract are verified by a Verifier contract that you can set when initializing the contract. 66 | 67 | This contract has been tested mainly on the Arbitrum Goerli testnet network as this is the only network that currently supports the Data Streams feature as of now. 68 | 69 | ## Tech Stack 70 | 71 | - [hardhat](https://hardhat.org/) 72 | 73 | ## Quick start 74 | 75 | 1. Install dependencies 76 | 77 | ```bash 78 | cd contracts 79 | npm install 80 | ``` 81 | 82 | 2. Set environment variables by copying `.env.example` to `.env` and filling in the values: 83 | 84 | - _PRIVATE_KEY_ - for the account you want to use. 85 | - _ETHERSCAN_API_KEY_ - API key for Etherscan API access. 86 | - _INFURA_KEY_ - API key for Infura Ethereum node access. 87 | 88 | ## Deploy 89 | 90 | You can deploy the contract by executing the deploy script: 91 | 92 | ```bash 93 | npx hardhat run scripts/deploy.ts --network goerli 94 | ``` 95 | 96 | For your convenience the deploy script has all the constructor arguments filled in. If you want to change the constructor arguments you can check what data streams Chainlink currenly supports [here](https://docs.chain.link/data-streams/stream-ids?network=arbitrum&page=1#arbitrum-goerli) 97 | 98 | ## Register your upkeep 99 | 100 | After deploying the `DataStreamsConsumer` contract you should register your upkeep by following [Chainlink's guide](https://docs.chain.link/data-streams/getting-started#register-the-upkeep). In our use case "Contract to automate" and "Contract emitting logs" are the same contract i.e `DataStreamsConsumer`. When you choose "Contract emitting logs" you should click the "use custom ABI instead?" option and send the `DataStreamsConsumer.sol` ABI from the `artifacts/contracts/DataStreamsConsumer.sol/DataStreamsConsumer.json` file. After sending the ABI you will have to pick an emitted log. Pick the `InitiateTrade` option from the select with the events options and continue registering your upkeep. 101 | 102 | You should set an initial balance of at least 2 LINK. The other inputs are optional and you can enter whatever value you want. 103 | 104 | ## Emit a log 105 | 106 | After you deploy the contracts and register the upkeep you should emit the `InitialTrade` event from the emitter contract. You can do this by running: 107 | 108 | ```bash 109 | npx hardhat run scripts/trade.ts --network goerli 110 | ``` 111 | 112 | If you haven't changed the feed it will trade WETH to USDC on the Arbitrum Goerli network. For successful trade you need to have WETH in your account. The trade script will approve the consumer contract to handle your tokens. 113 | 114 | You can change the values of the trade function as you wish in case you want to change the value expected to be traded (as long as you have this much WETH). 115 | 116 | ## Upgrading the contract 117 | 118 | We use the proxy pattern provided by the `hardhat` environment for a better experience developing the Data Streams functionality. 119 | 120 | After you have deployed the contract and you want to make some improvements on the `DataStreamsConsumer` contract you can run the `upgrade.ts` script as long as you change the `proxyAddress` variable with your proxy address which you can get from the `openzeppelin` folder. 121 | 122 | ## Testing 123 | 124 | 1. In order to test this project you need to have filled the .env.example file 125 | 126 | 2. `npx hardhat test` 127 | 128 | ## Questions? 129 | 130 | You can [open an issue](https://github.com/smartcontractkit/datastreams-demo/issues) or drop us a line on [Discord](https://discord.com/invite/chainlink). 131 | -------------------------------------------------------------------------------- /app/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ALCHEMY_API_KEY= 2 | NEXT_PUBLIC_WALLET_CONNECT_ID= 3 | NEXT_PUBLIC_GTM_ID= 4 | 5 | CHAINLINK_CLIENT_ID= 6 | CHAINLINK_CLIENT_SECRET= 7 | CHAINLINK_API_URL= 8 | CHAINLINK_WEBSOCKET_URL= 9 | 10 | ENABLE_TRADE= 11 | -------------------------------------------------------------------------------- /app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | .env -------------------------------------------------------------------------------- /app/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | -------------------------------------------------------------------------------- /app/.prettierrc: -------------------------------------------------------------------------------- 1 | { "plugins": ["prettier-plugin-tailwindcss"] } 2 | -------------------------------------------------------------------------------- /app/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 SmartContract 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /app/img/arch-overview-trade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/datastreams-demo/16761e9fd4c944534544e4e53fe6dbcf27b20590/app/img/arch-overview-trade.png -------------------------------------------------------------------------------- /app/img/arch-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/datastreams-demo/16761e9fd4c944534544e4e53fe6dbcf27b20590/app/img/arch-overview.png -------------------------------------------------------------------------------- /app/img/architecture-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/datastreams-demo/16761e9fd4c944534544e4e53fe6dbcf27b20590/app/img/architecture-overview.png -------------------------------------------------------------------------------- /app/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | webpack: (config) => { 5 | config.resolve.fallback = { fs: false, net: false, tls: false }; 6 | return config; 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chainlink-low-latency-data-feed-demo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "postinstall": "npx next telemetry disable" 11 | }, 12 | "dependencies": { 13 | "@hackbg/lolsdk": "git+https://github.com/hackbg/chainlink-low-latency-consumer.git#8e68debef907923c24bbe44f4d6ad89b261386c2", 14 | "@hookform/resolvers": "^3.3.1", 15 | "@radix-ui/react-dialog": "1.0.4", 16 | "@radix-ui/react-label": "^2.0.2", 17 | "@radix-ui/react-slot": "^1.0.2", 18 | "@radix-ui/react-toast": "^1.1.4", 19 | "@rainbow-me/rainbowkit": "^1.0.9", 20 | "@types/node": "20.5.6", 21 | "@types/react": "18.2.21", 22 | "@types/react-dom": "18.2.7", 23 | "@types/react-gtm-module": "^2.0.1", 24 | "autoprefixer": "10.4.15", 25 | "class-variance-authority": "^0.7.0", 26 | "clsx": "^2.0.0", 27 | "date-fns": "^2.30.0", 28 | "eslint": "8.47.0", 29 | "eslint-config-next": "13.4.19", 30 | "ethereum-blockies": "^0.1.1", 31 | "lucide-react": "^0.269.0", 32 | "next": "13.4.19", 33 | "postcss": "8.4.28", 34 | "react": "18.2.0", 35 | "react-dom": "18.2.0", 36 | "react-gtm-module": "^2.0.11", 37 | "react-hook-form": "^7.46.2", 38 | "swr": "^2.2.2", 39 | "tailwind-merge": "^1.14.0", 40 | "tailwindcss": "3.3.3", 41 | "tailwindcss-animate": "^1.0.6", 42 | "typescript": "5.2.2", 43 | "viem": "^1.7.0", 44 | "wagmi": "^1.4.12", 45 | "zod": "^3.22.2" 46 | }, 47 | "devDependencies": { 48 | "@types/ethereum-blockies": "^0.1.0", 49 | "encoding": "^0.1.13", 50 | "lokijs": "^1.5.12", 51 | "pino-pretty": "^10.2.0", 52 | "prettier": "^3.0.2", 53 | "prettier-plugin-tailwindcss": "^0.5.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /app/public/arbitrum.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 12 | 14 | 17 | -------------------------------------------------------------------------------- /app/public/avax.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/public/binance.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/public/caret.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/public/coinbase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/public/ethereum.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/public/external-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/public/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/public/honeycomb.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /app/public/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/public/metamask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/public/polygon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/public/sort.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/public/sync-arrows.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/public/usdc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/public/wallet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/public/walletconnect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/_types.ts: -------------------------------------------------------------------------------- 1 | // TODO: find more suitable wording for this enum 2 | export enum ExchangePlatform { 3 | BINANCE = "BINANCE", 4 | COINBASE = "COINBASE", 5 | } 6 | 7 | export enum Pair { 8 | ETH_USD = "ETH-USD", 9 | AVAX_USD = "AVAX-USD", 10 | } 11 | 12 | export const binancePairs = { 13 | [Pair.ETH_USD]: "ETHUSDT", 14 | [Pair.AVAX_USD]: "AVAXUSDT", 15 | }; 16 | 17 | export const chainlinkPairToFeedId = { 18 | [Pair.ETH_USD]: 19 | "0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782", 20 | [Pair.AVAX_USD]: 21 | "0x0003735a076086936550bd316b18e5e27fc4f280ee5b6530ce68f5aad404c796", 22 | }; 23 | 24 | export type PriceResponse = { 25 | feedId: string; 26 | observationTimestamp: number; 27 | benchmarkPrice: string; 28 | }; 29 | -------------------------------------------------------------------------------- /app/src/app/api/feed/[feedId]/route.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@/lib/chainlink-sdk"; 2 | import { NextResponse } from "next/server"; 3 | import { getUnixTime } from "date-fns"; 4 | import { formatUnits } from "viem"; 5 | 6 | export async function GET( 7 | request: Request, 8 | { params }: { params: { feedId: string } }, 9 | ) { 10 | const { feedId } = params; 11 | const timestamp = getUnixTime(new Date()); 12 | try { 13 | const report = await api.fetchFeed({ 14 | timestamp, 15 | feed: feedId, 16 | }); 17 | return NextResponse.json([ 18 | { 19 | feedId, 20 | timestamp: Number(report.observationsTimestamp), 21 | price: formatUnits(report.benchmarkPrice, 8), 22 | }, 23 | ]); 24 | } catch (error: any) { 25 | return NextResponse.json([]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/app/api/feeds/[feedIds]/route.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@/lib/chainlink-sdk"; 2 | import { NextResponse } from "next/server"; 3 | import { getUnixTime } from "date-fns"; 4 | import { formatUnits } from "viem"; 5 | 6 | export async function GET( 7 | request: Request, 8 | { params }: { params: { feedIds: string } }, 9 | ) { 10 | const { feedIds } = params; 11 | const feeds = feedIds.split(","); 12 | const timestamp = getUnixTime(new Date()); 13 | try { 14 | const report = await api.fetchFeeds({ 15 | timestamp, 16 | feeds, 17 | }); 18 | const data = Object.keys(report).map((feedId) => ({ 19 | feedId, 20 | timestamp: Number(report[feedId].observationsTimestamp), 21 | price: formatUnits(report[feedId].benchmarkPrice, 8), 22 | })); 23 | return NextResponse.json(data); 24 | } catch (error: any) { 25 | return NextResponse.json([]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/app/datafeed-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Pair, chainlinkPairToFeedId } from "@/_types"; 4 | import { format, fromUnixTime } from "date-fns"; 5 | import { createContext, useContext, useEffect, useState } from "react"; 6 | import useSWR from "swr"; 7 | 8 | async function fetcher( 9 | input: RequestInfo, 10 | init?: RequestInit, 11 | ): Promise { 12 | const res = await fetch(input, init); 13 | return res.json(); 14 | } 15 | 16 | type DatafeedContextType = { 17 | prices: { 18 | [Pair.ETH_USD]: string; 19 | [Pair.AVAX_USD]: string; 20 | }; 21 | dates: { 22 | [Pair.ETH_USD]: string; 23 | [Pair.AVAX_USD]: string; 24 | }; 25 | }; 26 | 27 | const DatafeedContext = createContext({ 28 | prices: { 29 | [Pair.ETH_USD]: "", 30 | [Pair.AVAX_USD]: "", 31 | }, 32 | dates: { [Pair.ETH_USD]: "", [Pair.AVAX_USD]: "" }, 33 | }); 34 | 35 | export const useDatafeed = () => { 36 | return useContext(DatafeedContext); 37 | }; 38 | 39 | export const DatafeedProvider = ({ 40 | children, 41 | }: { 42 | children: React.ReactNode; 43 | }) => { 44 | const [ethUsdPrice, setEthUsdPrice] = useState(""); 45 | const [avaxUsdPrice, setAvaxUsdPrice] = useState(""); 46 | const [ethUsdDate, setEthUsdDate] = useState( 47 | format(new Date(), "MMM dd, y, HH:mm O"), 48 | ); 49 | const [avaxUsdDate, setAvaxUsdDate] = useState( 50 | format(new Date(), "MMM dd, y, HH:mm O"), 51 | ); 52 | 53 | const { data: ethData } = useSWR< 54 | { 55 | feedId: string; 56 | timestamp: number; 57 | price: string; 58 | }[] 59 | >( 60 | `/api/feed/${chainlinkPairToFeedId[Pair.ETH_USD]} 61 | `, 62 | fetcher, 63 | { refreshInterval: 1000 }, 64 | ); 65 | 66 | useEffect(() => { 67 | if (ethData) { 68 | const ethUsd = ethData.find( 69 | (entry) => entry.feedId === chainlinkPairToFeedId[Pair.ETH_USD], 70 | ); 71 | if (ethUsd) { 72 | setEthUsdPrice((Number(ethUsd.price) / 10 ** 10).toFixed(2)); 73 | setEthUsdDate( 74 | format(fromUnixTime(ethUsd.timestamp), "MMM dd, y, HH:mm O"), 75 | ); 76 | } 77 | } 78 | }, [ethData]); 79 | 80 | const { data: avaxData } = useSWR< 81 | { 82 | feedId: string; 83 | timestamp: number; 84 | price: string; 85 | }[] 86 | >( 87 | `/api/feed/${chainlinkPairToFeedId[Pair.AVAX_USD]} 88 | `, 89 | fetcher, 90 | { refreshInterval: 1000 }, 91 | ); 92 | 93 | useEffect(() => { 94 | if (avaxData) { 95 | const avaxUsd = avaxData.find( 96 | (entry) => entry.feedId === chainlinkPairToFeedId[Pair.AVAX_USD], 97 | ); 98 | if (avaxUsd) { 99 | setAvaxUsdPrice((Number(avaxUsd.price) / 10 ** 10).toFixed(2)); 100 | setAvaxUsdDate( 101 | format(fromUnixTime(avaxUsd.timestamp), "MMM dd, y, HH:mm O"), 102 | ); 103 | } 104 | } 105 | }, [avaxData]); 106 | 107 | return ( 108 | 117 | {children} 118 | 119 | ); 120 | }; 121 | -------------------------------------------------------------------------------- /app/src/app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; // Error components must be Client Components 2 | 3 | import { useEffect } from "react"; 4 | 5 | export default function Error({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error & { digest?: string }; 10 | reset: () => void; 11 | }) { 12 | useEffect(() => { 13 | // Log the error to an error reporting service 14 | console.error(error); 15 | }, [error]); 16 | 17 | return ( 18 |
19 |

Something went wrong!

20 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/datastreams-demo/16761e9fd4c944534544e4e53fe6dbcf27b20590/app/src/app/favicon.ico -------------------------------------------------------------------------------- /app/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | import "@rainbow-me/rainbowkit/styles.css"; 3 | 4 | import Image from "next/image"; 5 | import { Figtree } from "next/font/google"; 6 | import { siteConfig } from "@/config/site"; 7 | import { Metadata } from "next"; 8 | import { Providers } from "./providers"; 9 | import { cn } from "@/lib/utils"; 10 | import { ConnectWallet } from "@/components/connect-wallet"; 11 | import Link from "next/link"; 12 | import { SocketProvider } from "./socket-provider"; 13 | import { DatafeedProvider } from "./datafeed-provider"; 14 | import { Toaster } from "@/components/ui/toaster"; 15 | import GoogleTag from "@/components/google-tag"; 16 | import MobileConnectWallet from "@/components/mobile-connect-wallet"; 17 | import { isTradeEnabled } from "@/config/trade"; 18 | 19 | const figtree = Figtree({ subsets: ["latin"] }); 20 | 21 | export const metadata: Metadata = { 22 | title: siteConfig.name, 23 | description: siteConfig.description, 24 | }; 25 | 26 | export default function RootLayout({ 27 | children, 28 | }: { 29 | children: React.ReactNode; 30 | }) { 31 | return ( 32 | 33 | 39 | 40 | 41 | 42 |
43 |
44 |
45 | logo 46 |

47 | Chainlink | Data Streams Demo 48 |

49 |

50 | Data Streams Demo 51 |

52 |
53 | {isTradeEnabled && ( 54 | <> 55 |
56 | 57 |
58 |
59 | 60 |
61 | 62 | )} 63 |
64 |
65 |
66 |
67 |
NEW
68 |
69 | Data Streams is now available in mainnet early access for 70 | developers.  71 | 77 | Sign-up for early access today. 78 | 79 |
80 |
81 |
82 |
83 | 84 |
{children}
85 |
86 |
87 |
93 |
101 |

Purpose

102 |

103 | This dApp will show you how to use Chainlink Data 104 | Streams to use low-latency data feeds in your dApp. 105 |

106 |
107 | {isTradeEnabled && ( 108 |
109 |

110 | Getting started 111 |

112 |
113 |

114 | 1. Connect your wallet 115 | 116 | metamask 122 | walletconnect 128 | 129 |

130 |

2. Select a token pair to trade

131 |

3. Swap the amount of tokens desired

132 |
133 |
134 | )} 135 |
136 |

137 | For Developers 138 |

139 |

140 | This dApp is built using Chainlink Data Streams. It 141 | enables developers to use low-latency data feeds in 142 | their smart contracts. Learn how to build a full-stack 143 | dApp with Chainlink Data Streams. 144 |

145 | 151 | github 157 | 158 | Go to Repository 159 | 160 | external-link 166 | 167 |
168 |
169 |
170 |
171 |

Disclaimer

172 |

173 | This demo represents an educational example to use a 174 | Chainlink system, product, or service and is provided to 175 | demonstrate how to interact with Chainlink’s systems, 176 | products, and services to integrate them into your own. 177 | This template is provided “AS IS” and “AS AVAILABLE” 178 | without warranties of any kind, it has not been audited, 179 | and it may bne missing key checks or error handling to 180 | make the usage of the system, product or service more 181 | clear. Do not use the code in this example in a production 182 | environment without completing your own adults and 183 | application of best practices. Neither Chainlink Labs, the 184 | Chainlink Foundation, nor Chainlink node operators are 185 | responsible for unintended outputs that are generated due 186 | to errors in the code. 187 |

188 |
189 |
190 |
191 |
192 |
193 |
194 | 195 | 196 | 197 | ); 198 | } 199 | -------------------------------------------------------------------------------- /app/src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | export default function NotFound() { 2 | return
404
; 3 | } 4 | -------------------------------------------------------------------------------- /app/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { 3 | Table, 4 | TableBody, 5 | TableCell, 6 | TableHead, 7 | TableHeader, 8 | TableRow, 9 | } from "@/components/ui/table"; 10 | import { TradeButton } from "@/components/trade-button"; 11 | import ExchangePrice from "@/components/exchange-price"; 12 | import { ExchangePlatform, Pair } from "@/_types"; 13 | import DatafeedData from "@/components/datafeed-data"; 14 | import { isTradeEnabled } from "@/config/trade"; 15 | import { cn } from "@/lib/utils"; 16 | 17 | export default function Home() { 18 | return ( 19 |
20 |

21 | Chainlink Data Streams Demo dApp 22 |

23 |

24 | Low-latency, high frequency, gas efficient data feeds on Arbitrum Sepolia 25 | Testnet. 26 |

27 |
28 | 29 | 30 | 31 | 32 | Feed 33 | 34 | 35 | 36 | Network 37 | 38 | 39 | Answer 40 | 41 | 42 | Last Update 43 | 44 | 45 | Cex Comparison 46 | 47 | {isTradeEnabled && ( 48 | 49 | )} 50 | 51 | 52 | 53 | 54 | 55 |
56 | 62 | ETH/USD 63 |
64 |
65 | 66 |
67 | 73 | Arbitrum Sepolia 74 |
75 |
76 | 77 |
78 | 79 |
80 |
81 | 82 | 83 | 84 | 88 |
89 |
90 | 96 | 100 |
101 |
102 | 108 | 112 |
113 |
114 |
115 | {isTradeEnabled && ( 116 | 117 | 118 | 119 | )} 120 |
121 | 122 | 123 |
124 | 125 | AVAX/USD 126 |
127 |
128 | 129 |
130 | 136 | Arbitrum Sepolia 137 |
138 |
139 | 140 |
141 | 142 |
143 |
144 | 145 | 146 | 147 | 151 |
152 |
153 | 159 | 163 |
164 |
165 | 171 | 175 |
176 |
177 |
178 | {isTradeEnabled && ( 179 | 180 | 181 | 182 | )} 183 |
184 |
185 |
186 |
187 |
188 | ); 189 | } 190 | -------------------------------------------------------------------------------- /app/src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import Image from "next/image"; 5 | import { WagmiConfig } from "wagmi"; 6 | import { 7 | AvatarComponent, 8 | RainbowKitProvider, 9 | darkTheme, 10 | } from "@rainbow-me/rainbowkit"; 11 | import { chains, config } from "@/wagmi"; 12 | import blockies from "ethereum-blockies"; 13 | 14 | const CustomAvatar: AvatarComponent = ({ address, ensImage, size }) => { 15 | return ensImage ? ( 16 | ens-avatar 23 | ) : ( 24 | blockie 31 | ); 32 | }; 33 | 34 | export function Providers({ children }: { children: React.ReactNode }) { 35 | const [mounted, setMounted] = React.useState(false); 36 | React.useEffect(() => setMounted(true), []); 37 | return ( 38 | 39 | 48 | {mounted && children} 49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/src/app/socket-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ExchangePlatform, Pair, binancePairs } from "@/_types"; 4 | import { createContext, useContext, useEffect, useState } from "react"; 5 | 6 | const COINBASE_WEBSOCKET_URL = "wss://ws-feed.exchange.coinbase.com"; 7 | const BINANCE_WEBSOCKET_URL = "wss://stream.binance.com:9443/ws"; 8 | const BINANCE_WEBSOCKET_URL_US = "wss://stream.binance.us:9443/ws"; 9 | 10 | type SocketContextType = { 11 | sockets: { 12 | [ExchangePlatform.BINANCE]: any | null; 13 | [ExchangePlatform.COINBASE]: any | null; 14 | }; 15 | isConnected: { 16 | [ExchangePlatform.BINANCE]: boolean; 17 | [ExchangePlatform.COINBASE]: boolean; 18 | }; 19 | prices: { 20 | [ExchangePlatform.BINANCE]: { 21 | [Pair.ETH_USD]: string; 22 | [Pair.AVAX_USD]: string; 23 | }; 24 | [ExchangePlatform.COINBASE]: { 25 | [Pair.ETH_USD]: string; 26 | [Pair.AVAX_USD]: string; 27 | }; 28 | }; 29 | }; 30 | 31 | const SocketContext = createContext({ 32 | sockets: { 33 | [ExchangePlatform.BINANCE]: null, 34 | [ExchangePlatform.COINBASE]: null, 35 | }, 36 | isConnected: { 37 | [ExchangePlatform.BINANCE]: false, 38 | [ExchangePlatform.COINBASE]: false, 39 | }, 40 | prices: { 41 | [ExchangePlatform.BINANCE]: { [Pair.AVAX_USD]: "", [Pair.ETH_USD]: "" }, 42 | [ExchangePlatform.COINBASE]: { [Pair.AVAX_USD]: "", [Pair.ETH_USD]: "" }, 43 | }, 44 | }); 45 | 46 | export const useSocket = () => { 47 | return useContext(SocketContext); 48 | }; 49 | 50 | export const SocketProvider = ({ children }: { children: React.ReactNode }) => { 51 | const [coinbaseSocket, setCoinbaseSocket] = useState(null); 52 | const [isConnectedCoinbase, setIsConnectedCoinbase] = useState(false); 53 | const [binanceSocket, setBinanceSocket] = useState(null); 54 | const [isConnectedBinance, setIsConnectedBinace] = useState(false); 55 | const [binanceAvaxUsdPrice, setBinanceAvaxUsdPrice] = useState(""); 56 | const [binanceEthUsdPrice, setBinanceEthUsdPrice] = useState(""); 57 | const [coinbaseAvaxUsdPrice, setCoinbaseAvaxUsdPrice] = useState(""); 58 | const [coinbaseEthUsdPrice, setCoinbaseEthUsdPrice] = useState(""); 59 | 60 | useEffect(() => { 61 | (async () => { 62 | const response = await fetch("https://api.country.is/"); 63 | const result = await response.json(); 64 | const coinbaseSocketInstance = new WebSocket(COINBASE_WEBSOCKET_URL); 65 | const binanceSocketInstance = new WebSocket( 66 | result.country == "US" 67 | ? BINANCE_WEBSOCKET_URL_US 68 | : BINANCE_WEBSOCKET_URL, 69 | ); 70 | 71 | coinbaseSocketInstance.onopen = (e: any) => { 72 | setIsConnectedCoinbase(true); 73 | coinbaseSocketInstance.send( 74 | JSON.stringify({ 75 | type: "subscribe", 76 | product_ids: ["ETH-USD", "AVAX-USD"], 77 | channels: [ 78 | { 79 | name: "ticker", 80 | product_ids: ["ETH-USD", "AVAX-USD"], 81 | }, 82 | ], 83 | }), 84 | ); 85 | }; 86 | binanceSocketInstance.onopen = (e: any) => { 87 | setIsConnectedBinace(true); 88 | binanceSocketInstance.send( 89 | JSON.stringify({ 90 | id: 1, 91 | method: "SUBSCRIBE", 92 | params: ["ethusdt@ticker", "avaxusdt@ticker"], 93 | }), 94 | ); 95 | }; 96 | 97 | coinbaseSocketInstance.onclose = (e: any) => { 98 | setIsConnectedCoinbase(false); 99 | }; 100 | binanceSocketInstance.onclose = (e: any) => { 101 | setIsConnectedBinace(false); 102 | }; 103 | 104 | coinbaseSocketInstance.onmessage = (e: any) => { 105 | const data: { 106 | product_id: Pair; 107 | price: string; 108 | } = JSON.parse(e.data); 109 | if (data.product_id === Pair.AVAX_USD) { 110 | setCoinbaseAvaxUsdPrice(Number(data.price).toFixed(2)); 111 | } 112 | if (data.product_id === Pair.ETH_USD) { 113 | setCoinbaseEthUsdPrice(Number(data.price).toFixed(2)); 114 | } 115 | }; 116 | binanceSocketInstance.onmessage = (e: any) => { 117 | const data: { s: string; c: string } = JSON.parse(e.data); 118 | if (data.s === binancePairs[Pair.ETH_USD]) { 119 | setBinanceEthUsdPrice(Number(data.c).toFixed(2)); 120 | } 121 | if (data.s === binancePairs[Pair.AVAX_USD]) { 122 | setBinanceAvaxUsdPrice(Number(data.c).toFixed(2)); 123 | } 124 | }; 125 | 126 | setCoinbaseSocket(coinbaseSocketInstance); 127 | setBinanceSocket(binanceSocketInstance); 128 | })(); 129 | }, []); 130 | 131 | return ( 132 | 154 | {children} 155 | 156 | ); 157 | }; 158 | -------------------------------------------------------------------------------- /app/src/app/twitter-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/datastreams-demo/16761e9fd4c944534544e4e53fe6dbcf27b20590/app/src/app/twitter-image.jpg -------------------------------------------------------------------------------- /app/src/components/connect-wallet.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import blockies from "ethereum-blockies"; 5 | import { ConnectButton } from "@rainbow-me/rainbowkit"; 6 | import { Button } from "@/components/ui/button"; 7 | 8 | export const ConnectWallet = () => { 9 | return ( 10 | 11 | {({ 12 | account, 13 | chain, 14 | openAccountModal, 15 | openChainModal, 16 | openConnectModal, 17 | authenticationStatus, 18 | mounted, 19 | }) => { 20 | // Note: If your app doesn't use authentication, you 21 | // can remove all 'authenticationStatus' checks 22 | const ready = mounted && authenticationStatus !== "loading"; 23 | const connected = 24 | ready && 25 | account && 26 | chain && 27 | (!authenticationStatus || authenticationStatus === "authenticated"); 28 | return ( 29 |
39 | {(() => { 40 | if (!connected) { 41 | return ( 42 | 48 | ); 49 | } 50 | if (chain.unsupported) { 51 | return ( 52 | 58 | ); 59 | } 60 | return ( 61 |
62 | 82 | 115 |
116 | ); 117 | })()} 118 |
119 | ); 120 | }} 121 |
122 | ); 123 | }; 124 | -------------------------------------------------------------------------------- /app/src/components/datafeed-data.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Pair } from "@/_types"; 4 | import { useDatafeed } from "@/app/datafeed-provider"; 5 | 6 | const DatafeedData = ({ 7 | data, 8 | pair, 9 | }: { 10 | data: "price" | "date"; 11 | pair: Pair; 12 | }) => { 13 | const { prices, dates } = useDatafeed(); 14 | 15 | if (data === "price") { 16 | return {prices[pair] ? '$' : ''}{prices[pair]}; 17 | } 18 | if (data === "date") { 19 | return {dates[pair]}; 20 | } 21 | }; 22 | 23 | export default DatafeedData; 24 | -------------------------------------------------------------------------------- /app/src/components/exchange-price.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ExchangePlatform, Pair } from "@/_types"; 4 | import { useSocket } from "@/app/socket-provider"; 5 | 6 | const ExchangePrice = ({ 7 | source, 8 | pair, 9 | }: { 10 | source: ExchangePlatform; 11 | pair: Pair; 12 | }) => { 13 | const { prices } = useSocket(); 14 | return {prices[source][pair] ? '$' : ''}{prices[source][pair]}; 15 | }; 16 | 17 | export default ExchangePrice; 18 | -------------------------------------------------------------------------------- /app/src/components/google-tag.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import TagManager from "react-gtm-module"; 5 | 6 | const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID; 7 | 8 | export default function GoogleTag() { 9 | useEffect(() => { 10 | if (GTM_ID) { 11 | TagManager.initialize({ gtmId: GTM_ID }); 12 | } 13 | }, []); 14 | 15 | return <>; 16 | } 17 | -------------------------------------------------------------------------------- /app/src/components/mobile-connect-wallet.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import { ConnectButton } from "@rainbow-me/rainbowkit"; 5 | 6 | const MobileConnectWallet = () => { 7 | return ( 8 | 9 | {({ 10 | account, 11 | chain, 12 | openAccountModal, 13 | openChainModal, 14 | openConnectModal, 15 | authenticationStatus, 16 | mounted, 17 | }) => { 18 | // Note: If your app doesn't use authentication, you 19 | // can remove all 'authenticationStatus' checks 20 | const ready = mounted && authenticationStatus !== "loading"; 21 | const connected = 22 | ready && 23 | account && 24 | chain && 25 | (!authenticationStatus || authenticationStatus === "authenticated"); 26 | return ( 27 |
37 | {(() => { 38 | return ( 39 | Connect Wallet 51 | ); 52 | })()} 53 |
54 | ); 55 | }} 56 |
57 | ); 58 | }; 59 | 60 | export default MobileConnectWallet; 61 | -------------------------------------------------------------------------------- /app/src/components/trade-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useAccount } from "wagmi"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { toast } from "@/components/ui/use-toast"; 7 | import { 8 | Dialog, 9 | DialogContent, 10 | DialogFooter, 11 | DialogTrigger, 12 | } from "@/components/ui/dialog"; 13 | import TradeDialog from "@/components/trade-dialog"; 14 | 15 | import { Pair } from "@/_types"; 16 | import { useDatafeed } from "@/app/datafeed-provider"; 17 | 18 | export const TradeButton = ({ pair }: { pair: Pair }) => { 19 | const { isConnected } = useAccount(); 20 | const { prices } = useDatafeed(); 21 | 22 | return ( 23 | 24 | {isConnected ? ( 25 | 26 | 32 | 33 | ) : ( 34 | 45 | )} 46 | 47 | 48 | 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /app/src/components/trade-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as z from "zod"; 4 | import { useForm } from "react-hook-form"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import Image from "next/image"; 7 | import { Address, parseEther, parseUnits } from "viem"; 8 | import { 9 | useAccount, 10 | useBalance, 11 | useContractWrite, 12 | useSendTransaction, 13 | } from "wagmi"; 14 | 15 | import { toast } from "@/components/ui/use-toast"; 16 | import { Button } from "@/components/ui/button"; 17 | import { DialogFooter, DialogTrigger } from "@/components/ui/dialog"; 18 | import { Label } from "@/components/ui/label"; 19 | import { Input } from "@/components/ui/input"; 20 | import { Form, FormField, FormItem, FormLabel } from "@/components//ui/form"; 21 | import { symbols } from "@/config/trade"; 22 | import { useDatafeed } from "@/app/datafeed-provider"; 23 | import { Pair, chainlinkPairToFeedId } from "@/_types"; 24 | import { useState } from "react"; 25 | import { 26 | wethConfig, 27 | proxyConfig, 28 | usdcConfig, 29 | avaxConfig, 30 | } from "@/config/contracts"; 31 | import { Check } from "lucide-react"; 32 | 33 | const formSchema = z.object({ 34 | from: z.coerce.number().gt(0), 35 | to: z.coerce.number().gt(0), 36 | }); 37 | 38 | const TradeDialog = ({ pair }: { pair: Pair }) => { 39 | const [isLoading, setIsLoading] = useState(false); 40 | const [txHash, setTxHash] = useState
(); 41 | const { address } = useAccount(); 42 | const { prices } = useDatafeed(); 43 | const [tokenA, setTokenA] = useState
( 44 | pair === Pair.AVAX_USD ? avaxConfig.address : wethConfig.address, 45 | ); 46 | const [tokenB, setTokenB] = useState
(usdcConfig.address); 47 | const { data: tokenABalance } = useBalance({ address, token: tokenA }); 48 | const { data: tokenBBalance } = useBalance({ address, token: tokenB }); 49 | 50 | const [feedId, setFeedId] = useState( 51 | pair === Pair.AVAX_USD 52 | ? chainlinkPairToFeedId[Pair.AVAX_USD] 53 | : chainlinkPairToFeedId[Pair.ETH_USD], 54 | ); 55 | 56 | const form = useForm>({ 57 | resolver: zodResolver(formSchema), 58 | defaultValues: { 59 | from: 0, 60 | to: 0, 61 | }, 62 | }); 63 | 64 | const fromAmount = form.watch("from"); 65 | const { sendTransactionAsync: wrapEth } = useSendTransaction({ 66 | onSuccess() { 67 | toast({ 68 | title: `Wrapped ${fromAmount} ETH`, 69 | }); 70 | }, 71 | }); 72 | 73 | const { writeAsync: approveWeth } = useContractWrite({ 74 | ...wethConfig, 75 | functionName: "approve", 76 | onError(error) { 77 | toast({ 78 | variant: "destructive", 79 | title: error.name, 80 | description: error.message, 81 | }); 82 | }, 83 | onSuccess() { 84 | toast({ 85 | title: "Approve transaction has been sent", 86 | }); 87 | }, 88 | }); 89 | 90 | const { writeAsync: approveAvax } = useContractWrite({ 91 | ...avaxConfig, 92 | functionName: "approve", 93 | onError(error) { 94 | toast({ 95 | variant: "destructive", 96 | title: error.name, 97 | description: error.message, 98 | }); 99 | }, 100 | onSuccess() { 101 | toast({ 102 | title: "Approve transaction has been sent", 103 | }); 104 | }, 105 | }); 106 | 107 | const { writeAsync: approveUsdc } = useContractWrite({ 108 | ...usdcConfig, 109 | functionName: "approve", 110 | onError(error) { 111 | toast({ 112 | variant: "destructive", 113 | title: error.name, 114 | description: error.message, 115 | }); 116 | }, 117 | onSuccess() { 118 | toast({ 119 | title: "Approve transaction has been sent", 120 | }); 121 | }, 122 | }); 123 | 124 | const { writeAsync: trade } = useContractWrite({ 125 | ...proxyConfig, 126 | functionName: "trade", 127 | onError(error) { 128 | toast({ 129 | variant: "destructive", 130 | title: error.name, 131 | description: error.message, 132 | }); 133 | }, 134 | onSuccess() { 135 | toast({ 136 | title: "Swap in progress", 137 | }); 138 | }, 139 | }); 140 | 141 | async function onSubmit(values: z.infer) { 142 | setIsLoading(true); 143 | setFeedId( 144 | tokenA === wethConfig.address 145 | ? chainlinkPairToFeedId[Pair.ETH_USD] 146 | : chainlinkPairToFeedId[Pair.AVAX_USD], 147 | ); 148 | const amountA = parseUnits(`${values.from}`, tokenABalance?.decimals ?? 0); 149 | const amountB = parseUnits(`${values.to}`, tokenBBalance?.decimals ?? 0); 150 | 151 | if (amountA > (tokenABalance?.value ?? BigInt(0))) { 152 | toast({ 153 | title: "Error:", 154 | description: "Insufficient Balance", 155 | variant: "destructive", 156 | }); 157 | setIsLoading(false); 158 | return; 159 | } 160 | 161 | if (tokenA == wethConfig.address) { 162 | await wrapEth({ 163 | to: wethConfig.address, 164 | value: fromAmount ? parseEther(`${fromAmount}`) : undefined, 165 | }); 166 | await approveWeth({ 167 | args: [proxyConfig.address, parseEther(`${fromAmount}`)], 168 | }); 169 | } 170 | 171 | if (tokenA == avaxConfig.address) { 172 | await approveAvax({ 173 | args: [proxyConfig.address, parseEther(`${fromAmount}`)], 174 | }); 175 | } 176 | 177 | if (tokenA == usdcConfig.address) { 178 | await approveUsdc({ 179 | args: [proxyConfig.address, parseEther(`${fromAmount}`)], 180 | }); 181 | } 182 | 183 | const result = await trade({ 184 | args: [tokenA!, tokenB!, parseEther(`${fromAmount}`), feedId], 185 | }); 186 | toast({ 187 | title: "Swap completed:", 188 | description: `${values.from} ${tokenABalance?.symbol} for ${values.to} ${tokenBBalance?.symbol}`, 189 | variant: "success", 190 | }); 191 | setIsLoading(false); 192 | setTxHash(result.hash); 193 | } 194 | 195 | return txHash ? ( 196 |
197 | 198 |

Swap completed!

199 | 205 | 206 | View on Explorer 207 | 208 | external-link 214 | 215 |
216 | ) : ( 217 | <> 218 |
219 | 220 |
221 | ( 225 | 226 | 227 | From 228 | 229 | { 234 | if (Number(e.target.value) < 0) { 235 | return; 236 | } 237 | form.setValue( 238 | "to", 239 | tokenA === usdcConfig.address 240 | ? Math.round( 241 | (Number(e.target.value) + Number.EPSILON) * 100, 242 | ) / 243 | 100 / 244 | Number(prices[pair]) 245 | : Number(e.target.value) * Number(prices[pair]), 246 | ); 247 | field.onChange(e); 248 | }} 249 | /> 250 | 251 | )} 252 | /> 253 |
254 | 260 |
261 | {tokenABalance?.symbol && ( 262 | {tokenABalance.symbol} 268 | )} 269 | 270 | {tokenABalance?.symbol} 271 | 272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 | ( 283 | 284 | 285 | To 286 | 287 | { 292 | if (Number(e.target.value) < 0) { 293 | return; 294 | } 295 | form.setValue( 296 | "from", 297 | tokenA === usdcConfig.address 298 | ? Number(e.target.value) * Number(prices[pair]) 299 | : Math.round( 300 | (Number(e.target.value) + Number.EPSILON) * 100, 301 | ) / 302 | 100 / 303 | Number(prices[pair]), 304 | ); 305 | field.onChange(e); 306 | }} 307 | /> 308 | 309 | )} 310 | /> 311 |
312 | 318 |
319 | {tokenBBalance?.symbol && ( 320 | {tokenBBalance.symbol} 326 | )} 327 | 328 | {tokenBBalance?.symbol} 329 | 330 |
331 |
332 |
333 |
334 | Note: swap values are approximate 335 |
336 | 343 | 344 | 345 | 346 | 347 | 350 | 351 | 352 | 353 | ); 354 | }; 355 | 356 | export default TradeDialog; 357 | -------------------------------------------------------------------------------- /app/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /app/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = ({ 14 | className, 15 | ...props 16 | }: DialogPrimitive.DialogPortalProps) => ( 17 | 18 | ) 19 | DialogPortal.displayName = DialogPrimitive.Portal.displayName 20 | 21 | const DialogOverlay = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 33 | )) 34 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 35 | 36 | const DialogContent = React.forwardRef< 37 | React.ElementRef, 38 | React.ComponentPropsWithoutRef 39 | >(({ className, children, ...props }, ref) => ( 40 | 41 | 42 | 50 | {children} 51 | 52 | 53 | Close 54 | 55 | 56 | 57 | )) 58 | DialogContent.displayName = DialogPrimitive.Content.displayName 59 | 60 | const DialogHeader = ({ 61 | className, 62 | ...props 63 | }: React.HTMLAttributes) => ( 64 |
71 | ) 72 | DialogHeader.displayName = "DialogHeader" 73 | 74 | const DialogFooter = ({ 75 | className, 76 | ...props 77 | }: React.HTMLAttributes) => ( 78 |
85 | ) 86 | DialogFooter.displayName = "DialogFooter" 87 | 88 | const DialogTitle = React.forwardRef< 89 | React.ElementRef, 90 | React.ComponentPropsWithoutRef 91 | >(({ className, ...props }, ref) => ( 92 | 100 | )) 101 | DialogTitle.displayName = DialogPrimitive.Title.displayName 102 | 103 | const DialogDescription = React.forwardRef< 104 | React.ElementRef, 105 | React.ComponentPropsWithoutRef 106 | >(({ className, ...props }, ref) => ( 107 | 112 | )) 113 | DialogDescription.displayName = DialogPrimitive.Description.displayName 114 | 115 | export { 116 | Dialog, 117 | DialogTrigger, 118 | DialogContent, 119 | DialogHeader, 120 | DialogFooter, 121 | DialogTitle, 122 | DialogDescription, 123 | } 124 | -------------------------------------------------------------------------------- /app/src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { Slot } from "@radix-ui/react-slot" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { cn } from "@/lib/utils" 14 | import { Label } from "@/components/ui/label" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |