Pimlico
Learn how to use Lit Protocol's one time password (OTP) sign-in (email, SMS, and Whatsapp) with a smart account whose user operations are relayed and sponsored by Pimlico.
Objectives
At completion of this reading you should be able to:
- Explain what Pimlico is and the services it provides with smart accounts.
- Understand how to set up Lit with Pimlico.
What is Pimlico?
Pimlico is an infrastructure platform for building the next generation of smart accounts. If you are developing an ERC-4337 smart account, we provide bundlers, verifying paymasters, ERC-20 paymasters, and much more to help you build a more stable wallet and iterate faster.
Guide
This how-to guide walks you through the steps to integrate Lit's OTP sign-in with email, SMS, and Whatsapp with a smart account whose user operations are relayed and sponsored by Pimlico.
Pre-requisites
- Familiarity with JavaScript.
- Reading Pimlico's tutorial 1 to get an understanding of Pimlico and how Lit might fit in as a signer. This will be relevant to later steps in the guide.
- Obtain a Lit relay server by filling out this form.
1. Setup
Installing packages
npm install stytch @lit-protocol/pkp-ethers @lit-protocol/lit-auth-client @lit-protocol/auth-helpers @lit-protocol/types @lit-protocol/lit-node-client-nodejs
Make an account with Stytch and get the Project ID and Secret
You can sign up for a Stytch account here.
Once you have an account, you can find your Project ID and Secret in the Stytch Dashboard API Keys page.
2. Create a Stytch client with your Project ID and Secret
In a new JavaScript file create and initialize the Stytch client.
const stytchClient = new stytch.Client({
project_id: "project-test-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
secret: "secret-test-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
});
3. Send an OTP to the user's email, SMS, or Whatsapp
- SMS
const stytchResponse = await stytchClient.otps.email.loginOrCreate({
email: "<Your Email Address>",
})
const stytchResponse = await stytchClient.otps.sms.loginOrCreate({
phone_number: "<Your Phone Number>",
})
const stytchResponse = await stytchClient.otps.whatsapp.loginOrCreate({
phone_number: "<Your Phone Number>",
})
4. Authenticate the user with the OTP and get a session token
- SMS & WhatsApp
const authResponse = await stytchClient.otps.authenticate({
method_id: stytchResponse.email_id,
code: otpResponse.code,
session_duration_minutes: 60 * 24 * 7,
})
const sessionStatus = await stytchClient.sessions.authenticate({
session_token: authResponse.session_token,
})
const authResponse = await stytchClient.otps.authenticate({
method_id: stytchResponse.phone_id,
code: otpResponse.code,
session_duration_minutes: 60 * 24 * 7,
})
const sessionStatus = await stytchClient.sessions.authenticate({
session_token: authResponse.session_token,
})
5. Get a Lit Relay Server API Key
Checking in! If you didn't complete the pre-req of getting a Lit relay key, you can get it by filling out this form and the Lit development team will get back to you shortly.
You can also ping the Lit developement team on Discord once you fill out the form.
6. Mint a PKPs through Lit Protocol
import { LIT_NETWORK } from "@lit-protocol/constants";
import { StytchOtpProvider } from "@lit-protocol/providers";
import { LitRelay } from "@lit-protocol/lit-auth-client";
const litRelay = new LitRelay({
relayUrl: LitRelay.getRelayUrl(LIT_NETWORK.DatilDev),
relayApiKey: 'test-api-key',
});
const session = new StytchOtpProvider({ relay: litRelay, litNodeClient,
userId: sessionStatus.session.user_id,
appId: "project-test-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
});
const authMethod = await session.authenticate({
accessToken: sessionStatus.session_jwt
});
await litRelay.mintPKPWithAuthMethods([authMethod], {});
const pkps = await session.fetchPKPs(authMethod);
7. Generate the Controller Session Signatures or its context to generate them on demand
import { LitNodeClientNodeJs } from "@lit-protocol/lit-node-client-nodejs";
import { LIT_ABILITY, LIT_NETWORK } from "@lit-protocol/constants";
const litNodeClient = new LitNodeClientNodeJs({
litNetwork: LIT_NETWORK.DatilDev,
debug: false,
})
await litNodeClient.connect();
const resourceAbilities = [
{
resource: new LitActionResource("*"),
ability: LIT_ABILITY.PKPSigning,
},
];
const sessionKeyPair = litNodeClient.getSessionKey();
const authNeededCallback = async (params: AuthCallbackParams) => {
const response = await litNodeClient.signSessionKey({
sessionKey: sessionKeyPair,
statement: params.statement,
authMethods: [authMethod],
pkpPublicKey: pkp[pkp.length - 1].publicKey,
expiration: params.expiration,
resources: params.resources,
chainId: 1,
});
return response.authSig;
};
// Not needed when passing authContext to PKPEthersWallet
const sessionSigs = await litNodeClient.getSessionSigs({
chain: "ethereum",
expiration: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7).toISOString(),
resourceAbilityRequests: resourceAbilities,
sessionKey: sessionKeyPair,
authNeededCallback
}).catch((err) => {
console.log("error while attempting to access session signatures: ", err)
throw err;
});
It is recommended to generate the authContext
to create sessionSigs on demand. That way you won't have to worry about refreshing them when they expire or network conditions have changed.
You can check more in the Authentication section
8. Initialize the PKP Wallet
We will now generate a wallet that can act a regular Ethers.js wallet, but will use the PKPs minted through Lit to sign transactions under the hood.
const pkpWallet = new PKPEthersWallet({
pkpPubKey: pkp[pkp.length - 1].publicKey,
rpc: "<standard RPC URL for the chain you are using>", // e.g. https://rpc.ankr.com/eth_goerli
litNodeClient,
authContext: {
getSessionSigsProps: {
chain: 'ethereum',
expiration: new Date(Date.now() + 60_000 * 60).toISOString(),
resourceAbilityRequests: resourceAbilities,
authNeededCallback,
},
},
// controllerSessionSigs: sesionSigs, // (deprecated) If you will be passing sessionSigs directly, do not pass authContext
});
const pkpWallet = new PKPEthersWallet({
pkpPubKey: pkp[pkp.length - 1].publicKey,
litNodeClient,
authContext: {
getSessionSigsProps: {
chain: 'ethereum',
expiration: new Date(Date.now() + 60_000 * 60).toISOString(),
resourceAbilityRequests: resourceAbilities,
authNeededCallback,
},
},
// controllerSessionSigs: sesionSigs, // (deprecated) If you will be passing sessionSigs directly, do not pass authContext
});
await pkpWallet.init();
9. Use the PKP Wallet to sign user operations and send them through Pimlico
You can now use the pkpWallet
as a regular Ethers.js wallet to sign user operations.
To submit a user operation to Pimlico, you can follow the steps to sponsor a user operation with Pimlico's verifying paymaster and/or submit a user operation through Pimlico's bundler. If you would like to integrate Lit with the full flow of generating, signing, and submitting a user operation, you can follow the steps in tutorial 1, replacing the signing step with the PKP wallet and using pkpWallet.address
as the owner address of the smart account.
Modified from Plimlico's tutorial 1, an example of how to use the PKP wallet to sign a user operation is shown below:
const signature = await pkpWallet.signMessage(
ethers.utils.arrayify(await entryPoint.getUserOpHash(userOperation)),
)
userOperation.signature = signature
And an example of how you would generate the initCode
for a SimpleAccount using the PKP wallet is shown below:
const initCode = ethers.utils.hexConcat([
SIMPLE_ACCOUNT_FACTORY_ADDRESS,
simpleAccountFactory.interface.encodeFunctionData("createAccount", [pkpWallet.address, 0]),
])
Next steps
Continue exploring Lit by reading more about programmable signing.
Check out more examples from Pimlico.
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.