Initial commit
This commit is contained in:
127
src/services/app.ts
Normal file
127
src/services/app.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Rates } from './rates.js';
|
||||
import { Settings } from './settings.js';
|
||||
import { Wallets } from './wallets.js';
|
||||
|
||||
import { inject, ref } from 'vue';
|
||||
import { type Router } from 'vue-router';
|
||||
|
||||
// XO Stack
|
||||
import {
|
||||
type BaseStorage,
|
||||
BlockchainElectrum,
|
||||
StorageLocalStorage,
|
||||
} from '@xocash/stack';
|
||||
|
||||
import { ReactiveBlockchain } from './blockchain.js';
|
||||
|
||||
export type AppDependencies = {
|
||||
router: Router;
|
||||
settings: Settings;
|
||||
blockchain: ReactiveBlockchain;
|
||||
rates: Rates;
|
||||
walletStorage: BaseStorage;
|
||||
wallets: Wallets;
|
||||
cache: BaseStorage;
|
||||
};
|
||||
|
||||
export class App {
|
||||
public isDebugMode = ref(false);
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// Initialization
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
public router: Router;
|
||||
public settings: Settings;
|
||||
public blockchain: ReactiveBlockchain;
|
||||
public rates: Rates;
|
||||
public walletStorage: BaseStorage;
|
||||
public wallets: Wallets;
|
||||
public cache: BaseStorage;
|
||||
|
||||
constructor(
|
||||
dependencies: AppDependencies
|
||||
) {
|
||||
this.router = dependencies.router;
|
||||
this.settings = dependencies.settings;
|
||||
this.blockchain = dependencies.blockchain;
|
||||
this.rates = dependencies.rates;
|
||||
this.walletStorage = dependencies.walletStorage;
|
||||
this.wallets = dependencies.wallets;
|
||||
this.cache = dependencies.cache;
|
||||
}
|
||||
|
||||
static async create(router: Router) {
|
||||
// Setup app storage adapter.
|
||||
const appStorage = await StorageLocalStorage.createOrOpen('xoApp_V0.0.1');
|
||||
const settingsStore = await appStorage.createOrGetStore('settings');
|
||||
|
||||
// Create settings class.
|
||||
const settings = await Settings.from(settingsStore);
|
||||
|
||||
// Setup rates.
|
||||
const rates = await Rates.from();
|
||||
rates.start();
|
||||
|
||||
// Setup wallets storage adapter.
|
||||
const walletStorage = await StorageLocalStorage.createOrOpen(
|
||||
'xoWallets_V0.0.5'
|
||||
);
|
||||
|
||||
// Setup cache storage adapter.
|
||||
const cacheStorage = await StorageLocalStorage.createOrOpen(
|
||||
'xoWalletCache_V0.0.5'
|
||||
);
|
||||
const blockchainCache = await cacheStorage.createOrGetStore(
|
||||
'electrumBlockchain',
|
||||
{
|
||||
syncInMemory: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Setup blockchain (and use our cache).
|
||||
const blockchain = await ReactiveBlockchain.from(
|
||||
await BlockchainElectrum.from({
|
||||
store: blockchainCache,
|
||||
})
|
||||
);
|
||||
|
||||
// Setup wallet manager.
|
||||
const walletManager = new Wallets(
|
||||
blockchain.blockchain,
|
||||
walletStorage,
|
||||
cacheStorage,
|
||||
);
|
||||
await walletManager.start();
|
||||
|
||||
// Create new instance of app.
|
||||
return new this(
|
||||
{
|
||||
router,
|
||||
blockchain,
|
||||
cache: cacheStorage,
|
||||
rates,
|
||||
settings,
|
||||
walletStorage,
|
||||
wallets: walletManager,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async initializeBrowser(): Promise<void> {
|
||||
// Make sure that this browser supports Mutex Locks (navigator.lock).
|
||||
// TODO: Will we even need this for CashStamps?
|
||||
if (typeof navigator.locks === 'undefined') {
|
||||
throw new Error(
|
||||
'Your browser does not support Mutex Locks. Please update your browser or switch to a browser that supports this feature.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function useApp() {
|
||||
const app = inject<App>('app');
|
||||
if (!app) throw new Error('App not properly initialized');
|
||||
return app;
|
||||
}
|
||||
60
src/services/blockchain.ts
Normal file
60
src/services/blockchain.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Mixin } from '@/utils/mixin.js';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import {
|
||||
type AddressStatusPayload,
|
||||
type BlockHeightPayload,
|
||||
BaseBlockchain,
|
||||
} from '@xocash/stack';
|
||||
|
||||
export class ReactiveBlockchain extends Mixin([
|
||||
BaseBlockchain<AddressStatusPayload, BlockHeightPayload>,
|
||||
]) {
|
||||
static async from(
|
||||
blockchain: BaseBlockchain<AddressStatusPayload, BlockHeightPayload>
|
||||
) {
|
||||
return new ReactiveBlockchain(blockchain);
|
||||
}
|
||||
|
||||
// Dependencies.
|
||||
public readonly blockchain: BaseBlockchain<
|
||||
AddressStatusPayload,
|
||||
BlockHeightPayload
|
||||
>;
|
||||
|
||||
// Reactives.
|
||||
isConnected = ref(false);
|
||||
blockHeight = ref<number | undefined>(undefined);
|
||||
|
||||
constructor(
|
||||
blockchain: BaseBlockchain<AddressStatusPayload, BlockHeightPayload>
|
||||
) {
|
||||
super(blockchain);
|
||||
|
||||
// Bind our events.
|
||||
blockchain.on(
|
||||
'isConnectedUpdated',
|
||||
(isConnected) => {
|
||||
this.isConnected.value = isConnected;
|
||||
},
|
||||
500
|
||||
);
|
||||
|
||||
blockchain.on(
|
||||
'blockHeightUpdated',
|
||||
(blockHeight) => {
|
||||
this.blockHeight.value = blockHeight;
|
||||
},
|
||||
500
|
||||
);
|
||||
|
||||
// Assign our class members.
|
||||
this.blockchain = blockchain;
|
||||
|
||||
// Set the initial block height.
|
||||
this.blockchain.fetchChainTip().then((chainTip) => {
|
||||
this.blockHeight.value = chainTip.height;
|
||||
});
|
||||
}
|
||||
}
|
||||
109
src/services/rates.ts
Normal file
109
src/services/rates.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { ref, triggerRef } from 'vue';
|
||||
|
||||
import {
|
||||
BaseRates,
|
||||
RatesComposite,
|
||||
RatesOracle,
|
||||
ratesMedianPolicy,
|
||||
} from '@xocash/stack';
|
||||
|
||||
import { Mixin } from '@/utils/mixin';
|
||||
|
||||
/**
|
||||
*/
|
||||
export class Rates extends Mixin([BaseRates]){
|
||||
// Our rates adapter.
|
||||
private readonly adapter: BaseRates;
|
||||
|
||||
// Our individual rates as a reactives.
|
||||
// NOTE: Because our adapter sends us updates one by one, we must use triggerRef to update this.
|
||||
private rates = ref<{ [key: string]: number | undefined }>({});
|
||||
|
||||
static async from(refreshMilliseconds = 60_000) {
|
||||
const oracle = await RatesOracle.from();
|
||||
|
||||
const compositeAdapter = new RatesComposite(
|
||||
[oracle],
|
||||
ratesMedianPolicy,
|
||||
refreshMilliseconds
|
||||
);
|
||||
|
||||
return new Rates(compositeAdapter);
|
||||
}
|
||||
|
||||
constructor(ratesAdapter: BaseRates) {
|
||||
super(ratesAdapter);
|
||||
|
||||
this.adapter = ratesAdapter;
|
||||
|
||||
this.adapter.on(
|
||||
'rateUpdated',
|
||||
({ numeratorUnitCode, denominatorUnitCode, price }) => {
|
||||
if (denominatorUnitCode !== 'BCH') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.rates.value[numeratorUnitCode] = price;
|
||||
triggerRef(this.rates);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
toBCH(amount: number, fromCurrency: string): number {
|
||||
const rate = this.rates.value[fromCurrency];
|
||||
if (rate === undefined) {
|
||||
return 0;
|
||||
// throw new Error(`Currency ${fromCurrency} not supported.`);
|
||||
}
|
||||
|
||||
return Rates.roundToDigits(amount / rate, 8);
|
||||
}
|
||||
|
||||
toSats(amount: number, fromCurrency: string): number {
|
||||
return this.toBCH(amount * 100_000_000, fromCurrency);
|
||||
}
|
||||
|
||||
fromSats(satoshis: number | bigint, targetCurrency: string): number {
|
||||
if (typeof satoshis === 'bigint') {
|
||||
satoshis = Number(satoshis);
|
||||
}
|
||||
|
||||
return this.fromBCH(satoshis / 100_000_000, targetCurrency);
|
||||
}
|
||||
|
||||
fromBCH(amount: number, targetCurrency: string): number {
|
||||
const rate = this.rates.value[targetCurrency];
|
||||
if (rate === undefined) {
|
||||
return 0;
|
||||
// throw new Error(`Currency ${targetCurrency} not supported.`);
|
||||
}
|
||||
|
||||
return Rates.roundToDigits(amount * rate, 2);
|
||||
}
|
||||
|
||||
formatSats(sats: number | bigint, targetCurrency: string) {
|
||||
const amount = this.fromSats(sats, targetCurrency);
|
||||
|
||||
return this.formatCurrency(amount, targetCurrency);
|
||||
}
|
||||
|
||||
static roundToDigits(numberToRound: number, digits: number): number {
|
||||
// Set the options of the Number Format object.
|
||||
const options: Intl.NumberFormatOptions = {
|
||||
style: 'decimal',
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
useGrouping: false,
|
||||
};
|
||||
|
||||
// Create an instance of number format using above options.
|
||||
// NOTE: We force the locale to en-GB so that the number is formatted correctly (e.g. with a decimal, not a comma).
|
||||
const numberFormat = new Intl.NumberFormat('en-GB', options);
|
||||
|
||||
// Format the number.
|
||||
const formattedAmount = numberFormat.format(numberToRound);
|
||||
|
||||
// Return the formatted number.
|
||||
return Number(formattedAmount);
|
||||
}
|
||||
}
|
||||
309
src/services/wallet.ts
Normal file
309
src/services/wallet.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import { computed, ref, shallowRef, triggerRef } from 'vue';
|
||||
|
||||
import {
|
||||
type BlockchainUTXO,
|
||||
type BlockchainUTXOs,
|
||||
type MapDiff,
|
||||
type WalletAddresses,
|
||||
type WalletTransactions,
|
||||
type WalletTransaction,
|
||||
BaseWallet,
|
||||
BlockHeader,
|
||||
Bytes,
|
||||
ExtMap,
|
||||
calculateBalanceSats,
|
||||
calculateBalanceTokens,
|
||||
} from '@xocash/stack';
|
||||
|
||||
import { Mixin } from '../utils/mixin.js';
|
||||
import { binToHex } from '@bitauth/libauth';
|
||||
|
||||
type ReactiveWalletDependencies<T extends BaseWallet> = {
|
||||
wallet: T;
|
||||
};
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// ReactiveWallet - Generic Reactive Wallet Wrapper
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A reactive Vue wrapper for any wallet that extends BaseWallet.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This class wraps any wallet from the stack package and adds reactive Vue
|
||||
* bindings for use in the UI. It uses the Mixin pattern to delegate all
|
||||
* wallet functionality to the underlying wallet instance.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // With a watch-only HD wallet
|
||||
* const hdWallet = await WalletHDWatch.from(deps, { xpub: '...' });
|
||||
* const reactiveHD = new ReactiveWallet(hdWallet);
|
||||
* await reactiveHD.start();
|
||||
*
|
||||
* // With a watch-only P2PKH wallet
|
||||
* const p2pkhWallet = await WalletP2PKHWatch.from(deps, { publicKey: '...' });
|
||||
* const reactiveP2PKH = new ReactiveWallet(p2pkhWallet);
|
||||
* await reactiveP2PKH.start();
|
||||
*
|
||||
* // With a regular wallet (signing capable)
|
||||
* const wallet = await WalletHD.from(deps, { mnemonic: '...' });
|
||||
* const reactiveWallet = new ReactiveWallet(wallet);
|
||||
* await reactiveWallet.start();
|
||||
* ```
|
||||
*/
|
||||
export class ReactiveWallet<T extends BaseWallet = BaseWallet> extends Mixin([
|
||||
BaseWallet,
|
||||
]) {
|
||||
public static async from<T extends BaseWallet>(deps: ReactiveWalletDependencies<T>): Promise<ReactiveWallet<T>> {
|
||||
const reactiveWallet = new ReactiveWallet(deps);
|
||||
await reactiveWallet.start();
|
||||
return reactiveWallet;
|
||||
}
|
||||
|
||||
//-----------------------------------
|
||||
// Reactive Wallet State
|
||||
//-----------------------------------
|
||||
|
||||
/** All addresses belonging to this wallet. */
|
||||
addresses = shallowRef<WalletAddresses>(new ExtMap());
|
||||
|
||||
/** All transactions involving this wallet. */
|
||||
transactions = shallowRef<WalletTransactions>(new ExtMap());
|
||||
|
||||
/** All unspent transaction outputs belonging to this wallet. */
|
||||
unspents = shallowRef<BlockchainUTXOs>(new ExtMap());
|
||||
|
||||
/** Block headers indexed by height. */
|
||||
blockHeaders = ref<{ [height: number]: BlockHeader }>({});
|
||||
|
||||
/** Whether the wallet has completed its initial load. */
|
||||
isReady = ref(false);
|
||||
|
||||
//-----------------------------------
|
||||
// Derived State
|
||||
//-----------------------------------
|
||||
|
||||
/** The current balance in satoshis. */
|
||||
balanceSats = computed(() => {
|
||||
return calculateBalanceSats(
|
||||
this.unspents.value.map((blockchainUTXO) => blockchainUTXO.utxo).toArray()
|
||||
);
|
||||
});
|
||||
|
||||
/** The current token balances by category. */
|
||||
balanceTokens = computed(() => {
|
||||
return calculateBalanceTokens(
|
||||
this.unspents.value.map((blockchainUTXO) => blockchainUTXO.utxo).toArray()
|
||||
);
|
||||
});
|
||||
|
||||
/** The underlying wallet instance. */
|
||||
readonly wallet: T;
|
||||
|
||||
/**
|
||||
* Creates a new ReactiveWallet.
|
||||
*
|
||||
* @param deps - The dependencies of the reactive wallet.
|
||||
*/
|
||||
constructor(deps: ReactiveWalletDependencies<T>) {
|
||||
super(deps.wallet);
|
||||
this.wallet = deps.wallet;
|
||||
|
||||
// Listen for state updates from the underlying wallet.
|
||||
deps.wallet.on(
|
||||
'stateUpdated',
|
||||
async () => {
|
||||
await Promise.all([
|
||||
this.updateAddresses(),
|
||||
this.updateUnspents(),
|
||||
this.updateTransactions(),
|
||||
]);
|
||||
|
||||
// Mark the wallet as ready after the first update.
|
||||
this.isReady.value = true;
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the reactive addresses from the underlying wallet.
|
||||
*/
|
||||
async updateAddresses(): Promise<void> {
|
||||
const addresses = await this.wallet.getAddresses();
|
||||
this.addresses.value = addresses;
|
||||
triggerRef(this.addresses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the reactive transactions from the underlying wallet.
|
||||
*
|
||||
* @returns The diff of added, removed, and updated transactions.
|
||||
*/
|
||||
async updateTransactions(): Promise<MapDiff<WalletTransaction>> {
|
||||
const transactions = await this.wallet.getTransactions();
|
||||
|
||||
// For each transaction, fetch the block header in the background.
|
||||
transactions.forEach((tx) => {
|
||||
if (tx.height <= 0) return;
|
||||
|
||||
this.wallet.blockchain.fetchBlockHeader(tx.height).then((blockHeader) => {
|
||||
this.blockHeaders.value[tx.height] = blockHeader;
|
||||
});
|
||||
});
|
||||
|
||||
// Create a diff of added, removed and updated transactions.
|
||||
const { added, removed, updated } = this.transactions.value.diff(
|
||||
transactions,
|
||||
(a, b) => a.height === b.height
|
||||
);
|
||||
|
||||
// Update existing transactions.
|
||||
updated.forEach((tx) => {
|
||||
const existingTx = this.transactions.value.get(tx.hash.toHex());
|
||||
if (existingTx) {
|
||||
Object.assign(existingTx, tx);
|
||||
}
|
||||
});
|
||||
|
||||
// Add new transactions.
|
||||
added.forEach((tx) => this.transactions.value.set(tx.hash.toHex(), tx));
|
||||
|
||||
// Remove old transactions.
|
||||
removed.forEach((tx) => this.transactions.value.delete(tx.hash.toHex()));
|
||||
|
||||
// Trigger reactivity if there were changes.
|
||||
if (added.size || updated.size || removed.size) {
|
||||
triggerRef(this.transactions);
|
||||
}
|
||||
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the reactive unspents from the underlying wallet.
|
||||
*
|
||||
* @returns The diff of added, removed, and updated unspents.
|
||||
*/
|
||||
async updateUnspents(): Promise<MapDiff<BlockchainUTXO>> {
|
||||
const unspents = await this.wallet.getUnspents();
|
||||
|
||||
// Create a diff of added, removed and updated unspents.
|
||||
const { added, removed, updated } = this.unspents.value.diff(
|
||||
unspents,
|
||||
(a, b) => a.utxo.outpoint.toString() === b.utxo.outpoint.toString()
|
||||
);
|
||||
|
||||
// Add new unspents.
|
||||
added.forEach((unspent) =>
|
||||
this.unspents.value.set(unspent.utxo.outpoint.toString(), unspent)
|
||||
);
|
||||
|
||||
// Update existing unspents.
|
||||
updated.forEach((unspent) => {
|
||||
const existingUnspent = this.unspents.value.get(
|
||||
unspent.utxo.outpoint.toString()
|
||||
);
|
||||
if (existingUnspent) {
|
||||
Object.assign(existingUnspent, unspent);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove old unspents.
|
||||
removed.forEach((unspent) =>
|
||||
this.unspents.value.delete(unspent.utxo.outpoint.toString())
|
||||
);
|
||||
|
||||
// Trigger reactivity if there were changes.
|
||||
if (added.size || updated.size || removed.size) {
|
||||
triggerRef(this.unspents);
|
||||
}
|
||||
|
||||
return { added, removed, updated };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the balance change for a given transaction.
|
||||
*
|
||||
* @param tx - The transaction to calculate the balance change for.
|
||||
* @returns The net balance change in satoshis (positive = incoming, negative = outgoing).
|
||||
*/
|
||||
calculateBalanceChange(tx: WalletTransaction): bigint {
|
||||
let incoming = 0n;
|
||||
let outgoing = 0n;
|
||||
|
||||
// Calculate incoming funds from outputs.
|
||||
tx.transaction.getOutputs().forEach((output) => {
|
||||
const outputAddress = binToHex(output.lockingBytecode);
|
||||
if (this.addresses.value.get(outputAddress)) {
|
||||
incoming += BigInt(output.valueSatoshis);
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate outgoing funds from inputs.
|
||||
tx.sourceOutputs.forEach((sourceOutput) => {
|
||||
const lockscriptHex = binToHex(sourceOutput.lockingBytecode);
|
||||
if (this.addresses.value.get(lockscriptHex)) {
|
||||
outgoing += sourceOutput.valueSatoshis;
|
||||
}
|
||||
});
|
||||
|
||||
return incoming - outgoing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the token balance change for a given transaction.
|
||||
*
|
||||
* @param tx - The transaction to calculate the token balance change for.
|
||||
* @returns An object with category IDs as keys and balance changes as values.
|
||||
*/
|
||||
calculateTokenBalanceChange(tx: WalletTransaction): {
|
||||
[categoryId: string]: bigint;
|
||||
} {
|
||||
const categories: {
|
||||
[categoryId: string]: { incoming: bigint; outgoing: bigint };
|
||||
} = {};
|
||||
|
||||
// Calculate incoming tokens from outputs.
|
||||
tx.transaction.getOutputs().forEach((output) => {
|
||||
if (output.token) {
|
||||
const categoryId = Bytes.from(output.token.category).toHex();
|
||||
|
||||
if (!categories[categoryId]) {
|
||||
categories[categoryId] = { incoming: 0n, outgoing: 0n };
|
||||
}
|
||||
|
||||
const outputAddress = binToHex(output.lockingBytecode);
|
||||
if (this.addresses.value.get(outputAddress)) {
|
||||
categories[categoryId].incoming += BigInt(output.token.amount);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate outgoing tokens from inputs.
|
||||
tx.sourceOutputs.forEach((sourceOutput) => {
|
||||
if (sourceOutput.token) {
|
||||
const categoryId = Bytes.from(sourceOutput.token.category).toHex();
|
||||
|
||||
if (!categories[categoryId]) {
|
||||
categories[categoryId] = { incoming: 0n, outgoing: 0n };
|
||||
}
|
||||
|
||||
const lockscriptHex = binToHex(sourceOutput.lockingBytecode);
|
||||
if (this.addresses.value.get(lockscriptHex)) {
|
||||
categories[categoryId].outgoing += BigInt(sourceOutput.token.amount);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Object.entries(categories).reduce(
|
||||
(acc, [categoryId, amounts]) => {
|
||||
acc[categoryId] = amounts.incoming - amounts.outgoing;
|
||||
return acc;
|
||||
},
|
||||
{} as { [categoryId: string]: bigint }
|
||||
);
|
||||
}
|
||||
}
|
||||
223
src/services/wallets.ts
Normal file
223
src/services/wallets.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { shallowReactive, toRaw } from 'vue';
|
||||
|
||||
import {
|
||||
type BaseStorage,
|
||||
type WalletHDEntropy,
|
||||
type WalletHDDerivationData,
|
||||
type WalletHDGenesisData,
|
||||
type WalletBlockchain,
|
||||
type WalletP2PKHGenesisData,
|
||||
type WalletName,
|
||||
StoreInMemory,
|
||||
BaseStore,
|
||||
WalletHD,
|
||||
WalletP2PKH,
|
||||
BaseWallet,
|
||||
Mnemonic,
|
||||
type MnemonicRaw,
|
||||
} from '@xocash/stack';
|
||||
|
||||
import { ReactiveWallet } from 'src/services/wallet.js';
|
||||
|
||||
// Remove our definition for using Mnemonic in the WalletHDGenesisData type, we store them as MnemonicRaw so we can instantiate them.
|
||||
export type AppWalletHDGenesisData = // Remove mnemonic option
|
||||
((
|
||||
| Exclude<WalletHDEntropy, { mnemonic: Mnemonic }>
|
||||
// Add mnemonic option with MnemonicRaw type
|
||||
| { mnemonic: MnemonicRaw }
|
||||
) &
|
||||
WalletHDDerivationData) & {
|
||||
type: 'WalletHD';
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type AppWalletP2PKHGenesisData = WalletP2PKHGenesisData & {
|
||||
type: 'WalletP2PKH';
|
||||
name: string;
|
||||
};
|
||||
export type AppWalletData = AppWalletP2PKHGenesisData | AppWalletHDGenesisData;
|
||||
export type AppWalletStore = { [uid: string]: AppWalletData };
|
||||
|
||||
export type WalletsStore = WalletP2PKHGenesisData | AppWalletHDGenesisData;
|
||||
export type WalletsSupported = WalletP2PKHGenesisData | AppWalletHDGenesisData;
|
||||
|
||||
export class Wallets {
|
||||
public walletsStore?: BaseStore;
|
||||
|
||||
public wallets = shallowReactive<{
|
||||
[walletId: string]: ReactiveWallet;
|
||||
}>({});
|
||||
|
||||
constructor(
|
||||
public blockchain: WalletBlockchain,
|
||||
public storage: BaseStorage,
|
||||
public cache: BaseStorage,
|
||||
) {}
|
||||
|
||||
async start(): Promise<void> {
|
||||
// Get or create a store for WalletGenesis data.
|
||||
this.walletsStore = await this.storage.createOrGetStore('wallets', {
|
||||
syncInMemory: true,
|
||||
});
|
||||
|
||||
// Get all child wallets.
|
||||
const wallets = await this.walletsStore.all<WalletsSupported>();
|
||||
|
||||
// Initialize each wallet.
|
||||
const walletInitPromises = Object.entries(wallets).map(
|
||||
async ([walletId, genesisData]) => {
|
||||
// Get the cache for this wallet.
|
||||
const cacheStore = await this.cache.createOrGetStore(walletId, {
|
||||
syncInMemory: true,
|
||||
});
|
||||
|
||||
// If this is a WalletHD type...
|
||||
if (genesisData.type === 'WalletHD') {
|
||||
// Convert the mnemonic from Raw to Instance if we are using a mnemonic.
|
||||
const walletData =
|
||||
'mnemonic' in genesisData
|
||||
? {
|
||||
...genesisData,
|
||||
mnemonic: Mnemonic.fromRaw(genesisData.mnemonic),
|
||||
}
|
||||
: genesisData;
|
||||
|
||||
// Instantiate the wallet.
|
||||
const wallet = await WalletHD.from(
|
||||
{
|
||||
blockchain: this.blockchain,
|
||||
cache: cacheStore,
|
||||
},
|
||||
walletData
|
||||
);
|
||||
|
||||
// Add a persistent store for our activities.
|
||||
await wallet.activities.store.addStore(
|
||||
await this.storage.createStore(
|
||||
`activities_${await wallet.getId()}`,
|
||||
{}
|
||||
)
|
||||
);
|
||||
|
||||
// Push it to our list of wallets.
|
||||
this.wallets[walletId] = await ReactiveWallet.from({
|
||||
wallet,
|
||||
});
|
||||
|
||||
// Start the wallet.
|
||||
// NOTE: We deliberately do not await this as we want it to happen in the background.
|
||||
wallet.start();
|
||||
} else if (genesisData.type === 'WalletP2PKH') {
|
||||
// Instantiate the wallet.
|
||||
const wallet = await WalletP2PKH.from(
|
||||
{
|
||||
blockchain: this.blockchain,
|
||||
},
|
||||
genesisData
|
||||
);
|
||||
|
||||
// Add a persistent store for our activities.
|
||||
await wallet.activities.store.addStore(
|
||||
await this.storage.createStore(
|
||||
`activities_${await wallet.getId()}`,
|
||||
{}
|
||||
)
|
||||
);
|
||||
|
||||
// Push it to our list of wallets.
|
||||
this.wallets[walletId] = await ReactiveWallet.from({
|
||||
wallet,
|
||||
});
|
||||
|
||||
// Start the wallet.
|
||||
// NOTE: We deliberately do not await this as we want it to happen in the background.
|
||||
wallet.start();
|
||||
}
|
||||
// Otherwise, this is an unsupported wallet type.
|
||||
else {
|
||||
console.warn(
|
||||
`${walletId} has an unsupported wallet type`,
|
||||
genesisData
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.time('walletInit');
|
||||
// Wait for all wallets to initialize.
|
||||
await Promise.all(walletInitPromises);
|
||||
console.timeEnd('walletInit');
|
||||
}
|
||||
|
||||
async createWallet(
|
||||
walletData: WalletHDGenesisData | WalletP2PKHGenesisData
|
||||
): Promise<string> {
|
||||
// Extract the raw Wallet Data.
|
||||
// NOTE: This is a Vue Quirk. We cannot store reactives as structuredClones cannot be performed on them.
|
||||
// Thus, we must extract the raw value.
|
||||
const walletDataRaw = toRaw(walletData);
|
||||
|
||||
const walletCache = StoreInMemory.from();
|
||||
|
||||
let wallet: BaseWallet;
|
||||
|
||||
const dependencies = {
|
||||
blockchain: this.blockchain,
|
||||
cache: walletCache,
|
||||
};
|
||||
|
||||
if (walletDataRaw.type === 'WalletHD') {
|
||||
wallet = await WalletHD.from(dependencies, walletDataRaw);
|
||||
} else if (walletData.type === 'WalletP2PKH') {
|
||||
wallet = await WalletP2PKH.from(dependencies, walletDataRaw);
|
||||
} else {
|
||||
throw new Error(`Unsupported wallet data: ${walletData}`);
|
||||
}
|
||||
|
||||
// Get the Wallet ID.
|
||||
const walletId = await wallet.getId();
|
||||
|
||||
// Add the wallet to our store.
|
||||
await this.walletsStore?.set(walletId, {
|
||||
...walletData,
|
||||
mnemonic:
|
||||
'mnemonic' in walletData ? walletData.mnemonic.toRaw() : undefined,
|
||||
});
|
||||
|
||||
// Add a persistent store for this wallet's activities.
|
||||
await wallet.activities.store.addStore(
|
||||
await this.storage.createStore(`activities_${await wallet.getId()}`, {})
|
||||
);
|
||||
|
||||
// Determine the new Wallet's Default name.
|
||||
const walletCount = Object.keys(this.wallets).length;
|
||||
const walletName =
|
||||
walletCount === 0 ? 'Default Wallet' : `Wallet ${walletCount + 1}`;
|
||||
|
||||
// Set the default metadata for this wallet.
|
||||
// NOTE: Give it a timestamp of 0. This is to support wallet syncing. Without it, this new activity would overwrite the current wallet name activity from the server/other wallets
|
||||
await wallet.activities.add<WalletName>({
|
||||
key: 'WalletName',
|
||||
value: walletName,
|
||||
timestamp: BigInt(0),
|
||||
});
|
||||
|
||||
// Start the wallet.
|
||||
// NOTE: We deliberately do not await this as we want it to happen in the background.
|
||||
wallet.start();
|
||||
|
||||
// Push it to our list of wallets.
|
||||
this.wallets[walletId] = await ReactiveWallet.from({
|
||||
wallet,
|
||||
});
|
||||
|
||||
return walletId;
|
||||
}
|
||||
|
||||
async deleteWallet(walletId: string): Promise<void> {
|
||||
await this.walletsStore?.delete(walletId);
|
||||
await this.storage.deleteStore(`activities_${walletId}`);
|
||||
|
||||
delete this.wallets[walletId];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user