Signed-off-by: James Zuccon <zuccon@gmail.com>
This commit is contained in:
James Zuccon
2024-09-06 10:59:30 +10:00
parent b9a53659ce
commit f189c4643b
12 changed files with 311 additions and 13 deletions
+6
View File
@@ -30,6 +30,12 @@ export default defineConfig({
title: 'XO Stack',
description: 'XO Stack',
themeConfig: {
// Set the logo
logo: {
light: '/logo-light.svg',
dark: '/logo-dark.svg',
},
// Enable search functionality.
search: {
provider: 'local',
+14
View File
@@ -0,0 +1,14 @@
<!--.vitepress/theme/MyLayout.vue-->
<script setup>
import DefaultTheme from 'vitepress/theme'
const { Layout } = DefaultTheme
</script>
<template>
<Layout>
<template #aside-outline-before>
My custom sidebar top content
</template>
</Layout>
</template>
+7 -11
View File
@@ -1,17 +1,13 @@
// https://vitepress.dev/guide/custom-theme
import { h } from 'vue'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import './style.css'
import type { Theme } from 'vitepress';
import DefaultTheme from 'vitepress/theme';
import Layout from './Layout.vue';
import './style.css';
export default {
extends: DefaultTheme,
Layout: () => {
return h(DefaultTheme.Layout, null, {
// https://vitepress.dev/guide/extending-default-theme#layout-slots
})
},
Layout,
enhanceApp({ app, router, siteData }) {
// ...
}
} satisfies Theme
},
} satisfies Theme;
+16
View File
@@ -70,6 +70,22 @@
--vp-c-danger-soft: var(--vp-c-red-soft);
}
.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: #b2fb54 !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: #b2fb54 !important;
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 80 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 80 KiB

+2 -2
View File
@@ -10,8 +10,8 @@
"dist"
],
"scripts": {
"build": "tsc",
"build-old": "rollup -c"
"build": "npm run clean && tsc",
"clean": "rm -r ./dist"
},
"author": "",
"license": "ISC"
+2
View File
@@ -8,3 +8,5 @@ export * from './public-key.js';
export * from './satoshis.js';
export * from './script.js';
export * from './transaction.js';
export * from './transaction-graph.js';
export * from './transaction-hash.js';
@@ -0,0 +1,125 @@
import { Transaction } from '@xo/primitives';
import { binToHex, Output } from '@bitauth/libauth';
export interface UTXO extends Output {
outpointTransactionHash: string;
outpointIndex: number;
}
export class TransactionGraph {
private transactions: Map<string, Transaction>;
constructor() {
this.transactions = new Map();
}
addTransaction(tx: Transaction) {
this.transactions.set(tx.toHashHex(), tx);
}
removeTransaction(txHash: string) {
this.transactions.delete(txHash);
}
getTransaction(txHash: string): Transaction | undefined {
return this.transactions.get(txHash);
}
getAllTransactions(): Transaction[] {
return Array.from(this.transactions.values());
}
deriveUtxoState(): UTXO[] {
const utxoMap = new Map<string, UTXO>();
// First, add all outputs as potential UTXOs
for (const tx of this.transactions.values()) {
tx.getOutputs().forEach((output, outpointIndex) => {
const txHashHex = tx.toHashHex();
const utxo: UTXO = {
outpointTransactionHash: txHashHex,
outpointIndex,
valueSatoshis: output.valueSatoshis,
lockingBytecode: output.lockingBytecode,
};
utxoMap.set(`${txHashHex}:${outpointIndex}`, utxo);
});
}
// Then, remove all spent outputs
for (const tx of this.transactions.values()) {
tx.getInputs().forEach((input) => {
const outpointTransactionHashHex = binToHex(
input.outpointTransactionHash
);
utxoMap.delete(`${outpointTransactionHashHex}:${input.outpointIndex}`);
});
}
return Array.from(utxoMap.values());
}
isTransactionValid(tx: Transaction): boolean {
const currentUtxos = this.deriveUtxoState();
const utxoMap = new Map(
currentUtxos.map((utxo) => [
`${utxo.outpointTransactionHash}:${utxo.outpointIndex}`,
utxo,
])
);
return tx.getInputs().every((input) => {
const outpointTransactionHashHex = binToHex(
input.outpointTransactionHash
);
return utxoMap.has(
`${outpointTransactionHashHex}:${input.outpointIndex}`
);
});
}
getDependentTransactions(txHash: string): Transaction[] {
return this.getAllTransactions().filter((tx) => {
tx.getInputs().some((input) => {
const outpointTransactionHashHex = binToHex(
input.outpointTransactionHash
);
return outpointTransactionHashHex === txHash;
});
});
}
getBroadcastOrder(): Transaction[] {
const visited = new Set<string>();
const order: Transaction[] = [];
const visit = (txid: string) => {
if (visited.has(txid)) return;
const tx = this.transactions.get(txid);
if (!tx) return;
visited.add(txid);
// First, visit all transactions that this transaction depends on
for (const input of tx.getInputs()) {
const outpointTransactionHashHex = binToHex(
input.outpointTransactionHash
);
visit(outpointTransactionHashHex);
}
order.push(tx);
};
// Start the topological sort from each transaction
for (const tx of this.transactions.values()) {
visit(tx.toHashHex());
}
return order;
}
}
+106
View File
@@ -0,0 +1,106 @@
import {
binToHex,
hexToBin,
hashTransaction,
isHex,
} from '@bitauth/libauth';
/**
* Transaction Hash Primitive.
*/
export class TransactionHash
{
public readonly bytes: Uint8Array;
/**
* Instantiates TransactionHash from a Transaction Hash's bytes.
*
* @param bytes {Uint8Array} The Transaction Hash's bytes.
*/
constructor(bytes: Uint8Array)
{
// If bytes is not exactly 32, then this is an invalid transaction hash...
if(bytes.length !== 32)
{
throw(new Error(`Invalid Transaction Hash: Expected 32 bytes but received ${bytes.length}`));
}
this.bytes = bytes;
}
/**
* Instantiates TransactionHash from a hex string.
*
* @param hex {string} The hex string.
*
* @throws {Error} If hex is invalid.
*
* @returns {TransactionHash} Instance of TransactionHash.
*/
public static fromHex(hex: string): TransactionHash
{
if(!isHex(hex))
{
throw(new Error('Invalid hex provided'));
}
const bytes = hexToBin(hex);
return new TransactionHash(bytes);
}
/**
* Instantiates TransactionHash from a Transaction's bytes.
*
* @param transactionBytes {Uint8Array} The Transaction's bytes.
*
* @returns {TransactionHash} Instance of TransactionHash.
*/
public static fromTransactionBytes(transactionBytes: Uint8Array): TransactionHash
{
const transactionHash = hashTransaction(transactionBytes);
return new TransactionHash(hexToBin(transactionHash));
}
/**
* Instantiates TransactionHash from a Transaction's bytes in hex.
*
* @param transactionHex {Uint8Array} The Transaction's bytes in hex.
*
* @throws {Error} If hex is invalid.
*
* @returns {TransactionHash} Instance of TransactionHash.
*/
public static fromTransactionHex(transactionHex: string): TransactionHash
{
if(!isHex(transactionHex))
{
throw(new Error('Invalid hex provided'));
}
const bytes = hexToBin(transactionHex);
return TransactionHash.fromTransactionBytes(bytes);
}
/**
* Returns transaction hash as a hex string.
*
* @returns {string} Hex string of the Transaction Hash.
*/
public toHex(): string
{
return binToHex(this.bytes);
}
/**
* Returns transaction hash as a Uint8Array.
*
* @returns {Uint8Array} Uint8Array of the Transaction Hash.
*/
public toBytes(): Uint8Array
{
return this.bytes;
}
}
+14
View File
@@ -1,3 +1,5 @@
import { TransactionHash } from './transaction-hash.js';
import {
binToHex,
hexToBin,
@@ -69,6 +71,18 @@ export class Transaction {
return binToHex(bytes);
}
public toHash(): TransactionHash {
return TransactionHash.fromTransactionBytes(this.toBytes());
}
public toHashHex(): string {
return this.toHash().toHex();
}
public toHashBytes(): Uint8Array {
return this.toHash().toBytes();
}
public getVersion() {
return this.transaction.version;
}
+17
View File
@@ -1,3 +1,4 @@
import { Transaction } from '@xo/primitives';
import { AddressNotification, ElectrumRequest } from './blockchain-types.js';
import { ElectrumClient } from '@electrum-cash/network';
@@ -75,6 +76,22 @@ export class Blockchain {
return response;
}
// TODO: async commit() method
// This won't actually broadcast, this will just commit the transaction to our graph.
// The reason we want this is because, for protos like BIP70, we want service to verify the tx and broadcast it.
// And we just want to be able to monitor whether service did the needful (broadcasted it).
// This should accept options: { waitForSeen: boolean }
async commitTransaction(
transaction: Transaction,
opts: { waitUntilSeen: boolean } = { waitUntilSeen: true }
) {
console.log(transaction);
console.log(opts);
}
// TODO: async broadcast() method
// This should accept options: { waitForSeen: boolean }
async subscribeAddress(address: string, callback: AddressCallback) {
await this.electrumClient.subscribe(
'blockchain.address.subscribe',