Add eth_signTypedData support to our wallet subproviders
This commit is contained in:
		@@ -1,19 +1,19 @@
 | 
			
		||||
import { schemas } from '@0xproject/json-schemas';
 | 
			
		||||
import { EIP712Schema, EIP712Types, eip712Utils } from '@0xproject/order-utils';
 | 
			
		||||
import { EIP712_DOMAIN_NAME, EIP712_DOMAIN_SCHEMA, EIP712_DOMAIN_VERSION } from '@0xproject/order-utils';
 | 
			
		||||
import { Order, SignedOrder } from '@0xproject/types';
 | 
			
		||||
import { BigNumber } from '@0xproject/utils';
 | 
			
		||||
import { BigNumber, signTypedDataUtils } from '@0xproject/utils';
 | 
			
		||||
import _ = require('lodash');
 | 
			
		||||
 | 
			
		||||
import { ExchangeContract } from '../contract_wrappers/generated/exchange';
 | 
			
		||||
 | 
			
		||||
import { assert } from './assert';
 | 
			
		||||
 | 
			
		||||
const EIP712_ZEROEX_TRANSACTION_SCHEMA: EIP712Schema = {
 | 
			
		||||
const EIP712_ZEROEX_TRANSACTION_SCHEMA = {
 | 
			
		||||
    name: 'ZeroExTransaction',
 | 
			
		||||
    parameters: [
 | 
			
		||||
        { name: 'salt', type: EIP712Types.Uint256 },
 | 
			
		||||
        { name: 'signerAddress', type: EIP712Types.Address },
 | 
			
		||||
        { name: 'data', type: EIP712Types.Bytes },
 | 
			
		||||
        { name: 'salt', type: 'uint256' },
 | 
			
		||||
        { name: 'signerAddress', type: 'address' },
 | 
			
		||||
        { name: 'data', type: 'bytes' },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -37,16 +37,25 @@ export class TransactionEncoder {
 | 
			
		||||
    public getTransactionHex(data: string, salt: BigNumber, signerAddress: string): string {
 | 
			
		||||
        const exchangeAddress = this._getExchangeContract().address;
 | 
			
		||||
        const executeTransactionData = {
 | 
			
		||||
            salt,
 | 
			
		||||
            salt: salt.toString(),
 | 
			
		||||
            signerAddress,
 | 
			
		||||
            data,
 | 
			
		||||
        };
 | 
			
		||||
        const executeTransactionHashBuff = eip712Utils.structHash(
 | 
			
		||||
            EIP712_ZEROEX_TRANSACTION_SCHEMA,
 | 
			
		||||
            executeTransactionData,
 | 
			
		||||
        );
 | 
			
		||||
        const eip721MessageBuffer = eip712Utils.createEIP712Message(executeTransactionHashBuff, exchangeAddress);
 | 
			
		||||
        const messageHex = `0x${eip721MessageBuffer.toString('hex')}`;
 | 
			
		||||
        const typedData = {
 | 
			
		||||
            types: {
 | 
			
		||||
                EIP712Domain: EIP712_DOMAIN_SCHEMA.parameters,
 | 
			
		||||
                ZeroExTransaction: EIP712_ZEROEX_TRANSACTION_SCHEMA.parameters,
 | 
			
		||||
            },
 | 
			
		||||
            domain: {
 | 
			
		||||
                name: EIP712_DOMAIN_NAME,
 | 
			
		||||
                version: EIP712_DOMAIN_VERSION,
 | 
			
		||||
                verifyingContract: exchangeAddress,
 | 
			
		||||
            },
 | 
			
		||||
            message: executeTransactionData,
 | 
			
		||||
            primaryType: EIP712_ZEROEX_TRANSACTION_SCHEMA.name,
 | 
			
		||||
        };
 | 
			
		||||
        const eip712MessageBuffer = signTypedDataUtils.signTypedDataHash(typedData);
 | 
			
		||||
        const messageHex = `0x${eip712MessageBuffer.toString('hex')}`;
 | 
			
		||||
        return messageHex;
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { BlockchainLifecycle } from '@0xproject/dev-utils';
 | 
			
		||||
import { assetDataUtils, eip712Utils, orderHashUtils } from '@0xproject/order-utils';
 | 
			
		||||
import { assetDataUtils, orderHashUtils } from '@0xproject/order-utils';
 | 
			
		||||
import { SignedOrder } from '@0xproject/types';
 | 
			
		||||
import { BigNumber } from '@0xproject/utils';
 | 
			
		||||
import * as chai from 'chai';
 | 
			
		||||
@@ -126,22 +126,6 @@ describe('Exchange libs', () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('LibOrder', () => {
 | 
			
		||||
        describe('getOrderSchema', () => {
 | 
			
		||||
            it('should output the correct order schema hash', async () => {
 | 
			
		||||
                const orderSchema = await libs.getOrderSchemaHash.callAsync();
 | 
			
		||||
                const schemaHashBuffer = orderHashUtils._getOrderSchemaBuffer();
 | 
			
		||||
                const schemaHashHex = `0x${schemaHashBuffer.toString('hex')}`;
 | 
			
		||||
                expect(schemaHashHex).to.be.equal(orderSchema);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        describe('getDomainSeparatorSchema', () => {
 | 
			
		||||
            it('should output the correct domain separator schema hash', async () => {
 | 
			
		||||
                const domainSeparatorSchema = await libs.getDomainSeparatorSchemaHash.callAsync();
 | 
			
		||||
                const domainSchemaBuffer = eip712Utils._getDomainSeparatorSchemaBuffer();
 | 
			
		||||
                const schemaHashHex = `0x${domainSchemaBuffer.toString('hex')}`;
 | 
			
		||||
                expect(schemaHashHex).to.be.equal(domainSeparatorSchema);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        describe('getOrderHash', () => {
 | 
			
		||||
            it('should output the correct orderHash', async () => {
 | 
			
		||||
                signedOrder = await orderFactory.newSignedOrderAsync();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,22 @@
 | 
			
		||||
import { EIP712Schema, EIP712Types, eip712Utils, generatePseudoRandomSalt } from '@0xproject/order-utils';
 | 
			
		||||
import {
 | 
			
		||||
    EIP712_DOMAIN_NAME,
 | 
			
		||||
    EIP712_DOMAIN_SCHEMA,
 | 
			
		||||
    EIP712_DOMAIN_VERSION,
 | 
			
		||||
    generatePseudoRandomSalt,
 | 
			
		||||
} from '@0xproject/order-utils';
 | 
			
		||||
import { SignatureType } from '@0xproject/types';
 | 
			
		||||
import { signTypedDataUtils } from '@0xproject/utils';
 | 
			
		||||
import * as ethUtil from 'ethereumjs-util';
 | 
			
		||||
 | 
			
		||||
import { signingUtils } from './signing_utils';
 | 
			
		||||
import { SignedTransaction } from './types';
 | 
			
		||||
 | 
			
		||||
const EIP712_ZEROEX_TRANSACTION_SCHEMA: EIP712Schema = {
 | 
			
		||||
const EIP712_ZEROEX_TRANSACTION_SCHEMA = {
 | 
			
		||||
    name: 'ZeroExTransaction',
 | 
			
		||||
    parameters: [
 | 
			
		||||
        { name: 'salt', type: EIP712Types.Uint256 },
 | 
			
		||||
        { name: 'signerAddress', type: EIP712Types.Address },
 | 
			
		||||
        { name: 'data', type: EIP712Types.Bytes },
 | 
			
		||||
        { name: 'salt', type: 'uint256' },
 | 
			
		||||
        { name: 'signerAddress', type: 'address' },
 | 
			
		||||
        { name: 'data', type: 'bytes' },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -27,20 +33,30 @@ export class TransactionFactory {
 | 
			
		||||
        const salt = generatePseudoRandomSalt();
 | 
			
		||||
        const signerAddress = `0x${this._signerBuff.toString('hex')}`;
 | 
			
		||||
        const executeTransactionData = {
 | 
			
		||||
            salt,
 | 
			
		||||
            salt: salt.toString(),
 | 
			
		||||
            signerAddress,
 | 
			
		||||
            data,
 | 
			
		||||
        };
 | 
			
		||||
        const executeTransactionHashBuff = eip712Utils.structHash(
 | 
			
		||||
            EIP712_ZEROEX_TRANSACTION_SCHEMA,
 | 
			
		||||
            executeTransactionData,
 | 
			
		||||
        );
 | 
			
		||||
        const txHash = eip712Utils.createEIP712Message(executeTransactionHashBuff, this._exchangeAddress);
 | 
			
		||||
        const signature = signingUtils.signMessage(txHash, this._privateKey, signatureType);
 | 
			
		||||
        const typedData = {
 | 
			
		||||
            types: {
 | 
			
		||||
                EIP712Domain: EIP712_DOMAIN_SCHEMA.parameters,
 | 
			
		||||
                ZeroExTransaction: EIP712_ZEROEX_TRANSACTION_SCHEMA.parameters,
 | 
			
		||||
            },
 | 
			
		||||
            domain: {
 | 
			
		||||
                name: EIP712_DOMAIN_NAME,
 | 
			
		||||
                version: EIP712_DOMAIN_VERSION,
 | 
			
		||||
                verifyingContract: this._exchangeAddress,
 | 
			
		||||
            },
 | 
			
		||||
            message: executeTransactionData,
 | 
			
		||||
            primaryType: EIP712_ZEROEX_TRANSACTION_SCHEMA.name,
 | 
			
		||||
        };
 | 
			
		||||
        const eip712MessageBuffer = signTypedDataUtils.signTypedDataHash(typedData);
 | 
			
		||||
        const signature = signingUtils.signMessage(eip712MessageBuffer, this._privateKey, signatureType);
 | 
			
		||||
        const signedTx = {
 | 
			
		||||
            exchangeAddress: this._exchangeAddress,
 | 
			
		||||
            signature: `0x${signature.toString('hex')}`,
 | 
			
		||||
            ...executeTransactionData,
 | 
			
		||||
            salt,
 | 
			
		||||
        };
 | 
			
		||||
        return signedTx;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,21 @@
 | 
			
		||||
[
 | 
			
		||||
    {
 | 
			
		||||
        "version": "2.0.0",
 | 
			
		||||
        "changes": [
 | 
			
		||||
            {
 | 
			
		||||
                "note": "Added ecSignOrderAsync to first sign an order as EIP712 and fallback to EthSign",
 | 
			
		||||
                "pr": 1102
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "note": "Added ecSignTypedDataOrderAsync to sign an order exclusively as EIP712",
 | 
			
		||||
                "pr": 1102
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "note": "Rename ecSignOrderHashAsync to ecSignHashAsync removing SignerType parameter",
 | 
			
		||||
                "pr": 1102
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "version": "1.0.7",
 | 
			
		||||
        "changes": [
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ export const EIP712_DOMAIN_SCHEMA = {
 | 
			
		||||
    name: 'EIP712Domain',
 | 
			
		||||
    parameters: [
 | 
			
		||||
        { name: 'name', type: 'string' },
 | 
			
		||||
        { name: 'version', type: 'string ' },
 | 
			
		||||
        { name: 'version', type: 'string' },
 | 
			
		||||
        { name: 'verifyingContract', type: 'address' },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,8 @@ export { ExchangeTransferSimulator } from './exchange_transfer_simulator';
 | 
			
		||||
export { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store';
 | 
			
		||||
export { OrderFilledCancelledLazyStore } from './store/order_filled_cancelled_lazy_store';
 | 
			
		||||
 | 
			
		||||
export { EIP712_DOMAIN_NAME, EIP712_DOMAIN_SCHEMA, EIP712_DOMAIN_VERSION } from './constants';
 | 
			
		||||
 | 
			
		||||
export { Provider, JSONRPCRequestPayload, JSONRPCErrorCallback, JSONRPCResponsePayload } from 'ethereum-types';
 | 
			
		||||
export {
 | 
			
		||||
    SignedOrder,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,12 @@
 | 
			
		||||
        "version": "2.1.0",
 | 
			
		||||
        "changes": [
 | 
			
		||||
            {
 | 
			
		||||
                "note": "Add Metamask Subprovider to handle inconsistent JSON RPC behaviour"
 | 
			
		||||
                "note": "Add Metamask Subprovider to handle inconsistent JSON RPC behaviour",
 | 
			
		||||
                "pr": 1102
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                "note": "Add support for eth_signTypedData in Mnemonic, Private and EthLightWallet",
 | 
			
		||||
                "pr": 1102
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ export abstract class BaseWalletSubprovider extends Subprovider {
 | 
			
		||||
    public abstract async getAccountsAsync(): Promise<string[]>;
 | 
			
		||||
    public abstract async signTransactionAsync(txParams: PartialTxParams): Promise<string>;
 | 
			
		||||
    public abstract async signPersonalMessageAsync(data: string, address: string): Promise<string>;
 | 
			
		||||
    public abstract async signTypedDataAsync(address: string, typedData: any): Promise<string>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This method conforms to the web3-provider-engine interface.
 | 
			
		||||
@@ -36,6 +37,8 @@ export abstract class BaseWalletSubprovider extends Subprovider {
 | 
			
		||||
    public async handleRequest(payload: JSONRPCRequestPayload, next: Callback, end: ErrorCallback): Promise<void> {
 | 
			
		||||
        let accounts;
 | 
			
		||||
        let txParams;
 | 
			
		||||
        let address;
 | 
			
		||||
        let typedData;
 | 
			
		||||
        switch (payload.method) {
 | 
			
		||||
            case 'eth_coinbase':
 | 
			
		||||
                try {
 | 
			
		||||
@@ -86,7 +89,7 @@ export abstract class BaseWalletSubprovider extends Subprovider {
 | 
			
		||||
            case 'eth_sign':
 | 
			
		||||
            case 'personal_sign':
 | 
			
		||||
                const data = payload.method === 'eth_sign' ? payload.params[1] : payload.params[0];
 | 
			
		||||
                const address = payload.method === 'eth_sign' ? payload.params[0] : payload.params[1];
 | 
			
		||||
                address = payload.method === 'eth_sign' ? payload.params[0] : payload.params[1];
 | 
			
		||||
                try {
 | 
			
		||||
                    const ecSignatureHex = await this.signPersonalMessageAsync(data, address);
 | 
			
		||||
                    end(null, ecSignatureHex);
 | 
			
		||||
@@ -94,6 +97,15 @@ export abstract class BaseWalletSubprovider extends Subprovider {
 | 
			
		||||
                    end(err);
 | 
			
		||||
                }
 | 
			
		||||
                return;
 | 
			
		||||
            case 'eth_signTypedData':
 | 
			
		||||
                [address, typedData] = payload.params;
 | 
			
		||||
                try {
 | 
			
		||||
                    const signature = await this.signTypedDataAsync(address, typedData);
 | 
			
		||||
                    end(null, signature);
 | 
			
		||||
                } catch (err) {
 | 
			
		||||
                    end(err);
 | 
			
		||||
                }
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                next();
 | 
			
		||||
 
 | 
			
		||||
@@ -57,7 +57,7 @@ export class EthLightwalletSubprovider extends BaseWalletSubprovider {
 | 
			
		||||
    /**
 | 
			
		||||
     * Sign a personal Ethereum signed message. The signing account will be the account
 | 
			
		||||
     * associated with the provided address.
 | 
			
		||||
     * If you've added the MnemonicWalletSubprovider to your app's provider, you can simply send an `eth_sign`
 | 
			
		||||
     * If you've added the this Subprovider to your app's provider, you can simply send an `eth_sign`
 | 
			
		||||
     * or `personal_sign` JSON RPC request, and this method will be called auto-magically.
 | 
			
		||||
     * If you are not using this via a ProviderEngine instance, you can call it directly.
 | 
			
		||||
     * @param data Hex string message to sign
 | 
			
		||||
@@ -71,4 +71,20 @@ export class EthLightwalletSubprovider extends BaseWalletSubprovider {
 | 
			
		||||
        const result = privKeyWallet.signPersonalMessageAsync(data, address);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * Sign an EIP712 Typed Data message. The signing address will associated with the provided address.
 | 
			
		||||
     * If you've added this Subprovider to your app's provider, you can simply send an `eth_signTypedData`
 | 
			
		||||
     * JSON RPC request, and this method will be called auto-magically.
 | 
			
		||||
     * If you are not using this via a ProviderEngine instance, you can call it directly.
 | 
			
		||||
     * @param address Address of the account to sign with
 | 
			
		||||
     * @param data the typed data object
 | 
			
		||||
     * @return Signature hex string (order: rsv)
 | 
			
		||||
     */
 | 
			
		||||
    public async signTypedDataAsync(address: string, typedData: any): Promise<string> {
 | 
			
		||||
        let privKey = this._keystore.exportPrivateKey(address, this._pwDerivedKey);
 | 
			
		||||
        const privKeyWallet = new PrivateKeyWalletSubprovider(privKey);
 | 
			
		||||
        privKey = '';
 | 
			
		||||
        const result = privKeyWallet.signTypedDataAsync(address, typedData);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -187,6 +187,16 @@ export class LedgerSubprovider extends BaseWalletSubprovider {
 | 
			
		||||
            throw err;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * eth_signTypedData is currently not supported on Ledger devices.
 | 
			
		||||
     * @param address Address of the account to sign with
 | 
			
		||||
     * @param data the typed data object
 | 
			
		||||
     * @return Signature hex string (order: rsv)
 | 
			
		||||
     */
 | 
			
		||||
    // tslint:disable-next-line:prefer-function-over-method
 | 
			
		||||
    public async signTypedDataAsync(address: string, typedData: any): Promise<string> {
 | 
			
		||||
        throw new Error(WalletSubproviderErrors.MethodNotSupported);
 | 
			
		||||
    }
 | 
			
		||||
    private async _createLedgerClientAsync(): Promise<LedgerEthereumClient> {
 | 
			
		||||
        await this._connectionLock.acquire();
 | 
			
		||||
        if (!_.isUndefined(this._ledgerClientIfExists)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -108,6 +108,25 @@ export class MnemonicWalletSubprovider extends BaseWalletSubprovider {
 | 
			
		||||
        const sig = await privateKeyWallet.signPersonalMessageAsync(data, address);
 | 
			
		||||
        return sig;
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * Sign an EIP712 Typed Data message.  The signing account will be the account
 | 
			
		||||
     * associated with the provided address.
 | 
			
		||||
     * If you've added this MnemonicWalletSubprovider to your app's provider, you can simply send an `eth_signTypedData`
 | 
			
		||||
     * JSON RPC request, and this method will be called auto-magically.
 | 
			
		||||
     * If you are not using this via a ProviderEngine instance, you can call it directly.
 | 
			
		||||
     * @param address Address of the account to sign with
 | 
			
		||||
     * @param data the typed data object
 | 
			
		||||
     * @return Signature hex string (order: rsv)
 | 
			
		||||
     */
 | 
			
		||||
    public async signTypedDataAsync(address: string, typedData: any): Promise<string> {
 | 
			
		||||
        if (_.isUndefined(typedData)) {
 | 
			
		||||
            throw new Error(WalletSubproviderErrors.DataMissingForSignPersonalMessage);
 | 
			
		||||
        }
 | 
			
		||||
        assert.isETHAddressHex('address', address);
 | 
			
		||||
        const privateKeyWallet = this._privateKeyWalletForAddress(address);
 | 
			
		||||
        const sig = await privateKeyWallet.signTypedDataAsync(address, typedData);
 | 
			
		||||
        return sig;
 | 
			
		||||
    }
 | 
			
		||||
    private _privateKeyWalletForAddress(address: string): PrivateKeyWalletSubprovider {
 | 
			
		||||
        const derivedKeyInfo = this._findDerivedKeyInfoForAddress(address);
 | 
			
		||||
        const privateKeyHex = derivedKeyInfo.hdKey.privateKey.toString('hex');
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { assert } from '@0xproject/assert';
 | 
			
		||||
import { signTypedDataUtils } from '@0xproject/utils';
 | 
			
		||||
import EthereumTx = require('ethereumjs-tx');
 | 
			
		||||
import * as ethUtil from 'ethereumjs-util';
 | 
			
		||||
import * as _ from 'lodash';
 | 
			
		||||
@@ -84,4 +85,29 @@ export class PrivateKeyWalletSubprovider extends BaseWalletSubprovider {
 | 
			
		||||
        const rpcSig = ethUtil.toRpcSig(sig.v, sig.r, sig.s);
 | 
			
		||||
        return rpcSig;
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * Sign an EIP712 Typed Data message. The signing address will be calculated from the private key.
 | 
			
		||||
     * The address must be provided it must match the address calculated from the private key.
 | 
			
		||||
     * If you've added this Subprovider to your app's provider, you can simply send an `eth_signTypedData`
 | 
			
		||||
     * JSON RPC request, and this method will be called auto-magically.
 | 
			
		||||
     * If you are not using this via a ProviderEngine instance, you can call it directly.
 | 
			
		||||
     * @param address Address of the account to sign with
 | 
			
		||||
     * @param data the typed data object
 | 
			
		||||
     * @return Signature hex string (order: rsv)
 | 
			
		||||
     */
 | 
			
		||||
    public async signTypedDataAsync(address: string, typedData: any): Promise<string> {
 | 
			
		||||
        if (_.isUndefined(typedData)) {
 | 
			
		||||
            throw new Error(WalletSubproviderErrors.DataMissingForSignTypedData);
 | 
			
		||||
        }
 | 
			
		||||
        assert.isETHAddressHex('address', address);
 | 
			
		||||
        if (address !== this._address) {
 | 
			
		||||
            throw new Error(
 | 
			
		||||
                `Requested to sign message with address: ${address}, instantiated with address: ${this._address}`,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        const dataBuff = signTypedDataUtils.signTypedDataHash(typedData);
 | 
			
		||||
        const sig = ethUtil.ecsign(dataBuff, this._privateKeyBuffer);
 | 
			
		||||
        const rpcSig = ethUtil.toRpcSig(sig.v, sig.r, sig.s);
 | 
			
		||||
        return rpcSig;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -107,8 +107,10 @@ export interface ResponseWithTxParams {
 | 
			
		||||
export enum WalletSubproviderErrors {
 | 
			
		||||
    AddressNotFound = 'ADDRESS_NOT_FOUND',
 | 
			
		||||
    DataMissingForSignPersonalMessage = 'DATA_MISSING_FOR_SIGN_PERSONAL_MESSAGE',
 | 
			
		||||
    DataMissingForSignTypedData = 'DATA_MISSING_FOR_SIGN_TYPED_DATA',
 | 
			
		||||
    SenderInvalidOrNotSupplied = 'SENDER_INVALID_OR_NOT_SUPPLIED',
 | 
			
		||||
    FromAddressMissingOrInvalid = 'FROM_ADDRESS_MISSING_OR_INVALID',
 | 
			
		||||
    MethodNotSupported = 'METHOD_NOT_SUPPORTED',
 | 
			
		||||
}
 | 
			
		||||
export enum LedgerSubproviderErrors {
 | 
			
		||||
    TooOldLedgerFirmware = 'TOO_OLD_LEDGER_FIRMWARE',
 | 
			
		||||
 
 | 
			
		||||
@@ -47,6 +47,13 @@ describe('MnemonicWalletSubprovider', () => {
 | 
			
		||||
                const txHex = await subprovider.signTransactionAsync(txData);
 | 
			
		||||
                expect(txHex).to.be.equal(fixtureData.TX_DATA_ACCOUNT_1_SIGNED_RESULT);
 | 
			
		||||
            });
 | 
			
		||||
            it('signs an EIP712 sign typed data message', async () => {
 | 
			
		||||
                const signature = await subprovider.signTypedDataAsync(
 | 
			
		||||
                    fixtureData.TEST_RPC_ACCOUNT_0,
 | 
			
		||||
                    fixtureData.EIP712_TEST_TYPED_DATA,
 | 
			
		||||
                );
 | 
			
		||||
                expect(signature).to.be.equal(fixtureData.EIP712_TEST_TYPED_DATA_SIGNED_RESULT);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        describe('failure cases', () => {
 | 
			
		||||
            it('throws an error if address is invalid ', async () => {
 | 
			
		||||
@@ -118,6 +125,20 @@ describe('MnemonicWalletSubprovider', () => {
 | 
			
		||||
                });
 | 
			
		||||
                provider.sendAsync(payload, callback);
 | 
			
		||||
            });
 | 
			
		||||
            it('signs an EIP712 sign typed data message with eth_signTypedData', (done: DoneCallback) => {
 | 
			
		||||
                const payload = {
 | 
			
		||||
                    jsonrpc: '2.0',
 | 
			
		||||
                    method: 'eth_signTypedData',
 | 
			
		||||
                    params: [fixtureData.TEST_RPC_ACCOUNT_0, fixtureData.EIP712_TEST_TYPED_DATA],
 | 
			
		||||
                    id: 1,
 | 
			
		||||
                };
 | 
			
		||||
                const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => {
 | 
			
		||||
                    expect(err).to.be.a('null');
 | 
			
		||||
                    expect(response.result).to.be.equal(fixtureData.EIP712_TEST_TYPED_DATA_SIGNED_RESULT);
 | 
			
		||||
                    done();
 | 
			
		||||
                });
 | 
			
		||||
                provider.sendAsync(payload, callback);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        describe('failure cases', () => {
 | 
			
		||||
            it('should throw if `data` param not hex when calling eth_sign', (done: DoneCallback) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,13 @@ describe('PrivateKeyWalletSubprovider', () => {
 | 
			
		||||
                const txHex = await subprovider.signTransactionAsync(fixtureData.TX_DATA);
 | 
			
		||||
                expect(txHex).to.be.equal(fixtureData.TX_DATA_SIGNED_RESULT);
 | 
			
		||||
            });
 | 
			
		||||
            it('signs an EIP712 sign typed data message', async () => {
 | 
			
		||||
                const signature = await subprovider.signTypedDataAsync(
 | 
			
		||||
                    fixtureData.TEST_RPC_ACCOUNT_0,
 | 
			
		||||
                    fixtureData.EIP712_TEST_TYPED_DATA,
 | 
			
		||||
                );
 | 
			
		||||
                expect(signature).to.be.equal(fixtureData.EIP712_TEST_TYPED_DATA_SIGNED_RESULT);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    describe('calls through a provider', () => {
 | 
			
		||||
@@ -103,6 +110,20 @@ describe('PrivateKeyWalletSubprovider', () => {
 | 
			
		||||
                });
 | 
			
		||||
                provider.sendAsync(payload, callback);
 | 
			
		||||
            });
 | 
			
		||||
            it('signs an EIP712 sign typed data message with eth_signTypedData', (done: DoneCallback) => {
 | 
			
		||||
                const payload = {
 | 
			
		||||
                    jsonrpc: '2.0',
 | 
			
		||||
                    method: 'eth_signTypedData',
 | 
			
		||||
                    params: [fixtureData.TEST_RPC_ACCOUNT_0, fixtureData.EIP712_TEST_TYPED_DATA],
 | 
			
		||||
                    id: 1,
 | 
			
		||||
                };
 | 
			
		||||
                const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => {
 | 
			
		||||
                    expect(err).to.be.a('null');
 | 
			
		||||
                    expect(response.result).to.be.equal(fixtureData.EIP712_TEST_TYPED_DATA_SIGNED_RESULT);
 | 
			
		||||
                    done();
 | 
			
		||||
                });
 | 
			
		||||
                provider.sendAsync(payload, callback);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        describe('failure cases', () => {
 | 
			
		||||
            it('should throw if `data` param not hex when calling eth_sign', (done: DoneCallback) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -30,4 +30,35 @@ export const fixtureData = {
 | 
			
		||||
        '0xf85f8080822710940000000000000000000000000000000000000000808078a0712854c73c69445cc1b22a7c3d7312ff9a97fe4ffba35fd636e8236b211b6e7ca0647cee031615e52d916c7c707025bc64ad525d8f1b9876c3435a863b42743178',
 | 
			
		||||
    TX_DATA_ACCOUNT_1_SIGNED_RESULT:
 | 
			
		||||
        '0xf85f8080822710940000000000000000000000000000000000000000808078a04b02af7ff3f18ce114b601542cc8ebdc50921354f75dd510d31793453a0710e6a0540082a01e475465801b8186a2edc79ec1a2dcf169b9781c25a58a417023c9ca',
 | 
			
		||||
    EIP712_TEST_TYPED_DATA: {
 | 
			
		||||
        types: {
 | 
			
		||||
            EIP712Domain: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'name',
 | 
			
		||||
                    type: 'string',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            Test: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'testAddress',
 | 
			
		||||
                    type: 'address',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'testNumber',
 | 
			
		||||
                    type: 'uint256',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
        domain: {
 | 
			
		||||
            name: 'Test',
 | 
			
		||||
        },
 | 
			
		||||
        message: {
 | 
			
		||||
            testAddress: '0x0000000000000000000000000000000000000000',
 | 
			
		||||
            testNumber: '12345',
 | 
			
		||||
        },
 | 
			
		||||
        primaryType: 'Test',
 | 
			
		||||
    },
 | 
			
		||||
    EIP712_TEST_TYPED_DATA_HASH: '0xb460d69ca60383293877cd765c0f97bd832d66bca720f7e32222ce1118832493',
 | 
			
		||||
    EIP712_TEST_TYPED_DATA_SIGNED_RESULT:
 | 
			
		||||
        '0x20af5b6bfc3658942198d6eeda159b4ed589f90cee6eac3ba117818ffba5fd7e354a353aad93faabd6eb6c66e17921c92bd1cd09c92a770f554470dc3e254ce701',
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,13 @@
 | 
			
		||||
[
 | 
			
		||||
    {
 | 
			
		||||
        "version": "1.2.0",
 | 
			
		||||
        "changes": [
 | 
			
		||||
            {
 | 
			
		||||
                "note": "Added `EIP712Parameter` `EIP712Types` `EIP712TypedData` for EIP712 signing",
 | 
			
		||||
                "pr": 1102
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "timestamp": 1538693146,
 | 
			
		||||
        "version": "1.1.4",
 | 
			
		||||
 
 | 
			
		||||
@@ -589,3 +589,18 @@ export interface Metadata {
 | 
			
		||||
    externalTypeToLink: ExternalTypeToLink;
 | 
			
		||||
    externalExportToLink: ExternalExportToLink;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EIP712Parameter {
 | 
			
		||||
    name: string;
 | 
			
		||||
    type: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EIP712Types {
 | 
			
		||||
    [key: string]: EIP712Parameter[];
 | 
			
		||||
}
 | 
			
		||||
export interface EIP712TypedData {
 | 
			
		||||
    types: EIP712Types;
 | 
			
		||||
    domain: any;
 | 
			
		||||
    message: any;
 | 
			
		||||
    primaryType: string;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,29 +1,30 @@
 | 
			
		||||
import * as ethUtil from 'ethereumjs-util';
 | 
			
		||||
import * as ethers from 'ethers';
 | 
			
		||||
 | 
			
		||||
export interface EIP712Parameter {
 | 
			
		||||
    name: string;
 | 
			
		||||
    type: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EIP712Types {
 | 
			
		||||
    [key: string]: EIP712Parameter[];
 | 
			
		||||
}
 | 
			
		||||
export interface EIP712TypedData {
 | 
			
		||||
    types: EIP712Types;
 | 
			
		||||
    domain: any;
 | 
			
		||||
    message: any;
 | 
			
		||||
    primaryType: string;
 | 
			
		||||
}
 | 
			
		||||
import { EIP712TypedData, EIP712Types } from '@0xproject/types';
 | 
			
		||||
 | 
			
		||||
export const signTypedDataUtils = {
 | 
			
		||||
    findDependencies(primaryType: string, types: EIP712Types, found: string[] = []): string[] {
 | 
			
		||||
    /**
 | 
			
		||||
     * Computes the Sign Typed Data hash
 | 
			
		||||
     * @param   typedData An object that conforms to the EIP712TypedData interface
 | 
			
		||||
     * @return  A Buffer containing the hash of the sign typed data.
 | 
			
		||||
     */
 | 
			
		||||
    signTypedDataHash(typedData: EIP712TypedData): Buffer {
 | 
			
		||||
        return ethUtil.sha3(
 | 
			
		||||
            Buffer.concat([
 | 
			
		||||
                Buffer.from('1901', 'hex'),
 | 
			
		||||
                signTypedDataUtils._structHash('EIP712Domain', typedData.domain, typedData.types),
 | 
			
		||||
                signTypedDataUtils._structHash(typedData.primaryType, typedData.message, typedData.types),
 | 
			
		||||
            ]),
 | 
			
		||||
        );
 | 
			
		||||
    },
 | 
			
		||||
    _findDependencies(primaryType: string, types: EIP712Types, found: string[] = []): string[] {
 | 
			
		||||
        if (found.includes(primaryType) || types[primaryType] === undefined) {
 | 
			
		||||
            return found;
 | 
			
		||||
        }
 | 
			
		||||
        found.push(primaryType);
 | 
			
		||||
        for (const field of types[primaryType]) {
 | 
			
		||||
            for (const dep of signTypedDataUtils.findDependencies(field.type, types, found)) {
 | 
			
		||||
            for (const dep of signTypedDataUtils._findDependencies(field.type, types, found)) {
 | 
			
		||||
                if (!found.includes(dep)) {
 | 
			
		||||
                    found.push(dep);
 | 
			
		||||
                }
 | 
			
		||||
@@ -31,8 +32,8 @@ export const signTypedDataUtils = {
 | 
			
		||||
        }
 | 
			
		||||
        return found;
 | 
			
		||||
    },
 | 
			
		||||
    encodeType(primaryType: string, types: EIP712Types): string {
 | 
			
		||||
        let deps = signTypedDataUtils.findDependencies(primaryType, types);
 | 
			
		||||
    _encodeType(primaryType: string, types: EIP712Types): string {
 | 
			
		||||
        let deps = signTypedDataUtils._findDependencies(primaryType, types);
 | 
			
		||||
        deps = deps.filter(d => d !== primaryType);
 | 
			
		||||
        deps = [primaryType].concat(deps.sort());
 | 
			
		||||
        let result = '';
 | 
			
		||||
@@ -41,9 +42,9 @@ export const signTypedDataUtils = {
 | 
			
		||||
        }
 | 
			
		||||
        return result;
 | 
			
		||||
    },
 | 
			
		||||
    encodeData(primaryType: string, data: any, types: EIP712Types): string {
 | 
			
		||||
    _encodeData(primaryType: string, data: any, types: EIP712Types): string {
 | 
			
		||||
        const encodedTypes = ['bytes32'];
 | 
			
		||||
        const encodedValues = [signTypedDataUtils.typeHash(primaryType, types)];
 | 
			
		||||
        const encodedValues = [signTypedDataUtils._typeHash(primaryType, types)];
 | 
			
		||||
        for (const field of types[primaryType]) {
 | 
			
		||||
            let value = data[field.name];
 | 
			
		||||
            if (field.type === 'string' || field.type === 'bytes') {
 | 
			
		||||
@@ -52,7 +53,7 @@ export const signTypedDataUtils = {
 | 
			
		||||
                encodedValues.push(value);
 | 
			
		||||
            } else if (types[field.type] !== undefined) {
 | 
			
		||||
                encodedTypes.push('bytes32');
 | 
			
		||||
                value = ethUtil.sha3(signTypedDataUtils.encodeData(field.type, value, types));
 | 
			
		||||
                value = ethUtil.sha3(signTypedDataUtils._encodeData(field.type, value, types));
 | 
			
		||||
                encodedValues.push(value);
 | 
			
		||||
            } else if (field.type.lastIndexOf(']') === field.type.length - 1) {
 | 
			
		||||
                throw new Error('Arrays currently unimplemented in encodeData');
 | 
			
		||||
@@ -63,19 +64,10 @@ export const signTypedDataUtils = {
 | 
			
		||||
        }
 | 
			
		||||
        return ethers.utils.defaultAbiCoder.encode(encodedTypes, encodedValues);
 | 
			
		||||
    },
 | 
			
		||||
    typeHash(primaryType: string, types: EIP712Types): Buffer {
 | 
			
		||||
        return ethUtil.sha3(signTypedDataUtils.encodeType(primaryType, types));
 | 
			
		||||
    _typeHash(primaryType: string, types: EIP712Types): Buffer {
 | 
			
		||||
        return ethUtil.sha3(signTypedDataUtils._encodeType(primaryType, types));
 | 
			
		||||
    },
 | 
			
		||||
    structHash(primaryType: string, data: any, types: EIP712Types): Buffer {
 | 
			
		||||
        return ethUtil.sha3(signTypedDataUtils.encodeData(primaryType, data, types));
 | 
			
		||||
    },
 | 
			
		||||
    signTypedDataHash(typedData: EIP712TypedData): Buffer {
 | 
			
		||||
        return ethUtil.sha3(
 | 
			
		||||
            Buffer.concat([
 | 
			
		||||
                Buffer.from('1901', 'hex'),
 | 
			
		||||
                signTypedDataUtils.structHash('EIP712Domain', typedData.domain, typedData.types),
 | 
			
		||||
                signTypedDataUtils.structHash(typedData.primaryType, typedData.message, typedData.types),
 | 
			
		||||
            ]),
 | 
			
		||||
        );
 | 
			
		||||
    _structHash(primaryType: string, data: any, types: EIP712Types): Buffer {
 | 
			
		||||
        return ethUtil.sha3(signTypedDataUtils._encodeData(primaryType, data, types));
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,37 @@ const expect = chai.expect;
 | 
			
		||||
 | 
			
		||||
describe('signTypedDataUtils', () => {
 | 
			
		||||
    describe('signTypedDataHash', () => {
 | 
			
		||||
        const signTypedDataHashHex = '0x55eaa6ec02f3224d30873577e9ddd069a288c16d6fb407210eecbc501fa76692';
 | 
			
		||||
        const signTypedData = {
 | 
			
		||||
        const simpleSignTypedDataHashHex = '0xb460d69ca60383293877cd765c0f97bd832d66bca720f7e32222ce1118832493';
 | 
			
		||||
        const simpleSignTypedData = {
 | 
			
		||||
            types: {
 | 
			
		||||
                EIP712Domain: [
 | 
			
		||||
                    {
 | 
			
		||||
                        name: 'name',
 | 
			
		||||
                        type: 'string',
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
                Test: [
 | 
			
		||||
                    {
 | 
			
		||||
                        name: 'testAddress',
 | 
			
		||||
                        type: 'address',
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        name: 'testNumber',
 | 
			
		||||
                        type: 'uint256',
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
            domain: {
 | 
			
		||||
                name: 'Test',
 | 
			
		||||
            },
 | 
			
		||||
            message: {
 | 
			
		||||
                testAddress: '0x0000000000000000000000000000000000000000',
 | 
			
		||||
                testNumber: '12345',
 | 
			
		||||
            },
 | 
			
		||||
            primaryType: 'Test',
 | 
			
		||||
        };
 | 
			
		||||
        const orderSignTypedDataHashHex = '0x55eaa6ec02f3224d30873577e9ddd069a288c16d6fb407210eecbc501fa76692';
 | 
			
		||||
        const orderSignTypedData = {
 | 
			
		||||
            types: {
 | 
			
		||||
                EIP712Domain: [
 | 
			
		||||
                    {
 | 
			
		||||
@@ -97,11 +126,15 @@ describe('signTypedDataUtils', () => {
 | 
			
		||||
            },
 | 
			
		||||
            primaryType: 'Order',
 | 
			
		||||
        };
 | 
			
		||||
        it.only('creates a known hash of the sign typed data', () => {
 | 
			
		||||
            const hash = signTypedDataUtils.signTypedDataHash(signTypedData).toString('hex');
 | 
			
		||||
        it('creates a hash of the test sign typed data', () => {
 | 
			
		||||
            const hash = signTypedDataUtils.signTypedDataHash(simpleSignTypedData).toString('hex');
 | 
			
		||||
            const hashHex = `0x${hash}`;
 | 
			
		||||
            expect(hashHex).to.be.eq(signTypedDataHashHex);
 | 
			
		||||
            console.log(hash);
 | 
			
		||||
            expect(hashHex).to.be.eq(simpleSignTypedDataHashHex);
 | 
			
		||||
        });
 | 
			
		||||
        it('creates a hash of the order sign typed data', () => {
 | 
			
		||||
            const hash = signTypedDataUtils.signTypedDataHash(orderSignTypedData).toString('hex');
 | 
			
		||||
            const hashHex = `0x${hash}`;
 | 
			
		||||
            expect(hashHex).to.be.eq(orderSignTypedDataHashHex);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user