Skip to content

Commit 447dbb0

Browse files
authored
Merge pull request #66 from sapio-lang/ux-improvements
Big Batch of Miami Prep
2 parents b042af3 + b04955e commit 447dbb0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+3834
-1374
lines changed

.eslintrc.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@
2020
"plugins": ["react", "@typescript-eslint", "unused-imports"],
2121
"rules": {
2222
"unused-imports/no-unused-imports": "error"
23-
}
23+
},
24+
"ignorePatterns": ["**/pkg/*"]
2425
}

config-overrides.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
};

desktop/chat.ts

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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<void> {
89+
const partial: Partial<EnvelopeOut> = 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+
}

desktop/declarations.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ declare module 'await-spawn' {
66
): Promise<BufferList>;
77
export = spawn;
88
}
9+
10+
declare module 'another-json' {
11+
declare function stringify(js: any): string;
12+
}

desktop/handlers.ts

+58-57
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
get_emulator_log,
44
kill_emulator,
55
sapio,
6+
SapioWorkspace,
67
start_sapio_oracle,
78
} from './sapio';
89
import { readFile, writeFile } from 'fs/promises';
@@ -11,7 +12,9 @@ import { preferences, Prefs } from './settings';
1112
import { get_bitcoin_node } from './bitcoin_rpc';
1213
import RpcError from 'bitcoin-core-ts/dist/src/errors/rpc-error';
1314
import path from 'path';
15+
import { setup_chat } from './chat';
1416
export default function (window: BrowserWindow) {
17+
setup_chat();
1518
ipcMain.handle('bitcoin::command', async (event, arg) => {
1619
const node = await get_bitcoin_node();
1720
try {
@@ -41,23 +44,37 @@ export default function (window: BrowserWindow) {
4144
const contracts = await sapio.list_contracts();
4245
return contracts;
4346
});
44-
ipcMain.handle('sapio::create_contract', async (event, [which, args]) => {
45-
const result = await sapio.create_contract(which, args);
46-
return result;
47-
});
47+
ipcMain.handle(
48+
'sapio::create_contract',
49+
async (event, workspace, [which, psbt, args]) => {
50+
const result = await sapio.create_contract(
51+
workspace,
52+
which,
53+
psbt,
54+
args
55+
);
56+
return result;
57+
}
58+
);
4859

4960
ipcMain.handle('sapio::show_config', async (event) => {
5061
return await sapio.show_config();
5162
});
5263

5364
ipcMain.handle('sapio::load_wasm_plugin', (event) => {
54-
const plugin = dialog.showOpenDialogSync({
55-
properties: ['openFile'],
65+
const plugins = dialog.showOpenDialogSync({
66+
properties: ['openFile', 'multiSelections'],
5667
filters: [{ extensions: ['wasm'], name: 'WASM' }],
5768
});
58-
if (plugin && plugin.length)
59-
return sapio.load_contract_file_name(plugin[0]!);
60-
return { err: 'No Plugin Selected' };
69+
const errs = [];
70+
if (!plugins || !plugins.length) return { err: 'No Plugin Selected' };
71+
for (const plugin of plugins) {
72+
const loaded = sapio.load_contract_file_name(plugin);
73+
if ('err' in loaded) {
74+
return loaded;
75+
}
76+
}
77+
return { ok: null };
6178
});
6279

6380
ipcMain.handle('sapio::open_contract_from_file', (event) => {
@@ -77,62 +94,46 @@ export default function (window: BrowserWindow) {
7794
return { ok: data };
7895
}
7996
});
80-
ipcMain.handle('sapio::compiled_contracts::list', (event) => {
81-
return sapio.list_compiled_contracts();
82-
});
83-
ipcMain.handle('sapio::compiled_contracts::trash', (event, file_name) => {
84-
return sapio.trash_compiled_contract(file_name);
85-
});
97+
ipcMain.handle(
98+
'sapio::compiled_contracts::list',
99+
async (event, workspace) => {
100+
return (
101+
await SapioWorkspace.new(workspace)
102+
).list_compiled_contracts();
103+
}
104+
);
105+
ipcMain.handle(
106+
'sapio::compiled_contracts::trash',
107+
async (event, workspace, file_name) => {
108+
return (
109+
await SapioWorkspace.new(workspace)
110+
).trash_compiled_contract(file_name);
111+
}
112+
);
86113
ipcMain.handle('sapio::psbt::finalize', (event, psbt) => {
87114
return sapio.psbt_finalize(psbt);
88115
});
89-
90116
ipcMain.handle(
91117
'sapio::compiled_contracts::open',
92-
async (event, file_name) => {
93-
const data = JSON.parse(
94-
await readFile(
95-
path.join(
96-
app.getPath('userData'),
97-
'compiled_contracts',
98-
file_name,
99-
'bound.json'
100-
),
101-
{
102-
encoding: 'utf-8',
103-
}
104-
)
105-
);
106-
const args = JSON.parse(
107-
await readFile(
108-
path.join(
109-
app.getPath('userData'),
110-
'compiled_contracts',
111-
file_name,
112-
'args.json'
113-
),
114-
{
115-
encoding: 'utf-8',
116-
}
117-
)
118-
);
119-
const mod = await readFile(
120-
path.join(
121-
app.getPath('userData'),
122-
'compiled_contracts',
123-
file_name,
124-
'module.json'
125-
),
126-
{
127-
encoding: 'utf-8',
128-
}
129-
);
130-
131-
const name = JSON.parse(mod).module;
132-
118+
async (event, workspace_name, file_name) => {
119+
const workspace = await SapioWorkspace.new(workspace_name);
120+
const data = await workspace.read_bound_data_for(file_name);
121+
const name = await workspace.read_module_for(file_name);
122+
const args = await workspace.read_args_for(file_name);
133123
return { ok: { data, name, args } };
134124
}
135125
);
126+
127+
ipcMain.handle('sapio::workspaces::init', async (event, workspace) => {
128+
await SapioWorkspace.new(workspace);
129+
});
130+
ipcMain.handle('sapio::workspaces::list', async (event) => {
131+
return await SapioWorkspace.list_all();
132+
});
133+
ipcMain.handle('sapio::workspaces::trash', async (event, workspace) => {
134+
return (await SapioWorkspace.new(workspace)).trash_workspace(workspace);
135+
});
136+
136137
ipcMain.handle('write_clipboard', (event, s: string) => {
137138
clipboard.writeText(s);
138139
});

desktop/main.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import { custom_sapio_config } from './settings';
55

66
import { createMenu } from './createMenu';
77
import register_handlers from './handlers';
8-
import { start_sapio_oracle } from './sapio';
8+
import { SapioWorkspace, start_sapio_oracle } from './sapio';
99
import { register_devtools } from './devtools';
1010
import { get_bitcoin_node } from './bitcoin_rpc';
1111

1212
let mainWindow: BrowserWindow | null = null;
1313

1414
async function createWindow() {
1515
await get_bitcoin_node();
16+
await SapioWorkspace.new('default');
1617
const startUrl =
1718
process.env.ELECTRON_START_URL ||
1819
url.format({

0 commit comments

Comments
 (0)