├── .dockerignore ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── docker-compose.yml ├── rustfmt.toml ├── src ├── args.rs ├── bdk_utils.rs ├── bitcoind_client.rs ├── cli.rs ├── convert.rs ├── disk.rs ├── error.rs ├── hex_utils.rs ├── main.rs ├── proxy.rs ├── rgb_utils.rs └── swap.rs ├── test_data ├── test_cookie ├── test_cookie_bad ├── test_env_file └── test_env_file_bad └── tests ├── common.sh ├── scripts ├── close_coop.sh ├── close_coop_nobtc_acceptor.sh ├── close_coop_other_side.sh ├── close_coop_vanilla.sh ├── close_coop_zero_balance.sh ├── close_force.sh ├── close_force_nobtc_acceptor.sh ├── close_force_other_side.sh ├── close_force_pending_htlc.sh ├── multi_open_close.sh ├── multihop.sh ├── multiple_payments.sh ├── open_after_double_send.sh ├── restart.sh ├── send_payment.sh ├── send_receive.sh ├── send_vanilla_payment.sh ├── swap_roundtrip.sh ├── swap_roundtrip_buy.sh ├── swap_roundtrip_fail.sh ├── swap_roundtrip_fail_amount_maker.sh ├── swap_roundtrip_fail_amount_taker.sh ├── swap_roundtrip_multihop_buy.sh ├── swap_roundtrip_multihop_sell.sh ├── swap_roundtrip_timeout.sh └── vanilla_keysend.sh ├── test.sh └── tmux.conf /.dockerignore: -------------------------------------------------------------------------------- 1 | /datacore 2 | /dataindex 3 | /dataldk 4 | /dataldk0 5 | /dataldk1 6 | /dataldk2 7 | **/target 8 | /test_data 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration Checks 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout source code 10 | uses: actions/checkout@v2 11 | with: 12 | submodules: recursive 13 | - name: Install Rust stable toolchain 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: stable 17 | override: true 18 | profile: minimal 19 | - name: Build on Rust stable 20 | run: cargo build --verbose --color always 21 | - name: Check formatting 22 | run: rustup component add rustfmt && cargo fmt -- --check 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | .ldk 12 | 13 | # RPC auth 14 | .env 15 | 16 | # Test files 17 | /cargo_build.log 18 | /datacore 19 | /dataindex 20 | /dataldk0 21 | /dataldk1 22 | /dataldk2 23 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "client_side_validation"] 2 | path = client_side_validation 3 | url = git@github.com:RGB-Tools/client_side_validation.git 4 | shallow = true 5 | [submodule "rust-lightning"] 6 | path = rust-lightning 7 | url = git@github.com:RGB-Tools/rust-lightning.git 8 | shallow = true 9 | [submodule "rgb-wallet"] 10 | path = rgb-wallet 11 | url = git@github.com:RGB-Tools/rgb-wallet.git 12 | shallow = true 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ldk-sample" 3 | version = "0.1.0" 4 | authors = ["Valentine Wallace "] 5 | license = "MIT OR Apache-2.0" 6 | edition = "2018" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | lightning = { version = "0.0.115", features = ["max_level_trace"], path = "./rust-lightning/lightning" } 12 | lightning-block-sync = { version = "0.0.115", features = [ "rpc-client" ] } 13 | lightning-invoice = { version = "0.23", path = "./rust-lightning/lightning-invoice" } 14 | lightning-net-tokio = { version = "0.0.115" } 15 | lightning-persister = { version = "0.0.115", path = "./rust-lightning/lightning-persister" } 16 | lightning-background-processor = { version = "0.0.115", features = [ "futures" ] } 17 | lightning-rapid-gossip-sync = { version = "0.0.115" } 18 | 19 | base64 = "0.13.0" 20 | bitcoin_30 = { package = "bitcoin", version = "0.30.0" } 21 | bitcoin = { package = "bitcoin", version = "0.29.2" } 22 | bitcoin-bech32 = "0.12" 23 | bech32 = "0.8" 24 | hex = "0.3" 25 | libc = "0.2" 26 | 27 | chrono = "0.4" 28 | rand = "0.4" 29 | serde_json = { version = "1.0" } 30 | tokio = { version = "1", features = [ "io-util", "macros", "rt", "rt-multi-thread", "sync", "net", "time" ] } 31 | 32 | # RGB and related 33 | amplify = "=4.0.0" 34 | bdk = { version = "0.28", features = ["electrum", "keys-bip39", "sqlite-bundled"] } 35 | bp-core = { version = "=0.10.5", features = ["serde"] } 36 | bp-seals = "=0.10.5" 37 | futures = "0.3" 38 | miniscript = { version = "8.0", features = ["serde"] } 39 | reqwest = { version = "0.11", default-features = false, features = ["json", "multipart", "native-tls", "stream"] } 40 | rgb-contracts = { version = "=0.10.0-rc.2", features = ["all", "electrum"] } 41 | rgb_core = { package = "rgb-core", version = "=0.10.5" } 42 | rgb-schemata = "=0.10.0-rc.1" 43 | rgb-std = { version = "=0.10.3", features = ["all"] } 44 | rgb-wallet = { version = "=0.10.3", features = ["all"] } 45 | serde = { version = "^1.0", features = ["derive"] } 46 | strict_encoding = "=2.5.0" 47 | thiserror = "1.0" 48 | tokio-util = { version = "0.7.4", features = ["codec"] } 49 | 50 | [patch.crates-io] 51 | commit_verify = { path = "./client_side_validation/commit_verify" } 52 | lightning = { path = "./rust-lightning/lightning" } 53 | lightning-background-processor = { path = "./rust-lightning/lightning-background-processor"} 54 | rgb-std = { path = "./rgb-wallet/std" } 55 | rgb-wallet = { path = "./rgb-wallet" } 56 | 57 | [profile.release] 58 | panic = "abort" 59 | 60 | [profile.dev] 61 | panic = "abort" 62 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 lightningdevkit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Archived project 2 | This repository is no longer being maintained and has been archived for 3 | historical purposes. 4 | 5 | Development has moved to the 6 | [rgb-lightning-node](https://github.com/RGB-Tools/rgb-lightning-node) project, 7 | which is the evolution of this CLI sample to a daemon controlled via REST APIs. 8 | 9 | # RGB Lightning Sample 10 | 11 | RGB-enabled LN node based on [ldk-sample]. 12 | 13 | Please notice that an RGB-enabled LN node daemon is available in the 14 | [rgb-lightning-node] repository. For any application interested in integrating 15 | RGB on lightning it is recommended to use the node as this sample has less 16 | features and is less maintained. 17 | 18 | The node enables the possibility to create payment channels containing assets 19 | issued using the RGB protocol, as well as routing RGB asset denominated 20 | payments across multiple channels, given that they all possess the necessary 21 | liquidity. In this way, RGB assets can be transferred with the same user 22 | experience and security assumptions of regular Bitcoin Lightning Network 23 | payments. This is achieved by adding to each lightning commitment transaction a 24 | dedicated extra output containing the anchor to the RGB state transition. 25 | 26 | More context on how RGB works on the Lightning Network can be found 27 | [here](https://docs.rgb.info/lightning-network-compatibility). 28 | 29 | The RGB functionality for now can be tested only in regtest or testnet 30 | environments, but an advanced user may be able to apply changes in order to use 31 | it also on other networks. 32 | Please be careful, this software is early alpha, we do not take any 33 | responsability for loss of funds or any other issue you may encounter. 34 | 35 | Also note that the following RGB projects (included in this project as git 36 | sumbodules) have been modified in order to make the creation of static 37 | consignments (without entropy) possible. Here links to compare the applied 38 | changes: 39 | - [client_side_validation](https://github.com/RGB-Tools/client_side_validation/compare/v0.10.4...static_0.10) 40 | - [rgb-wallet](https://github.com/RGB-Tools/rgb-wallet/compare/v0.10.3...static_0.10) 41 | 42 | But most importantly [rust-lightning] has been changed in order to support 43 | RGB channels, 44 | [here](https://github.com/RGB-Tools/rust-lightning/compare/v0.0.115...rgb) 45 | a compare with `v0.0.115`, the version we applied the changes to. 46 | 47 | ## Installation 48 | 49 | Clone the project, including (shallow) submodules: 50 | ```sh 51 | git clone https://github.com/RGB-Tools/rgb-lightning-sample --recurse-submodules --shallow-submodules 52 | ``` 53 | 54 | Build the ldk-sample crate: 55 | ```sh 56 | cargo build 57 | ``` 58 | 59 | ## Usage in a test environment 60 | 61 | A test environment has been added for easier testing. It currently supports the 62 | regtest and testnet networks. 63 | 64 | Instructions and commands are meant to be run from the project's root 65 | directory. 66 | 67 | The `docker-compose.yml` file manages, when using the regtest network: 68 | - a bitcoind node 69 | - an electrs instance 70 | - an [RGB proxy server] instance 71 | 72 | Run this command in order to start with a clean test environment (specifying 73 | the desired network): 74 | ```sh 75 | tests/test.sh --start --network 76 | ``` 77 | 78 | The command will create the directories needed by the services, start the 79 | docker containers and mine some blocks. The test environment will always start 80 | in a clean state, taking down previous running services (if any) and 81 | re-creating data directories. 82 | 83 | Once services are running, ldk nodes can be started. 84 | Each ldk node needs to be started in a separate shell with `cargo run`, 85 | specifying: 86 | - bitcoind user, password, host and port 87 | - ldk data directory 88 | - ldk peer listening port 89 | - network 90 | 91 | Here's an example of how to start three regtest nodes, each one using the 92 | shared regtest services provided by docker compose: 93 | ```sh 94 | # 1st shell 95 | cargo run user:password@localhost:18443 dataldk0/ 9735 regtest 96 | 97 | # 2nd shell 98 | cargo run user:password@localhost:18443 dataldk1/ 9736 regtest 99 | 100 | # 3rd shell 101 | cargo run user:password@localhost:18443 dataldk2/ 9737 regtest 102 | ``` 103 | 104 | Here's an example of how to start three testnet nodes, each one using the 105 | external testnet services: 106 | 107 | ```sh 108 | # 1st shell 109 | cargo run user:password@electrum.iriswallet.com:18332 dataldk0/ 9735 testnet 110 | 111 | # 2nd shell 112 | cargo run user:password@electrum.iriswallet.com:18332 dataldk1/ 9736 testnet 113 | 114 | # 3rd shell 115 | cargo run user:password@electrum.iriswallet.com:18332 dataldk2/ 9737 testnet 116 | ``` 117 | 118 | Once ldk nodes are running, they can be operated via their CLI. 119 | See the [on-chain] and [off-chain] sections below and the CLI `help` command for 120 | information on the available commands. 121 | 122 | To stop running nodes, exit their CLI with the `quit` command (or `^D`). 123 | 124 | To stop running services and to cleanup data directories, run: 125 | ```sh 126 | tests/test.sh --stop 127 | ``` 128 | 129 | If needed, more nodes can be added. To do so: 130 | - add data directories for the additional ldk nodes (`dataldk`) 131 | - run additional `cargo run`s for ldk nodes, specifying the correct bitcoind 132 | string, data directory, peer listening port and network 133 | 134 | ## On-chain operations 135 | 136 | On-chain RGB operations are available as CLI commands. The following sections 137 | briefly explain how to use each one of them. 138 | 139 | ### Issuing an asset 140 | To issue a new asset, call the `issueasset` command followed by: 141 | - total supply 142 | - ticker 143 | - name 144 | - precision 145 | 146 | Example: 147 | ``` 148 | issueasset 1000 USDT Tether 0 149 | ``` 150 | 151 | ### Receiving assets 152 | To receive assets, call the `receiveasset`. 153 | Provide the sender with the returned blinded UTXO. 154 | 155 | Example: 156 | ``` 157 | receiveasset 158 | ``` 159 | 160 | ### Sending assets 161 | To send assets to another node with an on-chain transaction, call the 162 | `sendasset` command followed by: 163 | - the asset's contract ID 164 | - the amount to be sent 165 | - the recipient's blinded UTXO 166 | 167 | Example: 168 | ``` 169 | sendasset rgb1lfxs4dmqs7a90vrz0yaje60fakuvu9u9esx882shy437yxazmysqamnv2r 400 txob1y3w8h9n4v4tkn37uj55dvqyuhvftrr2cxecp4pzkhjxjc4zcfxtsmdt2vf 170 | ``` 171 | 172 | ### Refreshing a transfer 173 | Transfers complete automatically on the sender side after the `sendasset` 174 | command. To complete a transfer on the receiver side, once the send operation 175 | is complete, call the `refresh` command. 176 | 177 | Example: 178 | ``` 179 | refresh 180 | ``` 181 | 182 | ### Showing an asset's balance 183 | To show an asset's balance, call the `assetbalance` command followed by the 184 | asset's contract ID for which the balance should be displayed. 185 | 186 | Example: 187 | ``` 188 | assetbalance rgb1lfxs4dmqs7a90vrz0yaje60fakuvu9u9esx882shy437yxazmysqamnv2r 189 | ``` 190 | 191 | ### Mining blocks 192 | A command to mine new blocks is provided for convenience. To mine new blocks, 193 | call the `mine` command followed by the desired number of blocks. 194 | 195 | Example: 196 | ``` 197 | mine 6 198 | ``` 199 | 200 | ## Off-chain operations 201 | 202 | Off-chain RGB operations are available as modified CLI commands. The following 203 | sections briefly explain which commands support RGB functionality and how to use 204 | them. 205 | 206 | ### Opening channels 207 | To open a new channel, call the `openchannel` command followed by: 208 | - the peer's pubkey, host and port 209 | - the bitcoin amount to allocate to the channel, in satoshis 210 | - the bitcoin amount to push, in millisatoshis 211 | - the RGB asset's contract ID 212 | - the RGB amount to allocate to the channel 213 | - the `--public` optional flag, to announce the channel 214 | 215 | Example: 216 | ``` 217 | openchannel 03ddf2eedb06d5bbd128ccd4f558cb4a7428bfbe359259c718db7d2a8eead169fb@127.0.0.1:9736 999666 546000 rgb1lfxs4dmqs7a90vrz0yaje60fakuvu9u9esx882shy437yxazmysqamnv2r 218 | zmysqamnv2r 100 --public 219 | ``` 220 | 221 | ### Listing channels 222 | To list the available channels, call the `listchannels` command. The output 223 | contains RGB information about the channel: 224 | - `rgb_contract_id`: the asset's contract ID 225 | - `rgb_local_amount`: the amount allocated to the local peer 226 | - `rgb_remote_amount`: the amount allocated to the remote peer 227 | 228 | Example: 229 | ``` 230 | listchannels 231 | ``` 232 | 233 | ### Sending assets 234 | To send RGB assets over the LN network, call the `coloredkeysend` command followed by: 235 | - the receiving peer's pubkey 236 | - the bitcoin amount in satoshis 237 | - the RGB asset's contract ID 238 | - the RGB amount 239 | 240 | Example: 241 | ``` 242 | coloredkeysend 03ddf2eedb06d5bbd128ccd4f558cb4a7428bfbe359259c718db7d2a8eead169fb 2000000 rgb1lfxs4dmqs7a90vrz0yaje60fakuvu9u9esx882shy437yxazmysqamnv2r 10 243 | ``` 244 | 245 | At the moment, only the `coloredkeysend` command has been modified to support RGB 246 | functionality. The invoice-based `sendpayment` will be added in the future. 247 | 248 | ### Closing channels 249 | To close a channel, call the `closechannel` (for a cooperative close) or the `forceclosechannel` (for a unilateral close) command followed by: 250 | - the channel ID 251 | - the peer's pubkey 252 | 253 | Example (cooperative): 254 | ``` 255 | closechannel 83034b8a3302bb9cc63d75ffd49b03e224cb28d4911702827a8dd2553d0f5229 03ddf2eedb06d5bbd128ccd4f558cb4a7428bfbe359259c718db7d2a8eead169fb 256 | ``` 257 | 258 | Example (unilateral): 259 | ``` 260 | forceclosechannel 83034b8a3302bb9cc63d75ffd49b03e224cb28d4911702827a8dd2553d0f5229 03ddf2eedb06d5bbd128ccd4f558cb4a7428bfbe359259c718db7d2a8eead169fb 261 | ``` 262 | 263 | ## Scripted tests 264 | 265 | A few scenarios can be tested using a scripted sequence. This is only supported 266 | on regtest as mining needs to be automated as well. 267 | 268 | The entrypoint for scripted tests is the shell command `tests/test.sh`, 269 | which can be called from the project's root directory. The default network is 270 | "regtest" so it is not mandatory to specify it via the `--network` CLI option. 271 | 272 | To view the available tests, call it with the `-l` option. 273 | Example: 274 | ```sh 275 | tests/test.sh -l 276 | ``` 277 | 278 | To start a test, call it with: 279 | - the `-t` option followed by the test name 280 | - the `--start` option to automatically create directories and start the services 281 | - the `--stop` option to automatically stop the services and cleanup 282 | Example: 283 | ```sh 284 | tests/test.sh -t multihop --start --stop 285 | ``` 286 | 287 | To get a help message, call it with the `-h` option. 288 | Example: 289 | ```sh 290 | tests/test.sh -h 291 | ``` 292 | 293 | ## License 294 | 295 | Licensed under either: 296 | 297 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 298 | * MIT License ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 299 | 300 | at your option. 301 | 302 | [RGB proxy server]: https://github.com/grunch/rgb-proxy-server 303 | [ldk-sample]: https://github.com/lightningdevkit/ldk-sample 304 | [off-chain]: #off-chain-operations 305 | [on-chain]: #on-chain-operations 306 | [rgb-lightning-node]: https://github.com/RGB-Tools/rgb-lightning-node 307 | [rust-lightning]: https://github.com/lightningdevkit/rust-lightning 308 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | bitcoind: 5 | container_name: bitcoind 6 | image: registry.gitlab.com/hashbeam/docker/bitcoind:25.0 7 | command: "-fallbackfee=0.0002" 8 | environment: 9 | MYUID: 1000 10 | MYGID: 1000 11 | RPCAUTH: "user:84c66d54d736d8b02aaa5b02e07e759b$$cc56c229b2a49ae2bfd5932cc8a6135d435bb9a7ac037ddd351d65936082c03d" 12 | ports: 13 | - 18443:18443 14 | volumes: 15 | - ./datacore:/srv/app/.bitcoin 16 | electrs: 17 | container_name: electrs 18 | image: registry.gitlab.com/hashbeam/docker/electrs:0.9.14 19 | environment: 20 | MYUID: 1000 21 | MYGID: 1000 22 | BTCPASS: "password" 23 | volumes: 24 | - ./dataindex:/srv/app/db 25 | depends_on: 26 | - bitcoind 27 | ports: 28 | - 50001:50001 29 | proxy: 30 | image: ghcr.io/grunch/rgb-proxy-server:0.1.0 31 | ports: 32 | - 3000:3000 33 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true # use tab characters for indentation, spaces for alignment 2 | use_field_init_shorthand = true 3 | max_width = 100 4 | use_small_heuristics = "Max" 5 | fn_args_layout = "Compressed" 6 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::LdkUserInfo; 2 | use bitcoin::network::constants::Network; 3 | use lightning::ln::msgs::NetAddress; 4 | use std::collections::HashMap; 5 | use std::env; 6 | use std::fs; 7 | use std::net::IpAddr; 8 | use std::path::{Path, PathBuf}; 9 | use std::str::FromStr; 10 | 11 | pub(crate) fn parse_startup_args() -> Result { 12 | if env::args().len() < 4 { 13 | println!("rgb-lightning-sample requires at least 3 arguments: `cargo run [:@]: ldk_storage_directory_path [] [bitcoin-network] [announced-node-name announced-listen-addr*]`"); 14 | return Err(()); 15 | } 16 | let bitcoind_rpc_info = env::args().skip(1).next().unwrap(); 17 | let bitcoind_rpc_info_parts: Vec<&str> = bitcoind_rpc_info.rsplitn(2, "@").collect(); 18 | 19 | // Parse rpc auth after getting network for default .cookie location 20 | let bitcoind_rpc_path: Vec<&str> = bitcoind_rpc_info_parts[0].split(":").collect(); 21 | if bitcoind_rpc_path.len() != 2 { 22 | println!("ERROR: bad bitcoind RPC path provided"); 23 | return Err(()); 24 | } 25 | let bitcoind_rpc_host = bitcoind_rpc_path[0].to_string(); 26 | let bitcoind_rpc_port = bitcoind_rpc_path[1].parse::().unwrap(); 27 | 28 | let ldk_storage_dir_path = env::args().skip(2).next().unwrap(); 29 | 30 | let mut ldk_peer_port_set = true; 31 | let ldk_peer_listening_port: u16 = match env::args().skip(3).next().map(|p| p.parse()) { 32 | Some(Ok(p)) => p, 33 | Some(Err(_)) => { 34 | ldk_peer_port_set = false; 35 | 9735 36 | } 37 | None => { 38 | ldk_peer_port_set = false; 39 | 9735 40 | } 41 | }; 42 | 43 | let mut arg_idx = match ldk_peer_port_set { 44 | true => 4, 45 | false => 3, 46 | }; 47 | let network: Network = match env::args().skip(arg_idx).next().as_ref().map(String::as_str) { 48 | Some("testnet") => Network::Testnet, 49 | Some("regtest") => Network::Regtest, 50 | Some("signet") => Network::Signet, 51 | Some(net) => { 52 | panic!("Unsupported network provided. Options are: `regtest`, `testnet`, and `signet`. Got {}", net); 53 | } 54 | None => Network::Testnet, 55 | }; 56 | 57 | let (bitcoind_rpc_username, bitcoind_rpc_password) = if bitcoind_rpc_info_parts.len() == 1 { 58 | get_rpc_auth_from_env_vars() 59 | .or(get_rpc_auth_from_env_file(None)) 60 | .or(get_rpc_auth_from_cookie(None, Some(network), None)) 61 | .or({ 62 | println!("ERROR: unable to get bitcoind RPC username and password"); 63 | print_rpc_auth_help(); 64 | Err(()) 65 | })? 66 | } else if bitcoind_rpc_info_parts.len() == 2 { 67 | parse_rpc_auth(bitcoind_rpc_info_parts[1])? 68 | } else { 69 | println!("ERROR: bad bitcoind RPC URL provided"); 70 | return Err(()); 71 | }; 72 | 73 | let ldk_announced_node_name = match env::args().skip(arg_idx + 1).next().as_ref() { 74 | Some(s) => { 75 | if s.len() > 32 { 76 | panic!("Node Alias can not be longer than 32 bytes"); 77 | } 78 | arg_idx += 1; 79 | let mut bytes = [0; 32]; 80 | bytes[..s.len()].copy_from_slice(s.as_bytes()); 81 | bytes 82 | } 83 | None => [0; 32], 84 | }; 85 | 86 | let mut ldk_announced_listen_addr = Vec::new(); 87 | loop { 88 | match env::args().skip(arg_idx + 1).next().as_ref() { 89 | Some(s) => match IpAddr::from_str(s) { 90 | Ok(IpAddr::V4(a)) => { 91 | ldk_announced_listen_addr 92 | .push(NetAddress::IPv4 { addr: a.octets(), port: ldk_peer_listening_port }); 93 | arg_idx += 1; 94 | } 95 | Ok(IpAddr::V6(a)) => { 96 | ldk_announced_listen_addr 97 | .push(NetAddress::IPv6 { addr: a.octets(), port: ldk_peer_listening_port }); 98 | arg_idx += 1; 99 | } 100 | Err(_) => panic!("Failed to parse announced-listen-addr into an IP address"), 101 | }, 102 | None => break, 103 | } 104 | } 105 | 106 | Ok(LdkUserInfo { 107 | bitcoind_rpc_username, 108 | bitcoind_rpc_password, 109 | bitcoind_rpc_host, 110 | bitcoind_rpc_port, 111 | ldk_storage_dir_path, 112 | ldk_peer_listening_port, 113 | ldk_announced_listen_addr, 114 | ldk_announced_node_name, 115 | network, 116 | }) 117 | } 118 | 119 | // Default datadir relative to home directory 120 | #[cfg(target_os = "windows")] 121 | const DEFAULT_BITCOIN_DATADIR: &str = "AppData/Roaming/Bitcoin"; 122 | #[cfg(target_os = "linux")] 123 | const DEFAULT_BITCOIN_DATADIR: &str = ".bitcoin"; 124 | #[cfg(target_os = "macos")] 125 | const DEFAULT_BITCOIN_DATADIR: &str = "Library/Application Support/Bitcoin"; 126 | 127 | // Environment variable/.env keys 128 | const BITCOIND_RPC_USER_KEY: &str = "RPC_USER"; 129 | const BITCOIND_RPC_PASSWORD_KEY: &str = "RPC_PASSWORD"; 130 | 131 | fn print_rpc_auth_help() { 132 | // Get the default data directory 133 | let home_dir = env::home_dir() 134 | .as_ref() 135 | .map(|ref p| p.to_str()) 136 | .flatten() 137 | .unwrap_or("$HOME") 138 | .replace("\\", "/"); 139 | let data_dir = format!("{}/{}", home_dir, DEFAULT_BITCOIN_DATADIR); 140 | println!("To provide the bitcoind RPC username and password, you can either:"); 141 | println!( 142 | "1. Provide the username and password as the first argument to this program in the format: \ 143 | :@:" 144 | ); 145 | println!("2. Provide : in a .cookie file in the default \ 146 | bitcoind data directory (automatically created by bitcoind on startup): `{}`", data_dir); 147 | println!( 148 | "3. Set the {} and {} environment variables", 149 | BITCOIND_RPC_USER_KEY, BITCOIND_RPC_PASSWORD_KEY 150 | ); 151 | println!( 152 | "4. Provide {} and {} fields in a .env file in the current directory", 153 | BITCOIND_RPC_USER_KEY, BITCOIND_RPC_PASSWORD_KEY 154 | ); 155 | } 156 | 157 | fn parse_rpc_auth(rpc_auth: &str) -> Result<(String, String), ()> { 158 | let rpc_auth_info: Vec<&str> = rpc_auth.split(':').collect(); 159 | if rpc_auth_info.len() != 2 { 160 | println!("ERROR: bad bitcoind RPC username/password combo provided"); 161 | return Err(()); 162 | } 163 | let rpc_username = rpc_auth_info[0].to_string(); 164 | let rpc_password = rpc_auth_info[1].to_string(); 165 | Ok((rpc_username, rpc_password)) 166 | } 167 | 168 | fn get_cookie_path( 169 | data_dir: Option<(&str, bool)>, network: Option, cookie_file_name: Option<&str>, 170 | ) -> Result { 171 | let data_dir_path = match data_dir { 172 | Some((dir, true)) => env::home_dir().ok_or(())?.join(dir), 173 | Some((dir, false)) => PathBuf::from(dir), 174 | None => env::home_dir().ok_or(())?.join(DEFAULT_BITCOIN_DATADIR), 175 | }; 176 | 177 | let data_dir_path_with_net = match network { 178 | Some(Network::Testnet) => data_dir_path.join("testnet3"), 179 | Some(Network::Regtest) => data_dir_path.join("regtest"), 180 | Some(Network::Signet) => data_dir_path.join("signet"), 181 | _ => data_dir_path, 182 | }; 183 | 184 | let cookie_path = data_dir_path_with_net.join(cookie_file_name.unwrap_or(".cookie")); 185 | 186 | Ok(cookie_path) 187 | } 188 | 189 | fn get_rpc_auth_from_cookie( 190 | data_dir: Option<(&str, bool)>, network: Option, cookie_file_name: Option<&str>, 191 | ) -> Result<(String, String), ()> { 192 | let cookie_path = get_cookie_path(data_dir, network, cookie_file_name)?; 193 | let cookie_contents = fs::read_to_string(cookie_path).or(Err(()))?; 194 | parse_rpc_auth(&cookie_contents) 195 | } 196 | 197 | fn get_rpc_auth_from_env_vars() -> Result<(String, String), ()> { 198 | if let (Ok(username), Ok(password)) = 199 | (env::var(BITCOIND_RPC_USER_KEY), env::var(BITCOIND_RPC_PASSWORD_KEY)) 200 | { 201 | Ok((username, password)) 202 | } else { 203 | Err(()) 204 | } 205 | } 206 | 207 | fn get_rpc_auth_from_env_file(env_file_name: Option<&str>) -> Result<(String, String), ()> { 208 | let env_file_map = parse_env_file(env_file_name)?; 209 | if let (Some(username), Some(password)) = 210 | (env_file_map.get(BITCOIND_RPC_USER_KEY), env_file_map.get(BITCOIND_RPC_PASSWORD_KEY)) 211 | { 212 | Ok((username.to_string(), password.to_string())) 213 | } else { 214 | Err(()) 215 | } 216 | } 217 | 218 | fn parse_env_file(env_file_name: Option<&str>) -> Result, ()> { 219 | // Default .env file name is .env 220 | let env_file_name = match env_file_name { 221 | Some(filename) => filename, 222 | None => ".env", 223 | }; 224 | 225 | // Read .env file 226 | let env_file_path = Path::new(env_file_name); 227 | let env_file_contents = fs::read_to_string(env_file_path).or(Err(()))?; 228 | 229 | // Collect key-value pairs from .env file into a map 230 | let mut env_file_map: HashMap = HashMap::new(); 231 | for line in env_file_contents.lines() { 232 | let line_parts: Vec<&str> = line.splitn(2, '=').collect(); 233 | if line_parts.len() != 2 { 234 | println!("ERROR: bad .env file format"); 235 | return Err(()); 236 | } 237 | env_file_map.insert(line_parts[0].to_string(), line_parts[1].to_string()); 238 | } 239 | 240 | Ok(env_file_map) 241 | } 242 | 243 | #[cfg(test)] 244 | mod rpc_auth_tests { 245 | use super::*; 246 | 247 | const TEST_ENV_FILE: &str = "test_data/test_env_file"; 248 | const TEST_ENV_FILE_BAD: &str = "test_data/test_env_file_bad"; 249 | const TEST_ABSENT_FILE: &str = "nonexistent_file"; 250 | const TEST_DATA_DIR: &str = "test_data"; 251 | const TEST_COOKIE: &str = "test_cookie"; 252 | const TEST_COOKIE_BAD: &str = "test_cookie_bad"; 253 | const EXPECTED_USER: &str = "testuser"; 254 | const EXPECTED_PASSWORD: &str = "testpassword"; 255 | 256 | #[test] 257 | fn test_parse_rpc_auth_success() { 258 | let (username, password) = parse_rpc_auth("testuser:testpassword").unwrap(); 259 | assert_eq!(username, EXPECTED_USER); 260 | assert_eq!(password, EXPECTED_PASSWORD); 261 | } 262 | 263 | #[test] 264 | fn test_parse_rpc_auth_fail() { 265 | let result = parse_rpc_auth("testuser"); 266 | assert!(result.is_err()); 267 | } 268 | 269 | #[test] 270 | fn test_get_cookie_path_success() { 271 | let test_cases = vec![ 272 | ( 273 | None, 274 | None, 275 | None, 276 | env::home_dir().unwrap().join(DEFAULT_BITCOIN_DATADIR).join(".cookie"), 277 | ), 278 | ( 279 | Some((TEST_DATA_DIR, true)), 280 | Some(Network::Testnet), 281 | None, 282 | env::home_dir().unwrap().join(TEST_DATA_DIR).join("testnet3").join(".cookie"), 283 | ), 284 | ( 285 | Some((TEST_DATA_DIR, false)), 286 | Some(Network::Regtest), 287 | Some(TEST_COOKIE), 288 | PathBuf::from(TEST_DATA_DIR).join("regtest").join(TEST_COOKIE), 289 | ), 290 | ( 291 | Some((TEST_DATA_DIR, false)), 292 | Some(Network::Signet), 293 | None, 294 | PathBuf::from(TEST_DATA_DIR).join("signet").join(".cookie"), 295 | ), 296 | ( 297 | Some((TEST_DATA_DIR, false)), 298 | Some(Network::Bitcoin), 299 | None, 300 | PathBuf::from(TEST_DATA_DIR).join(".cookie"), 301 | ), 302 | ]; 303 | 304 | for (data_dir, network, cookie_file, expected_path) in test_cases { 305 | let path = get_cookie_path(data_dir, network, cookie_file).unwrap(); 306 | assert_eq!(path, expected_path); 307 | } 308 | } 309 | 310 | #[test] 311 | fn test_get_rpc_auth_from_cookie_success() { 312 | let (username, password) = get_rpc_auth_from_cookie( 313 | Some((TEST_DATA_DIR, false)), 314 | Some(Network::Bitcoin), 315 | Some(TEST_COOKIE), 316 | ) 317 | .unwrap(); 318 | assert_eq!(username, EXPECTED_USER); 319 | assert_eq!(password, EXPECTED_PASSWORD); 320 | } 321 | 322 | #[test] 323 | fn test_get_rpc_auth_from_cookie_fail() { 324 | let result = get_rpc_auth_from_cookie( 325 | Some((TEST_DATA_DIR, false)), 326 | Some(Network::Bitcoin), 327 | Some(TEST_COOKIE_BAD), 328 | ); 329 | assert!(result.is_err()); 330 | } 331 | 332 | #[test] 333 | fn test_parse_env_file_success() { 334 | let env_file_map = parse_env_file(Some(TEST_ENV_FILE)).unwrap(); 335 | assert_eq!(env_file_map.get(BITCOIND_RPC_USER_KEY).unwrap(), EXPECTED_USER); 336 | assert_eq!(env_file_map.get(BITCOIND_RPC_PASSWORD_KEY).unwrap(), EXPECTED_PASSWORD); 337 | } 338 | 339 | #[test] 340 | fn test_parse_env_file_fail() { 341 | let env_file_map = parse_env_file(Some(TEST_ENV_FILE_BAD)); 342 | assert!(env_file_map.is_err()); 343 | 344 | // Make sure the test file doesn't exist 345 | assert!(!Path::new(TEST_ABSENT_FILE).exists()); 346 | let env_file_map = parse_env_file(Some(TEST_ABSENT_FILE)); 347 | assert!(env_file_map.is_err()); 348 | } 349 | 350 | #[test] 351 | fn test_get_rpc_auth_from_env_file_success() { 352 | let (username, password) = get_rpc_auth_from_env_file(Some(TEST_ENV_FILE)).unwrap(); 353 | assert_eq!(username, EXPECTED_USER); 354 | assert_eq!(password, EXPECTED_PASSWORD); 355 | } 356 | 357 | #[test] 358 | fn test_get_rpc_auth_from_env_file_fail() { 359 | let rpc_user_and_password = get_rpc_auth_from_env_file(Some(TEST_ABSENT_FILE)); 360 | assert!(rpc_user_and_password.is_err()); 361 | } 362 | 363 | #[test] 364 | fn test_get_rpc_auth_from_env_vars_success() { 365 | env::set_var(BITCOIND_RPC_USER_KEY, EXPECTED_USER); 366 | env::set_var(BITCOIND_RPC_PASSWORD_KEY, EXPECTED_PASSWORD); 367 | let (username, password) = get_rpc_auth_from_env_vars().unwrap(); 368 | assert_eq!(username, EXPECTED_USER); 369 | assert_eq!(password, EXPECTED_PASSWORD); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/bdk_utils.rs: -------------------------------------------------------------------------------- 1 | use bdk::bitcoin::util::bip32::ExtendedPrivKey; 2 | use bdk::bitcoin::Network; 3 | use bdk::blockchain::Blockchain; 4 | use bdk::blockchain::{ConfigurableBlockchain, ElectrumBlockchain, ElectrumBlockchainConfig}; 5 | use bdk::database::any::SqliteDbConfiguration; 6 | use bdk::database::{ConfigurableDatabase, SqliteDatabase}; 7 | use bdk::template::P2Wpkh; 8 | use bdk::{SyncOptions, Wallet}; 9 | use bitcoin::secp256k1::SecretKey; 10 | use bitcoin::{PrivateKey, Transaction}; 11 | 12 | const DERIVATION_PATH_ACCOUNT: u32 = 0; 13 | const BDK_DB_NAME: &str = "bdk_db"; 14 | 15 | pub(crate) fn calculate_descriptor_from_xprv( 16 | xprv: ExtendedPrivKey, network: Network, change: bool, 17 | ) -> String { 18 | let change_num = u8::from(change); 19 | let coin_type = i32::from(network != Network::Bitcoin); 20 | let hardened = "'"; 21 | let child_number = "/*"; 22 | let master = ""; 23 | let derivation_path = 24 | format!("{master}/84{hardened}/{coin_type}{hardened}/{DERIVATION_PATH_ACCOUNT}{hardened}/{change_num}{child_number}"); 25 | format!("wpkh({xprv}{derivation_path})") 26 | } 27 | 28 | pub(crate) fn get_bdk_wallet( 29 | ldk_data_dir: String, xprv: ExtendedPrivKey, network: Network, 30 | ) -> Wallet { 31 | let descriptor = calculate_descriptor_from_xprv(xprv, network, false); 32 | let change_descriptor = calculate_descriptor_from_xprv(xprv, network, true); 33 | 34 | let bdk_db = format!("{ldk_data_dir}/{BDK_DB_NAME}"); 35 | let bdk_config = SqliteDbConfiguration { path: bdk_db }; 36 | let bdk_database = SqliteDatabase::from_config(&bdk_config).expect("valid bdk config"); 37 | 38 | Wallet::new(&descriptor, Some(&change_descriptor), network, bdk_database) 39 | .expect("valid bdk wallet") 40 | } 41 | 42 | pub(crate) fn get_bdk_wallet_seckey( 43 | ldk_data_dir: String, network: Network, seckey: SecretKey, 44 | ) -> Wallet { 45 | std::fs::create_dir_all(&ldk_data_dir).expect("successful dir creation"); 46 | let bdk_db = format!("{ldk_data_dir}/{BDK_DB_NAME}"); 47 | let bdk_config = SqliteDbConfiguration { path: bdk_db }; 48 | let bdk_database = SqliteDatabase::from_config(&bdk_config).expect("valid bdk config"); 49 | 50 | let priv_key = PrivateKey::new(seckey, network); 51 | Wallet::new(P2Wpkh(priv_key), None, network, bdk_database).expect("valid bdk wallet") 52 | } 53 | 54 | pub(crate) fn broadcast_tx(tx: &Transaction, electrum_url: String) { 55 | let config = ElectrumBlockchainConfig { 56 | url: electrum_url, 57 | socks5: None, 58 | retry: 3, 59 | timeout: Some(5), 60 | stop_gap: 2000, 61 | validate_domain: false, 62 | }; 63 | let blockchain = ElectrumBlockchain::from_config(&config).expect("valid blockchain config"); 64 | blockchain.broadcast(tx).expect("able to broadcast"); 65 | } 66 | 67 | pub(crate) fn sync_wallet(wallet: &Wallet, electrum_url: String) { 68 | let config = ElectrumBlockchainConfig { 69 | url: electrum_url, 70 | socks5: None, 71 | retry: 3, 72 | timeout: Some(5), 73 | stop_gap: 20, 74 | validate_domain: false, 75 | }; 76 | let blockchain = ElectrumBlockchain::from_config(&config).expect("valid blockchain config"); 77 | wallet.sync(&blockchain, SyncOptions { progress: None }).expect("successful sync") 78 | } 79 | -------------------------------------------------------------------------------- /src/bitcoind_client.rs: -------------------------------------------------------------------------------- 1 | use crate::convert::{BlockchainInfo, FeeResponse, Generated, NewAddress}; 2 | use crate::disk::FilesystemLogger; 3 | use base64; 4 | use bitcoin::blockdata::transaction::Transaction; 5 | use bitcoin::consensus::encode; 6 | use bitcoin::hash_types::{BlockHash, Txid}; 7 | use bitcoin::util::address::Address; 8 | use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; 9 | use lightning::log_error; 10 | use lightning::routing::utxo::{UtxoLookup, UtxoResult}; 11 | use lightning::util::logger::Logger; 12 | use lightning_block_sync::http::HttpEndpoint; 13 | use lightning_block_sync::rpc::RpcClient; 14 | use lightning_block_sync::{AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource}; 15 | use serde_json; 16 | use std::collections::HashMap; 17 | use std::str::FromStr; 18 | use std::sync::atomic::{AtomicU32, Ordering}; 19 | use std::sync::Arc; 20 | use std::time::Duration; 21 | 22 | pub struct BitcoindClient { 23 | bitcoind_rpc_client: Arc, 24 | host: String, 25 | port: u16, 26 | rpc_user: String, 27 | rpc_password: String, 28 | fees: Arc>, 29 | handle: tokio::runtime::Handle, 30 | logger: Arc, 31 | } 32 | 33 | #[derive(Clone, Eq, Hash, PartialEq)] 34 | pub enum Target { 35 | Background, 36 | Normal, 37 | HighPriority, 38 | } 39 | 40 | impl BlockSource for BitcoindClient { 41 | fn get_header<'a>( 42 | &'a self, header_hash: &'a BlockHash, height_hint: Option, 43 | ) -> AsyncBlockSourceResult<'a, BlockHeaderData> { 44 | Box::pin(async move { self.bitcoind_rpc_client.get_header(header_hash, height_hint).await }) 45 | } 46 | 47 | fn get_block<'a>( 48 | &'a self, header_hash: &'a BlockHash, 49 | ) -> AsyncBlockSourceResult<'a, BlockData> { 50 | Box::pin(async move { self.bitcoind_rpc_client.get_block(header_hash).await }) 51 | } 52 | 53 | fn get_best_block<'a>(&'a self) -> AsyncBlockSourceResult<(BlockHash, Option)> { 54 | Box::pin(async move { self.bitcoind_rpc_client.get_best_block().await }) 55 | } 56 | } 57 | 58 | /// The minimum feerate we are allowed to send, as specify by LDK. 59 | const MIN_FEERATE: u32 = 253; 60 | 61 | impl BitcoindClient { 62 | pub(crate) async fn new( 63 | host: String, port: u16, rpc_user: String, rpc_password: String, 64 | handle: tokio::runtime::Handle, logger: Arc, 65 | ) -> std::io::Result { 66 | let http_endpoint = HttpEndpoint::for_host(host.clone()).with_port(port); 67 | let rpc_credentials = 68 | base64::encode(format!("{}:{}", rpc_user.clone(), rpc_password.clone())); 69 | let bitcoind_rpc_client = RpcClient::new(&rpc_credentials, http_endpoint)?; 70 | let _dummy = bitcoind_rpc_client 71 | .call_method::("getblockchaininfo", &vec![]) 72 | .await 73 | .map_err(|_| { 74 | std::io::Error::new(std::io::ErrorKind::PermissionDenied, 75 | "Failed to make initial call to bitcoind - please check your RPC user/password and access settings") 76 | })?; 77 | let mut fees: HashMap = HashMap::new(); 78 | fees.insert(Target::Background, AtomicU32::new(MIN_FEERATE)); 79 | fees.insert(Target::Normal, AtomicU32::new(2000)); 80 | fees.insert(Target::HighPriority, AtomicU32::new(5000)); 81 | let client = Self { 82 | bitcoind_rpc_client: Arc::new(bitcoind_rpc_client), 83 | host, 84 | port, 85 | rpc_user, 86 | rpc_password, 87 | fees: Arc::new(fees), 88 | handle: handle.clone(), 89 | logger, 90 | }; 91 | BitcoindClient::poll_for_fee_estimates( 92 | client.fees.clone(), 93 | client.bitcoind_rpc_client.clone(), 94 | handle, 95 | ); 96 | Ok(client) 97 | } 98 | 99 | fn poll_for_fee_estimates( 100 | fees: Arc>, rpc_client: Arc, 101 | handle: tokio::runtime::Handle, 102 | ) { 103 | handle.spawn(async move { 104 | loop { 105 | let background_estimate = { 106 | let background_conf_target = serde_json::json!(144); 107 | let background_estimate_mode = serde_json::json!("ECONOMICAL"); 108 | let resp = rpc_client 109 | .call_method::( 110 | "estimatesmartfee", 111 | &vec![background_conf_target, background_estimate_mode], 112 | ) 113 | .await 114 | .unwrap(); 115 | match resp.feerate_sat_per_kw { 116 | Some(feerate) => std::cmp::max(feerate, MIN_FEERATE), 117 | None => MIN_FEERATE, 118 | } 119 | }; 120 | 121 | let normal_estimate = { 122 | let normal_conf_target = serde_json::json!(18); 123 | let normal_estimate_mode = serde_json::json!("ECONOMICAL"); 124 | let resp = rpc_client 125 | .call_method::( 126 | "estimatesmartfee", 127 | &vec![normal_conf_target, normal_estimate_mode], 128 | ) 129 | .await 130 | .unwrap(); 131 | match resp.feerate_sat_per_kw { 132 | Some(feerate) => std::cmp::max(feerate, MIN_FEERATE), 133 | None => 2000, 134 | } 135 | }; 136 | 137 | let high_prio_estimate = { 138 | let high_prio_conf_target = serde_json::json!(6); 139 | let high_prio_estimate_mode = serde_json::json!("CONSERVATIVE"); 140 | let resp = rpc_client 141 | .call_method::( 142 | "estimatesmartfee", 143 | &vec![high_prio_conf_target, high_prio_estimate_mode], 144 | ) 145 | .await 146 | .unwrap(); 147 | 148 | match resp.feerate_sat_per_kw { 149 | Some(feerate) => std::cmp::max(feerate, MIN_FEERATE), 150 | None => 5000, 151 | } 152 | }; 153 | 154 | fees.get(&Target::Background) 155 | .unwrap() 156 | .store(background_estimate, Ordering::Release); 157 | fees.get(&Target::Normal).unwrap().store(normal_estimate, Ordering::Release); 158 | fees.get(&Target::HighPriority) 159 | .unwrap() 160 | .store(high_prio_estimate, Ordering::Release); 161 | tokio::time::sleep(Duration::from_secs(60)).await; 162 | } 163 | }); 164 | } 165 | 166 | pub fn get_new_rpc_client(&self) -> std::io::Result { 167 | let http_endpoint = HttpEndpoint::for_host(self.host.clone()).with_port(self.port); 168 | let rpc_credentials = 169 | base64::encode(format!("{}:{}", self.rpc_user.clone(), self.rpc_password.clone())); 170 | RpcClient::new(&rpc_credentials, http_endpoint) 171 | } 172 | 173 | pub async fn get_new_address(&self) -> Address { 174 | let addr_args = vec![serde_json::json!("LDK output address")]; 175 | let addr = self 176 | .bitcoind_rpc_client 177 | .call_method::("getnewaddress", &addr_args) 178 | .await 179 | .unwrap(); 180 | Address::from_str(addr.0.as_str()).unwrap() 181 | } 182 | 183 | pub async fn get_blockchain_info(&self) -> BlockchainInfo { 184 | self.bitcoind_rpc_client 185 | .call_method::("getblockchaininfo", &vec![]) 186 | .await 187 | .unwrap() 188 | } 189 | 190 | pub async fn generate_to_adress(&self, blocks: u16, address: String) -> Generated { 191 | let blocks = serde_json::json!(blocks); 192 | let address = serde_json::json!(address); 193 | self.bitcoind_rpc_client 194 | .call_method::("generatetoaddress", &vec![blocks, address]) 195 | .await 196 | .unwrap() 197 | } 198 | } 199 | 200 | impl FeeEstimator for BitcoindClient { 201 | fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 { 202 | match confirmation_target { 203 | ConfirmationTarget::Background => { 204 | self.fees.get(&Target::Background).unwrap().load(Ordering::Acquire) 205 | } 206 | ConfirmationTarget::Normal => { 207 | self.fees.get(&Target::Normal).unwrap().load(Ordering::Acquire) 208 | } 209 | ConfirmationTarget::HighPriority => { 210 | self.fees.get(&Target::HighPriority).unwrap().load(Ordering::Acquire) 211 | } 212 | } 213 | } 214 | } 215 | 216 | impl BroadcasterInterface for BitcoindClient { 217 | fn broadcast_transaction(&self, tx: &Transaction) { 218 | let bitcoind_rpc_client = self.bitcoind_rpc_client.clone(); 219 | let tx_serialized = encode::serialize_hex(tx); 220 | let tx_json = serde_json::json!(tx_serialized); 221 | let logger = Arc::clone(&self.logger); 222 | self.handle.spawn(async move { 223 | // This may error due to RL calling `broadcast_transaction` with the same transaction 224 | // multiple times, but the error is safe to ignore. 225 | match bitcoind_rpc_client 226 | .call_method::("sendrawtransaction", &vec![tx_json]) 227 | .await 228 | { 229 | Ok(_) => {} 230 | Err(e) => { 231 | let err_str = e.get_ref().unwrap().to_string(); 232 | log_error!(logger, 233 | "Warning, failed to broadcast a transaction, this is likely okay but may indicate an error: {}\nTransaction: {}", 234 | err_str, 235 | tx_serialized); 236 | print!("Warning, failed to broadcast a transaction, this is likely okay but may indicate an error: {}\n> ", err_str); 237 | } 238 | } 239 | }); 240 | } 241 | } 242 | 243 | impl UtxoLookup for BitcoindClient { 244 | fn get_utxo(&self, _genesis_hash: &BlockHash, _short_channel_id: u64) -> UtxoResult { 245 | // P2PGossipSync takes None for a UtxoLookup, so this will never be called. 246 | todo!(); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/convert.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::hashes::hex::FromHex; 2 | use bitcoin::BlockHash; 3 | use lightning_block_sync::http::JsonResponse; 4 | use std::convert::TryInto; 5 | 6 | pub struct NewAddress(pub String); 7 | impl TryInto for JsonResponse { 8 | type Error = std::io::Error; 9 | fn try_into(self) -> std::io::Result { 10 | Ok(NewAddress(self.0.as_str().unwrap().to_string())) 11 | } 12 | } 13 | 14 | pub struct FeeResponse { 15 | pub feerate_sat_per_kw: Option, 16 | pub errored: bool, 17 | } 18 | 19 | impl TryInto for JsonResponse { 20 | type Error = std::io::Error; 21 | fn try_into(self) -> std::io::Result { 22 | let errored = !self.0["errors"].is_null(); 23 | Ok(FeeResponse { 24 | errored, 25 | feerate_sat_per_kw: match self.0["feerate"].as_f64() { 26 | // Bitcoin Core gives us a feerate in BTC/KvB, which we need to convert to 27 | // satoshis/KW. Thus, we first multiply by 10^8 to get satoshis, then divide by 4 28 | // to convert virtual-bytes into weight units. 29 | Some(feerate_btc_per_kvbyte) => { 30 | Some((feerate_btc_per_kvbyte * 100_000_000.0 / 4.0).round() as u32) 31 | } 32 | None => None, 33 | }, 34 | }) 35 | } 36 | } 37 | 38 | pub struct BlockchainInfo { 39 | pub latest_height: usize, 40 | pub latest_blockhash: BlockHash, 41 | pub chain: String, 42 | } 43 | 44 | impl TryInto for JsonResponse { 45 | type Error = std::io::Error; 46 | fn try_into(self) -> std::io::Result { 47 | Ok(BlockchainInfo { 48 | latest_height: self.0["blocks"].as_u64().unwrap() as usize, 49 | latest_blockhash: BlockHash::from_hex(self.0["bestblockhash"].as_str().unwrap()) 50 | .unwrap(), 51 | chain: self.0["chain"].as_str().unwrap().to_string(), 52 | }) 53 | } 54 | } 55 | 56 | pub struct Generated(Vec); 57 | 58 | impl TryInto for JsonResponse { 59 | type Error = std::io::Error; 60 | fn try_into(self) -> std::io::Result { 61 | let generated: Vec = 62 | serde_json::from_value(self.0).expect("valid generated response"); 63 | Ok(Generated(generated)) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/disk.rs: -------------------------------------------------------------------------------- 1 | use crate::{cli, NetworkGraph}; 2 | use bitcoin::secp256k1::PublicKey; 3 | use bitcoin::Network; 4 | use chrono::Utc; 5 | use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringParameters}; 6 | use lightning::util::logger::{Logger, Record}; 7 | use lightning::util::ser::{ReadableArgs, Writer}; 8 | use std::collections::HashMap; 9 | use std::fs; 10 | use std::fs::File; 11 | use std::io::{BufRead, BufReader}; 12 | use std::net::SocketAddr; 13 | use std::path::Path; 14 | use std::sync::Arc; 15 | 16 | pub(crate) struct FilesystemLogger { 17 | data_dir: String, 18 | } 19 | impl FilesystemLogger { 20 | pub(crate) fn new(data_dir: String) -> Self { 21 | let logs_path = format!("{}/logs", data_dir); 22 | fs::create_dir_all(logs_path.clone()).unwrap(); 23 | Self { data_dir: logs_path } 24 | } 25 | } 26 | impl Logger for FilesystemLogger { 27 | fn log(&self, record: &Record) { 28 | let raw_log = record.args.to_string(); 29 | let log = format!( 30 | "{} {:<5} [{}:{}] {}\n", 31 | // Note that a "real" lightning node almost certainly does *not* want subsecond 32 | // precision for message-receipt information as it makes log entries a target for 33 | // deanonymization attacks. For testing, however, its quite useful. 34 | Utc::now().format("%Y-%m-%d %H:%M:%S%.3f"), 35 | record.level.to_string(), 36 | record.module_path, 37 | record.line, 38 | raw_log 39 | ); 40 | let logs_file_path = format!("{}/logs.txt", self.data_dir.clone()); 41 | fs::OpenOptions::new() 42 | .create(true) 43 | .append(true) 44 | .open(logs_file_path) 45 | .unwrap() 46 | .write_all(log.as_bytes()) 47 | .unwrap(); 48 | } 49 | } 50 | pub(crate) fn persist_channel_peer(path: &Path, peer_info: &str) -> std::io::Result<()> { 51 | let mut file = fs::OpenOptions::new().create(true).append(true).open(path)?; 52 | file.write_all(format!("{}\n", peer_info).as_bytes()) 53 | } 54 | 55 | pub(crate) fn read_channel_peer_data( 56 | path: &Path, 57 | ) -> Result, std::io::Error> { 58 | let mut peer_data = HashMap::new(); 59 | if !Path::new(&path).exists() { 60 | return Ok(HashMap::new()); 61 | } 62 | let file = File::open(path)?; 63 | let reader = BufReader::new(file); 64 | for line in reader.lines() { 65 | match cli::parse_peer_info(line.unwrap()) { 66 | Ok((pubkey, socket_addr)) => { 67 | peer_data.insert(pubkey, socket_addr); 68 | } 69 | Err(e) => return Err(e), 70 | } 71 | } 72 | Ok(peer_data) 73 | } 74 | 75 | pub(crate) fn read_network( 76 | path: &Path, network: Network, logger: Arc, 77 | ) -> NetworkGraph { 78 | if let Ok(file) = File::open(path) { 79 | if let Ok(graph) = NetworkGraph::read(&mut BufReader::new(file), logger.clone()) { 80 | return graph; 81 | } 82 | } 83 | NetworkGraph::new(network, logger) 84 | } 85 | 86 | pub(crate) fn read_scorer( 87 | path: &Path, graph: Arc, logger: Arc, 88 | ) -> ProbabilisticScorer, Arc> { 89 | let params = ProbabilisticScoringParameters::default(); 90 | if let Ok(file) = File::open(path) { 91 | let args = (params.clone(), Arc::clone(&graph), Arc::clone(&logger)); 92 | if let Ok(scorer) = ProbabilisticScorer::read(&mut BufReader::new(file), args) { 93 | return scorer; 94 | } 95 | } 96 | ProbabilisticScorer::new(params, graph, logger) 97 | } 98 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /// The error variants returned by functions 2 | #[derive(Debug, thiserror::Error)] 3 | pub enum Error { 4 | #[error("IO error: {0}")] 5 | IO(#[from] std::io::Error), 6 | 7 | #[error("Proxy error: {0}")] 8 | Proxy(#[from] reqwest::Error), 9 | 10 | #[error("ERROR: no uncolored UTXOs are available (hint: call createutxos)")] 11 | NoAvailableUtxos, 12 | 13 | #[error("ERROR: unknown RGB contract ID")] 14 | UnknownContractId, 15 | } 16 | -------------------------------------------------------------------------------- /src/hex_utils.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::secp256k1::PublicKey; 2 | use std::fmt::Write; 3 | 4 | pub fn to_vec(hex: &str) -> Option> { 5 | let mut out = Vec::with_capacity(hex.len() / 2); 6 | 7 | let mut b = 0; 8 | for (idx, c) in hex.as_bytes().iter().enumerate() { 9 | b <<= 4; 10 | match *c { 11 | b'A'..=b'F' => b |= c - b'A' + 10, 12 | b'a'..=b'f' => b |= c - b'a' + 10, 13 | b'0'..=b'9' => b |= c - b'0', 14 | _ => return None, 15 | } 16 | if (idx & 1) == 1 { 17 | out.push(b); 18 | b = 0; 19 | } 20 | } 21 | 22 | Some(out) 23 | } 24 | 25 | #[inline] 26 | pub fn hex_str(value: &[u8]) -> String { 27 | let mut res = String::with_capacity(2 * value.len()); 28 | for v in value { 29 | write!(&mut res, "{:02x}", v).expect("Unable to write"); 30 | } 31 | res 32 | } 33 | 34 | pub fn to_compressed_pubkey(hex: &str) -> Option { 35 | if hex.len() != 33 * 2 { 36 | return None; 37 | } 38 | let data = match to_vec(&hex[0..33 * 2]) { 39 | Some(bytes) => bytes, 40 | None => return None, 41 | }; 42 | match PublicKey::from_slice(&data) { 43 | Ok(pk) => Some(pk), 44 | Err(_) => None, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[allow(deprecated)] 2 | mod args; 3 | mod bdk_utils; 4 | pub mod bitcoind_client; 5 | mod cli; 6 | mod convert; 7 | mod disk; 8 | mod error; 9 | mod hex_utils; 10 | mod proxy; 11 | mod rgb_utils; 12 | mod swap; 13 | 14 | use crate::bdk_utils::{broadcast_tx, get_bdk_wallet, get_bdk_wallet_seckey, sync_wallet}; 15 | use crate::bitcoind_client::BitcoindClient; 16 | use crate::cli::HTLC_MIN_MSAT; 17 | use crate::disk::FilesystemLogger; 18 | use crate::proxy::post_consignment; 19 | use crate::rgb_utils::{get_asset_owned_values, update_transition_beneficiary, RgbUtilities}; 20 | use crate::swap::SwapType; 21 | use bdk::bitcoin::psbt::PartiallySignedTransaction; 22 | use bdk::bitcoin::OutPoint; 23 | use bdk::bitcoin::Txid; 24 | use bdk::database::SqliteDatabase; 25 | use bdk::wallet::AddressIndex; 26 | use bdk::Wallet; 27 | use bdk::{FeeRate, SignOptions}; 28 | use bitcoin::blockdata::transaction::Transaction; 29 | use bitcoin::hashes::hex::FromHex; 30 | use bitcoin::network::constants::Network; 31 | use bitcoin::secp256k1::Secp256k1; 32 | use bitcoin::util::bip32::{ChildNumber, ExtendedPrivKey}; 33 | use bitcoin::{BlockHash, PackedLockTime, Script, Sequence, TxIn, TxOut, Witness}; 34 | use bitcoin_bech32::WitnessProgram; 35 | use bp::seals::txout::CloseMethod; 36 | use lightning::chain; 37 | use lightning::chain::keysinterface::{ 38 | DelayedPaymentOutputDescriptor, EntropySource, InMemorySigner, KeysManager, 39 | SpendableOutputDescriptor, 40 | }; 41 | use lightning::chain::{chainmonitor, ChannelMonitorUpdateStatus}; 42 | use lightning::chain::{Filter, Watch}; 43 | use lightning::events::{Event, PaymentFailureReason, PaymentPurpose}; 44 | use lightning::ln::channelmanager; 45 | use lightning::ln::channelmanager::{ 46 | ChainParameters, ChannelManagerReadArgs, SimpleArcChannelManager, 47 | }; 48 | use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler, SimpleArcPeerManager}; 49 | use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; 50 | use lightning::onion_message::SimpleArcOnionMessenger; 51 | use lightning::rgb_utils::{ 52 | drop_rgb_runtime, get_rgb_channel_info, get_rgb_runtime, is_channel_rgb, is_transfer_colored, 53 | read_rgb_transfer_info, RgbInfo, RgbUtxo, RgbUtxos, STATIC_BLINDING, 54 | }; 55 | use lightning::routing::gossip; 56 | use lightning::routing::gossip::{NodeId, P2PGossipSync}; 57 | use lightning::routing::router::DefaultRouter; 58 | use lightning::routing::scoring::ProbabilisticScorerUsingTime; 59 | use lightning::util::config::UserConfig; 60 | use lightning::util::ser::ReadableArgs; 61 | use lightning_background_processor::{process_events_async, GossipSync}; 62 | use lightning_block_sync::init; 63 | use lightning_block_sync::poll; 64 | use lightning_block_sync::SpvClient; 65 | use lightning_block_sync::UnboundedCache; 66 | use lightning_net_tokio::SocketDescriptor; 67 | use lightning_persister::FilesystemPersister; 68 | use rand::{thread_rng, Rng}; 69 | use reqwest::Client as RestClient; 70 | use rgb::validation::ConsignmentApi; 71 | use rgb_core::Assign; 72 | use rgb_schemata::{nia_rgb20, nia_schema}; 73 | use rgbstd::containers::{Bindle, BuilderSeal, Transfer as RgbTransfer}; 74 | use rgbstd::contract::{ContractId, GraphSeal}; 75 | use rgbstd::interface::{rgb20, TransitionBuilder, TypedState}; 76 | use rgbstd::persistence::{Inventory, Stash}; 77 | use rgbstd::validation::Validity; 78 | use rgbstd::{Chain, Txid as RgbTxid}; 79 | use seals::txout::blind::SingleBlindSeal; 80 | use std::collections::hash_map::Entry; 81 | use std::collections::HashMap; 82 | use std::convert::{TryFrom, TryInto}; 83 | use std::fmt; 84 | use std::fs; 85 | use std::fs::File; 86 | use std::io; 87 | use std::io::Write; 88 | use std::path::{Path, PathBuf}; 89 | use std::str::FromStr; 90 | use std::sync::atomic::{AtomicBool, Ordering}; 91 | use std::sync::{Arc, Mutex}; 92 | use std::time::{Duration, SystemTime}; 93 | use strict_encoding::{FieldName, TypeName}; 94 | 95 | const FEE_RATE: f32 = 10.0; 96 | const ELECTRUM_URL_REGTEST: &str = "127.0.0.1:50001"; 97 | const ELECTRUM_URL_TESTNET: &str = "ssl://electrum.iriswallet.com:50013"; 98 | const PROXY_ENDPOINT_REGTEST: &str = "rpc://127.0.0.1:3000/json-rpc"; 99 | const PROXY_URL_REGTEST: &str = "http://127.0.0.1:3000/json-rpc"; 100 | const PROXY_ENDPOINT_TESTNET: &str = "rpcs://proxy.iriswallet.com/json-rpc"; 101 | const PROXY_URL_TESTNET: &str = "https://proxy.iriswallet.com/json-rpc"; 102 | const PROXY_TIMEOUT: u8 = 90; 103 | const UTXO_SIZE_SAT: u64 = 32000; 104 | 105 | pub(crate) enum HTLCStatus { 106 | Pending, 107 | Succeeded, 108 | Failed, 109 | } 110 | 111 | pub(crate) struct MillisatAmount(Option); 112 | 113 | impl fmt::Display for MillisatAmount { 114 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 115 | match self.0 { 116 | Some(amt) => write!(f, "{}", amt), 117 | None => write!(f, "unknown"), 118 | } 119 | } 120 | } 121 | 122 | pub(crate) struct PaymentInfo { 123 | preimage: Option, 124 | secret: Option, 125 | status: HTLCStatus, 126 | amt_msat: MillisatAmount, 127 | } 128 | 129 | pub(crate) type PaymentInfoStorage = Arc>>; 130 | 131 | type ChainMonitor = chainmonitor::ChainMonitor< 132 | InMemorySigner, 133 | Arc, 134 | Arc, 135 | Arc, 136 | Arc, 137 | Arc, 138 | >; 139 | 140 | pub(crate) type PeerManager = SimpleArcPeerManager< 141 | SocketDescriptor, 142 | ChainMonitor, 143 | BitcoindClient, 144 | BitcoindClient, 145 | BitcoindClient, 146 | FilesystemLogger, 147 | >; 148 | 149 | pub(crate) type Scorer = 150 | ProbabilisticScorerUsingTime, Arc, std::time::Instant>; 151 | 152 | pub(crate) type Router = 153 | DefaultRouter, Arc, Arc>>; 154 | 155 | pub(crate) type ChannelManager = 156 | SimpleArcChannelManager; 157 | 158 | pub(crate) type NetworkGraph = gossip::NetworkGraph>; 159 | 160 | type OnionMessenger = SimpleArcOnionMessenger; 161 | 162 | async fn handle_ldk_events( 163 | channel_manager: &Arc, network_graph: &NetworkGraph, 164 | keys_manager: &KeysManager, inbound_payments: &PaymentInfoStorage, 165 | outbound_payments: &PaymentInfoStorage, network: Network, event: Event, ldk_data_dir: String, 166 | proxy_client: Arc, proxy_url: String, 167 | wallet_arc: Arc>>, electrum_url: String, 168 | whitelisted_trades: &Arc>>, 169 | maker_trades: &Arc>>, 170 | ) { 171 | match event { 172 | Event::FundingGenerationReady { 173 | temporary_channel_id, 174 | counterparty_node_id, 175 | channel_value_satoshis, 176 | output_script, 177 | .. 178 | } => { 179 | struct RgbState { 180 | rgb_change_amount: u64, 181 | channel_rgb_amount: u64, 182 | rgb_inputs: Vec, 183 | rgb_info: RgbInfo, 184 | asset_transition_builder: TransitionBuilder, 185 | assignment_id: u16, 186 | } 187 | 188 | let addr = WitnessProgram::from_scriptpubkey( 189 | &output_script[..], 190 | match network { 191 | Network::Bitcoin => bitcoin_bech32::constants::Network::Bitcoin, 192 | Network::Testnet => bitcoin_bech32::constants::Network::Testnet, 193 | Network::Regtest => bitcoin_bech32::constants::Network::Regtest, 194 | Network::Signet => bitcoin_bech32::constants::Network::Signet, 195 | }, 196 | ) 197 | .expect("Lightning funding tx should always be to a SegWit output") 198 | .to_scriptpubkey(); 199 | let script = Script::from_byte_iter(addr.into_iter().map(Ok)).expect("valid script"); 200 | 201 | let ldk_data_dir = PathBuf::from(&ldk_data_dir); 202 | let is_colored = is_channel_rgb(&temporary_channel_id, &ldk_data_dir); 203 | let mut beneficiaries = vec![]; 204 | let rgb_state = if is_colored { 205 | let (rgb_info, _) = 206 | get_rgb_channel_info(&temporary_channel_id, &PathBuf::from(&ldk_data_dir)); 207 | 208 | let mut runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); 209 | 210 | let channel_rgb_amount: u64 = rgb_info.local_rgb_amount; 211 | let asset_owned_values = get_asset_owned_values( 212 | rgb_info.contract_id, 213 | &runtime, 214 | wallet_arc.clone(), 215 | electrum_url, 216 | ) 217 | .expect("known contract"); 218 | 219 | let asset_transition_builder = runtime 220 | .transition_builder( 221 | rgb_info.contract_id, 222 | TypeName::try_from("RGB20").unwrap(), 223 | None::<&str>, 224 | ) 225 | .expect("ok"); 226 | let assignment_id = asset_transition_builder 227 | .assignments_type(&FieldName::from("beneficiary")) 228 | .expect("valid assignment"); 229 | 230 | let mut rgb_inputs: Vec = vec![]; 231 | let mut input_amount: u64 = 0; 232 | for (_opout, (outpoint, amount)) in asset_owned_values { 233 | if input_amount >= channel_rgb_amount { 234 | break; 235 | } 236 | rgb_inputs.push(OutPoint { 237 | txid: Txid::from_str(&outpoint.txid.to_string()).unwrap(), 238 | vout: outpoint.vout.into_u32(), 239 | }); 240 | input_amount += amount; 241 | } 242 | let rgb_change_amount = input_amount - channel_rgb_amount; 243 | drop(runtime); 244 | drop_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); 245 | 246 | Some(RgbState { 247 | channel_rgb_amount, 248 | rgb_change_amount, 249 | rgb_inputs, 250 | rgb_info, 251 | asset_transition_builder, 252 | assignment_id, 253 | }) 254 | } else { 255 | None 256 | }; 257 | 258 | let rgb_utxos_path = format!("{}/rgb_utxos", ldk_data_dir.display()); 259 | let serialized_utxos = 260 | fs::read_to_string(&rgb_utxos_path).expect("able to read rgb utxos file"); 261 | let mut rgb_utxos: RgbUtxos = 262 | serde_json::from_str(&serialized_utxos).expect("valid rgb utxos"); 263 | let unspendable_utxos: Vec = rgb_utxos 264 | .utxos 265 | .iter() 266 | .filter(|u| { 267 | rgb_state 268 | .as_ref() 269 | .map(|state| !state.rgb_inputs.contains(&u.outpoint) || !u.colored) 270 | .unwrap_or(true) 271 | }) 272 | .map(|u| u.outpoint) 273 | .collect(); 274 | let wallet = wallet_arc.lock().unwrap(); 275 | let mut builder = wallet.build_tx(); 276 | if let Some(rgb_state) = &rgb_state { 277 | builder.add_utxos(&rgb_state.rgb_inputs).expect("valid utxos"); 278 | } 279 | builder 280 | .unspendable(unspendable_utxos) 281 | .fee_rate(FeeRate::from_sat_per_vb(FEE_RATE)) 282 | .ordering(bdk::wallet::tx_builder::TxOrdering::Untouched) 283 | .add_recipient(script, channel_value_satoshis) 284 | .drain_to( 285 | wallet 286 | .get_address(AddressIndex::New) 287 | .expect("able to get new address") 288 | .address 289 | .script_pubkey(), 290 | ) 291 | .add_data(&[1]); 292 | 293 | let psbt = builder.finish().expect("valid psbt finish").0; 294 | let (mut psbt, state_and_consignment_and_change_vout) = match rgb_state { 295 | Some(mut rgb_state) => { 296 | let funding_seal = BuilderSeal::Revealed(GraphSeal::with_vout( 297 | CloseMethod::OpretFirst, 298 | 0, 299 | STATIC_BLINDING, 300 | )); 301 | beneficiaries.push(funding_seal); 302 | rgb_state.asset_transition_builder = rgb_state 303 | .asset_transition_builder 304 | .add_raw_state_static( 305 | rgb_state.assignment_id, 306 | funding_seal, 307 | TypedState::Amount(rgb_state.channel_rgb_amount), 308 | ) 309 | .expect("ok"); 310 | 311 | let change_vout = 2; 312 | if rgb_state.rgb_change_amount > 0 { 313 | let change_seal = BuilderSeal::Revealed(GraphSeal::with_vout( 314 | CloseMethod::OpretFirst, 315 | change_vout, 316 | STATIC_BLINDING, 317 | )); 318 | beneficiaries.push(change_seal); 319 | rgb_state.asset_transition_builder = rgb_state 320 | .asset_transition_builder 321 | .add_raw_state_static( 322 | rgb_state.assignment_id, 323 | change_seal, 324 | TypedState::Amount(rgb_state.rgb_change_amount), 325 | ) 326 | .expect("ok"); 327 | } 328 | let mut runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); 329 | 330 | let (psbt, consignment) = runtime.send_rgb( 331 | rgb_state.rgb_info.contract_id, 332 | psbt, 333 | rgb_state.asset_transition_builder.clone(), 334 | beneficiaries, 335 | ); 336 | drop(runtime); 337 | drop_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); 338 | (psbt, Some((rgb_state, consignment, change_vout))) 339 | } 340 | None => (psbt, None), 341 | }; 342 | 343 | // Sign the final funding transaction 344 | wallet.sign(&mut psbt, SignOptions::default()).expect("able to sign"); 345 | let funding_tx = psbt.extract_tx(); 346 | let funding_txid = funding_tx.txid(); 347 | 348 | let consignment_path = format!("{}/consignment_{funding_txid}", ldk_data_dir.display()); 349 | if let Some((rgb_state, consignment, change_vout)) = 350 | &state_and_consignment_and_change_vout 351 | { 352 | consignment.save(&consignment_path).expect("successful save"); 353 | 354 | if rgb_state.rgb_change_amount > 0 { 355 | let rgb_change_utxo = RgbUtxo { 356 | outpoint: OutPoint { txid: funding_txid, vout: *change_vout }, 357 | colored: true, 358 | }; 359 | rgb_utxos.utxos.push(rgb_change_utxo); 360 | let serialized_utxos = 361 | serde_json::to_string(&rgb_utxos).expect("valid rgb utxos"); 362 | fs::write(rgb_utxos_path, serialized_utxos) 363 | .expect("able to write rgb utxos file"); 364 | } 365 | let funding_consignment_path = format!( 366 | "{}/consignment_{}", 367 | ldk_data_dir.display(), 368 | hex::encode(temporary_channel_id) 369 | ); 370 | consignment.save(funding_consignment_path).expect("successful save"); 371 | } 372 | 373 | let proxy_ref = (*proxy_client).clone(); 374 | let proxy_url_copy = proxy_url; 375 | let channel_manager_copy = channel_manager.clone(); 376 | tokio::spawn(async move { 377 | if let Some((_, consignment, _)) = state_and_consignment_and_change_vout { 378 | let res = post_consignment( 379 | proxy_ref, 380 | &proxy_url_copy, 381 | funding_txid.to_string(), 382 | consignment_path.into(), 383 | ) 384 | .await; 385 | if res.is_err() || res.unwrap().result.is_none() { 386 | return; 387 | } 388 | 389 | let mut runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); 390 | let transfer: RgbTransfer = consignment.unbindle(); 391 | let validated_transfer = match transfer.validate(runtime.resolver()) { 392 | Ok(consignment) => consignment, 393 | Err(consignment) => consignment, 394 | }; 395 | let validation_status = validated_transfer.into_validation_status().unwrap(); 396 | let validity = validation_status.validity(); 397 | if !vec![Validity::Valid, Validity::UnminedTerminals].contains(&validity) { 398 | return; 399 | } 400 | 401 | drop(runtime); 402 | drop_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); 403 | } 404 | 405 | // Give the funding transaction back to LDK for opening the channel. 406 | if channel_manager_copy 407 | .funding_transaction_generated( 408 | &temporary_channel_id, 409 | &counterparty_node_id, 410 | funding_tx, 411 | ) 412 | .is_err() 413 | { 414 | println!( 415 | "\nERROR: Channel went away before we could fund it. The peer disconnected or refused the channel."); 416 | } else { 417 | println!("FUNDING COMPLETED"); 418 | } 419 | 420 | print!("> "); 421 | io::stdout().flush().unwrap(); 422 | }); 423 | } 424 | Event::PaymentClaimable { 425 | payment_hash, 426 | purpose, 427 | amount_msat, 428 | receiver_node_id: _, 429 | via_channel_id: _, 430 | via_user_channel_id: _, 431 | claim_deadline: _, 432 | onion_fields: _, 433 | } => { 434 | println!( 435 | "\nEVENT: received payment from payment hash {} of {} millisatoshis", 436 | hex_utils::hex_str(&payment_hash.0), 437 | amount_msat, 438 | ); 439 | print!("> "); 440 | io::stdout().flush().unwrap(); 441 | let payment_preimage = match purpose { 442 | PaymentPurpose::InvoicePayment { payment_preimage, .. } => payment_preimage, 443 | PaymentPurpose::SpontaneousPayment(preimage) => Some(preimage), 444 | }; 445 | channel_manager.claim_funds(payment_preimage.unwrap()); 446 | } 447 | Event::PaymentClaimed { payment_hash, purpose, amount_msat, receiver_node_id: _ } => { 448 | println!( 449 | "\nEVENT: claimed payment from payment hash {} of {} millisatoshis", 450 | hex_utils::hex_str(&payment_hash.0), 451 | amount_msat, 452 | ); 453 | print!("> "); 454 | io::stdout().flush().unwrap(); 455 | let (payment_preimage, payment_secret) = match purpose { 456 | PaymentPurpose::InvoicePayment { payment_preimage, payment_secret, .. } => { 457 | (payment_preimage, Some(payment_secret)) 458 | } 459 | PaymentPurpose::SpontaneousPayment(preimage) => (Some(preimage), None), 460 | }; 461 | let mut payments = inbound_payments.lock().unwrap(); 462 | match payments.entry(payment_hash) { 463 | Entry::Occupied(mut e) => { 464 | let payment = e.get_mut(); 465 | payment.status = HTLCStatus::Succeeded; 466 | payment.preimage = payment_preimage; 467 | payment.secret = payment_secret; 468 | } 469 | Entry::Vacant(e) => { 470 | e.insert(PaymentInfo { 471 | preimage: payment_preimage, 472 | secret: payment_secret, 473 | status: HTLCStatus::Succeeded, 474 | amt_msat: MillisatAmount(Some(amount_msat)), 475 | }); 476 | } 477 | } 478 | println!("Event::PaymentClaimed end"); 479 | 480 | maker_trades.lock().unwrap().remove(&payment_hash); 481 | } 482 | Event::PaymentSent { payment_preimage, payment_hash, fee_paid_msat, .. } => { 483 | let mut payments = outbound_payments.lock().unwrap(); 484 | for (hash, payment) in payments.iter_mut() { 485 | if *hash == payment_hash { 486 | payment.preimage = Some(payment_preimage); 487 | payment.status = HTLCStatus::Succeeded; 488 | println!( 489 | "\nEVENT: successfully sent payment of {} millisatoshis{} from \ 490 | payment hash {:?} with preimage {:?}", 491 | payment.amt_msat, 492 | if let Some(fee) = fee_paid_msat { 493 | format!(" (fee {} msat)", fee) 494 | } else { 495 | "".to_string() 496 | }, 497 | hex_utils::hex_str(&payment_hash.0), 498 | hex_utils::hex_str(&payment_preimage.0) 499 | ); 500 | print!("> "); 501 | io::stdout().flush().unwrap(); 502 | } 503 | } 504 | print!("> "); 505 | } 506 | Event::OpenChannelRequest { .. } => { 507 | // Unreachable, we don't set manually_accept_inbound_channels 508 | } 509 | Event::PaymentPathSuccessful { .. } => {} 510 | Event::PaymentPathFailed { .. } => {} 511 | Event::ProbeSuccessful { .. } => {} 512 | Event::ProbeFailed { .. } => {} 513 | Event::PaymentFailed { payment_hash, reason, .. } => { 514 | print!( 515 | "\nEVENT: Failed to send payment to payment hash {:?}: {:?}", 516 | hex_utils::hex_str(&payment_hash.0), 517 | if let Some(r) = reason { r } else { PaymentFailureReason::RetriesExhausted } 518 | ); 519 | print!("> "); 520 | io::stdout().flush().unwrap(); 521 | 522 | let mut payments = outbound_payments.lock().unwrap(); 523 | if payments.contains_key(&payment_hash) { 524 | let payment = payments.get_mut(&payment_hash).unwrap(); 525 | payment.status = HTLCStatus::Failed; 526 | } 527 | } 528 | Event::PaymentForwarded { 529 | prev_channel_id, 530 | next_channel_id, 531 | fee_earned_msat, 532 | claim_from_onchain_tx, 533 | outbound_amount_forwarded_msat, 534 | } => { 535 | let read_only_network_graph = network_graph.read_only(); 536 | let nodes = read_only_network_graph.nodes(); 537 | let channels = channel_manager.list_channels(); 538 | 539 | let node_str = |channel_id: &Option<[u8; 32]>| match channel_id { 540 | None => String::new(), 541 | Some(channel_id) => match channels.iter().find(|c| c.channel_id == *channel_id) { 542 | None => String::new(), 543 | Some(channel) => { 544 | match nodes.get(&NodeId::from_pubkey(&channel.counterparty.node_id)) { 545 | None => "private node".to_string(), 546 | Some(node) => match &node.announcement_info { 547 | None => "unnamed node".to_string(), 548 | Some(announcement) => { 549 | format!("node {}", announcement.alias) 550 | } 551 | }, 552 | } 553 | } 554 | }, 555 | }; 556 | let channel_str = |channel_id: &Option<[u8; 32]>| { 557 | channel_id 558 | .map(|channel_id| format!(" with channel {}", hex_utils::hex_str(&channel_id))) 559 | .unwrap_or_default() 560 | }; 561 | let from_prev_str = 562 | format!(" from {}{}", node_str(&prev_channel_id), channel_str(&prev_channel_id)); 563 | let to_next_str = 564 | format!(" to {}{}", node_str(&next_channel_id), channel_str(&next_channel_id)); 565 | 566 | let from_onchain_str = if claim_from_onchain_tx { 567 | "from onchain downstream claim" 568 | } else { 569 | "from HTLC fulfill message" 570 | }; 571 | let amt_args = if let Some(v) = outbound_amount_forwarded_msat { 572 | format!("{}", v) 573 | } else { 574 | "?".to_string() 575 | }; 576 | if let Some(fee_earned) = fee_earned_msat { 577 | println!( 578 | "\nEVENT: Forwarded payment for {} msat{}{}, earning {} msat {}", 579 | amt_args, from_prev_str, to_next_str, fee_earned, from_onchain_str 580 | ); 581 | } else { 582 | println!( 583 | "\nEVENT: Forwarded payment for {} msat{}{}, claiming onchain {}", 584 | amt_args, from_prev_str, to_next_str, from_onchain_str 585 | ); 586 | } 587 | print!("> "); 588 | io::stdout().flush().unwrap(); 589 | } 590 | Event::HTLCHandlingFailed { .. } => {} 591 | Event::PendingHTLCsForwardable { time_forwardable } => { 592 | let forwarding_channel_manager = channel_manager.clone(); 593 | let min = time_forwardable.as_millis() as u64; 594 | tokio::spawn(async move { 595 | let millis_to_sleep = thread_rng().gen_range(min, min * 5) as u64; 596 | tokio::time::sleep(Duration::from_millis(millis_to_sleep)).await; 597 | forwarding_channel_manager.process_pending_htlc_forwards(); 598 | }); 599 | } 600 | Event::SpendableOutputs { outputs } => { 601 | let secp_ctx = Secp256k1::new(); 602 | let output_descriptors = &outputs.iter().collect::>(); 603 | let tx_feerate = FEE_RATE as u32 * 250; // 1 sat/vB = 250 sat/kw 604 | let mut runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); 605 | 606 | for outp in output_descriptors { 607 | let outpoint = match outp { 608 | SpendableOutputDescriptor::StaticPaymentOutput(descriptor) => { 609 | descriptor.outpoint 610 | } 611 | SpendableOutputDescriptor::DelayedPaymentOutput(descriptor) => { 612 | descriptor.outpoint 613 | } 614 | SpendableOutputDescriptor::StaticOutput { ref outpoint, output: _ } => { 615 | *outpoint 616 | } 617 | }; 618 | 619 | let txid = outpoint.txid; 620 | let witness_txid = RgbTxid::from_str(&txid.to_string()).unwrap(); 621 | 622 | let rgb_inputs: Vec = 623 | vec![OutPoint { txid, vout: outpoint.index as u32 }]; 624 | 625 | let transfer_info_path = format!("{ldk_data_dir}/{txid}_transfer_info"); 626 | let (is_colored, asset_transition_builder, assignment_id, amt_rgb, contract_id) = 627 | if is_transfer_colored(&transfer_info_path) { 628 | let transfer_info = read_rgb_transfer_info(&transfer_info_path); 629 | let contract_id = transfer_info.contract_id; 630 | 631 | runtime 632 | .consume_anchor(transfer_info.anchor) 633 | .expect("should consume anchor"); 634 | for (id, bundle) in transfer_info.bundles { 635 | runtime 636 | .consume_bundle(id, bundle, witness_txid) 637 | .expect("should consume bundle"); 638 | } 639 | let seal_holder = BuilderSeal::Revealed(GraphSeal::with_vout( 640 | CloseMethod::OpretFirst, 641 | transfer_info.vout, 642 | STATIC_BLINDING, 643 | )); 644 | let seal_counterparty = BuilderSeal::Revealed(GraphSeal::with_vout( 645 | CloseMethod::OpretFirst, 646 | transfer_info.vout ^ 1, 647 | STATIC_BLINDING, 648 | )); 649 | let beneficiaries = vec![seal_holder, seal_counterparty]; 650 | let beneficiaries: Vec> = beneficiaries 651 | .into_iter() 652 | .map(|b| match b { 653 | BuilderSeal::Revealed(graph_seal) => { 654 | BuilderSeal::Revealed(graph_seal.resolve(witness_txid)) 655 | } 656 | BuilderSeal::Concealed(seal) => BuilderSeal::Concealed(seal), 657 | }) 658 | .collect(); 659 | let consignment = 660 | runtime.transfer(contract_id, beneficiaries).expect("valid transfer"); 661 | let transfer: RgbTransfer = consignment.clone().unbindle(); 662 | 663 | let validated_transfer = transfer 664 | .clone() 665 | .validate(runtime.resolver()) 666 | .expect("invalid contract"); 667 | let status = runtime 668 | .accept_transfer(validated_transfer.clone(), true) 669 | .expect("valid transfer"); 670 | let validity = status.validity(); 671 | if !matches!(validity, Validity::Valid) { 672 | println!("WARNING: error consuming transfer"); 673 | continue; 674 | } 675 | 676 | let bundle = &transfer 677 | .anchored_bundles() 678 | .find(|ab| ab.anchor.txid.to_string() == outpoint.txid.to_string()) 679 | .expect("found bundle for closing tx") 680 | .bundle; 681 | 682 | let mut amt_rgb = 0; 683 | for bundle_item in bundle.values() { 684 | if let Some(transition) = &bundle_item.transition { 685 | for assignment in transition.assignments.values() { 686 | for fungible_assignment in assignment.as_fungible() { 687 | if let Assign::Revealed { seal, state } = 688 | fungible_assignment 689 | { 690 | if seal.vout == (outpoint.index as u32).into() { 691 | amt_rgb += state.value.as_u64(); 692 | } 693 | }; 694 | } 695 | } 696 | } 697 | } 698 | 699 | let asset_transition_builder = runtime 700 | .transition_builder( 701 | contract_id, 702 | TypeName::try_from("RGB20").unwrap(), 703 | None::<&str>, 704 | ) 705 | .expect("ok"); 706 | let assignment_id = asset_transition_builder 707 | .assignments_type(&FieldName::from("beneficiary")) 708 | .expect("valid assignment"); 709 | 710 | ( 711 | true, 712 | Some(asset_transition_builder), 713 | Some(assignment_id), 714 | Some(amt_rgb), 715 | Some(contract_id), 716 | ) 717 | } else { 718 | (false, None, None, None, None) 719 | }; 720 | 721 | let wallet = wallet_arc.lock().unwrap(); 722 | let address = wallet.get_address(AddressIndex::New).expect("valid address").address; 723 | 724 | let (tx, rgb_vars) = match outp { 725 | SpendableOutputDescriptor::StaticPaymentOutput(descriptor) => { 726 | let signer = keys_manager.derive_channel_keys( 727 | descriptor.channel_value_satoshis, 728 | &descriptor.channel_keys_id, 729 | ); 730 | let intermediate_wallet = get_bdk_wallet_seckey( 731 | format!("{ldk_data_dir}/intermediate"), 732 | network, 733 | signer.payment_key, 734 | ); 735 | sync_wallet(&intermediate_wallet, electrum_url.clone()); 736 | let mut builder = intermediate_wallet.build_tx(); 737 | builder 738 | .add_utxos(&rgb_inputs) 739 | .expect("valid utxos") 740 | .fee_rate(FeeRate::from_sat_per_vb(FEE_RATE)) 741 | .manually_selected_only() 742 | .drain_to(address.script_pubkey()); 743 | 744 | if is_colored { 745 | builder.add_data(&[1]); 746 | } 747 | 748 | let psbt = builder.finish().expect("valid psbt finish").0; 749 | 750 | let (mut psbt, rgb_vars) = if is_colored { 751 | let mut beneficiaries = vec![]; 752 | let (vout, asset_transition_builder) = update_transition_beneficiary( 753 | &psbt, 754 | &mut beneficiaries, 755 | asset_transition_builder.unwrap(), 756 | assignment_id.unwrap(), 757 | amt_rgb.unwrap(), 758 | ); 759 | let (psbt, consignment) = runtime.send_rgb( 760 | contract_id.unwrap(), 761 | psbt, 762 | asset_transition_builder, 763 | beneficiaries, 764 | ); 765 | (psbt, Some((vout, consignment))) 766 | } else { 767 | (psbt, None) 768 | }; 769 | 770 | intermediate_wallet 771 | .sign(&mut psbt, SignOptions::default()) 772 | .expect("able to sign"); 773 | 774 | (psbt.extract_tx(), rgb_vars) 775 | } 776 | SpendableOutputDescriptor::DelayedPaymentOutput(descriptor) => { 777 | let signer = keys_manager.derive_channel_keys( 778 | descriptor.channel_value_satoshis, 779 | &descriptor.channel_keys_id, 780 | ); 781 | let input = vec![TxIn { 782 | previous_output: descriptor.outpoint.into_bitcoin_outpoint(), 783 | script_sig: Script::new(), 784 | sequence: Sequence(descriptor.to_self_delay as u32), 785 | witness: Witness::new(), 786 | }]; 787 | let witness_weight = DelayedPaymentOutputDescriptor::MAX_WITNESS_LENGTH; 788 | let input_value = descriptor.output.value; 789 | let output = vec![]; 790 | let mut spend_tx = 791 | Transaction { version: 2, lock_time: PackedLockTime(0), input, output }; 792 | let _expected_max_weight = 793 | lightning::util::transaction_utils::maybe_add_change_output( 794 | &mut spend_tx, 795 | input_value, 796 | witness_weight, 797 | tx_feerate, 798 | address.script_pubkey(), 799 | ) 800 | .expect("can add change"); 801 | 802 | spend_tx 803 | .output 804 | .push(TxOut { value: 0, script_pubkey: Script::new_op_return(&[1]) }); 805 | 806 | let psbt = PartiallySignedTransaction::from_unsigned_tx(spend_tx.clone()) 807 | .expect("valid transaction"); 808 | 809 | let (psbt, rgb_vars) = if is_colored { 810 | let mut beneficiaries = vec![]; 811 | let (vout, asset_transition_builder) = update_transition_beneficiary( 812 | &psbt, 813 | &mut beneficiaries, 814 | asset_transition_builder.unwrap(), 815 | assignment_id.unwrap(), 816 | amt_rgb.unwrap(), 817 | ); 818 | let (psbt, consignment) = runtime.send_rgb( 819 | contract_id.unwrap(), 820 | psbt, 821 | asset_transition_builder, 822 | beneficiaries, 823 | ); 824 | (psbt, Some((vout, consignment))) 825 | } else { 826 | (psbt, None) 827 | }; 828 | 829 | let mut spend_tx = psbt.extract_tx(); 830 | let input_idx = 0; 831 | let witness_vec = signer 832 | .sign_dynamic_p2wsh_input(&spend_tx, input_idx, descriptor, &secp_ctx) 833 | .expect("possible dynamic sign"); 834 | spend_tx.input[input_idx].witness = Witness::from_vec(witness_vec); 835 | 836 | (spend_tx, rgb_vars) 837 | } 838 | SpendableOutputDescriptor::StaticOutput { outpoint: _, ref output } => { 839 | let derivation_idx = 840 | if output.script_pubkey == keys_manager.destination_script { 841 | 1 842 | } else { 843 | 2 844 | }; 845 | let secret = keys_manager 846 | .master_key 847 | .ckd_priv( 848 | &secp_ctx, 849 | ChildNumber::from_hardened_idx(derivation_idx).unwrap(), 850 | ) 851 | .unwrap(); 852 | let intermediate_wallet = get_bdk_wallet_seckey( 853 | format!("{ldk_data_dir}/intermediate"), 854 | network, 855 | secret.private_key, 856 | ); 857 | sync_wallet(&intermediate_wallet, electrum_url.clone()); 858 | let mut builder = intermediate_wallet.build_tx(); 859 | builder 860 | .add_utxos(&rgb_inputs) 861 | .expect("valid utxos") 862 | .add_data(&[1]) 863 | .fee_rate(FeeRate::from_sat_per_vb(FEE_RATE)) 864 | .manually_selected_only() 865 | .drain_to(address.script_pubkey()); 866 | let psbt = builder.finish().expect("valid psbt finish").0; 867 | 868 | let (mut psbt, rgb_vars) = if is_colored { 869 | let mut beneficiaries = vec![]; 870 | let (vout, asset_transition_builder) = update_transition_beneficiary( 871 | &psbt, 872 | &mut beneficiaries, 873 | asset_transition_builder.unwrap(), 874 | assignment_id.unwrap(), 875 | amt_rgb.unwrap(), 876 | ); 877 | let (psbt, consignment) = runtime.send_rgb( 878 | contract_id.unwrap(), 879 | psbt, 880 | asset_transition_builder, 881 | beneficiaries, 882 | ); 883 | (psbt, Some((vout, consignment))) 884 | } else { 885 | (psbt, None) 886 | }; 887 | 888 | intermediate_wallet 889 | .sign(&mut psbt, SignOptions::default()) 890 | .expect("able to sign"); 891 | 892 | (psbt.extract_tx(), rgb_vars) 893 | } 894 | }; 895 | 896 | broadcast_tx(&tx, electrum_url.clone()); 897 | sync_wallet(&wallet, electrum_url.clone()); 898 | 899 | if let Some((vout, consignment)) = rgb_vars { 900 | let rgb_utxos_path = format!("{}/rgb_utxos", ldk_data_dir); 901 | let serialized_utxos = 902 | fs::read_to_string(&rgb_utxos_path).expect("able to read rgb utxos file"); 903 | let mut rgb_utxos: RgbUtxos = 904 | serde_json::from_str(&serialized_utxos).expect("valid rgb utxos"); 905 | rgb_utxos.utxos.push(RgbUtxo { 906 | outpoint: OutPoint { txid: tx.txid(), vout }, 907 | colored: true, 908 | }); 909 | let serialized_utxos = 910 | serde_json::to_string(&rgb_utxos).expect("valid rgb utxos"); 911 | fs::write(rgb_utxos_path, serialized_utxos) 912 | .expect("able to write rgb utxos file"); 913 | 914 | let transfer: RgbTransfer = consignment.unbindle(); 915 | let validated_transfer = 916 | transfer.clone().validate(runtime.resolver()).expect("invalid contract"); 917 | let _status = runtime 918 | .accept_transfer(validated_transfer, true) 919 | .expect("valid consignment"); 920 | } 921 | } 922 | drop(runtime); 923 | drop_rgb_runtime(&PathBuf::from(ldk_data_dir)); 924 | 925 | println!("Event::SpendableOutputs complete"); 926 | } 927 | Event::ChannelPending { channel_id, counterparty_node_id, .. } => { 928 | println!( 929 | "\nEVENT: Channel {} with peer {} is pending awaiting funding lock-in!", 930 | hex_utils::hex_str(&channel_id), 931 | hex_utils::hex_str(&counterparty_node_id.serialize()), 932 | ); 933 | print!("> "); 934 | io::stdout().flush().unwrap(); 935 | } 936 | Event::ChannelReady { 937 | ref channel_id, 938 | user_channel_id: _, 939 | ref counterparty_node_id, 940 | channel_type: _, 941 | } => { 942 | println!( 943 | "\nEVENT: Channel {} with peer {} is ready to be used!", 944 | hex_utils::hex_str(channel_id), 945 | hex_utils::hex_str(&counterparty_node_id.serialize()), 946 | ); 947 | let is_colored = is_channel_rgb(&channel_id, &ldk_data_dir.clone().into()); 948 | if is_colored { 949 | let mut runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); 950 | 951 | let funding_consignment_path = 952 | format!("{}/consignment_{}", ldk_data_dir, hex::encode(channel_id)); 953 | 954 | let funding_consignment_bindle = 955 | Bindle::::load(funding_consignment_path) 956 | .expect("successful consignment load"); 957 | let transfer: RgbTransfer = funding_consignment_bindle.unbindle(); 958 | 959 | let validated_transfer = 960 | transfer.validate(runtime.resolver()).expect("invalid contract"); 961 | let _status = 962 | runtime.accept_transfer(validated_transfer, true).expect("valid consignment"); 963 | 964 | drop(runtime); 965 | drop_rgb_runtime(&PathBuf::from(ldk_data_dir)); 966 | } 967 | 968 | print!("> "); 969 | io::stdout().flush().unwrap(); 970 | } 971 | Event::ChannelClosed { channel_id, reason, user_channel_id: _ } => { 972 | println!( 973 | "\nEVENT: Channel {} closed due to: {:?}", 974 | hex_utils::hex_str(&channel_id), 975 | reason 976 | ); 977 | 978 | print!("> "); 979 | io::stdout().flush().unwrap(); 980 | } 981 | Event::DiscardFunding { .. } => { 982 | // A "real" node should probably "lock" the UTXOs spent in funding transactions until 983 | // the funding transaction either confirms, or this event is generated. 984 | } 985 | Event::HTLCIntercepted { 986 | is_swap, 987 | payment_hash, 988 | intercept_id, 989 | inbound_amount_msat, 990 | expected_outbound_amount_msat, 991 | inbound_rgb_amount, 992 | expected_outbound_rgb_amount, 993 | requested_next_hop_scid, 994 | prev_short_channel_id, 995 | } => { 996 | if !is_swap { 997 | channel_manager.fail_intercepted_htlc(intercept_id).unwrap(); 998 | } 999 | 1000 | let ldk_data_dir_path = PathBuf::from(ldk_data_dir.clone()); 1001 | let get_rgb_info = |channel_id| { 1002 | let info_file_path = ldk_data_dir_path.join(hex::encode(channel_id)); 1003 | if info_file_path.exists() { 1004 | let (rgb_info, _) = get_rgb_channel_info(channel_id, &ldk_data_dir_path); 1005 | Some(( 1006 | rgb_info.contract_id, 1007 | rgb_info.local_rgb_amount, 1008 | rgb_info.remote_rgb_amount, 1009 | )) 1010 | } else { 1011 | None 1012 | } 1013 | }; 1014 | 1015 | let inbound_channel = channel_manager 1016 | .list_channels() 1017 | .into_iter() 1018 | .find(|details| details.short_channel_id == Some(prev_short_channel_id)) 1019 | .expect("Should always be a valid channel"); 1020 | let outbound_channel = channel_manager 1021 | .list_channels() 1022 | .into_iter() 1023 | .find(|details| details.short_channel_id == Some(requested_next_hop_scid)) 1024 | .expect("Should always be a valid channel"); 1025 | 1026 | let inbound_rgb_info = get_rgb_info(&inbound_channel.channel_id); 1027 | let outbound_rgb_info = get_rgb_info(&outbound_channel.channel_id); 1028 | 1029 | println!("EVENT: Requested swap with params inbound_msat={} outbound_msat={} inbound_rgb={:?} outbound_rgb={:?} inbound_contract_id={:?}, outbound_contract_id={:?}", inbound_amount_msat, expected_outbound_amount_msat, inbound_rgb_amount, expected_outbound_rgb_amount, inbound_rgb_info.map(|i| i.0), outbound_rgb_info.map(|i| i.0)); 1030 | 1031 | let mut trades_lock = whitelisted_trades.lock().unwrap(); 1032 | let (whitelist_contract_id, whitelist_swap_type) = match trades_lock.get(&payment_hash) 1033 | { 1034 | None => { 1035 | println!("ERROR: rejecting non-whitelisted swap"); 1036 | channel_manager.fail_intercepted_htlc(intercept_id).unwrap(); 1037 | return; 1038 | } 1039 | Some(x) => x, 1040 | }; 1041 | 1042 | match whitelist_swap_type { 1043 | SwapType::BuyAsset { amount_rgb, amount_msats } => { 1044 | // We subtract HTLC_MIN_MSAT because a node receiving an RGB payment also receives that amount of sats with it as the payment amount, 1045 | // so we exclude it from the calculation of how many sats we are effectively giving out. 1046 | let net_msat_diff = (expected_outbound_amount_msat) 1047 | .saturating_sub(inbound_amount_msat.saturating_sub(HTLC_MIN_MSAT)); 1048 | 1049 | if inbound_rgb_amount != Some(*amount_rgb) 1050 | || inbound_rgb_info.map(|x| x.0) != Some(*whitelist_contract_id) 1051 | || net_msat_diff != *amount_msats 1052 | || outbound_rgb_info.is_some() 1053 | { 1054 | println!("ERROR: swap doesn't match the whitelisted info, rejecting it"); 1055 | channel_manager.fail_intercepted_htlc(intercept_id).unwrap(); 1056 | return; 1057 | } 1058 | } 1059 | SwapType::SellAsset { amount_rgb, amount_msats } => { 1060 | let net_msat_diff = 1061 | inbound_amount_msat.saturating_sub(expected_outbound_amount_msat); 1062 | 1063 | if expected_outbound_rgb_amount != Some(*amount_rgb) 1064 | || outbound_rgb_info.map(|x| x.0) != Some(*whitelist_contract_id) 1065 | || net_msat_diff != *amount_msats 1066 | || inbound_rgb_info.is_some() 1067 | { 1068 | println!("ERROR: swap doesn't match the whitelisted info, rejecting it"); 1069 | channel_manager.fail_intercepted_htlc(intercept_id).unwrap(); 1070 | return; 1071 | } 1072 | } 1073 | } 1074 | 1075 | println!("Swap is whitelisted, forwarding the htlc..."); 1076 | trades_lock.remove(&payment_hash); 1077 | 1078 | channel_manager 1079 | .forward_intercepted_htlc( 1080 | intercept_id, 1081 | channelmanager::NextHopForward::ShortChannelId(requested_next_hop_scid), 1082 | expected_outbound_amount_msat, 1083 | expected_outbound_rgb_amount, 1084 | ) 1085 | .expect("Forward should be valid"); 1086 | } 1087 | } 1088 | } 1089 | 1090 | async fn start_ldk() { 1091 | let args = match args::parse_startup_args() { 1092 | Ok(user_args) => user_args, 1093 | Err(()) => return, 1094 | }; 1095 | 1096 | // Initialize the LDK data directory if necessary. 1097 | let ldk_data_dir = format!("{}/.ldk", args.ldk_storage_dir_path); 1098 | let ldk_data_dir_path = PathBuf::from(&ldk_data_dir); 1099 | fs::create_dir_all(ldk_data_dir.clone()).unwrap(); 1100 | 1101 | let blinded_dir = ldk_data_dir_path.join("blinded_utxos"); 1102 | fs::create_dir_all(blinded_dir).expect("successful directory creation"); 1103 | 1104 | // ## Setup 1105 | // Step 1: Initialize the Logger 1106 | let logger = Arc::new(FilesystemLogger::new(ldk_data_dir.clone())); 1107 | 1108 | // Initialize our bitcoind client. 1109 | let bitcoind_client = match BitcoindClient::new( 1110 | args.bitcoind_rpc_host.clone(), 1111 | args.bitcoind_rpc_port, 1112 | args.bitcoind_rpc_username.clone(), 1113 | args.bitcoind_rpc_password.clone(), 1114 | tokio::runtime::Handle::current(), 1115 | Arc::clone(&logger), 1116 | ) 1117 | .await 1118 | { 1119 | Ok(client) => Arc::new(client), 1120 | Err(e) => { 1121 | println!("Failed to connect to bitcoind client: {}", e); 1122 | return; 1123 | } 1124 | }; 1125 | 1126 | // Check that the bitcoind we've connected to is running the network we expect 1127 | let bitcoind_chain = bitcoind_client.get_blockchain_info().await.chain; 1128 | if bitcoind_chain 1129 | != match args.network { 1130 | bitcoin::Network::Bitcoin => "main", 1131 | bitcoin::Network::Testnet => "test", 1132 | bitcoin::Network::Regtest => "regtest", 1133 | bitcoin::Network::Signet => "signet", 1134 | } { 1135 | println!( 1136 | "Chain argument ({}) didn't match bitcoind chain ({})", 1137 | args.network, bitcoind_chain 1138 | ); 1139 | return; 1140 | } 1141 | 1142 | // RGB setup 1143 | let (electrum_url, proxy_url, proxy_endpoint, rgb_network) = match args.network { 1144 | bitcoin::Network::Testnet => { 1145 | (ELECTRUM_URL_TESTNET, PROXY_URL_TESTNET, PROXY_ENDPOINT_TESTNET, Chain::Testnet3) 1146 | } 1147 | bitcoin::Network::Regtest => { 1148 | (ELECTRUM_URL_REGTEST, PROXY_URL_REGTEST, PROXY_ENDPOINT_REGTEST, Chain::Regtest) 1149 | } 1150 | _ => { 1151 | println!("ERROR: PoC does not support selected network"); 1152 | return; 1153 | } 1154 | }; 1155 | fs::write(format!("{ldk_data_dir}/electrum_url"), electrum_url).expect("able to write"); 1156 | fs::write(format!("{ldk_data_dir}/rgb_network"), rgb_network.to_string()) 1157 | .expect("able to write"); 1158 | let mut runtime = get_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); 1159 | if runtime.schema_ids().unwrap().is_empty() { 1160 | runtime.import_iface(rgb20()).unwrap(); 1161 | runtime.import_schema(nia_schema()).unwrap(); 1162 | runtime.import_iface_impl(nia_rgb20()).unwrap(); 1163 | } 1164 | drop(runtime); 1165 | drop_rgb_runtime(&PathBuf::from(ldk_data_dir.clone())); 1166 | 1167 | let rest_client = RestClient::builder() 1168 | .timeout(Duration::from_secs(PROXY_TIMEOUT as u64)) 1169 | .build() 1170 | .expect("valid proxy"); 1171 | let proxy_client = Arc::new(rest_client); 1172 | 1173 | // ## Setup 1174 | // Step 2: Initialize the FeeEstimator 1175 | 1176 | // BitcoindClient implements the FeeEstimator trait, so it'll act as our fee estimator. 1177 | let fee_estimator = bitcoind_client.clone(); 1178 | 1179 | // Step 3: Initialize the BroadcasterInterface 1180 | 1181 | // BitcoindClient implements the BroadcasterInterface trait, so it'll act as our transaction 1182 | // broadcaster. 1183 | let broadcaster = bitcoind_client.clone(); 1184 | 1185 | // Step 4: Initialize Persist 1186 | let persister = Arc::new(FilesystemPersister::new(ldk_data_dir.clone())); 1187 | 1188 | // Step 5: Initialize the ChainMonitor 1189 | let chain_monitor: Arc = Arc::new(chainmonitor::ChainMonitor::new( 1190 | None, 1191 | broadcaster.clone(), 1192 | logger.clone(), 1193 | fee_estimator.clone(), 1194 | persister.clone(), 1195 | )); 1196 | 1197 | // Step 6: Initialize the KeysManager 1198 | 1199 | // The key seed that we use to derive the node privkey (that corresponds to the node pubkey) and 1200 | // other secret key material. 1201 | let keys_seed_path = format!("{}/keys_seed", ldk_data_dir.clone()); 1202 | let keys_seed = if let Ok(seed) = fs::read(keys_seed_path.clone()) { 1203 | assert_eq!(seed.len(), 32); 1204 | let mut key = [0; 32]; 1205 | key.copy_from_slice(&seed); 1206 | key 1207 | } else { 1208 | let mut key = [0; 32]; 1209 | thread_rng().fill_bytes(&mut key); 1210 | match File::create(keys_seed_path.clone()) { 1211 | Ok(mut f) => { 1212 | f.write_all(&key).expect("Failed to write node keys seed to disk"); 1213 | f.sync_all().expect("Failed to sync node keys seed to disk"); 1214 | } 1215 | Err(e) => { 1216 | println!("ERROR: Unable to create keys seed file {}: {}", keys_seed_path, e); 1217 | return; 1218 | } 1219 | } 1220 | key 1221 | }; 1222 | let cur = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); 1223 | 1224 | let master_xprv = ExtendedPrivKey::new_master(Network::Testnet, &keys_seed).unwrap(); 1225 | let secp = Secp256k1::new(); 1226 | let xprv: ExtendedPrivKey = 1227 | master_xprv.ckd_priv(&secp, ChildNumber::Hardened { index: 535 }).unwrap(); 1228 | let ldk_seed: [u8; 32] = xprv.private_key.secret_bytes(); 1229 | 1230 | let keys_manager = Arc::new(KeysManager::new( 1231 | &ldk_seed, 1232 | cur.as_secs(), 1233 | cur.subsec_nanos(), 1234 | ldk_data_dir_path.clone(), 1235 | )); 1236 | let bdk_wallet = get_bdk_wallet(ldk_data_dir.clone(), keys_manager.master_key, args.network); 1237 | sync_wallet(&bdk_wallet, electrum_url.to_string()); 1238 | let wallet = Arc::new(Mutex::new(bdk_wallet)); 1239 | 1240 | // Step 7: Read ChannelMonitor state from disk 1241 | let mut channelmonitors = 1242 | persister.read_channelmonitors(keys_manager.clone(), keys_manager.clone()).unwrap(); 1243 | 1244 | // Step 8: Poll for the best chain tip, which may be used by the channel manager & spv client 1245 | let polled_chain_tip = init::validate_best_block_header(bitcoind_client.as_ref()) 1246 | .await 1247 | .expect("Failed to fetch best block header and best block"); 1248 | 1249 | // Step 9: Initialize routing ProbabilisticScorer 1250 | let network_graph_path = format!("{}/network_graph", ldk_data_dir.clone()); 1251 | let network_graph = 1252 | Arc::new(disk::read_network(Path::new(&network_graph_path), args.network, logger.clone())); 1253 | 1254 | let scorer_path = format!("{}/scorer", ldk_data_dir.clone()); 1255 | let scorer = Arc::new(Mutex::new(disk::read_scorer( 1256 | Path::new(&scorer_path), 1257 | Arc::clone(&network_graph), 1258 | Arc::clone(&logger), 1259 | ))); 1260 | 1261 | // Step 10: Create Router 1262 | let router = Arc::new(DefaultRouter::new( 1263 | network_graph.clone(), 1264 | logger.clone(), 1265 | keys_manager.get_secure_random_bytes(), 1266 | scorer.clone(), 1267 | )); 1268 | 1269 | // Step 11: Initialize the ChannelManager 1270 | let mut user_config = UserConfig::default(); 1271 | user_config.channel_handshake_limits.force_announced_channel_preference = false; 1272 | user_config.accept_intercept_htlcs = true; 1273 | let mut restarting_node = true; 1274 | let (channel_manager_blockhash, channel_manager) = { 1275 | if let Ok(mut f) = fs::File::open(format!("{}/manager", ldk_data_dir.clone())) { 1276 | let mut channel_monitor_mut_references = Vec::new(); 1277 | for (_, channel_monitor) in channelmonitors.iter_mut() { 1278 | channel_monitor_mut_references.push(channel_monitor); 1279 | } 1280 | let read_args = ChannelManagerReadArgs::new( 1281 | keys_manager.clone(), 1282 | keys_manager.clone(), 1283 | keys_manager.clone(), 1284 | fee_estimator.clone(), 1285 | chain_monitor.clone(), 1286 | broadcaster.clone(), 1287 | router.clone(), 1288 | logger.clone(), 1289 | user_config, 1290 | channel_monitor_mut_references, 1291 | ldk_data_dir_path.clone(), 1292 | ); 1293 | <(BlockHash, ChannelManager)>::read(&mut f, read_args).unwrap() 1294 | } else { 1295 | // We're starting a fresh node. 1296 | restarting_node = false; 1297 | 1298 | let polled_best_block = polled_chain_tip.to_best_block(); 1299 | let polled_best_block_hash = polled_best_block.block_hash(); 1300 | let chain_params = 1301 | ChainParameters { network: args.network, best_block: polled_best_block }; 1302 | let fresh_channel_manager = channelmanager::ChannelManager::new( 1303 | fee_estimator.clone(), 1304 | chain_monitor.clone(), 1305 | broadcaster.clone(), 1306 | router.clone(), 1307 | logger.clone(), 1308 | keys_manager.clone(), 1309 | keys_manager.clone(), 1310 | keys_manager.clone(), 1311 | user_config, 1312 | chain_params, 1313 | ldk_data_dir_path.clone(), 1314 | ); 1315 | (polled_best_block_hash, fresh_channel_manager) 1316 | } 1317 | }; 1318 | 1319 | // initialize RGB UTXOs file on first run 1320 | if !restarting_node { 1321 | let rgb_utxos = RgbUtxos { utxos: vec![] }; 1322 | let rgb_utxos_path = format!("{}/rgb_utxos", ldk_data_dir); 1323 | let serialized_utxos = serde_json::to_string(&rgb_utxos).expect("valid rgb utxos"); 1324 | fs::write(rgb_utxos_path, serialized_utxos).expect("able to write rgb utxos file"); 1325 | } 1326 | 1327 | // Step 12: Sync ChannelMonitors and ChannelManager to chain tip 1328 | let mut chain_listener_channel_monitors = Vec::new(); 1329 | let mut cache = UnboundedCache::new(); 1330 | let chain_tip = if restarting_node { 1331 | let mut chain_listeners = vec![( 1332 | channel_manager_blockhash, 1333 | &channel_manager as &(dyn chain::Listen + Send + Sync), 1334 | )]; 1335 | 1336 | for (blockhash, channel_monitor) in channelmonitors.drain(..) { 1337 | let outpoint = channel_monitor.get_funding_txo().0; 1338 | chain_listener_channel_monitors.push(( 1339 | blockhash, 1340 | (channel_monitor, broadcaster.clone(), fee_estimator.clone(), logger.clone()), 1341 | outpoint, 1342 | )); 1343 | } 1344 | 1345 | for monitor_listener_info in chain_listener_channel_monitors.iter_mut() { 1346 | chain_listeners.push(( 1347 | monitor_listener_info.0, 1348 | &monitor_listener_info.1 as &(dyn chain::Listen + Send + Sync), 1349 | )); 1350 | } 1351 | 1352 | init::synchronize_listeners( 1353 | bitcoind_client.as_ref(), 1354 | args.network, 1355 | &mut cache, 1356 | chain_listeners, 1357 | ) 1358 | .await 1359 | .unwrap() 1360 | } else { 1361 | polled_chain_tip 1362 | }; 1363 | 1364 | // Step 13: Give ChannelMonitors to ChainMonitor 1365 | for item in chain_listener_channel_monitors.drain(..) { 1366 | let channel_monitor = item.1 .0; 1367 | let funding_outpoint = item.2; 1368 | assert_eq!( 1369 | chain_monitor.watch_channel(funding_outpoint, channel_monitor), 1370 | ChannelMonitorUpdateStatus::Completed 1371 | ); 1372 | } 1373 | 1374 | // Step 14: Optional: Initialize the P2PGossipSync 1375 | let gossip_sync = Arc::new(P2PGossipSync::new( 1376 | Arc::clone(&network_graph), 1377 | None::>, 1378 | logger.clone(), 1379 | )); 1380 | 1381 | // Step 15: Initialize the PeerManager 1382 | let channel_manager: Arc = Arc::new(channel_manager); 1383 | let onion_messenger: Arc = Arc::new(OnionMessenger::new( 1384 | Arc::clone(&keys_manager), 1385 | Arc::clone(&keys_manager), 1386 | Arc::clone(&logger), 1387 | IgnoringMessageHandler {}, 1388 | )); 1389 | let mut ephemeral_bytes = [0; 32]; 1390 | let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); 1391 | rand::thread_rng().fill_bytes(&mut ephemeral_bytes); 1392 | let lightning_msg_handler = MessageHandler { 1393 | chan_handler: channel_manager.clone(), 1394 | route_handler: gossip_sync.clone(), 1395 | onion_message_handler: onion_messenger.clone(), 1396 | }; 1397 | let peer_manager: Arc = Arc::new(PeerManager::new( 1398 | lightning_msg_handler, 1399 | current_time.try_into().unwrap(), 1400 | &ephemeral_bytes, 1401 | logger.clone(), 1402 | IgnoringMessageHandler {}, 1403 | Arc::clone(&keys_manager), 1404 | )); 1405 | 1406 | // ## Running LDK 1407 | // Step 16: Initialize networking 1408 | 1409 | let peer_manager_connection_handler = peer_manager.clone(); 1410 | let listening_port = args.ldk_peer_listening_port; 1411 | let stop_listen_connect = Arc::new(AtomicBool::new(false)); 1412 | let stop_listen = Arc::clone(&stop_listen_connect); 1413 | tokio::spawn(async move { 1414 | let listener = tokio::net::TcpListener::bind(format!("[::]:{}", listening_port)) 1415 | .await 1416 | .expect("Failed to bind to listen port - is something else already listening on it?"); 1417 | loop { 1418 | let peer_mgr = peer_manager_connection_handler.clone(); 1419 | let tcp_stream = listener.accept().await.unwrap().0; 1420 | if stop_listen.load(Ordering::Acquire) { 1421 | return; 1422 | } 1423 | tokio::spawn(async move { 1424 | lightning_net_tokio::setup_inbound( 1425 | peer_mgr.clone(), 1426 | tcp_stream.into_std().unwrap(), 1427 | ) 1428 | .await; 1429 | }); 1430 | } 1431 | }); 1432 | 1433 | // Step 17: Connect and Disconnect Blocks 1434 | let channel_manager_listener = channel_manager.clone(); 1435 | let chain_monitor_listener = chain_monitor.clone(); 1436 | let bitcoind_block_source = bitcoind_client.clone(); 1437 | let network = args.network; 1438 | tokio::spawn(async move { 1439 | let chain_poller = poll::ChainPoller::new(bitcoind_block_source.as_ref(), network); 1440 | let chain_listener = (chain_monitor_listener, channel_manager_listener); 1441 | let mut spv_client = SpvClient::new(chain_tip, chain_poller, &mut cache, &chain_listener); 1442 | loop { 1443 | spv_client.poll_best_tip().await.unwrap(); 1444 | tokio::time::sleep(Duration::from_secs(1)).await; 1445 | } 1446 | }); 1447 | 1448 | // TODO: persist payment info to disk 1449 | let inbound_payments: PaymentInfoStorage = Arc::new(Mutex::new(HashMap::new())); 1450 | let outbound_payments: PaymentInfoStorage = Arc::new(Mutex::new(HashMap::new())); 1451 | 1452 | // Step 18: Handle LDK Events 1453 | let channel_manager_event_listener = Arc::clone(&channel_manager); 1454 | let whitelisted_trades = Arc::new(Mutex::new(HashMap::new())); 1455 | let maker_trades = Arc::new(Mutex::new(HashMap::new())); 1456 | let network_graph_event_listener = Arc::clone(&network_graph); 1457 | let keys_manager_event_listener = Arc::clone(&keys_manager); 1458 | let inbound_payments_event_listener = Arc::clone(&inbound_payments); 1459 | let outbound_payments_event_listener = Arc::clone(&outbound_payments); 1460 | let network = args.network; 1461 | let ldk_data_dir_copy = ldk_data_dir.clone(); 1462 | let proxy_client_copy = proxy_client.clone(); 1463 | let wallet_copy = wallet.clone(); 1464 | let whitelisted_trades_copy = whitelisted_trades.clone(); 1465 | let maker_trades_copy = maker_trades.clone(); 1466 | let event_handler = move |event: Event| { 1467 | let channel_manager_event_listener = Arc::clone(&channel_manager_event_listener); 1468 | let network_graph_event_listener = Arc::clone(&network_graph_event_listener); 1469 | let keys_manager_event_listener = Arc::clone(&keys_manager_event_listener); 1470 | let inbound_payments_event_listener = Arc::clone(&inbound_payments_event_listener); 1471 | let outbound_payments_event_listener = Arc::clone(&outbound_payments_event_listener); 1472 | let ldk_data_dir_copy = ldk_data_dir_copy.clone(); 1473 | let proxy_client_copy = proxy_client_copy.clone(); 1474 | let wallet_copy = wallet_copy.clone(); 1475 | let whitelisted_trades_copy = whitelisted_trades_copy.clone(); 1476 | let maker_trades_copy = maker_trades_copy.clone(); 1477 | async move { 1478 | handle_ldk_events( 1479 | &channel_manager_event_listener, 1480 | &network_graph_event_listener, 1481 | &keys_manager_event_listener, 1482 | &inbound_payments_event_listener, 1483 | &outbound_payments_event_listener, 1484 | network, 1485 | event, 1486 | ldk_data_dir_copy, 1487 | proxy_client_copy, 1488 | proxy_url.to_string(), 1489 | wallet_copy, 1490 | electrum_url.to_string(), 1491 | &whitelisted_trades_copy, 1492 | &maker_trades_copy, 1493 | ) 1494 | .await; 1495 | } 1496 | }; 1497 | 1498 | // Step 19: Persist ChannelManager and NetworkGraph 1499 | let persister = Arc::new(FilesystemPersister::new(ldk_data_dir.clone())); 1500 | 1501 | // Step 20: Background Processing 1502 | let (bp_exit, bp_exit_check) = tokio::sync::watch::channel(()); 1503 | let background_processor = tokio::spawn(process_events_async( 1504 | persister, 1505 | event_handler, 1506 | chain_monitor.clone(), 1507 | channel_manager.clone(), 1508 | GossipSync::p2p(gossip_sync.clone()), 1509 | peer_manager.clone(), 1510 | logger.clone(), 1511 | Some(scorer.clone()), 1512 | move |t| { 1513 | let mut bp_exit_fut_check = bp_exit_check.clone(); 1514 | Box::pin(async move { 1515 | tokio::select! { 1516 | _ = tokio::time::sleep(t) => false, 1517 | _ = bp_exit_fut_check.changed() => true, 1518 | } 1519 | }) 1520 | }, 1521 | false, 1522 | )); 1523 | 1524 | // Regularly reconnect to channel peers. 1525 | let connect_cm = Arc::clone(&channel_manager); 1526 | let connect_pm = Arc::clone(&peer_manager); 1527 | let peer_data_path = format!("{}/channel_peer_data", ldk_data_dir.clone()); 1528 | let stop_connect = Arc::clone(&stop_listen_connect); 1529 | tokio::spawn(async move { 1530 | let mut interval = tokio::time::interval(Duration::from_secs(1)); 1531 | loop { 1532 | interval.tick().await; 1533 | match disk::read_channel_peer_data(Path::new(&peer_data_path)) { 1534 | Ok(info) => { 1535 | let peers = connect_pm.get_peer_node_ids(); 1536 | for node_id in connect_cm 1537 | .list_channels() 1538 | .iter() 1539 | .map(|chan| chan.counterparty.node_id) 1540 | .filter(|id| !peers.iter().any(|(pk, _)| id == pk)) 1541 | { 1542 | if stop_connect.load(Ordering::Acquire) { 1543 | return; 1544 | } 1545 | for (pubkey, peer_addr) in info.iter() { 1546 | if *pubkey == node_id { 1547 | let _ = cli::do_connect_peer( 1548 | *pubkey, 1549 | peer_addr.clone(), 1550 | Arc::clone(&connect_pm), 1551 | ) 1552 | .await; 1553 | } 1554 | } 1555 | } 1556 | } 1557 | Err(e) => println!("ERROR: errored reading channel peer info from disk: {:?}", e), 1558 | } 1559 | } 1560 | }); 1561 | 1562 | // Regularly broadcast our node_announcement. This is only required (or possible) if we have 1563 | // some public channels, and is only useful if we have public listen address(es) to announce. 1564 | // In a production environment, this should occur only after the announcement of new channels 1565 | // to avoid churn in the global network graph. 1566 | let peer_man = Arc::clone(&peer_manager); 1567 | let network = args.network; 1568 | if !args.ldk_announced_listen_addr.is_empty() { 1569 | tokio::spawn(async move { 1570 | let mut interval = tokio::time::interval(Duration::from_secs(60)); 1571 | loop { 1572 | interval.tick().await; 1573 | peer_man.broadcast_node_announcement( 1574 | [0; 3], 1575 | args.ldk_announced_node_name, 1576 | args.ldk_announced_listen_addr.clone(), 1577 | ); 1578 | } 1579 | }); 1580 | } 1581 | 1582 | // Start the CLI. 1583 | cli::poll_for_user_input( 1584 | Arc::clone(&peer_manager), 1585 | Arc::clone(&channel_manager), 1586 | Arc::clone(&keys_manager), 1587 | Arc::clone(&network_graph), 1588 | Arc::clone(&onion_messenger), 1589 | Arc::clone(&router), 1590 | inbound_payments, 1591 | outbound_payments, 1592 | ldk_data_dir.clone(), 1593 | network, 1594 | Arc::clone(&logger), 1595 | Arc::clone(&bitcoind_client), 1596 | proxy_client.clone(), 1597 | proxy_url, 1598 | proxy_endpoint, 1599 | wallet.clone(), 1600 | electrum_url.to_string(), 1601 | whitelisted_trades, 1602 | maker_trades, 1603 | ) 1604 | .await; 1605 | 1606 | // Disconnect our peers and stop accepting new connections. This ensures we don't continue 1607 | // updating our channel data after we've stopped the background processor. 1608 | stop_listen_connect.store(true, Ordering::Release); 1609 | peer_manager.disconnect_all_peers(); 1610 | 1611 | // Stop the background processor. 1612 | bp_exit.send(()).unwrap(); 1613 | background_processor.await.unwrap().unwrap(); 1614 | } 1615 | 1616 | #[tokio::main] 1617 | pub async fn main() { 1618 | #[cfg(not(target_os = "windows"))] 1619 | { 1620 | // Catch Ctrl-C with a dummy signal handler. 1621 | unsafe { 1622 | let mut new_action: libc::sigaction = core::mem::zeroed(); 1623 | let mut old_action: libc::sigaction = core::mem::zeroed(); 1624 | 1625 | extern "C" fn dummy_handler( 1626 | _: libc::c_int, _: *const libc::siginfo_t, _: *const libc::c_void, 1627 | ) { 1628 | } 1629 | 1630 | new_action.sa_sigaction = dummy_handler as libc::sighandler_t; 1631 | new_action.sa_flags = libc::SA_SIGINFO; 1632 | 1633 | libc::sigaction( 1634 | libc::SIGINT, 1635 | &new_action as *const libc::sigaction, 1636 | &mut old_action as *mut libc::sigaction, 1637 | ); 1638 | } 1639 | } 1640 | 1641 | start_ldk().await; 1642 | } 1643 | -------------------------------------------------------------------------------- /src/proxy.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use amplify::s; 3 | use reqwest::header::CONTENT_TYPE; 4 | use reqwest::{multipart, Body, Client}; 5 | use serde::{Deserialize, Serialize}; 6 | use tokio::fs::File; 7 | use tokio_util::codec::{BytesCodec, FramedRead}; 8 | 9 | use std::path::PathBuf; 10 | 11 | const JSON: &str = "application/json"; 12 | 13 | #[derive(Debug, Deserialize, Serialize, Clone)] 14 | pub struct JsonRpcError { 15 | pub(crate) code: i64, 16 | message: String, 17 | } 18 | 19 | #[derive(Debug, Deserialize, Serialize)] 20 | pub struct JsonRpcRequest

