Skip to content

Instantly share code, notes, and snippets.

@zhfnjust
Last active October 1, 2023 01:59
Show Gist options
  • Save zhfnjust/32050d4598cde921ba160939ca9f8b44 to your computer and use it in GitHub Desktop.
Save zhfnjust/32050d4598cde921ba160939ca9f8b44 to your computer and use it in GitHub Desktop.
dotwallet-signer.ts
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