Resolve "WIP: UTXO Selection and Storage"
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
# Quickstart
|
||||
|
||||
Install the XO Stack
|
||||
|
||||
```sh
|
||||
npm install @xo/stack
|
||||
```
|
||||
|
||||
Example Usage:
|
||||
|
||||
```ts
|
||||
// Import XO Modules.
|
||||
import { Address, BlockchainElectrum, StorageMemory } from '@xo/stack';
|
||||
|
||||
// The Electrum Servers we will be using.
|
||||
const ELECTRUM_SERVERS = [
|
||||
'bch.imaginary.cash',
|
||||
'cashnode.bch.ninja',
|
||||
'electrum.imaginary.cash',
|
||||
];
|
||||
|
||||
// Create an instance of the services that WalletHD will require.
|
||||
// NOTE: Other storage adapters currently broken, in the midst of refactor.
|
||||
const blockchainElectrum = new BlockchainElectrum(ELECTRUM_SERVERS);
|
||||
const cachesStore = new StorageMemory();
|
||||
const walletsStore = new StorageMemory();
|
||||
|
||||
// Start each of our services.
|
||||
await Promise.all([
|
||||
blockchainElectrum.start(),
|
||||
cachesStore.start(),
|
||||
walletsStore.start(),
|
||||
]);
|
||||
|
||||
// Create an instance of a HDWallet.
|
||||
const walletHD = await Wallet.from(
|
||||
{
|
||||
derivationPath: "m/44'/145'/0'",
|
||||
mnemonic:
|
||||
'page pencil stock planet limb cluster assault speak off joke private pioneer',
|
||||
},
|
||||
{
|
||||
blockchain: blockchainElectrum,
|
||||
cache: cachesStore,
|
||||
storage: walletsStore,
|
||||
},
|
||||
);
|
||||
|
||||
// Scan the wallet and start monitoring addresess for transactions.
|
||||
await walletHD.start();
|
||||
|
||||
// Send a transaction to the network, automatically populating UTXOs to match the output amount.
|
||||
// NOTE: Pre-signed inputs can be provided and change outputs will be automatically appended.
|
||||
const txResult = await walletHD.send({
|
||||
outputs: [
|
||||
{
|
||||
lockingBytecode: Address.fromCashAddr(
|
||||
'bitcoincash:qpc7mdjncqzf96vfzfaaqfrmru4rpg5qeq4m237t78',
|
||||
).toLockscriptBytes(),
|
||||
valueSatoshis: 10_000,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Structure/Patterns
|
||||
|
||||
General structure:
|
||||
|
||||
- All blockchain adapters will implement `IBlockhain`
|
||||
- All storage adapters will implement `IStorage`
|
||||
- All wallets will implement `IWallet`
|
||||
- Likewise for any other "service" we need in XO Stack (e.g. BCMR?, Prices?).
|
||||
|
||||
Most (almost all?) service methods in our interfaces will be `async`. This is so that we can handle situations where things might be remote (or require OS approval of some sort). For example:
|
||||
|
||||
1. Storage Adapters will need to allow backup to third-party services (so the methods must be `async`).
|
||||
2. Some Wallets **may** have remote dependencies (e.g. imagine a Multisig, Co-pay-like service).
|
||||
3. Wallets on IOS may have to tie in, in some way, with the host OS (to get approvals).
|
||||
4. There is also the `ServiceWorker` ideal where we would like to be able to run everything in a `ServiceWorker` and communicate to/from it from frontend. I'm not sure whether this will be realizable in practice yet.
|
||||
|
||||
The approach here aims to be extendable: If there is something that the XO Stack does not support, someone can:
|
||||
|
||||
1. Extend existing XO classes to customize it to their own needs
|
||||
2. BYO their own adapter
|
||||
3. Create their own wallet type that can still leverage other parts of XO stack
|
||||
|
||||
### Wallets
|
||||
|
||||
Wallets can be **composed** of other Wallets. For example:
|
||||
|
||||
- WalletP2PKH is standalone
|
||||
- WalletHD **has many** WalletP2PKH
|
||||
- SomeCustomWallet **may have many** WalletHD (or other kinds of wallet).
|
||||
|
||||
The only requirement is that a Wallet **MUST** implement the IWallet interface.
|
||||
|
||||
### Blockchain
|
||||
|
||||
Blockchains represent a connection to the BCH Network ("Blockchain" might not be the best term, but let's bikeshed over that later). This blockchain might be:
|
||||
|
||||
- Via Electrum-Cash (current implementation)
|
||||
- Via Chaingraph (not sure if we'll implement this, but it probably wouldn't be too difficult)
|
||||
- Local (as in, something like an in-memory "regtest")
|
||||
|
||||
We will likely want redundancies or possibly a fallback to a "local" blockchain in some cases where a device is disconnected from the internet (e.g. online merchant, offline customer).
|
||||
|
||||
The current plan (not implemented yet) is to create a main `Blockchain` class that can be instantiated something like follows:
|
||||
|
||||
```ts
|
||||
const blockchain = new Blockchain([
|
||||
new BlockchainElectrum(...),
|
||||
new BlockchainLocal(...),
|
||||
]);
|
||||
```
|
||||
|
||||
This `Blockchain` class will still implement the `IBlockchain` interface, but will try to direct requests to the correct Blockchain instance. This still needs more fleshing out.
|
||||
|
||||
### Stores
|
||||
|
||||
A **store** is just a means of storing data. It can be in-memory, to a file (e.g. SQLite), to somewhere remote (Firebase, MySQL, MongoDB, etc).
|
||||
|
||||
It is hierachical, meaning it can be structured like a tree (not just a simple KV table). This is in anticipation of WalletSets which likely going to have complex data structures. Ideally, where possible, we'll want our adapters to only mutate the relevant portion of an entry as opposed to the ENTIRE entry for a wallet (which would increase I/O and the risk of corruption).
|
||||
|
||||
As with `Blockchain`, we'll want support for redundancies (e.g. Local and Remote Backups).
|
||||
|
||||
The current plan (not implemented yet) is to create a `Storage` class that can be instantiated something like follows:
|
||||
|
||||
```ts
|
||||
const storage = new Storage([
|
||||
// Local store on Mobile Devices.
|
||||
new StorageSQLite(...),
|
||||
// GP's storage service.
|
||||
new StorageGPStore(...),
|
||||
]);
|
||||
```
|
||||
|
||||
This still needs a bit more fleshing out but, to allow syncing, UIDs (or WalletSet Activity IDs) will probably be used where possible (as opposed to autoincrement-like indexes).
|
||||
|
||||
### Double-Spends and Detecting Conflicting State
|
||||
|
||||
1. Currently DSP's are constrained to P2PKH.
|
||||
2. We may be able to extend these to **some** P2SH - but the requirement is that the P2SH must have an `OP_CHECKSIG`.
|
||||
3. There will be at least some (potentially many) kinds of P2SH that lack an `OP_CHECKSIG`.
|
||||
4. One problem that we have is that we cannot reliably determine (with Electrum) whether a P2SH may have been double-spent.
|
||||
5. ???
|
||||
6. Lack of profit
|
||||
|
||||
The point you're trying to get at is that Nodes on the network can have a differing mempool state.
|
||||
|
||||
How do we detect that? Fulcrum can't really provide us with that information.
|
||||
|
||||
How do we handle it if we can detect it? E.g. avoiding any "contentious" dependencies.
|
||||
|
||||
Do we have to store a whole graph?
|
||||
|
||||
Do we just broadcast to one of our nodes and then broadcast to ALL of our nodes ~5s later to see if there's contention?
|
||||
|
||||
### Electrum 4.0.1 Notes
|
||||
|
||||
1. Maybe default constructor generic to `ElectrumClientEvents`:
|
||||
|
||||
```ts
|
||||
class ElectrumClient<ElectrumEvents extends ElectrumClientEvents = ElectrumClientEvents>
|
||||
```
|
||||
|
||||
2. `RequestResponse` might be typed incorrectly: It now takes `RPCParameter | RPCParameter[]` which are defined as follows: `string | number | boolean | null;` (note that it only accept parameters, not responses which are often/maybe-always JSON).
|
||||
|
||||
Previously, it would accept:
|
||||
|
||||
```
|
||||
export type RequestResponse =
|
||||
| object
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| RequestResponse[];
|
||||
```
|
||||
|
||||
Perhaps we could type it: `export type RequestResponse = RPCParameter & object | Array<RPCParameter & object>;` (replace `object` with something like `RPCResponse`, but I think that might be taken already?)
|
||||
|
||||
3. `RPCNotification` might be typed incorrectly (or maybe this is for future proofing?). The `params` property is marked as optional (but I think all notifications will have at least one). Might be more elegant to have it required and just return an empty array.
|
||||
|
||||
4. The current approach still requires polyfills because `@electrum/sockets` itself is imported by `@electrum/network` and `@electrum/sockets` exports `ElectrumTcpSocket`.
|
||||
|
||||
A workable approach might be:
|
||||
|
||||
1. Move socket interface back to `@electum/network`.
|
||||
2. Move the `ElectrumWebSocket` back to `@electrum/network`.
|
||||
3. Rename `@electrum/sockets` to `@electrum/tcp-socket` and only include the TCP Socket in there.
|
||||
4. This will still allow third-parties to define their own sockets (e.g. `@custom/electrum-libp2p-socket`).
|
||||
|
||||
5. This is what's required for polyfilling in Vite currently:
|
||||
|
||||
```sh
|
||||
npm install events
|
||||
npm install vite-plugin-node-polyfills
|
||||
```
|
||||
|
||||
```js
|
||||
[
|
||||
nodePolyfills,
|
||||
{
|
||||
include: ['events', 'net', 'tls'],
|
||||
// NOTE: Not disabling these causes an error on Vite Polyfills V0.22.
|
||||
globals: {
|
||||
Buffer: false,
|
||||
global: false,
|
||||
process: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
## Markdown Extensions (TODO: Remove me later)
|
||||
|
||||
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).
|
||||
Reference in New Issue
Block a user