-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[FEATURE] Create detectSnapLocation method to install a Local Snap (#…
…5923) * Local snaps install with controller version 0.26.2
- Loading branch information
1 parent
437d572
commit 639b9bd
Showing
17 changed files
with
455 additions
and
275 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
v14 | ||
v16.13.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* eslint-disable import/prefer-default-export */ | ||
import ReactNativeBlobUtil, { FetchBlobResponse } from 'react-native-blob-util'; | ||
import Logger from '../../../util/Logger'; | ||
|
||
const SNAPS_FETCH_LOG_TAG = 'Snaps/ fetch'; | ||
|
||
/** | ||
* Reads and parses file from ReactNativeBlobUtil response | ||
* @param path The path to the file to read and parse. | ||
* @returns The parsed file data. | ||
*/ | ||
const readAndParseFile = async (path: string) => { | ||
try { | ||
const data = await ReactNativeBlobUtil.fs.readFile(path, 'utf8'); | ||
return data; | ||
} catch (error) { | ||
Logger.log(SNAPS_FETCH_LOG_TAG, 'readAndParseFile error', error); | ||
} | ||
}; | ||
|
||
/** | ||
* Converts a FetchBlobResponse object to a React Native Response object. | ||
* @param response The FetchBlobResponse object to convert. | ||
* @returns A new Response object with the same data as the input object. | ||
*/ | ||
const convertFetchBlobResponseToResponse = async ( | ||
fetchBlobResponse: FetchBlobResponse, | ||
): Promise<Response> => { | ||
const headers = new Headers(fetchBlobResponse.respInfo.headers); | ||
const status = fetchBlobResponse.respInfo.status; | ||
const dataPath = fetchBlobResponse.data; | ||
const data = await readAndParseFile(dataPath); | ||
const response = new Response(data, { headers, status }); | ||
return response; | ||
}; | ||
|
||
export const fetchFunction = async ( | ||
inputRequest: RequestInfo, | ||
): Promise<Response> => { | ||
const { config } = ReactNativeBlobUtil; | ||
const urlToFetch: string = | ||
typeof inputRequest === 'string' ? inputRequest : inputRequest.url; | ||
const response: FetchBlobResponse = await config({ fileCache: true }).fetch( | ||
'GET', | ||
urlToFetch, | ||
); | ||
return await convertFetchBlobResponseToResponse(response); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import { | ||
SnapManifest, | ||
VirtualFile, | ||
NpmSnapFileNames, | ||
createSnapManifest, | ||
normalizeRelative, | ||
} from '@metamask/snaps-utils'; | ||
|
||
import { SnapLocation } from './location'; | ||
|
||
export interface HttpOptions { | ||
/** | ||
* @default fetch | ||
*/ | ||
fetch?: typeof fetch; | ||
fetchOptions?: RequestInit; | ||
} | ||
|
||
export class HttpLocation implements SnapLocation { | ||
// We keep contents separate because then we can use only one Blob in cache, | ||
// which we convert to Uint8Array when actually returning the file. | ||
// | ||
// That avoids deepCloning file contents. | ||
// I imagine ArrayBuffers are copy-on-write optimized, meaning | ||
// in most often case we'll only have one file contents in common case. | ||
private readonly cache = new Map< | ||
string, | ||
{ file: VirtualFile; contents: Blob | string } | ||
>(); | ||
|
||
private validatedManifest?: VirtualFile<SnapManifest>; | ||
|
||
private readonly url: URL; | ||
|
||
private readonly fetchFn: typeof fetch; | ||
|
||
private readonly fetchOptions?: RequestInit; | ||
|
||
constructor(url: URL, opts: HttpOptions = {}) { | ||
// TODO get the asserts working from @metamask/utils | ||
// assertStruct(url.toString(), HttpSnapIdStruct, 'Invalid Snap Id: '); | ||
this.fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis); | ||
this.fetchOptions = opts.fetchOptions; | ||
this.url = url; | ||
} | ||
|
||
async manifest(): Promise<VirtualFile<SnapManifest>> { | ||
if (this.validatedManifest) { | ||
return this.validatedManifest.clone(); | ||
} | ||
|
||
// jest-fetch-mock doesn't handle new URL(), we need to convert .toString() | ||
const canonicalPath = new URL( | ||
NpmSnapFileNames.Manifest, | ||
this.url.toString(), | ||
).toString(); | ||
const contents = await ( | ||
await this.fetchFn(canonicalPath, this.fetchOptions) | ||
).text(); | ||
const manifest = JSON.parse(contents); | ||
const vfile = new VirtualFile<SnapManifest>({ | ||
value: contents, | ||
result: createSnapManifest(manifest), | ||
path: NpmSnapFileNames.Manifest, | ||
data: { canonicalPath }, | ||
}); | ||
this.validatedManifest = vfile; | ||
|
||
return this.manifest(); | ||
} | ||
|
||
async fetch(path: string): Promise<VirtualFile> { | ||
const relativePath = normalizeRelative(path); | ||
const cached = this.cache.get(relativePath); | ||
if (cached !== undefined) { | ||
const { file, contents } = cached; | ||
const value = contents.toString(); | ||
const vfile = file.clone(); | ||
vfile.value = value; | ||
return vfile; | ||
} | ||
|
||
const canonicalPath = this.toCanonical(relativePath).toString(); | ||
const response = await this.fetchFn(canonicalPath, this.fetchOptions); | ||
const vfile = new VirtualFile({ | ||
value: '', | ||
path: relativePath, | ||
data: { canonicalPath }, | ||
}); | ||
|
||
const blob = await response.text(); | ||
|
||
//TODO: get the asserts working from @metamask/utils | ||
// assert( | ||
// !this.cache.has(relativePath), | ||
// 'Corrupted cache, multiple files with same path.', | ||
// ); | ||
this.cache.set(relativePath, { file: vfile, contents: blob }); | ||
|
||
return this.fetch(relativePath); | ||
} | ||
|
||
get root(): URL { | ||
return new URL(this.url.toString()); | ||
} | ||
|
||
private toCanonical(path: string): URL { | ||
// TODO get the asserts working from @metamask/utils | ||
// assert(!path.startsWith('/'), 'Tried to parse absolute path.'); | ||
return new URL(path, this.url.toString()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './location'; | ||
export * from './fetch'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
/* eslint-disable import/prefer-default-export */ | ||
import { | ||
SnapIdPrefixes, | ||
SnapManifest, | ||
VirtualFile, | ||
} from '@metamask/snaps-utils'; | ||
|
||
import { HttpLocation, HttpOptions } from './http'; | ||
import { SnapLocation } from './location'; | ||
|
||
export class LocalLocation implements SnapLocation { | ||
readonly #http: HttpLocation; | ||
|
||
constructor(url: URL, opts: HttpOptions = {}) { | ||
// TODO get the asserts working from @metamask/utils | ||
// assertStruct(url.toString(), LocalSnapIdStruct, 'Invalid Snap Id'); | ||
// // TODO(ritave): Write deepMerge() which merges fetchOptions. | ||
// assert( | ||
// opts.fetchOptions === undefined, | ||
// 'Currently adding fetch options to local: is unsupported.', | ||
// ); | ||
|
||
this.#http = new HttpLocation( | ||
new URL(url.toString().slice(SnapIdPrefixes.local.length)), | ||
opts, | ||
); | ||
} | ||
|
||
async manifest(): Promise<VirtualFile<SnapManifest>> { | ||
const vfile = await this.#http.manifest(); | ||
|
||
return convertCanonical(vfile); | ||
} | ||
|
||
async fetch(path: string): Promise<VirtualFile> { | ||
return convertCanonical(await this.#http.fetch(path)); | ||
} | ||
|
||
get shouldAlwaysReload() { | ||
return true; | ||
} | ||
} | ||
|
||
/** | ||
* Converts vfiles with canonical `http:` paths into `local:` paths. | ||
* | ||
* @param vfile - The {@link VirtualFile} to convert. | ||
* @returns The same object with updated `.data.canonicalPath`. | ||
*/ | ||
function convertCanonical<Result>( | ||
vfile: VirtualFile<Result>, | ||
): VirtualFile<Result> { | ||
//TODO get the asserts working from @metamask/utils | ||
// assert(vfile.data.canonicalPath !== undefined); | ||
vfile.data.canonicalPath = `local:${vfile.data.canonicalPath}`; | ||
return vfile; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { SemVerRange, SnapManifest, VirtualFile } from '@metamask/snaps-utils'; | ||
import { LocalLocation } from './local'; | ||
|
||
export interface NpmOptions { | ||
/** | ||
* @default DEFAULT_REQUESTED_SNAP_VERSION | ||
*/ | ||
versionRange?: SemVerRange; | ||
/** | ||
* Whether to allow custom NPM registries outside of {@link DEFAULT_NPM_REGISTRY}. | ||
* | ||
* @default false | ||
*/ | ||
allowCustomRegistries?: boolean; | ||
} | ||
|
||
type DetectSnapLocationOptions = NpmOptions & { | ||
/** | ||
* The function used to fetch data. | ||
* | ||
* @default globalThis.fetch | ||
*/ | ||
fetch?: typeof fetch; | ||
/** | ||
* @default false | ||
*/ | ||
allowHttp?: boolean; | ||
}; | ||
|
||
/** | ||
* This should be exported from the @metamask/snaps-contracts package | ||
* for now we will define it ourselves | ||
*/ | ||
export interface SnapLocation { | ||
/** | ||
* All files are relative to the manifest, except the manifest itself. | ||
*/ | ||
manifest(): Promise<VirtualFile<SnapManifest>>; | ||
fetch(path: string): Promise<VirtualFile>; | ||
|
||
readonly shouldAlwaysReload?: boolean; | ||
} | ||
|
||
/** | ||
* Auto-magically detects which SnapLocation object to create based on the provided {@link location}. | ||
* | ||
* @param location - A {@link https://github.com/MetaMask/SIPs/blob/main/SIPS/sip-8.md SIP-8} uri. | ||
* @param opts - NPM options and feature flags. | ||
* @returns SnapLocation based on url. | ||
*/ | ||
export function detectSnapLocation( | ||
location: string | URL, | ||
opts?: DetectSnapLocationOptions, | ||
): SnapLocation { | ||
const root = new URL(location.toString()); | ||
switch (root.protocol) { | ||
case 'local:': | ||
return new LocalLocation(root, opts); | ||
default: | ||
throw new TypeError( | ||
`Unrecognized "${root.protocol}" snap location protocol.`, | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.