├── backend ├── .gitignore ├── types │ └── coinselect │ │ └── index.d.ts ├── .DS_Store ├── .env.sample ├── routes │ ├── api │ │ ├── index.ts │ │ └── v1 │ │ │ ├── index.ts │ │ │ ├── user.ts │ │ │ └── wallet.ts │ └── index.ts ├── index.ts ├── interfaces │ ├── knex.ts │ ├── index.ts │ └── blockstream.ts ├── db │ ├── knex.ts │ └── migrations │ │ ├── 20220325161331_update_user.js │ │ ├── 20220326221659_update_usr.js │ │ ├── 20220222205626_users_table.js │ │ └── 20220504071552_p2sh.js ├── helpers │ ├── encryptKey.ts │ ├── password.ts │ ├── index.ts │ ├── auth.ts │ ├── jwt.ts │ ├── blockstream-api.ts │ ├── transactions.ts │ └── bitcoinlib.ts ├── utils │ └── validator │ │ ├── user.ts │ │ └── wallet.ts ├── app.ts ├── tsconfig.json ├── package.json ├── knexfile.ts ├── tests │ └── api.spec.ts └── controllers │ ├── user.ts │ └── wallet.ts ├── client ├── .eslintrc.json ├── public │ ├── favicon.ico │ ├── assets │ │ └── fonts │ │ │ └── itc-avant │ │ │ ├── ITCAvantGardeStdBk.otf │ │ │ ├── ITCAvantGardeStdMd.otf │ │ │ ├── ITCAvantGardeStdXLt.otf │ │ │ ├── ITCAvantGardeStdBkCn.otf │ │ │ ├── ITCAvantGardeStdBkObl.otf │ │ │ ├── ITCAvantGardeStdBold.otf │ │ │ ├── ITCAvantGardeStdBoldCn.otf │ │ │ ├── ITCAvantGardeStdDemi.otf │ │ │ ├── ITCAvantGardeStdDemiCn.otf │ │ │ ├── ITCAvantGardeStdMdCn.otf │ │ │ ├── ITCAvantGardeStdMdObl.otf │ │ │ ├── ITCAvantGardeStdXLtCn.otf │ │ │ ├── ITCAvantGardeStdXLtObl.otf │ │ │ ├── ITCAvantGardeStdBkCnObl.otf │ │ │ ├── ITCAvantGardeStdBoldObl.otf │ │ │ ├── ITCAvantGardeStdDemiObl.otf │ │ │ ├── ITCAvantGardeStdMdCnObl.otf │ │ │ ├── ITCAvantGardeStdXLtCnObl.otf │ │ │ ├── ITCAvantGardeStdBoldCnObl.otf │ │ │ └── ITCAvantGardeStdDemiCnObl.otf │ └── vercel.svg ├── postcss.config.js ├── next.config.js ├── next-env.d.ts ├── pages │ ├── _app.tsx │ ├── api │ │ └── hello.ts │ ├── utxos │ │ ├── components │ │ │ ├── EmptyState.tsx │ │ │ └── UtxoRow.tsx │ │ └── index.tsx │ ├── addresses │ │ ├── components │ │ │ ├── EmptyState.tsx │ │ │ ├── P2shRow.tsx │ │ │ ├── MultiSuccess.tsx │ │ │ ├── AddressRow.tsx │ │ │ └── MultisigTrans.tsx │ │ └── index.tsx │ ├── transactions │ │ ├── components │ │ │ ├── EmptyState.tsx │ │ │ └── TransactionRow.tsx │ │ └── index.tsx │ ├── types │ │ ├── index.ts │ │ └── blockstream.ts │ ├── send │ │ ├── components │ │ │ ├── TransactionSuccessAlert.tsx │ │ │ ├── TransactionSummary.tsx │ │ │ └── CreateTxForm.tsx │ │ └── index.tsx │ ├── index.tsx │ ├── utils │ │ └── index.ts │ ├── login.tsx │ ├── settings │ │ └── index.tsx │ ├── receive │ │ └── index.tsx │ └── signup.tsx ├── helpers │ ├── localstorage.ts │ └── axios.ts ├── styles │ ├── _variables.scss │ ├── _fonts.scss │ ├── login.scss │ ├── form.scss │ └── globals.scss ├── components │ ├── Loader.tsx │ ├── topbar.tsx │ ├── WalletContext.ts │ ├── sidebar.tsx │ ├── MobileNav.tsx │ └── bodywrap.tsx ├── tsconfig.json ├── .gitignore ├── package.json ├── tailwind.config.js └── README.md ├── .DS_Store ├── .vscode └── settings.json ├── .metals ├── metals.mv.db ├── metals.lock.db └── metals.log └── README.md /backend/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .env -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /backend/types/coinselect/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "coinselect"; 2 | 3 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/.DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.watcherExclude": { 3 | "**/target": true 4 | } 5 | } -------------------------------------------------------------------------------- /backend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/backend/.DS_Store -------------------------------------------------------------------------------- /.metals/metals.mv.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/.metals/metals.mv.db -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /backend/.env.sample: -------------------------------------------------------------------------------- 1 | DEV_DB_USER = '' 2 | DEV_DB_PASS = '' 3 | 4 | TEST_DB_USER = '' 5 | TEST_DB_PASS = '' 6 | 7 | TOKEN_SECRET = '' 8 | ENCRYPT_SECRET = '' -------------------------------------------------------------------------------- /client/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /.metals/metals.lock.db: -------------------------------------------------------------------------------- 1 | #FileLock 2 | #Sun Apr 03 17:29:54 WAT 2022 3 | hostName=localhost 4 | id=17ff042f573cf7225be8a47e5401e1ecbb6be11ba00 5 | method=file 6 | server=localhost\:49635 7 | -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdBk.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdBk.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdMd.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdMd.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdXLt.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdXLt.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdBkCn.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdBkCn.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdBkObl.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdBkObl.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdBold.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdBoldCn.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdBoldCn.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdDemi.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdDemi.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdDemiCn.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdDemiCn.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdMdCn.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdMdCn.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdMdObl.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdMdObl.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdXLtCn.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdXLtCn.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdXLtObl.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdXLtObl.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdBkCnObl.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdBkCnObl.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdBoldObl.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdBoldObl.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdDemiObl.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdDemiObl.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdMdCnObl.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdMdCnObl.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdXLtCnObl.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdXLtCnObl.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdBoldCnObl.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdBoldCnObl.otf -------------------------------------------------------------------------------- /client/public/assets/fonts/itc-avant/ITCAvantGardeStdDemiCnObl.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elraphty/FullstackBitcoinWallet/HEAD/client/public/assets/fonts/itc-avant/ITCAvantGardeStdDemiCnObl.otf -------------------------------------------------------------------------------- /backend/routes/api/index.ts: -------------------------------------------------------------------------------- 1 | import express, {Router} from 'express'; 2 | import v1 from './v1'; 3 | 4 | const router: Router = express.Router(); 5 | 6 | router.use('/v1', v1); 7 | 8 | export default router; -------------------------------------------------------------------------------- /backend/index.ts: -------------------------------------------------------------------------------- 1 | import app from "./app"; 2 | 3 | const PORT = process.env.PORT || 8000; 4 | 5 | app.listen(PORT, (): void => { 6 | console.log(`Server Running here 👉 https://localhost:${PORT}`); 7 | }); -------------------------------------------------------------------------------- /client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /backend/routes/api/v1/index.ts: -------------------------------------------------------------------------------- 1 | import express, {Router} from 'express'; 2 | import wallet from './wallet'; 3 | import user from './user'; 4 | 5 | const router: Router = express.Router(); 6 | 7 | router.use('/wallet', wallet); 8 | 9 | router.use('/user', user); 10 | 11 | export default router; -------------------------------------------------------------------------------- /client/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.scss'; 2 | import '../styles/form.scss'; 3 | import '../styles/login.scss'; 4 | 5 | import type { AppProps } from 'next/app'; 6 | 7 | function MyApp({ Component, pageProps }: AppProps) { 8 | return 9 | } 10 | 11 | export default MyApp 12 | -------------------------------------------------------------------------------- /client/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /backend/interfaces/knex.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id?: number; 3 | email: string; 4 | password?: string; 5 | pk?: string; 6 | pub?: string; 7 | } 8 | 9 | export interface P2SH { 10 | id?: number; 11 | userid?: number; 12 | address: string; 13 | redeem?: string; 14 | } 15 | 16 | export interface UserLogin extends User { 17 | token: string; 18 | } -------------------------------------------------------------------------------- /backend/routes/api/v1/user.ts: -------------------------------------------------------------------------------- 1 | import express, { Router } from 'express'; 2 | import { registerUser, userLogin } from '../../../controllers/user'; 3 | import { createUser } from '../../../utils/validator/user'; 4 | 5 | const router: Router = express.Router(); 6 | 7 | router.post('/', createUser, registerUser); 8 | 9 | router.post('/login', createUser, userLogin); 10 | 11 | export default router; -------------------------------------------------------------------------------- /backend/db/knex.ts: -------------------------------------------------------------------------------- 1 | const env = process.env.NODE_ENV || 'development' 2 | 3 | import {Knex, knex} from 'knex'; 4 | import {development, test, production} from '../knexfile'; 5 | 6 | let KnexSetup: Knex; 7 | if (env === 'production') { 8 | KnexSetup = knex(production); 9 | } else if (env === 'test') { 10 | KnexSetup = knex(test); 11 | } else { 12 | KnexSetup = knex(development) 13 | } 14 | 15 | export default KnexSetup -------------------------------------------------------------------------------- /backend/db/migrations/20220325161331_update_user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | exports.up = function (knex) { 6 | return knex.schema.alterTable('users', (t) => { 7 | t.string('pk'); 8 | }); 9 | }; 10 | 11 | /** 12 | * @param { import("knex").Knex } knex 13 | * @returns { Promise } 14 | */ 15 | exports.down = function (knex) { 16 | return knex.schema.dropTable('users'); 17 | }; 18 | -------------------------------------------------------------------------------- /backend/db/migrations/20220326221659_update_usr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | exports.up = function (knex) { 6 | return knex.schema.alterTable('users', (t) => { 7 | t.string('pub'); 8 | }); 9 | }; 10 | 11 | /** 12 | * @param { import("knex").Knex } knex 13 | * @returns { Promise } 14 | */ 15 | exports.down = function (knex) { 16 | return knex.schema.dropTable('users'); 17 | }; 18 | -------------------------------------------------------------------------------- /client/helpers/localstorage.ts: -------------------------------------------------------------------------------- 1 | export const getFromStorage = (key: string): string | null => { 2 | let token: string | null = ''; 3 | if (typeof window !== 'undefined') { 4 | token = window.localStorage.getItem(key) 5 | } 6 | return token; 7 | }; 8 | 9 | export const setToStorage = (key: string, value: any) => { 10 | if (typeof window !== 'undefined') { 11 | return window.localStorage.setItem(key, value) 12 | } 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /client/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | @import '_fonts'; 2 | 3 | $primaryBackground: #000000; 4 | $primaryButton: rgb(168, 85, 247); 5 | $textLightColor: #FEFEFE; 6 | $textLightError: #FFB1A3; 7 | $textDarkColor: #000000; 8 | $textFont: ITC_AVANT, 'sans-serif'; 9 | $textSizeMd: 1.2rem; 10 | $textSizeSm: 0.9rem; 11 | $textSizeXs: 0.8rem; 12 | $font: ITC_AVANT; 13 | $fontDemi: ITC_AVANT_DEMI; 14 | $fontBold: ITC_AVANT_BOLD; 15 | $bodyWrapHeight: calc(94.5vh - 15px); 16 | $bodyMobileWrapHeight: calc(93vh - 15px); -------------------------------------------------------------------------------- /client/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | 3 | const Loader: NextPage = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ) 18 | } 19 | 20 | export default Loader; -------------------------------------------------------------------------------- /client/styles/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: ITC_AVANT; 3 | src: url(../public/assets/fonts/itc-avant/ITCAvantGardeStdBk.otf); 4 | } 5 | @font-face { 6 | font-family: ITC_AVANT_BOLD; 7 | src: url(../public/assets/fonts/itc-avant/ITCAvantGardeStdBold.otf); 8 | } 9 | @font-face { 10 | font-family: ITC_AVANT_GOTHIC; 11 | src: url(../public/assets/fonts/itc-avant/ITCAvantGardeStdBoldCn.otf); 12 | } 13 | @font-face { 14 | font-family: ITC_AVANT_DEMI; 15 | src: url(../public/assets/fonts/itc-avant/ITCAvantGardeStdDemi.otf); 16 | } 17 | -------------------------------------------------------------------------------- /backend/helpers/encryptKey.ts: -------------------------------------------------------------------------------- 1 | import Cryptr from 'cryptr'; 2 | import dotenv from 'dotenv'; 3 | dotenv.config(); 4 | 5 | const secretKey: string | undefined = process.env.ENCRYPT_SECRET; 6 | const cryptr = new Cryptr(secretKey || 'SECRETKEY*888000099JJJJJJ'); 7 | 8 | export const encryptKey = (key: string): string => { 9 | const encryptedString = cryptr.encrypt(key); 10 | return encryptedString; 11 | }; 12 | 13 | export const decryptKey = (encryptedKey: string): string => { 14 | const decryptedString = cryptr.decrypt(encryptedKey); 15 | return decryptedString; 16 | }; -------------------------------------------------------------------------------- /client/styles/login.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | min-width: 100vw; 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | background: #000000; 13 | } 14 | 15 | .wrap { 16 | width: 30vw; 17 | @media (max-width: 1000px) { 18 | width: 40vw; 19 | }; 20 | @media (max-width: 800px) { 21 | width: 45vw; 22 | }; 23 | @media (max-width: 500px) { 24 | width: 60vw; 25 | }; 26 | @media (max-width: 400px) { 27 | width: 70vw; 28 | }; 29 | } -------------------------------------------------------------------------------- /backend/db/migrations/20220222205626_users_table.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | exports.up = function (knex) { 6 | return knex.schema.createTable('users', (t) => { 7 | t.increments('id').primary().notNullable(); 8 | t.string('email').notNullable().unique(); 9 | t.string('password').notNullable(); 10 | }); 11 | }; 12 | 13 | /** 14 | * @param { import("knex").Knex } knex 15 | * @returns { Promise } 16 | */ 17 | exports.down = function (knex) { 18 | return knex.schema.dropTable('users'); 19 | }; 20 | -------------------------------------------------------------------------------- /client/components/topbar.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { useRouter } from 'next/router'; 3 | import { setToStorage } from '../helpers/localstorage'; 4 | 5 | const Topbar: NextPage = () => { 6 | const router = useRouter(); 7 | 8 | const logout = async () => { 9 | await setToStorage('token', ''); 10 | router.push('/login'); 11 | } 12 | 13 | return ( 14 |
15 |
16 |

