diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index f8b7082..01bed6c 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import "./globals.css";
import { StacksProvider } from "@/context/StacksContext";
+import Navbar from "@/components/Navbar/Navbar";
export const metadata: Metadata = {
title: "S-pay | Premium Stacks Payments",
@@ -21,6 +22,7 @@ export default function RootLayout({
+
{children}
diff --git a/frontend/app/merchant/register/page.tsx b/frontend/app/merchant/register/page.tsx
new file mode 100644
index 0000000..ea3b917
--- /dev/null
+++ b/frontend/app/merchant/register/page.tsx
@@ -0,0 +1,16 @@
+"use client";
+
+import { RegisterMerchantForm } from "@/components/RegisterMerchantForm/RegisterMerchantForm";
+import styles from "./register.module.css";
+
+export default function MerchantRegisterPage() {
+ return (
+
+
Register as Merchant
+
+ Requires 50 STX verification stake (register-merchant)
+
+
+
+ );
+}
diff --git a/frontend/app/merchant/register/register.module.css b/frontend/app/merchant/register/register.module.css
new file mode 100644
index 0000000..180a9df
--- /dev/null
+++ b/frontend/app/merchant/register/register.module.css
@@ -0,0 +1,18 @@
+.page {
+ min-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+}
+
+.title {
+ font-size: 2rem;
+ margin-bottom: 0.5rem;
+}
+
+.subtitle {
+ color: #a0a0aa;
+ margin-bottom: 2rem;
+}
diff --git a/frontend/app/page.module.css b/frontend/app/page.module.css
index 89a854d..ca249c3 100644
--- a/frontend/app/page.module.css
+++ b/frontend/app/page.module.css
@@ -48,39 +48,6 @@
line-height: 1.6;
}
-.actions {
- display: flex;
- gap: 1.5rem;
- justify-content: center;
-}
-
-.primaryBtn {
- background: var(--primary);
- color: white;
- padding: 1rem 2.5rem;
- border-radius: 12px;
- font-weight: 600;
- transition: var(--transition-smooth);
-}
-
-.primaryBtn:hover {
- transform: translateY(-2px);
- box-shadow: 0 0 20px var(--primary-glow);
-}
-
-.secondaryBtn {
- border: 1px solid var(--glass-border);
- color: var(--foreground);
- padding: 1rem 2.5rem;
- border-radius: 12px;
- font-weight: 600;
- transition: var(--transition-smooth);
-}
-
-.secondaryBtn:hover {
- background: rgba(255, 255, 255, 0.05);
-}
-
@media (max-width: 768px) {
.title {
font-size: 3rem;
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index 2daab69..02b3287 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -1,4 +1,5 @@
import styles from "./page.module.css";
+import { ConnectWallet } from "@/components/ConnectWallet/ConnectWallet";
export default function Home() {
return (
@@ -10,23 +11,11 @@ export default function Home() {
Powered by Stacks.
- Experience the most premium payment gateway for the Bitcoin ecosystem.
+ Experience the most premium payment gateway for the Bitcoin ecosystem.
Fast, decentralized, and built for modern commerce.
-
- Connect Wallet
- Learn More
-
+
);
}
-// Final polish 62
-// Final polish 63
-// Final polish 64
-// Final polish 65
-// Final polish 66
-// Final polish 67
-// Final polish 68
-// Final polish 69
-// Final polish 70
diff --git a/frontend/app/pay/page.tsx b/frontend/app/pay/page.tsx
new file mode 100644
index 0000000..84c87da
--- /dev/null
+++ b/frontend/app/pay/page.tsx
@@ -0,0 +1,16 @@
+"use client";
+
+import { PaymentForm } from "@/components/PaymentForm/PaymentForm";
+import styles from "./pay.module.css";
+
+export default function PayPage() {
+ return (
+
+
Process Payment
+
+ Send STX via S-pay protocol (process-payment)
+
+
+
+ );
+}
diff --git a/frontend/app/pay/pay.module.css b/frontend/app/pay/pay.module.css
new file mode 100644
index 0000000..180a9df
--- /dev/null
+++ b/frontend/app/pay/pay.module.css
@@ -0,0 +1,18 @@
+.page {
+ min-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+}
+
+.title {
+ font-size: 2rem;
+ margin-bottom: 0.5rem;
+}
+
+.subtitle {
+ color: #a0a0aa;
+ margin-bottom: 2rem;
+}
diff --git a/frontend/app/register/page.tsx b/frontend/app/register/page.tsx
new file mode 100644
index 0000000..977b569
--- /dev/null
+++ b/frontend/app/register/page.tsx
@@ -0,0 +1,16 @@
+"use client";
+
+import { RegisterUserForm } from "@/components/RegisterUserForm/RegisterUserForm";
+import styles from "./register.module.css";
+
+export default function RegisterPage() {
+ return (
+
+
Register as User
+
+ Pick a unique username to join S-pay (string-ascii 24 max)
+
+
+
+ );
+}
diff --git a/frontend/app/register/register.module.css b/frontend/app/register/register.module.css
new file mode 100644
index 0000000..180a9df
--- /dev/null
+++ b/frontend/app/register/register.module.css
@@ -0,0 +1,18 @@
+.page {
+ min-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+}
+
+.title {
+ font-size: 2rem;
+ margin-bottom: 0.5rem;
+}
+
+.subtitle {
+ color: #a0a0aa;
+ margin-bottom: 2rem;
+}
diff --git a/frontend/app/vault/page.tsx b/frontend/app/vault/page.tsx
new file mode 100644
index 0000000..5b9a8c8
--- /dev/null
+++ b/frontend/app/vault/page.tsx
@@ -0,0 +1,16 @@
+"use client";
+
+import { VaultDepositForm } from "@/components/VaultDepositForm/VaultDepositForm";
+import styles from "./vault.module.css";
+
+export default function VaultPage() {
+ return (
+
+
Vault Deposit
+
+ Deposit STX to your S-pay vault (vault-deposit)
+
+
+
+ );
+}
diff --git a/frontend/app/vault/vault.module.css b/frontend/app/vault/vault.module.css
new file mode 100644
index 0000000..180a9df
--- /dev/null
+++ b/frontend/app/vault/vault.module.css
@@ -0,0 +1,18 @@
+.page {
+ min-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+}
+
+.title {
+ font-size: 2rem;
+ margin-bottom: 0.5rem;
+}
+
+.subtitle {
+ color: #a0a0aa;
+ margin-bottom: 2rem;
+}
diff --git a/frontend/components/ConnectWallet/ConnectWallet.module.css b/frontend/components/ConnectWallet/ConnectWallet.module.css
new file mode 100644
index 0000000..8855cab
--- /dev/null
+++ b/frontend/components/ConnectWallet/ConnectWallet.module.css
@@ -0,0 +1,42 @@
+.actions {
+ display: flex;
+ gap: 1.5rem;
+ justify-content: center;
+ align-items: center;
+}
+
+.address {
+ font-family: monospace;
+ font-size: 0.95rem;
+ color: var(--foreground);
+ background: rgba(255, 255, 255, 0.05);
+ padding: 0.5rem 1rem;
+ border-radius: 8px;
+}
+
+.primaryBtn {
+ background: var(--primary);
+ color: white;
+ padding: 1rem 2.5rem;
+ border-radius: 12px;
+ font-weight: 600;
+ transition: var(--transition-smooth);
+}
+
+.primaryBtn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 0 20px var(--primary-glow);
+}
+
+.secondaryBtn {
+ border: 1px solid var(--glass-border);
+ color: var(--foreground);
+ padding: 1rem 2.5rem;
+ border-radius: 12px;
+ font-weight: 600;
+ transition: var(--transition-smooth);
+}
+
+.secondaryBtn:hover {
+ background: rgba(255, 255, 255, 0.05);
+}
diff --git a/frontend/components/ConnectWallet/ConnectWallet.tsx b/frontend/components/ConnectWallet/ConnectWallet.tsx
new file mode 100644
index 0000000..4649bfe
--- /dev/null
+++ b/frontend/components/ConnectWallet/ConnectWallet.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { useStacks } from "@/context/StacksContext";
+import styles from "./ConnectWallet.module.css";
+
+export function ConnectWallet() {
+ const { userData, handleConnect, handleDisconnect } = useStacks();
+ const address = userData?.profile?.stxAddress?.mainnet ?? userData?.profile?.stxAddress?.testnet;
+
+ return (
+
+ {address ? (
+ <>
+ {address.slice(0, 6)}…{address.slice(-4)}
+
+ Disconnect
+
+ >
+ ) : (
+ <>
+
+ Connect Wallet
+
+ Learn More
+ >
+ )}
+
+ );
+}
diff --git a/frontend/components/Navbar/Navbar.module.css b/frontend/components/Navbar/Navbar.module.css
index ae3cf90..f2aa914 100644
--- a/frontend/components/Navbar/Navbar.module.css
+++ b/frontend/components/Navbar/Navbar.module.css
@@ -49,6 +49,21 @@
color: var(--foreground);
}
+.actions {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.address {
+ font-family: monospace;
+ font-size: 0.85rem;
+ color: var(--foreground);
+ background: rgba(255, 255, 255, 0.05);
+ padding: 0.4rem 0.8rem;
+ border-radius: 8px;
+}
+
.connectBtn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--glass-border);
diff --git a/frontend/components/Navbar/Navbar.tsx b/frontend/components/Navbar/Navbar.tsx
index b6b4cda..10f2525 100644
--- a/frontend/components/Navbar/Navbar.tsx
+++ b/frontend/components/Navbar/Navbar.tsx
@@ -1,9 +1,13 @@
"use client";
import Link from "next/link";
+import { useStacks } from "@/context/StacksContext";
import styles from "./Navbar.module.css";
export default function Navbar() {
+ const { userData, handleConnect, handleDisconnect } = useStacks();
+ const addr = userData?.profile?.stxAddress?.mainnet ?? userData?.profile?.stxAddress?.testnet;
+
return (
@@ -11,12 +15,23 @@ export default function Navbar() {
S-pay
.
+ Register
+ Pay
+ Vault
+ Merchant
Payments
Tokens
Developers
- Connect
+ {addr ? (
+ <>
+ {addr.slice(0, 6)}…{addr.slice(-4)}
+ Disconnect
+ >
+ ) : (
+ Connect
+ )}
diff --git a/frontend/components/PaymentForm/PaymentForm.module.css b/frontend/components/PaymentForm/PaymentForm.module.css
new file mode 100644
index 0000000..4716d90
--- /dev/null
+++ b/frontend/components/PaymentForm/PaymentForm.module.css
@@ -0,0 +1,22 @@
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ max-width: 400px;
+}
+
+.input {
+ padding: 0.6rem 1rem;
+ border-radius: 8px;
+ border: 1px solid var(--glass-border);
+ background: rgba(255, 255, 255, 0.05);
+ color: var(--foreground);
+}
+
+.btn {
+ background: var(--primary);
+ color: white;
+ padding: 0.75rem;
+ border-radius: 8px;
+ font-weight: 600;
+}
diff --git a/frontend/components/PaymentForm/PaymentForm.tsx b/frontend/components/PaymentForm/PaymentForm.tsx
new file mode 100644
index 0000000..d6d5383
--- /dev/null
+++ b/frontend/components/PaymentForm/PaymentForm.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { useState } from "react";
+import { useProcessPayment } from "@/hooks/useProcessPayment";
+import styles from "./PaymentForm.module.css";
+
+export function PaymentForm() {
+ const [recipient, setRecipient] = useState("");
+ const [amountStx, setAmountStx] = useState("");
+ const { processPayment } = useProcessPayment();
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ const amount = BigInt(Math.floor(parseFloat(amountStx || "0") * 1e6));
+ if (recipient && amount > 0n) processPayment(amount, recipient);
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/components/RegisterMerchantForm/RegisterMerchantForm.module.css b/frontend/components/RegisterMerchantForm/RegisterMerchantForm.module.css
new file mode 100644
index 0000000..d32b08f
--- /dev/null
+++ b/frontend/components/RegisterMerchantForm/RegisterMerchantForm.module.css
@@ -0,0 +1,26 @@
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ max-width: 400px;
+}
+
+.input {
+ padding: 0.6rem 1rem;
+ border-radius: 8px;
+ border: 1px solid var(--glass-border);
+ background: rgba(255, 255, 255, 0.05);
+ color: var(--foreground);
+}
+
+.btn {
+ background: var(--primary);
+ color: white;
+ padding: 0.75rem;
+ border-radius: 8px;
+ font-weight: 600;
+}
+
+.hint {
+ color: #a0a0aa;
+}
diff --git a/frontend/components/RegisterMerchantForm/RegisterMerchantForm.tsx b/frontend/components/RegisterMerchantForm/RegisterMerchantForm.tsx
new file mode 100644
index 0000000..dcf294b
--- /dev/null
+++ b/frontend/components/RegisterMerchantForm/RegisterMerchantForm.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import { useState } from "react";
+import { useRegisterMerchant } from "@/hooks/useRegisterMerchant";
+import { useStacks } from "@/context/StacksContext";
+import styles from "./RegisterMerchantForm.module.css";
+
+export function RegisterMerchantForm() {
+ const [businessName, setBusinessName] = useState("");
+ const [website, setWebsite] = useState("");
+ const { registerMerchant } = useRegisterMerchant();
+ const { userData } = useStacks();
+ const isConnected = !!userData;
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (businessName.trim() && website.trim())
+ registerMerchant(businessName.trim(), website.trim());
+ };
+
+ if (!isConnected) return Connect wallet first
;
+
+ return (
+
+ );
+}
diff --git a/frontend/components/RegisterUserForm/RegisterUserForm.module.css b/frontend/components/RegisterUserForm/RegisterUserForm.module.css
new file mode 100644
index 0000000..c864cbc
--- /dev/null
+++ b/frontend/components/RegisterUserForm/RegisterUserForm.module.css
@@ -0,0 +1,26 @@
+.form {
+ display: flex;
+ gap: 0.75rem;
+}
+
+.input {
+ padding: 0.6rem 1rem;
+ border-radius: 8px;
+ border: 1px solid var(--glass-border);
+ background: rgba(255, 255, 255, 0.05);
+ color: var(--foreground);
+ min-width: 200px;
+}
+
+.btn {
+ background: var(--primary);
+ color: white;
+ padding: 0.6rem 1.25rem;
+ border-radius: 8px;
+ font-weight: 600;
+}
+
+.hint {
+ color: #a0a0aa;
+ font-size: 0.9rem;
+}
diff --git a/frontend/components/RegisterUserForm/RegisterUserForm.tsx b/frontend/components/RegisterUserForm/RegisterUserForm.tsx
new file mode 100644
index 0000000..86bb849
--- /dev/null
+++ b/frontend/components/RegisterUserForm/RegisterUserForm.tsx
@@ -0,0 +1,35 @@
+"use client";
+
+import { useState } from "react";
+import { useRegisterUser } from "@/hooks/useRegisterUser";
+import styles from "./RegisterUserForm.module.css";
+
+export function RegisterUserForm() {
+ const [username, setUsername] = useState("");
+ const { registerUser, isConnected } = useRegisterUser();
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (username.trim()) registerUser(username.trim());
+ };
+
+ if (!isConnected) {
+ return Connect wallet to register
;
+ }
+
+ return (
+
+ );
+}
diff --git a/frontend/components/VaultDepositForm/VaultDepositForm.module.css b/frontend/components/VaultDepositForm/VaultDepositForm.module.css
new file mode 100644
index 0000000..3845ba4
--- /dev/null
+++ b/frontend/components/VaultDepositForm/VaultDepositForm.module.css
@@ -0,0 +1,21 @@
+.form {
+ display: flex;
+ gap: 0.75rem;
+}
+
+.input {
+ padding: 0.6rem 1rem;
+ border-radius: 8px;
+ border: 1px solid var(--glass-border);
+ background: rgba(255, 255, 255, 0.05);
+ color: var(--foreground);
+ min-width: 150px;
+}
+
+.btn {
+ background: var(--primary);
+ color: white;
+ padding: 0.6rem 1.25rem;
+ border-radius: 8px;
+ font-weight: 600;
+}
diff --git a/frontend/components/VaultDepositForm/VaultDepositForm.tsx b/frontend/components/VaultDepositForm/VaultDepositForm.tsx
new file mode 100644
index 0000000..b8a1463
--- /dev/null
+++ b/frontend/components/VaultDepositForm/VaultDepositForm.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import { useState } from "react";
+import { useVaultDeposit } from "@/hooks/useVaultDeposit";
+import styles from "./VaultDepositForm.module.css";
+
+export function VaultDepositForm() {
+ const [amountStx, setAmountStx] = useState("");
+ const { vaultDeposit } = useVaultDeposit();
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ const amount = BigInt(Math.floor(parseFloat(amountStx || "0") * 1e6));
+ if (amount > 0n) vaultDeposit(amount);
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/context/StacksContext.tsx b/frontend/context/StacksContext.tsx
index 8ed417d..f1fb025 100644
--- a/frontend/context/StacksContext.tsx
+++ b/frontend/context/StacksContext.tsx
@@ -1,8 +1,14 @@
"use client";
-import React, { createContext, useContext, useState, useEffect } from 'react';
-import { AppConfig, UserSession, showConnect } from '@stacks/connect';
-import { StacksMainnet } from '@stacks/network';
+import React, { createContext, useContext, useState, useEffect } from "react";
+import {
+ AppConfig,
+ UserSession,
+ showConnect,
+ openContractCall,
+ openSTXTransfer,
+} from "@stacks/connect";
+import { StacksMainnet } from "@stacks/network";
export interface StacksUserData {
profile: {
@@ -13,12 +19,21 @@ export interface StacksUserData {
};
}
+export interface ContractCallBase {
+ contractAddress: string;
+ contractName: string;
+ functionName: string;
+ functionArgs: unknown[];
+}
+
interface StacksContextType {
userSession: UserSession;
userData: StacksUserData | null;
handleConnect: () => void;
handleDisconnect: () => void;
- network: any;
+ network: unknown;
+ callContract: (options: ContractCallBase) => void;
+ transferStx: (recipient: string, amountMicroStx: bigint) => void;
}
const StacksContext = createContext(undefined);
@@ -55,8 +70,39 @@ export function StacksProvider({ children }: { children: React.ReactNode }) {
setUserData(null);
};
+ const callContract = (options: ContractCallBase) => {
+ openContractCall({
+ ...options,
+ userSession,
+ network,
+ onFinish: () => {},
+ onCancel: () => {},
+ });
+ };
+
+ const transferStx = (recipient: string, amountMicroStx: bigint) => {
+ openSTXTransfer({
+ recipient,
+ amount: amountMicroStx,
+ userSession,
+ network,
+ onFinish: () => {},
+ onCancel: () => {},
+ });
+ };
+
return (
-
+
{children}
);
diff --git a/frontend/hooks/useMerchantWithdraw.ts b/frontend/hooks/useMerchantWithdraw.ts
new file mode 100644
index 0000000..d909113
--- /dev/null
+++ b/frontend/hooks/useMerchantWithdraw.ts
@@ -0,0 +1,18 @@
+"use client";
+
+import { useCallback } from "react";
+import { useStacks } from "@/context/StacksContext";
+import { buildMerchantWithdrawOptions } from "@/lib/contract-calls";
+
+export function useMerchantWithdraw() {
+ const { callContract } = useStacks();
+
+ const merchantWithdraw = useCallback(
+ (amountMicroStx: bigint | number) => {
+ callContract(buildMerchantWithdrawOptions(amountMicroStx));
+ },
+ [callContract]
+ );
+
+ return { merchantWithdraw };
+}
diff --git a/frontend/hooks/useProcessPayment.ts b/frontend/hooks/useProcessPayment.ts
new file mode 100644
index 0000000..461bbf2
--- /dev/null
+++ b/frontend/hooks/useProcessPayment.ts
@@ -0,0 +1,18 @@
+"use client";
+
+import { useCallback } from "react";
+import { useStacks } from "@/context/StacksContext";
+import { buildProcessPaymentOptions } from "@/lib/contract-calls";
+
+export function useProcessPayment() {
+ const { callContract } = useStacks();
+
+ const processPayment = useCallback(
+ (amountMicroStx: bigint | number, recipient: string) => {
+ callContract(buildProcessPaymentOptions(amountMicroStx, recipient));
+ },
+ [callContract]
+ );
+
+ return { processPayment };
+}
diff --git a/frontend/hooks/useProtocolStatus.ts b/frontend/hooks/useProtocolStatus.ts
new file mode 100644
index 0000000..75dd10c
--- /dev/null
+++ b/frontend/hooks/useProtocolStatus.ts
@@ -0,0 +1,25 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+import { useStacks } from "@/context/StacksContext";
+import { getProtocolStatus } from "@/lib/read-only";
+
+export function useProtocolStatus() {
+ const { userData } = useStacks();
+ const [status, setStatus] = useState(null);
+
+ const address =
+ userData?.profile?.stxAddress?.mainnet ?? userData?.profile?.stxAddress?.testnet;
+
+ const fetchStatus = useCallback(async () => {
+ const sender = address ?? "SP2DBFGMT7SATSJPCCA38SDDPBNNQ86QWADJ3E6WT";
+ const result = await getProtocolStatus(sender);
+ setStatus(result);
+ }, [address]);
+
+ useEffect(() => {
+ fetchStatus();
+ }, [fetchStatus]);
+
+ return { status, refetch: fetchStatus };
+}
diff --git a/frontend/hooks/useReclaimStake.ts b/frontend/hooks/useReclaimStake.ts
new file mode 100644
index 0000000..7270b0d
--- /dev/null
+++ b/frontend/hooks/useReclaimStake.ts
@@ -0,0 +1,15 @@
+"use client";
+
+import { useCallback } from "react";
+import { useStacks } from "@/context/StacksContext";
+import { buildReclaimStakeOptions } from "@/lib/contract-calls";
+
+export function useReclaimStake() {
+ const { callContract } = useStacks();
+
+ const reclaimStake = useCallback(() => {
+ callContract(buildReclaimStakeOptions());
+ }, [callContract]);
+
+ return { reclaimStake };
+}
diff --git a/frontend/hooks/useRegisterMerchant.ts b/frontend/hooks/useRegisterMerchant.ts
new file mode 100644
index 0000000..b2b1b7d
--- /dev/null
+++ b/frontend/hooks/useRegisterMerchant.ts
@@ -0,0 +1,18 @@
+"use client";
+
+import { useCallback } from "react";
+import { useStacks } from "@/context/StacksContext";
+import { buildRegisterMerchantOptions } from "@/lib/contract-calls";
+
+export function useRegisterMerchant() {
+ const { callContract } = useStacks();
+
+ const registerMerchant = useCallback(
+ (businessName: string, website: string) => {
+ callContract(buildRegisterMerchantOptions(businessName, website));
+ },
+ [callContract]
+ );
+
+ return { registerMerchant };
+}
diff --git a/frontend/hooks/useRegisterUser.ts b/frontend/hooks/useRegisterUser.ts
new file mode 100644
index 0000000..22e35fa
--- /dev/null
+++ b/frontend/hooks/useRegisterUser.ts
@@ -0,0 +1,19 @@
+"use client";
+
+import { useCallback } from "react";
+import { useStacks } from "@/context/StacksContext";
+import { buildRegisterUserOptions } from "@/lib/contract-calls";
+
+export function useRegisterUser() {
+ const { callContract, userData } = useStacks();
+
+ const registerUser = useCallback(
+ (username: string) => {
+ const opts = buildRegisterUserOptions(username);
+ callContract(opts);
+ },
+ [callContract]
+ );
+
+ return { registerUser, isConnected: !!userData };
+}
diff --git a/frontend/hooks/useTransferStx.ts b/frontend/hooks/useTransferStx.ts
new file mode 100644
index 0000000..6bdadfa
--- /dev/null
+++ b/frontend/hooks/useTransferStx.ts
@@ -0,0 +1,17 @@
+"use client";
+
+import { useCallback } from "react";
+import { useStacks } from "@/context/StacksContext";
+
+export function useTransferStx() {
+ const { transferStx } = useStacks();
+
+ const transfer = useCallback(
+ (recipient: string, amountMicroStx: bigint) => {
+ transferStx(recipient, amountMicroStx);
+ },
+ [transferStx]
+ );
+
+ return { transfer };
+}
diff --git a/frontend/hooks/useUserData.ts b/frontend/hooks/useUserData.ts
new file mode 100644
index 0000000..71a8e15
--- /dev/null
+++ b/frontend/hooks/useUserData.ts
@@ -0,0 +1,36 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+import { principalCV } from "@stacks/transactions";
+import { useStacks } from "@/context/StacksContext";
+import { callReadOnly } from "@/lib/read-only";
+
+export function useUserData() {
+ const { userData } = useStacks();
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(false);
+
+ const address =
+ userData?.profile?.stxAddress?.mainnet ?? userData?.profile?.stxAddress?.testnet;
+
+ const fetchUserData = useCallback(async () => {
+ if (!address) return;
+ setLoading(true);
+ try {
+ const result = await callReadOnly(
+ "get-user-data",
+ [principalCV(address)],
+ address
+ );
+ setData(result);
+ } finally {
+ setLoading(false);
+ }
+ }, [address]);
+
+ useEffect(() => {
+ fetchUserData();
+ }, [fetchUserData]);
+
+ return { userData: data, loading, refetch: fetchUserData };
+}
diff --git a/frontend/hooks/useVaultDeposit.ts b/frontend/hooks/useVaultDeposit.ts
new file mode 100644
index 0000000..76b7162
--- /dev/null
+++ b/frontend/hooks/useVaultDeposit.ts
@@ -0,0 +1,18 @@
+"use client";
+
+import { useCallback } from "react";
+import { useStacks } from "@/context/StacksContext";
+import { buildVaultDepositOptions } from "@/lib/contract-calls";
+
+export function useVaultDeposit() {
+ const { callContract } = useStacks();
+
+ const vaultDeposit = useCallback(
+ (amountMicroStx: bigint | number) => {
+ callContract(buildVaultDepositOptions(amountMicroStx));
+ },
+ [callContract]
+ );
+
+ return { vaultDeposit };
+}
diff --git a/frontend/hooks/useVaultWithdraw.ts b/frontend/hooks/useVaultWithdraw.ts
new file mode 100644
index 0000000..36bee29
--- /dev/null
+++ b/frontend/hooks/useVaultWithdraw.ts
@@ -0,0 +1,18 @@
+"use client";
+
+import { useCallback } from "react";
+import { useStacks } from "@/context/StacksContext";
+import { buildVaultWithdrawOptions } from "@/lib/contract-calls";
+
+export function useVaultWithdraw() {
+ const { callContract } = useStacks();
+
+ const vaultWithdraw = useCallback(
+ (amountMicroStx: bigint | number) => {
+ callContract(buildVaultWithdrawOptions(amountMicroStx));
+ },
+ [callContract]
+ );
+
+ return { vaultWithdraw };
+}
diff --git a/frontend/lib/constants.ts b/frontend/lib/constants.ts
new file mode 100644
index 0000000..b1fb526
--- /dev/null
+++ b/frontend/lib/constants.ts
@@ -0,0 +1,10 @@
+/**
+ * S-pay contract configuration.
+ * Uses mainnet s-pay-v3 deployment.
+ */
+export const SPAY_CONTRACT = {
+ address: "SP2DBFGMT7SATSJPCCA38SDDPBNNQ86QWADJ3E6WT",
+ name: "s-pay-v3",
+} as const;
+
+export const SPAY_FULL_CONTRACT = `${SPAY_CONTRACT.address}.${SPAY_CONTRACT.name}`;
diff --git a/frontend/lib/contract-calls.ts b/frontend/lib/contract-calls.ts
new file mode 100644
index 0000000..cdf01e5
--- /dev/null
+++ b/frontend/lib/contract-calls.ts
@@ -0,0 +1,72 @@
+/**
+ * Contract call helpers using @stacks/transactions for Clarity types
+ * and @stacks/connect for openContractCall signing.
+ */
+import { stringAsciiCV, uintCV, principalCV } from "@stacks/transactions";
+import { SPAY_CONTRACT } from "./constants";
+
+export function buildRegisterUserOptions(username: string) {
+ return {
+ contractAddress: SPAY_CONTRACT.address,
+ contractName: SPAY_CONTRACT.name,
+ functionName: "register-user",
+ functionArgs: [stringAsciiCV(username)],
+ };
+}
+
+export function buildVaultDepositOptions(amount: bigint | number) {
+ return {
+ contractAddress: SPAY_CONTRACT.address,
+ contractName: SPAY_CONTRACT.name,
+ functionName: "vault-deposit",
+ functionArgs: [uintCV(amount)],
+ };
+}
+
+export function buildProcessPaymentOptions(amount: bigint | number, recipient: string) {
+ return {
+ contractAddress: SPAY_CONTRACT.address,
+ contractName: SPAY_CONTRACT.name,
+ functionName: "process-payment",
+ functionArgs: [uintCV(amount), principalCV(recipient)],
+ };
+}
+
+export function buildRegisterMerchantOptions(
+ businessName: string,
+ website: string
+) {
+ return {
+ contractAddress: SPAY_CONTRACT.address,
+ contractName: SPAY_CONTRACT.name,
+ functionName: "register-merchant",
+ functionArgs: [stringAsciiCV(businessName), stringAsciiCV(website)],
+ };
+}
+
+export function buildVaultWithdrawOptions(amount: bigint | number) {
+ return {
+ contractAddress: SPAY_CONTRACT.address,
+ contractName: SPAY_CONTRACT.name,
+ functionName: "vault-withdraw",
+ functionArgs: [uintCV(amount)],
+ };
+}
+
+export function buildMerchantWithdrawOptions(amount: bigint | number) {
+ return {
+ contractAddress: SPAY_CONTRACT.address,
+ contractName: SPAY_CONTRACT.name,
+ functionName: "merchant-withdraw",
+ functionArgs: [uintCV(amount)],
+ };
+}
+
+export function buildReclaimStakeOptions() {
+ return {
+ contractAddress: SPAY_CONTRACT.address,
+ contractName: SPAY_CONTRACT.name,
+ functionName: "reclaim-stake",
+ functionArgs: [],
+ };
+}
diff --git a/frontend/lib/read-only.ts b/frontend/lib/read-only.ts
new file mode 100644
index 0000000..cd86b8c
--- /dev/null
+++ b/frontend/lib/read-only.ts
@@ -0,0 +1,30 @@
+/**
+ * Read-only contract calls using @stacks/transactions for encoding/decoding.
+ */
+import { cvToHex, deserializeCV, cvToValue } from "@stacks/transactions";
+import type { ClarityValue } from "@stacks/transactions";
+import { SPAY_CONTRACT } from "./constants";
+
+const API_BASE = "https://api.mainnet.hiro.so/v2";
+
+export async function callReadOnly(
+ functionName: string,
+ functionArgs: ClarityValue[],
+ senderAddress: string
+): Promise {
+ const args = functionArgs.map((cv) => cvToHex(cv));
+ const url = `${API_BASE}/contracts/call-read/${SPAY_CONTRACT.address}/${SPAY_CONTRACT.name}/${functionName}`;
+ const res = await fetch(url, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ sender: senderAddress, arguments: args }),
+ });
+ if (!res.ok) throw new Error(`Read call failed: ${res.statusText}`);
+ const data = await res.json();
+ return deserializeCV(data.result) as T;
+}
+
+export async function getProtocolStatus(senderAddress: string) {
+ const cv = await callReadOnly("get-protocol-status", [], senderAddress);
+ return cvToValue(cv);
+}