UNPKG

@colony/purser-software

Version:

A javascript library to interact with a software Ethereum wallet, based on the ethers.js library

337 lines (315 loc) 10.2 kB
/* @flow */ import { encrypt } from 'ethers/utils/secret-storage'; import { privateToPublic } from 'ethereumjs-util'; import { derivationPathSerializer, userInputValidator, } from '@colony/purser-core/helpers'; import { warning } from '@colony/purser-core/utils'; import { hexSequenceNormalizer } from '@colony/purser-core/normalizers'; import { addressValidator, hexSequenceValidator, } from '@colony/purser-core/validators'; import { PATH, DESCRIPTORS, HEX_HASH_TYPE, REQUIRED_PROPS, } from '@colony/purser-core/defaults'; import { TYPE_SOFTWARE, SUBTYPE_ETHERS } from '@colony/purser-core/types'; import type { WalletArgumentsType, TransactionObjectType, } from '@colony/purser-core/flowtypes'; import { signTransaction, signMessage, verifyMessage } from './staticMethods'; import { walletClass as messages } from './messages'; const { GETTERS, WALLET_PROPS } = DESCRIPTORS; /* * "Private" (internal) variable(s) */ let internalKeystoreJson: string | void; let internalEncryptionPassword: string | void; /** * @NOTE We're no longer directly extending the Ethers Wallet Class * * This is due to the fact that we need more control over the resulting Class * object (SoftwareWallet in this case). * * We're still shadowing the Ethers Wallet, meaning when opening or creating a new * wallet, we will first create a Ethers Wallet instance than pass that along * to the SoftwareWallet constructor. * * This way we don't have to deal with non-configurable or non-writable props, * or the providers being baked in. */ export default class SoftwareWallet { address: string; privateKey: string; originalMnemonic: string; derivationPath: string; type: string; subtype: string; chainId: number; /* * @TODO Add specific Flow types * * For the three main wallet methods */ sign: (...*) => Promise<string>; signMessage: (...*) => Promise<string>; verifyMessage: (...*) => Promise<string>; constructor(ethersInstance: WalletArgumentsType = {}) { const { address, privateKey, password, originalMnemonic: mnemonic, keystore, chainId, sign: ethersSign, signMessage: ethersSignMessage, } = ethersInstance; /* * Validate the private key and address that's coming in from ethers. */ addressValidator(address); hexSequenceValidator(privateKey); /* * If we have a keystore JSON string and encryption password, set them * to the internal variables. */ internalEncryptionPassword = password; internalKeystoreJson = keystore; /* * Set the private key to a "internal" variable since we only allow * access to it through a getter and not directly via a prop. */ Object.defineProperties(this, { address: Object.assign({}, { value: address }, WALLET_PROPS), type: Object.assign({}, { value: TYPE_SOFTWARE }, WALLET_PROPS), subtype: Object.assign({}, { value: SUBTYPE_ETHERS }, WALLET_PROPS), chainId: Object.assign({}, { value: chainId }, WALLET_PROPS), /* * Getters */ privateKey: Object.assign({}, { get: async () => privateKey }, GETTERS), /* * @TODO Allow users control of the derivation path * When instantiating a new class instance. But this is only if the feature * turns out to be required. */ derivationPath: Object.assign( {}, { get: async () => derivationPathSerializer({ change: PATH.CHANGE, addressIndex: PATH.INDEX, }), }, GETTERS, ), sign: Object.assign( {}, { value: async (transactionObject: TransactionObjectType) => { /* * Validate the trasaction's object input */ userInputValidator({ firstArgument: transactionObject, }); const { chainId: transactionChainId = this.chainId } = transactionObject || {}; return signTransaction( Object.assign({}, transactionObject, { chainId: transactionChainId, /* * @NOTE We need to bind the whole ethers instance * * Since the `sign` will look for different methods inside the * class's prototype, and if it fails to find them, it will * crash */ callback: (ethersSign: any).bind(ethersInstance), }), ); }, }, WALLET_PROPS, ), signMessage: Object.assign( {}, { value: async (messageObject: Object = {}) => { /* * Validate the trasaction's object input */ userInputValidator({ firstArgument: messageObject, requiredOr: REQUIRED_PROPS.SIGN_MESSAGE, }); return signMessage({ message: messageObject.message, messageData: messageObject.messageData, /* * @NOTE We need to bind the whole ethers instance * * Since the `signMessage` will look for different methods inside the * class's prototype, and if it fails to find them, it will * crash */ callback: (ethersSignMessage: any).bind(ethersInstance), }); }, }, WALLET_PROPS, ), verifyMessage: Object.assign( {}, { value: async (signatureVerificationObject: Object = {}) => { /* * Validate the trasaction's object input */ userInputValidator({ firstArgument: signatureVerificationObject, requiredAll: REQUIRED_PROPS.VERIFY_MESSAGE, }); const { message, signature } = signatureVerificationObject; return verifyMessage({ address, message, signature, }); }, }, WALLET_PROPS, ), }); /* * Only set the `mnemonic` prop if it's available, so it won't show up * as being defined, but set to `undefined` */ if (mnemonic) { Object.defineProperty( (this: any), 'mnemonic', Object.assign({}, { get: async () => mnemonic }, GETTERS), ); } } get keystore(): Promise<string | void> { /* * We're wrapping the getter (returning actually) in a IIFE so we can * write it using a `async` pattern. */ return (async () => { if (internalEncryptionPassword) { const privateKey: string = await this.privateKey; /* * Memoizing the getter * * This is quite an expensive operation, so we're memoizing it that * on the next call (an the others after that) it won't re-calculate * the value again. */ Object.defineProperty( (this: any), 'keystore', Object.assign({}, GETTERS, { value: (internalKeystoreJson && Promise.resolve(internalKeystoreJson)) || /* * We're usign Ethers's direct secret storage encrypt method to generate * the keystore JSON string * * @TODO Validate the password * * The password won't work if it's not a string, so it will be best if * we write a string validator for it */ encrypt( privateKey, internalEncryptionPassword.toString(), ), }), ); return ( (internalKeystoreJson && Promise.resolve(internalKeystoreJson)) || /* * We're usign Ethers's direct secret storage encrypt method to generate * the keystore JSON string * * @TODO Validate the password * * The password won't work if it's not a string, so it will be best if * we write a string validator for it */ encrypt( privateKey, internalEncryptionPassword.toString(), ) ); } warning(messages.noPassword); return Promise.reject(); })(); } /* * Just set the encryption password, we don't return anything from here, * hence we don't have a need for `this`. * * This is just an convenince to allow us to set the encryption password * after the wallet has be created / instantiated. */ /* eslint-disable-next-line class-methods-use-this */ set keystore(newEncryptionPassword: string): void { internalEncryptionPassword = newEncryptionPassword; } get publicKey(): Promise<string | void> { /* * We're wrapping the getter (returning actually) in a IIFE so we can * write it using a `async` pattern. */ return (async () => { const privateKey: string = await this.privateKey; const reversedPublicKey: string = privateToPublic(privateKey).toString( HEX_HASH_TYPE, ); /* * Validate the reversed public key */ hexSequenceValidator(reversedPublicKey); /* * Then normalize it to ensure it has the `0x` prefix */ const normalizedPublicKey: string = hexSequenceNormalizer( reversedPublicKey, ); /* * Memoizing the getter * * While this is not an expensive operation, it's still a good idea * to memoize it so it returns a tiny bit faster. */ Object.defineProperty( (this: any), 'publicKey', Object.assign({}, GETTERS, { value: Promise.resolve(normalizedPublicKey), }), ); return normalizedPublicKey; })(); } } /* * 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((SoftwareWallet: any).prototype, { publicKey: GETTERS, keystore: GETTERS, });