|
|
|
@@ -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:
|
|
|
|
|
|
|
|
|
|