diff --git a/api_auth_docker/api-sample.properties b/api_auth_docker/api-sample.properties index 27ddfd6..3987819 100644 --- a/api_auth_docker/api-sample.properties +++ b/api_auth_docker/api-sample.properties @@ -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: diff --git a/cyphernodeconf_docker/templates/gatekeeper/api.properties b/cyphernodeconf_docker/templates/gatekeeper/api.properties index d22c2f5..1e1b3e2 100644 --- a/cyphernodeconf_docker/templates/gatekeeper/api.properties +++ b/cyphernodeconf_docker/templates/gatekeeper/api.properties @@ -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: diff --git a/cyphernodeconf_docker/templates/installer/start.sh b/cyphernodeconf_docker/templates/installer/start.sh index a3d4a2b..0ec0163 100644 --- a/cyphernodeconf_docker/templates/installer/start.sh +++ b/cyphernodeconf_docker/templates/installer/start.sh @@ -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 diff --git a/doc/API.v0.md b/doc/API.v0.md index 133e5ff..ef937b0 100644 --- a/doc/API.v0.md +++ b/doc/API.v0.md @@ -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 +} +``` diff --git a/doc/BATCHING.md b/doc/BATCHING.md new file mode 100644 index 0000000..b1f2c13 --- /dev/null +++ b/doc/BATCHING.md @@ -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`. diff --git a/doc/CN-Arch.jpg b/doc/CN-Arch.jpg index b87ad2f..5bada13 100644 Binary files a/doc/CN-Arch.jpg and b/doc/CN-Arch.jpg differ diff --git a/doc/openapi/v0/cyphernode-api.yaml b/doc/openapi/v0/cyphernode-api.yaml index 408a9bc..68db867 100644 --- a/doc/openapi/v0/cyphernode-api.yaml +++ b/doc/openapi/v0/cyphernode-api.yaml @@ -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: diff --git a/doc/openapi/v0/cyphernode-callbacks.yaml b/doc/openapi/v0/cyphernode-callbacks.yaml index 79bded3..36ef2b0 100644 --- a/doc/openapi/v0/cyphernode-callbacks.yaml +++ b/doc/openapi/v0/cyphernode-callbacks.yaml @@ -116,7 +116,7 @@ components: type: "integer" fees: type: "number" - is_replaceable: + replaceable: type: "integer" blockhash: $ref: '#/components/schemas/TypeHashString' diff --git a/proxy_docker/app/data/cyphernode.sql b/proxy_docker/app/data/cyphernode.sql index 743ce18..b3ba8c1 100644 --- a/proxy_docker/app/data/cyphernode.sql +++ b/proxy_docker/app/data/cyphernode.sql @@ -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, diff --git a/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh b/proxy_docker/app/data/sqlmigrate20181213_0-0.1.sh old mode 100644 new mode 100755 diff --git a/proxy_docker/app/data/sqlmigrate20190104_0.1-0.2.sh b/proxy_docker/app/data/sqlmigrate20190104_0.1-0.2.sh old mode 100644 new mode 100755 diff --git a/proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sh b/proxy_docker/app/data/sqlmigrate20190130_0.1-0.2.sh old mode 100644 new mode 100755 diff --git a/proxy_docker/app/data/sqlmigrate20191127_0.2.4-0.3.0.sh b/proxy_docker/app/data/sqlmigrate20191127_0.2.4-0.3.0.sh old mode 100644 new mode 100755 diff --git a/proxy_docker/app/data/sqlmigrate20200610_0.4.0-0.5.0.sh b/proxy_docker/app/data/sqlmigrate20200610_0.4.0-0.5.0.sh new file mode 100755 index 0000000..5e9d7cb --- /dev/null +++ b/proxy_docker/app/data/sqlmigrate20200610_0.4.0-0.5.0.sh @@ -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 diff --git a/proxy_docker/app/data/sqlmigrate20200610_0.4.0-0.5.0.sql b/proxy_docker/app/data/sqlmigrate20200610_0.4.0-0.5.0.sql new file mode 100644 index 0000000..580ef70 --- /dev/null +++ b/proxy_docker/app/data/sqlmigrate20200610_0.4.0-0.5.0.sql @@ -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; diff --git a/proxy_docker/app/script/batching.sh b/proxy_docker/app/script/batching.sh new file mode 100644 index 0000000..6982a6a --- /dev/null +++ b/proxy_docker/app/script/batching.sh @@ -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 diff --git a/proxy_docker/app/script/blockchainrpc.sh b/proxy_docker/app/script/blockchainrpc.sh index 7d85aec..9cf452d 100644 --- a/proxy_docker/app/script/blockchainrpc.sh +++ b/proxy_docker/app/script/blockchainrpc.sh @@ -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 $? +} diff --git a/proxy_docker/app/script/callbacks_job.sh b/proxy_docker/app/script/callbacks_job.sh index 3bc7a06..1a6508a 100644 --- a/proxy_docker/app/script/callbacks_job.sh +++ b/proxy_docker/app/script/callbacks_job.sh @@ -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})\"," diff --git a/proxy_docker/app/script/callbacks_txid.sh b/proxy_docker/app/script/callbacks_txid.sh index 94f6546..0358057 100644 --- a/proxy_docker/app/script/callbacks_txid.sh +++ b/proxy_docker/app/script/callbacks_txid.sh @@ -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 } diff --git a/proxy_docker/app/script/computefees.sh b/proxy_docker/app/script/computefees.sh index 01f01c0..be35197 100644 --- a/proxy_docker/app/script/computefees.sh +++ b/proxy_docker/app/script/computefees.sh @@ -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} diff --git a/proxy_docker/app/script/confirmation.sh b/proxy_docker/app/script/confirmation.sh index 996511f..4218d15 100644 --- a/proxy_docker/app/script/confirmation.sh +++ b/proxy_docker/app/script/confirmation.sh @@ -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 diff --git a/proxy_docker/app/script/newblock.sh b/proxy_docker/app/script/newblock.sh index 7ecfa09..eb4c49d 100644 --- a/proxy_docker/app/script/newblock.sh +++ b/proxy_docker/app/script/newblock.sh @@ -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 } diff --git a/proxy_docker/app/script/requesthandler.sh b/proxy_docker/app/script/requesthandler.sh index 63f63a1..b033128 100644 --- a/proxy_docker/app/script/requesthandler.sh +++ b/proxy_docker/app/script/requesthandler.sh @@ -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 diff --git a/proxy_docker/app/script/test-batching.sh b/proxy_docker/app/script/test-batching.sh new file mode 100755 index 0000000..5eea4ff --- /dev/null +++ b/proxy_docker/app/script/test-batching.sh @@ -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 diff --git a/proxy_docker/app/script/unwatchrequest.sh b/proxy_docker/app/script/unwatchrequest.sh index 8c0a334..ad9784d 100644 --- a/proxy_docker/app/script/unwatchrequest.sh +++ b/proxy_docker/app/script/unwatchrequest.sh @@ -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} +} diff --git a/proxy_docker/app/script/walletoperations.sh b/proxy_docker/app/script/walletoperations.sh index 067937b..b436fe1 100644 --- a/proxy_docker/app/script/walletoperations.sh +++ b/proxy_docker/app/script/walletoperations.sh @@ -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} } - diff --git a/proxy_docker/app/script/watchrequest.sh b/proxy_docker/app/script/watchrequest.sh index cd07df7..4901eaa 100644 --- a/proxy_docker/app/script/watchrequest.sh +++ b/proxy_docker/app/script/watchrequest.sh @@ -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