diff --git a/packages/lit-agent-signer b/packages/lit-agent-signer deleted file mode 160000 index 07ddd925..00000000 --- a/packages/lit-agent-signer +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 07ddd925a395326b6a5d2af1ac037a74d5cd0ebd diff --git a/packages/lit-agent-signer/.env.example b/packages/lit-agent-signer/.env.example new file mode 100644 index 00000000..42b7e534 --- /dev/null +++ b/packages/lit-agent-signer/.env.example @@ -0,0 +1 @@ +LIT_AUTH_PRIVATE_KEY= \ No newline at end of file diff --git a/packages/lit-agent-signer/.github/workflows/ci.yml b/packages/lit-agent-signer/.github/workflows/ci.yml new file mode 100644 index 00000000..5085b9fc --- /dev/null +++ b/packages/lit-agent-signer/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '18.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Run tests + run: npm test + env: + LIT_AUTH_PRIVATE_KEY: ${{ secrets.LIT_AUTH_PRIVATE_KEY }} diff --git a/packages/lit-agent-signer/.gitignore b/packages/lit-agent-signer/.gitignore new file mode 100644 index 00000000..f64f9fe0 --- /dev/null +++ b/packages/lit-agent-signer/.gitignore @@ -0,0 +1,27 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# IDE files +.idea/ +.vscode/ +*.sublime-* + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment variables +.env +.env.local +.env.*.local + +# OS files +.DS_Store +Thumbs.db + +lit-session-storage \ No newline at end of file diff --git a/packages/lit-agent-signer/.prettierignore b/packages/lit-agent-signer/.prettierignore new file mode 100644 index 00000000..d6dfb212 --- /dev/null +++ b/packages/lit-agent-signer/.prettierignore @@ -0,0 +1,48 @@ +# Dependencies +node_modules +.pnp +.pnp.js + +# Build outputs +dist +build +coverage +lib +.next +out + +# Cache and logs +.cache +.npm +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment and config files +.env* +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Editor directories and files +.idea +.vscode +*.swp +*.swo + +# System files +.DS_Store +Thumbs.db + +# Package files +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Generated files +*.min.js +*.map + +lit-session-storage \ No newline at end of file diff --git a/packages/lit-agent-signer/.prettierrc b/packages/lit-agent-signer/.prettierrc new file mode 100644 index 00000000..1962ea2e --- /dev/null +++ b/packages/lit-agent-signer/.prettierrc @@ -0,0 +1,21 @@ +{ + "arrowParens": "always", + "bracketSpacing": true, + "endOfLine": "lf", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "singleAttributePerLine": false, + "bracketSameLine": false, + "jsxBracketSameLine": false, + "jsxSingleQuote": false, + "printWidth": 80, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false, + "vueIndentScriptAndStyle": false +} diff --git a/packages/lit-agent-signer/LICENSE b/packages/lit-agent-signer/LICENSE new file mode 100644 index 00000000..9c29fb8e --- /dev/null +++ b/packages/lit-agent-signer/LICENSE @@ -0,0 +1,7 @@ +Copyright 2024 WorkGraph, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/lit-agent-signer/README.md b/packages/lit-agent-signer/README.md new file mode 100644 index 00000000..b9b7b9d9 --- /dev/null +++ b/packages/lit-agent-signer/README.md @@ -0,0 +1,119 @@ +# Lit Serverside Signer SDK + +A lightweight SDK for signing transactions and messages with Lit Protocol. This SDK simplifies the process of creating wallets and signing transactions using Lit's Programmable Key Pairs (PKPs). + +This SDK is a wrapper around our [full-featured SDK](https://github.com/lit-protocol/js-sdk) which also supports client-side functionality, decryption, and more. +functionality, decryption, and more. + +## Features + +- 🔑 Easy wallet creation and management +- ✍️ Transaction and message signing +- 🔒 Secure key management via [Lit Protocol](https://litprotocol.com) +- 🚀 Simple, serverside-focused API +- ⚡ Lightweight and efficient + +## Installation + +```bash +npm install @lit-protocol/lit-agent-signer +# or +yarn add @lit-protocol/lit-agent-signer +``` + +## Quick Start + +```typescript +import { LitClient } from '@lit-protocol/lit-agent-signer'; + +// Initialize the client with your Lit auth key +const client = await LitClient.create(process.env.LIT_AUTH_PRIVATE_KEY); + +// Create a new wallet +const { pkp } = await client.createWallet(); +console.log('Wallet created:', pkp); + +// Sign a transaction or message +const signedMessage = await client.sign({ + toSign: '0x8111e78458fec7fb123fdfe3c559a1f7ae33bf21bf81d1bad589e9422c648cbd', +}); +console.log('Message signed:', signedMessage); +``` + +## Usage Guide + +### Initialization + +First, initialize the client with your Lit authentication key: + +```typescript +const client = await LitClient.create(authKey); +``` + +### Wallet Management + +Create a new wallet: + +```typescript +const { pkp } = await client.createWallet(); +``` + +Get existing wallet: + +```typescript +const pkp = client.getPkp(); +``` + +### Signing + +Sign a message or transaction: + +```typescript +const signedMessage = await client.sign({ + toSign: '0x8111e78458fec7fb123fdfe3c559a1f7ae33bf21bf81d1bad589e9422c648cbd', +}); +``` + +### Execute JavaScript Code + +You can also execute JavaScript code using Lit Protocol: + +```typescript +const result = await client.executeJs({ + code: ` + Lit.Actions.setResponse({ response: message + " - processed by Lit Protocol" }); + `, + jsParams: { + message: 'Hello', + }, +}); +``` + +### Cleanup + +When you're done, disconnect the client: + +```typescript +await client.disconnect(); +``` + +## Environment Variables + +Make sure to set up the following environment variable: + +- `LIT_AUTH_PRIVATE_KEY`: Your Lit Protocol authentication key + +## Examples + +Check out the `examples` directory for more detailed examples: + +- `basic-usage.ts`: Complete example showing all main features +- `minimal-signing.ts`: Minimal example focused on transaction signing + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +MIT License diff --git a/packages/lit-agent-signer/eslint.config.mjs b/packages/lit-agent-signer/eslint.config.mjs new file mode 100644 index 00000000..ec99ac95 --- /dev/null +++ b/packages/lit-agent-signer/eslint.config.mjs @@ -0,0 +1,63 @@ +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import globals from 'globals'; +import tsParser from '@typescript-eslint/parser'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import js from '@eslint/js'; +import { FlatCompat } from '@eslint/eslintrc'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: ['**/dist/', '**/node_modules/', '**/*.js', '**/*.jsx'], + }, + ...compat.extends( + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking' + ), + { + plugins: { + '@typescript-eslint': typescriptEslint, + }, + + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + + parser: tsParser, + ecmaVersion: 2020, + sourceType: 'module', + + parserOptions: { + project: ['./tsconfig.json'], + }, + }, + + rules: { + '@typescript-eslint/no-explicit-any': 'warn', + + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-assignment': 'warn', + '@typescript-eslint/no-unsafe-member-access': 'warn', + '@typescript-eslint/no-unsafe-return': 'warn', + }, + }, +]; diff --git a/packages/lit-agent-signer/examples/README.md b/packages/lit-agent-signer/examples/README.md new file mode 100644 index 00000000..3019fa1b --- /dev/null +++ b/packages/lit-agent-signer/examples/README.md @@ -0,0 +1,90 @@ +# Lit Protocol SDK Examples + +This directory contains examples demonstrating how to use the Lit Protocol SDK. + +## Available Examples + +### 1. Basic Usage Example (`basic-usage.ts`) + +The `basic-usage.ts` file demonstrates the core functionality of the SDK: + +- Initializing the client +- Creating a wallet (PKP) +- Signing messages +- Executing JavaScript code +- Proper cleanup + +### 2. Minimal Signing Example (`minimal-signing.ts`) + +The `minimal-signing.ts` file shows a streamlined example focused on: + +- Quick client initialization +- Wallet creation +- Transaction signing + +## Prerequisites + +1. Create a `.env` file in the root directory with your authentication key: + +```env +LIT_AUTH_PRIVATE_KEY=your_auth_key_here +``` + +2. Install dependencies: + +```bash +npm install +# or +yarn +``` + +## Running the Examples + +To run the basic usage example: + +```bash +npm run example +# or +yarn example +``` + +To run the minimal signing example: + +```bash +npm run minimalSigningExample +# or +yarn minimalSigningExample +``` + +## Example Output + +When running the basic usage example, you should see output similar to: + +``` +🚀 Initializing Lit client... +✓ Client initialized, checking if ready... +Client ready: true +Creating new wallet... +✓ Wallet created: { ... } +✓ Message signed: { ... } +✓ JS execution result: { ... } +✓ Client disconnected +``` + +When running the minimal signing example, you should see: + +``` +✓ Wallet created: { ... } +✓ Transaction signed: { ... } +``` + +## Troubleshooting + +If you encounter any issues: + +1. Make sure your `.env` file is properly configured +2. Verify that you have installed all dependencies +3. Ensure you're using a compatible Node.js version +4. Check that your authentication key is valid + +For more detailed information, refer to the main [README.md](../README.md) in the root directory. diff --git a/packages/lit-agent-signer/examples/basic-usage.ts b/packages/lit-agent-signer/examples/basic-usage.ts new file mode 100644 index 00000000..be66d59f --- /dev/null +++ b/packages/lit-agent-signer/examples/basic-usage.ts @@ -0,0 +1,66 @@ +import { LitClient } from '../dist/index'; +import * as dotenv from 'dotenv'; +import { LIT_NETWORK } from '@lit-protocol/constants'; + +// Load environment variables +dotenv.config(); + +async function main() { + try { + // Initialize the client with your private key + const authKey = process.env.LIT_AUTH_PRIVATE_KEY; + if (!authKey) { + throw new Error('LIT_AUTH_PRIVATE_KEY environment variable is required'); + } + + console.log('🚀 Initializing Lit client...'); + const client = await LitClient.create(authKey, { + litNetwork: LIT_NETWORK.DatilTest, + }); + + // Check if client is ready + console.log('✓ Client initialized, checking if ready...'); + console.log('Client ready:', client.isReady()); + + // Create a new wallet if one doesn't exist + let pkp = client.getPkp(); + if (!pkp) { + console.log('Creating new wallet...'); + const mintInfo = await client.createWallet(); + console.log('✓ Wallet created:', mintInfo); + pkp = client.getPkp(); + } else { + console.log('✓ Using existing wallet'); + } + + // Sign a message. This must be 32 bytes hex, like an eth txn for example + console.log('Signing message...'); + const messageToSign = + '0x8111e78458fec7fb123fdfe3c559a1f7ae33bf21bf81d1bad589e9422c648cbd'; + const signedMessage = await client.sign({ + toSign: messageToSign, + }); + console.log('✓ Message signed:', signedMessage); + + // Execute some JS code + console.log('Executing JS code...'); + const result = await client.executeJs({ + code: ` + Lit.Actions.setResponse({ response: message + " - processed by Lit Protocol" }); + `, + jsParams: { + message: 'Hello', + }, + }); + console.log('✓ JS execution result:', result); + + // Cleanup + await client.disconnect(); + console.log('✓ Client disconnected'); + } catch (error) { + console.error('Error:', error); + process.exit(1); + } +} + +main(); diff --git a/packages/lit-agent-signer/examples/minimal-signing.ts b/packages/lit-agent-signer/examples/minimal-signing.ts new file mode 100644 index 00000000..2ea909de --- /dev/null +++ b/packages/lit-agent-signer/examples/minimal-signing.ts @@ -0,0 +1,27 @@ +import { LitClient } from '../dist/index'; +import * as dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +async function main() { + const client = await LitClient.create(process.env.LIT_AUTH_PRIVATE_KEY!); + + const { pkp } = await client.createWallet(); + console.log('✓ Wallet created:', pkp); + + const signedTxn = await client.sign({ + toSign: + '0x8111e78458fec7fb123fdfe3c559a1f7ae33bf21bf81d1bad589e9422c648cbd', + }); + console.log('✓ Transaction signed:', signedTxn); +} + +main() + .catch((error) => { + console.error(error); + process.exit(1); + }) + .finally(() => { + process.exit(0); + }); diff --git a/packages/lit-agent-signer/jest.config.cjs b/packages/lit-agent-signer/jest.config.cjs new file mode 100644 index 00000000..8049f634 --- /dev/null +++ b/packages/lit-agent-signer/jest.config.cjs @@ -0,0 +1 @@ +/** @type {import('@jest/types').Config.InitialOptions} */ module.exports = { preset: 'ts-jest', testEnvironment: 'node', transform: { '^.+\.ts$': ['ts-jest', { tsconfig: require('path').resolve(__dirname, 'tsconfig.test.json') }] }, moduleFileExtensions: ['ts', 'js', 'json'], testMatch: ['**/__tests__/**/*.test.ts'], displayName: 'lit-agent-signer', rootDir: '.' }; diff --git a/packages/lit-agent-signer/jest.config.ts b/packages/lit-agent-signer/jest.config.ts new file mode 100644 index 00000000..8709cacf --- /dev/null +++ b/packages/lit-agent-signer/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'lit-agent-signer', + preset: '../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/packages/lit-agent-signer', +}; diff --git a/packages/lit-agent-signer/jest.setup.ts b/packages/lit-agent-signer/jest.setup.ts new file mode 100644 index 00000000..c326ecd8 --- /dev/null +++ b/packages/lit-agent-signer/jest.setup.ts @@ -0,0 +1,17 @@ +import { TextEncoder, TextDecoder } from 'util'; +import { LocalStorage } from 'node-localstorage'; + +// Create a storage directory if it doesn't exist +const localStorage = new LocalStorage('./scratch'); + +// Extend the NodeJS global type +declare global { + // eslint-disable-next-line no-var + var localStorage: Storage; +} + +// Setup LocalStorage for Node.js environment +Object.assign(global, { localStorage }); + +// TextEncoder and TextDecoder are already available in the global scope in Node.js +// No need to explicitly declare or assign them diff --git a/packages/lit-agent-signer/package.json b/packages/lit-agent-signer/package.json new file mode 100644 index 00000000..1f648dd3 --- /dev/null +++ b/packages/lit-agent-signer/package.json @@ -0,0 +1,47 @@ +{ + "name": "@lit-protocol/agent-signer", + "version": "0.5.0", + "description": "Makes signing transactions and messages with Lit Protocol easy", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "nx build lit-agent-signer", + "test": "dotenvx run -- nx test lit-agent-signer", + "lint": "nx lint lit-agent-signer", + "format": "prettier --write src/**/*.ts", + "example": "npx tsx examples/basic-usage.ts", + "minimalSigningExample": "npx tsx examples/minimal-signing.ts" + }, + "dependencies": { + "@lit-protocol/auth-helpers": "^7", + "@lit-protocol/contracts-sdk": "^7", + "@lit-protocol/lit-node-client-nodejs": "^7", + "ethers": "^5.7.2", + "node-localstorage": "^3.0.5" + }, + "devDependencies": { + "@babel/core": "^7.24.0", + "@babel/preset-env": "^7.24.0", + "@dotenvx/dotenvx": "^1.31.3", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.17.0", + "@lit-protocol/types": "^7.0.2", + "@types/jest": "^29.5.12", + "@types/node": "^20.11.24", + "@types/node-localstorage": "^1.3.3", + "@typescript-eslint/eslint-plugin": "^8", + "@typescript-eslint/parser": "^8", + "esbuild": "^0.20.1", + "eslint": "^9", + "globals": "^15.14.0", + "jest": "^29.7.0", + "prettier": "^3.4.2", + "ts-jest": "^29.1.2", + "typescript": "^5.3.3" + } +} diff --git a/packages/lit-agent-signer/project.json b/packages/lit-agent-signer/project.json new file mode 100644 index 00000000..05c5aca5 --- /dev/null +++ b/packages/lit-agent-signer/project.json @@ -0,0 +1,30 @@ +{ + "name": "lit-agent-signer", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/lit-agent-signer/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/lit-agent-signer", + "main": "packages/lit-agent-signer/src/index.ts", + "tsConfig": "packages/lit-agent-signer/tsconfig.json", + "assets": ["packages/lit-agent-signer/*.md"] + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "packages/lit-agent-signer/jest.config.ts" + } + } + }, + "tags": ["core"] +} diff --git a/packages/lit-agent-signer/src/__tests__/index.test.ts b/packages/lit-agent-signer/src/__tests__/index.test.ts new file mode 100644 index 00000000..a3c8ba30 --- /dev/null +++ b/packages/lit-agent-signer/src/__tests__/index.test.ts @@ -0,0 +1,371 @@ +import { + describe, + expect, + it, + beforeAll, + afterAll, + beforeEach, +} from '@jest/globals'; +import { LitClient } from '../index'; +import { ethers } from 'ethers'; +import { + LIT_RPC, + AUTH_METHOD_SCOPE, + LIT_NETWORK, +} from '@lit-protocol/constants'; +import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs'; +import { getSessionSigs } from '../utils'; +import { localStorage } from '../index'; +import { SessionSigsMap, MintWithAuthResponse } from '@lit-protocol/types'; +import { LitResourceAbilityRequest } from '@lit-protocol/auth-helpers'; + +interface PKP { + tokenId: string; + publicKey: string; + ethAddress: string; +} + +interface WalletInfo { + pkp: PKP; + tx: ethers.ContractTransaction; + tokenId: string; + res: MintWithAuthResponse; +} + +interface NodeSignature { + sig: string; + derivedVia: string; + signedMessage: string; + address: string; + algo: string; +} + +interface Capability { + sig: string; + derivedVia: string; + signedMessage: string; + address: string; +} + +interface SignedMessageContent { + capabilities: Capability[]; + sessionKey: string; + resourceAbilityRequests: LitResourceAbilityRequest[]; + issuedAt: string; + expiration: string; +} + +describe('LitClient Integration Tests', () => { + beforeAll(() => { + // Clear all storage before any tests run + if (existsSync('./lit-session-storage')) { + rmSync('./lit-session-storage', { recursive: true, force: true }); + } + mkdirSync('./lit-session-storage'); + // Initialize empty files + writeFileSync('./lit-session-storage/pkp', ''); + writeFileSync('./lit-session-storage/capacityCreditId', ''); + }); + + afterAll(() => { + // Clean up after all tests + if (existsSync('./lit-session-storage')) { + rmSync('./lit-session-storage', { recursive: true, force: true }); + } + }); + + describe('DatilDev Network', () => { + let litClient: LitClient; + + beforeAll(async () => { + if (!process.env.LIT_AUTH_PRIVATE_KEY) { + throw new Error( + 'LIT_AUTH_PRIVATE_KEY environment variable is required' + ); + } + + litClient = await LitClient.create(process.env.LIT_AUTH_PRIVATE_KEY, { + litNetwork: LIT_NETWORK.DatilDev, + }); + + await new Promise((resolve) => { + const checkReady = () => { + try { + if (litClient.isReady()) resolve(true); + else setTimeout(checkReady, 500); + } catch (e) { + console.log('error', e); + setTimeout(checkReady, 500); + } + }; + checkReady(); + }); + }, 30000); + + afterAll(async () => { + await litClient?.disconnect(); + }); + + // All tests for DatilDev + describe('Basic Operations', () => { + it('should confirm client is ready', () => { + const ready = litClient.isReady(); + expect(ready).toBe(true); + }); + + it('should execute JavaScript code', async () => { + const result = await litClient.executeJs({ + code: `(async () => { Lit.Actions.setResponse({"response": "Hello from Lit Protocol!" }); })()`, + jsParams: {}, + }); + expect(result).toHaveProperty('response'); + expect(result.response).toBe('Hello from Lit Protocol!'); + }, 10000); + + it('should execute JavaScript code from IPFS', async () => { + // First create a wallet to get a public key + const walletInfo = await litClient.createWallet(); + expect(walletInfo.pkp).toBeDefined(); + + const result = await litClient.executeJs({ + ipfsId: 'QmQwNvbP9YAY4B4wYgFoD6cNnX3udNDBjWC7RqN48GdpmN', + jsParams: { + publicKey: walletInfo.pkp.publicKey, + }, + }); + expect(result).toHaveProperty('response'); + expect(result.response).toBeDefined(); + }, 30000); + }); + + describe('Wallet Operations', () => { + it('should create a wallet and sign a message', async () => { + const walletInfo: WalletInfo = await litClient.createWallet(); + expect(walletInfo.pkp).toBeDefined(); + + const messageToSign = + '0x8111e78458fec7fb123fdfe3c559a1f7ae33bf21bf81d1bad589e9422c648cbd'; + const signResult = await litClient.sign({ toSign: messageToSign }); + expect(signResult.signature).toBeDefined(); + }, 30000); + }); + + describe('PKP Actions', () => { + it('should add and verify a permitted action', async () => { + const walletInfo: WalletInfo = await litClient.createWallet(); + const provider = new ethers.providers.JsonRpcProvider( + LIT_RPC.CHRONICLE_YELLOWSTONE + ); + await provider.waitForTransaction(walletInfo.tx.hash, 2); + + const ipfsId = 'QmTestHash123'; + const addResult = await litClient.addPermittedAction({ + ipfsId, + scopes: [AUTH_METHOD_SCOPE.SignAnything], + }); + await provider.waitForTransaction(addResult.transactionHash, 2); + + const isPermitted = + await litClient.litContracts?.pkpPermissionsContractUtils.read.isPermittedAction( + walletInfo.pkp.tokenId, + ipfsId + ); + expect(isPermitted).toBe(true); + }, 60000); + }); + + describe('Capacity Credits', () => { + beforeEach(() => { + // Create directory if it doesn't exist + if (!existsSync('./lit-session-storage')) { + mkdirSync('./lit-session-storage'); + } + // Clear all storage including lit-session-storage + localStorage.clear(); + rmSync('./lit-session-storage', { recursive: true, force: true }); + mkdirSync('./lit-session-storage'); + }); + + it('should not mint capacity credits on dev network', async () => { + const walletInfo: WalletInfo = await litClient.createWallet(); + expect(walletInfo.pkp).toBeDefined(); + console.log('DatilDev Network:', litClient.getNetwork()); + const capacityCreditId = litClient.getCapacityCreditId(); + console.log('DatilDev Capacity Credit ID:', capacityCreditId); + expect(litClient.getNetwork()).toBe(LIT_NETWORK.DatilDev); + expect(capacityCreditId).toBeNull(); + expect(localStorage.getItem('capacityCreditId')).toBeNull(); + }, 30000); + }); + }); + + describe('DatilTest Network', () => { + let litClient: LitClient; + + beforeAll(async () => { + if (!process.env.LIT_AUTH_PRIVATE_KEY) { + throw new Error( + 'LIT_AUTH_PRIVATE_KEY environment variable is required' + ); + } + + litClient = await LitClient.create(process.env.LIT_AUTH_PRIVATE_KEY, { + litNetwork: LIT_NETWORK.DatilTest, + }); + + await new Promise((resolve) => { + const checkReady = () => { + try { + if (litClient.isReady()) resolve(true); + else setTimeout(checkReady, 500); + } catch (e) { + console.log('error', e); + setTimeout(checkReady, 500); + } + }; + checkReady(); + }); + }, 30000); + + afterAll(async () => { + await litClient?.disconnect(); + }); + + describe('Basic Operations', () => { + it('should confirm client is ready', () => { + const ready = litClient.isReady(); + expect(ready).toBe(true); + }); + + it('should execute JavaScript code', async () => { + const result = await litClient.executeJs({ + code: `(async () => { Lit.Actions.setResponse({"response": "Hello from Lit Protocol!" }); })()`, + jsParams: {}, + }); + expect(result).toHaveProperty('response'); + expect(result.response).toBe('Hello from Lit Protocol!'); + }, 10000); + + it('should execute JavaScript code from IPFS', async () => { + // First create a wallet to get a public key + const walletInfo = await litClient.createWallet(); + expect(walletInfo.pkp).toBeDefined(); + + const result = await litClient.executeJs({ + ipfsId: 'QmQwNvbP9YAY4B4wYgFoD6cNnX3udNDBjWC7RqN48GdpmN', + jsParams: { + publicKey: walletInfo.pkp.publicKey, + }, + }); + expect(result).toHaveProperty('response'); + expect(result.response).toBeDefined(); + }, 30000); + }); + + describe('Wallet Operations', () => { + it('should create a wallet and sign a message', async () => { + const walletInfo: WalletInfo = await litClient.createWallet(); + expect(walletInfo.pkp).toBeDefined(); + + const messageToSign = + '0x8111e78458fec7fb123fdfe3c559a1f7ae33bf21bf81d1bad589e9422c648cbd'; + const signResult = await litClient.sign({ toSign: messageToSign }); + expect(signResult.signature).toBeDefined(); + }, 30000); + }); + + describe('PKP Actions', () => { + it('should add and verify a permitted action', async () => { + const walletInfo: WalletInfo = await litClient.createWallet(); + const provider = new ethers.providers.JsonRpcProvider( + LIT_RPC.CHRONICLE_YELLOWSTONE + ); + await provider.waitForTransaction(walletInfo.tx.hash, 2); + + const ipfsId = 'QmTestHash123'; + const addResult = await litClient.addPermittedAction({ + ipfsId, + scopes: [AUTH_METHOD_SCOPE.SignAnything], + }); + await provider.waitForTransaction(addResult.transactionHash, 2); + + const isPermitted = + await litClient.litContracts?.pkpPermissionsContractUtils.read.isPermittedAction( + walletInfo.pkp.tokenId, + ipfsId + ); + expect(isPermitted).toBe(true); + }, 60000); + }); + + describe('Capacity Credits', () => { + beforeEach(() => { + // Create directory if it doesn't exist + if (!existsSync('./lit-session-storage')) { + mkdirSync('./lit-session-storage'); + } + // Clear all storage including lit-session-storage + localStorage.clear(); + rmSync('./lit-session-storage', { recursive: true, force: true }); + mkdirSync('./lit-session-storage'); + }); + + it('should mint and store capacity credits', async () => { + await litClient.createWallet(); + const capacityCreditId = litClient.getCapacityCreditId(); + expect(capacityCreditId).toBeDefined(); + expect(typeof capacityCreditId).toBe('string'); + expect(localStorage.getItem('capacityCreditId')).toBe(capacityCreditId); + }, 60000); + + it('should load capacity credit ID from storage on client creation', async () => { + const mockId = '12345'; + localStorage.setItem('capacityCreditId', mockId); + + const newClient = await LitClient.create( + process.env.LIT_AUTH_PRIVATE_KEY!, + { + litNetwork: LIT_NETWORK.DatilTest, + } + ); + + expect(newClient.getCapacityCreditId()).toBe(mockId); + await newClient.disconnect(); + }, 30000); + + it('should use capacity credits in session signatures', async () => { + await litClient.createWallet(); + const capacityCreditId = litClient.getCapacityCreditId(); + console.log('DatilTest Network:', litClient.getNetwork()); + console.log('DatilTest Capacity Credit ID:', capacityCreditId); + expect(litClient.getNetwork()).toBe(LIT_NETWORK.DatilTest); + expect(capacityCreditId).toBeDefined(); + + const sessionSigs: SessionSigsMap = await getSessionSigs(litClient); + console.log( + 'DatilTest Session Sigs:', + JSON.stringify(sessionSigs, null, 2) + ); + expect(sessionSigs).toBeDefined(); + + // Check for capacity delegation in the capabilities + const anyNode = Object.values(sessionSigs)[0] as NodeSignature; + expect(anyNode).toBeDefined(); + const parsedMessage = JSON.parse( + anyNode.signedMessage + ) as SignedMessageContent; + const capabilities = parsedMessage.capabilities; + expect(capabilities).toBeDefined(); + + // Find the capacity delegation capability + const capacityDelegation = capabilities.find((cap: Capability) => + cap.signedMessage.includes( + `lit-ratelimitincrease://${capacityCreditId}` + ) + ); + expect(capacityDelegation).toBeDefined(); + expect(capacityDelegation?.derivedVia).toBe('web3.eth.personal.sign'); + }, 60000); + }); + }); +}); diff --git a/packages/lit-agent-signer/src/index.ts b/packages/lit-agent-signer/src/index.ts new file mode 100644 index 00000000..1b6be1ec --- /dev/null +++ b/packages/lit-agent-signer/src/index.ts @@ -0,0 +1,257 @@ +import * as LitJsSdk from '@lit-protocol/lit-node-client-nodejs'; +import { + LIT_NETWORK, + LIT_RPC, + AUTH_METHOD_SCOPE, + AUTH_METHOD_SCOPE_VALUES, +} from '@lit-protocol/constants'; +import { ethers } from 'ethers'; +import { LitContracts } from '@lit-protocol/contracts-sdk'; +import { + ExecuteJsResponse, + LIT_NETWORKS_KEYS, + MintWithAuthResponse, + SigResponse, +} from '@lit-protocol/types'; +import { LocalStorage } from 'node-localstorage'; + +import { + getSessionSigs, + readPkpFromStorage, + readCapacityTokenIdFromStorage, + readNetworkFromStorage, +} from './utils'; + +const storage = new LocalStorage('./lit-session-storage'); +Object.assign(global, { localStorage: storage }); +export const localStorage = storage; + +export { + readPkpFromStorage, + readCapacityTokenIdFromStorage, + readNetworkFromStorage, +}; + +type ExecuteJsParams = { + jsParams: object; +} & ({ code: string; ipfsId?: never } | { code?: never; ipfsId: string }); + +export class LitClient { + litNodeClient: LitJsSdk.LitNodeClientNodeJs | null = null; + ethersWallet: ethers.Wallet | null = null; + litContracts: LitContracts | null = null; + private pkp: MintWithAuthResponse['pkp'] | null = + null; + private capacityCreditId: string | null = null; + + /** + * Initialize the SDK + * @param authKey The authentication key + * @returns A Promise that resolves to a new LitClient instance + */ + static async create( + authKey: string, + { + litNetwork = LIT_NETWORK.DatilDev, + debug = false, + }: { + litNetwork?: LIT_NETWORKS_KEYS; + debug?: boolean; + } = {} + ): Promise { + const client = new LitClient(); + client.litNodeClient = new LitJsSdk.LitNodeClientNodeJs({ + litNetwork, + debug, + }); + await client.litNodeClient.connect(); + + client.ethersWallet = new ethers.Wallet( + authKey, + new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE) + ); + + client.litContracts = new LitContracts({ + signer: client.ethersWallet, + network: litNetwork, + debug, + }); + await client.litContracts.connect(); + + // Load PKP and capacity credit ID from storage + try { + const pkp = localStorage.getItem('pkp'); + const capacityCreditId = localStorage.getItem('capacityCreditId'); + + if (pkp) { + client.pkp = JSON.parse( + pkp + ) as MintWithAuthResponse['pkp']; + } + if (capacityCreditId) { + client.capacityCreditId = capacityCreditId; + } + } catch (error) { + // If storage files don't exist yet, that's okay - we'll create them when needed + console.log('Storage not initialized yet: ', error); + } + + return client; + } + + /** + * Check if the client is ready + */ + isReady(): boolean { + if (!this.litNodeClient) { + throw new Error('LitNodeClient not initialized'); + } + return this.litNodeClient.ready; + } + + /** + * Execute JavaScript code + */ + async executeJs(params: ExecuteJsParams): Promise { + if (!this.litNodeClient) { + throw new Error('LitNodeClient not initialized'); + } + try { + const sessionSigs = await getSessionSigs(this); + + return this.litNodeClient.executeJs({ + sessionSigs, + ...params, + }); + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error(`Failed to execute JS: ${error.message}`); + } + throw error; + } + } + + /** + * Create a new wallet + */ + async createWallet(): Promise<{ + pkp: { tokenId: string; publicKey: string; ethAddress: string }; + tx: ethers.ContractTransaction; + tokenId: string; + res: MintWithAuthResponse; + }> { + if (!this.litContracts || !this.ethersWallet) { + throw new Error('Client not properly initialized'); + } + + const mintInfo = await this.litContracts.pkpNftContractUtils.write.mint(); + + // Save to storage + localStorage.setItem('pkp', JSON.stringify(mintInfo.pkp)); + this.pkp = mintInfo.pkp; + + if ( + this.litContracts.network === LIT_NETWORK.DatilTest || + this.litContracts.network === LIT_NETWORK.Datil + ) { + const capacityCreditInfo = await this.litContracts.mintCapacityCreditsNFT( + { + requestsPerKilosecond: 10, + daysUntilUTCMidnightExpiration: 1, + } + ); + localStorage.setItem( + 'capacityCreditId', + capacityCreditInfo.capacityTokenIdStr + ); + this.capacityCreditId = capacityCreditInfo.capacityTokenIdStr; + } + + return mintInfo; + } + + /** + * Get the PKP + */ + getPkp() { + const pkp = localStorage.getItem('pkp'); + return pkp + ? (JSON.parse(pkp) as MintWithAuthResponse['pkp']) + : null; + } + + /** + * Add a permitted action to the PKP + */ + async addPermittedAction({ + ipfsId, + scopes = [AUTH_METHOD_SCOPE.SignAnything], + }: { + ipfsId: string; + scopes?: AUTH_METHOD_SCOPE_VALUES[]; + }) { + if (!this.ethersWallet || !this.pkp || !this.litContracts) { + throw new Error('Client not properly initialized or PKP not set'); + } + + return this.litContracts.addPermittedAction({ + ipfsId, + authMethodScopes: scopes, + pkpTokenId: this.pkp.tokenId, + }); + } + + /** + * Sign a message + */ + async sign({ toSign }: { toSign: string }): Promise { + if (!this.litNodeClient || !this.pkp) { + throw new Error('Client not properly initialized or PKP not set'); + } + + const sessionSigs = await getSessionSigs(this); + + const signingResult = await this.litNodeClient.pkpSign({ + pubKey: this.pkp.publicKey, + sessionSigs, + toSign: ethers.utils.arrayify(toSign), + }); + + return signingResult; + } + + /** + * Disconnect the client and cleanup + */ + async disconnect() { + if (this.litNodeClient) { + await this.litNodeClient.disconnect(); + } + } + + /** + * Get permitted auth methods for the PKP + */ + async getPermittedAuthMethods() { + if ( + !this.litNodeClient || + !this.ethersWallet || + !this.pkp || + !this.litContracts + ) { + throw new Error('Client not properly initialized or PKP not set'); + } + + return this.litContracts.pkpPermissionsContract.read.getPermittedAuthMethods( + this.pkp.tokenId + ); + } + + getCapacityCreditId() { + return this.capacityCreditId; + } + + getNetwork() { + return this.litContracts?.network; + } +} diff --git a/packages/lit-agent-signer/src/utils.ts b/packages/lit-agent-signer/src/utils.ts new file mode 100644 index 00000000..a9f0e681 --- /dev/null +++ b/packages/lit-agent-signer/src/utils.ts @@ -0,0 +1,101 @@ +import { LitClient } from '.'; + +import { LIT_ABILITY, LIT_NETWORK } from '@lit-protocol/constants'; +import { + LitActionResource, + LitPKPResource, + createSiweMessage, + generateAuthSig, +} from '@lit-protocol/auth-helpers'; +import { SessionSigsMap } from '@lit-protocol/types'; +import { localStorage } from './index'; +import { MintWithAuthResponse } from '@lit-protocol/types'; +import { ethers } from 'ethers'; +import { LIT_NETWORKS_KEYS } from '@lit-protocol/types'; + +export const readPkpFromStorage = () => { + const pkp = localStorage.getItem('pkp'); + return pkp + ? (JSON.parse(pkp) as MintWithAuthResponse['pkp']) + : null; +}; + +export const readCapacityTokenIdFromStorage = () => { + return localStorage.getItem('capacityCreditId'); +}; + +export const readNetworkFromStorage = () => { + const network = localStorage.getItem('network'); + return network as LIT_NETWORKS_KEYS | null; +}; + +export async function getSessionSigs( + litClient: LitClient +): Promise { + if (!litClient.litNodeClient) { + throw new Error('Lit Node Client not properly initialized'); + } + if (!litClient.ethersWallet) { + throw new Error('Ethers Wallet not properly initialized'); + } + if (!litClient.litContracts) { + throw new Error('Lit Contracts not properly initialized'); + } + + let capacityDelegationAuthSig; + if ( + litClient.litContracts.network === LIT_NETWORK.DatilTest || + litClient.litContracts.network === LIT_NETWORK.Datil + ) { + const capacityCreditId = litClient.getCapacityCreditId(); + if (capacityCreditId) { + capacityDelegationAuthSig = + await litClient.litNodeClient.createCapacityDelegationAuthSig({ + dAppOwnerWallet: litClient.ethersWallet, + capacityTokenId: capacityCreditId, + delegateeAddresses: [litClient.ethersWallet.address], + uses: '1', + expiration: new Date(Date.now() + 1000 * 60 * 10).toISOString(), + }); + } + } + + // get session sigs + const sessionSigs = await litClient.litNodeClient.getSessionSigs({ + chain: 'ethereum', + expiration: new Date(Date.now() + 1000 * 60 * 10).toISOString(), // 10 minutes + capabilityAuthSigs: capacityDelegationAuthSig + ? [capacityDelegationAuthSig.capacityDelegationAuthSig] + : undefined, + resourceAbilityRequests: [ + { + resource: new LitActionResource('*'), + ability: LIT_ABILITY.LitActionExecution, + }, + { + resource: new LitPKPResource('*'), + ability: LIT_ABILITY.PKPSigning, + }, + ], + authNeededCallback: async ({ + uri, + expiration, + resourceAbilityRequests, + }) => { + const toSign = await createSiweMessage({ + uri, + expiration, + resources: resourceAbilityRequests, + walletAddress: await litClient.ethersWallet!.getAddress(), + nonce: await litClient.litNodeClient!.getLatestBlockhash(), + litNodeClient: litClient.litNodeClient, + }); + + return await generateAuthSig({ + signer: litClient.ethersWallet!, + toSign, + }); + }, + }); + return sessionSigs; +} diff --git a/packages/lit-agent-signer/tsconfig.json b/packages/lit-agent-signer/tsconfig.json new file mode 100644 index 00000000..df925836 --- /dev/null +++ b/packages/lit-agent-signer/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "esnext", + "lib": ["es2018"], + "declaration": true, + "declarationDir": "dist", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node", + "rootDir": "./src", + "typeRoots": ["./node_modules/@types", "./src/types"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "examples"] +} \ No newline at end of file diff --git a/packages/lit-agent-signer/tsconfig.spec.json b/packages/lit-agent-signer/tsconfig.spec.json new file mode 100644 index 00000000..9b2a121d --- /dev/null +++ b/packages/lit-agent-signer/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/packages/lit-agent-signer/tsconfig.test.json b/packages/lit-agent-signer/tsconfig.test.json new file mode 100644 index 00000000..9bce8d2b --- /dev/null +++ b/packages/lit-agent-signer/tsconfig.test.json @@ -0,0 +1 @@ +{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "moduleResolution": "node", "esModuleInterop": true, "strict": true, "skipLibCheck": true, "types": ["jest", "node"], "rootDir": "./src" }, "include": ["src/**/*"] }