@metaphi/airwallet-api
Version:
Metaphi Airwallet API to add whitelabel, non-custodial wallets to dApps
490 lines (417 loc) • 13 kB
JavaScript
const { MetaphiJsonRpcProvider } = require('./MetaphiProvider')
const { uuidv4, getMetaphiIframeDomain } = require('./utils')
const { MetaphiIframe, MetaphiInputTypes, WALLET_EMBED_ID } = require('./constants')
/**
* Class to handle communication with a Metaphi Wallet instance,
* embedded in an Iframe.
*
*/
class WalletPlugin {
_callbacks = { callbackFns: {} };
_options;
_isLoaded = false;
_wallet = {};
_provider = null;
_walletUI = null;
_networkConfig = null
constructor(options) {
// Set options.
this._options = options;
this._accountConfig = options.accountConfig;
this._networkConfig = options.networkConfig;
// Set metaphi iframe domain.
this._metaphiBaseDomain = getMetaphiIframeDomain(options.env);
console.log('Metaphi Base Domain: ', this._metaphiBaseDomain)
if (options.custom.userInputMethod) {
this._walletUI = options.custom.userInputMethod;
}
// Setup provider. This provider doesnot have a signer yet.
this._setupProvider()
console.log('Metaphi wallet initialized.', options);
}
/** Public Functions */
// Client-side only.
init = async () => {
if (!global.window) {
throw new Error('Metaphi Wallet should be initialized client-side.');
}
return new Promise((resolve, reject) => {
const embed = document.getElementById(WALLET_EMBED_ID);
if (!!embed) return resolve(true)
// Setup iframe.
const ifrm = document.createElement('iframe');
ifrm.setAttribute('id', WALLET_EMBED_ID);
const source = this._getIframeSource();
ifrm.setAttribute('src', source);
ifrm.setAttribute('height', 0);
ifrm.setAttribute('width', 0);
ifrm.setAttribute('style', 'position: absolute; top: 0; left: 0;');
document.getElementById('mWalletContainer').appendChild(ifrm); // to place at end of document
this._isLoaded = true;
// Setup listener, for callbacks.
window.addEventListener('message', this._receiveMessage);
// If embed doesn't exist, wait for iframe to load before resolving.
ifrm.addEventListener("load", function() {
resolve(true)
});
})
};
// Client-side only.
destroy = () => {
// Remove iframe.
// Remove listeners.
window.removeEventListener('message', this._receiveMessage);
this._isLoaded = false;
};
/**
* Check, if loaded.
*
* @returns {Boolean}
*/
isLoaded = () => {
return this._isLoaded;
};
/**
* Connect wallet.
* @param {Function} callback
*/
connect = (callback) => {
// Add callback.
this._getCallbackStore()['on_connect'].push(callback);
this._login(callback);
};
/**
* Disconnect wallet.
* @param {Function} callback
*/
disconnect = (callback) => {
// Add callback.
this._getCallbackStore()['on_disconnect'].push(callback);
this._sendEvent({ event: 'disconnect' }, callback);
};
/**
*
* @param {Object} payload { message: String }
* @param {function} callback
*/
signMessage = async (payload, callback) => {
const tx = { ...payload, address: this._wallet.address };
const ok = await this.getUserInput(MetaphiInputTypes.TRANSACTION_SIGN, tx);
if (!ok) {
if (callback) callback({ err: 'User didnot authorize signing.' });
}
console.log('Sending event: signMessage ', payload)
this._sendEvent({ event: 'signMessage', payload }, callback);
};
/**
*
* @param {Object} payload { transaction: Object }
* @param {Function} callback
*/
signTransaction = async (payload, callback) => {
const tx = { ...payload, address: this._wallet.address };
const ok = await this.getUserInput(MetaphiInputTypes.TRANSACTION_SIGN, tx);
if (!ok) {
if (callback) callback({ err: 'User didnot authorize signing.' });
}
console.log('Sending event: signTransaction ', payload)
this._sendEvent({ event: 'signTransaction', payload }, callback);
};
/**
* Get address of connected wallet.
*
* @returns {String}
*/
getAddress = () => {
return this._wallet.address;
};
/**
* Get chain ID.
*
* @returns Number
*/
getChainId = () => {
return this._networkConfig.chainId
}
/**
* Get provider instance.
*
* @returns {JsonRpcProvider} jsonRpc provider
*/
getProvider = () => {
return this._provider;
};
/**
*
* @param {*} callback
*/
getUserInput = async (inputType, payload = {}) => {
let value
if (this._walletUI) {
value = await this._walletUI.getUserInput(inputType, payload);
} else {
// Default inputs.
switch (inputType) {
case MetaphiInputTypes.EMAIL:
value = prompt('Please enter your email.');
break;
case MetaphiInputTypes.VERIFICATION_CODE:
value = prompt('Please enter authorization code send to your email.');
break;
case MetaphiInputTypes.USER_PIN:
case MetaphiInputTypes.PIN_RECONNECT:
value = prompt('Please enter your user pin.');
break;
case MetaphiInputTypes.TRANSACTION_SIGN:
value = confirm('Sign this message?');
break;
}
}
return value
};
showUserError = (error, inputType) => {
if (!this._walletUI) {
this.showUserErrorDefault(error);
} else {
this._walletUI.showError(error, inputType);
}
};
showUserErrorDefault = (error) => {
alert(error.message);
};
// Internal. Only exposed, for testing.
createWallet = (callback) => {
this._sendEvent({ event: 'createWallet' }, callback);
};
/** Private Instances */
_getInstance = () => {
return document.getElementById(WALLET_EMBED_ID);
};
_getPluginSourceUrl = () => {
}
_initCallbackStore = () => {
window['__METAPHI__'] = {
callbacks: {
_METAPHI_INTERNAL_CALLBACK_: this._handleInternalCallback,
// events
on_connect: [this._handleConnect],
on_disconnect: [this._handleDisconnect],
on_verify: [this._handleVerify],
on_login: [this._handleLogin],
},
};
};
_getCallbackStore = () => {
if (!window['__METAPHI__']) {
this._initCallbackStore();
}
return window['__METAPHI__'].callbacks;
};
// Get callback Id.
_getCallbackId = (callbackFn) => {
if (!callbackFn) {
return null;
}
const callbackId = uuidv4();
this._getCallbackStore()[callbackId] = callbackFn;
return callbackId;
};
_handleInternalCallback = (payload) => {
const { method, data } = payload;
if (method === 'wallet_connected') {
this._wallet.address = data.address;
}
if (method === 'wallet_disconnected') {
this._wallet = {};
}
};
/**
* Get logged-in user from Metaphi Iframe.
* @returns Promise<LoggedInUser { email: string, autoconnect: boolean }>
*/
_getLoggedInUser = () => {
console.log('_getLoggedInUser')
let res, rej
const promise = new Promise((resolve, reject) => {
res = resolve
rej = reject
})
console.log('Sending Event: ', MetaphiIframe.GET_LOGGED_IN_USER)
this._sendEvent({ event: MetaphiIframe.GET_LOGGED_IN_USER }, ({ loggedInUser, err }) => {
console.log("Callback: ", loggedInUser, err)
if (err) return rej(err)
return res(loggedInUser)
});
return promise
}
// Event
_login = async () => {
console.log("Event: _login")
let email
// Case 1. User is already logged-in.
// If the user is logged-in, Metaphi will have the emailId
// and a corresponding jwt. Additionally, if the user-pin is
// cached with Metaphi, autoconnect will be set to true.
// LoggedInUser: { email: string, autoconnect: boolean }
const loggedInUser = await this._getLoggedInUser()
if (loggedInUser) {
email = loggedInUser.email
// Case 1a. If user has autoconnect set to true,
// login the user automatically
if (loggedInUser.autoconnect) {
this._sendLoginEvent(email)
return
}
// Case 1b. If user has autoconnect is set to false,
// prompt the user for the pin. And then, autoconnect.
const userPin = await this.getUserInput(MetaphiInputTypes.PIN_RECONNECT, { email, dApp: this._accountConfig.dApp });
console.log('Retrieved User Pin: ', userPin)
this._sendLoginEvent(email, userPin)
return
}
// Case 2. User is not logged-in.
email = await this.getUserInput(MetaphiInputTypes.EMAIL);
this._sendLoginEvent(email)
};
_sendLoginEvent = (email, userPin) => {
// Login event.
const payload = { email, userPin };
this._sendEvent({ event: 'login', payload });
}
// Event listener
_handleLogin = (payload) => {
console.log('Handling Login: ', payload)
if (payload.err) {
console.log(`Error: ${payload.err}`);
return;
}
if (payload.verified) {
console.log('Login is verified: ', payload)
// Trigger connect step.
return this._connect(payload.email, payload.autoconnect);
}
// Trigger verification step.
return this._verify(payload.email);
};
// Event
_verify = async (email) => {
const verificationCode = await this.getUserInput(MetaphiInputTypes.VERIFICATION_CODE);
const payload = {
email,
verificationCode,
};
this._sendEvent({ event: 'verify', payload });
};
// Listener
_handleVerify = (payload) => {
if (payload.err) {
this.showUserError({ message: payload.err }, 'verificationcode');
return;
}
if (!payload.verified) {
this.showUserError(
{
message: 'Incorrect verification code.',
},
'verificationcode',
);
return;
}
// verifying.
if (!payload.err && payload.verified) {
this._connect(payload.email);
}
};
// Event.
_connect = async (email, autoconnect) => {
console.log(`Connect event: ${email} | ${autoconnect}`)
let userPin
if (!autoconnect) {
userPin = await this.getUserInput(MetaphiInputTypes.USER_PIN);
}
this._sendConnectEvent(email, userPin)
};
// Send connect event.
_sendConnectEvent = async (email, userPin) => {
const payload = { email, userPin };
this._sendEvent({ event: 'connect', payload });
this._walletUI.updateState(MetaphiInputTypes.PROCESSING);
}
// Listener.
_handleConnect = (payload) => {
this._wallet.address = payload.address;
if (payload.connected) {
this._walletUI.updateState(MetaphiInputTypes.SUCCESS, {
address: payload.address,
dApp: this._accountConfig.dApp,
});
} else {
this.showUserError({ message: 'Error connecting wallet.' });
}
};
_handleDisconnect = () => {
// handle connect.
};
// Execute callback.
_executeCallback = (callbackId, payload) => {
if (!callbackId) {
return;
}
const callbackFn = this._getCallbackStore()[callbackId];
if (typeof callbackFn === 'function') callbackFn(payload);
else if (Array.isArray(callbackFn)) {
callbackFn.forEach((fn) => {
fn(payload);
});
// Remove all, expect the first natives function.
callbackFn.splice(1);
}
// Delete non-internal callbacks.
const isInternalCallback = callbackId === '_METAPHI_INTERNAL_CALLBACK_';
if (!isInternalCallback && !Array.isArray(callbackFn))
delete this._getCallbackStore()[callbackId];
};
// Send event.
_sendEvent = ({ event, payload }, callback) => {
const callbackId = this._getCallbackId(callback);
const request = {
method: event,
payload: payload,
callbackId,
};
this._postMessage(request);
};
// Post message to child frame.
_postMessage = (request) => {
const frame = this._getInstance();
const targetDomain = this._metaphiBaseDomain
frame.contentWindow.postMessage(request, targetDomain);
};
// Handle message.
_receiveMessage = (event) => {
if (event.origin.startsWith(this._metaphiBaseDomain)) {
this._executeCallback(event.data.callbackId, event.data.payload);
}
};
_getIframeSource = () => {
const rpc = this._networkConfig.rpcUrl;
const apiKey = this._accountConfig.apiKey;
const clientId = this._accountConfig.clientId;
const source = this._accountConfig.domain;
const src = `${this._metaphiBaseDomain}/wallet/plugin?clientId=${clientId}&apiKey=${apiKey}&rpc=${rpc}&source=${source}`;
return src;
};
_setupProvider = () => {
const { rpcUrl, name, chainId } = this._networkConfig
this._provider = new MetaphiJsonRpcProvider(rpcUrl, { name, chainId }, this)
// web3 convention.
if (global.window) {
global.window.ethereum = this._provider;
}
}
}
if (global.window) {
global.window.WalletPlugin = WalletPlugin;
console.log('Metaphi is loaded.', !!global.window.WalletPlugin);
}
export default WalletPlugin;