Skip to content

Instantly share code, notes, and snippets.

@xhliu
Created March 6, 2023 17:48
Show Gist options
  • Save xhliu/73104028deaf95c8b6665bf96496fe11 to your computer and use it in GitHub Desktop.
Save xhliu/73104028deaf95c8b6665bf96496fe11 to your computer and use it in GitHub Desktop.
import { Networks, PublicKey, Transaction } from "bsv";
import { bsv } from "scryptlib";
import { Provider } from "../abstract-provider";
import { Signer, SignTransactionOptions, SignatureRequest, SignatureResponse } from "../abstract-signer";
import { DefaultProvider } from "../providers/default-provider";
import { AddressOption} from "../types";
import { parseAddresses } from "../utils"
// see https://doc.sensilet.com/guide/sensilet-api.html
interface SensiletWalletAPI {
isConnect(): Promise<boolean>;
requestAccount(): Promise<string>;
exitAccount(): void;
signTx(options: {
list: { txHex: string, address: string; inputIndex: number, scriptHex: string; satoshis: number, sigtype: number }[]
}): Promise<{
sigList: Array<{ publicKey: string, r: string, s: string, sig: string }>
}>;
// TODO: add rests
getAddress(): Promise<string>;
getPublicKey(): Promise<string>;
signMessage(msg: string): Promise<string>;
getBsvBalance(): Promise<{
address: string,
balance: { confirmed: number, unconfirmed: number, total: number }
}>;
signTransaction(txHex: string, inputInfos: {
inputIndex: number;
scriptHex: string;
satoshis: number;
sighashType: number;
address: number | string;
}[]): Promise<SigResult[]>;
}
interface SigResult {
sig: string;
publicKey: string;
}
// TODO: export this default value from scryptlib
const DEFAULT_SIGHASH_TYPE = bsv.crypto.Signature.ALL;
export class SensiletSigner extends Signer {
static readonly DEBUG_TAG = "SensiletSigner";
private _target: SensiletWalletAPI;
private _address: AddressOption;
constructor(provider?: Provider) {
super(provider || new DefaultProvider());
if (typeof (window as any).sensilet !== 'undefined') {
console.log(SensiletSigner.DEBUG_TAG, 'Sensilet is installed!');
this._target = (window as any).sensilet;
} else {
console.warn(SensiletSigner.DEBUG_TAG, "sensilet is not installed");
}
}
/**
* Get an object that can directly interact with the Sensilet wallet
* @returns SensiletWalletAPI or undefined if the provider has not yet established a connection with the wallet
*/
getSensilet(): SensiletWalletAPI | undefined {
return this._target;
}
/**
* Check if the wallet is connected
* @returns {boolean} true | false
*/
isSensiletConnected(): Promise<boolean> {
if(this._target) {
return this._target.isConnect();
}
return Promise.resolve(false);
}
/**
* 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<SensiletWalletAPI> {
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();
}
async connect(provider: Provider): Promise<this> {
// 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()) {
await provider.connect();
}
const network = await this.getNetwork();
await provider.updateNetwork(network);
this.provider = provider;
return this;
}
async getDefaultAddress(): Promise<bsv.Address> {
const sensilet = await this.getConnectedTarget();
const address = await sensilet.getAddress();
return bsv.Address.fromString(address);
}
async getNetwork(): Promise<bsv.Networks.Network> {
const address = await this.getDefaultAddress();
return address.network;
}
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)
}
async getDefaultPubKey(): Promise<PublicKey> {
const sensilet = await this.getConnectedTarget();
const pubKey = await sensilet.getPublicKey();
return Promise.resolve(new bsv.PublicKey(pubKey));
}
async getPubKey(address: AddressOption): Promise<PublicKey> {
throw new Error(`Method ${this.constructor.name}#getPubKey not implemented.`);
}
async signRawTransaction(rawTxHex: string, options: SignTransactionOptions): Promise<string> {
const sigReqsByInputIndex: Map<number, SignatureRequest> = (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();
}
async signTransaction(tx: Transaction, options?: SignTransactionOptions): Promise<Transaction> {
const network = await this.getNetwork();
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);
tx.inputs.forEach((input, inputIndex) => {
// TODO: multisig?
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;
}
async signMessage(message: string, address?: AddressOption): Promise<string> {
if (address) {
throw new Error(`${this.constructor.name}#signMessge with \`address\` param is not supported!`);
}
const sensilet = await this.getConnectedTarget();
return sensilet.signMessage(message);
}
async getSignatures(rawTxHex: string, sigRequests: SignatureRequest[]): Promise<SignatureResponse[]> {
const network = await this.getNetwork()
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();
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
}
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment