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:
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
+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 {
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"
:methods="encrypter.methods"
/>
</div>
## Hasher
+216 -203
View File
@@ -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:
+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.
```sh
npm install @xocash/primitives
```
```sh
npm install @xocash/primitives
```
+6 -3
View File
@@ -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,
);
}
/**
+3 -3
View File
@@ -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 -11
View File
@@ -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()));
}
}
+1 -1
View File
@@ -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.
+11 -13
View File
@@ -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) {
+5 -5
View File
@@ -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(
+2 -2
View File
@@ -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));
}
/**
+4 -4
View File
@@ -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);
}
+5 -5
View File
@@ -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',
},
},
+2 -2
View File
@@ -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 -1
View File
@@ -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
+7 -7
View File
@@ -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);
});
});
+3 -1
View File
@@ -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;