@btc-vision/transaction
Version:
OPNet transaction library allows you to create and sign transactions for the OPNet network.
385 lines (314 loc) • 11.7 kB
text/typescript
import {
type MessageHash,
type PublicKey,
type SchnorrSignature,
type Signature,
type UniversalSigner,
} from '@btc-vision/ecpair';
import { backend } from '../ecc/backend.js';
import { crypto, fromHex, type Network, toHex, toXOnly } from '@btc-vision/bitcoin';
import { TweakedSigner } from '../signer/TweakedSigner.js';
import { EcKeyPair } from './EcKeyPair.js';
import { MLDSASecurityLevel, type QuantumBIP32Interface } from '@btc-vision/bip32';
import { isOPWallet, type OPWallet } from '../transaction/browser/types/OPWallet.js';
import type { MLDSASignature } from '../transaction/interfaces/IWeb3ProviderTypes.js';
import { SignatureType } from '../transaction/browser/types/Unisat.js';
export interface SignedMessage {
readonly signature: Uint8Array;
readonly message: Uint8Array;
}
export interface MLDSASignedMessage {
readonly signature: Uint8Array;
readonly message: Uint8Array;
readonly publicKey: Uint8Array;
readonly securityLevel: MLDSASecurityLevel;
}
interface WindowWithOPWallet {
opnet?: OPWallet;
}
class MessageSignerBase {
public sha256(message: Uint8Array): Uint8Array {
return crypto.sha256(message);
}
public async trySignSchnorrWithOPWallet(
message: Uint8Array | string,
): Promise<SignedMessage | null> {
const wallet = this.getOPWallet();
if (!wallet) {
return null;
}
const messageBuffer =
typeof message === 'string' ? new TextEncoder().encode(message) : message;
const hashedMessage = this.sha256(messageBuffer);
const messageHex = toHex(hashedMessage);
const signatureHex = await wallet.signData(
messageHex,
SignatureType.schnorr,
typeof message === 'string' ? message : undefined,
);
return {
signature: fromHex(signatureHex),
message: hashedMessage,
};
}
public async trySignECDSAWithOPWallet(
message: Uint8Array | string,
): Promise<SignedMessage | null> {
const wallet = this.getOPWallet();
if (!wallet) {
return null;
}
const messageBuffer =
typeof message === 'string' ? new TextEncoder().encode(message) : message;
const hashedMessage = this.sha256(messageBuffer);
const messageHex = toHex(hashedMessage);
const signatureHex = await wallet.signData(
messageHex,
SignatureType.ecdsa,
typeof message === 'string' ? message : undefined,
);
return {
signature: fromHex(signatureHex),
message: hashedMessage,
};
}
public async trySignMLDSAWithOPWallet(
message: Uint8Array | string,
): Promise<MLDSASignedMessage | null> {
const wallet = this.getOPWallet();
if (!wallet) {
return null;
}
const messageBuffer =
typeof message === 'string' ? new TextEncoder().encode(message) : message;
const hashedMessage = this.sha256(messageBuffer);
const messageHex = toHex(hashedMessage);
const result: MLDSASignature = await wallet.web3.signMLDSAMessage(
messageHex,
typeof message === 'string' ? message : undefined,
);
return {
signature: fromHex(result.signature),
message: hashedMessage,
publicKey: fromHex(result.publicKey),
securityLevel: result.securityLevel,
};
}
public async signMessageAuto(
message: Uint8Array | string,
keypair?: UniversalSigner,
): Promise<SignedMessage> {
if (!keypair) {
const walletResult = await this.trySignSchnorrWithOPWallet(message);
if (walletResult) {
return walletResult;
}
throw new Error('No keypair provided and OP_WALLET is not available.');
}
return this.signMessage(keypair, message);
}
public async signMessageECDSAAuto(
message: Uint8Array | string,
keypair?: UniversalSigner,
): Promise<SignedMessage> {
if (!keypair) {
const walletResult = await this.trySignECDSAWithOPWallet(message);
if (walletResult) {
return walletResult;
}
throw new Error('No keypair provided and OP_WALLET is not available.');
}
return this.signECDSA(keypair, message);
}
public async tweakAndSignMessageAuto(
message: Uint8Array | string,
keypair?: UniversalSigner,
network?: Network,
): Promise<SignedMessage> {
if (!keypair) {
const walletResult = await this.trySignSchnorrWithOPWallet(message);
if (walletResult) {
return walletResult;
}
throw new Error('No keypair provided and OP_WALLET is not available.');
}
if (!network) {
throw new Error('Network is required when signing with a local keypair.');
}
return this.tweakAndSignMessage(keypair, message, network);
}
public async signMLDSAMessageAuto(
message: Uint8Array | string,
mldsaKeypair?: QuantumBIP32Interface,
): Promise<MLDSASignedMessage> {
if (!mldsaKeypair) {
const walletResult = await this.trySignMLDSAWithOPWallet(message);
if (walletResult) {
return walletResult;
}
throw new Error('No ML-DSA keypair provided and OP_WALLET is not available.');
}
return this.signMLDSAMessage(mldsaKeypair, message);
}
public async verifyMLDSAWithOPWallet(
message: Uint8Array | string,
signature: MLDSASignedMessage,
): Promise<boolean | null> {
const wallet = this.getOPWallet();
if (!wallet) {
return null;
}
const messageBuffer =
typeof message === 'string' ? new TextEncoder().encode(message) : message;
const hashedMessage = this.sha256(messageBuffer);
const mldsaSignature: MLDSASignature = {
signature: toHex(signature.signature),
publicKey: toHex(signature.publicKey),
securityLevel: signature.securityLevel,
messageHash: toHex(hashedMessage),
};
return wallet.web3.verifyMLDSASignature(toHex(hashedMessage), mldsaSignature);
}
public async getMLDSAPublicKeyFromOPWallet(): Promise<Uint8Array | null> {
const wallet = this.getOPWallet();
if (!wallet) {
return null;
}
const publicKeyHex = await wallet.web3.getMLDSAPublicKey();
return fromHex(publicKeyHex);
}
public tweakAndSignMessage(
keypair: UniversalSigner,
message: Uint8Array | string,
network: Network,
): SignedMessage {
const tweaked = TweakedSigner.tweakSigner(keypair, { network });
return this.signMessage(tweaked, message);
}
public signMessage(keypair: UniversalSigner, message: Uint8Array | string): SignedMessage {
if (typeof message === 'string') {
message = new TextEncoder().encode(message);
}
if (!keypair.privateKey) {
throw new Error('Private key not found in keypair.');
}
const hashedMessage = this.sha256(message);
if (!backend.signSchnorr) {
throw new Error('backend.signSchnorr is not available.');
}
return {
signature: backend.signSchnorr(hashedMessage as MessageHash, keypair.privateKey),
message: hashedMessage,
};
}
public signECDSA(keypair: UniversalSigner, message: Uint8Array | string): SignedMessage {
if (typeof message === 'string') {
message = new TextEncoder().encode(message);
}
if (!keypair.privateKey) {
throw new Error('Private key not found in keypair.');
}
const hashedMessage = this.sha256(message);
if (!backend.sign) {
throw new Error('backend.signSchnorr is not available.');
}
return {
signature: backend.sign(hashedMessage as MessageHash, keypair.privateKey),
message: hashedMessage,
};
}
public verifyECDSASignature(
publicKey: Uint8Array | PublicKey,
message: Uint8Array | string,
signature: Uint8Array | Signature,
): boolean {
if (typeof message === 'string') {
message = new TextEncoder().encode(message);
}
if (signature.length !== 64) {
throw new Error('Invalid signature length.');
}
const hashedMessage = this.sha256(message);
if (!backend.verify) {
throw new Error('backend.verifySchnorr is not available.');
}
return backend.verify(
hashedMessage as MessageHash,
publicKey as PublicKey,
signature as Signature,
);
}
public verifySignature(
publicKey: Uint8Array,
message: Uint8Array | string,
signature: Uint8Array,
): boolean {
if (typeof message === 'string') {
message = new TextEncoder().encode(message);
}
if (signature.length !== 64) {
throw new Error('Invalid signature length.');
}
const hashedMessage = this.sha256(message);
if (!backend.verifySchnorr) {
throw new Error('backend.verifySchnorr is not available.');
}
return backend.verifySchnorr(
hashedMessage as MessageHash,
toXOnly(publicKey as PublicKey),
signature as SchnorrSignature,
);
}
public tweakAndVerifySignature(
publicKey: Uint8Array,
message: Uint8Array | string,
signature: Uint8Array,
): boolean {
const tweakedPublicKey = EcKeyPair.tweakPublicKey(publicKey);
return this.verifySignature(tweakedPublicKey, message, signature);
}
public signMLDSAMessage(
mldsaKeypair: QuantumBIP32Interface,
message: Uint8Array | string,
): MLDSASignedMessage {
if (typeof message === 'string') {
message = new TextEncoder().encode(message);
}
if (!mldsaKeypair.privateKey) {
throw new Error('ML-DSA private key not found in keypair.');
}
const hashedMessage = this.sha256(message);
const signature = mldsaKeypair.sign(hashedMessage);
return {
signature: new Uint8Array(signature),
message: hashedMessage,
publicKey: new Uint8Array(mldsaKeypair.publicKey),
securityLevel: mldsaKeypair.securityLevel,
};
}
public verifyMLDSASignature(
mldsaKeypair: QuantumBIP32Interface,
message: Uint8Array | string,
signature: Uint8Array,
): boolean {
if (typeof message === 'string') {
message = new TextEncoder().encode(message);
}
const hashedMessage = this.sha256(message);
return mldsaKeypair.verify(hashedMessage, signature);
}
public isOPWalletAvailable(): boolean {
return this.getOPWallet() !== null;
}
private getOPWallet(): OPWallet | null {
if (typeof window === 'undefined') {
return null;
}
const _window = window as WindowWithOPWallet;
if (!_window.opnet || !isOPWallet(_window.opnet)) {
return null;
}
return _window.opnet;
}
}
export const MessageSigner = new MessageSignerBase();