├── .gitignore
├── now.json
├── README.md
├── lib
├── functions
│ ├── clear.js
│ ├── getAddress.js
│ ├── getCoins.js
│ ├── sync.js
│ ├── showCoins.js
│ ├── createTransaction.js
│ ├── info.js
│ ├── help.js
│ └── index.js
├── modules
│ ├── index.js
│ ├── log.js
│ ├── code.js
│ ├── coins.js
│ ├── sync.js
│ └── createTransaction.js
├── notebook.js
├── blockchain.js
└── keyfile.js
├── scripts
└── deploy.sh
├── components
├── wallet
│ ├── ItemContainer.js
│ └── Items.js
├── Button.js
├── Text.js
├── Layout.js
└── Form.js
├── pages
├── index.js
├── wallet.js
├── login.js
└── create.js
├── .babelrc
├── next.config.js
├── LICENSE
├── package.json
└── test
└── lib
├── notebook.test.js
├── keyfile.test.js
└── blockchain.test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .next
3 | out
4 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "alias": ["dcoinwallet.com"]
3 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DCoinWallet
2 |
3 | "A Bitcoin Wallet" for power users.
4 |
--------------------------------------------------------------------------------
/lib/functions/clear.js:
--------------------------------------------------------------------------------
1 | export default (api) => {
2 | api.removeAll()
3 | }
4 |
--------------------------------------------------------------------------------
/lib/functions/getAddress.js:
--------------------------------------------------------------------------------
1 | export default (api) => {
2 | return api.keyfile.getAddress()
3 | }
4 |
--------------------------------------------------------------------------------
/lib/functions/getCoins.js:
--------------------------------------------------------------------------------
1 | export default (api) => {
2 | return api.blockchain.getCoins()
3 | }
4 |
--------------------------------------------------------------------------------
/scripts/deploy.sh:
--------------------------------------------------------------------------------
1 | npm run build
2 | npm run export
3 | cp now.json ./out/now.json
4 | cd out
5 | now
6 | now alias
--------------------------------------------------------------------------------
/lib/functions/sync.js:
--------------------------------------------------------------------------------
1 | export default (api) => {
2 | setTimeout(() => {
3 | api.addItem({ type: 'sync' })
4 | }, 0)
5 | }
6 |
--------------------------------------------------------------------------------
/lib/functions/showCoins.js:
--------------------------------------------------------------------------------
1 | export default (api) => {
2 | setTimeout(() => {
3 | api.addItem({ type: 'coins' })
4 | }, 10)
5 | }
6 |
--------------------------------------------------------------------------------
/lib/functions/createTransaction.js:
--------------------------------------------------------------------------------
1 | export default (api, to, amount, options = {}) => {
2 | setTimeout(() => {
3 | api.addItem({
4 | type: 'createTransaction',
5 | to,
6 | amount,
7 | options
8 | })
9 | }, 0)
10 | }
11 |
--------------------------------------------------------------------------------
/lib/modules/index.js:
--------------------------------------------------------------------------------
1 | import code from './code'
2 | import log from './log'
3 | import coins from './coins'
4 | import sync from './sync'
5 | import createTransaction from './createTransaction'
6 |
7 | export default {
8 | code,
9 | log,
10 | coins,
11 | sync,
12 | createTransaction
13 | }
14 |
--------------------------------------------------------------------------------
/components/wallet/ItemContainer.js:
--------------------------------------------------------------------------------
1 | export default ({ children }) => (
2 |
3 | { children }
4 |
12 |
13 | )
14 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 |
3 | export default () => (
4 |
15 | )
16 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "root-import"
4 | ],
5 | "env": {
6 | "development": {
7 | "presets": "next/babel"
8 | },
9 | "production": {
10 | "presets": "next/babel"
11 | },
12 | "test": {
13 | "presets": [
14 | ["env", { "modules": "commonjs" }],
15 | "next/babel"
16 | ]
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/modules/log.js:
--------------------------------------------------------------------------------
1 | import Container from '~/components/wallet/ItemContainer'
2 |
3 | export default ({ data }) => (
4 |
5 |
6 | {data.text}
7 |
8 |
15 |
16 | )
17 |
--------------------------------------------------------------------------------
/lib/functions/info.js:
--------------------------------------------------------------------------------
1 | export default (api) => {
2 | const { payload } = api.keyfile
3 | const text = `
4 | * Wallet Name: ${payload.name}
5 | * Network: ${payload.meta.network}
6 | * No. of keys: ${payload.meta.keyCount}
7 | * Reuse keys: ${String(payload.meta.allowReuse)}
8 | `.trim()
9 |
10 | setTimeout(() => {
11 | api.addItem({ type: 'log', text })
12 | }, 10)
13 | }
14 |
--------------------------------------------------------------------------------
/lib/functions/help.js:
--------------------------------------------------------------------------------
1 | export default (api) => {
2 | const text = `
3 | * help()- get this help message
4 | * info()- get information about the wallet
5 | * getAddress()- get an address from the wallet
6 | * showCoins()- show all upspent coins
7 | * getCoins()- get all unspent coins
8 | * crateTransaction(toAddress, amount)- create a transaction
9 | `.trim()
10 |
11 | setTimeout(() => {
12 | api.addItem({ type: 'log', text })
13 | }, 10)
14 | }
15 |
--------------------------------------------------------------------------------
/lib/functions/index.js:
--------------------------------------------------------------------------------
1 | import info from './info'
2 | import getAddress from './getAddress'
3 | import getCoins from './getCoins'
4 | import showCoins from './showCoins'
5 | import sync from './sync'
6 | import clear from './clear'
7 | import createTransaction from './createTransaction'
8 | import help from './help'
9 |
10 | export default {
11 | info,
12 | getAddress,
13 | getCoins,
14 | showCoins,
15 | sync,
16 | clear,
17 | createTransaction,
18 | help
19 | }
20 |
--------------------------------------------------------------------------------
/components/Button.js:
--------------------------------------------------------------------------------
1 | export default (props) => (
2 |
3 | {props.children}
4 |
19 |
20 | )
21 |
--------------------------------------------------------------------------------
/components/Text.js:
--------------------------------------------------------------------------------
1 | export const H1 = ({children}) => (
2 |
3 | { children }
4 |
10 |
11 | )
12 |
13 | export const Information = ({ children }) => (
14 |
15 | {children}
16 |
24 |
25 | )
26 |
--------------------------------------------------------------------------------
/components/Layout.js:
--------------------------------------------------------------------------------
1 | export default ({ children }) => (
2 |
3 | {children}
4 |
13 |
14 |
20 |
21 | )
22 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | webpack (cfg) {
3 | // Bitcoin lib is not working well when we are uglifying.
4 | // So, we need to stop doing that.
5 | cfg.plugins = cfg.plugins.filter((plugin) => {
6 | if (plugin.constructor.name === 'UglifyJsPlugin') {
7 | return false
8 | } else {
9 | return true
10 | }
11 | })
12 | return cfg
13 | },
14 |
15 | exportPathMap: function () {
16 | return {
17 | '/': { page: '/' },
18 | '/login': { page: '/login' },
19 | '/create': { page: '/create' },
20 | '/wallet': { page: '/login' }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/components/wallet/Items.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Modules from '~/lib/modules'
3 |
4 | export default class Items extends React.Component {
5 | state = { items: [] }
6 |
7 | componentDidMount () {
8 | const { notebook } = this.props
9 | this.stopWatching = notebook.onChange(() => {
10 | const items = notebook.getItems()
11 | this.setState({ items })
12 | })
13 | }
14 |
15 | componentWillUnmount () {
16 | this.stopWatching()
17 | }
18 |
19 | componentDidUpdate () {
20 | window.scrollTo(0, document.body.scrollHeight)
21 | }
22 |
23 | render () {
24 | const { items } = this.state
25 | const { notebook } = this.props
26 |
27 | return (
28 |
29 | { items.map((item, index) => {
30 | const Module = Modules[item.type]
31 | if (!Module) {
32 | throw new Error(`Incorrect item type: ${item.type}`)
33 | }
34 | return ( )
35 | }) }
36 |
37 | )
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Arunoda Susiripala
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dcoinwallet",
3 | "version": "0.0.0",
4 | "repository": "https://github.com/arunoda/dcoinwallet.git",
5 | "author": "Arunoda Susiripala ",
6 | "license": "MIT",
7 | "scripts": {
8 | "lint": "standard",
9 | "testonly": "jest",
10 | "test": "standard && jest",
11 | "dev": "next",
12 | "build": "next build",
13 | "export": "next export"
14 | },
15 | "dependencies": {
16 | "aes-js": "^3.1.0",
17 | "babel-eslint": "^7.2.3",
18 | "babel-plugin-root-import": "^5.1.0",
19 | "babel-preset-env": "^1.6.0",
20 | "bcryptjs": "^2.4.3",
21 | "bitcoinjs-lib": "^3.1.1",
22 | "create-hmac": "^1.1.6",
23 | "isomorphic-unfetch": "^2.0.0",
24 | "next": "^3.0.3",
25 | "randombytes": "^2.0.5",
26 | "react": "^15.6.1",
27 | "react-dom": "^15.6.1",
28 | "standard": "^10.0.2",
29 | "unfetch": "^3.0.0",
30 | "vm-browserify": "^0.0.4"
31 | },
32 | "devDependencies": {
33 | "babel-jest": "^20.0.3",
34 | "jest": "^20.0.4",
35 | "regenerator-runtime": "^0.10.5"
36 | },
37 | "standard": {
38 | "parser": "babel-eslint",
39 | "ignore": [
40 | "**/node_modules/**"
41 | ]
42 | },
43 | "engines": {
44 | "node": "8.x.x"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/lib/modules/code.js:
--------------------------------------------------------------------------------
1 | import Container from '~/components/wallet/ItemContainer'
2 |
3 | function resultToText (result) {
4 | if (typeof result === 'string') return result
5 | return JSON.stringify(result, null, 2)
6 | }
7 |
8 | export default ({ data }) => (
9 |
10 |
11 | {data.code}
12 |
13 | {
14 | data.result ? (
15 |
16 |
RESULT:
17 |
18 | {resultToText(data.result)}
19 |
20 |
21 | ) : null
22 | }
23 |
50 |
51 | )
52 |
--------------------------------------------------------------------------------
/lib/modules/coins.js:
--------------------------------------------------------------------------------
1 | import Container from '~/components/wallet/ItemContainer'
2 |
3 | function getAddressUrl (network, address) {
4 | const type = network === 'testnet' ? 'tBTC' : 'BTC'
5 | return `https://www.blocktrail.com/${type}/address/${address}`
6 | }
7 |
8 | export default ({ notebook }) => (
9 |
10 |
11 | {notebook.blockchain.getCoins().map((coin) => (
12 |
13 |
{(coin.value / 100000000)} BTC
14 | in
15 |
16 |
20 | {coin.address}
21 |
22 |
23 | {coin.confirmations === 0 ? (
Unconfirmed ) : null}
24 |
25 | ))}
26 |
27 |
58 |
59 | )
60 |
--------------------------------------------------------------------------------
/components/Form.js:
--------------------------------------------------------------------------------
1 | export const InputField = ({ name, description, children }) => (
2 |
3 |
{ name }
4 |
{ description }
5 | { children }
6 |
17 |
18 | )
19 |
20 | export const Description = ({ children }) => (
21 |
22 | { children }
23 |
34 |
35 | )
36 |
37 | export const Input = ({ handleRef = () => null, ...props }) => (
38 |
39 | handleRef(r)}
42 | />
43 |
50 |
51 | )
52 |
53 | export const Select = ({ options, handleRef = () => null, ...props }) => (
54 |
55 | handleRef(r)}
58 | >
59 | { Object.keys(options).map(value => (
60 | {options[value]}
61 | ))}
62 |
63 |
70 |
71 | )
72 |
73 | export const Submit = ({ children }) => (
74 |
75 | { children }
76 |
81 |
82 | )
83 |
--------------------------------------------------------------------------------
/lib/modules/sync.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Container from '~/components/wallet/ItemContainer'
3 |
4 | export default class Sync extends React.Component {
5 | state = { currentState: 'READY' }
6 |
7 | componentDidMount () {
8 | const { notebook } = this.props
9 | this.setState({ currentState: 'SYNC' })
10 |
11 | notebook.sync()
12 | .then(() => {
13 | this.setState({ currentState: 'READY' })
14 | })
15 | .catch((error) => {
16 | this.setState({ currentState: 'ERROR', error })
17 | })
18 | }
19 |
20 | getContent () {
21 | const { currentState, error } = this.state
22 | switch (currentState) {
23 | case 'SYNC':
24 | return (
25 |
26 | Syncing ...
27 |
32 |
33 | )
34 | case 'READY':
35 | return (
36 |
37 | Synced
38 |
43 |
44 | )
45 | case 'ERROR':
46 | return (
47 |
48 | {error.message}
49 |
54 |
55 | )
56 | }
57 | }
58 |
59 | render () {
60 | return (
61 |
62 |
63 | { this.getContent() }
64 |
72 |
73 |
74 | )
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/test/lib/notebook.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it, expect */
2 |
3 | import { Notebook } from '../../lib/notebook'
4 |
5 | describe('Notebook', () => {
6 | describe('items', () => {
7 | it('should support to listen to changes', (done) => {
8 | const nb = new Notebook(null)
9 | const item = { aa: 10 }
10 |
11 | nb.addItem({ bb: 20 })
12 | nb.onChange(() => {
13 | expect(nb.getItems()[1]).toEqual(item)
14 | done()
15 | })
16 | nb.addItem(item)
17 | })
18 |
19 | it('should support stop listening to changes', (done) => {
20 | const nb = new Notebook(null)
21 | const item = { aa: 10 }
22 | let called = false
23 |
24 | const stop = nb.onChange(() => {
25 | called = true
26 | })
27 | stop()
28 | nb.addItem(item)
29 |
30 | setTimeout(() => {
31 | expect(called).toBe(false)
32 | done()
33 | }, 1000)
34 | })
35 |
36 | it('should support removing all items', (done) => {
37 | const nb = new Notebook(null)
38 | nb.addItem({aa: 10})
39 | nb.addItem({bb: 20})
40 |
41 | expect(nb.getItems().length).toBe(2)
42 | nb.onChange(() => {
43 | expect(nb.getItems()).toEqual([])
44 | done()
45 | })
46 | nb.removeAll()
47 | })
48 | })
49 |
50 | describe('functions', () => {
51 | it('should run a registered function', async (done) => {
52 | const keyfile = { name: 'Super Wallet' }
53 | const nb = new Notebook(keyfile)
54 |
55 | nb.registerFunction('printName', (api) => {
56 | setTimeout(() => {
57 | api.addItem({
58 | type: 'log',
59 | message: api.keyfile.name
60 | })
61 | })
62 | return api.keyfile.name
63 | })
64 |
65 | nb.onChange(() => {
66 | expect(nb.getItems()[0]).toEqual({ type: 'log', message: keyfile.name })
67 | done()
68 | })
69 |
70 | const response = await nb.run(`
71 | printName()
72 | `)
73 | expect(response).toBe(keyfile.name)
74 | })
75 |
76 | it('should catch errors when running', (done) => {
77 | const nb = new Notebook(null)
78 | nb.run('someBadFunction()')
79 | .catch((err) => {
80 | expect(err.message).toMatch(/someBadFunction is not defined/)
81 | done()
82 | })
83 | })
84 | })
85 | })
86 |
--------------------------------------------------------------------------------
/lib/notebook.js:
--------------------------------------------------------------------------------
1 | import vm from 'vm-browserify'
2 | import functionMap from './functions'
3 | import Blockchain from './blockchain'
4 |
5 | export class Notebook {
6 | constructor (keyfile) {
7 | this.keyfile = keyfile
8 | this.items = []
9 | this.onChangeCallbacks = new Set()
10 | this.functionMap = {}
11 | }
12 |
13 | setKeyfile (keyfile) {
14 | this.keyfile = keyfile
15 | this.blockchain = new Blockchain(
16 | this.keyfile.payload.meta.network,
17 | this.keyfile.payload.addresses
18 | )
19 | this.items = []
20 | }
21 |
22 | getItems () {
23 | return JSON.parse(JSON.stringify(this.items))
24 | }
25 |
26 | addItem (item) {
27 | this.items.push(item)
28 | this._fireAll()
29 | }
30 |
31 | removeAll () {
32 | this.items = []
33 | this._fireAll()
34 | }
35 |
36 | async sync () {
37 | await this.blockchain.sync()
38 | this.blockchain.buildCoins()
39 |
40 | // implement allow and disable reuse feature
41 | if (!this.keyfile.payload.meta.allowReuse) {
42 | const transactions = this.blockchain.getTransactions()
43 | transactions.forEach((tx) => {
44 | tx.outputs.forEach((output) => {
45 | this.keyfile.markAddressUsed(output.address)
46 | })
47 | })
48 | }
49 | }
50 |
51 | registerFunction (name, callback) {
52 | this.functionMap[name] = (...args) => {
53 | const api = {
54 | addItem: (item) => this.addItem(item),
55 | removeAll: () => this.removeAll(),
56 | keyfile: this.keyfile,
57 | blockchain: this.blockchain
58 | }
59 | return callback(api, ...args)
60 | }
61 | }
62 |
63 | run (code) {
64 | const context = {
65 | ...this.functionMap,
66 | keyfile: this.keyfile
67 | }
68 |
69 | return new Promise((resolve, reject) => {
70 | try {
71 | const result = vm.runInNewContext(code, context)
72 | resolve(result)
73 | } catch (err) {
74 | reject(err)
75 | }
76 | })
77 | }
78 |
79 | _fireAll () {
80 | this.onChangeCallbacks.forEach(cb => cb())
81 | }
82 |
83 | onChange (cb) {
84 | this.onChangeCallbacks.add(cb)
85 | return () => {
86 | this.onChangeCallbacks.delete(cb)
87 | }
88 | }
89 | }
90 |
91 | const notebook = new Notebook()
92 | Object.keys(functionMap).forEach((name) => {
93 | notebook.registerFunction(name, functionMap[name])
94 | })
95 | export default notebook
96 |
--------------------------------------------------------------------------------
/lib/blockchain.js:
--------------------------------------------------------------------------------
1 | import fetch from 'unfetch'
2 |
3 | export default class Blockchain {
4 | constructor (network, addresses) {
5 | this.addresses = new Set(addresses)
6 | this.network = network
7 | }
8 |
9 | _toCoinId (txHash, outputIndex) {
10 | return `${txHash}::${outputIndex}`
11 | }
12 |
13 | buildCoins () {
14 | const coinMap = new Map()
15 | const usedCoins = {}
16 |
17 | this.transactions.forEach((tx) => {
18 | // If the input has an coin where we own the address, delete the coin.
19 | tx.inputs.forEach((input) => {
20 | const { transaction, index } = input.output
21 | const coinId = this._toCoinId(transaction, index)
22 | usedCoins[coinId] = true
23 | })
24 | })
25 |
26 | this.transactions.forEach((tx) => {
27 | // If the output has an address we own, make it a coin.
28 | tx.outputs.forEach((output, index) => {
29 | // We don't own this coin
30 | if (!this.addresses.has(output.address)) return
31 |
32 | const coinId = this._toCoinId(tx.hash, index)
33 | // We've used this coin
34 | if (usedCoins[coinId]) return
35 |
36 | coinMap.set(coinId, {
37 | transaction: tx.hash,
38 | index,
39 | value: output.value,
40 | address: output.address,
41 | confirmations: tx.confirmations
42 | })
43 | })
44 | })
45 |
46 | this.coins = Array.from(coinMap.values())
47 | }
48 |
49 | // TODO: throttle fetching transactions
50 | async sync () {
51 | const fetchAll = Array.from(this.addresses).map((a) => this.fetchTransaction(a))
52 | const transactionsList = await Promise.all(fetchAll)
53 | const transactions = transactionsList
54 | .reduce((all, txList) => all.concat(txList), [])
55 | .sort((tx1, tx2) => tx1.block.height - tx2.block.height)
56 |
57 | this.transactions = new Set(transactions)
58 | }
59 |
60 | // TODO: Add retrying
61 | // TODO: Add support for pagination
62 | async fetchTransaction (address) {
63 | const networkCode = this.network === 'testnet' ? 'tbtc' : 'btc'
64 | const endpoint = `https://api.blocktrail.com/v1/${networkCode}/address/${address}/transactions?api_key=MY_APIKEY&sort_dir=desc&limit=200`
65 |
66 | const res = await fetch(endpoint)
67 | const { data } = await res.json()
68 |
69 | const transactions = data.map(item => ({
70 | hash: item.hash,
71 | confirmations: item.confirmations,
72 | block: {
73 | hash: item.block_hash,
74 | height: item.block_height
75 | },
76 | timestamp: (new Date(item.time)).getTime(),
77 | inputs: item.inputs.map(input => ({
78 | address: input.address,
79 | output: {
80 | transaction: input.output_hash,
81 | index: input.output_index
82 | },
83 | value: input.value
84 | })),
85 | outputs: item.outputs.map(output => ({
86 | address: output.address,
87 | value: output.value
88 | }))
89 | }))
90 |
91 | return transactions
92 | }
93 |
94 | getCoins () {
95 | return JSON.parse(JSON.stringify(this.coins))
96 | }
97 |
98 | getTransactions () {
99 | return JSON.parse(JSON.stringify(Array.from(this.transactions)))
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/test/lib/keyfile.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it, expect */
2 |
3 | import Keyfile from '../../lib/keyfile'
4 |
5 | describe('Keyfile', () => {
6 | describe('create', () => {
7 | it('should contain necessory fields', () => {
8 | const payload = Keyfile.create('name', 'password', { keyCount: 2 })
9 | expect(payload.meta).toEqual({
10 | network: 'testnet',
11 | allowReuse: true,
12 | keyCount: 2
13 | })
14 | expect(payload.addresses.length).toBe(2)
15 | expect(typeof payload.keyPairs.encrypted).toBe('string')
16 | expect(typeof payload.keyPairs.hash).toBe('string')
17 | expect(typeof payload.salt).toBe('string')
18 | })
19 | })
20 |
21 | describe('getName', () => {
22 | it('should give the provided name of the keyfile', () => {
23 | const payload = Keyfile.create('the-name', 'password', { keyCount: 2 })
24 | const keyfile = Keyfile.import(payload)
25 |
26 | expect(keyfile.getName()).toBe('the-name')
27 | })
28 | })
29 |
30 | describe('getAddress', () => {
31 | describe('with allowReuse', () => {
32 | const payload = Keyfile.create('name', 'password', { keyCount: 2 })
33 | const givenAddresses = {}
34 |
35 | it('should give all the address in a random order', () => {
36 | const keyfile = Keyfile.import(payload)
37 | for (let lc = 0; lc < 100; lc++) {
38 | givenAddresses[keyfile.getAddress()] = true
39 | }
40 |
41 | expect(Object.keys(givenAddresses).length).toBe(2)
42 | })
43 | })
44 |
45 | describe('without allowReuse', () => {
46 | const payload = Keyfile.create('name', 'password', {
47 | keyCount: 2,
48 | allowReuse: false
49 | })
50 |
51 | it('should not give reused addresses', () => {
52 | const keyfile = Keyfile.import(payload)
53 | keyfile.markAddressUsed(payload.addresses[0])
54 |
55 | for (let lc = 0; lc < 100; lc++) {
56 | expect(keyfile.getAddress()).toBe(payload.addresses[1])
57 | }
58 | })
59 |
60 | it('should return null if there are no addresses', () => {
61 | const keyfile = Keyfile.import(payload)
62 | keyfile.markAddressUsed(payload.addresses[0])
63 | keyfile.markAddressUsed(payload.addresses[1])
64 |
65 | expect(keyfile.getAddress()).toBe(null)
66 | })
67 | })
68 | })
69 |
70 | describe('getKey', () => {
71 | const payload = Keyfile.create('name', 'password', {
72 | keyCount: 2,
73 | allowReuse: false
74 | })
75 |
76 | it('should decrypt and get the relevant key', () => {
77 | const keyfile = Keyfile.import(payload)
78 | const address = keyfile.getAddress()
79 | const key = keyfile.getKey('password', address)
80 |
81 | expect(key.getAddress()).toBe(address)
82 | })
83 |
84 | it('should return null if there is no key for the address', () => {
85 | const keyfile = Keyfile.import(payload)
86 | const key = keyfile.getKey('password', 'fake-address')
87 |
88 | expect(key).toBe(null)
89 | })
90 |
91 | it('should throw if the password is incorrect', () => {
92 | const keyfile = Keyfile.import(payload)
93 | const address = keyfile.getAddress()
94 | const run = () => keyfile.getKey('wrong-password', address)
95 |
96 | expect(run).toThrow(/Incorrect password/)
97 | })
98 | })
99 | })
100 |
--------------------------------------------------------------------------------
/lib/keyfile.js:
--------------------------------------------------------------------------------
1 | import bitcoin from 'bitcoinjs-lib'
2 | import randomBytes from 'randombytes'
3 | import AES from 'aes-js'
4 | import createHmac from 'create-hmac'
5 |
6 | export default class Keyfile {
7 | constructor (payload) {
8 | this.payload = payload
9 | this.unusedAddresses = this.payload.addresses.map(a => a)
10 | }
11 |
12 | markAddressUsed (address) {
13 | const index = this.unusedAddresses.indexOf(address)
14 | if (index < 0) return
15 | this.unusedAddresses.splice(index, 1)
16 | }
17 |
18 | getName () {
19 | return this.payload.name
20 | }
21 |
22 | getAddress () {
23 | if (this.unusedAddresses.length === 0) return null
24 |
25 | const index = Math.floor(Math.random() * this.unusedAddresses.length)
26 | const address = this.unusedAddresses[index]
27 |
28 | return address
29 | }
30 |
31 | getKey (password, address) {
32 | const { salt, keyPairs, meta } = this.payload
33 | const keyPairsJson = decrypt(password, salt, keyPairs.encrypted)
34 | const hash = bitcoin.crypto.sha256(keyPairsJson).toString('hex')
35 |
36 | if (hash !== keyPairs.hash) {
37 | throw new Error('Incorrect password')
38 | }
39 |
40 | const keys = JSON.parse(keyPairsJson)
41 | const keyWIF = keys[address]
42 | if (!keyWIF) return null
43 |
44 | return bitcoin.ECPair.fromWIF(keyWIF, bitcoin.networks[meta.network])
45 | }
46 | }
47 |
48 | function generateKey (password, salt) {
49 | const hmac = createHmac('sha256', Buffer.from(salt, 'utf8'))
50 | hmac.update(password)
51 | return hmac.digest()
52 | }
53 |
54 | function encrypt (password, salt, input) {
55 | const key = generateKey(password, salt)
56 | const inputBytes = AES.utils.utf8.toBytes(input)
57 |
58 | const AesCtr = AES.ModeOfOperation.ctr
59 | const aesCtr = new AesCtr(key)
60 | const encryptedBytes = aesCtr.encrypt(inputBytes)
61 |
62 | return AES.utils.hex.fromBytes(encryptedBytes)
63 | }
64 |
65 | function decrypt (password, salt, encryptedHex) {
66 | const key = generateKey(password, salt)
67 | const encryptedBytes = AES.utils.hex.toBytes(encryptedHex)
68 |
69 | const AesCtr = AES.ModeOfOperation.ctr
70 | const aesCtr = new AesCtr(key)
71 | const decryptedBytes = aesCtr.decrypt(encryptedBytes)
72 |
73 | return AES.utils.utf8.fromBytes(decryptedBytes)
74 | }
75 |
76 | Keyfile.create = function (name, password, options = {}) {
77 | const {
78 | keyCount = 100,
79 | network = 'testnet',
80 | allowReuse = true
81 | } = options
82 |
83 | const salt = randomBytes(32).toString('hex')
84 | const addresses = []
85 | const keyPairs = {}
86 |
87 | for (let lc = 0; lc < keyCount; lc++) {
88 | const keyPair = bitcoin.ECPair.makeRandom({
89 | network: bitcoin.networks[network]
90 | })
91 |
92 | const address = keyPair.getAddress()
93 | addresses.push(address)
94 | keyPairs[address] = keyPair.toWIF()
95 | }
96 |
97 | const keyPairsJSON = JSON.stringify(keyPairs)
98 | const encryptedKeyPairs = encrypt(
99 | password, salt, keyPairsJSON
100 | )
101 |
102 | const payload = {
103 | name,
104 | salt,
105 | meta: { network, allowReuse, keyCount },
106 | addresses,
107 | keyPairs: {
108 | encrypted: encryptedKeyPairs,
109 | hash: bitcoin.crypto.sha256(keyPairsJSON).toString('hex')
110 | }
111 | }
112 |
113 | return payload
114 | }
115 |
116 | Keyfile.import = function (payload) {
117 | return new Keyfile(payload)
118 | }
119 |
--------------------------------------------------------------------------------
/pages/wallet.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Layout from '~/components/Layout'
3 | import notebook from '~/lib/notebook'
4 |
5 | import Items from '~/components/wallet/Items'
6 |
7 | export default class Wallet extends React.Component {
8 | state = { error: null, currentState: null }
9 |
10 | componentDidMount () {
11 | if (!notebook.keyfile) {
12 | this.setState({ currentState: 'NO_KEYFILE' })
13 | return
14 | }
15 |
16 | this.setState({ currentState: 'SYNC' })
17 | notebook.sync()
18 | .then(() => {
19 | this.setState({ currentState: 'READY' })
20 | })
21 | .catch((error) => {
22 | this.setState({ currentState: 'ERROR', error })
23 | })
24 | }
25 |
26 | onShellKeyEnter (e) {
27 | // Make sure to clear errors for any keystroke
28 | this.setState({ error: null })
29 | // Only process if this is SHIFT + ENTER
30 | if (!(e.keyCode === 13 && e.shiftKey)) return
31 |
32 | // Do for add the new line.
33 | e.preventDefault()
34 |
35 | const code = this.shell.value
36 | this.shell.value = ''
37 | notebook.run(code)
38 | .then((result) => {
39 | notebook.addItem({
40 | type: 'code',
41 | code,
42 | result
43 | })
44 | })
45 | .catch((error) => {
46 | this.setState({ error })
47 | })
48 | }
49 |
50 | renderPaper () {
51 | const { error } = this.state
52 |
53 | return (
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {error ? (
{error.message}
) : null}
62 |
68 |
69 |
96 |
97 | )
98 | }
99 |
100 | renderError () {
101 | const { error } = this.state
102 | return (
103 |
104 |
Error
105 |
{error.message}
106 |
107 | )
108 | }
109 |
110 | getContent () {
111 | const { currentState } = this.state
112 |
113 | switch (currentState) {
114 | case 'NO_KEYFILE':
115 | return (No Keyfile Found!
)
116 | case 'SYNC':
117 | return (Syncing
)
118 | case 'ERROR':
119 | return this.renderError()
120 | case 'READY':
121 | return this.renderPaper()
122 | }
123 | }
124 |
125 | render () {
126 | return (
127 |
128 | { this.getContent() }
129 |
130 | )
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/test/lib/blockchain.test.js:
--------------------------------------------------------------------------------
1 | /* global describe, it, expect */
2 |
3 | import Blockchain from '../../lib/blockchain'
4 |
5 | describe('Blockchain', () => {
6 | describe('fetchTransaction', () => {
7 | it('should get transactions for a given address', async () => {
8 | const chain = new Blockchain('testnet', [])
9 | const transactions = await chain.fetchTransaction('mgAdp2rShLktCWw2cNELyGBaWqu2Ms1G3u')
10 | const tx = transactions[0]
11 |
12 | expect(tx.confirmations > 0).toBe(true)
13 | delete tx.confirmations
14 |
15 | expect(tx).toEqual({
16 | hash: 'df08e105021fdaf3e2f7859a2af7f45cfe1816b4d685125f3c56b2ff7af195b7',
17 | block: {
18 | hash: '000000000000364426dd97b1930093c94625844de9e311260ccdb60c96fee7c0',
19 | height: 1154932
20 | },
21 | timestamp: 1500969440000,
22 | inputs: [
23 | {
24 | address: 'mgAdp2rShLktCWw2cNELyGBaWqu2Ms1G3u',
25 | output: {
26 | transaction: 'f56ee7315508dc3cbcc337c17954323476d81f621b801ef60d322d0d8914dadc',
27 | index: 0
28 | },
29 | value: 68650000
30 | }
31 | ],
32 | outputs: [
33 | { address: 'mkrLJ8yWxCseEdoY29N2JiwPAiAti6RjVh', value: 68540000 },
34 | { address: 'mnSEDvuEkmu9GtfbjKyYfvBxJ2gR8ZrGTN', value: 100000 }
35 | ]
36 | })
37 | })
38 | })
39 |
40 | describe('sync', () => {
41 | it('should merge all transactions from all addresses', async () => {
42 | const chain = new Blockchain('testnet', ['add1', 'add2'])
43 | let id = 4
44 | chain.fetchTransaction = async function (address) {
45 | return [
46 | { hash: `${address}-t1`, block: { hash: 'a', height: id-- } },
47 | { hash: `${address}-t2`, block: { hash: 'b', height: id-- } }
48 | ]
49 | }
50 |
51 | await chain.sync()
52 |
53 | // It should merge with the ordering block.height with desc. order.
54 | expect(chain.getTransactions()).toEqual([
55 | { hash: `add2-t2`, block: { hash: 'b', height: 1 } },
56 | { hash: `add2-t1`, block: { hash: 'a', height: 2 } },
57 | { hash: `add1-t2`, block: { hash: 'b', height: 3 } },
58 | { hash: `add1-t1`, block: { hash: 'a', height: 4 } }
59 | ])
60 | })
61 | })
62 |
63 | describe('buildCoins', () => {
64 | it('should collect all the coins for own addresses', () => {
65 | const chain = new Blockchain('testnet', ['add1', 'add2'])
66 | chain.transactions = new Set([
67 | {
68 | hash: 'h1',
69 | inputs: [],
70 | outputs: [
71 | { address: 'add1', value: 200 },
72 | { address: 'add10', value: 200 }
73 | ]
74 | },
75 | {
76 | hash: 'h2',
77 | inputs: [],
78 | outputs: [
79 | { address: 'add1', value: 10 },
80 | { address: 'add2', value: 20 }
81 | ]
82 | }
83 | ])
84 |
85 | chain.buildCoins()
86 |
87 | expect(chain.getCoins()).toEqual([
88 | { transaction: 'h1', index: 0, value: 200, address: 'add1' },
89 | { transaction: 'h2', index: 0, value: 10, address: 'add1' },
90 | { transaction: 'h2', index: 1, value: 20, address: 'add2' }
91 | ])
92 | })
93 |
94 | it('should remove spent coins', () => {
95 | const chain = new Blockchain('testnet', ['add1', 'add2'])
96 | chain.transactions = new Set([
97 | {
98 | hash: 'h1',
99 | inputs: [
100 | { output: { transaction: 'kkr', index: 1 } }
101 | ],
102 | outputs: [
103 | { address: 'add1', value: 200 },
104 | { address: 'add10', value: 200 }
105 | ]
106 | },
107 | {
108 | hash: 'h2',
109 | inputs: [
110 | { output: { transaction: 'h1',
111 | index: 0 } }
112 | ],
113 | outputs: [
114 | { address: 'add1', value: 10 },
115 | { address: 'add2', value: 20 }
116 | ]
117 | }
118 | ])
119 |
120 | chain.buildCoins()
121 |
122 | expect(chain.getCoins()).toEqual([
123 | { transaction: 'h2', index: 0, value: 10, address: 'add1' },
124 | { transaction: 'h2', index: 1, value: 20, address: 'add2' }
125 | ])
126 | })
127 | })
128 | })
129 |
--------------------------------------------------------------------------------
/pages/login.js:
--------------------------------------------------------------------------------
1 | /* global window */
2 |
3 | import React from 'react'
4 | import Router from 'next/router'
5 | import Layout from '~/components/Layout'
6 | import { H1, Information } from '~/components/Text'
7 | import { InputField, Description, Input, Submit } from '~/components/Form'
8 | import Button from '~/components/Button'
9 | import Keyfile from '~/lib/keyfile'
10 | import notebook from '~/lib/notebook'
11 |
12 | export default class Login extends React.Component {
13 | state = { currentState: null }
14 |
15 | reset () {
16 | this.setState({
17 | currentState: null,
18 | error: null
19 | })
20 | }
21 |
22 | readKeyfile (file) {
23 | return new Promise((resolve, reject) => {
24 | const { FileReader } = window
25 | const reader = new FileReader()
26 | reader.onload = (e) => {
27 | const text = e.target.result
28 | try {
29 | const keyfileJson = JSON.parse(text)
30 | resolve(keyfileJson)
31 | } catch (ex) {
32 | reject(new Error('Invalid keyfile.'))
33 | }
34 | }
35 | reader.readAsText(file)
36 | })
37 | }
38 |
39 | submit (e) {
40 | e.preventDefault()
41 | const files = this.keyfile.files
42 | const password = this.password.value
43 |
44 | if (files.length === 0) {
45 | window.alert('Select the keyfile.')
46 | return
47 | }
48 |
49 | if (password === '') {
50 | window.alert('Enter the password')
51 | return
52 | }
53 |
54 | this.setState({ currentState: 'PROCESSING' })
55 |
56 | this.readKeyfile(files[0])
57 | .then(keyfileJSON => {
58 | const keyfile = Keyfile.import(keyfileJSON)
59 | const address = keyfile.getAddress()
60 | // An error will be thrown if the password is incorrect.
61 | keyfile.getKey(password, address)
62 |
63 | notebook.setKeyfile(keyfile)
64 | Router.push('/wallet')
65 | })
66 | .catch((error) => {
67 | this.setState({ currentState: 'ERRORED', error })
68 | })
69 | }
70 |
71 | renderForm () {
72 | return (
73 |
106 | )
107 | }
108 |
109 | renderProcessing () {
110 | return (
111 |
112 | Processing ...
113 |
114 | )
115 | }
116 |
117 | renderError () {
118 | const { error } = this.state
119 | return (
120 |
121 |
122 |
ERROR
123 |
{ error.message }
124 |
125 |
126 | this.reset()}>Login Again
127 |
128 |
141 |
142 | )
143 | }
144 |
145 | renderContent () {
146 | switch (this.state.currentState) {
147 | case 'PROCESSING':
148 | return this.renderProcessing()
149 | case 'ERRORED':
150 | return this.renderError()
151 | default:
152 | return this.renderForm()
153 | }
154 | }
155 |
156 | render () {
157 | return (
158 |
159 | Login
160 |
161 | Login to your wallet by providing the keyfile and the password.
162 |
163 | { this.renderContent() }
164 |
165 | )
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/lib/modules/createTransaction.js:
--------------------------------------------------------------------------------
1 | /* global prompt */
2 | import React from 'react'
3 | import Container from '~/components/wallet/ItemContainer'
4 | import bitcoin from 'bitcoinjs-lib'
5 | import fetch from 'unfetch'
6 |
7 | export default class CreateTransaction extends React.Component {
8 | state = {}
9 |
10 | componentDidMount () {
11 | const password = prompt('Enter keyfile password:').trim()
12 | try {
13 | const tx = this.create(password)
14 | this.setState({ currentState: 'READY', tx })
15 | } catch (error) {
16 | this.setState({ error })
17 | }
18 | }
19 |
20 | create (password) {
21 | const { data } = this.props
22 | const amount = data.amount * 100000000
23 | const tx = this.buildTransaction(password, amount, 0)
24 |
25 | // get the feerate dynamically
26 | const totalFees = tx.byteLength() * 300
27 | const actualTx = this.buildTransaction(password, amount, totalFees)
28 |
29 | return actualTx
30 | }
31 |
32 | buildTransaction (password, amount, totalFees = 0) {
33 | const { data, notebook } = this.props
34 | const coins = notebook.blockchain.getCoins()
35 | const confirmedCoins = coins.filter((coin) => coin.confirmations > 0)
36 |
37 | // TODO: check these coins inside a input of a pending transaction
38 |
39 | // pick coins
40 | const pickedCoins = []
41 | let remainingAmount = amount + totalFees
42 | let totalCoinValue = 0
43 |
44 | while (remainingAmount > 0) {
45 | const coin = confirmedCoins.pop()
46 | if (!coin) break
47 | pickedCoins.push(coin)
48 | remainingAmount -= coin.value
49 | totalCoinValue += coin.value
50 | }
51 |
52 | if (remainingAmount >= 0) {
53 | throw new Error(`Not enough balance to send the amount: ${data.amount} BTC`)
54 | }
55 |
56 | // create the transaction payload
57 | const network = bitcoin.networks[notebook.keyfile.payload.meta.network]
58 | const tx = new bitcoin.TransactionBuilder(network)
59 | pickedCoins.forEach(({ transaction, index }) => {
60 | tx.addInput(transaction, index)
61 | })
62 |
63 | tx.addOutput(data.to, amount)
64 | tx.addOutput(notebook.keyfile.getAddress(), totalCoinValue - amount - totalFees)
65 |
66 | pickedCoins.forEach(({ address }, index) => {
67 | const key = notebook.keyfile.getKey(password, address)
68 | tx.sign(index, key)
69 | })
70 |
71 | return tx.build()
72 | }
73 |
74 | send () {
75 | const { notebook } = this.props
76 | const { tx } = this.state
77 | const payload = { hex: tx.toHex() }
78 | const network = notebook.keyfile.payload.meta.network === 'testnet' ? 'tbtc' : 'btc'
79 |
80 | this.setState({ currentState: 'SENDING' })
81 | fetch(`https://${network}.blockr.io/api/v1/tx/push`, {
82 | body: JSON.stringify(payload),
83 | method: 'POST'
84 | })
85 | .then(res => res.json())
86 | .then((payload) => {
87 | if (payload.status === 'success') {
88 | this.setState({ currentState: 'SENT' })
89 | } else {
90 | const error = new Error(JSON.stringify(payload))
91 | this.setState({ error })
92 | }
93 | })
94 | .catch((error) => {
95 | this.setState({ currentState: 'ERROR', error })
96 | })
97 | }
98 |
99 | getAction () {
100 | const { currentState } = this.state
101 | switch (currentState) {
102 | case 'READY':
103 | return (
104 | this.send()}>SEND
105 | )
106 | case 'SENT':
107 | return (SENT
)
108 | case 'SENDING':
109 | return (SENDING ...
)
110 | }
111 | }
112 |
113 | getContent () {
114 | const { error, tx } = this.state
115 | if (error) {
116 | return (
117 |
118 | Error: {error.message}
119 |
125 |
126 | )
127 | }
128 |
129 | if (tx) {
130 | return (
131 |
132 |
{tx.toHex()}
133 |
134 | {this.getAction()}
135 |
136 |
156 |
157 | )
158 | }
159 | }
160 |
161 | render () {
162 | return (
163 |
164 |
165 | { this.getContent() }
166 |
167 |
172 |
173 | )
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/pages/create.js:
--------------------------------------------------------------------------------
1 | /* global window */
2 | import React from 'react'
3 | import Link from 'next/link'
4 | import Router from 'next/router'
5 | import Keyfile from '~/lib/keyfile'
6 | import Layout from '~/components/Layout'
7 | import Button from '~/components/Button'
8 | import { H1, Information } from '~/components/Text'
9 | import { InputField, Description, Input, Select, Submit } from '~/components/Form'
10 |
11 | export default class Create extends React.Component {
12 | state = { currentState: null }
13 |
14 | submit (e) {
15 | e.preventDefault()
16 | const name = this.name.value
17 | const password = this.password.value
18 | const password2 = this.password2.value
19 | const keyCount = parseInt(this.keyCount.value)
20 | const network = this.network.value
21 | const allowReuse = this.reuseKeys.value === 'true'
22 |
23 | if (name === '') {
24 | window.alert('Enter a name for your wallet.')
25 | return
26 | }
27 |
28 | if (password !== password2) {
29 | window.alert('Both passwords are not the same.')
30 | return
31 | }
32 |
33 | if (password === '') {
34 | window.alert('Enter a password for your wallet.')
35 | return
36 | }
37 |
38 | if (!(keyCount > 1)) {
39 | window.alert('Key count should be greater than 1')
40 | return
41 | }
42 |
43 | this.setState({ currentState: 'CREATE' }, () => {
44 | setTimeout(() => {
45 | const payload = Keyfile.create(name, password, { network, keyCount, allowReuse })
46 | this.setState({
47 | currentState: 'WARNING',
48 | payload
49 | })
50 | }, 1000)
51 | })
52 | }
53 |
54 | showDownload () {
55 | this.setState({ currentState: 'DOWNLOAD' })
56 | }
57 |
58 | redirectToLogin () {
59 | setTimeout(() => {
60 | Router.push('/login')
61 | }, 1000)
62 | }
63 |
64 | networkChanged () {
65 | const network = this.network.value
66 | if (network === 'bitcoin') {
67 | this.keyCount.value = 100
68 | this.reuseKeys.value = 'false'
69 | } else {
70 | this.keyCount.value = 10
71 | this.reuseKeys.value = 'true'
72 | }
73 | }
74 |
75 | renderLoading () {
76 | return (
77 |
78 |
Hang tight...
79 |
(It'll take a while to generate addresses and keys for your wallet.)
80 |
86 |
87 | )
88 | }
89 |
90 | renderDownload () {
91 | const { payload } = this.state
92 | const { Blob, URL } = window
93 | const keyFileContent = JSON.stringify(payload, null, 2)
94 | const keyFileBlob = new Blob([keyFileContent], {type: 'application/json'})
95 | const filename = `${payload.name.toLowerCase().replace(/ /g, '-')}-keyfile.json`
96 |
97 | return (
98 |
99 |
105 |
106 | (After the download, you'll be redirected to the login page.)
107 |
108 |
134 |
135 | )
136 | }
137 |
138 | renderForm () {
139 | return (
140 |
216 | )
217 | }
218 |
219 | renderWarning () {
220 | return (
221 |
222 |
223 |
WARNING
224 |
225 | Your keyfile and password is the only entry to your wallet.
226 | Follow these steps for the better security:
227 |
228 |
229 | Always use the wallet at https://dcoinwallet.com .
230 | Save the keyfile in a secure location.
231 | Better if you keep it in a USB drive.
232 | Use a long and secure password. If not, go back .
233 | There is no recovery or reset process.
234 | As a recovery method, put the keyfile USB drive and the password (written in a paper) inside two different physical safes.
235 |
236 |
237 |
238 | this.showDownload()}>Yes, I Got It
239 |
240 |
259 |
260 | )
261 | }
262 |
263 | renderContent () {
264 | const { currentState } = this.state
265 |
266 | switch (currentState) {
267 | case 'CREATE':
268 | return this.renderLoading()
269 | case 'WARNING':
270 | return this.renderWarning()
271 | case 'DOWNLOAD':
272 | return this.renderDownload()
273 | default:
274 | return this.renderForm()
275 | }
276 | }
277 |
278 | render () {
279 | return (
280 |
281 | Create a Wallet
282 |
283 | This process will create a file containing a set of Bitcoin addresses and keys.
284 | They are encrypted with a password you provide.
285 |
286 | { this.renderContent() }
287 |
288 | )
289 | }
290 | }
291 |
--------------------------------------------------------------------------------