diff --git a/.github/workflows/pool-tests.yml b/.github/workflows/pool-tests.yml new file mode 100644 index 0000000..834af0a --- /dev/null +++ b/.github/workflows/pool-tests.yml @@ -0,0 +1,57 @@ +name: Pool Contract Tests + +on: + push: + branches: ["**"] + pull_request: + branches: [main] + +jobs: + clarinet-test: + name: Clarinet Unit Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Clarinet + run: | + curl -L https://github.com/hirosystems/clarinet/releases/latest/download/clarinet-linux-x64-glibc.tar.gz \ + | tar -xz -C /usr/local/bin + + - name: Run Clarinet checks + run: clarinet check + + - name: Run contract tests + run: clarinet test --coverage + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + file: coverage.lcov + fail_ci_if_error: false + + typescript-test: + name: TypeScript Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Run TypeScript tests + working-directory: frontend + run: npm test -- --reporter=verbose + + - name: Type check + working-directory: frontend + run: npx tsc --noEmit diff --git a/Clarinet.toml b/Clarinet.toml new file mode 100644 index 0000000..e1bbe9a --- /dev/null +++ b/Clarinet.toml @@ -0,0 +1,37 @@ +[project] +name = "bitstake" +requirements = [] +authors = [] +telemetry = false + +[contracts.bitstake-pool-registry] +path = "contracts/bitstake-pool-registry.clar" +clarity_version = 2 +epoch = "2.5" + +[contracts.bitstake-pool-deposits] +path = "contracts/bitstake-pool-deposits.clar" +clarity_version = 2 +epoch = "2.5" + +[contracts.bitstake-lbstx] +path = "contracts/bitstake-lbstx.clar" +clarity_version = 2 +epoch = "2.5" + +[contracts.bitstake-bbstx] +path = "contracts/bitstake-bbstx.clar" +clarity_version = 2 +epoch = "2.5" + +[contracts.bitstake-mbstx] +path = "contracts/bitstake-mbstx.clar" +clarity_version = 2 +epoch = "2.5" + +[contracts.bitstake-rewards] +path = "contracts/bitstake-rewards.clar" +clarity_version = 2 +epoch = "2.5" + +[[deployment_plan.genesis]] diff --git a/contracts/bitstake-bbstx.clar b/contracts/bitstake-bbstx.clar new file mode 100644 index 0000000..881e313 --- /dev/null +++ b/contracts/bitstake-bbstx.clar @@ -0,0 +1,42 @@ +;; bitstake-bbstx.clar +;; bbSTX — Balanced Pool liquid staking token (Pool 2, 3-cycle lockup) +;; SIP-010 compliant + +(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token bbstx) + +(define-constant CONTRACT-OWNER tx-sender) +(define-constant ERR-NOT-OWNER (err u100)) +(define-constant ERR-NOT-SENDER (err u101)) +(define-constant ERR-ZERO (err u103)) + +(define-data-var token-name (string-ascii 32) "Balanced bitstake STX") +(define-data-var token-symbol (string-ascii 10) "bbSTX") +(define-data-var token-decimals uint u6) +(define-data-var token-uri (optional (string-utf8 256)) none) + +(define-read-only (get-name) (ok (var-get token-name))) +(define-read-only (get-symbol) (ok (var-get token-symbol))) +(define-read-only (get-decimals) (ok (var-get token-decimals))) +(define-read-only (get-balance (account principal)) (ok (ft-get-balance bbstx account))) +(define-read-only (get-total-supply) (ok (ft-get-supply bbstx))) +(define-read-only (get-token-uri) (ok (var-get token-uri))) + +(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq tx-sender sender) ERR-NOT-SENDER) + (asserts! (> amount u0) ERR-ZERO) + (try! (ft-transfer? bbstx amount sender recipient)) + (match memo m (print m) 0x) + (ok true))) + +(define-public (mint (amount uint) (recipient principal)) + (begin + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER) + (ft-mint? bbstx amount recipient))) + +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) ERR-NOT-SENDER) + (ft-burn? bbstx amount owner))) diff --git a/contracts/bitstake-lbstx.clar b/contracts/bitstake-lbstx.clar new file mode 100644 index 0000000..795d85d --- /dev/null +++ b/contracts/bitstake-lbstx.clar @@ -0,0 +1,42 @@ +;; bitstake-lbstx.clar +;; lbSTX — Liquid Pool liquid staking token (Pool 1, 1-cycle lockup) +;; SIP-010 compliant + +(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token lbstx) + +(define-constant CONTRACT-OWNER tx-sender) +(define-constant ERR-NOT-OWNER (err u100)) +(define-constant ERR-NOT-SENDER (err u101)) +(define-constant ERR-ZERO (err u103)) + +(define-data-var token-name (string-ascii 32) "Liquid bitstake STX") +(define-data-var token-symbol (string-ascii 10) "lbSTX") +(define-data-var token-decimals uint u6) +(define-data-var token-uri (optional (string-utf8 256)) none) + +(define-read-only (get-name) (ok (var-get token-name))) +(define-read-only (get-symbol) (ok (var-get token-symbol))) +(define-read-only (get-decimals) (ok (var-get token-decimals))) +(define-read-only (get-balance (account principal)) (ok (ft-get-balance lbstx account))) +(define-read-only (get-total-supply) (ok (ft-get-supply lbstx))) +(define-read-only (get-token-uri) (ok (var-get token-uri))) + +(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq tx-sender sender) ERR-NOT-SENDER) + (asserts! (> amount u0) ERR-ZERO) + (try! (ft-transfer? lbstx amount sender recipient)) + (match memo m (print m) 0x) + (ok true))) + +(define-public (mint (amount uint) (recipient principal)) + (begin + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER) + (ft-mint? lbstx amount recipient))) + +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) ERR-NOT-SENDER) + (ft-burn? lbstx amount owner))) diff --git a/contracts/bitstake-mbstx.clar b/contracts/bitstake-mbstx.clar new file mode 100644 index 0000000..d247b07 --- /dev/null +++ b/contracts/bitstake-mbstx.clar @@ -0,0 +1,48 @@ +;; bitstake-mbstx.clar +;; mbSTX — Maxi Pool liquid staking token (Pool 3, 12-cycle lockup) +;; SIP-010 compliant + +(impl-trait 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.sip-010-trait-ft-standard.sip-010-trait) + +(define-fungible-token mbstx) + +(define-constant CONTRACT-OWNER tx-sender) +(define-constant ERR-NOT-OWNER (err u100)) +(define-constant ERR-NOT-SENDER (err u101)) +(define-constant ERR-ZERO (err u103)) + +(define-data-var token-name (string-ascii 32) "Maxi bitstake STX") +(define-data-var token-symbol (string-ascii 10) "mbSTX") +(define-data-var token-decimals uint u6) +(define-data-var token-uri (optional (string-utf8 256)) none) + +(define-read-only (get-name) (ok (var-get token-name))) +(define-read-only (get-symbol) (ok (var-get token-symbol))) +(define-read-only (get-decimals) (ok (var-get token-decimals))) +(define-read-only (get-balance (account principal)) (ok (ft-get-balance mbstx account))) +(define-read-only (get-total-supply) (ok (ft-get-supply mbstx))) +(define-read-only (get-token-uri) (ok (var-get token-uri))) + +(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34)))) + (begin + (asserts! (is-eq tx-sender sender) ERR-NOT-SENDER) + (asserts! (> amount u0) ERR-ZERO) + (try! (ft-transfer? mbstx amount sender recipient)) + (match memo m (print m) 0x) + (ok true))) + +(define-public (mint (amount uint) (recipient principal)) + (begin + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER) + (ft-mint? mbstx amount recipient))) + +(define-public (burn (amount uint) (owner principal)) + (begin + (asserts! (is-eq tx-sender owner) ERR-NOT-SENDER) + (ft-burn? mbstx amount owner))) + +(define-public (set-token-uri (new-uri (optional (string-utf8 256)))) + (begin + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER) + (var-set token-uri new-uri) + (ok true))) diff --git a/contracts/bitstake-pool-deposits.clar b/contracts/bitstake-pool-deposits.clar new file mode 100644 index 0000000..f28f210 --- /dev/null +++ b/contracts/bitstake-pool-deposits.clar @@ -0,0 +1,106 @@ +;; bitstake-pool-deposits.clar +;; Handles deposits and withdrawals for each pool tier + +(define-constant CONTRACT-OWNER tx-sender) +(define-constant ERR-NOT-OWNER (err u100)) +(define-constant ERR-POOL-INACTIVE (err u102)) +(define-constant ERR-BELOW-MIN (err u103)) +(define-constant ERR-NO-POSITION (err u105)) +(define-constant ERR-LOCKED (err u106)) + +;; (pool-id, depositor) -> position +(define-map positions + { pool-id: uint, depositor: principal } + { + amount: uint, + deposited-at: uint, + unlock-block: uint + } +) + +;; Track total deposited per user across all pools +(define-map user-total-stacked principal uint) + +;; ── Deposit ─────────────────────────────────────────────────────────── + +(define-public (deposit-to-pool (pool-id uint) (amount uint)) + (let ( + (pool (unwrap! (contract-call? .bitstake-pool-registry get-pool pool-id) (err u101))) + (p-data (unwrap! pool (err u101))) + (min-dep (get min-deposit p-data)) + (cycles (get lockup-cycles p-data)) + ) + (asserts! (get active p-data) ERR-POOL-INACTIVE) + (asserts! (>= amount min-dep) ERR-BELOW-MIN) + + ;; Transfer STX to contract + (try! (stx-transfer? amount tx-sender (as-contract tx-sender))) + + ;; Record position (add to existing if any) + (let ( + (key { pool-id: pool-id, depositor: tx-sender }) + (existing (map-get? positions key)) + (unlock (+ block-height (* cycles u2100))) ;; ~2100 blocks per cycle + ) + (match existing + pos (map-set positions key { + amount: (+ (get amount pos) amount), + deposited-at: block-height, + unlock-block: unlock + }) + (map-set positions key { + amount: amount, + deposited-at: block-height, + unlock-block: unlock + }) + ) + ) + + ;; Update registry total + (try! (contract-call? .bitstake-pool-registry add-to-total pool-id amount)) + + (print { event: "deposit", pool-id: pool-id, depositor: tx-sender, amount: amount }) + (ok true) + ) +) + +;; ── Withdraw ────────────────────────────────────────────────────────── + +(define-public (withdraw-from-pool (pool-id uint)) + (let ( + (key { pool-id: pool-id, depositor: tx-sender }) + (position (unwrap! (map-get? positions key) ERR-NO-POSITION)) + (amount (get amount position)) + ) + (asserts! (>= block-height (get unlock-block position)) ERR-LOCKED) + + (map-delete positions key) + (try! (contract-call? .bitstake-pool-registry remove-from-total pool-id amount)) + (try! (as-contract (stx-transfer? amount tx-sender tx-sender))) + + (print { event: "withdrawal", pool-id: pool-id, depositor: tx-sender, amount: amount }) + (ok amount) + ) +) + +;; ── Read-Only ───────────────────────────────────────────────────────── + +(define-read-only (get-position (pool-id uint) (depositor principal)) + (ok (map-get? positions { pool-id: pool-id, depositor: depositor })) +) + +(define-read-only (is-locked (pool-id uint) (depositor principal)) + (match (map-get? positions { pool-id: pool-id, depositor: depositor }) + pos (ok (< block-height (get unlock-block pos))) + (ok false) + ) +) + +(define-read-only (blocks-until-unlock (pool-id uint) (depositor principal)) + (match (map-get? positions { pool-id: pool-id, depositor: depositor }) + pos (if (>= block-height (get unlock-block pos)) + (ok u0) + (ok (- (get unlock-block pos) block-height))) + (ok u0) + ) +) diff --git a/contracts/bitstake-pool-registry.clar b/contracts/bitstake-pool-registry.clar new file mode 100644 index 0000000..ab5bf2d --- /dev/null +++ b/contracts/bitstake-pool-registry.clar @@ -0,0 +1,109 @@ +;; bitstake-pool-registry.clar +;; Registry for all stacking pool tiers: Liquid, Balanced, Maxi + +(define-constant CONTRACT-OWNER tx-sender) +(define-constant ERR-NOT-OWNER (err u100)) +(define-constant ERR-POOL-NOT-FOUND (err u101)) +(define-constant ERR-POOL-INACTIVE (err u102)) +(define-constant ERR-BELOW-MINIMUM (err u103)) +(define-constant ERR-ALREADY-EXISTS (err u104)) + +;; Pool registry +(define-map pools + uint + { + name: (string-ascii 32), + lockup-cycles: uint, + min-deposit: uint, + total-stacked: uint, + active: bool, + token-symbol: (string-ascii 10) + } +) + +(define-data-var pool-count uint u0) + +;; ── Pool Management ─────────────────────────────────────────────────── + +(define-public (create-pool + (name (string-ascii 32)) + (lockup-cycles uint) + (min-deposit uint) + (token-symbol (string-ascii 10)) +) + (let ((id (+ (var-get pool-count) u1))) + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER) + (asserts! (> lockup-cycles u0) ERR-BELOW-MINIMUM) + (map-set pools id { + name: name, + lockup-cycles: lockup-cycles, + min-deposit: min-deposit, + total-stacked: u0, + active: true, + token-symbol: token-symbol + }) + (var-set pool-count id) + (print { event: "pool-created", pool-id: id, name: name, lockup-cycles: lockup-cycles }) + (ok id) + ) +) + +(define-public (set-pool-active (pool-id uint) (active bool)) + (let ((pool (unwrap! (map-get? pools pool-id) ERR-POOL-NOT-FOUND))) + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER) + (map-set pools pool-id (merge pool { active: active })) + (ok true) + ) +) + +;; Internal: update total-stacked when a deposit is made +(define-public (add-to-total (pool-id uint) (amount uint)) + (let ((pool (unwrap! (map-get? pools pool-id) ERR-POOL-NOT-FOUND))) + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER) + (map-set pools pool-id (merge pool { total-stacked: (+ (get total-stacked pool) amount) })) + (ok true) + ) +) + +(define-public (remove-from-total (pool-id uint) (amount uint)) + (let ((pool (unwrap! (map-get? pools pool-id) ERR-POOL-NOT-FOUND))) + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER) + (map-set pools pool-id (merge pool { total-stacked: (- (get total-stacked pool) amount) })) + (ok true) + ) +) + +;; ── Read-Only ───────────────────────────────────────────────────────── + +(define-read-only (get-pool (pool-id uint)) + (ok (map-get? pools pool-id)) +) + +(define-read-only (get-pool-count) + (ok (var-get pool-count)) +) + +(define-read-only (is-pool-active (pool-id uint)) + (match (map-get? pools pool-id) + pool (ok (get active pool)) + (ok false) + ) +) + +(define-read-only (get-min-deposit (pool-id uint)) + (match (map-get? pools pool-id) + pool (ok (get min-deposit pool)) + ERR-POOL-NOT-FOUND + ) +) + +;; ── Initialise Default Pools ────────────────────────────────────────── + +(begin + ;; Pool 1: Liquid — 1 cycle, 100 STX minimum + (try! (create-pool "Liquid" u1 u100000000 "lbSTX")) + ;; Pool 2: Balanced — 3 cycles, 500 STX minimum + (try! (create-pool "Balanced" u3 u500000000 "bbSTX")) + ;; Pool 3: Maxi — 12 cycles, 1000 STX minimum + (try! (create-pool "Maxi" u12 u1000000000 "mbSTX")) +) diff --git a/contracts/bitstake-rewards.clar b/contracts/bitstake-rewards.clar new file mode 100644 index 0000000..d7d17be --- /dev/null +++ b/contracts/bitstake-rewards.clar @@ -0,0 +1,82 @@ +;; bitstake-rewards.clar +;; Tracks and distributes stacking rewards per pool tier + +(define-constant CONTRACT-OWNER tx-sender) +(define-constant ERR-NOT-OWNER (err u100)) +(define-constant ERR-NO-REWARDS (err u105)) +(define-constant ERR-ZERO (err u103)) + +;; reward-epoch -> pool-id -> total rewards (uSTX) +(define-map epoch-rewards + { epoch: uint, pool-id: uint } + { total-reward: uint, total-stacked: uint } +) + +;; (depositor, pool-id) -> last claimed epoch +(define-map last-claimed + { depositor: principal, pool-id: uint } + uint +) + +(define-data-var current-epoch uint u0) + +;; ── Epoch Management ────────────────────────────────────────────────── + +(define-public (advance-epoch) + (begin + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER) + (var-set current-epoch (+ (var-get current-epoch) u1)) + (print { event: "epoch-advanced", epoch: (var-get current-epoch) }) + (ok (var-get current-epoch)) + ) +) + +(define-public (record-epoch-rewards (pool-id uint) (reward-amount uint) (stacked-amount uint)) + (let ((epoch (var-get current-epoch))) + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER) + (asserts! (> reward-amount u0) ERR-ZERO) + (map-set epoch-rewards + { epoch: epoch, pool-id: pool-id } + { total-reward: reward-amount, total-stacked: stacked-amount } + ) + (print { event: "rewards-recorded", epoch: epoch, pool-id: pool-id, reward: reward-amount }) + (ok true) + ) +) + +;; ── Reward Calculation ──────────────────────────────────────────────── + +(define-read-only (get-epoch-reward (epoch uint) (pool-id uint)) + (map-get? epoch-rewards { epoch: epoch, pool-id: pool-id }) +) + +(define-read-only (get-current-epoch) + (ok (var-get current-epoch)) +) + +(define-read-only (get-last-claimed (depositor principal) (pool-id uint)) + (default-to u0 (map-get? last-claimed { depositor: depositor, pool-id: pool-id })) +) + +;; Estimate claimable rewards for a depositor in one epoch +(define-read-only (estimate-epoch-reward (depositor principal) (pool-id uint) (epoch uint) (user-stacked uint)) + (match (map-get? epoch-rewards { epoch: epoch, pool-id: pool-id }) + data (if (> (get total-stacked data) u0) + (ok (/ (* user-stacked (get total-reward data)) (get total-stacked data))) + (ok u0)) + (ok u0) + ) +) + +;; ── Claim ───────────────────────────────────────────────────────────── + +(define-public (mark-claimed (depositor principal) (pool-id uint)) + (begin + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER) + (map-set last-claimed + { depositor: depositor, pool-id: pool-id } + (var-get current-epoch) + ) + (ok true) + ) +) diff --git a/docs/multi-pool-tiers.md b/docs/multi-pool-tiers.md new file mode 100644 index 0000000..a9f5003 --- /dev/null +++ b/docs/multi-pool-tiers.md @@ -0,0 +1,80 @@ +# Multi-Pool Strategy Tiers + +bitstake provides three distinct stacking pool tiers, each targeting a different risk/lockup/yield profile. + +## Pool Tiers + +| Pool | Token | Lockup | Min Deposit | Target APY | +|------|-------|--------|-------------|------------| +| Liquid | lbSTX | 1 cycle (~15 days) | 100 STX | ~7% | +| Balanced | bbSTX | 3 cycles (~45 days) | 500 STX | ~9% | +| Maxi | mbSTX | 12 cycles (~6 months) | 1,000 STX | ~13% | + +A Stacks stacking cycle is approximately 2,100 blocks (~2 weeks). + +## Contracts + +### `bitstake-pool-registry` +Central registry that defines each pool's parameters. Callable by the owner to: +- Create new pool tiers +- Activate / deactivate pools +- Track total stacked per pool + +### `bitstake-pool-deposits` +Handles user deposits and withdrawals. Key rules: +- Deposits transfer STX to the contract and record a position with an `unlock-block` +- `unlock-block = current-block + (lockup-cycles × 2100)` +- Withdrawals are blocked until `block-height >= unlock-block` (ERR u106) +- Deposits accumulate (add to existing position if one exists) + +### Token Contracts (`bitstake-lbstx`, `bitstake-bbstx`, `bitstake-mbstx`) +SIP-010 fungible tokens representing pool shares. The owner (pool deposits contract) mints tokens 1:1 with deposits and burns on withdrawal. + +### `bitstake-rewards` +Epoch-based reward accounting: +- Owner advances epoch each stacking cycle +- Per-epoch reward amounts are recorded per pool +- Pro-rata reward estimation: `(user_stacked / total_stacked) × epoch_reward` + +## Error Codes + +| Code | Meaning | +|------|---------| +| `u100` | Not owner | +| `u101` | Pool not found | +| `u102` | Pool inactive | +| `u103` | Below minimum deposit | +| `u104` | Already exists | +| `u105` | No position | +| `u106` | Funds still locked | + +## Deposit Flow + +``` +User → deposit-to-pool(pool-id, amount) + → [check pool active] + → [check amount >= min-deposit] + → stx-transfer?(amount, tx-sender, contract) + → map-set positions { amount, deposited-at, unlock-block } + → add-to-total(pool-id, amount) + → (owner calls mint on token contract) +``` + +## Withdrawal Flow + +``` +User → withdraw-from-pool(pool-id) + → [check position exists] + → [check block-height >= unlock-block] + → map-delete positions + → remove-from-total(pool-id, amount) + → stx-transfer?(amount, contract, user) + → (owner calls burn on token contract) +``` + +## Deployment Order + +1. `bitstake-pool-registry` — initialises 3 default pools in `begin` block +2. `bitstake-pool-deposits` — references registry via contract-call? +3. Token contracts (`lbstx`, `bbstx`, `mbstx`) — minted/burned by deposits contract +4. `bitstake-rewards` — standalone, updated by owner each epoch diff --git a/frontend/src/components/DepositForm.tsx b/frontend/src/components/DepositForm.tsx new file mode 100644 index 0000000..c5987a0 --- /dev/null +++ b/frontend/src/components/DepositForm.tsx @@ -0,0 +1,106 @@ +import React, { useState } from "react"; +import { openContractCall } from "@stacks/connect"; +import { Cl } from "@stacks/transactions"; +import type { PoolData } from "../hooks/usePoolTiers"; +import { CONTRACTS, STACKS_NETWORK } from "../constants/pools"; + +interface DepositFormProps { + pool: PoolData; + userAddress: string; + onSuccess: (amount: bigint) => void; +} + +export function DepositForm({ pool, userAddress, onSuccess }: DepositFormProps) { + const [amountStr, setAmountStr] = useState(""); + const [status, setStatus] = useState<"idle" | "pending" | "success" | "error">("idle"); + const [errorMsg, setErrorMsg] = useState(""); + + const amountSTX = parseFloat(amountStr) || 0; + const amountMicro = BigInt(Math.floor(amountSTX * 1_000_000)); + const isValid = amountSTX >= pool.minDepositSTX && !isNaN(amountSTX); + + async function handleDeposit(e: React.FormEvent) { + e.preventDefault(); + if (!isValid) return; + + setStatus("pending"); + setErrorMsg(""); + + const [contractAddress, contractName] = CONTRACTS.POOL_DEPOSITS.split("."); + + try { + await openContractCall({ + network: STACKS_NETWORK, + contractAddress, + contractName, + functionName: "deposit-to-pool", + functionArgs: [Cl.uint(pool.id), Cl.uint(amountMicro)], + postConditionMode: 1, + onFinish: () => { + setStatus("success"); + onSuccess(amountMicro); + setAmountStr(""); + }, + onCancel: () => { + setStatus("idle"); + }, + }); + } catch (err: any) { + setStatus("error"); + setErrorMsg(err?.message ?? "Transaction failed"); + } + } + + return ( +
+ ); +} diff --git a/frontend/src/components/PoolSelector.tsx b/frontend/src/components/PoolSelector.tsx new file mode 100644 index 0000000..fc91bdb --- /dev/null +++ b/frontend/src/components/PoolSelector.tsx @@ -0,0 +1,59 @@ +import React, { useState } from "react"; +import { usePoolTiers } from "../hooks/usePoolTiers"; +import { PoolTierCard } from "./PoolTierCard"; +import { DepositForm } from "./DepositForm"; + +interface PoolSelectorProps { + userAddress: string | null; + onDepositSuccess?: (poolId: number, amount: bigint) => void; +} + +export function PoolSelector({ userAddress, onDepositSuccess }: PoolSelectorProps) { + const { pools, isLoading, refetch } = usePoolTiers(); + const [selectedPoolId, setSelectedPoolId] = useState+ Select a pool tier that matches your lockup preference and yield target. +
+ + {isLoading ? ( ++ Connect your wallet to deposit into the {selectedPool.name} pool. +
+ )} +{pool.description}
+ + {pool.active && ( + + )} +