Merge branch 'xo_integration_part_2' into 'development'
XO Integration Part II See merge request GeneralProtocols/xo/stack!93
This commit is contained in:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user