From 3d6a16bccc4ffe954a7a284605e77a7d1d79750c Mon Sep 17 00:00:00 2001 From: Sergey Chystiakov Date: Sun, 6 Jul 2025 01:51:39 +0200 Subject: [PATCH 01/13] initial commit of p2p board --- examples/swap-board-ml-ml/.env.example | 5 + examples/swap-board-ml-ml/.gitignore | 50 +++ examples/swap-board-ml-ml/README.md | 156 +++++++ examples/swap-board-ml-ml/next.config.js | 15 + examples/swap-board-ml-ml/package.json | 33 ++ examples/swap-board-ml-ml/postcss.config.js | 6 + .../swap-board-ml-ml/prisma/schema.prisma | 40 ++ .../src/app/api/offers/route.ts | 63 +++ .../src/app/api/swaps/[id]/route.ts | 101 +++++ .../src/app/api/swaps/route.ts | 62 +++ .../swap-board-ml-ml/src/app/create/page.tsx | 245 +++++++++++ examples/swap-board-ml-ml/src/app/globals.css | 3 + examples/swap-board-ml-ml/src/app/layout.tsx | 47 ++ .../swap-board-ml-ml/src/app/offers/page.tsx | 178 ++++++++ examples/swap-board-ml-ml/src/app/page.tsx | 59 +++ .../src/app/swap/[id]/page.tsx | 403 ++++++++++++++++++ .../swap-board-ml-ml/src/lib/htlc-utils.ts | 90 ++++ examples/swap-board-ml-ml/src/lib/prisma.ts | 9 + examples/swap-board-ml-ml/src/types/swap.ts | 46 ++ examples/swap-board-ml-ml/tailwind.config.js | 27 ++ examples/swap-board-ml-ml/tsconfig.json | 28 ++ 21 files changed, 1666 insertions(+) create mode 100644 examples/swap-board-ml-ml/.env.example create mode 100644 examples/swap-board-ml-ml/.gitignore create mode 100644 examples/swap-board-ml-ml/README.md create mode 100644 examples/swap-board-ml-ml/next.config.js create mode 100644 examples/swap-board-ml-ml/package.json create mode 100644 examples/swap-board-ml-ml/postcss.config.js create mode 100644 examples/swap-board-ml-ml/prisma/schema.prisma create mode 100644 examples/swap-board-ml-ml/src/app/api/offers/route.ts create mode 100644 examples/swap-board-ml-ml/src/app/api/swaps/[id]/route.ts create mode 100644 examples/swap-board-ml-ml/src/app/api/swaps/route.ts create mode 100644 examples/swap-board-ml-ml/src/app/create/page.tsx create mode 100644 examples/swap-board-ml-ml/src/app/globals.css create mode 100644 examples/swap-board-ml-ml/src/app/layout.tsx create mode 100644 examples/swap-board-ml-ml/src/app/offers/page.tsx create mode 100644 examples/swap-board-ml-ml/src/app/page.tsx create mode 100644 examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx create mode 100644 examples/swap-board-ml-ml/src/lib/htlc-utils.ts create mode 100644 examples/swap-board-ml-ml/src/lib/prisma.ts create mode 100644 examples/swap-board-ml-ml/src/types/swap.ts create mode 100644 examples/swap-board-ml-ml/tailwind.config.js create mode 100644 examples/swap-board-ml-ml/tsconfig.json diff --git a/examples/swap-board-ml-ml/.env.example b/examples/swap-board-ml-ml/.env.example new file mode 100644 index 0000000..810b817 --- /dev/null +++ b/examples/swap-board-ml-ml/.env.example @@ -0,0 +1,5 @@ +# Database +DATABASE_URL="file:./dev.db" + +# Mintlayer Network (testnet or mainnet) +NEXT_PUBLIC_MINTLAYER_NETWORK="testnet" diff --git a/examples/swap-board-ml-ml/.gitignore b/examples/swap-board-ml-ml/.gitignore new file mode 100644 index 0000000..19ee168 --- /dev/null +++ b/examples/swap-board-ml-ml/.gitignore @@ -0,0 +1,50 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# database +prisma/dev.db +prisma/dev.db-journal + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +Thumbs.db diff --git a/examples/swap-board-ml-ml/README.md b/examples/swap-board-ml-ml/README.md new file mode 100644 index 0000000..4b84012 --- /dev/null +++ b/examples/swap-board-ml-ml/README.md @@ -0,0 +1,156 @@ +# Mintlayer P2P Swap Board + +A minimal peer-to-peer token swap board for Mintlayer tokens using HTLC (Hash Time Locked Contracts) atomic swaps. + +## Features + +- **Create Swap Offers**: Post your intent to swap one Mintlayer token for another +- **Browse & Accept Offers**: View available offers and accept the ones that interest you +- **Atomic Swaps**: Secure token exchanges using HTLC contracts via mintlayer-connect-sdk +- **Status Tracking**: Real-time monitoring of swap progress with clear status indicators +- **Wallet Integration**: Connect with Mojito wallet for seamless transactions + +## Tech Stack + +- **Frontend**: Next.js 14 (App Router) + React + Tailwind CSS +- **Backend**: Next.js API routes +- **Database**: SQLite with Prisma ORM +- **Blockchain**: Mintlayer Connect SDK for HTLC operations +- **Package Manager**: pnpm (workspace integration) + +## Getting Started + +### Prerequisites + +- Node.js 18+ +- pnpm +- Mojito wallet extension + +### Installation + +1. Install dependencies: +```bash +cd examples/swap-board-ml-ml +pnpm install +``` + +2. Set up the database: +```bash +pnpm db:generate +pnpm db:push +``` + +3. Copy environment variables: +```bash +cp .env.example .env.local +``` + +4. Start the development server: +```bash +pnpm dev +``` + +5. Open [http://localhost:3000](http://localhost:3000) in your browser + +## Usage + +### Creating an Offer + +1. Navigate to `/create` +2. Connect your Mojito wallet +3. Fill in the swap details: + - Token to give (Token ID) + - Amount to give + - Token to receive (Token ID) + - Amount to receive + - Optional contact information +4. Submit the offer + +### Accepting an Offer + +1. Browse offers at `/offers` +2. Connect your wallet +3. Click "Accept Offer" on any available offer +4. You'll be redirected to the swap progress page + +### Monitoring Swaps + +1. Visit `/swap/[id]` to track swap progress +2. The page shows: + - Current swap status + - Progress steps + - Next actions required + - HTLC details when available + +## Swap Process + +1. **Offer Created**: User posts swap intention +2. **Offer Accepted**: Another user accepts the offer +3. **HTLC Creation**: Creator creates initial HTLC with secret hash +4. **Counterparty HTLC**: Taker creates matching HTLC +5. **Token Claiming**: Both parties reveal secrets to claim tokens +6. **Completion**: Swap finalized or manually refunded after timelock expires + +## Database Schema + +### Offer Model +- Stores swap offers with token details and creator information +- Tracks offer status (open, taken, completed, cancelled) + +### Swap Model +- Manages active swaps linked to offers +- Stores HTLC secrets, transaction hashes, and status updates +- Tracks swap progress from pending to completion + +## API Endpoints + +- `GET/POST /api/offers` - List and create swap offers +- `POST /api/swaps` - Accept an offer (creates new swap) +- `GET/POST /api/swaps/[id]` - Get and update swap status + +## Development + +### Database Operations + +```bash +# Generate Prisma client +pnpm db:generate + +# Push schema changes +pnpm db:push + +# Open database browser +pnpm db:studio +``` + +### Building + +```bash +# Build for production +pnpm build + +# Start production server +pnpm start +``` + +## Security Considerations + +- HTLC contracts provide atomic swap guarantees +- Timelock mechanisms prevent indefinite locks - users must manually refund after expiry +- No private keys are stored in the database +- All transactions require wallet confirmation + +## Contributing + +This is a minimal example implementation. For production use, consider: + +- Enhanced error handling and validation +- Comprehensive testing suite +- Rate limiting and spam protection +- Advanced UI/UX improvements +- Mobile responsiveness optimization +- Real-time notifications + +## License + +This project is part of the Mintlayer Connect SDK examples. diff --git a/examples/swap-board-ml-ml/next.config.js b/examples/swap-board-ml-ml/next.config.js new file mode 100644 index 0000000..120333d --- /dev/null +++ b/examples/swap-board-ml-ml/next.config.js @@ -0,0 +1,15 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + appDir: true, + }, + webpack: (config) => { + config.experiments = { + ...config.experiments, + asyncWebAssembly: true, + }; + return config; + }, +} + +module.exports = nextConfig diff --git a/examples/swap-board-ml-ml/package.json b/examples/swap-board-ml-ml/package.json new file mode 100644 index 0000000..abb76e9 --- /dev/null +++ b/examples/swap-board-ml-ml/package.json @@ -0,0 +1,33 @@ +{ + "name": "swap-board-ml-ml", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:studio": "prisma studio" + }, + "dependencies": { + "@mintlayer/sdk": "workspace:*", + "@prisma/client": "^5.7.0", + "next": "14.0.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "prisma": "^5.7.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "autoprefixer": "^10.4.16", + "eslint": "^8.55.0", + "eslint-config-next": "14.0.4", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "typescript": "^5.3.0" + } +} diff --git a/examples/swap-board-ml-ml/postcss.config.js b/examples/swap-board-ml-ml/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/examples/swap-board-ml-ml/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/examples/swap-board-ml-ml/prisma/schema.prisma b/examples/swap-board-ml-ml/prisma/schema.prisma new file mode 100644 index 0000000..4dde81c --- /dev/null +++ b/examples/swap-board-ml-ml/prisma/schema.prisma @@ -0,0 +1,40 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +model Offer { + id Int @id @default(autoincrement()) + direction String // "tokenA->tokenB" + tokenA String + tokenB String + amountA String + amountB String + price Float + creatorMLAddress String + contact String? + status String @default("open") // open, taken, completed, cancelled + createdAt DateTime @default(now()) + + swaps Swap[] +} + +model Swap { + id Int @id @default(autoincrement()) + offerId Int + takerMLAddress String + status String @default("pending") // pending, htlc_created, in_progress, completed, refunded + secretHash String? + secret String? + claimTxHash String? + createdAt DateTime @default(now()) + + offer Offer @relation(fields: [offerId], references: [id]) +} diff --git a/examples/swap-board-ml-ml/src/app/api/offers/route.ts b/examples/swap-board-ml-ml/src/app/api/offers/route.ts new file mode 100644 index 0000000..56eac3f --- /dev/null +++ b/examples/swap-board-ml-ml/src/app/api/offers/route.ts @@ -0,0 +1,63 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { CreateOfferRequest } from '@/types/swap' + +export async function GET() { + try { + const offers = await prisma.offer.findMany({ + where: { + status: 'open' + }, + orderBy: { + createdAt: 'desc' + } + }) + + return NextResponse.json(offers) + } catch (error) { + console.error('Error fetching offers:', error) + return NextResponse.json( + { error: 'Failed to fetch offers' }, + { status: 500 } + ) + } +} + +export async function POST(request: NextRequest) { + try { + const body: CreateOfferRequest = await request.json() + + // Validate required fields + if (!body.tokenA || !body.tokenB || !body.amountA || !body.amountB || !body.creatorMLAddress) { + return NextResponse.json( + { error: 'Missing required fields' }, + { status: 400 } + ) + } + + // Calculate price (amountB / amountA) + const price = parseFloat(body.amountB) / parseFloat(body.amountA) + + const offer = await prisma.offer.create({ + data: { + direction: `${body.tokenA}->${body.tokenB}`, + tokenA: body.tokenA, + tokenB: body.tokenB, + amountA: body.amountA, + amountB: body.amountB, + price: price, + creatorMLAddress: body.creatorMLAddress, + contact: body.contact || null, + status: 'open' + } + }) + + return NextResponse.json(offer, { status: 201 }) + } catch (error) { + console.error('Error creating offer:', error) + return NextResponse.json( + { error: 'Failed to create offer' }, + { status: 500 } + ) + } +} diff --git a/examples/swap-board-ml-ml/src/app/api/swaps/[id]/route.ts b/examples/swap-board-ml-ml/src/app/api/swaps/[id]/route.ts new file mode 100644 index 0000000..6534a7f --- /dev/null +++ b/examples/swap-board-ml-ml/src/app/api/swaps/[id]/route.ts @@ -0,0 +1,101 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { UpdateSwapRequest } from '@/types/swap' + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const swapId = parseInt(params.id) + + if (isNaN(swapId)) { + return NextResponse.json( + { error: 'Invalid swap ID' }, + { status: 400 } + ) + } + + const swap = await prisma.swap.findUnique({ + where: { id: swapId }, + include: { + offer: true + } + }) + + if (!swap) { + return NextResponse.json( + { error: 'Swap not found' }, + { status: 404 } + ) + } + + return NextResponse.json(swap) + } catch (error) { + console.error('Error fetching swap:', error) + return NextResponse.json( + { error: 'Failed to fetch swap' }, + { status: 500 } + ) + } +} + +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const swapId = parseInt(params.id) + const body: UpdateSwapRequest = await request.json() + + if (isNaN(swapId)) { + return NextResponse.json( + { error: 'Invalid swap ID' }, + { status: 400 } + ) + } + + // Check if swap exists + const existingSwap = await prisma.swap.findUnique({ + where: { id: swapId } + }) + + if (!existingSwap) { + return NextResponse.json( + { error: 'Swap not found' }, + { status: 404 } + ) + } + + // Update swap with provided fields + const updateData: any = {} + if (body.status) updateData.status = body.status + if (body.secretHash) updateData.secretHash = body.secretHash + if (body.secret) updateData.secret = body.secret + if (body.claimTxHash) updateData.claimTxHash = body.claimTxHash + + const updatedSwap = await prisma.swap.update({ + where: { id: swapId }, + data: updateData, + include: { + offer: true + } + }) + + // If swap is completed, update offer status + if (body.status === 'completed') { + await prisma.offer.update({ + where: { id: updatedSwap.offerId }, + data: { status: 'completed' } + }) + } + + return NextResponse.json(updatedSwap) + } catch (error) { + console.error('Error updating swap:', error) + return NextResponse.json( + { error: 'Failed to update swap' }, + { status: 500 } + ) + } +} diff --git a/examples/swap-board-ml-ml/src/app/api/swaps/route.ts b/examples/swap-board-ml-ml/src/app/api/swaps/route.ts new file mode 100644 index 0000000..6ace226 --- /dev/null +++ b/examples/swap-board-ml-ml/src/app/api/swaps/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { AcceptOfferRequest } from '@/types/swap' + +export async function POST(request: NextRequest) { + try { + const body: AcceptOfferRequest = await request.json() + + // Validate required fields + if (!body.offerId || !body.takerMLAddress) { + return NextResponse.json( + { error: 'Missing required fields' }, + { status: 400 } + ) + } + + // Check if offer exists and is open + const offer = await prisma.offer.findUnique({ + where: { id: body.offerId } + }) + + if (!offer) { + return NextResponse.json( + { error: 'Offer not found' }, + { status: 404 } + ) + } + + if (offer.status !== 'open') { + return NextResponse.json( + { error: 'Offer is no longer available' }, + { status: 400 } + ) + } + + // Create swap and update offer status + const [swap] = await prisma.$transaction([ + prisma.swap.create({ + data: { + offerId: body.offerId, + takerMLAddress: body.takerMLAddress, + status: 'pending' + }, + include: { + offer: true + } + }), + prisma.offer.update({ + where: { id: body.offerId }, + data: { status: 'taken' } + }) + ]) + + return NextResponse.json(swap, { status: 201 }) + } catch (error) { + console.error('Error accepting offer:', error) + return NextResponse.json( + { error: 'Failed to accept offer' }, + { status: 500 } + ) + } +} diff --git a/examples/swap-board-ml-ml/src/app/create/page.tsx b/examples/swap-board-ml-ml/src/app/create/page.tsx new file mode 100644 index 0000000..d1c625a --- /dev/null +++ b/examples/swap-board-ml-ml/src/app/create/page.tsx @@ -0,0 +1,245 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Client } from '@mintlayer/sdk' + +export default function CreateOfferPage() { + const [formData, setFormData] = useState({ + tokenA: '', + tokenB: '', + amountA: '', + amountB: '', + contact: '' + }) + const [loading, setLoading] = useState(false) + const [userAddress, setUserAddress] = useState('') + const [client, setClient] = useState(null) + + useEffect(() => { + initializeClient() + }, []) + + const initializeClient = async () => { + try { + const network = (process.env.NEXT_PUBLIC_MINTLAYER_NETWORK as 'testnet' | 'mainnet') || 'testnet' + const newClient = await Client.create({ network }) + setClient(newClient) + } catch (error) { + console.error('Error initializing client:', error) + } + } + + const connectWallet = async () => { + try { + if (client) { + const connect = await client.connect() + const address = connect.testnet.receiving[0] + setUserAddress(address) + } + } catch (error) { + console.error('Error connecting wallet:', error) + alert('Failed to connect wallet. Please make sure Mojito wallet is installed.') + } + } + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData(prev => ({ + ...prev, + [name]: value + })) + } + + const calculatePrice = () => { + if (formData.amountA && formData.amountB) { + const price = parseFloat(formData.amountB) / parseFloat(formData.amountA) + return price.toFixed(6) + } + return '0' + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!userAddress) { + alert('Please connect your wallet first') + return + } + + if (!formData.tokenA || !formData.tokenB || !formData.amountA || !formData.amountB) { + alert('Please fill in all required fields') + return + } + + setLoading(true) + try { + const response = await fetch('/api/offers', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...formData, + creatorMLAddress: userAddress, + }), + }) + + if (response.ok) { + alert('Offer created successfully!') + // Reset form + setFormData({ + tokenA: '', + tokenB: '', + amountA: '', + amountB: '', + contact: '' + }) + // Redirect to offers page + window.location.href = '/offers' + } else { + const error = await response.json() + alert(`Error: ${error.error}`) + } + } catch (error) { + console.error('Error creating offer:', error) + alert('Failed to create offer') + } finally { + setLoading(false) + } + } + + return ( +
+

