Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Safe Message Decoding #55

Merged
merged 5 commits into from
Sep 19, 2024
Merged
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
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,17 @@
},
"dependencies": {
"@safe-global/safe-deployments": "^1.37.0",
"@safe-global/safe-gateway-typescript-sdk": "^3.22.2",
"@safe-global/safe-modules-deployments": "^2.2.0",
"near-api-js": "^5.0.0",
"near-ca": "^0.5.2",
"semver": "^7.6.3",
"viem": "^2.16.5"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^22.3.0",
"@types/semver": "^7.5.8",
"@types/yargs": "^17.0.32",
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
Expand All @@ -67,4 +70,4 @@
"glob": "^9.0.0",
"base-x": "^3.0.0"
}
}
}
148 changes: 148 additions & 0 deletions src/lib/safe-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/// This file is a viem implementation of the useDecodedSafeMessage hook from:
/// https://github.com/safe-global/safe-wallet-web
import { type SafeInfo } from "@safe-global/safe-gateway-typescript-sdk";
import { gte } from "semver";
import {
Address,
fromHex,
Hash,
hashMessage,
hashTypedData,
isHex,
TypedDataDomain,
} from "viem";

interface TypedDataTypes {
name: string;
type: string;
}
type TypedMessageTypes = {
[key: string]: TypedDataTypes[];
};

export type EIP712TypedData = {
domain: TypedDataDomain;
types: TypedMessageTypes;
message: Record<string, unknown>;
primaryType: string;
};

export type MinimalSafeInfo = Pick<SafeInfo, "address" | "version" | "chainId">;

/*
* From v1.3.0, EIP-1271 support was moved to the CompatibilityFallbackHandler.
* Also 1.3.0 introduces the chainId in the domain part of the SafeMessage
*/
const EIP1271_FALLBACK_HANDLER_SUPPORTED_SAFE_VERSION = "1.3.0";

const generateSafeMessageMessage = (
message: string | EIP712TypedData
): string => {
return typeof message === "string"
? hashMessage(message)
: hashTypedData(message);
};

/**
* Generates `SafeMessage` typed data for EIP-712
* https://github.com/safe-global/safe-contracts/blob/main/contracts/handler/CompatibilityFallbackHandler.sol#L12
* @param safe Safe which will sign the message
* @param message Message to sign
* @returns `SafeMessage` types for signing
*/
const generateSafeMessageTypedData = (
{ version, chainId, address }: MinimalSafeInfo,
message: string | EIP712TypedData
): EIP712TypedData => {
if (!version) {
throw Error("Cannot create SafeMessage without version information");
}
const isHandledByFallbackHandler = gte(
version,
EIP1271_FALLBACK_HANDLER_SUPPORTED_SAFE_VERSION
);
const verifyingContract = address.value as Address;
return {
domain: isHandledByFallbackHandler
? {
chainId: Number(BigInt(chainId)),
verifyingContract,
}
: { verifyingContract },
types: {
SafeMessage: [{ name: "message", type: "bytes" }],
},
message: {
message: generateSafeMessageMessage(message),
},
primaryType: "SafeMessage",
};
};

const generateSafeMessageHash = (
safe: MinimalSafeInfo,
message: string | EIP712TypedData
): Hash => {
const typedData = generateSafeMessageTypedData(safe, message);
return hashTypedData(typedData);
};

/**
* If message is a hex value and is Utf8 encoded string we decode it, else we return the raw message
* @param {string} message raw input message
* @returns {string}
*/
const getDecodedMessage = (message: string): string => {
if (isHex(message)) {
try {
return fromHex(message, "string");
} catch (e) {
// the hex string is not UTF8 encoding so return the raw message.
}
}

return message;
};

/**
* Returns the decoded message, the hash of the `message` and the hash of the `safeMessage`.
* The `safeMessageMessage` is the value inside the SafeMessage and the `safeMessageHash` gets signed if the connected wallet does not support `eth_signTypedData`.
*
* @param message message as string, UTF-8 encoded hex string or EIP-712 Typed Data
* @param safe SafeInfo of the opened Safe
* @returns `{
* decodedMessage,
* safeMessageMessage,
* safeMessageHash
* }`
*/
export function decodedSafeMessage(
message: string | EIP712TypedData,
safe: MinimalSafeInfo
): {
decodedMessage: string | EIP712TypedData;
safeMessageMessage: string;
safeMessageHash: Hash;
} {
const decodedMessage =
typeof message === "string" ? getDecodedMessage(message) : message;

return {
decodedMessage,
safeMessageMessage: generateSafeMessageMessage(decodedMessage),
safeMessageHash: generateSafeMessageHash(safe, decodedMessage),
};
}

