diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f386da5c..b0573970 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ jobs: name: Login to Docker Hub uses: docker/login-action@v3 with: - username: commerceblockx + username: mercurylayer password: ${{ secrets.DOCKER_HUB_PASSWORD }} - name: Set up Docker Buildx @@ -35,7 +35,7 @@ jobs: context: . file: ./server/Dockerfile push: true - tags: commerceblockx/mercury-server:${{ env.COMMIT_HASH }} + tags: mercurylayer/mercury-server:${{ env.COMMIT_HASH }} - name: Build and push for token-server uses: docker/build-push-action@v5 @@ -43,7 +43,7 @@ jobs: context: . file: ./token-server/Dockerfile push: true - tags: commerceblockx/token-server:${{ env.COMMIT_HASH }} + tags: mercurylayer/token-server:${{ env.COMMIT_HASH }} - name: Build and push for mercury-explorer uses: docker/build-push-action@v5 @@ -51,7 +51,7 @@ jobs: context: . file: ./explorer/Dockerfile push: true - tags: commerceblockx/mercury-explorer:${{ env.COMMIT_HASH }} + tags: mercurylayer/mercury-explorer:${{ env.COMMIT_HASH }} - name: Build and push for keylist-cronjob uses: docker/build-push-action@v5 @@ -59,4 +59,4 @@ jobs: context: . file: ./keylistCron/Dockerfile push: true - tags: commerceblockx/keylist-cronjob:${{ env.COMMIT_HASH }} + tags: mercurylayer/keylist-cronjob:${{ env.COMMIT_HASH }} diff --git a/Cargo.lock b/Cargo.lock index b8678b55..a8a001d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,6 +166,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "askama" version = "0.12.1" @@ -240,6 +246,37 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "async-utility" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a349201d80b4aa18d17a34a182bdd7f8ddf845e9e57d2ea130a12e10ef1e3a47" +dependencies = [ + "futures-util", + "gloo-timers", + "tokio", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-wsocket" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d50cb541e6d09e119e717c64c46ed33f49be7fa592fa805d56c11d6a7ff093c" +dependencies = [ + "async-utility", + "futures", + "futures-util", + "js-sys", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-tungstenite", + "url", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "atoi" version = "2.0.0" @@ -264,6 +301,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "atomic-destructor" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d919cb60ba95c87ba42777e9e246c4e8d658057299b437b7512531ce0a09a23" +dependencies = [ + "tracing", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -285,6 +331,16 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes 0.14.0", +] + [[package]] name = "base64" version = "0.13.1" @@ -324,6 +380,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + [[package]] name = "binascii" version = "0.1.4" @@ -341,12 +403,11 @@ dependencies = [ [[package]] name = "bip39" -version = "1.2.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29b9e657de8ff1c3488a4ab77cb51d604eab53415ce34f0bc800f2eac9b13c28" +checksum = "33415e24172c1b7d6066f6d999545375ab8e1d95421d6784bdfff9496f292387" dependencies = [ - "bitcoin_hashes 0.11.0", - "rand_core 0.4.2", + "bitcoin_hashes 0.12.0", "serde", "unicode-normalization", ] @@ -358,15 +419,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e99ff7289b20a7385f66a0feda78af2fc119d28fb56aea8886a9cd0a4abdd75" dependencies = [ "base64 0.13.1", - "bech32", + "bech32 0.9.1", "bitcoin-private", "bitcoin_hashes 0.12.0", "bitcoinconsensus", "hex_lit", - "secp256k1", + "secp256k1 0.27.0", + "serde", +] + +[[package]] +name = "bitcoin" +version = "0.32.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" +dependencies = [ + "base58ck", + "bech32 0.11.0", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes 0.14.0", + "hex-conservative", + "hex_lit", + "secp256k1 0.29.1", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ "serde", ] +[[package]] +name = "bitcoin-io" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" + [[package]] name = "bitcoin-private" version = "0.1.0" @@ -374,10 +468,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" [[package]] -name = "bitcoin_hashes" -version = "0.11.0" +name = "bitcoin-units" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" +checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +dependencies = [ + "bitcoin-internals", + "serde", +] [[package]] name = "bitcoin_hashes" @@ -389,6 +487,17 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +dependencies = [ + "bitcoin-io", + "hex-conservative", + "serde", +] + [[package]] name = "bitcoinconsensus" version = "0.20.2-0.5.0" @@ -423,6 +532,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -479,6 +597,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.1.15" @@ -494,6 +621,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.38" @@ -516,6 +667,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -563,9 +715,9 @@ name = "client-rust" version = "0.1.0" dependencies = [ "anyhow", - "bech32", + "bech32 0.9.1", "bip39", - "bitcoin", + "bitcoin 0.30.1", "chrono", "clap", "config", @@ -686,9 +838,9 @@ checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" [[package]] name = "critical-section" -version = "1.1.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam-queue" @@ -722,6 +874,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -734,6 +887,12 @@ dependencies = [ "cipher", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + [[package]] name = "der" version = "0.7.8" @@ -808,6 +967,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "dlv-list" version = "0.3.0" @@ -838,7 +1008,7 @@ dependencies = [ "libsecp256k1", "once_cell", "parking_lot", - "rand_core 0.6.4", + "rand_core", "sha2", "typenum", "wasm-bindgen", @@ -859,7 +1029,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bc133f1c8d829d254f013f946653cbeb2b08674b960146361d1e9b67733ad19" dependencies = [ - "bitcoin", + "bitcoin 0.30.1", "bitcoin-private", "byteorder", "libc", @@ -985,6 +1155,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1002,9 +1178,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -1163,6 +1339,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "goblin" version = "0.8.0" @@ -1185,7 +1373,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.9", "indexmap 1.9.3", "slab", "tokio", @@ -1212,6 +1400,17 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashlink" version = "0.9.1" @@ -1245,6 +1444,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +dependencies = [ + "arrayvec", +] + [[package]] name = "hex_lit" version = "0.1.1" @@ -1289,6 +1497,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.5" @@ -1296,7 +1515,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", - "http", + "http 0.2.9", "pin-project-lite", ] @@ -1329,7 +1548,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.9", "http-body", "httparse", "httpdate", @@ -1378,15 +1597,143 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "idna" -version = "0.2.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de910d521f7cc3135c4de8db1cb910e0b5ed1dc6f57c381cd07e8e661ce10094" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -1422,9 +1769,22 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ + "block-padding", "generic-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "ipnet" version = "2.8.0" @@ -1494,9 +1854,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.158" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libm" @@ -1572,6 +1932,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "lock_api" version = "0.4.10" @@ -1603,6 +1969,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.2", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1612,18 +1987,6 @@ dependencies = [ "regex-automata 0.1.10", ] -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - -[[package]] -name = "maybe-uninit" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" - [[package]] name = "md-5" version = "0.10.6" @@ -1642,15 +2005,16 @@ checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "mercury-server" -version = "0.2.0" +version = "0.2.1" dependencies = [ - "bitcoin", + "bitcoin 0.30.1", "chrono", "config", "env_logger", "hex", "log", "mercurylib", + "nostr-sdk", "rand", "reqwest", "rocket", @@ -1685,9 +2049,9 @@ dependencies = [ name = "mercurylib" version = "0.1.0" dependencies = [ - "bech32", + "bech32 0.9.1", "bip39", - "bitcoin", + "bitcoin 0.30.1", "ecies", "hex", "secp256k1-zkp", @@ -1702,9 +2066,9 @@ name = "mercuryrustlib" version = "0.1.0" dependencies = [ "anyhow", - "bech32", + "bech32 0.9.1", "bip39", - "bitcoin", + "bitcoin 0.30.1", "chrono", "clap", "config", @@ -1744,6 +2108,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniscript" +version = "12.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd3c9608217b0d6fa9c9c8ddd875b85ab72bd4311cfc8db35e1b5a08fc11f4d" +dependencies = [ + "bech32 0.11.0", + "bitcoin 0.32.5", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1755,13 +2129,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1773,43 +2147,130 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http", + "http 0.2.9", "httparse", "log", "memchr", "mime", "spin 0.9.8", "tokio", - "tokio-util", - "version_check", + "tokio-util", + "version_check", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "negentropy" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e664971378a3987224f7a0e10059782035e89899ae403718ee07de85bec42afe" + +[[package]] +name = "negentropy" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a88da9dd148bbcdce323dd6ac47d369b4769d4a3b78c6c52389b9269f77932" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nostr" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aad4b767bbed24ac5eb4465bfb83bc1210522eb99d67cf4e547ec2ec7e47786" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bech32 0.11.0", + "bip39", + "bitcoin 0.32.5", + "cbc", + "chacha20", + "chacha20poly1305", + "getrandom", + "instant", + "negentropy 0.3.1", + "negentropy 0.4.3", + "once_cell", + "scrypt", + "serde", + "serde_json", + "unicode-normalization", + "url", +] + +[[package]] +name = "nostr-database" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23696338d51e45cd44e061823847f4b0d1d362eca80d5033facf9c184149f72f" +dependencies = [ + "async-trait", + "lru", + "nostr", + "thiserror", + "tokio", + "tracing", ] [[package]] -name = "native-tls" -version = "0.2.11" +name = "nostr-relay-pool" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "15fcc6e3f0ca54d0fc779009bc5f2684cea9147be3b6aa68a7d301ea590f95f5" dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", + "async-utility", + "async-wsocket", + "atomic-destructor", + "negentropy 0.3.1", + "negentropy 0.4.3", + "nostr", + "nostr-database", + "thiserror", + "tokio", + "tokio-stream", + "tracing", ] [[package]] -name = "nom" -version = "7.1.3" +name = "nostr-sdk" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +checksum = "491221fc89b1aa189a0de640127127d68b4e7c5c1d44371b04d9a6d10694b5af" dependencies = [ - "memchr", - "minimal-lexical", + "async-utility", + "atomic-destructor", + "nostr", + "nostr-database", + "nostr-relay-pool", + "thiserror", + "tokio", + "tracing", ] [[package]] @@ -1835,7 +2296,7 @@ dependencies = [ "num-iter", "num-traits", "rand", - "smallvec 1.11.1", + "smallvec", "zeroize", ] @@ -1897,9 +2358,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" dependencies = [ "critical-section", "portable-atomic", @@ -2002,10 +2463,21 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "smallvec 1.11.1", + "smallvec", "windows-targets 0.48.5", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.14" @@ -2018,6 +2490,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + [[package]] name = "pear" version = "0.2.7" @@ -2052,9 +2534,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" @@ -2146,6 +2628,17 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "polyval" version = "0.6.1" @@ -2160,9 +2653,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "powerfmt" @@ -2215,7 +2708,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -2225,15 +2718,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - [[package]] name = "rand_core" version = "0.6.4" @@ -2328,7 +2815,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.9", "http-body", "hyper", "hyper-tls", @@ -2448,7 +2935,7 @@ dependencies = [ "cookie", "either", "futures", - "http", + "http 0.2.9", "hyper", "indexmap 2.0.2", "log", @@ -2458,7 +2945,7 @@ dependencies = [ "pin-project-lite", "ref-cast", "serde", - "smallvec 1.11.1", + "smallvec", "stable-pattern", "state", "time", @@ -2492,7 +2979,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core 0.6.4", + "rand_core", "signature", "spki", "subtle", @@ -2557,14 +3044,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.12" +version = "0.23.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" dependencies = [ "once_cell", "ring 0.17.3", "rustls-pki-types", - "rustls-webpki 0.102.7", + "rustls-webpki 0.102.8", "subtle", "zeroize", ] @@ -2581,9 +3068,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" [[package]] name = "rustls-webpki" @@ -2597,9 +3084,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.7" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring 0.17.3", "rustls-pki-types", @@ -2618,6 +3105,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "schannel" version = "0.1.22" @@ -2685,6 +3181,18 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.7.0" @@ -2703,7 +3211,19 @@ checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" dependencies = [ "bitcoin_hashes 0.12.0", "rand", - "secp256k1-sys", + "secp256k1-sys 0.8.1", + "serde", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes 0.14.0", + "rand", + "secp256k1-sys 0.10.1", "serde", ] @@ -2716,6 +3236,15 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + [[package]] name = "secp256k1-zkp" version = "0.9.2" @@ -2723,7 +3252,7 @@ source = "git+https://github.com/ssantos21/rust-secp256k1-zkp.git?branch=blinded dependencies = [ "bitcoin-private", "rand", - "secp256k1", + "secp256k1 0.27.0", "secp256k1-zkp-sys", ] @@ -2733,7 +3262,7 @@ version = "0.8.1" source = "git+https://github.com/ssantos21/rust-secp256k1-zkp.git?branch=blinded-musig-scheme#e03de85e3c74e58249bb3bb43e8642e5e744cfa0" dependencies = [ "cc", - "secp256k1-sys", + "secp256k1-sys 0.8.1", ] [[package]] @@ -2816,6 +3345,7 @@ version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ + "indexmap 2.0.2", "itoa", "ryu", "serde", @@ -2895,7 +3425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" dependencies = [ "digest 0.10.7", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -2915,18 +3445,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "0.6.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" -dependencies = [ - "maybe-uninit", -] - -[[package]] -name = "smallvec" -version = "1.11.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" dependencies = [ "serde", ] @@ -2949,12 +3470,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3034,12 +3555,12 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rustls 0.23.12", + "rustls 0.23.19", "rustls-pemfile", "serde", "serde_json", "sha2", - "smallvec 1.11.1", + "smallvec", "sqlformat", "thiserror", "time", @@ -3125,7 +3646,7 @@ dependencies = [ "serde", "sha1", "sha2", - "smallvec 1.11.1", + "smallvec", "sqlx-core", "stringprep", "thiserror", @@ -3166,7 +3687,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "smallvec 1.11.1", + "smallvec", "sqlx-core", "stringprep", "thiserror", @@ -3211,6 +3732,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "state" version = "0.6.0" @@ -3271,6 +3798,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -3377,12 +3915,59 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "token-server" +version = "0.1.1" +dependencies = [ + "config", + "hex", + "rand", + "reqwest", + "rocket", + "schemars", + "serde", + "serde_json", + "sqlx", + "uuid 1.4.1", +] + +[[package]] +name = "token-server-v2" version = "0.1.0" dependencies = [ + "anyhow", + "bech32 0.9.1", + "bip39", + "bitcoin 0.30.1", "config", + "electrum-client", "hex", + "miniscript", "rand", "reqwest", "rocket", @@ -3395,28 +3980,27 @@ dependencies = [ [[package]] name = "tokio" -version = "1.33.0" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.4", + "socket2 0.5.8", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -3433,6 +4017,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls 0.23.19", + "tokio", +] + [[package]] name = "tokio-socks" version = "0.5.1" @@ -3456,6 +4050,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "rustls 0.23.19", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.3", +] + [[package]] name = "tokio-util" version = "0.7.9" @@ -3575,7 +4185,7 @@ dependencies = [ "once_cell", "regex", "sharded-slab", - "smallvec 1.11.1", + "smallvec", "thread_local", "tracing", "tracing-core", @@ -3588,6 +4198,26 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.2.0", + "httparse", + "log", + "rand", + "rustls 0.23.19", + "rustls-pki-types", + "sha1", + "thiserror", + "utf-8", +] + [[package]] name = "typenum" version = "1.17.0" @@ -3648,11 +4278,11 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.9" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c8070a9942f5e7cfccd93f490fdebd230ee3c3c9f107cb25bad5351ef671cf" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ - "smallvec 0.6.14", + "tinyvec", ] [[package]] @@ -3818,15 +4448,34 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.3.0" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fe195a4f217c25b25cb5058ced57059824a678474874038dc88d211bf508d3" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.1" @@ -4232,6 +4881,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "yaml-rust" version = "0.4.5" @@ -4250,6 +4911,30 @@ dependencies = [ "is-terminal", ] +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -4270,8 +4955,51 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] diff --git a/Cargo.toml b/Cargo.toml index 5f7d46e9..c08e951b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,4 @@ [workspace] -members = ["clients/apps/rust", "clients/libs/rust", "server", "wasm", "lib", "token-server", "clients/tests/rust"] +members = ["clients/apps/rust", "clients/libs/rust", "server", "wasm", "lib", "token-server", "clients/tests/rust", "token-server-v2"] exclude = ["clients/apps/kotlin", "clients/apps/nodejs", "clients/apps/react-app", "clients/libs/nodejs", "docs", "enclave", "explorer", "keylistCron"] resolver = "2" diff --git a/Test.md b/Test.md new file mode 100644 index 00000000..f50dcb1a --- /dev/null +++ b/Test.md @@ -0,0 +1,72 @@ +# Running Rust tests + +1. `$ docker compose -f docker-compose-token-servers.yml up --build` to run the Mercury and token servers. This also starts a Esplora node. + +2. Run the commands below to start the Bitcoin network. + +```bash +$ container_id=$(docker ps -qf "name=esplora-container") +$ wallet_name="esplora_wallet" +$ docker exec $container_id cli createwallet $wallet_name +$ address=$(docker exec $container_id cli getnewaddress $wallet_name) +$ docker exec $container_id cli generatetoaddress 101 "$address" +``` + +3. `cd clients/tests/rust/` + +4. `cargo run` + +# Running Web tests + +1. `$ docker compose -f docker-compose-token-servers.yml up --build` to run the Mercury and token servers. This also starts a Esplora node. + +2. `$ docker compose -f docker-compose-token-lnd-nodes.yml up --build` to start LND nodes to test lightning latch functions. + +3. Run the commands below to start the Bitcoin network. + +```bash +$ container_id=$(docker ps -qf "name=esplora-container") +$ wallet_name="esplora_wallet" +$ docker exec $container_id cli createwallet $wallet_name +$ address=$(docker exec $container_id cli getnewaddress $wallet_name) +$ docker exec $container_id cli generatetoaddress 101 "$address" +``` + +4. Run the commands below to start and set up the LND nodes. + +```bash +$ container_id=$(docker ps -qf "name=mercurylayer-bitcoind-1") +$ wallet_name="new_wallet" +$ docker exec $container_id bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass createwallet $wallet_name +$ address=$(docker exec $container_id bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass getnewaddress $wallet_name) +$ docker exec $container_id bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass generatetoaddress 101 "$address" +$ container_id_alice=$(docker ps -qf "name=mercurylayer-alice-1") +$ container_id_bob=$(docker ps -qf "name=mercurylayer-bob-1") +$ identity_pubkey_bob=$(docker exec $container_id_bob lncli -n regtest getinfo | jq -r '.identity_pubkey') +$ docker exec $container_id_alice lncli -n regtest connect $identity_pubkey_bob@bob:9735 +$ address=$(docker exec $container_id_bob lncli -n regtest newaddress p2wkh | jq -r '.address') +$ container_id_bitcoind=$(docker ps -qf "name=mercurylayer-bitcoind-1") +$ docker exec $container_id_bitcoind bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass sendtoaddress $address 0.5 +$ docker exec $(docker ps -qf "name=mercurylayer-bitcoind-1") bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass -generate 6 +$ identity_pubkey_alice=$(docker exec $container_id_alice lncli -n regtest getinfo | jq -r '.identity_pubkey') +$ docker exec $container_id_bob lncli -n regtest openchannel $identity_pubkey_alice 100000 +$ docker exec $(docker ps -qf "name=mercurylayer-bitcoind-1") bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass -generate 5 +``` + + +5. Install the nodejs components and run the tests: + +```bash +$ cd clients/libs/web +$ npm i +$ cd ../../tests/web +$ npm i +$ npm install chai +$ node server-regtest.cjs # start the test server +$ npx vitest --browser.name=chromium --browser.headless --reporter=basic --disable-console-intercept +``` + +# Other Environments + +The `docker compose -f docker-compose-token-servers.yml` file can be used to build a Mercury Layer infrastructure in test or production environments as it has all the necessary servers including Lockbox, Electrs/Esplora, Token, and Mercury. +The default values ​​of this file however must be changed. diff --git a/clients/apps/nodejs/index.js b/clients/apps/nodejs/index.js index 7c4ade41..3a81a826 100644 --- a/clients/apps/nodejs/index.js +++ b/clients/apps/nodejs/index.js @@ -30,10 +30,8 @@ async function main() { program.command('new-token') .description('Get new token.') - .argument('', 'name of the wallet') - .action(async (wallet_name) => { - - const token = await mercurynodejslib.newToken(clientConfig, wallet_name); + .action(async () => { + const token = await mercurynodejslib.newToken(clientConfig); console.log(JSON.stringify(token, null, 2)); }); diff --git a/clients/apps/rust/Cargo.toml b/clients/apps/rust/Cargo.toml index 1113f7a7..3142a8d6 100644 --- a/clients/apps/rust/Cargo.toml +++ b/clients/apps/rust/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" anyhow = "1.0" bech32 = { version = "0.9.1", default-features = false } bitcoin = { version = "0.30.1", features = ["serde", "base64", "rand-std", "std", "bitcoinconsensus"], default-features = false } -bip39 = "1.2.0" +bip39 = "2.0" clap = { version = "4.2.5", features = ["derive"]} chrono = "0.4.31" config = "0.13.1" diff --git a/clients/apps/rust/rust-toolchain.toml b/clients/apps/rust/rust-toolchain.toml index a56a283d..0193dee3 100644 --- a/clients/apps/rust/rust-toolchain.toml +++ b/clients/apps/rust/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.80.1" +channel = "1.83.0" diff --git a/clients/apps/rust/src/main.rs b/clients/apps/rust/src/main.rs index 7b15138e..daeae748 100644 --- a/clients/apps/rust/src/main.rs +++ b/clients/apps/rust/src/main.rs @@ -58,6 +58,7 @@ enum Commands { force_send: Option, /// Batch id for atomic transfers batch_id: Option, + duplicated_indexes: Option>, }, /// Send a statechain coin to a transfer address TransferReceive { wallet_name: String }, @@ -100,9 +101,9 @@ async fn main() -> Result<()> { println!("Wallet created: {:?}", wallet); }, Commands::NewToken { } => { - let token_id = mercuryrustlib::deposit::get_token(&client_config).await?; + let token_response = mercuryrustlib::deposit::get_token(&client_config).await?; - let obj = json!({"token": token_id}); + let obj = json!(token_response); println!("{}", serde_json::to_string_pretty(&obj).unwrap()); }, @@ -158,12 +159,12 @@ async fn main() -> Result<()> { println!("{}", serde_json::to_string_pretty(&obj).unwrap()); }, - Commands::TransferSend { wallet_name, statechain_id, to_address, force_send, batch_id } => { + Commands::TransferSend { wallet_name, statechain_id, to_address, force_send, batch_id, duplicated_indexes } => { mercuryrustlib::coin_status::update_coins(&client_config, &wallet_name).await?; let force_send = force_send.unwrap_or(false); - mercuryrustlib::transfer_sender::execute(&client_config, &to_address, &wallet_name, &statechain_id, force_send, batch_id).await?; + mercuryrustlib::transfer_sender::execute(&client_config, &to_address, &wallet_name, &statechain_id, duplicated_indexes, force_send, batch_id).await?; let obj = json!({"Transfer": "sent"}); diff --git a/clients/libs/nodejs/coin_enum.js b/clients/libs/nodejs/coin_enum.js index 6482dd14..9b9583bb 100644 --- a/clients/libs/nodejs/coin_enum.js +++ b/clients/libs/nodejs/coin_enum.js @@ -9,6 +9,8 @@ const CoinStatus = { TRANSFERRED: "TRANSFERRED", // the coin was transferred WITHDRAWN: "WITHDRAWN", // the coin was withdrawn DUPLICATED: "DUPLICATED", // the coin was withdrawn + DUPLICATED: "DUPLICATED", // the coin was withdrawn + INVALIDATED: "INVALIDATED", // the coin was invalidated }; module.exports = { CoinStatus }; \ No newline at end of file diff --git a/clients/libs/rust/Cargo.toml b/clients/libs/rust/Cargo.toml index c502f3e9..43341475 100644 --- a/clients/libs/rust/Cargo.toml +++ b/clients/libs/rust/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" anyhow = "1.0" bech32 = { version = "0.9.1", default-features = false } bitcoin = { version = "0.30.1", features = ["serde", "base64", "rand-std", "std", "bitcoinconsensus"], default-features = false } -bip39 = "1.2.0" +bip39 = "2.0" clap = { version = "4.2.5", features = ["derive"]} chrono = "0.4.31" config = "0.13.1" diff --git a/clients/libs/rust/migrations/0001_signer_data_table.sql b/clients/libs/rust/migrations/0001_signer_data_table.sql index 93511757..df3f25dc 100644 --- a/clients/libs/rust/migrations/0001_signer_data_table.sql +++ b/clients/libs/rust/migrations/0001_signer_data_table.sql @@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS wallet ( ); CREATE TABLE IF NOT EXISTS backup_txs ( + wallet_name TEXT NOT NULL, statechain_id TEXT NOT NULL, txs TEXT NOT NULL ); diff --git a/clients/libs/rust/rust-toolchain.toml b/clients/libs/rust/rust-toolchain.toml index a56a283d..0193dee3 100644 --- a/clients/libs/rust/rust-toolchain.toml +++ b/clients/libs/rust/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.80.1" +channel = "1.83.0" diff --git a/clients/libs/rust/src/broadcast_backup_tx.rs b/clients/libs/rust/src/broadcast_backup_tx.rs index db544d38..7278b56b 100644 --- a/clients/libs/rust/src/broadcast_backup_tx.rs +++ b/clients/libs/rust/src/broadcast_backup_tx.rs @@ -16,7 +16,7 @@ pub async fn execute(client_config: &ClientConfig, wallet_name: &str, statechain } } - let backup_txs = get_backup_txs(&client_config.pool, &statechain_id).await?; + let backup_txs = get_backup_txs(&client_config.pool, &wallet.name, &statechain_id).await?; // If the user sends to himself, he will have two coins with same statechain_id // In this case, we need to find the one with the lowest locktime diff --git a/clients/libs/rust/src/coin_status.rs b/clients/libs/rust/src/coin_status.rs index 664138a7..d7b68811 100644 --- a/clients/libs/rust/src/coin_status.rs +++ b/clients/libs/rust/src/coin_status.rs @@ -59,12 +59,20 @@ async fn check_deposit(client_config: &ClientConfig, coin: &mut Coin, wallet_net let utxo_txid = utxo.tx_hash.to_string(); let utxo_vout = utxo.tx_pos as u32; - let backup_tx = create_tx1(client_config, coin, wallet_netwotk, &utxo_txid, utxo_vout).await?; + if coin.status != CoinStatus::INITIALISED { + return Err(anyhow!("The coin with the public key {} is not in the INITIALISED state", coin.user_pubkey.to_string())); + } + + coin.utxo_txid = Some(utxo_txid.to_string()); + coin.utxo_vout = Some(utxo_vout); + + coin.status = CoinStatus::IN_MEMPOOL; + + let backup_tx = create_tx1(client_config, coin, wallet_netwotk, 1u32).await?; let activity_utxo = format!("{}:{}", utxo.tx_hash.to_string(), utxo.tx_pos); let activity = Some(create_activity(&activity_utxo, utxo.value as u32, "deposit")); - // return Ok(Some(activity)); deposit_result = Some(DepositResult { activity: activity.unwrap(), @@ -238,7 +246,7 @@ pub async fn update_coins(client_config: &ClientConfig, wallet_name: &str) -> Re let backup_tx = deposit_result.backup_tx; wallet.activities.push(activity); - insert_backup_txs(&client_config.pool, &coin.statechain_id.as_ref().unwrap(), &[backup_tx].to_vec()).await?; + insert_backup_txs(&client_config.pool, &wallet.name, &coin.statechain_id.as_ref().unwrap(), &[backup_tx].to_vec()).await?; } } else if coin.status == CoinStatus::IN_TRANSFER { @@ -257,6 +265,21 @@ pub async fn update_coins(client_config: &ClientConfig, wallet_name: &str) -> Re wallet.coins.extend(duplicated_coins); + // invalidate duplicated coins that were not transferred + for i in 0..wallet.coins.len() { + if wallet.coins[i].status == CoinStatus::DUPLICATED { + let is_transferred = (0..wallet.coins.len()).any(|j| + i != j && // Skip comparing with self + wallet.coins[j].statechain_id == wallet.coins[i].statechain_id && + wallet.coins[j].locktime == wallet.coins[i].locktime && + wallet.coins[j].status == CoinStatus::TRANSFERRED + ); + if is_transferred { + wallet.coins[i].status = CoinStatus::INVALIDATED; + } + } + } + update_wallet(&client_config.pool, &wallet).await?; Ok(()) diff --git a/clients/libs/rust/src/deposit.rs b/clients/libs/rust/src/deposit.rs index 3ed62f31..bb179b1a 100644 --- a/clients/libs/rust/src/deposit.rs +++ b/clients/libs/rust/src/deposit.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Result, Ok}; -use mercurylib::{deposit::{create_deposit_msg1, create_aggregated_address}, wallet::{Wallet, BackupTx, CoinStatus, Coin}, transaction:: get_user_backup_address, utils::get_blockheight}; +use mercurylib::{deposit::{create_deposit_msg1, create_aggregated_address}, wallet::{Wallet, BackupTx, Coin}, transaction:: get_user_backup_address, utils::get_blockheight}; use crate::{client_config::ClientConfig, sqlite_manager::{get_wallet, update_wallet}, transaction::new_transaction, utils::info_config}; @@ -23,19 +23,8 @@ pub async fn get_deposit_bitcoin_address(client_config: &ClientConfig, wallet_na Ok(aggregated_public_key.aggregate_address) } -pub async fn create_tx1(client_config: &ClientConfig, coin: &mut Coin, wallet_netwotk: &str, tx0_hash: &str, tx0_vout: u32) -> Result { - - if coin.status != CoinStatus::INITIALISED { - return Err(anyhow!("The coin with the public key {} is not in the INITIALISED state", coin.user_pubkey.to_string())); - } - - if coin.utxo_txid.is_some() && coin.utxo_vout.is_some() { - return Err(anyhow!("The coin with the public key {} has already been deposited", coin.user_pubkey.to_string())); - } - coin.utxo_txid = Some(tx0_hash.to_string()); - coin.utxo_vout = Some(tx0_vout); - - coin.status = CoinStatus::IN_MEMPOOL; +// When sending duplicated coins, the tx_n of the backup_tx must be different +pub async fn create_tx1(client_config: &ClientConfig, coin: &mut Coin, wallet_netwotk: &str, tx_n: u32) -> Result { let to_address = get_user_backup_address(&coin, wallet_netwotk.to_string())?; @@ -73,7 +62,7 @@ pub async fn create_tx1(client_config: &ClientConfig, coin: &mut Coin, wallet_ne } let backup_tx = BackupTx { - tx_n: 1, + tx_n, tx: signed_tx, client_public_nonce: coin.public_nonce.as_ref().unwrap().to_string(), server_public_nonce: coin.server_public_nonce.as_ref().unwrap().to_string(), @@ -132,7 +121,7 @@ pub async fn init(client_config: &ClientConfig, wallet: &Wallet, token_id: uuid: Ok(wallet) } -pub async fn get_token(client_config: &ClientConfig) -> Result { +pub async fn get_token(client_config: &ClientConfig) -> Result { let endpoint = client_config.statechain_entity.clone(); let path = "deposit/get_token"; @@ -149,7 +138,7 @@ pub async fn get_token(client_config: &ClientConfig) -> Result { let value = response.text().await?; - let token: mercurylib::deposit::TokenID = serde_json::from_str(value.as_str())?; + let token: mercurylib::deposit::TokenResponse = serde_json::from_str(value.as_str())?; - return Ok(token.token_id); + return Ok(token); } diff --git a/clients/libs/rust/src/lib.rs b/clients/libs/rust/src/lib.rs index 17004821..71d7f8dd 100644 --- a/clients/libs/rust/src/lib.rs +++ b/clients/libs/rust/src/lib.rs @@ -16,11 +16,13 @@ pub use mercurylib::wallet::CoinStatus; pub use mercurylib::wallet::Coin; pub use mercurylib::wallet::BackupTx; pub use mercurylib::wallet::Activity; +pub use mercurylib::wallet::get_previous_outpoint; pub use mercurylib::transfer::sender::{TransferSenderRequestPayload, TransferSenderResponsePayload, create_transfer_signature, create_transfer_update_msg}; pub use mercurylib::transaction::{SignFirstRequestPayload, SignFirstResponsePayload, create_and_commit_nonces}; pub use mercurylib::utils::get_blockheight; pub use mercurylib::{validate_address, decode_transfer_address}; +pub use mercurylib::deposit::TokenResponse; pub fn add(left: usize, right: usize) -> usize { left + right diff --git a/clients/libs/rust/src/sqlite_manager.rs b/clients/libs/rust/src/sqlite_manager.rs index 088fb753..fc14571b 100644 --- a/clients/libs/rust/src/sqlite_manager.rs +++ b/clients/libs/rust/src/sqlite_manager.rs @@ -53,13 +53,14 @@ pub async fn update_wallet(pool: &Pool, wallet: &Wallet) -> Result<()> { Ok(()) } -pub async fn insert_backup_txs(pool: &Pool, statechain_id: &str, backup_txs: &Vec) -> Result<()> { +pub async fn insert_backup_txs(pool: &Pool, wallet_name: &str, statechain_id: &str, backup_txs: &Vec) -> Result<()> { let backup_txs_json = json!(backup_txs).to_string(); - let query = "INSERT INTO backup_txs (statechain_id, txs) VALUES ($1, $2)"; + let query = "INSERT INTO backup_txs (wallet_name, statechain_id, txs) VALUES ($1, $2, $3)"; let _ = sqlx::query(query) + .bind(wallet_name) .bind(statechain_id) .bind(backup_txs_json) .execute(pool) @@ -68,27 +69,29 @@ pub async fn insert_backup_txs(pool: &Pool, statechain_id: &str, backup_ Ok(()) } -pub async fn update_backup_txs(pool: &Pool, statechain_id: &str, backup_txs: &Vec) -> Result<()> { +pub async fn update_backup_txs(pool: &Pool, wallet_name: &str, statechain_id: &str, backup_txs: &Vec) -> Result<()> { let backup_txs_json = json!(backup_txs).to_string(); - let query = "UPDATE backup_txs SET txs = $1 WHERE statechain_id = $2"; + let query = "UPDATE backup_txs SET txs = $1 WHERE statechain_id = $2 AND wallet_name = $3"; let _ = sqlx::query(query) .bind(backup_txs_json) .bind(statechain_id) + .bind(wallet_name) .execute(pool) .await?; Ok(()) } -pub async fn get_backup_txs(pool: &Pool, statechain_id: &str,) -> Result> { +pub async fn get_backup_txs(pool: &Pool, wallet_name: &str, statechain_id: &str,) -> Result> { - let query = "SELECT txs FROM backup_txs WHERE statechain_id = $1"; + let query = "SELECT txs FROM backup_txs WHERE statechain_id = $1 AND wallet_name = $2"; let row = sqlx::query(query) .bind(statechain_id) + .bind(wallet_name) .fetch_one(pool) .await?; @@ -103,23 +106,25 @@ pub async fn get_backup_txs(pool: &Pool, statechain_id: &str,) -> Result Ok(backup_txs) } -pub async fn insert_or_update_backup_txs(pool: &Pool, statechain_id: &str, backup_txs: &Vec) -> Result<()> { +pub async fn insert_or_update_backup_txs(pool: &Pool, wallet_name: &str, statechain_id: &str, backup_txs: &Vec) -> Result<()> { let mut transaction = pool.begin().await?; let backup_txs_json = json!(backup_txs).to_string(); - let query = "DELETE FROM backup_txs WHERE statechain_id = $1"; + let query = "DELETE FROM backup_txs WHERE statechain_id = $1 AND wallet_name = $2"; let _ = sqlx::query(query) .bind(statechain_id) + .bind(wallet_name) .execute(&mut *transaction) .await?; - let query = "INSERT INTO backup_txs (statechain_id, txs) VALUES ($1, $2)"; + let query = "INSERT INTO backup_txs (statechain_id, wallet_name, txs) VALUES ($1, $2, $3)"; let _ = sqlx::query(query) .bind(statechain_id) + .bind(wallet_name) .bind(backup_txs_json) .execute(&mut *transaction) .await?; diff --git a/clients/libs/rust/src/transfer_receiver.rs b/clients/libs/rust/src/transfer_receiver.rs index 3e4d560e..8a5ac4f0 100644 --- a/clients/libs/rust/src/transfer_receiver.rs +++ b/clients/libs/rust/src/transfer_receiver.rs @@ -5,7 +5,7 @@ use anyhow::{anyhow, Ok, Result}; use bitcoin::{Txid, Address}; use chrono::Utc; use electrum_client::ElectrumApi; -use mercurylib::{utils::{get_network, InfoConfig}, wallet::{Activity, Coin, CoinStatus}}; +use mercurylib::{utils::{get_network, InfoConfig}, wallet::{get_previous_outpoint, Activity, BackupTx, Coin, CoinStatus}}; use reqwest::StatusCode; pub async fn new_transfer_address(client_config: &ClientConfig, wallet_name: &str) -> Result{ @@ -28,6 +28,49 @@ pub struct TransferReceiveResult { pub received_statechain_ids: Vec, } +pub struct DuplicatedCoinData { + pub txid: String, + pub vout: u32, + pub amount: u64, + pub index: u32, +} + +pub struct MessageResult { + pub is_batch_locked: bool, + pub statechain_id: Option, + pub duplicated_coins: Vec, +} + +pub fn sort_coins_by_statechain(coins: &mut Vec) { + // Create a map to store the position of first occurrence of each statechain_id + let mut first_positions: HashMap = HashMap::new(); + + // Record the position of the first occurrence of each statechain_id + for (idx, coin) in coins.iter().enumerate() { + if let Some(id) = &coin.statechain_id { + first_positions.entry(id.clone()).or_insert(idx); + } + } + + // Sort the vector maintaining original order of different statechain_ids + coins.sort_by(|a, b| { + match (&a.statechain_id, &b.statechain_id) { + (None, None) => a.duplicate_index.cmp(&b.duplicate_index), + (None, Some(_)) => std::cmp::Ordering::Greater, + (Some(_), None) => std::cmp::Ordering::Less, + (Some(id_a), Some(id_b)) => { + if id_a == id_b { + // Same statechain_id: sort by duplicate_index + a.duplicate_index.cmp(&b.duplicate_index) + } else { + // Different statechain_ids: compare their first positions + first_positions[id_a].cmp(&first_positions[id_b]) + } + } + } + }); +} + pub async fn execute(client_config: &ClientConfig, wallet_name: &str) -> Result{ let mut wallet = get_wallet(&client_config.pool, &wallet_name).await?; @@ -59,6 +102,8 @@ pub async fn execute(client_config: &ClientConfig, wallet_name: &str) -> Result< let mut temp_coins = wallet.coins.clone(); let mut temp_activities = wallet.activities.clone(); + let mut duplicated_coins: Vec = Vec::new(); + let block_header = client_config.electrum_client.block_headers_subscribe_raw()?; let blockheight = block_header.height as u32; @@ -74,10 +119,17 @@ pub async fn execute(client_config: &ClientConfig, wallet_name: &str) -> Result< let mut coin = coin.unwrap(); - let message_result = process_encrypted_message(client_config, &mut coin, enc_message, &wallet.network, &info_config, blockheight, &mut temp_activities).await; + let is_msg_valid = validate_encrypted_message(client_config, &coin, enc_message, &wallet.network, &info_config, blockheight).await; + + if is_msg_valid.is_err() { + println!("Validation error: {}", is_msg_valid.err().unwrap().to_string()); + continue; + } + + let message_result = process_encrypted_message(client_config, &mut coin, enc_message, &wallet.network, &wallet.name, &mut temp_activities).await; if message_result.is_err() { - println!("Error: {}", message_result.err().unwrap().to_string()); + println!("Processing error: {}", message_result.err().unwrap().to_string()); continue; } @@ -91,6 +143,21 @@ pub async fn execute(client_config: &ClientConfig, wallet_name: &str) -> Result< received_statechain_ids.push(message_result.statechain_id.unwrap()); } + if message_result.duplicated_coins.len() > 0 { + + assert!(!message_result.is_batch_locked); + + for duplicated_coin_data in message_result.duplicated_coins { + let mut duplicated_coin = coin.clone(); + duplicated_coin.status = CoinStatus::DUPLICATED; + duplicated_coin.utxo_txid = Some(duplicated_coin_data.txid); + duplicated_coin.utxo_vout = Some(duplicated_coin_data.vout); + duplicated_coin.amount = Some(duplicated_coin_data.amount as u32); + duplicated_coin.duplicate_index = duplicated_coin_data.index; + duplicated_coins.push(duplicated_coin); + } + } + } else { let new_coin = mercurylib::transfer::receiver::duplicate_coin_to_initialized_state(&wallet, &auth_pubkey); @@ -102,14 +169,21 @@ pub async fn execute(client_config: &ClientConfig, wallet_name: &str) -> Result< let mut new_coin = new_coin.unwrap(); - let message_result = process_encrypted_message(client_config, &mut new_coin, enc_message, &wallet.network, &info_config, blockheight, &mut temp_activities).await; + let is_msg_valid = validate_encrypted_message(client_config, &new_coin, enc_message, &wallet.network, &info_config, blockheight).await; + + if is_msg_valid.is_err() { + println!("Validation error: {}", is_msg_valid.err().unwrap().to_string()); + continue; + } + + let message_result = process_encrypted_message(client_config, &mut new_coin, enc_message, &wallet.network, &wallet.name, &mut temp_activities).await; if message_result.is_err() { - println!("Error: {}", message_result.err().unwrap().to_string()); + println!("Processing error: {}", message_result.err().unwrap().to_string()); continue; } - temp_coins.push(new_coin); + temp_coins.push(new_coin.clone()); let message_result = message_result.unwrap(); @@ -120,10 +194,28 @@ pub async fn execute(client_config: &ClientConfig, wallet_name: &str) -> Result< if message_result.statechain_id.is_some() { received_statechain_ids.push(message_result.statechain_id.unwrap()); } + + if message_result.duplicated_coins.len() > 0 { + + assert!(!message_result.is_batch_locked); + + for duplicated_coin_data in message_result.duplicated_coins { + let mut duplicated_coin = new_coin.clone(); + duplicated_coin.status = CoinStatus::DUPLICATED; + duplicated_coin.utxo_txid = Some(duplicated_coin_data.txid); + duplicated_coin.utxo_vout = Some(duplicated_coin_data.vout); + duplicated_coin.amount = Some(duplicated_coin_data.amount as u32); + duplicated_coin.duplicate_index = duplicated_coin_data.index; + // temp_coins.push(duplicated_coin); + duplicated_coins.push(duplicated_coin); + } + } } } } + temp_coins.extend(duplicated_coins); + sort_coins_by_statechain(&mut temp_coins); wallet.coins = temp_coins.clone(); wallet.activities = temp_activities.clone(); @@ -149,143 +241,235 @@ async fn get_msg_addr(auth_pubkey: &str, client_config: &ClientConfig) -> Result Ok(response.list_enc_transfer_msg) } -pub struct MessageResult { - pub is_batch_locked: bool, - pub statechain_id: Option, +pub fn split_backup_transactions(backup_transactions: &Vec) -> Vec> { + // HashMap to store grouped transactions + let mut grouped_txs: HashMap<(String, u32), Vec> = HashMap::new(); + + // Vector to keep track of order of appearance of outpoints + let mut order_of_appearance: Vec<(String, u32)> = Vec::new(); + // HashSet to track which outpoints we've seen + let mut seen_outpoints: HashSet<(String, u32)> = HashSet::new(); + + // Process each transaction + for tx in backup_transactions { + // Get the outpoint for this transaction + let outpoint = get_previous_outpoint(&tx).expect("Valid outpoint"); + + // Create a key tuple from txid and vout + let key = (outpoint.txid, outpoint.vout); + + // If we haven't seen this outpoint before, record its order + if seen_outpoints.insert(key.clone()) { + order_of_appearance.push(key.clone()); + } + + // Add the transaction to its group + grouped_txs.entry(key).or_insert_with(Vec::new).push(tx.clone()); + } + + // Create result vector maintaining order of first appearance + let mut result: Vec> = Vec::with_capacity(order_of_appearance.len()); + + // Add vectors to result in order of first appearance + for key in order_of_appearance { + if let Some(mut transactions) = grouped_txs.remove(&key) { + // Sort each group by tx_n + transactions.sort_by_key(|tx| tx.tx_n); + result.push(transactions); + } + } + + result } -async fn process_encrypted_message(client_config: &ClientConfig, coin: &mut Coin, enc_message: &str, network: &str, info_config: &InfoConfig, blockheight: u32, activities: &mut Vec) -> Result { +async fn validate_encrypted_message(client_config: &ClientConfig, coin: &Coin, enc_message: &str, network: &str, info_config: &InfoConfig, blockheight: u32) -> Result<()> { let client_auth_key = coin.auth_privkey.clone(); let new_user_pubkey = coin.user_pubkey.clone(); let transfer_msg = mercurylib::transfer::receiver::decrypt_transfer_msg(enc_message, &client_auth_key)?; - let tx0_outpoint = mercurylib::transfer::receiver::get_tx0_outpoint(&transfer_msg.backup_transactions)?; - - let tx0_hex = get_tx0(&client_config.electrum_client, &tx0_outpoint.txid).await?; + let grouped_backup_transactions = split_backup_transactions(&transfer_msg.backup_transactions); - let is_transfer_signature_valid = mercurylib::transfer::receiver::verify_transfer_signature(&new_user_pubkey, &tx0_outpoint, &transfer_msg)?; + for (index, backup_transactions) in grouped_backup_transactions.iter().enumerate() { + + let tx0_outpoint = mercurylib::transfer::receiver::get_tx0_outpoint(backup_transactions)?; - if !is_transfer_signature_valid { - return Err(anyhow::anyhow!("Invalid transfer signature".to_string())); - } + let tx0_hex = get_tx0(&client_config.electrum_client, &tx0_outpoint.txid).await?; - let statechain_info = utils::get_statechain_info(&transfer_msg.statechain_id, &client_config).await?; + if index == 0 { + let is_transfer_signature_valid = mercurylib::transfer::receiver::verify_transfer_signature(&new_user_pubkey, &tx0_outpoint, &transfer_msg)?; - if statechain_info.is_none() { - return Err(anyhow::anyhow!("Statechain info not found".to_string())); - } + if !is_transfer_signature_valid { + return Err(anyhow::anyhow!("Invalid transfer signature".to_string())); + } + } - let statechain_info = statechain_info.unwrap(); + let statechain_info = utils::get_statechain_info(&transfer_msg.statechain_id, &client_config).await?; - let is_tx0_output_pubkey_valid = mercurylib::transfer::receiver::validate_tx0_output_pubkey(&statechain_info.enclave_public_key, &transfer_msg, &tx0_outpoint, &tx0_hex, network)?; + if statechain_info.is_none() { + return Err(anyhow::anyhow!("Statechain info not found".to_string())); + } - if !is_tx0_output_pubkey_valid { - return Err(anyhow::anyhow!("Invalid tx0 output pubkey".to_string())); - } + let statechain_info = statechain_info.unwrap(); - let latest_backup_tx_pays_to_user_pubkey = mercurylib::transfer::receiver::verify_latest_backup_tx_pays_to_user_pubkey(&transfer_msg, &new_user_pubkey, network)?; + let is_tx0_output_pubkey_valid = mercurylib::transfer::receiver::validate_tx0_output_pubkey(&statechain_info.enclave_public_key, &transfer_msg, &tx0_outpoint, &tx0_hex, network)?; - if !latest_backup_tx_pays_to_user_pubkey { - return Err(anyhow::anyhow!("Latest Backup Tx does not pay to the expected public key".to_string())); - } + if !is_tx0_output_pubkey_valid { + return Err(anyhow::anyhow!("Invalid tx0 output pubkey".to_string())); + } - if statechain_info.num_sigs != transfer_msg.backup_transactions.len() as u32 { - return Err(anyhow::anyhow!("num_sigs is not correct".to_string())); - } + let latest_backup_tx_pays_to_user_pubkey = mercurylib::transfer::receiver::verify_latest_backup_tx_pays_to_user_pubkey(&transfer_msg, &new_user_pubkey, network)?; - let (is_tx0_output_unspent, tx0_status) = verify_tx0_output_is_unspent_and_confirmed(&client_config.electrum_client, &tx0_outpoint, &tx0_hex, &network, client_config.confirmation_target).await?; + if !latest_backup_tx_pays_to_user_pubkey { + return Err(anyhow::anyhow!("Latest Backup Tx does not pay to the expected public key".to_string())); + } - if !is_tx0_output_unspent { - return Err(anyhow::anyhow!("tx0 output is spent or not confirmed".to_string())); + if statechain_info.num_sigs != transfer_msg.backup_transactions.len() as u32 { + return Err(anyhow::anyhow!("num_sigs is not correct".to_string())); + } + + let (is_tx0_output_unspent, _) = verify_tx0_output_is_unspent_and_confirmed(&client_config.electrum_client, &tx0_outpoint, &tx0_hex, &network, client_config.confirmation_target).await?; + + if !is_tx0_output_unspent { + return Err(anyhow::anyhow!("tx0 output is spent or not confirmed".to_string())); + } + + let current_fee_rate_sats_per_byte = if info_config.fee_rate_sats_per_byte > client_config.max_fee_rate { + client_config.max_fee_rate + } else { + info_config.fee_rate_sats_per_byte + }; + + let previous_lock_time = mercurylib::transfer::receiver::validate_signature_scheme( + backup_transactions, + &statechain_info, + &tx0_hex, + blockheight, + client_config.fee_rate_tolerance, + current_fee_rate_sats_per_byte, + info_config.initlock, + info_config.interval); + + if previous_lock_time.is_err() { + let error = previous_lock_time.err().unwrap(); + return Err(anyhow!("Signature scheme validation failed. Error {}", error.to_string())); + } } - let current_fee_rate_sats_per_byte = if info_config.fee_rate_sats_per_byte > client_config.max_fee_rate { - client_config.max_fee_rate - } else { - info_config.fee_rate_sats_per_byte + Ok(()) +} + +async fn process_encrypted_message(client_config: &ClientConfig, coin: &mut Coin, enc_message: &str, network: &str, wallet_name: &str, activities: &mut Vec) -> Result { + + let mut transfer_receive_result = MessageResult { + is_batch_locked: false, + statechain_id: None, + duplicated_coins: Vec::new(), }; - let previous_lock_time = mercurylib::transfer::receiver::validate_signature_scheme( - &transfer_msg, - &statechain_info, - &tx0_hex, - blockheight, - client_config.fee_rate_tolerance, - current_fee_rate_sats_per_byte, - info_config.initlock, - info_config.interval); - - if previous_lock_time.is_err() { - let error = previous_lock_time.err().unwrap(); - return Err(anyhow!("Signature scheme validation failed. Error {}", error.to_string())); - } + let client_auth_key = coin.auth_privkey.clone(); - let previous_lock_time = previous_lock_time.unwrap(); + let transfer_msg = mercurylib::transfer::receiver::decrypt_transfer_msg(enc_message, &client_auth_key)?; - let transfer_receiver_request_payload = mercurylib::transfer::receiver::create_transfer_receiver_request_payload(&statechain_info, &transfer_msg, &coin)?; + let grouped_backup_transactions = split_backup_transactions(&transfer_msg.backup_transactions); - // unlock the statecoin - it might be part of a batch + for (index, backup_transactions) in grouped_backup_transactions.iter().enumerate() { - // the pub_auth_key has not been updated yet in the server (it will be updated after the transfer/receive call) - // So we need to manually sign the statechain_id with the client_auth_key - let signed_statechain_id_for_unlock = mercurylib::transfer::receiver::sign_message(&transfer_msg.statechain_id, &coin)?; + if index == 0 { - unlock_statecoin(&client_config, &transfer_msg.statechain_id, &signed_statechain_id_for_unlock, &coin.auth_pubkey).await?; + let tx0_outpoint = mercurylib::transfer::receiver::get_tx0_outpoint(backup_transactions)?; + let tx0_hex = get_tx0(&client_config.electrum_client, &tx0_outpoint.txid).await?; - let transfer_receiver_result = send_transfer_receiver_request_payload(&client_config, &transfer_receiver_request_payload).await; + let statechain_info = utils::get_statechain_info(&transfer_msg.statechain_id, &client_config).await?; + let statechain_info = statechain_info.unwrap(); - let server_public_key_hex = match transfer_receiver_result { - std::result::Result::Ok(server_public_key_hex) => { + let (_, tx0_status) = verify_tx0_output_is_unspent_and_confirmed(&client_config.electrum_client, &tx0_outpoint, &tx0_hex, &network, client_config.confirmation_target).await?; - if server_public_key_hex.is_batch_locked { - return Ok(MessageResult { - is_batch_locked: true, - statechain_id: None, - }); - } + let backup_tx = backup_transactions.last().unwrap(); - server_public_key_hex.server_pubkey.unwrap() - }, - Err(err) => { - return Err(anyhow::anyhow!("Error: {}", err.to_string())); - } - }; + let last_tx_lock_time = mercurylib::utils::get_blockheight(&backup_tx)?; - let new_key_info = mercurylib::transfer::receiver::get_new_key_info(&server_public_key_hex, &coin, &transfer_msg.statechain_id, &tx0_outpoint, &tx0_hex, network)?; - - coin.server_pubkey = Some(server_public_key_hex); - coin.aggregated_pubkey = Some(new_key_info.aggregate_pubkey); - coin.aggregated_address = Some(new_key_info.aggregate_address); - coin.statechain_id = Some(transfer_msg.statechain_id.clone()); - coin.signed_statechain_id = Some(new_key_info.signed_statechain_id.clone()); - coin.amount = Some(new_key_info.amount); - coin.utxo_txid = Some(tx0_outpoint.txid.clone()); - coin.utxo_vout = Some(tx0_outpoint.vout); - coin.locktime = Some(previous_lock_time); - coin.status = tx0_status; - - let date = Utc::now(); // This will get the current date and time in UTC - let iso_string = date.to_rfc3339(); // Converts the date to an ISO 8601 string - - let activity = Activity { - utxo: tx0_outpoint.txid.clone(), - amount: new_key_info.amount, - action: "Receive".to_string(), - date: iso_string - }; + let transfer_receiver_request_payload = mercurylib::transfer::receiver::create_transfer_receiver_request_payload(&statechain_info, &transfer_msg, &coin)?; - activities.push(activity); + // unlock the statecoin - it might be part of a batch - insert_or_update_backup_txs(&client_config.pool, &transfer_msg.statechain_id, &transfer_msg.backup_transactions).await?; + // the pub_auth_key has not been updated yet in the server (it will be updated after the transfer/receive call) + // So we need to manually sign the statechain_id with the client_auth_key + let signed_statechain_id_for_unlock = mercurylib::transfer::receiver::sign_message(&transfer_msg.statechain_id, &coin)?; - Ok(MessageResult { - is_batch_locked: false, - statechain_id: Some(transfer_msg.statechain_id.clone()), - }) + unlock_statecoin(&client_config, &transfer_msg.statechain_id, &signed_statechain_id_for_unlock, &coin.auth_pubkey).await?; + + let transfer_receiver_result = send_transfer_receiver_request_payload(&client_config, &transfer_receiver_request_payload).await; + + let server_public_key_hex = match transfer_receiver_result { + std::result::Result::Ok(server_public_key_hex) => { + + if server_public_key_hex.is_batch_locked { + return Ok(MessageResult { + is_batch_locked: true, + statechain_id: None, + duplicated_coins: Vec::new(), + }); + } + + server_public_key_hex.server_pubkey.unwrap() + }, + Err(err) => { + return Err(anyhow::anyhow!("Error: {}", err.to_string())); + } + }; + + let new_key_info = mercurylib::transfer::receiver::get_new_key_info(&server_public_key_hex, &coin, &transfer_msg.statechain_id, &tx0_outpoint, &tx0_hex, network)?; + + coin.server_pubkey = Some(server_public_key_hex); + coin.aggregated_pubkey = Some(new_key_info.aggregate_pubkey); + coin.aggregated_address = Some(new_key_info.aggregate_address); + coin.statechain_id = Some(transfer_msg.statechain_id.clone()); + coin.signed_statechain_id = Some(new_key_info.signed_statechain_id.clone()); + coin.amount = Some(new_key_info.amount); + coin.utxo_txid = Some(tx0_outpoint.txid.clone()); + coin.utxo_vout = Some(tx0_outpoint.vout); + coin.locktime = Some(last_tx_lock_time); + coin.status = tx0_status; + + let date = Utc::now(); // This will get the current date and time in UTC + let iso_string = date.to_rfc3339(); // Converts the date to an ISO 8601 string + + let activity = Activity { + utxo: tx0_outpoint.txid.clone(), + amount: new_key_info.amount, + action: "Receive".to_string(), + date: iso_string + }; + + activities.push(activity); - // Ok(transfer_msg.statechain_id.clone()) + insert_or_update_backup_txs(&client_config.pool, wallet_name, &transfer_msg.statechain_id, &transfer_msg.backup_transactions).await?; + + transfer_receive_result.is_batch_locked = false; + transfer_receive_result.statechain_id = Some(transfer_msg.statechain_id.clone()); + } else { + + let tx0_outpoint = mercurylib::transfer::receiver::get_tx0_outpoint(backup_transactions)?; + let tx0_hex = get_tx0(&client_config.electrum_client, &tx0_outpoint.txid).await?; + + let first_backup_tx = backup_transactions.first().unwrap(); + + let tx_outpoint = get_previous_outpoint(&first_backup_tx)?; + + let amount = mercurylib::transfer::receiver::get_amount_from_tx0(&tx0_hex, &tx_outpoint)?; + + transfer_receive_result.duplicated_coins.push(DuplicatedCoinData { + txid: tx_outpoint.txid, + vout: tx_outpoint.vout, + amount, + index: index as u32, + }); + } + } + + Ok(transfer_receive_result) } async fn get_tx0(electrum_client: &electrum_client::Client, tx0_txid: &str) -> Result { @@ -297,14 +481,12 @@ async fn get_tx0(electrum_client: &electrum_client::Client, tx0_txid: &str) -> R return Err(anyhow!("tx0 not found")); } - // let tx0 = bitcoin::consensus::encode::deserialize(&tx_bytes[0])?; - let tx0_hex = hex::encode(&tx_bytes[0]); Ok(tx0_hex) } -async fn verify_tx0_output_is_unspent_and_confirmed(electrum_client: &electrum_client::Client, tx0_outpoint: &mercurylib::transfer::receiver::TxOutpoint, tx0_hex: &str, network: &str, confirmation_target: u32) -> Result<(bool, CoinStatus)> { +async fn verify_tx0_output_is_unspent_and_confirmed(electrum_client: &electrum_client::Client, tx0_outpoint: &mercurylib::transfer::TxOutpoint, tx0_hex: &str, network: &str, confirmation_target: u32) -> Result<(bool, CoinStatus)> { let output_address = mercurylib::transfer::receiver::get_output_address_from_tx0(&tx0_outpoint, &tx0_hex, &network)?; let network = get_network(&network)?; diff --git a/clients/libs/rust/src/transfer_sender.rs b/clients/libs/rust/src/transfer_sender.rs index 6904465a..e413b5a2 100644 --- a/clients/libs/rust/src/transfer_sender.rs +++ b/clients/libs/rust/src/transfer_sender.rs @@ -1,18 +1,204 @@ -use crate::{client_config::ClientConfig, sqlite_manager::{get_backup_txs, get_wallet, update_backup_txs, update_wallet}, transaction::new_transaction, utils::info_config}; +use std::{cmp::Ordering, str::FromStr}; + +use crate::{client_config::ClientConfig, deposit::create_tx1, sqlite_manager::{get_backup_txs, get_wallet, update_backup_txs, update_wallet}, transaction::new_transaction, utils::info_config}; use anyhow::{anyhow, Result}; use chrono::Utc; -use mercurylib::{wallet::{Coin, BackupTx, Activity, CoinStatus}, utils::get_blockheight, decode_transfer_address, transfer::sender::{TransferSenderRequestPayload, TransferSenderResponsePayload, create_transfer_signature, create_transfer_update_msg}}; +use mercurylib::{decode_transfer_address, transfer::sender::{create_transfer_signature, create_transfer_update_msg, TransferSenderRequestPayload, TransferSenderResponsePayload}, utils::get_blockheight, wallet::{get_previous_outpoint, Activity, BackupTx, Coin, CoinStatus, Wallet}}; use electrum_client::ElectrumApi; +pub async fn create_backup_transactions( + client_config: &ClientConfig, + recipient_address: &str, + wallet: &mut Wallet, + statechain_id: &str, + duplicated_indexes: Option>, +) -> Result> { + + // throw error if duplicated_indexes contains an index that does not exist in wallet.coins + // this can be moved to the caller function + if duplicated_indexes.is_some() { + for index in duplicated_indexes.as_ref().unwrap() { + if *index as usize >= wallet.coins.len() { + return Err(anyhow!("Index {} does not exist in wallet.coins", index)); + } + } + } + + let mut coin_list: Vec<&mut Coin> = Vec::new(); + + let backup_transactions = get_backup_txs(&client_config.pool, &wallet.name, &statechain_id).await?; + + // Get coins that already have a backup transaction + for coin in wallet.coins.iter_mut() { + // Check if coin matches any backup transaction and has one of the specified statuses + let has_matching_tx = backup_transactions.iter().any(|backup_tx| { + if let Ok(tx_outpoint) = get_previous_outpoint(backup_tx) { + if let (Some(utxo_txid), Some(utxo_vout)) = (coin.utxo_txid.clone(), coin.utxo_vout) { + (coin.status == CoinStatus::DUPLICATED || + coin.status == CoinStatus::CONFIRMED || + coin.status == CoinStatus::IN_TRANSFER) && + tx_outpoint.txid == utxo_txid && + tx_outpoint.vout == utxo_vout + } else { + false + } + } else { + false + } + }); + + let mut coin_to_add = false; + + if duplicated_indexes.is_some() { + if coin.statechain_id == Some(statechain_id.to_string()) && + (coin.status == CoinStatus::CONFIRMED || coin.status == CoinStatus::IN_TRANSFER) { + coin_to_add = true; + } + + if coin.statechain_id == Some(statechain_id.to_string()) && coin.status == CoinStatus::DUPLICATED && + duplicated_indexes.is_some() && duplicated_indexes.as_ref().unwrap().contains(&coin.duplicate_index) { + coin_to_add = true; + } + } + + if has_matching_tx || coin_to_add { + if coin.locktime.is_none() { + return Err(anyhow::anyhow!("coin.locktime is None")); + } + + let block_header = client_config.electrum_client.block_headers_subscribe_raw()?; + let current_blockheight = block_header.height as u32; + + if current_blockheight > coin.locktime.unwrap() { + return Err(anyhow::anyhow!("The coin is expired. Coin locktime is {} and current blockheight is {}", coin.locktime.unwrap(), current_blockheight)); + } + + coin_list.push(coin); + } + } + + // The backup transaction for the CONFIRMED coin is created when it is detected in the mempool + // So it is exepcted that the coin with duplicate_index == 0 is in the list since it must have at least one backup transaction + let coins_with_zero_index = coin_list + .iter() + .filter(|coin| coin.duplicate_index == 0 && (coin.status == CoinStatus::CONFIRMED || coin.status == CoinStatus::IN_TRANSFER)) + .collect::>(); + + if coins_with_zero_index.len() != 1 { + return Err(anyhow!("There must be at least one coin with duplicate_index == 0")); + } + + for coin in coin_list.iter_mut() { + if coin.status == CoinStatus::DUPLICATED { + let address = bitcoin::Address::from_str(&coin.aggregated_address.as_ref().unwrap())?.require_network(client_config.network)?; + let utxo_list = client_config.electrum_client.script_list_unspent(&address.script_pubkey())?; + + for unspent in utxo_list { + if coin.utxo_txid == Some(unspent.tx_hash.to_string()) && coin.utxo_vout == Some(unspent.tx_pos as u32) { + let mut is_confirmed = false; + + if unspent.height > 0 { + let block_header = client_config.electrum_client.block_headers_subscribe_raw()?; + let blockheight = block_header.height; + + let confirmations = blockheight - unspent.height + 1; + + if confirmations as u32 >= client_config.confirmation_target { + is_confirmed = true; + } + } + + if !is_confirmed { + return Err(anyhow!("The coin with duplicated index {} has not yet been confirmed. This transfer cannot be performed.", coin.duplicate_index)); + } + + break; + } + } + } + } + + // Move the coin with CONFIRMED status to the first position + coin_list.sort_by(|a, b| { + match (&a.status, &b.status) { + (CoinStatus::CONFIRMED, _) => Ordering::Less, + (_, CoinStatus::CONFIRMED) => Ordering::Greater, + _ => Ordering::Equal, + } + }); + + let mut new_backup_transactions = Vec::new(); + + // create backup transaction for every coin + let backup_transactions = get_backup_txs(&client_config.pool, &wallet.name, &statechain_id).await?; + + let mut new_tx_n = backup_transactions.len() as u32; + + for coin in coin_list { + + let mut filtered_transactions: Vec = Vec::new(); + + for backup_tx in &backup_transactions { + if let Ok(tx_outpoint) = get_previous_outpoint(&backup_tx) { + if let (Some(utxo_txid), Some(utxo_vout)) = (coin.utxo_txid.clone(), coin.utxo_vout) { + if tx_outpoint.txid == utxo_txid && tx_outpoint.vout == utxo_vout { + filtered_transactions.push(backup_tx.clone()); + } + } + } + } + + filtered_transactions.sort_by(|a, b| a.tx_n.cmp(&b.tx_n)); + + if filtered_transactions.len() == 0 { + new_tx_n = new_tx_n + 1; + let bkp_tx1 = create_tx1(client_config, coin, &wallet.network, new_tx_n).await?; + filtered_transactions.push(bkp_tx1); + } + + let qt_backup_tx = filtered_transactions.len() as u32; + + new_tx_n = new_tx_n + 1; + + let bkp_tx1 = &filtered_transactions[0]; + + let signed_tx = create_backup_tx_to_receiver(client_config, coin, bkp_tx1, recipient_address, qt_backup_tx, &wallet.network).await?; + + let backup_tx = BackupTx { + tx_n: new_tx_n, + tx: signed_tx.clone(), + client_public_nonce: coin.public_nonce.as_ref().unwrap().to_string(), + server_public_nonce: coin.server_public_nonce.as_ref().unwrap().to_string(), + client_public_key: coin.user_pubkey.clone(), + server_public_key: coin.server_pubkey.as_ref().unwrap().to_string(), + blinding_factor: coin.blinding_factor.as_ref().unwrap().to_string(), + }; + + filtered_transactions.push(backup_tx); + + if coin.duplicate_index == 0 { + new_backup_transactions.splice(0..0, filtered_transactions); + } else { + new_backup_transactions.extend(filtered_transactions); + } + + coin.status = CoinStatus::IN_TRANSFER; + } + + new_backup_transactions.sort_by(|a, b| a.tx_n.cmp(&b.tx_n)); + + Ok(new_backup_transactions) +} + pub async fn execute( client_config: &ClientConfig, recipient_address: &str, wallet_name: &str, statechain_id: &str, + duplicated_indexes: Option>, force_send: bool, batch_id: Option) -> Result<()> { - let mut wallet: mercurylib::wallet::Wallet = get_wallet(&client_config.pool, &wallet_name).await?; let is_address_valid = mercurylib::validate_address(recipient_address, &wallet.network)?; @@ -21,23 +207,16 @@ pub async fn execute( return Err(anyhow!("Invalid address")); } - let mut backup_transactions = get_backup_txs(&client_config.pool, &statechain_id).await?; - - if backup_transactions.len() == 0 { - return Err(anyhow!("No backup transaction associated with this statechain ID were found")); - } - - let qt_backup_tx = backup_transactions.len() as u32; - - backup_transactions.sort_by(|a, b| a.tx_n.cmp(&b.tx_n)); - - let new_tx_n = backup_transactions.len() as u32 + 1; - let is_coin_duplicated = wallet.coins.iter().any(|c| { c.statechain_id == Some(statechain_id.to_string()) && c.status == CoinStatus::DUPLICATED }); + if is_coin_duplicated && !force_send { + return Err(anyhow::anyhow!("Coin is duplicated. If you want to proceed, use the command '--force, -f' option. \ + You will no longer be able to move other duplicate coins with the same statechain_id and this will cause PERMANENT LOSS of these duplicate coin funds.")); + } + let are_there_duplicate_coins_withdrawn = wallet.coins.iter().any(|c| { c.statechain_id == Some(statechain_id.to_string()) && (c.status == CoinStatus::WITHDRAWING || c.status == CoinStatus::WITHDRAWING) && @@ -50,70 +229,35 @@ pub async fn execute( This coin can be withdrawn, however.")); } - // If the user sends to himself, he will have two coins with same statechain_id - // In this case, we need to find the one with the lowest locktime - let coin = wallet.coins - .iter_mut() - .filter(|c| c.statechain_id == Some(statechain_id.to_string()) && c.status != CoinStatus::DUPLICATED) // Filter coins with the specified statechain_id + let coin = &wallet.coins + .iter() + .filter(|c| + c.statechain_id == Some(statechain_id.to_string()) && + (c.status == CoinStatus::CONFIRMED || c.status == CoinStatus::IN_TRANSFER) && + c.duplicate_index == 0) // Filter coins with the specified statechain_id .min_by_key(|c| c.locktime.unwrap_or(u32::MAX)); // Find the one with the lowest locktime if coin.is_none() { - return Err(anyhow!("No coins associated with this statechain ID were found")); + return Err(anyhow!("No coins with status CONFIRMED or IN_TRANSFER associated with this statechain ID were found")); } - let coin = coin.unwrap(); + let coin = coin.unwrap().clone(); - if is_coin_duplicated && !force_send { - return Err(anyhow::anyhow!("Coin is duplicated. If you want to proceed, use the command '--force, -f' option. \ - You will no longer be able to move other duplicate coins with the same statechain_id and this will cause PERMANENT LOSS of these duplicate coin funds.")); - } - - if coin.amount.is_none() { - return Err(anyhow::anyhow!("coin.amount is None")); - } - - if coin.status != CoinStatus::CONFIRMED && coin.status != CoinStatus::IN_TRANSFER { - return Err(anyhow::anyhow!("Coin status must be CONFIRMED or IN_TRANSFER to transfer it. The current status is {}", coin.status)); - } - - if coin.locktime.is_none() { - return Err(anyhow::anyhow!("coin.locktime is None")); - } - - let block_header = client_config.electrum_client.block_headers_subscribe_raw()?; - let current_blockheight = block_header.height as u32; - - if current_blockheight > coin.locktime.unwrap() { - return Err(anyhow::anyhow!("The coin is expired. Coin locktime is {} and current blockheight is {}", coin.locktime.unwrap(), current_blockheight)); - } - - let statechain_id = coin.statechain_id.as_ref().unwrap(); - let signed_statechain_id = coin.signed_statechain_id.as_ref().unwrap(); + let statechain_id = coin.statechain_id.as_ref().unwrap().clone(); + let signed_statechain_id = coin.signed_statechain_id.as_ref().unwrap().clone(); let (_, _, recipient_auth_pubkey) = decode_transfer_address(recipient_address)?; - let x1 = get_new_x1(&client_config, statechain_id, signed_statechain_id, &recipient_auth_pubkey.to_string(), batch_id).await?; - - let bkp_tx1 = &backup_transactions[0]; - - let signed_tx = create_backup_tx_to_receiver(client_config, coin, bkp_tx1, recipient_address, qt_backup_tx, &wallet.network).await?; - - let backup_tx = BackupTx { - tx_n: new_tx_n, - tx: signed_tx.clone(), - client_public_nonce: coin.public_nonce.as_ref().unwrap().to_string(), - server_public_nonce: coin.server_public_nonce.as_ref().unwrap().to_string(), - client_public_key: coin.user_pubkey.clone(), - server_public_key: coin.server_pubkey.as_ref().unwrap().to_string(), - blinding_factor: coin.blinding_factor.as_ref().unwrap().to_string(), - }; - - backup_transactions.push(backup_tx); + let x1 = get_new_x1(&client_config, &statechain_id, &signed_statechain_id, &recipient_auth_pubkey.to_string(), batch_id).await?; let input_txid = coin.utxo_txid.as_ref().unwrap(); let input_vout = coin.utxo_vout.unwrap(); let client_seckey = coin.user_privkey.as_ref(); - let transfer_signature = create_transfer_signature(recipient_address, input_txid, input_vout, client_seckey)?; + let coin_amount = coin.amount.unwrap(); + + let transfer_signature = create_transfer_signature(recipient_address, &input_txid, input_vout, &client_seckey)?; + + let backup_transactions = create_backup_transactions(client_config, recipient_address, &mut wallet, &statechain_id, duplicated_indexes).await?; let transfer_update_msg_request_payload = create_transfer_update_msg(&x1, recipient_address, &coin, &transfer_signature, &backup_transactions)?; @@ -129,7 +273,7 @@ pub async fn execute( return Err(anyhow::anyhow!("Failed to update transfer message".to_string())); } - update_backup_txs(&client_config.pool, &coin.statechain_id.as_ref().unwrap(), &backup_transactions).await?; + update_backup_txs(&client_config.pool, &wallet.name, &coin.statechain_id.as_ref().unwrap(), &backup_transactions).await?; let date = Utc::now(); // This will get the current date and time in UTC let iso_string = date.to_rfc3339(); // Converts the date to an ISO 8601 string @@ -138,13 +282,12 @@ pub async fn execute( let activity = Activity { utxo, - amount: coin.amount.unwrap(), + amount: coin_amount, action: "Transfer".to_string(), date: iso_string }; wallet.activities.push(activity); - coin.status = CoinStatus::IN_TRANSFER; update_wallet(&client_config.pool, &wallet).await?; diff --git a/clients/libs/rust/src/withdraw.rs b/clients/libs/rust/src/withdraw.rs index 27fb2b67..37c9475e 100644 --- a/clients/libs/rust/src/withdraw.rs +++ b/clients/libs/rust/src/withdraw.rs @@ -15,7 +15,7 @@ pub async fn execute(client_config: &ClientConfig, wallet_name: &str, statechain return Err(anyhow!("Invalid address")); } - let backup_txs = get_backup_txs(&client_config.pool, &statechain_id).await?; + let backup_txs = get_backup_txs(&client_config.pool, &wallet.name, &statechain_id).await?; if backup_txs.len() == 0 { return Err(anyhow!("No backup transaction associated with this statechain ID were found")); diff --git a/clients/libs/web/broadcast_backup_tx.js b/clients/libs/web/broadcast_backup_tx.js index 657c4e14..f703557a 100644 --- a/clients/libs/web/broadcast_backup_tx.js +++ b/clients/libs/web/broadcast_backup_tx.js @@ -10,9 +10,9 @@ const execute = async (clientConfig, walletName, statechainId, toAddress, feeRat await initWasm(wasmUrl); - let wallet = storageManager.getItem(walletName); + let wallet = storageManager.getWallet(walletName); - let backupTxs = storageManager.getItem(statechainId); + let backupTxs = storageManager.getBackupTransactions(walletName, statechainId); if (!feeRate) { const response = await axios.get(`${clientConfig.esploraServer}/api/fee-estimates`); @@ -69,7 +69,7 @@ const execute = async (clientConfig, walletName, statechainId, toAddress, feeRat coin.withdrawal_address = toAddress; coin.status = CoinStatus.WITHDRAWING; - storageManager.setItem(walletName, wallet, true); + storageManager.updateWallet(wallet); utils.completeWithdraw(clientConfig, coin.statechain_id, coin.signed_statechain_id); diff --git a/clients/libs/web/coin_enum.js b/clients/libs/web/coin_enum.js index f4187a4e..195342a7 100644 --- a/clients/libs/web/coin_enum.js +++ b/clients/libs/web/coin_enum.js @@ -9,6 +9,7 @@ const CoinStatus = { TRANSFERRED: "TRANSFERRED", // the coin was transferred WITHDRAWN: "WITHDRAWN", // the coin was withdrawn DUPLICATED: "DUPLICATED", // the coin was duplicated + INVALIDATED: "INVALIDATED", // the coin was invalidated }; export default CoinStatus; \ No newline at end of file diff --git a/clients/libs/web/coin_status.js b/clients/libs/web/coin_status.js index 521080a3..731e5522 100644 --- a/clients/libs/web/coin_status.js +++ b/clients/libs/web/coin_status.js @@ -46,7 +46,16 @@ const checkDeposit = async (clientConfig, coin, walletNetwork) => { const utxo_txid = utxo.txid; const utxo_vout = utxo.vout; - const backup_tx = await deposit.createTx1(clientConfig, coin, walletNetwork, utxo_txid, utxo_vout); + if (coin.status !== CoinStatus.INITIALISED) { + throw new Error(`The coin with the aggregated address ${aggregated_address} is not in the INITIALISED state`); + } + + + coin.utxo_txid = utxo_txid; + coin.utxo_vout = utxo_vout; + coin.status = CoinStatus.IN_MEMPOOL; + + const backup_tx = await deposit.createTx1(clientConfig, coin, walletNetwork, 1); const activity_utxo = `${utxo_txid}:${utxo_vout}`; @@ -221,7 +230,7 @@ const updateCoins = async (clientConfig, walletName) => { await initWasm(wasmUrl); - let wallet = storageManager.getItem(walletName); + let wallet = storageManager.getWallet(walletName); const network = wallet.network; @@ -234,7 +243,7 @@ const updateCoins = async (clientConfig, walletName) => { if (depositResult) { wallet.activities.push(depositResult.activity); - storageManager.setItem(coin.statechain_id, [depositResult.backup_tx], false); + storageManager.createBackupTransactions(wallet.name, coin.statechain_id, [depositResult.backup_tx]) } } else if (coin.status === CoinStatus.IN_TRANSFER) { let is_transferred = await checkTransfer(clientConfig, coin); @@ -254,7 +263,23 @@ const updateCoins = async (clientConfig, walletName) => { const duplicatedCoins = await checkForDuplicated(clientConfig, wallet.coins); wallet.coins = [...wallet.coins, ...duplicatedCoins]; - storageManager.setItem(wallet.name, wallet, true); + // invalidate duplicated coins that were not transferred + for (let i = 0; i < wallet.coins.length; i++) { + if (wallet.coins[i].status === CoinStatus.DUPLICATED) { + const is_transferred = wallet.coins.some((coin, j) => + i !== j && // Skip comparing with self + coin.statechain_id === wallet.coins[i].statechain_id && + coin.locktime === wallet.coins[i].locktime && + coin.status === CoinStatus.TRANSFERRED + ); + + if (is_transferred) { + wallet.coins[i].status = CoinStatus.INVALIDATED; + } + } + } + + storageManager.updateWallet(wallet); } export default { updateCoins } \ No newline at end of file diff --git a/clients/libs/web/deposit.js b/clients/libs/web/deposit.js index 6a836008..7ef3801a 100644 --- a/clients/libs/web/deposit.js +++ b/clients/libs/web/deposit.js @@ -5,13 +5,12 @@ import wasmUrl from 'mercury-wasm/mercury_wasm_bg.wasm?url' import * as mercury_wasm from 'mercury-wasm'; import storageManager from './storage_manager.js'; import transaction from './transaction.js'; -import CoinStatus from './coin_enum.js'; import utils from './utils.js'; const getTokenFromServer = async (clientConfig) => { const statechain_entity_url = clientConfig.statechainEntity; - const path = "tokens/token_init"; + const path = "deposit/get_token"; const url = statechain_entity_url + '/' + path; const response = await axios.get(url); @@ -25,20 +24,8 @@ const getTokenFromServer = async (clientConfig) => { return token; } -const getToken = async (clientConfig, walletName) => { - - let wallet = storageManager.getItem(walletName); - - let token = await getTokenFromServer(clientConfig); - - // for dev purposes - token.confirmed = true; - - wallet.tokens.push(token); - - storageManager.setItem(walletName, wallet, true); - - return token; +const getToken = async (clientConfig) => { + return await getTokenFromServer(clientConfig); } const init = async (clientConfig, wallet, token_id) => { @@ -47,7 +34,7 @@ const init = async (clientConfig, wallet, token_id) => { wallet.coins.push(coin); - storageManager.setItem(wallet.name, wallet, true); + storageManager.updateWallet(wallet); let depositMsg1 = mercury_wasm.createDepositMsg1(coin, token_id); @@ -55,11 +42,29 @@ const init = async (clientConfig, wallet, token_id) => { const path = "deposit/init/pod"; const url = statechain_entity_url + '/' + path; - const response = await axios.post(url, depositMsg1); + let response = null; + try { + response = await axios.post(url, depositMsg1); + } catch (error) { + // Get error message from response body + let err_msg = ""; + if (error.response) { + // Server responded with error + console.log('Error body:', error.response.data); + err_msg = error.response.data; + } else if (error.request) { + // Request made but no response received + console.log('No response received:', error.request); + err_msg = error.request; + } else { + // Error setting up request + console.log('Error:', error.message); + err_msg = error.message; + } + + throw new Error(`Deposit error: ${err_msg}`); + } - if (response.status != 200) { - throw new Error(`Deposit error: ${response.data}`); - } let depositMsg1Response = response.data; @@ -69,22 +74,16 @@ const init = async (clientConfig, wallet, token_id) => { coin.signed_statechain_id = depositInitResult.signed_statechain_id; coin.server_pubkey = depositInitResult.server_pubkey; - storageManager.setItem(wallet.name, wallet, true); + storageManager.updateWallet(wallet); } -const getDepositBitcoinAddress = async (clientConfig, walletName, amount) => { +const getDepositBitcoinAddress = async (clientConfig, walletName, token_id, amount) => { await initWasm(wasmUrl); - let wallet = storageManager.getItem(walletName); + let wallet = storageManager.getWallet(walletName); - let foundToken = wallet.tokens.find(token => token.confirmed === true && token.spent === false); - - if (!foundToken) { - throw new Error(`There is no token available`); - } - - await init(clientConfig, wallet, foundToken.token_id); + await init(clientConfig, wallet, token_id); let coin = wallet.coins[wallet.coins.length - 1]; @@ -94,26 +93,12 @@ const getDepositBitcoinAddress = async (clientConfig, walletName, amount) => { coin.aggregated_address = aggregatedPublicKey.aggregate_address; coin.aggregated_pubkey = aggregatedPublicKey.aggregate_pubkey; - foundToken.spent = true; - - storageManager.setItem(wallet.name, wallet, true); + storageManager.updateWallet(wallet); return { "deposit_address": coin.aggregated_address, "statechain_id": coin.statechain_id }; } -const createTx1 = async (clientConfig, coin, walletNetwork, tx0Hash, tx0Vout) => { - - if (coin.status !== CoinStatus.INITIALISED) { - throw new Error(`The coin with the aggregated address ${aggregated_address} is not in the INITIALISED state`); - } - - if ('utxo_txid' in coin && 'input_vout' in coin) { - throw new Error(`The coin with the aggregated address ${aggregated_address} has already been deposited`); - } - - coin.utxo_txid = tx0Hash; - coin.utxo_vout = tx0Vout; - coin.status = CoinStatus.IN_MEMPOOL; +const createTx1 = async (clientConfig, coin, walletNetwork, txN) => { const toAddress = mercury_wasm.getUserBackupAddress(coin, walletNetwork); const isWithdrawal = false; @@ -137,7 +122,7 @@ const createTx1 = async (clientConfig, coin, walletNetwork, tx0Hash, tx0Vout) => ); let backupTx = { - tx_n: 1, + tx_n: txN, tx: signedTx, client_public_nonce: coin.public_nonce, server_public_nonce: coin.server_public_nonce, diff --git a/clients/libs/web/lightning-latch.js b/clients/libs/web/lightning-latch.js index f105890d..86b6e6c2 100644 --- a/clients/libs/web/lightning-latch.js +++ b/clients/libs/web/lightning-latch.js @@ -7,7 +7,7 @@ const createPreImage = async (clientConfig, walletName, statechainId) => { const batchId = uuidv4(); - const wallet = storageManager.getItem(walletName); + const wallet = storageManager.getWallet(walletName); const coinsWithStatechainId = wallet.coins.filter(c => { return c.statechain_id === statechainId @@ -55,7 +55,7 @@ const sendPaymentHash = async (clientConfig, paymentHashPayload) => { const confirmPendingInvoice = async (clientConfig, walletName, statechainId) => { - const wallet = storageManager.getItem(walletName); + const wallet = storageManager.getWallet(walletName); const coinsWithStatechainId = wallet.coins.filter(c => { return c.statechain_id === statechainId @@ -84,7 +84,7 @@ const confirmPendingInvoice = async (clientConfig, walletName, statechainId) => const retrievePreImage = async (clientConfig, walletName, statechainId, batchId) => { - const wallet = storageManager.getItem(walletName); + const wallet = storageManager.getWallet(walletName); const coinsWithStatechainId = wallet.coins.filter(c => { return c.statechain_id === statechainId diff --git a/clients/libs/web/main.js b/clients/libs/web/main.js index 8d3abec7..8ee31a72 100644 --- a/clients/libs/web/main.js +++ b/clients/libs/web/main.js @@ -10,6 +10,7 @@ import transfer_receive from './transfer_receive.js'; import lightningLatch from './lightning-latch.js'; import { v4 as uuidv4 } from 'uuid'; import { decodeInvoice } from '../../tests/web/test-utils.js'; +import utils from './utils.js'; const greet = async () => { @@ -26,18 +27,17 @@ const createWallet = async (clientConfig, name) => { const wallet = await walletManager.createWallet(clientConfig, name); - storageManager.setItem(name, wallet, false); + storageManager.createWallet(wallet); return wallet; } -const newToken = async (clientConfig, walletName) => { - const token = await deposit.getToken(clientConfig, walletName); - return token; +const newToken = async (clientConfig) => { + return await deposit.getToken(clientConfig); } -const getDepositBitcoinAddress = async (clientConfig, walletName, amount) => { - const address_info = await deposit.getDepositBitcoinAddress(clientConfig, walletName, amount); +const getDepositBitcoinAddress = async (clientConfig, walletName, token_id, amount) => { + const address_info = await deposit.getDepositBitcoinAddress(clientConfig, walletName, token_id, amount); return address_info; } @@ -45,13 +45,16 @@ const listStatecoins = async (clientConfig, walletName) => { await coin_status.updateCoins(clientConfig, walletName); - let wallet = storageManager.getItem(walletName); + let wallet = storageManager.getWallet(walletName); let coins = wallet.coins.map(coin => ({ statechain_id: coin.statechain_id, + utxo_txid: coin.utxo_txid, + utxo_vout: coin.utxo_vout, amount: coin.amount, status: coin.status, - adress: coin.address, + address: coin.address, + aggregated_address: coin.aggregated_address, locktime: coin.locktime, duplicate_index: coin.duplicate_index })); @@ -88,11 +91,11 @@ const newTransferAddress = async (walletName, generateBatchId) => { return res; } -const transferSend = async (clientConfig, walletName, statechainId, toAddress, forceSend, batchId) => { +const transferSend = async (clientConfig, walletName, statechainId, toAddress, forceSend, batchId, duplicatedIndexes) => { await coin_status.updateCoins(clientConfig, walletName); - return await transfer_send.execute(clientConfig, walletName, statechainId, toAddress, forceSend, batchId); + return await transfer_send.execute(clientConfig, walletName, statechainId, toAddress, forceSend, batchId, duplicatedIndexes); } const transferReceive = async (clientConfig, walletName) => { @@ -153,5 +156,9 @@ export default { confirmPendingInvoice, retrievePreImage, getPaymentHash, - verifyInvoice -} + verifyInvoice, + getPreviousOutpoint: utils.getPreviousOutpoint, + getBlockheight: utils.getBlockheight, + splitBackupTransactions: transfer_receive.splitBackupTransactions, + infoConfig: utils.infoConfig, +}; diff --git a/clients/libs/web/storage_manager.js b/clients/libs/web/storage_manager.js index 21502f48..3a95fd24 100644 --- a/clients/libs/web/storage_manager.js +++ b/clients/libs/web/storage_manager.js @@ -1,5 +1,32 @@ const namespace = 'mercury-layer' +const createWallet = (wallet) => { + setItem(wallet.name, wallet); +} + +const updateWallet = (wallet) => { + setItem(wallet.name, wallet, true); +} + +const getWallet = (walletName) => { + return getItem(walletName); +} + +const createBackupTransactions = (walletName, statechainId, backupTransactions) => { + const key = `${walletName}-${statechainId}`; + setItem(key, backupTransactions); +} + +const updateBackupTransactions = (walletName, statechainId, backupTransactions) => { + const key = `${walletName}-${statechainId}`; + setItem(key, backupTransactions, true); +} + +const getBackupTransactions = (walletName, statechainId) => { + const key = `${walletName}-${statechainId}`; + return getItem(key); +} + const setItem = (key, jsonData, isUpdate = false) => { const namespacedKey = `${namespace}:${key}`; @@ -21,4 +48,4 @@ const getItem = (key) => { return JSON.parse(jsonData); } -export default { setItem, getItem } +export default { createWallet, updateWallet, getWallet, createBackupTransactions, updateBackupTransactions, getBackupTransactions }; diff --git a/clients/libs/web/transfer_receive.js b/clients/libs/web/transfer_receive.js index d9ba11bf..1771c48c 100644 --- a/clients/libs/web/transfer_receive.js +++ b/clients/libs/web/transfer_receive.js @@ -10,13 +10,13 @@ const newTransferAddress = async (walletName) => { await initWasm(wasmUrl); - let wallet = storageManager.getItem(walletName); + let wallet = storageManager.getWallet(walletName); let coin = mercury_wasm.getNewCoin(wallet); wallet.coins.push(coin); - storageManager.setItem(walletName, wallet, true); + storageManager.updateWallet(wallet); return coin.address; } @@ -32,6 +32,60 @@ const getMsgAddr = async (clientConfig, auth_pubkey) => { return response.data.list_enc_transfer_msg; } +function splitBackupTransactions(backupTransactions) { + // Map to store grouped transactions + const groupedTxs = new Map(); + + // Array to keep track of order of appearance of outpoints + const orderOfAppearance = []; + // Set to track which outpoints we've seen + const seenOutpoints = new Set(); + + // Process each transaction + for (const tx of backupTransactions) { + try { + // Get the outpoint for this transaction + const outpoint = mercury_wasm.getPreviousOutpoint(tx); + + // Create a key string from txid and vout (since Map keys need to be strings/primitives) + const key = `${outpoint.txid}:${outpoint.vout}`; + + // If we haven't seen this outpoint before, record its order + if (!seenOutpoints.has(key)) { + seenOutpoints.add(key); + orderOfAppearance.push(key); + } + + // Add the transaction to its group + if (!groupedTxs.has(key)) { + groupedTxs.set(key, []); + } + // Create a deep copy of the transaction before adding it + // groupedTxs.get(key).push(JSON.parse(JSON.stringify(tx))); + groupedTxs.get(key).push(tx); + + } catch (error) { + console.error('Error processing transaction:', error); + throw error; + } + } + + // Create result array + const result = []; + + // Add arrays to result in order of first appearance + for (const key of orderOfAppearance) { + const transactions = groupedTxs.get(key); + if (transactions) { + // Sort each group by tx_n + transactions.sort((a, b) => a.tx_n - b.tx_n); + result.push(transactions); + } + } + + return result; +} + const getTx0 = async (clientConfig, tx0_txid) => { let response = await axios.get(`${clientConfig.esploraServer}/api/tx/${tx0_txid}/hex`); @@ -99,10 +153,6 @@ const unlockStatecoin = async (clientConfig, statechainId, signedStatechainId, a } } -const sleep = (ms) => { - return new Promise(resolve => setTimeout(resolve, ms)); -} - const sendTransferReceiverRequestPayload = async (clientConfig, transferReceiverRequestPayload) => { try { @@ -131,129 +181,237 @@ const sendTransferReceiverRequestPayload = async (clientConfig, transferReceiver } -const processEncryptedMessage = async (clientConfig, coin, encMessage, network, serverInfo, blockheight, activities) => { +const validateEncryptedMessage = async (clientConfig, coin, encMessage, network, serverInfo, blockheight) => { let clientAuthKey = coin.auth_privkey; let newUserPubkey = coin.user_pubkey; let transferMsg = mercury_wasm.decryptTransferMsg(encMessage, clientAuthKey); - let tx0Outpoint = mercury_wasm.getTx0Outpoint(transferMsg.backup_transactions); + let groupedBackupTransactions = splitBackupTransactions(transferMsg.backup_transactions); - const tx0Hex = await getTx0(clientConfig, tx0Outpoint.txid); + for (const [index, backupTransactions] of groupedBackupTransactions.entries()) { - const isTransferSignatureValid = mercury_wasm.verifyTransferSignature(newUserPubkey, tx0Outpoint, transferMsg); + let tx0Outpoint = mercury_wasm.getTx0Outpoint(backupTransactions); - if (!isTransferSignatureValid) { - throw new Error("Invalid transfer signature"); - } - - const statechainInfo = await utils.getStatechainInfo(clientConfig, transferMsg.statechain_id); + const tx0Hex = await getTx0(clientConfig, tx0Outpoint.txid); - if (statechainInfo == null) { - throw new Error("Statechain info not found"); - } + if (index == 0) { + const isTransferSignatureValid = mercury_wasm.verifyTransferSignature(newUserPubkey, tx0Outpoint, transferMsg); - const isTx0OutputPubkeyValid = mercury_wasm.validateTx0OutputPubkey(statechainInfo.enclave_public_key, transferMsg, tx0Outpoint, tx0Hex, network); + if (!isTransferSignatureValid) { + throw new Error("Invalid transfer signature"); + } + } + + const statechainInfo = await utils.getStatechainInfo(clientConfig, transferMsg.statechain_id); - if (!isTx0OutputPubkeyValid) { - throw new Error("Invalid tx0 output pubkey"); - } + if (statechainInfo == null) { + throw new Error("Statechain info not found"); + } - let latestBackupTxPaysToUserPubkey = mercury_wasm.verifyLatestBackupTxPaysToUserPubkey(transferMsg, newUserPubkey, network); + const isTx0OutputPubkeyValid = mercury_wasm.validateTx0OutputPubkey(statechainInfo.enclave_public_key, transferMsg, tx0Outpoint, tx0Hex, network); - if (!latestBackupTxPaysToUserPubkey) { - throw new Error("Latest Backup Tx does not pay to the expected public key"); - } + if (!isTx0OutputPubkeyValid) { + throw new Error("Invalid tx0 output pubkey"); + } - if (statechainInfo.num_sigs != transferMsg.backup_transactions.length) { - throw new Error("num_sigs is not correct"); - } - - let isTx0OutputUnspent = await verifyTx0OutputIsUnspentAndConfirmed(clientConfig, tx0Outpoint, tx0Hex, network); - if (!isTx0OutputUnspent.result) { - throw new Error("tx0 output is spent or not confirmed"); - } + let latestBackupTxPaysToUserPubkey = mercury_wasm.verifyLatestBackupTxPaysToUserPubkey(transferMsg, newUserPubkey, network); - const currentFeeRateSatsPerByte = (serverInfo.feeRateSatsPerByte > clientConfig.maxFeeRate) ? clientConfig.maxFeeRate: serverInfo.feeRateSatsPerByte; + if (!latestBackupTxPaysToUserPubkey) { + throw new Error("Latest Backup Tx does not pay to the expected public key"); + } - const feeRateTolerance = clientConfig.feeRateTolerance; + if (statechainInfo.num_sigs != transferMsg.backup_transactions.length) { + throw new Error("num_sigs is not correct"); + } + + let isTx0OutputUnspent = await verifyTx0OutputIsUnspentAndConfirmed(clientConfig, tx0Outpoint, tx0Hex, network); + if (!isTx0OutputUnspent.result) { + throw new Error("tx0 output is spent or not confirmed"); + } + + const currentFeeRateSatsPerByte = (serverInfo.feeRateSatsPerByte > clientConfig.maxFeeRate) ? clientConfig.maxFeeRate: serverInfo.feeRateSatsPerByte; - let isSignatureValid = mercury_wasm.validateSignatureScheme( - transferMsg, - statechainInfo, - tx0Hex, - blockheight, - feeRateTolerance, - currentFeeRateSatsPerByte, - serverInfo.initlock, - serverInfo.interval - ) + const feeRateTolerance = clientConfig.feeRateTolerance; - if (!isSignatureValid.result) { - throw new Error(`Invalid signature scheme, ${isSignatureValid.msg}`); + let isSignatureValid = mercury_wasm.validateSignatureScheme( + backupTransactions, + statechainInfo, + tx0Hex, + blockheight, + feeRateTolerance, + currentFeeRateSatsPerByte, + serverInfo.initlock, + serverInfo.interval + ) + + if (!isSignatureValid.result) { + throw new Error(`Invalid signature scheme, ${isSignatureValid.msg}`); + } } +} - let previousLockTime = isSignatureValid.previousLockTime; +const processEncryptedMessage = async (clientConfig, coin, encMessage, network, walletName, activities) => { - const transferReceiverRequestPayload = mercury_wasm.createTransferReceiverRequestPayload(statechainInfo, transferMsg, coin); + let transferReceiveResult = { + isBatchLocked: false, + statechainId: null, + duplicatedCoins: [], + }; - let signedStatechainIdForUnlock = mercury_wasm.signMessage(transferMsg.statechain_id, coin); + let clientAuthKey = coin.auth_privkey; - await unlockStatecoin(clientConfig, transferMsg.statechain_id, signedStatechainIdForUnlock, coin.auth_pubkey); + let transferMsg = mercury_wasm.decryptTransferMsg(encMessage, clientAuthKey); - let serverPublicKeyHex = ""; + let groupedBackupTransactions = splitBackupTransactions(transferMsg.backup_transactions); - try { - const transferReceiverResult = await sendTransferReceiverRequestPayload(clientConfig, transferReceiverRequestPayload); + for (const [index, backupTransactions] of groupedBackupTransactions.entries()) { + + if (index == 0) { + + let tx0Outpoint = mercury_wasm.getTx0Outpoint(backupTransactions); + const tx0Hex = await getTx0(clientConfig, tx0Outpoint.txid); + + const statechainInfo = await utils.getStatechainInfo(clientConfig, transferMsg.statechain_id); + if (statechainInfo == null) { + throw new Error("Statechain info not found"); + } + + let isTx0OutputUnspent = await verifyTx0OutputIsUnspentAndConfirmed(clientConfig, tx0Outpoint, tx0Hex, network); + if (!isTx0OutputUnspent.result) { + throw new Error("tx0 output is spent or not confirmed"); + } + + const backupTx = backupTransactions.at(-1); + + let lastTxLockTime = mercury_wasm.getBlockheight(backupTx); - if (transferReceiverResult.isBatchLocked) { - return { - isBatchLocked: true, - statechainId: null, + const transferReceiverRequestPayload = mercury_wasm.createTransferReceiverRequestPayload(statechainInfo, transferMsg, coin); + + let signedStatechainIdForUnlock = mercury_wasm.signMessage(transferMsg.statechain_id, coin); + + await unlockStatecoin(clientConfig, transferMsg.statechain_id, signedStatechainIdForUnlock, coin.auth_pubkey); + + let serverPublicKeyHex = ""; + + try { + const transferReceiverResult = await sendTransferReceiverRequestPayload(clientConfig, transferReceiverRequestPayload); + + if (transferReceiverResult.isBatchLocked) { + return { + isBatchLocked: true, + statechainId: null, + duplicatedCoins: [], + }; + } + + serverPublicKeyHex = transferReceiverResult.serverPubkey; + } catch (error) { + throw new Error(error); + } + + let newKeyInfo = mercury_wasm.getNewKeyInfo(serverPublicKeyHex, coin, transferMsg.statechain_id, tx0Outpoint, tx0Hex, network); + + coin.server_pubkey = serverPublicKeyHex; + coin.aggregated_pubkey = newKeyInfo.aggregate_pubkey; + coin.aggregated_address = newKeyInfo.aggregate_address; + coin.statechain_id = transferMsg.statechain_id; + coin.signed_statechain_id = newKeyInfo.signed_statechain_id; + coin.amount = newKeyInfo.amount; + coin.utxo_txid = tx0Outpoint.txid; + coin.utxo_vout = tx0Outpoint.vout; + coin.locktime = lastTxLockTime; + coin.status = isTx0OutputUnspent.status; + + let utxo = `${tx0Outpoint.txid}:${tx0Outpoint.vout}`; + + let activity = { + utxo: utxo, + amount: newKeyInfo.amount, + action: "Receive", + date: new Date().toISOString() }; - } - serverPublicKeyHex = transferReceiverResult.serverPubkey; - } catch (error) { - throw new Error(error); + activities.push(activity); + + storageManager.updateBackupTransactions(walletName, transferMsg.statechain_id, transferMsg.backup_transactions); + + transferReceiveResult.isBatchLocked = false; + transferReceiveResult.statechainId = transferMsg.statechain_id; + } else { + let tx0Outpoint = mercury_wasm.getTx0Outpoint(backupTransactions); + const tx0Hex = await getTx0(clientConfig, tx0Outpoint.txid); + + const firstBackupTx = backupTransactions.at(0); + + let txOutpoint = mercury_wasm.getPreviousOutpoint(firstBackupTx); + + let amount = mercury_wasm.getAmountFromTx0(tx0Hex, txOutpoint); + + transferReceiveResult.duplicatedCoins.push({ + txid: txOutpoint.txid, + vout: txOutpoint.vout, + amount, + index, + }); + } } - let newKeyInfo = mercury_wasm.getNewKeyInfo(serverPublicKeyHex, coin, transferMsg.statechain_id, tx0Outpoint, tx0Hex, network); - - coin.server_pubkey = serverPublicKeyHex; - coin.aggregated_pubkey = newKeyInfo.aggregate_pubkey; - coin.aggregated_address = newKeyInfo.aggregate_address; - coin.statechain_id = transferMsg.statechain_id; - coin.signed_statechain_id = newKeyInfo.signed_statechain_id; - coin.amount = newKeyInfo.amount; - coin.utxo_txid = tx0Outpoint.txid; - coin.utxo_vout = tx0Outpoint.vout; - coin.locktime = previousLockTime; - coin.status = isTx0OutputUnspent.status; - - let utxo = `${tx0Outpoint.txid}:${tx0Outpoint.vout}`; - - let activity = { - utxo: utxo, - amount: newKeyInfo.amount, - action: "Receive", - date: new Date().toISOString() - }; + return transferReceiveResult; +} - activities.push(activity); +function sortCoinsByStatechain(coins) { + // Create a map to store the position of first occurrence of each statechain_id + const firstPositions = new Map(); - storageManager.setItem(transferMsg.statechain_id, transferMsg.backup_transactions, true); + // Record the position of the first occurrence of each statechain_id + coins.forEach((coin, idx) => { + if (coin.statechain_id) { + if (!firstPositions.has(coin.statechain_id)) { + firstPositions.set(coin.statechain_id, idx); + } + } + }); - return { - isBatchLocked: false, - statechainId: transferMsg.statechain_id, - }; + // Sort the array maintaining original order of different statechain_ids + coins.sort((a, b) => { + // Helper function to compare duplicate_index values + const compareDuplicateIndex = (x, y) => { + // Handle undefined cases with a default value of 0 + const indexA = x.duplicate_index ?? 0; + const indexB = y.duplicate_index ?? 0; + return indexA - indexB; + }; + + // Handle cases where statechain_id might be null/undefined + if (!a.statechain_id && !b.statechain_id) { + return compareDuplicateIndex(a, b); + } + if (!a.statechain_id) { + return 1; // equivalent to Ordering::Greater + } + if (!b.statechain_id) { + return -1; // equivalent to Ordering::Less + } + + // Both have statechain_id + if (a.statechain_id === b.statechain_id) { + // Same statechain_id: sort by duplicate_index + return compareDuplicateIndex(a, b); + } else { + // Different statechain_ids: compare their first positions + const posA = firstPositions.get(a.statechain_id); + const posB = firstPositions.get(b.statechain_id); + return posA - posB; + } + }); } + const execute = async (clientConfig, walletName) => { await initWasm(wasmUrl); - let wallet = storageManager.getItem(walletName); + let wallet = storageManager.getWallet(walletName); const serverInfo = await utils.infoConfig(clientConfig); @@ -284,6 +442,7 @@ const execute = async (clientConfig, walletName) => { let tempCoins = [...wallet.coins]; let tempActivities = [...wallet.activities]; + let duplicatedCoins = []; const response = await axios.get(`${clientConfig.esploraServer}/api/blocks/tip/height`); const blockheight = response.data; @@ -296,8 +455,10 @@ const execute = async (clientConfig, walletName) => { if (coin) { try { - let messageResult = await processEncryptedMessage(clientConfig, coin, encMessage, wallet.network, serverInfo, blockheight, tempActivities); + await validateEncryptedMessage(clientConfig, coin, encMessage, wallet.network, serverInfo, blockheight); + + let messageResult = await processEncryptedMessage(clientConfig, coin, encMessage, wallet.network, wallet.name, tempActivities); if (messageResult.isBatchLocked) { isThereBatchLocked = true; } @@ -305,6 +466,29 @@ const execute = async (clientConfig, walletName) => { if (messageResult.statechainId) { receivedStatechainIds.push(messageResult.statechainId); } + + if (messageResult.duplicatedCoins.length > 0 && messageResult.isBatchLocked) { + throw new Error("Batch is locked and there are duplicated coins"); + } + + for (const duplicatedCoinData of messageResult.duplicatedCoins) { + + if (duplicatedCoinData.index == 0) { + throw new Error("Duplicated coin with index 0"); + } + + const duplicatedCoin = { + ...coin, + status: CoinStatus.DUPLICATED, + utxo_txid: duplicatedCoinData.txid, + utxo_vout: duplicatedCoinData.vout, + amount: duplicatedCoinData.amount, + duplicate_index: duplicatedCoinData.index + }; + + duplicatedCoins.push(duplicatedCoin); + } + } catch (error) { console.error(`Error: ${error}`); continue; @@ -315,8 +499,10 @@ const execute = async (clientConfig, walletName) => { let newCoin = await mercury_wasm.duplicateCoinToInitializedState(wallet, authPubkey); if (newCoin) { - let messageResult = await processEncryptedMessage(clientConfig, newCoin, encMessage, wallet.network, serverInfo, blockheight, tempActivities); + await validateEncryptedMessage(clientConfig, newCoin, encMessage, wallet.network, serverInfo, blockheight); + + let messageResult = await processEncryptedMessage(clientConfig, newCoin, encMessage, wallet.network, wallet.name, tempActivities); if (messageResult.isBatchLocked) { isThereBatchLocked = true; } @@ -325,6 +511,28 @@ const execute = async (clientConfig, walletName) => { tempCoins.push(newCoin); receivedStatechainIds.push(messageResult.statechainId); } + + if (messageResult.duplicatedCoins.length > 0 && messageResult.isBatchLocked) { + throw new Error("Batch is locked and there are duplicated coins"); + } + + for (const duplicatedCoinData of messageResult.duplicatedCoins) { + + if (duplicatedCoinData.index == 0) { + throw new Error("Duplicated coin with index 0"); + } + + const duplicatedCoin = { + ...newCoin, + status: CoinStatus.DUPLICATED, + utxo_txid: duplicatedCoinData.txid, + utxo_vout: duplicatedCoinData.vout, + amount: duplicatedCoinData.amount, + duplicate_index: duplicatedCoinData.index + }; + + duplicatedCoins.push(duplicatedCoin); + } } } catch (error) { console.error(`Error: ${error}`); @@ -334,10 +542,12 @@ const execute = async (clientConfig, walletName) => { } } + tempCoins.push(...duplicatedCoins); + sortCoinsByStatechain(tempCoins); wallet.coins = [...tempCoins]; wallet.activities = [...tempActivities]; - storageManager.setItem(walletName, wallet, true); + storageManager.updateWallet(wallet); return { isThereBatchLocked, @@ -345,4 +555,4 @@ const execute = async (clientConfig, walletName) => { }; } -export default { newTransferAddress, execute } \ No newline at end of file +export default { newTransferAddress, execute, splitBackupTransactions } \ No newline at end of file diff --git a/clients/libs/web/transfer_send.js b/clients/libs/web/transfer_send.js index 15b91bb4..71007025 100644 --- a/clients/libs/web/transfer_send.js +++ b/clients/libs/web/transfer_send.js @@ -5,27 +5,252 @@ import * as mercury_wasm from 'mercury-wasm'; import storageManager from './storage_manager.js'; import utils from './utils.js'; import CoinStatus from './coin_enum.js'; +import deposit from './deposit.js'; import transaction from './transaction.js'; -const execute = async (clientConfig, walletName, statechainId, toAddress, forceSend, batchId) => { +async function createBackupTransactions( + clientConfig, + recipientAddress, + wallet, + statechainId, + duplicatedIndexes +) { + + // Validate duplicated indexes + if (duplicatedIndexes) { + for (const index of duplicatedIndexes) { + if (index >= wallet.coins.length) { + throw new Error(`Index ${index} does not exist in wallet.coins`); + } + } + } - await initWasm(wasmUrl); + let coinList = []; + + const backupTransactions = storageManager.getBackupTransactions(wallet.name, statechainId); + + // Get coins that already have a backup transaction + for (const coin of wallet.coins) { + // Check if coin matches any backup transaction and has one of the specified statuses + const hasMatchingTx = backupTransactions.some(backupTx => { + try { + const txOutpoint = mercury_wasm.getPreviousOutpoint(backupTx); + + if (coin.utxo_txid && coin.utxo_vout !== undefined) { + return ( + (coin.status === CoinStatus.DUPLICATED || + coin.status === CoinStatus.CONFIRMED || + coin.status === CoinStatus.IN_TRANSFER) && + txOutpoint.txid === coin.utxo_txid && + txOutpoint.vout === coin.utxo_vout + ); + } + return false; + } catch (error) { + console.log(error); + return false; + } + }); + + let coinToAdd = false; + + if (duplicatedIndexes) { + if (coin.statechain_id === statechainId && + (coin.status === CoinStatus.CONFIRMED || coin.status === CoinStatus.IN_TRANSFER)) { + coinToAdd = true; + } + + if (coin.statechain_id === statechainId && + coin.status === CoinStatus.DUPLICATED && + duplicatedIndexes.includes(coin.duplicate_index)) { + coinToAdd = true; + } + } + + if (hasMatchingTx || coinToAdd) { + if (!coin.locktime) { + throw new Error("coin.locktime is undefined or null"); + } + + const response = await axios.get(`${clientConfig.esploraServer}/api/blocks/tip/height`); + const block_header = response.data; + const currentBlockheight = parseInt(block_header, 10); + + if (isNaN(currentBlockheight)) { + throw new Error(`Invalid block height: ${block_header}`); + } + + if (currentBlockheight > coin.locktime) { + throw new Error( + `The coin is expired. Coin locktime is ${coin.locktime} and current blockheight is ${currentBlockheight}` + ); + } + + coinList.push(coin); + } + } + + // Validate coins with zero index + const coinsWithZeroIndex = coinList.filter( + coin => coin.duplicate_index === 0 && + (coin.status === CoinStatus.CONFIRMED || coin.status === CoinStatus.IN_TRANSFER) + ); + + if (coinsWithZeroIndex.length !== 1) { + throw new Error("There must be at least one coin with duplicate_index == 0"); + } + + for (let coin of coinList) { + if (coin.status === 'DUPLICATED') { + try { + let response = await axios.get(`${clientConfig.esploraServer}/api/address/${coin.aggregated_address}/utxo`); + let utxo_list = response.data; + + let utxo = null; + + for (let unspent of utxo_list) { + if (coin.utxo_txid === unspent.txid && coin.utxo_vout === unspent.vout) { + utxo = unspent; + break; + } + } + + if (utxo) { + + let isConfirmed = false; + + if (utxo.status.confirmed) { + const response = await axios.get(`${clientConfig.esploraServer}/api/blocks/tip/height`); + const block_header = response.data; + const blockheight = parseInt(block_header, 10); + + if (isNaN(blockheight)) { + throw new Error(`Invalid block height: ${block_header}`); + } + + const confirmations = blockheight - parseInt(utxo.status.block_height, 10) + 1; + + const confirmationTarget = clientConfig.confirmationTarget; + + isConfirmed = confirmations >= confirmationTarget; + } - let wallet = storageManager.getItem(walletName); + if (!isConfirmed) { + throw new Error(`The coin with duplicated index ${coin.duplicate_index} has not yet been confirmed. This transfer cannot be performed.`); + } + + } + } catch (error) { + throw error; + } + } + } + + // Move the coin with CONFIRMED status to the first position + coinList.sort((a, b) => { + if (a.status === CoinStatus.CONFIRMED) return -1; + if (b.status === CoinStatus.CONFIRMED) return 1; + return 0; + }); + + const newBackupTransactions = []; + + // Create backup transaction for every coin + const backupTxs = storageManager.getBackupTransactions(wallet.name, statechainId); + let newTxN = backupTxs.length; + + for (const coin of coinList) { + let filteredTransactions = []; + + for (const backupTx of backupTxs) { + try { + const txOutpoint = mercury_wasm.getPreviousOutpoint(backupTx); + if (coin.utxo_txid && coin.utxo_vout !== undefined) { + if (txOutpoint.txid === coin.utxo_txid && txOutpoint.vout === coin.utxo_vout) { + filteredTransactions.push({ ...backupTx }); + } + } + } catch { + continue; + } + } + + filteredTransactions.sort((a, b) => a.tx_n - b.tx_n); + + if (filteredTransactions.length === 0) { + newTxN++; + const bkpTx1 = await deposit.createTx1(clientConfig, coin, wallet.network, newTxN); + filteredTransactions.push(bkpTx1); + } + + const qtBackupTx = filteredTransactions.length; + newTxN++; - let backupTxs = storageManager.getItem(statechainId); + const bkpTx1 = filteredTransactions[0]; - if (backupTxs.length === 0) { - throw new Error(`There is no backup transaction for the statechain id ${statechainId}`); + const block_height = mercury_wasm.getBlockheight(bkpTx1); + + const serverInfo = await utils.infoConfig(clientConfig); + + let feeRateSatsPerByte = (serverInfo.feeRateSatsPerByte > clientConfig.maxFeeRate) ? clientConfig.maxFeeRate: serverInfo.feeRateSatsPerByte; + + const isWithdrawal = false; + + const signedTx = await transaction.newTransaction( + clientConfig, + coin, + recipientAddress, + isWithdrawal, + qtBackupTx, + block_height, + wallet.network, + feeRateSatsPerByte, + serverInfo.initlock, + serverInfo.interval + ); + + const backupTx = { + tx_n: newTxN, + tx: signedTx, + client_public_nonce: coin.public_nonce, + server_public_nonce: coin.server_public_nonce, + client_public_key: coin.user_pubkey, + server_public_key: coin.server_pubkey, + blinding_factor: coin.blinding_factor + }; + + filteredTransactions.push(backupTx); + + if (coin.duplicate_index === 0) { + newBackupTransactions.unshift(...filteredTransactions); + } else { + newBackupTransactions.push(...filteredTransactions); + } + + coin.status = CoinStatus.IN_TRANSFER; } - const new_tx_n = backupTxs.length + 1; + newBackupTransactions.sort((a, b) => a.txN - b.txN); + + return newBackupTransactions; +} + +const execute = async (clientConfig, walletName, statechainId, toAddress, forceSend, batchId, duplicatedIndexes) => { + + await initWasm(wasmUrl); + + let wallet = storageManager.getWallet(walletName); const isCoinDuplicated = wallet.coins.some(c => c.statechain_id === statechainId && c.status === CoinStatus.DUPLICATED ); + if (isCoinDuplicated && !forceSend) { + throw new Error("Coin is duplicated. If you want to proceed, use the command '--force, -f' option. " + + "You will no longer be able to move other duplicate coins with the same statechain_id and this will cause PERMANENT LOSS of these duplicate coin funds."); + } + const areThereDuplicateCoinsWithdrawn = wallet.coins.some(c => c.statechain_id === statechainId && (c.status === CoinStatus.WITHDRAWING || c.status === CoinStatus.WITHDRAWN) && @@ -40,11 +265,13 @@ const execute = async (clientConfig, walletName, statechainId, toAddress, forceS } let coinsWithStatechainId = wallet.coins.filter(c => { - return c.statechain_id === statechainId && c.status != CoinStatus.DUPLICATED + return c.statechain_id === statechainId && + (c.status == CoinStatus.CONFIRMED || c.status == CoinStatus.IN_TRANSFER) && + c.duplicate_index === 0; }); if (!coinsWithStatechainId || coinsWithStatechainId.length === 0) { - throw new Error(`There is no coin for the statechain id ${statechainId}`); + throw new Error(`No coins with status CONFIRMED or IN_TRANSFER associated with this statechain ID ${statechainId} were found.`); } // If the user sends to himself, he will have two coins with same statechain_id @@ -52,103 +279,43 @@ const execute = async (clientConfig, walletName, statechainId, toAddress, forceS // Sort the coins by locktime in ascending order and pick the first one let coin = coinsWithStatechainId.sort((a, b) => a.locktime - b.locktime)[0]; - if (coin.status != CoinStatus.CONFIRMED && coin.status != CoinStatus.IN_TRANSFER) { - throw new Error(`Coin status must be CONFIRMED or IN_TRANSFER to transfer it. The current status is ${coin.status}`); - } - - if (isCoinDuplicated && !forceSend) { - throw new Error("Coin is duplicated. If you want to proceed, use the command '--force, -f' option. " + - "You will no longer be able to move other duplicate coins with the same statechain_id and this will cause PERMANENT LOSS of these duplicate coin funds."); - } - - if (coin.locktime == null) { - throw new Error("Coin.locktime is null"); - } - - let response = await axios.get(`${clientConfig.esploraServer}/api/blocks/tip/height`); - const block_header = response.data; - const currentBlockheight = parseInt(block_header, 10); - - if (isNaN(currentBlockheight)) { - throw new Error(`Invalid block height: ${block_header}`); - } - - if (currentBlockheight > coin.locktime) { - throw new Error(`The coin is expired. Coin locktime is ${coin.locktime} and current blockheight is ${currentBlockheight}`); - } - const statechain_id = coin.statechain_id; const signed_statechain_id = coin.signed_statechain_id; - const isWithdrawal = false; - const qtBackupTx = backupTxs.length; - - backupTxs.sort((a, b) => a.tx_n - b.tx_n); - - const bkp_tx1 = backupTxs[0]; - - const block_height = mercury_wasm.getBlockheight(bkp_tx1); - const decodedTransferAddress = mercury_wasm.decodeTransferAddress(toAddress); const new_auth_pubkey = decodedTransferAddress.auth_pubkey; const new_x1 = await get_new_x1(clientConfig, statechain_id, signed_statechain_id, new_auth_pubkey, batchId); - const serverInfo = await utils.infoConfig(clientConfig); - - let feeRateSatsPerByte = (serverInfo.feeRateSatsPerByte > clientConfig.maxFeeRate) ? clientConfig.maxFeeRate: serverInfo.feeRateSatsPerByte; - - const signed_tx = await transaction.newTransaction( - clientConfig, - coin, - toAddress, - isWithdrawal, - qtBackupTx, - block_height, - wallet.network, - feeRateSatsPerByte, - serverInfo.initlock, - serverInfo.interval - ); - - const backup_tx = { - tx_n: new_tx_n, - tx: signed_tx, - client_public_nonce: coin.public_nonce, - server_public_nonce: coin.server_public_nonce, - client_public_key: coin.user_pubkey, - server_public_key: coin.server_pubkey, - blinding_factor: coin.blinding_factor - }; - - backupTxs.push(backup_tx); - const input_txid = coin.utxo_txid; const input_vout = coin.utxo_vout; const client_seckey = coin.user_privkey; + const coin_amount = coin.amount; const recipient_address = toAddress; const transfer_signature = mercury_wasm.createTransferSignature(recipient_address, input_txid, input_vout, client_seckey); + const backupTxs = await createBackupTransactions(clientConfig, recipient_address, wallet, statechain_id, duplicatedIndexes); + const transferUpdateMsgRequestPayload = mercury_wasm.createTransferUpdateMsg(new_x1, recipient_address, coin, transfer_signature, backupTxs); const statechain_entity_url = clientConfig.statechainEntity; const path = "transfer/update_msg"; const url = statechain_entity_url + '/' + path; - response = await axios.post(url, transferUpdateMsgRequestPayload); + let response = await axios.post(url, transferUpdateMsgRequestPayload); if (!response.data.updated) { throw new Error(`Transfer update failed`); } - storageManager.setItem(coin.statechain_id, backupTxs, true); + storageManager.updateBackupTransactions(walletName, coin.statechain_id, backupTxs); let utxo = `${coin.utxo_txid}:${coin.input_vout}`; let activity = { utxo: utxo, - amount: coin.amount, + amount: coin_amount, action: "Transfer", date: new Date().toISOString() }; @@ -156,7 +323,7 @@ const execute = async (clientConfig, walletName, statechainId, toAddress, forceS wallet.activities.push(activity); coin.status = CoinStatus.IN_TRANSFER; - storageManager.setItem(wallet.name, wallet, true); + storageManager.updateWallet(wallet); return coin; }; diff --git a/clients/libs/web/utils.js b/clients/libs/web/utils.js index 01e704ed..4a5ffa22 100644 --- a/clients/libs/web/utils.js +++ b/clients/libs/web/utils.js @@ -1,4 +1,7 @@ import axios from 'axios'; +import initWasm from 'mercury-wasm'; +import wasmUrl from 'mercury-wasm/mercury_wasm_bg.wasm?url' +import * as mercury_wasm from 'mercury-wasm'; const infoConfig = async (clientConfig) => { @@ -67,4 +70,14 @@ const getStatechainInfo = async (clientConfig, statechainId) => { } } -export default { infoConfig, createActivity, completeWithdraw, getStatechainInfo } \ No newline at end of file +async function getPreviousOutpoint(backupTx) { + await initWasm(wasmUrl); + return mercury_wasm.getPreviousOutpoint(backupTx); +} + +async function getBlockheight(backupTx) { + await initWasm(wasmUrl); + return mercury_wasm.getBlockheight(backupTx); +} + +export default { infoConfig, createActivity, completeWithdraw, getStatechainInfo, getPreviousOutpoint, getBlockheight }; \ No newline at end of file diff --git a/clients/libs/web/withdraw.js b/clients/libs/web/withdraw.js index b85c884b..860c20a0 100644 --- a/clients/libs/web/withdraw.js +++ b/clients/libs/web/withdraw.js @@ -6,9 +6,9 @@ import transaction from './transaction.js'; const execute = async (clientConfig, walletName, statechainId, toAddress, feeRate, duplicatedIndex) => { - let wallet = storageManager.getItem(walletName); + let wallet = storageManager.getWallet(walletName); - let backupTxs = storageManager.getItem(statechainId); + let backupTxs = storageManager.getBackupTransactions(walletName, statechainId); if (backupTxs.length === 0) { throw new Error(`There is no backup transaction for the statechain id ${statechainId}`); @@ -79,7 +79,7 @@ const execute = async (clientConfig, walletName, statechainId, toAddress, feeRat wallet.activities.push(activity); - storageManager.setItem(wallet.name, wallet, true); + storageManager.updateWallet(wallet); const isThereMoreDuplicatedCoins = wallet.coins.some(coin => (coin.status === CoinStatus.DUPLICATED || coin.status === CoinStatus.CONFIRMED) && diff --git a/clients/tests/rust/rust-toolchain.toml b/clients/tests/rust/rust-toolchain.toml index a56a283d..0193dee3 100644 --- a/clients/tests/rust/rust-toolchain.toml +++ b/clients/tests/rust/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.80.1" +channel = "1.83.0" diff --git a/clients/tests/rust/src/bitcoin_core.rs b/clients/tests/rust/src/bitcoin_core.rs index eff64c8c..fd3dd503 100644 --- a/clients/tests/rust/src/bitcoin_core.rs +++ b/clients/tests/rust/src/bitcoin_core.rs @@ -6,7 +6,8 @@ pub fn get_container_id() -> Result { let output = Command::new("docker") .arg("ps") .arg("-qf") - .arg("name=lnd_docker-bitcoind-1") + // .arg("name=lnd_docker-bitcoind-1") + .arg("name=esplora-container") .output() .expect("Failed to execute docker ps command"); @@ -14,7 +15,7 @@ pub fn get_container_id() -> Result { let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string(); if container_id.is_empty() { - return Err(anyhow!("No container found with the name lnd_docker-bitcoind-1")); + return Err(anyhow!("No container found with the name esplora-container")); } Ok(container_id) @@ -44,7 +45,7 @@ pub fn sendtoaddress(amount_in_sats: u32, address: &str) -> Result { let amount = amount_in_sats as f64 / 100_000_000.0; let bitcoin_command = format!( - "bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass sendtoaddress {} {}", address, amount + "cli sendtoaddress {} {}", address, amount ); execute_bitcoin_command(&bitcoin_command) @@ -53,7 +54,7 @@ pub fn sendtoaddress(amount_in_sats: u32, address: &str) -> Result { pub fn generatetoaddress(num_blocks: u32, address: &str) -> Result { let bitcoin_command = format!( - "bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass generatetoaddress {} {}", num_blocks, address + "cli generatetoaddress {} {}", num_blocks, address ); let res = execute_bitcoin_command(&bitcoin_command); @@ -67,7 +68,7 @@ pub fn generatetoaddress(num_blocks: u32, address: &str) -> Result { pub fn getnewaddress() -> Result { let bitcoin_command = format!( - "bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass getnewaddress" + "cli getnewaddress" ); execute_bitcoin_command(&bitcoin_command) diff --git a/clients/tests/rust/src/main.rs b/clients/tests/rust/src/main.rs index 692f76b7..763d9cc9 100644 --- a/clients/tests/rust/src/main.rs +++ b/clients/tests/rust/src/main.rs @@ -3,12 +3,15 @@ pub mod electrs; pub mod bitcoin_core; pub mod ta01_sign_second_not_called; pub mod ta02_duplicate_deposits; +pub mod ta03_multiple_deposits; pub mod tb01_simple_transfer; pub mod tb02_transfer_address_reuse; pub mod tb03_simple_atomic_transfer; pub mod tb04_simple_lightning_latch; +pub mod tb05_timelock; pub mod tm01_sender_double_spends; -mod tv05; +mod tv01; +pub mod utils; use anyhow::{Result, Ok}; #[tokio::main(flavor = "current_thread")] @@ -18,10 +21,12 @@ async fn main() -> Result<()> { tb02_transfer_address_reuse::execute().await?; tb03_simple_atomic_transfer::execute().await?; tb04_simple_lightning_latch::execute().await?; + tb05_timelock::execute().await?; tm01_sender_double_spends::execute().await?; ta01_sign_second_not_called::execute().await?; ta02_duplicate_deposits::execute().await?; - tv05::execute().await?; - + ta03_multiple_deposits::execute().await?; + tv01::execute().await?; + Ok(()) } diff --git a/clients/tests/rust/src/ta01_sign_second_not_called.rs b/clients/tests/rust/src/ta01_sign_second_not_called.rs index 40e5112d..f2401198 100644 --- a/clients/tests/rust/src/ta01_sign_second_not_called.rs +++ b/clients/tests/rust/src/ta01_sign_second_not_called.rs @@ -114,7 +114,9 @@ async fn ta01(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) let amount = 1000; - let token_id = mercuryrustlib::deposit::get_token(client_config).await?; + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; let address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; @@ -158,7 +160,7 @@ async fn ta01(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) let force_send = false; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_ok()); diff --git a/clients/tests/rust/src/ta02_duplicate_deposits.rs b/clients/tests/rust/src/ta02_duplicate_deposits.rs index c179d747..bfb75c25 100644 --- a/clients/tests/rust/src/ta02_duplicate_deposits.rs +++ b/clients/tests/rust/src/ta02_duplicate_deposits.rs @@ -9,7 +9,9 @@ async fn withdraw_flow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: let amount = 1000; - let token_id = mercuryrustlib::deposit::get_token(client_config).await?; + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; let deposit_address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; @@ -69,7 +71,7 @@ async fn withdraw_flow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: let force_send = false; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, force_send, batch_id.clone()).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, None, force_send, batch_id.clone()).await; assert!(result.is_err()); @@ -86,7 +88,7 @@ async fn withdraw_flow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, None, force_send, batch_id).await; assert!(result.is_err()); @@ -107,7 +109,9 @@ async fn transfer_flow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: let amount = 1000; - let token_id = mercuryrustlib::deposit::get_token(client_config).await?; + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; let deposit_address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; @@ -167,7 +171,7 @@ async fn transfer_flow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: let force_send = true; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, force_send, batch_id.clone()).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, None, force_send, batch_id.clone()).await; assert!(result.is_ok()); @@ -181,7 +185,7 @@ async fn transfer_flow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: let wallet1: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; let transferred_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.status == CoinStatus::TRANSFERRED); - let duplicated_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.status == CoinStatus::INVALIDATED); assert!(transferred_coin.is_some()); assert!(duplicated_coin.is_some()); @@ -200,10 +204,10 @@ async fn transfer_flow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: let error_msg = result.err().unwrap().to_string(); - assert!(error_msg == "Signature does not match authentication key."); - - + // assert!(error_msg == "Signature does not match authentication key."); + assert!(error_msg == "No duplicated coins associated with this statechain ID and index 1 were found"); + Ok(()) } @@ -230,7 +234,7 @@ pub async fn execute() -> Result<()> { withdraw_flow(&client_config, &wallet1, &wallet2).await?; transfer_flow(&client_config, &wallet1, &wallet2).await?; - println!("TA02 - Test \"Multiple Deposits in the Same Adress\" completed successfully"); + println!("TA02 - Test \"Duplicate Deposits in the Same Adress\" completed successfully"); Ok(()) } \ No newline at end of file diff --git a/clients/tests/rust/src/ta03_multiple_deposits.rs b/clients/tests/rust/src/ta03_multiple_deposits.rs new file mode 100644 index 00000000..b092550c --- /dev/null +++ b/clients/tests/rust/src/ta03_multiple_deposits.rs @@ -0,0 +1,744 @@ +use std::{env, process::Command, thread, time::Duration}; + +use anyhow::{Result, Ok}; +use mercuryrustlib::{client_config::ClientConfig, BackupTx, CoinStatus, Wallet}; + +use crate::{bitcoin_core, electrs}; + +async fn deposit(amount_in_sats: u32, client_config: &ClientConfig, deposit_address: &str) -> Result<()> { + + let _ = bitcoin_core::sendtoaddress(amount_in_sats, &deposit_address)?; + + let core_wallet_address = bitcoin_core::getnewaddress()?; + let remaining_blocks = client_config.confirmation_target; + let _ = bitcoin_core::generatetoaddress(remaining_blocks, &core_wallet_address)?; + + // It appears that Electrs takes a few seconds to index the transaction + let mut is_tx_indexed = false; + + while !is_tx_indexed { + is_tx_indexed = electrs::check_address(client_config, &deposit_address, amount_in_sats).await?; + thread::sleep(Duration::from_secs(1)); + } + + Ok(()) +} + +fn validate_backup_transactions(backup_transactions: &Vec, interval: u32) -> Result<()> { + let mut current_txid: Option = None; + let mut current_vout: Option = None; + let mut current_tx_n = 0u32; + let mut previous_lock_time = 0u32; + + for backup_tx in backup_transactions { + let outpoint = mercuryrustlib::get_previous_outpoint(&backup_tx)?; + + let current_lock_time = mercuryrustlib::get_blockheight(&backup_tx)?; + + if current_txid.is_some() && current_vout.is_some() { + assert!(current_txid.unwrap() == outpoint.txid && current_vout.unwrap() == outpoint.vout); + assert!(current_lock_time == previous_lock_time - interval); + assert!(backup_tx.tx_n > current_tx_n); + } + + current_txid = Some(outpoint.txid); + current_vout = Some(outpoint.vout); + current_tx_n = backup_tx.tx_n; + previous_lock_time = current_lock_time; + } + + Ok(()) +} + +async fn basic_workflow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) -> Result<()> { + + let amount = 1000; + + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; + + let deposit_address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; + + deposit(amount, &client_config, &deposit_address).await?; + + let amount = 2000; + + deposit(amount, &client_config, &deposit_address).await?; + + deposit(amount, &client_config, &deposit_address).await?; + + let amount = 1000; + + deposit(amount, &client_config, &deposit_address).await?; + + mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; + let wallet1: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::CONFIRMED); + let duplicated_coin_1 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin_2 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin_3 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 3 && coin.status == CoinStatus::DUPLICATED); + + assert!(new_coin.is_some()); + assert!(duplicated_coin_1.is_some()); + assert!(duplicated_coin_2.is_some()); + assert!(duplicated_coin_3.is_some()); + + let new_coin = new_coin.unwrap(); + + let statechain_id = new_coin.statechain_id.as_ref().unwrap(); + + let wallet2_transfer_adress = mercuryrustlib::transfer_receiver::new_transfer_address(&client_config, &wallet2.name).await?; + + let batch_id = None; + + let force_send = true; + + let duplicated_indexes = vec![1, 3]; + + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, Some(duplicated_indexes), force_send, batch_id).await; + + assert!(result.is_ok()); + + let wallet1 = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::IN_TRANSFER); + let duplicated_coin_1 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::IN_TRANSFER); + let duplicated_coin_2 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin_3 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 3 && coin.status == CoinStatus::IN_TRANSFER); + + assert!(new_coin.is_some()); + assert!(duplicated_coin_1.is_some()); + assert!(duplicated_coin_2.is_some()); + assert!(duplicated_coin_3.is_some()); + + let new_coin = new_coin.unwrap(); + + let backup_transactions = mercuryrustlib::sqlite_manager::get_backup_txs(&client_config.pool, &wallet1.name, &statechain_id).await?; + + /* for backup_tx in backup_transactions.iter() { + let tx_outpoint = mercuryrustlib::get_previous_outpoint(&backup_tx)?; + println!("statechain_id: {}", statechain_id); + println!("txid: {} vout: {}", tx_outpoint.txid, tx_outpoint.vout); + println!("tx_n: {}", backup_tx.tx_n); + // println!("client_public_nonce: {}", backup_tx.client_public_nonce); + // println!("server_public_nonce: {}", backup_tx.server_public_nonce); + // println!("client_public_key: {}", backup_tx.client_public_key); + // println!("server_public_key: {}", backup_tx.server_public_key); + // println!("blinding_factor: {}", backup_tx.blinding_factor); + println!("----------------------"); + } */ + + let info_config = mercuryrustlib::utils::info_config(&client_config).await?; + + let split_backup_transactions = mercuryrustlib::transfer_receiver::split_backup_transactions(&backup_transactions); + + for (index, bk_txs) in split_backup_transactions.iter().enumerate() { + if index == 0 { + let first_bkp_tx = bk_txs.first().unwrap(); + let first_backup_outpoint = mercuryrustlib::get_previous_outpoint(&first_bkp_tx)?; + assert!(first_backup_outpoint.txid == new_coin.utxo_txid.clone().unwrap() && first_backup_outpoint.vout == new_coin.utxo_vout.unwrap()); + } + validate_backup_transactions(&bk_txs, info_config.interval)?; + } + + let transfer_receive_result = mercuryrustlib::transfer_receiver::execute(&client_config, &wallet2.name).await?; + + let received_statechain_ids = transfer_receive_result.received_statechain_ids; + + assert!(received_statechain_ids.contains(&statechain_id.to_string())); + assert!(received_statechain_ids.len() == 1); + + let wallet2: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet2.name).await?; + + let w2_new_coin = wallet2.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::CONFIRMED); + let w2_duplicated_coin_1 = wallet2.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 1 && coin.status == CoinStatus::DUPLICATED); + let w2_duplicated_coin_2 = wallet2.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + + assert!(w2_new_coin.is_some()); + assert!(w2_duplicated_coin_1.is_some()); + assert!(w2_duplicated_coin_2.is_some()); + + /* let mut coins_json = Vec::new(); + + for coin in wallet2.coins.iter() { + let obj = json!({ + "coin.user_pubkey": coin.user_pubkey, + "coin.aggregated_address": coin.aggregated_address.as_ref().unwrap_or(&"".to_string()), + "coin.address": coin.address, + "coin.statechain_id": coin.statechain_id.as_ref().unwrap_or(&"".to_string()), + "coin.amount": coin.amount.unwrap_or(0), + "coin.status": coin.status, + "coin.locktime": coin.locktime.unwrap_or(0), + "coin.duplicate_index": coin.duplicate_index, + }); + + coins_json.push(obj); + } + + let coins_json_string = serde_json::to_string_pretty(&coins_json).unwrap(); + println!("{}", coins_json_string); */ + + mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; + let wallet1: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::TRANSFERRED); + let duplicated_coin_1 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::TRANSFERRED); + let duplicated_coin_2 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::INVALIDATED); + let duplicated_coin_3 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 3 && coin.status == CoinStatus::TRANSFERRED); + + assert!(new_coin.is_some()); + assert!(duplicated_coin_1.is_some()); + assert!(duplicated_coin_2.is_some()); + assert!(duplicated_coin_3.is_some()); + + let core_wallet_address = bitcoin_core::getnewaddress()?; + + let fee_rate = None; + + let result = mercuryrustlib::withdraw::execute(&client_config, &wallet2.name, statechain_id, &core_wallet_address, fee_rate, Some(1)).await; + assert!(result.is_ok()); + + let result = mercuryrustlib::withdraw::execute(&client_config, &wallet2.name, statechain_id, &core_wallet_address, fee_rate, Some(2)).await; + assert!(result.is_ok()); + + let result = mercuryrustlib::withdraw::execute(&client_config, &wallet2.name, statechain_id, &core_wallet_address, fee_rate, None).await; + assert!(result.is_ok()); + + Ok(()) +} + +async fn resend_workflow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) -> Result<()> { + + let amount = 1000; + + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; + + let deposit_address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; + + deposit(amount, &client_config, &deposit_address).await?; + + let amount = 2000; + + deposit(amount, &client_config, &deposit_address).await?; + + deposit(amount, &client_config, &deposit_address).await?; + + let amount = 1000; + + deposit(amount, &client_config, &deposit_address).await?; + + mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; + let wallet1: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::CONFIRMED); + let duplicated_coin_1 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin_2 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin_3 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 3 && coin.status == CoinStatus::DUPLICATED); + + assert!(new_coin.is_some()); + assert!(duplicated_coin_1.is_some()); + assert!(duplicated_coin_2.is_some()); + assert!(duplicated_coin_3.is_some()); + + let new_coin = new_coin.unwrap(); + + let statechain_id = new_coin.statechain_id.as_ref().unwrap(); + + let wallet2_transfer_adress = mercuryrustlib::transfer_receiver::new_transfer_address(&client_config, &wallet2.name).await?; + + let batch_id = None; + + let force_send = true; + + let duplicated_indexes = vec![1, 3]; + + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, Some(duplicated_indexes), force_send, batch_id).await; + + assert!(result.is_ok()); + + let wallet1 = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::IN_TRANSFER); + let duplicated_coin_1 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::IN_TRANSFER); + let duplicated_coin_2 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin_3 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 3 && coin.status == CoinStatus::IN_TRANSFER); + + assert!(new_coin.is_some()); + assert!(duplicated_coin_1.is_some()); + assert!(duplicated_coin_2.is_some()); + assert!(duplicated_coin_3.is_some()); + + let batch_id = None; + + let duplicated_indexes = vec![2]; + + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, Some(duplicated_indexes), force_send, batch_id).await; + + assert!(result.is_ok()); + + let wallet1 = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::IN_TRANSFER); + let duplicated_coin_1 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::IN_TRANSFER); + let duplicated_coin_2 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::IN_TRANSFER); + let duplicated_coin_3 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 3 && coin.status == CoinStatus::IN_TRANSFER); + + assert!(new_coin.is_some()); + assert!(duplicated_coin_1.is_some()); + assert!(duplicated_coin_2.is_some()); + assert!(duplicated_coin_3.is_some()); + + let transfer_receive_result = mercuryrustlib::transfer_receiver::execute(&client_config, &wallet2.name).await?; + + let received_statechain_ids = transfer_receive_result.received_statechain_ids; + + assert!(received_statechain_ids.contains(&statechain_id.to_string())); + assert!(received_statechain_ids.len() == 1); + + let wallet2: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet2.name).await?; + + let w2_new_coin = wallet2.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::CONFIRMED); + let w2_duplicated_coin_1 = wallet2.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 1 && coin.status == CoinStatus::DUPLICATED); + let w2_duplicated_coin_2 = wallet2.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + let w2_duplicated_coin_3 = wallet2.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 3 && coin.status == CoinStatus::DUPLICATED); + + assert!(w2_new_coin.is_some()); + assert!(w2_duplicated_coin_1.is_some()); + assert!(w2_duplicated_coin_2.is_some()); + assert!(w2_duplicated_coin_3.is_some()); + + let core_wallet_address = bitcoin_core::getnewaddress()?; + + let fee_rate = None; + + let result = mercuryrustlib::withdraw::execute(&client_config, &wallet2.name, statechain_id, &core_wallet_address, fee_rate, Some(1)).await; + assert!(result.is_ok()); + + let result = mercuryrustlib::withdraw::execute(&client_config, &wallet2.name, statechain_id, &core_wallet_address, fee_rate, Some(2)).await; + assert!(result.is_ok()); + + let result = mercuryrustlib::withdraw::execute(&client_config, &wallet2.name, statechain_id, &core_wallet_address, fee_rate, Some(3)).await; + assert!(result.is_ok()); + + let result = mercuryrustlib::withdraw::execute(&client_config, &wallet2.name, statechain_id, &core_wallet_address, fee_rate, None).await; + assert!(result.is_ok()); + + Ok(()) +} + +async fn multiple_sends_workflow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet, wallet3: &Wallet) -> Result<()> { + + let amount = 1000; + + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; + + let deposit_address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; + + deposit(amount, &client_config, &deposit_address).await?; + + let amount = 2000; + + deposit(amount, &client_config, &deposit_address).await?; + + deposit(amount, &client_config, &deposit_address).await?; + + let amount = 1000; + + deposit(amount, &client_config, &deposit_address).await?; + + mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; + let wallet1: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::CONFIRMED); + let duplicated_coin_1 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin_2 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin_3 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 3 && coin.status == CoinStatus::DUPLICATED); + + assert!(new_coin.is_some()); + assert!(duplicated_coin_1.is_some()); + assert!(duplicated_coin_2.is_some()); + assert!(duplicated_coin_3.is_some()); + + let new_coin = new_coin.unwrap(); + + let statechain_id = new_coin.statechain_id.as_ref().unwrap(); + + let wallet2_transfer_adress = mercuryrustlib::transfer_receiver::new_transfer_address(&client_config, &wallet2.name).await?; + + let batch_id = None; + + let force_send = true; + + let duplicated_indexes = vec![1, 2, 3]; + + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, Some(duplicated_indexes), force_send, batch_id).await; + + assert!(result.is_ok()); + + let wallet1 = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::IN_TRANSFER); + let duplicated_coin_1 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::IN_TRANSFER); + let duplicated_coin_2 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::IN_TRANSFER); + let duplicated_coin_3 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 3 && coin.status == CoinStatus::IN_TRANSFER); + + assert!(new_coin.is_some()); + assert!(duplicated_coin_1.is_some()); + assert!(duplicated_coin_2.is_some()); + assert!(duplicated_coin_3.is_some()); + + let transfer_receive_result = mercuryrustlib::transfer_receiver::execute(&client_config, &wallet2.name).await?; + + let received_statechain_ids = transfer_receive_result.received_statechain_ids; + + assert!(received_statechain_ids.contains(&statechain_id.to_string())); + assert!(received_statechain_ids.len() == 1); + + let wallet2: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet2.name).await?; + + let w2_new_coin = wallet2.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::CONFIRMED); + let w2_duplicated_coin_1 = wallet2.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 1 && coin.status == CoinStatus::DUPLICATED); + let w2_duplicated_coin_2 = wallet2.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + let w2_duplicated_coin_3 = wallet2.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 3 && coin.status == CoinStatus::DUPLICATED); + + assert!(w2_new_coin.is_some()); + assert!(w2_duplicated_coin_1.is_some()); + assert!(w2_duplicated_coin_2.is_some()); + assert!(w2_duplicated_coin_3.is_some()); + + let wallet3_transfer_adress = mercuryrustlib::transfer_receiver::new_transfer_address(&client_config, &wallet3.name).await?; + + let batch_id = None; + + let force_send = true; + + let duplicated_indexes = vec![1, 2, 3]; + + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet3_transfer_adress, &wallet2.name, &statechain_id, Some(duplicated_indexes), force_send, batch_id).await; + + assert!(result.is_ok()); + + let transfer_receive_result = mercuryrustlib::transfer_receiver::execute(&client_config, &wallet3.name).await?; + + let received_statechain_ids = transfer_receive_result.received_statechain_ids; + + assert!(received_statechain_ids.contains(&statechain_id.to_string())); + assert!(received_statechain_ids.len() == 1); + + let wallet3: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet3.name).await?; + + let w3_new_coin = wallet3.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::CONFIRMED); + let w3_duplicated_coin_1 = wallet3.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 1 && coin.status == CoinStatus::DUPLICATED); + let w3_duplicated_coin_2 = wallet3.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + let w3_duplicated_coin_3 = wallet3.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 3 && coin.status == CoinStatus::DUPLICATED); + + assert!(w3_new_coin.is_some()); + assert!(w3_duplicated_coin_1.is_some()); + assert!(w3_duplicated_coin_2.is_some()); + assert!(w3_duplicated_coin_3.is_some()); + + let core_wallet_address = bitcoin_core::getnewaddress()?; + + let fee_rate = None; + + let result = mercuryrustlib::withdraw::execute(&client_config, &wallet3.name, statechain_id, &core_wallet_address, fee_rate, Some(3)).await; + assert!(result.is_ok()); + + let wallet1_transfer_adress = mercuryrustlib::transfer_receiver::new_transfer_address(&client_config, &wallet1.name).await?; + + let batch_id = None; + + let force_send = true; + + let duplicated_indexes = vec![1, 2, 3]; + + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet1_transfer_adress, &wallet3.name, &statechain_id, Some(duplicated_indexes), force_send, batch_id).await; + + assert!(result.is_err()); + + assert!(result.err().unwrap().to_string().contains("There have been withdrawals of other coins with this same statechain_id (possibly duplicates).This transfer cannot be performed because the recipient would reject it due to the difference in signature count.This coin can be withdrawn, however.")); + + Ok(()) +} + +async fn send_to_itself_workflow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) -> Result<()> { + + let amount = 1000; + + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; + + let deposit_address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; + + deposit(amount, &client_config, &deposit_address).await?; + + let amount = 2000; + + deposit(amount, &client_config, &deposit_address).await?; + + deposit(amount, &client_config, &deposit_address).await?; + + let amount = 1000; + + deposit(amount, &client_config, &deposit_address).await?; + + mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; + let wallet1: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::CONFIRMED); + let duplicated_coin_1 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin_2 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin_3 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 3 && coin.status == CoinStatus::DUPLICATED); + + assert!(new_coin.is_some()); + assert!(duplicated_coin_1.is_some()); + assert!(duplicated_coin_2.is_some()); + assert!(duplicated_coin_3.is_some()); + + let new_coin = new_coin.unwrap(); + + let statechain_id = new_coin.statechain_id.as_ref().unwrap(); + + let wallet1_transfer_adress = mercuryrustlib::transfer_receiver::new_transfer_address(&client_config, &wallet1.name).await?; + + let batch_id = None; + + let force_send = true; + + let duplicated_indexes = vec![1, 3]; + + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet1_transfer_adress, &wallet1.name, &statechain_id, Some(duplicated_indexes), force_send, batch_id).await; + + assert!(result.is_ok()); + + let transfer_receive_result = mercuryrustlib::transfer_receiver::execute(&client_config, &wallet1.name).await?; + + let received_statechain_ids = transfer_receive_result.received_statechain_ids; + + assert!(received_statechain_ids.contains(&statechain_id.to_string())); + assert!(received_statechain_ids.len() == 1); + + mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; + let wallet1: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let transferred_coin_0 = wallet1.coins.iter().filter(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::TRANSFERRED); + let transferred_coin_1 = wallet1.coins.iter().filter(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::TRANSFERRED); + let invalidated_coin_2 = wallet1.coins.iter().filter(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::INVALIDATED); + let transferred_coin_3 = wallet1.coins.iter().filter(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 3 && coin.status == CoinStatus::TRANSFERRED); + + assert!(transferred_coin_0.count() == 1); + assert!(transferred_coin_1.count() == 1); + assert!(invalidated_coin_2.count() == 1); + assert!(transferred_coin_3.count() == 1); + + let confirmed_coin_0 = wallet1.coins.iter().filter(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::CONFIRMED); + let duplicated_coin_1 = wallet1.coins.iter().filter(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::DUPLICATED); + let duplicated_coin_2 = wallet1.coins.iter().filter(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + + assert!(confirmed_coin_0.count() == 1); + assert!(duplicated_coin_1.count() == 1); + assert!(duplicated_coin_2.count() == 1); + + // now send the coins to another wallet. + + let wallet2_transfer_adress = mercuryrustlib::transfer_receiver::new_transfer_address(&client_config, &wallet2.name).await?; + + let batch_id = None; + + let force_send = true; + + let duplicated_indexes = vec![1, 2]; + + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, Some(duplicated_indexes), force_send, batch_id).await; + + assert!(result.is_ok()); + + let wallet1: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::IN_TRANSFER); + let duplicated_coin_1 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 1 && coin.status == CoinStatus::IN_TRANSFER); + let duplicated_coin_2 = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 2 && coin.status == CoinStatus::IN_TRANSFER); + + /* let mut coins_json = Vec::new(); + + for coin in wallet1.coins.iter() { + let obj = json!({ + "coin.user_pubkey": coin.user_pubkey, + "coin.aggregated_address": coin.aggregated_address.as_ref().unwrap_or(&"".to_string()), + "coin.address": coin.address, + "coin.statechain_id": coin.statechain_id.as_ref().unwrap_or(&"".to_string()), + "coin.amount": coin.amount.unwrap_or(0), + "coin.status": coin.status, + "coin.locktime": coin.locktime.unwrap_or(0), + "coin.duplicate_index": coin.duplicate_index, + }); + + coins_json.push(obj); + } + + let coins_json_string = serde_json::to_string_pretty(&coins_json).unwrap(); + println!("{}", coins_json_string); */ + + assert!(new_coin.is_some()); + assert!(duplicated_coin_1.is_some()); + assert!(duplicated_coin_2.is_some()); + + let transfer_receive_result = mercuryrustlib::transfer_receiver::execute(&client_config, &wallet2.name).await?; + + let received_statechain_ids = transfer_receive_result.received_statechain_ids; + + assert!(received_statechain_ids.contains(&statechain_id.to_string())); + assert!(received_statechain_ids.len() == 1); + + let wallet2: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet2.name).await?; + + let w2_new_coin = wallet2.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::CONFIRMED); + let w2_duplicated_coin_1 = wallet2.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 1 && coin.status == CoinStatus::DUPLICATED); + let w2_duplicated_coin_2 = wallet2.coins.iter().find(|&coin| coin.statechain_id == Some(statechain_id.to_string()) && coin.duplicate_index == 2 && coin.status == CoinStatus::DUPLICATED); + + assert!(w2_new_coin.is_some()); + assert!(w2_duplicated_coin_1.is_some()); + assert!(w2_duplicated_coin_2.is_some()); + + let core_wallet_address = bitcoin_core::getnewaddress()?; + + let fee_rate = None; + + let result = mercuryrustlib::withdraw::execute(&client_config, &wallet2.name, statechain_id, &core_wallet_address, fee_rate, Some(1)).await; + assert!(result.is_ok()); + + let result = mercuryrustlib::withdraw::execute(&client_config, &wallet2.name, statechain_id, &core_wallet_address, fee_rate, Some(2)).await; + assert!(result.is_ok()); + + let result = mercuryrustlib::withdraw::execute(&client_config, &wallet2.name, statechain_id, &core_wallet_address, fee_rate, None).await; + assert!(result.is_ok()); + + Ok(()) + +} + +async fn send_unconfirmed_duplicated_workflow(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) -> Result<()> { + + let amount = 1000; + + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; + + let deposit_address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; + + deposit(amount, &client_config, &deposit_address).await?; + + let amount = 1000; + + deposit(amount, &client_config, &deposit_address).await?; + + let amount = 2000; + + let _ = bitcoin_core::sendtoaddress(amount, &deposit_address)?; + + let mut is_tx_indexed = false; + + while !is_tx_indexed { + is_tx_indexed = electrs::check_address(client_config, &deposit_address, amount).await?; + thread::sleep(Duration::from_secs(1)); + } + + mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; + let wallet1: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.duplicate_index == 0 && coin.status == CoinStatus::CONFIRMED); + let confirmed_duplicated_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.status == CoinStatus::DUPLICATED && coin.amount == Some(1000)); + let unconfirmed_duplicated_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.status == CoinStatus::DUPLICATED && coin.amount == Some(2000)); + + assert!(new_coin.is_some()); + assert!(confirmed_duplicated_coin.is_some()); + assert!(unconfirmed_duplicated_coin.is_some()); + + let unconfirmed_duplicated_coin = unconfirmed_duplicated_coin.unwrap(); + + let new_coin = new_coin.unwrap(); + + let statechain_id = new_coin.statechain_id.as_ref().unwrap(); + + let wallet2_transfer_adress = mercuryrustlib::transfer_receiver::new_transfer_address(&client_config, &wallet2.name).await?; + + let batch_id = None; + + let force_send = true; + + let duplicated_indexes = vec![1, 2]; + + // try to send the unconfirmed duplicated coin + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, Some(duplicated_indexes), force_send, batch_id).await; + + assert!(result.is_err()); + + let error_message = result.err().unwrap().to_string(); + + let expected_error_message = format!("The coin with duplicated index {} has not yet been confirmed. This transfer cannot be performed.", unconfirmed_duplicated_coin.duplicate_index); + + assert!(error_message.contains(expected_error_message.as_str())); + + let core_wallet_address = bitcoin_core::getnewaddress()?; + let remaining_blocks = client_config.confirmation_target; + let _ = bitcoin_core::generatetoaddress(remaining_blocks, &core_wallet_address)?; + + let batch_id = None; + + let duplicated_indexes = vec![1, 2]; + + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, Some(duplicated_indexes), force_send, batch_id).await; + + assert!(result.is_ok()); + + Ok(()) +} + +pub async fn execute() -> Result<()> { + + let _ = Command::new("rm").arg("wallet.db").arg("wallet.db-shm").arg("wallet.db-wal").output().expect("failed to execute process"); + + env::set_var("ML_NETWORK", "regtest"); + + let client_config = mercuryrustlib::client_config::load().await; + + let wallet1 = mercuryrustlib::wallet::create_wallet( + "wallet1", + &client_config).await?; + + mercuryrustlib::sqlite_manager::insert_wallet(&client_config.pool, &wallet1).await?; + + let wallet2 = mercuryrustlib::wallet::create_wallet( + "wallet2", + &client_config).await?; + + mercuryrustlib::sqlite_manager::insert_wallet(&client_config.pool, &wallet2).await?; + + basic_workflow(&client_config, &wallet1, &wallet2).await?; + + resend_workflow(&client_config, &wallet1, &wallet2).await?; + + let wallet3 = mercuryrustlib::wallet::create_wallet( + "wallet3", + &client_config).await?; + + mercuryrustlib::sqlite_manager::insert_wallet(&client_config.pool, &wallet3).await?; + + multiple_sends_workflow(&client_config, &wallet1, &wallet2, &wallet3).await?; + + send_to_itself_workflow(&client_config, &wallet1, &wallet2).await?; + + send_unconfirmed_duplicated_workflow(&client_config, &wallet1, &wallet2).await?; + + println!("TA03 - Test \"Multiple Deposits in the Same Adress\" completed successfully"); + + Ok(()) +} \ No newline at end of file diff --git a/clients/tests/rust/src/tb01_simple_transfer.rs b/clients/tests/rust/src/tb01_simple_transfer.rs index e3c7d98d..8412595b 100644 --- a/clients/tests/rust/src/tb01_simple_transfer.rs +++ b/clients/tests/rust/src/tb01_simple_transfer.rs @@ -6,19 +6,19 @@ use mercuryrustlib::{client_config::ClientConfig, CoinStatus, Wallet}; use crate::{bitcoin_core, electrs}; -async fn try_to_send_unconfirmed_coin(client_config: &ClientConfig, to_address: &str, wallet: &Wallet, statechain_id: &str, current_status: &str) -> Result<()> { +async fn try_to_send_unconfirmed_coin(client_config: &ClientConfig, to_address: &str, wallet: &Wallet, statechain_id: &str) -> Result<()> { let batch_id = None; let force_send = false; - let result = mercuryrustlib::transfer_sender::execute(&client_config, to_address, &wallet.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, to_address, &wallet.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_err()); let error = result.err().unwrap(); - let error_message = format!("Coin status must be CONFIRMED or IN_TRANSFER to transfer it. The current status is {}", current_status); + let error_message = format!("No coins with status CONFIRMED or IN_TRANSFER associated with this statechain ID were found"); assert!(error.to_string() == error_message); @@ -27,7 +27,9 @@ async fn try_to_send_unconfirmed_coin(client_config: &ClientConfig, to_address: async fn sucessfully_transfer(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) -> Result<()> { - let token_id = mercuryrustlib::deposit::get_token(client_config).await?; + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; let amount = 1000; @@ -90,7 +92,7 @@ async fn sucessfully_transfer(client_config: &ClientConfig, wallet1: &Wallet, wa let statechain_id = new_coin.statechain_id.as_ref().unwrap(); - try_to_send_unconfirmed_coin(&client_config, &wallet2_transfer_adress, &wallet1, statechain_id, &CoinStatus::IN_MEMPOOL.to_string()).await?; + try_to_send_unconfirmed_coin(&client_config, &wallet2_transfer_adress, &wallet1, statechain_id).await?; let core_wallet_address = bitcoin_core::getnewaddress()?; let _ = bitcoin_core::generatetoaddress(1, &core_wallet_address)?; @@ -103,7 +105,7 @@ async fn sucessfully_transfer(client_config: &ClientConfig, wallet1: &Wallet, wa assert!(new_coin.status == CoinStatus::UNCONFIRMED); - try_to_send_unconfirmed_coin(&client_config, &wallet2_transfer_adress, &wallet1, statechain_id, &CoinStatus::UNCONFIRMED.to_string()).await?; + try_to_send_unconfirmed_coin(&client_config, &wallet2_transfer_adress, &wallet1, statechain_id).await?; let remaining_blocks = client_config.confirmation_target - 1; let _ = bitcoin_core::generatetoaddress(remaining_blocks, &core_wallet_address)?; @@ -120,7 +122,7 @@ async fn sucessfully_transfer(client_config: &ClientConfig, wallet1: &Wallet, wa let force_send = false; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_ok()); diff --git a/clients/tests/rust/src/tb02_transfer_address_reuse.rs b/clients/tests/rust/src/tb02_transfer_address_reuse.rs index e02ed726..a63ba074 100644 --- a/clients/tests/rust/src/tb02_transfer_address_reuse.rs +++ b/clients/tests/rust/src/tb02_transfer_address_reuse.rs @@ -11,7 +11,9 @@ async fn tb02(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) // Create first deposit address - let token_id = mercuryrustlib::deposit::get_token(client_config).await?; + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; let address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; @@ -19,7 +21,9 @@ async fn tb02(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) // Create second deposit address - let token_id = mercuryrustlib::deposit::get_token(client_config).await?; + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; let address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; @@ -56,7 +60,7 @@ async fn tb02(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) let statechain_id = coin.statechain_id.as_ref().unwrap(); - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_ok()); } @@ -112,7 +116,7 @@ async fn tb02(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) let force_send = false; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet1_transfer_adress, &wallet2.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet1_transfer_adress, &wallet2.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_ok()); diff --git a/clients/tests/rust/src/tb03_simple_atomic_transfer.rs b/clients/tests/rust/src/tb03_simple_atomic_transfer.rs index 4bbfb346..deec5b6c 100644 --- a/clients/tests/rust/src/tb03_simple_atomic_transfer.rs +++ b/clients/tests/rust/src/tb03_simple_atomic_transfer.rs @@ -10,13 +10,17 @@ pub async fn tb03(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wall // Create first deposit address - let token_id = mercuryrustlib::deposit::get_token(client_config).await?; + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; let wallet1_address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; let _ = bitcoin_core::sendtoaddress(amount, &wallet1_address)?; - let token_id = mercuryrustlib::deposit::get_token(client_config).await?; + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; let wallet2_address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet2.name, &token_id, amount).await?; @@ -48,7 +52,7 @@ pub async fn tb03(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wall let force_send = false; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet3_transfer_adress, &wallet1.name, &statechain_id_1, force_send, batch_id.clone()).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet3_transfer_adress, &wallet1.name, &statechain_id_1, None, force_send, batch_id.clone()).await; assert!(result.is_ok()); @@ -57,7 +61,7 @@ pub async fn tb03(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wall let new_coin = wallet2.coins.iter().find(|&coin| coin.aggregated_address == Some(wallet2_address.clone()) && coin.status == CoinStatus::CONFIRMED).unwrap(); let statechain_id_2 = new_coin.statechain_id.as_ref().unwrap(); - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet4_transfer_adress, &wallet2.name, &statechain_id_2, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet4_transfer_adress, &wallet2.name, &statechain_id_2, None, force_send, batch_id).await; assert!(result.is_ok()); diff --git a/clients/tests/rust/src/tb04_simple_lightning_latch.rs b/clients/tests/rust/src/tb04_simple_lightning_latch.rs index eb4f1e27..2d670d4c 100644 --- a/clients/tests/rust/src/tb04_simple_lightning_latch.rs +++ b/clients/tests/rust/src/tb04_simple_lightning_latch.rs @@ -12,7 +12,9 @@ pub async fn tb04(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wall // Create first deposit address - let token_id = mercuryrustlib::deposit::get_token(client_config).await?; + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; let deposit_address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; @@ -44,7 +46,7 @@ pub async fn tb04(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wall let force_send = false; - mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, force_send, Some(batch_id.clone())).await?; + mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, statechain_id, None, force_send, Some(batch_id.clone())).await?; // receiver confirms that payment hash was generated by the server let response = mercuryrustlib::lightning_latch::get_payment_hash(&client_config, &batch_id).await?; diff --git a/clients/tests/rust/src/tb05_timelock.rs b/clients/tests/rust/src/tb05_timelock.rs new file mode 100644 index 00000000..d8482520 --- /dev/null +++ b/clients/tests/rust/src/tb05_timelock.rs @@ -0,0 +1,111 @@ +use std::{env, process::Command, thread, time::Duration}; + +use anyhow::{Result, Ok}; +use electrum_client::ElectrumApi; +use mercuryrustlib::{client_config::ClientConfig, CoinStatus, Wallet}; + +use crate::{bitcoin_core, electrs}; + +pub async fn old_state_broadcasted(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet) -> Result<()> { + + let amount = 1000; + + // Create first deposit address + + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; + + let deposit_address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; + + let _ = bitcoin_core::sendtoaddress(amount, &deposit_address)?; + + let core_wallet_address = bitcoin_core::getnewaddress()?; + let remaining_blocks = client_config.confirmation_target; + let _ = bitcoin_core::generatetoaddress(remaining_blocks, &core_wallet_address)?; + + // It appears that Electrs takes a few seconds to index the transaction + let mut is_tx_indexed = false; + + while !is_tx_indexed { + is_tx_indexed = electrs::check_address(client_config, &deposit_address, amount).await?; + thread::sleep(Duration::from_secs(1)); + } + + let wallet1_transfer_adress = mercuryrustlib::transfer_receiver::new_transfer_address(&client_config, &wallet1.name).await?; + + mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; + let wallet1: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + let new_coin = wallet1.coins.iter().find(|&coin| coin.aggregated_address == Some(deposit_address.clone()) && coin.status == CoinStatus::CONFIRMED).unwrap(); + let statechain_id_1 = new_coin.statechain_id.as_ref().unwrap(); + + let force_send = false; + + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet1_transfer_adress, &wallet1.name, &statechain_id_1.clone(), None, force_send, None).await; + + assert!(result.is_ok()); + + let wallet2_transfer_adress = mercuryrustlib::transfer_receiver::new_transfer_address(&client_config, &wallet2.name).await?; + + mercuryrustlib::coin_status::update_coins(&client_config, &wallet1.name).await?; + let wallet1: mercuryrustlib::Wallet = mercuryrustlib::sqlite_manager::get_wallet(&client_config.pool, &wallet1.name).await?; + + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id_1.clone(), None, force_send, None).await; + + assert!(result.is_ok()); + + let backup_transactions = mercuryrustlib::sqlite_manager::get_backup_txs(&client_config.pool, &wallet1.name, &statechain_id_1).await?; + + assert!(backup_transactions.len() == 3); + + let bkp_tx = backup_transactions.get(1).unwrap(); + + assert!(bkp_tx.tx_n == 2); + + let tx_bytes = hex::decode(&bkp_tx.tx)?; + let txid = client_config.electrum_client.transaction_broadcast_raw(&tx_bytes); + + assert!(txid.is_err()); + + let tx_lock_time = mercuryrustlib::get_blockheight(&bkp_tx)?; + + let current_blockheight = electrs::get_blockheight(&client_config).await? as u32; + + let height_diff = tx_lock_time - current_blockheight; + + let _ = bitcoin_core::generatetoaddress(height_diff, &core_wallet_address)?; + + let txid = client_config.electrum_client.transaction_broadcast_raw(&tx_bytes); + + assert!(txid.is_ok()); + + Ok(()) + +} + +pub async fn execute() -> Result<()> { + + let _ = Command::new("rm").arg("wallet.db").arg("wallet.db-shm").arg("wallet.db-wal").output().expect("failed to execute process"); + + env::set_var("ML_NETWORK", "regtest"); + + let client_config = mercuryrustlib::client_config::load().await; + + let wallet1 = mercuryrustlib::wallet::create_wallet( + "wallet1", + &client_config).await?; + + mercuryrustlib::sqlite_manager::insert_wallet(&client_config.pool, &wallet1).await?; + + let wallet2 = mercuryrustlib::wallet::create_wallet( + "wallet2", + &client_config).await?; + + mercuryrustlib::sqlite_manager::insert_wallet(&client_config.pool, &wallet2).await?; + + old_state_broadcasted(&client_config, &wallet1, &wallet2).await?; + + println!("TB05 - Timelock tests completed successfully"); + + Ok(()) +} \ No newline at end of file diff --git a/clients/tests/rust/src/tm01_sender_double_spends.rs b/clients/tests/rust/src/tm01_sender_double_spends.rs index 113a3fc2..de4bafab 100644 --- a/clients/tests/rust/src/tm01_sender_double_spends.rs +++ b/clients/tests/rust/src/tm01_sender_double_spends.rs @@ -8,7 +8,9 @@ async fn tm01(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet, let amount = 1000; - let token_id = mercuryrustlib::deposit::get_token(client_config).await?; + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; let address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; @@ -40,7 +42,7 @@ async fn tm01(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet, let wallet2_transfer_adress = mercuryrustlib::transfer_receiver::new_transfer_address(&client_config, &wallet2.name).await?; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_ok()); @@ -49,7 +51,7 @@ async fn tm01(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet, let batch_id = None; // this first "double spend" is legitimate, as it will overwrite the previous transaction - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet3_transfer_adress, &wallet1.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet3_transfer_adress, &wallet1.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_ok()); @@ -63,7 +65,7 @@ async fn tm01(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet, let batch_id = None; // this second "double spend" is not legitimate, as the statecoin has already been received by wallet3 - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_err()); @@ -77,11 +79,11 @@ async fn tm01(client_config: &ClientConfig, wallet1: &Wallet, wallet2: &Wallet, let batch_id = None; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_err()); - assert!(result.err().unwrap().to_string().contains("Coin status must be CONFIRMED or IN_TRANSFER to transfer it. The current status is TRANSFERRED")); + assert!(result.err().unwrap().to_string().contains("No coins with status CONFIRMED or IN_TRANSFER associated with this statechain ID were found")); Ok(()) } diff --git a/clients/tests/rust/src/tv05.rs b/clients/tests/rust/src/tv01.rs similarity index 87% rename from clients/tests/rust/src/tv05.rs rename to clients/tests/rust/src/tv01.rs index 29bff91f..82aa3432 100644 --- a/clients/tests/rust/src/tv05.rs +++ b/clients/tests/rust/src/tv01.rs @@ -9,7 +9,9 @@ async fn w1_transfer_to_w2(client_config: &ClientConfig, wallet1: &Wallet, walle let amount = 1000; - let token_id = mercuryrustlib::deposit::get_token(client_config).await?; + let token_response = mercuryrustlib::deposit::get_token(client_config).await?; + + let token_id = crate::utils::handle_token_response(client_config, &token_response).await?; let deposit_address = mercuryrustlib::deposit::get_deposit_bitcoin_address(&client_config, &wallet1.name, &token_id, amount).await?; @@ -39,7 +41,7 @@ async fn w1_transfer_to_w2(client_config: &ClientConfig, wallet1: &Wallet, walle let batch_id = None; let force_send = false; - let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, force_send, batch_id).await; + let result = mercuryrustlib::transfer_sender::execute(&client_config, &wallet2_transfer_adress, &wallet1.name, &statechain_id, None, force_send, batch_id).await; assert!(result.is_ok()); let transfer_receive_result = mercuryrustlib::transfer_receiver::execute(&client_config, &wallet2.name).await?; @@ -65,7 +67,13 @@ async fn w1_transfer_to_w2(client_config: &ClientConfig, wallet1: &Wallet, walle let err = result.err().unwrap(); - assert!(err.to_string() == "Electrum server error: {\"code\":2,\"message\":\"non-final\"}"); + let electrs_msg_err = "Electrum server error: {\"code\":2,\"message\":\"non-final\"}"; + + let esplora_msg_err = r#"Electrum server error: "sendrawtransaction RPC error: {\"code\":-26,\"message\":\"non-final\"}""#; + + let err_msg = err.to_string(); + + assert!(err_msg == electrs_msg_err || err_msg == esplora_msg_err); let _ = bitcoin_core::generatetoaddress(990, &core_wallet_address.clone().unwrap())?; @@ -97,7 +105,7 @@ pub async fn execute() -> Result<()> { w1_transfer_to_w2(&client_config, &wallet1, &wallet2).await?; - println!("TV05 - Result as reported."); + println!("TV01 - Result as reported."); Ok(()) } \ No newline at end of file diff --git a/clients/tests/rust/src/utils.rs b/clients/tests/rust/src/utils.rs new file mode 100644 index 00000000..53f202ed --- /dev/null +++ b/clients/tests/rust/src/utils.rs @@ -0,0 +1,32 @@ +use std::{thread, time::Duration}; + +use anyhow::{Result, Ok}; +use mercuryrustlib::{client_config::ClientConfig,TokenResponse}; + +use crate::{bitcoin_core, electrs}; + +pub async fn handle_token_response(client_config: &ClientConfig, token_response: &TokenResponse) -> Result { + + let token_id = token_response.token_id.clone(); + + if token_response.payment_method == "onchain" { + + let remaining_blocks = token_response.confirmation_target; + let deposit_address = token_response.deposit_address.clone().unwrap(); + + let amount = token_response.fee as u32; + + let _ = bitcoin_core::sendtoaddress(amount, &deposit_address)?; + + let core_wallet_address = bitcoin_core::getnewaddress()?; + let _ = bitcoin_core::generatetoaddress(remaining_blocks as u32, &core_wallet_address)?; + + let mut is_tx_indexed = false; + while !is_tx_indexed { + is_tx_indexed = electrs::check_address(client_config, &deposit_address, amount).await?; + thread::sleep(Duration::from_secs(1)); + } + } + + return Ok(token_id); +} diff --git a/clients/tests/web/package-lock.json b/clients/tests/web/package-lock.json index ea1ed337..c8520810 100644 --- a/clients/tests/web/package-lock.json +++ b/clients/tests/web/package-lock.json @@ -11,6 +11,7 @@ "axios": "^1.7.2", "body-parser": "^1.20.2", "bolt11": "^1.4.1", + "chai": "^5.1.2", "cors": "^2.8.5", "express": "^4.19.2", "install": "^0.13.0", @@ -1323,7 +1324,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, "engines": { "node": ">=12" } @@ -1630,10 +1630,9 @@ } }, "node_modules/chai": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", - "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", - "dev": true, + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -1666,7 +1665,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, "engines": { "node": ">= 16" } @@ -1939,7 +1937,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, "engines": { "node": ">=6" } @@ -2477,7 +2474,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, "engines": { "node": "*" } @@ -2929,7 +2925,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", - "dev": true, "dependencies": { "get-func-name": "^2.0.1" } @@ -5736,7 +5731,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", - "dev": true, "engines": { "node": ">= 14.16" } diff --git a/clients/tests/web/package.json b/clients/tests/web/package.json index 66de7bfe..ebfd10ee 100644 --- a/clients/tests/web/package.json +++ b/clients/tests/web/package.json @@ -15,6 +15,7 @@ "axios": "^1.7.2", "body-parser": "^1.20.2", "bolt11": "^1.4.1", + "chai": "^5.1.2", "cors": "^2.8.5", "express": "^4.19.2", "install": "^0.13.0", diff --git a/clients/tests/web/test-utils.js b/clients/tests/web/test-utils.js index 9b953c45..2c26022a 100644 --- a/clients/tests/web/test-utils.js +++ b/clients/tests/web/test-utils.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import clientConfig from './ClientConfig.js'; const generateBlocks = async (blocks) => { const body = { @@ -122,4 +123,66 @@ const decodeInvoice = async (paymentRequest) => { } } -export { generateBlocks, depositCoin, sleep, generateInvoice, payInvoice, payHoldInvoice, settleInvoice, decodeInvoice }; +const checkDepositsConfirmation = async (address) => { + let allDepositsConfirmed = false; + const startTime = Date.now(); + const timeoutDuration = 60000; // 1 minute in milliseconds + + while (Date.now() - startTime < timeoutDuration) { + + const response = await axios.get(`${clientConfig.esploraServer}/api/address/${address}/utxo`); + const transactions = response.data; + + // Check if all transactions are confirmed + const allConfirmed = transactions.every(tx => tx.status.confirmed === true); + + if (allConfirmed) { + allDepositsConfirmed = true; + console.log('All deposits confirmed!'); + break; + } + + // Wait for 2 seconds before next check + await new Promise(resolve => setTimeout(resolve, 2000)); + } + + return allDepositsConfirmed; +} + +const handleTokenResponse = async (tokenResponse) => { + + let tokenId = tokenResponse.token_id; + + if (tokenResponse.payment_method == "onchain") { + + console.log(">>>>>>>>>>>> Onchain payment method"); + + let remainingBlocks = tokenResponse.confirmation_target; + let depositAddress = tokenResponse.deposit_address; + + let amount = tokenResponse.fee; + + await depositCoin(depositAddress, amount); + + await generateBlocks(remainingBlocks); + + await checkDepositsConfirmation(depositAddress); + + console.log(">>>>>>>>>>>> Deposit confirmed"); + } + + return tokenId; +} + +export { + generateBlocks, + depositCoin, + sleep, + generateInvoice, + payInvoice, + payHoldInvoice, + settleInvoice, + decodeInvoice, + handleTokenResponse, + checkDepositsConfirmation +}; diff --git a/clients/tests/web/test/ta02-duplicate-deposits.test.js b/clients/tests/web/test/ta02-duplicate-deposits.test.js index f577d4f9..d085f098 100644 --- a/clients/tests/web/test/ta02-duplicate-deposits.test.js +++ b/clients/tests/web/test/ta02-duplicate-deposits.test.js @@ -3,7 +3,7 @@ import { describe, test, expect } from "vitest"; import CoinStatus from 'mercuryweblib/coin_enum.js'; import clientConfig from '../ClientConfig.js'; import mercuryweblib from 'mercuryweblib'; -import { generateBlocks, depositCoin } from '../test-utils.js'; +import { generateBlocks, depositCoin, handleTokenResponse } from '../test-utils.js'; describe('TA02 - Duplicated Deposits', () => { test("withdraw flow", async () => { @@ -14,11 +14,13 @@ describe('TA02 - Duplicated Deposits', () => { let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb02_1"); let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb02_1"); - await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse = await mercuryweblib.newToken(clientConfig, wallet1.name); + + let token_id = await handleTokenResponse(tokenResponse); const amount = 1000; - let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id, amount); const statechainId = result.statechain_id; @@ -111,11 +113,13 @@ describe('TA02 - Duplicated Deposits', () => { let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb02_2"); let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb02_2"); - await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse = await mercuryweblib.newToken(clientConfig, wallet1.name); + + let token_id = await handleTokenResponse(tokenResponse); const amount = 1000; - let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id, amount); const statechainId = result.statechain_id; @@ -191,23 +195,23 @@ describe('TA02 - Duplicated Deposits', () => { coin.status == CoinStatus.TRANSFERRED ); - duplicatedCoin = coins.find(coin => + let invalidatedCoin = coins.find(coin => coin.statechain_id === statechainId && - coin.status == CoinStatus.DUPLICATED + coin.status == CoinStatus.INVALIDATED ); expect(transferredCoin).to.not.be.null; expect(transferredCoin.duplicate_index).to.equal(0); - expect(duplicatedCoin).to.not.be.null; - expect(duplicatedCoin.duplicate_index).to.equal(1); + expect(invalidatedCoin).to.not.be.null; + expect(invalidatedCoin.duplicate_index).to.equal(1); try { const withdrawAddress = "bcrt1qn5948np2j8t68xgpceneg3ua4wcwhwrsqj8scv"; await mercuryweblib.withdrawCoin(clientConfig, wallet1.name, statechainId, withdrawAddress, null, 1); } catch (error) { - // expect(error.message).to.equal("Signature does not match authentication key."); - expect(error.message).to.equal("Request failed with status code 401"); + expect(error.message).to.equal("No duplicated coins associated with this statechain ID and index 1 were found"); + // expect(error.message).to.equal("Request failed with status code 401"); } }); }, 100000); \ No newline at end of file diff --git a/clients/tests/web/test/ta03_multiple_deposits.test.js b/clients/tests/web/test/ta03_multiple_deposits.test.js new file mode 100644 index 00000000..668f5048 --- /dev/null +++ b/clients/tests/web/test/ta03_multiple_deposits.test.js @@ -0,0 +1,1173 @@ +import { describe, test, expect } from "vitest"; + +import axios from 'axios'; +import CoinStatus from 'mercuryweblib/coin_enum.js'; +import clientConfig from '../ClientConfig.js'; +import mercuryweblib from 'mercuryweblib'; +import { generateBlocks, depositCoin, checkDepositsConfirmation, handleTokenResponse } from '../test-utils.js'; + +async function validateBackupTransactions(backupTransactions, interval) { + let currentTxid = null; + let currentVout = null; + let currentTxN = 0; + let previousLockTime = 0; + + try { + for (const backupTx of backupTransactions) { + // Assuming these are async functions that return promises + const outpoint = await mercuryweblib.getPreviousOutpoint(backupTx); + const currentLockTime = await mercuryweblib.getBlockheight(backupTx); + + if (currentTxid !== null && currentVout !== null) { + // Validate transaction chain + expect(currentTxid).to.equal(outpoint.txid); + expect(currentVout).to.equal(outpoint.vout); + expect(currentLockTime).to.equal(previousLockTime - interval); + expect(backupTx.tx_n).to.be.greaterThan(currentTxN); + } + + // Update current values + currentTxid = outpoint.txid; + currentVout = outpoint.vout; + currentTxN = backupTx.tx_n; + previousLockTime = currentLockTime; + } + + return true; // Success case + + } catch (error) { + throw error; // Re-throw the error for handling by the caller + } +} + +describe('TA03 - Multiple Deposits', () => { + test("basic workflow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb03_1"); + localStorage.removeItem("mercury-layer:wallet2_tb03_1"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb03_1"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb03_1"); + + let tokenResponse = await mercuryweblib.newToken(clientConfig, wallet1.name); + + let token_id = await handleTokenResponse(tokenResponse); + + let amount = 1000; + + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id, amount); + + const statechainId = result.statechain_id; + + let isDepositInMempool = false; + let isDepositConfirmed = false; + let areBlocksGenerated = false; + + await depositCoin(result.deposit_address, amount); + + while (!isDepositConfirmed) { + + const coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool) { + isDepositInMempool = true; + } else if (coin.statechain_id === statechainId && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed = true; + break; + } + } + + if (isDepositInMempool && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + amount = 2000; + + await depositCoin(result.deposit_address, amount); + + while (true) { + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + let duplicatedCoin = coins.find(coin => coin.statechain_id === statechainId && coin.status === CoinStatus.DUPLICATED && coin.duplicate_index == 1); + if (duplicatedCoin) { + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + amount = 3000; + + await depositCoin(result.deposit_address, amount); + + while (true) { + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + let duplicatedCoin = coins.find(coin => coin.statechain_id === statechainId && coin.status === CoinStatus.DUPLICATED && coin.duplicate_index == 2); + if (duplicatedCoin) { + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + amount = 3000; + + await depositCoin(result.deposit_address, amount); + + while (true) { + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + let duplicatedCoin = coins.find(coin => coin.statechain_id === statechainId && coin.status === CoinStatus.DUPLICATED && coin.duplicate_index == 3); + if (duplicatedCoin) { + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + const newCoin = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.CONFIRMED && + coin.duplicate_index == 0 + ); + + const duplicatedCoin1 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 1 + ); + + const duplicatedCoin2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 2 + ); + + const duplicatedCoin3 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 3 + ); + + expect(newCoin).to.not.be.undefined; + expect(duplicatedCoin1).to.not.be.undefined; + expect(duplicatedCoin2).to.not.be.undefined; + expect(duplicatedCoin3).to.not.be.undefined; + + await generateBlocks(clientConfig.confirmationTarget); + + const allDepositsConfirmed = await checkDepositsConfirmation(newCoin.aggregated_address); + + expect(allDepositsConfirmed).to.be.true; + + let transferAddress = await mercuryweblib.newTransferAddress(wallet2.name); + + let duplicatedIndexes = [1, 3]; + + result = await mercuryweblib.transferSend( + clientConfig, + wallet1.name, + statechainId, + transferAddress.transfer_receive, + true, + null, + duplicatedIndexes + ); + expect(result).to.have.property('statechain_id'); + + const jsonData = localStorage.getItem(`mercury-layer:${wallet1.name}-${statechainId}`); + let backupTxs = JSON.parse(jsonData); + + expect(backupTxs.length).to.equal(6); + + const split_backup_transactions = mercuryweblib.splitBackupTransactions(backupTxs); + + const infoConfig = await mercuryweblib.infoConfig(clientConfig); + + for (const [index, bkTxs] of split_backup_transactions.entries()) { + if (index === 0) { + const firstBkpTx = bkTxs[0]; + expect(firstBkpTx).to.not.be.undefined; + + const firstBackupOutpoint = await mercuryweblib.getPreviousOutpoint(firstBkpTx); + expect(firstBackupOutpoint).to.not.be.undefined; + expect(newCoin.utxo_txid).to.be.equals(firstBackupOutpoint.txid); + expect(newCoin.utxo_vout).to.be.equals(firstBackupOutpoint.vout); + } + + await validateBackupTransactions(bkTxs, infoConfig.interval); + } + + let transferReceiveResult = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + expect(transferReceiveResult.receivedStatechainIds).contains(newCoin.statechain_id); + expect(transferReceiveResult.receivedStatechainIds.length).to.equal(1); + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + const transferredCoin0 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.TRANSFERRED && + coin.duplicate_index == 0 + ); + + const transferredCoin1 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.TRANSFERRED && + coin.duplicate_index == 1 + ); + + const invalidatedCoin = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.INVALIDATED && + coin.duplicate_index == 2 + ); + + const transferredCoin3 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.TRANSFERRED && + coin.duplicate_index == 3 + ); + + expect(transferredCoin0).to.not.be.undefined; + expect(transferredCoin1).to.not.be.undefined; + expect(invalidatedCoin).to.not.be.undefined; + expect(transferredCoin3).to.not.be.undefined; + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet2.name); + + const confirmedCoinWallet2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.CONFIRMED && + coin.duplicate_index == 0 + ); + + const duplicatedCoin1Wallet2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 1 + ); + + const duplicatedCoin2Wallet2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 2 + ); + + expect(confirmedCoinWallet2).to.not.be.undefined; + expect(duplicatedCoin1Wallet2).to.not.be.undefined; + expect(duplicatedCoin2Wallet2).to.not.be.undefined; + + const toAddress = "bcrt1qn5948np2j8t68xgpceneg3ua4wcwhwrsqj8scv"; + + let txid = await mercuryweblib.withdrawCoin(clientConfig, wallet2.name, statechainId, toAddress, null, 1); + expect(txid).to.be.string; + + txid = await mercuryweblib.withdrawCoin(clientConfig, wallet2.name, statechainId, toAddress, null, 2); + expect(txid).to.be.string; + + txid = await mercuryweblib.withdrawCoin(clientConfig, wallet2.name, statechainId, toAddress, null, null); + expect(txid).to.be.string; + }); +}, 5000000000); + +describe('TA03 - Multiple Deposits', () => { + test("resend workflow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb03_2"); + localStorage.removeItem("mercury-layer:wallet2_tb03_2"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb03_2"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb03_2"); + + let tokenResponse = await mercuryweblib.newToken(clientConfig, wallet1.name); + + let token_id = await handleTokenResponse(tokenResponse); + + let amount = 1000; + + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id, amount); + + const statechainId = result.statechain_id; + + let isDepositInMempool = false; + let isDepositConfirmed = false; + let areBlocksGenerated = false; + + await depositCoin(result.deposit_address, amount); + + while (!isDepositConfirmed) { + + const coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool) { + isDepositInMempool = true; + } else if (coin.statechain_id === statechainId && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed = true; + break; + } + } + + if (isDepositInMempool && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + amount = 2000; + + await depositCoin(result.deposit_address, amount); + + while (true) { + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + let duplicatedCoin = coins.find(coin => coin.statechain_id === statechainId && coin.status === CoinStatus.DUPLICATED && coin.duplicate_index == 1); + if (duplicatedCoin) { + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + amount = 3000; + + await depositCoin(result.deposit_address, amount); + + while (true) { + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + let duplicatedCoin = coins.find(coin => coin.statechain_id === statechainId && coin.status === CoinStatus.DUPLICATED && coin.duplicate_index == 2); + if (duplicatedCoin) { + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + amount = 3000; + + await depositCoin(result.deposit_address, amount); + + while (true) { + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + let duplicatedCoin = coins.find(coin => coin.statechain_id === statechainId && coin.status === CoinStatus.DUPLICATED && coin.duplicate_index == 3); + if (duplicatedCoin) { + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + const newCoin = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.CONFIRMED && + coin.duplicate_index == 0 + ); + + const duplicatedCoin1 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 1 + ); + + const duplicatedCoin2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 2 + ); + + const duplicatedCoin3 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 3 + ); + + expect(newCoin).to.not.be.undefined; + expect(duplicatedCoin1).to.not.be.undefined; + expect(duplicatedCoin2).to.not.be.undefined; + expect(duplicatedCoin3).to.not.be.undefined; + + await generateBlocks(clientConfig.confirmationTarget); + + const allDepositsConfirmed = await checkDepositsConfirmation(newCoin.aggregated_address); + + expect(allDepositsConfirmed).to.be.true; + + let transferAddress = await mercuryweblib.newTransferAddress(wallet2.name); + + let duplicatedIndexes = [1, 3]; + + result = await mercuryweblib.transferSend( + clientConfig, + wallet1.name, + statechainId, + transferAddress.transfer_receive, + true, + null, + duplicatedIndexes + ); + expect(result).to.have.property('statechain_id'); + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + let coin0 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.IN_TRANSFER && + coin.duplicate_index == 0 + ); + + let coin1 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.IN_TRANSFER && + coin.duplicate_index == 1 + ); + + let coin2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 2 + ); + + let coin3 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.IN_TRANSFER && + coin.duplicate_index == 3 + ); + + expect(coin0).to.not.be.undefined; + expect(coin1).to.not.be.undefined; + expect(coin2).to.not.be.undefined; + expect(coin3).to.not.be.undefined; + + duplicatedIndexes = [2]; + + result = await mercuryweblib.transferSend( + clientConfig, + wallet1.name, + statechainId, + transferAddress.transfer_receive, + true, + null, + duplicatedIndexes + ); + expect(result).to.have.property('statechain_id'); + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + coin0 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.IN_TRANSFER && + coin.duplicate_index == 0 + ); + + coin1 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.IN_TRANSFER && + coin.duplicate_index == 1 + ); + + coin2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.IN_TRANSFER && + coin.duplicate_index == 2 + ); + + coin3 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.IN_TRANSFER && + coin.duplicate_index == 3 + ); + + expect(coin0).to.not.be.undefined; + expect(coin1).to.not.be.undefined; + expect(coin2).to.not.be.undefined; + expect(coin3).to.not.be.undefined; + + let transferReceiveResult = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + expect(transferReceiveResult.receivedStatechainIds).contains(newCoin.statechain_id); + expect(transferReceiveResult.receivedStatechainIds.length).to.equal(1); + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet2.name); + + coin0 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.CONFIRMED && + coin.duplicate_index == 0 + ); + + coin1 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 1 + ); + + coin2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 2 + ); + + coin3 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 3 + ); + + expect(coin0).to.not.be.undefined; + expect(coin1).to.not.be.undefined; + expect(coin2).to.not.be.undefined; + expect(coin3).to.not.be.undefined; + + expect(newCoin.utxo_txid).to.be.equals(coin0.utxo_txid); + expect(newCoin.utxo_vout).to.be.equals(coin0.utxo_vout); + + const toAddress = "bcrt1qn5948np2j8t68xgpceneg3ua4wcwhwrsqj8scv"; + + let txid = await mercuryweblib.withdrawCoin(clientConfig, wallet2.name, statechainId, toAddress, null, 1); + expect(txid).to.be.string; + + txid = await mercuryweblib.withdrawCoin(clientConfig, wallet2.name, statechainId, toAddress, null, 2); + expect(txid).to.be.string; + + txid = await mercuryweblib.withdrawCoin(clientConfig, wallet2.name, statechainId, toAddress, null, 3); + expect(txid).to.be.string; + + txid = await mercuryweblib.withdrawCoin(clientConfig, wallet2.name, statechainId, toAddress, null, null); + expect(txid).to.be.string; + + }); +}, 5000000000); + +describe('TA03 - Multiple Deposits', () => { + test("resend workflow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb03_3"); + localStorage.removeItem("mercury-layer:wallet2_tb03_3"); + localStorage.removeItem("mercury-layer:wallet3_tb03_3"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb03_3"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb03_3"); + let wallet3 = await mercuryweblib.createWallet(clientConfig, "wallet3_tb03_3"); + + let tokenResponse = await mercuryweblib.newToken(clientConfig, wallet1.name); + + let token_id = await handleTokenResponse(tokenResponse); + + let amount = 1000; + + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id, amount); + + const statechainId = result.statechain_id; + + let isDepositInMempool = false; + let isDepositConfirmed = false; + let areBlocksGenerated = false; + + await depositCoin(result.deposit_address, amount); + + while (!isDepositConfirmed) { + + const coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool) { + isDepositInMempool = true; + } else if (coin.statechain_id === statechainId && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed = true; + break; + } + } + + if (isDepositInMempool && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + amount = 2000; + + await depositCoin(result.deposit_address, amount); + + while (true) { + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + let duplicatedCoin = coins.find(coin => coin.statechain_id === statechainId && coin.status === CoinStatus.DUPLICATED && coin.duplicate_index == 1); + if (duplicatedCoin) { + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + amount = 3000; + + await depositCoin(result.deposit_address, amount); + + while (true) { + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + let duplicatedCoin = coins.find(coin => coin.statechain_id === statechainId && coin.status === CoinStatus.DUPLICATED && coin.duplicate_index == 2); + if (duplicatedCoin) { + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + amount = 3000; + + await depositCoin(result.deposit_address, amount); + + while (true) { + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + let duplicatedCoin = coins.find(coin => coin.statechain_id === statechainId && coin.status === CoinStatus.DUPLICATED && coin.duplicate_index == 3); + if (duplicatedCoin) { + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + const newCoin = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.CONFIRMED && + coin.duplicate_index == 0 + ); + + const duplicatedCoin1 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 1 + ); + + const duplicatedCoin2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 2 + ); + + const duplicatedCoin3 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 3 + ); + + expect(newCoin).to.not.be.undefined; + expect(duplicatedCoin1).to.not.be.undefined; + expect(duplicatedCoin2).to.not.be.undefined; + expect(duplicatedCoin3).to.not.be.undefined; + + await generateBlocks(clientConfig.confirmationTarget); + + const allDepositsConfirmed = await checkDepositsConfirmation(newCoin.aggregated_address); + + expect(allDepositsConfirmed).to.be.true; + + let transferAddress = await mercuryweblib.newTransferAddress(wallet2.name); + + let duplicatedIndexes = [1, 2, 3]; + + result = await mercuryweblib.transferSend( + clientConfig, + wallet1.name, + statechainId, + transferAddress.transfer_receive, + true, + null, + duplicatedIndexes + ); + expect(result).to.have.property('statechain_id'); + + let transferReceiveResult = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + expect(transferReceiveResult.receivedStatechainIds).contains(newCoin.statechain_id); + expect(transferReceiveResult.receivedStatechainIds.length).to.equal(1); + + transferAddress = await mercuryweblib.newTransferAddress(wallet3.name); + + duplicatedIndexes = [1, 2, 3]; + + result = await mercuryweblib.transferSend( + clientConfig, + wallet2.name, + statechainId, + transferAddress.transfer_receive, + true, + null, + duplicatedIndexes + ); + expect(result).to.have.property('statechain_id'); + + transferReceiveResult = await mercuryweblib.transferReceive(clientConfig, wallet3.name); + expect(transferReceiveResult.receivedStatechainIds).contains(newCoin.statechain_id); + expect(transferReceiveResult.receivedStatechainIds.length).to.equal(1); + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet3.name); + + let coin0 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.CONFIRMED && + coin.duplicate_index == 0 + ); + + let coin1 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 1 + ); + + let coin2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 2 + ); + + let coin3 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 3 + ); + + expect(coin0).to.not.be.undefined; + expect(coin1).to.not.be.undefined; + expect(coin2).to.not.be.undefined; + expect(coin3).to.not.be.undefined; + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet2.name); + + coin0 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.TRANSFERRED && + coin.duplicate_index == 0 + ); + + coin1 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.TRANSFERRED && + coin.duplicate_index == 1 + ); + + coin2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.TRANSFERRED && + coin.duplicate_index == 2 + ); + + coin3 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.TRANSFERRED && + coin.duplicate_index == 3 + ); + + expect(coin0).to.not.be.undefined; + expect(coin1).to.not.be.undefined; + expect(coin2).to.not.be.undefined; + expect(coin3).to.not.be.undefined; + + const toAddress = "bcrt1qn5948np2j8t68xgpceneg3ua4wcwhwrsqj8scv"; + + let txid = await mercuryweblib.withdrawCoin(clientConfig, wallet3.name, statechainId, toAddress, null, 1); + expect(txid).to.be.string; + + txid = await mercuryweblib.withdrawCoin(clientConfig, wallet3.name, statechainId, toAddress, null, 2); + expect(txid).to.be.string; + + txid = await mercuryweblib.withdrawCoin(clientConfig, wallet3.name, statechainId, toAddress, null, 3); + expect(txid).to.be.string; + + txid = await mercuryweblib.withdrawCoin(clientConfig, wallet3.name, statechainId, toAddress, null, null); + expect(txid).to.be.string; + + }); +}, 5000000000); + +describe('TA03 - Multiple Deposits', () => { + test("send to itself workflow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb03_4"); + localStorage.removeItem("mercury-layer:wallet2_tb03_4"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb03_4"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb03_4"); + + let tokenResponse = await mercuryweblib.newToken(clientConfig, wallet1.name); + + let token_id = await handleTokenResponse(tokenResponse); + + let amount = 1000; + + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id, amount); + + const statechainId = result.statechain_id; + + let isDepositInMempool = false; + let isDepositConfirmed = false; + let areBlocksGenerated = false; + + await depositCoin(result.deposit_address, amount); + + while (!isDepositConfirmed) { + + const coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool) { + isDepositInMempool = true; + } else if (coin.statechain_id === statechainId && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed = true; + break; + } + } + + if (isDepositInMempool && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + amount = 2000; + + await depositCoin(result.deposit_address, amount); + + while (true) { + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + let duplicatedCoin = coins.find(coin => coin.statechain_id === statechainId && coin.status === CoinStatus.DUPLICATED && coin.duplicate_index == 1); + if (duplicatedCoin) { + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + amount = 3000; + + await depositCoin(result.deposit_address, amount); + + while (true) { + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + let duplicatedCoin = coins.find(coin => coin.statechain_id === statechainId && coin.status === CoinStatus.DUPLICATED && coin.duplicate_index == 2); + if (duplicatedCoin) { + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + amount = 3000; + + await depositCoin(result.deposit_address, amount); + + while (true) { + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + let duplicatedCoin = coins.find(coin => coin.statechain_id === statechainId && coin.status === CoinStatus.DUPLICATED && coin.duplicate_index == 3); + if (duplicatedCoin) { + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + const newCoin = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.CONFIRMED && + coin.duplicate_index == 0 + ); + + let duplicatedCoin1 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 1 + ); + + let duplicatedCoin2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 2 + ); + + const duplicatedCoin3 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 3 + ); + + expect(newCoin).to.not.be.undefined; + expect(duplicatedCoin1).to.not.be.undefined; + expect(duplicatedCoin2).to.not.be.undefined; + expect(duplicatedCoin3).to.not.be.undefined; + + await generateBlocks(clientConfig.confirmationTarget); + + const allDepositsConfirmed = await checkDepositsConfirmation(newCoin.aggregated_address); + + expect(allDepositsConfirmed).to.be.true; + + let transferAddress = await mercuryweblib.newTransferAddress(wallet1.name); + + let duplicatedIndexes = [1, 3]; + + result = await mercuryweblib.transferSend( + clientConfig, + wallet1.name, + statechainId, + transferAddress.transfer_receive, + true, + null, + duplicatedIndexes + ); + expect(result).to.have.property('statechain_id'); + + let transferReceiveResult = await mercuryweblib.transferReceive(clientConfig, wallet1.name); + expect(transferReceiveResult.receivedStatechainIds).contains(newCoin.statechain_id); + expect(transferReceiveResult.receivedStatechainIds.length).to.equal(1); + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + let transferredCoin0 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.TRANSFERRED && + coin.duplicate_index == 0 + ); + + let transferredCoin1 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.TRANSFERRED && + coin.duplicate_index == 1 + ); + + let invalidatedCoin2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.INVALIDATED && + coin.duplicate_index == 2 + ); + + let transferredCoin3 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.TRANSFERRED && + coin.duplicate_index == 3 + ); + + expect(transferredCoin0).to.not.be.undefined; + expect(transferredCoin1).to.not.be.undefined; + expect(invalidatedCoin2).to.not.be.undefined; + expect(transferredCoin3).to.not.be.undefined; + + let confirmedCoin0 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.CONFIRMED && + coin.duplicate_index == 0 + ); + + duplicatedCoin1 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 1 + ); + + duplicatedCoin2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 2 + ); + + expect(confirmedCoin0).to.not.be.undefined; + expect(duplicatedCoin1).to.not.be.undefined; + expect(duplicatedCoin2).to.not.be.undefined; + + transferAddress = await mercuryweblib.newTransferAddress(wallet2.name); + + duplicatedIndexes = [1, 2]; + + result = await mercuryweblib.transferSend( + clientConfig, + wallet1.name, + statechainId, + transferAddress.transfer_receive, + true, + null, + duplicatedIndexes + ); + expect(result).to.have.property('statechain_id'); + + transferReceiveResult = await mercuryweblib.transferReceive(clientConfig, wallet2.name); + expect(transferReceiveResult.receivedStatechainIds).contains(newCoin.statechain_id); + expect(transferReceiveResult.receivedStatechainIds.length).to.equal(1); + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet2.name); + + confirmedCoin0 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.CONFIRMED && + coin.duplicate_index == 0 + ); + + duplicatedCoin1 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 1 + ); + + duplicatedCoin2 = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.duplicate_index == 2 + ); + + expect(confirmedCoin0).to.not.be.undefined; + expect(duplicatedCoin1).to.not.be.undefined; + expect(duplicatedCoin2).to.not.be.undefined; + + const toAddress = "bcrt1qn5948np2j8t68xgpceneg3ua4wcwhwrsqj8scv"; + + let txid = await mercuryweblib.withdrawCoin(clientConfig, wallet2.name, statechainId, toAddress, null, null); + expect(txid).to.be.string; + + txid = await mercuryweblib.withdrawCoin(clientConfig, wallet2.name, statechainId, toAddress, null, 1); + expect(txid).to.be.string; + + txid = await mercuryweblib.withdrawCoin(clientConfig, wallet2.name, statechainId, toAddress, null, 2); + expect(txid).to.be.string; + }); +}, 5000000000); + +describe('TA03 - Multiple Deposits', () => { + test("bsend unconfirmed duplicated workflow", async () => { + + localStorage.removeItem("mercury-layer:wallet1_tb03_5"); + localStorage.removeItem("mercury-layer:wallet2_tb03_5"); + + let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb03_5"); + let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb03_5"); + + let tokenResponse = await mercuryweblib.newToken(clientConfig, wallet1.name); + + let token_id = await handleTokenResponse(tokenResponse); + + let amount = 1000; + + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id, amount); + + const statechainId = result.statechain_id; + + let isDepositInMempool = false; + let isDepositConfirmed = false; + let areBlocksGenerated = false; + + await depositCoin(result.deposit_address, amount); + + while (!isDepositConfirmed) { + + const coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + for (let coin of coins) { + if (coin.statechain_id === statechainId && coin.status === CoinStatus.IN_MEMPOOL && !isDepositInMempool) { + isDepositInMempool = true; + } else if (coin.statechain_id === statechainId && coin.status === CoinStatus.CONFIRMED) { + isDepositConfirmed = true; + break; + } + } + + if (isDepositInMempool && !areBlocksGenerated) { + areBlocksGenerated = true; + await generateBlocks(clientConfig.confirmationTarget); + } + + await new Promise(r => setTimeout(r, 1000)); + } + + amount = 1000; + + await depositCoin(result.deposit_address, amount); + + await generateBlocks(clientConfig.confirmationTarget); + + while (true) { + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + let duplicatedCoin = coins.find(coin => coin.statechain_id === statechainId && coin.status === CoinStatus.DUPLICATED && coin.duplicate_index == 1); + if (duplicatedCoin) { + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + const newCoin = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.CONFIRMED && + coin.duplicate_index == 0 + ); + + const confirmedDuplicatedCoin = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.amount == 1000 + ); + + let allDepositsConfirmed = await checkDepositsConfirmation(newCoin.aggregated_address); + + expect(allDepositsConfirmed).to.be.true; + + amount = 2000; + + await depositCoin(result.deposit_address, amount); + + while (true) { + let coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + let duplicatedCoin = coins.find(coin => coin.statechain_id === statechainId && coin.status === CoinStatus.DUPLICATED && coin.duplicate_index == 2); + if (duplicatedCoin) { + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + + coins = await mercuryweblib.listStatecoins(clientConfig, wallet1.name); + + const unconfirmedDuplicatedCoin = coins.find(coin => + coin.statechain_id === statechainId && + coin.status == CoinStatus.DUPLICATED && + coin.amount == 2000 + ); + + expect(newCoin).to.not.be.undefined; + expect(confirmedDuplicatedCoin).to.not.be.undefined; + expect(unconfirmedDuplicatedCoin).to.not.be.undefined; + + let transferAddress = await mercuryweblib.newTransferAddress(wallet2.name); + + let duplicatedIndexes = [1, 2]; + + try { + result = await mercuryweblib.transferSend( + clientConfig, + wallet1.name, + statechainId, + transferAddress.transfer_receive, + true, + null, + duplicatedIndexes + ); + } catch (error) { + expect(error.message).to.equal(`The coin with duplicated index ${unconfirmedDuplicatedCoin.duplicate_index} has not yet been confirmed. This transfer cannot be performed.`); + } + + await generateBlocks(clientConfig.confirmationTarget); + + allDepositsConfirmed = await checkDepositsConfirmation(newCoin.aggregated_address); + + expect(allDepositsConfirmed).to.be.true; + + result = await mercuryweblib.transferSend( + clientConfig, + wallet1.name, + statechainId, + transferAddress.transfer_receive, + true, + null, + duplicatedIndexes + ); + expect(result).to.have.property('statechain_id'); + + }); +}, 5000000000); \ No newline at end of file diff --git a/clients/tests/web/test/tb01-simple-transfer.test.js b/clients/tests/web/test/tb01-simple-transfer.test.js index 50188b74..8ad3933b 100644 --- a/clients/tests/web/test/tb01-simple-transfer.test.js +++ b/clients/tests/web/test/tb01-simple-transfer.test.js @@ -3,7 +3,7 @@ import { describe, test, expect } from "vitest"; import CoinStatus from 'mercuryweblib/coin_enum.js'; import clientConfig from '../ClientConfig.js'; import mercuryweblib from 'mercuryweblib'; -import { generateBlocks, depositCoin } from '../test-utils.js'; +import { generateBlocks, depositCoin, handleTokenResponse } from '../test-utils.js'; describe('TB01 - Simple Transfer', () => { test("expected flow", async () => { @@ -14,11 +14,13 @@ describe('TB01 - Simple Transfer', () => { let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb01"); let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb01"); - await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse = await mercuryweblib.newToken(clientConfig, wallet1.name); + + let token_id = await handleTokenResponse(tokenResponse); const amount = 1000; - let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id, amount); const statechainId = result.statechain_id; diff --git a/clients/tests/web/test/tb03-simple-atomic-transfer.test.js b/clients/tests/web/test/tb03-simple-atomic-transfer.test.js index e4de80c6..18cf6306 100644 --- a/clients/tests/web/test/tb03-simple-atomic-transfer.test.js +++ b/clients/tests/web/test/tb03-simple-atomic-transfer.test.js @@ -3,7 +3,7 @@ import { describe, test, expect } from "vitest"; import CoinStatus from 'mercuryweblib/coin_enum.js'; import clientConfig from '../ClientConfig.js'; import mercuryweblib from 'mercuryweblib'; -import { generateBlocks, depositCoin, sleep } from '../test-utils.js'; +import { generateBlocks, depositCoin, sleep, handleTokenResponse } from '../test-utils.js'; describe('TB03 - Simple Atomic Transfer', () => { test("expected flow", async () => { @@ -18,13 +18,16 @@ describe('TB03 - Simple Atomic Transfer', () => { let wallet3 = await mercuryweblib.createWallet(clientConfig, "wallet3_tb03"); let wallet4 = await mercuryweblib.createWallet(clientConfig, "wallet4_tb03"); - await mercuryweblib.newToken(clientConfig, wallet1.name); - await mercuryweblib.newToken(clientConfig, wallet2.name); + let tokenResponse1 = await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse2 = await mercuryweblib.newToken(clientConfig, wallet2.name); + + let token_id1 = await handleTokenResponse(tokenResponse1); + let token_id2 = await handleTokenResponse(tokenResponse2); const amount = 1000; - let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); - let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id1, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, token_id2, amount); const statechainId1 = result1.statechain_id; const statechainId2 = result2.statechain_id; @@ -110,13 +113,16 @@ describe('TB03 - Atomic swap with second batchid missing', () => { let wallet3 = await mercuryweblib.createWallet(clientConfig, "wallet3_tb03"); let wallet4 = await mercuryweblib.createWallet(clientConfig, "wallet4_tb03"); - await mercuryweblib.newToken(clientConfig, wallet1.name); - await mercuryweblib.newToken(clientConfig, wallet2.name); + let tokenResponse1 = await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse2 = await mercuryweblib.newToken(clientConfig, wallet2.name); + + let token_id1 = await handleTokenResponse(tokenResponse1); + let token_id2 = await handleTokenResponse(tokenResponse2); const amount = 1000; - let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); - let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id1, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, token_id2, amount); const statechainId1 = result1.statechain_id; const statechainId2 = result2.statechain_id; @@ -202,13 +208,16 @@ describe('TB03 - Atomic swap without first batchid', () => { let wallet3 = await mercuryweblib.createWallet(clientConfig, "wallet3_tb03"); let wallet4 = await mercuryweblib.createWallet(clientConfig, "wallet4_tb03"); - await mercuryweblib.newToken(clientConfig, wallet1.name); - await mercuryweblib.newToken(clientConfig, wallet2.name); + let tokenResponse1 = await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse2 = await mercuryweblib.newToken(clientConfig, wallet2.name); + + let token_id1 = await handleTokenResponse(tokenResponse1); + let token_id2 = await handleTokenResponse(tokenResponse2); const amount = 1000; - let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); - let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id1, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, token_id2, amount); const statechainId1 = result1.statechain_id; const statechainId2 = result2.statechain_id; @@ -294,13 +303,16 @@ describe('TB03 - Atomic swap with timeout', () => { let wallet3 = await mercuryweblib.createWallet(clientConfig, "wallet3_tb03"); let wallet4 = await mercuryweblib.createWallet(clientConfig, "wallet4_tb03"); - await mercuryweblib.newToken(clientConfig, wallet1.name); - await mercuryweblib.newToken(clientConfig, wallet2.name); + let tokenResponse1 = await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse2 = await mercuryweblib.newToken(clientConfig, wallet2.name); + + let token_id1 = await handleTokenResponse(tokenResponse1); + let token_id2 = await handleTokenResponse(tokenResponse2); const amount = 1000; - let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); - let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id1, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, token_id2, amount); const statechainId1 = result1.statechain_id; const statechainId2 = result2.statechain_id; @@ -349,37 +361,31 @@ describe('TB03 - Atomic swap with timeout', () => { let toAddress3 = await mercuryweblib.newTransferAddress(wallet3.name, true); let toAddress4 = await mercuryweblib.newTransferAddress(wallet4.name); - await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId1, toAddress3.transfer_receive, false, toAddress3.batch_id); - await mercuryweblib.transferSend(clientConfig, wallet2.name, statechainId2, toAddress4.transfer_receive, false, toAddress3.batch_id); + // await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId1, toAddress3.transfer_receive, false, toAddress3.batch_id); + // await mercuryweblib.transferSend(clientConfig, wallet2.name, statechainId2, toAddress4.transfer_receive, false, toAddress3.batch_id); - let transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); + // // let transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); - expect(transferReceive3.isThereBatchLocked).toBe(true); + // // expect(transferReceive3.isThereBatchLocked).toBe(true); - await sleep(20000); + // // await sleep(20000); - let errorMessage; - console.error = (msg) => { - errorMessage = msg; - }; + // // let transferReceive4 = await mercuryweblib.transferReceive(clientConfig, wallet4.name); - let transferReceive4 = await mercuryweblib.transferReceive(clientConfig, wallet4.name); - - // Assert the captured error message - const expectedMessage = 'Failed to update transfer message'; - expect(errorMessage).contains(expectedMessage); + // // expect(transferReceive4.isThereBatchLocked).toBe(false); + // // expect(transferReceive4.receivedStatechainIds).to.include(statechainId2); - toAddress3 = await mercuryweblib.newTransferAddress(wallet3.name, true); - toAddress4 = await mercuryweblib.newTransferAddress(wallet4.name); + // // toAddress3 = await mercuryweblib.newTransferAddress(wallet3.name, true); + // // toAddress4 = await mercuryweblib.newTransferAddress(wallet4.name); await mercuryweblib.transferSend(clientConfig, wallet1.name, statechainId1, toAddress3.transfer_receive, false, toAddress3.batch_id); await mercuryweblib.transferSend(clientConfig, wallet2.name, statechainId2, toAddress4.transfer_receive, false, toAddress3.batch_id); - transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); + let transferReceive3 = await mercuryweblib.transferReceive(clientConfig, wallet3.name); expect(transferReceive3.isThereBatchLocked).toBe(true); - transferReceive4 = await mercuryweblib.transferReceive(clientConfig, wallet4.name); + let transferReceive4 = await mercuryweblib.transferReceive(clientConfig, wallet4.name); expect(transferReceive4.isThereBatchLocked).toBe(false); @@ -409,13 +415,16 @@ describe('TB03 - Atomic swap with first party steal', () => { let wallet3 = await mercuryweblib.createWallet(clientConfig, "wallet3_tb03"); let wallet4 = await mercuryweblib.createWallet(clientConfig, "wallet4_tb03"); - await mercuryweblib.newToken(clientConfig, wallet1.name); - await mercuryweblib.newToken(clientConfig, wallet2.name); + let tokenResponse1 = await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse2 = await mercuryweblib.newToken(clientConfig, wallet2.name); + + let token_id1 = await handleTokenResponse(tokenResponse1); + let token_id2 = await handleTokenResponse(tokenResponse2); const amount = 1000; - let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); - let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id1, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, token_id2, amount); const statechainId1 = result1.statechain_id; const statechainId2 = result2.statechain_id; @@ -518,13 +527,16 @@ describe('TB03 - Atomic swap with second party steal', () => { let wallet3 = await mercuryweblib.createWallet(clientConfig, "wallet3_tb03"); let wallet4 = await mercuryweblib.createWallet(clientConfig, "wallet4_tb03"); - await mercuryweblib.newToken(clientConfig, wallet1.name); - await mercuryweblib.newToken(clientConfig, wallet2.name); + let tokenResponse1 = await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse2 = await mercuryweblib.newToken(clientConfig, wallet2.name); + + let token_id1 = await handleTokenResponse(tokenResponse1); + let token_id2 = await handleTokenResponse(tokenResponse2); const amount = 1000; - let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); - let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id1, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, token_id2, amount); const statechainId1 = result1.statechain_id; const statechainId2 = result2.statechain_id; diff --git a/clients/tests/web/test/tb04-simple-lightning-latch.test.js b/clients/tests/web/test/tb04-simple-lightning-latch.test.js index e5b7c158..f5c12e27 100644 --- a/clients/tests/web/test/tb04-simple-lightning-latch.test.js +++ b/clients/tests/web/test/tb04-simple-lightning-latch.test.js @@ -3,7 +3,7 @@ import { describe, test, expect } from "vitest"; import CoinStatus from 'mercuryweblib/coin_enum.js'; import clientConfig from '../ClientConfig.js'; import mercuryweblib from 'mercuryweblib'; -import { generateBlocks, depositCoin, sleep, generateInvoice, payInvoice, payHoldInvoice, settleInvoice, decodeInvoice } from '../test-utils.js'; +import { generateBlocks, depositCoin, sleep, generateInvoice, payInvoice, payHoldInvoice, settleInvoice, decodeInvoice, handleTokenResponse } from '../test-utils.js'; async function sha256(preimage) { let buffer; @@ -44,11 +44,13 @@ describe('TB04 - Simple Lightning Latch', () => { let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb04"); let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb04"); - await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse = await mercuryweblib.newToken(clientConfig, wallet1.name); + + let token_id = await handleTokenResponse(tokenResponse); const amount = 1000; - let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id, amount); const statechainId = result.statechain_id; @@ -120,13 +122,16 @@ describe('TB04 - The sender tries to get the pre-image before the batch is unloc let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb04"); let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb04"); - await mercuryweblib.newToken(clientConfig, wallet1.name); - await mercuryweblib.newToken(clientConfig, wallet2.name); + let tokenResponse1 = await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse2 = await mercuryweblib.newToken(clientConfig, wallet2.name); + + let token_id1 = await handleTokenResponse(tokenResponse1); + let token_id2 = await handleTokenResponse(tokenResponse2); const amount = 1000; - let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); - let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id1, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, token_id2, amount); const statechainId1 = result1.statechain_id; const statechainId2 = result2.statechain_id; @@ -221,13 +226,16 @@ describe('TB04 - Statecoin sender can recover (resend their coin) after batch ti let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb04"); let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb04"); - await mercuryweblib.newToken(clientConfig, wallet1.name); - await mercuryweblib.newToken(clientConfig, wallet2.name); + let tokenResponse1 = await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse2 = await mercuryweblib.newToken(clientConfig, wallet2.name); + + let token_id1 = await handleTokenResponse(tokenResponse1); + let token_id2 = await handleTokenResponse(tokenResponse2); const amount = 1000; - let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); - let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, amount); + let result1 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id1, amount); + let result2 = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet2.name, token_id2, amount); const statechainId1 = result1.statechain_id; const statechainId2 = result2.statechain_id; @@ -332,11 +340,13 @@ describe('TB04 - Statecoin trade with invoice creation, payment and settlement', let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb04"); let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb04"); - await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse = await mercuryweblib.newToken(clientConfig, wallet1.name); + + let token_id = await handleTokenResponse(tokenResponse); const amount = 1000; - let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id, amount); const statechainId = result.statechain_id; @@ -410,11 +420,13 @@ describe('TB04 - Receiver tries to transfer invoice amount to another invoice be let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb04"); let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb04"); - await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse = await mercuryweblib.newToken(clientConfig, wallet1.name); + + let token_id = await handleTokenResponse(tokenResponse); const amount = 1000; - let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id, amount); const statechainId = result.statechain_id; @@ -501,11 +513,13 @@ describe('Statecoin sender sends coin without batch_id (receiver should still be let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb04"); let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb04"); - await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse = await mercuryweblib.newToken(clientConfig, wallet1.name); + + let token_id = await handleTokenResponse(tokenResponse); const amount = 1000; - let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id, amount); const statechainId = result.statechain_id; @@ -581,11 +595,13 @@ describe('TB04 - Sender sends coin without batch_id, and then resends to a diffe let wallet2 = await mercuryweblib.createWallet(clientConfig, "wallet2_tb04"); let wallet3 = await mercuryweblib.createWallet(clientConfig, "wallet3_tb04"); - await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse = await mercuryweblib.newToken(clientConfig, wallet1.name); + + let token_id = await handleTokenResponse(tokenResponse); const amount = 1000; - let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id, amount); const statechainId = result.statechain_id; @@ -659,11 +675,13 @@ describe('Coin receiver creates a non hold invoice, and sends to sender (i.e. an let wallet1 = await mercuryweblib.createWallet(clientConfig, "wallet1_tb04"); - await mercuryweblib.newToken(clientConfig, wallet1.name); + let tokenResponse = await mercuryweblib.newToken(clientConfig, wallet1.name); + + let token_id = await handleTokenResponse(tokenResponse); const amount = 1000; - let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, amount); + let result = await mercuryweblib.getDepositBitcoinAddress(clientConfig, wallet1.name, token_id, amount); const statechainId = result.statechain_id; diff --git a/docker-compose-lockbox.yml b/docker-compose-lockbox.yml new file mode 100644 index 00000000..b61ea534 --- /dev/null +++ b/docker-compose-lockbox.yml @@ -0,0 +1,140 @@ +services: + + vault: + image: hashicorp/vault + environment: + - VAULT_DEV_ROOT_TOKEN_ID=2bbz80mr1zm0tfrjwxyq7f48ofgky848 + ports: + - "8200:8200" + + vault-init: + image: curlimages/curl + depends_on: + - vault + command: > + sh -c ' + sleep 10 && + curl --header "X-Vault-Token: 2bbz80mr1zm0tfrjwxyq7f48ofgky848" \ + --header "Content-Type: application/json" \ + --request POST \ + --data '"'"'{"data": {"seed": "8b10a037120cf37441bd7623da2aa488c21889017ffb4f4d303b9dbcbada5bee"}}'"'"' \ + http://vault:8200/v1/secret/data/mercury-seed && + touch /tmp/healthy && + tail -f /dev/null + ' + healthcheck: + test: ["CMD-SHELL", "test -f /tmp/healthy"] + interval: 1s + timeout: 1s + retries: 30 + + lockbox: + build: + context: lockbox + dockerfile: ./Dockerfile + depends_on: + - db_lockbox + - vault-init + environment: + - LOCKBOX_DATABASE_URL=postgres://postgres:postgres@db_lockbox:5432/enclave + - LOCKBOX_PORT=18080 + - KEY_MANAGER=hashicorp_container + - HASHICORP_CONTAINER_TOKEN=2bbz80mr1zm0tfrjwxyq7f48ofgky848 + - HASHICORP_CONTAINER_URL=http://vault:8200 + - HASHICORP_CONTAINER_PATH=mercury-seed + - HASHICORP_CONTAINER_MOUNT_POINT=secret + - HASHICORP_CONTAINER_KEY_NAME=seed + ports: + - "18080:18080" + + mercury-server: + build: + context: . + dockerfile: server/Dockerfile + depends_on: + - db_server + - lockbox + environment: + BITCOIN_NETWORK: regtest + LOCKHEIGHT_INIT: 1000 + LH_DECREMENT: 10 + DB_USER: postgres + DB_PASSWORD: postgres + DB_HOST: db_server + DB_PORT: 5432 + DB_NAME: mercury + BATCH_TIMEOUT: 20 + ENCLAVES: '[{"url": "http://lockbox:18080", "allow_deposit": true}]' + NOSTR_INFO: '{ + "relay_server": "wss://relay.damus.io/", + "relay_interval": 15, + "nostr_privkey": "nsec17e0nvplcze4k7q9nazrw0k3aracwhg6vmuareewjp83ta89njw5spjcgzs", + "server_url": "http://mercury_server.xyz", + "location": "UK", + "active": true, + "onchain_payments": false, + "ln_payments": true, + "fee": 0.0001, + "unit": "BTC" + }' + ports: + - "8000:8000" + + db_lockbox: + image: postgres:16.2 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: enclave + ports: + - "5433:5432" + volumes: + - postgres_lockbox_data:/var/lib/postgresql/data + + db_server: + image: postgres:16.2 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: mercury + ports: + - "5432:5432" + volumes: + - postgres_server_data:/var/lib/postgresql/data + web: + build: + context: . + dockerfile: ./web/Dockerfile + ports: + - "3000:80" + environment: + - REACT_APP_MERCURY_SERVER=http://mercury-server:8000 + - REACT_APP_BITCOIN_NETWORK=regtest + - REACT_APP_LOCKHEIGHT_INIT=1000 + - REACT_APP_LH_DECREMENT=10 + - REACT_APP_BATCH_TIMEOUT=20 + - REACT_APP_ENCLAVES='[{"url": "http://lockbox:18080", "allow_deposit": true}]' + - REACT_APP_DB_USER=postgres + - REACT_APP_DB_PASSWORD=postgres + - REACT_APP_DB_HOST=db_server + - REACT_APP_DB_PORT=5432 + - REACT_APP_DB_NAME=mercury + - REACT_APP_LOCKBOX_DB_USER=postgres + - REACT_APP_LOCKBOX_DB_PASSWORD=postgres + - REACT_APP_LOCKBOX_DB_HOST=db_lockbox + - REACT_APP_LOCKBOX_DB_PORT=5432 + - REACT_APP_LOCKBOX_DB_NAME=enclave + - REACT_APP_LOCKBOX_DATABASE_URL=postgres://postgres:postgres@db_lockbox:5432/enclave + - REACT_APP_LOCKBOX_PORT=18080 + - REACT_APP_KEY_MANAGER=hashicorp_container + - REACT_APP_HASHICORP_CONTAINER_TOKEN=2bbz80mr1zm0tfrjwxyq7f48ofgky848 + - REACT_APP_HASHICORP_CONTAINER_URL=http://vault:8200 + - REACT_APP_HASHICORP_CONTAINER_PATH=mercury-seed + - REACT_APP_HASHICORP_CONTAINER_MOUNT_POINT=secret + - REACT_APP_HASHICORP_CONTAINER_KEY_NAME=seed + - REACT_APP_VAULT_DEV_ROOT_TOKEN_ID=2bbz80mr1zm0tfrjwxyq7f48ofgky848 + - REACT_APP_VAULT_PORT=8200 + depends_on: + - mercury-server + +volumes: + postgres_lockbox_data: + postgres_server_data: diff --git a/docker-compose-main.yml b/docker-compose-main.yml new file mode 100644 index 00000000..c04b01ab --- /dev/null +++ b/docker-compose-main.yml @@ -0,0 +1,134 @@ +services: + + vault: + image: hashicorp/vault + environment: + - VAULT_DEV_ROOT_TOKEN_ID=2bbz80mr1zm0tfrjwxyq7f48ofgky848 + ports: + - "8200:8200" + + vault-init: + image: curlimages/curl + depends_on: + - vault + command: > + sh -c ' + sleep 10 && + curl --header "X-Vault-Token: 2bbz80mr1zm0tfrjwxyq7f48ofgky848" \ + --header "Content-Type: application/json" \ + --request POST \ + --data '"'"'{"data": {"seed": "8b10a037120cf37441bd7623da2aa488c21889017ffb4f4d303b9dbcbada5bee"}}'"'"' \ + http://vault:8200/v1/secret/data/mercury-seed && + touch /tmp/healthy && + tail -f /dev/null + ' + healthcheck: + test: ["CMD-SHELL", "test -f /tmp/healthy"] + interval: 1s + timeout: 1s + retries: 30 + + lockbox: + image: mercurylayer/lockbox:latest + depends_on: + - db_lockbox + - vault-init + environment: + - LOCKBOX_DATABASE_URL=postgres://postgres:postgres@db_lockbox:5432/enclave + - LOCKBOX_PORT=18080 + - KEY_MANAGER=hashicorp_container + - HASHICORP_CONTAINER_TOKEN=2bbz80mr1zm0tfrjwxyq7f48ofgky848 + - HASHICORP_CONTAINER_URL=http://vault:8200 + - HASHICORP_CONTAINER_PATH=mercury-seed + - HASHICORP_CONTAINER_MOUNT_POINT=secret + - HASHICORP_CONTAINER_KEY_NAME=seed + ports: + - "18080:18080" + + token-server-v2: + image: mercurylayer/token-server:latest + container_name: mercurylayer-token-server-v2-1 + environment: + PUBLIC_KEY_DESCRIPTOR: wpkh([656a457c/84'/1'/0']tpubDCTXiLu1wcqUwQK6QMPPUTBzbRjsqMABzCvd5vG22KGoA95cTG1VkszQQJyx24UP8KEJVKrKRDRtUPodHVV59CfNqUkXjKUagowHJVSWq4C/0/*)#vn0n5xcd + BITCOIN_NETWORK: mainnet + ELECTRUM_SERVER: tcp://blockstream.info:50001 + FEE: 10000 + CONFIRMATION_TARGET: 2 + DB_USER: postgres + DB_PASSWORD: postgres + DB_HOST: db_server + DB_PORT: 5432 + DB_NAME: mercury + ports: + - "8001:8001" + depends_on: + - db_server + + mercury-server: + image: mercurylayer/mercury-server:latest + container_name: mercurylayer-mercury-server-1 + depends_on: + - db_server + - lockbox + - token-server-v2 + environment: + BITCOIN_NETWORK: mainnet + LOCKHEIGHT_INIT: 50000 + LH_DECREMENT: 6 + DB_USER: postgres + DB_PASSWORD: postgres + DB_HOST: db_server + DB_PORT: 5432 + DB_NAME: mercury + BATCH_TIMEOUT: 20 + ENCLAVES: '[{"url": "http://lockbox:18080", "allow_deposit": true}]' + NOSTR_INFO: '{ + "relay_server": "wss://relay.damus.io/", + "relay_interval": 15, + "nostr_privkey": "nsec17e0nvplcze4k7q9nazrw0k3aracwhg6vmuareewjp83ta89njw5spjcgzs", + "server_url": "http://xxxxxxxxxx.onion", + "location": "UK", + "active": true, + "onchain_payments": false, + "ln_payments": true, + "fee": 0.0001, + "unit": "BTC" + }' + TOKEN_SERVER_URL: http://token-server-v2:8001 + ports: + - "8000:8000" + + db_lockbox: + image: postgres:16.2 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: enclave + ports: + - "5433:5432" + volumes: + - postgres_lockbox_data:/var/lib/postgresql/data + + db_server: + image: postgres:16.2 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: mercury + ports: + - "5432:5432" + volumes: + - postgres_server_data:/var/lib/postgresql/data + + tor: + image: dperson/torproxy:latest + container_name: tor_service + restart: unless-stopped + volumes: + - ./tor:/etc/tor:rw + - ./hidden_service:/var/lib/tor/hidden_service + entrypoint: /usr/bin/tor -f /etc/tor/torrc + depends_on: + - mercury-server + +volumes: + postgres_lockbox_data: + postgres_server_data: diff --git a/docker-compose-sim-prd.yml b/docker-compose-sim-prd.yml new file mode 100644 index 00000000..9034188f --- /dev/null +++ b/docker-compose-sim-prd.yml @@ -0,0 +1,53 @@ +version: '3.8' + +services: + enclave-sgx: + build: + context: enclave + dockerfile: Dockerfiles/SIM/Dockerfile + environment: + - ENCLAVE_DATABASE_URL=postgres://mercurylayer:@:5432/enclave + - ENCLAVE_PORT=18080 + - SEED_DIR=./seed + mercury-server: + build: + context: . + dockerfile: server/Dockerfile + environment: + - LOCKBOX_URL=http://enclave-sgx:18080 + - BITCOIN_NETWORK=testnet + - LOCKHEIGHT_INIT=1000 + - LH_DECREMENT=10 + - DB_USER=mercurylayer + - DB_PASSWORD= + - DB_HOST= + - DB_PORT=5432 + - DB_NAME=mercury + ports: + - "8000:8000" + token-server: + build: + context: token-server + dockerfile: Dockerfile + environment: + - PROCESSOR_URL = "" + - API_KEY = "" + - FEE = 10000 + - UNIT = "BTC" + - DELAY = 3600 + - DB_USER = "mercurylayer" + - DB_PASSWORD = "" + - DB_HOST = "" + - DB_PORT = 5432 + - DB_NAME = "mercury" + - BITCOIND = "http://user:password@127.0.0.1:8332/" + - LIGHTNINGD = "" + - LNMACAROON = "" + ports: + - "18080:18080" + explorer: + build: + context: explorer + dockerfile: Dockerfile + ports: + - "80:80" diff --git a/docker-compose-test.yml b/docker-compose-test.yml index c434911a..3f924abb 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -59,7 +59,7 @@ services: context: . dockerfile: ./server/Dockerfile environment: - NETWORK: regtest + BITCOIN_NETWORK: regtest LOCKHEIGHT_INIT: 1100 LH_DECREMENT: 1 DB_USER: postgres diff --git a/docker-compose-token-lnd-nodes.yml b/docker-compose-token-lnd-nodes.yml new file mode 100644 index 00000000..2dfda69a --- /dev/null +++ b/docker-compose-token-lnd-nodes.yml @@ -0,0 +1,96 @@ +services: + + bitcoind: + image: lncm/bitcoind:v22.0@sha256:37a1adb29b3abc9f972f0d981f45e41e5fca2e22816a023faa9fdc0084aa4507 + container_name: mercurylayer-bitcoind-1 + user: root + command: -regtest -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 -rpcauth=user:63cf03615adebaa9356591f95b07ec7b$$920588e53f94798bda636acac1b6a77e10e3ee7fe57e414d62f3ee9e580cd27a -fallbackfee=0.0001 -zmqpubrawblock=tcp://0.0.0.0:28332 -zmqpubrawtx=tcp://0.0.0.0:28333 + ports: + - "18443:18443" + - "28332:28332" + - "28333:28333" + volumes: + - bitcoin_data:/root/.bitcoin + + alice: + image: lightninglabs/lndinit:v0.1.21-beta-lnd-v0.18.0-beta + container_name: mercurylayer-alice-1 + user: root + hostname: lnd + entrypoint: + - sh + - -c + - | + if [[ ! -f /data/seed.txt ]]; then + lndinit gen-seed > /data/seed.txt + fi + if [[ ! -f /data/walletpassword.txt ]]; then + lndinit gen-password > /data/walletpassword.txt + fi + lndinit -v init-wallet \ + --secret-source=file \ + --file.seed=/data/seed.txt \ + --file.wallet-password=/data/walletpassword.txt \ + --init-file.output-wallet-dir=/root/.lnd/data/chain/bitcoin/regtest \ + --init-file.validate-password + mkdir -p /data/.lnd + if [ ! -f "/data/.lnd/umbrel-lnd.conf" ]; then + touch "/data/.lnd/umbrel-lnd.conf" + fi + lnd --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=bitcoind --bitcoind.rpcuser=user --bitcoind.rpcpass=pass --bitcoind.zmqpubrawblock=tcp://bitcoind:28332 --bitcoind.zmqpubrawtx=tcp://bitcoind:28333 --configfile=/data/.lnd/umbrel-lnd.conf --wallet-unlock-password-file=/data/walletpassword.txt --wallet-unlock-allow-create + ports: + - "9735:9735" + - "10009:10009" + - "8080:8080" + volumes: + - alice-data:/data/.lnd + restart: unless-stopped + environment: + HOME: /data + command: [ '/init-wallet-k8s.sh' ] + depends_on: + - bitcoind + + bob: + image: lightninglabs/lndinit:v0.1.21-beta-lnd-v0.18.0-beta + container_name: mercurylayer-bob-1 + user: root + hostname: lnd + entrypoint: + - sh + - -c + - | + if [[ ! -f /data/seed.txt ]]; then + lndinit gen-seed > /data/seed.txt + fi + if [[ ! -f /data/walletpassword.txt ]]; then + lndinit gen-password > /data/walletpassword.txt + fi + lndinit -v init-wallet \ + --secret-source=file \ + --file.seed=/data/seed.txt \ + --file.wallet-password=/data/walletpassword.txt \ + --init-file.output-wallet-dir=/root/.lnd/data/chain/bitcoin/regtest \ + --init-file.validate-password + mkdir -p /data/.lnd + if [ ! -f "/data/.lnd/umbrel-lnd.conf" ]; then + touch "/data/.lnd/umbrel-lnd.conf" + fi + lnd --listen=0.0.0.0:9735 --rpclisten=0.0.0.0:10009 --restlisten=0.0.0.0:8080 --bitcoin.active --bitcoin.regtest --bitcoin.node=bitcoind --bitcoind.rpchost=bitcoind --bitcoind.rpcuser=user --bitcoind.rpcpass=pass --bitcoind.zmqpubrawblock=tcp://bitcoind:28332 --bitcoind.zmqpubrawtx=tcp://bitcoind:28333 --configfile=/data/.lnd/umbrel-lnd.conf --wallet-unlock-password-file=/data/walletpassword.txt --wallet-unlock-allow-create + ports: + - "9736:9735" + - "10010:10009" + - "8081:8080" + volumes: + - bob-data:/data/.lnd + restart: unless-stopped + environment: + HOME: /data + command: [ '/init-wallet-k8s.sh' ] + depends_on: + - bitcoind + +volumes: + bitcoin_data: + alice-data: + bob-data: diff --git a/docker-compose-token-servers.yml b/docker-compose-token-servers.yml new file mode 100644 index 00000000..0be0449b --- /dev/null +++ b/docker-compose-token-servers.yml @@ -0,0 +1,143 @@ +services: + + esplora: + image: blockstream/esplora + container_name: esplora-container + ports: + - "50001:50001" + - "50002:50002" + - "8094:80" + volumes: + - ./data_bitcoin_regtest:/data + environment: + - CORS_ALLOW=* + command: /srv/explorer/run.sh bitcoin-regtest explorer + + vault: + image: hashicorp/vault + environment: + - VAULT_DEV_ROOT_TOKEN_ID=2bbz80mr1zm0tfrjwxyq7f48ofgky848 + ports: + - "8200:8200" + + vault-init: + image: curlimages/curl + depends_on: + - vault + command: > + sh -c ' + sleep 10 && + curl --header "X-Vault-Token: 2bbz80mr1zm0tfrjwxyq7f48ofgky848" \ + --header "Content-Type: application/json" \ + --request POST \ + --data '"'"'{"data": {"seed": "8b10a037120cf37441bd7623da2aa488c21889017ffb4f4d303b9dbcbada5bee"}}'"'"' \ + http://vault:8200/v1/secret/data/mercury-seed && + touch /tmp/healthy && + tail -f /dev/null + ' + healthcheck: + test: ["CMD-SHELL", "test -f /tmp/healthy"] + interval: 1s + timeout: 1s + retries: 30 + + lockbox: + build: + context: lockbox + dockerfile: ./Dockerfile + depends_on: + - db_lockbox + - vault-init + environment: + - LOCKBOX_DATABASE_URL=postgres://postgres:postgres@db_lockbox:5432/enclave + - LOCKBOX_PORT=18080 + - KEY_MANAGER=hashicorp_container + - HASHICORP_CONTAINER_TOKEN=2bbz80mr1zm0tfrjwxyq7f48ofgky848 + - HASHICORP_CONTAINER_URL=http://vault:8200 + - HASHICORP_CONTAINER_PATH=mercury-seed + - HASHICORP_CONTAINER_MOUNT_POINT=secret + - HASHICORP_CONTAINER_KEY_NAME=seed + ports: + - "18080:18080" + + token-server-v2: + build: + context: . + dockerfile: ./token-server-v2/Dockerfile + container_name: mercurylayer-token-server-v2-1 + environment: + PUBLIC_KEY_DESCRIPTOR: wpkh([656a457c/84'/1'/0']tpubDCTXiLu1wcqUwQK6QMPPUTBzbRjsqMABzCvd5vG22KGoA95cTG1VkszQQJyx24UP8KEJVKrKRDRtUPodHVV59CfNqUkXjKUagowHJVSWq4C/0/*)#vn0n5xcd + BITCOIN_NETWORK: regtest + ELECTRUM_SERVER: tcp://esplora:50001 + FEE: 10000 + CONFIRMATION_TARGET: 2 + DB_USER: postgres + DB_PASSWORD: postgres + DB_HOST: db_server + DB_PORT: 5432 + DB_NAME: mercury + ports: + - "8001:8001" + depends_on: + - esplora + - db_server + + mercury-server: + build: + context: . + dockerfile: server/Dockerfile + container_name: mercurylayer-mercury-server-1 + depends_on: + - db_server + - lockbox + - token-server-v2 + environment: + BITCOIN_NETWORK: regtest + LOCKHEIGHT_INIT: 1000 + LH_DECREMENT: 10 + DB_USER: postgres + DB_PASSWORD: postgres + DB_HOST: db_server + DB_PORT: 5432 + DB_NAME: mercury + BATCH_TIMEOUT: 20 + ENCLAVES: '[{"url": "http://lockbox:18080", "allow_deposit": true}]' + NOSTR_INFO: '{ + "relay_server": "wss://relay.damus.io/", + "relay_interval": 15, + "nostr_privkey": "nsec17e0nvplcze4k7q9nazrw0k3aracwhg6vmuareewjp83ta89njw5spjcgzs", + "server_url": "http://mercury_server.xyz", + "location": "UK", + "active": true, + "onchain_payments": false, + "ln_payments": true, + "fee": 0.0001, + "unit": "BTC" + }' + TOKEN_SERVER_URL: http://token-server-v2:8001 + ports: + - "8000:8000" + + db_lockbox: + image: postgres:16.2 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: enclave + ports: + - "5433:5432" + volumes: + - postgres_lockbox_data:/var/lib/postgresql/data + + db_server: + image: postgres:16.2 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: mercury + ports: + - "5432:5432" + volumes: + - postgres_server_data:/var/lib/postgresql/data + +volumes: + postgres_lockbox_data: + postgres_server_data: diff --git a/docker-compose-tor.yml b/docker-compose-tor.yml new file mode 100644 index 00000000..045d54fb --- /dev/null +++ b/docker-compose-tor.yml @@ -0,0 +1,156 @@ +services: + + vault: + image: hashicorp/vault + environment: + - VAULT_DEV_ROOT_TOKEN_ID=2bbz80mr1zm0tfrjwxyq7f48ofgky848 + ports: + - "8200:8200" + + vault-init: + image: curlimages/curl + depends_on: + - vault + command: > + sh -c ' + sleep 10 && + curl --header "X-Vault-Token: 2bbz80mr1zm0tfrjwxyq7f48ofgky848" \ + --header "Content-Type: application/json" \ + --request POST \ + --data '"'"'{"data": {"seed": "8b10a037120cf37441bd7623da2aa488c21889017ffb4f4d303b9dbcbada5bee"}}'"'"' \ + http://vault:8200/v1/secret/data/mercury-seed && + touch /tmp/healthy && + tail -f /dev/null + ' + healthcheck: + test: ["CMD-SHELL", "test -f /tmp/healthy"] + interval: 1s + timeout: 1s + retries: 30 + + lockbox: + build: + context: lockbox + dockerfile: ./Dockerfile + depends_on: + - db_lockbox + - vault-init + environment: + - LOCKBOX_DATABASE_URL=postgres://postgres:postgres@db_lockbox:5432/enclave + - LOCKBOX_PORT=18080 + - KEY_MANAGER=hashicorp_container + - HASHICORP_CONTAINER_TOKEN=2bbz80mr1zm0tfrjwxyq7f48ofgky848 + - HASHICORP_CONTAINER_URL=http://vault:8200 + - HASHICORP_CONTAINER_PATH=mercury-seed + - HASHICORP_CONTAINER_MOUNT_POINT=secret + - HASHICORP_CONTAINER_KEY_NAME=seed + ports: + - "18080:18080" + + mercury-server: + build: + context: . + dockerfile: server/Dockerfile + depends_on: + - db_server + - lockbox + environment: + BITCOIN_NETWORK: regtest + LOCKHEIGHT_INIT: 1000 + LH_DECREMENT: 10 + DB_USER: postgres + DB_PASSWORD: postgres + DB_HOST: db_server + DB_PORT: 5432 + DB_NAME: mercury + BATCH_TIMEOUT: 20 + ENCLAVES: '[{"url": "http://lockbox:18080", "allow_deposit": true}]' + NOSTR_INFO: '{ + "relay_server": "wss://relay.damus.io/", + "relay_interval": 15, + "nostr_privkey": "nsec17e0nvplcze4k7q9nazrw0k3aracwhg6vmuareewjp83ta89njw5spjcgzs", + "server_url": "http://mercury_server.xyz", + "location": "UK", + "active": true, + "onchain_payments": false, + "ln_payments": true, + "fee": 0.0001, + "unit": "BTC" + }' + ports: + - "8000:8000" + + db_lockbox: + image: postgres:16.2 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: enclave + ports: + - "5433:5432" + volumes: + - postgres_lockbox_data:/var/lib/postgresql/data + + db_server: + image: postgres:16.2 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: mercury + ports: + - "5432:5432" + volumes: + - postgres_server_data:/var/lib/postgresql/data + web: + build: + context: . + dockerfile: ./web/Dockerfile + ports: + - "3000:80" + environment: + - REACT_APP_MERCURY_SERVER=http://mercury-server:8000 + - REACT_APP_BITCOIN_NETWORK=regtest + - REACT_APP_LOCKHEIGHT_INIT=1000 + - REACT_APP_LH_DECREMENT=10 + - REACT_APP_BATCH_TIMEOUT=20 + - REACT_APP_ENCLAVES='[{"url": "http://lockbox:18080", "allow_deposit": true}]' + - REACT_APP_DB_USER=postgres + - REACT_APP_DB_PASSWORD=postgres + - REACT_APP_DB_HOST=db_server + - REACT_APP_DB_PORT=5432 + - REACT_APP_DB_NAME=mercury + - REACT_APP_LOCKBOX_DB_USER=postgres + - REACT_APP_LOCKBOX_DB_PASSWORD=postgres + - REACT_APP_LOCKBOX_DB_HOST=db_lockbox + - REACT_APP_LOCKBOX_DB_PORT=5432 + - REACT_APP_LOCKBOX_DB_NAME=enclave + - REACT_APP_LOCKBOX_DATABASE_URL=postgres://postgres:postgres@db_lockbox:5432/enclave + - REACT_APP_LOCKBOX_PORT=18080 + - REACT_APP_KEY_MANAGER=hashicorp_container + - REACT_APP_HASHICORP_CONTAINER_TOKEN=2bbz80mr1zm0tfrjwxyq7f48ofgky848 + - REACT_APP_HASHICORP_CONTAINER_URL=http://vault:8200 + - REACT_APP_HASHICORP_CONTAINER_PATH=mercury-seed + - REACT_APP_HASHICORP_CONTAINER_MOUNT_POINT=secret + - REACT_APP_HASHICORP_CONTAINER_KEY_NAME=seed + - REACT_APP_VAULT_DEV_ROOT_TOKEN_ID=2bbz80mr1zm0tfrjwxyq7f48ofgky848 + - REACT_APP_VAULT_PORT=8200 + depends_on: + - mercury-server + + tor: + image: dperson/torproxy:latest + container_name: tor_node + restart: always + volumes: + - ./tor_data:/var/lib/tor # Persist Tor configuration + environment: + - USER=tor + - GROUP=tor + command: > + --hidden-service /var/lib/tor/hidden_service + --hidden-service-dir /var/lib/tor/hidden_service + --hidden-service-port 80=rest_server:8000 + depends_on: + - mercury-server + +volumes: + postgres_lockbox_data: + postgres_server_data: diff --git a/explorer/Dockerfile b/explorer/Dockerfile index f53e7f16..d4091840 100644 --- a/explorer/Dockerfile +++ b/explorer/Dockerfile @@ -2,9 +2,9 @@ FROM nginx:latest # Copy the contents of your local /explorer folder to the container's working directory -COPY ../explorer/index.html /usr/share/nginx/html -COPY ../explorer/main.js /usr/share/nginx/html -COPY ../explorer/nginx.conf /etc/nginx/nginx.conf +COPY index.html /usr/share/nginx/html +COPY main.js /usr/share/nginx/html +COPY nginx.conf /etc/nginx/nginx.conf # Expose port 80 for web traffic EXPOSE 80 diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 60d39cae..c6c1aa70 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -14,7 +14,7 @@ required-features = ["bindings"] [dependencies] bitcoin = { version = "0.30.1", features = ["serde", "base64", "rand-std", "std"], default-features = false } -bip39 = "1.2.0" +bip39 = "2.0" bech32 = { version = "0.9.1", default-features = false } ecies = {version = "0.2.7", default-features = false, features = ["pure"]} hex = "0.4.3" diff --git a/lib/rust-toolchain.toml b/lib/rust-toolchain.toml index a56a283d..0193dee3 100644 --- a/lib/rust-toolchain.toml +++ b/lib/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.80.1" +channel = "1.83.0" diff --git a/lib/src/deposit/mod.rs b/lib/src/deposit/mod.rs index 57191a61..f4b898bb 100644 --- a/lib/src/deposit/mod.rs +++ b/lib/src/deposit/mod.rs @@ -10,6 +10,15 @@ pub struct TokenID { pub token_id: String, } +#[derive(Debug, Serialize, Deserialize)] +pub struct TokenResponse { + pub token_id: String, + pub payment_method: String, + pub deposit_address: Option, + pub fee: u64, + pub confirmation_target: u64, +} + #[derive(Debug, Serialize, Deserialize)] #[cfg_attr(feature = "bindings", derive(uniffi::Record))] pub struct DepositMsg1 { diff --git a/lib/src/transfer/mod.rs b/lib/src/transfer/mod.rs index 03be674d..aeaf48b2 100644 --- a/lib/src/transfer/mod.rs +++ b/lib/src/transfer/mod.rs @@ -98,3 +98,10 @@ pub struct TransferMsg { pub t1: [u8; 32], pub user_public_key: String, } + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "bindings", derive(uniffi::Record))] +pub struct TxOutpoint { + pub txid: String, + pub vout: u32, +} diff --git a/lib/src/transfer/receiver.rs b/lib/src/transfer/receiver.rs index 541aa0c4..ebf76ed9 100644 --- a/lib/src/transfer/receiver.rs +++ b/lib/src/transfer/receiver.rs @@ -4,9 +4,9 @@ use bitcoin::{hashes::{sha256, Hash}, sighash::{self, SighashCache, TapSighashTy use secp256k1_zkp::{PublicKey, schnorr::Signature, Secp256k1, Message, XOnlyPublicKey, musig::{MusigPubNonce, BlindingFactor, blinded_musig_pubkey_xonly_tweak_add, MusigAggNonce, MusigSession}, SecretKey, Scalar, KeyPair}; use serde::{Serialize, Deserialize}; -use crate::{error::MercuryError, utils::get_network, wallet::{BackupTx, Coin, CoinStatus, Wallet}}; +use crate::{error::MercuryError, utils::get_network, wallet::{get_previous_outpoint, BackupTx, Coin, CoinStatus, Wallet}}; -use super::TransferMsg; +use super::{TransferMsg, TxOutpoint}; #[derive(Debug, Serialize, Deserialize)] #[cfg_attr(feature = "bindings", derive(uniffi::Record))] @@ -77,13 +77,6 @@ pub struct StatechainInfoResponsePayload { pub x1_pub: Option, } -#[derive(Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "bindings", derive(uniffi::Record))] -pub struct TxOutpoint { - pub txid: String, - pub vout: u32, -} - #[derive(Debug, Serialize, Deserialize)] #[cfg_attr(feature = "bindings", derive(uniffi::Record))] pub struct NewKeyInfo { @@ -159,20 +152,7 @@ pub fn get_tx0_outpoint(backup_transactions: &Vec) -> Result 1 { - return Err(MercuryError::Tx1HasMoreThanOneInput); - } - - if tx1.output.len() > 1 { - return Err(MercuryError::Tx1HasMoreThanOneInput); - } - - let tx0_txid = tx1.input[0].previous_output.txid; - let tx0_vout = tx1.input[0].previous_output.vout as u32; - - Ok(TxOutpoint{ txid: tx0_txid.to_string(), vout: tx0_vout }) + get_previous_outpoint(bkp_tx1) } pub fn verify_transfer_signature(new_user_pubkey: &str, tx0_outpoint: &TxOutpoint, transfer_msg: &TransferMsg) -> Result { @@ -261,8 +241,19 @@ pub fn get_output_address_from_tx0(tx0_outpoint: &TxOutpoint, tx0_hex: &str, net Ok(address.to_string()) } +#[cfg_attr(feature = "bindings", uniffi::export)] +pub fn get_amount_from_tx0(tx0_hex: &str, tx0_outpoint: &TxOutpoint,) -> Result { + + let tx0: Transaction = bitcoin::consensus::encode::deserialize(&hex::decode(&tx0_hex)?)?; + + assert!(tx0_outpoint.txid == tx0.txid().to_string()); + + Ok(tx0.output[tx0_outpoint.vout as usize].value) +} + +#[cfg_attr(feature = "bindings", uniffi::export)] pub fn validate_signature_scheme( - transfer_msg: &TransferMsg, + backup_transactions: &Vec, statechain_info: &StatechainInfoResponsePayload, tx0_hex: &str, current_blockheight: u32, @@ -275,9 +266,19 @@ pub fn validate_signature_scheme( let mut sig_scheme_validation = true; - for (index, backup_tx) in transfer_msg.backup_transactions.iter().enumerate() { + for backup_tx in backup_transactions.iter() { - let statechain_info = statechain_info.statechain_info.get(index).unwrap(); + let statechain_info = statechain_info.statechain_info + .iter() + .find(|info| info.tx_n == backup_tx.tx_n); + + if statechain_info.is_none() { + println!("statechain_info not found"); + sig_scheme_validation = false; + break; + } + + let statechain_info = statechain_info.unwrap(); let is_signature_valid = verify_transaction_signature(&backup_tx.tx, &tx0_hex, fee_rate_tolerance, current_fee_rate_sats_per_byte); if is_signature_valid.is_err() { @@ -483,7 +484,8 @@ fn get_tx_hash(tx_0: &Transaction, tx_n: &Transaction) -> Result "TRANSFERRED", Self::WITHDRAWN => "WITHDRAWN", Self::DUPLICATED => "DUPLICATED", + Self::INVALIDATED => "INVALIDATED", }) } } @@ -165,6 +168,7 @@ impl FromStr for CoinStatus { "TRANSFERRED" => Ok(CoinStatus::TRANSFERRED), "WITHDRAWN" => Ok(CoinStatus::WITHDRAWN), "DUPLICATED" => Ok(CoinStatus::DUPLICATED), + "INVALIDATED" => Ok(CoinStatus::INVALIDATED), _ => Err(CoinStatusParseError {}), } } @@ -186,7 +190,26 @@ pub struct BackupTx { pub client_public_key: String, pub server_public_key: String, pub blinding_factor: String, -} +} + +#[cfg_attr(feature = "bindings", uniffi::export)] +pub fn get_previous_outpoint(backup_tx: &BackupTx) -> Result { + + let tx1: Transaction = bitcoin::consensus::encode::deserialize(&hex::decode(backup_tx.tx.clone())?)?; + + if tx1.input.len() > 1 { + return Err(MercuryError::Tx1HasMoreThanOneInput); + } + + if tx1.output.len() > 1 { + return Err(MercuryError::Tx1HasMoreThanOneInput); + } + + let tx0_txid = tx1.input[0].previous_output.txid; + let tx0_vout = tx1.input[0].previous_output.vout as u32; + + Ok(TxOutpoint{ txid: tx0_txid.to_string(), vout: tx0_vout }) +} pub fn set_config(wallet: &mut Wallet, config: &ServerConfig) { wallet.initlock = config.initlock; diff --git a/lockbox/.dockerignore b/lockbox/.dockerignore new file mode 100644 index 00000000..ed50b44e --- /dev/null +++ b/lockbox/.dockerignore @@ -0,0 +1,2 @@ +Settings.toml +build/** diff --git a/lockbox/.env b/lockbox/.env new file mode 100644 index 00000000..7b54077c --- /dev/null +++ b/lockbox/.env @@ -0,0 +1,30 @@ +# general settings +# KEY_MANAGER can be one of: google_kms, hashicorp_api, hashicorp_container or filesystem +LOCKBOX_DATABASE_URL=postgresql://postgres:postgres@localhost/enclave +LOCKBOX_PORT=18080 +KEY_MANAGER=filesystem + +# filesystem settings +SEED_FILEPATH=./seed + +# google_kms settings +GCLOUD_PROJECT_ID=mercury-441416 +GCLOUD_PROJECT_NUMBER=100600525477 +GCLOUD_LOCATION_ID=global +GCLOUD_SECRET_MANAGER_KEY_NAME=encrypted-key +GCLOUD_KMS_RING=enclave +GCLOUD_CRYPTO_KEY=sealing + +# hashicorp settings +# HASHICORP_API_HCP_CLIENT_ID= +# HASHICORP_API_HCP_CLIENT_SECRET= +# HASHICORP_API_ORGANIZATION_ID= +# HASHICORP_API_PROJECT_ID= +# HASHICORP_API_APP_NAME= +# HASHICORP_API_SECRET_NAME= + +HASHICORP_CONTAINER_TOKEN=2bbz80mr1zm0tfrjwxyq7f48ofgky848 +HASHICORP_CONTAINER_URL=http://127.0.0.1:8200 +HASHICORP_CONTAINER_PATH=mercury-seed +HASHICORP_CONTAINER_MOUNT_POINT=secret +HASHICORP_CONTAINER_KEY_NAME=seed diff --git a/lockbox/.gitignore b/lockbox/.gitignore new file mode 100644 index 00000000..51dd3884 --- /dev/null +++ b/lockbox/.gitignore @@ -0,0 +1,35 @@ +# Ignore build directory +build/ + +# Ignore CMake generated files +CMakeFiles/ +CMakeCache.txt +cmake_install.cmake +Makefile + +# Ignore compiled binaries and executables +*.out +*.exe +*.dll +*.so +*.dylib +*.a +*.o +*.obj +*.lib + +# Ignore temporary files and editor-specific files +*.tmp +*.swp +*.swo +*~ +*.DS_Store + +# Ignore IDE and editor-specific files +.vscode/ +.idea/ +*.user +*.workspace + +# Ignore logs and other unnecessary files +*.log diff --git a/lockbox/CMakeLists.txt b/lockbox/CMakeLists.txt new file mode 100644 index 00000000..67b95d59 --- /dev/null +++ b/lockbox/CMakeLists.txt @@ -0,0 +1,103 @@ +# Set the minimum required version of CMake +cmake_minimum_required(VERSION 3.15) + +# Define the project name and language +project(MercuryLockbox VERSION 0.1 + DESCRIPTION "Mercury Lockbox" + LANGUAGES C CXX) + +# Add C compiler settings +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# Specify C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Add the executable and source files +add_executable(MercuryLockbox src/main.cpp) + +# Include directories (e.g., header files in include/) +# target_include_directories(MercuryLockbox PRIVATE include) + +# Add the cmake directory to the module path +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) + +# Include the secp256k1_zkp configuration +include(secp256k1_zkp) + +add_library(LockboxLibrary + src/server.cpp + src/utils.cpp + src/google_key_manager.cpp + src/hashicorp_api_key_manager.cpp + src/hashicorp_container_key_manager.cpp + src/filesystem_key_manager.cpp + src/enclave.cpp + src/monocypher.c + src/db_manager.cpp) + +set_source_files_properties(src/monocypher.c PROPERTIES LANGUAGE C) + +target_include_directories(LockboxLibrary PUBLIC include) + +# Ensure proper C/C++ linkage +target_compile_options(LockboxLibrary PRIVATE + $<$:-std=c11> + $<$:-std=c++17>) + +target_link_libraries(LockboxLibrary + Crow::Crow + cpr::cpr + pqxx + tomlplusplus::tomlplusplus + google-cloud-cpp::storage + google-cloud-cpp::kms + google-cloud-cpp::secretmanager + secp256k1_zkp + OpenSSL::SSL + OpenSSL::Crypto) + +include(FetchContent) +FetchContent_Declare( + tomlplusplus + GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git + GIT_TAG v3.4.0 +) +FetchContent_MakeAvailable(tomlplusplus) + +FetchContent_Declare( + libpqxx + GIT_REPOSITORY https://github.com/jtv/libpqxx.git + GIT_TAG 7.8.1 +) + +FetchContent_MakeAvailable(libpqxx) + +find_package(Crow) +find_package(cpr REQUIRED) +find_package(google_cloud_cpp_storage REQUIRED) +find_package(google_cloud_cpp_kms REQUIRED) +find_package(google_cloud_cpp_secretmanager REQUIRED) +find_package(OpenSSL REQUIRED) + +target_include_directories(MercuryLockbox PRIVATE include) +# Link the library to the main executable +target_link_libraries(MercuryLockbox PRIVATE + LockboxLibrary + Crow::Crow + cpr::cpr + pqxx + tomlplusplus::tomlplusplus + google-cloud-cpp::storage + google-cloud-cpp::kms + google-cloud-cpp::secretmanager + secp256k1_zkp + OpenSSL::SSL + OpenSSL::Crypto) + +# Copy Settings.toml after building MercuryLockbox +# add_custom_command(TARGET MercuryLockbox POST_BUILD +# COMMAND ${CMAKE_COMMAND} -E copy_if_different +# ${CMAKE_SOURCE_DIR}/Settings.toml +# $/Settings.toml) diff --git a/lockbox/CMakePresets.json b/lockbox/CMakePresets.json new file mode 100644 index 00000000..868e24ac --- /dev/null +++ b/lockbox/CMakePresets.json @@ -0,0 +1,13 @@ +{ + "version": 2, + "configurePresets": [ + { + "name": "vcpkg", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + } + } + ] + } \ No newline at end of file diff --git a/lockbox/Dockerfile b/lockbox/Dockerfile new file mode 100644 index 00000000..26d1ceb8 --- /dev/null +++ b/lockbox/Dockerfile @@ -0,0 +1,46 @@ +# Start with Ubuntu as base image +FROM ubuntu:22.04 + +# Prevent interactive prompts during package installation +ENV DEBIAN_FRONTEND=noninteractive + +# Install essential packages +RUN apt-get update && apt-get install -y \ + build-essential \ + cmake \ + curl \ + git \ + ninja-build \ + pkg-config \ + tar \ + unzip \ + zip \ + libpq-dev \ + autoconf \ + automake \ + libtool \ + && rm -rf /var/lib/apt/lists/* + +# Set up working directory +WORKDIR /app + +# Install vcpkg +RUN git clone https://github.com/Microsoft/vcpkg.git /opt/vcpkg && \ + /opt/vcpkg/bootstrap-vcpkg.sh -disableMetrics + +# Add vcpkg to PATH +ENV PATH="/opt/vcpkg:${PATH}" + +# Set vcpkg root +ENV VCPKG_ROOT=/opt/vcpkg + +# Copy your project files +COPY . . + +# Create build directory and run cmake +RUN mkdir -p build && cd build +RUN cd build && cmake --preset=vcpkg .. +RUN cd build && cmake --build . + +# Set the default command +CMD ["./build/MercuryLockbox"] \ No newline at end of file diff --git a/lockbox/README.md b/lockbox/README.md new file mode 100644 index 00000000..3926699b --- /dev/null +++ b/lockbox/README.md @@ -0,0 +1,34 @@ +## Lockbox server + +### 1. Running from source: + +1. Install necessary packages: `sudo apt-get -y install libpq-dev autoconf automake libtool` +2. Install vcpkg package manager, following the instruction [here](https://learn.microsoft.com/en-us/vcpkg/get_started/get-started?pivots=shell-bash). +3. Install `ninja` build system (`sudo apt-get -y install ninja-build`). +3. Then run the commands below: + +```bash +$ mkdir -p build && cd build +$ cmake --preset=vcpkg .. +$ cmake --build . +``` +4. Set the desired key manager in `Settings.toml`. Currently, there are 4 available: `google_kms`, `hashicorp_container`, `hashicorp_api` and `filesystem`. + +5. Copy the `Settings.toml` file: `$ cp ../Settings.toml .` + +6. Then, to run the server: `$ ./MercuryLockbox`. + +### 2. Running from Dockerfile: + +1. Edit the `.env` file according to your personal settings. + +2. Run `$ docker build -t mercury-lockbox .` +3. 1. For `filesystem` or `hashicorp` key manager, you can run `$ docker run --env-file .env mercury-lockbox`. + 2. For `google_kms` key manager, you need to pass your service account key file at runtime: + ```bash + docker run --env-file .env \ + -v /path/to/service-account-key.json:/app/credentials.json \ + -e GOOGLE_APPLICATION_CREDENTIALS=/app/credentials.json \ + mercury-lockbox + ``` + diff --git a/lockbox/Settings.toml b/lockbox/Settings.toml new file mode 100644 index 00000000..68a2b10b --- /dev/null +++ b/lockbox/Settings.toml @@ -0,0 +1,28 @@ +[general] +database_connection_string = "postgresql://postgres:postgres@localhost/enclave" +server_port = 18080 +key_manager = "filesystem" # "google_kms", "hashicorp_container, "hashicorp_api", "filesystem" +[filesystem] +seed_filepath = "./seed" +[gcloud] +project_id = "mercury-441416" +project_number = "100600525477" +location_id = "global" +[secretmanager] +key_name = "encrypted-key" +[kms] +ring = "enclave" +crypto_key = "sealing" +[hashicorp_api] +hcp_client_id = "" +hcp_client_secret = "" +organization_id = "" +project_id = "" +app_name = "" +secret_name = "" +[hashicorp_container] +token = "2bbz80mr1zm0tfrjwxyq7f48ofgky848" +url = "http://127.0.0.1:8200" +path = "mercury-seed" +mount_point = "secret" +key_name = "seed" diff --git a/lockbox/cmake/secp256k1_zkp.cmake b/lockbox/cmake/secp256k1_zkp.cmake new file mode 100644 index 00000000..1b3edba3 --- /dev/null +++ b/lockbox/cmake/secp256k1_zkp.cmake @@ -0,0 +1,48 @@ +# cmake/secp256k1_zkp.cmake +include(ExternalProject) + +# Set secp256k1-zkp build directory relative to the main build directory +set(SECP256K1_BUILD_DIR ${CMAKE_BINARY_DIR}/secp256k1-zkp) +set(SECP256K1_INSTALL_DIR ${CMAKE_BINARY_DIR}/secp256k1-zkp-install) + +# Configure and build secp256k1-zkp as an external project +ExternalProject_Add( + secp256k1_zkp_external + GIT_REPOSITORY https://github.com/ssantos21/secp256k1-zkp.git + GIT_TAG blinded-musig-scheme + PREFIX ${SECP256K1_BUILD_DIR} + CONFIGURE_COMMAND + cd && + ./autogen.sh && + ./configure + --prefix=${SECP256K1_INSTALL_DIR} + --enable-module-schnorrsig + --enable-experimental + --enable-module-musig + --enable-benchmark=no + --enable-tests=no + --enable-exhaustive-tests=no + BUILD_COMMAND cd && make + BUILD_IN_SOURCE 1 + INSTALL_COMMAND "" + BUILD_BYPRODUCTS + /.libs/libsecp256k1.a +) + +# Create an interface library for secp256k1-zkp +add_library(secp256k1_zkp INTERFACE) +add_dependencies(secp256k1_zkp secp256k1_zkp_external) + +# Get the source directory of secp256k1-zkp after it's cloned +ExternalProject_Get_Property(secp256k1_zkp_external SOURCE_DIR) + +# Set include directories and link the static library +target_include_directories(secp256k1_zkp + INTERFACE + ${SOURCE_DIR}/include +) + +target_link_libraries(secp256k1_zkp + INTERFACE + ${SOURCE_DIR}/.libs/libsecp256k1.a +) diff --git a/lockbox/include/CLI11.hpp b/lockbox/include/CLI11.hpp new file mode 100644 index 00000000..8a5b4c54 --- /dev/null +++ b/lockbox/include/CLI11.hpp @@ -0,0 +1,10998 @@ +// CLI11: Version 2.4.2 +// Originally designed by Henry Schreiner +// https://github.com/CLIUtils/CLI11 +// +// This is a standalone header file generated by MakeSingleHeader.py in CLI11/scripts +// from: v2.4.2 +// +// CLI11 2.4.2 Copyright (c) 2017-2024 University of Cincinnati, developed by Henry +// Schreiner under NSF AWARD 1414736. All rights reserved. +// +// Redistribution and use in source and binary forms of CLI11, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holder nor the names of its contributors +// may be used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +// Standard combined includes: +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#define CLI11_VERSION_MAJOR 2 +#define CLI11_VERSION_MINOR 4 +#define CLI11_VERSION_PATCH 2 +#define CLI11_VERSION "2.4.2" + + + + +// The following version macro is very similar to the one in pybind11 +#if !(defined(_MSC_VER) && __cplusplus == 199711L) && !defined(__INTEL_COMPILER) +#if __cplusplus >= 201402L +#define CLI11_CPP14 +#if __cplusplus >= 201703L +#define CLI11_CPP17 +#if __cplusplus > 201703L +#define CLI11_CPP20 +#endif +#endif +#endif +#elif defined(_MSC_VER) && __cplusplus == 199711L +// MSVC sets _MSVC_LANG rather than __cplusplus (supposedly until the standard is fully implemented) +// Unless you use the /Zc:__cplusplus flag on Visual Studio 2017 15.7 Preview 3 or newer +#if _MSVC_LANG >= 201402L +#define CLI11_CPP14 +#if _MSVC_LANG > 201402L && _MSC_VER >= 1910 +#define CLI11_CPP17 +#if _MSVC_LANG > 201703L && _MSC_VER >= 1910 +#define CLI11_CPP20 +#endif +#endif +#endif +#endif + +#if defined(CLI11_CPP14) +#define CLI11_DEPRECATED(reason) [[deprecated(reason)]] +#elif defined(_MSC_VER) +#define CLI11_DEPRECATED(reason) __declspec(deprecated(reason)) +#else +#define CLI11_DEPRECATED(reason) __attribute__((deprecated(reason))) +#endif + +// GCC < 10 doesn't ignore this in unevaluated contexts +#if !defined(CLI11_CPP17) || \ + (defined(__GNUC__) && !defined(__llvm__) && !defined(__INTEL_COMPILER) && __GNUC__ < 10 && __GNUC__ > 4) +#define CLI11_NODISCARD +#else +#define CLI11_NODISCARD [[nodiscard]] +#endif + +/** detection of rtti */ +#ifndef CLI11_USE_STATIC_RTTI +#if(defined(_HAS_STATIC_RTTI) && _HAS_STATIC_RTTI) +#define CLI11_USE_STATIC_RTTI 1 +#elif defined(__cpp_rtti) +#if(defined(_CPPRTTI) && _CPPRTTI == 0) +#define CLI11_USE_STATIC_RTTI 1 +#else +#define CLI11_USE_STATIC_RTTI 0 +#endif +#elif(defined(__GCC_RTTI) && __GXX_RTTI) +#define CLI11_USE_STATIC_RTTI 0 +#else +#define CLI11_USE_STATIC_RTTI 1 +#endif +#endif + +/** availability */ +#if defined CLI11_CPP17 && defined __has_include && !defined CLI11_HAS_FILESYSTEM +#if __has_include() +// Filesystem cannot be used if targeting macOS < 10.15 +#if defined __MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500 +#define CLI11_HAS_FILESYSTEM 0 +#elif defined(__wasi__) +// As of wasi-sdk-14, filesystem is not implemented +#define CLI11_HAS_FILESYSTEM 0 +#else +#include +#if defined __cpp_lib_filesystem && __cpp_lib_filesystem >= 201703 +#if defined _GLIBCXX_RELEASE && _GLIBCXX_RELEASE >= 9 +#define CLI11_HAS_FILESYSTEM 1 +#elif defined(__GLIBCXX__) +// if we are using gcc and Version <9 default to no filesystem +#define CLI11_HAS_FILESYSTEM 0 +#else +#define CLI11_HAS_FILESYSTEM 1 +#endif +#else +#define CLI11_HAS_FILESYSTEM 0 +#endif +#endif +#endif +#endif + +/** availability */ +#if defined(__GNUC__) && !defined(__llvm__) && !defined(__INTEL_COMPILER) && __GNUC__ < 5 +#define CLI11_HAS_CODECVT 0 +#else +#define CLI11_HAS_CODECVT 1 +#include +#endif + +/** disable deprecations */ +#if defined(__GNUC__) // GCC or clang +#define CLI11_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push") +#define CLI11_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop") + +#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") + +#elif defined(_MSC_VER) +#define CLI11_DIAGNOSTIC_PUSH __pragma(warning(push)) +#define CLI11_DIAGNOSTIC_POP __pragma(warning(pop)) + +#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED __pragma(warning(disable : 4996)) + +#else +#define CLI11_DIAGNOSTIC_PUSH +#define CLI11_DIAGNOSTIC_POP + +#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED + +#endif + +/** Inline macro **/ +#ifdef CLI11_COMPILE +#define CLI11_INLINE +#else +#define CLI11_INLINE inline +#endif + + + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +#include // NOLINT(build/include) +#else +#include +#include +#endif + + + + +#ifdef CLI11_CPP17 +#include +#endif // CLI11_CPP17 + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +#include +#include // NOLINT(build/include) +#endif // CLI11_HAS_FILESYSTEM + + + +#if defined(_WIN32) +#if !(defined(_AMD64_) || defined(_X86_) || defined(_ARM_)) +#if defined(__amd64__) || defined(__amd64) || defined(__x86_64__) || defined(__x86_64) || defined(_M_X64) || \ + defined(_M_AMD64) +#define _AMD64_ +#elif defined(i386) || defined(__i386) || defined(__i386__) || defined(__i386__) || defined(_M_IX86) +#define _X86_ +#elif defined(__arm__) || defined(_M_ARM) || defined(_M_ARMT) +#define _ARM_ +#elif defined(__aarch64__) || defined(_M_ARM64) +#define _ARM64_ +#elif defined(_M_ARM64EC) +#define _ARM64EC_ +#endif +#endif + +// first +#ifndef NOMINMAX +// if NOMINMAX is already defined we don't want to mess with that either way +#define NOMINMAX +#include +#undef NOMINMAX +#else +#include +#endif + +// second +#include +// third +#include +#include +#endif + + +namespace CLI { + + +/// Convert a wide string to a narrow string. +CLI11_INLINE std::string narrow(const std::wstring &str); +CLI11_INLINE std::string narrow(const wchar_t *str); +CLI11_INLINE std::string narrow(const wchar_t *str, std::size_t size); + +/// Convert a narrow string to a wide string. +CLI11_INLINE std::wstring widen(const std::string &str); +CLI11_INLINE std::wstring widen(const char *str); +CLI11_INLINE std::wstring widen(const char *str, std::size_t size); + +#ifdef CLI11_CPP17 +CLI11_INLINE std::string narrow(std::wstring_view str); +CLI11_INLINE std::wstring widen(std::string_view str); +#endif // CLI11_CPP17 + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +/// Convert a char-string to a native path correctly. +CLI11_INLINE std::filesystem::path to_path(std::string_view str); +#endif // CLI11_HAS_FILESYSTEM + + + + +namespace detail { + +#if !CLI11_HAS_CODECVT +/// Attempt to set one of the acceptable unicode locales for conversion +CLI11_INLINE void set_unicode_locale() { + static const std::array unicode_locales{{"C.UTF-8", "en_US.UTF-8", ".UTF-8"}}; + + for(const auto &locale_name : unicode_locales) { + if(std::setlocale(LC_ALL, locale_name) != nullptr) { + return; + } + } + throw std::runtime_error("CLI::narrow: could not set locale to C.UTF-8"); +} + +template struct scope_guard_t { + F closure; + + explicit scope_guard_t(F closure_) : closure(closure_) {} + ~scope_guard_t() { closure(); } +}; + +template CLI11_NODISCARD CLI11_INLINE scope_guard_t scope_guard(F &&closure) { + return scope_guard_t{std::forward(closure)}; +} + +#endif // !CLI11_HAS_CODECVT + +CLI11_DIAGNOSTIC_PUSH +CLI11_DIAGNOSTIC_IGNORE_DEPRECATED + +CLI11_INLINE std::string narrow_impl(const wchar_t *str, std::size_t str_size) { +#if CLI11_HAS_CODECVT +#ifdef _WIN32 + return std::wstring_convert>().to_bytes(str, str + str_size); + +#else + return std::wstring_convert>().to_bytes(str, str + str_size); + +#endif // _WIN32 +#else // CLI11_HAS_CODECVT + (void)str_size; + std::mbstate_t state = std::mbstate_t(); + const wchar_t *it = str; + + std::string old_locale = std::setlocale(LC_ALL, nullptr); + auto sg = scope_guard([&] { std::setlocale(LC_ALL, old_locale.c_str()); }); + set_unicode_locale(); + + std::size_t new_size = std::wcsrtombs(nullptr, &it, 0, &state); + if(new_size == static_cast(-1)) { + throw std::runtime_error("CLI::narrow: conversion error in std::wcsrtombs at offset " + + std::to_string(it - str)); + } + std::string result(new_size, '\0'); + std::wcsrtombs(const_cast(result.data()), &str, new_size, &state); + + return result; + +#endif // CLI11_HAS_CODECVT +} + +CLI11_INLINE std::wstring widen_impl(const char *str, std::size_t str_size) { +#if CLI11_HAS_CODECVT +#ifdef _WIN32 + return std::wstring_convert>().from_bytes(str, str + str_size); + +#else + return std::wstring_convert>().from_bytes(str, str + str_size); + +#endif // _WIN32 +#else // CLI11_HAS_CODECVT + (void)str_size; + std::mbstate_t state = std::mbstate_t(); + const char *it = str; + + std::string old_locale = std::setlocale(LC_ALL, nullptr); + auto sg = scope_guard([&] { std::setlocale(LC_ALL, old_locale.c_str()); }); + set_unicode_locale(); + + std::size_t new_size = std::mbsrtowcs(nullptr, &it, 0, &state); + if(new_size == static_cast(-1)) { + throw std::runtime_error("CLI::widen: conversion error in std::mbsrtowcs at offset " + + std::to_string(it - str)); + } + std::wstring result(new_size, L'\0'); + std::mbsrtowcs(const_cast(result.data()), &str, new_size, &state); + + return result; + +#endif // CLI11_HAS_CODECVT +} + +CLI11_DIAGNOSTIC_POP + +} // namespace detail + +CLI11_INLINE std::string narrow(const wchar_t *str, std::size_t str_size) { return detail::narrow_impl(str, str_size); } +CLI11_INLINE std::string narrow(const std::wstring &str) { return detail::narrow_impl(str.data(), str.size()); } +// Flawfinder: ignore +CLI11_INLINE std::string narrow(const wchar_t *str) { return detail::narrow_impl(str, std::wcslen(str)); } + +CLI11_INLINE std::wstring widen(const char *str, std::size_t str_size) { return detail::widen_impl(str, str_size); } +CLI11_INLINE std::wstring widen(const std::string &str) { return detail::widen_impl(str.data(), str.size()); } +// Flawfinder: ignore +CLI11_INLINE std::wstring widen(const char *str) { return detail::widen_impl(str, std::strlen(str)); } + +#ifdef CLI11_CPP17 +CLI11_INLINE std::string narrow(std::wstring_view str) { return detail::narrow_impl(str.data(), str.size()); } +CLI11_INLINE std::wstring widen(std::string_view str) { return detail::widen_impl(str.data(), str.size()); } +#endif // CLI11_CPP17 + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +CLI11_INLINE std::filesystem::path to_path(std::string_view str) { + return std::filesystem::path{ +#ifdef _WIN32 + widen(str) +#else + str +#endif // _WIN32 + }; +} +#endif // CLI11_HAS_FILESYSTEM + + + + +namespace detail { +#ifdef _WIN32 +/// Decode and return UTF-8 argv from GetCommandLineW. +CLI11_INLINE std::vector compute_win32_argv(); +#endif +} // namespace detail + + + +namespace detail { + +#ifdef _WIN32 +CLI11_INLINE std::vector compute_win32_argv() { + std::vector result; + int argc = 0; + + auto deleter = [](wchar_t **ptr) { LocalFree(ptr); }; + // NOLINTBEGIN(*-avoid-c-arrays) + auto wargv = std::unique_ptr(CommandLineToArgvW(GetCommandLineW(), &argc), deleter); + // NOLINTEND(*-avoid-c-arrays) + + if(wargv == nullptr) { + throw std::runtime_error("CommandLineToArgvW failed with code " + std::to_string(GetLastError())); + } + + result.reserve(static_cast(argc)); + for(size_t i = 0; i < static_cast(argc); ++i) { + result.push_back(narrow(wargv[i])); + } + + return result; +} +#endif + +} // namespace detail + + + + +/// Include the items in this namespace to get free conversion of enums to/from streams. +/// (This is available inside CLI as well, so CLI11 will use this without a using statement). +namespace enums { + +/// output streaming for enumerations +template ::value>::type> +std::ostream &operator<<(std::ostream &in, const T &item) { + // make sure this is out of the detail namespace otherwise it won't be found when needed + return in << static_cast::type>(item); +} + +} // namespace enums + +/// Export to CLI namespace +using enums::operator<<; + +namespace detail { +/// a constant defining an expected max vector size defined to be a big number that could be multiplied by 4 and not +/// produce overflow for some expected uses +constexpr int expected_max_vector_size{1 << 29}; +// Based on http://stackoverflow.com/questions/236129/split-a-string-in-c +/// Split a string by a delim +CLI11_INLINE std::vector split(const std::string &s, char delim); + +/// Simple function to join a string +template std::string join(const T &v, std::string delim = ",") { + std::ostringstream s; + auto beg = std::begin(v); + auto end = std::end(v); + if(beg != end) + s << *beg++; + while(beg != end) { + s << delim << *beg++; + } + return s.str(); +} + +/// Simple function to join a string from processed elements +template ::value>::type> +std::string join(const T &v, Callable func, std::string delim = ",") { + std::ostringstream s; + auto beg = std::begin(v); + auto end = std::end(v); + auto loc = s.tellp(); + while(beg != end) { + auto nloc = s.tellp(); + if(nloc > loc) { + s << delim; + loc = nloc; + } + s << func(*beg++); + } + return s.str(); +} + +/// Join a string in reverse order +template std::string rjoin(const T &v, std::string delim = ",") { + std::ostringstream s; + for(std::size_t start = 0; start < v.size(); start++) { + if(start > 0) + s << delim; + s << v[v.size() - start - 1]; + } + return s.str(); +} + +// Based roughly on http://stackoverflow.com/questions/25829143/c-trim-whitespace-from-a-string + +/// Trim whitespace from left of string +CLI11_INLINE std::string <rim(std::string &str); + +/// Trim anything from left of string +CLI11_INLINE std::string <rim(std::string &str, const std::string &filter); + +/// Trim whitespace from right of string +CLI11_INLINE std::string &rtrim(std::string &str); + +/// Trim anything from right of string +CLI11_INLINE std::string &rtrim(std::string &str, const std::string &filter); + +/// Trim whitespace from string +inline std::string &trim(std::string &str) { return ltrim(rtrim(str)); } + +/// Trim anything from string +inline std::string &trim(std::string &str, const std::string filter) { return ltrim(rtrim(str, filter), filter); } + +/// Make a copy of the string and then trim it +inline std::string trim_copy(const std::string &str) { + std::string s = str; + return trim(s); +} + +/// remove quotes at the front and back of a string either '"' or '\'' +CLI11_INLINE std::string &remove_quotes(std::string &str); + +/// remove quotes from all elements of a string vector and process escaped components +CLI11_INLINE void remove_quotes(std::vector &args); + +/// Add a leader to the beginning of all new lines (nothing is added +/// at the start of the first line). `"; "` would be for ini files +/// +/// Can't use Regex, or this would be a subs. +CLI11_INLINE std::string fix_newlines(const std::string &leader, std::string input); + +/// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered) +inline std::string trim_copy(const std::string &str, const std::string &filter) { + std::string s = str; + return trim(s, filter); +} +/// Print a two part "help" string +CLI11_INLINE std::ostream & +format_help(std::ostream &out, std::string name, const std::string &description, std::size_t wid); + +/// Print subcommand aliases +CLI11_INLINE std::ostream &format_aliases(std::ostream &out, const std::vector &aliases, std::size_t wid); + +/// Verify the first character of an option +/// - is a trigger character, ! has special meaning and new lines would just be annoying to deal with +template bool valid_first_char(T c) { + return ((c != '-') && (static_cast(c) > 33)); // space and '!' not allowed +} + +/// Verify following characters of an option +template bool valid_later_char(T c) { + // = and : are value separators, { has special meaning for option defaults, + // and control codes other than tab would just be annoying to deal with in many places allowing space here has too + // much potential for inadvertent entry errors and bugs + return ((c != '=') && (c != ':') && (c != '{') && ((static_cast(c) > 32) || c == '\t')); +} + +/// Verify an option/subcommand name +CLI11_INLINE bool valid_name_string(const std::string &str); + +/// Verify an app name +inline bool valid_alias_name_string(const std::string &str) { + static const std::string badChars(std::string("\n") + '\0'); + return (str.find_first_of(badChars) == std::string::npos); +} + +/// check if a string is a container segment separator (empty or "%%") +inline bool is_separator(const std::string &str) { + static const std::string sep("%%"); + return (str.empty() || str == sep); +} + +/// Verify that str consists of letters only +inline bool isalpha(const std::string &str) { + return std::all_of(str.begin(), str.end(), [](char c) { return std::isalpha(c, std::locale()); }); +} + +/// Return a lower case version of a string +inline std::string to_lower(std::string str) { + std::transform(std::begin(str), std::end(str), std::begin(str), [](const std::string::value_type &x) { + return std::tolower(x, std::locale()); + }); + return str; +} + +/// remove underscores from a string +inline std::string remove_underscore(std::string str) { + str.erase(std::remove(std::begin(str), std::end(str), '_'), std::end(str)); + return str; +} + +/// Find and replace a substring with another substring +CLI11_INLINE std::string find_and_replace(std::string str, std::string from, std::string to); + +/// check if the flag definitions has possible false flags +inline bool has_default_flag_values(const std::string &flags) { + return (flags.find_first_of("{!") != std::string::npos); +} + +CLI11_INLINE void remove_default_flag_values(std::string &flags); + +/// Check if a string is a member of a list of strings and optionally ignore case or ignore underscores +CLI11_INLINE std::ptrdiff_t find_member(std::string name, + const std::vector names, + bool ignore_case = false, + bool ignore_underscore = false); + +/// Find a trigger string and call a modify callable function that takes the current string and starting position of the +/// trigger and returns the position in the string to search for the next trigger string +template inline std::string find_and_modify(std::string str, std::string trigger, Callable modify) { + std::size_t start_pos = 0; + while((start_pos = str.find(trigger, start_pos)) != std::string::npos) { + start_pos = modify(str, start_pos); + } + return str; +} + +/// close a sequence of characters indicated by a closure character. Brackets allows sub sequences +/// recognized bracket sequences include "'`[(<{ other closure characters are assumed to be literal strings +CLI11_INLINE std::size_t close_sequence(const std::string &str, std::size_t start, char closure_char); + +/// Split a string '"one two" "three"' into 'one two', 'three' +/// Quote characters can be ` ' or " or bracket characters [{(< with matching to the matching bracket +CLI11_INLINE std::vector split_up(std::string str, char delimiter = '\0'); + +/// get the value of an environmental variable or empty string if empty +CLI11_INLINE std::string get_environment_value(const std::string &env_name); + +/// This function detects an equal or colon followed by an escaped quote after an argument +/// then modifies the string to replace the equality with a space. This is needed +/// to allow the split up function to work properly and is intended to be used with the find_and_modify function +/// the return value is the offset+1 which is required by the find_and_modify function. +CLI11_INLINE std::size_t escape_detect(std::string &str, std::size_t offset); + +/// @brief detect if a string has escapable characters +/// @param str the string to do the detection on +/// @return true if the string has escapable characters +CLI11_INLINE bool has_escapable_character(const std::string &str); + +/// @brief escape all escapable characters +/// @param str the string to escape +/// @return a string with the escapble characters escaped with '\' +CLI11_INLINE std::string add_escaped_characters(const std::string &str); + +/// @brief replace the escaped characters with their equivalent +CLI11_INLINE std::string remove_escaped_characters(const std::string &str); + +/// generate a string with all non printable characters escaped to hex codes +CLI11_INLINE std::string binary_escape_string(const std::string &string_to_escape); + +CLI11_INLINE bool is_binary_escaped_string(const std::string &escaped_string); + +/// extract an escaped binary_string +CLI11_INLINE std::string extract_binary_string(const std::string &escaped_string); + +/// process a quoted string, remove the quotes and if appropriate handle escaped characters +CLI11_INLINE bool process_quoted_string(std::string &str, char string_char = '\"', char literal_char = '\''); + +} // namespace detail + + + + +namespace detail { +CLI11_INLINE std::vector split(const std::string &s, char delim) { + std::vector elems; + // Check to see if empty string, give consistent result + if(s.empty()) { + elems.emplace_back(); + } else { + std::stringstream ss; + ss.str(s); + std::string item; + while(std::getline(ss, item, delim)) { + elems.push_back(item); + } + } + return elems; +} + +CLI11_INLINE std::string <rim(std::string &str) { + auto it = std::find_if(str.begin(), str.end(), [](char ch) { return !std::isspace(ch, std::locale()); }); + str.erase(str.begin(), it); + return str; +} + +CLI11_INLINE std::string <rim(std::string &str, const std::string &filter) { + auto it = std::find_if(str.begin(), str.end(), [&filter](char ch) { return filter.find(ch) == std::string::npos; }); + str.erase(str.begin(), it); + return str; +} + +CLI11_INLINE std::string &rtrim(std::string &str) { + auto it = std::find_if(str.rbegin(), str.rend(), [](char ch) { return !std::isspace(ch, std::locale()); }); + str.erase(it.base(), str.end()); + return str; +} + +CLI11_INLINE std::string &rtrim(std::string &str, const std::string &filter) { + auto it = + std::find_if(str.rbegin(), str.rend(), [&filter](char ch) { return filter.find(ch) == std::string::npos; }); + str.erase(it.base(), str.end()); + return str; +} + +CLI11_INLINE std::string &remove_quotes(std::string &str) { + if(str.length() > 1 && (str.front() == '"' || str.front() == '\'' || str.front() == '`')) { + if(str.front() == str.back()) { + str.pop_back(); + str.erase(str.begin(), str.begin() + 1); + } + } + return str; +} + +CLI11_INLINE std::string &remove_outer(std::string &str, char key) { + if(str.length() > 1 && (str.front() == key)) { + if(str.front() == str.back()) { + str.pop_back(); + str.erase(str.begin(), str.begin() + 1); + } + } + return str; +} + +CLI11_INLINE std::string fix_newlines(const std::string &leader, std::string input) { + std::string::size_type n = 0; + while(n != std::string::npos && n < input.size()) { + n = input.find('\n', n); + if(n != std::string::npos) { + input = input.substr(0, n + 1) + leader + input.substr(n + 1); + n += leader.size(); + } + } + return input; +} + +CLI11_INLINE std::ostream & +format_help(std::ostream &out, std::string name, const std::string &description, std::size_t wid) { + name = " " + name; + out << std::setw(static_cast(wid)) << std::left << name; + if(!description.empty()) { + if(name.length() >= wid) + out << "\n" << std::setw(static_cast(wid)) << ""; + for(const char c : description) { + out.put(c); + if(c == '\n') { + out << std::setw(static_cast(wid)) << ""; + } + } + } + out << "\n"; + return out; +} + +CLI11_INLINE std::ostream &format_aliases(std::ostream &out, const std::vector &aliases, std::size_t wid) { + if(!aliases.empty()) { + out << std::setw(static_cast(wid)) << " aliases: "; + bool front = true; + for(const auto &alias : aliases) { + if(!front) { + out << ", "; + } else { + front = false; + } + out << detail::fix_newlines(" ", alias); + } + out << "\n"; + } + return out; +} + +CLI11_INLINE bool valid_name_string(const std::string &str) { + if(str.empty() || !valid_first_char(str[0])) { + return false; + } + auto e = str.end(); + for(auto c = str.begin() + 1; c != e; ++c) + if(!valid_later_char(*c)) + return false; + return true; +} + +CLI11_INLINE std::string find_and_replace(std::string str, std::string from, std::string to) { + + std::size_t start_pos = 0; + + while((start_pos = str.find(from, start_pos)) != std::string::npos) { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); + } + + return str; +} + +CLI11_INLINE void remove_default_flag_values(std::string &flags) { + auto loc = flags.find_first_of('{', 2); + while(loc != std::string::npos) { + auto finish = flags.find_first_of("},", loc + 1); + if((finish != std::string::npos) && (flags[finish] == '}')) { + flags.erase(flags.begin() + static_cast(loc), + flags.begin() + static_cast(finish) + 1); + } + loc = flags.find_first_of('{', loc + 1); + } + flags.erase(std::remove(flags.begin(), flags.end(), '!'), flags.end()); +} + +CLI11_INLINE std::ptrdiff_t +find_member(std::string name, const std::vector names, bool ignore_case, bool ignore_underscore) { + auto it = std::end(names); + if(ignore_case) { + if(ignore_underscore) { + name = detail::to_lower(detail::remove_underscore(name)); + it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { + return detail::to_lower(detail::remove_underscore(local_name)) == name; + }); + } else { + name = detail::to_lower(name); + it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { + return detail::to_lower(local_name) == name; + }); + } + + } else if(ignore_underscore) { + name = detail::remove_underscore(name); + it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { + return detail::remove_underscore(local_name) == name; + }); + } else { + it = std::find(std::begin(names), std::end(names), name); + } + + return (it != std::end(names)) ? (it - std::begin(names)) : (-1); +} + +static const std::string escapedChars("\b\t\n\f\r\"\\"); +static const std::string escapedCharsCode("btnfr\"\\"); +static const std::string bracketChars{"\"'`[(<{"}; +static const std::string matchBracketChars("\"'`])>}"); + +CLI11_INLINE bool has_escapable_character(const std::string &str) { + return (str.find_first_of(escapedChars) != std::string::npos); +} + +CLI11_INLINE std::string add_escaped_characters(const std::string &str) { + std::string out; + out.reserve(str.size() + 4); + for(char s : str) { + auto sloc = escapedChars.find_first_of(s); + if(sloc != std::string::npos) { + out.push_back('\\'); + out.push_back(escapedCharsCode[sloc]); + } else { + out.push_back(s); + } + } + return out; +} + +CLI11_INLINE std::uint32_t hexConvert(char hc) { + int hcode{0}; + if(hc >= '0' && hc <= '9') { + hcode = (hc - '0'); + } else if(hc >= 'A' && hc <= 'F') { + hcode = (hc - 'A' + 10); + } else if(hc >= 'a' && hc <= 'f') { + hcode = (hc - 'a' + 10); + } else { + hcode = -1; + } + return static_cast(hcode); +} + +CLI11_INLINE char make_char(std::uint32_t code) { return static_cast(static_cast(code)); } + +CLI11_INLINE void append_codepoint(std::string &str, std::uint32_t code) { + if(code < 0x80) { // ascii code equivalent + str.push_back(static_cast(code)); + } else if(code < 0x800) { // \u0080 to \u07FF + // 110yyyyx 10xxxxxx; 0x3f == 0b0011'1111 + str.push_back(make_char(0xC0 | code >> 6)); + str.push_back(make_char(0x80 | (code & 0x3F))); + } else if(code < 0x10000) { // U+0800...U+FFFF + if(0xD800 <= code && code <= 0xDFFF) { + throw std::invalid_argument("[0xD800, 0xDFFF] are not valid UTF-8."); + } + // 1110yyyy 10yxxxxx 10xxxxxx + str.push_back(make_char(0xE0 | code >> 12)); + str.push_back(make_char(0x80 | (code >> 6 & 0x3F))); + str.push_back(make_char(0x80 | (code & 0x3F))); + } else if(code < 0x110000) { // U+010000 ... U+10FFFF + // 11110yyy 10yyxxxx 10xxxxxx 10xxxxxx + str.push_back(make_char(0xF0 | code >> 18)); + str.push_back(make_char(0x80 | (code >> 12 & 0x3F))); + str.push_back(make_char(0x80 | (code >> 6 & 0x3F))); + str.push_back(make_char(0x80 | (code & 0x3F))); + } +} + +CLI11_INLINE std::string remove_escaped_characters(const std::string &str) { + + std::string out; + out.reserve(str.size()); + for(auto loc = str.begin(); loc < str.end(); ++loc) { + if(*loc == '\\') { + if(str.end() - loc < 2) { + throw std::invalid_argument("invalid escape sequence " + str); + } + auto ecloc = escapedCharsCode.find_first_of(*(loc + 1)); + if(ecloc != std::string::npos) { + out.push_back(escapedChars[ecloc]); + ++loc; + } else if(*(loc + 1) == 'u') { + // must have 4 hex characters + if(str.end() - loc < 6) { + throw std::invalid_argument("unicode sequence must have 4 hex codes " + str); + } + std::uint32_t code{0}; + std::uint32_t mplier{16 * 16 * 16}; + for(int ii = 2; ii < 6; ++ii) { + std::uint32_t res = hexConvert(*(loc + ii)); + if(res > 0x0F) { + throw std::invalid_argument("unicode sequence must have 4 hex codes " + str); + } + code += res * mplier; + mplier = mplier / 16; + } + append_codepoint(out, code); + loc += 5; + } else if(*(loc + 1) == 'U') { + // must have 8 hex characters + if(str.end() - loc < 10) { + throw std::invalid_argument("unicode sequence must have 8 hex codes " + str); + } + std::uint32_t code{0}; + std::uint32_t mplier{16 * 16 * 16 * 16 * 16 * 16 * 16}; + for(int ii = 2; ii < 10; ++ii) { + std::uint32_t res = hexConvert(*(loc + ii)); + if(res > 0x0F) { + throw std::invalid_argument("unicode sequence must have 8 hex codes " + str); + } + code += res * mplier; + mplier = mplier / 16; + } + append_codepoint(out, code); + loc += 9; + } else if(*(loc + 1) == '0') { + out.push_back('\0'); + ++loc; + } else { + throw std::invalid_argument(std::string("unrecognized escape sequence \\") + *(loc + 1) + " in " + str); + } + } else { + out.push_back(*loc); + } + } + return out; +} + +CLI11_INLINE std::size_t close_string_quote(const std::string &str, std::size_t start, char closure_char) { + std::size_t loc{0}; + for(loc = start + 1; loc < str.size(); ++loc) { + if(str[loc] == closure_char) { + break; + } + if(str[loc] == '\\') { + // skip the next character for escaped sequences + ++loc; + } + } + return loc; +} + +CLI11_INLINE std::size_t close_literal_quote(const std::string &str, std::size_t start, char closure_char) { + auto loc = str.find_first_of(closure_char, start + 1); + return (loc != std::string::npos ? loc : str.size()); +} + +CLI11_INLINE std::size_t close_sequence(const std::string &str, std::size_t start, char closure_char) { + + auto bracket_loc = matchBracketChars.find(closure_char); + switch(bracket_loc) { + case 0: + return close_string_quote(str, start, closure_char); + case 1: + case 2: + case std::string::npos: + return close_literal_quote(str, start, closure_char); + default: + break; + } + + std::string closures(1, closure_char); + auto loc = start + 1; + + while(loc < str.size()) { + if(str[loc] == closures.back()) { + closures.pop_back(); + if(closures.empty()) { + return loc; + } + } + bracket_loc = bracketChars.find(str[loc]); + if(bracket_loc != std::string::npos) { + switch(bracket_loc) { + case 0: + loc = close_string_quote(str, loc, str[loc]); + break; + case 1: + case 2: + loc = close_literal_quote(str, loc, str[loc]); + break; + default: + closures.push_back(matchBracketChars[bracket_loc]); + break; + } + } + ++loc; + } + if(loc > str.size()) { + loc = str.size(); + } + return loc; +} + +CLI11_INLINE std::vector split_up(std::string str, char delimiter) { + + auto find_ws = [delimiter](char ch) { + return (delimiter == '\0') ? std::isspace(ch, std::locale()) : (ch == delimiter); + }; + trim(str); + + std::vector output; + while(!str.empty()) { + if(bracketChars.find_first_of(str[0]) != std::string::npos) { + auto bracketLoc = bracketChars.find_first_of(str[0]); + auto end = close_sequence(str, 0, matchBracketChars[bracketLoc]); + if(end >= str.size()) { + output.push_back(std::move(str)); + str.clear(); + } else { + output.push_back(str.substr(0, end + 1)); + if(end + 2 < str.size()) { + str = str.substr(end + 2); + } else { + str.clear(); + } + } + + } else { + auto it = std::find_if(std::begin(str), std::end(str), find_ws); + if(it != std::end(str)) { + std::string value = std::string(str.begin(), it); + output.push_back(value); + str = std::string(it + 1, str.end()); + } else { + output.push_back(str); + str.clear(); + } + } + trim(str); + } + return output; +} + +CLI11_INLINE std::size_t escape_detect(std::string &str, std::size_t offset) { + auto next = str[offset + 1]; + if((next == '\"') || (next == '\'') || (next == '`')) { + auto astart = str.find_last_of("-/ \"\'`", offset - 1); + if(astart != std::string::npos) { + if(str[astart] == ((str[offset] == '=') ? '-' : '/')) + str[offset] = ' '; // interpret this as a space so the split_up works properly + } + } + return offset + 1; +} + +CLI11_INLINE std::string binary_escape_string(const std::string &string_to_escape) { + // s is our escaped output string + std::string escaped_string{}; + // loop through all characters + for(char c : string_to_escape) { + // check if a given character is printable + // the cast is necessary to avoid undefined behaviour + if(isprint(static_cast(c)) == 0) { + std::stringstream stream; + // if the character is not printable + // we'll convert it to a hex string using a stringstream + // note that since char is signed we have to cast it to unsigned first + stream << std::hex << static_cast(static_cast(c)); + std::string code = stream.str(); + escaped_string += std::string("\\x") + (code.size() < 2 ? "0" : "") + code; + + } else { + escaped_string.push_back(c); + } + } + if(escaped_string != string_to_escape) { + auto sqLoc = escaped_string.find('\''); + while(sqLoc != std::string::npos) { + escaped_string.replace(sqLoc, sqLoc + 1, "\\x27"); + sqLoc = escaped_string.find('\''); + } + escaped_string.insert(0, "'B\"("); + escaped_string.push_back(')'); + escaped_string.push_back('"'); + escaped_string.push_back('\''); + } + return escaped_string; +} + +CLI11_INLINE bool is_binary_escaped_string(const std::string &escaped_string) { + size_t ssize = escaped_string.size(); + if(escaped_string.compare(0, 3, "B\"(") == 0 && escaped_string.compare(ssize - 2, 2, ")\"") == 0) { + return true; + } + return (escaped_string.compare(0, 4, "'B\"(") == 0 && escaped_string.compare(ssize - 3, 3, ")\"'") == 0); +} + +CLI11_INLINE std::string extract_binary_string(const std::string &escaped_string) { + std::size_t start{0}; + std::size_t tail{0}; + size_t ssize = escaped_string.size(); + if(escaped_string.compare(0, 3, "B\"(") == 0 && escaped_string.compare(ssize - 2, 2, ")\"") == 0) { + start = 3; + tail = 2; + } else if(escaped_string.compare(0, 4, "'B\"(") == 0 && escaped_string.compare(ssize - 3, 3, ")\"'") == 0) { + start = 4; + tail = 3; + } + + if(start == 0) { + return escaped_string; + } + std::string outstring; + + outstring.reserve(ssize - start - tail); + std::size_t loc = start; + while(loc < ssize - tail) { + // ssize-2 to skip )" at the end + if(escaped_string[loc] == '\\' && (escaped_string[loc + 1] == 'x' || escaped_string[loc + 1] == 'X')) { + auto c1 = escaped_string[loc + 2]; + auto c2 = escaped_string[loc + 3]; + + std::uint32_t res1 = hexConvert(c1); + std::uint32_t res2 = hexConvert(c2); + if(res1 <= 0x0F && res2 <= 0x0F) { + loc += 4; + outstring.push_back(static_cast(res1 * 16 + res2)); + continue; + } + } + outstring.push_back(escaped_string[loc]); + ++loc; + } + return outstring; +} + +CLI11_INLINE void remove_quotes(std::vector &args) { + for(auto &arg : args) { + if(arg.front() == '\"' && arg.back() == '\"') { + remove_quotes(arg); + // only remove escaped for string arguments not literal strings + arg = remove_escaped_characters(arg); + } else { + remove_quotes(arg); + } + } +} + +CLI11_INLINE bool process_quoted_string(std::string &str, char string_char, char literal_char) { + if(str.size() <= 1) { + return false; + } + if(detail::is_binary_escaped_string(str)) { + str = detail::extract_binary_string(str); + return true; + } + if(str.front() == string_char && str.back() == string_char) { + detail::remove_outer(str, string_char); + if(str.find_first_of('\\') != std::string::npos) { + str = detail::remove_escaped_characters(str); + } + return true; + } + if((str.front() == literal_char || str.front() == '`') && str.back() == str.front()) { + detail::remove_outer(str, str.front()); + return true; + } + return false; +} + +std::string get_environment_value(const std::string &env_name) { + char *buffer = nullptr; + std::string ename_string; + +#ifdef _MSC_VER + // Windows version + std::size_t sz = 0; + if(_dupenv_s(&buffer, &sz, env_name.c_str()) == 0 && buffer != nullptr) { + ename_string = std::string(buffer); + free(buffer); + } +#else + // This also works on Windows, but gives a warning + buffer = std::getenv(env_name.c_str()); + if(buffer != nullptr) { + ename_string = std::string(buffer); + } +#endif + return ename_string; +} + +} // namespace detail + + + +// Use one of these on all error classes. +// These are temporary and are undef'd at the end of this file. +#define CLI11_ERROR_DEF(parent, name) \ + protected: \ + name(std::string ename, std::string msg, int exit_code) : parent(std::move(ename), std::move(msg), exit_code) {} \ + name(std::string ename, std::string msg, ExitCodes exit_code) \ + : parent(std::move(ename), std::move(msg), exit_code) {} \ + \ + public: \ + name(std::string msg, ExitCodes exit_code) : parent(#name, std::move(msg), exit_code) {} \ + name(std::string msg, int exit_code) : parent(#name, std::move(msg), exit_code) {} + +// This is added after the one above if a class is used directly and builds its own message +#define CLI11_ERROR_SIMPLE(name) \ + explicit name(std::string msg) : name(#name, msg, ExitCodes::name) {} + +/// These codes are part of every error in CLI. They can be obtained from e using e.exit_code or as a quick shortcut, +/// int values from e.get_error_code(). +enum class ExitCodes { + Success = 0, + IncorrectConstruction = 100, + BadNameString, + OptionAlreadyAdded, + FileError, + ConversionError, + ValidationError, + RequiredError, + RequiresError, + ExcludesError, + ExtrasError, + ConfigError, + InvalidError, + HorribleError, + OptionNotFound, + ArgumentMismatch, + BaseClass = 127 +}; + +// Error definitions + +/// @defgroup error_group Errors +/// @brief Errors thrown by CLI11 +/// +/// These are the errors that can be thrown. Some of them, like CLI::Success, are not really errors. +/// @{ + +/// All errors derive from this one +class Error : public std::runtime_error { + int actual_exit_code; + std::string error_name{"Error"}; + + public: + CLI11_NODISCARD int get_exit_code() const { return actual_exit_code; } + + CLI11_NODISCARD std::string get_name() const { return error_name; } + + Error(std::string name, std::string msg, int exit_code = static_cast(ExitCodes::BaseClass)) + : runtime_error(msg), actual_exit_code(exit_code), error_name(std::move(name)) {} + + Error(std::string name, std::string msg, ExitCodes exit_code) : Error(name, msg, static_cast(exit_code)) {} +}; + +// Note: Using Error::Error constructors does not work on GCC 4.7 + +/// Construction errors (not in parsing) +class ConstructionError : public Error { + CLI11_ERROR_DEF(Error, ConstructionError) +}; + +/// Thrown when an option is set to conflicting values (non-vector and multi args, for example) +class IncorrectConstruction : public ConstructionError { + CLI11_ERROR_DEF(ConstructionError, IncorrectConstruction) + CLI11_ERROR_SIMPLE(IncorrectConstruction) + static IncorrectConstruction PositionalFlag(std::string name) { + return IncorrectConstruction(name + ": Flags cannot be positional"); + } + static IncorrectConstruction Set0Opt(std::string name) { + return IncorrectConstruction(name + ": Cannot set 0 expected, use a flag instead"); + } + static IncorrectConstruction SetFlag(std::string name) { + return IncorrectConstruction(name + ": Cannot set an expected number for flags"); + } + static IncorrectConstruction ChangeNotVector(std::string name) { + return IncorrectConstruction(name + ": You can only change the expected arguments for vectors"); + } + static IncorrectConstruction AfterMultiOpt(std::string name) { + return IncorrectConstruction( + name + ": You can't change expected arguments after you've changed the multi option policy!"); + } + static IncorrectConstruction MissingOption(std::string name) { + return IncorrectConstruction("Option " + name + " is not defined"); + } + static IncorrectConstruction MultiOptionPolicy(std::string name) { + return IncorrectConstruction(name + ": multi_option_policy only works for flags and exact value options"); + } +}; + +/// Thrown on construction of a bad name +class BadNameString : public ConstructionError { + CLI11_ERROR_DEF(ConstructionError, BadNameString) + CLI11_ERROR_SIMPLE(BadNameString) + static BadNameString OneCharName(std::string name) { return BadNameString("Invalid one char name: " + name); } + static BadNameString MissingDash(std::string name) { + return BadNameString("Long names strings require 2 dashes " + name); + } + static BadNameString BadLongName(std::string name) { return BadNameString("Bad long name: " + name); } + static BadNameString BadPositionalName(std::string name) { + return BadNameString("Invalid positional Name: " + name); + } + static BadNameString DashesOnly(std::string name) { + return BadNameString("Must have a name, not just dashes: " + name); + } + static BadNameString MultiPositionalNames(std::string name) { + return BadNameString("Only one positional name allowed, remove: " + name); + } +}; + +/// Thrown when an option already exists +class OptionAlreadyAdded : public ConstructionError { + CLI11_ERROR_DEF(ConstructionError, OptionAlreadyAdded) + explicit OptionAlreadyAdded(std::string name) + : OptionAlreadyAdded(name + " is already added", ExitCodes::OptionAlreadyAdded) {} + static OptionAlreadyAdded Requires(std::string name, std::string other) { + return {name + " requires " + other, ExitCodes::OptionAlreadyAdded}; + } + static OptionAlreadyAdded Excludes(std::string name, std::string other) { + return {name + " excludes " + other, ExitCodes::OptionAlreadyAdded}; + } +}; + +// Parsing errors + +/// Anything that can error in Parse +class ParseError : public Error { + CLI11_ERROR_DEF(Error, ParseError) +}; + +// Not really "errors" + +/// This is a successful completion on parsing, supposed to exit +class Success : public ParseError { + CLI11_ERROR_DEF(ParseError, Success) + Success() : Success("Successfully completed, should be caught and quit", ExitCodes::Success) {} +}; + +/// -h or --help on command line +class CallForHelp : public Success { + CLI11_ERROR_DEF(Success, CallForHelp) + CallForHelp() : CallForHelp("This should be caught in your main function, see examples", ExitCodes::Success) {} +}; + +/// Usually something like --help-all on command line +class CallForAllHelp : public Success { + CLI11_ERROR_DEF(Success, CallForAllHelp) + CallForAllHelp() + : CallForAllHelp("This should be caught in your main function, see examples", ExitCodes::Success) {} +}; + +/// -v or --version on command line +class CallForVersion : public Success { + CLI11_ERROR_DEF(Success, CallForVersion) + CallForVersion() + : CallForVersion("This should be caught in your main function, see examples", ExitCodes::Success) {} +}; + +/// Does not output a diagnostic in CLI11_PARSE, but allows main() to return with a specific error code. +class RuntimeError : public ParseError { + CLI11_ERROR_DEF(ParseError, RuntimeError) + explicit RuntimeError(int exit_code = 1) : RuntimeError("Runtime error", exit_code) {} +}; + +/// Thrown when parsing an INI file and it is missing +class FileError : public ParseError { + CLI11_ERROR_DEF(ParseError, FileError) + CLI11_ERROR_SIMPLE(FileError) + static FileError Missing(std::string name) { return FileError(name + " was not readable (missing?)"); } +}; + +/// Thrown when conversion call back fails, such as when an int fails to coerce to a string +class ConversionError : public ParseError { + CLI11_ERROR_DEF(ParseError, ConversionError) + CLI11_ERROR_SIMPLE(ConversionError) + ConversionError(std::string member, std::string name) + : ConversionError("The value " + member + " is not an allowed value for " + name) {} + ConversionError(std::string name, std::vector results) + : ConversionError("Could not convert: " + name + " = " + detail::join(results)) {} + static ConversionError TooManyInputsFlag(std::string name) { + return ConversionError(name + ": too many inputs for a flag"); + } + static ConversionError TrueFalse(std::string name) { + return ConversionError(name + ": Should be true/false or a number"); + } +}; + +/// Thrown when validation of results fails +class ValidationError : public ParseError { + CLI11_ERROR_DEF(ParseError, ValidationError) + CLI11_ERROR_SIMPLE(ValidationError) + explicit ValidationError(std::string name, std::string msg) : ValidationError(name + ": " + msg) {} +}; + +/// Thrown when a required option is missing +class RequiredError : public ParseError { + CLI11_ERROR_DEF(ParseError, RequiredError) + explicit RequiredError(std::string name) : RequiredError(name + " is required", ExitCodes::RequiredError) {} + static RequiredError Subcommand(std::size_t min_subcom) { + if(min_subcom == 1) { + return RequiredError("A subcommand"); + } + return {"Requires at least " + std::to_string(min_subcom) + " subcommands", ExitCodes::RequiredError}; + } + static RequiredError + Option(std::size_t min_option, std::size_t max_option, std::size_t used, const std::string &option_list) { + if((min_option == 1) && (max_option == 1) && (used == 0)) + return RequiredError("Exactly 1 option from [" + option_list + "]"); + if((min_option == 1) && (max_option == 1) && (used > 1)) { + return {"Exactly 1 option from [" + option_list + "] is required but " + std::to_string(used) + + " were given", + ExitCodes::RequiredError}; + } + if((min_option == 1) && (used == 0)) + return RequiredError("At least 1 option from [" + option_list + "]"); + if(used < min_option) { + return {"Requires at least " + std::to_string(min_option) + " options used but only " + + std::to_string(used) + " were given from [" + option_list + "]", + ExitCodes::RequiredError}; + } + if(max_option == 1) + return {"Requires at most 1 options be given from [" + option_list + "]", ExitCodes::RequiredError}; + + return {"Requires at most " + std::to_string(max_option) + " options be used but " + std::to_string(used) + + " were given from [" + option_list + "]", + ExitCodes::RequiredError}; + } +}; + +/// Thrown when the wrong number of arguments has been received +class ArgumentMismatch : public ParseError { + CLI11_ERROR_DEF(ParseError, ArgumentMismatch) + CLI11_ERROR_SIMPLE(ArgumentMismatch) + ArgumentMismatch(std::string name, int expected, std::size_t received) + : ArgumentMismatch(expected > 0 ? ("Expected exactly " + std::to_string(expected) + " arguments to " + name + + ", got " + std::to_string(received)) + : ("Expected at least " + std::to_string(-expected) + " arguments to " + name + + ", got " + std::to_string(received)), + ExitCodes::ArgumentMismatch) {} + + static ArgumentMismatch AtLeast(std::string name, int num, std::size_t received) { + return ArgumentMismatch(name + ": At least " + std::to_string(num) + " required but received " + + std::to_string(received)); + } + static ArgumentMismatch AtMost(std::string name, int num, std::size_t received) { + return ArgumentMismatch(name + ": At Most " + std::to_string(num) + " required but received " + + std::to_string(received)); + } + static ArgumentMismatch TypedAtLeast(std::string name, int num, std::string type) { + return ArgumentMismatch(name + ": " + std::to_string(num) + " required " + type + " missing"); + } + static ArgumentMismatch FlagOverride(std::string name) { + return ArgumentMismatch(name + " was given a disallowed flag override"); + } + static ArgumentMismatch PartialType(std::string name, int num, std::string type) { + return ArgumentMismatch(name + ": " + type + " only partially specified: " + std::to_string(num) + + " required for each element"); + } +}; + +/// Thrown when a requires option is missing +class RequiresError : public ParseError { + CLI11_ERROR_DEF(ParseError, RequiresError) + RequiresError(std::string curname, std::string subname) + : RequiresError(curname + " requires " + subname, ExitCodes::RequiresError) {} +}; + +/// Thrown when an excludes option is present +class ExcludesError : public ParseError { + CLI11_ERROR_DEF(ParseError, ExcludesError) + ExcludesError(std::string curname, std::string subname) + : ExcludesError(curname + " excludes " + subname, ExitCodes::ExcludesError) {} +}; + +/// Thrown when too many positionals or options are found +class ExtrasError : public ParseError { + CLI11_ERROR_DEF(ParseError, ExtrasError) + explicit ExtrasError(std::vector args) + : ExtrasError((args.size() > 1 ? "The following arguments were not expected: " + : "The following argument was not expected: ") + + detail::rjoin(args, " "), + ExitCodes::ExtrasError) {} + ExtrasError(const std::string &name, std::vector args) + : ExtrasError(name, + (args.size() > 1 ? "The following arguments were not expected: " + : "The following argument was not expected: ") + + detail::rjoin(args, " "), + ExitCodes::ExtrasError) {} +}; + +/// Thrown when extra values are found in an INI file +class ConfigError : public ParseError { + CLI11_ERROR_DEF(ParseError, ConfigError) + CLI11_ERROR_SIMPLE(ConfigError) + static ConfigError Extras(std::string item) { return ConfigError("INI was not able to parse " + item); } + static ConfigError NotConfigurable(std::string item) { + return ConfigError(item + ": This option is not allowed in a configuration file"); + } +}; + +/// Thrown when validation fails before parsing +class InvalidError : public ParseError { + CLI11_ERROR_DEF(ParseError, InvalidError) + explicit InvalidError(std::string name) + : InvalidError(name + ": Too many positional arguments with unlimited expected args", ExitCodes::InvalidError) { + } +}; + +/// This is just a safety check to verify selection and parsing match - you should not ever see it +/// Strings are directly added to this error, but again, it should never be seen. +class HorribleError : public ParseError { + CLI11_ERROR_DEF(ParseError, HorribleError) + CLI11_ERROR_SIMPLE(HorribleError) +}; + +// After parsing + +/// Thrown when counting a non-existent option +class OptionNotFound : public Error { + CLI11_ERROR_DEF(Error, OptionNotFound) + explicit OptionNotFound(std::string name) : OptionNotFound(name + " not found", ExitCodes::OptionNotFound) {} +}; + +#undef CLI11_ERROR_DEF +#undef CLI11_ERROR_SIMPLE + +/// @} + + + + +// Type tools + +// Utilities for type enabling +namespace detail { +// Based generally on https://rmf.io/cxx11/almost-static-if +/// Simple empty scoped class +enum class enabler {}; + +/// An instance to use in EnableIf +constexpr enabler dummy = {}; +} // namespace detail + +/// A copy of enable_if_t from C++14, compatible with C++11. +/// +/// We could check to see if C++14 is being used, but it does not hurt to redefine this +/// (even Google does this: https://github.com/google/skia/blob/main/include/private/SkTLogic.h) +/// It is not in the std namespace anyway, so no harm done. +template using enable_if_t = typename std::enable_if::type; + +/// A copy of std::void_t from C++17 (helper for C++11 and C++14) +template struct make_void { + using type = void; +}; + +/// A copy of std::void_t from C++17 - same reasoning as enable_if_t, it does not hurt to redefine +template using void_t = typename make_void::type; + +/// A copy of std::conditional_t from C++14 - same reasoning as enable_if_t, it does not hurt to redefine +template using conditional_t = typename std::conditional::type; + +/// Check to see if something is bool (fail check by default) +template struct is_bool : std::false_type {}; + +/// Check to see if something is bool (true if actually a bool) +template <> struct is_bool : std::true_type {}; + +/// Check to see if something is a shared pointer +template struct is_shared_ptr : std::false_type {}; + +/// Check to see if something is a shared pointer (True if really a shared pointer) +template struct is_shared_ptr> : std::true_type {}; + +/// Check to see if something is a shared pointer (True if really a shared pointer) +template struct is_shared_ptr> : std::true_type {}; + +/// Check to see if something is copyable pointer +template struct is_copyable_ptr { + static bool const value = is_shared_ptr::value || std::is_pointer::value; +}; + +/// This can be specialized to override the type deduction for IsMember. +template struct IsMemberType { + using type = T; +}; + +/// The main custom type needed here is const char * should be a string. +template <> struct IsMemberType { + using type = std::string; +}; + +namespace adl_detail { +/// Check for existence of user-supplied lexical_cast. +/// +/// This struct has to be in a separate namespace so that it doesn't see our lexical_cast overloads in CLI::detail. +/// Standard says it shouldn't see them if it's defined before the corresponding lexical_cast declarations, but this +/// requires a working implementation of two-phase lookup, and not all compilers can boast that (msvc, ahem). +template class is_lexical_castable { + template + static auto test(int) -> decltype(lexical_cast(std::declval(), std::declval()), std::true_type()); + + template static auto test(...) -> std::false_type; + + public: + static constexpr bool value = decltype(test(0))::value; +}; +} // namespace adl_detail + +namespace detail { + +// These are utilities for IsMember and other transforming objects + +/// Handy helper to access the element_type generically. This is not part of is_copyable_ptr because it requires that +/// pointer_traits be valid. + +/// not a pointer +template struct element_type { + using type = T; +}; + +template struct element_type::value>::type> { + using type = typename std::pointer_traits::element_type; +}; + +/// Combination of the element type and value type - remove pointer (including smart pointers) and get the value_type of +/// the container +template struct element_value_type { + using type = typename element_type::type::value_type; +}; + +/// Adaptor for set-like structure: This just wraps a normal container in a few utilities that do almost nothing. +template struct pair_adaptor : std::false_type { + using value_type = typename T::value_type; + using first_type = typename std::remove_const::type; + using second_type = typename std::remove_const::type; + + /// Get the first value (really just the underlying value) + template static auto first(Q &&pair_value) -> decltype(std::forward(pair_value)) { + return std::forward(pair_value); + } + /// Get the second value (really just the underlying value) + template static auto second(Q &&pair_value) -> decltype(std::forward(pair_value)) { + return std::forward(pair_value); + } +}; + +/// Adaptor for map-like structure (true version, must have key_type and mapped_type). +/// This wraps a mapped container in a few utilities access it in a general way. +template +struct pair_adaptor< + T, + conditional_t, void>> + : std::true_type { + using value_type = typename T::value_type; + using first_type = typename std::remove_const::type; + using second_type = typename std::remove_const::type; + + /// Get the first value (really just the underlying value) + template static auto first(Q &&pair_value) -> decltype(std::get<0>(std::forward(pair_value))) { + return std::get<0>(std::forward(pair_value)); + } + /// Get the second value (really just the underlying value) + template static auto second(Q &&pair_value) -> decltype(std::get<1>(std::forward(pair_value))) { + return std::get<1>(std::forward(pair_value)); + } +}; + +// Warning is suppressed due to "bug" in gcc<5.0 and gcc 7.0 with c++17 enabled that generates a Wnarrowing warning +// in the unevaluated context even if the function that was using this wasn't used. The standard says narrowing in +// brace initialization shouldn't be allowed but for backwards compatibility gcc allows it in some contexts. It is a +// little fuzzy what happens in template constructs and I think that was something GCC took a little while to work out. +// But regardless some versions of gcc generate a warning when they shouldn't from the following code so that should be +// suppressed +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnarrowing" +#endif +// check for constructibility from a specific type and copy assignable used in the parse detection +template class is_direct_constructible { + template + static auto test(int, std::true_type) -> decltype( +// NVCC warns about narrowing conversions here +#ifdef __CUDACC__ +#ifdef __NVCC_DIAG_PRAGMA_SUPPORT__ +#pragma nv_diag_suppress 2361 +#else +#pragma diag_suppress 2361 +#endif +#endif + TT{std::declval()} +#ifdef __CUDACC__ +#ifdef __NVCC_DIAG_PRAGMA_SUPPORT__ +#pragma nv_diag_default 2361 +#else +#pragma diag_default 2361 +#endif +#endif + , + std::is_move_assignable()); + + template static auto test(int, std::false_type) -> std::false_type; + + template static auto test(...) -> std::false_type; + + public: + static constexpr bool value = decltype(test(0, typename std::is_constructible::type()))::value; +}; +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +// Check for output streamability +// Based on https://stackoverflow.com/questions/22758291/how-can-i-detect-if-a-type-can-be-streamed-to-an-stdostream + +template class is_ostreamable { + template + static auto test(int) -> decltype(std::declval() << std::declval(), std::true_type()); + + template static auto test(...) -> std::false_type; + + public: + static constexpr bool value = decltype(test(0))::value; +}; + +/// Check for input streamability +template class is_istreamable { + template + static auto test(int) -> decltype(std::declval() >> std::declval(), std::true_type()); + + template static auto test(...) -> std::false_type; + + public: + static constexpr bool value = decltype(test(0))::value; +}; + +/// Check for complex +template class is_complex { + template + static auto test(int) -> decltype(std::declval().real(), std::declval().imag(), std::true_type()); + + template static auto test(...) -> std::false_type; + + public: + static constexpr bool value = decltype(test(0))::value; +}; + +/// Templated operation to get a value from a stream +template ::value, detail::enabler> = detail::dummy> +bool from_stream(const std::string &istring, T &obj) { + std::istringstream is; + is.str(istring); + is >> obj; + return !is.fail() && !is.rdbuf()->in_avail(); +} + +template ::value, detail::enabler> = detail::dummy> +bool from_stream(const std::string & /*istring*/, T & /*obj*/) { + return false; +} + +// check to see if an object is a mutable container (fail by default) +template struct is_mutable_container : std::false_type {}; + +/// type trait to test if a type is a mutable container meaning it has a value_type, it has an iterator, a clear, and +/// end methods and an insert function. And for our purposes we exclude std::string and types that can be constructed +/// from a std::string +template +struct is_mutable_container< + T, + conditional_t().end()), + decltype(std::declval().clear()), + decltype(std::declval().insert(std::declval().end())>(), + std::declval()))>, + void>> : public conditional_t::value || + std::is_constructible::value, + std::false_type, + std::true_type> {}; + +// check to see if an object is a mutable container (fail by default) +template struct is_readable_container : std::false_type {}; + +/// type trait to test if a type is a container meaning it has a value_type, it has an iterator, a clear, and an end +/// methods and an insert function. And for our purposes we exclude std::string and types that can be constructed from +/// a std::string +template +struct is_readable_container< + T, + conditional_t().end()), decltype(std::declval().begin())>, void>> + : public std::true_type {}; + +// check to see if an object is a wrapper (fail by default) +template struct is_wrapper : std::false_type {}; + +// check if an object is a wrapper (it has a value_type defined) +template +struct is_wrapper, void>> : public std::true_type {}; + +// Check for tuple like types, as in classes with a tuple_size type trait +template class is_tuple_like { + template + // static auto test(int) + // -> decltype(std::conditional<(std::tuple_size::value > 0), std::true_type, std::false_type>::type()); + static auto test(int) -> decltype(std::tuple_size::type>::value, std::true_type{}); + template static auto test(...) -> std::false_type; + + public: + static constexpr bool value = decltype(test(0))::value; +}; + +/// Convert an object to a string (directly forward if this can become a string) +template ::value, detail::enabler> = detail::dummy> +auto to_string(T &&value) -> decltype(std::forward(value)) { + return std::forward(value); +} + +/// Construct a string from the object +template ::value && !std::is_convertible::value, + detail::enabler> = detail::dummy> +std::string to_string(const T &value) { + return std::string(value); // NOLINT(google-readability-casting) +} + +/// Convert an object to a string (streaming must be supported for that type) +template ::value && !std::is_constructible::value && + is_ostreamable::value, + detail::enabler> = detail::dummy> +std::string to_string(T &&value) { + std::stringstream stream; + stream << value; + return stream.str(); +} + +/// If conversion is not supported, return an empty string (streaming is not supported for that type) +template ::value && !is_ostreamable::value && + !is_readable_container::type>::value, + detail::enabler> = detail::dummy> +std::string to_string(T &&) { + return {}; +} + +/// convert a readable container to a string +template ::value && !is_ostreamable::value && + is_readable_container::value, + detail::enabler> = detail::dummy> +std::string to_string(T &&variable) { + auto cval = variable.begin(); + auto end = variable.end(); + if(cval == end) { + return {"{}"}; + } + std::vector defaults; + while(cval != end) { + defaults.emplace_back(CLI::detail::to_string(*cval)); + ++cval; + } + return {"[" + detail::join(defaults) + "]"}; +} + +/// special template overload +template ::value, detail::enabler> = detail::dummy> +auto checked_to_string(T &&value) -> decltype(to_string(std::forward(value))) { + return to_string(std::forward(value)); +} + +/// special template overload +template ::value, detail::enabler> = detail::dummy> +std::string checked_to_string(T &&) { + return std::string{}; +} +/// get a string as a convertible value for arithmetic types +template ::value, detail::enabler> = detail::dummy> +std::string value_string(const T &value) { + return std::to_string(value); +} +/// get a string as a convertible value for enumerations +template ::value, detail::enabler> = detail::dummy> +std::string value_string(const T &value) { + return std::to_string(static_cast::type>(value)); +} +/// for other types just use the regular to_string function +template ::value && !std::is_arithmetic::value, detail::enabler> = detail::dummy> +auto value_string(const T &value) -> decltype(to_string(value)) { + return to_string(value); +} + +/// template to get the underlying value type if it exists or use a default +template struct wrapped_type { + using type = def; +}; + +/// Type size for regular object types that do not look like a tuple +template struct wrapped_type::value>::type> { + using type = typename T::value_type; +}; + +/// This will only trigger for actual void type +template struct type_count_base { + static const int value{0}; +}; + +/// Type size for regular object types that do not look like a tuple +template +struct type_count_base::value && !is_mutable_container::value && + !std::is_void::value>::type> { + static constexpr int value{1}; +}; + +/// the base tuple size +template +struct type_count_base::value && !is_mutable_container::value>::type> { + static constexpr int value{std::tuple_size::value}; +}; + +/// Type count base for containers is the type_count_base of the individual element +template struct type_count_base::value>::type> { + static constexpr int value{type_count_base::value}; +}; + +/// Set of overloads to get the type size of an object + +/// forward declare the subtype_count structure +template struct subtype_count; + +/// forward declare the subtype_count_min structure +template struct subtype_count_min; + +/// This will only trigger for actual void type +template struct type_count { + static const int value{0}; +}; + +/// Type size for regular object types that do not look like a tuple +template +struct type_count::value && !is_tuple_like::value && !is_complex::value && + !std::is_void::value>::type> { + static constexpr int value{1}; +}; + +/// Type size for complex since it sometimes looks like a wrapper +template struct type_count::value>::type> { + static constexpr int value{2}; +}; + +/// Type size of types that are wrappers,except complex and tuples(which can also be wrappers sometimes) +template struct type_count::value>::type> { + static constexpr int value{subtype_count::value}; +}; + +/// Type size of types that are wrappers,except containers complex and tuples(which can also be wrappers sometimes) +template +struct type_count::value && !is_complex::value && !is_tuple_like::value && + !is_mutable_container::value>::type> { + static constexpr int value{type_count::value}; +}; + +/// 0 if the index > tuple size +template +constexpr typename std::enable_if::value, int>::type tuple_type_size() { + return 0; +} + +/// Recursively generate the tuple type name +template + constexpr typename std::enable_if < I::value, int>::type tuple_type_size() { + return subtype_count::type>::value + tuple_type_size(); +} + +/// Get the type size of the sum of type sizes for all the individual tuple types +template struct type_count::value>::type> { + static constexpr int value{tuple_type_size()}; +}; + +/// definition of subtype count +template struct subtype_count { + static constexpr int value{is_mutable_container::value ? expected_max_vector_size : type_count::value}; +}; + +/// This will only trigger for actual void type +template struct type_count_min { + static const int value{0}; +}; + +/// Type size for regular object types that do not look like a tuple +template +struct type_count_min< + T, + typename std::enable_if::value && !is_tuple_like::value && !is_wrapper::value && + !is_complex::value && !std::is_void::value>::type> { + static constexpr int value{type_count::value}; +}; + +/// Type size for complex since it sometimes looks like a wrapper +template struct type_count_min::value>::type> { + static constexpr int value{1}; +}; + +/// Type size min of types that are wrappers,except complex and tuples(which can also be wrappers sometimes) +template +struct type_count_min< + T, + typename std::enable_if::value && !is_complex::value && !is_tuple_like::value>::type> { + static constexpr int value{subtype_count_min::value}; +}; + +/// 0 if the index > tuple size +template +constexpr typename std::enable_if::value, int>::type tuple_type_size_min() { + return 0; +} + +/// Recursively generate the tuple type name +template + constexpr typename std::enable_if < I::value, int>::type tuple_type_size_min() { + return subtype_count_min::type>::value + tuple_type_size_min(); +} + +/// Get the type size of the sum of type sizes for all the individual tuple types +template struct type_count_min::value>::type> { + static constexpr int value{tuple_type_size_min()}; +}; + +/// definition of subtype count +template struct subtype_count_min { + static constexpr int value{is_mutable_container::value + ? ((type_count::value < expected_max_vector_size) ? type_count::value : 0) + : type_count_min::value}; +}; + +/// This will only trigger for actual void type +template struct expected_count { + static const int value{0}; +}; + +/// For most types the number of expected items is 1 +template +struct expected_count::value && !is_wrapper::value && + !std::is_void::value>::type> { + static constexpr int value{1}; +}; +/// number of expected items in a vector +template struct expected_count::value>::type> { + static constexpr int value{expected_max_vector_size}; +}; + +/// number of expected items in a vector +template +struct expected_count::value && is_wrapper::value>::type> { + static constexpr int value{expected_count::value}; +}; + +// Enumeration of the different supported categorizations of objects +enum class object_category : int { + char_value = 1, + integral_value = 2, + unsigned_integral = 4, + enumeration = 6, + boolean_value = 8, + floating_point = 10, + number_constructible = 12, + double_constructible = 14, + integer_constructible = 16, + // string like types + string_assignable = 23, + string_constructible = 24, + wstring_assignable = 25, + wstring_constructible = 26, + other = 45, + // special wrapper or container types + wrapper_value = 50, + complex_number = 60, + tuple_value = 70, + container_value = 80, + +}; + +/// Set of overloads to classify an object according to type + +/// some type that is not otherwise recognized +template struct classify_object { + static constexpr object_category value{object_category::other}; +}; + +/// Signed integers +template +struct classify_object< + T, + typename std::enable_if::value && !std::is_same::value && std::is_signed::value && + !is_bool::value && !std::is_enum::value>::type> { + static constexpr object_category value{object_category::integral_value}; +}; + +/// Unsigned integers +template +struct classify_object::value && std::is_unsigned::value && + !std::is_same::value && !is_bool::value>::type> { + static constexpr object_category value{object_category::unsigned_integral}; +}; + +/// single character values +template +struct classify_object::value && !std::is_enum::value>::type> { + static constexpr object_category value{object_category::char_value}; +}; + +/// Boolean values +template struct classify_object::value>::type> { + static constexpr object_category value{object_category::boolean_value}; +}; + +/// Floats +template struct classify_object::value>::type> { + static constexpr object_category value{object_category::floating_point}; +}; +#if defined _MSC_VER +// in MSVC wstring should take precedence if available this isn't as useful on other compilers due to the broader use of +// utf-8 encoding +#define WIDE_STRING_CHECK \ + !std::is_assignable::value && !std::is_constructible::value +#define STRING_CHECK true +#else +#define WIDE_STRING_CHECK true +#define STRING_CHECK !std::is_assignable::value && !std::is_constructible::value +#endif + +/// String and similar direct assignment +template +struct classify_object< + T, + typename std::enable_if::value && !std::is_integral::value && WIDE_STRING_CHECK && + std::is_assignable::value>::type> { + static constexpr object_category value{object_category::string_assignable}; +}; + +/// String and similar constructible and copy assignment +template +struct classify_object< + T, + typename std::enable_if::value && !std::is_integral::value && + !std::is_assignable::value && (type_count::value == 1) && + WIDE_STRING_CHECK && std::is_constructible::value>::type> { + static constexpr object_category value{object_category::string_constructible}; +}; + +/// Wide strings +template +struct classify_object::value && !std::is_integral::value && + STRING_CHECK && std::is_assignable::value>::type> { + static constexpr object_category value{object_category::wstring_assignable}; +}; + +template +struct classify_object< + T, + typename std::enable_if::value && !std::is_integral::value && + !std::is_assignable::value && (type_count::value == 1) && + STRING_CHECK && std::is_constructible::value>::type> { + static constexpr object_category value{object_category::wstring_constructible}; +}; + +/// Enumerations +template struct classify_object::value>::type> { + static constexpr object_category value{object_category::enumeration}; +}; + +template struct classify_object::value>::type> { + static constexpr object_category value{object_category::complex_number}; +}; + +/// Handy helper to contain a bunch of checks that rule out many common types (integers, string like, floating point, +/// vectors, and enumerations +template struct uncommon_type { + using type = typename std::conditional< + !std::is_floating_point::value && !std::is_integral::value && + !std::is_assignable::value && !std::is_constructible::value && + !std::is_assignable::value && !std::is_constructible::value && + !is_complex::value && !is_mutable_container::value && !std::is_enum::value, + std::true_type, + std::false_type>::type; + static constexpr bool value = type::value; +}; + +/// wrapper type +template +struct classify_object::value && is_wrapper::value && + !is_tuple_like::value && uncommon_type::value)>::type> { + static constexpr object_category value{object_category::wrapper_value}; +}; + +/// Assignable from double or int +template +struct classify_object::value && type_count::value == 1 && + !is_wrapper::value && is_direct_constructible::value && + is_direct_constructible::value>::type> { + static constexpr object_category value{object_category::number_constructible}; +}; + +/// Assignable from int +template +struct classify_object::value && type_count::value == 1 && + !is_wrapper::value && !is_direct_constructible::value && + is_direct_constructible::value>::type> { + static constexpr object_category value{object_category::integer_constructible}; +}; + +/// Assignable from double +template +struct classify_object::value && type_count::value == 1 && + !is_wrapper::value && is_direct_constructible::value && + !is_direct_constructible::value>::type> { + static constexpr object_category value{object_category::double_constructible}; +}; + +/// Tuple type +template +struct classify_object< + T, + typename std::enable_if::value && + ((type_count::value >= 2 && !is_wrapper::value) || + (uncommon_type::value && !is_direct_constructible::value && + !is_direct_constructible::value) || + (uncommon_type::value && type_count::value >= 2))>::type> { + static constexpr object_category value{object_category::tuple_value}; + // the condition on this class requires it be like a tuple, but on some compilers (like Xcode) tuples can be + // constructed from just the first element so tuples of can be constructed from a string, which + // could lead to issues so there are two variants of the condition, the first isolates things with a type size >=2 + // mainly to get tuples on Xcode with the exception of wrappers, the second is the main one and just separating out + // those cases that are caught by other object classifications +}; + +/// container type +template struct classify_object::value>::type> { + static constexpr object_category value{object_category::container_value}; +}; + +// Type name print + +/// Was going to be based on +/// http://stackoverflow.com/questions/1055452/c-get-name-of-type-in-template +/// But this is cleaner and works better in this case + +template ::value == object_category::char_value, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "CHAR"; +} + +template ::value == object_category::integral_value || + classify_object::value == object_category::integer_constructible, + detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "INT"; +} + +template ::value == object_category::unsigned_integral, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "UINT"; +} + +template ::value == object_category::floating_point || + classify_object::value == object_category::number_constructible || + classify_object::value == object_category::double_constructible, + detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "FLOAT"; +} + +/// Print name for enumeration types +template ::value == object_category::enumeration, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "ENUM"; +} + +/// Print name for enumeration types +template ::value == object_category::boolean_value, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "BOOLEAN"; +} + +/// Print name for enumeration types +template ::value == object_category::complex_number, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "COMPLEX"; +} + +/// Print for all other types +template ::value >= object_category::string_assignable && + classify_object::value <= object_category::other, + detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "TEXT"; +} +/// typename for tuple value +template ::value == object_category::tuple_value && type_count_base::value >= 2, + detail::enabler> = detail::dummy> +std::string type_name(); // forward declaration + +/// Generate type name for a wrapper or container value +template ::value == object_category::container_value || + classify_object::value == object_category::wrapper_value, + detail::enabler> = detail::dummy> +std::string type_name(); // forward declaration + +/// Print name for single element tuple types +template ::value == object_category::tuple_value && type_count_base::value == 1, + detail::enabler> = detail::dummy> +inline std::string type_name() { + return type_name::type>::type>(); +} + +/// Empty string if the index > tuple size +template +inline typename std::enable_if::value, std::string>::type tuple_name() { + return std::string{}; +} + +/// Recursively generate the tuple type name +template +inline typename std::enable_if<(I < type_count_base::value), std::string>::type tuple_name() { + auto str = std::string{type_name::type>::type>()} + ',' + + tuple_name(); + if(str.back() == ',') + str.pop_back(); + return str; +} + +/// Print type name for tuples with 2 or more elements +template ::value == object_category::tuple_value && type_count_base::value >= 2, + detail::enabler>> +inline std::string type_name() { + auto tname = std::string(1, '[') + tuple_name(); + tname.push_back(']'); + return tname; +} + +/// get the type name for a type that has a value_type member +template ::value == object_category::container_value || + classify_object::value == object_category::wrapper_value, + detail::enabler>> +inline std::string type_name() { + return type_name(); +} + +// Lexical cast + +/// Convert to an unsigned integral +template ::value, detail::enabler> = detail::dummy> +bool integral_conversion(const std::string &input, T &output) noexcept { + if(input.empty() || input.front() == '-') { + return false; + } + char *val{nullptr}; + errno = 0; + std::uint64_t output_ll = std::strtoull(input.c_str(), &val, 0); + if(errno == ERANGE) { + return false; + } + output = static_cast(output_ll); + if(val == (input.c_str() + input.size()) && static_cast(output) == output_ll) { + return true; + } + val = nullptr; + std::int64_t output_sll = std::strtoll(input.c_str(), &val, 0); + if(val == (input.c_str() + input.size())) { + output = (output_sll < 0) ? static_cast(0) : static_cast(output_sll); + return (static_cast(output) == output_sll); + } + // remove separators + if(input.find_first_of("_'") != std::string::npos) { + std::string nstring = input; + nstring.erase(std::remove(nstring.begin(), nstring.end(), '_'), nstring.end()); + nstring.erase(std::remove(nstring.begin(), nstring.end(), '\''), nstring.end()); + return integral_conversion(nstring, output); + } + if(input.compare(0, 2, "0o") == 0) { + val = nullptr; + errno = 0; + output_ll = std::strtoull(input.c_str() + 2, &val, 8); + if(errno == ERANGE) { + return false; + } + output = static_cast(output_ll); + return (val == (input.c_str() + input.size()) && static_cast(output) == output_ll); + } + if(input.compare(0, 2, "0b") == 0) { + val = nullptr; + errno = 0; + output_ll = std::strtoull(input.c_str() + 2, &val, 2); + if(errno == ERANGE) { + return false; + } + output = static_cast(output_ll); + return (val == (input.c_str() + input.size()) && static_cast(output) == output_ll); + } + return false; +} + +/// Convert to a signed integral +template ::value, detail::enabler> = detail::dummy> +bool integral_conversion(const std::string &input, T &output) noexcept { + if(input.empty()) { + return false; + } + char *val = nullptr; + errno = 0; + std::int64_t output_ll = std::strtoll(input.c_str(), &val, 0); + if(errno == ERANGE) { + return false; + } + output = static_cast(output_ll); + if(val == (input.c_str() + input.size()) && static_cast(output) == output_ll) { + return true; + } + if(input == "true") { + // this is to deal with a few oddities with flags and wrapper int types + output = static_cast(1); + return true; + } + // remove separators + if(input.find_first_of("_'") != std::string::npos) { + std::string nstring = input; + nstring.erase(std::remove(nstring.begin(), nstring.end(), '_'), nstring.end()); + nstring.erase(std::remove(nstring.begin(), nstring.end(), '\''), nstring.end()); + return integral_conversion(nstring, output); + } + if(input.compare(0, 2, "0o") == 0) { + val = nullptr; + errno = 0; + output_ll = std::strtoll(input.c_str() + 2, &val, 8); + if(errno == ERANGE) { + return false; + } + output = static_cast(output_ll); + return (val == (input.c_str() + input.size()) && static_cast(output) == output_ll); + } + if(input.compare(0, 2, "0b") == 0) { + val = nullptr; + errno = 0; + output_ll = std::strtoll(input.c_str() + 2, &val, 2); + if(errno == ERANGE) { + return false; + } + output = static_cast(output_ll); + return (val == (input.c_str() + input.size()) && static_cast(output) == output_ll); + } + return false; +} + +/// Convert a flag into an integer value typically binary flags sets errno to nonzero if conversion failed +inline std::int64_t to_flag_value(std::string val) noexcept { + static const std::string trueString("true"); + static const std::string falseString("false"); + if(val == trueString) { + return 1; + } + if(val == falseString) { + return -1; + } + val = detail::to_lower(val); + std::int64_t ret = 0; + if(val.size() == 1) { + if(val[0] >= '1' && val[0] <= '9') { + return (static_cast(val[0]) - '0'); + } + switch(val[0]) { + case '0': + case 'f': + case 'n': + case '-': + ret = -1; + break; + case 't': + case 'y': + case '+': + ret = 1; + break; + default: + errno = EINVAL; + return -1; + } + return ret; + } + if(val == trueString || val == "on" || val == "yes" || val == "enable") { + ret = 1; + } else if(val == falseString || val == "off" || val == "no" || val == "disable") { + ret = -1; + } else { + char *loc_ptr{nullptr}; + ret = std::strtoll(val.c_str(), &loc_ptr, 0); + if(loc_ptr != (val.c_str() + val.size()) && errno == 0) { + errno = EINVAL; + } + } + return ret; +} + +/// Integer conversion +template ::value == object_category::integral_value || + classify_object::value == object_category::unsigned_integral, + detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + return integral_conversion(input, output); +} + +/// char values +template ::value == object_category::char_value, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + if(input.size() == 1) { + output = static_cast(input[0]); + return true; + } + return integral_conversion(input, output); +} + +/// Boolean values +template ::value == object_category::boolean_value, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + errno = 0; + auto out = to_flag_value(input); + if(errno == 0) { + output = (out > 0); + } else if(errno == ERANGE) { + output = (input[0] != '-'); + } else { + return false; + } + return true; +} + +/// Floats +template ::value == object_category::floating_point, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + if(input.empty()) { + return false; + } + char *val = nullptr; + auto output_ld = std::strtold(input.c_str(), &val); + output = static_cast(output_ld); + if(val == (input.c_str() + input.size())) { + return true; + } + // remove separators + if(input.find_first_of("_'") != std::string::npos) { + std::string nstring = input; + nstring.erase(std::remove(nstring.begin(), nstring.end(), '_'), nstring.end()); + nstring.erase(std::remove(nstring.begin(), nstring.end(), '\''), nstring.end()); + return lexical_cast(nstring, output); + } + return false; +} + +/// complex +template ::value == object_category::complex_number, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + using XC = typename wrapped_type::type; + XC x{0.0}, y{0.0}; + auto str1 = input; + bool worked = false; + auto nloc = str1.find_last_of("+-"); + if(nloc != std::string::npos && nloc > 0) { + worked = lexical_cast(str1.substr(0, nloc), x); + str1 = str1.substr(nloc); + if(str1.back() == 'i' || str1.back() == 'j') + str1.pop_back(); + worked = worked && lexical_cast(str1, y); + } else { + if(str1.back() == 'i' || str1.back() == 'j') { + str1.pop_back(); + worked = lexical_cast(str1, y); + x = XC{0}; + } else { + worked = lexical_cast(str1, x); + y = XC{0}; + } + } + if(worked) { + output = T{x, y}; + return worked; + } + return from_stream(input, output); +} + +/// String and similar direct assignment +template ::value == object_category::string_assignable, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + output = input; + return true; +} + +/// String and similar constructible and copy assignment +template < + typename T, + enable_if_t::value == object_category::string_constructible, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + output = T(input); + return true; +} + +/// Wide strings +template < + typename T, + enable_if_t::value == object_category::wstring_assignable, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + output = widen(input); + return true; +} + +template < + typename T, + enable_if_t::value == object_category::wstring_constructible, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + output = T{widen(input)}; + return true; +} + +/// Enumerations +template ::value == object_category::enumeration, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + typename std::underlying_type::type val; + if(!integral_conversion(input, val)) { + return false; + } + output = static_cast(val); + return true; +} + +/// wrapper types +template ::value == object_category::wrapper_value && + std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + typename T::value_type val; + if(lexical_cast(input, val)) { + output = val; + return true; + } + return from_stream(input, output); +} + +template ::value == object_category::wrapper_value && + !std::is_assignable::value && std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + typename T::value_type val; + if(lexical_cast(input, val)) { + output = T{val}; + return true; + } + return from_stream(input, output); +} + +/// Assignable from double or int +template < + typename T, + enable_if_t::value == object_category::number_constructible, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + int val = 0; + if(integral_conversion(input, val)) { + output = T(val); + return true; + } + + double dval = 0.0; + if(lexical_cast(input, dval)) { + output = T{dval}; + return true; + } + + return from_stream(input, output); +} + +/// Assignable from int +template < + typename T, + enable_if_t::value == object_category::integer_constructible, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + int val = 0; + if(integral_conversion(input, val)) { + output = T(val); + return true; + } + return from_stream(input, output); +} + +/// Assignable from double +template < + typename T, + enable_if_t::value == object_category::double_constructible, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + double val = 0.0; + if(lexical_cast(input, val)) { + output = T{val}; + return true; + } + return from_stream(input, output); +} + +/// Non-string convertible from an int +template ::value == object_category::other && std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + int val = 0; + if(integral_conversion(input, val)) { +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4800) +#endif + // with Atomic this could produce a warning due to the conversion but if atomic gets here it is an old style + // so will most likely still work + output = val; +#ifdef _MSC_VER +#pragma warning(pop) +#endif + return true; + } + // LCOV_EXCL_START + // This version of cast is only used for odd cases in an older compilers the fail over + // from_stream is tested elsewhere an not relevant for coverage here + return from_stream(input, output); + // LCOV_EXCL_STOP +} + +/// Non-string parsable by a stream +template ::value == object_category::other && !std::is_assignable::value && + is_istreamable::value, + detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + return from_stream(input, output); +} + +/// Fallback overload that prints a human-readable error for types that we don't recognize and that don't have a +/// user-supplied lexical_cast overload. +template ::value == object_category::other && !std::is_assignable::value && + !is_istreamable::value && !adl_detail::is_lexical_castable::value, + detail::enabler> = detail::dummy> +bool lexical_cast(const std::string & /*input*/, T & /*output*/) { + static_assert(!std::is_same::value, // Can't just write false here. + "option object type must have a lexical cast overload or streaming input operator(>>) defined, if it " + "is convertible from another type use the add_option(...) with XC being the known type"); + return false; +} + +/// Assign a value through lexical cast operations +/// Strings can be empty so we need to do a little different +template ::value && + (classify_object::value == object_category::string_assignable || + classify_object::value == object_category::string_constructible || + classify_object::value == object_category::wstring_assignable || + classify_object::value == object_category::wstring_constructible), + detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, AssignTo &output) { + return lexical_cast(input, output); +} + +/// Assign a value through lexical cast operations +template ::value && std::is_assignable::value && + classify_object::value != object_category::string_assignable && + classify_object::value != object_category::string_constructible && + classify_object::value != object_category::wstring_assignable && + classify_object::value != object_category::wstring_constructible, + detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, AssignTo &output) { + if(input.empty()) { + output = AssignTo{}; + return true; + } + + return lexical_cast(input, output); +} + +/// Assign a value through lexical cast operations +template ::value && !std::is_assignable::value && + classify_object::value == object_category::wrapper_value, + detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, AssignTo &output) { + if(input.empty()) { + typename AssignTo::value_type emptyVal{}; + output = emptyVal; + return true; + } + return lexical_cast(input, output); +} + +/// Assign a value through lexical cast operations for int compatible values +/// mainly for atomic operations on some compilers +template ::value && !std::is_assignable::value && + classify_object::value != object_category::wrapper_value && + std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, AssignTo &output) { + if(input.empty()) { + output = 0; + return true; + } + int val{0}; + if(lexical_cast(input, val)) { +#if defined(__clang__) +/* on some older clang compilers */ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wsign-conversion" +#endif + output = val; +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + return true; + } + return false; +} + +/// Assign a value converted from a string in lexical cast to the output value directly +template ::value && std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, AssignTo &output) { + ConvertTo val{}; + bool parse_result = (!input.empty()) ? lexical_cast(input, val) : true; + if(parse_result) { + output = val; + } + return parse_result; +} + +/// Assign a value from a lexical cast through constructing a value and move assigning it +template < + typename AssignTo, + typename ConvertTo, + enable_if_t::value && !std::is_assignable::value && + std::is_move_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, AssignTo &output) { + ConvertTo val{}; + bool parse_result = input.empty() ? true : lexical_cast(input, val); + if(parse_result) { + output = AssignTo(val); // use () form of constructor to allow some implicit conversions + } + return parse_result; +} + +/// primary lexical conversion operation, 1 string to 1 type of some kind +template ::value <= object_category::other && + classify_object::value <= object_category::wrapper_value, + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + return lexical_assign(strings[0], output); +} + +/// Lexical conversion if there is only one element but the conversion type is for two, then call a two element +/// constructor +template ::value <= 2) && expected_count::value == 1 && + is_tuple_like::value && type_count_base::value == 2, + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + // the remove const is to handle pair types coming from a container + using FirstType = typename std::remove_const::type>::type; + using SecondType = typename std::tuple_element<1, ConvertTo>::type; + FirstType v1; + SecondType v2; + bool retval = lexical_assign(strings[0], v1); + retval = retval && lexical_assign((strings.size() > 1) ? strings[1] : std::string{}, v2); + if(retval) { + output = AssignTo{v1, v2}; + } + return retval; +} + +/// Lexical conversion of a container types of single elements +template ::value && is_mutable_container::value && + type_count::value == 1, + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + output.erase(output.begin(), output.end()); + if(strings.empty()) { + return true; + } + if(strings.size() == 1 && strings[0] == "{}") { + return true; + } + bool skip_remaining = false; + if(strings.size() == 2 && strings[0] == "{}" && is_separator(strings[1])) { + skip_remaining = true; + } + for(const auto &elem : strings) { + typename AssignTo::value_type out; + bool retval = lexical_assign(elem, out); + if(!retval) { + return false; + } + output.insert(output.end(), std::move(out)); + if(skip_remaining) { + break; + } + } + return (!output.empty()); +} + +/// Lexical conversion for complex types +template ::value, detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + + if(strings.size() >= 2 && !strings[1].empty()) { + using XC2 = typename wrapped_type::type; + XC2 x{0.0}, y{0.0}; + auto str1 = strings[1]; + if(str1.back() == 'i' || str1.back() == 'j') { + str1.pop_back(); + } + auto worked = lexical_cast(strings[0], x) && lexical_cast(str1, y); + if(worked) { + output = ConvertTo{x, y}; + } + return worked; + } + return lexical_assign(strings[0], output); +} + +/// Conversion to a vector type using a particular single type as the conversion type +template ::value && (expected_count::value == 1) && + (type_count::value == 1), + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + bool retval = true; + output.clear(); + output.reserve(strings.size()); + for(const auto &elem : strings) { + + output.emplace_back(); + retval = retval && lexical_assign(elem, output.back()); + } + return (!output.empty()) && retval; +} + +// forward declaration + +/// Lexical conversion of a container types with conversion type of two elements +template ::value && is_mutable_container::value && + type_count_base::value == 2, + detail::enabler> = detail::dummy> +bool lexical_conversion(std::vector strings, AssignTo &output); + +/// Lexical conversion of a vector types with type_size >2 forward declaration +template ::value && is_mutable_container::value && + type_count_base::value != 2 && + ((type_count::value > 2) || + (type_count::value > type_count_base::value)), + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output); + +/// Conversion for tuples +template ::value && is_tuple_like::value && + (type_count_base::value != type_count::value || + type_count::value > 2), + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output); // forward declaration + +/// Conversion for operations where the assigned type is some class but the conversion is a mutable container or large +/// tuple +template ::value && !is_mutable_container::value && + classify_object::value != object_category::wrapper_value && + (is_mutable_container::value || type_count::value > 2), + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + + if(strings.size() > 1 || (!strings.empty() && !(strings.front().empty()))) { + ConvertTo val; + auto retval = lexical_conversion(strings, val); + output = AssignTo{val}; + return retval; + } + output = AssignTo{}; + return true; +} + +/// function template for converting tuples if the static Index is greater than the tuple size +template +inline typename std::enable_if<(I >= type_count_base::value), bool>::type +tuple_conversion(const std::vector &, AssignTo &) { + return true; +} + +/// Conversion of a tuple element where the type size ==1 and not a mutable container +template +inline typename std::enable_if::value && type_count::value == 1, bool>::type +tuple_type_conversion(std::vector &strings, AssignTo &output) { + auto retval = lexical_assign(strings[0], output); + strings.erase(strings.begin()); + return retval; +} + +/// Conversion of a tuple element where the type size !=1 but the size is fixed and not a mutable container +template +inline typename std::enable_if::value && (type_count::value > 1) && + type_count::value == type_count_min::value, + bool>::type +tuple_type_conversion(std::vector &strings, AssignTo &output) { + auto retval = lexical_conversion(strings, output); + strings.erase(strings.begin(), strings.begin() + type_count::value); + return retval; +} + +/// Conversion of a tuple element where the type is a mutable container or a type with different min and max type sizes +template +inline typename std::enable_if::value || + type_count::value != type_count_min::value, + bool>::type +tuple_type_conversion(std::vector &strings, AssignTo &output) { + + std::size_t index{subtype_count_min::value}; + const std::size_t mx_count{subtype_count::value}; + const std::size_t mx{(std::min)(mx_count, strings.size() - 1)}; + + while(index < mx) { + if(is_separator(strings[index])) { + break; + } + ++index; + } + bool retval = lexical_conversion( + std::vector(strings.begin(), strings.begin() + static_cast(index)), output); + if(strings.size() > index) { + strings.erase(strings.begin(), strings.begin() + static_cast(index) + 1); + } else { + strings.clear(); + } + return retval; +} + +/// Tuple conversion operation +template +inline typename std::enable_if<(I < type_count_base::value), bool>::type +tuple_conversion(std::vector strings, AssignTo &output) { + bool retval = true; + using ConvertToElement = typename std:: + conditional::value, typename std::tuple_element::type, ConvertTo>::type; + if(!strings.empty()) { + retval = retval && tuple_type_conversion::type, ConvertToElement>( + strings, std::get(output)); + } + retval = retval && tuple_conversion(std::move(strings), output); + return retval; +} + +/// Lexical conversion of a container types with tuple elements of size 2 +template ::value && is_mutable_container::value && + type_count_base::value == 2, + detail::enabler>> +bool lexical_conversion(std::vector strings, AssignTo &output) { + output.clear(); + while(!strings.empty()) { + + typename std::remove_const::type>::type v1; + typename std::tuple_element<1, typename ConvertTo::value_type>::type v2; + bool retval = tuple_type_conversion(strings, v1); + if(!strings.empty()) { + retval = retval && tuple_type_conversion(strings, v2); + } + if(retval) { + output.insert(output.end(), typename AssignTo::value_type{v1, v2}); + } else { + return false; + } + } + return (!output.empty()); +} + +/// lexical conversion of tuples with type count>2 or tuples of types of some element with a type size>=2 +template ::value && is_tuple_like::value && + (type_count_base::value != type_count::value || + type_count::value > 2), + detail::enabler>> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + static_assert( + !is_tuple_like::value || type_count_base::value == type_count_base::value, + "if the conversion type is defined as a tuple it must be the same size as the type you are converting to"); + return tuple_conversion(strings, output); +} + +/// Lexical conversion of a vector types for everything but tuples of two elements and types of size 1 +template ::value && is_mutable_container::value && + type_count_base::value != 2 && + ((type_count::value > 2) || + (type_count::value > type_count_base::value)), + detail::enabler>> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + bool retval = true; + output.clear(); + std::vector temp; + std::size_t ii{0}; + std::size_t icount{0}; + std::size_t xcm{type_count::value}; + auto ii_max = strings.size(); + while(ii < ii_max) { + temp.push_back(strings[ii]); + ++ii; + ++icount; + if(icount == xcm || is_separator(temp.back()) || ii == ii_max) { + if(static_cast(xcm) > type_count_min::value && is_separator(temp.back())) { + temp.pop_back(); + } + typename AssignTo::value_type temp_out; + retval = retval && + lexical_conversion(temp, temp_out); + temp.clear(); + if(!retval) { + return false; + } + output.insert(output.end(), std::move(temp_out)); + icount = 0; + } + } + return retval; +} + +/// conversion for wrapper types +template ::value == object_category::wrapper_value && + std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + if(strings.empty() || strings.front().empty()) { + output = ConvertTo{}; + return true; + } + typename ConvertTo::value_type val; + if(lexical_conversion(strings, val)) { + output = ConvertTo{val}; + return true; + } + return false; +} + +/// conversion for wrapper types +template ::value == object_category::wrapper_value && + !std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + using ConvertType = typename ConvertTo::value_type; + if(strings.empty() || strings.front().empty()) { + output = ConvertType{}; + return true; + } + ConvertType val; + if(lexical_conversion(strings, val)) { + output = val; + return true; + } + return false; +} + +/// Sum a vector of strings +inline std::string sum_string_vector(const std::vector &values) { + double val{0.0}; + bool fail{false}; + std::string output; + for(const auto &arg : values) { + double tv{0.0}; + auto comp = lexical_cast(arg, tv); + if(!comp) { + errno = 0; + auto fv = detail::to_flag_value(arg); + fail = (errno != 0); + if(fail) { + break; + } + tv = static_cast(fv); + } + val += tv; + } + if(fail) { + for(const auto &arg : values) { + output.append(arg); + } + } else { + std::ostringstream out; + out.precision(16); + out << val; + output = out.str(); + } + return output; +} + +} // namespace detail + + + +namespace detail { + +// Returns false if not a short option. Otherwise, sets opt name and rest and returns true +CLI11_INLINE bool split_short(const std::string ¤t, std::string &name, std::string &rest); + +// Returns false if not a long option. Otherwise, sets opt name and other side of = and returns true +CLI11_INLINE bool split_long(const std::string ¤t, std::string &name, std::string &value); + +// Returns false if not a windows style option. Otherwise, sets opt name and value and returns true +CLI11_INLINE bool split_windows_style(const std::string ¤t, std::string &name, std::string &value); + +// Splits a string into multiple long and short names +CLI11_INLINE std::vector split_names(std::string current); + +/// extract default flag values either {def} or starting with a ! +CLI11_INLINE std::vector> get_default_flag_values(const std::string &str); + +/// Get a vector of short names, one of long names, and a single name +CLI11_INLINE std::tuple, std::vector, std::string> +get_names(const std::vector &input); + +} // namespace detail + + + +namespace detail { + +CLI11_INLINE bool split_short(const std::string ¤t, std::string &name, std::string &rest) { + if(current.size() > 1 && current[0] == '-' && valid_first_char(current[1])) { + name = current.substr(1, 1); + rest = current.substr(2); + return true; + } + return false; +} + +CLI11_INLINE bool split_long(const std::string ¤t, std::string &name, std::string &value) { + if(current.size() > 2 && current.compare(0, 2, "--") == 0 && valid_first_char(current[2])) { + auto loc = current.find_first_of('='); + if(loc != std::string::npos) { + name = current.substr(2, loc - 2); + value = current.substr(loc + 1); + } else { + name = current.substr(2); + value = ""; + } + return true; + } + return false; +} + +CLI11_INLINE bool split_windows_style(const std::string ¤t, std::string &name, std::string &value) { + if(current.size() > 1 && current[0] == '/' && valid_first_char(current[1])) { + auto loc = current.find_first_of(':'); + if(loc != std::string::npos) { + name = current.substr(1, loc - 1); + value = current.substr(loc + 1); + } else { + name = current.substr(1); + value = ""; + } + return true; + } + return false; +} + +CLI11_INLINE std::vector split_names(std::string current) { + std::vector output; + std::size_t val = 0; + while((val = current.find(',')) != std::string::npos) { + output.push_back(trim_copy(current.substr(0, val))); + current = current.substr(val + 1); + } + output.push_back(trim_copy(current)); + return output; +} + +CLI11_INLINE std::vector> get_default_flag_values(const std::string &str) { + std::vector flags = split_names(str); + flags.erase(std::remove_if(flags.begin(), + flags.end(), + [](const std::string &name) { + return ((name.empty()) || (!(((name.find_first_of('{') != std::string::npos) && + (name.back() == '}')) || + (name[0] == '!')))); + }), + flags.end()); + std::vector> output; + output.reserve(flags.size()); + for(auto &flag : flags) { + auto def_start = flag.find_first_of('{'); + std::string defval = "false"; + if((def_start != std::string::npos) && (flag.back() == '}')) { + defval = flag.substr(def_start + 1); + defval.pop_back(); + flag.erase(def_start, std::string::npos); // NOLINT(readability-suspicious-call-argument) + } + flag.erase(0, flag.find_first_not_of("-!")); + output.emplace_back(flag, defval); + } + return output; +} + +CLI11_INLINE std::tuple, std::vector, std::string> +get_names(const std::vector &input) { + + std::vector short_names; + std::vector long_names; + std::string pos_name; + for(std::string name : input) { + if(name.length() == 0) { + continue; + } + if(name.length() > 1 && name[0] == '-' && name[1] != '-') { + if(name.length() == 2 && valid_first_char(name[1])) + short_names.emplace_back(1, name[1]); + else if(name.length() > 2) + throw BadNameString::MissingDash(name); + else + throw BadNameString::OneCharName(name); + } else if(name.length() > 2 && name.substr(0, 2) == "--") { + name = name.substr(2); + if(valid_name_string(name)) + long_names.push_back(name); + else + throw BadNameString::BadLongName(name); + } else if(name == "-" || name == "--") { + throw BadNameString::DashesOnly(name); + } else { + if(!pos_name.empty()) + throw BadNameString::MultiPositionalNames(name); + if(valid_name_string(name)) { + pos_name = name; + } else { + throw BadNameString::BadPositionalName(name); + } + } + } + return std::make_tuple(short_names, long_names, pos_name); +} + +} // namespace detail + + + +class App; + +/// Holds values to load into Options +struct ConfigItem { + /// This is the list of parents + std::vector parents{}; + + /// This is the name + std::string name{}; + /// Listing of inputs + std::vector inputs{}; + + /// The list of parents and name joined by "." + CLI11_NODISCARD std::string fullname() const { + std::vector tmp = parents; + tmp.emplace_back(name); + return detail::join(tmp, "."); + } +}; + +/// This class provides a converter for configuration files. +class Config { + protected: + std::vector items{}; + + public: + /// Convert an app into a configuration + virtual std::string to_config(const App *, bool, bool, std::string) const = 0; + + /// Convert a configuration into an app + virtual std::vector from_config(std::istream &) const = 0; + + /// Get a flag value + CLI11_NODISCARD virtual std::string to_flag(const ConfigItem &item) const { + if(item.inputs.size() == 1) { + return item.inputs.at(0); + } + if(item.inputs.empty()) { + return "{}"; + } + throw ConversionError::TooManyInputsFlag(item.fullname()); // LCOV_EXCL_LINE + } + + /// Parse a config file, throw an error (ParseError:ConfigParseError or FileError) on failure + CLI11_NODISCARD std::vector from_file(const std::string &name) const { + std::ifstream input{name}; + if(!input.good()) + throw FileError::Missing(name); + + return from_config(input); + } + + /// Virtual destructor + virtual ~Config() = default; +}; + +/// This converter works with INI/TOML files; to write INI files use ConfigINI +class ConfigBase : public Config { + protected: + /// the character used for comments + char commentChar = '#'; + /// the character used to start an array '\0' is a default to not use + char arrayStart = '['; + /// the character used to end an array '\0' is a default to not use + char arrayEnd = ']'; + /// the character used to separate elements in an array + char arraySeparator = ','; + /// the character used separate the name from the value + char valueDelimiter = '='; + /// the character to use around strings + char stringQuote = '"'; + /// the character to use around single characters and literal strings + char literalQuote = '\''; + /// the maximum number of layers to allow + uint8_t maximumLayers{255}; + /// the separator used to separator parent layers + char parentSeparatorChar{'.'}; + /// Specify the configuration index to use for arrayed sections + int16_t configIndex{-1}; + /// Specify the configuration section that should be used + std::string configSection{}; + + public: + std::string + to_config(const App * /*app*/, bool default_also, bool write_description, std::string prefix) const override; + + std::vector from_config(std::istream &input) const override; + /// Specify the configuration for comment characters + ConfigBase *comment(char cchar) { + commentChar = cchar; + return this; + } + /// Specify the start and end characters for an array + ConfigBase *arrayBounds(char aStart, char aEnd) { + arrayStart = aStart; + arrayEnd = aEnd; + return this; + } + /// Specify the delimiter character for an array + ConfigBase *arrayDelimiter(char aSep) { + arraySeparator = aSep; + return this; + } + /// Specify the delimiter between a name and value + ConfigBase *valueSeparator(char vSep) { + valueDelimiter = vSep; + return this; + } + /// Specify the quote characters used around strings and literal strings + ConfigBase *quoteCharacter(char qString, char literalChar) { + stringQuote = qString; + literalQuote = literalChar; + return this; + } + /// Specify the maximum number of parents + ConfigBase *maxLayers(uint8_t layers) { + maximumLayers = layers; + return this; + } + /// Specify the separator to use for parent layers + ConfigBase *parentSeparator(char sep) { + parentSeparatorChar = sep; + return this; + } + /// get a reference to the configuration section + std::string §ionRef() { return configSection; } + /// get the section + CLI11_NODISCARD const std::string §ion() const { return configSection; } + /// specify a particular section of the configuration file to use + ConfigBase *section(const std::string §ionName) { + configSection = sectionName; + return this; + } + + /// get a reference to the configuration index + int16_t &indexRef() { return configIndex; } + /// get the section index + CLI11_NODISCARD int16_t index() const { return configIndex; } + /// specify a particular index in the section to use (-1) for all sections to use + ConfigBase *index(int16_t sectionIndex) { + configIndex = sectionIndex; + return this; + } +}; + +/// the default Config is the TOML file format +using ConfigTOML = ConfigBase; + +/// ConfigINI generates a "standard" INI compliant output +class ConfigINI : public ConfigTOML { + + public: + ConfigINI() { + commentChar = ';'; + arrayStart = '\0'; + arrayEnd = '\0'; + arraySeparator = ' '; + valueDelimiter = '='; + } +}; + + + +class Option; + +/// @defgroup validator_group Validators + +/// @brief Some validators that are provided +/// +/// These are simple `std::string(const std::string&)` validators that are useful. They return +/// a string if the validation fails. A custom struct is provided, as well, with the same user +/// semantics, but with the ability to provide a new type name. +/// @{ + +/// +class Validator { + protected: + /// This is the description function, if empty the description_ will be used + std::function desc_function_{[]() { return std::string{}; }}; + + /// This is the base function that is to be called. + /// Returns a string error message if validation fails. + std::function func_{[](std::string &) { return std::string{}; }}; + /// The name for search purposes of the Validator + std::string name_{}; + /// A Validator will only apply to an indexed value (-1 is all elements) + int application_index_ = -1; + /// Enable for Validator to allow it to be disabled if need be + bool active_{true}; + /// specify that a validator should not modify the input + bool non_modifying_{false}; + + Validator(std::string validator_desc, std::function func) + : desc_function_([validator_desc]() { return validator_desc; }), func_(std::move(func)) {} + + public: + Validator() = default; + /// Construct a Validator with just the description string + explicit Validator(std::string validator_desc) : desc_function_([validator_desc]() { return validator_desc; }) {} + /// Construct Validator from basic information + Validator(std::function op, std::string validator_desc, std::string validator_name = "") + : desc_function_([validator_desc]() { return validator_desc; }), func_(std::move(op)), + name_(std::move(validator_name)) {} + /// Set the Validator operation function + Validator &operation(std::function op) { + func_ = std::move(op); + return *this; + } + /// This is the required operator for a Validator - provided to help + /// users (CLI11 uses the member `func` directly) + std::string operator()(std::string &str) const; + + /// This is the required operator for a Validator - provided to help + /// users (CLI11 uses the member `func` directly) + std::string operator()(const std::string &str) const { + std::string value = str; + return (active_) ? func_(value) : std::string{}; + } + + /// Specify the type string + Validator &description(std::string validator_desc) { + desc_function_ = [validator_desc]() { return validator_desc; }; + return *this; + } + /// Specify the type string + CLI11_NODISCARD Validator description(std::string validator_desc) const; + + /// Generate type description information for the Validator + CLI11_NODISCARD std::string get_description() const { + if(active_) { + return desc_function_(); + } + return std::string{}; + } + /// Specify the type string + Validator &name(std::string validator_name) { + name_ = std::move(validator_name); + return *this; + } + /// Specify the type string + CLI11_NODISCARD Validator name(std::string validator_name) const { + Validator newval(*this); + newval.name_ = std::move(validator_name); + return newval; + } + /// Get the name of the Validator + CLI11_NODISCARD const std::string &get_name() const { return name_; } + /// Specify whether the Validator is active or not + Validator &active(bool active_val = true) { + active_ = active_val; + return *this; + } + /// Specify whether the Validator is active or not + CLI11_NODISCARD Validator active(bool active_val = true) const { + Validator newval(*this); + newval.active_ = active_val; + return newval; + } + + /// Specify whether the Validator can be modifying or not + Validator &non_modifying(bool no_modify = true) { + non_modifying_ = no_modify; + return *this; + } + /// Specify the application index of a validator + Validator &application_index(int app_index) { + application_index_ = app_index; + return *this; + } + /// Specify the application index of a validator + CLI11_NODISCARD Validator application_index(int app_index) const { + Validator newval(*this); + newval.application_index_ = app_index; + return newval; + } + /// Get the current value of the application index + CLI11_NODISCARD int get_application_index() const { return application_index_; } + /// Get a boolean if the validator is active + CLI11_NODISCARD bool get_active() const { return active_; } + + /// Get a boolean if the validator is allowed to modify the input returns true if it can modify the input + CLI11_NODISCARD bool get_modifying() const { return !non_modifying_; } + + /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the + /// same. + Validator operator&(const Validator &other) const; + + /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the + /// same. + Validator operator|(const Validator &other) const; + + /// Create a validator that fails when a given validator succeeds + Validator operator!() const; + + private: + void _merge_description(const Validator &val1, const Validator &val2, const std::string &merger); +}; + +/// Class wrapping some of the accessors of Validator +class CustomValidator : public Validator { + public: +}; +// The implementation of the built in validators is using the Validator class; +// the user is only expected to use the const (static) versions (since there's no setup). +// Therefore, this is in detail. +namespace detail { + +/// CLI enumeration of different file types +enum class path_type { nonexistent, file, directory }; + +/// get the type of the path from a file name +CLI11_INLINE path_type check_path(const char *file) noexcept; + +/// Check for an existing file (returns error message if check fails) +class ExistingFileValidator : public Validator { + public: + ExistingFileValidator(); +}; + +/// Check for an existing directory (returns error message if check fails) +class ExistingDirectoryValidator : public Validator { + public: + ExistingDirectoryValidator(); +}; + +/// Check for an existing path +class ExistingPathValidator : public Validator { + public: + ExistingPathValidator(); +}; + +/// Check for an non-existing path +class NonexistentPathValidator : public Validator { + public: + NonexistentPathValidator(); +}; + +/// Validate the given string is a legal ipv4 address +class IPV4Validator : public Validator { + public: + IPV4Validator(); +}; + +class EscapedStringTransformer : public Validator { + public: + EscapedStringTransformer(); +}; + +} // namespace detail + +// Static is not needed here, because global const implies static. + +/// Check for existing file (returns error message if check fails) +const detail::ExistingFileValidator ExistingFile; + +/// Check for an existing directory (returns error message if check fails) +const detail::ExistingDirectoryValidator ExistingDirectory; + +/// Check for an existing path +const detail::ExistingPathValidator ExistingPath; + +/// Check for an non-existing path +const detail::NonexistentPathValidator NonexistentPath; + +/// Check for an IP4 address +const detail::IPV4Validator ValidIPV4; + +/// convert escaped characters into their associated values +const detail::EscapedStringTransformer EscapedString; + +/// Validate the input as a particular type +template class TypeValidator : public Validator { + public: + explicit TypeValidator(const std::string &validator_name) + : Validator(validator_name, [](std::string &input_string) { + using CLI::detail::lexical_cast; + auto val = DesiredType(); + if(!lexical_cast(input_string, val)) { + return std::string("Failed parsing ") + input_string + " as a " + detail::type_name(); + } + return std::string(); + }) {} + TypeValidator() : TypeValidator(detail::type_name()) {} +}; + +/// Check for a number +const TypeValidator Number("NUMBER"); + +/// Modify a path if the file is a particular default location, can be used as Check or transform +/// with the error return optionally disabled +class FileOnDefaultPath : public Validator { + public: + explicit FileOnDefaultPath(std::string default_path, bool enableErrorReturn = true); +}; + +/// Produce a range (factory). Min and max are inclusive. +class Range : public Validator { + public: + /// This produces a range with min and max inclusive. + /// + /// Note that the constructor is templated, but the struct is not, so C++17 is not + /// needed to provide nice syntax for Range(a,b). + template + Range(T min_val, T max_val, const std::string &validator_name = std::string{}) : Validator(validator_name) { + if(validator_name.empty()) { + std::stringstream out; + out << detail::type_name() << " in [" << min_val << " - " << max_val << "]"; + description(out.str()); + } + + func_ = [min_val, max_val](std::string &input) { + using CLI::detail::lexical_cast; + T val; + bool converted = lexical_cast(input, val); + if((!converted) || (val < min_val || val > max_val)) { + std::stringstream out; + out << "Value " << input << " not in range ["; + out << min_val << " - " << max_val << "]"; + return out.str(); + } + return std::string{}; + }; + } + + /// Range of one value is 0 to value + template + explicit Range(T max_val, const std::string &validator_name = std::string{}) + : Range(static_cast(0), max_val, validator_name) {} +}; + +/// Check for a non negative number +const Range NonNegativeNumber((std::numeric_limits::max)(), "NONNEGATIVE"); + +/// Check for a positive valued number (val>0.0), ::min here is the smallest positive number +const Range PositiveNumber((std::numeric_limits::min)(), (std::numeric_limits::max)(), "POSITIVE"); + +/// Produce a bounded range (factory). Min and max are inclusive. +class Bound : public Validator { + public: + /// This bounds a value with min and max inclusive. + /// + /// Note that the constructor is templated, but the struct is not, so C++17 is not + /// needed to provide nice syntax for Range(a,b). + template Bound(T min_val, T max_val) { + std::stringstream out; + out << detail::type_name() << " bounded to [" << min_val << " - " << max_val << "]"; + description(out.str()); + + func_ = [min_val, max_val](std::string &input) { + using CLI::detail::lexical_cast; + T val; + bool converted = lexical_cast(input, val); + if(!converted) { + return std::string("Value ") + input + " could not be converted"; + } + if(val < min_val) + input = detail::to_string(min_val); + else if(val > max_val) + input = detail::to_string(max_val); + + return std::string{}; + }; + } + + /// Range of one value is 0 to value + template explicit Bound(T max_val) : Bound(static_cast(0), max_val) {} +}; + +namespace detail { +template ::type>::value, detail::enabler> = detail::dummy> +auto smart_deref(T value) -> decltype(*value) { + return *value; +} + +template < + typename T, + enable_if_t::type>::value, detail::enabler> = detail::dummy> +typename std::remove_reference::type &smart_deref(T &value) { + return value; +} +/// Generate a string representation of a set +template std::string generate_set(const T &set) { + using element_t = typename detail::element_type::type; + using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair + std::string out(1, '{'); + out.append(detail::join( + detail::smart_deref(set), + [](const iteration_type_t &v) { return detail::pair_adaptor::first(v); }, + ",")); + out.push_back('}'); + return out; +} + +/// Generate a string representation of a map +template std::string generate_map(const T &map, bool key_only = false) { + using element_t = typename detail::element_type::type; + using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair + std::string out(1, '{'); + out.append(detail::join( + detail::smart_deref(map), + [key_only](const iteration_type_t &v) { + std::string res{detail::to_string(detail::pair_adaptor::first(v))}; + + if(!key_only) { + res.append("->"); + res += detail::to_string(detail::pair_adaptor::second(v)); + } + return res; + }, + ",")); + out.push_back('}'); + return out; +} + +template struct has_find { + template + static auto test(int) -> decltype(std::declval().find(std::declval()), std::true_type()); + template static auto test(...) -> decltype(std::false_type()); + + static const auto value = decltype(test(0))::value; + using type = std::integral_constant; +}; + +/// A search function +template ::value, detail::enabler> = detail::dummy> +auto search(const T &set, const V &val) -> std::pair { + using element_t = typename detail::element_type::type; + auto &setref = detail::smart_deref(set); + auto it = std::find_if(std::begin(setref), std::end(setref), [&val](decltype(*std::begin(setref)) v) { + return (detail::pair_adaptor::first(v) == val); + }); + return {(it != std::end(setref)), it}; +} + +/// A search function that uses the built in find function +template ::value, detail::enabler> = detail::dummy> +auto search(const T &set, const V &val) -> std::pair { + auto &setref = detail::smart_deref(set); + auto it = setref.find(val); + return {(it != std::end(setref)), it}; +} + +/// A search function with a filter function +template +auto search(const T &set, const V &val, const std::function &filter_function) + -> std::pair { + using element_t = typename detail::element_type::type; + // do the potentially faster first search + auto res = search(set, val); + if((res.first) || (!(filter_function))) { + return res; + } + // if we haven't found it do the longer linear search with all the element translations + auto &setref = detail::smart_deref(set); + auto it = std::find_if(std::begin(setref), std::end(setref), [&](decltype(*std::begin(setref)) v) { + V a{detail::pair_adaptor::first(v)}; + a = filter_function(a); + return (a == val); + }); + return {(it != std::end(setref)), it}; +} + +// the following suggestion was made by Nikita Ofitserov(@himikof) +// done in templates to prevent compiler warnings on negation of unsigned numbers + +/// Do a check for overflow on signed numbers +template +inline typename std::enable_if::value, T>::type overflowCheck(const T &a, const T &b) { + if((a > 0) == (b > 0)) { + return ((std::numeric_limits::max)() / (std::abs)(a) < (std::abs)(b)); + } + return ((std::numeric_limits::min)() / (std::abs)(a) > -(std::abs)(b)); +} +/// Do a check for overflow on unsigned numbers +template +inline typename std::enable_if::value, T>::type overflowCheck(const T &a, const T &b) { + return ((std::numeric_limits::max)() / a < b); +} + +/// Performs a *= b; if it doesn't cause integer overflow. Returns false otherwise. +template typename std::enable_if::value, bool>::type checked_multiply(T &a, T b) { + if(a == 0 || b == 0 || a == 1 || b == 1) { + a *= b; + return true; + } + if(a == (std::numeric_limits::min)() || b == (std::numeric_limits::min)()) { + return false; + } + if(overflowCheck(a, b)) { + return false; + } + a *= b; + return true; +} + +/// Performs a *= b; if it doesn't equal infinity. Returns false otherwise. +template +typename std::enable_if::value, bool>::type checked_multiply(T &a, T b) { + T c = a * b; + if(std::isinf(c) && !std::isinf(a) && !std::isinf(b)) { + return false; + } + a = c; + return true; +} + +} // namespace detail +/// Verify items are in a set +class IsMember : public Validator { + public: + using filter_fn_t = std::function; + + /// This allows in-place construction using an initializer list + template + IsMember(std::initializer_list values, Args &&...args) + : IsMember(std::vector(values), std::forward(args)...) {} + + /// This checks to see if an item is in a set (empty function) + template explicit IsMember(T &&set) : IsMember(std::forward(set), nullptr) {} + + /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter + /// both sides of the comparison before computing the comparison. + template explicit IsMember(T set, F filter_function) { + + // Get the type of the contained item - requires a container have ::value_type + // if the type does not have first_type and second_type, these are both value_type + using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed + using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map + + using local_item_t = typename IsMemberType::type; // This will convert bad types to good ones + // (const char * to std::string) + + // Make a local copy of the filter function, using a std::function if not one already + std::function filter_fn = filter_function; + + // This is the type name for help, it will take the current version of the set contents + desc_function_ = [set]() { return detail::generate_set(detail::smart_deref(set)); }; + + // This is the function that validates + // It stores a copy of the set pointer-like, so shared_ptr will stay alive + func_ = [set, filter_fn](std::string &input) { + using CLI::detail::lexical_cast; + local_item_t b; + if(!lexical_cast(input, b)) { + throw ValidationError(input); // name is added later + } + if(filter_fn) { + b = filter_fn(b); + } + auto res = detail::search(set, b, filter_fn); + if(res.first) { + // Make sure the version in the input string is identical to the one in the set + if(filter_fn) { + input = detail::value_string(detail::pair_adaptor::first(*(res.second))); + } + + // Return empty error string (success) + return std::string{}; + } + + // If you reach this point, the result was not found + return input + " not in " + detail::generate_set(detail::smart_deref(set)); + }; + } + + /// You can pass in as many filter functions as you like, they nest (string only currently) + template + IsMember(T &&set, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other) + : IsMember( + std::forward(set), + [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, + other...) {} +}; + +/// definition of the default transformation object +template using TransformPairs = std::vector>; + +/// Translate named items to other or a value set +class Transformer : public Validator { + public: + using filter_fn_t = std::function; + + /// This allows in-place construction + template + Transformer(std::initializer_list> values, Args &&...args) + : Transformer(TransformPairs(values), std::forward(args)...) {} + + /// direct map of std::string to std::string + template explicit Transformer(T &&mapping) : Transformer(std::forward(mapping), nullptr) {} + + /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter + /// both sides of the comparison before computing the comparison. + template explicit Transformer(T mapping, F filter_function) { + + static_assert(detail::pair_adaptor::type>::value, + "mapping must produce value pairs"); + // Get the type of the contained item - requires a container have ::value_type + // if the type does not have first_type and second_type, these are both value_type + using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed + using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map + using local_item_t = typename IsMemberType::type; // Will convert bad types to good ones + // (const char * to std::string) + + // Make a local copy of the filter function, using a std::function if not one already + std::function filter_fn = filter_function; + + // This is the type name for help, it will take the current version of the set contents + desc_function_ = [mapping]() { return detail::generate_map(detail::smart_deref(mapping)); }; + + func_ = [mapping, filter_fn](std::string &input) { + using CLI::detail::lexical_cast; + local_item_t b; + if(!lexical_cast(input, b)) { + return std::string(); + // there is no possible way we can match anything in the mapping if we can't convert so just return + } + if(filter_fn) { + b = filter_fn(b); + } + auto res = detail::search(mapping, b, filter_fn); + if(res.first) { + input = detail::value_string(detail::pair_adaptor::second(*res.second)); + } + return std::string{}; + }; + } + + /// You can pass in as many filter functions as you like, they nest + template + Transformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other) + : Transformer( + std::forward(mapping), + [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, + other...) {} +}; + +/// translate named items to other or a value set +class CheckedTransformer : public Validator { + public: + using filter_fn_t = std::function; + + /// This allows in-place construction + template + CheckedTransformer(std::initializer_list> values, Args &&...args) + : CheckedTransformer(TransformPairs(values), std::forward(args)...) {} + + /// direct map of std::string to std::string + template explicit CheckedTransformer(T mapping) : CheckedTransformer(std::move(mapping), nullptr) {} + + /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter + /// both sides of the comparison before computing the comparison. + template explicit CheckedTransformer(T mapping, F filter_function) { + + static_assert(detail::pair_adaptor::type>::value, + "mapping must produce value pairs"); + // Get the type of the contained item - requires a container have ::value_type + // if the type does not have first_type and second_type, these are both value_type + using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed + using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map + using local_item_t = typename IsMemberType::type; // Will convert bad types to good ones + // (const char * to std::string) + using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair + + // Make a local copy of the filter function, using a std::function if not one already + std::function filter_fn = filter_function; + + auto tfunc = [mapping]() { + std::string out("value in "); + out += detail::generate_map(detail::smart_deref(mapping)) + " OR {"; + out += detail::join( + detail::smart_deref(mapping), + [](const iteration_type_t &v) { return detail::to_string(detail::pair_adaptor::second(v)); }, + ","); + out.push_back('}'); + return out; + }; + + desc_function_ = tfunc; + + func_ = [mapping, tfunc, filter_fn](std::string &input) { + using CLI::detail::lexical_cast; + local_item_t b; + bool converted = lexical_cast(input, b); + if(converted) { + if(filter_fn) { + b = filter_fn(b); + } + auto res = detail::search(mapping, b, filter_fn); + if(res.first) { + input = detail::value_string(detail::pair_adaptor::second(*res.second)); + return std::string{}; + } + } + for(const auto &v : detail::smart_deref(mapping)) { + auto output_string = detail::value_string(detail::pair_adaptor::second(v)); + if(output_string == input) { + return std::string(); + } + } + + return "Check " + input + " " + tfunc() + " FAILED"; + }; + } + + /// You can pass in as many filter functions as you like, they nest + template + CheckedTransformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other) + : CheckedTransformer( + std::forward(mapping), + [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, + other...) {} +}; + +/// Helper function to allow ignore_case to be passed to IsMember or Transform +inline std::string ignore_case(std::string item) { return detail::to_lower(item); } + +/// Helper function to allow ignore_underscore to be passed to IsMember or Transform +inline std::string ignore_underscore(std::string item) { return detail::remove_underscore(item); } + +/// Helper function to allow checks to ignore spaces to be passed to IsMember or Transform +inline std::string ignore_space(std::string item) { + item.erase(std::remove(std::begin(item), std::end(item), ' '), std::end(item)); + item.erase(std::remove(std::begin(item), std::end(item), '\t'), std::end(item)); + return item; +} + +/// Multiply a number by a factor using given mapping. +/// Can be used to write transforms for SIZE or DURATION inputs. +/// +/// Example: +/// With mapping = `{"b"->1, "kb"->1024, "mb"->1024*1024}` +/// one can recognize inputs like "100", "12kb", "100 MB", +/// that will be automatically transformed to 100, 14448, 104857600. +/// +/// Output number type matches the type in the provided mapping. +/// Therefore, if it is required to interpret real inputs like "0.42 s", +/// the mapping should be of a type or . +class AsNumberWithUnit : public Validator { + public: + /// Adjust AsNumberWithUnit behavior. + /// CASE_SENSITIVE/CASE_INSENSITIVE controls how units are matched. + /// UNIT_OPTIONAL/UNIT_REQUIRED throws ValidationError + /// if UNIT_REQUIRED is set and unit literal is not found. + enum Options { + CASE_SENSITIVE = 0, + CASE_INSENSITIVE = 1, + UNIT_OPTIONAL = 0, + UNIT_REQUIRED = 2, + DEFAULT = CASE_INSENSITIVE | UNIT_OPTIONAL + }; + + template + explicit AsNumberWithUnit(std::map mapping, + Options opts = DEFAULT, + const std::string &unit_name = "UNIT") { + description(generate_description(unit_name, opts)); + validate_mapping(mapping, opts); + + // transform function + func_ = [mapping, opts](std::string &input) -> std::string { + Number num{}; + + detail::rtrim(input); + if(input.empty()) { + throw ValidationError("Input is empty"); + } + + // Find split position between number and prefix + auto unit_begin = input.end(); + while(unit_begin > input.begin() && std::isalpha(*(unit_begin - 1), std::locale())) { + --unit_begin; + } + + std::string unit{unit_begin, input.end()}; + input.resize(static_cast(std::distance(input.begin(), unit_begin))); + detail::trim(input); + + if(opts & UNIT_REQUIRED && unit.empty()) { + throw ValidationError("Missing mandatory unit"); + } + if(opts & CASE_INSENSITIVE) { + unit = detail::to_lower(unit); + } + if(unit.empty()) { + using CLI::detail::lexical_cast; + if(!lexical_cast(input, num)) { + throw ValidationError(std::string("Value ") + input + " could not be converted to " + + detail::type_name()); + } + // No need to modify input if no unit passed + return {}; + } + + // find corresponding factor + auto it = mapping.find(unit); + if(it == mapping.end()) { + throw ValidationError(unit + + " unit not recognized. " + "Allowed values: " + + detail::generate_map(mapping, true)); + } + + if(!input.empty()) { + using CLI::detail::lexical_cast; + bool converted = lexical_cast(input, num); + if(!converted) { + throw ValidationError(std::string("Value ") + input + " could not be converted to " + + detail::type_name()); + } + // perform safe multiplication + bool ok = detail::checked_multiply(num, it->second); + if(!ok) { + throw ValidationError(detail::to_string(num) + " multiplied by " + unit + + " factor would cause number overflow. Use smaller value."); + } + } else { + num = static_cast(it->second); + } + + input = detail::to_string(num); + + return {}; + }; + } + + private: + /// Check that mapping contains valid units. + /// Update mapping for CASE_INSENSITIVE mode. + template static void validate_mapping(std::map &mapping, Options opts) { + for(auto &kv : mapping) { + if(kv.first.empty()) { + throw ValidationError("Unit must not be empty."); + } + if(!detail::isalpha(kv.first)) { + throw ValidationError("Unit must contain only letters."); + } + } + + // make all units lowercase if CASE_INSENSITIVE + if(opts & CASE_INSENSITIVE) { + std::map lower_mapping; + for(auto &kv : mapping) { + auto s = detail::to_lower(kv.first); + if(lower_mapping.count(s)) { + throw ValidationError(std::string("Several matching lowercase unit representations are found: ") + + s); + } + lower_mapping[detail::to_lower(kv.first)] = kv.second; + } + mapping = std::move(lower_mapping); + } + } + + /// Generate description like this: NUMBER [UNIT] + template static std::string generate_description(const std::string &name, Options opts) { + std::stringstream out; + out << detail::type_name() << ' '; + if(opts & UNIT_REQUIRED) { + out << name; + } else { + out << '[' << name << ']'; + } + return out.str(); + } +}; + +inline AsNumberWithUnit::Options operator|(const AsNumberWithUnit::Options &a, const AsNumberWithUnit::Options &b) { + return static_cast(static_cast(a) | static_cast(b)); +} + +/// Converts a human-readable size string (with unit literal) to uin64_t size. +/// Example: +/// "100" => 100 +/// "1 b" => 100 +/// "10Kb" => 10240 // you can configure this to be interpreted as kilobyte (*1000) or kibibyte (*1024) +/// "10 KB" => 10240 +/// "10 kb" => 10240 +/// "10 kib" => 10240 // *i, *ib are always interpreted as *bibyte (*1024) +/// "10kb" => 10240 +/// "2 MB" => 2097152 +/// "2 EiB" => 2^61 // Units up to exibyte are supported +class AsSizeValue : public AsNumberWithUnit { + public: + using result_t = std::uint64_t; + + /// If kb_is_1000 is true, + /// interpret 'kb', 'k' as 1000 and 'kib', 'ki' as 1024 + /// (same applies to higher order units as well). + /// Otherwise, interpret all literals as factors of 1024. + /// The first option is formally correct, but + /// the second interpretation is more wide-spread + /// (see https://en.wikipedia.org/wiki/Binary_prefix). + explicit AsSizeValue(bool kb_is_1000); + + private: + /// Get mapping + static std::map init_mapping(bool kb_is_1000); + + /// Cache calculated mapping + static std::map get_mapping(bool kb_is_1000); +}; + +namespace detail { +/// Split a string into a program name and command line arguments +/// the string is assumed to contain a file name followed by other arguments +/// the return value contains is a pair with the first argument containing the program name and the second +/// everything else. +CLI11_INLINE std::pair split_program_name(std::string commandline); + +} // namespace detail +/// @} + + + + +CLI11_INLINE std::string Validator::operator()(std::string &str) const { + std::string retstring; + if(active_) { + if(non_modifying_) { + std::string value = str; + retstring = func_(value); + } else { + retstring = func_(str); + } + } + return retstring; +} + +CLI11_NODISCARD CLI11_INLINE Validator Validator::description(std::string validator_desc) const { + Validator newval(*this); + newval.desc_function_ = [validator_desc]() { return validator_desc; }; + return newval; +} + +CLI11_INLINE Validator Validator::operator&(const Validator &other) const { + Validator newval; + + newval._merge_description(*this, other, " AND "); + + // Give references (will make a copy in lambda function) + const std::function &f1 = func_; + const std::function &f2 = other.func_; + + newval.func_ = [f1, f2](std::string &input) { + std::string s1 = f1(input); + std::string s2 = f2(input); + if(!s1.empty() && !s2.empty()) + return std::string("(") + s1 + ") AND (" + s2 + ")"; + return s1 + s2; + }; + + newval.active_ = active_ && other.active_; + newval.application_index_ = application_index_; + return newval; +} + +CLI11_INLINE Validator Validator::operator|(const Validator &other) const { + Validator newval; + + newval._merge_description(*this, other, " OR "); + + // Give references (will make a copy in lambda function) + const std::function &f1 = func_; + const std::function &f2 = other.func_; + + newval.func_ = [f1, f2](std::string &input) { + std::string s1 = f1(input); + std::string s2 = f2(input); + if(s1.empty() || s2.empty()) + return std::string(); + + return std::string("(") + s1 + ") OR (" + s2 + ")"; + }; + newval.active_ = active_ && other.active_; + newval.application_index_ = application_index_; + return newval; +} + +CLI11_INLINE Validator Validator::operator!() const { + Validator newval; + const std::function &dfunc1 = desc_function_; + newval.desc_function_ = [dfunc1]() { + auto str = dfunc1(); + return (!str.empty()) ? std::string("NOT ") + str : std::string{}; + }; + // Give references (will make a copy in lambda function) + const std::function &f1 = func_; + + newval.func_ = [f1, dfunc1](std::string &test) -> std::string { + std::string s1 = f1(test); + if(s1.empty()) { + return std::string("check ") + dfunc1() + " succeeded improperly"; + } + return std::string{}; + }; + newval.active_ = active_; + newval.application_index_ = application_index_; + return newval; +} + +CLI11_INLINE void +Validator::_merge_description(const Validator &val1, const Validator &val2, const std::string &merger) { + + const std::function &dfunc1 = val1.desc_function_; + const std::function &dfunc2 = val2.desc_function_; + + desc_function_ = [=]() { + std::string f1 = dfunc1(); + std::string f2 = dfunc2(); + if((f1.empty()) || (f2.empty())) { + return f1 + f2; + } + return std::string(1, '(') + f1 + ')' + merger + '(' + f2 + ')'; + }; +} + +namespace detail { + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +CLI11_INLINE path_type check_path(const char *file) noexcept { + std::error_code ec; + auto stat = std::filesystem::status(to_path(file), ec); + if(ec) { + return path_type::nonexistent; + } + switch(stat.type()) { + case std::filesystem::file_type::none: // LCOV_EXCL_LINE + case std::filesystem::file_type::not_found: + return path_type::nonexistent; // LCOV_EXCL_LINE + case std::filesystem::file_type::directory: + return path_type::directory; + case std::filesystem::file_type::symlink: + case std::filesystem::file_type::block: + case std::filesystem::file_type::character: + case std::filesystem::file_type::fifo: + case std::filesystem::file_type::socket: + case std::filesystem::file_type::regular: + case std::filesystem::file_type::unknown: + default: + return path_type::file; + } +} +#else +CLI11_INLINE path_type check_path(const char *file) noexcept { +#if defined(_MSC_VER) + struct __stat64 buffer; + if(_stat64(file, &buffer) == 0) { + return ((buffer.st_mode & S_IFDIR) != 0) ? path_type::directory : path_type::file; + } +#else + struct stat buffer; + if(stat(file, &buffer) == 0) { + return ((buffer.st_mode & S_IFDIR) != 0) ? path_type::directory : path_type::file; + } +#endif + return path_type::nonexistent; +} +#endif + +CLI11_INLINE ExistingFileValidator::ExistingFileValidator() : Validator("FILE") { + func_ = [](std::string &filename) { + auto path_result = check_path(filename.c_str()); + if(path_result == path_type::nonexistent) { + return "File does not exist: " + filename; + } + if(path_result == path_type::directory) { + return "File is actually a directory: " + filename; + } + return std::string(); + }; +} + +CLI11_INLINE ExistingDirectoryValidator::ExistingDirectoryValidator() : Validator("DIR") { + func_ = [](std::string &filename) { + auto path_result = check_path(filename.c_str()); + if(path_result == path_type::nonexistent) { + return "Directory does not exist: " + filename; + } + if(path_result == path_type::file) { + return "Directory is actually a file: " + filename; + } + return std::string(); + }; +} + +CLI11_INLINE ExistingPathValidator::ExistingPathValidator() : Validator("PATH(existing)") { + func_ = [](std::string &filename) { + auto path_result = check_path(filename.c_str()); + if(path_result == path_type::nonexistent) { + return "Path does not exist: " + filename; + } + return std::string(); + }; +} + +CLI11_INLINE NonexistentPathValidator::NonexistentPathValidator() : Validator("PATH(non-existing)") { + func_ = [](std::string &filename) { + auto path_result = check_path(filename.c_str()); + if(path_result != path_type::nonexistent) { + return "Path already exists: " + filename; + } + return std::string(); + }; +} + +CLI11_INLINE IPV4Validator::IPV4Validator() : Validator("IPV4") { + func_ = [](std::string &ip_addr) { + auto result = CLI::detail::split(ip_addr, '.'); + if(result.size() != 4) { + return std::string("Invalid IPV4 address must have four parts (") + ip_addr + ')'; + } + int num = 0; + for(const auto &var : result) { + using CLI::detail::lexical_cast; + bool retval = lexical_cast(var, num); + if(!retval) { + return std::string("Failed parsing number (") + var + ')'; + } + if(num < 0 || num > 255) { + return std::string("Each IP number must be between 0 and 255 ") + var; + } + } + return std::string{}; + }; +} + +CLI11_INLINE EscapedStringTransformer::EscapedStringTransformer() { + func_ = [](std::string &str) { + try { + if(str.size() > 1 && (str.front() == '\"' || str.front() == '\'' || str.front() == '`') && + str.front() == str.back()) { + process_quoted_string(str); + } else if(str.find_first_of('\\') != std::string::npos) { + if(detail::is_binary_escaped_string(str)) { + str = detail::extract_binary_string(str); + } else { + str = remove_escaped_characters(str); + } + } + return std::string{}; + } catch(const std::invalid_argument &ia) { + return std::string(ia.what()); + } + }; +} +} // namespace detail + +CLI11_INLINE FileOnDefaultPath::FileOnDefaultPath(std::string default_path, bool enableErrorReturn) + : Validator("FILE") { + func_ = [default_path, enableErrorReturn](std::string &filename) { + auto path_result = detail::check_path(filename.c_str()); + if(path_result == detail::path_type::nonexistent) { + std::string test_file_path = default_path; + if(default_path.back() != '/' && default_path.back() != '\\') { + // Add folder separator + test_file_path += '/'; + } + test_file_path.append(filename); + path_result = detail::check_path(test_file_path.c_str()); + if(path_result == detail::path_type::file) { + filename = test_file_path; + } else { + if(enableErrorReturn) { + return "File does not exist: " + filename; + } + } + } + return std::string{}; + }; +} + +CLI11_INLINE AsSizeValue::AsSizeValue(bool kb_is_1000) : AsNumberWithUnit(get_mapping(kb_is_1000)) { + if(kb_is_1000) { + description("SIZE [b, kb(=1000b), kib(=1024b), ...]"); + } else { + description("SIZE [b, kb(=1024b), ...]"); + } +} + +CLI11_INLINE std::map AsSizeValue::init_mapping(bool kb_is_1000) { + std::map m; + result_t k_factor = kb_is_1000 ? 1000 : 1024; + result_t ki_factor = 1024; + result_t k = 1; + result_t ki = 1; + m["b"] = 1; + for(std::string p : {"k", "m", "g", "t", "p", "e"}) { + k *= k_factor; + ki *= ki_factor; + m[p] = k; + m[p + "b"] = k; + m[p + "i"] = ki; + m[p + "ib"] = ki; + } + return m; +} + +CLI11_INLINE std::map AsSizeValue::get_mapping(bool kb_is_1000) { + if(kb_is_1000) { + static auto m = init_mapping(true); + return m; + } + static auto m = init_mapping(false); + return m; +} + +namespace detail { + +CLI11_INLINE std::pair split_program_name(std::string commandline) { + // try to determine the programName + std::pair vals; + trim(commandline); + auto esp = commandline.find_first_of(' ', 1); + while(detail::check_path(commandline.substr(0, esp).c_str()) != path_type::file) { + esp = commandline.find_first_of(' ', esp + 1); + if(esp == std::string::npos) { + // if we have reached the end and haven't found a valid file just assume the first argument is the + // program name + if(commandline[0] == '"' || commandline[0] == '\'' || commandline[0] == '`') { + bool embeddedQuote = false; + auto keyChar = commandline[0]; + auto end = commandline.find_first_of(keyChar, 1); + while((end != std::string::npos) && (commandline[end - 1] == '\\')) { // deal with escaped quotes + end = commandline.find_first_of(keyChar, end + 1); + embeddedQuote = true; + } + if(end != std::string::npos) { + vals.first = commandline.substr(1, end - 1); + esp = end + 1; + if(embeddedQuote) { + vals.first = find_and_replace(vals.first, std::string("\\") + keyChar, std::string(1, keyChar)); + } + } else { + esp = commandline.find_first_of(' ', 1); + } + } else { + esp = commandline.find_first_of(' ', 1); + } + + break; + } + } + if(vals.first.empty()) { + vals.first = commandline.substr(0, esp); + rtrim(vals.first); + } + + // strip the program name + vals.second = (esp < commandline.length() - 1) ? commandline.substr(esp + 1) : std::string{}; + ltrim(vals.second); + return vals; +} + +} // namespace detail +/// @} + + + + +class Option; +class App; + +/// This enum signifies the type of help requested +/// +/// This is passed in by App; all user classes must accept this as +/// the second argument. + +enum class AppFormatMode { + Normal, ///< The normal, detailed help + All, ///< A fully expanded help + Sub, ///< Used when printed as part of expanded subcommand +}; + +/// This is the minimum requirements to run a formatter. +/// +/// A user can subclass this is if they do not care at all +/// about the structure in CLI::Formatter. +class FormatterBase { + protected: + /// @name Options + ///@{ + + /// The width of the first column + std::size_t column_width_{30}; + + /// @brief The required help printout labels (user changeable) + /// Values are Needs, Excludes, etc. + std::map labels_{}; + + ///@} + /// @name Basic + ///@{ + + public: + FormatterBase() = default; + FormatterBase(const FormatterBase &) = default; + FormatterBase(FormatterBase &&) = default; + FormatterBase &operator=(const FormatterBase &) = default; + FormatterBase &operator=(FormatterBase &&) = default; + + /// Adding a destructor in this form to work around bug in GCC 4.7 + virtual ~FormatterBase() noexcept {} // NOLINT(modernize-use-equals-default) + + /// This is the key method that puts together help + virtual std::string make_help(const App *, std::string, AppFormatMode) const = 0; + + ///@} + /// @name Setters + ///@{ + + /// Set the "REQUIRED" label + void label(std::string key, std::string val) { labels_[key] = val; } + + /// Set the column width + void column_width(std::size_t val) { column_width_ = val; } + + ///@} + /// @name Getters + ///@{ + + /// Get the current value of a name (REQUIRED, etc.) + CLI11_NODISCARD std::string get_label(std::string key) const { + if(labels_.find(key) == labels_.end()) + return key; + return labels_.at(key); + } + + /// Get the current column width + CLI11_NODISCARD std::size_t get_column_width() const { return column_width_; } + + ///@} +}; + +/// This is a specialty override for lambda functions +class FormatterLambda final : public FormatterBase { + using funct_t = std::function; + + /// The lambda to hold and run + funct_t lambda_; + + public: + /// Create a FormatterLambda with a lambda function + explicit FormatterLambda(funct_t funct) : lambda_(std::move(funct)) {} + + /// Adding a destructor (mostly to make GCC 4.7 happy) + ~FormatterLambda() noexcept override {} // NOLINT(modernize-use-equals-default) + + /// This will simply call the lambda function + std::string make_help(const App *app, std::string name, AppFormatMode mode) const override { + return lambda_(app, name, mode); + } +}; + +/// This is the default Formatter for CLI11. It pretty prints help output, and is broken into quite a few +/// overridable methods, to be highly customizable with minimal effort. +class Formatter : public FormatterBase { + public: + Formatter() = default; + Formatter(const Formatter &) = default; + Formatter(Formatter &&) = default; + Formatter &operator=(const Formatter &) = default; + Formatter &operator=(Formatter &&) = default; + + /// @name Overridables + ///@{ + + /// This prints out a group of options with title + /// + CLI11_NODISCARD virtual std::string + make_group(std::string group, bool is_positional, std::vector opts) const; + + /// This prints out just the positionals "group" + virtual std::string make_positionals(const App *app) const; + + /// This prints out all the groups of options + std::string make_groups(const App *app, AppFormatMode mode) const; + + /// This prints out all the subcommands + virtual std::string make_subcommands(const App *app, AppFormatMode mode) const; + + /// This prints out a subcommand + virtual std::string make_subcommand(const App *sub) const; + + /// This prints out a subcommand in help-all + virtual std::string make_expanded(const App *sub) const; + + /// This prints out all the groups of options + virtual std::string make_footer(const App *app) const; + + /// This displays the description line + virtual std::string make_description(const App *app) const; + + /// This displays the usage line + virtual std::string make_usage(const App *app, std::string name) const; + + /// This puts everything together + std::string make_help(const App * /*app*/, std::string, AppFormatMode) const override; + + ///@} + /// @name Options + ///@{ + + /// This prints out an option help line, either positional or optional form + virtual std::string make_option(const Option *opt, bool is_positional) const { + std::stringstream out; + detail::format_help( + out, make_option_name(opt, is_positional) + make_option_opts(opt), make_option_desc(opt), column_width_); + return out.str(); + } + + /// @brief This is the name part of an option, Default: left column + virtual std::string make_option_name(const Option *, bool) const; + + /// @brief This is the options part of the name, Default: combined into left column + virtual std::string make_option_opts(const Option *) const; + + /// @brief This is the description. Default: Right column, on new line if left column too large + virtual std::string make_option_desc(const Option *) const; + + /// @brief This is used to print the name on the USAGE line + virtual std::string make_option_usage(const Option *opt) const; + + ///@} +}; + + + + +using results_t = std::vector; +/// callback function definition +using callback_t = std::function; + +class Option; +class App; + +using Option_p = std::unique_ptr