Create Swap Offer

+ +
+
+
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ + {formData.amountA && formData.amountB && ( +
+

+ Exchange Rate: 1 {formData.tokenA || 'TokenA'} = {calculatePrice()} {formData.tokenB || 'TokenB'} +

+
+ )} + +
+ + +
+ +
+

+ Your Mintlayer Address: +

+ {userAddress ? ( +

+ {userAddress} +

+ ) : ( +
+

Not connected

+ +
+ )} +
+ + +
+
+
+ ) +} diff --git a/examples/swap-board-ml-ml/src/app/globals.css b/examples/swap-board-ml-ml/src/app/globals.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/examples/swap-board-ml-ml/src/app/globals.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/examples/swap-board-ml-ml/src/app/layout.tsx b/examples/swap-board-ml-ml/src/app/layout.tsx new file mode 100644 index 0000000..e7fa70f --- /dev/null +++ b/examples/swap-board-ml-ml/src/app/layout.tsx @@ -0,0 +1,47 @@ +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import './globals.css' + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'Mintlayer P2P Swap Board', + description: 'Peer-to-peer token swaps on Mintlayer using HTLC atomic swaps', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + +
+ +
+ {children} +
+
+ + + ) +} diff --git a/examples/swap-board-ml-ml/src/app/offers/page.tsx b/examples/swap-board-ml-ml/src/app/offers/page.tsx new file mode 100644 index 0000000..93de217 --- /dev/null +++ b/examples/swap-board-ml-ml/src/app/offers/page.tsx @@ -0,0 +1,178 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Client } from '@mintlayer/sdk' +import { Offer } from '@/types/swap' + +export default function OffersPage() { + const [offers, setOffers] = useState([]) + const [loading, setLoading] = useState(true) + const [accepting, setAccepting] = useState(null) + const [client, setClient] = useState(null) + const [userAddress, setUserAddress] = useState('') + + useEffect(() => { + fetchOffers() + initializeClient() + }, []) + + const initializeClient = async () => { + try { + const network = (process.env.NEXT_PUBLIC_MINTLAYER_NETWORK as 'testnet' | 'mainnet') || 'testnet' + const newClient = await Client.create({ network }) + setClient(newClient) + } catch (error) { + console.error('Error initializing client:', error) + } + } + + const fetchOffers = async () => { + try { + const response = await fetch('/api/offers') + const data = await response.json() + setOffers(data) + } catch (error) { + console.error('Error fetching offers:', error) + } finally { + setLoading(false) + } + } + + const connectWallet = async () => { + try { + if (client) { + const connect = await client.connect() + const address = connect.testnet.receiving[0] + setUserAddress(address) + } + } catch (error) { + console.error('Error connecting wallet:', error) + alert('Failed to connect wallet. Please make sure Mojito wallet is installed.') + } + } + + const acceptOffer = async (offerId: number) => { + if (!userAddress) { + alert('Please connect your wallet first') + return + } + + setAccepting(offerId) + try { + const response = await fetch('/api/swaps', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + offerId, + takerMLAddress: userAddress, + }), + }) + + if (response.ok) { + const swap = await response.json() + // Redirect to swap page + window.location.href = `/swap/${swap.id}` + } else { + const error = await response.json() + alert(`Error: ${error.error}`) + } + } catch (error) { + console.error('Error accepting offer:', error) + alert('Failed to accept offer') + } finally { + setAccepting(null) + } + } + + if (loading) { + return ( +
+
Loading offers...
+
+ ) + } + + return ( +
+
+

Available Offers

+
+ {userAddress ? ( +
+ Connected: {userAddress.slice(0, 10)}... +
+ ) : ( + + )} +
+
+ + {offers.length === 0 ? ( +
+

No offers available at the moment.

+ + Create the first offer + +
+ ) : ( +
+ {offers.map((offer) => ( +
+
+
+
+ + {offer.amountA} {offer.tokenA.substring(0, 5)} + + + + + + {offer.amountB} {offer.tokenB.substring(0, 5)} + +
+
+ Price: {offer.price.toFixed(6)} {offer.tokenB.substring(0, 5)}/{offer.tokenA.substring(0, 5)} +
+
+ Creator: {offer.creatorMLAddress.slice(0, 20)}... +
+ {offer.contact && ( +
+ Contact: {offer.contact} +
+ )} +
+ Created: {new Date(offer.createdAt).toLocaleString()} +
+
+
+ + {offer.creatorMLAddress === userAddress && ( +
Your offer
+ )} +
+
+
+ ))} +
+ )} +
+ ) +} diff --git a/examples/swap-board-ml-ml/src/app/page.tsx b/examples/swap-board-ml-ml/src/app/page.tsx new file mode 100644 index 0000000..a796c90 --- /dev/null +++ b/examples/swap-board-ml-ml/src/app/page.tsx @@ -0,0 +1,59 @@ +import Link from 'next/link' + +export default function Home() { + return ( +
+
+

+ Mintlayer P2P Swap Board +

+

+ Trade Mintlayer tokens directly with other users using secure HTLC atomic swaps +

+ +
+ +
+
+ + + +
+

Browse Offers

+

+ View available token swap offers from other users and accept the ones that interest you. +

+
+ + + +
+
+ + + +
+

Create Offer

+

+ Post your own token swap offer and wait for other users to accept it. +

+
+ +
+ +
+

How it works

+
+
    +
  1. Create or browse token swap offers
  2. +
  3. Accept an offer to initiate an atomic swap
  4. +
  5. Both parties create Hash Time Locked Contracts (HTLCs)
  6. +
  7. Exchange secrets to claim tokens securely
  8. +
  9. Manual refund available if swap fails after timelock expires
  10. +
+
+
+
+
+ ) +} diff --git a/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx b/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx new file mode 100644 index 0000000..6bc7833 --- /dev/null +++ b/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx @@ -0,0 +1,403 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Client } from '@mintlayer/sdk' +import { Swap } from '@/types/swap' + +export default function SwapPage({ params }: { params: { id: string } }) { + const [swap, setSwap] = useState(null) + const [loading, setLoading] = useState(true) + const [userAddress, setUserAddress] = useState('') + const [client, setClient] = useState(null) + const [secretHash, setSecretHash] = useState(null) + const [generatingSecret, setGeneratingSecret] = useState(false) + const [creatingHtlc, setCreatingHtlc] = useState(false) + + useEffect(() => { + fetchSwap() + initializeClient() + + // Poll for updates every 10 seconds + const interval = setInterval(fetchSwap, 10000) + return () => clearInterval(interval) + }, [params.id]) + + const initializeClient = async () => { + try { + const network = (process.env.NEXT_PUBLIC_MINTLAYER_NETWORK as 'testnet' | 'mainnet') || 'testnet' + const newClient = await Client.create({ network }) + setClient(newClient) + } catch (error) { + console.error('Error initializing client:', error) + } + } + + const fetchSwap = async () => { + try { + const response = await fetch(`/api/swaps/${params.id}`) + if (response.ok) { + const data = await response.json() + setSwap(data) + } + } catch (error) { + console.error('Error fetching swap:', error) + } finally { + setLoading(false) + } + } + + const connectWallet = async () => { + try { + if (client) { + const connect = await client.connect() + const address = connect.testnet.receiving[0] + setUserAddress(address) + } + } catch (error) { + console.error('Error connecting wallet:', error) + alert('Failed to connect wallet. Please make sure Mojito wallet is installed.') + } + } + + const generateSecretHash = async () => { + if (!client) { + alert('Please connect your wallet first') + return + } + + setGeneratingSecret(true) + try { + const secretHashResponse = await client.requestSecretHash({}) + setSecretHash(secretHashResponse) + console.log('Generated secret hash:', secretHashResponse) + } catch (error) { + console.error('Error generating secret hash:', error) + alert('Failed to generate secret hash. Please try again.') + } finally { + setGeneratingSecret(false) + } + } + + const createHtlc = async () => { + if (!client || !userAddress || !secretHash || !swap?.offer) { + alert('Missing required data for HTLC creation') + return + } + + setCreatingHtlc(true) + try { + const htlcParams = { + amount: swap.offer.amountA, + token_id: swap.offer.tokenA === 'ML' ? null : swap.offer.tokenA, + secret_hash: { hex: secretHash.secret_hash_hex }, + spend_address: swap.takerMLAddress, // Taker can spend with secret + refund_address: userAddress, // Creator can refund after timelock + refund_timelock: { + type: 'ForBlockCount', + content: 144 // ~24 hours assuming 10min blocks + } + } + + const signedTx = await client.createHtlc(htlcParams) + console.log('HTLC created:', signedTx) + + // Update swap status + await fetch(`/api/swaps/${swap.id}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + status: 'htlc_created', + secretHash: JSON.stringify(secretHash) + }) + }) + + // Refresh swap data + fetchSwap() + alert('HTLC created successfully!') + } catch (error) { + console.error('Error creating HTLC:', error) + alert('Failed to create HTLC. Please try again.') + } finally { + setCreatingHtlc(false) + } + } + + const getStatusColor = (status: string) => { + switch (status) { + case 'pending': return 'text-yellow-600 bg-yellow-100' + case 'htlc_created': return 'text-blue-600 bg-blue-100' + case 'in_progress': return 'text-purple-600 bg-purple-100' + case 'completed': return 'text-green-600 bg-green-100' + case 'refunded': return 'text-red-600 bg-red-100' + default: return 'text-gray-600 bg-gray-100' + } + } + + const getStatusDescription = (status: string) => { + switch (status) { + case 'pending': return 'Waiting for HTLC creation' + case 'htlc_created': return 'HTLC created, waiting for counterparty' + case 'in_progress': return 'Both HTLCs created, ready to claim' + case 'completed': return 'Swap completed successfully' + case 'refunded': return 'Swap was refunded' + default: return 'Unknown status' + } + } + + const isUserCreator = swap?.offer?.creatorMLAddress === userAddress + const isUserTaker = swap?.takerMLAddress === userAddress + + if (loading) { + return ( +
+
Loading swap details...
+
+ ) + } + + if (!swap) { + return ( +
+
+

Swap Not Found

+ + Back to offers + +
+
+ ) + } + + return ( +
+
+
+

Swap #{swap.id}

+ {!userAddress && ( + + )} +
+
+ + {swap.status.toUpperCase()} + + {getStatusDescription(swap.status)} + {userAddress && ( + + Connected: {userAddress.slice(0, 10)}... + + )} +
+
+ +
+ {/* Swap Details */} +
+

Swap Details

+ +
+
+
+
+ {swap.offer?.amountA} {swap.offer?.tokenA.substring(0, 5)} +
+
From Creator
+
+ + + +
+
+ {swap.offer?.amountB} {swap.offer?.tokenB.substring(0, 5)} +
+
To Taker
+
+
+ +
+
+
+ Creator: +
+ {swap.offer?.creatorMLAddress} +
+
+
+ Taker: +
+ {swap.takerMLAddress} +
+
+
+
+ +
+
+
Created: {new Date(swap.createdAt).toLocaleString()}
+ {swap.offer?.contact && ( +
Contact: {swap.offer.contact}
+ )} +
+
+
+
+ + {/* Progress Steps */} +
+

Progress

+ +
+
+
+ Offer accepted +
+ +
+
+ HTLC created +
+ +
+
+ Counterparty HTLC created +
+ +
+
+ Tokens claimed +
+
+
+
+ + {/* Action Section */} +
+

Next Steps

+ + {swap.status === 'pending' && ( +
+

+ {isUserCreator + ? "You need to create the initial HTLC to start the swap process." + : "Waiting for the creator to create the initial HTLC." + } +

+ {isUserCreator && ( +
+ {!secretHash ? ( +
+

+ Step 1: Generate a secret hash for the HTLC +

+ +
+ ) : ( +
+

+ ✅ Secret hash generated successfully +

+
+ {JSON.stringify(secretHash, null, 2)} +
+

+ Step 2: Create the HTLC contract +

+ +
+ )} +
+ )} +
+ )} + + {swap.status === 'htlc_created' && ( +
+

+ {isUserTaker + ? "The creator has created their HTLC. You need to create your counterparty HTLC." + : "You've created your HTLC. Waiting for the taker to create their counterparty HTLC." + } +

+ {isUserTaker && ( + + )} +
+ )} + + {swap.status === 'in_progress' && ( +
+

+ Both HTLCs are created. You can now claim your tokens by revealing the secret. +

+ +
+ )} + + {swap.status === 'completed' && ( +
+

+ 🎉 Swap completed successfully! Both parties have received their tokens. +

+ {swap.claimTxHash && ( +

+ Transaction: {swap.claimTxHash} +

+ )} +
+ )} + + {swap.status === 'refunded' && ( +
+

+ The swap was manually refunded after the timelock expired. +

+
+ )} + + {(swap.status === 'htlc_created' || swap.status === 'in_progress') && ( +
+

+ Timelock Protection: If the swap is not completed within the timelock period, + you can manually refund your HTLC to recover your tokens. +

+ +
+ )} +
+
+ ) +} diff --git a/examples/swap-board-ml-ml/src/lib/htlc-utils.ts b/examples/swap-board-ml-ml/src/lib/htlc-utils.ts new file mode 100644 index 0000000..8a5b0c0 --- /dev/null +++ b/examples/swap-board-ml-ml/src/lib/htlc-utils.ts @@ -0,0 +1,90 @@ +import { Client } from '@mintlayer/sdk' + +export interface HTLCParams { + amount: string + token_id?: string | null + secret_hash: any + spend_address: string + refund_address: string + refund_timelock: { + type: 'ForBlockCount' | 'UntilTime' + content: number | string + } +} + +export interface SecretHashResponse { + secret: string + secret_hash: { + hex: string + string?: string | null + } +} + +/** + * Generate a secret hash using the wallet + */ +export async function generateSecretHash(client: Client): Promise { + return await client.requestSecretHash({}) +} + +/** + * Create an HTLC with the given parameters + */ +export async function createHTLC(client: Client, params: HTLCParams): Promise { + return await client.createHtlc(params) +} + +/** + * Build HTLC parameters for a swap offer + */ +export function buildHTLCParams( + offer: any, + secretHash: any, + spendAddress: string, + refundAddress: string, + timelockBlocks: number = 144 // ~24 hours +): HTLCParams { + return { + amount: offer.amountA, + token_id: offer.tokenA === 'ML' ? null : offer.tokenA, + secret_hash: { hex: secretHash.secret_hash_hex }, + spend_address: spendAddress, + refund_address: refundAddress, + refund_timelock: { + type: 'ForBlockCount', + content: timelockBlocks + } + } +} + +/** + * Extract secret from a completed HTLC transaction + */ +export async function extractHTLCSecret( + client: Client, + transactionId: string, + transactionHex: string +): Promise { + return await client.extractHtlcSecret({ + transaction_id: transactionId, + transaction_hex: transactionHex, + format: 'hex' + }) +} + +/** + * Get timelock expiry information + */ +export function getTimelockInfo(blocks: number): { + estimatedHours: number + estimatedExpiry: Date +} { + const estimatedMinutes = blocks * 10 // Assuming 10min blocks + const estimatedHours = estimatedMinutes / 60 + const estimatedExpiry = new Date(Date.now() + estimatedMinutes * 60 * 1000) + + return { + estimatedHours, + estimatedExpiry + } +} diff --git a/examples/swap-board-ml-ml/src/lib/prisma.ts b/examples/swap-board-ml-ml/src/lib/prisma.ts new file mode 100644 index 0000000..af2a01e --- /dev/null +++ b/examples/swap-board-ml-ml/src/lib/prisma.ts @@ -0,0 +1,9 @@ +import { PrismaClient } from '@prisma/client' + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined +} + +export const prisma = globalForPrisma.prisma ?? new PrismaClient() + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma diff --git a/examples/swap-board-ml-ml/src/types/swap.ts b/examples/swap-board-ml-ml/src/types/swap.ts new file mode 100644 index 0000000..0d99497 --- /dev/null +++ b/examples/swap-board-ml-ml/src/types/swap.ts @@ -0,0 +1,46 @@ +export interface Offer { + id: number + direction: string + tokenA: string + tokenB: string + amountA: string + amountB: string + price: number + creatorMLAddress: string + contact?: string + status: 'open' | 'taken' | 'completed' | 'cancelled' + createdAt: Date +} + +export interface Swap { + id: number + offerId: number + takerMLAddress: string + status: 'pending' | 'htlc_created' | 'in_progress' | 'completed' | 'refunded' + secretHash?: string + secret?: string + claimTxHash?: string + createdAt: Date + offer?: Offer +} + +export interface CreateOfferRequest { + tokenA: string + tokenB: string + amountA: string + amountB: string + creatorMLAddress: string + contact?: string +} + +export interface AcceptOfferRequest { + offerId: number + takerMLAddress: string +} + +export interface UpdateSwapRequest { + status?: Swap['status'] + secretHash?: string + secret?: string + claimTxHash?: string +} diff --git a/examples/swap-board-ml-ml/tailwind.config.js b/examples/swap-board-ml-ml/tailwind.config.js new file mode 100644 index 0000000..3660a2b --- /dev/null +++ b/examples/swap-board-ml-ml/tailwind.config.js @@ -0,0 +1,27 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + colors: { + mintlayer: { + 50: '#f0f9ff', + 100: '#e0f2fe', + 200: '#bae6fd', + 300: '#7dd3fc', + 400: '#38bdf8', + 500: '#0ea5e9', + 600: '#0284c7', + 700: '#0369a1', + 800: '#075985', + 900: '#0c4a6e', + }, + }, + }, + }, + plugins: [], +} diff --git a/examples/swap-board-ml-ml/tsconfig.json b/examples/swap-board-ml-ml/tsconfig.json new file mode 100644 index 0000000..abb59dc --- /dev/null +++ b/examples/swap-board-ml-ml/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "es6"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} From e81d0a9c8a1b44cac0b37e839e976c2c61c7342a Mon Sep 17 00:00:00 2001 From: Sergey Chystiakov Date: Sun, 6 Jul 2025 02:07:37 +0200 Subject: [PATCH 02/13] counterparty htlc creation --- .../src/app/swap/[id]/page.tsx | 73 ++++++++++++++++++- .../swap-board-ml-ml/src/lib/htlc-utils.ts | 25 ++++++- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx b/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx index 6bc7833..82293ee 100644 --- a/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx +++ b/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx @@ -12,6 +12,7 @@ export default function SwapPage({ params }: { params: { id: string } }) { const [secretHash, setSecretHash] = useState(null) const [generatingSecret, setGeneratingSecret] = useState(false) const [creatingHtlc, setCreatingHtlc] = useState(false) + const [creatingCounterpartyHtlc, setCreatingCounterpartyHtlc] = useState(false) useEffect(() => { fetchSwap() @@ -122,6 +123,52 @@ export default function SwapPage({ params }: { params: { id: string } }) { } } + const createCounterpartyHtlc = async () => { + if (!client || !userAddress || !swap?.offer || !swap.secretHash) { + alert('Missing required data for counterparty HTLC creation') + return + } + + setCreatingCounterpartyHtlc(true) + try { + // Parse the stored secret hash from the creator's HTLC + const creatorSecretHash = JSON.parse(swap.secretHash) + + const htlcParams = { + amount: swap.offer.amountB, // Taker gives amountB + token_id: swap.offer.tokenB === 'ML' ? null : swap.offer.tokenB, + secret_hash: { hex: creatorSecretHash.secret_hash_hex }, // Use same secret hash + spend_address: swap.offer.creatorMLAddress, // Creator can spend with secret + refund_address: userAddress, // Taker can refund after timelock + refund_timelock: { + type: 'ForBlockCount', + content: 144 // ~24 hours assuming 10min blocks + } + } + + const signedTx = await client.createHtlc(htlcParams) + console.log('Counterparty HTLC created:', signedTx) + + // Update swap status + await fetch(`/api/swaps/${swap.id}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + status: 'in_progress' + }) + }) + + // Refresh swap data + fetchSwap() + alert('Counterparty HTLC created successfully!') + } catch (error) { + console.error('Error creating counterparty HTLC:', error) + alert('Failed to create counterparty HTLC. Please try again.') + } finally { + setCreatingCounterpartyHtlc(false) + } + } + const getStatusColor = (status: string) => { switch (status) { case 'pending': return 'text-yellow-600 bg-yellow-100' @@ -340,16 +387,34 @@ export default function SwapPage({ params }: { params: { id: string } }) { {swap.status === 'htlc_created' && (
-

+

{isUserTaker ? "The creator has created their HTLC. You need to create your counterparty HTLC." : "You've created your HTLC. Waiting for the taker to create their counterparty HTLC." }

{isUserTaker && ( - +
+
+

Creator's HTLC Details:

+
+
Amount: {swap.offer?.amountA} {swap.offer?.tokenA}
+
Secret Hash: {swap.secretHash ? JSON.parse(swap.secretHash).secret_hash_hex.slice(0, 20) + '...' : 'N/A'}
+
+
+
+

+ Create your counterparty HTLC with {swap.offer?.amountB} {swap.offer?.tokenB} +

+ +
+
)}
)} diff --git a/examples/swap-board-ml-ml/src/lib/htlc-utils.ts b/examples/swap-board-ml-ml/src/lib/htlc-utils.ts index 8a5b0c0..7aa5c4b 100644 --- a/examples/swap-board-ml-ml/src/lib/htlc-utils.ts +++ b/examples/swap-board-ml-ml/src/lib/htlc-utils.ts @@ -35,7 +35,7 @@ export async function createHTLC(client: Client, params: HTLCParams): Promise Date: Sun, 6 Jul 2025 02:22:20 +0200 Subject: [PATCH 03/13] add token tickers --- .../swap-board-ml-ml/prisma/schema.prisma | 24 ++-- .../src/app/api/swaps/[id]/route.ts | 4 + .../swap-board-ml-ml/src/app/create/page.tsx | 118 ++++++++++++++---- .../swap-board-ml-ml/src/app/offers/page.tsx | 39 +++++- .../src/app/swap/[id]/page.tsx | 77 ++++++++++-- .../swap-board-ml-ml/src/lib/htlc-utils.ts | 43 +++++-- examples/swap-board-ml-ml/src/types/swap.ts | 8 ++ 7 files changed, 260 insertions(+), 53 deletions(-) diff --git a/examples/swap-board-ml-ml/prisma/schema.prisma b/examples/swap-board-ml-ml/prisma/schema.prisma index 4dde81c..a767c52 100644 --- a/examples/swap-board-ml-ml/prisma/schema.prisma +++ b/examples/swap-board-ml-ml/prisma/schema.prisma @@ -22,19 +22,23 @@ model Offer { contact String? status String @default("open") // open, taken, completed, cancelled createdAt DateTime @default(now()) - + swaps Swap[] } model Swap { - id Int @id @default(autoincrement()) - offerId Int - takerMLAddress String - status String @default("pending") // pending, htlc_created, in_progress, completed, refunded - secretHash String? - secret String? - claimTxHash String? - createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + offerId Int + takerMLAddress String + status String @default("pending") // pending, htlc_created, in_progress, completed, refunded + secretHash String? + secret String? + creatorHtlcTxHash String? // Creator's HTLC transaction ID + creatorHtlcTxHex String? // Creator's signed HTLC transaction hex + takerHtlcTxHash String? // Taker's HTLC transaction ID + takerHtlcTxHex String? // Taker's signed HTLC transaction hex + claimTxHash String? // Final claim transaction ID + createdAt DateTime @default(now()) - offer Offer @relation(fields: [offerId], references: [id]) + offer Offer @relation(fields: [offerId], references: [id]) } diff --git a/examples/swap-board-ml-ml/src/app/api/swaps/[id]/route.ts b/examples/swap-board-ml-ml/src/app/api/swaps/[id]/route.ts index 6534a7f..b3fb6e3 100644 --- a/examples/swap-board-ml-ml/src/app/api/swaps/[id]/route.ts +++ b/examples/swap-board-ml-ml/src/app/api/swaps/[id]/route.ts @@ -72,6 +72,10 @@ export async function POST( if (body.status) updateData.status = body.status if (body.secretHash) updateData.secretHash = body.secretHash if (body.secret) updateData.secret = body.secret + if (body.creatorHtlcTxHash) updateData.creatorHtlcTxHash = body.creatorHtlcTxHash + if (body.creatorHtlcTxHex) updateData.creatorHtlcTxHex = body.creatorHtlcTxHex + if (body.takerHtlcTxHash) updateData.takerHtlcTxHash = body.takerHtlcTxHash + if (body.takerHtlcTxHex) updateData.takerHtlcTxHex = body.takerHtlcTxHex if (body.claimTxHash) updateData.claimTxHash = body.claimTxHash const updatedSwap = await prisma.swap.update({ diff --git a/examples/swap-board-ml-ml/src/app/create/page.tsx b/examples/swap-board-ml-ml/src/app/create/page.tsx index d1c625a..673bbe8 100644 --- a/examples/swap-board-ml-ml/src/app/create/page.tsx +++ b/examples/swap-board-ml-ml/src/app/create/page.tsx @@ -3,6 +3,12 @@ import { useState, useEffect } from 'react' import { Client } from '@mintlayer/sdk' +interface Token { + token_id: string + symbol: string + number_of_decimals: number +} + export default function CreateOfferPage() { const [formData, setFormData] = useState({ tokenA: '', @@ -14,9 +20,12 @@ export default function CreateOfferPage() { const [loading, setLoading] = useState(false) const [userAddress, setUserAddress] = useState('') const [client, setClient] = useState(null) + const [tokens, setTokens] = useState([]) + const [loadingTokens, setLoadingTokens] = useState(true) useEffect(() => { initializeClient() + fetchTokens() }, []) const initializeClient = async () => { @@ -29,6 +38,29 @@ export default function CreateOfferPage() { } } + const fetchTokens = async () => { + try { + setLoadingTokens(true) + const network = (process.env.NEXT_PUBLIC_MINTLAYER_NETWORK as 'testnet' | 'mainnet') || 'testnet' + const networkId = network === 'mainnet' ? 0 : 1 + const response = await fetch(`https://api.mintini.app/dex_tokens?network=${networkId}`) + + if (response.ok) { + const tokenData = await response.json() + // Add native ML token at the beginning + const allTokens = [ + { token_id: 'ML', symbol: 'ML', number_of_decimals: 11 }, + ...tokenData + ] + setTokens(allTokens) + } + } catch (error) { + console.error('Error fetching tokens:', error) + } finally { + setLoadingTokens(false) + } + } + const connectWallet = async () => { try { if (client) { @@ -42,7 +74,7 @@ export default function CreateOfferPage() { } } - const handleInputChange = (e: React.ChangeEvent) => { + const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target setFormData(prev => ({ ...prev, @@ -50,6 +82,18 @@ export default function CreateOfferPage() { })) } + const getTokenDisplay = (tokenId: string) => { + if (tokenId === 'ML') return 'ML (Native)' + const token = tokens.find(t => t.token_id === tokenId) + if (!token) return tokenId + const shortId = token.token_id.slice(-8) // Last 8 characters + return `${token.symbol} (...${shortId})` + } + + const getSelectedToken = (tokenId: string) => { + return tokens.find(t => t.token_id === tokenId) + } + const calculatePrice = () => { if (formData.amountA && formData.amountB) { const price = parseFloat(formData.amountB) / parseFloat(formData.amountA) @@ -119,16 +163,32 @@ export default function CreateOfferPage() { - + {loadingTokens ? ( +
+ Loading tokens... +
+ ) : ( + + )} + {formData.tokenA && getSelectedToken(formData.tokenA) && ( +
+ Decimals: {getSelectedToken(formData.tokenA)?.number_of_decimals} +
+ )}
@@ -155,16 +215,32 @@ export default function CreateOfferPage() { - + {loadingTokens ? ( +
+ Loading tokens... +
+ ) : ( + + )} + {formData.tokenB && getSelectedToken(formData.tokenB) && ( +
+ Decimals: {getSelectedToken(formData.tokenB)?.number_of_decimals} +
+ )}
diff --git a/examples/swap-board-ml-ml/src/app/offers/page.tsx b/examples/swap-board-ml-ml/src/app/offers/page.tsx index 93de217..cf18ca5 100644 --- a/examples/swap-board-ml-ml/src/app/offers/page.tsx +++ b/examples/swap-board-ml-ml/src/app/offers/page.tsx @@ -4,16 +4,24 @@ import { useState, useEffect } from 'react' import { Client } from '@mintlayer/sdk' import { Offer } from '@/types/swap' +interface Token { + token_id: string + symbol: string + number_of_decimals: number +} + export default function OffersPage() { const [offers, setOffers] = useState([]) const [loading, setLoading] = useState(true) const [accepting, setAccepting] = useState(null) const [client, setClient] = useState(null) const [userAddress, setUserAddress] = useState('') + const [tokens, setTokens] = useState([]) useEffect(() => { fetchOffers() initializeClient() + fetchTokens() }, []) const initializeClient = async () => { @@ -26,6 +34,25 @@ export default function OffersPage() { } } + const fetchTokens = async () => { + try { + const network = (process.env.NEXT_PUBLIC_MINTLAYER_NETWORK as 'testnet' | 'mainnet') || 'testnet' + const networkId = network === 'mainnet' ? 0 : 1 + const response = await fetch(`https://api.mintini.app/dex_tokens?network=${networkId}`) + + if (response.ok) { + const tokenData = await response.json() + const allTokens = [ + { token_id: 'ML', symbol: 'ML', number_of_decimals: 11 }, + ...tokenData + ] + setTokens(allTokens) + } + } catch (error) { + console.error('Error fetching tokens:', error) + } + } + const fetchOffers = async () => { try { const response = await fetch('/api/offers') @@ -51,6 +78,12 @@ export default function OffersPage() { } } + const getTokenSymbol = (tokenId: string) => { + if (tokenId === 'ML') return 'ML' + const token = tokens.find(t => t.token_id === tokenId) + return token ? token.symbol : tokenId.slice(-8) + } + const acceptOffer = async (offerId: number) => { if (!userAddress) { alert('Please connect your wallet first') @@ -132,17 +165,17 @@ export default function OffersPage() {
- {offer.amountA} {offer.tokenA.substring(0, 5)} + {offer.amountA} {getTokenSymbol(offer.tokenA)} - {offer.amountB} {offer.tokenB.substring(0, 5)} + {offer.amountB} {getTokenSymbol(offer.tokenB)}
- Price: {offer.price.toFixed(6)} {offer.tokenB.substring(0, 5)}/{offer.tokenA.substring(0, 5)} + Price: {offer.price.toFixed(6)} {getTokenSymbol(offer.tokenB)}/{getTokenSymbol(offer.tokenA)}
Creator: {offer.creatorMLAddress.slice(0, 20)}... diff --git a/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx b/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx index 82293ee..bc6cd2f 100644 --- a/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx +++ b/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx @@ -13,10 +13,12 @@ export default function SwapPage({ params }: { params: { id: string } }) { const [generatingSecret, setGeneratingSecret] = useState(false) const [creatingHtlc, setCreatingHtlc] = useState(false) const [creatingCounterpartyHtlc, setCreatingCounterpartyHtlc] = useState(false) + const [tokens, setTokens] = useState([]) useEffect(() => { fetchSwap() initializeClient() + fetchTokens() // Poll for updates every 10 seconds const interval = setInterval(fetchSwap, 10000) @@ -33,6 +35,31 @@ export default function SwapPage({ params }: { params: { id: string } }) { } } + const fetchTokens = async () => { + try { + const network = (process.env.NEXT_PUBLIC_MINTLAYER_NETWORK as 'testnet' | 'mainnet') || 'testnet' + const networkId = network === 'mainnet' ? 0 : 1 + const response = await fetch(`https://api.mintini.app/dex_tokens?network=${networkId}`) + + if (response.ok) { + const tokenData = await response.json() + const allTokens = [ + { token_id: 'ML', symbol: 'ML', number_of_decimals: 11 }, + ...tokenData + ] + setTokens(allTokens) + } + } catch (error) { + console.error('Error fetching tokens:', error) + } + } + + const getTokenSymbol = (tokenId: string) => { + if (tokenId === 'ML') return 'ML' + const token = tokens.find((t: any) => t.token_id === tokenId) + return token ? token.symbol : tokenId.slice(-8) + } + const fetchSwap = async () => { try { const response = await fetch(`/api/swaps/${params.id}`) @@ -87,6 +114,7 @@ export default function SwapPage({ params }: { params: { id: string } }) { setCreatingHtlc(true) try { + // Step 1: Build the HTLC transaction const htlcParams = { amount: swap.offer.amountA, token_id: swap.offer.tokenA === 'ML' ? null : swap.offer.tokenA, @@ -99,22 +127,33 @@ export default function SwapPage({ params }: { params: { id: string } }) { } } - const signedTx = await client.createHtlc(htlcParams) - console.log('HTLC created:', signedTx) + // Step 2: Sign the transaction + const signedTxHex = await client.createHtlc(htlcParams) + console.log('HTLC signed:', signedTxHex) + + // Step 3: Broadcast the transaction to the network + const broadcastResult = await client.broadcastTx(signedTxHex) + console.log('HTLC broadcast result:', broadcastResult) - // Update swap status + const txId = broadcastResult.tx_id || broadcastResult.transaction_id || broadcastResult.id + + // Step 4: Update swap status with transaction ID and hex + // Note: We save the signed transaction hex because it's needed later + // to extract the secret when someone claims the HTLC using extractHtlcSecret() await fetch(`/api/swaps/${swap.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'htlc_created', - secretHash: JSON.stringify(secretHash) + secretHash: JSON.stringify(secretHash), + creatorHtlcTxHash: txId, + creatorHtlcTxHex: signedTxHex }) }) // Refresh swap data fetchSwap() - alert('HTLC created successfully!') + alert(`HTLC created and broadcasted successfully! TX ID: ${txId}`) } catch (error) { console.error('Error creating HTLC:', error) alert('Failed to create HTLC. Please try again.') @@ -146,21 +185,32 @@ export default function SwapPage({ params }: { params: { id: string } }) { } } - const signedTx = await client.createHtlc(htlcParams) - console.log('Counterparty HTLC created:', signedTx) + // Step 2: Sign the transaction + const signedTxHex = await client.createHtlc(htlcParams) + console.log('Counterparty HTLC signed:', signedTxHex) + + // Step 3: Broadcast the transaction to the network + const broadcastResult = await client.broadcastTx(signedTxHex) + console.log('Counterparty HTLC broadcast result:', broadcastResult) + + const txId = broadcastResult.tx_id || broadcastResult.transaction_id || broadcastResult.id - // Update swap status + // Step 4: Update swap status with transaction ID and hex + // Note: We save the signed transaction hex because it's needed later + // to extract the secret when someone claims the HTLC using extractHtlcSecret() await fetch(`/api/swaps/${swap.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - status: 'in_progress' + status: 'in_progress', + takerHtlcTxHash: txId, + takerHtlcTxHex: signedTxHex }) }) // Refresh swap data fetchSwap() - alert('Counterparty HTLC created successfully!') + alert(`Counterparty HTLC created and broadcasted successfully! TX ID: ${txId}`) } catch (error) { console.error('Error creating counterparty HTLC:', error) alert('Failed to create counterparty HTLC. Please try again.') @@ -251,7 +301,7 @@ export default function SwapPage({ params }: { params: { id: string } }) {
- {swap.offer?.amountA} {swap.offer?.tokenA.substring(0, 5)} + {swap.offer?.amountA} {getTokenSymbol(swap.offer?.tokenA || '')}
From Creator
@@ -260,7 +310,7 @@ export default function SwapPage({ params }: { params: { id: string } }) {
- {swap.offer?.amountB} {swap.offer?.tokenB.substring(0, 5)} + {swap.offer?.amountB} {getTokenSymbol(swap.offer?.tokenB || '')}
To Taker
@@ -400,6 +450,9 @@ export default function SwapPage({ params }: { params: { id: string } }) {
Amount: {swap.offer?.amountA} {swap.offer?.tokenA}
Secret Hash: {swap.secretHash ? JSON.parse(swap.secretHash).secret_hash_hex.slice(0, 20) + '...' : 'N/A'}
+ {swap.creatorHtlcTxHash && ( +
TX ID: {swap.creatorHtlcTxHash.slice(0, 20)}...
+ )}
diff --git a/examples/swap-board-ml-ml/src/lib/htlc-utils.ts b/examples/swap-board-ml-ml/src/lib/htlc-utils.ts index 7aa5c4b..db8dfcf 100644 --- a/examples/swap-board-ml-ml/src/lib/htlc-utils.ts +++ b/examples/swap-board-ml-ml/src/lib/htlc-utils.ts @@ -14,6 +14,7 @@ export interface HTLCParams { export interface SecretHashResponse { secret: string + secret_hash_hex: string secret_hash: { hex: string string?: string | null @@ -28,10 +29,17 @@ export async function generateSecretHash(client: Client): Promise { - return await client.createHtlc(params) +export async function createHTLC(client: Client, params: HTLCParams): Promise<{ txHex: string, txId: string }> { + // Sign the transaction + const signedTxHex = await client.createHtlc(params) + + // Broadcast to network + const broadcastResult = await client.broadcastTx(signedTxHex) + const txId = broadcastResult.tx_id || broadcastResult.transaction_id || broadcastResult.id + + return { txHex: signedTxHex, txId } } /** @@ -82,19 +90,40 @@ export function buildCounterpartyHTLCParams( /** * Extract secret from a completed HTLC transaction + * @param client - Mintlayer client + * @param spendTransactionId - The transaction ID that spent the HTLC + * @param originalHtlcTxHex - The original signed HTLC transaction hex + * @returns The extracted secret in hex format */ export async function extractHTLCSecret( client: Client, - transactionId: string, - transactionHex: string + spendTransactionId: string, + originalHtlcTxHex: string ): Promise { return await client.extractHtlcSecret({ - transaction_id: transactionId, - transaction_hex: transactionHex, + transaction_id: spendTransactionId, + transaction_hex: originalHtlcTxHex, format: 'hex' }) } +/** + * Create and broadcast an HTLC, returning both transaction ID and hex + */ +export async function createAndBroadcastHTLC( + client: Client, + params: HTLCParams +): Promise<{ txId: string, txHex: string }> { + // Step 1: Sign the transaction + const signedTxHex = await client.createHtlc(params) + + // Step 2: Broadcast to network + const broadcastResult = await client.broadcastTx(signedTxHex) + const txId = broadcastResult.tx_id || broadcastResult.transaction_id || broadcastResult.id + + return { txId, txHex: signedTxHex } +} + /** * Get timelock expiry information */ diff --git a/examples/swap-board-ml-ml/src/types/swap.ts b/examples/swap-board-ml-ml/src/types/swap.ts index 0d99497..dc0ff01 100644 --- a/examples/swap-board-ml-ml/src/types/swap.ts +++ b/examples/swap-board-ml-ml/src/types/swap.ts @@ -19,6 +19,10 @@ export interface Swap { status: 'pending' | 'htlc_created' | 'in_progress' | 'completed' | 'refunded' secretHash?: string secret?: string + creatorHtlcTxHash?: string + creatorHtlcTxHex?: string + takerHtlcTxHash?: string + takerHtlcTxHex?: string claimTxHash?: string createdAt: Date offer?: Offer @@ -42,5 +46,9 @@ export interface UpdateSwapRequest { status?: Swap['status'] secretHash?: string secret?: string + creatorHtlcTxHash?: string + creatorHtlcTxHex?: string + takerHtlcTxHash?: string + takerHtlcTxHex?: string claimTxHash?: string } From 5b91f920f0951b8baec55004a59abcb1a3cf6485 Mon Sep 17 00:00:00 2001 From: Sergey Chystiakov Date: Sun, 6 Jul 2025 02:22:33 +0200 Subject: [PATCH 04/13] add token tickers --- .../src/app/swap/[id]/page.tsx | 4 +- pnpm-lock.yaml | 2800 ++++++++++++++++- 2 files changed, 2700 insertions(+), 104 deletions(-) diff --git a/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx b/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx index bc6cd2f..a4f0105 100644 --- a/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx +++ b/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx @@ -448,7 +448,7 @@ export default function SwapPage({ params }: { params: { id: string } }) {

Creator's HTLC Details:

-
Amount: {swap.offer?.amountA} {swap.offer?.tokenA}
+
Amount: {swap.offer?.amountA} {getTokenSymbol(swap.offer?.tokenA || '')}
Secret Hash: {swap.secretHash ? JSON.parse(swap.secretHash).secret_hash_hex.slice(0, 20) + '...' : 'N/A'}
{swap.creatorHtlcTxHash && (
TX ID: {swap.creatorHtlcTxHash.slice(0, 20)}...
@@ -457,7 +457,7 @@ export default function SwapPage({ params }: { params: { id: string } }) {

- Create your counterparty HTLC with {swap.offer?.amountB} {swap.offer?.tokenB} + Create your counterparty HTLC with {swap.offer?.amountB} {getTokenSymbol(swap.offer?.tokenB || '')}

+
+
+

Available to Claim:

+
+ {isUserCreator && ( +
+ You can claim: {swap.offer?.amountB} {getTokenSymbol(swap.offer?.tokenB || '')} from taker's HTLC +
+ ✓ Your wallet has the secret +
+ )} + {isUserTaker && ( +
+ You can claim: {swap.offer?.amountA} {getTokenSymbol(swap.offer?.tokenA || '')} from creator's HTLC +
+ ⚠ You need to provide the secret +
+ )} +
+
+ +
)} {swap.status === 'completed' && (
-

- 🎉 Swap completed successfully! Both parties have received their tokens. +

+ 🎉 Swap completed successfully!

{swap.claimTxHash && ( -

- Transaction: {swap.claimTxHash} -

+
+

+ Claim Transaction: {swap.claimTxHash} +

+ {swap.secret && ( +
+

Revealed Secret:

+

{swap.secret}

+
+ )} + {!swap.secret && ( + + )} +
)}
)} diff --git a/examples/swap-board-ml-ml/src/lib/htlc-utils.ts b/examples/swap-board-ml-ml/src/lib/htlc-utils.ts index db8dfcf..94818e6 100644 --- a/examples/swap-board-ml-ml/src/lib/htlc-utils.ts +++ b/examples/swap-board-ml-ml/src/lib/htlc-utils.ts @@ -89,20 +89,20 @@ export function buildCounterpartyHTLCParams( } /** - * Extract secret from a completed HTLC transaction + * Extract secret from a completed HTLC claim transaction * @param client - Mintlayer client - * @param spendTransactionId - The transaction ID that spent the HTLC - * @param originalHtlcTxHex - The original signed HTLC transaction hex + * @param claimTransactionId - The transaction ID that claimed/spent the HTLC + * @param claimTransactionHex - The signed claim transaction hex (contains the secret) * @returns The extracted secret in hex format */ export async function extractHTLCSecret( client: Client, - spendTransactionId: string, - originalHtlcTxHex: string + claimTransactionId: string, + claimTransactionHex: string ): Promise { return await client.extractHtlcSecret({ - transaction_id: spendTransactionId, - transaction_hex: originalHtlcTxHex, + transaction_id: claimTransactionId, + transaction_hex: claimTransactionHex, format: 'hex' }) } @@ -124,6 +124,32 @@ export async function createAndBroadcastHTLC( return { txId, txHex: signedTxHex } } +/** + * Claim/spend an HTLC + * @param client - Mintlayer client + * @param htlcTxHash - Hash of the HTLC transaction to spend + * @param secret - Secret to reveal (optional if wallet has it) + */ +export async function claimHTLC( + client: Client, + htlcTxHash: string, + secret?: string +): Promise<{ txId: string, txHex: string }> { + // Step 1: Sign the spend transaction + const spendParams: any = { htlc_tx_hash: htlcTxHash } + if (secret) { + spendParams.secret = secret + } + + const signedTxHex = await client.spendHtlc(spendParams) + + // Step 2: Broadcast to network + const broadcastResult = await client.broadcastTx(signedTxHex) + const txId = broadcastResult.tx_id || broadcastResult.transaction_id || broadcastResult.id + + return { txId, txHex: signedTxHex } +} + /** * Get timelock expiry information */ diff --git a/examples/swap-board-ml-ml/src/types/swap.ts b/examples/swap-board-ml-ml/src/types/swap.ts index dc0ff01..b299fd3 100644 --- a/examples/swap-board-ml-ml/src/types/swap.ts +++ b/examples/swap-board-ml-ml/src/types/swap.ts @@ -24,6 +24,7 @@ export interface Swap { takerHtlcTxHash?: string takerHtlcTxHex?: string claimTxHash?: string + claimTxHex?: string createdAt: Date offer?: Offer } @@ -51,4 +52,5 @@ export interface UpdateSwapRequest { takerHtlcTxHash?: string takerHtlcTxHex?: string claimTxHash?: string + claimTxHex?: string } From ac88ede4663b270338df783fd87cf0573c8e3c5d Mon Sep 17 00:00:00 2001 From: Sergey Chystiakov Date: Sun, 6 Jul 2025 03:43:16 +0200 Subject: [PATCH 06/13] fix: spend htlc token --- packages/sdk/src/mintlayer-connect-sdk.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/sdk/src/mintlayer-connect-sdk.ts b/packages/sdk/src/mintlayer-connect-sdk.ts index 7f73198..d1373ce 100644 --- a/packages/sdk/src/mintlayer-connect-sdk.ts +++ b/packages/sdk/src/mintlayer-connect-sdk.ts @@ -3402,6 +3402,11 @@ class Client { params: { to: useHtlcUtxo[0].utxo.htlc.refund_key, amount: useHtlcUtxo[0].utxo.value.amount.decimal, + ...( + useHtlcUtxo[0].utxo.value.type === 'TokenV1' + ? { token_id: useHtlcUtxo[0].utxo.value.token_id } + : {} + ) }, opts: { forceSpendUtxo: useHtlcUtxo, @@ -3446,6 +3451,11 @@ class Client { params: { to: useHtlcUtxo[0].utxo.htlc.spend_key, amount: useHtlcUtxo[0].utxo.value.amount.decimal, + ...( + useHtlcUtxo[0].utxo.value.type === 'TokenV1' + ? { token_id: useHtlcUtxo[0].utxo.value.token_id } + : {} + ) }, opts: { forceSpendUtxo: useHtlcUtxo, From 3bcdfb1b5c6083410e6b6d2604bd820a771b4c19 Mon Sep 17 00:00:00 2001 From: Sergey Chystiakov Date: Sun, 6 Jul 2025 03:57:11 +0200 Subject: [PATCH 07/13] fix: htlc --- packages/sdk/src/mintlayer-connect-sdk.ts | 26 +++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/mintlayer-connect-sdk.ts b/packages/sdk/src/mintlayer-connect-sdk.ts index d1373ce..1294f5f 100644 --- a/packages/sdk/src/mintlayer-connect-sdk.ts +++ b/packages/sdk/src/mintlayer-connect-sdk.ts @@ -3397,6 +3397,16 @@ class Client { useHtlcUtxo = created.filter(({utxo}) => utxo.type === 'Htlc') || null; } + let token_details = undefined; + + if(useHtlcUtxo[0].utxo.value.type === 'TokenV1'){ + const request = await fetch(`${this.getApiServer()}/token/${useHtlcUtxo[0].utxo.value.token_id}`); + if (!request.ok) { + throw new Error('Failed to fetch token'); + } + token_details = await request.json(); + } + return this.buildTransaction({ type: 'Transfer', params: { @@ -3406,7 +3416,8 @@ class Client { useHtlcUtxo[0].utxo.value.type === 'TokenV1' ? { token_id: useHtlcUtxo[0].utxo.value.token_id } : {} - ) + ), + token_details }, opts: { forceSpendUtxo: useHtlcUtxo, @@ -3446,6 +3457,16 @@ class Client { useHtlcUtxo = created.filter(({utxo}) => utxo.type === 'Htlc') || null; } + let token_details = undefined; + + if(useHtlcUtxo[0].utxo.value.type === 'TokenV1'){ + const request = await fetch(`${this.getApiServer()}/token/${useHtlcUtxo[0].utxo.value.token_id}`); + if (!request.ok) { + throw new Error('Failed to fetch token'); + } + token_details = await request.json(); + } + return this.buildTransaction({ type: 'Transfer', params: { @@ -3455,7 +3476,8 @@ class Client { useHtlcUtxo[0].utxo.value.type === 'TokenV1' ? { token_id: useHtlcUtxo[0].utxo.value.token_id } : {} - ) + ), + token_details, }, opts: { forceSpendUtxo: useHtlcUtxo, From d254bac6e53d5974228d52c82591327e58ac4f02 Mon Sep 17 00:00:00 2001 From: Sergey Chystiakov Date: Sun, 6 Jul 2025 09:59:28 +0200 Subject: [PATCH 08/13] add step for counterpart htlc --- .../src/app/swap/[id]/page.tsx | 144 +++++++++++++++--- 1 file changed, 121 insertions(+), 23 deletions(-) diff --git a/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx b/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx index f7348b4..d347362 100644 --- a/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx +++ b/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx @@ -361,6 +361,62 @@ export default function SwapPage({ params }: { params: { id: string } }) { } } + const claimWithExtractedSecret = async () => { + if (!client || !userAddress || !swap?.secret) { + alert('Missing required data for claiming with extracted secret') + return + } + + const isUserTaker = swap.takerMLAddress === userAddress + if (!isUserTaker) { + alert('Only the taker can use this function') + return + } + + if (!swap.creatorHtlcTxHash) { + alert('Creator HTLC not found') + return + } + + setClaimingHtlc(true) + try { + // Use the extracted secret to claim creator's HTLC + const spendParams = { + htlc_tx_hash: swap.creatorHtlcTxHash, + secret: swap.secret + } + + // Step 1: Sign the spend transaction + const signedSpendTxHex = await client.spendHtlc(spendParams) + console.log('Taker HTLC spend signed:', signedSpendTxHex) + + // Step 2: Broadcast the spend transaction + const broadcastResult = await client.broadcastTx(signedSpendTxHex) + console.log('Taker HTLC spend broadcast result:', broadcastResult) + + const spendTxId = broadcastResult.tx_id || broadcastResult.transaction_id || broadcastResult.id + + // Step 3: Update swap status to fully completed + await fetch(`/api/swaps/${swap.id}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + status: 'completed' + // Note: We don't update claimTxHash here as it refers to the first claim + }) + }) + + // Refresh swap data + fetchSwap() + alert(`Successfully claimed creator's HTLC! TX ID: ${spendTxId}. Atomic swap completed!`) + } catch (error) { + console.error('Error claiming with extracted secret:', error) + alert('Failed to claim HTLC with extracted secret. Please try again.') + } finally { + setClaimingHtlc(false) + } + } + const getStatusColor = (status: string) => { switch (status) { case 'pending': return 'text-yellow-600 bg-yellow-100' @@ -493,7 +549,7 @@ export default function SwapPage({ params }: { params: { id: string } }) {
Offer accepted @@ -501,25 +557,39 @@ export default function SwapPage({ params }: { params: { id: string } }) {
- HTLC created + Creator HTLC created
- Counterparty HTLC created + Taker HTLC created
- Tokens claimed + First HTLC claimed (secret revealed) +
+ +
+
+ Secret extracted +
+ +
+
+ Second HTLC claimed (swap complete)
@@ -652,27 +722,55 @@ export default function SwapPage({ params }: { params: { id: string } }) { {swap.status === 'completed' && (
-

- 🎉 Swap completed successfully! -

{swap.claimTxHash && ( -
+
+

+ {isUserCreator ? "You have claimed the taker's HTLC!" : "The creator has claimed your HTLC!"} +

Claim Transaction: {swap.claimTxHash}

- {swap.secret && ( -
-

Revealed Secret:

-

{swap.secret}

+ + {swap.secret ? ( +
+
+

Revealed Secret:

+

{swap.secret}

+
+ + {isUserTaker && ( +
+

+ Now you can claim the creator's HTLC using this secret: +

+ +
+ )} + + {isUserCreator && ( +

+ ✅ Atomic swap completed! Both parties have their tokens. +

+ )} +
+ ) : ( +
+

+ {isUserTaker ? "Extract the secret to claim the creator's HTLC:" : "The taker needs to extract the secret:"} +

+
- )} - {!swap.secret && ( - )}
)} From 0b0f68c097dcbea78feb5cf84b37f7032152dc3d Mon Sep 17 00:00:00 2001 From: Sergey Chystiakov Date: Sun, 6 Jul 2025 10:08:22 +0200 Subject: [PATCH 09/13] adjust last step --- .../src/app/swap/[id]/page.tsx | 55 ++++++++++++++----- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx b/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx index d347362..bf6da87 100644 --- a/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx +++ b/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx @@ -238,7 +238,6 @@ export default function SwapPage({ params }: { params: { id: string } }) { setClaimingHtlc(true) try { let htlcTxHash: string - let secret: string if (isUserCreator) { // Creator claims taker's HTLC using the secret stored in their wallet @@ -263,15 +262,48 @@ export default function SwapPage({ params }: { params: { id: string } }) { alert('Secret is required to claim HTLC') return } - secret = inputSecret + + // Build spend HTLC parameters for taker (with secret) + const spendParams = { + transaction_id: htlcTxHash, + secret: inputSecret + } + + // Step 1: Sign the spend transaction + const signedSpendTxHex = await client.spendHtlc(spendParams) + console.log('HTLC spend signed:', signedSpendTxHex) + + // Step 2: Broadcast the spend transaction + const broadcastResult = await client.broadcastTx(signedSpendTxHex) + console.log('HTLC spend broadcast result:', broadcastResult) + + const spendTxId = broadcastResult.tx_id || broadcastResult.transaction_id || broadcastResult.id + + // Step 3: Update swap status with both transaction ID and hex + // Note: We save the claim transaction hex because it's needed to extract the secret + const updateData: any = { + status: 'completed', + claimTxHash: spendTxId, + claimTxHex: signedSpendTxHex, + secret: inputSecret + } + + await fetch(`/api/swaps/${swap.id}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updateData) + }) + + // Refresh swap data + fetchSwap() + alert(`HTLC claimed successfully! Spend TX ID: ${spendTxId}`) + return } - // Build spend HTLC parameters - const spendParams = isUserCreator - ? { transaction_id: htlcTxHash } // Creator: wallet provides secret automatically - : { transaction_id: htlcTxHash, secret: secret } // Taker: must provide secret + // Build spend HTLC parameters for creator (no secret needed) + const spendParams = { transaction_id: htlcTxHash } - // Step 1: Sign the spend transaction + // Step 1: Sign the spend transaction (creator case) const signedSpendTxHex = await client.spendHtlc(spendParams) console.log('HTLC spend signed:', signedSpendTxHex) @@ -283,17 +315,12 @@ export default function SwapPage({ params }: { params: { id: string } }) { // Step 3: Update swap status with both transaction ID and hex // Note: We save the claim transaction hex because it's needed to extract the secret - const updateData: any = { + const updateData = { status: 'completed', claimTxHash: spendTxId, claimTxHex: signedSpendTxHex } - // Only include secret if taker provided it (creator's secret is in wallet) - if (!isUserCreator && secret) { - updateData.secret = secret - } - await fetch(`/api/swaps/${swap.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -382,7 +409,7 @@ export default function SwapPage({ params }: { params: { id: string } }) { try { // Use the extracted secret to claim creator's HTLC const spendParams = { - htlc_tx_hash: swap.creatorHtlcTxHash, + transaction_id: swap.creatorHtlcTxHash, secret: swap.secret } From 0ddb8b7164127c3ee198e631f32f549de5a42947 Mon Sep 17 00:00:00 2001 From: Sergey Chystiakov Date: Sun, 6 Jul 2025 10:10:25 +0200 Subject: [PATCH 10/13] adjust layout --- examples/swap-board-ml-ml/src/app/page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/swap-board-ml-ml/src/app/page.tsx b/examples/swap-board-ml-ml/src/app/page.tsx index a796c90..e2edab6 100644 --- a/examples/swap-board-ml-ml/src/app/page.tsx +++ b/examples/swap-board-ml-ml/src/app/page.tsx @@ -10,7 +10,7 @@ export default function Home() {

Trade Mintlayer tokens directly with other users using secure HTLC atomic swaps

- +
@@ -21,11 +21,11 @@ export default function Home() {

Browse Offers

- View available token swap offers from other users and accept the ones that interest you. + View available token swap offers from other users.

- +
@@ -40,7 +40,7 @@ export default function Home() {
- +

How it works

From 06b78cef8399115789cfb6c5c0f0a2208b19ca6c Mon Sep 17 00:00:00 2001 From: Sergey Chystiakov Date: Sun, 13 Jul 2025 20:37:34 +0200 Subject: [PATCH 11/13] adjust complete status --- examples/swap-board-ml-ml/prisma/schema.prisma | 2 +- .../swap-board-ml-ml/src/app/swap/[id]/page.tsx | 13 +++++++++---- examples/swap-board-ml-ml/src/lib/htlc-utils.ts | 2 +- examples/swap-board-ml-ml/src/types/swap.ts | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/examples/swap-board-ml-ml/prisma/schema.prisma b/examples/swap-board-ml-ml/prisma/schema.prisma index a14232c..b722594 100644 --- a/examples/swap-board-ml-ml/prisma/schema.prisma +++ b/examples/swap-board-ml-ml/prisma/schema.prisma @@ -30,7 +30,7 @@ model Swap { id Int @id @default(autoincrement()) offerId Int takerMLAddress String - status String @default("pending") // pending, htlc_created, in_progress, completed, refunded + status String @default("pending") // pending, htlc_created, in_progress, completed, fully_completed, refunded secretHash String? secret String? creatorHtlcTxHash String? // Creator's HTLC transaction ID diff --git a/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx b/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx index bf6da87..bb53d96 100644 --- a/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx +++ b/examples/swap-board-ml-ml/src/app/swap/[id]/page.tsx @@ -428,7 +428,7 @@ export default function SwapPage({ params }: { params: { id: string } }) { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - status: 'completed' + status: 'fully_completed' // Note: We don't update claimTxHash here as it refers to the first claim }) }) @@ -614,7 +614,7 @@ export default function SwapPage({ params }: { params: { id: string } }) {
Second HTLC claimed (swap complete)
@@ -747,12 +747,17 @@ export default function SwapPage({ params }: { params: { id: string } }) {
)} - {swap.status === 'completed' && ( + {(swap.status === 'completed' || swap.status === 'fully_completed') && (
{swap.claimTxHash && (

- {isUserCreator ? "You have claimed the taker's HTLC!" : "The creator has claimed your HTLC!"} + {swap.status === 'fully_completed' + ? "🎉 Atomic swap completed successfully! Both parties have their tokens." + : isUserCreator + ? "You have claimed the taker's HTLC!" + : "The creator has claimed your HTLC!" + }

Claim Transaction: {swap.claimTxHash} diff --git a/examples/swap-board-ml-ml/src/lib/htlc-utils.ts b/examples/swap-board-ml-ml/src/lib/htlc-utils.ts index 94818e6..9165465 100644 --- a/examples/swap-board-ml-ml/src/lib/htlc-utils.ts +++ b/examples/swap-board-ml-ml/src/lib/htlc-utils.ts @@ -136,7 +136,7 @@ export async function claimHTLC( secret?: string ): Promise<{ txId: string, txHex: string }> { // Step 1: Sign the spend transaction - const spendParams: any = { htlc_tx_hash: htlcTxHash } + const spendParams: any = { transaction_id: htlcTxHash } if (secret) { spendParams.secret = secret } diff --git a/examples/swap-board-ml-ml/src/types/swap.ts b/examples/swap-board-ml-ml/src/types/swap.ts index b299fd3..98a0b82 100644 --- a/examples/swap-board-ml-ml/src/types/swap.ts +++ b/examples/swap-board-ml-ml/src/types/swap.ts @@ -16,7 +16,7 @@ export interface Swap { id: number offerId: number takerMLAddress: string - status: 'pending' | 'htlc_created' | 'in_progress' | 'completed' | 'refunded' + status: 'pending' | 'htlc_created' | 'in_progress' | 'completed' | 'fully_completed' | 'refunded' secretHash?: string secret?: string creatorHtlcTxHash?: string From 3aabc787f60fbce8dd363324d8f4d2ea39543af5 Mon Sep 17 00:00:00 2001 From: Sergey Chystiakov Date: Sun, 13 Jul 2025 20:40:14 +0200 Subject: [PATCH 12/13] fix deps --- pnpm-lock.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1844e62..971c3b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,9 +39,6 @@ importers: examples/swap-board-ml-ml: dependencies: - '@mintlayer/react': - specifier: workspace:* - version: link:../../packages/react '@mintlayer/sdk': specifier: workspace:* version: link:../../packages/sdk From 5a860f3c96f770bac4f1f1b187d7fd43de533218 Mon Sep 17 00:00:00 2001 From: Sergey Chystiakov Date: Sun, 13 Jul 2025 20:48:06 +0200 Subject: [PATCH 13/13] add readme for examples folder --- examples/Readme.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 examples/Readme.md diff --git a/examples/Readme.md b/examples/Readme.md new file mode 100644 index 0000000..b6c796f --- /dev/null +++ b/examples/Readme.md @@ -0,0 +1,59 @@ +# Mintlayer Connect SDK Examples + +This directory contains example applications demonstrating how to use the Mintlayer Connect SDK for various use cases. + +## Available Examples + +### 🌐 [Demo](./demo/) +A comprehensive web-based demo showcasing all Mintlayer Connect SDK features including: +- Wallet connection and management +- Token transfers and NFT operations +- Token issuance, minting, and burning +- DEX order creation and filling +- Delegation and staking operations +- HTLC (Hash Time Locked Contracts) for atomic swaps +- Bridge requests and transaction signing + +**Tech Stack:** Vanilla JavaScript + HTML + Webpack +**Network Support:** Mainnet and Testnet + +### 🔄 [P2P Swap Board](./swap-board-ml-ml/) +A peer-to-peer token swap application using HTLC atomic swaps for secure token exchanges: +- Create and browse swap offers +- Accept offers and execute atomic swaps +- Real-time swap status tracking +- Wallet integration with Mojito wallet + +**Tech Stack:** Next.js 14 + React + Tailwind CSS + SQLite/Prisma +**Features:** Full-stack P2P swap marketplace with database persistence + +## Getting Started + +Each example includes its own setup instructions. Navigate to the specific example directory and follow the README instructions. + +### Prerequisites +- Node.js 18+ +- pnpm (recommended) or npm +- Mojito wallet extension (for wallet integration examples) + +### Quick Start +```bash +# Navigate to desired example +cd demo # or swap-board-ml-ml + +# Install dependencies +pnpm install + +# Follow example-specific instructions in their README +``` + +## Example Structure + +- **demo/**: Interactive web demo with all SDK features +- **swap-board-ml-ml/**: Full P2P swap application +- **nodejs/**: (Coming soon) Node.js server examples +- **react-app/**: (Coming soon) React application examples + +## Documentation + +For detailed SDK documentation and API reference, visit the main project documentation.