Functional Electrum subscriptions
This commit is contained in:
@@ -17,7 +17,7 @@
|
|||||||
v-if="state.selectedFromFunction?.extendedInput"
|
v-if="state.selectedFromFunction?.extendedInput"
|
||||||
v-model="state.selectedFromFunction.models[i]"
|
v-model="state.selectedFromFunction.models[i]"
|
||||||
class="extended-input"
|
class="extended-input"
|
||||||
@input="autoGrow($event.target)"
|
@input="(e) => autoGrow(e.target as HTMLTextAreaElement)"
|
||||||
></textarea>
|
></textarea>
|
||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
@@ -37,11 +37,11 @@
|
|||||||
<div v-if="state.toFunctions.length || state.primitive" class="section output-section">
|
<div v-if="state.toFunctions.length || state.primitive" class="section output-section">
|
||||||
<h2>{{ state.toFunctions.length ? 'Output' : 'Instance' }}</h2>
|
<h2>{{ state.toFunctions.length ? 'Output' : 'Instance' }}</h2>
|
||||||
|
|
||||||
<template v-for="(toFunction, i) of toFunctions" :key="i">
|
<template v-for="(toFunction, i) of state.toFunctions" :key="i">
|
||||||
<div>
|
<div>
|
||||||
<div class="output-box-container">
|
<div class="output-box-container">
|
||||||
<LabelledSection :label="toFunction.label" :content="getPrimitive(() => toFunction.callback(state.primitive))" />
|
<LabelledSection :label="toFunction.label" :content="getPrimitive(() => toFunction.callback(state.primitive!))" />
|
||||||
<CopyButton :content="getPrimitive(() => toFunction.callback(state.primitive))" />
|
<CopyButton :content="getPrimitive(() => toFunction.callback(state.primitive!))" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -51,37 +51,51 @@
|
|||||||
<div class="form-group select-group">
|
<div class="form-group select-group">
|
||||||
<label for="functionSelect">Method</label>
|
<label for="functionSelect">Method</label>
|
||||||
<select v-model="state.selectedMethod">
|
<select v-model="state.selectedMethod">
|
||||||
<option v-for="method of props.methods" :key="method.label" :value="method">{{ method.label }}</option>
|
<option v-for="method of state.methods" :key="method.label" :value="method">{{ method.label }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group input-group" :class="{ 'extended': state.selectedMethod?.extendedInput }">
|
<div v-if="state.selectedMethod" class="form-group input-group" :class="{ 'extended': state.selectedMethod.extendedInput }">
|
||||||
<template v-for="(_input, i) of state.selectedMethod.inputs" :key="i">
|
<template v-for="(_input, i) of state.selectedMethod.inputs" :key="i">
|
||||||
<label for="field">{{ state.selectedMethod.inputs[i] }}</label>
|
<label for="field">{{ state.selectedMethod.inputs[i] }}</label>
|
||||||
<div>
|
<div>
|
||||||
<textarea
|
<div class="input-with-remove">
|
||||||
v-if="state.selectedMethod?.extendedInput"
|
<textarea
|
||||||
v-model="state.selectedMethod.models[i]"
|
v-if="state.selectedMethod.extendedInput"
|
||||||
class="extended-input"
|
v-model="state.selectedMethod.models[i]"
|
||||||
@input="autoGrow($event.target)"
|
class="extended-input"
|
||||||
></textarea>
|
@input="(e) => autoGrow(e.target as HTMLTextAreaElement)"
|
||||||
<input
|
></textarea>
|
||||||
v-else
|
<input
|
||||||
type="text"
|
v-else
|
||||||
v-model="state.selectedMethod.models[i]"
|
type="text"
|
||||||
/>
|
v-model="state.selectedMethod.models[i]"
|
||||||
<div v-if="state.selectedMethod.error" class="text-error">{{ state.selectedMethod.error }}</div>
|
/>
|
||||||
|
<button
|
||||||
|
v-if="state.selectedMethod.dynamicInputs && !state.selectedMethod.inputs[i].length"
|
||||||
|
style="background: transparent"
|
||||||
|
class="remove-input-button"
|
||||||
|
@click="() => removeMethodInput(i)"
|
||||||
|
title="Remove input"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<div v-if="state.selectedMethod.error" class="text-error">{{ state.selectedMethod.error }}</div>
|
||||||
|
<div v-if="state.selectedMethod?.dynamicInputs" class="add-input-container">
|
||||||
|
<button class="flat-rounded-button" @click="addMethodInput">Add Parameter</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" class="gradient-button" @click="() => executeMethod(state.selectedMethod?.callback(state.primitive, state.selectedMethod.models))">Execute Method</button>
|
<button type="submit" class="gradient-button" @click="() => executeMethod(() => state.selectedMethod?.callback(state.primitive!, state.selectedMethod.models))">Execute Method</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Result -->
|
<!-- Result -->
|
||||||
<div>
|
<div v-if="state.selectedMethod">
|
||||||
<div class="output-box-container">
|
<div class="output-box-container">
|
||||||
<LabelledSection label="Result" :content="state.selectedMethod.result" />
|
<LabelledSection label="Result" :content="state.selectedMethod.result || ''" />
|
||||||
<CopyButton :content="state.selectedMethod.result" />
|
<CopyButton :content="state.selectedMethod.result || ''" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -134,6 +148,9 @@ export interface Method<Primitive> {
|
|||||||
// Error (if one occurred).
|
// Error (if one occurred).
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|
||||||
|
// Whether the number of inputs is dynamic.
|
||||||
|
dynamicInputs?: boolean;
|
||||||
|
|
||||||
// Function that takes the models and returns the primitive
|
// Function that takes the models and returns the primitive
|
||||||
callback: (primitive: Primitive, value: Array<any>) => string | Promise<string>;
|
callback: (primitive: Primitive, value: Array<any>) => string | Promise<string>;
|
||||||
|
|
||||||
@@ -191,7 +208,7 @@ async function setPrimitive(functionToExecute: () => Primitive | Promise<Primiti
|
|||||||
state.value.error = undefined;
|
state.value.error = undefined;
|
||||||
|
|
||||||
state.value.toFunctions.forEach(async (wrappedMethod, i) => {
|
state.value.toFunctions.forEach(async (wrappedMethod, i) => {
|
||||||
state.value.toFunctions[i].result = await wrappedMethod.callback(state.value.primitive)
|
state.value.toFunctions[i].result = await wrappedMethod.callback(state.value.primitive!)
|
||||||
});
|
});
|
||||||
|
|
||||||
state.value.methods.forEach((method, i) => {
|
state.value.methods.forEach((method, i) => {
|
||||||
@@ -218,14 +235,20 @@ function getPrimitive(functionToExecute: () => string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeMethod(wrappedMethod: () => string | Promise<string>): Promise<void | string> {
|
async function executeMethod(wrappedMethod: () => string | Promise<string> | undefined): Promise<void | string> {
|
||||||
|
if(!wrappedMethod || !state.value.selectedMethod) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if(!state.value.primitive) {
|
if(!state.value.primitive) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
state.value.selectedMethod.result = await wrappedMethod;
|
const result = await wrappedMethod();
|
||||||
|
|
||||||
|
// Update the result directly on the selected method
|
||||||
|
state.value.selectedMethod.result = result;
|
||||||
state.value.selectedMethod.error = undefined;
|
state.value.selectedMethod.error = undefined;
|
||||||
} catch(error: any) {
|
} catch(error: any) {
|
||||||
state.value.selectedMethod.error = error.message;
|
state.value.selectedMethod.error = error.message;
|
||||||
@@ -234,10 +257,29 @@ async function executeMethod(wrappedMethod: () => string | Promise<string>): Pro
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function addMethodInput() {
|
||||||
|
if(!state.value.selectedMethod) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.value.selectedMethod.models.push('');
|
||||||
|
state.value.selectedMethod.inputs.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeMethodInput(index: number) {
|
||||||
|
if(!state.value.selectedMethod) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.value.selectedMethod.models.splice(index, 1);
|
||||||
|
state.value.selectedMethod.inputs.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
// Select the first "from" function.
|
// Select the first "from" function.
|
||||||
state.value.selectedFromFunction = state.value.fromFunctions[0];
|
state.value.selectedFromFunction = state.value.fromFunctions[0];
|
||||||
|
|
||||||
const autoGrow = (element: HTMLTextAreaElement) => {
|
const autoGrow = (element: HTMLTextAreaElement | null) => {
|
||||||
|
if (!element) return;
|
||||||
element.style.height = 'auto'
|
element.style.height = 'auto'
|
||||||
element.style.height = `${Math.min(element.scrollHeight, 300)}px`
|
element.style.height = `${Math.min(element.scrollHeight, 300)}px`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
.form-group select,
|
.form-group select,
|
||||||
.form-group input,
|
.form-group input,
|
||||||
.form-group button {
|
.form-group button:not(.remove-input-button) {
|
||||||
width: 100%; /* Makes fields responsive */
|
width: 100%; /* Makes fields responsive */
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -133,6 +133,44 @@
|
|||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-input-container >button.flat-rounded-button {
|
||||||
|
border-radius: 100px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
width: auto;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: bold;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-input-container > button.flat-rounded-button:hover {
|
||||||
|
background: var(--vp-c-divider); /* Super light grey */
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-input-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-remove {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-input-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-input-button:hover {
|
||||||
|
color: #f46666;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile responsiveness */
|
/* Mobile responsiveness */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.select-group {
|
.select-group {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
encodeExtendedJson,
|
encodeExtendedJson,
|
||||||
Transaction,
|
Transaction,
|
||||||
} from '@xocash/stack';
|
} from '@xocash/stack';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
export const electrumClient = {
|
export const electrumClient = {
|
||||||
from: [
|
from: [
|
||||||
@@ -19,6 +20,7 @@ export const electrumClient = {
|
|||||||
application: 'XO-tools',
|
application: 'XO-tools',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await electrum.start();
|
||||||
|
|
||||||
return electrum;
|
return electrum;
|
||||||
},
|
},
|
||||||
@@ -26,10 +28,32 @@ export const electrumClient = {
|
|||||||
],
|
],
|
||||||
to: [],
|
to: [],
|
||||||
methods: [
|
methods: [
|
||||||
|
{
|
||||||
|
label: 'Request',
|
||||||
|
inputs: ['Method', 'Parameters'],
|
||||||
|
dynamicInputs: true,
|
||||||
|
models: ['', ''],
|
||||||
|
callback: async (
|
||||||
|
electrum: BlockchainElectrum,
|
||||||
|
args: [string, string],
|
||||||
|
) => {
|
||||||
|
const [method, ...parameters] = args;
|
||||||
|
|
||||||
|
// If the first parameter is empty, don't include it in the request
|
||||||
|
// This parameter can not be removed in the UI so we handle a blank parameter gracefully
|
||||||
|
// Since other parameters can be removed, we assume that blank parameters are intentional and so we include them as part of the request
|
||||||
|
if (!parameters[0] && parameters.length === 1) {
|
||||||
|
parameters.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await electrum.request(method, ...parameters);
|
||||||
|
return encodeExtendedJson(response, 2);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Fetch Unspents',
|
label: 'Fetch Unspents',
|
||||||
inputs: ['Address'],
|
inputs: ['Address'],
|
||||||
models: ['Address'],
|
models: [''],
|
||||||
callback: async (electrum: BlockchainElectrum, args: [string]) => {
|
callback: async (electrum: BlockchainElectrum, args: [string]) => {
|
||||||
const unspents = await electrum.fetchUnspents(args[0]);
|
const unspents = await electrum.fetchUnspents(args[0]);
|
||||||
return encodeExtendedJson(unspents.toArray(), 2);
|
return encodeExtendedJson(unspents.toArray(), 2);
|
||||||
@@ -38,7 +62,7 @@ export const electrumClient = {
|
|||||||
{
|
{
|
||||||
label: 'Fetch Transaction',
|
label: 'Fetch Transaction',
|
||||||
inputs: ['Transaction Hash'],
|
inputs: ['Transaction Hash'],
|
||||||
models: ['Transaction Hash'],
|
models: [''],
|
||||||
callback: async (electrum: BlockchainElectrum, args: [string]) => {
|
callback: async (electrum: BlockchainElectrum, args: [string]) => {
|
||||||
const transaction = await electrum.fetchTransaction(args[0]);
|
const transaction = await electrum.fetchTransaction(args[0]);
|
||||||
return encodeExtendedJson(transaction, 2);
|
return encodeExtendedJson(transaction, 2);
|
||||||
@@ -47,7 +71,7 @@ export const electrumClient = {
|
|||||||
{
|
{
|
||||||
label: 'Fetch Address History',
|
label: 'Fetch Address History',
|
||||||
inputs: ['Address'],
|
inputs: ['Address'],
|
||||||
models: ['Address'],
|
models: [''],
|
||||||
callback: async (electrum: BlockchainElectrum, args: [string]) => {
|
callback: async (electrum: BlockchainElectrum, args: [string]) => {
|
||||||
const transactions = await electrum.fetchAddressHistory(args[0]);
|
const transactions = await electrum.fetchAddressHistory(args[0]);
|
||||||
return encodeExtendedJson(transactions.toArray(), 2);
|
return encodeExtendedJson(transactions.toArray(), 2);
|
||||||
@@ -56,11 +80,50 @@ export const electrumClient = {
|
|||||||
{
|
{
|
||||||
label: 'Broadcast Transaction',
|
label: 'Broadcast Transaction',
|
||||||
inputs: ['Transaction'],
|
inputs: ['Transaction'],
|
||||||
models: ['Transaction'],
|
models: [''],
|
||||||
callback: async (electrum: BlockchainElectrum, args: [string]) => {
|
callback: async (electrum: BlockchainElectrum, args: [string]) => {
|
||||||
const tx = Transaction.from(args[0]);
|
const tx = Transaction.from(args[0]);
|
||||||
return await electrum.broadcastTransaction(tx);
|
return await electrum.broadcastTransaction(tx);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Subscribe Address',
|
||||||
|
inputs: ['Address'],
|
||||||
|
models: [''],
|
||||||
|
callback: async (electrum: BlockchainElectrum, args: [string]) => {
|
||||||
|
// Use a vue ref so we can update the UI dynamically.
|
||||||
|
const addressEvents = ref<string>('');
|
||||||
|
|
||||||
|
await electrum.subscribeAddress(args[0], (status) => {
|
||||||
|
// Don't update if its null
|
||||||
|
if (!status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addressEvents.value += `${status}\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return addressEvents;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Subscribe Transaction',
|
||||||
|
inputs: ['Transaction Hash'],
|
||||||
|
models: [''],
|
||||||
|
callback: async (electrum: BlockchainElectrum, args: [string]) => {
|
||||||
|
const transactionEvents = ref<string>('');
|
||||||
|
|
||||||
|
await electrum.subscribeTransaction(args[0], (transaction) => {
|
||||||
|
// Don't update if its null
|
||||||
|
if (!transaction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionEvents.value += `${transaction}\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return transactionEvents;
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,4 +27,5 @@ import {
|
|||||||
:toFunctions="electrumClient.to"
|
:toFunctions="electrumClient.to"
|
||||||
:methods="electrumClient.methods"
|
:methods="electrumClient.methods"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
Reference in New Issue
Block a user