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
|
```ts
|
||||||
class SomeClass {
|
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 blockchain: BaseBlockchain;
|
||||||
protected store: BaseStore;
|
protected store: BaseStore;
|
||||||
|
|
||||||
@@ -86,6 +87,8 @@ For example:
|
|||||||
class SomeClass {
|
class SomeClass {
|
||||||
// Static "from" factory method.
|
// Static "from" factory method.
|
||||||
// NOTE: The below args are shorthand for using the same arguments as the constructor.
|
// 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>) {
|
static async from(...args: ConstructorParameters<typeof SomeClass>) {
|
||||||
// Create the instance.
|
// Create the instance.
|
||||||
const someClass = new SomeClass(...args);
|
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".*
|
*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
|
### 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.
|
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 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).
|
- 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:
|
An example of working with Zod:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@@ -132,4 +216,19 @@ const parsedPayload = SomeRemotePayload.parse(rawPayload);
|
|||||||
export type StoreRemoteHTTPEvent = z.infer<typeof StoreRemoteHTTPEventSchema>;
|
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",
|
"docs:preview": "lerna run preview --scope=@xocash/docs --include-dependencies",
|
||||||
"format": "lerna run format",
|
"format": "lerna run format",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"madge": "npx madge --circular ./packages",
|
"madge": "lerna run madge",
|
||||||
"test": "lerna run test",
|
"test": "lerna run test",
|
||||||
"test:coverage": "lerna run test:coverage",
|
"test:coverage": "lerna run test:coverage",
|
||||||
"version:patch": "lerna version patch",
|
"version:patch": "lerna version patch",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"build": "tsc --build --clean && tsc --build",
|
"build": "tsc --build --clean && tsc --build",
|
||||||
"prepublishOnly": "npm run build",
|
"prepublishOnly": "npm run build",
|
||||||
"format": "prettier --write \"**/*.{js,ts,md,json}\" --ignore-path .gitignore",
|
"format": "prettier --write \"**/*.{js,ts,md,json}\" --ignore-path .gitignore",
|
||||||
|
"madge": "npx madge --circular ./src/index.ts",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type BCHUnits = ['sats', 'bits', 'mBCH', 'BCH'];
|
export type BCHUnits = 'sats' | 'bits' | 'mBCH' | 'BCH';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Satoshis Primitive
|
* Satoshis Primitive
|
||||||
@@ -206,4 +206,70 @@ export class Satoshis {
|
|||||||
toRaw() {
|
toRaw() {
|
||||||
return this.satoshis;
|
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",
|
"bench": "vitest bench",
|
||||||
"build": "tsc --build --clean && tsc --build",
|
"build": "tsc --build --clean && tsc --build",
|
||||||
"format": "prettier --write \"**/*.{js,ts,md,json}\" --ignore-path .gitignore",
|
"format": "prettier --write \"**/*.{js,ts,md,json}\" --ignore-path .gitignore",
|
||||||
|
"madge": "npx madge --circular ./src/index.ts",
|
||||||
"prepublishOnly": "npm run build",
|
"prepublishOnly": "npm run build",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
|
|||||||
Reference in New Issue
Block a user