From 67f1cd8b48611e4bda315bc4a649492083fd6cd3 Mon Sep 17 00:00:00 2001 From: Mathieu Geukens Date: Sun, 2 Mar 2025 12:37:03 +0100 Subject: [PATCH 1/7] rework ElectrumNetworkProvider --- packages/cashscript/package.json | 2 +- .../src/network/ElectrumNetworkProvider.ts | 118 +++++------------- yarn.lock | 106 +++++++++++----- 3 files changed, 102 insertions(+), 124 deletions(-) diff --git a/packages/cashscript/package.json b/packages/cashscript/package.json index b5fde331..f6372471 100644 --- a/packages/cashscript/package.json +++ b/packages/cashscript/package.json @@ -45,10 +45,10 @@ "dependencies": { "@bitauth/libauth": "^3.1.0-next.2", "@cashscript/utils": "^0.11.0-next.1", + "@electrum-cash/network": "^4.1.1", "@mr-zwets/bchn-api-wrapper": "^1.0.1", "change-case": "^5.4.4", "delay": "^6.0.0", - "electrum-cash": "^2.0.10", "fast-deep-equal": "^3.1.3", "pako": "^2.1.0", "semver": "^7.6.3" diff --git a/packages/cashscript/src/network/ElectrumNetworkProvider.ts b/packages/cashscript/src/network/ElectrumNetworkProvider.ts index 789b7ef9..ddab9aae 100644 --- a/packages/cashscript/src/network/ElectrumNetworkProvider.ts +++ b/packages/cashscript/src/network/ElectrumNetworkProvider.ts @@ -1,55 +1,35 @@ import { binToHex } from '@bitauth/libauth'; import { sha256 } from '@cashscript/utils'; import { - ElectrumCluster, - ElectrumTransport, - ClusterOrder, - RequestResponse, -} from 'electrum-cash'; + ElectrumClient, + type RequestResponse, + type ElectrumClientEvents, +} from '@electrum-cash/network'; import { Utxo, Network } from '../interfaces.js'; import NetworkProvider from './NetworkProvider.js'; import { addressToLockScript } from '../utils.js'; export default class ElectrumNetworkProvider implements NetworkProvider { - private electrum: ElectrumCluster; - private concurrentRequests: number = 0; - - constructor( - public network: Network = Network.MAINNET, - electrum?: ElectrumCluster, - private manualConnectionManagement?: boolean, - ) { - // If a custom Electrum Cluster is passed, we use it instead of the default. - if (electrum) { - this.electrum = electrum; - return; - } + private electrum: ElectrumClient; + + constructor(public network: Network = Network.MAINNET) { + const server = this.getServerForNetwork(network); + this.electrum = new ElectrumClient('CashScript Application', '1.4.1', server); + } - if (network === Network.MAINNET) { - // Initialise a 2-of-3 Electrum Cluster with 6 reliable hardcoded servers - // using the first three servers as "priority" servers - this.electrum = new ElectrumCluster('CashScript Application', '1.4.1', 2, 3, ClusterOrder.PRIORITY); - this.electrum.addServer('bch.imaginary.cash', 50004, ElectrumTransport.WSS.Scheme, false); - this.electrum.addServer('blackie.c3-soft.com', 50004, ElectrumTransport.WSS.Scheme, false); - this.electrum.addServer('electroncash.de', 60002, ElectrumTransport.WSS.Scheme, false); - this.electrum.addServer('electroncash.dk', 50004, ElectrumTransport.WSS.Scheme, false); - this.electrum.addServer('bch.loping.net', 50004, ElectrumTransport.WSS.Scheme, false); - this.electrum.addServer('electrum.imaginary.cash', 50004, ElectrumTransport.WSS.Scheme, false); - } else if (network === Network.TESTNET3) { - // Initialise a 1-of-2 Electrum Cluster with 2 hardcoded servers - this.electrum = new ElectrumCluster('CashScript Application', '1.4.1', 1, 2, ClusterOrder.PRIORITY); - this.electrum.addServer('blackie.c3-soft.com', 60004, ElectrumTransport.WSS.Scheme, false); - this.electrum.addServer('electroncash.de', 60004, ElectrumTransport.WSS.Scheme, false); - // this.electrum.addServer('bch.loping.net', 60004, ElectrumTransport.WSS.Scheme, false); - // this.electrum.addServer('testnet.imaginary.cash', 50004, ElectrumTransport.WSS.Scheme); - } else if (network === Network.TESTNET4) { - this.electrum = new ElectrumCluster('CashScript Application', '1.4.1', 1, 1, ClusterOrder.PRIORITY); - this.electrum.addServer('testnet4.imaginary.cash', 50004, ElectrumTransport.WSS.Scheme, false); - } else if (network === Network.CHIPNET) { - this.electrum = new ElectrumCluster('CashScript Application', '1.4.1', 1, 1, ClusterOrder.PRIORITY); - this.electrum.addServer('chipnet.bch.ninja', 50004, ElectrumTransport.WSS.Scheme, false); - } else { - throw new Error(`Tried to instantiate an ElectrumNetworkProvider for unsupported network ${network}`); + // Get Electrum server based on network + private getServerForNetwork(network: Network): string { + switch (network) { + case Network.MAINNET: + return 'bch.imaginary.cash'; + case Network.TESTNET3: + return 'blackie.c3-soft.com'; + case Network.TESTNET4: + return 'testnet4.imaginary.cash'; + case Network.CHIPNET: + return 'chipnet.bch.ninja'; + default: + throw new Error(`Unsupported network: ${network}`); } } @@ -85,59 +65,19 @@ export default class ElectrumNetworkProvider implements NetworkProvider { return await this.performRequest('blockchain.transaction.broadcast', txHex) as string; } - async connectCluster(): Promise { - try { - return await this.electrum.startup(); - } catch (e) { - return []; - } - } - - async disconnectCluster(): Promise { - return this.electrum.shutdown(); - } - + // Perform request with auto-disconnect async performRequest( name: string, ...parameters: (string | number | boolean)[] ): Promise { - // Only connect the cluster when no concurrent requests are running - if (this.shouldConnect()) { - this.connectCluster(); - } - - this.concurrentRequests += 1; - - await this.electrum.ready(); - - let result; try { - result = await this.electrum.request(name, ...parameters); + await this.electrum.connect(); + const result = await this.electrum.request(name, ...parameters); + if (result instanceof Error) throw result; + return result; } finally { - // Always disconnect the cluster, also if the request fails - // as long as no other concurrent requests are running - if (this.shouldDisconnect()) { - await this.disconnectCluster(); - } + await this.electrum.disconnect(); } - - this.concurrentRequests -= 1; - - if (result instanceof Error) throw result; - - return result; - } - - private shouldConnect(): boolean { - if (this.manualConnectionManagement) return false; - if (this.concurrentRequests !== 0) return false; - return true; - } - - private shouldDisconnect(): boolean { - if (this.manualConnectionManagement) return false; - if (this.concurrentRequests !== 1) return false; - return true; } } diff --git a/yarn.lock b/yarn.lock index 9a1a8d22..5e08360e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1056,6 +1056,38 @@ resolved "https://registry.yarnpkg.com/@cspell/url/-/url-8.17.2.tgz#81f8cd7d31b7d160971a207da5b6486eebc5aecb" integrity sha512-yy4eYWNX2iutXmy4Igbn/hL/NYaNt94DylohPtgVr0Zxnn/AAArt9Bv1KXPpjB8VFy2wzzPzWmZ+MWDUVpHCbg== +"@electrum-cash/debug-logs@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@electrum-cash/debug-logs/-/debug-logs-1.0.0.tgz#7d021515f1b881f477176cb640f2178d094181e5" + integrity sha512-GU/CvRR9lZ0d8gy9CXGW7f//OHCIydBavv9q+JcxjGj8Xr7HwlGqHx+Wzhx9y3YmJrXfExpgClcd++gTjdEmzA== + dependencies: + debug "^4.3.7" + +"@electrum-cash/network@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@electrum-cash/network/-/network-4.1.1.tgz#1f9571c783f613ce960374b4bffb73c27b28c429" + integrity sha512-v5abF2qGRTnBoi9tcS/iz7j82D8HYsK9iY0NM5v8/Qu8SnlMGGNz8UDFl+YzRPFXb4SUL3K0uf3Oydy82DB3oA== + dependencies: + "@electrum-cash/debug-logs" "^1.0.0" + "@electrum-cash/web-socket" "^1.0.0" + async-mutex "^0.5.0" + debug "^4.3.2" + eventemitter3 "^5.0.1" + lossless-json "^4.0.1" + +"@electrum-cash/web-socket@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@electrum-cash/web-socket/-/web-socket-1.0.0.tgz#0ab57e46d41e941ffb57deff86afea3ae1379d5d" + integrity sha512-+VQ6aPE7nUysyDn9SB7/uqKuVJmjrhvr0LRK2ANTeR1DfmXBnv5z29e/0zK5A7ZoAz0gdZZuLDzN46r8S5Cxig== + dependencies: + "@electrum-cash/debug-logs" "^1.0.0" + "@monsterbitar/isomorphic-ws" "^5.3.0" + "@types/ws" "^8.5.5" + async-mutex "^0.5.0" + eventemitter3 "^5.0.1" + lossless-json "^4.0.1" + ws "^8.13.0" + "@esbuild/aix-ppc64@0.23.1": version "0.23.1" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz#51299374de171dbd80bb7d838e1cfce9af36f353" @@ -2385,6 +2417,11 @@ npmlog "^4.1.2" write-file-atomic "^2.3.0" +"@monsterbitar/isomorphic-ws@^5.3.0": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@monsterbitar/isomorphic-ws/-/isomorphic-ws-5.3.1.tgz#acd6be86c7568682d8146f8b5fadbec74bf8789e" + integrity sha512-BWfWUffbg3uO4K6Cyokg9ff43lPaXAOZcCnNe1lcjCjUMDVrRAb5qEHG5qeJp3ud2SPYbORaNsls5as6SR3oig== + "@mr-zwets/bchn-api-wrapper@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@mr-zwets/bchn-api-wrapper/-/bchn-api-wrapper-1.0.1.tgz#1ecd9fca91ed7e33df9769e243ae04871d2d356f" @@ -2778,10 +2815,10 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== -"@types/ws@^7.4.6": - version "7.4.7" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" - integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== +"@types/ws@^8.5.5": + version "8.5.14" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.14.tgz#93d44b268c9127d96026cf44353725dd9b6c3c21" + integrity sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw== dependencies: "@types/node" "*" @@ -3250,12 +3287,12 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -async-mutex@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.3.2.tgz#1485eda5bda1b0ec7c8df1ac2e815757ad1831df" - integrity sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA== +async-mutex@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.5.0.tgz#353c69a0b9e75250971a64ac203b0ebfddd75482" + integrity sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA== dependencies: - tslib "^2.3.1" + tslib "^2.4.0" asynckit@^0.4.0: version "0.4.0" @@ -4543,6 +4580,13 @@ debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: dependencies: ms "2.1.2" +debug@^4.3.7: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -4823,17 +4867,6 @@ electron-to-chromium@^1.5.73: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.84.tgz#8e334ca206bb293a20b16418bf454783365b0a95" integrity sha512-I+DQ8xgafao9Ha6y0qjHHvpZ9OfyA1qKlkHkjywxzniORU2awxyz7f/iVJcULmrF2yrM3nHQf+iDjJtbbexd/g== -electrum-cash@^2.0.10: - version "2.0.10" - resolved "https://registry.yarnpkg.com/electrum-cash/-/electrum-cash-2.0.10.tgz#0b2831d01b214bff1dfee7b90022f36126cb6945" - integrity sha512-xxKXWyDsnUR2pkqzb7r7ANf6yzlHO8+Rkq8HOFkKiH1tF1BJwyh6sDhGAy4lkTv2sU8Uv1em/itYZ5n2Cr6zaA== - dependencies: - "@types/ws" "^7.4.6" - async-mutex "^0.3.1" - debug "^4.3.2" - isomorphic-ws "^4.0.1" - ws "^7.5.2" - elliptic@^6.5.2: version "6.5.3" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" @@ -5325,6 +5358,11 @@ eventemitter3@^3.1.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" @@ -6979,11 +7017,6 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= -isomorphic-ws@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" - integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== - isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -7837,6 +7870,11 @@ lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.2.1: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lossless-json@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/lossless-json/-/lossless-json-4.0.2.tgz#f00c52815805d1421930a87e2670e27350958a3f" + integrity sha512-+z0EaLi2UcWi8MZRxA5iTb6m4Ys4E80uftGY+yG5KNFJb5EceQXOhdW/pWJZ8m97s26u7yZZAYMcKWNztSZssA== + loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -8210,7 +8248,7 @@ ms@2.1.2, ms@^2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1: +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -10614,10 +10652,10 @@ tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== -tslib@^2.3.1: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tslib@^2.4.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== tsx@^4.19.2: version "4.19.2" @@ -11198,10 +11236,10 @@ write-pkg@^3.1.0: sort-keys "^2.0.0" write-json-file "^2.2.0" -ws@^7.5.2: - version "7.5.9" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@^8.13.0: + version "8.18.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" + integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== xdg-basedir@^5.1.0: version "5.1.0" From 9b772a4baa93de12641966e5f8eb99a638195069 Mon Sep 17 00:00:00 2001 From: Mathieu Geukens Date: Sun, 2 Mar 2025 12:47:55 +0100 Subject: [PATCH 2/7] update docs for electrumNetworkProvider & release-notes --- website/docs/releases/release-notes.md | 5 ++++- website/docs/sdk/network-provider.md | 10 +++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index bb560464..e189c873 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -2,10 +2,12 @@ title: Release Notes --- -## v0.11.0-next.1 +## v0.11.0-next.2 This update adds CashScript support for the new BCH 2025 network upgrade. To read more about the upgrade, see [this blog post](https://blog.bitjson.com/2025-chips/). +This release also contains several breaking changes, please refer to the [migration notes](/docs/releases/migration-notes) for more information. + #### cashc compiler - :hammer_and_wrench: Remove warning for opcount and update warning for byte size to match new limits. @@ -16,6 +18,7 @@ This update adds CashScript support for the new BCH 2025 network upgrade. To rea - Libauth template generation and debugging for multi-contract transactions - :hammer_and_wrench: Deprecate the simple transaction builder. You can still use the simple transaction builder with the current SDK, but this support will be removed in a future release. - :hammer_and_wrench: Update debug tooling to use the new `BCH_2025_05` instruction set. +- :boom: **BREAKING**: Remove support for custom Clusters from `ElectrumNetworkProvider`. - :boom: **BREAKING**: Remove support for old contracts compiled with CashScript v0.6.x or earlier. ## v0.10.5 diff --git a/website/docs/sdk/network-provider.md b/website/docs/sdk/network-provider.md index 7e7e83b2..814db388 100644 --- a/website/docs/sdk/network-provider.md +++ b/website/docs/sdk/network-provider.md @@ -6,16 +6,12 @@ The CashScript SDK needs to connect to the BCH network to perform certain operat ## ElectrumNetworkProvider -The ElectrumNetworkProvider uses [electrum-cash][electrum-cash] to connect to the BCH network. Both `network` and `electrum` parameters are optional, and they default to mainnet and a 2-of-3 ElectrumCluster with a number of reliable electrum servers. +The ElectrumNetworkProvider uses [@electrum-cash/network][electrum-cash] to connect to the BCH network. Both `network` and `electrum` parameters are optional, and they default to mainnet with the `bch.imaginary.cash` electrum server. ```ts -new ElectrumNetworkProvider(network?: Network, electrum?: ElectrumCluster) +new ElectrumNetworkProvider(network?: Network, electrum?: ElectrumClient) ``` -:::note -In some cases it might be desirable to overwrite the 2-of-3 ElectrumCluster default to use only a 1-of-1 cluster because of network latency. -::: - ### Network ```ts @@ -127,6 +123,6 @@ To implement a Custom NetworkProvider, refer to the [NetworkProvider interface]( ::: -[electrum-cash]: https://www.npmjs.com/package/electrum-cash +[electrum-cash]: https://www.npmjs.com/package/@electrum-cash/network [fullstack]: https://fullstack.cash/ [bchjs]: https://bchjs.fullstack.cash/ From 84f31dfe01e924d9a626cd684d14d9d8b7637ca3 Mon Sep 17 00:00:00 2001 From: Mathieu Geukens Date: Tue, 4 Mar 2025 08:43:59 +0100 Subject: [PATCH 3/7] small docs update --- website/docs/sdk/network-provider.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/docs/sdk/network-provider.md b/website/docs/sdk/network-provider.md index 814db388..e65aa962 100644 --- a/website/docs/sdk/network-provider.md +++ b/website/docs/sdk/network-provider.md @@ -25,7 +25,7 @@ The network parameter can be one of 5 different options. const provider = new ElectrumProvider('chipnet'); ``` -### getUtxos +### getUtxos() ```ts async provider.getUtxos(address: string): Promise; ``` @@ -45,7 +45,7 @@ interface Utxo { const userUtxos = await provider.getUtxos(userAddress) ``` -### getBlockHeight +### getBlockHeight() ```ts async provider.getBlockHeight(): Promise; ``` @@ -56,7 +56,7 @@ Get the current blockHeight. const currentBlockHeight = await provider.getBlockHeight() ``` -### getRawTransaction +### getRawTransaction() ```ts async provider.getRawTransaction(txid: string): Promise; ``` @@ -68,7 +68,7 @@ Retrieve the Hex transaction details for a given transaction ID. const rawTransaction = await provider.getRawTransaction(txid) ``` -### sendRawTransaction +### sendRawTransaction() ```ts async provider.sendRawTransaction(txHex: string): Promise; ``` @@ -79,7 +79,7 @@ Broadcast a raw hex transaction to the network. const txId = await provider.sendRawTransaction(txHex) ``` -### performRequest +### performRequest() Perform an arbitrary electrum request, refer to the docs at [electrum-cash-protocol](https://electrum-cash-protocol.readthedocs.io/en/latest/). From 48e91799003658a7003472da9cca4cda18ace1e0 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 4 Mar 2025 10:54:41 +0100 Subject: [PATCH 4/7] Re-add concurrent connection management + test --- .../src/network/ElectrumNetworkProvider.ts | 39 ++++++++++++++++--- .../network/ElectrumNetworkProvider.test.ts | 25 ++++++++++++ ...st.ts => FullStackNetworkProvider.test.ts} | 0 3 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 packages/cashscript/test/e2e/network/ElectrumNetworkProvider.test.ts rename packages/cashscript/test/e2e/network/{FullStack.test.ts => FullStackNetworkProvider.test.ts} (100%) diff --git a/packages/cashscript/src/network/ElectrumNetworkProvider.ts b/packages/cashscript/src/network/ElectrumNetworkProvider.ts index ddab9aae..e6403560 100644 --- a/packages/cashscript/src/network/ElectrumNetworkProvider.ts +++ b/packages/cashscript/src/network/ElectrumNetworkProvider.ts @@ -11,6 +11,7 @@ import { addressToLockScript } from '../utils.js'; export default class ElectrumNetworkProvider implements NetworkProvider { private electrum: ElectrumClient; + private concurrentRequests: number = 0; constructor(public network: Network = Network.MAINNET) { const server = this.getServerForNetwork(network); @@ -65,19 +66,45 @@ export default class ElectrumNetworkProvider implements NetworkProvider { return await this.performRequest('blockchain.transaction.broadcast', txHex) as string; } - // Perform request with auto-disconnect async performRequest( name: string, ...parameters: (string | number | boolean)[] ): Promise { - try { + // Only connect the electrum client when no concurrent requests are running + if (this.shouldConnect()) { await this.electrum.connect(); - const result = await this.electrum.request(name, ...parameters); - if (result instanceof Error) throw result; - return result; + } + + this.concurrentRequests += 1; + + let result; + try { + result = await this.electrum.request(name, ...parameters); } finally { - await this.electrum.disconnect(); + // Always disconnect the electrum client, also if the request fails + // as long as no other concurrent requests are running + if (this.shouldDisconnect()) { + await this.electrum.disconnect(); + } } + + this.concurrentRequests -= 1; + + if (result instanceof Error) throw result; + + return result; + } + + private shouldConnect(): boolean { + // if (this.manualConnectionManagement) return false; + if (this.concurrentRequests !== 0) return false; + return true; + } + + private shouldDisconnect(): boolean { + // if (this.manualConnectionManagement) return false; + if (this.concurrentRequests !== 1) return false; + return true; } } diff --git a/packages/cashscript/test/e2e/network/ElectrumNetworkProvider.test.ts b/packages/cashscript/test/e2e/network/ElectrumNetworkProvider.test.ts new file mode 100644 index 00000000..69ff7223 --- /dev/null +++ b/packages/cashscript/test/e2e/network/ElectrumNetworkProvider.test.ts @@ -0,0 +1,25 @@ +import { describeOrSkip } from '../../../test/test-util.js'; +import { ElectrumNetworkProvider, Network } from '../../../src/index.js'; + +describeOrSkip(!process.env.TESTS_USE_MOCKNET, 'ElectrumNetworkProvider', () => { + // TODO: Test more of the API + it('should be able to request data', async () => { + const provider = new ElectrumNetworkProvider(Network.CHIPNET); + const blockHeight = await provider.getBlockHeight(); + expect(blockHeight).toBeDefined(); + }); + + it('should be able to handle multiple concurrent requests', async () => { + const provider = new ElectrumNetworkProvider(Network.CHIPNET); + + const transactionPromise = provider.getRawTransaction('d72923b7cf345d2a90f951d66f437d4fbb941fcbd87bb54d8d88e51ecb3e3b9f'); + const blockHeightPromise = provider.getBlockHeight(); + + const transaction = await transactionPromise; + const blockHeight = await blockHeightPromise; + + expect(blockHeight).toBeDefined(); + expect(transaction).toBeDefined(); + }); +}); + diff --git a/packages/cashscript/test/e2e/network/FullStack.test.ts b/packages/cashscript/test/e2e/network/FullStackNetworkProvider.test.ts similarity index 100% rename from packages/cashscript/test/e2e/network/FullStack.test.ts rename to packages/cashscript/test/e2e/network/FullStackNetworkProvider.test.ts From bc8e58c9fa98b5eaf55f66d675eafaab55221c33 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 4 Mar 2025 11:20:53 +0100 Subject: [PATCH 5/7] Add options object to pass in custom electrum/hostname and re-add manual connection management + tests --- .../src/network/ElectrumNetworkProvider.ts | 50 ++++++++++++++++--- .../network/ElectrumNetworkProvider.test.ts | 36 +++++++++++++ 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/packages/cashscript/src/network/ElectrumNetworkProvider.ts b/packages/cashscript/src/network/ElectrumNetworkProvider.ts index e6403560..49ae4478 100644 --- a/packages/cashscript/src/network/ElectrumNetworkProvider.ts +++ b/packages/cashscript/src/network/ElectrumNetworkProvider.ts @@ -9,13 +9,35 @@ import { Utxo, Network } from '../interfaces.js'; import NetworkProvider from './NetworkProvider.js'; import { addressToLockScript } from '../utils.js'; + +interface OptionsBase { + manualConnectionManagement?: boolean; +} + +interface CustomHostNameOptions extends OptionsBase { + hostname: string; +} + +interface CustomElectrumOptions extends OptionsBase { + electrum: ElectrumClient; +} + +type Options = OptionsBase | CustomHostNameOptions | CustomElectrumOptions; + export default class ElectrumNetworkProvider implements NetworkProvider { private electrum: ElectrumClient; private concurrentRequests: number = 0; + private manualConnectionManagement: boolean; + + constructor(public network: Network = Network.MAINNET, options: Options = {}) { + this.electrum = this.instantiateElectrumClient(network, options); + this.manualConnectionManagement = options?.manualConnectionManagement ?? false; + } - constructor(public network: Network = Network.MAINNET) { - const server = this.getServerForNetwork(network); - this.electrum = new ElectrumClient('CashScript Application', '1.4.1', server); + private instantiateElectrumClient(network: Network, options: Options): ElectrumClient { + if ('electrum' in options) return options.electrum; + const server = 'hostname' in options ? options.hostname : this.getServerForNetwork(network); + return new ElectrumClient('CashScript Application', '1.4.1', server); } // Get Electrum server based on network @@ -24,7 +46,7 @@ export default class ElectrumNetworkProvider implements NetworkProvider { case Network.MAINNET: return 'bch.imaginary.cash'; case Network.TESTNET3: - return 'blackie.c3-soft.com'; + return 'testnet.imaginary.cash'; case Network.TESTNET4: return 'testnet4.imaginary.cash'; case Network.CHIPNET: @@ -66,6 +88,22 @@ export default class ElectrumNetworkProvider implements NetworkProvider { return await this.performRequest('blockchain.transaction.broadcast', txHex) as string; } + async connect(): Promise { + if (!this.manualConnectionManagement) { + throw new Error('Manual connection management is disabled'); + } + + return this.electrum.connect(); + } + + async disconnect(): Promise { + if (!this.manualConnectionManagement) { + throw new Error('Manual connection management is disabled'); + } + + return this.electrum.disconnect(); + } + async performRequest( name: string, ...parameters: (string | number | boolean)[] @@ -96,13 +134,13 @@ export default class ElectrumNetworkProvider implements NetworkProvider { } private shouldConnect(): boolean { - // if (this.manualConnectionManagement) return false; + if (this.manualConnectionManagement) return false; if (this.concurrentRequests !== 0) return false; return true; } private shouldDisconnect(): boolean { - // if (this.manualConnectionManagement) return false; + if (this.manualConnectionManagement) return false; if (this.concurrentRequests !== 1) return false; return true; } diff --git a/packages/cashscript/test/e2e/network/ElectrumNetworkProvider.test.ts b/packages/cashscript/test/e2e/network/ElectrumNetworkProvider.test.ts index 69ff7223..653b1a56 100644 --- a/packages/cashscript/test/e2e/network/ElectrumNetworkProvider.test.ts +++ b/packages/cashscript/test/e2e/network/ElectrumNetworkProvider.test.ts @@ -1,5 +1,6 @@ import { describeOrSkip } from '../../../test/test-util.js'; import { ElectrumNetworkProvider, Network } from '../../../src/index.js'; +import { ElectrumClient } from '@electrum-cash/network'; describeOrSkip(!process.env.TESTS_USE_MOCKNET, 'ElectrumNetworkProvider', () => { // TODO: Test more of the API @@ -21,5 +22,40 @@ describeOrSkip(!process.env.TESTS_USE_MOCKNET, 'ElectrumNetworkProvider', () => expect(blockHeight).toBeDefined(); expect(transaction).toBeDefined(); }); + + it('should be able to pass in a custom electrum client', async () => { + const electrum = new ElectrumClient('CashScript Application', '1.4.1', 'chipnet.bch.ninja'); + const provider = new ElectrumNetworkProvider(Network.CHIPNET, { electrum }); + const blockHeight = await provider.getBlockHeight(); + expect(blockHeight).toBeDefined(); + }); + + it('should be able to pass in a custom host name', async () => { + const provider = new ElectrumNetworkProvider(Network.CHIPNET, { hostname: 'chipnet.bch.ninja' }); + const blockHeight = await provider.getBlockHeight(); + expect(blockHeight).toBeDefined(); + }); + + describe('manual connection management', () => { + it('should throw an error if trying to use connect/disconnect without specifying manualConnectionManagement', async () => { + const provider = new ElectrumNetworkProvider(Network.CHIPNET); + expect(provider.connect()).rejects.toThrow('Manual connection management is disabled'); + }); + + it('should throw an error if client is not connected', async () => { + const provider = new ElectrumNetworkProvider(Network.CHIPNET, { manualConnectionManagement: true }); + expect(provider.getBlockHeight()).rejects.toThrow('Unable to send request to a disconnected server'); + }); + + it('should be able to send requests after connecting', async () => { + const provider = new ElectrumNetworkProvider(Network.CHIPNET, { manualConnectionManagement: true }); + + await provider.connect(); + const blockHeight = await provider.getBlockHeight(); + await provider.disconnect(); + + expect(blockHeight).toBeDefined(); + }); + }); }); From 8d0755637a0ecf81cd959a0959064aefc60d7926 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 4 Mar 2025 11:45:39 +0100 Subject: [PATCH 6/7] Update docs and release notes for ElectrumNetworkProvider changes --- website/docs/releases/migration-notes.md | 40 ++++++++++++++++++- website/docs/releases/release-notes.md | 2 +- website/docs/sdk/network-provider.md | 50 ++++++++++++++++++++++-- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/website/docs/releases/migration-notes.md b/website/docs/releases/migration-notes.md index 881a1ab0..04d704dc 100644 --- a/website/docs/releases/migration-notes.md +++ b/website/docs/releases/migration-notes.md @@ -4,7 +4,9 @@ title: Migration Notes ## v0.10 to v0.11 -### CashScript SDK +There are several breaking changes to the SDK in this release. They are listed below in their own sections. + +### CashScript SDK (Transaction Builder) The 'Simple Transaction builder' has been marked as deprecated and the 'Advanced Transaction Builder' is now simply referred to as the CashScript `Transaction Builder`, as there is only one supported for the future. @@ -69,6 +71,42 @@ const txDetails = await new TransactionBuilder({ provider }) With the new transaction builder, all inputs and outputs are explicitly specified. This means that there are no automatic change outputs added (for BCH or tokens). This means that there are also no `.withMinChange()`, `.withoutChange()`, `.withoutTokenChange()`, `withHardcodedFee()`, or `.withFeePerByte()` methods. The developer is responsible for manually adding change outputs. There is still an option to use `.setMaxFee()` as a security measure to prevent the transaction from being too expensive. +### CashScript SDK (ElectrumNetworkProvider) + +The underlying `electrum-cash` library has been mgrated to the new `@electrum-cash/network` package. This drops support for electrum cluster functionality. We reworked the second parameter of the `ElectrumNetworkProvider` constructor to be an options object, which can contain a custom electrum client or a custom hostname. + +If you were not using custom clusters, there is no need to change anything. If you were using custom clusters, you will need to update your code to use the new `@electrum-cash/network` package and use a single client instead of a cluster. + +#### Example + +Before: + +```ts +import { ElectrumCluster, ClusterOrder } from 'electrum-cash'; +import { ElectrumNetworkProvider } from 'cashscript'; + +const customCluster = new ElectrumCluster('CashScript Application', '1.4.1', 2, 3, ClusterOrder.PRIORITY); +customCluster.addServer('bch.imaginary.cash', 50004, ElectrumTransport.WSS.Scheme, false); +customCluster.addServer('blackie.c3-soft.com', 50004, ElectrumTransport.WSS.Scheme, false); +customCluster.addServer('electroncash.dk', 50004, ElectrumTransport.WSS.Scheme, false); + +const provider = new ElectrumNetworkProvider('mainnet', customCluster); +``` + +After: + +```ts +import { ElectrumClient } from '@electrum-cash/network'; +import { ElectrumNetworkProvider } from 'cashscript'; + +const customClient = new ElectrumClient('CashScript Application', '1.4.1', 'bch.imaginary.cash'); +const provider = new ElectrumNetworkProvider('mainnet', { electrum: customClient }); + +// or + +const provider = new ElectrumNetworkProvider('mainnet', { hostname: 'bch.imaginary.cash' }); +``` + ## v0.9 to v0.10 ### CashScript SDK diff --git a/website/docs/releases/release-notes.md b/website/docs/releases/release-notes.md index e189c873..482ffae7 100644 --- a/website/docs/releases/release-notes.md +++ b/website/docs/releases/release-notes.md @@ -18,7 +18,7 @@ This release also contains several breaking changes, please refer to the [migrat - Libauth template generation and debugging for multi-contract transactions - :hammer_and_wrench: Deprecate the simple transaction builder. You can still use the simple transaction builder with the current SDK, but this support will be removed in a future release. - :hammer_and_wrench: Update debug tooling to use the new `BCH_2025_05` instruction set. -- :boom: **BREAKING**: Remove support for custom Clusters from `ElectrumNetworkProvider`. +- :boom: **BREAKING**: Remove support for custom Clusters from `ElectrumNetworkProvider` and added a configuration object to the constructor. - :boom: **BREAKING**: Remove support for old contracts compiled with CashScript v0.6.x or earlier. ## v0.10.5 diff --git a/website/docs/sdk/network-provider.md b/website/docs/sdk/network-provider.md index e65aa962..97faf9c6 100644 --- a/website/docs/sdk/network-provider.md +++ b/website/docs/sdk/network-provider.md @@ -6,23 +6,42 @@ The CashScript SDK needs to connect to the BCH network to perform certain operat ## ElectrumNetworkProvider -The ElectrumNetworkProvider uses [@electrum-cash/network][electrum-cash] to connect to the BCH network. Both `network` and `electrum` parameters are optional, and they default to mainnet with the `bch.imaginary.cash` electrum server. +The ElectrumNetworkProvider uses [@electrum-cash/network][electrum-cash] to connect to the BCH network. Both `network` and `options` parameters are optional, and they default to mainnet with the `bch.imaginary.cash` electrum server. ```ts -new ElectrumNetworkProvider(network?: Network, electrum?: ElectrumClient) +new ElectrumNetworkProvider(network?: Network, options?: Options) ``` +Using the `network` parameter, you can specify the network to connect to. -### Network ```ts type Network = 'mainnet' | 'chipnet' | 'testnet3' | 'testnet4' | 'regtest'; ``` +Using the `options` parameter, you can specify a custom electrum client or hostname, and enable manual connection management. + +```ts +type Options = OptionsBase | CustomHostNameOptions | CustomElectrumOptions; + +interface OptionsBase { + manualConnectionManagement?: boolean; +} + +interface CustomHostNameOptions extends OptionsBase { + hostname: string; +} + +interface CustomElectrumOptions extends OptionsBase { + electrum: ElectrumClient; +} +``` + The network parameter can be one of 5 different options. #### Example ```ts -const provider = new ElectrumProvider('chipnet'); +const hostname = 'chipnet.bch.ninja'; +const provider = new ElectrumNetworkProvider('chipnet', { hostname }); ``` ### getUtxos() @@ -89,6 +108,29 @@ const verbose = true // get parsed transaction as json result const txId = await provider.performRequest('blockchain.transaction.get', txid, verbose) ``` +### Manual Connection Management + +By default, the ElectrumNetworkProvider will automatically connect and disconnect to the electrum client as needed. However, you can enable manual connection management by setting the `manualConnectionManagement` option to `true`. This can be useful if you are passing a custom electrum client and are using that client for other purposes, such as subscribing to events. + +```ts +const provider = new ElectrumNetworkProvider('chipnet', { manualConnectionManagement: true }); +``` + +#### connect() +```ts +provider.connect(): Promise; +``` + +Connects to the electrum client. + +#### disconnect() +```ts +provider.disconnect(): Promise; +``` + +Disconnects from the electrum client, returns `true` if the client was connected, `false` if it was already disconnected. + + ## Advanced Options All network functionality that the CashScript SDK needs is encapsulated in a network provider. This allows different network providers to be used and makes it easy to swap out dependencies. From 23ef0ae7991ee784232bfad5c1657546acc929f4 Mon Sep 17 00:00:00 2001 From: Mathieu Geukens Date: Tue, 4 Mar 2025 12:38:20 +0100 Subject: [PATCH 7/7] tiny docs fix --- website/docs/sdk/network-provider.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/website/docs/sdk/network-provider.md b/website/docs/sdk/network-provider.md index 97faf9c6..48fd1de3 100644 --- a/website/docs/sdk/network-provider.md +++ b/website/docs/sdk/network-provider.md @@ -12,7 +12,7 @@ The ElectrumNetworkProvider uses [@electrum-cash/network][electrum-cash] to conn new ElectrumNetworkProvider(network?: Network, options?: Options) ``` -Using the `network` parameter, you can specify the network to connect to. +Using the `network` parameter, you can specify the network to connect to. The network parameter can be one of 5 different options: ```ts type Network = 'mainnet' | 'chipnet' | 'testnet3' | 'testnet4' | 'regtest'; @@ -36,8 +36,6 @@ interface CustomElectrumOptions extends OptionsBase { } ``` -The network parameter can be one of 5 different options. - #### Example ```ts const hostname = 'chipnet.bch.ninja';