First push from Satoshi Portal's own cyphernode

This commit is contained in:
kexkey
2018-09-22 14:49:26 -04:00
commit c39f81f683
47 changed files with 3517 additions and 0 deletions

96
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,96 @@
# Contributions are welcome!
There are a lot to contribute. Bugfixes, improvements, new features, documentation, tests, etc. Let's be a team!
# How to contribute
## Cyphernode Architecture
Cyphernode is an assembly of Docker containers being called by a request dispatcher.
- The request dispatcher (requesthandler.sh) is the HTTP entry point.
- The request dispatcher is stateful: it keeps some data to be more effective on next calls.
- The request dispatcher is where Cyphernode scales with new features: add your switch, dispatch requests to your stuff.
We are trying to construct each container so that it can be used separately, as a standalone reusable component.
Important to us:
- Be as optimized as possible, using Alpine when possible and having the smallest Docker image size possible
- Reuse existing software: built-in shell commands, well-established pieces of software, etc.
- Use open-source software
- Don't reinvent the wheel
- Expose the less possible surface
## Center element: proxy_docker
The proxy_docker is the container receiving and dispatching calls from clients. When adding a feature to Cyphernode, it is the first part to be modified to integrate the new feature.
### proxy_docker/app/script/requesthandler.sh
You will find in there the switch statement used to dispatch the requests. Just add a case block with your command, using other cases as examples for POST or GET requests.
### proxy_docker/app/config
You will find there config files. config.properties should be used to centralize configs. spender and watcher properties are used to obfuscate credentials on curl calls.
### proxy_docker/app/data
watching.sql contains the data model. Called "watching" because in the beginning of the project, it was only used for watching addresses. Now could be used to index the blockchain (build an explorer) and add more features.
## cron_docker
If you have jobs to be scheduled, use this container. Just add an executable and add it to the crontab.
Currently used to make sure callbacks have been called for missed transactions.
## docker-compose
Deployment flexibility:
- distribute your components all over the world using docker-compose.yml constraints in a swarm
- scales your system with load-balancers
- automatically restarts a component if it crashes
# What to contribute
## TODO
- Stress/load tests
- How does netcat behave on high traffic
- sqlite3 tweaks for dealing with threaded calls
- Security check
- Installation scripts:
- Configuration (config files)
- Deployment (docker-compose)
- Launcher app with lunanode
## Improvements
- wget is included in Alpine, we could use it instead of curl if we don't have to POST over HTTPS
- Make sure everything is thread-safe
- There's currently a flock in do_callbacks, do we need one elsewhere?
- Using inter-containers direct calls (through docker.sock?) instead of HTTP?
- Possibility to automate additions of new endpoints?
- With name, type (GET/POST), function name and script file, it's possible
- Autoconfig pruned property (in config.properties) by using getblockchaininfo RPC call
- Compile lightning-cli during configuration process (if not too slow) instead of precompile it
- Add results of gettransaction RPC calls in the DB (like already done for getrawtransaction results) and check DB before calling the node
- only when confirmations > 0 in DB
## Features
- Timestamping (OTS)
- Electrum Server features
- Web Admin Panel
- Using Trezor/Coldcard for authentication/signing
- Multi-authorization: SuperAdmin, Accounting (transactions, etc.), Marketing (usage, etc.), etc.
- Web Wallet
- Using Trezor/Coldcard for authentication/signing
- Index the blockchain
- PGP features: signing, verifying, etc.
- Full blockchain explorer
- e-mail notifications when things happen (monitoring):
- less than X BTC in the spending wallet
- more than Y% CPU used for more than Z minutes on the proxy container
- bitcoind instance unreachable
- any error

11
cron_docker/Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM alpine
RUN apk add --update --no-cache \
curl
COPY callbacks_cron /etc/periodic/15min/callbacks_cron
RUN chmod +x /etc/periodic/15min/callbacks_cron
ENTRYPOINT ["crond"]
CMD ["-f"]

13
cron_docker/README.md Normal file
View File

@@ -0,0 +1,13 @@
# Cyphernode CRON container
## Configure your container by modifying `env.properties` file
```properties
PROXY_URL=cyphernode:8888/executecallbacks
```
## Building docker image
```shell
docker build -t proxycronimg .
```

View File

@@ -0,0 +1,3 @@
#!/bin/sh
curl ${PROXY_URL}

View File

@@ -0,0 +1 @@
PROXY_URL=cyphernode:8888/executecallbacks

515
doc/API.md Normal file
View File

@@ -0,0 +1,515 @@
# Cyphernode
## Current API
### Watch a Bitcoin Address (called by application)
Inserts the address and callbacks in the DB and imports the address to the Watching wallet.
```http
POST http://cyphernode:8888/watch
with body...
{"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","unconfirmedCallbackURL":"192.168.111.233:1111/callback0conf","confirmedCallbackURL":"192.168.111.233:1111/callback1conf"}
```
Proxy response:
```json
{
"id": "291",
"event": "watch",
"imported": "1",
"inserted": "1",
"address": "2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp",
"unconfirmedCallbackURL": "192.168.133.233:1111/callback0conf",
"confirmedCallbackURL": "192.168.133.233:1111/callback1conf",
"estimatesmartfee2blocks": "0.000010",
"estimatesmartfee6blocks": "0.000010",
"estimatesmartfee36blocks": "0.000010",
"estimatesmartfee144blocks": "0.000010"
}
```
### Un-watch a previously watched Bitcoin Address (called by application)
Updates the watched address row in DB so that callbacks won't be called on tx confirmations for that address.
```http
GET http://cyphernode:8888/unwatch/2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp
```
Proxy response:
```json
{
"event": "unwatch",
"address": "2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp"
}
```
### Get a list of Bitcoin addresses being watched (called by application)
Returns the list of currently watched addresses and callback information.
```http
GET http://cyphernode:8888/getactivewatches
```
Proxy response:
```json
{
"watches": [
{
"id":"291",
"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp",
"imported":"1",
"unconfirmedCallbackURL":"192.168.133.233:1111/callback0conf",
"confirmedCallbackURL":"192.168.133.233:1111/callback1conf",
"watching_since":"2018-09-06 21:14:03"}
]
}
```
### Confirm a Transaction on Watched Address (called by Bitcoin node on transaction confirmations)
Confirms a transaction on an imported address. The Watching Bitcoin node will notify Cyphernode (thanks to walletnotify in bitcoin.conf) by calling this endpoint with txid when a tx is new or updated on an address. If address is still being watched (flag in DB), the corresponding callbacks will be called.
```http
GET http://cyphernode:8888/conf/b081ca7724386f549cf0c16f71db6affeb52ff7a0d9b606fb2e5c43faffd3387
```
Proxy response:
```json
{
"result":"confirmed"
}
```
### Get the Best Block Hash (called by application)
Returns the best block hash of the watching Bitcoin node.
```http
GET http://cyphernode:8888/getbestblockhash
```
Proxy response:
```json
{
"result":"00000000000000262588c21afbf9e1da151daf10b11215d501271163f26ea74a",
"error":null,
"id":null
}
```
### Get Block Info (called by application)
Returns block info for the supplied block hash.
```http
GET http://cyphernode:8888/getblockinfo/000000006f82a384c208ecfa04d05beea02d420f3f398ddda5c7f900de5718ea
```
Proxy response:
```json
{
"result":
{
"hash":"000000006f82a384c208ecfa04d05beea02d420f3f398ddda5c7f900de5718ea",
"confirmations":124329,
"strippedsize":8067,
"size":8067,
"weight":32268,
"height":1288528,
"version":536870912,
"versionHex":"20000000",
"merkleroot":"f1596255c357713c9827a739b17c8445cdcb81c4d336e24516c66f714a8f7030",
"tx":["65759382e7047f89f7e80676026c10401d47ce47ea997138251c59ca58f28a03","4d2d0cdd89061ce4bb8bdf5502aec9799e5065d205e8ab75c6598bd90a0e6e4c",[...],"a6d2fda52467aa7ca1271529ded5510bd12ad58f99f73fe995f50691aea4eb06"],
"time":1521404668,
"mediantime":1521398617,
"nonce":4209349744,
"bits":"1d00ffff",
"difficulty":1,
"chainwork":"000000000000000000000000000000000000000000000037e37554821063611f",
"nTx":32,
"previousblockhash":"000000005b55cefec377f7f73e656fef835a928b6eeb2060a89e7eb23a573c49",
"nextblockhash":"000000000ab13797b6dddd5e28d2ed62b937cd65ecf49b1f9d75108b9ea500f9"
},
"error":null,
"id":null
}
```
### Get the Best Block Info (called by application)
Returns best block info: calls getblockinfo with bestblockhash.
```http
GET http://cyphernode:8888/getbestblockinfo
```
Proxy response:
```json
{
"result":
{
"hash":"000000006f82a384c208ecfa04d05beea02d420f3f398ddda5c7f900de5718ea",
"confirmations":124329,
"strippedsize":8067,
"size":8067,
"weight":32268,
"height":1288528,
"version":536870912,
"versionHex":"20000000",
"merkleroot":"f1596255c357713c9827a739b17c8445cdcb81c4d336e24516c66f714a8f7030",
"tx":["65759382e7047f89f7e80676026c10401d47ce47ea997138251c59ca58f28a03","4d2d0cdd89061ce4bb8bdf5502aec9799e5065d205e8ab75c6598bd90a0e6e4c",[...],"a6d2fda52467aa7ca1271529ded5510bd12ad58f99f73fe995f50691aea4eb06"],
"time":1521404668,
"mediantime":1521398617,
"nonce":4209349744,
"bits":"1d00ffff",
"difficulty":1,
"chainwork":"000000000000000000000000000000000000000000000037e37554821063611f",
"nTx":32,
"previousblockhash":"000000005b55cefec377f7f73e656fef835a928b6eeb2060a89e7eb23a573c49",
"nextblockhash":"000000000ab13797b6dddd5e28d2ed62b937cd65ecf49b1f9d75108b9ea500f9"
},
"error":null,
"id":null
}
```
### Get a transaction details (node's getrawtransaction) (called by application)
Calls getrawtransaction RPC for the supplied txid.
```http
GET http://cyphernode:8888/gettransaction/af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648
```
Proxy response:
```json
{
"result":
{
"txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648",
"hash":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648",
"version":1,
"size":223,
"vsize":223,
"locktime":0,
"vin":[
{
"txid":"53a0e2ffa456b97d3944b652bba771221e60f4d852546bd6b351d33261b3e8b6",
"vout":0,
"scriptSig":
{
"asm":"30440220127c4adc1cf985cd884c383e69440ce4d48a0c4fdce6bf9d70faa0ee8092acb80220632cb6c99ded7f261814e602fc8fa8e7fe8cb6a95d45c497846b8624f7d19b3c[ALL] 03df001c8b58ac42b6cbfc2223b8efaa7e9a1911e529bd2c8b7f90140079034e75",
"hex":"4730440220127c4adc1cf985cd884c383e69440ce4d48a0c4fdce6bf9d70faa0ee8092acb80220632cb6c99ded7f261814e602fc8fa8e7fe8cb6a95d45c497846b8624f7d19b3c012103df001c8b58ac42b6cbfc2223b8efaa7e9a1911e529bd2c8b7f90140079034e75"
},
"sequence":4294967295
}
],
"vout":[
{
"value":0.84000000,
"n":0,
"scriptPubKey":
{
"asm":"OP_HASH160 c449a7fafb3b13b2952e064f2c3c58e851bb9430 OP_EQUAL",
"hex":"a914c449a7fafb3b13b2952e064f2c3c58e851bb943087",
"reqSigs":1,
"type":"scripthash",
"addresses":[
"2NB96fbwy8eoHttuZTtbwvvhEYrBwz494ov"
]
}
},
{
"value":0.01890000,
"n":1,
"scriptPubKey":
{
"asm":"OP_DUP OP_HASH160 b0379374df5eab8be9a21ee96711712bdb781a95 OP_EQUALVERIFY OP_CHECKSIG",
"hex":"76a914b0379374df5eab8be9a21ee96711712bdb781a9588ac",
"reqSigs":1,
"type":"pubkeyhash",
"addresses":[
"mwahoJcaVuy2TiMtGDZV9PaujFeD9z1a1q"
]
}
}
],
"hex":"0100000001b6e8b36132d351b3d66b5452d8f4601e2271a7bb52b644397db9[...]4df5eab8be9a21ee96711712bdb781a9588ac00000000",
"blockhash":"000000009249e7d725cc087cb781ade1dbfaf2bd777822948d5fccd4044f8299",
"confirmations":1106162,
"time":1415240575,
"blocktime":1415240575
},
"error":null,
"id":null
}
```
### Try missed callbacks (called by proxycronnode, a CRON job that retries if something went wrong)
[See cron_docker](../cron_docker)
Looks in DB for watched addresses, ask the watching Bitcoin node if those addresses got payments, if so it executes the callbacks that would be usually executed when "conf" is called by the node. This is useful if the watching node went down or there was a network glitch when a transaction on a watched address got confirmed.
```http
GET http://cyphernode:8888/executecallbacks
```
Proxy response: EMPTY
### Get spending wallet's balance (called by application)
Calls getbalance RPC on the spending wallet.
```http
GET http://cyphernode:8888/getbalance
```
Proxy response:
```json
{
"balance":1.51911837
}
```
### Get a new Bitcoin address from spending wallet (called by application)
Calls getnewaddress RPC on the spending wallet. Used to refill the spending wallet from cold wallet (ie Trezor).
```http
GET http://cyphernode:8888/getnewaddress
```
Proxy response:
```json
{
"address":"2NEC972DZpRM7SfuJUG9rYEix2P9A8qsNKF"
}
```
### Spend coins from spending wallet (called by application)
Calls sendtoaddress RPC on the spending wallet with supplied info.
```http
POST http://cyphernode:8888/spend
with body...
{"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233}
```
Proxy response:
```json
{
"status": "accepted",
"hash": "af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648"
}
```
### Add an output to the next batched transaction (called by application)
Inserts output information in the DB. Used when batchspend is called later.
```http
POST http://cyphernode:8888/addtobatch
with body...
{"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233}
```
Proxy response: EMPTY
### Spend a batched transaction with outputs added with addtobatch (called by application)
Calls sendmany RPC on spending wallet with the unspent "addtobatch" inserted outputs. Will be useful during next bull run.
```http
GET http://cyphernode:8888/batchspend
```
Proxy response:
```json
{
"status": "accepted",
"hash": "af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648"
}
```
### Get derived address(es) using path in config and provided index (called by application)
Derives addresses for supplied index. Must be used with derivation.pub32 and derivation.path properties in config.properties.
```http
GET http://cyphernode:8888/deriveindex/25-30
GET http://cyphernode:8888/deriveindex/34
```
Proxy response:
```json
{
"addresses":[
{"address":"2N6Q9kBcLtNswgMSLSQ5oduhbctk7hxEJW8"},
{"address":"2NFLhFghAPKEPuZCKoeXYYxuaBxhKXbmhBV"},
{"address":"2N7gepbQtRM5Hm4PTjvGadj9wAwEwnAsKiP"},
{"address":"2Mth8XDZpXkY9d95tort8HYEAuEesow2tF6"},
{"address":"2MwqEmAXhUw6H7bJwMhD13HGWVEj2HgFiNH"},
{"address":"2N2Y4BVRdrRFhweub2ehHXveGZC3nryMEJw"}
]
}
```
### Get derived address(es) using provided path and index (called by application)
Derives addresses for supplied pub32 and path. config.properties' derivation.pub32 and derivation.path are not used.
```http
POST http://cyphernode:8888/derivepubpath
with body...
{"pub32":"tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk","path":"0/25-30"}
or
{"pub32":"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb","path":"0/34"}
or
{"pub32":"vpub5SLqN2bLY4WeZF3kL4VqiWF1itbf3A6oRrq9aPf16AZMVWYCuN9TxpAZwCzVgW94TNzZPNc9XAHD4As6pdnExBtCDGYRmNJrcJ4eV9hNqcv","path":"0/25-30"}
```
Proxy response:
```json
{
"addresses":[
{"address":"mz3bWMW3BWGT9YGDjJwS8TfhJMMtZ91Frm"},
{"address":"mkjmKEX3KJrVpiqLSSxKB6jjgm3WhPnrv8"},
{"address":"mk43Tmf6E5nsmETTaNMTZK9TikaeVJRJ4a"},
{"address":"n1SEcVHHKpHyNr695JpXNdH6b9cWQ26qkt"},
{"address":"mzWqwZkA31kYVy1kpMoZgvfzSDyGgEi7Yg"},
{"address":"mp5jtEDNa88xfSQGs5yYQGk7guGWvaG4ci"}
]
}
```
### Get info from Lightning Network node (called by application)
Calls getinfo from lightningd. Useful to let your users know where to connect to.
```http
GET http://cyphernode:8888/ln_getinfo
```
Proxy response:
```json
{
"id": "03lightningnode",
"alias": "SatoshiPortal08",
"color": "008000",
"address": [
],
"binding": [
{
"type": "ipv6",
"address": "::",
"port": 9735
},
{
"type": "ipv4",
"address": "0.0.0.0",
"port": 9735
}
],
"version": "v0.6.1rc1-40-gae61f64",
"blockheight": 1412861,
"network": "testnet"
}
```
### Create a Lightning Network invoice (called by application)
Returns a LN invoice. Label must be unique. Description will be used by your user for payment. Expiry is in seconds.
```http
POST http://cyphernode:8888/ln_create_invoice
with body...
{"msatoshi":10000,"label":"koNCcrSvhX3dmyFhW","description":"Bylls order #10649","expiry":900}
```
Proxy response:
```json
{
"payment_hash": "fd27edf261d4b089c3478dece4f2c92c8c68db7be3999e89d452d39c083ad00f",
"expires_at": 1536593926,
"bolt11": "lntb100n1pdedryzpp5l5n7munp6jcgns683hkwfukf9jxx3kmmuwveazw52tfeczp66q8sdqagfukcmrnyphhyer9wgszxvfsxc6rjxqzuycqp2ak5feh7x7wkkt76uc5ptzcv90jhzhs5swzefv9344hnv74c25dvsstx7l24y46sx5tnkenu480pe06wtly2h5lrj63vszzgrxt4grkcqcltquj"
}
```
### Pay a Lightning Network invoice (called by application)
Make a LN payment. expected_msatoshi and expected_description are respectively the amount and description you gave your user for her to create the invoice; they must match the given bolt11 invoice supplied by your user.
```http
POST http://cyphernode:8888/ln_pay
with body...
{"bolt11":"lntb1pdca82tpp5gv8mn5jqlj6xztpnt4r472zcyrwf3y2c3cvm4uzg2gqcnj90f83qdp2gf5hgcm0d9hzqnm4w3kx2apqdaexgetjyq3nwvpcxgcqp2g3d86wwdfvyxcz7kce7d3n26d2rw3wf5tzpm2m5fl2z3mm8msa3xk8nv2y32gmzlhwjved980mcmkgq83u9wafq9n4w28amnmwzujgqpmapcr3","expected_msatoshi":10000,"expected_description":"Bitcoin Outlet order #7082"}
```
Proxy response:
```json
{
"id": 9,
"payment_hash": "85b8e69733202e126620e7745be9e23a6b544b758145d86848f3e513e6e1ca42",
"destination": "03whatever",
"msatoshi": 50000000,
"msatoshi_sent": 10000,
"created_at": 1537025047,
"status": "complete",
"payment_preimage": "fececdc787a007a721a1945b70cb022149cc2ee4268964c99ba37a877bded664",
"description": "Bitcoin Outlet order #7082",
"getroute_tries": 1,
"sendpay_tries": 1,
"route": [
{
"id": "03whatever",
"channel": "1413467:78:0",
"msatoshi": 10000,
"delay": 10
}
],
"failures": [
]
}
```
### Get a new Bitcoin address from the Lightning Network node (to fund it) (called by application)
Returns a Bitcoin bech32 address to fund your LN wallet.
```http
GET http://cyphernode:8888/ln_newaddr
```
Proxy response:
```json
{
"address": "tb1q9n8jfwe9qvlgczfxa5n4pe7haarqflzerqfhk9"
}
```

