Last active
October 1, 2023 01:59
-
-
Save zhfnjust/32050d4598cde921ba160939ca9f8b44 to your computer and use it in GitHub Desktop.
dotwallet-signer.ts
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, DEFAULT_SIGHASH_TYPE, toHex } from "scryptlib"; | |
import { Provider, UtxoQueryOptions } from "../abstract-provider"; | |
import { Signer, SignTransactionOptions, SignatureRequest, SignatureResponse } from "../abstract-signer"; | |
import { AddressOption, UTXO } from "../types"; | |
import { filterUTXO, parseAddresses } from "../utils" | |
import superagent from 'superagent'; | |
const DAPP_API_PATHS = { | |
dapp_list_unspent: `/v1/grandet_dapp/dapp_list_unspent`, | |
dapp_list_unspent_by_address: `/v1/grandet_dapp/dapp_list_unspent_by_address`, | |
dapp_sign_raw_transaction: `/v1/grandet_dapp/dapp_sign_raw_transaction`, | |
dapp_get_signature: `/v1/grandet_dapp/dapp_get_signature`, | |
dapp_get_balance: `/v1/grandet_dapp/dapp_get_balance`, | |
dapp_send_raw_transaction: `/v1/grandet_dapp/dapp_send_raw_transaction`, | |
dapp_get_raw_change_address: `/v1/grandet_dapp/dapp_get_raw_change_address`, | |
dapp_get_public_key: `/v1/grandet_dapp/dapp_get_public_key`, | |
get_access_token: `/v1/oauth2/get_access_token` | |
}; | |
/** | |
* This option can be used in both development environment and production environment. | |
* See [access-token]{@link https://oauth.net/2/access-tokens} and [DotWallet APIs for authorization]{@link https://developers.dotwallet.com/documents/en/#authorization} to known how to get a access token. | |
*/ | |
function handleRes(res: any) { | |
if (res.ok) { | |
const body = res.body ? res.body : JSON.parse(res.text); | |
const { code, data, msg } = body; | |
if (code === 0) { | |
return data; | |
} else if (code === 75000) { | |
window.localStorage.removeItem('access_token'); | |
} | |
throw new Error(`error response, code = ${code}, msg: ${msg}`) | |
} else { | |
throw new Error(`error response`) | |
} | |
} | |
const API_DOTWALLET = `https://api.ddpurse.com`; | |
/** | |
* a [signer]{@link https://docs.scrypt.io/how-to-test-a-contract#signer } which implemented the protocol with the [dotwallet]{@link https://www.dotwallet.com/en}, | |
* and dapps can use to interact with the dotwallet. | |
*/ | |
export class DotwalletSigner extends Signer { | |
static readonly DEBUG_TAG = "DotwalletSigner"; | |
private accessToken: string; | |
private _address: AddressOption; | |
private state: string; | |
private sender = { | |
"appid": "bsv_coin_regular", | |
"user_index": 0 | |
} | |
private default_public_key: bsv.PublicKey; | |
private utxos_public_key: Map<string, string> = new Map<string, string>(); | |
constructor(accessToken: string, provider: Provider) { | |
super(provider); | |
this.accessToken = accessToken; | |
} | |
/** | |
* Check if the wallet has been authenticated | |
* @returns {boolean} true | false | |
*/ | |
override isAuthenticated(): Promise<boolean> { | |
if (this.accessToken) { | |
return Promise.resolve(true); | |
} | |
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 }> { | |
if (this.accessToken) { | |
return Promise.resolve({ | |
isAuthenticated: true, | |
error: '' | |
}); | |
} | |
return Promise.resolve({ | |
isAuthenticated: false, | |
error: '' | |
}); | |
} | |
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('Dotwallet 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`); | |
} | |
} | |
await this.getDefaultPubKey(); | |
return this; | |
} | |
override async getDefaultAddress(): Promise<bsv.Address> { | |
const public_key = await this.getDefaultPubKey(); | |
return public_key.toAddress(bsv.Networks.mainnet); | |
} | |
async getNetwork(): Promise<bsv.Networks.Network> { | |
const address = await this.getDefaultAddress(); | |
return address.network; | |
} | |
override async getBalance(address?: AddressOption): Promise<{ confirmed: number, unconfirmed: number }> { | |
if (address) { | |
return this.connectedProvider.getBalance(address); | |
} | |
const res = await superagent.post(`${API_DOTWALLET}${DAPP_API_PATHS.dapp_get_balance}`) | |
.set('Content-Type', 'application/json') | |
.set('Authorization', `Bearer ${this.accessToken}`) | |
.send({ | |
"sender": this.sender, | |
}) | |
const data = handleRes(res); | |
const { confirm, unconfirm } = data; | |
return { | |
confirmed: confirm, | |
unconfirmed: unconfirm | |
}; | |
} | |
override async getDefaultPubKey(): Promise<PublicKey> { | |
if (this.default_public_key) { | |
return this.default_public_key; | |
} | |
const res = await superagent.post( | |
`${API_DOTWALLET}${DAPP_API_PATHS.dapp_get_public_key}` | |
) | |
.set('Content-Type', 'application/json') | |
.set('Authorization', `Bearer ${this.accessToken}`) | |
.send({ | |
"sender": this.sender, | |
}) | |
const data = handleRes(res); | |
const { public_key } = data; | |
this.default_public_key = bsv.PublicKey.fromString(public_key); | |
return this.default_public_key; | |
} | |
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) => { | |
let useAddressToSign = options && options.address ? options.address : this._address | |
if (input.output?.script.isPublicKeyHashOut()) { | |
useAddressToSign = input.output.script.toAddress(network) | |
} | |
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> { | |
throw new Error(`Method ${this.constructor.name}#signMessage not implemented.`); | |
} | |
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, | |
prevTxId: sigReq.prevTxId, | |
outputIndex: sigReq.outputIndex, | |
address: address.toString() | |
} | |
}); | |
}); | |
return Promise.all(inputInfos.map(async inputInfo => { | |
const res = await superagent.post(`${API_DOTWALLET}${DAPP_API_PATHS.dapp_get_signature}`) | |
.set('Content-Type', 'application/json') | |
.set('Authorization', `Bearer ${this.accessToken}`) | |
.send({ | |
sender: this.sender, | |
input_index: inputInfo.inputIndex, | |
sig_type: inputInfo.sigtype, | |
rawtx: rawTxHex, | |
addr: inputInfo.address, | |
}); | |
const data = handleRes(res); | |
const { hex_signature } = data; | |
let publicKey = this.utxos_public_key.get(`${inputInfo.prevTxId}:${inputInfo.outputIndex}`); | |
if (!publicKey) { | |
publicKey = toHex(this.default_public_key); | |
} | |
return { | |
inputIndex: inputInfo.inputIndex, | |
sig: hex_signature, | |
publicKey: publicKey, | |
sigHashType: inputInfo.sigtype | |
} | |
})) | |
} | |
/** | |
* 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. | |
*/ | |
async listUnspent(address: AddressOption, options?: UtxoQueryOptions): Promise<UTXO[]> { | |
const res = await superagent.post(`${API_DOTWALLET}${DAPP_API_PATHS.dapp_list_unspent}`) | |
.set('Content-Type', 'application/json') | |
.set('Authorization', `Bearer ${this.accessToken}`) | |
.send({ | |
"sender": this.sender, | |
"min_amount": 0 | |
}) | |
const data = handleRes(res); | |
const utxos: UTXO[] = data.utxos.map((utxo: any) => ({ | |
txId: utxo.tx_hash, | |
outputIndex: utxo.output_index, | |
satoshis: utxo.satoshis, | |
script: utxo.script, | |
address: utxo.addr, | |
pubkey: utxo.pubkey | |
})); | |
utxos.forEach(utxo => { | |
this.utxos_public_key.set(`${utxo.txId}:${utxo.outputIndex}`, utxo['pubkey']); | |
}); | |
if (options) { | |
return filterUTXO(utxos, options) | |
} | |
return utxos; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment