Quick Start
Start Here
This guide focuses on demonstrating how you can use Programmable Key Pairs (PKPs) to build secure, non-custodial user wallets.
PKPs are decentralized public / private key pairs generated by the Lit Network. These key pairs can be used for various use cases, but a popular way to use them is to offer wallets for your users.
The following steps will walk you through the process of creating a PKP, assigning permissions to it, and signing a transaction.
This guide uses Lit's Datil Network, the Mainnet Beta, which is designed for application developers aiming to build production-ready applications. For those developing in a test environment, the Datil-test Network is recommended. More on Lit networks here.
For developers looking to explore beyond the basics, check out Advanced Topics.
You can also view an embedded wallets demo here: https://lit-pkp-auth-demo.vercel.app/
Steps
Install and Import the Lit SDK
Ensure you have the following requirements in place:
- Operating System: Linux, Mac OS, or Windows.
- Development Environment: You'll need an Integrated Development Environment (IDE) installed. We recommend Visual Studio Code.
- Languages: The Lit JS SDK supports JavaScript. Make sure you have the appropriate language environment set up.
- Internet Connection: A stable internet connection is required for installation, updates, and interacting with the Lit nodes.
Install the @lit-protocol/lit-node-client
package, which can be used in both browser and Node environments:
yarn add @lit-protocol/lit-node-client
Use the Lit JS SDK:
import * as LitJsSdk from "@lit-protocol/lit-node-client";
You should use at least Node v19.9.0
- crypto support.
- webcrypto library support if targeting
web
.
Client-Side Usage
Within a file (in the Lit example repos it will likely be called lit.js
), set up your Lit object.
client.connect()
will return a promise that resolves when you are connected to the Lit Network.
import { LitNetwork } from "@lit-protocol/constants";
const client = new LitJsSdk.LitNodeClient({
litNetwork: LitNetwork.Datil,
});
await client.connect();
To avoid errors from Lit nodes due to stale sessionSigs
, make sure to clear the local storage for sessionSigs
before reconnecting or restarting the client. One way to do this is to disconnect the client first and then reconnect.
The client listens to network state, and those listeners will keep your client running until you explicitly disconnect from the Lit network. To stop the client listeners and allow the browser to disconnect gracefully, call:
await client.disconnect();
Server-Side Usage
In this example stub, the litNodeClient is stored in a global variable app.locals.litNodeClient
so that it can be used throughout the server. app.locals
is provided by Express for this purpose. You may have to use what your own server framework provides for this purpose, instead.
Keep in mind that in the server-side implementation, the client class is named LitNodeClientNodeJs.
app.locals.litNodeClient.connect()
returns a promise that resolves when you are connected to the Lit network.
import { LitNetwork } from "@lit-protocol/constants";
app.locals.litNodeClient = new LitJsSdk.LitNodeClientNodeJs({
alertWhenUnauthorized: false,
litNetwork: LitNetwork.Datil,
});
await app.locals.litNodeClient.connect();
The litNodeClient listens to network state, and those listeners will keep your Node.js process running until you explicitly disconnect from the Lit network. To stop the litNodeClient listeners and allow node to exit gracefully, call:
await app.locals.litNodeClient.disconnect();
Install the Required Packages
yarn add @lit-protocol/contracts-sdk
yarn add @lit-protocol/lit-auth-client
Set up a controller wallet
To initialize a LitContracts client you need an Ethereum Signer. This can be a standard Ethereum wallet (ethers) or a PKP (more info on the latter here). Here, we're going to use a standard Ethereum wallet.
You'll need to use ethers.js v5 with the Lit SDK. The Lit SDK is not compatible with ethers.js v6 or higher.
Initialize the contracts-sdk
import { LitContracts } from '@lit-protocol/contracts-sdk';
import { LitNetwork } from "@lit-protocol/constants";
const contractClient = new LitContracts({
signer: wallet,
network: LitNetwork.Datil,
});
await contractClient.connect();
You’ll need to ensure you have some test tokens to pay for gas fees. You can claim test tokens from our verified faucet.
Authenticate with the Lit Network
In order to interact with the nodes in the Lit Network, you will need to generate and present signatures. You can do this by generating a 'Session Sig'. Any signature compliant with EIP-4361 (also known as Sign in with Ethereum (SIWE)) cam be used for this.
Obtaining a SessionSigs
in the browser
Using the Lit SDK and the methods createSiweMessageWithRecaps
and generateAuthSig
from the @lit-protocol/auth-helpers
package, we can create a SessionSigs
by signing a SIWE message using a private key stored in a browser wallet like MetaMask:
import { LitNetwork } from "@lit-protocol/constants";
import { LitNodeClient } from "@lit-protocol/lit-node-client";
import {
LitAbility,
LitAccessControlConditionResource,
createSiweMessage,
generateAuthSig,
} from "@lit-protocol/auth-helpers";
import * as ethers from "ethers";
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const ethersSigner = provider.getSigner();
const litNodeClient = new LitNodeClient({
litNetwork: LitNetwork.Datil,
});
await litNodeClient.connect();
const sessionSigs = await litNodeClient.getSessionSigs({
chain: "ethereum",
expiration: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(), // 24 hours
resourceAbilityRequests: [
{
resource: new LitActionResource("*"),
ability: LitAbility.LitActionExecution,
},
],
authNeededCallback: async ({ resourceAbilityRequests, expiration, uri }) => {
const toSign = await createSiweMessageWithRecaps({
uri,
expiration,
resources: resourceAbilityRequests,
walletAddress: await ethersSigner.getAddress(),
nonce: await litNodeClient.getLatestBlockhash(),
litNodeClient,
});
return await generateAuthSig({
signer: ethersSigner,
toSign,
});
},
});
Be sure to use the latest blockhash from the litNodeClient
as the nonce. You can get it from the litNodeClient.getLatestBlockhash()
.
Obtaining an Session Signature
on the server-side
If you want to obtain an Session Signature
on the server-side, you can instantiate an ethers.Signer
to sign a SIWE message, which will produce a signature that can be used in an Session Signature
object.
The nonce should be the latest Ethereum blockhash returned by the nodes during the handshake
const LitJsSdk = require('@lit-protocol/lit-node-client-nodejs');
const { ethers } = require("ethers");
const siwe = require('siwe');
let nonce = await litNodeClient.getLatestBlockhash();
// Initialize the signer
const wallet = new ethers.Wallet('<Your private key>');
const address = ethers.getAddress(await wallet.getAddress());
// Craft the SIWE message
const domain = 'localhost';
const origin = 'https://localhost/login';
const statement =
'This is a test statement. You can put anything you want here.';
// expiration time in ISO 8601 format. This is 7 days in the future, calculated in milliseconds
const expirationTime = new Date(
Date.now() + 1000 * 60 * 60 * 24 * 7 * 10000
).toISOString();
const siweMessage = new siwe.SiweMessage({
domain,
address: address,
statement,
uri: origin,
version: '1',
chainId: 1,
nonce,
expirationTime,
});
const messageToSign = siweMessage.prepareMessage();
// Sign the message and format the authSig
const signature = await wallet.signMessage(messageToSign);
const authSig = {
sig: signature,
derivedVia: 'web3.eth.personal.sign',
signedMessage: messageToSign,
address: address,
};
console.log(authSig);
// Form the authNeededCallback to create a session with
// the wallet signature.
const authNeededCallback = async (params) => {
const response = await client.signSessionKey({
statement: params.statement,
authMethods: [
{
authMethodType: 1,
// use the authSig created above to authenticate
// allowing the pkp to sign on behalf.
accessToken: JSON.stringify(authSig),
},
],
pkpPublicKey: `<your pkp public key>`,
expiration: params.expiration,
resources: params.resources,
chainId: 1,
});
return response.authSig;
};
// Set resources to allow for signing of any message.
const resourceAbilities = [
{
resource: new LitActionResource('*'),
ability: LitAbility.PKPSigning,
},
];
// Get the session key for the session signing request
// will be accessed from local storage or created just in time.
const sessionKeyPair = client.getSessionKey();
// Request a session with the callback to sign
// with an EOA wallet from the custom auth needed callback created above.
const sessionSigs = await client.getSessionSigs({
chain: "ethereum",
expiration: new Date(Date.now() + 60_000 * 60).toISOString(),
resourceAbilityRequests: resourceAbilities,
authNeededCallback,
});
Mint a PKP and Add Permitted Scopes
Now that we have installed all of the required packages and authenticated with the Lit nodes we will mint a PKP and set its permitted scopes using the contracts-sdk
.
Permitted scopes are a crucial part of defining the capabilities of each authentication method you use. They determine what actions a given authentication method can perform with the PKP. For instance, the SignAnything
scope allows an auth method to sign any data, while the PersonalSign
scope restricts it to signing messages using the EIP-191 scheme.
You can also set scopes: []
which will mean that the auth method can only be used for authentication, but not authorization. This means that the auth method can be used to prove that the user is who they say they are, but cannot be used to sign transactions or messages. You can read more about Auth Method scopes here.
The following code block demonstrates how to mint a PKP with specific permitted scopes:
The PKP NFT represents root ownership of the key pair. The NFT owner can grant other users (via a wallet address) or Lit Actions the ability to use the PKP to sign transactions or assign additional authentication methods.
import { AuthMethodScope, AuthMethodType } from '@lit-protocol/constants';
const authMethod = {
authMethodType: AuthMethodType.EthWallet,
accessToken: JSON.stringify(authSig),
};
const mintInfo = await contractClient.mintWithAuth({
authMethod: authMethod,
scopes: [
// AuthMethodScope.NoPermissions,
AuthMethodScope.SignAnything,
AuthMethodScope.PersonalSign
],
});
// output:
{
pkp: {
tokenId: string;
publicKey: string;
ethAddress: string;
};
tx: ethers.ContractReceipt;
}
You should now have successfully minted a PKP! You can verify that the PKP has the necessary permissions for signing by calling the following function:
import { LitAuthClient } from '@lit-protocol/lit-auth-client';
const authId = await LitAuthClient.getAuthIdByAuthMethod(authMethod);
await contractClient.pkpPermissionsContract.read.getPermittedAuthMethodScopes(
mintInfo.pkp.tokenId,
AuthMethodType.EthWallet,
authId,
3
);
const signAnythingScope = scopes[1];
const personalSignScope = scopes[2];
Additional Demos:
- Minting a PKP with an auth method and permitted scopes (Easy)
- Minting a PKP with an auth method and permitted scopes (Advanced)
- Minting a PKP using social login
Mint Capacity Credits and Delegate Usage
In order to execute a transaction with Lit, you’ll need to reserve capacity on the network using Capacity Credits. These allow holders to reserve a set number of requests (requests per second) over a desired period of time (i.e. one week). You can mint a Capacity Credit NFT using the contracts-sdk
in a couple of easy steps.
The first step is to initialize a signer. This should be a wallet controlled by your application and the same wallet you’ll use to mint the Capacity Credit NFT:
import { LitNetwork } from "@lit-protocol/constants";
const walletWithCapacityCredit = new Wallet("<your private key or mnemonic>");
let contractClient = new LitContracts({
signer: dAppOwnerWallet,
network: LitNetwork.Datil,
});
await contractClient.connect();
After you’ve set your wallet, your next step is to mint the NFT:
// this identifier will be used in delegation requests.
const { capacityTokenIdStr } = await contractClient.mintCapacityCreditsNFT({
requestsPerKilosecond: 80,
// requestsPerDay: 14400,
// requestsPerSecond: 10,
daysUntilUTCMidnightExpiration: 2,
});
In the above example, we are configuring 2 properties:
requestsPerDay
- How many requests can be sent in a 24 hour period.daysUntilUTCMidnightExpiration
- The number of days until the nft will expire. expiration will occur atUTC Midnight
of the day specified.
Once you mint your NFT you will be able to send X many requests per day where X is the number specified in requestsPerDay
. Once the Capacity Credit
is minted the tokenId
can be used in delegation requests.
Delegate usage to your PKP
Once you have minted a Capacity Credits NFT, you can delegate usage of it to the PKP we minted earlier. This will allow that PKP to make requests to the Lit network.
const { capacityDelegationAuthSig } =
await litNodeClient.createCapacityDelegationAuthSig({
uses: '1',
dAppOwnerWallet: wallet,
capacityTokenId: capacityTokenIdStr,
delegateeAddresses: [secondWalletPKPInfo.ethAddress],
});
To delegate your Rate Limit NFT there are 4 properties to configure:
uses
- How many times the delegation may be useddAppOwnerWallet
- The owner of the wallet as anethers Wallet instance
capacityTokenId
- Thetoken identifier
of the Rate Limit NFTdelegateeAddresses
- The wallet addresses which will be delegated to
The delegateeAddress
parameter is optional. If omitted, anyone can use your capacityDelegationAuthSig
to use your app without restrictions. In this case, you can utilize other restrictions like the uses
param to limit the amount of usage by your users.
Using a delegated AuthSig
from a backend
If using a mainnet
in order to keep the wallet which holds the Capacity Credit NFT
secure it is recommended to call createCapacityDelegationAuthSig
from LitNodeClient
in a backend context. There are a few recommended web servers you can use in order to host an api endpoint which can return the capacityDelegationAuthSig
. Some links are provided below to help get started:
Generating a Session Signature from the Capacity Credit delegation
We can use the Capacity Credit delegation to generate a Session Signature for the PKP:
const pkpAuthNeededCallback = async ({
expiration,
resources,
resourceAbilityRequests,
}) => {
// -- validate
if (!expiration) {
throw new Error('expiration is required');
}
if (!resources) {
throw new Error('resources is required');
}
if (!resourceAbilityRequests) {
throw new Error('resourceAbilityRequests is required');
}
const response = await litNodeClient.signSessionKey({
statement: 'Some custom statement.',
authMethods: [secondWalletControllerAuthMethod], // authMethods for signing the sessionSigs
pkpPublicKey: secondWalletPKPInfo.publicKey, // public key of the wallet which is delegated
expiration: expiration,
resources: resources,
chainId: 1,
// optional (this would use normal siwe lib, without it, it would use lit-siwe)
resourceAbilityRequests: resourceAbilityRequests,
});
console.log('response:', response);
return response.authSig;
};
const pkpSessionSigs = await litNodeClient.getSessionSigs({
pkpPublicKey: secondWalletPKPInfo.publicKey, // public key of the wallet which is delegated
expiration: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(), // 24 hours
chain: 'ethereum',
resourceAbilityRequests: [
{
resource: new LitPKPResource('*'),
ability: LitAbility.PKPSigning,
},
],
authNeededCallback: pkpAuthNeededCallback,
capacityDelegationAuthSig, // here is where we add the delegation to our session request
});
console.log("generated session with delegation ", pkpSessionSigs);
const res = await litNodeClient.executeJs({
sessionSigs: pkpSessionSigs,
code: `(async () => {
const sigShare = await LitActions.signEcdsa({
toSign: dataToSign,
publicKey,
sigName: "sig",
});
})();`,
authMethods: [],
jsParams: { // parameters to js function above
dataToSign: ethers.utils.arrayify(
ethers.utils.keccak256([1, 2, 3, 4, 5])
),
publicKey: secondWalletPKPInfo.publicKey,
},
});
console.log("signature result ", res);
Managing Authentication Methods
Authentication methods ('auth methods' for short) are the specific credentials (i.e. a wallet address or email account) that have permission to control and manage permissions associated with the underlying PKP (for example, to add another auth method or sign a transaction).
To manage the auth methods registered to a Programmabale Key Pair
you will need to use the LitContracts
package that we installed earlier. Once that has been installed, you can add an auth method in the following steps:
If a Programmable Key Pair
owns itself through the sendPkpToIteself
flag you will need to use an instance of the PkpEthersWallet
as the signer
in the LitContracts
constructor options:
import { LitContracts } from "@lit-protocol/contracts-sdk";
const litContracts = new LitContracts({
signer: pkpWallet, // pkp wallet of the owner of the pkp NFT
});
await litContracts.connect();
If the NFT
was not sent to the PKP
then you may use the wallet which minted the PKP
:
import { LitContracts } from "@lit-protocol/contracts-sdk";
import { ethers } from 'ethers';
import { LIT_RPC }
const provider = new ethers.providers.JsonRpcProvider(
LIT_RPC.CHRONICLE_YELLOWSTONE
);
const wallet = new ethers.Wallet('<Your private key>', provider);
const litContracts = new LitContracts({
signer: wallet, // pkp wallet of the owner of the pkp NFT
});
await litContracts.connect();
Adding an Auth Method
Modifying AuthMethods for a given PKP
may be done through the PKPPermissions Contract
below is an example of adding and auth method to an existing PKP
and giving it a scope of 1
for sign anything.
const transaction =
await litContracts.pkpPermissionsContract.write.addPermittedAuthMethod(
"<The token ID of the PKP you want to add an auth method to>",
"<The auth method object you want to add>",
[BigNumber.from(1)], // 1 is the permission for arbitrary signing
{ gasPrice: utils.parseUnits("0.001", "gwei"), gasLimit: 400000 }
);
const result = await transaction.wait();
Sign a Transaction
Lit Action Signing
You can use Lit Actions to sign transactions. These are JavaScript programs that can be used to specify the signing and authentication logic for PKPs.
To sign a Lit Action with your PKP, we'll use the litNodeClient
to call the executeJs
parameter.
toSign
data is required to be in 32 byte format.
The ethers.utils.arrayify(ethers.utils.keccak256(...)
can be used to convert the toSign
data to the correct format.
Set up the Lit Action code to be run on the Lit nodes like so:
const litActionCode = `
const go = async () => {
// The params toSign, publicKey, sigName are passed from the jsParams fields and are available here
const sigShare = await Lit.Actions.signEcdsa({ toSign, publicKey, sigName });
};
go();
`;
const signatures = await litNodeClient.executeJs({
code: litActionCode,
authSig,
jsParams: {
toSign: [84, 104, 105, 115, 32, 109, 101, 115, 115, 97, 103, 101, 32, 105, 115, 32, 101, 120, 97, 99, 116, 108, 121, 32, 51, 50, 32, 98, 121, 116, 101, 115],
publicKey: mintInfo.pkp.publicKey,
sigName: "sig1",
},
});
console.log("signatures: ", signatures);
You can also use the ipfsId
param if you’d prefer to store your Lit Action code on IPFS.
The ipfs ID: QmRwN9GKHvCn4Vk7biqtr6adjXMs7PzzYPCzNCRjPFiDjm
contains the same code as the "litActionCode" variable above. You can check out the full code here.
const signatures = await litNodeClient.executeJs({
ipfsId: "QmRwN9GKHvCn4Vk7biqtr6adjXMs7PzzYPCzNCRjPFiDjm",
authSig,
jsParams: {
toSign: [84, 104, 105, 115, 32, 109, 101, 115, 115, 97, 103, 101, 32, 105, 115, 32, 101, 120, 97, 99, 116, 108, 121, 32, 51, 50, 32, 98, 121, 116, 101, 115],
publicKey: mintInfo.pkp.publicKey,
sigName: "sig1",
},
});
Learn More
By now you should have successfully minted a PKP, assigned an auth method and permitted scopes, and used it to sign a message with a Lit Action. If you’d like to learn more about all of the available functionality provided by PKPs, please follow the links below:
- Managing PKP Auth Methods.
- Minting PKPs with Social Auth.
- Running Custom Authentication.
- Connecting PKPs to dApps with WalletConnect.
Not finding the answer you're looking for? Share your feedback on these docs by creating an issue in our GitHub Issues and Reports repository or get support by visiting our Support page.