Skip to content

Commit

Permalink
Add signMessage & signTypedMessage
Browse files Browse the repository at this point in the history
  • Loading branch information
plusminushalf committed Nov 20, 2023
1 parent 7d0e9bb commit 560df26
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 33 deletions.
101 changes: 97 additions & 4 deletions src/accounts/privateKeyToSafeSmartAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import {
type Chain,
type Client,
type Hex,
SignableMessage,
type Transport,
TypedData,
TypedDataDefinition,
concatHex,
encodeFunctionData,
encodePacked,
getContractAddress,
hashMessage,
hashTypedData,
hexToBigInt,
keccak256,
toBytes,
zeroAddress
} from "viem"
import { privateKeyToAccount, toAccount } from "viem/accounts"
Expand Down Expand Up @@ -84,6 +90,59 @@ export class SignTransactionNotSupportedBySafeSmartAccount extends BaseError {
}
}

const adjustVInSignature = (
signingMethod: "eth_sign" | "eth_signTypedData",
signature: string
): Hex => {
const ETHEREUM_V_VALUES = [0, 1, 27, 28]
const MIN_VALID_V_VALUE_FOR_SAFE_ECDSA = 27
let signatureV = parseInt(signature.slice(-2), 16)
if (!ETHEREUM_V_VALUES.includes(signatureV)) {
throw new Error("Invalid signature")
}
if (signingMethod === "eth_sign") {
/*
The Safe's expected V value for ECDSA signature is:
- 27 or 28
- 31 or 32 if the message was signed with a EIP-191 prefix. Should be calculated as ECDSA V value + 4
Some wallets do that, some wallets don't, V > 30 is used by contracts to differentiate between
prefixed and non-prefixed messages. The only way to know if the message was signed with a
prefix is to check if the signer address is the same as the recovered address.
More info:
https://docs.safe.global/safe-core-protocol/signatures
*/
if (signatureV < MIN_VALID_V_VALUE_FOR_SAFE_ECDSA) {
signatureV += MIN_VALID_V_VALUE_FOR_SAFE_ECDSA
}
signatureV += 4
}
if (signingMethod === "eth_signTypedData") {
// Metamask with ledger returns V=0/1 here too, we need to adjust it to be ethereum's valid value (27 or 28)
if (signatureV < MIN_VALID_V_VALUE_FOR_SAFE_ECDSA) {
signatureV += MIN_VALID_V_VALUE_FOR_SAFE_ECDSA
}
}
return (signature.slice(0, -2) + signatureV.toString(16)) as Hex
}

const generateSafeMessageMessage = <
const TTypedData extends TypedData | { [key: string]: unknown },
TPrimaryType extends string = string
>(
message: SignableMessage | TypedDataDefinition<TTypedData, TPrimaryType>
): Hex => {
const signableMessage = message as SignableMessage

if (typeof signableMessage === "string" || signableMessage.raw) {
return hashMessage(signableMessage)
}

return hashTypedData(
message as TypedDataDefinition<TTypedData, TPrimaryType>
)
}

export type PrivateKeySafeSmartAccount<
transport extends Transport = Transport,
chain extends Chain | undefined = Chain | undefined
Expand Down Expand Up @@ -372,15 +431,49 @@ export async function privateKeyToSafeSmartAccount<
const account = toAccount({
address: accountAddress,
async signMessage({ message }) {
// TODO
return privateKeyAccount.signMessage({ message })
const messageHash = hashTypedData({
domain: {
chainId: chainId,
verifyingContract: accountAddress
},
types: {
SafeMessage: [{ name: "message", type: "bytes" }]
},
primaryType: "SafeMessage",
message: {
message: generateSafeMessageMessage(message)
}
})

return adjustVInSignature(
"eth_sign",
await privateKeyAccount.signMessage({
message: {
raw: toBytes(messageHash)
}
})
)
},
async signTransaction(_, __) {
throw new SignTransactionNotSupportedBySafeSmartAccount()
},
async signTypedData(typedData) {
// TODO
return privateKeyAccount.signTypedData({ ...typedData, privateKey })
return adjustVInSignature(
"eth_signTypedData",
await privateKeyAccount.signTypedData({
domain: {
chainId: chainId,
verifyingContract: accountAddress
},
types: {
SafeMessage: [{ name: "message", type: "bytes" }]
},
primaryType: "SafeMessage",
message: {
message: generateSafeMessageMessage(typedData)
}
})
)
}
})

