@orb-labs/orby-core-mini-react-native
Version:
React Native library for injecting Orby into dapps via WebView
2 lines (1 loc) • 24.7 kB
TypeScript
export declare const orbyInjectionScriptForDapps = "\n(function () {\n const originalFetch = window.fetch;\n const OriginalXMLHttpRequest = window.XMLHttpRequest;\n const rpcToChainIdMap = new Map();\n const rpcRequestMap = new Map();\n const callbackMap = new Map();\n let requestTimeoutLength = 10_000;\n let appRule;\n\n window.fetch = new Proxy(originalFetch, {\n apply: async function (target, thisArg, args) {\n try {\n const signal =\n args && args.length > 1 && args[1] ? args[1].signal : undefined;\n if (signal && signal.aborted) {\n return Promise.reject(\n new DOMException(\"Request was aborted\", \"AbortError\")\n );\n }\n\n const result = await handleInterceptedRequest(args, \"FETCH\", {\n target,\n thisArg,\n signal,\n });\n if (result.intercepted) {\n return convertToResponseObject(result.plaintextResponse);\n }\n\n if (args && args.length > 1) args[1] = { ...args[1], signal };\n return Reflect.apply(target, thisArg, args);\n } catch (error) {\n console.error(error);\n return Reflect.apply(target, thisArg, args);\n }\n },\n });\n\n window.XMLHttpRequest = function() {\n const xhr = new OriginalXMLHttpRequest();\n let orbyResponse = null;\n const state = {\n method: null,\n requestUrl: null,\n headers: null,\n allOpenArgs: null,\n };\n\n // Store the original methods before they get wrapped\n const originalOpen = xhr.open;\n const originalSend = xhr.send;\n const originalSetRequestHeader = xhr.setRequestHeader;\n\n // Create new open method\n xhr.open = function (...args) {\n state.allOpenArgs = args;\n state.method = args[0];\n state.requestUrl = args[1];\n state.headers = new Map();\n return originalOpen.apply(xhr, args);\n };\n\n xhr.setRequestHeader = function (header, value) {\n state.headers.set(header, value);\n return originalSetRequestHeader.apply(xhr, [header, value]);\n };\n\n xhr.send = function (body) {\n try {\n const originalReadyStateChange = xhr.onreadystatechange;\n xhr.onreadystatechange = function () {\n if (xhr.readyState == 4 && orbyResponse.intercepted) {\n returnOrbyResponse(xhr, orbyResponse.plaintextResponse);\n }\n\n if (originalReadyStateChange) {\n originalReadyStateChange.call(xhr);\n }\n };\n\n const xhrMetadata = {\n timeout: xhr.timeout,\n withCredentials: xhr.withCredentials,\n responseType: xhr.responseType,\n allOpenArgs: state.allOpenArgs,\n };\n const args = [\n state.requestUrl,\n { method: state.method, headers: state.headers, body },\n xhrMetadata,\n ];\n handleInterceptedRequest(args, \"XHR\", xhrMetadata)\n .then((result) => {\n orbyResponse = result;\n return originalSend.call(xhr, body);\n })\n .catch(() => {\n return originalSend.call(xhr, body);\n });\n } catch {\n return originalSend.call(xhr, body);\n }\n };\n\n return xhr;\n }\n\n // handles the intercepted request\n async function handleInterceptedRequest(\n args,\n requestType,\n fetchOrXhrMetadata\n ) {\n await fetchAppRule();\n const { requiresInterception, requestCategorizations } =\n passRequestThroughAppRule(args);\n let plaintextResponse;\n if (requiresInterception) {\n plaintextResponse = await processRequestByCategorizations(\n args,\n requestType,\n requestCategorizations,\n fetchOrXhrMetadata\n );\n }\n return { intercepted: requiresInterception, plaintextResponse };\n }\n\n // fetches app rule from Orby if its not already available.\n async function fetchAppRule() {\n if (!appRule) {\n const res = await sendOrbyRequest({\n action: \"getAppRule\",\n id: crypto.randomUUID(),\n appUrl: window.location.href,\n });\n appRule = res.appRule;\n requestTimeoutLength = res.requestTimeoutLength;\n }\n }\n\n // processes requests by the categorizations we got from orby.\n async function processRequestByCategorizations(\n args,\n requestType,\n requestCategorizations,\n fetchOrXhrMetadata\n ) {\n if (requestCategorizations.length <= 1) {\n return await processRequest(\n args,\n requestType,\n requestCategorizations[0],\n fetchOrXhrMetadata\n );\n } else if (shouldProcessTogether(requestCategorizations)) {\n let newRequestCategorization = {\n appRuleApplies: requestCategorizations.every((c) => c.appRuleApplies),\n isJsonRpcCall: requestCategorizations.every((c) => c.isJsonRpcCall),\n requiresRequestAlteration: requestCategorizations.every(\n (c) => c.requiresRequestAlteration\n ),\n requiresRequestRedirection: requestCategorizations.every(\n (c) => c.requiresRequestRedirection\n ),\n requiresResponseAlteration: requestCategorizations.every(\n (c) => c.requiresResponseAlteration\n ),\n };\n return await processRequest(\n args,\n requestType,\n newRequestCategorization,\n fetchOrXhrMetadata\n );\n } else {\n return await processRequestsIndividually(\n args,\n requestType,\n requestCategorizations,\n fetchOrXhrMetadata\n );\n }\n }\n\n // returns whether requests should be processed together or not.\n function shouldProcessTogether(requestCategorizations) {\n return (\n (requestCategorizations.every((c) => c.appRuleApplies) &&\n requestCategorizations.every((c) => c.isJsonRpcCall)) ||\n (requestCategorizations.every((c) => !c.appRuleApplies) &&\n requestCategorizations.every((c) => !c.isJsonRpcCall))\n );\n }\n\n // processes each request in a set individually.\n async function processRequestsIndividually(\n args,\n requestType,\n requestCategorizations,\n fetchOrXhrMetadata\n ) {\n const requestBody = args[1] || {};\n const payload = JSON.parse(stringifyBody(requestBody.body));\n const promises = requestCategorizations.map(async (c, index) => {\n const newArgs = JSON.parse(JSON.stringify(args));\n newArgs[1] = {\n ...newArgs[1],\n body: JSON.stringify([payload[index]]),\n };\n\n return await processRequest(newArgs, requestType, c, fetchOrXhrMetadata);\n });\n\n const responses = await Promise.all(promises);\n const resultsPromises = responses.map(async (curr) => {\n return curr.body[0];\n });\n\n const combinedResult = await Promise.all(resultsPromises);\n return { ...responses[0], body: combinedResult };\n }\n\n // preocesses requests\n async function processRequest(\n args,\n requestType,\n requestCategorization,\n fetchOrXhrMetadata\n ) {\n let response;\n if (requestCategorization.requiresRequestAlteration) {\n const newArgs = await alterRequest(args, fetchOrXhrMetadata);\n response = await sendOrdinaryRequest(\n newArgs,\n requestType,\n fetchOrXhrMetadata\n );\n } else if (requestCategorization.requiresRequestRedirection) {\n response = await redirectRequest(\n args,\n requestType,\n requestCategorization,\n fetchOrXhrMetadata\n );\n } else {\n response = await sendOrdinaryRequest(\n args,\n requestType,\n fetchOrXhrMetadata\n );\n }\n\n let finalResponse = await alterResponse(\n args,\n response,\n requestCategorization,\n fetchOrXhrMetadata\n );\n if (requestType == \"XHR\") formatXHRResponse(finalResponse);\n return finalResponse;\n }\n\n // alter request\n async function alterRequest(args, fetchOrXhrMetadata) {\n const result = await sendOrbyRequest(\n {\n action: \"alterRequest\",\n id: crypto.randomUUID(),\n appUrl: window.location.href,\n request: args,\n },\n fetchOrXhrMetadata.signal\n );\n\n if (result.success && result.response) {\n const clonedArgs = structuredClone(args);\n if (result.response.body.requestUrl)\n clonedArgs[0] = result.response.body.requestUrl;\n if (result.response.body.requestBody)\n clonedArgs[1].body = result.response.body.requestBody.body;\n\n return clonedArgs;\n }\n\n return args;\n }\n\n // redirect request\n async function redirectRequest(\n args,\n requestType,\n requestCategorization,\n fetchOrXhrMetadata\n ) {\n let message = { isJsonRpcCall: requestCategorization.isJsonRpcCall };\n if (requestCategorization.isJsonRpcCall)\n message.rpcChainId = await getUniqueChainIdentifier(args[0]);\n\n const result = await sendOrbyRequest(\n {\n action: \"redirectRequest\",\n id: crypto.randomUUID(),\n appUrl: window.location.href,\n request: args,\n ...message,\n },\n fetchOrXhrMetadata.signal\n );\n\n if (!result.success) {\n return await sendOrdinaryRequest(args, requestType, fetchOrXhrMetadata);\n } else if (result.success && requestType === \"FETCH\") {\n return result.response;\n } else {\n const normalResponse = await sendOrdinaryRequest(\n args,\n requestType,\n fetchOrXhrMetadata\n );\n return { ...normalResponse, body: result.response.body };\n }\n }\n\n // alter response\n async function alterResponse(\n args,\n currentResponse,\n requestCategorization,\n fetchOrXhrMetadata\n ) {\n if (requestCategorization.requiresResponseAlteration) {\n if (fetchOrXhrMetadata.signal && args.length > 1) {\n delete args[1].signal;\n }\n\n const alteredResponse = await sendOrbyRequest(\n {\n action: \"alterResponse\",\n id: crypto.randomUUID(),\n appUrl: window.location.href,\n request: [args[0], currentResponse.body, args],\n },\n fetchOrXhrMetadata.signal\n );\n if (alteredResponse.success) {\n return { ...currentResponse, body: alteredResponse.response.body };\n }\n }\n return currentResponse;\n }\n\n // sends ordinary fetch and chr requests\n async function sendOrdinaryRequest(args, requestType, fetchOrXhrMetadata) {\n if (requestType == \"FETCH\") {\n return await handleFetchRequest(args, fetchOrXhrMetadata);\n } else if (requestType == \"XHR\") {\n return await handleXHRequest(args, fetchOrXhrMetadata);\n }\n }\n\n // handle original xhr request\n async function handleXHRequest(args, fetchOrXhrMetadata) {\n return new Promise((resolve, reject) => {\n const localxhr = new window.originalXMLHttpRequest();\n\n localxhr.onreadystatechange = function () {\n if (localxhr.readyState === 4) {\n resolve(captureXHRResponse(localxhr));\n }\n };\n\n localxhr.onerror = () => {\n reject(new Error(\"Network error occurred\"));\n };\n\n localxhr.timeout = fetchOrXhrMetadata.timeout;\n localxhr.withCredentials = fetchOrXhrMetadata.withCredentials;\n localxhr.responseType = fetchOrXhrMetadata.responseType;\n localxhr.open(...fetchOrXhrMetadata.allOpenArgs);\n args[1].headers.forEach((value, key) => {\n localxhr.setRequestHeader(key, value);\n });\n\n // Send the request\n if (args[1].body) {\n localxhr.send(args[1].body);\n } else {\n localxhr.send();\n }\n });\n }\n\n // handles original fetch request\n async function handleFetchRequest(args, fetchOrXhrMetadata) {\n const { target, thisArg, signal } = fetchOrXhrMetadata;\n if (args && args.length > 1) args[1] = { ...args[1], signal };\n const response = await Reflect.apply(target, thisArg, args);\n const data = await response.json();\n return {\n headers: [...response.headers.entries()],\n status: response.status,\n statusText: response.statusText,\n body: data,\n };\n }\n\n // Function that receives a response from Orby.\n window.receiveOrbyResponse = function (message) {\n if (typeof payload === 'string') {\n try {\n payload = JSON.parse(payload);\n } catch (e) {\n return;\n }\n }\n const { id, response } = JSON.parse(message);\n if (type && id && callbackMap.has(id)) {\n const { resolve, timeOut } = callbackMap.get(id);\n resolve(response);\n clearTimeout(timeOut);\n callbackMap.delete(id);\n }\n };\n\n // Function that sends a request to the Orby.\n function sendOrbyRequest(message, signal) {\n return new Promise((resolve, reject) => {\n const timeOut = setTimeout(\n () => timeOutRequest(message),\n requestTimeoutLength\n );\n callbackMap.set(message.id, { resolve, reject, timeOut });\n if (signal) {\n signal.addEventListener(\"abort\", () => abortRequest(message), {\n once: true,\n });\n\n if (message.request && message.request.length > 1) {\n delete message.request[1].signal;\n }\n }\n window.ReactNativeWebView.postMessage(\n JSON.stringify({ type: \"ODI_REQUEST\", ...message })\n );\n });\n }\n\n // Pass a request through the app rules\n function passRequestThroughAppRule(request) {\n const requestUrl = request[0];\n const requestBody = request[1] || {};\n\n // return if there are not requestBody or if the app domain is prohibited.\n if (appRule.isProhibitedApp) {\n return {\n errored: true,\n requiresInterception: false,\n requestCategorizations: [\n {\n appRuleApplies: false,\n isJsonRpcCall: false,\n requiresRequestAlteration: false,\n requiresResponseAlteration: false,\n requiresRequestRedirection: false,\n },\n ],\n };\n }\n\n // check if the request requires app specific request alteration.\n const requiresRequestAlteration =\n appRule.appSpecificRequestAlterationRules.some(\n (appSpecificRequestAlterationRule) => {\n const regex = new RegExp(appSpecificRequestAlterationRule);\n return regex.test(requestUrl);\n }\n );\n\n // check if the request requires app specific response alteration.\n const requiresResponseAlteration =\n appRule.appSpecificResponseAlterationRules.some(\n (appSpecificResponseAlterationRule) => {\n const regex = new RegExp(appSpecificResponseAlterationRule);\n return regex.test(requestUrl);\n }\n );\n\n // check if the request requires app specific request redirection.\n const requiresRequestRedirection =\n appRule.appSpecificRequestRedirectRules.some(\n (appSpecificRequestRedirectRule) => {\n const regex = new RegExp(appSpecificRequestRedirectRule);\n return regex.test(requestUrl);\n }\n );\n\n if (\n requiresRequestAlteration ||\n requiresRequestRedirection ||\n requiresResponseAlteration\n ) {\n return {\n errored: false,\n requiresInterception: true,\n requestCategorizations: [\n {\n appRuleApplies: true,\n isJsonRpcCall: false,\n requiresRequestAlteration,\n requiresResponseAlteration,\n requiresRequestRedirection,\n },\n ],\n };\n } else if (requestBody && requestBody.body) {\n try {\n const payload = JSON.parse(stringifyBody(requestBody.body));\n\n if (Array.isArray(payload)) {\n const requestCategorizations = payload.map((operation) =>\n appRuleAppliesAndIsJsonRpcCall(operation)\n );\n return {\n errored: false,\n requiresInterception: requestCategorizations.some(\n (c) => c.appRuleApplies\n ),\n requestCategorizations,\n };\n } else {\n const requestCategorization = appRuleAppliesAndIsJsonRpcCall(payload);\n return {\n errored: false,\n requiresInterception: requestCategorization.appRuleApplies,\n requestCategorizations: [requestCategorization],\n };\n }\n } catch (error) {\n return {\n errored: true,\n requiresInterception: false,\n requestCategorizations: [\n {\n appRuleApplies: false,\n isJsonRpcCall: false,\n requiresRequestAlteration: false,\n requiresResponseAlteration: false,\n requiresRequestRedirection: false,\n },\n ],\n };\n }\n } else {\n return {\n errored: false,\n requiresInterception: false,\n requestCategorizations: [\n {\n appRuleApplies: false,\n isJsonRpcCall: false,\n requiresRequestAlteration: false,\n requiresResponseAlteration: false,\n requiresRequestRedirection: false,\n },\n ],\n };\n }\n }\n\n function appRuleAppliesAndIsJsonRpcCall(payload) {\n const isJsonRpcCall =\n payload.hasOwnProperty(\"jsonrpc\") && payload.jsonrpc !== undefined;\n const requiresRequestRedirection =\n isJsonRpcCall && appRule.rpcRedirectRules.includes(payload.method);\n return {\n appRuleApplies: requiresRequestRedirection,\n isJsonRpcCall,\n requiresRequestAlteration: false,\n requiresResponseAlteration: false,\n requiresRequestRedirection,\n };\n }\n\n // Function that fetches the rpc chain id\n async function getUniqueChainIdentifier(appRpcUrl) {\n try {\n if (rpcToChainIdMap.has(appRpcUrl)) {\n return rpcToChainIdMap.get(appRpcUrl);\n } else if (rpcRequestMap.has(appRpcUrl)) {\n return rpcRequestMap.get(appRpcUrl);\n }\n\n const fetchPromise = (async () => {\n const responses = appRule.rpcFunctionsForUniqueChainIdentifiers.map(\n async (rpcFunc) => {\n const response = await originalFetch(appRpcUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n id: Math.floor(Math.random() * 1000) + 1,\n jsonrpc: \"2.0\",\n method: rpcFunc,\n params: [],\n }),\n });\n return response.json();\n }\n );\n\n const results = await Promise.allSettled(responses);\n const successfulResults = results.filter(\n (result) => result.status === \"fulfilled\"\n );\n if (successfulResults.length === 0) {\n throw new Error(\"Unable to fetch unique chain identifier\");\n }\n\n const chainId = parseInt(successfulResults[0]?.value?.result, 16);\n const identifier = Number.isNaN(chainId)\n ? successfulResults[1]?.value?.result\n : chainId.toString();\n\n rpcToChainIdMap.set(appRpcUrl, identifier);\n rpcRequestMap.delete(appRpcUrl);\n return identifier;\n })();\n\n rpcRequestMap.set(appRpcUrl, fetchPromise);\n return fetchPromise;\n } catch (error) {\n rpcRequestMap.delete(appRpcUrl);\n throw error;\n }\n }\n\n // Function that times out a request by deleting its callback functions.\n function abortRequest(message) {\n if (callbackMap.has(message.id)) {\n const { reject } = callbackMap.get(message.id);\n reject(new DOMException(\"Request was aborted\", \"AbortError\"));\n callbackMap.delete(message.id);\n }\n }\n\n // Function that times out a request by deleting its callback functions.\n function timeOutRequest(message) {\n if (callbackMap.has(message.id)) {\n const { reject } = callbackMap.get(message.id);\n reject(new Error(\"Request timed out\"));\n callbackMap.delete(message.id);\n }\n }\n\n // Function that converts a plaintext response to a response object.\n function convertToResponseObject(plaintextResponse) {\n return new Response(JSON.stringify(plaintextResponse.body), {\n headers: new Headers(plaintextResponse.headers),\n status: plaintextResponse.status,\n statusText: plaintextResponse.statusText,\n });\n }\n\n // Function that turns a request body into a string.\n function stringifyBody(body) {\n if (typeof body === \"string\") {\n return body;\n } else if (body instanceof Uint8Array) {\n const decoder = new TextDecoder(\"utf-8\");\n return decoder.decode(body);\n }\n }\n\n // Function that captures the response from a XHR request.\n function captureXHRResponse(xhr) {\n return {\n body: xhr.response,\n response: xhr.response,\n responseText: xhr.responseText,\n responseType: xhr.responseType,\n responseURL: xhr.responseURL,\n responseXML: xhr.responseXML,\n readyState: xhr.readyState,\n status: xhr.status,\n statusText: xhr.statusText,\n getAllResponseHeaders: xhr.getAllResponseHeaders(),\n timeout: xhr.timeout,\n withCredentials: xhr.withCredentials,\n upload: xhr.upload,\n };\n }\n\n // Function that returns orby response\n function returnOrbyResponse(xhr, plaintextResponse) {\n Object.defineProperties(xhr, {\n response: {\n value: plaintextResponse.body,\n writable: true,\n enumerable: true,\n configurable: true,\n },\n responseText: {\n value: plaintextResponse.responseText,\n writable: true,\n enumerable: true,\n configurable: true,\n },\n status: {\n value: plaintextResponse.status,\n writable: true,\n enumerable: true,\n configurable: true,\n },\n statusText: {\n value: plaintextResponse.statusText,\n writable: true,\n enumerable: true,\n configurable: true,\n },\n });\n }\n\n // formats XHR response properly.\n function formatXHRResponse(plaintextResponse) {\n try {\n if (plaintextResponse.response === plaintextResponse.responseText) {\n plaintextResponse.responseText = plaintextResponse.body;\n }\n\n switch (plaintextResponse.responseType) {\n case \"\":\n case \"text\":\n if (typeof plaintextResponse.body !== \"string\") {\n plaintextResponse.body = JSON.stringify(plaintextResponse.body);\n }\n return;\n\n case \"json\":\n if (typeof body === \"string\") {\n plaintextResponse.body = JSON.parse(plaintextResponse.body);\n }\n return;\n\n case \"arraybuffer\":\n if (plaintextResponse.body instanceof ArrayBuffer) return;\n if (typeof plaintextResponse.body === \"string\") {\n const encoder = new TextEncoder();\n plaintextResponse.body = encoder.encode(\n plaintextResponse.body\n ).buffer;\n } else if (typeof plaintextResponse.body === \"object\") {\n const str = JSON.stringify(plaintextResponse.body);\n const encoder = new TextEncoder();\n plaintextResponse.body = encoder.encode(str).buffer;\n }\n return;\n\n case \"blob\":\n if (plaintextResponse.body instanceof Blob) return;\n if (typeof plaintextResponse.body === \"string\") {\n plaintextResponse.body = new Blob([plaintextResponse.body], {\n type: \"text/plain\",\n });\n } else if (typeof plaintextResponse.body === \"object\") {\n plaintextResponse.body = new Blob(\n [JSON.stringify(plaintextResponse.body)],\n { type: \"application/json\" }\n );\n }\n return;\n\n case \"document\":\n if (plaintextResponse.body instanceof Document) return;\n if (typeof plaintextResponse.body === \"string\") {\n const parser = new DOMParser();\n plaintextResponse.body = parser.parseFromString(\n plaintextResponse.body,\n \"text/html\"\n );\n }\n return;\n\n default:\n return;\n }\n } catch (error) {\n return;\n }\n }\n})();\n";