@@ -30,6 +30,12 @@ export default defineConfig({
|
|||||||
title: 'XO Stack',
|
title: 'XO Stack',
|
||||||
description: 'XO Stack',
|
description: 'XO Stack',
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
|
// Set the logo
|
||||||
|
logo: {
|
||||||
|
light: '/logo-light.svg',
|
||||||
|
dark: '/logo-dark.svg',
|
||||||
|
},
|
||||||
|
|
||||||
// Enable search functionality.
|
// Enable search functionality.
|
||||||
search: {
|
search: {
|
||||||
provider: 'local',
|
provider: 'local',
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,17 +1,13 @@
|
|||||||
// https://vitepress.dev/guide/custom-theme
|
// https://vitepress.dev/guide/custom-theme
|
||||||
import { h } from 'vue'
|
import type { Theme } from 'vitepress';
|
||||||
import type { Theme } from 'vitepress'
|
import DefaultTheme from 'vitepress/theme';
|
||||||
import DefaultTheme from 'vitepress/theme'
|
import Layout from './Layout.vue';
|
||||||
import './style.css'
|
import './style.css';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
extends: DefaultTheme,
|
extends: DefaultTheme,
|
||||||
Layout: () => {
|
Layout,
|
||||||
return h(DefaultTheme.Layout, null, {
|
|
||||||
// https://vitepress.dev/guide/extending-default-theme#layout-slots
|
|
||||||
})
|
|
||||||
},
|
|
||||||
enhanceApp({ app, router, siteData }) {
|
enhanceApp({ app, router, siteData }) {
|
||||||
// ...
|
// ...
|
||||||
}
|
},
|
||||||
} satisfies Theme
|
} satisfies Theme;
|
||||||
|
|||||||
@@ -70,6 +70,22 @@
|
|||||||
--vp-c-danger-soft: var(--vp-c-red-soft);
|
--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
|
* 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 |
@@ -10,8 +10,8 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "npm run clean && tsc",
|
||||||
"build-old": "rollup -c"
|
"clean": "rm -r ./dist"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ export * from './public-key.js';
|
|||||||
export * from './satoshis.js';
|
export * from './satoshis.js';
|
||||||
export * from './script.js';
|
export * from './script.js';
|
||||||
export * from './transaction.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { TransactionHash } from './transaction-hash.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
binToHex,
|
binToHex,
|
||||||
hexToBin,
|
hexToBin,
|
||||||
@@ -69,6 +71,18 @@ export class Transaction {
|
|||||||
return binToHex(bytes);
|
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() {
|
public getVersion() {
|
||||||
return this.transaction.version;
|
return this.transaction.version;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Transaction } from '@xo/primitives';
|
||||||
import { AddressNotification, ElectrumRequest } from './blockchain-types.js';
|
import { AddressNotification, ElectrumRequest } from './blockchain-types.js';
|
||||||
|
|
||||||
import { ElectrumClient } from '@electrum-cash/network';
|
import { ElectrumClient } from '@electrum-cash/network';
|
||||||
@@ -75,6 +76,22 @@ export class Blockchain {
|
|||||||
return response;
|
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) {
|
async subscribeAddress(address: string, callback: AddressCallback) {
|
||||||
await this.electrumClient.subscribe(
|
await this.electrumClient.subscribe(
|
||||||
'blockchain.address.subscribe',
|
'blockchain.address.subscribe',
|
||||||
|
|||||||
Reference in New Issue
Block a user