Skip to content

Commit

Permalink
Add methods for signing messages and validating signatures (#211)
Browse files Browse the repository at this point in the history
* add signMessage method

* add changest

* add type guard to signMessage

* add readme for signing a message

* calculating hash/validation wip

* Add methods to validate sig

* remove unused codes, slice return value

* dep bump

* fix types in tests

* add test for generating a message hash

* add test comments

* add test for check1271Signature

* add tests for check1271SignatureBytes

* add tests for isSignedMessage

* remove console logs

* message validation docs wip

* add docs for validating message

* fix tests for calculating message hash

* why tf this test is failing

* fix the test by adding timeout

* add test for check1271SignatureBytes

* fix sdk build command

* Separate isMessageHashSigned and isMessageSigned

* remove unused comment

* fix prettier formatting
mmv08 authored Sep 2, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent e8d78d3 commit 035159a
Showing 40 changed files with 1,761 additions and 726 deletions.
5 changes: 5 additions & 0 deletions .changeset/happy-pandas-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@gnosis.pm/safe-apps-sdk': minor
---

Add txs.signMessage method that triggers a call to SignMessageLib
13 changes: 13 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"mode": "pre",
"tag": "next",
"initialVersions": {
"@gnosis.pm/cra-template-safe-app": "4.0.0",
"@gnosis.pm/safe-apps-provider": "0.7.1",
"@gnosis.pm/safe-apps-react-sdk": "4.0.3",
"@gnosis.pm/safe-apps-sdk": "4.2.0",
"@gnosis.pm/safe-apps-web3-react": "0.6.2",
"@gnosis.pm/safe-apps-web3modal": "2.0.0"
},
"changesets": []
}
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -19,14 +19,14 @@
"lerna": "^4.0.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.28.5",
"@typescript-eslint/parser": "^4.28.5",
"@typescript-eslint/eslint-plugin": "^4.29.3",
"@typescript-eslint/parser": "^4.29.3",
"eslint": "7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"jest": "27.0.6",
"eslint-plugin-prettier": "^4.0.0",
"jest": "27.1.0",
"prettier": "^2.3.2",
"ts-jest": "^27.0.4",
"typescript": "^4.3.5"
"ts-jest": "^27.0.5",
"typescript": "^4.4.2"
}
}
6 changes: 3 additions & 3 deletions packages/safe-apps-react-sdk/package.json
Original file line number Diff line number Diff line change
@@ -20,11 +20,11 @@
"react": "16.x.x || 17.x.x"
},
"devDependencies": {
"@types/jest": "^26.0.24",
"@types/jest": "^27.0.1",
"@types/react-dom": "^17.0.9",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-flowtype": "^5.8.2",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-flowtype": "^5.9.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
20 changes: 20 additions & 0 deletions packages/safe-apps-sdk/README.md
Original file line number Diff line number Diff line change
@@ -139,6 +139,26 @@ try {

> Note: `value` accepts a number or a string as a decimal or hex number.
### Signing messages

Because the Safe is a smart contract wallet, it doesn't have a private key that the wallet can use to sign messages. Instead, we have a library to sign messages, and the validation logic follows [EIP-1271 - Standard Signature Validation Method for Contracts](https://eips.ethereum.org/EIPS/eip-1271). Signing a message with the Safe requires sending a Safe transaction that needs to be approved by Safe owners. To dive into the smart contract implementation, you can start with [library tests](https://github.com/gnosis/safe-contracts/blob/main/test/libraries/SignMessageLib.spec.ts) in the safe-contracts repo.

To trigger the transaction to sign a message, you can use `sdk.txs.signMessage()`

```js
const message = "I'm the owner of wallet 0x000000";
const tx = await sdk.txs.signMessage(message);
// { safeTxHash: '0x...' }
```

To validate if the message is signed, use `sdk.safe.isMessageSigned()`

```js
const message = "I'm the owner of wallet 0x000000";
const messageHash = sdk.safe.calculateMessageHash(message);
const messageIsSigned = await sdk.safe.isMessageSigned(messageHash);
```

### Retrieving transaction's status

Once you received safe transaction hash, you might want to get the status of the transaction (was it executed? how many confirmations does it have?):
11 changes: 8 additions & 3 deletions packages/safe-apps-sdk/dist/package.json
Original file line number Diff line number Diff line change
@@ -21,10 +21,15 @@
},
"author": "Gnosis (https://gnosis.io)",
"license": "MIT",
"dependencies": {},
"dependencies": {
"@changesets/cli": "^2.16.0",
"ethers": "^5.4.5"
},
"devDependencies": {
"@types/jest": "^26.0.24",
"@types/node": "^16.4.10",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.2",
"@typescript-eslint/eslint-plugin": "^4.29.3",
"@typescript-eslint/parser": "^4.29.3",
"rimraf": "^3.0.2"
},
"repository": {

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/safe-apps-sdk/dist/src/communication/methods.d.ts
Original file line number Diff line number Diff line change
@@ -3,5 +3,6 @@ export declare enum Methods {
rpcCall = "rpcCall",
getSafeInfo = "getSafeInfo",
getTxBySafeTxHash = "getTxBySafeTxHash",
getSafeBalances = "getSafeBalances"
getSafeBalances = "getSafeBalances",
signMessage = "signMessage"
}
1 change: 1 addition & 0 deletions packages/safe-apps-sdk/dist/src/communication/methods.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion packages/safe-apps-sdk/dist/src/safe/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Communicator, SafeInfo, SafeBalances, GetBalanceParams } from '../types';
import { Communicator, SafeInfo, SafeBalances, GetBalanceParams, BytesLike } from '../types';
declare class Safe {
private readonly communicator;
constructor(communicator: Communicator);
getInfo(): Promise<SafeInfo>;
experimental_getBalances({ currency }?: GetBalanceParams): Promise<SafeBalances>;
calculateMessageHash(message: BytesLike): string;
private check1271Signature;
private check1271SignatureBytes;
isMessageSigned(message: BytesLike, signature?: string): Promise<boolean>;
}
export { Safe };
81 changes: 81 additions & 0 deletions packages/safe-apps-sdk/dist/src/safe/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/safe-apps-sdk/dist/src/safe/index.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/safe-apps-sdk/dist/src/safe/signatures.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ethers } from 'ethers';
declare const MAGIC_VALUE = "0x1626ba7e";
declare const MAGIC_VALUE_BYTES = "0x20c13b0b";
declare const EIP_1271_INTERFACE: ethers.utils.Interface;
declare const EIP_1271_BYTES_INTERFACE: ethers.utils.Interface;
export { EIP_1271_INTERFACE, EIP_1271_BYTES_INTERFACE, MAGIC_VALUE, MAGIC_VALUE_BYTES };
17 changes: 17 additions & 0 deletions packages/safe-apps-sdk/dist/src/safe/signatures.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/safe-apps-sdk/dist/src/safe/signatures.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/safe-apps-sdk/dist/src/txs/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { GatewayTransactionDetails, SendTransactionsParams, Communicator, SendTransactionsResponse } from '../types';
import { GatewayTransactionDetails, SendTransactionsParams, Communicator, SendTransactionsResponse, BytesLike } from '../types';
declare class TXs {
private readonly communicator;
constructor(communicator: Communicator);
getBySafeTxHash(safeTxHash: string): Promise<GatewayTransactionDetails>;
signMessage(message: BytesLike): Promise<SendTransactionsResponse>;
send({ txs, params }: SendTransactionsParams): Promise<SendTransactionsResponse>;
}
export { TXs };
7 changes: 7 additions & 0 deletions packages/safe-apps-sdk/dist/src/txs/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/safe-apps-sdk/dist/src/txs/index.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions packages/safe-apps-sdk/dist/src/types/messaging.d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Methods } from '../communication/methods';
import { SafeInfo } from './sdk';
import { SafeInfo, SendTransactionsResponse } from './sdk';
import { GatewayTransactionDetails, SafeBalances } from './gateway';
export declare type RequestId = string;
export declare type InterfaceMessageEvent = MessageEvent<Response>;
export interface MethodToResponse {
[Methods.sendTransactions]: Record<string, string>;
[Methods.sendTransactions]: SendTransactionsResponse;
[Methods.rpcCall]: unknown;
[Methods.getSafeInfo]: SafeInfo;
[Methods.getTxBySafeTxHash]: GatewayTransactionDetails;
[Methods.getSafeBalances]: SafeBalances[];
[Methods.signMessage]: SendTransactionsResponse;
}
export declare type SDKRequestData<M extends Methods = Methods, P = unknown> = {
id: RequestId;
2 changes: 1 addition & 1 deletion packages/safe-apps-sdk/dist/src/types/rpc.d.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { RPC_CALLS } from '../eth/constants';
export declare type RpcCallNames = keyof typeof RPC_CALLS;
export declare type RPCPayload<P = unknown[]> = {
call: RpcCallNames;
params: P | never[];
params: P | unknown[];
};
export interface Log {
address: string;
5 changes: 5 additions & 0 deletions packages/safe-apps-sdk/dist/src/types/sdk.d.ts
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@ export declare type BaseTransaction = {
value: string;
data: string;
};
export declare type Bytes = ArrayLike<number>;
export declare type BytesLike = Bytes | string;
export declare type GetTxBySafeTxHashParams = {
safeTxHash: string;
};
@@ -16,6 +18,9 @@ export interface SendTransactionsParams {
export declare type GetBalanceParams = {
currency?: string;
};
export declare type SignMessageParams = {
message: BytesLike;
};
export declare type SendTransactionsResponse = {
safeTxHash: string;
};
1 change: 0 additions & 1 deletion packages/safe-apps-sdk/dist/src/utils.js
2 changes: 1 addition & 1 deletion packages/safe-apps-sdk/dist/src/utils.js.map
11 changes: 8 additions & 3 deletions packages/safe-apps-sdk/package.json
Original file line number Diff line number Diff line change
@@ -21,10 +21,15 @@
},
"author": "Gnosis (https://gnosis.io)",
"license": "MIT",
"dependencies": {},
"dependencies": {
"@changesets/cli": "^2.16.0",
"ethers": "^5.4.5"
},
"devDependencies": {
"@types/jest": "^26.0.24",
"@types/node": "^16.4.10",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.2",
"@typescript-eslint/eslint-plugin": "^4.29.3",
"@typescript-eslint/parser": "^4.29.3",
"rimraf": "^3.0.2"
},
"repository": {
1 change: 1 addition & 0 deletions packages/safe-apps-sdk/src/communication/methods.ts
Original file line number Diff line number Diff line change
@@ -4,4 +4,5 @@ export enum Methods {
getSafeInfo = 'getSafeInfo',
getTxBySafeTxHash = 'getTxBySafeTxHash',
getSafeBalances = 'getSafeBalances',
signMessage = 'signMessage',
}
2 changes: 1 addition & 1 deletion packages/safe-apps-sdk/src/eth/eth.test.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { PastLogsOptions, TransactionConfig } from '../types';
describe('Safe Apps SDK Read RPC Requests', () => {
const sdkInstance = new SDK({ whitelistedDomains: [/http:\/\/localhost:3000/] });
/* eslint-disable-next-line */
let spy: jest.SpyInstance<void, [message: any, targetOrigin: string, transfer?: Transferable[] | undefined]>;
let spy: jest.SpyInstance<void, [message: any, options?: PostMessageOptions]>;

beforeEach(() => {
spy = jest.spyOn(window.parent, 'postMessage');
103 changes: 102 additions & 1 deletion packages/safe-apps-sdk/src/safe/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { ethers } from 'ethers';
import { EIP_1271_INTERFACE, EIP_1271_BYTES_INTERFACE, MAGIC_VALUE_BYTES, MAGIC_VALUE } from './signatures';
import { Methods } from '../communication/methods';
import { Communicator, SafeInfo, SafeBalances, GetBalanceParams } from '../types';
import { RPC_CALLS } from '../eth/constants';
import {
Communicator,
SafeInfo,
SafeBalances,
GetBalanceParams,
RPCPayload,
TransactionConfig,
BytesLike,
} from '../types';

class Safe {
private readonly communicator: Communicator;
@@ -28,6 +39,96 @@ class Safe {

return response.data;
}

calculateMessageHash(message: BytesLike): string {
if (typeof message === 'string') {
message = ethers.utils.toUtf8Bytes(message);
}

return ethers.utils.keccak256(message);
}

private async check1271Signature(messageHash: Uint8Array, signature = '0x'): Promise<boolean> {
const safeInfo = await this.getInfo();

const encodedIsValidSignatureCall = EIP_1271_INTERFACE.encodeFunctionData('isValidSignature', [
messageHash,
signature,
]);

const payload = {
call: RPC_CALLS.eth_call,
params: [
{
to: safeInfo.safeAddress,
data: encodedIsValidSignatureCall,
},
'latest',
],
};
try {
const response = await this.communicator.send<Methods.rpcCall, RPCPayload<[TransactionConfig, string]>, string>(
Methods.rpcCall,
payload,
);

return response.data.slice(0, 10).toLowerCase() === MAGIC_VALUE;
} catch (err) {
return false;
}
}

private async check1271SignatureBytes(messageHash: Uint8Array, signature = '0x'): Promise<boolean> {
const safeInfo = await this.getInfo();

const encodedIsValidSignatureCall = EIP_1271_BYTES_INTERFACE.encodeFunctionData('isValidSignature', [
messageHash,
signature,
]);

const payload = {
call: RPC_CALLS.eth_call,
params: [
{
to: safeInfo.safeAddress,
data: encodedIsValidSignatureCall,
},
'latest',
],
};

try {
const response = await this.communicator.send<Methods.rpcCall, RPCPayload<[TransactionConfig, string]>, string>(
Methods.rpcCall,
payload,
);

return response.data.slice(0, 10).toLowerCase() === MAGIC_VALUE_BYTES;
} catch (err) {
return false;
}
}

async isMessageSigned(message: BytesLike, signature = '0x'): Promise<boolean> {
const messageHash = this.calculateMessageHash(message);
const messageHashSigned = await this.isMessageHashSigned(messageHash, signature);

return messageHashSigned;
}

async isMessageHashSigned(messageHash: string, signature = '0x'): Promise<boolean> {
const checks = [this.check1271Signature, this.check1271SignatureBytes];

const msgBytes = ethers.utils.arrayify(messageHash);
for (const check of checks) {
const isValid = await check(msgBytes, signature);
if (isValid) {
return true;
}
}

return false;
}
}

export { Safe };
295 changes: 290 additions & 5 deletions packages/safe-apps-sdk/src/safe/safe.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import SDK from '../index';
import SDK from '../sdk';
import { SafeInfo } from '../types';
import { Methods } from '../communication/methods';

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

describe('Safe Apps SDK safe methods', () => {
const sdkInstance = new SDK();
/* eslint-disable-next-line */
let spy: jest.SpyInstance<void, [message: any, targetOrigin: string, transfer?: Transferable[] | undefined]>;
let postMessageSpy: jest.SpyInstance<void, [message: any, options?: PostMessageOptions]>;

beforeEach(() => {
spy = jest.spyOn(window.parent, 'postMessage');
postMessageSpy = jest.spyOn(window.parent, 'postMessage');
});

afterEach(() => {
@@ -18,18 +21,300 @@ describe('Safe Apps SDK safe methods', () => {
test('Should send a valid message to the interface', () => {
sdkInstance.safe.getInfo();

expect(spy).toHaveBeenCalledWith(
expect(postMessageSpy).toHaveBeenCalledWith(
expect.objectContaining({ method: Methods.getSafeInfo, params: undefined }),
'*',
);
});
});

describe('SDK.safe.calculateMessageHash', () => {
test('Should generate a valid message hash', () => {
const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo');
safeInfoSpy.mockImplementationOnce(
(): Promise<SafeInfo> =>
Promise.resolve({
chainId: 4,
safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
owners: [],
threshold: 1,
}),
);
// to test message/hash I signed a test message on rinkeby
// https://dashboard.tenderly.co/tx/rinkeby/0x9308fb61d9f4282080334e3f35b357fc689e06808b8ad2817536813948e3720d
const message = 'approve rugpull';
const expectedHash = '0xb4fd0d8fd75eea963cec570dd58d8c3f5f93569f5c112e227fa64f275623b4db';
const hash = sdkInstance.safe.calculateMessageHash(message);

expect(hash).toEqual(expectedHash);
});
});

describe('SDK.safe.check1271Signature', () => {
test('Should send a valid message to the interface', async () => {
const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo');
safeInfoSpy.mockImplementationOnce(
(): Promise<SafeInfo> =>
Promise.resolve({
chainId: 4,
safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
owners: [],
threshold: 1,
}),
);
const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000'; // ethers.utils.formatBytes32String('approve rugpull')
// @ts-expect-error method is private but we are testing it
sdkInstance.safe.check1271Signature(message);
await sleep(200);
expect(postMessageSpy).toHaveBeenCalledWith(
expect.objectContaining({
method: Methods.rpcCall,
params: {
call: 'eth_call',
params: [
{
to: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
data: '0x1626ba7e617070726f76652072756770756c6c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000',
},
'latest',
],
},
}),
'*',
);
});

test('Should return true if the message is signed and magic value is returned', async () => {
const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo');
// @ts-expect-error method is private but we are testing it
const rpcCallSpy = jest.spyOn(sdkInstance.safe.communicator, 'send');
safeInfoSpy.mockImplementationOnce(
(): Promise<SafeInfo> =>
Promise.resolve({
chainId: 4,
safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
owners: [],
threshold: 1,
}),
);
rpcCallSpy.mockImplementationOnce(() =>
Promise.resolve({
id: '1',
success: true,
data: '0x1626ba7e00000000000000000000000000000000000000000000000000000000',
}),
);

const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000'; // ethers.utils.formatBytes32String('approve rugpull')
// @ts-expect-error method is private but we are testing it
expect(await sdkInstance.safe.check1271Signature(message)).toEqual(true);
});

test('Should return false if the message isnt signed and underlying call reverts', async () => {
const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo');
// @ts-expect-error method is private but we are testing it
const rpcCallSpy = jest.spyOn(sdkInstance.safe.communicator, 'send');
safeInfoSpy.mockImplementationOnce(
(): Promise<SafeInfo> =>
Promise.resolve({
chainId: 4,
safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
owners: [],
threshold: 1,
}),
);
rpcCallSpy.mockImplementationOnce(() => Promise.reject(new Error('Hash not approved')));

const message = '0x68616c6c6f000000000000000000000000000000000000000000000000000000'; // ethers.utils.formatBytes32String('hallo')
// @ts-expect-error method is private but we are testing it
expect(await sdkInstance.safe.check1271Signature(message)).toEqual(false);
});
});

describe('SDK.safe.check1271SignatureBytes', () => {
test('Should send a valid message to the interface', async () => {
const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo');
safeInfoSpy.mockImplementationOnce(
(): Promise<SafeInfo> =>
Promise.resolve({
chainId: 4,
safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
owners: [],
threshold: 1,
}),
);
const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000'; // ethers.utils.formatBytes32String('approve rugpull')
// @ts-expect-error method is private but we are testing it
sdkInstance.safe.check1271SignatureBytes(message);
await sleep(200);
expect(postMessageSpy).toHaveBeenCalledWith(
expect.objectContaining({
method: Methods.rpcCall,
params: {
call: 'eth_call',
params: [
{
to: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
data: '0x20c13b0b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000020617070726f76652072756770756c6c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
},
'latest',
],
},
}),
'*',
);
});

test('Should return true if the message is signed and magic value is returned', async () => {
const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo');
// @ts-expect-error method is private but we are testing it
const rpcCallSpy = jest.spyOn(sdkInstance.safe.communicator, 'send');
safeInfoSpy.mockImplementationOnce(
(): Promise<SafeInfo> =>
Promise.resolve({
chainId: 4,
safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
owners: [],
threshold: 1,
}),
);
rpcCallSpy.mockImplementationOnce(() =>
Promise.resolve({
id: '1',
success: true,
data: '0x20c13b0b00000000000000000000000000000000000000000000000000000000',
}),
);

const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000'; // ethers.utils.formatBytes32String('approve rugpull')
// @ts-expect-error method is private but we are testing it
expect(await sdkInstance.safe.check1271SignatureBytes(message)).toEqual(true);
});

test('Should return false if the message isnt signed and underlying call reverts', async () => {
const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo');
// @ts-expect-error method is private but we are testing it
const rpcCallSpy = jest.spyOn(sdkInstance.safe.communicator, 'send');
safeInfoSpy.mockImplementationOnce(
(): Promise<SafeInfo> =>
Promise.resolve({
chainId: 4,
safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
owners: [],
threshold: 1,
}),
);
rpcCallSpy.mockImplementationOnce(() => Promise.reject(new Error('Hash not approved')));

const message = '0x68616c6c6f000000000000000000000000000000000000000000000000000000'; // ethers.utils.formatBytes32String('hallo')
// @ts-expect-error method is private but we are testing it
expect(await sdkInstance.safe.check1271SignatureBytes(message)).toEqual(false);
});
});

describe('SDK.safe.isMessageSigned', () => {
test('Should call SDK.safe.isMessageHashSigned with a hash of the message', () => {
const isMessageHashSignedSpy = jest.spyOn(sdkInstance.safe, 'isMessageHashSigned');

// ethers.utils.formatBytes32String('approve rugpull')
const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000';
const expectedHash = '0xdbdc6aad7abc8ee87185f03191abf6b43ea347f0e669f829ab1af0ee59bae246';

sdkInstance.safe.isMessageSigned(message);
expect(isMessageHashSignedSpy).toHaveBeenCalledWith(expectedHash, '0x');
});
});

describe('SDK.safe.isMessageHashSigned', () => {
test('Should return true if check1271Signature return true', async () => {
const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo');
// @ts-expect-error method is private but we are testing it
const check1271SignatureSpy = jest.spyOn(sdkInstance.safe, 'check1271Signature');
safeInfoSpy.mockImplementationOnce(
(): Promise<SafeInfo> =>
Promise.resolve({
chainId: 4,
safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
owners: [],
threshold: 1,
}),
);
// @ts-expect-error ts fails to infer the return type because of a private method
check1271SignatureSpy.mockImplementationOnce(() => Promise.resolve(true));

// ethers.utils.formatBytes32String('approve rugpull')
const message = sdkInstance.safe.calculateMessageHash(
'0x617070726f76652072756770756c6c0000000000000000000000000000000000',
);
const signed = await sdkInstance.safe.isMessageHashSigned(message);

expect(signed).toEqual(true);
});

test('Should return true if check1271SignatureBytes return true', async () => {
const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo');
// @ts-expect-error method is private but we are testing it
const check1271SignatureSpy = jest.spyOn(sdkInstance.safe, 'check1271Signature');
// @ts-expect-error method is private but we are testing it
const check1271SignatureBytesSpy = jest.spyOn(sdkInstance.safe, 'check1271SignatureBytes');
safeInfoSpy.mockImplementationOnce(
(): Promise<SafeInfo> =>
Promise.resolve({
chainId: 4,
safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
owners: [],
threshold: 1,
}),
);
// @ts-expect-error ts fails to infer the return type because of a private method
check1271SignatureSpy.mockImplementationOnce(() => Promise.resolve(false));
// @ts-expect-error ts fails to infer the return type because of a private method
check1271SignatureBytesSpy.mockImplementationOnce(() => Promise.resolve(true));

// ethers.utils.formatBytes32String('approve rugpull')
const message = sdkInstance.safe.calculateMessageHash(
'0x617070726f76652072756770756c6c0000000000000000000000000000000000',
);
const signed = await sdkInstance.safe.isMessageHashSigned(message);

expect(signed).toEqual(true);
});

test('Should return false if all of the underlying checks return false', async () => {
const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo');
// @ts-expect-error method is private but we are testing it
const check1271SignatureSpy = jest.spyOn(sdkInstance.safe, 'check1271Signature');
// @ts-expect-error method is private but we are testing it
const check1271SignatureBytesSpy = jest.spyOn(sdkInstance.safe, 'check1271SignatureBytes');
safeInfoSpy.mockImplementationOnce(
(): Promise<SafeInfo> =>
Promise.resolve({
chainId: 4,
safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
owners: [],
threshold: 1,
}),
);
// @ts-expect-error ts fails to infer the return type because of a private method
check1271SignatureSpy.mockImplementationOnce(() => Promise.resolve(false));
// @ts-expect-error ts fails to infer the return type because of a private method
check1271SignatureBytesSpy.mockImplementationOnce(() => Promise.resolve(false));

// ethers.utils.formatBytes32String('approve rugpull')
const message = sdkInstance.safe.calculateMessageHash(
'0x617070726f76652072756770756c6c0000000000000000000000000000000000',
);
const signed = await sdkInstance.safe.isMessageHashSigned(message);

expect(signed).toEqual(false);
});
});

describe('SDK.safe.getBalances', () => {
test('Should send a valid message to the interface', () => {
sdkInstance.safe.experimental_getBalances({ currency: 'eur' });

expect(spy).toHaveBeenCalledWith(
expect(postMessageSpy).toHaveBeenCalledWith(
expect.objectContaining({ method: Methods.getSafeBalances, params: { currency: 'eur' } }),
'*',
);
13 changes: 13 additions & 0 deletions packages/safe-apps-sdk/src/safe/signatures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ethers } from 'ethers';

const MAGIC_VALUE = '0x1626ba7e';
const MAGIC_VALUE_BYTES = '0x20c13b0b';

const EIP_1271_INTERFACE = new ethers.utils.Interface([
'function isValidSignature(bytes32 _dataHash, bytes calldata _signature) external view',
]);
const EIP_1271_BYTES_INTERFACE = new ethers.utils.Interface([
'function isValidSignature(bytes calldata _data, bytes calldata _signature) public view',
]);

export { EIP_1271_INTERFACE, EIP_1271_BYTES_INTERFACE, MAGIC_VALUE, MAGIC_VALUE_BYTES };
6 changes: 3 additions & 3 deletions packages/safe-apps-sdk/src/sdk.ts
Original file line number Diff line number Diff line change
@@ -11,9 +11,9 @@ export type Opts = {

class SafeAppsSDK {
private readonly communicator: Communicator;
public readonly eth;
public readonly txs;
public readonly safe;
public readonly eth: Eth;
public readonly txs: TXs;
public readonly safe: Safe;

constructor(opts: Opts = {}) {
const { whitelistedDomains = null, debug = false } = opts;
15 changes: 15 additions & 0 deletions packages/safe-apps-sdk/src/txs/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Methods } from '../communication/methods';
import {
GatewayTransactionDetails,
SignMessageParams,
SendTransactionsParams,
GetTxBySafeTxHashParams,
Communicator,
SendTransactionsResponse,
BytesLike,
} from '../types';

class TXs {
@@ -28,6 +30,19 @@ class TXs {
return response.data;
}

async signMessage(message: BytesLike): Promise<SendTransactionsResponse> {
const messagePayload = {
message,
};

const response = await this.communicator.send<Methods.signMessage, SignMessageParams, SendTransactionsResponse>(
Methods.signMessage,
messagePayload,
);

return response.data;
}

async send({ txs, params }: SendTransactionsParams): Promise<SendTransactionsResponse> {
if (!txs || !txs.length) {
throw new Error('No transactions were passed');
14 changes: 13 additions & 1 deletion packages/safe-apps-sdk/src/txs/txs.test.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { Methods } from '../communication/methods';
describe('Safe Apps SDK transaction methods', () => {
const sdkInstance = new SDK();
/* eslint-disable-next-line */
let spy: jest.SpyInstance<void, [message: any, targetOrigin: string, transfer?: Transferable[] | undefined]>;
let spy: jest.SpyInstance<void, [message: any, options?: PostMessageOptions]>;

beforeEach(() => {
spy = jest.spyOn(window.parent, 'postMessage');
@@ -56,4 +56,16 @@ describe('Safe Apps SDK transaction methods', () => {
);
});
});

describe('SDK.txs.signMessage', () => {
test('Should include params with non-hashed message to the message body', () => {
const message = 'approve rugpull';

sdkInstance.txs.signMessage(message);
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({ method: Methods.signMessage, params: { message } }),
'*',
);
});
});
});
5 changes: 3 additions & 2 deletions packages/safe-apps-sdk/src/types/messaging.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { Methods } from '../communication/methods';
import { SafeInfo } from './sdk';
import { SafeInfo, SendTransactionsResponse } from './sdk';
import { GatewayTransactionDetails, SafeBalances } from './gateway';

export type RequestId = string;

export type InterfaceMessageEvent = MessageEvent<Response>;

export interface MethodToResponse {
[Methods.sendTransactions]: Record<string, string>;
[Methods.sendTransactions]: SendTransactionsResponse;
[Methods.rpcCall]: unknown;
[Methods.getSafeInfo]: SafeInfo;
[Methods.getTxBySafeTxHash]: GatewayTransactionDetails;
[Methods.getSafeBalances]: SafeBalances[];
[Methods.signMessage]: SendTransactionsResponse;
}

export type SDKRequestData<M extends Methods = Methods, P = unknown> = {
2 changes: 1 addition & 1 deletion packages/safe-apps-sdk/src/types/rpc.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ export type RpcCallNames = keyof typeof RPC_CALLS;

export type RPCPayload<P = unknown[]> = {
call: RpcCallNames;
params: P | never[];
params: P | unknown[];
};

export interface Log {
8 changes: 8 additions & 0 deletions packages/safe-apps-sdk/src/types/sdk.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,10 @@ export type BaseTransaction = {
data: string;
};

export type Bytes = ArrayLike<number>;

export type BytesLike = Bytes | string;

export type GetTxBySafeTxHashParams = {
safeTxHash: string;
};
@@ -19,6 +23,10 @@ export interface SendTransactionsParams {

export type GetBalanceParams = { currency?: string };

export type SignMessageParams = {
message: BytesLike;
};

export type SendTransactionsResponse = {
safeTxHash: string;
};
2 changes: 1 addition & 1 deletion packages/safe-apps-sdk/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import pkg from '../package.json';
// console.log({ pkg });

// Slice is needed for versions like '1.0.0-beta.0'
export const getSDKVersion = (): string => pkg.version.slice(0, 5);
4 changes: 2 additions & 2 deletions packages/safe-apps-web3-react/package.json
Original file line number Diff line number Diff line change
@@ -30,8 +30,8 @@
"react": "16.x.x || 17.x.x"
},
"devDependencies": {
"@types/jest": "^26.0.24",
"@types/node": "^16.4.10",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.2",
"@web3-react/core": "6.1.9",
"react": "17.0.2",
"rimraf": "^3.0.2"
1,787 changes: 1,107 additions & 680 deletions yarn.lock

Large diffs are not rendered by default.

0 comments on commit 035159a

Please sign in to comment.