118
doc/INSTALL.md Normal file
View File

@@ -0,0 +1,118 @@
# Cyphernode
Indirection layer between client and Bitcoin-related services.
Here's the plan:
- The containers are not publicly exposing ports.
- Everything is accessible exclusively within the encrypted overlay network.
- If your system is distributed:
- ...should be doubly encrypted by an OpenVPN tunnel
- ...the hosts should be secured and the VPN tunnel should have limited scope by iptables rules on each host.
- We can have different Bitcoin Nodes for watching and spending, giving the flexibility to have different security models one each.
- Only the Proxy has Bitcoin Node RPC credentials.
- The Proxy is exclusively accessible by the Overlay network's containers.
- To manually manage the Proxy (and have access to it), one has to gain access to the Docker host servers as a docker user.
- **Coming soon**: added security to use the spending features of the Proxy with Trezor and Coldcard.
## Setting up
Default setup assumes your Bitcoin Node is already running somewhere. The reason is that it takes a lot of disk space and often already exists in your infrastructure, why not reusing it. After all, full blockchain sync takes a while.
You could also just uncomment it in the docker-compose file. If you run it in pruned mode, say so in config.properties. The computefees feature won't work in pruned mode.
### Set the swarm
(10.8.0.2 is the host's VPN IP address)
```shell
debian@dev:~/dev/Cyphernode$ docker swarm init --task-history-limit 1 --advertise-addr 10.8.0.2
Swarm initialized: current node (hufy324d291dyakizsuvjd0uw) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-2pxouynn9g8si42e8g9ujwy0v9po45axx367fy0fkjhzo3l1z8-75nirjfkobl7htvpfh986pyz3 10.8.0.2:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
```
### Create the Overlay Network and make sure your app joins it!
(if your app is not a Docker container, you will have to expose Cyphernode's port and secure it! In that case, use a reverse proxy with TLS)
```shell
debian@dev:~/dev/Cyphernode$ docker network create --driver=overlay --attachable --opt encrypted cyphernodenet
debian@dev:~/dev/Cyphernode$ docker network connect cyphernodenet yourappcontainer
```
### Configuration
```shell
debian@dev:~/dev/Cyphernode$ vi proxy_docker/env.properties
debian@dev:~/dev/Cyphernode$ vi proxy_docker/app/config/derivation.properties
debian@dev:~/dev/Cyphernode$ vi proxy_docker/app/config/watcher_btcnode_curlcfg.properties
debian@dev:~/dev/Cyphernode$ vi proxy_docker/app/config/spender_btcnode_curlcfg.properties
debian@dev:~/dev/Cyphernode$ vi proxy_docker/app/config/config.properties
debian@dev:~/dev/Cyphernode$ vi cron_docker/env.properties
debian@dev:~/dev/Cyphernode$ vi pycoin_docker/env.properties
```
### Build cron image
[See how to build proxycron image](../cron_docker)
### Build btcproxy image
[See how to build btcproxy image](../proxy_docker)
### Build pycoin image
[See how to build pycoin image](../pycoin_docker)
### Build clightning image
[See how to build clightning image](https://github.com/SatoshiPortal/dockers/tree/master/rpi/LN/c-lightning)
### Deploy
```shell
debian@dev:~/dev/Cyphernode$ docker stack deploy --compose-file docker-compose.yml cyphernodestack
Creating service cyphernodestack_cyphernode
Creating service cyphernodestack_proxycronnode
Creating service cyphernodestack_pycoinnode
Creating service cyphernodestack_clightningnode
```
## Off-site Bitcoin Node
### Join swarm created on Cyphernode server
```shell
pi@SP-BTC01:~ $ docker swarm join --token SWMTKN-1-2pxouynn9g8si42e8g9ujwy0v9po45axx367fy0fkjhzo3l1z8-75nirjfkobl7htvpfh986pyz3 10.8.0.2:2377
```
### Build node container image
[See how to build Bitcoin Node image](https://github.com/SatoshiPortal/dockers/tree/master/rpi/bitcoin-core)
### Run node or connect already-running node
```shell
pi@SP-BTC01:~ $ docker run -d --rm --mount type=bind,source="$HOME/.bitcoin",target="/bitcoinuser/.bitcoin" --name btcnode --network cyphernodenet btcnode
```
```shell
pi@SP-BTC01:~ $ docker network connect cyphernodenet btcnode
```
## Test deployment (from any host)
```shell
echo "GET /getbestblockinfo" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 -
echo "GET /getbalance" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 -
echo "GET /getbestblockhash" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 -
echo "GET /getblockinfo/00000000a64e0d1ae0c39166f4e8717a672daf3d61bf7bbb41b0f487fcae74d2" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 -
curl -v -H "Content-Type: application/json" -d '{"address":"2MsWyaQ8APbnqasFpWopqUKqsdpiVY3EwLE","amount":0.2}' cyphernode:8888/spend
echo "GET /ln_getinfo" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 -
echo "GET /ln_newaddr" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 -
curl -v -H "Content-Type: application/json" -d '{"msatoshi":10000,"label":"koNCcrSvhX3dmyFhW","description":"Bylls order #10649","expiry":900}' cyphernode:8888/ln_create_invoice
curl -v -H "Content-Type: application/json" -d '{"bolt11":"lntb1pdca82tpp5gv8mn5jqlj6xztpnt4r472zcyrwf3y2c3cvm4uzg2gqcnj90f83qdp2gf5hgcm0d9hzqnm4w3kx2apqdaexgetjyq3nwvpcxgcqp2g3d86wwdfvyxcz7kce7d3n26d2rw3wf5tzpm2m5fl2z3mm8msa3xk8nv2y32gmzlhwjved980mcmkgq83u9wafq9n4w28amnmwzujgqpmapcr3","msatoshi":10000,"description":"Bitcoin Outlet order #7082"}' cyphernode:8888/ln_pay
```

75
docker-compose.yml Normal file
View File

@@ -0,0 +1,75 @@
version: "3"
services:
cyphernode:
# Bitcoin Mini Proxy
env_file:
- proxy_docker/env.properties
image: btcproxyimg
# ports:
# - "8888:8888"
volumes:
# Variable substitutions don't work
# Match with DB_PATH in proxy_docker/env.properties
- "~/btcproxydb:/proxyuser/db"
- "~/.lightning:/proxyuser/.lightning"
deploy:
placement:
constraints: [node.hostname==dev]
networks:
- cyphernodenet
proxycronnode:
# Async jobs
env_file:
- cron_docker/env.properties
image: proxycronimg
deploy:
placement:
constraints: [node.hostname==dev]
networks:
- cyphernodenet
pycoinnode:
# Pycoin
env_file:
- pycoin_docker/env.properties
image: pycoinimg
# ports:
# - "7777:7777"
deploy:
placement:
constraints: [node.hostname==dev]
networks:
- cyphernodenet
clightningnode:
# c-lightning lightning network node
image: clnimg
ports:
- "9735:9735"
volumes:
- "~/.lightning:/lnuser/.lightning"
deploy:
placement:
constraints: [node.hostname==dev]
networks:
- cyphernodenet
# spbtcnode:
# Bitcoin node
# image: btcnode
# ports:
# - "18333:18333"
# - "18332:18332"
# - "29000:29000"
# - "8333:8333"
# - "8332:8332"
# volumes:
# - "~/.bitcoin:/bitcoinuser/.bitcoin"
# networks:
# - cyphernodenet
networks:
cyphernodenet:
external: true

56
proxy_docker/Dockerfile Normal file
View File

@@ -0,0 +1,56 @@
FROM alpine
ARG USER_ID
ARG GROUP_ID
ENV USERNAME proxyuser
ENV HOME /${USERNAME}
ENV USER_ID ${USER_ID:-1000}
ENV GROUP_ID ${GROUP_ID:-1000}
RUN addgroup -g ${GROUP_ID} ${USERNAME} \
&& adduser -u ${USER_ID} -G ${USERNAME} -D -s /bin/sh -h ${HOME} ${USERNAME}
RUN apk add --update --no-cache \
sqlite \
jq \
curl
COPY --chown=proxyuser app/script/callbacks_job.sh ${HOME}/callbacks_job.sh
COPY --chown=proxyuser app/script/blockchainrpc.sh ${HOME}/blockchainrpc.sh
COPY --chown=proxyuser app/script/call_lightningd.sh ${HOME}/call_lightningd.sh
COPY --chown=proxyuser app/script/bitcoin.sh ${HOME}/bitcoin.sh
COPY --chown=proxyuser app/script/requesthandler.sh ${HOME}/requesthandler.sh
COPY --chown=proxyuser app/script/watchrequest.sh ${HOME}/watchrequest.sh
COPY --chown=proxyuser app/script/walletoperations.sh ${HOME}/walletoperations.sh
COPY --chown=proxyuser app/script/confirmation.sh ${HOME}/confirmation.sh
COPY --chown=proxyuser app/config/watcher_btcnode_curlcfg.properties ${HOME}/watcher_btcnode_curlcfg.properties
COPY --chown=proxyuser app/config/spender_btcnode_curlcfg.properties ${HOME}/spender_btcnode_curlcfg.properties
COPY --chown=proxyuser app/config/config.properties ${HOME}/config.properties
COPY --chown=proxyuser app/script/startproxy.sh ${HOME}/startproxy.sh
COPY --chown=proxyuser app/script/trace.sh ${HOME}/trace.sh
COPY --chown=proxyuser app/script/sendtobitcoinnode.sh ${HOME}/sendtobitcoinnode.sh
COPY --chown=proxyuser app/script/responsetoclient.sh ${HOME}/responsetoclient.sh
COPY --chown=proxyuser app/script/importaddress.sh ${HOME}/importaddress.sh
COPY --chown=proxyuser app/script/sql.sh ${HOME}/sql.sh
COPY --chown=proxyuser app/data/watching.sql ${HOME}/watching.sql
COPY --chown=proxyuser app/script/computefees.sh ${HOME}/computefees.sh
COPY --chown=proxyuser app/script/unwatchrequest.sh ${HOME}/unwatchrequest.sh
COPY --chown=proxyuser app/script/getactivewatches.sh ${HOME}/getactivewatches.sh
COPY --chown=proxyuser app/script/utils.sh ${HOME}/utils.sh
COPY --chown=proxyuser app/script/manage_missed_conf.sh ${HOME}/manage_missed_conf.sh
COPY --chown=proxyuser app/script/tests.sh ${HOME}/tests.sh
COPY --chown=proxyuser app/script/tests-cb.sh ${HOME}/tests-cb.sh
COPY --chown=proxyuser app/bin/lightning-cli_x86 ${HOME}/lightning-cli
USER ${USERNAME}
WORKDIR ${HOME}
RUN chmod +x startproxy.sh requesthandler.sh \
&& chmod 600 watcher_btcnode_curlcfg.properties \
&& chmod 600 spender_btcnode_curlcfg.properties \
&& chmod 600 config.properties \
&& mkdir db
VOLUME ["${HOME}/db", "${HOME}/.lightning"]
ENTRYPOINT ["./startproxy.sh"]

520
proxy_docker/README.md Normal file
View File

@@ -0,0 +1,520 @@
# Cyphernode Proxy
We assume you are the user pi on a Raspberry Pi.
## Create proxyuser that will run the processes
Log in your host and:
```shell
sudo useradd proxyuser
```
## Configure your container by modifying `env.properties` file
```properties
TRACING=1
WATCHER_BTC_NODE_RPC_URL=btcnode:18332/wallet/watching01.dat
SPENDER_BTC_NODE_RPC_URL=btcnode:18332/wallet/spending01.dat
PROXY_LISTENING_PORT=8888
# Variable substitutions don't work
DB_PATH=/proxyuser/db
DB_FILE=/proxyuser/db/proxydb
# Pycoin container
PYCOIN_CONTAINER=pycoinnode:7777
# OTS container
OTS_CONTAINER=otsnode:6666
```
## Set your Watching Bitcoin node RPC credentials in `app/config/watcher_btcnode_curlcfg.properties`
```properties
user=rpc_username:rpc_password
```
## Set your Spending Bitcoin node RPC credentials in `app/config/spender_btcnode_curlcfg.properties`
```properties
user=rpc_username:rpc_password
```
## Set your address derivation information in `app/config/derivation.properties`
```properties
derivation.xpub=upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb
derivation.path=0/n
watchingnode.pruned=false
```
## Building docker image
```shell
docker build -t btcproxyimg --build-arg USER_ID=$(id -u proxyuser) --build-arg GROUP_ID=$(id -g proxyuser) .
```
## Create sqlite3 database path and give rights
```shell
mkdir ~/btcproxydb ; sudo chown -R proxyuser:pi ~/btcproxydb ; sudo chmod g+ws ~/btcproxydb
```
## What you MUST have in your Watching Bitcoin node's bitcoin.conf file
(or something similar)
(*Not in the Spending Bitcoin node!*)
```properties
walletnotify=curl cyphernode:8888/conf/%s
```
## Current API
### Watch a Bitcoin Address (called by application)
```http
POST http://cyphernode:8888/watch
with body...
{"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","unconfirmedCallbackURL":"192.168.111.233:1111/callback0conf","confirmedCallbackURL":"192.168.111.233:1111/callback1conf"}
```
Proxy response:
```json
{
"id": "291",
"event": "watch",
"imported": "1",
"inserted": "1",
"address": "2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp",
"unconfirmedCallbackURL": "192.168.133.233:1111/callback0conf",
"confirmedCallbackURL": "192.168.133.233:1111/callback1conf",
"estimatesmartfee2blocks": "0.000010",
"estimatesmartfee6blocks": "0.000010",
"estimatesmartfee36blocks": "0.000010",
"estimatesmartfee144blocks": "0.000010"
}
```
### Un-watch a previously watched Bitcoin Address (called by application)
```http
GET http://cyphernode:8888/unwatch/2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp
```
Proxy response:
```json
{
"event": "unwatch",
"address": "2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp"
}
```
### Get a list of Bitcoin addresses being watched (called by application)
```http
GET http://cyphernode:8888/getactivewatches
```
Proxy response:
```json
{
"watches": [
{
"id":"291",
"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp",
"imported":"1",
"unconfirmedCallbackURL":"192.168.133.233:1111/callback0conf",
"confirmedCallbackURL":"192.168.133.233:1111/callback1conf",
"watching_since":"2018-09-06 21:14:03"}
]
}
```
### Confirm a Transaction on Watched Address (called by Bitcoin node on transaction confirmations)
```http
GET http://cyphernode:8888/conf/b081ca7724386f549cf0c16f71db6affeb52ff7a0d9b606fb2e5c43faffd3387
```
Proxy response:
```json
{
"result":"confirmed"
}
```
### Get the Best Block Hash (called by application)
```http
GET http://cyphernode:8888/getbestblockhash
```
Proxy response:
```json
{
"result":"00000000000000262588c21afbf9e1da151daf10b11215d501271163f26ea74a",
"error":null,
"id":null
}
```
### Get Block Info (called by application)
```http
GET http://cyphernode:8888/getblockinfo/000000006f82a384c208ecfa04d05beea02d420f3f398ddda5c7f900de5718ea
```
Proxy response:
```json
{
"result":
{
"hash":"000000006f82a384c208ecfa04d05beea02d420f3f398ddda5c7f900de5718ea",
"confirmations":124329,
"strippedsize":8067,
"size":8067,
"weight":32268,
"height":1288528,
"version":536870912,
"versionHex":"20000000",
"merkleroot":"f1596255c357713c9827a739b17c8445cdcb81c4d336e24516c66f714a8f7030",
"tx":["65759382e7047f89f7e80676026c10401d47ce47ea997138251c59ca58f28a03","4d2d0cdd89061ce4bb8bdf5502aec9799e5065d205e8ab75c6598bd90a0e6e4c",[...],"a6d2fda52467aa7ca1271529ded5510bd12ad58f99f73fe995f50691aea4eb06"],
"time":1521404668,
"mediantime":1521398617,
"nonce":4209349744,
"bits":"1d00ffff",
"difficulty":1,
"chainwork":"000000000000000000000000000000000000000000000037e37554821063611f",
"nTx":32,
"previousblockhash":"000000005b55cefec377f7f73e656fef835a928b6eeb2060a89e7eb23a573c49",
"nextblockhash":"000000000ab13797b6dddd5e28d2ed62b937cd65ecf49b1f9d75108b9ea500f9"
},
"error":null,
"id":null
}
```
### Get the Best Block Info (called by application)
```http
GET http://cyphernode:8888/getbestblockinfo
```
Proxy response:
```json
{
"result":
{
"hash":"000000006f82a384c208ecfa04d05beea02d420f3f398ddda5c7f900de5718ea",
"confirmations":124329,
"strippedsize":8067,
"size":8067,
"weight":32268,
"height":1288528,
"version":536870912,
"versionHex":"20000000",
"merkleroot":"f1596255c357713c9827a739b17c8445cdcb81c4d336e24516c66f714a8f7030",
"tx":["65759382e7047f89f7e80676026c10401d47ce47ea997138251c59ca58f28a03","4d2d0cdd89061ce4bb8bdf5502aec9799e5065d205e8ab75c6598bd90a0e6e4c",[...],"a6d2fda52467aa7ca1271529ded5510bd12ad58f99f73fe995f50691aea4eb06"],
"time":1521404668,
"mediantime":1521398617,
"nonce":4209349744,
"bits":"1d00ffff",
"difficulty":1,
"chainwork":"000000000000000000000000000000000000000000000037e37554821063611f",
"nTx":32,
"previousblockhash":"000000005b55cefec377f7f73e656fef835a928b6eeb2060a89e7eb23a573c49",
"nextblockhash":"000000000ab13797b6dddd5e28d2ed62b937cd65ecf49b1f9d75108b9ea500f9"
},
"error":null,
"id":null
}
```
### Get a transaction details (node's getrawtransaction) (called by application)
```http
GET http://cyphernode:8888/gettransaction/af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648
```
Proxy response:
```json
{
"result":
{
"txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648",
"hash":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648",
"version":1,
"size":223,
"vsize":223,
"locktime":0,
"vin":[
{
"txid":"53a0e2ffa456b97d3944b652bba771221e60f4d852546bd6b351d33261b3e8b6",
"vout":0,
"scriptSig":
{
"asm":"30440220127c4adc1cf985cd884c383e69440ce4d48a0c4fdce6bf9d70faa0ee8092acb80220632cb6c99ded7f261814e602fc8fa8e7fe8cb6a95d45c497846b8624f7d19b3c[ALL] 03df001c8b58ac42b6cbfc2223b8efaa7e9a1911e529bd2c8b7f90140079034e75",
"hex":"4730440220127c4adc1cf985cd884c383e69440ce4d48a0c4fdce6bf9d70faa0ee8092acb80220632cb6c99ded7f261814e602fc8fa8e7fe8cb6a95d45c497846b8624f7d19b3c012103df001c8b58ac42b6cbfc2223b8efaa7e9a1911e529bd2c8b7f90140079034e75"
},
"sequence":4294967295
}
],
"vout":[
{
"value":0.84000000,
"n":0,
"scriptPubKey":
{
"asm":"OP_HASH160 c449a7fafb3b13b2952e064f2c3c58e851bb9430 OP_EQUAL",
"hex":"a914c449a7fafb3b13b2952e064f2c3c58e851bb943087",
"reqSigs":1,
"type":"scripthash",
"addresses":[
"2NB96fbwy8eoHttuZTtbwvvhEYrBwz494ov"
]
}
},
{
"value":0.01890000,
"n":1,
"scriptPubKey":
{
"asm":"OP_DUP OP_HASH160 b0379374df5eab8be9a21ee96711712bdb781a95 OP_EQUALVERIFY OP_CHECKSIG",
"hex":"76a914b0379374df5eab8be9a21ee96711712bdb781a9588ac",
"reqSigs":1,
"type":"pubkeyhash",
"addresses":[
"mwahoJcaVuy2TiMtGDZV9PaujFeD9z1a1q"
]
}
}
],
"hex":"0100000001b6e8b36132d351b3d66b5452d8f4601e2271a7bb52b644397db9[...]4df5eab8be9a21ee96711712bdb781a9588ac00000000",
"blockhash":"000000009249e7d725cc087cb781ade1dbfaf2bd777822948d5fccd4044f8299",
"confirmations":1106162,
"time":1415240575,
"blocktime":1415240575
},
"error":null,
"id":null
}
```
### Try missed callbacks (called by proxycronnode, a CRON job that retries if something went wrong)
[See cron_docker](../cron_docker)
```http
GET http://cyphernode:8888/executecallbacks
```
Proxy response: EMPTY
### Get spending wallet's balance (called by application)
```http
GET http://cyphernode:8888/getbalance
```
Proxy response:
```json
{
"balance":1.51911837
}
```
### Get a new Bitcoin address from spending wallet (called by application)
```http
GET http://cyphernode:8888/getnewaddress
```
Proxy response:
```json
{
"address":"2NEC972DZpRM7SfuJUG9rYEix2P9A8qsNKF"
}
```
### Spend coins from spending wallet (called by application)
```http
POST http://cyphernode:8888/spend
with body...
{"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233}
```
Proxy response:
```json
{
"status": "accepted",
"hash": "af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648"
}
```
### Add an output to the next batched transaction (called by application)
```http
POST http://cyphernode:8888/addtobatch
with body...
{"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233}
```
Proxy response: EMPTY
### Spend a batched transaction with outputs added with addtobatch (called by application)
```http
GET http://cyphernode:8888/batchspend
```
Proxy response:
```json
{
"status": "accepted",
"hash": "af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648"
}
```
### Get derived address(es) using path in config and provided index (called by application)
```http
GET http://cyphernode:8888/deriveindex/25-30
GET http://cyphernode:8888/deriveindex/34
```
Proxy response:
```json
{
"addresses":[
{"address":"2N6Q9kBcLtNswgMSLSQ5oduhbctk7hxEJW8"},
{"address":"2NFLhFghAPKEPuZCKoeXYYxuaBxhKXbmhBV"},
{"address":"2N7gepbQtRM5Hm4PTjvGadj9wAwEwnAsKiP"},
{"address":"2Mth8XDZpXkY9d95tort8HYEAuEesow2tF6"},
{"address":"2MwqEmAXhUw6H7bJwMhD13HGWVEj2HgFiNH"},
{"address":"2N2Y4BVRdrRFhweub2ehHXveGZC3nryMEJw"}
]
}
```
### Get derived address(es) using provided path and index (called by application)
```http
POST http://cyphernode:8888/derivepubpath
with body...
{"pub32":"tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk","path":"0/25-30"}
or
{"pub32":"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb","path":"0/34"}
or
{"pub32":"vpub5SLqN2bLY4WeZF3kL4VqiWF1itbf3A6oRrq9aPf16AZMVWYCuN9TxpAZwCzVgW94TNzZPNc9XAHD4As6pdnExBtCDGYRmNJrcJ4eV9hNqcv","path":"0/25-30"}
```
Proxy response:
```json
{
"addresses":[
{"address":"mz3bWMW3BWGT9YGDjJwS8TfhJMMtZ91Frm"},
{"address":"mkjmKEX3KJrVpiqLSSxKB6jjgm3WhPnrv8"},
{"address":"mk43Tmf6E5nsmETTaNMTZK9TikaeVJRJ4a"},
{"address":"n1SEcVHHKpHyNr695JpXNdH6b9cWQ26qkt"},
{"address":"mzWqwZkA31kYVy1kpMoZgvfzSDyGgEi7Yg"},
{"address":"mp5jtEDNa88xfSQGs5yYQGk7guGWvaG4ci"}
]
}
```
### Get info from Lightning Network node (called by application)
```http
GET http://cyphernode:8888/ln_getinfo
```
Proxy response:
```json
{
"id": "03bb990f43e6a6eccb223288d32fcb91209b12370c0a8bf5cdf4ad7bc11e33f253",
"alias": "SatoshiPortal01",
"color": "008000",
"address": [
],
"binding": [
{
"type": "ipv6",
"address": "::",
"port": 9735
},
{
"type": "ipv4",
"address": "0.0.0.0",
"port": 9735
}
],
"version": "v0.6.1rc1-40-gae61f64",
"blockheight": 1412861,
"network": "testnet"
}
```
### Create a Lightning Network invoice (called by application)
```http
POST http://cyphernode:8888/ln_create_invoice
with body...
{"msatoshi":10000,"label":"koNCcrSvhX3dmyFhW","description":"Bylls order #10649","expiry":900}
```
Proxy response:
```json
{
"payment_hash": "fd27edf261d4b089c3478dece4f2c92c8c68db7be3999e89d452d39c083ad00f",
"expires_at": 1536593926,
"bolt11": "lntb100n1pdedryzpp5l5n7munp6jcgns683hkwfukf9jxx3kmmuwveazw52tfeczp66q8sdqagfukcmrnyphhyer9wgszxvfsxc6rjxqzuycqp2ak5feh7x7wkkt76uc5ptzcv90jhzhs5swzefv9344hnv74c25dvsstx7l24y46sx5tnkenu480pe06wtly2h5lrj63vszzgrxt4grkcqcltquj"
}
```
### Pay a Lightning Network invoice (called by application)
```http
POST http://cyphernode:8888/ln_pay
with body...
{"bolt11":"lntb1pdca82tpp5gv8mn5jqlj6xztpnt4r472zcyrwf3y2c3cvm4uzg2gqcnj90f83qdp2gf5hgcm0d9hzqnm4w3kx2apqdaexgetjyq3nwvpcxgcqp2g3d86wwdfvyxcz7kce7d3n26d2rw3wf5tzpm2m5fl2z3mm8msa3xk8nv2y32gmzlhwjved980mcmkgq83u9wafq9n4w28amnmwzujgqpmapcr3","expected_msatoshi":10000,"expected_description":"Bitcoin Outlet order #7082"}
```
Proxy response:
```json
{
"": ""
}
```
### Get a new Bitcoin address from the Lightning Network node (to fund it) (called by application)
```http
GET http://cyphernode:8888/ln_newaddr
```
Proxy response:
```json
{
"address": "tb1q9n8jfwe9qvlgczfxa5n4pe7haarqflzerqfhk9"
}
```

View File

@@ -0,0 +1,5 @@
# Nota bene
lightning-cli binary has been pre-compiled in an Alpine docker container.
Use lightning-cli_arm for armhf architecture, lightning-cli_x86 for x86_64.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
derivation.pub32=upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb
derivation.path=0/n
watchingnode.pruned=false

View File

@@ -0,0 +1 @@
user=rpc_username:rpc_password

View File

@@ -0,0 +1 @@
user=rpc_username:rpc_password

View File

@@ -0,0 +1,55 @@
PRAGMA foreign_keys = ON;
CREATE TABLE watching (
id INTEGER PRIMARY KEY AUTOINCREMENT,
address TEXT,
watching INTEGER DEFAULT FALSE,
callback0conf TEXT,
calledback0conf INTEGER DEFAULT FALSE,
callback1conf TEXT,
calledback1conf INTEGER DEFAULT FALSE,
imported INTEGER DEFAULT FALSE,
inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_watching_address ON watching (address);
CREATE TABLE watching_tx (
watching_id INTEGER REFERENCES watching,
tx_id INTEGER REFERENCES tx,
vout INTEGER,
amount REAL
);
CREATE UNIQUE INDEX idx_watching_tx ON watching_tx (watching_id, tx_id);
CREATE TABLE tx (
id INTEGER PRIMARY KEY AUTOINCREMENT,
txid TEXT UNIQUE,
hash TEXT UNIQUE,
confirmations INTEGER DEFAULT 0,
timereceived INTEGER,
fee REAL,
size INTEGER,
vsize INTEGER,
is_replaceable INTEGER,
blockhash TEXT,
blockheight INTEGER,
blocktime INTEGER,
raw_tx TEXT,
inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_tx_timereceived ON tx (timereceived);
CREATE INDEX idx_tx_fee ON tx (fee);
CREATE INDEX idx_tx_size ON tx (size);
CREATE INDEX idx_tx_vsize ON tx (vsize);
CREATE INDEX idx_tx_blockhash ON tx (blockhash);
CREATE INDEX idx_tx_blockheight ON tx (blockheight);
CREATE INDEX idx_tx_blocktime ON tx (blocktime);
CREATE TABLE recipient (
id INTEGER PRIMARY KEY AUTOINCREMENT,
address TEXT,
amount REAL,
tx_id INTEGER REFERENCES tx,
inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_recipient_address ON recipient (address);

View File

@@ -0,0 +1,46 @@
#!/bin/sh
. ./trace.sh
. ./utils.sh
deriveindex()
{
trace "Entering deriveindex()..."
local index=${1}
trace "[deriveindex] index=${index}"
local pub32=$(get_prop "derivation.pub32")
local path=$(get_prop "derivation.path" | sed -En "s/n/${index}/p")
# pub32=$(grep "derivation.pub32" config.properties | cut -d'=' -f2)
# path=$(grep "derivation.path" config.properties | cut -d'=' -f2 | sed -En "s/n/${index}/p")
local data="{\"pub32\":\"${pub32}\",\"path\":\"${path}\"}"
trace "[deriveindex] data=${data}"
send_to_pycoin "${data}"
return $?
}
send_to_pycoin()
{
trace "Entering send_to_pycoin()..."
local data=${1}
local result
local returncode
trace "[send_to_pycoin] curl -s -H \"Content-Type: application/json\" -d \"${data}\" ${PYCOIN_CONTAINER}/derive"
result=$(curl -s -H "Content-Type: application/json" -d "${data}" ${PYCOIN_CONTAINER}/derive)
returncode=$?
trace_rc ${returncode}
trace "[send_to_pycoin] result=${result}"
# Output response to stdout before exiting with return code
echo "${result}"
trace_rc ${returncode}
return ${returncode}
}

View File

@@ -0,0 +1,79 @@
#!/bin/sh
. ./trace.sh
. ./sendtobitcoinnode.sh
get_best_block_hash()
{
trace "Entering get_best_block_hash()..."
local data='{"method":"getbestblockhash"}'
send_to_watcher_node "${data}"
return $?
}
getestimatesmartfee()
{
trace "Entering getestimatesmartfee()..."
local nb_blocks=${1}
trace "[getestimatesmartfee] nb_blocks=${nb_blocks}"
send_to_watcher_node "{\"method\":\"estimatesmartfee\",\"params\":[${nb_blocks}]}" | jq ".result.feerate" | awk '{ printf "%.8f", $0 }'
return $?
}
get_block_info()
{
trace "Entering get_block_info()..."
local block_hash=${1}
trace "[get_block_info] block_hash=${block_hash}"
local data="{\"method\":\"getblock\",\"params\":[\"${block_hash}\"]}"
trace "[get_block_info] data=${data}"
send_to_watcher_node "${data}"
return $?
}
get_best_block_info()
{
trace "Entering get_best_block_info()..."
local block_hash=$(echo "$(get_best_block_hash)" | jq ".result" | tr -d '"')
trace "[get_best_block_info] block_hash=${block_hash}"
get_block_info ${block_hash}
return $?
}
get_rawtransaction()
{
trace "Entering get_rawtransaction()..."
local txid=${1}
trace "[get_rawtransaction] txid=${txid}"
local rawtx
rawtx=$(sql "SELECT raw_tx FROM tx WHERE txid=\"${txid}\"")
if [ -z ${rawtx} ]; then
trace "[get_rawtransaction] rawtx not found in DB, let's fetch the Bitcoin node"
local data="{\"method\":\"getrawtransaction\",\"params\":[\"${txid}\",true]}"
trace "[get_rawtransaction] data=${data}"
send_to_watcher_node "${data}"
return $?
else
trace "[get_rawtransaction] rawtx found in DB, no need to fetch the Bitcoin node"
echo ${rawtx}
return 0
fi
}
get_transaction()
{
trace "Entering get_transaction()..."
local txid=${1}
trace "[get_transaction] txid=${txid}"
local data="{\"method\":\"gettransaction\",\"params\":[\"${txid}\",true]}"
trace "[get_transaction] data=${data}"
send_to_watcher_node "${data}"
return $?
}

View File

@@ -0,0 +1,102 @@
#!/bin/sh
. ./trace.sh
ln_create_invoice()
{
trace "Entering ln_create_invoice()..."
local result
local request=${1}
local msatoshi=$(echo "${request}" | jq ".msatoshi" | tr -d '"')
trace "[ln_create_invoice] msatoshi=${msatoshi}"
local label=$(echo "${request}" | jq ".label")
trace "[ln_create_invoice] label=${label}"
local description=$(echo "${request}" | jq ".description")
trace "[ln_create_invoice] description=${description}"
local expiry=$(echo "${request}" | jq ".expiry" | tr -d '"')
trace "[ln_create_invoice] expiry=${expiry}"
result=$(./lightning-cli invoice ${msatoshi} "${label}" "${description}" ${expiry})
returncode=$?
trace_rc ${returncode}
trace "[ln_create_invoice] result=${result}"
echo "${result}"
return ${returncode}
}
ln_getinfo()
{
trace "Entering ln_get_info()..."
local result
result=$(./lightning-cli getinfo)
returncode=$?
trace_rc ${returncode}
trace "[ln_getinfo] result=${result}"
echo "${result}"
return ${returncode}
}
ln_pay() {
trace "Entering ln_pay()..."
local result
local request=${1}
local bolt11=$(echo "${request}" | jq ".bolt11" | tr -d '"')
trace "[ln_pay] bolt11=${bolt11}"
local expected_msatoshi=$(echo "${request}" | jq ".expected_msatoshi")
trace "[ln_pay] expected_msatoshi=${expected_msatoshi}"
local expected_description=$(echo "${request}" | jq ".expected_description")
trace "[ln_pay] expected_description=${expected_description}"
result=$(./lightning-cli decodepay ${bolt11})
local invoice_msatoshi=$(echo "${result}" | jq ".msatoshi")
trace "[ln_pay] invoice_msatoshi=${invoice_msatoshi}"
local invoice_description=$(echo "${result}" | jq ".description")
trace "[ln_pay] invoice_description=${invoice_description}"
if [ "${expected_msatoshi}" != "${invoice_msatoshi}" ]; then
result="{\"result\":\"error\",\"expected_msatoshi\":${expected_msatoshi},\"invoice_msatoshi\":${invoice_msatoshi}}"
returncode=1
elif [ "${expected_description}" != "${invoice_description}" ]; then
result="{\"result\":\"error\",\"expected_description\":${expected_description},\"invoice_description\":${invoice_description}}"
returncode=1
else
result=$(./lightning-cli pay ${bolt11})
returncode=$?
trace_rc ${returncode}
fi
trace "[ln_pay] result=${result}"
echo "${result}"
return ${returncode}
}
ln_newaddr()
{
trace "Entering ln_newaddr()..."
local result
call_lightningd newaddr
result=$(./lightning-cli newaddr)
returncode=$?
trace_rc ${returncode}
trace "[ln_newaddr] result=${result}"
echo "${result}"
return ${returncode}
}
case "${0}" in *call_lightningd.sh) call_lightningd $@;; esac

View File

@@ -0,0 +1,148 @@
#!/bin/sh
. ./trace.sh
. ./sql.sh
do_callbacks()
{
(
flock -x 200 || return 0
trace "Entering do_callbacks()..."
# Let's fetch all the watching addresses still being watched but not called back
local callbacks=$(sql 'SELECT DISTINCT callback0conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id, is_replaceable FROM watching w LEFT JOIN watching_tx ON w.id = watching_id LEFT JOIN tx ON tx.id = tx_id WHERE NOT calledback0conf and watching_id NOT NULL and callback0conf NOT NULL and watching')
trace "[do_callbacks] callbacks0conf=${callbacks}"
local returncode
local address
local IFS=$'\n'
for row in ${callbacks}
do
build_callback ${row}
returncode=$?
trace_rc ${returncode}
if [ "${returncode}" -eq 0 ]; then
address=$(echo "${row}" | cut -d '|' -f2)
sql "UPDATE watching SET calledback0conf=1 WHERE address=\"${address}\""
trace_rc $?
fi
done
callbacks=$(sql 'SELECT DISTINCT callback1conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id, is_replaceable FROM watching w, watching_tx wt, tx t WHERE w.id = watching_id AND tx_id = t.id AND NOT calledback1conf and confirmations>0 and callback1conf NOT NULL and watching')
trace "[do_callbacks] callbacks1conf=${callbacks}"
for row in ${callbacks}
do
build_callback ${row}
returncode=$?
if [ "${returncode}" -eq 0 ]; then
address=$(echo "${row}" | cut -d '|' -f2)
sql "UPDATE watching SET calledback1conf=1, watching=0 WHERE address=\"${address}\""
trace_rc $?
fi
done
) 200>./.callbacks.lock
}
build_callback()
{
trace "Entering build_callback()..."
local row=$@
local id
local url
local data
local address
local txid
local vout_n
local sent_amount
local confirmations
local ts_firstseen
local fee
local size
local vsize
local blockhash
local blocktime
local blockheight
# callback0conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id
trace "[build_callback] row=${row}"
id=$(echo "${row}" | cut -d '|' -f14)
trace "[build_callback] id=${id}"
url=$(echo "${row}" | cut -d '|' -f1)
trace "[build_callback] url=${url}"
address=$(echo "${row}" | cut -d '|' -f2)
trace "[build_callback] address=${address}"
txid=$(echo "${row}" | cut -d '|' -f3)
trace "[build_callback] txid=${txid}"
vout_n=$(echo "${row}" | cut -d '|' -f4)
trace "[build_callback] vout_n=${vout_n}"
sent_amount=$(echo "${row}" | cut -d '|' -f5)
trace "[build_callback] sent_amount=${sent_amount}"
confirmations=$(echo "${row}" | cut -d '|' -f6)
trace "[build_callback] confirmations=${confirmations}"
ts_firstseen=$(echo "${row}" | cut -d '|' -f7)
trace "[build_callback] ts_firstseen=${ts_firstseen}"
# If node in pruned mode, we can't calculate the fees and then we don't want
# to send 0.00000000 as fees but empty string to distinguish.
fee=$(echo "${row}" | cut -d '|' -f8)
if [ -n "${fee}" ]; then
fee=$(echo "${fee}" | awk '{ printf "%.8f", $0 }')
fi
trace "[build_callback] fee=${fee}"
size=$(echo "${row}" | cut -d '|' -f9)
trace "[build_callback] size=${size}"
vsize=$(echo "${row}" | cut -d '|' -f10)
trace "[build_callback] vsize=${vsize}"
is_replaceable=$(echo "${row}" | cut -d '|' -f15)
trace "[build_callback] is_replaceable=${is_replaceable}"
blockhash=$(echo "${row}" | cut -d '|' -f11)
trace "[build_callback] blockhash=${blockhash}"
blockheight=$(echo "${row}" | cut -d '|' -f12)
trace "[build_callback] blockheight=${blockheight}"
blocktime=$(echo "${row}" | cut -d '|' -f13)
trace "[build_callback] blocktime=${blocktime}"
data="{\"id\":\"${id}\","
data="${data}\"address\":\"${address}\","
data="${data}\"hash\":\"${txid}\","
data="${data}\"vout_n\":${vout_n},"
data="${data}\"sent_amount\":${sent_amount},"
data="${data}\"confirmations\":${confirmations},"
data="${data}\"received\":\"$(date -Is -d @${ts_firstseen})\","
data="${data}\"size\":${size},"
data="${data}\"vsize\":${vsize},"
data="${data}\"fees\":${fee},"
data="${data}\"is_replaceable\":${is_replaceable},"
data="${data}\"blockhash\":\"${blockhash}\","
if [ -z ${blocktime} ]; then
data="${data}\"blocktime\":\"\","
else
data="${data}\"blocktime\":\"$(date -Is -d @${blocktime})\","
fi
data="${data}\"blockheight\":\"${blockheight}\"}"
trace "[build_callback] data=${data}"
curl_callback "${url}" "${data}"
return $?
}
curl_callback()
{
trace "Entering curl_callback()..."
local url=${1}
local data=${2}
trace "[curl_callback] curl -H \"Content-Type: application/json\" -d \"${data}\" ${url}"
curl -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d "${data}" ${url}
local returncode=$?
trace_rc ${returncode}
return ${returncode}
}
case "${0}" in *callbacks_job.sh) do_callbacks $@;; esac

View File

@@ -0,0 +1,112 @@
#!/bin/sh
. ./trace.sh
. ./sendtobitcoinnode.sh
. ./sql.sh
. ./blockchainrpc.sh
compute_fees()
{
local pruned=$(get_prop "watchingnode.pruned")
if [ "${pruned}" = "true" ]; then
trace "[compute_fees] pruned=${pruned}"
# We want null instead of 0.00000000 in this case.
echo "null"
exit 0
fi
local txid=${1}
local tx_raw_details=$(cat rawtx-${txid}.blob)
trace "[compute_fees] tx_raw_details=${tx_raw_details}"
local vin_total_amount=$(compute_vin_total_amount "${tx_raw_details}")
local vout_total_amount=0
local vout_value
local vout_values=$(echo "${tx_raw_details}" | jq ".result.vout[].value")
for vout_value in ${vout_values}
do
vout_total_amount=$(awk "BEGIN { printf(\"%.8f\", ${vout_total_amount}+${vout_value}); exit }")
done
trace "[compute_fees] vin total amount=${vin_total_amount}"
trace "[compute_fees] vout total amount=${vout_total_amount}"
local fees=$(awk "BEGIN { printf(\"%.8f\", ${vin_total_amount}-${vout_total_amount}); exit }")
trace "[compute_fees] fees=${fees}"
echo ${fees}
}
compute_vin_total_amount()
{
trace "Entering compute_vin_total_amount()..."
local main_tx=${1}
# local vin_txids=$(echo ${main_tx} | jq '.result.vin[].txid')
local vin_txids_vout=$(echo ${main_tx} | jq '.result.vin[] | ((.txid + "-") + (.vout | tostring))')
trace "[compute_vin_total_amount] vin_txids_vout=${vin_txids_vout}"
local returncode
local vin_txid_vout
local vin_txid
local vin_raw_tx
local vin_vout_amount=0
local vout
local vin_total_amount=0
local vin_hash
local vin_confirmations
local vin_timereceived
local vin_vsize
local vin_blockhash
local vin_blockheight
local vin_blocktime
local txid_already_inserted=true
for vin_txid_vout in ${vin_txids_vout}
do
# vin_txid=$(echo ${vin_txid} | tr -d '"')
vin_txid=$(echo ${vin_txid_vout} | tr -d '"' | cut -d '-' -f1)
# Check if we already have the tx in our DB
vin_raw_tx=$(sql "SELECT raw_tx FROM tx WHERE txid=\"${vin_txid}\"")
if [ -z "${vin_raw_tx}" ]; then
txid_already_inserted=false
vin_raw_tx=$(get_rawtransaction "${vin_txid}")
returncode=$?
if [ "${returncode}" -ne 0 ]; then
return ${returncode}
fi
fi
# vout=$(echo ${main_tx} | jq ".result.vin[] | select(.txid == \"${vin_txid}\") | .vout")
vout=$(echo ${vin_txid_vout} | tr -d '"' | cut -d '-' -f2)
trace "[compute_vin_total_amount] vout=${vout}"
vin_vout_amount=$(echo ${vin_raw_tx} | jq ".result.vout[] | select(.n == ${vout}) | .value" | awk '{ printf "%.8f", $0 }')
trace "[compute_vin_total_amount] vin_vout_amount=${vin_vout_amount}"
vin_total_amount=$(awk "BEGIN { printf(\"%.8f\", ${vin_total_amount}+${vin_vout_amount}); exit}")
trace "[compute_vin_total_amount] vin_total_amount=${vin_total_amount}"
vin_hash=$(echo ${vin_raw_tx} | jq ".result.hash")
vin_confirmations=$(echo ${vin_raw_tx} | jq ".result.confirmations")
vin_timereceived=$(echo ${vin_raw_tx} | jq ".result.time")
vin_size=$(echo ${vin_raw_tx} | jq ".result.size")
vin_vsize=$(echo ${vin_raw_tx} | jq ".result.vsize")
vin_blockhash=$(echo ${vin_raw_tx} | jq ".result.blockhash")
vin_blockheight=$(echo ${vin_raw_tx} | jq ".result.blockheight")
vin_blocktime=$(echo ${vin_raw_tx} | jq ".result.blocktime")
# Let's insert the vin tx in the DB just in case it would be useful
if ! ${txid_already_inserted}; then
# Sometimes raw tx are too long to be passed as paramater, so let's write
# it to a temp file for it to be read by sqlite3 and then delete the file
echo "${vin_raw_tx}" > rawtx-${vin_txid}.blob
sql "INSERT OR IGNORE INTO tx (txid, hash, confirmations, timereceived, size, vsize, blockhash, blockheight, blocktime, raw_tx) VALUES (\"${vin_txid}\", ${vin_hash}, ${vin_confirmations}, ${vin_timereceived}, ${vin_size}, ${vin_vsize}, ${vin_blockhash}, ${vin_blockheight}, ${vin_blocktime}, readfile('rawtx-${vin_txid}.blob'))"
trace_rc $?
rm rawtx-${vin_txid}.blob
txid_already_inserted=true
fi
done
echo ${vin_total_amount}
return 0
}
case "${0}" in *computefees.sh) compute_vin_total_amount $@;; esac

View File

@@ -0,0 +1,150 @@
#!/bin/sh
. ./trace.sh
. ./sql.sh
. ./callbacks_job.sh
. ./sendtobitcoinnode.sh
. ./responsetoclient.sh
. ./computefees.sh
. ./blockchainrpc.sh
confirmation_request()
{
# We are receiving a HTTP request, let's find the TXID from it
trace "Entering confirmation_request()..."
local request=${1}
local txid=$(echo "${request}" | cut -d ' ' -f2 | cut -d '/' -f3)
confirmation "${txid}"
return $?
}
confirmation()
{
trace "Entering confirmation()..."
local txid=${1}
local tx_details=$(get_transaction ${txid})
########################################################################################################
# First of all, let's make sure we're working on watched addresses...
local address
local addresseswhere
local addresses=$(echo ${tx_details} | jq ".result.details[].address")
local notfirst=false
local IFS=$'\n'
for address in ${addresses}
do
trace "[confirmation] address=${address}"
if ${notfirst}; then
addresseswhere="${addresseswhere},${address}"
else
addresseswhere="${address}"
notfirst=true
fi
done
local rows=$(sql "SELECT id, address FROM watching WHERE address IN (${addresseswhere}) AND watching")
if [ ${#rows} -eq 0 ]; then
trace "[confirmation] No watched address in this tx!"
return 0
fi
########################################################################################################
local tx=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"")
local id_inserted
local tx_raw_details=$(get_rawtransaction ${txid})
local tx_nb_conf=$(echo ${tx_details} | jq '.result.confirmations')
# Sometimes raw tx are too long to be passed as paramater, so let's write
# it to a temp file for it to be read by sqlite3 and then delete the file
echo "${tx_raw_details}" > rawtx-${txid}.blob
if [ -z ${tx} ]; then
# TX not found in our DB.
# 0-conf or missed conf (managed or while spending) or spending an unconfirmed
# (note: spending an unconfirmed TX must be avoided or we'll get here spending an unprocessed watching)
# Let's first insert the tx in our DB
local tx_hash=$(echo ${tx_raw_details} | jq '.result.hash')
local tx_ts_firstseen=$(echo ${tx_details} | jq '.result.timereceived')
local tx_amount=$(echo ${tx_details} | jq '.result.amount')
local tx_size=$(echo ${tx_raw_details} | jq '.result.size')
local tx_vsize=$(echo ${tx_raw_details} | jq '.result.vsize')
local tx_replaceable=$(echo ${tx_details} | jq '.result."bip125-replaceable"')
tx_replaceable=$([ ${tx_replaceable} = "yes" ] && echo 1 || echo 0)
local fees=$(compute_fees "${txid}")
trace "[confirmation] fees=${fees}"
# If we missed 0-conf...
local tx_blockhash=$(echo ${tx_details} | jq '.result.blockhash')
local tx_blockheight=$(echo ${tx_details} | jq '.result.blockheight')
local tx_blocktime=$(echo ${tx_details} | jq '.result.blocktime')
sql "INSERT OR IGNORE INTO tx (txid, hash, confirmations, timereceived, fee, size, vsize, is_replaceable, blockhash, blockheight, blocktime, raw_tx) VALUES (\"${txid}\", ${tx_hash}, ${tx_nb_conf}, ${tx_ts_firstseen}, ${fees}, ${tx_size}, ${tx_vsize}, ${tx_replaceable}, ${tx_blockhash}, ${tx_blockheight}, ${tx_blocktime}, readfile('rawtx-${txid}.blob'))"
trace_rc $?
id_inserted=$(sql "SELECT id FROM tx WHERE txid='${txid}'")
trace_rc $?
else
# TX found in our DB.
# 1-conf or spending watched address (in this case, we probably missed conf)
local tx_blockhash=$(echo ${tx_details} | jq '.result.blockhash')
local tx_blockheight=$(echo ${tx_details} | jq '.result.blockheight')
local tx_blocktime=$(echo ${tx_details} | jq '.result.blocktime')
sql "UPDATE tx SET
confirmations=${tx_nb_conf},
blockhash=${tx_blockhash},
blockheight=${tx_blockheight},
blocktime=${tx_blocktime},
raw_tx=readfile('rawtx-${txid}.blob')
WHERE txid=\"${txid}\""
trace_rc $?
id_inserted=${tx}
fi
# Delete the temp file containing the raw tx (see above)
rm rawtx-${txid}.blob
########################################################################################################
# Let's now insert in the join table if not already done
tx=$(sql "SELECT tx_id FROM watching_tx WHERE tx_id=\"${tx}\"")
if [ -z "${tx}" ]; then
trace "[confirmation] For this tx, there's no watching_tx row, let's create"
local watching_id
# If the tx is batched and pays multiple watched addresses, we have to insert
# those additional addresses in watching_tx!
for row in ${rows}
do
watching_id=$(echo "${row}" | cut -d '|' -f1)
address=$(echo "${row}" | cut -d '|' -f2)
tx_vout_n=$(echo ${tx_details} | jq ".result.details[] | select(.address==\"${address}\") | .vout")
tx_vout_amount=$(echo ${tx_details} | jq ".result.details[] | select(.address==\"${address}\") | .amount")
sql "INSERT OR IGNORE INTO watching_tx (watching_id, tx_id, vout, amount) VALUES (${watching_id}, ${id_inserted}, ${tx_vout_n}, ${tx_vout_amount})"
trace_rc $?
done
else
trace "[confirmation] For this tx, there's already watching_tx rows"
fi
########################################################################################################
do_callbacks
echo '{"result":"confirmed"}'
return 0
}
case "${0}" in *confirmation.sh) confirmation $@;; esac

View File

@@ -0,0 +1,64 @@
#!/bin/sh
. ./trace.sh
. ./sql.sh
getactivewatches()
{
trace "Entering getactivewatches()..."
local watches
watches=$(sql "SELECT id, address, imported, callback0conf, callback1conf, inserted_ts FROM watching WHERE watching AND NOT calledback1conf")
returncode=$?
trace_rc ${returncode}
local id
local address
local imported
local inserted
local cb0conf_url
local cb1conf_url
local timestamp
local notfirst=false
echo -n "{\"watches\":["
local IFS=$'\n'
for row in ${watches}
do
if ${notfirst}; then
echo ","
else
notfirst=true
fi
trace "[getactivewatches] row=${row}"
id=$(echo "${row}" | cut -d '|' -f1)
trace "[getactivewatches] id=${id}"
address=$(echo "${row}" | cut -d '|' -f2)
trace "[getactivewatches] address=${address}"
imported=$(echo "${row}" | cut -d '|' -f3)
trace "[getactivewatches] imported=${imported}"
cb0conf_url=$(echo "${row}" | cut -d '|' -f4)
trace "[getactivewatches] cb0conf_url=${cb0conf_url}"
cb1conf_url=$(echo "${row}" | cut -d '|' -f5)
trace "[getactivewatches] cb1conf_url=${cb1conf_url}"
timestamp=$(echo "${row}" | cut -d '|' -f6)
trace "[getactivewatches] timestamp=${timestamp}"
data="{\"id\":\"${id}\","
data="${data}\"address\":\"${address}\","
data="${data}\"imported\":\"${imported}\","
data="${data}\"unconfirmedCallbackURL\":\"${cb0conf_url}\","
data="${data}\"confirmedCallbackURL\":\"${cb1conf_url}\","
data="${data}\"watching_since\":\"${timestamp}\"}"
trace "[getactivewatches] data=${data}"
echo -n "${data}"
done
echo "]}"
return ${returncode}
}
case "${0}" in *getactivewatches.sh) getactivewatches;; esac

View File

@@ -0,0 +1,21 @@
#!/bin/sh
. ./trace.sh
. ./sendtobitcoinnode.sh
importaddress_rpc()
{
trace "[Entering importaddress_rpc()]"
local address=${1}
local data="{\"method\":\"importaddress\",\"params\":[\"${address}\",\"\",false]}"
local result
result=$(send_to_watcher_node ${data})
local returncode=$?
echo "${result}"
return ${returncode}
}
case "${0}" in *importaddress.sh) importaddress_rpc $@;; esac

View File

@@ -0,0 +1,86 @@
#!/bin/sh
. ./trace.sh
. ./sql.sh
. ./importaddress.sh
. ./confirmation.sh
manage_not_imported()
{
# When we tried to import watched addresses in the watching node,
# if it didn't succeed, we try again here.
trace "[Entering manage_not_imported()]"
local watches=$(sql 'SELECT address FROM watching WHERE watching AND NOT imported')
trace "[manage_not_imported] watches=${watches}"
local result
local returncode
local IFS=$'\n'
for address in ${watches}
do
result=$(importaddress_rpc "${address}")
returncode=$?
trace_rc ${returncode}
if [ "${returncode}" -eq 0 ]; then
sql "UPDATE watching SET imported=1 WHERE address=\"${address}\""
fi
done
return 0
}
manage_missed_conf()
{
# Maybe we missed confirmations, because we were down or no network or
# whatever, so we look at what might be missed and do confirmations.
trace "[Entering manage_missed_conf()]"
# local watches=$(sql 'SELECT address FROM watching WHERE txid IS NULL AND watching AND imported')
#local watches=$(sql 'SELECT address FROM watching LEFT JOIN watching_tx ON id = watching_id WHERE watching AND imported AND tx_id IS NULL')
local watches=$(sql 'SELECT address FROM watching w LEFT JOIN watching_tx ON w.id = watching_id LEFT JOIN tx t ON t.id = tx_id WHERE watching AND imported AND (tx_id IS NULL OR t.confirmations=0)')
trace "[manage_missed_conf] watches=${watches}"
if [ ${#watches} -eq 0 ]; then
trace "[manage_missed_conf] Nothing missed!"
return 0
fi
local addresses
local data
local result
local returncode
local IFS=$'\n'
for address in ${watches}
do
if [ -z ${addresses} ]; then
addresses="[\"${address}\""
else
addresses="${addresses},\"${address}\""
fi
done
addresses="${addresses}]"
# Watching addresses with UTXO are transactions being watched that went through without us knowing it, we missed the conf
data="{\"method\":\"listunspent\",\"params\":[0, 9999999, ${addresses}]}"
local unspents
unspents=$(send_to_watcher_node ${data})
returncode=$?
trace_rc ${returncode}
if [ "${returncode}" -ne 0 ]; then
return ${returncode}
fi
# | tr -d '"'
local txids=$(echo ${unspents} | jq ".result[].txid" | tr -d '"')
for txid in ${txids}
do
confirmation "${txid}"
done
return 0
}
case "${0}" in *manage_missed_conf.sh) manage_not_imported $@; manage_missed_conf $@;; esac

View File

@@ -0,0 +1,238 @@
#!/bin/sh
#
#
#
#
. ./sendtobitcoinnode.sh
. ./callbacks_job.sh
. ./watchrequest.sh
. ./unwatchrequest.sh
. ./getactivewatches.sh
. ./confirmation.sh
. ./blockchainrpc.sh
. ./responsetoclient.sh
. ./trace.sh
. ./manage_missed_conf.sh
. ./walletoperations.sh
. ./bitcoin.sh
. ./call_lightningd.sh
main()
{
trace "Entering main()..."
local step=0
local cmd
local http_method
local line
local content_length
local response
local returncode
while read line; do
line=$(echo "${line}" | tr -d '\r\n')
trace "[main] line=${line}"
if [ "${cmd}" = "" ]; then
# First line!
# Looking for something like:
# GET /cmd/params HTTP/1.1
# POST / HTTP/1.1
cmd=$(echo "${line}" | cut -d '/' -f2 | cut -d ' ' -f1)
trace "[main] cmd=${cmd}"
http_method=$(echo "${line}" | cut -d ' ' -f1)
trace "[main] http_method=${http_method}"
if [ "${http_method}" = "GET" ]; then
step=1
fi
fi
if [ "${line}" = "" ]; then
trace "[main] empty line"
if [ ${step} -eq 1 ]; then
trace "[main] body part finished, disconnecting"
break
else
trace "[main] headers part finished, body incoming"
step=1
fi
fi
# line=content-length: 406
case "${line}" in *[cC][oO][nN][tT][eE][nN][tT]-[lL][eE][nN][gG][tT][hH]*)
content_length=$(echo ${line} | cut -d ':' -f2)
trace "[main] content_length=${content_length}";
;;
esac
if [ ${step} -eq 1 ]; then
trace "[main] step=${step}"
if [ "${http_method}" = "POST" ]; then
read -n ${content_length} line
trace "[main] line=${line}"
fi
case "${cmd}" in
watch)
# POST http://192.168.111.152:8080/watch
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","unconfirmedCallbackURL":"192.168.111.233:1111/callback0conf","confirmedCallbackURL":"192.168.111.233:1111/callback1conf"}
response=$(watchrequest "${line}")
response_to_client "${response}" ${?}
break
;;
unwatch)
# curl (GET) 192.168.111.152:8080/unwatch/2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp
response=$(unwatchrequest "${line}")
response_to_client "${response}" ${?}
break
;;
getactivewatches)
# curl (GET) 192.168.111.152:8080/getactivewatches
response=$(getactivewatches)
response_to_client "${response}" ${?}
break
;;
conf)
# curl (GET) 192.168.111.152:8080/conf/b081ca7724386f549cf0c16f71db6affeb52ff7a0d9b606fb2e5c43faffd3387
response=$(confirmation_request "${line}")
response_to_client "${response}" ${?}
break
;;
getbestblockhash)
# curl (GET) http://192.168.111.152:8080/getbestblockhash
response=$(get_best_block_hash)
response_to_client "${response}" ${?}
break
;;
getblockinfo)
# curl (GET) http://192.168.111.152:8080/getblockinfo/000000006f82a384c208ecfa04d05beea02d420f3f398ddda5c7f900de5718ea
response=$(get_block_info $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3))
response_to_client "${response}" ${?}
break
;;
gettransaction)
# curl (GET) http://192.168.111.152:8080/gettransaction/af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648
response=$(get_rawtransaction $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3))
response_to_client "${response}" ${?}
break
;;
getbestblockinfo)
# curl (GET) http://192.168.111.152:8080/getbestblockinfo
response=$(get_best_block_info)
response_to_client "${response}" ${?}
break
;;
executecallbacks)
# curl (GET) http://192.168.111.152:8080/executecallbacks
manage_not_imported
manage_missed_conf
response=$(do_callbacks)
response_to_client "${response}" ${?}
break
;;
getbalance)
# curl (GET) http://192.168.111.152:8080/getbalance
response=$(getbalance)
response_to_client "${response}" ${?}
break
;;
getnewaddress)
# curl (GET) http://192.168.111.152:8080/getnewaddress
response=$(getnewaddress)
response_to_client "${response}" ${?}
break
;;
spend)
# POST http://192.168.111.152:8080/spend
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233}
response=$(spend "${line}")
response_to_client "${response}" ${?}
break
;;
addtobatch)
# POST http://192.168.111.152:8080/addtobatch
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233}
response=$(addtobatching $(echo "${line}" | jq ".address" | tr -d '"') $(echo "${line}" | jq ".amount"))
response_to_client "${response}" ${?}
break
;;
batchspend)
# GET http://192.168.111.152:8080/batchspend
response=$(batchspend "${line}")
response_to_client "${response}" ${?}
break
;;
deriveindex)
# curl GET http://192.168.111.152:8080/deriveindex/25-30
# curl GET http://192.168.111.152:8080/deriveindex/34
response=$(deriveindex $(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3))
response_to_client "${response}" ${?}
break
;;
derivepubpath)
# POST http://192.168.111.152:8080/derivepubpath
# BODY {"pub32":"tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk","path":"0/25-30"}
# BODY {"pub32":"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb","path":"0/25-30"}
# BODY {"pub32":"vpub5SLqN2bLY4WeZF3kL4VqiWF1itbf3A6oRrq9aPf16AZMVWYCuN9TxpAZwCzVgW94TNzZPNc9XAHD4As6pdnExBtCDGYRmNJrcJ4eV9hNqcv","path":"0/25-30"}
response=$(send_to_pycoin "${line}")
response_to_client "${response}" ${?}
break
;;
ln_getinfo)
# GET http://192.168.111.152:8080/ln_getinfo
response=$(ln_getinfo)
response_to_client "${response}" ${?}
break
;;
ln_create_invoice)
# POST http://192.168.111.152:8080/ln_create_invoice
# BODY {"msatoshi":"10000","label":"koNCcrSvhX3dmyFhW","description":"Bylls order #10649","expiry":"900"}
response=$(ln_create_invoice "${line}")
response_to_client "${response}" ${?}
break
;;
ln_pay)
# POST http://192.168.111.152:8080/ln_pay
# BODY {"bolt11":"lntb1pdca82tpp5gv8mn5jqlj6xztpnt4r472zcyrwf3y2c3cvm4uzg2gqcnj90f83qdp2gf5hgcm0d9hzqnm4w3kx2apqdaexgetjyq3nwvpcxgcqp2g3d86wwdfvyxcz7kce7d3n26d2rw3wf5tzpm2m5fl2z3mm8msa3xk8nv2y32gmzlhwjved980mcmkgq83u9wafq9n4w28amnmwzujgqpmapcr3","expected_msatoshi":"10000","expected_description":"Bitcoin Outlet order #7082"}
response=$(ln_pay "${line}")
response_to_client "${response}" ${?}
break
;;
ln_newaddr)
# GET http://192.168.111.152:8080/ln_newaddr
response=$(ln_newaddr)
response_to_client "${response}" ${?}
break
;;
esac
break
fi
done
trace "[main] exiting"
return 0
}
export NODE_RPC_URL=$BTC_NODE_RPC_URL
export TRACING
export DB_PATH
export DB_FILE
main
exit $?

