From 0ae39ec7674d18cadc3735789f03a621a0b4bd88 Mon Sep 17 00:00:00 2001 From: Juan Enrique Alcaraz Date: Thu, 17 Aug 2023 12:41:37 +0200 Subject: [PATCH] Add tests --- jest.config.js | 1 - package.json | 2 +- teardown.js | 3 - test/helpers/accounts.js | 8 -- test/helpers/addTrust.js | 63 ++++++++++++ test/helpers/core.js | 4 +- test/helpers/getAccounts.js | 6 ++ test/helpers/setupAccount.js | 85 ++++++++++++++++ test/helpers/setupWeb3.js | 17 ++++ test/helpers/transactions.js | 185 +++-------------------------------- test/helpers/web3.js | 13 --- test/safe.test.js | 81 +++++++++++++-- 12 files changed, 261 insertions(+), 207 deletions(-) delete mode 100644 teardown.js delete mode 100644 test/helpers/accounts.js create mode 100644 test/helpers/addTrust.js create mode 100644 test/helpers/getAccounts.js create mode 100644 test/helpers/setupAccount.js create mode 100644 test/helpers/setupWeb3.js delete mode 100644 test/helpers/web3.js diff --git a/jest.config.js b/jest.config.js index bf24fc6f..5b171d8a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,7 +3,6 @@ if (!('crypto' in globalThis)) globalThis.crypto = require('crypto'); module.exports = { collectCoverage: true, - globalTeardown: '/teardown.js', // Resolve modules with alias moduleNameMapper: { '^~(.*)$': '/src$1', diff --git a/package.json b/package.json index 3fbafd2a..3b01c278 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 --forceExit", + "test": "jest --runInBand", "test:watch": "npm run test -- --watch" }, "devDependencies": { diff --git a/teardown.js b/teardown.js deleted file mode 100644 index b3a8f7cb..00000000 --- a/teardown.js +++ /dev/null @@ -1,3 +0,0 @@ -const { provider } = require('./test/helpers/web3'); - -module.exports = () => provider.engine.stop(); diff --git a/test/helpers/accounts.js b/test/helpers/accounts.js deleted file mode 100644 index 910a76e5..00000000 --- a/test/helpers/accounts.js +++ /dev/null @@ -1,8 +0,0 @@ -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/helpers/addTrust.js b/test/helpers/addTrust.js new file mode 100644 index 00000000..3014a062 --- /dev/null +++ b/test/helpers/addTrust.js @@ -0,0 +1,63 @@ +import { getSafeContract } from '~/common/getContracts'; + +import getTrustConnection from './getTrustConnection'; + +export default async function addTrust( + { account, safeAddress, safeAddressToTrust, limitPercentage }, + core, +) { + const { + contracts: { hub }, + options: { hubAddress }, + safe, + utils, + web3, + } = core; + // Retrieve safe + const safeSdk = await safe.getSafeSdk(account, { safeAddress }); + + // Prepare transaction to trust a Safe + return ( + safeSdk + .createTransaction({ + safeTransactionData: { + to: hubAddress, + value: 0, + data: hub.methods + .trust(safeAddressToTrust, limitPercentage) + .encodeABI(), + }, + }) + .then((safeTx) => safeSdk.signTransaction(safeTx)) + // Execute manually the transaction + .then((signedSafeTx) => + web3.eth.sendTransaction({ + from: account.address, + to: safeAddress, + value: 0, + data: getSafeContract(web3, safeAddress) + .methods.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(), + ) + .encodeABI(), + }), + ) + .then(() => + utils.loop( + () => + getTrustConnection(core, account, safeAddress, safeAddressToTrust), + (isReady) => isReady, + { label: 'Wait for the graph to index newly added trust connection' }, + ), + ) + ); +} diff --git a/test/helpers/core.js b/test/helpers/core.js index 6dd6c0c9..ed174d56 100644 --- a/test/helpers/core.js +++ b/test/helpers/core.js @@ -1,8 +1,6 @@ import CirclesCore from '~'; -import web3 from './web3'; - -export default function createCore(opts) { +export default function createCore(web3, opts) { return new CirclesCore(web3, { apiServiceEndpoint: process.env.API_SERVICE_ENDPOINT, fallbackHandlerAddress: process.env.SAFE_DEFAULT_CALLBACK_HANDLER, diff --git a/test/helpers/getAccounts.js b/test/helpers/getAccounts.js new file mode 100644 index 00000000..6562d2b0 --- /dev/null +++ b/test/helpers/getAccounts.js @@ -0,0 +1,6 @@ +import privateKeys from './accounts.json'; + +const getAccounts = (web3) => + privateKeys.map((key) => web3.eth.accounts.privateKeyToAccount(key)); + +export default getAccounts; diff --git a/test/helpers/setupAccount.js b/test/helpers/setupAccount.js new file mode 100644 index 00000000..faff828b --- /dev/null +++ b/test/helpers/setupAccount.js @@ -0,0 +1,85 @@ +import { ZERO_ADDRESS } from '~/common/constants'; +import { getSafeContract } from '~/common/getContracts'; + +// Set up manually a Safe for being fully usable in Circles +export default async function setupAccount({ account, nonce }, core) { + const { + contracts: { safeMaster, hub, proxyFactory }, + options: { + fallbackHandlerAddress, + hubAddress, + proxyFactoryAddress, + safeMasterAddress, + }, + safe, + web3, + } = core; + const safeAddress = await safe.predictAddress(account, { nonce }); + let safeSdk; + + return ( + // Deploy manually a Safe + web3.eth + .sendTransaction({ + from: account.address, + to: proxyFactoryAddress, + value: 0, + data: proxyFactory.methods + .createProxyWithNonce( + safeMasterAddress, + safeMaster.methods + .setup( + [account.address], + 1, + ZERO_ADDRESS, + '0x', + fallbackHandlerAddress, + ZERO_ADDRESS, + 0, + ZERO_ADDRESS, + ) + .encodeABI(), + nonce, + ) + .encodeABI(), + }) + // Instantiate deployed Safe + .then(async () => { + safeSdk = await safe.getSafeSdk(account, { safeAddress }); + }) + // Prepare transaction for Safe to be signed up + .then(() => + safeSdk.createTransaction({ + safeTransactionData: { + to: hubAddress, + value: 0, + data: hub.methods.signup().encodeABI(), + }, + }), + ) + .then((safeTx) => safeSdk.signTransaction(safeTx)) + // Execute manually the transaction + .then((signedSafeTx) => + web3.eth.sendTransaction({ + from: account.address, + to: safeAddress, + value: 0, + data: getSafeContract(web3, safeAddress) + .methods.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(), + ) + .encodeABI(), + }), + ) + .then(() => safeAddress) + ); +} diff --git a/test/helpers/setupWeb3.js b/test/helpers/setupWeb3.js new file mode 100644 index 00000000..3ced3f13 --- /dev/null +++ b/test/helpers/setupWeb3.js @@ -0,0 +1,17 @@ +import Web3 from 'web3'; +import HDWalletProvider from '@truffle/hdwallet-provider'; + +import privateKeys from './accounts.json'; + +export default function setupWeb3() { + const provider = new HDWalletProvider({ + privateKeys, + providerOrUrl: process.env.RPC_URL, + }); + const web3 = new Web3(provider); + + return { + web3, + provider, + }; +} diff --git a/test/helpers/transactions.js b/test/helpers/transactions.js index 0038557c..c7c33ef9 100644 --- a/test/helpers/transactions.js +++ b/test/helpers/transactions.js @@ -2,117 +2,14 @@ const Safe = require('@circles/safe-contracts/build/contracts/GnosisSafe.json'); const ProxyFactory = require('@circles/safe-contracts/build/contracts/ProxyFactory.json'); import { ZERO_ADDRESS } from '~/common/constants'; -import web3 from './web3'; -import getTrustConnection from './getTrustConnection'; -import isContractDeployed from './isContractDeployed'; -import { formatTypedData, signTypedData } from './typedData'; -const SAFE_DEPLOYMENT_GAS = web3.utils.toWei('0.01', 'ether'); -let counter = 0; - -export async function fundSafe(account, safeAddress) { - // Fund deployment (we don't want to wait to have enough trust connections) - return await web3.eth.sendTransaction({ - from: account.address, - to: safeAddress, - value: SAFE_DEPLOYMENT_GAS, - }); -} - -export async function deploySafe(core, account) { - counter += 1; - - const nonce = parseInt(`${counter}${Math.round(Math.random() * 10000)}`, 10); - - const safeAddress = await core.safe.prepareDeploy(account, { - nonce, - }); - - await fundSafe(account, safeAddress); - - await core.safe.deploy(account, { - safeAddress, - }); - - await core.utils.loop( - () => web3.eth.getCode(safeAddress), - isContractDeployed, - { - label: `Wait until Safe ${safeAddress} got deployed`, - }, - ); - - return safeAddress; -} - -export async function deployToken(core, account, userOptions) { - await core.token.deploy(account, userOptions); - - const tokenAddress = await core.utils.loop( - () => { - return core.token.getAddress(account, userOptions); - }, - (address) => { - return address !== ZERO_ADDRESS; - }, - { - label: `Wait until token for safe ${userOptions.safeAddress} is deployed`, - }, - ); - - return tokenAddress; -} - -export async function deploySafeAndToken(core, account) { - const safeAddress = await deploySafe(core, account); - const tokenAddress = await deployToken(core, account, { safeAddress }); - - return { - safeAddress, - tokenAddress, - }; -} - -export async function addTrustConnection(core, account, userOptions) { - const transactionHash = await core.trust.addConnection(account, userOptions); - - await core.utils.loop( - () => { - return getTrustConnection( - core, - account, - userOptions.canSendTo, - userOptions.user, - ); - }, - (isReady) => isReady, - { - label: `Wait for trust connection between ${userOptions.canSendTo} and ${userOptions.user} to show up in the Graph`, - }, - ); - - return transactionHash; -} - -export async function addSafeOwner(core, account, userOptions) { - const transactionHash = await core.safe.addOwner(account, userOptions); - - await core.utils.loop( - () => { - return core.safe.getOwners(account, { - safeAddress: userOptions.safeAddress, - }); - }, - (owners) => { - return owners.includes(userOptions.ownerAddress); - }, - { label: 'Wait for newly added address to be listed as Safe owner' }, - ); - - return transactionHash; -} - -const signAndSendRawTransaction = async (account, to, data, gas = 10000000) => { +const signAndSendRawTransaction = async ( + web3, + account, + to, + data, + gas = 10000000, +) => { const nonce = await web3.eth.getTransactionCount(account.address, 'pending'); const payload = { nonce, @@ -138,7 +35,7 @@ const signAndSendRawTransaction = async (account, to, data, gas = 10000000) => { return web3.eth.sendSignedTransaction(rawTransaction); }; -const createSafeWithProxy = async (proxy, safe, owner) => { +const createSafeWithProxy = async (web3, proxy, safe, owner) => { const proxyData = safe.methods .setup( [owner.address], @@ -157,6 +54,7 @@ const createSafeWithProxy = async (proxy, safe, owner) => { .encodeABI(); const tx = await signAndSendRawTransaction( + web3, owner, proxy.options.address, data, @@ -169,7 +67,7 @@ const createSafeWithProxy = async (proxy, safe, owner) => { return new web3.eth.Contract(Safe.abi, userSafeAddress); }; -export async function deployCRCVersionSafe(owner) { +export async function deployCRCVersionSafe(web3, owner) { // Get the CRC version contracts contract const safeContract = new web3.eth.Contract( Safe.abi, @@ -180,61 +78,10 @@ export async function deployCRCVersionSafe(owner) { process.env.PROXY_FACTORY_ADDRESS_CRC, ); - return await createSafeWithProxy(proxyFactoryContract, safeContract, owner); -} - -async function execTransaction( - account, - safeInstance, - { to, from, value = 0, txData }, -) { - const operation = 0; // CALL - const safeTxGas = '1239215'; // based on data // @TODO: CHANGE - const baseGas = '1239215'; // general transaction // @TODO: CHANGE - const gasPrice = 0; // no refund - const gasToken = ZERO_ADDRESS; // Paying in Eth - const refundReceiver = ZERO_ADDRESS; - const nonce = await safeInstance.methods.nonce().call(); - const safeAddress = safeInstance.options.address; - - const typedData = formatTypedData({ - to, - value, - txData, - operation, - safeTxGas, - baseGas, - gasPrice, - gasToken, - refundReceiver, - nonce, - verifyingContract: safeAddress, - }); - const signature = signTypedData(account.privateKey, typedData); - const signatures = signature; - - return await safeInstance.methods - .execTransaction( - to, - value, - txData, - operation, - safeTxGas, - baseGas, - gasPrice, - gasToken, - refundReceiver, - signatures, - ) - .send({ from, gas: '10000000' }); // @TODO: '1266349' ? Need to change gas, safeTxGase, baseGas -} - -export async function deployCRCVersionToken(web3, account, safe, hub) { - await execTransaction(account, safe, { - to: hub.options.address, - from: account.address, - txData: hub.methods.signup().encodeABI(), - }); - - return await hub.methods.userToToken(safe.options.address).call(); + return await createSafeWithProxy( + web3, + proxyFactoryContract, + safeContract, + owner, + ); } diff --git a/test/helpers/web3.js b/test/helpers/web3.js deleted file mode 100644 index 8bfcafdf..00000000 --- a/test/helpers/web3.js +++ /dev/null @@ -1,13 +0,0 @@ -import Web3 from 'web3'; -import HDWalletProvider from '@truffle/hdwallet-provider'; - -import privateKeys from './accounts.json'; - -export const provider = new HDWalletProvider({ - privateKeys, - providerOrUrl: process.env.RPC_URL || 'http://localhost:8545', -}); - -const web3 = new Web3(provider); - -export default web3; diff --git a/test/safe.test.js b/test/safe.test.js index 2906d796..ddd09b33 100644 --- a/test/safe.test.js +++ b/test/safe.test.js @@ -1,24 +1,60 @@ import { SAFE_LAST_VERSION, SAFE_CRC_VERSION } from '~/common/constants'; -import { SafeDeployedError } from '~/common/error'; +import { SafeAlreadyDeployedError, SafeNotTrustError } from '~/common/error'; import createCore from './helpers/core'; -import accounts from './helpers/accounts'; -import web3 from './helpers/web3'; +import getAccounts from './helpers/getAccounts'; import { deployCRCVersionSafe } from './helpers/transactions'; import generateSaltNonce from './helpers/generateSaltNonce'; +import setupAccount from './helpers/setupAccount'; +import setupWeb3 from './helpers/setupWeb3'; +// Temporary method for adding trusts with new relayer since trusts functionality is not yet migrated +import addTrust from './helpers/addTrust'; describe('Safe', () => { - const core = createCore(); + const { web3, provider } = setupWeb3(); + const core = createCore(web3); + const accounts = getAccounts(web3); + let predeployedSafes; + + afterAll(() => provider.engine.stop()); + beforeAll(async () => { + // Predeploy manually 3 accounts because of the minimun trusting requirement + predeployedSafes = await Promise.all( + Array.from(Array(3).keys()).map((index) => + setupAccount( + { account: accounts[index + 1], nonce: generateSaltNonce() }, + core, + ), + ), + ); + }); describe('when a new Safe gets manually created', () => { const nonce = generateSaltNonce(); let safeAddress; it('should deploy a Safe successfully', async () => { - safeAddress = await core.safe.deploySafe(accounts[0], { nonce }); + safeAddress = await core.safe.predictAddress(accounts[0], { nonce }); expect(web3.utils.isAddress(safeAddress)).toBe(true); + // Let's make the trust connections needed to get the Safe deployed + await Promise.all( + predeployedSafes.map((predeployedAddress, index) => + addTrust( + { + account: accounts[index + 1], + safeAddress: predeployedAddress, + safeAddressToTrust: safeAddress, + limitPercentage: 50, + }, + core, + ), + ), + ); + + await core.safe.deploySafe(accounts[0], { nonce }); + return core.safe .isDeployed(accounts[0], { safeAddress }) .then((isDeployed) => expect(isDeployed).toBe(true)); @@ -39,15 +75,39 @@ describe('Safe', () => { it('should throw error when trying to deploy twice with same nonce', () => expect(() => core.safe.deploySafe(accounts[0], { nonce }), - ).rejects.toThrow(SafeDeployedError)); + ).rejects.toThrow(SafeAlreadyDeployedError)); + + it('should throw error when trying to deploy without minimun required trusts', () => + expect(() => + core.safe.deploySafe(accounts[0], { nonce: generateSaltNonce() }), + ).rejects.toThrow(SafeNotTrustError)); }); describe('when managing the owners of a Safe', () => { let safeAddress; beforeAll(async () => { - safeAddress = await core.safe.deploySafe(accounts[0], { - nonce: generateSaltNonce(), + const nonce = generateSaltNonce(); + + safeAddress = await core.safe.predictAddress(accounts[0], { nonce }); + + // Let's make the trust connections needed to get the Safe deployed + await Promise.all( + predeployedSafes.map((predeployedAddress, index) => + addTrust( + { + account: accounts[index + 1], + safeAddress: predeployedAddress, + safeAddressToTrust: safeAddress, + limitPercentage: 50, + }, + core, + ), + ), + ); + + await core.safe.deploySafe(accounts[0], { + nonce, }); }); @@ -99,7 +159,10 @@ describe('Safe', () => { beforeAll(async () => { // Deploy a Safe with the CRC version (v1.1.1+Circles) - const CRCSafeContractInstance = await deployCRCVersionSafe(CRCSafeOwner); + const CRCSafeContractInstance = await deployCRCVersionSafe( + web3, + CRCSafeOwner, + ); CRCSafeAddress = CRCSafeContractInstance.options.address; });