From ee5cb361aa74fbb78912acd375de208531a8fbf5 Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Wed, 12 Jun 2024 11:54:17 +0200 Subject: [PATCH] feat: getInfo endpoint returns extended Safe Info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api-kit’s getSafeInfo endpoint returns way more information than our communicator’s getInfo endpoint. This causes some dapps to use the getInfo endpoint for just the readOnly flag (when running in an iframe) and then to do api-kit getSafeInfo for the nonce and other props. By returning nearly the same information from the getInfo endpoint we make developers live easier. . --- packages/safe-apps-sdk/src/safe/index.ts | 6 +- packages/safe-apps-sdk/src/safe/safe.test.ts | 148 ++++--------------- packages/safe-apps-sdk/src/txs/txs.test.ts | 14 +- packages/safe-apps-sdk/src/types/sdk.ts | 9 ++ 4 files changed, 42 insertions(+), 135 deletions(-) diff --git a/packages/safe-apps-sdk/src/safe/index.ts b/packages/safe-apps-sdk/src/safe/index.ts index dad67a37..11df45e2 100644 --- a/packages/safe-apps-sdk/src/safe/index.ts +++ b/packages/safe-apps-sdk/src/safe/index.ts @@ -4,7 +4,6 @@ import { Methods } from '../communication/methods.js'; import { RPC_CALLS } from '../eth/constants.js'; import { Communicator, - SafeInfo, ChainInfo, SafeBalances, GetBalanceParams, @@ -14,6 +13,7 @@ import { AddressBookItem, isObjectEIP712TypedData, EIP712TypedData, + SafeInfoExtended, } from '../types/index.js'; import requirePermission from '../decorators/requirePermissions.js'; @@ -33,8 +33,8 @@ class Safe { return response.data; } - async getInfo(): Promise { - const response = await this.communicator.send( + async getInfo(): Promise { + const response = await this.communicator.send( Methods.getSafeInfo, undefined, ); diff --git a/packages/safe-apps-sdk/src/safe/safe.test.ts b/packages/safe-apps-sdk/src/safe/safe.test.ts index daf4513f..ce4a0d76 100644 --- a/packages/safe-apps-sdk/src/safe/safe.test.ts +++ b/packages/safe-apps-sdk/src/safe/safe.test.ts @@ -1,5 +1,5 @@ import SDK from '../sdk.js'; -import { SafeInfo } from '../types/index.js'; +import { SafeInfoExtended } from '../types/index.js'; import { Methods } from '../communication/methods.js'; import { PostMessageOptions } from '../types/index.js'; import { PermissionsError } from '../types/permissions.js'; @@ -7,6 +7,20 @@ import { Wallet } from '../wallet/index.js'; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +export const ExtendedSafeInfo: SafeInfoExtended = { + chainId: 4, + safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', + nonce: 1, + implementation: '0x3E5c63644E683549055b9Be8653de26E0B4CD36E', + modules: null, + fallbackHandler: null, + version: '1.3.0', + guard: null, + owners: [], + threshold: 1, + isReadOnly: false, +}; + describe('Safe Apps SDK safe methods', () => { const sdkInstance = new SDK(); let postMessageSpy: jest.SpyInstance; @@ -33,16 +47,7 @@ describe('Safe Apps SDK safe methods', () => { describe('SDK.safe.calculateMessageHash', () => { test('Should generate correct EIP-191 message hash', () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); - safeInfoSpy.mockImplementationOnce( - (): Promise => - Promise.resolve({ - chainId: 4, - safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', - owners: [], - threshold: 1, - isReadOnly: false, - }), - ); + safeInfoSpy.mockImplementationOnce((): Promise => Promise.resolve(ExtendedSafeInfo)); // to test message/hash I signed a test message on rinkeby // https://dashboard.tenderly.co/tx/rinkeby/0x9308fb61d9f4282080334e3f35b357fc689e06808b8ad2817536813948e3720d const message = 'approve rugpull'; @@ -56,16 +61,7 @@ describe('Safe Apps SDK safe methods', () => { describe('SDK.safe.calculateTypedMessageHash', () => { test('Should generate correct EIP-712 message hash', () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); - safeInfoSpy.mockImplementationOnce( - (): Promise => - Promise.resolve({ - chainId: 4, - safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', - owners: [], - threshold: 1, - isReadOnly: false, - }), - ); + safeInfoSpy.mockImplementationOnce((): Promise => Promise.resolve(ExtendedSafeInfo)); const typedMessage = { domain: { @@ -107,16 +103,7 @@ describe('Safe Apps SDK safe methods', () => { describe('SDK.safe.check1271Signature', () => { test('Should send a valid message to the interface', async () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); - safeInfoSpy.mockImplementationOnce( - (): Promise => - Promise.resolve({ - chainId: 4, - safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', - owners: [], - threshold: 1, - isReadOnly: false, - }), - ); + safeInfoSpy.mockImplementationOnce((): Promise => Promise.resolve(ExtendedSafeInfo)); const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000'; // stringToHex('approve rugpull', { size: 32 }) // @ts-expect-error method is private but we are testing it sdkInstance.safe.check1271Signature(message); @@ -143,16 +130,7 @@ describe('Safe Apps SDK safe methods', () => { 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 => - Promise.resolve({ - chainId: 4, - safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', - owners: [], - threshold: 1, - isReadOnly: false, - }), - ); + safeInfoSpy.mockImplementationOnce((): Promise => Promise.resolve(ExtendedSafeInfo)); rpcCallSpy.mockImplementationOnce(() => Promise.resolve({ id: '1', @@ -170,16 +148,7 @@ describe('Safe Apps SDK safe methods', () => { 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 => - Promise.resolve({ - chainId: 4, - safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', - owners: [], - threshold: 1, - isReadOnly: false, - }), - ); + safeInfoSpy.mockImplementationOnce((): Promise => Promise.resolve(ExtendedSafeInfo)); rpcCallSpy.mockImplementationOnce(() => Promise.reject(new Error('Hash not approved'))); const message = '0x68616c6c6f000000000000000000000000000000000000000000000000000000'; // stringToHex('hallo') @@ -191,16 +160,7 @@ describe('Safe Apps SDK safe methods', () => { describe('SDK.safe.check1271SignatureBytes', () => { test('Should send a valid message to the interface', async () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); - safeInfoSpy.mockImplementationOnce( - (): Promise => - Promise.resolve({ - chainId: 4, - safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', - owners: [], - threshold: 1, - isReadOnly: false, - }), - ); + safeInfoSpy.mockImplementationOnce((): Promise => Promise.resolve(ExtendedSafeInfo)); const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000'; // stringToHex('approve rugpull', { size: 32 }) // @ts-expect-error method is private but we are testing it sdkInstance.safe.check1271SignatureBytes(message); @@ -227,16 +187,7 @@ describe('Safe Apps SDK safe methods', () => { 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 => - Promise.resolve({ - chainId: 4, - safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', - owners: [], - threshold: 1, - isReadOnly: false, - }), - ); + safeInfoSpy.mockImplementationOnce((): Promise => Promise.resolve(ExtendedSafeInfo)); rpcCallSpy.mockImplementationOnce(() => Promise.resolve({ id: '1', @@ -254,16 +205,7 @@ describe('Safe Apps SDK safe methods', () => { 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 => - Promise.resolve({ - chainId: 4, - safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', - owners: [], - threshold: 1, - isReadOnly: false, - }), - ); + safeInfoSpy.mockImplementationOnce((): Promise => Promise.resolve(ExtendedSafeInfo)); rpcCallSpy.mockImplementationOnce(() => Promise.reject(new Error('Hash not approved'))); const message = '0x68616c6c6f000000000000000000000000000000000000000000000000000000'; // stringToHex('hallo') @@ -288,16 +230,7 @@ describe('Safe Apps SDK safe methods', () => { 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 => - Promise.resolve({ - chainId: 4, - safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', - owners: [], - threshold: 1, - isReadOnly: false, - }), - ); + safeInfoSpy.mockImplementationOnce((): Promise => Promise.resolve(ExtendedSafeInfo)); const message = '0x617070726f76652072756770756c6c0000000000000000000000000000000000'; // stringToHex('approve rugpull', { size: 32 }) @@ -326,16 +259,7 @@ describe('Safe Apps SDK safe methods', () => { 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 => - Promise.resolve({ - chainId: 4, - safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', - owners: [], - threshold: 1, - isReadOnly: false, - }), - ); + safeInfoSpy.mockImplementationOnce((): Promise => Promise.resolve(ExtendedSafeInfo)); // @ts-expect-error ts fails to infer the return type because of a private method check1271SignatureSpy.mockImplementationOnce(() => Promise.resolve(true)); @@ -354,16 +278,7 @@ describe('Safe Apps SDK safe methods', () => { 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 => - Promise.resolve({ - chainId: 4, - safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', - owners: [], - threshold: 1, - isReadOnly: false, - }), - ); + safeInfoSpy.mockImplementationOnce((): Promise => Promise.resolve(ExtendedSafeInfo)); // @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 @@ -384,16 +299,7 @@ describe('Safe Apps SDK safe methods', () => { 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 => - Promise.resolve({ - chainId: 4, - safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', - owners: [], - threshold: 1, - isReadOnly: false, - }), - ); + safeInfoSpy.mockImplementationOnce((): Promise => Promise.resolve(ExtendedSafeInfo)); // @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 diff --git a/packages/safe-apps-sdk/src/txs/txs.test.ts b/packages/safe-apps-sdk/src/txs/txs.test.ts index 5eaa4f6b..72dabd2e 100644 --- a/packages/safe-apps-sdk/src/txs/txs.test.ts +++ b/packages/safe-apps-sdk/src/txs/txs.test.ts @@ -1,6 +1,7 @@ -import SDK, { SafeInfo } from '../index.js'; +import SDK, { SafeInfoExtended } from '../index.js'; import { Methods } from '../communication/methods.js'; import { PostMessageOptions } from '../types/index.js'; +import { ExtendedSafeInfo } from '../safe/safe.test'; describe('Safe Apps SDK transaction methods', () => { const sdkInstance = new SDK(); @@ -70,16 +71,7 @@ describe('Safe Apps SDK transaction methods', () => { test('Should include params with non-hashed message to the typed message body', async () => { const safeInfoSpy = jest.spyOn(sdkInstance.safe, 'getInfo'); - safeInfoSpy.mockImplementationOnce( - (): Promise => - Promise.resolve({ - chainId: 4, - safeAddress: '0x9C6FEA0B2eAc5b6D8bBB6C30401D42aA95398190', - owners: [], - threshold: 1, - isReadOnly: false, - }), - ); + safeInfoSpy.mockImplementationOnce((): Promise => Promise.resolve(ExtendedSafeInfo)); const domain = { name: 'Ether Mail', diff --git a/packages/safe-apps-sdk/src/types/sdk.ts b/packages/safe-apps-sdk/src/types/sdk.ts index f8571c11..67cc42a5 100644 --- a/packages/safe-apps-sdk/src/types/sdk.ts +++ b/packages/safe-apps-sdk/src/types/sdk.ts @@ -76,6 +76,15 @@ export type SafeInfo = { isReadOnly: boolean; }; +export type SafeInfoExtended = SafeInfo & { + nonce: number; + implementation: string; + modules: string[] | null; + fallbackHandler: string | null; + guard: string | null; + version: string | null; +}; + export type EnvironmentInfo = { origin: string; };