Skip to content

Commit e12c134

Browse files
author
Hack.bg Admin
committed
feat(gui): show program and sender utxos
1 parent 5da2c7c commit e12c134

2 files changed

Lines changed: 85 additions & 42 deletions

File tree

gui/index.css

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ pre.sh {
194194
textarea {
195195
gap: 1rem;
196196
font-size: 1.16rem;
197-
min-height: 1.25rem;
198197
line-height: 1.25;
199198
border: none;
200199
box-shadow: none;
@@ -608,7 +607,11 @@ a.help {
608607
border-bottom: 1px solid #fff3;
609608
width: 100%;
610609

611-
.loading { color: #fff; margin: 1rem auto; font-size: 2rem; }
610+
.loading {
611+
color: #fff;
612+
margin: 1rem auto;
613+
font-size: 2rem;
614+
}
612615

613616
.editors {
614617
background: #0002;
@@ -618,13 +621,14 @@ a.help {
618621
font-size: 1rem;
619622
gap: 1px;
620623
padding: 2rem 5rem;
621-
624+
select {
625+
padding-left: 0.25rem
626+
}
622627
& > .box {
623628
background: linear-gradient(to right, #d8d8d8, #c8c8c8);
624629
margin-right: 0.5rem;
625630
border-top: none;
626631
}
627-
628632
.project {
629633
input, textarea, select {
630634
border: none;
@@ -641,9 +645,6 @@ a.help {
641645
}
642646
}
643647
}
644-
645-
select { padding-left: 0.5rem }
646-
647648
}
648649
}
649650
textarea {
@@ -923,7 +924,7 @@ textarea {
923924
textarea {
924925
margin-bottom: 0;
925926
padding-left: 0.25rem;
926-
min-height: 36ch;
927+
min-height: 35ch;
927928
&:focus-visible { outline: none }
928929
}
929930

@@ -1032,7 +1033,7 @@ textarea {
10321033
}
10331034
.phase-form {
10341035
color: #fff;
1035-
max-width: 60ch;
1036+
max-width: 100ch;
10361037
gap: 0;
10371038
display: flex;
10381039
flex-flow: row wrap;
@@ -1109,14 +1110,14 @@ pre.compile-errors {
11091110
/*background: #f8f4;*/
11101111
padding-bottom: 0.5rem;
11111112
border-radius: 3px;
1113+
max-width: 100ch;
11121114
input, select, button {
11131115
/*color: #fff;*/
11141116
font-weight: bold;
11151117
text-shadow: 0 1px 0 #0002;
11161118
background: #0004;
11171119
font-size: 1.15rem;
11181120
padding: 0.5rem;
1119-
margin: 0.25rem -0.25rem;
11201121
flex-grow: 1;
11211122
&[disabled] { opacity: 0.8 }
11221123
&:hover { background: #0005; }
@@ -1130,15 +1131,17 @@ pre.compile-errors {
11301131
/*text-align: right !important*/
11311132
/*}*/
11321133
label {
1133-
padding: 0 0.5rem 0 0;
11341134
border-left: 1px solid #0408;
1135-
margin-left: 0.33rem;
1136-
/*border-bottom: 1px solid #4448;*/
1135+
border: 1px solid #4448;
11371136
align-self: stretch;
11381137
display: flex;
11391138
flex-flow: column nowrap;
11401139
align-items: stretch;
11411140
justify-content: space-between;
1141+
background: #fff0;
1142+
padding: 0.5rem 0.25rem;
1143+
gap: 0.25rem;
1144+
&:focus-within { background: #fff }
11421145
&.pick-chain {
11431146
flex-flow: row wrap;
11441147
align-items: center;
@@ -1156,7 +1159,7 @@ pre.compile-errors {
11561159
}
11571160
}
11581161
button {
1159-
margin-left: 0.5rem;
1162+
margin: 0 0.5rem 0.25rem;
11601163
border: 1px solid #9f84;
11611164
padding: 0.5rem 1rem;
11621165
flex-grow: 0;
@@ -1192,6 +1195,14 @@ pre.compile-errors {
11921195
}
11931196

11941197
}
1198+
.utxos {
1199+
margin: 0;
1200+
padding: 0 0.5rem;
1201+
font-size: 1.125rem;
1202+
display: flex;
1203+
flex-flow: column nowrap;
1204+
li { display: flex; flex-flow: row nowrap; padding: 0.25rem 0; }
1205+
}
11951206

11961207
.docs {
11971208
background: #c0c0c0; overflow: auto; flex-grow: 1

gui/index.ts

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as Monaco from 'npm:monaco-editor';
2-
import Bitcoin from '../platform/Bitcoin/Bitcoin.ts';
2+
import Bitcoin, { Esplora } from '../platform/Bitcoin/Bitcoin.ts';
33
import Html from '../library/Html.ts';
44
import Wasm from './wasm.ts';
55
import scrollTo from 'npm:animated-scroll-to';
@@ -19,7 +19,6 @@ type User = ReturnType<typeof User>;
1919
const chain = Bitcoin.LiquidTestnet(); // Chain handle (initialized once)
2020
const nonSecret = (n: number) => new Uint8Array(new Array(32).fill(n));
2121
const usersByName = {};
22-
const usersByPubkey = {};
2322

2423
/** Launch the editor and user views. */
2524
export default function App ({
@@ -79,12 +78,7 @@ const userSelectors = (host = document) =>
7978
host.querySelectorAll('select.pick-user') as unknown as HTMLSelectElement[];
8079

8180
const addUser = (...[name, options]: Parameters<typeof User>): User => {
82-
if (usersByName[name]) throw new Error(`user already exists: ${name}`);
8381
const user = User(name, options);
84-
const pubkey = user.pubkey;
85-
if (usersByPubkey[pubkey]) throw new Error(`pubkey already exists: ${name}: ${pubkey}`)
86-
usersByName[name] = user;
87-
usersByPubkey[pubkey] = user;
8882
user.balance().then(value => console.debug(user, value));
8983
return user;
9084
}
@@ -313,14 +307,16 @@ const ProgramEditor = ({
313307
users = [],
314308
errors = Html(['pre.compile-errors.collapsible']).firstChild as HTMLElement,
315309
params = Wasm.params(source),
316-
fields = Object.entries(params).map(ProgramField({ id, users, kind: 'param::' })),
310+
fields = Object.entries(params).map(ProgramField({ id, users, kind: 'Parameter: ' })),
317311
compiler = Wasm.compiler({ chain: chain.ID, genesis: chain.GENESIS }),
318312
button = CompileButton(id, { params, compiler, source, errors }),
319313
stage1 = ProgramForm('simf-compile', SelectChain(), ...fields, button),
320314
amount = Label('Commit amount:', ['input.balance[type="number"]', { value: '2345' }]),
321-
sender = SelectSender({ users }),
315+
sender = SelectSender({ chain, users }),
322316
commit = CommitButton(id, { users, params, compiler, source, errors, sender: sender.select }),
323-
stage2 = ProgramForm('simf-commit', ['label', ['strong', 'Sender:'], sender.select], ['div.row', amount, sender.balance], commit),
317+
title2 = ['label', ['strong', 'Fund program from:'], sender.select, sender.utxos],
318+
//utxos2 = UtxoList({ esplora: chain.esplora }),
319+
stage2 = ProgramForm('simf-commit', title2, ['div.row.gap', amount, sender.balance], commit),
324320
}) => Field(id).open(open).content(TextArea(id, source)).content(errors)
325321
.sidebar(['div.phase-form', stage1])
326322
.sidebar(['div.phase-form', stage2])
@@ -338,7 +334,7 @@ const ProgramForm = (id: string, ...rest: unknown[]) =>
338334
[`div.program-form.col.grow#${id}`, ...rest];
339335

340336
const InputAmount = (id: string, name = 'Amount:') =>
341-
['label.gap', ['strong', name], ['input', { id }]];
337+
['label', ['strong', name], ['input', { id }]];
342338

343339
const SelectPubkey = (id: string, {
344340
users = [],
@@ -375,9 +371,11 @@ const SelectPubkeyX = (id: string, {
375371
} = {}) => update();
376372

377373
const SelectSender = ({
374+
chain = null,
378375
users = [],
379376
name = 'Sender:',
380377
balance = InputBalance({ label: ['strong', 'Sender balance:'], p2wpkh: users[0]?.p2wpkh }),
378+
utxos = UtxoList({ esplora: chain.esplora }),
381379
view = Html(['div.col.select-sender', Label(name, ['select.pick-user']), balance]),
382380
input = view.querySelector('input'),
383381
select = Object.assign(view.querySelector('select'), { onchange: () => update() }),
@@ -388,8 +386,11 @@ const SelectSender = ({
388386
select.value = pubkey;
389387
const sender = users.find(x=>x.pubkey === pubkey);
390388
console.log({sender, users, pubkey});
391-
if (sender) getBalances(sender.p2wpkh).then(value=>input.value = value);
392-
return { name, balance, view, input, select, update };
389+
if (sender) Promise.all([
390+
getBalances(sender.p2wpkh).then(value=>input.value = value),
391+
utxos.load(sender.p2wpkh),
392+
])
393+
return { name, balance, view, input, select, utxos, update };
393394
},
394395
} = {}) => update();
395396

@@ -399,7 +400,7 @@ const InputBalance = ({
399400
label = ['strong', 'Balance:'],
400401
input = [`input.balance[balance=${p2wpkh}]`, { disabled: true, value }]
401402
} = {}) => {
402-
return Html(['label.gap', label, input]).firstChild;
403+
return Html(['label.grow', label, input]).firstChild;
403404
}
404405

405406
const collectParams = (id: string, params) => {
@@ -418,25 +419,56 @@ const CompileButton = (id: string, {
418419
chain = Bitcoin.LiquidTestnet,
419420
genesis = chain.GENESIS, // TODO autofetch from block 0
420421
compiler = Wasm.compiler({ chain: chain.ID, genesis }),
422+
balance = Html(['input[disabled]']).firstChild as HTMLInputElement,
423+
utxos = UtxoList({ esplora: chain.esplora }),
421424
button = Button('compile', () => compile()),
422425
errors = Html(['pre.compile-errors.collapsible']).firstChild as HTMLElement,
423-
input = Html(['input', { placeholder: 'compile to get P2TR' }]).firstChild as HTMLInputElement,
424-
view = Html(['label.col.gap.align-stretch', ['label.justify-between.gap',
425-
['div.row.gap.align-center.justify-between', ['strong', 'Program address (P2TR):'], button], input], errors]).firstChild,
426+
input = Html(['input', { placeholder: 'provide parameters and compile to get P2TR' }]).firstChild as HTMLInputElement,
427+
label = ['strong', 'Program address (P2TR):'],
428+
title = ['div.row.gap.align-center.justify-between', label, button],
429+
view = Html(['label.col.gap.align-stretch', ['label.align-end', title, input], utxos, errors]).firstChild,
426430
compile = () => ErrorBoundaryAsync(errors, async () => {
427431
//const { default: Wasm } = await import('./wasm.ts');
428432
errors.innerText = '';
429433
errors.style.display = 'none';
430-
const args = collectParams(id, params);
431-
const program = compiler.compile(source, { args });
432-
const address = program.toJSON().p2tr;
433-
input.value = address;
434-
const balance = await getBalances(address);
435-
console.debug('Balance of', address, 'is', balance);
436-
document.querySelector('#simf-commit .balance').value = String(balance);
434+
const args = collectParams(id, params);
435+
const prog = compiler.compile(source, { args });
436+
const p2tr = prog.toJSON().p2tr;
437+
input.value = p2tr;
438+
return await Promise.all([
439+
utxos.load(p2tr),
440+
getBalances(p2tr).then(value => balance.value = String(value)),
441+
])
437442
})
438443
} = {}) => Object.assign(view, { compile }) as HTMLLabelElement & { compile: typeof compile };
439444

445+
const UtxoList = ({
446+
esplora,
447+
address = null,
448+
utxos = [],
449+
view = Html(['ul.utxos']).firstChild as HTMLElement,
450+
state = () => ({ address, utxos, load }),
451+
load = (addr = address)=> {
452+
address = addr;
453+
if (!address) return view;
454+
console.debug('Loading UTXOs for', address);
455+
view.innerText = 'Loading UTXOs...';
456+
return Object.assign(ErrorBoundaryAsync(view, initUtxoList), state());
457+
async function initUtxoList () {
458+
const utxos = await esplora.getAddressUtxos(addr);
459+
if (utxos.length === 0) {
460+
view.innerText = 'No balance here. Send some funds!';
461+
} else {
462+
view.innerText = '';
463+
for (const utxo of utxos) {
464+
Html.append(view, Html(['li','UTXO ', ['strong', String(utxo.value)], ['code', utxo.txid]]));
465+
}
466+
}
467+
return view;
468+
}
469+
}
470+
}) => Object.assign(view, state());
471+
440472
const CommitButton = (id: string, {
441473
users = [],
442474
source = null,
@@ -449,14 +481,14 @@ const CommitButton = (id: string, {
449481
view = Html(['label', ['strong', 'Commit TX:'], button]).firstChild,
450482
sender = null as HTMLInputElement,
451483
commit = () => ErrorBoundaryAsync(errors, async () => {
452-
const args = collectParams(id, params);
453-
const program = compiler.compile(source, { args });
454-
const address = program.toJSON().p2tr;
484+
const args = collectParams(id, params);
485+
const prog = compiler.compile(source, { args });
486+
const addr = prog.toJSON().p2tr;
455487
const pubkey = sender.value;
456488
const user = users.find(x=>x.pubkey === pubkey);
457489
console.log({sender, pubkey, user, users})
458490
if (!user) throw new Error(`not our pubkey: ${user}`);
459-
const utxos = await chain().esplora.getAddressUtxos(user.p2wpkh) as unknown[];
491+
const utxos = await chain().esplora.getAddressUtxos(addr) as unknown[];
460492
if (utxos.length < 1) throw new Error(`fund the address first: ${user.p2wpkh}`)
461493
}),
462494
} = {}) => Object.assign(view, { commit }) as HTMLLabelElement & { commit: typeof commit };

0 commit comments

Comments
 (0)