From 39fd6d2a389eebfae6ba011e8230d4491618081f Mon Sep 17 00:00:00 2001 From: Anson Date: Thu, 27 Feb 2025 02:19:38 +0000 Subject: [PATCH] feat(wip): add la orbis sdk --- global.d.ts | 11 +- la-utils/la-account/getEthPrivateKey.ts | 85 ++++++++ la-utils/la-db-providers/useorbis/la-api.ts | 199 ++++++++++++++++++ .../useorbis/src/OrbisDBProvider.ts | 108 ++++++++++ .../la-db-providers/useorbis/src/handleDB.ts | 37 ++++ .../la-db-providers/useorbis/src/types.ts | 21 ++ la-utils/la-utils/la-transformer.ts | 5 + my-lit-action/lit-action.ts | 99 +++++---- package.json | 3 + postinstall.config.ts | 24 +++ scripts/postbuild.ts | 17 +- 11 files changed, 553 insertions(+), 56 deletions(-) create mode 100644 la-utils/la-account/getEthPrivateKey.ts create mode 100644 la-utils/la-db-providers/useorbis/la-api.ts create mode 100644 la-utils/la-db-providers/useorbis/src/OrbisDBProvider.ts create mode 100644 la-utils/la-db-providers/useorbis/src/handleDB.ts create mode 100644 la-utils/la-db-providers/useorbis/src/types.ts create mode 100644 la-utils/la-utils/la-transformer.ts create mode 100644 postinstall.config.ts diff --git a/global.d.ts b/global.d.ts index e6a8865..b2ada59 100644 --- a/global.d.ts +++ b/global.d.ts @@ -387,17 +387,16 @@ declare namespace Lit { /** * Encrypt data - * @param {Object} params - * @param {string} params.accessControlConditions The access control conditions - * @param {string} params.to_encrypt The message to encrypt - * @returns {Promise<{ciphertext: string; dataToEncryptHash: string}>} Contains two items: The ciphertext result after encryption, named "ciphertext" and the dataToEncryptHash, named "dataToEncryptHash" + * @param {Object[]} accessControlConditions The access control conditions + * @param {Uint8Array} to_encrypt The message to encrypt + * @returns { {ciphertext: string, dataToEncryptHash: string} } Contains two items: The ciphertext result after encryption, named "ciphertext" and the dataToEncryptHash, named "dataToEncryptHash" */ function encrypt({ accessControlConditions, to_encrypt, }: { - accessControlConditions: string; - to_encrypt: string; + accessControlConditions: Object[]; + to_encrypt: Uint8Array; }): Promise<{ ciphertext: string; dataToEncryptHash: string; diff --git a/la-utils/la-account/getEthPrivateKey.ts b/la-utils/la-account/getEthPrivateKey.ts new file mode 100644 index 0000000..7115876 --- /dev/null +++ b/la-utils/la-account/getEthPrivateKey.ts @@ -0,0 +1,85 @@ +/** + * Generates an encrypted Ethereum private key that can only be accessed by a specific Ethereum address. + * + * This function: + * 1. Creates a random Ethereum wallet + * 2. Sets up access control conditions based on the provided Ethereum address + * 3. Encrypts the private key using Lit Protocol's encryption + * + * The encryption ensures that only the owner of the specified Ethereum address + * can decrypt and access the private key, providing a secure key management solution. + * + * @param accAddress - The Ethereum address that will have permission to decrypt the private key + * @returns An object containing: + * - publicData: Contains the encrypted data (ciphertext), hash, public key, and access conditions + * - privateData: Contains the unencrypted private key (for initial storage or use) + * + * @example + * const result = await genEncryptedPrivateKey("0x1234...abcd"); + * // Store publicData on chain or in public storage + * // Securely handle privateData or discard if not needed + * + { + publicData: { + ciphertext: "sRfc9+j3x/ln4/jmYDihjKKxWfixmns6UaFxuZXDV3ivWZt771VEgsjJSYoKB+s708FIb5alHKIeU7D2fc+zZ/z3pQijhnVg8uWlUrbnGBpHyhjkRXozifZKrajX8jpZBRN31VtTocUFyQvR7TlHHuXI6ojaiKxbYP9Lpuc+cSllTPHmJZhiA+W3atQSVa8ly61wtQH0G10C", + dataToEncryptHash: "1c3d3ffed83ad057d51360e771f56e52e70d86363a159595d1e601a2cb1c5f1d", + keyType: "K256", + publicKey: "0x04068c15f31c625a3ca7c8accc9f7d4290dcd2092e44839828d76d3bd69df5f49dc645ad42beea40653defc6ed5459178428f668f1c0ea832f19cab068d4cb368d", + accs: [] + }, + privateData: { + privateKey: "0x..." + } +} + */ +export async function genEncryptedPrivateKey(accAddress: string): Promise<{ + publicData: { + ciphertext: string; + dataToEncryptHash: string; + keyType: "K256"; + publicKey: `0x${string}`; + accs: any[]; + }; + privateData: { + privateKey: string; + }; +}> { + const wallet = ethers.Wallet.createRandom(); + const keypair = { + privateKey: wallet.privateKey.toString(), + publicKey: wallet.publicKey, + }; + + const accs = [ + { + contractAddress: "", + standardContractType: "", + chain: "ethereum", + method: "", + parameters: [":userAddress"], + returnValueTest: { + comparator: "=", + value: accAddress, + }, + }, + ]; + + // -- encrypt the keypair + const { ciphertext, dataToEncryptHash } = await Lit.Actions.encrypt({ + accessControlConditions: accs, + to_encrypt: new TextEncoder().encode(`lit_${keypair.privateKey}`), + }); + + return { + publicData: { + ciphertext, + dataToEncryptHash, + keyType: "K256", + publicKey: keypair.publicKey as `0x${string}`, + accs, + }, + privateData: { + privateKey: keypair.privateKey, + }, + }; +} diff --git a/la-utils/la-db-providers/useorbis/la-api.ts b/la-utils/la-db-providers/useorbis/la-api.ts new file mode 100644 index 0000000..116473b --- /dev/null +++ b/la-utils/la-db-providers/useorbis/la-api.ts @@ -0,0 +1,199 @@ +import { handleDB } from "./src/handleDB"; +import { genEncryptedPrivateKey } from "../../la-account/getEthPrivateKey"; +import { hexPrefixed } from "../../la-utils/la-transformer"; + +/** + * API constants and configuration + */ +const LIT_ACTION_NAME = "db-bro"; +const DEFAULT_MESSAGE = "HAKUNA MATATA"; + +/** + * ========== Types ========== + */ +/** + * Interface for action parameters + */ +interface BaseParams { + action: "register" | "read" | "use"; + pkpPublicKey: string; +} + +interface UseParams extends BaseParams { + action: "use"; + address: string; + customMessage?: string; +} + +/** + * Registers a new encrypted private key in the database. + * + * This function: + * 1. Computes the PKP owner address from the public key + * 2. Generates an encrypted private key with access control set to the PKP address + * 3. Stores the metadata in OrbisDB + * + * @param pkpPublicKey - The public key of the PKP that will own the encrypted key + * @returns Object containing the database ID, public data, and owner address + */ +async function register(pkpPublicKey: string) { + pkpPublicKey = hexPrefixed(pkpPublicKey); + + // The PKP owns the encrypted private key + const privateKeyOwnerAddress = ethers.utils.computeAddress(pkpPublicKey); + + // The PKP address is set as the access control condition + const { publicData, privateData } = await genEncryptedPrivateKey( + privateKeyOwnerAddress + ); + + // We then write to the OrbisDB table in the columns 'owner' and 'metadata'. + const { id } = await handleDB({ + privateKey: privateData.privateKey, + action: "write", + data: { + ownerAddress: privateKeyOwnerAddress, + metadata: JSON.stringify(publicData), + }, + }); + + return { + id, + publicData, + ownerAddress: privateKeyOwnerAddress, + }; +} + +/** + * Retrieves all stored metadata for the PKP. + * + * @param pkpPublicKey - The public key of the PKP + * @returns Array of metadata objects from the database + */ +async function read(pkpPublicKey: string) { + pkpPublicKey = hexPrefixed(pkpPublicKey); + const privateKeyOwnerAddress = ethers.utils.computeAddress(pkpPublicKey); + const wallet = ethers.Wallet.createRandom(); + + const res = await handleDB({ + privateKey: wallet.privateKey.toString(), + action: "read", + data: { + ownerAddress: privateKeyOwnerAddress, + }, + }); + + const allMetadata = res.rows.map((row) => { + const metadata = JSON.parse(row.metadata); + return { + ...metadata, + address: ethers.utils.computeAddress(metadata.publicKey), + }; + }); + + return allMetadata; +} + +/** + * Uses a stored private key to sign a message. + * + * This function: + * 1. Retrieves the metadata for the PKP + * 2. Finds the specified address in the metadata + * 3. Decrypts the private key using Lit Protocol + * 4. Signs a message with the decrypted key + * + * @param params - Parameters including pkpPublicKey, address to use, and optional custom message + * @returns Object containing the address, signature, and signed message + */ +async function use(params: UseParams) { + params.pkpPublicKey = hexPrefixed(params.pkpPublicKey); + const allMetadata = await read(params.pkpPublicKey); + const selectedMetadata = allMetadata.find( + (metadata) => metadata.address === params.address + ); + + if (!selectedMetadata) { + throw new Error(`No metadata found for address ${params.address}`); + } + + const decryptRes = await Lit.Actions.decryptAndCombine({ + accessControlConditions: selectedMetadata.accs, + ciphertext: selectedMetadata.ciphertext, + dataToEncryptHash: selectedMetadata.dataToEncryptHash, + chain: "ethereum", + authSig: null as unknown as string, // <-- Signed by the PKP on Lit Action, that's why is null. + }); + + const privateKey = decryptRes.replace("lit_", ""); + const wallet = new ethers.Wallet(privateKey); + const signedMessage = params.customMessage || DEFAULT_MESSAGE; + const signature = await wallet.signMessage(signedMessage); + + return { + address: wallet.address, + signature, + signedMessage, + }; +} + +/** + * Main API function that handles all operations (register, read, use) through Lit Actions. + * + * This function determines which action to perform based on the params.action field, + * delegates to the appropriate specialized function, and wraps everything in a Lit.Actions.runOnce call. + * + * @param params - Parameters for the action including action type and PKP public key + * @returns Result of the action + */ +async function runOrbisAction(params: BaseParams | UseParams) { + const res = await Lit.Actions.runOnce( + { + waitForResponse: true, + name: LIT_ACTION_NAME, + }, + async () => { + try { + let result; + + switch (params.action) { + case "register": + result = await register(params.pkpPublicKey); + break; + + case "read": + result = await read(params.pkpPublicKey); + break; + + case "use": + result = await use(params as UseParams); + break; + + default: + throw new Error(`Unknown action: ${(params as any).action}`); + } + + return JSON.stringify({ + success: true, + message: result, + }); + } catch (error: unknown) { + return JSON.stringify({ + success: false, + error: + error instanceof Error ? error.message : "Unknown error occurred", + }); + } + } + ); + + return JSON.parse(res); +} + +export const orbisAPI = { + entries: (pubkey: string) => + runOrbisAction({ action: "read", pkpPublicKey: pubkey }), + register: (pubkey: string) => + runOrbisAction({ action: "register", pkpPublicKey: pubkey }), + use: (params: UseParams) => runOrbisAction(params), +}; diff --git a/la-utils/la-db-providers/useorbis/src/OrbisDBProvider.ts b/la-utils/la-db-providers/useorbis/src/OrbisDBProvider.ts new file mode 100644 index 0000000..e5bc737 --- /dev/null +++ b/la-utils/la-db-providers/useorbis/src/OrbisDBProvider.ts @@ -0,0 +1,108 @@ +import { + type CeramicDocument, + type IEVMProvider, + OrbisDB, +} from "@useorbis/db-sdk"; +import { OrbisEVMAuth } from "@useorbis/db-sdk/auth"; +import type { KeyManagementRequest } from "./types"; + +export class OrbisDBProvider { + private _CERAMIC_GATEWAY = + "https://ceramic-orbisdb-mainnet-direct.hirenodes.io/"; + private _ORBIS_GATEWAY = "https://studio.useorbis.com"; + + /** + * Charles: will transform it into an API key soon (24 Oct, 2024) + * 8ball: + * - Data can be written by anyone, read by anyone, modified only by the original author. + * - EnvIDs will be migrated to UUIDv7 at some point in the future, for convenience sake. + */ + private _ORBIS_ENV = `did:pkh:eip155:1:0x3b5dd260598b7579a0b015a1f3bbf322adc499a1`; + + private _CONTEXT = { + Bulkie: "kjzl6kcym7w8y924czxug1jh44yznwt8zp3bgyffhw9mklmtplj0ain6xv0z568", + } as const; + + private _TABLE = { + key_management: + "kjzl6hvfrbw6ca1r1v0zm8y57yn2aawpuj4qvuyljchbgzk1xugplcbjuibr4qt", + } as const; + + private _db: OrbisDB; + private _privateKey: string; + private _wallet: InstanceType; + + constructor(privateKey: string) { + this._privateKey = privateKey; + this._wallet = new ethers.Wallet(this._privateKey); + + this._db = new OrbisDB({ + ceramic: { + gateway: this._CERAMIC_GATEWAY, + }, + nodes: [ + { + gateway: this._ORBIS_GATEWAY, + env: this._ORBIS_ENV, + }, + ], + }); + } + + async connect() { + const auth = new OrbisEVMAuth(this._wallet as unknown as IEVMProvider); + + await this._db.connectUser({ auth, saveSession: false }); + + const connected = await this._db.isUserConnected(); + + if (!connected) { + throw new Error("User not connected"); + } + } + + async write(params: KeyManagementRequest): Promise { + const writeRes = await this._db + .insert(this._TABLE.key_management) + .value({ + owner: params.ownerAddress, + metadata: params.metadata, + }) + .context(this._CONTEXT.Bulkie) + .run(); + + return writeRes; + } + + async update( + docId: string, + params: KeyManagementRequest + ): Promise { + const prepare = this._db.update(docId).replace({ + owner: params.ownerAddress, + metadata: params.metadata, + }); + + const res = await prepare.run(); + + if (!res) { + throw new Error("❌ Update failed"); + } + + return res; + } + + async read(ownerAddress: string): Promise<{ + columns: Array; + rows: Record[]; + }> { + const prepare = this._db.select().from(this._TABLE.key_management).where({ + // controller: this._ORBIS_ENV, + owner: ownerAddress, + }); + + const res = await prepare.run(); + + return res; + } +} diff --git a/la-utils/la-db-providers/useorbis/src/handleDB.ts b/la-utils/la-db-providers/useorbis/src/handleDB.ts new file mode 100644 index 0000000..9c96136 --- /dev/null +++ b/la-utils/la-db-providers/useorbis/src/handleDB.ts @@ -0,0 +1,37 @@ +import { OrbisDBProvider } from "./OrbisDBProvider"; +import type { + DBOperation, + DBResponseType, + KeyManagementRequest, +} from "./types"; + +export async function handleDB({ + privateKey, + action, + data, + docId, +}: { + privateKey: `0x${string}` | string; + action: T; + data: KeyManagementRequest; + docId?: string; +}): Promise> { + const db = new OrbisDBProvider(privateKey); + await db.connect(); + + try { + if (action === "write") { + return (await db.write(data)) as DBResponseType; + } else if (action === "update") { + if (docId === undefined) { + throw new Error("❌ docId is required for update action"); + } + return (await db.update(docId, data)) as DBResponseType; + } else { + return (await db.read(data.ownerAddress)) as DBResponseType; + } + } catch (error) { + console.error("❌ Error in handleDB:", error); + throw error; + } +} diff --git a/la-utils/la-db-providers/useorbis/src/types.ts b/la-utils/la-db-providers/useorbis/src/types.ts new file mode 100644 index 0000000..ca9fe27 --- /dev/null +++ b/la-utils/la-db-providers/useorbis/src/types.ts @@ -0,0 +1,21 @@ +import { type CeramicDocument } from "@useorbis/db-sdk"; +export type KeyManagementRequest = { ownerAddress: string; metadata?: string }; + +export type WriteResponse = CeramicDocument; + +export type UpdateResponse = CeramicDocument; + +export type ReadResponse = { + columns: Array; + rows: Record[]; +}; + +export type DBOperation = "write" | "update" | "read"; + +export type DBResponseType = T extends "write" + ? WriteResponse + : T extends "update" + ? UpdateResponse + : T extends "read" + ? ReadResponse + : never; diff --git a/la-utils/la-utils/la-transformer.ts b/la-utils/la-utils/la-transformer.ts new file mode 100644 index 0000000..56efa1b --- /dev/null +++ b/la-utils/la-utils/la-transformer.ts @@ -0,0 +1,5 @@ +import { type Hex } from "viem"; + +export function hexPrefixed(str: string): Hex { + return str.startsWith("0x") ? (str as Hex) : (`0x${str}` as Hex); +} diff --git a/my-lit-action/lit-action.ts b/my-lit-action/lit-action.ts index b512184..3624311 100644 --- a/my-lit-action/lit-action.ts +++ b/my-lit-action/lit-action.ts @@ -4,7 +4,7 @@ import { contractCall } from "../la-utils/la-transactions/handlers/contractCall" import { nativeSend } from "../la-utils/la-transactions/handlers/nativeSend"; import { contractExample } from "./contract-example"; import { composeTxUrl } from "./utils"; - +import { orbisAPI } from "../la-utils/la-db-providers/useorbis/la-api"; // Define your jsParams here. It's sending from ./my-app/main.ts declare global { var params: { @@ -15,46 +15,59 @@ declare global { (async () => { console.log("👋 Hello via Lit Action!"); - // Access your jsParams here - console.log("PKP Public Key:", params.pkpPublicKey); - - // using a helper function - const pkpEthAddress = toEthAddress(params.pkpPublicKey); - console.log("PKP ETH Address:", pkpEthAddress); - - // Get the provider - const provider = await getYellowstoneProvider(); - - // Example 1: Send transaction using the nativeSend handler, which is a wrapper around the primitive functions - const txHash = await nativeSend({ - provider, - pkpPublicKey: params.pkpPublicKey, - to: pkpEthAddress, - amount: "0.0001", - }); - - // Example 2: Call a contract function - const txHash2 = await contractCall({ - provider, - pkpPublicKey: params.pkpPublicKey, - callerAddress: pkpEthAddress, - abi: [contractExample.methods.mintNextAndAddAuthMethods], - contractAddress: contractExample.address, - functionName: "mintNextAndAddAuthMethods", - args: [ - 2, - [2], - ["0x170d13600caea2933912f39a0334eca3d22e472be203f937c4bad0213d92ed71"], - ["0x0000000000000000000000000000000000000000000000000000000000000000"], - [[1]], - true, - true, - ], - overrides: { - value: 1n, - }, - }); - - console.log(`🎉 [nativeSend] Transaction sent: ${composeTxUrl(txHash)}`); - console.log(`🎉 [contractCall] Transaction sent: ${composeTxUrl(txHash2)}`); + const orbisEntries = await orbisAPI.entries(params.pkpPublicKey); + console.log("👋 orbisEntries:", orbisEntries); + + // const useRes = await orbisAPI.use({ + // action: "use" + // }) + + // const registerRes = await runOrbisAction({ + // operation: "register", + // pkpPublicKey: params.pkpPublicKey, + // }); + // console.log("👋 registerRes:", registerRes); + + // // Access your jsParams here + // console.log("PKP Public Key:", params.pkpPublicKey); + + // // using a helper function + // const pkpEthAddress = toEthAddress(params.pkpPublicKey); + // console.log("PKP ETH Address:", pkpEthAddress); + + // // Get the provider + // const provider = await getYellowstoneProvider(); + + // // Example 1: Send transaction using the nativeSend handler, which is a wrapper around the primitive functions + // const txHash = await nativeSend({ + // provider, + // pkpPublicKey: params.pkpPublicKey, + // to: pkpEthAddress, + // amount: "0.0001", + // }); + + // // Example 2: Call a contract function + // const txHash2 = await contractCall({ + // provider, + // pkpPublicKey: params.pkpPublicKey, + // callerAddress: pkpEthAddress, + // abi: [contractExample.methods.mintNextAndAddAuthMethods], + // contractAddress: contractExample.address, + // functionName: "mintNextAndAddAuthMethods", + // args: [ + // 2, + // [2], + // ["0x170d13600caea2933912f39a0334eca3d22e472be203f937c4bad0213d92ed71"], + // ["0x0000000000000000000000000000000000000000000000000000000000000000"], + // [[1]], + // true, + // true, + // ], + // overrides: { + // value: 1n, + // }, + // }); + + // console.log(`🎉 [nativeSend] Transaction sent: ${composeTxUrl(txHash)}`); + // console.log(`🎉 [contractCall] Transaction sent: ${composeTxUrl(txHash2)}`); })(); diff --git a/package.json b/package.json index 9d51faa..dd1f77a 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ }, "type": "module", "scripts": { + "postinstall": "bun run postinstall.config.ts", "cli": "bun run scripts/cli.ts", "start": "bun run build && bun run my-app/main.ts", "watch": "chokidar './la-utils/**' './my-lit-action/**' './my-app/**' -c 'bun run build && bun run my-app/main.ts'", @@ -25,12 +26,14 @@ "@pinata/sdk": "2.1.0", "@t3-oss/env-core": "^0.12.0", "@types/inquirer": "^9.0.7", + "@useorbis/db-sdk": "^0.0.60-alpha", "chokidar-cli": "^3.0.0", "degit": "^2.8.4", "ethers": "5.7.2", "inquirer": "^12.4.2", "json-with-bigint": "^2.4.2", "pino-caller": "^3.4.0", + "viem": "^2.23.5", "zod": "^3.24.2" } } diff --git a/postinstall.config.ts b/postinstall.config.ts new file mode 100644 index 0000000..cdc490b --- /dev/null +++ b/postinstall.config.ts @@ -0,0 +1,24 @@ +import fs from 'fs'; +import path from 'path'; + +function disableOrbisSchemaValidation() { + const filePath = path.join(__dirname, './node_modules/@useorbis/db-sdk/dist/querybuilder/index.js'); + try { + const fileContent = fs.readFileSync(filePath, 'utf8'); + + // Find the line with ajv.compile and replace it + const modifiedContent = fileContent.replace( + /validate: ajv\.compile\(content\.schema\),/g, + '// Temporarily disabled "validate: ajv.compile(content.schema)" due to schema validation issues' + ); + + // Write the modified content back to the file + fs.writeFileSync(filePath, modifiedContent, 'utf8'); + + console.log(`✅ Successfully modified file at: ${path.resolve(filePath)}`); + } catch (error) { + console.log(`❌ Error modifying file: ${error.message}`); + } +} + +disableOrbisSchemaValidation(); diff --git a/scripts/postbuild.ts b/scripts/postbuild.ts index 7a78291..ed58c80 100644 --- a/scripts/postbuild.ts +++ b/scripts/postbuild.ts @@ -1,6 +1,6 @@ /** * Postbuild script to process the lit-action.js file - * Purpose: Reads the compiled lit-action.js, escapes backticks, and wraps it in a module export + * Purpose: Reads the compiled lit-action.js, removes Ajv code, and converts it to a string export * Usage: Run after build process to prepare the lit-action code for distribution */ @@ -8,9 +8,12 @@ import fs from "fs"; console.log("- postbuilding..."); const actionCode = fs.readFileSync("./dist/lit-action.js", "utf-8"); -// Escape both backticks and template literal expressions -const escapedActionCode = actionCode - .replace(/`/g, "\\`") - .replace(/\${/g, "\\${"); -const code = `export const litActionCodeString = \`${escapedActionCode}\`;`; -fs.writeFileSync("./dist/lit-action.js", code); + +// Create a JavaScript string literal with the code properly escaped +// using JSON.stringify to handle all escaping correctly +const codeAsString = JSON.stringify(actionCode); + +// Create a regular string concatenation without template literals +const outputCode = "export const litActionCodeString = " + codeAsString + ";"; + +fs.writeFileSync("./dist/lit-action.js", outputCode);