Skip to content

Instantly share code, notes, and snippets.

@utxo-detective
Created September 13, 2024 17:51
Show Gist options
  • Select an option

  • Save utxo-detective/fcfa4cffc81c6826211616d167e5e6c2 to your computer and use it in GitHub Desktop.

Select an option

Save utxo-detective/fcfa4cffc81c6826211616d167e5e6c2 to your computer and use it in GitHub Desktop.
sCrypt Unisat Provider
import {
Provider,
Signer,
SignatureRequest,
SignatureResponse,
AddressOption
} from 'scrypt-ts'
import { bsv } from 'scryptlib'
import { Transaction } from '@scure/btc-signer'
// Extend the existing Window interface declaration
declare global {
interface Window {
unisat: UnisatAPI;
}
}
interface UnisatBalance {
confirmed: number;
unconfirmed: number;
total: number;
}
export interface UnisatAPI {
getAccounts: () => Promise<string[]>;
requestAccounts: () => Promise<string[]>;
on: (event: string, handler: (accounts: string[]) => void) => void;
removeListener: (event: string, handler: (accounts: string[]) => void) => void;
getNetwork(): Promise<string>;
getPublicKey(): Promise<string>;
getBalance(): Promise<UnisatBalance>;
signMessage(message: string, type: 'bsv' | 'ecdsa'): Promise<string>;
signPsbt(psbtHex: string): Promise<string>;
}
/**
* A Signer implementation for the Unisat wallet
*/
export class UnisatSigner extends Signer {
static readonly DEBUG_TAG = 'UnisatSigner';
private _target: UnisatAPI | null = null;
constructor(provider: Provider) {
super(provider);
}
// Add this method to implement the abstract member
setProvider(provider: Provider): void {
this.provider = provider;
}
override async isAuthenticated(): Promise<boolean> {
this._initTarget();
const accounts = await this._target!.getAccounts();
return accounts.length > 0;
}
override async requestAuth(): Promise<{ isAuthenticated: boolean; error: string }> {
let isAuthenticated = false;
let error = '';
try {
await this.getConnectedTarget();
await this.alignProviderNetwork();
isAuthenticated = true;
} catch (e) {
error = e instanceof Error ? e.message : String(e);
}
return { isAuthenticated, error };
}
private _initTarget() {
if (this._target) {
return;
}
if (typeof window !== 'undefined' && 'unisat' in window) {
const unisatWallet = window.unisat;
if (this.isValidUnisatAPI(unisatWallet)) {
this._target = unisatWallet;
} else {
throw new Error('Unisat wallet does not implement the expected API');
}
} else {
throw new Error('Unisat wallet is not installed');
}
}
private isValidUnisatAPI(obj: unknown): obj is UnisatAPI {
if (typeof obj !== 'object' || obj === null) {
return false;
}
const requiredMethods: (keyof UnisatAPI)[] = [
'requestAccounts',
'getAccounts',
'getNetwork',
'getPublicKey',
'getBalance',
'signMessage',
'signPsbt',
'on',
'removeListener'
];
return requiredMethods.every(
(method) => typeof (obj as Record<string, unknown>)[method] === 'function'
);
}
private async getConnectedTarget(): Promise<UnisatAPI> {
const isAuthenticated = await this.isAuthenticated();
if (!isAuthenticated) {
try {
this._initTarget();
await this._target!.requestAccounts();
} catch (e) {
throw new Error(`Unisat requestAccounts failed: ${e}`);
}
}
return this._target!;
}
async connect(provider?: Provider): Promise<this> {
const isAuthenticated = await this.isAuthenticated();
if (!isAuthenticated) {
throw new Error('Unisat wallet is not connected!');
}
if (provider) {
if (!provider.isConnected()) {
await provider.connect();
}
this.setProvider(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 unisat = await this.getConnectedTarget();
const addresses = await unisat.getAccounts();
return bsv.Address.fromString(addresses[0]);
}
async getNetwork(): Promise<bsv.Networks.Network> {
const unisat = await this.getConnectedTarget();
const network = await unisat.getNetwork();
return network === 'mainnet' ? bsv.Networks.mainnet : bsv.Networks.testnet;
}
override async getBalance(
address?: AddressOption
): Promise<{ confirmed: number; unconfirmed: number }> {
if (address) {
return this.connectedProvider.getBalance(address);
}
const unisat = await this.getConnectedTarget();
const balance = await unisat.getBalance();
return { confirmed: balance.confirmed, unconfirmed: balance.unconfirmed };
}
override async getDefaultPubKey(): Promise<bsv.PublicKey> {
const unisat = await this.getConnectedTarget();
const pubKeyHex = await unisat.getPublicKey();
return bsv.PublicKey.fromString(pubKeyHex);
}
override async getPubKey(): Promise<bsv.PublicKey> {
// Unisat doesn't provide a method to get public key for a specific address
// We'll return the default public key instead
return this.getDefaultPubKey();
}
override async signMessage(message: string, address?: AddressOption): Promise<string> {
if (address) {
throw new Error(
`${this.constructor.name}#signMessage with \`address\` param is not supported!`
);
}
const unisat = await this.getConnectedTarget();
return unisat.signMessage(message, 'bsv');
}
override async getSignatures(
rawTxHex: string,
sigRequests: SignatureRequest[]
): Promise<SignatureResponse[]> {
const unisat = await this.getConnectedTarget();
// Convert rawTxHex to Uint8Array
const rawTxBytes = Uint8Array.from(Buffer.from(rawTxHex, 'hex'));
// Create a new Transaction and convert it to PSBT
const tx = Transaction.fromRaw(rawTxBytes, { allowUnknownOutputs: true });
const psbtHex = Buffer.from(tx.toPSBT()).toString('hex');
// Sign the PSBT
const signedPsbtHex = await unisat.signPsbt(psbtHex);
// Parse the signed PSBT
const signedPsbt = Transaction.fromPSBT(Buffer.from(signedPsbtHex, 'hex'));
return sigRequests.map((sigReq, index) => {
const input = signedPsbt.getInput(index);
const partialSig = input.partialSig?.[0];
return {
inputIndex: sigReq.inputIndex,
sig: partialSig ? Buffer.from(partialSig[1]).toString('hex') : '',
publicKey: partialSig ? Buffer.from(partialSig[0]).toString('hex') : '',
sigHashType: sigReq.sigHashType || 0,
};
});
}
}
@utxo-detective
Copy link
Author

sCrypt uses a SignatureRequest[] instead of psbts, so this is trying to adapt to psbts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment