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