@metamask/ppom-validator
Advanced tools
Comparing version 0.1.2 to 0.2.0
@@ -9,2 +9,8 @@ # Changelog | ||
## [0.2.0] | ||
### Changed | ||
- Adding code to verify signature of data blobs fetched from CDN ([#35](https://github.com/MetaMask/ppom-validator/pull/35)) | ||
- Rate limit requests to the provider ([#28](https://github.com/MetaMask/ppom-validator/pull/28)) | ||
- Validate path of data files ([#27](https://github.com/MetaMask/ppom-validator/pull/27)) | ||
## [0.1.2] | ||
@@ -33,5 +39,6 @@ ### Changed | ||
[Unreleased]: https://github.com/MetaMask/ppom-validator/compare/v0.1.2...HEAD | ||
[Unreleased]: https://github.com/MetaMask/ppom-validator/compare/v0.2.0...HEAD | ||
[0.2.0]: https://github.com/MetaMask/ppom-validator/compare/v0.1.2...v0.2.0 | ||
[0.1.2]: https://github.com/MetaMask/ppom-validator/compare/v0.1.1...v0.1.2 | ||
[0.1.1]: https://github.com/MetaMask/ppom-validator/compare/v0.0.1...v0.1.1 | ||
[0.0.1]: https://github.com/MetaMask/ppom-validator/releases/tag/v0.0.1 |
@@ -12,2 +12,3 @@ import { BaseControllerV2, RestrictedControllerMessenger } from '@metamask/base-controller'; | ||
filePath: string; | ||
signature: string; | ||
}; | ||
@@ -26,7 +27,4 @@ /** | ||
* @property storageMetadata - Metadata of files storaged in storage. | ||
* @property providerRequestLimit - Number of requests in last 5 minutes that PPOM can make. | ||
* @property providerRequests - Array of timestamps in last 5 minutes when request was made from PPOM to provider. | ||
*/ | ||
export declare type PPOMState = { | ||
chainId: string; | ||
chainStatus: Record<string, { | ||
@@ -39,7 +37,2 @@ chainId: string; | ||
storageMetadata: FileMetadataList; | ||
refreshInterval: number; | ||
fileScheduleInterval: number; | ||
providerRequestLimit: number; | ||
providerRequests: number[]; | ||
securityAlertsEnabled: boolean; | ||
versionFileETag?: string; | ||
@@ -87,6 +80,10 @@ }; | ||
* @param options.cdnBaseUrl - Base URL for the CDN. | ||
* @param options.providerRequestLimit - Limit of number of requests that can be sent to provider per transaction. | ||
* @param options.dataUpdateDuration - Duration after which data is fetched again. | ||
* @param options.fileFetchScheduleDuration - Duration after which next data file is fetched. | ||
* @param options.state - Initial state of the controller. | ||
* @param options.blockaidPublicKey - Public key of blcokaid for verifying signatures of data files. | ||
* @returns The PPOMController instance. | ||
*/ | ||
constructor({ chainId, messenger, onNetworkChange, provider, storageBackend, securityAlertsEnabled, onPreferencesChange, ppomProvider, cdnBaseUrl, state, }: { | ||
constructor({ chainId, messenger, onNetworkChange, provider, storageBackend, securityAlertsEnabled, onPreferencesChange, ppomProvider, cdnBaseUrl, providerRequestLimit, dataUpdateDuration, fileFetchScheduleDuration, state, blockaidPublicKey, }: { | ||
chainId: string; | ||
@@ -101,3 +98,7 @@ onNetworkChange: (callback: (networkState: any) => void) => void; | ||
cdnBaseUrl: string; | ||
providerRequestLimit?: number; | ||
dataUpdateDuration?: number; | ||
fileFetchScheduleDuration?: number; | ||
state?: PPOMState; | ||
blockaidPublicKey: string; | ||
}); | ||
@@ -107,9 +108,4 @@ /** | ||
* This function will acquire mutex lock and invoke internal method #updatePPOM. | ||
* | ||
* @param options - Options. | ||
* @param options.updateForAllChains - True is update if required to be done for all chains in cache. | ||
*/ | ||
updatePPOM({ updateForAllChains }?: { | ||
updateForAllChains: boolean; | ||
}): Promise<void>; | ||
updatePPOM(): Promise<void>; | ||
/** | ||
@@ -116,0 +112,0 @@ * Use the PPOM. |
@@ -13,3 +13,3 @@ "use strict"; | ||
}; | ||
var _PPOMController_instances, _PPOMController_ppom, _PPOMController_provider, _PPOMController_storage, _PPOMController_refreshDataInterval, _PPOMController_fileScheduleInterval, _PPOMController_ppomMutex, _PPOMController_ppomProvider, _PPOMController_cdnBaseUrl, _PPOMController_registerMessageHandlers, _PPOMController_maybeUpdatePPOM, _PPOMController_shouldUpdate, _PPOMController_updatePPOM, _PPOMController_updateVersionInfo, _PPOMController_checkFilePresentInStorage, _PPOMController_getFile, _PPOMController_setChainIdDataFetched, _PPOMController_getNewFilesForCurrentChain, _PPOMController_getListOfFilesToBeFetched, _PPOMController_deleteOldChainIds, _PPOMController_getNewFilesForAllChains, _PPOMController_getAPIResponse, _PPOMController_fetchVersionInfo, _PPOMController_fetchBlob, _PPOMController_jsonRpcRequest, _PPOMController_getPPOM, _PPOMController_onFileScheduledInterval, _PPOMController_scheduleFileDownloadForAllChains; | ||
var _PPOMController_instances, _PPOMController_ppom, _PPOMController_provider, _PPOMController_storage, _PPOMController_refreshDataInterval, _PPOMController_fileScheduleInterval, _PPOMController_ppomMutex, _PPOMController_ppomProvider, _PPOMController_cdnBaseUrl, _PPOMController_providerRequestLimit, _PPOMController_providerRequests, _PPOMController_chainId, _PPOMController_dataUpdateDuration, _PPOMController_fileFetchScheduleDuration, _PPOMController_securityAlertsEnabled, _PPOMController_blockaidPublicKey, _PPOMController_onNetworkChange, _PPOMController_onPreferenceChange, _PPOMController_registerMessageHandlers, _PPOMController_resetPPOM, _PPOMController_maybeUpdatePPOM, _PPOMController_isDataRequiredForCurrentChain, _PPOMController_updatePPOM, _PPOMController_updateVersionInfo, _PPOMController_checkFilePresentInStorage, _PPOMController_checkFilePath, _PPOMController_getFile, _PPOMController_setChainIdDataFetched, _PPOMController_getNewFilesForCurrentChain, _PPOMController_getListOfFilesToBeFetched, _PPOMController_deleteOldChainIds, _PPOMController_getNewFilesForAllChains, _PPOMController_getAPIResponse, _PPOMController_checkIfVersionInfoETagChanged, _PPOMController_fetchVersionInfo, _PPOMController_fetchBlob, _PPOMController_jsonRpcRequest, _PPOMController_getPPOM, _PPOMController_onDataUpdateDuration, _PPOMController_scheduleFileDownloadForAllChains; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -21,6 +21,11 @@ exports.PPOMController = exports.NETWORK_CACHE_DURATION = exports.REFRESH_TIME_INTERVAL = void 0; | ||
const ppom_storage_1 = require("./ppom-storage"); | ||
const util_1 = require("./util"); | ||
exports.REFRESH_TIME_INTERVAL = 1000 * 60 * 60 * 2; | ||
const PROVIDER_REQUEST_LIMIT = 500; | ||
const PROVIDER_REQUEST_LIMIT = 300; | ||
const FILE_FETCH_SCHEDULE_INTERVAL = 1000 * 60 * 5; | ||
exports.NETWORK_CACHE_DURATION = 1000 * 60 * 60 * 24 * 7; | ||
const NETWORK_CACHE_LIMIT = { | ||
MAX: 5, | ||
MIN: 2, | ||
}; | ||
// The following methods on provider are allowed to PPOM | ||
@@ -30,2 +35,3 @@ const ALLOWED_PROVIDER_CALLS = [ | ||
'eth_blockNumber', | ||
'eth_createAccessList', | ||
'eth_getLogs', | ||
@@ -48,10 +54,4 @@ 'eth_getFilterLogs', | ||
versionInfo: { persist: false, anonymous: false }, | ||
chainId: { persist: false, anonymous: false }, | ||
chainStatus: { persist: false, anonymous: false }, | ||
storageMetadata: { persist: false, anonymous: false }, | ||
refreshInterval: { persist: false, anonymous: false }, | ||
fileScheduleInterval: { persist: false, anonymous: false }, | ||
providerRequestLimit: { persist: false, anonymous: false }, | ||
providerRequests: { persist: false, anonymous: false }, | ||
securityAlertsEnabled: { persist: false, anonymous: false }, | ||
versionFileETag: { persist: false, anonymous: false }, | ||
@@ -92,10 +92,13 @@ }; | ||
* @param options.cdnBaseUrl - Base URL for the CDN. | ||
* @param options.providerRequestLimit - Limit of number of requests that can be sent to provider per transaction. | ||
* @param options.dataUpdateDuration - Duration after which data is fetched again. | ||
* @param options.fileFetchScheduleDuration - Duration after which next data file is fetched. | ||
* @param options.state - Initial state of the controller. | ||
* @param options.blockaidPublicKey - Public key of blcokaid for verifying signatures of data files. | ||
* @returns The PPOMController instance. | ||
*/ | ||
constructor({ chainId, messenger, onNetworkChange, provider, storageBackend, securityAlertsEnabled, onPreferencesChange, ppomProvider, cdnBaseUrl, state, }) { | ||
constructor({ chainId, messenger, onNetworkChange, provider, storageBackend, securityAlertsEnabled, onPreferencesChange, ppomProvider, cdnBaseUrl, providerRequestLimit, dataUpdateDuration, fileFetchScheduleDuration, state, blockaidPublicKey, }) { | ||
const initialState = { | ||
versionInfo: state?.versionInfo ?? [], | ||
storageMetadata: state?.storageMetadata ?? [], | ||
chainId, | ||
chainStatus: state?.chainStatus ?? { | ||
@@ -108,7 +111,2 @@ [chainId]: { | ||
}, | ||
refreshInterval: state?.refreshInterval ?? exports.REFRESH_TIME_INTERVAL, | ||
fileScheduleInterval: state?.fileScheduleInterval ?? FILE_FETCH_SCHEDULE_INTERVAL, | ||
providerRequestLimit: state?.providerRequestLimit ?? PROVIDER_REQUEST_LIMIT, | ||
providerRequests: state?.providerRequests ?? [], | ||
securityAlertsEnabled, | ||
}; | ||
@@ -133,3 +131,18 @@ super({ | ||
_PPOMController_ppomProvider.set(this, void 0); | ||
// base URL of the CDN | ||
_PPOMController_cdnBaseUrl.set(this, void 0); | ||
// Limit of number of requests ppom can send to the provider per transaction | ||
_PPOMController_providerRequestLimit.set(this, void 0); | ||
// Number of requests sent to provider by ppom for current transaction | ||
_PPOMController_providerRequests.set(this, 0); | ||
// id of current chain selected | ||
_PPOMController_chainId.set(this, void 0); | ||
// interval at which data files are refreshed, default will be 2 hours | ||
_PPOMController_dataUpdateDuration.set(this, void 0); | ||
// interval at which files for a network are fetched | ||
_PPOMController_fileFetchScheduleDuration.set(this, void 0); | ||
// true if user has enabled preference for blockaid security check | ||
_PPOMController_securityAlertsEnabled.set(this, void 0); | ||
_PPOMController_blockaidPublicKey.set(this, void 0); | ||
__classPrivateFieldSet(this, _PPOMController_chainId, chainId, "f"); | ||
__classPrivateFieldSet(this, _PPOMController_provider, provider, "f"); | ||
@@ -150,40 +163,15 @@ __classPrivateFieldSet(this, _PPOMController_ppomProvider, ppomProvider, "f"); | ||
__classPrivateFieldSet(this, _PPOMController_cdnBaseUrl, cdnBaseUrl, "f"); | ||
onNetworkChange((networkControllerState) => { | ||
const id = networkControllerState.providerConfig.chainId; | ||
if (id === this.state.chainId) { | ||
return; | ||
} | ||
let { chainStatus } = this.state; | ||
const existingNetworkObject = chainStatus[id]; | ||
chainStatus = { | ||
...chainStatus, | ||
[id]: { | ||
chainId: id, | ||
lastVisited: new Date().getTime(), | ||
dataFetched: existingNetworkObject?.dataFetched ?? false, | ||
}, | ||
}; | ||
this.update((draftState) => { | ||
draftState.chainId = id; | ||
draftState.chainStatus = chainStatus; | ||
}); | ||
}); | ||
onPreferencesChange((preferenceControllerState) => { | ||
const blockaidEnabled = preferenceControllerState.securityAlertsEnabled; | ||
if (blockaidEnabled === this.state.securityAlertsEnabled) { | ||
return; | ||
} | ||
if (blockaidEnabled) { | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_scheduleFileDownloadForAllChains).call(this); | ||
} | ||
else { | ||
clearInterval(__classPrivateFieldGet(this, _PPOMController_refreshDataInterval, "f")); | ||
clearInterval(__classPrivateFieldGet(this, _PPOMController_fileScheduleInterval, "f")); | ||
} | ||
this.update((draftState) => { | ||
draftState.securityAlertsEnabled = blockaidEnabled; | ||
}); | ||
}); | ||
__classPrivateFieldSet(this, _PPOMController_providerRequestLimit, providerRequestLimit ?? PROVIDER_REQUEST_LIMIT, "f"); | ||
__classPrivateFieldSet(this, _PPOMController_dataUpdateDuration, dataUpdateDuration ?? exports.REFRESH_TIME_INTERVAL, "f"); | ||
__classPrivateFieldSet(this, _PPOMController_fileFetchScheduleDuration, fileFetchScheduleDuration ?? FILE_FETCH_SCHEDULE_INTERVAL, "f"); | ||
__classPrivateFieldSet(this, _PPOMController_securityAlertsEnabled, securityAlertsEnabled, "f"); | ||
__classPrivateFieldSet(this, _PPOMController_blockaidPublicKey, blockaidPublicKey, "f"); | ||
// add new network to chainStatus list | ||
onNetworkChange(__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_onNetworkChange).bind(this)); | ||
// enable / disable PPOM validations as user changes preferences | ||
onPreferencesChange(__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_onPreferenceChange).bind(this)); | ||
// register message handlers | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_registerMessageHandlers).call(this); | ||
if (securityAlertsEnabled) { | ||
// start scheduled task to fetch data files | ||
if (__classPrivateFieldGet(this, _PPOMController_securityAlertsEnabled, "f")) { | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_scheduleFileDownloadForAllChains).call(this); | ||
@@ -195,12 +183,9 @@ } | ||
* This function will acquire mutex lock and invoke internal method #updatePPOM. | ||
* | ||
* @param options - Options. | ||
* @param options.updateForAllChains - True is update if required to be done for all chains in cache. | ||
*/ | ||
async updatePPOM({ updateForAllChains } = { updateForAllChains: true }) { | ||
if (!this.state.securityAlertsEnabled) { | ||
throw Error('User has not enabled blockaidSecurityCheck'); | ||
async updatePPOM() { | ||
if (!__classPrivateFieldGet(this, _PPOMController_securityAlertsEnabled, "f")) { | ||
throw Error('User has securityAlertsEnabled set to false'); | ||
} | ||
await __classPrivateFieldGet(this, _PPOMController_ppomMutex, "f").use(async () => { | ||
await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_updatePPOM).call(this, updateForAllChains); | ||
await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_updatePPOM).call(this); | ||
}); | ||
@@ -216,10 +201,10 @@ } | ||
async usePPOM(callback) { | ||
if (!this.state.securityAlertsEnabled) { | ||
throw Error('User has not enabled blockaidSecurityCheck'); | ||
if (!__classPrivateFieldGet(this, _PPOMController_securityAlertsEnabled, "f")) { | ||
throw Error('User has securityAlertsEnabled set to false'); | ||
} | ||
return await __classPrivateFieldGet(this, _PPOMController_ppomMutex, "f").use(async () => { | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_resetPPOM).call(this); | ||
await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_maybeUpdatePPOM).call(this); | ||
if (!__classPrivateFieldGet(this, _PPOMController_ppom, "f")) { | ||
__classPrivateFieldSet(this, _PPOMController_ppom, await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_getPPOM).call(this), "f"); | ||
} | ||
__classPrivateFieldSet(this, _PPOMController_ppom, await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_getPPOM).call(this), "f"); | ||
__classPrivateFieldSet(this, _PPOMController_providerRequests, 0, "f"); | ||
return await callback(__classPrivateFieldGet(this, _PPOMController_ppom, "f")); | ||
@@ -230,5 +215,50 @@ }); | ||
exports.PPOMController = PPOMController; | ||
_PPOMController_ppom = new WeakMap(), _PPOMController_provider = new WeakMap(), _PPOMController_storage = new WeakMap(), _PPOMController_refreshDataInterval = new WeakMap(), _PPOMController_fileScheduleInterval = new WeakMap(), _PPOMController_ppomMutex = new WeakMap(), _PPOMController_ppomProvider = new WeakMap(), _PPOMController_cdnBaseUrl = new WeakMap(), _PPOMController_instances = new WeakSet(), _PPOMController_registerMessageHandlers = function _PPOMController_registerMessageHandlers() { | ||
_PPOMController_ppom = new WeakMap(), _PPOMController_provider = new WeakMap(), _PPOMController_storage = new WeakMap(), _PPOMController_refreshDataInterval = new WeakMap(), _PPOMController_fileScheduleInterval = new WeakMap(), _PPOMController_ppomMutex = new WeakMap(), _PPOMController_ppomProvider = new WeakMap(), _PPOMController_cdnBaseUrl = new WeakMap(), _PPOMController_providerRequestLimit = new WeakMap(), _PPOMController_providerRequests = new WeakMap(), _PPOMController_chainId = new WeakMap(), _PPOMController_dataUpdateDuration = new WeakMap(), _PPOMController_fileFetchScheduleDuration = new WeakMap(), _PPOMController_securityAlertsEnabled = new WeakMap(), _PPOMController_blockaidPublicKey = new WeakMap(), _PPOMController_instances = new WeakSet(), _PPOMController_onNetworkChange = function _PPOMController_onNetworkChange(networkControllerState) { | ||
const id = networkControllerState.providerConfig.chainId; | ||
if (id === __classPrivateFieldGet(this, _PPOMController_chainId, "f")) { | ||
return; | ||
} | ||
let chainStatus = { ...this.state.chainStatus }; | ||
// delete ols chainId if total number of chainId is equal 5 | ||
const chainIds = Object.keys(chainStatus); | ||
if (chainIds.length >= NETWORK_CACHE_LIMIT.MAX) { | ||
const oldestChainId = chainIds.sort((c1, c2) => Number(chainStatus[c2]?.lastVisited) - | ||
Number(chainStatus[c1]?.lastVisited))[NETWORK_CACHE_LIMIT.MAX - 1]; | ||
if (oldestChainId) { | ||
delete chainStatus[oldestChainId]; | ||
} | ||
} | ||
const existingNetworkObject = chainStatus[id]; | ||
__classPrivateFieldSet(this, _PPOMController_chainId, id, "f"); | ||
chainStatus = { | ||
...chainStatus, | ||
[id]: { | ||
lastVisited: new Date().getTime(), | ||
dataFetched: existingNetworkObject?.dataFetched ?? false, | ||
}, | ||
}; | ||
this.update((draftState) => { | ||
draftState.chainStatus = chainStatus; | ||
}); | ||
}, _PPOMController_onPreferenceChange = function _PPOMController_onPreferenceChange(preferenceControllerState) { | ||
const blockaidEnabled = preferenceControllerState.securityAlertsEnabled; | ||
if (blockaidEnabled === __classPrivateFieldGet(this, _PPOMController_securityAlertsEnabled, "f")) { | ||
return; | ||
} | ||
if (blockaidEnabled) { | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_scheduleFileDownloadForAllChains).call(this); | ||
} | ||
else { | ||
clearInterval(__classPrivateFieldGet(this, _PPOMController_refreshDataInterval, "f")); | ||
clearInterval(__classPrivateFieldGet(this, _PPOMController_fileScheduleInterval, "f")); | ||
} | ||
__classPrivateFieldSet(this, _PPOMController_securityAlertsEnabled, blockaidEnabled, "f"); | ||
}, _PPOMController_registerMessageHandlers = function _PPOMController_registerMessageHandlers() { | ||
this.messagingSystem.registerActionHandler(`${controllerName}:usePPOM`, this.usePPOM.bind(this)); | ||
this.messagingSystem.registerActionHandler(`${controllerName}:updatePPOM`, this.updatePPOM.bind(this)); | ||
}, _PPOMController_resetPPOM = function _PPOMController_resetPPOM() { | ||
if (__classPrivateFieldGet(this, _PPOMController_ppom, "f")) { | ||
__classPrivateFieldGet(this, _PPOMController_ppom, "f").free(); | ||
__classPrivateFieldSet(this, _PPOMController_ppom, undefined, "f"); | ||
} | ||
}, _PPOMController_maybeUpdatePPOM = | ||
@@ -242,44 +272,25 @@ /** | ||
async function _PPOMController_maybeUpdatePPOM() { | ||
if (__classPrivateFieldGet(this, _PPOMController_ppom, "f")) { | ||
__classPrivateFieldGet(this, _PPOMController_ppom, "f").free(); | ||
__classPrivateFieldSet(this, _PPOMController_ppom, undefined, "f"); | ||
if (__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_isDataRequiredForCurrentChain).call(this)) { | ||
await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_getNewFilesForCurrentChain).call(this); | ||
} | ||
if (await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_shouldUpdate).call(this)) { | ||
await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_updatePPOM).call(this, false); | ||
} | ||
}, _PPOMController_shouldUpdate = | ||
/** | ||
* Determine if an update to the ppom configuration is needed. | ||
* The function will return true if data is not already fetched for the chain. | ||
* | ||
* @returns True if PPOM data requires update. | ||
*/ | ||
async function _PPOMController_shouldUpdate() { | ||
const { chainId, chainStatus } = this.state; | ||
return !chainStatus[chainId]?.dataFetched; | ||
}, _PPOMController_isDataRequiredForCurrentChain = function _PPOMController_isDataRequiredForCurrentChain() { | ||
const { chainStatus } = this.state; | ||
return !chainStatus[__classPrivateFieldGet(this, _PPOMController_chainId, "f")]?.dataFetched; | ||
}, _PPOMController_updatePPOM = | ||
/** | ||
* Update the PPOM configuration. | ||
* This function will fetch the latest version info when needed, and update the PPOM storage. | ||
* | ||
* @param updateForAllChains - True if update is required to be done for all chains in chainStatus. | ||
/* | ||
* Update the PPOM configuration for all chainId. | ||
*/ | ||
async function _PPOMController_updatePPOM(updateForAllChains) { | ||
const versionInfoUpdated = await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_updateVersionInfo).call(this, updateForAllChains); | ||
if (!versionInfoUpdated) { | ||
return; | ||
} | ||
await __classPrivateFieldGet(this, _PPOMController_storage, "f").syncMetadata(this.state.versionInfo); | ||
if (updateForAllChains) { | ||
async function _PPOMController_updatePPOM() { | ||
const versionInfoUpdated = await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_updateVersionInfo).call(this); | ||
if (versionInfoUpdated) { | ||
await __classPrivateFieldGet(this, _PPOMController_storage, "f").syncMetadata(this.state.versionInfo); | ||
await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_getNewFilesForAllChains).call(this); | ||
} | ||
else { | ||
await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_getNewFilesForCurrentChain).call(this); | ||
} | ||
}, _PPOMController_updateVersionInfo = | ||
/* | ||
* Fetch the version info from the CDN and update the version info in state. | ||
* Function returns true if update is available for versionInfo. | ||
*/ | ||
async function _PPOMController_updateVersionInfo(updateForAllChains) { | ||
const versionInfo = await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_fetchVersionInfo).call(this, updateForAllChains); | ||
async function _PPOMController_updateVersionInfo() { | ||
const versionInfo = await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_fetchVersionInfo).call(this); | ||
if (versionInfo) { | ||
@@ -297,15 +308,22 @@ this.update((draftState) => { | ||
file.checksum === fileVersionInfo.checksum); | ||
}, _PPOMController_checkFilePath = function _PPOMController_checkFilePath(filePath) { | ||
const filePathRegex = /^[\w./]+$/u; | ||
if (!filePath.match(filePathRegex)) { | ||
throw new Error(`Invalid file path for data file: ${filePath}`); | ||
} | ||
}, _PPOMController_getFile = | ||
/** | ||
/* | ||
* Gets a single file from CDN and write to the storage. | ||
* | ||
* @param fileVersionInfo - Information about the file to be retrieved. | ||
*/ | ||
async function _PPOMController_getFile(fileVersionInfo) { | ||
const { storageMetadata } = this.state; | ||
// do not fetch file if the storage version is latest | ||
if (__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_checkFilePresentInStorage).call(this, storageMetadata, fileVersionInfo)) { | ||
return; | ||
} | ||
// validate file path for valid characters | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_checkFilePath).call(this, fileVersionInfo.filePath); | ||
const fileUrl = `${URL_PREFIX}${__classPrivateFieldGet(this, _PPOMController_cdnBaseUrl, "f")}/${fileVersionInfo.filePath}`; | ||
const fileData = await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_fetchBlob).call(this, fileUrl); | ||
await (0, util_1.validateSignature)(fileData, fileVersionInfo.signature, __classPrivateFieldGet(this, _PPOMController_blockaidPublicKey, "f"), fileVersionInfo.filePath); | ||
await __classPrivateFieldGet(this, _PPOMController_storage, "f").writeFile({ | ||
@@ -327,14 +345,11 @@ data: fileData, | ||
}, _PPOMController_getNewFilesForCurrentChain = | ||
/** | ||
* Fetches new files and save them to storage. | ||
* The function is invoked if user if attempting transaction for a network, | ||
/* | ||
* Fetches new files for current network and save them to storage. | ||
* The function is invoked if user if attempting transaction for current network, | ||
* for which data is not previously fetched. | ||
* | ||
* @returns A promise that resolves to return void. | ||
*/ | ||
async function _PPOMController_getNewFilesForCurrentChain() { | ||
const { chainId, versionInfo } = this.state; | ||
const { versionInfo } = this.state; | ||
for (const fileVersionInfo of versionInfo) { | ||
// download all files for the current chain. | ||
if (fileVersionInfo.chainId !== chainId) { | ||
if (fileVersionInfo.chainId !== __classPrivateFieldGet(this, _PPOMController_chainId, "f")) { | ||
continue; | ||
@@ -347,6 +362,7 @@ } | ||
} | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_setChainIdDataFetched).call(this, chainId); | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_setChainIdDataFetched).call(this, __classPrivateFieldGet(this, _PPOMController_chainId, "f")); | ||
}, _PPOMController_getListOfFilesToBeFetched = function _PPOMController_getListOfFilesToBeFetched() { | ||
const { chainStatus, storageMetadata, versionInfo: stateVersionInfo, } = this.state; | ||
// create a map of chainId and files belonging to that chainId | ||
// not include the files for which the version in storage is the latest one | ||
const chainIdsFileInfoList = Object.keys(chainStatus).map((chainId) => ({ | ||
@@ -374,6 +390,10 @@ chainId, | ||
}, _PPOMController_deleteOldChainIds = function _PPOMController_deleteOldChainIds() { | ||
// We keep minimum of 2 chainIds in the state | ||
if (Object.keys(this.state.chainStatus)?.length <= NETWORK_CACHE_LIMIT.MIN) { | ||
return; | ||
} | ||
const currentTimestamp = new Date().getTime(); | ||
const oldChaninIds = Object.keys(this.state.chainStatus).filter((chainId) => this.state.chainStatus[chainId].lastVisited < | ||
currentTimestamp - exports.NETWORK_CACHE_DURATION && | ||
chainId !== this.state.chainId); | ||
chainId !== __classPrivateFieldGet(this, _PPOMController_chainId, "f")); | ||
const chainStatus = { ...this.state.chainStatus }; | ||
@@ -387,11 +407,11 @@ oldChaninIds.forEach((chainId) => { | ||
}, _PPOMController_getNewFilesForAllChains = | ||
/** | ||
* Function that fetched and saves to storage files for all networks. | ||
* Files are not fetched parallely but at an interval. | ||
* | ||
* @returns A promise that resolves to return void. | ||
/* | ||
* Function that fetches and saves to storage files for all networks. | ||
* Files are not fetched parallely but at regular intervals to | ||
* avoid sending a lot of parallel requests to CDN. | ||
*/ | ||
async function _PPOMController_getNewFilesForAllChains() { | ||
// delete chains more than a week old | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_deleteOldChainIds).call(this); | ||
// clear already scheduled fetch if any | ||
// clear existing scheduled task to fetch files if any | ||
if (__classPrivateFieldGet(this, _PPOMController_fileScheduleInterval, "f")) { | ||
@@ -402,11 +422,11 @@ clearInterval(__classPrivateFieldGet(this, _PPOMController_fileScheduleInterval, "f")); | ||
const fileToBeFetchedList = __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_getListOfFilesToBeFetched).call(this); | ||
// if schedule interval is large so that not all files can be fetched in | ||
// refreshInterval, reduce schedule interval | ||
let scheduleInterval = this.state.fileScheduleInterval; | ||
if (this.state.refreshInterval / (fileToBeFetchedList.length + 1) < | ||
scheduleInterval) { | ||
// Get scheduled interval, if schedule interval is large so that not all files can be fetched in | ||
// this.#dataUpdateDuration, reduce schedule interval | ||
let scheduleInterval = __classPrivateFieldGet(this, _PPOMController_fileFetchScheduleDuration, "f"); | ||
if (__classPrivateFieldGet(this, _PPOMController_dataUpdateDuration, "f") / (fileToBeFetchedList.length + 1) < | ||
__classPrivateFieldGet(this, _PPOMController_fileFetchScheduleDuration, "f")) { | ||
scheduleInterval = | ||
this.state.refreshInterval / (fileToBeFetchedList.length + 1); | ||
__classPrivateFieldGet(this, _PPOMController_dataUpdateDuration, "f") / (fileToBeFetchedList.length + 1); | ||
} | ||
// schedule files to be fetched in intervals | ||
// schedule files to be fetched in regular intervals | ||
__classPrivateFieldSet(this, _PPOMController_fileScheduleInterval, setInterval(() => { | ||
@@ -417,12 +437,17 @@ const fileToBeFetched = fileToBeFetchedList.pop(); | ||
} | ||
const { chainStatus } = this.state; | ||
const { fileVersionInfo, isLastFileOfNetwork } = fileToBeFetched; | ||
// get the file from CDN | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_getFile).call(this, fileVersionInfo) | ||
.then(() => { | ||
if (isLastFileOfNetwork) { | ||
// set dataFetched for chainId to true | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_setChainIdDataFetched).call(this, fileVersionInfo.chainId); | ||
} | ||
}) | ||
.catch((exp) => console.error(`Error in getting file ${fileVersionInfo.filePath}: ${exp.message}`)); | ||
// check here if chain is present in chainStatus, it may be removed from chainStatus | ||
// if more than 5 networks are added to it. | ||
if (chainStatus[fileVersionInfo.chainId]) { | ||
// get the file from CDN | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_getFile).call(this, fileVersionInfo) | ||
.then(() => { | ||
if (isLastFileOfNetwork) { | ||
// if this was last file for the chainId set dataFetched for chainId to true | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_setChainIdDataFetched).call(this, fileVersionInfo.chainId); | ||
} | ||
}) | ||
.catch((exp) => console.error(`Error in getting file ${fileVersionInfo.filePath}: ${exp.message}`)); | ||
} | ||
// clear interval if all files are fetched | ||
@@ -435,15 +460,11 @@ if (!fileToBeFetchedList.length) { | ||
/* | ||
* getAPIResponse - Generic method to fetch file from CDN. | ||
* Generic method to fetch file from CDN. | ||
*/ | ||
async function _PPOMController_getAPIResponse(url, options = {}, method = 'GET') { | ||
const controller = new AbortController(); | ||
const timeoutId = setTimeout(() => controller.abort(), 10000); | ||
const response = await (0, controller_utils_1.safelyExecute)(async () => fetch(url, { | ||
const response = await (0, controller_utils_1.safelyExecute)(async () => (0, controller_utils_1.timeoutFetch)(url, { | ||
method, | ||
cache: 'no-cache', | ||
redirect: 'error', | ||
signal: controller.signal, | ||
...options, | ||
}), true); | ||
clearTimeout(timeoutId); | ||
}, 10000), true); | ||
if (response?.status !== 200) { | ||
@@ -453,2 +474,19 @@ throw new Error(`Failed to fetch file with url: ${url}`); | ||
return response; | ||
}, _PPOMController_checkIfVersionInfoETagChanged = | ||
/* | ||
* Function sends a HEAD request to version info file and compares the ETag to the one saved in controller state. | ||
* If ETag is not changed we can be sure that there is not change in files and we do not need to fetch data again. | ||
*/ | ||
async function _PPOMController_checkIfVersionInfoETagChanged(url) { | ||
const headResponse = await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_getAPIResponse).call(this, url, { | ||
headers: versionInfoFileHeaders, | ||
}, 'HEAD'); | ||
const { versionFileETag } = this.state; | ||
if (headResponse.headers.get('ETag') === versionFileETag) { | ||
return false; | ||
} | ||
this.update((draftState) => { | ||
draftState.versionFileETag = headResponse.headers.get('ETag'); | ||
}); | ||
return true; | ||
}, _PPOMController_fetchVersionInfo = | ||
@@ -458,15 +496,8 @@ /* | ||
*/ | ||
async function _PPOMController_fetchVersionInfo(updateForAllChains) { | ||
async function _PPOMController_fetchVersionInfo() { | ||
const url = `${URL_PREFIX}${__classPrivateFieldGet(this, _PPOMController_cdnBaseUrl, "f")}/${PPOM_VERSION_FILE_NAME}`; | ||
if (updateForAllChains) { | ||
const headResponse = await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_getAPIResponse).call(this, url, { | ||
headers: versionInfoFileHeaders, | ||
}, 'HEAD'); | ||
const { versionFileETag } = this.state; | ||
if (headResponse.headers.get('ETag') === versionFileETag) { | ||
return undefined; | ||
} | ||
this.update((draftState) => { | ||
draftState.versionFileETag = headResponse.headers.get('ETag'); | ||
}); | ||
// If ETag is same it is not required to fetch data files again | ||
const eTagChanged = await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_checkIfVersionInfoETagChanged).call(this, url); | ||
if (!eTagChanged) { | ||
return undefined; | ||
} | ||
@@ -479,3 +510,3 @@ const response = await __classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_getAPIResponse).call(this, url, { | ||
/* | ||
* Fetch the blob from the PPOM cdn. | ||
* Fetch the blob file from the PPOM cdn. | ||
*/ | ||
@@ -490,21 +521,17 @@ async function _PPOMController_fetchBlob(url) { | ||
*/ | ||
async function _PPOMController_jsonRpcRequest(req) { | ||
async function _PPOMController_jsonRpcRequest(method, params) { | ||
return new Promise((resolve, reject) => { | ||
const currentTimestamp = new Date().getTime(); | ||
const requests = this.state.providerRequests.filter((requestTime) => requestTime - currentTimestamp < FILE_FETCH_SCHEDULE_INTERVAL); | ||
if (requests.length >= 5) { | ||
reject(new Error('Number of request to provider from PPOM exceed rate limit')); | ||
// Throw error if number of request to provider from PPOM exceed the limit for current transaction | ||
if (__classPrivateFieldGet(this, _PPOMController_providerRequests, "f") > __classPrivateFieldGet(this, _PPOMController_providerRequestLimit, "f")) { | ||
reject(util_1.PROVIDER_ERRORS.limitExceeded()); | ||
return; | ||
} | ||
this.update((draftState) => { | ||
draftState.providerRequests = [ | ||
...this.state.providerRequests, | ||
currentTimestamp, | ||
]; | ||
}); | ||
if (!ALLOWED_PROVIDER_CALLS.includes(req.method)) { | ||
reject(new Error(`Method not allowed on provider ${req.method}`)); | ||
__classPrivateFieldSet(this, _PPOMController_providerRequests, __classPrivateFieldGet(this, _PPOMController_providerRequests, "f") + 1, "f"); | ||
// Throw error if the method called on provider by PPOM is not allowed for PPOM | ||
if (!ALLOWED_PROVIDER_CALLS.includes(method)) { | ||
reject(util_1.PROVIDER_ERRORS.methodNotSupported()); | ||
return; | ||
} | ||
__classPrivateFieldGet(this, _PPOMController_provider, "f").sendAsync(req, (error, res) => { | ||
// Invoke provider and return result | ||
__classPrivateFieldGet(this, _PPOMController_provider, "f").sendAsync((0, util_1.createPayload)(method, params), (error, res) => { | ||
if (error) { | ||
@@ -520,12 +547,11 @@ reject(error); | ||
/* | ||
* Initialize the PPOM. | ||
* This function will be called when the PPOM is first used. | ||
* or when the PPOM is out of date. | ||
* It will load the PPOM data from storage and initialize the PPOM. | ||
* This function can be called to initialise PPOM or re-initilise it, | ||
* when new files are required to be passed to it. | ||
* | ||
* It will load the data files from storage and pass data files and wasm file to ppom. | ||
*/ | ||
async function _PPOMController_getPPOM() { | ||
const { ppomInit, PPOM } = __classPrivateFieldGet(this, _PPOMController_ppomProvider, "f"); | ||
const { chainId } = this.state; | ||
// Get all the files for the chainId | ||
const files = await Promise.all(this.state.versionInfo | ||
.filter((file) => file.chainId === chainId) | ||
.filter((file) => file.chainId === __classPrivateFieldGet(this, _PPOMController_chainId, "f")) | ||
.map(async (file) => { | ||
@@ -535,5 +561,13 @@ const data = await __classPrivateFieldGet(this, _PPOMController_storage, "f").readFile(file.name, file.chainId); | ||
})); | ||
// The following code throw error if no data files are found for the chainId. | ||
// This check has been put in place after suggestion of security team. | ||
// If we want to disable ppom validation on all instances of Metamask, | ||
// this can be achieved by returning empty data from version file. | ||
if (!files.length) { | ||
throw new Error(`Aborting validation as no files are found for the network with chainId: ${__classPrivateFieldGet(this, _PPOMController_chainId, "f")}`); | ||
} | ||
const { ppomInit, PPOM } = __classPrivateFieldGet(this, _PPOMController_ppomProvider, "f"); | ||
await ppomInit('./ppom_bg.wasm'); | ||
return PPOM.new(__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_jsonRpcRequest).bind(this), files); | ||
}, _PPOMController_onFileScheduledInterval = function _PPOMController_onFileScheduledInterval() { | ||
}, _PPOMController_onDataUpdateDuration = function _PPOMController_onDataUpdateDuration() { | ||
this.updatePPOM().catch(() => { | ||
@@ -543,6 +577,5 @@ // console.error(`Error while trying to update PPOM: ${exp.message}`); | ||
}, _PPOMController_scheduleFileDownloadForAllChains = function _PPOMController_scheduleFileDownloadForAllChains() { | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_onFileScheduledInterval).call(this); | ||
__classPrivateFieldSet(this, _PPOMController_refreshDataInterval, setInterval(__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_onFileScheduledInterval).bind(this), this.state.refreshInterval), "f"); | ||
__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_onDataUpdateDuration).call(this); | ||
__classPrivateFieldSet(this, _PPOMController_refreshDataInterval, setInterval(__classPrivateFieldGet(this, _PPOMController_instances, "m", _PPOMController_onDataUpdateDuration).bind(this), __classPrivateFieldGet(this, _PPOMController_dataUpdateDuration, "f")), "f"); | ||
}; | ||
// todo: handle empty version info file to hold on validations | ||
//# sourceMappingURL=ppom-controller.js.map |
{ | ||
"name": "@metamask/ppom-validator", | ||
"version": "0.1.2", | ||
"version": "0.2.0", | ||
"description": "This module has code to integrate Blockaid PPOM with MetaMask", | ||
@@ -36,3 +36,5 @@ "homepage": "https://github.com/MetaMask/ppom-validator#readme", | ||
"@metamask/controller-utils": "^4.0.0", | ||
"await-semaphore": "^0.1.3" | ||
"await-semaphore": "^0.1.3", | ||
"elliptic": "^6.5.4", | ||
"json-rpc-random-id": "^1.0.1" | ||
}, | ||
@@ -47,3 +49,5 @@ "devDependencies": { | ||
"@metamask/eslint-config-typescript": "^11.0.0", | ||
"@types/elliptic": "^6.4.14", | ||
"@types/jest": "^28.1.6", | ||
"@types/json-rpc-random-id": "^1.0.1", | ||
"@types/node": "^16", | ||
@@ -50,0 +54,0 @@ "@typescript-eslint/eslint-plugin": "^5.43.0", |
Sorry, the diff of this file is not supported yet
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
114346
15
1010
0
5
30
+ Addedelliptic@^6.5.4
+ Addedjson-rpc-random-id@^1.0.1