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:
@@ -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:
|
||||
|
||||
1. **Encapsulation and Data Privacy**
|
||||
|
||||
- Internal implementation details are hidden from consumers
|
||||
- State management is contained within specific classes
|
||||
- Clear boundaries between different system components
|
||||
- Reduced likelihood of state-related bugs
|
||||
|
||||
2. **Adapter Pattern Benefits**
|
||||
|
||||
- Pluggable components that can be swapped without changing application code
|
||||
- Easy to add new implementations for different environments
|
||||
- Testing simplified through mock adapters
|
||||
- Reduced vendor lock-in
|
||||
|
||||
3. **Developer Experience**
|
||||
|
||||
- Strong IDE support with autocomplete suggestions
|
||||
- Type safety when using TypeScript
|
||||
- Clear inheritance hierarchies
|
||||
@@ -57,6 +60,7 @@ The storage adapter interface that defines how data is persisted:
|
||||
- Query capabilities
|
||||
|
||||
Available implementations:
|
||||
|
||||
- **StoreInMemory**: Volatile storage for testing and development
|
||||
- **StoreIndexedDB**: Browser-based persistent storage
|
||||
- **StoreSQLite**: Native application storage solution
|
||||
@@ -71,6 +75,7 @@ Defines the interface for interacting with the BCH network:
|
||||
- Network status management
|
||||
|
||||
Available implementations:
|
||||
|
||||
- **BlockchainInMemory**: Mock blockchain for testing
|
||||
- **BlockchainElectrum**: Production-ready Electrum protocol client
|
||||
|
||||
@@ -84,6 +89,7 @@ Represents different wallet types for managing BCH:
|
||||
- Balance tracking
|
||||
|
||||
Available implementations:
|
||||
|
||||
- **WalletP2PKH**: Basic Pay-to-Public-Key-Hash wallet
|
||||
- **WalletHD**: Hierarchical Deterministic wallet implementation
|
||||
- **WalletXO**: Extended HD wallet with XO-specific features
|
||||
|
||||
@@ -204,4 +204,4 @@ TODO: When we have docs public, move this back into the top config section above
|
||||
.link-text-icon {
|
||||
margin-left: 6px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -78,6 +78,7 @@ Note that these are still unstable and will change in future.
|
||||
:toFunctions="encrypter.to"
|
||||
:methods="encrypter.methods"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
## Hasher
|
||||
|
||||
@@ -18,26 +18,28 @@
|
||||
**XO**
|
||||
|
||||
- Object-Oriented approach
|
||||
- Built on-top of LibAuth.
|
||||
- Declarative-style API (for ease of use)
|
||||
- Adapter-based (can swap in/out blockchain adapters, storage adapters, etc).
|
||||
|
||||
- Built on-top of LibAuth.
|
||||
- Declarative-style API (for ease of use)
|
||||
- Adapter-based (can swap in/out blockchain adapters, storage adapters, etc).
|
||||
|
||||
- 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
|
||||
|
||||
- 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>
|
||||
@@ -50,79 +52,80 @@ This is still in flux, but we are anticipating (at least) three packages/layers:
|
||||
|
||||
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
|
||||
// NOTE: Each `from` method is static and throws on invalid input.
|
||||
const cashAddress = Address.fromLegacy('someLegacyAddress').toCashAddr();
|
||||
```
|
||||
```ts
|
||||
// NOTE: Each `from` method is static and throws on invalid input.
|
||||
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**
|
||||
|
||||
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)
|
||||
|
||||
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>
|
||||
<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
|
||||
// Create an instance of a HDWallet.
|
||||
const walletHD = await WalletHD.from(
|
||||
// Adapters
|
||||
{
|
||||
blockchain: blockchainElectrum,
|
||||
cache: cachesStore,
|
||||
// NOTE: This will likely change to "Activities"
|
||||
storage: walletsStore,
|
||||
},
|
||||
// Settings.
|
||||
// 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
|
||||
{
|
||||
derivationPath: "m/44'/145'/0'",
|
||||
mnemonic:
|
||||
```ts
|
||||
// Create an instance of a HDWallet.
|
||||
const walletHD = await WalletHD.from(
|
||||
// Adapters
|
||||
{
|
||||
blockchain: blockchainElectrum,
|
||||
cache: cachesStore,
|
||||
// NOTE: This will likely change to "Activities"
|
||||
storage: walletsStore,
|
||||
},
|
||||
// Settings.
|
||||
// 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
|
||||
{
|
||||
derivationPath: "m/44'/145'/0'",
|
||||
mnemonic:
|
||||
'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.
|
||||
// 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
|
||||
|
||||
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>
|
||||
|
||||
## 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.
|
||||
|
||||
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".
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
| Key | Derivation (Rough Example) | Comments |
|
||||
|------------------------|------------------------------------------------------------------|---------------------------------------|
|
||||
| ---------------------- | ---------------------------------------------------------------- | ------------------------------------- |
|
||||
| Default HD Wallet | Root Seed -> m/44'/145'/0' | Backwards compat with current Wallets |
|
||||
| WalletId | hash256(hash256(rootSeed) + hash256('walletId')) | Identifier for Wallet |
|
||||
| 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>
|
||||
<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>
|
||||
|
||||
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>
|
||||
<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>
|
||||
|
||||
```ts
|
||||
@@ -364,7 +369,6 @@ export type Activities = { [uuidv7: string]: Activity };
|
||||
|
||||
... where the payload Activity itself looks something like this:
|
||||
|
||||
|
||||
```ts
|
||||
export type Activity<Type extends string, ValueType> = {
|
||||
// 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 XOTransactionCategory = Activity<'XOTransactionCategory', string>;
|
||||
export type XOFrozenCoin = Activity<'XOFrozenCoin', boolean>;
|
||||
export type XOTemplateAction = Activity<'XOTemplateAction', TemplateActionPayload>;
|
||||
export type XOTemplateActionResult = Activity<'XOTemplateActionResult', TemplateActionResultPayload>;
|
||||
export type XOTemplateAction = Activity<
|
||||
'XOTemplateAction',
|
||||
TemplateActionPayload
|
||||
>;
|
||||
export type XOTemplateActionResult = Activity<
|
||||
'XOTemplateActionResult',
|
||||
TemplateActionResultPayload
|
||||
>;
|
||||
```
|
||||
|
||||
<details>
|
||||
<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
|
||||
export type WalletDetails = Activity<'WalletDetails', {
|
||||
```ts
|
||||
export type WalletDetails = Activity<
|
||||
'WalletDetails',
|
||||
{
|
||||
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.
|
||||
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`.
|
||||
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*.
|
||||
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`.
|
||||
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_.
|
||||
|
||||
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>
|
||||
|
||||
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
|
||||
// Pseudo-code for clarity.
|
||||
const relevantActivitiesToBuildSnapshotWith = activities.filter(activity => activity !== null);
|
||||
const relevantActivitiesToBuildSnapshotWith = activities.filter(
|
||||
(activity) => activity !== null,
|
||||
);
|
||||
```
|
||||
|
||||
#### (Maybe) Using enums to represent "type"
|
||||
@@ -571,8 +587,8 @@ Perks:
|
||||
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)
|
||||
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?
|
||||
- JN suggestion: WalletSync server requires an on-chain tx to "authenticate" a wallet before our WalletSync server will allow it to store data.
|
||||
- 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.
|
||||
|
||||
Negatives:
|
||||
|
||||
@@ -584,10 +600,10 @@ Negatives:
|
||||
Other considerations:
|
||||
|
||||
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.
|
||||
- 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?)
|
||||
- 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?)
|
||||
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.
|
||||
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
|
||||
interface Action {
|
||||
// Human-readble name
|
||||
name: string,
|
||||
name: string;
|
||||
// Human-readable description
|
||||
description: string,
|
||||
description: string;
|
||||
// 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:
|
||||
// 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.
|
||||
// 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.
|
||||
// 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:
|
||||
// 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)
|
||||
// 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:
|
||||
|
||||
```ts
|
||||
const createOfferResult = await wallet.executeAction(
|
||||
BCHGURU_TEMPLATE,
|
||||
{
|
||||
// The action from the template to invoke.
|
||||
action: 'bch_createOffer',
|
||||
// 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.
|
||||
data: {
|
||||
oraclePublicKey: '0x02d09db08af1ff4e8453919cc866a4be427d7bfe18f2c05e5444c196fcf6fd2818',
|
||||
player1Prediction: 100_000_000,
|
||||
betAmount: 250_000,
|
||||
// ...
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const createOfferResult = await wallet.executeAction(BCHGURU_TEMPLATE, {
|
||||
// The action from the template to invoke.
|
||||
action: 'bch_createOffer',
|
||||
// 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.
|
||||
data: {
|
||||
oraclePublicKey:
|
||||
'0x02d09db08af1ff4e8453919cc866a4be427d7bfe18f2c05e5444c196fcf6fd2818',
|
||||
player1Prediction: 100_000_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.
|
||||
@@ -689,125 +702,125 @@ const domainSandboxSeed = sha256(sha256(rootSeed) + sha256(domain));
|
||||
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).
|
||||
- *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
|
||||
- Create transactions that consolidate amounts for follow-up transactions
|
||||
- Create Token Genesis transactions and
|
||||
- Authenticate with services using a unique sandboxed id.
|
||||
- Create transactions that consolidate amounts for follow-up transactions
|
||||
- Create Token Genesis transactions and
|
||||
- Authenticate with services using a unique sandboxed id.
|
||||
|
||||
<details>
|
||||
<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
|
||||
const providedData: CompilationDataBCH = {
|
||||
bytecode: {
|
||||
// Data required by this action.
|
||||
... dataProvidedToTemplateAction,
|
||||
// This is a change address provided by the wallet (used by the tx builder to auto-append change outputs).
|
||||
changeLockingBytecode,
|
||||
```ts
|
||||
const providedData: CompilationDataBCH = {
|
||||
bytecode: {
|
||||
// Data required by this action.
|
||||
...dataProvidedToTemplateAction,
|
||||
// This is a change address provided by the wallet (used by the tx builder to auto-append change outputs).
|
||||
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',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
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'
|
||||
}
|
||||
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
|
||||
{
|
||||
actions: {
|
||||
createCampaign: {
|
||||
name: '...',
|
||||
description: '...',
|
||||
transactions: [
|
||||
// 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.
|
||||
inputs: ['*'],
|
||||
outputs: [
|
||||
{
|
||||
lockingBytecode: {
|
||||
script: '#SandboxP2PKH',
|
||||
},
|
||||
valueSatoshis: 1000n,
|
||||
```ts
|
||||
{
|
||||
actions: {
|
||||
createCampaign: {
|
||||
name: '...',
|
||||
description: '...',
|
||||
transactions: [
|
||||
// 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.
|
||||
inputs: ['*'],
|
||||
outputs: [
|
||||
{
|
||||
lockingBytecode: {
|
||||
script: '#SandboxP2PKH',
|
||||
},
|
||||
// Have TX Builder automatically append any change outputs
|
||||
'*'
|
||||
],
|
||||
},
|
||||
// Second transaction actually creates the campaign (using the outpoint from above as input).
|
||||
{
|
||||
// ...
|
||||
inputs: [
|
||||
{
|
||||
unlockingBytecode: {
|
||||
script: '#SandboxP2PKH_unlock',
|
||||
},
|
||||
sequenceNumber: 0,
|
||||
outpointTransactionHash: '$(<tx0_hash>)',
|
||||
outpointIndex: 0,
|
||||
valueSatoshis: 1000n,
|
||||
},
|
||||
// Have TX Builder automatically append any change outputs
|
||||
'*'
|
||||
],
|
||||
},
|
||||
// Second transaction actually creates the campaign (using the outpoint from above as input).
|
||||
{
|
||||
// ...
|
||||
inputs: [
|
||||
{
|
||||
unlockingBytecode: {
|
||||
script: '#SandboxP2PKH_unlock',
|
||||
},
|
||||
// Have Tx Builder automatically append additional inputs from default wallet to meet required sats/token amounts.
|
||||
'*'
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
lockingBytecode: {
|
||||
script: '#CampaignMain',
|
||||
},
|
||||
valueSatoshis: 1000n,
|
||||
token: {
|
||||
category: '$(<tx0_hash>)',
|
||||
amount: 0n,
|
||||
nft: {
|
||||
capability: 'minting',
|
||||
commitment: '$(<0x0000000000000000>)',
|
||||
},
|
||||
sequenceNumber: 0,
|
||||
outpointTransactionHash: '$(<tx0_hash>)',
|
||||
outpointIndex: 0,
|
||||
},
|
||||
// Have Tx Builder automatically append additional inputs from default wallet to meet required sats/token amounts.
|
||||
'*'
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
lockingBytecode: {
|
||||
script: '#CampaignMain',
|
||||
},
|
||||
valueSatoshis: 1000n,
|
||||
token: {
|
||||
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>
|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -10,6 +10,6 @@ These Primitives follow a declarative style and are **NOT** optimized for tree-s
|
||||
|
||||
1. Install the Primitives.
|
||||
|
||||
```sh
|
||||
npm install @xocash/primitives
|
||||
```
|
||||
```sh
|
||||
npm install @xocash/primitives
|
||||
```
|
||||
|
||||
@@ -209,7 +209,7 @@ export class Address {
|
||||
type: 'P2SH20' | 'P2SH32' = 'P2SH32',
|
||||
): 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.
|
||||
*/
|
||||
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,
|
||||
type: AddressType = 'P2PKH',
|
||||
): Address {
|
||||
return this.fromHash160Bytes(Bytes.fromHex(hash160Hex).toBytes(), type);
|
||||
return this.fromHash160Bytes(
|
||||
Bytes.fromHex(hash160Hex).toUint8Array(),
|
||||
type,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -73,7 +73,7 @@ export class BlockHeader {
|
||||
* @throws Error if the hex string length is not 160
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
getHash(): Uint8Array {
|
||||
return hash256(this.toBytes()).toReversed();
|
||||
return hash256(this.bytes).toReversed();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,16 +15,19 @@ import {
|
||||
/**
|
||||
* Bytes Primitive.
|
||||
*/
|
||||
export class Bytes {
|
||||
protected readonly bytes: Uint8Array;
|
||||
|
||||
export class Bytes extends Uint8Array {
|
||||
/**
|
||||
* Instantiate Bytes from a Uint8Array.
|
||||
*
|
||||
* @param bytes {Uint8Array} The bytes to use.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
public toBase64(): string {
|
||||
return binToBase64(this.bytes);
|
||||
return binToBase64(this);
|
||||
}
|
||||
|
||||
public toBigInt(): bigint {
|
||||
return binToBigIntUintLE(this.bytes);
|
||||
return binToBigIntUintLE(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +135,7 @@ export class Bytes {
|
||||
* @returns Bytes as a hex string.
|
||||
*/
|
||||
public toHex(): string {
|
||||
return binToHex(this.bytes);
|
||||
return binToHex(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +144,7 @@ export class Bytes {
|
||||
* @returns Bytes as a UTF8 string.
|
||||
*/
|
||||
public toUtf8(): string {
|
||||
return binToUtf8(this.bytes);
|
||||
return binToUtf8(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,11 +152,12 @@ export class Bytes {
|
||||
*
|
||||
* @returns Bytes as a Uint8Array.
|
||||
*/
|
||||
public toBytes(): Uint8Array {
|
||||
return this.bytes;
|
||||
public toUint8Array(): Uint8Array {
|
||||
// NOTE: We clone the instance of Uint8Array so that we don't accidentally mutate it.
|
||||
return new Uint8Array([...this]);
|
||||
}
|
||||
|
||||
public invert(): Bytes {
|
||||
return new Bytes(Uint8Array.from([...this.bytes].reverse()));
|
||||
return new Bytes(Uint8Array.from([...this].reverse()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export class HDPrivateNode {
|
||||
public static fromSeed(seed: Uint8Array | string): HDPrivateNode {
|
||||
// Cast from hex if a string was provided.
|
||||
if (typeof seed === 'string') {
|
||||
seed = Bytes.fromHex(seed).toBytes();
|
||||
seed = Bytes.fromHex(seed);
|
||||
}
|
||||
|
||||
// If the seed is not 64 bytes, throw an error.
|
||||
|
||||
@@ -98,7 +98,7 @@ export class PrivateKey {
|
||||
*/
|
||||
public static fromHex(privateKeyHex: string): PrivateKey {
|
||||
// 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.
|
||||
return new this(privateKeyBin);
|
||||
@@ -268,7 +268,7 @@ export class PrivateKey {
|
||||
*/
|
||||
public signMessageRecoverableCompact(message: Uint8Array | string) {
|
||||
if (typeof message === 'string') {
|
||||
message = Bytes.fromUtf8(message).toBytes();
|
||||
message = Bytes.fromUtf8(message);
|
||||
}
|
||||
|
||||
const messageHash = sha256.hash(message);
|
||||
@@ -292,7 +292,7 @@ export class PrivateKey {
|
||||
*/
|
||||
public signMessageCompact(message: Uint8Array | string) {
|
||||
if (typeof message === 'string') {
|
||||
message = Bytes.fromUtf8(message).toBytes();
|
||||
message = Bytes.fromUtf8(message);
|
||||
}
|
||||
|
||||
const messageHash = sha256.hash(message);
|
||||
@@ -316,7 +316,7 @@ export class PrivateKey {
|
||||
*/
|
||||
public signMessageSchnorr(message: Uint8Array | string) {
|
||||
if (typeof message === 'string') {
|
||||
message = Bytes.fromUtf8(message).toBytes();
|
||||
message = Bytes.fromUtf8(message);
|
||||
}
|
||||
|
||||
const messageHash = sha256.hash(message);
|
||||
@@ -340,7 +340,7 @@ export class PrivateKey {
|
||||
*/
|
||||
public signMessageDER(message: Uint8Array | string) {
|
||||
if (typeof message === 'string') {
|
||||
message = Bytes.fromUtf8(message).toBytes();
|
||||
message = Bytes.fromUtf8(message);
|
||||
}
|
||||
|
||||
const messageHash = sha256.hash(message);
|
||||
@@ -363,10 +363,8 @@ export class PrivateKey {
|
||||
*/
|
||||
public signMessageElectron(message: string): Uint8Array {
|
||||
// Compile the message.
|
||||
const magicBytes = Bytes.fromUtf8(
|
||||
'\x18Bitcoin Signed Message:\n',
|
||||
).toBytes();
|
||||
const messageBytes = Bytes.fromUtf8(message).toBytes();
|
||||
const magicBytes = Bytes.fromUtf8('\x18Bitcoin Signed Message:\n');
|
||||
const messageBytes = Bytes.fromUtf8(message);
|
||||
const messageLengthBytes = bigIntToCompactUint(BigInt(messageBytes.length)); // hexToBin(messageBytes.length.toString(16)); //
|
||||
const messageFinal = new Uint8Array([
|
||||
...magicBytes,
|
||||
@@ -406,7 +404,7 @@ export class PrivateKey {
|
||||
// If a string was given, convert it from hex to bytes.
|
||||
// TODO: Consider using Bytes class - it ensures valid hex.
|
||||
if (typeof messageHash === 'string') {
|
||||
messageHash = Bytes.fromHex(messageHash).toBytes();
|
||||
messageHash = Bytes.fromHex(messageHash);
|
||||
}
|
||||
|
||||
if (messageHash.length !== 32) {
|
||||
@@ -441,7 +439,7 @@ export class PrivateKey {
|
||||
// If a string was given, convert it from hex to bytes.
|
||||
// TODO: Consider using Bytes class - it ensures valid hex.
|
||||
if (typeof messageHash === 'string') {
|
||||
messageHash = Bytes.fromHex(messageHash).toBytes();
|
||||
messageHash = Bytes.fromHex(messageHash);
|
||||
}
|
||||
|
||||
if (messageHash.length !== 32) {
|
||||
@@ -476,7 +474,7 @@ export class PrivateKey {
|
||||
// If a string was given, convert it from hex to bytes.
|
||||
// TODO: Consider using Bytes class - it ensures valid hex.
|
||||
if (typeof messageHash === 'string') {
|
||||
messageHash = Bytes.fromHex(messageHash).toBytes();
|
||||
messageHash = Bytes.fromHex(messageHash);
|
||||
}
|
||||
|
||||
if (messageHash.length !== 32) {
|
||||
@@ -511,7 +509,7 @@ export class PrivateKey {
|
||||
// If a string was given, convert it from hex to bytes.
|
||||
// TODO: Consider using Bytes class - it ensures valid hex.
|
||||
if (typeof messageHash === 'string') {
|
||||
messageHash = Bytes.fromHex(messageHash).toBytes();
|
||||
messageHash = Bytes.fromHex(messageHash);
|
||||
}
|
||||
|
||||
if (messageHash.length !== 32) {
|
||||
|
||||
@@ -78,7 +78,7 @@ export class PublicKey {
|
||||
* @returns The created Public Key entity.
|
||||
*/
|
||||
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,
|
||||
): boolean {
|
||||
if (typeof messageHash === 'string') {
|
||||
messageHash = Bytes.fromHex(messageHash).toBytes();
|
||||
messageHash = Bytes.fromHex(messageHash);
|
||||
}
|
||||
|
||||
if (typeof signature === 'string') {
|
||||
signature = Bytes.fromHex(signature).toBytes();
|
||||
signature = Bytes.fromHex(signature);
|
||||
}
|
||||
|
||||
const validity = secp256k1.verifySignatureDER(
|
||||
@@ -261,11 +261,11 @@ export class PublicKey {
|
||||
signature: Uint8Array | string,
|
||||
): boolean {
|
||||
if (typeof messageHash === 'string') {
|
||||
messageHash = Bytes.fromHex(messageHash).toBytes();
|
||||
messageHash = Bytes.fromHex(messageHash);
|
||||
}
|
||||
|
||||
if (typeof signature === 'string') {
|
||||
signature = Bytes.fromHex(signature).toBytes();
|
||||
signature = Bytes.fromHex(signature);
|
||||
}
|
||||
|
||||
const validity = secp256k1.verifySignatureSchnorr(
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
* console.log(script.toHex());
|
||||
*
|
||||
* // Output as bytecode.
|
||||
* console.log(script.toBytes());
|
||||
* console.log(script);
|
||||
* ```
|
||||
*/
|
||||
export class Script {
|
||||
@@ -177,7 +177,7 @@ export class Script {
|
||||
*/
|
||||
public static fromHex(hex: string): Script {
|
||||
// Return new Script entity constructed from the binary.
|
||||
return this.fromBytes(Bytes.fromHex(hex).toBytes());
|
||||
return this.fromBytes(Bytes.fromHex(hex));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -212,7 +212,7 @@ export class Transaction extends BaseTransaction {
|
||||
* @returns {Transaction} The created Transaction Primitive.
|
||||
*/
|
||||
public static fromHex(hex: string): Transaction {
|
||||
const bytes = Bytes.fromHex(hex).toBytes();
|
||||
const bytes = Bytes.fromHex(hex).toUint8Array();
|
||||
return this.fromBytes(bytes);
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ export class TransactionResolved extends BaseTransaction {
|
||||
* @returns {Transaction} The created Transaction Primitive.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ export class TransactionHash {
|
||||
* @returns {TransactionHash} Instance of TransactionHash.
|
||||
*/
|
||||
public static fromHex(hex: string): TransactionHash {
|
||||
const bytes = Bytes.fromHex(hex).toBytes();
|
||||
const bytes = Bytes.fromHex(hex).toUint8Array();
|
||||
return this.fromBytes(bytes);
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ export class TransactionHash {
|
||||
* @returns {TransactionHash} Instance of TransactionHash.
|
||||
*/
|
||||
public static fromTransactionHex(transactionHex: string): TransactionHash {
|
||||
const bytes = Bytes.fromHex(transactionHex).toBytes();
|
||||
const bytes = Bytes.fromHex(transactionHex).toUint8Array();
|
||||
return this.fromTransactionBytes(bytes);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Address } from '~/src/address';
|
||||
import { Bytes } from '~/src/bytes';
|
||||
import { Address } from '../src/address';
|
||||
import { Bytes } from '../src/bytes';
|
||||
|
||||
describe('Success cases', () => {
|
||||
// Test cases with different address representations
|
||||
@@ -27,7 +27,7 @@ describe('Success cases', () => {
|
||||
type: 'P2PKH',
|
||||
payload: Bytes.fromHex(
|
||||
'626025854e5a67ca5eb122e213f409ec842d45e4',
|
||||
).toBytes(),
|
||||
).toUint8Array(),
|
||||
},
|
||||
representations: {
|
||||
cashAddr: 'bitcoincash:qp3xqfv9fedx0jj7ky3wyyl5p8kggt29usvlhrrxws',
|
||||
@@ -35,13 +35,13 @@ describe('Success cases', () => {
|
||||
tokenAddr: 'bitcoincash:zp3xqfv9fedx0jj7ky3wyyl5p8kggt29ust4yadq3r',
|
||||
lockscriptBytes: Bytes.fromHex(
|
||||
'76a914626025854e5a67ca5eb122e213f409ec842d45e488ac',
|
||||
).toBytes(),
|
||||
).toUint8Array(),
|
||||
lockscriptHex: '76a914626025854e5a67ca5eb122e213f409ec842d45e488ac',
|
||||
lockscriptASM:
|
||||
'OP_DUP OP_HASH160 OP_PUSHBYTES_20 0x626025854e5a67ca5eb122e213f409ec842d45e4 OP_EQUALVERIFY OP_CHECKSIG',
|
||||
hash160Bytes: Bytes.fromHex(
|
||||
'626025854e5a67ca5eb122e213f409ec842d45e4',
|
||||
).toBytes(),
|
||||
).toUint8Array(),
|
||||
hash160Hex: '626025854e5a67ca5eb122e213f409ec842d45e4',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { BlockHeader } from '~/src/block-header';
|
||||
import { Bytes } from '~/src/bytes';
|
||||
import { BlockHeader } from '../src/block-header';
|
||||
import { Bytes } from '../src/bytes';
|
||||
|
||||
describe('Success cases', () => {
|
||||
// Test cases with different block header representations
|
||||
@@ -16,11 +16,11 @@ describe('Success cases', () => {
|
||||
{
|
||||
raw: Bytes.fromHex(
|
||||
'0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d',
|
||||
).toBytes(),
|
||||
).toUint8Array(),
|
||||
representations: {
|
||||
bytes: Bytes.fromHex(
|
||||
'0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d',
|
||||
).toBytes(),
|
||||
).toUint8Array(),
|
||||
hex: '0000602093ea70b294aeef69db85981bd387285824fc5af732e487010000000000000000135576e1ba9cae5509d60b4fbb9e66612f35b5ee8db264cb12612d46cc8db60b5e34a164d46b021864abbb1d',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('Success cases', () => {
|
||||
},
|
||||
toSeed: Bytes.fromHex(
|
||||
'7066f43a742829f0edce7d8e3c31651df320f13913995a6500f4e515db372b35202227f923ff181defc35cea02f8468df39f8fa9d97a202d252e61562e4bbfb5',
|
||||
).toBytes(),
|
||||
).toUint8Array(),
|
||||
toPhrase:
|
||||
'goose just fan kidney barrel album wave crowd movie flame cousin exhaust',
|
||||
toPhraseWithoutPassphrase:
|
||||
@@ -124,7 +124,7 @@ describe('Success cases', () => {
|
||||
},
|
||||
toSeed: Bytes.fromHex(
|
||||
'2f706756ab2083f7ce07c0c0fcde8f14f062da5de839f42763262b25e0d871f03dd2b561d55eaacc471c94da656ee7b0007fb48a64e31ec91a1a2ba706b46b89',
|
||||
).toBytes(),
|
||||
).toUint8Array(),
|
||||
toPhrase:
|
||||
'goose just fan kidney barrel album wave crowd movie flame cousin exhaust GeneralProtocols!',
|
||||
toPhraseWithoutPassphrase:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Satoshis } from '~/src/satoshis';
|
||||
import { Satoshis } from '../src/satoshis';
|
||||
|
||||
describe('Success cases', () => {
|
||||
// Test cases with single representation for each unit
|
||||
|
||||
@@ -17,11 +17,11 @@ describe('Success cases', () => {
|
||||
{
|
||||
raw: Bytes.fromHex(
|
||||
'76a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac',
|
||||
).toBytes(),
|
||||
).toUint8Array(),
|
||||
representations: {
|
||||
bytes: Bytes.fromHex(
|
||||
'76a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac',
|
||||
).toBytes(),
|
||||
).toUint8Array(),
|
||||
asm: 'OP_DUP\nOP_HASH160\nOP_PUSHBYTES_20\n0x876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d6\nOP_EQUALVERIFY\nOP_CHECKSIG',
|
||||
hex: '76a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac',
|
||||
},
|
||||
@@ -38,19 +38,19 @@ describe('Success cases', () => {
|
||||
fromMethods.forEach(({ name, key }) => {
|
||||
it.each(testCases)(`${name}()`, ({ raw, representations }) => {
|
||||
const script = Script[name](representations[key]);
|
||||
expect(script.toRaw()).toStrictEqual(raw);
|
||||
expect(script.toRaw()).toEqual(raw);
|
||||
});
|
||||
});
|
||||
|
||||
// Test generic from method
|
||||
it.each(testCases)('from(Uint8Array)', ({ raw, representations }) => {
|
||||
const script = Script.from(representations.bytes);
|
||||
expect(script.toRaw()).toStrictEqual(raw);
|
||||
expect(script.toRaw()).toEqual(raw);
|
||||
});
|
||||
|
||||
it.each(testCases)('from(string)', ({ raw, representations }) => {
|
||||
const script = Script.from(representations.hex);
|
||||
expect(script.toRaw()).toStrictEqual(raw);
|
||||
expect(script.toRaw()).toEqual(raw);
|
||||
});
|
||||
|
||||
// Test to methods with their respective types
|
||||
@@ -63,14 +63,14 @@ describe('Success cases', () => {
|
||||
toMethods.forEach(({ name, key }) => {
|
||||
it.each(testCases)(`${name}()`, ({ raw, representations }) => {
|
||||
const script = Script.fromRaw(raw);
|
||||
expect(script[name]()).toStrictEqual(representations[key]);
|
||||
expect(script[name]()).toEqual(representations[key]);
|
||||
});
|
||||
});
|
||||
|
||||
// Test fromRaw specifically
|
||||
it.each(testCases)('fromRaw() creates correct instance', ({ raw }) => {
|
||||
const script = Script.fromRaw(raw);
|
||||
expect(script.toRaw()).toStrictEqual(raw);
|
||||
expect(script.toRaw()).toEqual(raw);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -157,7 +157,9 @@ export class SSEClient {
|
||||
this.messageBuffer = new Uint8Array([...this.messageBuffer, ...chunk]);
|
||||
|
||||
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 completeEventCount = 0;
|
||||
|
||||
Reference in New Issue
Block a user