View File

@@ -0,0 +1,21 @@
#!/bin/sh
. ./trace.sh
response_to_client()
{
trace "Entering response_to_client()..."
local response=${1}
local returncode=${2}
([ -z "${returncode}" ] || [ "${returncode}" -eq "0" ]) && echo -ne "HTTP/1.1 200 OK\r\n"
[ -n "${returncode}" ] && [ "${returncode}" -ne "0" ] && echo -ne "HTTP/1.1 400 Bad Request\r\n"
echo -e "Content-Type: application/json\r\nContent-Length: ${#response}\r\n\r\n${response}"
# Small delay needed for the data to be processed correctly by peer
sleep 0.2s
}
case "${0}" in *responsetoclient.sh) response_to_client $@;; esac

View File

@@ -0,0 +1,59 @@
#!/bin/sh
. ./trace.sh
send_to_watcher_node()
{
trace "Entering send_to_watcher_node()..."
send_to_bitcoin_node ${WATCHER_NODE_RPC_URL} watcher_btcnode_curlcfg.properties $@
local returncode=$?
trace_rc ${returncode}
return ${returncode}
}
send_to_spender_node()
{
trace "Entering send_to_spender_node()..."
send_to_bitcoin_node ${SPENDER_NODE_RPC_URL} spender_btcnode_curlcfg.properties $@
local returncode=$?
trace_rc ${returncode}
return ${returncode}
}
send_to_bitcoin_node()
{
trace "Entering send_to_bitcoin_node()..."
local returncode
local result
local errorstring
local node_url=${1}
local configfile=${2}
local data=${3}
trace "[send_to_bitcoin_node] curl -s --config ${configfile} -H \"Content-Type: application/json\" -d \"${data}\" ${node_url}"
result=$(curl -s --config ${configfile} -H "Content-Type: application/json" -d "${data}" ${node_url})
returncode=$?
trace_rc ${returncode}
trace "[send_to_bitcoin_node] result=${result}"
if [ "${returncode}" -eq 0 ]; then
# Node responded, let's see if we got an error message from the node
# jq -e will have a return code of 1 if the supplied tag is null.
errorstring=$(echo "${result}" | jq -e ".error")
if [ "$?" -eq "0" ]; then
# Error tag not null, so there's an error
trace "[send_to_bitcoin_node] Node responded, error found in response: ${errorstring}"
returncode=1
else
trace "[send_to_bitcoin_node] Node responded, no error found in response, yayy!"
fi
fi
# Output response to stdout before exiting with return code
echo "${result}"
trace_rc ${returncode}
return ${returncode}
}
case "${0}" in *sendtobitcoinnode.sh) send_to_bitcoin_node $@;; esac

