UNPKG

@eternl/cardano-dapp-connector-bridge

Version:

A postMessage bridge to connect dApps to the Eternl DApp Browser loading apps into an iframe.

302 lines (194 loc) 8.28 kB
import { IBridge, IBridgeRequest, IBridgeRequestEvent } from './types' const generateUID = () => { return ('000' + ((Math.random() * 46656) | 0).toString(36)).slice(-3) + ('000' + ((Math.random() * 46656) | 0).toString(36)).slice(-3); } /** * Initializes the postMessage bridge for the Eternl DApp Browser. * Call this function as early as possible in your page. * * Once the bridge is established, you can use * window.cardano.eternl.enable() as your normally do. * * The optional callback will notify your code when the bridge is established. * * The window.cardano.eternl object will have isBridge set to true, and it will include * the feeAddress. * * @param onBridgeCreated (optional) callback function to be called when the bridge is established. */ export const initCardanoDAppConnectorBridge = (onBridgeCreated?: (api: any) => void) => { if (typeof window === 'undefined') { return } const _debug = false // set to true for debug logs. const _label = 'DAppConnectorBridge: ' // set to true for debug logs. let _walletNamespace: string | null = null // eg. 'eternl' let _initialApiObject: any = null // CIP0030 initial api object let _fullApiObject = null // CIP0030 full api object const _bridge = <IBridge>{ type: 'cardano-dapp-connector-bridge', source: null, origin: null } const _requestMap = <Record<string, any>>{ } const _methodMap = <Record<string, string>>{ // Initial 4 methods to establish connection. More endpoints will be added by the wallet. connect: 'connect', handshake: 'handshake', enable: 'enable', isEnabled: 'isEnabled', supportedExtensions: 'supportedExtensions' } function createRequest(method: string) { const args = [...arguments] if (args.length > 0) { args.shift() } return new Promise(((resolve, reject) => { const request = <IBridgeRequest>{ payload: { type: _bridge.type, to: _walletNamespace, uid: generateUID(), method: method, args: args }, resolve: resolve, reject: reject } _requestMap[request.payload.uid] = request if (_debug) { console.log(_label+'_requestMap:', _requestMap) } _bridge.source?.postMessage(request.payload, _bridge.origin ?? '*') })) } function generateApiFunction(method: string) { return function() { // @ts-ignore return createRequest(method, ...arguments) } } function generateApiObject(obj: any) { const apiObj: any = {} for (const key in obj) { const value: any = obj[key] if (_debug) { console.log(_label+'init: key/value:', key, value) } if (typeof value === 'string') { if (key === 'feeAddress') { apiObj[key] = value } else { apiObj[key] = generateApiFunction(value) _methodMap[value] = value } } else if (typeof value === 'object') { apiObj[key] = generateApiObject(value) } else { apiObj[key] = value } } return apiObj } function initBridge(source: Window | null, origin: string, walletNamespace: string, initialApi: any) { if (!window.hasOwnProperty('cardano')) { window.cardano = {} } if (window.cardano.hasOwnProperty(walletNamespace)) { console.warn('Warn: '+_label+'window.cardano.' + walletNamespace + ' already present, skipping initialApi creation.') return null } _bridge.source = source _bridge.origin = origin _walletNamespace = walletNamespace const initialApiObj = { isBridge: true, // https://github.com/cardano-foundation/CIPs/tree/master/CIP-0030 isEnabled: function() { return createRequest('isEnabled') }, enable: function() { // @ts-ignore return createRequest('enable', ...arguments) }, apiVersion: initialApi.apiVersion, name: initialApi.name, icon: initialApi.icon ?? null, supportedExtensions: initialApi.supportedExtensions, // experimental API: https://github.com/cardano-foundation/CIPs/blob/master/CIP-0030/README.md#experimental-api experimental: {} } window.cardano[walletNamespace] = initialApiObj if (initialApi.experimental) { initialApiObj.experimental = { ...generateApiObject(initialApi.experimental) } } return window.cardano[walletNamespace] } function isValidBridge(event: IBridgeRequestEvent) { if (!_initialApiObject) { if (event.data.method !== _methodMap.connect) { console.error('Error: '+_label+'send \'connect\' first.') return false } const initialApi = event.data.initialApi if (!initialApi || !initialApi.isBridge || !initialApi.apiVersion || !initialApi.name) { console.error('Error: '+_label+'\'connect\' is missing correct initialApi.', initialApi) return false } if (!event.data.walletNamespace) { console.error('Error: '+_label+'\'connect\' is missing walletNamespace.', event.data.walletNamespace) return false } _initialApiObject = initBridge(event.source, event.origin, event.data.walletNamespace, initialApi) } if (!(_initialApiObject && window.hasOwnProperty('cardano') && window.cardano[event.data.walletNamespace!] === _initialApiObject)) { console.warn('Warn: '+_label+'bridge not set up correctly:', _bridge, _initialApiObject, _walletNamespace) return false } return true } function isValidMessage(event: IBridgeRequestEvent) { if (!event.data || !event.origin || !event.source) return false if (event.data.type !== _bridge.type) return false if (!_methodMap.hasOwnProperty(event.data.method)) return false if (_walletNamespace && event.data.walletNamespace !== _walletNamespace) return false return true } async function onMessage(event: MessageEvent) { if (!isValidMessage(event as IBridgeRequestEvent) || !isValidBridge( event as IBridgeRequestEvent)) { return } if (_debug) { console.log('########################') console.log(_label+'onMessage: got message') console.log(_label+'onMessage: origin:', event.origin) // console.log(_label+'onMessage: source:', payload.source) // Don't log source, might break browser security rules console.log(_label+'onMessage: data: ', event.data) console.log('########################') } if (event.data.method === _methodMap.connect) { const success = await createRequest('handshake') if (success && _initialApiObject) { if (onBridgeCreated) onBridgeCreated(_initialApiObject) } return } if (!event.data.uid) { return } const request = _requestMap[event.data.uid] if (!request) return const error = event.data.error if (error) { request.reject(error) delete _requestMap[event.data.uid] return } // Bridge is set up correctly, message is valid, method is known. let response = event.data.response if (event.data.method === _methodMap.enable) { _fullApiObject = null if (typeof response === 'object') { _fullApiObject = { ...generateApiObject(response) } response = _fullApiObject if (_debug) { console.log(_label+'onMessage: fullApiObject:', _fullApiObject) } } } request.resolve(response) delete _requestMap[event.data.uid] } window.addEventListener("message", onMessage, false) }