safe-apps-sdk
Version:
SDK developed to integrate third-party apps with Safe app.
451 lines (397 loc) • 18 kB
text/typescript
import SDK from '../sdk';
import { SafeInfo } from '../types';
import { Methods } from '../communication/methods';
import { PostMessageOptions } from '../types';
import { PermissionsError } from '../types/permissions';
import { Wallet } from '../wallet';
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
describe('Safe Apps SDK safe methods', () => {
const sdkInstance = new SDK();
let postMessageSpy: jest.SpyInstance<void, [message: any, options?: PostMessageOptions]>;
beforeEach(() => {
postMessageSpy = jest.spyOn(window.parent, 'postMessage');
});
afterEach(() => {
jest.clearAllMocks();
});
describe('SDK.safe.getInfo', () => {
test('Should send a valid message to the interface', () => {
sdkInstance.safe.getInfo();
expect(postMessageSpy).toHaveBeenCalledWith(
expect.objectContaining({ method: Methods.getSafeInfo, params: undefined }),
'*',
);
});
});
describe('SDK.safe.calculateMessageHash', () => {
test('Should generate correct EIP-191 message hash', () => {
const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo');
safeInfoSpy.mockImplementationOnce(
(): Promise<SafeInfo> =>
Promise.resolve({
chainId: 4,
safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
owners: [],
threshold: 1,
isReadOnly: false,
}),
);
// to test message/hash I signed a test message on rinkeby
// https://dashboard.tenderly.co/tx/rinkeby/0x9308fb61d9f4282080334e3f35b357fc689e06808b8ad2817536813948e3720d
const message = 'approve rugpull';
const expectedHash = '0xe32c44147e358bc973757518210c3baec92de66115c513ea1146d61ad4fd93af';
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,
isReadOnly: false,
}),
);
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,
isReadOnly: false,
}),
);
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,
isReadOnly: false,
}),
);
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,
isReadOnly: false,
}),
);
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,
isReadOnly: false,
}),
);
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,
isReadOnly: false,
}),
);
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 = '0xaae9257b8ff1c926ac3cdf36923661de4e81bf934e38958beeede3519aa18b08';
sdkInstance.safe.isMessageSigned(message);
expect(isMessageHashSignedSpy).toHaveBeenCalledWith(expectedHash, '0x');
});
});
describe('SDK.safe.isMessageHashSigned', () => {
test('Should send call messages to check the message the interface', async () => {
const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo');
safeInfoSpy.mockImplementation(
(): Promise<SafeInfo> =>
Promise.resolve({
chainId: 4,
safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
owners: [],
threshold: 1,
isReadOnly: false,
}),
);
const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000'; // ethers.utils.formatBytes32String('approve rugpull')
sdkInstance.safe.isMessageHashSigned(message);
await sleep(200);
// calling first check1271Signature method
expect(postMessageSpy).toHaveBeenCalledWith(
expect.objectContaining({
method: Methods.rpcCall,
params: {
call: 'eth_call',
params: [
{
to: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190',
data: '0x1626ba7e617070726f76652072756770756c6c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000',
},
'latest',
],
},
}),
'*',
);
});
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,
isReadOnly: false,
}),
);
// @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,
isReadOnly: false,
}),
);
// @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,
isReadOnly: false,
}),
);
// @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(postMessageSpy).toHaveBeenCalledWith(
expect.objectContaining({ method: Methods.getSafeBalances, params: { currency: 'eur' } }),
'*',
);
});
});
describe('SDK.safe.getChainInfo', () => {
test('Should send a valid message to the interface', () => {
sdkInstance.safe.getChainInfo();
expect(postMessageSpy).toHaveBeenCalledWith(expect.objectContaining({ method: Methods.getChainInfo }), '*');
});
});
describe('SDK.safe.getOffChainSignature', () => {
test('Should request the off-chain signature from the interface', () => {
sdkInstance.safe.getOffChainSignature('0x123');
expect(postMessageSpy).toHaveBeenCalledWith(
expect.objectContaining({ method: Methods.getOffChainSignature, params: '0x123' }),
'*',
);
});
});
describe('SDK.safe.requestAddressBook', () => {
const wrongPermissions = [
{
date: 1657713994059,
invoker: 'https://test.eth',
parentCapability: 'wrongPermission',
},
];
const correctPermissions = [
{
date: 1657713994059,
invoker: 'https://test.eth',
parentCapability: 'requestAddressBook',
},
];
test('Should call requestAddressBook when the permissions are ok', async () => {
jest
.spyOn(Wallet.prototype, 'getPermissions')
.mockImplementationOnce(jest.fn().mockResolvedValue(correctPermissions));
sdkInstance.safe.requestAddressBook();
await sleep(200);
expect(postMessageSpy).toHaveBeenCalledWith(expect.objectContaining({ method: Methods.requestAddressBook }), '*');
});
test('Should call requestPermissions when the current permissions are not ok', async () => {
jest
.spyOn(Wallet.prototype, 'getPermissions')
.mockImplementationOnce(jest.fn().mockResolvedValue(wrongPermissions));
const requestPermissionsSpy = jest.spyOn(Wallet.prototype, 'requestPermissions');
sdkInstance.safe.requestAddressBook();
await sleep(200);
expect(requestPermissionsSpy).toHaveBeenCalledWith([{ requestAddressBook: {} }]);
});
test('Should throw PermissionError when the permissions are not ok', async () => {
jest
.spyOn(Wallet.prototype, 'getPermissions')
.mockImplementationOnce(jest.fn().mockResolvedValue(wrongPermissions));
jest
.spyOn(Wallet.prototype, 'requestPermissions')
.mockImplementationOnce(jest.fn().mockResolvedValue(wrongPermissions));
try {
await sdkInstance.safe.requestAddressBook();
} catch (e) {
expect(e).toBeInstanceOf(PermissionsError);
}
});
});
});