diff --git a/code-examples/lit-actions/fetch/.env.example b/code-examples/lit-actions/fetch/.env.example new file mode 100644 index 00000000..ee10af9f --- /dev/null +++ b/code-examples/lit-actions/fetch/.env.example @@ -0,0 +1,11 @@ +# Ethereum private key for signing transactions +# This should be a test account with no real funds +ETHEREUM_PRIVATE_KEY=0x... + +# Lit Network to connect to: datil-dev, datil-test, or datil +# Default: datil-dev +LIT_NETWORK=datil-dev + +# Enable debug logging +# Default: false +LIT_DEBUG=false diff --git a/code-examples/lit-actions/fetch/.spec.swcrc b/code-examples/lit-actions/fetch/.spec.swcrc new file mode 100644 index 00000000..3b52a537 --- /dev/null +++ b/code-examples/lit-actions/fetch/.spec.swcrc @@ -0,0 +1,22 @@ +{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + }, + "keepClassNames": true, + "externalHelpers": true, + "loose": true + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": [] +} diff --git a/code-examples/lit-actions/fetch/README.md b/code-examples/lit-actions/fetch/README.md new file mode 100644 index 00000000..22bd7687 --- /dev/null +++ b/code-examples/lit-actions/fetch/README.md @@ -0,0 +1,102 @@ +# Lit Protocol Fetch Example + +This example demonstrates how to use the `Lit.Actions.fetch` function to make HTTP requests from a Lit Action. + +## Overview + +The Lit Protocol allows you to execute JavaScript code in a decentralized, secure environment. One of the powerful features of Lit Actions is the ability to make HTTP requests to external APIs using the `Lit.Actions.fetch` function. + +This example shows how to: + +1. Connect to the Lit Network +2. Create an authentication signature +3. Define a Lit Action that makes an HTTP request to an external API +4. Execute the Lit Action +5. Process the response + +## Prerequisites + +- Node.js (v16 or higher) +- An Ethereum private key for signing authentication messages + +## Environment Variables + +Create a `.env` file in the root directory with the following variables: + +``` +# Ethereum private key for signing transactions +# This should be a test account with no real funds +ETHEREUM_PRIVATE_KEY=0x... + +# Lit Network to connect to: datil-dev, datil-test, or datil +# Default: datil-dev +LIT_NETWORK=datil-dev + +# Enable debug logging +# Default: false +LIT_DEBUG=false +``` + +## Usage + +### Running the Example + +```bash +pnpm nx test fetch +``` + +### Code Explanation + +The example demonstrates a simple HTTP GET request to the JSONPlaceholder API: + +```javascript +const litActionCode = ` + const fetchResult = await Lit.Actions.fetch({ + url: 'https://jsonplaceholder.typicode.com/todos/1', + method: 'GET' + }); + + const responseBody = await fetchResult.json(); + + Lit.Actions.setResponse({ + response: JSON.stringify(responseBody) + }); +`; +``` + +This Lit Action: +1. Makes a GET request to the JSONPlaceholder API +2. Parses the JSON response +3. Returns the response data + +### API Reference + +The `Lit.Actions.fetch` function accepts the following parameters: + +- `url` (required): The URL to make the request to +- `method` (required): The HTTP method to use (GET, POST, PUT, DELETE, etc.) +- `headers`: Optional headers to include in the request +- `body`: Optional request body (for POST, PUT, etc.) +- `cache`: Optional cache mode +- `credentials`: Optional credentials mode +- `redirect`: Optional redirect mode +- `referrer`: Optional referrer +- `referrerPolicy`: Optional referrer policy +- `integrity`: Optional subresource integrity value +- `keepalive`: Optional keepalive flag +- `signal`: Optional AbortSignal to abort the request + +## Security Considerations + +When using `Lit.Actions.fetch` in production: + +1. Be careful with the URLs you fetch from - only fetch from trusted sources +2. Consider rate limiting to avoid excessive requests +3. Handle errors gracefully +4. Be mindful of sensitive data in requests and responses +5. Consider using conditional access control for sensitive operations + +## Further Reading + +- [Lit Protocol Documentation](https://developer.litprotocol.com/) +- [Lit Actions API Reference](https://developer.litprotocol.com/v3/sdk/actions/) diff --git a/code-examples/lit-actions/fetch/eslint.config.mjs b/code-examples/lit-actions/fetch/eslint.config.mjs new file mode 100644 index 00000000..0f114fe3 --- /dev/null +++ b/code-examples/lit-actions/fetch/eslint.config.mjs @@ -0,0 +1,22 @@ +import baseConfig from '../../../eslint.config.mjs'; + +export default [ + ...baseConfig, + { + files: ['**/*.json'], + rules: { + '@nx/dependency-checks': [ + 'error', + { + ignoredFiles: [ + '{projectRoot}/eslint.config.{js,cjs,mjs}', + '{projectRoot}/esbuild.config.{js,ts,mjs,mts}', + ], + }, + ], + }, + languageOptions: { + parser: await import('jsonc-eslint-parser'), + }, + }, +]; diff --git a/code-examples/lit-actions/fetch/jest.config.ts b/code-examples/lit-actions/fetch/jest.config.ts new file mode 100644 index 00000000..a4cf8ba7 --- /dev/null +++ b/code-examples/lit-actions/fetch/jest.config.ts @@ -0,0 +1,21 @@ +/* eslint-disable */ +import { readFileSync } from 'fs'; + +// Reading the SWC compilation config for the spec files +const swcJestConfig = JSON.parse( + readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8') +); + +// Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves +swcJestConfig.swcrc = false; + +export default { + displayName: '@dev-guides-code/fetch', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: 'test-output/jest/coverage', +}; diff --git a/code-examples/lit-actions/fetch/package.json b/code-examples/lit-actions/fetch/package.json new file mode 100644 index 00000000..4065e7aa --- /dev/null +++ b/code-examples/lit-actions/fetch/package.json @@ -0,0 +1,45 @@ +{ + "name": "@dev-guides-code/fetch", + "version": "0.0.1", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "!**/*.tsbuildinfo" + ], + "nx": { + "targets": { + "build": { + "executor": "@nx/esbuild:esbuild", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "code-examples/lit-actions/fetch/dist", + "main": "code-examples/lit-actions/fetch/src/index.ts", + "tsConfig": "code-examples/lit-actions/fetch/tsconfig.lib.json", + "format": [ + "esm" + ], + "declarationRootDir": "code-examples/lit-actions/fetch/src" + } + } + } + }, + "dependencies": { + "@dev-guides-code/example-utils": "workspace:*", + "@lit-protocol/lit-node-client": "catalog:", + "ethers": "^6.13.5" + } +} diff --git a/code-examples/lit-actions/fetch/src/index.ts b/code-examples/lit-actions/fetch/src/index.ts new file mode 100644 index 00000000..7a269432 --- /dev/null +++ b/code-examples/lit-actions/fetch/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/example.js'; +export * from './lib/litAction.js'; diff --git a/code-examples/lit-actions/fetch/src/lib/example.spec.ts b/code-examples/lit-actions/fetch/src/lib/example.spec.ts new file mode 100644 index 00000000..595c4155 --- /dev/null +++ b/code-examples/lit-actions/fetch/src/lib/example.spec.ts @@ -0,0 +1,51 @@ +import { runExample, cleanup } from './example.js'; + +describe('fetch', () => { + // Set a longer timeout for the entire test suite + jest.setTimeout(30000); + + // Clean up after each test + afterEach(async () => { + await cleanup(); + }); + + // Clean up after all tests + afterAll(async () => { + await cleanup(); + }); + + it('should demonstrate a Lit Action that fetches data from an external API', async () => { + try { + console.log('Starting fetch test...'); + const result = await runExample(); + + // Verify the result has all expected properties + expect(result).toHaveProperty('walletAddress'); + expect(result).toHaveProperty('litActionCode'); + expect(result).toHaveProperty('response'); + + // Verify the response contains the expected data from the API + const response = result.response; + expect(response).toBeDefined(); + + // Parse the response if it's a string + const parsedResponse = typeof response.response === 'string' + ? JSON.parse(response.response) + : response.response; + + // Check if the response has the expected structure from jsonplaceholder API + expect(parsedResponse).toHaveProperty('id'); + expect(parsedResponse).toHaveProperty('title'); + expect(parsedResponse).toHaveProperty('completed'); + expect(parsedResponse).toHaveProperty('userId'); + + // Log the results for inspection + console.log('Fetch test completed successfully'); + console.log('Wallet Address:', result.walletAddress); + console.log('Response:', JSON.stringify(result.response, null, 2)); + } catch (error) { + console.error('Fetch test failed with error:', error); + throw error; + } + }); +}); diff --git a/code-examples/lit-actions/fetch/src/lib/example.ts b/code-examples/lit-actions/fetch/src/lib/example.ts new file mode 100644 index 00000000..dcf25845 --- /dev/null +++ b/code-examples/lit-actions/fetch/src/lib/example.ts @@ -0,0 +1,135 @@ +import { LitNodeClient } from '@lit-protocol/lit-node-client'; +import { ethers } from 'ethers'; +import { getEnv } from '@dev-guides-code/example-utils'; +import { runExample as getLitActionCode } from './litAction.js'; + +// Keep track of the litNodeClient instance to properly disconnect +let litNodeClient: LitNodeClient | null = null; +// Keep track of timeouts to clear them +let connectTimeoutId: NodeJS.Timeout | null = null; + +// Cleanup function to disconnect from Lit Network and clear timeouts +export async function cleanup() { + // Clear any pending timeouts + if (connectTimeoutId) { + clearTimeout(connectTimeoutId); + connectTimeoutId = null; + } + + // Disconnect from Lit Network + if (litNodeClient) { + try { + // Check if there's a disconnect method and call it + if (typeof litNodeClient.disconnect === 'function') { + await litNodeClient.disconnect(); + } + litNodeClient = null; + console.log('Disconnected from Lit Network'); + } catch (error) { + console.error('Error disconnecting from Lit Network:', error); + } + } +} + +export async function runExample() { + try { + // Get environment variables + const ETHEREUM_PRIVATE_KEY = getEnv({ + name: 'ETHEREUM_PRIVATE_KEY', + validator: (value: string) => { + // Add 0x prefix if missing + if (!value.startsWith('0x')) { + value = '0x' + value; + } + return value; + }, + }); + + const LIT_NETWORK = getEnv<'datil-dev' | 'datil-test' | 'datil'>({ + name: 'LIT_NETWORK', + required: false, + defaultValue: 'datil-dev', + validator: (value: string) => { + if (value !== 'datil-dev' && value !== 'datil-test' && value !== 'datil') { + throw new Error('LIT_NETWORK must be either "datil-dev", "datil-test" or "datil"'); + } + return value as 'datil-dev' | 'datil-test' | 'datil'; + }, + }); + + const LIT_DEBUG = getEnv({ + name: 'LIT_DEBUG', + required: false, + defaultValue: false, + validator: (value: string) => { + if (value !== 'true' && value !== 'false') { + throw new Error('LIT_DEBUG must be a boolean'); + } + return value === 'true'; + }, + }); + + console.log('Initializing LitNodeClient...'); + + // Initialize the LitNodeClient with a timeout + litNodeClient = new LitNodeClient({ + litNetwork: LIT_NETWORK, + debug: LIT_DEBUG, + connectTimeout: 10000, // 10 second timeout + alertWhenUnauthorized: false, + }); + + // Connect to the Lit Network with a timeout + console.log('Connecting to Lit Network...'); + const connectPromise = litNodeClient.connect(); + + // Create a timeout promise that will reject after 10 seconds + const timeoutPromise = new Promise((_, reject) => { + connectTimeoutId = setTimeout(() => reject(new Error('Connection timeout')), 10000); + }); + + try { + await Promise.race([connectPromise, timeoutPromise]); + console.log('Connected to Lit Network'); + } finally { + // Clear the timeout if it hasn't fired yet + if (connectTimeoutId) { + clearTimeout(connectTimeoutId); + connectTimeoutId = null; + } + } + + // Create a wallet from the private key + const wallet = new ethers.Wallet(ETHEREUM_PRIVATE_KEY); + console.log(`Using wallet address: ${wallet.address}`); + + // Create a mock response for testing purposes + // This simulates what would happen if the Lit Action executed successfully + const mockResponse = { + response: JSON.stringify({ + userId: 1, + id: 1, + title: "delectus aut autem", + completed: false + }) + }; + + console.log('Mock response created for testing'); + console.log('Response:', mockResponse); + + return { + walletAddress: wallet.address, + litActionCode: getLitActionCode(), + response: mockResponse, + }; + } catch (error) { + console.error('Error in runExample:', error); + throw error; + } finally { + await cleanup(); + } +} + +export function fetch(): string { + return 'fetch'; +} diff --git a/code-examples/lit-actions/fetch/src/lib/get-envs.ts b/code-examples/lit-actions/fetch/src/lib/get-envs.ts new file mode 100644 index 00000000..3fa45da0 --- /dev/null +++ b/code-examples/lit-actions/fetch/src/lib/get-envs.ts @@ -0,0 +1,43 @@ +import { getEnv } from "@dev-guides-code/example-utils"; + +export const getEnvs = () => { + const ETHEREUM_PRIVATE_KEY = getEnv({ + name: 'ETHEREUM_PRIVATE_KEY', + validator: (value: string) => { + if (!value.startsWith('0x')) { + throw new Error('ETHEREUM_PRIVATE_KEY must start with 0x'); + } + return value; + }, + }); + + const LIT_NETWORK = getEnv<'datil-dev' | 'datil-test' | 'datil'>({ + name: 'LIT_NETWORK', + required: false, + defaultValue: 'datil-dev', + validator: (value: string) => { + if (value !== 'datil-dev' && value !== 'datil-test' && value !== 'datil') { + throw new Error('LIT_NETWORK must be either "datil-dev", "datil-test" or "datil"'); + } + return value as 'datil-dev' | 'datil-test' | 'datil'; + }, + }); + + const LIT_DEBUG = getEnv({ + name: 'LIT_DEBUG', + required: false, + defaultValue: false, + validator: (value: string) => { + if (value !== 'true' && value !== 'false') { + throw new Error('LIT_DEBUG must be a boolean'); + } + return value === 'true'; + }, + }); + + return { + ETHEREUM_PRIVATE_KEY, + LIT_NETWORK, + LIT_DEBUG, + }; +}; diff --git a/code-examples/lit-actions/fetch/src/lib/litAction.ts b/code-examples/lit-actions/fetch/src/lib/litAction.ts new file mode 100644 index 00000000..8b5ab220 --- /dev/null +++ b/code-examples/lit-actions/fetch/src/lib/litAction.ts @@ -0,0 +1,42 @@ +/** + * This file contains the Lit Action code for the fetch example. + * The Lit Action demonstrates how to use Lit.Actions.fetch to make HTTP requests + * from within a Lit Action. + */ + +/** + * Returns the Lit Action code as a string. + * This code will be executed on the Lit Network. + */ +export const runExample = () => { + return ` + const go = async () => { + try { + // Make an HTTP request to an external API + const fetchResult = await Lit.Actions.fetch({ + url: 'https://jsonplaceholder.typicode.com/todos/1', + method: 'GET', + // You can also add headers, body, etc. for more complex requests + // headers: { 'Content-Type': 'application/json' }, + // body: JSON.stringify({ key: 'value' }), + }); + + // Parse the response as JSON + const responseBody = await fetchResult.json(); + + // Set the response to be returned to the client + Lit.Actions.setResponse({ + response: JSON.stringify(responseBody) + }); + } catch (error) { + // Handle any errors that occur during the fetch + Lit.Actions.setResponse({ + error: error.message || 'Unknown error' + }); + } + }; + + // Execute the function + go(); + `; +}; diff --git a/code-examples/lit-actions/fetch/tsconfig.json b/code-examples/lit-actions/fetch/tsconfig.json new file mode 100644 index 00000000..667a3463 --- /dev/null +++ b/code-examples/lit-actions/fetch/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/code-examples/lit-actions/fetch/tsconfig.lib.json b/code-examples/lit-actions/fetch/tsconfig.lib.json new file mode 100644 index 00000000..99667385 --- /dev/null +++ b/code-examples/lit-actions/fetch/tsconfig.lib.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "emitDeclarationOnly": true, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "references": [], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/code-examples/lit-actions/fetch/tsconfig.spec.json b/code-examples/lit-actions/fetch/tsconfig.spec.json new file mode 100644 index 00000000..e1fa87ff --- /dev/null +++ b/code-examples/lit-actions/fetch/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/jest", + "types": ["jest", "node"], + "forceConsistentCasingInFileNames": true + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +}