Files
xo-stack/packages/stack/src/wallet/base-wallet.ts
T

224 lines
6.5 KiB
TypeScript

import { type TransactionBuilder } from './transaction-builder.js';
import {
WalletUTXOs,
type InputTemplateBCH,
type TransactionTemplate,
type WalletAddresses,
type WalletBlockchain,
type WalletTransactions,
} from './types.js';
import { type BlockchainUTXOs } from '../blockchain/index.js';
import { type Activities, type BaseStore } from '../storage/index.js';
import { EventEmitter } from '../utils/index.js';
import {
type TokenBalances,
type Address,
Bytes,
Hasher,
encodeExtendedJson,
} from '@xocash/primitives';
export type WalletGenesisData = Record<string, unknown>;
export type WalletEvents = {
shouldMonitorUpdated: boolean;
addressesUpdated: WalletAddresses;
transactionsUpdated: WalletTransactions;
unspentsUpdated: BlockchainUTXOs;
stateUpdated: string | null;
};
/**
* Common interface for all wallet types.
*
* @remarks
*
* All function signatures are deliberately asynchronous to accommodate for cases where wallets might be remote.
* For example, we might eventually want to support a multisig service.
* Or we might have to support some other custodial "Apple Pay"-like OS-interface to get accepted onto their app store.
*/
export abstract class BaseWallet<
WalletEventsType extends WalletEvents = WalletEvents,
> extends EventEmitter<WalletEventsType> {
/**
* Derive a key to use based on the given Genesis Data.
*
* @remarks
*
* These keys can be used for Wallet Id's, Signing Keys, Encryption Keys and for Dapp Samdboxes.
*/
static deriveKey(genesisData: WalletGenesisData, keyName: string) {
// Hash the genesis data using Hash256 and the key ("WalletID") for this derivation using Hash256.
const genesisDataHashed = Hasher.fromUTF8(
encodeExtendedJson(genesisData),
).toHash256Bytes();
const keyHashed = Hasher.fromUTF8(keyName).toHash256Bytes();
// Combine the hashes and do one final hash for good measure.
return Hasher.fromBytes(
new Uint8Array([...genesisDataHashed, ...keyHashed]),
).toHash256Bytes();
}
static deriveId(genesisData: WalletGenesisData) {
return BaseWallet.deriveKey(genesisData, 'walletId');
}
// Dependencies.
abstract readonly activities: Activities;
abstract readonly blockchain: WalletBlockchain;
abstract readonly cache: BaseStore;
// Genesis Data.
abstract readonly genesisData: WalletGenesisData;
/**
* Get the ID for this wallet.
*/
async getId() {
// NOTE: Use the cache to optimize future calls.
return Bytes.from(
await this.cache.getOrSet('walletId', async () =>
BaseWallet.deriveKey(this.genesisData, 'walletId'),
),
).toHex();
}
/**
* Get the encryption key to use for this wallet.
*/
async getEncryptionKey() {
// NOTE: Use the cache to optimize future calls.
return await this.cache.getOrSet('encryptionKey', async () =>
BaseWallet.deriveKey(this.genesisData, 'encryptionKey'),
);
}
/**
* Get the signing key to use for this wallet.
*/
async getSigningKey() {
// NOTE: Use the cache to optimize future calls.
return this.cache.getOrSet('signingKey', async () =>
BaseWallet.deriveKey(this.genesisData, 'signingKey'),
);
}
/**
* Start monitoring this wallet for any transaction activity.
*/
abstract start(): Promise<void>;
/**
* Stop monitoring this wallet for any transaction activity.
*/
abstract stop(): Promise<void>;
/**
* Remove any event listeners from this wallet.
*/
abstract destroy(): Promise<void>;
/**
* Fetch all transactions from the blockchain involving this wallet.
*
* @remarks
*
* We will want to add a query argument so that workers can use this efficiently:
*
* ```ts
* {
* limit?: number;
* offset?: number;
* sort?: () => number;
* filter?: () => boolean;
* }
* ```
*
* This "query" argument should be turned into a generic type as we will want to re-use it for Activities.
*/
abstract getTransactions(): Promise<WalletTransactions>;
/**
* Fetch all unspents from the blockchain belonging to this wallet.
*/
abstract getUnspents(): Promise<BlockchainUTXOs>;
/**
* Fetch all unspents from the blockchain belonging to this wallet.
*
* @remarks
*
* I was considering removing this and just storing unspents as "UTXOSignable"s.
* But that might not be ideal if we want to have Watch-only wallets.
* A better approach might be to have Fetch Unspents return `Array<UTXO | UTXOSignable>`
* Still need to flesh that out.
*
* EDIT: We're going to want to store state for caching anyway...
* May as well just getUnspentDirectives() from the local state instead of fetch.
* Likewise for balances.
*/
abstract getUnspentDirectives(): Promise<WalletUTXOs>;
/**
* Get the satoshi balance of this wallet.
*
* @remarks
*
* Consider confirmed/unconfirmed balances (or some other representation).
*
* This becomes important in some instances because services like BitPay require one confirmation.
* This'll also have bearing on how we define our UTXO Selection strategies.
*/
abstract getBalanceSats(): Promise<bigint>;
/**
* Get the token balance of this wallet.
*
* @remarks
*
* Consider confirmed/unconfirmed balances (or some other representation).
*
* This becomes important in some instances because services like BitPay require one confirmation.
* This'll also have bearing on how we define our UTXO Selection strategies.
*/
abstract getBalanceTokens(): Promise<TokenBalances>;
/**
* Get all addresses belonging to this wallet.
*/
abstract getAddresses(): Promise<WalletAddresses>;
/**
* Get the address that this wallet should receive to.
*/
abstract getReceivingAddress(): Promise<Address>;
/**
* Select UTXOs from this wallet to meet the amounts required.
*
* @remarks
*
* TODO: We are going to want to return the Transaction that was compiled.
*
* TODO: This should accept a second argument (probably function) that can filter/sory UTXOs.
* The intent here is to provide a "spending policy". E.g. Only use (or prioritise) UTXOs with 1-conf.
*/
abstract signTransaction(
template: Partial<TransactionTemplate>,
): Promise<TransactionBuilder>;
/**
* Select UTXOs from this wallet to meet the amounts required and broadcast it to the Blockchain.
*
* @remarks
*
* TODO: We are going to want to return the Transaction that was compiled.
*/
abstract sendTransaction(
template: Partial<TransactionTemplate>,
): Promise<Uint8Array>;
}