├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── CONTRIBUTING ├── LICENSE ├── README.md ├── config-overrides.js ├── contrib └── settings-generator │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ └── main.rs ├── desktop ├── bitcoin_rpc.ts ├── chat.ts ├── createMenu.ts ├── declarations.d.ts ├── devtools.ts ├── handlers.ts ├── main.ts ├── preload.ts ├── sapio.ts ├── sapio_args.ts └── settings.ts ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.test.js ├── App.tsx ├── AppSlice.tsx ├── Data │ ├── BitcoinNode.ts │ ├── BitcoinStatusBar.css │ ├── BitcoinStatusBar.tsx │ ├── ContractManager.ts │ ├── DataSlice.ts │ ├── ModelManager.tsx │ ├── Simulation.css │ ├── Simulation.tsx │ ├── SimulationSlice.tsx │ ├── Transaction.css │ ├── Transaction.ts │ └── UTXO.ts ├── Glyphs.css ├── Miniscript │ ├── .appveyor.yml │ ├── .cargo-ok │ ├── .gitignore │ ├── .travis.yml │ ├── Cargo.toml │ ├── LICENSE_APACHE │ ├── LICENSE_MIT │ ├── README.md │ ├── rust-toolchain.toml │ ├── src │ │ ├── lib.rs │ │ └── utils.rs │ └── tests │ │ └── web.rs ├── Settings │ └── SettingsSlice.tsx ├── Store │ └── store.ts ├── UX │ ├── AppNavbar.css │ ├── AppNavbar.tsx │ ├── Chat │ │ ├── Channel.css │ │ ├── Channel.tsx │ │ ├── Chat.css │ │ ├── Chat.tsx │ │ ├── NewChannel.tsx │ │ └── NewNickname.tsx │ ├── ContractCreator │ │ ├── ContractCreatorSlice.tsx │ │ ├── CreateContractModal.css │ │ ├── CreateContractModal.tsx │ │ ├── LoadHexModal.tsx │ │ ├── SapioCompilerModal.tsx │ │ ├── SapioPluginPicker │ │ │ ├── PluginForm.css │ │ │ ├── PluginForm.tsx │ │ │ ├── PluginSelector.css │ │ │ ├── PluginSelector.tsx │ │ │ └── PluginTile.tsx │ │ ├── SaveHexModal.tsx │ │ └── ViewContractModal.tsx │ ├── CustomForms │ │ └── Widgets.tsx │ ├── Diagram │ │ ├── DemoCanvasWidget.tsx │ │ └── DiagramComponents │ │ │ ├── ConfirmationWidget.tsx │ │ │ ├── OutputLink.ts │ │ │ ├── OutputPortModel.ts │ │ │ ├── SpendLink │ │ │ ├── SpendLink.tsx │ │ │ ├── SpendLinkFactory.tsx │ │ │ ├── SpendLinkModel.ts │ │ │ ├── SpendLinkWidget.tsx │ │ │ └── SpendPortModel.tsx │ │ │ ├── TransactionNode │ │ │ ├── Ants.css │ │ │ ├── TransactionNodeFactory.tsx │ │ │ ├── TransactionNodeModel.ts │ │ │ └── TransactionNodeWidget.tsx │ │ │ └── UTXONode │ │ │ ├── Ants.css │ │ │ ├── UTXONodeFactory.tsx │ │ │ ├── UTXONodeModel.ts │ │ │ └── UTXONodeWidget.tsx │ ├── Entity │ │ ├── Detail │ │ │ ├── Hex.tsx │ │ │ ├── InputDetail.css │ │ │ ├── InputDetail.tsx │ │ │ ├── OutpointDetail.css │ │ │ ├── OutpointDetail.tsx │ │ │ ├── OutputDetail.css │ │ │ ├── OutputDetail.tsx │ │ │ ├── PSBTDetail.css │ │ │ ├── PSBTDetail.tsx │ │ │ ├── TransactionDetail.css │ │ │ ├── TransactionDetail.tsx │ │ │ ├── UTXODetail.css │ │ │ └── UTXODetail.tsx │ │ ├── EntitySlice.tsx │ │ ├── EntityViewer.css │ │ └── EntityViewer.tsx │ ├── Miniscript │ │ ├── Compiler.css │ │ └── Compiler.tsx │ ├── ModalSlice.tsx │ ├── Modals.tsx │ └── Settings │ │ ├── Settings.css │ │ └── Settings.tsx ├── Wallet │ ├── AvailableBalance.tsx │ ├── ContractList.tsx │ ├── DeleteDialog.tsx │ ├── NewWorkspace.tsx │ ├── Slice │ │ └── Reducer.ts │ ├── Wallet.css │ ├── Wallet.tsx │ ├── WalletHistory.tsx │ ├── WalletSend.tsx │ ├── WalletSendDialog.tsx │ ├── WalletSendForm.tsx │ └── Workspaces.tsx ├── common │ ├── chat_interface.d.ts │ ├── preload_interface.d.ts │ └── settings_gen.ts ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── serviceWorker.js ├── setupTests.js └── util.tsx ├── tsconfig.base.json ├── tsconfig.json ├── tsconfig.node.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "ecmaVersion": "latest", 18 | "sourceType": "module" 19 | }, 20 | "plugins": ["react", "@typescript-eslint", "unused-imports"], 21 | "rules": { 22 | "unused-imports/no-unused-imports": "error", 23 | "react/display-name": "warn" 24 | }, 25 | "ignorePatterns": ["**/pkg/*"] 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | tags 26 | 27 | **/node_modules 28 | .vscode 29 | .eslintcache 30 | 31 | # artifacts 32 | contrib/settings-generator/target/ 33 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 4 4 | } 5 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | To the fullest extent of the law possible in any and all jurisdictions, contributor agrees to 2 | transfer all intellectualy property rights (including copyright or patent) to Judica, Inc for code 3 | or patches submitted to this project. 4 | 5 | 6 | This is a temporary measure, and the code will eventually be released under an open source licensing 7 | scheme, but the above (aggressive) Contributor License Agreement is in place to permit flexibility 8 | in choosing a licensing scheme down the line. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Welcome to Sapio Studio, a project for visualizing and exploring bitcoin smart 2 | contract trees built with [Sapio](https://learn.sapio-lang.org). 3 | 4 | ## Getting Started 5 | 6 | ### Make sure you have a Bitcoin node running 7 | 8 | Before starting Sapio make sure you have a Bitcoin node running and that you've loaded your wallet 9 | via `bitcoin-cli`. 10 | 11 | ### Install dependencies 12 | 13 | `yarn install` 14 | 15 | ### Run Sapio 16 | 17 | In one terminal run code: `yarn start-react` 18 | 19 | After previous command finishes compiling run: `yarn start-electron` 20 | 21 | ## Connecting With Sapio & Bitcoin 22 | 23 | Open the application and configure your preferences for your Bitcoin Node and 24 | Sapio Client. 25 | 26 | You will need to load your own contract modules before you can do much. 27 | 28 | ## This is ALPHA SOFTWARE 29 | 30 | Expect things to be broken, ugly, and not ready for production. 31 | 32 | 33 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | // Refer: https://github.com/lokesh-007/wasm-react-rust 2 | const path = require('path'); 3 | const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin'); 4 | module.exports = function override(config, env) { 5 | config.resolve.extensions.push('.wasm'); 6 | config.module.rules.forEach((rule) => { 7 | (rule.oneOf || []).forEach((oneOf) => { 8 | if (oneOf.loader && oneOf.loader.indexOf('file-loader') >= 0) { 9 | // Make file-loader ignore WASM files 10 | oneOf.exclude.push(/\.wasm$/); 11 | } 12 | }); 13 | }); 14 | config.plugins = (config.plugins || []).concat([ 15 | new WasmPackPlugin({ 16 | crateDirectory: path.resolve(__dirname, './src/Miniscript'), 17 | outDir: path.resolve(__dirname, './src/Miniscript/pkg'), 18 | }), 19 | ]); 20 | return config; 21 | }; 22 | -------------------------------------------------------------------------------- /contrib/settings-generator/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.1.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 10 | 11 | [[package]] 12 | name = "bech32" 13 | version = "0.8.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" 16 | 17 | [[package]] 18 | name = "bitcoin_hashes" 19 | version = "0.10.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0" 22 | 23 | [[package]] 24 | name = "cc" 25 | version = "1.0.73" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 28 | 29 | [[package]] 30 | name = "dyn-clone" 31 | version = "1.0.4" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" 34 | 35 | [[package]] 36 | name = "hashbrown" 37 | version = "0.11.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 40 | 41 | [[package]] 42 | name = "indexmap" 43 | version = "1.8.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" 46 | dependencies = [ 47 | "autocfg", 48 | "hashbrown", 49 | "serde", 50 | ] 51 | 52 | [[package]] 53 | name = "itoa" 54 | version = "1.0.1" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" 57 | 58 | [[package]] 59 | name = "proc-macro2" 60 | version = "1.0.36" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 63 | dependencies = [ 64 | "unicode-xid", 65 | ] 66 | 67 | [[package]] 68 | name = "quote" 69 | version = "1.0.15" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" 72 | dependencies = [ 73 | "proc-macro2", 74 | ] 75 | 76 | [[package]] 77 | name = "ryu" 78 | version = "1.0.9" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" 81 | 82 | [[package]] 83 | name = "sapio-bitcoin" 84 | version = "0.28.0-rc.3" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "4f633d922199b181adf1611bdabce34d07474de7e60504068fe04e8fc7e78d32" 87 | dependencies = [ 88 | "bech32", 89 | "bitcoin_hashes", 90 | "sapio-secp256k1", 91 | ] 92 | 93 | [[package]] 94 | name = "sapio-secp256k1" 95 | version = "0.21.6" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "ff57f47f870f557e99622a6a951fd130b5216ab54e0a5503a0ade6cd5b601f7d" 98 | dependencies = [ 99 | "sapio-secp256k1-sys", 100 | ] 101 | 102 | [[package]] 103 | name = "sapio-secp256k1-sys" 104 | version = "0.21.4" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "43348a3209b43cb24c95f06b7a0e819327fb542aba1aecc727b72c6fe93502e7" 107 | dependencies = [ 108 | "cc", 109 | ] 110 | 111 | [[package]] 112 | name = "schemars" 113 | version = "0.8.8" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "c6b5a3c80cea1ab61f4260238409510e814e38b4b563c06044edf91e7dc070e3" 116 | dependencies = [ 117 | "dyn-clone", 118 | "indexmap", 119 | "schemars_derive", 120 | "serde", 121 | "serde_json", 122 | ] 123 | 124 | [[package]] 125 | name = "schemars_derive" 126 | version = "0.8.8" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "41ae4dce13e8614c46ac3c38ef1c0d668b101df6ac39817aebdaa26642ddae9b" 129 | dependencies = [ 130 | "proc-macro2", 131 | "quote", 132 | "serde_derive_internals", 133 | "syn", 134 | ] 135 | 136 | [[package]] 137 | name = "serde" 138 | version = "1.0.136" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" 141 | dependencies = [ 142 | "serde_derive", 143 | ] 144 | 145 | [[package]] 146 | name = "serde_derive" 147 | version = "1.0.136" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" 150 | dependencies = [ 151 | "proc-macro2", 152 | "quote", 153 | "syn", 154 | ] 155 | 156 | [[package]] 157 | name = "serde_derive_internals" 158 | version = "0.25.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6" 161 | dependencies = [ 162 | "proc-macro2", 163 | "quote", 164 | "syn", 165 | ] 166 | 167 | [[package]] 168 | name = "serde_json" 169 | version = "1.0.79" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" 172 | dependencies = [ 173 | "itoa", 174 | "ryu", 175 | "serde", 176 | ] 177 | 178 | [[package]] 179 | name = "settings-generator" 180 | version = "0.1.0" 181 | dependencies = [ 182 | "sapio-bitcoin", 183 | "schemars", 184 | "serde", 185 | "serde_json", 186 | ] 187 | 188 | [[package]] 189 | name = "syn" 190 | version = "1.0.86" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" 193 | dependencies = [ 194 | "proc-macro2", 195 | "quote", 196 | "unicode-xid", 197 | ] 198 | 199 | [[package]] 200 | name = "unicode-xid" 201 | version = "0.2.2" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 204 | -------------------------------------------------------------------------------- /contrib/settings-generator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "settings-generator" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | sapio-bitcoin = "0.28.0-rc.3" 10 | serde_json = "1.0.79" 11 | serde = "1.0.136" 12 | 13 | 14 | [dependencies.schemars] 15 | version = "0.8.8" 16 | features=["preserve_order"] -------------------------------------------------------------------------------- /desktop/bitcoin_rpc.ts: -------------------------------------------------------------------------------- 1 | import { preferences } from './settings'; 2 | import Client from 'bitcoin-core-ts'; 3 | import { readFile } from 'fs/promises'; 4 | import { setTimeout } from 'timers/promises'; 5 | 6 | let current_node: Client | null = null; 7 | let initializing = false; 8 | async function load_node_from_prefs(): Promise { 9 | await preferences.initialize(); 10 | let network = preferences.data.bitcoin.network.toLowerCase(); 11 | if (network === 'bitcoin') network = 'mainnet'; 12 | const port = preferences.data.bitcoin.port; 13 | const host = preferences.data.bitcoin.host; 14 | let split: string[]; 15 | if ('CookieFile' in preferences.data.bitcoin.auth) { 16 | const cookie = preferences.data.bitcoin.auth.CookieFile; 17 | const upw = await readFile(cookie, { encoding: 'utf-8' }); 18 | split = upw.split(':'); 19 | } else if ('UserPass' in preferences.data.bitcoin.auth) { 20 | split = preferences.data.bitcoin.auth.UserPass; 21 | } else { 22 | console.log('BROKEN'); 23 | const client = new Client({ 24 | network, 25 | port, 26 | host, 27 | }); 28 | return client; 29 | } 30 | if (split.length === 2) { 31 | const username = split[0] ?? ''; 32 | const password = split[1] ?? ''; 33 | const client = new Client({ 34 | network, 35 | username, 36 | password, 37 | port, 38 | host, 39 | }); 40 | return client; 41 | } else { 42 | throw Error('Malformed Cookie File'); 43 | } 44 | } 45 | 46 | export function deinit_bitcoin_node() { 47 | current_node = null; 48 | } 49 | export async function get_bitcoin_node(): Promise { 50 | // happy path 51 | if (current_node) return current_node; 52 | // only allow one initializer at a time... 53 | if (!initializing) { 54 | console.log('initializing'); 55 | initializing = true; 56 | current_node = await load_node_from_prefs(); 57 | initializing = false; 58 | console.log('initialized'); 59 | } else while (initializing) await setTimeout(10); 60 | 61 | console.log('returning'); 62 | return get_bitcoin_node(); 63 | } 64 | -------------------------------------------------------------------------------- /desktop/chat.ts: -------------------------------------------------------------------------------- 1 | import Database from 'better-sqlite3'; 2 | import * as Bitcoin from 'bitcoinjs-lib'; 3 | import * as ed from '@noble/ed25519'; 4 | import { ipcMain } from 'electron'; 5 | import { EnvelopeIn, EnvelopeOut } from '../src/common/chat_interface'; 6 | import fetch from 'node-fetch'; 7 | import { stringify } from 'another-json'; 8 | 9 | let g_chat_server: ChatServer | null = null; 10 | export function setup_chat() { 11 | ipcMain.handle('chat::init', async (event) => { 12 | if (g_chat_server) return; 13 | const privateKey = ed.utils.randomPrivateKey(); 14 | const publicKey = await ed.getPublicKey(privateKey); 15 | g_chat_server = new ChatServer(privateKey, publicKey); 16 | }); 17 | ipcMain.handle('chat::send', async (event, message: EnvelopeIn) => { 18 | if (!g_chat_server) return; 19 | return g_chat_server.send_message(message); 20 | }); 21 | ipcMain.handle('chat::add_user', (event, name: string, key: string) => { 22 | if (!g_chat_server) return; 23 | return g_chat_server.add_user(name, key); 24 | }); 25 | ipcMain.handle('chat::list_users', (event) => { 26 | if (!g_chat_server) return; 27 | return g_chat_server.list_users(); 28 | }); 29 | ipcMain.handle('chat::list_channels', (event) => { 30 | if (!g_chat_server) return; 31 | return g_chat_server.list_channels(); 32 | }); 33 | ipcMain.handle('chat::list_messages_channel', (event, channel, since) => { 34 | if (!g_chat_server) return; 35 | return g_chat_server.list_messages_channel(channel, since); 36 | }); 37 | } 38 | 39 | class ChatServer { 40 | db: Database.Database; 41 | insert: Database.Statement; 42 | list_all_users: Database.Statement; 43 | list_all_channels: Database.Statement; 44 | list_msg_chan: Database.Statement; 45 | my_pk: Uint8Array; 46 | my_sk: Uint8Array; 47 | constructor(privateKey: Uint8Array, publicKey: Uint8Array) { 48 | this.db = new Database( 49 | '/Users/jr/Library/Application Support/org.judica.tor-chat/chat.sqlite3', 50 | { readonly: false } 51 | ); 52 | this.insert = this.db.prepare( 53 | 'INSERT INTO user (nickname, key) VALUES (@name, @key);' 54 | ); 55 | this.list_all_users = this.db.prepare( 56 | 'SELECT nickname, key from user;' 57 | ); 58 | this.list_all_channels = this.db.prepare( 59 | 'SELECT DISTINCT channel_id from messages;' 60 | ); 61 | this.list_msg_chan = this.db.prepare( 62 | ` 63 | SELECT messages.body, user.nickname, messages.received_time 64 | FROM messages 65 | INNER JOIN user ON messages.user = user.userid 66 | where messages.channel_id = ? AND messages.received_time > ?; 67 | ` 68 | ); 69 | this.my_pk = publicKey; 70 | this.my_sk = privateKey; 71 | } 72 | add_user(name: string, key: string) { 73 | this.insert.run({ name, key }); 74 | } 75 | 76 | list_users(): [string, string][] { 77 | let res: any[] = this.list_all_users.all(); 78 | return res; 79 | } 80 | 81 | list_channels(): string[] { 82 | return this.list_all_channels.all(); 83 | } 84 | list_messages_channel(chan: string, since: number) { 85 | return this.list_msg_chan.all(chan, since); 86 | } 87 | 88 | async send_message(m: EnvelopeIn): Promise { 89 | const partial: Partial = m; 90 | partial.key = Array.from(this.my_pk); 91 | const encoded = Buffer.from(stringify(partial), 'utf-8'); 92 | // keys, messages & other inputs can be Uint8Arrays or hex strings 93 | // Uint8Array.from([0xde, 0xad, 0xbe, 0xef]) === 'deadbeef' 94 | const message = Uint8Array.from(encoded); 95 | 96 | const signature = Buffer.from( 97 | await ed.sign(message, this.my_sk) 98 | ).toString('base64'); 99 | // const isValid = await ed.verify(signature, message, publicKey); 100 | partial.signatures = { 101 | [Bitcoin.crypto.sha256(Buffer.from(this.my_pk)).toString('hex')]: { 102 | 'ed25519:1': signature, 103 | }, 104 | }; 105 | await fetch('http://127.0.0.1:46789/msg', { 106 | method: 'POST', 107 | body: JSON.stringify(partial) + '\n', 108 | }); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /desktop/createMenu.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, Menu, shell } from 'electron'; 2 | 3 | export function createMenu(window: BrowserWindow) { 4 | const template = [ 5 | { 6 | label: 'Edit', 7 | submenu: [ 8 | { role: 'undo' }, 9 | { role: 'redo' }, 10 | { type: 'separator' }, 11 | { role: 'cut' }, 12 | { role: 'copy' }, 13 | { role: 'paste' }, 14 | { role: 'pasteandmatchstyle' }, 15 | { role: 'delete' }, 16 | { role: 'selectall' }, 17 | ], 18 | }, 19 | { 20 | label: 'View', 21 | submenu: [ 22 | { role: 'reload' }, 23 | { role: 'forcereload' }, 24 | { role: 'toggledevtools' }, 25 | { type: 'separator' }, 26 | { role: 'resetzoom' }, 27 | { role: 'zoomin' }, 28 | { role: 'zoomout' }, 29 | { type: 'separator' }, 30 | { role: 'togglefullscreen' }, 31 | ], 32 | }, 33 | { 34 | role: 'window', 35 | submenu: [{ role: 'minimize' }, { role: 'close' }], 36 | }, 37 | { 38 | role: 'help', 39 | submenu: [ 40 | { 41 | label: 'Learn More', 42 | click() { 43 | shell.openExternal('https://electronjs.org'); 44 | }, 45 | }, 46 | ], 47 | }, 48 | ]; 49 | 50 | if (process.platform === 'darwin') { 51 | template.unshift({ 52 | label: app.getName(), 53 | submenu: [ 54 | { role: 'about' }, 55 | { type: 'separator' }, 56 | { role: 'services' }, 57 | { type: 'separator' }, 58 | { role: 'hide' }, 59 | { role: 'hideothers' }, 60 | { role: 'unhide' }, 61 | { type: 'separator' }, 62 | { role: 'quit' }, 63 | ], 64 | }); 65 | 66 | // Edit menu 67 | /* 68 | template[2].submenu.push({ type: 'separator' }, { 69 | label: 'Speech', 70 | submenu: [ 71 | { role: 'startspeaking' }, 72 | { role: 'stopspeaking' } 73 | ] 74 | }); 75 | */ 76 | 77 | // Window menu 78 | template[3]!.submenu = [ 79 | { role: 'close' }, 80 | { role: 'minimize' }, 81 | { role: 'zoom' }, 82 | { type: 'separator' }, 83 | { role: 'front' }, 84 | ]; 85 | } 86 | 87 | const menu = Menu.buildFromTemplate(template as any); 88 | Menu.setApplicationMenu(menu); 89 | } 90 | -------------------------------------------------------------------------------- /desktop/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'await-spawn' { 2 | import { ChildProcess, spawn } from 'child_process'; 3 | type SpawnParams = Parameters; 4 | declare async function spawn( 5 | ...spawn_params: SpawnParams 6 | ): Promise & { child: ChildProcess }; 7 | export = spawn; 8 | } 9 | 10 | declare module 'another-json' { 11 | declare function stringify(js: any): string; 12 | } 13 | -------------------------------------------------------------------------------- /desktop/devtools.ts: -------------------------------------------------------------------------------- 1 | import installExtension, { 2 | REDUX_DEVTOOLS, 3 | REACT_DEVELOPER_TOOLS, 4 | } from 'electron-devtools-installer'; 5 | // Or if you can not use ES6 imports 6 | /** 7 | const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); 8 | */ 9 | import { app } from 'electron'; 10 | 11 | export const register_devtools = () => 12 | app.on('ready', () => { 13 | installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS]) 14 | .then((name) => console.log(`Added Extension: ${name}`)) 15 | .catch((err) => console.log('An error occurred: ', err)); 16 | }); 17 | -------------------------------------------------------------------------------- /desktop/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import path from 'path'; 3 | import url from 'url'; 4 | import { custom_sapio_config } from './settings'; 5 | 6 | import { createMenu } from './createMenu'; 7 | import register_handlers from './handlers'; 8 | import { SapioWorkspace, start_sapio_oracle } from './sapio'; 9 | import { register_devtools } from './devtools'; 10 | import { get_bitcoin_node } from './bitcoin_rpc'; 11 | 12 | let mainWindow: BrowserWindow | null = null; 13 | 14 | async function createWindow() { 15 | await get_bitcoin_node(); 16 | await SapioWorkspace.new('default'); 17 | const startUrl = 18 | process.env.ELECTRON_START_URL || 19 | url.format({ 20 | pathname: path.join(__dirname, '../index.html'), 21 | protocol: 'file:', 22 | slashes: true, 23 | }); 24 | mainWindow = new BrowserWindow({ 25 | width: 800, 26 | height: 600, 27 | show: false, 28 | frame: false, 29 | backgroundColor: 'black', 30 | webPreferences: { 31 | preload: path.join(__dirname, 'preload.js'), 32 | allowRunningInsecureContent: false, 33 | contextIsolation: true, 34 | nodeIntegration: false, 35 | sandbox: true, 36 | }, 37 | }); 38 | mainWindow.once('ready-to-show', () => { 39 | mainWindow && mainWindow.show(); 40 | }); 41 | mainWindow.loadURL(startUrl); 42 | mainWindow.on('closed', function () { 43 | mainWindow = null; 44 | }); 45 | createMenu(mainWindow); 46 | register_handlers(mainWindow); 47 | custom_sapio_config(); 48 | start_sapio_oracle(); 49 | } 50 | register_devtools(); 51 | 52 | app.on('ready', createWindow); 53 | 54 | app.on('window-all-closed', function () { 55 | if (process.platform !== 'darwin') { 56 | app.quit(); 57 | } 58 | }); 59 | 60 | app.on('activate', function () { 61 | if (mainWindow === null) { 62 | createWindow(); 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /desktop/preload.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer, contextBridge } from 'electron'; 2 | 3 | function bitcoin_command( 4 | command: { method: string; parameters: any[] }[] 5 | ): Promise { 6 | return ipcRenderer.invoke('bitcoin::command', command).then((msg) => { 7 | if ('ok' in msg) { 8 | return msg.ok; 9 | } else if ('err' in msg) { 10 | throw new Error(JSON.stringify(msg.err)); 11 | } 12 | }); 13 | } 14 | 15 | type Result = { ok: T } | { err: string }; 16 | function create_contract( 17 | workspace: string, 18 | which: string, 19 | txn: string | null, 20 | args: string 21 | ): Promise> { 22 | return ipcRenderer.invoke('sapio::create_contract', workspace, [ 23 | which, 24 | txn, 25 | args, 26 | ]); 27 | } 28 | 29 | function open_contract_from_file(): Promise> { 30 | return ipcRenderer.invoke('sapio::open_contract_from_file'); 31 | } 32 | function load_wasm_plugin(workspace: string): Promise> { 33 | return ipcRenderer.invoke('sapio::load_wasm_plugin', workspace); 34 | } 35 | 36 | function show_config(): Promise> { 37 | return ipcRenderer.invoke('sapio::show_config'); 38 | } 39 | 40 | function load_contract_list(workspace: string) { 41 | return ipcRenderer.invoke('sapio::load_contract_list', workspace); 42 | } 43 | 44 | const psbt = { 45 | finalize: (psbt: string) => { 46 | return ipcRenderer.invoke('sapio::psbt::finalize', psbt); 47 | }, 48 | }; 49 | const compiled_contracts = { 50 | list: (workspace: string) => { 51 | return ipcRenderer.invoke('sapio::compiled_contracts::list', workspace); 52 | }, 53 | trash: (workspace: string, file_name: string) => { 54 | return ipcRenderer.invoke( 55 | 'sapio::compiled_contracts::trash', 56 | workspace, 57 | file_name 58 | ); 59 | }, 60 | open: (workspace: string, file_name: string) => { 61 | return ipcRenderer.invoke( 62 | 'sapio::compiled_contracts::open', 63 | workspace, 64 | file_name 65 | ); 66 | }, 67 | }; 68 | const workspaces = { 69 | init: (workspace: string) => { 70 | return ipcRenderer.invoke('sapio::workspaces::init', workspace); 71 | }, 72 | list: () => { 73 | return ipcRenderer.invoke('sapio::workspaces::list'); 74 | }, 75 | trash: (workspace: string) => { 76 | return ipcRenderer.invoke('sapio::workspaces::trash', workspace); 77 | }, 78 | }; 79 | 80 | function save_psbt(psbt: string): Promise { 81 | return ipcRenderer.invoke('save_psbt', psbt); 82 | } 83 | 84 | function fetch_psbt(): Promise { 85 | return ipcRenderer.invoke('fetch_psbt'); 86 | } 87 | function save_contract(contract: string): Promise { 88 | return ipcRenderer.invoke('save_contract', contract); 89 | } 90 | function save_settings(which: string, data: string): Promise { 91 | return ipcRenderer.invoke('save_settings', which, data); 92 | } 93 | 94 | function load_settings_sync(which: string): any { 95 | return ipcRenderer.invoke('load_settings_sync', which); 96 | } 97 | 98 | function write_clipboard(s: string) { 99 | ipcRenderer.invoke('write_clipboard', s); 100 | } 101 | 102 | function select_filename() { 103 | return ipcRenderer.invoke('select_filename'); 104 | } 105 | 106 | function emulator_kill() { 107 | console.log('Killing'); 108 | return ipcRenderer.invoke('emulator::kill'); 109 | } 110 | function emulator_start() { 111 | return ipcRenderer.invoke('emulator::start'); 112 | } 113 | function emulator_read_log(): Promise { 114 | return ipcRenderer.invoke('emulator::read_log'); 115 | } 116 | 117 | const chat = { 118 | init: () => ipcRenderer.invoke('chat::init'), 119 | send: (message: any /*EnvelopeIn*/) => 120 | ipcRenderer.invoke('chat::send', message), 121 | add_user: (name: string, key: string) => 122 | ipcRenderer.invoke('chat::add_user', name, key), 123 | list_users: () => ipcRenderer.invoke('chat::list_users'), 124 | list_channels: () => ipcRenderer.invoke('chat::list_channels'), 125 | list_messages_channel: (channel: string, since: number) => 126 | ipcRenderer.invoke('chat::list_messages_channel', channel, since), 127 | }; 128 | 129 | // to typecheck, uncomment and import preloads 130 | const api /*:preloads*/ = { 131 | bitcoin_command, 132 | save_psbt, 133 | save_contract, 134 | fetch_psbt, 135 | write_clipboard, 136 | save_settings, 137 | load_settings_sync, 138 | select_filename, 139 | sapio: { 140 | create_contract, 141 | show_config, 142 | load_wasm_plugin, 143 | open_contract_from_file, 144 | load_contract_list, 145 | compiled_contracts, 146 | psbt, 147 | workspaces, 148 | }, 149 | emulator: { 150 | kill: emulator_kill, 151 | start: emulator_start, 152 | read_log: emulator_read_log, 153 | }, 154 | chat, 155 | }; 156 | contextBridge.exposeInMainWorld('electron', api); 157 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "org.judica.sapio-studio", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/core": "^10.0.27", 7 | "@emotion/react": "^11.8.2", 8 | "@emotion/styled": "^11.8.1", 9 | "@ibm/plex": "^5.1.0", 10 | "@mui/core": "^5.0.0-alpha.47", 11 | "@mui/icons-material": "^5.5.0", 12 | "@mui/material": "^5.5.0", 13 | "@mui/styles": "^5.0.0", 14 | "@mui/x-data-grid": "^5.2.0", 15 | "@noble/ed25519": "^1.6.0", 16 | "@projectstorm/react-canvas-core": "^6.6.1", 17 | "@projectstorm/react-diagrams": "^6.6.1", 18 | "@projectstorm/react-diagrams-core": "^6.6.1", 19 | "@projectstorm/react-diagrams-defaults": "^6.6.1", 20 | "@projectstorm/react-diagrams-routing": "^6.6.1", 21 | "@reduxjs/toolkit": "^1.6.1", 22 | "@rjsf/core": "4.2.0", 23 | "@rjsf/material-ui": "4.2.0", 24 | "@testing-library/jest-dom": "^4.2.4", 25 | "@testing-library/react": "^9.3.2", 26 | "@testing-library/user-event": "^7.1.2", 27 | "@types/better-sqlite3": "^7.5.0", 28 | "@types/color": "^3.0.1", 29 | "@types/jest": "^27.0.1", 30 | "@types/lodash": "^4.14.150", 31 | "@types/node": "^17.0.21", 32 | "@types/node-fetch": "^2.6.1", 33 | "@types/react": "18", 34 | "@types/react-dom": "^17.0.9", 35 | "@wasm-tool/wasm-pack-plugin": "^1.6.0", 36 | "another-json": "^0.2.0", 37 | "await-spawn": "^4.0.2", 38 | "better-sqlite3": "^7.5.0", 39 | "bitcoin-core-ts": "^3.0.3", 40 | "bitcoinjs-lib": "^5.1.6", 41 | "bl": "^5.0.0", 42 | "closest": "^0.0.1", 43 | "color": "^3.1.2", 44 | "dagre": "^0.8.5", 45 | "electron-rebuild": "^3.2.7", 46 | "eslint-plugin-unused-imports": "^2.0.0", 47 | "esm": "^3.2.25", 48 | "lodash": "^4.17.15", 49 | "mathjs": "^7.5.1", 50 | "node-fetch": "2.6.7", 51 | "pathfinding": "^0.4.18", 52 | "paths-js": "^0.4.10", 53 | "prettier": "^2.6.0", 54 | "react": "18", 55 | "react-app-rewired": "^2.2.1", 56 | "react-dom": "18", 57 | "react-redux": "^8.0.0", 58 | "react-scripts": "^4.0.3", 59 | "resize-observer-polyfill": "^1.5.1", 60 | "typescript": "^4.3.5" 61 | }, 62 | "main": "./dist/desktop/main.js", 63 | "scripts": { 64 | "build-electron": "yarn tsc -p ./tsconfig.node.json", 65 | "watch": "yarn tsc -w", 66 | "rebuild": "electron-rebuild -f -w desktop/", 67 | "build": "react-app-rewired build", 68 | "eject": "react-scripts eject", 69 | "prettier": "prettier --write src desktop", 70 | "prettier:check": "prettier --check src desktop", 71 | "start-electron": "export ELECTRON_START_URL='http://localhost:3000' && yarn build-electron && electron .", 72 | "start-react": "export BROWSER=none REACT_EDITOR=none && react-app-rewired start", 73 | "test": "react-app-rewired test" 74 | }, 75 | "browserslist": { 76 | "production": [ 77 | ">0.2%", 78 | "not dead", 79 | "not op_mini all" 80 | ], 81 | "development": [ 82 | "last 1 chrome version", 83 | "last 1 firefox version", 84 | "last 1 safari version" 85 | ] 86 | }, 87 | "devDependencies": { 88 | "@types/bl": "^5.0.2", 89 | "@typescript-eslint/eslint-plugin": "^5.15.0", 90 | "@typescript-eslint/parser": "^5.15.0", 91 | "babel-eslint": "^10.1.0", 92 | "electron": "^17.1.2", 93 | "electron-devtools-installer": "^3.2.0", 94 | "eslint-config-react": "^1.1.7", 95 | "eslint-config-react-app": "^6.0.0", 96 | "eslint-plugin-react": "^7.29.4" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapio-lang/sapio-studio/4eb904f75ae6257e2777dd8f9bc4ebe7ac3d91a1/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 25 | Sapio Studio by Judica, Inc 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapio-lang/sapio-studio/4eb904f75ae6257e2777dd8f9bc4ebe7ac3d91a1/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapio-lang/sapio-studio/4eb904f75ae6257e2777dd8f9bc4ebe7ac3d91a1/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App-logo { 2 | height: 40vmin; 3 | pointer-events: none; 4 | } 5 | 6 | @media (prefers-reduced-motion: no-preference) { 7 | .App-logo { 8 | animation: App-logo-spin infinite 20s linear; 9 | } 10 | } 11 | 12 | .App-header { 13 | background-color: #282c34; 14 | min-height: 100vh; 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | justify-content: center; 19 | font-size: calc(10px + 2vmin); 20 | color: white; 21 | } 22 | 23 | .App-link { 24 | color: #61dafb; 25 | } 26 | 27 | @keyframes App-logo-spin { 28 | from { 29 | transform: rotate(0deg); 30 | } 31 | to { 32 | transform: rotate(360deg); 33 | } 34 | } 35 | 36 | .txhex { 37 | width: 100%; 38 | } 39 | 40 | .txcontainer { 41 | background-color: lightgrey; 42 | } 43 | 44 | .utxocontainer { 45 | background-color: lightgrey; 46 | } 47 | 48 | .viewer { 49 | height: 50%; 50 | } 51 | 52 | body { 53 | height: 100vh; 54 | width: 100vw; 55 | overflow: hidden; 56 | } 57 | 58 | .App { 59 | height: 100vh; 60 | max-height: 100vh; 61 | background-color: black; 62 | color: white; 63 | display: grid; 64 | grid-template-rows: 1fr auto; 65 | overflow: hidden; 66 | } 67 | 68 | .App-area { 69 | display: grid; 70 | grid-template-columns: 200px 1fr; 71 | overflow: scroll; 72 | max-height: 100%; 73 | } 74 | 75 | .App-gutter { 76 | min-height: 2em; 77 | height: max-content; 78 | } 79 | 80 | .area-nav { 81 | width: 200px; 82 | } 83 | 84 | .area-inner { 85 | width: calc(100vw - 200px); 86 | overflow-y: scroll; 87 | max-height: 100%; 88 | } 89 | 90 | .area-overlays { 91 | position: absolute; 92 | bottom: 0px; 93 | } 94 | 95 | .main-container { 96 | height: 100%; 97 | display: grid; 98 | } 99 | 100 | .main-container > .row { 101 | height: 100%; 102 | width: 100%; 103 | } 104 | 105 | .row { 106 | width: 100%; 107 | } 108 | 109 | .wallet-container { 110 | height: 100%; 111 | width: 100%; 112 | } 113 | 114 | .settings-container { 115 | height: 100%; 116 | } 117 | 118 | .miniscript-container { 119 | height: 100%; 120 | width: 100%; 121 | overflow: scroll; 122 | } 123 | 124 | .chat-container { 125 | height: 100%; 126 | width: 100%; 127 | overflow: scroll; 128 | } 129 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/AppSlice.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import _ from 'lodash'; 3 | import { createSelectorCreator, defaultMemoize } from 'reselect'; 4 | import { CreatedContract, Data } from './common/preload_interface'; 5 | import { AppDispatch, RootState } from './Store/store'; 6 | 7 | type Pages = 8 | | 'ContractCreator' 9 | | 'ContractViewer' 10 | | 'Wallet' 11 | | 'Settings' 12 | | 'MiniscriptCompiler' 13 | | 'Chat'; 14 | type StateType = { 15 | data: CreatedContract | null; 16 | counter: number; 17 | status_bar: boolean; 18 | showing: Pages; 19 | }; 20 | function default_state(): StateType { 21 | return { 22 | data: null, 23 | counter: -1, 24 | status_bar: true, 25 | showing: 'Wallet', 26 | }; 27 | } 28 | 29 | export const appSlice = createSlice({ 30 | name: 'App', 31 | initialState: default_state(), 32 | reducers: { 33 | switch_showing: (state, action: PayloadAction) => { 34 | state.showing = action.payload; 35 | }, 36 | load_new_model: (state, action: PayloadAction) => { 37 | state.data = action.payload; 38 | state.counter += 1; 39 | }, 40 | toggle_status_bar: (state) => { 41 | state.status_bar = !state.status_bar; 42 | }, 43 | add_effect_to_contract: ( 44 | state, 45 | action: PayloadAction< 46 | [string, string, Record] 47 | > 48 | ) => { 49 | if (state.data === null) return; 50 | if (state.data.args.context.effects === undefined) 51 | state.data.args.context.effects = {}; 52 | if (state.data.args.context.effects.effects === undefined) 53 | state.data.args.context.effects.effects = {}; 54 | const data = 55 | state.data.args.context.effects.effects[action.payload[0]] ?? 56 | {}; 57 | data[action.payload[1]] = action.payload[2]; 58 | state.data.args.context.effects.effects[action.payload[0]] = data; 59 | }, 60 | }, 61 | }); 62 | 63 | export const { 64 | switch_showing, 65 | load_new_model, 66 | toggle_status_bar, 67 | add_effect_to_contract, 68 | } = appSlice.actions; 69 | 70 | export const create_contract_of_type = 71 | (type_arg: string, txn: string | null, contract: any) => 72 | async (dispatch: AppDispatch, getState: () => RootState) => { 73 | const state = getState(); 74 | const compiled_contract = await window.electron.sapio.create_contract( 75 | state.walletReducer.workspace, 76 | type_arg, 77 | txn, 78 | contract 79 | ); 80 | if ('ok' in compiled_contract && compiled_contract.ok) 81 | dispatch( 82 | load_new_model({ 83 | args: JSON.parse(contract), 84 | name: type_arg, 85 | data: JSON.parse(compiled_contract.ok), 86 | }) 87 | ); 88 | }; 89 | export const recreate_contract = 90 | () => async (dispatch: AppDispatch, getState: () => RootState) => { 91 | const s = getState(); 92 | if (s.appReducer.data === null) return; 93 | return create_contract_of_type( 94 | s.appReducer.data.name, 95 | s.appReducer.data.data.program['funding']?.txs[0]?.linked_psbt 96 | ?.psbt ?? null, 97 | JSON.stringify(s.appReducer.data.args) 98 | )(dispatch, getState); 99 | }; 100 | 101 | export const open_contract_directory = 102 | (file_name: string) => 103 | async (dispatch: AppDispatch, getState: () => RootState) => { 104 | const state = getState(); 105 | window.electron.sapio.compiled_contracts 106 | .open(state.walletReducer.workspace, file_name) 107 | .then((v) => { 108 | if ('err' in v) return; 109 | return 'ok' in v && dispatch(load_new_model(v.ok)); 110 | }); 111 | }; 112 | 113 | export const create_contract_from_file = async ( 114 | dispatch: AppDispatch, 115 | getState: () => RootState 116 | ) => { 117 | window.electron.sapio 118 | .open_contract_from_file() 119 | .then((v) => 'ok' in v && dispatch(load_new_model(JSON.parse(v.ok)))); 120 | }; 121 | 122 | export const selectWorkspace: (state: RootState) => string = (state) => 123 | state.walletReducer.workspace; 124 | 125 | export const selectContract: (state: RootState) => [Data | null, number] = ( 126 | state: RootState 127 | ) => [state.appReducer.data?.data ?? null, state.appReducer.counter]; 128 | 129 | export const selectCreatedContract: ( 130 | state: RootState 131 | ) => CreatedContract | null = (state: RootState) => { 132 | return state.appReducer.data; 133 | }; 134 | 135 | export const selectStatusBar: (state: RootState) => boolean = ( 136 | state: RootState 137 | ) => state.appReducer.status_bar; 138 | 139 | const createDeepEqualSelector = createSelectorCreator( 140 | defaultMemoize, 141 | _.isEqual 142 | ); 143 | export const selectHasEffect = createDeepEqualSelector( 144 | [ 145 | (state: RootState, path: string) => 146 | state.appReducer.data?.args.context.effects?.effects ?? {}, 147 | ], 148 | (d) => Object.fromEntries(Object.keys(d).map((k) => [k, null])) 149 | ); 150 | 151 | export const selectShowing: (state: RootState) => Pages = (state: RootState) => 152 | state.appReducer.showing; 153 | 154 | export default appSlice.reducer; 155 | -------------------------------------------------------------------------------- /src/Data/BitcoinStatusBar.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapio-lang/sapio-studio/4eb904f75ae6257e2777dd8f9bc4ebe7ac3d91a1/src/Data/BitcoinStatusBar.css -------------------------------------------------------------------------------- /src/Data/BitcoinStatusBar.tsx: -------------------------------------------------------------------------------- 1 | import { clamp } from 'lodash'; 2 | import React from 'react'; 3 | import { BitcoinNodeManager } from './BitcoinNode'; 4 | import './BitcoinStatusBar.css'; 5 | import { Paper, Toolbar, Typography, useTheme } from '@mui/material'; 6 | import { useSelector } from 'react-redux'; 7 | import { selectNodePollFreq } from '../Settings/SettingsSlice'; 8 | interface BitcoinStatusBarProps { 9 | api: BitcoinNodeManager; 10 | } 11 | export function BitcoinStatusBar(props: BitcoinStatusBarProps) { 12 | const theme = useTheme(); 13 | const freq = useSelector(selectNodePollFreq); 14 | const [balance, setBalance] = React.useState(0); 15 | const [blockchaininfo, setBlockchaininfo] = React.useState(null); 16 | React.useEffect(() => { 17 | let next: ReturnType | null = null; 18 | let mounted = true; 19 | const periodic_update_stats = async () => { 20 | next = null; 21 | try { 22 | const balance = await props.api.check_balance(); 23 | setBalance(balance); 24 | } catch (err) { 25 | console.error(err); 26 | setBalance(0); 27 | } 28 | 29 | try { 30 | const info = await props.api.blockchaininfo(); 31 | console.log(balance); 32 | setBlockchaininfo(info); 33 | } catch (err) { 34 | console.error(err); 35 | setBlockchaininfo(null); 36 | } 37 | 38 | if (mounted) { 39 | let prefs = freq; 40 | prefs = clamp(prefs ?? 0, 5, 5 * 60); 41 | console.log('StatusBar', 'NEXT PERIODIC CHECK IN ', prefs); 42 | next = setTimeout(periodic_update_stats, prefs * 1000); 43 | } 44 | }; 45 | 46 | let prefs = freq; 47 | prefs = clamp(prefs ?? 0, 5, 5 * 60); 48 | next = setTimeout(periodic_update_stats, prefs * 1000); 49 | return () => { 50 | mounted = false; 51 | if (next !== null) clearTimeout(next); 52 | }; 53 | }, []); 54 | 55 | const network = blockchaininfo?.chain ?? 'disconnected'; 56 | const headers = blockchaininfo?.headers ?? '?'; 57 | const blocks = blockchaininfo?.headers ?? '?'; 58 | return ( 59 | 71 | 72 | 73 |
chain: {network}
74 |
75 | 76 |
77 | balance: {balance} BTC 78 |
79 |
80 | 81 |
82 | processed: {blocks}/{headers} 83 |
84 |
85 |
86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/Data/DataSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import _ from 'lodash'; 3 | import { RootState } from '../Store/store'; 4 | import { Status } from './BitcoinNode'; 5 | import { TransactionState } from '../UX/Diagram/DiagramComponents/TransactionNode/TransactionNodeModel'; 6 | type TXID = string; 7 | export type DataStateType = { 8 | status: Record; 9 | state: Record; 10 | }; 11 | 12 | function default_state(): DataStateType { 13 | return { 14 | status: {}, 15 | state: {}, 16 | }; 17 | } 18 | 19 | export const settingsSlice = createSlice({ 20 | name: 'Settings', 21 | initialState: default_state(), 22 | reducers: { 23 | load_status: ( 24 | state, 25 | action: PayloadAction<{ 26 | status: Record; 27 | state: Record; 28 | }> 29 | ) => { 30 | if ( 31 | _.isEqual(state.status, action.payload.status) && 32 | _.isEqual(state.state, action.payload.state) 33 | ) 34 | return; 35 | state.status = action.payload.status; 36 | state.state = action.payload.state; 37 | }, 38 | }, 39 | }); 40 | 41 | export const { load_status } = settingsSlice.actions; 42 | 43 | const memo_number = -1; 44 | const g_memo: Record> = {}; 45 | 46 | export const selectStatus: ( 47 | txid: TXID 48 | ) => (state: RootState) => TransactionState = (txid) => (state: RootState) => { 49 | // return type must be based on state 50 | return state.dataReducer.state[txid]!; 51 | }; 52 | export const dataReducer = settingsSlice.reducer; 53 | -------------------------------------------------------------------------------- /src/Data/ModelManager.tsx: -------------------------------------------------------------------------------- 1 | import { DiagramModel, LinkModel } from '@projectstorm/react-diagrams'; 2 | import { ContractModel, timing_cache } from './ContractManager'; 3 | import { TransactionModel, PhantomTransactionModel } from './Transaction'; 4 | import { UTXOModel } from './UTXO'; 5 | import { SpendPortModel } from '../UX/Diagram/DiagramComponents/SpendLink/SpendPortModel'; 6 | 7 | export class ModelManager { 8 | model: DiagramModel; 9 | constructor(model: DiagramModel) { 10 | this.model = model; 11 | } 12 | load(contract: ContractModel) { 13 | this.model.addAll( 14 | ...contract.txn_models.filter( 15 | (v) => !(v instanceof PhantomTransactionModel) 16 | ) 17 | ); 18 | this.model.addAll(...contract.utxo_models); 19 | const utxo_links: LinkModel[] = contract.utxo_models 20 | .map((m: UTXOModel) => m.getOutPorts()) 21 | .flat(1) 22 | .map((p: SpendPortModel) => 23 | Object.entries(p.getLinks()).map((v) => v[1]) 24 | ) 25 | .flat(1); 26 | this.model.addAll(...utxo_links); 27 | const tx_links: LinkModel[] = contract.txn_models 28 | .filter((v) => !(v instanceof PhantomTransactionModel)) 29 | .map((m: TransactionModel) => m.getOutPorts()) 30 | .flat(1) 31 | .map((p: SpendPortModel) => 32 | Object.entries(p.getLinks()).map((v) => v[1]) 33 | ) 34 | .flat(1); 35 | 36 | this.model.addAll(...tx_links); 37 | } 38 | unload(contract: ContractModel) { 39 | contract.txn_models.forEach((m) => m.remove_from_model(this.model)); 40 | timing_cache.cache.clear(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Data/Simulation.css: -------------------------------------------------------------------------------- 1 | .Simulation { 2 | background-blend-mode: screen; 3 | max-height: 50vh; 4 | padding-bottom: 3em; 5 | padding-right: 1em; 6 | padding-left: 1em; 7 | } 8 | 9 | .Controlers { 10 | padding-top: 40px; 11 | display: grid; 12 | grid-template-columns: 1fr 1fr; 13 | column-gap: 1em; 14 | } 15 | 16 | .Controler { 17 | display: grid; 18 | grid-template-rows: min-content 1fr; 19 | } 20 | 21 | .ControlerSettings { 22 | display: grid; 23 | grid-template-columns: min-content 15vw 15vw min-content; 24 | column-gap: 1em; 25 | } 26 | 27 | .ControlerSliders { 28 | height: min-content; 29 | } 30 | -------------------------------------------------------------------------------- /src/Data/SimulationSlice.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { RootState } from '../Store/store'; 3 | import { hasOwn, TXID } from '../util'; 4 | type SimulationState = { 5 | unreachable_models: Record; 6 | show: boolean; 7 | }; 8 | function default_state(): SimulationState { 9 | return { 10 | unreachable_models: {}, 11 | show: false, 12 | }; 13 | } 14 | 15 | export const simulationSlice = createSlice({ 16 | name: 'Simulation', 17 | initialState: default_state(), 18 | reducers: { 19 | set_unreachable: (state, action: PayloadAction>) => { 20 | state.unreachable_models = action.payload; 21 | }, 22 | toggle_showing: (state) => { 23 | state.show = !state.show; 24 | }, 25 | }, 26 | }); 27 | 28 | export const { set_unreachable, toggle_showing } = simulationSlice.actions; 29 | 30 | export const selectIsReachable: (state: RootState) => (t: TXID) => boolean = 31 | (state: RootState) => (t: TXID) => 32 | !hasOwn(state.simulationReducer.unreachable_models, t); 33 | export const selectSimIsShowing: (state: RootState) => boolean = ( 34 | state: RootState 35 | ) => state.simulationReducer.show; 36 | 37 | export const simulationReducer = simulationSlice.reducer; 38 | -------------------------------------------------------------------------------- /src/Data/Transaction.css: -------------------------------------------------------------------------------- 1 | .truncate { 2 | white-space: nowrap; 3 | overflow: scroll; 4 | display: block; 5 | } 6 | code { 7 | background-color: #3a3a3a; 8 | } 9 | -------------------------------------------------------------------------------- /src/Data/Transaction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DiagramModel, 3 | LinkModel, 4 | NodeModel, 5 | } from '@projectstorm/react-diagrams'; 6 | import * as Bitcoin from 'bitcoinjs-lib'; 7 | import { OutputLinkModel } from '../UX/Diagram/DiagramComponents/OutputLink'; 8 | import { SpendLinkModel } from '../UX/Diagram/DiagramComponents/SpendLink/SpendLinkModel'; 9 | import { TransactionNodeModel } from '../UX/Diagram/DiagramComponents/TransactionNode/TransactionNodeModel'; 10 | import { HasKeys, TXID } from '../util'; 11 | import { NodeColorT, SigningDataStore, NodeColor } from './ContractManager'; 12 | import './Transaction.css'; 13 | import { new_utxo_inner_data, UTXOModel } from './UTXO'; 14 | import { select_txn } from '../UX/Entity/EntitySlice'; 15 | import { store } from '../Store/store'; 16 | import { TransactionData, UTXOFormatData } from '../common/preload_interface'; 17 | import _ from 'lodash'; 18 | 19 | export class TransactionModel extends TransactionNodeModel implements HasKeys { 20 | tx: Bitcoin.Transaction; 21 | witness_set: SigningDataStore; 22 | utxo_models: Array; 23 | public utxo_links: Array; 24 | public input_links: Array; 25 | constructor( 26 | tx: Bitcoin.Transaction, 27 | all_witnesses: SigningDataStore, 28 | name: string, 29 | color: NodeColorT, 30 | utxo_labels: Array 31 | ) { 32 | super({}, tx.getId().substr(0, 16), name, NodeColor.get(color), tx); 33 | this.tx = tx; 34 | this.utxo_models = []; 35 | this.utxo_links = []; 36 | this.input_links = []; 37 | this.witness_set = all_witnesses; 38 | for (let y = 0; y < this.tx.outs.length; ++y) { 39 | const subcolor = NodeColor.clone(color); 40 | const metadata: UTXOFormatData = _.merge( 41 | { 42 | color: NodeColor.get(subcolor), 43 | label: name, 44 | }, 45 | utxo_labels[y] 46 | ); 47 | // TODO: Get rid of assertion 48 | const out: Bitcoin.TxOutput = tx.outs[y] as Bitcoin.TxOutput; 49 | const utxo = new UTXOModel( 50 | new_utxo_inner_data(out.script, out.value, tx, y), 51 | metadata, 52 | this 53 | ); 54 | this.utxo_models.push(utxo); 55 | this.utxo_links.push( 56 | this.addOutPort('out' + y, true).create_link( 57 | utxo.addInPort('create'), 58 | this, 59 | undefined 60 | ) 61 | ); 62 | } 63 | this.registerListener({ 64 | selectionChanged: (event: any): void => { 65 | if (event.isSelected) store.dispatch(select_txn(tx.getId())); 66 | }, 67 | }); 68 | } 69 | 70 | get_json(): TransactionData { 71 | return { 72 | psbt: this.witness_set.psbts[0]!.toBase64(), 73 | hex: this.tx.toHex(), 74 | metadata: { 75 | label: this.getOptions().name, 76 | color: this.getOptions().color, 77 | }, 78 | output_metadata: this.utxo_models.map((u) => { 79 | return { 80 | color: u.getOptions().color, 81 | label: u.getOptions().name, 82 | simp: {}, 83 | }; 84 | }), 85 | }; 86 | } 87 | 88 | get_txid(): TXID { 89 | return this.tx.getId(); 90 | } 91 | remove_from_model(model: DiagramModel) { 92 | if (!(this instanceof PhantomTransactionModel)) { 93 | // TODO: is this a valid cast 94 | model.removeNode(this as unknown as NodeModel); 95 | this.utxo_links.map((x) => 96 | model.removeLink(x as unknown as LinkModel) 97 | ); 98 | } 99 | this.utxo_models.map((x) => model.removeNode(x)); 100 | this.input_links.map((x) => 101 | model.removeLink(x as unknown as LinkModel) 102 | ); 103 | } 104 | } 105 | 106 | export class PhantomTransactionModel extends TransactionModel { 107 | override_txid: TXID; 108 | constructor( 109 | override_txid: TXID, 110 | tx: Bitcoin.Transaction, 111 | all_witnesses: SigningDataStore, 112 | name: string, 113 | color: NodeColorT, 114 | utxo_labels: Array 115 | ) { 116 | super(tx, all_witnesses, name, color, utxo_labels); 117 | this.override_txid = override_txid; 118 | } 119 | get_txid(): TXID { 120 | return this.override_txid; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Data/UTXO.ts: -------------------------------------------------------------------------------- 1 | import { Transaction } from 'bitcoinjs-lib'; 2 | import { 3 | UTXOInnerData, 4 | UTXONodeModel, 5 | } from '../UX/Diagram/DiagramComponents/UTXONode/UTXONodeModel'; 6 | import { TransactionModel } from './Transaction'; 7 | import { is_mock_outpoint, txid_buf_to_string } from '../util'; 8 | import { store } from '../Store/store'; 9 | import { select_utxo } from '../UX/Entity/EntitySlice'; 10 | import { UTXOFormatData } from '../common/preload_interface'; 11 | import { SpendLinkModel } from '../UX/Diagram/DiagramComponents/SpendLink/SpendLinkModel'; 12 | export function new_utxo_inner_data( 13 | script: Buffer, 14 | amount: number, 15 | txn: Transaction, 16 | index: number 17 | ): UTXOInnerData { 18 | return { 19 | txid: txn.getId(), 20 | index: index, 21 | script: script, 22 | amount: amount, 23 | spends: [], 24 | }; 25 | } 26 | export class UTXOModel extends UTXONodeModel { 27 | constructor( 28 | utxo: UTXOInnerData, 29 | metadata: UTXOFormatData, 30 | txn: TransactionModel 31 | ) { 32 | super( 33 | { 34 | name: metadata.label, 35 | color: metadata.color, 36 | amount: utxo.amount, 37 | confirmed: false, 38 | txn, 39 | utxo, 40 | metadata, 41 | }, 42 | txn.get_txid(), 43 | utxo.index 44 | ); 45 | this.registerListener({ 46 | selectionChanged: (event: any) => { 47 | // TODO: get store the right way? 48 | if (event.isSelected) 49 | store.dispatch( 50 | select_utxo({ hash: utxo.txid, nIn: utxo.index }) 51 | ); 52 | }, 53 | }); 54 | } 55 | getAmount(): number { 56 | return this.getOptions().utxo.amount; 57 | } 58 | 59 | spent_by( 60 | spender: TransactionModel, 61 | s_idx: number, 62 | idx: number 63 | ): SpendLinkModel { 64 | return this.addOutPort('tx' + s_idx).spend_link( 65 | spender.addInPort('in' + idx, true), 66 | spender, 67 | undefined 68 | ); 69 | } 70 | } 71 | 72 | export function update_utxomodel(utxo_in: UTXOModel) { 73 | const to_iterate: Array[] = [[utxo_in]]; 74 | while (to_iterate.length) { 75 | const s = to_iterate.pop()!; 76 | for (const utxo of s) { 77 | // Pop a node for processing... 78 | utxo.getOptions().utxo.spends.forEach((spend: TransactionModel) => { 79 | spend.tx.ins 80 | .filter( 81 | (inp) => 82 | txid_buf_to_string(inp.hash) === 83 | utxo.getOptions().utxo.txid 84 | ) 85 | .forEach((inp) => { 86 | inp.hash = utxo.getOptions().txn.tx.getHash(); 87 | }); 88 | spend.tx.ins 89 | .filter((inp) => 90 | is_mock_outpoint({ 91 | hash: txid_buf_to_string(inp.hash), 92 | nIn: inp.index, 93 | }) 94 | ) 95 | .forEach((inp) => { 96 | // TODO: Only modify the mock that was intended 97 | inp.hash = utxo.getOptions().txn.tx.getHash(); 98 | inp.index = utxo.getOptions().utxo.index; 99 | }); 100 | to_iterate.push(spend.utxo_models); 101 | }); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Miniscript/.appveyor.yml: -------------------------------------------------------------------------------- 1 | install: 2 | - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe 3 | - if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly 4 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin 5 | - rustc -V 6 | - cargo -V 7 | 8 | build: false 9 | 10 | test_script: 11 | - cargo test --locked 12 | -------------------------------------------------------------------------------- /src/Miniscript/.cargo-ok: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapio-lang/sapio-studio/4eb904f75ae6257e2777dd8f9bc4ebe7ac3d91a1/src/Miniscript/.cargo-ok -------------------------------------------------------------------------------- /src/Miniscript/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | bin/ 5 | pkg/ 6 | wasm-pack.log 7 | -------------------------------------------------------------------------------- /src/Miniscript/.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: false 3 | 4 | cache: cargo 5 | 6 | matrix: 7 | include: 8 | # Builds with wasm-pack. 9 | - rust: beta 10 | env: RUST_BACKTRACE=1 11 | addons: 12 | firefox: latest 13 | chrome: stable 14 | before_script: 15 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 16 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 17 | - cargo install-update -a 18 | - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f 19 | script: 20 | - cargo generate --git . --name testing 21 | # Having a broken Cargo.toml (in that it has curlies in fields) anywhere 22 | # in any of our parent dirs is problematic. 23 | - mv Cargo.toml Cargo.toml.tmpl 24 | - cd testing 25 | - wasm-pack build 26 | - wasm-pack test --chrome --firefox --headless 27 | 28 | # Builds on nightly. 29 | - rust: nightly 30 | env: RUST_BACKTRACE=1 31 | before_script: 32 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 33 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 34 | - cargo install-update -a 35 | - rustup target add wasm32-unknown-unknown 36 | script: 37 | - cargo generate --git . --name testing 38 | - mv Cargo.toml Cargo.toml.tmpl 39 | - cd testing 40 | - cargo check 41 | - cargo check --target wasm32-unknown-unknown 42 | - cargo check --no-default-features 43 | - cargo check --target wasm32-unknown-unknown --no-default-features 44 | - cargo check --no-default-features --features console_error_panic_hook 45 | - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook 46 | - cargo check --no-default-features --features "console_error_panic_hook wee_alloc" 47 | - cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc" 48 | 49 | # Builds on beta. 50 | - rust: beta 51 | env: RUST_BACKTRACE=1 52 | before_script: 53 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 54 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 55 | - cargo install-update -a 56 | - rustup target add wasm32-unknown-unknown 57 | script: 58 | - cargo generate --git . --name testing 59 | - mv Cargo.toml Cargo.toml.tmpl 60 | - cd testing 61 | - cargo check 62 | - cargo check --target wasm32-unknown-unknown 63 | - cargo check --no-default-features 64 | - cargo check --target wasm32-unknown-unknown --no-default-features 65 | - cargo check --no-default-features --features console_error_panic_hook 66 | - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook 67 | # Note: no enabling the `wee_alloc` feature here because it requires 68 | # nightly for now. 69 | -------------------------------------------------------------------------------- /src/Miniscript/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "miniscript-shim" 3 | version = "0.1.0" 4 | authors = ["Jeremy Rubin "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [features] 11 | default = ["console_error_panic_hook"] 12 | 13 | [dependencies] 14 | wasm-bindgen = "0.2.63" 15 | sapio-miniscript = {version = "7.0.0-10", features =["use-serde", "compiler", "serde"]} 16 | serde_json = "1.0.59" 17 | 18 | 19 | # The `console_error_panic_hook` crate provides better debugging of panics by 20 | # logging them with `console.error`. This is great for development, but requires 21 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 22 | # code size when deploying. 23 | console_error_panic_hook = { version = "0.1.6", optional = true } 24 | 25 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 26 | # compared to the default allocator's ~10K. It is slower than the default 27 | # allocator, however. 28 | # 29 | # Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. 30 | wee_alloc = { version = "0.4.5", optional = true } 31 | 32 | [dev-dependencies] 33 | wasm-bindgen-test = "0.3.13" 34 | 35 | [profile.release] 36 | # Tell `rustc` to optimize for small code size. 37 | opt-level = "s" 38 | -------------------------------------------------------------------------------- /src/Miniscript/LICENSE_MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Jeremy Rubin 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /src/Miniscript/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

wasm-pack-template

4 | 5 | A template for kick starting a Rust and WebAssembly project using wasm-pack. 6 | 7 |

8 | Build Status 9 |

10 | 11 |

12 | Tutorial 13 | | 14 | Chat 15 |

16 | 17 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 18 | 19 |
20 | 21 | ## About 22 | 23 | [**📚 Read this template tutorial! 📚**][template-docs] 24 | 25 | This template is designed for compiling Rust libraries into WebAssembly and 26 | publishing the resulting package to NPM. 27 | 28 | Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other 29 | templates and usages of `wasm-pack`. 30 | 31 | [tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html 32 | [template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html 33 | 34 | ## 🚴 Usage 35 | 36 | ### 🐑 Use `cargo generate` to Clone this Template 37 | 38 | [Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate) 39 | 40 | ``` 41 | cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project 42 | cd my-project 43 | ``` 44 | 45 | ### 🛠️ Build with `wasm-pack build` 46 | 47 | ``` 48 | wasm-pack build 49 | ``` 50 | 51 | ### 🔬 Test in Headless Browsers with `wasm-pack test` 52 | 53 | ``` 54 | wasm-pack test --headless --firefox 55 | ``` 56 | 57 | ### 🎁 Publish to NPM with `wasm-pack publish` 58 | 59 | ``` 60 | wasm-pack publish 61 | ``` 62 | 63 | ## 🔋 Batteries Included 64 | 65 | - [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating 66 | between WebAssembly and JavaScript. 67 | - [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook) 68 | for logging panic messages to the developer console. 69 | - [`wee_alloc`](https://github.com/rustwasm/wee_alloc), an allocator optimized 70 | for small code size. 71 | -------------------------------------------------------------------------------- /src/Miniscript/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2022-07-25" 3 | -------------------------------------------------------------------------------- /src/Miniscript/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(once_cell)] 2 | #![feature(result_flattening)] 3 | mod utils; 4 | 5 | use crate::bitcoin::Script; 6 | use crate::bitcoin::XOnlyPublicKey; 7 | use std::collections::HashMap; 8 | use std::rc::Rc; 9 | use std::sync::Arc; 10 | use wasm_bindgen::prelude::*; 11 | 12 | // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global 13 | // allocator. 14 | #[cfg(feature = "wee_alloc")] 15 | #[global_allocator] 16 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 17 | 18 | #[wasm_bindgen] 19 | extern "C" { 20 | fn alert(s: &str); 21 | } 22 | 23 | use core::str::FromStr; 24 | use sapio_miniscript::*; 25 | #[wasm_bindgen] 26 | pub fn compile(s: &str) -> Result { 27 | let pol = policy::Concrete::from_str(s).map_err(|e| e.to_string())?; 28 | let ms: Miniscript = pol.compile().map_err(|e| e.to_string())?; 29 | Ok(ms.to_string()) 30 | } 31 | use bitcoin::secp256k1::VerifyOnly; 32 | use sapio_miniscript::bitcoin::secp256k1::Secp256k1; 33 | use std::sync::LazyLock; 34 | static SECP: LazyLock> = LazyLock::new(|| Secp256k1::verification_only()); 35 | use bitcoin::util::taproot::TaprootSpendInfo; 36 | use sapio_miniscript::TranslatePk; 37 | #[wasm_bindgen] 38 | #[derive(Debug)] 39 | pub struct KeyTab { 40 | v: HashMap, 41 | } 42 | 43 | #[wasm_bindgen] 44 | impl KeyTab { 45 | pub fn new() -> KeyTab { 46 | KeyTab { v: HashMap::new() } 47 | } 48 | pub fn add(&mut self, k: String, v: String) { 49 | self.v.insert(k, v); 50 | } 51 | } 52 | 53 | #[wasm_bindgen] 54 | #[derive(Debug)] 55 | pub struct Fragments { 56 | v: Vec, 57 | } 58 | 59 | #[wasm_bindgen] 60 | impl Fragments { 61 | pub fn new() -> Self { 62 | Fragments { v: vec![] } 63 | } 64 | pub fn add(&mut self, s: String) { 65 | self.v.push(s) 66 | } 67 | pub fn add_all(&mut self, s: Box<[JsValue]>) -> bool { 68 | for v in s.iter() { 69 | if let Some(st) = v.as_string() { 70 | self.v.push(st) 71 | } else { 72 | return false; 73 | } 74 | } 75 | return true; 76 | } 77 | } 78 | 79 | #[wasm_bindgen] 80 | pub fn taproot(frags: Fragments, keytab: &KeyTab) -> Result { 81 | let key = keytab 82 | .v 83 | .iter() 84 | .map(|(k, v)| XOnlyPublicKey::from_str(&v).map(|key| (k, key))) 85 | .collect::, _>>() 86 | .map_err(|e| e.to_string())?; 87 | let ms: Vec> = frags 88 | .v 89 | .iter() 90 | .map(|s| Miniscript::::from_str(&s).map_err(|e| e.to_string())) 91 | .collect::>, _>>() 92 | .map_err(|e| e.to_string())?; 93 | 94 | let fpk: &Fn(&String) -> Result = &|k| { 95 | key.get(&k) 96 | .cloned() 97 | .ok_or_else(|| format!("Missing Key: {}", k)) 98 | }; 99 | let scripts: Vec<(u32, Script)> = ms 100 | .iter() 101 | .map(|s| { 102 | s.translate_pk(fpk, |k| Err(format!("No PKH Support for {}", k))) 103 | .map(|s: Miniscript| (1, s.encode())) 104 | }) 105 | .collect::, _>>()?; 106 | use bitcoin::hashes::Hash; 107 | let nums: XOnlyPublicKey = { 108 | let mut b = bitcoin::hashes::sha256::Hash::hash("Hello".as_bytes()).into_inner(); 109 | loop { 110 | if let Ok(k) = XOnlyPublicKey::from_slice(&b[..]) { 111 | break k; 112 | } else { 113 | b = bitcoin::hashes::sha256::Hash::hash(&b[..]).into_inner(); 114 | } 115 | } 116 | }; 117 | let tsi = 118 | TaprootSpendInfo::with_huffman_tree(&SECP, nums, scripts).map_err(|e| e.to_string())?; 119 | use sapio_miniscript::bitcoin::hashes::hex::ToHex; 120 | let js = serde_json::json! {{ 121 | "tweak": tsi.tap_tweak().as_hash().to_hex(), 122 | "internal_key": tsi.internal_key().to_hex(), 123 | "merkle_root": tsi.merkle_root().map(|m|m.to_hex()), 124 | "scripts": tsi.as_script_map().iter().collect::>(), 125 | "address":{ 126 | "main": sapio_miniscript::bitcoin::Address::p2tr_tweaked(tsi.output_key(), bitcoin::network::constants::Network::Bitcoin), 127 | "test": sapio_miniscript::bitcoin::Address::p2tr_tweaked(tsi.output_key(), bitcoin::network::constants::Network::Testnet), 128 | "regtest": sapio_miniscript::bitcoin::Address::p2tr_tweaked(tsi.output_key(), bitcoin::network::constants::Network::Regtest), 129 | "signet": sapio_miniscript::bitcoin::Address::p2tr_tweaked(tsi.output_key(), bitcoin::network::constants::Network::Signet), 130 | } 131 | }}; 132 | Ok(serde_json::to_string_pretty(&js).map_err(|e| e.to_string())?) 133 | } 134 | -------------------------------------------------------------------------------- /src/Miniscript/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn set_panic_hook() { 2 | // When the `console_error_panic_hook` feature is enabled, we can call the 3 | // `set_panic_hook` function at least once during initialization, and then 4 | // we will get better error messages if our code ever panics. 5 | // 6 | // For more details see 7 | // https://github.com/rustwasm/console_error_panic_hook#readme 8 | #[cfg(feature = "console_error_panic_hook")] 9 | console_error_panic_hook::set_once(); 10 | } 11 | -------------------------------------------------------------------------------- /src/Miniscript/tests/web.rs: -------------------------------------------------------------------------------- 1 | //! Test suite for the Web and headless browsers. 2 | 3 | #![cfg(target_arch = "wasm32")] 4 | 5 | extern crate wasm_bindgen_test; 6 | use wasm_bindgen_test::*; 7 | 8 | wasm_bindgen_test_configure!(run_in_browser); 9 | 10 | #[wasm_bindgen_test] 11 | fn pass() { 12 | assert_eq!(1 + 1, 2); 13 | } 14 | -------------------------------------------------------------------------------- /src/Settings/SettingsSlice.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import _ from 'lodash'; 3 | import { RootState } from '../Store/store'; 4 | import { Dispatch } from '@reduxjs/toolkit'; 5 | type Networks = 'Bitcoin' | 'Regtest' | 'Testnet' | 'Signet'; 6 | type Settings = { 7 | display: DisplaySettings; 8 | bitcoin: { network: Networks }; 9 | }; 10 | 11 | type DisplaySettings = { 12 | animation_speed: 'Disabled' | { Enabled: number }; 13 | node_polling_freq: number; 14 | satoshis: 15 | | { BitcoinAfter: number } 16 | | { AlwaysSats: null } 17 | | { AlwaysBitcoin: null }; 18 | }; 19 | export type SettingsStateType = { 20 | settings: Settings; 21 | }; 22 | 23 | function default_state(): SettingsStateType { 24 | return { 25 | settings: { 26 | display: { 27 | animation_speed: 'Disabled', 28 | node_polling_freq: 0, 29 | satoshis: { AlwaysBitcoin: null }, 30 | }, 31 | bitcoin: { 32 | network: 'Bitcoin', 33 | }, 34 | }, 35 | }; 36 | } 37 | 38 | export async function poll_settings(dispatch: Dispatch) { 39 | dispatch( 40 | load_settings({ 41 | settings: { 42 | display: (await window.electron.load_settings_sync( 43 | 'display' 44 | )) as unknown as any, 45 | bitcoin: (await window.electron.load_settings_sync( 46 | 'bitcoin' 47 | )) as unknown as any, 48 | }, 49 | }) 50 | ); 51 | } 52 | export const settingsSlice = createSlice({ 53 | name: 'Settings', 54 | initialState: default_state(), 55 | reducers: { 56 | load_settings: (state, action: PayloadAction) => { 57 | state = _.merge(state, action.payload); 58 | }, 59 | }, 60 | }); 61 | 62 | export const { load_settings } = settingsSlice.actions; 63 | 64 | export const selectMaxSats: (state: RootState) => number = ( 65 | state: RootState 66 | ) => { 67 | if (!state.settingsReducer.settings.display.satoshis) return 0; 68 | if ('BitcoinAfter' in state.settingsReducer.settings.display.satoshis) 69 | return state.settingsReducer.settings.display.satoshis.BitcoinAfter; 70 | else if ('AlwaysSats' in state.settingsReducer.settings.display.satoshis) 71 | return 100000000 * 21000000; 72 | return 0; 73 | }; 74 | 75 | export const selectNodePollFreq: (state: RootState) => number = ( 76 | state: RootState 77 | ) => { 78 | return state.settingsReducer.settings.display.node_polling_freq; 79 | }; 80 | 81 | export const selectNetwork: (state: RootState) => Networks = ( 82 | state: RootState 83 | ) => state.settingsReducer.settings.bitcoin.network; 84 | 85 | export const selectAnimateFlow: (state: RootState) => number = ( 86 | state: RootState 87 | ) => { 88 | if (state.settingsReducer.settings.display.animation_speed === 'Disabled') 89 | return 0; 90 | return state.settingsReducer.settings.display.animation_speed.Enabled; 91 | }; 92 | 93 | export const settingsReducer = settingsSlice.reducer; 94 | -------------------------------------------------------------------------------- /src/Store/store.ts: -------------------------------------------------------------------------------- 1 | import { Action, configureStore, ThunkAction } from '@reduxjs/toolkit'; 2 | import { useDispatch } from 'react-redux'; 3 | import appReducer from '../AppSlice'; 4 | import { dataReducer } from '../Data/DataSlice'; 5 | import { simulationReducer } from '../Data/SimulationSlice'; 6 | import { settingsReducer } from '../Settings/SettingsSlice'; 7 | import { contractCreatorReducer } from '../UX/ContractCreator/ContractCreatorSlice'; 8 | import entityReducer from '../UX/Entity/EntitySlice'; 9 | import modalReducer from '../UX/ModalSlice'; 10 | import { walletReducer } from '../Wallet/Slice/Reducer'; 11 | 12 | export const store = configureStore({ 13 | reducer: { 14 | entityReducer, 15 | appReducer, 16 | contractCreatorReducer, 17 | simulationReducer, 18 | settingsReducer, 19 | modalReducer, 20 | dataReducer, 21 | walletReducer, 22 | }, 23 | devTools: true, 24 | }); 25 | 26 | export type AppDispatch = typeof store.dispatch; 27 | export type RootState = ReturnType; 28 | export type AppThunk = ThunkAction< 29 | ReturnType, 30 | RootState, 31 | unknown, 32 | Action 33 | >; 34 | 35 | export const useAppDispatch: () => AppDispatch = useDispatch; 36 | -------------------------------------------------------------------------------- /src/UX/AppNavbar.css: -------------------------------------------------------------------------------- 1 | .Draggable { 2 | -webkit-user-select: none; 3 | -webkit-app-region: drag; 4 | } 5 | 6 | .AppNavBar { 7 | width: 100%; 8 | height: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /src/UX/Chat/Channel.css: -------------------------------------------------------------------------------- 1 | .Typing { 2 | display: grid; 3 | grid-template-columns: 1fr max-content; 4 | width: 100%; 5 | } 6 | 7 | .ChatBox { 8 | display: grid; 9 | height: 100%; 10 | max-height: 100%; 11 | grid-template-rows: max-content 1fr max-content; 12 | } 13 | 14 | .MessageArea { 15 | max-height: 80vh; 16 | overflow-y: scroll; 17 | overflow-x: hidden; 18 | overflow-wrap: anywhere; 19 | } 20 | -------------------------------------------------------------------------------- /src/UX/Chat/Channel.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, FilledInput, FormGroup } from '@mui/material'; 2 | import * as React from 'react'; 3 | import './Channel.css'; 4 | import FormControl, { useFormControl } from '@mui/material/FormControl'; 5 | import _ from 'lodash'; 6 | 7 | export function Channel(props: { channel_id: string; close: () => void }) { 8 | const [messages, set_messages] = React.useState([]); 9 | React.useEffect(() => { 10 | let cancel: ReturnType; 11 | let since = 0; 12 | let all_messages: any[] = []; 13 | async function f() { 14 | const last = since; 15 | /// todo: small concurrency bug here can lead to messages appearing twice 16 | const m = await window.electron.chat.list_messages_channel( 17 | props.channel_id, 18 | last 19 | ); 20 | if (m.length) { 21 | since = Math.max( 22 | _(m) 23 | .map((msg) => msg.received_time) 24 | .max(), 25 | since 26 | ); 27 | all_messages = [...all_messages, ...m]; 28 | set_messages(all_messages); 29 | } 30 | cancel = setTimeout(f, 1000); 31 | } 32 | cancel = setTimeout(f, 0); 33 | return () => { 34 | clearTimeout(cancel); 35 | }; 36 | }, []); 37 | const msg_area = React.useRef(null); 38 | React.useEffect(() => { 39 | if (!msg_area.current) return; 40 | msg_area.current.scrollTop = msg_area.current.scrollHeight; 41 | }, [messages]); 42 | 43 | return ( 44 | 45 | 46 | 47 | {messages.map((m, i) => ( 48 |
49 | {m.nickname}: {m.body} 50 |
51 | ))} 52 |
53 | 54 |
55 | 56 |
57 |
58 | ); 59 | } 60 | 61 | function Typing(props: { channel_id: string }) { 62 | const [typed, set_typed] = React.useState(''); 63 | const { focused } = useFormControl() || {}; 64 | return ( 65 | { 70 | ev.preventDefault(); 71 | }} 72 | > 73 | 74 | 75 | set_typed(ev.currentTarget.value)} 78 | value={typed} 79 | autoFocus 80 | type="text" 81 | /> 82 | 99 | 100 | 101 | 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /src/UX/Chat/Chat.css: -------------------------------------------------------------------------------- 1 | .Chat { 2 | display: grid; 3 | height: 100%; 4 | width: 100%; 5 | grid-template-columns: 0.5fr 1fr; 6 | column-gap: 2em; 7 | } 8 | -------------------------------------------------------------------------------- /src/UX/Chat/Chat.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button } from '@mui/material'; 2 | import * as React from 'react'; 3 | import VisibilityIcon from '@mui/icons-material/Visibility'; 4 | import { 5 | DataGrid, 6 | GridActionsCellItem, 7 | GridColumns, 8 | GridToolbarContainer, 9 | } from '@mui/x-data-grid'; 10 | import './Chat.css'; 11 | import { Channel } from './Channel'; 12 | import { Add } from '@mui/icons-material'; 13 | import { NewNickname } from './NewNickname'; 14 | import { NewChannel } from './NewChannel'; 15 | 16 | export function Chat() { 17 | React.useEffect(() => { 18 | async function f() { 19 | await window.electron.chat.init(); 20 | } 21 | f(); 22 | }); 23 | return ( 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | const UserGrid: GridColumns = [ 32 | { 33 | field: 'actions-load', 34 | type: 'actions', 35 | flex: 0.2, 36 | getActions: (params) => [ 37 | } 40 | label="Open" 41 | onClick={() => { 42 | // todo: 43 | }} 44 | />, 45 | ], 46 | }, 47 | { 48 | field: 'nickname', 49 | headerName: 'NickName', 50 | minWidth: 100, 51 | type: 'text', 52 | flex: 1, 53 | }, 54 | { 55 | field: 'key', 56 | headerName: 'Key Hash', 57 | minWidth: 100, 58 | type: 'text', 59 | flex: 1, 60 | }, 61 | ]; 62 | function Users() { 63 | const [users, set_users] = React.useState< 64 | { nickname: string; key: string }[] 65 | >([]); 66 | React.useEffect(() => { 67 | let cancel: ReturnType; 68 | async function f() { 69 | await window.electron.chat.init(); 70 | set_users(await window.electron.chat.list_users()); 71 | cancel = setTimeout(f, 5000); 72 | } 73 | cancel = setTimeout(f, 0); 74 | return () => { 75 | clearTimeout(cancel); 76 | }; 77 | }, []); 78 | const [add_new_user, set_add_new_user] = React.useState(false); 79 | function CustomToolbar() { 80 | return ( 81 | 82 | 85 | 86 | ); 87 | } 88 | return ( 89 |
90 | set_add_new_user(false)} 93 | > 94 | 95 | { 98 | return { id: v.key, ...v }; 99 | })} 100 | columns={UserGrid} 101 | disableExtendRowFullWidth={false} 102 | columnBuffer={3} 103 | pageSize={10} 104 | rowsPerPageOptions={[5]} 105 | disableColumnSelector 106 | disableSelectionOnClick 107 | /> 108 |
109 | ); 110 | } 111 | 112 | function Channels() { 113 | const [channels, set_channels] = React.useState<{ channel_id: string }[]>( 114 | [] 115 | ); 116 | React.useEffect(() => { 117 | let cancel: ReturnType; 118 | async function f() { 119 | set_channels(await window.electron.chat.list_channels()); 120 | cancel = setTimeout(f, 5000); 121 | } 122 | cancel = setTimeout(f, 0); 123 | return () => { 124 | clearTimeout(cancel); 125 | }; 126 | }, []); 127 | const [channel, set_channel] = React.useState(null); 128 | const [add_new_channel, set_add_new_channel] = 129 | React.useState(false); 130 | const ChannelColumns: GridColumns = [ 131 | { 132 | field: 'actions-load', 133 | type: 'actions', 134 | flex: 0.2, 135 | getActions: (params) => [ 136 | } 139 | label="Open" 140 | onClick={() => { 141 | typeof params.id === 'string' && set_channel(params.id); 142 | }} 143 | />, 144 | ], 145 | }, 146 | { 147 | field: 'channel_id', 148 | headerName: 'Channel', 149 | minWidth: 100, 150 | type: 'text', 151 | flex: 1, 152 | }, 153 | ]; 154 | function CustomToolbar() { 155 | return ( 156 | 157 | 160 | 161 | ); 162 | } 163 | return ( 164 |
165 | set_add_new_channel(false)} 168 | /> 169 | {channel === null && ( 170 | { 173 | return { id: v.channel_id, ...v }; 174 | })} 175 | columns={ChannelColumns} 176 | disableExtendRowFullWidth={false} 177 | columnBuffer={3} 178 | pageSize={10} 179 | rowsPerPageOptions={[5]} 180 | disableColumnSelector 181 | disableSelectionOnClick 182 | /> 183 | )} 184 | {channel !== null && ( 185 | { 188 | set_channel(null); 189 | }} 190 | > 191 | )} 192 |
193 | ); 194 | } 195 | -------------------------------------------------------------------------------- /src/UX/Chat/NewChannel.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogContentText, 7 | DialogTitle, 8 | TextField, 9 | } from '@mui/material'; 10 | import React from 'react'; 11 | 12 | export function NewChannel(props: { show: boolean; hide: () => void }) { 13 | const [value, set_value] = React.useState(null); 14 | return ( 15 | 16 | Create a new channel 17 | 18 | 19 | The name of the new channel to create... 20 | 21 | set_value(ev.currentTarget.value)} 23 | value={value} 24 | autoFocus 25 | margin="dense" 26 | label="Name" 27 | name="name" 28 | type="text" 29 | fullWidth 30 | variant="standard" 31 | /> 32 | 33 | 34 | 35 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/UX/Chat/NewNickname.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogContentText, 7 | DialogTitle, 8 | TextField, 9 | } from '@mui/material'; 10 | import React from 'react'; 11 | 12 | export function NewNickname(props: { show: boolean; hide: () => void }) { 13 | const [value, set_value] = React.useState(null); 14 | const [key_value, set_key_value] = React.useState(null); 15 | return ( 16 | 17 | Create a new User 18 | 19 | 20 | The key and nickname for the new person. Keys must be 21 | unique. 22 | 23 | set_value(ev.currentTarget.value)} 25 | value={value} 26 | autoFocus 27 | margin="dense" 28 | label="Name" 29 | name="name" 30 | type="text" 31 | fullWidth 32 | variant="standard" 33 | /> 34 | set_key_value(ev.currentTarget.value)} 36 | value={key_value} 37 | autoFocus 38 | margin="dense" 39 | label="Key Hash" 40 | name="keyhash" 41 | type="text" 42 | fullWidth 43 | variant="standard" 44 | /> 45 | 46 | 47 | 48 | 62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/UX/ContractCreator/ContractCreatorSlice.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { createSelectorCreator, defaultMemoize } from 'reselect'; 3 | 4 | import { RootState } from '../../Store/store'; 5 | import { 6 | API, 7 | APIPath, 8 | Continuation, 9 | ContinuationTable, 10 | } from '../../common/preload_interface'; 11 | import _ from 'lodash'; 12 | type CreatorStateType = { 13 | apis: null | API; 14 | selected_api: keyof API | null; 15 | show: boolean; 16 | continuations: ContinuationTable; 17 | }; 18 | function default_state(): CreatorStateType { 19 | return { 20 | apis: null, 21 | show: false, 22 | selected_api: null, 23 | continuations: {}, 24 | }; 25 | } 26 | 27 | export const contractCreatorSlice = createSlice({ 28 | name: 'ContractCreator', 29 | initialState: default_state(), 30 | reducers: { 31 | set_apis: (state, action: PayloadAction) => { 32 | state.apis = action.payload; 33 | }, 34 | select_api: (state, action: PayloadAction) => { 35 | state.selected_api = action.payload; 36 | }, 37 | show_apis: (state, action: PayloadAction) => { 38 | state.show = action.payload; 39 | }, 40 | set_continuations: ( 41 | state, 42 | action: PayloadAction 43 | ) => { 44 | state.continuations = action.payload; 45 | }, 46 | }, 47 | }); 48 | export const { show_apis, set_apis, select_api, set_continuations } = 49 | contractCreatorSlice.actions; 50 | 51 | //export const register = (dispatch: Dispatch) => { 52 | // window.electron.register_callback('create_contracts', (apis: API) => { 53 | // dispatch(set_apis(apis)); 54 | // dispatch(show_apis(true)); 55 | // }); 56 | //}; 57 | const selectAPIs = (rs: RootState): API | null => { 58 | return rs.contractCreatorReducer.apis; 59 | }; 60 | const selectSelectedAPI = (rs: RootState): keyof API | null => { 61 | return rs.contractCreatorReducer.selected_api; 62 | }; 63 | 64 | const createDeepEqualSelector = createSelectorCreator( 65 | defaultMemoize, 66 | _.isEqual 67 | ); 68 | 69 | export const selectAPI = createDeepEqualSelector( 70 | [selectAPIs, selectSelectedAPI], 71 | (apis, key) => (apis === null || key === null ? null : apis[key] ?? null) 72 | ); 73 | export const selectAPIEntries = createDeepEqualSelector([selectAPIs], (apis) => 74 | Object.entries(apis ?? {}) 75 | ); 76 | 77 | export const showAPIs = (rs: RootState): boolean => { 78 | return rs.contractCreatorReducer.show; 79 | }; 80 | 81 | export const selectContinuation = ( 82 | rs: RootState 83 | ): ((out: string) => null | Record) => { 84 | return (s: string) => { 85 | const v = rs.contractCreatorReducer.continuations[s]; 86 | return v ?? null; 87 | }; 88 | }; 89 | 90 | export const contractCreatorReducer = contractCreatorSlice.reducer; 91 | -------------------------------------------------------------------------------- /src/UX/ContractCreator/CreateContractModal.css: -------------------------------------------------------------------------------- 1 | .PluginPage { 2 | overflow: scroll; 3 | height: 100vh; 4 | width: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /src/UX/ContractCreator/CreateContractModal.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Paper, Typography } from '@mui/material'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { selectAPI, select_api } from './ContractCreatorSlice'; 4 | import { PluginSelector } from './SapioPluginPicker/PluginSelector'; 5 | import * as React from 'react'; 6 | import './CreateContractModal.css'; 7 | function CreateContractModalInner() { 8 | const dispatch = useDispatch(); 9 | const selected = useSelector(selectAPI); 10 | const unselect = 11 | selected === null ? null : ( 12 | 13 | ); 14 | 15 | return ( 16 | 17 |
18 | {unselect} 19 | Applications 20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | export const CreateContractModal = React.memo(CreateContractModalInner); 27 | -------------------------------------------------------------------------------- /src/UX/ContractCreator/LoadHexModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { load_new_model } from '../../AppSlice'; 4 | 5 | import Dialog from '@mui/material/Dialog'; 6 | import DialogActions from '@mui/material/DialogActions'; 7 | import DialogContent from '@mui/material/DialogContent'; 8 | import DialogContentText from '@mui/material/DialogContentText'; 9 | import DialogTitle from '@mui/material/DialogTitle'; 10 | import { Button, TextField } from '@mui/material'; 11 | import { close_modal } from '../ModalSlice'; 12 | 13 | const FIELD_NAME = 'data'; 14 | export function LoadHexModal(props: { show: boolean }) { 15 | const formRef = React.useRef(null); 16 | const dispatch = useDispatch(); 17 | function handleSubmit() { 18 | if (formRef.current) { 19 | const elt = formRef.current.elements.namedItem(FIELD_NAME); 20 | if (elt) { 21 | // TODO: More robust type corercion 22 | if ((elt as any).value) { 23 | dispatch(load_new_model(JSON.parse((elt as any).value))); 24 | } 25 | } 26 | } 27 | dispatch(close_modal()); 28 | } 29 | return ( 30 | dispatch(close_modal())}> 31 | Paste Hex JSON 32 | 33 | 34 | 35 |

The data pasted must be output through Sapio Studio.

36 |
37 |
38 | 39 | 40 |
41 | 42 | 43 | 44 | 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/UX/ContractCreator/SapioCompilerModal.tsx: -------------------------------------------------------------------------------- 1 | import { Button, TextField } from '@mui/material'; 2 | import Dialog from '@mui/material/Dialog'; 3 | import DialogActions from '@mui/material/DialogActions'; 4 | import DialogContent from '@mui/material/DialogContent'; 5 | import DialogTitle from '@mui/material/DialogTitle'; 6 | import React, { FormEventHandler } from 'react'; 7 | import { useDispatch } from 'react-redux'; 8 | import { close_modal } from '../ModalSlice'; 9 | 10 | export function SapioCompilerModal(props: { show: boolean }) { 11 | const dispatch = useDispatch(); 12 | const handleSubmit: FormEventHandler = (event) => { 13 | event.preventDefault(); 14 | event.stopPropagation(); 15 | const form = event.currentTarget; 16 | // triggers reconnect 17 | dispatch(close_modal()); 18 | }; 19 | return ( 20 | dispatch(close_modal())}> 21 | Set Contract Generator URL 22 | 23 |
24 | 30 | 31 | 32 |
33 | 34 | 35 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/UX/ContractCreator/SapioPluginPicker/PluginForm.css: -------------------------------------------------------------------------------- 1 | .PluginForm { 2 | display: grid; 3 | grid-template-columns: 10% 1fr 10%; 4 | padding-bottom: 100px; 5 | padding-top: 20; 6 | } 7 | -------------------------------------------------------------------------------- /src/UX/ContractCreator/SapioPluginPicker/PluginForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Button } from '@mui/material'; 4 | import Form, { ISubmitEvent } from '@rjsf/core'; 5 | import { create_contract_of_type, switch_showing } from '../../../AppSlice'; 6 | import { useAppDispatch } from '../../../Store/store'; 7 | import { show_apis } from '../ContractCreatorSlice'; 8 | import './PluginForm.css'; 9 | import { logo_image, Plugin } from './PluginTile'; 10 | 11 | interface PluginFormProps { 12 | app: Plugin; 13 | } 14 | export function PluginForm(props: PluginFormProps) { 15 | const dispatch = useAppDispatch(); 16 | const handleSubmit = async (event: ISubmitEvent, type: string) => { 17 | const formData = event.formData; 18 | dispatch(switch_showing('ContractViewer')); 19 | await dispatch( 20 | create_contract_of_type(type, null, JSON.stringify(formData)) 21 | ); 22 | dispatch(show_apis(false)); 23 | }; 24 | const [data, set_data] = React.useState({}); 25 | const handleClick = async () => { 26 | const s = await navigator.clipboard.readText(); 27 | set_data(JSON.parse(s)); 28 | }; 29 | const copyInput = () => { 30 | navigator.clipboard.writeText(JSON.stringify(props.app.api)); 31 | }; 32 | return ( 33 |
34 |
35 |
36 | {logo_image(props.app)} 37 | 38 | 39 |
) => 43 | handleSubmit(e, props.app.key) 44 | } 45 | >
46 |
47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/UX/ContractCreator/SapioPluginPicker/PluginSelector.css: -------------------------------------------------------------------------------- 1 | .PluginSelectorGrid { 2 | display: grid; 3 | grid-template-columns: 20px 1fr 20px; 4 | } 5 | 6 | .PluginSelectorGrid > div { 7 | display: grid; 8 | grid-template-columns: auto auto auto; 9 | grid-auto-flow: row; 10 | justify-content: center; 11 | text-align: center; 12 | } 13 | 14 | .back { 15 | } 16 | 17 | .middle { 18 | } 19 | 20 | .forward { 21 | } 22 | 23 | .PluginTile { 24 | padding: 20px; 25 | } 26 | -------------------------------------------------------------------------------- /src/UX/ContractCreator/SapioPluginPicker/PluginSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './PluginSelector.css'; 3 | import { PluginTile } from './PluginTile'; 4 | import { PluginForm } from './PluginForm'; 5 | import { selectAPI, selectAPIEntries } from '../ContractCreatorSlice'; 6 | import { useDispatch, useSelector } from 'react-redux'; 7 | export function PluginSelector() { 8 | const dispatch = useDispatch(); 9 | const selected = useSelector(selectAPI); 10 | const all_apis = useSelector(selectAPIEntries); 11 | 12 | if (selected !== null) { 13 | return ( 14 |
15 | 16 |
17 | ); 18 | } else { 19 | const tiles = Array.from(all_apis, ([name, app], i) => ( 20 | 21 | )); 22 | return ( 23 |
24 |
25 |
{tiles}
26 |
27 |
28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/UX/ContractCreator/SapioPluginPicker/PluginTile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { JSONSchema7 } from 'json-schema'; 3 | import { useDispatch } from 'react-redux'; 4 | import { select_api } from '../ContractCreatorSlice'; 5 | 6 | interface TileProps { 7 | app: Plugin; 8 | } 9 | export interface Plugin { 10 | api: JSONSchema7; 11 | name: string; 12 | key: string; 13 | logo: string; 14 | } 15 | export function logo_image(app: Plugin) { 16 | const logo = 'data:image/png;base64,' + app.logo; 17 | return ( 18 |
27 | logo 28 |
29 | ); 30 | } 31 | export function PluginTile(props: TileProps) { 32 | const dispatch = useDispatch(); 33 | return ( 34 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/UX/ContractCreator/SaveHexModal.tsx: -------------------------------------------------------------------------------- 1 | import { Button, TextField } from '@mui/material'; 2 | import Dialog from '@mui/material/Dialog'; 3 | import DialogActions from '@mui/material/DialogActions'; 4 | import DialogContent from '@mui/material/DialogContent'; 5 | import DialogTitle from '@mui/material/DialogTitle'; 6 | import { useDispatch } from 'react-redux'; 7 | import { ContractModel } from '../../Data/ContractManager'; 8 | import { TXIDAndWTXIDMap, txid_buf_to_string } from '../../util'; 9 | import { close_modal } from '../ModalSlice'; 10 | import * as React from 'react'; 11 | 12 | interface IProps { 13 | contract: ContractModel; 14 | show: boolean; 15 | } 16 | export function SaveHexModal(props: IProps) { 17 | const dispatch = useDispatch(); 18 | const non_phantoms = props.contract.txn_models.filter((item) => { 19 | return ( 20 | -1 !== 21 | item.tx.ins.findIndex((inp) => 22 | TXIDAndWTXIDMap.has_by_txid( 23 | props.contract.txid_map, 24 | txid_buf_to_string(inp.hash) 25 | ) 26 | ) 27 | ); 28 | }); 29 | const data = JSON.stringify( 30 | { 31 | program: non_phantoms.map((t) => t.get_json()), 32 | }, 33 | undefined, 34 | 4 35 | ); 36 | const handleSave = () => { 37 | window.electron.save_contract(data); 38 | }; 39 | return ( 40 | dispatch(close_modal())} 43 | fullScreen 44 | > 45 | Contract JSON 46 | 47 |
e.preventDefault()}> 48 | 60 | 61 |
62 | 63 | 64 | 65 | 66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/UX/ContractCreator/ViewContractModal.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Select } from '@mui/material'; 2 | import Dialog from '@mui/material/Dialog'; 3 | import DialogActions from '@mui/material/DialogActions'; 4 | import DialogContent from '@mui/material/DialogContent'; 5 | import DialogTitle from '@mui/material/DialogTitle'; 6 | import React from 'react'; 7 | import { useDispatch } from 'react-redux'; 8 | import { BitcoinNodeManager } from '../../Data/BitcoinNode'; 9 | import { close_modal } from '../ModalSlice'; 10 | 11 | export function ViewContractModal(props: { 12 | show: boolean; 13 | bitcoin_node_manager: BitcoinNodeManager; 14 | }) { 15 | const dispatch = useDispatch(); 16 | return ( 17 | dispatch(close_modal())}> 18 | View Existing Contract 19 | 20 |
21 | { 97 | const idx: number = 98 | parseInt( 99 | witness_selection_form.current?.value ?? '0' 100 | ) ?? 0; 101 | if (idx < props.witnesses.length && idx >= 0) { 102 | setWitness(idx); 103 | } 104 | }} 105 | > 106 | {witness_options} 107 | 108 |
109 |
{witness_display}
110 | 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /src/UX/Entity/Detail/OutpointDetail.css: -------------------------------------------------------------------------------- 1 | .OutpointDetail { 2 | display: grid; 3 | grid-template-columns: 1fr min-content; 4 | } 5 | 6 | .TXIDDetail { 7 | display: grid; 8 | grid-template-columns: 1fr 16em; 9 | } 10 | -------------------------------------------------------------------------------- /src/UX/Entity/Detail/OutpointDetail.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, Tooltip } from '@mui/material'; 2 | import { green } from '@mui/material/colors'; 3 | import DoubleArrowIcon from '@mui/icons-material/DoubleArrow'; 4 | import React from 'react'; 5 | import { useDispatch } from 'react-redux'; 6 | import { select_txn, select_utxo } from '../EntitySlice'; 7 | import Hex from './Hex'; 8 | import './OutpointDetail.css'; 9 | export function OutpointDetail(props: { txid: string; n: number }) { 10 | const dispatch = useDispatch(); 11 | return ( 12 |
13 | 18 | 19 | dispatch(select_txn(props.txid))} 22 | > 23 | 24 | 25 | 26 |
27 | ); 28 | } 29 | 30 | export function RefOutpointDetail(props: { txid: string; n: number }) { 31 | const dispatch = useDispatch(); 32 | return ( 33 |
34 | 39 | 40 | 43 | dispatch( 44 | select_utxo({ 45 | hash: props.txid, 46 | nIn: props.n, 47 | }) 48 | ) 49 | } 50 | > 51 | 52 | 53 | 54 |
55 | ); 56 | } 57 | 58 | export function TXIDDetail(props: { txid: string }) { 59 | return ; 60 | } 61 | -------------------------------------------------------------------------------- /src/UX/Entity/Detail/OutputDetail.css: -------------------------------------------------------------------------------- 1 | .OutputDetail { 2 | display: grid; 3 | grid-template-columns: minmax(4em, 10em) minmax(5em, 1fr) min-content; 4 | column-gap: 0.5em; 5 | } 6 | -------------------------------------------------------------------------------- /src/UX/Entity/Detail/OutputDetail.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, Tooltip } from '@mui/material'; 2 | import { green } from '@mui/material/colors'; 3 | import DoubleArrowIcon from '@mui/icons-material/DoubleArrow'; 4 | import * as Bitcoin from 'bitcoinjs-lib'; 5 | import React from 'react'; 6 | import { useDispatch } from 'react-redux'; 7 | import { UTXOModel } from '../../../Data/UTXO'; 8 | import { PrettyAmountField } from '../../../util'; 9 | import { select_utxo } from '../EntitySlice'; 10 | import Hex from './Hex'; 11 | import './OutputDetail.css'; 12 | 13 | interface OutputDetailProps { 14 | txoutput: UTXOModel; 15 | } 16 | export function OutputDetail(props: OutputDetailProps) { 17 | const dispatch = useDispatch(); 18 | const opts = props.txoutput.getOptions(); 19 | const decomp = 20 | Bitcoin.script.decompile(opts.utxo.script) ?? Buffer.from(''); 21 | const script = Bitcoin.script.toASM(decomp); 22 | 23 | return ( 24 |
25 | 26 | 27 | 28 | 31 | dispatch( 32 | select_utxo({ 33 | hash: opts.txn.get_txid(), 34 | nIn: opts.utxo.index, 35 | }) 36 | ) 37 | } 38 | > 39 | 40 | 41 | 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/UX/Entity/Detail/PSBTDetail.css: -------------------------------------------------------------------------------- 1 | .PSBTDetail { 2 | display: grid; 3 | grid-row-gap: 1em; 4 | } 5 | 6 | .PSBTActions { 7 | display: grid; 8 | grid-template-columns: repeat(4, min-content) max-content; 9 | grid-column-gap: 1em; 10 | } 11 | -------------------------------------------------------------------------------- /src/UX/Entity/Detail/TransactionDetail.css: -------------------------------------------------------------------------------- 1 | .TransactionDetail { 2 | padding: 10px; 3 | display: grid; 4 | row-gap: 1em; 5 | } 6 | 7 | .TransactionDetail > * { 8 | padding-top: 10px; 9 | } 10 | 11 | .serialized-tx { 12 | display: grid; 13 | grid-template-columns: 1fr 16em; 14 | } 15 | 16 | .purpose { 17 | display: grid; 18 | grid-template-columns: 1fr 16em; 19 | } 20 | 21 | .color { 22 | display: grid; 23 | grid-template-columns: 1fr 1fr; 24 | } 25 | 26 | .properties { 27 | display: grid; 28 | grid-template-columns: 1fr 1fr; 29 | grid-auto-rows: auto; 30 | } 31 | -------------------------------------------------------------------------------- /src/UX/Entity/Detail/UTXODetail.css: -------------------------------------------------------------------------------- 1 | .COutPoint { 2 | } 3 | 4 | .COutPoint > * { 5 | padding: 0.5em; 6 | } 7 | 8 | .Spend { 9 | display: grid; 10 | grid-template-columns: 1fr min-content; 11 | } 12 | 13 | .Spend > * { 14 | padding: 0.5em; 15 | } 16 | 17 | .UTXODetail { 18 | padding: 10px; 19 | } 20 | 21 | .UTXODetail > * { 22 | padding-top: 1em; 23 | } 24 | 25 | .UTXODetail > h1, 26 | h3, 27 | h4, 28 | h5 { 29 | text-align: center; 30 | } 31 | 32 | .TXIDDetail { 33 | justify-content: center; 34 | align-items: center; 35 | } 36 | 37 | .TXIDDetail > * { 38 | padding: 0; 39 | width: 100%; 40 | border: 1px transparent none; 41 | } 42 | 43 | .TXIDDetail > * > code { 44 | width: 16em; 45 | max-width: 90%; 46 | } 47 | -------------------------------------------------------------------------------- /src/UX/Entity/EntitySlice.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import * as Bitcoin from 'bitcoinjs-lib'; 3 | import Color from 'color'; 4 | import { BitcoinNodeManager, QueriedUTXO } from '../../Data/BitcoinNode'; 5 | import { ContractModel } from '../../Data/ContractManager'; 6 | import { update_utxomodel, UTXOModel } from '../../Data/UTXO'; 7 | import { AppDispatch, RootState } from '../../Store/store'; 8 | import { hasOwn, Outpoint, outpoint_to_id, TXID } from '../../util'; 9 | export type EntityType = ['TXN', TXID] | ['UTXO', Outpoint] | ['NULL', null]; 10 | type StateType = { 11 | utxos: Record; 12 | flash: string | null; 13 | last_selected: EntityType; 14 | show_entity_viewer: boolean; 15 | colors: Record; 16 | purpose: Record; 17 | }; 18 | function default_state(): StateType { 19 | return { 20 | utxos: {}, 21 | flash: null, 22 | last_selected: ['NULL', null], 23 | show_entity_viewer: false, 24 | colors: {}, 25 | purpose: {}, 26 | }; 27 | } 28 | 29 | export const entitySlice = createSlice({ 30 | name: 'Entity', 31 | initialState: default_state(), 32 | reducers: { 33 | set_custom_color: (state, action: PayloadAction<[string, string]>) => { 34 | state.colors[action.payload[0]] = action.payload[1]; 35 | }, 36 | set_custom_purpose: ( 37 | state, 38 | action: PayloadAction<[string, string]> 39 | ) => { 40 | state.purpose[action.payload[0]] = action.payload[1]; 41 | }, 42 | clear_utxos: (state, action: { type: string }) => { 43 | state.utxos = {}; 44 | }, 45 | select_txn: (state, action: PayloadAction) => { 46 | state.last_selected = ['TXN', action.payload]; 47 | state.show_entity_viewer = true; 48 | }, 49 | select_utxo: (state, action: PayloadAction) => { 50 | state.last_selected = ['UTXO', action.payload]; 51 | state.show_entity_viewer = true; 52 | }, 53 | deselect_entity: (state) => { 54 | state.show_entity_viewer = false; 55 | }, 56 | __flash: (state, action: { type: string; payload: string | null }) => { 57 | if (action.payload) state.flash = action.payload; 58 | }, 59 | __load_utxo: ( 60 | state, 61 | action: { 62 | type: string; 63 | payload: [Outpoint, QueriedUTXO]; 64 | } 65 | ) => { 66 | const id = outpoint_to_id(action.payload[0]); 67 | if (hasOwn(state.utxos, id)) return; 68 | if (action.payload[1]) state.utxos[id] = action.payload[1]; 69 | }, 70 | }, 71 | }); 72 | 73 | /// private API 74 | const { __load_utxo, __flash } = entitySlice.actions; 75 | export const { 76 | deselect_entity, 77 | select_txn, 78 | select_utxo, 79 | clear_utxos, 80 | set_custom_color, 81 | set_custom_purpose, 82 | } = entitySlice.actions; 83 | export const fetch_utxo = 84 | (args: Outpoint) => 85 | async (dispatch: AppDispatch, getState: () => RootState) => { 86 | if (hasOwn(getState().entityReducer.utxos, outpoint_to_id(args))) 87 | return; 88 | const utxo = await BitcoinNodeManager.fetch_utxo(args.hash, args.nIn); 89 | if (utxo) { 90 | dispatch(__load_utxo([args, utxo])); 91 | } 92 | }; 93 | 94 | export const create = 95 | (tx: Bitcoin.Transaction, entity: UTXOModel, contract: ContractModel) => 96 | async (dispatch: AppDispatch, getState: () => RootState) => { 97 | await BitcoinNodeManager.fund_out(tx) 98 | .then((funded) => { 99 | tx = funded; 100 | update_utxomodel(entity); 101 | // TODO: Fix continue APIs, maybe add a Data merge operation 102 | // const data: Data = { 103 | // program: [{ 104 | // txs: contract.txn_models.map((t) => { 105 | // return { linked_psbt: t.get_json() }; 106 | // }), continue_apis: {} 107 | // }], 108 | // }; 109 | // dispatch(load_new_model(data)); 110 | }) 111 | .catch((error) => { 112 | dispatch(__flash(error.message)); 113 | setTimeout(() => { 114 | dispatch(__flash(null)); 115 | }, 3000); 116 | }); 117 | }; 118 | 119 | export const selectUTXO = ( 120 | state: RootState 121 | ): ((id: Outpoint) => QueriedUTXO | null) => { 122 | return (id: Outpoint) => { 123 | const id_s = outpoint_to_id(id); 124 | return state.entityReducer.utxos[id_s] ?? null; 125 | }; 126 | }; 127 | 128 | export const selectUTXOColor: ( 129 | id: Outpoint 130 | ) => (state: RootState) => Color | null = (id) => (state) => { 131 | const color = state.entityReducer.colors[outpoint_to_id(id)]; 132 | if (!color) return null; 133 | return Color(color); 134 | }; 135 | export const selectTXNColor: (id: TXID) => (state: RootState) => Color | null = 136 | (id) => (state) => { 137 | const color = state.entityReducer.colors[id]; 138 | if (!color) return null; 139 | return Color(color); 140 | }; 141 | export const selectTXNPurpose: ( 142 | id: TXID 143 | ) => (state: RootState) => string | null = (id) => (state) => 144 | state.entityReducer.purpose[id] ?? null; 145 | 146 | export const selectUTXOFlash = (state: RootState): string | null => 147 | state.entityReducer.flash; 148 | 149 | export const selectEntityToView = (state: RootState): EntityType => 150 | state.entityReducer.last_selected; 151 | export const selectShouldViewEntity = (state: RootState): boolean => 152 | state.entityReducer.show_entity_viewer; 153 | 154 | export default entitySlice.reducer; 155 | -------------------------------------------------------------------------------- /src/UX/Entity/EntityViewer.css: -------------------------------------------------------------------------------- 1 | /* 2 | From: 3 | https://codepen.io/devuri/pen/aaqZqZ 4 | License: MIT 5 | */ 6 | 7 | /* MODAL FADE LEFT RIGHT BOTTOM */ 8 | 9 | .modal.fade:not(.in).left .modal-dialog { 10 | -webkit-transform: translate3d(-25%, 0, 0); 11 | transform: translate3d(-25%, 0, 0); 12 | } 13 | 14 | .modal.fade:not(.in).right .modal-dialog { 15 | -webkit-transform: translate3d(25%, 0, 0); 16 | transform: translate3d(25%, 0, 0); 17 | } 18 | 19 | .modal.fade:not(.in).bottom .modal-dialog { 20 | -webkit-transform: translate3d(0, 25%, 0); 21 | transform: translate3d(0, 25%, 0); 22 | } 23 | 24 | .modal-right > .modal-dialog { 25 | position: absolute; 26 | right: 0; 27 | top: 0; 28 | margin: 0; 29 | width: 33%; 30 | margin-top: 60px; 31 | min-height: calc(100vh - 60px); 32 | max-height: 100%; 33 | } 34 | 35 | .modal.left .modal-dialog { 36 | position: absolute; 37 | top: 40px; 38 | left: 0; 39 | margin: 0; 40 | } 41 | 42 | .modal.left .modal-dialog.modal-sm { 43 | max-width: 300px; 44 | } 45 | 46 | .modal.left .modal-content, 47 | .modal-right > * > .modal-content { 48 | min-height: calc(100vh - 60px); 49 | border: 0; 50 | } 51 | 52 | .modal-bright-backdrop { 53 | /* So it doesn't oclude the view, nor using inspect element */ 54 | background-color: transparent; 55 | z-index: -1; 56 | } 57 | 58 | .EntityViewer { 59 | overflow-y: scroll; 60 | max-width: 50vw; 61 | min-width: 5vw; 62 | } 63 | 64 | .EntityViewerFrame { 65 | display: grid; 66 | grid-template-columns: 3px max-content; 67 | min-width: 3px; 68 | position: absolute; 69 | right: 0px; 70 | top: 0px; 71 | background-blend-mode: screen; 72 | border-bottom: 1px solid grey; 73 | max-height: 50vh; 74 | overflow-y: scroll; 75 | } 76 | 77 | .EntityViewerResize { 78 | border-left: 1px solid grey; 79 | border-right: 1px solid grey; 80 | } 81 | 82 | .EntityViewerResize:hover { 83 | background-color: green; 84 | border-left: 3px solid green; 85 | border-right: 3px solid green; 86 | cursor: ew-resize; 87 | } 88 | 89 | .modal { 90 | font-family: 'IBM Plex Mono', 'Menlo', 'DejaVu Sans Mono', 91 | 'Bitstream Vera Sans Mono', Courier, monospace; 92 | } 93 | -------------------------------------------------------------------------------- /src/UX/Entity/EntityViewer.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, Tooltip, useTheme } from '@mui/material'; 2 | import { red } from '@mui/material/colors'; 3 | import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; 4 | import React from 'react'; 5 | import { useDispatch } from 'react-redux'; 6 | import { ContractModel } from '../../Data/ContractManager'; 7 | import { TransactionDetail } from './Detail/TransactionDetail'; 8 | import { UTXODetail } from './Detail/UTXODetail'; 9 | import { deselect_entity } from './EntitySlice'; 10 | import './EntityViewer.css'; 11 | import Color from 'color'; 12 | 13 | interface CurrentylViewedEntityProps { 14 | current_contract: ContractModel; 15 | } 16 | 17 | export function CurrentlyViewedEntity(props: CurrentylViewedEntityProps) { 18 | const theme = useTheme(); 19 | const dispatch = useDispatch(); 20 | const [width, setWidth] = React.useState('20em'); 21 | const onMouseUp = (e: MouseEvent) => { 22 | e.preventDefault(); 23 | document.removeEventListener('mousemove', onMouseMove); 24 | document.removeEventListener('mouseup', onMouseUp); 25 | }; 26 | const onMouseMove = (e: MouseEvent) => { 27 | e.preventDefault(); 28 | const width = (window.innerWidth - e.clientX).toString() + 'px'; 29 | setWidth(width); 30 | }; 31 | const onMouseDown: React.MouseEventHandler = (e) => { 32 | e.preventDefault(); 33 | document.addEventListener('mousemove', onMouseMove); 34 | document.addEventListener('mouseup', onMouseUp); 35 | }; 36 | 37 | return ( 38 |
46 |
47 |
48 | 49 | dispatch(deselect_entity())} 52 | > 53 | 54 | 55 | 56 |
62 | 65 | props.current_contract.lookup_utxo_model(a, b) 66 | } 67 | /> 68 | 69 |
70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/UX/Miniscript/Compiler.css: -------------------------------------------------------------------------------- 1 | .MiniscriptCompiler { 2 | padding: 50px; 3 | max-width: 100%; 4 | overflow: hidden; 5 | } 6 | 7 | .CompilerOutput { 8 | max-width: 50%; 9 | } 10 | 11 | .PathRow { 12 | white-space: pre-wrap; 13 | word-break: break-word; 14 | padding: 8px; 15 | text-align: left; 16 | border-bottom: 1px solid #ddd; 17 | } 18 | -------------------------------------------------------------------------------- /src/UX/ModalSlice.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { RootState } from '../Store/store'; 3 | 4 | type Modals = 'ViewContract' | 'LoadHex' | 'SaveHex' | 'SapioServer'; 5 | type StateType = { 6 | open_modal: Modals | null; 7 | }; 8 | function default_state(): StateType { 9 | return { 10 | open_modal: null, 11 | }; 12 | } 13 | 14 | export const modalSlice = createSlice({ 15 | name: 'App', 16 | initialState: default_state(), 17 | reducers: { 18 | close_modal: (state) => { 19 | state.open_modal = null; 20 | }, 21 | open_modal: (state, action: PayloadAction) => { 22 | state.open_modal = action.payload; 23 | }, 24 | }, 25 | }); 26 | 27 | export const { close_modal, open_modal } = modalSlice.actions; 28 | 29 | export const selectModal: (state: RootState) => Modals | null = ( 30 | state: RootState 31 | ) => state.modalReducer.open_modal; 32 | export default modalSlice.reducer; 33 | -------------------------------------------------------------------------------- /src/UX/Modals.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { BitcoinNodeManager } from '../Data/BitcoinNode'; 4 | import { ContractModel } from '../Data/ContractManager'; 5 | import { LoadHexModal } from './ContractCreator/LoadHexModal'; 6 | import { SapioCompilerModal } from './ContractCreator/SapioCompilerModal'; 7 | import { SaveHexModal } from './ContractCreator/SaveHexModal'; 8 | import { ViewContractModal } from './ContractCreator/ViewContractModal'; 9 | import { selectModal } from './ModalSlice'; 10 | 11 | export function Modals(props: { 12 | contract: ContractModel; 13 | bitcoin_node_manager: BitcoinNodeManager; 14 | }) { 15 | const dispatch = useDispatch(); 16 | const which = useSelector(selectModal); 17 | return ( 18 |
19 | 23 | 24 | 27 | 28 | 29 | 30 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/UX/Settings/Settings.css: -------------------------------------------------------------------------------- 1 | .SettingPane { 2 | max-height: 100%; 3 | margin-bottom: 100px; 4 | height: 100%; 5 | } 6 | 7 | .SettingForm { 8 | max-height: 100%; 9 | height: 100%; 10 | } 11 | 12 | .Settings { 13 | max-height: 100%; 14 | display: grid; 15 | height: 100%; 16 | grid-template-columns: max-content auto; 17 | } 18 | 19 | .SettingsPanes { 20 | max-height: 100%; 21 | padding-right: 50px; 22 | overflow: scroll; 23 | height: 100%; 24 | padding-left: 50px; 25 | } 26 | 27 | .SettingsNav { 28 | border-bottom: 1; 29 | border-color: 'divider'; 30 | max-height: 100%; 31 | height: fit-content; 32 | } 33 | -------------------------------------------------------------------------------- /src/Wallet/AvailableBalance.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@mui/material'; 2 | import React from 'react'; 3 | import { BitcoinNodeManager } from '../Data/BitcoinNode'; 4 | 5 | export function AvailableBalance(props: { 6 | bitcoin_node_manager: BitcoinNodeManager; 7 | }) { 8 | const [amount, setAmount] = React.useState(0); 9 | React.useEffect(() => { 10 | let cancel = false; 11 | const update = async () => { 12 | if (cancel) return; 13 | try { 14 | const amt = await props.bitcoin_node_manager.check_balance(); 15 | setAmount(amt); 16 | } catch (err: any) { 17 | console.error(err); 18 | setAmount(0); 19 | } 20 | setTimeout(update, 5000); 21 | }; 22 | 23 | update(); 24 | return () => { 25 | cancel = true; 26 | }; 27 | }, []); 28 | return Amount: {amount}; 29 | } 30 | -------------------------------------------------------------------------------- /src/Wallet/ContractList.tsx: -------------------------------------------------------------------------------- 1 | import { Delete } from '@mui/icons-material'; 2 | import VisibilityIcon from '@mui/icons-material/Visibility'; 3 | import { Typography } from '@mui/material'; 4 | import { DataGrid, GridActionsCellItem, GridColumns } from '@mui/x-data-grid'; 5 | import React from 'react'; 6 | import { useSelector } from 'react-redux'; 7 | import { open_contract_directory, switch_showing } from '../AppSlice'; 8 | import { useAppDispatch } from '../Store/store'; 9 | import { DeleteDialog } from './DeleteDialog'; 10 | import { selectWorkspace } from './Slice/Reducer'; 11 | 12 | export function ContractList(props: { idx: number; value: number }) { 13 | const dispatch = useAppDispatch(); 14 | const [contracts, set_contracts] = React.useState([]); 15 | const [to_delete, set_to_delete] = React.useState(null); 16 | const [trigger_now, set_trigger_now] = React.useState(0); 17 | const workspace = useSelector(selectWorkspace); 18 | React.useEffect(() => { 19 | let cancel = false; 20 | const update = async () => { 21 | if (cancel) return; 22 | 23 | try { 24 | const list = 25 | await window.electron.sapio.compiled_contracts.list( 26 | workspace 27 | ); 28 | set_contracts(list); 29 | } catch (err) { 30 | console.error(err); 31 | set_contracts([]); 32 | } 33 | setTimeout(update, 5000); 34 | }; 35 | 36 | update(); 37 | return () => { 38 | cancel = true; 39 | }; 40 | }, [trigger_now, workspace]); 41 | const contract_rows = contracts.map((id) => { 42 | const [mod, args, time] = id.split('-'); 43 | return { 44 | id, 45 | mod, 46 | args, 47 | time: new Date(parseInt(time!)), 48 | }; 49 | }); 50 | const delete_contract = (fname: string | number) => { 51 | if (typeof fname === 'number') return; 52 | set_to_delete(fname); 53 | }; 54 | 55 | const columns: GridColumns = [ 56 | { 57 | field: 'actions-load', 58 | type: 'actions', 59 | flex: 0.2, 60 | getActions: (params) => [ 61 | } 64 | label="Open" 65 | onClick={() => { 66 | dispatch(switch_showing('ContractViewer')); 67 | dispatch( 68 | open_contract_directory( 69 | typeof params.id === 'number' ? '' : params.id 70 | ) 71 | ); 72 | }} 73 | />, 74 | ], 75 | }, 76 | { 77 | field: 'time', 78 | headerName: 'Time', 79 | minWidth: 100, 80 | type: 'dateTime', 81 | flex: 1, 82 | }, 83 | { 84 | field: 'args', 85 | headerName: 'Args Hash', 86 | width: 100, 87 | flex: 1, 88 | }, 89 | { 90 | field: 'mod', 91 | headerName: 'Module', 92 | width: 100, 93 | type: 'text', 94 | flex: 1, 95 | }, 96 | { 97 | field: 'actions-delete', 98 | type: 'actions', 99 | flex: 0.2, 100 | getActions: (params) => [ 101 | } 104 | label="Delete" 105 | onClick={() => delete_contract(params.id)} 106 | />, 107 | ], 108 | }, 109 | ]; 110 | return ( 111 | 137 | ); 138 | } 139 | -------------------------------------------------------------------------------- /src/Wallet/DeleteDialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogContentText, 7 | DialogTitle, 8 | } from '@mui/material'; 9 | import React from 'react'; 10 | import { useSelector } from 'react-redux'; 11 | import { selectWorkspace } from './Slice/Reducer'; 12 | 13 | export function DeleteDialog(props: { 14 | set_to_delete: () => void; 15 | to_delete: ['workspace', string] | ['contract', string] | null; 16 | reload: () => void; 17 | }) { 18 | const workspace = useSelector(selectWorkspace); 19 | return ( 20 | 21 | Confirm Deletion 22 | 23 | 24 | Confirm deletion of "{props.to_delete}"? File will 25 | be in your trash folder. 26 | 27 | 28 | 29 | 53 | 54 | 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/Wallet/NewWorkspace.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogContentText, 7 | DialogTitle, 8 | TextField, 9 | } from '@mui/material'; 10 | import React from 'react'; 11 | 12 | export function NewWorkspace(props: { 13 | show: boolean; 14 | hide: () => void; 15 | reload: () => void; 16 | }) { 17 | const [value, set_value] = React.useState(null); 18 | return ( 19 | 20 | Create a new Workspace 21 | 22 | 23 | What should the workspace be called? 24 | 25 | set_value(ev.currentTarget.value)} 27 | value={value} 28 | autoFocus 29 | margin="dense" 30 | label="Name" 31 | name="name" 32 | type="text" 33 | fullWidth 34 | variant="standard" 35 | /> 36 | 37 | 38 | 39 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/Wallet/Slice/Reducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { RootState } from '../../Store/store'; 3 | 4 | export type TabIndexes = 0 | 1 | 2 | 3 | 4; 5 | type StateType = { 6 | showing: TabIndexes; 7 | workspace: string; 8 | }; 9 | function default_state(): StateType { 10 | return { 11 | showing: 0, 12 | workspace: 'default', 13 | }; 14 | } 15 | 16 | export const walletSlice = createSlice({ 17 | name: 'Wallet', 18 | initialState: default_state(), 19 | reducers: { 20 | switch_wallet_tab: (state, action: PayloadAction) => { 21 | state.showing = action.payload; 22 | }, 23 | switch_workspace: (state, action: PayloadAction) => { 24 | state.workspace = action.payload; 25 | }, 26 | }, 27 | }); 28 | 29 | export const { switch_wallet_tab, switch_workspace } = walletSlice.actions; 30 | 31 | export const selectWalletTab = (r: RootState) => { 32 | return r.walletReducer.showing; 33 | }; 34 | 35 | export const selectWorkspace = (r: RootState) => { 36 | return r.walletReducer.workspace; 37 | }; 38 | export const walletReducer = walletSlice.reducer; 39 | -------------------------------------------------------------------------------- /src/Wallet/Wallet.css: -------------------------------------------------------------------------------- 1 | .WalletTransactionList { 2 | height: 75%; 3 | } 4 | 5 | .WalletTransactionListInner { 6 | padding-top: 3em; 7 | padding-bottom: 30px; 8 | height: 100%; 9 | display: grid; 10 | grid-template-columns: 10% 80% 10%; 11 | } 12 | 13 | .Wallet { 14 | width: 100%; 15 | height: 100%; 16 | display: grid; 17 | grid-template-rows: max-content auto; 18 | } 19 | 20 | .WalletSpendInner { 21 | display: grid; 22 | grid-template-columns: 10% 80% 10%; 23 | } 24 | 25 | .WalletSpendOuter { 26 | width: 100%; 27 | padding-top: 3em; 28 | } 29 | 30 | .ContractList { 31 | height: 75%; 32 | } 33 | 34 | .ContractListInner { 35 | padding-top: 3em; 36 | padding-bottom: 30px; 37 | height: 100%; 38 | display: grid; 39 | grid-template-columns: 10% 80% 10%; 40 | } 41 | 42 | .WorkspaceList { 43 | height: 75%; 44 | } 45 | 46 | .WorkspaceListInner { 47 | padding-top: 3em; 48 | padding-bottom: 30px; 49 | height: 100%; 50 | display: grid; 51 | grid-template-columns: 10% 80% 10%; 52 | } 53 | -------------------------------------------------------------------------------- /src/Wallet/Wallet.tsx: -------------------------------------------------------------------------------- 1 | import { Tab, Tabs } from '@mui/material'; 2 | import { Box } from '@mui/system'; 3 | import React from 'react'; 4 | import { BitcoinNodeManager } from '../Data/BitcoinNode'; 5 | import './Wallet.css'; 6 | import { ContractList } from './ContractList'; 7 | import { WalletSend } from './WalletSend'; 8 | import { WalletHistory } from './WalletHistory'; 9 | import { Workspaces } from './Workspaces'; 10 | import { useDispatch, useSelector } from 'react-redux'; 11 | import { 12 | selectWalletTab, 13 | switch_wallet_tab, 14 | TabIndexes, 15 | } from './Slice/Reducer'; 16 | 17 | export function Wallet(props: { bitcoin_node_manager: BitcoinNodeManager }) { 18 | const dispatch = useDispatch(); 19 | const idx = useSelector(selectWalletTab); 20 | const handleChange = (_: any, idx: TabIndexes) => { 21 | dispatch(switch_wallet_tab(idx)); 22 | }; 23 | return ( 24 |
25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/Wallet/WalletHistory.tsx: -------------------------------------------------------------------------------- 1 | import { DataGrid, GridColDef, GridValueGetterParams } from '@mui/x-data-grid'; 2 | import React from 'react'; 3 | import { BitcoinNodeManager } from '../Data/BitcoinNode'; 4 | 5 | type TxInfo = { 6 | involvesWatchonly: boolean; // (boolean) Only returns true if imported addresses were involved in transaction. 7 | address: string; // (string) The bitcoin address of the transaction. 8 | category: 'send' | 'receive' | 'generate' | 'immature' | 'orphan'; // (string) The transaction category. 9 | 10 | // "send" Transactions sent. 11 | // "receive" Non-coinbase transactions received. 12 | // "generate" Coinbase transactions received with more than 100 confirmations. 13 | // "immature" Coinbase transactions received with 100 or fewer confirmations. 14 | // "orphan" Orphaned coinbase transactions received. 15 | amount: number; // (numeric) The amount in BTC. This is negative for the 'send' category, and is positive 16 | 17 | // for all other categories 18 | label: string; // (string) A comment for the address/transaction, if any 19 | vout: number; // (numeric) the vout value 20 | fee: number; // (numeric) The amount of the fee in BTC. This is negative and only available for the 21 | 22 | // 'send' category of transactions. 23 | confirmations: number; // (numeric) The number of confirmations for the transaction. Negative confirmations means the 24 | 25 | // transaction conflicted that many blocks ago. 26 | generated: boolean; // (boolean) Only present if transaction only input is a coinbase one. 27 | trusted: boolean; // (boolean) Only present if we consider transaction to be trusted and so safe to spend from. 28 | blockhash: string; // (string) The block hash containing the transaction. 29 | blockheight: number; // (numeric) The block height containing the transaction. 30 | blockindex: number; // (numeric) The index of the transaction in the block that includes it. 31 | blocktime: number; // (numeric) The block time expressed in UNIX epoch time. 32 | txid: string; // (string) The transaction id. 33 | id: string; 34 | walletconflicts: string[]; // (json array) Conflicting transaction ids. 35 | 36 | // (string) The transaction id. 37 | time: number; // (numeric) The transaction time expressed in UNIX epoch time. 38 | timereceived: number; // (numeric) The time received expressed in UNIX epoch time. 39 | comment: string; // (string) If a comment is associated with the transaction, only present if not empty. 40 | 'bip125-replaceable': string; // (string) ("yes|no|unknown") Whether this transaction could be replaced due to BIP125 (replace-by-fee); 41 | 42 | // may be unknown for unconfirmed transactions not in the mempool 43 | abandoned: boolean; // (boolean) 'true' if the transaction has been abandoned (inputs are respendable). Only available for the 44 | }; 45 | export function WalletHistory(props: { 46 | bitcoin_node_manager: BitcoinNodeManager; 47 | value: number; 48 | idx: number; 49 | }) { 50 | const [transactions, setTransactions] = React.useState([]); 51 | React.useEffect(() => { 52 | let cancel = false; 53 | const update = async () => { 54 | if (cancel) return; 55 | 56 | try { 57 | const txns = await props.bitcoin_node_manager.list_transactions( 58 | 10 59 | ); 60 | setTransactions(txns); 61 | } catch (err) { 62 | console.error(err); 63 | setTransactions([]); 64 | } 65 | setTimeout(update, 5000); 66 | }; 67 | 68 | update(); 69 | return () => { 70 | cancel = true; 71 | }; 72 | }, []); 73 | 74 | const columns: GridColDef[] = [ 75 | { field: 'amount', headerName: 'Amount', width: 130, type: 'number' }, 76 | { field: 'category', headerName: 'Category', width: 130 }, 77 | { field: 'txid', headerName: 'TXID', width: 130 }, 78 | { 79 | field: 'blockheight', 80 | headerName: 'Height', 81 | width: 130, 82 | type: 'number', 83 | }, 84 | { 85 | field: 'time', 86 | headerName: 'Time', 87 | width: 130, 88 | type: 'number', 89 | valueGetter: (params: GridValueGetterParams) => { 90 | const d: number = params.row.blocktime; 91 | return d ? new Date(d * 1000).toUTCString() : 'None'; 92 | }, 93 | }, 94 | ]; 95 | 96 | (transactions ?? []).forEach((v) => { 97 | v['id'] = v.txid; 98 | }); 99 | return ( 100 | 121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /src/Wallet/WalletSend.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BitcoinNodeManager } from '../Data/BitcoinNode'; 3 | import { WalletSendDialog } from './WalletSendDialog'; 4 | import { WalletSendForm } from './WalletSendForm'; 5 | 6 | export function WalletSend(props: { 7 | bitcoin_node_manager: BitcoinNodeManager; 8 | value: number; 9 | idx: number; 10 | }) { 11 | const [params, set_params] = React.useState({ amt: -1, to: '' }); 12 | return ( 13 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/Wallet/WalletSendDialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogContentText, 7 | DialogTitle, 8 | } from '@mui/material'; 9 | import React from 'react'; 10 | import { BitcoinNodeManager } from '../Data/BitcoinNode'; 11 | 12 | export function WalletSendDialog(props: { 13 | show: boolean; 14 | amt: number; 15 | to: string; 16 | close: () => void; 17 | bitcoin_node_manager: BitcoinNodeManager; 18 | }) { 19 | return ( 20 | { 23 | props.close(); 24 | }} 25 | > 26 | Confirm Spend 27 | 28 | 29 | Confirm sending 30 | {props.amt} BTC to {props.to} 31 | 32 | 33 | 34 | 41 | 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/Wallet/WalletSendForm.tsx: -------------------------------------------------------------------------------- 1 | import { Button, TextField, Typography } from '@mui/material'; 2 | import { Box } from '@mui/system'; 3 | import React from 'react'; 4 | import { BitcoinNodeManager } from '../Data/BitcoinNode'; 5 | import { AvailableBalance } from './AvailableBalance'; 6 | 7 | export function WalletSendForm(props: { 8 | bitcoin_node_manager: BitcoinNodeManager; 9 | set_params: (a: number, b: string) => void; 10 | }) { 11 | const [address, setAddress] = React.useState(null); 12 | 13 | const get_address = async () => { 14 | try { 15 | const address = await props.bitcoin_node_manager.get_new_address(); 16 | setAddress(address); 17 | } catch (err) { 18 | // console.error(err); 19 | setAddress(null); 20 | } 21 | }; 22 | const handleSubmit: React.FormEventHandler = async ( 23 | event 24 | ) => { 25 | event.preventDefault(); 26 | const amt = event.currentTarget.amount.value; 27 | const to = event.currentTarget.address.value; 28 | props.set_params(amt, to); 29 | event.currentTarget.reset(); 30 | }; 31 | 32 | return ( 33 |
34 |
35 |
36 | 39 | {address && `New Address: ${address}`} 40 | 41 | 47 | 54 | 61 | 62 | 63 |
64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/Wallet/Workspaces.tsx: -------------------------------------------------------------------------------- 1 | import { Add, Delete, FolderOpen } from '@mui/icons-material'; 2 | import { Button, Typography } from '@mui/material'; 3 | import { 4 | DataGrid, 5 | GridActionsCellItem, 6 | GridColumns, 7 | GridToolbarContainer, 8 | } from '@mui/x-data-grid'; 9 | import React from 'react'; 10 | import { useDispatch, useSelector } from 'react-redux'; 11 | import { DeleteDialog } from './DeleteDialog'; 12 | import { NewWorkspace } from './NewWorkspace'; 13 | import { 14 | selectWorkspace, 15 | switch_wallet_tab, 16 | switch_workspace, 17 | } from './Slice/Reducer'; 18 | 19 | export function Workspaces(props: { idx: number; value: number }) { 20 | const dispatch = useDispatch(); 21 | const workspace = useSelector(selectWorkspace); 22 | const [workspaces, set_workspaces] = React.useState([]); 23 | const [to_delete, set_to_delete] = React.useState(null); 24 | const [trigger_now, set_trigger_now] = React.useState(0); 25 | const [show_new_workspace, set_new_workspace] = React.useState(false); 26 | const hide_new_workspace = () => { 27 | set_new_workspace(false); 28 | }; 29 | const reload = () => { 30 | set_trigger_now(trigger_now + 1); 31 | }; 32 | React.useEffect(() => { 33 | let cancel = false; 34 | const update = async () => { 35 | if (cancel) return; 36 | 37 | try { 38 | const list = await window.electron.sapio.workspaces.list(); 39 | set_workspaces(list); 40 | } catch (err) { 41 | console.error(err); 42 | set_workspaces([]); 43 | } 44 | setTimeout(update, 5000); 45 | }; 46 | 47 | update(); 48 | return () => { 49 | cancel = true; 50 | }; 51 | }, [trigger_now]); 52 | const contract_rows = workspaces.map((id) => { 53 | return { 54 | id, 55 | name: id, 56 | }; 57 | }); 58 | const delete_workspace = (fname: string | number) => { 59 | if (typeof fname === 'number') return; 60 | set_to_delete(fname); 61 | }; 62 | 63 | const columns: GridColumns = [ 64 | { 65 | field: 'actions-load', 66 | type: 'actions', 67 | flex: 0.2, 68 | getActions: (params) => [ 69 | } 72 | label="Open" 73 | onClick={() => { 74 | // TODO: Better tabbing? 75 | dispatch(switch_wallet_tab(3)); 76 | typeof params.id === 'string' && 77 | dispatch(switch_workspace(params.id)); 78 | }} 79 | />, 80 | ], 81 | }, 82 | { 83 | field: 'name', 84 | headerName: 'Name', 85 | width: 100, 86 | type: 'text', 87 | flex: 1, 88 | }, 89 | { 90 | field: 'actions-delete', 91 | type: 'actions', 92 | flex: 0.2, 93 | getActions: (params) => [ 94 | } 97 | label="Delete" 98 | onClick={() => delete_workspace(params.id)} 99 | />, 100 | ], 101 | }, 102 | ]; 103 | function CustomToolbar() { 104 | return ( 105 | 106 | 109 | Currenty Active Workspace: {workspace} 110 | 111 | ); 112 | } 113 | return ( 114 | 147 | ); 148 | } 149 | -------------------------------------------------------------------------------- /src/common/chat_interface.d.ts: -------------------------------------------------------------------------------- 1 | export type EnvelopeIn = { 2 | msg: { 3 | Data: string; 4 | }; 5 | channel: string; 6 | sent_time_ms: number; 7 | }; 8 | 9 | export type EnvelopeOut = { 10 | msg: { 11 | Data: string; 12 | }; 13 | channel: string; 14 | key: number[]; 15 | sent_time_ms: number; 16 | signatures: {}; 17 | }; 18 | -------------------------------------------------------------------------------- /src/common/preload_interface.d.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema7 } from 'json-schema'; 2 | import { schemas } from './settings_gen'; 3 | export type Result = { ok: T } | { err: string }; 4 | import { EnvelopeIn } from './chat_interface'; 5 | export const callbacks = { 6 | simulate: 0, 7 | load_hex: 0, 8 | save_hex: 0, 9 | create_contracts: 0, 10 | load_contract: 0, 11 | 'bitcoin-node-bar': 0, 12 | }; 13 | export type ContractArgs = { 14 | arguments: Record; 15 | context: { 16 | amount: number; 17 | network: 'Regtest' | 'Signet' | 'Testnet' | 'Bitcoin'; 18 | effects?: { 19 | effects?: Record< 20 | string, 21 | Record> 22 | >; 23 | }; 24 | }; 25 | }; 26 | 27 | export type Callbacks = keyof typeof callbacks; 28 | export type API = Record< 29 | string, 30 | { name: string; key: string; api: JSONSchema7; logo: string } 31 | >; 32 | export type CreatedContract = { 33 | name: string; 34 | args: ContractArgs; 35 | data: Data; 36 | }; 37 | 38 | export type APIPath = string; 39 | export type Continuation = { 40 | schema: JSONSchema7; 41 | path: APIPath; 42 | }; 43 | export type ObjectMetadata = { simp: Record } & Record< 44 | string, 45 | any 46 | >; 47 | export type DataItem = { 48 | metadata: ObjectMetadata; 49 | /// formatted as txid:idx 50 | out: string; 51 | txs: Array<{ linked_psbt: TransactionData }>; 52 | continue_apis: Record; 53 | }; 54 | export type Data = { 55 | program: Record; 56 | }; 57 | 58 | export type TransactionData = { 59 | psbt: string; 60 | hex: string; 61 | metadata: { 62 | color?: string; 63 | label?: string; 64 | }; 65 | output_metadata?: Array; 66 | }; 67 | 68 | export type UTXOFormatData = { 69 | color: string; 70 | label: string; 71 | simp: Record; 72 | } & Record; 73 | 74 | export type ContinuationTable = Record>; 75 | export type preloads = { 76 | bitcoin_command: ( 77 | command: { 78 | method: string; 79 | parameters: any[]; 80 | }[] 81 | ) => Promise; 82 | // register_callback: (msg: Callbacks, action: (args: any) => void) => () => void; 83 | save_psbt: (psbt: string) => Promise; 84 | 85 | save_contract: (contract: string) => Promise; 86 | fetch_psbt: () => Promise; 87 | write_clipboard: (s: string) => void; 88 | save_settings: ( 89 | which: keyof typeof schemas, 90 | data: string 91 | ) => Promise; 92 | load_settings_sync: (which: keyof typeof schemas) => any; 93 | select_filename: () => Promise; 94 | sapio: { 95 | create_contract: ( 96 | workspace: string, 97 | which: string, 98 | txn: string | null, 99 | args: string 100 | ) => Promise>; 101 | show_config: () => Promise>; 102 | load_wasm_plugin: (workspace: string) => Promise>; 103 | open_contract_from_file: () => Promise>; 104 | load_contract_list: (workspace: string) => Promise>; 105 | compiled_contracts: { 106 | list: (workspace: string) => Promise; 107 | trash: (workspace: string, file_name: string) => Promise; 108 | open: ( 109 | workspace: string, 110 | file_name: string 111 | ) => Promise>; 112 | }; 113 | psbt: { 114 | finalize: (psbt: string) => Promise>; 115 | }; 116 | workspaces: { 117 | init: (workspace: string) => Promise; 118 | list: () => Promise; 119 | trash: (workspace) => Promise; 120 | }; 121 | }; 122 | emulator: { 123 | kill: () => Promise; 124 | start: () => Promise; 125 | read_log: () => Promise; 126 | }; 127 | 128 | chat: { 129 | init: () => Promise; 130 | send: (message: EnvelopeIn) => Promise; 131 | add_user: (name: string, key: string) => Promise; 132 | list_users: () => Promise<{ nickname: string; key: string }[]>; 133 | list_channels: () => Promise<{ channel_id: string }[]>; 134 | list_messages_channel: ( 135 | channel: string, 136 | since: number 137 | ) => Promise; 138 | }; 139 | }; 140 | -------------------------------------------------------------------------------- /src/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapio-lang/sapio-studio/4eb904f75ae6257e2777dd8f9bc4ebe7ac3d91a1/src/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapio-lang/sapio-studio/4eb904f75ae6257e2777dd8f9bc4ebe7ac3d91a1/src/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapio-lang/sapio-studio/4eb904f75ae6257e2777dd8f9bc4ebe7ac3d91a1/src/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sapio-lang/sapio-studio/4eb904f75ae6257e2777dd8f9bc4ebe7ac3d91a1/src/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 4 | 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 5 | 'Helvetica Neue', sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | html, 10 | body { 11 | height: 100%; 12 | } 13 | 14 | #root { 15 | height: 100%; 16 | } 17 | code { 18 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 19 | monospace; 20 | overflow: hidden; 21 | scrollbar-width: none; 22 | } 23 | 24 | .truncate { 25 | white-space: nowrap; 26 | overflow: hidden; 27 | text-overflow: ellipsis; 28 | max-width: 32em; 29 | width: 10vw; 30 | min-width: 100%; 31 | margin: 0; 32 | padding: 0; 33 | } 34 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import App from './App'; 5 | import './index.css'; 6 | import * as serviceWorker from './serviceWorker'; 7 | import { store } from './Store/store'; 8 | 9 | const root = document.getElementById('root'); 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | root 15 | ); 16 | 17 | // If you want your app to work offline and load faster, you can change 18 | // unregister() to register() below. Note this comes with some pitfalls. 19 | // Learn more about service workers: https://bit.ly/CRA-PWA 20 | serviceWorker.unregister(); 21 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then((registration) => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch((error) => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then((response) => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && 112 | contentType.indexOf('javascript') === -1) 113 | ) { 114 | // No service worker found. Probably a different app. Reload the page. 115 | navigator.serviceWorker.ready.then((registration) => { 116 | registration.unregister().then(() => { 117 | window.location.reload(); 118 | }); 119 | }); 120 | } else { 121 | // Service worker found. Proceed as normal. 122 | registerValidSW(swUrl, config); 123 | } 124 | }) 125 | .catch(() => { 126 | console.log( 127 | 'No internet connection found. App is running in offline mode.' 128 | ); 129 | }); 130 | } 131 | 132 | export function unregister() { 133 | if ('serviceWorker' in navigator) { 134 | navigator.serviceWorker.ready.then((registration) => { 135 | registration.unregister(); 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "downlevelIteration": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "incremental": true, 9 | "isolatedModules": true, 10 | "jsx": "react-jsx", 11 | "lib": [ 12 | "dom", 13 | "dom.iterable", 14 | "ES2020" 15 | ], 16 | "module": "commonjs", 17 | "moduleResolution": "node", 18 | "noEmit": false, 19 | "noFallthroughCasesInSwitch": true, 20 | "outDir": "./dist", 21 | "resolveJsonModule": true, 22 | "skipLibCheck": true, 23 | "strict": true, 24 | "noUncheckedIndexedAccess": true, 25 | "target": "ES2020" 26 | }, 27 | "exclude": [ 28 | "build", 29 | "public", 30 | "dist" 31 | ], 32 | "include": [ 33 | "src", 34 | "desktop" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "noEmit": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "noEmit": false, 6 | "module": "commonjs" 7 | } 8 | } --------------------------------------------------------------------------------