diff --git a/.env.example b/.env.example index db7e61b4..37686fee 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,8 @@ HUB_ADDRESS=0xCfEB869F69431e42cdB54A4F4f105C19C080A601 PROXY_FACTORY_ADDRESS=0x9b1f7F645351AF3631a656421eD2e40f2802E6c0 SAFE_ADDRESS=0x2612Af3A521c2df9EAF28422Ca335b04AdF3ac66 SAFE_DEFAULT_CALLBACK_HANDLER=0x67B5656d60a809915323Bf2C40A8bEF15A152e3e +MULTI_SEND_ADDRESS=0xe982E462b094850F12AF94d21D470e21bE9D0E9C +MULTI_SEND_CALL_ONLY_ADDRESS=0x0290FB167208Af455bB137780163b7B7a9a10C16 # Smart contract addresses of the Base CRC version PROXY_FACTORY_ADDRESS_CRC=0xD833215cBcc3f914bD1C9ece3EE7BF8B14f841bb diff --git a/.gitignore b/.gitignore index 389bcc9e..d1ee710b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ docs lib node_modules +coverage diff --git a/jest.config.js b/jest.config.js index 3065352a..bf24fc6f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,11 +2,15 @@ require('dotenv').config(); if (!('crypto' in globalThis)) globalThis.crypto = require('crypto'); module.exports = { - testEnvironment: 'node', - testTimeout: 360 * 1000, - + collectCoverage: true, + globalTeardown: '/teardown.js', // Resolve modules with alias moduleNameMapper: { '^~(.*)$': '/src$1', }, + modulePathIgnorePatterns: ['/test/helpers'], + testEnvironment: 'node', + testMatch: ['**/safe.test.js'], + testTimeout: 360 * 1000, + verbose: true, }; diff --git a/package-lock.json b/package-lock.json index 90b64ca4..9f383a7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@circles/circles-contracts": "^3.3.2", "@circles/safe-contracts": "=1.0.14", "@gnosis.pm/safe-contracts": "^1.3.0", + "@safe-global/protocol-kit": "1.2.0", + "@safe-global/safe-core-sdk-types": "2.2.0", "eth-lib": "^0.2.8" }, "devDependencies": { @@ -26,6 +28,7 @@ "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.1.0", "@rollup/plugin-terser": "^0.4.3", + "@truffle/hdwallet-provider": "2.1.13", "babel-plugin-add-module-exports": "^1.0.4", "babel-plugin-module-resolver": "^5.0.0", "documentation": "^14.0.2", @@ -4770,6 +4773,102 @@ } } }, + "node_modules/@safe-global/protocol-kit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@safe-global/protocol-kit/-/protocol-kit-1.2.0.tgz", + "integrity": "sha512-drU2uK30AZ4tqI/9ER7PGMD/lZp/5B9T02t+noTk7WF9Xb7HxskJd8GNU01KE55oyH31Y0AfXaE68H/f9lYa4A==", + "dependencies": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/solidity": "^5.7.0", + "@safe-global/safe-deployments": "^1.26.0", + "ethereumjs-util": "^7.1.5", + "semver": "^7.5.4", + "web3": "^1.8.1", + "web3-core": "^1.8.1", + "web3-utils": "^1.8.1" + } + }, + "node_modules/@safe-global/protocol-kit/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@safe-global/protocol-kit/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@safe-global/protocol-kit/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/@safe-global/safe-core-sdk-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@safe-global/safe-core-sdk-types/-/safe-core-sdk-types-2.2.0.tgz", + "integrity": "sha512-vVG9qQnUYx+Xwsbuqraq25MPJX1I1aV1P81ZnHZa1lEMU7stqYWAmykUm/mvqsm8+AsvEB/wBKlFjbFJ/duzoA==", + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/contracts": "^5.7.0", + "@safe-global/safe-deployments": "^1.26.0", + "web3-core": "^1.8.1", + "web3-utils": "^1.8.1" + } + }, + "node_modules/@safe-global/safe-deployments": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@safe-global/safe-deployments/-/safe-deployments-1.26.0.tgz", + "integrity": "sha512-Tw89O4/paT19ieMoiWQbqRApb0Bef/DxweS9rxodXAM5EQModkbyFXGZca+YxXE67sLvWjLr2jJUOxwze8mhGw==", + "dependencies": { + "semver": "^7.3.7" + } + }, + "node_modules/@safe-global/safe-deployments/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@safe-global/safe-deployments/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@safe-global/safe-deployments/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/@scure/base": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", @@ -5373,9 +5472,9 @@ } }, "node_modules/@truffle/hdwallet-provider": { - "version": "2.1.12", - "resolved": "https://registry.npmjs.org/@truffle/hdwallet-provider/-/hdwallet-provider-2.1.12.tgz", - "integrity": "sha512-peIiWE5DGee6VmL/BPSRUJqV/P4EXRi+rUHaPJm1b+8kr5GksuzQSloGPbI5ykwwQbi0bL7WyLqaENyHFUzGBQ==", + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@truffle/hdwallet-provider/-/hdwallet-provider-2.1.13.tgz", + "integrity": "sha512-wh93LLumxH8+pPY11DrsNVHjSO3AqMkwPNAWqEi0hRw2BH4QzDhEf2G88GDWJPZPY/zGFSxYHDACtJmUUfRwYw==", "dependencies": { "@ethereumjs/common": "^2.4.0", "@ethereumjs/tx": "^3.3.0", @@ -11425,7 +11524,6 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.5.tgz", "integrity": "sha512-HTm14iMQKK2FjFLRTM5lAVcyaUzOnqbPtesFIvREgXpJHdQm8bWS+GkQgIkfaBYRHuCnea7w8UVNfwiAQhlr9A==", "dev": true, - "hasInstallScript": true, "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" @@ -11783,7 +11881,6 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.7.tgz", "integrity": "sha512-vLt1O5Pp+flcArHGIyKEQq883nBt8nN8tVBcoL0qUXj2XT1n7p70yGIq2VK98I5FdZ1YHc0wk/koOnHjnXWk1Q==", "dev": true, - "hasInstallScript": true, "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" @@ -27085,6 +27182,88 @@ "picomatch": "^2.3.1" } }, + "@safe-global/protocol-kit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@safe-global/protocol-kit/-/protocol-kit-1.2.0.tgz", + "integrity": "sha512-drU2uK30AZ4tqI/9ER7PGMD/lZp/5B9T02t+noTk7WF9Xb7HxskJd8GNU01KE55oyH31Y0AfXaE68H/f9lYa4A==", + "requires": { + "@ethersproject/address": "^5.7.0", + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/solidity": "^5.7.0", + "@safe-global/safe-deployments": "^1.26.0", + "ethereumjs-util": "^7.1.5", + "semver": "^7.5.4", + "web3": "^1.8.1", + "web3-core": "^1.8.1", + "web3-utils": "^1.8.1" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "@safe-global/safe-core-sdk-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@safe-global/safe-core-sdk-types/-/safe-core-sdk-types-2.2.0.tgz", + "integrity": "sha512-vVG9qQnUYx+Xwsbuqraq25MPJX1I1aV1P81ZnHZa1lEMU7stqYWAmykUm/mvqsm8+AsvEB/wBKlFjbFJ/duzoA==", + "requires": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/contracts": "^5.7.0", + "@safe-global/safe-deployments": "^1.26.0", + "web3-core": "^1.8.1", + "web3-utils": "^1.8.1" + } + }, + "@safe-global/safe-deployments": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@safe-global/safe-deployments/-/safe-deployments-1.26.0.tgz", + "integrity": "sha512-Tw89O4/paT19ieMoiWQbqRApb0Bef/DxweS9rxodXAM5EQModkbyFXGZca+YxXE67sLvWjLr2jJUOxwze8mhGw==", + "requires": { + "semver": "^7.3.7" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "@scure/base": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", @@ -27610,9 +27789,9 @@ } }, "@truffle/hdwallet-provider": { - "version": "2.1.12", - "resolved": "https://registry.npmjs.org/@truffle/hdwallet-provider/-/hdwallet-provider-2.1.12.tgz", - "integrity": "sha512-peIiWE5DGee6VmL/BPSRUJqV/P4EXRi+rUHaPJm1b+8kr5GksuzQSloGPbI5ykwwQbi0bL7WyLqaENyHFUzGBQ==", + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@truffle/hdwallet-provider/-/hdwallet-provider-2.1.13.tgz", + "integrity": "sha512-wh93LLumxH8+pPY11DrsNVHjSO3AqMkwPNAWqEi0hRw2BH4QzDhEf2G88GDWJPZPY/zGFSxYHDACtJmUUfRwYw==", "requires": { "@ethereumjs/common": "^2.4.0", "@ethereumjs/tx": "^3.3.0", diff --git a/package.json b/package.json index 55ddb571..3fbafd2a 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "docs:serve": "documentation serve --watch ./src/**", "docs:lint": "documentation lint ./src/**", "lint": "eslint --ignore-path .gitignore --ignore-pattern lib .", - "test": "jest --runInBand", + "test": "jest --runInBand --forceExit", "test:watch": "npm run test -- --watch" }, "devDependencies": { @@ -42,6 +42,7 @@ "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.1.0", "@rollup/plugin-terser": "^0.4.3", + "@truffle/hdwallet-provider": "2.1.13", "babel-plugin-add-module-exports": "^1.0.4", "babel-plugin-module-resolver": "^5.0.0", "documentation": "^14.0.2", @@ -65,6 +66,8 @@ "@circles/circles-contracts": "^3.3.2", "@circles/safe-contracts": "=1.0.14", "@gnosis.pm/safe-contracts": "^1.3.0", + "@safe-global/protocol-kit": "1.2.0", + "@safe-global/safe-core-sdk-types": "2.2.0", "eth-lib": "^0.2.8" } } diff --git a/src/common/createErrorType.js b/src/common/createErrorType.js new file mode 100644 index 00000000..8b3ca5ef --- /dev/null +++ b/src/common/createErrorType.js @@ -0,0 +1,13 @@ +export default function createErrorType(name, code, init) { + function E(message) { + if (!Error.captureStackTrace) this.stack = new Error().stack; + else Error.captureStackTrace(this, this.constructor); + this.message = message; + this.code = code; + init && init.apply(this, arguments); + } + E.prototype = new Error(); + E.prototype.name = name; + E.prototype.constructor = E; + return E; +} diff --git a/src/common/error.js b/src/common/error.js index 9c57d421..9bcfb934 100644 --- a/src/common/error.js +++ b/src/common/error.js @@ -1,4 +1,5 @@ import createSymbolObject from '~/common/createSymbolObject'; +import createErrorType from '~/common/createErrorType'; export const ErrorCodes = createSymbolObject([ 'FAILED_REQUEST', @@ -6,6 +7,7 @@ export const ErrorCodes = createSymbolObject([ 'INVALID_OPTIONS', 'INVALID_TRANSFER', 'SAFE_NOT_FOUND', + 'SAFE_ALREADY_DEPLOYED', 'TOKEN_NOT_FOUND', 'TOO_COMPLEX_TRANSFER', 'TOO_MANY_ATTEMPTS', @@ -57,3 +59,8 @@ export class TransferError extends CoreError { this.transfer = transferData; } } + +export const SafeDeployedError = createErrorType( + 'SafeDeployedError', + ErrorCodes.SAFE_ALREADY_DEPLOYED, +); diff --git a/src/common/safeContractAbis.js b/src/common/safeContractAbis.js new file mode 100644 index 00000000..bf409d29 --- /dev/null +++ b/src/common/safeContractAbis.js @@ -0,0 +1,15 @@ +import GnosisSafeContract from '@gnosis.pm/safe-contracts/build/artifacts/contracts/GnosisSafeL2.sol/GnosisSafeL2.json'; +import ProxyFactoryContract from '@gnosis.pm/safe-contracts/build/artifacts/contracts/proxies/GnosisSafeProxyFactory.sol/GnosisSafeProxyFactory.json'; +import CompatibilityFallbackHandler from '@gnosis.pm/safe-contracts/build/artifacts/contracts/handler/CompatibilityFallbackHandler.sol/CompatibilityFallbackHandler.json'; +import MultiSend from '@gnosis.pm/safe-contracts/build/artifacts/contracts/libraries/MultiSend.sol/MultiSend.json'; +import MultiSendCallOnly from '@gnosis.pm/safe-contracts/build/artifacts/contracts/libraries/MultiSendCallOnly.sol/MultiSendCallOnly.json'; + +const abis = { + safeMasterCopyAbi: GnosisSafeContract.abi, + safeProxyFactoryAbi: ProxyFactoryContract.abi, + fallbackHandlerAbi: CompatibilityFallbackHandler.abi, + multiSendAbi: MultiSend.abi, + multiSendCallOnlyAbi: MultiSendCallOnly.abi, +}; +// TODO: docs +export default abis; diff --git a/src/index.js b/src/index.js index 7f0094fe..282ded6f 100644 --- a/src/index.js +++ b/src/index.js @@ -59,6 +59,12 @@ export default class CirclesCore { fallbackHandlerAddress: { type: web3.utils.checkAddressChecksum, }, + multiSendAddress: { + type: web3.utils.checkAddressChecksum, + }, + multiSendCallOnlyAddress: { + type: web3.utils.checkAddressChecksum, + }, graphNodeEndpoint: { type: 'string', }, diff --git a/src/safe.js b/src/safe.js index 3e1d40f0..8eca4b61 100644 --- a/src/safe.js +++ b/src/safe.js @@ -1,113 +1,12 @@ -import { - SAFE_LAST_VERSION, - SAFE_THRESHOLD, - SENTINEL_ADDRESS, -} from '~/common/constants'; -import CoreError from '~/common/error'; +import { SAFE_LAST_VERSION, ZERO_ADDRESS } from '~/common/constants'; +import CoreError, { SafeDeployedError } from '~/common/error'; import checkAccount from '~/common/checkAccount'; import checkOptions from '~/common/checkOptions'; -import { - getSafeContract, - getSafeCRCVersionContract, -} from '~/common/getContracts'; +import { getSafeCRCVersionContract } from '~/common/getContracts'; import loop from '~/common/loop'; - -/** - * Helper method to receive a list of all Gnosis Safe owners. - * - * @access private - * - * @param {Web3} web3 - Web3 instance - * @param {string} safeAddress - * - * @return {string[]} - array of owner addresses - */ -export async function getOwners(web3, safeAddress) { - // Get Safe at given address - const safe = getSafeContract(web3, safeAddress); - - // Call 'getOwners' method and return list of owners - return await safe.methods.getOwners().call(); -} - -/** - * Helper method to get the Safe version. - * - * @access private - * - * @param {Web3} web3 - Web3 instance - * @param {string} safeAddress - * - * @return {string} - version of the Safe - */ -export async function getVersion(web3, safeAddress) { - // Get Safe at given address - const safe = getSafeContract(web3, safeAddress); - - // Call 'VERSION' method and return it - return await safe.methods.VERSION().call(); -} - -/** - * Predict Safe address - * - * @access private - * - * @param {Web3} web3 - Web3 instance - * @param {Object} utils - utils module instance - * @param {number} nonce - Safe creation salt nonce - * @param {string} address - Safe owner address - * - * @return {string} - predicted Safe address - */ -async function predictAddress(web3, utils, nonce, address) { - const { safe } = await utils.requestRelayer({ - path: ['safes', 'predict'], - version: 3, - method: 'POST', - data: { - saltNonce: nonce, - owners: [address], - threshold: SAFE_THRESHOLD, - }, - }); - - return web3.utils.toChecksumAddress(safe); -} - -/** - * Returns if the Safe is created and / or deployed. - * - * @access private - * - * @param {Object} utils - utils module instance - * @param {string} safeAddress - Safe address - * - * @return {Object} - Safe status - */ -async function getSafeStatus(utils, safeAddress) { - let isCreated = false; - let isDeployed = false; - - try { - const { txHash } = await utils.requestRelayer({ - path: ['safes', safeAddress, 'funded'], - version: 2, - }); - isCreated = true; - isDeployed = txHash !== null; - } catch (error) { - // Ignore Not Found errors - if (!error.request || error.request.status !== 404) { - throw error; - } - } - - return { - isCreated, - isDeployed, - }; -} +import Safe, { + getSafeContract as _getSafeContract, +} from '@safe-global/protocol-kit'; /** * Safe submodule to deploy and interact with the Gnosis Safe. @@ -127,8 +26,149 @@ export default function createSafeModule( utils, globalOptions, ) { - const { fallbackHandlerAddress } = globalOptions; - const { safeMaster } = contracts; + const { fallbackHandlerAddress, proxyFactoryAddress, safeMasterAddress } = + globalOptions; + const { safeMaster, proxyFactory } = contracts; + + ///////////////////////// + /* Safe Module Methods */ + ///////////////////////// + const getSafe = ({ + predictedSafe, + safeAddress, + signerAddress, + params = {}, + }) => + utils.getContractNetworks().then((contractNetworks) => + Safe.create({ + ethAdapter: utils.createEthAdapter(signerAddress), + contractNetworks, + ...(predictedSafe && { predictedSafe }), + ...(safeAddress && { safeAddress }), + ...params, + }), + ); + + /** + * Predict Safe address + * + * @access private + * + * @param {string} ownerAddress - Safe owner address + * @param {number} nonce - Safe creation salt nonce + * + * @return {string} - predicted Safe address + */ + const predictAddress = (ownerAddress, nonce) => + utils.createSafeFactory(ownerAddress).then((safeFactory) => + safeFactory.predictSafeAddress( + { + owners: [ownerAddress], + threshold: 1, + }, + nonce, + ), + ); + + const isSafeDeployed = (accountAddress, nonce) => + getSafe({ + predictedSafe: { + safeAccountConfig: { + owners: [accountAddress], + threshold: 1, + }, + safeDeploymentConfig: { + saltNonce: nonce, + }, + }, + }).then((safeSdk) => safeSdk.isSafeDeployed()); + + const deploySafe = async (ownerAddress, nonce) => { + const isDeployed = await isSafeDeployed(ownerAddress, nonce); + + if (isDeployed) { + throw new SafeDeployedError( + `Safe with nonce ${nonce} is already deployed.`, + ); + } + + const initializer = await safeMaster.methods + .setup( + [ownerAddress], + 1, + ZERO_ADDRESS, + '0x', + fallbackHandlerAddress, + ZERO_ADDRESS, + 0, + ZERO_ADDRESS, + ) + .encodeABI(); + const data = await proxyFactory.methods + .createProxyWithNonce(safeMasterAddress, initializer, nonce) + .encodeABI(); + + await utils.requestNewRelayer({ + path: ['transactions'], + method: 'POST', + isTrailingSlash: false, + data: { + target: proxyFactoryAddress, + data, + }, + }); + + return predictAddress(ownerAddress, nonce); + }; + + /** + * Helper method to get the Safe version. + * + * @access private + * + * @param {string} safeAddress + * + * @return {string} - version of the Safe + */ + const getVersion = (safeAddress) => + getSafe({ safeAddress }).then((safeSdk) => safeSdk.getContractVersion()); + + /** + * Helper method to receive a list of all Gnosis Safe owners. + * + * @access private + * + * @param {string} safeAddress + * + * @return {string[]} - array of owner addresses + */ + const getOwners = (safeAddress) => + getSafe({ safeAddress }).then((safeSdk) => safeSdk.getOwners()); + + const encodeSafeTransaction = ({ + signedSafeTx, + signerAddress, + safeVersion, + }) => + _getSafeContract({ + ethAdapter: utils.createEthAdapter(signerAddress), + safeVersion, + customContracts: utils.getCustomContracts(), + }).then((safeSingletonContract) => + safeSingletonContract.encode('execTransaction', [ + signedSafeTx.data.to, + signedSafeTx.data.value, + signedSafeTx.data.data, + signedSafeTx.data.operation, + signedSafeTx.data.safeTxGas, + signedSafeTx.data.baseGas, + signedSafeTx.data.gasPrice, + signedSafeTx.data.gasToken, + signedSafeTx.data.refundReceiver, + signedSafeTx.encodedSignatures(), + ]), + ); + return { /** * Predict Safe address. @@ -144,157 +184,176 @@ export default function createSafeModule( predictAddress: async (account, userOptions) => { checkAccount(web3, account); - const options = checkOptions(userOptions, { + const { nonce } = checkOptions(userOptions, { nonce: { type: 'number', }, }); - return await predictAddress(web3, utils, options.nonce, account.address); + return predictAddress(account.address, nonce); }, /** - * Returns status of a Safe in the system. Is it created or already - * deployed? + * Deploy a new Safe with the Relayer. * - * @namespace core.safe.getSafeStatus + * @namespace core.safe.deploySafe * * @param {Object} account - web3 account instance * @param {Object} userOptions - options - * @param {number} userOptions.safeAddress - Safe address + * @param {number} userOptions.nonce - nonce to predict address * - * @return {Object} - Safe status + * @return {string} - Gnosis Safe address */ - getSafeStatus: async (account, userOptions) => { + deploySafe: (account, userOptions) => { checkAccount(web3, account); - const options = checkOptions(userOptions, { - safeAddress: { - type: web3.utils.checkAddressChecksum, + const { nonce } = checkOptions(userOptions, { + nonce: { + type: 'number', }, }); - return await getSafeStatus(utils, options.safeAddress); + return deploySafe(account.address, nonce); }, - /** - * Register a to-be-created Safe in the Relayer and receive a predicted - * Safe address. - * - * @namespace core.safe.prepareDeploy - * - * @param {Object} account - web3 account instance - * @param {Object} userOptions - options - * @param {number} userOptions.nonce - nonce to predict address - * - * @return {string} - Predicted Gnosis Safe address - */ - prepareDeploy: async (account, userOptions) => { + isSafeDeployed: (account, userOptions) => { checkAccount(web3, account); - const options = checkOptions(userOptions, { + const { nonce } = checkOptions(userOptions, { nonce: { type: 'number', }, }); - // Check if Safe already exists - const predictedSafeAddress = await predictAddress( - web3, - utils, - options.nonce, - account.address, - ); + return isSafeDeployed(account.address, nonce); + }, - // Return predicted Safe address when Safe is already in the system - const status = await getSafeStatus(utils, predictedSafeAddress); - if (status.isCreated) { - return predictedSafeAddress; - } + /** + * Returns a list of all owners of the given Gnosis Safe. + * + * @namespace core.safe.getOwners + * + * @param {Object} account - web3 account instance + * @param {Object} userOptions - options + * @param {number} userOptions.safeAddress - address of the Gnosis Safe + * + * @return {string[]} - array of owner addresses + */ + getOwners: (account, userOptions) => { + checkAccount(web3, account); - // .. otherwise start creation of Safe - const { safe } = await utils.requestRelayer({ - path: ['safes'], - version: 3, - method: 'POST', - data: { - saltNonce: options.nonce, - owners: [account.address], - threshold: SAFE_THRESHOLD, + const { safeAddress } = checkOptions(userOptions, { + safeAddress: { + type: web3.utils.checkAddressChecksum, }, }); - return web3.utils.toChecksumAddress(safe); + return getOwners(safeAddress); }, /** - * Returns true if there are enough balance on this address to deploy - * a Safe. + * Add an address as an owner of a given Gnosis Safe. * - * @namespace core.safe.isFunded + * @namespace core.safe.addOwner * * @param {Object} account - web3 account instance - * @param {Object} userOptions - user arguments - * @param {string} userOptions.safeAddress - safe address to check + * @param {Object} userOptions - options + * @param {number} userOptions.safeAddress - address of the Gnosis Safe + * @param {number} userOptions.ownerAddress - owner address to be added * - * @return {boolean} - has enough funds + * @return {string} - transaction hash */ - isFunded: async (account, userOptions) => { + addOwner: async (account, userOptions) => { checkAccount(web3, account); - const options = checkOptions(userOptions, { + const { safeAddress, ownerAddress } = checkOptions(userOptions, { safeAddress: { type: web3.utils.checkAddressChecksum, }, + ownerAddress: { + type: web3.utils.checkAddressChecksum, + }, }); - try { - const result = await utils.requestRelayer({ - path: ['safes', 'estimates'], - data: { - numberOwners: 1, - }, - version: 3, - method: 'POST', - }); - - const balance = await web3.eth.getBalance(options.safeAddress); + const safeSdk = await getSafe({ + safeAddress, + signerAddress: account.address, + }); + const data = await safeSdk + .createAddOwnerTx({ ownerAddress }) + .then((addOwnerTx) => safeSdk.signTransaction(addOwnerTx)) + .then(async (signedSafeTx) => + encodeSafeTransaction({ + signedSafeTx, + signerAddress: account.address, + safeVersion: await safeSdk.getContractVersion(), + }), + ); - return web3.utils.toBN(balance).gte(web3.utils.toBN(result[0].payment)); - } catch { - return false; - } + return utils.requestNewRelayer({ + path: ['transactions'], + method: 'POST', + isTrailingSlash: false, + data: { + target: safeAddress, + chainId: await web3.eth.getChainId(), + data, + }, + }); }, /** - * Requests the relayer to not wait for the Safe deployment task. - * This might still fail when the Safe is not funded or does not - * have enough trust connections yet. + * Remove owner of a given Gnosis Safe. * - * @namespace core.safe.deploy + * @namespace core.safe.removeOwner * * @param {Object} account - web3 account instance * @param {Object} userOptions - options - * @param {number} userOptions.safeAddress - to-be-deployed Safe address + * @param {number} userOptions.safeAddress - address of the Gnosis Safe + * @param {number} userOptions.ownerAddress - owner address to be removed * - * @return {boolean} - returns true when successful + * @return {string} - transaction hash */ - deploy: async (account, userOptions) => { + removeOwner: async (account, userOptions) => { checkAccount(web3, account); - const options = checkOptions(userOptions, { + const { safeAddress, ownerAddress } = checkOptions(userOptions, { safeAddress: { type: web3.utils.checkAddressChecksum, }, + ownerAddress: { + type: web3.utils.checkAddressChecksum, + }, }); - await utils.requestRelayer({ - path: ['safes', options.safeAddress, 'funded'], - version: 2, - method: 'PUT', + const safeSdk = await getSafe({ + safeAddress, + signerAddress: account.address, }); + const data = await safeSdk + .createRemoveOwnerTx({ + ownerAddress, + threshold: await safeSdk.getThreshold(), + }) + .then((addOwnerTx) => safeSdk.signTransaction(addOwnerTx)) + .then(async (signedSafeTx) => + encodeSafeTransaction({ + signedSafeTx, + signerAddress: account.address, + safeVersion: await safeSdk.getContractVersion(), + }), + ); - return true; + return utils.requestNewRelayer({ + path: ['transactions'], + method: 'POST', + isTrailingSlash: false, + data: { + target: safeAddress, + chainId: await web3.eth.getChainId(), + data, + }, + }); }, /** @@ -359,120 +418,6 @@ export default function createSafeModule( }); }, - /** - * Returns a list of all owners of the given Gnosis Safe. - * - * @namespace core.safe.getOwners - * - * @param {Object} account - web3 account instance - * @param {Object} userOptions - options - * @param {number} userOptions.safeAddress - address of the Gnosis Safe - * - * @return {string[]} - array of owner addresses - */ - getOwners: async (account, userOptions) => { - checkAccount(web3, account); - - const options = checkOptions(userOptions, { - safeAddress: { - type: web3.utils.checkAddressChecksum, - }, - }); - - return await getOwners(web3, options.safeAddress); - }, - - /** - * Add an address as an owner of a given Gnosis Safe. - * - * @namespace core.safe.addOwner - * - * @param {Object} account - web3 account instance - * @param {Object} userOptions - options - * @param {number} userOptions.safeAddress - address of the Gnosis Safe - * @param {number} userOptions.ownerAddress - owner address to be added - * - * @return {string} - transaction hash - */ - addOwner: async (account, userOptions) => { - checkAccount(web3, account); - - const options = checkOptions(userOptions, { - safeAddress: { - type: web3.utils.checkAddressChecksum, - }, - ownerAddress: { - type: web3.utils.checkAddressChecksum, - }, - }); - - // Get Safe at given address - const safe = getSafeContract(web3, options.safeAddress); - - // Prepare 'addOwnerWithThreshold' method - const txData = safe.methods - .addOwnerWithThreshold(options.ownerAddress, SAFE_THRESHOLD) - .encodeABI(); - - // Call method and return result - return await utils.executeTokenSafeTx(account, { - safeAddress: options.safeAddress, - to: options.safeAddress, - txData, - }); - }, - - /** - * Remove owner of a given Gnosis Safe. - * - * @namespace core.safe.removeOwner - * - * @param {Object} account - web3 account instance - * @param {Object} userOptions - options - * @param {number} userOptions.safeAddress - address of the Gnosis Safe - * @param {number} userOptions.ownerAddress - owner address to be removed - * - * @return {string} - transaction hash - */ - removeOwner: async (account, userOptions) => { - checkAccount(web3, account); - - const options = checkOptions(userOptions, { - safeAddress: { - type: web3.utils.checkAddressChecksum, - }, - ownerAddress: { - type: web3.utils.checkAddressChecksum, - }, - }); - - // Get Safe at given address - const safe = getSafeContract(web3, options.safeAddress); - - // We need the list of owners before ... - const owners = await getOwners(web3, options.safeAddress); - - // .. to find out which previous owner in the list is pointing at the one we want to remove - const ownerIndex = owners.findIndex( - (owner) => owner === options.ownerAddress, - ); - - const prevOwner = - ownerIndex > 0 ? owners[ownerIndex - 1] : SENTINEL_ADDRESS; - - // Prepare 'removeOwner' method by passing pointing owner and the owner to be removed - const txData = await safe.methods - .removeOwner(prevOwner, options.ownerAddress, SAFE_THRESHOLD) - .encodeABI(); - - // Call method and return result - return await utils.executeTokenSafeTx(account, { - safeAddress: options.safeAddress, - to: options.safeAddress, - txData, - }); - }, - /** * Get Safe version. * @@ -484,16 +429,16 @@ export default function createSafeModule( * * @return {string} - transaction hash */ - getVersion: async (account, userOptions) => { + getVersion: (account, userOptions) => { checkAccount(web3, account); - const options = checkOptions(userOptions, { + const { safeAddress } = checkOptions(userOptions, { safeAddress: { type: web3.utils.checkAddressChecksum, }, }); - return await getVersion(web3, options.safeAddress); + return getVersion(safeAddress); }, /** @@ -516,7 +461,7 @@ export default function createSafeModule( }, }); - const safeVersion = await getVersion(web3, options.safeAddress); + const safeVersion = await getVersion(options.safeAddress); let txHashChangeMasterCopy; let txHashFallbackHandler; @@ -551,7 +496,7 @@ export default function createSafeModule( // Wait to check that the version is updated await loop( () => { - return getVersion(web3, options.safeAddress); + return getVersion(options.safeAddress); }, (version) => { return version == SAFE_LAST_VERSION; diff --git a/src/utils.js b/src/utils.js index a2008129..457b26b6 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,4 +1,5 @@ import fetch from 'isomorphic-fetch'; +import { SafeFactory, Web3Adapter } from '@safe-global/protocol-kit'; import CoreError, { RequestError, ErrorCodes } from '~/common/error'; import TransactionQueue from '~/common/queue'; @@ -13,6 +14,7 @@ import { signTypedData, } from '~/common/typedData'; import { getTokenContract, getSafeContract } from '~/common/getContracts'; +import safeContractAbis from '~/common/safeContractAbis'; /** @access private */ const transactionQueue = new TransactionQueue(); @@ -71,6 +73,14 @@ async function request(endpoint, userOptions) { return json; }); + } else if (contentType && contentType.includes('text/plain')) { + return response.text().then((text) => { + if (response.status >= 400) { + throw new RequestError(url, text, response.status); + } + + return text; + }); } else { if (response.status >= 400) { throw new RequestError(url, response.body, response.status); @@ -529,6 +539,11 @@ export default function createUtilsModule(web3, contracts, globalOptions) { graphNodeEndpoint, relayServiceEndpoint, subgraphName, + proxyFactoryAddress, + safeMasterAddress, + fallbackHandlerAddress, + multiSendAddress, + multiSendCallOnlyAddress, } = globalOptions; const { hub } = contracts; @@ -600,6 +615,31 @@ export default function createUtilsModule(web3, contracts, globalOptions) { }); } + const getCustomContracts = () => ({ + safeMasterCopyAddress: safeMasterAddress, + safeProxyFactoryAddress: proxyFactoryAddress, + fallbackHandlerAddress: fallbackHandlerAddress, + multiSendAddress, + multiSendCallOnlyAddress, + ...safeContractAbis, + }); + + const getContractNetworks = () => + web3.eth.getChainId().then((chainId) => ({ + [chainId]: getCustomContracts(), + })); + + const createEthAdapter = (signerAddress) => + new Web3Adapter({ web3, signerAddress }); + + const createSafeFactory = (signerAddress) => + getContractNetworks().then((contractNetworks) => + SafeFactory.create({ + ethAdapter: createEthAdapter(signerAddress), + contractNetworks, + }), + ); + return { /** * Iterate on a request until a response condition is met and then, returns the response. @@ -662,6 +702,14 @@ export default function createUtilsModule(web3, contracts, globalOptions) { return parseInt(web3.utils.fromWei(`${value}`, 'ether'), 10); }, + // TODO: docs + requestNewRelayer: (userOptions) => + request(relayServiceEndpoint, userOptions), + createEthAdapter, + createSafeFactory, + getCustomContracts, + getContractNetworks, + /** * Send an API request to the Gnosis Relayer. * diff --git a/teardown.js b/teardown.js new file mode 100644 index 00000000..b3a8f7cb --- /dev/null +++ b/teardown.js @@ -0,0 +1,3 @@ +const { provider } = require('./test/helpers/web3'); + +module.exports = () => provider.engine.stop(); diff --git a/test/helpers/account.js b/test/helpers/account.js deleted file mode 100644 index 85c7ceaa..00000000 --- a/test/helpers/account.js +++ /dev/null @@ -1,6 +0,0 @@ -import privateKeys from '../accounts.json'; -import web3 from './web3'; - -export default function getAccount(accountIndex = 0) { - return web3.eth.accounts.privateKeyToAccount(privateKeys[accountIndex]); -} diff --git a/test/helpers/accounts.js b/test/helpers/accounts.js new file mode 100644 index 00000000..910a76e5 --- /dev/null +++ b/test/helpers/accounts.js @@ -0,0 +1,8 @@ +import privateKeys from './accounts.json'; +import web3 from './web3'; + +const accounts = privateKeys.map((key) => + web3.eth.accounts.privateKeyToAccount(key), +); + +export default accounts; diff --git a/test/accounts.json b/test/helpers/accounts.json similarity index 100% rename from test/accounts.json rename to test/helpers/accounts.json diff --git a/test/helpers/core.js b/test/helpers/core.js index 006a3f1b..6dd6c0c9 100644 --- a/test/helpers/core.js +++ b/test/helpers/core.js @@ -14,6 +14,8 @@ export default function createCore(opts) { relayServiceEndpoint: process.env.RELAY_SERVICE_ENDPOINT, safeMasterAddress: process.env.SAFE_ADDRESS, subgraphName: process.env.SUBGRAPH_NAME, + multiSendAddress: process.env.MULTI_SEND_ADDRESS, + multiSendCallOnlyAddress: process.env.MULTI_SEND_CALL_ONLY_ADDRESS, ...opts, }); } diff --git a/test/helpers/generateSaltNonce.js b/test/helpers/generateSaltNonce.js new file mode 100644 index 00000000..75114513 --- /dev/null +++ b/test/helpers/generateSaltNonce.js @@ -0,0 +1,3 @@ +const { randomUUID } = require('crypto'); + +module.exports = () => Buffer.from(randomUUID()).readUInt32BE(0); diff --git a/test/helpers/web3.js b/test/helpers/web3.js index 798bf8d8..8bfcafdf 100644 --- a/test/helpers/web3.js +++ b/test/helpers/web3.js @@ -1,8 +1,12 @@ import Web3 from 'web3'; +import HDWalletProvider from '@truffle/hdwallet-provider'; -const provider = new Web3.providers.HttpProvider( - process.env.RPC_URL || 'http://localhost:8545', -); +import privateKeys from './accounts.json'; + +export const provider = new HDWalletProvider({ + privateKeys, + providerOrUrl: process.env.RPC_URL || 'http://localhost:8545', +}); const web3 = new Web3(provider); diff --git a/test/safe.test.js b/test/safe.test.js index 7941d07a..afb543d6 100644 --- a/test/safe.test.js +++ b/test/safe.test.js @@ -1,272 +1,105 @@ +import { SAFE_LAST_VERSION, SAFE_CRC_VERSION } from '~/common/constants'; +import { SafeDeployedError } from '~/common/error'; + import createCore from './helpers/core'; -import getAccount from './helpers/account'; +import accounts from './helpers/accounts'; import web3 from './helpers/web3'; -import isContractDeployed from './helpers/isContractDeployed'; import { addTrustConnection, - deploySafeAndToken, fundSafe, deployCRCVersionSafe, deployToken, } from './helpers/transactions'; - -import { SAFE_LAST_VERSION, SAFE_CRC_VERSION } from '~/common/constants'; +import generateSaltNonce from './helpers/generateSaltNonce'; describe('Safe', () => { - let core; - let accounts; - - beforeAll(() => { - accounts = new Array(4).fill({}).map((item, index) => { - return getAccount(index); - }); - - core = createCore(); - }); + const core = createCore(); describe('when a new Safe gets manually created', () => { + const nonce = generateSaltNonce(); let safeAddress; - let nonce; - beforeAll(async () => { - nonce = Date.now(); - safeAddress = await core.safe.prepareDeploy(accounts[0], { - nonce, - }); - }); + it('should deploy a Safe successfully', async () => { + safeAddress = await core.safe.deploySafe(accounts[0], { nonce }); - it('should be a valid address', () => { expect(web3.utils.isAddress(safeAddress)).toBe(true); }); - it('should have predicted its future Safe address', async () => { - const predictedSafeAddress = await core.safe.predictAddress(accounts[0], { - nonce, - }); - - expect(web3.utils.isAddress(predictedSafeAddress)).toBe(true); - expect(predictedSafeAddress).toBe(safeAddress); - }); - - it('should return the correct status', async () => { - const status = await core.safe.getSafeStatus(accounts[0], { - safeAddress, - }); - - expect(status.isCreated).toBe(true); - expect(status.isDeployed).toBe(false); - }); - - it('should recover the same Safe address when doing it again', async () => { - const safeAddressAgain = await core.safe.prepareDeploy(accounts[0], { - nonce, - }); - - expect(safeAddressAgain).toBe(safeAddress); - }); - - it('should be able to manually fund it for deployment', async () => { - expect( - await core.safe.isFunded(accounts[0], { - safeAddress, - }), - ).toBe(false); - - await fundSafe(accounts[0], safeAddress); - - expect( - await core.safe.isFunded(accounts[0], { - safeAddress, - }), - ).toBe(true); - - const result = await core.safe.deploy(accounts[0], { - safeAddress, - }); - - // .. wait for Relayer to really deploy Safe - await core.utils.loop( - () => web3.eth.getCode(safeAddress), - isContractDeployed, - { - label: 'Wait until Safe got deployed', - }, - ); - - // Deploy Token as well to pay our fees later - await core.token.deploy(accounts[0], { - safeAddress, - }); - - expect(result).toBe(true); - - // Check if the status is correct - const status = await core.safe.getSafeStatus(accounts[0], { - safeAddress, - }); - - expect(status.isCreated).toBe(true); - expect(status.isDeployed).toBe(true); - }); - }); - - describe('when a new Safe gets created through collecting trust connections', () => { - let safeAddress; - let trustAccounts; - let trustSafeAddresses; - - beforeAll(async () => { - trustAccounts = [accounts[1], accounts[2], accounts[3]]; - trustSafeAddresses = []; - - const safeCreationNonce = new Date().getTime(); - - safeAddress = await core.safe.prepareDeploy(accounts[0], { - nonce: safeCreationNonce, - }); - - // Create manually funded accounts for trust connections - const tasks = trustAccounts.map((account) => { - return deploySafeAndToken(core, account); - }); - - const results = await Promise.all(tasks); - results.forEach((result) => { - trustSafeAddresses.push(result.safeAddress); - }); - }); - - it('should get funded through the relayer', async () => { - // It should not be funded - expect( - await core.safe.isFunded(accounts[0], { - safeAddress, - }), - ).toBe(false); - - // Receive 3 incoming trust connections from other users - const connectionTasks = trustAccounts.map((account, index) => { - return addTrustConnection(core, account, { - user: safeAddress, - canSendTo: trustSafeAddresses[index], - limitPercentage: 10, - }); - }); - - await Promise.all(connectionTasks); - - // Deploy Safe - await core.safe.deploy(accounts[0], { - safeAddress, - }); - - // .. wait for Relayer to really deploy Safe - await core.utils.loop( - () => web3.eth.getCode(safeAddress), - isContractDeployed, - { - label: 'Wait until Safe got deployed', - retryDelay: 4000, - }, - ); + it('should be deployed', () => + core.safe + .isSafeDeployed(accounts[0], { nonce }) + .then((isDeployed) => expect(isDeployed).toBe(true))); - // Deploy Token - const tokenAddress = await deployToken(core, accounts[0], { - safeAddress, - }); - - const code = await web3.eth.getCode(tokenAddress); - expect(code).not.toBe('0x'); - expect(web3.utils.isAddress(tokenAddress)).toBe(true); - }); + it('should throw error when trying to deploy twice with same nonce', () => + expect(() => + core.safe.deploySafe(accounts[0], { nonce }), + ).rejects.toThrow(SafeDeployedError)); }); describe('when I want to manage the owners of a Safe', () => { let safeAddress; beforeAll(async () => { - const result = await deploySafeAndToken(core, accounts[0]); - safeAddress = result.safeAddress; - }); - - it('should return a list of the current owners', async () => { - const owners = await core.safe.getOwners(accounts[0], { - safeAddress, + safeAddress = await core.safe.deploySafe(accounts[0], { + nonce: generateSaltNonce(), }); - - expect(owners[0]).toBe(accounts[0].address); - expect(owners.length).toBe(1); }); - it('should add another owner to the Safe', async () => { + it('should return the current owners list', () => + core.safe + .getOwners(accounts[0], { safeAddress }) + .then((owners) => expect(owners).toEqual([accounts[0].address]))); + + it('should add owner to the Safe', async () => { const response = await core.safe.addOwner(accounts[0], { safeAddress, ownerAddress: accounts[1].address, }); - expect(web3.utils.isHexStrict(response)).toBe(true); + expect(typeof response.taskId).toBe('string'); const owners = await core.utils.loop( - () => { - return core.safe.getOwners(accounts[0], { - safeAddress, - }); - }, + () => core.safe.getOwners(accounts[0], { safeAddress }), (owners) => owners.length === 2, - { label: 'Wait for newly added address to show up as Safe owner' }, + { label: 'Wait for new owner to be added' }, ); - expect(owners[0]).toBe(accounts[1].address); - expect(owners[1]).toBe(accounts[0].address); - expect(owners.length).toBe(2); - - await core.utils.loop( - () => { - return core.safe.getAddresses(accounts[0], { - ownerAddress: accounts[1].address, - }); - }, - (addresses) => addresses.includes(safeAddress), - { label: 'Wait for newly added address to show up as Safe owner' }, - ); + expect(owners).toContain(accounts[1].address); + expect(owners).toHaveLength(2); }); - it('should remove an owner from the Safe', async () => { + it('should remove owner from the Safe', async () => { const response = await core.safe.removeOwner(accounts[0], { safeAddress, ownerAddress: accounts[1].address, }); - expect(web3.utils.isHexStrict(response)).toBe(true); + expect(typeof response.taskId).toBe('string'); const owners = await core.utils.loop( - () => { - return core.safe.getOwners(accounts[0], { - safeAddress, - }); - }, + () => core.safe.getOwners(accounts[0], { safeAddress }), (owners) => owners.length === 1, - { label: 'Wait for newly added address to show up as Safe owner' }, + { label: 'Wait for new owner to be added' }, ); - expect(owners[0]).toBe(accounts[0].address); - expect(owners.length).toBe(1); + expect(owners).not.toContain(accounts[1].address); + expect(owners).toHaveLength(1); }); }); describe('when I want to update the Safe version', () => { let safeAddress; - let ownerCRCVersion; let CRCVersionSafeAddress; let CRCVersionSafeInstance; + const ownerCRCVersion = accounts[2]; beforeAll(async () => { // Deploy new version (v1.3.0) - const result = await deploySafeAndToken(core, accounts[0]); - safeAddress = result.safeAddress; + safeAddress = await core.safe.deploySafe(accounts[0], { + nonce: generateSaltNonce(), + }); // Deploy a Safe with the CRC version (v1.1.1+Circles) - ownerCRCVersion = getAccount(7); CRCVersionSafeInstance = await deployCRCVersionSafe(ownerCRCVersion); CRCVersionSafeAddress = CRCVersionSafeInstance.options.address; await fundSafe(accounts[0], CRCVersionSafeAddress);