Building a Simple Donation System with Bitcoin
In this tutorial, we will demonstrate how to create a basic donation system using Bitcoin. The system allows users to pay to a specific address, and the status of each payment can be tracked in real time.
Prerequisites
Before starting this tutorial, make sure you have the following:
Docker Compose installed. It is used to run the multi-container Docker applications.
Node.js installed for running the server and installing packages.
If you don't have a Next.js application ready, you can create a new one by using the following command:
npx create-next-app@latest
Ensure the experimental server action feature is enabled in your Next.js app.
Add the following configuration to next.config.js
:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},
}
module.exports = nextConfig
Server Action
is not required for bamotf to work, but it is required for
this tutorial.
Setting up the Application
Start up bamotf
Let's start by setting up the Docker services required for bamotf. Create a
docker-compose.yml
file in the root directory of your project and paste the
following:
version: '3.8'
services:
bamotf:
image: bamotf/server:latest
environment:
- REDIS_URL=redis://redis:6379
- POSTGRES_URL=postgresql://johndoe:randompassword@postgres:5432
- DEV_MODE_ENABLED=true
- DEV_API_KEY=my-key
- DEV_WEBHOOK_URL=http://host.docker.internal:3000/webhook/bamotf
- DEV_WEBHOOK_SECRET=my-secret
ports:
- 21000:21000
depends_on:
- postgres
- redis
postgres:
image: postgres:latest
environment:
- POSTGRES_USER=johndoe
- POSTGRES_PASSWORD=randompassword
volumes:
- ./docker-data/postgres:/var/lib/postgresql/data
redis:
image: redis:latest
This configuration will run docker services: bamotf, postgres, and redis. The bamotf service is our main service and it will interact with the other services.
You can start the Docker services by running:
docker-compose up -d
Remember to add Docker data directories to your .gitignore
file to avoid
pushing them to the repository:
# docker image files
docker-data
Install required packages
We will use three packages from the bamotf library: @bamotf/react, @bamotf/node, and @bamotf/utils. Install these packages using the following command:
npm install --save @bamotf/react @bamotf/node
Configuring bamotf
Create a new file utils/bamotf.ts
in your project directory:
import {Bamotf} from '@bamotf/node'
// TODO: Replace with your API key
const bamotf = new Bamotf('my-key')
export {bamotf}
Creating Payment Flow
Replace your app/page.tsx
with this code to create simple payment form:
import {bamotf} from '@/utils/bamotf'
import {redirect} from 'next/navigation'
const donate = async () => {
'use server'
// Addresses are unique to each payment. You normally want this index to
// come from your database, but for this example we'll just use 0 for
// creating one payment intent.
const index = 0
// TODO: Replace with your XPUB
const xpub =
'tpubD6NzVbkrYhZ4XkJSgrMFnNTX9yThvRVCaBTeXhXMC54PfJ9Y8uwzLNQcT53fCW2ADemefH1ADEX7CeyjnkBNws7NoP7nfjKo93wBNQuRVMw'
const address = await bamotf.address.derive(xpub, index, 'development')
const pi = await bamotf.paymentIntents.create({
amount: 1000,
currency: 'USD',
address,
})
return redirect(`/${pi.id}`)
}
export default async function Home() {
return (
<form action={donate}>
<button type="submit">Donate $10</button>
</form>
)
}
Creating the Payment Status Page Create a new page app/[id]/page.tsx
to
display the status of a specific payment:
import React from 'react'
import {bamotf} from '@/utils/bamotf'
import {PaymentDetails} from './payment-details'
import '@bamotf/react/components.css'
export default async function DonationStatusPage({
params,
}: {
params: {id: string}
}) {
const pi = await bamotf.paymentIntents.retrieve(params.id)
const amountInBtc = await bamotf.currency.toBitcoin(pi)
if (pi.status === 'succeeded') {
return <>Thank you of your donation</>
}
return <PaymentDetails amount={amountInBtc} address={pi.address} />
}
Since Next.js 13 follows the "use client" patern, we need to create a separate
file for the client-side components. Next, create the
app/[id]/payment-details.tsx
:
'use client'
// Make the PaymentDetails component available only to the client
// This is a Next.js workaround for exporting client-side components
// See: https://nextjs.org/docs/getting-started/react-essentials#third-party-packages
export {PaymentDetails} from '@bamotf/react'
With these steps, your payment system is now set up. Users can make payments in Bitcoin and check their status in real time.
Creating a Webhook
To receive payment status updates, we need to create a webhook. Create a new
file app/webhook/bamotf/route.ts
:
import {bamotf} from '@/utils/bamotf'
import {NextResponse} from 'next/server'
export async function POST(request: Request) {
// TODO: add your secret here
const secret = 'my-secret'
const rawBody = await request.text()
const signatureHeader = request.headers.get('x-webhook-signature') || ''
const {success, parsed} = bamotf.webhooks.constructEvent(
rawBody,
signatureHeader,
secret,
)
if (!success) {
return NextResponse.json(
{success: false, error: 'Invalid signature'},
{status: 401, statusText: 'Unauthorized'},
)
}
const {
event,
data: {paymentIntent},
} = parsed
switch (event) {
case 'payment_intent.succeeded':
// here is where you update the database with the payment status
// and send a confirmation email to the user or push a web socket event to
// the client to update the UI
// ...
console.log(`✅ Payment intent succeeded: ${paymentIntent.id}`)
return NextResponse.json({success: true})
default:
console.error(`Unknown event: ${event}`)
return NextResponse.json({success: false, error: 'Unknown event'})
}
}
Running the Application
To run the application, use the following command:
npm run dev
This will start the Next.js development server and expose it on
http://localhost:3000
(opens in a new tab). You can now open the
application in your browser and start the payment. You can visualise the payment
status in real time by opening the application in a different tab on
http://localhost:21000
(opens in a new tab). There's also a
simulate function where
you can simulate a payment during development.