Skip to content

Commit

Permalink
Merge pull request #259 from sCrypt-Inc/signer_remove_connect
Browse files Browse the repository at this point in the history
signer: remove connect
  • Loading branch information
zhfnjust authored Apr 24, 2024
2 parents 46c53ea + 37c8d67 commit 61af84c
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 87 deletions.
91 changes: 63 additions & 28 deletions docs/advanced/how-to-add-a-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,33 @@ export abstract class Provider extends EventEmitter {
*/
abstract isConnected(): boolean;

/**
* call this function in the constructor to initialize connection
*/
protected _initializeConnection() {
new Promise((resolve, reject) => {
setTimeout(() => {
this.connect().then((self) => {
resolve(self);
}, (error) => {
reject(error);
});
}, 0);
});
}

/**
* check if the connection is ready
*/
protected async _ready(): Promise<void> {
if (!this.isConnected()) {
try {
await this.connect();
} catch (error) { throw error }
}
}


/**
* 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.
Expand All @@ -47,7 +74,7 @@ export abstract class Provider extends EventEmitter {
* update provider network
* @param network Network type to be updated
*/
abstract updateNetwork(network: bsv.Networks.Network): Promise<boolean>;
abstract updateNetwork(network: bsv.Networks.Network): Promise<void>;

/**
* @returns The network this provider is connected to.
Expand Down Expand Up @@ -115,14 +142,6 @@ export abstract class Provider extends EventEmitter {
*/
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;
Expand All @@ -146,57 +165,80 @@ It is recommended that your provider implements all `abstract` methods. For non-
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.
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. We just need to check if the API responds correctly. If your chosen provider does, here's probably the place to implement the connection logic.

```ts
isConnected(): boolean {
return true;

private _network: bsv.Networks.Network;
private _isConnected: boolean = false;

constructor(network: bsv.Networks.Network) {
super();
this._network = network;
this._initializeConnection();
}

override isConnected(): boolean {
return this._isConnected;
}

override async connect(): Promise<this> {
this.emit(ProviderEvent.Connected, true);
return Promise.resolve(this);
try {
const res = await superagent.get(`${this.apiPrefix}/woc`)
.timeout(3000);
if (res.ok && res.text === "Whats On Chain") {
this._isConnected = true;
this.emit(ProviderEvent.Connected, true);
} else {
throw new Error(`${res.body.msg ? res.body.msg : res.text}`);
}
} catch (error) {
this._isConnected = false;
this.emit(ProviderEvent.Connected, false);
throw new Error(`connect failed: ${error.message?? "unknown error"}`);
}

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> {
override updateNetwork(network: bsv.Networks.Network): void {
this._network = network;
this.emit(ProviderEvent.NetworkChange, network);
return Promise.resolve(true);
}

override async getNetwork(): Promise<bsv.Networks.Network> {
override getNetwork(): 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> {
override updateNetwork(network: bsv.Networks.Network): void {
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);
return Promise.resolve(1);
}
```

4. Let's implement the function that will send the transaction data to our provider:

```ts
override async sendRawTransaction(rawTxHex: string): Promise<TxHash> {
await this._ready();
// 1 second per KB
const size = Math.max(1, rawTxHex.length / 2 / 1024); //KB
const timeout = Math.max(10000, 1000 * size);
Expand Down Expand Up @@ -229,7 +271,7 @@ override async listUnspent(
address: AddressOption,
options?: UtxoQueryOptions
): Promise<UTXO[]> {

await this._ready();
const res = await superagent.get(`${this.apiPrefix}/address/${address}/unspent`);
const utxos: UTXO[] =
res.body.map(item => ({
Expand Down Expand Up @@ -280,13 +322,6 @@ override async getTransaction(txHash: string): Promise<TransactionResponse> {
```


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`:
Expand Down
92 changes: 39 additions & 53 deletions docs/advanced/how-to-add-a-signer.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ If you want to implement your own signer, you must inherit from the base class `


```ts
/**
* A `Signer` is a class which in some way directly or indirectly has access to a private key, which can sign messages and transactions to authorize the network to perform operations.
*/


/**
* A `Signer` is a class which in some way directly or indirectly has access to a private key, which can sign messages and transactions to authorize the network to perform operations.
*/
Expand All @@ -33,6 +38,8 @@ export abstract class Signer {
this.provider = provider;
}

// Authentication

abstract getNetwork(): Promise<bsv.Networks.Network>;

/**
Expand All @@ -48,28 +55,26 @@ export abstract class Signer {
abstract requestAuth(): Promise<{ isAuthenticated: boolean, error: string }>;

/**
* Connect a provider to `this`.
* set provider
* @param provider The target provider.
* @returns
*/
abstract connect(provider: Provider): Promise<this>;

// Account
abstract setProvider(provider: Provider): void;

/**
*
* @returns A promise which resolves to the address to the default private key of the signer.
* @returns A promise which resolves to the public key of the default private key of the signer.
*/
abstract getDefaultAddress(): Promise<bsv.Address>;
abstract getDefaultPubKey(): Promise<bsv.PublicKey>;

/**
*
* @returns A promise which resolves to the public key of the default private key of the signer.
*
* @returns A promise which resolves to the address to the default private key of the signer.
*/
abstract getDefaultPubKey(): Promise<bsv.PublicKey>;
abstract getDefaultAddress(): Promise<bsv.Address>;

/**
*
*
* @param address The request address, using the default address if omitted.
* @returns The public key result.
* @throws If the private key for the address does not belong this signer.
Expand All @@ -87,16 +92,18 @@ export abstract class Signer {
* @throws If any input of the transaction can not be signed properly.
*/
async signRawTransaction(rawTxHex: string, options: SignTransactionOptions): Promise<string> {
...
const signedTx = await this.signTransaction(new bsv.Transaction(rawTxHex), options);
return signedTx.toString();
}

/**
* Sign a transaction object.
* Sign a transaction object. By default only signs inputs, which are unlocking P2PKH UTXO's.
* @param tx The transaction object to sign.
* @param options The options for signing, see the details of `SignTransactionOptions`.
* @returns A promise which resolves to the signed transaction object.
*/
async signTransaction(tx: bsv.Transaction, options?: SignTransactionOptions): Promise<bsv.Transaction>{
async signTransaction(tx: bsv.Transaction, options?: SignTransactionOptions): Promise<bsv.Transaction> {

...
}

Expand All @@ -123,17 +130,14 @@ export abstract class Signer {
*/
get connectedProvider(): Provider {
if (!this.provider) {
throw new Error(`the provider of singer ${this.constructor.name} is not set yet!`);
}
if (!this.provider.isConnected()) {
throw new Error(`the provider of singer ${this.constructor.name} is not connected yet!`);
throw new Error(`the provider of signer ${this.constructor.name} is not set yet!`);
}

return this.provider;
}

/**
* Sign the transaction, then broadcast the transaction
* Sign transaction and broadcast it
* @param tx A transaction is signed and broadcast
* @param options The options for signing, see the details of `SignTransactionOptions`.
* @returns A promise which resolves to the transaction id.
Expand All @@ -152,7 +156,7 @@ export abstract class Signer {
* @returns A promise which resolves to a list of UTXO for the query options.
*/
listUnspent(address: AddressOption, options?: UtxoQueryOptions): Promise<UTXO[]> {
// default implemention using provider, can be overrided.
// Default implementation using provider. Can be overriden.
return this.connectedProvider.listUnspent(address, options);
}

Expand All @@ -161,8 +165,9 @@ export abstract class Signer {
* @param address The query address.
* @returns A promise which resolves to the address balance status.
*/
getBalance(address?: AddressOption): Promise<{ confirmed: number, unconfirmed: number }> {
// default implemention using provider, can be overrided.
async getBalance(address?: AddressOption): Promise<{ confirmed: number, unconfirmed: number }> {
// Default implementation using provider. Can be overriden.
address = address ? address : await this.getDefaultAddress();
return this.connectedProvider.getBalance(address);
}

Expand All @@ -176,6 +181,12 @@ export abstract class Signer {
return !!(value && value._isSigner);
}

/**
* Align provider's network after the signer is authenticated
*/
async alignProviderNetwork() {
...
}
}
```

Expand Down Expand Up @@ -232,35 +243,8 @@ override async requestAuth(): Promise<{ isAuthenticated: boolean, error: string
}
```

3. In the `connect` method, you usually attempt to connect to a provider and save it:

```ts
override async connect(provider?: Provider): Promise<this> {
// we should make sure panda is connected before we connect a provider.
const isAuthenticated = await this.isAuthenticated();

if (!isAuthenticated) {
throw new Error('panda is not connected!');
}

if (provider) {
if (!provider.isConnected()) {
await provider.connect();
}
this.provider = provider;
} else {
if (this.provider) {
await this.provider.connect();
} else {
throw new Error(`No provider found`);
}
}

return this;
}
```

4. Returns the address to the default private key of the wallet in `getDefaultAddress`:
3. Returns the address to the default private key of the wallet in `getDefaultAddress`:

```ts
/**
Expand Down Expand Up @@ -295,7 +279,7 @@ override async getDefaultAddress(): Promise<bsv.Address> {
}
```

5. Returns the public key to the default private key of the wallet in `getDefaultPubKey`:
4. Returns the public key to the default private key of the wallet in `getDefaultPubKey`:

```ts
override async getDefaultPubKey(): Promise<bsv.PublicKey> {
Expand All @@ -305,15 +289,16 @@ override async getDefaultPubKey(): Promise<bsv.PublicKey> {
}
```

6. Since Panda is a single-address wallet, we simply ignore the `getPubKey` method:
5. Since Panda is a single-address wallet, we simply ignore the `getPubKey` method:

```ts
override async getPubKey(address: AddressOption): Promise<PublicKey> {
throw new Error(`Method ${this.constructor.name}#getPubKey not implemented.`);
}
```

7. Both `signTransaction` and `signRawTransaction` sign the transaction, and are already implemented in the base class. You just need to implement the `getSignatures` function. The following code calls panda's `getSignatures` API to request a wallet signature.
6. Both `signTransaction` and `signRawTransaction` sign the transaction, and are already implemented in the base class. You just need to implement the `getSignatures` function. The following code calls panda's `getSignatures` API to request a wallet signature.


```ts
/**
Expand Down Expand Up @@ -350,7 +335,8 @@ override async getSignatures(rawTxHex: string, sigRequests: SignatureRequest[]):
}
```

8. Panda supports signing messages, if your wallet does not support it, you can throw an exception in the `signMessage` function:

7. Panda supports signing messages, if your wallet does not support it, you can throw an exception in the `signMessage` function:

```ts
override async signMessage(message: string, address?: AddressOption): Promise<string> {
Expand Down
Loading

0 comments on commit 61af84c

Please sign in to comment.