UNPKG

@lit-protocol/auth-browser

Version:

Browser-specific authentication utilities for the Lit Protocol, enabling seamless connection to various blockchain networks including Ethereum, Cosmos, and Solana.

683 lines 25.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.signMessageAsync = exports.signMessage = exports.signAndSaveAuthMessage = exports.checkAndSignEVMAuthMessage = exports.disconnectWeb3 = exports.connectWeb3 = exports.decodeCallResult = exports.encodeCallData = exports.getRPCUrls = exports.getMustResign = exports.getChainId = exports.chainHexIdToChainName = void 0; exports.isSignedMessageExpired = isSignedMessageExpired; const tslib_1 = require("tslib"); const buffer_1 = require("buffer"); const depd_1 = tslib_1.__importDefault(require("depd")); const bytes_1 = require("@ethersproject/bytes"); const providers_1 = require("@ethersproject/providers"); const strings_1 = require("@ethersproject/strings"); // import WalletConnectProvider from '@walletconnect/ethereum-provider'; const wallet_1 = require("@ethersproject/wallet"); const ethereum_provider_1 = require("@walletconnect/ethereum-provider"); const ethers_1 = require("ethers"); const utils_1 = require("ethers/lib/utils"); const siwe_1 = require("siwe"); // @ts-ignore: If importing 'nacl' directly, the built files will use .default instead const nacl = tslib_1.__importStar(require("tweetnacl")); const naclUtil = tslib_1.__importStar(require("tweetnacl-util")); // @ts-ignore: If importing 'nacl' directly, the built files will use .default instead const constants_1 = require("@lit-protocol/constants"); const misc_1 = require("@lit-protocol/misc"); const misc_browser_1 = require("@lit-protocol/misc-browser"); const modal_1 = tslib_1.__importDefault(require("../connect-modal/modal")); const deprecated = (0, depd_1.default)('lit-js-sdk:auth-browser:index'); if (globalThis && typeof globalThis.Buffer === 'undefined') { globalThis.Buffer = buffer_1.Buffer; } const WALLET_ERROR = { REQUESTED_CHAIN_HAS_NOT_BEEN_ADDED: 4902, NO_SUCH_METHOD: -32601, }; /** ---------- Local Helpers ---------- */ let litWCProvider; /** * * Convert chain hex id to chain name * * @param { string } chainHexId * @returns { void | string } */ const chainHexIdToChainName = (chainHexId) => { // -- setup const entries = Object.entries(constants_1.LIT_CHAINS); const hexIds = Object.values(constants_1.LIT_CHAINS).map((chain) => '0x' + chain.chainId.toString(16)); // -- validate:: must begin with 0x if (!chainHexId.startsWith('0x')) { throw new constants_1.WrongParamFormat({ info: { param: 'chainHexId', value: chainHexId, }, }, '%s should begin with "0x"', chainHexId); } // -- validate:: hex id must be listed in constants if (!hexIds.includes(chainHexId)) { throw new constants_1.UnsupportedChainException({ info: { chainHexId, }, }, `Unsupported chain selected. Please select one of: %s`, Object.keys(constants_1.LIT_CHAINS)); } // -- search const chainName = entries.find((data) => '0x' + data[1].chainId.toString(16) === chainHexId) || null; // -- success case if (chainName) { return chainName[0]; } // -- fail case throw new constants_1.UnknownError({ info: { chainHexId, }, }, 'Failed to convert %s', chainHexId); }; exports.chainHexIdToChainName = chainHexIdToChainName; /** * Get chain id of the current network * @param { string } chain * @param { Web3Provider } web3 * @returns { Promise<IEither> } */ const getChainId = async (chain, web3) => { let resultOrError; try { const resp = await web3.getNetwork(); resultOrError = (0, constants_1.ERight)(resp.chainId); } catch (e) { // couldn't get chainId. throw the incorrect network error (0, misc_1.log)('getNetwork threw an exception', e); resultOrError = (0, constants_1.ELeft)(new constants_1.WrongNetworkException({ info: { chain, }, }, `Incorrect network selected. Please switch to the %s network in your wallet and try again.`, chain)); } return resultOrError; }; exports.getChainId = getChainId; /** * Check if the Expiration Time in the signedMessage string is expired. * @param { string } signedMessage - The signed message containing the Expiration Time. * @returns true if expired, false otherwise. */ function isSignedMessageExpired(signedMessage) { // Extract the Expiration Time from the signed message. const dateStr = signedMessage .split('\n')[9] ?.replace('Expiration Time: ', ''); const expirationTime = new Date(dateStr); const currentTime = new Date(); // Compare the Expiration Time with the current time. return currentTime > expirationTime; } /** * * Check if the message must resign * * @param { AuthSig } authSig * @param { any } resources * * @returns { boolean } */ const getMustResign = (authSig, resources) => { let mustResign; // if it's not expired, then we don't need to resign if (!isSignedMessageExpired(authSig.signedMessage)) { return false; } try { const parsedSiwe = new siwe_1.SiweMessage(authSig.signedMessage); (0, misc_1.log)('parsedSiwe.resources', parsedSiwe.resources); if (JSON.stringify(parsedSiwe.resources) !== JSON.stringify(resources)) { (0, misc_1.log)('signing auth message because resources differ from the resources in the auth sig'); mustResign = true; } if (parsedSiwe.address !== (0, utils_1.getAddress)(parsedSiwe.address)) { (0, misc_1.log)('signing auth message because parsedSig.address is not equal to the same address but checksummed. This usually means the user had a non-checksummed address saved and so they need to re-sign.'); mustResign = true; } } catch (e) { (0, misc_1.log)('error parsing siwe sig. making the user sign again: ', e); mustResign = true; } return mustResign; }; exports.getMustResign = getMustResign; /** * * Get RPC Urls in the correct format * need to make it look like this: --- rpc: { 1: "https://mainnet.mycustomnode.com", 3: "https://ropsten.mycustomnode.com", 100: "https://dai.poa.network", // ... }, --- * * @returns */ const getRPCUrls = () => { const rpcUrls = {}; const keys = Object.keys(constants_1.LIT_CHAINS); for (const chainName of keys) { const chainId = constants_1.LIT_CHAINS[chainName].chainId; const rpcUrl = constants_1.LIT_CHAINS[chainName].rpcUrls[0]; rpcUrls[chainId.toString()] = rpcUrl; } return rpcUrls; }; exports.getRPCUrls = getRPCUrls; /** ---------- Exports ---------- */ /** * @deprecated * encodeCallData has been removed. * * @param { IABIEncode } * @returns { string } */ exports.encodeCallData = deprecated.function(({ abi, functionName, functionParams }) => { throw new constants_1.RemovedFunctionError({}, 'encodeCallData has been removed.'); }, 'encodeCallData has been removed.'); /** * @deprecated * (ABI) Decode call data * * @param { IABIDecode } * @returns { string } */ exports.decodeCallResult = deprecated.function(({ abi, functionName, data }) => { const _interface = new ethers_1.ethers.utils.Interface(abi); const decoded = _interface.decodeFunctionResult(functionName, data); return decoded; }, 'decodeCallResult will be removed.'); /** * @browserOnly * Connect to web 3 * * @param { ConnectWeb3 } * * @return { Promise<ConnectWeb3Result> } web3, account */ const connectWeb3 = async ({ chainId = 1, walletConnectProjectId, }) => { // -- check if it's nodejs if ((0, misc_1.isNode)()) { (0, misc_1.log)('connectWeb3 is not supported in nodejs.'); return { web3: null, account: null }; } const rpcUrls = (0, exports.getRPCUrls)(); let providerOptions = {}; if (walletConnectProjectId) { const wcProvider = await ethereum_provider_1.EthereumProvider.init({ projectId: walletConnectProjectId, chains: [chainId], showQrModal: true, optionalMethods: ['eth_sign'], rpcMap: rpcUrls, }); providerOptions = { walletconnect: { provider: wcProvider, }, }; if ((0, misc_1.isBrowser)()) { litWCProvider = wcProvider; } } (0, misc_1.log)('getting provider via lit connect modal'); const dialog = new modal_1.default({ providerOptions }); const provider = await dialog.getWalletProvider(); (0, misc_1.log)('got provider'); // @ts-ignore const web3 = new providers_1.Web3Provider(provider); // trigger metamask popup try { deprecated('@deprecated soon to be removed. - trying to enable provider. this will trigger the metamask popup.'); // @ts-ignore await provider.enable(); } catch (e) { (0, misc_1.log)("error enabling provider but swallowed it because it's not important. most wallets use a different function now to enable the wallet so you can ignore this error, because those other methods will be tried.", e); } (0, misc_1.log)('listing accounts'); const accounts = await web3.listAccounts(); (0, misc_1.log)('accounts', accounts); const account = ethers_1.ethers.utils.getAddress(accounts[0]); return { web3, account }; }; exports.connectWeb3 = connectWeb3; /** * @browserOnly * Delete any saved AuthSigs from local storage. Takes no params and returns * nothing. This will also clear out the WalletConnect cache in local storage. * We often run this function as a result of the user pressing a "Logout" button. * * @return { void } */ const disconnectWeb3 = () => { if ((0, misc_1.isNode)()) { (0, misc_1.log)('disconnectWeb3 is not supported in nodejs.'); return; } // @ts-ignore if ((0, misc_1.isBrowser)() && litWCProvider) { try { litWCProvider.disconnect(); } catch (err) { (0, misc_1.log)('Attempted to disconnect global WalletConnectProvider for lit-connect-modal', err); } } const storage = constants_1.LOCAL_STORAGE_KEYS; localStorage.removeItem(storage.AUTH_SIGNATURE); localStorage.removeItem(storage.AUTH_SOL_SIGNATURE); localStorage.removeItem(storage.AUTH_COSMOS_SIGNATURE); localStorage.removeItem(storage.WEB3_PROVIDER); localStorage.removeItem(storage.WALLET_SIGNATURE); }; exports.disconnectWeb3 = disconnectWeb3; /** * @browserOnly * Check and sign EVM auth message * * @param { CheckAndSignAuthParams } * @returns */ const checkAndSignEVMAuthMessage = async ({ chain, resources, switchChain, expiration, uri, walletConnectProjectId, nonce, }) => { // -- check if it's nodejs if ((0, misc_1.isNode)()) { (0, misc_1.log)('checkAndSignEVMAuthMessage is not supported in nodejs. You can create a SIWE on your own using the SIWE package.'); return { sig: '', derivedVia: '', signedMessage: '', address: '', }; } // --- scoped methods --- const _throwIncorrectNetworkError = (error) => { if (error.code === WALLET_ERROR.NO_SUCH_METHOD) { throw new constants_1.WrongNetworkException({ info: { chain, }, }, `Incorrect network selected. Please switch to the ${chain} network in your wallet and try again.`); } else { throw error; } }; // -- 1. prepare const selectedChain = constants_1.LIT_CHAINS[chain]; const expirationString = expiration ?? getDefaultExpiration(); const { web3, account } = await (0, exports.connectWeb3)({ chainId: selectedChain.chainId, walletConnectProjectId, }); (0, misc_1.log)(`got web3 and account: ${account}`); // -- 2. prepare all required variables const currentChainIdOrError = await (0, exports.getChainId)(chain, web3); const selectedChainId = selectedChain.chainId; const selectedChainIdHex = (0, misc_1.numberToHex)(selectedChainId); let authSigOrError = (0, misc_browser_1.getStorageItem)(constants_1.LOCAL_STORAGE_KEYS.AUTH_SIGNATURE); (0, misc_1.log)('currentChainIdOrError:', currentChainIdOrError); (0, misc_1.log)('selectedChainId:', selectedChainId); (0, misc_1.log)('selectedChainIdHex:', selectedChainIdHex); (0, misc_1.log)('authSigOrError:', authSigOrError); // -- 3. check all variables before executing business logic if (currentChainIdOrError.type === constants_1.EITHER_TYPE.ERROR) { throw new constants_1.UnknownError({ info: { chainId: chain, }, cause: currentChainIdOrError.result, }, 'Unknown error when getting chain id'); } (0, misc_1.log)('chainId from web3', currentChainIdOrError); (0, misc_1.log)(`checkAndSignAuthMessage with chainId ${currentChainIdOrError} and chain set to ${chain} and selectedChain is `, selectedChain); // -- 4. case: (current chain id is NOT equal to selected chain) AND is set to switch chain if (currentChainIdOrError.result !== selectedChainId && switchChain) { const provider = web3.provider; // -- (case) if able to switch chain id try { (0, misc_1.log)('trying to switch to chainId', selectedChainIdHex); await provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: selectedChainIdHex }], }); // -- (case) if unable to switch chain } catch (switchError) { (0, misc_1.log)('error switching to chainId', switchError); // -- (error case) if (switchError.code === WALLET_ERROR.REQUESTED_CHAIN_HAS_NOT_BEEN_ADDED) { try { const data = [ { chainId: selectedChainIdHex, chainName: selectedChain.name, nativeCurrency: { name: selectedChain.name, symbol: selectedChain.symbol, decimals: selectedChain.decimals, }, rpcUrls: selectedChain.rpcUrls, blockExplorerUrls: selectedChain.blockExplorerUrls, }, ]; await provider.request({ method: 'wallet_addEthereumChain', params: data, }); } catch (addError) { _throwIncorrectNetworkError(addError); } } else { _throwIncorrectNetworkError(switchError); } } // we may have switched the chain to the selected chain. set the chainId accordingly currentChainIdOrError.result = selectedChain.chainId; } // -- 5. case: Lit auth signature is NOT in the local storage (0, misc_1.log)('checking if sig is in local storage'); if (authSigOrError.type === constants_1.EITHER_TYPE.ERROR) { (0, misc_1.log)('signing auth message because sig is not in local storage'); try { const authSig = await _signAndGetAuth({ web3, account, chainId: selectedChain.chainId, resources, expiration: expirationString, uri, nonce, }); authSigOrError = { type: constants_1.EITHER_TYPE.SUCCESS, result: JSON.stringify(authSig), }; } catch (e) { throw new constants_1.UnknownError({ info: { account, chainId: selectedChain.chainId, resources, expiration: expirationString, uri, nonce, }, cause: e, }, 'Could not get authenticated message'); } // Log new authSig (0, misc_1.log)('5. authSigOrError:', authSigOrError); } // -- 6. case: Lit auth signature IS in the local storage const authSigString = authSigOrError.result; let authSig = JSON.parse(authSigString); (0, misc_1.log)('6. authSig:', authSig); // -- 7. case: when we are NOT on the right wallet address if (account.toLowerCase() !== authSig.address.toLowerCase()) { (0, misc_1.log)('signing auth message because account is not the same as the address in the auth sig'); authSig = await _signAndGetAuth({ web3, account, chainId: selectedChain.chainId, resources, expiration: expirationString, uri, nonce, }); (0, misc_1.log)('7. authSig:', authSig); // -- 8. case: we are on the right wallet, but need to check the resources of the sig and re-sign if they don't match } else { const mustResign = (0, exports.getMustResign)(authSig, resources); if (mustResign) { authSig = await _signAndGetAuth({ web3, account, chainId: selectedChain.chainId, resources, expiration: expirationString, uri, nonce, }); } (0, misc_1.log)('8. mustResign:', mustResign); } // -- 9. finally, if the authSig is expired, re-sign // if it's not expired, then we don't need to resign const checkAuthSig = (0, misc_1.validateSessionSig)(authSig); if (isSignedMessageExpired(authSig.signedMessage) || !checkAuthSig.isValid) { if (!checkAuthSig.isValid) { (0, misc_1.log)(`Invalid AuthSig: ${checkAuthSig.errors.join(', ')}`); } (0, misc_1.log)('9. authSig expired!, resigning..'); authSig = await _signAndGetAuth({ web3, account, chainId: selectedChain.chainId, resources, expiration: expirationString, uri, nonce, }); } return authSig; }; exports.checkAndSignEVMAuthMessage = checkAndSignEVMAuthMessage; const getDefaultExpiration = () => { return new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(); }; const _signAndGetAuth = async ({ web3, account, chainId, resources, expiration, uri, nonce, }) => { await (0, exports.signAndSaveAuthMessage)({ web3, account, chainId, resources, expiration, uri, nonce, }); const authSigOrError = (0, misc_browser_1.getStorageItem)(constants_1.LOCAL_STORAGE_KEYS.AUTH_SIGNATURE); if (authSigOrError.type === 'ERROR') { throw new constants_1.LocalStorageItemNotFoundException({ info: { storageKey: constants_1.LOCAL_STORAGE_KEYS.AUTH_SIGNATURE, }, }, 'Failed to get authSig from local storage'); } const authSig = typeof authSigOrError.result === 'string' ? JSON.parse(authSigOrError.result) : authSigOrError.result; return authSig; }; /** * @browserOnly * Sign the auth message with the user's wallet, and store it in localStorage. * Called by checkAndSignAuthMessage if the user does not have a signature stored. * * @param { signAndSaveAuthParams } * @returns { AuthSig } */ const signAndSaveAuthMessage = async ({ web3, account, chainId, resources, expiration, uri, nonce, }) => { // check if it's nodejs if ((0, misc_1.isNode)()) { (0, misc_1.log)('checkAndSignEVMAuthMessage is not supported in nodejs.'); return { sig: '', derivedVia: '', signedMessage: '', address: '', }; } // -- 1. prepare 'sign-in with ethereum' message const preparedMessage = { domain: globalThis.location.host, address: (0, utils_1.getAddress)(account), // convert to EIP-55 format or else SIWE complains version: '1', chainId, expirationTime: expiration, nonce, }; if (resources && resources.length > 0) { preparedMessage.resources = resources; } if (uri) { preparedMessage.uri = uri; } else { preparedMessage.uri = globalThis.location.href; } const message = new siwe_1.SiweMessage(preparedMessage); const body = message.prepareMessage(); const formattedAccount = (0, utils_1.getAddress)(account); // -- 2. sign the message const signedResult = await (0, exports.signMessage)({ body, web3, account: formattedAccount, }); // -- 3. prepare auth message const authSig = { sig: signedResult.signature, derivedVia: 'web3.eth.personal.sign', signedMessage: body, address: signedResult.address, }; // -- 4. store auth and a keypair in localstorage for communication with sgx if ((0, misc_1.isBrowser)()) { localStorage.setItem(constants_1.LOCAL_STORAGE_KEYS.AUTH_SIGNATURE, JSON.stringify(authSig)); } const commsKeyPair = nacl.box.keyPair(); if ((0, misc_1.isBrowser)()) { localStorage.setItem(constants_1.LOCAL_STORAGE_KEYS.KEY_PAIR, JSON.stringify({ publicKey: naclUtil.encodeBase64(commsKeyPair.publicKey), secretKey: naclUtil.encodeBase64(commsKeyPair.secretKey), })); } (0, misc_1.log)(`generated and saved ${constants_1.LOCAL_STORAGE_KEYS.KEY_PAIR}`); return authSig; }; exports.signAndSaveAuthMessage = signAndSaveAuthMessage; /** * @browserOnly * Sign Messags * * @param { SignMessageParams } * * @returns { Promise<SignedMessage> } */ const signMessage = async ({ body, web3, account, }) => { // check if it's nodejs if ((0, misc_1.isNode)()) { (0, misc_1.log)('signMessage is not supported in nodejs.'); return { signature: '', address: '', }; } // -- validate if (!web3 || !account) { (0, misc_1.log)(`web3: ${web3} OR ${account} not found. Connecting web3..`); const res = await (0, exports.connectWeb3)({ chainId: 1 }); web3 = res.web3; account = res.account; } (0, misc_1.log)('pausing...'); await new Promise((resolve) => setTimeout(resolve, 500)); (0, misc_1.log)('signing with ', account); const signature = await (0, exports.signMessageAsync)(web3.getSigner(), account, body); const address = (0, wallet_1.verifyMessage)(body, signature).toLowerCase(); (0, misc_1.log)('Signature: ', signature); (0, misc_1.log)('recovered address: ', address); if (address.toLowerCase() !== account.toLowerCase()) { const msg = `ruh roh, the user signed with a different address (${address}) then they're using with web3 (${account}). This will lead to confusion.`; alert('Something seems to be wrong with your wallets message signing. maybe restart your browser or your wallet. Your recovered sig address does not match your web3 account address'); throw new constants_1.InvalidSignatureError({ info: { address, account, }, }, msg); } return { signature, address }; }; exports.signMessage = signMessage; /** * @browserOnly * wrapper around signMessage that tries personal_sign first. this is to fix a * bug with walletconnect where just using signMessage was failing * * @param { any | JsonRpcProvider} signer * @param { string } address * @param { string } message * * @returns { Promise<any | JsonRpcSigner> } */ const signMessageAsync = async (signer, address, message) => { // check if it's nodejs if ((0, misc_1.isNode)()) { (0, misc_1.log)('signMessageAsync is not supported in nodejs.'); return null; } const messageBytes = (0, strings_1.toUtf8Bytes)(message); if (signer instanceof providers_1.JsonRpcSigner) { try { (0, misc_1.log)('Signing with personal_sign'); const signature = await signer.provider.send('personal_sign', [ (0, bytes_1.hexlify)(messageBytes), address.toLowerCase(), ]); return signature; } catch (e) { (0, misc_1.log)('Signing with personal_sign failed, trying signMessage as a fallback'); if (e.message.includes('personal_sign')) { return await signer.signMessage(messageBytes); } throw e; } } else { (0, misc_1.log)('signing with signMessage'); return await signer.signMessage(messageBytes); } }; exports.signMessageAsync = signMessageAsync; /** * * Get the number of decimal places in a token * * @property { string } contractAddress The token contract address * @property { string } chain The chain on which the token is deployed * * @returns { number } The number of decimal places in the token */ // export const decimalPlaces = async ({ // contractAddress, // chain, // }: { // contractAddress: string; // chain: Chain; // }): Promise<number> => { // const rpcUrl = LIT_CHAINS[chain].rpcUrls[0] as string; // const web3 = new JsonRpcProvider(rpcUrl); // const contract = new Contract( // contractAddress, // (ABI_ERC20 as any).abi, // web3 // ); // return await contract['decimals'](); // }; //# sourceMappingURL=eth.js.map