From 1185f61cde1bf76f0809f78869fd297ed10151c5 Mon Sep 17 00:00:00 2001 From: sodium16 Date: Fri, 10 Apr 2026 14:46:53 +0530 Subject: [PATCH 1/2] first frontend commit --- docs/ABI.md | 33 ++++ projects/hackalgo-frontend/src/Home.tsx | 82 +++----- .../src/assets/asset_ids.json | 4 + .../src/components/AppCalls.tsx | 98 --------- .../src/hooks/useAlgoMint.ts | 186 ++++++++++++++++++ .../src/pages/CreatorPage.tsx | 183 +++++++++++++++++ .../src/pages/GalleryPage.tsx | 135 +++++++++++++ 7 files changed, 571 insertions(+), 150 deletions(-) create mode 100644 docs/ABI.md create mode 100644 projects/hackalgo-frontend/src/assets/asset_ids.json delete mode 100644 projects/hackalgo-frontend/src/components/AppCalls.tsx create mode 100644 projects/hackalgo-frontend/src/hooks/useAlgoMint.ts create mode 100644 projects/hackalgo-frontend/src/pages/CreatorPage.tsx create mode 100644 projects/hackalgo-frontend/src/pages/GalleryPage.tsx diff --git a/docs/ABI.md b/docs/ABI.md new file mode 100644 index 0000000..042b8ad --- /dev/null +++ b/docs/ABI.md @@ -0,0 +1,33 @@ +# Algo-Mint Smart Contract ABI (ARC-4 / ARC-19) + +## Global State (read-only) +- `creator`: address – the person who minted the NFTs +- `total_nfts`: uint64 – number of NFTs minted (e.g., 10) +- `total_pct_bps`: uint64 – total % of earnings offered, in basis points (e.g., 500 = 5%) +- `duration_years`: uint64 – contract active years (e.g., 3) +- `start_quarter`: uint64 – quarter index when contract started (e.g., 20261) + +## Methods + +### mint_future_nft(axfer: pay) -> void +- Called by creator only, once. +- Sends ALGO to cover minimum balance requirement. +- Mints the entire series of NFTs (ARC-19) with metadata URI. + +### buy_nft(asset_id: uint64, axfer: pay) -> void +- Investor calls this to purchase a specific NFT. +- Payment is forwarded to the creator’s address. +- Transfers the NFT from creator to investor. + +### report_income(quarter: uint64, income_amount: uint64) -> void +- Called by creator only, once per quarter. +- `income_amount` in microAlgos or stablecoin units (we’ll use microAlgos for demo). +- Computes `payout_per_nft = (income_amount * pct_per_nft_bps) / 10000` +- Stores the total pending payout for each NFT holder. + +### claim_payout(asset_id: uint64) -> void +- Any NFT holder can call to claim their accumulated payouts. +- Transfers ALGO from contract account to caller. + +### get_pending_payout(asset_id: uint64, address: address) -> uint64 +- Read-only view method to check how much is claimable. \ No newline at end of file diff --git a/projects/hackalgo-frontend/src/Home.tsx b/projects/hackalgo-frontend/src/Home.tsx index 68313ee..f2af536 100644 --- a/projects/hackalgo-frontend/src/Home.tsx +++ b/projects/hackalgo-frontend/src/Home.tsx @@ -1,74 +1,52 @@ -// src/components/Home.tsx import { useWallet } from '@txnlab/use-wallet-react' import React, { useState } from 'react' import ConnectWallet from './components/ConnectWallet' -import Transact from './components/Transact' -import AppCalls from './components/AppCalls' +import CreatorPage from './pages/CreatorPage' +import GalleryPage from './pages/GalleryPage' interface HomeProps {} const Home: React.FC = () => { const [openWalletModal, setOpenWalletModal] = useState(false) - const [openDemoModal, setOpenDemoModal] = useState(false) - const [appCallsDemoModal, setAppCallsDemoModal] = useState(false) const { activeAddress } = useWallet() + const [activeTab, setActiveTab] = useState<'creator' | 'gallery'>('creator') const toggleWalletModal = () => { setOpenWalletModal(!openWalletModal) } - const toggleDemoModal = () => { - setOpenDemoModal(!openDemoModal) - } - - const toggleAppCallsModal = () => { - setAppCallsDemoModal(!appCallsDemoModal) - } - return ( -
-
-
-

- Welcome to
AlgoKit 🙂
-

-

- This starter has been generated using official AlgoKit React template. Refer to the resource below for next steps. -

