@chorus-one/signer-local
Version:
Local signer for Chorus One SDK that utilizes a BIP39 mnemonic for signing operations
158 lines (138 loc) • 5.58 kB
text/typescript
import type { LocalSignerConfig } from './types'
import type { Signature, SignerData, AddressDerivationFn, MnemonicToSeedFn, SeedToKeypairFn } from '@chorus-one/signer'
import { KeyType } from '@chorus-one/signer'
import type { Logger } from '@chorus-one/utils'
import { nopLogger } from '@chorus-one/utils'
import { mnemonicToSeed } from 'bip39'
import { deriveKey as deriveEd25519, sign as signEd25519 } from './ed25519'
import { deriveKey as deriveSecp256k1, sign as signSecp256k1 } from './secp256k1'
/**
* The LocalSigner in the Chorus One SDK is a specialized implementation of the Signer interface that utilizes a `BIP39`
* mnemonic for signing operations.
*
* This signer is ideal for local environments where you need a straightforward and
* secure method to generate and manage cryptographic keys from mnemonic phrases.
*/
export class LocalSigner {
private readonly config: LocalSignerConfig
private accounts: Map<string, { hdPath: string; privateKey: Uint8Array; publicKey: Uint8Array }>
private addressDerivationFn: AddressDerivationFn
private mnemonicToSeedFn: MnemonicToSeedFn
private seedToKeypairFn?: SeedToKeypairFn
private logger: Logger
/**
* Constructs a new LocalSigner.
*
* @param params - The parameters required to initialize the LocalSigner
* @param params.mnemonic - A string containing your `BIP39` mnemonic phrase
* @param params.keyType - An enum specifying the signing key type (e.g. SECP256K1, ED25519)
* @param params.accounts - An array of account objects, each containing an HD path
* @param params.addressDerivationFn - A function that derives the address from the public key
* @param params.logger - (Optional) A logger to use for logging messages, i.e `console`
*
* @returns A new instance of LocalSigner.
*/
constructor (params: {
mnemonic: string
accounts: [{ hdPath: string }]
keyType: KeyType
addressDerivationFn: AddressDerivationFn
mnemonicToSeedFn?: MnemonicToSeedFn
seedToKeypairFn?: SeedToKeypairFn
logger?: Logger
}) {
const { addressDerivationFn, ...config } = params
this.config = config
this.accounts = new Map()
this.logger = params.logger ?? nopLogger
this.addressDerivationFn = addressDerivationFn
this.mnemonicToSeedFn = params.mnemonicToSeedFn ?? mnemonicToSeed
this.seedToKeypairFn = params.seedToKeypairFn
}
/**
* Initializes the signer, performing any necessary setup or configuration.
* @returns A promise that resolves once the initialization is complete.
*/
async init (): Promise<void> {
const seed = await this.mnemonicToSeedFn(this.config.mnemonic)
await Promise.all(
this.config.accounts.map(async (account) => {
let key: { publicKey: Uint8Array; privateKey: Uint8Array }
switch (this.config.keyType) {
case undefined:
case KeyType.SECP256K1: {
const fn = this.seedToKeypairFn ?? deriveSecp256k1
key = await fn(seed, account.hdPath)
break
}
case KeyType.ED25519: {
const fn = this.seedToKeypairFn ?? deriveEd25519
key = await fn(seed, account.hdPath)
break
}
default:
throw new Error('unsupported key type')
}
const publicKey = key.publicKey
const privateKey = key.privateKey
const derivedAddresses = await this.addressDerivationFn(key.publicKey, account.hdPath)
derivedAddresses.forEach((address) => {
this.accounts.set(address.toLowerCase(), {
hdPath: account.hdPath,
privateKey,
publicKey
})
})
})
)
}
/**
* Signs the provided data using the private key associated with the signer's address.
*
* @param signerAddress - The address of the signer
* @param signerData - The data to be signed, which can be a raw message or custom data
*
* @returns A promise that resolves to an object containing the signature and public key.
*/
async sign (signerAddress: string, signerData: SignerData): Promise<{ sig: Signature; pk: Uint8Array }> {
this.logger.info(`signing data from address: ${signerAddress}`)
if (signerData.message === undefined) {
throw new Error('missing message to sign')
}
const content = signerData.message
let sig: Signature | undefined
switch (this.config.keyType) {
case undefined:
case KeyType.SECP256K1:
sig = await signSecp256k1(content, this.getPrivateKey(signerAddress))
break
case KeyType.ED25519:
sig = await signEd25519(content, this.getPrivateKey(signerAddress))
break
default:
throw new Error('unsupported key type')
}
return { sig, pk: await this.getPublicKey(signerAddress) }
}
/**
* Retrieves the public key associated with the signer's address.
*
* @param address - The address of the signer
*
* @returns A promise that resolves to a Uint8Array representing the public key.
*/
async getPublicKey (address: string): Promise<Uint8Array> {
const account = this.accounts.get(address.toLowerCase())
if (account === undefined) {
throw new Error(`no public key found for address: ${address}`)
}
return account.publicKey
}
private getPrivateKey (address: string): Uint8Array {
const account = this.accounts.get(address.toLowerCase())
if (account === undefined) {
throw new Error(`no private key found for address: ${address}`)
}
return account.privateKey
}
}