Skip to content

Commit

Permalink
feat(protocol kit): Add get modules paginated (#774)
Browse files Browse the repository at this point in the history
* Add modules paginated to safe

* Add to main module

* Add method to contract class

* Add tests

* Fix v1.0.0 tests

* Fix naming

* Keep same behavior for version 1.0.0

* Fix documentation

* Fix parameter name
  • Loading branch information
leonardotc authored Apr 18, 2024
1 parent ce7d14b commit 71fa34a
Show file tree
Hide file tree
Showing 16 changed files with 196 additions and 5 deletions.
11 changes: 11 additions & 0 deletions packages/protocol-kit/src/Safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,17 @@ class Safe {
return this.#moduleManager.getModules()
}

/**
* Returns the list of addresses of all the enabled Safe modules. The list will start on the next position address in relation to start.
*
* @param start - The address to be "offsetted" from the list, should be SENTINEL_ADDRESS otherwise.
* @param pageSize - The size of the page. It will be the max length of the returning array. Must be greater then 0.
* @returns The list of addresses of all the enabled Safe modules
*/
async getModulesPaginated(start: string, pageSize: number = 10): Promise<string[]> {
return this.#moduleManager.getModulesPaginated(start, pageSize)
}

/**
* Checks if a specific Safe module is enabled for the current Safe.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ abstract class SafeContractEthers implements SafeContract {

abstract getModules(): Promise<string[]>

abstract getModulesPaginated(start: string, pageSize: number): Promise<string[]>

abstract isModuleEnabled(moduleAddress: string): Promise<boolean>

async isValidTransaction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { EMPTY_DATA, ZERO_ADDRESS } from '@safe-global/protocol-kit/adapters/eth
import { Gnosis_safe as Safe } from '@safe-global/protocol-kit/typechain/src/ethers-v6/v1.0.0/Gnosis_safe'
import { SafeSetupConfig } from '@safe-global/safe-core-sdk-types'
import SafeContractEthers from '../SafeContractEthers'
import { SENTINEL_ADDRESS } from '@safe-global/protocol-kit/utils/constants'

class SafeContract_V1_0_0_Ethers extends SafeContractEthers {
constructor(public contract: Safe) {
Expand Down Expand Up @@ -54,6 +55,18 @@ class SafeContract_V1_0_0_Ethers extends SafeContractEthers {
return this.contract.getModules()
}

async getModulesPaginated(start: string, pageSize: number): Promise<string[]> {
if (pageSize <= 0) throw new Error('Invalid page size for fetching paginated modules')

const array = await this.getModules()
if (start === SENTINEL_ADDRESS) {
return array.slice(0, pageSize)
} else {
const moduleIndex = array.findIndex((module: string) => sameString(module, start))
return moduleIndex === -1 ? [] : array.slice(moduleIndex + 1, pageSize)
}
}

async isModuleEnabled(moduleAddress: string): Promise<boolean> {
const modules = await this.getModules()
const isModuleEnabled = modules.some((enabledModuleAddress: string) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ class SafeContract_V1_1_1_Ethers extends SafeContractEthers {
return this.contract.getModules()
}

async getModulesPaginated(start: string, pageSize: number): Promise<string[]> {
const { array } = await this.contract.getModulesPaginated(start, pageSize)
return array
}

async isModuleEnabled(moduleAddress: string): Promise<boolean> {
const modules = await this.getModules()
const isModuleEnabled = modules.some((enabledModuleAddress: string) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ class SafeContract_V1_2_0_Ethers extends SafeContractEthers {
return this.contract.getModules()
}

async getModulesPaginated(start: string, pageSize: number): Promise<string[]> {
const { array } = await this.contract.getModulesPaginated(start, pageSize)
return array
}

async isModuleEnabled(moduleAddress: string): Promise<boolean> {
return this.contract.isModuleEnabled(moduleAddress)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ class SafeContract_V1_3_0_Ethers extends SafeContractEthers {
}

async getModules(): Promise<string[]> {
const { array } = await this.contract.getModulesPaginated(SENTINEL_ADDRESS, 10)
return await this.getModulesPaginated(SENTINEL_ADDRESS, 10)
}

async getModulesPaginated(start: string, pageSize: number): Promise<string[]> {
const { array } = await this.contract.getModulesPaginated(start, pageSize)
return array
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ class SafeContract_V1_4_1_Ethers extends SafeContractEthers {
}

async getModules(): Promise<string[]> {
const { array } = await this.contract.getModulesPaginated(SENTINEL_ADDRESS, 10)
return await this.getModulesPaginated(SENTINEL_ADDRESS, 10)
}

async getModulesPaginated(start: string, pageSize: number): Promise<string[]> {
const { array } = await this.contract.getModulesPaginated(start, pageSize)
return array
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ abstract class SafeContractWeb3 implements SafeContract {

abstract getModules(): Promise<string[]>

abstract getModulesPaginated(start: string, pageSize: number): Promise<string[]>

abstract isModuleEnabled(moduleAddress: string): Promise<boolean>

async isValidTransaction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import {
Web3TransactionResult
} from '@safe-global/protocol-kit/adapters/web3/types'
import { sameString, toTxResult } from '@safe-global/protocol-kit/adapters/web3/utils'
import { EMPTY_DATA, ZERO_ADDRESS } from '@safe-global/protocol-kit/adapters/web3/utils/constants'
import {
EMPTY_DATA,
ZERO_ADDRESS,
SENTINEL_ADDRESS
} from '@safe-global/protocol-kit/adapters/web3/utils/constants'
import { Gnosis_safe as Safe } from '@safe-global/protocol-kit/typechain/src/web3-v1/v1.0.0/Gnosis_safe'
import { SafeSetupConfig } from '@safe-global/safe-core-sdk-types'
import SafeContractWeb3 from '../SafeContractWeb3'
Expand Down Expand Up @@ -47,6 +51,18 @@ class SafeContract_V1_0_0_Web3 extends SafeContractWeb3 {
return this.contract.methods.getModules().call()
}

async getModulesPaginated(start: string, pageSize: number): Promise<string[]> {
if (pageSize <= 0) throw new Error('Invalid page size for fetching paginated modules')

const array = await this.getModules()
if (start === SENTINEL_ADDRESS) {
return array.slice(0, pageSize)
} else {
const moduleIndex = array.findIndex((module: string) => sameString(module, start))
return moduleIndex === -1 ? [] : array.slice(moduleIndex + 1, pageSize)
}
}

async isModuleEnabled(moduleAddress: string): Promise<boolean> {
const modules = await this.getModules()
const isModuleEnabled = modules.some((enabledModuleAddress: string) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ class SafeContract_V1_1_1_Web3 extends SafeContractWeb3 {
return this.contract.methods.getModules().call()
}

async getModulesPaginated(start: string, pageSize: number): Promise<string[]> {
const { array } = await this.contract.methods.getModulesPaginated(start, pageSize).call()
return array
}

async isModuleEnabled(moduleAddress: string): Promise<boolean> {
const modules = await this.getModules()
const isModuleEnabled = modules.some((enabledModuleAddress: string) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ class SafeContract_V1_2_0_Web3 extends SafeContractWeb3 {
return this.contract.methods.getModules().call()
}

async getModulesPaginated(start: string, pageSize: number): Promise<string[]> {
const { array } = await this.contract.methods.getModulesPaginated(start, pageSize).call()
return array
}

async isModuleEnabled(moduleAddress: string): Promise<boolean> {
return this.contract.methods.isModuleEnabled(moduleAddress).call()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ class SafeContract_V1_3_0_Web3 extends SafeContractWeb3 {
}

async getModules(): Promise<string[]> {
const { array } = await this.contract.methods.getModulesPaginated(SENTINEL_ADDRESS, 10).call()
return await this.getModulesPaginated(SENTINEL_ADDRESS, 10)
}

async getModulesPaginated(start: string, pageSize: number): Promise<string[]> {
const { array } = await this.contract.methods.getModulesPaginated(start, pageSize).call()
return array
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ class SafeContract_V1_4_1_Web3 extends SafeContractWeb3 {
}

async getModules(): Promise<string[]> {
const { array } = await this.contract.methods.getModulesPaginated(SENTINEL_ADDRESS, 10).call()
return await this.getModulesPaginated(SENTINEL_ADDRESS, 10)
}

async getModulesPaginated(start: string, pageSize: number): Promise<string[]> {
const { array } = await this.contract.methods.getModulesPaginated(start, pageSize).call()
return array
}

Expand Down
7 changes: 7 additions & 0 deletions packages/protocol-kit/src/managers/moduleManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ class ModuleManager {
return this.#safeContract.getModules()
}

async getModulesPaginated(start: string, pageSize: number): Promise<string[]> {
if (!this.#safeContract) {
throw new Error('Safe is not deployed')
}
return this.#safeContract.getModulesPaginated(start, pageSize)
}

async isModuleEnabled(moduleAddress: string): Promise<boolean> {
if (!this.#safeContract) {
throw new Error('Safe is not deployed')
Expand Down
103 changes: 103 additions & 0 deletions packages/protocol-kit/tests/e2e/moduleManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,109 @@ describe('Safe modules manager', () => {
})
})

describe('getModulesPaginated', async () => {
it('should fail if the Safe is not deployed', async () => {
const { predictedSafe, accounts, contractNetworks } = await setupTests()
const [account1] = accounts
const ethAdapter = await getEthAdapter(account1.signer)
const safeSdk = await Safe.create({
ethAdapter,
predictedSafe,
contractNetworks
})
chai
.expect(safeSdk.getModulesPaginated(SENTINEL_ADDRESS, 10))
.to.be.rejectedWith('Safe is not deployed')
})

it('should return the enabled modules', async () => {
const { safe, accounts, dailyLimitModule, contractNetworks } = await setupTests()
const [account1] = accounts
const ethAdapter = await getEthAdapter(account1.signer)
const safeAddress = await safe.getAddress()
const safeSdk = await Safe.create({
ethAdapter,
safeAddress,
contractNetworks
})
chai.expect((await safeSdk.getModulesPaginated(SENTINEL_ADDRESS, 10)).length).to.be.eq(0)
const tx = await safeSdk.createEnableModuleTx(await dailyLimitModule.getAddress())
const txResponse = await safeSdk.executeTransaction(tx)
await waitSafeTxReceipt(txResponse)
chai.expect((await safeSdk.getModulesPaginated(SENTINEL_ADDRESS, 10)).length).to.be.eq(1)
})

it('should constraint returned modules by pageSize', async () => {
const { safe, accounts, dailyLimitModule, contractNetworks, socialRecoveryModule } =
await setupTests()
const [account1] = accounts
const ethAdapter = await getEthAdapter(account1.signer)
const safeAddress = await safe.getAddress()
const dailyLimitsAddress = await await dailyLimitModule.getAddress()
const socialRecoveryAddress = await await socialRecoveryModule.getAddress()
const safeSdk = await Safe.create({
ethAdapter,
safeAddress,
contractNetworks
})

chai.expect((await safeSdk.getModulesPaginated(SENTINEL_ADDRESS, 10)).length).to.be.eq(0)
const txDailyLimits = await safeSdk.createEnableModuleTx(dailyLimitsAddress)
const dailyLimitsResponse = await safeSdk.executeTransaction(txDailyLimits)
await waitSafeTxReceipt(dailyLimitsResponse)
const txSocialRecovery = await safeSdk.createEnableModuleTx(socialRecoveryAddress)
const soecialRecoveryResponse = await safeSdk.executeTransaction(txSocialRecovery)
await waitSafeTxReceipt(soecialRecoveryResponse)

chai.expect((await safeSdk.getModulesPaginated(SENTINEL_ADDRESS, 10)).length).to.be.eq(2)
chai.expect((await safeSdk.getModulesPaginated(SENTINEL_ADDRESS, 1)).length).to.be.eq(1)
chai.expect((await safeSdk.getModulesPaginated(SENTINEL_ADDRESS, 1)).length).to.be.eq(1)
})

it('should offset the returned modules', async () => {
const { safe, accounts, dailyLimitModule, contractNetworks, socialRecoveryModule } =
await setupTests()
const [account1] = accounts
const ethAdapter = await getEthAdapter(account1.signer)
const safeAddress = await safe.getAddress()
const dailyLimitsAddress = await await dailyLimitModule.getAddress()
const socialRecoveryAddress = await await socialRecoveryModule.getAddress()
const safeSdk = await Safe.create({
ethAdapter,
safeAddress,
contractNetworks
})

const txDailyLimits = await safeSdk.createEnableModuleTx(dailyLimitsAddress)
const dailyLimitsResponse = await safeSdk.executeTransaction(txDailyLimits)
await waitSafeTxReceipt(dailyLimitsResponse)
const txSocialRecovery = await safeSdk.createEnableModuleTx(socialRecoveryAddress)
const soecialRecoveryResponse = await safeSdk.executeTransaction(txSocialRecovery)
await waitSafeTxReceipt(soecialRecoveryResponse)

const [firstModule, secondModule] = await safeSdk.getModulesPaginated(SENTINEL_ADDRESS, 10)

chai.expect((await safeSdk.getModulesPaginated(SENTINEL_ADDRESS, 10)).length).to.be.eq(2)
chai.expect((await safeSdk.getModulesPaginated(firstModule, 10)).length).to.be.eq(1)
chai.expect((await safeSdk.getModulesPaginated(secondModule, 10)).length).to.be.eq(0)
})

it('should fail if pageSize is invalid', async () => {
const { predictedSafe, accounts, contractNetworks } = await setupTests()
const [account1] = accounts
const ethAdapter = await getEthAdapter(account1.signer)
const safeSdk = await Safe.create({
ethAdapter,
predictedSafe,
contractNetworks
})

chai
.expect(safeSdk.getModulesPaginated(SENTINEL_ADDRESS, 0))
.to.be.rejectedWith('Invalid page size for fetching paginated modules')
})
})

describe('isModuleEnabled', async () => {
it('should fail if the Safe is not deployed', async () => {
const { predictedSafe, accounts, dailyLimitModule, contractNetworks } = await setupTests()
Expand Down
1 change: 1 addition & 0 deletions packages/safe-core-sdk-types/src/contracts/SafeContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface SafeContract {
approvedHashes(ownerAddress: string, hash: string): Promise<bigint>
approveHash(hash: string, options?: TransactionOptions): Promise<TransactionResult>
getModules(): Promise<string[]>
getModulesPaginated(start: string, pageSize: number): Promise<string[]>
isModuleEnabled(moduleAddress: string): Promise<boolean>
isValidTransaction(
safeTransaction: SafeTransaction,
Expand Down

0 comments on commit 71fa34a

Please sign in to comment.