View File

@@ -0,0 +1,12 @@
#!/bin/sh
. ./trace.sh
sql()
{
trace "sqlite3 ${DB_FILE} '${1}'"
sqlite3 -cmd ".timeout 20000" ${DB_FILE} "${1}"
return $?
}
case "${0}" in *sql.sh) sql $@;; esac

View File

@@ -0,0 +1,15 @@
#!/bin/sh
export PROXY_LISTENING_PORT
export WATCHER_NODE_RPC_URL=$WATCHER_BTC_NODE_RPC_URL
export SPENDER_NODE_RPC_URL=$SPENDER_BTC_NODE_RPC_URL
export TRACING
export DB_PATH
export DB_FILE
if [ ! -e ${DB_FILE} ]; then
echo "DB not found, creating..." > /dev/stderr
cat watching.sql | sqlite3 $DB_FILE
fi
nc -vlkp${PROXY_LISTENING_PORT} -e ./requesthandler.sh

View File

@@ -0,0 +1,9 @@
#!/bin/sh
read line
echo ${line} > /dev/stderr
echo -ne "HTTP/1.1 200 OK\r\n"
echo -e "Content-Type: application/json\r\nContent-Length: 0\r\n\r\n"
# Small delay needed for the data to be processed correctly by peer
sleep 0.5s

