Merge pull request #201 from SatoshiPortal/features/batching

Features/batching
This commit is contained in:
kexkey
2020-09-23 11:35:45 -04:00
committed by GitHub
27 changed files with 2549 additions and 210 deletions

View File

@@ -17,6 +17,7 @@ action_getactivewatchesbyxpub=watcher
action_getactivewatchesbylabel=watcher
action_getactivexpubwatches=watcher
action_watchtxid=watcher
action_unwatchtxid=watcher
action_getactivewatches=watcher
action_get_txns_by_watchlabel=watcher
action_get_unused_addresses_by_watchlabel=watcher
@@ -31,6 +32,7 @@ action_ln_decodebolt11=watcher
action_ln_listpeers=watcher
action_ln_getroute=watcher
action_ln_listpays=watcher
action_bitcoin_estimatesmartfee=watcher
# Spender can do what the watcher can do, plus:
action_getxnslist=spender
@@ -55,6 +57,12 @@ action_ln_decodebolt11=spender
action_ln_connectfund=spender
action_ln_listfunds=spender
action_ln_withdraw=spender
action_createbatcher=spender
action_updatebatcher=spender
action_removefrombatch=spender
action_listbatchers=spender
action_getbatcher=spender
action_getbatchdetails=spender
# Admin can do what the spender can do, plus:

View File

@@ -22,6 +22,7 @@ action_getactivexpubwatches=watcher
action_get_txns_by_watchlabel=watcher
action_get_unused_addresses_by_watchlabel=watcher
action_watchtxid=watcher
action_unwatchtxid=watcher
action_getactivewatches=watcher
action_getbestblockhash=watcher
action_getbestblockinfo=watcher
@@ -36,6 +37,7 @@ action_ln_decodebolt11=watcher
action_ln_listpeers=watcher
action_ln_getroute=watcher
action_ln_listpays=watcher
action_bitcoin_estimatesmartfee=watcher
# Spender can do what the watcher can do, plus:
action_get_txns_spending=spender
@@ -59,6 +61,12 @@ action_ln_decodebolt11=spender
action_ln_connectfund=spender
action_ln_listfunds=spender
action_ln_withdraw=spender
action_createbatcher=spender
action_updatebatcher=spender
action_removefrombatch=spender
action_listbatchers=spender
action_getbatcher=spender
action_getbatchdetails=spender
# Admin can do what the spender can do, plus:

View File

@@ -27,6 +27,7 @@ start_apps() {
export TOR_DATAPATH
export LIGHTNING_DATAPATH
export BITCOIN_DATAPATH
export LOGS_DATAPATH
export APP_SCRIPT_PATH
export APP_ID
export DOCKER_MODE

View File

@@ -2,9 +2,9 @@
## Current API
### Watch a Bitcoin Address (called by application)
### Watch a Bitcoin Address (called by your application)
Inserts the address and callbacks in the DB and imports the address to the Watching wallet. The callback URLs and event message are optional. If eventMessage is not supplied, tx_confirmation for that watch will not be published. Event message should be in base64 format to avoid dealing with escaping special characters.
Inserts the address, webhook URLs and eventMessage in the DB and imports the address to the Watching wallet. The webhook URLs (callbackURLs) and event message are optional. If eventMessage is not supplied, the event will not be published to the tx_confirmation topic on confirmations. Event message should be in base64 format to avoid dealing with escaping special characters. The same address can be watched by different requests with different webhook URLs.
```http
POST http://cyphernode:8888/watch
@@ -31,12 +31,18 @@ Proxy response:
}
```
### Un-watch a previously watched Bitcoin Address (called by application)
### Un-watch a previously watched Bitcoin Address (called by your application)
Updates the watched address row in DB so that callbacks won't be called on tx confirmations for that address.
Updates the watched address row in DB so that webhooks won't be called on tx confirmations for that address. You can POST the URLs to make sure you unwatch the good watcher, since there may be multiple watchers on the same address with different webhook URLs. You can also, more conveniently, supply the watch id to unwatch.
```http
GET http://cyphernode:8888/unwatch/2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp
or
POST http://192.168.111.152:8080/unwatch
with body...
{"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","unconfirmedCallbackURL":"192.168.111.233:1111/callback0conf","confirmedCallbackURL":"192.168.111.233:1111/callback1conf"}
or
{"id":3124}
```
Proxy response:
@@ -44,11 +50,13 @@ Proxy response:
```json
{
"event": "unwatch",
"address": "2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp"
"address": "2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp",
"unconfirmedCallbackURL": "192.168.133.233:1111/callback0conf",
"confirmedCallbackURL": "192.168.133.233:1111/callback1conf"
}
```
### Get a list of Bitcoin addresses being watched (called by application)
### Get a list of Bitcoin addresses being watched (called by your application)
Returns the list of currently watched addresses and callback information.
@@ -61,17 +69,19 @@ 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",
"eventMessage":"eyJib3VuY2VfYWRkcmVzcyI6IjJNdkEzeHIzOHIxNXRRZWhGblBKMVhBdXJDUFR2ZTZOamNGIiwibmJfY29uZiI6MH0K"}
{
"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",
"eventMessage":"eyJib3VuY2VfYWRkcmVzcyI6IjJNdkEzeHIzOHIxNXRRZWhGblBKMVhBdXJDUFR2ZTZOamNGIiwibmJfY29uZiI6MH0K"
}
]
}
```
### Get a list of txns from a watched label
Returns the list of transactions not spend(txns) from watched label.
@@ -126,7 +136,7 @@ Proxy response:
}
```
### Watch a Bitcoin xpub/ypub/zpub/tpub/upub/vpub extended public key (called by application)
### Watch a Bitcoin xpub/ypub/zpub/tpub/upub/vpub extended public key (called by your application)
Used to watch the transactions related to an xpub. It will first derive 100 addresses using the provided xpub, derivation path and index information. It will add those addresses to the watching DB table and add those addresses to the Watching-by-xpub wallet. The watching process will take care of calling the provided callbacks when a transaction occurs. When a transaction is seen, Cyphernode will derive and start watching new addresses related to the xpub, keeping a 100 address gap between the last used address in a transaction and the last watched address of that xpub. The label can be used later, instead of the whole xpub, with unwatchxpub* and and getactivewatchesby*.
@@ -151,7 +161,7 @@ Proxy response:
}
```
### Un-watch a previously watched Bitcoin xpub by providing the xpub (called by application)
### Un-watch a previously watched Bitcoin xpub by providing the xpub (called by your application)
Updates the watched address rows in DB so that callbacks won't be called on tx confirmations for the provided xpub and related addresses.
@@ -168,7 +178,7 @@ Proxy response:
}
```
### Un-watch a previously watched Bitcoin xpub by providing the label (called by application)
### Un-watch a previously watched Bitcoin xpub by providing the label (called by your application)
Updates the watched address rows in DB so that callbacks won't be called on tx confirmations for the provided xpub and related addresses.
@@ -185,7 +195,7 @@ Proxy response:
}
```
### Watch a TXID (called by application)
### Watch a TXID (called by your application)
Used to watch a transaction. Will call the 1-conf callback url after the transaction has been mined. Will call the x-conf callback url after the transaction has x confirmations.
@@ -209,7 +219,7 @@ Proxy response:
}
```
### Get a list of Bitcoin xpub being watched (called by application)
### Get a list of Bitcoin xpub being watched (called by your application)
Returns the list of currently watched xpub and callback information.
@@ -235,7 +245,7 @@ Proxy response:
}
```
### Get a list of Bitcoin addresses being watched by provided xpub (called by application)
### Get a list of Bitcoin addresses being watched by provided xpub (called by your application)
Returns the list of currently watched addresses related to the provided xpub and callback information.
@@ -262,7 +272,7 @@ Proxy response:
}
```
### Get a list of Bitcoin addresses being watched by provided xpub label (called by application)
### Get a list of Bitcoin addresses being watched by provided xpub label (called by your application)
Returns the list of currently watched addresses related to the provided xpub label and callback information.
@@ -321,7 +331,7 @@ When cyphernode receives a transaction confirmation (/conf endpoint) on a watche
"size":371,
"vsize":166,
"fees":0.00002992,
"is_replaceable":0,
"replaceable":false,
"blockhash":"",
"blocktime":"",
"blockheight":""
@@ -340,7 +350,7 @@ When cyphernode receives a transaction confirmation (/conf endpoint) on a watche
"size":371,
"vsize":166,
"fees":0.00002992,
"is_replaceable":0,
"replaceable":false,
"blockhash":"00000000000000000011bb83bb9bed0f6e131d0d0c903ec3a063e00b3aa00bf6",
"blocktime":"2018-10-18T16:58:49+0000",
"blockheight":""
@@ -367,7 +377,7 @@ Proxy response:
}
```
### Get the blockchain information (called by application)
### Get the blockchain information (called by your application)
Returns the blockchain information of the Bitcoin node. Used for example by the welcome app to get syncing progression.
@@ -431,7 +441,7 @@ Proxy response:
}
```
### Get the Block Hash from Height (called by application)
### Get the Block Hash from Height (called by your application)
Returns the best block hash matching height provided.
@@ -449,7 +459,7 @@ Proxy response:
}
```
### Get the Best Block Hash (called by application)
### Get the Best Block Hash (called by your application)
Returns the best block hash of the watching Bitcoin node.
@@ -467,7 +477,7 @@ Proxy response:
}
```
### Get Block Info (called by application)
### Get Block Info (called by your application)
Returns block info for the supplied block hash.
@@ -506,7 +516,7 @@ Proxy response:
}
```
### Get the Best Block Info (called by application)
### Get the Best Block Info (called by your application)
Returns best block info: calls getblockinfo with bestblockhash.
@@ -545,7 +555,7 @@ Proxy response:
}
```
### Get a transaction details (node's getrawtransaction) (called by application)
### Get a transaction details (node's getrawtransaction) (called by your application)
Calls getrawtransaction RPC for the supplied txid.
@@ -662,7 +672,7 @@ Proxy response:
}
```
### Get spending wallet's balance (called by application)
### Get spending wallet's balance (called by your application)
Calls getbalance RPC on the spending wallet.
@@ -678,7 +688,7 @@ Proxy response:
}
```
### Get spending wallet's extended balances (called by application)
### Get spending wallet's extended balances (called by your application)
Calls getbalances RPC on the spending wallet.
@@ -700,7 +710,7 @@ Proxy response:
}
```
### Get a new Bitcoin address from spending wallet (called by application)
### Get a new Bitcoin address from spending wallet (called by your application)
Calls getnewaddress RPC on the spending wallet. Used to refill the spending wallet from cold wallet (ie Trezor). Will derive the default address type (set in your bitcoin.conf file, p2sh-segwit if not specified) or you can supply the address type like the following examples.
@@ -725,7 +735,7 @@ Proxy response:
}
```
### Spend coins from spending wallet (called by application)
### Spend coins from spending wallet (called by your application)
Calls sendtoaddress RPC on the spending wallet with supplied info. Can supply an eventMessage to be published on successful spending. eventMessage should be base64 encoded to avoid dealing with escaping special characters.
@@ -757,7 +767,7 @@ Proxy response:
}
```
### Bump transaction's fees (called by application)
### Bump transaction's fees (called by your application)
Calls bumpfee RPC on the spending wallet with supplied info.
@@ -780,7 +790,7 @@ Proxy response:
}
```
### Add an output to the next batched transaction (called by application)
### Add an output to the next batched transaction (called by your application)
Inserts output information in the DB. Used when batchspend is called later.
@@ -792,7 +802,7 @@ with body...
Proxy response: EMPTY
### Spend a batched transaction with outputs added with addtobatch (called by application)
### Spend a batched transaction with outputs added with addtobatch (called by your application)
Calls sendmany RPC on spending wallet with the unspent "addtobatch" inserted outputs. Will be useful during next bull run.
@@ -809,7 +819,7 @@ Proxy response:
}
```
### Get derived address(es) using path in config and provided index (called by application)
### Get derived address(es) using path in config and provided index (called by your application)
Derives addresses for supplied index. Must be used with derivation.pub32 and derivation.path properties in config.properties.
@@ -833,7 +843,7 @@ Proxy response:
}
```
### Get derived address(es) using provided path and index (called by application)
### Get derived address(es) using provided path and index (called by your application)
Derives addresses for supplied pub32 and path. config.properties' derivation.pub32 and derivation.path are not used.
@@ -866,7 +876,7 @@ Proxy response:
}
```
### Get info from Lightning Network node (called by application)
### Get info from Lightning Network node (called by your application)
Calls getinfo from lightningd. Useful to let your users know where to connect to.
@@ -901,7 +911,7 @@ Proxy response:
}
```
### Create a Lightning Network invoice (called by application)
### Create a Lightning Network invoice (called by your application)
Returns a LN invoice. Label must be unique. Description will be used by your user for payment. Expiry is in seconds and optional. If msatoshi is not supplied, will use "any" (ie donation invoice). callbackUrl is optional.
@@ -923,7 +933,7 @@ Proxy response:
}
```
### Pay a Lightning Network invoice (called by application)
### Pay a Lightning Network invoice (called by your 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. If the bolt11 invoice doesn't contain an amount, then the expected_msatoshi supplied here will be used as the paid amount.
@@ -962,7 +972,7 @@ Proxy response:
```
### Get a new Bitcoin address from the Lightning Network node (to fund it) (called by application)
### Get a new Bitcoin address from the Lightning Network node (to fund it) (called by your application)
Returns a Bitcoin bech32 address to fund your LN wallet.
@@ -1098,7 +1108,7 @@ Proxy response:
}
```
### Get the list of peers, with channels, from Lightning Network node (called by application)
### Get the list of peers, with channels, from Lightning Network node (called by your application)
Calls listpeers from lightningd. Returns the list of peers and the channels opened with them, even for currently offline peers.
@@ -1389,7 +1399,7 @@ Proxy response:
"txid": "6b38....b0c3b"
}
```
### Stamp a hash on the Bitcoin blockchain using OTS (called by application)
### Stamp a hash on the Bitcoin blockchain using OTS (called by your application)
Will stamp the supplied hash to the Bitcoin blockchain using OTS. Cyphernode will curl the callback when the OTS stamping is complete.
@@ -1496,3 +1506,270 @@ Proxy response:
"message": "Base64 string of the information text"
}
```
### Create a batcher
Used to create a batching template, by setting a label and a default confTarget.
```http
POST http://cyphernode:8888/createbatcher
with body...
{"batcherLabel":"lowfees","confTarget":32}
```
Proxy response:
```json
{
"result": {
"batcherId": 1
},
"error": null
}
```
### Update a batcher
Used to change batching template settings.
```http
POST http://cyphernode:8888/updatebatcher
with body...
{"batcherId":5,"confTarget":12}
or
{"batcherLabel":"fast","confTarget":2}
```
Proxy response:
```json
{
"result": {
"batcherId": 1,
"batcherLabel": "default",
"confTarget": 6
},
"error": null
}
```
### Add an output to the next batched transaction (called by your 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}
or
{"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"batcherId":34,"webhookUrl":"https://myCypherApp:3000/batchExecuted"}
or
{"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"batcherLabel":"lowfees","webhookUrl":"https://myCypherApp:3000/batchExecuted"}
or
{"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"batcherId":34,"webhookUrl":"https://myCypherApp:3000/batchExecuted"}
```
Proxy response:
```json
{
"result": {
"batcherId": 1,
"outputId": 34,
"nbOutputs": 7,
"oldest": "2020-09-09 14:00:01",
"total": 0.04016971
},
"error": null
}
```
### Remove an output from the next batched transaction (called by your application)
Removes a previously added output scheduled for the next batch.
```http
POST http://cyphernode:8888/removefrombatch
with body...
{"outputId":72}
```
Proxy response:
```json
{
"result": {
"batcherId": 1,
"outputId": 72,
"nbOutputs": 6,
"oldest": "2020-09-09 14:00:01",
"total": 0.03783971
},
"error": null
}
```
### Spend a batched transaction with outputs previously added with addtobatch (called by your application)
Calls the sendmany RPC on spending wallet with the unspent "addtobatch" inserted outputs. Will execute default batcher if no batcherId/batcherLabel supplied and default confTarget if no confTarget supplied.
```http
POST http://cyphernode:8888/batchspend
with body...
{}
or
{"batcherId":34}
or
{"batcherId":34,"confTarget":12}
or
{"batcherLabel":"fastest","confTarget":2}
```
Proxy response:
```json
{
"result": {
"batcherId":34,
"confTarget":6,
"nbOutputs":83,
"oldest":123123,
"total":10.86990143,
"txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648",
"hash":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648",
"details":{
"firstseen":123123,
"size":424,
"vsize":371,
"replaceable":true,
"fee":0.00004112
},
"outputs":{
"1abc":0.12,
"3abc":0.66,
"bc1abc":2.848,
...
}
},
"error":null
}
```
### Get batcher (called by your application)
Will return current state/summary of the requested batching template.
```http
POST http://cyphernode:8888/getbatcher
with body...
{}
or
{"batcherId":34}
or
{"batcherLabel":"fastest"}
```
Proxy response:
```json
{
"result": {
"batcherId": 1,
"batcherLabel": "default",
"confTarget": 6,
"nbOutputs": 12,
"oldest": 123123,
"total": 0.86990143
},
"error": null
}
```
### Get batch details (called by your application)
Will return current state and details of the requested batch, including all outputs. A batch is the combination of a batcher and an optional txid. If no txid is supplied, will return current non-yet-executed batch.
```http
POST http://cyphernode:8888/getbatchdetails
with body...
{}
or
{"batcherId":34}
or
{"batcherLabel":"fastest","txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648"}
```
Proxy response:
```json
{
"result": {
"batcherId": 34,
"batcherLabel": "Special batcher for a special client",
"confTarget": 6,
"nbOutputs": 83,
"oldest": 123123,
"total": 10.86990143,
"txid": "af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648",
"hash": "af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648",
"details": {
"firstseen": 123123,
"size": 424,
"vsize": 371,
"replaceable":true,
"fee": 0.00004112
},
"outputs": {
"1abc": 0.12,
"3abc": 0.66,
"bc1abc": 2.848,
...
}
},
"error": null
}
```
### Get a list of existing batch templates (called by your application)
Will return a list of batch templates. batcherId 1 is a default batcher created at installation time.
```http
GET http://cyphernode:8888/listbatchers
```
Proxy response:
```json
{
"result": [
{"batcherId":1,"batcherLabel":"default","confTarget":6,"nbOutputs":12,"oldest":123123,"total":0.86990143},
{"batcherId":2,"batcherLabel":"lowfee","confTarget":32,"nbOutputs":44,"oldest":123123,"total":0.49827387},
{"batcherId":3,"batcherLabel":"highfee","confTarget":2,"nbOutputs":7,"oldest":123123,"total":4.16843782}
],
"error": null
}
```
### Get an estimation of current Bitcoin fees
This will call the Bitcoin Core estimatesmartfee RPC call and return the result as is.
```http
POST http://cyphernode:8888/bitcoin_estimatesmartfee
with body...
{"confTarget":2}
```
Proxy response:
```json
{
"result": {
"feerate": 0.00001000,
"blocks": 4
},
"error": null,
"id": null
}
```

