safe-message-tools
Version:
CLI tools for signing and verifying messages with Gnosis Safe. Supports EIP-712, EIP-1271, hardware wallets, and multi-signature coordination.
127 lines (107 loc) • 3.18 kB
JavaScript
import { ethers } from 'ethers'
import { readFileSync } from 'fs'
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import { safeTypedData, safeMessageHash } from '../lib/safe.js'
const cli = yargs(hideBin(process.argv))
.option('safe', {
type: 'string',
required: true,
describe: 'Safe address'
})
.option('msg', {
type: 'string',
required: true,
describe: 'Message file path'
})
.option('rpc', {
type: 'string',
required: true,
describe: 'RPC endpoint URL'
})
.option('wallet', {
type: 'string',
required: true,
choices: ['ledger'],
describe: 'Hardware wallet type'
})
.option('path', {
type: 'string',
default: 'm/44\'/60\'/0\'/0/0',
describe: 'Derivation path'
})
.help()
.argv
async function signWithLedger(domain, types, value, path) {
try {
const { default: TransportNodeHid } = await import('@ledgerhq/hw-transport-node-hid')
const { default: AppEth } = await import('@ledgerhq/hw-app-eth')
const transport = await TransportNodeHid.create()
const eth = new AppEth(transport)
console.log('confirm on ledger...')
const { address } = await eth.getAddress(path)
// Use proper EIP-712 signing
const signature = await eth.signEIP712Message(path, {
domain,
types,
primaryType: 'SafeMessage',
message: value
})
const sig = ethers.Signature.from({
r: '0x' + signature.r,
s: '0x' + signature.s,
v: signature.v
}).serialized
await transport.close()
return { signature: sig, address }
} catch (error) {
if (error.message.includes('Cannot resolve module')) {
throw new Error('ledger dependencies not installed: npm install @ledgerhq/hw-transport-node-hid @ledgerhq/hw-app-eth')
}
throw error
}
}
async function main() {
const { safe, msg, rpc, wallet, path } = cli
if (!ethers.isAddress(safe)) {
console.error('invalid safe address')
process.exit(1)
}
let message
try {
message = readFileSync(msg, 'utf8').trim()
} catch {
console.error(`failed to read ${msg}`)
process.exit(1)
}
const provider = new ethers.JsonRpcProvider(rpc)
let chainId
try {
const network = await provider.getNetwork()
chainId = Number(network.chainId)
} catch {
console.error('rpc connection failed')
process.exit(1)
}
const { domain, types, value } = safeTypedData(safe, message, chainId)
const messageHash = safeMessageHash(safe, message, chainId)
try {
const result = await signWithLedger(domain, types, value, path)
console.log(`Message: ${message}`)
console.log(`Safe: ${safe}`)
console.log(`Chain: ${chainId}`)
console.log(`Hash: ${messageHash}`)
console.log(`Signature: ${result.signature}`)
console.log(`Signer: ${result.address}`)
console.log(`Wallet: ${wallet}`)
console.log(`Path: ${path}`)
} catch (error) {
console.error(`${wallet} signing failed: ${error.message}`)
process.exit(1)
}
}
main().catch(err => {
console.error(err.message)
process.exit(1)
})