View File

@@ -0,0 +1,322 @@
#!/bin/sh
tests()
{
local address
local address1
local address2
local address3
local response
# getbestblockhash
# (GET) http://cyphernode:8888/getbestblockhash
echo "Testing getbestblockhash..."
response=$(curl -s cyphernode:8888/getbestblockhash)
echo "response=${response}"
local blockhash=$(echo ${response} | jq ".result" | tr -d '\"')
echo "blockhash=${blockhash}"
if [ -z "${blockhash}" ]; then
exit 2
fi
echo "Tested getbestblockhash."
# getbestblockinfo
# curl (GET) http://cyphernode:8888/getbestblockinfo
echo "Testing getbestblockinfo..."
response=$(curl -s cyphernode:8888/getbestblockinfo)
echo "response=${response}"
local blockhash2=$(echo ${response} | jq ".result.hash" | tr -d '\"')
echo "blockhash2=${blockhash2}"
if [ "${blockhash2}" != "${blockhash}" ]; then
exit 4
fi
echo "Tested getbestblockinfo."
# getblockinfo
# (GET) http://cyphernode:8888/getblockinfo/000000006f82a384c208ecfa04d05beea02d420f3f398ddda5c7f900de5718ea
echo "Testing getblockinfo..."
response=$(curl -s cyphernode:8888/getblockinfo/${blockhash})
echo "response=${response}"
blockhash2=$(echo ${response} | jq ".result.hash" | tr -d '\"')
echo "blockhash2=${blockhash2}"
if [ "${blockhash2}" != "${blockhash}" ]; then
exit 6
fi
echo "Tested getblockinfo."
# gettransaction
# (GET) http://cyphernode:8888/gettransaction/af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648
echo "Testing gettransaction..."
response=$(curl -s cyphernode:8888/gettransaction/af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648)
echo "response=${response}"
local txid=$(echo ${response} | jq ".result.txid" | tr -d '\"')
echo "txid=${txid}"
if [ "${txid}" != "af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648" ]; then
exit 8
fi
echo "Tested gettransaction."
# getnewaddress
# (GET) http://cyphernode:8888/getnewaddress
# returns {"address":"2MuiUu8AyuByAGYRDAqqhdYxt8gXcsQ1Ymw"}
echo "Testing getnewaddress..."
response=$(curl -s cyphernode:8888/getnewaddress)
echo "response=${response}"
address1=$(echo ${response} | jq ".address" | tr -d '\"')
echo "address1=${address1}"
if [ -z "${address1}" ]; then
exit 10
fi
address2=$(curl -s cyphernode:8888/getnewaddress | jq ".address" | tr -d '\"')
echo "address2=${address2}"
echo "Tested getnewaddress."
# getbalance
# (GET) http://cyphernode:8888/getbalance
echo "Testing getbalance..."
response=$(curl -s cyphernode:8888/getbalance)
echo "response=${response}"
local balance=$(echo ${response} | jq ".balance")
echo "balance=${balance}"
if [ -z "${balance}" ]; then
exit 12
fi
echo "Tested getbalance."
# watch
# POST http://cyphernode:8888/watch
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","unconfirmedCallbackURL":"192.168.122.233:1111/callback0conf","confirmedCallbackURL":"192.168.122.233:1111/callback1conf"}
echo "Testing watch..."
local url1="$(hostname):1111/callback0conf"
local url2="$(hostname):1111/callback1conf"
echo "url1=${url1}"
echo "url2=${url2}"
response=$(curl -s -H "Content-Type: application/json" -d "{\"address\":\"${address1}\",\"unconfirmedCallbackURL\":\"${url1}\",\"confirmedCallbackURL\":\"${url2}\"}" cyphernode:8888/watch)
echo "response=${response}"
local id=$(echo "${response}" | jq ".id" | tr -d '\"')
echo "id=${id}"
local event=$(echo "${response}" | jq ".event" | tr -d '\"')
echo "event=${event}"
if [ "${event}" != "watch" ]; then
exit 15
fi
address=$(echo "${response}" | jq ".address" | tr -d '\"')
echo "address=${address}"
if [ "${address}" != "${address1}" ]; then
exit 20
fi
local imported=$(echo "${response}" | jq ".imported" | tr -d '\"')
echo "imported=${imported}"
if [ "${imported}" != "1" ]; then
exit 30
fi
local inserted=$(echo "${response}" | jq ".inserted" | tr -d '\"')
echo "inserted=${inserted}"
if [ "${inserted}" != "1" ]; then
exit 40
fi
local unconfirmedCallbackURL=$(echo "${response}" | jq ".unconfirmedCallbackURL" | tr -d '\"')
echo "unconfirmedCallbackURL=${unconfirmedCallbackURL}"
if [ "${unconfirmedCallbackURL}" != "${url1}" ]; then
exit 60
fi
local confirmedCallbackURL=$(echo "${response}" | jq ".confirmedCallbackURL" | tr -d '\"')
echo "confirmedCallbackURL=${confirmedCallbackURL}"
if [ "${confirmedCallbackURL}" != "${url2}" ]; then
exit 70
fi
# Let's watch another address just to be able to test unwatch later and test if found in getactivewatches
response=$(curl -s -H "Content-Type: application/json" -d "{\"address\":\"${address2}\",\"unconfirmedCallbackURL\":\"${url1}2\",\"confirmedCallbackURL\":\"${url2}2\"}" cyphernode:8888/watch)
echo "response=${response}"
echo "Tested watch."
# getactivewatches
# (GET) http://cyphernode:8888/getactivewatches
echo "Testing getactivewatches..."
response=$(curl -s cyphernode:8888/getactivewatches)
echo "response=${response}"
response=$(echo ${response} | jq ".watches[]")
echo "response=${response}"
local id2=$(echo ${response} | jq "select(.address == \"${address1}\") | .id" | tr -d '\"')
echo "id2=${id2}"
if [ "${id2}" != "${id}" ]; then
exit 80
fi
id2=$(echo ${response} | jq "select(.address == \"${address2}\") | .id" | tr -d '\"')
echo "id2=${id2}"
if [ -z "${id2}" ]; then
exit 90
fi
echo "Tested getactivewatches."
# unwatch
# (GET) http://cyphernode:8888/unwatch/2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp
echo "Testing unwatch..."
response=$(curl -s cyphernode:8888/unwatch/${address2})
echo "response=${response}"
event=$(echo "${response}" | jq ".event" | tr -d '\"')
echo "event=${event}"
if [ "${event}" != "unwatch" ]; then
exit 100
fi
address=$(echo "${response}" | jq ".address" | tr -d '\"')
echo "address=${address}"
if [ "${address}" != "${address2}" ]; then
exit 110
fi
response=$(curl -s cyphernode:8888/getactivewatches)
echo "response=${response}"
response=$(echo "${response}" | jq ".watches[]")
echo "response=${response}"
id2=$(echo ${response} | jq "select(.address == \"${address2}\") | .id" | tr -d '\"')
echo "id2=${id2}"
if [ -n "${id2}" ]; then
exit 120
fi
echo "Tested unwatch."
# deriveindex
# (GET) http://cyphernode:8888/deriveindex/25-30
# {"addresses":[{"address":"2N6Q9kBcLtNswgMSLSQ5oduhbctk7hxEJW8"},{"address":"2NFLhFghAPKEPuZCKoeXYYxuaBxhKXbmhBV"},{"address":"2N7gepbQtRM5Hm4PTjvGadj9wAwEwnAsKiP"},{"address":"2Mth8XDZpXkY9d95tort8HYEAuEesow2tF6"},{"address":"2MwqEmAXhUw6H7bJwMhD13HGWVEj2HgFiNH"},{"address":"2N2Y4BVRdrRFhweub2ehHXveGZC3nryMEJw"}]}
echo "Testing deriveindex..."
response=$(curl -v cyphernode:8888/deriveindex/25-30)
echo "response=${response}"
local nbaddr=$(echo "${response}" | jq ".addresses | length")
if [ "${nbaddr}" -ne "6" ]; then
exit 130
fi
address=$(echo "${response}" | jq ".addresses[2].address" | tr -d '\"')
if [ "${address}" != "2N7gepbQtRM5Hm4PTjvGadj9wAwEwnAsKiP" ]; then
exit 140
fi
echo "Tested deriveindex."
# derivepubpath
# (GET) http://cyphernode:8888/derivepubpath
# BODY {"pub32":"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb","path":"0/25-30"}
# {"addresses":[{"address":"2N6Q9kBcLtNswgMSLSQ5oduhbctk7hxEJW8"},{"address":"2NFLhFghAPKEPuZCKoeXYYxuaBxhKXbmhBV"},{"address":"2N7gepbQtRM5Hm4PTjvGadj9wAwEwnAsKiP"},{"address":"2Mth8XDZpXkY9d95tort8HYEAuEesow2tF6"},{"address":"2MwqEmAXhUw6H7bJwMhD13HGWVEj2HgFiNH"},{"address":"2N2Y4BVRdrRFhweub2ehHXveGZC3nryMEJw"}]}
echo "Testing derivepubpath..."
response=$(curl -v -H "Content-Type: application/json" -d "{\"pub32\":\"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb\",\"path\":\"0/25-30\"}" cyphernode:8888/derivepubpath)
echo "response=${response}"
local nbaddr=$(echo "${response}" | jq ".addresses | length")
if [ "${nbaddr}" -ne "6" ]; then
exit 150
fi
address=$(echo "${response}" | jq ".addresses[2].address" | tr -d '\"')
if [ "${address}" != "2N7gepbQtRM5Hm4PTjvGadj9wAwEwnAsKiP" ]; then
exit 160
fi
echo "Tested derivepubpath."
# spend
# POST http://cyphernode:8888/spend
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233}
# By spending to a watched address, we will test the spending feature and trigger the confirmation to test
# confirmations of watched addresses... Cleva!!!
echo "Testing spend, conf and callbacks..."
response=$(curl -v -H "Content-Type: application/json" -d "{\"address\":\"${address1}\",\"amount\":0.00001}" cyphernode:8888/spend)
echo "response=${response}"
wait_for_callbacks
echo "Tested spend, conf and callbacks."
# addtobatch
# POST http://cyphernode:8888/addtobatch
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233}
# By spending to a watched address, we will test the spending feature and trigger the confirmation to test
# confirmations of watched addresses... Cleva!!!
# echo "Testing addtobatch..."
# response=$(curl -v -H "Content-Type: application/json" -d "{\"address\":\"${address1}\",\"amount\":0.00001}" cyphernode:8888/spend)
# echo "response=${response}"
# wait_for_callbacks
# echo "Tested addtobatch ."
# conf
# (GET) http://cyphernode:8888/conf/b081ca7724386f549cf0c16f71db6affeb52ff7a0d9b606fb2e5c43faffd3387
# Let's trigger tx confirmation even if not confirmed. Will be funny. Should take care of
# multiple confirmations of the same state.
# executecallbacks
# (GET) http://cyphernode::8080/executecallbacks
#echo "GET /getbestblockinfo" | nc cyphernode:8888 - | sed -En "s/^(\{.*)/\1/p" | jq
# spend
# POST http://cyphernode:8888/spend
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233}
#curl -v -H "Content-Type: application/json" -d '{"address":"2MsWyaQ8APbnqasFpWopqUKqsdpiVY3EwLE","amount":0.0001}' cyphernode:8888/spend
# ln_getinfo
# (GET) http://cyphernode:8888/ln_getinfo
echo "Testing ln_getinfo..."
response=$(curl -s cyphernode:8888/ln_getinfo)
echo "response=${response}"
local port=$(echo ${response} | jq ".binding[] | select(.type == \"ipv4\") | .port")
echo "port=${port}"
if [ "${port}" != "9735" ]; then
exit 170
fi
echo "Tested ln_getinfo."
# ln_newaddr
# (GET) http://cyphernode:8888/ln_newaddr
echo "Testing ln_newaddr..."
response=$(curl -s cyphernode:8888/ln_newaddr)
echo "response=${response}"
address=$(echo ${response} | jq ".address")
echo "address=${address}"
if [ -z "${address}" ]; then
exit 180
fi
echo "Tested ln_newaddr."
# ln_create_invoice
# POST http://cyphernode:8888/ln_create_invoice
# BODY {"msatoshi":"10000","label":"koNCcrSvhX3dmyFhW","description":"Bylls order #10649","expiry":"10"}
#echo "Testing ln_create_invoice..."
#response=$(curl -v -H "Content-Type: application/json" -d "{\"msatoshi\":10000,\"label\":\"koNCcrSvhX3dmyFhW\",\"description\":\"Bylls order #10649\",\"expiry\":10}" cyphernode:8888/ln_create_invoice)
#echo "response=${response}"
#echo "Tested ln_create_invoice."
# ln_pay
}
wait_for_callbacks()
{
nc -vlp1111 -e ./tests-cb.sh
nc -vlp1111 -e ./tests-cb.sh
}
tests

