lit-wrapper-sdk
Version:
A wrapper SDK for Lit Protocol
662 lines (661 loc) • 26 kB
JavaScript
import { LitNodeClient } from "@lit-protocol/lit-node-client";
import { LIT_ABILITY, LIT_RPC, AUTH_METHOD_SCOPE, } from "@lit-protocol/constants";
import { LitActionResource, LitPKPResource } from "@lit-protocol/auth-helpers";
import { LitContracts } from "@lit-protocol/contracts-sdk";
import { EthWalletProvider } from "@lit-protocol/lit-auth-client";
import * as ethers from "ethers";
import bs58 from "bs58";
import { Connection, PublicKey, SystemProgram, Transaction, clusterApiUrl, } from "@solana/web3.js";
import { api } from "@lit-protocol/wrapped-keys";
const { generatePrivateKey, signTransactionWithEncryptedKey, getEncryptedKey } = api;
import { getAssociatedTokenAddress, createAssociatedTokenAccountInstruction, createTransferInstruction, } from "@solana/spl-token";
import { FlagForLitTxn, } from "./types.js";
// @ts-ignore
import { litAction } from "./actions/solana-conditional.js";
import loadSolanaKit from "./fileHandler.js";
class LitWrapper {
constructor(litNetwork) {
this.litNetwork = litNetwork;
this.litNodeClient = new LitNodeClient({
litNetwork: this.litNetwork,
debug: false,
});
this.pkp = null;
this.wk = null;
}
// ------------------------------- AUTH FUNCTIONS -------------------------------
async createPKP(userPrivateKey) {
try {
const ethersWallet = new ethers.Wallet(userPrivateKey, new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE));
const litContracts = new LitContracts({
signer: ethersWallet,
network: this.litNetwork,
debug: false,
});
await litContracts.connect();
const pkp = (await litContracts.pkpNftContractUtils.write.mint())
.pkp;
this.pkp = pkp;
return pkp;
}
catch (error) {
console.error(error);
}
}
async addAuthAddress(userPrivateKey, pkpTokenId, ethAddress) {
const ethersWallet = new ethers.Wallet(userPrivateKey, new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE));
const litContracts = new LitContracts({
signer: ethersWallet,
network: this.litNetwork,
debug: false,
});
await litContracts.connect();
const response = await litContracts.addPermittedAuthMethod({
pkpTokenId: pkpTokenId,
authMethodType: 1,
authMethodId: ethAddress,
authMethodScopes: [AUTH_METHOD_SCOPE.SignAnything],
});
return response;
}
async removeAuthAddress(userPrivateKey, pkpTokenId, ethAddress) {
const ethersWallet = new ethers.Wallet(userPrivateKey, new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE));
const litContracts = new LitContracts({
signer: ethersWallet,
network: this.litNetwork,
debug: false,
});
await litContracts.connect();
const response = await litContracts.pkpPermissionsContract.write.removePermittedAddress(pkpTokenId, ethAddress);
return response;
}
async addPermittedAction({ userPrivateKey, pkpTokenId, litActionCode, pinataAPIKey, }) {
const ipfsCID = await this.uploadViaPinata({
pinataAPIKey,
litActionCode,
});
const ethersWallet = new ethers.Wallet(userPrivateKey, new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE));
const litContracts = new LitContracts({
signer: ethersWallet,
network: this.litNetwork,
debug: false,
});
await litContracts.connect();
const response = await litContracts.addPermittedAction({
pkpTokenId: pkpTokenId,
ipfsId: ipfsCID,
authMethodScopes: [AUTH_METHOD_SCOPE.SignAnything],
});
return { ipfsCID, response };
}
async removePermittedAction({ userPrivateKey, pkpTokenId, ipfsCID, }) {
try {
const ethersWallet = new ethers.Wallet(userPrivateKey, new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE));
const litContracts = new LitContracts({
signer: ethersWallet,
network: this.litNetwork,
debug: false,
});
const bytesIpfsCID = `0x${Buffer.from(bs58.decode(ipfsCID)).toString("hex")}`;
await litContracts.connect();
const response = await litContracts.pkpPermissionsContract.write.removePermittedAction(pkpTokenId, bytesIpfsCID);
return response;
}
catch (error) {
throw new Error("Failed to remove permitted action");
}
}
async checkPermits(pkpTokenId) {
const litContracts = new LitContracts({
network: this.litNetwork,
debug: false,
});
await litContracts.connect();
let permittedActions = await litContracts.pkpPermissionsContract.read.getPermittedActions(pkpTokenId);
let permittedAuthMethods = await litContracts.pkpPermissionsContract.read.getPermittedAuthMethods(pkpTokenId);
let permittedAddresses = await litContracts.pkpPermissionsContract.read.getPermittedAddresses(pkpTokenId);
const results = {
actions: permittedActions,
authMethods: permittedAuthMethods,
addresses: permittedAddresses,
};
console.log(results);
return results;
}
// ------------------------------- UTILITY FUNCTIONS -------------------------------
async createPKPWithLitAction({ userPrivateKey, litActionCode, pinataAPIKey, }) {
await this.createPKP(userPrivateKey);
if (!this.pkp) {
throw new Error("PKP not initialized");
}
const ipfsCID = this.addPermittedAction({
userPrivateKey,
pkpTokenId: this.pkp.tokenId,
litActionCode,
pinataAPIKey,
});
let pkp = this.pkp;
return { pkp, ipfsCID };
}
async createPKPSessionSigs(userPrivateKey, pkpPublicKey) {
try {
if (!this.litNodeClient.ready) {
await this.litNodeClient.connect();
}
const ethersWallet = new ethers.Wallet(userPrivateKey, new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE));
const authMethod = await EthWalletProvider.authenticate({
signer: ethersWallet,
litNodeClient: this.litNodeClient,
});
const pkpSessionSigs = await this.litNodeClient.getPkpSessionSigs({
pkpPublicKey: pkpPublicKey,
chain: "ethereum",
authMethods: [authMethod],
resourceAbilityRequests: [
{
resource: new LitActionResource("*"),
ability: LIT_ABILITY.LitActionExecution,
},
{
resource: new LitPKPResource("*"),
ability: LIT_ABILITY.PKPSigning,
},
],
expiration: new Date(Date.now() + 1000 * 60 * 10).toISOString(), // 10 minutes
});
return pkpSessionSigs;
}
catch (error) {
console.error(error);
}
}
async getSessionSigs(userPrivateKey, pkpPublicKey, type) {
const response = await this.createPKPSessionSigs(userPrivateKey, pkpPublicKey);
if (!response) {
throw new Error("Failed to get session sigs");
}
return response;
}
async executeLitAction({ userPrivateKey, pkpPublicKey, litActionIpfsCid, litActionCode, params, }) {
try {
if (!this.litNodeClient.ready) {
await this.litNodeClient.connect();
}
const result = await this.litNodeClient.executeJs({
sessionSigs: await this.getSessionSigs(userPrivateKey, pkpPublicKey, "pkp"),
ipfsId: litActionIpfsCid,
code: litActionCode,
jsParams: { ...params },
});
return result;
}
catch (error) {
console.error(error);
}
}
async getDecipheringDetails({ userPrivateKey, pkp, wk, }) {
if (pkp) {
this.pkp = pkp;
}
if (wk) {
this.wk = wk;
}
if (!this.pkp) {
throw new Error("PKP not initialized");
}
if (!this.wk) {
throw new Error("WK not initialized");
}
try {
this.litNodeClient.connect();
const { ciphertext: solanaCipherText, dataToEncryptHash: solanaDataToEncryptHash, } = await getEncryptedKey({
pkpSessionSigs: await this.getSessionSigs(userPrivateKey, this.pkp.publicKey, "pkp"),
litNodeClient: this.litNodeClient,
id: wk.id,
});
return {
ciphertext: solanaCipherText,
dataToEncryptHash: solanaDataToEncryptHash,
};
}
catch (error) {
throw new Error("Failed to get deciphering details");
}
finally {
this.litNodeClient?.disconnect();
}
}
async uploadViaPinata({ pinataAPIKey, litActionCode, }) {
const formData = new FormData();
const file = new File([litActionCode], "Action.txt", {
type: "text/plain",
});
const pinataMetadata = JSON.stringify({
name: "EVM-SWAP",
});
const pinataOptions = JSON.stringify({
cidVersion: 0,
});
formData.append("file", file);
formData.append("pinataMetadata", pinataMetadata);
formData.append("pinataOptions", pinataOptions);
const request = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
method: "POST",
headers: {
Authorization: `Bearer ${pinataAPIKey}`,
},
body: formData,
});
const response = await request.json();
return response.IpfsHash;
}
// ------------------------------- SOLANA FUNCTIONS -------------------------------
async createSolanaWK(userPrivateKey) {
try {
if (!this.pkp) {
await this.createPKP(userPrivateKey);
}
if (!this.pkp) {
throw new Error("PKP not initialized");
}
console.log("PKP: ", this.pkp);
if (!this.litNodeClient.ready) {
await this.litNodeClient.connect();
}
const wrappedKeyInfo = await generatePrivateKey({
pkpSessionSigs: await this.getSessionSigs(userPrivateKey, this.pkp.publicKey, "pkp"),
network: "solana",
memo: "This is a test memo",
litNodeClient: this.litNodeClient,
});
if (!wrappedKeyInfo) {
throw new Error("Failed to generate wrapped key");
}
const response = {
pkpInfo: this.pkp,
wkInfo: wrappedKeyInfo,
};
console.log("WK: ", response.wkInfo);
return response;
}
catch (error) {
console.error;
}
finally {
this.litNodeClient?.disconnect();
}
}
async getConditionalLitAction(conditionalLogic) {
return litAction(conditionalLogic);
}
async conditionalSigningOnSolana({ userPrivateKey, litTransaction, broadcastTransaction, conditionalLogic, pkp, wk, params, }) {
if (pkp) {
this.pkp = pkp;
}
if (wk) {
this.wk = wk;
}
if (!this.pkp) {
throw new Error("PKP not initialized");
}
if (!this.wk) {
throw new Error("WK not initialized");
}
try {
this.litNodeClient.connect();
const { ciphertext: solanaCipherText, dataToEncryptHash: solanaDataToEncryptHash, } = await getEncryptedKey({
pkpSessionSigs: await this.getSessionSigs(userPrivateKey, this.pkp.publicKey, "pkp"),
litNodeClient: this.litNodeClient,
id: this.wk.id,
});
const wkAccessControlConditions = {
contractAddress: "",
standardContractType: "",
chain: "ethereum",
method: "",
parameters: [":userAddress"],
returnValueTest: {
comparator: "=",
value: this.pkp.ethAddress,
},
};
if (!this.litNodeClient.ready) {
await this.litNodeClient.connect();
}
const combinedCode = litAction(conditionalLogic);
const result = await this.litNodeClient.executeJs({
sessionSigs: await this.getSessionSigs(userPrivateKey, this.pkp.publicKey, "pkp"),
code: combinedCode,
jsParams: {
pkpAddress: this.pkp.ethAddress,
ciphertext: solanaCipherText,
dataToEncryptHash: solanaDataToEncryptHash,
unsignedTransaction: litTransaction,
broadcast: broadcastTransaction,
accessControlConditions: [wkAccessControlConditions],
...params,
},
});
return result;
// return false;
}
catch (error) {
console.error(error);
}
finally {
this.litNodeClient?.disconnect();
}
}
async executeCustomActionOnSolana({ userPrivateKey, litActionCode, pkp, wk, params, }) {
if (pkp) {
this.pkp = pkp;
}
if (wk) {
this.wk = wk;
}
if (!this.pkp) {
throw new Error("PKP not initialized");
}
if (!this.wk) {
throw new Error("WK not initialized");
}
try {
this.litNodeClient.connect();
const { ciphertext: solanaCipherText, dataToEncryptHash: solanaDataToEncryptHash, } = await getEncryptedKey({
pkpSessionSigs: await this.getSessionSigs(userPrivateKey, this.pkp.publicKey, "pkp"),
litNodeClient: this.litNodeClient,
id: this.wk.id,
});
const wkAccessControlConditions = {
contractAddress: "",
standardContractType: "",
chain: "ethereum",
method: "",
parameters: [":userAddress"],
returnValueTest: {
comparator: "=",
value: this.pkp.ethAddress,
},
};
if (!this.litNodeClient.ready) {
await this.litNodeClient.connect();
}
const result = await this.litNodeClient.executeJs({
sessionSigs: await this.getSessionSigs(userPrivateKey, this.pkp.publicKey, "pkp"),
code: litActionCode,
jsParams: {
pkpAddress: this.pkp.ethAddress,
ciphertext: solanaCipherText,
dataToEncryptHash: solanaDataToEncryptHash,
accessControlConditions: [wkAccessControlConditions],
...params,
},
});
return result;
// return false;
}
catch (error) {
console.error(error);
}
finally {
this.litNodeClient?.disconnect();
}
}
async executeSolanaAgentKit({ userPrivateKey, MESSAGE, RPC_URL, OPENAI_API_KEY, pkp, wk, }) {
const AgentKitAction = await loadSolanaKit();
const params = {
MESSAGE,
RPC_URL,
OPENAI_API_KEY,
};
return this.executeCustomActionOnSolana({
userPrivateKey,
litActionCode: AgentKitAction,
pkp,
wk,
params,
});
}
async sendSolanaWKTxnWithSol({ amount, toAddress, network, broadcastTransaction, userPrivateKey, wk, pkp, }) {
if (pkp) {
this.pkp = pkp;
}
if (wk) {
this.wk = wk;
}
if (!this.pkp) {
throw new Error("PKP not initialized");
}
if (!this.wk) {
throw new Error("WK not initialized");
}
try {
const litTransaction = await this.createSerializedLitTxn({
wk,
toAddress,
amount,
network,
flag: FlagForLitTxn.SOL,
});
if (!this.litNodeClient.ready) {
await this.litNodeClient.connect();
}
if (!litTransaction) {
throw new Error("Failed to create Lit Transaction");
}
const signedTransaction = await signTransactionWithEncryptedKey({
pkpSessionSigs: await this.getSessionSigs(userPrivateKey, this.pkp.publicKey, "pkp"),
network: "solana",
id: this.wk.id,
unsignedTransaction: litTransaction,
broadcast: broadcastTransaction,
litNodeClient: this.litNodeClient,
});
return signedTransaction;
}
catch (error) {
console.error(error);
}
finally {
this.litNodeClient?.disconnect();
}
}
async sendSolanaWKTxnWithCustomToken({ tokenMintAddress, amount, toAddress, network, broadcastTransaction, userPrivateKey, wk, pkp, }) {
if (pkp) {
this.pkp = pkp;
}
if (wk) {
this.wk = wk;
}
if (!this.pkp) {
throw new Error("PKP not initialized");
}
if (!this.wk) {
throw new Error("WK not initialized");
}
try {
const litTransaction = await this.createSerializedLitTxn({
wk: this.wk,
toAddress,
amount,
network,
flag: FlagForLitTxn.CUSTOM,
tokenMintAddress: tokenMintAddress,
});
if (!litTransaction) {
throw new Error("Failed to create Lit Transaction");
}
if (!this.litNodeClient.ready) {
await this.litNodeClient.connect();
}
const signedTransaction = await signTransactionWithEncryptedKey({
pkpSessionSigs: await this.getSessionSigs(userPrivateKey, this.pkp.publicKey, "pkp"),
network: "solana",
id: this.wk.id,
unsignedTransaction: litTransaction,
broadcast: broadcastTransaction,
litNodeClient: this.litNodeClient,
});
return signedTransaction;
}
catch (error) {
console.error(error);
}
finally {
this.litNodeClient?.disconnect();
}
}
async createSerializedLitTxn({ toAddress, amount, network, flag, tokenMintAddress, wk, }) {
if (wk) {
this.wk = wk;
}
if (!this.wk) {
throw new Error("WK not initialized");
}
try {
const generatedSolanaPublicKey = new PublicKey(this.wk.generatedPublicKey);
const receiverPublicKey = new PublicKey(toAddress);
// console.log("Sending from address: ", generatedSolanaPublicKey.toString());
// console.log("Sending to address: ", receiverPublicKey.toString());
const solanaConnection = new Connection(clusterApiUrl(network), "confirmed");
const { blockhash } = await solanaConnection.getLatestBlockhash();
if (flag == FlagForLitTxn.SOL) {
const solanaTransaction = new Transaction();
solanaTransaction.add(SystemProgram.transfer({
fromPubkey: generatedSolanaPublicKey,
toPubkey: receiverPublicKey,
lamports: amount,
}));
solanaTransaction.feePayer = generatedSolanaPublicKey;
solanaTransaction.recentBlockhash = blockhash;
const serializedTransaction = solanaTransaction
.serialize({
requireAllSignatures: false, // should be false as we're not signing the message
verifySignatures: false, // should be false as we're not signing the message
})
.toString("base64");
const litTransaction = {
serializedTransaction,
chain: network,
};
return litTransaction;
}
else if (flag == FlagForLitTxn.CUSTOM) {
if (tokenMintAddress == undefined) {
console.error("Token mint address is required for custom token transfer txn");
return;
}
const tokenAccount = await getAssociatedTokenAddress(new PublicKey(tokenMintAddress), generatedSolanaPublicKey);
const destinationAccount = await getAssociatedTokenAddress(new PublicKey(tokenMintAddress), receiverPublicKey);
const transaction = new Transaction();
// Check if destination token account exists
const destinationAccountInfo = await solanaConnection.getAccountInfo(destinationAccount);
if (!destinationAccountInfo) {
transaction.add(createAssociatedTokenAccountInstruction(generatedSolanaPublicKey, destinationAccount, receiverPublicKey, new PublicKey(tokenMintAddress)));
}
// Add transfer instruction
transaction.add(createTransferInstruction(tokenAccount, destinationAccount, generatedSolanaPublicKey, amount));
transaction.feePayer = generatedSolanaPublicKey;
const { blockhash } = await solanaConnection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
const serializedTransaction = transaction
.serialize({
requireAllSignatures: false,
verifySignatures: false,
})
.toString("base64");
const litTransaction = {
serializedTransaction,
chain: network,
};
return litTransaction;
}
else {
console.error("Invalid flag for Lit Transaction");
}
}
catch (error) {
console.error(error);
}
}
}
class LitTester {
constructor(userPrivateKey, litNetwork) {
this.litNetwork = litNetwork;
this.userPrivateKey = userPrivateKey;
this.pkp = null;
this.initialized = false;
}
static async init(userPrivateKey, litNetwork) {
const instance = new LitTester(userPrivateKey, litNetwork);
await instance.initializePKP();
return instance;
}
async initializePKP() {
if (this.initialized)
return;
try {
const ethersWallet = new ethers.Wallet(this.userPrivateKey, new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE));
const litContracts = new LitContracts({
signer: ethersWallet,
network: this.litNetwork,
debug: false,
});
await litContracts.connect();
const pkp = (await litContracts.pkpNftContractUtils.write.mint())
.pkp;
this.pkp = pkp;
console.log("PKP: ", this.pkp);
this.initialized = true;
}
catch (error) {
console.error(error);
throw error;
}
}
async testLitAction({ litActionCode, params }) {
if (!this.pkp) {
throw new Error("PKP not initialized");
}
const litNodeClient = new LitNodeClient({
litNetwork: this.litNetwork,
debug: false,
});
try {
await litNodeClient.connect();
const ethersWallet = new ethers.Wallet(this.userPrivateKey, new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE));
const authMethod = await EthWalletProvider.authenticate({
signer: ethersWallet,
litNodeClient,
});
const pkpSessionSigs = await litNodeClient.getPkpSessionSigs({
pkpPublicKey: this.pkp.publicKey,
chain: "ethereum",
authMethods: [authMethod],
resourceAbilityRequests: [
{
resource: new LitActionResource("*"),
ability: LIT_ABILITY.LitActionExecution,
},
{
resource: new LitPKPResource("*"),
ability: LIT_ABILITY.PKPSigning,
},
],
});
const result = await litNodeClient.executeJs({
sessionSigs: pkpSessionSigs,
code: litActionCode,
jsParams: { pkpPublicKey: this.pkp.publicKey, ...params },
});
return result;
}
catch (error) {
console.error(error);
}
finally {
litNodeClient?.disconnect();
}
}
}
export { LitWrapper, LitTester, FlagForLitTxn };