Spaces:
Runtime error
Runtime error
File size: 10,327 Bytes
711e9c6 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 |
---
sidebar_position: 1
---
# How to Add a Provider
In the [contract testing section](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#provider), we learned about the Provider class in sCrypt. This class serves as an abstraction of a Bitcoin node, allowing your application to communicate with the Bitcoin network.
`sCrypt` provides the following providers by default:
* `DummyProvider`: A mockup provider intended for local testing. It does not connect to the Bitcoin blockchain and thus cannot send transactions.
* `DefaultProvider`: The default provider is the safest and easiest way to begin developing on Bitcoin, and is also robust enough for use in production. It can be used in testnet as well as mainnet.
* For a full list of providers, see [here](../reference/classes/Provider.md#hierarchy).
## Implementation
### Base Class `Provider`
To implement your own provider, you must extend the base class `Provider`. Here's the definition of this class:
```ts
/**
* A Provider is an abstraction of non-account-based operations on a blockchain and is generally not directly involved in signing transaction or data.
*/
export abstract class Provider extends EventEmitter {
constructor() {
super()
this._isProvider = true;
}
/**
* check if provider is ready
*/
abstract isConnected(): boolean;
/**
* Implement the connection provider, for example, verify the api key during the connection process.
* @returns a connected provider. Throw an exception if the connection fails.
*/
abstract connect(): Promise<this>;
/**
* update provider network
* @param network Network type to be updated
*/
abstract updateNetwork(network: bsv.Networks.Network): Promise<boolean>;
/**
* @returns The network this provider is connected to.
*/
abstract getNetwork(): Promise<bsv.Networks.Network>;
/**
* @returns The fee rate for sending transactions through this provider.
*/
abstract getFeePerKb(): Promise<number>;
/**
* Get a best guess of the fee for a transaction.
* @param tx A transaction object to estimate.
* @returns The estimated fee in satoshis.
*/
async getEstimateFee(tx: bsv.Transaction): Promise<number> {
const copy = new bsv.Transaction(tx.uncheckedSerialize());
// use a copy bcoz `feePerKb` resets all the signatures for inputs.
copy.feePerKb(await this.getFeePerKb());
return copy.getEstimateFee();
}
// Executions
/**
* Send a raw transaction hex string.
* @param rawTxHex The raw transaction hex string to send.
* @returns A promise which resolves to the hash of the transaction that has been sent.
*/
abstract sendRawTransaction(rawTxHex: string): Promise<TxHash>;
/**
* Send a transaction object.
* @param tx The transaction object to send.
* @returns A promise which resolves to the hash of the transaction that has been sent.
* @throws If there is a problem with the `tx` object during serialization.
*/
sendTransaction(tx: bsv.Transaction): Promise<TxHash> {
// TODO: fix tx.serialize issue
return this.sendRawTransaction(tx.serialize({ disableIsFullySigned: true }));
}
// Queries
/**
* Get a transaction from the network.
* @param txHash The hash value of the transaction.
* @returns The query result with the transaction information.
*/
abstract getTransaction(txHash: TxHash): Promise<TransactionResponse>
/**
* Get a list of the P2PKH UTXOs.
* @param address The address of the returned UTXOs belongs to.
* @param options The optional query conditions, see details in `UtxoQueryOptions`.
* @returns A promise which resolves to a list of UTXO for the query options.
*/
abstract listUnspent(address: AddressOption, options?: UtxoQueryOptions): Promise<UTXO[]>;
/**
* Get the balance of BSVs in satoshis for an address.
* @param address The query address.
* @returns A promise which resolves to the address balance status.
*/
abstract getBalance(address: AddressOption): Promise<{ confirmed: number, unconfirmed: number }>;
/**
* Get a list of UTXO for a certain contract instance.
* @param genesisTxHash The hash value of deployment transaction of the contract instance.
* @param outputIndex The output index of the deployment transaction of the contract instance.
* @returns A promise which resolves to a list of transaction UTXO.
*/
abstract getContractUTXOs(genesisTxHash: TxHash, outputIndex: number): Promise<UTXO[]>;
// Inspection
readonly _isProvider: boolean;
/**
* Check if an object is a `Provider`
* @param value The target object
* @returns Returns `true` if and only if `object` is a Provider.
*/
static isProvider(value: any): value is Provider {
return !!(value && value._isProvider);
}
}
```
It is recommended that your provider implements all `abstract` methods. For non-`abstract` methods, the default implementation is usually sufficient.
### `Example: WhatsonchainProvider`
Let's walk through the process of implementing our own provider. In this example we'll implement a provider for [WhatsOnChain](https://whatsonchain.com) (WoC).
1. First let's implement the `isConnected()` and `connect()` functions. Because WoC doesn't need to maintan an open connection, not does it require any authentication by default, it's simply marked as connected by default. If your chosen provider does, here's probably the place to implement the connection logic.
```ts
isConnected(): boolean {
return true;
}
override async connect(): Promise<this> {
this.emit(ProviderEvent.Connected, true);
return Promise.resolve(this);
}
```
2. Next, we'll implement the network functions. Here, your providers selected network can be toggled. WoC supports both the Bitcoin mainnet along with testnet, so we don't do further checking:
```ts
override async updateNetwork(network: bsv.Networks.Network): Promise<boolean> {
this._network = network;
this.emit(ProviderEvent.NetworkChange, network);
return Promise.resolve(true);
}
override async getNetwork(): Promise<bsv.Networks.Network> {
return Promise.resolve(this._network);
}
```
If your provider is only meant for the testnet, you could do something like this:
```ts
override async updateNetwork(network: bsv.Networks.Network): Promise<boolean> {
if (network != bsv.Networks.testnet) {
throw new Error('Network not supported.')
}
this._network = network;
this.emit(ProviderEvent.NetworkChange, network);
return Promise.resolve(true);
}
```
3. Now let's set the transaction fee rate. In our example, we hard-code the value to be 50 satoshis per Kb:
```ts
override async getFeePerKb(): Promise<number> {
return Promise.resolve(50);
}
```
4. Let's implement the function that will send the transaction data to our provider:
```ts
override async sendRawTransaction(rawTxHex: string): Promise<TxHash> {
// 1 second per KB
const size = Math.max(1, rawTxHex.length / 2 / 1024); //KB
const timeout = Math.max(10000, 1000 * size);
try {
const res = await superagent.post(
`${this.apiPrefix}/tx/raw`
)
.timeout({
response: timeout, // Wait 5 seconds for the server to start sending,
deadline: 60000, // but allow 1 minute for the file to finish loading.
})
.set('Content-Type', 'application/json')
.send({ txhex: rawTxHex })
return res.body;
} catch (error) {
if (error.response && error.response.text) {
throw new Error(`WhatsonchainProvider ERROR: ${error.response.text}`)
}
throw new Error(`WhatsonchainProvider ERROR: ${error.message}`)
}
}
```
In the function we use the [`superagent`](https://www.npmjs.com/package/superagent) to send requests to WoC's HTTP endpoint. Check out their [docs](https://docs.taal.com/core-products/whatsonchain) for a description of the endpoints they provide.
5. Now we need to implement some queries. First let's implement the function to get a list of [UTXO's](https://wiki.bitcoinsv.io/index.php/UTXO) for a certain address:
```ts
override async listUnspent(
address: AddressOption,
options?: UtxoQueryOptions
): Promise<UTXO[]> {
const res = await superagent.get(`${this.apiPrefix}/address/${address}/unspent`);
const utxos: UTXO[] =
res.body.map(item => ({
txId: item.tx_hash,
outputIndex: item.tx_pos,
satoshis: item.value,
script: bsv.Script.buildPublicKeyHashOut(address).toHex(),
}));
if (options?.minSatoshis && utxos.reduce((s, u) => s + u.satoshis, 0) < options.minSatoshis) {
throw new Error(`WhatsonchainProvider ERROR: not enough utxos for the request amount of ${options.minSatoshis} on address ${address.toString()}`);
}
return utxos;
}
```
Next, we'll make the `getBalance` function parse out the addresses balance from the UTXO's:
```ts
override async getBalance(
address?: AddressOption
): Promise<{ confirmed: number, unconfirmed: number }> {
return this.listUnspent(address, { minSatoshis: 0 }).then(utxos => {
return {
confirmed: utxos.reduce((acc, utxo) => {
acc += utxo.satoshis;
return acc;
}, 0),
unconfirmed: 0
}
})
}
```
We also implement the function to query the raw transaction using the transactions ID:
```ts
override async getTransaction(txHash: string): Promise<TransactionResponse> {
try {
const res = await superagent.get(`${this.apiPrefix}/tx/${txHash}/hex`);
return new bsv.Transaction(res.text)
} catch (e) {
throw new Error(`WhatsonchainProvider ERROR: failed fetching raw transaction data: ${e.message}`);
}
}
```
Lastly, if our provider doesn't support a certain query, we can simply throw an error by default:
```ts
override async getContractUTXOs(genesisTxHash: string, outputIndex?: number): Promise<UTXO[]> {
throw new Error("Method #getContractUTXOs not implemented in WhatsonchainProvider.");
}
```
## Using the Provider
Providers are usually used by a `Signer`:
```ts
const provider = new WhatsonchainProvider(bsv.Networks.mainnet)
const signer = new TestWallet(privateKey, provider)
await contractInstance.connect(signer);
```
Here, the signer will use our `WhatsonchainProvider` for each Bitcoin network operation it needs. The next section describes signers and how we can implement a custom one.
|