{ 21 | method: String, 22 | jsonrpc: String, 23 | id: Option, 24 | params: Option

, 25 | } 26 | 27 | #[derive(Debug, Deserialize, Serialize, Clone)] 28 | pub struct JsonRpcResponse { 29 | id: Option, 30 | pub(crate) result: Option, 31 | pub(crate) error: Option, 32 | } 33 | 34 | #[derive(Debug, Deserialize, Serialize)] 35 | pub struct BlindedUtxoParam { 36 | blinded_utxo: String, 37 | } 38 | pub async fn post_consignment( 39 | proxy_client: Client, url: &str, consignment_id: String, consignment_path: PathBuf, 40 | ) -> Result, Error> { 41 | let file = File::open(consignment_path.clone()).await?; 42 | let stream = FramedRead::new(file, BytesCodec::new()); 43 | let file_name = consignment_path 44 | .clone() 45 | .file_name() 46 | .map(|filename| filename.to_string_lossy().into_owned()) 47 | .expect("valid file name"); 48 | let consignment_file = multipart::Part::stream(Body::wrap_stream(stream)).file_name(file_name); 49 | 50 | let params = serde_json::to_string(&BlindedUtxoParam { blinded_utxo: consignment_id }) 51 | .expect("valid param"); 52 | let form = multipart::Form::new() 53 | .text("method", "consignment.post") 54 | .text("jsonrpc", "2.0") 55 | .text("id", "1") 56 | .text("params", params) 57 | .part("file", consignment_file); 58 | Ok(proxy_client.post(url).multipart(form).send().await?.json::>().await?) 59 | } 60 | 61 | pub async fn get_consignment( 62 | proxy_client: Client, url: &str, consignment_id: String, 63 | ) -> Result, reqwest::Error> { 64 | let body = JsonRpcRequest { 65 | method: s!("consignment.get"), 66 | jsonrpc: s!("2.0"), 67 | id: None, 68 | params: Some(BlindedUtxoParam { blinded_utxo: consignment_id }), 69 | }; 70 | proxy_client 71 | .post(url) 72 | .header(CONTENT_TYPE, JSON) 73 | .json(&body) 74 | .send() 75 | .await? 76 | .json::>() 77 | .await 78 | } 79 | -------------------------------------------------------------------------------- /src/rgb_utils.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | use std::time::SystemTime; 3 | 4 | use amplify::RawArray; 5 | use bdk::bitcoin::psbt::PartiallySignedTransaction; 6 | use bdk::bitcoin::OutPoint; 7 | use bdk::database::SqliteDatabase; 8 | use bdk::Wallet; 9 | use bitcoin_30::hashes::Hash; 10 | use bitcoin_30::psbt::PartiallySignedTransaction as RgbPsbt; 11 | use bp::seals::txout::blind::SingleBlindSeal; 12 | use bp::seals::txout::CloseMethod; 13 | use bp::Outpoint as RgbOutpoint; 14 | use lightning::rgb_utils::STATIC_BLINDING; 15 | use rgb::Runtime; 16 | use rgb_core::{Operation, Opout}; 17 | use rgb_schemata::{nia_rgb20, nia_schema}; 18 | use rgbstd::containers::{Bindle, BuilderSeal, Transfer as RgbTransfer}; 19 | use rgbstd::contract::{ContractId, GenesisSeal, GraphSeal}; 20 | use rgbstd::interface::{rgb20, ContractBuilder, TransitionBuilder, TypedState}; 21 | use rgbstd::persistence::Inventory; 22 | use rgbstd::stl::{ 23 | Amount, AssetNaming, ContractData, DivisibleAssetSpec, Name, Precision, RicardianContract, 24 | Ticker, Timestamp, 25 | }; 26 | use rgbstd::Txid as RgbTxid; 27 | use rgbwallet::psbt::opret::OutputOpret; 28 | use rgbwallet::psbt::{PsbtDbc, RgbExt, RgbInExt}; 29 | use seals::txout::blind::BlindSeal; 30 | use seals::txout::{ExplicitSeal, TxPtr}; 31 | use std::collections::{BTreeMap, HashMap}; 32 | use std::convert::TryFrom; 33 | use std::str::FromStr; 34 | 35 | use crate::bdk_utils::sync_wallet; 36 | use crate::error::Error; 37 | 38 | pub(crate) fn get_rgb_total_amount( 39 | contract_id: ContractId, runtime: &Runtime, wallet_arc: Arc>>, 40 | electrum_url: String, 41 | ) -> Result { 42 | let asset_owned_values = 43 | get_asset_owned_values(contract_id, runtime, wallet_arc, electrum_url)?; 44 | Ok(asset_owned_values.iter().map(|(_, (_, amt))| amt).sum()) 45 | } 46 | 47 | pub(crate) fn get_asset_owned_values( 48 | contract_id: ContractId, runtime: &Runtime, wallet_arc: Arc>>, 49 | electrum_url: String, 50 | ) -> Result, Error> { 51 | let wallet = wallet_arc.lock().unwrap(); 52 | sync_wallet(&wallet, electrum_url); 53 | let unspents_outpoints: Vec = 54 | wallet.list_unspent().expect("valid unspent list").iter().map(|u| u.outpoint).collect(); 55 | let outpoints: Vec = unspents_outpoints 56 | .iter() 57 | .map(|o| RgbOutpoint::new(RgbTxid::from_str(&o.txid.to_string()).unwrap(), o.vout)) 58 | .collect(); 59 | let history = runtime.debug_history().get(&contract_id).ok_or(Error::UnknownContractId)?; 60 | let mut contract_state = BTreeMap::new(); 61 | for output in history.fungibles() { 62 | if outpoints.contains(&output.seal) { 63 | contract_state.insert(output.opout, (output.seal, output.state.value.as_u64())); 64 | } 65 | } 66 | Ok(contract_state) 67 | } 68 | 69 | pub(crate) fn update_transition_beneficiary( 70 | psbt: &PartiallySignedTransaction, beneficiaries: &mut Vec>>, 71 | mut asset_transition_builder: TransitionBuilder, assignment_id: u16, amt_rgb: u64, 72 | ) -> (u32, TransitionBuilder) { 73 | let mut seal_vout = 0; 74 | if let Some((index, _)) = psbt 75 | .clone() 76 | .unsigned_tx 77 | .output 78 | .iter_mut() 79 | .enumerate() 80 | .find(|(_, o)| o.script_pubkey.is_op_return()) 81 | { 82 | seal_vout = index as u32 ^ 1; 83 | } 84 | let seal = BuilderSeal::Revealed(GraphSeal::with_vout( 85 | CloseMethod::OpretFirst, 86 | seal_vout, 87 | STATIC_BLINDING, 88 | )); 89 | beneficiaries.push(seal); 90 | asset_transition_builder = asset_transition_builder 91 | .add_raw_state_static(assignment_id, seal, TypedState::Amount(amt_rgb)) 92 | .expect("ok"); 93 | (seal_vout, asset_transition_builder) 94 | } 95 | 96 | pub(crate) trait RgbUtilities { 97 | fn issue_contract( 98 | &mut self, amount: u64, outpoint: OutPoint, ticker: String, name: String, precision: u8, 99 | ) -> ContractId; 100 | 101 | fn send_rgb( 102 | &mut self, contract_id: ContractId, psbt: PartiallySignedTransaction, 103 | asset_transition_builder: TransitionBuilder, beneficiaries: Vec>, 104 | ) -> (PartiallySignedTransaction, Bindle); 105 | } 106 | 107 | impl RgbUtilities for Runtime { 108 | fn issue_contract( 109 | &mut self, amount: u64, outpoint: OutPoint, ticker: String, name: String, precision: u8, 110 | ) -> ContractId { 111 | let spec = DivisibleAssetSpec { 112 | naming: AssetNaming { 113 | ticker: Ticker::try_from(ticker).expect("valid ticker"), 114 | name: Name::try_from(name).expect("valid name"), 115 | details: None, 116 | }, 117 | precision: Precision::try_from(precision).expect("valid precision"), 118 | }; 119 | let terms = RicardianContract::default(); 120 | let contract_data = ContractData { terms, media: None }; 121 | let created_at = 122 | SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); 123 | let created = Timestamp::from(i32::try_from(created_at).unwrap()); 124 | let seal = ExplicitSeal::::from_str(&format!("opret1st:{outpoint}")).unwrap(); 125 | let seal = GenesisSeal::from(seal); 126 | 127 | let builder = ContractBuilder::with(rgb20(), nia_schema(), nia_rgb20()) 128 | .expect("valid contract builder") 129 | .set_chain(self.chain()) 130 | .add_global_state("spec", spec) 131 | .expect("invalid spec") 132 | .add_global_state("data", contract_data) 133 | .expect("invalid data") 134 | .add_global_state("issuedSupply", Amount::from(amount)) 135 | .expect("invalid issuedSupply") 136 | .add_global_state("created", created) 137 | .expect("invalid created") 138 | .add_fungible_state("assetOwner", seal, amount) 139 | .expect("invalid global state data"); 140 | let contract = builder.issue_contract().expect("failure issuing contract"); 141 | let contract_id = contract.contract_id(); 142 | let validated_contract = contract 143 | .validate(self.resolver()) 144 | .expect("internal error: failed validating self-issued contract"); 145 | self.import_contract(validated_contract).expect("failure importing issued contract"); 146 | contract_id 147 | } 148 | 149 | fn send_rgb( 150 | &mut self, contract_id: ContractId, psbt: PartiallySignedTransaction, 151 | asset_transition_builder: TransitionBuilder, beneficiaries: Vec>, 152 | ) -> (PartiallySignedTransaction, Bindle) { 153 | let mut psbt = RgbPsbt::from_str(&psbt.to_string()).unwrap(); 154 | let prev_outputs = psbt 155 | .unsigned_tx 156 | .input 157 | .iter() 158 | .map(|txin| txin.previous_output) 159 | .map(|outpoint| RgbOutpoint::new(outpoint.txid.to_byte_array().into(), outpoint.vout)) 160 | .collect::>(); 161 | let mut asset_transition_builder = asset_transition_builder; 162 | for (opout, _state) in 163 | self.state_for_outpoints(contract_id, prev_outputs.iter().copied()).expect("ok") 164 | { 165 | asset_transition_builder = 166 | asset_transition_builder.add_input(opout).expect("valid input"); 167 | } 168 | let transition = asset_transition_builder 169 | .complete_transition(contract_id) 170 | .expect("should complete transition"); 171 | let mut contract_inputs = HashMap::>::new(); 172 | for outpoint in prev_outputs { 173 | for id in self.contracts_by_outpoints([outpoint]).expect("ok") { 174 | contract_inputs.entry(id).or_default().push(outpoint); 175 | } 176 | } 177 | let inputs = contract_inputs.remove(&contract_id).unwrap_or_default(); 178 | for (input, txin) in psbt.inputs.iter_mut().zip(&psbt.unsigned_tx.input) { 179 | let prevout = txin.previous_output; 180 | let outpoint = RgbOutpoint::new(prevout.txid.to_byte_array().into(), prevout.vout); 181 | if inputs.contains(&outpoint) { 182 | input.set_rgb_consumer(contract_id, transition.id()).expect("ok"); 183 | } 184 | } 185 | psbt.push_rgb_transition(transition).expect("ok"); 186 | let bundles = psbt.rgb_bundles().expect("able to get bundles"); 187 | let (opreturn_index, _) = psbt 188 | .unsigned_tx 189 | .output 190 | .iter() 191 | .enumerate() 192 | .find(|(_, o)| o.script_pubkey.is_op_return()) 193 | .expect("psbt should have an op_return output"); 194 | let (_, opreturn_output) = 195 | psbt.outputs.iter_mut().enumerate().find(|(i, _)| i == &opreturn_index).unwrap(); 196 | opreturn_output.set_opret_host().expect("cannot set opret host"); 197 | psbt.rgb_bundle_to_lnpbp4().expect("ok"); 198 | let anchor = psbt.dbc_conclude_static(CloseMethod::OpretFirst).expect("should conclude"); 199 | let witness_txid = psbt.unsigned_tx.txid(); 200 | self.consume_anchor(anchor).expect("should consume anchor"); 201 | for (id, bundle) in bundles { 202 | self.consume_bundle(id, bundle, witness_txid.to_byte_array().into()) 203 | .expect("should consume bundle"); 204 | } 205 | let beneficiaries: Vec> = beneficiaries 206 | .into_iter() 207 | .map(|b| match b { 208 | BuilderSeal::Revealed(graph_seal) => BuilderSeal::Revealed( 209 | graph_seal.resolve(RgbTxid::from_raw_array(witness_txid.to_byte_array())), 210 | ), 211 | BuilderSeal::Concealed(seal) => BuilderSeal::Concealed(seal), 212 | }) 213 | .collect(); 214 | let transfer = self.transfer(contract_id, beneficiaries).expect("valid transfer"); 215 | 216 | let psbt = PartiallySignedTransaction::from_str(&psbt.to_string()).unwrap(); 217 | 218 | (psbt, transfer) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/swap.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use lightning::ln::PaymentHash; 4 | use rgb::contract::ContractId; 5 | 6 | use crate::hex_utils; 7 | 8 | #[derive(Debug, Clone, Copy)] 9 | pub enum SwapType { 10 | BuyAsset { amount_rgb: u64, amount_msats: u64 }, 11 | SellAsset { amount_rgb: u64, amount_msats: u64 }, 12 | } 13 | 14 | impl SwapType { 15 | pub fn opposite(self) -> Self { 16 | match self { 17 | SwapType::BuyAsset { amount_rgb, amount_msats } => { 18 | SwapType::SellAsset { amount_rgb, amount_msats } 19 | } 20 | SwapType::SellAsset { amount_rgb, amount_msats } => { 21 | SwapType::BuyAsset { amount_rgb, amount_msats } 22 | } 23 | } 24 | } 25 | 26 | pub fn is_buy(&self) -> bool { 27 | matches!(self, SwapType::BuyAsset { .. }) 28 | } 29 | 30 | pub fn amount_msats(&self) -> u64 { 31 | match self { 32 | SwapType::BuyAsset { amount_msats, .. } | SwapType::SellAsset { amount_msats, .. } => { 33 | *amount_msats 34 | } 35 | } 36 | } 37 | pub fn amount_rgb(&self) -> u64 { 38 | match self { 39 | SwapType::BuyAsset { amount_rgb, .. } | SwapType::SellAsset { amount_rgb, .. } => { 40 | *amount_rgb 41 | } 42 | } 43 | } 44 | 45 | pub fn side(&self) -> &'static str { 46 | match self { 47 | SwapType::BuyAsset { .. } => "buy", 48 | SwapType::SellAsset { .. } => "sell", 49 | } 50 | } 51 | } 52 | 53 | #[derive(Debug)] 54 | pub struct SwapString { 55 | pub asset_id: ContractId, 56 | pub swap_type: SwapType, 57 | pub expiry: u64, 58 | pub payment_hash: PaymentHash, 59 | } 60 | 61 | impl std::str::FromStr for SwapString { 62 | type Err = &'static str; 63 | 64 | fn from_str(s: &str) -> Result { 65 | let mut iter = s.split(":"); 66 | let amount = iter.next(); 67 | let asset_id = iter.next(); 68 | let side = iter.next(); 69 | let price = iter.next(); 70 | let expiry = iter.next(); 71 | let payment_hash = iter.next(); 72 | 73 | if payment_hash.is_none() || iter.next().is_some() { 74 | return Err("Parsing swap string: wrong number of parts"); 75 | } 76 | 77 | let amount = amount.unwrap().parse::(); 78 | let asset_id = ContractId::from_str(asset_id.unwrap()); 79 | let price = price.unwrap().parse::(); 80 | let expiry = expiry.unwrap().parse::(); 81 | let payment_hash = hex_utils::to_vec(payment_hash.unwrap()) 82 | .and_then(|vec| vec.try_into().ok()) 83 | .map(|slice| PaymentHash(slice)); 84 | 85 | if amount.is_err() 86 | || asset_id.is_err() 87 | || price.is_err() 88 | || expiry.is_err() 89 | || payment_hash.is_none() 90 | { 91 | return Err("Parsing swap string: unable to parse parts"); 92 | } 93 | 94 | let amount = amount.unwrap(); 95 | let asset_id = asset_id.unwrap(); 96 | let price = price.unwrap(); 97 | let expiry = expiry.unwrap(); 98 | let payment_hash = payment_hash.unwrap(); 99 | 100 | if amount == 0 || price == 0 || expiry == 0 { 101 | return Err("Parsing swap string: amount, price and expiry should be non-zero"); 102 | } 103 | 104 | let swap_type = match side { 105 | Some("buy") => SwapType::BuyAsset { amount_rgb: amount, amount_msats: amount * price }, 106 | Some("sell") => { 107 | SwapType::SellAsset { amount_rgb: amount, amount_msats: amount * price } 108 | } 109 | _ => { 110 | return Err("Invalid swap type"); 111 | } 112 | }; 113 | 114 | Ok(SwapString { asset_id, swap_type, expiry, payment_hash }) 115 | } 116 | } 117 | 118 | pub fn get_current_timestamp() -> u64 { 119 | std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() 120 | } 121 | -------------------------------------------------------------------------------- /test_data/test_cookie: -------------------------------------------------------------------------------- 1 | testuser:testpassword -------------------------------------------------------------------------------- /test_data/test_cookie_bad: -------------------------------------------------------------------------------- 1 | testuser -------------------------------------------------------------------------------- /test_data/test_env_file: -------------------------------------------------------------------------------- 1 | RPC_USER=testuser 2 | RPC_PASSWORD=testpassword -------------------------------------------------------------------------------- /test_data/test_env_file_bad: -------------------------------------------------------------------------------- 1 | RPC_USER=testuser 2 | RPC_PASSWORD -------------------------------------------------------------------------------- /tests/common.sh: -------------------------------------------------------------------------------- 1 | #!/use/bin/env bash 2 | 3 | 4 | # BITCOIN_CLI, COMPOSE and TMUX_CMD vars expected to be set in the environment 5 | VERBOSE=${VERBOSE:-0} 6 | TIMESTAMP=$(date +%s%3N) 7 | 8 | T_1=30 9 | 10 | NODE1_PORT=9735 11 | NODE2_PORT=9736 12 | NODE3_PORT=9737 13 | 14 | # shell colors 15 | C0='\033[0;31m' # red 16 | C1='\033[0;32m' # green 17 | C2='\033[0;33m' # orange 18 | C3='\033[0;34m' # blue 19 | NC='\033[0m' # No Color 20 | 21 | 22 | _exit() { 23 | timestamp 24 | _latest_logs 25 | echo 26 | printf "\n${C0}ERR: %s${NC}\n" "$@" 27 | exit 3 28 | } 29 | 30 | _tit() { 31 | printf "\n${C3}========[ %s ]${NC}\n" "$@" 32 | } 33 | 34 | _subtit() { 35 | printf "${C2}==== %s${NC}\n" "$@" 36 | } 37 | 38 | _debug() { 39 | [ "$VERBOSE" != 0 ] && printf "== %s\n" "$@" >&2 40 | } 41 | 42 | _out() { 43 | printf "${C1}--> %s${NC}\n" "$@" 44 | } 45 | 46 | _get_last_text() { 47 | local pane pattern 48 | pane="$1" 49 | pattern="$2" 50 | $TMUX_CMD capture-pane -ep -t "$pane" -S -20 \ 51 | | tac | sed -n "0,/$pattern/p" | tac 52 | } 53 | export -f _get_last_text 54 | 55 | _latest_logs() { 56 | [ "$VERBOSE" = 0 ] && return 57 | _tit "latest node logs" 58 | for node_num in 1 2 3; do 59 | _subtit "node $node_num" 60 | $TMUX_CMD capture-pane -ep -t "node$node_num" -S -20 | grep -v '^$' 61 | done 62 | } 63 | 64 | _wait_for_text() { 65 | local timeout pane pattern lines 66 | timeout="$1" 67 | pane="$2" 68 | pattern="$3" 69 | lines="${4:-0}" 70 | _debug "expecting \"$pattern\"" 71 | timeout --foreground "$timeout" bash </dev/null; do 174 | sleep 1 175 | done 176 | } 177 | 178 | issue_asset() { 179 | local rgb_amt=1000 180 | _tit "issue RGB asset ($rgb_amt)" 181 | $TMUX_CMD send-keys -t node1 "issueasset $rgb_amt USDT Tether 0" C-m 182 | ASSET_ID=$(_wait_for_text_multi 20 node1 "issueasset" "Asset ID:" |awk '{print $NF}') 183 | _out "asset ID: $ASSET_ID" 184 | } 185 | 186 | asset_balance() { 187 | local num expected balance 188 | num="$1" 189 | expected="${2:--1}" 190 | _subtit "asset balance on node $num" 191 | $TMUX_CMD send-keys -t "node$num" "assetbalance $ASSET_ID" C-m 192 | balance="$(_wait_for_text_multi $T_1 "node$num" "assetbalance" "Asset balance"\ 193 | | grep -Eo '[0-9]+$')" 194 | _out "asset balance: $balance" 195 | if [ "$expected" != -1 ]; then 196 | [ "$balance" != "$expected" ] && \ 197 | _exit "balance does not match the expected one ($expected)" 198 | fi 199 | _wait_for_text_multi $T_1 "node$num" "assetbalance" ">" >/dev/null 200 | } 201 | 202 | blind() { 203 | local num="$1" 204 | _tit "generate blinded UTXO on node $num" 205 | $TMUX_CMD send-keys -t "node$num" "receiveasset" C-m 206 | blinded_utxo="$(_wait_for_text_multi $T_1 "node$num" "receiveasset" "Blinded UTXO:" \ 207 | | grep -Eo '[0-9a-zA-Z]+$')" 208 | _out "blinded UTXO: $blinded_utxo" 209 | } 210 | 211 | send_assets() { 212 | local num rgb_amt 213 | num="$1" 214 | rgb_amt="$2" 215 | _tit "send $rgb_amt assets on-chain from node $num to blinded UTXO $blinded_utxo" 216 | $TMUX_CMD send-keys -t "node$num" "sendasset $ASSET_ID $rgb_amt $blinded_utxo" C-m 217 | timestamp 218 | check "$num" 219 | _wait_for_text_multi $T_1 "node$num" "sendasset" "RGB send complete" 220 | timestamp 221 | } 222 | 223 | refresh() { 224 | local num="$1" 225 | _tit "refresh on node $num" 226 | $TMUX_CMD send-keys -t "node$num" "refresh" C-m 227 | timestamp 228 | check "$num" 229 | _wait_for_text_multi $T_1 "node$num" "refresh" "Refresh complete" 230 | timestamp 231 | } 232 | 233 | open_colored_channel() { 234 | open_colored_channel_custom_sats_amt $1 $2 $3 $4 $5 30010 $6 235 | } 236 | 237 | open_big_colored_channel() { 238 | open_colored_channel_custom_sats_amt $1 $2 $3 $4 $5 16777215 $6 239 | } 240 | 241 | open_colored_channel_custom_sats_amt() { 242 | local src_num dst_num dst_port dst_id rgb_amt sats_amt current_chan_num 243 | src_num="$1" 244 | dst_num="$2" 245 | dst_port="$3" 246 | dst_id="$4" 247 | rgb_amt="$5" 248 | sats_amt="$6" 249 | current_chan_num="${7:-0}" 250 | _tit "open channel from node $src_num to node $dst_num with $rgb_amt assets" 251 | $TMUX_CMD send-keys -t "node$src_num" "opencoloredchannel $dst_id@127.0.0.1:$dst_port $sats_amt 1394000 $ASSET_ID $rgb_amt --public" C-m 252 | check "$src_num" 253 | _wait_for_text_multi $T_1 "node$src_num" "opencoloredchannel" "HANDLED ACCEPT CHANNEL" 254 | timestamp 255 | _wait_for_text_multi $T_1 "node$src_num" "opencoloredchannel" "FUNDING COMPLETED" 256 | timestamp 257 | _wait_for_text_multi $T_1 "node$src_num" "opencoloredchannel" "HANDLED FUNDING SIGNED" 258 | timestamp 259 | check "$dst_num" 260 | _wait_for_text $T_1 "node$dst_num" "HANDLED OPEN CHANNEL" 261 | timestamp 262 | _wait_for_text_multi $T_1 "node$dst_num" "HANDLED OPEN CHANNEL" "HANDLED FUNDING CREATED" 263 | timestamp 264 | 265 | mine 6 266 | check "$src_num" 267 | _wait_for_text_multi $T_1 "node$src_num" "> mine" "EVENT: Channel .* with peer .* is ready to be used" 268 | timestamp 269 | check "$dst_num" 270 | _wait_for_text_multi $T_1 "node$dst_num" "HANDLED OPEN CHANNEL" "EVENT: Channel .* with peer .* is ready to be used" 271 | timestamp 272 | 273 | local lines channels chan_peer_line chan_id_line 274 | $TMUX_CMD send-keys -t "node$src_num" "listchannels" C-m 275 | sleep 1 276 | lines=$(((current_chan_num+1)*20)) 277 | channels="$(_wait_for_text 5 "node$src_num" "listchannels" $lines | sed -n '/^\[/,/^\]/p' | sed -n '/^\[/,/^\]/p')" 278 | chan_peer_line=$(echo "$channels" | grep -n "$dst_id" |cut -d: -f1) 279 | chan_id_line=$((chan_peer_line-2)) 280 | CHANNEL_ID=$(echo "$channels" | sed -n "${chan_id_line},${chan_id_line}p" | grep -Eo '[0-9a-f]{64}') 281 | _out "channel ID: $CHANNEL_ID" 282 | } 283 | 284 | open_vanilla_channel() { 285 | local src_num dst_num dst_port dst_id msat_amount 286 | src_num="$1" 287 | dst_num="$2" 288 | dst_port="$3" 289 | dst_id="$4" 290 | msat_amount="$5" 291 | _tit "open channel from node $src_num to node $dst_num of $msat_amount mSAT" 292 | $TMUX_CMD send-keys -t "node$src_num" "openchannel $dst_id@127.0.0.1:$dst_port $msat_amount 546000 --public" C-m 293 | check $src_num 294 | _wait_for_text_multi $T_1 "node$src_num" "openchannel" "HANDLED ACCEPT CHANNEL" 295 | timestamp 296 | _wait_for_text_multi $T_1 "node$src_num" "openchannel" "FUNDING COMPLETED" 297 | timestamp 298 | _wait_for_text_multi $T_1 "node$src_num" "openchannel" "HANDLED FUNDING SIGNED" 299 | timestamp 300 | check $dst_num 301 | _wait_for_text $T_1 "node$dst_num" "HANDLED OPEN CHANNEL" 302 | timestamp 303 | _wait_for_text_multi $T_1 "node$dst_num" "HANDLED OPEN CHANNEL" "HANDLED FUNDING CREATED" 304 | timestamp 305 | 306 | mine 6 307 | sleep 3 308 | 309 | $TMUX_CMD send-keys -t "node$src_num" "listchannels" C-m 310 | sleep 1 311 | CHANNEL_ID=$(_wait_for_text 5 "node$src_num" "[^_]channel_id:" \ 312 | | head -1 | grep -Eo '[0-9a-f]{64}') 313 | _out "channel ID: $CHANNEL_ID" 314 | } 315 | 316 | 317 | list_channels() { 318 | local node_num chan_num lines text matches 319 | node_num="$1" 320 | chan_num="${2:-1}" 321 | lines=$((chan_num*20)) 322 | _subtit "list channels ($chan_num expected) on node $node_num" 323 | $TMUX_CMD send-keys -t "node$node_num" "listchannels" C-m 324 | sleep 1 325 | text="$(_wait_for_text 5 "node$node_num" "listchannels" $lines | sed -n '/^\[/,/^\]/p')" 326 | echo "$text" 327 | matches=$(echo "$text" | grep -c "is_channel_ready: true") 328 | [ "$matches" = "$chan_num" ] || _exit "one or more channels not ready" 329 | } 330 | 331 | list_payments() { 332 | local node_num payment_num lines text matches 333 | node_num="$1" 334 | payment_num="${2:-1}" 335 | lines=$((payment_num*10)) 336 | _tit "list payments on node $node_num" 337 | $TMUX_CMD send-keys -t "node$node_num" "listpayments" C-m 338 | text="$(_wait_for_text 5 "node$node_num" "listpayments" $lines | sed -n '/^\[/,/^\]/p')" 339 | echo "$text" 340 | matches=$(echo "$text" | grep -c "payment_hash:") 341 | [ "$matches" = "$payment_num" ] || _exit "payment number doesn't match the expected one" 342 | _wait_for_text_multi $T_1 "node$node_num" "listpayments" ">" >/dev/null 343 | } 344 | 345 | list_unspents() { 346 | [ "$VERBOSE" = 0 ] && return 347 | local num="$1" 348 | _tit "list unspents on node $num" 349 | $TMUX_CMD send-keys -t "node$num" "listunspent" C-m 350 | _wait_for_text_multi 5 "node$num" "listunspent" "Unspents:" 40 | sed -n '/^Unspents:/,/^>/p' 351 | } 352 | 353 | close_channel() { 354 | local src_num dst_num dst_id chan_id 355 | src_num="$1" 356 | dst_num="$2" 357 | dst_id="$3" 358 | chan_id="${4:-$CHANNEL_ID}" 359 | _tit "close channel with peer $dst_id from node $src_num (cooperative)" 360 | $TMUX_CMD send-keys -t "node$src_num" "closechannel $chan_id $dst_id" C-m 361 | timestamp 362 | check "$src_num" 363 | _wait_for_text_multi $T_1 "node$src_num" "closechannel" "HANDLED SHUTDOWN" 364 | timestamp 365 | check "$dst_num" 366 | _wait_for_text_multi $T_1 "node$dst_num" "HANDLED SHUTDOWN" "EVENT: Channel .* closed due to: CooperativeClosure" 367 | timestamp 368 | 369 | mine 6 370 | check "$dst_num" 371 | _wait_for_text_multi $T_1 "node$dst_num" "EVENT: Channel .* closed" "Event::SpendableOutputs complete" 372 | timestamp 373 | 374 | check "$src_num" 375 | _wait_for_text_multi $T_1 "node$src_num" "EVENT: Channel .* closed" "Event::SpendableOutputs complete" 376 | timestamp 377 | mine 1 378 | } 379 | 380 | forceclose_channel_init() { 381 | local src_num dst_id chan_id 382 | src_num="$1" 383 | dst_id="$2" 384 | chan_id="$3" 385 | _tit "close channel from node $src_num (unilateral)" 386 | $TMUX_CMD send-keys -t "node$src_num" "forceclosechannel $chan_id $dst_id" C-m 387 | timestamp 388 | check "$src_num" 389 | _wait_for_text_multi $T_1 "node$src_num" "forceclosechannel" "EVENT: Channel .* closed due to: HolderForceClosed" 390 | timestamp 391 | } 392 | 393 | forceclose_channel() { 394 | local src_num dst_num dst_id chan_id 395 | src_num="$1" 396 | dst_num="$2" 397 | dst_id="$3" 398 | chan_id="${4:-$CHANNEL_ID}" 399 | 400 | forceclose_channel_init "$src_num" "$dst_id" "$chan_id" 401 | 402 | check "$dst_num" 403 | _wait_for_text $T_1 "node$dst_num" "EVENT: Channel .* closed due to: CounterpartyForceClosed" 404 | timestamp 405 | 406 | mine 6 407 | check "$dst_num" 408 | _wait_for_text_multi $T_1 "node$dst_num" "EVENT: Channel .* closed" "Event::SpendableOutputs complete" 409 | timestamp 410 | 411 | mine 144 412 | check "$src_num" 413 | _wait_for_text_multi $T_1 "node$src_num" "forceclosechannel" "Event::SpendableOutputs complete" 414 | timestamp 415 | mine 1 416 | } 417 | 418 | colored_keysend_init() { 419 | local src_num dst_num dst_id rgb_amt 420 | src_num="$1" 421 | dst_num="$2" 422 | dst_id="$3" 423 | rgb_amt="$4" 424 | 425 | _tit "send $rgb_amt assets off-chain from node $src_num to node $dst_num" 426 | $TMUX_CMD send-keys -t "node$src_num" "coloredkeysend $dst_id 3000000 $ASSET_ID $rgb_amt" C-m 427 | timestamp 428 | check "$src_num" 429 | _wait_for_text_multi $T_1 "node$src_num" "keysend" "EVENT: initiated sending" 430 | timestamp 431 | } 432 | 433 | colored_keysend() { 434 | local src_num dst_num dst_id rgb_amt 435 | src_num="$1" 436 | dst_num="$2" 437 | dst_id="$3" 438 | rgb_amt="$4" 439 | 440 | colored_keysend_init "$src_num" "$dst_num" "$dst_id" "$rgb_amt" 441 | 442 | _wait_for_text_multi $T_1 "node$src_num" "keysend" "EVENT: successfully sent payment" 443 | timestamp 444 | _wait_for_text_multi $T_1 "node$src_num" "EVENT: successfully sent payment" "HANDLED REVOKE AND ACK" 445 | timestamp 446 | 447 | check "$dst_num" 448 | _wait_for_text $T_1 "node$dst_num" "EVENT: received payment" 449 | timestamp 450 | _wait_for_text_multi $T_1 "node$dst_num" "EVENT: received payment" "Event::PaymentClaimed end" 451 | timestamp 452 | _wait_for_text_multi $T_1 "node$dst_num" "Event::PaymentClaimed end" "HANDLED COMMITMENT SIGNED" 453 | timestamp 454 | } 455 | 456 | keysend_init() { 457 | local src_num dst_num dst_id sats_amt 458 | src_num="$1" 459 | dst_num="$2" 460 | dst_id="$3" 461 | sats_amt="$4" 462 | 463 | _tit "send $sats_amt sats off-chain from node $src_num to node $dst_num" 464 | $TMUX_CMD send-keys -t "node$src_num" "keysend $dst_id $sats_amt" C-m 465 | timestamp 466 | check "$src_num" 467 | _wait_for_text_multi $T_1 "node$src_num" "keysend" "EVENT: initiated sending" 468 | timestamp 469 | } 470 | 471 | keysend() { 472 | local src_num dst_num dst_id sats_amt 473 | src_num="$1" 474 | dst_num="$2" 475 | dst_id="$3" 476 | sats_amt="$4" 477 | 478 | keysend_init "$src_num" "$dst_num" "$dst_id" "$sats_amt" 479 | 480 | _wait_for_text_multi $T_1 "node$src_num" "keysend" "EVENT: successfully sent payment" 481 | timestamp 482 | _wait_for_text_multi $T_1 "node$src_num" "EVENT: successfully sent payment" "HANDLED REVOKE AND ACK" 483 | timestamp 484 | 485 | check "$dst_num" 486 | _wait_for_text $T_1 "node$dst_num" "EVENT: received payment" 487 | timestamp 488 | _wait_for_text_multi $T_1 "node$dst_num" "EVENT: received payment" "Event::PaymentClaimed end" 489 | timestamp 490 | _wait_for_text_multi $T_1 "node$dst_num" "Event::PaymentClaimed end" "HANDLED COMMITMENT SIGNED" 491 | timestamp 492 | } 493 | 494 | get_colored_invoice() { 495 | local num rgb_amt text pattern 496 | num="$1" 497 | rgb_amt="$2" 498 | 499 | _tit "get invoice for $rgb_amt assets from node $num" 500 | $TMUX_CMD send-keys -t "node$num" "getcoloredinvoice 3000000 900 $ASSET_ID $rgb_amt" C-m 501 | timestamp 502 | check "$num" 503 | pattern="SUCCESS: generated invoice: " 504 | INVOICE="$(_wait_for_text_multi $T_1 "node$num" \ 505 | 'getcoloredinvoice' "$pattern" 3 | sed "s/$pattern//" \ 506 | |grep -Eo '^[0-9a-z]+$' | sed -E ':a; N; $!ba; s/[\n ]//g')" 507 | timestamp 508 | _out "invoice: $INVOICE" 509 | } 510 | 511 | get_vanilla_invoice() { 512 | local num msat_amount text pattern 513 | num="$1" 514 | msat_amount="$2" 515 | 516 | _tit "get invoice for $msat_amount mSATs from node $num" 517 | $TMUX_CMD send-keys -t node$num "getinvoice $msat_amount 900" C-m 518 | timestamp 519 | check "$num" 520 | pattern="SUCCESS: generated invoice: " 521 | INVOICE="$(_wait_for_text_multi $T_1 "node$num" \ 522 | 'getinvoice' "$pattern" 3 | sed "s/$pattern//" \ 523 | |grep -Eo '^[0-9a-z]+$' | sed -E ':a; N; $!ba; s/[\n ]//g')" 524 | timestamp 525 | _out "invoice: $INVOICE" 526 | } 527 | 528 | send_payment() { 529 | local src_num dst_num invoice 530 | src_num="$1" 531 | dst_num="$2" 532 | invoice="$3" 533 | 534 | _tit "pay LN invoice from node $src_num" 535 | $TMUX_CMD send-keys -t "node$src_num" "sendpayment $invoice" C-m 536 | timestamp 537 | check "$src_num" 538 | _wait_for_text_multi $T_1 "node$src_num" "sendpayment" "EVENT: initiated sending" 539 | timestamp 540 | _wait_for_text_multi $T_1 "node$src_num" "sendpayment" "EVENT: successfully sent payment" 541 | timestamp 542 | 543 | check "$dst_num" 544 | _wait_for_text $T_1 "node$dst_num" "EVENT: received payment" 545 | timestamp 546 | _wait_for_text_multi $T_1 "node$dst_num" "EVENT: received payment" "Event::PaymentClaimed end" 547 | timestamp 548 | _wait_for_text_multi $T_1 "node$dst_num" "Event::PaymentClaimed end" "HANDLED COMMITMENT SIGNED" 549 | timestamp 550 | } 551 | 552 | exit_node() { 553 | local num 554 | num="$1" 555 | 556 | _subtit "exit node $num" 557 | $TMUX_CMD send-keys -t "node$num" "exit" C-m 558 | timestamp 559 | _wait_for_text_multi $T_1 "node$num" "exit" "Exiting node..." >/dev/null 560 | timestamp 561 | } 562 | 563 | start_node() { 564 | local num data port 565 | num="$1" 566 | 567 | case "$num" in 568 | 1) 569 | data="dataldk0" 570 | port="9735" 571 | ;; 572 | 2) 573 | data="dataldk1" 574 | port="9736" 575 | ;; 576 | 3) 577 | data="dataldk2" 578 | port="9737" 579 | ;; 580 | *) 581 | esac 582 | 583 | _subtit "start node $num" 584 | $TMUX_CMD send-keys -t "node$num" \ 585 | "target/debug/ldk-sample user:password@localhost:18443 $data/ $port regtest" C-m 586 | timestamp 587 | _wait_for_text_multi $T_1 "node$num" "target\/debug\/ldk-sample" "LDK startup successful." >/dev/null 588 | _wait_for_text_multi $T_1 "node$num" "LDK startup successful." ">" >/dev/null 589 | timestamp 590 | } 591 | 592 | check_channel_reestablish() { 593 | local num prevtext 594 | num="$1" 595 | prevtext="$2" 596 | 597 | check "$num" 598 | _wait_for_text_multi $T_1 "node$num" "$prevtext" "HANDLED CHANNEL READY" >/dev/null 599 | timestamp 600 | } 601 | 602 | send_swap() { 603 | local node exchange swaptype amt_msat amt_asset 604 | node="$1" 605 | exchange="$2" 606 | swaptype="$3" 607 | amt_msat="$4" 608 | amt_asset="$5" 609 | 610 | _tit "node $node swapping ($swaptype) $amt_msat msats for $amt_asset $ASSET_ID through node $exchange" 611 | $TMUX_CMD send-keys -t node$node "sendswap $exchange $swaptype $amt_msat $ASSET_ID $amt_asset" C-m 612 | timestamp 613 | check $node 614 | _wait_for_text_multi $T_1 node$node "sendswap" "EVENT: initiated swap" 615 | timestamp 616 | _wait_for_text_multi $T_1 node$node "sendswap" "EVENT: successfully sent payment" 617 | timestamp 618 | _wait_for_text $T_1 node$node "EVENT: received payment" 619 | timestamp 620 | _wait_for_text $T_1 node$node "Event::PaymentClaimed end" 621 | timestamp 622 | _wait_for_text_multi $T_1 node$node "Event::PaymentClaimed end" "HANDLED COMMITMENT SIGNED" 623 | timestamp 624 | } 625 | 626 | maker_init() { 627 | local node amount asset side price timeout 628 | node="$1" 629 | amount="$2" 630 | side="$3" 631 | timeout="$4" 632 | price="$5" 633 | 634 | _tit "node $node initializating trade (mm-side): swapping ($side) $amount of $ASSET_ID at $price msats/asset" 635 | timestamp 636 | $TMUX_CMD send-keys -t node$node "makerinit $amount $ASSET_ID $side $timeout $price" C-m 637 | swap_string=$(_wait_for_text 5 node$node "SUCCESS! swap_string =" |awk '{print $NF}') 638 | payment_secret=$(_wait_for_text 5 node$node "payment_secret: " |awk '{print $NF}') 639 | _out "swap_string: $swap_string" 640 | _out "payment_secret: $payment_secret" 641 | sleep 1 642 | } 643 | maker_init_amount_failure() { 644 | local node amount asset side price timeout 645 | node="$1" 646 | amount="$2" 647 | side="$3" 648 | timeout="$4" 649 | price="$5" 650 | 651 | _tit "node $node initializating trade (mm-side): swapping ($side) $amount of $ASSET_ID at $price msats/asset" 652 | timestamp 653 | $TMUX_CMD send-keys -t node$node "makerinit $amount $ASSET_ID $side $timeout $price" C-m 654 | _wait_for_text 5 node$node "ERROR: do not have enough RGB assets" 655 | } 656 | 657 | taker() { 658 | local node 659 | node="$1" 660 | 661 | _tit "node $node taking the trade $swap_string" 662 | $TMUX_CMD send-keys -t node$node "taker $swap_string" C-m 663 | taker_pk=$(_wait_for_text 5 node$node "our_pk: " |awk '{print $NF}') 664 | _out "taker_pk: $taker_pk" 665 | sleep 1 666 | } 667 | 668 | taker_expect_timeout() { 669 | local node 670 | node="$1" 671 | 672 | _tit "node $node taking the trade $swap_string" 673 | $TMUX_CMD send-keys -t node$node "taker $swap_string" C-m 674 | _wait_for_text_multi $T_1 node$node "taker" "ERROR: the swap offer has already expired" 675 | timestamp 676 | } 677 | 678 | taker_amount_failure() { 679 | local node 680 | node="$1" 681 | 682 | _tit "node $node taking the trade $swap_string" 683 | $TMUX_CMD send-keys -t node$node "taker $swap_string" C-m 684 | _wait_for_text 5 node$node "ERROR: do not have enough RGB assets" 685 | } 686 | 687 | taker_list() { 688 | local node text trades_num text 689 | node="$1" 690 | trades_num="$2" 691 | 692 | lines=$((trades_num*9)) 693 | 694 | _tit "listing whitelisted taker trades on node $node" 695 | $TMUX_CMD send-keys -t node$node "tradeslist taker" C-m 696 | text="$(_wait_for_text 5 "node$node" "tradeslist taker" $lines)" 697 | echo "$text" 698 | matches=$(echo "$text" | grep -c "side: .*") 699 | [ "$matches" = "$trades_num" ] || _exit "not enough trades" 700 | } 701 | 702 | maker_list() { 703 | local node text trades_num text 704 | node="$1" 705 | trades_num="$2" 706 | 707 | lines=$((trades_num*10)) 708 | 709 | _tit "listing whitelisted maker trades on node $node" 710 | $TMUX_CMD send-keys -t node$node "tradeslist maker" C-m 711 | text="$(_wait_for_text 5 "node$node" "tradeslist maker" $lines)" 712 | echo "$text" 713 | matches=$(echo "$text" | grep -c "side: .*") 714 | [ "$matches" = "$trades_num" ] || _exit "not enough trades" 715 | } 716 | 717 | maker_execute() { 718 | local node 719 | node="$1" 720 | 721 | _tit "node $node completing the trade..." 722 | $TMUX_CMD send-keys -t node$node "makerexecute $swap_string $payment_secret $taker_pk" C-m 723 | timestamp 724 | _wait_for_text_multi $T_1 node$node "makerexecute" "EVENT: initiated swap" 725 | timestamp 726 | _wait_for_text_multi $T_1 node$node "makerexecute" "EVENT: successfully sent payment" 727 | timestamp 728 | _wait_for_text $T_1 node$node "EVENT: received payment" 729 | timestamp 730 | _wait_for_text $T_1 node$node "Event::PaymentClaimed end" 731 | timestamp 732 | _wait_for_text_multi $T_1 node$node "Event::PaymentClaimed end" "HANDLED COMMITMENT SIGNED" 733 | timestamp 734 | 735 | } 736 | 737 | maker_execute_expect_failure() { 738 | local node taker_pk 739 | node="$1" 740 | taker_pk="$2" 741 | failure_node="$3" 742 | 743 | _tit "node $node completing the trade..." 744 | $TMUX_CMD send-keys -t node$node "makerexecute $swap_string $payment_secret $taker_pk" C-m 745 | timestamp 746 | _wait_for_text_multi $T_1 node$node "makerexecute" "EVENT: initiated swap" 747 | timestamp 748 | _wait_for_text $T_1 node$failure_node "ERROR: rejecting non-whitelisted swap" 749 | timestamp 750 | _wait_for_text_multi $T_1 node$node "makerexecute" "EVENT: Failed to send payment to payment hash .* RetriesExhausted>" 751 | timestamp 752 | 753 | } 754 | -------------------------------------------------------------------------------- /tests/scripts/close_coop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | create_utxos 3 12 | 13 | # issue asset 14 | issue_asset 15 | 16 | # open channel 17 | open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 18 | list_channels 1 19 | list_channels 2 20 | asset_balance 1 400 21 | 22 | # send assets 23 | colored_keysend 1 2 "$NODE2_ID" 100 24 | list_channels 1 25 | list_channels 2 26 | list_payments 1 27 | list_payments 2 28 | 29 | # close channel 30 | close_channel 1 2 "$NODE2_ID" 31 | asset_balance 1 900 32 | asset_balance 2 100 33 | 34 | # spend RGB assets on-chain 35 | _skip_remaining 36 | blind 3 37 | send_assets 1 700 38 | blind 3 39 | send_assets 2 50 40 | mine 1 41 | refresh 3 42 | asset_balance 1 200 43 | asset_balance 2 50 44 | asset_balance 3 750 45 | 46 | exit 0 47 | -------------------------------------------------------------------------------- /tests/scripts/close_coop_nobtc_acceptor.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 3 11 | 12 | # issue asset 13 | issue_asset 14 | 15 | # open channel 16 | open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 17 | list_channels 1 18 | list_channels 2 19 | asset_balance 1 400 20 | 21 | # send assets 22 | colored_keysend 1 2 "$NODE2_ID" 100 23 | list_channels 1 24 | list_channels 2 25 | list_payments 1 26 | list_payments 2 27 | 28 | # close channel 29 | close_channel 1 2 "$NODE2_ID" 30 | asset_balance 1 900 31 | asset_balance 2 100 32 | 33 | # spend RGB assets on-chain 34 | _skip_remaining 35 | blind 3 36 | send_assets 1 700 37 | blind 3 38 | create_utxos 2 39 | send_assets 2 50 40 | mine 1 41 | refresh 3 42 | asset_balance 1 200 43 | asset_balance 2 50 44 | asset_balance 3 750 45 | 46 | exit 0 47 | -------------------------------------------------------------------------------- /tests/scripts/close_coop_other_side.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | create_utxos 3 12 | 13 | # issue asset 14 | issue_asset 15 | 16 | # open channel 17 | open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 18 | list_channels 1 19 | list_channels 2 20 | asset_balance 1 400 21 | 22 | # send assets 23 | colored_keysend 1 2 "$NODE2_ID" 100 24 | list_channels 1 25 | list_channels 2 26 | list_payments 1 27 | list_payments 2 28 | 29 | # close channel 30 | close_channel 2 1 "$NODE1_ID" 31 | asset_balance 1 900 32 | asset_balance 2 100 33 | 34 | # spend RGB assets on-chain 35 | _skip_remaining 36 | blind 3 37 | send_assets 1 700 38 | blind 3 39 | send_assets 2 50 40 | mine 1 41 | refresh 3 42 | asset_balance 1 200 43 | asset_balance 2 50 44 | asset_balance 3 750 45 | 46 | exit 0 47 | -------------------------------------------------------------------------------- /tests/scripts/close_coop_vanilla.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | get_address 1 9 | fund_address $address 10 | mine 1 11 | 12 | # wait for bdk and ldk to sync up with electrs 13 | sleep 5 14 | 15 | # open channel 16 | open_vanilla_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 16777215 17 | list_channels 1 18 | list_channels 2 19 | 20 | # get invoice 21 | get_vanilla_invoice 2 3000000 22 | 23 | # send payment 24 | send_payment 1 2 "$INVOICE" 25 | list_channels 1 26 | list_channels 2 27 | list_payments 1 28 | list_payments 2 29 | 30 | close_channel 1 2 "$NODE2_ID" -------------------------------------------------------------------------------- /tests/scripts/close_coop_zero_balance.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | 12 | # issue asset 13 | issue_asset 14 | 15 | # open channel 16 | open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 1000 17 | list_channels 1 18 | list_channels 2 19 | asset_balance 1 0 20 | 21 | # close channel 22 | close_channel 1 2 "$NODE2_ID" 23 | asset_balance 1 1000 24 | asset_balance 2 0 25 | 26 | exit 0 27 | -------------------------------------------------------------------------------- /tests/scripts/close_force.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | create_utxos 3 12 | 13 | # issue asset 14 | issue_asset 15 | 16 | # open channel 17 | open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 18 | list_channels 1 19 | list_channels 2 20 | asset_balance 1 400 21 | 22 | # send assets 23 | colored_keysend 1 2 "$NODE2_ID" 100 24 | list_channels 1 25 | list_channels 2 26 | list_payments 1 27 | list_payments 2 28 | 29 | # force-close channel 30 | forceclose_channel 1 2 "$NODE2_ID" 31 | asset_balance 1 900 32 | asset_balance 2 100 33 | 34 | # spend RGB assets on-chain 35 | _skip_remaining 36 | blind 3 37 | send_assets 1 700 38 | blind 3 39 | send_assets 2 50 40 | mine 1 41 | refresh 3 42 | asset_balance 1 200 43 | asset_balance 2 50 44 | asset_balance 3 750 45 | 46 | exit 0 47 | -------------------------------------------------------------------------------- /tests/scripts/close_force_nobtc_acceptor.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | #create_utxos 2 11 | create_utxos 3 12 | 13 | # issue asset 14 | issue_asset 15 | 16 | # open channel 17 | open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 18 | list_channels 1 19 | list_channels 2 20 | asset_balance 1 400 21 | 22 | # send assets 23 | colored_keysend 1 2 "$NODE2_ID" 100 24 | list_channels 1 25 | list_channels 2 26 | list_payments 1 27 | list_payments 2 28 | 29 | # force-close channel 30 | forceclose_channel 1 2 "$NODE2_ID" 31 | asset_balance 1 900 32 | asset_balance 2 100 33 | 34 | exit 0 35 | -------------------------------------------------------------------------------- /tests/scripts/close_force_other_side.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | create_utxos 3 12 | 13 | # issue asset 14 | issue_asset 15 | 16 | # open channel 17 | open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 18 | list_channels 1 19 | list_channels 2 20 | asset_balance 1 400 21 | 22 | # send assets 23 | colored_keysend 1 2 "$NODE2_ID" 100 24 | list_channels 1 25 | list_channels 2 26 | list_payments 1 27 | list_payments 2 28 | 29 | # force-close channel 30 | forceclose_channel 2 1 "$NODE1_ID" 31 | asset_balance 1 900 32 | asset_balance 2 100 33 | 34 | # spend RGB assets on-chain 35 | _skip_remaining 36 | blind 3 37 | send_assets 1 700 38 | blind 3 39 | send_assets 2 50 40 | mine 1 41 | refresh 3 42 | asset_balance 1 200 43 | asset_balance 2 50 44 | asset_balance 3 750 45 | 46 | exit 0 47 | -------------------------------------------------------------------------------- /tests/scripts/close_force_pending_htlc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | create_utxos 3 12 | 13 | # issue asset 14 | issue_asset 15 | 16 | # send assets 17 | blind 2 18 | send_assets 1 400 19 | asset_balance 1 600 20 | 21 | # open channel 22 | open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 500 23 | channel12_id="$CHANNEL_ID" 24 | list_channels 1 25 | list_channels 2 26 | asset_balance 1 100 27 | 28 | # refresh 29 | refresh 2 30 | asset_balance 2 400 31 | 32 | # open channel 33 | open_colored_channel 2 3 "$NODE3_PORT" "$NODE3_ID" 400 1 34 | channel23_id="$CHANNEL_ID" 35 | list_channels 2 2 36 | list_channels 3 37 | asset_balance 2 0 38 | 39 | # send assets and exit intermediate node while payment is goind through 40 | colored_keysend_init 1 3 "$NODE3_ID" 50 41 | _wait_for_text "$T_1" node2 "HANDLED UPDATE ADD HTLC" 42 | timestamp 43 | _wait_for_text "$T_1" node2 "HANDLED REVOKE AND ACK" 44 | timestamp 45 | _subtit "exit from node 2" 46 | $TMUX_CMD send-keys -t node2 "" >/dev/null 2>&1 47 | timestamp 48 | sleep 10 49 | list_channels 1 50 | list_payments 1 51 | _wait_for_text_multi 5 node1 "listpayments" "htlc_direction: outbound" >/dev/null 52 | timestamp 53 | _wait_for_text_multi 5 node1 "listpayments" "htlc_status: pending" >/dev/null 54 | timestamp 55 | asset_balance 1 100 56 | 57 | # force-close channel from node 1 58 | forceclose_channel_init 1 "$NODE2_ID" "$channel12_id" 59 | 60 | mine 144 61 | _wait_for_text_multi "$T_1" node1 "mine" "Event::SpendableOutputs complete" 62 | timestamp 63 | mine 1 64 | asset_balance 1 550 65 | 66 | mine 100 67 | _wait_for_text_multi "$T_1" node1 "mine" "COMPLETED GET_FULLY_SIGNED_HTLC_TX" 68 | timestamp 69 | 70 | mine 144 71 | _wait_for_text_multi "$T_1" node1 "mine" "Event::SpendableOutputs complete" 72 | timestamp 73 | mine 1 74 | asset_balance 1 600 75 | 76 | exit 0 77 | -------------------------------------------------------------------------------- /tests/scripts/multi_open_close.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | create_utxos 3 12 | 13 | # issue asset 14 | issue_asset 15 | 16 | # open channel 17 | open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 18 | list_channels 1 19 | list_channels 2 20 | asset_balance 1 400 21 | 22 | # send assets 23 | colored_keysend 1 2 "$NODE2_ID" 100 24 | list_channels 1 25 | list_channels 2 26 | list_payments 1 27 | list_payments 2 28 | 29 | # close channel 30 | close_channel 1 2 "$NODE2_ID" 31 | asset_balance 1 900 32 | asset_balance 2 100 33 | 34 | # open channel 35 | open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 500 36 | list_channels 1 37 | list_channels 2 38 | asset_balance 1 400 39 | 40 | # send assets 41 | colored_keysend 1 2 "$NODE2_ID" 100 42 | list_channels 1 43 | list_channels 2 44 | list_payments 1 45 | list_payments 2 46 | 47 | # close channel 48 | close_channel 1 2 "$NODE2_ID" 49 | asset_balance 1 800 50 | asset_balance 2 200 51 | 52 | # spend RGB assets on-chain 53 | _skip_remaining 54 | blind 3 55 | send_assets 1 700 56 | blind 3 57 | send_assets 2 150 58 | mine 1 59 | refresh 3 60 | asset_balance 1 100 61 | asset_balance 2 50 62 | asset_balance 3 850 63 | 64 | exit 0 65 | -------------------------------------------------------------------------------- /tests/scripts/multihop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | create_utxos 3 12 | 13 | # issue asset 14 | issue_asset 15 | 16 | # send assets 17 | blind 2 18 | send_assets 1 400 19 | asset_balance 1 600 20 | 21 | # open channel 22 | open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 500 23 | channel12_id="$CHANNEL_ID" 24 | list_channels 1 25 | list_channels 2 26 | asset_balance 1 100 27 | 28 | # refresh 29 | refresh 2 30 | asset_balance 2 400 31 | 32 | # open channel 33 | open_colored_channel 2 3 "$NODE3_PORT" "$NODE3_ID" 300 1 34 | channel23_id="$CHANNEL_ID" 35 | list_channels 2 2 36 | list_channels 3 37 | asset_balance 2 100 38 | 39 | 40 | # send payment 41 | get_colored_invoice 3 50 42 | send_payment 1 3 "$INVOICE" 43 | 44 | list_channels 1 45 | list_channels 2 2 46 | list_channels 3 47 | list_payments 1 48 | list_payments 3 49 | 50 | # close channels 51 | close_channel 2 1 "$NODE1_ID" "$channel12_id" 52 | asset_balance 1 550 53 | asset_balance 2 150 54 | close_channel 3 2 "$NODE2_ID" "$channel23_id" 55 | asset_balance 2 400 56 | asset_balance 3 50 57 | 58 | # spend RGB assets on-chain 59 | _skip_remaining 60 | blind 3 61 | send_assets 1 200 62 | blind 3 63 | send_assets 2 150 64 | mine 1 65 | refresh 3 66 | blind 2 67 | send_assets 3 375 68 | refresh 2 69 | asset_balance 1 350 70 | asset_balance 2 625 71 | asset_balance 3 25 72 | 73 | exit 0 74 | -------------------------------------------------------------------------------- /tests/scripts/multiple_payments.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | create_utxos 3 12 | 13 | # issue asset 14 | issue_asset 15 | 16 | # open channel 17 | open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 18 | list_channels 1 19 | list_channels 2 20 | asset_balance 1 400 21 | 22 | # send assets (3 times) 23 | get_colored_invoice 2 1 24 | send_payment 1 2 "$INVOICE" 25 | list_channels 1 26 | list_channels 2 27 | list_payments 1 28 | list_payments 2 29 | get_colored_invoice 2 2 30 | send_payment 1 2 "$INVOICE" 31 | list_channels 1 32 | list_channels 2 33 | list_payments 1 2 34 | list_payments 2 2 35 | get_colored_invoice 2 3 36 | send_payment 1 2 "$INVOICE" 37 | list_channels 1 38 | list_channels 2 39 | list_payments 1 3 40 | list_payments 2 3 41 | 42 | # close channel 43 | close_channel 1 2 "$NODE2_ID" 44 | asset_balance 1 994 45 | asset_balance 2 6 46 | 47 | # spend RGB assets on-chain 48 | _skip_remaining 49 | blind 3 50 | send_assets 1 700 51 | blind 3 52 | send_assets 2 6 53 | mine 1 54 | refresh 3 55 | asset_balance 1 294 56 | asset_balance 2 0 57 | asset_balance 3 706 58 | 59 | exit 0 60 | -------------------------------------------------------------------------------- /tests/scripts/open_after_double_send.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | create_utxos 3 12 | 13 | # issue asset 14 | issue_asset 15 | 16 | # send assets (1) 17 | blind 2 18 | send_assets 1 100 19 | asset_balance 1 900 20 | 21 | # send assets (2) 22 | blind 2 23 | send_assets 1 200 24 | asset_balance 1 700 25 | 26 | refresh 2 27 | asset_balance 2 300 28 | 29 | # open channel 30 | open_colored_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 250 31 | list_channels 1 32 | list_channels 2 33 | asset_balance 1 700 34 | asset_balance 2 50 35 | 36 | ## send assets 37 | colored_keysend 2 1 "$NODE1_ID" 50 38 | list_channels 1 39 | list_channels 2 40 | list_payments 1 41 | list_payments 2 42 | 43 | ## close channel 44 | close_channel 1 2 "$NODE2_ID" 45 | asset_balance 1 750 46 | asset_balance 2 250 47 | 48 | ## spend RGB assets on-chain 49 | _skip_remaining 50 | blind 3 51 | send_assets 1 725 52 | blind 3 53 | send_assets 2 225 54 | mine 1 55 | refresh 3 56 | asset_balance 1 25 57 | asset_balance 2 25 58 | asset_balance 3 950 59 | 60 | exit 0 61 | -------------------------------------------------------------------------------- /tests/scripts/restart.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | _tit "restart nodes" 9 | exit_node 1 10 | start_node 1 11 | exit_node 2 12 | start_node 2 13 | exit_node 3 14 | start_node 3 15 | 16 | # create RGB UTXOs 17 | create_utxos 1 18 | create_utxos 2 19 | create_utxos 3 20 | 21 | # issue asset 22 | issue_asset 23 | _tit "restart nodes" 24 | exit_node 1 25 | start_node 1 26 | exit_node 2 27 | start_node 2 28 | exit_node 3 29 | start_node 3 30 | asset_balance 1 1000 31 | 32 | # open channel 33 | open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 34 | _tit "restart nodes" 35 | exit_node 1 36 | start_node 1 37 | 38 | check_channel_reestablish 1 "LDK startup successful." 39 | exit_node 2 40 | start_node 2 41 | check_channel_reestablish 2 "LDK startup successful." 42 | exit_node 3 43 | start_node 3 44 | list_channels 1 45 | list_channels 2 46 | asset_balance 1 400 47 | 48 | # send assets 49 | colored_keysend 1 2 "$NODE2_ID" 100 50 | _tit "restart nodes" 51 | exit_node 1 52 | start_node 1 53 | exit_node 2 54 | start_node 2 55 | exit_node 3 56 | start_node 3 57 | list_channels 1 58 | list_channels 2 59 | # TODO: pending https://github.com/lightningdevkit/ldk-sample/pull/104 60 | #list_payments 1 61 | #list_payments 2 62 | 63 | # close channel 64 | close_channel 1 2 "$NODE2_ID" 65 | sleep 3 66 | _tit "restart nodes" 67 | exit_node 1 68 | start_node 1 69 | exit_node 2 70 | start_node 2 71 | exit_node 3 72 | start_node 3 73 | asset_balance 1 900 74 | asset_balance 2 100 75 | 76 | # spend RGB assets on-chain 77 | _skip_remaining 78 | blind 3 79 | send_assets 1 700 80 | blind 3 81 | send_assets 2 50 82 | mine 1 83 | refresh 3 84 | _tit "restart nodes" 85 | exit_node 1 86 | start_node 1 87 | exit_node 2 88 | start_node 2 89 | exit_node 3 90 | start_node 3 91 | asset_balance 1 200 92 | asset_balance 2 50 93 | asset_balance 3 750 94 | 95 | exit 0 96 | -------------------------------------------------------------------------------- /tests/scripts/send_payment.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | create_utxos 3 12 | 13 | # issue asset 14 | issue_asset 15 | 16 | # open channel 17 | open_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 18 | list_channels 1 19 | list_channels 2 20 | asset_balance 1 400 21 | 22 | # get invoice 23 | get_colored_invoice 2 100 24 | 25 | # send payment 26 | send_payment 1 2 "$INVOICE" 27 | list_channels 1 28 | list_channels 2 29 | list_payments 1 30 | list_payments 2 31 | 32 | # close channel 33 | close_channel 1 2 "$NODE2_ID" 34 | asset_balance 1 900 35 | asset_balance 2 100 36 | 37 | # spend assets 38 | _skip_remaining 39 | blind 3 40 | send_assets 1 700 41 | blind 3 42 | send_assets 2 50 43 | mine 1 44 | refresh 3 45 | asset_balance 1 200 46 | asset_balance 2 50 47 | asset_balance 3 750 48 | 49 | exit 0 50 | -------------------------------------------------------------------------------- /tests/scripts/send_receive.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | 12 | # issue asset 13 | issue_asset 14 | 15 | # send assets 1->2 (1) 16 | blind 2 17 | send_assets 1 400 18 | mine 1 19 | asset_balance 1 600 20 | refresh 2 21 | asset_balance 2 400 22 | 23 | # send assets 2->1 24 | blind 1 25 | send_assets 2 300 26 | mine 1 27 | asset_balance 2 100 28 | refresh 1 29 | asset_balance 1 900 30 | 31 | # send assets 1->2 (2) 32 | blind 2 33 | send_assets 1 200 34 | mine 1 35 | asset_balance 1 700 36 | refresh 2 37 | asset_balance 2 300 38 | 39 | exit 0 40 | -------------------------------------------------------------------------------- /tests/scripts/send_vanilla_payment.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | get_address 1 9 | fund_address $address 10 | mine 1 11 | 12 | # wait for bdk and ldk to sync up with electrs 13 | sleep 5 14 | 15 | # open channel 16 | open_vanilla_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 16777215 17 | list_channels 1 18 | list_channels 2 19 | 20 | # get invoice 21 | get_vanilla_invoice 2 3000000 22 | 23 | # send payment 24 | send_payment 1 2 "$INVOICE" 25 | list_channels 1 26 | list_channels 2 27 | list_payments 1 28 | list_payments 2 29 | -------------------------------------------------------------------------------- /tests/scripts/swap_roundtrip.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | 12 | # issue asset 13 | issue_asset 14 | 15 | # open channel 16 | open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 17 | list_channels 1 1 18 | list_channels 2 1 19 | 20 | open_vanilla_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 16777215 21 | list_channels 2 2 22 | list_channels 1 2 23 | 24 | 25 | maker_init 2 10 "sell" 900 26 | taker 1 27 | taker_list 1 1 28 | maker_list 2 1 29 | maker_execute 2 30 | 31 | sleep 5 32 | 33 | list_channels 2 2 34 | list_channels 1 2 35 | -------------------------------------------------------------------------------- /tests/scripts/swap_roundtrip_buy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | create_utxos 3 12 | 13 | # issue asset 14 | issue_asset 15 | 16 | # open channel 17 | open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 18 | list_channels 1 1 19 | list_channels 2 1 20 | 21 | open_vanilla_channel 2 3 "$NODE3_PORT" "$NODE3_ID" 16777215 22 | list_channels 2 2 23 | list_channels 3 1 24 | 25 | open_vanilla_channel 3 1 "$NODE1_PORT" "$NODE1_ID" 16777215 26 | list_channels 3 2 27 | list_channels 1 2 28 | 29 | 30 | maker_init 1 10 "buy" 900 31 | taker 2 32 | taker_list 2 1 33 | maker_list 1 1 34 | maker_execute 1 35 | 36 | sleep 5 37 | 38 | list_channels 2 2 39 | list_channels 1 2 40 | list_channels 3 2 41 | -------------------------------------------------------------------------------- /tests/scripts/swap_roundtrip_fail.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | 12 | # issue asset 13 | issue_asset 14 | 15 | # open channel 16 | open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 17 | list_channels 1 1 18 | list_channels 2 1 19 | 20 | open_vanilla_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 16777215 21 | list_channels 2 2 22 | list_channels 1 2 23 | 24 | 25 | maker_init 2 10 "sell" 900 26 | # taker 1 like `swap_roundtrip` but we don't whitelist the swap so it will fail 27 | maker_execute_expect_failure 2 "$NODE1_ID" 1 28 | -------------------------------------------------------------------------------- /tests/scripts/swap_roundtrip_fail_amount_maker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | 12 | # issue asset 13 | issue_asset 14 | 15 | # open channel 16 | open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 17 | list_channels 1 1 18 | list_channels 2 1 19 | 20 | open_vanilla_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 16777215 21 | list_channels 2 2 22 | list_channels 1 2 23 | 24 | # The amount is too large, we don't have enough 25 | maker_init_amount_failure 2 1000 "buy" 100 26 | -------------------------------------------------------------------------------- /tests/scripts/swap_roundtrip_fail_amount_taker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | 12 | # issue asset 13 | issue_asset 14 | 15 | # open channel 16 | open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 17 | list_channels 1 1 18 | list_channels 2 1 19 | 20 | open_vanilla_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 16777215 21 | list_channels 2 2 22 | list_channels 1 2 23 | 24 | maker_init 2 1000 "sell" 100 25 | # The amount is too large, we don't have enough 26 | taker_amount_failure 1 27 | -------------------------------------------------------------------------------- /tests/scripts/swap_roundtrip_multihop_buy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | create_utxos 3 12 | 13 | # issue asset 14 | issue_asset 15 | 16 | # send assets 17 | blind 2 18 | send_assets 1 400 19 | asset_balance 1 600 20 | 21 | # open channel 22 | open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 500 23 | list_channels 1 24 | list_channels 2 25 | asset_balance 1 100 26 | 27 | refresh 2 28 | asset_balance 2 400 29 | 30 | # open channel 31 | open_big_colored_channel 2 3 "$NODE3_PORT" "$NODE3_ID" 300 1 32 | list_channels 2 2 33 | list_channels 3 34 | asset_balance 2 100 35 | 36 | # needs more funding 37 | create_utxos 1 38 | create_utxos 2 39 | create_utxos 3 40 | 41 | open_vanilla_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 16777215 42 | list_channels 2 3 43 | list_channels 1 2 44 | 45 | open_vanilla_channel 3 2 "$NODE2_PORT" "$NODE2_ID" 16777215 46 | list_channels 3 2 47 | list_channels 2 4 48 | 49 | sleep 5 50 | 51 | maker_init 1 2 "buy" 90 52 | taker 3 53 | taker_list 3 1 54 | maker_list 1 1 55 | maker_execute 1 56 | 57 | sleep 5 58 | 59 | list_payments 1 2 60 | list_payments 3 0 61 | 62 | exit 0 63 | -------------------------------------------------------------------------------- /tests/scripts/swap_roundtrip_multihop_sell.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | create_utxos 3 12 | 13 | # issue asset 14 | issue_asset 15 | 16 | # send assets 17 | blind 2 18 | send_assets 1 400 19 | asset_balance 1 600 20 | 21 | # open channel 22 | open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 500 23 | channel12_id="$CHANNEL_ID" 24 | list_channels 1 25 | list_channels 2 26 | asset_balance 1 100 27 | 28 | refresh 2 29 | asset_balance 2 400 30 | 31 | # open channel 32 | open_big_colored_channel 2 3 "$NODE3_PORT" "$NODE3_ID" 300 1 33 | channel23_id="$CHANNEL_ID" 34 | list_channels 2 2 35 | list_channels 3 36 | asset_balance 2 100 37 | 38 | # needs more funding 39 | create_utxos 1 40 | create_utxos 2 41 | create_utxos 3 42 | 43 | open_vanilla_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 16777215 44 | list_channels 2 3 45 | list_channels 1 2 46 | 47 | open_vanilla_channel 3 2 "$NODE2_PORT" "$NODE2_ID" 16777215 48 | list_channels 3 2 49 | list_channels 2 4 50 | 51 | sleep 10 52 | 53 | maker_init 3 2 "sell" 90 54 | taker 1 55 | taker_list 1 1 56 | maker_list 3 1 57 | maker_execute 3 58 | 59 | sleep 5 60 | 61 | list_payments 1 0 62 | list_payments 3 2 63 | 64 | exit 0 65 | -------------------------------------------------------------------------------- /tests/scripts/swap_roundtrip_timeout.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | # create RGB UTXOs 9 | create_utxos 1 10 | create_utxos 2 11 | 12 | # issue asset 13 | issue_asset 14 | 15 | # open channel 16 | open_big_colored_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 600 17 | list_channels 1 1 18 | list_channels 2 1 19 | 20 | open_vanilla_channel 2 1 "$NODE1_PORT" "$NODE1_ID" 16777215 21 | list_channels 2 2 22 | list_channels 1 2 23 | 24 | # the timeout is too short, it will already be expired when the taker accepts 25 | maker_init 2 10 "sell" 1 26 | sleep 3 27 | taker_expect_timeout 1 28 | -------------------------------------------------------------------------------- /tests/scripts/vanilla_keysend.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source tests/common.sh 4 | 5 | 6 | get_node_ids 7 | 8 | get_address 1 9 | fund_address $address 10 | mine 1 11 | 12 | # wait for bdk and ldk to sync up with electrs 13 | sleep 5 14 | 15 | # open channel 16 | open_vanilla_channel 1 2 "$NODE2_PORT" "$NODE2_ID" 16777215 17 | list_channels 1 18 | list_channels 2 19 | 20 | # send payment 21 | keysend 1 2 "$NODE2_ID" 3000000 22 | list_channels 1 23 | list_channels 2 24 | list_payments 1 25 | list_payments 2 26 | 27 | -------------------------------------------------------------------------------- /tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # automated tests for RGB LN node PoC 4 | # 5 | # note: in case something goes wrong, the test won't handle the resulting 6 | # situation and might leave one or more running processes; if that 7 | # happens use the "dislocate" command to re-attach to those processes and 8 | # exit them 9 | 10 | prog=$(realpath "$(dirname "$0")") 11 | name=$(basename "$0") 12 | bad_net_msg="incorrect network; available networks: testnet, regtest" 13 | build_log_file="cargo_build.log" 14 | 15 | COMPOSE="docker compose" 16 | if ! $COMPOSE >/dev/null; then 17 | echo "could not call docker compose (hint: install docker compose plugin)" 18 | exit 1 19 | fi 20 | BITCOIN_CLI="$COMPOSE exec -u blits bitcoind bitcoin-cli -regtest" 21 | NETWORK="regtest" 22 | INITIAL_BLOCKS=103 23 | SUCCESSFUL_SCRIPTS=() 24 | FAILED_SCRIPTS=() 25 | TEST_PRINTS=1 26 | TMUX_CMD="tmux -L rgb-tmux" 27 | export BITCOIN_CLI COMPOSE NETWORK TEST_PRINTS TMUX_CMD 28 | 29 | 30 | _cleanup() { 31 | # cd back to calling directory 32 | cd - >/dev/null || exit 1 33 | # test run time in case the test ended abruptly 34 | _show_time " (interrupted)" 35 | } 36 | 37 | _die () { 38 | echo "ERR: $*" 39 | exit 1 40 | } 41 | 42 | _handle_network() { 43 | set -a 44 | case $NETWORK in 45 | regtest) 46 | RGB_ELECTRUM_SERVER=electrs:50001 47 | ELECTRUM_URL=electrs 48 | ELECTRUM_PORT=50001 49 | ;; 50 | testnet) 51 | RGB_ELECTRUM_SERVER=ssl://electrum.iriswallet.com:50013 52 | ELECTRUM_URL=ssl://electrum.iriswallet.com 53 | ELECTRUM_PORT=50013 54 | ;; 55 | *) 56 | _die "$bad_net_msg" 57 | ;; 58 | esac 59 | set +a 60 | } 61 | 62 | _find_scripts() { 63 | SCRIPTS=$(find "${prog}/scripts/" -name '*.sh' -printf '%f\n' | sort) 64 | SCRIPT_NUM=$(echo "$SCRIPTS" | wc -l) 65 | } 66 | 67 | _run_script() { 68 | local name file 69 | name="$1" 70 | file="$2" 71 | echo && echo "starting test script: $name" 72 | _stop_start_tmux 73 | TIME_START=$(date +%s) 74 | bash "$file" 75 | SCRIPT_EXIT="$?" 76 | _show_time " ($name)" 77 | } 78 | 79 | _show_time() { 80 | local msg="$1" 81 | if [ -n "$TIME_START" ]; then 82 | TIME_END="$(date +%s)" 83 | echo && echo "test run time$msg: $((TIME_END-TIME_START)) seconds" 84 | unset TIME_START TIME_END 85 | fi 86 | } 87 | 88 | _start_services() { 89 | _stop_services 90 | 91 | mkdir -p data{rgb0,rgb1,rgb2,core,index,ldk0,ldk1,ldk2} 92 | # see docker-compose.yml for the exposed ports 93 | EXPOSED_PORTS=(3000 50001) 94 | for port in "${EXPOSED_PORTS[@]}"; do 95 | if [ -n "$(ss -HOlnt "sport = :$port")" ];then 96 | _die "port $port is already bound, services can't be started" 97 | fi 98 | done 99 | case $NETWORK in 100 | regtest) 101 | $COMPOSE up -d 102 | echo && echo "preparing bitcoind wallet" 103 | $BITCOIN_CLI createwallet miner >/dev/null 104 | $BITCOIN_CLI -rpcwallet=miner -generate $INITIAL_BLOCKS >/dev/null 105 | export HEIGHT=$INITIAL_BLOCKS 106 | # wait for electrs to have completed startup 107 | until $COMPOSE logs electrs |grep 'finished full compaction' >/dev/null; do 108 | sleep 1 109 | done 110 | ;; 111 | *) 112 | _die "$bad_net_msg" 113 | ;; 114 | esac 115 | } 116 | 117 | _stop_services() { 118 | $COMPOSE down --remove-orphans 119 | rm -rf data{rgb0,rgb1,rgb2,core,index,ldk0,ldk1,ldk2} 120 | } 121 | 122 | _stop_start_tmux() { 123 | local bin_base="target/debug" 124 | [ "$RELEASE" = 1 ] && bin_base="target/release" 125 | _stop_tmux 126 | 127 | echo "starting tmux" 128 | $TMUX_CMD -f tests/tmux.conf new-session -d -n node1 -s rgb-lightning-sample -x 200 -y 100 129 | $TMUX_CMD send-keys "$bin_base/ldk-sample user:password@localhost:18443 dataldk0/ 9735 regtest" C-m 130 | $TMUX_CMD new-window -n node2 131 | $TMUX_CMD send-keys "$bin_base/ldk-sample user:password@localhost:18443 dataldk1/ 9736 regtest" C-m 132 | $TMUX_CMD new-window -n node3 133 | $TMUX_CMD send-keys "$bin_base/ldk-sample user:password@localhost:18443 dataldk2/ 9737 regtest" C-m 134 | sleep 1 135 | 136 | echo && echo "to attach the tmux session, execute \"$TMUX_CMD attach-session -t rgb-lightning-sample\"" 137 | } 138 | 139 | _stop_tmux() { 140 | echo && echo "stopping tmux" 141 | $TMUX_CMD kill-server >/dev/null 2>&1 142 | sleep 1 143 | } 144 | 145 | 146 | _help() { 147 | echo "$name [-h|--help]" 148 | echo " show this help message" 149 | echo 150 | echo "$name [-l|--list]" 151 | echo " list available test scripts" 152 | echo 153 | echo "$name [-n|--network]" 154 | echo " choose the bitcoin network to be used" 155 | echo " available options: regtest (default), testnet" 156 | echo 157 | echo "$name [-r|--release]" 158 | echo " build in release mode" 159 | echo 160 | echo "$name [-s|--skip-final-part]" 161 | echo " skip final test portion (if supported by test)" 162 | echo 163 | echo "$name [-t|--test ] [--start] [--stop]" 164 | echo " build and exit if an error is returned" 165 | echo " -t run test with provided name" 166 | echo " -t all run all tests (restarting services in between)" 167 | echo " --start stop services, clean up, start services," 168 | echo " create bitcoind wallet used for mining," 169 | echo " generate initial blocks" 170 | echo " --stop stop services and clean up" 171 | echo 172 | echo "$name [-v|--verbose]" 173 | echo " enable verbose output" 174 | } 175 | 176 | # cmdline arguments 177 | [ -z "$1" ] && _help 178 | while [ -n "$1" ]; do 179 | case $1 in 180 | -h|--help) 181 | _help 182 | exit 0 183 | ;; 184 | -l|--list-tests) 185 | _find_scripts 186 | echo "list of available test scripts:" 187 | for s in $SCRIPTS; do 188 | echo " - ${s%.sh}" 189 | done 190 | exit 0 191 | ;; 192 | -n|--network) 193 | [ "$2" = "regtest" ] || [ "$2" = "testnet" ] || _die "$bad_net_msg" 194 | NETWORK="$2" 195 | shift 196 | ;; 197 | -r|--release) 198 | RELEASE=1 199 | ;; 200 | -s|--skip-final-spend) 201 | export SKIP=1 202 | ;; 203 | -v|--verbose) 204 | export VERBOSE=1 205 | ;; 206 | --start) 207 | start=1 208 | ;; 209 | --stop) 210 | stop=1 211 | ;; 212 | -t|--test) 213 | script_name="$2" 214 | if [ "$script_name" = "all" ]; then 215 | script=$script_name 216 | else 217 | script="${prog}/scripts/${script_name}.sh" 218 | [ -r "$script" ] || _die "script \"$script_name\" not found" 219 | fi 220 | shift 221 | ;; 222 | *) 223 | _die "unsupported argument \"$1\"" 224 | ;; 225 | esac 226 | shift 227 | done 228 | 229 | # make sure to cleanup on exit 230 | trap _cleanup EXIT INT TERM 231 | 232 | # cd to project root 233 | cd "$prog/.." || exit 234 | 235 | # check network and set env variables accordingly 236 | _handle_network 237 | 238 | # build project (if test run has been requested) 239 | if [ -n "$script" ]; then 240 | echo -n "building project (see file $build_log_file for the build log)... " 241 | [ "$RELEASE" = 1 ] && release="--release" 242 | cargo build $release >$build_log_file 2>&1 || exit 1 243 | echo "done" 244 | echo 245 | fi 246 | 247 | # start services if requested 248 | [ "$start" = "1" ] && _start_services 249 | 250 | # start test script if requested (regtest only) 251 | if [ -n "$script" ] && [ -n "$script_name" ]; then 252 | [ "$NETWORK" = "regtest" ] || _die "tests are only available on regtest" 253 | if [ "$script" = "all" ]; then 254 | _find_scripts 255 | echo && echo "running $SCRIPT_NUM tests" 256 | time_total_start=$(date +%s) 257 | for s in $SCRIPTS; do 258 | echo && echo && echo "========================================" 259 | _stop_services 260 | _start_services 261 | _run_script "${s%.sh}" "${prog}/scripts/${s}" 262 | if [ "$SCRIPT_EXIT" = 0 ]; then 263 | SUCCESSFUL_SCRIPTS+=("$s") 264 | else 265 | EXIT_CODE=1 266 | FAILED_SCRIPTS+=("$s") 267 | fi 268 | done 269 | time_total_end=$(date +%s) 270 | echo && echo "total test run time: $((time_total_end-time_total_start)) seconds" 271 | unset time_total_start time_total_end 272 | echo "${#SUCCESSFUL_SCRIPTS[@]} out of $SCRIPT_NUM tests were successful" 273 | if [ "${#FAILED_SCRIPTS[@]}" -gt 0 ]; then 274 | echo && echo "successful tests:" 275 | for s in "${SUCCESSFUL_SCRIPTS[@]}"; do 276 | echo "- ${s%.sh}" 277 | done 278 | echo && echo "failed tests:" 279 | for s in "${FAILED_SCRIPTS[@]}"; do 280 | echo "- ${s%.sh}" 281 | done 282 | fi 283 | else 284 | _run_script "$script_name" "$script" 285 | if [ "$SCRIPT_EXIT" != 0 ]; then 286 | EXIT_CODE=1 287 | fi 288 | fi 289 | fi 290 | 291 | # stop services if requested 292 | if [ "$stop" = "1" ]; then 293 | if [ -n "$script" ]; then 294 | echo && echo "services will now be stopped" 295 | echo && read -rp "press to continue" 296 | fi 297 | _stop_services 298 | _stop_tmux 299 | fi 300 | 301 | # exit with 0 if all tests ran successfully 302 | exit ${EXIT_CODE:-0} 303 | -------------------------------------------------------------------------------- /tests/tmux.conf: -------------------------------------------------------------------------------- 1 | set -g base-index 1 2 | --------------------------------------------------------------------------------