diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index fbaf17c..895dfd3 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -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',
diff --git a/docs/.vitepress/theme/Layout.vue b/docs/.vitepress/theme/Layout.vue
new file mode 100644
index 0000000..5970ec5
--- /dev/null
+++ b/docs/.vitepress/theme/Layout.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ My custom sidebar top content
+
+
+
\ No newline at end of file
diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts
index def4cfc..9e01c32 100644
--- a/docs/.vitepress/theme/index.ts
+++ b/docs/.vitepress/theme/index.ts
@@ -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;
diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css
index d63aee8..0ac631e 100644
--- a/docs/.vitepress/theme/style.css
+++ b/docs/.vitepress/theme/style.css
@@ -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
* -------------------------------------------------------------------------- */
diff --git a/docs/logo-dark.svg b/docs/logo-dark.svg
new file mode 100644
index 0000000..a8e5dd5
--- /dev/null
+++ b/docs/logo-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/logo-light.svg b/docs/logo-light.svg
new file mode 100644
index 0000000..675c98d
--- /dev/null
+++ b/docs/logo-light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/primitives/package.json b/packages/primitives/package.json
index fd680c6..468d1b6 100644
--- a/packages/primitives/package.json
+++ b/packages/primitives/package.json
@@ -10,8 +10,8 @@
"dist"
],
"scripts": {
- "build": "tsc",
- "build-old": "rollup -c"
+ "build": "npm run clean && tsc",
+ "clean": "rm -r ./dist"
},
"author": "",
"license": "ISC"
diff --git a/packages/primitives/src/index.ts b/packages/primitives/src/index.ts
index 9a80c35..129f108 100644
--- a/packages/primitives/src/index.ts
+++ b/packages/primitives/src/index.ts
@@ -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';
diff --git a/packages/primitives/src/transaction-graph.ts b/packages/primitives/src/transaction-graph.ts
new file mode 100644
index 0000000..7e3a182
--- /dev/null
+++ b/packages/primitives/src/transaction-graph.ts
@@ -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;
+
+ 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();
+
+ // 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();
+ 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;
+ }
+}
diff --git a/packages/primitives/src/transaction-hash.ts b/packages/primitives/src/transaction-hash.ts
new file mode 100644
index 0000000..01e8db5
--- /dev/null
+++ b/packages/primitives/src/transaction-hash.ts
@@ -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;
+ }
+}
diff --git a/packages/primitives/src/transaction.ts b/packages/primitives/src/transaction.ts
index 1f03cba..f9c5be7 100644
--- a/packages/primitives/src/transaction.ts
+++ b/packages/primitives/src/transaction.ts
@@ -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;
}
diff --git a/packages/wallet/src/wallet/blockchain.ts b/packages/wallet/src/wallet/blockchain.ts
index cec3f48..91ea11d 100644
--- a/packages/wallet/src/wallet/blockchain.ts
+++ b/packages/wallet/src/wallet/blockchain.ts
@@ -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',