Skip to content

Commit a7ab805

Browse files
Merge pull request #76 from LIT-Protocol/andrew/solana-openai
OpenAI Example
2 parents b7585f9 + 3236e2d commit a7ab805

File tree

11 files changed

+5921
-0
lines changed

11 files changed

+5921
-0
lines changed

solana-openai/nodejs/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ETHEREUM_PRIVATE_KEY=
2+
OPENAI_API_KEY=
3+
LIT_PKP_PUBLIC_KEY=

solana-openai/nodejs/.mocharc.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "https://json.schemastore.org/mocharc.json",
3+
"require": "tsx"
4+
}

solana-openai/nodejs/README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Solana + OpenAI Example within a Lit Action
2+
3+
This code example demonstrates how an OpenAI API key can be used to generate a response, which is then signed by a Lit Solana Wrapped Key—all within the confines of Lit's Trusted Execution Environment (TEE).
4+
5+
## Prerequisites
6+
7+
- **An Ethereum private key**
8+
- This private key will be used to:
9+
- Own the PKP we mint. The minted PKP will be used to create new Wrapped Keys.
10+
- In order to pay for this, the corresponding Ethereum account must have Lit Test Tokens. If you do not have any, you can get some from [the faucet](https://chronicle-yellowstone-faucet.getlit.dev/).
11+
- **An OpenAI API key**
12+
- We will use this to make a request to OpenAI with our query to generate a response.
13+
- This example currently uses the GPT-4o-mini model, please enable this model on your API key or change it in the code if needed.
14+
- **Node.js and Yarn**
15+
- Please have these installed before running the example.
16+
17+
## Installation and Setup
18+
19+
1. Clone the repository
20+
2. `cd` into the code example directory: `cd solana-openai/nodejs`
21+
3. Install the dependencies: `yarn`
22+
4. Create and fill in the `.env` file: `cp .env.example .env`
23+
- **Required**:
24+
- `ETHEREUM_PRIVATE_KEY`: This is the Ethereum private key that will be used to mint a PKP if one is not provided
25+
- `OPENAI_API_KEY`: This is the OpenAI API key that will be used to make a request within our Lit Action
26+
- **Optional**:
27+
- `LIT_PKP_PUBLIC_KEY`: You can also provide your own PKP for this example, but please make sure the PKP is owned by the provided Ethereum wallet
28+
5. Test the example: `yarn test`
29+
30+
## Executing the Example
31+
32+
Executing the example can be done with running `yarn test`. This script will bundle our Lit Action with the necessary modules and run our [test file](./test/decryptApiKeyInActionTest.spec.ts).
33+
34+
Here's an overview of how the code example works:
35+
36+
1. Using an imported Ethereum private key, connect the wallet to the Lit RPC endpoint `Chronicle Yellowstone`
37+
2. Connect to the Lit network using the `LitNodeClient` on the `datil-dev` network
38+
3. Connect the `LitContracts` client to the Lit network (`datil-dev` in this case)
39+
4. **If a PKP is not provided in the .env file**: Mint a new PKP.
40+
5. Use your Ethereum wallet to create an AuthMethod.
41+
6. Use the created AuthMethod for authentication in generating session signatures for our PKP.
42+
7. Generate a new Solana Wrapped Key.
43+
8. Retrieve the encrypted private key of our Wrapped Key.
44+
9. Set up Access Control Conditions (ACCs) to encrypt our OpenAI API key. This ensures it is not revealed at any point. The ACCs are the same ones that were used to encrypt the Wrapped Key.
45+
10. Write a simple prompt for the AI to answer.
46+
11. Run the Lit Action, providing our encrypted Solana Wrapped Key and OpenAI API key metadata, as well as our prompt.
47+
48+
### Lit Action Overview
49+
50+
Within the Lit Action:
51+
52+
1. Decrypt the Solana Wrapped Key and OpenAI API key.
53+
2. Remove the salt from the decrypted Solana private key.
54+
3. Send the prompt to OpenAI and receive a response.
55+
4. Generate a Solana keypair from the decrypted private key.
56+
5. Use the Solana keypair to sign the OpenAI response.
57+
6. Ensure the signature is valid.
58+
59+
### Expected Output
60+
61+
After running the test, the end of the output in the console should appear similar to:
62+
63+
```ts
64+
Executed the Lit Action
65+
{
66+
success: true,
67+
signedData: {},
68+
decryptedData: {},
69+
claimData: {},
70+
response: 'Signed message. Is signature valid: true',
71+
logs: 'OpenAI Response: Consider conducting thorough research and assessing market trends before making your decision on DogeCoin.\n' +
72+
'Solana Signature: Uint8Array(64) [\n' +
73+
' 169, 139, 64, 61, 97, 115, 213, 176, 232, 227, 185,\n' +
74+
' 208, 231, 224, 254, 129, 141, 144, 51, 240, 0, 85,\n' +
75+
' 60, 104, 205, 68, 58, 92, 171, 46, 147, 148, 56,\n' +
76+
' 219, 95, 164, 23, 248, 73, 40, 176, 185, 12, 142,\n' +
77+
' 5, 71, 131, 184, 90, 235, 30, 140, 4, 29, 233,\n' +
78+
' 63, 238, 142, 143, 14, 98, 51, 23, 8\n' +
79+
']\n'
80+
}
81+
```
82+
83+
## Specific Files to Reference
84+
85+
- [./src/index.ts](./src/index.ts): Contains the core logic for the example
86+
- [./src/litAction.js](./src/litAction.js): Contains the Lit Action code that will be bundled
87+
- [./src/litAction.bundle.js](./src/litAction.bundle.js): Contains the bundled Lit Action code
88+
- [./src/utils.ts](./src/utils.ts): Contains the code for minting a new PKP and ensuring the required environment variables are set

solana-openai/nodejs/package.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "simple-solana-openai-example",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"type": "module",
7+
"scripts": {
8+
"spec": "npx @dotenvx/dotenvx run -- mocha test/**/*.spec.ts",
9+
"build:lit": "esbuild src/litAction.js --bundle --minify --format=esm --outfile=src/litAction.bundle.js",
10+
"test": "yarn build:lit && yarn spec"
11+
},
12+
"devDependencies": {
13+
"@types/chai": "^4.3.19",
14+
"@types/chai-json-schema": "^1.4.10",
15+
"@types/mocha": "^10.0.8",
16+
"chai": "^5.1.1",
17+
"chai-json-schema": "^1.5.1",
18+
"mocha": "^10.7.3",
19+
"ts-node": "^10.9.2",
20+
"tsc": "^2.0.4",
21+
"tsx": "^4.19.1",
22+
"typescript": "^5.6.2"
23+
},
24+
"dependencies": {
25+
"@dotenvx/dotenvx": "^0.44.1",
26+
"@lit-protocol/auth-helpers": "^6.4.10",
27+
"@lit-protocol/constants": "^6.4.10",
28+
"@lit-protocol/contracts-sdk": "^6.4.10",
29+
"@lit-protocol/lit-auth-client": "^6.11.0",
30+
"@lit-protocol/lit-node-client": "^6.11.0",
31+
"@lit-protocol/wrapped-keys": "^6.11.0",
32+
"@solana/web3.js": "^1.95.4",
33+
"esbuild": "^0.24.0"
34+
}
35+
}

