diff --git a/package-lock.json b/package-lock.json index 11af544..46ab175 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@getsafle/safle-vault", - "version": "2.9.0", + "version": "2.9.1", "license": "MIT", "dependencies": { "@getsafle/safle-identity-wallet": "^1.3.0", @@ -28,7 +28,9 @@ "@getsafle/vault-stacks-controller": "^1.0.5", "@getsafle/vault-velas-controller": "^1.3.1", "bip39": "^3.0.4", + "crypto": "^1.0.1", "crypto-js": "^4.1.1", + "eth-sig-util": "^3.0.1", "ethers": "^5.5.3", "jest": "^29.4.3", "limiter": "^2.1.0", @@ -6362,6 +6364,7 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-3.0.1.tgz", "integrity": "sha512-0Us50HiGGvZgjtWTyAI/+qTzYPMLy5Q451D0Xy68bxq1QMWdoOddDwGvsqcFT27uohKgalM9z/yxplyt+mY2iQ==", "deprecated": "Deprecated in favor of '@metamask/eth-sig-util'", + "license": "ISC", "dependencies": { "ethereumjs-abi": "^0.6.8", "ethereumjs-util": "^5.1.1", diff --git a/package.json b/package.json index a70fe5f..083b958 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,9 @@ "@getsafle/vault-stacks-controller": "^1.0.5", "@getsafle/vault-velas-controller": "^1.3.1", "bip39": "^3.0.4", + "crypto": "^1.0.1", "crypto-js": "^4.1.1", + "eth-sig-util": "^3.0.1", "ethers": "^5.5.3", "jest": "^29.4.3", "limiter": "^2.1.0", diff --git a/src/lib/keyring.js b/src/lib/keyring.js index c69ee68..8ab3213 100644 --- a/src/lib/keyring.js +++ b/src/lib/keyring.js @@ -591,7 +591,102 @@ class Keyring { return { response: signedMessage }; } } + async signTypedMessage(address, data, pin, encryptionKey, rpcUrl = "") { + if ( + typeof pin != "string" || + pin.match(/^[0-9]+$/) === null || + pin < 0 || + pin.length != 6 + ) { + return { error: ERROR_MESSAGE.INCORRECT_PIN_TYPE }; + } + + const res = await this.validatePin(pin); + + if (res.response == false || res.error) { + return { error: ERROR_MESSAGE.INCORRECT_PIN }; + } + + const err = helper.validateEncryptionKey( + this.vault, + JSON.stringify(encryptionKey) + ); + + if (err.error) { + return { error: err.error }; + } + + const { error, response } = await this.exportPrivateKey(address, pin); + + if (error) { + return { error }; + } + + const { privateKey, isImported } = response; + + if (isImported) { + const web3 = new Web3(new Web3.providers.HttpProvider(rpcUrl)); + + if (this.chain === "ethereum") { + const signedMessage = await this.keyringInstance.sign( + data, + privateKey, + web3 + ); + + return { response: signedMessage.message }; + } + + if (Chains.evmChains.hasOwnProperty(this.chain)) { + const keyringInstance = await helper.getCoinInstance(this.chain); + + const signedMessage = await keyringInstance.sign( + data, + privateKey, + web3 + ); + + return { response: signedMessage.message }; + } + + if (Chains?.[this.chain]) { + const { signedMessage } = await this[this.chain].signTypedMessage( + data, + address, + privateKey + ); + return { response: signedMessage }; + } + + return { error: ERROR_MESSAGE.UNSUPPORTED_NON_EVM_FUNCTIONALITY }; + } else { + const accounts = await this.getAccounts(); + if (accounts.response.filter((e) => e.address === address).length < 1) { + return { error: ERROR_MESSAGE.NONEXISTENT_KEYRING_ACCOUNT }; + } + + if ( + Chains.evmChains.hasOwnProperty(this.chain) || + this.chain === "ethereum" + ) { + const msgParams = { from: address, data: data }; + + const signedMsg = await this.keyringInstance.signTypedMessage( + msgParams + ); + + return { response: signedMsg }; + } + + const { signedMessage } = await this[this.chain].signTypedMessage( + data, + address + ); + + return { response: signedMessage }; + } + } async signTransaction(rawTx, pin, rpcUrl) { if ( typeof pin != "string" || diff --git a/src/lib/test/keyring.test.js b/src/lib/test/keyring.test.js index d783518..c283fef 100644 --- a/src/lib/test/keyring.test.js +++ b/src/lib/test/keyring.test.js @@ -1,13 +1,15 @@ jest.setTimeout(30000); - +const crypto = require("crypto"); +const assert = require("assert"); const { before } = require("lodash"); let KeyRing = require("../keyring"); let Vault = require("../vault"); +const sigUtil = require("eth-sig-util"); const Web3 = require("web3"); const NETWORKS = { ethereum: { - URL: "https://eth-goerli.public.blastapi.io", - CHAIN_ID: 5, + URL: "https://eth.llamarpc.com", + CHAIN_ID: 1, }, bsc: { URL: "https://data-seed-prebsc-1-s1.binance.org:8545/", @@ -18,8 +20,8 @@ const NETWORKS = { CHAIN_ID: 80001, }, optimism: { - URL: "https://optimism-goerli.public.blastapi.io", - CHAIN_ID: 420, + URL: "https://optimism.llamarpc.com", + CHAIN_ID: 10, }, arbitrum: { URL: "https://sepolia-rollup.arbitrum.io/rpc", @@ -666,6 +668,114 @@ describe("sign", () => { }); }); +describe("sign message", () => { + Object.keys(NETWORKS).forEach((chainName) => { + const networkConfig = getNetworkConfig(chainName); + vault.changeNetwork(chainName); + test(`signMessage for ${chainName}`, async () => { + const message = `Hello, eth!`; + let web3 = new Web3(networkConfig.url); + const hash = crypto.createHash("sha256").update(message).digest(); + const hexData = web3.utils.toHex(hash); + + const msgParams = { + from: "0x80f850d6bfa120bcc462df27cf94d7d23bd8b7fd", + data: hexData, + }; + + const raw_sign = await vault.signMessage( + "0x80f850d6bfa120bcc462df27cf94d7d23bd8b7fd", + msgParams.data, + pin, + bufView, + ethUrl + ); + console.log(raw_sign); + }); + test(`signTypeMessage for ${chainName}`, async () => { + const accounts = ["0x80f850d6bfa120bcc462df27cf94d7d23bd8b7fd"]; + + // Ensure accounts is not empty + if (accounts.length === 0) { + throw new Error("No accounts found"); + } + + // console.log(accounts[0]); + + let msgParams = { + from: "0x80f850d6bfa120bcc462df27cf94d7d23bd8b7fd", + data: { + types: { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ], + Person: [ + { name: "name", type: "string" }, + { name: "wallet", type: "address" }, + ], + Mail: [ + { name: "from", type: "Person" }, + { name: "to", type: "Person" }, + { name: "contents", type: "string" }, + ], + }, + primaryType: "Mail", + domain: { + name: "Ether Mail", + version: "1", + chainId: NETWORKS[chainName].CHAIN_ID, + verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + }, + message: { + from: { + name: "Cow", + wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + }, + to: { + name: "Bob", + wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + }, + contents: "Hello, Bob!", + }, + }, + }; + + const rawSignature = await vault.signTypedMessage( + "0x80f850d6bfa120bcc462df27cf94d7d23bd8b7fd", + msgParams.data, + pin, + bufView, + networkConfig.url + ); + console.log("Raw signature:", rawSignature); + + // Verify the signature + msgParams = { ...msgParams.data, from: msgParams.from }; + console.log(); + const recoveredAddress = sigUtil.recoverTypedSignature({ + data: msgParams, + sig: rawSignature.response, + }); + + // console.log("Recovered address:", recoveredAddress); + // console.log("Original address:", msgParams.from); + + // Compare the recovered address with the original signer's address + const isSignatureValid = + recoveredAddress.toLowerCase() === msgParams.from.toLowerCase(); + + assert( + isSignatureValid, + `Signature verification failed for ${chainName}` + ); + assert(rawSignature, `Failed to Sign Message for ${chainName}`); + }); + }); +}); + describe("validateMnemonic", () => { let signUpPhrase = "join danger verb slide lava blossom garment school panel shaft damp ghost";