Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c87ad46a48 |
+1
-1
@@ -1,4 +1,4 @@
|
||||
image: node:24
|
||||
image: node:22
|
||||
|
||||
# We use stages for clarity of intent, and "needs" for dependencies and sequencing.
|
||||
# "needs" should work in parallel where acceptable, and get any artifacts directly from the source step.
|
||||
|
||||
Generated
+5707
-4155
File diff suppressed because it is too large
Load Diff
+9
-10
@@ -54,26 +54,25 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.10.0",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.66.0",
|
||||
"@typescript-eslint/parser": "^8.66.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
||||
"@typescript-eslint/parser": "^8.5.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"eslint": "^9.10.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"lerna": "^10.0.0",
|
||||
"lerna": "^9.0.3",
|
||||
"madge": "^8.0.0",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "^8.66.0",
|
||||
"typescript": "5.7.2",
|
||||
"typescript-eslint": "^8.6.0",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"overrides": {
|
||||
"nx": {
|
||||
"brace-expansion": "5.0.9"
|
||||
"vitepress": {
|
||||
"vite": "^7.1.10"
|
||||
},
|
||||
"lerna": {
|
||||
"js-yaml": "4.3.1",
|
||||
"tar": "7.5.22"
|
||||
"vitepress-plugin-mermaid": {
|
||||
"vite": "^7.1.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
"@monaco-editor/loader": "^1.5.0",
|
||||
"@xocash/primitives": "*",
|
||||
"@xocash/stack": "*",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"vitepress-mermaid-renderer": "^1.2.0"
|
||||
"monaco-editor": "^0.52.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typedoc": "^0.28.14",
|
||||
"typedoc-plugin-markdown": "^4.3.2",
|
||||
"typedoc-vitepress-theme": "^1.1.1",
|
||||
"vitepress": "2.0.0-alpha.18"
|
||||
"vitepress": "^1.6.4",
|
||||
"vitepress-plugin-mermaid": "^2.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { withMermaid } from 'vitepress-plugin-mermaid';
|
||||
import typedocSidebar from '../api/typedoc-sidebar.json';
|
||||
|
||||
// Node imports for auto blog sidebar (works at docs/src/.vitepress)
|
||||
@@ -38,8 +39,8 @@ function toPascalCase(str: string) {
|
||||
.map((word) => {
|
||||
if (word.length === 0) return '';
|
||||
|
||||
if (word in SPECIAL_CASES) {
|
||||
return SPECIAL_CASES[word as keyof typeof SPECIAL_CASES];
|
||||
if (SPECIAL_CASES[word]) {
|
||||
return SPECIAL_CASES[word];
|
||||
}
|
||||
|
||||
// Capitalize first letter, lowercase rest
|
||||
@@ -113,208 +114,210 @@ function blogSidebar() {
|
||||
}
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
outDir: '../dist',
|
||||
title: 'XO Stack',
|
||||
description:
|
||||
'XO Stack: Building smart applications on the Bitcoin Cash Network',
|
||||
head: [
|
||||
// Favicon.
|
||||
['link', { rel: 'icon', href: '/favicon.png' }],
|
||||
// Basic OpenGraph tags
|
||||
['meta', { property: 'og:type', content: 'website' }],
|
||||
['meta', { property: 'og:title', content: 'XO Stack' }],
|
||||
[
|
||||
'meta',
|
||||
{
|
||||
property: 'og:description',
|
||||
content:
|
||||
'XO Stack: Building smart applications on the Bitcoin Cash Network',
|
||||
},
|
||||
export default withMermaid(
|
||||
defineConfig({
|
||||
outDir: '../dist',
|
||||
title: 'XO Stack',
|
||||
description:
|
||||
'XO Stack: Building smart applications on the Bitcoin Cash Network',
|
||||
head: [
|
||||
// Favicon.
|
||||
['link', { rel: 'icon', href: '/favicon.png' }],
|
||||
// Basic OpenGraph tags
|
||||
['meta', { property: 'og:type', content: 'website' }],
|
||||
['meta', { property: 'og:title', content: 'XO Stack' }],
|
||||
[
|
||||
'meta',
|
||||
{
|
||||
property: 'og:description',
|
||||
content:
|
||||
'XO Stack: Building smart applications on the Bitcoin Cash Network',
|
||||
},
|
||||
],
|
||||
[
|
||||
'meta',
|
||||
{
|
||||
property: 'og:image',
|
||||
content: 'https://stack.xo.cash/stack-opengraph.jpg',
|
||||
},
|
||||
],
|
||||
['meta', { property: 'og:url', content: 'https://stack.xo.cash/' }],
|
||||
|
||||
// Optional additional tags
|
||||
['meta', { property: 'og:site_name', content: 'XO Stack' }],
|
||||
|
||||
// Twitter Card tags (optional but recommended)
|
||||
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
|
||||
['meta', { name: 'twitter:site', content: '@yourtwitterhandle' }],
|
||||
],
|
||||
[
|
||||
'meta',
|
||||
{
|
||||
property: 'og:image',
|
||||
content: 'https://stack.xo.cash/stack-opengraph.jpg',
|
||||
themeConfig: {
|
||||
// Set the logo
|
||||
logo: {
|
||||
light: '/logo-light.svg',
|
||||
dark: '/logo-dark.svg',
|
||||
},
|
||||
],
|
||||
['meta', { property: 'og:url', content: 'https://stack.xo.cash/' }],
|
||||
|
||||
// Optional additional tags
|
||||
['meta', { property: 'og:site_name', content: 'XO Stack' }],
|
||||
|
||||
// Twitter Card tags (optional but recommended)
|
||||
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
|
||||
['meta', { name: 'twitter:site', content: '@yourtwitterhandle' }],
|
||||
],
|
||||
themeConfig: {
|
||||
// Set the logo
|
||||
logo: {
|
||||
light: '/logo-light.svg',
|
||||
dark: '/logo-dark.svg',
|
||||
},
|
||||
|
||||
// Enable search functionality.
|
||||
// TODO: Re-enable once we release documentation.
|
||||
/*
|
||||
// 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: 'Blog', link: '/blog' },
|
||||
{ text: 'Tools', link: '/tools/primitives' },
|
||||
{ text: 'Playground', link: '/playground' },
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
// Default Side Menu.
|
||||
'/': [
|
||||
{
|
||||
text: 'Guide',
|
||||
items: [
|
||||
{ text: 'Quick Start', link: '/quick-start' },
|
||||
{ text: 'Architecture', link: '/architecture' },
|
||||
{ text: 'Best Practices', link: '/api-examples' },
|
||||
{ text: 'Contributing', link: '/contributing' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Frameworks',
|
||||
items: [
|
||||
{ text: 'Vue', link: '/frameworks/vue' },
|
||||
{ text: 'React', link: '/frameworks/react' },
|
||||
{ text: 'Svelte', link: '/frameworks/svelte' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'API',
|
||||
items: convertNavToPascalCase(typedocSidebar),
|
||||
},
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: 'Home', link: '/' },
|
||||
// TODO: Re-enable once we release documentation
|
||||
// { text: 'Documentation', link: '/quick-start' },
|
||||
{ text: 'Blog', link: '/blog' },
|
||||
{ text: 'Tools', link: '/tools/primitives' },
|
||||
{ text: 'Playground', link: '/playground' },
|
||||
],
|
||||
// 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: 'Encrypter',
|
||||
link: '/tools/primitives#encrypter',
|
||||
},
|
||||
{
|
||||
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',
|
||||
link: '/tools/stack/blockchain',
|
||||
items: [
|
||||
{
|
||||
text: 'Electrum',
|
||||
link: '/tools/stack/blockchain#electrum',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
sidebar: {
|
||||
// Default Side Menu.
|
||||
'/': [
|
||||
{
|
||||
text: 'Guide',
|
||||
items: [
|
||||
{ text: 'Quick Start', link: '/quick-start' },
|
||||
{ text: 'Architecture', link: '/architecture' },
|
||||
{ text: 'Best Practices', link: '/api-examples' },
|
||||
{ text: 'Contributing', link: '/contributing' },
|
||||
],
|
||||
},
|
||||
{
|
||||
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: 'Encrypter',
|
||||
link: '/tools/primitives#encrypter',
|
||||
},
|
||||
{
|
||||
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',
|
||||
link: '/tools/stack/blockchain',
|
||||
items: [
|
||||
{
|
||||
text: 'Electrum',
|
||||
link: '/tools/stack/blockchain#electrum',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
// Blog sidebar (auto-generated)
|
||||
'/blog/': blogSidebar(),
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
// { icon: 'github', link: 'https://gitlab.com/GeneralProtocols/xo/stack' },
|
||||
{ icon: 'x', link: 'https://x.com/xo_bitcoincash' },
|
||||
],
|
||||
// Blog sidebar (auto-generated)
|
||||
'/blog/': blogSidebar(),
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
// { icon: 'github', link: 'https://gitlab.com/GeneralProtocols/xo/stack' },
|
||||
{ icon: 'x', link: 'https://x.com/xo_bitcoincash' },
|
||||
],
|
||||
|
||||
// This is the outline on the right-hand side of the page.
|
||||
outline: {
|
||||
level: [2, 4],
|
||||
label: 'On this page',
|
||||
},
|
||||
},
|
||||
|
||||
// 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',
|
||||
// This is the outline on the right-hand side of the page.
|
||||
outline: {
|
||||
level: [2, 4],
|
||||
label: 'On this page',
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
// Configure to use browser version of @xocash/stack
|
||||
conditions: ['browser', 'import', 'module', 'default'],
|
||||
},
|
||||
// SSR has its own separate ssr.resolve.conditions that must be explicitly set since not follow global resolve conditions.
|
||||
ssr: {
|
||||
|
||||
// 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',
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
// Configure to use browser version of @xocash/stack
|
||||
conditions: ['browser', 'import', 'module', 'default'],
|
||||
},
|
||||
// SSR has its own separate ssr.resolve.conditions that must be explicitly set since not follow global resolve conditions.
|
||||
ssr: {
|
||||
resolve: {
|
||||
// Configure to use browser version of @xocash/stack
|
||||
conditions: ['browser', 'import', 'module', 'default'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,61 +1,15 @@
|
||||
import { h, nextTick, watch } from 'vue';
|
||||
// https://vitepress.dev/guide/custom-theme
|
||||
import type { Theme } from 'vitepress';
|
||||
import { useData } from 'vitepress';
|
||||
import DefaultTheme from 'vitepress/theme';
|
||||
import Layout from './Layout.vue';
|
||||
|
||||
import './style.css';
|
||||
import './custom.css';
|
||||
import './tools.css';
|
||||
import './mermaid-overrides.css';
|
||||
|
||||
import { createMermaidRenderer } from 'vitepress-mermaid-renderer';
|
||||
import './mermaid-lightbox.client';
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
|
||||
Layout: () => {
|
||||
const { isDark, localeIndex } = useData();
|
||||
|
||||
const initMermaid = () => {
|
||||
const mermaidRenderer = createMermaidRenderer({
|
||||
theme: isDark.value ? 'dark' : 'forest',
|
||||
});
|
||||
|
||||
mermaidRenderer.setToolbar({
|
||||
showLanguageLabel: false,
|
||||
downloadFormat: 'svg',
|
||||
fullscreenMode: 'browser',
|
||||
desktop: {
|
||||
copyCode: 'enabled',
|
||||
toggleFullscreen: 'enabled',
|
||||
resetView: 'enabled',
|
||||
zoomOut: 'enabled',
|
||||
zoomIn: 'enabled',
|
||||
zoomLevel: 'enabled',
|
||||
download: 'enabled',
|
||||
},
|
||||
fullscreen: {
|
||||
copyCode: 'disabled',
|
||||
toggleFullscreen: 'enabled',
|
||||
resetView: 'disabled',
|
||||
zoomLevel: 'disabled',
|
||||
download: 'enabled',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
nextTick(initMermaid);
|
||||
|
||||
watch(
|
||||
() => [isDark.value, localeIndex.value] as const,
|
||||
() => {
|
||||
initMermaid();
|
||||
},
|
||||
);
|
||||
|
||||
return h(Layout);
|
||||
},
|
||||
|
||||
enhanceApp() {},
|
||||
Layout,
|
||||
enhanceApp({ _app, _router, _siteData }) {},
|
||||
} satisfies Theme;
|
||||
|
||||
@@ -36,26 +36,20 @@ export class Encrypter {
|
||||
...opts,
|
||||
};
|
||||
|
||||
// Something about the browser crypto API. It needs a "clean" Uint8Array, where TS could see the construction or something
|
||||
const cleanKey = new Uint8Array(key);
|
||||
|
||||
// Create a key derivation key
|
||||
const keyMaterialBase = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
cleanKey,
|
||||
key,
|
||||
'PBKDF2',
|
||||
false,
|
||||
['deriveBits', 'deriveKey'],
|
||||
);
|
||||
|
||||
// Something about the browser crypto API. It needs a "clean" Uint8Array, where TS could see the construction or something
|
||||
const cleanSalt = new Uint8Array(encryptionOpts.salt);
|
||||
|
||||
// Derive the actual encryption key
|
||||
const derivedKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: cleanSalt,
|
||||
salt: encryptionOpts.salt,
|
||||
iterations: encryptionOpts.iterations,
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
@@ -93,9 +87,6 @@ export class Encrypter {
|
||||
// Generate random IV
|
||||
const iv = crypto.getRandomValues(new Uint8Array(this.opts.ivLength));
|
||||
|
||||
// Something about the browser crypto API. It needs a "clean" Uint8Array, where TS could see the construction or something
|
||||
const cleanData = new Uint8Array(data);
|
||||
|
||||
// Encrypt the data
|
||||
const encryptedData = await crypto.subtle.encrypt(
|
||||
{
|
||||
@@ -103,7 +94,7 @@ export class Encrypter {
|
||||
iv,
|
||||
},
|
||||
this.key,
|
||||
cleanData,
|
||||
data,
|
||||
);
|
||||
|
||||
// Combine IV and encrypted data
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"@generalprotocols/oracle-client": "^0.0.1-development.11878773826",
|
||||
"@xocash/primitives": "*",
|
||||
"async-mutex": "^0.5.0",
|
||||
"better-sqlite3": "^13.0.3",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"idb": "8.0.3",
|
||||
"json-to-graphql-query": "^2.3.0",
|
||||
"msgpackr": "^1.11.2",
|
||||
|
||||
@@ -297,16 +297,10 @@ export class StoreRemoteHTTP<T = unknown> extends BaseStore<
|
||||
'X-Timestamp': timestamp.toString(),
|
||||
};
|
||||
|
||||
const body = payload ? pack(payload) : pack({});
|
||||
const bodyBuffer = new Uint8Array(body.buffer);
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers,
|
||||
// Because we support the DOM types, it doesnt register this UInt8Array as an acceptable BodyInit, so we need to cast it to unknown.
|
||||
// Could be fixed in a later version of typescript, but there are build issues atm.
|
||||
// TODO: (not really, unless we intend to use this code) Remove the type cast.
|
||||
body: bodyBuffer as unknown as BodyInit,
|
||||
body: payload ? pack(payload) : pack({}),
|
||||
};
|
||||
|
||||
// Use the URL class to compile the full path.
|
||||
|
||||
@@ -0,0 +1,699 @@
|
||||
import { BaseWallet } from './base-wallet.js';
|
||||
import {
|
||||
type TransactionTemplate,
|
||||
type WalletBlockchain,
|
||||
type WalletDependencies,
|
||||
} from './types.js';
|
||||
import { TransactionBuilder } from './transaction-builder.js';
|
||||
import { WalletP2PKH } from './wallet-p2pkh.js';
|
||||
|
||||
import { type BaseStore, Activities, StoreInMemory } from '../storage/index.js';
|
||||
import { type DistributiveOmit } from '../utils/index.js';
|
||||
|
||||
import {
|
||||
type TokenBalances,
|
||||
HDPrivateNode,
|
||||
ExtMap,
|
||||
Mnemonic,
|
||||
calculateBalanceSats,
|
||||
calculateBalanceTokens,
|
||||
} from '@xocash/primitives';
|
||||
|
||||
import { binToNumberUint32LE, hexToBin } from '@bitauth/libauth';
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Constants
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* XO derivation purpose path. All XO key derivation starts from this BIP43 purpose node.
|
||||
* This matches engine/crypto's `XO_BASE_PATH = m/2415'`.
|
||||
*/
|
||||
export const XO_PURPOSE_PATH = "m/2415'";
|
||||
|
||||
/**
|
||||
* Default number of consecutive empty addresses before stopping the address gap scan.
|
||||
* If this many sequential child indexes produce addresses with no on-chain activity,
|
||||
* we assume there are no more used keys beyond this point.
|
||||
*/
|
||||
export const XO_ADDRESS_GAP = 100;
|
||||
|
||||
/**
|
||||
* Template key use case index for signing and spending (e.g. Schnorr signatures, P2PKH).
|
||||
* Matches engine/crypto's `TemplateKeyUseCase.SigningAndSpending = 0`.
|
||||
*/
|
||||
export const XO_USE_CASE_SIGNING_AND_SPENDING = 0;
|
||||
|
||||
/**
|
||||
* Template key use case index for encryption.
|
||||
* Not yet implemented in engine/crypto, but we scan it for forward-compatibility.
|
||||
* Matches engine/crypto's `AccountKeyUseCase.Encryption = 1`.
|
||||
*/
|
||||
export const XO_USE_CASE_ENCRYPTION = 1;
|
||||
|
||||
/**
|
||||
* All use case indexes to scan for each template.
|
||||
* We check both so that keys derived under either use case are discovered.
|
||||
*/
|
||||
export const XO_USE_CASES = [
|
||||
XO_USE_CASE_SIGNING_AND_SPENDING,
|
||||
XO_USE_CASE_ENCRYPTION,
|
||||
];
|
||||
|
||||
/**
|
||||
* Maximum valid non-hardened BIP32 child index (2^31 - 1).
|
||||
* Matches engine/crypto's `NON_HARDENED_INDEX_MAX`.
|
||||
*/
|
||||
const NON_HARDENED_INDEX_MAX = 0x7fffffff;
|
||||
|
||||
/**
|
||||
* Expected length of a template identifier hex string (32 bytes = 64 hex chars).
|
||||
* Template identifiers are SHA-256 hashes represented as hex.
|
||||
*/
|
||||
const TEMPLATE_IDENTIFIER_HEX_LENGTH = 64;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Types
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Entropy sources for WalletXO. Accepts the same formats as WalletHD:
|
||||
* a hex seed string, an extended private key, or a Mnemonic instance.
|
||||
*/
|
||||
export type WalletXOEntropy =
|
||||
| { seed: string }
|
||||
| { xpriv: string }
|
||||
| { mnemonic: Mnemonic };
|
||||
|
||||
/**
|
||||
* Genesis data for constructing a WalletXO instance.
|
||||
* Contains the entropy source, the list of template identifiers to scan,
|
||||
* and an optional address gap override.
|
||||
*/
|
||||
export type WalletXOGenesisData = {
|
||||
type: 'WalletXO';
|
||||
templateIdentifiers: string[];
|
||||
addressGap?: number;
|
||||
} & WalletXOEntropy;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Utilities
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a 31-bit BIP32 non-hardened child index from a template identifier hex string.
|
||||
* This mirrors the engine/crypto `createPathIndexFromHash` function but operates on
|
||||
* the raw template identifier bytes directly (template identifiers are already 32-byte hashes).
|
||||
*
|
||||
* The algorithm reads the first 4 bytes as a little-endian uint32, then masks to 31 bits
|
||||
* to guarantee a valid non-hardened index (0 to 2,147,483,647).
|
||||
*
|
||||
* @see engine/crypto/source/utils.ts - createPathIndexFromHash
|
||||
* @param templateIdentifier - A 64-character hex string representing a 32-byte template identifier
|
||||
* @returns Path index (31-bit unsigned integer, 0 to 2,147,483,647)
|
||||
*/
|
||||
const createPathIndexFromTemplateIdentifier = (
|
||||
templateIdentifier: string,
|
||||
): number => {
|
||||
// Convert the hex template identifier to raw bytes.
|
||||
const hashBytes = hexToBin(templateIdentifier);
|
||||
|
||||
// Read the first 4 bytes as a little-endian uint32.
|
||||
const uint32FirstFourBytes = binToNumberUint32LE(hashBytes.slice(0, 4));
|
||||
|
||||
// Mask to 31 bits to stay within the valid non-hardened index range.
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return uint32FirstFourBytes & NON_HARDENED_INDEX_MAX;
|
||||
};
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// WalletXO
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A wallet that implements the same HD key derivation as the XO Crypto package.
|
||||
* This allows a user to import the mnemonic used with an XO Templates wallet
|
||||
* and then derive keys based on a template identifier.
|
||||
*
|
||||
* Crypto Package Reference: engine/crypto/source/derivation.ts
|
||||
*
|
||||
* Derivation Path: m/2415'/{templateIndex}'/{useCaseIndex}'/{childIndex}
|
||||
*
|
||||
* - templateIndex: derived from the first 4 bytes of the template identifier
|
||||
* (little-endian uint32, masked to 31 bits). This is computed via the same
|
||||
* algorithm as engine/crypto's `createPathIndexFromHash`.
|
||||
* - useCaseIndex: 0 (SigningAndSpending) or 1 (Encryption). Both are scanned.
|
||||
* - childIndex: 0..N, scanned using address gap logic to discover used keys.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* // Create a WalletXO from a mnemonic and template identifiers.
|
||||
* const walletXO = await WalletXO.from(
|
||||
* { blockchain },
|
||||
* {
|
||||
* mnemonic: Mnemonic.from('abandon abandon abandon ... about'),
|
||||
* templateIdentifiers: ['aabb...64hexchars...'],
|
||||
* },
|
||||
* );
|
||||
*
|
||||
* // Scan for transactions and start monitoring.
|
||||
* await walletXO.start();
|
||||
*
|
||||
* // Dynamically add a new template later.
|
||||
* await walletXO.addTemplate('ccdd...64hexchars...');
|
||||
* ```
|
||||
*/
|
||||
export class WalletXO extends BaseWallet {
|
||||
/**
|
||||
* Factory method to create a new WalletXO instance with default dependencies.
|
||||
* Fills in optional dependencies (activities, cache) with defaults if not provided.
|
||||
*
|
||||
* @param deps - Wallet dependencies (blockchain required, activities/cache optional)
|
||||
* @param genesisData - Entropy source + template identifiers + optional address gap
|
||||
* @returns A new WalletXO instance
|
||||
*/
|
||||
static async from<T extends typeof WalletXO>(
|
||||
this: T,
|
||||
deps: WalletDependencies,
|
||||
genesisData: DistributiveOmit<WalletXOGenesisData, 'type'>,
|
||||
) {
|
||||
// Assign defaults if not otherwise provided.
|
||||
const activities = deps.activities
|
||||
? deps.activities
|
||||
: await Activities.from();
|
||||
const cache = deps.cache ? deps.cache : StoreInMemory.from();
|
||||
|
||||
// Assemble the final dependencies with all required fields.
|
||||
const depsFinal = {
|
||||
activities,
|
||||
cache,
|
||||
blockchain: deps.blockchain,
|
||||
};
|
||||
|
||||
// Return a new instance of WalletXO.
|
||||
return new this(depsFinal, genesisData) as InstanceType<T>;
|
||||
}
|
||||
|
||||
// Dependencies.
|
||||
public readonly activities: Activities;
|
||||
public readonly blockchain: WalletBlockchain;
|
||||
public readonly cache: BaseStore;
|
||||
|
||||
// Genesis Data.
|
||||
public readonly genesisData: WalletXOGenesisData;
|
||||
|
||||
/** The root HD node derived from the provided entropy source. */
|
||||
public readonly hdPrivateNode: HDPrivateNode;
|
||||
|
||||
/**
|
||||
* The XO purpose HD node at m/2415'.
|
||||
* All template key derivations are relative to this node, matching
|
||||
* the engine/crypto package's `derivePurposeHDNodeFromSeed` output.
|
||||
*/
|
||||
public readonly purposeNode: HDPrivateNode;
|
||||
|
||||
/**
|
||||
* Child wallets grouped by template identifier and use case index.
|
||||
* Structure: Map<templateIdentifier, { [useCaseIndex]: WalletP2PKH[] }>
|
||||
*/
|
||||
public wallets: Map<string, { [useCaseIndex: number]: WalletP2PKH[] }> =
|
||||
new Map();
|
||||
|
||||
constructor(
|
||||
dependencies: Required<WalletDependencies>,
|
||||
genesisData: DistributiveOmit<WalletXOGenesisData, 'type'>,
|
||||
) {
|
||||
super();
|
||||
|
||||
// Assign our dependencies.
|
||||
this.activities = dependencies.activities;
|
||||
this.blockchain = dependencies.blockchain;
|
||||
this.cache = dependencies.cache;
|
||||
|
||||
// Assign our genesis data with the type discriminator.
|
||||
this.genesisData = {
|
||||
type: 'WalletXO',
|
||||
...genesisData,
|
||||
};
|
||||
|
||||
// Instantiate the HDPrivateNode from the entropy source provided in genesis data.
|
||||
if ('seed' in genesisData) {
|
||||
this.hdPrivateNode = HDPrivateNode.fromSeed(genesisData.seed);
|
||||
} else if ('mnemonic' in genesisData) {
|
||||
this.hdPrivateNode = HDPrivateNode.fromMnemonic(genesisData.mnemonic);
|
||||
} else {
|
||||
this.hdPrivateNode = HDPrivateNode.fromXPriv(genesisData.xpriv);
|
||||
}
|
||||
|
||||
// Derive the XO purpose node at m/2415'.
|
||||
// This is the root for all template key derivations and matches the
|
||||
// engine/crypto purposeHDNode exactly.
|
||||
this.purposeNode = this.hdPrivateNode.derivePath(XO_PURPOSE_PATH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the wallet by scanning all template identifiers from genesis data
|
||||
* for used keys and then starting all derived child wallets for monitoring.
|
||||
*/
|
||||
async start() {
|
||||
// Get the address gap from genesis data or use the default.
|
||||
const addressGap = this.genesisData.addressGap || XO_ADDRESS_GAP;
|
||||
|
||||
// Scan all templates provided in genesis data for used keys.
|
||||
for (const templateIdentifier of this.genesisData.templateIdentifiers) {
|
||||
await this.scanTemplate(templateIdentifier, 0, addressGap);
|
||||
}
|
||||
|
||||
// Start all derived child wallets.
|
||||
// Child wallets may already be started from deriveWalletsForUseCase,
|
||||
// but WalletP2PKH.start() is idempotent (checks isStarted flag).
|
||||
const startPromises: Promise<void>[] = [];
|
||||
for (const wallet of this.getAllChildWallets()) {
|
||||
startPromises.push(wallet.start());
|
||||
}
|
||||
await Promise.all(startPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring all child wallets for transaction activity.
|
||||
*/
|
||||
async stop() {
|
||||
const stopPromises: Promise<void>[] = [];
|
||||
for (const wallet of this.getAllChildWallets()) {
|
||||
stopPromises.push(wallet.stop());
|
||||
}
|
||||
await Promise.all(stopPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy all child wallets and remove all event listeners from this wallet.
|
||||
*/
|
||||
async destroy() {
|
||||
const destroyPromises: Promise<void>[] = [];
|
||||
for (const wallet of this.getAllChildWallets()) {
|
||||
destroyPromises.push(wallet.destroy());
|
||||
}
|
||||
await Promise.all(destroyPromises);
|
||||
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically add a new template identifier and scan for derived keys.
|
||||
* If the template has already been added, this is a no-op.
|
||||
*
|
||||
* @param templateIdentifier - A 64-character hex string template identifier
|
||||
* @throws {Error} If the template identifier is not 64 hex characters
|
||||
*/
|
||||
async addTemplate(templateIdentifier: string) {
|
||||
// Validate the template identifier length.
|
||||
if (templateIdentifier.length !== TEMPLATE_IDENTIFIER_HEX_LENGTH) {
|
||||
console.error('Template identifier must be ${TEMPLATE_IDENTIFIER_HEX_LENGTH} hex characters, got ${templateIdentifier.length}');
|
||||
throw new Error(
|
||||
`Template identifier must be ${TEMPLATE_IDENTIFIER_HEX_LENGTH} hex characters, got ${templateIdentifier.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Skip if this template has already been added.
|
||||
if (this.wallets.has(templateIdentifier)) {
|
||||
console.error('Template ${templateIdentifier} has already been added');
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the address gap to use for scanning.
|
||||
const addressGap = this.genesisData.addressGap || XO_ADDRESS_GAP;
|
||||
console.log('addressGap', addressGap);
|
||||
|
||||
// Scan the template for derived keys across all use cases.
|
||||
console.log('scanning template');
|
||||
await this.scanTemplate(templateIdentifier, 0, addressGap);
|
||||
console.log('scanTemplate complete');
|
||||
|
||||
// Start all newly derived child wallets for this template.
|
||||
const templateWallets = this.wallets.get(templateIdentifier);
|
||||
if (templateWallets) {
|
||||
const startPromises: Promise<void>[] = [];
|
||||
for (const useCaseWallets of Object.values(templateWallets)) {
|
||||
for (const wallet of useCaseWallets) {
|
||||
startPromises.push(wallet.start());
|
||||
}
|
||||
}
|
||||
await Promise.all(startPromises);
|
||||
}
|
||||
|
||||
this.emit('stateUpdated', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all transactions from the blockchain involving any child wallet.
|
||||
* Results are flattened and deduplicated across all templates and use cases.
|
||||
*/
|
||||
async getTransactions() {
|
||||
const fetchPromises = [];
|
||||
|
||||
for (const wallet of this.getAllChildWallets()) {
|
||||
fetchPromises.push(wallet.getTransactions());
|
||||
}
|
||||
|
||||
const txs = await Promise.all(fetchPromises);
|
||||
|
||||
// Flatten and deduplicate transactions across all child wallets.
|
||||
const txsFlattened = ExtMap.flatten(txs);
|
||||
|
||||
// Emit an event to notify that transactions have been updated.
|
||||
this.emit('transactionsUpdated', txsFlattened);
|
||||
|
||||
return txsFlattened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all unspent outputs from the blockchain across all child wallets.
|
||||
*/
|
||||
async getUnspents() {
|
||||
const fetchPromises = [];
|
||||
|
||||
for (const wallet of this.getAllChildWallets()) {
|
||||
fetchPromises.push(wallet.getUnspents());
|
||||
}
|
||||
|
||||
const unspents = await Promise.all(fetchPromises);
|
||||
|
||||
return ExtMap.flatten(unspents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all unspent output directives (with unlocking bytecode) across all child wallets.
|
||||
*/
|
||||
async getUnspentDirectives() {
|
||||
const fetchPromises = [];
|
||||
|
||||
for (const wallet of this.getAllChildWallets()) {
|
||||
fetchPromises.push(wallet.getUnspentDirectives());
|
||||
}
|
||||
|
||||
const unspentDirectives = await Promise.all(fetchPromises);
|
||||
|
||||
return ExtMap.flatten(unspentDirectives);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total satoshi balance across all child wallets.
|
||||
*/
|
||||
async getBalanceSats() {
|
||||
const unspents = await this.getUnspents();
|
||||
|
||||
const utxos = unspents
|
||||
.map((blockchainUTXO) => blockchainUTXO.utxo)
|
||||
.toArray();
|
||||
|
||||
return calculateBalanceSats(utxos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total token balances across all child wallets.
|
||||
*/
|
||||
async getBalanceTokens(): Promise<TokenBalances> {
|
||||
const unspents = await this.getUnspents();
|
||||
|
||||
const utxos = unspents
|
||||
.map((blockchainUTXO) => blockchainUTXO.utxo)
|
||||
.toArray();
|
||||
|
||||
return calculateBalanceTokens(utxos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all addresses belonging to this wallet across all templates and use cases.
|
||||
*/
|
||||
async getAddresses() {
|
||||
const addressPromises = [];
|
||||
|
||||
for (const wallet of this.getAllChildWallets()) {
|
||||
addressPromises.push(wallet.getReceivingAddress());
|
||||
}
|
||||
|
||||
const addresses = await Promise.all(addressPromises);
|
||||
|
||||
return ExtMap.fromArray(addresses, (address) => address.toLockscriptHex());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default receiving address for this wallet.
|
||||
* Returns the address of the first derived child wallet.
|
||||
*
|
||||
* @throws {Error} If no child wallets have been derived yet
|
||||
*/
|
||||
async getReceivingAddress() {
|
||||
const allWallets = this.getAllChildWallets();
|
||||
|
||||
if (allWallets.length === 0) {
|
||||
throw new Error(
|
||||
'No child wallets available. Add a template and start the wallet first.',
|
||||
);
|
||||
}
|
||||
|
||||
// Return the first child wallet's receiving address.
|
||||
return allWallets[0].privateKey.derivePublicKey().deriveAddress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a signed transaction using UTXOs from all child wallets.
|
||||
* The change output is sent to the first child wallet's address.
|
||||
*
|
||||
* @param txTemplate - Partial transaction template with desired outputs
|
||||
* @returns A TransactionBuilder with the compiled transaction
|
||||
*/
|
||||
async signTransaction(
|
||||
txTemplate: Partial<TransactionTemplate>,
|
||||
): Promise<TransactionBuilder> {
|
||||
// Use the first child wallet's address as the change address.
|
||||
const changeAddress = await this.getReceivingAddress();
|
||||
const changeLockingBytecode = changeAddress.toLockscriptBytes();
|
||||
|
||||
// Gather unspent directives from all child wallets.
|
||||
const unspents = await this.getUnspentDirectives();
|
||||
|
||||
// Build the transaction.
|
||||
return TransactionBuilder.build(
|
||||
txTemplate,
|
||||
unspents.toArray(),
|
||||
changeLockingBytecode,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build, sign, and broadcast a transaction using UTXOs from all child wallets.
|
||||
*
|
||||
* @param txTemplate - Partial transaction template with desired outputs
|
||||
* @returns The raw transaction bytes that were broadcast
|
||||
*/
|
||||
async sendTransaction(txTemplate: Partial<TransactionTemplate>) {
|
||||
// Generate the transaction.
|
||||
const generatedTx = await this.signTransaction(txTemplate);
|
||||
|
||||
// Convert the transaction to bytes.
|
||||
const generatedTxBytes = generatedTx.toBytes();
|
||||
|
||||
// Broadcast the transaction to the blockchain.
|
||||
await this.blockchain.broadcastTransaction(generatedTxBytes);
|
||||
|
||||
return generatedTxBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a batch of WalletP2PKH child wallets for a specific template + use case combination.
|
||||
* Each child wallet monitors its own address on the blockchain.
|
||||
*
|
||||
* The derivation path relative to the purpose node is:
|
||||
* {templateIndex}'/{useCaseIndex}'/{childIndex}
|
||||
*
|
||||
* This produces identical keys to engine/crypto's `deriveUseCaseChildNode` when given
|
||||
* the same purpose node, template index, use case index, and child index.
|
||||
*
|
||||
* @param templateIdentifier - The 64-char hex template identifier (used as storage key)
|
||||
* @param templateIndex - The BIP32-compatible template index derived from the identifier
|
||||
* @param useCaseIndex - The use case index (0 = SigningAndSpending, 1 = Encryption)
|
||||
* @param count - Number of child wallets to derive
|
||||
* @param startIndex - Starting child index (default 0)
|
||||
* @returns Array of newly derived WalletP2PKH instances
|
||||
*/
|
||||
async deriveWalletsForUseCase(
|
||||
templateIdentifier: string,
|
||||
templateIndex: number,
|
||||
useCaseIndex: number,
|
||||
count: number,
|
||||
startIndex = 0,
|
||||
): Promise<WalletP2PKH[]> {
|
||||
const newWallets: WalletP2PKH[] = [];
|
||||
|
||||
// Ensure the wallet storage structure exists for this template and all use cases.
|
||||
if (!this.wallets.has(templateIdentifier)) {
|
||||
const useCases: { [useCaseIndex: number]: WalletP2PKH[] } = {};
|
||||
for (const uc of XO_USE_CASES) {
|
||||
useCases[uc] = [];
|
||||
}
|
||||
this.wallets.set(templateIdentifier, useCases);
|
||||
}
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const childIndex = startIndex + i;
|
||||
|
||||
// Build the relative derivation path from the purpose node.
|
||||
// This matches engine/crypto's deriveUseCaseChildNode relative path construction:
|
||||
// `${accountOrTemplateIndex}'/${accountOrTemplateUseCaseIndex}'/${useCaseChildIndex}`
|
||||
const relativePath = `${templateIndex}'/${useCaseIndex}'/${childIndex}`;
|
||||
|
||||
// Use the full absolute path as the cache key to avoid collisions.
|
||||
const fullPath = `${XO_PURPOSE_PATH}/${relativePath}`;
|
||||
|
||||
// Derive the private key at this path. The cache avoids re-deriving the same key.
|
||||
const privateKeyHex = await this.cache.getOrSet(fullPath, () => {
|
||||
const childNode = this.purposeNode.deriveRelativePath(relativePath);
|
||||
return childNode.toPrivateKey().toHex();
|
||||
});
|
||||
|
||||
// Create a child P2PKH wallet for this derived private key.
|
||||
const wallet = await WalletP2PKH.from(
|
||||
{
|
||||
activities: this.activities,
|
||||
blockchain: this.blockchain,
|
||||
cache: this.cache,
|
||||
},
|
||||
{ privateKey: privateKeyHex },
|
||||
);
|
||||
|
||||
// Bubble up state-update events from child wallets to this parent wallet.
|
||||
wallet.on('stateUpdated', async () => {
|
||||
this.emit('stateUpdated', null);
|
||||
});
|
||||
|
||||
// Start monitoring the child wallet's address on the blockchain.
|
||||
await wallet.start();
|
||||
|
||||
newWallets.push(wallet);
|
||||
}
|
||||
|
||||
// Append the newly derived wallets to the storage for this template + use case.
|
||||
const templateWallets = this.wallets.get(templateIdentifier)!;
|
||||
templateWallets[useCaseIndex] = [
|
||||
...templateWallets[useCaseIndex],
|
||||
...newWallets,
|
||||
];
|
||||
|
||||
return newWallets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a single template across all use cases using address gap logic.
|
||||
* For each use case, derives batches of child wallets and checks their transaction
|
||||
* history. Scanning stops for a use case when `addressGap` consecutive child indexes
|
||||
* produce addresses with no on-chain activity.
|
||||
*
|
||||
* @param templateIdentifier - The 64-char hex template identifier
|
||||
* @param startIndex - Starting child index for scanning (default 0)
|
||||
* @param addressGap - Number of consecutive empty addresses before stopping (default XO_ADDRESS_GAP)
|
||||
*/
|
||||
async scanTemplate(
|
||||
templateIdentifier: string,
|
||||
startIndex = 0,
|
||||
addressGap = XO_ADDRESS_GAP,
|
||||
) {
|
||||
// Validate the template identifier length (must be a 32-byte hex string).
|
||||
if (templateIdentifier.length !== TEMPLATE_IDENTIFIER_HEX_LENGTH) {
|
||||
throw new Error(
|
||||
`Template identifier must be ${TEMPLATE_IDENTIFIER_HEX_LENGTH} hex characters, got ${templateIdentifier.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Compute the BIP32-compatible template index from the identifier.
|
||||
// This uses the same algorithm as engine/crypto's createPathIndexFromHash.
|
||||
console.log('creating path index from template identifier');
|
||||
const templateIndex =
|
||||
createPathIndexFromTemplateIdentifier(templateIdentifier);
|
||||
console.log('path index created');
|
||||
|
||||
// Scan each use case for this template (SigningAndSpending + Encryption).
|
||||
for (const useCaseIndex of XO_USE_CASES) {
|
||||
console.log('scanning use case', useCaseIndex);
|
||||
let currentIndex = startIndex;
|
||||
let emptyAddressCount = 0;
|
||||
|
||||
// Continue scanning until we find 'addressGap' consecutive empty addresses.
|
||||
while (emptyAddressCount < addressGap) {
|
||||
console.log('deriving wallets for use case', useCaseIndex);
|
||||
|
||||
// Derive a batch of wallets for this use case.
|
||||
const batch = await this.deriveWalletsForUseCase(
|
||||
templateIdentifier,
|
||||
templateIndex,
|
||||
useCaseIndex,
|
||||
addressGap,
|
||||
currentIndex,
|
||||
);
|
||||
console.log('batch derived', typeof batch, batch.length);
|
||||
|
||||
// Check the transaction history of each wallet in the batch.
|
||||
const results = await Promise.all(
|
||||
batch.map((wallet) => wallet.getTransactions()),
|
||||
);
|
||||
console.log('results', typeof results, results.length);
|
||||
|
||||
// Process results: reset empty count on activity, increment on empty.
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
if (results[i].size > 0) {
|
||||
// This address has transactions; reset the empty address counter.
|
||||
emptyAddressCount = 0;
|
||||
} else {
|
||||
// This address is empty; increment the consecutive empty counter.
|
||||
emptyAddressCount++;
|
||||
}
|
||||
|
||||
currentIndex++;
|
||||
|
||||
// If we've hit the address gap threshold, stop scanning this use case.
|
||||
if (emptyAddressCount >= addressGap) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('stateUpdated', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all child WalletP2PKH instances across all templates and use cases
|
||||
* into a single flat array. Used by aggregate methods to operate over every
|
||||
* derived wallet uniformly.
|
||||
*
|
||||
* @returns Flat array of all child WalletP2PKH instances
|
||||
*/
|
||||
private getAllChildWallets(): WalletP2PKH[] {
|
||||
const allWallets: WalletP2PKH[] = [];
|
||||
|
||||
for (const templateWallets of this.wallets.values()) {
|
||||
for (const useCaseWallets of Object.values(templateWallets)) {
|
||||
allWallets.push(...useCaseWallets);
|
||||
}
|
||||
}
|
||||
|
||||
return allWallets;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Factory Types/Functions
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
export type WalletXOFactory<T extends WalletXO = WalletXO> = (
|
||||
services: WalletDependencies,
|
||||
genesisData: WalletXOGenesisData,
|
||||
) => Promise<T>;
|
||||
|
||||
export async function useWalletXO<T extends WalletXO = WalletXO>(
|
||||
services: WalletDependencies,
|
||||
genesisData: WalletXOGenesisData,
|
||||
): Promise<T> {
|
||||
return (await WalletXO.from(services, genesisData)) as T;
|
||||
}
|
||||
@@ -4,18 +4,19 @@
|
||||
// However, Chaingraph backend is currently broken for many queries, so let's not do this yet.
|
||||
|
||||
import { beforeAll, describe, it, expect } from 'vitest';
|
||||
import { BaseBlockchain, BlockchainElectrum } from '../../src/index.js';
|
||||
import {
|
||||
BaseBlockchain,
|
||||
BlockchainChaingraph,
|
||||
BlockchainElectrum,
|
||||
} from '../../src/index.js';
|
||||
|
||||
const adapters: Record<
|
||||
string,
|
||||
() => Promise<BaseBlockchain<unknown, unknown>>
|
||||
> = {
|
||||
// Commenting out since the chaingraph endopoints we are hitting are currently broken.
|
||||
// demo won't allow non-browser clients
|
||||
// Pat's server isn't responding to block height requests correctly (or we are using an old version, which the demo server is also running).
|
||||
// chaingraph: async () => {
|
||||
// return await BlockchainChaingraph.from();
|
||||
// },
|
||||
chaingraph: async () => {
|
||||
return await BlockchainChaingraph.from();
|
||||
},
|
||||
electrum: async () => {
|
||||
return await BlockchainElectrum.from();
|
||||
},
|
||||
@@ -58,7 +59,7 @@ describe.each(Object.entries(adapters))('%s Adapter', (name, createAdapter) => {
|
||||
});
|
||||
|
||||
it(`${name}.fetchBlockHeader() `, async () => {
|
||||
const result = await blockchain.fetchBlockHeader(1);
|
||||
const result = await blockchain.fetchBlockHeader(testBlockHeight);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.toHex()).toBeTypeOf('string');
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
monitorSyncTipsSchema,
|
||||
} from '../../src/blockchain/blockchain-chaingraph-helpers.js';
|
||||
|
||||
const chaingraphServer = 'https://gql.chaingraph.pat.mn/v1/graphql';
|
||||
const chaingraphServer = 'https://demo.chaingraph.cash/v1/graphql';
|
||||
|
||||
describe('GraphQLClient', () => {
|
||||
const graphQLClient = new GraphQLClient(chaingraphServer);
|
||||
|
||||
Reference in New Issue
Block a user