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
108 changes: 108 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ This will generate a set of binaries in `./releases`. Run the one you wish to us
* [Change Withdrawal Credentials](#change-withdrawal-credentials)
* [Get Address](#get-address)
* [Get Public Key](#get-public-key)
* [Sign EIP-712 Data](#sign-eip-712-data)

# 🔗 Connecting to a Lattice

Expand Down Expand Up @@ -182,3 +183,110 @@ The Lattice is able to export a few types of *formatted* addresses, which depend
| `84'` | `0'` | Bitcoin bech32 | `bc1...` |
| `49'` | `0'` | Bitcoin wrapped segwit | `3...` |
| `44'` | `0'` | Bitcoin legacy | `1...` |


## <a id="sign-eip-712-data">🔏 Sign EIP-712 Data</a>

The Lattice can sign [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed structured data, which is commonly used by DeFi protocols, DAOs, and modern dApps for operations like token permits, governance voting, and order signing.

### What is EIP-712?

EIP-712 is a standard for hashing and signing typed structured data. Unlike regular message signing, EIP-712 provides:
- **Type safety**: Data is strongly typed with a defined schema
- **Human readability**: Users can see exactly what they're signing
- **Domain separation**: Prevents signatures from being replayed across different applications

### Using the Sign EIP-712 Command

When you select the `Sign EIP-712 Data` command, you'll be prompted to provide the typed data in one of two ways:

1. **From a JSON file**: Provide the path to a JSON file containing the EIP-712 data
2. **Direct input**: Paste the JSON directly into the CLI

### EIP-712 Data Format

The JSON must contain the following fields:

```json
{
"types": {
"EIP712Domain": [...],
"YourMessageType": [...]
},
"primaryType": "YourMessageType",
"domain": {
"name": "Application Name",
"version": "1",
"chainId": 1,
"verifyingContract": "0x..."
},
"message": {
// Your actual message data
}
}
```

### Example: Token Permit

A common use case is EIP-2612 token permits. See `sample-eip712.json` for a complete example:

```json
{
"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",
"chainId": 1,
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
},
"message": {
"owner": "0x1234567890123456789012345678901234567890",
"spender": "0x2345678901234567890123456789012345678901",
"value": 1000000000000000000,
"nonce": 0,
"deadline": 1234567890
}
}
```

### Signing Process

1. The CLI will validate your EIP-712 data structure
2. You'll see a formatted preview of what you're about to sign
3. After confirmation, the request is sent to your Lattice
4. Confirm the signature on your Lattice device
5. The signature is returned in multiple formats:
- **Full signature**: Complete hex string (65 bytes)
- **Components**: Separate v, r, s values for use in smart contracts

### Saving Signatures

After signing, you can optionally save the signature to a JSON file that includes:
- The complete signature
- Individual v, r, s components
- The original typed data
- A timestamp

This is useful for record-keeping or for later submission to smart contracts or dApps.

### Security Notes

- Always verify the domain and message contents before signing
- The Lattice will display key information about what you're signing
- Never sign data from untrusted sources
- Signatures are binding and cannot be revoked once used on-chain
3 changes: 2 additions & 1 deletion src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './buildDepositData';
export * from './changeBLSCredentials';
export * from './getAddress';
export * from './getPubkey';
export * from './getPubkey';
export * from './signEIP712';
185 changes: 185 additions & 0 deletions src/commands/signEIP712.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { Client } from "gridplus-sdk";
import {
clearPrintedLines,
closeSpinner,
printColor,
startNewSpinner,
pathStrToInt,
} from '../utils';
import {
promptForSelect,
promptForString,
promptForBool,
promptGetPath
} from '../prompts';
import { validateEIP712Data } from '../utils/eip712Validator';
import { formatEIP712ForDisplay, formatSignature } from '../utils/eip712Formatter';
import { readFileSync, writeFileSync } from 'fs';

/**
* Sign EIP-712 typed data using the Lattice hardware wallet.
* Supports multiple input methods and output formats.
*/
export async function cmdSignEIP712(client: Client) {
try {
// Get input method
const inputMethod = await promptForSelect(
"How would you like to provide the EIP-712 data?",
["From file (JSON)", "Direct input (JSON string)", "Cancel"]
);

if (inputMethod === "Cancel") {
return;
}

let typedDataStr: string;

// Get the typed data based on input method
if (inputMethod === "From file (JSON)") {
const filePath = await promptForString("Enter path to JSON file: ");
try {
typedDataStr = readFileSync(filePath, 'utf-8');
} catch (err) {
printColor(`Failed to read file: ${err}`, "red");
return;
}
} else {
typedDataStr = await promptForString(
"Enter EIP-712 JSON (or paste and press Enter): "
);
}

// Parse and validate the typed data
let typedData: any;
try {
typedData = JSON.parse(typedDataStr);
} catch (err) {
printColor("Invalid JSON format", "red");
return;
}

// Validate EIP-712 structure
const validationResult = validateEIP712Data(typedData);
if (!validationResult.isValid) {
printColor(`Invalid EIP-712 structure: ${validationResult.error}`, "red");
return;
}

// Display formatted preview
console.log("\n📋 EIP-712 Data Preview:");
console.log("========================");
const preview = formatEIP712ForDisplay(typedData);
console.log(preview);
console.log("========================\n");

// Confirm before signing
const shouldSign = await promptForBool(
"Do you want to sign this data? "
);

if (!shouldSign) {
printColor("Signing cancelled", "yellow");
return;
}

// Ask for derivation path (optional, use default ETH path)
const useDefaultPath = await promptForBool(
"Use default Ethereum signing path (m/44'/60'/0'/0/0)? "
);

let signerPath: number[];
if (useDefaultPath) {
signerPath = pathStrToInt("m/44'/60'/0'/0/0");
} else {
const pathStr = await promptGetPath("m/44'/60'/0'/0/0");
signerPath = pathStrToInt(pathStr);
}

// Prepare the signing request
const spinner = startNewSpinner("Requesting signature from Lattice...");
let spinnerClosed = false;

try {
// Sign the typed data using GridPlus SDK format
// According to the SDK docs, EIP-712 uses protocol: 'eip712' with ETH_MSG currency
const signatureResult = await client.sign({
currency: 'ETH_MSG',
data: {
protocol: 'eip712',
payload: typedData,
signerPath: signerPath
}
} as any);

closeSpinner(spinner, "Signature received from Lattice");
spinnerClosed = true;

// Format the signature - GridPlus SDK returns {sig: {v, r, s}, signer: Buffer}
const formattedSig = formatSignature(signatureResult.sig);

// Display signature
console.log("\n✅ Signature Generated:");
console.log("========================");
console.log(`Full signature: ${formattedSig.full}`);
console.log(`\nComponents:`);
console.log(` v: ${formattedSig.v}`);
console.log(` r: ${formattedSig.r}`);
console.log(` s: ${formattedSig.s}`);

// Show signer address if available
if (signatureResult.signer) {
const signerAddress = '0x' + signatureResult.signer.toString('hex');
console.log(`\nSigner address: ${signerAddress}`);
}
console.log("========================\n");

// Ask if user wants to save
const shouldSave = await promptForBool(
"Save signature to file? "
);

if (shouldSave) {
const outputPath = await promptForString(
"Enter output file path: ",
"signature.json"
);

const output = {
signature: formattedSig.full,
components: {
v: formattedSig.v,
r: formattedSig.r,
s: formattedSig.s
},
signer: signatureResult.signer ? '0x' + signatureResult.signer.toString('hex') : undefined,
typedData: typedData,
timestamp: new Date().toISOString()
};

try {
writeFileSync(outputPath, JSON.stringify(output, null, 2));
printColor(`Signature saved to ${outputPath}`, "green");
} catch (err) {
printColor(`Failed to save signature: ${err}`, "red");
}
}

} catch (err) {
if (!spinnerClosed) {
closeSpinner(
spinner,
`Failed to sign data: ${err instanceof Error ? err.message : 'Unknown error'}`,
false
);
} else {
printColor(`Error after signing: ${err instanceof Error ? err.message : 'Unknown error'}`, "red");
}
}

} catch (err) {
printColor(
`Error in EIP-712 signing: ${err instanceof Error ? err.message : 'Unknown error'}`,
"red"
);
}
}

Choose a reason for hiding this comment

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

A minor: It's a best practice to end files with newline character that's why GH is showing the ⛔ icon.

1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const COMMANDS = {
EXPORT_DEPOSIT_DATA: 'Export Validator Deposit Data',
GET_ADDRESS: 'Get Address',
GET_PUBLIC_KEY: 'Get Public Key',
SIGN_EIP712: 'Sign EIP-712 Data',
};

const DEFAULT_PATHS = {
Expand Down
7 changes: 6 additions & 1 deletion src/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
cmdChangeBLSCredentials,
cmdGenDepositData,
cmdGetAddresses,
cmdGetPubkeys
cmdGetPubkeys,
cmdSignEIP712
} from "./commands";
import { clearPrintedLines } from "./utils";

Expand Down Expand Up @@ -64,6 +65,7 @@ export const promptForCommand = async (client: Client) => {
COMMANDS.CHANGE_BLS_WITHDRAWAL_CREDS,
COMMANDS.GET_ADDRESS,
COMMANDS.GET_PUBLIC_KEY,
COMMANDS.SIGN_EIP712,
COMMANDS.EXIT,
],
});
Expand All @@ -82,6 +84,9 @@ export const promptForCommand = async (client: Client) => {
case COMMANDS.CHANGE_BLS_WITHDRAWAL_CREDS:
await cmdChangeBLSCredentials(client);
break;
case COMMANDS.SIGN_EIP712:
await cmdSignEIP712(client);
break;
case COMMANDS.EXIT:
process.exit(0);
break;
Expand Down
Loading