├── _config.yml
├── misc
├── requirements.txt
├── test.html
├── genWallet.js
├── test.py
└── when_transaction.py
├── BitShovel.png
├── bitshovel-production
├── nodemon.json
├── docs
├── BitShovel.fund.png
├── Docker.md
└── getstarted.md
├── examples
├── publish.py
├── listen.py
└── publish.go
├── dummyapp.js
├── .vscode
└── launch.json
├── docker-compose.yml
├── shoveladdress.js
├── shovelregistry.js
├── shovelcache.js
├── package.json
├── .gitignore
├── shovellisteners.js
├── shovelwallet.js
├── utils.js
├── Dockerfile
├── shovelbus.js
├── test
└── test.js
├── bitshovel.js
└── README.md
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-minimal
--------------------------------------------------------------------------------
/misc/requirements.txt:
--------------------------------------------------------------------------------
1 | sseclient-py
2 | requests
3 |
--------------------------------------------------------------------------------
/BitShovel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dfoderick/bitshovel/HEAD/BitShovel.png
--------------------------------------------------------------------------------
/bitshovel-production:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | export NODE_ENV=production
3 | npm start
4 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "verbose": true,
3 | "ignore": ["wallet.json","wallet.json.*"]
4 | }
--------------------------------------------------------------------------------
/docs/BitShovel.fund.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dfoderick/bitshovel/HEAD/docs/BitShovel.fund.png
--------------------------------------------------------------------------------
/examples/publish.py:
--------------------------------------------------------------------------------
1 | import redis
2 | bus = redis.Redis()
3 | bus.publish("bitshovel.send",'"Hello from BitShovel! Python"')
4 |
--------------------------------------------------------------------------------
/examples/listen.py:
--------------------------------------------------------------------------------
1 | import redis
2 | bus = redis.Redis().pubsub()
3 | bitshovel_reader = bus.subscribe("bitshovel.watch")
4 |
5 | for message in bus.listen():
6 | print(message)
7 |
--------------------------------------------------------------------------------
/dummyapp.js:
--------------------------------------------------------------------------------
1 | /*
2 | This is a dummy application that never exits. It keeps the Docker container
3 | alive so that a developer can enter the container and debug the real application.
4 | See Dockerfile for usage.
5 | */
6 |
7 | "use strict"
8 |
9 | let i = 0
10 |
11 | setInterval(function() {
12 | i++
13 | }, 30000)
14 |
--------------------------------------------------------------------------------
/examples/publish.go:
--------------------------------------------------------------------------------
1 | package main
2 | import "github.com/go-redis/redis"
3 | import "log"
4 | func main() {
5 | client := redis.NewClient(&redis.Options{
6 | Addr: "localhost:6379",
7 | Password: "", // no password set
8 | DB: 0, // use default DB
9 | })
10 | _, err := client.Publish("bitshovel.send", "Hello from BitShovel! Go").Result()
11 | if err != nil {
12 | log.Fatal(err)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Launch Program",
11 | "program": "${workspaceFolder}\\bitshovel.js"
12 | }
13 | ]
14 | }
--------------------------------------------------------------------------------
/misc/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # Start BitShovel service with the command 'docker-compose up --no-recreate'
2 | # This will prevent docker from wiping the image and forcing it to create a new wallet
3 |
4 | version: '3'
5 |
6 | services:
7 | redis:
8 | image: redis:latest
9 | container_name: bitshovel-redis
10 | ports:
11 | - "127.0.0.1:6379:6379"
12 | #network_mode: host
13 |
14 | bitshovel:
15 | container_name: bitshovel-service
16 | build: .
17 | # links:
18 | # - redis
19 | network_mode: host
20 |
--------------------------------------------------------------------------------
/shoveladdress.js:
--------------------------------------------------------------------------------
1 | const utils = require('./utils')
2 | const bsv = require('bsv')
3 |
4 | let toCashAddress = function toCashAddress(originalAddress) {
5 | const a = new bsv.Address(originalAddress)
6 | const ac = a.toCashAddress()
7 | return ac.replace("bitcoincash:","")
8 | }
9 |
10 | let toOriginalAddress = function toOriginalAddress(cashAddress) {
11 | return (new bsv.Address(cashAddress)).toLegacyAddress()
12 | }
13 |
14 | module.exports = {
15 | toCashAddress: toCashAddress,
16 | toOriginalAddress: toOriginalAddress
17 | }
--------------------------------------------------------------------------------
/shovelregistry.js:
--------------------------------------------------------------------------------
1 | //application registry
2 | //hdwallet
3 | const utils = require('./utils')
4 |
5 | //TODO: could be handled by redis instead
6 | let APPS = {}
7 |
8 | let register = function register (appname, walletaddress) {
9 | APPS[appname] = walletaddress
10 | console.log(`registered ${appname} on ${walletaddress}`)
11 | }
12 |
13 | let has = function has(appname) {
14 | return APPS.hasOwnProperty(appname)
15 | }
16 |
17 | let get = function get(appname) {
18 | return APPS[appname]
19 | }
20 |
21 | module.exports = {
22 | APPS: APPS,
23 | register: register,
24 | has: has,
25 | get: get
26 | }
--------------------------------------------------------------------------------
/shovelcache.js:
--------------------------------------------------------------------------------
1 | const utils = require('./utils')
2 | const redis = require('redis');
3 |
4 | //keys stored in redis
5 | const WALLET_ADDRESS_KEY = 'bitshovel.wallet.address';
6 | const WALLET_PRIVATE_KEY = 'bitshovel.wallet.private';
7 |
8 | let cache
9 |
10 | let start = function start() {
11 | cache = redis.createClient()
12 | }
13 |
14 | //store wallet address in redis
15 | let storeWalletAddress = function storeWalletAddress(address) {
16 | cache.set(WALLET_ADDRESS_KEY, address, function(err) {
17 | console.error(err)
18 | })
19 | }
20 |
21 | module.exports = {
22 | start: start,
23 | storeWalletAddress: storeWalletAddress
24 | }
--------------------------------------------------------------------------------
/misc/genWallet.js:
--------------------------------------------------------------------------------
1 | // generate a wallet.json file
2 | // remember to fund the wallet using the address generated
3 | // the easiest way to do that is go to https://www.moneybutton.com/test
4 | // and use withdraw button
5 | // then review the tx on a block explorer e.g. https://bchsvexplorer.com
6 |
7 | const bsv = require('bsv')
8 | const fs = require('fs');
9 |
10 | var pk = bsv.PrivateKey()
11 | console.log(pk)
12 | var address = new bsv.Address(pk.publicKey, bsv.Networks.mainnet)
13 | console.log(address.toLegacyAddress())
14 |
15 | wallet = {
16 | "wif": pk.toWIF(),
17 | "address": address.toLegacyAddress()
18 | }
19 |
20 | const sWallet = JSON.stringify(wallet, null, 2);
21 | console.log(sWallet)
22 |
23 | fs.writeFile('wallet.json', sWallet, 'utf8', function(err) {
24 | if(err) {
25 | return console.log(err);
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/misc/test.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import base64
4 | import json
5 | import pprint
6 | import requests
7 | import redis
8 |
9 | from sseclient import SSEClient
10 |
11 | def with_requests(url):
12 | """Get a streaming response for the given event feed using requests."""
13 | return requests.get(url, stream=True)
14 |
15 | r = redis.StrictRedis(host="localhost", port=6379, password="", decode_responses=True)
16 | query = {"v": 3, "q": { "find": {} }}
17 | #squery = json.dumps(query)
18 | squery = '{"v": 3, "q": { "find": {} }}'
19 | print(squery)
20 | b64 = base64.encodestring(squery)
21 | print(b64)
22 | url = 'https://bitsocket.org/s/'+b64
23 | response = with_requests(url)
24 | client = SSEClient(response)
25 | for event in client.events():
26 | bsock = json.loads(event.data)
27 | r.publish("bitshovel.watch",event.data)
28 | print(bsock)
29 | #pprint.pprint(bsock)
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bitshovel",
3 | "version": "0.1.0",
4 | "description": "read and write to blockchain in any language instantly",
5 | "main": "bitshovel.js",
6 | "scripts": {
7 | "start": "node bitshovel.js",
8 | "dev": "./node_modules/.bin/nodemon bitshovel.js",
9 | "test": "node node_modules/mocha/bin/mocha"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/dfoderick/bitshovel.git"
14 | },
15 | "author": "dfoderick",
16 | "license": "ISC",
17 | "bugs": {
18 | "url": "https://github.com/dfoderick/bitshovel/issues"
19 | },
20 | "homepage": "https://github.com/dfoderick/bitshovel#readme",
21 | "dependencies": {
22 | "axios": "^0.18.0",
23 | "bsv": "^0.20.3",
24 | "datapay": "0.0.3",
25 | "eventsource": "^1.0.7",
26 | "mocha": "^5.2.0",
27 | "nodemon": "^1.18.9",
28 | "redis": "^2.8.0",
29 | "requests": "^0.2.2"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/misc/when_transaction.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import base64
4 | import json
5 | import pprint
6 | import requests
7 | import redis
8 | import sseclient
9 |
10 | def with_requests(url):
11 | """Get a streaming response for the given event feed using requests."""
12 | return requests.get(url, stream=True)
13 |
14 | r = redis.StrictRedis(host="localhost", port=6379, password="", decode_responses=True)
15 | query = {"v": 3, "q": { "find": {} }}
16 | b64 = base64.encodestring(json.dumps(query))
17 | #print(b64)
18 | url = 'https://bitsocket.org/s/'+b64
19 | response = with_requests(url)
20 | client = sseclient.SSEClient(response)
21 | for event in client.events():
22 | try:
23 | bsock = json.loads(event.data)
24 | #shovel the bitsocket tx to redis
25 | r.publish("bitshovel.watch",event.data)
26 | print(bsock["data"][0]["_id"])
27 | #pprint.pprint(bsock)
28 | except Exception as ex:
29 | print(ex)
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Created by https://www.gitignore.io/api/node,sublimetext
4 |
5 | ### Node ###
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 |
16 | # Directory for instrumented libs generated by jscoverage/JSCover
17 | lib-cov
18 |
19 | # Coverage directory used by tools like istanbul
20 | coverage
21 |
22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
23 | .grunt
24 |
25 | # node-waf configuration
26 | .lock-wscript
27 |
28 | # Compiled binary addons (http://nodejs.org/api/addons.html)
29 | build/Release
30 |
31 | # Dependency directory
32 | node_modules
33 |
34 | # Optional npm cache directory
35 | .npm
36 |
37 | # Optional REPL history
38 | .node_repl_history
39 |
40 |
41 | ### SublimeText ###
42 | # cache files for sublime text
43 | *.tmlanguage.cache
44 | *.tmPreferences.cache
45 | *.stTheme.cache
46 |
47 | # workspace files are user-specific
48 | *.sublime-workspace
49 |
50 | # project files should be checked into the repository, unless a significant
51 | # proportion of contributors will probably not be using SublimeText
52 | *.sublime-project
53 |
54 | # sftp configuration file
55 | sftp-config.json
56 |
57 | #IMPORTANT!
58 | wallet.json
59 | wallet.json.*
60 |
61 | nohup.out
62 | database/
63 | get-pip.py
64 |
--------------------------------------------------------------------------------
/shovellisteners.js:
--------------------------------------------------------------------------------
1 | //eventsource for listening to bitsocket streams
2 | const utils = require('./utils')
3 | const EventSource = require('eventsource');
4 | const BITSOCKET_SOURCE = 'https://bitgraph.network/s/'
5 |
6 | //maintains list of active bitsocket listeners
7 | let BITSOCKET_LISTENERS = [];
8 |
9 | let listen = function listen (name, query) {
10 | const listener = {
11 | name : name,
12 | bitsocket : createEventSource(query)
13 | }
14 | BITSOCKET_LISTENERS.push(listener);
15 | return listener
16 | }
17 |
18 | function createEventSource(query) {
19 | console.log(query)
20 | const url = utils.urlString(BITSOCKET_SOURCE, query)
21 | return new EventSource(url)
22 | }
23 |
24 | //stop listening to bitsocket messages
25 | //this will shut down all bitcoin tx from broadcasting on your local bus
26 | //you can still send tx (using CHANNEL_SEND)
27 | let close = function close(name) {
28 | let soxlen = BITSOCKET_LISTENERS.length;
29 | while (soxlen--) {
30 | const listener = BITSOCKET_LISTENERS[soxlen];
31 | if (listener.name === name) {
32 | console.log(`stopping bitsocket listener ${listener.name}`)
33 | listener.bitsocket.close();
34 | BITSOCKET_LISTENERS.splice(soxlen, 1);
35 | }
36 | }
37 | }
38 |
39 | module.exports = {
40 | listen: listen,
41 | close: close
42 | }
--------------------------------------------------------------------------------
/docs/Docker.md:
--------------------------------------------------------------------------------
1 | # Dockerize your bitcoin apps
2 | Design your application in simple logical steps that can be deployed as components using "when-then" analysis. Plug in components can run anywhere on your network
3 | and are easily accessible with a simple command or API.
4 | Think of them as black boxes that are either localized to your network or applications that are accessible globally using the bitcoin network.
5 | If you want your app accessible from the local message bus then only listen to the local bus.
6 | If you want your app accessible from anywhere then listen to the bitcoin message bus.
7 |
8 | ## Example: Memo as a BitShovel plug in
9 |
10 | Read about the basics of setting up a Docker image here:
11 |
12 | https://docs.docker.com/docker-hub/#step-4-build-and-push-a-container-image-to-docker-hub-from-your-computer
13 |
14 | Create a Dockerfile for your app. You can use the following as an example.
15 | ```
16 | FROM python:2
17 | ADD memo.py /
18 | RUN pip install redis
19 | CMD [ "python", "./memo.py" ]
20 | ```
21 | Build it and test it locally.
22 | ```
23 | docker build -t bitshovel-memo .
24 | docker run -network=host bitshovel-memo
25 | ```
26 | When you are ready to publish your component then push it to Docker Hub.
27 | ```
28 | docker login --username=
29 | docker images
30 | docker tag 0b8e7c334643 /bitshovel-memo:latest
31 | docker push /bitshovel-memo
32 | ```
33 | Thereafter, anyone can download and run your component with just a single `docker run` command.
34 | ```
35 | docker run -network=host dfoderick/bitshovel-memo
36 | ```
37 |
--------------------------------------------------------------------------------
/shovelwallet.js:
--------------------------------------------------------------------------------
1 | const utils = require('./utils')
2 | const bsv = require('bsv')
3 | const fs = require('fs')
4 |
5 | const walletFileName = `wallet.json`;
6 |
7 | //example wallet.json
8 | // {
9 | // "wif":"private key",
10 | // "address": "optional address in legacy format"
11 | // }
12 | let generateWallet = function generateWallet(key) {
13 | let pk = null;
14 | if (key !== null && key !== undefined && key !== '') {
15 | pk = bsv.PrivateKey(key)
16 | } else {
17 | pk = bsv.PrivateKey()
18 | }
19 | const address = new bsv.Address(pk.publicKey, bsv.Networks.mainnet)
20 | console.log(`generated wallet with address ${address}`);
21 |
22 | const wallet = {
23 | "wif": pk.toWIF(),
24 | "address": address.toLegacyAddress()
25 | }
26 | return storeWallet(wallet)
27 | }
28 |
29 | let storeWallet = function storeWallet(wallet) {
30 | const sWallet = JSON.stringify(wallet, null, 2);
31 | backupWallet()
32 | fs.writeFileSync(walletFileName, sWallet, 'utf8', function(err) {
33 | if(err) {
34 | console.log(err);
35 | return
36 | }
37 | });
38 | return wallet;
39 | }
40 |
41 | let backupWallet = function backupWallet() {
42 | if (fs.existsSync(walletFileName)) {
43 | let timestamp = (new Date()).toISOString()
44 | .replace(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).(\d{3})Z$/, '$1$2$3.$4$5$6.$7000000');
45 | fs.renameSync(walletFileName, `${walletFileName}.${timestamp}`)
46 | }
47 | }
48 |
49 | module.exports = {
50 | walletFileName: walletFileName,
51 | generateWallet: generateWallet,
52 | storeWallet:storeWallet ,
53 | backupWallet: backupWallet
54 | }
55 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | let parseCommand = function parseCommand(command, msg) {
2 | const words = splitString(msg)
3 | console.log(words)
4 | return {
5 | "command": command,
6 | "action": words[0].toLowerCase(),
7 | "name": words[1].toLowerCase(),
8 | "parameter": words[2]
9 | }
10 | }
11 |
12 | let splitString = function splitString(s) {
13 | let clean = naiveClean(s)
14 | //finds the tokens in a string
15 | let matches = clean.match(/[\""].+?[\""]|[^ ]+/g)
16 | //console.log(matches)
17 | //but surrounding quotes are still there so remove
18 | if (matches) {
19 | for (var i=0, len=matches.length; i> /home/myuser/.profile
31 | RUN runuser -l myuser -c "npm config set prefix '~/.npm-global'"
32 |
33 | # Expose the port the API will be served on.
34 | #EXPOSE 3000
35 |
36 | # Switch to user account.
37 | USER myuser
38 | # Prep 'sudo' commands.
39 | RUN echo 'password' | sudo -S pwd
40 |
41 | # Clone the repository
42 | WORKDIR /home/myuser
43 | RUN git clone https://github.com/dfoderick/bitshovel
44 | WORKDIR /home/myuser/bitshovel
45 |
46 | # Install dependencies
47 | RUN npm install
48 |
49 | # Start the application.
50 | COPY bitshovel-production bitshovel-production
51 | CMD ["./bitshovel-production"]
52 |
53 | #Dummy app just to get the container running without exiting, for debugging.
54 | #You can then enter the container with command: docker exec -it /bin/bash
55 | #COPY dummyapp.js dummyapp.js
56 | #CMD ["node", "dummyapp.js"]
57 |
--------------------------------------------------------------------------------
/shovelbus.js:
--------------------------------------------------------------------------------
1 | //for now, redis is simplest message bus. later can support more brokers
2 | const utils = require('./utils')
3 | const redis = require('redis');
4 |
5 | //local apps will watch/listen/subscribe to this channel
6 | const CHANNEL_READ = "bitshovel.watch";
7 | //channel names for the local bus
8 | //messages on these channels will be tna format. See https://github.com/21centurymotorcompany/tna
9 | const CHANNEL_SEND = 'bitshovel.send';
10 | //TODO: there could be one channel for commands, with command type as part of the message
11 | //manage wallet
12 | const CHANNEL_WALLET = 'bitshovel.wallet';
13 | //const CHANNEL_ADDRESS = 'bitshovel.address';
14 | //stream for bitsocket streams
15 | const CHANNEL_STREAM = 'bitshovel.stream';
16 | //query for bitdb queries
17 | const CHANNEL_QUERY = 'bitshovel.query';
18 | //bitshovel application command
19 | const CHANNEL_APP = 'bitshovel.app';
20 |
21 | let thissub
22 | let thispub
23 |
24 | let sub = function sub() {return thissub}
25 | let pub = function pub() {return thispub}
26 |
27 | //set up connection to local message bus
28 | let start = function start() {
29 | thissub = redis.createClient();
30 | //export the on method of sub!!!
31 | sub.on = thissub.on
32 | thispub = redis.createClient();
33 |
34 | thissub.on("connect", function () {
35 | console.log("Connected to local bus");
36 | });
37 |
38 | thissub.on("subscribe", function (channel, message) {
39 | if (channel === CHANNEL_WALLET) {
40 | console.log(`Use message '${CHANNEL_WALLET}' to change wallet`);
41 | }
42 | if (channel === CHANNEL_STREAM) {
43 | console.log(`Use message '${CHANNEL_STREAM}' to stream live bitcoin messages`);
44 | }
45 | if (channel === CHANNEL_QUERY) {
46 | console.log(`Use message '${CHANNEL_QUERY}' to get historical bitcoin messages`);
47 | }
48 | if (channel === CHANNEL_APP) {
49 | console.log(`Use message '${CHANNEL_APP}' to control app functions`);
50 | }
51 | if (channel === CHANNEL_SEND) {
52 | console.log(`Use message '${CHANNEL_SEND}' to write to bitcoin (send bitcoin message)`);
53 | }
54 | })
55 |
56 | }
57 |
58 | //subscribe to single channel
59 | let subscribe = function subscribe(channel) {
60 | thissub.subscribe(channel);
61 | }
62 |
63 | let subscribeall = function subscribeall() {
64 | subscribe(CHANNEL_STREAM);
65 | subscribe(CHANNEL_QUERY);
66 | subscribe(CHANNEL_SEND);
67 | subscribe(CHANNEL_APP);
68 | subscribe(CHANNEL_WALLET);
69 | }
70 |
71 | //shovel the bitcoin (bitsocket tna) message on to the local bus
72 | function publish(msg) {
73 | //announce to the local bus that a bitcoin tx has been broadcast
74 | thispub.publish(CHANNEL_READ, msg);
75 | }
76 |
77 | module.exports = {
78 | start: start,
79 | subscribeall: subscribeall,
80 | sub: sub,
81 | subscribe: subscribe,
82 | pub: pub,
83 | publish: publish,
84 | CHANNEL_WALLET: CHANNEL_WALLET,
85 | CHANNEL_STREAM: CHANNEL_STREAM,
86 | CHANNEL_QUERY: CHANNEL_QUERY,
87 | CHANNEL_APP: CHANNEL_APP,
88 | CHANNEL_SEND: CHANNEL_SEND
89 | }
90 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const utils = require('../utils');
3 | const shoveladdress = require('../shoveladdress');
4 | const shovelcache = require('../shovelcache');
5 | const shovelregistry = require('../shovelregistry');
6 | const shovelwallet = require('../shovelwallet');
7 | const shovelbus = require('../shovelbus');
8 |
9 | describe('bitshovel', function() {
10 | describe('utils',function() {
11 | //command, action, name, parameter
12 | it('command stream', function(done) {
13 | let cmd = utils.parseCommand("stream","start streamname query")
14 | assert.equal(cmd.command, "stream")
15 | done()
16 | })
17 | it('command filter', function(done) {
18 | let cmd = utils.parseCommand("test","start stream {\"find\":{}}")
19 | assert.equal(cmd.action, "start")
20 | assert.equal(cmd.name, "stream")
21 | assert.equal(cmd.parameter, "{\"find\":{}}")
22 | done()
23 | })
24 | it('command with spaces', function(done) {
25 | let cmd = utils.parseCommand("test",'start stream "{this is one param}"')
26 | assert.equal(cmd.action, "start")
27 | assert.equal(cmd.name, "stream")
28 | assert.equal(cmd.parameter, "{this is one param}")
29 | done()
30 | })
31 | it('isStart', function(done) {
32 | assert(utils.isStart('start'))
33 | assert(utils.isStart("START"))
34 | assert(utils.isStart('on'))
35 | assert(utils.isStart('stop') === false)
36 | done()
37 | })
38 | it('isStop', function(done) {
39 | assert(utils.isStop('stop'))
40 | assert(utils.isStop("STOP"))
41 | assert(utils.isStop('off'))
42 | assert(utils.isStop('') === false)
43 | done()
44 | })
45 | it('parse op_return with hex', function(done) {
46 | let pushdata = "\"BitShovel Test\""
47 | let words = utils.splitString(`x0602 "${pushdata}"`)
48 | console.log(words)
49 | clean = utils.cleanQuotes(pushdata)
50 | console.log(clean)
51 | assert(words.length === 2)
52 | assert.equal(words[1], clean)
53 | done()
54 | })
55 | // it('parse with internal quotes', function(done) {
56 | // let words = utils.splitString(`first "second \"data\""`)
57 | // console.log(words)
58 | // //assert(words.length === 2)
59 | // assert.equal(words[1], "second \"data\"")
60 | // done()
61 | // })
62 | })
63 |
64 | describe('address tests',function() {
65 | it('toCashAddress', function (done) {
66 | let cash = shoveladdress.toCashAddress('1N9zmDbiE2w7QT8oW524eD3BXdcQ8PpFgM')
67 | assert.equal(cash, 'qr5qut7whhqvh3ae8hl0y3tgveaa4s834uckgu0t49')
68 | done()
69 | })
70 | it('toOriginalAddress', function (done) {
71 | let bitcoinaddress = shoveladdress.toOriginalAddress('qr5qut7whhqvh3ae8hl0y3tgveaa4s834uckgu0t49')
72 | assert.equal(bitcoinaddress, '1N9zmDbiE2w7QT8oW524eD3BXdcQ8PpFgM')
73 | done()
74 | })
75 | })
76 |
77 | describe('registry tests',function() {
78 | it('register', function (done) {
79 | let startcount = Object.keys(shovelregistry.APPS).length
80 | shovelregistry.register("testapp","anyaddress")
81 | assert.equal(Object.keys(shovelregistry.APPS).length, startcount + 1)
82 | done()
83 | })
84 | it('has', function (done) {
85 | shovelregistry.register("testapp","anyaddress")
86 | assert.ok(shovelregistry.has("testapp"))
87 | done()
88 | })
89 | it('get', function (done) {
90 | shovelregistry.register("testapp","anyaddress")
91 | assert.equal(shovelregistry.get("testapp"), 'anyaddress')
92 | done()
93 | })
94 | })
95 |
96 | describe('wallet tests',function() {
97 | it('generate wallet', function (done) {
98 | let wallet = shovelwallet.generateWallet()
99 | assert.equal(Object.keys(wallet).length, 2)
100 | assert(wallet.wif)
101 | assert(wallet.address)
102 | done()
103 | })
104 | })
105 |
106 | })
107 |
--------------------------------------------------------------------------------
/docs/getstarted.md:
--------------------------------------------------------------------------------
1 | # Getting Started with BitShovel
2 | BitShovel is a tool that makes it easy to write bitcoin applications.
3 |
4 | Take a few minutes to [understand it](/README.md) if you don't know what it is.
5 |
6 | In this tutorial you will learn how to install BitShovel and use it to read and write to bitcoin using a few simple commands.
7 |
8 | # Dependencies
9 | You should have Docker installed as well as redis-cli (currently required for command line testing and demonstration)
10 |
11 | ### Step 1: Install BitShovel
12 | First you need a local message bus. Redis is nice and simple. Run it if you don't have it already. This may require sudo.
13 | ```
14 | docker run -p 6379:6379 redis
15 | ```
16 | Now for the hard part. Installing BitShovel!
17 | ```
18 | docker run --network=host dfoderick/bitshovel:getstarted
19 | ```
20 | BitShovel will download and start. You should see it respond with `Connected to local bus...`
21 | Hmmm. That was too easy. BitShovel is now installed and running.
22 | ### Step 2: Download the Memo.cash component
23 | The base implementation of BitShovel will read and write to bitcoin but it doesn't know about any application specific protocols like memo.cash. Open another terminal and run the following command to download and run a component that understands memo.cash messages.
24 | ```
25 | docker run --network=host dfoderick/bitshovel-memo:getstarted
26 | ```
27 | That's it! Now your shovel will know how to post to the memo.cash web site using bitcoin. In the near future there will be additional components that you can download just as easily to add functionality to your apps. And you can write your own custom components in any language.
28 | ### Step 3: Testing your installation and Funding your wallet
29 | Before we send messages to bitcoin, let's make life a bit easier on ourselves. Open yet another terminal and run the following commands (or put them in your Bash ~/.bashrc script).
30 | ```
31 | alias sub="redis-cli subscribe"
32 | alias pub="redis-cli publish"
33 | alias get="redis-cli get"
34 | ```
35 | BitShovel created a wallet when it started for the first time. To get the address for your wallet run
36 | ```
37 | get bitshovel.wallet.address
38 | ```
39 | Your wallet will need money so that it can send messages to bitcoin. There are at least 3 ways to get it funded.
40 |
41 | * The simplest way to fund your wallet is go to https://fund.fullcyclemining.com/
42 | Enter the address from above. Select an amount (0.05 cents is plenty for testing) then slide the money button. Your payment information will show below when it is successful.
43 | 
44 |
45 | * If that doesn't work then fund the wallet using MoneyButton's test page. https://www.moneybutton.com/test
46 |
47 | * Alternatively, if you already have a wallet that is funded and you know the private key then you can tell BitShovel to use it instead.
48 | ```
49 | pub bitshovel.wallet
50 | get bitshovel.wallet.address
51 | ```
52 | Remember your address. You will listen to messages going to this address in the next step.
53 | ### Step 4: Listen for bitcoin messages
54 | At this point BitShovel is not doing anything. Lets tell it to listen for bitcoin messages on the local message bus.
55 | ```
56 | sub bitshovel.watch
57 | ```
58 | Not so interesting. It subscribes to the watch event and then just sits there. The terminal is waiting for bitcoin messages but there are not any messages locally yet. Leave that terminal window open and visible.
59 | Open another terminal window and run the following commands to get the last 3 messages from bitcoin.
60 | (Remember to run the alias commands if you did not put them in your startup script.)
61 | ```
62 | alias pub="redis-cli publish"
63 | pub bitshovel.query '{"find":{},"limit":3}'
64 | ```
65 | Better. Three bitcoin messages should scroll in the watch window. You are now streaming messages from bitcoin on to our local message bus. Next tell BitShovel to listen for messages that you will be sending in the next step.
66 | ```
67 | pub bitshovel.app 'start walletname PasteYourWalletAddressFromAbove'
68 | ```
69 | Again, this command does not display anthing. It tells BitShovel to sit and wait for someone to send something to your wallet address. That someone will be you in the next step. Let's move on...
70 | ### Step 5: Sending messages to bitcoin
71 | Finally, we can send a message to bitcoin! This command will create a transaction that contains an OP_RETURN.
72 | ```
73 | pub bitshovel.send '"Hello from BitShovel!"'
74 | ```
75 | The watch window that you set up in the previous step should show the transaction in a few seconds. Our UI is not so lovely at this point but we can console ourselves because we know there are many UI tools already that make for a great experience using bitcoin. Lets use some of them now. Search for the message you just sent using searchbsv.com.
76 | https://searchbsv.com/search?q=BitShovel
77 |
78 | It's there. Great! Next, we can make use of the Memo plugin.
79 | ```
80 | pub memo.send "Test from BitShovel Memo"
81 | ```
82 | Notice the difference? You used memo.send instead of bitshovel.send. The difference is that you sent it to the memo component - the memo component transformed the message according to the Memo protocol and then it forwarded your message to BitShovel which broadcast the message to bitcoin. Yeah!
83 |
84 | Log in to https://sv.memo.cash and find your message. Notice that your user name is just your address. Let's change that.
85 | ```
86 | pub memo.send 'setname "BitShovel"'
87 | ```
88 | That's better. Now publish a message with a topic. "BitShovel" is the topic and "Test Topic" is the message.
89 | ```
90 | pub memo.send 'posttopic "BitShovel" "Test Topic"'
91 | ```
92 | Refresh the Memo page to see the results.
93 | # Conclusion
94 | Pause for a moment and consider what you have learned in this tutorial. You have seen that with just a simple download and few commands you can read and write to bitcoin.
95 | * You can get notified whenever someone sends a message or payment to your wallet address.
96 | * You can do something intereting whenever that notification happens.
97 | * You can then send a message back to bitcoin in response.
98 |
99 | BitShovel makes it easy.
100 |
--------------------------------------------------------------------------------
/bitshovel.js:
--------------------------------------------------------------------------------
1 | //ALERT: This software is alpha. Use at your own risk, especially if using mainnet
2 | //Always, Always, Always protect your private key!
3 |
4 | //This the connection point between bitcoin and your local application
5 | //implemented with unwriter libraries for reading and writing to bitcoin
6 |
7 | const utils = require('./utils')
8 | const shovelwallet = require('./shovelwallet')
9 | const shoveladdress = require('./shoveladdress')
10 | const shovelcache = require('./shovelcache')
11 | //application registry
12 | const shovelregistry = require('./shovelregistry')
13 | //bitsocket listeners
14 | const shovellisteners = require('./shovellisteners')
15 | const localbus = require('./shovelbus')
16 | //for writing to bitcoin
17 | const datapay = require('datapay');
18 | const fs = require('fs');
19 | const axios = require('axios')
20 |
21 | const DEBUG = false
22 |
23 | //eventually these items will be configurable
24 | const BITDB_SOURCE = 'https://bitdb.network/q/'
25 | const BITDB_API_KEY = 'qrr6u2amzamlf7cay750j0fnd76fhhcpxgr2ekv2wg'
26 |
27 | //create a wallet if there is none
28 | if (!fs.existsSync(shovelwallet.walletFileName)) {
29 | shovelwallet.generateWallet();
30 | }
31 | let wallet = require(`./${shovelwallet.walletFileName}`);
32 |
33 | function main () {
34 | localbus.start()
35 | shovelcache.start()
36 | shovelcache.storeWalletAddress(wallet.address);
37 |
38 | localbus.sub.on("message", function (channel, message) {
39 | try {
40 | console.log(`got message from local bus ${channel}: ${message}`);
41 | if (channel === localbus.CHANNEL_SEND) {
42 | //send the message to bitcoin
43 | shovelToBitcoin(message);
44 | }
45 | //example: bitshovel.stream start|stop name query
46 | if (channel === localbus.CHANNEL_STREAM) {
47 | const cmd = utils.parseCommand("stream", message)
48 | if (utils.isStart(cmd.action)) {
49 | //start listening to bitsocket messages
50 | //this will start broadcasting bitcoin tx on to your local bus
51 | console.log(`starting bitsocket listener ${message}`);
52 | startBitSocket(cmd);
53 | }
54 | if (utils.isStop(cmd.action)) {
55 | shovellisteners.close(cmd.name)
56 | }
57 | }
58 | if (channel === localbus.CHANNEL_APP) {
59 | //application (address) level command. Assumes an address is specified
60 | //bitsocket used cashaddr. will be converted to use legacy address soon
61 | const cmd = utils.parseCommand("app", message)
62 | console.log(cmd)
63 | if (utils.isStart(cmd.action)) {
64 | console.log(`starting app listener ${message}`);
65 | startBitSocket(cmd);
66 | } else if (utils.isStop(cmd.action)) {
67 | stopBitSocket(cmd);
68 | } else if (cmd.action === 'register') {
69 | registerApplication(cmd)
70 | } else {
71 | console.log(`unknown command ${cmd}`)
72 | }
73 | }
74 | if (channel === localbus.CHANNEL_QUERY) {
75 | const url = utils.urlString(BITDB_SOURCE, makeBitquery(JSON.parse(message)))
76 | getBitdb(url)
77 | }
78 | if (channel === localbus.CHANNEL_WALLET) {
79 | //store the private key in local wallet
80 | //TODO:should encrypt private key on the wire
81 | if (wallet.wif != message) {
82 | wallet = shovelwallet.generateWallet(message);
83 | shovelcache.storeWalletAddress(wallet.address);
84 | }
85 | }
86 | }
87 | catch (err) {
88 | console.error(err)
89 | }
90 | })
91 | localbus.subscribeall()
92 | }
93 |
94 | main()
95 |
96 | function registerApplication(cmd) {
97 | //for now use the bitshovel wallet, in future probably want each app to have wallet
98 | localbus.pub.set(cmd.name, wallet.address, function(err) {
99 | if (err) {
100 | console.error(err)
101 | }
102 | })
103 | //localbus_pub.hmset(CHANNEL_APP, cmd.name, wallet.address)
104 | //register the app in the app registry
105 | shovelregistry.register(cmd.name, wallet.address)
106 | }
107 |
108 | function getBitdb(url) {
109 | var header = {
110 | headers: { key: BITDB_API_KEY }
111 | }
112 | axios.get(url, header).then(function(r) {
113 | //console.log(`queried ${r.data.u.length} results`)
114 | //unconfirmed tx
115 | for (let item of r.data.u) {
116 | localbus.publish(JSON.stringify(item))
117 | }
118 | //confirmed tx
119 | for (let item of r.data.c) {
120 | localbus.publish(JSON.stringify(item))
121 | }
122 | })
123 | }
124 |
125 | //start listening for bitcoin messages
126 | //example query...
127 | // {"v": 3, "q": { "find": {} }}
128 | function startBitSocket(cmd) {
129 | //let address = findAddressforApplication(cmd)
130 | let query = queryFromCommand(cmd)
131 | const listener = shovellisteners.listen(cmd.name, query)
132 | listener.bitsocket.onmessage = function(e) {
133 | //surprising that this works! function keeps the listener name in context when it fires
134 | console.log(listener.name);
135 | logTna(e);
136 | localbus.publish(e.data);
137 | }
138 | listener.bitsocket.onerror = function(err) {
139 | console.log(err);
140 | }
141 | }
142 |
143 | function logit(category, s) {
144 | console.log(`${category} ${s}`)
145 | }
146 |
147 | function logTna(tna) {
148 | console.log(tna);
149 | }
150 |
151 | function queryFromCommand(cmd) {
152 | if (cmd.command === "stream") {
153 | //parameter is the query
154 | return makeBitquery(JSON.parse(cmd.parameter))
155 | }
156 | if (cmd.command === "app") {
157 | //parameter is the address to monitor
158 | address = findAddressforApplication(cmd)
159 | console.log(address)
160 | const qry = {
161 | "v": 3,
162 | "q": {
163 | "find": {
164 | "out.e.a": shoveladdress.toCashAddress(address)
165 | }
166 | }
167 | }
168 | return makeBitquery(qry)
169 | }
170 | //what to do?
171 | return cmd.parameter
172 | }
173 |
174 | //translate application alias to address
175 | function findAddressforApplication(cmd) {
176 | // (async () => {
177 | //if param was passed then use that as address
178 | if (cmd.parameter) {
179 | return cmd.parameter
180 | }
181 | //otherwise look up the app by name
182 | // await localbus_pub.hget(CHANNEL_APP, cmd.name, function (err, obj) {
183 | // if (obj) {
184 | // return obj
185 | // }
186 | // return cmd.name
187 | // })
188 | if (shovelregistry.has(cmd.name)) {
189 | return shovelregistry.get(cmd.name)
190 | }
191 | return cmd.name
192 | // })()
193 | }
194 |
195 | //make a standard bitquery query
196 | //basically it will add any missing attributes to make it a standard query
197 | function makeBitquery(query) {
198 | let fullquery = query
199 | if (!query.hasOwnProperty("q")) {
200 | //assume that query is just the q attribute, fill in the rest
201 | fullquery = {
202 | "v": 3,
203 | "q": query
204 | }
205 | }
206 | return JSON.stringify(fullquery)
207 | }
208 |
209 | //write the message to bitcoin by creating a transaction
210 | function shovelToBitcoin(message) {
211 | //TODO: for now assume ascii text, later need to distinguish from hex string
212 | //console.log(`shoveling to bitcoin >${message}<`);
213 | let split = utils.splitString(message)
214 | console.log(split)
215 |
216 | if (!DEBUG) {
217 | datapay.send({
218 | data: split,
219 | pay: { key: wallet.wif }
220 | }, function sendResult(err,txid) {
221 | if (err) {
222 | console.log(err);
223 | }
224 | if (txid) {
225 | console.log(`txid:${txid}`);
226 | }
227 | });
228 | }
229 | //TODO:could raise event saying that tx was sent
230 | }
231 |
232 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Why use BitShovel for your blockchain apps?
3 |
4 | * Easily read and write messages to bitcoin in [any language](#examples)
5 | * Combines read and write operations into a single simple API
6 | * Builds on top of [unwriter]((https://github.com/unwriter)) [excellent libraries](https://github.com/21centurymotorcompany)
7 | * Easy to deploy using Docker
8 | * Provides a simple messaging infrastructure (pubsub, work queues, etc.) for all your applications
9 | * Compatible with event driven programming and microservices architectures
10 | * Additional application services will plug in to the messaging infrastructure with a simple Docker install
11 |
12 | ## What is BitShovel?
13 | BitShovel fully embraces the architecture [unwriter](https://github.com/unwriter) explains in [Bitsocket: The Realtime API for Bitcoin](https://www.yours.org/content/bitsocket--the-realtime-api-for-bitcoin-0c646d55c152) and recognizes a fundamental truth: *Bitcoin is the universal messaging system.*
14 |
15 | Think of BitShovel as a bitcoin application accelerator - a building block for Metanet.
16 | > Broadcast your message to bitcoin if you need any of the enhanced capabilities that bitcoin provides:
17 | > * You want to preserve an immutable audit trail of your message
18 | > * You need to transfer money with the transaction as a receipt
19 | > * To initiate a remote process or service
20 |
21 | > To communicate with a local applicaiton or if none of the enhanced services that bitcoin provides are needed then just broadcast the messate on the local bus.
22 |
23 | **It can be that simple!**
24 |
25 | BitShovel is not a library. It is a downloadable application service black box plug-in component that runs in a process on your local network. You communicate with BitShovel applications by sending it messages. A sequence of messages form a workflow of events.
26 |
27 | With BitShovel your apps can listen to events that happen on bitcoin. It can also kick off a remote workflow by sending a message to bitcoin. Your application is now a bot, an oracle, an autonomous agent, a smart contract, a living entity on bitcoin.
28 |
29 | 
30 |
31 | The name BitShovel comes from its utility as a low level tool. 'Shoveling' is the term for scooping a message on one message bus and forwarding it onto another message bus. Sometimes the same action can be referred to as a bridge or an extender.
32 |
33 | ## Getting Started
34 | There is a [Getting Started with BitShovel Tutorial](docs/getstarted.md) which also serves as an installation guide.
35 |
36 | ## Install using Docker Compose
37 | ```
38 | git clone https://github.com/dfoderick/bitshovel
39 | cd bitshovel
40 | ```
41 | Run the following command *once* to build the docker image.
42 | ```
43 | docker-compose build --no-cache
44 | ```
45 | Thereafter, run the following command (notice the `no-recreate` option) so that your private keys in wallet.json will not get wiped out.
46 | ```
47 | docker-compose up --no-recreate
48 | ```
49 | To find out what docker containers are running use
50 | ```
51 | docker ps
52 | ```
53 | ## Configure your private keys
54 | >
55 | > *An Important Note on Private Keys!*
56 | > It is very easy to lose the private keys on a development server in Docker. Do not store a large amount of bitcoin in your wallet. Fund the wallet with small amounts and make backups of your private keys
57 | >
58 |
59 | If you want to write data to bitcoin then BitShovel will need private keys for your wallet.
60 | BitShovel will create a wallet.json by default when it starts up. You can either update
61 | the wallet.json file with your own private keys or you can fund the wallet that BitShovel
62 | generated. Use the following command (while BitShovel is running) to find out the wallet address to fund.
63 | ```
64 | redis-cli GET bitshovel.wallet.address
65 | ```
66 | The easiest way to fund the wallet is by using moneybutton to withdraw to the BitShovel address.
67 | https://www.moneybutton.com/test
68 |
69 | ## Create your app
70 | Any process that can read and write messages can now be a blockchain app. [See examples below](#examples). Test that your BitShovel instance is running using the command lines below and then get started writing apps.
71 |
72 | # Test BitShovel from command line
73 | To make testing from the command line easier make the following aliases. You can choose any alias that suits.
74 | ```
75 | alias sub="redis-cli subscribe"
76 | alias pub="redis-cli publish"
77 | alias get="redis-cli get"
78 | ```
79 |
80 | Open terminal and create a listener for bitcoin messages. Messages will scroll in this process.
81 | ```
82 | sub bitshovel.watch
83 | ```
84 | Open another terminal to control BitShovel and start pumping it with commands.
85 | This command will listen to all messages on the bitcoin network.
86 | ```
87 | pub bitshovel.stream 'start streamname {"find":{}}'
88 | ```
89 | Stop shovel from reading messages.
90 | ```
91 | pub bitshovel.stream 'stop streamname'
92 | ```
93 | For fun, lets listen to all the messages that BitShovel is creating. The following command will get your wallet address.
94 | ```
95 | get bitshovel.wallet.address
96 | ```
97 | Copy and paste it into the following command to listen for transactions on that address.
98 | ```
99 | pub bitshovel.app 'start walletname PasteYourWalletAddressFromAbove'
100 | ```
101 | Send a message to the bitcoin network - requires wallet to be funded and needs double quotes (for now).
102 | ```
103 | pub bitshovel.send '"Hello from BitShovel!"'
104 | ```
105 | After a few seconds delay the bitshovel.watch terminal should show the message that you just sent to bitcoin.
106 |
107 | You can also find the message on bitcoin explorer.
108 | https://bitgraph.network/explorer/ewogICJ2IjogMywKICAicSI6IHsKICAgICJmaW5kIjogeyAib3V0LmIwIjogeyAib3AiOiAxMDYgfSwgIm91dC5zMSI6IkhlbGxvIGZyb20gQml0U2hvdmVsISIgfSwKICAgICJwcm9qZWN0IjogeyAib3V0LiQiOiAxIH0KICB9Cn0=
109 |
110 | Query bitcoin for all messages using the demo string using the following query.
111 | ```
112 | {
113 | "v": 3,
114 | "q": {
115 | "find": { "out.b0": { "op": 106 }, "out.s1":"Hello from BitShovel!" },
116 | "project": { "out.$": 1 }
117 | }
118 | }
119 | ```
120 | You could also search using searchbsv.com..
121 | https://searchbsv.com/search?q=BitShovel
122 |
123 |
124 | You can also send a Bitquery query to BitShovel and it will replay the bitcoin messages on your local message bus. This query will get the last bitcoin message and publish it on your local bus.
125 | ```
126 | pub bitshovel.query '{"find":{},"limit":1}'
127 | ```
128 |
129 | # Bitshovel Events (aka Channels or Topics)
130 | * **bitshovel.watch**
131 | Subscribe to this event to get notified when a new bitcoin message appears on the local bus.
132 | * **bitshovel.send**
133 | Publish message to this channel to write to bitcoin
134 | * **bitshovel.stream**
135 | Start (Stop) listening to bitcoin messages. Pass it the Bitquery. Bitcoin messages that match the query filter will be forwarded to the local bus.
136 | * **bitshovel.query**
137 | Get historical transactions from the bitcoin network. Tx that match the query will be replayed on the local bus.
138 | * **bitshovel.wallet**
139 | Update the wallet to use a new private key
140 | * **bitshovel.app**
141 | Application level commands. Currently allows listening for messages for a single application (address)
142 |
143 | # Examples
144 | A Python program to send (write) to bitcoin.
145 | ```
146 | import redis
147 | bus = redis.Redis()
148 | bus.publish("bitshovel.send",'"Hello from BitShovel! Python"')
149 | ```
150 | A Python program to listen to bitcoin messages on the local bus.
151 | To see messages in this window you have to also tell BitShovel to watch for messages on bitcoin.
152 | e.g. ```pub bitshovel.stream 'start streamname {"find":{}}'```
153 | ```
154 | import redis
155 | bus = redis.Redis().pubsub()
156 | bitshovel_reader = bus.subscribe("bitshovel.watch")
157 | for message in bus.listen():
158 | print(message)
159 | ```
160 | A golang program to write to bitcoin.
161 | ```
162 | package main
163 | import "github.com/go-redis/redis"
164 | import "log"
165 | func main() {
166 | client := redis.NewClient(&redis.Options{
167 | Addr: "localhost:6379",
168 | Password: "", // no password set
169 | DB: 0, // use default DB
170 | })
171 | _, err := client.Publish("bitshovel.send", "Hello from BitShovel! Go").Result()
172 | if err != nil {
173 | log.Fatal(err)
174 | }
175 | }
176 | ```
177 |
178 | # Native install for Developers
179 | Normal users can run BitShovel using the Docker instruction above. Developers wishing to customize the app can install it natively to edit the source file.
180 |
181 | ## Install redis
182 | Install and run redis using
183 | ```
184 | docker run --name bitshovel-redis -d redis
185 | ```
186 |
187 | ## Get source code
188 | ```
189 | git clone https://github.com/dfoderick/bitshovel
190 | cd bitshovel
191 | npm install
192 | ```
193 | ## Run BitShovel server
194 | ```
195 | node bitshovel.js
196 | ```
197 | will respond with 'Connected to local bus...'
198 |
--------------------------------------------------------------------------------