Created
March 6, 2023 17:48
-
-
Save xhliu/73104028deaf95c8b6665bf96496fe11 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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