--- sidebar_position: 2 --- # How to Add a Signer As described in [this section](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#provider), a signer is an abstraction of private keys, which can be used to sign messages and transactions. A simple signer would be a single private key, while a complex signer is a wallet. `sCrypt` provides the following signers by default: 1. `TestWallet` : a simple wallet that can hold multiple private keys, with in-memory utxo management. Should only be used for testing. 2. `SensiletSigner`: a signer powered by the popular smart contract wallet [Sensilet](https://sensilet.com/). Can be used in production. ## Implementation ### Base Class `Signer` If you want to implement your own signer, you must inherit from the base class `Signer`. ```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. */ export abstract class Signer { provider?: Provider; readonly _isSigner: boolean; constructor(provider?: Provider) { this._isSigner = true; this.provider = provider; } /** * Connect a provider to `this`. * @param provider The target provider. * @returns */ abstract connect(provider: Provider): Promise; // Account /** * * @returns A promise which resolves to the address to the default private key of the signer. */ abstract getDefaultAddress(): Promise; /** * * @returns A promise which resolves to the public key of the default private key of the signer. */ abstract getDefaultPubKey(): Promise; /** * * @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. */ abstract getPubKey(address?: AddressOption): Promise; // Signing /** * Sign a raw transaction hex string. * * @param rawTxHex The raw transaction hex to sign. * @param options The options for signing, see the details of `SignTransactionOptions`. * @returns A promise which resolves to the signed transaction hex string. * @throws If any input of the transaction can not be signed properly. */ abstract signRawTransaction(rawTxHex: string, options: SignTransactionOptions): Promise; /** * Sign a transaction object. * @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. */ abstract signTransaction(tx: bsv.Transaction, options?: SignTransactionOptions): Promise; /** * Sign a message string. * @param message The message to be signed. * @param address The optional address whose private key will be used to sign `message`, using the default private key if omitted. * @returns A promise which resolves to the signautre of the message. */ abstract signMessage(message: string, address?: AddressOption): Promise; /** * Get the requested transaction signatures for the raw transaction. * @param rawTxHex The raw transaction hex to get signatures from. * @param sigRequests The signature requst informations, see details in `SignatureRequest`. * @returns A promise which resolves to a list of `SignatureReponse` corresponding to `sigRequests`. */ abstract getSignatures(rawTxHex: string, sigRequests: SignatureRequest[]): Promise; /** * Get the connected provider. * @returns the connected provider. * @throws if no provider is connected to `this`. */ 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!`); } return this.provider; } /** * Sign the transaction, then broadcast the transaction * @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. */ async signAndsendTransaction(tx: bsv.Transaction, options?: SignTransactionOptions): Promise { await tx.sealAsync(); const signedTx = await this.signTransaction(tx, options); await this.connectedProvider.sendTransaction(signedTx); return signedTx; }; /** * 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. */ listUnspent(address: AddressOption, options?: UtxoQueryOptions): Promise { // default implemention using provider, can be overrided. return this.connectedProvider.listUnspent(address, options); } /** * 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. */ getBalance(address?: AddressOption): Promise<{ confirmed: number, unconfirmed: number }> { // default implemention using provider, can be overrided. return this.connectedProvider.getBalance(address); } // Inspection /** * Check if an object is a `Signer` * @param value The target object * @returns Returns `true` if and only if `object` is a Provider. */ static isSigner(value: any): value is Signer { return !!(value && value._isSigner); } } ``` It is recommended that your signer implements all `abstract` methods. For non-`abstract` methods, the default implementation is usually sufficient. ### `Example: SensiletSigner` Next, we use the [Sensilet wallet](https://sensilet.com/) as an example to show how to implement a `SensiletSigner`. 1. In the `connect` method, you usually attempt to connect to a provider and save it: ```ts override async connect(provider: Provider): Promise { // we should make sure sensilet is connected before we connect a provider. const isSensiletConnected = await this.isSensiletConnected(); if(!isSensiletConnected) { Promise.reject(new Error('Sensilet is not connected!')) } if(!provider.isConnected()) { // connect the provider await provider.connect(); } this.provider = provider; return this; } ``` 2. Returns the address to the default private key of the wallet in `getDefaultAddress`: ```ts /** * Get an object that can directly interact with the Sensilet wallet, * if there is no connection with the wallet, it will request to establish a connection. * @returns SensiletWalletAPI */ async getConnectedTarget(): Promise { const isSensiletConnected = await this.isSensiletConnected(); if (!isSensiletConnected) { // trigger connecting to sensilet account when it's not connected. try { const addr = await this._target.requestAccount(); this._address = bsv.Address.fromString(addr); } catch (e) { throw new Error('Sensilet requestAccount failed') } } return this.getSensilet(); } override async getDefaultAddress(): Promise { // const sensilet = await this.getConnectedTarget(); const address = await sensilet.getAddress(); return bsv.Address.fromString(address); } ``` 3. Returns the public key to the default private key of the wallet in `getDefaultPubKey`: ```ts override async getDefaultPubKey(): Promise { const sensilet = await this.getConnectedTarget(); const pubKey = await sensilet.getPublicKey(); return Promise.resolve(new bsv.PublicKey(pubKey)); } ``` 4. Since Sensilet is a single-address wallet, we simply ignore the `getPubKey` method: ```ts override async getPubKey(address: AddressOption): Promise { throw new Error(`Method ${this.constructor.name}#getPubKey not implemented.`); } ``` 5. Both `signTransaction` and `signRawTransaction` sign the transaction, but their parameters are different. `signRawTransaction` converts the parameters and delegates the implementation of the signing to `signTransaction`. The following are types used in these two functions: ```ts /** * `SignatureRequest` contains required informations for a signer to sign a certain input of a transaction. */ export interface SignatureRequest { /** The index of input to sign. */ inputIndex: number; /** The previous output satoshis value of the input to spend. */ satoshis: number; /** The address(es) of corresponding private key(s) required to sign the input. */ address: AddressesOption; /** The previous output script of input, default value is a P2PKH locking script for the `address` if omitted. */ scriptHex?: string; /** The sighash type, default value is `SIGHASH_ALL | SIGHASH_FORKID` if omitted. */ sigHashType?: number; /** The extra information for signing. */ data?: unknown; } /** * `SignatureResponse` contains the signing result corresponding to a `SignatureRequest`. */ export interface SignatureResponse { /** The index of input. */ inputIndex: number; /** The signature.*/ sig: string; /** The public key bound with the `sig`. */ publicKey: string; /** The sighash type, default value is `SIGHASH_ALL | SIGHASH_FORKID` if omitted. */ sigHashType: number; } /** * `SignTransactionOptions` is the options can be provided when signing a transaction. */ export interface SignTransactionOptions { /** The `SignatureRequest` for the some inputs of the transaction. */ sigRequests?: SignatureRequest[]; /** The address(es) whose corresponding private key(s) should be used to sign the tx. */ address?: AddressesOption; } ``` `signTransaction` will convert the above parameter types to the parameter types required by the [sensilet api](https://doc.sensilet.com/guide/sensilet-api.html#signtx). And call the sensilet api to complete the signature, which is implemented in `getSignatures` function. ```ts override async signRawTransaction(rawTxHex: string, options: SignTransactionOptions): Promise { // convert `rawTxHex` to a transation object const sigReqsByInputIndex: Map = (options?.sigRequests || []).reduce((m, sigReq) => { m.set(sigReq.inputIndex, sigReq); return m; }, new Map()); const tx = new bsv.Transaction(rawTxHex); tx.inputs.forEach((_, inputIndex) => { const sigReq = sigReqsByInputIndex.get(inputIndex); if (!sigReq) { throw new Error(`\`SignatureRequest\` info should be provided for the input ${inputIndex} to call #signRawTransaction`) } const script = sigReq.scriptHex ? new bsv.Script(sigReq.scriptHex) : bsv.Script.buildPublicKeyHashOut(sigReq.address.toString()); // set ref output of the input tx.inputs[inputIndex].output = new bsv.Transaction.Output({ script, satoshis: sigReq.satoshis }) }); const signedTx = await this.signTransaction(tx, options); return signedTx.toString(); } override async signTransaction(tx: Transaction, options?: SignTransactionOptions): Promise { const network = await this.getNetwork(); // Generate default `sigRequests` if not passed by user const sigRequests: SignatureRequest[] = options?.sigRequests?.length ? options.sigRequests : tx.inputs.map((input, inputIndex) => { const useAddressToSign = options && options.address ? options.address : input.output?.script.isPublicKeyHashOut() ? input.output.script.toAddress(network) : this._address; return { inputIndex, satoshis: input.output?.satoshis, address: useAddressToSign, scriptHex: input.output?.script?.toHex(), sigHashType: DEFAULT_SIGHASH_TYPE, } }) const sigResponses = await this.getSignatures(tx.toString(), sigRequests); // Set the acquired signature as an unlocking script for the transaction tx.inputs.forEach((input, inputIndex) => { const sigResp = sigResponses.find(sigResp => sigResp.inputIndex === inputIndex); if (sigResp && input.output?.script.isPublicKeyHashOut()) { var unlockingScript = new bsv.Script("") .add(Buffer.from(sigResp.sig, 'hex')) .add(Buffer.from(sigResp.publicKey, 'hex')); input.setScript(unlockingScript) } }) return tx; } /** * Get signatures with sensilet api * @param rawTxHex a transation raw hex * @param sigRequests a `SignatureRequest` array for the some inputs of the transaction. * @returns a `SignatureResponse` array */ async getSignatures(rawTxHex: string, sigRequests: SignatureRequest[]): Promise { const network = await this.getNetwork() // convert `sigRequests` to the parameter type required by sensilet `signTx` api const inputInfos = sigRequests.flatMap((sigReq) => { const addresses = parseAddresses(sigReq.address, network); return addresses.map(address => { return { txHex: rawTxHex, inputIndex: sigReq.inputIndex, scriptHex: sigReq.scriptHex || bsv.Script.buildPublicKeyHashOut(address).toHex(), satoshis: sigReq.satoshis, sigtype: sigReq.sigHashType || DEFAULT_SIGHASH_TYPE, address: address.toString() } }); }); const sensilet = await this.getConnectedTarget(); // call sensilet `signTx` api to sign transaction // https://doc.sensilet.com/guide/sensilet-api.html#signtx const sigResults = await sensilet.signTx({ list: inputInfos }); return inputInfos.map((inputInfo, idx) => { return { inputIndex: inputInfo.inputIndex, sig: sigResults.sigList[idx].sig, publicKey: sigResults.sigList[idx].publicKey, sigHashType: sigRequests[idx].sigHashType || DEFAULT_SIGHASH_TYPE } }) } ``` 6. Sensilet 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 { if (address) { throw new Error(`${this.constructor.name}#signMessge with \`address\` param is not supported!`); } const sensilet = await this.getConnectedTarget(); return sensilet.signMessage(message); } ``` So far, we have implemented all abstract methods. The remaining non-abstract methods can reuse the default implementation, that is, delegating to the connected [provider](../how-to-deploy-and-call-a-contract/how-to-deploy-and-call-a-contract.md#provider). If you have a customized implementation, you can override them. For example, we can use the [Sensilet api `getBsvBalance`](https://doc.sensilet.com/guide/sensilet-api.html#getbsvbalance) to obtain the balance of an address. ```ts override getBalance(address?: AddressOption): Promise<{ confirmed: number, unconfirmed: number }> { if(address) { return this.connectedProvider.getBalance(address); } return this.getConnectedTarget().then(target => target.getBsvBalance()).then(r => r.balance) } ``` Now we have implemented `SensiletSigner`. The full code is [here](https://gist.github.com/xhliu/73104028deaf95c8b6665bf96496fe11#file-sensiletsigner-ts-L44). ## Use your signer Just connect your signer to a smart contract instance like any other signers: ```ts // declare your signer const your_signer = new YourSigner(new DefaultProvider()); // connect the signer to the contract instance await instance.connect(your_signer); ```