Logout

17 |
18 |
19 | ) 20 | } 21 | 22 | export default Topbar; -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /backend/helpers/password.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | 3 | /** 4 | * @class BcryptHelper 5 | */ 6 | 7 | 8 | /** 9 | * this function hashes a password and returns the hash 10 | * @param {*} password 11 | */ 12 | export const hashPassword = (password: string): string => { 13 | const hash = bcrypt.hashSync(password, 10) 14 | return hash 15 | } 16 | 17 | /** 18 | * 19 | * @param {*} password 20 | * @param {*} hash 21 | * @param {*} callback 22 | */ 23 | 24 | export const verifyPassword = (password: string, hash: string | any): boolean => { 25 | return bcrypt.compareSync(password, hash); 26 | }; -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | -------------------------------------------------------------------------------- /backend/utils/validator/user.ts: -------------------------------------------------------------------------------- 1 | import { body } from 'express-validator'; 2 | 3 | const myWhitelist: string = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_#@.'; 4 | 5 | export const createUser = [ 6 | body('email') 7 | .not().isEmpty() 8 | .isEmail() 9 | .ltrim() 10 | .rtrim() 11 | .whitelist(myWhitelist) 12 | .escape() 13 | .withMessage('Email is required'), 14 | body('password') 15 | .not().isEmpty() 16 | .isString() 17 | .ltrim() 18 | .rtrim() 19 | .whitelist(myWhitelist) 20 | .escape() 21 | .isLength({min: 6}) 22 | .withMessage('Password is required'), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/db/migrations/20220504071552_p2sh.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param { import("knex").Knex } knex 3 | * @returns { Promise } 4 | */ 5 | exports.up = function (knex) { 6 | return knex.schema.createTable('p2sh', (t) => { 7 | t.increments('id').primary().notNullable(); 8 | t.integer('userid').notNullable().references('id').inTable('users'); 9 | t.string('address').notNullable().unique(); 10 | t.text('redeem').notNullable(); 11 | }); 12 | }; 13 | 14 | /** 15 | * @param { import("knex").Knex } knex 16 | * @returns { Promise } 17 | */ 18 | exports.down = function (knex) { 19 | return knex.schema.dropTable('p2sh'); 20 | }; 21 | -------------------------------------------------------------------------------- /client/pages/utxos/components/EmptyState.tsx: -------------------------------------------------------------------------------- 1 | import { CollectionIcon } from "@heroicons/react/outline"; 2 | 3 | const EmptyState = () => ( 4 | 16 | ); 17 | 18 | export default EmptyState; 19 | -------------------------------------------------------------------------------- /client/pages/addresses/components/EmptyState.tsx: -------------------------------------------------------------------------------- 1 | import { SwitchHorizontalIcon } from '@heroicons/react/outline'; 2 | 3 | const EmptyState = () => ( 4 | 16 | ); 17 | 18 | export default EmptyState; 19 | -------------------------------------------------------------------------------- /client/components/WalletContext.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DecoratedUtxo, Address } from '../pages/types'; 3 | 4 | export type WalletContextType = { 5 | utxos: DecoratedUtxo[]; 6 | addresses: Address[]; 7 | changeAddresses: Address[]; 8 | walletBalance: number; 9 | }; 10 | 11 | interface WalletValue { 12 | getValue: () => WalletContextType; 13 | }; 14 | 15 | const WalletContext = React.createContext({ 16 | getValue: () => ({ 17 | utxos: [], 18 | addresses: [], 19 | changeAddresses: [], 20 | walletBalance: 0 21 | }) 22 | }); 23 | 24 | export const WalletProvider = WalletContext.Provider; 25 | 26 | export default WalletContext; -------------------------------------------------------------------------------- /client/pages/transactions/components/EmptyState.tsx: -------------------------------------------------------------------------------- 1 | import { SwitchHorizontalIcon } from '@heroicons/react/outline'; 2 | 3 | const EmptyState = () => ( 4 | 16 | ); 17 | 18 | export default EmptyState; -------------------------------------------------------------------------------- /backend/routes/index.ts: -------------------------------------------------------------------------------- 1 | import express, { Router, Request, Response } from 'express'; 2 | import api from './api'; 3 | 4 | const router: Router = express.Router(); 5 | 6 | router.get("/", (req: Request, res: Response): void => { 7 | res.send("This is the index route") 8 | }); 9 | 10 | router.use('/api', api); 11 | 12 | // 404 route 13 | router.all('*', (req: Request, res: Response): void => { 14 | const errorMessage = { 15 | message: 'You are hitting a wrong route, find the valid routes below', 16 | endpoints: { 17 | signup: 'POST /api/v1/auth/signup', 18 | login: 'POST /api/v1/auth/login' 19 | }, 20 | success: false 21 | } 22 | 23 | res.status(404).json(errorMessage) 24 | }) 25 | 26 | export default router; -------------------------------------------------------------------------------- /client/helpers/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse } from "axios"; 2 | 3 | export const BASE_URL = 'http://localhost:8000/api/v1/'; 4 | 5 | axios.defaults.baseURL = BASE_URL; 6 | 7 | export const postWithToken = async (url: string, body: Object, token: string): Promise => { 8 | return axios.post(url, body, { 9 | headers: { 10 | Authorization: `BEARER ${token}`, 11 | 'Content-type': 'application/json' 12 | } 13 | }) 14 | }; 15 | export const getWithToken = async (url: string, token: string): Promise => { 16 | return axios.get(url, { 17 | headers: { 18 | Authorization: `BEARER ${token}`, 19 | 'Content-type': 'application/json' 20 | } 21 | }); 22 | }; 23 | 24 | export default axios; -------------------------------------------------------------------------------- /backend/app.ts: -------------------------------------------------------------------------------- 1 | import express, { Application, Response, Request, NextFunction } from 'express'; 2 | import cors from 'cors'; 3 | import bodyParser from 'body-parser'; 4 | import routes from './routes'; 5 | import dotenv from 'dotenv'; 6 | import { responseError } from './helpers'; 7 | dotenv.config(); 8 | 9 | const app: Application = express(); 10 | 11 | // App middlewares 12 | app.use(cors()); 13 | app.use(bodyParser.json()); 14 | app.use(bodyParser.urlencoded({ extended: false })); 15 | 16 | // Router files 17 | app.use('/', routes); 18 | 19 | // Error handler 20 | app.use((err: Error, req: Request, res: Response, next: NextFunction): void => { 21 | if (err) { 22 | res.locals.message = err.message; 23 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 24 | 25 | responseError(res, 500, err.message); 26 | } 27 | }); 28 | 29 | export default app; -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@heroicons/react": "^1.0.6", 13 | "axios": "^0.26.1", 14 | "bitcoinjs-lib": "^6.0.1", 15 | "formik": "^2.2.9", 16 | "moment": "^2.29.1", 17 | "next": "12.1.0", 18 | "node-sass": "^7.0.1", 19 | "react": "17.0.2", 20 | "react-dom": "17.0.2", 21 | "react-qr-code": "^2.0.4", 22 | "sweetalert": "^2.1.2", 23 | "yup": "^0.32.11" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "17.0.19", 27 | "@types/react": "17.0.39", 28 | "autoprefixer": "^10.4.4", 29 | "eslint": "8.9.0", 30 | "eslint-config-next": "12.1.0", 31 | "postcss": "^8.4.12", 32 | "tailwindcss": "^3.0.23", 33 | "typescript": "4.5.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express"; 2 | import { ValidationError } from "express-validator"; 3 | import { DataResponse, ErrorResponse, ErrorValidationResponse } from "../interfaces"; 4 | 5 | export const responseSuccess = (res: Response, status: number, msg: string, data: any): Response => { 6 | const result: DataResponse = { 7 | msg, 8 | data 9 | } 10 | 11 | return res.status(status).send(result); 12 | }; 13 | 14 | export const responseError = (res: Response, status: number, msg: string): void => { 15 | const result: ErrorResponse = { 16 | msg, 17 | } 18 | 19 | res.status(status).send(result); 20 | }; 21 | 22 | export const responseErrorValidation = (res: Response, status: number, errors: ValidationError[]): void => { 23 | const result: ErrorValidationResponse = { 24 | msg: 'Validation Error', 25 | errors, 26 | } 27 | 28 | res.status(status).send(result); 29 | }; -------------------------------------------------------------------------------- /backend/helpers/auth.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { verifyUser } from './jwt'; 3 | import { responseError } from '.'; 4 | import { User } from '../interfaces/knex'; 5 | import { RequestUser } from '../interfaces'; 6 | interface TokenData { 7 | data: User 8 | }; 9 | 10 | export const authUser = (req: Request, res: Response, next: NextFunction) => { 11 | // check if there is an authorization header 12 | if (!req.headers.authorization) return responseError(res, 503, 'Unauthorized'); 13 | else { 14 | 15 | const token = req.headers.authorization.substring(7); 16 | 17 | verifyUser(token, (err: string, ans: TokenData) => { 18 | if (err) { 19 | return responseError(res, 503, 'Not an authorized user'); 20 | } 21 | 22 | const reqUser = req as RequestUser; 23 | 24 | // set user to session 25 | reqUser.user = ans.data; 26 | 27 | return next(); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /client/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /client/pages/addresses/components/P2shRow.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronRightIcon } from '@heroicons/react/solid'; 2 | 3 | import { P2SHAdress } from '../../types'; 4 | 5 | interface Props { 6 | address: P2SHAdress; 7 | } 8 | 9 | export default function AddressRow({ address }: Props) { 10 | return ( 11 |
  • 12 |
    13 |
    14 |
    15 |
    16 |
    17 |

    18 | {address.address} 19 |

    20 |
    21 |
    22 |
    23 |
    24 |
    29 |
    30 |
    31 |
  • 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /client/pages/types/index.ts: -------------------------------------------------------------------------------- 1 | import { Payment } from 'bitcoinjs-lib'; 2 | import { 3 | BlockstreamAPITransactionResponse, 4 | BlockstreamAPIUtxoResponse, 5 | Vin, 6 | Vout, 7 | } from './blockstream'; 8 | 9 | export interface Address extends Payment { 10 | derivationPath: string; 11 | masterFingerprint: Buffer; 12 | type?: "used" | "unused"; 13 | } 14 | 15 | export interface P2SHAdress { 16 | address: string; 17 | redeem?: Payment 18 | } 19 | export interface DecoratedVin extends Vin { 20 | isMine: boolean; 21 | isChange: boolean; 22 | } 23 | 24 | export interface DecoratedVout extends Vout { 25 | isMine: boolean; 26 | isChange: boolean; 27 | } 28 | 29 | export interface DecoratedTx extends BlockstreamAPITransactionResponse { 30 | vin: DecoratedVin[]; 31 | vout: DecoratedVout[]; 32 | type: "sent" | "received" | "moved"; 33 | } 34 | 35 | export interface DecoratedUtxo extends BlockstreamAPIUtxoResponse { 36 | address: Address; 37 | bip32Derivation: { 38 | masterFingerprint: Buffer; 39 | pubkey: Buffer; 40 | path: string; 41 | }[]; 42 | } -------------------------------------------------------------------------------- /backend/helpers/jwt.ts: -------------------------------------------------------------------------------- 1 | /** Created by Raphael Osaze Eyerin 2 | * On 5th March 2022 3 | * This functions are for jwt actions 4 | */ 5 | 6 | import jwt, {JwtPayload, VerifyErrors} from 'jsonwebtoken'; 7 | import { v4 } from 'uuid'; 8 | import { User } from '../interfaces/knex'; 9 | import dotenv from 'dotenv'; 10 | dotenv.config(); 11 | 12 | const TOKEN_SECRET: any = process.env.TOKEN_SECRET; 13 | 14 | export const signUser = (data: User) => { 15 | const token = jwt.sign({ 16 | data, 17 | exp: Date.now() + (1000 * 60 * 60 * 24), 18 | jwtid: v4() 19 | }, TOKEN_SECRET); 20 | 21 | return token; 22 | }; 23 | 24 | export const verifyUser = (data: string, callback: Function) => { 25 | jwt.verify(data, TOKEN_SECRET, (err: VerifyErrors | null, res: any) => { 26 | if (err) return callback(err, false) 27 | else if ( 28 | res?.exp < Date.now() 29 | ) { 30 | const err = { 31 | status: 403, 32 | message: 'Sorry aunthenticatiom error, try to log in again' 33 | } 34 | 35 | return callback(err, false); 36 | } 37 | return callback(false, res); 38 | }); 39 | }; -------------------------------------------------------------------------------- /backend/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from "express-validator"; 2 | import { Address, BlockstreamAPITransactionResponse, BlockstreamAPIUtxoResponse, Vin, Vout } from "./blockstream"; 3 | import { User } from "./knex"; 4 | import { Request } from "express"; 5 | export interface DataResponse { 6 | msg: string; 7 | data: any; 8 | } 9 | export interface ErrorResponse { 10 | msg: string; 11 | } 12 | export interface ErrorValidationResponse { 13 | msg: string; 14 | errors: ValidationError[]; 15 | } 16 | 17 | export interface RequestUser extends Request { 18 | user: User; 19 | } 20 | export interface DecoratedVin extends Vin { 21 | isMine: boolean; 22 | isChange: boolean; 23 | } 24 | export interface DecoratedVout extends Vout { 25 | isMine: boolean; 26 | isChange: boolean; 27 | } 28 | export interface DecoratedTx extends BlockstreamAPITransactionResponse { 29 | vin: DecoratedVin[]; 30 | vout: DecoratedVout[]; 31 | type: "sent" | "received" | "moved"; 32 | } 33 | export interface DecoratedUtxo extends BlockstreamAPIUtxoResponse { 34 | address: Address; 35 | bip32Derivation: { 36 | masterFingerprint: Buffer; 37 | pubkey: Buffer; 38 | path: string; 39 | }[]; 40 | } -------------------------------------------------------------------------------- /backend/helpers/blockstream-api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | axios.defaults.baseURL = 'https://blockstream.info/testnet/api'; 4 | 5 | import { 6 | Address, 7 | BlockstreamAPITransactionResponse, 8 | BlockstreamAPIUtxoResponse, 9 | } from "../interfaces/blockstream"; 10 | 11 | export const getTransactionsFromAddress = async ( 12 | address: Address 13 | ): Promise => { 14 | const { data } = await axios.get( 15 | `/address/${address.address}/txs` 16 | ); 17 | return data; 18 | }; 19 | 20 | export const getUtxosFromAddress = async ( 21 | address: Address 22 | ): Promise => { 23 | const { data } = await axios.get( 24 | `/address/${address.address}/utxo` 25 | ); 26 | 27 | return data; 28 | }; 29 | 30 | export const getTransactionHex = async (txid: string): Promise => { 31 | const { data } = await axios.get( 32 | `/tx/${txid}/hex` 33 | ); 34 | 35 | return data; 36 | }; 37 | 38 | export const getFeeRates = async (): Promise => { 39 | const { data } = await axios.get(`/fee-estimates`); 40 | 41 | return data; 42 | }; 43 | 44 | export const broadcastTx = async (txHex: string): Promise => { 45 | const { data } = await axios.post(`/tx`, txHex); 46 | 47 | return data; 48 | }; 49 | -------------------------------------------------------------------------------- /client/components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import Link from 'next/link'; 3 | import { useEffect, useState } from 'react'; 4 | 5 | const Sidebar: NextPage = () => { 6 | const [url, setUrl] = useState(''); 7 | useEffect(() => { 8 | if(window) { 9 | const _url = window.location.pathname.replace('/',''); 10 | setUrl(_url); 11 | } 12 | }, []); 13 | 14 | return ( 15 |
    16 |
      17 |
    • Home
    • 18 |
    • Addresses
    • 19 |
    • Utxos
    • 20 |
    • Transactions
    • 21 |
    • Send
    • 22 |
    • Receive
    • 23 |
    • Settings
    • 24 |
    25 |
    26 | ) 27 | } 28 | 29 | export default Sidebar; -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require("tailwindcss/colors"); 2 | 3 | module.exports = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx}', 6 | './components/**/*.{js,ts,jsx,tsx}', 7 | ], 8 | theme: { 9 | colors: { 10 | gray: colors.gray, 11 | red: colors.red, 12 | yellow: colors.amber, 13 | indigo: colors.indigo, 14 | white: colors.white, 15 | orange: colors.orange, 16 | blue: colors.blue, 17 | black: colors.black, 18 | green: colors.green, 19 | purple: colors.purple, 20 | pink: colors.pink, 21 | }, 22 | extend: { 23 | colors: { 24 | brand: { 25 | DEFAULT: '#0747A6', 26 | dark: '#043A8B', 27 | text: '#04275D', 28 | }, 29 | }, 30 | fontFamily: { 31 | heading: ['circular', 'sans-serif', 'Helvetica', 'Arial'], 32 | text: ['circular', '-apple-system', 'sans-serif'], 33 | }, 34 | gridTemplateRows: { 35 | 7: 'repeat(7, minmax(0, 1fr))', 36 | 8: 'repeat(18, minmax(0, 1fr))', 37 | 9: 'repeat(9, minmax(0, 1fr))', 38 | 10: 'repeat(10, minmax(0, 1fr))', 39 | 11: 'repeat(11, minmax(0, 1fr))', 40 | 12: 'repeat(12, minmax(0, 1fr))', 41 | }, 42 | }, 43 | }, 44 | plugins: [], 45 | }; 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FULLSTACK JAVASCRIPT BITCOIN WALLET 2 | 3 | ### A full-stack Non-custodial Bitcoin wallet built with bitcoinjs, and blockstream APIs 4 | 5 | ## Prerequisites 6 | - Bitcoin knowledge 7 | - Nodejs installed 8 | - Typescript knowledge 9 | - Reactjs/Nextjs knowledge 10 | 11 | ## Project structure 12 | 13 | - Client - Reactjs/Nextjs 14 | - Backend - Nodejs/Expressjs 15 | - Database - Postgres 16 | - Database ORM Library - Knex 17 | 18 | NOTE: You need postgres installed to run this application successfully 19 | 20 | ## How To Run 21 | 22 | Clone the repository 23 | 24 | ```git clone https://github.com/elraphty/FullstackBitcoinWallet.git``` 25 | 26 | After cloning the repository, install the dependencies 27 | 28 | ### BACKEND 29 | 30 | ```cd backend``` 31 | 32 | ``` npm install ``` 33 | 34 | - Create a .env file ```touch .env``` 35 | - Copy the placeholders from the .env.sample file into your .env 36 | - Create a local postgres database 37 | - Update the .env variable values with yours 38 | - After creating and updating the .env file 39 | - Run ``` knex migrate:latest ``` (Knex will create and migrate the database schema for you) 40 | - Run ``` npm run dev ``` 41 | 42 | 43 | ### CLIENT 44 | 45 | ```cd client ``` 46 | 47 | ``` yarn install ``` 48 | 49 | ``` npm run dev ``` 50 | 51 | -------------------------------------------------------------------------------- /backend/routes/api/v1/wallet.ts: -------------------------------------------------------------------------------- 1 | import express, { Router } from 'express'; 2 | import { generateMnenomic, generateMasterKeys, generateAddress, getUtxos, getTransactions, createTransactions, broadcastTransaction, getPrivateKey, getPublicKey, generateMultiAddress, getMultiAddress, exportMultiAddress } from '../../../controllers/wallet'; 3 | import { generateKeys, broadcastTx, multiAddress } from '../../../utils/validator/wallet'; 4 | import { authUser } from '../../../helpers/auth'; 5 | 6 | const router: Router = express.Router(); 7 | 8 | router.get('/mnenomic', generateMnenomic); 9 | 10 | router.post('/privatekey', generateKeys, generateMasterKeys); 11 | 12 | router.get('/getaddress', authUser, generateAddress); 13 | 14 | router.post('/createp2shaddress', multiAddress, authUser, generateMultiAddress); 15 | 16 | router.get('/getp2shaddress', authUser, getMultiAddress); 17 | 18 | router.get('/utxos', authUser, getUtxos); 19 | 20 | router.get('/transactions', authUser, getTransactions); 21 | 22 | router.post('/createtransaction', authUser, createTransactions); 23 | 24 | router.post('/broadcasttransaction', authUser, broadcastTx, broadcastTransaction); 25 | 26 | router.get('/privateKey', authUser, getPrivateKey); 27 | 28 | router.get('/publicKey', authUser, getPublicKey); 29 | 30 | router.get('/p2sh', authUser, exportMultiAddress); 31 | 32 | export default router; -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Language and Environment */ 4 | "target": "es2016", 5 | /* Modules */ 6 | "module": "commonjs", /* Specify what module code is generated. */ 7 | // "rootDir": "./", /* Specify the root folder within your source files. */ 8 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module 9 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 10 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 11 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 12 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 13 | 14 | /* Type Checking */ 15 | "strict": true, /* Enable all strict type-checking options. */ 16 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 17 | "typeRoots": [ "./types", "./node_modules/@types"] 18 | }, 19 | "exclude": ["node_modules", "types"] 20 | } 21 | -------------------------------------------------------------------------------- /client/pages/types/blockstream.ts: -------------------------------------------------------------------------------- 1 | export interface Vin { 2 | txid: string; 3 | vout: number; 4 | prevout: { 5 | scriptpubkey: string; 6 | scriptpubkey_asm: string; 7 | scriptpubkey_type: string; 8 | scriptpubkey_address: string; 9 | value: number; 10 | }; 11 | scriptsig: string; 12 | scriptsig_asm: string; 13 | witness: string[]; 14 | is_coinbase: boolean; 15 | sequence: number; 16 | } 17 | 18 | export interface Vout { 19 | scriptpubkey: string; 20 | scriptpubkey_asm: string; 21 | scriptpubkey_type: string; 22 | scriptpubkey_address: string; 23 | value: number; 24 | } 25 | 26 | export interface BlockstreamAPITransactionResponse { 27 | txid: string; 28 | version: number; 29 | locktime: number; 30 | vin: Vin[]; 31 | vout: Vout[]; 32 | size: number; 33 | weight: number; 34 | fee: number; 35 | status: { 36 | confirmed: boolean; 37 | block_height: number; 38 | block_hash: string; 39 | block_time: number; 40 | }; 41 | } 42 | 43 | export interface BlockstreamAPIUtxoResponse { 44 | txid: string; 45 | vout: number; 46 | status: { 47 | confirmed: boolean; 48 | block_height: number; 49 | block_hash: string; 50 | block_time: number; 51 | }; 52 | value: number; 53 | } 54 | 55 | export interface BlockstreamApiFeeEstimatesResponse { 56 | [targetBlocks: string]: number; 57 | } -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "walletbackend", 3 | "version": "1.0.0", 4 | "description": "A fullstack bitcoin wallet application", 5 | "main": "index.ts", 6 | "scripts": { 7 | "test": "mocha -r ts-node/register tests/*.spec.ts --exit", 8 | "dev": "ts-node ./index.ts" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@types/body-parser": "^1.19.2", 14 | "@types/cors": "^2.8.12", 15 | "@types/mocha": "^9.1.0", 16 | "@types/supertest": "^2.0.12", 17 | "mocha": "^9.2.2", 18 | "supertest": "^6.2.2", 19 | "ts-node": "^10.5.0", 20 | "typescript": "^4.5.5" 21 | }, 22 | "dependencies": { 23 | "@types/bcryptjs": "^2.4.2", 24 | "@types/cryptr": "^4.0.1", 25 | "@types/dotenv": "^8.2.0", 26 | "@types/express": "^4.17.13", 27 | "@types/express-validator": "^3.0.0", 28 | "@types/jsonwebtoken": "^8.5.8", 29 | "@types/knex": "^0.16.1", 30 | "@types/pg": "^8.6.4", 31 | "@types/uuid": "^8.3.4", 32 | "axios": "^0.26.0", 33 | "bcryptjs": "^2.4.3", 34 | "bip32": "^3.0.1", 35 | "bip39": "^3.0.4", 36 | "bitcoinjs-lib": "^6.0.1", 37 | "body-parser": "^1.19.2", 38 | "coinselect": "^3.1.12", 39 | "cors": "^2.8.5", 40 | "cryptr": "^6.0.2", 41 | "dotenv": "^16.0.0", 42 | "ecpair": "^2.0.1", 43 | "express": "^4.17.3", 44 | "express-validator": "^6.14.0", 45 | "jsonwebtoken": "^8.5.1", 46 | "knex": "^1.0.3", 47 | "pg": "^8.7.3", 48 | "tiny-secp256k1": "^2.2.1", 49 | "uuid": "^8.3.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /backend/knexfile.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import dotenv from 'dotenv'; 3 | import { Knex } from 'knex'; 4 | 5 | dotenv.config(); 6 | 7 | export const development: Knex.Config = { 8 | client: 'pg', 9 | connection: { 10 | host: '127.0.0.1', 11 | user: process.env.DEV_DB_USER, 12 | password: process.env.DEV_DB_PASS, 13 | database: 'bitcoinwallet' 14 | }, 15 | migrations: { 16 | directory: path.join(__dirname, '/db/migrations') 17 | }, 18 | seeds: { 19 | directory: path.join(__dirname, '/db/seeds') 20 | } 21 | }; 22 | 23 | export const test: Knex.Config = { 24 | client: 'pg', 25 | connection: { 26 | host: '127.0.0.1', 27 | user: process.env.TEST_DB_USER, 28 | password: process.env.TEST_DB_PASS, 29 | database: 'bitcoinwallet' 30 | }, 31 | migrations: { 32 | directory: path.join(__dirname, '/db/migrations') 33 | }, 34 | seeds: { 35 | directory: path.join(__dirname, '/db/seeds') 36 | } 37 | }; 38 | 39 | export const production: Knex.Config = { 40 | client: 'pg', 41 | connection: { 42 | host: process.env.DB_HOST, 43 | user: process.env.DB_USER, 44 | port: 5432, 45 | password: process.env.DB_PASS, 46 | database: process.env.DB_NAME, 47 | ssl: { rejectUnauthorized: false } 48 | }, 49 | // connection: process.env.PRODUCTION_DB, 50 | migrations: { 51 | directory: path.join(__dirname, '/db/migrations') 52 | }, 53 | seeds: { 54 | directory: path.join(__dirname, '/db/seeds') 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /backend/utils/validator/wallet.ts: -------------------------------------------------------------------------------- 1 | import { body } from 'express-validator'; 2 | 3 | const myWhitelist: string = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_#@.'; 4 | 5 | export const generateKeys = [ 6 | body('mnemonic') 7 | .not().isEmpty() 8 | .isString() 9 | .ltrim() 10 | .rtrim() 11 | .whitelist(myWhitelist) 12 | .escape() 13 | .withMessage('Send your 24 characters long mnemonic words'), 14 | body('password') 15 | .not().isEmpty() 16 | .isString() 17 | .ltrim() 18 | .rtrim() 19 | .whitelist(myWhitelist) 20 | .escape() 21 | .withMessage('Password is required'), 22 | body('email') 23 | .not().isEmpty() 24 | .isEmail() 25 | .ltrim() 26 | .rtrim() 27 | .whitelist(myWhitelist) 28 | .escape() 29 | .withMessage('Email is required') 30 | ] 31 | 32 | export const generateAdd = [ 33 | body('publicKey') 34 | .not().isEmpty() 35 | .isString() 36 | .ltrim() 37 | .rtrim() 38 | .escape() 39 | .withMessage('Requires public key') 40 | ] 41 | 42 | export const broadcastTx = [ 43 | body('txHex') 44 | .not().isEmpty() 45 | .isString() 46 | .ltrim() 47 | .rtrim() 48 | .escape() 49 | .withMessage('Requires transaction Hex') 50 | ] 51 | 52 | export const multiAddress = [ 53 | body('publicKeys.*') 54 | .not().isEmpty() 55 | .isString() 56 | .ltrim() 57 | .rtrim() 58 | .escape() 59 | .withMessage('Wrong publicKey data'), 60 | body('signers') 61 | .not().isEmpty() 62 | .isNumeric() 63 | .ltrim() 64 | .rtrim() 65 | .escape() 66 | .withMessage('Signer count must be a number'), 67 | ] -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /client/pages/send/components/TransactionSuccessAlert.tsx: -------------------------------------------------------------------------------- 1 | import { CheckCircleIcon } from '@heroicons/react/solid'; 2 | 3 | interface Props { 4 | txid: string; 5 | } 6 | 7 | const TransactionSuccessAlert = ({ txid }: Props) => { 8 | return ( 9 |
    10 |
    11 |
    12 |
    17 |
    18 |

    19 | Your transaction has been broadcasted! 20 |

    21 |
    22 |

    23 | Congratulations! You have broadcast your transaction to the 24 | bitcoin network. 25 |

    26 |

    Transaction ID: {txid}

    27 |
    28 |
    29 | 39 |
    40 |
    41 |
    42 |
    43 | ); 44 | }; 45 | 46 | export default TransactionSuccessAlert; 47 | -------------------------------------------------------------------------------- /client/pages/addresses/components/MultiSuccess.tsx: -------------------------------------------------------------------------------- 1 | import { CheckCircleIcon } from '@heroicons/react/solid'; 2 | 3 | interface Props { 4 | address: string; 5 | } 6 | 7 | const P2shSuccessAlert = ({ address }: Props) => { 8 | return ( 9 |
    10 |
    11 |
    12 |
    17 |
    18 |

    19 | Your transaction has been broadcasted! 20 |

    21 |
    22 |

    23 | Pay To Script Hash (P2SH) address created successfully. 24 |

    25 |

    Address: {address}

    26 |
    27 |
    28 |
    29 | 30 |
    31 |
    32 |
    33 |
    34 |
    35 | ); 36 | }; 37 | 38 | export default P2shSuccessAlert; -------------------------------------------------------------------------------- /client/pages/utxos/components/UtxoRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DecoratedUtxo } from '../../types'; 3 | 4 | interface Props { 5 | utxo: DecoratedUtxo; 6 | } 7 | 8 | const UtxoRow = ({ utxo }: Props) => { 9 | return ( 10 |
  • 11 | 45 |
  • 46 | ); 47 | }; 48 | 49 | export default UtxoRow; 50 | -------------------------------------------------------------------------------- /backend/interfaces/blockstream.ts: -------------------------------------------------------------------------------- 1 | import { payments } from "bitcoinjs-lib"; 2 | 3 | export interface Address extends payments.Payment { 4 | derivationPath: string; 5 | masterFingerprint: Buffer; 6 | type?: "used" | "unused"; 7 | } 8 | 9 | export interface DecoratedUtxo extends BlockstreamAPIUtxoResponse { 10 | address: Address; 11 | bip32Derivation: { 12 | masterFingerprint: Buffer; 13 | pubkey: Buffer; 14 | path: string; 15 | }[]; 16 | } 17 | 18 | export interface Vin { 19 | txid: string; 20 | vout: number; 21 | prevout: { 22 | scriptpubkey: string; 23 | scriptpubkey_asm: string; 24 | scriptpubkey_type: string; 25 | scriptpubkey_address: string; 26 | value: number; 27 | }; 28 | scriptsig: string; 29 | scriptsig_asm: string; 30 | witness: string[]; 31 | is_coinbase: boolean; 32 | sequence: number; 33 | } 34 | 35 | export interface Vout { 36 | scriptpubkey: string; 37 | scriptpubkey_asm: string; 38 | scriptpubkey_type: string; 39 | scriptpubkey_address: string; 40 | value: number; 41 | } 42 | 43 | export interface BlockstreamAPITransactionResponse { 44 | txid: string; 45 | version: number; 46 | locktime: number; 47 | vin: Vin[]; 48 | vout: Vout[]; 49 | size: number; 50 | weight: number; 51 | fee: number; 52 | status: { 53 | confirmed: boolean; 54 | block_height: number; 55 | block_hash: string; 56 | block_time: number; 57 | }; 58 | } 59 | 60 | export interface BlockstreamAPIUtxoResponse { 61 | txid: string; 62 | vout: number; 63 | status: { 64 | confirmed: boolean; 65 | block_height: number; 66 | block_hash: string; 67 | block_time: number; 68 | }; 69 | value: number; 70 | } 71 | 72 | export interface BlockstreamApiFeeEstimatesResponse { 73 | [targetBlocks: string]: number; 74 | } 75 | 76 | export interface SignedTransactionData { 77 | txHex: string; 78 | txId: string; 79 | } 80 | -------------------------------------------------------------------------------- /client/styles/form.scss: -------------------------------------------------------------------------------- 1 | .form { 2 | @apply w-full; 3 | } 4 | 5 | .form.center { 6 | @apply flex; 7 | @apply flex-col; 8 | @apply justify-center; 9 | } 10 | 11 | .form__content { 12 | @apply flex; 13 | @apply flex-col; 14 | @apply flex-auto; 15 | @apply p-4; 16 | } 17 | 18 | .form_heading { 19 | color: #ffffff; 20 | align-self: flex-start; 21 | font-weight: bolder; 22 | font-size: 1.4rem; 23 | margin-bottom: 25px; 24 | padding: 5px 0px; 25 | font-family: ITC_AVANT_BOLD; 26 | @media (max-width: 800px) { 27 | font-size: 1.25rem; 28 | } 29 | @media (max-width: 500px) { 30 | font-size: 1.2rem; 31 | } 32 | @media (max-width: 400px) { 33 | font-size: 1.1rem; 34 | } 35 | } 36 | 37 | .form__title { 38 | @apply text-4xl; 39 | @apply text-brand-dark; 40 | @apply font-normal; 41 | } 42 | 43 | .inputgroup { 44 | @apply w-full; 45 | @apply h-auto; 46 | @apply flex; 47 | @apply flex-col; 48 | @apply justify-start; 49 | @apply items-start; 50 | @apply space-y-2; 51 | } 52 | 53 | .form__label { 54 | @apply w-full; 55 | @apply font-normal; 56 | @apply text-brand-text; 57 | @apply text-xs; 58 | @apply flex; 59 | @apply justify-between; 60 | @apply capitalize; 61 | } 62 | 63 | .form__input { 64 | @apply px-5; 65 | @apply h-9; 66 | @apply 2xl:h-10; 67 | @apply w-full; 68 | @apply flex; 69 | @apply items-center; 70 | @apply text-xs; 71 | @apply font-normal; 72 | @apply text-brand-text; 73 | @apply border; 74 | @apply border-solid; 75 | @apply border-[#F1F1F1]; 76 | @apply rounded-md; 77 | @apply 2xl:text-sm; 78 | } 79 | 80 | .form__input::placeholder { 81 | @apply text-gray-400; 82 | @apply font-normal; 83 | @apply text-[0.7rem]; 84 | @apply 2xl:text-xs; 85 | @apply font-normal; 86 | } 87 | 88 | .form__input:hover { 89 | @apply border-brand; 90 | } 91 | 92 | .form__input:focus { 93 | @apply ring-2; 94 | @apply border-brand; 95 | @apply outline-none; 96 | @apply ring-brand; 97 | @apply ring-opacity-50; 98 | } 99 | -------------------------------------------------------------------------------- /backend/tests/api.spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import assert from 'assert'; 3 | import app from '../app'; 4 | 5 | beforeEach((done) => { 6 | app.listen(done); 7 | }); 8 | 9 | const walletBase = '/api/v1/wallet'; 10 | 11 | describe('Index route should return 200', function () { 12 | it('responds with a message', function (done) { 13 | request(app) 14 | .get('/') 15 | .expect('Content-Type', /text\/html/) 16 | .end((err, res) => { 17 | if (err) return done(err); 18 | assert.equal(res.statusCode, 200); 19 | assert(res.text.includes('This is the index route')) 20 | return done(); 21 | }); 22 | }); 23 | }); 24 | 25 | describe('A wrong route should return 404', function () { 26 | it('responds with an error message', function (done) { 27 | request(app) 28 | .get('/user') 29 | .set('Accept', 'application/json') 30 | .expect('Content-Type', /application\/json/) 31 | .end((err, res) => { 32 | if (err) return done(err); 33 | assert.equal(res.statusCode, 404); 34 | assert(res.text.includes('You are hitting a wrong route, find the valid routes below')); 35 | return done(); 36 | }); 37 | }); 38 | }); 39 | 40 | 41 | describe('Wallet tests', function () { 42 | it('It should return a mnemonic code', function (done) { 43 | request(app) 44 | .get(`${walletBase}/mnenomic`) 45 | .set('Accept', 'application/json') 46 | .expect('Content-Type', /application\/json/) 47 | .end((err, res) => { 48 | if (err) return done(err); 49 | 50 | assert.equal(res.statusCode, 200); 51 | assert(res.body.msg, 'Successfully generated mnenomic'); 52 | 53 | const mnemonicSplit = res.body.data.split(' '); 54 | assert.equal(mnemonicSplit.length, 24); 55 | 56 | return done(); 57 | }); 58 | }); 59 | }); 60 | 61 | -------------------------------------------------------------------------------- /client/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { useEffect } from 'react'; 3 | import Sidebar from '../components/Sidebar'; 4 | import Topbar from '../components/Topbar'; 5 | import BodyWrap from '../components/BodyWrap'; 6 | 7 | const Dashboard: NextPage = () => { 8 | return ( 9 | <> 10 | 11 | 12 | 13 |

    Hello Bitcoineer! Welcome to my simple wallet, you can

    14 |
    15 |
      16 |
    • Addresses 17 |
        18 |
      1. Generate P2WPKH, and P2PKH addresses, with their change addresses, P2SH can also be generated
      2. 19 |
      20 |
    • 21 |
    • UTXOS 22 |
        23 |
      1. View P2WPKH, and P2PKH Unspent Transaction Outputs (UTXOS)
      2. 24 |
      25 |
    • 26 |
    • Transactions 27 |
        28 |
      1. View P2WPKH, and P2PKH transactions
      2. 29 |
      3. Create transactions with P2WPKH, and P2PKH addresses
      4. 30 |
      31 |
    • 32 |
    • Receive 33 |
        34 |
      1. You can switch between P2WPKH, and P2PKH copy then copy the generated address
      2. 35 |
      36 |
    • 37 |
    • Settings 38 |
        39 |
      1. Copy master public key
      2. 40 |
      3. Export P2SH addreses and Redeem scripts
      4. 41 |
      5. Copy and hide master private key
      6. 42 |
      43 |
    • 44 |
    45 |
    46 |
    47 | 48 | ) 49 | } 50 | 51 | export default Dashboard; -------------------------------------------------------------------------------- /client/components/MobileNav.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { useEffect, useState } from 'react'; 3 | import { useRouter } from 'next/router'; 4 | import { setToStorage } from '../helpers/localstorage'; 5 | import { MenuIcon, XIcon } from "@heroicons/react/outline"; 6 | import Link from 'next/link'; 7 | 8 | const MobileNav: NextPage = () => { 9 | const router = useRouter(); 10 | const [url, setUrl] = useState(''); 11 | const [showNav, setShowNav] = useState(false); 12 | 13 | useEffect(() => { 14 | if (window) { 15 | const _url = window.location.pathname.replace('/', ''); 16 | setUrl(_url); 17 | } 18 | }, []); 19 | 20 | const logout = async () => { 21 | await setToStorage('token', ''); 22 | router.push('/login'); 23 | } 24 | 25 | const toggleMobileNav = () => { 26 | setShowNav(!showNav); 27 | } 28 | 29 | return ( 30 |
    31 |
    32 | 33 |
    34 | 47 |
    48 | ) 49 | } 50 | 51 | export default MobileNav; -------------------------------------------------------------------------------- /client/pages/transactions/components/TransactionRow.tsx: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { CalendarIcon } from '@heroicons/react/outline'; 3 | 4 | import { DecoratedTx } from '../../types'; 5 | 6 | interface Props { 7 | transaction: DecoratedTx; 8 | } 9 | 10 | function classNames(...classes: string[]) { 11 | return classes.filter(Boolean).join(" "); 12 | } 13 | 14 | const TransactionRow = ({ transaction }: Props) => { 15 | return ( 16 |
  • 17 |
    18 |
    19 |
    20 |
    21 | 27 | {transaction.type} 28 | 29 |
    30 |
    31 |
    32 |

    33 | {transaction.txid} 34 |

    35 |

    36 |

    46 |
    47 |
    48 |
    49 |
    50 | 51 | {transaction.vout[1].value.toLocaleString()} sats 52 | 53 |
    54 |
    55 |
    56 |
  • 57 | ); 58 | }; 59 | 60 | export default TransactionRow; 61 | -------------------------------------------------------------------------------- /client/pages/addresses/components/AddressRow.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronRightIcon } from '@heroicons/react/solid'; 2 | import { DotsCircleHorizontalIcon } from '@heroicons/react/outline'; 3 | 4 | import { Address } from '../../types'; 5 | 6 | interface Props { 7 | address: Address; 8 | } 9 | 10 | function classNames(...classes: string[]) { 11 | return classes.filter(Boolean).join(" "); 12 | } 13 | 14 | export default function AddressRow({ address }: Props) { 15 | return ( 16 |
  • 17 |
    18 |
    19 |
    20 |
    21 |
    22 |

    23 | {address.address} 24 |

    25 |

    26 |

    34 |
    35 |
    36 | 46 | {address.type || "Unknown"} 47 | 48 |
    49 |
    50 |
    51 |
    52 |
    57 |
    58 |
    59 |
  • 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /client/pages/send/components/TransactionSummary.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useState } from 'react'; 2 | import { Psbt } from 'bitcoinjs-lib'; 3 | import { DecoratedUtxo } from '../../types'; 4 | import { 5 | SortAscendingIcon, 6 | SortDescendingIcon, 7 | ArrowLeftIcon 8 | } from "@heroicons/react/outline"; 9 | 10 | import TransactionSuccessAlert from './TransactionSuccessAlert'; 11 | 12 | interface Props { 13 | transaction: Psbt; 14 | utxos: DecoratedUtxo[]; 15 | broadcastTx: () => Promise; 16 | tHex: string; 17 | txId: string; 18 | status: string; 19 | setStep: Dispatch> 20 | } 21 | 22 | const TransactionSummary = ({ transaction, utxos, broadcastTx, tHex, status, txId, setStep }: Props) => { 23 | const [btnText, setBtnText] = useState('Broadcast transaction'); 24 | const [btnDis, setBtnDis] = useState(false); 25 | const [error, setError] = useState(''); 26 | 27 | const broadcastTxFromForm = async () => { 28 | setBtnText('Broadcasting transaction....'); 29 | setBtnDis(true); 30 | 31 | broadcastTx().then(res => { 32 | setBtnText('Broadcasted transaction'); 33 | setBtnDis(true); 34 | }) 35 | .catch(e => { 36 | setBtnText('Broadcast transaction'); 37 | setBtnDis(false); 38 | }); 39 | }; 40 | 41 | return ( 42 |
    43 |
    62 | ); 63 | }; 64 | 65 | export default TransactionSummary; 66 | -------------------------------------------------------------------------------- /client/components/bodywrap.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | import { getFromStorage } from '../helpers/localstorage'; 4 | import { useRouter } from 'next/router'; 5 | import Sidebar from './Sidebar'; 6 | import Topbar from './Topbar'; 7 | import MobileNav from './MobileNav'; 8 | import { WalletContextType, WalletProvider } from './WalletContext'; 9 | import { DecoratedUtxo, Address } from '../pages/types'; 10 | import { getWithToken } from '../helpers/axios'; 11 | 12 | const BodyWrap: NextPage = (props) => { 13 | const router = useRouter(); 14 | const [utxos, setUtxos] = useState([]); 15 | const [addresses, setAddresses] = useState([]); 16 | const [changeAddresses, setChangeAddresses] = useState([]); 17 | const [walletBalance, setBalance] = useState(0); 18 | 19 | useEffect(() => { 20 | const token = getFromStorage('token'); 21 | if (!token) { 22 | router.push('/login'); 23 | } 24 | }, [router, utxos]) 25 | 26 | useEffect(() => { 27 | const getData = async () => { 28 | const token = await getFromStorage('token') || ''; 29 | // Get Addresses 30 | const getAddresses = async () => { 31 | const addresses = await getWithToken('wallet/getaddress', token); 32 | // console.log('Addresses ==', addresses); 33 | 34 | setAddresses(addresses.data.data.address); 35 | setChangeAddresses(addresses.data.data.changeAddress); 36 | }; 37 | 38 | // const getUtxos = async () => { 39 | // const utxosRes = await getWithToken('wallet/utxos', token); 40 | 41 | // setUtxos(utxosRes.data.data); 42 | 43 | // const data: DecoratedUtxo[] = utxosRes.data.data; 44 | 45 | // // If the UTXO length is more than 0 46 | // if (data.length > 0) { 47 | // // Loop to sum utxo values 48 | // let balance: number = 0; 49 | 50 | // for (let utxo of utxos) { 51 | // balance += utxo.value; 52 | // } 53 | 54 | // setBalance(balance); 55 | // } 56 | // }; 57 | 58 | 59 | // getUtxos(); 60 | getAddresses(); 61 | } 62 | getData(); 63 | }, [utxos]) 64 | 65 | const getContextValue = useCallback(() => ({ 66 | addresses, 67 | changeAddresses, 68 | utxos, 69 | walletBalance 70 | }), [addresses, changeAddresses, utxos, walletBalance]); 71 | 72 | return ( 73 | <> 74 | 75 | 76 | 77 | 78 |
    79 | {props.children} 80 |
    81 |
    82 | 83 | ) 84 | } 85 | 86 | export default BodyWrap; -------------------------------------------------------------------------------- /backend/controllers/user.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import knex from '../db/knex'; 3 | import { validationResult } from 'express-validator'; 4 | import { responseSuccess, responseErrorValidation, responseError } from '../helpers'; 5 | import { User, UserLogin } from '../interfaces/knex'; 6 | import { hashPassword, verifyPassword } from '../helpers/password'; 7 | import { signUser } from '../helpers/jwt'; 8 | 9 | // Controller for registering user 10 | export const registerUser = async (req: Request, res: Response, next: NextFunction): Promise => { 11 | try { 12 | // Finds the validation errors in this request and wraps them in an object with handy functions 13 | const errors = validationResult(req); 14 | if (!errors.isEmpty()) { 15 | return responseErrorValidation(res, 400, errors.array()); 16 | } 17 | 18 | const email: string = req.body.email; 19 | const pass: string = req.body.password; 20 | 21 | const user: User[] = await knex('users').where({ email }); 22 | 23 | if (user.length > 0) { 24 | return responseError(res, 404, 'User already exists'); 25 | } 26 | 27 | const password: string = hashPassword(pass); 28 | 29 | await knex('users').insert({ email, password }); 30 | 31 | responseSuccess(res, 200, 'Successfully created user', {}); 32 | } catch (err) { 33 | next(err); 34 | } 35 | }; 36 | 37 | // Controller for user login 38 | export const userLogin = async (req: Request, res: Response, next: NextFunction): Promise => { 39 | try { 40 | // Finds the validation errors in this request and wraps them in an object with handy functions 41 | const errors = validationResult(req); 42 | if (!errors.isEmpty()) { 43 | return responseErrorValidation(res, 400, errors.array()); 44 | } 45 | 46 | const email: string = req.body.email; 47 | const pass: string = req.body.password; 48 | 49 | const users: UserLogin[] = await knex('users').where({ email }); 50 | 51 | if (users.length > 0) { 52 | let user = users[0]; 53 | if (!verifyPassword(pass, user.password)) { 54 | return responseError(res, 404, 'Error with login'); 55 | } 56 | 57 | // // delete user password and pk 58 | delete user.password; 59 | delete user.pk; 60 | 61 | const token = signUser(user); 62 | 63 | // Add token to user object 64 | user.token = token; 65 | 66 | return responseSuccess(res, 200, 'Successfully login', user); 67 | } else { 68 | return responseError(res, 404, 'Not a valid user'); 69 | } 70 | } catch (err) { 71 | next(err); 72 | } 73 | }; -------------------------------------------------------------------------------- /client/pages/transactions/index.tsx: -------------------------------------------------------------------------------- 1 | import TransactionRow from './components/TransactionRow'; 2 | import EmptyState from './components/EmptyState'; 3 | import BodyWrap from '../../components/BodyWrap'; 4 | import Loader from '../../components/Loader'; 5 | import { getFromStorage } from '../../helpers/localstorage'; 6 | import { getWithToken } from '../../helpers/axios'; 7 | 8 | import { DecoratedTx } from '../types'; 9 | import { useEffect, useState, useCallback } from 'react'; 10 | 11 | export default function Transactions() { 12 | const [transactions, setTransactions] = useState([]); 13 | const [addressType, setAddressType] = useState('p2wpkh'); 14 | const [isLoading, setIsLoading] = useState(false); 15 | 16 | const getTransactions = useCallback(async () => { 17 | const token = await getFromStorage('token'); 18 | 19 | if (token) { 20 | setIsLoading(true); 21 | 22 | const transRes = await getWithToken(`wallet/transactions?type=${addressType}`, token); 23 | 24 | setTransactions(transRes.data.data); 25 | setIsLoading(false); 26 | } 27 | }, [addressType]); 28 | 29 | useEffect(() => { 30 | getTransactions(); 31 | }, [getTransactions]); 32 | 33 | const switchAddressType = useCallback((_type: string) => { 34 | setAddressType(_type); 35 | getTransactions(); 36 | }, [getTransactions]); 37 | 38 | return ( 39 | 40 | {!isLoading ? ( 41 |
    42 |
    43 |
    44 |
    45 |

    46 | Transactions 47 |

    48 |
    49 | 54 | 57 |
    58 |
    59 | {transactions.length ? ( 60 |
      61 | {transactions.map((transaction) => ( 62 | 66 | ))} 67 |
    68 | ) : ( 69 | 70 | )} 71 |
    72 |
    73 |
    74 |
    75 |
    ) : } 76 | 77 |
    78 | ); 79 | } -------------------------------------------------------------------------------- /backend/helpers/transactions.ts: -------------------------------------------------------------------------------- 1 | import { DecoratedTx, DecoratedVin, DecoratedVout } from "../interfaces"; 2 | import { Address, BlockstreamAPITransactionResponse, Vin, Vout } from "../interfaces/blockstream"; 3 | 4 | const getTxType = ( 5 | tx: BlockstreamAPITransactionResponse, 6 | vin: DecoratedVin[], 7 | vout: DecoratedVout[] 8 | ) => { 9 | const amountIn = sum(vin, true); 10 | const amountOut = sum(vout, true); 11 | 12 | if (amountIn === amountOut + (amountIn > 0 ? tx.fee : 0)) { 13 | return "moved"; 14 | } else { 15 | const feeContribution = amountIn > 0 ? tx.fee : 0; 16 | const netAmount = amountIn - amountOut - feeContribution; 17 | return netAmount > 0 ? "sent" : "received"; 18 | } 19 | }; 20 | 21 | const isVout = (item: Vin | Vout): item is Vout => { 22 | return (item as Vout).value !== undefined; 23 | }; 24 | 25 | const sum = ( 26 | items: (DecoratedVin | DecoratedVout)[], 27 | isMine: boolean, 28 | isChange?: boolean 29 | ) => { 30 | let filtered = items; 31 | if (isMine) filtered = filtered.filter((item) => item.isMine === isMine); 32 | if (isChange) 33 | filtered = filtered.filter((item) => item.isChange === isChange); 34 | let total = filtered.reduce((accum: number, item: Vin | Vout) => { 35 | if (isVout(item)) { 36 | return accum + item.value; 37 | } else { 38 | return accum + item.prevout.value; 39 | } 40 | }, 0); 41 | return total; 42 | }; 43 | 44 | 45 | export const createMap = ( 46 | items: T[], 47 | key: string 48 | ) => { 49 | return items.reduce((map: { [key: string]: T }, object: T) => { 50 | map[object[key]] = object; 51 | return map; 52 | }, {}); 53 | }; 54 | 55 | export const decorateTx = ( 56 | tx: BlockstreamAPITransactionResponse, 57 | externalAddresses: Address[], 58 | changeAddresses: Address[] 59 | ): DecoratedTx => { 60 | const externalMap = createMap(externalAddresses, "address"); 61 | const changeMap = createMap(changeAddresses, "address"); 62 | 63 | const vin: DecoratedVin[] = tx.vin.map((vin) => { 64 | const isChange = !!changeMap[vin.prevout.scriptpubkey_address]; 65 | const isMine = isChange || !!externalMap[vin.prevout.scriptpubkey_address]; 66 | return { ...vin, isChange: isChange, isMine: isMine }; 67 | }); 68 | const vout: DecoratedVout[] = tx.vout.map((vout) => { 69 | const isChange = !!changeMap[vout.scriptpubkey_address]; 70 | const isMine = isChange || !!externalMap[vout.scriptpubkey_address]; 71 | return { ...vout, isChange: isChange, isMine: isMine }; 72 | }); 73 | 74 | const type = getTxType(tx, vin, vout); 75 | 76 | const txCopy: DecoratedTx = { 77 | ...tx, 78 | vin, 79 | vout, 80 | type, 81 | }; 82 | 83 | return txCopy; 84 | }; 85 | 86 | 87 | export const serializeTxs = ( 88 | transactions: BlockstreamAPITransactionResponse[], 89 | addresses: Address[], 90 | changeAddresses: Address[] 91 | ): DecoratedTx[] => { 92 | const filteredTxs = Object.values(createMap(transactions, "txid")); 93 | 94 | const serializedTxs: DecoratedTx[] = []; 95 | 96 | filteredTxs.forEach((transaction) => { 97 | const tx = decorateTx(transaction, addresses, changeAddresses); 98 | serializedTxs.push(tx); 99 | }); 100 | 101 | serializedTxs.sort((a, b) => b.status.block_height - a.status.block_height); 102 | 103 | return serializedTxs; 104 | }; -------------------------------------------------------------------------------- /client/pages/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | DecoratedVin, 4 | DecoratedVout, 5 | DecoratedTx, 6 | } from '../types'; 7 | import {BlockstreamAPITransactionResponse, 8 | Vin, 9 | Vout} from '../types/blockstream'; 10 | 11 | const getTxType = ( 12 | tx: BlockstreamAPITransactionResponse, 13 | vin: DecoratedVin[], 14 | vout: DecoratedVout[] 15 | ) => { 16 | const amountIn = sum(vin, true); 17 | const amountOut = sum(vout, true); 18 | 19 | if (amountIn === amountOut + (amountIn > 0 ? tx.fee : 0)) { 20 | return "moved"; 21 | } else { 22 | const feeContribution = amountIn > 0 ? tx.fee : 0; 23 | const netAmount = amountIn - amountOut - feeContribution; 24 | return netAmount > 0 ? "sent" : "received"; 25 | } 26 | }; 27 | 28 | const isVout = (item: Vin | Vout): item is Vout => { 29 | return (item as Vout).value !== undefined; 30 | }; 31 | 32 | const sum = ( 33 | items: (DecoratedVin | DecoratedVout)[], 34 | isMine: boolean, 35 | isChange?: boolean 36 | ) => { 37 | let filtered = items; 38 | if (isMine) filtered = filtered.filter((item) => item.isMine === isMine); 39 | if (isChange) 40 | filtered = filtered.filter((item) => item.isChange === isChange); 41 | let total = filtered.reduce((accum: number, item: Vin | Vout) => { 42 | if (isVout(item)) { 43 | return accum + item.value; 44 | } else { 45 | return accum + item.prevout.value; 46 | } 47 | }, 0); 48 | return total; 49 | }; 50 | 51 | export const createMap = ( 52 | items: T[], 53 | key: string 54 | ) => { 55 | return items.reduce((map: { [key: string]: T }, object: T) => { 56 | map[object[key]] = object; 57 | return map; 58 | }, {}); 59 | }; 60 | 61 | export const serializeTxs = ( 62 | transactions: BlockstreamAPITransactionResponse[], 63 | addresses: Address[], 64 | changeAddresses: Address[] 65 | ): DecoratedTx[] => { 66 | const filteredTxs = Object.values(createMap(transactions, "txid")); 67 | 68 | const serializedTxs: DecoratedTx[] = []; 69 | 70 | filteredTxs.forEach((transaction) => { 71 | const tx = decorateTx(transaction, addresses, changeAddresses); 72 | serializedTxs.push(tx); 73 | }); 74 | 75 | serializedTxs.sort((a, b) => b.status.block_height - a.status.block_height); 76 | 77 | return serializedTxs; 78 | }; 79 | 80 | export const decorateTx = ( 81 | tx: BlockstreamAPITransactionResponse, 82 | externalAddresses: Address[], 83 | changeAddresses: Address[] 84 | ): DecoratedTx => { 85 | const externalMap = createMap(externalAddresses, "address"); 86 | const changeMap = createMap(changeAddresses, "address"); 87 | 88 | const vin: DecoratedVin[] = tx.vin.map((vin) => { 89 | const isChange = !!changeMap[vin.prevout.scriptpubkey_address]; 90 | const isMine = isChange || !!externalMap[vin.prevout.scriptpubkey_address]; 91 | return { ...vin, isChange: isChange, isMine: isMine }; 92 | }); 93 | const vout: DecoratedVout[] = tx.vout.map((vout) => { 94 | const isChange = !!changeMap[vout.scriptpubkey_address]; 95 | const isMine = isChange || !!externalMap[vout.scriptpubkey_address]; 96 | return { ...vout, isChange: isChange, isMine: isMine }; 97 | }); 98 | 99 | const type = getTxType(tx, vin, vout); 100 | 101 | const txCopy: DecoratedTx = { 102 | ...tx, 103 | vin, 104 | vout, 105 | type, 106 | }; 107 | 108 | return txCopy; 109 | }; 110 | -------------------------------------------------------------------------------- /client/pages/utxos/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useCallback } from 'react'; 2 | import { DecoratedUtxo } from '../types'; 3 | import EmptyState from './components/EmptyState'; 4 | import UtxoRow from './components/UtxoRow'; 5 | import BodyWrap from '../../components/BodyWrap'; 6 | import Loader from '../../components/Loader'; 7 | import { getFromStorage } from '../../helpers/localstorage'; 8 | import { getWithToken } from '../../helpers/axios'; 9 | 10 | export default function Utxos() { 11 | const [utxos, setUtxos] = useState([]); 12 | const [walletBalance, setWalletBalance] = useState(0); 13 | const [isLoading, setIsLoading] = useState(false); 14 | const [addressType, setAddressType] = useState('p2wpkh'); 15 | 16 | const getUtxos = useCallback(async () => { 17 | const token = await getFromStorage('token'); 18 | 19 | if (token) { 20 | setIsLoading(true); 21 | 22 | const utxosRes = await getWithToken(`wallet/utxos?type=${addressType}`, token); 23 | 24 | setUtxos(utxosRes.data.data); 25 | setIsLoading(false); 26 | 27 | const data: DecoratedUtxo[] = utxosRes.data.data; 28 | // If the UTXO length is more than 0 29 | if (data.length > 0) { 30 | // Loop to sum utxo values 31 | let balance: number = 0; 32 | data.forEach(utxo => { 33 | balance += utxo.value; 34 | }); 35 | 36 | setWalletBalance(balance); 37 | } 38 | } 39 | }, [addressType]); 40 | 41 | useEffect(() => { 42 | getUtxos(); 43 | }, [addressType, getUtxos]); 44 | 45 | const switchAddressType = useCallback((_type: string) => { 46 | setAddressType(_type); 47 | getUtxos(); 48 | }, [getUtxos]); 49 | 50 | return ( 51 | 52 | { 53 | !isLoading ? ( 54 |
    55 |
    56 |
    57 |
    58 |

    UTXOs

    59 | 60 |
    61 | 66 | 69 |
    70 | 71 |

    Wallet balance - {walletBalance / 100000000} BTC

    72 |
    73 | {utxos.length ? ( 74 |
      75 | {utxos.map((utxo, i) => ( 76 | 77 | ))} 78 |
    79 | ) : ( 80 | 81 | )} 82 |
    83 |
    84 |
    85 |
    86 |
    87 | ) : 88 | } 89 |
    90 | ); 91 | } -------------------------------------------------------------------------------- /client/pages/send/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Psbt } from 'bitcoinjs-lib'; 3 | import BodyWrap from '../../components/BodyWrap'; 4 | 5 | import CreateTxForm from './components/CreateTxForm'; 6 | import TransactionSummary from './components/TransactionSummary'; 7 | 8 | import { DecoratedUtxo } from '../types'; 9 | import { postWithToken, getWithToken } from '../../helpers/axios'; 10 | import { getFromStorage } from '../../helpers/localstorage'; 11 | 12 | export default function Send() { 13 | const [step, setStep] = useState(0); 14 | const [transaction, setTransaction] = useState(undefined); 15 | const [error, setError] = useState(''); 16 | const [tHex, setTHex] = useState(''); 17 | const [txId, setTxID] = useState(''); 18 | const [status, setStatus] = useState(''); 19 | const [utxos, setUtxos] = useState([]); 20 | 21 | useEffect(() => { 22 | const getUtxos = async () => { 23 | const token = await getFromStorage('token'); 24 | 25 | if (token) { 26 | const utxosRes = await getWithToken('wallet/utxos', token); 27 | 28 | setUtxos(utxosRes.data.data); 29 | } 30 | }; 31 | 32 | getUtxos(); 33 | }, []); 34 | 35 | const createTransactionWithFormValues = async ( 36 | recipientAddress: string, 37 | amountToSend: number, 38 | type: string, 39 | ) => { 40 | const token = await getFromStorage('token'); 41 | try { 42 | if (token) { 43 | const body = { 44 | recipientAddress, 45 | amount: Number(amountToSend) 46 | } 47 | 48 | const res = await postWithToken(`wallet/createtransaction?type=${type}`, body, token); 49 | 50 | setTransaction(res.data.data.transaction.data); 51 | setTHex(res.data.data.tHex.txHex); 52 | setStep(1); 53 | 54 | } 55 | } catch (e) { 56 | setError((e as Error).message); 57 | } 58 | }; 59 | 60 | const broadcastTx = async (): Promise => { 61 | const token = await getFromStorage('token'); 62 | let txHex: string = ''; 63 | if (token) { 64 | const body = { 65 | txHex: tHex, 66 | } 67 | postWithToken('wallet/broadcasttransaction', body, token) 68 | .then(res => { 69 | setStatus('success'); 70 | setTxID(res.data.data); 71 | }) 72 | .catch(e => { 73 | setStatus('error'); 74 | }); 75 | 76 | } 77 | return txHex; 78 | }; 79 | 80 | return ( 81 | 82 |
    83 |
    84 |
    85 |
    86 | {step === 0 && ( 87 | 91 | )} 92 | {step === 1 && ( 93 | 102 | )} 103 |
    104 |
    105 |
    106 |
    107 |
    108 | ); 109 | } -------------------------------------------------------------------------------- /client/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import Head from 'next/head'; 3 | import { useCallback, useEffect, useMemo, useState } from 'react'; 4 | import { Formik, Form } from 'formik'; 5 | import { useRouter } from 'next/router'; 6 | import * as Yup from "yup"; 7 | import Link from 'next/link'; 8 | import axios from 'axios'; 9 | import { BASE_URL } from '../helpers/axios'; 10 | import { setToStorage, getFromStorage } from '../helpers/localstorage'; 11 | 12 | export type LoginFormValues = { 13 | email: string; 14 | password: string; 15 | }; 16 | 17 | const validationSchema = Yup.object().shape({ 18 | email: Yup.string().required('This field is required!').email('Input a valid email'), 19 | password: Yup.string().required('This field is required!').min(6, 'Password must be up to six(6) characters') 20 | }); 21 | 22 | const Login: NextPage = () => { 23 | const router = useRouter(); 24 | const [loginError, setLoginError] = useState(''); 25 | 26 | useEffect(() => { 27 | const token = getFromStorage('token'); 28 | if(token) { 29 | router.push('/'); 30 | } 31 | }, [router]) 32 | 33 | const inputClassName = useMemo( 34 | () => 35 | "px-5 h-9 2xl:h-10 w-full flex items-center text-xs font-normal text-brand-text border border-solid border-[#F1F1F1] rounded-lg 2xl:text-sm", 36 | [], 37 | ); 38 | 39 | const initialValues = useMemo( 40 | (): LoginFormValues => ({ 41 | email: '', 42 | password: '', 43 | }), 44 | [], 45 | ); 46 | 47 | const formSubmit = useCallback((values: LoginFormValues, { setSubmitting }) => { 48 | const body: LoginFormValues = { 49 | email: values.email, 50 | password: values.password 51 | }; 52 | 53 | axios.post(`${BASE_URL}user/login`, body) 54 | .then(async res => { 55 | await setToStorage('token', res.data.data.token); 56 | router.push('/'); 57 | }) 58 | .catch(err => { 59 | setLoginError('Could not login, username or password is incorrect'); 60 | }); 61 | 62 | setSubmitting(false); 63 | }, [router]); 64 | 65 | return ( 66 |
    67 | 68 | Bitcoin wallet 69 | 70 | 71 | 72 | 73 |
    74 |
    75 |

    User Login

    76 |
    77 | {({ values, errors, isSubmitting, handleChange }) => ( 78 |
    79 | {loginError?

    {loginError}

    : null} 80 |
    81 |
    82 | 85 |
    86 | 94 |
    95 |
    96 | {errors.email ?

    {errors.email}

    : null} 97 |
    98 |
    99 |
    100 | 103 |
    104 | 112 |
    113 |
    114 | {errors.password ?

    {errors.password}

    : null} 115 |
    116 |
    117 | 123 |
    124 |

    {"Don't have an account? Signup"}

    125 |
    126 | )}
    127 |
    128 |
    129 | ) 130 | } 131 | 132 | export default Login 133 | -------------------------------------------------------------------------------- /client/pages/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState, useContext, useCallback } from 'react'; 2 | import BodyWrap from '../../components/BodyWrap'; 3 | import { getFromStorage } from '../../helpers/localstorage'; 4 | import { getWithToken } from '../../helpers/axios'; 5 | 6 | const defaulPrivKey = '***************************************************************************************************************'; 7 | 8 | export default function Settings() { 9 | const [privKey, setPrivKey] = useState(defaulPrivKey); 10 | const [pubKey, setPubKey] = useState('xpubFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'); 11 | const [p2sh, setP2sh] = useState([]); 12 | 13 | useEffect(() => { 14 | const getPublicKey = async () => { 15 | const token = await getFromStorage('token'); 16 | 17 | if (token) { 18 | const addresses = await getWithToken('wallet/publicKey', token); 19 | setPubKey(addresses.data.data); 20 | } 21 | }; 22 | 23 | const getP2sh = async () => { 24 | const token = await getFromStorage('token'); 25 | 26 | if (token) { 27 | const p2sh = await getWithToken('wallet/p2sh', token); 28 | setP2sh(p2sh.data.data); 29 | } 30 | }; 31 | 32 | getPublicKey(); 33 | getP2sh(); 34 | }, []); 35 | 36 | const getPrivKey = useCallback(async () => { 37 | const token = await getFromStorage('token'); 38 | 39 | if (token) { 40 | const addresses = await getWithToken('wallet/privateKey', token); 41 | setPrivKey(addresses.data.data); 42 | navigator.clipboard.writeText( 43 | addresses.data.data 44 | ); 45 | } 46 | }, []); 47 | 48 | return ( 49 | 50 |
    51 |
    52 |
    53 |

    Public Key

    54 |