View File

@@ -0,0 +1,15 @@
#!/bin/sh
trace()
{
if [ -n "${TRACING}" ]; then
echo "$(date -Is) ${1}" > /dev/stderr
fi
}
trace_rc()
{
if [ -n "${TRACING}" ]; then
echo "$(date -Is) Last return code: ${1}" > /dev/stderr
fi
}

View File

@@ -0,0 +1,27 @@
#!/bin/sh
. ./trace.sh
. ./sql.sh
unwatchrequest()
{
trace "Entering unwatchrequest()..."
local request=${1}
local address=$(echo "${request}" | cut -d ' ' -f2 | cut -d '/' -f3)
local returncode
trace "[unwatchrequest] Unwatch request on address ${address})"
sql "UPDATE watching SET watching=0 WHERE address=\"${address}\""
returncode=$?
trace_rc ${returncode}
data="{\"event\":\"unwatch\",\"address\":\"${address}\"}"
trace "[unwatchrequest] responding=${data}"
echo "${data}"
return ${returncode}
}
case "${0}" in *unwatchrequest.sh) unwatchrequest $@;; esac

View File

@@ -0,0 +1,17 @@
#!/bin/sh
. ./trace.sh
get_prop()
{
trace "Entering get_prop()..."
local property=${1}
trace "[get_prop] property=${property}"
local value=$(grep "${property}" config.properties | cut -d'=' -f2)
trace "[get_prop] value=${value}"
echo ${value}
}

