Last active
August 24, 2023 11:26
-
-
Save zhfnjust/955fc91782c6bccfe647b15a6b912553 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 { PublicKey, Transaction } from "bsv"; | |
import { bsv, toHex, DEFAULT_SIGHASH_TYPE } from "scryptlib"; | |
import { Provider } from "../abstract-provider"; | |
import { Signer, SignTransactionOptions, SignatureRequest, SignatureResponse } from "../abstract-signer"; | |
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; | |
} | |
/** | |
* a [signer]{@link https://docs.scrypt.io/how-to-test-a-contract#signer } which implemented the protocol with the [sensilet wallet]{@link https://sensilet.com}, | |
* and dapps can use to interact with the Sensilet wallet | |
*/ | |
export class SensiletSigner extends Signer { | |
static readonly DEBUG_TAG = "SensiletSigner"; | |
private _target: SensiletWalletAPI; | |
private _address: AddressOption; | |
constructor(provider: Provider) { | |
super(provider); | |
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"); | |
} | |
} | |
/** | |
* Check if the wallet has been authenticated | |
* @returns {boolean} true | false | |
*/ | |
override isAuthenticated(): Promise<boolean> { | |
if (this._target) { | |
return this._target.isConnect(); | |
} | |
return Promise.resolve(false); | |
} | |
/** | |
* Request wallet authentication | |
* @returns A promise which resolves to if the wallet has been authenticated and the authenticate error message | |
*/ | |
override async requestAuth(): Promise<{ isAuthenticated: boolean, error: string }> { | |
let isAuthenticated: boolean = false | |
let error: string = '' | |
try { | |
await this.getConnectedTarget() | |
isAuthenticated = true | |
} catch (e) { | |
error = e.toString() | |
} | |
return Promise.resolve({ isAuthenticated, error }) | |
} | |
private async getConnectedTarget(): Promise<SensiletWalletAPI> { | |
const isAuthenticated = await this.isAuthenticated() | |
if (!isAuthenticated) { | |
// trigger connecting to sensilet account when it's not authorized. | |
try { | |
const addr = await this._target.requestAccount(); | |
this._address = bsv.Address.fromString(addr); | |
} catch (e) { | |
throw new Error('Sensilet requestAccount failed') | |
} | |
} | |
return this._target; | |
} | |
override async connect(provider?: Provider): Promise<this> { | |
// we should make sure sensilet is connected before we connect a provider. | |
const isAuthenticated = await this.isAuthenticated(); | |
if (!isAuthenticated) { | |
throw new Error('Sensilet 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; | |
} | |
override 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; | |
} | |
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) | |
} | |
override async getDefaultPubKey(): Promise<PublicKey> { | |
const sensilet = await this.getConnectedTarget(); | |
const pubKey = await sensilet.getPublicKey(); | |
return Promise.resolve(new bsv.PublicKey(pubKey)); | |
} | |
override async getPubKey(address: AddressOption): Promise<PublicKey> { | |
throw new Error(`Method ${this.constructor.name}#getPubKey not implemented.`); | |
} | |
override 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(); | |
} | |
override async signTransaction(tx: Transaction, options?: SignTransactionOptions): Promise<Transaction> { | |
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 { | |
prevTxId: toHex(input.prevTxId), | |
outputIndex: input.outputIndex, | |
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) => { | |
// 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; | |
} | |
override 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); | |
} | |
override 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 => { | |
let scriptHex = sigReq.scriptHex | |
if (!scriptHex) { | |
scriptHex = bsv.Script.buildPublicKeyHashOut(address).toHex() | |
} else if (sigReq.csIdx !== undefined) { | |
scriptHex = bsv.Script.fromHex(scriptHex).subScript(sigReq.csIdx).toHex() | |
} | |
return { | |
txHex: rawTxHex, | |
inputIndex: sigReq.inputIndex, | |
scriptHex, | |
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