@aragon/wrapper
Version:
Library for Aragon client implementations
443 lines • 59.7 kB
JavaScript
"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,