Skip to content
Open
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
1 change: 1 addition & 0 deletions src/components/signatures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './signTypedData';
export * from './signTypedData-variants';
export * from './signTypedDataV3-sign';
export * from './signTypedDataV4-sign';
export * from './signTypedDataV4-sign-with-salt';
export * from './siwe';
export * from './malformed-signatures';
export * from './malformed-transactions';
251 changes: 251 additions & 0 deletions src/components/signatures/signTypedDataV4-sign-with-salt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import { recoverTypedSignature } from '@metamask/eth-sig-util';
import { toChecksumAddress } from 'ethereumjs-util';
import globalContext from '../..';

export function signTypedDataV4WithSaltComponent(parentContainer) {
parentContainer.insertAdjacentHTML(
'beforeend',
`<div class="col-xl-4 col-lg-6 col-md-12 col-sm-12 col-12 d-flex align-items-stretch">
<div class="card full-width">
<div class="card-body">
<h4>
Sign Typed Data V4 with salt
</h4>

<button
class="btn btn-primary btn-lg btn-block mb-3"
id="signTypedDataV4WithSalt"
disabled
>
Sign
</button>

<p class="info-text alert alert-warning">
Result:
<span id="signTypedDataV4WithSaltResult"></span>
</p>

<button
class="btn btn-primary btn-lg btn-block mb-3"
id="signTypedDataV4WithSaltVerify"
disabled
>
Verify
</button>

<p class="info-text alert alert-warning">
Recovery result:
<span id="signTypedDataV4WithSaltVerifyResult"></span>
</p>
</div>
</div>
</div>`,
);

const signTypedDataV4WithSalt = document.getElementById(
'signTypedDataV4WithSalt',
);
const signTypedDataV4WithSaltResult = document.getElementById(
'signTypedDataV4WithSaltResult',
);
const signTypedDataV4WithSaltVerify = document.getElementById(
'signTypedDataV4WithSaltVerify',
);
const signTypedDataV4WithSaltVerifyResult = document.getElementById(
'signTypedDataV4WithSaltVerifyResult',
);

document.addEventListener('globalConnectionChange', function (e) {
if (e.detail.connected) {
// MetaMask is connected, enable the button
signTypedDataV4WithSalt.disabled = false;
}
});

document.addEventListener('disableAndClear', function () {
signTypedDataV4WithSalt.disabled = true;
signTypedDataV4WithSaltVerify.disabled = true;
});

/**
* Sign Typed Data V4
*/
signTypedDataV4WithSalt.onclick = async () => {
const msgParams = {
domain: {
chainId: globalContext.chainIdInt.toString(),
Copy link

Copilot AI Apr 22, 2025

Choose a reason for hiding this comment

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

The chainId is converted to a string during signing, but in verification it is used as a number (line 163). Consider using the same data type for chainId in both places to prevent potential verification issues.

Suggested change
chainId: globalContext.chainIdInt.toString(),
chainId: globalContext.chainIdInt,

Copilot uses AI. Check for mistakes.
name: 'Ether Mail',
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
version: '1',
salt: 'test',
Copy link

Copilot AI Apr 22, 2025

Choose a reason for hiding this comment

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

The salt parameter is hard-coded as 'test'. If this is only intended for development, consider adding a clear comment or mechanism to supply a proper salt value for production use.

Copilot uses AI. Check for mistakes.
},
message: {
contents: 'Hello, Bob!',
from: {
name: 'Cow',
wallets: [
'0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
'0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF',
],
},
to: [
{
name: 'Bob',
wallets: [
'0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
'0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57',
'0xB0B0b0b0b0b0B000000000000000000000000000',
],
},
],
attachment: '0x',
},
primaryType: 'Mail',
types: {
EIP712Domain: [
{
name: 'name',
type: 'string',
},
{
name: 'version',
type: 'string',
},
{
name: 'chainId',
type: 'uint256',
},
{
name: 'verifyingContract',
type: 'string',
},
{
name: 'salt',
type: 'string',
},
],
Group: [
{ name: 'name', type: 'string' },
{ name: 'members', type: 'Person[]' },
],
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person[]' },
{ name: 'contents', type: 'string' },
{ name: 'attachment', type: 'bytes' },
],
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallets', type: 'address[]' },
],
},
};
try {
const from = globalContext.accounts[0];
const sign = await globalContext.provider.request({
method: 'eth_signTypedData_v4',
params: [from, JSON.stringify(msgParams)],
});
signTypedDataV4WithSaltResult.innerHTML = sign;
signTypedDataV4WithSaltVerify.disabled = false;
} catch (err) {
console.error(err);
signTypedDataV4WithSaltResult.innerHTML = `Error: ${err.message}`;
}
};

/**
* Sign Typed Data V4 Verification
*/
signTypedDataV4WithSaltVerify.onclick = async () => {
const msgParams = {
domain: {
chainId: globalContext.chainIdInt,
name: 'Ether Mail',
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
version: '1',
salt: 'test',
},
message: {
contents: 'Hello, Bob!',
from: {
name: 'Cow',
wallets: [
'0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
'0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF',
],
},
to: [
{
name: 'Bob',
wallets: [
'0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
'0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57',
'0xB0B0b0b0b0b0B000000000000000000000000000',
],
},
],
attachment: '0x',
},
primaryType: 'Mail',
types: {
EIP712Domain: [
{
name: 'name',
type: 'string',
},
{
name: 'version',
type: 'string',
},
{
name: 'chainId',
type: 'uint256',
},
{
name: 'verifyingContract',
type: 'string',
},
{
name: 'salt',
type: 'string',
},
],
Group: [
{ name: 'name', type: 'string' },
{ name: 'members', type: 'Person[]' },
],
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person[]' },
{ name: 'contents', type: 'string' },
{ name: 'attachment', type: 'bytes' },
],
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallets', type: 'address[]' },
],
},
};
try {
const from = globalContext.accounts[0];
const sign = signTypedDataV4WithSaltResult.innerHTML;
const recoveredAddr = recoverTypedSignature({
data: msgParams,
signature: sign,
version: 'V4',
});
if (toChecksumAddress(recoveredAddr) === toChecksumAddress(from)) {
console.log(`Successfully verified signer as ${recoveredAddr}`);
signTypedDataV4WithSaltVerifyResult.innerHTML = recoveredAddr;
} else {
console.log(
`Failed to verify signer when comparing ${recoveredAddr} to ${from}`,
);
}
} catch (err) {
console.error(err);
signTypedDataV4WithSaltVerifyResult.innerHTML = `Error: ${err.message}`;
}
};
}
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
signTypedDataVariantsComponent,
signTypedDataV3Component,
signTypedDataV4Component,
signTypedDataV4WithSaltComponent,
siweComponent,
malformedSignaturesComponent,
malformedTransactionsComponent,
Expand Down Expand Up @@ -201,6 +202,7 @@ personalSignComponent(signaturesRow);
signTypedDataComponent(signaturesRow);
signTypedDataV3Component(signaturesRow);
signTypedDataV4Component(signaturesRow);
signTypedDataV4WithSaltComponent(signaturesRow);
permitSignComponent(signaturesRow);
signTypedDataVariantsComponent(signaturesRow);
siweComponent(signaturesRow);
Expand Down
Loading