diff --git a/guides/integrating-the-safe-core-sdk.md b/guides/integrating-the-safe-core-sdk.md
index 59258d609..813d29add 100644
--- a/guides/integrating-the-safe-core-sdk.md
+++ b/guides/integrating-the-safe-core-sdk.md
@@ -207,14 +207,14 @@ Once we have the Safe transaction object we can share it with the other owners o
- `safeAddress`: The Safe address.
- `safeTransactionData`: The `data` object inside the Safe transaction object returned from the method `createTransaction`.
-- `safeTxHash`: The Safe transaction hash, calculated by calling the method `getTransactionHash` from the Protocol Kit.
+- `safeTxHash`: The Safe transaction hash, calculated by calling the method `getHash` from the Protocol Kit.
- `senderAddress`: The Safe owner or delegate proposing the transaction.
- `senderSignature`: The signature generated by signing the `safeTxHash` with the `senderAddress`.
- `origin`: Optional string that allows to provide more information about the app proposing the transaction.
```js
-const safeTxHash = await safeSdk.getTransactionHash(safeTransaction)
-const senderSignature = await safeSdk.signTransactionHash(safeTxHash)
+const safeTxHash = await safeSdk.getHash(safeTransaction)
+const senderSignature = await safeSdk.signHash(safeTxHash)
await safeService.proposeTransaction({
safeAddress,
safeTransactionData: safeTransaction.data,
@@ -288,13 +288,13 @@ type SafeMultisigTransactionResponse = {
## 7. Confirm/reject the transaction
-The owners of the Safe can now sign the transaction obtained from the Safe Transaction Service by calling the method `signTransactionHash` from the Protocol Kit to generate the signature and by calling the method `confirmTransaction` from the Safe API Kit to add the signature to the service.
+The owners of the Safe can now sign the transaction obtained from the Safe Transaction Service by calling the method `signHash` from the Protocol Kit to generate the signature and by calling the method `confirmTransaction` from the Safe API Kit to add the signature to the service.
```js
// transaction: SafeMultisigTransactionResponse
const hash = transaction.safeTxHash
-let signature = await safeSdk.signTransactionHash(hash)
+let signature = await safeSdk.signHash(hash)
await safeService.confirmTransaction(hash, signature.data)
```
diff --git a/packages/api-kit/tests/endpoint/index.test.ts b/packages/api-kit/tests/endpoint/index.test.ts
index 27b8c0e96..1b276107c 100644
--- a/packages/api-kit/tests/endpoint/index.test.ts
+++ b/packages/api-kit/tests/endpoint/index.test.ts
@@ -364,7 +364,7 @@ describe('Endpoint tests', () => {
const signerAddress = await signer.getAddress()
const safeSdk = await Safe.create({ ethAdapter, safeAddress })
const safeTransaction = await safeSdk.createTransaction({ safeTransactionData })
- const senderSignature = await safeSdk.signTransactionHash(safeTxHash)
+ const senderSignature = await safeSdk.signHash(safeTxHash)
await chai
.expect(
safeApiKit.proposeTransaction({
@@ -407,7 +407,7 @@ describe('Endpoint tests', () => {
const signerAddress = await signer.getAddress()
const safeSdk = await Safe.create({ ethAdapter, safeAddress })
const safeTransaction = await safeSdk.createTransaction({ safeTransactionData })
- const senderSignature = await safeSdk.signTransactionHash(safeTxHash)
+ const senderSignature = await safeSdk.signHash(safeTxHash)
await chai
.expect(
safeApiKit.proposeTransaction({
diff --git a/packages/onramp-kit/src/packs/monerium/SafeMoneriumClient.test.ts b/packages/onramp-kit/src/packs/monerium/SafeMoneriumClient.test.ts
index c163e744b..f5bf27e4a 100644
--- a/packages/onramp-kit/src/packs/monerium/SafeMoneriumClient.test.ts
+++ b/packages/onramp-kit/src/packs/monerium/SafeMoneriumClient.test.ts
@@ -148,8 +148,8 @@ describe('SafeMoneriumClient', () => {
data: txData
})
- safeSdk.getTransactionHash = jest.fn().mockResolvedValueOnce('0xTransactionHash')
- safeSdk.signTransactionHash = jest.fn().mockResolvedValueOnce('0xTransactionSignature')
+ safeSdk.getHash = jest.fn().mockResolvedValueOnce('0xTransactionHash')
+ safeSdk.signHash = jest.fn().mockResolvedValueOnce('0xTransactionSignature')
jest.spyOn(SafeApiKit.prototype, 'getTransaction').mockResolvedValueOnce({
confirmationsRequired: 1,
diff --git a/packages/onramp-kit/src/packs/monerium/SafeMoneriumClient.ts b/packages/onramp-kit/src/packs/monerium/SafeMoneriumClient.ts
index 0d0eaf332..97d98823a 100644
--- a/packages/onramp-kit/src/packs/monerium/SafeMoneriumClient.ts
+++ b/packages/onramp-kit/src/packs/monerium/SafeMoneriumClient.ts
@@ -125,9 +125,9 @@ export class SafeMoneriumClient extends MoneriumClient {
}
})
- const safeTxHash = await this.#safeSdk.getTransactionHash(safeTransaction)
+ const safeTxHash = await this.#safeSdk.getHash(safeTransaction)
- const senderSignature = await this.#safeSdk.signTransactionHash(safeTxHash)
+ const senderSignature = await this.#safeSdk.signHash(safeTxHash)
const chainId = await this.#safeSdk.getChainId()
diff --git a/packages/protocol-kit/README.md b/packages/protocol-kit/README.md
index f4553426f..425c60e31 100644
--- a/packages/protocol-kit/README.md
+++ b/packages/protocol-kit/README.md
@@ -133,7 +133,7 @@ To connect `owner2` to the Safe we need to create a new instance of the class `E
```js
const ethAdapterOwner2 = new EthersAdapter({ ethers, signerOrProvider: owner2 })
const safeSdk2 = await safeSdk.connect({ ethAdapter: ethAdapterOwner2, safeAddress })
-const txHash = await safeSdk2.getTransactionHash(safeTransaction)
+const txHash = await safeSdk2.getHash(safeTransaction)
const approveTxResponse = await safeSdk2.approveTransactionHash(txHash)
await approveTxResponse.transactionResponse?.wait()
```
@@ -633,7 +633,7 @@ const safeTransaction1 = await safeSdk.createTransaction({ safeTransactionData }
const safeTransaction2 = await copyTransaction(safeTransaction1)
```
-### getTransactionHash
+### getHash
Returns the transaction hash of a Safe transaction.
@@ -642,10 +642,10 @@ const safeTransactionData: SafeTransactionDataPartial = {
// ...
}
const safeTransaction = await safeSdk.createTransaction({ safeTransactionData })
-const txHash = await safeSdk.getTransactionHash(safeTransaction)
+const txHash = await safeSdk.getHash(safeTransaction)
```
-### signTransactionHash
+### signHash
Signs a hash using the current owner account.
@@ -654,8 +654,8 @@ const safeTransactionData: SafeTransactionDataPartial = {
// ...
}
const safeTransaction = await safeSdk.createTransaction({ safeTransactionData })
-const txHash = await safeSdk.getTransactionHash(safeTransaction)
-const signature = await safeSdk.signTransactionHash(txHash)
+const txHash = await safeSdk.getHash(safeTransaction)
+const signature = await safeSdk.signHash(txHash)
```
### signTypedData
@@ -701,7 +701,7 @@ const safeTransactionData: SafeTransactionDataPartial = {
// ...
}
const safeTransaction = await safeSdk.createTransaction({ safeTransactionData })
-const txHash = await safeSdk.getTransactionHash(safeTransaction)
+const txHash = await safeSdk.getHash(safeTransaction)
const txResponse = await safeSdk.approveTransactionHash(txHash)
await txResponse.transactionResponse?.wait()
```
@@ -743,7 +743,7 @@ const safeTransactionData: SafeTransactionDataPartial = {
// ...
}
const safeTransaction = await safeSdk.createTransaction({ safeTransactionData })
-const txHash = await safeSdk.getTransactionHash(safeTransaction)
+const txHash = await safeSdk.getHash(safeTransaction)
const ownerAddresses = await safeSdk.getOwnersWhoApprovedTx(txHash)
```
diff --git a/packages/protocol-kit/src/Safe.ts b/packages/protocol-kit/src/Safe.ts
index a78608630..1efe146f0 100644
--- a/packages/protocol-kit/src/Safe.ts
+++ b/packages/protocol-kit/src/Safe.ts
@@ -3,16 +3,20 @@ import {
EthAdapter,
OperationType,
SafeMultisigTransactionResponse,
+ SafeMultisigConfirmationResponse,
SafeSignature,
SafeTransaction,
SafeTransactionDataPartial,
- SafeTransactionEIP712Args,
+ SafeEIP712Args,
SafeVersion,
TransactionOptions,
TransactionResult,
MetaTransactionData,
- Transaction
+ Transaction,
+ CompatibilityFallbackHandlerContract,
+ EIP712TypedData
} from '@safe-global/safe-core-sdk-types'
+import { soliditySha3, utf8ToHex } from 'web3-utils'
import {
PREDETERMINED_SALT_NONCE,
encodeSetupCallData,
@@ -43,6 +47,7 @@ import {
sameString
} from './utils'
import {
+ buildSignature,
generateEIP712Signature,
generatePreValidatedSignature,
generateSignature
@@ -56,6 +61,7 @@ import {
} from './utils/transactions/utils'
import { isSafeConfigWithPredictedSafe } from './utils/types'
import {
+ getCompatibilityFallbackHandlerContract,
getMultiSendCallOnlyContract,
getProxyFactoryContract,
getSafeContract
@@ -70,6 +76,8 @@ class Safe {
#guardManager!: GuardManager
#fallbackHandlerManager!: FallbackHandlerManager
+ #MAGIC_VALUE = '0x1626ba7e'
+
/**
* Creates an instance of the Safe Core SDK.
* @param config - Ethers Safe configuration
@@ -475,7 +483,7 @@ class Safe {
const signedSafeTransaction = await this.createTransaction({
safeTransactionData: safeTransaction.data
})
- safeTransaction.signatures.forEach((signature) => {
+ safeTransaction.signatures.forEach((signature: EthSafeSignature) => {
signedSafeTransaction.addSignature(signature)
})
return signedSafeTransaction
@@ -484,15 +492,21 @@ class Safe {
/**
* Returns the transaction hash of a Safe transaction.
*
- * @param safeTransaction - The Safe transaction
- * @returns The transaction hash of the Safe transaction
+ * @param txOrMessage - The Safe transaction or a raw message
+ * @returns The hashed Safe transaction or message
*/
- async getTransactionHash(safeTransaction: SafeTransaction): Promise {
+ async getHash(txOrMessage: SafeTransaction | string): Promise {
if (!this.#contractManager.safeContract) {
throw new Error('Safe is not deployed')
}
- const safeTransactionData = safeTransaction.data
+
+ if (typeof txOrMessage === 'string') {
+ return soliditySha3(utf8ToHex(txOrMessage)) || ''
+ }
+
+ const safeTransactionData = txOrMessage.data
const txHash = await this.#contractManager.safeContract.getTransactionHash(safeTransactionData)
+
return txHash
}
@@ -500,30 +514,59 @@ class Safe {
* Signs a hash using the current signer account.
*
* @param hash - The hash to sign
+ * @param isSmartContract - If the signature is a Smart Contract signature following EIP-1271. Optional. Default value is false
* @returns The Safe signature
*/
- async signTransactionHash(hash: string): Promise {
- return generateSignature(this.#ethAdapter, hash)
+ async signHash(hash: string, isSmartContract = false): Promise {
+ const signature = await generateSignature(this.#ethAdapter, hash)
+
+ // If is a Smart Contract signature the signer is the Safe and not the signer account
+ if (isSmartContract) {
+ const safeAddress = await this.getAddress()
+
+ return new EthSafeSignature(safeAddress, signature.data, isSmartContract)
+ }
+
+ return signature
}
/**
* Signs a transaction according to the EIP-712 using the current signer account.
*
- * @param safeTransaction - The Safe transaction to be signed
+ * @param eip712Data - The Safe Transaction or message hash to be signed
* @param methodVersion - EIP-712 version. Optional
* @returns The Safe signature
*/
async signTypedData(
- safeTransaction: SafeTransaction,
- methodVersion?: 'v3' | 'v4'
+ eip712Data: SafeTransaction | EIP712TypedData | string,
+ methodVersion?: 'v3' | 'v4',
+ isSmartContract = false
): Promise {
- const safeTransactionEIP712Args: SafeTransactionEIP712Args = {
+ let data
+
+ if (eip712Data.hasOwnProperty('signatures')) {
+ data = (eip712Data as SafeTransaction).data
+ } else {
+ data = eip712Data as EIP712TypedData | string
+ }
+
+ const safeEIP712Args: SafeEIP712Args = {
safeAddress: await this.getAddress(),
safeVersion: await this.getContractVersion(),
chainId: await this.getEthAdapter().getChainId(),
- safeTransactionData: safeTransaction.data
+ data
}
- return generateEIP712Signature(this.#ethAdapter, safeTransactionEIP712Args, methodVersion)
+
+ const signature = await generateEIP712Signature(this.#ethAdapter, safeEIP712Args, methodVersion)
+
+ // If is a Smart Contract signature the signer is the Safe and not the signer account
+ if (isSmartContract) {
+ const safeAddress = await this.getAddress()
+
+ return new EthSafeSignature(safeAddress, signature.data, isSmartContract)
+ }
+
+ return signature
}
/**
@@ -540,7 +583,8 @@ class Safe {
| 'eth_sign'
| 'eth_signTypedData'
| 'eth_signTypedData_v3'
- | 'eth_signTypedData_v4' = 'eth_signTypedData_v4'
+ | 'eth_signTypedData_v4' = 'eth_signTypedData_v4',
+ isSmartContract = false
): Promise {
const transaction = isSafeMultisigTransactionResponse(safeTransaction)
? await this.toSafeTransactionType(safeTransaction)
@@ -560,24 +604,24 @@ class Safe {
let signature: SafeSignature
if (signingMethod === 'eth_signTypedData_v4') {
- signature = await this.signTypedData(transaction, 'v4')
+ signature = await this.signTypedData(transaction, 'v4', isSmartContract)
} else if (signingMethod === 'eth_signTypedData_v3') {
- signature = await this.signTypedData(transaction, 'v3')
+ signature = await this.signTypedData(transaction, 'v3', isSmartContract)
} else if (signingMethod === 'eth_signTypedData') {
- signature = await this.signTypedData(transaction)
+ signature = await this.signTypedData(transaction, undefined, isSmartContract)
} else {
const safeVersion = await this.getContractVersion()
if (!hasSafeFeature(SAFE_FEATURES.ETH_SIGN, safeVersion)) {
throw new Error('eth_sign is only supported by Safes >= v1.1.0')
}
- const txHash = await this.getTransactionHash(transaction)
- signature = await this.signTransactionHash(txHash)
+ const txHash = await this.getHash(transaction)
+ signature = await this.signHash(txHash, isSmartContract)
}
const signedSafeTransaction = await this.createTransaction({
safeTransactionData: transaction.data
})
- transaction.signatures.forEach((signature) => {
+ transaction.signatures.forEach((signature: EthSafeSignature) => {
signedSafeTransaction.addSignature(signature)
})
signedSafeTransaction.addSignature(signature)
@@ -899,10 +943,12 @@ class Safe {
nonce: serviceTransactionResponse.nonce
}
const safeTransaction = await this.createTransaction({ safeTransactionData })
- serviceTransactionResponse.confirmations?.map((confirmation) => {
- const signature = new EthSafeSignature(confirmation.owner, confirmation.signature)
- safeTransaction.addSignature(signature)
- })
+ serviceTransactionResponse.confirmations?.map(
+ (confirmation: SafeMultisigConfirmationResponse) => {
+ const signature = new EthSafeSignature(confirmation.owner, confirmation.signature)
+ safeTransaction.addSignature(signature)
+ }
+ )
return safeTransaction
}
@@ -926,7 +972,7 @@ class Safe {
const signedSafeTransaction = await this.copyTransaction(transaction)
- const txHash = await this.getTransactionHash(signedSafeTransaction)
+ const txHash = await this.getHash(signedSafeTransaction)
const ownersWhoApprovedTx = await this.getOwnersWhoApprovedTx(txHash)
for (const owner of ownersWhoApprovedTx) {
signedSafeTransaction.addSignature(generatePreValidatedSignature(owner))
@@ -973,7 +1019,7 @@ class Safe {
const signedSafeTransaction = await this.copyTransaction(transaction)
- const txHash = await this.getTransactionHash(signedSafeTransaction)
+ const txHash = await this.getHash(signedSafeTransaction)
const ownersWhoApprovedTx = await this.getOwnersWhoApprovedTx(txHash)
for (const owner of ownersWhoApprovedTx) {
signedSafeTransaction.addSignature(generatePreValidatedSignature(owner))
@@ -1216,6 +1262,81 @@ class Safe {
return transactionBatch
}
+
+ private async getFallbackHandlerContract(): Promise {
+ if (!this.#contractManager.safeContract) {
+ throw new Error('Safe is not deployed')
+ }
+
+ const safeVersion =
+ (await this.#contractManager.safeContract.getVersion()) ?? DEFAULT_SAFE_VERSION
+ const chainId = await this.#ethAdapter.getChainId()
+
+ const compatibilityFallbackHandlerContract = await getCompatibilityFallbackHandlerContract({
+ ethAdapter: this.#ethAdapter,
+ safeVersion,
+ customContracts: this.#contractManager.contractNetworks?.[chainId]
+ })
+
+ return compatibilityFallbackHandlerContract
+ }
+
+ /**
+ * Call the CompatibilityFallbackHandler getMessageHash method
+ * @param messageHash The hash of the message
+ * @returns Returns the Safe message hash to be signed
+ * @link https://github.com/safe-global/safe-contracts/blob/8ffae95faa815acf86ec8b50021ebe9f96abde10/contracts/handler/CompatibilityFallbackHandler.sol#L26-L28
+ */
+ getSafeMessageHash = async (messageHash: string): Promise => {
+ const safeAddress = await this.getAddress()
+ const fallbackHandler = await this.getFallbackHandlerContract()
+
+ const data = fallbackHandler.encode('getMessageHash', [messageHash])
+
+ const safeMessageHash = await this.#ethAdapter.call({
+ from: safeAddress,
+ to: safeAddress,
+ data: data || '0x'
+ })
+
+ return safeMessageHash
+ }
+
+ /**
+ * Call the CompatibilityFallbackHandler isValidSignature method
+ * @param messageHash The hash of the message
+ * @param signature The signature to be validated or '0x'. You can send as signature one of the following:
+ * 1) An array of SafeSignature. In this case the signatures are concatenated for validation (buildSignature())
+ * 2) The concatenated signatures as string
+ * 3) '0x' if you want to validate an onchain message (Approved hash)
+ * @returns A boolean indicating if the signature is valid
+ * @link https://github.com/safe-global/safe-contracts/blob/main/contracts/handler/CompatibilityFallbackHandler.sol
+ */
+ isValidSignature = async (
+ messageHash: string,
+ signature: SafeSignature[] | string = '0x'
+ ): Promise => {
+ const safeAddress = await this.getAddress()
+ const fallbackHandler = await this.getFallbackHandlerContract()
+
+ const data = fallbackHandler.encode('isValidSignature(bytes32,bytes)', [
+ messageHash,
+ signature && Array.isArray(signature) ? buildSignature(signature) : signature
+ ])
+
+ try {
+ const isValidSignatureResponse = await this.#ethAdapter.call({
+ from: safeAddress,
+ to: safeAddress,
+ data: data || '0x'
+ })
+
+ return isValidSignatureResponse.slice(0, 10).toLowerCase() === this.#MAGIC_VALUE
+ } catch (error) {
+ console.error(error)
+ return false
+ }
+ }
}
export default Safe
diff --git a/packages/protocol-kit/src/adapters/ethers/EthersAdapter.ts b/packages/protocol-kit/src/adapters/ethers/EthersAdapter.ts
index a9eb7d8de..efda0f658 100644
--- a/packages/protocol-kit/src/adapters/ethers/EthersAdapter.ts
+++ b/packages/protocol-kit/src/adapters/ethers/EthersAdapter.ts
@@ -4,11 +4,13 @@ import { BigNumber } from '@ethersproject/bignumber'
import { Provider } from '@ethersproject/providers'
import { generateTypedData, validateEip3770Address } from '@safe-global/protocol-kit/utils'
import {
+ EIP712TypedDataMessage,
+ EIP712TypedDataTx,
Eip3770Address,
EthAdapter,
EthAdapterTransaction,
GetContractProps,
- SafeTransactionEIP712Args
+ SafeEIP712Args
} from '@safe-global/safe-core-sdk-types'
import { ethers } from 'ethers'
import CompatibilityFallbackHandlerContractEthers from './contracts/CompatibilityFallbackHandler/CompatibilityFallbackHandlerEthersContract'
@@ -243,19 +245,22 @@ class EthersAdapter implements EthAdapter {
return this.#signer.signMessage(messageArray)
}
- async signTypedData(safeTransactionEIP712Args: SafeTransactionEIP712Args): Promise {
+ async signTypedData(safeEIP712Args: SafeEIP712Args): Promise {
if (!this.#signer) {
throw new Error('EthAdapter must be initialized with a signer to use this method')
}
if (isTypedDataSigner(this.#signer)) {
- const typedData = generateTypedData(safeTransactionEIP712Args)
+ const typedData = generateTypedData(safeEIP712Args)
const signature = await this.#signer._signTypedData(
typedData.domain,
- { SafeTx: typedData.types.SafeTx },
+ typedData.primaryType === 'SafeMessage'
+ ? { SafeMessage: (typedData as EIP712TypedDataMessage).types.SafeMessage }
+ : { SafeTx: (typedData as EIP712TypedDataTx).types.SafeTx },
typedData.message
)
return signature
}
+
throw new Error('The current signer does not implement EIP-712 to sign typed data')
}
diff --git a/packages/protocol-kit/src/adapters/web3/Web3Adapter.ts b/packages/protocol-kit/src/adapters/web3/Web3Adapter.ts
index 5db04dcad..b67b5722a 100644
--- a/packages/protocol-kit/src/adapters/web3/Web3Adapter.ts
+++ b/packages/protocol-kit/src/adapters/web3/Web3Adapter.ts
@@ -5,7 +5,7 @@ import {
EthAdapter,
EthAdapterTransaction,
GetContractProps,
- SafeTransactionEIP712Args
+ SafeEIP712Args
} from '@safe-global/safe-core-sdk-types'
import Web3 from 'web3'
import { Transaction } from 'web3-core'
@@ -267,13 +267,13 @@ class Web3Adapter implements EthAdapter {
}
async signTypedData(
- safeTransactionEIP712Args: SafeTransactionEIP712Args,
+ safeEIP712Args: SafeEIP712Args,
methodVersion?: 'v3' | 'v4'
): Promise {
if (!this.#signerAddress) {
throw new Error('This method requires a signer')
}
- const typedData = generateTypedData(safeTransactionEIP712Args)
+ const typedData = generateTypedData(safeEIP712Args)
let method = 'eth_signTypedData_v3'
if (methodVersion === 'v4') {
method = 'eth_signTypedData_v4'
diff --git a/packages/protocol-kit/src/utils/eip-712/index.ts b/packages/protocol-kit/src/utils/eip-712/index.ts
index 3d86bc791..c6e21e31b 100644
--- a/packages/protocol-kit/src/utils/eip-712/index.ts
+++ b/packages/protocol-kit/src/utils/eip-712/index.ts
@@ -1,4 +1,15 @@
-import { GenerateTypedData, SafeTransactionEIP712Args } from '@safe-global/safe-core-sdk-types'
+import { TypedDataDomain } from 'ethers'
+import { _TypedDataEncoder } from 'ethers/lib/utils'
+import { soliditySha3, utf8ToHex } from 'web3-utils'
+import {
+ EIP712MessageTypes,
+ EIP712TxTypes,
+ EIP712TypedData,
+ SafeEIP712Args,
+ SafeTransactionData,
+ EIP712TypedDataMessage,
+ EIP712TypedDataTx
+} from '@safe-global/safe-core-sdk-types'
import semverSatisfies from 'semver/functions/satisfies'
const EQ_OR_GT_1_3_0 = '>=1.3.0'
@@ -22,10 +33,7 @@ export const EIP712_DOMAIN = [
]
// This function returns the types structure for signing off-chain messages according to EIP-712
-export function getEip712MessageTypes(safeVersion: string): {
- EIP712Domain: typeof EIP712_DOMAIN | typeof EIP712_DOMAIN_BEFORE_V130
- SafeTx: Array<{ type: string; name: string }>
-} {
+export function getEip712TxTypes(safeVersion: string): EIP712TxTypes {
const eip712WithChainId = semverSatisfies(safeVersion, EQ_OR_GT_1_3_0)
return {
EIP712Domain: eip712WithChainId ? EIP712_DOMAIN : EIP712_DOMAIN_BEFORE_V130,
@@ -44,30 +52,74 @@ export function getEip712MessageTypes(safeVersion: string): {
}
}
+export function getEip712MessageTypes(safeVersion: string): EIP712MessageTypes {
+ const eip712WithChainId = semverSatisfies(safeVersion, EQ_OR_GT_1_3_0)
+ return {
+ EIP712Domain: eip712WithChainId ? EIP712_DOMAIN : EIP712_DOMAIN_BEFORE_V130,
+ SafeMessage: [{ type: 'bytes', name: 'message' }]
+ }
+}
+
+export const hashTypedData = (typedData: EIP712TypedData): string => {
+ // `ethers` doesn't require `EIP712Domain` and otherwise throws
+ const { EIP712Domain: _, ...types } = typedData.types
+ return _TypedDataEncoder.hash(typedData.domain as TypedDataDomain, types, typedData.message)
+}
+
+const hashMessage = (message: string): string => {
+ return soliditySha3(utf8ToHex(message)) || ''
+}
+
+const hashSafeMessage = (message: string | EIP712TypedData): string => {
+ return typeof message === 'string' ? hashMessage(message) : hashTypedData(message)
+}
+
export function generateTypedData({
safeAddress,
safeVersion,
chainId,
- safeTransactionData
-}: SafeTransactionEIP712Args): GenerateTypedData {
+ data
+}: SafeEIP712Args): EIP712TypedDataTx | EIP712TypedDataMessage {
+ const isSafeTransactionDataType = data.hasOwnProperty('to')
+
const eip712WithChainId = semverSatisfies(safeVersion, EQ_OR_GT_1_3_0)
- const typedData: GenerateTypedData = {
- types: getEip712MessageTypes(safeVersion),
- domain: {
- verifyingContract: safeAddress
- },
- primaryType: 'SafeTx',
- message: {
- ...safeTransactionData,
- value: safeTransactionData.value,
- safeTxGas: safeTransactionData.safeTxGas,
- baseGas: safeTransactionData.baseGas,
- gasPrice: safeTransactionData.gasPrice,
- nonce: safeTransactionData.nonce
+
+ let typedData: EIP712TypedDataTx | EIP712TypedDataMessage
+
+ if (isSafeTransactionDataType) {
+ const txData = data as SafeTransactionData
+
+ typedData = {
+ types: getEip712TxTypes(safeVersion),
+ domain: {
+ verifyingContract: safeAddress
+ },
+ primaryType: 'SafeTx',
+ message: {
+ ...txData,
+ value: txData.value,
+ safeTxGas: txData.safeTxGas,
+ baseGas: txData.baseGas,
+ gasPrice: txData.gasPrice,
+ nonce: txData.nonce
+ }
+ }
+ } else {
+ const message = data as string | EIP712TypedData
+
+ typedData = {
+ types: getEip712MessageTypes(safeVersion),
+ domain: {
+ verifyingContract: safeAddress
+ },
+ primaryType: 'SafeMessage',
+ message: { message: hashSafeMessage(message) }
}
}
+
if (eip712WithChainId) {
typedData.domain.chainId = chainId
}
+
return typedData
}
diff --git a/packages/protocol-kit/src/utils/signatures/SafeSignature.ts b/packages/protocol-kit/src/utils/signatures/SafeSignature.ts
index f54c6de61..5e48b0a69 100644
--- a/packages/protocol-kit/src/utils/signatures/SafeSignature.ts
+++ b/packages/protocol-kit/src/utils/signatures/SafeSignature.ts
@@ -3,6 +3,7 @@ import { SafeSignature } from '@safe-global/safe-core-sdk-types'
export class EthSafeSignature implements SafeSignature {
signer: string
data: string
+ isSmartContractSignature: boolean
/**
* Creates an instance of a Safe signature.
@@ -11,9 +12,10 @@ export class EthSafeSignature implements SafeSignature {
* @param signature - The Safe signature
* @returns The Safe signature instance
*/
- constructor(signer: string, signature: string) {
+ constructor(signer: string, signature: string, isSmartContractSignature = false) {
this.signer = signer
this.data = signature
+ this.isSmartContractSignature = isSmartContractSignature
}
/**
@@ -21,7 +23,11 @@ export class EthSafeSignature implements SafeSignature {
*
* @returns The static part of the Safe signature
*/
- staticPart(/* dynamicOffset: number */) {
+ staticPart(dynamicOffset?: string) {
+ if (this.isSmartContractSignature) {
+ return `${this.signer.slice(2).padStart(64, '0')}${dynamicOffset || ''}00`
+ }
+
return this.data
}
@@ -31,6 +37,11 @@ export class EthSafeSignature implements SafeSignature {
* @returns The dynamic part of the Safe signature
*/
dynamicPart() {
+ if (this.isSmartContractSignature) {
+ const dynamicPartLength = (this.data.slice(2).length / 2).toString(16).padStart(64, '0')
+ return `${dynamicPartLength}${this.data.slice(2)}`
+ }
+
return ''
}
}
diff --git a/packages/protocol-kit/src/utils/signatures/utils.ts b/packages/protocol-kit/src/utils/signatures/utils.ts
index cab75eb71..40e000a14 100644
--- a/packages/protocol-kit/src/utils/signatures/utils.ts
+++ b/packages/protocol-kit/src/utils/signatures/utils.ts
@@ -1,8 +1,4 @@
-import {
- EthAdapter,
- SafeSignature,
- SafeTransactionEIP712Args
-} from '@safe-global/safe-core-sdk-types'
+import { EthAdapter, SafeSignature, SafeEIP712Args } from '@safe-global/safe-core-sdk-types'
import { bufferToHex, ecrecover, pubToAddress } from 'ethereumjs-util'
import { sameString } from '../address'
import { EthSafeSignature } from './SafeSignature'
@@ -103,21 +99,64 @@ export async function generateSignature(
if (!signerAddress) {
throw new Error('EthAdapter must be initialized with a signer to use this method')
}
+
let signature = await ethAdapter.signMessage(hash)
+
signature = adjustVInSignature('eth_sign', signature, hash, signerAddress)
return new EthSafeSignature(signerAddress, signature)
}
export async function generateEIP712Signature(
ethAdapter: EthAdapter,
- safeTransactionEIP712Args: SafeTransactionEIP712Args,
+ safeEIP712Args: SafeEIP712Args,
methodVersion?: 'v3' | 'v4'
): Promise {
const signerAddress = await ethAdapter.getSignerAddress()
if (!signerAddress) {
throw new Error('EthAdapter must be initialized with a signer to use this method')
}
- let signature = await ethAdapter.signTypedData(safeTransactionEIP712Args, methodVersion)
+
+ let signature = await ethAdapter.signTypedData(safeEIP712Args, methodVersion)
+
signature = adjustVInSignature('eth_signTypedData', signature)
return new EthSafeSignature(signerAddress, signature)
}
+
+export const buildSignature = (signatures: SafeSignature[]): string => {
+ const SIGNATURE_LENGTH_BYTES = 65
+
+ signatures.sort((left, right) =>
+ left.signer.toLowerCase().localeCompare(right.signer.toLowerCase())
+ )
+
+ let signatureBytes = '0x'
+ let dynamicBytes = ''
+
+ for (const sig of signatures) {
+ if (sig.isSmartContractSignature) {
+ /*
+ A contract signature has a static part of 65 bytes and the dynamic part that needs to be appended
+ at the end of signature bytes.
+ The signature format is
+ Signature type == 0
+ Constant part: 65 bytes
+ {32-bytes signature verifier}{32-bytes dynamic data position}{1-byte signature type}
+ Dynamic part (solidity bytes): 32 bytes + signature data length
+ {32-bytes signature length}{bytes signature data}
+ */
+ const dynamicPartPosition = (
+ signatures.length * SIGNATURE_LENGTH_BYTES +
+ dynamicBytes.length / 2
+ )
+ .toString(16)
+ .padStart(64, '0')
+
+ signatureBytes += sig.staticPart(dynamicPartPosition)
+ dynamicBytes += sig.dynamicPart()
+ } else {
+ signatureBytes += sig.data.slice(2)
+ }
+ }
+
+ return signatureBytes + dynamicBytes
+}
diff --git a/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts b/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts
index 2eb45494e..489285bea 100644
--- a/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts
+++ b/packages/protocol-kit/src/utils/transactions/SafeTransaction.ts
@@ -3,6 +3,7 @@ import {
SafeTransaction,
SafeTransactionData
} from '@safe-global/safe-core-sdk-types'
+import { buildSignature } from '../signatures'
class EthSafeTransaction implements SafeTransaction {
data: SafeTransactionData
@@ -17,16 +18,7 @@ class EthSafeTransaction implements SafeTransaction {
}
encodedSignatures(): string {
- const signers = Array.from(this.signatures.keys()).sort()
- const baseOffset = signers.length * 65
- let staticParts = ''
- let dynamicParts = ''
- signers.forEach((signerAddress) => {
- const signature = this.signatures.get(signerAddress)
- staticParts += signature?.staticPart(/*baseOffset + dynamicParts.length / 2*/).slice(2)
- dynamicParts += signature?.dynamicPart()
- })
- return '0x' + staticParts + dynamicParts
+ return buildSignature(Array.from(this.signatures.values()))
}
}
diff --git a/packages/protocol-kit/tests/e2e/eip1271.test.ts b/packages/protocol-kit/tests/e2e/eip1271.test.ts
new file mode 100644
index 000000000..1d8c347a4
--- /dev/null
+++ b/packages/protocol-kit/tests/e2e/eip1271.test.ts
@@ -0,0 +1,333 @@
+import Safe from '@safe-global/protocol-kit/index'
+import { safeVersionDeployed } from '@safe-global/protocol-kit/hardhat/deploy/deploy-contracts'
+import {
+ OperationType,
+ SafeTransaction,
+ SafeTransactionDataPartial
+} from '@safe-global/safe-core-sdk-types'
+import chai from 'chai'
+import chaiAsPromised from 'chai-as-promised'
+import { deployments, waffle } from 'hardhat'
+import { getContractNetworks } from './utils/setupContractNetworks'
+import { getSafeWithOwners } from './utils/setupContracts'
+import { getEthAdapter } from './utils/setupEthAdapter'
+import { getAccounts } from './utils/setupTestNetwork'
+import { waitSafeTxReceipt } from './utils/transactions'
+import { itif } from './utils/helpers'
+import { BigNumber, ethers } from 'ethers'
+import { buildSignature } from '@safe-global/protocol-kit/utils'
+
+chai.use(chaiAsPromised)
+
+export const preimageSafeTransactionHash = (
+ safeAddress: string,
+ safeTx: SafeTransaction,
+ chainId: number
+): string => {
+ return ethers.utils._TypedDataEncoder.encode(
+ { verifyingContract: safeAddress, chainId },
+ {
+ // "SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)"
+ SafeTx: [
+ { type: 'address', name: 'to' },
+ { type: 'uint256', name: 'value' },
+ { type: 'bytes', name: 'data' },
+ { type: 'uint8', name: 'operation' },
+ { type: 'uint256', name: 'safeTxGas' },
+ { type: 'uint256', name: 'baseGas' },
+ { type: 'uint256', name: 'gasPrice' },
+ { type: 'address', name: 'gasToken' },
+ { type: 'address', name: 'refundReceiver' },
+ { type: 'uint256', name: 'nonce' }
+ ]
+ },
+ safeTx.data
+ )
+}
+
+export const calculateSafeMessageHash = (
+ safeAddress: string,
+ message: string,
+ chainId: number
+): string => {
+ return ethers.utils._TypedDataEncoder.hash(
+ { verifyingContract: safeAddress, chainId },
+ {
+ SafeMessage: [{ type: 'bytes', name: 'message' }]
+ },
+ { message }
+ )
+}
+
+const MESSAGE = 'I am the owner of this Safe account'
+
+describe.only('EIP1271', () => {
+ describe('Using a 2/3 Safe in the context of the EIP1271', async () => {
+ const setupTests = deployments.createFixture(async ({ deployments }) => {
+ await deployments.fixture()
+ const accounts = await getAccounts()
+ const chainId: number = (await waffle.provider.getNetwork()).chainId
+ const contractNetworks = await getContractNetworks(chainId)
+
+ const [account1, account2] = accounts
+
+ // Create a 1/1 Safe to sign the messages
+ const signerSafe = await getSafeWithOwners([accounts[0].address], 1)
+
+ // Create a 2/3 Safe
+ const safe = await getSafeWithOwners(
+ [accounts[0].address, accounts[1].address, signerSafe.address],
+ 2
+ )
+
+ // Adapter and Safe instance for owner 1
+ const ethAdapter1 = await getEthAdapter(account1.signer)
+ const safeSdk1 = await Safe.create({
+ ethAdapter: ethAdapter1,
+ safeAddress: safe.address,
+ contractNetworks
+ })
+
+ // Adapter and Safe instance for owner 2
+ const ethAdapter2 = await getEthAdapter(account2.signer)
+ const safeSdk2 = await Safe.create({
+ ethAdapter: ethAdapter2,
+ safeAddress: safe.address,
+ contractNetworks
+ })
+
+ // Adapter and Safe instance for owner 3
+ const ethAdapter3 = await getEthAdapter(signerSafe.signer)
+ const safeSdk3 = await Safe.create({
+ ethAdapter: ethAdapter3,
+ safeAddress: signerSafe.address,
+ contractNetworks
+ })
+
+ return {
+ safe,
+ signerSafe,
+ accounts,
+ contractNetworks,
+ chainId,
+ ethAdapter1,
+ ethAdapter2,
+ ethAdapter3,
+ safeSdk1,
+ safeSdk2,
+ safeSdk3
+ }
+ })
+
+ itif(safeVersionDeployed >= '1.3.0')(
+ 'should validate on-chain messages (Approved hashes)',
+ async () => {
+ const { contractNetworks, safeSdk1, safeSdk2, ethAdapter1 } = await setupTests()
+
+ const chainId: number = await safeSdk1.getChainId()
+ const safeVersion = await safeSdk1.getContractVersion()
+
+ const customContract = contractNetworks[chainId]
+
+ const signMessageLibContract = await ethAdapter1.getSignMessageLibContract({
+ safeVersion,
+ customContractAddress: customContract.signMessageLibAddress,
+ customContractAbi: customContract.signMessageLibAbi
+ })
+
+ const messageHash = await safeSdk1.getHash(MESSAGE)
+
+ const txData = signMessageLibContract.encode('signMessage', [messageHash])
+
+ const safeTransactionData: SafeTransactionDataPartial = {
+ to: customContract.signMessageLibAddress,
+ value: '0',
+ data: txData,
+ operation: OperationType.DelegateCall
+ }
+
+ const tx = await safeSdk1.createTransaction({ safeTransactionData })
+ const signedTx = await safeSdk1.signTransaction(tx)
+ const signedTx2 = await safeSdk2.signTransaction(signedTx)
+ const execResponse = await safeSdk1.executeTransaction(signedTx2)
+
+ await waitSafeTxReceipt(execResponse)
+
+ const validatedResponse1 = await safeSdk1.isValidSignature(messageHash)
+ chai.expect(validatedResponse1).to.be.true
+
+ const validatedResponse2 = await safeSdk1.isValidSignature(messageHash, '0x')
+ chai.expect(validatedResponse2).to.be.true
+ }
+ )
+
+ itif(safeVersionDeployed >= '1.3.0')('should validate off-chain messages', async () => {
+ const { safeSdk1, safeSdk2 } = await setupTests()
+
+ // Hash the message
+ const messageHash = await safeSdk1.getHash(MESSAGE)
+ const safeMessageHash = await safeSdk1.getSafeMessageHash(messageHash)
+
+ // Sign the Safe message hash with the owners
+ const ethSignSig1 = await safeSdk1.signHash(safeMessageHash)
+ const ethSignSig2 = await safeSdk2.signHash(safeMessageHash)
+
+ // Validate the signature sending the Safe message hash and the concatenated signatures
+ const isValid1 = await safeSdk1.isValidSignature(
+ messageHash,
+ buildSignature([ethSignSig1, ethSignSig2])
+ )
+
+ chai.expect(isValid1).to.be.true
+
+ // Validate the signature sending the Safe message hash and the array of SafeSignature
+ const isValid2 = await safeSdk1.isValidSignature(messageHash, [ethSignSig1, ethSignSig2])
+
+ chai.expect(isValid2).to.be.true
+
+ // Validate the signature is not valid when not enough signers has signed
+ const isValid3 = await safeSdk1.isValidSignature(messageHash, [ethSignSig1])
+
+ chai.expect(isValid3).to.be.false
+ })
+
+ itif(safeVersionDeployed >= '1.3.0')(
+ 'should validate a mix EIP191 and EIP712 signatures',
+ async () => {
+ const { safeSdk1, safeSdk2 } = await setupTests()
+
+ // Hash the message
+ const messageHash = await safeSdk1.getHash(MESSAGE)
+ const safeMessageHash = await safeSdk1.getSafeMessageHash(messageHash)
+
+ // Sign the Safe message with the owners
+ const ethSignSig = await safeSdk1.signHash(safeMessageHash)
+
+ const typedDataSig = await safeSdk2.signTypedData(MESSAGE)
+ // Validate the signature sending the Safe message hash and the concatenated signatures
+ const isValid = await safeSdk1.isValidSignature(messageHash, [typedDataSig, ethSignSig])
+
+ chai.expect(isValid).to.be.true
+ }
+ )
+
+ itif(safeVersionDeployed >= '1.3.0')(
+ 'should validate Smart contracts as signers (threshold = 1)',
+ async () => {
+ const { safeSdk1, safeSdk2, safeSdk3 } = await setupTests()
+
+ // Hash the message
+ const messageHash = await safeSdk1.getHash(MESSAGE)
+ const safeMessageHash = await safeSdk1.getSafeMessageHash(messageHash)
+
+ // Sign the Safe message with the owners
+ const ethSignSig = await safeSdk1.signHash(safeMessageHash)
+ const typedDataSig = await safeSdk2.signTypedData(messageHash)
+
+ // Sign with the Smart contract
+ const safeSignerMessageHash = await safeSdk3.getSafeMessageHash(messageHash)
+ const signerSafeSig = await safeSdk3.signHash(safeSignerMessageHash, true)
+
+ // Validate the signature sending the Safe message hash and the concatenated signatures
+ const isValid = await safeSdk1.isValidSignature(messageHash, [
+ signerSafeSig,
+ ethSignSig,
+ typedDataSig
+ ])
+
+ chai.expect(isValid).to.be.true
+ }
+ )
+
+ itif(safeVersionDeployed >= '1.3.0')('should revert when message is not signed', async () => {
+ const { safeSdk1 } = await setupTests()
+
+ const response = await safeSdk1.isValidSignature(await safeSdk1.getHash(MESSAGE), '0x')
+
+ chai.expect(response).to.be.false
+ })
+
+ itif(safeVersionDeployed >= '1.3.0')(
+ 'should generate the correct safeMessageHash',
+ async () => {
+ const { safe, safeSdk1 } = await setupTests()
+
+ const chainId = await safeSdk1.getChainId()
+ const messageHash = await safeSdk1.getHash(MESSAGE)
+ const safeMessageHash = await safeSdk1.getSafeMessageHash(messageHash)
+
+ chai
+ .expect(safeMessageHash)
+ .to.be.eq(calculateSafeMessageHash(safe.address, messageHash, chainId))
+ }
+ )
+
+ it.only('should allow use to sign transactions using Safe Accounts (threshold = 1)', async () => {
+ const { safe, accounts, safeSdk1, safeSdk2, safeSdk3, signerSafe } = await setupTests()
+
+ const [account1] = accounts
+
+ await account1.signer.sendTransaction({
+ to: safe.address,
+ value: BigNumber.from('1000000000000000000') // 1 ETH
+ })
+
+ const balanceBefore = await safeSdk1.getBalance()
+ console.log('BALANCE BEFORE: ', balanceBefore.toString())
+
+ const safeTransactionData: SafeTransactionDataPartial = {
+ to: account1.address,
+ value: '100000000000000000', // 0.01 ETH
+ data: '0x'
+ }
+
+ const tx = await safeSdk1.createTransaction({ safeTransactionData })
+ const txHash = await safeSdk1.getHash(tx)
+
+ const signature1 = await safeSdk1.signHash(await safeSdk1.getSafeMessageHash(txHash))
+ const signature2 = await safeSdk3.signHash(
+ await safeSdk3.getSafeMessageHash(
+ preimageSafeTransactionHash(signerSafe.address, tx, await safeSdk3.getChainId())
+ ),
+ true
+ )
+
+ console.log('OWNER 1: ', signature1.signer)
+ console.log('OWNER 2: ', signature2.signer)
+
+ // const isValidSignature = await safeSdk1.isValidSignature(txHash, [signature1, signature2])
+ // console.log('IS VALID SIGNATURE: ', isValidSignature)
+ // chai.expect(isValidSignature).to.be.true
+
+ // TODO: This is failing because the owner is invalid
+ tx.addSignature(signature1)
+ tx.addSignature(signature2)
+ console.log(signature1, signature2)
+ console.log('signature: ', buildSignature([signature1, signature2]))
+
+ const execResponse = await safeSdk1.executeTransaction(tx, { gasLimit: 1000000 })
+
+ const receipt = await waitSafeTxReceipt(execResponse)
+ const balanceAfter = await safeSdk1.getBalance()
+
+ console.log('BALANCE AFTER: ', balanceAfter.toString())
+ console.log('RECEIPT:', receipt)
+ chai.expect(tx.signatures.size).to.be.eq(2)
+ chai.expect(receipt?.status).to.be.eq(1)
+
+ // TODO: This is failing because the owner is invalid
+ // const signedTx = await safeSdk1.signTransaction(tx)
+ // const signedTx2 = await safeSdk3.signTransaction(signedTx, 'eth_signTypedData_v4', true)
+
+ // const execResponse = await safeSdk1.executeTransaction(signedTx2, { gasLimit: 1000000 })
+
+ // const receipt = await waitSafeTxReceipt(execResponse)
+ // const balanceAfter = await safeSdk1.getBalance()
+
+ // console.log('BALANCE AFTER: ', balanceAfter.toString())
+ // console.log('RECEIPT:', receipt)
+ // chai.expect(tx.signatures.size).to.be.eq(2)
+ // chai.expect(receipt?.status).to.be.eq(1)
+ })
+ })
+})
diff --git a/packages/protocol-kit/tests/e2e/execution.test.ts b/packages/protocol-kit/tests/e2e/execution.test.ts
index e1acfe2f6..461946b9e 100644
--- a/packages/protocol-kit/tests/e2e/execution.test.ts
+++ b/packages/protocol-kit/tests/e2e/execution.test.ts
@@ -144,7 +144,7 @@ describe('Transactions execution', () => {
}
const tx = await safeSdk1.createTransaction({ safeTransactionData })
const signedTx = await safeSdk1.signTransaction(tx)
- const txHash = await safeSdk2.getTransactionHash(tx)
+ const txHash = await safeSdk2.getHash(tx)
const txResponse = await safeSdk2.approveTransactionHash(txHash)
await waitSafeTxReceipt(txResponse)
await chai
@@ -321,7 +321,7 @@ describe('Transactions execution', () => {
const tx = await safeSdk1.createTransaction({ safeTransactionData })
// Signature: on-chain
- const txHash = await safeSdk1.getTransactionHash(tx)
+ const txHash = await safeSdk1.getHash(tx)
const txResponse1 = await safeSdk1.approveTransactionHash(txHash)
await waitSafeTxReceipt(txResponse1)
@@ -376,7 +376,7 @@ describe('Transactions execution', () => {
const tx = await safeSdk1.createTransaction({ safeTransactionData })
// Signature: on-chain
- const txHash = await safeSdk1.getTransactionHash(tx)
+ const txHash = await safeSdk1.getHash(tx)
const txResponse1 = await safeSdk1.approveTransactionHash(txHash)
await waitSafeTxReceipt(txResponse1)
@@ -437,7 +437,7 @@ describe('Transactions execution', () => {
const tx = await safeSdk1.createTransaction({ safeTransactionData })
// Signature: on-chain
- const txHash = await safeSdk1.getTransactionHash(tx)
+ const txHash = await safeSdk1.getHash(tx)
const txResponse1 = await safeSdk1.approveTransactionHash(txHash)
await waitSafeTxReceipt(txResponse1)
@@ -504,7 +504,7 @@ describe('Transactions execution', () => {
const tx = await safeSdk1.createTransaction({ safeTransactionData })
// Signature: on-chain
- const txHash = await safeSdk1.getTransactionHash(tx)
+ const txHash = await safeSdk1.getHash(tx)
const txResponse1 = await safeSdk1.approveTransactionHash(txHash)
await waitSafeTxReceipt(txResponse1)
@@ -557,7 +557,7 @@ describe('Transactions execution', () => {
}
const tx = await safeSdk1.createTransaction({ safeTransactionData })
const signedTx = await safeSdk1.signTransaction(tx)
- const txHash = await safeSdk2.getTransactionHash(tx)
+ const txHash = await safeSdk2.getHash(tx)
const txResponse1 = await safeSdk2.approveTransactionHash(txHash)
await waitSafeTxReceipt(txResponse1)
const txResponse2 = await safeSdk3.executeTransaction(signedTx)
@@ -835,7 +835,7 @@ describe('Transactions execution', () => {
]
const multiSendTx = await safeSdk1.createTransaction({ safeTransactionData })
const signedMultiSendTx = await safeSdk1.signTransaction(multiSendTx)
- const txHash = await safeSdk2.getTransactionHash(multiSendTx)
+ const txHash = await safeSdk2.getHash(multiSendTx)
const txResponse1 = await safeSdk2.approveTransactionHash(txHash)
await waitSafeTxReceipt(txResponse1)
const txResponse2 = await safeSdk3.executeTransaction(signedMultiSendTx)
@@ -892,7 +892,7 @@ describe('Transactions execution', () => {
]
const multiSendTx = await safeSdk1.createTransaction({ safeTransactionData })
const signedMultiSendTx = await safeSdk1.signTransaction(multiSendTx)
- const txHash = await safeSdk2.getTransactionHash(multiSendTx)
+ const txHash = await safeSdk2.getHash(multiSendTx)
const txResponse1 = await safeSdk2.approveTransactionHash(txHash)
await waitSafeTxReceipt(txResponse1)
const txResponse2 = await safeSdk3.executeTransaction(signedMultiSendTx)
diff --git a/packages/protocol-kit/tests/e2e/offChainSignatures.test.ts b/packages/protocol-kit/tests/e2e/offChainSignatures.test.ts
index c4d368f94..a34d9f7d3 100644
--- a/packages/protocol-kit/tests/e2e/offChainSignatures.test.ts
+++ b/packages/protocol-kit/tests/e2e/offChainSignatures.test.ts
@@ -38,7 +38,7 @@ describe('Off-chain signatures', () => {
}
})
- describe('signTransactionHash', async () => {
+ describe('signHash', async () => {
it('should sign a transaction hash with the current signer if the Safe is not deployed', async () => {
const { predictedSafe, accounts, contractNetworks } = await setupTests()
const [account1] = accounts
@@ -49,7 +49,7 @@ describe('Off-chain signatures', () => {
contractNetworks
})
const txHash = '0xcbf14050c5fcc9b71d4a3ab874cc728db101d19d4466d56fcdbb805117a28c64'
- const signature = await safeSdk.signTransactionHash(txHash)
+ const signature = await safeSdk.signHash(txHash)
chai.expect(signature.staticPart().length).to.be.eq(132)
})
@@ -68,8 +68,8 @@ describe('Off-chain signatures', () => {
data: '0x'
}
const tx = await safeSdk.createTransaction({ safeTransactionData })
- const txHash = await safeSdk.getTransactionHash(tx)
- const signature = await safeSdk.signTransactionHash(txHash)
+ const txHash = await safeSdk.getHash(tx)
+ const signature = await safeSdk.signHash(txHash)
chai.expect(signature.staticPart().length).to.be.eq(132)
})
})
diff --git a/packages/protocol-kit/tests/e2e/onChainSignatures.test.ts b/packages/protocol-kit/tests/e2e/onChainSignatures.test.ts
index 6eef8d655..e713b6f26 100644
--- a/packages/protocol-kit/tests/e2e/onChainSignatures.test.ts
+++ b/packages/protocol-kit/tests/e2e/onChainSignatures.test.ts
@@ -66,7 +66,7 @@ describe('On-chain signatures', () => {
data: '0x'
}
const tx = await safeSdk1.createTransaction({ safeTransactionData })
- const hash = await safeSdk1.getTransactionHash(tx)
+ const hash = await safeSdk1.getHash(tx)
await chai
.expect(safeSdk1.approveTransactionHash(hash))
.to.be.rejectedWith('Transaction hashes can only be approved by Safe owners')
@@ -87,7 +87,7 @@ describe('On-chain signatures', () => {
data: '0x'
}
const tx = await safeSdk1.createTransaction({ safeTransactionData })
- const txHash = await safeSdk1.getTransactionHash(tx)
+ const txHash = await safeSdk1.getHash(tx)
const txResponse = await safeSdk1.approveTransactionHash(txHash)
await waitSafeTxReceipt(txResponse)
chai.expect(await safe.approvedHashes(account1.address, txHash)).to.be.equal(1)
@@ -108,7 +108,7 @@ describe('On-chain signatures', () => {
data: '0x'
}
const tx = await safeSdk1.createTransaction({ safeTransactionData })
- const txHash = await safeSdk1.getTransactionHash(tx)
+ const txHash = await safeSdk1.getHash(tx)
chai.expect(await safe.approvedHashes(account1.address, txHash)).to.be.equal(0)
const txResponse1 = await safeSdk1.approveTransactionHash(txHash)
await waitSafeTxReceipt(txResponse1)
@@ -151,7 +151,7 @@ describe('On-chain signatures', () => {
data: '0x'
}
const tx = await safeSdk1.createTransaction({ safeTransactionData })
- const txHash = await safeSdk1.getTransactionHash(tx)
+ const txHash = await safeSdk1.getHash(tx)
const ownersWhoApproved0 = await safeSdk1.getOwnersWhoApprovedTx(txHash)
chai.expect(ownersWhoApproved0.length).to.be.eq(0)
const txResponse1 = await safeSdk1.approveTransactionHash(txHash)
diff --git a/packages/protocol-kit/tests/unit/eip-712.test.ts b/packages/protocol-kit/tests/unit/eip-712.test.ts
index ba50ac4ac..31a187c7f 100644
--- a/packages/protocol-kit/tests/unit/eip-712.test.ts
+++ b/packages/protocol-kit/tests/unit/eip-712.test.ts
@@ -4,7 +4,7 @@ import {
EIP712_DOMAIN,
EIP712_DOMAIN_BEFORE_V130,
generateTypedData,
- getEip712MessageTypes
+ getEip712TxTypes
} from '@safe-global/protocol-kit/utils'
const safeAddress = '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1'
@@ -21,36 +21,36 @@ const safeTransactionData: SafeTransactionData = {
nonce: 999
}
-describe('EIP-712 sign typed data', () => {
- describe('getEip712MessageTypes', async () => {
+describe.only('EIP-712 sign typed data', () => {
+ describe('getEip712TxTypes', async () => {
it('should have the domain typed as EIP712_DOMAIN_BEFORE_V130 for Safes == v1.0.0', async () => {
- const { EIP712Domain } = getEip712MessageTypes('1.0.0')
+ const { EIP712Domain } = getEip712TxTypes('1.0.0')
chai.expect(EIP712Domain).to.be.eq(EIP712_DOMAIN_BEFORE_V130)
})
it('should have the domain typed as EIP712_DOMAIN_BEFORE_V130 for Safes == v1.1.1', async () => {
- const { EIP712Domain } = getEip712MessageTypes('1.1.1')
+ const { EIP712Domain } = getEip712TxTypes('1.1.1')
chai.expect(EIP712Domain).to.be.eq(EIP712_DOMAIN_BEFORE_V130)
})
it('should have the domain typed as EIP712_DOMAIN_BEFORE_V130 for Safes == v1.2.0', async () => {
- const { EIP712Domain } = getEip712MessageTypes('1.2.0')
+ const { EIP712Domain } = getEip712TxTypes('1.2.0')
chai.expect(EIP712Domain).to.be.eq(EIP712_DOMAIN_BEFORE_V130)
})
it('should have the domain typed as EIP712_DOMAIN for Safes >= v1.3.0', async () => {
- const { EIP712Domain } = getEip712MessageTypes('1.3.0')
+ const { EIP712Domain } = getEip712TxTypes('1.3.0')
chai.expect(EIP712Domain).to.be.eq(EIP712_DOMAIN)
})
})
- describe('generateTypedData', async () => {
+ describe.only('generateTypedData', async () => {
it('should generate the typed data for Safes == v1.0.0', async () => {
const { domain } = generateTypedData({
safeAddress,
safeVersion: '1.0.0',
chainId: 4,
- safeTransactionData
+ data: safeTransactionData
})
chai.expect(domain.verifyingContract).to.be.eq(safeAddress)
chai.expect(domain.chainId).to.be.undefined
@@ -61,7 +61,7 @@ describe('EIP-712 sign typed data', () => {
safeAddress,
safeVersion: '1.1.1',
chainId: 4,
- safeTransactionData
+ data: safeTransactionData
})
chai.expect(domain.verifyingContract).to.be.eq(safeAddress)
chai.expect(domain.chainId).to.be.undefined
@@ -72,7 +72,7 @@ describe('EIP-712 sign typed data', () => {
safeAddress,
safeVersion: '1.2.0',
chainId: 4,
- safeTransactionData
+ data: safeTransactionData
})
chai.expect(domain.verifyingContract).to.be.eq(safeAddress)
chai.expect(domain.chainId).to.be.undefined
@@ -84,10 +84,266 @@ describe('EIP-712 sign typed data', () => {
safeAddress,
safeVersion: '1.3.0',
chainId,
- safeTransactionData
+ data: safeTransactionData
})
chai.expect(domain.verifyingContract).to.be.eq(safeAddress)
chai.expect(domain.chainId).to.be.eq(chainId)
})
+
+ it('should generate the correct types for a EIP-191 message for >= 1.3.0 Safes', () => {
+ const message = 'Hello world!'
+
+ const safeMessage = generateTypedData({
+ safeAddress,
+ safeVersion: '1.3.0',
+ chainId: 1,
+ data: message
+ })
+
+ chai.expect(safeMessage).to.deep.eq({
+ types: {
+ EIP712Domain: [
+ {
+ name: 'chainId',
+ type: 'uint256'
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address'
+ }
+ ],
+ SafeMessage: [{ name: 'message', type: 'bytes' }]
+ },
+ domain: {
+ chainId: 1,
+ verifyingContract: safeAddress
+ },
+ primaryType: 'SafeMessage',
+ message: {
+ message: '0xecd0e108a98e192af1d2c25055f4e3bed784b5c877204e73219a5203251feaab'
+ }
+ })
+ })
+
+ it('should generate the correct types for a EIP-191 message for < 1.3.0 Safes', () => {
+ const message = 'Hello world!'
+
+ const safeMessage = generateTypedData({
+ safeAddress,
+ safeVersion: '1.1.1',
+ chainId: 1,
+ data: message
+ })
+
+ chai.expect(safeMessage).to.deep.eq({
+ types: {
+ EIP712Domain: [
+ {
+ name: 'verifyingContract',
+ type: 'address'
+ }
+ ],
+ SafeMessage: [{ name: 'message', type: 'bytes' }]
+ },
+ domain: {
+ verifyingContract: safeAddress
+ },
+ primaryType: 'SafeMessage',
+ message: {
+ message: '0xecd0e108a98e192af1d2c25055f4e3bed784b5c877204e73219a5203251feaab'
+ }
+ })
+ })
+
+ it('should generate the correct types for an EIP-712 message for >=1.3.0 Safes', () => {
+ const message = {
+ domain: {
+ chainId: 1,
+ name: 'Ether Mail',
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ version: '1'
+ },
+ message: {
+ contents: 'Hello, Bob!',
+ from: {
+ name: 'Cow',
+ wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'
+ },
+ to: {
+ name: 'Bob',
+ wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'
+ }
+ },
+ primaryType: 'Mail',
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string'
+ },
+ {
+ name: 'version',
+ type: 'string'
+ },
+ {
+ name: 'chainId',
+ type: 'uint256'
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address'
+ }
+ ],
+ Mail: [
+ {
+ name: 'from',
+ type: 'Person'
+ },
+ {
+ name: 'to',
+ type: 'Person'
+ },
+ {
+ name: 'contents',
+ type: 'string'
+ }
+ ],
+ Person: [
+ {
+ name: 'name',
+ type: 'string'
+ },
+ {
+ name: 'wallet',
+ type: 'address'
+ }
+ ]
+ }
+ }
+
+ const safeMessage = generateTypedData({
+ safeAddress,
+ safeVersion: '1.3.0',
+ chainId: 1,
+ data: message
+ })
+
+ chai.expect(safeMessage).to.deep.eq({
+ types: {
+ EIP712Domain: [
+ {
+ name: 'chainId',
+ type: 'uint256'
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address'
+ }
+ ],
+ SafeMessage: [{ name: 'message', type: 'bytes' }]
+ },
+ domain: {
+ chainId: 1,
+ verifyingContract: safeAddress
+ },
+ primaryType: 'SafeMessage',
+ message: {
+ message: '0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2'
+ }
+ })
+ })
+
+ it('should generate the correct types for an EIP-712 message for <1.3.0 Safes', () => {
+ const message = {
+ domain: {
+ chainId: 1,
+ name: 'Ether Mail',
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
+ version: '1'
+ },
+ message: {
+ contents: 'Hello, Bob!',
+ from: {
+ name: 'Cow',
+ wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'
+ },
+ to: {
+ name: 'Bob',
+ wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'
+ }
+ },
+ primaryType: 'Mail',
+ types: {
+ EIP712Domain: [
+ {
+ name: 'name',
+ type: 'string'
+ },
+ {
+ name: 'version',
+ type: 'string'
+ },
+ {
+ name: 'chainId',
+ type: 'uint256'
+ },
+ {
+ name: 'verifyingContract',
+ type: 'address'
+ }
+ ],
+ Mail: [
+ {
+ name: 'from',
+ type: 'Person'
+ },
+ {
+ name: 'to',
+ type: 'Person'
+ },
+ {
+ name: 'contents',
+ type: 'string'
+ }
+ ],
+ Person: [
+ {
+ name: 'name',
+ type: 'string'
+ },
+ {
+ name: 'wallet',
+ type: 'address'
+ }
+ ]
+ }
+ }
+
+ const safeMessage = generateTypedData({
+ safeAddress,
+ safeVersion: '1.1.1',
+ chainId: 1,
+ data: message
+ })
+
+ chai.expect(safeMessage).to.deep.eq({
+ types: {
+ EIP712Domain: [
+ {
+ name: 'verifyingContract',
+ type: 'address'
+ }
+ ],
+ SafeMessage: [{ name: 'message', type: 'bytes' }]
+ },
+ domain: {
+ verifyingContract: safeAddress
+ },
+ primaryType: 'SafeMessage',
+ message: {
+ message: '0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2'
+ }
+ })
+ })
})
})
diff --git a/packages/safe-core-sdk-types/src/ethereumLibs/EthAdapter.ts b/packages/safe-core-sdk-types/src/ethereumLibs/EthAdapter.ts
index 880341eec..fd68d0659 100644
--- a/packages/safe-core-sdk-types/src/ethereumLibs/EthAdapter.ts
+++ b/packages/safe-core-sdk-types/src/ethereumLibs/EthAdapter.ts
@@ -7,11 +7,7 @@ import { SafeContract } from '@safe-global/safe-core-sdk-types/contracts/SafeCon
import { SafeProxyFactoryContract } from '@safe-global/safe-core-sdk-types/contracts/SafeProxyFactoryContract'
import { SignMessageLibContract } from '@safe-global/safe-core-sdk-types/contracts/SignMessageLibContract'
import { SimulateTxAccessorContract } from '@safe-global/safe-core-sdk-types/contracts/SimulateTxAccessorContract'
-import {
- Eip3770Address,
- SafeTransactionEIP712Args,
- SafeVersion
-} from '@safe-global/safe-core-sdk-types/types'
+import { Eip3770Address, SafeEIP712Args, SafeVersion } from '@safe-global/safe-core-sdk-types/types'
import { SingletonDeployment } from '@safe-global/safe-deployments'
import { AbiItem } from 'web3-utils'
@@ -94,10 +90,7 @@ export interface EthAdapter {
getTransaction(transactionHash: string): Promise
getSignerAddress(): Promise
signMessage(message: string): Promise
- signTypedData(
- safeTransactionEIP712Args: SafeTransactionEIP712Args,
- signTypedDataVersion?: string
- ): Promise
+ signTypedData(safeEIP712Args: SafeEIP712Args, signTypedDataVersion?: string): Promise
estimateGas(
transaction: EthAdapterTransaction,
callback?: (error: Error, gas: number) => void
diff --git a/packages/safe-core-sdk-types/src/types.ts b/packages/safe-core-sdk-types/src/types.ts
index 09382eac4..95df98172 100644
--- a/packages/safe-core-sdk-types/src/types.ts
+++ b/packages/safe-core-sdk-types/src/types.ts
@@ -47,7 +47,8 @@ export interface SafeTransactionDataPartial extends MetaTransactionData {
export interface SafeSignature {
readonly signer: string
readonly data: string
- staticPart(): string
+ readonly isSmartContractSignature: boolean
+ staticPart(dynamicOffset?: string): string
dynamicPart(): string
}
@@ -91,14 +92,14 @@ export interface Eip3770Address {
address: string
}
-export interface SafeTransactionEIP712Args {
+export interface SafeEIP712Args {
safeAddress: string
safeVersion: string
chainId: number
- safeTransactionData: SafeTransactionData
+ data: SafeTransactionData | EIP712TypedData | string
}
-export interface Eip712MessageTypes {
+export interface EIP712TxTypes {
EIP712Domain: {
type: string
name: string
@@ -109,13 +110,28 @@ export interface Eip712MessageTypes {
}[]
}
-export interface GenerateTypedData {
- types: Eip712MessageTypes
+export interface EIP712MessageTypes {
+ EIP712Domain: {
+ type: string
+ name: string
+ }[]
+ SafeMessage: [
+ {
+ type: 'bytes'
+ name: 'message'
+ }
+ ]
+}
+
+export type EIP712Types = EIP712TxTypes | EIP712MessageTypes
+
+export interface EIP712TypedDataTx {
+ types: EIP712TxTypes
domain: {
chainId?: number
verifyingContract: string
}
- primaryType: string
+ primaryType: 'SafeTx'
message: {
to: string
value: string
@@ -130,6 +146,42 @@ export interface GenerateTypedData {
}
}
+export interface EIP712TypedDataMessage {
+ types: EIP712MessageTypes
+ domain: {
+ chainId?: number
+ verifyingContract: string
+ }
+ primaryType: 'SafeMessage'
+ message: {
+ message: string
+ }
+}
+
+interface TypedDataDomain {
+ name?: string
+ version?: string
+ chainId?: unknown
+ verifyingContract?: string
+ salt?: ArrayLike | string
+}
+
+interface TypedDataTypes {
+ name: string
+ type: string
+}
+
+type TypedMessageTypes = {
+ [key: string]: TypedDataTypes[]
+}
+
+export interface EIP712TypedData {
+ domain: TypedDataDomain
+ types: TypedMessageTypes
+ message: Record
+ primaryType?: string
+}
+
export type SafeMultisigConfirmationResponse = {
readonly owner: string
readonly submissionDate: string
diff --git a/playground/api-kit/confirm-transaction.ts b/playground/api-kit/confirm-transaction.ts
index e8d841c16..67dcc2f3a 100644
--- a/playground/api-kit/confirm-transaction.ts
+++ b/playground/api-kit/confirm-transaction.ts
@@ -52,7 +52,7 @@ async function main() {
// const transactions = await service.getAllTransactions()
const safeTxHash = transaction.transactionHash
- const signature = await safe.signTransactionHash(safeTxHash)
+ const signature = await safe.signHash(safeTxHash)
// Confirm the Safe transaction
const signatureResponse = await service.confirmTransaction(safeTxHash, signature.data)
diff --git a/playground/api-kit/propose-transaction.ts b/playground/api-kit/propose-transaction.ts
index a195089c8..b4b7da42a 100644
--- a/playground/api-kit/propose-transaction.ts
+++ b/playground/api-kit/propose-transaction.ts
@@ -52,8 +52,8 @@ async function main() {
const safeTransaction = await safe.createTransaction({ safeTransactionData })
const senderAddress = await signer.getAddress()
- const safeTxHash = await safe.getTransactionHash(safeTransaction)
- const signature = await safe.signTransactionHash(safeTxHash)
+ const safeTxHash = await safe.getHash(safeTransaction)
+ const signature = await safe.signHash(safeTxHash)
// Propose transaction to the service
await service.proposeTransaction({
diff --git a/playground/config/run.ts b/playground/config/run.ts
index 7b546f34a..ad9ceaa0b 100644
--- a/playground/config/run.ts
+++ b/playground/config/run.ts
@@ -4,7 +4,8 @@ const playInput = process.argv[2]
const playgroundProtocolKitPaths = {
'deploy-safe': 'protocol-kit/deploy-safe',
- 'generate-safe-address': 'protocol-kit/generate-safe-address'
+ 'generate-safe-address': 'protocol-kit/generate-safe-address',
+ eip1271: 'protocol-kit/eip1271'
}
const playgroundApiKitPaths = {
'propose-transaction': 'api-kit/propose-transaction',
diff --git a/playground/protocol-kit/eip1271.ts b/playground/protocol-kit/eip1271.ts
new file mode 100644
index 000000000..0f531e48f
--- /dev/null
+++ b/playground/protocol-kit/eip1271.ts
@@ -0,0 +1,65 @@
+import Safe from '@safe-global/protocol-kit'
+import { EthersAdapter } from '@safe-global/protocol-kit'
+import { ethers } from 'ethers'
+
+// This file can be used to play around with the Safe Core SDK
+
+interface Config {
+ RPC_URL: string
+ OWNER1_PRIVATE_KEY: string
+ OWNER2_PRIVATE_KEY: string
+ OWNER3_PRIVATE_KEY: string
+ SAFE_2_3_ADDRESS: string
+}
+
+const config: Config = {
+ RPC_URL: '',
+ // Create a Safe 2/3 with 3 owners and fill this info
+ OWNER1_PRIVATE_KEY: '',
+ OWNER2_PRIVATE_KEY: '',
+ OWNER3_PRIVATE_KEY: '',
+ SAFE_2_3_ADDRESS: ''
+}
+
+async function main() {
+ const provider = new ethers.providers.JsonRpcProvider(config.RPC_URL)
+ const signer1 = new ethers.Wallet(config.OWNER1_PRIVATE_KEY, provider)
+ const signer2 = new ethers.Wallet(config.OWNER2_PRIVATE_KEY, provider)
+
+ // Create safeSdk instances
+ const safeSdk1 = await Safe.create({
+ ethAdapter: new EthersAdapter({
+ ethers,
+ signerOrProvider: signer1
+ }),
+ safeAddress: config.SAFE_2_3_ADDRESS
+ })
+
+ const safeSdk2 = await Safe.create({
+ ethAdapter: new EthersAdapter({
+ ethers,
+ signerOrProvider: signer2
+ }),
+ safeAddress: config.SAFE_2_3_ADDRESS
+ })
+
+ const MESSAGE_TO_SIGN = 'I am the owner of this Safe account'
+
+ const messageHash = await safeSdk1.getHash(MESSAGE_TO_SIGN)
+ const safeMessageHash = await safeSdk1.getSafeMessageHash(messageHash)
+
+ const ethSignSig = await safeSdk1.signHash(safeMessageHash)
+ const typedDataSig = await safeSdk2.signTypedData(messageHash)
+
+ // Validate the signature sending the Safe message hash and the concatenated signatures
+ const isValid = await safeSdk1.isValidSignature(messageHash, [typedDataSig, ethSignSig])
+
+ console.log('Message: ', MESSAGE_TO_SIGN)
+ console.log('Message Hash: ', messageHash)
+ console.log('Safe Message Hash: ', safeMessageHash)
+ console.log('Signatures: ', ethSignSig, typedDataSig)
+
+ console.log(`The signature is ${isValid ? 'valid' : 'invalid'}`)
+}
+
+main()