@radixdlt/account
Version:
A JavaScript client library for interacting with the Radix Distributed Ledger.
462 lines (414 loc) • 12.6 kB
text/typescript
import {
BehaviorSubject,
combineLatest,
Observable,
of,
ReplaySubject,
Subscription,
throwError,
} from 'rxjs'
import { SigningKey, isSigningKey } from './signingKey'
import {
SigningKeysT,
SigningKeyT,
DeriveNextInput,
SwitchSigningKeyInput,
SwitchToSigningKey,
SwitchToIndex,
AddSigningKeyByPrivateKeyInput,
SigningKeychainT,
DeriveHWSigningKeyInput,
Signing,
} from './_types'
import { map, mergeMap, shareReplay, take, tap } from 'rxjs/operators'
import {
Keystore,
KeystoreT,
PublicKeyT,
SignatureT,
HDPathRadix,
HDPathRadixT,
Int32,
HDMasterSeed,
MnemomicT,
Mnemonic,
} from '@radixdlt/crypto'
import { Option } from 'prelude-ts'
import { arraysEqual, log, msgFromError } from '@radixdlt/util'
import { ResultAsync } from 'neverthrow'
import { HardwareSigningKeyT, HardwareWalletT } from '@radixdlt/hardware-wallet'
import { BuiltTransactionReadyToSign } from '@radixdlt/primitives'
const stringifySigningKeysArray = (signingKeys: SigningKeyT[]): string =>
signingKeys.map(a => a.toString()).join(',\n')
const stringifySigningKeys = (signingKeys: SigningKeysT): string => {
const allSigningKeysString = stringifySigningKeysArray(signingKeys.all)
return `
size: ${signingKeys.size()},
#hdSigningKeys: ${signingKeys.hdSigningKeys().length},
#nonHDSigningKeys: ${signingKeys.nonHDSigningKeys().length},
#localHDSigningKeys: ${signingKeys.localHDSigningKeys().length},
#hardwareHDSigningKeys: ${signingKeys.hardwareHDSigningKeys().length},
all: ${allSigningKeysString}
`
}
type MutableSigningKeysT = SigningKeysT &
Readonly<{
add: (signingKey: SigningKeyT) => void
}>
const createSigningKeys = (_all: SigningKeyT[]): MutableSigningKeysT => {
const all: SigningKeyT[] = []
const getHDSigningKeyByHDPath = (
hdPath: HDPathRadixT,
): Option<SigningKeyT> => {
const signingKey = all
.filter(a => a.isHDSigningKey)
.find(a => a.hdPath!.equals(hdPath))
return Option.of(signingKey)
}
const getAnySigningKeyByPublicKey = (
publicKey: PublicKeyT,
): Option<SigningKeyT> => {
const signingKey = all.find(a => a.publicKey.equals(publicKey))
return Option.of(signingKey)
}
const localHDSigningKeys = () => all.filter(a => a.isLocalHDSigningKey)
const hardwareHDSigningKeys = () => all.filter(a => a.isHardwareSigningKey)
const nonHDSigningKeys = () => all.filter(a => !a.isHDSigningKey)
const hdSigningKeys = () => all.filter(a => a.isHDSigningKey)
const add = (signingKey: SigningKeyT): void => {
if (
all.find(a => a.type.uniqueKey === signingKey.type.uniqueKey) !==
undefined
) {
// already there
return
}
// new
all.push(signingKey)
}
const signingKeys: MutableSigningKeysT = {
toString: (): string => {
throw new Error('Overriden below')
},
equals: (other: SigningKeysT): boolean => arraysEqual(other.all, all),
add,
localHDSigningKeys,
hardwareHDSigningKeys,
nonHDSigningKeys,
hdSigningKeys,
all,
size: () => all.length,
getHDSigningKeyByHDPath,
getAnySigningKeyByPublicKey,
}
return {
...signingKeys,
toString: (): string => stringifySigningKeys(signingKeys),
}
}
export const isSwitchToIndex = (
something: unknown,
): something is SwitchToIndex => {
const inspection = something as SwitchToIndex
return inspection.toIndex !== undefined
}
const MutableSigningKeys = {
create: createSigningKeys,
}
const create = (
input: Readonly<{
mnemonic: MnemomicT
startWithInitialSigningKey?: boolean
}>,
): SigningKeychainT => {
const subs = new Subscription()
const { mnemonic } = input
const startWithInitialSigningKey = input.startWithInitialSigningKey ?? true
const masterSeed = HDMasterSeed.fromMnemonic({ mnemonic })
const hdNodeDeriverWithBip32Path = masterSeed.masterNode().derive
let unsafeActiveSigningKey: SigningKeyT = (undefined as unknown) as SigningKeyT
const activeSigningKeySubject = new ReplaySubject<SigningKeyT>()
const setActiveSigningKey = (newSigningKey: SigningKeyT): void => {
activeSigningKeySubject.next(newSigningKey)
unsafeActiveSigningKey = newSigningKey
}
const signingKeysSubject = new BehaviorSubject<MutableSigningKeysT>(
MutableSigningKeys.create([]),
)
const revealMnemonic = (): MnemomicT => mnemonic
const numberOfAllSigningKeys = (): number =>
signingKeysSubject.getValue().size()
const numberOfLocalHDSigningKeys = (): number =>
signingKeysSubject.getValue().localHDSigningKeys().length
const numberOfHWSigningKeys = (): number =>
signingKeysSubject.getValue().hardwareHDSigningKeys().length
const _addAndMaybeSwitchToNewSigningKey = (
newSigningKey: SigningKeyT,
alsoSwitchTo?: boolean,
): SigningKeyT => {
const alsoSwitchTo_ = alsoSwitchTo ?? false
const signingKeys = signingKeysSubject.getValue()
signingKeys.add(newSigningKey)
signingKeysSubject.next(signingKeys)
if (alsoSwitchTo_) {
setActiveSigningKey(newSigningKey)
}
return newSigningKey
}
const deriveHWSigningKey = (
input: DeriveHWSigningKeyInput,
): Observable<SigningKeyT> => {
const nextPath = (): HDPathRadixT => {
const index = numberOfHWSigningKeys()
return HDPathRadix.create({
address: { index, isHardened: true },
})
}
const hdPath: HDPathRadixT =
input.keyDerivation === 'next' ? nextPath() : input.keyDerivation
return input.hardwareWalletConnection.pipe(
take(1),
mergeMap(
(
hardwareWallet: HardwareWalletT,
): Observable<HardwareSigningKeyT> =>
hardwareWallet.makeSigningKey(
hdPath,
input.verificationPrompt,
),
),
map((hardwareSigningKey: HardwareSigningKeyT) => {
const signingKey = SigningKey.fromHDPathWithHWSigningKey({
hdPath,
hardwareSigningKey,
})
_addAndMaybeSwitchToNewSigningKey(
signingKey,
input.alsoSwitchTo,
)
return signingKey
}),
)
}
const _deriveLocalHDSigningKeyWithPath = (
input: Readonly<{
hdPath: HDPathRadixT
alsoSwitchTo?: boolean // defaults to false
}>,
): Observable<SigningKeyT> => {
const { hdPath } = input
const newSigningKey = _addAndMaybeSwitchToNewSigningKey(
SigningKey.byDerivingNodeAtPath({
hdPath,
deriveNodeAtPath: () => hdNodeDeriverWithBip32Path(hdPath),
}),
input.alsoSwitchTo,
)
return of(newSigningKey)
}
const _deriveNextLocalHDSigningKeyAtIndex = (
input: Readonly<{
addressIndex: Readonly<{
index: Int32
isHardened?: boolean // defaults to true
}>
alsoSwitchTo?: boolean // defaults to false
}>,
): Observable<SigningKeyT> =>
_deriveLocalHDSigningKeyWithPath({
hdPath: HDPathRadix.create({
address: input.addressIndex,
}),
alsoSwitchTo: input.alsoSwitchTo,
})
const deriveNextLocalHDSigningKey = (
input?: DeriveNextInput,
): Observable<SigningKeyT> => {
const index = numberOfLocalHDSigningKeys()
return _deriveNextLocalHDSigningKeyAtIndex({
addressIndex: {
index,
isHardened: input?.isHardened ?? true,
},
alsoSwitchTo: input?.alsoSwitchTo,
})
}
const switchSigningKey = (input: SwitchSigningKeyInput): SigningKeyT => {
const isSwitchToSigningKey = (
something: unknown,
): something is SwitchToSigningKey => {
const inspection = input as SwitchToSigningKey
return (
inspection.toSigningKey !== undefined &&
isSigningKey(inspection.toSigningKey)
)
}
if (input === 'last') {
const lastIndex = numberOfAllSigningKeys() - 1
return switchSigningKey({ toIndex: lastIndex })
} else if (input === 'first') {
return switchSigningKey({ toIndex: 0 })
} else if (isSwitchToSigningKey(input)) {
const toSigningKey = input.toSigningKey
setActiveSigningKey(toSigningKey)
log.info(
`Active signingKey switched to: ${toSigningKey.toString()}`,
)
return toSigningKey
} else if (isSwitchToIndex(input)) {
const unsafeTargetIndex = input.toIndex
const signingKeys = signingKeysSubject.getValue()
const safeTargetIndex = Math.min(
unsafeTargetIndex,
signingKeys.size(),
)
const firstSigningKey = Array.from(signingKeys.all)[safeTargetIndex]
if (!firstSigningKey) {
const err = `No signingKeys.`
log.error(err)
throw new Error(err)
}
return switchSigningKey({ toSigningKey: firstSigningKey })
} else {
const err = `Incorrect implementation, failed to type check 'input' of switchSigningKey. Probably is 'isSigningKey' typeguard wrong.`
log.error(err)
throw new Error(err)
}
}
if (startWithInitialSigningKey) {
subs.add(
deriveNextLocalHDSigningKey({
alsoSwitchTo: true,
}).subscribe(),
)
}
const activeSigningKey$ = activeSigningKeySubject.asObservable()
const signingKeys$ = signingKeysSubject.asObservable().pipe(shareReplay())
const restoreLocalHDSigningKeysUpToIndex = (
index: number,
): Observable<SigningKeysT> => {
if (index < 0) {
const errMsg = `targetIndex must not be negative`
console.error(errMsg)
return throwError(new Error(errMsg))
}
const localHDSigningKeysSize = numberOfLocalHDSigningKeys()
const numberOfSigningKeysToCreate = index - localHDSigningKeysSize
if (numberOfSigningKeysToCreate < 0) {
return signingKeys$
}
const signingKeysObservableList: Observable<SigningKeyT>[] = Array(
numberOfSigningKeysToCreate,
)
.fill(undefined)
.map((_, index) =>
_deriveNextLocalHDSigningKeyAtIndex({
addressIndex: { index: localHDSigningKeysSize + index },
}),
)
return combineLatest(signingKeysObservableList).pipe(
mergeMap(_ => signingKeys$),
take(1),
)
}
const addSigningKeyFromPrivateKey = (
input: AddSigningKeyByPrivateKeyInput,
): SigningKeyT => {
const signingKey = SigningKey.fromPrivateKey(input)
_addAndMaybeSwitchToNewSigningKey(signingKey, input.alsoSwitchTo)
return signingKey
}
return {
revealMnemonic,
// should only be used for testing
__unsafeGetSigningKey: (): SigningKeyT => unsafeActiveSigningKey,
deriveNextLocalHDSigningKey,
deriveHWSigningKey,
switchSigningKey,
restoreLocalHDSigningKeysUpToIndex,
addSigningKeyFromPrivateKey,
observeSigningKeys: (): Observable<SigningKeysT> => signingKeys$,
observeActiveSigningKey: (): Observable<SigningKeyT> =>
activeSigningKey$,
sign: (
tx: BuiltTransactionReadyToSign,
nonXrdHRP?: string,
): Observable<SignatureT> =>
activeSigningKey$.pipe(mergeMap(a => a.sign(tx, nonXrdHRP))),
signHash: (hashedMessage: Buffer): Observable<SignatureT> =>
activeSigningKey$.pipe(mergeMap(a => a.signHash(hashedMessage))),
}
}
const byLoadingAndDecryptingKeystore = (
input: Readonly<{
password: string
load: () => Promise<KeystoreT>
startWithInitialSigningKey?: boolean
}>,
): ResultAsync<SigningKeychainT, Error> => {
const loadKeystore = (): ResultAsync<KeystoreT, Error> =>
ResultAsync.fromPromise(input.load(), (e: unknown) => {
const underlyingError = msgFromError(e)
const errMsg = `Failed to load keystore, underlying error: '${underlyingError}'`
log.error(errMsg)
return new Error(errMsg)
})
return loadKeystore()
.map((keystore: KeystoreT) => {
log.info('Keystore successfully loaded.')
return { ...input, keystore }
})
.andThen(SigningKeychain.fromKeystore)
}
const fromKeystore = (
input: Readonly<{
keystore: KeystoreT
password: string
startWithInitialSigningKey?: boolean
}>,
): ResultAsync<SigningKeychainT, Error> =>
Keystore.decrypt(input)
.map(entropy => ({ entropy }))
.andThen(Mnemonic.fromEntropy)
.map(mnemonic => ({
mnemonic,
startWithInitialSigningKey: input.startWithInitialSigningKey,
}))
.map(create)
const byEncryptingMnemonicAndSavingKeystore = (
input: Readonly<{
mnemonic: MnemomicT
password: string
save: (keystoreToSave: KeystoreT) => Promise<void>
startWithInitialSigningKey?: boolean
}>,
): ResultAsync<SigningKeychainT, Error> => {
const { mnemonic, password, startWithInitialSigningKey } = input
const save = (keystoreToSave: KeystoreT): ResultAsync<KeystoreT, Error> =>
ResultAsync.fromPromise(input.save(keystoreToSave), (e: unknown) => {
const underlyingError = msgFromError(e)
const errMsg = `Failed to save keystore, underlying error: '${underlyingError}'`
log.error(errMsg)
return new Error(errMsg)
}).map(() => {
log.info('Keystore successfully saved.')
return keystoreToSave
})
return Keystore.encryptSecret({
secret: mnemonic.entropy,
password,
})
.andThen(save)
.map((keystore: KeystoreT) => ({
keystore,
password,
startWithInitialSigningKey,
}))
.andThen(SigningKeychain.fromKeystore)
}
export const SigningKeychain = {
create,
fromKeystore,
byLoadingAndDecryptingKeystore,
byEncryptingMnemonicAndSavingKeystore,
}