UNPKG

@aragon/wrapper

Version:

Library for Aragon client implementations

443 lines 59.7 kB
"use strict";var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");const _excluded=["appId"];Object.defineProperty(exports,"__esModule",{value:!0// force cache invalidation }),Object.defineProperty(exports,"AddressIdentityProvider",{enumerable:!0,get:function(){return _identity.AddressIdentityProvider}}),Object.defineProperty(exports,"apm",{enumerable:!0,get:function(){return _apm.default}}),exports.detectProvider=exports.default=void 0,Object.defineProperty(exports,"ensResolve",{enumerable:!0,get:function(){return _ens.resolve}}),Object.defineProperty(exports,"getRecommendedGasLimit",{enumerable:!0,get:function(){return _transactions.getRecommendedGasLimit}}),Object.defineProperty(exports,"providers",{enumerable:!0,get:function(){return _rpcMessenger.providers}});var _objectWithoutProperties2=_interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties")),_defineProperty2=_interopRequireDefault(require("@babel/runtime/helpers/defineProperty")),_rxjs=require("rxjs"),_operators=require("rxjs/operators"),_web=_interopRequireDefault(require("web3")),_web3Utils=require("web3-utils"),_dotProp=_interopRequireDefault(require("dot-prop")),_rpcMessenger=_interopRequireWildcard(require("@aragon/rpc-messenger")),handlers=_interopRequireWildcard(require("./rpc/handlers")),_apps=_interopRequireWildcard(require("./apps")),_cache=_interopRequireDefault(require("./cache")),_apm=_interopRequireWildcard(require("./core/apm")),_repo=require("./core/apm/repo"),_aragonOS=require("./core/aragonOS"),_kernel=require("./core/aragonOS/kernel"),_configuration=require("./configuration"),configurationKeys=_interopRequireWildcard(require("./configuration/keys")),_ens=_interopRequireWildcard(require("./ens")),_identity=require("./identity"),_interfaces=require("./interfaces"),_radspec=require("./radspec"),_utils=require("./utils"),_abi=require("./utils/abi"),_apps2=require("./utils/apps"),_callscript=require("./utils/callscript"),_forwarding=require("./utils/forwarding"),_intents=require("./utils/intents"),_transactions=require("./utils/transactions");function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!=key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function ownKeys(object,enumerableOnly){var keys=Object.keys(object);if(Object.getOwnPropertySymbols){var symbols=Object.getOwnPropertySymbols(object);enumerableOnly&&(symbols=symbols.filter(function(sym){return Object.getOwnPropertyDescriptor(object,sym).enumerable})),keys.push.apply(keys,symbols)}return keys}function _objectSpread(target){for(var source,i=1;i<arguments.length;i++)source=null==arguments[i]?{}:arguments[i],i%2?ownKeys(Object(source),!0).forEach(function(key){(0,_defineProperty2.default)(target,key,source[key])}):Object.getOwnPropertyDescriptors?Object.defineProperties(target,Object.getOwnPropertyDescriptors(source)):ownKeys(Object(source)).forEach(function(key){Object.defineProperty(target,key,Object.getOwnPropertyDescriptor(source,key))});return target}// Try to get an injected web3 provider, return a public one otherwise. const detectProvider=()=>"undefined"==typeof web3?"wss://rinkeby.eth.aragon.network/ws":web3.currentProvider// eslint-disable-line ;/** * An Aragon wrapper. * * @param {string} daoAddress * The address of the DAO. * @param {Object} options * Wrapper options. * @param {Object} options.apm * Options for fetching information from aragonPM * @param {string} options.apm.ensRegistryAddress * ENS registry for aragonPM * @param {Object} [options.apm.ipfs] * IPFS provider config for aragonPM * @param {string} [options.apm.ipfs.gateway] * IPFS gateway to fetch aragonPM artifacts from * @param {number} [options.apm.ipfs.fetchTimeout] * Timeout for retrieving aragonPM artifacts from IPFS before failing * @param {Object} [options.cache] * Options for the internal cache * @param {boolean} [options.cache.forceLocalStorage=false] * Downgrade to localStorage even if IndexedDB is available * @param {Object} [options.events] * Options for handling Ethereum events * @param {boolean} [options.events.subscriptionEventDelay] * Time in ms to delay a new event from a contract subscription * @param {Object} [options.events.blockSizeLimit] * Optional max amount of blocks to fetch from past events * @param {Function} [options.defaultGasPriceFn=function] * A factory function to provide the default gas price for transactions. * It can return a promise of number string or a number string. The function * has access to a recommended gas limit which can be used for custom * calculations. This function can also be used to get a good gas price * estimation from a 3rd party resource. * @param {string|Object} [options.provider=web3.currentProvider] * The Web3 provider to use for blockchain communication. Defaults to `web3.currentProvider` * if web3 is injected, otherwise will fallback to wss://rinkeby.eth.aragon.network/ws */exports.detectProvider=detectProvider;class Aragon{constructor(daoAddress){let options=1<arguments.length&&arguments[1]!==void 0?arguments[1]:{};const defaultOptions={defaultGasPriceFn:()=>{},provider:detectProvider(),cache:{forceLocalStorage:!1,prefix:null},events:{subscriptionDelayTime:0}};options=Object.assign(defaultOptions,options),(0,_configuration.setConfiguration)(configurationKeys.FORCE_LOCAL_STORAGE,!!(options.cache&&options.cache.forceLocalStorage)),(0,_configuration.setConfiguration)(configurationKeys.SUBSCRIPTION_EVENT_DELAY,Number.isFinite(options.events&&options.events.subscriptionEventDelay)?options.events.subscriptionEventDelay:0),(0,_configuration.setConfiguration)(configurationKeys.PAST_EVENTS_BLOCK_SIZE,!!(options.events&&options.events.blockSizeLimit)&&options.events.blockSizeLimit),this.web3=new _web.default(options.provider),this.ens=(0,_ens.default)(options.provider,options.apm.ensRegistryAddress);// Set up APM utilities const{ipfs:apmIpfsOptions={}}=options.apm;this.apm=(0,_apm.default)(this.web3,{fetchTimeout:apmIpfsOptions.fetchTimeout,ipfsGateway:apmIpfsOptions.gateway}),this.kernelProxy=(0,_utils.makeProxy)(daoAddress,"Kernel",this.web3);// Set up cache const cachePrefix=options.cache.prefix?`${options.cache.prefix}:${daoAddress}`:daoAddress;// Set up app contexts this.cache=new _cache.default(cachePrefix),this.appContextPool=new _apps.default,this.defaultGasPriceFn=options.defaultGasPriceFn}/** * Initialise the wrapper. * * @param {Object} [options] Options * @param {Object} [options.accounts] `initAccount()` options (see below) * @param {Object} [options.acl] `initACL()` options (see below) * @param {Object} [options.guiStyle] `initGuiStyle()` options (see below) * @return {Promise<void>} * @throws {Error} Will throw an error if the `daoAddress` is detected to not be a Kernel instance */async init(){let aclAddress,options=0<arguments.length&&arguments[0]!==void 0?arguments[0]:{};try{// Check if address is kernel // web3 throws if it's an empty address ('0x') aclAddress=await this.kernelProxy.call("acl")}catch(_){throw Error(`Provided daoAddress is not a DAO`)}await this.cache.init(),await this.kernelProxy.updateInitializationBlock(),await this.initAccounts(options.accounts),await this.initAcl(Object.assign({aclAddress},options.acl)),await this.initIdentityProviders(),this.initApps(),this.initForwarders(),this.initAppIdentifiers(),this.initNetwork(options.network),this.initGuiStyle(options.guiStyle),this.pathIntents=new _rxjs.Subject,this.transactions=new _rxjs.Subject,this.signatures=new _rxjs.Subject}/** * Initialise the accounts observable. * * @param {Object} [options] Options * @param {boolean} [options.fetchFromWeb3] Whether or not accounts should also be fetched from * the provided Web3 instance * @param {Array<string>} [options.providedAccounts] Array of accounts that the user controls * @return {Promise<void>} */async initAccounts(){let{fetchFromWeb3,providedAccounts=[]}=0<arguments.length&&arguments[0]!==void 0?arguments[0]:{};this.accounts=new _rxjs.ReplaySubject(1);const accounts=fetchFromWeb3?providedAccounts.concat(await this.web3.eth.getAccounts()):providedAccounts;this.setAccounts(accounts)}/** * Initialise the ACL (Access Control List). * * @return {Promise<void>} */async initAcl(){let{aclAddress}=0<arguments.length&&arguments[0]!==void 0?arguments[0]:{};aclAddress||(aclAddress=await this.kernelProxy.call("acl")),this.aclProxy=(0,_utils.makeProxy)(aclAddress,"ACL",this.web3,{initializationBlock:this.kernelProxy.initializationBlock});const ACL_CACHE_KEY=(0,_utils.getCacheKey)(aclAddress,"acl"),currentBlock=await this.web3.eth.getBlockNumber(),cacheBlockHeight=Math.max(currentBlock-100,0),cachedAclState=await this.cache.get(ACL_CACHE_KEY,{}),{permissions:cachedPermissions,blockNumber:cachedBlockNumber}=cachedAclState,pastEventsOptions={toBlock:cacheBlockHeight,// When using cache, fetch events from the next block after cache fromBlock:cachedPermissions?cachedBlockNumber+1:void 0},pastEvents$=this.aclProxy.pastEvents(null,pastEventsOptions).pipe((0,_operators.mergeMap)(pastEvents=>(0,_rxjs.from)(pastEvents)),// Custom cache event (0,_operators.endWith)({event:ACL_CACHE_KEY,returnValues:{}})),currentEvents$=this.aclProxy.events(null,{fromBlock:cacheBlockHeight+1}).pipe((0,_operators.startWith)({event:"starting current events",returnValues:{}})),fetchedPermissions$=(0,_rxjs.concat)(pastEvents$,currentEvents$).pipe((0,_operators.scan)((_ref,event)=>{let[permissions]=_ref;const eventData=event.returnValues;if(eventData.app){// NOTE: dotprop.get() doesn't work through proxies, so we manually access permissions const appPermissions=permissions[eventData.app]||{};if(event.event==="SetPermission"){const key=`${eventData.role}.allowedEntities`,allowedEntitiesSet=new Set(_dotProp.default.get(appPermissions,key,[]));// Converts to and from a set to avoid duplicated entities eventData.allowed?allowedEntitiesSet.add(eventData.entity):allowedEntitiesSet.delete(eventData.entity),_dotProp.default.set(appPermissions,key,Array.from(allowedEntitiesSet))}event.event==="ChangePermissionManager"&&_dotProp.default.set(appPermissions,`${eventData.role}.manager`,eventData.manager),permissions[eventData.app]=appPermissions}return[permissions,event]},[(0,_utils.makeAddressMapProxy)(cachedPermissions||{})]),// Cache if we're finished syncing up to cache block height (0,_operators.map)(_ref2=>{let[permissions,event]=_ref2;return event.event===ACL_CACHE_KEY&&this.cache.set(ACL_CACHE_KEY,// Make copy for cache {permissions:Object.assign({},permissions),blockNumber:cacheBlockHeight}),permissions}),// Throttle so it only continues after 30ms without new values // Avoids DDOSing subscribers as during initialization there may be // hundreds of events processed in a short timespan (0,_operators.debounceTime)(30),(0,_operators.publishReplay)(1));fetchedPermissions$.connect();const cachedPermissions$=cachedPermissions?(0,_rxjs.of)((0,_utils.makeAddressMapProxy)(cachedPermissions)):(0,_rxjs.of)();this.permissions=(0,_rxjs.concat)(cachedPermissions$,fetchedPermissions$).pipe((0,_operators.publishReplay)(1)),this.permissions.connect()}/** * Check if an object is an app. * * @param {Object} app * @return {boolean} */isApp(app){return app.kernelAddress&&this.isKernelAddress(app.kernelAddress)}/** * Check if an address is this DAO's kernel. * * @param {string} address * @return {boolean} */isKernelAddress(address){return(0,_utils.addressesEqual)(address,this.kernelProxy.address)}/** * Initialize apps observable. * * @return {void} */initApps(){/****************************** * * * CACHING * * * ******************************/const applicationInfoCache=new _utils.AsyncRequestCache(async cacheKey=>{const[appId,codeAddress]=cacheKey.split(".");return(0,_aragonOS.getAragonOsInternalAppInfo)(appId)||(0,_apm.getApmInternalAppInfo)(appId)||this.apm.fetchLatestRepoContentForContract(await this.ens.resolve(appId),codeAddress)}),proxyContractValueCache=new _utils.AsyncRequestCache(proxyAddress=>{if(this.isKernelAddress(proxyAddress)){const kernelProxy=(0,_utils.makeProxy)(proxyAddress,"ERCProxy",this.web3);return Promise.all([// Use Kernel ABI this.kernelProxy.call("KERNEL_APP_ID"),// Use ERC897 proxy ABI // Note that this won't work on old Aragon Core 0.5 Kernels, // as they had not implemented ERC897 yet kernelProxy.call("implementation")]).then(values=>({appId:values[0],codeAddress:values[1]}))}const appProxy=(0,_utils.makeProxy)(proxyAddress,"AppProxy",this.web3),appProxyForwarder=(0,_utils.makeProxy)(proxyAddress,"Forwarder",this.web3);return Promise.all([appProxy.call("kernel"),appProxy.call("appId"),appProxy.call("implementation"),// Not all apps implement the forwarding interface appProxyForwarder.call("isForwarder").catch(()=>!1)]).then(values=>({kernelAddress:values[0],appId:values[1],codeAddress:values[2],isForwarder:values[3]}))}),installedApps$=this.permissions.pipe((0,_operators.map)(Object.keys),// Dedupe until apps change (0,_operators.distinctUntilChanged)((oldProxies,newProxies)=>{if(oldProxies.length!==newProxies.length)return!1;const oldSet=new Set(oldProxies),intersection=new Set(newProxies.filter(newProxy=>oldSet.has(newProxy)));return intersection.size===oldSet.size}),// Add Kernel as the first "app" (0,_operators.map)(proxyAddresses=>{const appsWithoutKernel=proxyAddresses.filter(address=>!this.isKernelAddress(address));return[this.kernelProxy.address].concat(appsWithoutKernel)}),// Get proxy values // Note that we can safely discard throttled values, // so we use a `switchMap()` instead of a `mergeMap()` (0,_operators.switchMap)(proxyAddresses=>Promise.all(proxyAddresses.map(async proxyAddress=>{let proxyValues;try{proxyValues=await proxyContractValueCache.request(proxyAddress)}catch(_){}return _objectSpread({proxyAddress},proxyValues)}))),// Filter to remove any non-apps assigned in permissions (0,_operators.map)(appProxies=>appProxies.filter(appProxy=>this.isApp(appProxy)||this.isKernelAddress(appProxy.proxyAddress)))),updatedApps$=this.kernelProxy// Only need to subscribe from latest block .events("SetApp",{fromBlock:"latest"}).pipe(// Only care about changes if they're in the APP_BASE namespace (0,_operators.filter)(_ref3=>{let{returnValues}=_ref3;return(0,_kernel.isKernelAppCodeNamespace)(returnValues.namespace)}),// Merge with latest value of installedApps$ so we can return the full list of apps (0,_operators.withLatestFrom)(installedApps$,function(setAppEvent,apps){const{appId:setAppId}=setAppEvent.returnValues;return apps.map(async app=>{if(app.appId!==setAppId)return app;let proxyValues;try{proxyValues=await proxyContractValueCache.request(app.proxyAddress,!0)}catch(_){}return _objectSpread(_objectSpread(_objectSpread({},app),proxyValues),{},{updated:!0})})}),// Emit resolved array of promises, one at a time (0,_operators.concatMap)(updatedApps=>Promise.all(updatedApps))),apps$=(0,_rxjs.merge)(installedApps$,updatedApps$),appsWithInfo$=apps$.pipe((0,_operators.concatMap)(apps=>Promise.all(apps.map(async app=>{let appInfo;if(app.appId&&app.codeAddress){const cacheKey=`${app.appId}.${app.codeAddress}`;try{appInfo=await applicationInfoCache.request(cacheKey)}catch(_){}}// This is a hack to fix web3.js and ethers not being able to detect reverts on decoding // `eth_call`s (apps that implement fallbacks may revert if they haven't defined // `isForwarder()`) // Ideally web3.js would throw an error if it receives a revert from an `eth_call`, but // as of v1.2.1, it interprets reverts as `true` :(. // // We check if the app's ABI actually has `isForwarder()` declared, and if not, override // the isForwarder setting to false. let isForwarderOverride={};return app.isForwarder&&appInfo&&Array.isArray(appInfo.abi)&&!appInfo.abi.some(_ref4=>{let{type,name}=_ref4;return"function"===type&&"isForwarder"===name})&&(isForwarderOverride={isForwarder:!1}),_objectSpread(_objectSpread(_objectSpread({},appInfo),app),isForwarderOverride)}))));this.apps=appsWithInfo$.pipe((0,_operators.publishReplay)(1)),this.apps.connect();/******************************* * * * REPOS * * * ******************************/ // Initialize installed repos from the list of apps const installedRepoCache=new Map,repo$=apps$.pipe(// Map installed apps into a deduped list of their aragonPM repos, with these assumptions: // - No apps are lying about their appId (malicious apps _could_ masquerade as other // apps by setting this value themselves) // - `contractAddress`s will stay the same across all installed apps. // This is technically not true as apps could set this value themselves // (e.g. as pinned apps do), but these apps wouldn't be able to upgrade anyway // // Ultimately returns an array of objects, holding the repo's: // - appId // - base contractAddress (0,_operators.map)(apps=>Object.values(apps.filter(_ref5=>{let{appId}=_ref5;return!(0,_aragonOS.isAragonOsInternalApp)(appId)}).reduce((installedRepos,_ref6)=>{let{appId,codeAddress,updated}=_ref6;return installedRepos[appId]={appId,updated,contractAddress:codeAddress},installedRepos},{}))),// Filter list of installed repos into: // - New repos we haven't seen before (to begin subscribing to their version events) // - Repos we've seen before, to trigger a recalculation of the currently installed version (0,_operators.map)(repos=>{const newRepoAppIds=[],updatedRepoAppIds=[];return repos.forEach(repo=>{const{appId,updated}=repo;installedRepoCache.has(appId)?updated&&updatedRepoAppIds.push(appId):newRepoAppIds.push(appId),installedRepoCache.set(appId,repo)}),[newRepoAppIds,updatedRepoAppIds]}),// Stop if there's no new repos or updated apps (0,_operators.filter)(_ref7=>{let[newRepoAppIds,updatedRepoAppIds]=_ref7;return newRepoAppIds.length||updatedRepoAppIds.length}),// Project new repos into their ids and web3 proxy objects (0,_operators.concatMap)(async _ref8=>{let[newRepoAppIds,updatedRepoAppIds]=_ref8;const newRepos=(await Promise.all(newRepoAppIds.map(async appId=>{let repoProxy;try{const repoAddress=await this.ens.resolve(appId);repoProxy=(0,_repo.makeRepoProxy)(repoAddress,this.web3),await repoProxy.updateInitializationBlock()}catch(err){console.error(`Could not find repo for ${appId}`,err)}return{appId,repoProxy}}))// Filter out repos we couldn't create proxies for (they were likely due to publishing // invalid aragonPM repos) // Note that we don't need to worry about doing this for the updated repos list; if // we could not create the original repo proxy when we first saw the repo, the updates // won't do anything because we weren't able to fetch enough information (versions list) ).filter(newRepos=>newRepos.repoProxy);return[newRepos,updatedRepoAppIds]}),// Here's where the fun begins! // It'll be easy to get lost, so remember to take it slowly. // Just remember, with this `mergeMap()`, we'll be subscribing to all the projected (returned) // observables and merging their respective emissions into a single observable. // // The output of this merged observable are update events containing the following: // - `appId`: mandatory, signifies which repo was updated // - `repoAddress`: optional, address of the repo contract itself // - `versions`: optional, new version information (0,_operators.mergeMap)(_ref9=>{let[newRepos,updatedRepoAppIds]=_ref9;// Create a new observable to project each new update as its own update emission. const update$=(0,_rxjs.of)(...updatedRepoAppIds).pipe((0,_operators.map)(appId=>({appId}))),newRepo$=(0,_rxjs.of)(...newRepos),repoAddress$=newRepo$.pipe((0,_operators.map)(_ref10=>{let{appId,repoProxy}=_ref10;return{appId,repoAddress:repoProxy.address}})),version$=newRepo$.pipe(// `mergeMap()` to "flatten" the async transformation. This async function returns an // observable, which is ultimately the NewVersion stream. More on this, after the break. // Note: we don't care about the ordering, so we use `mergeMap()` instead of `concatMap()` (0,_operators.mergeMap)(async _ref11=>{let{appId,repoProxy}=_ref11;const initialVersions=[// Immediately query state from the repo contract, to avoid having to wait until all // past events sync (may be long) ...(await(0,_repo.getAllRepoVersions)(repoProxy))];// Return an observable subscribed to NewVersion events, giving us: // - Timestamps for versions that were published prior to this process running // - Notifications for newly published versions // // Reduce this with the cached version information to emit version updates for the repo. return repoProxy.events("NewVersion").pipe(// Project each event to a new version info object, one at a time (0,_operators.concatMap)(async event=>{const{versionId:eventVersionId}=event.returnValues,timestamp=1e3*(await this.web3.eth.getBlock(event.blockNumber)).timestamp,versionIndex=initialVersions.findIndex(_ref12=>{let{versionId}=_ref12;return versionId===eventVersionId}),versionInfo=-1===versionIndex?await(0,_repo.getRepoVersionById)(repoProxy,eventVersionId):initialVersions[versionIndex];// Adjust from Ethereum time return _objectSpread(_objectSpread({},versionInfo),{},{timestamp})}),// Trick to immediately emit (e.g. similar to a do/while loop) (0,_operators.startWith)(null),// Reduce newly emitted versions into the full list of versions (0,_operators.scan)((_ref13,newVersionInfo)=>{let{appId,versions}=_ref13,newVersions=versions;if(newVersionInfo){const versionIndex=versions.findIndex(_ref14=>{let{versionId}=_ref14;return versionId===newVersionInfo.versionId});-1===versionIndex?newVersions=versions.concat(newVersionInfo):(newVersions=Array.from(versions),newVersions[versionIndex]=newVersionInfo)}return{appId,versions:newVersions}},{appId,versions:initialVersions}))}),// This bit is interesting. // We've "flattened" our async transformation with the `mergeMap()` above, but it still // returns an observable. We need to flatten this observable's emissions into the upper // stream, which is what `mergeAll()` achieves. (0,_operators.mergeAll)());// Create a new observable to project each new repo as its own emission. // Merge all of the repo update events resulting from the apps being updated, and return it // to the upper `mergeMap()` so it can be re-flattened into a single event stream. return(0,_rxjs.merge)(repoAddress$,version$,update$)}),// Reduce the event stream into a current representation of the installed repos, and which // repo to update next. (0,_operators.scan)((_ref15,repoUpdate)=>{let{repos}=_ref15;const{appId:updatedAppId}=repoUpdate,update=(0,_objectWithoutProperties2.default)(repoUpdate,_excluded),updatedRepoInfo=_objectSpread(_objectSpread({},repos[updatedAppId]),update);return{repos:_objectSpread(_objectSpread({},repos),{},{[updatedAppId]:updatedRepoInfo}),updatedRepoAppId:updatedAppId}},{repos:{},updatedRepoAppId:null}),// Stop if we don't have enough information yet to continue (0,_operators.filter)(_ref16=>{let{repos,updatedRepoAppId}=_ref16;return!!updatedRepoAppId&&Array.isArray(repos[updatedRepoAppId].versions)}),// Grab the full information of the updated repo using its latest values. // With this, we've taken the basic stream of updates for events and mapped them onto their // full repo objects. (0,_operators.concatMap)(async _ref17=>{let{repos,updatedRepoAppId:appId}=_ref17;const{repoAddress,versions}=repos[appId],installedRepoInfo=installedRepoCache.get(appId),baseRepoInfo={appId,repoAddress,versions},fetchVersionInfo=version=>applicationInfoCache.request(`${appId}.${version.contractAddress}`).catch(()=>({})).then(content=>({content,version:version.version})),latestVersion=versions[versions.length-1],currentVersion=Array.from(versions)// Apply reverse to find the latest version with the currently installed contract address .reverse().find(version=>(0,_utils.addressesEqual)(version.contractAddress,installedRepoInfo.contractAddress));if(!currentVersion)// The organization has installed an unpublished version of this app // Avoid returning a current version as we don't know what version they're using return _objectSpread(_objectSpread({},baseRepoInfo),{},{currentVersion:null,latestVersion:await fetchVersionInfo(latestVersion)});const currentVersionInfoRequest=fetchVersionInfo(currentVersion),latestVersionInfoRequest=(0,_utils.addressesEqual)(currentVersion.contractAddress,latestVersion.contractAddress)?currentVersionInfoRequest:fetchVersionInfo(latestVersion);return _objectSpread(_objectSpread({},baseRepoInfo),{},{currentVersion:await currentVersionInfoRequest,latestVersion:await latestVersionInfoRequest})}));this.installedRepos=repo$.pipe(// Finally, we reduce the merged updates from individual repos into one final, expanding array // of the installed repos (0,_operators.scan)((repos,updatedRepo)=>{const repoIndex=repos.findIndex(repo=>repo.repoAddress===updatedRepo.repoAddress);if(-1===repoIndex)return repos.concat(updatedRepo);else{const nextRepos=Array.from(repos);return nextRepos[repoIndex]=updatedRepo,nextRepos}},[]),// Throttle updates, but must keep trailing to ensure we don't drop any updates (0,_operators.throttleTime)(500,_rxjs.asyncScheduler,{leading:!1,trailing:!0}),(0,_operators.publishReplay)(1)),this.installedRepos.connect()}/** * Initialise forwarder observable. * * @return {void} */initForwarders(){this.forwarders=this.apps.pipe((0,_operators.map)(apps=>apps.filter(app=>app.isForwarder)),(0,_operators.publishReplay)(1)),this.forwarders.connect()}/** * Initialise app identifier observable. * * @return {void} */initAppIdentifiers(){this.appIdentifiers=new _rxjs.BehaviorSubject({}).pipe((0,_operators.scan)((identifiers,_ref18)=>{let{address,identifier}=_ref18;return Object.assign(identifiers,{[address]:identifier})}),(0,_operators.publishReplay)(1)),this.appIdentifiers.connect()}/** * Set the identifier of an app. * * @param {string} address The proxy address of the app * @param {string} identifier The identifier of the app * @return {void} */setAppIdentifier(address,identifier){this.appIdentifiers.next({address,identifier})}/** * Initialise identity providers. * * @return {Promise<void>} */async initIdentityProviders(){const defaultIdentityProviders=[{name:"local",provider:new _identity.LocalIdentityProvider}],identityProviders=[...defaultIdentityProviders,...[]];// TODO: detect other installed providers // Init all providers // Set up identity modification intent observable await Promise.all(identityProviders.map(_ref19=>{let{provider}=_ref19;// Most providers should have this defined to a noop function by default, but just in case return"function"==typeof provider.init?provider.init():Promise.resolve()})),this.identityProviderRegistrar=new Map(identityProviders.map(_ref20=>{let{name,provider}=_ref20;return[name,provider]})),this.identityIntents=new _rxjs.Subject}/** * Modify the identity metadata for an address using the highest priority provider. * * @param {string} address Address to modify * @param {Object} metadata Modification metadata object * @return {Promise} Resolves if the modification was successful */modifyAddressIdentity(address,metadata){const provider=this.identityProviderRegistrar.get("local");return provider&&"function"==typeof provider.modify?provider.modify(address,metadata):Promise.reject(new Error(`Provider (${"local"}) not installed`))}/** * Resolve the identity metadata for an address using the highest priority provider. * * @param {string} address Address to resolve * @return {Promise} Resolves with the identity or null if not found */resolveAddressIdentity(address){const provider=this.identityProviderRegistrar.get("local");// TODO - get provider return provider&&"function"==typeof provider.resolve?provider.resolve(address):Promise.reject(new Error(`Provider (${"local"}) not installed`))}/** * Search identities based on a term * * @param {string} searchTerm * @return {Promise} Resolves with the identity or null if not found */searchIdentities(searchTerm){const provider=this.identityProviderRegistrar.get("local");// TODO - get provider return provider&&"function"==typeof provider.search?provider.search(searchTerm):Promise.reject(new Error(`Provider (${"local"}) not installed`))}/** * Request an identity modification using the highest priority provider. * * Returns a promise which delegates resolution to the handler * which listens and handles `this.identityIntents` * * @param {string} address Address to modify * @return {Promise} Resolved by the handler of identityIntents */requestAddressIdentityModification(address){// TODO - get provider return this.identityProviderRegistrar.has("local")?new Promise((resolve,reject)=>{this.identityIntents.next({address,providerName:"local",resolve,reject(err){reject(err||new Error("The identity modification was not completed"))}})}):Promise.reject(new Error(`Provider (${"local"}) not installed`))}/** * Remove selected local identities * * @param {Array<string>} addresses The addresses to be removed from the local identity provider * @return {Promise} */async removeLocalIdentities(addresses){const localProvider=this.identityProviderRegistrar.get("local");for(const address of addresses)await localProvider.remove(address)}/** * Get all local identities for listing functionality * * @return {Promise<Object>} */getLocalIdentities(){return this.identityProviderRegistrar.get("local").getAll()}/** * Initialise the GUI style observable. * * @param {Object} style GUI style options * @param {string} style.appearance "dark" or "light" * @param {Object} [style.theme] The theme object * @return {void} */initGuiStyle(){let{appearance,theme}=0<arguments.length&&arguments[0]!==void 0?arguments[0]:{};this.guiStyle=new _rxjs.BehaviorSubject({appearance:appearance||"light",theme:theme||null})}/** * Set the GUI style (theme and appearance). * * @param {string} appearance "dark" or "light" * @param {Object} [theme] The theme object. * @return {void} */setGuiStyle(appearance){let theme=1<arguments.length&&arguments[1]!==void 0?arguments[1]:null;this.guiStyle.next({appearance,theme})}/** * Initialise the network observable. * * @param {Object} network information of node * @return {Promise<void>} */async initNetwork(network){this.network=new _rxjs.ReplaySubject(1),network?this.network.next(network):this.network.next({id:await this.web3.eth.getChainId(),type:await this.web3.eth.net.getNetworkType()})}/** * Request an app's path be changed. * * @param {string} appAddress * @param {string} path * @return {Promise} Succeeds if path request was allowed */async requestAppPath(appAddress,path){if("string"!=typeof path)throw new Error("Path must be a string");if(!(await this.getApp(appAddress)))throw new Error(`Cannot request path for non-installed app: ${appAddress}`);return new Promise((resolve,reject)=>{this.pathIntents.next({appAddress,path,resolve,reject(err){reject(err||new Error("The path was rejected"))}})})}/** * Set an app's path. * * @param {string} appAddress * @param {string} path * @return {void} */setAppPath(appAddress,path){if("string"!=typeof path)throw new Error("Path must be a string");this.appContextPool.emit(appAddress,_apps.APP_CONTEXTS.PATH,path)}/** * Run an app. * * As there may be race conditions with losing messages from cross-context environments, * running an app is split up into two parts: * * 1. Set up any required state for the app. This step is allowed to be asynchronous. * 2. Connect the app to a running context, by associating the context's message provider * to the app. This step is synchronous. * * @param {string} proxyAddress * The address of the app proxy. * @return {Promise<function>} */async runApp(proxyAddress){// Step 1: Set up required state for the app // Only get the first result from the observable, so our running contexts don't get // reinitialized if new apps appear const apps=await this.apps.pipe((0,_operators.first)()).toPromise(),app=apps.find(app=>(0,_utils.addressesEqual)(app.proxyAddress,proxyAddress)),appProxy=(0,_utils.makeProxyFromAppABI)(app.proxyAddress,app.abi,this.web3);// Step 2: Associate app with running context return await appProxy.updateInitializationBlock(),sandboxMessengerProvider=>{// Set up messenger const messenger=new _rpcMessenger.default(sandboxMessengerProvider),request$=messenger.requests().pipe((0,_operators.map)(request=>({request,proxy:appProxy,wrapper:this})),// Use the same request$ result in each handler // Turns request$ into a subject (0,_operators.publishReplay)(1));// Wrap requests with the application proxy // Note that we have to do this synchronously with the creation of the message provider, // as we otherwise risk race conditions and may lose messages request$.connect();// Register request handlers const handlerSubscription=handlers.combineRequestHandlers(// Generic handlers handlers.createRequestHandler(request$,"accounts",handlers.accounts),handlers.createRequestHandler(request$,"cache",handlers.cache),handlers.createRequestHandler(request$,"describe_script",handlers.describeScript),handlers.createRequestHandler(request$,"describe_transaction",handlers.describeTransaction),handlers.createRequestHandler(request$,"get_apps",handlers.getApps),handlers.createRequestHandler(request$,"network",handlers.network),handlers.createRequestHandler(request$,"path",handlers.path),handlers.createRequestHandler(request$,"gui_style",handlers.guiStyle),handlers.createRequestHandler(request$,"trigger",handlers.trigger),handlers.createRequestHandler(request$,"web3_eth",handlers.web3Eth),// Contract handlers handlers.createRequestHandler(request$,"intent",handlers.intent),handlers.createRequestHandler(request$,"call",handlers.call),handlers.createRequestHandler(request$,"sign_message",handlers.signMessage),handlers.createRequestHandler(request$,"events",handlers.events),handlers.createRequestHandler(request$,"past_events",handlers.pastEvents),// External contract handlers handlers.createRequestHandler(request$,"external_call",handlers.externalCall),handlers.createRequestHandler(request$,"external_events",handlers.externalEvents),handlers.createRequestHandler(request$,"external_intent",handlers.externalIntent),handlers.createRequestHandler(request$,"external_past_events",handlers.externalPastEvents),// Identity handlers handlers.createRequestHandler(request$,"identify",handlers.appIdentifier),handlers.createRequestHandler(request$,"address_identity",handlers.addressIdentity),handlers.createRequestHandler(request$,"search_identities",handlers.searchIdentities)).subscribe(response=>messenger.sendResponse(response.id,response.payload)),shutdown=()=>handlerSubscription.unsubscribe(),shutdownAndClearCache=async()=>(shutdown(),Object.keys(await this.cache.getAll()).reduce((promise,cacheKey)=>promise.then(()=>cacheKey.startsWith(proxyAddress)?this.cache.remove(cacheKey):Promise.resolve()),Promise.resolve()));// The attached unsubscribe isn't automatically bound to the subscription return{shutdown,shutdownAndClearCache}}}/** * Set the available accounts for the current user. * * @param {Array<string>} accounts * @return {void} */setAccounts(accounts){this.accounts.next(accounts)}/** * Get the available accounts for the current user. * * @return {Promise<Array<string>>} An array of addresses */getAccounts(){return this.accounts.pipe((0,_operators.first)()).toPromise()}/** * Allows apps to sign arbitrary data via a RPC call * * @param {string} message to be signed * @param {string} requestingApp proxy address of requesting app * @return {Promise<string>} signature hash */signMessage(message,requestingApp){return"string"==typeof message?new Promise((resolve,reject)=>{this.signatures.next({message,requestingApp,resolve,reject(err){reject(err||new Error("The message was not signed"))}})}):Promise.reject(new Error("Message to sign must be a string"))}/** * @param {Array<Object>} transactionPath An array of Ethereum transactions that describe each * step in the path * @param {Object} [options] * @param {boolean} [options.external] Whether the transaction path is initiating an action on * an external destination (not the currently running app) * @return {Promise<string>} Promise that should be resolved with the sent transaction hash */performTransactionPath(transactionPath){let{external}=1<arguments.length&&arguments[1]!==void 0?arguments[1]:{};return new Promise((resolve,reject)=>{this.transactions.next({resolve,external:!!external,transaction:transactionPath[0],path:transactionPath,reject(err){reject(err||new Error("The transaction was not signed"))}})})}/** * Performs an action on the ACL using transaction pathing * * @param {string} method * @param {Array<*>} params * @return {Promise<string>} transaction hash */async performACLIntent(method,params){const path=await this.getACLTransactionPath(method,params);return this.performTransactionPath(path)}/** * Looks for app with the provided proxyAddress and returns its app object if found * * @param {string} proxyAddress * @return {Promise<Object>} The app object */getApp(proxyAddress){return this.apps.pipe((0,_operators.map)(apps=>apps.find(app=>(0,_utils.addressesEqual)(app.proxyAddress,proxyAddress))),(0,_operators.first)()).toPromise()}/** * Calculate the transaction path for a transaction to `destination` * that invokes `methodSignature` with `params`. * * @param {string} destination * @param {string} methodSignature * @param {Array<*>} params * @param {string} [finalForwarder] Address of the final forwarder that can perfom the action * @return {Promise<Array<Object>>} An array of Ethereum transactions that describe each step in the path */async getTransactionPath(destination,methodSignature,params,finalForwarder){const accounts=await this.getAccounts();for(let account of accounts){const path=await this.calculateTransactionPath(account,destination,methodSignature,params,finalForwarder);if(0<path.length)try{return this.describeTransactionPath(path)}catch(_){return path}}return[]}/** * Calculate the transaction path for a transaction to an external `destination` * (not the currently running app) that invokes a method matching the * `methodAbiFragment` with `params`. * * @param {string} destination Address of the external contract * @param {object} methodAbiFragment ABI fragment of method to invoke * @param {Array<*>} params * @return {Promise<Array<Object>>} An array of Ethereum transactions that describe each step in the path. * If the destination is a non-installed contract, always results in an array containing a * single transaction. */async getExternalTransactionPath(destination,methodAbiFragment,params){if((0,_utils.addressesEqual)(destination,this.aclProxy.address))try{return this.getACLTransactionPath(methodAbiFragment.name,params)}catch(_){return[]}const installedApp=await this.getApp(destination);if(installedApp)// Destination is an installed app; need to go through normal transaction pathing return this.getTransactionPath(destination,methodAbiFragment.name,params);// Destination is not an installed app on this org, just create a direct transaction // with the first account const account=(await this.getAccounts())[0];try{const tx=await(0,_transactions.createDirectTransaction)(account,destination,methodAbiFragment,params,this.web3);return this.describeTransactionPath([tx])}catch(_){return[]}}/** * Calculate the transaction path for a basket of intents. * Expects the `intentBasket` to be an array of tuples holding the following: * {string} destination: destination address * {string} methodSignature: method to invoke on destination * {Array<*>} params: method params * These are the same parameters as the ones used for `getTransactionPath()` * * Allows user to specify how many of the intents should be checked to ensure their paths are * compatible. `checkMode` supports: * 'all': All intents will be checked to make sure they use the same forwarding path. * 'single': assumes all intents can use the path found from the first intent * * @param {Array<Array<string, string, Array<*>>>} intentBasket Intents * @param {Object} [options] * @param {string} [options.checkMode] Path checking mode * @return {Promise<Object>} An object containing: * - `path` (Array<Object>): a multi-step transaction path that eventually invokes this basket. * Empty if no such path could be found. * - `transactions` (Array<Object>): array of Ethereum transactions that invokes this basket. * If a multi-step transaction path was found, returns the first transaction in that path. * Empty if no such transactions could be found. */async getTransactionPathForIntentBasket(intentBasket){let{checkMode="all"}=1<arguments.length&&arguments[1]!==void 0?arguments[1]:{};// Get transaction paths for entire basket const intentsToCheck="all"===checkMode?intentBasket// all -- use all intents :"single"===checkMode?[intentBasket[0]]// single -- only use first intent :[],intentPaths=await Promise.all(intentsToCheck.map(_ref21=>{let[destination,methodSignature,params]=_ref21;return(0,_utils.addressesEqual)(destination,this.aclProxy.address)?this.getACLTransactionPath(methodSignature,params):this.getTransactionPath(destination,methodSignature,params)})),pathsMatch=(0,_intents.doIntentPathsMatch)(intentPaths);if(pathsMatch){// Create direct transactions for each intent in the intentBasket const sender=(await this.getAccounts())[0],directTransactions=await Promise.all(intentBasket.map(async _ref22=>{let[destination,methodSignature,params]=_ref22;return(0,_transactions.createDirectTransactionForApp)(sender,await this.getApp(destination),methodSignature,params,this.web3)}));// TODO: don't assume it's the first account if(1===intentPaths[0].length)// Sender has direct access try{const decoratedTransactions=await this.describeTransactionPath(await Promise.all(directTransactions.map(transaction=>this.applyTransactionGas(transaction))));return{path:[],transactions:decoratedTransactions}}catch(_){}else{// Need to encode calls scripts for each forwarder transaction in the path const createForwarderTransaction=(0,_transactions.createForwarderTransactionBuilder)(sender,{},this.web3),forwarderPath=intentPaths[0]// Ignore the last part of the path, which was the original intent .slice(0,-1)// Start from the "last" forwarder and move backwards to the sender .reverse()// Just use the forwarders' addresses .map(_ref23=>{let{to}=_ref23;return to}).reduce((path,nextForwarder)=>{const lastStep=path[0],encodedLastStep=(0,_callscript.encodeCallScript)(Array.isArray(lastStep)?lastStep:[lastStep]);return[createForwarderTransaction(nextForwarder,encodedLastStep),...path]},// Start the recursive calls script encoding with the direct transactions for the // intent basket [directTransactions]);try{return forwarderPath[0]=await this.applyTransactionGas(forwarderPath[0],!0),{path:await this.describeTransactionPath(forwarderPath),// When we have a path, we only need to send the first transaction to start it transactions:[forwarderPath[0]]}}catch(_){}}}// Failed to find a path return{path:[],transactions:[]}}/** * Get the permission manager for an `app`'s and `role`. * * @param {string} appAddress * @param {string} roleHash * @return {Promise<string>} The permission manager */async getPermissionManager(appAddress,roleHash){const permissions=await this.permissions.pipe((0,_operators.first)()).toPromise(),appPermissions=permissions[appAddress];return _dotProp.default.get(appPermissions,`${roleHash}.manager`)}/** * Calculates transaction path for performing a method on the ACL * * @param {string} methodSignature * @param {Array<*>} params * @return {Promise<Array<Object>>} An array of Ethereum transactions that describe each step in the path */async getACLTransactionPath(methodSignature,params){const aclAddr=this.aclProxy.address,acl=await this.getApp(aclAddr),method=(0,_apps2.findAppMethodFromSignature)(acl,methodSignature,{allowDeprecated:!1});if(!method)throw new Error(`No method named ${methodSignature} on ACL`);if(method.roles&&0!==method.roles.length)// This action can be done with regular transaction pathing (it's protected by an ACL role) return this.getTransactionPath(aclAddr,methodSignature,params);else{// Some ACL functions don't have a role and are instead protected by a manager // Inspect the matched method's ABI to find the position of the 'app' and 'role' parameters // needed to get the permission manager const methodAbiFragment=(0,_abi.findMethodAbiFragment)(acl.abi,methodSignature);if(!methodAbiFragment)throw new Error(`Method ${method} not found on ACL ABI`);const inputNames=methodAbiFragment.inputs.map(input=>input.name),appIndex=inputNames.indexOf("_app"),roleIndex=inputNames.indexOf("_role");if(-1===appIndex||-1===roleIndex)throw new Error(`Method ${methodSignature} doesn't take _app and _role as input. Permission manager cannot be found.`);const manager=await this.getPermissionManager(params[appIndex],params[roleIndex]);return this.getTransactionPath(aclAddr,methodSignature,params,manager)}}/** * Decodes an EVM callscript and returns the transaction path it describes. * * @param {string} script * @return {Array<Object>} An array of Ethereum transactions that describe each step in the path */decodeTransactionPath(script){// In the future we may support more EVMScripts, but for now let's just assume we're only // dealing with call scripts if(!(0,_callscript.isCallScript)(script))throw new Error(`Script could not be decoded: ${script}`);const path=(0,_callscript.decodeCallScript)(script);return path.map(segment=>{const{data}=segment;if((0,_forwarding.isValidForwardCall)(data)){const forwardedEvmScript=(0,_forwarding.parseForwardCall)(data);try{segment.children=this.decodeTransactionPath(forwardedEvmScript)}catch(err){}}return segment})}/** * Use radspec to create a human-readable description for each transaction in the given `path` * * @param {Array<Object>} path * @return {Promise<Array<Object>>} The given `path`, with decorated with descriptions at each step */async describeTransactionPath(path){return Promise.all(path.map(async step=>{let decoratedStep;if(Array.isArray(step)){// Intent basket with multiple transactions in a single callscript // First see if the step can be handled with a specialized descriptor try{decoratedStep=await(0,_radspec.tryDescribingUpgradeOrganizationBasket)(step,this)}catch(err){}// If the step wasn't handled, just individually describe each of the transactions return decoratedStep||this.describeTransactionPath(step)}// Single transaction step // First see if the step can be handled with a specialized descriptor try{decoratedStep=await(0,_radspec.tryDescribingUpdateAppIntent)(step,this)}catch(err){}// Finally, if the step wasn't handled yet, evaluate via radspec normally if(!decoratedStep)try{decoratedStep=await(0,_radspec.tryEvaluatingRadspec)(step,this)}catch(err){}// Annotate the description, if one was found if(decoratedStep){if(decoratedStep.description)try{const processed=await(0,_radspec.postprocessRadspecDescription)(decoratedStep.description,this);decoratedStep.description=processed.description,decoratedStep.annotatedDescription=processed.annotatedDescription}catch(err){}decoratedStep.children&&(decoratedStep.children=await this.describeTransactionPath(decoratedStep.children))}return decoratedStep||step}))}/** * Whether the `sender` can use the `forwarder` to invoke `script`. * * @param {string} forwarder * @param {string} sender * @param {string} script * @return {Promise<bool>} */canForward(forwarder,sender,script){const canForward=new this.web3.eth.Contract((0,_interfaces.getAbi)("aragon/Forwarder"),forwarder).methods.canForward;return canForward(sender,script).call().catch(()=>!1)}getDefaultGasPrice(gasLimit){return this.defaultGasPriceFn(gasLimit)}/** * Calculates and applies the gas limit and gas price for a transaction * * @param {Object} transaction * @param {bool} isForwarding * @return {Promise<Object>} The transaction with the gas limit and gas price added. * If the transaction fails from the estimateGas check, the promise will * be rejected with the error. */async applyTransactionGas(transaction){let isForwarding=!!(1<arguments.length&&void 0!==arguments[1])&&arguments[1];// If a pretransaction is required for the main transaction to be performed,