@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
JavaScript
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
;