UNPKG

@colony/purser-core

Version:

A collection of helpers, utils, validators and normalizers to assist the individual purser modules

273 lines (250 loc) 8.91 kB
/* @flow */ import { computeAddress } from 'ethers/utils'; import HDKey from 'hdkey'; import { safeIntegerValidator, hexSequenceValidator, addressValidator, } from './validators'; import { addressNormalizer, hexSequenceNormalizer } from './normalizers'; import { genericClass as messages } from './messages'; import { HEX_HASH_TYPE, DESCRIPTORS, SPLITTER, CHAIN_IDS } from './defaults'; import { TYPE_GENERIC, SUBTYPE_GENERIC } from './types'; import type { GenericClassArgumentsType } from './flowtypes'; const { GETTERS, SETTERS, WALLET_PROPS, GENERIC_PROPS } = DESCRIPTORS; /* * "Private" (internal) variable(s). * * These are used as return values from getters which don't have an accompanying setter, * but we still want to set them internally. */ let internalPublicKey: string; let internalDerivationPath: string; /* * @TODO Support extra props * * Support the extra props required for the software wallet (privateKey, mnemonic, etc...) * Also, we need to find a way to extend both this and the `ethers` wallet class */ export default class GenericWallet { address: string; otherAddresses: Object[]; chainId: number; /* * Both `publicKey` and `derivationPath` are getters. */ publicKey: Promise<string>; derivationPath: Promise<string>; type: string; subtype: string; setDefaultAddress: number => Promise<boolean>; /* * @TODO Add specific Flow types * * For the three main wallet methods */ sign: (...*) => Promise<string>; signMessage: (...*) => Promise<string>; verifyMessage: (...*) => Promise<string>; constructor({ publicKey, chainCode, rootDerivationPath, addressCount = 10, chainId = CHAIN_IDS.HOMESTEAD, }: GenericClassArgumentsType) { /* * Validate address count (this comes from the end user) */ safeIntegerValidator(addressCount); /* * Validate the `publicKey` and `chainCode` hex sequences. These come from * various external services, and we shouldn't trust them. */ hexSequenceValidator(publicKey); hexSequenceValidator(chainCode); /* * Derive the public key with the address index, so we can get the address */ const hdKey = new HDKey(); /* * Sadly Flow doesn't have the correct types for node's Buffer Object */ /* $FlowFixMe */ hdKey.publicKey = Buffer.from(publicKey, HEX_HASH_TYPE); /* * Sadly Flow doesn't have the correct types for node's Buffer Object */ /* $FlowFixMe */ hdKey.chainCode = Buffer.from(chainCode, HEX_HASH_TYPE); const otherAddresses = Array.from( /* * We default to `1`, but this time, to prevent the case where the * user passes in the value `0` manually (which will break the array map) */ new Array(addressCount || 1), (value, index) => { const addressObject = {}; const derivationKey = hdKey.deriveChild(index); /* * Set this individual address's derivation path */ addressObject.derivationPath = rootDerivationPath.substr(-1) === SPLITTER ? `${rootDerivationPath}${index}` : `${rootDerivationPath}${SPLITTER}${index}`; /* * This is the derrived public key, not the one originally fetched one */ const derivedPublicKey = derivationKey.publicKey.toString( HEX_HASH_TYPE, ); addressObject.publicKey = hexSequenceNormalizer(derivedPublicKey); /* * Generate the address from the derived public key */ const addressFromPublicKey = computeAddress( /* * Sadly Flow doesn't have the correct types for node's Buffer Object */ /* $FlowFixMe */ derivationKey.publicKey, ); /* * Also validate the address that comes from the `HDKey` library. */ addressValidator(addressFromPublicKey); addressObject.address = addressNormalizer(addressFromPublicKey); return addressObject; }, ); /* * Set the "private" (internal) variables values */ internalPublicKey = otherAddresses[0].publicKey; internalDerivationPath = otherAddresses[0].derivationPath; /* * Set the Wallet Object's values * * We're using `defineProperties` instead of strait up assignment, so that * we can customize the prop's descriptors * * @TODO Reduce code repetition when setting Class props * * We do this here and in the software wallet, so it might make sense to * write a helper method for this. */ Object.defineProperties(this, { address: Object.assign({}, { value: otherAddresses[0].address }, SETTERS), chainId: Object.assign({}, { value: chainId }, WALLET_PROPS), type: Object.assign({}, { value: TYPE_GENERIC }, GENERIC_PROPS), subtype: Object.assign({}, { value: SUBTYPE_GENERIC }, GENERIC_PROPS), /** * Set the default address/public key/path one of the (other) addresses from the array. * This is usefull since most methods (sign, signMessage) use this props as defaults. * * There's an argument to be made here that we can derive new addresses only when this * method gets called. * * This would be helpful to offload the initial cost of deriving a number * of `addressCount` addresses. * * On the other hand, if we do this, we won't be able to show the user what * addresses are available up front. * * @method setDefaultAddress * * @param {number} addressIndex The address index from the array * * @return {Promise<boolean>} True if it was able to set it, false otherwise */ setDefaultAddress: Object.assign( {}, { /* * @TODO Accept both number and object as argument * To make the arguments consistent across the wallet instance methods */ value: async (addressIndex: number = 0): Promise<boolean> => { safeIntegerValidator(addressIndex); if (addressIndex >= 0 && addressIndex < otherAddresses.length) { /* * Address count will always be at least `1` (the first derived address). * * This method is useful (can be used) only when the user generated more than * one address when instantiating the Wallet. */ this.address = otherAddresses[addressIndex].address; internalPublicKey = otherAddresses[addressIndex].publicKey; internalDerivationPath = otherAddresses[addressIndex].derivationPath; return true; } throw new Error( `${ messages.addressIndexOutsideRange }: index (${addressIndex}) count (${addressCount})`, ); }, }, WALLET_PROPS, ), /* * These are just a placeholder static methods. They should be replaced (or deleted at least) * with methods that actually has some functionality. */ sign: Object.assign({}, { value: async () => {} }, GENERIC_PROPS), signMessage: Object.assign({}, { value: async () => {} }, GENERIC_PROPS), verifyMessage: Object.assign( {}, { value: async () => {} }, GENERIC_PROPS, ), }); /* * The `otherAddresses` prop is only available if we have more than one. * * Otherwise it's pointless since it just repeats information (first index * is also the default one). */ if (addressCount > 1) { Object.defineProperty( (this: any), 'otherAddresses', Object.assign( {}, { /* * Map out the publicKey and derivation path from the `otherAddresses` * array that gets assigned to the Wallet instance. * * The user should only have access to `the publicKey` and `derivationPath` from the * default account (set via `setDefaultAddress()`) */ value: otherAddresses.map(({ address }) => address), }, WALLET_PROPS, ), ); } } /* * Public Key Getter */ /* eslint-disable-next-line class-methods-use-this */ get publicKey(): Promise<string> { return Promise.resolve(internalPublicKey); } /* eslint-disable-next-line class-methods-use-this */ get derivationPath(): Promise<string> { return Promise.resolve(internalDerivationPath); } } /* * We need to use `defineProperties` to make props enumerable. * When adding them via a `Class` getter/setter it will prevent that by default */ Object.defineProperties((GenericWallet: any).prototype, { publicKey: GETTERS, derivationPath: GETTERS, });