- -
- - Getting started - - -
- + - - {activeAddress && ( - - )} - - {activeAddress && ( - - )}
- - - - +
+
+
+ +
+ {activeTab === 'creator' ? ( + + ) : ( + + )} +
+ +
) } diff --git a/projects/hackalgo-frontend/src/assets/asset_ids.json b/projects/hackalgo-frontend/src/assets/asset_ids.json new file mode 100644 index 0000000..b403e42 --- /dev/null +++ b/projects/hackalgo-frontend/src/assets/asset_ids.json @@ -0,0 +1,4 @@ +{ + "assetIds": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010] +} + diff --git a/projects/hackalgo-frontend/src/components/AppCalls.tsx b/projects/hackalgo-frontend/src/components/AppCalls.tsx deleted file mode 100644 index 3efb0b2..0000000 --- a/projects/hackalgo-frontend/src/components/AppCalls.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { useWallet } from '@txnlab/use-wallet-react' -import { useSnackbar } from 'notistack' -import { useState } from 'react' -import { AlgomintFactory } from '../contracts/Algomint' -import { OnSchemaBreak, OnUpdate } from '@algorandfoundation/algokit-utils/types/app' -import { getAlgodConfigFromViteEnvironment, getIndexerConfigFromViteEnvironment } from '../utils/network/getAlgoClientConfigs' -import { AlgorandClient } from '@algorandfoundation/algokit-utils' - -interface AppCallsInterface { - openModal: boolean - setModalState: (value: boolean) => void -} - -const AppCalls = ({ openModal, setModalState }: AppCallsInterface) => { - const [loading, setLoading] = useState(false) - const [contractInput, setContractInput] = useState('') - const { enqueueSnackbar } = useSnackbar() - const { transactionSigner, activeAddress } = useWallet() - - const algodConfig = getAlgodConfigFromViteEnvironment() - const indexerConfig = getIndexerConfigFromViteEnvironment() - const algorand = AlgorandClient.fromConfig({ - algodConfig, - indexerConfig, - }) - algorand.setDefaultSigner(transactionSigner) - - const sendAppCall = async () => { - setLoading(true) - - // Please note, in typical production scenarios, - // you wouldn't want to use deploy directly from your frontend. - // Instead, you would deploy your contract on your backend and reference it by id. - // Given the simplicity of the starter contract, we are deploying it on the frontend - // for demonstration purposes. - const factory = new AlgomintFactory({ - defaultSender: activeAddress ?? undefined, - algorand, - }) - const deployResult = await factory - .deploy({ - onSchemaBreak: OnSchemaBreak.AppendApp, - onUpdate: OnUpdate.AppendApp, - }) - .catch((e: Error) => { - enqueueSnackbar(`Error deploying the contract: ${e.message}`, { variant: 'error' }) - setLoading(false) - return undefined - }) - - if (!deployResult) { - return - } - - const { appClient } = deployResult - - const response = await appClient.send.hello({ args: { name: contractInput } }).catch((e: Error) => { - enqueueSnackbar(`Error calling the contract: ${e.message}`, { variant: 'error' }) - setLoading(false) - return undefined - }) - - if (!response) { - return - } - - enqueueSnackbar(`Response from the contract: ${response.return}`, { variant: 'success' }) - setLoading(false) - } - - return ( - -
-

Say hello to your Algorand smart contract

-
- { - setContractInput(e.target.value) - }} - /> -
- - -
-
-
- ) -} - -export default AppCalls diff --git a/projects/hackalgo-frontend/src/hooks/useAlgoMint.ts b/projects/hackalgo-frontend/src/hooks/useAlgoMint.ts new file mode 100644 index 0000000..bb052a6 --- /dev/null +++ b/projects/hackalgo-frontend/src/hooks/useAlgoMint.ts @@ -0,0 +1,186 @@ +import { useCallback, useMemo, useState } from 'react' +import assetIdsJson from '../assets/asset_ids.json' + +type AlgoMintTerms = { + nft_count: number + total_pct_bps: number + duration_years: number +} + +export type AlgoMintNft = { + assetId: number + owner: string | null +} + +type PersistedState = { + terms: AlgoMintTerms | null + creatorAddress: string | null + nfts: AlgoMintNft[] + pendingPayoutByAssetId: Record +} + +const STORAGE_KEY = 'algomint:mock:state:v1' + +function loadState(): PersistedState { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return defaultState() + const parsed = JSON.parse(raw) as PersistedState + return { + ...defaultState(), + ...parsed, + } + } catch { + return defaultState() + } +} + +function saveState(state: PersistedState) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)) +} + +function defaultState(): PersistedState { + return { + terms: null, + creatorAddress: null, + nfts: [], + pendingPayoutByAssetId: {}, + } +} + +function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +export function useAlgoMint() { + const [state, setState] = useState(() => loadState()) + + const persist = useCallback((next: PersistedState) => { + setState(next) + saveState(next) + }, []) + + const terms = state.terms + const creatorAddress = state.creatorAddress + + const listNfts = useCallback(async (): Promise => { + await delay(400) + if (state.nfts.length > 0) return state.nfts + + const fallback = (assetIdsJson as { assetIds: number[] }).assetIds.map((assetId) => ({ + assetId, + owner: null, + })) + return fallback + }, [state.nfts]) + + // ABI-shaped method names/args (mocked for now) + const mint_future_nft = useCallback( + async (args: { creator: string; nft_count: number; total_pct_bps: number; duration_years: number; mbr_payment?: number }) => { + await delay(900) + + if (args.nft_count <= 0) throw new Error('NFT count must be > 0') + if (args.total_pct_bps <= 0 || args.total_pct_bps > 10_000) throw new Error('Total % bps must be between 1 and 10000') + if (args.duration_years <= 0) throw new Error('Duration years must be > 0') + + // Mock: generate unique-ish asset IDs. + const base = Date.now() % 1_000_000_000 + const nfts: AlgoMintNft[] = Array.from({ length: args.nft_count }).map((_, i) => ({ + assetId: base + i + 1, + owner: null, + })) + + const next: PersistedState = { + ...state, + creatorAddress: args.creator, + terms: { nft_count: args.nft_count, total_pct_bps: args.total_pct_bps, duration_years: args.duration_years }, + nfts, + pendingPayoutByAssetId: {}, + } + persist(next) + + return nfts.map((n) => n.assetId) + }, + [persist, state], + ) + + const buy_nft = useCallback( + async (args: { buyer: string; asset_id: number; purchase_payment?: number }) => { + await delay(700) + const nextNfts = state.nfts.map((n) => (n.assetId === args.asset_id ? { ...n, owner: args.buyer } : n)) + const next = { ...state, nfts: nextNfts } + persist(next) + }, + [persist, state], + ) + + const get_pending_payout = useCallback( + async (args: { asset_id: number; address: string }) => { + await delay(250) + const nft = state.nfts.find((n) => n.assetId === args.asset_id) ?? null + if (!nft || nft.owner !== args.address) return 0 + return state.pendingPayoutByAssetId[args.asset_id] ?? 0 + }, + [state.nfts, state.pendingPayoutByAssetId], + ) + + const claim_payout = useCallback( + async (args: { address: string; asset_id: number }) => { + await delay(600) + const nft = state.nfts.find((n) => n.assetId === args.asset_id) ?? null + if (!nft || nft.owner !== args.address) throw new Error('You do not own this NFT') + + const nextPending = { ...state.pendingPayoutByAssetId } + nextPending[args.asset_id] = 0 + persist({ ...state, pendingPayoutByAssetId: nextPending }) + }, + [persist, state], + ) + + const report_income = useCallback( + async (args: { creator: string; quarter: number; income_amount: number }) => { + await delay(800) + if (!state.terms || !state.creatorAddress) throw new Error('Nothing minted yet') + if (args.creator !== state.creatorAddress) throw new Error('Only the creator can report income') + if (args.income_amount <= 0) throw new Error('Income must be > 0') + if (args.quarter <= 0) throw new Error('Quarter must be > 0') + + const { nft_count, total_pct_bps } = state.terms + const totalPayout = (args.income_amount * total_pct_bps) / 10_000 + const perNft = totalPayout / nft_count + + const nextPending = { ...state.pendingPayoutByAssetId } + for (const nft of state.nfts) { + if (!nft.owner) continue + nextPending[nft.assetId] = (nextPending[nft.assetId] ?? 0) + perNft + } + + persist({ ...state, pendingPayoutByAssetId: nextPending }) + return { totalPayout, perNft } + }, + [persist, state], + ) + + const resetMock = useCallback(async () => { + await delay(250) + const next = defaultState() + persist(next) + }, [persist]) + + const api = useMemo( + () => ({ + terms, + creatorAddress, + listNfts, + mint_future_nft, + buy_nft, + get_pending_payout, + claim_payout, + report_income, + resetMock, + }), + [terms, creatorAddress, listNfts, mint_future_nft, buy_nft, get_pending_payout, claim_payout, report_income, resetMock], + ) + + return api +} diff --git a/projects/hackalgo-frontend/src/pages/CreatorPage.tsx b/projects/hackalgo-frontend/src/pages/CreatorPage.tsx new file mode 100644 index 0000000..bc5b272 --- /dev/null +++ b/projects/hackalgo-frontend/src/pages/CreatorPage.tsx @@ -0,0 +1,183 @@ +import { useWallet } from '@txnlab/use-wallet-react' +import { useSnackbar } from 'notistack' +import { useMemo, useState } from 'react' +import { useAlgoMint } from '../hooks/useAlgoMint' + +export default function CreatorPage(props: { onRequestWalletConnect: () => void }) { + const { activeAddress } = useWallet() + const { enqueueSnackbar } = useSnackbar() + const algoMint = useAlgoMint() + + const [nftCount, setNftCount] = useState(10) + const [totalPercent, setTotalPercent] = useState(5) + const [durationYears, setDurationYears] = useState(3) + const [incomeAmount, setIncomeAmount] = useState(10_000) + const [quarter, setQuarter] = useState(20261) + + const canReportIncome = useMemo( + () => !!activeAddress && algoMint.creatorAddress === activeAddress && !!algoMint.terms, + [activeAddress, algoMint.creatorAddress, algoMint.terms], + ) + + return ( +
+
+
+
+

Mint your future earnings NFTs

+

Hackathon demo: uses mocked calls (fast to swap to ABI later).

+
+ + +
+ +
+ + + + + +
+ +
+ + +
+ {algoMint.terms ? ( + <> + Minted: {algoMint.terms.nft_count} NFTs, {(algoMint.terms.total_pct_bps / 100).toFixed(2)}% for{' '} + {algoMint.terms.duration_years} years + + ) : ( + <>No mint yet + )} +
+
+ +
Quarterly payout demo
+ +
+ + + + + +
+ + {!canReportIncome && algoMint.terms && ( +
Only the creator wallet that minted can report income in this demo.
+ )} +
+
+ ) +} diff --git a/projects/hackalgo-frontend/src/pages/GalleryPage.tsx b/projects/hackalgo-frontend/src/pages/GalleryPage.tsx new file mode 100644 index 0000000..17ea1c1 --- /dev/null +++ b/projects/hackalgo-frontend/src/pages/GalleryPage.tsx @@ -0,0 +1,135 @@ +import { useWallet } from '@txnlab/use-wallet-react' +import { useSnackbar } from 'notistack' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { AlgoMintNft, useAlgoMint } from '../hooks/useAlgoMint' + +type NftRow = AlgoMintNft & { pendingPayout: number } + +export default function GalleryPage(props: { onRequestWalletConnect: () => void }) { + const { activeAddress } = useWallet() + const { enqueueSnackbar } = useSnackbar() + const algoMint = useAlgoMint() + + const [rows, setRows] = useState([]) + const [loading, setLoading] = useState(true) + + const refresh = useCallback(async () => { + setLoading(true) + try { + const nfts = await algoMint.listNfts() + const withPending: NftRow[] = await Promise.all( + nfts.map(async (n) => ({ + ...n, + pendingPayout: activeAddress ? await algoMint.get_pending_payout({ asset_id: n.assetId, address: activeAddress }) : 0, + })), + ) + setRows(withPending) + } finally { + setLoading(false) + } + }, [activeAddress, algoMint]) + + useEffect(() => { + void refresh() + }, [refresh]) + + const ownedAssetIds = useMemo(() => { + if (!activeAddress) return new Set() + return new Set(rows.filter((r) => r.owner === activeAddress).map((r) => r.assetId)) + }, [activeAddress, rows]) + + return ( +
+
+
+
+

Gallery

+

Buy + claim payouts (mock).

+
+ + +
+ + {loading ? ( +
+ + Loading NFTs… +
+ ) : ( +
+ + + + + + + + + + + {rows.map((r) => { + const isOwned = activeAddress ? ownedAssetIds.has(r.assetId) : false + const isForSale = !r.owner + return ( + + + + + + + ) + })} + +
Asset IDOwnerPending payoutActions
{r.assetId}{r.owner ?? For sale}{(r.pendingPayout ?? 0).toFixed(2)} +
+ + + +
+
+
+ )} +
+
+ ) +} From 6d110a04ea667c5466a77f50d743f7b98dea17fe Mon Sep 17 00:00:00 2001 From: sodium16 Date: Fri, 10 Apr 2026 14:53:12 +0530 Subject: [PATCH 2/2] first frontend commit2 --- .github/workflows/hackalgo-frontend-cd.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/hackalgo-frontend-cd.yaml b/.github/workflows/hackalgo-frontend-cd.yaml index 0908468..2d25f48 100644 --- a/.github/workflows/hackalgo-frontend-cd.yaml +++ b/.github/workflows/hackalgo-frontend-cd.yaml @@ -20,11 +20,17 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 20 + cache: npm + cache-dependency-path: projects/hackalgo-frontend/package-lock.json - name: Install algokit run: pipx install algokit + - name: Install frontend dependencies + working-directory: projects/hackalgo-frontend + run: npm ci + - name: Bootstrap dependencies run: algokit project bootstrap all --project-name 'hackalgo-frontend' \ No newline at end of file