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

This commit is contained in:
devperate
2025-04-28 02:49:25 +00:00
committed by Jim Hamill
parent 1d5e3131c8
commit d4398439c7
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 -1
View File
@@ -204,4 +204,4 @@ TODO: When we have docs public, move this back into the top config section above
.link-text-icon { .link-text-icon {
margin-left: 6px; margin-left: 6px;
} }
</style> </style>
+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
+216 -203
View File
@@ -18,26 +18,28 @@
**XO** **XO**
- Object-Oriented approach - Object-Oriented approach
- Built on-top of LibAuth.
- Declarative-style API (for ease of use) - Built on-top of LibAuth.
- Adapter-based (can swap in/out blockchain adapters, storage adapters, etc). - Declarative-style API (for ease of use)
- Adapter-based (can swap in/out blockchain adapters, storage adapters, etc).
- Layered packages: - Layered packages:
1. @xocash/primitives
Network-naive layer offering common BCH primitives (Address, HDNodes, PrivateKey, PublicKey, etc). 1. @xocash/primitives
2. @xocash/stack Network-naive layer offering common BCH primitives (Address, HDNodes, PrivateKey, PublicKey, etc).
Network-aware layer, offering blockchain, storage, wallet and P2P interfaces/adapters. 2. @xocash/stack
3. @xocash/wallet | @xocash/dapp Network-aware layer, offering blockchain, storage, wallet and P2P interfaces/adapters.
Higher-level conveniences over @xocash/stack, providing sensible default adapters for given use-cases. 3. @xocash/wallet | @xocash/dapp
Higher-level conveniences over @xocash/stack, providing sensible default adapters for given use-cases.
- Moderately tree-shakable - Moderately tree-shakable
- Most bundlers cannot tree-shake unused class methods (but may be able to in future). - Most bundlers cannot tree-shake unused class methods (but may be able to in future).
</div> </div>
</div> </div>
@@ -50,79 +52,80 @@ This is still in flux, but we are anticipating (at least) three packages/layers:
1. **@xocash/primitives** 1. **@xocash/primitives**
This is just an OO wrapper over LibAuth to make BCH primitives easier for less technical devs to use. This is just an OO wrapper over LibAuth to make BCH primitives easier for less technical devs to use.
It includes primitives like Address, HDPrivateKey, HDPublicKey, Mnemonic, PrivateKey, PublicKey, etc. It includes primitives like Address, HDPrivateKey, HDPublicKey, Mnemonic, PrivateKey, PublicKey, etc.
Each primitive is immutable. The interfaces here are generally `from`, `to` and `methods` that can be performed upon the Primitive. E.g. Each primitive is immutable. The interfaces here are generally `from`, `to` and `methods` that can be performed upon the Primitive. E.g.
```ts ```ts
// NOTE: Each `from` method is static and throws on invalid input. // NOTE: Each `from` method is static and throws on invalid input.
const cashAddress = Address.fromLegacy('someLegacyAddress').toCashAddr(); const cashAddress = Address.fromLegacy('someLegacyAddress').toCashAddr();
``` ```
These are all network-naive - the aim is that the only package dependency here is LibAuth. These are all network-naive - the aim is that the only package dependency here is LibAuth.
2. **@xocash/stack** 2. **@xocash/stack**
This is a level above primitives that features various network-aware components to build apps with XO. This is a level above primitives that features various network-aware components to build apps with XO.
It includes the modular "building blocks": Blockchain Adapters, Storage Adapters, P2P Adapters (TBD), Wallets and Utils. It includes the modular "building blocks": Blockchain Adapters, Storage Adapters, P2P Adapters (TBD), Wallets and Utils.
These will lean on Dependency Injection patterns so that adapters can be swapped in/out. These will lean on Dependency Injection patterns so that adapters can be swapped in/out.
For example, when instantiating a wallet, the dev can use choose which Blockchain Adapter (e.g. Electrum-Cash, Chaingraph, Local/Memory) they wish to use and which storage adapter (Memory, IndexedDB, SQLite, Mongo, etc) they wish to use - or create their own Adapters provided they satisfy the XO interfaces. For example, when instantiating a wallet, the dev can use choose which Blockchain Adapter (e.g. Electrum-Cash, Chaingraph, Local/Memory) they wish to use and which storage adapter (Memory, IndexedDB, SQLite, Mongo, etc) they wish to use - or create their own Adapters provided they satisfy the XO interfaces.
(NOTE: Some adapters might end up in their own package if they drag in heavy dependencies). (NOTE: Some adapters might end up in their own package if they drag in heavy dependencies).
3. **@xocash/conveniences** (almost definitely won't be named this) 3. **@xocash/conveniences** (almost definitely won't be named this)
This is a higher-level package that just instantiates the above for common scenarios: E.g. Building a Wallet, building a Dapp, etc. This is a higher-level package that just instantiates the above for common scenarios: E.g. Building a Wallet, building a Dapp, etc.
Essentially, the intent is to just provide sensible defaults (adapters) for those common use-cases. Essentially, the intent is to just provide sensible defaults (adapters) for those common use-cases.
(To better manage package dependecies, it's possible that this might end up as an explicit package per use-case: e.g. @xocash/dapp, @xocash/wallet, etc). (To better manage package dependecies, it's possible that this might end up as an explicit package per use-case: e.g. @xocash/dapp, @xocash/wallet, etc).
<details> <details>
<summary>Why the OO approach?</summary> <summary>Why the OO approach?</summary>
The main reasons are for cleaner documentation/IDE-autocomplete and easier DX: We want to try to handle a lot of the state for developers and an OO approach allows us to encapsulate that nicely and provide declarative interfaces to access it. The main reasons are for cleaner documentation/IDE-autocomplete and easier DX: We want to try to handle a lot of the state for developers and an OO approach allows us to encapsulate that nicely and provide declarative interfaces to access it.
For example, instantiation of a HD Wallet might look something like this: For example, instantiation of a HD Wallet might look something like this:
```ts ```ts
// Create an instance of a HDWallet. // Create an instance of a HDWallet.
const walletHD = await WalletHD.from( const walletHD = await WalletHD.from(
// Adapters // Adapters
{ {
blockchain: blockchainElectrum, blockchain: blockchainElectrum,
cache: cachesStore, cache: cachesStore,
// NOTE: This will likely change to "Activities" // NOTE: This will likely change to "Activities"
storage: walletsStore, storage: walletsStore,
}, },
// Settings. // Settings.
// NOTE: Currently, this supports other forms of instantation (e.g. Seed or XPriv). // NOTE: Currently, this supports other forms of instantation (e.g. Seed or XPriv).
// See WalletHDGenesisData: https://gitlab.com/GeneralProtocols/xo/stack/-/blob/development/packages/stack/src/wallet/wallet-hd.ts?ref_type=heads#L43 // See WalletHDGenesisData: https://gitlab.com/GeneralProtocols/xo/stack/-/blob/development/packages/stack/src/wallet/wallet-hd.ts?ref_type=heads#L43
{ {
derivationPath: "m/44'/145'/0'", derivationPath: "m/44'/145'/0'",
mnemonic: mnemonic:
'page pencil stock planet limb cluster assault speak off joke private pioneer', 'page pencil stock planet limb cluster assault speak off joke private pioneer',
},
);
// Send a transaction by providing a tx template.
// NOTE: There's currently a TxBuilder that populates UTXOs to meet the output amounts and append change.
await walletHD.send({
outputs: [
{
lockingBytecode: Address.from('someAddress').toLockscriptBytes(),
valueSatoshis: 20000n,
}, },
); ],
});
```
// Send a transaction by providing a tx template. 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
// NOTE: There's currently a TxBuilder that populates UTXOs to meet the output amounts and append change.
await walletHD.send({
outputs: [
{
lockingBytecode: Address.from('someAddress').toLockscriptBytes(),
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
</details> </details>
## Wallets and Wallet-Set/Root-Seed/Account ## Wallets and Wallet-Set/Root-Seed/Account
@@ -151,19 +154,19 @@ We're considering deviating a bit from the LibAuth approach documented [here](ht
1. Only supporting a single root-seed per WalletSet. 1. Only supporting a single root-seed per WalletSet.
Part of the intent here is to make it so that a user only requires a single detail for Wallet Recovery/Sync on another device (but this also ties in closely with the below items). Part of the intent here is to make it so that a user only requires a single detail for Wallet Recovery/Sync on another device (but this also ties in closely with the below items).
2. Allowing existing HD Wallet mnemonics to used to create a "WalletSet". 2. Allowing existing HD Wallet mnemonics to used to create a "WalletSet".
This is more of a convenience and that it **may** be achievable is actually probably more of a consequence using a single-root seed. This is more of a convenience and that it **may** be achievable is actually probably more of a consequence using a single-root seed.
3. Storing other metadata (e.g. Tx Received Time/Fiat-Amount, Tx Memos, Tx Categories, Frozen Coins, etc) using Activities. 3. Storing other metadata (e.g. Tx Received Time/Fiat-Amount, Tx Memos, Tx Categories, Frozen Coins, etc) using Activities.
Ideally, we want to use "Activities" to store other information about a user's wallet(s), so that other pieces of state can be synced across devices seamlessly. Ideally, we want to use "Activities" to store other information about a user's wallet(s), so that other pieces of state can be synced across devices seamlessly.
4. The ability to backup "Action" (P2SH) data to an external store (or stores) prior to broadcasting to Blockchain. 4. The ability to backup "Action" (P2SH) data to an external store (or stores) prior to broadcasting to Blockchain.
This is to prevent, for example, loss of P2SH Wallet/AddressData in case user loses access to their device: We want to make sure that all data can be recoverable on another device via a root-seed only. This is to prevent, for example, loss of P2SH Wallet/AddressData in case user loses access to their device: We want to make sure that all data can be recoverable on another device via a root-seed only.
5. Storing only the instructions (e.g. path) as to how keys are derived (as opposed to any keys themselves) in Activities. 5. Storing only the instructions (e.g. path) as to how keys are derived (as opposed to any keys themselves) in Activities.
@@ -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 |
@@ -345,9 +348,10 @@ For XO, we want to try to make it so that:
<details> <details>
<summary>On the note of encryption</summary> <summary>On the note of encryption</summary>
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).
@@ -355,7 +359,8 @@ In the structure being considered currently, we index each activity using a UUID
<details> <details>
<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,34 +395,44 @@ 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>
<summary>"Atomic" Activities</summary> <summary>"Atomic" Activities</summary>
For XO, we want to also be able to store additional metadata. A lot of this metadata will be mutable (e.g. `WalletName`, `TrasnactionMemo`, etc). To ensure we ONLY mutate what we intend to mutate (and maintain the "eventual consistency" approach), we try to keep these structures relevant to the precise payload that they're mutating. For XO, we want to also be able to store additional metadata. A lot of this metadata will be mutable (e.g. `WalletName`, `TrasnactionMemo`, etc). To ensure we ONLY mutate what we intend to mutate (and maintain the "eventual consistency" approach), we try to keep these structures relevant to the precise payload that they're mutating.
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"
@@ -571,8 +587,8 @@ Perks:
1. Does not require WalletSync Servers (Wallet Data/State is synced from chain) 1. Does not require WalletSync Servers (Wallet Data/State is synced from chain)
2. Removes the "peer discovery" problem (wallet only has to have blockchain connection to find its data) 2. Removes the "peer discovery" problem (wallet only has to have blockchain connection to find its data)
3. Natural DoS protection: The tx's to store on-chain costs sats. 3. Natural DoS protection: The tx's to store on-chain costs sats.
- TODO: Compare with concern of using WalletSync servers: How do we defend these against spam? - TODO: Compare with concern of using WalletSync servers: How do we defend these against spam?
- JN suggestion: WalletSync server requires an on-chain tx to "authenticate" a wallet before our WalletSync server will allow it to store data. - JN suggestion: WalletSync server requires an on-chain tx to "authenticate" a wallet before our WalletSync server will allow it to store data.
Negatives: Negatives:
@@ -584,10 +600,10 @@ Negatives:
Other considerations: Other considerations:
1. Ideally we want to avoid using sequence numbers for data spread across multiple txs. 1. Ideally we want to avoid using sequence numbers for data spread across multiple txs.
- We can probably follow a tx chain instead which means we eliminate the risk caused by accidentally using the same derivation path twice (we wouldn't know which sequence number corresponds to which activity if this happened). - We can probably follow a tx chain instead which means we eliminate the risk caused by accidentally using the same derivation path twice (we wouldn't know which sequence number corresponds to which activity if this happened).
2. NFT commitments are another possibility. 2. NFT commitments are another possibility.
- We're capped to 40 bytes for commitments, but we could spread the payload across multiple commitment outputs (first 28 bytes would be AES-GCM header). - We're capped to 40 bytes for commitments, but we could spread the payload across multiple commitment outputs (first 28 bytes would be AES-GCM header).
- This might be more expensive in practice (if we ever need to unlock these... Tx V5 detached signatures might mitigate, or even a custom P2SH unlock that references a sig at a hard-coded input?) - This might be more expensive in practice (if we ever need to unlock these... Tx V5 detached signatures might mitigate, or even a custom P2SH unlock that references a sig at a hard-coded input?)
3. To save space, action data params would have to be ordered (we want to be able to omit the field name). 3. To save space, action data params would have to be ordered (we want to be able to omit the field name).
4. Action data params might not have fixed length (e.g. a payout address might be P2PKH, P2SH20 or P2SH32), so we'd probably need to length-prefix each data variable. 4. Action data params might not have fixed length (e.g. a payout address might be P2PKH, P2SH20 or P2SH32), so we'd probably need to length-prefix each data variable.
5. Outpoints would probably be 33 bytes each (TxHash:IndexID) - might be a lot of data for more complex actions? 5. Outpoints would probably be 33 bytes each (TxHash:IndexID) - might be a lot of data for more complex actions?
@@ -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.
{ action: 'bch_createOffer',
// The action from the template to invoke. // The data to pass into this action.
action: 'bch_createOffer', // NOTE: In practice, these will need to be converted to Uint8Array. An 'encodeData' util function will be provided to help.
// The data to pass into this action. data: {
// NOTE: In practice, these will need to be converted to Uint8Array. An 'encodeData' util function will be provided to help. oraclePublicKey:
data: { '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,125 +702,125 @@ 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
- Authenticate with services using a unique sandboxed id. - Authenticate with services using a unique sandboxed id.
<details> <details>
<summary>More details on the Sandbox Account</summary> <summary>More details on the Sandbox Account</summary>
In CashConnect, this is just the "default" key that is provided to the Dapp. In CashConnect, this is just the "default" key that is provided to the Dapp.
```ts ```ts
const providedData: CompilationDataBCH = { const providedData: CompilationDataBCH = {
bytecode: { bytecode: {
// Data required by this action. // Data required by this action.
... dataProvidedToTemplateAction, ...dataProvidedToTemplateAction,
// This is a change address provided by the wallet (used by the tx builder to auto-append change outputs). // This is a change address provided by the wallet (used by the tx builder to auto-append change outputs).
changeLockingBytecode, changeLockingBytecode,
},
keys: {
privateKeys: {
sandboxKey: theDerivedKeyForThisDappOrTemplate,
}, },
keys: { },
privateKeys: { };
sandboxKey: theDerivedKeyForThisDappOrTemplate ```
},
It is used for signing requests (e.g. authenticating with the service) and also as a means to allow creating Token Genesis and Amount Consolidation Transactions (needed to conform to some tx shapes).
For example, a template might include the following scripts:
```ts
{
//... other template fields
scripts: {
// Sandbox-specific scripts
SandboxP2PKH: {
script: 'OP_DUP OP_HASH160 <$(<wallet.public_key> OP_HASH160)> OP_EQUALVERIFY OP_CHECKSIG',
lockingType: 'p2pkh',
}, },
}; SandboxP2PKH_unlock: {
``` script: '<wallet.schnorr_signature.all_outputs_all_utxos> <wallet.public_key>',
unlocks: 'SandboxP2PKH'
It is used for signing requests (e.g. authenticating with the service) and also as a means to allow creating Token Genesis and Amount Consolidation Transactions (needed to conform to some tx shapes).
For example, a template might include the following scripts:
```ts
{
//... other template fields
scripts: {
// Sandbox-specific scripts
SandboxP2PKH: {
script: 'OP_DUP OP_HASH160 <$(<wallet.public_key> OP_HASH160)> OP_EQUALVERIFY OP_CHECKSIG',
lockingType: 'p2pkh',
},
SandboxP2PKH_unlock: {
script: '<wallet.schnorr_signature.all_outputs_all_utxos> <wallet.public_key>',
unlocks: 'SandboxP2PKH'
}
} }
} }
``` }
```
Using BCHouse as an example (which requires a Token Genesis), an action to create a campaign might then look as follows: Using BCHouse as an example (which requires a Token Genesis), an action to create a campaign might then look as follows:
```ts ```ts
{ {
actions: { actions: {
createCampaign: { createCampaign: {
name: '...', name: '...',
description: '...', description: '...',
transactions: [ transactions: [
// First transaction is to create a Token Genesis Output so that we can mint an NFT. // First transaction is to create a Token Genesis Output so that we can mint an NFT.
{ {
// Have Tx Builder automatically append inputs from default wallet to meet required sats/token amounts. // Have Tx Builder automatically append inputs from default wallet to meet required sats/token amounts.
inputs: ['*'], inputs: ['*'],
outputs: [ outputs: [
{ {
lockingBytecode: { lockingBytecode: {
script: '#SandboxP2PKH', script: '#SandboxP2PKH',
},
valueSatoshis: 1000n,
}, },
// Have TX Builder automatically append any change outputs valueSatoshis: 1000n,
'*' },
], // Have TX Builder automatically append any change outputs
}, '*'
// Second transaction actually creates the campaign (using the outpoint from above as input). ],
{ },
// ... // Second transaction actually creates the campaign (using the outpoint from above as input).
inputs: [ {
{ // ...
unlockingBytecode: { inputs: [
script: '#SandboxP2PKH_unlock', {
}, unlockingBytecode: {
sequenceNumber: 0, script: '#SandboxP2PKH_unlock',
outpointTransactionHash: '$(<tx0_hash>)',
outpointIndex: 0,
}, },
// Have Tx Builder automatically append additional inputs from default wallet to meet required sats/token amounts. sequenceNumber: 0,
'*' outpointTransactionHash: '$(<tx0_hash>)',
], outpointIndex: 0,
outputs: [ },
{ // Have Tx Builder automatically append additional inputs from default wallet to meet required sats/token amounts.
lockingBytecode: { '*'
script: '#CampaignMain', ],
}, outputs: [
valueSatoshis: 1000n, {
token: { lockingBytecode: {
category: '$(<tx0_hash>)', script: '#CampaignMain',
amount: 0n, },
nft: { valueSatoshis: 1000n,
capability: 'minting', token: {
commitment: '$(<0x0000000000000000>)', category: '$(<tx0_hash>)',
}, amount: 0n,
nft: {
capability: 'minting',
commitment: '$(<0x0000000000000000>)',
}, },
}, },
// Have TX Builder automatically append any change outputs },
'*' // Have TX Builder automatically append any change outputs
], '*'
} ],
] }
} ]
} }
} }
``` }
```
The other main use-case is Amount Consolidation where a contract may require that only a single input is provided. In that case, we can first shuffle the required funds into a P2PKH Sandbox Account and then use that as the outpoint in the follow-up transaction. The other main use-case is Amount Consolidation where a contract may require that only a single input is provided. In that case, we can first shuffle the required funds into a P2PKH Sandbox Account and then use that as the outpoint in the follow-up transaction.
</details> </details>
For intra-wallet (non WebDapp cases), we might want to do similar for an XO Connect, but instead of using the domain, we use the template hash (and probably template-instantiation data to ensure each instance gets its own sandbox?). For intra-wallet (non WebDapp cases), we might want to do similar for an XO Connect, but instead of using the domain, we use the template hash (and probably template-instantiation data to ensure each instance gets its own sandbox?).
>we could end up with overlapping derivation indexes used across devices. > we could end up with overlapping derivation indexes used across devices.
The above problem would still exist, but we would: The above problem would still exist, but we would:
+3 -3
View File
@@ -10,6 +10,6 @@ These Primitives follow a declarative style and are **NOT** optimized for tree-s
1. Install the Primitives. 1. Install the Primitives.
```sh ```sh
npm install @xocash/primitives npm install @xocash/primitives
``` ```
+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;