├── LICENSE ├── README.md ├── examples ├── 2fa.json └── revault.json ├── index.html ├── rust ├── .gitignore ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── screenshot.png └── www ├── app.js ├── js ├── area-plugin.min.js ├── auto-arrange-plugin.min.js ├── comment-plugin.min.js ├── connection-mastery-plugin.min.js ├── connection-plugin.min.js ├── context-menu-plugin.min.js ├── history-plugin.min.js ├── lodash.min.js ├── rete.min.js ├── vue-render-plugin.min.js └── vue.min.js └── style.css /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Stepan Snigirev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Miniscript playground 2 | 3 | WORK IN PROGRESS 4 | 5 | WASM + [rust-miniscript](https://github.com/rust-bitcoin/rust-miniscript) + [rete.js](https://github.com/retejs/rete) = miniscript node editor 6 | 7 | Try online: [miniscript.fun](https://miniscript.fun) 8 | 9 | ![](screenshot.png) 10 | 11 | ## How to build and run 12 | 13 | You will need [rust](https://www.rust-lang.org/tools/install) and [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/), then: 14 | ``` 15 | cd rust 16 | cargo build 17 | wasm-pack build --target web 18 | cd .. 19 | python3 -m http.server 20 | ``` 21 | 22 | You'll see the website on http://localhost:8000 23 | 24 | ## Roadmap 25 | 26 | Planned: 27 | 28 | - [x] all policy op-codes 29 | - [x] address derivation 30 | - [x] automatic url encoding of full editor state (`#/full/b64-encoded-json`) 31 | - [x] node inspector with node output 32 | - [x] bip39 and DescriptorKey nodes (mnemonic to root key, then derive with path and select allowed derivation) 33 | - [x] network selector (bitcoin / testnet / regtest / signet) 34 | - [x] export/import file and loading nodes state from external json (`#/url/path/to/config.json`) 35 | - [x] add labels for inputs like threshold, mnemonic, password, derivation path etc. 36 | - [ ] allow aliases for keys (any string instead of a valid (x)pub) 37 | - [ ] add control panel with all nodes 38 | - [ ] add info-description for nodes 39 | - [ ] use sortedmulti instead of multi 40 | - [ ] build nodes from url with policy (`#/policy/andor(blah(blah))`) 41 | - [ ] build nodes from url with descriptor (`#/descriptor/wsh(andor(blah(blah)))`) - uplift? 42 | - [ ] simple miniscript wallet using block explorer 43 | - [ ] fetch balances 44 | - [ ] create psbt 45 | - [ ] finalize psbt 46 | - [ ] Specter-DIY QR support 47 | - [ ] allow xprv and WIF keys? 48 | - [ ] taproot and tapscript 49 | - [ ] wpkh, sh(wsh), sh(wpkh) 50 | -------------------------------------------------------------------------------- /examples/2fa.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": "bitcoin", 3 | "id": "demo@0.1.0", 4 | "nodes": { 5 | "2": { 6 | "id": 2, 7 | "data": { 8 | "mnemonic": "crash fatal hollow thank swallow submit tattoo portion code foam math force", 9 | "password": "qwe", 10 | "derivation": "m/48h/0h/0h/2h" 11 | }, 12 | "inputs": {}, 13 | "outputs": { 14 | "key": { 15 | "connections": [ 16 | { 17 | "node": 3, 18 | "input": "key", 19 | "data": {} 20 | } 21 | ] 22 | } 23 | }, 24 | "position": [ 25 | -152.8076377700496, 26 | -17.469585946171378 27 | ], 28 | "name": "BIP39" 29 | }, 30 | "3": { 31 | "id": 3, 32 | "data": {}, 33 | "inputs": { 34 | "key": { 35 | "connections": [ 36 | { 37 | "node": 2, 38 | "output": "key", 39 | "data": {} 40 | } 41 | ] 42 | } 43 | }, 44 | "outputs": { 45 | "key": { 46 | "connections": [ 47 | { 48 | "node": 11, 49 | "input": "pol1", 50 | "data": {} 51 | } 52 | ] 53 | } 54 | }, 55 | "position": [ 56 | 122.914765186986, 57 | 5.616892819342844 58 | ], 59 | "name": "Key" 60 | }, 61 | "6": { 62 | "id": 6, 63 | "data": {}, 64 | "inputs": { 65 | "pol": { 66 | "connections": [ 67 | { 68 | "node": 11, 69 | "output": "pol", 70 | "data": {} 71 | } 72 | ] 73 | } 74 | }, 75 | "outputs": { 76 | "desc": { 77 | "connections": [ 78 | { 79 | "node": 7, 80 | "input": "desc", 81 | "data": {} 82 | } 83 | ] 84 | } 85 | }, 86 | "position": [ 87 | 693.6821541648973, 88 | 53.389483440603115 89 | ], 90 | "name": "Descriptor" 91 | }, 92 | "7": { 93 | "id": 7, 94 | "data": { 95 | "idx": 0 96 | }, 97 | "inputs": { 98 | "desc": { 99 | "connections": [ 100 | { 101 | "node": 6, 102 | "output": "desc", 103 | "data": {} 104 | } 105 | ] 106 | } 107 | }, 108 | "outputs": { 109 | "addr": { 110 | "connections": [] 111 | } 112 | }, 113 | "position": [ 114 | 988.2460797658175, 115 | 20.323670883283746 116 | ], 117 | "name": "Address" 118 | }, 119 | "9": { 120 | "id": 9, 121 | "data": { 122 | "key": "xpub6DywUwVSjaQuHySfPtkSVc8XyjxyFzCJ9e2e3KQMf5QBycShzmB47q9Jtbf1uPQtFVLgpQ1dLwrR3UCBoEyNxEX2c2WzPEss1LZRp6o9DEV/0/*" 123 | }, 124 | "inputs": { 125 | "key": { 126 | "connections": [] 127 | } 128 | }, 129 | "outputs": { 130 | "key": { 131 | "connections": [ 132 | { 133 | "node": 10, 134 | "input": "pol1", 135 | "data": {} 136 | } 137 | ] 138 | } 139 | }, 140 | "position": [ 141 | -136.09896079872425, 142 | 334.11622465974835 143 | ], 144 | "name": "Key" 145 | }, 146 | "10": { 147 | "id": 10, 148 | "data": { 149 | "ratio": 99 150 | }, 151 | "inputs": { 152 | "pol1": { 153 | "connections": [ 154 | { 155 | "node": 9, 156 | "output": "key", 157 | "data": {} 158 | } 159 | ] 160 | }, 161 | "pol2": { 162 | "connections": [ 163 | { 164 | "node": 13, 165 | "output": "pol", 166 | "data": {} 167 | } 168 | ] 169 | } 170 | }, 171 | "outputs": { 172 | "pol": { 173 | "connections": [ 174 | { 175 | "node": 11, 176 | "input": "pol2", 177 | "data": {} 178 | } 179 | ] 180 | } 181 | }, 182 | "position": [ 183 | 140.62768701089715, 184 | 227.67396146156142 185 | ], 186 | "name": "Or" 187 | }, 188 | "11": { 189 | "id": 11, 190 | "data": {}, 191 | "inputs": { 192 | "pol1": { 193 | "connections": [ 194 | { 195 | "node": 3, 196 | "output": "key", 197 | "data": {} 198 | } 199 | ] 200 | }, 201 | "pol2": { 202 | "connections": [ 203 | { 204 | "node": 10, 205 | "output": "pol", 206 | "data": {} 207 | } 208 | ] 209 | } 210 | }, 211 | "outputs": { 212 | "pol": { 213 | "connections": [ 214 | { 215 | "node": 6, 216 | "input": "pol", 217 | "data": {} 218 | } 219 | ] 220 | } 221 | }, 222 | "position": [ 223 | 418.6244747244316, 224 | 63.23954774359686 225 | ], 226 | "name": "And" 227 | }, 228 | "13": { 229 | "id": 13, 230 | "data": { 231 | "num": 12960 232 | }, 233 | "inputs": {}, 234 | "outputs": { 235 | "pol": { 236 | "connections": [ 237 | { 238 | "node": 10, 239 | "input": "pol2", 240 | "data": {} 241 | } 242 | ] 243 | } 244 | }, 245 | "position": [ 246 | -139.81149767680319, 247 | 521.6599845978153 248 | ], 249 | "name": "Older" 250 | } 251 | } 252 | } -------------------------------------------------------------------------------- /examples/revault.json: -------------------------------------------------------------------------------- 1 | {"id":"demo@0.1.0","nodes":{"6":{"id":6,"data":{},"inputs":{"pol":{"connections":[{"node":8,"output":"pol","data":{}}]}},"outputs":{"desc":{"connections":[{"node":7,"input":"desc","data":{}}]}},"position":[827.2563990815245,279.64567139130827],"name":"Descriptor"},"7":{"id":7,"data":{"idx":0},"inputs":{"desc":{"connections":[{"node":6,"output":"desc","data":{}}]}},"outputs":{"addr":{"connections":[]}},"position":[1122.3006668714986,252.3892593730548],"name":"Address"},"8":{"id":8,"data":{"ratio":9},"inputs":{"pol1":{"connections":[{"node":10,"output":"pol","data":{}}]},"pol2":{"connections":[{"node":9,"output":"pol","data":{}}]}},"outputs":{"pol":{"connections":[{"node":6,"input":"pol","data":{}}]}},"position":[539.7785187461096,261.73422249533155],"name":"Or"},"9":{"id":9,"data":{"thresh":3},"inputs":{"policies":{"connections":[{"node":13,"output":"key","data":{}},{"node":14,"output":"key","data":{}},{"node":22,"output":"key","data":{}}]}},"outputs":{"pol":{"connections":[{"node":8,"input":"pol2","data":{}}]}},"position":[50.97798766840302,929.8555875494229],"name":"Threshold"},"10":{"id":10,"data":{},"inputs":{"pol1":{"connections":[{"node":11,"output":"pol","data":{}}]},"pol2":{"connections":[{"node":15,"output":"pol","data":{}}]}},"outputs":{"pol":{"connections":[{"node":8,"input":"pol1","data":{}}]}},"position":[244.13287322825937,163.76229566846416],"name":"And"},"11":{"id":11,"data":{"thresh":2},"inputs":{"policies":{"connections":[{"node":19,"output":"key","data":{}},{"node":20,"output":"key","data":{}},{"node":21,"output":"key","data":{}}]}},"outputs":{"pol":{"connections":[{"node":10,"input":"pol1","data":{}}]}},"position":[-88.88949924349397,-183.66528682247528],"name":"Threshold"},"12":{"id":12,"data":{"num":1290},"inputs":{},"outputs":{"pol":{"connections":[{"node":15,"input":"pol2","data":{}}]}},"position":[-459.3368488511535,595.6358175836106],"name":"Older"},"13":{"id":13,"data":{"key":"xpub6EvTWdPaWk4SMyt51Zwq5yvNKMBZr24hPDLFcArvd6W3iRuTQ2eZWcrJR7N7ctnHAcGdtDdD9ATDTrEaaTPwbGhhaSWyU97B64JhmaM5uu3/0/*"},"inputs":{"key":{"connections":[]}},"outputs":{"key":{"connections":[{"node":9,"input":"policies","data":{}}]}},"position":[-271.82122028608967,958.290584727851],"name":"Key"},"14":{"id":14,"data":{"key":"xpub6CWGuhPzd1cMoPupGHKHyG2MCVTnVARRXVns8LFp1ibc19ZAnWCLFNf35Xc9bEyiG4wUN8pBra4Yh3gxGdtQjB1MK68TPpocSDLUYAf4fnd/0/*"},"inputs":{"key":{"connections":[]}},"outputs":{"key":{"connections":[{"node":9,"input":"policies","data":{}}]}},"position":[-501.59028731749675,1089.3701482726492],"name":"Key"},"15":{"id":15,"data":{},"inputs":{"pol1":{"connections":[{"node":16,"output":"pol","data":{}}]},"pol2":{"connections":[{"node":12,"output":"pol","data":{}}]}},"outputs":{"pol":{"connections":[{"node":10,"input":"pol2","data":{}}]}},"position":[-108.57625211982548,402.6066832486713],"name":"And"},"16":{"id":16,"data":{"thresh":3},"inputs":{"policies":{"connections":[{"node":17,"output":"key","data":{}},{"node":18,"output":"key","data":{}},{"node":23,"output":"key","data":{}}]}},"outputs":{"pol":{"connections":[{"node":15,"input":"pol1","data":{}}]}},"position":[-866.597747841136,395.0611831712697],"name":"Threshold"},"17":{"id":17,"data":{"key":"xpub6FHmQQjpxgAQZb88QhEAZE5gANtyBjJjHRT1bi5m4RAVdtB2zYgiX6E7cyxCK9ZBB3m5VWe6Lc2EWZztXe7b5ZA5UbZccDm9qidU1n55yiy"},"inputs":{"key":{"connections":[]}},"outputs":{"key":{"connections":[{"node":16,"input":"policies","data":{}}]}},"position":[-1182.1877215662512,303.964919063744],"name":"Key"},"18":{"id":18,"data":{"key":"xpub6EehKwrhACGqgkrPyPRZ3zxMoreVUkFaYckbCwP3s6gMgTBuDjQLQs1XarhTfV1hTjvJfdZXXCGX7qHUM7Rz6o36w12uEUUg1hSPgVRzTAr"},"inputs":{"key":{"connections":[]}},"outputs":{"key":{"connections":[{"node":16,"input":"policies","data":{}}]}},"position":[-1209.6072719759593,594.9103154902438],"name":"Key"},"19":{"id":19,"data":{"key":"xpub6EK3QXDHrj3HeXedaYxZnKHEdtmY8ccnYk6WHco7U9Ru9xBLP6Ug5FdtjwqigAxeYNVWe14LGmkGQ9nwCqiUgnZ4Rt8ato6mvQyNRMhv4Wi/0/*"},"inputs":{"key":{"connections":[]}},"outputs":{"key":{"connections":[{"node":11,"input":"policies","data":{}}]}},"position":[-438.19899670805273,-51.67724682662989],"name":"Key"},"20":{"id":20,"data":{"key":"xpub6CPE9XefeSBxNG2vmXXJ247sJWWixmBdz15E2PGweKnzuZaWfrGQsQLuM7t4wM9fLw871afg3iWNPBm4vQPC8efSMbqQFvT1KMrxeQ1kHAU/0/*"},"inputs":{"key":{"connections":[]}},"outputs":{"key":{"connections":[{"node":11,"input":"policies","data":{}}]}},"position":[-386.2967196437753,-293.0470047032918],"name":"Key"},"21":{"id":21,"data":{"key":"xpub6DVebFbt5rtyYb8Xj4twxjivGZpw5Z51pdyefftKGrGHqr6RfoZkLY9HJkKgx5cBToQAqY3V7vWatKhZ9rxRyhzx3Tj9866bsUoxCn59c7A"},"inputs":{"key":{"connections":[]}},"outputs":{"key":{"connections":[{"node":11,"input":"policies","data":{}}]}},"position":[-642.246150860148,-200.79213393240119],"name":"Key"},"22":{"id":22,"data":{"key":"xpub6DLb8bF7d1M1BZffkWjL5CTETTyrGRHvb8EVWxZmpx2T9T9KkufAiBpB8ZzfP4v59y7V6Ly72Ep8zh2k6jy73TJPcJWEqLHXr5AYhscwq72/0/*"},"inputs":{"key":{"connections":[]}},"outputs":{"key":{"connections":[{"node":9,"input":"policies","data":{}}]}},"position":[-291.09865062496556,1172.2041892146071],"name":"Key"},"23":{"id":23,"data":{"key":"xpub6Bo96rk3NyBGhUvNkbD173RZJx4hLKpRqfM8TpbdfyvPxQYEramZCPnvGwZ1jtqDBjwPnDbATxNNSy3N5tabSayTbrAZcE4mHNrqpZz179v"},"inputs":{"key":{"connections":[]}},"outputs":{"key":{"connections":[{"node":16,"input":"policies","data":{}}]}},"position":[-1423.943010648504,423.75142363071006],"name":"Key"}},"comments":[{"text":"All stakeholders must sign","position":[-531.5902873174967,895.0763256877427],"links":[22,14,9,13],"type":"frame","width":832.5682749858997,"height":522.1278635268644},{"text":"All cosigners must sign","position":[-1453.943010648504,273.964919063744],"links":[17,16,18,23],"type":"frame","width":837.3452628073679,"height":565.9453964264998},{"text":"At least 2 managers must sign","position":[-672.246150860148,-354.5131448465522],"links":[11,19,21,20],"type":"frame","width":833.3566516166541,"height":547.8358980199223}]} -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Miniscript builder 7 | 8 | 9 | 10 | 11 |
12 |
Right-click to add or delete nodes.
For frame comment select nodes and press Shifh+F.
13 | 21 |
22 |
23 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /rust/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | pkg -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "compiler" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | 9 | [dependencies] 10 | wasm-bindgen = { version = "0.2.78" } 11 | # miniscript = {version = "9.0.1", features=["compiler"]} 12 | miniscript = {git = "https://github.com/rust-bitcoin/rust-miniscript.git", rev = "4a3ba11c2fd5063be960741d557f3f7a28041e1f", features=["compiler"]} 13 | bip39 = "1.0.1" 14 | -------------------------------------------------------------------------------- /rust/README.md: -------------------------------------------------------------------------------- 1 | # Wasm Rust miniscript functions 2 | 3 | ## build 4 | 5 | ```sh 6 | cargo build 7 | wasm-pack build --target web 8 | ``` 9 | -------------------------------------------------------------------------------- /rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use std::str::FromStr; 3 | 4 | use miniscript::bitcoin::{self, secp256k1}; 5 | use miniscript::bitcoin::util::bip32::{ 6 | ExtendedPrivKey, ExtendedPubKey, DerivationPath 7 | }; 8 | use miniscript::policy::Concrete; 9 | use miniscript::{Descriptor, DescriptorPublicKey}; 10 | 11 | use bip39::Mnemonic; 12 | 13 | // https://github.com/rustwasm/wasm-bindgen/issues/1742#issuecomment-643793491 14 | macro_rules! jserr { 15 | ($expression:expr) => { 16 | match $expression { 17 | Ok(a) => a, 18 | Err(e) => { 19 | return Err(JsValue::from(format!("{}", e))); 20 | } 21 | } 22 | }; 23 | } 24 | 25 | #[wasm_bindgen] 26 | pub fn compile(policy: &str) -> Result{ 27 | let policy = jserr!(Concrete::::from_str(policy)); 28 | let minisc = jserr!(Descriptor::new_wsh(jserr!(policy.compile()))); 29 | Ok(minisc.to_string().into()) 30 | } 31 | 32 | #[wasm_bindgen] 33 | pub fn address(desc: &str, idx: u32, network: &str) -> Result{ 34 | let network = jserr!(network.parse::()); 35 | if idx >= 0x80000000 { 36 | return Err(JsValue::from("Invalid index")); 37 | } 38 | let secp_ctx = secp256k1::Secp256k1::verification_only(); 39 | let desc = jserr!(Descriptor::::from_str(desc)); 40 | let desc = jserr!(desc.at_derivation_index(idx)); 41 | let desc = jserr!(desc.derived_descriptor(&secp_ctx)); 42 | let addr = jserr!(desc.address(network)); 43 | Ok(addr.to_string().into()) 44 | } 45 | 46 | #[wasm_bindgen] 47 | pub fn bip39_root(mnemonic: &str, password: &str, network: &str) -> Result { 48 | let network = jserr!(network.parse::()); 49 | let mnemonic = jserr!(Mnemonic::parse(mnemonic)); 50 | let seed = mnemonic.to_seed(password); 51 | 52 | // generate root bip-32 key from seed 53 | let root = jserr!(ExtendedPrivKey::new_master(network, &seed)); 54 | 55 | let secp_ctx = secp256k1::Secp256k1::new(); 56 | let fingerprint = root.fingerprint(&secp_ctx); 57 | Ok(format!("[{}]{}", fingerprint, root).into()) 58 | } 59 | 60 | #[wasm_bindgen] 61 | pub fn bip39_derive(mnemonic: &str, password: &str, path: &str, network: &str) -> Result { 62 | let network = jserr!(network.parse::()); 63 | let mnemonic = jserr!(Mnemonic::parse(mnemonic)); 64 | let seed = mnemonic.to_seed(password); 65 | let derivation = jserr!(DerivationPath::from_str(path)); 66 | 67 | // generate root bip-32 key from seed 68 | let secp_ctx = secp256k1::Secp256k1::new(); 69 | let root = jserr!(ExtendedPrivKey::new_master(network, &seed)); 70 | let fingerprint = root.fingerprint(&secp_ctx); 71 | 72 | let child = jserr!(root.derive_priv(&secp_ctx, &derivation)); 73 | let xpub = ExtendedPubKey::from_priv(&secp_ctx, &child); 74 | let key = format!("[{}{}]{}", fingerprint, &path[1..], xpub); 75 | 76 | Ok(key.into()) 77 | } 78 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stepansnigirev/miniscript-builder/a13777cd4390f4d5d8d8738f41df81b00268308b/screenshot.png -------------------------------------------------------------------------------- /www/app.js: -------------------------------------------------------------------------------- 1 | var numSocket = new Rete.Socket('Number'); 2 | var policySocket = new Rete.Socket('Policy'); 3 | var keySocket = new Rete.Socket('Key'); 4 | var descriptorSocket = new Rete.Socket('Descriptor'); 5 | var stringSocket = new Rete.Socket('String'); 6 | 7 | // helper function 8 | function sleep(ms) { 9 | return new Promise(resolve => setTimeout(resolve, ms)); 10 | } 11 | 12 | var network = "bitcoin"; 13 | 14 | /*************************** TEMPLATES *************************/ 15 | 16 | // TODO: lots of copy-paste, figure out how to simplify 17 | var VueNumControl = { 18 | props: ['readonly', 'emitter', 'ikey', 'getData', 'putData', 'label'], 19 | template: `
{{label}}
`, 20 | data() { 21 | return { 22 | value: 1, 23 | label: "Number", 24 | } 25 | }, 26 | methods: { 27 | change(e){ 28 | this.value = +e.target.value; 29 | this.update(); 30 | }, 31 | update() { 32 | if (this.ikey) 33 | this.putData(this.ikey, this.value) 34 | this.emitter.trigger('process'); 35 | } 36 | }, 37 | mounted() { 38 | this.value = this.getData(this.ikey); 39 | } 40 | } 41 | 42 | // TODO: ugly, make label dynamic and styled in css 43 | var VueRatioControl = { 44 | props: ['readonly', 'emitter', 'ikey', 'getData', 'putData', 'label'], 45 | template: `
{{label}}
`, 46 | data() { 47 | return { 48 | value: 1, 49 | label: "Ratio:", 50 | } 51 | }, 52 | methods: { 53 | change(e){ 54 | this.value = +e.target.value; 55 | if(this.value <= 0){ 56 | this.value = 1; 57 | } 58 | this.update(); 59 | }, 60 | update() { 61 | if (this.ikey) 62 | this.putData(this.ikey, this.value) 63 | this.emitter.trigger('process'); 64 | } 65 | }, 66 | mounted() { 67 | this.value = this.getData(this.ikey); 68 | if(this.value <= 0){ 69 | this.value = 1; 70 | } 71 | } 72 | } 73 | 74 | var VueStringControl = { 75 | props: ['readonly', 'emitter', 'ikey', 'getData', 'putData', 'label'], 76 | template: `
{{label}}
`, 77 | data() { 78 | return { 79 | value: "", 80 | label: "", 81 | } 82 | }, 83 | methods: { 84 | change(e){ 85 | this.value = e.target.value; 86 | this.update(); 87 | }, 88 | update() { 89 | if (this.ikey) 90 | this.putData(this.ikey, this.value) 91 | this.emitter.trigger('process'); 92 | } 93 | }, 94 | mounted() { 95 | this.value = this.getData(this.ikey); 96 | } 97 | } 98 | 99 | var VuePreviewControl = { 100 | props: ['readonly', 'emitter', 'ikey', 'getData', 'putData'], 101 | template: `
{{value}}
`, 102 | data() { 103 | return { 104 | value: "", 105 | } 106 | }, 107 | methods: { 108 | change(e){ 109 | this.value = e.target.value; 110 | this.update(); 111 | }, 112 | update() { 113 | if (this.ikey) 114 | this.putData(this.ikey, this.value) 115 | this.emitter.trigger('process'); 116 | } 117 | }, 118 | mounted() { 119 | this.value = this.getData(this.ikey); 120 | } 121 | } 122 | 123 | /*************************** CONTROLS *************************/ 124 | 125 | // TODO: lots of copy-paste, figure out how to simplify 126 | class NumControl extends Rete.Control { 127 | 128 | constructor(emitter, key, readonly, label="Number") { 129 | super(key); 130 | this.component = VueNumControl; 131 | this.props = { emitter, ikey: key, readonly, label }; 132 | } 133 | 134 | setValue(val) { 135 | this.vueContext.value = val; 136 | } 137 | } 138 | 139 | class RatioControl extends Rete.Control { 140 | 141 | constructor(emitter, key, readonly, label="Probability ratio:") { 142 | super(key); 143 | this.component = VueRatioControl; 144 | this.props = { emitter, ikey: key, readonly, label }; 145 | } 146 | 147 | setValue(val) { 148 | this.vueContext.value = val; 149 | } 150 | } 151 | 152 | class StringControl extends Rete.Control { 153 | 154 | constructor(emitter, key, readonly, label="") { 155 | super(key); 156 | this.component = VueStringControl; 157 | this.props = { emitter, ikey: key, readonly, label }; 158 | } 159 | 160 | setValue(val) { 161 | this.vueContext.value = val; 162 | } 163 | } 164 | 165 | class PreviewControl extends Rete.Control { 166 | 167 | constructor(emitter, key, readonly) { 168 | super(key); 169 | this.component = VuePreviewControl; 170 | this.props = { emitter, ikey: key, readonly }; 171 | } 172 | 173 | setValue(val) { 174 | this.vueContext.value = val; 175 | } 176 | } 177 | 178 | /*************************** POLICY COMPONENTS *************************/ 179 | 180 | class BIP39Component extends Rete.Component { 181 | 182 | constructor(){ 183 | super("BIP39"); 184 | } 185 | 186 | builder(node) { 187 | var out = new Rete.Output('key', "Key", keySocket); 188 | return node 189 | .addControl(new PreviewControl(this.editor, 'preview', true)) 190 | .addControl(new StringControl(this.editor, 'mnemonic', false, "Recovery phrase:")) 191 | .addControl(new StringControl(this.editor, 'password', false, "Password:")) 192 | .addControl(new StringControl(this.editor, 'derivation', false, "Derivation path:")) 193 | .addOutput(out); 194 | } 195 | 196 | worker(node, inputs, outputs) { 197 | let out = ''; 198 | try{ 199 | let mnemonic = (node.data.mnemonic) ? node.data.mnemonic : ''; 200 | let password = (node.data.password) ? node.data.password : ''; 201 | let derivation = (node.data.derivation) ? node.data.derivation : 'm'; 202 | out = miniscript.bip39_derive(mnemonic, password, derivation, network) + "/0/*"; 203 | }catch (e){ 204 | out = `${e}`; 205 | console.error(e); 206 | } 207 | this.editor.nodes.find(n => n.id == node.id).controls.get('preview').setValue(out); 208 | outputs['key'] = out; 209 | } 210 | } 211 | 212 | class KeyComponent extends Rete.Component { 213 | 214 | constructor(){ 215 | super("Key"); 216 | } 217 | 218 | builder(node) { 219 | var out = new Rete.Output('key', "Policy", policySocket); 220 | var inp = new Rete.Input("key", "Key", keySocket); 221 | inp.addControl(new StringControl(this.editor, 'key', false, "[Extended] Public Key:")); 222 | return node 223 | .addControl(new PreviewControl(this.editor, 'preview', true)) 224 | .addInput(inp) 225 | .addOutput(out); 226 | } 227 | 228 | worker(node, inputs, outputs) { 229 | let k = inputs['key'].length ? inputs['key'][0] : node.data.key; 230 | k = k ? k : ''; 231 | let val = `pk(${k})`; 232 | this.editor.nodes.find(n => n.id == node.id).controls.get('preview').setValue(val); 233 | outputs['key'] = val; 234 | } 235 | } 236 | 237 | class AndComponent extends Rete.Component { 238 | constructor(){ 239 | super("And"); 240 | } 241 | 242 | builder(node) { 243 | var inp1 = new Rete.Input('pol1',"Policy", policySocket); 244 | var inp2 = new Rete.Input('pol2', "Policy", policySocket); 245 | var out = new Rete.Output('pol', "Policy", policySocket); 246 | 247 | inp1.addControl(new StringControl(this.editor, 'pol1')) 248 | inp2.addControl(new StringControl(this.editor, 'pol2')) 249 | 250 | return node 251 | .addInput(inp1) 252 | .addInput(inp2) 253 | .addControl(new PreviewControl(this.editor, 'preview', true)) 254 | .addOutput(out); 255 | } 256 | 257 | worker(node, inputs, outputs) { 258 | var p1 = inputs['pol1'].length?inputs['pol1'][0]:node.data.pol1; 259 | var p2 = inputs['pol2'].length?inputs['pol2'][0]:node.data.pol2; 260 | 261 | var pol = `and(${p1 ? p1 : ''},${p2 ? p2 : ''})`; 262 | this.editor.nodes.find(n => n.id == node.id).controls.get('preview').setValue(pol); 263 | outputs['pol'] = pol; 264 | } 265 | } 266 | 267 | // TODO: add probabilities 268 | class OrComponent extends Rete.Component { 269 | constructor(){ 270 | super("Or"); 271 | } 272 | 273 | builder(node) { 274 | var inp1 = new Rete.Input('pol1',"Policy", policySocket); 275 | var inp2 = new Rete.Input('pol2', "Policy", policySocket); 276 | var out = new Rete.Output('pol', "Policy", policySocket); 277 | 278 | inp1.addControl(new StringControl(this.editor, 'pol1')) 279 | inp2.addControl(new StringControl(this.editor, 'pol2')) 280 | 281 | return node 282 | .addInput(inp1) 283 | .addInput(inp2) 284 | .addControl(new PreviewControl(this.editor, 'preview', true)) 285 | .addControl(new RatioControl(this.editor, "ratio")) 286 | .addOutput(out); 287 | } 288 | 289 | worker(node, inputs, outputs) { 290 | var ratio = (node.data.ratio) ? node.data.ratio : 1; 291 | let r1 = Math.round(ratio*100); 292 | let r2 = 100; 293 | // mutual reduce 294 | while(r1%10 == 0 && r2%10 == 0){ 295 | r1 = Math.round(r1/10); 296 | r2 = Math.round(r2/10); 297 | } 298 | var p1 = inputs['pol1'].length?inputs['pol1'][0]:node.data.pol1; 299 | var p2 = inputs['pol2'].length?inputs['pol2'][0]:node.data.pol2; 300 | // handle undefined 301 | p1 = (p1) ? p1 : ''; 302 | p2 = (p2) ? p2 : ''; 303 | if(r1 != r2){ 304 | if(r1 != 1){ 305 | p1 = `${r1}@${p1}`; 306 | } 307 | if(r2 != 1){ 308 | p2 = `${r2}@${p2}`; 309 | } 310 | } 311 | var pol = `or(${p1},${p2})`; 312 | this.editor.nodes.find(n => n.id == node.id).controls.get('preview').setValue(pol); 313 | outputs['pol'] = pol; 314 | } 315 | } 316 | 317 | class TimeComponent extends Rete.Component { 318 | constructor(name="After"){ 319 | super(name); 320 | this._op = name.toLowerCase(); 321 | this._label = (this._op == "after") ? "Blockheight:" : "Blocks from prev tx:"; 322 | } 323 | 324 | builder(node) { 325 | // var inp = new Rete.Input('num',"Number", numSocket); 326 | var out = new Rete.Output('pol', "Policy", policySocket); 327 | 328 | return node 329 | .addControl(new PreviewControl(this.editor, 'preview', true)) 330 | .addControl(new NumControl(this.editor, 'num', false, this._label)) 331 | .addOutput(out); 332 | } 333 | 334 | worker(node, inputs, outputs) { 335 | var n = node.data.num; 336 | n = (n==undefined) ? 0 : n; 337 | var pol = `${this._op}(${n})`; 338 | 339 | this.editor.nodes.find(n => n.id == node.id).controls.get('preview').setValue(pol); 340 | outputs['pol'] = pol; 341 | } 342 | } 343 | 344 | class HashComponent extends Rete.Component { 345 | constructor(name="SHA256"){ 346 | super(name); 347 | this._op = name.toLowerCase(); 348 | this._len = this._op.search("256") >= 0 ? 32 : 20 349 | } 350 | 351 | builder(node) { 352 | // var inp = new Rete.Input('num',"Number", numSocket); 353 | var out = new Rete.Output('pol', "Policy", policySocket); 354 | 355 | return node 356 | .addControl(new PreviewControl(this.editor, 'preview', true)) 357 | .addControl(new StringControl(this.editor, 'hash', false, `${this._len}-byte hash in hex`)) 358 | .addOutput(out); 359 | } 360 | 361 | worker(node, inputs, outputs) { 362 | var n = node.data.hash; 363 | n = (n==undefined) ? '' : n; 364 | var pol = `${this._op}(${n})`; 365 | 366 | this.editor.nodes.find(n => n.id == node.id).controls.get('preview').setValue(pol); 367 | outputs['pol'] = pol; 368 | } 369 | } 370 | 371 | class ThreshComponent extends Rete.Component { 372 | constructor(){ 373 | super("Threshold"); 374 | } 375 | 376 | builder(node) { 377 | let pol = new Rete.Input(`policies`, "Policies", policySocket, true); 378 | node.addInput(pol); 379 | 380 | var out = new Rete.Output('pol', "Policy", policySocket); 381 | 382 | return node 383 | .addControl(new PreviewControl(this.editor, 'preview', true)) 384 | .addControl(new NumControl(this.editor, 'thresh', false, "Threshold:")) 385 | .addOutput(out); 386 | } 387 | 388 | worker(node, inputs, outputs) { 389 | let n = node.data.thresh; 390 | n = (n==undefined) ? 1 : n; 391 | let nodeobj = this.editor.nodes.find(n => n.id == node.id); 392 | let args = ""; 393 | // sort alphabetically, bad but at least deterministic 394 | inputs['policies'].sort().forEach((inpol)=>{ 395 | args += `,${inpol}` 396 | }) 397 | let pol = `thresh(${n}${args})`; 398 | nodeobj.controls.get('preview').setValue(pol); 399 | outputs['pol'] = pol; 400 | nodeobj.update(); 401 | } 402 | } 403 | 404 | /*************************** DESCRIPTOR *************************/ 405 | 406 | class DescriptorComponent extends Rete.Component { 407 | constructor(){ 408 | super("Descriptor"); 409 | } 410 | 411 | builder(node) { 412 | var inp = new Rete.Input('pol',"Policy", policySocket); 413 | var out = new Rete.Output('desc', "Descriptor", descriptorSocket); 414 | 415 | inp.addControl(new StringControl(this.editor, 'pol')) 416 | 417 | return node 418 | .addInput(inp) 419 | .addControl(new PreviewControl(this.editor, 'preview', true)) 420 | .addOutput(out); 421 | } 422 | 423 | worker(node, inputs, outputs) { 424 | let pol = inputs['pol'].length ? inputs['pol'][0] : node.data.pol; 425 | let desc = ''; 426 | try{ 427 | desc = miniscript.compile(pol); 428 | }catch(e){ 429 | desc = `Error: ${e}`; 430 | } 431 | this.editor.nodes.find(n => n.id == node.id).controls.get('preview').setValue(desc); 432 | outputs['desc'] = desc; 433 | } 434 | } 435 | 436 | class AddressComponent extends Rete.Component { 437 | constructor(){ 438 | super("Address"); 439 | } 440 | 441 | builder(node) { 442 | var inp = new Rete.Input('desc',"Descriptor", descriptorSocket); 443 | var out = new Rete.Output('addr', "Address", stringSocket); 444 | 445 | inp.addControl(new StringControl(this.editor, 'desc', false, "Descriptor:")) 446 | 447 | return node 448 | .addInput(inp) 449 | .addControl(new PreviewControl(this.editor, 'preview', true)) 450 | .addControl(new NumControl(this.editor, 'idx', false, "Index:")) 451 | .addOutput(out); 452 | } 453 | 454 | worker(node, inputs, outputs) { 455 | let desc = inputs['desc'].length ? inputs['desc'][0] : node.data.desc; 456 | let idx = node.data.idx; 457 | let addr = ''; 458 | try{ 459 | addr = miniscript.address(desc, idx, network); 460 | }catch(e){ 461 | addr = `Error: ${e}`; 462 | } 463 | this.editor.nodes.find(n => n.id == node.id).controls.get('preview').setValue(addr); 464 | outputs['addr'] = addr; 465 | } 466 | } 467 | 468 | function toBase64(obj){ 469 | // ugly hack: inject network here 470 | obj.network = network; 471 | return btoa(JSON.stringify(obj)).replace(/\//g, '_').replace(/\+/g, '-'); 472 | } 473 | 474 | function fromBase64(b64){ 475 | let obj = JSON.parse(atob(b64.replace(/_/g, '/').replace(/-/g, '+'))); 476 | return obj; 477 | } 478 | 479 | async function loadObj(obj){ 480 | // get network from obj 481 | let net = obj.network; 482 | if(net != undefined){ 483 | document.getElementById("network").value = net; 484 | network = net; 485 | } 486 | await editor.fromJSON(obj); 487 | editor.view.resize(); 488 | AreaPlugin.zoomAt(editor); 489 | editor.trigger("process"); 490 | await sleep(300); 491 | editor.trigger("process"); 492 | // select descriptor by default 493 | let d = editor.nodes.find(n => n.name == "Descriptor"); 494 | if(d){ 495 | editor.selectNode(d); 496 | } 497 | } 498 | 499 | async function loadHash(){ 500 | try{ 501 | let obj = {}; 502 | if(window.location.hash.startsWith("#/full/")){ 503 | obj = fromBase64(window.location.hash.substr("#/full/".length)) 504 | }else if(window.location.hash.startsWith("#/url/")){ 505 | let url = window.location.hash.substr("#/url/".length); 506 | let res = await fetch(url); 507 | obj = await res.json(); 508 | }else{ 509 | throw "Can't parse hash"; 510 | } 511 | await loadObj(obj); 512 | }catch(e){ 513 | console.error(`Error: ${e}`) 514 | } 515 | } 516 | 517 | function exportJSON(el){ 518 | let obj = editor.toJSON(); 519 | let data = "text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(obj)); 520 | 521 | el.setAttribute("href", "data:"+data); 522 | el.setAttribute("download", "data.json"); 523 | } 524 | document.getElementById("jsonfile").addEventListener("change", async (e) => { 525 | files = e.currentTarget.files; 526 | for(let i=0; i<1; i++){ 527 | let reader = new FileReader(); 528 | reader.onload = async function(e) { 529 | let obj = JSON.parse(reader.result) 530 | await loadObj(obj); 531 | } 532 | reader.readAsText(files[i]); 533 | } 534 | }); 535 | 536 | function arrangeNodes(){ 537 | editor.arrange(); 538 | editor.view.resize(); 539 | AreaPlugin.zoomAt(editor); 540 | } 541 | 542 | /*************************** APP MAIN *************************/ 543 | 544 | function displayOut(node){ 545 | if(!node || !engine){ 546 | return; 547 | } 548 | try{ 549 | if(!engine.data){ 550 | return; 551 | } 552 | let d = engine.data.nodes[node.id].outputData; 553 | let res = Object.values(d)[0]; 554 | document.getElementById("node-name").innerText = node.name; 555 | document.getElementById("node-output").innerText = res; 556 | }catch(e){ 557 | console.error(e); 558 | } 559 | } 560 | 561 | async function updateNetwork(){ 562 | network = document.getElementById("network").value; 563 | editor.trigger('process'); 564 | } 565 | 566 | window.app_init = async ()=>{ 567 | var container = document.querySelector('#rete'); 568 | let AndC = new AndComponent(); 569 | let AfterC = new TimeComponent("After"); 570 | let OlderC = new TimeComponent("Older"); 571 | let ThreshC = new ThreshComponent(); 572 | let KeyC = new KeyComponent(); 573 | let OrC = new OrComponent(); 574 | let DescC = new DescriptorComponent(); 575 | let AddressC = new AddressComponent(); 576 | let Sha256_C = new HashComponent("SHA256"); 577 | let Ripemd160_C = new HashComponent("Ripemd160"); 578 | let Hash256_C = new HashComponent("Hash256"); 579 | let Hash160_C = new HashComponent("Hash160"); 580 | let BIP39_C = new BIP39Component(); 581 | var components = [ 582 | BIP39_C, 583 | KeyC, 584 | AfterC, 585 | OlderC, 586 | ThreshC, 587 | AndC, 588 | OrC, 589 | Sha256_C, 590 | Ripemd160_C, 591 | Hash256_C, 592 | Hash160_C, 593 | DescC, 594 | AddressC, 595 | ]; 596 | 597 | window.editor = new Rete.NodeEditor('demo@0.1.0', container); 598 | editor.use(ConnectionPlugin.default); 599 | editor.use(VueRenderPlugin.default); 600 | editor.use(ContextMenuPlugin.default, { 601 | searchBar: false, 602 | }); 603 | editor.use(AreaPlugin); 604 | editor.use(CommentPlugin.default); 605 | editor.use(HistoryPlugin); 606 | // editor.use(DockPlugin.default,{ 607 | // container: document.querySelector('#dock'), 608 | // // itemClass: 'item' // default: dock-item 609 | // plugins: [VueRenderPlugin.default], // render plugins 610 | // }); 611 | editor.use(ConnectionMasteryPlugin.default); 612 | editor.use(AutoArrangePlugin.default); //, { margin: {x: 50, y: 50 }, depth: 0 }); // depth - max depth for arrange (0 - unlimited) 613 | 614 | window.engine = new Rete.Engine('demo@0.1.0'); 615 | 616 | components.map(c => { 617 | editor.register(c); 618 | engine.register(c); 619 | }); 620 | 621 | if(window.location.hash.length > 5){ 622 | await loadHash(); 623 | }else{ 624 | let p1 = await OlderC.createNode({num: 12960}); 625 | let mn = await BIP39_C.createNode({ 626 | mnemonic: "crash fatal hollow thank swallow submit tattoo portion code foam math force", 627 | password:"", 628 | derivation:"m/48h/0h/0h/2h", 629 | }); 630 | let p2 = await KeyC.createNode(); 631 | let p3 = await KeyC.createNode({key: "038b4059419fe3b95acdee6aff2f9afdca87231d14bd2cbcd3367b11d9d819a71d"}); 632 | let thresh = await ThreshC.createNode({thresh: 2}); 633 | let desc = await DescC.createNode(); 634 | let addr = await AddressC.createNode({idx: 0}); 635 | 636 | p1.position = [80, 200]; 637 | mn.position = [-200, 400]; 638 | p2.position = [80, 400]; 639 | p3.position = [80, 600]; 640 | thresh.position = [500, 240]; 641 | desc.position = [800, 240]; 642 | addr.position = [1100, 240]; 643 | 644 | editor.addNode(p1); 645 | editor.addNode(mn); 646 | editor.addNode(p2); 647 | editor.addNode(p3); 648 | editor.addNode(thresh); 649 | editor.addNode(desc); 650 | editor.addNode(addr); 651 | 652 | editor.connect(mn.outputs.get('key'), p2.inputs.get('key')); 653 | editor.connect(p1.outputs.get('pol'), thresh.inputs.get('policies')); 654 | editor.connect(thresh.outputs.get('pol'), desc.inputs.get('pol')); 655 | editor.connect(p2.outputs.get('key'), thresh.inputs.get('policies')); 656 | editor.connect(p3.outputs.get('key'), thresh.inputs.get('policies')); 657 | editor.connect(desc.outputs.get('desc'), addr.inputs.get('desc')); 658 | 659 | editor.view.resize(); 660 | AreaPlugin.zoomAt(editor); 661 | } 662 | 663 | editor.on('process nodecreated noderemoved connectioncreated connectionremoved', async () => { 664 | let obj = editor.toJSON(); 665 | let h = toBase64(obj); 666 | window.location.hash = "/full/"+h; 667 | await engine.abort(); 668 | await engine.process(obj); 669 | if(editor.selected.list.length){ 670 | displayOut(editor.selected.list[0]); 671 | } 672 | }); 673 | 674 | editor.on('nodetranslated', async () => { 675 | let obj = editor.toJSON(); 676 | let h = toBase64(obj); 677 | window.location.hash = "/full/"+h; 678 | }); 679 | 680 | editor.on('nodeselected', async (node) => { 681 | displayOut(editor.selected.list[0]); 682 | }); 683 | 684 | editor.trigger('process'); 685 | 686 | // on hash change 687 | window.addEventListener("hashchange", async ()=>{ 688 | let obj = editor.toJSON(); 689 | let h = toBase64(obj); 690 | let prevhash = "#/full/"+h; 691 | // external hash change 692 | if(window.location.hash != prevhash){ 693 | loadHash(); 694 | } 695 | }, false); 696 | 697 | // select descriptor by default 698 | let d = editor.nodes.find(n => n.name == "Descriptor"); 699 | if(d){ 700 | editor.selectNode(d); 701 | } 702 | }; -------------------------------------------------------------------------------- /www/js/area-plugin.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * rete-area-plugin v0.2.1 3 | * (c) 2019 4 | * Released under the ISC license. 5 | */ 6 | !function(t){"use strict";var u,e=Object.prototype,s=e.hasOwnProperty,n="function"==typeof Symbol?Symbol:{},o=n.iterator||"@@iterator",r=n.asyncIterator||"@@asyncIterator",i=n.toStringTag||"@@toStringTag",a="object"==typeof module,c=t.regeneratorRuntime;if(c)a&&(module.exports=c);else{(c=t.regeneratorRuntime=a?module.exports:{}).wrap=w;var l="suspendedStart",f="suspendedYield",p="executing",d="completed",y={},h={};h[o]=function(){return this};var v=Object.getPrototypeOf,g=v&&v(v(A([])));g&&g!==e&&s.call(g,o)&&(h=g);var m=L.prototype=b.prototype=Object.create(h);E.prototype=m.constructor=L,L.constructor=E,L[i]=E.displayName="GeneratorFunction",c.isGeneratorFunction=function(t){var e="function"==typeof t&&t.constructor;return!!e&&(e===E||"GeneratorFunction"===(e.displayName||e.name))},c.mark=function(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,L):(t.__proto__=L,i in t||(t[i]="GeneratorFunction")),t.prototype=Object.create(m),t},c.awrap=function(t){return{__await:t}},k(_.prototype),_.prototype[r]=function(){return this},c.AsyncIterator=_,c.async=function(t,e,n,r){var o=new _(w(t,e,n,r));return c.isGeneratorFunction(e)?o:o.next().then(function(t){return t.done?t.value:o.next()})},k(m),m[i]="Generator",m[o]=function(){return this},m.toString=function(){return"[object Generator]"},c.keys=function(n){var r=[];for(var t in n)r.push(t);return r.reverse(),function t(){for(;r.length;){var e=r.pop();if(e in n)return t.value=e,t.done=!1,t}return t.done=!0,t}},c.values=A,z.prototype={constructor:z,reset:function(t){if(this.prev=0,this.next=0,this.sent=this._sent=u,this.done=!1,this.delegate=null,this.method="next",this.arg=u,this.tryEntries.forEach(T),!t)for(var e in this)"t"===e.charAt(0)&&s.call(this,e)&&!isNaN(+e.slice(1))&&(this[e]=u)},stop:function(){this.done=!0;var t=this.tryEntries[0].completion;if("throw"===t.type)throw t.arg;return this.rval},dispatchException:function(n){if(this.done)throw n;var r=this;function t(t,e){return i.type="throw",i.arg=n,r.next=t,e&&(r.method="next",r.arg=u),!!e}for(var e=this.tryEntries.length-1;0<=e;--e){var o=this.tryEntries[e],i=o.completion;if("root"===o.tryLoc)return t("end");if(o.tryLoc<=this.prev){var a=s.call(o,"catchLoc"),c=s.call(o,"finallyLoc");if(a&&c){if(this.preve.max&&(t.zoom=e.max)}},{key:"restrictTranslate",value:function(t){var e="boolean"==typeof this.translateExtent?{width:5e3,height:4e3}:this.translateExtent,n=this.editor.view.container,r=t.transform.k,o=e.width*r,i=e.height*r,a=n.clientWidth/2,c=n.clientHeight/2;t.x-=a,t.y-=c,t.x>o?t.x=o:t.x<-o&&(t.x=-o),t.y>i?t.y=i:t.y<-i&&(t.y=-i),t.x+=a,t.y+=c}}]),r}(),s=function(){function c(t,e){var r=this,n=e.size,o=void 0===n?16:n,i=e.dynamic,a=void 0===i||i;u(this,c),this.editor=t,this.size=o,a?this.editor.on("nodetranslate",this.onTranslate.bind(this)):this.editor.on("rendernode",function(t){var e=t.node,n=t.el;n.addEventListener("mouseup",r.onDrag.bind(r,e)),n.addEventListener("touchend",r.onDrag.bind(r,e)),n.addEventListener("touchcancel",r.onDrag.bind(r,e))})}return t(c,[{key:"onTranslate",value:function(t){var e=t.x,n=t.y;t.x=this.snap(e),t.y=this.snap(n)}},{key:"onDrag",value:function(t){var e=w(t.position,2),n=e[0],r=e[1];t.position[0]=this.snap(n),t.position[1]=this.snap(r),console.log(this,n,r,t.position),this.editor.view.nodes.get(t).update(),this.editor.view.updateConnections({node:t})}},{key:"snap",value:function(t){return Math.round(t/this.size)*this.size}}]),c}(),x=function(t){return 0===t.length?0:Math.min.apply(Math,e(t))},b=function(t){return 0===t.length?0:Math.max.apply(Math,e(t))};return{install:function(t,e){var n=e.background||!1,r=e.snap||!1,o=e.scaleExtent||!1,i=e.translateExtent||!1;n&&(this._background=new a(t,n)),(o||i)&&(this._restrictor=new c(t,o,i)),r&&(this._snap=new s(t,r))},zoomAt:function(t){var e,n,r,o,i,a,c=1this.depth||(e[n]||(e[n]=[]),e[n].includes(t))))return e[n].push(t),this.getNodes(t,"output").map(function(t){return r.getNodesTable(t,e,n+1)}),this.getNodes(t,"input").map(function(t){return r.getNodesTable(t,e,n-1)}),e}},{key:"nodeSize",value:function(t){var r=this.editor.view.nodes.get(t).el;return{width:r.clientWidth,height:r.clientHeight}}},{key:"arrange",value:function(){for(var e=this,t=0e.left&&n.righte.top&&n.bottome.right||n.righte.bottom||n.bottom * {\n pointer-events: all; }\n .connection .main-path {\n fill: none;\n stroke-width: 5px;\n stroke: steelblue; }\n");var n={name:"connection",install:function(r){r.bind("connectionpath"),r.bind("connectiondrop"),r.bind("connectionpick"),r.bind("resetconnection");var n=new e(r),i=new v(n),c=new WeakMap;function a(t){var e=c.get(this);if(e){var n=e.input,o=e.output;r.view.container.dispatchEvent(new PointerEvent("pointermove",t)),t.preventDefault(),t.stopPropagation(),i.start({input:n,output:o},n||o)}}function t(t){var e=document.elementFromPoint(t.clientX,t.clientY);n.io&&r.trigger("connectiondrop",n.io),e&&i.complete(function t(e,n){return e.get(n)||(n.parentElement?t(e,n.parentElement):null)}(c,e)||{})}r.on("resetconnection",function(){return i.complete()}),r.on("rendersocket",function(t){var e=t.el,n=t.input,o=t.output;c.set(e,{input:n,output:o}),e.removeEventListener("pointerdown",a),e.addEventListener("pointerdown",a)}),window.addEventListener("pointerup",t),r.on("renderconnection",function(t){var e=t.el,n=t.connection,o=t.points;p({el:e,d:l(r,o,n),connection:n})}),r.on("updateconnection",function(t){var e=t.el,n=t.connection,o=t.points;f({el:e,d:l(r,o,n)})}),r.on("destroy",function(){window.removeEventListener("pointerup",t)})}};t.default=n,t.defaultPath=h,Object.defineProperty(t,"__esModule",{value:!0})}); 7 | //# sourceMappingURL=connection-plugin.min.js.map 8 | -------------------------------------------------------------------------------- /www/js/context-menu-plugin.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * rete-context-menu-plugin v0.5.2 3 | * (c) 2019 Vitaliy Stoliarov 4 | * Released under the MIT license. 5 | */ 6 | !function(e){"use strict";var u,t=Object.prototype,c=t.hasOwnProperty,n="function"==typeof Symbol?Symbol:{},i=n.iterator||"@@iterator",r=n.asyncIterator||"@@asyncIterator",o=n.toStringTag||"@@toStringTag",a="object"==typeof module,s=e.regeneratorRuntime;if(s)a&&(module.exports=s);else{(s=e.regeneratorRuntime=a?module.exports:{}).wrap=y;var d="suspendedStart",f="suspendedYield",m="executing",h="completed",p={},l={};l[i]=function(){return this};var v=Object.getPrototypeOf,A=v&&v(v(I([])));A&&A!==t&&c.call(A,i)&&(l=A);var b=w.prototype=x.prototype=Object.create(l);C.prototype=b.constructor=w,w.constructor=C,w[o]=C.displayName="GeneratorFunction",s.isGeneratorFunction=function(e){var t="function"==typeof e&&e.constructor;return!!t&&(t===C||"GeneratorFunction"===(t.displayName||t.name))},s.mark=function(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,w):(e.__proto__=w,o in e||(e[o]="GeneratorFunction")),e.prototype=Object.create(b),e},s.awrap=function(e){return{__await:e}},E(S.prototype),S.prototype[r]=function(){return this},s.AsyncIterator=S,s.async=function(e,t,n,r){var i=new S(y(e,t,n,r));return s.isGeneratorFunction(t)?i:i.next().then(function(e){return e.done?e.value:i.next()})},E(b),b[o]="Generator",b[i]=function(){return this},b.toString=function(){return"[object Generator]"},s.keys=function(n){var r=[];for(var e in n)r.push(e);return r.reverse(),function e(){for(;r.length;){var t=r.pop();if(t in n)return e.value=t,e.done=!1,e}return e.done=!0,e}},s.values=I,O.prototype={constructor:O,reset:function(e){if(this.prev=0,this.next=0,this.sent=this._sent=u,this.done=!1,this.delegate=null,this.method="next",this.arg=u,this.tryEntries.forEach(j),!e)for(var t in this)"t"===t.charAt(0)&&c.call(this,t)&&!isNaN(+t.slice(1))&&(this[t]=u)},stop:function(){this.done=!0;var e=this.tryEntries[0].completion;if("throw"===e.type)throw e.arg;return this.rval},dispatchException:function(n){if(this.done)throw n;var r=this;function e(e,t){return o.type="throw",o.arg=n,r.next=e,t&&(r.method="next",r.arg=u),!!t}for(var t=this.tryEntries.length-1;0<=t;--t){var i=this.tryEntries[t],o=i.completion;if("root"===i.tryLoc)return e("end");if(i.tryLoc<=this.prev){var a=c.call(i,"catchLoc"),s=c.call(i,"finallyLoc");if(a&&s){if(this.prev\n.socket(\n :class="[type, socket.name] | kebab",\n :title="socket.name"\n)\n\n\n