Resolve "WIP: UTXO Selection and Storage"
@@ -0,0 +1,215 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import typedocSidebar from '../api/typedoc-sidebar.json';
|
||||
|
||||
// Special cases map for acronyms and exceptions
|
||||
const SPECIAL_CASES = {
|
||||
hd: 'HD',
|
||||
indexeddb: 'IndexedDB',
|
||||
inmemory: 'InMemory',
|
||||
json: 'JSON',
|
||||
p2p: 'P2P',
|
||||
p2pkh: 'P2PKH',
|
||||
p2sh: 'P2SH',
|
||||
p2sh20: 'P2SH20',
|
||||
p2sh32: 'P2SH32',
|
||||
sse: 'SSE',
|
||||
utxo: 'UTXO',
|
||||
};
|
||||
|
||||
function toPascalCase(str: string) {
|
||||
// Handle special cases first
|
||||
const lowerStr = str.toLowerCase();
|
||||
|
||||
if (lowerStr.startsWith('@')) {
|
||||
return str;
|
||||
}
|
||||
|
||||
// Split by common delimiters (hyphen, space, underscore)
|
||||
return str
|
||||
.split(/[-\s_]/)
|
||||
.map((word) => {
|
||||
if (word.length === 0) return '';
|
||||
|
||||
if (SPECIAL_CASES[word]) {
|
||||
return SPECIAL_CASES[word];
|
||||
}
|
||||
|
||||
// Capitalize first letter, lowercase rest
|
||||
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
function convertNavToPascalCase(nav: Array<any>) {
|
||||
return nav.map((item) => {
|
||||
const result = { ...item };
|
||||
|
||||
// Convert the text field
|
||||
if (result.text) {
|
||||
// If there was a @ prefix, preserve it
|
||||
result.text = item.text.startsWith('@')
|
||||
? result.text
|
||||
: toPascalCase(result.text);
|
||||
}
|
||||
|
||||
// Recursively process nested items
|
||||
if (result.items && Array.isArray(result.items)) {
|
||||
result.items = convertNavToPascalCase(result.items);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
outDir: '../dist',
|
||||
title: 'Stack',
|
||||
description:
|
||||
'XO Stack: Building smart applications on the Bitcoin Cash Network',
|
||||
themeConfig: {
|
||||
// Set the logo
|
||||
logo: {
|
||||
light: '/logo-light.svg',
|
||||
dark: '/logo-dark.svg',
|
||||
},
|
||||
|
||||
// Enable search functionality.
|
||||
// TODO: Re-enable once we release documentation.
|
||||
/*
|
||||
search: {
|
||||
provider: 'local',
|
||||
},
|
||||
*/
|
||||
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: 'Home', link: '/' },
|
||||
// TODO: Re-enable once we release documentation
|
||||
// { text: 'Documentation', link: '/quick-start' },
|
||||
{ text: 'Tools', link: '/tools/primitives' },
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
// Default Side Menu.
|
||||
'/': [
|
||||
{
|
||||
text: 'Guide',
|
||||
items: [
|
||||
{ text: 'Quick Start', link: '/quick-start' },
|
||||
{ text: 'Best Practices', link: '/api-examples' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Frameworks',
|
||||
items: [
|
||||
{ text: 'Vue', link: '/frameworks/vue' },
|
||||
{ text: 'React', link: '/frameworks/react' },
|
||||
{ text: 'Svelte', link: '/frameworks/svelte' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'API',
|
||||
items: convertNavToPascalCase(typedocSidebar),
|
||||
},
|
||||
],
|
||||
// Side-menu for all routes under /tools/
|
||||
'/tools/': [
|
||||
{
|
||||
text: 'XO Tools',
|
||||
items: [
|
||||
{
|
||||
text: 'Primitives',
|
||||
link: '/tools/primitives',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Address', link: '/tools/primitives#address' },
|
||||
{
|
||||
text: 'Block Header',
|
||||
link: '/tools/primitives#block-header',
|
||||
},
|
||||
{ text: 'Bytes', link: '/tools/primitives#bytes' },
|
||||
{ text: 'Hasher', link: '/tools/primitives#hasher' },
|
||||
{
|
||||
text: 'HD Private Node',
|
||||
link: '/tools/primitives#hd-private-node',
|
||||
},
|
||||
{
|
||||
text: 'HD Public Node',
|
||||
link: '/tools/primitives#hd-public-node',
|
||||
},
|
||||
{
|
||||
text: 'Mnemonic',
|
||||
link: '/tools/primitives#mnemonic',
|
||||
},
|
||||
{
|
||||
text: 'Private Key',
|
||||
link: '/tools/primitives#private-key',
|
||||
},
|
||||
{
|
||||
text: 'Public Key',
|
||||
link: '/tools/primitives#public-key',
|
||||
},
|
||||
{
|
||||
text: 'Satoshis',
|
||||
link: '/tools/primitives#satoshis',
|
||||
},
|
||||
{
|
||||
text: 'Script',
|
||||
link: '/tools/primitives#script',
|
||||
},
|
||||
{
|
||||
text: 'Transaction',
|
||||
link: '/tools/primitives#transaction',
|
||||
},
|
||||
],
|
||||
},
|
||||
/*
|
||||
{
|
||||
text: 'Stack',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
text: 'Blockchain',
|
||||
items: [{ text: 'Blockchain Electrum' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
*/
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
/*
|
||||
|
||||
*/
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://gitlab.com/GeneralProtocols/xo/stack' },
|
||||
],
|
||||
|
||||
// This is the outline on the right-hand side of the page.
|
||||
outline: {
|
||||
level: [2, 4],
|
||||
label: 'On this page',
|
||||
},
|
||||
},
|
||||
|
||||
head: [['link', { rel: 'icon', href: '/favicon.png' }]],
|
||||
|
||||
// Set the dev and build targets to "esnext"
|
||||
// NOTE: We do this so that we can support top-level-await.
|
||||
vite: {
|
||||
esbuild: {
|
||||
target: 'esnext',
|
||||
},
|
||||
build: {
|
||||
target: 'esnext',
|
||||
},
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
target: 'esnext',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
<!-- .vitepress/theme/MyLayout.vue -->
|
||||
<script setup>
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
|
||||
const { Layout } = DefaultTheme
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<!-- Override the home-hero-image slot with the Stack, BCH, and Apps icons along with gradient arrows -->
|
||||
<template #home-hero-image>
|
||||
<div class="icon-container">
|
||||
|
||||
<!-- Stack Icon -->
|
||||
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="stack-icon">
|
||||
<path d="m2.394 15.759s7.554 4.246 9.09 5.109c.165.093.333.132.492.132.178 0 .344-.049.484-.127 1.546-.863 9.155-5.113 9.155-5.113.246-.138.385-.393.385-.656 0-.566-.614-.934-1.116-.654 0 0-7.052 3.958-8.539 4.77-.211.115-.444.161-.722.006-1.649-.928-8.494-4.775-8.494-4.775-.502-.282-1.117.085-1.117.653 0 .262.137.517.382.655zm0-3.113s7.554 4.246 9.09 5.109c.165.093.333.132.492.132.178 0 .344-.049.484-.127 1.546-.863 9.155-5.113 9.155-5.113.246-.138.385-.393.385-.656 0-.566-.614-.934-1.116-.654 0 0-7.052 3.958-8.539 4.77-.211.115-.444.161-.722.006-1.649-.928-8.494-4.775-8.494-4.775-.502-.282-1.117.085-1.117.653 0 .262.137.517.382.655zm10.271-9.455c-.246-.128-.471-.191-.692-.191-.223 0-.443.065-.675.191l-8.884 5.005c-.276.183-.414.444-.414.698 0 .256.139.505.414.664l8.884 5.006c.221.133.447.203.678.203.223 0 .452-.065.689-.203l8.884-5.006c.295-.166.451-.421.451-.68 0-.25-.145-.503-.451-.682z" fill-rule="nonzero"/>
|
||||
</svg>
|
||||
|
||||
|
||||
<!-- Right Arrow with gradient -->
|
||||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="url(#gradient)" fill-rule="evenodd" clip-rule="evenodd" class="right-arrow" stroke="url(#gradient)" stroke-width="2">
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#5271ff;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#38b6ff;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M4 .755l14.374 11.245-14.374 11.219.619.781 15.381-12-15.391-12-.609.755z"/>
|
||||
</svg>
|
||||
|
||||
<!-- BCH Icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="currentColor" class="bch-icon">
|
||||
<path d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm-.5 12.497v2.503c1.984 0 3.344-.188 3.344-1.258 0-1.148-1.469-1.245-3.344-1.245zm2.789-2.247c0-1-1.039-1.25-2.789-1.25v2.5c1.105 0 2.789-.078 2.789-1.25zm-2.289-7.25c-4.971 0-9 4.029-9 9s4.029 9 9 9 9-4.029 9-9-4.029-9-9-9zm1 13.5v1.5h-1v-1.511h-.5v1.511h-1v-1.511h-2.484l.25-1.489h.539c.442 0 .695-.425.695-.854v-4.444c0-.416-.242-.702-.683-.702h-.817v-1.5h2.5v-1.5h1v1.5h.5v-1.5h1v1.526c2.158.073 3.012.891 3.257 1.812.29 1.09-.429 2.005-1.046 2.228.75.192 1.789.746 1.789 2.026 0 1.741-1.344 2.908-4 2.908z"/>
|
||||
</svg>
|
||||
|
||||
<!-- Left Arrow with gradient -->
|
||||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill="url(#gradient)" fill-rule="evenodd" clip-rule="evenodd" class="left-arrow" stroke="url(#gradient)" stroke-width="2">
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#5271ff;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#38b6ff;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M20 .755l-14.374 11.245 14.374 11.219-.619.781-15.381-12 15.391-12 .609.755z"/>
|
||||
</svg>
|
||||
|
||||
<!-- Apps Icon -->
|
||||
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="apps-icon">
|
||||
<path d="m21 4c0-.478-.379-1-1-1h-16c-.62 0-1 .519-1 1v16c0 .621.52 1 1 1h16c.478 0 1-.379 1-1zm-16.5.5h15v15h-15zm6.75 9.25v3.25c0 .53-.47 1-1 1h-3.25c-.53 0-1-.47-1-1v-3.25c0-.53.47-1 1-1h3.25c.53 0 1 .47 1 1zm0-6.75v3.25c0 .53-.47 1-1 1h-3.25c-.53 0-1-.47-1-1v-3.25c0-.53.47-1 1-1h3.25c.53 0 1 .47 1 1zm6.75 0v3.25c0 .53-.47 1-1 1h-3.25c-.53 0-1-.47-1-1v-3.25c0-.53.47-1 1-1h3.25c.53 0 1 .47 1 1z" fill-rule="nonzero"/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<Tooltip :show="showTooltip && !isCopied">
|
||||
<button
|
||||
class="copy-button"
|
||||
:class="{ 'copied': isCopied }"
|
||||
@click="handleCopy"
|
||||
@mouseenter="showTooltip = true"
|
||||
@mouseleave="showTooltip = false"
|
||||
>
|
||||
<Transition mode="out-in">
|
||||
<img v-if="!isCopied" key="icon" src="../icons/copy.svg" alt="Copy" class="copy-icon" />
|
||||
<span v-else key="text" class="copied-text">Copied!</span>
|
||||
</Transition>
|
||||
</button>
|
||||
<template #content>Copy</template>
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Tooltip from './Tooltip.vue';
|
||||
|
||||
type Props = {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
content: ''
|
||||
});
|
||||
|
||||
const isCopied = ref(false);
|
||||
const showTooltip = ref(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(props.content);
|
||||
isCopied.value = true;
|
||||
showTooltip.value = false;
|
||||
setTimeout(() => {
|
||||
isCopied.value = false;
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tooltip-container {
|
||||
position: absolute !important;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
position: relative;
|
||||
padding: 5px 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
background: var(--vp-home-hero-name-background);
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
opacity: 0.9;
|
||||
width: 2.2em;
|
||||
height: 2.2em;
|
||||
overflow: hidden;
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
opacity: 1;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.copy-button.copied {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.copied-text {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="labelled-section-container">
|
||||
<div class="labelled-section">
|
||||
{{ content || ' ' }}
|
||||
</div>
|
||||
<label class="section-label" :class="{ 'is-floating': hasContent }">
|
||||
{{ label }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
export interface Props {
|
||||
content?: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
content: '',
|
||||
label: ''
|
||||
})
|
||||
|
||||
const hasContent = computed(() => props.content.length > 0)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.labelled-section-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.labelled-section {
|
||||
box-sizing: content-box;
|
||||
padding: 10px 8px;
|
||||
font-size: 1rem;
|
||||
border: 1px dashed #ccc;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg);
|
||||
line-height: 1.5;
|
||||
min-height: calc(1.5em);
|
||||
font-family: monospace;
|
||||
max-width: 100%;
|
||||
text-wrap: wrap;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--vp-c-bg);
|
||||
padding: 0 4px;
|
||||
font-size: 1em;
|
||||
font-family: monospace;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.section-label.is-floating {
|
||||
top: 0;
|
||||
font-size: 0.8em;
|
||||
transform: translateY(-50%);
|
||||
font-style: normal;
|
||||
color: var(--vp-c-text-2);
|
||||
opacity: 1.0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div class="input-wrapper">
|
||||
<h2>Input</h2>
|
||||
|
||||
<div class="form-group-row" :class="{ 'extended': state.selectedFromFunction?.extendedInput }">
|
||||
<div class="form-group select-group">
|
||||
<label for="functionSelect">Select Function</label>
|
||||
<select v-model="state.selectedFromFunction">
|
||||
<option v-for="fromFunction of props.fromFunctions" :key="fromFunction.label" :value="fromFunction">{{ fromFunction.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group input-group" :class="{ 'extended': state.selectedFromFunction?.extendedInput }">
|
||||
<template v-for="(_input, i) of state.selectedFromFunction.inputs" :key="i">
|
||||
<label for="field">{{ state.selectedFromFunction.inputs[i] }}</label>
|
||||
<div>
|
||||
<textarea
|
||||
v-if="state.selectedFromFunction?.extendedInput"
|
||||
v-model="state.selectedFromFunction.models[i]"
|
||||
class="extended-input"
|
||||
@input="autoGrow($event.target)"
|
||||
></textarea>
|
||||
<input
|
||||
v-else
|
||||
type="text"
|
||||
v-model="state.selectedFromFunction.models[i]"
|
||||
/>
|
||||
<div v-if="state.error" class="text-error">{{ state.error }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="gradient-button" @click="() => setPrimitive(() => state.selectedFromFunction.callback(state.selectedFromFunction.models))">Create Output</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section output-section">
|
||||
<h2>Output</h2>
|
||||
|
||||
<template v-for="(toFunction, i) of toFunctions" :key="i">
|
||||
<div>
|
||||
<div class="output-box-container">
|
||||
<LabelledSection :label="toFunction.label" :content="getPrimitive(() => toFunction.callback(state.primitive))" />
|
||||
<CopyButton :content="getPrimitive(() => toFunction.callback(state.primitive))" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="state.methods.length && state.primitive">
|
||||
<h2>Methods</h2>
|
||||
|
||||
<div class="form-group-row" :class="{ 'extended': state.selectedMethod?.extendedInput }">
|
||||
<div class="form-group select-group">
|
||||
<label for="functionSelect">Select Function</label>
|
||||
<select v-model="state.selectedMethod">
|
||||
<option v-for="method of props.methods" :key="method.label" :value="method">{{ method.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group input-group" :class="{ 'extended': state.selectedMethod?.extendedInput }">
|
||||
<template v-for="(_input, i) of state.selectedMethod.inputs" :key="i">
|
||||
<label for="field">{{ state.selectedMethod.inputs[i] }}</label>
|
||||
<div>
|
||||
<textarea
|
||||
v-if="state.selectedMethod?.extendedInput"
|
||||
v-model="state.selectedMethod.models[i]"
|
||||
class="extended-input"
|
||||
@input="autoGrow($event.target)"
|
||||
></textarea>
|
||||
<input
|
||||
v-else
|
||||
type="text"
|
||||
v-model="state.selectedMethod.models[i]"
|
||||
/>
|
||||
<div v-if="state.error" class="text-error">{{ state.error }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="gradient-button" @click="() => executeMethod(state.selectedMethod.callback(state.primitive, state.selectedMethod.models))">Execute Method</button>
|
||||
</div>
|
||||
<!-- Result -->
|
||||
<div>
|
||||
<div class="output-box-container">
|
||||
<LabelledSection label="Result" :content="state.selectedMethod.result" />
|
||||
<CopyButton :content="state.selectedMethod.result" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup generic="Primitive" lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import LabelledSection from './LabelledSection.vue';
|
||||
import CopyButton from './CopyButton.vue';
|
||||
|
||||
export interface FromFunction<Primitive> {
|
||||
// The label of the function
|
||||
label: string,
|
||||
|
||||
// Array of inputs that the function takes
|
||||
inputs: Array<string>,
|
||||
|
||||
// Array of models that the input stores its value in
|
||||
models: Array<any>,
|
||||
|
||||
// Function that takes the models and returns the primitive
|
||||
callback: (value: Array<any>) => Primitive,
|
||||
|
||||
// Whether the input is extended onto a new line
|
||||
extendedInput?: boolean
|
||||
}
|
||||
|
||||
export interface ToFunction<Primitive> {
|
||||
label: string;
|
||||
callback: (primitive: Primitive) => string;
|
||||
result?: string;
|
||||
}
|
||||
|
||||
export interface Method<Primitive> {
|
||||
// The label of the function
|
||||
label: string;
|
||||
|
||||
// Array of inputs that the function takes
|
||||
inputs: Array<string>,
|
||||
|
||||
// Array of models that the input stores its value in
|
||||
models: Array<any>,
|
||||
|
||||
// Result of method execution,
|
||||
result?: string;
|
||||
|
||||
// Function that takes the models and returns the primitive
|
||||
callback: (primitive: Primitive, value: Array<any>) => string;
|
||||
|
||||
// Whether the input is extended onto a new line
|
||||
extendedInput?: boolean
|
||||
}
|
||||
|
||||
export interface Props<Primitive> {
|
||||
fromFunctions: Array<FromFunction<Primitive>>,
|
||||
toFunctions: Array<ToFunction<Primitive>>,
|
||||
methods?: Array<Method<Primitive>>,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props<Primitive>>(), {
|
||||
fromFunctions: () => [],
|
||||
toFunctions: () => [],
|
||||
methods: () => [],
|
||||
});
|
||||
|
||||
const state: Ref<{
|
||||
fromFunctions: Array<FromFunction<Primitive>>,
|
||||
toFunctions: Array<ToFunction<Primitive>>,
|
||||
methods: Array<Method<Primitive>>,
|
||||
|
||||
// Selected In Function
|
||||
selectedFromFunction: FromFunction<Primitive>,
|
||||
|
||||
// Selected Method
|
||||
selectedMethod?: Method<Primitive>,
|
||||
|
||||
// The Primitive.
|
||||
primitive?: Primitive,
|
||||
|
||||
// Error to display if an invalid input was given.
|
||||
error?: string;
|
||||
}> = ref({
|
||||
// From functions, to functions and methods.
|
||||
fromFunctions: [ ...props.fromFunctions ],
|
||||
toFunctions: [ ...props.toFunctions ],
|
||||
methods: [ ...props.methods ],
|
||||
|
||||
// The selected function.
|
||||
selectedFromFunction: props.fromFunctions[0],
|
||||
|
||||
// The selected method.
|
||||
selectedMethod: props.methods[0] || undefined,
|
||||
|
||||
// The Primitive.
|
||||
primitive: undefined,
|
||||
})
|
||||
|
||||
async function setPrimitive(functionToExecute: () => Primitive | Promise<Primitive>): void {
|
||||
try {
|
||||
state.value.primitive = await functionToExecute();
|
||||
state.value.error = undefined;
|
||||
|
||||
state.value.toFunctions.forEach((wrappedMethod, i) => {
|
||||
state.value.toFunctions[i].result = wrappedMethod.callback(state.value.primitive)
|
||||
});
|
||||
} catch(error: any) {
|
||||
state.value.error = `${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
function getPrimitive(functionToExecute: () => string): string {
|
||||
try {
|
||||
if(!state.value.primitive) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return functionToExecute();
|
||||
} catch(error: any) {
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeMethod(wrappedMethod: () => string | Promise<string>): string {
|
||||
try {
|
||||
if(!state.value.primitive) {
|
||||
return '';
|
||||
}
|
||||
|
||||
state.value.selectedMethod.result = await wrappedMethod;
|
||||
} catch(error: any) {
|
||||
return error.message;
|
||||
}
|
||||
};
|
||||
|
||||
// Select the first "from" function.
|
||||
state.value.selectedFromFunction = state.value.fromFunctions[0];
|
||||
|
||||
const autoGrow = (element: HTMLTextAreaElement) => {
|
||||
element.style.height = 'auto'
|
||||
element.style.height = `${Math.min(element.scrollHeight, 300)}px`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.text-error {
|
||||
color: #f46666;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="tooltip-container">
|
||||
<slot></slot>
|
||||
<Transition>
|
||||
<div v-if="show" class="tooltip" :class="position">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type Props = {
|
||||
show: boolean;
|
||||
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
position: 'top',
|
||||
show: false
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tooltip-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: var(--vp-c-text-1);
|
||||
color: var(--vp-c-bg);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tooltip.top {
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-6px);
|
||||
}
|
||||
|
||||
.tooltip.bottom {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(6px);
|
||||
}
|
||||
|
||||
.tooltip.left {
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) translateX(-6px);
|
||||
}
|
||||
|
||||
.tooltip.right {
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) translateX(6px);
|
||||
}
|
||||
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,164 @@
|
||||
:root {
|
||||
--primary: #5271ff;
|
||||
--vp-nav-logo-height: 48px;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--primary: #38b6ff;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.vp-nav .title {
|
||||
font-size: 24px !important;
|
||||
}
|
||||
|
||||
.vp-doc h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.vp-doc h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.vp-doc h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.vp-doc h4 {
|
||||
font-size: 14px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.VPSidebarItem .text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
ul.VPDocOutlineItem.root {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-2.is-active > .item > .indicator,
|
||||
.VPSidebarItem.level-3.is-active > .item > .indicator,
|
||||
.VPSidebarItem.level-4.is-active > .item > .indicator,
|
||||
.VPSidebarItem.level-5.is-active > .item > .indicator {
|
||||
background-color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-1.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-2.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-3.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-4.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-5.is-active > .item .link > .text {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-1.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-2.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-3.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-4.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-5.is-link > .item > .link:hover .text {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.outline-title {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.outline-link {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* Customization of VitePress CSS */
|
||||
|
||||
:root {
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||
120deg,
|
||||
#5271ff,
|
||||
#38b6ff
|
||||
);
|
||||
}
|
||||
|
||||
.VPButton.brand {
|
||||
background: linear-gradient(-45deg, #5271ff, #38b6ff) !important;
|
||||
}
|
||||
|
||||
.VPButton.brand:hover {
|
||||
background: linear-gradient(45deg, #5271ff, #5271ff) !important;
|
||||
}
|
||||
|
||||
/* .vitepress/theme/custom.css */
|
||||
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%; /* Ensure it takes up the full height of the parent */
|
||||
width: 100%; /* Ensure it takes up the full width of the parent */
|
||||
}
|
||||
|
||||
/* .vitepress/theme/custom.css */
|
||||
|
||||
/* Container for all icons and arrows */
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center; /* Align the icons and arrows vertically */
|
||||
justify-content: center; /* Center the icons and arrows horizontally */
|
||||
gap: 20px; /* Space between the icons and arrows */
|
||||
}
|
||||
|
||||
/* BCH icon styling */
|
||||
.bch-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
transform: rotate(-15deg); /* Rotate the BCH icon by 15 degrees to the left */
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Stack icon styling */
|
||||
.stack-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Apps icon styling */
|
||||
.apps-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Arrow between Stack and BCH */
|
||||
.right-arrow {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Arrow between BCH and Apps */
|
||||
.left-arrow {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Light / dark mode logos */
|
||||
|
||||
:root:not(.dark) .dark-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:root:is(.dark) .light-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Text colors */
|
||||
|
||||
.text-error {
|
||||
color: #f46666;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve">
|
||||
<g id="Text-files" stroke="#000000" stroke-width="2" stroke-linejoin="round">
|
||||
<path fill-rule="evenodd" d="M53.9791489,9.1429005H50.010849c-0.0826988,0-0.1562004,0.0283995-0.2331009,0.0469999V5.0228
|
||||
C49.7777481,2.253,47.4731483,0,44.6398468,0h-34.422596C7.3839517,0,5.0793519,2.253,5.0793519,5.0228v46.8432999
|
||||
c0,2.7697983,2.3045998,5.0228004,5.1378999,5.0228004h6.0367002v2.2678986C16.253952,61.8274002,18.4702511,64,21.1954517,64
|
||||
h32.783699c2.7252007,0,4.9414978-2.1725998,4.9414978-4.8432007V13.9861002
|
||||
C58.9206467,11.3155003,56.7043495,9.1429005,53.9791489,9.1429005z M7.1110516,51.8661003V5.0228
|
||||
c0-1.6487999,1.3938999-2.9909999,3.1062002-2.9909999h34.422596c1.7123032,0,3.1062012,1.3422,3.1062012,2.9909999v46.8432999
|
||||
c0,1.6487999-1.393898,2.9911003-3.1062012,2.9911003h-34.422596C8.5049515,54.8572006,7.1110516,53.5149002,7.1110516,51.8661003z
|
||||
M56.8888474,59.1567993c0,1.550602-1.3055,2.8115005-2.9096985,2.8115005h-32.783699
|
||||
c-1.6042004,0-2.9097996-1.2608986-2.9097996-2.8115005v-2.2678986h26.3541946
|
||||
c2.8333015,0,5.1379013-2.2530022,5.1379013-5.0228004V11.1275997c0.0769005,0.0186005,0.1504021,0.0469999,0.2331009,0.0469999
|
||||
h3.9682999c1.6041985,0,2.9096985,1.2609005,2.9096985,2.8115005V59.1567993z"/>
|
||||
<path fill-rule="evenodd" d="M38.6031494,13.2063999H16.253952c-0.5615005,0-1.0159006,0.4542999-1.0159006,1.0158005
|
||||
c0,0.5615997,0.4544001,1.0158997,1.0159006,1.0158997h22.3491974c0.5615005,0,1.0158997-0.4542999,1.0158997-1.0158997
|
||||
C39.6190491,13.6606998,39.16465,13.2063999,38.6031494,13.2063999z"/>
|
||||
<path fill-rule="evenodd" d="M38.6031494,21.3334007H16.253952c-0.5615005,0-1.0159006,0.4542999-1.0159006,1.0157986
|
||||
c0,0.5615005,0.4544001,1.0159016,1.0159006,1.0159016h22.3491974c0.5615005,0,1.0158997-0.454401,1.0158997-1.0159016
|
||||
C39.6190491,21.7877007,39.16465,21.3334007,38.6031494,21.3334007z"/>
|
||||
<path fill-rule="evenodd" d="M38.6031494,29.4603004H16.253952c-0.5615005,0-1.0159006,0.4543991-1.0159006,1.0158997
|
||||
s0.4544001,1.0158997,1.0159006,1.0158997h22.3491974c0.5615005,0,1.0158997-0.4543991,1.0158997-1.0158997
|
||||
S39.16465,29.4603004,38.6031494,29.4603004z"/>
|
||||
<path fill-rule="evenodd" d="M28.4444485,37.5872993H16.253952c-0.5615005,0-1.0159006,0.4543991-1.0159006,1.0158997
|
||||
s0.4544001,1.0158997,1.0159006,1.0158997h12.1904964c0.5615025,0,1.0158005-0.4543991,1.0158005-1.0158997
|
||||
S29.0059509,37.5872993,28.4444485,37.5872993z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,15 @@
|
||||
// https://vitepress.dev/guide/custom-theme
|
||||
import type { Theme } from 'vitepress';
|
||||
import DefaultTheme from 'vitepress/theme';
|
||||
import Layout from './Layout.vue';
|
||||
import './style.css';
|
||||
import './custom.css';
|
||||
import './tools.css';
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout,
|
||||
enhanceApp({ app, router, siteData }) {
|
||||
// ...
|
||||
},
|
||||
} satisfies Theme;
|
||||
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 80 KiB |
@@ -0,0 +1,560 @@
|
||||
/**
|
||||
* Customize default theme styling by overriding CSS variables:
|
||||
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||
*/
|
||||
|
||||
/**
|
||||
* Colors
|
||||
*
|
||||
* Each colors have exact same color scale system with 3 levels of solid
|
||||
* colors with different brightness, and 1 soft color.
|
||||
*
|
||||
* - `XXX-1`: The most solid color used mainly for colored text. It must
|
||||
* satisfy the contrast ratio against when used on top of `XXX-soft`.
|
||||
*
|
||||
* - `XXX-2`: The color used mainly for hover state of the button.
|
||||
*
|
||||
* - `XXX-3`: The color for solid background, such as bg color of the button.
|
||||
* It must satisfy the contrast ratio with pure white (#ffffff) text on
|
||||
* top of it.
|
||||
*
|
||||
* - `XXX-soft`: The color used for subtle background such as custom container
|
||||
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
|
||||
* on top of it.
|
||||
*
|
||||
* The soft color must be semi transparent alpha channel. This is crucial
|
||||
* because it allows adding multiple "soft" colors on top of each other
|
||||
* to create a accent, such as when having inline code block inside
|
||||
* custom containers.
|
||||
*
|
||||
* - `default`: The color used purely for subtle indication without any
|
||||
* special meanings attched to it such as bg color for menu hover state.
|
||||
*
|
||||
* - `brand`: Used for primary brand colors, such as link text, button with
|
||||
* brand theme, etc.
|
||||
*
|
||||
* - `tip`: Used to indicate useful information. The default theme uses the
|
||||
* brand color for this by default.
|
||||
*
|
||||
* - `warning`: Used to indicate warning to the users. Used in custom
|
||||
* container, badges, etc.
|
||||
*
|
||||
* - `danger`: Used to show error, or dangerous message to the users. Used
|
||||
* in custom container, badges, etc.
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-c-default-1: var(--vp-c-gray-1);
|
||||
--vp-c-default-2: var(--vp-c-gray-2);
|
||||
--vp-c-default-3: var(--vp-c-gray-3);
|
||||
--vp-c-default-soft: var(--vp-c-gray-soft);
|
||||
|
||||
--vp-c-brand-1: var(--vp-c-indigo-1);
|
||||
--vp-c-brand-2: var(--vp-c-indigo-2);
|
||||
--vp-c-brand-3: var(--vp-c-indigo-3);
|
||||
--vp-c-brand-soft: var(--vp-c-indigo-soft);
|
||||
|
||||
--vp-c-tip-1: var(--vp-c-brand-1);
|
||||
--vp-c-tip-2: var(--vp-c-brand-2);
|
||||
--vp-c-tip-3: var(--vp-c-brand-3);
|
||||
--vp-c-tip-soft: var(--vp-c-brand-soft);
|
||||
|
||||
--vp-c-warning-1: var(--vp-c-yellow-1);
|
||||
--vp-c-warning-2: var(--vp-c-yellow-2);
|
||||
--vp-c-warning-3: var(--vp-c-yellow-3);
|
||||
--vp-c-warning-soft: var(--vp-c-yellow-soft);
|
||||
|
||||
--vp-c-danger-1: var(--vp-c-red-1);
|
||||
--vp-c-danger-2: var(--vp-c-red-2);
|
||||
--vp-c-danger-3: var(--vp-c-red-3);
|
||||
--vp-c-danger-soft: var(--vp-c-red-soft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Button
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-button-brand-border: transparent;
|
||||
--vp-button-brand-text: var(--vp-c-white);
|
||||
--vp-button-brand-bg: var(--vp-c-brand-3);
|
||||
--vp-button-brand-hover-border: transparent;
|
||||
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
|
||||
--vp-button-brand-active-border: transparent;
|
||||
--vp-button-brand-active-text: var(--vp-c-white);
|
||||
--vp-button-brand-active-bg: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Home
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||
120deg,
|
||||
#38b6ff 30%,
|
||||
#5271ff
|
||||
);
|
||||
|
||||
/*
|
||||
|
||||
--vp-home-hero-image-background-image: linear-gradient(
|
||||
-45deg,
|
||||
#5271ff 50%,
|
||||
#38b6ff 50%
|
||||
|
||||
);
|
||||
|
||||
--vp-home-hero-image-filter: blur(44px);
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(68px);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Custom Block
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-custom-block-tip-border: transparent;
|
||||
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
|
||||
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Algolia
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides
|
||||
*/
|
||||
|
||||
:root {
|
||||
--primary: #5271ff;
|
||||
--vp-nav-logo-height: 48px;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--primary: #38b6ff;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.vp-nav .title {
|
||||
font-size: 24px !important;
|
||||
}
|
||||
|
||||
.vp-doc h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.vp-doc h2 {
|
||||
font-size: 22px;
|
||||
color: var(--primary);
|
||||
font-weight: bold;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 5px;
|
||||
border: none !important;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.vp-doc h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.vp-doc h4 {
|
||||
font-size: 14px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.VPSidebarItem .text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-2.is-active > .item > .indicator,
|
||||
.VPSidebarItem.level-3.is-active > .item > .indicator,
|
||||
.VPSidebarItem.level-4.is-active > .item > .indicator,
|
||||
.VPSidebarItem.level-5.is-active > .item > .indicator {
|
||||
background-color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-1.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-2.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-3.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-4.is-active > .item .link > .text,
|
||||
.VPSidebarItem.level-5.is-active > .item .link > .text {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-1.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-2.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-3.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-4.is-link > .item > .link:hover .text,
|
||||
.VPSidebarItem.level-5.is-link > .item > .link:hover .text {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
.outline-title {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.outline-link {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* Customization of VitePress CSS */
|
||||
|
||||
:root {
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||
120deg,
|
||||
#5271ff,
|
||||
#38b6ff
|
||||
);
|
||||
}
|
||||
|
||||
.VPButton.brand {
|
||||
background: linear-gradient(-45deg, #5271ff, #38b6ff) !important;
|
||||
}
|
||||
|
||||
.VPButton.brand:hover {
|
||||
background: linear-gradient(45deg, #5271ff, #5271ff) !important;
|
||||
}
|
||||
|
||||
/* .vitepress/theme/custom.css */
|
||||
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%; /* Ensure it takes up the full height of the parent */
|
||||
width: 100%; /* Ensure it takes up the full width of the parent */
|
||||
}
|
||||
|
||||
/* .vitepress/theme/custom.css */
|
||||
|
||||
/* Container for all icons and arrows */
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center; /* Align the icons and arrows vertically */
|
||||
justify-content: center; /* Center the icons and arrows horizontally */
|
||||
gap: 20px; /* Space between the icons and arrows */
|
||||
}
|
||||
|
||||
/* BCH icon styling */
|
||||
.bch-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
transform: rotate(-15deg); /* Rotate the BCH icon by 15 degrees to the left */
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Stack icon styling */
|
||||
.stack-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Apps icon styling */
|
||||
.apps-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Arrow between Stack and BCH */
|
||||
.right-arrow {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Arrow between BCH and Apps */
|
||||
.left-arrow {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Light / dark mode logos */
|
||||
|
||||
:root:not(.dark) .dark-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:root:is(.dark) .light-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Feature box override */
|
||||
|
||||
.feature-box-or {
|
||||
flex-direction: row !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.feature-box-or h2 {
|
||||
border: none !important;
|
||||
padding-top: 10px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.feature-box-or img {
|
||||
margin: auto !important;
|
||||
}
|
||||
|
||||
/* Promo on front page */
|
||||
|
||||
.promo {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.promo-text {
|
||||
flex: 2; /* Adjusted flex value for more text space */
|
||||
min-width: 250px; /* Ensures text is responsive */
|
||||
}
|
||||
|
||||
.promo-text h1 {
|
||||
font-size: 2rem; /* Adjust font size as needed */
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.promo-text p {
|
||||
font-size: 1.1rem; /* Slightly larger text for better readability */
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.promo-text .clip {
|
||||
background: var(--vp-home-hero-name-background);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: var(
|
||||
--vp-home-hero-name-color
|
||||
); /* Make the text color transparent to reveal background */
|
||||
font-weight: bold;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.promo-image {
|
||||
position: relative; /* To position the animated elements inside */
|
||||
flex: 1; /* Reduce the image's width */
|
||||
min-width: 150px; /* Ensures the image is smaller */
|
||||
max-width: 250px; /* Set a max width for the image */
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.promo-image img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
position: relative; /* Image is above the animated icons */
|
||||
z-index: 1; /* Ensure image is in front of the icons */
|
||||
}
|
||||
|
||||
/* X and O animation styles behind promo-image */
|
||||
.animated-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none; /* Ignore background interactions */
|
||||
z-index: 0; /* Ensure the animated elements are behind the image */
|
||||
}
|
||||
|
||||
.xo {
|
||||
position: absolute;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.xo-1,
|
||||
.xo-3 {
|
||||
color: var(--primary); /* X elements using --primary color */
|
||||
}
|
||||
|
||||
.xo-2,
|
||||
.xo-4 {
|
||||
color: var(--vp-c-text-1); /* O elements using --vp-c-text-1 color */
|
||||
}
|
||||
|
||||
/* Unique positions and animations for each XO element */
|
||||
.xo-1 {
|
||||
top: 10%;
|
||||
left: 15%;
|
||||
animation:
|
||||
rotate-small 6s infinite linear,
|
||||
move-elliptical 10s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.xo-2 {
|
||||
top: 40%;
|
||||
left: 60%;
|
||||
animation:
|
||||
rotate-large 8s infinite linear,
|
||||
move-circular 12s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.xo-3 {
|
||||
top: 70%;
|
||||
left: 30%;
|
||||
animation:
|
||||
rotate-reverse 5s infinite linear,
|
||||
move-diagonal 14s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.xo-4 {
|
||||
top: 20%;
|
||||
left: 80%;
|
||||
animation:
|
||||
rotate-small-reverse 7s infinite linear,
|
||||
move-random 16s infinite ease-in-out;
|
||||
}
|
||||
|
||||
/* Keyframe for self-rotation: Small */
|
||||
@keyframes rotate-small {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Keyframe for self-rotation: Large */
|
||||
@keyframes rotate-large {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(720deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Keyframe for reverse self-rotation */
|
||||
@keyframes rotate-reverse {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-small-reverse {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(-720deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Keyframe for elliptical movement */
|
||||
@keyframes move-elliptical {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
transform: translate(60px, 40px);
|
||||
}
|
||||
50% {
|
||||
transform: translate(120px, 0);
|
||||
}
|
||||
75% {
|
||||
transform: translate(60px, -40px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Keyframe for circular movement */
|
||||
@keyframes move-circular {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
transform: translate(50px, 50px);
|
||||
}
|
||||
50% {
|
||||
transform: translate(100px, 0);
|
||||
}
|
||||
75% {
|
||||
transform: translate(50px, -50px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Keyframe for diagonal movement */
|
||||
@keyframes move-diagonal {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
transform: translate(100px, 100px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Keyframe for random, unpredictable movement */
|
||||
@keyframes move-random {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
20% {
|
||||
transform: translate(40px, -30px);
|
||||
}
|
||||
40% {
|
||||
transform: translate(80px, 50px);
|
||||
}
|
||||
60% {
|
||||
transform: translate(-50px, -40px);
|
||||
}
|
||||
80% {
|
||||
transform: translate(30px, 70px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.promo {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.promo-image {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
.tool h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Wrapper for input section */
|
||||
.input-wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Flex container for dropdown and input */
|
||||
.form-group-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* Ensure responsiveness */
|
||||
gap: 15px; /* Space between dropdown and input */
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group-row.extended {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Form group styling */
|
||||
.form-group {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.select-group {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.input-group.extended {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.form-group select,
|
||||
.form-group input,
|
||||
.form-group button {
|
||||
width: 100%; /* Makes fields responsive */
|
||||
padding: 10px;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Extended input styling */
|
||||
.extended-input {
|
||||
width: 100%;
|
||||
min-height: 2.5rem;
|
||||
max-height: 300px;
|
||||
resize: none;
|
||||
padding: 0.5rem;
|
||||
line-height: 1.5;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.form-group button {
|
||||
background-color: #333;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-group button:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
/* Output section */
|
||||
.output-section {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.output-box-container {
|
||||
position: relative;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.output-box-label {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.output-box {
|
||||
box-sizing: content-box;
|
||||
border: 1px dashed #ccc;
|
||||
padding: 10px;
|
||||
min-height: 24px;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Gradient Button Styling */
|
||||
.gradient-button {
|
||||
padding: 10px 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
background: var(--vp-home-hero-name-background);
|
||||
border: none;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
border-radius: 10px; /* Full-rounded button */
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); /* Soft shadow */
|
||||
}
|
||||
|
||||
.gradient-button:hover {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.select-group {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Runtime API Examples
|
||||
|
||||
This page demonstrates usage of some of the runtime APIs provided by VitePress.
|
||||
|
||||
The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
|
||||
|
||||
```md
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { theme, page, frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
## Results
|
||||
|
||||
### Theme Data
|
||||
|
||||
<pre>{{ theme }}</pre>
|
||||
|
||||
### Page Data
|
||||
|
||||
<pre>{{ page }}</pre>
|
||||
|
||||
### Page Frontmatter
|
||||
|
||||
<pre>{{ frontmatter }}</pre>
|
||||
```
|
||||
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { site, theme, page, frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
## Results
|
||||
|
||||
### Theme Data
|
||||
|
||||
<pre>{{ theme }}</pre>
|
||||
|
||||
### Page Data
|
||||
|
||||
<pre>{{ page }}</pre>
|
||||
|
||||
### Page Frontmatter
|
||||
|
||||
<pre>{{ frontmatter }}</pre>
|
||||
|
||||
## More
|
||||
|
||||
Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).
|
||||
@@ -0,0 +1,9 @@
|
||||
# React
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install @xo/react
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -0,0 +1,9 @@
|
||||
# Svelte
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install @xo/svelte
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -0,0 +1,9 @@
|
||||
# Vue
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install @xo/vue
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,169 @@
|
||||
---
|
||||
# VitePress configuration
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: 'XO Stack'
|
||||
text: 'On Bitcoin Cash'
|
||||
tagline: Building smart applications on the BCH UTXO Network
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Tools
|
||||
link: /tools/primitives
|
||||
- theme: brand
|
||||
text: GitLab
|
||||
link: https://gitlab.com/GeneralProtocols/xo/stack
|
||||
---
|
||||
|
||||
<!--
|
||||
TODO: When we have docs public, move this back into the top config section above.
|
||||
- theme: brand
|
||||
text: Quick Start
|
||||
link: /quick-start
|
||||
-->
|
||||
|
||||
<!-- Promo section on front page with animated X and O behind the image -->
|
||||
<div class="promo">
|
||||
<div class="promo-text">
|
||||
<h1>Create robust scalable <span class="clip">Bitcoin Cash</span> apps, faster than ever before.</h1>
|
||||
<p>XO provides cutting-edge tools to help developers create Bitcoin Cash apps with ease and efficiency. With a focus on scalability, security, and speed, you can bring your app to life in no time.</p>
|
||||
<p>Start building now and unlock the full potential of Bitcoin Cash in the digital economy!</p>
|
||||
</div>
|
||||
<div class="promo-image">
|
||||
<!-- Animated X and O elements behind the image section -->
|
||||
<div class="animated-bg">
|
||||
<span class="xo xo-1">x</span>
|
||||
<span class="xo xo-2">o</span>
|
||||
<span class="xo xo-3">o</span>
|
||||
<span class="xo xo-4">x</span>
|
||||
</div>
|
||||
<img src="/icons/stack_light.png" alt="Stack Light Image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature section -->
|
||||
<div class="VPFeatures VPHomeFeatures">
|
||||
<div class="container">
|
||||
<h1 style="text-align:center;margin:20px">Stack Features</h1>
|
||||
<div class="items" style="">
|
||||
<div class="item grid-3">
|
||||
<div class="VPLink no-icon VPFeature feature-box-or">
|
||||
<article class="box">
|
||||
<img class="VPImage dark-only" height="48" width="48" src="/icons/api-dark.svg" alt="">
|
||||
<img class="VPImage light-only" height="48" width="48" src="/icons/api-light.svg" alt="">
|
||||
<h2 class="title">API</h2>
|
||||
<p class="details">Access powerful APIs to build smart apps on the BCH UTXO Network.</p>
|
||||
<!--v-if-->
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item grid-3">
|
||||
<div class="VPLink no-icon VPFeature feature-box-or">
|
||||
<article class="box">
|
||||
<img class="VPImage dark-only" height="48" width="48" src="/icons/framework-dark.svg" alt="">
|
||||
<img class="VPImage light-only" height="48" width="48" src="/icons/framework-light.svg" alt="">
|
||||
<h2 class="title">Framework</h2>
|
||||
<p class="details">A solid framework for building scalable applications.</p>
|
||||
<!--v-if-->
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item grid-3">
|
||||
<div class="VPLink no-icon VPFeature feature-box-or">
|
||||
<article class="box">
|
||||
<img class="VPImage dark-only" height="48" width="48" src="/icons/tools-dark.svg" alt="">
|
||||
<img class="VPImage light-only" height="48" width="48" src="/icons/tools-light.svg" alt="">
|
||||
<h2 class="title">Tools</h2>
|
||||
<p class="details">Essential tools to streamline development and deployment.</p>
|
||||
<!--v-if-->
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Logo section -->
|
||||
<div class="vp-doc" style="text-align: center; margin: 15px;">
|
||||
<h4>Developed by:</h4>
|
||||
</div>
|
||||
<div style="text-align: center; margin: 10px;">
|
||||
<a href="https://generalprotocols.com" target="_blank" style="display: inline-block;">
|
||||
<img class="light-only" src="./gp_logo_light.png" alt="GP Logo" style="width: 100px; height: auto;">
|
||||
<img class="dark-only" src="./gp_logo_dark.png" alt="GP Logo" style="width: 100px; height: auto;">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style scoped>
|
||||
.items {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 40px;
|
||||
}
|
||||
|
||||
.VPFeature {
|
||||
display: block;
|
||||
border: 1px solid var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
height: 100%;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
transition: border-color 0.25s, background-color 0.25s;
|
||||
}
|
||||
|
||||
.VPFeature.link:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.box > :deep(.VPImage) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-default-soft);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 24px;
|
||||
transition: background-color 0.25s;
|
||||
}
|
||||
|
||||
.title {
|
||||
line-height: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex-grow: 1;
|
||||
padding-top: 8px;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.link-text {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.link-text-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.link-text-icon {
|
||||
margin-left: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
### Double-Spends and Detecting Conflicting State
|
||||
|
||||
1. Currently DSP's are constrained to P2PKH.
|
||||
2. We may be able to extend these to **some** P2SH - but the requirement is that the P2SH must have an `OP_CHECKSIG`.
|
||||
3. There will be at least some (potentially many) kinds of P2SH that lack an `OP_CHECKSIG`.
|
||||
4. One problem that we have is that we cannot reliably determine (with Electrum) whether a P2SH may have been double-spent.
|
||||
5. ???
|
||||
6. Lack of profit
|
||||
|
||||
The point you're trying to get at is that Nodes on the network can have a differing mempool state.
|
||||
|
||||
How do we detect that? Fulcrum can't really provide us with that information.
|
||||
|
||||
How do we handle it if we can detect it? E.g. avoiding any "contentious" dependencies.
|
||||
|
||||
Do we have to store a whole graph?
|
||||
|
||||
For now, do owe just broadcast to one of our nodes and then broadcast to ALL of our nodes 5s later to see if there's contention?
|
||||
|
||||
### Electrum 4.0.1 Notes
|
||||
|
||||
1. Maybe default constructor generic to `ElectrumClientEvents`:
|
||||
|
||||
```ts
|
||||
class ElectrumClient<ElectrumEvents extends ElectrumClientEvents = ElectrumClientEvents>
|
||||
```
|
||||
|
||||
2. `RequestResponse` might be typed incorrectly: It now takes `RPCParameter | RPCParameter[]` which are defined as follows: `string | number | boolean | null;` (note that it only accept parameters, not responses which are often/maybe-always JSON).
|
||||
|
||||
Previously, it would accept:
|
||||
|
||||
```
|
||||
export type RequestResponse =
|
||||
| object
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| RequestResponse[];
|
||||
```
|
||||
|
||||
Perhaps we could type it: `export type RequestResponse = RPCParameter & object | Array<RPCParameter & object>;` (replace `object` with something like `RPCResponse`, but I think that might be taken already?)
|
||||
|
||||
3. `RPCNotification` might be typed incorrectly (or maybe this is for future proofing?). The `params` property is marked as optional (but I think all notifications will have at least one). Might be more elegant to have it required and just return an empty array.
|
||||
|
||||
4. The current approach still requires polyfills because `@electrum/sockets` itself is imported by `@electrum/network` and `@electrum/sockets` exports `ElectrumTcpSocket`.
|
||||
|
||||
A workable approach might be:
|
||||
|
||||
1. Move socket interface back to `@electum/network`.
|
||||
2. Move the `ElectrumWebSocket` back to `@electrum/network`.
|
||||
3. Rename `@electrum/sockets` to `@electrum/tcp-socket` and only include the TCP Socket in there.
|
||||
4. This will still allow third-parties to define their own sockets (e.g. `@custom/electrum-libp2p-socket`).
|
||||
|
||||
5. This is what's required for polyfilling in Vite currently:
|
||||
|
||||
```sh
|
||||
npm install events
|
||||
npm install vite-plugin-node-polyfills
|
||||
```
|
||||
|
||||
```js
|
||||
[
|
||||
nodePolyfills,
|
||||
{
|
||||
include: ['events', 'net', 'tls'],
|
||||
// NOTE: Not disabling these causes an error on Vite Polyfills V0.22.
|
||||
globals: {
|
||||
Buffer: false,
|
||||
global: false,
|
||||
process: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
```
|
||||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,218 @@
|
||||
# Quickstart
|
||||
|
||||
Install the XO Stack
|
||||
|
||||
```sh
|
||||
npm install @xo/stack
|
||||
```
|
||||
|
||||
Example Usage:
|
||||
|
||||
```ts
|
||||
// Import XO Modules.
|
||||
import { Address, BlockchainElectrum, StorageMemory } from '@xo/stack';
|
||||
|
||||
// The Electrum Servers we will be using.
|
||||
const ELECTRUM_SERVERS = [
|
||||
'bch.imaginary.cash',
|
||||
'cashnode.bch.ninja',
|
||||
'electrum.imaginary.cash',
|
||||
];
|
||||
|
||||
// Create an instance of the services that WalletHD will require.
|
||||
// NOTE: Other storage adapters currently broken, in the midst of refactor.
|
||||
const blockchainElectrum = new BlockchainElectrum(ELECTRUM_SERVERS);
|
||||
const cachesStore = new StorageMemory();
|
||||
const walletsStore = new StorageMemory();
|
||||
|
||||
// Start each of our services.
|
||||
await Promise.all([
|
||||
blockchainElectrum.start(),
|
||||
cachesStore.start(),
|
||||
walletsStore.start(),
|
||||
]);
|
||||
|
||||
// Create an instance of a HDWallet.
|
||||
const walletHD = await Wallet.from(
|
||||
{
|
||||
derivationPath: "m/44'/145'/0'",
|
||||
mnemonic:
|
||||
'page pencil stock planet limb cluster assault speak off joke private pioneer',
|
||||
},
|
||||
{
|
||||
blockchain: blockchainElectrum,
|
||||
cache: cachesStore,
|
||||
storage: walletsStore,
|
||||
},
|
||||
);
|
||||
|
||||
// Scan the wallet and start monitoring addresess for transactions.
|
||||
await walletHD.start();
|
||||
|
||||
// Send a transaction to the network, automatically populating UTXOs to match the output amount.
|
||||
// NOTE: Pre-signed inputs can be provided and change outputs will be automatically appended.
|
||||
const txResult = await walletHD.send({
|
||||
outputs: [
|
||||
{
|
||||
lockingBytecode: Address.fromCashAddr(
|
||||
'bitcoincash:qpc7mdjncqzf96vfzfaaqfrmru4rpg5qeq4m237t78',
|
||||
).toLockscriptBytes(),
|
||||
valueSatoshis: 10_000,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Structure/Patterns
|
||||
|
||||
General structure:
|
||||
|
||||
- All blockchain adapters will implement `IBlockhain`
|
||||
- All storage adapters will implement `IStorage`
|
||||
- All wallets will implement `IWallet`
|
||||
- Likewise for any other "service" we need in XO Stack (e.g. BCMR?, Prices?).
|
||||
|
||||
Most (almost all?) service methods in our interfaces will be `async`. This is so that we can handle situations where things might be remote (or require OS approval of some sort). For example:
|
||||
|
||||
1. Storage Adapters will need to allow backup to third-party services (so the methods must be `async`).
|
||||
2. Some Wallets **may** have remote dependencies (e.g. imagine a Multisig, Co-pay-like service).
|
||||
3. Wallets on IOS may have to tie in, in some way, with the host OS (to get approvals).
|
||||
4. There is also the `ServiceWorker` ideal where we would like to be able to run everything in a `ServiceWorker` and communicate to/from it from frontend. I'm not sure whether this will be realizable in practice yet.
|
||||
|
||||
The approach here aims to be extendable: If there is something that the XO Stack does not support, someone can:
|
||||
|
||||
1. Extend existing XO classes to customize it to their own needs
|
||||
2. BYO their own adapter
|
||||
3. Create their own wallet type that can still leverage other parts of XO stack
|
||||
|
||||
### Wallets
|
||||
|
||||
Wallets can be **composed** of other Wallets. For example:
|
||||
|
||||
- WalletP2PKH is standalone
|
||||
- WalletHD **has many** WalletP2PKH
|
||||
- SomeCustomWallet **may have many** WalletHD (or other kinds of wallet).
|
||||
|
||||
The only requirement is that a Wallet **MUST** implement the IWallet interface.
|
||||
|
||||
### Blockchain
|
||||
|
||||
Blockchains represent a connection to the BCH Network ("Blockchain" might not be the best term, but let's bikeshed over that later). This blockchain might be:
|
||||
|
||||
- Via Electrum-Cash (current implementation)
|
||||
- Via Chaingraph (not sure if we'll implement this, but it probably wouldn't be too difficult)
|
||||
- Local (as in, something like an in-memory "regtest")
|
||||
|
||||
We will likely want redundancies or possibly a fallback to a "local" blockchain in some cases where a device is disconnected from the internet (e.g. online merchant, offline customer).
|
||||
|
||||
The current plan (not implemented yet) is to create a main `Blockchain` class that can be instantiated something like follows:
|
||||
|
||||
```ts
|
||||
const blockchain = new Blockchain([
|
||||
new BlockchainElectrum(...),
|
||||
new BlockchainLocal(...),
|
||||
]);
|
||||
```
|
||||
|
||||
This `Blockchain` class will still implement the `IBlockchain` interface, but will try to direct requests to the correct Blockchain instance. This still needs more fleshing out.
|
||||
|
||||
### Stores
|
||||
|
||||
A **store** is just a means of storing data. It can be in-memory, to a file (e.g. SQLite), to somewhere remote (Firebase, MySQL, MongoDB, etc).
|
||||
|
||||
It is hierachical, meaning it can be structured like a tree (not just a simple KV table). This is in anticipation of WalletSets which likely going to have complex data structures. Ideally, where possible, we'll want our adapters to only mutate the relevant portion of an entry as opposed to the ENTIRE entry for a wallet (which would increase I/O and the risk of corruption).
|
||||
|
||||
As with `Blockchain`, we'll want support for redundancies (e.g. Local and Remote Backups).
|
||||
|
||||
The current plan (not implemented yet) is to create a `Storage` class that can be instantiated something like follows:
|
||||
|
||||
```ts
|
||||
const storage = new Storage([
|
||||
// Local store on Mobile Devices.
|
||||
new StorageSQLite(...),
|
||||
// GP's storage service.
|
||||
new StorageGPStore(...),
|
||||
]);
|
||||
```
|
||||
|
||||
This still needs a bit more fleshing out but, to allow syncing, UIDs (or WalletSet Activity IDs) will probably be used where possible (as opposed to autoincrement-like indexes).
|
||||
|
||||
### Double-Spends and Detecting Conflicting State
|
||||
|
||||
1. Currently DSP's are constrained to P2PKH.
|
||||
2. We may be able to extend these to **some** P2SH - but the requirement is that the P2SH must have an `OP_CHECKSIG`.
|
||||
3. There will be at least some (potentially many) kinds of P2SH that lack an `OP_CHECKSIG`.
|
||||
4. One problem that we have is that we cannot reliably determine (with Electrum) whether a P2SH may have been double-spent.
|
||||
5. ???
|
||||
6. Lack of profit
|
||||
|
||||
The point you're trying to get at is that Nodes on the network can have a differing mempool state.
|
||||
|
||||
How do we detect that? Fulcrum can't really provide us with that information.
|
||||
|
||||
How do we handle it if we can detect it? E.g. avoiding any "contentious" dependencies.
|
||||
|
||||
Do we have to store a whole graph?
|
||||
|
||||
Do we just broadcast to one of our nodes and then broadcast to ALL of our nodes ~5s later to see if there's contention?
|
||||
|
||||
### Electrum 4.0.1 Notes
|
||||
|
||||
1. Maybe default constructor generic to `ElectrumClientEvents`:
|
||||
|
||||
```ts
|
||||
class ElectrumClient<ElectrumEvents extends ElectrumClientEvents = ElectrumClientEvents>
|
||||
```
|
||||
|
||||
2. `RequestResponse` might be typed incorrectly: It now takes `RPCParameter | RPCParameter[]` which are defined as follows: `string | number | boolean | null;` (note that it only accept parameters, not responses which are often/maybe-always JSON).
|
||||
|
||||
Previously, it would accept:
|
||||
|
||||
```
|
||||
export type RequestResponse =
|
||||
| object
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| RequestResponse[];
|
||||
```
|
||||
|
||||
Perhaps we could type it: `export type RequestResponse = RPCParameter & object | Array<RPCParameter & object>;` (replace `object` with something like `RPCResponse`, but I think that might be taken already?)
|
||||
|
||||
3. `RPCNotification` might be typed incorrectly (or maybe this is for future proofing?). The `params` property is marked as optional (but I think all notifications will have at least one). Might be more elegant to have it required and just return an empty array.
|
||||
|
||||
4. The current approach still requires polyfills because `@electrum/sockets` itself is imported by `@electrum/network` and `@electrum/sockets` exports `ElectrumTcpSocket`.
|
||||
|
||||
A workable approach might be:
|
||||
|
||||
1. Move socket interface back to `@electum/network`.
|
||||
2. Move the `ElectrumWebSocket` back to `@electrum/network`.
|
||||
3. Rename `@electrum/sockets` to `@electrum/tcp-socket` and only include the TCP Socket in there.
|
||||
4. This will still allow third-parties to define their own sockets (e.g. `@custom/electrum-libp2p-socket`).
|
||||
|
||||
5. This is what's required for polyfilling in Vite currently:
|
||||
|
||||
```sh
|
||||
npm install events
|
||||
npm install vite-plugin-node-polyfills
|
||||
```
|
||||
|
||||
```js
|
||||
[
|
||||
nodePolyfills,
|
||||
{
|
||||
include: ['events', 'net', 'tls'],
|
||||
// NOTE: Not disabling these causes an error on Vite Polyfills V0.22.
|
||||
globals: {
|
||||
Buffer: false,
|
||||
global: false,
|
||||
process: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
## Markdown Extensions (TODO: Remove me later)
|
||||
|
||||
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).
|
||||
@@ -0,0 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<!-- Resume Markdown parsing -->
|
||||
TODO: Once we have other tools, we might want to turn this into a page like that on Quick Start that presents a "box" of each Tool Group.
|
||||
@@ -0,0 +1,605 @@
|
||||
import {
|
||||
Address,
|
||||
BlockHeader,
|
||||
Bytes,
|
||||
encodeExtendedJson,
|
||||
decodeExtendedJson,
|
||||
Hasher,
|
||||
HDPrivateNode,
|
||||
HDPublicNode,
|
||||
Mnemonic,
|
||||
PrivateKey,
|
||||
PublicKey,
|
||||
Satoshis,
|
||||
Script,
|
||||
Transaction,
|
||||
} from '@xocash/primitives';
|
||||
|
||||
export const address = {
|
||||
from: [
|
||||
{
|
||||
label: 'From',
|
||||
inputs: ['CashAddr or Legacy Address'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Address.from(...args),
|
||||
extendedInput: true,
|
||||
},
|
||||
{
|
||||
label: 'From CashAddr',
|
||||
inputs: ['CashAddr'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Address.fromCashAddr(...args),
|
||||
extendedInput: true,
|
||||
},
|
||||
{
|
||||
label: 'From Legacy',
|
||||
inputs: ['Legacy Address'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Address.fromLegacy(...args),
|
||||
extendedInput: true,
|
||||
},
|
||||
{
|
||||
label: 'From Redeem Script Hex (P2SH20)',
|
||||
inputs: ['RedeemScript Hex'],
|
||||
models: [''],
|
||||
callback: (args: [string]) =>
|
||||
Address.fromRedeemScriptHex(...args, 'P2SH20'),
|
||||
extendedInput: true,
|
||||
},
|
||||
{
|
||||
label: 'From Redeem Script Hex (P2SH32)',
|
||||
inputs: ['RedeemScript Hex'],
|
||||
models: [''],
|
||||
callback: (args: [string]) =>
|
||||
Address.fromRedeemScriptHex(...args, 'P2SH32'),
|
||||
extendedInput: true,
|
||||
},
|
||||
{
|
||||
label: 'From Lockscript Hex',
|
||||
inputs: ['Lockscript Hex'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Address.fromLockscriptHex(...args),
|
||||
extendedInput: true,
|
||||
},
|
||||
{
|
||||
label: 'From Hash160 Hex',
|
||||
inputs: ['Hash160 Hex'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Address.fromHash160Hex(...args),
|
||||
extendedInput: true,
|
||||
},
|
||||
],
|
||||
to: [
|
||||
{
|
||||
label: 'To CashAddr (mainnet)',
|
||||
callback: (address: Address) => address.toCashAddr('bitcoincash'),
|
||||
},
|
||||
{
|
||||
label: 'To CashAddr (testnet)',
|
||||
callback: (address: Address) => address.toCashAddr('bchtest'),
|
||||
},
|
||||
{
|
||||
label: 'To CashAddr (regtest)',
|
||||
callback: (address: Address) => address.toCashAddr('bchreg'),
|
||||
},
|
||||
{
|
||||
label: 'To TokenAware CashAddr (mainnet)',
|
||||
callback: (address: Address) => address.toTokenAddress('bitcoincash'),
|
||||
},
|
||||
{
|
||||
label: 'To TokenAware CashAddr (testnet)',
|
||||
callback: (address: Address) => address.toTokenAddress('bchtest'),
|
||||
},
|
||||
{
|
||||
label: 'To TokenAware CashAddr (regtest)',
|
||||
callback: (address: Address) => address.toTokenAddress('bchreg'),
|
||||
},
|
||||
{
|
||||
label: 'To Legacy',
|
||||
callback: (address: Address) => address.toLegacy(),
|
||||
},
|
||||
{
|
||||
label: 'To Hash160/RIPEMD Hex',
|
||||
callback: (address: Address) => address.toHash160Hex(),
|
||||
},
|
||||
{
|
||||
label: 'To Lockscript Hex',
|
||||
callback: (address: Address) => address.toLockscriptHex(),
|
||||
},
|
||||
{
|
||||
label: 'To Lockscript ASM',
|
||||
callback: (address: Address) => address.toLockscriptAsm(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const blockHeader = {
|
||||
from: [
|
||||
{
|
||||
label: 'From Hex',
|
||||
inputs: ['UTF-8'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => BlockHeader.fromHex(...args),
|
||||
},
|
||||
],
|
||||
to: [
|
||||
{
|
||||
label: 'To Raw',
|
||||
callback: (blockHeader: BlockHeader) => blockHeader.toRaw(),
|
||||
},
|
||||
{
|
||||
label: 'Version',
|
||||
callback: (blockHeader: BlockHeader) => blockHeader.getVersion(),
|
||||
},
|
||||
{
|
||||
label: 'Previous Block Hash',
|
||||
callback: (blockHeader: BlockHeader) =>
|
||||
blockHeader.getPreviousBlockHashHex(),
|
||||
},
|
||||
{
|
||||
label: 'Merkle Root',
|
||||
callback: (blockHeader: BlockHeader) => blockHeader.getMerkleRootHex(),
|
||||
},
|
||||
{
|
||||
label: 'Timestamp',
|
||||
callback: (blockHeader: BlockHeader) =>
|
||||
blockHeader.getTimestamp().toString(),
|
||||
},
|
||||
{
|
||||
label: 'Bits',
|
||||
callback: (blockHeader: BlockHeader) => blockHeader.getBits().toString(),
|
||||
},
|
||||
{
|
||||
label: 'Nonce',
|
||||
callback: (blockHeader: BlockHeader) => blockHeader.getNonce().toString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const bytes = {
|
||||
from: [
|
||||
{
|
||||
label: 'From Base64',
|
||||
inputs: ['Base64'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Bytes.fromBase64(...args),
|
||||
},
|
||||
{
|
||||
label: 'From Hex',
|
||||
inputs: ['Hex'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Bytes.fromHex(...args),
|
||||
},
|
||||
{
|
||||
label: 'From UTF-8',
|
||||
inputs: ['UTF-8'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Bytes.fromUtf8(...args),
|
||||
},
|
||||
],
|
||||
to: [
|
||||
{
|
||||
label: 'To Base64',
|
||||
callback: (bytes: Bytes) => bytes.toBase64(),
|
||||
},
|
||||
{
|
||||
label: 'To Hex',
|
||||
callback: (bytes: Bytes) => bytes.toHex(),
|
||||
},
|
||||
{
|
||||
label: 'To UTF-8',
|
||||
callback: (bytes: Bytes) => bytes.toUtf8(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const hasher = {
|
||||
from: [
|
||||
{
|
||||
label: 'From Hex',
|
||||
inputs: ['Hex'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Hasher.fromHex(...args),
|
||||
},
|
||||
{
|
||||
label: 'From Base64',
|
||||
inputs: ['Base64'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Hasher.fromBase64(...args),
|
||||
},
|
||||
{
|
||||
label: 'From UTF-8',
|
||||
inputs: ['UTF-8'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Hasher.fromUTF8(...args),
|
||||
},
|
||||
],
|
||||
to: [
|
||||
{
|
||||
label: 'To Sha256 Hex',
|
||||
callback: (hasher: Hasher) => hasher.toSHA256Hex(),
|
||||
},
|
||||
{
|
||||
label: 'To RipeMD160 Hex',
|
||||
callback: (hasher: Hasher) => hasher.toRIPEMD160Hex(),
|
||||
},
|
||||
{
|
||||
label: 'To Hash160 Hex',
|
||||
callback: (hasher: Hasher) => hasher.toHash160Hex(),
|
||||
},
|
||||
{
|
||||
label: 'To Hash256 Hex',
|
||||
callback: (hasher: Hasher) => hasher.toHash256Hex(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const hdPrivateNode = {
|
||||
from: [
|
||||
{
|
||||
label: 'Generate Random',
|
||||
inputs: ['Derivation Path'],
|
||||
models: ["m/44'/145'/0'/0/1"],
|
||||
callback: () => HDPrivateNode.generateRandom(),
|
||||
},
|
||||
{
|
||||
label: 'From Mnemonic',
|
||||
inputs: ['Mnemonic', 'Derivation Path'],
|
||||
models: ['', "m/44'/145'/0'/0/1"],
|
||||
callback: (args: [string]) => HDPrivateNode.fromMnemonic(...args),
|
||||
},
|
||||
{
|
||||
label: 'From XPriv Key',
|
||||
inputs: ['XPriv Key', 'Derivation Path'],
|
||||
models: ['', "m/44'/145'/0'/0/1"],
|
||||
callback: (args: [string]) => HDPrivateNode.fromXPriv(...args),
|
||||
},
|
||||
{
|
||||
label: 'From Raw',
|
||||
inputs: ['Raw (LibAuth Format/ExtJSON Encoded)'],
|
||||
models: [''],
|
||||
callback: (args: [string]) =>
|
||||
HDPrivateNode.fromRaw(decodeExtendedJson(args.join(''))),
|
||||
extendedInput: true,
|
||||
},
|
||||
],
|
||||
to: [
|
||||
{
|
||||
label: 'To XPriv Key',
|
||||
callback: (hdPrivateNode: HDPrivateNode) => hdPrivateNode.toXPriv(),
|
||||
},
|
||||
{
|
||||
label: 'To String',
|
||||
callback: (hdPrivateNode: HDPrivateNode) => hdPrivateNode.toString(),
|
||||
},
|
||||
{
|
||||
label: 'To Raw (LibAuth Format/ExtJSON Encoded)',
|
||||
callback: (hdPrivateNode: HDPrivateNode) =>
|
||||
encodeExtendedJson(hdPrivateNode.toRaw(), 2),
|
||||
},
|
||||
{
|
||||
label: 'To XPub Key',
|
||||
callback: (hdPrivateNode: HDPrivateNode) =>
|
||||
hdPrivateNode.deriveHDPublicNode().toXPub(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const hdPublicNode = {
|
||||
from: [
|
||||
{
|
||||
label: 'From XPub Key',
|
||||
inputs: ['XPub Key'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => HDPublicNode.fromXPub(...args),
|
||||
},
|
||||
{
|
||||
label: 'From Raw',
|
||||
inputs: ['Raw (LibAuth Format/ExtJSON Encoded)'],
|
||||
models: [''],
|
||||
callback: (args: [string]) =>
|
||||
HDPublicNode.fromRaw(decodeExtendedJson(args.join(''))),
|
||||
extendedInput: true,
|
||||
},
|
||||
],
|
||||
to: [
|
||||
{
|
||||
label: 'To XPub Key',
|
||||
callback: (hdPublicNode: HDPublicNode) => hdPublicNode.toXPub(),
|
||||
},
|
||||
{
|
||||
label: 'To String',
|
||||
callback: (hdPublicNode: HDPublicNode) => hdPublicNode.toString(),
|
||||
},
|
||||
{
|
||||
label: 'To Raw (LibAuth Format/ExtJSON Encoded)',
|
||||
callback: (hdPublicNode: HDPublicNode) =>
|
||||
encodeExtendedJson(hdPublicNode.toRaw(), 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const mnemonic = {
|
||||
from: [
|
||||
{
|
||||
label: 'Generate Random',
|
||||
callback: () => Mnemonic.generateRandom(),
|
||||
},
|
||||
{
|
||||
label: 'From String',
|
||||
inputs: ['String'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Mnemonic.fromString(...args),
|
||||
extendedInput: true,
|
||||
},
|
||||
],
|
||||
to: [
|
||||
{
|
||||
label: 'To String (without passphrase)',
|
||||
callback: (mnemonic: Mnemonic) => mnemonic.toString(false),
|
||||
},
|
||||
{
|
||||
label: 'To String (with passphrase)',
|
||||
callback: (mnemonic: Mnemonic) => mnemonic.toString(true),
|
||||
},
|
||||
{
|
||||
label: 'Words',
|
||||
callback: (mnemonic: Mnemonic) => mnemonic.toWords(),
|
||||
},
|
||||
{
|
||||
label: 'Passphrase',
|
||||
callback: (mnemonic: Mnemonic) => mnemonic.getPassphrase(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// TODO: Message Signing outputs. Maybe add an `extension` flag to the `to` that allows another input field and uses that to generate the outputs
|
||||
export const privateKey = {
|
||||
from: [
|
||||
{
|
||||
label: 'Generate Random',
|
||||
callback: () => PrivateKey.generateRandom(),
|
||||
},
|
||||
{
|
||||
label: 'From Hex',
|
||||
inputs: ['Hex'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => PrivateKey.fromHex(...args),
|
||||
},
|
||||
{
|
||||
label: 'From WIF',
|
||||
inputs: ['WIF'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => PrivateKey.fromWIF(...args),
|
||||
},
|
||||
],
|
||||
to: [
|
||||
{
|
||||
label: 'To Hex',
|
||||
callback: (privateKey: PrivateKey) => privateKey.toHex(),
|
||||
},
|
||||
{
|
||||
label: 'To WIF',
|
||||
callback: (privateKey: PrivateKey) => privateKey.toWif(),
|
||||
},
|
||||
{
|
||||
label: 'To String',
|
||||
callback: (privateKey: PrivateKey) => privateKey.toString(),
|
||||
},
|
||||
{
|
||||
label: 'Derived Public Key (Hex)',
|
||||
callback: (privateKey: PrivateKey) =>
|
||||
privateKey.derivePublicKey().toString(),
|
||||
},
|
||||
{
|
||||
label: 'Derived BCH Address (CashAddr)',
|
||||
callback: (privateKey: PrivateKey) =>
|
||||
privateKey.derivePublicKey().deriveAddress().toCashAddr('bitcoincash'),
|
||||
},
|
||||
],
|
||||
methods: [
|
||||
{
|
||||
label: 'Sign Message (Schnorr)',
|
||||
inputs: ['UTF8'],
|
||||
models: ['Your text here'],
|
||||
callback: (privateKey: PrivateKey, args: [string]) =>
|
||||
Bytes.fromBytes(privateKey.signMessageSchnorr(...args)).toHex(),
|
||||
},
|
||||
{
|
||||
label: 'Sign Message (DER)',
|
||||
inputs: ['UTF8'],
|
||||
models: ['Your text here'],
|
||||
callback: (privateKey: PrivateKey, args: [string]) =>
|
||||
Bytes.fromBytes(privateKey.signMessageDER(...args)).toHex(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const publicKey = {
|
||||
from: [
|
||||
{
|
||||
label: 'From Hex',
|
||||
inputs: ['Hex'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => PublicKey.fromHex(...args),
|
||||
},
|
||||
{
|
||||
label: 'From WIF',
|
||||
inputs: ['WIF'],
|
||||
models: [''],
|
||||
callback: (args: [string]) =>
|
||||
PrivateKey.fromWIF(...args).derivePublicKey(),
|
||||
},
|
||||
],
|
||||
to: [
|
||||
{
|
||||
label: 'Derived Hash160 Hex',
|
||||
callback: (publicKey: PublicKey) => publicKey.deriveHash160Hex(),
|
||||
},
|
||||
{
|
||||
label: 'Derived Address',
|
||||
callback: (publicKey: PublicKey) =>
|
||||
publicKey.deriveAddress().toCashAddr(),
|
||||
},
|
||||
{
|
||||
label: 'To Hex',
|
||||
callback: (publicKey: PublicKey) => publicKey.toHex(),
|
||||
},
|
||||
{
|
||||
label: 'To String',
|
||||
callback: (publicKey: PublicKey) => publicKey.toString(),
|
||||
},
|
||||
],
|
||||
methods: [
|
||||
{
|
||||
label: 'Verify Message (Schnorr)',
|
||||
inputs: ['Message (UTF8)', 'Signature (Hex)'],
|
||||
models: ['Your text here'],
|
||||
callback: (publicKey: PublicKey, args: [string, string]) =>
|
||||
publicKey.verifyMessageSchnorr(...args).toString(),
|
||||
},
|
||||
{
|
||||
label: 'Verify Message (DER)',
|
||||
inputs: ['Message (UTF8)', 'Signature (Hex)'],
|
||||
models: ['Your text here'],
|
||||
callback: (publicKey: PublicKey, args: [string, string]) =>
|
||||
publicKey.verifyMessageDER(...args).toString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const satoshis = {
|
||||
from: [
|
||||
{
|
||||
label: 'From Sats',
|
||||
inputs: ['Sats'],
|
||||
models: [''],
|
||||
callback: (args: [number]) => Satoshis.fromSats(...args),
|
||||
},
|
||||
{
|
||||
label: 'From Bits',
|
||||
inputs: ['Bits'],
|
||||
models: [''],
|
||||
callback: (args: [number]) => Satoshis.fromBits(...args),
|
||||
},
|
||||
{
|
||||
label: 'From MBCH',
|
||||
inputs: ['MBCH'],
|
||||
models: [''],
|
||||
callback: (args: [number]) => Satoshis.fromMBCH(...args),
|
||||
},
|
||||
{
|
||||
label: 'From BCH',
|
||||
inputs: ['BCH'],
|
||||
models: [''],
|
||||
callback: (args: [number]) => Satoshis.fromBCH(...args),
|
||||
},
|
||||
],
|
||||
to: [
|
||||
{
|
||||
label: 'To Sats',
|
||||
callback: (satoshis: Satoshis) => satoshis.toSats().toString(),
|
||||
},
|
||||
{
|
||||
label: 'To Bits',
|
||||
callback: (satoshis: Satoshis) => satoshis.toBits().toString(),
|
||||
},
|
||||
{
|
||||
label: 'To MBCH',
|
||||
callback: (satoshis: Satoshis) => satoshis.toMBCH().toString(),
|
||||
},
|
||||
{
|
||||
label: 'To BCH',
|
||||
callback: (satoshis: Satoshis) => satoshis.toBCH().toString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const script = {
|
||||
from: [
|
||||
{
|
||||
label: 'From ASM',
|
||||
inputs: ['ASM'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Script.fromASM(...args),
|
||||
},
|
||||
{
|
||||
label: 'From Hex',
|
||||
inputs: ['Hex'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Script.fromHex(...args),
|
||||
},
|
||||
],
|
||||
to: [
|
||||
{
|
||||
label: 'To ASM',
|
||||
callback: (script: Script) => script.toASM(),
|
||||
},
|
||||
{
|
||||
label: 'To Hex',
|
||||
callback: (script: Script) => script.toHex(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const transaction = {
|
||||
from: [
|
||||
{
|
||||
label: 'From Hex',
|
||||
inputs: ['Hex'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Transaction.fromHex(...args),
|
||||
extendedInput: true,
|
||||
},
|
||||
{
|
||||
label: 'From Raw',
|
||||
inputs: ['Raw (LibAuth Format/ExtJSON Encoded)'],
|
||||
models: [''],
|
||||
callback: (args: [...string[]]) =>
|
||||
Transaction.fromRaw(decodeExtendedJson(args.join(''))),
|
||||
extendedInput: true,
|
||||
},
|
||||
],
|
||||
to: [
|
||||
{
|
||||
label: 'To Transaction Hash Hex',
|
||||
callback: (transaction: Transaction) => transaction.toHashHex(),
|
||||
},
|
||||
{
|
||||
label: 'To Raw (LibAuth Format/ExtJSON Encoded)',
|
||||
callback: (transaction: Transaction) =>
|
||||
encodeExtendedJson(transaction.toRaw(), 2),
|
||||
},
|
||||
{
|
||||
label: 'To Hex',
|
||||
callback: (transaction: Transaction) => transaction.toHex(),
|
||||
},
|
||||
{
|
||||
label: 'Version',
|
||||
callback: (transaction: Transaction) =>
|
||||
transaction.getVersion().toString(),
|
||||
},
|
||||
{
|
||||
label: 'Inputs',
|
||||
callback: (transaction: Transaction) => {
|
||||
return encodeExtendedJson(transaction.getInputs(), 2);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Outputs',
|
||||
callback: (transaction: Transaction) => {
|
||||
return encodeExtendedJson(transaction.getOutputs(), 2);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Locktime',
|
||||
callback: (transaction: Transaction) =>
|
||||
transaction.getLocktime().toString(),
|
||||
},
|
||||
{
|
||||
label: 'Is Coinbase',
|
||||
callback: (transaction: Transaction) =>
|
||||
transaction.isCoinbase().toString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
<script lang="ts" setup>
|
||||
import PrimitiveToolVue, { type FromFunction, type ToFunction } from '@theme/components/PrimitiveTool.vue'
|
||||
|
||||
// Import our tool functions
|
||||
import {
|
||||
address,
|
||||
blockHeader,
|
||||
bytes,
|
||||
hasher,
|
||||
hdPrivateNode,
|
||||
hdPublicNode,
|
||||
mnemonic,
|
||||
privateKey,
|
||||
publicKey,
|
||||
satoshis,
|
||||
script,
|
||||
transactionHash,
|
||||
transaction,
|
||||
} from './primitive-functions.ts'
|
||||
</script>
|
||||
|
||||
<!-- Resume Markdown parsing -->
|
||||
|
||||
# Primitive Tools
|
||||
|
||||
## Address
|
||||
<div class="tool">
|
||||
<p>A Utility tool for converting between different address formats.</p>
|
||||
|
||||
<PrimitiveToolVue
|
||||
:fromFunctions="address.from"
|
||||
:toFunctions="address.to"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Block Header
|
||||
<div class="tool">
|
||||
<p>A utility tool for converting between different byte representations.</p>
|
||||
|
||||
<PrimitiveToolVue
|
||||
:fromFunctions="blockHeader.from"
|
||||
:toFunctions="blockHeader.to"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Bytes
|
||||
<div class="tool">
|
||||
<p>A utility tool for converting between different byte representations.</p>
|
||||
|
||||
<PrimitiveToolVue
|
||||
:fromFunctions="bytes.from"
|
||||
:toFunctions="bytes.to"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Hasher
|
||||
<div class="tool">
|
||||
<p>A utility tool for computing various cryptographic hashes commonly used in Bitcoin Cash.</p>
|
||||
|
||||
<PrimitiveToolVue
|
||||
:fromFunctions="hasher.from"
|
||||
:toFunctions="hasher.to"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## HD Private Node
|
||||
<div class="tool">
|
||||
<p>A utility tool for generating and manipulating HD Private Nodes.</p>
|
||||
|
||||
<PrimitiveToolVue
|
||||
:fromFunctions="hdPrivateNode.from"
|
||||
:toFunctions="hdPrivateNode.to"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## HD Public Node
|
||||
<div class="tool">
|
||||
<p>A utility tool for generating and manipulating HD Public Nodes.</p>
|
||||
|
||||
<PrimitiveToolVue
|
||||
:fromFunctions="hdPublicNode.from"
|
||||
:toFunctions="hdPublicNode.to"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Mnemonic
|
||||
<div class="tool">
|
||||
<p>A utility tool for generating and validating BIP39 Mnemonics. Note that mnemonics will be normalized by removing redundant whitespace and will also be validated against the BIP39 English Word List (other word-lists can be specified via the API)</p>
|
||||
|
||||
<PrimitiveToolVue
|
||||
:fromFunctions="mnemonic.from"
|
||||
:toFunctions="mnemonic.to"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Private Key
|
||||
<div class="tool">
|
||||
<p>A utility tool for generating and manipulating Private Keys.</p>
|
||||
|
||||
<PrimitiveToolVue
|
||||
:fromFunctions="privateKey.from"
|
||||
:toFunctions="privateKey.to"
|
||||
:methods="privateKey.methods"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Public Key
|
||||
<div class="tool">
|
||||
<p>A utility tool for generating and manipulating Public Keys.</p>
|
||||
|
||||
<PrimitiveToolVue
|
||||
:fromFunctions="publicKey.from"
|
||||
:toFunctions="publicKey.to"
|
||||
:methods="publicKey.methods"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Satoshis
|
||||
<div class="tool">
|
||||
<p>A utility tool for converting between Bitcoin Cash value units.</p>
|
||||
|
||||
<PrimitiveToolVue
|
||||
:fromFunctions="satoshis.from"
|
||||
:toFunctions="satoshis.to"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Script
|
||||
<div class="tool">
|
||||
<p>A utility tool for generating and manipulating Script objects.</p>
|
||||
|
||||
<PrimitiveToolVue
|
||||
:fromFunctions="script.from"
|
||||
:toFunctions="script.to"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Transaction
|
||||
<div class="tool">
|
||||
<p>A utility tool for generating and manipulating Transaction objects.</p>
|
||||
|
||||
<PrimitiveToolVue
|
||||
:fromFunctions="transaction.from"
|
||||
:toFunctions="transaction.to"
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Bytes } from '@xocash/primitives';
|
||||
|
||||
import { Encryption } from '@xocash/stack';
|
||||
|
||||
export const encryption = {
|
||||
from: [
|
||||
{
|
||||
label: 'From Passphrase',
|
||||
inputs: ['Passphrase'],
|
||||
models: [''],
|
||||
callback: (args: [string]) => Encryption.fromPassphrase(...args),
|
||||
extendedInput: true,
|
||||
},
|
||||
],
|
||||
to: [],
|
||||
methods: [
|
||||
{
|
||||
label: 'Encrypt',
|
||||
inputs: ['UTF8'],
|
||||
models: ['Your text here'],
|
||||
callback: async (encryption: Encryption, args: [string]) =>
|
||||
Bytes.fromBytes(await encryption.encrypt(...args)).toHex(),
|
||||
},
|
||||
{
|
||||
label: 'Decrypt',
|
||||
inputs: ['Hex'],
|
||||
models: ['Encrypted Data (hex)'],
|
||||
callback: async (encryption: Encryption, args: [string]) =>
|
||||
Bytes.fromBytes(
|
||||
await encryption.decrypt(Bytes.fromHex(...args).toBytes()),
|
||||
).toUtf8(),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import PrimitiveToolVue, { type FromFunction, type ToFunction } from '@theme/components/PrimitiveTool.vue'
|
||||
|
||||
// Import our tool functions
|
||||
import {
|
||||
encryption
|
||||
} from './utils-functions.ts'
|
||||
</script>
|
||||
|
||||
<!-- Resume Markdown parsing -->
|
||||
|
||||
# Utils
|
||||
|
||||
## Encryption
|
||||
<div class="tool">
|
||||
<p>A Utility tool for converting between different address formats.</p>
|
||||
|
||||
<PrimitiveToolVue
|
||||
:fromFunctions="encryption.from"
|
||||
:toFunctions="encryption.to"
|
||||
:methods="encryption.methods"
|
||||
/>
|
||||
</div>
|
||||