Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c87ad46a48 | ||
|
|
53b7fb3aef | ||
|
|
a029b43fc4 | ||
|
|
f009cd31da | ||
|
|
38dc5be0f4 | ||
|
|
b47a7c43ad | ||
|
|
3341340933 | ||
|
|
2ed7002ebf | ||
|
|
5057eb3421
|
+4
-2
@@ -97,8 +97,10 @@ publish:development:
|
|||||||
- git config --global user.email "${GITLAB_USER_EMAIL}"
|
- git config --global user.email "${GITLAB_USER_EMAIL}"
|
||||||
- git config --global user.name "${GITLAB_USER_NAME}"
|
- git config --global user.name "${GITLAB_USER_NAME}"
|
||||||
|
|
||||||
# Configure git to push with CI token
|
# Configure git to push with CI token.
|
||||||
- git remote set-url origin "https://gitlab-ci-token:$GL_TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH.git"
|
# CI_REPOSITORY_URL uses the CI_JOB_TOKEN to form the repository URL, in the format "https://gitlab-ci-token:$CI_JOB_TOKEN@gitlab.example.com/my-group/my-project.git"
|
||||||
|
# The CI_JOB_TOKEN is short lived and only valid for the duration of the CI/CD job.
|
||||||
|
- git remote set-url origin "${CI_REPOSITORY_URL}"
|
||||||
|
|
||||||
# Ensure full Git history so Lerna can detect changes
|
# Ensure full Git history so Lerna can detect changes
|
||||||
- git fetch --unshallow
|
- git fetch --unshallow
|
||||||
|
|||||||
Generated
+6
-5
@@ -12,8 +12,7 @@
|
|||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bitauth/libauth": "^3.0.0",
|
"@bitauth/libauth": "^3.0.0"
|
||||||
"js-yaml": "^4.1.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.10.0",
|
"@eslint/js": "^9.10.0",
|
||||||
@@ -5453,6 +5452,7 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/array-differ": {
|
"node_modules/array-differ": {
|
||||||
@@ -9855,6 +9855,7 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -12954,9 +12955,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/preact": {
|
"node_modules/preact": {
|
||||||
"version": "10.27.2",
|
"version": "10.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz",
|
||||||
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
|
"integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
---
|
||||||
|
title: 'What Makes the XO Wallet Different'
|
||||||
|
date: 2025-12-20
|
||||||
|
description: A big picture look at how the XO Wallet aims to change the way people interact with Bitcoin Cash apps, focusing on safety, clarity, and user control.
|
||||||
|
author: Saqib Noor
|
||||||
|
tags: [wallet, general audience]
|
||||||
|
---
|
||||||
|
|
||||||
|
# What Makes the XO Wallet Different?
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Wallets are the primary way people interact with crypto ecosystems, from everyday value transfers to more complex decentralised applications. For Bitcoin Cash, a peer to peer electronic cash system, wallets are how people directly send, receive, and understand value without intermediaries. They are the bridge between users, apps, and the blockchain itself. Yet for all their importance, wallets have changed very little in how they help users understand what is actually happening when they interact with an app.
|
||||||
|
|
||||||
|
This article explains what makes the **XO Wallet** different. It does not dive deep into technical details or code. Instead, it looks at the bigger picture: how wallets can move beyond simply signing transactions, how understanding and safety can be improved, and why this matters as Bitcoin Cash applications become more powerful and more complex.
|
||||||
|
|
||||||
|
## A wallet that understands what you are doing!
|
||||||
|
|
||||||
|
Most crypto wallets today are very good at one thing: signing transactions. They are much less good at understanding *why* you are signing them.
|
||||||
|
|
||||||
|
When you interact with an app, a DeFi protocol, or even a simple token transfer, the wallet often shows you raw data, script hashes, or vague warnings. Users are expected to trust the app, trust the website, and hope nothing goes wrong.
|
||||||
|
|
||||||
|
The XO Wallet is built around a different idea.
|
||||||
|
|
||||||
|
Instead of being blind, the wallet is designed to **understand the intent of an action**, not just the bytes of a transaction.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## From Signing Transactions to Understanding Intent
|
||||||
|
|
||||||
|
The key difference comes from **templates**, which act as a cornerstone of how the XO Wallet understands and verifies actions.
|
||||||
|
|
||||||
|
A template is a shared description of:
|
||||||
|
- What kind of actions are available
|
||||||
|
- Who is involved
|
||||||
|
- What funds or tokens are at stake
|
||||||
|
- What rules apply
|
||||||
|
- What state must be tracked over time
|
||||||
|
|
||||||
|
Templates are not arbitrary or hidden. They are designed to be visible, reviewable, and trusted over time through auditing, reputation, and community scrutiny. This gives the wallet a reliable basis for understanding what an action is meant to do.
|
||||||
|
|
||||||
|
With templates, the wallet is no longer guessing. It knows whether you are:
|
||||||
|
- Receiving tokens
|
||||||
|
- Locking funds into a specific contract and understanding what that contract allows
|
||||||
|
- Opening or closing a position
|
||||||
|
- Signing a message
|
||||||
|
- Transferring ownership
|
||||||
|
|
||||||
|
That means the wallet can explain actions in **human terms**, before you approve them, and verify that what is happening matches the intent you were shown.
|
||||||
|
|
||||||
|
### Trust Once, Not Every Time
|
||||||
|
|
||||||
|
A key difference between templates and traditional decentralised apps is **how often trust is required**.
|
||||||
|
|
||||||
|
In today's DeFi models, users implicitly trust every website, interface update, and transaction request they interact with. This trust is granted repeatedly, often by habit rather than careful review.
|
||||||
|
|
||||||
|
Templates change this dynamic.
|
||||||
|
|
||||||
|
A template is something that needs to be trusted **once**, not at every interaction. Once a template has been reviewed, audited, and understood, the wallet can rely on it consistently to interpret actions, and enforce rules.
|
||||||
|
|
||||||
|
This mirrors how people already think about wallets themselves. You choose a wallet carefully, then rely on it repeatedly without re-evaluating it on every transaction.
|
||||||
|
|
||||||
|
Templates extend this trust model to application logic, concentrating trust into fewer, more stable components. This gives users and the community more room for scrutiny, stronger incentives for correctness, and a safer foundation than repeatedly trusting changing front ends or opaque transaction requests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How This Changes the Flow
|
||||||
|
|
||||||
|
Instead of apps directly asking wallets to sign opaque transactions, the flow becomes structured and understandable.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
App --> Template
|
||||||
|
|
||||||
|
subgraph XO Wallet-Template interaction
|
||||||
|
Template -->|human readable intent| Wallet
|
||||||
|
Wallet -->|trusted source| Template
|
||||||
|
end
|
||||||
|
|
||||||
|
User --> Wallet
|
||||||
|
|
||||||
|
App[Application]
|
||||||
|
Template[Template]
|
||||||
|
Wallet[XO Wallet]
|
||||||
|
User[User]
|
||||||
|
```
|
||||||
|
|
||||||
|
The wallet is no longer a passive signer.
|
||||||
|
It becomes an active interpreter between apps and users.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State That Does Not Disappear
|
||||||
|
|
||||||
|
A common pain point in crypto is lost context.
|
||||||
|
|
||||||
|
You open a position, receive a token, or lock funds, then later:
|
||||||
|
- Change wallets
|
||||||
|
- Reinstall an app
|
||||||
|
- Lose track of where the funds came from
|
||||||
|
|
||||||
|
In much of today’s crypto ecosystem, users implicitly trust applications to track this state for them. The app remembers what a position represents, which coins belong to which protocol, and how everything fits together. If that app disappears, changes direction, or is no longer maintained, having the raw on chain data alone may not be enough to continue using the protocol in a meaningful way.
|
||||||
|
|
||||||
|
To function properly, they often need visibility into a user’s coins, balances, and activity so they can decide what to spend, what to lock, and how to construct transactions. This requirement also pushes apps to be deeply invasive.
|
||||||
|
|
||||||
|
XO approaches this differently.
|
||||||
|
|
||||||
|
XO treats **state as a first class concept**, managed by the wallet itself.
|
||||||
|
|
||||||
|
The wallet tracks which outputs belong to which templates, and what they represent. This allows it to:
|
||||||
|
- Show meaningful balances
|
||||||
|
- Restore context after a restart
|
||||||
|
- Understand ownership over time
|
||||||
|
- Avoid accidental loss of access
|
||||||
|
|
||||||
|
Managing and preserving this state is a significant responsibility, arguably as important as private key management itself. Losing state can mean losing understanding, control, or even access, even if the underlying funds still exist.
|
||||||
|
|
||||||
|
In the XO model, applications can still manage state if they choose to, but users are no longer forced to rely on them. By default, users remain self custodial not only of their keys, but of their wallet state as well, with options for secure backup, recovery, and continuity built into the wallet rather than delegated elsewhere.
|
||||||
|
|
||||||
|
For users, this feels simple. For wallets, it is a major step forward.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Clear Ownership, Clear Responsibility
|
||||||
|
|
||||||
|
XO templates define ownership explicitly.
|
||||||
|
|
||||||
|
Instead of vague ideas of “this UTXO belongs to you”, the wallet knows:
|
||||||
|
- Which conditions control which output
|
||||||
|
- What actions that ownership enables
|
||||||
|
- What value is associated with that ownership
|
||||||
|
|
||||||
|
A simple example helps make this clearer.
|
||||||
|
|
||||||
|
Imagine you participate in a liquidity pool. The funds you contribute are locked into a shared pool, and the UTXO representing your position is clearly of interest to you. However, its value is not obvious in the same way as a simple coin balance. It depends on pool size, relative contributions, fees, and changing conditions over time.
|
||||||
|
|
||||||
|
With templates, this ambiguity is addressed directly. The template defines what ownership means in this context. It can include built in calculations that estimate your share of the pool’s value at any given moment. The wallet can then present this information clearly, without guessing or relying on external apps to explain it.
|
||||||
|
|
||||||
|
This clarity allows the wallet to:
|
||||||
|
- Estimate balances more accurately
|
||||||
|
- Show expected outcomes before actions
|
||||||
|
- Prevent actions that would violate the ownership model defined by the template
|
||||||
|
|
||||||
|
It also lays the groundwork for better privacy and smarter coin selection in the future, since the wallet understands not just which coins exist, but what they represent and how they should be treated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Designed for Normal People, Not Just Developers
|
||||||
|
|
||||||
|
Although the XO wallet is powerful under the hood, the goal is not to expose complexity to the everyday user.
|
||||||
|
|
||||||
|
The XO Wallet aims to:
|
||||||
|
- Explain actions in plain language
|
||||||
|
- Show only what matters at the right time
|
||||||
|
- Reduce scary jargon and warnings, replace them with clarity and guides
|
||||||
|
- Make advanced Bitcoin Cash applications feel natural to use
|
||||||
|
|
||||||
|
This requires more than good engineering. It requires a thoughtful, empowering user interface that helps people feel confident, informed, and in control. To succeed, wallets must compete not only with other crypto products, but with the best experiences people already expect and demand from modern, traditional financial applications.
|
||||||
|
|
||||||
|
You should not need to understand raw transaction data, unfamiliar warnings, or confusing approval screens to feel confident. A well designed wallet should feel as intuitive and dependable as any trusted financial tool, while preserving the openness and self custody that make peer to peer systems truly valuable and loved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A Foundation, Not Just a Product
|
||||||
|
|
||||||
|
The XO Wallet is not just another wallet.
|
||||||
|
|
||||||
|
It is a reference implementation of a broader idea:
|
||||||
|
- Wallets should be active interpreters, not passive signers
|
||||||
|
- Wallets should be smart enough to help users understand transactions in an intuitive, human way
|
||||||
|
- Trust should be placed in well-vetted, stable templates rather than in websites, domains, or opaque front ends
|
||||||
|
- Safety should come from clear structure and shared rules, not from blind trust in external applications
|
||||||
|
|
||||||
|
In this model, decentralisation is strengthened rather than weakened. Users are less dependent on specific apps continuing to exist, and more reliant on open, reviewable definitions of intent that wallets can understand and enforce.
|
||||||
|
|
||||||
|
This is how the XO Stack moves from vision to reality, one layer at a time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Comes Next
|
||||||
|
|
||||||
|
This post focuses on the *why*.
|
||||||
|
|
||||||
|
Future articles will explore:
|
||||||
|
- How wallets choose which templates to trust
|
||||||
|
- How the wallet handles state
|
||||||
|
- How different apps can interoperate safely
|
||||||
|
- How this model supports more complex use cases without sacrificing user control
|
||||||
|
|
||||||
|
The goal is simple:
|
||||||
|
make powerful Bitcoin Cash applications usable, understandable, and safe for everyone.
|
||||||
@@ -6,10 +6,9 @@ export const MetadataRegistrySchema = z
|
|||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
uris: z
|
uris: z
|
||||||
.object({
|
.object({
|
||||||
// Since 'icon' and 'web' are strongly recommended uri identifiers(https://github.com/bitjson/chip-bcmr?tab=readme-ov-file#recommended-uri-identifiers)
|
// 'icon' and 'web' are strongly recommended uri identifiers(https://github.com/bitjson/chip-bcmr?tab=readme-ov-file#recommended-uri-identifiers)
|
||||||
// We require it as non optional.
|
|
||||||
icon: z.url(),
|
icon: z.url(),
|
||||||
web: z.url(),
|
web: z.url().optional(),
|
||||||
})
|
})
|
||||||
// Allows any other keys with any values.
|
// Allows any other keys with any values.
|
||||||
.catchall(z.unknown()),
|
.catchall(z.unknown()),
|
||||||
|
|||||||
@@ -192,17 +192,17 @@ export class BlockchainElectrum extends BaseBlockchain<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchUnspents(address: string) {
|
async fetchUnspents(address: string) {
|
||||||
// Wait for connection to be established before attempting to subscribe.
|
|
||||||
// TODO: Consider doing this at a higher level.
|
|
||||||
if (!this.isConnected) {
|
|
||||||
await this.waitFor('isConnectedUpdated', (isConnected) => isConnected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the cache key.
|
// Define the cache key.
|
||||||
const cacheKey = `addressUnspents.${address}`;
|
const cacheKey = `addressUnspents.${address}`;
|
||||||
|
|
||||||
// Attempt to get the address unspents from the cache or fetch them if not available.
|
// Attempt to get the address unspents from the cache or fetch them if not available.
|
||||||
const addressUnspents = await this.store.getOrSet(cacheKey, async () => {
|
const addressUnspents = await this.store.getOrSet(cacheKey, async () => {
|
||||||
|
// Wait for connection to be established before attempting to subscribe.
|
||||||
|
// TODO: Consider doing this at a higher level.
|
||||||
|
if (!this.isConnected) {
|
||||||
|
await this.waitFor('isConnectedUpdated', (isConnected) => isConnected);
|
||||||
|
}
|
||||||
|
|
||||||
return await fetchUnspentTransactionOutputs(
|
return await fetchUnspentTransactionOutputs(
|
||||||
this.electrumClient,
|
this.electrumClient,
|
||||||
address,
|
address,
|
||||||
@@ -249,17 +249,17 @@ export class BlockchainElectrum extends BaseBlockchain<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchTransaction(txHash: string) {
|
async fetchTransaction(txHash: string) {
|
||||||
// Wait for connection to be established before attempting to subscribe.
|
|
||||||
// TODO: Consider doing this at a higher level.
|
|
||||||
if (!this.isConnected) {
|
|
||||||
await this.waitFor('isConnectedUpdated', (isConnected) => isConnected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the cache key.
|
// Define the cache key.
|
||||||
const cacheKey = `transactions.${txHash}`;
|
const cacheKey = `transactions.${txHash}`;
|
||||||
|
|
||||||
// Attempt to get the transaction hex from the cache or fetch it if not available.
|
// Attempt to get the transaction hex from the cache or fetch it if not available.
|
||||||
const transactionRaw = await this.store.getOrSet(cacheKey, async () => {
|
const transactionRaw = await this.store.getOrSet(cacheKey, async () => {
|
||||||
|
// Wait for connection to be established before attempting to subscribe.
|
||||||
|
// TODO: Consider doing this at a higher level.
|
||||||
|
if (!this.isConnected) {
|
||||||
|
await this.waitFor('isConnectedUpdated', (isConnected) => isConnected);
|
||||||
|
}
|
||||||
|
|
||||||
const txHex = await fetchTransaction(this.electrumClient, txHash);
|
const txHex = await fetchTransaction(this.electrumClient, txHash);
|
||||||
return Transaction.fromHex(txHex).toRaw();
|
return Transaction.fromHex(txHex).toRaw();
|
||||||
});
|
});
|
||||||
@@ -272,17 +272,17 @@ export class BlockchainElectrum extends BaseBlockchain<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchAddressHistory(address: string) {
|
async fetchAddressHistory(address: string) {
|
||||||
// Wait for connection to be established before attempting to subscribe.
|
|
||||||
// TODO: Consider doing this at a higher level.
|
|
||||||
if (!this.isConnected) {
|
|
||||||
await this.waitFor('isConnectedUpdated', (isConnected) => isConnected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the cache key.
|
// Define the cache key.
|
||||||
const cacheKey = `addressHistory.${address}`;
|
const cacheKey = `addressHistory.${address}`;
|
||||||
|
|
||||||
// Attempt to get the transaction history from the cache or fetch them if not available.
|
// Attempt to get the transaction history from the cache or fetch them if not available.
|
||||||
const addressHistory = await this.store.getOrSet(cacheKey, async () => {
|
const addressHistory = await this.store.getOrSet(cacheKey, async () => {
|
||||||
|
// Wait for connection to be established before attempting to subscribe.
|
||||||
|
// TODO: Consider doing this at a higher level.
|
||||||
|
if (!this.isConnected) {
|
||||||
|
await this.waitFor('isConnectedUpdated', (isConnected) => isConnected);
|
||||||
|
}
|
||||||
|
|
||||||
return await fetchHistory(this.electrumClient, address);
|
return await fetchHistory(this.electrumClient, address);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -305,17 +305,17 @@ export class BlockchainElectrum extends BaseBlockchain<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchBlockHeader(blockHeight: number) {
|
async fetchBlockHeader(blockHeight: number) {
|
||||||
// Wait for connection to be established before attempting to subscribe.
|
|
||||||
// TODO: Consider doing this at a higher level.
|
|
||||||
if (!this.isConnected) {
|
|
||||||
await this.waitFor('isConnectedUpdated', (isConnected) => isConnected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the cache key.
|
// Define the cache key.
|
||||||
const cacheKey = `blockHeader.${blockHeight}`;
|
const cacheKey = `blockHeader.${blockHeight}`;
|
||||||
|
|
||||||
// Attempt to get the transaction history from the cache or fetch them if not available.
|
// Attempt to get the transaction history from the cache or fetch them if not available.
|
||||||
const blockHeaderRaw = await this.store.getOrSet(cacheKey, async () => {
|
const blockHeaderRaw = await this.store.getOrSet(cacheKey, async () => {
|
||||||
|
// Wait for connection to be established before attempting to subscribe.
|
||||||
|
// TODO: Consider doing this at a higher level.
|
||||||
|
if (!this.isConnected) {
|
||||||
|
await this.waitFor('isConnectedUpdated', (isConnected) => isConnected);
|
||||||
|
}
|
||||||
|
|
||||||
const headerHex = await fetchBlockHeaderFromBlockHeight(
|
const headerHex = await fetchBlockHeaderFromBlockHeight(
|
||||||
this.electrumClient,
|
this.electrumClient,
|
||||||
blockHeight,
|
blockHeight,
|
||||||
@@ -333,15 +333,15 @@ export class BlockchainElectrum extends BaseBlockchain<
|
|||||||
// TODO: ElectrumClient provides a chainHeight property that is populated after a `blockchain.headers.subscribe` call.
|
// TODO: ElectrumClient provides a chainHeight property that is populated after a `blockchain.headers.subscribe` call.
|
||||||
// We may be able to leverage this side-effect to avoid fetching the chain tip.
|
// We may be able to leverage this side-effect to avoid fetching the chain tip.
|
||||||
async fetchChainTip() {
|
async fetchChainTip() {
|
||||||
// Wait for connection to be established before attempting to subscribe.
|
|
||||||
// TODO: Consider doing this at a higher level.
|
|
||||||
if (!this.isConnected) {
|
|
||||||
await this.waitFor('isConnectedUpdated', (isConnected) => isConnected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we don't currently have a block height, fetch the current chain tip.
|
// If we don't currently have a block height, fetch the current chain tip.
|
||||||
// NOTE: We don't use our cache here because the key will be the same and so on-boot, we will have a stale value.
|
// NOTE: We don't use our cache here because the key will be the same and so on-boot, we will have a stale value.
|
||||||
if (!this.blockHeight) {
|
if (!this.blockHeight) {
|
||||||
|
// Wait for connection to be established before attempting to subscribe.
|
||||||
|
// TODO: Consider doing this at a higher level.
|
||||||
|
if (!this.isConnected) {
|
||||||
|
await this.waitFor('isConnectedUpdated', (isConnected) => isConnected);
|
||||||
|
}
|
||||||
|
|
||||||
const chainTip = await fetchCurrentChainTip(this.electrumClient);
|
const chainTip = await fetchCurrentChainTip(this.electrumClient);
|
||||||
|
|
||||||
this.blockHeight = chainTip.height;
|
this.blockHeight = chainTip.height;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export * from './wallet/index.js';
|
|||||||
// For node-only providers, export these from index.node.ts.
|
// For node-only providers, export these from index.node.ts.
|
||||||
export * from './bcmr/bcmr-composite.js';
|
export * from './bcmr/bcmr-composite.js';
|
||||||
export * from './bcmr/bcmr-http.js';
|
export * from './bcmr/bcmr-http.js';
|
||||||
|
export * from './bcmr/bcmr-helpers.js';
|
||||||
export * from './blockchain/blockchain-electrum.js';
|
export * from './blockchain/blockchain-electrum.js';
|
||||||
export * from './blockchain/blockchain-chaingraph.js';
|
export * from './blockchain/blockchain-chaingraph.js';
|
||||||
export * from './storage/store-remote-http.js';
|
export * from './storage/store-remote-http.js';
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { BaseRates } from './base-rates.js';
|
import { BaseRates } from './base-rates.js';
|
||||||
import { UUIDV7 } from '../utils/uuid.js';
|
import { UUIDV7 } from '../utils/uuid.js';
|
||||||
|
|
||||||
|
export type CachedRate = {
|
||||||
|
price: number;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A composite rate adapter that combines rates from multiple rate sources
|
* A composite rate adapter that combines rates from multiple rate sources
|
||||||
* using a configurable policy.
|
* using a configurable policy.
|
||||||
@@ -13,7 +18,16 @@ export class RatesComposite extends BaseRates {
|
|||||||
private availablePairs: Set<string> = new Set();
|
private availablePairs: Set<string> = new Set();
|
||||||
|
|
||||||
// Cache of the latest rates from each adapter
|
// Cache of the latest rates from each adapter
|
||||||
private rateCache: Map<string, Map<string, Map<string, number>>> = new Map();
|
/**
|
||||||
|
* Cache of the latest rates from each adapter
|
||||||
|
*
|
||||||
|
* The cache is a nested map of:
|
||||||
|
* - denominatorCode -> numeratorCode -> adapterId -> CachedRate
|
||||||
|
*
|
||||||
|
* Eg: BCH -> USD -> 1 (Oracles) -> { price: 100, timestamp: 1714857600 }
|
||||||
|
*/
|
||||||
|
private rateCache: Map<string, Map<string, Map<string, CachedRate>>> =
|
||||||
|
new Map();
|
||||||
|
|
||||||
// Off callbacks for unsubscribing from events
|
// Off callbacks for unsubscribing from events
|
||||||
private unsubscribeFunctions: Map<string, () => void> = new Map();
|
private unsubscribeFunctions: Map<string, () => void> = new Map();
|
||||||
@@ -210,7 +224,7 @@ export class RatesComposite extends BaseRates {
|
|||||||
|
|
||||||
// Store the rate in the cache
|
// Store the rate in the cache
|
||||||
const rateMap = denomMap.get(numeratorUnitCode)!;
|
const rateMap = denomMap.get(numeratorUnitCode)!;
|
||||||
rateMap.set(adapterId, price);
|
rateMap.set(adapterId, { price, timestamp: Date.now() });
|
||||||
|
|
||||||
// Add pair to available pairs
|
// Add pair to available pairs
|
||||||
this.availablePairs.add(`${numeratorUnitCode}/${denominatorUnitCode}`);
|
this.availablePairs.add(`${numeratorUnitCode}/${denominatorUnitCode}`);
|
||||||
@@ -225,12 +239,19 @@ export class RatesComposite extends BaseRates {
|
|||||||
// Process each numerator currency
|
// Process each numerator currency
|
||||||
for (const [numerator, rateMap] of numeratorMap.entries()) {
|
for (const [numerator, rateMap] of numeratorMap.entries()) {
|
||||||
// Go through our adapters in order, plucking the rate for each adapter
|
// Go through our adapters in order, plucking the rate for each adapter
|
||||||
const rates = this.adapters
|
const cachedRates = this.adapters
|
||||||
|
// First, get the Cached rate for each adapter
|
||||||
.map(({ id }) => rateMap.get(id))
|
.map(({ id }) => rateMap.get(id))
|
||||||
.filter((rate) => rate !== undefined);
|
// Then, filter out any undefined rates (service is down, rate limit, etc.)
|
||||||
|
.filter((rate) => rate !== undefined)
|
||||||
|
// Then, filter out any rates that are older than the refresh interval so we don't emit stale rates
|
||||||
|
// it's up to the client to hold onto "stale" rates if they need them, rather than us mixing old and new rates without disclosing it.
|
||||||
|
.filter(
|
||||||
|
(rate) => rate.timestamp > Date.now() - this.refreshIntervalMs,
|
||||||
|
);
|
||||||
|
|
||||||
// Apply policy to get combined rate
|
// Apply policy to get combined rate
|
||||||
const combinedRate = this.policy(rates);
|
const combinedRate = this.policy(cachedRates.map(({ price }) => price));
|
||||||
|
|
||||||
// Only emit if we have a valid rate
|
// Only emit if we have a valid rate
|
||||||
if (combinedRate !== null) {
|
if (combinedRate !== null) {
|
||||||
@@ -244,7 +265,6 @@ export class RatesComposite extends BaseRates {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RatesCompositePolicy = (rates: number[]) => number | null;
|
export type RatesCompositePolicy = (rates: number[]) => number | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,20 +15,27 @@ export type StoreEvents = {
|
|||||||
clear: undefined;
|
clear: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export abstract class BaseStorage {
|
/**
|
||||||
|
* Abstract base class for storage implementations.
|
||||||
|
*
|
||||||
|
* @template E The events type for stores created by this storage.
|
||||||
|
* Must extend StoreEvents (at least includes base events).
|
||||||
|
* Defaults to StoreEvents for base type hints.
|
||||||
|
*/
|
||||||
|
export abstract class BaseStorage<E extends StoreEvents = StoreEvents> {
|
||||||
abstract listStores(): Promise<Array<string>>;
|
abstract listStores(): Promise<Array<string>>;
|
||||||
abstract createStore(
|
abstract createStore<T = unknown>(
|
||||||
storeName: string,
|
storeName: string,
|
||||||
opts: StorageOpts,
|
opts: StorageOpts,
|
||||||
): Promise<BaseStore>;
|
): Promise<BaseStore<T, E>>;
|
||||||
abstract getStore(
|
abstract getStore<T = unknown>(
|
||||||
|
storeName: string,
|
||||||
|
opts?: StorageOpts,
|
||||||
|
): Promise<BaseStore<T, E> | null>;
|
||||||
|
abstract createOrGetStore<T = unknown>(
|
||||||
storeName: string,
|
storeName: string,
|
||||||
opts: StorageOpts,
|
opts: StorageOpts,
|
||||||
): Promise<BaseStore | null>;
|
): Promise<BaseStore<T, E>>;
|
||||||
abstract createOrGetStore(
|
|
||||||
storeName: string,
|
|
||||||
opts: StorageOpts,
|
|
||||||
): Promise<BaseStore>;
|
|
||||||
abstract deleteStore(storeName: string): Promise<void>;
|
abstract deleteStore(storeName: string): Promise<void>;
|
||||||
abstract deleteDatabase(): Promise<void>;
|
abstract deleteDatabase(): Promise<void>;
|
||||||
}
|
}
|
||||||
@@ -79,7 +86,10 @@ export type StoreValueProxy<T> = {
|
|||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export abstract class BaseStore<K = unknown> extends EventEmitter<StoreEvents> {
|
export abstract class BaseStore<
|
||||||
|
K = unknown,
|
||||||
|
E extends StoreEvents = StoreEvents,
|
||||||
|
> extends EventEmitter<E> {
|
||||||
/**
|
/**
|
||||||
* Returns an array of all keys in the store.
|
* Returns an array of all keys in the store.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { BaseStorage, BaseStore, StorageOpts } from './base-store.js';
|
import {
|
||||||
|
BaseStorage,
|
||||||
|
BaseStore,
|
||||||
|
StorageOpts,
|
||||||
|
StoreEvents,
|
||||||
|
} from './base-store.js';
|
||||||
import { SSESession } from '../utils/index.js';
|
import { SSESession } from '../utils/index.js';
|
||||||
import {
|
import {
|
||||||
Bytes,
|
Bytes,
|
||||||
@@ -15,13 +20,18 @@ export type EncrptedStoreOpts = StorageOpts & {
|
|||||||
encrypter: Encrypter;
|
encrypter: Encrypter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type StoreRemoteHTTPEvents = StoreEvents & {
|
||||||
|
connected: undefined;
|
||||||
|
disconnected: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A storage adapter for a remote storage server.
|
* A storage adapter for a remote storage server.
|
||||||
*
|
*
|
||||||
* All data sent to the server is first packed using MessagePack and then encrypted using the provided encrypter and then packed again using MessagePack.
|
* All data sent to the server is first packed using MessagePack and then encrypted using the provided encrypter and then packed again using MessagePack.
|
||||||
* The requests are signed using the provided private key and the timestamp of the request to mitigate replay attacks and to authenticate the request.
|
* The requests are signed using the provided private key and the timestamp of the request to mitigate replay attacks and to authenticate the request.
|
||||||
*/
|
*/
|
||||||
export class StorageRemoteHTTP extends BaseStorage {
|
export class StorageRemoteHTTP extends BaseStorage<StoreRemoteHTTPEvents> {
|
||||||
/**
|
/**
|
||||||
* Creates a new StorageRemoteHTTP instance.
|
* Creates a new StorageRemoteHTTP instance.
|
||||||
*
|
*
|
||||||
@@ -33,7 +43,7 @@ export class StorageRemoteHTTP extends BaseStorage {
|
|||||||
return storage;
|
return storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
private stores: Map<string, StoreRemoteHTTP> = new Map();
|
private stores: Map<string, StoreRemoteHTTP<any>> = new Map();
|
||||||
|
|
||||||
constructor(protected baseURL: string) {
|
constructor(protected baseURL: string) {
|
||||||
super();
|
super();
|
||||||
@@ -45,7 +55,9 @@ export class StorageRemoteHTTP extends BaseStorage {
|
|||||||
* @param storeName The name of the store to get.
|
* @param storeName The name of the store to get.
|
||||||
* @returns The store or null if it does not exist.
|
* @returns The store or null if it does not exist.
|
||||||
*/
|
*/
|
||||||
async getStore(storeName: string): Promise<StoreRemoteHTTP | null> {
|
async getStore<T = unknown>(
|
||||||
|
storeName: string,
|
||||||
|
): Promise<StoreRemoteHTTP<T> | null> {
|
||||||
return this.stores.get(storeName) || null;
|
return this.stores.get(storeName) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,8 +68,11 @@ export class StorageRemoteHTTP extends BaseStorage {
|
|||||||
* @param opts The options for the store. This includes the private key and encrypter.
|
* @param opts The options for the store. This includes the private key and encrypter.
|
||||||
* @returns The created store.
|
* @returns The created store.
|
||||||
*/
|
*/
|
||||||
async createStore(storeName: string, opts: EncrptedStoreOpts): Promise<StoreRemoteHTTP> {
|
async createStore<T = unknown>(
|
||||||
const store = await StoreRemoteHTTP.from(
|
storeName: string,
|
||||||
|
opts: EncrptedStoreOpts,
|
||||||
|
): Promise<StoreRemoteHTTP<T>> {
|
||||||
|
const store = await StoreRemoteHTTP.from<T>(
|
||||||
this.baseURL,
|
this.baseURL,
|
||||||
opts.privateKey,
|
opts.privateKey,
|
||||||
opts.encrypter,
|
opts.encrypter,
|
||||||
@@ -73,7 +88,10 @@ export class StorageRemoteHTTP extends BaseStorage {
|
|||||||
* @param opts The options for the store. This includes the private key and encrypter.
|
* @param opts The options for the store. This includes the private key and encrypter.
|
||||||
* @returns The created or existing store.
|
* @returns The created or existing store.
|
||||||
*/
|
*/
|
||||||
async createOrGetStore(storeName: string, opts: EncrptedStoreOpts): Promise<StoreRemoteHTTP> {
|
async createOrGetStore<T = unknown>(
|
||||||
|
storeName: string,
|
||||||
|
opts: EncrptedStoreOpts,
|
||||||
|
): Promise<StoreRemoteHTTP<T>> {
|
||||||
const store = await this.getStore(storeName);
|
const store = await this.getStore(storeName);
|
||||||
if (store) {
|
if (store) {
|
||||||
return store;
|
return store;
|
||||||
@@ -110,12 +128,15 @@ export class StorageRemoteHTTP extends BaseStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StoreRemoteHTTP extends BaseStore {
|
export class StoreRemoteHTTP<T = unknown> extends BaseStore<
|
||||||
static async from(
|
T,
|
||||||
|
StoreRemoteHTTPEvents
|
||||||
|
> {
|
||||||
|
static async from<T = unknown>(
|
||||||
baseURL: string,
|
baseURL: string,
|
||||||
privateKey: PrivateKey,
|
privateKey: PrivateKey,
|
||||||
encrypter: Encrypter,
|
encrypter: Encrypter,
|
||||||
) {
|
): Promise<StoreRemoteHTTP<T>> {
|
||||||
const store = new this(baseURL, privateKey, encrypter);
|
const store = new this(baseURL, privateKey, encrypter);
|
||||||
|
|
||||||
await store.subscribeEvents();
|
await store.subscribeEvents();
|
||||||
@@ -235,6 +256,12 @@ export class StoreRemoteHTTP extends BaseStore {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onReconnect: createSSESessionOptions,
|
onReconnect: createSSESessionOptions,
|
||||||
|
onConnected: () => {
|
||||||
|
this.emit('connected', undefined);
|
||||||
|
},
|
||||||
|
onDisconnected: () => {
|
||||||
|
this.emit('disconnected', undefined);
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user