@roochnetwork/rooch-sdk
Version:
214 lines (188 loc) • 6.43 kB
text/typescript
// Copyright (c) RoochNetwork
// SPDX-License-Identifier: Apache-2.0
import { HDKey } from '@scure/bip32'
import { generateMnemonic } from '@scure/bip39'
import { wordlist } from '@scure/bip39/wordlists/english'
import { schnorr, secp256k1 } from '@noble/curves/secp256k1'
import { BitcoinAddress, BitcoinNetowkType, RoochAddress } from '../../address/index.js'
import {
Authenticator,
BitcoinSignMessage,
encodeRoochSercetKey,
isValidBIP86Path,
Keypair,
mnemonicToSeed,
decodeRoochSercetKey,
SignatureScheme,
} from '../../crypto/index.js'
import { Bytes } from '../../types/index.js'
import { blake2b, sha256, toHEX } from '../../utils/index.js'
import { Secp256k1PublicKey } from './publickey.js'
import { Transaction } from '../../transactions/index.js'
export const DEFAULT_SECP256K1_DERIVATION_PATH = "m/86'/0'/0'/0/1"
/**
* Secp256k1 Keypair data
*/
export interface Secp256k1KeypairData {
publicKey: Bytes
secretKey: Bytes
}
/**
* An Secp256k1 Keypair used for signing transactions.
*/
export class Secp256k1Keypair extends Keypair {
private keypair: Secp256k1KeypairData
/**
* Create a new keypair instance.
* Generate random keypair if no {@link Secp256k1Keypair} is provided.
*
* @param keypair secp256k1 keypair
*/
constructor(keypair?: Secp256k1KeypairData) {
super()
if (keypair) {
this.keypair = keypair
} else {
const secretKey: Uint8Array = secp256k1.utils.randomPrivateKey()
const publicKey: Uint8Array = secp256k1.getPublicKey(secretKey, true)
this.keypair = { publicKey, secretKey }
}
}
getBitcoinAddress(): BitcoinAddress {
return this.getSchnorrPublicKey().toAddress().bitcoinAddress
}
getBitcoinAddressWith(network: BitcoinNetowkType): BitcoinAddress {
return this.getSchnorrPublicKey().toAddressWith(network).bitcoinAddress
}
getRoochAddress(): RoochAddress {
return this.getSchnorrPublicKey().toAddress().roochAddress
}
/**
* Get the key scheme of the keypair Secp256k1
*/
getKeyScheme(): SignatureScheme {
return 'Secp256k1'
}
/**
* Generate a new random keypair
*/
static generate(): Secp256k1Keypair {
return new Secp256k1Keypair()
}
/**
* Create a keypair from a raw secret key byte array.
*
* This method should only be used to recreate a keypair from a previously
* generated secret key. Generating keypairs from a random seed should be done
* with the {@link Keypair.fromSeed} method.
*
* @throws error if the provided secret key is invalid and validation is not skipped.
*
* @param secretKey secret key byte array
* @param skipValidation skip secret key validation
*/
static fromSecretKey(secretKey: Uint8Array | string, skipValidation?: boolean): Secp256k1Keypair {
const decodeSecretKey =
typeof secretKey === 'string'
? (() => {
const decoded = decodeRoochSercetKey(secretKey)
if (decoded.schema !== 'Secp256k1') {
throw new Error('provided secretKey is invalid')
}
return decoded.secretKey
})()
: secretKey
const publicKey: Uint8Array = secp256k1.getPublicKey(decodeSecretKey, true)
if (!skipValidation) {
const encoder = new TextEncoder()
const signData = encoder.encode('rooch validation')
const msgHash = toHEX(blake2b(signData, { dkLen: 32 }))
const signature = secp256k1.sign(msgHash, decodeSecretKey)
if (!secp256k1.verify(signature, msgHash, publicKey, { lowS: true })) {
throw new Error('Provided secretKey is invalid')
}
}
return new Secp256k1Keypair({ publicKey, secretKey: decodeSecretKey })
}
/**
* Generate a keypair from a 32 byte seed.
*
* @param seed seed byte array
*/
static fromSeed(seed: Uint8Array): Secp256k1Keypair {
let publicKey = secp256k1.getPublicKey(seed, true)
return new Secp256k1Keypair({ publicKey, secretKey: seed })
}
/**
* The public key for this keypair
*/
getPublicKey(): Secp256k1PublicKey {
return new Secp256k1PublicKey(this.keypair.publicKey)
}
getSchnorrPublicKey(): Secp256k1PublicKey {
return new Secp256k1PublicKey(schnorr.getPublicKey(this.keypair.secretKey))
}
/**
* The Bech32 secret key string for this Secp256k1 keypair
*/
getSecretKey(): string {
return encodeRoochSercetKey(this.keypair.secretKey, this.getKeyScheme())
}
/**
* Return the ecdsa signature for the provided data.
*/
async sign(input: Bytes) {
const msgHash = sha256(input)
const sig = secp256k1.sign(msgHash, this.keypair.secretKey, {
lowS: true,
})
return sig.toCompactRawBytes()
}
/**
* Return the schnorr signature for the provided data.
*/
async sign_schnorr(input: Bytes, auxRand?: Bytes) {
const sig = schnorr.sign(input, this.keypair.secretKey, auxRand)
return sig
}
async signTransaction(input: Transaction): Promise<Authenticator> {
return await Authenticator.bitcoin(
new BitcoinSignMessage(input.hashData(), input.getInfo() ?? 'sdk'),
this,
)
}
/**
* Derive Secp256k1 keypair from mnemonics and path. The mnemonics must be normalized
* and validated against the english wordlist.
*
* If path is none, it will default to m/86'/0'/0'/0/1, otherwise the path must
* be compliant to BIP-32 in form m/86'/0'/{account_index}'/{change_index}/{address_index}.
*/
static deriveKeypair(mnemonics: string, path?: string): Secp256k1Keypair {
if (path == null) {
path = DEFAULT_SECP256K1_DERIVATION_PATH
}
if (!isValidBIP86Path(path)) {
throw new Error('Invalid derivation path')
}
const key = HDKey.fromMasterSeed(mnemonicToSeed(mnemonics)).derive(path)
if (key.publicKey == null || key.privateKey == null) {
throw new Error('Invalid key')
}
return Secp256k1Keypair.fromSecretKey(key.privateKey)
}
/**
* Generate a new mnemonic and derive a keypair from it.
*
* @param path Optional derivation path. If not provided, will use DEFAULT_SECP256K1_DERIVATION_PATH
* @returns An object containing the mnemonic and the derived keypair
*/
static generateWithMnemonic(path?: string): {
mnemonic: string
keypair: Secp256k1Keypair
} {
const mnemonic = generateMnemonic(wordlist)
const keypair = this.deriveKeypair(mnemonic, path)
return { mnemonic, keypair }
}
}