From 63593d41864f33c56eebef798f1a7c6e6485b5df Mon Sep 17 00:00:00 2001 From: moody Date: Tue, 25 Jun 2019 19:04:37 -0400 Subject: [PATCH] Add the iframe provider --- packages/providers/iframe-provider.d.ts | 10 +++ packages/providers/iframe-provider.js | 87 ++++++++++++++++++ packages/providers/index.d.ts | 3 +- packages/providers/index.js | 5 ++ packages/providers/src.ts/iframe-provider.ts | 94 ++++++++++++++++++++ packages/providers/src.ts/index.ts | 3 +- packages/providers/src.ts/web3-provider.ts | 23 ++++- packages/providers/web3-provider.d.ts | 14 ++- packages/providers/web3-provider.js | 9 +- 9 files changed, 240 insertions(+), 8 deletions(-) create mode 100644 packages/providers/iframe-provider.d.ts create mode 100644 packages/providers/iframe-provider.js create mode 100644 packages/providers/src.ts/iframe-provider.ts diff --git a/packages/providers/iframe-provider.d.ts b/packages/providers/iframe-provider.d.ts new file mode 100644 index 0000000000..b19540ea69 --- /dev/null +++ b/packages/providers/iframe-provider.d.ts @@ -0,0 +1,10 @@ +import { Networkish } from '@ethersproject/networks'; +import { Web3Provider } from './web3-provider'; +interface IFrameProviderOptions { + timeoutMs?: number; + targetOrigin?: string; +} +export default class IFrameProvider extends Web3Provider { + constructor(options?: IFrameProviderOptions, network?: Networkish); +} +export {}; diff --git a/packages/providers/iframe-provider.js b/packages/providers/iframe-provider.js new file mode 100644 index 0000000000..dff67ec6d9 --- /dev/null +++ b/packages/providers/iframe-provider.js @@ -0,0 +1,87 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +var web3_provider_1 = require("./web3-provider"); +var IFrameEthereumProvider = /** @class */ (function () { + function IFrameEthereumProvider(_a) { + var _b = _a === void 0 ? {} : _a, _c = _b.timeoutMs, timeoutMs = _c === void 0 ? 60000 : _c, _d = _b.targetOrigin, targetOrigin = _d === void 0 ? '*' : _d; + this.listenerAttached = false; + this.callbackMap = {}; + this.targetOrigin = targetOrigin; + this.timeoutMs = timeoutMs; + } + /** + * Return true if the current context is a window within an iframe. + */ + IFrameEthereumProvider.isWithinIframe = function () { + return window && window.parent && window.parent !== window.self; + }; + /** + * Handles events from the parent window + * @param event received from the parent window + */ + IFrameEthereumProvider.prototype.handleEvents = function (event) { + if (event.origin === this.targetOrigin) { + if (event.data && this.callbackMap[event.data.id]) { + this.callbackMap[event.data.id](null, event.data); + // Remove the resolver from the map so it is not rejected. + delete this.callbackMap[event.data.id]; + } + } + }; + /** + * Attach the message listener only once for this provider. + */ + IFrameEthereumProvider.prototype.attachListenerOnce = function () { + if (this.listenerAttached) { + return; + } + this.listenerAttached = true; + window.addEventListener('message', this.handleEvents); + }; + /** + * Send a JSON RPC to the parent window. + */ + IFrameEthereumProvider.prototype.sendAsync = function (request, callback) { + var _this = this; + if (!IFrameEthereumProvider.isWithinIframe()) { + throw new Error('Not embedded in an iframe.'); + } + var parentWindow = window && window.parent; + this.attachListenerOnce(); + var id = request.id; + this.callbackMap[id] = callback; + parentWindow.postMessage(request, this.targetOrigin); + setTimeout(function () { + if (_this.callbackMap[id]) { + callback(new Error("The RPC to the parent iframe has timed out after " + _this.timeoutMs + "ms"), null); + } + // We no longer care about the result of the RPC after the time out. + delete _this.callbackMap[id]; + }, this.timeoutMs); + }; + return IFrameEthereumProvider; +}()); +var IFrameProvider = /** @class */ (function (_super) { + __extends(IFrameProvider, _super); + function IFrameProvider(options, network) { + var _this = this; + var executor = new IFrameEthereumProvider(options); + _this = _super.call(this, executor, network) || this; + return _this; + } + return IFrameProvider; +}(web3_provider_1.Web3Provider)); +exports.default = IFrameProvider; diff --git a/packages/providers/index.d.ts b/packages/providers/index.d.ts index 36c51ab9f1..1fb36bc0a1 100644 --- a/packages/providers/index.d.ts +++ b/packages/providers/index.d.ts @@ -11,4 +11,5 @@ import { JsonRpcProvider, JsonRpcSigner } from "./json-rpc-provider"; import { NodesmithProvider } from "./nodesmith-provider"; import { Web3Provider } from "./web3-provider"; import { AsyncSendable } from "./web3-provider"; -export { Provider, BaseProvider, FallbackProvider, AlchemyProvider, EtherscanProvider, InfuraProvider, JsonRpcProvider, NodesmithProvider, Web3Provider, IpcProvider, JsonRpcSigner, getNetwork, Block, BlockTag, EventType, Filter, Log, Listener, TransactionReceipt, TransactionRequest, TransactionResponse, AsyncSendable, Network, Networkish }; +import IFrameProvider from './iframe-provider'; +export { Provider, BaseProvider, FallbackProvider, AlchemyProvider, EtherscanProvider, InfuraProvider, JsonRpcProvider, NodesmithProvider, Web3Provider, IFrameProvider, IpcProvider, JsonRpcSigner, getNetwork, Block, BlockTag, EventType, Filter, Log, Listener, TransactionReceipt, TransactionRequest, TransactionResponse, AsyncSendable, Network, Networkish }; diff --git a/packages/providers/index.js b/packages/providers/index.js index f449f2827a..8d4185ce89 100644 --- a/packages/providers/index.js +++ b/packages/providers/index.js @@ -1,4 +1,7 @@ "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); var abstract_provider_1 = require("@ethersproject/abstract-provider"); exports.Provider = abstract_provider_1.Provider; @@ -23,3 +26,5 @@ var nodesmith_provider_1 = require("./nodesmith-provider"); exports.NodesmithProvider = nodesmith_provider_1.NodesmithProvider; var web3_provider_1 = require("./web3-provider"); exports.Web3Provider = web3_provider_1.Web3Provider; +var iframe_provider_1 = __importDefault(require("./iframe-provider")); +exports.IFrameProvider = iframe_provider_1.default; diff --git a/packages/providers/src.ts/iframe-provider.ts b/packages/providers/src.ts/iframe-provider.ts new file mode 100644 index 0000000000..8682763ee5 --- /dev/null +++ b/packages/providers/src.ts/iframe-provider.ts @@ -0,0 +1,94 @@ +import { Networkish } from '@ethersproject/networks'; +import { AsyncSendable, JsonRpc, Web3Provider } from './web3-provider'; + +interface IFrameProviderOptions { + // How long in milliseconds to wait for a response from the parent window before timing out. + timeoutMs?: number; + + // The origin that should be allowed to host this DAPP. Passed directly to Window#postMessage. + targetOrigin?: string; +} + +class IFrameEthereumProvider implements AsyncSendable { + public readonly isMetaMask: false; + + private readonly targetOrigin: string; + private readonly timeoutMs: number; + private listenerAttached: boolean = false; + private callbackMap: { [ rpcId: number ]: (error: any, data: any) => void } = {}; + + constructor({ timeoutMs = 60000, targetOrigin = '*' }: IFrameProviderOptions = {}) { + this.targetOrigin = targetOrigin; + this.timeoutMs = timeoutMs; + } + + /** + * Return true if the current context is a window within an iframe. + */ + public static isWithinIframe(): boolean { + return window && window.parent && window.parent !== window.self; + } + + /** + * Handles events from the parent window + * @param event received from the parent window + */ + private handleEvents(event: MessageEvent): void { + if (event.origin === this.targetOrigin) { + if (event.data && this.callbackMap[ event.data.id ]) { + this.callbackMap[ event.data.id ](null, event.data); + + // Remove the resolver from the map so it is not rejected. + delete this.callbackMap[ event.data.id ]; + } + } + } + + /** + * Attach the message listener only once for this provider. + */ + private attachListenerOnce(): void { + if (this.listenerAttached) { + return; + } + + this.listenerAttached = true; + + window.addEventListener('message', this.handleEvents); + } + + /** + * Send a JSON RPC to the parent window. + */ + public sendAsync(request: JsonRpc, callback: (error: any, response: any) => void): void { + if (!IFrameEthereumProvider.isWithinIframe()) { + throw new Error('Not embedded in an iframe.'); + } + + const parentWindow = window && window.parent; + + this.attachListenerOnce(); + + const id = request.id; + + this.callbackMap[ id ] = callback; + + parentWindow.postMessage(request, this.targetOrigin); + + setTimeout(() => { + if (this.callbackMap[ id ]) { + callback(new Error(`The RPC to the parent iframe has timed out after ${this.timeoutMs}ms`), null); + } + + // We no longer care about the result of the RPC after the time out. + delete this.callbackMap[ id ]; + }, this.timeoutMs); + } +} + +export default class IFrameProvider extends Web3Provider { + constructor(options?: IFrameProviderOptions, network?: Networkish) { + const executor = new IFrameEthereumProvider(options); + super(executor, network); + } +} \ No newline at end of file diff --git a/packages/providers/src.ts/index.ts b/packages/providers/src.ts/index.ts index 1828644dd4..a4ce8f3b31 100644 --- a/packages/providers/src.ts/index.ts +++ b/packages/providers/src.ts/index.ts @@ -28,6 +28,7 @@ import { NodesmithProvider } from "./nodesmith-provider"; import { Web3Provider } from "./web3-provider"; import { AsyncSendable } from "./web3-provider"; +import IFrameProvider from './iframe-provider'; //////////////////////// @@ -51,7 +52,7 @@ export { JsonRpcProvider, NodesmithProvider, Web3Provider, - + IFrameProvider, IpcProvider, diff --git a/packages/providers/src.ts/web3-provider.ts b/packages/providers/src.ts/web3-provider.ts index 4082de9584..04ea946b41 100644 --- a/packages/providers/src.ts/web3-provider.ts +++ b/packages/providers/src.ts/web3-provider.ts @@ -7,13 +7,20 @@ import { defineReadOnly } from "@ethersproject/properties"; import { JsonRpcProvider } from "./json-rpc-provider"; +export type JsonRpc = { + jsonrpc: '2.0'; + id: number; + method: TMethod; + params: TParams; +} + // Exported Types export type AsyncSendable = { isMetaMask?: boolean; host?: string; path?: string; - sendAsync?: (request: any, callback: (error: any, response: any) => void) => void - send?: (request: any, callback: (error: any, response: any) => void) => void + sendAsync?: (request: JsonRpc, callback: (error: any, response: any) => void) => void + send?: (request: JsonRpc, callback: (error: any, response: any) => void) => void } /* @@ -53,6 +60,14 @@ export class Web3Provider extends JsonRpcProvider { defineReadOnly(this, "_web3Provider", web3Provider); } + /** + * Generate a unique identifier for a JSON RPC. + */ + private static getUniqueId(): number { + return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); + } + + send(method: string, params: any): Promise { // Metamask complains about eth_sign (and on some versions hangs) @@ -63,10 +78,12 @@ export class Web3Provider extends JsonRpcProvider { } return new Promise((resolve, reject) => { + const uniqueId = Web3Provider.getUniqueId(); + let request = { method: method, params: params, - id: 42, + id: uniqueId, jsonrpc: "2.0" }; diff --git a/packages/providers/web3-provider.d.ts b/packages/providers/web3-provider.d.ts index 1562b10a2b..617acaf019 100644 --- a/packages/providers/web3-provider.d.ts +++ b/packages/providers/web3-provider.d.ts @@ -1,15 +1,25 @@ import { Networkish } from "@ethersproject/networks"; import { JsonRpcProvider } from "./json-rpc-provider"; +export declare type JsonRpc = { + jsonrpc: '2.0'; + id: number; + method: TMethod; + params: TParams; +}; export declare type AsyncSendable = { isMetaMask?: boolean; host?: string; path?: string; - sendAsync?: (request: any, callback: (error: any, response: any) => void) => void; - send?: (request: any, callback: (error: any, response: any) => void) => void; + sendAsync?: (request: JsonRpc, callback: (error: any, response: any) => void) => void; + send?: (request: JsonRpc, callback: (error: any, response: any) => void) => void; }; export declare class Web3Provider extends JsonRpcProvider { readonly _web3Provider: AsyncSendable; private _sendAsync; constructor(web3Provider: AsyncSendable, network?: Networkish); + /** + * Generate a unique identifier for a JSON RPC. + */ + private static getUniqueId; send(method: string, params: any): Promise; } diff --git a/packages/providers/web3-provider.js b/packages/providers/web3-provider.js index 351208a42c..e11499124c 100644 --- a/packages/providers/web3-provider.js +++ b/packages/providers/web3-provider.js @@ -52,6 +52,12 @@ var Web3Provider = /** @class */ (function (_super) { properties_1.defineReadOnly(_this, "_web3Provider", web3Provider); return _this; } + /** + * Generate a unique identifier for a JSON RPC. + */ + Web3Provider.getUniqueId = function () { + return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); + }; Web3Provider.prototype.send = function (method, params) { var _this = this; // Metamask complains about eth_sign (and on some versions hangs) @@ -61,10 +67,11 @@ var Web3Provider = /** @class */ (function (_super) { params = [params[1], params[0]]; } return new Promise(function (resolve, reject) { + var uniqueId = Web3Provider.getUniqueId(); var request = { method: method, params: params, - id: 42, + id: uniqueId, jsonrpc: "2.0" }; _this._sendAsync(request, function (error, result) {