View File

@@ -0,0 +1,193 @@
#!/bin/sh
. ./trace.sh
. ./sendtobitcoinnode.sh
spend()
{
trace "Entering spend()..."
local data
local request=${1}
local address=$(echo "${request}" | jq ".address" | tr -d '"')
trace "[spend] address=${address}"
local amount=$(echo "${request}" | jq ".amount" | awk '{ printf "%.8f", $0 }')
trace "[spend] amount=${amount}"
local response
local id_inserted
response=$(send_to_spender_node "{\"method\":\"sendtoaddress\",\"params\":[\"${address}\",${amount}]}")
local returncode=$?
trace_rc ${returncode}
trace "[spend] response=${response}"
if [ "${returncode}" -eq 0 ]; then
local txid=$(echo ${response} | jq ".result" | tr -d '"')
trace "[spend] txid=${txid}"
# Let's insert the txid in our little DB to manage the confirmation and tell it's not a watching address
# sql "INSERT OR IGNORE INTO watching (watching, txid) VALUES (0, ${txid})"
sql "INSERT OR IGNORE INTO tx (txid) VALUES (\"${txid}\")"
trace_rc $?
id_inserted=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"")
trace_rc $?
sql "INSERT OR IGNORE INTO recipient (address, amount, tx_id) VALUES (\"${address}\", ${amount}, ${id_inserted})"
trace_rc $?
data="{\"status\":\"accepted\""
data="${data},\"hash\":\"${txid}\"}"
else
local message=$(echo "${response}" | jq -e ".error.message")
data="{\"message\":${message}}"
fi
trace "[spend] responding=${data}"
echo "${data}"
return ${returncode}
}
getbalance()
{
trace "Entering getbalance()..."
local response
local data='{"method":"getbalance"}'
response=$(send_to_spender_node "${data}")
local returncode=$?
trace_rc ${returncode}
trace "[getbalance] response=${response}"
if [ "${returncode}" -eq 0 ]; then
local balance=$(echo ${response} | jq ".result")
trace "[getbalance] balance=${balance}"
data="{\"balance\":${balance}}"
else
trace "[getbalance] Coudn't get balance!"
data=""
fi
trace "[getbalance] responding=${data}"
echo "${data}"
return ${returncode}
}
getnewaddress()
{
trace "Entering getnewaddress()..."
local response
local data='{"method":"getnewaddress"}'
response=$(send_to_spender_node "${data}")
local returncode=$?
trace_rc ${returncode}
trace "[getnewaddress] response=${response}"
if [ "${returncode}" -eq 0 ]; then
local address=$(echo ${response} | jq ".result")
trace "[getnewaddress] address=${address}"
data="{\"address\":${address}}"
else
trace "[getnewaddress] Coudn't get a new address!"
data=""
fi
trace "[getnewaddress] responding=${data}"
echo "${data}"
return ${returncode}
}
addtobatching()
{
trace "Entering addtobatching()..."
local address=${1}
trace "[addtobatching] address=${address}"
local amount=${2}
trace "[addtobatching] amount=${amount}"
sql "INSERT OR IGNORE INTO recipient (address, amount) VALUES (\"${address}\", ${amount})"
returncode=$?
trace_rc ${returncode}
return ${returncode}
}
batchspend()
{
trace "Entering batchspend()..."
local data
local response
local recipientswhere
local recipientsjson
local id_inserted
# We will batch all the addresses in DB without a TXID
local batching=$(sql 'SELECT address, amount FROM recipient WHERE tx_id IS NULL')
trace "[batchspend] batching=${batching}"
local returncode
local address
local amount
local notfirst=false
local IFS=$'\n'
for row in ${batching}
do
trace "[batchspend] row=${row}"
address=$(echo "${row}" | cut -d '|' -f1)
trace "[batchspend] address=${address}"
amount=$(echo "${row}" | cut -d '|' -f2)
trace "[batchspend] amount=${amount}"
if ${notfirst}; then
recipientswhere="${recipientswhere},"
recipientsjson="${recipientsjson},"
else
notfirst=true
fi
recipientswhere="${recipientswhere}\"${address}\""
recipientsjson="${recipientsjson}\"${address}\":${amount}"
done
response=$(send_to_spender_node "{\"method\":\"sendmany\",\"params\":[\"\", {${recipientsjson}}]}")
returncode=$?
trace_rc ${returncode}
trace "[batchspend] response=${response}"
if [ "${returncode}" -eq 0 ]; then
local txid=$(echo ${response} | jq ".result" | tr -d '"')
trace "[batchspend] txid=${txid}"
# Let's insert the txid in our little DB to manage the confirmation and tell it's not a watching address
# sql "INSERT OR IGNORE INTO watching (watching, txid) VALUES (0, ${txid})"
# trace_rc $?
sql "INSERT OR IGNORE INTO tx (txid) VALUES (\"${txid}\")"
returncode=$?
trace_rc ${returncode}
if [ "${returncode}" -eq 0 ]; then
id_inserted=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"")
trace "[batchspend] id_inserted: ${id_inserted}"
sql "UPDATE recipient SET tx_id=${id_inserted} WHERE address IN (${recipientswhere})"
trace_rc $?
fi
data="{\"status\":\"accepted\""
data="${data},\"hash\":\"${txid}\"}"
else
local message=$(echo "${response}" | jq -e ".error.message")
data="{\"message\":${message}}"
fi
trace "[batchspend] responding=${data}"
echo "${data}"
return ${returncode}
}
#case "${0}" in *walletoperations.sh) getbalance $@;; esac

