UNPKG

@metaphi/airwallet-api

Version:

Metaphi Airwallet API to add whitelabel, non-custodial wallets to dApps

1,768 lines (1,383 loc) 8.12 MB
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MetaphiJsonRpcProvider = void 0; var _keccak = require("@ethersproject/keccak256"); var _transactions = require("@ethersproject/transactions"); var _ethers = require("ethers"); const { JsonRpcProvider, JsonRpcSigner } = require('@ethersproject/providers'); const { defineReadOnly } = require("@ethersproject/properties"); const { Eip1193Bridge } = require('@ethersproject/experimental'); const { toUtf8Bytes } = require("@ethersproject/strings"); const { Signer } = require('@ethersproject/abstract-signer'); const _constructorGuard = 11; // Source: https://github.com/ethers-io/ethers.js/blob/master/packages/providers/src.ts/json-rpc-provider.ts#L320 class MetaphiJsonSigner extends Signer { constructor(constructorGuard, provider, addressOrIndex) { super(); defineReadOnly(this, "provider", provider); if (addressOrIndex == null) { addressOrIndex = 0; } if (typeof addressOrIndex === "string") { defineReadOnly(this, "_address", this.provider.formatter.address(addressOrIndex)); defineReadOnly(this, "_index", null); } else if (typeof addressOrIndex === "number") { defineReadOnly(this, "_index", addressOrIndex); defineReadOnly(this, "_address", null); } } getAddress = async () => { return new Promise((resolve, reject) => resolve(this._address)); }; signMessage = async message => { let self = this; return new Promise((resolve, reject) => { self.provider.getWallet().signMessage({ message: message }, ({ sig, err }) => { if (sig) resolve(sig); if (err) reject(err); }); }); }; signTransaction = async transaction => { console.log('Sign Transaction from Provider: ', transaction); let self = this; return new Promise((resolve, reject) => { self.provider.getWallet().signTransaction({ transaction: transaction }, ({ sig, err }) => { console.log('Signed Transaction: ', sig); if (sig) resolve(sig); if (err) reject(err); }); }); }; sendTransaction = async transaction => { this._checkProvider("sendTransaction"); const tx = await this.populateTransaction(transaction); const txData = { ...tx }; const signedTx = await this.signTransaction(txData); return await this.provider.sendTransaction(signedTx); }; } class MetaphiJsonRpcProvider extends JsonRpcProvider { _mWalletInstance = null; constructor(url, network, _mWalletInstance) { super(url, network); this._mWalletInstance = _mWalletInstance; } getSigner(addressOrIndex) { return new MetaphiJsonSigner(_constructorGuard, this, addressOrIndex); } getWallet() { return this._mWalletInstance; } } exports.MetaphiJsonRpcProvider = MetaphiJsonRpcProvider; },{"@ethersproject/abstract-signer":88,"@ethersproject/experimental":126,"@ethersproject/keccak256":143,"@ethersproject/properties":151,"@ethersproject/providers":162,"@ethersproject/strings":189,"@ethersproject/transactions":192,"ethers":501}],2:[function(require,module,exports){ (function (Buffer){(function (){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _axios = _interopRequireDefault(require("axios")); var _store = _interopRequireDefault(require("store")); var _expire = _interopRequireDefault(require("store/plugins/expire")); var _jsCookie = _interopRequireDefault(require("js-cookie")); var _shamirsSecretSharing = _interopRequireDefault(require("shamirs-secret-sharing")); var _crypto = _interopRequireDefault(require("crypto")); var _web = _interopRequireDefault(require("web3")); var _ethereumjsWallet = _interopRequireDefault(require("ethereumjs-wallet")); var _common = _interopRequireWildcard(require("@ethereumjs/common")); var _tx = require("@ethereumjs/tx"); var _ethereumjsUtil = require("ethereumjs-util"); var _utils = require("./utils"); var _hdwalletProvider = _interopRequireDefault(require("@truffle/hdwallet-provider")); var _maticjsPlasma = require("@maticnetwork/maticjs-plasma"); var _maticjs = require("@maticnetwork/maticjs"); var _maticjsWeb = require("@maticnetwork/maticjs-web3"); var _ethers = require("ethers"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * Metaphi Airwallet API. * * Use this library to integrate the Metaphi Airwallet into your dApp. * This library allows you to connect to Metaphi Wallet APIs, to recover or create new wallets. * The KMS systems allows users to create wallets locally and backup encrypted shares * to Metaphi and the dApp. * * To learn more about the KMS architecture, visit: https://docs.metaphi.xyz/kms-whitepaper. * */ // Device // Cryptography // Wallets & Transactions // Providers & Polygon // Initialization const common = new _common.default({ chain: _common.Chain.Mainnet }); _store.default.addPlugin(_expire.default); console.log("Loaded Metaphi Wallet Api."); (0, _maticjs.use)(_maticjsWeb.Web3ClientPlugin); const METAPHI_LOCAL_SHARE_PREFIX = "metaphi-key-share-"; class MetaphiWalletApi { /* Static properties */ // Environment _environment = "development"; // Endpoint for wallets. _METAPHI_WALLET_API = "https://api-staging.metaphi.xyz/v1/wallets"; // Endpoint for wallet verification. _METAPHI_WALLET_VERIFY_API = "https://api-staging.metaphi.xyz/v1/wallets/verify"; // Endpoint that exposes Metaphi Secret API. _METAPHI_WALLET_SECRET_API = "https://api-staging.metaphi.xyz/v1/wallets/secret"; // Endpoint that exposes the dApp Secret API. _DAPP_WALLET_SECRET_API = null; // Local share prefix. // ClientId of the dApp. _clientId = null; // API key of the dApp. _clientApiKey = null; /** Wallet Properties */ // Wallet Public Address _publicAddress = null; // Wallet Private Key _privateKey = null; /** Network Properties */ // Rpc _rpc = null; // Chain Id _chainId = 0; // Wallet Provider. _provider = null; // Web3 Provider _web3Provider = null; // Plasma Client, for Polygon _plasmaClient = null; // Additional features. _logger = null; constructor(options) { const { accountConfig, networkConfig, custom } = options; // Throw error, when accountConfig is missing. if (!accountConfig || !accountConfig.clientId || !accountConfig.apiKey) { throw new Error("Error initializing wallet: Missing clientId or apiKey"); } if (!networkConfig || !networkConfig.rpcUrl) { throw new Error("Error: Missing RPC"); } // Account Config. this._clientId = accountConfig.clientId; this._clientApiKey = accountConfig.apiKey; this._DAPP_WALLET_SECRET_API = "https://api-staging.metaphi.xyz/v1/dapp/secret"; // TODO: Should be initialized by dApp. this._environment = accountConfig.env || "development"; // RPC this._rpc = networkConfig.rpcUrl; // Chain Id this._chainId = networkConfig.chainId; // Custom functions. if (custom?.logger) { this._logger = custom.logger; } else { this._logger = function () { console.log(...arguments); }; } // User Id this._userId = null; } /* Public methods */ // Login Metaphi wallet login = async (userId, userPinInput) => { // Persist user id. this._userId = userId; // Fail-safe. _store.default.removeExpiredKeys(); let userPin = userPinInput || this._getCachedPin(); this._logger(`Logging in: ${userId} | ${userPin}`); let response = { verified: false, autoconnect: false }; // Check if logged-in user. Skip verification, if true. let address; let jwt = this._getAuthenticatedJwt(); if (jwt) { try { // TODO: When wallet is overriden, this will lead to stale results. const userDetails = await this._getUserDetails(jwt); if (userDetails.username === this._userId) { address = userDetails.address; } else { jwt = null; this._resetAuthenticatedJwt(); } } catch (ex) { jwt = null; // Reset expired jwt. this._resetAuthenticatedJwt(); } } // Get wallet. if (!jwt) { const response = await this._retrieveWallet(userId); jwt = response.jwt; address = response.address; } // Check if logged-in user. Skip verification, if true. response.verified = !!jwt; // Set public address. this._publicAddress = address; // If the user is logged-in, and reconnects via pin // cache the pin. const shouldCachePin = !!jwt && !!userPin; console.log('Caching pin: ', shouldCachePin, jwt, userPin); if (!!jwt && userPin !== undefined && userPin !== null && userPin !== 'null') { this._setCachedPin(userPin); response.autoconnect = true; } // Extract public address. this._logger(`Wallet Authenticated: ${this._publicAddress}`); return response; }; // Check if user is logged-in. isUserLoggedIn = () => { return !!this._userId; }; /** * Gets all logged-in users, with autoconnect status. * * @returns Array */ getLoggedInUsers = () => { _store.default.removeExpiredKeys(); const loggedInUsers = []; _store.default.each(function (value, key) { // Get all valid jwts. const groups = key.match(/(metaphi-jwt-)(\S*)/); if (groups && groups[2]) { const email = groups[2]; // Find corresponding cached pin, if exists. const cachedPin = _store.default.get(`metaphi-pin-${email}`); // Add to array. loggedInUsers.push({ email, autoconnect: !!cachedPin }); } }); return loggedInUsers; }; // Wallet Provider. getProvider = () => { return this._web3Provider; }; // Get public address of wallet. getAddress = () => { this._logger(`Connected Wallet Address: ${this._publicAddress}`); return this._publicAddress; }; // Create a new wallet for the user. Use with caution. createNewWallet = async userPin => { if (userPin === undefined) { throw new Error('Error creating wallet: User pin not found.'); } // If the public address does not exist or minimum shares are not met // a. Generate the wallet key and address locally. // b. Break it up into three parts, encrypt using symmetric key. // c. Store one piece locally, store one piece on the dApp and the final // piece on Metaphi. const userCreds = await this._getUserCreds(this._userId, userPin, true); const wallet = await this._createNewWallet(userCreds); this._logger(`Created New Wallet: ${wallet.address}`, "green"); // Setup wallet. this._setupWallet(wallet); }; // Transfer. transfer = async (toAddress, valueInWei) => { const plasmaClient = this._plasmaClient; // Source: https://maticnetwork.github.io/matic.js/docs/plasma/erc20/transfer/ // initialize token with null means use MATIC tokens const erc20Token = plasmaClient.erc20(null); const result = await erc20Token.transfer(valueInWei, toAddress); const txHash = await result.getTransactionHash(); const txReceipt = await result.getReceipt(); return txReceipt; }; // Personal Sign. // Inspired from: https://github.com/MetaMask/eth-sig-util/blob/8f5a90bed37e6891fe4e9ab98a8cd4f62188d5c4/src/personal-sign.ts personalSign = async messageString => { if (!messageString) { throw new Error("Missing parameter: messageString"); } if (!this._privateKey) { throw new Error("Missing private key."); } // const message = Buffer.from(messageString); // const msgHash = hashPersonalMessage(message); // return this._personalSign(msgHash, this._privateKey); try { const wallet = new _ethers.ethers.Wallet(this._privateKey); const signedTx = await wallet.signMessage(messageString); return signedTx; } catch (ex) { console.log(ex); this._logger(`Error signing message: ${ex.toString()}`); } }; // Sign a transaction. signTransaction = async transaction => { if (!this._privateKey) { this._logger("Error signing transaction: Private key missing", "red"); } try { const wallet = new _ethers.ethers.Wallet(this._privateKey); const signedTx = await wallet.signTransaction(transaction); return signedTx; } catch (ex) { console.log(ex); this._logger(`Error signing transaction: ${ex.toString()}`); } }; // Sign a message signMessage = () => {// TODO. // await walletMnemonic.signMessage("Hello World") }; // Connect wallet. connect = async userPin => { let pin = userPin || this._getCachedPin(); console.log('Connecting wallet with pin: ', pin); // Connect wallet. this._logger("Connecting wallet."); // If user pin isn't provided, check cache. if (pin === undefined) { this._logger("Error connecting wallet: Pin not found."); return; } await this._connectWallet(pin); if (this._publicAddress && this._privateKey) { this._logger("Wallet reconstruction successful. Wallet connected."); console.log('Caching pin.'); this._setCachedPin(pin); } else { this._logger(`Error connecting wallet.`); throw new Error("Wallet Reconstruction Unsuccessful."); } }; // Disconnect disconnect = () => { this._reset(); }; /* Factory methods */ // Recovers a wallet using the secret shares stored in Metaphi and the dApp. recoverOrCreateWallet = async userCreds => { // Public Address & Email of the user const { publicAddress, userEmail } = userCreds; let wallet = { address: publicAddress, privateKey: null }; // If the wallet secret and public address exist: // a. Retrieve 2/3 shares from local device, dApp and/or Metaphi // b. Decrypt it using userPin // c. Persist reconstructed privateKey in Wallet scope if (publicAddress !== null && userEmail != null) { this._logger(`Attempting to reconstruct key for ${userEmail}`); /** * List of shares, to reconstruct the secret key * These shares will be retrieved in the following order * 1. Device Share * 2. dApp Share * 3. Metaphi Share * * We need 2/3 shares to reconstruct the wallet. Incase, this criteria is not met, * we will recreate the wallet and shared secrets */ let shares = []; // Retrieve share from local device. let localShare = this._getShareFromDevice(userEmail); if (localShare) { this._logger(`Retrieved share from device.`); shares.push(localShare); } else { this._logger(`Share on device: Not found.`); } // Retrieve share from Metaphi. let metaphiShare = await this._getShareFromMetaphi(userCreds); if (metaphiShare) { shares.push(metaphiShare); } // Retrieve backup dApp share. // This happens when either the local share or dApp share is missing if (shares.length < 2) { let dAppShare = await this._getShareFromdApp(userCreds); if (dAppShare) { this._logger(`Retrieved share from dApp.`); shares.push(dAppShare); } } // Required number of shares exist. // Reconstruct private key. if (shares.length == 2) { const symmetric_key = this._generateSymmetricKey(userCreds); const privateKey = this._reconstructWalletFromSecret(symmetric_key, shares[0], shares[1]); wallet.privateKey = privateKey; } else { this._logger("Error fetching minimum number of shares. Please contact Metaphi or create a new wallet.", "red"); // TODO: Give the user an option to recreate wallet. } } else { this._logger("Error recovering wallet. Doesnot exist"); } // Return reconstructed wallet. return wallet; }; // Default function to retrieve user credential. // Override, with dApp functionality. defaultUserPinFunction = async () => { const userPin = prompt("Please enter your secret pin", "1234"); return userPin; }; /* Private methods */ _personalSign = (msgHash, privateKey) => { const sig = (0, _ethereumjsUtil.ecsign)(msgHash, new Buffer.from(this._privateKey.substr(2), "hex")); const serialized = (0, _utils.concatSig)((0, _ethereumjsUtil.toBuffer)(sig.v), sig.r, sig.s); return serialized; }; _reset = async () => { /** Empty Caches. */ // Authentication this._resetAuthenticatedJwt(); this._resetCachedPin(); /** Reset statics. */ // Wallet Public Address this._publicAddress = null; // Wallet Private Key this._privateKey = null; // User ID this._userId = null; this._logger("Wallet disconnected."); }; _triggerManualAuthentication = async userId => { this._logger("User is not logged in. Triggering authentication flow"); // Get wallet. let jwt = await this._retrieveWallet(userId); // Verification Flow. // This is triggered when there is no oauth flow setup for this dApp. if (!jwt) { this._logger("Triggering verification flow."); var verificationCode = prompt("Enter your verification code", "123456"); this._logger(`Entered Verification Code: ${verificationCode}.`); let { address, verified, jwt, wallet_id } = await this._verifyUserVerificationCode(userId, verificationCode); // Set public address. this._publicAddress = address; // Set jwt authorization this._setAuthenticatedJwt(jwt); return verified; } // OAuth one-click setup. // TODO: Get public address from jwt and set it. // TODO: Cache jwt. // Authenticated. return 1; }; // Get/Create new wallet. _retrieveWallet = async userId => { let myHeaders = new Headers(); myHeaders.append("X-Metaphi-Api-Key", this._clientApiKey); myHeaders.append("x-metaphi-account-id", this._clientId); myHeaders.append("Content-Type", "application/json"); // Wallet Authentication Flow. // TODO: Switch to axios. try { let raw = JSON.stringify({ email: userId }); var requestOptions = { method: "POST", headers: myHeaders, body: raw, redirect: "follow" }; const URL = this._METAPHI_WALLET_API; const response = await fetch(URL, requestOptions); const wallet = await response.json(); // Set userId this._userId = userId; this._logger("Retrieved wallet."); return { jwt: wallet.jwt, address: wallet.address }; } catch (ex) { this._logger("Error recovering wallet.", ex); } }; // Authenticate user via verification code. _verifyUserVerificationCode = async (userId, verificationCode) => { let myHeaders = new Headers(); myHeaders.append("X-Metaphi-Api-Key", this._clientApiKey); myHeaders.append("x-metaphi-account-id", this._clientId); myHeaders.append("Content-Type", "application/json"); try { // Verify code. // TODO: Switch to axios. const URL = this._METAPHI_WALLET_VERIFY_API; let raw = JSON.stringify({ email: userId, verification_code: verificationCode }); var requestOptions = { method: "POST", headers: myHeaders, body: raw, redirect: "follow" }; const response = await fetch(URL, requestOptions); const wallet = await response.json(); const { jwt, wallet: { address, wallet_id } } = wallet; // Set jwt. this._setAuthenticatedJwt(jwt); // Set public address. this._publicAddress = address; return { verified: true, jwt, address, wallet_id }; } catch (ex) { this._logger(`Error verifying wallet.`); } return { verified: false }; }; // Connect Metaphi Wallet _connectWallet = async userPin => { const userId = this._userId; const userCreds = await this._getUserCreds(userId, userPin); const wallet = await this.recoverOrCreateWallet(userCreds); // Setup wallet. this._setupWallet(wallet); }; _getUserCreds = async (userId, userPin, isNewWallet) => { if (!userPin) { throw new Error("User pin unknown."); } // Get user pin. // Prompt the user for a pin and generate a symmetric key. // This should be protected using faceid, webauthn, etc. // Key must be 32 bytes for aes256. this._logger("Retrieving user credential."); // Retrieve reconstructed wallet. this._logger("Reconstructing wallet."); const authorizedJwt = this._getAuthenticatedJwt(); const userCreds = { userPin, userEmail: userId, authorizedJwt: authorizedJwt, publicAddress: isNewWallet ? null : this._publicAddress }; return userCreds; }; _setupWallet = async wallet => { // If the wallet address has changed, update user if (wallet.address !== this._publicAddress) { // TODO: Handle this case, NS to comment this._publicAddress = wallet.address; this._logger(`Public Address changed from ${this._publicAddress} to ${wallet.address}`, "red"); console.warn(`Public Address changed from ${this._publicAddress} to ${wallet.address}`); } // Persist public key. this._publicAddress = wallet.address; // Persist private key. this._privateKey = wallet.privateKey; // Setup wallet provider. this._setupWalletProvider(); // Setup plasmaClient, for Polygon this._setupPlasmaClient(); }; _setupWalletProvider = () => { const privateKey = this._privateKey; const rpc = this._rpc; if (!privateKey || !rpc) { throw new Error("Invalid private key or rpc"); } // Assign. this._provider = new _hdwalletProvider.default(privateKey, rpc); // Assign. // Source: www.reddit.com/r/ethdev/comments/8d70mz/using_infura_with_web3_html_providerengine/ this._web3Provider = new _web.default(this._provider); window.metaphi = this._web3Provider; }; _setupPlasmaClient = async () => { const network = "testnet"; // 'testnet' or 'mainnet' const version = "mumbai"; // 'mumbai' or 'v1' const provider = this._provider; const publicAddress = this._publicAddress; // Setup Plasma Client. const plasmaClient = new _maticjsPlasma.PlasmaClient(); await plasmaClient.init({ network, version, parent: { provider: this._provider, defaultConfig: { from: publicAddress } }, child: { provider: provider, defaultConfig: { from: publicAddress } } }); // Assign. this._plasmaClient = plasmaClient; }; _getCachedPinName = () => { if (!this._userId) { throw new Error("User not found."); } return `metaphi-pin-${this._userId}`; }; _getCachedPin = () => { try { const keyName = this._getCachedPinName(); return _store.default.get(keyName); } catch (ex) { return null; } }; _setCachedPin = userPin => { if (userPin === null || userPin === 'null') return; const keyName = this._getCachedPinName(); const expires = new Date().getTime() + 30 * 60000; // Expires in half an hour. _store.default.set(keyName, userPin, expires); }; _resetCachedPin = () => { const keyName = this._getCachedPinName(); _store.default.remove(keyName); }; _getAuthenticatedJwtCookieName = () => { if (!this._userId) { throw new Error("User not found."); } return `metaphi-jwt-${this._userId}`; }; _getAuthenticatedJwt = () => { try { const keyName = this._getAuthenticatedJwtCookieName(); return _store.default.get(keyName); } catch (ex) { return null; } }; _setAuthenticatedJwt = jwt => { const keyName = this._getAuthenticatedJwtCookieName(); const expires = new Date().getTime() + 60 * 60000; // Expires in 1 hour. _store.default.set(keyName, jwt, expires); }; _resetAuthenticatedJwt = () => { const keyName = this._getAuthenticatedJwtCookieName(); _store.default.remove(keyName); }; // Get user details from jwt. _getUserDetails = async jwt => { this._logger(`Fetch wallet details: ${this._METAPHI_WALLET_API}`); const response = await _axios.default.get(this._METAPHI_WALLET_API, { api_key: { api_key: this._clientApiKey }, headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json", "X-Metaphi-Api-Key": this._clientApiKey, "x-metaphi-account-id": this._clientId } }); return response.data.user; }; // Get share from device. _getShareFromDevice = userEmail => { let share = _store.default.get(`${METAPHI_LOCAL_SHARE_PREFIX}-${userEmail}`); return share; }; // Retrive share from dApp. _getShareFromdApp = async userCreds => { this._logger(`Fetch share from dApp: ${this._DAPP_WALLET_SECRET_API}`); try { const response = await _axios.default.get(this._DAPP_WALLET_SECRET_API, { api_key: { api_key: this._clientApiKey }, params: { wallet_address: this._publicAddress }, headers: { Authorization: `Bearer ${userCreds.authorizedJwt}`, "Content-Type": "application/json", "X-Metaphi-Api-Key": this._clientApiKey, "x-metaphi-account-id": this._clientId } }); if (response.data.key_share.length) this._logger(`Fetched share from dApp.`);else this._logger(`Fetched empty share from dApp.`, "red"); return response.data.key_share; } catch (ex) { this._logger(`Error fetching share from dApp ${ex.toString()}`); } }; // Retrieve share from Metaphi. _getShareFromMetaphi = async userCreds => { this._logger(`Fetch share from Metaphi: ${this._METAPHI_WALLET_SECRET_API}`); try { const response = await _axios.default.get(this._METAPHI_WALLET_SECRET_API, { headers: { Authorization: `Bearer ${userCreds.authorizedJwt}`, "Content-Type": "application/json", "X-Metaphi-Api-Key": this._clientApiKey, "x-metaphi-account-id": this._clientId } }); if (response.data.key_share.length) this._logger(`Fetched share from Metaphi.`);else this._logger(`Fetched empty share from Metaphi.`, "red"); return response.data.key_share; } catch (ex) { this._logger(`Error fetching share from Metaphi: ${ex.toString()}`); } }; // Set share on device. _uploadToDevice = (userCreds, share) => { const key = `${METAPHI_LOCAL_SHARE_PREFIX}-${userCreds.userEmail}`; _store.default.set(key, share); this._logger(`Saved local share on device.`); }; // Uploads share to Metaphi. _uploadToMetaphi = async (userCreds, address, share) => { try { this._logger(`Uploading share to Metaphi: ${this._METAPHI_WALLET_API}`); var data = { address: address, key_share: share }; var config = { method: "patch", url: this._METAPHI_WALLET_API, headers: { Authorization: `Bearer ${userCreds.authorizedJwt}`, "Content-Type": "application/json", "X-Metaphi-Api-Key": this._clientApiKey, "x-metaphi-account-id": this._clientId }, data: data }; const response = await (0, _axios.default)(config); console.log("Wallet Patched: ", response.data); if (!response.data.jwt) { throw new Error("Error uploading share to Metaphi"); } this._logger(`Successfully uploaded share to Metaphi`); this._setAuthenticatedJwt(response.data.jwt); } catch (ex) { this._logger(`Error uploading share to Metaphi: ${ex.toString()}`); throw ex; } }; // Uploads share to dApp. _uploadTodApp = async (userCreds, address, share) => { // TODO: We assume for now that the dApp access to their // secret share contract is via Metaphi. We will also support // the dApps hosting their own contract gateway in the future. // This is why for now, we pass in the newWalletJwt. try { this._logger(`Uploading share to dApp: ${this._DAPP_WALLET_SECRET_API}`); const data = { key_share: share, wallet_address: address }; const config = { url: this._DAPP_WALLET_SECRET_API, method: "post", headers: { Authorization: `Bearer ${userCreds.authorizedJwt}`, "Content-Type": "application/json", "X-Metaphi-Api-Key": this._clientApiKey, "x-metaphi-account-id": this._clientId }, data }; const response = await (0, _axios.default)(config); if (!response.data.success) { throw new Error("Error uploading share to dApp"); } } catch (ex) { this._logger(`Error uploading share to dApp: ${ex.toString()}`); throw ex; } }; // Generates the symmetric key from user credentials. _generateSymmetricKey = userCreds => { const seed = userCreds.userEmail + ":" + userCreds.userPin; // Prompt the user for a pin and generate a symmetric key. // This should be protected using faceid, webauthn, etc. // Key must be 32 bytes for aes256. return Buffer.from(_crypto.default.createHash("sha256").update(seed).digest("base64"), "base64"); }; // Encrypt using an AES256 cipher. _aes256_encrypt = (value, key) => { var ivlength = 16; // AES blocksize var iv = _crypto.default.randomBytes(ivlength); var cipher = _crypto.default.createCipheriv("aes256", key, iv); var encrypted = cipher.update(value, "base64", "base64"); encrypted += cipher.final("base64"); const final = iv.toString("base64") + "::" + encrypted; return final; }; // Decrypts using an AES256 cipher. _aes256_decrypt = (ciphertext, key) => { var components = ciphertext.split("::"); var iv_from_ciphertext = Buffer.from(components.shift(), "base64"); try { var decipher = _crypto.default.createDecipheriv("aes256", key, iv_from_ciphertext); var deciphered = decipher.update(components.join("::"), "base64", "base64"); deciphered += decipher.final("base64"); return deciphered; } catch (err) { this._logger("Error: ", err); } }; // Creates a new wallet. _createNewWallet = async userCreds => { // Generate a wallet const EthWallet = _ethereumjsWallet.default.generate(); const address = EthWallet.getAddressString(); const privateKey = EthWallet.getPrivateKeyString(); // Create secrets from it. const shares = _shamirsSecretSharing.default.split(privateKey, { shares: 3, threshold: 2 }); // Generate symmetric key var symmetric_key = this._generateSymmetricKey(userCreds); this._logger(`Generated Symmetric Key`); // Encrypt the shares with the generated symmetric key. const encrypted_shares = shares.map(share => this._aes256_encrypt(share.toString("base64"), symmetric_key)); this._logger(`Generated Encrypted shares: ${encrypted_shares.length} \n${encrypted_shares[0]}\n\n${encrypted_shares[1]}\n\n${encrypted_shares[2]}}`); // Upload shares. let uploadedShareCount = 0; // Store a share locally. try { this._uploadToDevice(userCreds, encrypted_shares[0]); uploadedShareCount++; } catch (ex) { console.log('Error uploading local share.', ex); } // Store share in dApp contact. try { // Pass on a share to the dApp. await this._uploadTodApp(userCreds, address, encrypted_shares[1]); uploadedShareCount++; } catch (ex) { console.log('Error uploading dApp share.', ex); } if (uploadedShareCount) { // Store share in Metaphi. try { // Pass on a share to Metaphi. await this._uploadToMetaphi(userCreds, address, encrypted_shares[2]); uploadedShareCount++; } catch (ex) { console.log('Error uploading Metaphi share.', ex); } } if (uploadedShareCount == 2) { // If one of the share uploads failed, add flag to re-upload. _store.default.set(`${userCreds.userEmail}-regenerate`, 'true'); } // Wallet object. return { address, privateKey }; }; // Reconstructs the secret key from the two shares. _reconstructWalletFromSecret = (symmetric_key, keyShare1, keyShare2) => { this._logger(`Reconstructing secret:\n\nSymmetric Key: ${symmetric_key}\n\nShare1: ${keyShare1} \n\nShare 2: ${keyShare2}`); // Decrypt shares. const decrypted_shares = [keyShare1, keyShare2].map(share => { return this._aes256_decrypt(share.toString("base64"), symmetric_key); }); // Reconstruct the private key and return the wallet. const privateKey = _shamirsSecretSharing.default.combine([Buffer.from(decrypted_shares[0], "base64"), Buffer.from(decrypted_shares[1], "base64")]).toString(); this._logger(`Succesfully reconstructed secret.`); return privateKey; }; } var _default = MetaphiWalletApi; exports.default = _default; }).call(this)}).call(this,require("buffer").Buffer) },{"./utils":7,"@ethereumjs/common":60,"@ethereumjs/tx":64,"@maticnetwork/maticjs":211,"@maticnetwork/maticjs-plasma":207,"@maticnetwork/maticjs-web3":210,"@truffle/hdwallet-provider":227,"axios":293,"buffer":369,"crypto":395,"ethereumjs-util":491,"ethereumjs-wallet":497,"ethers":501,"js-cookie":561,"shamirs-secret-sharing":713,"store":720,"store/plugins/expire":721,"web3":849}],3:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _airwalletApi = _interopRequireDefault(require("./airwallet-api")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const s_wallet = Symbol(); /** * * Wrapper class for Wallet Instance. * Creates a Proxy object, abstracting direct access to private keys and functions. * Source: https://stackoverflow.com/questions/34869352/how-to-declare-private-variables-and-private-methods-in-es6-class */ class MetaphiWallet { constructor(options) { this[s_wallet] = new _airwalletApi.default(options); } /** * Logs a user in. * @param {String} userId - Email or phone. * @returns {Object} */ login = async (userId, userPin) => { return await this[s_wallet].login(userId, userPin); }; /** * Verifies a user. * @param {String} userId - Email or phone. * @param {Number} verificationCode - Authorization Code * @returns {Boolean} */ verifyAuthenticationCode = async (userId, verificationCode) => { return await this[s_wallet]._verifyUserVerificationCode(userId, verificationCode); }; /** * Transfer value to an address. * @param {Address} toAddress * @param {Number} value - in wei * @returns */ transfer = async (toAddress, value) => { return await this[s_wallet].transfer(toAddress, value); }; /** * Get web3 provider instance. * @returns {Provider} */ getProvider = () => { return this[s_wallet].getProvider(); }; /** * Get wallet public address. * @returns {Address} */ getAddress = () => { return this[s_wallet].getAddress(); }; /** * Sign a transaction * @param {Object} transaction object * @returns {String} signature */ signTransaction = async transaction => { return await this[s_wallet].signTransaction(transaction); }; /** * Sign a message. * @param {String} message * @returns {String} signed message */ personalSign = async message => { return await this[s_wallet].personalSign(message); }; /** * Connect wallet. * @param {Number} userPin * @returns {Boolean} */ connect = async userPin => { return await this[s_wallet].connect(userPin); }; /** * Disconnect wallet. * @returns {void} */ disconnect = () => { return this[s_wallet].disconnect(); }; /** * Check if user is already authenticated. * @returns {Boolean} */ isUserLoggedIn = () => { return this[s_wallet].isUserLoggedIn(); }; /** * Get list of userIds for logged in users. * @returns {Array} */ getLoggedInUsers = () => { return this[s_wallet].getLoggedInUsers(); }; /** * Get private key. Use with caution! * @returns {String} */ getPrivateKey = () => { return this[s_wallet]._privateKey; }; /** * Create a new wallet. * @param {Number} userPin * @returns */ createWallet = userPin => { return this[s_wallet].createNewWallet(userPin); }; } /** * Exposed proxy. * */ class MetaphiWalletProxy { constructor(options) { const wallet = new MetaphiWallet(options); return new Proxy(wallet, { get(oTarget, sKey) { return oTarget[sKey] || oTarget.getItem(sKey) || undefined; } }); } } exports.default = MetaphiWalletProxy; },{"./airwallet-api":2}],4:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WALLET_EMBED_ID = exports.MetaphiInputTypes = exports.MetaphiIframe = void 0; const MetaphiIframe = Object.freeze({ GET_LOGGED_IN_USER: 'get_logged_in_user' }); exports.MetaphiIframe = MetaphiIframe; const MetaphiInputTypes = Object.freeze({ VERIFICATION_CODE: 1, USER_PIN: 2, TRANSACTION_SIGN: 3, SUCCESS: 4, PROCESSING: 5, ERROR: 6, CONNECT: 7, PIN_RECONNECT: 8, EMAIL: 9 }); exports.MetaphiInputTypes = MetaphiInputTypes; const WALLET_EMBED_ID = 'mWalletPlugin'; exports.WALLET_EMBED_ID = WALLET_EMBED_ID; },{}],5:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "MetaphiIframe", { enumerable: true, get: function () { return _constants.MetaphiIframe; } }); Object.defineProperty(exports, "MetaphiInputTypes", { enumerable: true, get: function () { return _constants.MetaphiInputTypes; } }); Object.defineProperty(exports, "MetaphiWallet", { enumerable: true, get: function () { return _airwallet.default; } }); Object.defineProperty(exports, "WalletPlugin", { enumerable: true, get: function () { return _plugin.default; } }); var _airwallet = _interopRequireDefault(require("./airwallet")); var _plugin = _interopRequireDefault(require("./plugin")); var _constants = require("./constants"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } },{"./airwallet":3,"./constants":4,"./plugin":6}],6:[function(require,module,exports){ (function (global){(function (){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; 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. retur