@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
text/typescript
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)
}