diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/.env.sample b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/.env.sample
new file mode 100644
index 0000000..1d4e024
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/.env.sample
@@ -0,0 +1 @@
+VITE_QUICKNODE_ENDPOINT="YOUR_QUICKNODE_ENDPOINT"
\ No newline at end of file
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/.eslintrc.cjs b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/.eslintrc.cjs
new file mode 100644
index 0000000..d6c9537
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/.eslintrc.cjs
@@ -0,0 +1,18 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react-hooks/recommended',
+ ],
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
+ parser: '@typescript-eslint/parser',
+ plugins: ['react-refresh'],
+ rules: {
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+}
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/.gitignore b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/.gitignore
new file mode 100644
index 0000000..7098aff
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/.gitignore
@@ -0,0 +1,27 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Local environment variables
+.env
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/README.md b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/README.md
new file mode 100644
index 0000000..a078d02
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/README.md
@@ -0,0 +1,121 @@
+# Crypto Portfolio Tracker
+
+## Overview
+
+The Crypto Portfolio Tracker is a web application that allows users to input their cryptocurrency holdings and track their portfolio value over time. The app fetches current and historical exchange rates to display the total portfolio value and a chart of historical portfolio values in various currencies. It is built using TypeScript, Vite, and Tailwind CSS.
+
+> For a detailed guide on how to build this application and utilize the [Crypto Market Data API](https://marketplace.quicknode.com/add-on/crypto-market-data-api), please visit our [comprehensive guide on QuickNode](https://www.quicknode.com/guides/quicknode-products/marketplace/how-to-build-a-crypto-portfolio-tracker-with-the-crypto-market-data-api).
+
+![Crypto Portfolio Tracker Overview](public/overview.png)
+
+## Features
+
+- **Add and Manage Holdings**: Users can add, edit, and remove cryptocurrency holdings.
+- **Current Portfolio Value**: Fetches and displays the current value of the portfolio in the selected currency.
+- **Currency Selection**: Users can select the currency in which to view the portfolio value (default is USD).
+- **Time Interval Selection**: Users can select the time interval for viewing the historical portfolio value.
+- **Historical Portfolio Value**: Displays a chart of the portfolio's value over time.
+- **Pie Chart**: Visual representation of the portfolio distribution in percentage and USD value.
+- **Export Data**: Users can export the historical portfolio value data as a CSV file.
+
+## Getting Started
+
+### Prerequisites
+
+Before you begin, ensure you have the following:
+- [Node.js](https://nodejs.org/en/) installed on your system.
+- A QuickNode account with the [Crypto Market Data API](https://marketplace.quicknode.com/add-on/crypto-market-data-api) enabled.
+> Crypto Market Data API is a paid add-on. Please check the details [here](https://marketplace.quicknode.com/add-on/crypto-market-data-api) based on your needs.
+- [Typescript](https://www.typescriptlang.org/) and [ts-node](https://typestrong.org/ts-node/)
+
+You can install TypeScript and ts-node globally using the commands below:
+
+```bash
+npm install -g typescript ts-node
+```
+
+### Installation Dependencies
+
+1. Clone the repository to your local machine:
+```bash
+git clone https://github.com/quiknode-labs/qn-guide-examples.git
+```
+
+2. Navigate to the project directory:
+```bash
+cd sample-dapps/crypto-portfolio-tracker
+```
+
+3. Install the necessary dependencies:
+```bash
+npm install
+```
+
+### Setting Environment Variables
+
+Rename `.env.example` to `.env` and replace the `YOUR_QUICKNODE_ENDPOINT` placeholder with your QuickNode endpoint that the **Crypto Market Data API** is enabled.
+
+```sh
+VITE_QUICKNODE_ENDPOINT="YOUR_QUICKNODE_ENDPOINT"
+```
+
+> Please note that while we utilize `dotenv` for environment variable management, sensitive information like endpoints can still be visible on the frontend. This configuration is not recommended for production environments as-is.
+
+
+### Running the Application
+
+Run the development server:
+
+```bash
+npm run dev
+```
+
+Open [http://localhost:5173/](http://localhost:5173/) with your browser to see the application.
+
+## Usage
+
+- **Add Holdings**: Input the cryptocurrency asset and amount, then click **Add**.
+- **Manage Holdings**: Edit or remove holdings as needed.
+- **Select Currency**: Choose the currency in which to view your portfolio value.
+- **Select Time Interval**: Choose the time interval for viewing historical portfolio value (e.g., 5 Minutes, 1 Hour, 1 Day).
+- **View Current Portfolio Value**: The current portfolio value and pie chart are fetched immediately after you add or update assets to your holding.
+- **View Historical Portfolio Value**: Click **Calculate Portfolio Value** to fetch the historical values.
+- **Export Data**: Click **Export as CSV** to download your historical portfolio value data.
+
+## Project Structure
+
+- `src/`: Contains the source code for the application.
+ - `components/`: Contains React components.
+ - `interfaces/`: TypeScript interfaces for type definitions.
+ - `services/`: Functions for fetching data from APIs.
+ - `utils/`: Utility functions.
+- `public/`: Static assets.
+- `.env`: Environment variables.
+- `vite.config.ts`: Vite configuration.
+
+## API Integration
+
+The application integrates with the following APIs:
+- **QuickNode Crypto Market Data API**: For fetching current and historical exchange rates.
+ - `v1/getAssets`: Fetch all available assets.
+ - `v1/getCurrentExchangeRates`: Fetch current exchange rates.
+ - `v1/getHistoricalExchangeRates`: Fetch historical exchange rates.
+
+## Contributing
+
+Contributions are welcome! Please open an issue or submit a pull request for any changes.
+
+## Acknowledgements
+
+- [QuickNode](https://www.quicknode.com/) for providing the Crypto Market Data API.
+- [CoinAPI](https://www.coinapi.io/) for the market data.
+
+## Contact
+
+For any inquiries or issues, please contact [suysal@quicknode.com](mailto:suysal@quicknode.com).
+
+## Conclusion
+
+The **Crypto Portfolio Tracker** effectively utilizes **Crypto Market Data API** to provide a functional tool for managing and analyzing cryptocurrency portfolios. This application demonstrates how to integrate real-time and historical market data to track portfolio performance and gain valuable insights.
+
+The features implemented here are just a starting point. You can customize and extend the application to suit your specific needs, whether for personal use, financial analysis, or development of crypto-related projects.
\ No newline at end of file
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/index.html b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/index.html
new file mode 100644
index 0000000..e2b44fe
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Crypto Portfolio Tracker
+
+
+
+
+
+
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/package.json b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/package.json
new file mode 100644
index 0000000..a4bf4ec
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "crypto-portfolio-tracker-crypto-market-data-api",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5",
+ "@mui/icons-material": "^5.15.18",
+ "@mui/material": "^5.15.18",
+ "axios": "^1.7.2",
+ "chart.js": "^4.4.3",
+ "chartjs-adapter-date-fns": "^3.0.0",
+ "react": "^18.2.0",
+ "react-chartjs-2": "^5.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.66",
+ "@types/react-dom": "^18.2.22",
+ "@typescript-eslint/eslint-plugin": "^7.2.0",
+ "@typescript-eslint/parser": "^7.2.0",
+ "@vitejs/plugin-react": "^4.2.1",
+ "autoprefixer": "^10.4.19",
+ "eslint": "^8.57.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.6",
+ "postcss": "^8.4.38",
+ "tailwindcss": "^3.4.3",
+ "typescript": "^5.2.2",
+ "vite": "^5.2.0"
+ }
+}
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/postcss.config.js b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/postcss.config.js
new file mode 100644
index 0000000..2e7af2b
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/public/overview.png b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/public/overview.png
new file mode 100644
index 0000000..7619dc6
Binary files /dev/null and b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/public/overview.png differ
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/public/vite.svg b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/App.css b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/App.css
new file mode 100644
index 0000000..b9d355d
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/App.css
@@ -0,0 +1,42 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/App.tsx b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/App.tsx
new file mode 100644
index 0000000..7e2f0e3
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/App.tsx
@@ -0,0 +1,147 @@
+import React, { useState, useEffect } from "react";
+
+import Header from "./components/Header";
+import PortfolioInput from "./components/PortfolioInput";
+import PortfolioSummary from "./components/PortfolioSummary";
+import HistoricalChart from "./components/HistoricalChart";
+import PortfolioPieChart from "./components/PortfolioPieChart";
+import PortfolioControls from "./components/PortfolioControls";
+import { fetchAssets } from "./services/cryptoAPI";
+import { Asset, PortfolioHolding, HistoricalDataEntry } from "./interfaces";
+import {
+ addHolding,
+ updateHolding,
+ removeHolding,
+ fetchPortfolioData,
+ fetchTotalPortfolioValue,
+ handleExportCSV,
+} from "./utils/portfolioUtils";
+
+import CircularProgress from "@mui/material/CircularProgress";
+
+const App: React.FC = () => {
+ const [holdings, setHoldings] = useState([]);
+ const [totalValue, setTotalValue] = useState(0);
+ const [historicalData, setHistoricalData] = useState(
+ []
+ );
+ const [currency, setCurrency] = useState("USD");
+ const [timeInterval, setTimeInterval] = useState("1DAY");
+ const [exchangeRates, setExchangeRates] = useState<{ [key: string]: number }>(
+ {}
+ );
+ const [assets, setAssets] = useState<{
+ currencies: Asset[];
+ cryptos: Asset[];
+ }>({ currencies: [], cryptos: [] });
+ const [initialized, setInitialized] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ // Load holdings from local storage
+ const storedHoldings = localStorage.getItem("holdings");
+ if (storedHoldings) {
+ setHoldings(JSON.parse(storedHoldings));
+ }
+
+ // Fetch available assets
+ const fetchAndSetAssets = async () => {
+ const { currencies, cryptos } = await fetchAssets();
+ setAssets({ currencies, cryptos });
+ };
+
+ fetchAndSetAssets();
+
+ setInitialized(true);
+ }, []);
+
+ useEffect(() => {
+ if (initialized) {
+ // Save holdings to local storage
+ localStorage.setItem("holdings", JSON.stringify(holdings));
+ if (holdings.length > 0) {
+ // Automatically fetch total portfolio value
+ const fetchTotalValue = async () => {
+ await fetchTotalPortfolioValue(
+ holdings,
+ currency,
+ setExchangeRates,
+ setTotalValue,
+ setLoading
+ );
+ };
+ fetchTotalValue();
+ } else {
+ setTotalValue(0);
+ setExchangeRates({});
+ setLoading(false);
+ }
+ }
+ }, [holdings, currency, initialized]);
+
+ return (
+
+
+
+
+ setHoldings(addHolding(holdings, asset, amount))
+ }
+ assets={assets.cryptos}
+ holdings={holdings}
+ onUpdateHolding={(index, amount) =>
+ setHoldings(updateHolding(holdings, index, amount))
+ }
+ onRemoveHolding={(index) =>
+ setHoldings(removeHolding(holdings, index))
+ }
+ />
+
+ fetchPortfolioData(
+ holdings,
+ currency,
+ timeInterval,
+ 100,
+ setHistoricalData,
+ setLoading
+ )
+ }
+ exportCSV={() => handleExportCSV(historicalData)}
+ currencies={assets.currencies}
+ disableExport={historicalData.length === 0}
+ />
+
+ {loading && (
+
+ {" "}
+
+ )}
+
+ {!loading && totalValue !== 0 && (
+
+ )}
+
+
+ );
+};
+
+export default App;
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/assets/react.svg b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/Header.tsx b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/Header.tsx
new file mode 100644
index 0000000..f97fbc9
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/Header.tsx
@@ -0,0 +1,11 @@
+import React from "react";
+
+const Header: React.FC = () => {
+ return (
+
+ Crypto Portfolio Tracker
+
+ );
+};
+
+export default Header;
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/HistoricalChart.tsx b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/HistoricalChart.tsx
new file mode 100644
index 0000000..0577e01
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/HistoricalChart.tsx
@@ -0,0 +1,91 @@
+import React from "react";
+import { Line } from "react-chartjs-2";
+import {
+ Chart,
+ ChartOptions,
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ PointElement,
+ LineElement,
+ Title,
+ Tooltip,
+ Legend,
+} from "chart.js";
+import "chartjs-adapter-date-fns";
+
+import { colorPalette } from "../utils/colorPalette";
+
+import { HistoricalDataEntry } from "../interfaces";
+
+Chart.register(
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ PointElement,
+ LineElement,
+ Title,
+ Tooltip,
+ Legend
+);
+
+interface HistoricalChartProps {
+ data: HistoricalDataEntry[];
+ currency: string;
+}
+
+const HistoricalChart: React.FC = ({ data }) => {
+ if (!data || data.length === 0) {
+ return No historical data
;
+ }
+
+ const labels = data.map((entry) => entry.date);
+ // const assets = Object.keys(data[0]).filter((key) => key !== "date");
+
+ // Extract the last entry in the data array
+ const lastEntry = data[data.length - 1];
+ const assets = Object.keys(lastEntry).filter((key) => key !== "date");
+
+ // Sort the assets based on their value in the last entry
+ const sortedAssets = assets.sort(
+ (a, b) => (lastEntry[b] as number) - (lastEntry[a] as number)
+ );
+
+ const datasets = sortedAssets.map((asset, index) => ({
+ label: asset,
+ data: data.map((entry) => entry[asset] as number),
+ fill: true,
+ backgroundColor: `${colorPalette[index % colorPalette.length]}80`,
+ borderColor: colorPalette[index % colorPalette.length],
+ }));
+
+ const chartData = {
+ labels,
+ datasets,
+ };
+
+ const options: ChartOptions<"line"> = {
+ responsive: true,
+ scales: {
+ x: {
+ type: "time",
+ time: {
+ unit: "day",
+ },
+ },
+ y: {
+ stacked: true,
+ },
+ },
+ plugins: {
+ title: {
+ display: true,
+ text: "Historical Portfolio Value",
+ },
+ },
+ };
+
+ return ;
+};
+
+export default HistoricalChart;
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/PortfolioControls.tsx b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/PortfolioControls.tsx
new file mode 100644
index 0000000..d4c33f6
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/PortfolioControls.tsx
@@ -0,0 +1,66 @@
+import React from "react";
+
+import { Asset } from "../interfaces";
+
+interface PortfolioControlsProps {
+ currency: string;
+ setCurrency: (currency: string) => void;
+ timeInterval: string;
+ setTimeInterval: (interval: string) => void;
+ calculatePortfolioValue: () => void;
+ exportCSV: () => void;
+ currencies: Asset[];
+ disableExport: boolean;
+}
+const PortfolioControls: React.FC = ({
+ currency,
+ setCurrency,
+ timeInterval,
+ setTimeInterval,
+ calculatePortfolioValue,
+ exportCSV,
+ currencies,
+ disableExport,
+}) => (
+
+
+ Get Historical Chart
+
+
+ Export as CSV
+
+ setCurrency(e.target.value)}
+ className="p-2 border rounded-md"
+ >
+ {currencies.map((curr) => (
+
+ {curr.name} - {curr.asset_id}
+
+ ))}
+
+ setTimeInterval(e.target.value)}
+ className="p-2 border rounded-md"
+ >
+ 5 Minutes
+ 1 Hour
+ 4 Hour
+ 1 Day
+ 1 Week
+
+
+);
+
+export default PortfolioControls;
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/PortfolioInput.tsx b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/PortfolioInput.tsx
new file mode 100644
index 0000000..e0fc98a
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/PortfolioInput.tsx
@@ -0,0 +1,118 @@
+// src/components/PortfolioInput.tsx
+import React, { useState } from "react";
+import { PortfolioInputProps } from "../interfaces";
+import EditIcon from "@mui/icons-material/Edit";
+import DeleteIcon from "@mui/icons-material/Delete";
+
+const PortfolioInput: React.FC = ({
+ onAddHolding,
+ assets,
+ holdings,
+ onUpdateHolding,
+ onRemoveHolding,
+}) => {
+ const [asset, setAsset] = useState("");
+ const [amount, setAmount] = useState("");
+ const [isEditing, setIsEditing] = useState(null);
+
+ const handleAddHolding = () => {
+ if (asset && amount) {
+ onAddHolding(asset, Number(amount));
+ setAsset("");
+ setAmount("");
+ }
+ };
+
+ const handleUpdateHolding = (index: number) => {
+ if (amount) {
+ onUpdateHolding(index, Number(amount));
+ setAsset("");
+ setAmount("");
+ setIsEditing(null);
+ }
+ };
+
+ const handleRemoveHolding = (index: number) => {
+ console.log("index", index);
+ onRemoveHolding(index);
+ setAsset("");
+ setAmount("");
+ setIsEditing(null);
+ };
+
+ const availableAssets = assets.filter(
+ (asset) => !holdings.some((holding) => holding.asset === asset.asset_id)
+ );
+
+ return (
+
+
+
+ setAsset(e.target.value)}
+ className="p-2 border rounded-md"
+ disabled={isEditing !== null}
+ >
+ Select Asset
+ {availableAssets.map((asset) => (
+
+ {asset.name} - ({asset.asset_id})
+
+ ))}
+
+ setAmount(e.target.value)}
+ className="p-2 border rounded-md"
+ />
+ {isEditing === null ? (
+
+ Add Holding
+
+ ) : (
+ handleUpdateHolding(isEditing)}
+ className="bg-green-600 text-white py-2 rounded-md"
+ >
+ Update Holding
+
+ )}
+
+
+
+
+ );
+};
+
+export default PortfolioInput;
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/PortfolioPieChart.tsx b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/PortfolioPieChart.tsx
new file mode 100644
index 0000000..e338b39
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/PortfolioPieChart.tsx
@@ -0,0 +1,73 @@
+// src/components/PortfolioPieChart.tsx
+import React from "react";
+import { Pie } from "react-chartjs-2";
+import { Chart, registerables } from "chart.js";
+import { PortfolioHolding } from "../interfaces";
+import { colorPalette } from "../utils/colorPalette";
+
+Chart.register(...registerables);
+
+interface PortfolioPieChartProps {
+ holdings: PortfolioHolding[];
+ exchangeRates: { [key: string]: number };
+ currency: string;
+}
+
+const PortfolioPieChart: React.FC = ({
+ holdings,
+ exchangeRates,
+ currency,
+}) => {
+ if (!holdings || holdings.length === 0 || !exchangeRates) {
+ return No data available
;
+ }
+
+ const sortedHoldings = [...holdings].sort((a, b) => {
+ const valueA = a.amount * (exchangeRates[`${a.asset}-${currency}`] || 0);
+ const valueB = b.amount * (exchangeRates[`${b.asset}-${currency}`] || 0);
+ return valueB - valueA;
+ });
+
+ const labels = sortedHoldings.map((holding) => holding.asset);
+ const data = sortedHoldings.map(
+ (holding) =>
+ holding.amount * (exchangeRates[`${holding.asset}-${currency}`] || 0)
+ );
+
+ const totalValue = data.reduce((acc, value) => acc + value, 0);
+
+ const chartData = {
+ labels,
+ datasets: [
+ {
+ data,
+ backgroundColor: colorPalette,
+ },
+ ],
+ };
+
+ const options = {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ title: {
+ display: true,
+ text: "Portfolio Chart",
+ },
+ tooltip: {
+ callbacks: {
+ /* eslint-disable @typescript-eslint/no-explicit-any */
+ label: function (context: any) {
+ const value = context.raw;
+ const percentage = ((value / totalValue) * 100).toFixed(2);
+ return `$${value.toFixed(2)} (${percentage}%)`;
+ },
+ },
+ },
+ },
+ };
+
+ return ;
+};
+
+export default PortfolioPieChart;
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/PortfolioSummary.tsx b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/PortfolioSummary.tsx
new file mode 100644
index 0000000..4b81366
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/components/PortfolioSummary.tsx
@@ -0,0 +1,19 @@
+// src/components/PortfolioSummary.tsx
+import React from "react";
+import { PortfolioSummaryProps } from "../interfaces";
+
+const PortfolioSummary: React.FC = ({
+ totalValue,
+ currency,
+}) => {
+ return (
+
+
Total Portfolio Value
+
+ {totalValue.toFixed(2)} {currency}
+
+
+ );
+};
+
+export default PortfolioSummary;
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/index.css b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/index.css
new file mode 100644
index 0000000..a3319c7
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/index.css
@@ -0,0 +1,4 @@
+/* ./src/index.css */
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
\ No newline at end of file
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/interfaces/index.ts b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/interfaces/index.ts
new file mode 100644
index 0000000..b01df42
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/interfaces/index.ts
@@ -0,0 +1,80 @@
+// src/interfaces/index.ts
+
+export interface Asset {
+ asset_id: string;
+ name: string;
+ type_is_crypto: number;
+ data_quote_start: string;
+ data_quote_end: string;
+ data_orderbook_start: string;
+ data_orderbook_end: string;
+ data_trade_start: string;
+ data_trade_end: string;
+ data_symbols_count: number;
+ volume_1hrs_usd: number;
+ volume_1day_usd: number;
+ volume_1mth_usd: number;
+ price_usd?: number;
+ id_icon: string;
+ chain_addresses?: ChainAddress[];
+ data_start: string;
+ data_end: string;
+}
+
+export interface ChainAddress {
+ chain_id: string;
+ network_id: string;
+ address: string;
+}
+
+export interface ExchangeRate {
+ time: string;
+ asset_id_base: string;
+ asset_id_quote: string;
+ rate: number;
+}
+
+export interface HistoricalRate {
+ time_period_start: string;
+ time_period_end: string;
+ time_open: string;
+ time_close: string;
+ rate_open: number;
+ rate_high: number;
+ rate_low: number;
+ rate_close: number;
+}
+
+export interface HistoricalDataEntry {
+ date: string;
+ [key: string]: number | string;
+}
+
+export interface PortfolioHolding {
+ asset: string;
+ amount: number;
+}
+
+export interface PortfolioInputProps {
+ onAddHolding: (asset: string, amount: number) => void;
+ assets: Asset[];
+ holdings: PortfolioHolding[];
+ onUpdateHolding: (index: number, amount: number) => void;
+ onRemoveHolding: (index: number) => void;
+}
+
+export interface PortfolioSummaryProps {
+ totalValue: number;
+ currency: string;
+}
+
+export interface HistoricalChartProps {
+ data: { date: string; value: number }[];
+ currency: string;
+}
+
+export interface PortfolioPieChartProps {
+ holdings: PortfolioHolding[];
+ exchangeRates: { [key: string]: number };
+ currency: string;
+}
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/main.tsx b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/main.tsx
new file mode 100644
index 0000000..3d7150d
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.tsx'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/services/cryptoAPI.ts b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/services/cryptoAPI.ts
new file mode 100644
index 0000000..f3493f9
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/services/cryptoAPI.ts
@@ -0,0 +1,128 @@
+// src/services/cryptoAPI.ts
+import axios from "axios";
+
+const QUICKNODE_ENDPOINT = import.meta.env.VITE_QUICKNODE_ENDPOINT as string;
+
+import { Asset, ExchangeRate, HistoricalRate } from "../interfaces";
+
+const config = {
+ method: "post",
+ maxBodyLength: Infinity,
+ headers: {
+ "Content-Type": "application/json",
+ },
+};
+
+const fetchAssets = async (): Promise<{
+ currencies: Asset[];
+ cryptos: Asset[];
+}> => {
+ const data = {
+ jsonrpc: "2.0",
+ id: 1,
+ method: "v1/getAssets",
+ params: [],
+ };
+
+ try {
+ const response = await axios.post(QUICKNODE_ENDPOINT, data, config);
+ const assets: Asset[] = response.data.result; // Adjust according to the actual response structure
+
+ const currencies = assets
+ .filter((asset) => asset.type_is_crypto === 0)
+ .sort(
+ (a, b) =>
+ new Date(a.data_trade_start).getTime() -
+ new Date(b.data_trade_start).getTime()
+ );
+
+ const cryptos = assets
+ .filter(
+ (asset) => asset.type_is_crypto === 1 && asset.volume_1mth_usd > 0 && asset.volume_1day_usd > 0
+ )
+ .sort((a, b) => b.volume_1mth_usd - a.volume_1mth_usd);
+
+ return { currencies, cryptos };
+ } catch (err) {
+ console.error("Error fetching assets:", err);
+ return { currencies: [], cryptos: [] };
+ }
+};
+
+const fetchCurrentExchangeRates = async (
+ assetBase: string,
+ assetQuote: string
+): Promise => {
+ const data = JSON.stringify({
+ method: "v1/getCurrentExchangeRates",
+ params: [
+ {
+ asset_id_base: assetBase,
+ },
+ {
+ asset_id_quote: assetQuote,
+ },
+ ],
+ id: 1,
+ jsonrpc: "2.0",
+ });
+
+ try {
+ const response = await axios.post(QUICKNODE_ENDPOINT, data, config);
+ return response.data.result;
+ } catch (err) {
+ console.error("Error fetching current exchange rates:", err);
+ return {
+ time: "",
+ asset_id_base: "",
+ asset_id_quote: "",
+ rate: 0,
+ };
+ }
+};
+
+const fetchHistoricalExchangeRates = async (
+ assetBase: string,
+ assetQuote: string,
+ period: string,
+ timeStart: string,
+ timeEnd: string,
+ limit: number
+): Promise => {
+
+ const data = JSON.stringify({
+ method: "v1/getHistoricalExchangeRates",
+ params: [
+ {
+ assetBase: assetBase,
+ },
+ {
+ assetQuote: assetQuote,
+ },
+ {
+ period_id: period,
+ },
+ {
+ time_start: timeStart,
+ },
+ {
+ time_end: timeEnd,
+ },
+ {
+ limit: limit,
+ },
+ ],
+ id: 1,
+ jsonrpc: "2.0",
+ });
+
+ try {
+ const response = await axios.post(QUICKNODE_ENDPOINT, data, config);
+ return response.data.result; // Adjust according to the actual response structure
+ } catch (err) {
+ console.error("Error fetching historical exchange rates:", err);
+ return [];
+ }
+};
+
+export { fetchAssets, fetchCurrentExchangeRates, fetchHistoricalExchangeRates };
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/utils/colorPalette.ts b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/utils/colorPalette.ts
new file mode 100644
index 0000000..44d194f
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/utils/colorPalette.ts
@@ -0,0 +1,8 @@
+export const colorPalette = [
+ "#FF6384", // Red
+ "#36A2EB", // Blue
+ "#FF9F40", // Orange
+ "#9966FF", // Purple
+ "#4BC0C0", // Teal
+ "#FFCE56", // Yellow
+];
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/utils/csvExporter.ts b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/utils/csvExporter.ts
new file mode 100644
index 0000000..855a99b
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/utils/csvExporter.ts
@@ -0,0 +1,18 @@
+import { HistoricalDataEntry } from "../interfaces";
+
+export const exportToCSV = (data: HistoricalDataEntry[], filename: string) => {
+ const csvContent = [
+ Object.keys(data[0]).join(","), // header row
+ ...data.map((row) => Object.values(row).join(",")),
+ ].join("\n");
+
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
+ const link = document.createElement("a");
+ const url = URL.createObjectURL(blob);
+ link.setAttribute("href", url);
+ link.setAttribute("download", `${filename}.csv`);
+ link.style.visibility = "hidden";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+};
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/utils/portfolioUtils.ts b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/utils/portfolioUtils.ts
new file mode 100644
index 0000000..7701eeb
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/utils/portfolioUtils.ts
@@ -0,0 +1,138 @@
+import { PortfolioHolding, HistoricalDataEntry } from "../interfaces";
+import {
+ fetchCurrentExchangeRates,
+ fetchHistoricalExchangeRates,
+} from "../services/cryptoAPI";
+import { exportToCSV } from "./csvExporter";
+
+export const addHolding = (
+ holdings: PortfolioHolding[],
+ asset: string,
+ amount: number
+) => {
+ return [...holdings, { asset, amount }];
+};
+
+export const updateHolding = (
+ holdings: PortfolioHolding[],
+ index: number,
+ amount: number
+) => {
+ const updatedHoldings = [...holdings];
+ updatedHoldings[index].amount = amount;
+ return updatedHoldings;
+};
+
+export const removeHolding = (holdings: PortfolioHolding[], index: number) => {
+ return holdings.filter((_, i) => i !== index);
+};
+
+export const fetchPortfolioData = async (
+ holdings: PortfolioHolding[],
+ currency: string,
+ timeInterval: string,
+ limit: number,
+ setHistoricalData: (data: HistoricalDataEntry[]) => void,
+ setLoading: (loading: boolean) => void
+) => {
+ try {
+ setLoading(true);
+ const now = new Date();
+ const endDate = now.toISOString();
+ const startDate = new Date(
+ now.getTime() - limit * parseTimeInterval(timeInterval)
+ ).toISOString();
+
+ const historicalRates = await Promise.all(
+ holdings.map((holding) =>
+ fetchHistoricalExchangeRates(
+ holding.asset,
+ currency,
+ timeInterval,
+ startDate,
+ endDate,
+ limit
+ )
+ )
+ );
+
+ // Group historical data by date and include asset values
+ const historicalDataMap: { [key: string]: HistoricalDataEntry } = {};
+
+ historicalRates.forEach((rates, index) => {
+ rates.forEach((rate) => {
+ if (!historicalDataMap[rate.time_period_start]) {
+ historicalDataMap[rate.time_period_start] = {
+ date: rate.time_period_start,
+ };
+ }
+ historicalDataMap[rate.time_period_start][holdings[index].asset] =
+ rate.rate_close * holdings[index].amount;
+ });
+ });
+
+ const historicalData: HistoricalDataEntry[] =
+ Object.values(historicalDataMap);
+
+ setHistoricalData(historicalData);
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setLoading(false);
+ }
+};
+
+export const fetchTotalPortfolioValue = async (
+ holdings: PortfolioHolding[],
+ currency: string,
+ setExchangeRates: (rates: { [key: string]: number }) => void,
+ setTotalValue: (value: number) => void,
+ setLoading: (loading: boolean) => void
+) => {
+ try {
+ setLoading(true);
+
+ const currentRates = await Promise.all(
+ holdings.map((holding) =>
+ fetchCurrentExchangeRates(holding.asset, currency)
+ )
+ );
+
+ const exchangeRatesMap: { [key: string]: number } = {};
+ currentRates.forEach((rate) => {
+ exchangeRatesMap[`${rate.asset_id_base}-${rate.asset_id_quote}`] =
+ rate.rate;
+ });
+ setExchangeRates(exchangeRatesMap);
+
+ const totalValue = holdings.reduce((acc, holding) => {
+ const rate = exchangeRatesMap[`${holding.asset}-${currency}`] || 0;
+ return acc + holding.amount * rate;
+ }, 0);
+
+ setTotalValue(totalValue);
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setLoading(false);
+ }
+};
+
+// Handle CSV export
+export const handleExportCSV = (historicalData: HistoricalDataEntry[]) => {
+ exportToCSV(historicalData, "portfolio_value");
+};
+
+const parseTimeInterval = (interval: string) => {
+ const [value, unit] = interval.match(/\d+|\D+/g)!;
+ switch (unit) {
+ case "DAY":
+ return parseInt(value) * 24 * 60 * 60 * 1000;
+ case "HRS":
+ return parseInt(value) * 60 * 60 * 1000;
+ case "MIN":
+ return parseInt(value) * 60 * 1000;
+ default:
+ return 24 * 60 * 60 * 1000;
+ }
+};
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/vite-env.d.ts b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/tailwind.config.js b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/tailwind.config.js
new file mode 100644
index 0000000..5e3b6b8
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/tailwind.config.js
@@ -0,0 +1,9 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+};
+
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/tsconfig.json b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/tsconfig.json
new file mode 100644
index 0000000..a7fc6fb
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/tsconfig.node.json b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/tsconfig.node.json
new file mode 100644
index 0000000..97ede7e
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/vite.config.ts b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/vite.config.ts
new file mode 100644
index 0000000..5a33944
--- /dev/null
+++ b/sample-dapps/crypto-portfolio-tracker-with-the-crypto-market-data-api/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})