@metaphi/airwallet-api
Version:
Metaphi Airwallet API to add whitelabel, non-custodial wallets to dApps
1,768 lines (1,383 loc) • 8.12 MB
JavaScript
(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