Merge branch '43-feat-primitives-have-bytes-primitive-extends-uint8array-if-possible' into 'development'

Resolve "feat(Primitives): Have Bytes Primitive extends Uint8Array (if possible)"

Closes #43

See merge request GeneralProtocols/xo/stack!76
This commit is contained in:
Jim Hamill
2025-04-28 02:49:25 +00:00
19 changed files with 296 additions and 269 deletions
+6
View File
@@ -11,18 +11,21 @@ The XO Stack is a comprehensive framework designed for building applications on
The XO Stack adopts an Object-Oriented design for several key reasons: The XO Stack adopts an Object-Oriented design for several key reasons:
1. **Encapsulation and Data Privacy** 1. **Encapsulation and Data Privacy**
- Internal implementation details are hidden from consumers - Internal implementation details are hidden from consumers
- State management is contained within specific classes - State management is contained within specific classes
- Clear boundaries between different system components - Clear boundaries between different system components
- Reduced likelihood of state-related bugs - Reduced likelihood of state-related bugs
2. **Adapter Pattern Benefits** 2. **Adapter Pattern Benefits**
- Pluggable components that can be swapped without changing application code - Pluggable components that can be swapped without changing application code
- Easy to add new implementations for different environments - Easy to add new implementations for different environments
- Testing simplified through mock adapters - Testing simplified through mock adapters
- Reduced vendor lock-in - Reduced vendor lock-in
3. **Developer Experience** 3. **Developer Experience**
- Strong IDE support with autocomplete suggestions - Strong IDE support with autocomplete suggestions
- Type safety when using TypeScript - Type safety when using TypeScript
- Clear inheritance hierarchies - Clear inheritance hierarchies
@@ -57,6 +60,7 @@ The storage adapter interface that defines how data is persisted:
- Query capabilities - Query capabilities
Available implementations: Available implementations:
- **StoreInMemory**: Volatile storage for testing and development - **StoreInMemory**: Volatile storage for testing and development
- **StoreIndexedDB**: Browser-based persistent storage - **StoreIndexedDB**: Browser-based persistent storage
- **StoreSQLite**: Native application storage solution - **StoreSQLite**: Native application storage solution
@@ -71,6 +75,7 @@ Defines the interface for interacting with the BCH network:
- Network status management - Network status management
Available implementations: Available implementations:
- **BlockchainInMemory**: Mock blockchain for testing - **BlockchainInMemory**: Mock blockchain for testing
- **BlockchainElectrum**: Production-ready Electrum protocol client - **BlockchainElectrum**: Production-ready Electrum protocol client
@@ -84,6 +89,7 @@ Represents different wallet types for managing BCH:
- Balance tracking - Balance tracking
Available implementations: Available implementations:
- **WalletP2PKH**: Basic Pay-to-Public-Key-Hash wallet - **WalletP2PKH**: Basic Pay-to-Public-Key-Hash wallet
- **WalletHD**: Hierarchical Deterministic wallet implementation - **WalletHD**: Hierarchical Deterministic wallet implementation
- **WalletXO**: Extended HD wallet with XO-specific features - **WalletXO**: Extended HD wallet with XO-specific features
+1
View File
@@ -78,6 +78,7 @@ Note that these are still unstable and will change in future.
:toFunctions="encrypter.to" :toFunctions="encrypter.to"
:methods="encrypter.methods" :methods="encrypter.methods"
/> />
</div> </div>
## Hasher ## Hasher
+42 -29
View File
@@ -18,11 +18,13 @@
**XO** **XO**
- Object-Oriented approach - Object-Oriented approach
- Built on-top of LibAuth. - Built on-top of LibAuth.
- Declarative-style API (for ease of use) - Declarative-style API (for ease of use)
- Adapter-based (can swap in/out blockchain adapters, storage adapters, etc). - Adapter-based (can swap in/out blockchain adapters, storage adapters, etc).
- Layered packages: - Layered packages:
1. @xocash/primitives 1. @xocash/primitives
Network-naive layer offering common BCH primitives (Address, HDNodes, PrivateKey, PublicKey, etc). Network-naive layer offering common BCH primitives (Address, HDNodes, PrivateKey, PublicKey, etc).
@@ -116,13 +118,14 @@ This is still in flux, but we are anticipating (at least) three packages/layers:
outputs: [ outputs: [
{ {
lockingBytecode: Address.from('someAddress').toLockscriptBytes(), lockingBytecode: Address.from('someAddress').toLockscriptBytes(),
valueSatoshis: 20000n valueSatoshis: 20000n,
} },
] ],
}); });
``` ```
Tree-shaking is a big down-side of this in the sense that currently (with the exception of Google's `clojure`) no bundlers can shake unused class methods. However, apparently this is possible so **may** change in future: https://github.com/rollup/rollup/issues/349#issuecomment-2560719286 Tree-shaking is a big down-side of this in the sense that currently (with the exception of Google's `clojure`) no bundlers can shake unused class methods. However, apparently this is possible so **may** change in future: https://github.com/rollup/rollup/issues/349#issuecomment-2560719286
</details> </details>
## Wallets and Wallet-Set/Root-Seed/Account ## Wallets and Wallet-Set/Root-Seed/Account
@@ -174,7 +177,7 @@ To minimize the amount of data necessary for recovery and maintain backwards com
The below is very rough (I'm not suggesting this as a practical implementation, but just to illustrate the approach vaguely). The below is very rough (I'm not suggesting this as a practical implementation, but just to illustrate the approach vaguely).
| Key | Derivation (Rough Example) | Comments | | Key | Derivation (Rough Example) | Comments |
|------------------------|------------------------------------------------------------------|---------------------------------------| | ---------------------- | ---------------------------------------------------------------- | ------------------------------------- |
| Default HD Wallet | Root Seed -> m/44'/145'/0' | Backwards compat with current Wallets | | Default HD Wallet | Root Seed -> m/44'/145'/0' | Backwards compat with current Wallets |
| WalletId | hash256(hash256(rootSeed) + hash256('walletId')) | Identifier for Wallet | | WalletId | hash256(hash256(rootSeed) + hash256('walletId')) | Identifier for Wallet |
| EncryptionKey | hash256(hash256(rootSeed) + hash256('encryptionKey')) | Encrpytion Key for Activities | | EncryptionKey | hash256(hash256(rootSeed) + hash256('encryptionKey')) | Encrpytion Key for Activities |
@@ -348,6 +351,7 @@ For XO, we want to try to make it so that:
TODO: Do we encrypt Activity IDs (UUIDV7)? Might be overkill. Also, incurrs additional compute + bandwidth costs (+28 bytes per UUIDV7 due to AES-GCM header). TODO: Do we encrypt Activity IDs (UUIDV7)? Might be overkill. Also, incurrs additional compute + bandwidth costs (+28 bytes per UUIDV7 due to AES-GCM header).
There's also the problem that, when syncing from an encrypted remote, we would have to ensure we encrypt using the same AES-GCM IV, otherwise the values we generate locally for set-comparison would not be identical to the encrypted remote's values. There's also the problem that, when syncing from an encrypted remote, we would have to ensure we encrypt using the same AES-GCM IV, otherwise the values we generate locally for set-comparison would not be identical to the encrypted remote's values.
</details> </details>
In the structure being considered currently, we index each activity using a UUIDV7 (which contains a timestamp). In the structure being considered currently, we index each activity using a UUIDV7 (which contains a timestamp).
@@ -356,6 +360,7 @@ In the structure being considered currently, we index each activity using a UUID
<summary>Why UUIDV7?</summary> <summary>Why UUIDV7?</summary>
The original LibAuth proposal suggests using a millisecond timestamp as index (appended by entity), but we probably need more granularity as several Activities might occur on the same millisecond timestamp. UUIDV7 itself contains a timestamp (and is lexicographically sortable by timestamp), but then attaches additional randomness to enforce uniqueness. The original LibAuth proposal suggests using a millisecond timestamp as index (appended by entity), but we probably need more granularity as several Activities might occur on the same millisecond timestamp. UUIDV7 itself contains a timestamp (and is lexicographically sortable by timestamp), but then attaches additional randomness to enforce uniqueness.
</details> </details>
```ts ```ts
@@ -364,7 +369,6 @@ export type Activities = { [uuidv7: string]: Activity };
... where the payload Activity itself looks something like this: ... where the payload Activity itself looks something like this:
```ts ```ts
export type Activity<Type extends string, ValueType> = { export type Activity<Type extends string, ValueType> = {
// NOTE: Timestamp will be automatically generated if not provided. // NOTE: Timestamp will be automatically generated if not provided.
@@ -391,8 +395,14 @@ export type TransactionMemo = Activity<'TransactionMemo', string>;
export type XOWalletColor = Activity<'XOWalletColor', string>; export type XOWalletColor = Activity<'XOWalletColor', string>;
export type XOTransactionCategory = Activity<'XOTransactionCategory', string>; export type XOTransactionCategory = Activity<'XOTransactionCategory', string>;
export type XOFrozenCoin = Activity<'XOFrozenCoin', boolean>; export type XOFrozenCoin = Activity<'XOFrozenCoin', boolean>;
export type XOTemplateAction = Activity<'XOTemplateAction', TemplateActionPayload>; export type XOTemplateAction = Activity<
export type XOTemplateActionResult = Activity<'XOTemplateActionResult', TemplateActionResultPayload>; 'XOTemplateAction',
TemplateActionPayload
>;
export type XOTemplateActionResult = Activity<
'XOTemplateActionResult',
TemplateActionResultPayload
>;
``` ```
<details> <details>
@@ -403,22 +413,26 @@ export type XOTemplateActionResult = Activity<'XOTemplateActionResult', Template
For example, if we had a structure like so: For example, if we had a structure like so:
```ts ```ts
export type WalletDetails = Activity<'WalletDetails', { export type WalletDetails = Activity<
'WalletDetails',
{
name: string; name: string;
description: string description: string;
}>; }
>;
``` ```
... it can open us up to a scenario like the following: ... it can open us up to a scenario like the following:
1. *Wallet A* and *Wallet B* are not connected to each other. 1. _Wallet A_ and _Wallet B_ are not connected to each other.
2. *Wallet A* creates a `WalletDetails` activity and sets the name to "New Wallet Name", while not modifying the `description`. 2. _Wallet A_ creates a `WalletDetails` activity and sets the name to "New Wallet Name", while not modifying the `description`.
3. *Wallet B* creates a `WalletDetails` activity and sets the description to "New Wallet Description", while not modifying the `name`. 3. _Wallet B_ creates a `WalletDetails` activity and sets the description to "New Wallet Description", while not modifying the `name`.
4. When we sync these devices under a "latest wins" policy for `WalletDetails`, there will be data loss: We will prioritize the latest WalletDetails activity (*Wallet B*), which means that the name set by Wallet A would be lost as the `WalletDetails` structure of *Wallet B* would take precedence over that of *Wallet A*. 4. When we sync these devices under a "latest wins" policy for `WalletDetails`, there will be data loss: We will prioritize the latest WalletDetails activity (_Wallet B_), which means that the name set by Wallet A would be lost as the `WalletDetails` structure of _Wallet B_ would take precedence over that of _Wallet A_.
The unfortunate trade-off here is that: we end up with more activities and syncing becomes more expensive. However, by grouping by what might mutate, we end up with much better consistency/reliability. The unfortunate trade-off here is that: we end up with more activities and syncing becomes more expensive. However, by grouping by what might mutate, we end up with much better consistency/reliability.
Immutable data (e.g. something like a `XOTemplateAction`) can obviously have more complex Activity payloads as we would never modify "just a part" of that. Immutable data (e.g. something like a `XOTemplateAction`) can obviously have more complex Activity payloads as we would never modify "just a part" of that.
</details> </details>
Like the LibAuth proposal, the aim is that state will become "eventually consistent". We do this by syncing these individual activities between devices and then each device compiles a "snapshot" of the final state. Like the LibAuth proposal, the aim is that state will become "eventually consistent". We do this by syncing these individual activities between devices and then each device compiles a "snapshot" of the final state.
@@ -520,7 +534,9 @@ This might also yield other performance improvements when compiling snapshots in
```ts ```ts
// Pseudo-code for clarity. // Pseudo-code for clarity.
const relevantActivitiesToBuildSnapshotWith = activities.filter(activity => activity !== null); const relevantActivitiesToBuildSnapshotWith = activities.filter(
(activity) => activity !== null,
);
``` ```
#### (Maybe) Using enums to represent "type" #### (Maybe) Using enums to represent "type"
@@ -632,44 +648,41 @@ Example of interface for an Action (still WIP):
```ts ```ts
interface Action { interface Action {
// Human-readble name // Human-readble name
name: string, name: string;
// Human-readable description // Human-readable description
description: string, description: string;
// A list of transaction shapes that this action will execute sequentially. // A list of transaction shapes that this action will execute sequentially.
// NOTE: We need the capability to execute multiple transactions in an action so that: // NOTE: We need the capability to execute multiple transactions in an action so that:
// 1. We can create minting transactions required for token genesis. // 1. We can create minting transactions required for token genesis.
// 2. We can create "consolidation transactions" so that subsequent shapes that impose constraints on input counts, etc, will not have additional UTXOs appended. // 2. We can create "consolidation transactions" so that subsequent shapes that impose constraints on input counts, etc, will not have additional UTXOs appended.
// 3. Some contracts will simply require multi-tx flows for a given action. // 3. Some contracts will simply require multi-tx flows for a given action.
transactions: Array<TransactionShape>, transactions: Array<TransactionShape>;
// A list of "data" that should be returned to the Dapp. // A list of "data" that should be returned to the Dapp.
// This represents the "state" that the Dapp (or Wallet) MUST store. // This represents the "state" that the Dapp (or Wallet) MUST store.
// The format is name (or alias) to a CashASM evaluation. Using a CashASM over raw variable names gives us flexibility in that: // The format is name (or alias) to a CashASM evaluation. Using a CashASM over raw variable names gives us flexibility in that:
// 1. We can conceal values derived by the wallet from the consuming service. // 1. We can conceal values derived by the wallet from the consuming service.
// 2. In future, format values for direct consumption by a follow-up action (e.g. imagine a generic settlement service) // 2. In future, format values for direct consumption by a follow-up action (e.g. imagine a generic settlement service)
// 3. In future, "transactions" may be made optional so that that an action can be used to just collect data (e.g. collect pubKeys for multisig setup or collect pubkeys/payout address for AnyHedge P2P Setup). TODO: Go into more detail about this. // 3. In future, "transactions" may be made optional so that that an action can be used to just collect data (e.g. collect pubKeys for multisig setup or collect pubkeys/payout address for AnyHedge P2P Setup). TODO: Go into more detail about this.
resolves: { [name: string]: CashASM } resolves: { [name: string]: CashASM };
} }
``` ```
Actions are then invoked something like: Actions are then invoked something like:
```ts ```ts
const createOfferResult = await wallet.executeAction( const createOfferResult = await wallet.executeAction(BCHGURU_TEMPLATE, {
BCHGURU_TEMPLATE,
{
// The action from the template to invoke. // The action from the template to invoke.
action: 'bch_createOffer', action: 'bch_createOffer',
// The data to pass into this action. // The data to pass into this action.
// NOTE: In practice, these will need to be converted to Uint8Array. An 'encodeData' util function will be provided to help. // NOTE: In practice, these will need to be converted to Uint8Array. An 'encodeData' util function will be provided to help.
data: { data: {
oraclePublicKey: '0x02d09db08af1ff4e8453919cc866a4be427d7bfe18f2c05e5444c196fcf6fd2818', oraclePublicKey:
'0x02d09db08af1ff4e8453919cc866a4be427d7bfe18f2c05e5444c196fcf6fd2818',
player1Prediction: 100_000_000, player1Prediction: 100_000_000,
betAmount: 250_000, betAmount: 250_000,
// ... // ...
} },
} });
);
``` ```
The templates themselves are quite similar to LibAuth. It might be better to link to this document as many of the concepts/ideas will likely carry across to XO. The templates themselves are quite similar to LibAuth. It might be better to link to this document as many of the concepts/ideas will likely carry across to XO.
@@ -689,7 +702,7 @@ const domainSandboxSeed = sha256(sha256(rootSeed) + sha256(domain));
For CashConnect, the intent of this was to: For CashConnect, the intent of this was to:
1. Allow recovery from root-seed only (this is not perfect, but if a user re-visits a domain, they will be able to recover their key associated with that domain). 1. Allow recovery from root-seed only (this is not perfect, but if a user re-visits a domain, they will be able to recover their key associated with that domain).
- *The assumption of CashConnect was that the Wallet did not have the capability to store any P2SH data (this responsibility is relegated to the Dapp/Service).* - _The assumption of CashConnect was that the Wallet did not have the capability to store any P2SH data (this responsibility is relegated to the Dapp/Service)._
2. Give the domain a permissable sandbox account ("key") it could use for construction of actions 2. Give the domain a permissable sandbox account ("key") it could use for construction of actions
- Create transactions that consolidate amounts for follow-up transactions - Create transactions that consolidate amounts for follow-up transactions
- Create Token Genesis transactions and - Create Token Genesis transactions and
@@ -710,7 +723,7 @@ For CashConnect, the intent of this was to:
}, },
keys: { keys: {
privateKeys: { privateKeys: {
sandboxKey: theDerivedKeyForThisDappOrTemplate sandboxKey: theDerivedKeyForThisDappOrTemplate,
}, },
}, },
}; };
+6 -3
View File
@@ -209,7 +209,7 @@ export class Address {
type: 'P2SH20' | 'P2SH32' = 'P2SH32', type: 'P2SH20' | 'P2SH32' = 'P2SH32',
): Address { ): Address {
// Return a new instance of address. // Return a new instance of address.
return this.fromRedeemScript(Bytes.fromHex(hex).toBytes(), type); return this.fromRedeemScript(Bytes.fromHex(hex).toUint8Array(), type);
} }
/** /**
@@ -257,7 +257,7 @@ export class Address {
* @returns {Address} Instance of Address. * @returns {Address} Instance of Address.
*/ */
public static fromLockscriptHex(hex: string): Address { public static fromLockscriptHex(hex: string): Address {
return this.fromLockscriptBytes(Bytes.fromHex(hex).toBytes()); return this.fromLockscriptBytes(Bytes.fromHex(hex).toUint8Array());
} }
/** /**
@@ -298,7 +298,10 @@ export class Address {
hash160Hex: string, hash160Hex: string,
type: AddressType = 'P2PKH', type: AddressType = 'P2PKH',
): Address { ): Address {
return this.fromHash160Bytes(Bytes.fromHex(hash160Hex).toBytes(), type); return this.fromHash160Bytes(
Bytes.fromHex(hash160Hex).toUint8Array(),
type,
);
} }
/** /**
+3 -3
View File
@@ -73,7 +73,7 @@ export class BlockHeader {
* @throws Error if the hex string length is not 160 * @throws Error if the hex string length is not 160
*/ */
static fromHex(hex: string): BlockHeader { static fromHex(hex: string): BlockHeader {
return this.fromBytes(Bytes.fromHex(hex).toBytes()); return this.fromBytes(Bytes.fromHex(hex).toUint8Array());
} }
/** /**
@@ -119,7 +119,7 @@ export class BlockHeader {
* @returns 160-character hex string representing the block header * @returns 160-character hex string representing the block header
*/ */
toHex(): string { toHex(): string {
return Bytes.fromBytes(this.toBytes()).toHex(); return Bytes.fromBytes(this.bytes).toHex();
} }
/** /**
@@ -207,7 +207,7 @@ export class BlockHeader {
* @returns 32-byte array containing the block hash * @returns 32-byte array containing the block hash
*/ */
getHash(): Uint8Array { getHash(): Uint8Array {
return hash256(this.toBytes()).toReversed(); return hash256(this.bytes).toReversed();
} }
/** /**
+15 -11
View File
@@ -15,16 +15,19 @@ import {
/** /**
* Bytes Primitive. * Bytes Primitive.
*/ */
export class Bytes { export class Bytes extends Uint8Array {
protected readonly bytes: Uint8Array;
/** /**
* Instantiate Bytes from a Uint8Array. * Instantiate Bytes from a Uint8Array.
* *
* @param bytes {Uint8Array} The bytes to use. * @param bytes {Uint8Array} The bytes to use.
*/ */
protected constructor(bytes: Uint8Array) { protected constructor(bytes: Uint8Array) {
this.bytes = bytes; super(bytes);
}
// Override the constructor to ensure correct inheritance
static from(uint8Array: Uint8Array): Bytes {
return new Bytes(uint8Array);
} }
/** /**
@@ -119,11 +122,11 @@ export class Bytes {
* @returns Bytes as a Base64 string. * @returns Bytes as a Base64 string.
*/ */
public toBase64(): string { public toBase64(): string {
return binToBase64(this.bytes); return binToBase64(this);
} }
public toBigInt(): bigint { public toBigInt(): bigint {
return binToBigIntUintLE(this.bytes); return binToBigIntUintLE(this);
} }
/** /**
@@ -132,7 +135,7 @@ export class Bytes {
* @returns Bytes as a hex string. * @returns Bytes as a hex string.
*/ */
public toHex(): string { public toHex(): string {
return binToHex(this.bytes); return binToHex(this);
} }
/** /**
@@ -141,7 +144,7 @@ export class Bytes {
* @returns Bytes as a UTF8 string. * @returns Bytes as a UTF8 string.
*/ */
public toUtf8(): string { public toUtf8(): string {
return binToUtf8(this.bytes); return binToUtf8(this);
} }
/** /**
@@ -149,11 +152,12 @@ export class Bytes {
* *
* @returns Bytes as a Uint8Array. * @returns Bytes as a Uint8Array.
*/ */
public toBytes(): Uint8Array { public toUint8Array(): Uint8Array {
return this.bytes; // NOTE: We clone the instance of Uint8Array so that we don't accidentally mutate it.
return new Uint8Array([...this]);
} }
public invert(): Bytes { public invert(): Bytes {
return new Bytes(Uint8Array.from([...this.bytes].reverse())); return new Bytes(Uint8Array.from([...this].reverse()));
} }
} }
+1 -1
View File
@@ -66,7 +66,7 @@ export class HDPrivateNode {
public static fromSeed(seed: Uint8Array | string): HDPrivateNode { public static fromSeed(seed: Uint8Array | string): HDPrivateNode {
// Cast from hex if a string was provided. // Cast from hex if a string was provided.
if (typeof seed === 'string') { if (typeof seed === 'string') {
seed = Bytes.fromHex(seed).toBytes(); seed = Bytes.fromHex(seed);
} }
// If the seed is not 64 bytes, throw an error. // If the seed is not 64 bytes, throw an error.
+11 -13
View File
@@ -98,7 +98,7 @@ export class PrivateKey {
*/ */
public static fromHex(privateKeyHex: string): PrivateKey { public static fromHex(privateKeyHex: string): PrivateKey {
// Convert the hex to binary (Uint8Array). // Convert the hex to binary (Uint8Array).
const privateKeyBin = Bytes.fromHex(privateKeyHex).toBytes(); const privateKeyBin = Bytes.fromHex(privateKeyHex);
// Create a Private Key Entity from the binary. // Create a Private Key Entity from the binary.
return new this(privateKeyBin); return new this(privateKeyBin);
@@ -268,7 +268,7 @@ export class PrivateKey {
*/ */
public signMessageRecoverableCompact(message: Uint8Array | string) { public signMessageRecoverableCompact(message: Uint8Array | string) {
if (typeof message === 'string') { if (typeof message === 'string') {
message = Bytes.fromUtf8(message).toBytes(); message = Bytes.fromUtf8(message);
} }
const messageHash = sha256.hash(message); const messageHash = sha256.hash(message);
@@ -292,7 +292,7 @@ export class PrivateKey {
*/ */
public signMessageCompact(message: Uint8Array | string) { public signMessageCompact(message: Uint8Array | string) {
if (typeof message === 'string') { if (typeof message === 'string') {
message = Bytes.fromUtf8(message).toBytes(); message = Bytes.fromUtf8(message);
} }
const messageHash = sha256.hash(message); const messageHash = sha256.hash(message);
@@ -316,7 +316,7 @@ export class PrivateKey {
*/ */
public signMessageSchnorr(message: Uint8Array | string) { public signMessageSchnorr(message: Uint8Array | string) {
if (typeof message === 'string') { if (typeof message === 'string') {
message = Bytes.fromUtf8(message).toBytes(); message = Bytes.fromUtf8(message);
} }
const messageHash = sha256.hash(message); const messageHash = sha256.hash(message);
@@ -340,7 +340,7 @@ export class PrivateKey {
*/ */
public signMessageDER(message: Uint8Array | string) { public signMessageDER(message: Uint8Array | string) {
if (typeof message === 'string') { if (typeof message === 'string') {
message = Bytes.fromUtf8(message).toBytes(); message = Bytes.fromUtf8(message);
} }
const messageHash = sha256.hash(message); const messageHash = sha256.hash(message);
@@ -363,10 +363,8 @@ export class PrivateKey {
*/ */
public signMessageElectron(message: string): Uint8Array { public signMessageElectron(message: string): Uint8Array {
// Compile the message. // Compile the message.
const magicBytes = Bytes.fromUtf8( const magicBytes = Bytes.fromUtf8('\x18Bitcoin Signed Message:\n');
'\x18Bitcoin Signed Message:\n', const messageBytes = Bytes.fromUtf8(message);
).toBytes();
const messageBytes = Bytes.fromUtf8(message).toBytes();
const messageLengthBytes = bigIntToCompactUint(BigInt(messageBytes.length)); // hexToBin(messageBytes.length.toString(16)); // const messageLengthBytes = bigIntToCompactUint(BigInt(messageBytes.length)); // hexToBin(messageBytes.length.toString(16)); //
const messageFinal = new Uint8Array([ const messageFinal = new Uint8Array([
...magicBytes, ...magicBytes,
@@ -406,7 +404,7 @@ export class PrivateKey {
// If a string was given, convert it from hex to bytes. // If a string was given, convert it from hex to bytes.
// TODO: Consider using Bytes class - it ensures valid hex. // TODO: Consider using Bytes class - it ensures valid hex.
if (typeof messageHash === 'string') { if (typeof messageHash === 'string') {
messageHash = Bytes.fromHex(messageHash).toBytes(); messageHash = Bytes.fromHex(messageHash);
} }
if (messageHash.length !== 32) { if (messageHash.length !== 32) {
@@ -441,7 +439,7 @@ export class PrivateKey {
// If a string was given, convert it from hex to bytes. // If a string was given, convert it from hex to bytes.
// TODO: Consider using Bytes class - it ensures valid hex. // TODO: Consider using Bytes class - it ensures valid hex.
if (typeof messageHash === 'string') { if (typeof messageHash === 'string') {
messageHash = Bytes.fromHex(messageHash).toBytes(); messageHash = Bytes.fromHex(messageHash);
} }
if (messageHash.length !== 32) { if (messageHash.length !== 32) {
@@ -476,7 +474,7 @@ export class PrivateKey {
// If a string was given, convert it from hex to bytes. // If a string was given, convert it from hex to bytes.
// TODO: Consider using Bytes class - it ensures valid hex. // TODO: Consider using Bytes class - it ensures valid hex.
if (typeof messageHash === 'string') { if (typeof messageHash === 'string') {
messageHash = Bytes.fromHex(messageHash).toBytes(); messageHash = Bytes.fromHex(messageHash);
} }
if (messageHash.length !== 32) { if (messageHash.length !== 32) {
@@ -511,7 +509,7 @@ export class PrivateKey {
// If a string was given, convert it from hex to bytes. // If a string was given, convert it from hex to bytes.
// TODO: Consider using Bytes class - it ensures valid hex. // TODO: Consider using Bytes class - it ensures valid hex.
if (typeof messageHash === 'string') { if (typeof messageHash === 'string') {
messageHash = Bytes.fromHex(messageHash).toBytes(); messageHash = Bytes.fromHex(messageHash);
} }
if (messageHash.length !== 32) { if (messageHash.length !== 32) {
+5 -5
View File
@@ -78,7 +78,7 @@ export class PublicKey {
* @returns The created Public Key entity. * @returns The created Public Key entity.
*/ */
public static fromHex(publicKeyHex: string): PublicKey { public static fromHex(publicKeyHex: string): PublicKey {
return new this(Bytes.fromHex(publicKeyHex).toBytes()); return new this(Bytes.fromHex(publicKeyHex));
} }
/** /**
@@ -232,11 +232,11 @@ export class PublicKey {
signature: Uint8Array | string, signature: Uint8Array | string,
): boolean { ): boolean {
if (typeof messageHash === 'string') { if (typeof messageHash === 'string') {
messageHash = Bytes.fromHex(messageHash).toBytes(); messageHash = Bytes.fromHex(messageHash);
} }
if (typeof signature === 'string') { if (typeof signature === 'string') {
signature = Bytes.fromHex(signature).toBytes(); signature = Bytes.fromHex(signature);
} }
const validity = secp256k1.verifySignatureDER( const validity = secp256k1.verifySignatureDER(
@@ -261,11 +261,11 @@ export class PublicKey {
signature: Uint8Array | string, signature: Uint8Array | string,
): boolean { ): boolean {
if (typeof messageHash === 'string') { if (typeof messageHash === 'string') {
messageHash = Bytes.fromHex(messageHash).toBytes(); messageHash = Bytes.fromHex(messageHash);
} }
if (typeof signature === 'string') { if (typeof signature === 'string') {
signature = Bytes.fromHex(signature).toBytes(); signature = Bytes.fromHex(signature);
} }
const validity = secp256k1.verifySignatureSchnorr( const validity = secp256k1.verifySignatureSchnorr(
+2 -2
View File
@@ -24,7 +24,7 @@ import {
* console.log(script.toHex()); * console.log(script.toHex());
* *
* // Output as bytecode. * // Output as bytecode.
* console.log(script.toBytes()); * console.log(script);
* ``` * ```
*/ */
export class Script { export class Script {
@@ -177,7 +177,7 @@ export class Script {
*/ */
public static fromHex(hex: string): Script { public static fromHex(hex: string): Script {
// Return new Script entity constructed from the binary. // Return new Script entity constructed from the binary.
return this.fromBytes(Bytes.fromHex(hex).toBytes()); return this.fromBytes(Bytes.fromHex(hex));
} }
/** /**
+4 -4
View File
@@ -212,7 +212,7 @@ export class Transaction extends BaseTransaction {
* @returns {Transaction} The created Transaction Primitive. * @returns {Transaction} The created Transaction Primitive.
*/ */
public static fromHex(hex: string): Transaction { public static fromHex(hex: string): Transaction {
const bytes = Bytes.fromHex(hex).toBytes(); const bytes = Bytes.fromHex(hex).toUint8Array();
return this.fromBytes(bytes); return this.fromBytes(bytes);
} }
@@ -319,7 +319,7 @@ export class TransactionResolved extends BaseTransaction {
* @returns {Transaction} The created Transaction Primitive. * @returns {Transaction} The created Transaction Primitive.
*/ */
public static fromHex(hex: string, sourceOutputs: Array<any>) { public static fromHex(hex: string, sourceOutputs: Array<any>) {
const bytes = Bytes.fromHex(hex).toBytes(); const bytes = Bytes.fromHex(hex).toUint8Array();
return this.fromBytes(bytes, sourceOutputs); return this.fromBytes(bytes, sourceOutputs);
} }
@@ -425,7 +425,7 @@ export class TransactionHash {
* @returns {TransactionHash} Instance of TransactionHash. * @returns {TransactionHash} Instance of TransactionHash.
*/ */
public static fromHex(hex: string): TransactionHash { public static fromHex(hex: string): TransactionHash {
const bytes = Bytes.fromHex(hex).toBytes(); const bytes = Bytes.fromHex(hex).toUint8Array();
return this.fromBytes(bytes); return this.fromBytes(bytes);
} }
@@ -469,7 +469,7 @@ export class TransactionHash {
* @returns {TransactionHash} Instance of TransactionHash. * @returns {TransactionHash} Instance of TransactionHash.
*/ */
public static fromTransactionHex(transactionHex: string): TransactionHash { public static fromTransactionHex(transactionHex: string): TransactionHash {
const bytes = Bytes.fromHex(transactionHex).toBytes(); const bytes = Bytes.fromHex(transactionHex).toUint8Array();
return this.fromTransactionBytes(bytes); return this.fromTransactionBytes(bytes);
} }
+5 -5
View File
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { Address } from '~/src/address'; import { Address } from '../src/address';
import { Bytes } from '~/src/bytes'; import { Bytes } from '../src/bytes';
describe('Success cases', () => { describe('Success cases', () => {
// Test cases with different address representations // Test cases with different address representations
@@ -27,7 +27,7 @@ describe('Success cases', () => {
type: 'P2PKH', type: 'P2PKH',
payload: Bytes.fromHex( payload: Bytes.fromHex(
'626025854e5a67ca5eb122e213f409ec842d45e4', '626025854e5a67ca5eb122e213f409ec842d45e4',
).toBytes(), ).toUint8Array(),
}, },
representations: { representations: {
cashAddr: 'bitcoincash:qp3xqfv9fedx0jj7ky3wyyl5p8kggt29usvlhrrxws', cashAddr: 'bitcoincash:qp3xqfv9fedx0jj7ky3wyyl5p8kggt29usvlhrrxws',
@@ -35,13 +35,13 @@ describe('Success cases', () => {
tokenAddr: 'bitcoincash:zp3xqfv9fedx0jj7ky3wyyl5p8kggt29ust4yadq3r', tokenAddr: 'bitcoincash:zp3xqfv9fedx0jj7ky3wyyl5p8kggt29ust4yadq3r',
lockscriptBytes: Bytes.fromHex( lockscriptBytes: Bytes.fromHex(
'76a914626025854e5a67ca5eb122e213f409ec842d45e488ac', '76a914626025854e5a67ca5eb122e213f409ec842d45e488ac',
).toBytes(), ).toUint8Array(),
lockscriptHex: '76a914626025854e5a67ca5eb122e213f409ec842d45e488ac', lockscriptHex: '76a914626025854e5a67ca5eb122e213f409ec842d45e488ac',
lockscriptASM: lockscriptASM:
'OP_DUP OP_HASH160 OP_PUSHBYTES_20 0x626025854e5a67ca5eb122e213f409ec842d45e4 OP_EQUALVERIFY OP_CHECKSIG', 'OP_DUP OP_HASH160 OP_PUSHBYTES_20 0x626025854e5a67ca5eb122e213f409ec842d45e4 OP_EQUALVERIFY OP_CHECKSIG',
hash160Bytes: Bytes.fromHex( hash160Bytes: Bytes.fromHex(
'626025854e5a67ca5eb122e213f409ec842d45e4', '626025854e5a67ca5eb122e213f409ec842d45e4',
).toBytes(), ).toUint8Array(),
hash160Hex: '626025854e5a67ca5eb122e213f409ec842d45e4', hash160Hex: '626025854e5a67ca5eb122e213f409ec842d45e4',
}, },
}, },
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { BlockHeader } from '~/src/block-header'; import { BlockHeader } from '../src/block-header';
import { Bytes } from '~/src/bytes'; import { Bytes } from '../src/bytes';
describe('Success cases', () => { describe('Success cases', () => {
// Test cases with different block header representations // Test cases with different block header representations
@@ -16,11 +16,11 @@ describe('Success cases', () => {
{ {
raw: Bytes.fromHex( raw: Bytes.fromHex(
'0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d', '0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d',
).toBytes(), ).toUint8Array(),
representations: { representations: {
bytes: Bytes.fromHex( bytes: Bytes.fromHex(
'0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d', '0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d',
).toBytes(), ).toUint8Array(),
hex: '0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d', hex: '0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d',
}, },
}, },
+2 -2
View File
@@ -51,7 +51,7 @@ describe('Success cases', () => {
}, },
toSeed: Bytes.fromHex( toSeed: Bytes.fromHex(
'7066f43a742829f0edce7d8e3c31651df320f13913995a6500f4e515db372b35202227f923ff181defc35cea02f8468df39f8fa9d97a202d252e61562e4bbfb5', '7066f43a742829f0edce7d8e3c31651df320f13913995a6500f4e515db372b35202227f923ff181defc35cea02f8468df39f8fa9d97a202d252e61562e4bbfb5',
).toBytes(), ).toUint8Array(),
toPhrase: toPhrase:
'goose just fan kidney barrel album wave crowd movie flame cousin exhaust', 'goose just fan kidney barrel album wave crowd movie flame cousin exhaust',
toPhraseWithoutPassphrase: toPhraseWithoutPassphrase:
@@ -124,7 +124,7 @@ describe('Success cases', () => {
}, },
toSeed: Bytes.fromHex( toSeed: Bytes.fromHex(
'2f706756ab2083f7ce07c0c0fcde8f14f062da5de839f42763262b25e0d871f03dd2b561d55eaacc471c94da656ee7b0007fb48a64e31ec91a1a2ba706b46b89', '2f706756ab2083f7ce07c0c0fcde8f14f062da5de839f42763262b25e0d871f03dd2b561d55eaacc471c94da656ee7b0007fb48a64e31ec91a1a2ba706b46b89',
).toBytes(), ).toUint8Array(),
toPhrase: toPhrase:
'goose just fan kidney barrel album wave crowd movie flame cousin exhaust GeneralProtocols!', 'goose just fan kidney barrel album wave crowd movie flame cousin exhaust GeneralProtocols!',
toPhraseWithoutPassphrase: toPhraseWithoutPassphrase:
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { Satoshis } from '~/src/satoshis'; import { Satoshis } from '../src/satoshis';
describe('Success cases', () => { describe('Success cases', () => {
// Test cases with single representation for each unit // Test cases with single representation for each unit
+7 -7
View File
@@ -17,11 +17,11 @@ describe('Success cases', () => {
{ {
raw: Bytes.fromHex( raw: Bytes.fromHex(
'76a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac', '76a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac',
).toBytes(), ).toUint8Array(),
representations: { representations: {
bytes: Bytes.fromHex( bytes: Bytes.fromHex(
'76a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac', '76a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac',
).toBytes(), ).toUint8Array(),
asm: 'OP_DUP\nOP_HASH160\nOP_PUSHBYTES_20\n0x876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d6\nOP_EQUALVERIFY\nOP_CHECKSIG', asm: 'OP_DUP\nOP_HASH160\nOP_PUSHBYTES_20\n0x876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d6\nOP_EQUALVERIFY\nOP_CHECKSIG',
hex: '76a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac', hex: '76a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac',
}, },
@@ -38,19 +38,19 @@ describe('Success cases', () => {
fromMethods.forEach(({ name, key }) => { fromMethods.forEach(({ name, key }) => {
it.each(testCases)(`${name}()`, ({ raw, representations }) => { it.each(testCases)(`${name}()`, ({ raw, representations }) => {
const script = Script[name](representations[key]); const script = Script[name](representations[key]);
expect(script.toRaw()).toStrictEqual(raw); expect(script.toRaw()).toEqual(raw);
}); });
}); });
// Test generic from method // Test generic from method
it.each(testCases)('from(Uint8Array)', ({ raw, representations }) => { it.each(testCases)('from(Uint8Array)', ({ raw, representations }) => {
const script = Script.from(representations.bytes); const script = Script.from(representations.bytes);
expect(script.toRaw()).toStrictEqual(raw); expect(script.toRaw()).toEqual(raw);
}); });
it.each(testCases)('from(string)', ({ raw, representations }) => { it.each(testCases)('from(string)', ({ raw, representations }) => {
const script = Script.from(representations.hex); const script = Script.from(representations.hex);
expect(script.toRaw()).toStrictEqual(raw); expect(script.toRaw()).toEqual(raw);
}); });
// Test to methods with their respective types // Test to methods with their respective types
@@ -63,14 +63,14 @@ describe('Success cases', () => {
toMethods.forEach(({ name, key }) => { toMethods.forEach(({ name, key }) => {
it.each(testCases)(`${name}()`, ({ raw, representations }) => { it.each(testCases)(`${name}()`, ({ raw, representations }) => {
const script = Script.fromRaw(raw); const script = Script.fromRaw(raw);
expect(script[name]()).toStrictEqual(representations[key]); expect(script[name]()).toEqual(representations[key]);
}); });
}); });
// Test fromRaw specifically // Test fromRaw specifically
it.each(testCases)('fromRaw() creates correct instance', ({ raw }) => { it.each(testCases)('fromRaw() creates correct instance', ({ raw }) => {
const script = Script.fromRaw(raw); const script = Script.fromRaw(raw);
expect(script.toRaw()).toStrictEqual(raw); expect(script.toRaw()).toEqual(raw);
}); });
}); });
+3 -1
View File
@@ -157,7 +157,9 @@ export class SSEClient {
this.messageBuffer = new Uint8Array([...this.messageBuffer, ...chunk]); this.messageBuffer = new Uint8Array([...this.messageBuffer, ...chunk]);
const events: SSEvent[] = []; const events: SSEvent[] = [];
const lines = this.textDecoder.decode(this.messageBuffer).split(/\r\n|\r|\n/); const lines = this.textDecoder
.decode(this.messageBuffer)
.split(/\r\n|\r|\n/);
let currentEvent: Partial<SSEvent> = {}; let currentEvent: Partial<SSEvent> = {};
let completeEventCount = 0; let completeEventCount = 0;