Bitcoin custodial wallet written as a code challenge for a crypto exchange.
Table of contents:
- Get started
- Project structure
- Running the server locally
- Running the client locally
- API Usage
- Plaid
- To-do's
We're using Bun for this project, if you don't have it installed, run this command:
curl -fsSL https://bun.sh/install | bash
Then, install dependencies with:
cd bitcoin-custodial-wallet && bun i
This project is using workspaces as the monorepo management tool. It's organized as follows:
bitcoin-custodial-wallet/
├── package.json 👉 Root of monorepo
└── packages
├── eslint-config 👉 Unified linting configuration
├── server 👉 Express backend application
└── client 👉 Expo frontend application
The server uses environment variables for configuration. Create a .env
file in the packages/server
directory based on .env.example
.
To run the server, you'll need Docker and Docker Compose installed on your machine. From the workspace root, run the following command to build and start the containers:
docker-compose up -d --build
This will start both the server and MongoDB containers. The server will be accessible at http://localhost:8080
.
To run the client app (you can test it on iOS and Android platforms), you can cd into packages/client
folder and call a script from package.json with bun run <script>
. Alternatively, from the workspace root:
# iOS
bun run --filter client ios
# Android
bun run --filter client android
To sign-up a new user, you can use the following curl command:
curl -X POST http://localhost:8080/signup \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "user@example.com",
"password": "securepassword123"
}'
This command sends a POST request to the /signup
endpoint with the user's name, email, and password. If successful, it will return a JSON response with a success message, a JWT token and the new user's ID.
To sign-in an existing user, you can use the following curl command:
curl -X POST http://localhost:8080/signin \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "securepassword123"
}'
This command sends a POST request to the /signin
endpoint with the user's email and password. If successful, it will return a JSON response with a success message, a JWT token and the user's ID.
To retrieve information about the currently authenticated user, you can use the following curl command:
curl -X GET http://localhost:8080/me \
-H "Content-Type: application/json" \
-H "Authorization: Bearer JWT_TOKEN"
This command sends a GET request to the /me
endpoint. You need to replace JWT_TOKEN
with a valid JWT token obtained from the sign-in or sign-up process. If successful, it will return a JSON response with the user's information, like this:
{
"message": "User fetched successfully",
"data": {
"user": {
"id": "670b574fe04560073075c472",
"name": "John Doe",
"email": "user@example.com",
"bitcoinAddress": "bcrt1qeqs3es2zpzy3daljhvh40h3qk2thn34023hhsx"
}
}
}
To create a Plaid link token, you can use the following curl command:
curl -X POST http://localhost:8080/plaid/link/token/create \
-H "Content-Type: application/json" \
-H "Authorization: Bearer JWT_TOKEN"
This command sends a POST request to the /plaid/link/token/create
endpoint. You need to replace JWT_TOKEN
with a valid JWT token obtained from the sign-in or sign-up process. If successful, it will return a JSON response with the link token, like this:
{
"message": "Link token created successfully",
"data": {
"linkToken": {
"expiration": "2024-10-12T23:39:15Z",
"link_token": "link-sandbox-e888a731-8395-4a16-8ea8-a8ce678a956f",
"request_id": "TFZ0zSKUbzib05A"
}
}
}
We can use this link token to initialize Plaid Link in our client application.
Once the user linked their bank account using Plaid Link, we can exchange an ephemeral public token for a non-expiring access token.
curl -X POST http://localhost:8080/plaid/item/public_token/exchange \
-H "Content-Type: application/json" \
-H "Authorization: Bearer JWT_TOKEN" \
-d '{
"publicToken": "public-sandbox-d8fe6954-a206-41c1-8c8a-5a7f401e4c00"
}'
This command sends a POST request to the /plaid/item/public_token/exchange
endpoint with the public token received from Plaid Link. You need to replace JWT_TOKEN
with a valid JWT token obtained from the sign-in or sign-up process, and publicToken
with the actual public token you received. If successful, it will return a JSON response like this:
{
"message": "Public token exchanged successfully",
"data": {
"accessToken": {
"access_token": "access-sandbox-cf452a0f-90a7-4164-8729-4e8fca3f3963",
"item_id": "mRAeMXooEkCqBX4n5mKbSdD74V4MoVcLApbrz",
"request_id": "bozsnlU8ExSjurW"
}
}
}
We can use this access token to make authenticated requests to Plaid's API endpoints on behalf of the user's bank account. For example, we can retrieve account and transaction data, initiate payments, or perform other financial operations supported by Plaid. The access token should be securely stored and associated with the user's account in our application.
To retrieve the balance of the user's linked Plaid account, you can use the following curl command:
curl -X GET http://localhost:8080/plaid/account/balance \
-H "Content-Type: application/json" \
-H "Authorization: Bearer JWT_TOKEN"
This command sends a GET request to the /plaid/account/balance
endpoint. You need to replace JWT_TOKEN
with a valid JWT token obtained from the sign-in or sign-up process. If successful, it will return a JSON response with the account balance information, like this:
{
"message": "Account balance read successfully",
"data": { "balance": 100 }
}
Note that this endpoint requires authentication and will only work if the user has successfully linked their bank account using Plaid.
To generate a new Bitcoin address for a user, you can use the following curl command:
curl -X POST http://localhost:8080/bitcoin/generate_address \
-H "Content-Type: application/json" \
-H "Authorization: Bearer JWT_TOKEN"
This command sends a POST request to the /bitcoin/generate_address
endpoint. You need to replace JWT_TOKEN
with a valid JWT token obtained from the sign-in or sign-up process. If successful, it will return a JSON response with the newly generated Bitcoin address, like this:
{
"message": "Bitcoin address generated successfully",
"data": {
"bitcoinAddress": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"
}
}
Note that this endpoint requires authentication. A new Bitcoin address will only be generated if the user doesn't already have one associated with their account. If the user already has a Bitcoin address, the API will return an error.
To purchase Bitcoin for a user, you can use the following curl command:
curl -X POST http://localhost:8080/bitcoin/purchase \
-H "Content-Type: application/json" \
-H "Authorization: Bearer JWT_TOKEN" \
-d '{
"amountUSD": 100
}'
This command sends a POST request to the /bitcoin/purchase
endpoint. You need to replace JWT_TOKEN
with a valid JWT token obtained from the sign-in or sign-up process. The amountUSD
in the request body specifies the amount in USD to spend on purchasing Bitcoin.
If the purchase is successful, you'll receive a JSON response like this:
{
"message": "Bitcoin purchase successful",
"data": {
"transaction": "fb28a7ff107a72d0bc31bcc976db296e6db265a2a3f9cdbc1e64c2dbd9736e62",
"amountBTC": 0.00159234
}
}
The response includes the transaction ID and the amount of Bitcoin purchased based on the current exchange rate.
To retrieve the Bitcoin balance for a user, you can use the following curl command:
curl -X GET http://localhost:8080/bitcoin/balance \
-H "Content-Type: application/json" \
-H "Authorization: Bearer JWT_TOKEN"
This command sends a GET request to the /bitcoin/balance
endpoint. You need to replace JWT_TOKEN
with a valid JWT token obtained from the sign-in or sign-up process. If successful, it will return a JSON response with the user's Bitcoin balance, like this:
{
"message": "Bitcoin balance retrieved successfully",
"data": {
"balance": 0.00123456
}
}
Note that this endpoint requires authentication and will only work if the user has a Bitcoin address associated with their account.
To retrieve the current Bitcoin price, you can use the following curl command:
curl -X GET http://localhost:8080/bitcoin/price \
-H "Content-Type: application/json" \
-H "Authorization: Bearer JWT_TOKEN"
This command sends a GET request to the /bitcoin/price
endpoint. You need to replace JWT_TOKEN
with a valid JWT token obtained from the sign-in or sign-up process. If successful, it will return a JSON response with the current Bitcoin price, like this:
{
"message": "Bitcoin price fetched successfully",
"data": {
"price": 62794.8068
}
}
Note that this endpoint requires authentication. The price returned is in USD.
- Quickstart docs: https://plaid.com/docs/quickstart
- API reference: https://plaid.com/docs/api
- Link docs: https://plaid.com/docs/link
- Get API keys: https://dashboard.plaid.com/developers/keys
- React Native SDK docs: https://plaid.com/docs/link/react-native
- Integrate Plaid SDK to React Native using Expo Config Plugins: https://www.aronberezkin.com/posts/how-to-integrate-plaid-sdk-to-react-native-using-expo-config-plugins
- username: user_good
- password: pass_good
- 2FA code: 1234
- expo-asset - BTC and NFT image
- expo-router on web, it's not showing anything
- Data fetching with react-query
- Setup root imports on frontend
- Setup root imports on backend
- Setup ESLint on client
- Setup ESLint on server
- Replace @/ for ~/
- ThemedText and ThemedInput components
- @expo-google-fonts/inter
- Learn more about expo-router
- Define app structure and screens, unauthed vs authed, link them
- Add unit tests
- Add passkeys to sign up
- Customize app icon and splash screen
- Remove ios and android folders from git with Expo CNG
- Run on physical device
- Record demo
- useForm wrapper API?
- Add input validation on the frontend with Yup + react hook form
- Add input validation on the backend with Zod
- Try gluestack UI?
- From the creators of NativeBase
- https://gluestack.io/ui/docs/home/overview/introduction
- https://github.com/gluestack/gluestack-ui-starter-kits
- Or maybe antd?
- Offline support?
- Plaid web support?