solana-openai/nodejs/src/index.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { LitNodeClient, encryptString } from "@lit-protocol/lit-node-client";
2+
import { LitNetwork, LIT_RPC } from "@lit-protocol/constants";
3+
import {
4+
LitAbility,
5+
LitActionResource,
6+
LitPKPResource,
7+
} from "@lit-protocol/auth-helpers";
8+
import { LitContracts } from "@lit-protocol/contracts-sdk";
9+
import { AccessControlConditions } from "@lit-protocol/types";
10+
import { EthWalletProvider } from "@lit-protocol/lit-auth-client";
11+
import { LIT_NETWORKS_KEYS } from "@lit-protocol/types";
12+
import { api } from "@lit-protocol/wrapped-keys";
13+
import { getEncryptedKey } from "@lit-protocol/wrapped-keys/src/lib/api";
14+
import fs from "node:fs";
15+
import * as ethers from "ethers";
16+
17+
import { getEnv, mintPkp } from "./utils";
18+
const litActionCode = fs.readFileSync("src/litAction.bundle.js", "utf8");
19+
20+
const { generatePrivateKey } = api;
21+
22+
const ETHEREUM_PRIVATE_KEY = getEnv("ETHEREUM_PRIVATE_KEY");
23+
const OPENAI_API_KEY = getEnv("OPENAI_API_KEY");
24+
const LIT_PKP_PUBLIC_KEY = process.env["LIT_PKP_PUBLIC_KEY"];
25+
const LIT_NETWORK = process.env["LIT_NETWORK"] as LIT_NETWORKS_KEYS || LitNetwork.DatilDev;
26+
27+
export const solanaOpenAI = async () => {
28+
let litNodeClient: LitNodeClient;
29+
let pkpInfo: {
30+
tokenId?: string;
31+
publicKey?: string;
32+
ethAddress?: string;
33+
} = {
34+
publicKey: LIT_PKP_PUBLIC_KEY,
35+
};
36+
37+
try {
38+
const ethersWallet = new ethers.Wallet(
39+
ETHEREUM_PRIVATE_KEY,
40+
new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE)
41+
);
42+
43+
console.log("🔄 Connecting to the Lit network...");
44+
litNodeClient = new LitNodeClient({
45+
litNetwork: LIT_NETWORK,
46+
debug: false,
47+
});
48+
await litNodeClient.connect();
49+
console.log("✅ Connected to the Lit network");
50+
51+
console.log("🔄 Connecting LitContracts client to network...");
52+
const litContracts = new LitContracts({
53+
signer: ethersWallet,
54+
network: LIT_NETWORK,
55+
debug: false,
56+
});
57+
await litContracts.connect();
58+
console.log("✅ Connected LitContracts client to network");
59+
60+
if (LIT_PKP_PUBLIC_KEY === undefined || LIT_PKP_PUBLIC_KEY === "") {
61+
console.log("🔄 PKP wasn't provided, minting a new one...");
62+
pkpInfo = (await mintPkp(ethersWallet)) as {
63+
tokenId?: string;
64+
publicKey?: string;
65+
ethAddress?: string;
66+
};
67+
console.log("✅ PKP successfully minted");
68+
console.log(`ℹ️ PKP token ID: ${pkpInfo.tokenId}`);
69+
console.log(`ℹ️ PKP public key: ${pkpInfo.publicKey}`);
70+
console.log(`ℹ️ PKP ETH address: ${pkpInfo.ethAddress}`);
71+
} else {
72+
console.log(`ℹ️ Using provided PKP: ${LIT_PKP_PUBLIC_KEY}`);
73+
pkpInfo = {
74+
publicKey: LIT_PKP_PUBLIC_KEY,
75+
ethAddress: ethers.utils.computeAddress(`0x${LIT_PKP_PUBLIC_KEY}`),
76+
};
77+
}
78+
79+
console.log("🔄 Creating AuthMethod using the ethersSigner...");
80+
const authMethod = await EthWalletProvider.authenticate({
81+
signer: ethersWallet,
82+
litNodeClient,
83+
});
84+
console.log("✅ Finished creating the AuthMethod");
85+
86+
console.log("🔄 Getting the Session Signatures...");
87+
const pkpSessionSigs = await litNodeClient.getPkpSessionSigs({
88+
pkpPublicKey: pkpInfo.publicKey!,
89+
chain: "ethereum",
90+
authMethods: [authMethod],
91+
expiration: new Date(Date.now() + 1000 * 60 * 10).toISOString(), // 10 minutes
92+
resourceAbilityRequests: [
93+
{
94+
resource: new LitActionResource("*"),
95+
ability: LitAbility.LitActionExecution,
96+
},
97+
{
98+
resource: new LitPKPResource("*"),
99+
ability: LitAbility.PKPSigning,
100+
},
101+
],
102+
});
103+
console.log("✅ Generated the Session Signatures");
104+
105+
console.log("🔄 Generating wrapped key...");
106+
const response = await generatePrivateKey({
107+
pkpSessionSigs,
108+
network: "solana",
109+
memo: "This is a Dev Guide code example testing Solana key",
110+
litNodeClient,
111+
});
112+
console.log(`✅ Generated wrapped key with id: ${response.id} and public key: ${response.generatedPublicKey}`);
113+
114+
const {
115+
ciphertext: solanaCipherText,
116+
dataToEncryptHash: solanaDataToEncryptHash,
117+
} = await getEncryptedKey({
118+
pkpSessionSigs,
119+
litNodeClient,
120+
id: response.id,
121+
});
122+
123+
const accessControlConditions: AccessControlConditions = [
124+
{
125+
contractAddress: "",
126+
standardContractType: "",
127+
chain: "ethereum",
128+
method: "",
129+
parameters: [":userAddress"],
130+
returnValueTest: {
131+
comparator: "=",
132+
value: pkpInfo.ethAddress!,
133+
},
134+
},
135+
];
136+
137+
const {
138+
ciphertext: apiKeyCipherText,
139+
dataToEncryptHash: apiKeyDataToEncryptHash,
140+
} = await encryptString(
141+
{
142+
accessControlConditions: accessControlConditions,
143+
dataToEncrypt: OPENAI_API_KEY,
144+
},
145+
litNodeClient
146+
);
147+
148+
const prompt = "Should I buy DogeCoin?";
149+
150+
console.log("🔄 Executing the Lit Action...");
151+
const litActionResponse = await litNodeClient.executeJs({
152+
sessionSigs: pkpSessionSigs,
153+
code: litActionCode,
154+
jsParams: {
155+
accessControlConditions,
156+
solanaCipherText,
157+
solanaDataToEncryptHash,
158+
apiKeyCipherText,
159+
apiKeyDataToEncryptHash,
160+
prompt
161+
},
162+
});
163+
console.log("✅ Executed the Lit Action");
164+
console.log(litActionResponse);
165+
166+
return litActionResponse;
167+
} catch (error) {
168+
console.error(error);
169+
} finally {
170+
litNodeClient!.disconnect();
171+
}
172+
};

solana-openai/nodejs/src/litAction.bundle.js

Lines changed: 163 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)