-
Notifications
You must be signed in to change notification settings - Fork 0
Enhance portfolio insights and document exchange setup #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| node_modules/ | ||
| .next/ | ||
| .env* | ||
| .DS_Store |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| # Exchange & Wallet Integration Guide | ||
|
|
||
| This guide walks you through preparing API credentials or wallet connections for the data sources represented in the CryptoTracker Pro dashboard. | ||
|
|
||
| > **Security first.** Never commit API keys or seed phrases to source control. Store them in `.env.local` for local development and in your deployment platform's secret manager in production. | ||
|
|
||
| ## 1. Binance | ||
|
|
||
| 1. Log into your [Binance](https://www.binance.com/) account and open **Profile → API Management**. | ||
| 2. Create a new API key and label it (for example, `CryptoTrackerPro`). | ||
| 3. Complete Binance's security verifications to reveal the key and secret. | ||
| 4. Under **API Restrictions**: | ||
| - Enable **Read Only** access. _Do not enable withdrawal permissions._ | ||
| - If you plan to use IP restrictions, add the server's public IP and click **Confirm**. | ||
| 5. Store the credentials locally in `.env.local`: | ||
| ```bash | ||
| BINANCE_API_KEY=your_key_here | ||
| BINANCE_API_SECRET=your_secret_here | ||
| ``` | ||
| 6. Restart the Next.js dev server so the new environment variables are loaded. | ||
| 7. In your data-fetching service (e.g., `/lib/integrations/binance.js`), use the credentials to call the [`/api/v3/account`](https://binance-docs.github.io/apidocs/spot/en/#account-information-user_data) endpoint for balances and recent 24h change stats. | ||
|
|
||
| ## 2. Coinbase Advanced Trade (Coinbase Pro) | ||
|
|
||
| 1. Visit [Coinbase Advanced Trade](https://advanced.trade.coinbase.com/) → **API** and click **Create API key**. | ||
| 2. Choose the portfolio that contains the assets you want to track. | ||
| 3. Give the key a descriptive name and set **Permissions → View** only. | ||
| 4. Download the `API Key`, `API Secret`, and `Passphrase` that Coinbase provides. | ||
| 5. Save them in `.env.local`: | ||
| ```bash | ||
| COINBASE_API_KEY=your_key_here | ||
| COINBASE_API_SECRET=your_secret_here | ||
| COINBASE_API_PASSPHRASE=your_passphrase_here | ||
| ``` | ||
| 6. Use the official REST client or signed fetch calls against [`/accounts`](https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_getaccounts) to retrieve balances and product tickers for pricing data. | ||
| 7. When deploying, add the same values to your hosting provider's secret store. | ||
|
|
||
| ## 3. MetaMask Wallet | ||
|
|
||
| MetaMask exposes assets directly from the browser wallet, so no API keys are needed. | ||
|
|
||
| 1. Install the [MetaMask extension](https://metamask.io/download/) and import or create your wallet. | ||
| 2. In the CryptoTracker Pro UI, prompt the connection with `window.ethereum.request({ method: "eth_requestAccounts" })`. | ||
| 3. Store the selected account address in application state (for example, in a React context) and pass it to your balances hook. | ||
| 4. Use an on-chain data provider such as [Etherscan](https://docs.etherscan.io/) or [Alchemy](https://docs.alchemy.com/) to fetch token balances for the connected address. Persist the provider keys in `.env.local` (e.g., `ALCHEMY_API_KEY`). | ||
| 5. Refresh holdings on an interval or with a "Refresh" button to keep the dashboard in sync. | ||
|
|
||
| ## 4. Kraken | ||
|
|
||
| 1. Log into [Kraken](https://www.kraken.com/) and go to **Security → API**. | ||
| 2. Click **Add Key**, select a descriptive name, and enable the following permissions: **Query Funds**, **Query Ledgers**, and **Query Closed Orders**. Leave withdrawal permissions disabled. | ||
| 3. After creating the key, copy the **Key** and **Private Key** values. | ||
| 4. Add them to `.env.local`: | ||
| ```bash | ||
| KRAKEN_API_KEY=your_key_here | ||
| KRAKEN_API_SECRET=your_secret_here | ||
| ``` | ||
| 5. Use Kraken's [REST API](https://docs.kraken.com/rest/) endpoints such as `Balance` and `TradeBalance` to obtain holdings and valuation. | ||
| 6. If Kraken is currently disconnected in the UI, surface actionable messaging (e.g., "Reconnect" button) once credentials are added. | ||
|
|
||
| ## Environment Variable Checklist | ||
|
|
||
| Ensure the following keys are defined locally (and in production secrets) before fetching live data: | ||
|
|
||
| ``` | ||
| BINANCE_API_KEY= | ||
| BINANCE_API_SECRET= | ||
| COINBASE_API_KEY= | ||
| COINBASE_API_SECRET= | ||
| COINBASE_API_PASSPHRASE= | ||
| KRAKEN_API_KEY= | ||
| KRAKEN_API_SECRET= | ||
| ALCHEMY_API_KEY= # Optional, for MetaMask balance lookups | ||
| ``` | ||
|
|
||
| ## Next Steps | ||
|
|
||
| - Build dedicated integration modules under `lib/integrations` that encapsulate request signing for each exchange. | ||
| - Create an API route (e.g., `/api/portfolio`) that aggregates balances, normalises everything to USD, and feeds the `mockAssets` shape currently used in `app/page.jsx`. | ||
| - Replace the mocked arrays with responses from the aggregation route, then persist the raw data in a lightweight database (Supabase, PlanetScale, etc.) if you need history. | ||
| - Add UX affordances such as status badges (`Connected`, `Action Required`) that react to integration health checks. | ||
|
|
||
| Following these steps will let you progressively replace the mocked data with real balances from each provider while keeping secrets out of your client bundle. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,3 +4,119 @@ import { twMerge } from "tailwind-merge"; | |
| export function cn(...inputs) { | ||
| return twMerge(clsx(inputs)); | ||
| } | ||
|
|
||
| export function formatCurrency(value, { currency = "USD", maximumFractionDigits = 2 } = {}) { | ||
| return new Intl.NumberFormat("en-US", { | ||
| style: "currency", | ||
| currency, | ||
| maximumFractionDigits, | ||
| minimumFractionDigits: maximumFractionDigits | ||
| }).format(typeof value === "number" ? value : 0); | ||
| } | ||
|
|
||
| export function formatPercentage(value, { maximumFractionDigits = 2 } = {}) { | ||
| return Number.parseFloat(value ?? 0).toLocaleString("en-US", { | ||
| minimumFractionDigits: 0, | ||
| maximumFractionDigits | ||
| }); | ||
| } | ||
|
|
||
| function normalizePercentages(items) { | ||
| if (!items.length) return []; | ||
|
|
||
| const working = items.map((item, index) => ({ | ||
| symbol: item.symbol, | ||
| raw: item.percentage, | ||
| percentage: Math.floor(item.percentage), | ||
| index | ||
| })); | ||
|
|
||
| let remainder = Math.round( | ||
| 100 - working.reduce((sum, item) => sum + item.percentage, 0) | ||
| ); | ||
|
Comment on lines
+34
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of let remainder = 100 - working.reduce((sum, item) => sum + item.percentage, 0); |
||
|
|
||
| if (remainder > 0) { | ||
| const prioritised = [...working].sort( | ||
| (a, b) => (b.raw - b.percentage) - (a.raw - a.percentage) | ||
| ); | ||
| let cursor = 0; | ||
| const length = prioritised.length; | ||
| while (remainder > 0 && length > 0) { | ||
| prioritised[cursor % length].percentage += 1; | ||
| remainder -= 1; | ||
| cursor += 1; | ||
| } | ||
| } | ||
|
|
||
| return working | ||
| .sort((a, b) => a.index - b.index) | ||
| .map((item) => ({ symbol: item.symbol, percentage: item.percentage })); | ||
| } | ||
|
|
||
| export function calculatePortfolioStats(assets = []) { | ||
| if (!Array.isArray(assets) || assets.length === 0) { | ||
| return { | ||
| totalValue: 0, | ||
| weightedChange: 0, | ||
| bestPerformer: null, | ||
| worstPerformer: null, | ||
| allocation: [] | ||
| }; | ||
| } | ||
|
|
||
| const totalValue = assets.reduce((sum, asset) => sum + (asset.value ?? 0), 0); | ||
|
|
||
| const weightedChange = | ||
| totalValue > 0 | ||
| ? assets.reduce( | ||
| (sum, asset) => sum + (asset.value ?? 0) * (asset.change24h ?? 0), | ||
| 0 | ||
| ) / totalValue | ||
| : 0; | ||
|
|
||
| const bestPerformer = assets.reduce((best, asset) => { | ||
| if (!best) return asset; | ||
| return (asset.change24h ?? -Infinity) > (best.change24h ?? -Infinity) | ||
| ? asset | ||
| : best; | ||
| }, null); | ||
|
|
||
| const worstPerformer = assets.reduce((worst, asset) => { | ||
| if (!worst) return asset; | ||
| return (asset.change24h ?? Infinity) < (worst.change24h ?? Infinity) | ||
| ? asset | ||
| : worst; | ||
| }, null); | ||
|
|
||
| const sortedByValue = [...assets].sort( | ||
| (a, b) => (b.value ?? 0) - (a.value ?? 0) | ||
| ); | ||
| const primaryHoldings = sortedByValue.slice(0, 3).map((asset) => ({ | ||
| symbol: asset.symbol, | ||
| value: asset.value ?? 0 | ||
| })); | ||
| const otherValue = sortedByValue | ||
| .slice(3) | ||
| .reduce((sum, asset) => sum + (asset.value ?? 0), 0); | ||
| if (otherValue > 0) { | ||
| primaryHoldings.push({ symbol: "Other", value: otherValue }); | ||
| } | ||
|
|
||
| const allocation = | ||
| totalValue > 0 | ||
| ? normalizePercentages( | ||
| primaryHoldings.map((holding) => ({ | ||
| symbol: holding.symbol, | ||
| percentage: (holding.value / totalValue) * 100 | ||
| })) | ||
| ) | ||
| : []; | ||
|
|
||
| return { | ||
| totalValue, | ||
| weightedChange, | ||
| bestPerformer, | ||
| worstPerformer, | ||
| allocation | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To improve readability and enable syntax highlighting for this code block, it's best practice to specify the language in Markdown. Adding
bashis appropriate for this list of environment variables.