diff --git a/packages/docs/src/.vitepress/theme/components/PrimitiveTool.vue b/packages/docs/src/.vitepress/theme/components/PrimitiveTool.vue index 4ffa2e0..e8368f4 100644 --- a/packages/docs/src/.vitepress/theme/components/PrimitiveTool.vue +++ b/packages/docs/src/.vitepress/theme/components/PrimitiveTool.vue @@ -17,7 +17,7 @@ v-if="state.selectedFromFunction?.extendedInput" v-model="state.selectedFromFunction.models[i]" class="extended-input" - @input="autoGrow($event.target)" + @input="(e) => autoGrow(e.target as HTMLTextAreaElement)" >

{{ state.toFunctions.length ? 'Output' : 'Instance' }}

- @@ -134,6 +148,9 @@ export interface Method { // Error (if one occurred). error?: string; + // Whether the number of inputs is dynamic. + dynamicInputs?: boolean; + // Function that takes the models and returns the primitive callback: (primitive: Primitive, value: Array) => string | Promise; @@ -191,7 +208,7 @@ async function setPrimitive(functionToExecute: () => Primitive | Promise { - 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) => { @@ -218,14 +235,20 @@ function getPrimitive(functionToExecute: () => string): string { } } -async function executeMethod(wrappedMethod: () => string | Promise): Promise { +async function executeMethod(wrappedMethod: () => string | Promise | undefined): Promise { + if(!wrappedMethod || !state.value.selectedMethod) { + return; + } + try { if(!state.value.primitive) { 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; } catch(error: any) { state.value.selectedMethod.error = error.message; @@ -234,10 +257,29 @@ async function executeMethod(wrappedMethod: () => string | Promise): 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. 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 = `${Math.min(element.scrollHeight, 300)}px` } diff --git a/packages/docs/src/.vitepress/theme/tools.css b/packages/docs/src/.vitepress/theme/tools.css index f9ecff1..9210cac 100644 --- a/packages/docs/src/.vitepress/theme/tools.css +++ b/packages/docs/src/.vitepress/theme/tools.css @@ -49,7 +49,7 @@ .form-group select, .form-group input, -.form-group button { +.form-group button:not(.remove-input-button) { width: 100%; /* Makes fields responsive */ padding: 10px; font-size: 1rem; @@ -133,6 +133,44 @@ 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 */ @media (max-width: 768px) { .select-group { diff --git a/packages/docs/src/tools/stack/blockchain-functions.ts b/packages/docs/src/tools/stack/blockchain-functions.ts index 589bd77..a72363e 100644 --- a/packages/docs/src/tools/stack/blockchain-functions.ts +++ b/packages/docs/src/tools/stack/blockchain-functions.ts @@ -3,6 +3,7 @@ import { encodeExtendedJson, Transaction, } from '@xocash/stack'; +import { ref } from 'vue'; export const electrumClient = { from: [ @@ -19,6 +20,7 @@ export const electrumClient = { application: 'XO-tools', }, }); + await electrum.start(); return electrum; }, @@ -26,10 +28,32 @@ export const electrumClient = { ], to: [], 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', inputs: ['Address'], - models: ['Address'], + models: [''], callback: async (electrum: BlockchainElectrum, args: [string]) => { const unspents = await electrum.fetchUnspents(args[0]); return encodeExtendedJson(unspents.toArray(), 2); @@ -38,7 +62,7 @@ export const electrumClient = { { label: 'Fetch Transaction', inputs: ['Transaction Hash'], - models: ['Transaction Hash'], + models: [''], callback: async (electrum: BlockchainElectrum, args: [string]) => { const transaction = await electrum.fetchTransaction(args[0]); return encodeExtendedJson(transaction, 2); @@ -47,7 +71,7 @@ export const electrumClient = { { label: 'Fetch Address History', inputs: ['Address'], - models: ['Address'], + models: [''], callback: async (electrum: BlockchainElectrum, args: [string]) => { const transactions = await electrum.fetchAddressHistory(args[0]); return encodeExtendedJson(transactions.toArray(), 2); @@ -56,11 +80,50 @@ export const electrumClient = { { label: 'Broadcast Transaction', inputs: ['Transaction'], - models: ['Transaction'], + models: [''], callback: async (electrum: BlockchainElectrum, args: [string]) => { const tx = Transaction.from(args[0]); 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(''); + + 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(''); + + await electrum.subscribeTransaction(args[0], (transaction) => { + // Don't update if its null + if (!transaction) { + return; + } + + transactionEvents.value += `${transaction}\n`; + }); + + return transactionEvents; + }, + }, ], }; diff --git a/packages/docs/src/tools/stack/blockchain.md b/packages/docs/src/tools/stack/blockchain.md index 68f02ad..8315fa4 100644 --- a/packages/docs/src/tools/stack/blockchain.md +++ b/packages/docs/src/tools/stack/blockchain.md @@ -27,4 +27,5 @@ import { :toFunctions="electrumClient.to" :methods="electrumClient.methods" /> - \ No newline at end of file + +