View File

@@ -0,0 +1,75 @@
#!/bin/sh
. ./trace.sh
. ./importaddress.sh
. ./sql.sh
. ./sendtobitcoinnode.sh
watchrequest()
{
trace "Entering watchrequest()..."
local returncode
local request=${1}
local address=$(echo "${request}" | jq ".address" | tr -d '"')
local cb0conf_url=$(echo "${request}" | jq ".unconfirmedCallbackURL" | tr -d '"')
local cb1conf_url=$(echo "${request}" | jq ".confirmedCallbackURL" | tr -d '"')
local imported
local inserted
local id_inserted
local result
trace "[watchrequest] Watch request on address (${address}), cb 0-conf (${cb0conf_url}), cb 1-conf (${cb1conf_url})"
result=$(importaddress_rpc "${address}")
returncode=$?
trace_rc ${returncode}
if [ "${returncode}" -eq 0 ]; then
imported=1
else
imported=0
fi
# sql "INSERT OR IGNORE INTO watching (address, watching, callback0conf, callback1conf, imported) VALUES (\"${address}\", 1, \"${cb0conf_url}\", \"${cb1conf_url}\", ${imported})"
sql "INSERT OR IGNORE INTO watching (address, watching, callback0conf, callback1conf, imported) VALUES (\"${address}\", 1, \"${cb0conf_url}\", \"${cb1conf_url}\", ${imported})"
returncode=$?
trace_rc ${returncode}
if [ "${returncode}" -eq 0 ]; then
inserted=1
id_inserted=$(sql "SELECT id FROM watching WHERE address='${address}'")
trace "[watchrequest] id_inserted: ${id_inserted}"
else
inserted=0
fi
local fees2blocks
local fees6blocks
local fees36blocks
local fees144blocks
fees2blocks=$(getestimatesmartfee 2)
trace_rc $?
fees6blocks=$(getestimatesmartfee 6)
trace_rc $?
fees36blocks=$(getestimatesmartfee 36)
trace_rc $?
fees144blocks=$(getestimatesmartfee 144)
trace_rc $?
local data="{\"id\":\"${id_inserted}\",
\"event\":\"watch\",
\"imported\":\"${imported}\",
\"inserted\":\"${inserted}\",
\"address\":\"${address}\",
\"unconfirmedCallbackURL\":\"${cb0conf_url}\",
\"confirmedCallbackURL\":\"${cb1conf_url}\",
\"estimatesmartfee2blocks\":\"${fees2blocks}\",
\"estimatesmartfee6blocks\":\"${fees6blocks}\",
\"estimatesmartfee36blocks\":\"${fees36blocks}\",
\"estimatesmartfee144blocks\":\"${fees144blocks}\"}"
trace "[watchrequest] responding=${data}"
echo "${data}"
return ${returncode}
}
case "${0}" in *watchrequest.sh) watchrequest $@;; esac

View File

@@ -0,0 +1,11 @@
TRACING=1
WATCHER_BTC_NODE_RPC_URL=btcnode:18332/wallet/watching01.dat
SPENDER_BTC_NODE_RPC_URL=btcnode:18332/wallet/spending01.dat
PROXY_LISTENING_PORT=8888
# Variable substitutions don't work
DB_PATH=/proxyuser/db
DB_FILE=/proxyuser/db/proxydb
# Pycoin container
PYCOIN_CONTAINER=pycoinnode:7777
# OTS container
OTS_CONTAINER=otsnode:6666

31
pycoin_docker/Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
#FROM resin/raspberry-pi-alpine-python:3.6
FROM python:3.6-alpine
ARG USER_ID
ARG GROUP_ID
ENV USERNAME proxyuser
ENV HOME /${USERNAME}
ENV USER_ID ${USER_ID:-1000}
ENV GROUP_ID ${GROUP_ID:-1000}
RUN addgroup -g ${GROUP_ID} ${USERNAME} \
&& adduser -u ${USER_ID} -G ${USERNAME} -D -s /bin/sh -h ${HOME} ${USERNAME}
RUN apk add --update --no-cache git jq \
&& pip install --no-cache-dir pycoin \
&& cd \
&& git clone https://github.com/Kexkey/pycoin.git \
&& cp -rf pycoin/pycoin/* /usr/local/lib/python3.6/site-packages/pycoin
COPY --chown=proxyuser script/pycoin.sh ${HOME}/pycoin.sh
COPY --chown=proxyuser script/requesthandler.sh ${HOME}/requesthandler.sh
COPY --chown=proxyuser script/responsetoclient.sh ${HOME}/responsetoclient.sh
COPY --chown=proxyuser script/startpycoin.sh ${HOME}/startpycoin.sh
COPY --chown=proxyuser script/trace.sh ${HOME}/trace.sh
USER ${USERNAME}
WORKDIR ${HOME}
RUN chmod +x startpycoin.sh requesthandler.sh
ENTRYPOINT ["./startpycoin.sh"]

16
pycoin_docker/README.md Normal file
View File

@@ -0,0 +1,16 @@
# Build image
```shell
docker build -t pycoinimg --build-arg USER_ID=$(id -u proxyuser) --build-arg GROUP_ID=$(id -g proxyuser) .
```
# Usefull examples
See https://github.com/shivaenigma/pycoin
List SegWit addresses for path 0/24-30 for a pub32:
```shell
curl -H "Content-Type: application/json" -d '{"pub32":"tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk","path":"0/25-30"}' http://localhost:7777/derive
curl -H "Content-Type: application/json" -d '{"pub32":"vpub5SLqN2bLY4WeZF3kL4VqiWF1itbf3A6oRrq9aPf16AZMVWYCuN9TxpAZwCzVgW94TNzZPNc9XAHD4As6pdnExBtCDGYRmNJrcJ4eV9hNqcv","path":"0/25-30"}' http://localhost:7777/derive
```

View File

@@ -0,0 +1,2 @@
TRACING=1
PYCOIN_LISTENING_PORT=7777

View File

@@ -0,0 +1,47 @@
#!/bin/sh
. ./trace.sh
derive()
{
trace "Entering derive()..."
local request=${1}
local pub32=$(echo "${request}" | jq ".pub32" | tr -d '"')
local path=$(echo "${request}" | jq ".path" | tr -d '"')
local result
local returncode
trace "[derive] path=${path}"
trace "[derive] pub32=${pub32}"
result=$(ku -n BTC -s ${path} -a E:${pub32})
returncode=$?
trace_rc ${returncode}
trace "[derive] result=${result}"
local notfirst=false
echo -n "{\"addresses\":["
local IFS=$'\n'
for address in ${result}
do
if ${notfirst}; then
echo ","
else
notfirst=true
fi
trace "[derive] address=${address}"
data="{\"address\":\"${address}\"}"
trace "[derive] data=${data}"
echo -n "${data}"
done
echo "]}"
return ${returncode}
}

View File

@@ -0,0 +1,84 @@
#!/bin/sh
#
#
#
#
. ./pycoin.sh
. ./responsetoclient.sh
. ./trace.sh
main()
{
trace "Entering main()..."
local step=0
local cmd
local http_method
local line
local content_length
local response
local returncode
while read line; do
line=$(echo "${line}" | tr -d '\r\n')
trace "[main] line=${line}"
if [ "${cmd}" = "" ]; then
# First line!
# Looking for something like:
# GET /cmd/params HTTP/1.1
# POST / HTTP/1.1
cmd=$(echo "${line}" | cut -d '/' -f2 | cut -d ' ' -f1)
trace "[main] cmd=${cmd}"
http_method=$(echo "${line}" | cut -d ' ' -f1)
trace "[main] http_method=${http_method}"
if [ "${http_method}" = "GET" ]; then
step=1
fi
fi
if [ "${line}" = "" ]; then
trace "[main] empty line"
if [ ${step} -eq 1 ]; then
trace "[main] body part finished, disconnecting"
break
else
trace "[main] headers part finished, body incoming"
step=1
fi
fi
# line=content-length: 406
case "${line}" in *[cC][oO][nN][tT][eE][nN][tT]-[lL][eE][nN][gG][tT][hH]*)
content_length=$(echo ${line} | cut -d ':' -f2)
trace "[main] content_length=${content_length}";
;;
esac
if [ ${step} -eq 1 ]; then
trace "[main] step=${step}"
if [ "${http_method}" = "POST" ]; then
read -n ${content_length} line
trace "[main] line=${line}"
fi
case "${cmd}" in
derive)
# POST http://192.168.111.152:7777/derive
# BODY {"pub32":"tpubD6NzVbkrYhZ4YR3QK2tyfMMvBghAvqtNaNK1LTyDWcRHLcMUm3ZN2cGm5BS3MhCRCeCkXQkTXXjiJgqxpqXK7PeUSp86DTTgkLpcjMtpKWk","path":"0/25-30"}
# BODY {"pub32":"upub5GtUcgGed1aGH4HKQ3vMYrsmLXwmHhS1AeX33ZvDgZiyvkGhNTvGd2TA5Lr4v239Fzjj4ZY48t6wTtXUy2yRgapf37QHgt6KWEZ6bgsCLpb","path":"0/25-30"}
# BODY {"pub32":"vpub5SLqN2bLY4WeZF3kL4VqiWF1itbf3A6oRrq9aPf16AZMVWYCuN9TxpAZwCzVgW94TNzZPNc9XAHD4As6pdnExBtCDGYRmNJrcJ4eV9hNqcv","path":"0/25-30"}
response=$(derive "${line}")
response_to_client "${response}" ${?}
break
;;
esac
break
fi
done
trace "[main] exiting"
return 0
}
export TRACING
main
exit $?

View File

@@ -0,0 +1,21 @@
#!/bin/sh
. ./trace.sh
response_to_client()
{
trace "Entering response_to_client()..."
local response=${1}
local returncode=${2}
([ -z "${returncode}" ] || [ "${returncode}" -eq "0" ]) && echo -ne "HTTP/1.1 200 OK\r\n"
[ -n "${returncode}" ] && [ "${returncode}" -ne "0" ] && echo -ne "HTTP/1.1 400 Bad Request\r\n"
echo -e "Content-Type: application/json\r\nContent-Length: ${#response}\r\n\r\n${response}"
# Small delay needed for the data to be processed correctly by peer
sleep 0.2s
}
case "${0}" in *responsetoclient.sh) response_to_client $@;; esac

View File

@@ -0,0 +1,6 @@
#!/bin/sh
export TRACING
export PYCOIN_LISTENING_PORT
nc -vlkp${PYCOIN_LISTENING_PORT} -e ./requesthandler.sh

View File

@@ -0,0 +1,15 @@
#!/bin/sh
trace()
{
if [ -n "${TRACING}" ]; then
echo "$(date -Is) ${1}" > /dev/stderr
fi
}
trace_rc()
{
if [ -n "${TRACING}" ]; then
echo "$(date -Is) Last return code: ${1}" > /dev/stderr
fi
}