Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 5 additions & 27 deletions src/utils/normalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const MESSAGE_DATA_MOCK = {
name: 'Liquid staked Ether 2.0',
version: '2',
chainId: '0x1',
verifyingContract: '996101235222674412020337938588541139382869425796',
verifyingContract: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
},
primaryType: 'Permit',
message: {
Expand All @@ -66,38 +66,16 @@ describe('normalizeTypedMessage', () => {
}

it('should normalize verifyingContract address in domain', () => {
const normalizedData = parseNormalizerResult(MESSAGE_DATA_MOCK);
expect(normalizedData.domain.verifyingContract).toBe(
'0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
);
});

it('should normalize verifyingContract address in domain when provided data is an object', () => {
const NON_STRINGIFIED_MESSAGE_DATA_MOCK = MESSAGE_DATA_MOCK;
const normalizedData = JSON.parse(
normalizeTypedMessage(
NON_STRINGIFIED_MESSAGE_DATA_MOCK as unknown as string,
),
);
expect(normalizedData.domain.verifyingContract).toBe(
'0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
);
});

it('should handle octal verifyingContract address by normalizing it', () => {
const expectedNormalizedOctalAddress = '0x53';
const messageDataWithOctalAddress = {
const msgMock = {
...MESSAGE_DATA_MOCK,
domain: {
...MESSAGE_DATA_MOCK.domain,
verifyingContract: '0o123',
verifyingContract: '0Xae7ab96520de3a18e5e111b5eaab095312d7fe84',
},
};

const normalizedData = parseNormalizerResult(messageDataWithOctalAddress);

const normalizedData = parseNormalizerResult(msgMock);
expect(normalizedData.domain.verifyingContract).toBe(
expectedNormalizedOctalAddress,
'0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
);
});

Expand Down
36 changes: 6 additions & 30 deletions src/utils/normalize.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { add0x, isValidHexAddress, isStrictHexString } from '@metamask/utils';
import type { Hex } from '@metamask/utils';
import BN from 'bn.js';

type EIP712Domain = {
verifyingContract: string;
verifyingContract: Hex;
};

type SignTypedMessageDataV3V4 = {
Expand Down Expand Up @@ -45,7 +43,7 @@ export function normalizeTypedMessage(messageData: string) {
* @param data - The messageData to parse.
* @returns The data object for EIP712 normalization.
*/
function parseTypedMessage(data: string) {
export function parseTypedMessage(data: string) {
if (typeof data !== 'string') {
return data;
}
Expand All @@ -54,36 +52,14 @@ function parseTypedMessage(data: string) {
}

/**
* Normalizes the address to a hexadecimal format
* Normalizes the address to standard hexadecimal format
*
* @param address - The address to normalize.
* @returns The normalized address.
*/
function normalizeContractAddress(address: string): Hex | string {
if (isStrictHexString(address) && isValidHexAddress(address)) {
return address;
function normalizeContractAddress(address: Hex): Hex {
if (address.startsWith('0X')) {
return `0x${address.slice(2)}`;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normilizing decimal and octal value is removed now, only normalization now is replacing 0X prefix with 0x.


// Check if the address is in octal format, convert to hexadecimal
if (address.startsWith('0o')) {
// If octal, convert to hexadecimal
return octalToHex(address);
}

// Check if the address is in decimal format, convert to hexadecimal
try {
const decimalBN = new BN(address, 10);
const hexString = decimalBN.toString(16);
return add0x(hexString);
} catch (e) {
// Ignore errors and return the original address
}

// Returning the original address without normalization
return address;
}

function octalToHex(octalAddress: string): Hex {
const decimalAddress = parseInt(octalAddress.slice(2), 8).toString(16);
return add0x(decimalAddress);
}
135 changes: 134 additions & 1 deletion src/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ describe('wallet', () => {
},
primaryType: 'EIP712Domain',
domain: {
verifyingContract: '996101235222674412020337938588541139382869425796',
verifyingContract: '0Xae7ab96520de3a18e5e111b5eaab095312d7fe84',
},
message: {},
};
Expand Down Expand Up @@ -391,6 +391,139 @@ describe('wallet', () => {
signatureMethod: 'eth_signTypedData_v3',
});
});

it('should throw if verifyingContract is invalid hex value', async () => {
const { engine } = createTestSetup();
const getAccounts = async () => testAddresses.slice();
const witnessedMsgParams: TypedMessageParams[] = [];
const processTypedMessageV3 = async (msgParams: TypedMessageParams) => {
witnessedMsgParams.push(msgParams);
// Assume testMsgSig is the expected signature result
return testMsgSig;
};

engine.push(
createWalletMiddleware({ getAccounts, processTypedMessageV3 }),
);

const message = {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
},
primaryType: 'EIP712Domain',
domain: {
verifyingContract: '917551056842671309452305380979543736893630245704',
},
message: {},
};

const stringifiedMessage = JSON.stringify(message);

const payload = {
method: 'eth_signTypedData_v3',
params: [testAddresses[0], stringifiedMessage], // Assuming testAddresses[0] is a valid address from your setup
};

const promise = pify(engine.handle).call(engine, payload);
await expect(promise).rejects.toThrow('Invalid input.');
});
});

describe('signTypedDataV4', () => {
const getMsgParams = (verifyingContract?: string) => ({
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
},
primaryType: 'Permit',
domain: {
name: 'MyToken',
version: '1',
verifyingContract:
verifyingContract ?? '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
chainId: '0x1',
},
message: {
owner: testAddresses[0],
spender: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
value: 3000,
nonce: 0,
deadline: 50000000000,
},
});

it('should not throw if request is permit with valid hex value for verifyingContract address', async () => {
const { engine } = createTestSetup();
const getAccounts = async () => testAddresses.slice();
const witnessedMsgParams: TypedMessageParams[] = [];
const processTypedMessageV4 = async (msgParams: TypedMessageParams) => {
witnessedMsgParams.push(msgParams);
// Assume testMsgSig is the expected signature result
return testMsgSig;
};

engine.push(
createWalletMiddleware({ getAccounts, processTypedMessageV4 }),
);

const payload = {
method: 'eth_signTypedData_v4',
params: [testAddresses[0], JSON.stringify(getMsgParams())],
};

const promise = pify(engine.handle).call(engine, payload);
const result = await promise;
expect(result).toStrictEqual({
id: undefined,
jsonrpc: undefined,
result:
'0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c',
});
});

it('should throw if request is permit with invalid hex value for verifyingContract address', async () => {
const { engine } = createTestSetup();
const getAccounts = async () => testAddresses.slice();
const witnessedMsgParams: TypedMessageParams[] = [];
const processTypedMessageV4 = async (msgParams: TypedMessageParams) => {
witnessedMsgParams.push(msgParams);
// Assume testMsgSig is the expected signature result
return testMsgSig;
};

engine.push(
createWalletMiddleware({ getAccounts, processTypedMessageV4 }),
);

const payload = {
method: 'eth_signTypedData_v4',
params: [
testAddresses[0],
JSON.stringify(
getMsgParams('917551056842671309452305380979543736893630245704'),
),
],
};

const promise = pify(engine.handle).call(engine, payload);
await expect(promise).rejects.toThrow('Invalid input.');
});
});

describe('sign', () => {
Expand Down
27 changes: 22 additions & 5 deletions src/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import {
createScaffoldMiddleware,
} from '@metamask/json-rpc-engine';
import { providerErrors, rpcErrors } from '@metamask/rpc-errors';
import type {
Json,
JsonRpcRequest,
PendingJsonRpcResponse,
import {
isValidHexAddress,
type Json,
type JsonRpcRequest,
type PendingJsonRpcResponse,
} from '@metamask/utils';

import type { Block } from './types';
import { normalizeTypedMessage } from './utils/normalize';
import { normalizeTypedMessage, parseTypedMessage } from './utils/normalize';

/*
export type TransactionParams = {
Expand Down Expand Up @@ -278,6 +279,7 @@ WalletMiddlewareOptions): JsonRpcMiddleware<any, Block> {

const address = await validateAndNormalizeKeyholder(params[0], req);
const message = normalizeTypedMessage(params[1]);
validateVerifyingContract(message);
const version = 'V3';
const msgParams: TypedMessageParams = {
data: message,
Expand Down Expand Up @@ -308,6 +310,7 @@ WalletMiddlewareOptions): JsonRpcMiddleware<any, Block> {

const address = await validateAndNormalizeKeyholder(params[0], req);
const message = normalizeTypedMessage(params[1]);
validateVerifyingContract(message);
const version = 'V4';
const msgParams: TypedMessageParams = {
data: message,
Expand Down Expand Up @@ -490,6 +493,20 @@ WalletMiddlewareOptions): JsonRpcMiddleware<any, Block> {
}
}

/**
* Validates verifyingContract of typedSignMessage.
*
* @param data - The data passed in typedSign request.
*/
function validateVerifyingContract(data: string) {
const {
domain: { verifyingContract },
} = parseTypedMessage(data);
if (!isValidHexAddress(verifyingContract)) {
throw rpcErrors.invalidInput();
}
}

function resemblesAddress(str: string): boolean {
// hex prefix 2 + 20 bytes
return str.length === 2 + 20 * 2;
Expand Down