Created
September 13, 2024 17:51
-
-
Save utxo-detective/fcfa4cffc81c6826211616d167e5e6c2 to your computer and use it in GitHub Desktop.
sCrypt Unisat Provider
This file contains hidden or 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 { | |
| 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, | |
| }; | |
| }); | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
sCrypt uses a
SignatureRequest[]instead of psbts, so this is trying to adapt to psbts