From 71249155da6ba917eee3bb4679f10318c847efd0 Mon Sep 17 00:00:00 2001
From: netbonus <151201453+netbonus@users.noreply.github.com>
Date: Tue, 2 Sep 2025 16:28:58 -0400
Subject: [PATCH] Adds signing for eip712 txs
---
README.md | 108 +++++++++++++++++
src/commands/index.ts | 3 +-
src/commands/signEIP712.ts | 185 +++++++++++++++++++++++++++++
src/constants.ts | 1 +
src/prompts.ts | 7 +-
src/utils/eip712Formatter.ts | 211 +++++++++++++++++++++++++++++++++
src/utils/eip712Validator.ts | 218 +++++++++++++++++++++++++++++++++++
7 files changed, 731 insertions(+), 2 deletions(-)
create mode 100644 src/commands/signEIP712.ts
create mode 100644 src/utils/eip712Formatter.ts
create mode 100644 src/utils/eip712Validator.ts
diff --git a/README.md b/README.md
index 606a400..52dfd14 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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...` |
+
+
+## š Sign EIP-712 Data
+
+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
diff --git a/src/commands/index.ts b/src/commands/index.ts
index 92d83bc..45d028c 100644
--- a/src/commands/index.ts
+++ b/src/commands/index.ts
@@ -1,4 +1,5 @@
export * from './buildDepositData';
export * from './changeBLSCredentials';
export * from './getAddress';
-export * from './getPubkey';
\ No newline at end of file
+export * from './getPubkey';
+export * from './signEIP712';
\ No newline at end of file
diff --git a/src/commands/signEIP712.ts b/src/commands/signEIP712.ts
new file mode 100644
index 0000000..2599fff
--- /dev/null
+++ b/src/commands/signEIP712.ts
@@ -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"
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/constants.ts b/src/constants.ts
index e0d8ad2..6c3e079 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -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 = {
diff --git a/src/prompts.ts b/src/prompts.ts
index 6dc8686..054bf60 100644
--- a/src/prompts.ts
+++ b/src/prompts.ts
@@ -6,7 +6,8 @@ import {
cmdChangeBLSCredentials,
cmdGenDepositData,
cmdGetAddresses,
- cmdGetPubkeys
+ cmdGetPubkeys,
+ cmdSignEIP712
} from "./commands";
import { clearPrintedLines } from "./utils";
@@ -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,
],
});
@@ -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;
diff --git a/src/utils/eip712Formatter.ts b/src/utils/eip712Formatter.ts
new file mode 100644
index 0000000..a619be1
--- /dev/null
+++ b/src/utils/eip712Formatter.ts
@@ -0,0 +1,211 @@
+/**
+ * EIP-712 data formatting utilities
+ */
+
+interface FormattedSignature {
+ full: string;
+ v: string;
+ r: string;
+ s: string;
+}
+
+/**
+ * Format EIP-712 typed data for human-readable display
+ */
+export function formatEIP712ForDisplay(typedData: any): string {
+ const lines: string[] = [];
+
+ // Format domain
+ lines.push('š Domain:');
+ if (typedData.domain) {
+ for (const [key, value] of Object.entries(typedData.domain)) {
+ lines.push(` ${key}: ${formatValue(value)}`);
+ }
+ }
+ lines.push('');
+
+ // Format primary type
+ lines.push(`š Primary Type: ${typedData.primaryType}`);
+ lines.push('');
+
+ // Format message
+ lines.push('š§ Message:');
+ if (typedData.message) {
+ lines.push(formatObject(typedData.message, ' '));
+ }
+ lines.push('');
+
+ // Show type definitions summary
+ if (typedData.types) {
+ const typeCount = Object.keys(typedData.types).length;
+ lines.push(`š Type Definitions: ${typeCount} types defined`);
+
+ // List custom types (excluding EIP712Domain)
+ const customTypes = Object.keys(typedData.types).filter(t => t !== 'EIP712Domain');
+ if (customTypes.length > 0) {
+ lines.push(` Custom types: ${customTypes.join(', ')}`);
+ }
+ }
+
+ return lines.join('\n');
+}
+
+/**
+ * Format a value for display
+ */
+function formatValue(value: any): string {
+ if (value === null || value === undefined) {
+ return 'null';
+ }
+
+ if (typeof value === 'string') {
+ // Check if it looks like an address
+ if (value.startsWith('0x') && value.length === 42) {
+ return value;
+ }
+ // Check if it's a hex string
+ if (value.startsWith('0x')) {
+ return value.length > 66 ? `${value.substring(0, 66)}...` : value;
+ }
+ return value;
+ }
+
+ if (typeof value === 'number' || typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (typeof value === 'boolean') {
+ return value ? 'true' : 'false';
+ }
+
+ if (Array.isArray(value)) {
+ if (value.length === 0) {
+ return '[]';
+ }
+ if (value.length <= 3) {
+ return `[${value.map(v => formatValue(v)).join(', ')}]`;
+ }
+ return `[${formatValue(value[0])}, ... (${value.length} items)]`;
+ }
+
+ if (typeof value === 'object') {
+ return '{...}';
+ }
+
+ return String(value);
+}
+
+/**
+ * Format an object for display with indentation
+ */
+function formatObject(obj: any, indent: string = ''): string {
+ const lines: string[] = [];
+
+ for (const [key, value] of Object.entries(obj)) {
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
+ lines.push(`${indent}${key}:`);
+ lines.push(formatObject(value, indent + ' '));
+ } else {
+ lines.push(`${indent}${key}: ${formatValue(value)}`);
+ }
+ }
+
+ return lines.join('\n');
+}
+
+/**
+ * Format signature from raw bytes to standard format
+ */
+export function formatSignature(signature: string | Buffer | any): FormattedSignature {
+ let sigHex: string;
+
+ // Handle GridPlus SDK signature format
+ if (signature && typeof signature === 'object' && !Buffer.isBuffer(signature)) {
+ if (signature.v !== undefined && signature.r && signature.s) {
+ // Extract v value - it may be a Buffer or number
+ let vValue: number;
+ if (Buffer.isBuffer(signature.v)) {
+ vValue = signature.v[0];
+ } else if (typeof signature.v === 'number') {
+ vValue = signature.v;
+ } else {
+ vValue = parseInt(signature.v);
+ }
+
+ // Format r and s (they come as hex strings without 0x)
+ const r = signature.r.startsWith('0x') ? signature.r.slice(2) : signature.r;
+ const s = signature.s.startsWith('0x') ? signature.s.slice(2) : signature.s;
+
+ // Ensure proper padding
+ const rPadded = r.padStart(64, '0');
+ const sPadded = s.padStart(64, '0');
+ const vHex = vValue.toString(16).padStart(2, '0');
+
+ return {
+ full: '0x' + rPadded + sPadded + vHex,
+ v: '0x' + vHex,
+ r: '0x' + rPadded,
+ s: '0x' + sPadded
+ };
+ }
+ }
+
+ // Convert to hex string if Buffer
+ if (Buffer.isBuffer(signature)) {
+ sigHex = '0x' + signature.toString('hex');
+ } else if (typeof signature === 'string') {
+ // Ensure it has 0x prefix
+ sigHex = signature.startsWith('0x') ? signature : '0x' + signature;
+ } else {
+ throw new Error('Invalid signature format');
+ }
+
+ // Remove 0x prefix for processing
+ const sig = sigHex.slice(2);
+
+ // EIP-712 signatures are 65 bytes (130 hex chars)
+ // r: 32 bytes, s: 32 bytes, v: 1 byte
+ if (sig.length !== 130) {
+ throw new Error(`Invalid signature length: expected 130 hex chars, got ${sig.length}`);
+ }
+
+ const r = '0x' + sig.slice(0, 64);
+ const s = '0x' + sig.slice(64, 128);
+ const vHex = sig.slice(128, 130);
+ const vNum = parseInt(vHex, 16);
+
+ // Format v according to EIP-155
+ // Traditional v values are 27 or 28
+ // EIP-155 v values encode the chain ID
+ let v: string;
+ if (vNum === 0 || vNum === 1) {
+ // Modern format (0 or 1)
+ v = '0x' + (vNum + 27).toString(16);
+ } else {
+ // Already in correct format
+ v = '0x' + vNum.toString(16);
+ }
+
+ return {
+ full: sigHex,
+ v,
+ r,
+ s
+ };
+}
+
+/**
+ * Encode EIP-712 data for transmission to Lattice
+ * This may need adjustment based on gridplus-sdk requirements
+ */
+export function encodeForLattice(typedData: any): any {
+ // The Lattice/gridplus-sdk may expect a specific format
+ // This is a placeholder that returns the data as-is
+ // Actual implementation depends on SDK requirements
+ return {
+ types: typedData.types,
+ primaryType: typedData.primaryType,
+ domain: typedData.domain,
+ message: typedData.message
+ };
+}
\ No newline at end of file
diff --git a/src/utils/eip712Validator.ts b/src/utils/eip712Validator.ts
new file mode 100644
index 0000000..4059ab9
--- /dev/null
+++ b/src/utils/eip712Validator.ts
@@ -0,0 +1,218 @@
+/**
+ * EIP-712 typed data validation utilities
+ */
+
+interface ValidationResult {
+ isValid: boolean;
+ error?: string;
+}
+
+interface EIP712TypedData {
+ types: Record>;
+ primaryType: string;
+ domain: Record;
+ message: Record;
+}
+
+/**
+ * Validate EIP-712 typed data structure
+ */
+export function validateEIP712Data(data: any): ValidationResult {
+ // Check for required top-level fields
+ if (!data || typeof data !== 'object') {
+ return { isValid: false, error: 'Data must be an object' };
+ }
+
+ if (!data.types) {
+ return { isValid: false, error: 'Missing required field: types' };
+ }
+
+ if (!data.primaryType) {
+ return { isValid: false, error: 'Missing required field: primaryType' };
+ }
+
+ if (!data.domain) {
+ return { isValid: false, error: 'Missing required field: domain' };
+ }
+
+ if (!data.message) {
+ return { isValid: false, error: 'Missing required field: message' };
+ }
+
+ // Validate types structure
+ if (typeof data.types !== 'object' || Array.isArray(data.types)) {
+ return { isValid: false, error: 'Types must be an object' };
+ }
+
+ // Check that EIP712Domain type exists
+ if (!data.types.EIP712Domain) {
+ return { isValid: false, error: 'Missing EIP712Domain type definition' };
+ }
+
+ // Validate each type definition
+ for (const [typeName, typeFields] of Object.entries(data.types)) {
+ if (!Array.isArray(typeFields)) {
+ return {
+ isValid: false,
+ error: `Type ${typeName} must be an array of field definitions`
+ };
+ }
+
+ for (const field of typeFields) {
+ if (!field.name || !field.type) {
+ return {
+ isValid: false,
+ error: `Invalid field in type ${typeName}: must have name and type`
+ };
+ }
+
+ if (typeof field.name !== 'string' || typeof field.type !== 'string') {
+ return {
+ isValid: false,
+ error: `Invalid field in type ${typeName}: name and type must be strings`
+ };
+ }
+ }
+ }
+
+ // Validate that primaryType exists in types
+ if (!data.types[data.primaryType]) {
+ return {
+ isValid: false,
+ error: `Primary type '${data.primaryType}' not found in type definitions`
+ };
+ }
+
+ // Validate domain structure
+ const domainType = data.types.EIP712Domain;
+ for (const field of domainType) {
+ const fieldName = field.name;
+ if (data.domain[fieldName] !== undefined) {
+ // Basic type checking for common domain fields
+ if (fieldName === 'chainId' && typeof data.domain[fieldName] !== 'number') {
+ return {
+ isValid: false,
+ error: `Domain field 'chainId' must be a number`
+ };
+ }
+ if (fieldName === 'verifyingContract' &&
+ typeof data.domain[fieldName] === 'string' &&
+ !isValidAddress(data.domain[fieldName])) {
+ return {
+ isValid: false,
+ error: `Domain field 'verifyingContract' must be a valid address`
+ };
+ }
+ }
+ }
+
+ // Validate message structure against primaryType
+ const primaryTypeFields = data.types[data.primaryType];
+ for (const field of primaryTypeFields) {
+ if (data.message[field.name] === undefined) {
+ return {
+ isValid: false,
+ error: `Message missing required field: ${field.name}`
+ };
+ }
+ }
+
+ // Check for circular references
+ const circularCheck = checkCircularReferences(data.types);
+ if (!circularCheck.isValid) {
+ return circularCheck;
+ }
+
+ return { isValid: true };
+}
+
+/**
+ * Check for circular type references
+ */
+function checkCircularReferences(
+ types: Record>
+): ValidationResult {
+ const visited = new Set();
+ const recursionStack = new Set();
+
+ function checkType(typeName: string): boolean {
+ // Skip primitive types
+ if (isPrimitiveType(typeName)) {
+ return true;
+ }
+
+ // Extract base type from arrays
+ const baseType = typeName.replace(/\[\d*\]$/, '');
+
+ if (recursionStack.has(baseType)) {
+ return false; // Circular reference detected
+ }
+
+ if (visited.has(baseType)) {
+ return true; // Already checked, no circular reference
+ }
+
+ if (!types[baseType]) {
+ return true; // Type not defined, will be caught elsewhere
+ }
+
+ visited.add(baseType);
+ recursionStack.add(baseType);
+
+ for (const field of types[baseType]) {
+ if (!checkType(field.type)) {
+ return false;
+ }
+ }
+
+ recursionStack.delete(baseType);
+ return true;
+ }
+
+ for (const typeName of Object.keys(types)) {
+ if (typeName !== 'EIP712Domain' && !checkType(typeName)) {
+ return {
+ isValid: false,
+ error: `Circular reference detected involving type: ${typeName}`
+ };
+ }
+ }
+
+ return { isValid: true };
+}
+
+/**
+ * Check if a type is a primitive EIP-712 type
+ */
+function isPrimitiveType(type: string): boolean {
+ const primitives = [
+ 'bool',
+ 'string',
+ 'address',
+ 'bytes',
+ /^bytes\d+$/,
+ /^uint\d*$/,
+ /^int\d*$/
+ ];
+
+ const baseType = type.replace(/\[\d*\]$/, '');
+
+ return primitives.some(primitive => {
+ if (typeof primitive === 'string') {
+ return baseType === primitive;
+ }
+ return primitive.test(baseType);
+ });
+}
+
+/**
+ * Basic Ethereum address validation
+ */
+function isValidAddress(address: string): boolean {
+ if (typeof address !== 'string') {
+ return false;
+ }
+
+ // Check if it starts with 0x and has 40 hex characters
+ return /^0x[a-fA-F0-9]{40}$/.test(address);
+}
\ No newline at end of file