XO Integration Part II

This commit is contained in:
Jim Hamill
2025-05-13 01:05:31 +00:00
parent 96a50f071d
commit 065820ba40
5 changed files with 169 additions and 2 deletions
+99
View File
@@ -54,6 +54,7 @@ For example:
```ts
class SomeClass {
// NOTE: We do not use concrete implementations (e.g. "BlockchainELectrum" or "StoreIndexedDB"), but the abstractions ("BaseBlockchain" and "BaseStore") instead.
protected blockchain: BaseBlockchain;
protected store: BaseStore;
@@ -86,6 +87,8 @@ For example:
class SomeClass {
// Static "from" factory method.
// NOTE: The below args are shorthand for using the same arguments as the constructor.
// Do not feel forced to use this - there are definitely some situations where you will have to define the args manually.
// But do try to follow the argument patterns in the above "Dependency Injection" section if possible.
static async from(...args: ConstructorParameters<typeof SomeClass>) {
// Create the instance.
const someClass = new SomeClass(...args);
@@ -103,6 +106,85 @@ Although this isn't always necessary (i.e. not all instantiations need to be `as
*Semantically, think of "from" as meaning "instantiate from": This is more succinct than a `createFrom...` factory method which, if we have multiple ways of instantiation, can become verbose. E.g. `PrivateKey.fromWIF()` as opposed to `PrivateKey.createFromWIF()`. It also allows us to differentiate between actually "creating" something (e.g. a `Filesystem.createFile(...)`) versus "instantiating" a representation of a "thing".*
#### Extending from Static Factory Methods
If you anticipate that someone will `extend` a class and want to re-use your static factory method, this can be achieved as follows:
```ts
class YourClass {
// NOTE: The generic T extends typeof YourClass (so that we can return the correct type).:
static async from<T extends typeof YourClass>(
// NOTE: "this" as first argument is what technically, allows the extended class to instantiate correctly.
this: T,
params: SomeParams = {},
) {
// NOTE: So that Typescript can type correctly, we perform a cast.
return new this(params) as InstanceType<T>;
}
// ...
}
```
Extended classes can then be instantiated seamlessly as follows:
```ts
class YourExtendedClass extends YourClass {
// ...
}
// This will now be an instance of YourExtendedClass without having to redefine the `from(...)` method.
const yourExtClass = YourClass.from(...);
```
This adds quite a bit of confusing syntactic overhead, so only do this if you anticipate use-cases where extension is likely.
### EventEmitter Patterns for BaseAdapters
All BaseAdapters should extend EventEmitter. This design choice enables Stack components to work seamlessly with different frontend frameworks (vanilla JS, Vue, React, etc.) through a consistent event-based interface.
1. State Management
Adapters in Stack should maintain minimal state. While backend flows typically operate procedurally (fetching data as needed), frontends often require reactive state updates for UI rendering.
2. Framework Agnostic
Since reactive state handling varies across frameworks, we use event hooks rather than imposing specific state management patterns, allowing applications to handle higher-level state according to their framework (and product's) conventions.
To illustrate how this might be used with VueJS:
```ts
// Create a reactive variable (Vue example)
const isConnected = ref();
// Initialize adapter
const someAdapter = new SomeAdapter();
// Bind adapter state to reactive variable
someAdapter.on('isConnected', (isConnected) => {
isConnected.value = isConnected;
});
```
```ts
// Declare a reactive variable for unspents (Vue example)
const unspents = ref([]);
// Initialize the wallet
const someWallet = await WalletHD.from(genesisData);
// Hook into state updated event
someWallet.on(
'stateUpdated',
async () => {
// Sync unspents to the reactive variable
unspents.value = await someWallet.updateUnspents(),
},
// (Optional) Debounce for 500ms to prevent excessive redraws
500
);
```
**Note: While XO Wallet will provide convenience wrappers around these patterns, this represents the general approach to use in @xocash/stack.**
### Validations
Check types with TypeScript - Runtime type-checking is a code smell. If the function accepts a string, assume it's been given a string. TypeScript definitions should expose improperly called functions to the developer at compile time, don't re-implement it at runtime.
@@ -112,6 +194,8 @@ There are a few exceptions to this:
- If TypeScript's lack of dependant types makes validating certain properties of a type (e.g. the length of a Uint8Array), then runtime type-checking should be used to detect and return errors.
- If we are working with a network (remote/untrusted) payload, Type Validation should be done with [Zod](https://www.npmjs.com/package/zod).
#### Validating External Payloads
An example of working with Zod:
```ts
@@ -132,4 +216,19 @@ const parsedPayload = SomeRemotePayload.parse(rawPayload);
export type StoreRemoteHTTPEvent = z.infer<typeof StoreRemoteHTTPEventSchema>;
```
#### Leverage Primitives Over Repeated Validation
Avoid revalidation at every code-path: This is not only inefficient, but a giant code-smell. Instead, make use of Primitives as these provide assurances of correctness (Primitives are not just intended as a convenience - they are also intended to provide safety). For example, given a method:
```ts
// You can accept other types (this is a nice convenience).
broadcastTransaction(transaction: Transaction | Uint8Array): Transaction {
// ... but always cast to the Primitive (to ensure correctness).
if(!(transaction instanceof Transaction) {
transaction = Transaction.from(transaction);
}
// ... use the Transaction primitive in follow up code.
}
```
+1 -1
View File
@@ -23,7 +23,7 @@
"docs:preview": "lerna run preview --scope=@xocash/docs --include-dependencies",
"format": "lerna run format",
"lint": "eslint .",
"madge": "npx madge --circular ./packages",
"madge": "lerna run madge",
"test": "lerna run test",
"test:coverage": "lerna run test:coverage",
"version:patch": "lerna version patch",
+1
View File
@@ -13,6 +13,7 @@
"build": "tsc --build --clean && tsc --build",
"prepublishOnly": "npm run build",
"format": "prettier --write \"**/*.{js,ts,md,json}\" --ignore-path .gitignore",
"madge": "npx madge --circular ./src/index.ts",
"test": "vitest --run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest",
+67 -1
View File
@@ -1,4 +1,4 @@
export type BCHUnits = ['sats', 'bits', 'mBCH', 'BCH'];
export type BCHUnits = 'sats' | 'bits' | 'mBCH' | 'BCH';
/**
* Satoshis Primitive
@@ -206,4 +206,70 @@ export class Satoshis {
toRaw() {
return this.satoshis;
}
/**
* Formats the amount in the specified unit with formatting options.
*
* @param units - The unit to format as ('sats', 'bits', 'mBCH', 'BCH')
* @param opts - Formatting options
* @param opts.roundToDigits - Number of decimals to round to
* @param opts.appendSuffix - Whether to append the unit suffix
* @returns Formatted amount as string
*
* @example
* // Format as BCH with 2 decimal places and suffix
* satoshis.format('BCH', { roundToDigits: 2, appendSuffix: true }) // "0.12 BCH"
*
* @example
* // Format as satoshis without suffix
* satoshis.format('sats', { roundToDigits: 0, appendSuffix: false }) // "12345678"
*/
public format(
units: BCHUnits,
opts: {
roundToDigits?: number;
appendSuffix?: boolean;
showApproximationSymbol?: boolean;
} = {},
): string {
// Set default values if not provided
const roundToDigits = opts.roundToDigits ?? 4;
const appendSuffix = opts.appendSuffix ?? true;
const showApproximationSymbol = opts.showApproximationSymbol ?? true;
// Get the exact amount in the specified unit before rounding
let exactAmount: number;
// Use type assertion to help TypeScript understand this is a valid BCHUnits value
switch (units) {
case 'sats':
exactAmount = this.toSats();
break;
case 'bits':
exactAmount = Number(this.satoshis) / 100;
break;
case 'mBCH':
exactAmount = Number(this.satoshis) / 100_000;
break;
case 'BCH':
exactAmount = Number(this.satoshis) / 100_000_000;
break;
default:
throw new Error(`Unsupported unit: ${units}`);
}
// Format the amount with the specified number of decimal places
const roundedAmount = Satoshis.roundToDigits(exactAmount, roundToDigits);
// Check if rounding occurred
const wasRounded = exactAmount !== roundedAmount;
// Add approximation symbol if needed and enabled
const prefix = wasRounded && showApproximationSymbol ? '~' : '';
// Return the formatted amount with prefix and/or suffix
return appendSuffix
? `${prefix}${roundedAmount} ${units}`
: `${prefix}${roundedAmount}`;
}
}
+1
View File
@@ -23,6 +23,7 @@
"bench": "vitest bench",
"build": "tsc --build --clean && tsc --build",
"format": "prettier --write \"**/*.{js,ts,md,json}\" --ignore-path .gitignore",
"madge": "npx madge --circular ./src/index.ts",
"prepublishOnly": "npm run build",
"test": "vitest --run",
"test:coverage": "vitest run --coverage",