Initial PR for implementation of BitcoinJS wallet library#1
Initial PR for implementation of BitcoinJS wallet library#1
Conversation
This is the way to go I think. Make an interface like I would recommend using Bitcoin Core RPC for the default implementation. The README can tell the user how they should set up their Bitcoin Core wallet in order to work with this library. This would make it very easy for a user to implement their own NetworkManager and plug it in.
For default I would stick to one... This should probably also be managed by an ScriptManager class. Give it an hdNode and call methods to get a list of addresses. The tricky part would be how the ScriptManager will learn the info from NetworkManager to make decisions on how far to derive addresses... Sounds great. I look forward to your progress. |
| abstract fetchAddressStats(address: string): Promise<AddressStats>; | ||
|
|
||
| abstract fetchAddressUtxos(address: string): Promise<Utxos>; | ||
| } |
There was a problem hiding this comment.
This is an abstract class to ensure a compatible interface is injected into the Wallet class. The API responses are modeled after Esplora, not Bitcoin Core. I think Bitcoin Core RPC is not as well suited for servicing this Wallet as servers like ElectrumX or Esplora. For example, I'm not sure how to construct all the UTXOs of a wallet using Bitcoin Core's RPC whereas with Esplora you can easily query for all UTXOs belonging to an address.
I opted for using Esplora for this initial implementation because the Esplora server supports HTTP (the ElectrumX servers available online only support TCP). But any developer could use any blockchain service as long as they transform the response payload to match that of Esplora.
| Esplora.call(`address/${address}/utxo`); | ||
| } | ||
|
|
||
| export const esplora = new Esplora(); |
There was a problem hiding this comment.
This is the implementation of BlockchainService
| 'nextUnusedChangeAddress' | 'changeAddresses' | ||
| >; | ||
|
|
||
| export class Wallet { |
There was a problem hiding this comment.
Below is the Wallet that a developer would look at (along with the included tests) to learn how to build a BitcoinJS Wallet or even use it directly in a production environment...
| private blockchainService: BlockchainService, | ||
| private gapLimit = 20, | ||
| private derivationPath = "m/84'/0'/0'", | ||
| ) {} |
There was a problem hiding this comment.
The Wallet is mostly stateless. In most modern web applications, state management is handled through dedicated libraries like Redux, so the user is expected to maintain the state of the Wallet separately. It's still easy to use, in my opinion, as you can see in the tests.
| }; | ||
| }; | ||
|
|
||
| fetchWalletStats = async (masterPublicKey: string): Promise<WalletStats> => { |
There was a problem hiding this comment.
The user is expected to call this function to retrieve complete stats of the Wallet. The stats include:
- Total wallet balance
- Total wallet pending balance (mempool transactions)
- A list of Wallet addresses (up to the configured gap limit)
- A list of Wallet change addresses (up to the configured limit)
- A list of addresses which include UTXOs
- The next unused address
- The next unused change address
| }; | ||
| }; | ||
|
|
||
| fetchUtxos = async ( |
There was a problem hiding this comment.
Using the list of UTXO addresses retrieved from fetchWalletStats, the developer can use this method to retrieve a list of UTXOs belonging to each one of those addresses.
This method augments the retrieved UTXO object with address and derivationPath. This makes these values conveniently available during the transaction creation process.
| (addressMetadata) => address === addressMetadata.address, | ||
| )?.publicKey; | ||
|
|
||
| createTransaction = async ( |
There was a problem hiding this comment.
The developer would call this method, passing values mostly retrieved from the other methods, to create a transaction.
| const testMnemonic = | ||
| 'base index concert culture silver say return vote dial pepper cloud kingdom fly outer tornado'; | ||
|
|
||
| describe('Wallet basics', () => { |
There was a problem hiding this comment.
These tests demonstrate uses of the Wallet functionality so far...
| @@ -0,0 +1,177 @@ | |||
| /* eslint-disable no-restricted-syntax */ | |||
| import nock from 'nock'; | |||
There was a problem hiding this comment.
nock provides a way to mock the network during tests. It intercepts network calls and returns responses as configured below. I've configured it to return responses modeled after Esplora.
|
@junderw Apologies for the long absence! This stuff is new and challenging to me, so I'm learning as I go (+ lots of personal changes going on, sorry!) I've just pushed code with some progress. I've left GitHub review comments on the parts of the code relevant to our discussion. Feel free to take a look at your convenience. Added features are:
|
|
No worries. There's no deadline or anything. I'll go ahead and review it when I have time. Feel free to keep on moving forward, don't feel like you have to wait for my review. If you have any questions ask them here. Thanks. |
This PR is the initial implementation of a Bitcoin wallet library built on top of BitcoinJS. This library can serve as an example of how to use
bitcoinjs-libto build a fully functioning wallet. I also intend to use this library in production in my other project.Open questions
Design decisions
The default derivation path does not use hardened keys. This is because I want to encourage the use of watch-only mode.
The problem with a hardened derivation path is that it causes confusion when the user tries to setup the same wallet as watch-only