diff --git a/packages/aragon-cli/src/commands/apm_cmds/publish.js b/packages/aragon-cli/src/commands/apm_cmds/publish.js index f626df2b7..6ea8ff467 100644 --- a/packages/aragon-cli/src/commands/apm_cmds/publish.js +++ b/packages/aragon-cli/src/commands/apm_cmds/publish.js @@ -16,8 +16,6 @@ const startIPFS = require('../ipfs_cmds/start') const propagateIPFS = require('../ipfs_cmds/propagate') const execTask = require('../dao_cmds/utils/execHandler').task const listrOpts = require('@aragon/cli-utils/src/helpers/listr-options') -const { map, filter, first } = require('rxjs/operators') -const { addressesEqual } = require('../../util') const { prepareFilesForPublishing, @@ -548,28 +546,11 @@ exports.runPublishTask = ({ enabled: () => !onlyArtifacts, task: async (ctx, task) => { try { - const getTransactionPath = async wrapper => { - // Wait for app info to load - await wrapper.apps - .pipe( - map(apps => - apps.find(app => - addressesEqual(app.proxyAddress, proxyAddress) - ) - ), - filter(app => app), - first() - ) - .toPromise() - - return wrapper.getTransactionPath( - proxyAddress, - methodName, - params - ) - } - - return execTask(dao, getTransactionPath, { + return execTask({ + dao, + app: proxyAddress, + method: methodName, + params, ipfsCheck: false, reporter, gasPrice, diff --git a/packages/aragon-cli/src/commands/dao_cmds/acl_cmds/utils/aclExecHandler.js b/packages/aragon-cli/src/commands/dao_cmds/acl_cmds/utils/aclExecHandler.js index 3911e3738..e64fa5f9e 100644 --- a/packages/aragon-cli/src/commands/dao_cmds/acl_cmds/utils/aclExecHandler.js +++ b/packages/aragon-cli/src/commands/dao_cmds/acl_cmds/utils/aclExecHandler.js @@ -1,7 +1,6 @@ const execHandler = require('../../utils/execHandler').handler const { keccak256 } = require('web3').utils -const { map, filter, first } = require('rxjs/operators') -const { addressesEqual } = require('../../../../util') +const { ensureWeb3 } = require('../../../../helpers/web3-fallback') module.exports = async function( dao, @@ -9,34 +8,22 @@ module.exports = async function( params, { reporter, apm, network, gasPrice, wsProvider, role, silent, debug } ) { - const getTransactionPath = async wrapper => { - const aclAddr = wrapper.aclProxy.address - // Wait for app info to load - await wrapper.apps - .pipe( - map(apps => - apps.find(app => addressesEqual(app.proxyAddress, aclAddr)) - ), - filter(app => app), - first() - ) - .toPromise() + const web3 = await ensureWeb3(network) + const daoInstance = new web3.eth.Contract( + require('@aragon/os/build/contracts/Kernel').abi, + dao + ) + const aclAddress = await daoInstance.methods.acl().call() - let processedParams + const processedParams = role.startsWith('0x') + ? params + : params.map(param => (param === role ? keccak256(role) : param)) - // If the provided role is its name, the name is hashed - // TODO: Get role bytes from app artifacts - if (role.startsWith('0x')) { - processedParams = params - } else { - processedParams = params.map(param => - param === role ? '0x' + keccak256(role) : param - ) - } - - return wrapper.getACLTransactionPath(method, processedParams) - } - return execHandler(dao, getTransactionPath, { + return execHandler({ + dao, + app: aclAddress, + method, + params: processedParams, ipfsCheck: false, reporter, gasPrice, diff --git a/packages/aragon-cli/src/commands/dao_cmds/acl_cmds/view.js b/packages/aragon-cli/src/commands/dao_cmds/acl_cmds/view.js index bacb70af1..5bb45dc81 100644 --- a/packages/aragon-cli/src/commands/dao_cmds/acl_cmds/view.js +++ b/packages/aragon-cli/src/commands/dao_cmds/acl_cmds/view.js @@ -1,4 +1,4 @@ -import initAragonJS from '../utils/aragonjs-wrapper' +import { initAragonJS, getApps } from '../../../helpers/aragonjs-wrapper' const chalk = require('chalk') const TaskList = require('listr') const daoArg = require('../utils/daoArg') @@ -30,7 +30,7 @@ const printAppName = (appId, addr) => { if (addr === NO_MANAGER) return NO_MANAGER_TEXT return knownApps[appId] ? `${knownApps[appId].split('.')[0]} (${addr.slice(0, 6)})` - : addr.slice(0, 16) + '...' + : `${addr.slice(0, 8)}..${addr.slice(-6)}` } const appFromProxyAddress = (proxyAddress, apps) => { @@ -93,7 +93,7 @@ exports.handler = async function({ task.output = `Fetching permissions for ${dao}...` return new Promise((resolve, reject) => { - const resolveIfReady = () => { + const resolveIfReady = async () => { if (ctx.acl && ctx.apps) { resolve() } @@ -101,23 +101,24 @@ exports.handler = async function({ initAragonJS(dao, apm['ens-registry'], { provider: wsProvider || web3.currentProvider, + ipfsConf: apm.ipfs, onPermissions: permissions => { ctx.acl = permissions resolveIfReady() }, - onApps: apps => { - ctx.apps = apps - resolveIfReady() - }, onDaoAddress: addr => { ctx.daoAddress = addr }, - onError: err => reject(err), - }).catch(err => { - reporter.error('Error inspecting DAO') - reporter.debug(err) - process.exit(1) }) + .then(async wrapper => { + ctx.apps = await getApps(wrapper) + resolveIfReady() + }) + .catch(err => { + reporter.error('Error inspecting DAO') + reporter.debug(err) + process.exit(1) + }) }) }, }, diff --git a/packages/aragon-cli/src/commands/dao_cmds/act.js b/packages/aragon-cli/src/commands/dao_cmds/act.js index 94898dae8..532b5489e 100644 --- a/packages/aragon-cli/src/commands/dao_cmds/act.js +++ b/packages/aragon-cli/src/commands/dao_cmds/act.js @@ -2,12 +2,7 @@ const web3 = require('web3') const execHandler = require('./utils/execHandler').handler const getAppKernel = require('./utils/app-kernel') const { ensureWeb3 } = require('../../helpers/web3-fallback') -const { - parseArgumentStringIfPossible, - ZERO_ADDRESS, - addressesEqual, -} = require('../../util') -const { map, filter, first } = require('rxjs/operators') +const { parseArgumentStringIfPossible, ZERO_ADDRESS } = require('../../util') const EXECUTE_FUNCTION_NAME = 'execute' @@ -83,29 +78,13 @@ exports.handler = async function({ } const weiAmount = web3.utils.toWei(ethValue) - const fnArgs = [target, weiAmount, encodeCalldata(signature, callArgs)] - const getTransactionPath = async wrapper => { - // Wait for agent info to load - await wrapper.apps - .pipe( - map(apps => - apps.find(app => addressesEqual(app.proxyAddress, agentAddress)) - ), - filter(app => app), - first() - ) - .toPromise() - - return wrapper.getTransactionPath( - agentAddress, - EXECUTE_FUNCTION_NAME, - fnArgs - ) - } - - return execHandler(dao, getTransactionPath, { + return execHandler({ + dao, + app: agentAddress, + method: EXECUTE_FUNCTION_NAME, + params: fnArgs, ipfsCheck: true, reporter, apm, diff --git a/packages/aragon-cli/src/commands/dao_cmds/apps.js b/packages/aragon-cli/src/commands/dao_cmds/apps.js index ac1b0961e..9b6060fa7 100644 --- a/packages/aragon-cli/src/commands/dao_cmds/apps.js +++ b/packages/aragon-cli/src/commands/dao_cmds/apps.js @@ -1,4 +1,4 @@ -import initAragonJS from './utils/aragonjs-wrapper' +import { initAragonJS, getApps } from '../../helpers/aragonjs-wrapper' const TaskList = require('listr') const chalk = require('chalk') const daoArg = require('./utils/daoArg') @@ -56,26 +56,25 @@ exports.handler = async function({ [ { title: 'Inspecting DAO', - task: (ctx, task) => { + task: async (ctx, task) => { task.output = `Fetching apps for ${dao}...` + const { 'ens-registry': ensRegistry, ipfs } = apmOptions - return new Promise((resolve, reject) => { - initAragonJS(dao, apmOptions['ens-registry'], { + try { + const wrapper = await initAragonJS(dao, ensRegistry, { + ipfsConf: ipfs, provider: wsProvider || web3.currentProvider, - onApps: apps => { - ctx.apps = apps - resolve() - }, onDaoAddress: addr => { ctx.daoAddress = addr }, - onError: err => reject(err), - }).catch(err => { - reporter.error('Error inspecting DAO apps') - reporter.debug(err) - process.exit(1) }) - }) + + ctx.apps = await getApps(wrapper) + } catch (err) { + reporter.error('Error inspecting DAO apps') + reporter.debug(err) + process.exit(1) + } }, }, { diff --git a/packages/aragon-cli/src/commands/dao_cmds/exec.js b/packages/aragon-cli/src/commands/dao_cmds/exec.js index 798829638..51b79fe3d 100644 --- a/packages/aragon-cli/src/commands/dao_cmds/exec.js +++ b/packages/aragon-cli/src/commands/dao_cmds/exec.js @@ -1,7 +1,6 @@ const execHandler = require('./utils/execHandler').handler const daoArg = require('./utils/daoArg') -const { parseArgumentStringIfPossible, addressesEqual } = require('../../util') -const { map, filter, first } = require('rxjs/operators') +const { parseArgumentStringIfPossible } = require('../../util') exports.command = 'exec [fn-args..]' @@ -32,26 +31,11 @@ exports.handler = async function({ fnArgs, wsProvider, }) { - // TODO (daniel) refactor ConsoleReporter so we can do reporter.debug instead - if (global.DEBUG_MODE) console.log('fn-args before parsing', fnArgs) - fnArgs = fnArgs.map(parseArgumentStringIfPossible) - if (global.DEBUG_MODE) console.log('fn-args after parsing', fnArgs) - - const getTransactionPath = async wrapper => { - // Wait for app info to load - await wrapper.apps - .pipe( - map(apps => - apps.find(app => addressesEqual(app.proxyAddress, proxyAddress)) - ), - filter(app => app), - first() - ) - .toPromise() - - return wrapper.getTransactionPath(proxyAddress, fn, fnArgs) - } - return execHandler(dao, getTransactionPath, { + return execHandler({ + dao, + app: proxyAddress, + method: fn, + params: fnArgs.map(parseArgumentStringIfPossible), ipfsCheck: true, reporter, apm, diff --git a/packages/aragon-cli/src/commands/dao_cmds/install.js b/packages/aragon-cli/src/commands/dao_cmds/install.js index 496c8ad0a..33faa3531 100644 --- a/packages/aragon-cli/src/commands/dao_cmds/install.js +++ b/packages/aragon-cli/src/commands/dao_cmds/install.js @@ -1,5 +1,5 @@ const execTask = require('./utils/execHandler').task -const { resolveEnsDomain } = require('./utils/aragonjs-wrapper') +const { resolveEnsDomain } = require('../../helpers/aragonjs-wrapper') const TaskList = require('listr') const daoArg = require('./utils/daoArg') const { ensureWeb3 } = require('../../helpers/web3-fallback') @@ -117,17 +117,18 @@ exports.task = async ({ ctx.notInitialized = true } - const getTransactionPath = wrapper => { - const fnArgs = [ - ctx.repo.appId, - ctx.repo.contractAddress, - initPayload, - false, - ] - return wrapper.getTransactionPath(dao, 'newAppInstance', fnArgs) - } + const fnArgs = [ + ctx.repo.appId, + ctx.repo.contractAddress, + initPayload, + false, + ] - return execTask(dao, getTransactionPath, { + return execTask({ + dao, + app: dao, + method: 'newAppInstance', + params: fnArgs, ipfsCheck: false, reporter, gasPrice, @@ -187,15 +188,20 @@ exports.task = async ({ if (!ctx.accounts) { ctx.accounts = await web3.eth.getAccounts() } + const daoInstance = new web3.eth.Contract( + kernelAbi, + dao + ) + const aclAddress = await daoInstance.methods.acl().call() return Promise.all( permissions.map(params => { - const getTransactionPath = async wrapper => { - return wrapper.getACLTransactionPath('createPermission', params) - } - return ( - execTask(dao, getTransactionPath, { + execTask({ + dao, + app: aclAddress, + method: 'createPermission', + params, reporter, gasPrice, apm: apmOptions, diff --git a/packages/aragon-cli/src/commands/dao_cmds/upgrade.js b/packages/aragon-cli/src/commands/dao_cmds/upgrade.js index 010947533..e95074842 100644 --- a/packages/aragon-cli/src/commands/dao_cmds/upgrade.js +++ b/packages/aragon-cli/src/commands/dao_cmds/upgrade.js @@ -1,5 +1,5 @@ const execTask = require('./utils/execHandler').task -const { resolveEnsDomain } = require('./utils/aragonjs-wrapper') +const { resolveEnsDomain } = require('../../helpers/aragonjs-wrapper') const TaskList = require('listr') const daoArg = require('./utils/daoArg') const { ensureWeb3 } = require('../../helpers/web3-fallback') @@ -65,16 +65,11 @@ exports.task = async ({ .APP_BASES_NAMESPACE() .call() - const getTransactionPath = wrapper => { - const fnArgs = [ - basesNamespace, - ctx.repo.appId, - ctx.repo.contractAddress, - ] - return wrapper.getTransactionPath(dao, 'setApp', fnArgs) - } - - return execTask(dao, getTransactionPath, { + return execTask({ + dao, + app: dao, + method: 'setApp', + params: [basesNamespace, ctx.repo.appId, ctx.repo.contractAddress], ipfsCheck: false, reporter, gasPrice, diff --git a/packages/aragon-cli/src/commands/dao_cmds/utils/aragonjs-wrapper.js b/packages/aragon-cli/src/commands/dao_cmds/utils/aragonjs-wrapper.js deleted file mode 100644 index 7048ed7f1..000000000 --- a/packages/aragon-cli/src/commands/dao_cmds/utils/aragonjs-wrapper.js +++ /dev/null @@ -1,107 +0,0 @@ -import Aragon, { ensResolve } from '@aragon/wrapper' - -const noop = () => {} - -// Subscribe to wrapper's observables -const subscribe = ( - wrapper, - { onApps, onForwarders, onTransaction, onPermissions } -) => { - const { apps, forwarders, transactions, permissions } = wrapper - - const subscriptions = { - apps: apps.subscribe(onApps), - connectedApp: null, - forwarders: forwarders.subscribe(onForwarders), - transactions: transactions.subscribe(onTransaction), - permissions: permissions.subscribe(onPermissions), - } - - return subscriptions -} - -export async function resolveEnsDomain(domain, opts) { - try { - return await ensResolve(domain, opts) - } catch (err) { - if (err.message === 'ENS name not defined.') { - return '' - } - throw err - } -} - -const initWrapper = async ( - dao, - ensRegistryAddress, - { - provider, - gasPrice, - accounts = '', - walletProvider = null, - ipfsConf = {}, - onError = noop, - onApps = noop, - onForwarders = noop, - onTransaction = noop, - onDaoAddress = noop, - onPermissions = noop, - } = {} -) => { - const isDomain = dao => /[a-z0-9]+\.eth/.test(dao) - - const daoAddress = isDomain(dao) - ? await resolveEnsDomain(dao, { - provider, - registryAddress: ensRegistryAddress, - }) - : dao - - if (!daoAddress) { - onError(new Error('The provided DAO address is invalid')) - return - } - - onDaoAddress(daoAddress) - - // TODO: don't reinitialize if cached - - const wrapper = new Aragon(daoAddress, { - provider, - defaultGasPriceFn: () => gasPrice, - apm: { - ipfs: ipfsConf, - ensRegistryAddress, - }, - }) - - try { - await wrapper.init({ accounts: { providedAccounts: accounts } }) - } catch (err) { - if (err.message === 'connection not open') { - onError( - new Error('The wrapper can not be initialized without a connection') - ) - return - } - throw err - } - - const subscriptions = subscribe( - wrapper, - { onApps, onForwarders, onTransaction, onPermissions }, - { ipfsConf } - ) - - wrapper.cancel = () => { - Object.values(subscriptions).forEach(subscription => { - if (subscription) { - subscription.unsubscribe() - } - }) - } - - return wrapper -} - -export default initWrapper diff --git a/packages/aragon-cli/src/commands/dao_cmds/utils/execHandler.js b/packages/aragon-cli/src/commands/dao_cmds/utils/execHandler.js index 373e93eee..665c057bb 100644 --- a/packages/aragon-cli/src/commands/dao_cmds/utils/execHandler.js +++ b/packages/aragon-cli/src/commands/dao_cmds/utils/execHandler.js @@ -1,15 +1,46 @@ -import initAragonJS from './aragonjs-wrapper' +import { + initAragonJS, + getTransactionPath, +} from '../../../helpers/aragonjs-wrapper' const chalk = require('chalk') const startIPFS = require('../../ipfs_cmds/start') const TaskList = require('listr') const { ensureWeb3 } = require('../../../helpers/web3-fallback') const listrOpts = require('@aragon/cli-utils/src/helpers/listr-options') -exports.task = async function( +/** + * Return a task list for executing a method on a + * DAO's app. + * + * @param {Object} params Parameters + * @param {string} params.dao DAO name or address + * @param {string} params.app App address + * @param {string} params.method Method name + * @param {Array<*>} params.params Method parameters + * @param {boolean} params.ipfsCheck Check if IPFS is running + * @param {Object} params.reporter Reporter + * @param {Object} params.apm APM config + * @param {Object} params.web3 Web3 instance + * @param {Object} params.wsProvider Ethereum provider + * @param {string} params.gasPrice Gas price + * @param {boolean} params.silent Silent task + * @param {boolean} params.debug Debug mode + * @returns {Promise} Execution task list + */ +exports.task = async function({ dao, - getTransactionPath, - { ipfsCheck, reporter, apm, web3, wsProvider, gasPrice, silent, debug } -) { + app, + method, + params, + ipfsCheck, + reporter, + apm, + web3, + wsProvider, + gasPrice, + silent, + debug, +}) { const accounts = await web3.eth.getAccounts() return new TaskList( [ @@ -24,41 +55,25 @@ exports.task = async function( task: async (ctx, task) => { task.output = `Fetching DAO at ${dao}...` - return new Promise((resolve, reject) => { - let wrapper, appsLoaded - - const tryFindTransactionPath = async () => { - if (appsLoaded && wrapper && !ctx.transactionPath) { - try { - ctx.transactionPath = await getTransactionPath(wrapper) - resolve() - } catch (e) { - reject(e) - } - } - } - - initAragonJS(dao, apm['ens-registry'], { + try { + const wrapper = await initAragonJS(dao, apm['ens-registry'], { ipfsConf: apm.ipfs, gasPrice, provider: wsProvider || web3.currentProvider, accounts, - onApps: async apps => { - appsLoaded = true - await tryFindTransactionPath() - }, - onError: err => reject(err), }) - .then(async initializedWrapper => { - wrapper = initializedWrapper - await tryFindTransactionPath() - }) - .catch(err => { - reporter.error('Error inspecting DAO') - reporter.debug(err) - process.exit(1) - }) - }) + + ctx.transactionPath = await getTransactionPath( + app, + method, + params, + wrapper + ) + } catch (err) { + reporter.error('Error inspecting DAO') + reporter.debug(err) + process.exit(1) + } }, }, { @@ -80,13 +95,31 @@ exports.task = async function( ) } -exports.handler = async function(dao, getTransactionPath, args) { +/** + * Execute a method on a DAO's app. + * + * @param {Object} args Parameters + * @param {string} args.dao DAO name or address + * @param {string} args.app App address + * @param {string} args.method Method name + * @param {Array<*>} args.params Method parameters + * @param {boolean} args.ipfsCheck Check if IPFS is running + * @param {Object} args.reporter Reporter + * @param {Object} args.apm APM config + * @param {Object} args.web3 Web3 instance + * @param {Object} args.wsProvider Ethereum provider + * @param {string} args.gasPrice Gas price + * @param {boolean} args.silent Silent task + * @param {boolean} args.debug Debug mode + * @returns {Promise} Execution promise + */ +exports.handler = async function(args) { args = { ...args, web3: await ensureWeb3(args.network), } - const tasks = await exports.task(dao, getTransactionPath, args) + const tasks = await exports.task(args) return tasks.run().then(ctx => { args.reporter.success( diff --git a/packages/aragon-cli/src/helpers/aragonjs-wrapper.js b/packages/aragon-cli/src/helpers/aragonjs-wrapper.js new file mode 100644 index 000000000..552ba41aa --- /dev/null +++ b/packages/aragon-cli/src/helpers/aragonjs-wrapper.js @@ -0,0 +1,173 @@ +import Aragon, { ensResolve } from '@aragon/wrapper' +import { takeWhile, map, filter, first, defaultIfEmpty } from 'rxjs/operators' +import { addressesEqual } from '../util' +const noop = () => {} + +// Subscribe to wrapper's observables +const subscribe = ( + wrapper, + { onApps, onForwarders, onTransaction, onPermissions } +) => { + const { apps, forwarders, transactions, permissions } = wrapper + + const subscriptions = { + apps: apps.subscribe(onApps), + connectedApp: null, + forwarders: forwarders.subscribe(onForwarders), + transactions: transactions.subscribe(onTransaction), + permissions: permissions.subscribe(onPermissions), + } + + return subscriptions +} + +/** + * Resolve an ens domain + * + * @param {string} domain Domain + * @param {*} opts Options + * @returns {Promise} Resolved ens domain + */ +export async function resolveEnsDomain(domain, opts) { + try { + return await ensResolve(domain, opts) + } catch (err) { + if (err.message === 'ENS name not defined.') { + return '' + } + throw err + } +} + +/** + * Initialize the Aragon.js wrapper and subscribe to the `apps`, + * `forwarders`, `transactions` and `permissions` observables. + * + * @param {string} dao DAO address + * @param {string} ensRegistryAddress ENS Registry address + * @param {Object} options Options + * @param {Object} options.provider Eth provider + * @param {string} options.gasPrice Gas price + * @param {string} options.accounts Eth accounts + * @param {Object} options.ipfsConf IPFS configuration + * @param {function} options.onApps Apps callback + * @param {function} options.onForwarders Forwarders callback + * @param {function} options.onTransaction Transaction callback + * @param {function} options.onDaoAddress Dao address callback + * @param {function} options.onPermissions Permissions callback + * @returns {Promise} Aragon wrapper with an added `cancel` function + */ +export async function initAragonJS( + dao, + ensRegistryAddress, + { + provider, + gasPrice, + accounts = '', + ipfsConf = {}, + onApps = noop, + onForwarders = noop, + onTransaction = noop, + onDaoAddress = noop, + onPermissions = noop, + } = {} +) { + const isDomain = dao => /[a-z0-9]+\.eth/.test(dao) + + const daoAddress = isDomain(dao) + ? await resolveEnsDomain(dao, { + provider, + registryAddress: ensRegistryAddress, + }) + : dao + + if (!daoAddress) { + throw new Error('The provided DAO address is invalid') + } + + onDaoAddress(daoAddress) + + // TODO: don't reinitialize if cached + const wrapper = new Aragon(daoAddress, { + provider, + defaultGasPriceFn: () => gasPrice, + apm: { + ipfs: ipfsConf, + ensRegistryAddress, + }, + }) + + try { + await wrapper.init({ accounts: { providedAccounts: accounts } }) + } catch (err) { + if (err.message === 'connection not open') { + throw new Error('The wrapper cannot be initialized without a connection') + } + throw err + } + + const subscriptions = subscribe( + wrapper, + { onApps, onForwarders, onTransaction, onPermissions }, + { ipfsConf } + ) + + wrapper.cancel = () => { + Object.values(subscriptions).forEach(subscription => { + if (subscription) { + subscription.unsubscribe() + } + }) + } + + return wrapper +} + +/** + * Return a list of all installed apps. + * @param {Aragon} wrapper Aragon wrapper + * @returns {Promise} Installed apps + */ +export async function getApps(wrapper) { + return ( + wrapper.apps + // If the app list contains a single app, wait for more + .pipe(takeWhile(apps => apps.length <= 1, true)) + .toPromise() + ) +} + +/** + * Get transaction path on an Aragon app for `method` with `params` + * as parameters. Wait for apps to load before calling + * wrapper's `getTransactionPath`. If app is the ACL, call + * `getACLTransactionPath`. + * + * @param {string} appAddress App address + * @param {string} method Method name + * @param {Array<*>} params Method params + * @param {Aragon} wrapper Aragon wrapper + * @returns {Promise} Transaction path + */ +export async function getTransactionPath(appAddress, method, params, wrapper) { + // Wait for app info to load + const app = await wrapper.apps + .pipe( + // If the app list contains a single app, wait for more + takeWhile(apps => apps.length <= 1, true), + map(apps => + apps.find(app => addressesEqual(appAddress, app.proxyAddress)) + ), + filter(app => app), + defaultIfEmpty(null), // If app is not found, default to null + first() + ) + .toPromise() + + if (!app) throw new Error(`Can't find app ${appAddress}.`) + + // If app is the ACL, call getACLTransactionPath + return appAddress === wrapper.aclProxy.address + ? wrapper.getACLTransactionPath(method, params) + : wrapper.getTransactionPath(appAddress, method, params) +} diff --git a/packages/aragon-cli/test/helpers/aragonjs-wrapper.test.js b/packages/aragon-cli/test/helpers/aragonjs-wrapper.test.js new file mode 100644 index 000000000..6d08f418b --- /dev/null +++ b/packages/aragon-cli/test/helpers/aragonjs-wrapper.test.js @@ -0,0 +1,140 @@ +import test from 'ava' +import { from } from 'rxjs' +import sinon from 'sinon' +import proxyquire from 'proxyquire' +import { getTransactionPath, getApps } from '../../src/helpers/aragonjs-wrapper' + +const DEFAULT_ACL = '0x15737d270F7Bc777cD38592fbD50cEF74eE2F88a' +const DEFAULT_APPS = [[ + { appId: '0x01', proxyAddress: '0xb4124cEB3451635DAcedd11767f004d8a28c6eE7' }, + { appId: '0x02', proxyAddress: '0x8401Eb5ff34cc943f096A32EF3d5113FEbE8D4Eb' }, + { appId: '0x03', proxyAddress: DEFAULT_ACL } +]] +const DEFAULT_FORWARDERS = [[{}]] +const DEFAULT_TRANSACTIONS= [[{}]] +const DEFAULT_PERMISSIONS = [[{}]] + + +function createAragonJsStub( + apps = DEFAULT_APPS, + acl = DEFAULT_ACL, + forwarders = DEFAULT_FORWARDERS, + transactions = DEFAULT_TRANSACTIONS, + permissions = DEFAULT_PERMISSIONS, + ) { + + const AragonStub = class { + constructor() { + this.getTransactionPath = sinon.stub() + this.getACLTransactionPath = sinon.stub() + + this.aclProxy = { address: acl } + } + + async init() {} + get apps() { return from(apps) } + get forwarders() { return from(forwarders) } + get transactions() { return from(transactions) } + get permissions() { return from(permissions) } + } + + AragonStub.ensResolve = sinon.stub().returns('0x8401Eb5ff34cc943f096A32EF3d5113FEbE8D4Eb') + + return AragonStub +} + + + +test('getApps returns the correct app list', async t => { + t.plan(1) + + const AragonStub = createAragonJsStub() + const wrapperStub = new AragonStub() + + t.deepEqual(await getApps(wrapperStub), DEFAULT_APPS[0]) +}) + +test('getApps waits for more elements if first list contains only 1 app', async t => { + t.plan(1) + + const apps = [ + [{ appId: '0x01' }], + [{ appId: '0x01' }, { appId: '0x02' }, { appId: '0x03' }] + ] + + const AragonStub = createAragonJsStub(apps) + const wrapperStub = new AragonStub() + + t.deepEqual(await getApps(wrapperStub), apps[1]) +}) + +test('getTransactionPath throws if DAO does not contain app', async t => { + t.plan(1) + + const AragonStub = createAragonJsStub() + const wrapperStub = new AragonStub() + + await t.throwsAsync(getTransactionPath('0x8401Eb5ff34cc943f096A32EF3d5113FEbE8D4Ec', 'method', [], wrapperStub)) + +}) + +test('getTransactionPath calls wrapper getTransactionPath by default', async t => { + t.plan(2) + + const AragonStub = createAragonJsStub() + const wrapperStub = new AragonStub() + + const app = '0xb4124cEB3451635DAcedd11767f004d8a28c6eE7' + const method = 'myMethod' + const params = ['1', '0x00001'] + + await getTransactionPath(app, method, params, wrapperStub) + + t.deepEqual(wrapperStub.getTransactionPath.args, [[app, method, params]]) + t.is(wrapperStub.getACLTransactionPath.called, false) + +}) + +test('getTransactionPath calls wrapper getACLTransactionPath if app is the ACL', async t => { + t.plan(2) + + const AragonStub = createAragonJsStub() + const wrapperStub = new AragonStub() + + const app = DEFAULT_ACL + const method = 'myAclMethod' + const params = ['2', '0x00001'] + + await getTransactionPath(app, method, params, wrapperStub) + + t.is(wrapperStub.getTransactionPath.called, false) + t.deepEqual(wrapperStub.getACLTransactionPath.args, [[method, params]]) + +}) + +test('initAragonJS returns an instance of the Aragon wrapper', async t => { + t.plan(1) + + const AragonStub = createAragonJsStub() + var { initAragonJS } = proxyquire.noCallThru().load('../../src/helpers/aragonjs-wrapper', { '@aragon/wrapper': AragonStub }) + + const wrapper = await initAragonJS('test', '') + + t.is(wrapper instanceof AragonStub, true) + +}) + +test('initAragonJS callbacks subscribe to the right observables', async t => { + t.plan(4) + + const AragonStub = createAragonJsStub() + var { initAragonJS } = proxyquire.noCallThru().load('../../src/helpers/aragonjs-wrapper', { '@aragon/wrapper': AragonStub }) + + await initAragonJS('test', '', { + onApps: apps => t.is(apps, DEFAULT_APPS[0]), + onPermissions: permissions => t.is(permissions, DEFAULT_PERMISSIONS[0]), + onForwarders: forwarders => t.is(forwarders, DEFAULT_FORWARDERS[0]), + onTransaction: transactions => t.is(transactions, DEFAULT_TRANSACTIONS[0]) + }) + +}) \ No newline at end of file