// const isEIP712TypedData = (obj: any): obj is EIP712TypedData => {
// return (
// typeof obj === "object" &&
// obj != null &&
// "domain" in obj &&
// "types" in obj &&
// "message" in obj
// );
// };

// export const isBlindSigningPayload = (obj: EIP712TypedData | string): boolean =>
// !isEIP712TypedData(obj) && isHash(obj);
2 changes: 1 addition & 1 deletion tests/lib.bundler.spec.ts → tests/lib/bundler.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Erc4337Bundler } from "../src/lib/bundler";
import { Erc4337Bundler } from "../../src/lib/bundler";
describe("Safe Pack", () => {
const entryPoint = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { encodeMulti } from "../src/lib/multisend";
import { encodeMulti } from "../../src/lib/multisend";

describe("Multisend", () => {
it("encodeMulti", () => {
Expand Down
54 changes: 54 additions & 0 deletions tests/lib/safe-message.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { zeroAddress } from "viem";

import { decodedSafeMessage } from "../../src/lib/safe-message";

describe("Multisend", () => {
const plainMessage = `Welcome to OpenSea!

Click to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy).

This request will not trigger a blockchain transaction or cost any gas fees.

Wallet address:
0xdcf56f5a8cc380f63b6396dbddd0ae9fa605beee

Nonce:
2a29a96e-c741-4500-9de3-03a865ff05db`;
const safeInfo = {
address: {
value: "0xDcf56F5a8Cc380f63b6396Dbddd0aE9fa605BeeE",
},
chainId: "11155111",
version: "1.4.1+L2",
};
it("decodeSafeMessage", () => {
expect(decodedSafeMessage(plainMessage, safeInfo)).toStrictEqual({
decodedMessage: plainMessage,
safeMessageMessage:
"0xc90ef7cffa3b5b1422e6c49ca7a5d7c1e9f514db067ec9bad52db13e83cbbb7c",
safeMessageHash:
"0x19dbea8af895c61831f2830ebba00d6160e4527398ec1d88553a8f0b8318959d",
});
// Lower Safe Version.
expect(
decodedSafeMessage(plainMessage, { ...safeInfo, version: "1.2.1" })
).toStrictEqual({
decodedMessage: plainMessage,
safeMessageMessage:
"0xc90ef7cffa3b5b1422e6c49ca7a5d7c1e9f514db067ec9bad52db13e83cbbb7c",
safeMessageHash:
"0x58daaab88459f40802201741918791f85cb81a435328168ee6a1eaa735442809",
});
});

it("decodeSafeMessage", () => {
const versionlessSafeInfo = {
address: { value: zeroAddress },
chainId: "1",
version: null,
};
expect(() => decodedSafeMessage(plainMessage, versionlessSafeInfo)).toThrow(
"Cannot create SafeMessage without version information"
);
});
});
4 changes: 2 additions & 2 deletions tests/lib.safe.spec.ts → tests/lib/safe.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { zeroAddress } from "viem";

import { ContractSuite as EthPack } from "./ethers-safe";
import { ContractSuite as ViemPack } from "../src/lib/safe";
import { ContractSuite as ViemPack } from "../../src/lib/safe";
import { ContractSuite as EthPack } from "../ethers-safe";

describe("Safe Pack", () => {
let ethersPack: EthPack;
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,11 @@
dependencies:
semver "^7.6.2"

"@safe-global/safe-gateway-typescript-sdk@^3.22.2":
version "3.22.2"
resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.22.2.tgz#d4ff9972e58f9344fc95f8d41b2ec6517baa8e79"
integrity sha512-Y0yAxRaB98LFp2Dm+ACZqBSdAmI3FlpH/LjxOZ94g/ouuDJecSq0iR26XZ5QDuEL8Rf+L4jBJaoDC08CD0KkJw==

"@safe-global/safe-modules-deployments@^2.2.0":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@safe-global/safe-modules-deployments/-/safe-modules-deployments-2.2.1.tgz#a8b88f2afc6ec04fed09968fe1e4990ed975c86e"
Expand Down Expand Up @@ -1448,6 +1453,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469"
integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==

"@types/semver@^7.5.8":
version "7.5.8"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==

"@types/stack-utils@^2.0.0":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
Expand Down