Expand Down
19 changes: 14 additions & 5 deletions src/accounts/privateKeyToSimpleSmartAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,16 @@ const getAccountAddress = async <
client,
factoryAddress,
entryPoint,
owner
owner,
index = 0n
}: {
client: Client<TTransport, TChain>
factoryAddress: Address
owner: Address
entryPoint: Address
index?: bigint
}): Promise<Address> => {
const initCode = await getAccountInitCode(factoryAddress, owner)
const initCode = await getAccountInitCode(factoryAddress, owner, index)

return getSenderAddress(client, {
initCode,
Expand All @@ -113,11 +115,13 @@ export async function privateKeyToSimpleSmartAccount<
{
privateKey,
factoryAddress,
entryPoint
entryPoint,
index = 0n
}: {
privateKey: Hex
factoryAddress: Address
entryPoint: Address
index?: bigint
}
): Promise<PrivateKeySimpleSmartAccount<TTransport, TChain>> {
const privateKeyAccount = privateKeyToAccount(privateKey)
Expand All @@ -127,7 +131,8 @@ export async function privateKeyToSimpleSmartAccount<
client,
factoryAddress,
entryPoint,
owner: privateKeyAccount.address
owner: privateKeyAccount.address,
index
}),
getChainId(client)
])
Expand Down Expand Up @@ -177,7 +182,11 @@ export async function privateKeyToSimpleSmartAccount<

if ((contractCode?.length ?? 0) > 2) return "0x"

return getAccountInitCode(factoryAddress, privateKeyAccount.address)
return getAccountInitCode(
factoryAddress,
privateKeyAccount.address,
index
)
},
async encodeDeployCallData(_) {
throw new Error("Simple account doesn't support account deployment")
Expand Down
45 changes: 21 additions & 24 deletions test/safeSmartAccount.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { beforeAll, describe, expect, test } from "bun:test"
import dotenv from "dotenv"
import {
SignTransactionNotSupportedBySafeSmartAccount,
SignTransactionNotSupportedBySmartAccount
} from "permissionless/accounts"
import { UserOperation } from "permissionless/index.js"
import { SignTransactionNotSupportedBySafeSmartAccount } from "permissionless/accounts"
import {
Address,
Client,
BaseError,
Hex,
Transport,
decodeEventLog,
getContract,
zeroAddress
Expand All @@ -22,8 +17,7 @@ import {
getPimlicoPaymasterClient,
getPrivateKeyToSafeSmartAccount,
getPublicClient,
getSmartAccountClient,
getTestingChain
getSmartAccountClient
} from "./utils.js"

dotenv.config()
Expand Down Expand Up @@ -211,24 +205,27 @@ describe("Safe Account", () => {
}
)

console.log(transactionReceipt)

let eventFound = false

for (const log of transactionReceipt.logs) {
const event = decodeEventLog({
abi: EntryPointAbi,
...log
})
if (event.eventName === "UserOperationEvent") {
eventFound = true
const userOperation =
await bundlerClient.getUserOperationByHash({
hash: event.args.userOpHash
})
expect(userOperation?.userOperation.paymasterAndData).not.toBe(
"0x"
)
try {
const event = decodeEventLog({
abi: EntryPointAbi,
...log
})
if (event.eventName === "UserOperationEvent") {
eventFound = true
const userOperation =
await bundlerClient.getUserOperationByHash({
hash: event.args.userOpHash
})
expect(
userOperation?.userOperation.paymasterAndData
).not.toBe("0x")
}
} catch (e) {
const error = e as BaseError
if (error.name !== "AbiEventSignatureNotFoundError") throw e
}
}

Expand Down

0 comments on commit 560df26

Please sign in to comment.