Skip to content

Commit

Permalink
Safe Message Decoding (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
bh2smith authored Sep 19, 2024
1 parent cf9e610 commit 4342da4
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 5 deletions.
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

0 comments on commit 4342da4

Please sign in to comment.