50
doc/BATCHING.md Normal file
View File

@@ -0,0 +1,50 @@
# How Batching works in Cyphernode
Details on how batching was implemented in Cyphernode.
## Glossary
A Batcher is a batching template with corresponding past batched transactions and a queue of outputs waiting to be batched in the next batch transaction.
A batched transaction is a transaction that combines multiple recipients in one transaction with multiple outputs, instead of using multiple individual transactions.
An ongoing batch is a batcher with its queued outputs waiting to be part of the next batch transaction. There's no associated txid yet.
## Entities
### Database
See [Cyphernode's Entity-Relation Model](../proxy_docker/app/data/cyphernode.sql).
- `batcher`: batching template. The conf_target is the default confTarget that will be used when creating the batch transaction if no confTarget is supplied to batchspend that would override it.
- id: autoincrementing primary key
- label: optional unique label to be used on subsequent calls instead of using the id
- conf_target: optional default confTarget to be used when creating the batched transaction
- `recipient`: a batch output. Minimally requires the destination address and the amount.
- id: autoincrementing primary key
- address: destination Bitcoin address
- amount: amount to be sent, in BTC
- tx_id: foreign key on the tx table, the actual transaction if created
- webhook_url: optional URL that you want Cyphernode to call back when the batch transaction is broadcast
- batcher_id: foreign key on the batcher table, the corresponding batching template for this recipient
- label: an optional label for this output.
- `tx`: a transaction. The information about a broadcast Bitcoin transaction.
### Good to know
- There is a default batcher created on installation time, with id 1, label "default" and conf_target 6.
- When a recipient has no tx_id, it means it is waiting for the next batch.
- When a recipian has a batcher_id, it means it is part of a past or ongoing batch.
- When a batch transaction is broadcast, the webhook_url of each included recipient will be called by Cyphernode, if present, with information about the batched transaction in the POSTed body.
- Cyphernode knows when a callback webhook didn't work. It will retry the callback when a new blocks is mined, until it works.
## 2nd layer: the Batcher cypherapp
The Cyphernode's base functionalities for batching is pretty basic. Instead of adding complex batching features to the Cyphernode API, we decided to develop a [CypherApp](CYPHERAPPS.md).
The [Batcher](https://github.com/SatoshiPortal/batcher) cypherapp will take care of the following tasks:
- Merging same destination outputs into one, adding the amounts and calling the different webhook URLs on batch execution.
- Scheduling the batches.
- Executing the batches when an amount threshold has been reached.
- Hiding Cyphernode complexity by dealing only with a `batchRequestId` and a `batchId`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -146,6 +146,61 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ApiResponseTemporarilyUnavailable'
/unwatch:
post:
tags:
- "watching addresses"
- "core features"
summary: "Stop watching a Bitcoin address"
description: "Updates the watched Bitcoin address row in DB so that callbacks won't be called on tx confirmations for that address for the specified URLs or id."
operationId: "deleteWatchedAddress"
requestBody:
description: "Bitcoin address that needs to be watched"
required: true
content:
application/json:
schema:
type: "object"
properties:
address:
$ref: '#/components/schemas/TypeAddressString'
unconfirmedCallbackURL:
type: "string"
format: "url"
confirmedCallbackURL:
type: "string"
format: "url"
id:
description: "id returned by the corresponding watch"
type: "string"
responses:
'200':
description: "successfully unwatched"
content:
application/json:
schema:
type: "object"
properties:
event:
type: "string"
address:
$ref: '#/components/schemas/TypeAddressString'
unconfirmedCallbackURL:
type: "string"
format: "url"
confirmedCallbackURL:
type: "string"
format: "url"
'400':
$ref: '#/components/schemas/ApiResponseInvalidInput'
'403':
$ref: '#/components/schemas/ApiResponseNotAllowed'
'503':
description: "Resource temporarily unavailable"
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponseTemporarilyUnavailable'
/watchxpub:
post:
tags:
@@ -1181,16 +1236,115 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ApiResponseTemporarilyUnavailable'
/createbatcher:
post:
tags:
- "spending wallet"
- "core features"
- "batching"
summary: "Create a batching template, by setting a label and a default confTarget"
description: "Inserts batcher information to the DB."
operationId: "createbatcher"
requestBody:
description: "Batcher label and conf target"
required: true
content:
application/json:
schema:
type: "object"
properties:
batcherLabel:
type: "string"
confTarget:
type: "number"
responses:
'200':
description: "operation successful"
content:
application/json:
schema:
type: "object"
properties:
result:
type: "object"
properties:
batcherId:
type: "number"
error:
type: "object"
'403':
$ref: '#/components/schemas/ApiResponseNotAllowed'
'405':
$ref: '#/components/schemas/ApiResponseInvalidInput'
'503':
description: "Resource temporarily unavailable"
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponseTemporarilyUnavailable'
/updatebatcher:
post:
tags:
- "spending wallet"
- "core features"
- "batching"
summary: "Update a batching template, by changing the label or the default confTarget"
description: "Updates batcher information to the DB."
operationId: "updatebatcher"
requestBody:
description: "Batcher id, batcher label and conf target"
required: true
content:
application/json:
schema:
type: "object"
properties:
batcherId:
type: "number"
batcherLabel:
type: "string"
confTarget:
type: "number"
responses:
'200':
description: "operation successful"
content:
application/json:
schema:
type: "object"
properties:
result:
type: "object"
properties:
batcherId:
type: "number"
batcherLabel:
type: "string"
confTarget:
type: "number"
error:
type: "object"
'403':
$ref: '#/components/schemas/ApiResponseNotAllowed'
'405':
$ref: '#/components/schemas/ApiResponseInvalidInput'
'503':
description: "Resource temporarily unavailable"
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponseTemporarilyUnavailable'
/addtobatch:
post:
tags:
- "spending wallet"
- "core features"
- "batching"
summary: "Adds spending of some amount to some address to the next batch"
description: "Inserts output information in the DB. Used when batchspend is called later."
operationId: "spendInNextBatch"
requestBody:
description: "Address and amount"
description: "Address, amount, batcherId, batcherLabel and webhookUrl"
required: true
content:
application/json:
@@ -1204,9 +1358,90 @@ paths:
$ref: '#/components/schemas/TypeAddressString'
amount:
type: "number"
batcherId:
type: "number"
batcherLabel:
type: "string"
webhookUrl:
type: "string"
format: "url"
responses:
'200':
description: "operation successful"
content:
application/json:
schema:
type: "object"
properties:
result:
type: "object"
properties:
batcherId:
type: "number"
outputId:
type: "number"
nbOutputs:
type: "number"
oldest:
type: "string"
total:
type: "number"
error:
type: "object"
'403':
$ref: '#/components/schemas/ApiResponseNotAllowed'
'405':
$ref: '#/components/schemas/ApiResponseInvalidInput'
'503':
description: "Resource temporarily unavailable"
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponseTemporarilyUnavailable'
/removefrombatch:
post:
tags:
- "spending wallet"
- "core features"
- "batching"
summary: "Removes a previously added output from the next batch"
description: "Deletes output from the DB."
operationId: "removeFromNextBatch"
requestBody:
description: "outputId returned by corresponding addtobatch"
required: true
content:
application/json:
schema:
type: "object"
required:
- "outputId"
properties:
outputId:
type: "number"
responses:
'200':
description: "operation successful"
content:
application/json:
schema:
type: "object"
properties:
result:
type: "object"
properties:
batcherId:
type: "number"
outputId:
type: "number"
nbOutputs:
type: "number"
oldest:
type: "string"
total:
type: "number"
error:
type: "object"
'403':
$ref: '#/components/schemas/ApiResponseNotAllowed'
'405':
@@ -1218,13 +1453,28 @@ paths:
schema:
$ref: '#/components/schemas/ApiResponseTemporarilyUnavailable'
/batchspend:
get:
post:
tags:
- "spending wallet"
- "core features"
summary: "Spend previously amounts/addresses added with addtobatch"
description: "Creates a batched transaction whose outputs are the previously unspent addtobatch calls."
- "batching"
summary: "Spend previously added amounts/addresses in a batch"
description: "Creates a batched transaction whose outputs are the previously unspent addtobatch calls for the batcher."
operationId: "batchSpend"
requestBody:
description: "batcherId or batcherLabel with an optional confTarget to override the batcher's default"
required: true
content:
application/json:
schema:
type: "object"
properties:
batcherId:
type: "number"
batcherLabel:
type: "string"
confTarget:
type: "number"
responses:
'200':
description: "operation successful"
@@ -1232,18 +1482,232 @@ paths:
application/json:
schema:
type: "object"
required:
- "status"
- "hash"
properties:
status:
type: "string"
hash:
$ref: '#/components/schemas/TypeHashString'
'400':
$ref: '#/components/schemas/ApiResponseInvalidInput'
result:
type: "object"
properties:
batcherId:
type: "number"
confTarget:
type: "number"
nbOutputs:
type: "number"
oldest:
type: "string"
total:
type: "number"
txid:
type: "string"
hash:
type: "string"
details:
type: "object"
outputs:
type: "object"
error:
type: "object"
'403':
$ref: '#/components/schemas/ApiResponseNotAllowed'
'405':
$ref: '#/components/schemas/ApiResponseInvalidInput'
'503':
description: "Resource temporarily unavailable"
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponseTemporarilyUnavailable'
/getbatcher:
post:
tags:
- "spending wallet"
- "core features"
- "batching"
summary: "Returns current state/summary of the requested batching template"
description: "Get information from the batcher and recipient DB tables."
operationId: "getBatcher"
requestBody:
description: "Optional batcherId or batcherLabel, default batcher if not supplied"
required: true
content:
application/json:
schema:
type: "object"
properties:
batcherId:
type: "number"
batcherLabel:
type: "string"
responses:
'200':
description: "operation successful"
content:
application/json:
schema:
type: "object"
properties:
result:
type: "object"
properties:
batcherId:
type: "number"
batcherLabel:
type: "string"
confTarget:
type: "number"
nbOutputs:
type: "number"
oldest:
type: "string"
total:
type: "number"
error:
type: "object"
'403':
$ref: '#/components/schemas/ApiResponseNotAllowed'
'405':
$ref: '#/components/schemas/ApiResponseInvalidInput'
'503':
description: "Resource temporarily unavailable"
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponseTemporarilyUnavailable'
/getbatchdetails:
post:
tags:
- "spending wallet"
- "core features"
- "batching"
summary: "Returns current state and details of the requested batch, including all outputs"
description: "Get detailed information from the batcher and recipient DB tables."
operationId: "getBatcherDetails"
requestBody:
description: "Optional batcherId or batcherLabel and txid, default batcher if not supplied"
required: true
content:
application/json:
schema:
type: "object"
properties:
batcherId:
type: "number"
batcherLabel:
type: "string"
txid:
type: "string"
responses:
'200':
description: "operation successful"
content:
application/json:
schema:
type: "object"
properties:
result:
type: "object"
properties:
batcherId:
type: "number"
batcherLabel:
type: "string"
confTarget:
type: "number"
nbOutputs:
type: "number"
oldest:
type: "string"
total:
type: "number"
txid:
type: "string"
hash:
type: "string"
details:
type: "object"
outputs:
type: "object"
error:
type: "object"
'403':
$ref: '#/components/schemas/ApiResponseNotAllowed'
'405':
$ref: '#/components/schemas/ApiResponseInvalidInput'
'503':
description: "Resource temporarily unavailable"
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponseTemporarilyUnavailable'
/listbatchers:
get:
tags:
- "spending wallet"
- "core features"
- "batching"
summary: "Get list of batchers, including the default batcher"
description: "Returns the list of batch templates."
operationId: "listBatchers"
responses:
'200':
description: "successful operation"
content:
application/json:
schema:
type: "object"
properties:
result:
type: "array"
error:
type: "object"
'403':
$ref: '#/components/schemas/ApiResponseNotAllowed'
'503':
description: "Resource temporarily unavailable"
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponseTemporarilyUnavailable'
/bitcoin_estimatesmartfee:
post:
tags:
- "core features"
- "bitcoin"
summary: "Returns current fee estimation computed by Bitcoin Core's estimatesmartfee"
description: "Returns current fee estimation computed by Bitcoin Core's estimatesmartfee"
operationId: "estimateSmartFee"
requestBody:
description: "Conf Target"
required: true
content:
application/json:
schema:
type: "object"
properties:
confTarget:
type: "number"
responses:
'200':
description: "operation successful"
content:
application/json:
schema:
type: "object"
properties:
result:
type: "object"
properties:
feerate:
type: "number"
blocks:
type: "number"
error:
type: "object"
id:
type: "number"
'403':
$ref: '#/components/schemas/ApiResponseNotAllowed'
'405':
$ref: '#/components/schemas/ApiResponseInvalidInput'
'503':
description: "Resource temporarily unavailable"
content:

View File

@@ -116,7 +116,7 @@ components:
type: "integer"
fees:
type: "number"
is_replaceable:
replaceable:
type: "integer"
blockhash:
$ref: '#/components/schemas/TypeHashString'

View File

@@ -14,7 +14,7 @@ CREATE TABLE watching_by_pub32 (
CREATE TABLE watching (
id INTEGER PRIMARY KEY AUTOINCREMENT,
address TEXT UNIQUE,
address TEXT,
watching INTEGER DEFAULT FALSE,
callback0conf TEXT,
calledback0conf INTEGER DEFAULT FALSE,
@@ -26,6 +26,8 @@ CREATE TABLE watching (
event_message TEXT,
inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_watching_address ON watching (address);
CREATE UNIQUE INDEX idx_watching_01 ON watching (address, callback0conf, callback1conf);
CREATE TABLE watching_tx (
watching_id INTEGER REFERENCES watching,
@@ -48,6 +50,7 @@ CREATE TABLE tx (
blockhash TEXT,
blockheight INTEGER,
blocktime INTEGER,
conf_target INTEGER,
raw_tx TEXT,
inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP
);
@@ -64,13 +67,28 @@ CREATE TABLE recipient (
address TEXT,
amount REAL,
tx_id INTEGER REFERENCES tx,
inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP
inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP,
webhook_url TEXT,
calledback INTEGER DEFAULT FALSE,
calledback_ts INTEGER,
batcher_id INTEGER REFERENCES batcher,
label TEXT
);
CREATE INDEX idx_recipient_address ON recipient (address);
CREATE INDEX idx_recipient_label ON recipient (label);
CREATE TABLE batcher (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT UNIQUE,
conf_target INTEGER,
feerate REAL,
inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO batcher (id, label, conf_target, feerate) VALUES (1, "default", 6, NULL);
CREATE TABLE watching_by_txid (
id INTEGER PRIMARY KEY AUTOINCREMENT,
txid TEXT UNIQUE,
txid TEXT,
watching INTEGER DEFAULT FALSE,
callback1conf TEXT,
calledback1conf INTEGER DEFAULT FALSE,
@@ -80,6 +98,7 @@ CREATE TABLE watching_by_txid (
inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_watching_by_txid_txid ON watching_by_txid (txid);
CREATE UNIQUE INDEX idx_watching_by_txid_1x ON watching_by_txid (txid, callback1conf, callbackxconf);
CREATE TABLE stamp (
id INTEGER PRIMARY KEY AUTOINCREMENT,

0
proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh Normal file → Executable file
View File

0
proxy_docker/app/data/sqlmigrate20190104_0.1-0.2.sh Normal file → Executable file
View File

0
proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sh Normal file → Executable file
View File

View File

View File

@@ -0,0 +1,14 @@
#!/bin/sh
echo "Checking for extended batching support in DB..."
count=$(sqlite3 $DB_FILE "select count(*) from pragma_table_info('recipient') where name='batcher_id'")
if [ "${count}" -eq "0" ]; then
# batcher_id not there, we have to migrate
echo "Migrating database for extended batching support..."
echo "Backing up current DB..."
cp $DB_FILE $DB_FILE-sqlmigrate20200610_0.4.0-0.5.0
echo "Altering DB..."
cat sqlmigrate20200610_0.4.0-0.5.0.sql | sqlite3 $DB_FILE
else
echo "Database extended batching support migration already done, skipping!"
fi

View File

@@ -0,0 +1,75 @@
PRAGMA foreign_keys=off;
BEGIN TRANSACTION;
CREATE TABLE batcher (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT UNIQUE,
conf_target INTEGER,
feerate REAL,
inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO batcher (id, label, conf_target, feerate) VALUES (1, "default", 6, NULL);
ALTER TABLE recipient ADD COLUMN webhook_url TEXT;
ALTER TABLE recipient ADD COLUMN batcher_id INTEGER REFERENCES batcher;
ALTER TABLE recipient ADD COLUMN label INTEGER REFERENCES batcher;
ALTER TABLE recipient ADD COLUMN calledback INTEGER DEFAULT FALSE;
ALTER TABLE recipient ADD COLUMN calledback_ts INTEGER;
CREATE INDEX idx_recipient_label ON recipient (label);
ALTER TABLE tx ADD COLUMN conf_target INTEGER DEFAULT NULL;
ALTER TABLE watching RENAME TO watching_20200610;
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,
watching_by_pub32_id INTEGER REFERENCES watching_by_pub32,
pub32_index INTEGER,
event_message TEXT,
inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO watching SELECT * FROM watching_20200610;
DROP INDEX IF EXISTS idx_watching_address;
CREATE INDEX idx_watching_address ON watching (address);
DROP INDEX IF EXISTS idx_watching_01;
CREATE UNIQUE INDEX idx_watching_01 ON watching (address, callback0conf, callback1conf);
--DROP TABLE watching20200610;
ALTER TABLE watching_by_txid RENAME TO watching_by_txid_20200610;
CREATE TABLE watching_by_txid (
id INTEGER PRIMARY KEY AUTOINCREMENT,
txid TEXT,
watching INTEGER DEFAULT FALSE,
callback1conf TEXT,
calledback1conf INTEGER DEFAULT FALSE,
callbackxconf TEXT,
calledbackxconf INTEGER DEFAULT FALSE,
nbxconf INTEGER,
inserted_ts INTEGER DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO watching_by_txid SELECT * FROM watching_by_txid_20200610;
DROP INDEX IF EXISTS idx_watching_by_txid_txid;
CREATE INDEX idx_watching_by_txid_txid ON watching_by_txid (txid);
DROP INDEX IF EXISTS idx_watching_by_txid_1x;
CREATE UNIQUE INDEX idx_watching_by_txid_1x ON watching_by_txid (txid, callback1conf, callbackxconf);
--DROP TABLE watching_by_txid_20200610;
COMMIT;
PRAGMA foreign_keys=on;

View File

@@ -0,0 +1,880 @@
#!/bin/sh
. ./trace.sh
. ./sendtobitcoinnode.sh
createbatcher() {
trace "Entering createbatcher()..."
# POST http://192.168.111.152:8080/createbatcher
#
# args:
# - batcherLabel, optional, id can be used to reference the batcher
# - confTarget, optional, overriden by batchspend's confTarget, default Bitcoin Core conf_target will be used if not supplied
# NOTYET - feeRate, sat/vB, optional, overrides confTarget if supplied, overriden by batchspend's feeRate, default Bitcoin Core fee policy will be used if not supplied
#
# response:
# - batcherId, the batcher id
#
# BODY {"batcherLabel":"lowfees","confTarget":32}
# NOTYET BODY {"batcherLabel":"highfees","feeRate":231.8}
local request=${1}
local response
local label=$(echo "${request}" | jq ".batcherLabel")
trace "[createbatcher] label=${label}"
local conf_target=$(echo "${request}" | jq ".confTarget")
trace "[createbatcher] conf_target=${conf_target}"
local feerate=$(echo "${request}" | jq ".feeRate")
trace "[createbatcher] feerate=${feerate}"
# if [ "${feerate}" != "null" ]; then
# # If not null, let's nullify conf_target since feerate overrides it
# conf_target="null"
# trace "[createbatcher] Overriding conf_target=${conf_target}"
# fi
local batcher_id
batcher_id=$(sql "INSERT OR IGNORE INTO batcher (label, conf_target, feerate) VALUES (${label}, ${conf_target}, ${feerate}); SELECT LAST_INSERT_ROWID();")
if ("${batcher_id}" -eq "0"); then
trace "[createbatcher] Could not insert"
response='{"result":null,"error":{"code":-32700,"message":"Could not create batcher, label probably already exists","data":'${request}'}}'
else
trace "[createbatcher] Inserted"
response='{"result":{"batcherId":'${batcher_id}'},"error":null}'
fi
echo "${response}"
}
updatebatcher() {
trace "Entering updatebatcher()..."
# POST http://192.168.111.152:8080/updatebatcher
#
# args:
# - batcherId, optional, batcher id to update, will update default batcher if not supplied
# - batcherLabel, optional, id can be used to reference the batcher, will update default batcher if not supplied, if id is present then change the label with supplied text
# - confTarget, optional, new confirmation target for the batcher
# NOTYET - feeRate, sat/vB, optional, new feerate for the batcher
#
# response:
# - batcherId, the batcher id
# - batcherLabel, the batcher label
# - confTarget, the batcher default confirmation target
# NOTYET - feeRate, the batcher default feerate
#
# BODY {"batcherId":5,"confTarget":12}
# NOTYET BODY {"batcherLabel":"highfees","feeRate":400}
# NOTYET BODY {"batcherId":3,"batcherLabel":"ultrahighfees","feeRate":800}
# BODY {"batcherLabel":"fast","confTarget":2}
local request=${1}
local response
local whereclause
local returncode
local id=$(echo "${request}" | jq ".batcherId")
trace "[updatebatcher] id=${id}"
local label=$(echo "${request}" | jq ".batcherLabel")
trace "[updatebatcher] label=${label}"
local conf_target=$(echo "${request}" | jq ".confTarget")
trace "[updatebatcher] conf_target=${conf_target}"
local feerate=$(echo "${request}" | jq ".feeRate")
trace "[updatebatcher] feerate=${feerate}"
if [ "${id}" = "null" ] && [ "${label}" = "null" ]; then
# If id and label are null, use default batcher
trace "[updatebatcher] Using default batcher 1"
id=1
fi
# if [ "${feerate}" != "null" ]; then
# # If not null, let's nullify conf_target since feerate overrides it
# conf_target="null"
# trace "[updatebatcher] Overriding conf_target=${conf_target}"
# fi
if [ "${id}" = "null" ]; then
whereclause="label=${label}"
else
whereclause="id = ${id}"
fi
sql "UPDATE batcher set label=${label}, conf_target=${conf_target}, feerate=${feerate} WHERE ${whereclause}"
returncode=$?
trace_rc ${returncode}
if [ "${returncode}" -ne 0 ]; then
response='{"result":null,"error":{"code":-32700,"message":"Could not update batcher","data":'${request}'}}'
else
response='{"result":{"batcherId":'${id}'},"error":null}'
fi
echo "${response}"
}
addtobatch() {
trace "Entering addtobatch()..."
# POST http://192.168.111.152:8080/addtobatch
#
# args:
# - address, required, desination address
# - amount, required, amount to send to the destination address
# - outputLabel, optional, if you want to reference this output
# - batcherId, optional, the id of the batcher to which the output will be added, default batcher if not supplied, overrides batcherLabel
# - batcherLabel, optional, the label of the batcher to which the output will be added, default batcher if not supplied
# - webhookUrl, optional, the webhook to call when the batch is broadcast
#
# response:
# - batcherId, the id of the batcher
# - outputId, the id of the added output
# - nbOutputs, the number of outputs currently in the batch
# - oldest, the timestamp of the oldest output in the batch
# - total, the current sum of the batch's output amounts
#
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233}
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"batcherId":34,"webhookUrl":"https://myCypherApp:3000/batchExecuted"}
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"batcherLabel":"lowfees","webhookUrl":"https://myCypherApp:3000/batchExecuted"}
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"batcherId":34,"webhookUrl":"https://myCypherApp:3000/batchExecuted"}
local request=${1}
local response
local returncode=0
local inserted_id
local row
local address=$(echo "${request}" | jq -r ".address")
trace "[addtobatch] address=${address}"
local amount=$(echo "${request}" | jq ".amount")
trace "[addtobatch] amount=${amount}"
local label=$(echo "${request}" | jq ".outputLabel")
trace "[addtobatch] label=${label}"
local batcher_id=$(echo "${request}" | jq ".batcherId")
trace "[addtobatch] batcher_id=${batcher_id}"
local batcher_label=$(echo "${request}" | jq ".batcherLabel")
trace "[addtobatch] batcher_label=${batcher_label}"
local webhook_url=$(echo "${request}" | jq ".webhookUrl")
trace "[addtobatch] webhook_url=${webhook_url}"
local isvalid
isvalid=$(validateaddress "${address}" | jq ".result.isvalid")
if [ "${isvalid}" != "true" ]; then
response='{"result":null,"error":{"code":-32700,"message":"Invalid address","data":'${request}'}}'
trace "[addtobatch] Invalid address"
trace "[addtobatch] responding=${response}"
echo "${response}"
return 1
fi
if [ "${batcher_id}" = "null" ] && [ "${batcher_label}" = "null" ]; then
# If batcher_id and batcher_label are null, use default batcher
trace "[addtobatch] Using default batcher 1"
batcher_id=1
fi
if [ "${batcher_id}" = "null" ]; then
# Using batcher_label
batcher_id=$(sql "SELECT id FROM batcher WHERE label=${batcher_label}")
returncode=$?
trace_rc ${returncode}
fi
if [ -z "${batcher_id}" ]; then
# batcherLabel not found
response='{"result":null,"error":{"code":-32700,"message":"batcher not found","data":'${request}'}}'
else
# Check if address already pending for this batcher...
inserted_id=$(sql "SELECT id FROM recipient WHERE address=\"${address}\" AND tx_id IS NULL AND batcher_id=${batcher_id}")
if [ -n "${inserted_id}" ]; then
response='{"result":null,"error":{"code":-32700,"message":"Duplicated address","data":'${request}'}}'
trace "[addtobatch] Duplicated address"
trace "[addtobatch] responding=${response}"
echo "${response}"
return 1
fi
# Insert the new destination
inserted_id=$(sql "INSERT INTO recipient (address, amount, webhook_url, batcher_id, label) VALUES (\"${address}\", ${amount}, ${webhook_url}, ${batcher_id}, ${label}); SELECT LAST_INSERT_ROWID();")
returncode=$?
trace_rc ${returncode}
if [ "${returncode}" -ne 0 ]; then
response='{"result":null,"error":{"code":-32700,"message":"Could not add to batch","data":'${request}'}}'
else
row=$(sql "SELECT COUNT(id), MIN(inserted_ts), SUM(amount) FROM recipient WHERE tx_id IS NULL AND batcher_id=${batcher_id}")
returncode=$?
trace_rc ${returncode}
local count=$(echo "${row}" | cut -d '|' -f1)
trace "[addtobatch] count=${count}"
local oldest=$(echo "${row}" | cut -d '|' -f2)
trace "[addtobatch] oldest=${oldest}"
local total=$(echo "${row}" | cut -d '|' -f3)
trace "[addtobatch] total=${total}"
response='{"result":{"batcherId":'${batcher_id}',"outputId":'${inserted_id}',"nbOutputs":'${count}',"oldest":"'${oldest}'","total":'${total}'},"error":null}'
fi
fi
echo "${response}"
}
removefrombatch() {
trace "Entering removefrombatch()..."
# POST http://192.168.111.152:8080/removefrombatch
#
# args:
# - outputId, required, id of the output to remove
#
# response:
# - batcherId, the id of the batcher
# - outputId, the id of the removed output if found
# - nbOutputs, the number of outputs currently in the batch
# - oldest, the timestamp of the oldest output in the batch
# - total, the current sum of the batch's output amounts
#
# BODY {"id":72}
local request=${1}
local response
local returncode=0
local row
local batcher_id
local id=$(echo "${request}" | jq ".outputId")
trace "[removefrombatch] id=${id}"
if [ "${id}" = "null" ]; then
# id is required
trace "[removefrombatch] id missing"
response='{"result":null,"error":{"code":-32700,"message":"outputId is required","data":'${request}'}}'
else
batcher_id=$(sql "SELECT batcher_id FROM recipient WHERE id=${id}")
returncode=$?
trace_rc ${returncode}
if [ -n "${batcher_id}" ]; then
sql "DELETE FROM recipient WHERE id=${id}"
returncode=$?
trace_rc ${returncode}
if [ "${returncode}" -ne 0 ]; then
response='{"result":null,"error":{"code":-32700,"message":"Output was not removed","data":'${request}'}}'
else
row=$(sql "SELECT COUNT(id), COALESCE(MIN(inserted_ts), 0), COALESCE(SUM(amount), 0.00000000) FROM recipient WHERE tx_id IS NULL AND batcher_id=${batcher_id}")
returncode=$?
trace_rc ${returncode}
local count=$(echo "${row}" | cut -d '|' -f1)
trace "[removefrombatch] count=${count}"
local oldest=$(echo "${row}" | cut -d '|' -f2)
trace "[removefrombatch] oldest=${oldest}"
local total=$(echo "${row}" | cut -d '|' -f3)
trace "[removefrombatch] total=${total}"
response='{"result":{"batcherId":'${batcher_id}',"outputId":'${id}',"nbOutputs":'${count}',"oldest":"'${oldest}'","total":'${total}'},"error":null}'
fi
else
response='{"result":null,"error":{"code":-32700,"message":"Output not found or already spent","data":'${request}'}}'
fi
fi
echo "${response}"
}
batchspend() {
trace "Entering batchspend()..."
# POST http://192.168.111.152:8080/batchspend
#
# args:
# - batcherId, optional, id of the batcher to execute, overrides batcherLabel, default batcher will be spent if not supplied
# - batcherLabel, optional, label of the batcher to execute, default batcher will be executed if not supplied
# - confTarget, optional, overrides default value of createbatcher, default to value of createbatcher, default Bitcoin Core conf_target will be used if not supplied
# NOTYET - feeRate, optional, overrides confTarget if supplied, overrides default value of createbatcher, default to value of createbatcher, default Bitcoin Core value will be used if not supplied
#
# response:
# - batcherId, id of the executed batcher
# - confTarget, conf_target used for the spend
# - nbOutputs, the number of outputs spent in the batch
# - oldest, the timestamp of the oldest output in the spent batch
# - total, the sum of the spent batch's output amounts
# - txid, the batch transaction id
# - hash, the transaction hash
# - tx details: firstseen, size, vsize, replaceable, fee
# - outputs
#
# BODY {}
# BODY {"batcherId":"34","confTarget":12}
# NOTYET BODY {"batcherLabel":"highfees","feeRate":233.7}
# BODY {"batcherId":"411","confTarget":6}
local request=${1}
local response
local returncode=0
local row
local whereclause
local batcher_id=$(echo "${request}" | jq ".batcherId")
trace "[batchspend] batcher_id=${batcher_id}"
local batcher_label=$(echo "${request}" | jq ".batcherLabel")
trace "[batchspend] batcher_label=${batcher_label}"
local conf_target=$(echo "${request}" | jq ".confTarget")
trace "[batchspend] conf_target=${conf_target}"
local feerate=$(echo "${request}" | jq ".feeRate")
trace "[batchspend] feerate=${feerate}"
if [ "${batcher_id}" = "null" ] && [ "${batcher_label}" = "null" ]; then
# If batcher_id and batcher_label are null, use default batcher
trace "[batchspend] Using default batcher 1"
batcher_id=1
fi
if [ "${batcher_id}" = "null" ]; then
# Using batcher_label
whereclause="label=${batcher_label}"
else
whereclause="id=${batcher_id}"
fi
local batcher=$(sql "SELECT id, conf_target, feerate FROM batcher WHERE ${whereclause}")
returncode=$?
trace_rc ${returncode}
if [ -z "${batcher}" ]; then
# batcherLabel not found
response='{"result":null,"error":{"code":-32700,"message":"batcher not found","data":'${request}'}}'
else
# All good, let's try to batch spend!
# NOTYET
# We'll use supplied feerate
# If not supplied, we'll use supplied conf_target
# If not supplied, we'll use batcher default feerate
# If not set, we'll use batcher default conf_target
# If not set, default Bitcoin Core fee policy will be used
# We'll use the supplied conf_target
# If not supplied, we'll use the batcher default conf_target
# If not set, default Bitcoin Core fee policy will be used
# if [ "${feerate}" != "null" ]; then
# # If not null, let's nullify conf_target since feerate overrides it
# conf_target=
# trace "[batchspend] Overriding conf_target=${conf_target}"
# else
# if [ "${conf_target}" = "null" ]; then
# feerate=$(echo "${batcher}" | cut -d '|' -f3)
# if [ -z "${feerate}" ]; then
# # If null, let's use batcher conf_target
# conf_target=$(echo "${batcher}" | cut -d '|' -f2)
# fi
# fi
# fi
if [ "${conf_target}" = "null" ]; then
conf_target=$(echo "${batcher}" | cut -d '|' -f2)
trace "[batchspend] Using batcher default conf_target=${conf_target}"
fi
batcher_id=$(echo "${batcher}" | cut -d '|' -f1)
local batching=$(sql "SELECT address, amount, id, webhook_url FROM recipient WHERE tx_id IS NULL AND batcher_id=${batcher_id}")
trace "[batchspend] batching=${batching}"
local data
local recipientsjson
local webhooks_data
local id_inserted
local tx_details
local tx_raw_details
local address
local amount
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}"
recipient_id=$(echo "${row}" | cut -d '|' -f3)
trace "[batchspend] recipient_id=${recipient_id}"
webhook_url=$(echo "${row}" | cut -d '|' -f4)
trace "[batchspend] webhook_url=${webhook_url}"
if [ -z "${recipientsjson}" ]; then
whereclause="\"${recipient_id}\""
recipientsjson="\"${address}\":${amount}"
webhooks_data="{\"outputId\":${recipient_id},\"address\":\"${address}\",\"amount\":${amount},\"webhookUrl\":\"${webhook_url}\"}"
else
whereclause="${whereclause},\"${recipient_id}\""
recipientsjson="${recipientsjson},\"${address}\":${amount}"
webhooks_data="${webhooks_data},{\"outputId\":${recipient_id},\"address\":\"${address}\",\"amount\":${amount},\"webhookUrl\":\"${webhook_url}\"}"
fi
done
local bitcoincore_args="{\"method\":\"sendmany\",\"params\":[\"\", {${recipientsjson}}"
if [ -n "${conf_target}" ]; then
bitcoincore_args="${bitcoincore_args}, 1, \"\", null, null, ${conf_target}"
fi
bitcoincore_args="${bitcoincore_args}]}"
data=$(send_to_spender_node "${bitcoincore_args}")
returncode=$?
trace_rc ${returncode}
trace "[batchspend] data=${data}"
if [ "${returncode}" -eq 0 ]; then
local txid=$(echo "${data}" | jq -r ".result")
trace "[batchspend] txid=${txid}"
# Let's get transaction details on the spending wallet so that we have fee information
tx_details=$(get_transaction ${txid} "spender")
tx_raw_details=$(get_rawtransaction ${txid} | tr -d '\n')
# Amounts and fees are negative when spending so we absolute those fields
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 | fabs' | awk '{ printf "%.8f", $0 }')
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 -r '.result."bip125-replaceable"')
trace "[batchspend] tx_replaceable=${tx_replaceable}"
tx_replaceable=$([ "${tx_replaceable}" = "yes" ] && echo "true" || echo "false")
trace "[batchspend] tx_replaceable=${tx_replaceable}"
local fees=$(echo "${tx_details}" | jq '.result.fee | fabs' | awk '{ printf "%.8f", $0 }')
# 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}" > batchspend-rawtx-${txid}-$$.blob
# Get the info on the batch before setting it to done
row=$(sql "SELECT COUNT(id), COALESCE(MIN(inserted_ts), 0), COALESCE(SUM(amount), 0.00000000) FROM recipient WHERE tx_id IS NULL AND batcher_id=${batcher_id}")
# Let's insert the txid in our little DB -- then we'll already have it when receiving confirmation
id_inserted=$(sql "INSERT OR IGNORE INTO tx (txid, hash, confirmations, timereceived, fee, size, vsize, is_replaceable, conf_target, raw_tx) VALUES (\"${txid}\", ${tx_hash}, 0, ${tx_ts_firstseen}, ${fees}, ${tx_size}, ${tx_vsize}, ${tx_replaceable}, ${conf_target}, readfile('batchspend-rawtx-${txid}-$$.blob')); SELECT LAST_INSERT_ROWID();")
returncode=$?
trace_rc ${returncode}
if [ "${returncode}" -eq 0 ]; then
if [ "${id_inserted}" -eq 0 ]; then
id_inserted=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"")
fi
trace "[batchspend] id_inserted: ${id_inserted}"
sql "UPDATE recipient SET tx_id=${id_inserted} WHERE id IN (${whereclause})"
trace_rc $?
fi
# Use the selected row above (before the insert)
local count=$(echo "${row}" | cut -d '|' -f1)
trace "[batchspend] count=${count}"
local oldest=$(echo "${row}" | cut -d '|' -f2)
trace "[batchspend] oldest=${oldest}"
local total=$(echo "${row}" | cut -d '|' -f3)
trace "[batchspend] total=${total}"
response='{"result":{"batcherId":'${batcher_id}',"confTarget":'${conf_target}',"nbOutputs":'${count}',"oldest":"'${oldest}'","total":'${total}
response="${response},\"status\":\"accepted\",\"txid\":\"${txid}\",\"hash\":${tx_hash},\"details\":{\"firstseen\":${tx_ts_firstseen},\"size\":${tx_size},\"vsize\":${tx_vsize},\"replaceable\":${tx_replaceable},\"fee\":${fees}},\"outputs\":[${webhooks_data}]}"
response="${response},\"error\":null}"
# Delete the temp file containing the raw tx (see above)
rm batchspend-rawtx-${txid}-$$.blob
batch_webhooks "[${webhooks_data}]" '"batcherId":'${batcher_id}',"confTarget":'${conf_target}',"nbOutputs":'${count}',"oldest":"'${oldest}'","total":'${total}',"status":"accepted","txid":"'${txid}'","hash":'${tx_hash}',"details":{"firstseen":'${tx_ts_firstseen}',"size":'${tx_size}',"vsize":'${tx_vsize}',"replaceable":'${tx_replaceable}',"fee":'${fees}'}'
else
local message=$(echo "${data}" | jq -e ".error.message")
response='{"result":null,"error":{"code":-32700,"message":'${message}',"data":'${request}'}}'
fi
fi
trace "[batchspend] responding=${response}"
echo "${response}"
}
batch_check_webhooks() {
trace "Entering batch_check_webhooks()..."
local webhooks_data
local address
local amount
local recipient_id
local webhook_url
local batcher_id
local txid
local tx_hash
local tx_ts_firstseen
local tx_size
local tx_vsize
local tx_replaceable
local fees
local conf_target
local row
local count
local oldest
local total
local tx_id
local batching=$(sql "SELECT address, amount, r.id, webhook_url, b.id, t.txid, t.hash, t.timereceived, t.fee, t.size, t.vsize, t.is_replaceable, t.conf_target, t.id FROM recipient r, batcher b, tx t WHERE r.batcher_id=b.id AND r.tx_id=t.id AND NOT calledback AND tx_id IS NOT NULL AND webhook_url IS NOT NULL")
trace "[batch_check_webhooks] batching=${batching}"
local IFS=$'\n'
for row in ${batching}
do
trace "[batch_check_webhooks] row=${row}"
address=$(echo "${row}" | cut -d '|' -f1)
trace "[batch_check_webhooks] address=${address}"
amount=$(echo "${row}" | cut -d '|' -f2)
trace "[batch_check_webhooks] amount=${amount}"
recipient_id=$(echo "${row}" | cut -d '|' -f3)
trace "[batch_check_webhooks] recipient_id=${recipient_id}"
webhook_url=$(echo "${row}" | cut -d '|' -f4)
trace "[batch_check_webhooks] webhook_url=${webhook_url}"
batcher_id=$(echo "${row}" | cut -d '|' -f5)
trace "[batch_check_webhooks] batcher_id=${batcher_id}"
txid=$(echo "${row}" | cut -d '|' -f6)
trace "[batch_check_webhooks] txid=${txid}"
tx_hash=$(echo "${row}" | cut -d '|' -f7)
trace "[batch_check_webhooks] tx_hash=${tx_hash}"
tx_ts_firstseen=$(echo "${row}" | cut -d '|' -f8)
trace "[batch_check_webhooks] tx_ts_firstseen=${tx_ts_firstseen}"
fees=$(echo "${row}" | cut -d '|' -f9)
trace "[batch_check_webhooks] fees=${fees}"
tx_size=$(echo "${row}" | cut -d '|' -f10)
trace "[batch_check_webhooks] tx_size=${tx_size}"
tx_vsize=$(echo "${row}" | cut -d '|' -f11)
trace "[batch_check_webhooks] tx_vsize=${tx_vsize}"
tx_replaceable=$(echo "${row}" | cut -d '|' -f12)
tx_replaceable=$([ "${tx_replaceable}" -eq "1" ] && echo "true" || echo "false")
trace "[batch_check_webhooks] tx_replaceable=${tx_replaceable}"
conf_target=$(echo "${row}" | cut -d '|' -f13)
trace "[batch_check_webhooks] conf_target=${conf_target}"
tx_id=$(echo "${row}" | cut -d '|' -f14)
trace "[batch_check_webhooks] tx_id=${tx_id}"
webhooks_data="{\"outputId\":${recipient_id},\"address\":\"${address}\",\"amount\":${amount},\"webhookUrl\":\"${webhook_url}\"}"
# I know this query for each output is not very efficient, but this function should not execute often, only in case of
# failed callbacks on batches...
# Get the info on the batch
row=$(sql "SELECT COUNT(id), COALESCE(MIN(inserted_ts), 0), COALESCE(SUM(amount), 0.00000000) FROM recipient r WHERE tx_id=\"${tx_id}\"")
# Use the selected row above
count=$(echo "${row}" | cut -d '|' -f1)
trace "[batchspend] count=${count}"
oldest=$(echo "${row}" | cut -d '|' -f2)
trace "[batchspend] oldest=${oldest}"
total=$(echo "${row}" | cut -d '|' -f3)
trace "[batchspend] total=${total}"
batch_webhooks "[${webhooks_data}]" '"batcherId":'${batcher_id}',"confTarget":'${conf_target}',"nbOutputs":'${count}',"oldest":"'${oldest}'","total":'${total}',"status":"accepted","txid":"'${txid}'","hash":"'${tx_hash}'","details":{"firstseen":'${tx_ts_firstseen}',"size":'${tx_size}',"vsize":'${tx_vsize}',"replaceable":'${tx_replaceable}',"fee":'${fees}'}'
done
}
batch_webhooks() {
trace "Entering batch_webhooks()..."
# webhooks_data:
# {"outputId":1,"address":"1abc","amount":0.12,"webhookUrl":"https://bleah.com/batchwebhook"}"
local webhooks_data=${1}
trace "[batch_webhooks] webhooks_data=${webhooks_data}"
# tx:
# {"batcherId":1,"txid":"abc123","hash":"abc123","details":{"firstseen":123123,"size":200,"vsize":141,"replaceable":true,"fee":0.00001}}'
local tx=${2}
trace "[batch_webhooks] tx=${tx}"
local outputs
local output_id
local address
local amount
local webhook_url
local body
local successful_recipient_ids
local returncode
outputs=$(echo "${webhooks_data}" | jq -Mc ".[]")
local output
local IFS=$'\n'
for output in ${outputs}
do
webhook_url=$(echo "${output}" | jq -r ".webhookUrl")
trace "[batch_webhooks] webhook_url=${webhook_url}"
if [ -z "${webhook_url}" ] || [ "${webhook_url}" = "null" ]; then
trace "[batch_webhooks] Empty webhook_url, skipping"
continue
fi
output_id=$(echo "${output}" | jq ".outputId")
trace "[batch_webhooks] output_id=${output_id}"
address=$(echo "${output}" | jq ".address")
trace "[batch_webhooks] address=${address}"
amount=$(echo "${output}" | jq ".amount")
trace "[batch_webhooks] amount=${amount}"
body='{"outputId":'${output_id}',"address":'${address}',"amount":'${amount}','${tx}'}'
trace "[batch_webhooks] body=${body}"
notify_web "${webhook_url}" "${body}" ${TOR_ADDR_WATCH_WEBHOOKS}
returncode=$?
trace_rc ${returncode}
if [ "${returncode}" -eq 0 ]; then
if [ -n "${successful_recipient_ids}" ]; then
successful_recipient_ids="${successful_recipient_ids},${output_id}"
else
successful_recipient_ids="${output_id}"
fi
else
trace "[batch_webhooks] callback failed, won't set to true in DB"
fi
done
sql "UPDATE recipient SET calledback=1, calledback_ts=CURRENT_TIMESTAMP WHERE id IN (${successful_recipient_ids})"
trace_rc $?
}
listbatchers() {
trace "Entering listbatchers()..."
# curl (GET) http://192.168.111.152:8080/listbatchers
#
# {"result":[
# {"batcherId":1,"batcherLabel":"default","confTarget":6,"nbOutputs":12,"oldest":123123,"total":0.86990143},
# {"batcherId":2,"batcherLabel":"lowfee","confTarget":32,"nbOutputs":44,"oldest":123123,"total":0.49827387},
# {"batcherId":3,"batcherLabel":"highfee","confTarget":2,"nbOutputs":7,"oldest":123123,"total":4.16843782}
# ],
# "error":null}
local batchers=$(sql "SELECT b.id, '{\"batcherId\":' || b.id || ',\"batcherLabel\":\"' || b.label || '\",\"confTarget\":' || conf_target || ',\"nbOutputs\":' || COUNT(r.id) || ',\"oldest\":\"' ||COALESCE(MIN(r.inserted_ts), 0) || '\",\"total\":' ||COALESCE(SUM(amount), 0.00000000) || '}' FROM batcher b LEFT JOIN recipient r ON r.batcher_id=b.id AND r.tx_id IS NULL GROUP BY b.id")
trace "[listbatchers] batchers=${batchers}"
local returncode
local response
local batcher
local jsonstring
local IFS=$'\n'
for batcher in ${batchers}
do
jsonstring=$(echo ${batcher} | cut -d '|' -f2)
if [ -z "${response}" ]; then
response='{"result":['${jsonstring}
else
response="${response},${jsonstring}"
fi
done
response=${response}'],"error":null}'
trace "[listbatchers] responding=${response}"
echo "${response}"
}
getbatcher() {
trace "Entering getbatcher()..."
# POST (GET) http://192.168.111.152:8080/getbatcher
#
# args:
# - batcherId, optional, id of the batcher, overrides batcerhLabel, default batcher will be used if not supplied
# - batcherLabel, optional, label of the batcher, default batcher will be used if not supplied
#
# response:
# {"result":{"batcherId":1,"batcherLabel":"default","confTarget":6,"nbOutputs":12,"oldest":123123,"total":0.86990143},"error":null}
#
# BODY {}
# BODY {"batcherId":34}
local request=${1}
local response
local returncode=0
local batcher
local whereclause
local batcher_id=$(echo "${request}" | jq ".batcherId")
trace "[getbatcher] batcher_id=${batcher_id}"
local batcher_label=$(echo "${request}" | jq ".batcherLabel")
trace "[getbatcher] batcher_label=${batcher_label}"
if [ "${batcher_id}" = "null" ] && [ "${batcher_label}" = "null" ]; then
# If batcher_id and batcher_label are null, use default batcher
trace "[getbatcher] Using default batcher 1"
batcher_id=1
fi
if [ "${batcher_id}" = "null" ]; then
# Using batcher_label
whereclause="b.label=${batcher_label}"
else
# Using batcher_id
whereclause="b.id=${batcher_id}"
fi
batcher=$(sql "SELECT b.id, '{\"batcherId\":' || b.id || ',\"batcherLabel\":\"' || b.label || '\",\"confTarget\":' || conf_target || ',\"nbOutputs\":' || COUNT(r.id) || ',\"oldest\":\"' ||COALESCE(MIN(r.inserted_ts), 0) || '\",\"total\":' ||COALESCE(SUM(amount), 0.00000000) || '}' FROM batcher b LEFT JOIN recipient r ON r.batcher_id=b.id AND r.tx_id IS NULL WHERE ${whereclause} GROUP BY b.id")
trace "[getbatcher] batcher=${batcher}"
if [ -n "${batcher}" ]; then
batcher=$(echo "${batcher}" | cut -d '|' -f2)
response='{"result":'${batcher}',"error":null}'
else
response='{"result":null,"error":{"code":-32700,"message":"batcher not found","data":'${request}'}}'
fi
echo "${response}"
}
getbatchdetails() {
trace "Entering getbatchdetails()..."
# POST (GET) http://192.168.111.152:8080/getbatchdetails
#
# args:
# - batcherId, optional, id of the batcher, overrides batcherLabel, default batcher will be used if not supplied
# - batcherLabel, optional, label of the batcher, default batcher will be used if not supplied
# - txid, optional, if you want the details of an executed batch, supply the batch txid, will return current pending batch
# if not supplied
#
# response:
# {"result":{
# "batcherId":34,
# "batcherLabel":"Special batcher for a special client",
# "confTarget":6,
# "nbOutputs":83,
# "oldest":123123,
# "total":10.86990143,
# "txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648",
# "hash":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648",
# "details":{
# "firstseen":123123,
# "size":424,
# "vsize":371,
# "replaceable":yes,
# "fee":0.00004112
# },
# "outputs":[
# "1abc":0.12,
# "3abc":0.66,
# "bc1abc":2.848,
# ...
# ]
# }
# },"error":null}
#
# BODY {}
# BODY {"batcherId":34}
local request=${1}
local response
local returncode=0
local batch
local tx
local outputsjson
local whereclause
local batcher_id=$(echo "${request}" | jq ".batcherId")
trace "[getbatchdetails] batcher_id=${batcher_id}"
local batcher_label=$(echo "${request}" | jq ".batcherLabel")
trace "[getbatchdetails] batcher_label=${batcher_label}"
local txid=$(echo "${request}" | jq ".txid")
trace "[getbatchdetails] txid=${txid}"
if [ "${batcher_id}" = "null" ] && [ "${batcher_label}" = "null" ]; then
# If batcher_id and batcher_label are null, use default batcher
trace "[getbatchdetails] Using default batcher 1"
batcher_id=1
fi
if [ "${batcher_id}" = "null" ]; then
# Using batcher_label
whereclause="b.label=${batcher_label}"
else
# Using batcher_id
whereclause="b.id=${batcher_id}"
fi
if [ "${txid}" != "null" ]; then
# Using txid
whereclause="${whereclause} AND t.txid=${txid}"
else
# null txid
whereclause="${whereclause} AND t.txid IS NULL"
outerclause="AND r.tx_id IS NULL"
fi
# First get the batch summary
batch=$(sql "SELECT b.id, COALESCE(t.id, NULL), '{\"batcherId\":' || b.id || ',\"batcherLabel\":\"' || b.label || '\",\"confTarget\":' || b.conf_target || ',\"nbOutputs\":' || COUNT(r.id) || ',\"oldest\":\"' ||COALESCE(MIN(r.inserted_ts), 0) || '\",\"total\":' ||COALESCE(SUM(amount), 0.00000000) FROM batcher b LEFT JOIN recipient r ON r.batcher_id=b.id ${outerclause} LEFT JOIN tx t ON t.id=r.tx_id WHERE ${whereclause} GROUP BY b.id")
trace "[getbatchdetails] batch=${batch}"
if [ -n "${batch}" ]; then
local tx_id
local outputs
tx_id=$(echo "${batch}" | cut -d '|' -f2)
trace "[getbatchdetails] tx_id=${tx_id}"
if [ -n "${tx_id}" ]; then
# Using txid
outerclause="AND r.tx_id=${tx_id}"
tx=$(sql "SELECT '\"txid\":\"' || txid || '\",\"hash\":\"' || hash || '\",\"details\":{\"firstseen\":' || timereceived || ',\"size\":' || size || ',\"vsize\":' || vsize || ',\"replaceable\":' || CASE is_replaceable WHEN 1 THEN 'true' ELSE 'false' END || ',\"fee\":' || fee || '}' FROM tx WHERE id=${tx_id}")
else
# null txid
outerclause="AND r.tx_id IS NULL"
fi
batcher_id=$(echo "${batch}" | cut -d '|' -f1)
outputs=$(sql "SELECT '{\"outputId\":' || id || ',\"outputLabel\":\"' || COALESCE(label, '') || '\",\"address\":\"' || address || '\",\"amount\":' || amount || ',\"addedTimestamp\":\"' || inserted_ts || '\"}' FROM recipient r WHERE batcher_id=${batcher_id} ${outerclause}")
local output
local IFS=$'\n'
for output in ${outputs}
do
if [ -n "${outputsjson}" ]; then
outputsjson="${outputsjson},${output}"
else
outputsjson="${output}"
fi
done
batch=$(echo "${batch}" | cut -d '|' -f3)
response='{"result":'${batch}
if [ -n "${tx}" ]; then
response=${response}','${tx}
else
response=${response}',"txid":null,"hash":null'
fi
response=${response}',"outputs":['${outputsjson}']},"error":null}'
else
response='{"result":null,"error":{"code":-32700,"message":"batch not found or no corresponding txid","data":'${request}'}}'
fi
echo "${response}"
}
# curl localhost:8888/listbatchers | jq
# curl -d '{}' localhost:8888/getbatcher | jq
# curl -d '{}' localhost:8888/getbatchdetails | jq
# curl -d '{"outputLabel":"test002","address":"1abd","amount":0.0002}' localhost:8888/addtobatch | jq
# curl -d '{}' localhost:8888/batchspend | jq
# curl -d '{"outputId":1}' localhost:8888/removefrombatch | jq
# curl -d '{"batcherLabel":"lowfees","confTarget":32}' localhost:8888/createbatcher | jq
# curl localhost:8888/listbatchers | jq
# curl -d '{"batcherLabel":"lowfees"}' localhost:8888/getbatcher | jq
# curl -d '{"batcherLabel":"lowfees"}' localhost:8888/getbatchdetails | jq
# curl -d '{"batcherLabel":"lowfees","outputLabel":"test002","address":"1abd","amount":0.0002}' localhost:8888/addtobatch | jq
# curl -d '{"batcherLabel":"lowfees"}' localhost:8888/batchspend | jq
# curl -d '{"batcherLabel":"lowfees","outputId":9}' localhost:8888/removefrombatch | jq

View File

@@ -103,3 +103,14 @@ validateaddress() {
send_to_watcher_node "${data}"
return $?
}
bitcoin_estimatesmartfee() {
trace "Entering bitcoin_estimatesmartfee()..."
local conf_target=${1}
trace "[bitcoin_estimatesmartfee] conf_target=${conf_target}"
local data="{\"method\":\"estimatesmartfee\",\"params\":[${conf_target}]}"
trace "[bitcoin_estimatesmartfee] data=${data}"
send_to_watcher_node "${data}"
return $?
}

View File

@@ -11,7 +11,7 @@ do_callbacks() {
trace "Entering do_callbacks()..."
# Let's fetch all the watching addresses still being watched but not called back
local callbacks=$(sql 'SELECT DISTINCT w.callback0conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id, is_replaceable, pub32_index, pub32, label, derivation_path, event_message FROM watching w LEFT JOIN watching_tx ON w.id = watching_id LEFT JOIN tx ON tx.id = tx_id LEFT JOIN watching_by_pub32 w32 ON watching_by_pub32_id = w32.id WHERE NOT calledback0conf AND watching_id NOT NULL AND w.callback0conf NOT NULL AND w.watching')
local callbacks=$(sql 'SELECT DISTINCT w.callback0conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id, is_replaceable, pub32_index, pub32, label, derivation_path, event_message, hash FROM watching w LEFT JOIN watching_tx ON w.id = watching_id LEFT JOIN tx ON tx.id = tx_id LEFT JOIN watching_by_pub32 w32 ON watching_by_pub32_id = w32.id WHERE NOT calledback0conf AND watching_id NOT NULL AND w.callback0conf NOT NULL AND w.watching')
trace "[do_callbacks] callbacks0conf=${callbacks}"
local returncode
@@ -30,7 +30,7 @@ do_callbacks() {
fi
done
callbacks=$(sql 'SELECT DISTINCT w.callback1conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id, is_replaceable, pub32_index, pub32, label, derivation_path, event_message FROM watching w, watching_tx wt, tx t LEFT JOIN watching_by_pub32 w32 ON watching_by_pub32_id = w32.id WHERE w.id = watching_id AND tx_id = t.id AND NOT calledback1conf AND confirmations>0 AND w.callback1conf NOT NULL AND w.watching')
callbacks=$(sql 'SELECT DISTINCT w.callback1conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime, w.id, is_replaceable, pub32_index, pub32, label, derivation_path, event_message, hash FROM watching w, watching_tx wt, tx t LEFT JOIN watching_by_pub32 w32 ON watching_by_pub32_id = w32.id WHERE w.id = watching_id AND tx_id = t.id AND NOT calledback1conf AND confirmations>0 AND w.callback1conf NOT NULL AND w.watching')
trace "[do_callbacks] callbacks1conf=${callbacks}"
for row in ${callbacks}
@@ -45,7 +45,7 @@ do_callbacks() {
done
callbacks=$(sql "SELECT id, label, bolt11, callback_url, payment_hash, msatoshi, status, pay_index, msatoshi_received, paid_at, description, expires_at FROM ln_invoice WHERE NOT calledback AND callback_failed")
trace "[do_callbacks LN] ln_callbacks=${callbacks}"
trace "[do_callbacks] ln_callbacks=${callbacks}"
for row in ${callbacks}
do
@@ -152,6 +152,7 @@ build_callback() {
local derivation_path
local event_message
local hash
# w.callback0conf, address, txid, vout, amount, confirmations, timereceived, fee, size, vsize, blockhash, blockheight, blocktime,
# w.id, is_replaceable, pub32_index, pub32, label, derivation_path, event_message
@@ -171,6 +172,8 @@ build_callback() {
trace "[build_callback] address=${address}"
txid=$(echo "${row}" | cut -d '|' -f3)
trace "[build_callback] txid=${txid}"
hash=$(echo "${row}" | cut -d '|' -f21)
trace "[build_callback] hash=${hash}"
vout_n=$(echo "${row}" | cut -d '|' -f4)
trace "[build_callback] vout_n=${vout_n}"
sent_amount=$(echo "${row}" | cut -d '|' -f5 | awk '{ printf "%.8f", $0 }')
@@ -192,6 +195,7 @@ build_callback() {
vsize=$(echo "${row}" | cut -d '|' -f10)
trace "[build_callback] vsize=${vsize}"
is_replaceable=$(echo "${row}" | cut -d '|' -f15)
is_replaceable=$([ "${is_replaceable}" -eq "1" ] && echo "true" || echo "false")
trace "[build_callback] is_replaceable=${is_replaceable}"
blockhash=$(echo "${row}" | cut -d '|' -f11)
trace "[build_callback] blockhash=${blockhash}"
@@ -215,7 +219,8 @@ build_callback() {
data="{\"id\":\"${id}\","
data="${data}\"address\":\"${address}\","
data="${data}\"hash\":\"${txid}\","
data="${data}\"txid\":\"${txid}\","
data="${data}\"hash\":\"${hash}\","
data="${data}\"vout_n\":${vout_n},"
data="${data}\"sent_amount\":${sent_amount},"
data="${data}\"confirmations\":${confirmations},"
@@ -225,7 +230,7 @@ build_callback() {
if [ -n "${fee}" ]; then
data="${data}\"fees\":${fee},"
fi
data="${data}\"is_replaceable\":${is_replaceable},"
data="${data}\"replaceable\":${is_replaceable},"
if [ -n "${blocktime}" ]; then
data="${data}\"blockhash\":\"${blockhash}\","
data="${data}\"blocktime\":\"$(date -Is -d @${blocktime})\","

View File

@@ -23,7 +23,7 @@ do_callbacks_txid() {
build_callback_txid ${row}
returncode=$?
trace_rc ${returncode}
if [ "${returncode}" -eq 0 ]; then
if [ "${returncode}" -eq "0" ]; then
id=$(echo "${row}" | cut -d '|' -f1)
sql "UPDATE watching_by_txid SET calledback1conf=1 WHERE id=\"${id}\""
trace_rc $?
@@ -39,7 +39,8 @@ do_callbacks_txid() {
do
build_callback_txid ${row}
returncode=$?
if [ "${returncode}" -eq 0 ]; then
trace_rc ${returncode}
if [ "${returncode}" -eq "0" ]; then
id=$(echo "${row}" | cut -d '|' -f1)
sql "UPDATE watching_by_txid SET calledbackxconf=1, watching=0 WHERE id=\"${id}\""
trace_rc $?
@@ -136,6 +137,9 @@ build_callback_txid() {
trace "[build_callback_txid] Number of confirmations for tx is not enough to call back."
return 1
fi
else
trace "[build_callback_txid] Couldn't get tx from the Bitcoin node."
return 1
fi
}

View File

@@ -68,7 +68,7 @@ compute_vin_total_amount()
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}")
vin_raw_tx=$(get_rawtransaction "${vin_txid}" | tr -d '\n')
returncode=$?
if [ "${returncode}" -ne 0 ]; then
return ${returncode}

View File

@@ -66,8 +66,9 @@ confirmation() {
local tx=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"")
local id_inserted
local tx_raw_details=$(get_rawtransaction ${txid})
local tx_raw_details=$(get_rawtransaction ${txid} | tr -d '\n')
local tx_nb_conf=$(echo "${tx_details}" | jq -r '.result.confirmations // 0')
local tx_hash=$(echo "${tx_raw_details}" | jq '.result.hash')
# 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
@@ -80,14 +81,13 @@ confirmation() {
# 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 tx_replaceable=$(echo "${tx_details}" | jq -r '.result."bip125-replaceable"')
tx_replaceable=$([ ${tx_replaceable} = "yes" ] && echo "true" || echo "false")
local fees=$(compute_fees "${txid}")
trace "[confirmation] fees=${fees}"
@@ -184,8 +184,8 @@ confirmation() {
if [ -n "${event_message}" ]; then
# There's an event message, let's publish it!
trace "[confirmation] mosquitto_pub -h broker -t tx_confirmation -m \"{\"txid\":\"${txid}\",\"address\":\"${address}\",\"vout_n\":${tx_vout_n},\"amount\":${tx_vout_amount},\"confirmations\":${tx_nb_conf},\"eventMessage\":\"${event_message}\"}\""
response=$(mosquitto_pub -h broker -t tx_confirmation -m "{\"txid\":\"${txid}\",\"address\":\"${address}\",\"vout_n\":${tx_vout_n},\"amount\":${tx_vout_amount},\"confirmations\":${tx_nb_conf},\"eventMessage\":\"${event_message}\"}")
trace "[confirmation] mosquitto_pub -h broker -t tx_confirmation -m \"{\"txid\":\"${txid}\",\"hash\":${tx_hash},\"address\":\"${address}\",\"vout_n\":${tx_vout_n},\"amount\":${tx_vout_amount},\"confirmations\":${tx_nb_conf},\"eventMessage\":\"${event_message}\"}\""
response=$(mosquitto_pub -h broker -t tx_confirmation -m "{\"txid\":\"${txid}\",\"hash\":${tx_hash},\"address\":\"${address}\",\"vout_n\":${tx_vout_n},\"amount\":${tx_vout_amount},\"confirmations\":${tx_nb_conf},\"eventMessage\":\"${event_message}\"}")
returncode=$?
trace_rc ${returncode}
fi

View File

@@ -3,6 +3,7 @@
. ./trace.sh
. ./callbacks_txid.sh
. ./blockchainrpc.sh
. ./batching.sh
newblock() {
trace "Entering newblock()..."
@@ -22,4 +23,5 @@ newblock() {
trace_rc ${returncode}
do_callbacks_txid
batch_check_webhooks
}

View File

@@ -20,6 +20,7 @@
. ./call_lightningd.sh
. ./ots.sh
. ./newblock.sh
. ./batching.sh
main() {
trace "Entering main()..."
@@ -99,8 +100,35 @@ main() {
;;
unwatch)
# curl (GET) 192.168.111.152:8080/unwatch/2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp
# or
# POST http://192.168.111.152:8080/unwatch
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","unconfirmedCallbackURL":"192.168.111.233:1111/callback0conf","confirmedCallbackURL":"192.168.111.233:1111/callback1conf"}
# or
# BODY {"id":3124}
response=$(unwatchrequest "${line}")
# args:
# - address: string, required
# - unconfirmedCallbackURL: string, optional
# - confirmedCallbackURL: string, optional
# or
# - id: the id returned by the watch
local address="null"
local unconfirmedCallbackURL="null"
local confirmedCallbackURL="null"
local watchid="null"
# Let's make it work even for a GET request (equivalent to a POST with empty json object body)
if [ "$http_method" = "POST" ]; then
address=$(echo "${line}" | jq -r ".address")
unconfirmedCallbackURL=$(echo "${line}" | jq ".unconfirmedCallbackURL")
confirmedCallbackURL=$(echo "${line}" | jq ".confirmedCallbackURL")
watchid=$(echo "${line}" | jq ".id")
else
address=$(echo "${line}" | cut -d ' ' -f2 | cut -d '/' -f3)
fi
response=$(unwatchrequest "${watchid}" "${address}" "${unconfirmedCallbackURL}" "${confirmedCallbackURL}")
response_to_client "${response}" ${?}
break
;;
@@ -157,6 +185,28 @@ main() {
response_to_client "${response}" ${?}
break
;;
unwatchtxid)
# POST http://192.168.111.152:8080/unwatchtxid
# BODY {"txid":"b081ca7724386f549cf0c16f71db6affeb52ff7a0d9b606fb2e5c43faffd3387","unconfirmedCallbackURL":"192.168.111.233:1111/callback0conf","confirmedCallbackURL":"192.168.111.233:1111/callback1conf"}
# or
# BODY {"id":3124}
# args:
# - txid: string, required
# - unconfirmedCallbackURL: string, optional
# - confirmedCallbackURL: string, optional
# or
# - id: the id returned by watchtxid
local txid=$(echo "${line}" | jq -r ".txid")
local unconfirmedCallbackURL=$(echo "${line}" | jq ".unconfirmedCallbackURL")
local confirmedCallbackURL=$(echo "${line}" | jq ".confirmedCallbackURL")
local watchid=$(echo "${line}" | jq ".id")
response=$(unwatchtxidrequest "${watchid}" "${txid}" "${unconfirmedCallbackURL}" "${confirmedCallbackURL}")
response_to_client "${response}" ${?}
break
;;
getactivewatches)
# curl (GET) 192.168.111.152:8080/getactivewatches
@@ -300,21 +350,229 @@ main() {
response_to_client "${response}" ${?}
break
;;
createbatcher)
# POST http://192.168.111.152:8080/createbatcher
#
# args:
# - batcherLabel, optional, id can be used to reference the batcher
# - confTarget, optional, overriden by batchspend's confTarget, default Bitcoin Core conf_target will be used if not supplied
# NOTYET - feeRate, sat/vB, optional, overrides confTarget if supplied, overriden by batchspend's feeRate, default Bitcoin Core fee policy will be used if not supplied
#
# response:
# - batcherId, the batcher id
#
# BODY {"batcherLabel":"lowfees","confTarget":32}
# NOTYET BODY {"batcherLabel":"highfees","feeRate":231.8}
response=$(createbatcher "${line}")
response_to_client "${response}" ${?}
break
;;
updatebatcher)
# POST http://192.168.111.152:8080/updatebatcher
#
# args:
# - batcherId, optional, batcher id to update, will update default batcher if not supplied
# - batcherLabel, optional, id can be used to reference the batcher, will update default batcher if not supplied, if id is present then change the label with supplied text
# - confTarget, optional, new confirmation target for the batcher
# NOTYET - feeRate, sat/vB, optional, new feerate for the batcher
#
# response:
# - batcherId, the batcher id
# - batcherLabel, the batcher label
# - confTarget, the batcher default confirmation target
# NOTYET - feeRate, the batcher default feerate
#
# BODY {"batcherId":5,"confTarget":12}
# NOTYET BODY {"batcherLabel":"highfees","feeRate":400}
# NOTYET BODY {"batcherId":3,"label":"ultrahighfees","feeRate":800}
# BODY {"batcherLabel":"fast","confTarget":2}
response=$(updatebatcher "${line}")
response_to_client "${response}" ${?}
break
;;
addtobatch)
# POST http://192.168.111.152:8080/addtobatch
#
# args:
# - address, required, desination address
# - amount, required, amount to send to the destination address
# - outputLabel, optional, if you want to reference this output
# - batcherId, optional, the id of the batcher to which the output will be added, default batcher if not supplied, overrides batcherLabel
# - batcherLabel, optional, the label of the batcher to which the output will be added, default batcher if not supplied
# - webhookUrl, optional, the webhook to call when the batch is broadcast
#
# response:
# - batcherId, the id of the batcher
# - outputId, the id of the added output
# - nbOutputs, the number of outputs currently in the batch
# - oldest, the timestamp of the oldest output in the batch
# - total, the current sum of the batch's output amounts
#
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233}
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"batcherId":34,"webhookUrl":"https://myCypherApp:3000/batchExecuted"}
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"batcherLabel":"lowfees","webhookUrl":"https://myCypherApp:3000/batchExecuted"}
# BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233,"batcherId":34,"webhookUrl":"https://myCypherApp:3000/batchExecuted"}
response=$(addtobatching $(echo "${line}" | jq -r ".address") $(echo "${line}" | jq ".amount"))
response=$(addtobatch "${line}")
response_to_client "${response}" ${?}
break
;;
removefrombatch)
# POST http://192.168.111.152:8080/removefrombatch
#
# args:
# - outputId, required, id of the output to remove
#
# response:
# - batcherId, the id of the batcher
# - outputId, the id of the removed output if found
# - nbOutputs, the number of outputs currently in the batch
# - oldest, the timestamp of the oldest output in the batch
# - total, the current sum of the batch's output amounts
#
# BODY {"outputId":72}
response=$(removefrombatch "${line}")
response_to_client "${response}" ${?}
break
;;
batchspend)
# GET http://192.168.111.152:8080/batchspend
# POST http://192.168.111.152:8080/batchspend
#
# args:
# - batcherId, optional, id of the batcher to execute, overrides batcherLabel, default batcher will be spent if not supplied
# - batcherLabel, optional, label of the batcher to execute, default batcher will be executed if not supplied
# - confTarget, optional, overrides default value of createbatcher, default to value of createbatcher, default Bitcoin Core conf_target will be used if not supplied
# NOTYET - feeRate, optional, overrides confTarget if supplied, overrides default value of createbatcher, default to value of createbatcher, default Bitcoin Core value will be used if not supplied
#
# response:
# - batcherId, id of the executed batcher
# - confTarget, conf_target used for the spend
# - nbOutputs, the number of outputs spent in the batch
# - oldest, the timestamp of the oldest output in the spent batch
# - total, the sum of the spent batch's output amounts
# - txid, the batch transaction id
# - hash, the transaction hash
# - tx details: firstseen, size, vsize, replaceable, fee
# - outputs
#
# {"result":{
# "batcherId":34,
# "confTarget":6,
# "nbOutputs":83,
# "oldest":123123,
# "total":10.86990143,
# "txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648",
# "hash":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648",
# "details":{
# "firstseen":123123,
# "size":424,
# "vsize":371,
# "replaceable":yes,
# "fee":0.00004112
# },
# "outputs":{
# "1abc":0.12,
# "3abc":0.66,
# "bc1abc":2.848,
# ...
# }
# }
# },"error":null}
#
# BODY {}
# BODY {"batcherId":34,"confTarget":12}
# NOTYET BODY {"batcherLabel":"highfees","feeRate":233.7}
# BODY {"batcherId":411,"confTarget":6}
response=$(batchspend "${line}")
response_to_client "${response}" ${?}
break
;;
getbatcher)
# POST (GET) http://192.168.111.152:8080/getbatcher
#
# args:
# - batcherId, optional, id of the batcher, overrides batcherLabel, default batcher will be used if not supplied
# - batcherLabel, optional, label of the batcher, default batcher will be used if not supplied
#
# response:
# {"result":{"batcherId":1,"batcherLabel":"default","confTarget":6,"nbOutputs":12,"oldest":123123,"total":0.86990143},"error":null}
#
# BODY {}
# BODY {"batcherId":34}
response=$(getbatcher "${line}")
response_to_client "${response}" ${?}
break
;;
getbatchdetails)
# POST (GET) http://192.168.111.152:8080/getbatchdetails
#
# args:
# - batcherId, optional, id of the batcher, overrides batcherLabel, default batcher will be spent if not supplied
# - batcherLabel, optional, label of the batcher, default batcher will be used if not supplied
# - txid, optional, if you want the details of an executed batch, supply the batch txid, will return current pending batch
# if not supplied
#
# response:
# {"result":{
# "batcherId":34,
# "batcherLabel":"Special batcher for a special client",
# "confTarget":6,
# "nbOutputs":83,
# "oldest":123123,
# "total":10.86990143,
# "txid":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648",
# "hash":"af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648",
# "details":{
# "firstseen":123123,
# "size":424,
# "vsize":371,
# "replaceable":yes,
# "fee":0.00004112
# },
# "outputs":[
# "1abc":0.12,
# "3abc":0.66,
# "bc1abc":2.848,
# ...
# ]
# }
# },"error":null}
#
# BODY {}
# BODY {"batcherId":34}
response=$(getbatchdetails "${line}")
response_to_client "${response}" ${?}
break
;;
listbatchers)
# curl (GET) http://192.168.111.152:8080/listbatchers
#
# response:
# {"result":[
# {"batcherId":1,"batcherLabel":"default","confTarget":6,"nbOutputs":12,"oldest":123123,"total":0.86990143},
# {"batcherId":2,"batcherLabel":"lowfee","confTarget":32,"nbOutputs":44,"oldest":123123,"total":0.49827387},
# {"batcherId":3,"batcherLabel":"highfee","confTarget":2,"nbOutputs":7,"oldest":123123,"total":4.16843782}
# ],
# "error":null}
response=$(listbatchers)
response_to_client "${response}" ${?}
break
;;
bitcoin_estimatesmartfee)
# POST http://192.168.111.152:8080/bitcoin_estimatesmartfee
# BODY {"confTarget":2}
response=$(bitcoin_estimatesmartfee $(echo "${line}" | jq -r ".confTarget"))
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

View File

@@ -0,0 +1,326 @@
#!/bin/sh
# curl localhost:8888/listbatchers | jq
# curl -d '{}' localhost:8888/getbatcher | jq
# curl -d '{}' localhost:8888/getbatchdetails | jq
# curl -d '{"outputLabel":"test002","address":"1abd","amount":0.0002}' localhost:8888/addtobatch | jq
# curl -d '{}' localhost:8888/batchspend | jq
# curl -d '{"outputId":1}' localhost:8888/removefrombatch | jq
# curl -d '{"batcherLabel":"lowfees","confTarget":32}' localhost:8888/createbatcher | jq
# curl localhost:8888/listbatchers | jq
# curl -d '{"batcherLabel":"lowfees"}' localhost:8888/getbatcher | jq
# curl -d '{"batcherLabel":"lowfees"}' localhost:8888/getbatchdetails | jq
# curl -d '{"batcherLabel":"lowfees","outputLabel":"test002","address":"1abd","amount":0.0002}' localhost:8888/addtobatch | jq
# curl -d '{"batcherLabel":"lowfees"}' localhost:8888/batchspend | jq
# curl -d '{"batcherLabel":"lowfees","outputId":9}' localhost:8888/removefrombatch | jq
testbatching() {
local response
local id
local id2
local data
local data2
local address1
local address2
local amount1
local amount2
local url1="$(hostname):1111/callback"
echo "url1=${url1}"
local url2="$(hostname):1112/callback"
echo "url2=${url2}"
# List batchers (should show at least empty default batcher)
echo "Testing listbatchers..."
response=$(curl -s proxy:8888/listbatchers)
echo "response=${response}"
id=$(echo "${response}" | jq ".result[0].batcherId")
echo "batcherId=${id}"
if [ "${id}" -ne "1" ]; then
exit 10
fi
echo "Tested listbatchers."
# getbatcher the default batcher
echo "Testing getbatcher..."
response=$(curl -sd '{}' localhost:8888/getbatcher)
echo "response=${response}"
data=$(echo "${response}" | jq -r ".result.batcherLabel")
echo "batcherLabel=${data}"
if [ "${data}" != "default" ]; then
exit 20
fi
response=$(curl -sd '{"batcherId":1}' localhost:8888/getbatcher)
echo "response=${response}"
data=$(echo "${response}" | jq -r ".result.batcherLabel")
echo "batcherLabel=${data}"
if [ "${data}" != "default" ]; then
exit 25
fi
echo "Tested getbatcher."
# getbatchdetails the default batcher
echo "Testing getbatchdetails..."
response=$(curl -sd '{}' localhost:8888/getbatchdetails)
echo "response=${response}"
data=$(echo "${response}" | jq -r ".result.batcherLabel")
echo "batcherLabel=${data}"
if [ "${data}" != "default" ]; then
exit 30
fi
echo "${response}" | jq -e ".result.outputs"
if [ "$?" -ne 0 ]; then
exit 32
fi
response=$(curl -sd '{"batcherId":1}' localhost:8888/getbatchdetails)
echo "response=${response}"
data=$(echo "${response}" | jq -r ".result.batcherLabel")
echo "batcherLabel=${data}"
if [ "${data}" != "default" ]; then
exit 35
fi
echo "${response}" | jq -e ".result.outputs"
if [ "$?" -ne 0 ]; then
exit 37
fi
echo "Tested getbatchdetails."
# addtobatch to default batcher
echo "Testing addtobatch..."
address1=$(curl -s localhost:8888/getnewaddress | jq -r ".address")
echo "address1=${address1}"
response=$(curl -sd '{"outputLabel":"test001","address":"'${address1}'","amount":0.001}' localhost:8888/addtobatch)
echo "response=${response}"
id=$(echo "${response}" | jq ".result.batcherId")
echo "batcherId=${id}"
if [ "${id}" -ne "1" ]; then
exit 40
fi
id=$(echo "${response}" | jq -e ".result.outputId")
if [ "$?" -ne 0 ]; then
exit 42
fi
echo "outputId=${id}"
address2=$(curl -s localhost:8888/getnewaddress | jq -r ".address")
echo "address2=${address2}"
response=$(curl -sd '{"batcherId":1,"outputLabel":"test002","address":"'${address2}'","amount":22000000}' localhost:8888/addtobatch)
echo "response=${response}"
id2=$(echo "${response}" | jq ".result.batcherId")
echo "batcherId=${id2}"
if [ "${id2}" -ne "1" ]; then
exit 47
fi
id2=$(echo "${response}" | jq -e ".result.outputId")
if [ "$?" -ne 0 ]; then
exit 50
fi
echo "outputId=${id2}"
echo "Tested addtobatch."
# batchspend default batcher
echo "Testing batchspend..."
response=$(curl -sd '{}' localhost:8888/batchspend)
echo "response=${response}"
echo "${response}" | jq -e ".error"
if [ "$?" -ne 0 ]; then
exit 55
fi
echo "Tested batchspend."
# getbatchdetails the default batcher
echo "Testing getbatchdetails..."
response=$(curl -sd '{}' localhost:8888/getbatchdetails)
echo "response=${response}"
data=$(echo "${response}" | jq ".result.nbOutputs")
echo "nbOutputs=${data}"
echo "Tested getbatchdetails."
# removefrombatch from default batcher
echo "Testing removefrombatch..."
response=$(curl -sd '{"outputId":'${id}'}' localhost:8888/removefrombatch)
echo "response=${response}"
id=$(echo "${response}" | jq ".result.batcherId")
echo "batcherId=${id}"
if [ "${id}" -ne "1" ]; then
exit 60
fi
response=$(curl -sd '{"outputId":'${id2}'}' localhost:8888/removefrombatch)
echo "response=${response}"
id=$(echo "${response}" | jq ".result.batcherId")
echo "batcherId=${id}"
if [ "${id}" -ne "1" ]; then
exit 64
fi
echo "Tested removefrombatch."
# getbatchdetails the default batcher
echo "Testing getbatchdetails..."
response=$(curl -sd '{"batcherId":1}' localhost:8888/getbatchdetails)
echo "response=${response}"
data2=$(echo "${response}" | jq ".result.nbOutputs")
echo "nbOutputs=${data2}"
if [ "${data2}" -ne "$((${data}-2))" ]; then
exit 68
fi
echo "Tested getbatchdetails."
# Create a batcher
echo "Testing createbatcher..."
response=$(curl -s -H 'Content-Type: application/json' -d '{"batcherLabel":"testbatcher","confTarget":32}' proxy:8888/createbatcher)
echo "response=${response}"
id=$(echo "${response}" | jq -e ".result.batcherId")
if [ "$?" -ne "0" ]; then
exit 70
fi
# List batchers (should show at least default and testbatcher batchers)
echo "Testing listbatches..."
response=$(curl -s proxy:8888/listbatchers)
echo "response=${response}"
id=$(echo "${response}" | jq '.result[] | select(.batcherLabel == "testbatcher") | .batcherId')
echo "batcherId=${id}"
if [ -z "${id}" ]; then
exit 75
fi
echo "Tested listbatchers."
# getbatcher the testbatcher batcher
echo "Testing getbatcher..."
response=$(curl -sd '{"batcherId":'${id}'}' localhost:8888/getbatcher)
echo "response=${response}"
data=$(echo "${response}" | jq -r ".result.batcherLabel")
echo "batcherLabel=${data}"
if [ "${data}" != "testbatcher" ]; then
exit 80
fi
response=$(curl -sd '{"batcherLabel":"testbatcher"}' localhost:8888/getbatcher)
echo "response=${response}"
data=$(echo "${response}" | jq -r ".result.batcherId")
echo "batcherId=${data}"
if [ "${data}" != "${id}" ]; then
exit 90
fi
echo "Tested getbatcher."
# getbatchdetails the testbatcher batcher
echo "Testing getbatchdetails..."
response=$(curl -sd '{"batcherLabel":"testbatcher"}' localhost:8888/getbatchdetails)
echo "response=${response}"
data=$(echo "${response}" | jq -r ".result.batcherId")
echo "batcherId=${data}"
if [ "${data}" != "${id}" ]; then
exit 100
fi
echo "${response}" | jq -e ".result.outputs"
if [ "$?" -ne 0 ]; then
exit 110
fi
response=$(curl -sd '{"batcherId":'${id}'}' localhost:8888/getbatchdetails)
echo "response=${response}"
data=$(echo "${response}" | jq -r ".result.batcherLabel")
echo "batcherLabel=${data}"
if [ "${data}" != "testbatcher" ]; then
exit 120
fi
echo "${response}" | jq -e ".result.outputs"
if [ "$?" -ne 0 ]; then
exit 130
fi
echo "Tested getbatchdetails."
# addtobatch to testbatcher batcher
echo "Testing addtobatch..."
address1=$(curl -s localhost:8888/getnewaddress | jq -r ".address")
echo "address1=${address1}"
response=$(curl -sd '{"batcherId":'${id}',"outputLabel":"test001","address":"'${address1}'","amount":0.001,"webhookUrl":"'${url1}'/'${address1}'"}' localhost:8888/addtobatch)
echo "response=${response}"
data=$(echo "${response}" | jq ".result.batcherId")
echo "batcherId=${data}"
if [ "${data}" -ne "${id}" ]; then
exit 140
fi
id2=$(echo "${response}" | jq -e ".result.outputId")
if [ "$?" -ne 0 ]; then
exit 142
fi
echo "outputId=${id2}"
address2=$(curl -s localhost:8888/getnewaddress | jq -r ".address")
echo "address2=${address2}"
response=$(curl -sd '{"batcherLabel":"testbatcher","outputLabel":"test002","address":"'${address2}'","amount":0.002,"webhookUrl":"'${url2}'/'${address2}'"}' localhost:8888/addtobatch)
echo "response=${response}"
data=$(echo "${response}" | jq ".result.batcherId")
echo "batcherId=${data}"
if [ "${data}" -ne "${id}" ]; then
exit 150
fi
id2=$(echo "${response}" | jq -e ".result.outputId")
if [ "$?" -ne 0 ]; then
exit 152
fi
echo "outputId=${id2}"
echo "Tested addtobatch."
# batchspend testbatcher batcher
echo "Testing batchspend..."
response=$(curl -sd '{"batcherLabel":"testbatcher"}' localhost:8888/batchspend)
echo "response=${response}"
data2=$(echo "${response}" | jq -e ".result.txid")
if [ "$?" -ne 0 ]; then
exit 160
fi
echo "txid=${data2}"
data=$(echo "${response}" | jq ".result.outputs | length")
if [ "${data}" -ne "2" ]; then
exit 162
fi
echo "Tested batchspend."
# getbatchdetails the testbatcher batcher
echo "Testing getbatchdetails..."
echo "txid=${data2}"
response=$(curl -sd '{"batcherLabel":"testbatcher","txid":'${data2}'}' localhost:8888/getbatchdetails)
echo "response=${response}"
data=$(echo "${response}" | jq ".result.nbOutputs")
echo "nbOutputs=${data}"
if [ "${data}" -ne "2" ]; then
exit 170
fi
echo "Tested getbatchdetails."
# List batchers
# Add to batch
# List batchers
# Remove from batch
# List batchers
}
wait_for_callbacks() {
nc -vlp1111 -e sh -c 'echo -en "HTTP/1.1 200 OK\r\n\r\n" ; timeout 1 tee /dev/tty | cat ; echo 1>&2' &
nc -vlp1112 -e sh -c 'echo -en "HTTP/1.1 200 OK\r\n\r\n" ; timeout 1 tee /dev/tty | cat ; echo 1>&2' &
}
wait_for_callbacks
testbatching
wait

View File

@@ -6,16 +6,27 @@
unwatchrequest() {
trace "Entering unwatchrequest()..."
local request=${1}
local address=$(echo "${request}" | cut -d ' ' -f2 | cut -d '/' -f3)
local watchid=${1}
local address=${2}
local unconfirmedCallbackURL=${3}
local confirmedCallbackURL=${4}
local returncode
trace "[unwatchrequest] Unwatch request on address ${address}"
trace "[unwatchrequest] Unwatch request id ${watchid} on address ${address} with url0conf ${unconfirmedCallbackURL} and url1conf ${confirmedCallbackURL}"
sql "UPDATE watching SET watching=0 WHERE address=\"${address}\""
returncode=$?
trace_rc ${returncode}
if [ "${watchid}" != "null" ]; then
sql "UPDATE watching SET watching=0 WHERE id=${watchid}"
returncode=$?
trace_rc ${returncode}
data="{\"event\":\"unwatch\",\"id\":${watchid}}"
else
sql "UPDATE watching SET watching=0 WHERE address='${address}' AND callback0conf=${unconfirmedCallbackURL} AND callback1conf=${confirmedCallbackURL}"
returncode=$?
trace_rc ${returncode}
data="{\"event\":\"unwatch\",\"address\":\"${address}\",\"unconfirmedCallbackURL\":${unconfirmedCallbackURL},\"confirmedCallbackURL\":${confirmedCallbackURL}}"
fi
data="{\"event\":\"unwatch\",\"address\":\"${address}\"}"
trace "[unwatchrequest] responding=${data}"
echo "${data}"
@@ -80,3 +91,34 @@ unwatchpub32labelrequest() {
return ${returncode}
}
unwatchtxidrequest() {
trace "Entering unwatchtxidrequest()..."
local watchid=${1}
local txid=${2}
local unconfirmedCallbackURL=${3}
local confirmedCallbackURL=${4}
local returncode
trace "[unwatchtxidrequest] Unwatch request id ${watchid} on txid ${txid} with url0conf ${unconfirmedCallbackURL} and url1conf ${confirmedCallbackURL}"
if [ "${watchid}" != "null" ]; then
sql "UPDATE watching_by_txid SET watching=0 WHERE id=${watchid}"
returncode=$?
trace_rc ${returncode}
data="{\"event\":\"unwatchtxid\",\"id\":${watchid}}"
else
sql "UPDATE watching_by_txid SET watching=0 WHERE txid='${txid}' AND callback0conf=${unconfirmedCallbackURL} AND callback1conf=${confirmedCallbackURL}"
returncode=$?
trace_rc ${returncode}
data="{\"event\":\"unwatchtxid\",\"txid\":\"${txid}\",\"unconfirmedCallbackURL\":${unconfirmedCallbackURL},\"confirmedCallbackURL\":${confirmedCallbackURL}}"
fi
trace "[unwatchtxidrequest] responding=${data}"
echo "${data}"
return ${returncode}
}

View File

@@ -35,7 +35,7 @@ spend() {
# Let's get transaction details on the spending wallet so that we have fee information
tx_details=$(get_transaction ${txid} "spender")
tx_raw_details=$(get_rawtransaction ${txid})
tx_raw_details=$(get_rawtransaction ${txid} | tr -d '\n')
# Amounts and fees are negative when spending so we absolute those fields
local tx_hash=$(echo "${tx_raw_details}" | jq '.result.hash')
@@ -43,8 +43,8 @@ spend() {
local tx_amount=$(echo "${tx_details}" | jq '.result.amount | fabs' | awk '{ printf "%.8f", $0 }')
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 tx_replaceable=$(echo "${tx_details}" | jq -r '.result."bip125-replaceable"')
tx_replaceable=$([ ${tx_replaceable} = "yes" ] && echo "true" || echo "false")
local fees=$(echo "${tx_details}" | jq '.result.fee | fabs' | awk '{ printf "%.8f", $0 }')
# 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
@@ -69,7 +69,7 @@ spend() {
########################################################################################################
# Let's insert the txid in our little DB -- then we'll already have it when receiving confirmation
sql "INSERT OR IGNORE INTO tx (txid, hash, confirmations, timereceived, fee, size, vsize, is_replaceable, raw_tx) VALUES (\"${txid}\", ${tx_hash}, 0, ${tx_ts_firstseen}, ${fees}, ${tx_size}, ${tx_vsize}, ${tx_replaceable}, readfile('spend-rawtx-${txid}-$$.blob'))"
sql "INSERT OR IGNORE INTO tx (txid, hash, confirmations, timereceived, fee, size, vsize, is_replaceable, conf_target, raw_tx) VALUES (\"${txid}\", ${tx_hash}, 0, ${tx_ts_firstseen}, ${fees}, ${tx_size}, ${tx_vsize}, ${tx_replaceable}, ${conf_target}, readfile('spend-rawtx-${txid}-$$.blob'))"
trace_rc $?
id_inserted=$(sql "SELECT id FROM tx WHERE txid=\"${txid}\"")
trace_rc $?
@@ -77,7 +77,7 @@ spend() {
trace_rc $?
data="{\"status\":\"accepted\""
data="${data},\"hash\":\"${txid}\",\"details\":{\"address\":\"${address}\",\"amount\":${amount},\"firstseen\":${tx_ts_firstseen},\"size\":${tx_size},\"vsize\":${tx_vsize},\"replaceable\":${replaceable},\"fee\":${fees},\"subtractfeefromamount\":${subtractfeefromamount}}}"
data="${data},\"txid\":\"${txid}\",\"hash\":${tx_hash},\"details\":{\"address\":\"${address}\",\"amount\":${amount},\"firstseen\":${tx_ts_firstseen},\"size\":${tx_size},\"vsize\":${tx_vsize},\"replaceable\":${tx_replaceable},\"fee\":${fees},\"subtractfeefromamount\":${subtractfeefromamount}}}"
# Delete the temp file containing the raw tx (see above)
rm spend-rawtx-${txid}-$$.blob
@@ -156,6 +156,7 @@ get_txns_spending() {
return ${returncode}
}
getbalance() {
trace "Entering getbalance()..."
@@ -293,113 +294,6 @@ getnewaddress() {
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
local tx_details
local tx_raw_details
# 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 -r ".result")
trace "[batchspend] txid=${txid}"
# Let's get transaction details on the spending wallet so that we have fee information
tx_details=$(get_transaction ${txid} "spender")
tx_raw_details=$(get_rawtransaction ${txid})
# Amounts and fees are negative when spending so we absolute those fields
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 | fabs' | awk '{ printf "%.8f", $0 }')
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=$(echo "${tx_details}" | jq '.result.fee | fabs' | awk '{ printf "%.8f", $0 }')
# 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}" > batchspend-rawtx-${txid}-$$.blob
# Let's insert the txid in our little DB -- then we'll already have it when receiving confirmation
sql "INSERT OR IGNORE INTO tx (txid, hash, confirmations, timereceived, fee, size, vsize, is_replaceable, raw_tx) VALUES (\"${txid}\", ${tx_hash}, 0, ${tx_ts_firstseen}, ${fees}, ${tx_size}, ${tx_vsize}, ${tx_replaceable}, readfile('batchspend-rawtx-${txid}-$$.blob'))"
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}\"}"
# Delete the temp file containing the raw tx (see above)
rm batchspend-rawtx-${txid}-$$.blob
else
local message=$(echo "${response}" | jq -e ".error.message")
data="{\"message\":${message}}"
fi
trace "[batchspend] responding=${data}"
echo "${data}"
return ${returncode}
}
create_wallet() {
trace "[Entering create_wallet()]"
@@ -416,4 +310,3 @@ create_wallet() {
return ${returncode}
}

View File

@@ -52,12 +52,13 @@ watchrequest() {
imported=0
fi
sql "INSERT INTO watching (address, watching, callback0conf, callback1conf, imported, event_message) VALUES (\"${address}\", 1, ${cb0conf_url}, ${cb1conf_url}, ${imported}, ${event_message}) ON CONFLICT(address) DO UPDATE SET watching=1, callback0conf=excluded.callback0conf, calledback0conf=0, callback1conf=excluded.callback1conf, calledback1conf=0, event_message=excluded.event_message"
sql "INSERT INTO watching (address, watching, callback0conf, callback1conf, imported, event_message) VALUES (\"${address}\", 1, ${cb0conf_url}, ${cb1conf_url}, ${imported}, ${event_message}) ON CONFLICT(address,callback0conf,callback1conf) DO UPDATE SET watching=1, event_message=${event_message}, calledback0conf=0, calledback1conf=0"
returncode=$?
trace_rc ${returncode}
if [ "${returncode}" -eq 0 ]; then
inserted=1
id_inserted=$(sql "SELECT id FROM watching WHERE address='${address}'")
id_inserted=$(sql "SELECT id FROM watching WHERE address='${address}' AND callback0conf=${cb0conf_url} AND callback1conf=${cb1conf_url}")
trace "[watchrequest] id_inserted: ${id_inserted}"
else
inserted=0
@@ -78,15 +79,15 @@ watchrequest() {
result="{\"id\":\"${id_inserted}\",
\"event\":\"watch\",
\"imported\":\"${imported}\",
\"inserted\":\"${inserted}\",
\"imported\":${imported},
\"inserted\":${inserted},
\"address\":\"${address}\",
\"unconfirmedCallbackURL\":${cb0conf_url},
\"confirmedCallbackURL\":${cb1conf_url},
\"estimatesmartfee2blocks\":\"${fees2blocks}\",
\"estimatesmartfee6blocks\":\"${fees6blocks}\",
\"estimatesmartfee36blocks\":\"${fees36blocks}\",
\"estimatesmartfee144blocks\":\"${fees144blocks}\",
\"estimatesmartfee2blocks\":${fees2blocks},
\"estimatesmartfee6blocks\":${fees6blocks},
\"estimatesmartfee36blocks\":${fees36blocks},
\"estimatesmartfee144blocks\":${fees144blocks},
\"eventMessage\":${event_message}}"
trace "[watchrequest] responding=${result}"
@@ -270,7 +271,7 @@ insert_watches() {
inserted_values="${inserted_values})"
done
sql "INSERT INTO watching (address, watching, callback0conf, callback1conf, imported, watching_by_pub32_id, pub32_index) VALUES ${inserted_values} ON CONFLICT(address) DO UPDATE SET watching=1, callback0conf=excluded.callback0conf, calledback0conf=0, callback1conf=excluded.callback1conf, calledback1conf=0"
sql "INSERT INTO watching (address, watching, callback0conf, callback1conf, imported, watching_by_pub32_id, pub32_index) VALUES ${inserted_values} ON CONFLICT(address,callback0conf,callback1conf) DO UPDATE SET watching=1, event_message=${event_message}, calledback0conf=0, calledback1conf=0"
returncode=$?
trace_rc ${returncode}
@@ -313,7 +314,7 @@ extend_watchers() {
# we want to extend the watched addresses to 166 if our gap is 100 (default).
trace "[extend_watchers] We have addresses to add to watchers!"
watchpub32 "${label}" "${pub32}" "${derivation_path}" $((${last_imported_n} + 1)) "${callback0conf}" "${callback1conf}" ${upgrade_to_n} > /dev/null
watchpub32 "${label}" "${pub32}" "${derivation_path}" "$((${last_imported_n} + 1))" "${callback0conf}" "${callback1conf}" "${upgrade_to_n}" > /dev/null
returncode=$?
trace_rc ${returncode}
else
@@ -342,12 +343,13 @@ watchtxidrequest() {
local result
trace "[watchtxidrequest] Watch request on txid (${txid}), cb 1-conf (${cb1conf_url}) and cb x-conf (${cbxconf_url}) on ${nbxconf} confirmations."
sql "INSERT OR IGNORE INTO watching_by_txid (txid, watching, callback1conf, callbackxconf, nbxconf) VALUES (${txid}, 1, ${cb1conf_url}, ${cbxconf_url}, ${nbxconf})"
sql "INSERT INTO watching_by_txid (txid, watching, callback1conf, callbackxconf, nbxconf) VALUES (${txid}, 1, ${cb1conf_url}, ${cbxconf_url}, ${nbxconf}) ON CONFLICT(txid, callback1conf, callbackxconf) DO UPDATE SET watching=1, nbxconf=${nbxconf}, calledback1conf=0, calledbackxconf=0"
returncode=$?
trace_rc ${returncode}
if [ "${returncode}" -eq 0 ]; then
inserted=1
id_inserted=$(sql "SELECT id FROM watching_by_txid WHERE txid=${txid}")
id_inserted=$(sql "SELECT id FROM watching_by_txid WHERE txid=${txid} AND callback1conf=${cb1conf_url} AND callbackxconf=${cbxconf_url}")
trace "[watchtxidrequest] id_inserted: ${id_inserted}"
else
inserted=0