diff --git a/local-tests/setup/tinny-environment.ts b/local-tests/setup/tinny-environment.ts index 21daf4c6c9..50c2c5552c 100644 --- a/local-tests/setup/tinny-environment.ts +++ b/local-tests/setup/tinny-environment.ts @@ -181,7 +181,6 @@ export class TinnyEnvironment { return { privateKey: this.processEnvs.PRIVATE_KEYS[index], index }; // Return the key and its index } else { console.log('[𐬺🧪 Tinny Environment𐬺] No available keys. Waiting...', { - privateKeys: this.processEnvs.PRIVATE_KEYS, keysInUse: this.processEnvs.KEY_IN_USE, }); // Log a message indicating that we are waiting // Wait for the specified interval before checking again @@ -447,8 +446,9 @@ export class TinnyEnvironment { * @throws If there is an error sending the funds. */ getFunds = async (walletAddress: string, amount = '0.001') => { + const privateKey = await this.getAvailablePrivateKey(); + try { - const privateKey = await this.getAvailablePrivateKey(); const provider = new ethers.providers.JsonRpcBatchProvider(this.rpc); const wallet = new ethers.Wallet(privateKey.privateKey, provider); @@ -460,6 +460,9 @@ export class TinnyEnvironment { await tx.wait(); } catch (e) { throw new Error(`Failed to send funds to ${walletAddress}: ${e}`); + } finally { + // @ts-expect-error We don't have a user, but this works + this.releasePrivateKeyFromUser({ privateKey }); } }; diff --git a/local-tests/test.ts b/local-tests/test.ts index 1c43482116..431460344b 100644 --- a/local-tests/test.ts +++ b/local-tests/test.ts @@ -104,6 +104,13 @@ import { testFailImportWrappedKeysWithExpiredSessionSig } from './tests/wrapped- import { testExportWrappedKey } from './tests/wrapped-keys/testExportWrappedKey'; import { testSignMessageWithSolanaEncryptedKey } from './tests/wrapped-keys/testSignMessageWithSolanaEncryptedKey'; import { testSignTransactionWithSolanaEncryptedKey } from './tests/wrapped-keys/testSignTransactionWithSolanaEncryptedKey'; +import { testBatchGeneratePrivateKeys } from './tests/wrapped-keys/testBatchGeneratePrivateKeys'; + +import { setLitActionsCodeToLocal } from './tests/wrapped-keys/util'; +import { testUseEoaSessionSigsToRequestSingleResponse } from './tests/testUseEoaSessionSigsToRequestSingleResponse'; + +// Use the current LIT action code to test against +setLitActionsCodeToLocal(); (async () => { console.log('[𐬺🧪 Tinny𐬺] Running tests...'); @@ -118,6 +125,7 @@ import { testSignTransactionWithSolanaEncryptedKey } from './tests/wrapped-keys/ // --filter=WrappedKey const wrappedKeysTests = { // -- valid cases + testBatchGeneratePrivateKeys, testEthereumSignMessageGeneratedKey, testEthereumBroadcastTransactionGeneratedKey, testEthereumSignMessageWrappedKey, @@ -163,6 +171,7 @@ import { testSignTransactionWithSolanaEncryptedKey } from './tests/wrapped-keys/ testUseEoaSessionSigsToEncryptDecryptString, testUseEoaSessionSigsToEncryptDecryptFile, testUseEoaSessionSigsToEncryptDecryptZip, + testUseEoaSessionSigsToRequestSingleResponse, }; const pkpSessionSigsTests = { diff --git a/local-tests/tests/testUseEoaSessionSigsToRequestSingleResponse.ts b/local-tests/tests/testUseEoaSessionSigsToRequestSingleResponse.ts new file mode 100644 index 0000000000..69583ea7c6 --- /dev/null +++ b/local-tests/tests/testUseEoaSessionSigsToRequestSingleResponse.ts @@ -0,0 +1,57 @@ +import { getEoaSessionSigs } from 'local-tests/setup/session-sigs/get-eoa-session-sigs'; +import { TinnyEnvironment } from 'local-tests/setup/tinny-environment'; + +/** + * Test Commands: + * ✅ NETWORK=datil-dev yarn test:local --filter=testUseEoaSessionSigsToRequestSingleResponse + * ✅ NETWORK=datil-test yarn test:local --filter=testUseEoaSessionSigsToRequestSingleResponse + * ✅ NETWORK=datil yarn test:local --filter=testUseEoaSessionSigsToRequestSingleResponse + */ +export const testUseEoaSessionSigsToRequestSingleResponse = async ( + devEnv: TinnyEnvironment +) => { + const alice = await devEnv.createRandomPerson(); + + try { + const eoaSessionSigs = await getEoaSessionSigs(devEnv, alice); + + const res = await devEnv.litNodeClient.executeJs({ + sessionSigs: eoaSessionSigs, + code: `(async () => { + console.log('hello world') + })();`, + useSingleNode: true, + }); + + console.log('res:', res); + + // Expected output: + // { + // success: true, + // signedData: {}, + // decryptedData: {}, + // claimData: {}, + // response: "", + // logs: "hello world\n", + // } + + // -- assertions + if (res.response) { + throw new Error(`Expected "response" to be falsy`); + } + + if (!res.logs) { + throw new Error(`Expected "logs" in res`); + } + + if (!res.logs.includes('hello world')) { + throw new Error(`Expected "logs" to include 'hello world'`); + } + + if (!res.success) { + throw new Error(`Expected "success" in res`); + } + } finally { + devEnv.releasePrivateKeyFromUser(alice); + } +}; diff --git a/local-tests/tests/wrapped-keys/testBatchGeneratePrivateKeys.ts b/local-tests/tests/wrapped-keys/testBatchGeneratePrivateKeys.ts new file mode 100644 index 0000000000..698ef23919 --- /dev/null +++ b/local-tests/tests/wrapped-keys/testBatchGeneratePrivateKeys.ts @@ -0,0 +1,139 @@ +import { log } from '@lit-protocol/misc'; +import { TinnyEnvironment } from 'local-tests/setup/tinny-environment'; +import { api } from '@lit-protocol/wrapped-keys'; +import { getPkpSessionSigs } from 'local-tests/setup/session-sigs/get-pkp-session-sigs'; +import nacl from 'tweetnacl'; +import bs58 from 'bs58'; +import { ethers } from 'ethers'; +import { BatchGeneratePrivateKeysActionResult } from '../../../packages/wrapped-keys/src/lib/types'; + +const { batchGeneratePrivateKeys, exportPrivateKey } = api; + +async function verifySolanaSignature( + solanaResult: BatchGeneratePrivateKeysActionResult, + solanaMessageToSign +) { + const { + signMessage: { signature }, + generateEncryptedPrivateKey: { generatedPublicKey }, + } = solanaResult; + const signatureIsValidForPublicKey = nacl.sign.detached.verify( + Buffer.from(solanaMessageToSign), + bs58.decode(signature), + bs58.decode(generatedPublicKey) + ); + + console.log({ signatureIsValidForPublicKey, signature }); + if (!signatureIsValidForPublicKey) { + throw new Error( + `signature: ${signature} doesn't validate for the Solana public key: ${generatedPublicKey}` + ); + } +} +async function verifyEvmSignature(evmResult, messageToSign) { + function verifyMessageSignature() { + try { + return ethers.utils.verifyMessage( + messageToSign, + evmResult.signMessage.signature + ); + } catch (err) { + throw new Error( + `When validating signed Ethereum message is valid: ${err.message}` + ); + } + } + + const walletAddress = ethers.utils.computeAddress( + evmResult.generateEncryptedPrivateKey.generatedPublicKey + ); + + const recoveredAddress = verifyMessageSignature(); + + console.log({ + recoveredAddress, + walletAddress, + signature: evmResult.signMessage.signature, + }); + if (recoveredAddress !== walletAddress) { + throw new Error( + "Recovered address from verifyMessage doesn't match the wallet address" + ); + } +} + +/** + * Test Commands: + * ✅ NETWORK=datil-dev yarn test:local --filter=testSignMessageWithSolanaEncryptedKey + * ✅ NETWORK=datil-test yarn test:local --filter=testSignMessageWithSolanaEncryptedKey + * ✅ NETWORK=localchain yarn test:local --filter=testSignMessageWithSolanaEncryptedKey + */ +export const testBatchGeneratePrivateKeys = async ( + devEnv: TinnyEnvironment +) => { + const alice = await devEnv.createRandomPerson(); + + try { + const pkpSessionSigsSigning = await getPkpSessionSigs( + devEnv, + alice, + null, + new Date(Date.now() + 1000 * 60 * 10).toISOString() + ); // 10 mins expiry + + const solanaMessageToSign = 'This is a test solana message'; + const evmMessageToSign = 'This is a test evm message'; + const { results } = await batchGeneratePrivateKeys({ + pkpSessionSigs: pkpSessionSigsSigning, + actions: [ + { + network: 'evm', + signMessageParams: { messageToSign: evmMessageToSign }, + generateKeyParams: { memo: 'Test evm key' }, + }, + { + network: 'solana', + signMessageParams: { messageToSign: solanaMessageToSign }, + generateKeyParams: { memo: 'Test solana key' }, + }, + ], + litNodeClient: devEnv.litNodeClient, + }); + + if (results.length !== 2) { + throw new Error( + `Incorrect # of results; expected 2, got ${results.length}` + ); + } + + if ( + results[0].generateEncryptedPrivateKey.memo !== 'Test evm key' || + results[1].generateEncryptedPrivateKey.memo !== 'Test solana key' + ) { + throw new Error( + 'Results not in order sent; expected evm as first result, solana as second' + ); + } + + if ( + !results[0].signMessage.signature || + !results[1].signMessage.signature + ) { + throw new Error('Missing message signature in response'); + } + + console.log('solana verify sig'); + await verifySolanaSignature(results[1], solanaMessageToSign); + + console.log('evm verify sig'); + await verifyEvmSignature(results[0], evmMessageToSign); + console.log('results', results); + + log('✅ testBatchGenerateEncryptedKeys'); + } catch (err) { + console.log(err.message, err, err.stack); + throw err; + } finally { + devEnv.releasePrivateKeyFromUser(alice); + } +}; diff --git a/local-tests/tests/wrapped-keys/testFailEthereumSignTransactionWrappedKeyWithInvalidParam.ts b/local-tests/tests/wrapped-keys/testFailEthereumSignTransactionWrappedKeyWithInvalidParam.ts index f2e1ac8feb..8aaccca971 100644 --- a/local-tests/tests/wrapped-keys/testFailEthereumSignTransactionWrappedKeyWithInvalidParam.ts +++ b/local-tests/tests/wrapped-keys/testFailEthereumSignTransactionWrappedKeyWithInvalidParam.ts @@ -70,11 +70,7 @@ export const testFailEthereumSignTransactionWrappedKeyWithInvalidParam = async ( id, }); } catch (e: any) { - if ( - e.message.includes( - 'Error executing the Signing Lit Action: Error: When signing transaction- invalid hexlify value' - ) - ) { + if (e.message.includes('invalid hexlify value')) { console.log('✅ THIS IS EXPECTED: ', e); console.log(e.message); console.log( diff --git a/local-tests/tests/wrapped-keys/testFailEthereumSignTransactionWrappedKeyWithMissingParam.ts b/local-tests/tests/wrapped-keys/testFailEthereumSignTransactionWrappedKeyWithMissingParam.ts index c66d81c44d..f23c0d6910 100644 --- a/local-tests/tests/wrapped-keys/testFailEthereumSignTransactionWrappedKeyWithMissingParam.ts +++ b/local-tests/tests/wrapped-keys/testFailEthereumSignTransactionWrappedKeyWithMissingParam.ts @@ -66,11 +66,7 @@ export const testFailEthereumSignTransactionWrappedKeyWithMissingParam = async ( id, }); } catch (e: any) { - if ( - e.message.includes( - 'Error executing the Signing Lit Action: Error: Missing required field: toAddress' - ) - ) { + if (e.message.includes('Missing required field: toAddress')) { console.log('✅ THIS IS EXPECTED: ', e); console.log(e.message); console.log( diff --git a/local-tests/tests/wrapped-keys/testSignTransactionWithSolanaEncryptedKey.ts b/local-tests/tests/wrapped-keys/testSignTransactionWithSolanaEncryptedKey.ts index f0ac9282cd..55f9bfa402 100644 --- a/local-tests/tests/wrapped-keys/testSignTransactionWithSolanaEncryptedKey.ts +++ b/local-tests/tests/wrapped-keys/testSignTransactionWithSolanaEncryptedKey.ts @@ -11,6 +11,7 @@ import { clusterApiUrl, } from '@solana/web3.js'; import { getPkpSessionSigs } from 'local-tests/setup/session-sigs/get-pkp-session-sigs'; +import { ethers } from 'ethers'; const { importPrivateKey, signTransactionWithEncryptedKey } = api; @@ -125,7 +126,7 @@ export const testSignTransactionWithSolanaEncryptedKey = async ( // const confirmation = await solanaConnection.confirmTransaction(signedTx); // console.log(confirmation); // { context: { slot: 321490379 }, value: { err: null } } - const signatureBuffer = Buffer.from(signedTx, 'base64'); + const signatureBuffer = Buffer.from(ethers.utils.base58.decode(signedTx)); solanaTransaction.addSignature(solanaKeypair.publicKey, signatureBuffer); if (!solanaTransaction.verifySignatures()) { diff --git a/local-tests/tests/wrapped-keys/util.ts b/local-tests/tests/wrapped-keys/util.ts index 68bf85f8dc..4f3573fe33 100644 --- a/local-tests/tests/wrapped-keys/util.ts +++ b/local-tests/tests/wrapped-keys/util.ts @@ -1,7 +1,50 @@ import { LIT_NETWORKS_KEYS } from '@lit-protocol/types'; import { LIT_CHAINS } from '@lit-protocol/constants'; import { ethers } from 'ethers'; -import { EthereumLitTransaction } from '@lit-protocol/wrapped-keys'; +import { config } from '@lit-protocol/wrapped-keys'; +import { + litActionRepositoryCommon, + litActionRepository, +} from '@lit-protocol/wrapped-keys-lit-actions'; + +import type { + LitActionCodeRepository, + LitActionCodeRepositoryCommon, + EthereumLitTransaction, +} from '@lit-protocol/wrapped-keys'; + +const emptyLitActionRepositoryCommon: LitActionCodeRepositoryCommon = { + batchGenerateEncryptedKeys: '', +}; + +const emptyLitActionRepository: LitActionCodeRepository = { + signTransaction: { + evm: '', + solana: '', + }, + signMessage: { + evm: '', + solana: '', + }, + generateEncryptedKey: { + evm: '', + solana: '', + }, + exportPrivateKey: { + evm: '', + solana: '', + }, +}; + +export function resetLitActionsCode() { + config.setLitActionsCodeCommon(emptyLitActionRepositoryCommon); + config.setLitActionsCode(emptyLitActionRepository); +} + +export function setLitActionsCodeToLocal() { + config.setLitActionsCodeCommon(litActionRepositoryCommon); + config.setLitActionsCode(litActionRepository); +} export function getChainForNetwork(network: LIT_NETWORKS_KEYS): { chain: string; diff --git a/package.json b/package.json index bd2621b297..a4d1e90913 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "@walletconnect/utils": "2.9.2", "@walletconnect/web3wallet": "1.8.8", "ajv": "^8.12.0", - "axios": "^0.27.2", "base64url": "^3.0.1", "bitcoinjs-lib": "^6.1.0", "bs58": "^5.0.0", @@ -100,6 +99,7 @@ "@types/secp256k1": "^4.0.6", "@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/parser": "6.21.0", + "axios": "^1.6.0", "babel-jest": "27.5.1", "body-parser": "^1.20.2", "buffer": "^6.0.3", @@ -120,6 +120,7 @@ "eslint-plugin-react-hooks": "4.6.0", "ethereum-abi-types-generator": "^1.3.2", "express": "^4.18.2", + "form-data": "^4.0.0", "inquirer": "^9.2.21", "ipfs-unixfs-importer": "12.0.1", "jest": "27.5.1", diff --git a/packages/core/src/lib/lit-core.ts b/packages/core/src/lib/lit-core.ts index 074fdc107d..51b267c1a6 100644 --- a/packages/core/src/lib/lit-core.ts +++ b/packages/core/src/lib/lit-core.ts @@ -36,6 +36,7 @@ import { loadModules, unloadModules, } from '@lit-protocol/crypto'; +import { LogLevel } from '@lit-protocol/logger'; import { bootstrapLogManager, isBrowser, @@ -71,7 +72,6 @@ import { } from '@lit-protocol/types'; import { composeLitUrl } from './endpoint-version'; -import { LogLevel } from '@lit-protocol/logger'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type Listener = (...args: any[]) => void; @@ -1068,6 +1068,18 @@ export class LitCore { return nodePromises; }; + getRandomNodePromise( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback: (url: string) => Promise + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise[] { + const randomNodeIndex = Math.floor( + Math.random() * this.connectedNodes.size + ); + + const nodeUrlsArr = Array.from(this.connectedNodes); + return [callback(nodeUrlsArr[randomNodeIndex])]; + } /** * Retrieves the session signature for a given URL from the sessionSigs map. * Throws an error if sessionSigs is not provided or if the session signature for the URL is not found. diff --git a/packages/lit-node-client-nodejs/src/lib/helpers/get-signatures.ts b/packages/lit-node-client-nodejs/src/lib/helpers/get-signatures.ts index 4d25dc265f..0ecab7084e 100644 --- a/packages/lit-node-client-nodejs/src/lib/helpers/get-signatures.ts +++ b/packages/lit-node-client-nodejs/src/lib/helpers/get-signatures.ts @@ -146,7 +146,7 @@ export const getSignatures = (params: { if (allKeys.length !== initialKeys.length) { throwError({ - message: 'total number of valid signatures does not match requested', + message: `Total number of valid signatures does not match requested. Valid signatures: ${allKeys.length}, Requested signatures: ${initialKeys.length}`, errorKind: LIT_ERROR.NO_VALID_SHARES.kind, errorCode: LIT_ERROR.NO_VALID_SHARES.code, }); diff --git a/packages/lit-node-client-nodejs/src/lib/lit-node-client-nodejs.ts b/packages/lit-node-client-nodejs/src/lib/lit-node-client-nodejs.ts index db31ff00f7..858a46d065 100644 --- a/packages/lit-node-client-nodejs/src/lib/lit-node-client-nodejs.ts +++ b/packages/lit-node-client-nodejs/src/lib/lit-node-client-nodejs.ts @@ -996,6 +996,29 @@ export class LitNodeClientNodeJs throw new Error('All IPFS gateways failed to fetch the code.'); } + private async executeJsNodeRequest( + url: string, + formattedParams: JsonExecutionSdkParams, + requestId: string + ) { + // -- choose the right signature + const sessionSig = this.getSessionSigByUrl({ + sessionSigs: formattedParams.sessionSigs, + url, + }); + + const reqBody: JsonExecutionRequest = { + ...formattedParams, + authSig: sessionSig, + }; + + const urlWithPath = composeLitUrl({ + url, + endpoint: LIT_ENDPOINT.EXECUTE_JS, + }); + + return this.generatePromise(urlWithPath, reqBody, requestId); + } /** * * Execute JS on the nodes and combine and return any resulting signatures @@ -1074,31 +1097,24 @@ export class LitNodeClientNodeJs const requestId = this.getRequestId(); // ========== Get Node Promises ========== // Handle promises for commands sent to Lit nodes - const nodePromises = this.getNodePromises(async (url: string) => { - // -- choose the right signature - const sessionSig = this.getSessionSigByUrl({ - sessionSigs: formattedParams.sessionSigs, - url, - }); - - const reqBody: JsonExecutionRequest = { - ...formattedParams, - authSig: sessionSig, - }; - - const urlWithPath = composeLitUrl({ - url, - endpoint: LIT_ENDPOINT.EXECUTE_JS, - }); + const getNodePromises = async () => { + if (params.useSingleNode) { + return this.getRandomNodePromise((url: string) => + this.executeJsNodeRequest(url, formattedParams, requestId) + ); + } + return this.getNodePromises((url: string) => + this.executeJsNodeRequest(url, formattedParams, requestId) + ); + }; - return this.generatePromise(urlWithPath, reqBody, requestId); - }); + const nodePromises = await getNodePromises(); // -- resolve promises const res = await this.handleNodePromises( nodePromises, requestId, - this.connectedNodes.size + params.useSingleNode ? 1 : this.connectedNodes.size ); // -- case: promises rejected @@ -1162,7 +1178,7 @@ export class LitNodeClientNodeJs const signatures = getSignatures({ requestId, networkPubKeySet: this.networkPubKeySet, - minNodeCount: this.config.minNodeCount, + minNodeCount: params.useSingleNode ? 1 : this.config.minNodeCount, signedData: signedDataList, }); diff --git a/packages/types/src/lib/interfaces.ts b/packages/types/src/lib/interfaces.ts index d2a5f865ff..d44c6162cc 100644 --- a/packages/types/src/lib/interfaces.ts +++ b/packages/types/src/lib/interfaces.ts @@ -502,7 +502,8 @@ export interface JsonExecutionSdkParamsTargetNode } export interface JsonExecutionSdkParams - extends Pick { + extends Pick, + ExecuteJsAdvancedOptions { /** * JS code to run on the nodes */ @@ -522,14 +523,24 @@ export interface JsonExecutionSdkParams * auth methods to resolve */ authMethods?: AuthMethod[]; +} +export interface ExecuteJsAdvancedOptions { /** * a strategy for proccessing `reponse` objects returned from the * Lit Action execution context */ responseStrategy?: LitActionResponseStrategy; + /** + * Allow overriding the default `code` property in the `JsonExecutionSdkParams` + */ ipfsOptions?: IpfsOptions; + + /** + * Only run the action on a single node; this will only work if all code in your action is non-interactive + */ + useSingleNode?: boolean; } export interface JsonExecutionRequestTargetNode extends JsonExecutionRequest { @@ -687,7 +698,7 @@ export interface SigShare { bigr?: string; // backward compatibility bigR?: string; publicKey: string; - dataSigned?: string; + dataSigned?: string | 'fail'; siweMessage?: string; sigName?: string; } @@ -704,6 +715,8 @@ export interface PkpSignedData { export interface NodeShare { claimData: any; shareIndex: any; + + // I think this is deprecated unsignedJwt: any; signedData: SigShare; decryptedData: any; diff --git a/packages/wrapped-keys-lit-actions/esbuild.config.js b/packages/wrapped-keys-lit-actions/esbuild.config.js index 908ea24636..7b0ba54732 100644 --- a/packages/wrapped-keys-lit-actions/esbuild.config.js +++ b/packages/wrapped-keys-lit-actions/esbuild.config.js @@ -50,13 +50,14 @@ module.exports = { (async () => { await esbuild.build({ entryPoints: [ - './src/lib/solana/signTransactionWithSolanaEncryptedKey.js', - './src/lib/solana/signMessageWithSolanaEncryptedKey.js', + './src/lib/solana/signTransactionWithEncryptedSolanaKey.js', + './src/lib/solana/signMessageWithEncryptedSolanaKey.js', './src/lib/solana/generateEncryptedSolanaPrivateKey.js', - './src/lib/ethereum/signTransactionWithEthereumEncryptedKey.js', - './src/lib/ethereum/signMessageWithEthereumEncryptedKey.js', + './src/lib/ethereum/signTransactionWithEncryptedEthereumKey.js', + './src/lib/ethereum/signMessageWithEncryptedEthereumKey.js', './src/lib/ethereum/generateEncryptedEthereumPrivateKey.js', './src/lib/common/exportPrivateKey.js', + './src/lib/common/batchGenerateEncryptedKeys.js', ], bundle: true, minify: true, diff --git a/packages/wrapped-keys-lit-actions/src/index.ts b/packages/wrapped-keys-lit-actions/src/index.ts index 1b4f53103b..8ca684ec8c 100644 --- a/packages/wrapped-keys-lit-actions/src/index.ts +++ b/packages/wrapped-keys-lit-actions/src/index.ts @@ -1,12 +1,16 @@ +import * as batchGenerateEncryptedKeys from './generated/common/batchGenerateEncryptedKeys'; import * as exportPrivateKey from './generated/common/exportPrivateKey'; import * as generateEncryptedEthereumPrivateKey from './generated/ethereum/generateEncryptedEthereumPrivateKey'; -import * as signMessageWithEthereumEncryptedKey from './generated/ethereum/signMessageWithEthereumEncryptedKey'; -import * as signTransactionWithEthereumEncryptedKey from './generated/ethereum/signTransactionWithEthereumEncryptedKey'; +import * as signMessageWithEthereumEncryptedKey from './generated/ethereum/signMessageWithEncryptedEthereumKey'; +import * as signTransactionWithEthereumEncryptedKey from './generated/ethereum/signTransactionWithEncryptedEthereumKey'; import * as generateEncryptedSolanaPrivateKey from './generated/solana/generateEncryptedSolanaPrivateKey'; -import * as signMessageWithSolanaEncryptedKey from './generated/solana/signMessageWithSolanaEncryptedKey'; -import * as signTransactionWithSolanaEncryptedKey from './generated/solana/signTransactionWithSolanaEncryptedKey'; +import * as signMessageWithSolanaEncryptedKey from './generated/solana/signMessageWithEncryptedSolanaKey'; +import * as signTransactionWithSolanaEncryptedKey from './generated/solana/signTransactionWithEncryptedSolanaKey'; -import type { LitActionCodeRepository } from '@lit-protocol/wrapped-keys'; +import type { + LitActionCodeRepository, + LitActionCodeRepositoryCommon, +} from '@lit-protocol/wrapped-keys'; const litActionRepository: LitActionCodeRepository = { signTransaction: { @@ -27,8 +31,13 @@ const litActionRepository: LitActionCodeRepository = { }, }; +const litActionRepositoryCommon: LitActionCodeRepositoryCommon = { + batchGenerateEncryptedKeys: batchGenerateEncryptedKeys.code, +}; + export { // Individual exports to allow tree-shaking and only importing the lit actions you need + batchGenerateEncryptedKeys, exportPrivateKey, generateEncryptedEthereumPrivateKey, signMessageWithEthereumEncryptedKey, @@ -36,6 +45,8 @@ export { generateEncryptedSolanaPrivateKey, signMessageWithSolanaEncryptedKey, signTransactionWithSolanaEncryptedKey, + // Full export to bundle all lit actions litActionRepository, + litActionRepositoryCommon, }; diff --git a/packages/wrapped-keys-lit-actions/src/lib/common/batchGenerateEncryptedKeys.js b/packages/wrapped-keys-lit-actions/src/lib/common/batchGenerateEncryptedKeys.js new file mode 100644 index 0000000000..cb8df59981 --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/common/batchGenerateEncryptedKeys.js @@ -0,0 +1,147 @@ +const { encryptPrivateKey } = require('./internal/encryptKey'); +const { + generateEthereumPrivateKey, +} = require('../ethereum/internal/generatePrivateKey'); +const { signMessageEthereumKey } = require('../ethereum/internal/signMessage'); +const { + generateSolanaPrivateKey, +} = require('../solana/internal/generatePrivateKey'); +const { signMessageSolanaKey } = require('../solana/internal/signMessage'); + +/* global accessControlConditions, actions, Lit*/ + +async function processEthereumAction(action) { + const { network, generateKeyParams } = action; + const messageToSign = action.signMessageParams?.messageToSign; + + const ethereumKey = generateEthereumPrivateKey(); + + const [generatedPrivateKey, messageSignature] = await Promise.all([ + encryptPrivateKey({ + accessControlConditions, + publicKey: ethereumKey.publicKey, + privateKey: ethereumKey.privateKey, + }), + messageToSign + ? signMessageEthereumKey({ + messageToSign: messageToSign, + privateKey: ethereumKey.privateKey, + }) + : Promise.resolve(), + ]); + + return { + network, + generateEncryptedPrivateKey: { + ...generatedPrivateKey, + memo: generateKeyParams.memo, + }, + ...(messageSignature + ? { signMessage: { signature: messageSignature } } + : {}), + }; +} + +async function processSolanaAction(action) { + const { network, generateKeyParams } = action; + + const messageToSign = action.signMessageParams?.messageToSign; + + const solanaKey = generateSolanaPrivateKey(); + + const [generatedPrivateKey, messageSignature] = await Promise.all([ + encryptPrivateKey({ + accessControlConditions, + publicKey: solanaKey.publicKey, + privateKey: solanaKey.privateKey, + }), + messageToSign + ? signMessageSolanaKey({ + messageToSign: messageToSign, + privateKey: solanaKey.privateKey, + }) + : Promise.resolve(), + ]); + + return { + network, + generateEncryptedPrivateKey: { + ...generatedPrivateKey, + memo: generateKeyParams.memo, + }, + ...(messageSignature + ? { signMessage: { signature: messageSignature } } + : {}), + }; +} + +async function processActions(actions) { + return Promise.all( + actions.map(async (action, ndx) => { + const { network } = action; + + if (network === 'evm') { + return await processEthereumAction(action, ndx); + } else if (network === 'solana') { + return await processSolanaAction(action, ndx); + } else { + // Just in case :tm: + throw new Error(`Invalid network for action[${ndx}]: ${network}`); + } + }) + ); +} + +function validateParams(actions) { + if (!actions) { + throw new Error('Missing required field: actions'); + } + + if (!actions.length) { + throw new Error('No actions provided (empty array?)'); + } + + actions.forEach((action, ndx) => { + if (!['evm', 'solana'].includes(action.network)) { + throw new Error( + `Invalid field: actions[${ndx}].network: ${action.network}` + ); + } + + if (!action.generateKeyParams) { + throw new Error( + `Missing required field: actions[${ndx}].generateKeyParams` + ); + } + + if (!action.generateKeyParams?.memo) { + throw new Error( + `Missing required field: actions[${ndx}].generateKeyParams.memo` + ); + } + + if (action.signMessageParams && !action.signMessageParams?.messageToSign) { + throw new Error( + `Missing required field: actions[${ndx}].signMessageParams.messageToSign` + ); + } + }); +} + +(async () => { + try { + validateParams(actions); + + const batchGeneratePrivateKeysActionResult = await processActions(actions); + + Lit.Actions.setResponse({ + response: JSON.stringify(batchGeneratePrivateKeysActionResult), + }); + + // 1. Generate both EVM and solana private keys + // 2. Run appropriate signMessage for each key _and_ encrypt the keys for persistence to wrapped-keys backend + // 3. Return results for both signMessage ops and both encrypted key payloads for persistence + } catch (err) { + Lit.Actions.setResponse({ response: `Error: ${err.message}` }); + } +})(); diff --git a/packages/wrapped-keys-lit-actions/src/lib/common/exportPrivateKey.js b/packages/wrapped-keys-lit-actions/src/lib/common/exportPrivateKey.js index ae8d85bd72..c99cb7280b 100644 --- a/packages/wrapped-keys-lit-actions/src/lib/common/exportPrivateKey.js +++ b/packages/wrapped-keys-lit-actions/src/lib/common/exportPrivateKey.js @@ -1,5 +1,10 @@ +const { + getDecryptedKeyToSingleNode, +} = require('./internal/getDecryptedKeyToSingleNode'); const { removeSaltFromDecryptedKey } = require('../utils'); +/* global accessControlConditions, ciphertext, dataToEncryptHash, Lit */ + /** * * Exports the private key after decrypting and removing the salt from it. @@ -13,31 +18,21 @@ const { removeSaltFromDecryptedKey } = require('../utils'); */ (async () => { - let decryptedPrivateKey; try { - decryptedPrivateKey = await Lit.Actions.decryptToSingleNode({ + const decryptedPrivateKey = await getDecryptedKeyToSingleNode({ accessControlConditions, ciphertext, dataToEncryptHash, - chain: 'ethereum', - authSig: null, }); - } catch (err) { - const errorMessage = - 'Error: When decrypting to a single node- ' + err.message; - Lit.Actions.setResponse({ response: errorMessage }); - return; - } - if (!decryptedPrivateKey) { - // Exit the nodes which don't have the decryptedData - return; - } + if (!decryptedPrivateKey) { + // Silently exit on nodes which didn't run the `decryptToSingleNode` code + return; + } - try { const privateKey = removeSaltFromDecryptedKey(decryptedPrivateKey); Lit.Actions.setResponse({ response: privateKey }); } catch (err) { - Lit.Actions.setResponse({ response: err.message }); + Lit.Actions.setResponse({ response: `Error: ${err.message}` }); } })(); diff --git a/packages/wrapped-keys-lit-actions/src/lib/common/internal/encryptKey.js b/packages/wrapped-keys-lit-actions/src/lib/common/internal/encryptKey.js new file mode 100644 index 0000000000..42af63a266 --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/common/internal/encryptKey.js @@ -0,0 +1,24 @@ +import { LIT_PREFIX } from '../../constants'; + +/* global Lit */ + +/** + * @private + * @returns { Promise<{ciphertext: string, dataToEncryptHash: string, publicKey: string}> } - The ciphertext & dataToEncryptHash which are the result of the encryption, and the publicKey of the newly generated Ethers Wrapped Key. + */ +export async function encryptPrivateKey({ + accessControlConditions, + privateKey, + publicKey, +}) { + const { ciphertext, dataToEncryptHash } = await Lit.Actions.encrypt({ + accessControlConditions, + to_encrypt: new TextEncoder().encode(LIT_PREFIX + privateKey), + }); + + return { + ciphertext, + dataToEncryptHash, + publicKey, + }; +} diff --git a/packages/wrapped-keys-lit-actions/src/lib/common/internal/getDecryptedKeyToSingleNode.js b/packages/wrapped-keys-lit-actions/src/lib/common/internal/getDecryptedKeyToSingleNode.js new file mode 100644 index 0000000000..9f94fb0d02 --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/common/internal/getDecryptedKeyToSingleNode.js @@ -0,0 +1,20 @@ +/* global Lit */ + +export async function getDecryptedKeyToSingleNode({ + accessControlConditions, + ciphertext, + dataToEncryptHash, +}) { + try { + // May be undefined, since we're using `decryptToSingleNode` + return await Lit.Actions.decryptToSingleNode({ + accessControlConditions, + ciphertext, + dataToEncryptHash, + chain: 'ethereum', + authSig: null, + }); + } catch (err) { + throw new Error(`When decrypting key to a single node - ${err.message}`); + } +} diff --git a/packages/wrapped-keys-lit-actions/src/lib/constants.js b/packages/wrapped-keys-lit-actions/src/lib/constants.js new file mode 100644 index 0000000000..3e3434d67b --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/constants.js @@ -0,0 +1 @@ +export const LIT_PREFIX = 'lit_'; diff --git a/packages/wrapped-keys-lit-actions/src/lib/ethereum/generateEncryptedEthereumPrivateKey.js b/packages/wrapped-keys-lit-actions/src/lib/ethereum/generateEncryptedEthereumPrivateKey.js index 729b4f8913..2d6bc44f21 100644 --- a/packages/wrapped-keys-lit-actions/src/lib/ethereum/generateEncryptedEthereumPrivateKey.js +++ b/packages/wrapped-keys-lit-actions/src/lib/ethereum/generateEncryptedEthereumPrivateKey.js @@ -1,37 +1,26 @@ +/* global accessControlConditions, Lit */ + /** * - * Generates a random Ethers private key and only allows the provided PKP to to decrypt it + * Generates a random Ethers private key and only allows the provided PKP to decrypt it * * @jsParam pkpAddress - The Eth address of the PKP which is associated with the Wrapped Key * @jsParam accessControlConditions - The access control condition that allows only the pkpAddress to decrypt the Wrapped Key * * @returns { Promise } - Returns a stringified JSON object with ciphertext & dataToEncryptHash which are the result of the encryption. Also returns the publicKey of the newly generated Ethers Wrapped Key. */ +import { generateEthereumPrivateKey } from './internal/generatePrivateKey'; +import { encryptPrivateKey } from '../common/internal/encryptKey'; (async () => { - const LIT_PREFIX = 'lit_'; - - const resp = await Lit.Actions.runOnce( - { waitForResponse: true, name: 'encryptedPrivateKey' }, - async () => { - const wallet = ethers.Wallet.createRandom(); - const privateKey = LIT_PREFIX + wallet.privateKey.toString(); - let utf8Encode = new TextEncoder(); - const to_encrypt = utf8Encode.encode(privateKey); - - const { ciphertext, dataToEncryptHash } = await Lit.Actions.encrypt({ - accessControlConditions, - to_encrypt, - }); - return JSON.stringify({ - ciphertext, - dataToEncryptHash, - publicKey: wallet.publicKey, - }); - } - ); + const { privateKey, publicKey } = generateEthereumPrivateKey(); + const encryptedKeyResult = await encryptPrivateKey({ + accessControlConditions, + privateKey, + publicKey, + }); Lit.Actions.setResponse({ - response: resp, + response: JSON.stringify(encryptedKeyResult), }); })(); diff --git a/packages/wrapped-keys-lit-actions/src/lib/ethereum/internal/generatePrivateKey.js b/packages/wrapped-keys-lit-actions/src/lib/ethereum/internal/generatePrivateKey.js new file mode 100644 index 0000000000..299816e039 --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/ethereum/internal/generatePrivateKey.js @@ -0,0 +1,10 @@ +/* global ethers */ + +export function generateEthereumPrivateKey() { + const wallet = ethers.Wallet.createRandom(); + + return { + privateKey: wallet.privateKey.toString(), + publicKey: wallet.publicKey, + }; +} diff --git a/packages/wrapped-keys-lit-actions/src/lib/ethereum/internal/signMessage.js b/packages/wrapped-keys-lit-actions/src/lib/ethereum/internal/signMessage.js new file mode 100644 index 0000000000..5cc30864f0 --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/ethereum/internal/signMessage.js @@ -0,0 +1,39 @@ +/* global ethers */ + +async function signMessage({ privateKey, messageToSign }) { + try { + const wallet = new ethers.Wallet(privateKey); + const signature = await wallet.signMessage(messageToSign); + + return { signature, walletAddress: wallet.address }; + } catch (err) { + throw new Error(`When signing message - ${err.message}`); + } +} + +function verifyMessageSignature({ messageToSign, signature }) { + try { + return ethers.utils.verifyMessage(messageToSign, signature); + } catch (err) { + throw new Error( + `When validating signed Ethereum message is valid: ${err.message}` + ); + } +} + +export async function signMessageEthereumKey({ privateKey, messageToSign }) { + const { signature, walletAddress } = await signMessage({ + privateKey, + messageToSign, + }); + + const recoveredAddress = verifyMessageSignature({ messageToSign, signature }); + + if (recoveredAddress !== walletAddress) { + throw new Error( + "Recovered address from verifyMessage doesn't match the wallet address" + ); + } + + return signature; +} diff --git a/packages/wrapped-keys-lit-actions/src/lib/ethereum/internal/signTransaction.js b/packages/wrapped-keys-lit-actions/src/lib/ethereum/internal/signTransaction.js new file mode 100644 index 0000000000..192f133692 --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/ethereum/internal/signTransaction.js @@ -0,0 +1,140 @@ +/* global ethers, Lit */ + +export function getValidatedUnsignedTx(unsignedTransaction) { + try { + if (!unsignedTransaction.toAddress) { + throw new Error('Missing required field: toAddress'); + } + + if (!unsignedTransaction.chain) { + throw new Error('Missing required field: chain'); + } + + if (!unsignedTransaction.value) { + throw new Error('Missing required field: value'); + } + + if (!unsignedTransaction.chainId) { + throw new Error('Missing required field: chainId'); + } + + return { + to: unsignedTransaction.toAddress, + value: ethers.utils.hexlify( + ethers.utils.parseEther(unsignedTransaction.value) + ), + chainId: unsignedTransaction.chainId, + data: unsignedTransaction.dataHex, + }; + } catch (err) { + throw new Error(`Invalid unsignedTransaction - ${err.message}`); + } +} + +async function getLatestNonce({ walletAddress, chain }) { + try { + const nonce = await Lit.Actions.getLatestNonce({ + address: walletAddress, + chain: chain, + }); + + return nonce; + } catch (err) { + throw new Error(`Unable to get latest nonce - ${err.message}`); + } +} + +async function getEthersRPCProvider({ chain }) { + try { + const rpcUrl = await Lit.Actions.getRpcUrl({ + chain, + }); + + return new ethers.providers.JsonRpcProvider(rpcUrl); + } catch (err) { + throw new Error(`Getting the rpc for the chain: ${chain} - ${err.message}`); + } +} + +async function getGasPrice({ userProvidedGasPrice, provider }) { + try { + if (userProvidedGasPrice) { + return ethers.utils.parseUnits(userProvidedGasPrice, 'gwei'); + } else { + return await provider.getGasPrice(); + } + } catch (err) { + throw new Error(`When getting gas price - ${err.message}`); + } +} + +async function getGasLimit({ provider, userProvidedGasLimit, validatedTx }) { + if (userProvidedGasLimit) { + return userProvidedGasLimit; + } else { + try { + return await provider.estimateGas(validatedTx); + } catch (err) { + throw new Error(`When estimating gas - ${err.message}`); + } + } +} + +async function signTransaction({ validatedTx, wallet }) { + try { + return await wallet.signTransaction(validatedTx); + } catch (err) { + throw new Error(`When signing transaction - ${err.message}`); + } +} + +async function broadcastTransaction({ provider, signedTx }) { + try { + return await provider.sendTransaction(signedTx); + } catch (err) { + throw new Error(`When sending transaction - ${err.message}`); + } +} + +export async function signTransactionEthereumKey({ + broadcast, + privateKey, + validatedTx, + unsignedTransaction, +}) { + const wallet = new ethers.Wallet(privateKey); + + validatedTx.from = wallet.address; + + const [nonce, provider] = await Promise.all([ + getLatestNonce({ + walletAddress: wallet.address, + chain: unsignedTransaction.chain, + }), + getEthersRPCProvider({ + chain: unsignedTransaction.chain, + }), + ]); + + validatedTx.nonce = nonce; + + validatedTx.gasPrice = await getGasPrice({ + provider, + userProvidedGasPrice: unsignedTransaction.gasPrice, + }); + + validatedTx.gasLimit = await getGasLimit({ + provider, + validatedTx, + userProvidedGasLimit: unsignedTransaction.gasLimit, + }); + + const signedTx = await signTransaction({ validatedTx, wallet }); + + if (!broadcast) { + return signedTx; + } + + const txResponse = await broadcastTransaction({ provider, signedTx }); + return txResponse.hash; +} diff --git a/packages/wrapped-keys-lit-actions/src/lib/ethereum/signMessageWithEncryptedEthereumKey.js b/packages/wrapped-keys-lit-actions/src/lib/ethereum/signMessageWithEncryptedEthereumKey.js new file mode 100644 index 0000000000..6ae679ea2d --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/ethereum/signMessageWithEncryptedEthereumKey.js @@ -0,0 +1,45 @@ +const { signMessageEthereumKey } = require('./internal/signMessage'); +const { + getDecryptedKeyToSingleNode, +} = require('../common/internal/getDecryptedKeyToSingleNode'); +const { removeSaltFromDecryptedKey } = require('../utils'); + +/* global accessControlConditions, ciphertext, dataToEncryptHash, messageToSign, Lit */ + +/** + * Signs a message with the Ethers wallet which is also decrypted inside the Lit Action. + * + * @jsParam pkpAddress - The Eth address of the PKP which is associated with the Wrapped Key + * @jsParam ciphertext - For the encrypted Wrapped Key + * @jsParam dataToEncryptHash - For the encrypted Wrapped Key + * @jsParam messageToSign - The unsigned message to be signed by the Wrapped Key + * @jsParam accessControlConditions - The access control condition that allows only the pkpAddress to decrypt the Wrapped Key + * + * @returns { Promise } - Returns a message signed by the Ethers Wrapped key. Or returns errors if any. + */ + +(async () => { + try { + const decryptedPrivateKey = await getDecryptedKeyToSingleNode({ + accessControlConditions, + ciphertext, + dataToEncryptHash, + }); + + if (!decryptedPrivateKey) { + // Silently exit on nodes which didn't run the `decryptToSingleNode` code + return; + } + + const privateKey = removeSaltFromDecryptedKey(decryptedPrivateKey); + + const signature = await signMessageEthereumKey({ + privateKey, + messageToSign, + }); + + Lit.Actions.setResponse({ response: signature }); + } catch (err) { + Lit.Actions.setResponse({ response: `Error: ${err.message}` }); + } +})(); diff --git a/packages/wrapped-keys-lit-actions/src/lib/ethereum/signMessageWithEthereumEncryptedKey.js b/packages/wrapped-keys-lit-actions/src/lib/ethereum/signMessageWithEthereumEncryptedKey.js deleted file mode 100644 index 25e691906b..0000000000 --- a/packages/wrapped-keys-lit-actions/src/lib/ethereum/signMessageWithEthereumEncryptedKey.js +++ /dev/null @@ -1,67 +0,0 @@ -const { removeSaltFromDecryptedKey } = require('../utils'); - -/** - * - * Signs a message with the Ethers wallet which is also decrypted inside the Lit Action. - * - * @jsParam pkpAddress - The Eth address of the PKP which is associated with the Wrapped Key - * @jsParam ciphertext - For the encrypted Wrapped Key - * @jsParam dataToEncryptHash - For the encrypted Wrapped Key - * @jsParam messageToSign - The unsigned message to be signed by the Wrapped Key - * @jsParam accessControlConditions - The access control condition that allows only the pkpAddress to decrypt the Wrapped Key - * - * @returns { Promise } - Returns a message signed by the Ethers Wrapped key. Or returns errors if any. - */ - -(async () => { - let decryptedPrivateKey; - try { - decryptedPrivateKey = await Lit.Actions.decryptToSingleNode({ - accessControlConditions, - ciphertext, - dataToEncryptHash, - chain: 'ethereum', - authSig: null, - }); - } catch (err) { - const errorMessage = - 'Error: When decrypting to a single node- ' + err.message; - Lit.Actions.setResponse({ response: errorMessage }); - return; - } - - if (!decryptedPrivateKey) { - // Exit the nodes which don't have the decryptedData - return; - } - - let privateKey; - try { - privateKey = removeSaltFromDecryptedKey(decryptedPrivateKey); - } catch (err) { - Lit.Actions.setResponse({ response: err.message }); - return; - } - const wallet = new ethers.Wallet(privateKey); - - try { - const signature = await wallet.signMessage(messageToSign); - - const recoveredAddress = ethers.utils.verifyMessage( - messageToSign, - signature - ); - - if (recoveredAddress !== wallet.address) { - Lit.Actions.setResponse({ - response: "Error: Recovered address doesn't match the wallet address", - }); - return; - } - - Lit.Actions.setResponse({ response: signature }); - } catch (err) { - const errorMessage = 'Error: When signing message- ' + err.message; - Lit.Actions.setResponse({ response: errorMessage }); - } -})(); diff --git a/packages/wrapped-keys-lit-actions/src/lib/ethereum/signTransactionWithEncryptedEthereumKey.js b/packages/wrapped-keys-lit-actions/src/lib/ethereum/signTransactionWithEncryptedEthereumKey.js new file mode 100644 index 0000000000..2f35f19583 --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/ethereum/signTransactionWithEncryptedEthereumKey.js @@ -0,0 +1,55 @@ +const { + signTransactionEthereumKey, + getValidatedUnsignedTx, +} = require('./internal/signTransaction'); +const { + getDecryptedKeyToSingleNode, +} = require('../common/internal/getDecryptedKeyToSingleNode'); +const { removeSaltFromDecryptedKey } = require('../utils'); + +/* global accessControlConditions, ciphertext, dataToEncryptHash, unsignedTransaction, broadcast, Lit */ + +/** + * + * Signs a transaction with the Ethers wallet whose private key is decrypted inside the Lit Action. + * + * @jsParam pkpAddress - The Eth address of the PKP which is associated with the Wrapped Key + * @jsParam ciphertext - For the encrypted Wrapped Key + * @jsParam dataToEncryptHash - For the encrypted Wrapped Key + * @jsParam unsignedTransaction - The unsigned message to be signed by the Wrapped Key + * @jsParam broadcast - Flag used to determine whether to just sign the message or also to broadcast it using the node's RPC. Note, if the RPC doesn't exist for the chain then the Lit Action will throw an unsupported error. + * @jsParam accessControlConditions - The access control condition that allows only the pkpAddress to decrypt the Wrapped Key + * + * @returns { Promise } - Returns the transaction hash if broadcast is set as true else returns only the signed transaction. Or returns errors if any. + */ +(async () => { + try { + const validatedTx = getValidatedUnsignedTx(unsignedTransaction); + + const decryptedPrivateKey = await getDecryptedKeyToSingleNode({ + accessControlConditions, + ciphertext, + dataToEncryptHash, + }); + + if (!decryptedPrivateKey) { + // Silently exit on nodes which didn't run the `decryptToSingleNode` code + return; + } + + const privateKey = removeSaltFromDecryptedKey(decryptedPrivateKey); + + const txResult = await signTransactionEthereumKey({ + broadcast, + privateKey, + unsignedTransaction, + validatedTx, + }); + + Lit.Actions.setResponse({ response: txResult }); + } catch (err) { + Lit.Actions.setResponse({ + response: `Error: ${err.message}`, + }); + } +})(); diff --git a/packages/wrapped-keys-lit-actions/src/lib/ethereum/signTransactionWithEthereumEncryptedKey.js b/packages/wrapped-keys-lit-actions/src/lib/ethereum/signTransactionWithEthereumEncryptedKey.js deleted file mode 100644 index 64f6bcf276..0000000000 --- a/packages/wrapped-keys-lit-actions/src/lib/ethereum/signTransactionWithEthereumEncryptedKey.js +++ /dev/null @@ -1,150 +0,0 @@ -const { removeSaltFromDecryptedKey } = require('../utils'); - -/** - * - * Signs a transaction with the Ethers wallet whose private key is decrypted inside the Lit Action. - * - * @jsParam pkpAddress - The Eth address of the PKP which is associated with the Wrapped Key - * @jsParam ciphertext - For the encrypted Wrapped Key - * @jsParam dataToEncryptHash - For the encrypted Wrapped Key - * @jsParam unsignedTransaction - The unsigned message to be signed by the Wrapped Key - * @jsParam broadcast - Flag used to determine whether to just sign the message or also to broadcast it using the node's RPC. Note, if the RPC doesn't exist for the chain then the Lit Action will throw an unsupported error. - * @jsParam accessControlConditions - The access control condition that allows only the pkpAddress to decrypt the Wrapped Key - * - * @returns { Promise } - Returns the transaction hash if broadcast is set as true else returns only the signed transaction. Or returns errors if any. - */ -(async () => { - if (!unsignedTransaction.toAddress) { - Lit.Actions.setResponse({ - response: 'Error: Missing required field: toAddress', - }); - return; - } - - if (!unsignedTransaction.chain) { - Lit.Actions.setResponse({ - response: 'Error: Missing required field: chain', - }); - return; - } - - if (!unsignedTransaction.value) { - Lit.Actions.setResponse({ - response: 'Error: Missing required field: value', - }); - return; - } - - if (!unsignedTransaction.chainId) { - Lit.Actions.setResponse({ - response: 'Error: Missing required field: chainId', - }); - return; - } - - let decryptedPrivateKey; - try { - decryptedPrivateKey = await Lit.Actions.decryptToSingleNode({ - accessControlConditions, - ciphertext, - dataToEncryptHash, - chain: 'ethereum', - authSig: null, - }); - } catch (err) { - const errorMessage = - 'Error: When decrypting to a single node- ' + err.message; - Lit.Actions.setResponse({ response: errorMessage }); - return; - } - - if (!decryptedPrivateKey) { - // Exit the nodes which don't have the decryptedData - return; - } - - let privateKey; - try { - privateKey = removeSaltFromDecryptedKey(decryptedPrivateKey); - } catch (err) { - Lit.Actions.setResponse({ response: err.message }); - return; - } - const wallet = new ethers.Wallet(privateKey); - - let nonce; - try { - nonce = await Lit.Actions.getLatestNonce({ - address: wallet.address, - chain: unsignedTransaction.chain, - }); - } catch (err) { - const errorMessage = 'Error: Unable to get the nonce- ' + err.message; - Lit.Actions.setResponse({ response: errorMessage }); - return; - } - - const tx = { - to: unsignedTransaction.toAddress, - from: wallet.address, - value: ethers.utils.hexlify( - ethers.utils.parseEther(unsignedTransaction.value) - ), - chainId: unsignedTransaction.chainId, - data: unsignedTransaction.dataHex, - nonce, - }; - - let provider; - try { - const rpcUrl = await Lit.Actions.getRpcUrl({ - chain: unsignedTransaction.chain, - }); - provider = new ethers.providers.JsonRpcProvider(rpcUrl); - } catch (err) { - const errorMessage = - `Error: Getting the rpc for the chain: ${unsignedTransaction.chain}- ` + - err.message; - Lit.Actions.setResponse({ response: errorMessage }); - return; - } - - if (unsignedTransaction.gasPrice) { - tx.gasPrice = ethers.utils.parseUnits(unsignedTransaction.gasPrice, 'gwei'); - } else { - try { - tx.gasPrice = await provider.getGasPrice(); - } catch (err) { - const errorMessage = 'Error: When getting gas price- ' + err.message; - Lit.Actions.setResponse({ response: errorMessage }); - return; - } - } - - if (unsignedTransaction.gasLimit) { - tx.gasLimit = unsignedTransaction.gasLimit; - } else { - try { - tx.gasLimit = await provider.estimateGas(tx); - } catch (err) { - const errorMessage = 'Error: When estimating gas- ' + err.message; - Lit.Actions.setResponse({ response: errorMessage }); - return; - } - } - - try { - const signedTx = await wallet.signTransaction(tx); - - if (broadcast) { - const transactionResponse = await provider.sendTransaction(signedTx); - - Lit.Actions.setResponse({ response: transactionResponse.hash }); - } else { - Lit.Actions.setResponse({ response: signedTx }); - } - } catch (err) { - const errorMessage = 'Error: When signing transaction- ' + err.message; - Lit.Actions.setResponse({ response: errorMessage }); - } -})(); diff --git a/packages/wrapped-keys-lit-actions/src/lib/solana/generateEncryptedSolanaPrivateKey.js b/packages/wrapped-keys-lit-actions/src/lib/solana/generateEncryptedSolanaPrivateKey.js index 4d757f748c..613dd373d3 100644 --- a/packages/wrapped-keys-lit-actions/src/lib/solana/generateEncryptedSolanaPrivateKey.js +++ b/packages/wrapped-keys-lit-actions/src/lib/solana/generateEncryptedSolanaPrivateKey.js @@ -1,40 +1,25 @@ +const { generateSolanaPrivateKey } = require('./internal/generatePrivateKey'); +const { encryptPrivateKey } = require('../common/internal/encryptKey'); + +/* global accessControlConditions, Lit */ + /** - * - * Bundles solana/web3.js package as it's required to generate a random Solana key and only allows the provided PKP to to decrypt it + * Bundles solana/web3.js package as it's required to generate a random Solana key and only allows the provided PKP to decrypt it * * @jsParam pkpAddress - The Eth address of the PKP which is associated with the Wrapped Key * @jsParam accessControlConditions - The access control condition that allows only the pkpAddress to decrypt the Wrapped Key * * @returns { Promise } - Returns a stringified JSON object with ciphertext & dataToEncryptHash which are the result of the encryption. Also returns the publicKey of the newly generated Solana Wrapped Key. */ - -const { Keypair } = require('@solana/web3.js'); - (async () => { - const LIT_PREFIX = 'lit_'; - - const resp = await Lit.Actions.runOnce( - { waitForResponse: true, name: 'encryptedPrivateKey' }, - async () => { - const solanaKeypair = Keypair.generate(); - const privateKey = - LIT_PREFIX + Buffer.from(solanaKeypair.secretKey).toString('hex'); - let utf8Encode = new TextEncoder(); - const to_encrypt = utf8Encode.encode(privateKey); - - const { ciphertext, dataToEncryptHash } = await Lit.Actions.encrypt({ - accessControlConditions, - to_encrypt, - }); - return JSON.stringify({ - ciphertext, - dataToEncryptHash, - publicKey: solanaKeypair.publicKey.toString(), - }); - } - ); + const { privateKey, publicKey } = generateSolanaPrivateKey(); + const encryptedKeyResult = await encryptPrivateKey({ + accessControlConditions, + publicKey, + privateKey, + }); Lit.Actions.setResponse({ - response: resp, + response: JSON.stringify(encryptedKeyResult), }); })(); diff --git a/packages/wrapped-keys-lit-actions/src/lib/solana/internal/generatePrivateKey.js b/packages/wrapped-keys-lit-actions/src/lib/solana/internal/generatePrivateKey.js new file mode 100644 index 0000000000..35c0a4ec46 --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/solana/internal/generatePrivateKey.js @@ -0,0 +1,10 @@ +import { Keypair } from '@solana/web3.js'; + +export function generateSolanaPrivateKey() { + const solanaKeypair = Keypair.generate(); + + return { + privateKey: Buffer.from(solanaKeypair.secretKey).toString('hex'), + publicKey: solanaKeypair.publicKey.toString(), + }; +} diff --git a/packages/wrapped-keys-lit-actions/src/lib/solana/internal/signMessage.js b/packages/wrapped-keys-lit-actions/src/lib/solana/internal/signMessage.js new file mode 100644 index 0000000000..aee2d285cf --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/solana/internal/signMessage.js @@ -0,0 +1,54 @@ +import { Keypair } from '@solana/web3.js'; +import nacl from 'tweetnacl'; + +/* global ethers */ + +function signMessage({ messageToSign, solanaKeyPair }) { + try { + const signature = nacl.sign.detached( + new TextEncoder().encode(messageToSign), + solanaKeyPair.secretKey + ); + + return { signature }; + } catch (err) { + throw new Error(`When signing message - ${err.message}`); + } +} + +function verifyMessageSignature({ signature, solanaKeyPair, messageToSign }) { + try { + const isValid = nacl.sign.detached.verify( + Buffer.from(messageToSign), + signature, + solanaKeyPair.publicKey.toBuffer() + ); + + return isValid; + } catch (err) { + throw new Error( + `When validating signed Solana message is valid: ${err.message}` + ); + } +} + +export async function signMessageSolanaKey({ messageToSign, privateKey }) { + const solanaKeyPair = Keypair.fromSecretKey(Buffer.from(privateKey, 'hex')); + + const { signature } = signMessage({ + messageToSign, + solanaKeyPair, + }); + + const isValid = verifyMessageSignature({ + signature, + solanaKeyPair, + messageToSign, + }); + + if (!isValid) { + throw new Error('Signature did not verify to expected Solana public key'); + } + + return ethers.utils.base58.encode(signature); +} diff --git a/packages/wrapped-keys-lit-actions/src/lib/solana/internal/signTransaction.js b/packages/wrapped-keys-lit-actions/src/lib/solana/internal/signTransaction.js new file mode 100644 index 0000000000..c944426673 --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/solana/internal/signTransaction.js @@ -0,0 +1,69 @@ +const { + clusterApiUrl, + Connection, + Keypair, + Transaction, +} = require('@solana/web3.js'); + +/* global ethers */ + +export function validateUnsignedTransaction(unsignedTransaction) { + const VALID_NETWORKS = ['devnet', 'testnet', 'mainnet-beta']; + + if (!VALID_NETWORKS.includes(unsignedTransaction.chain)) { + throw new Error(`Invalid Solana network: ${unsignedTransaction.chain}`); + } + + if ( + !unsignedTransaction.serializedTransaction || + !unsignedTransaction.serializedTransaction.length === 0 + ) { + throw new Error( + `Invalid serializedTransaction: ${unsignedTransaction.serializedTransaction}` + ); + } +} + +function signTransaction({ solanaKeyPair, transaction }) { + try { + transaction.sign(solanaKeyPair); + + return { signature: ethers.utils.base58.encode(transaction.signature) }; + } catch (err) { + throw new Error(`When signing transaction - ${err.message}`); + } +} + +async function sendTransaction({ chain, transaction }) { + try { + const solanaConnection = new Connection(clusterApiUrl(chain), 'confirmed'); + return await solanaConnection.sendRawTransaction(transaction.serialize()); + } catch (err) { + throw new Error(`When sending transaction - ${err.message}`); + } +} + +export async function signTransactionSolanaKey({ + broadcast, + privateKey, + unsignedTransaction, +}) { + // Be sure you call validateUnsignedTransaction(unsignedTransaction); before calling this method! + + const solanaKeyPair = Keypair.fromSecretKey(Buffer.from(privateKey, 'hex')); + + const transaction = Transaction.from( + Buffer.from(unsignedTransaction.serializedTransaction, 'base64') + ); + + const { signature } = signTransaction({ transaction, solanaKeyPair }); + + if (!broadcast) { + return signature; + } + + return await sendTransaction({ + chain: unsignedTransaction.chain, + transaction, + }); +} diff --git a/packages/wrapped-keys-lit-actions/src/lib/solana/signMessageWithEncryptedSolanaKey.js b/packages/wrapped-keys-lit-actions/src/lib/solana/signMessageWithEncryptedSolanaKey.js new file mode 100644 index 0000000000..776b1fafcb --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/solana/signMessageWithEncryptedSolanaKey.js @@ -0,0 +1,46 @@ +const { signMessageSolanaKey } = require('./internal/signMessage'); +const { + getDecryptedKeyToSingleNode, +} = require('../common/internal/getDecryptedKeyToSingleNode'); +const { removeSaltFromDecryptedKey } = require('../utils'); + +/* global accessControlConditions, ciphertext, dataToEncryptHash, messageToSign, Lit */ + +/** + * + * Bundles solana/web3.js package as it's required to sign a message with the Solana wallet which is also decrypted inside the Lit Action. + * + * @jsParam pkpAddress - The Eth address of the PKP which is associated with the Wrapped Key + * @jsParam ciphertext - For the encrypted Wrapped Key + * @jsParam dataToEncryptHash - For the encrypted Wrapped Key + * @jsParam messageToSign - The unsigned message to be signed by the Wrapped Key + * @jsParam accessControlConditions - The access control condition that allows only the pkpAddress to decrypt the Wrapped Key + * + * @returns { Promise } - Returns a message signed by the Solana Wrapped key. Or returns errors if any. + */ + +(async () => { + try { + const decryptedPrivateKey = await getDecryptedKeyToSingleNode({ + accessControlConditions, + ciphertext, + dataToEncryptHash, + }); + + if (!decryptedPrivateKey) { + // Silently exit on nodes which didn't run the `decryptToSingleNode` code + return; + } + + const privateKey = removeSaltFromDecryptedKey(decryptedPrivateKey); + + const signature = await signMessageSolanaKey({ + messageToSign, + privateKey, + }); + + Lit.Actions.setResponse({ response: signature }); + } catch (err) { + Lit.Actions.setResponse({ response: `Error: ${err.message}` }); + } +})(); diff --git a/packages/wrapped-keys-lit-actions/src/lib/solana/signMessageWithSolanaEncryptedKey.js b/packages/wrapped-keys-lit-actions/src/lib/solana/signMessageWithSolanaEncryptedKey.js deleted file mode 100644 index 6da17a5233..0000000000 --- a/packages/wrapped-keys-lit-actions/src/lib/solana/signMessageWithSolanaEncryptedKey.js +++ /dev/null @@ -1,85 +0,0 @@ -const { Keypair } = require('@solana/web3.js'); -const bs58 = require('bs58'); -const nacl = require('tweetnacl'); - -const { removeSaltFromDecryptedKey } = require('../utils'); - -/** - * - * Bundles solana/web3.js package as it's required to sign a message with the Solana wallet which is also decrypted inside the Lit Action. - * - * @jsParam pkpAddress - The Eth address of the PKP which is associated with the Wrapped Key - * @jsParam ciphertext - For the encrypted Wrapped Key - * @jsParam dataToEncryptHash - For the encrypted Wrapped Key - * @jsParam messageToSign - The unsigned message to be signed by the Wrapped Key - * @jsParam accessControlConditions - The access control condition that allows only the pkpAddress to decrypt the Wrapped Key - * - * @returns { Promise } - Returns a message signed by the Solana Wrapped key. Or returns errors if any. - */ - -(async () => { - let decryptedPrivateKey; - try { - decryptedPrivateKey = await Lit.Actions.decryptToSingleNode({ - accessControlConditions, - chain: 'ethereum', - ciphertext, - dataToEncryptHash, - authSig: null, - }); - } catch (error) { - Lit.Actions.setResponse({ - response: `Error: When decrypting data to private key: ${error.message}`, - }); - return; - } - - if (!decryptedPrivateKey) { - // Exit the nodes which don't have the decryptedData - return; - } - - let privateKey; - try { - privateKey = removeSaltFromDecryptedKey(decryptedPrivateKey); - } catch (err) { - Lit.Actions.setResponse({ response: err.message }); - return; - } - const solanaKeyPair = Keypair.fromSecretKey(Buffer.from(privateKey, 'hex')); - - let signature; - try { - signature = nacl.sign.detached( - new TextEncoder().encode(messageToSign), - solanaKeyPair.secretKey - ); - } catch (error) { - Lit.Actions.setResponse({ - response: `Error: When signing message: ${error.message}`, - }); - return; - } - - try { - const isValid = nacl.sign.detached.verify( - Buffer.from(messageToSign), - signature, - solanaKeyPair.publicKey.toBuffer() - ); - if (!isValid) { - Lit.Actions.setResponse({ - response: - 'Error: Signature did not verify to expected Solana public key', - }); - return; - } - } catch (error) { - Lit.Actions.setResponse({ - response: `Error: When validating signed message is valid: ${error.message}`, - }); - return; - } - - Lit.Actions.setResponse({ response: bs58.encode(signature) }); -})(); diff --git a/packages/wrapped-keys-lit-actions/src/lib/solana/signTransactionWithEncryptedSolanaKey.js b/packages/wrapped-keys-lit-actions/src/lib/solana/signTransactionWithEncryptedSolanaKey.js new file mode 100644 index 0000000000..5a243e9c42 --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/solana/signTransactionWithEncryptedSolanaKey.js @@ -0,0 +1,55 @@ +const { + signTransactionSolanaKey, + validateUnsignedTransaction, +} = require('./internal/signTransaction'); +const { + getDecryptedKeyToSingleNode, +} = require('../common/internal/getDecryptedKeyToSingleNode'); +const { removeSaltFromDecryptedKey } = require('../utils'); + +/* global accessControlConditions, ciphertext, dataToEncryptHash, unsignedTransaction, Lit, broadcast */ + +/** + * + * Bundles solana/web3.js package as it's required to sign a transaction with the Solana wallet which is also decrypted inside the Lit Action. + * + * @jsParam pkpAddress - The Eth address of the PKP which is associated with the Wrapped Key + * @jsParam ciphertext - For the encrypted Wrapped Key + * @jsParam dataToEncryptHash - For the encrypted Wrapped Key + * @jsParam unsignedTransaction - The unsigned message to be signed by the Wrapped Key + * @jsParam broadcast - Flag used to determine whether to just sign the message or also to broadcast it using the node's RPC. + * @jsParam accessControlConditions - The access control condition that allows only the pkpAddress to decrypt the Wrapped Key + * + * @returns { Promise } - Returns the transaction signature. Or returns errors if any. + */ + +(async () => { + try { + validateUnsignedTransaction(unsignedTransaction); + + const decryptedPrivateKey = await getDecryptedKeyToSingleNode({ + accessControlConditions, + ciphertext, + dataToEncryptHash, + }); + + if (!decryptedPrivateKey) { + // Silently exit on nodes which didn't run the `decryptToSingleNode` code + return; + } + + const privateKey = removeSaltFromDecryptedKey(decryptedPrivateKey); + + const txResult = await signTransactionSolanaKey({ + broadcast, + privateKey, + unsignedTransaction, + }); + + Lit.Actions.setResponse({ response: txResult }); + } catch (err) { + Lit.Actions.setResponse({ + response: `Error: ${err.message}`, + }); + } +})(); diff --git a/packages/wrapped-keys-lit-actions/src/lib/solana/signTransactionWithSolanaEncryptedKey.js b/packages/wrapped-keys-lit-actions/src/lib/solana/signTransactionWithSolanaEncryptedKey.js deleted file mode 100644 index 25cd879464..0000000000 --- a/packages/wrapped-keys-lit-actions/src/lib/solana/signTransactionWithSolanaEncryptedKey.js +++ /dev/null @@ -1,103 +0,0 @@ -const { - clusterApiUrl, - Connection, - Keypair, - Transaction, -} = require('@solana/web3.js'); - -const { removeSaltFromDecryptedKey } = require('../utils'); - -/** - * - * Bundles solana/web3.js package as it's required to sign a transaction with the Solana wallet which is also decrypted inside the Lit Action. - * - * @jsParam pkpAddress - The Eth address of the PKP which is associated with the Wrapped Key - * @jsParam ciphertext - For the encrypted Wrapped Key - * @jsParam dataToEncryptHash - For the encrypted Wrapped Key - * @jsParam unsignedTransaction - The unsigned message to be signed by the Wrapped Key - * @jsParam broadcast - Flag used to determine whether to just sign the message or also to broadcast it using the node's RPC. - * @jsParam accessControlConditions - The access control condition that allows only the pkpAddress to decrypt the Wrapped Key - * - * @returns { Promise } - Returns the transaction signature. Or returns errors if any. - */ - -(async () => { - const VALID_NETWORKS = ['devnet', 'testnet', 'mainnet-beta']; - - if (!VALID_NETWORKS.includes(unsignedTransaction.chain)) { - Lit.Actions.setResponse({ - response: `Error: Invalid Solana network: ${unsignedTransaction.chain}`, - }); - return; - } - - if ( - !unsignedTransaction.serializedTransaction || - !unsignedTransaction.serializedTransaction.length === 0 - ) { - Lit.Actions.setResponse({ - response: `Error: Invalid serializedTransaction: ${unsignedTransaction.serializedTransaction}`, - }); - return; - } - - let decryptedPrivateKey; - try { - decryptedPrivateKey = await Lit.Actions.decryptToSingleNode({ - accessControlConditions, - chain: 'ethereum', - ciphertext, - dataToEncryptHash, - authSig: null, - }); - } catch (error) { - Lit.Actions.setResponse({ - response: `Error: When decrypting data to private key: ${error.message}`, - }); - return; - } - - if (!decryptedPrivateKey) { - // Exit the nodes which don't have the decryptedData - return; - } - - let privateKey; - try { - privateKey = removeSaltFromDecryptedKey(decryptedPrivateKey); - } catch (err) { - Lit.Actions.setResponse({ response: err.message }); - return; - } - - const solanaKeyPair = Keypair.fromSecretKey( - Uint8Array.from(Buffer.from(privateKey, 'hex')) - ); - - try { - const transaction = Transaction.from( - Buffer.from(unsignedTransaction.serializedTransaction, 'base64') - ); - - transaction.sign(solanaKeyPair); - const signature = ethers.utils.base58.encode(transaction.signature); - - if (broadcast) { - const solanaConnection = new Connection( - clusterApiUrl(unsignedTransaction.chain), - 'confirmed' - ); - const transactionSignature = await solanaConnection.sendRawTransaction( - transaction.serialize() - ); - - Lit.Actions.setResponse({ response: transactionSignature }); - } else { - Lit.Actions.setResponse({ response: signature }); - } - } catch (error) { - Lit.Actions.setResponse({ - response: `Error: During transaction signing and submission: ${error.message}`, - }); - } -})(); diff --git a/packages/wrapped-keys-lit-actions/src/lib/utils.js b/packages/wrapped-keys-lit-actions/src/lib/utils.js index 34c7e9be25..4cfc44eecf 100644 --- a/packages/wrapped-keys-lit-actions/src/lib/utils.js +++ b/packages/wrapped-keys-lit-actions/src/lib/utils.js @@ -1,9 +1,9 @@ -const LIT_PREFIX = 'lit_'; +import { LIT_PREFIX } from './constants'; export function removeSaltFromDecryptedKey(decryptedPrivateKey) { if (!decryptedPrivateKey.startsWith(LIT_PREFIX)) { throw new Error( - `Error: PKey was not encrypted with salt; all wrapped keys must be prefixed with '${LIT_PREFIX}'` + `PKey was not encrypted with salt; all wrapped keys must be prefixed with '${LIT_PREFIX}'` ); } diff --git a/packages/wrapped-keys-lit-actions/sync-actions-to-ipfs.js b/packages/wrapped-keys-lit-actions/sync-actions-to-ipfs.js new file mode 100644 index 0000000000..5fbba5d2f0 --- /dev/null +++ b/packages/wrapped-keys-lit-actions/sync-actions-to-ipfs.js @@ -0,0 +1,102 @@ +const axios = require('axios'); +const FormData = require('form-data'); + +const { + litActionRepository, + litActionRepositoryCommon, +} = require('./dist/src/index'); + +/** Usage: + * 1. Ensure you have a valid Pinata IPFS JWT in `LIT_IPFS_JWT` env var + * 2. Make sure you run `yarn build` to ensure that all LIT actions code has been built into the generated directory from the current commit + * 3. `node sync-actions-to-ipfs` -> this will print out JSON of the `LIT_ACTION_CID_REPOSITORY` and LIT_ACTION_CID_REPOSITORY_COMMON + * 4. Copy/paste the CIDs into those objects in `packages/wrapped-keys/src/lib/lit-actions-client/constants.ts` + * 5. Commit the changes and push them to your branch + */ + +const JWT = process.env.LIT_IPFS_JWT || ''; +if (!JWT) { + throw new Error('Missing Pinata IPFS JWT in LIT_IPFS_JWT env variable'); +} + +async function pinFileToIPFS(actionName, code) { + const formData = new FormData(); + formData.append('file', Buffer.from(code), { filename: actionName + '.js' }); + + formData.append( + 'pinataMetadata', + JSON.stringify({ + name: 'File name', + }) + ); + + formData.append( + 'pinataOptions', + JSON.stringify({ + cidVersion: 0, + }) + ); + + const res = await axios.post( + 'https://api.pinata.cloud/pinning/pinFileToIPFS', + formData, + { + maxBodyLength: 'Infinity', + headers: { + 'Content-Type': `multipart/form-data; boundary=${formData._boundary}`, + Authorization: `Bearer ${JWT}`, + }, + } + ); + + console.log(actionName, res.data.IpfsHash); + return res.data.IpfsHash; +} + +async function getCidRepository(codeRepository) { + const cidRepository = {}; + + await Promise.all( + Object.entries(codeRepository).map(([actionName, byNetworkObj]) => { + cidRepository[actionName] = {}; + + return Promise.all( + Object.entries(byNetworkObj).map(async ([networkName, codeStr]) => { + console.log('setting', actionName, networkName, codeStr.length); + + cidRepository[actionName][networkName] = await pinFileToIPFS( + actionName, + codeStr + ); + }) + ); + }) + ); + + return cidRepository; +} + +async function getCidRepositoryCommon(codeRepository) { + const cidRepository = {}; + + await Promise.all( + Object.entries(codeRepository).map(async ([actionName, codeStr]) => { + console.log('setting common', actionName, codeStr.length); + cidRepository[actionName] = await pinFileToIPFS(actionName, codeStr); + }) + ); + + return cidRepository; +} + +async function gogo() { + const [cidRepoCommon, cidRepo] = await Promise.all([ + getCidRepositoryCommon(litActionRepositoryCommon), + getCidRepository(litActionRepository), + ]); + + console.log('common', cidRepoCommon); + console.log('byNetwork', cidRepo); +} + +gogo(); diff --git a/packages/wrapped-keys/src/index.ts b/packages/wrapped-keys/src/index.ts index 4149ce1d66..ea0b95d524 100644 --- a/packages/wrapped-keys/src/index.ts +++ b/packages/wrapped-keys/src/index.ts @@ -7,6 +7,7 @@ import { signTransactionWithEncryptedKey, storeEncryptedKey, listEncryptedKeyMetadata, + batchGeneratePrivateKeys, } from './lib/api'; import { CHAIN_ETHEREUM, @@ -16,9 +17,13 @@ import { KEYTYPE_K256, KEYTYPE_ED25519, } from './lib/constants'; -import { setLitActionsCode } from './lib/lit-actions-client/code-repository'; +import { + setLitActionsCode, + setLitActionsCodeCommon, +} from './lib/lit-actions-client/code-repository'; import { LitActionCodeRepository, + LitActionCodeRepositoryCommon, LitActionCodeRepositoryEntry, } from './lib/lit-actions-client/types'; @@ -65,10 +70,12 @@ export const api = { signMessageWithEncryptedKey, signTransactionWithEncryptedKey, storeEncryptedKey, + batchGeneratePrivateKeys, }; export const config = { setLitActionsCode, + setLitActionsCodeCommon, }; export { @@ -84,6 +91,7 @@ export { ImportPrivateKeyResult, ListEncryptedKeyMetadataParams, LitActionCodeRepository, + LitActionCodeRepositoryCommon, LitActionCodeRepositoryEntry, SerializedTransaction, SignTransactionParams, diff --git a/packages/wrapped-keys/src/lib/api/batch-generate-private-keys.ts b/packages/wrapped-keys/src/lib/api/batch-generate-private-keys.ts new file mode 100644 index 0000000000..5c2313bb50 --- /dev/null +++ b/packages/wrapped-keys/src/lib/api/batch-generate-private-keys.ts @@ -0,0 +1,75 @@ +import { getKeyTypeFromNetwork } from './utils'; +import { batchGenerateKeysWithLitAction } from '../lit-actions-client'; +import { getLitActionCodeOrCidCommon } from '../lit-actions-client/utils'; +import { storePrivateKey } from '../service-client'; +import { + BatchGeneratePrivateKeysActionResult, + BatchGeneratePrivateKeysParams, + BatchGeneratePrivateKeysResult, +} from '../types'; +import { + getFirstSessionSig, + getPkpAccessControlCondition, + getPkpAddressFromSessionSig, +} from '../utils'; + +/** + * TODO: Document batch behaviour + * @param { BatchGeneratePrivateKeysParams } params Parameters to use for generating keys and optionally signing messages + * + * @returns { Promise } - The generated keys and, optionally, signed messages + */ +export async function batchGeneratePrivateKeys( + params: BatchGeneratePrivateKeysParams +): Promise { + const { pkpSessionSigs, litNodeClient } = params; + + const sessionSig = getFirstSessionSig(pkpSessionSigs); + const pkpAddress = getPkpAddressFromSessionSig(sessionSig); + + const allowPkpAddressToDecrypt = getPkpAccessControlCondition(pkpAddress); + + const { litActionCode, litActionIpfsCid } = getLitActionCodeOrCidCommon( + 'batchGenerateEncryptedKeys' + ); + + const actionResults = await batchGenerateKeysWithLitAction({ + ...params, + litActionIpfsCid: litActionCode ? undefined : litActionIpfsCid, + litActionCode: litActionCode ? litActionCode : undefined, + accessControlConditions: [allowPkpAddressToDecrypt], + pkpSessionSigs, + }); + + const results = await Promise.all( + actionResults.map( + async (result): Promise => { + const { generateEncryptedPrivateKey, network } = result; + + const signature = result.signMessage?.signature; + + const { id } = await storePrivateKey({ + sessionSig, + storedKeyMetadata: { + ...generateEncryptedPrivateKey, + keyType: getKeyTypeFromNetwork(network), + pkpAddress, + }, + litNetwork: litNodeClient.config.litNetwork, + }); + + return { + ...(signature ? { signMessage: { signature } } : {}), + generateEncryptedPrivateKey: { + memo: generateEncryptedPrivateKey.memo, + id, + generatedPublicKey: generateEncryptedPrivateKey.publicKey, + pkpAddress, + }, + }; + } + ) + ); + + return { pkpAddress, results }; +} diff --git a/packages/wrapped-keys/src/lib/api/generate-private-key.ts b/packages/wrapped-keys/src/lib/api/generate-private-key.ts index cb898c742f..2c8216c947 100644 --- a/packages/wrapped-keys/src/lib/api/generate-private-key.ts +++ b/packages/wrapped-keys/src/lib/api/generate-private-key.ts @@ -1,28 +1,14 @@ -import { NETWORK_EVM, NETWORK_SOLANA } from '../constants'; +import { getKeyTypeFromNetwork } from './utils'; import { generateKeyWithLitAction } from '../lit-actions-client'; import { getLitActionCodeOrCid } from '../lit-actions-client/utils'; import { storePrivateKey } from '../service-client'; -import { - GeneratePrivateKeyParams, - GeneratePrivateKeyResult, - KeyType, - Network, -} from '../types'; +import { GeneratePrivateKeyParams, GeneratePrivateKeyResult } from '../types'; import { getFirstSessionSig, getPkpAccessControlCondition, getPkpAddressFromSessionSig, } from '../utils'; -function getKeyTypeFromNetwork(network: Network): KeyType { - if (network === NETWORK_EVM) { - return 'K256'; - } else if (network === NETWORK_SOLANA) { - return 'ed25519'; - } else { - throw new Error('Network not implemented in generate-private-key'); - } -} /** * Generates a random private key inside a Lit Action, and persists the key and its metadata to the wrapped keys service. * Returns the public key of the random private key, and the PKP address that it was associated with. diff --git a/packages/wrapped-keys/src/lib/api/index.ts b/packages/wrapped-keys/src/lib/api/index.ts index 99b33eda35..d13292a017 100644 --- a/packages/wrapped-keys/src/lib/api/index.ts +++ b/packages/wrapped-keys/src/lib/api/index.ts @@ -1,3 +1,4 @@ +import { batchGeneratePrivateKeys } from './batch-generate-private-keys'; import { exportPrivateKey } from './export-private-key'; import { generatePrivateKey } from './generate-private-key'; import { getEncryptedKey } from './get-encrypted-key'; @@ -16,4 +17,5 @@ export { signMessageWithEncryptedKey, storeEncryptedKey, getEncryptedKey, + batchGeneratePrivateKeys, }; diff --git a/packages/wrapped-keys/src/lib/api/utils.ts b/packages/wrapped-keys/src/lib/api/utils.ts new file mode 100644 index 0000000000..f064062a63 --- /dev/null +++ b/packages/wrapped-keys/src/lib/api/utils.ts @@ -0,0 +1,12 @@ +import { NETWORK_EVM, NETWORK_SOLANA } from '../constants'; +import { KeyType, Network } from '../types'; + +export function getKeyTypeFromNetwork(network: Network): KeyType { + if (network === NETWORK_EVM) { + return 'K256'; + } else if (network === NETWORK_SOLANA) { + return 'ed25519'; + } else { + throw new Error(`Network not implemented ${network}`); + } +} diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/batch-generate-keys.ts b/packages/wrapped-keys/src/lib/lit-actions-client/batch-generate-keys.ts new file mode 100644 index 0000000000..4a041da3ad --- /dev/null +++ b/packages/wrapped-keys/src/lib/lit-actions-client/batch-generate-keys.ts @@ -0,0 +1,56 @@ +import { GLOBAL_OVERWRITE_IPFS_CODE_BY_NETWORK } from '@lit-protocol/constants'; +import { AccessControlConditions } from '@lit-protocol/types'; + +import { postLitActionValidation } from './utils'; +import { BatchGeneratePrivateKeysParams, Network } from '../types'; + +interface BatchGeneratePrivateKeysWithLitActionParams + extends BatchGeneratePrivateKeysParams { + accessControlConditions: AccessControlConditions; + litActionIpfsCid?: string; + litActionCode?: string; +} + +interface GeneratePrivateKeyLitActionResult { + ciphertext: string; + dataToEncryptHash: string; + publicKey: string; + memo: string; +} + +interface BatchGeneratePrivateKeysWithLitActionResult { + network: Network; + signMessage?: { signature: string }; + generateEncryptedPrivateKey: GeneratePrivateKeyLitActionResult; +} + +export async function batchGenerateKeysWithLitAction( + args: BatchGeneratePrivateKeysWithLitActionParams +): Promise { + const { + accessControlConditions, + litNodeClient, + actions, + pkpSessionSigs, + litActionIpfsCid, + litActionCode, + } = args; + + const result = await litNodeClient.executeJs({ + useSingleNode: true, + sessionSigs: pkpSessionSigs, + ipfsId: litActionIpfsCid, + code: litActionCode, + jsParams: { + actions, + accessControlConditions, + }, + ipfsOptions: { + overwriteCode: + GLOBAL_OVERWRITE_IPFS_CODE_BY_NETWORK[litNodeClient.config.litNetwork], + }, + }); + + const response = postLitActionValidation(result); + return JSON.parse(response); +} diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.ts b/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.ts index 3cd7b1a21e..54363e47a2 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.ts @@ -1,5 +1,7 @@ import { LitActionCodeRepository, + LitActionCodeRepositoryCommon, + LitActionCodeRepositoryCommonInput, LitActionCodeRepositoryEntry, LitActionCodeRepositoryInput, LitActionType, @@ -52,10 +54,10 @@ function assertIsLitActionRepositoryEntry( if ( typeof entry !== 'object' || !entry || - ('evm' in entry && - typeof (entry as LitActionCodeRepositoryEntry).evm !== 'string') || - ('solana' in entry && - typeof (entry as LitActionCodeRepositoryEntry).solana !== 'string') || + // @ts-expect-error assert function + ('evm' in entry && typeof entry.evm !== 'string') || + // @ts-expect-error assert function + ('solana' in entry && typeof entry.solana !== 'string') || Object.keys(entry).some((key) => !['evm', 'solana'].includes(key)) ) { throw new Error( @@ -80,4 +82,58 @@ function setLitActionsCode(repository: LitActionCodeRepositoryInput) { } } -export { litActionCodeRepository, setLitActionsCode }; +/** + * A repository for managing Lit Actions related to blockchain operations. + * Contains actions that are designed to be used for multiple networks + * @type {LitActionCodeRepositoryCommon} + */ +const litActionCodeRepositoryCommon: LitActionCodeRepositoryCommon = { + batchGenerateEncryptedKeys: '', +}; + +function assertIsLitActionKeyCommon( + key: string +): asserts key is keyof LitActionCodeRepositoryCommon { + if (!(key in litActionCodeRepositoryCommon)) { + throw new Error( + `Invalid key: ${key}; must be one of ${Object.keys( + litActionCodeRepositoryCommon + ).join(',')}` + ); + } +} + +/** + * Type Guard for LitActionCodeRepositoryEntry + */ +function assertIsLitActionRepositoryEntryCommon( + entry: unknown +): asserts entry is LitActionCodeRepositoryEntry { + if (typeof entry !== 'string') { + throw new Error( + `Invalid LitActionRepositoryCommon entry: ${JSON.stringify(entry)}` + ); + } +} + +/** + * Updates the litActionCodeRepository with the provided entries. + * @param { LitActionCodeRepositoryCommonInput } repository - user provided repository to set + */ +function setLitActionsCodeCommon( + repository: LitActionCodeRepositoryCommonInput +) { + for (const [actionType, actionCode] of Object.entries(repository)) { + assertIsLitActionKeyCommon(actionType); + assertIsLitActionRepositoryEntryCommon(actionCode); + + litActionCodeRepositoryCommon[actionType] = actionCode; + } +} + +export { + litActionCodeRepository, + setLitActionsCode, + litActionCodeRepositoryCommon, + setLitActionsCodeCommon, +}; diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/constants.ts b/packages/wrapped-keys/src/lib/lit-actions-client/constants.ts index a0f4c08e76..83360f84fc 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/constants.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/constants.ts @@ -1,22 +1,26 @@ -import { LitCidRepository } from './types'; +import { LitCidRepository, LitCidRepositoryCommon } from './types'; const LIT_ACTION_CID_REPOSITORY: LitCidRepository = Object.freeze({ signTransaction: Object.freeze({ - evm: 'QmRWGips9G3pHXNa3viGFpAyh1LwzrR35R4xMiG61NuHpS', - solana: 'QmPZR6FnTPMYpzKxNNHt4xRckDsAoQz76cxkLhJArSoe4w', + evm: 'QmRpAgGKEmgeBRhqdC2EH17QUt6puwsbm8Z2nNneVN4uJG', + solana: 'QmR1nPG2tnmC72zuCEMZUZrrMEkbDiMPNHW45Dsm2n7xnk', }), signMessage: Object.freeze({ - evm: 'QmNy5bHvgaN2rqo4kMU71jtgSSxDES6HSDgadBV29pfcRu', - solana: 'Qma1nRZN5eriT1a7Uffbiek5jsksvWCCqHuE1x1nk9zaAq', + evm: 'QmXi9iqJvXrHoUGSo5WREonrruDhzQ7cFr7Cry3wX2hmue', + solana: 'QmcEJGVqRYtVukjm2prCPT7Fs66GpaqZwmZoxEHMHor6Jz', }), generateEncryptedKey: Object.freeze({ - evm: 'QmaoPMSqcze3NW3KSA75ecWSkcmWT1J7kVr8LyJPCKRvHd', - solana: 'QmdRBXYLYvcNHrChmsZ2jFDY8dA99CcSdqHo3p1ES3UThL', + evm: 'QmeD6NYCWhUCLgxgpkgSguaKjwnpCnJ6Yf8SdsyPpK4eKK', + solana: 'QmPkVD3hEjMi1T5zQuvdrFCXaGTEMHNdAhAL4WHkqxijrQ', }), exportPrivateKey: Object.freeze({ - evm: 'Qmb5ZAm1EZRL7dYTtyYxkPxx4kBmoCjjzcgdrJH9cKMXxR', - solana: 'Qmb5ZAm1EZRL7dYTtyYxkPxx4kBmoCjjzcgdrJH9cKMXxR', + evm: 'QmUJ74pTUqeeHzDGdfwCph1vJVNJ1rRzJdvMiTjS1BMwYj', + solana: 'QmUJ74pTUqeeHzDGdfwCph1vJVNJ1rRzJdvMiTjS1BMwYj', }), }); -export { LIT_ACTION_CID_REPOSITORY }; +const LIT_ACTION_CID_REPOSITORY_COMMON: LitCidRepositoryCommon = Object.freeze({ + batchGenerateEncryptedKeys: 'QmR8Zs7ctSEctxBrSnAYhMXFXCC1ub8K1xvMn5Js3NCSAA', +}); + +export { LIT_ACTION_CID_REPOSITORY, LIT_ACTION_CID_REPOSITORY_COMMON }; diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/export-private-key.ts b/packages/wrapped-keys/src/lib/lit-actions-client/export-private-key.ts index 59f4c0fe57..8dd6ffb7ac 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/export-private-key.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/export-private-key.ts @@ -1,8 +1,8 @@ +import { GLOBAL_OVERWRITE_IPFS_CODE_BY_NETWORK } from '@lit-protocol/constants'; import { AccessControlConditions } from '@lit-protocol/types'; import { postLitActionValidation } from './utils'; import { ExportPrivateKeyParams, StoredKeyData } from '../types'; -import { GLOBAL_OVERWRITE_IPFS_CODE_BY_NETWORK } from '@lit-protocol/constants'; interface SignMessageWithLitActionParams extends ExportPrivateKeyParams { accessControlConditions: AccessControlConditions; diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/generate-key.ts b/packages/wrapped-keys/src/lib/lit-actions-client/generate-key.ts index 5ecceaa5fa..3cfc8253b2 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/generate-key.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/generate-key.ts @@ -1,7 +1,8 @@ +import { GLOBAL_OVERWRITE_IPFS_CODE_BY_NETWORK } from '@lit-protocol/constants'; import { AccessControlConditions } from '@lit-protocol/types'; + import { postLitActionValidation } from './utils'; import { GeneratePrivateKeyParams } from '../types'; -import { GLOBAL_OVERWRITE_IPFS_CODE_BY_NETWORK } from '@lit-protocol/constants'; interface GeneratePrivateKeyLitActionParams extends GeneratePrivateKeyParams { pkpAddress: string; @@ -25,6 +26,7 @@ export async function generateKeyWithLitAction({ pkpAddress, }: GeneratePrivateKeyLitActionParams): Promise { const result = await litNodeClient.executeJs({ + useSingleNode: true, sessionSigs: pkpSessionSigs, ipfsId: litActionIpfsCid, code: litActionCode, diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/index.ts b/packages/wrapped-keys/src/lib/lit-actions-client/index.ts index a383d8a0e9..e523e48e62 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/index.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/index.ts @@ -1,11 +1,13 @@ +import { batchGenerateKeysWithLitAction } from './batch-generate-keys'; +import { exportPrivateKeyWithLitAction } from './export-private-key'; import { generateKeyWithLitAction } from './generate-key'; import { signMessageWithLitAction } from './sign-message'; import { signTransactionWithLitAction } from './sign-transaction'; -import { exportPrivateKeyWithLitAction } from './export-private-key'; export { generateKeyWithLitAction, signTransactionWithLitAction, signMessageWithLitAction, exportPrivateKeyWithLitAction, + batchGenerateKeysWithLitAction, }; diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/sign-message.ts b/packages/wrapped-keys/src/lib/lit-actions-client/sign-message.ts index 330f90ed70..f5aec3f126 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/sign-message.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/sign-message.ts @@ -1,8 +1,8 @@ +import { GLOBAL_OVERWRITE_IPFS_CODE_BY_NETWORK } from '@lit-protocol/constants'; import { AccessControlConditions } from '@lit-protocol/types'; import { postLitActionValidation } from './utils'; import { SignMessageWithEncryptedKeyParams, StoredKeyData } from '../types'; -import { GLOBAL_OVERWRITE_IPFS_CODE_BY_NETWORK } from '@lit-protocol/constants'; interface SignMessageWithLitActionParams extends SignMessageWithEncryptedKeyParams { diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/sign-transaction.ts b/packages/wrapped-keys/src/lib/lit-actions-client/sign-transaction.ts index 6e3cee6842..5dde9e0b37 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/sign-transaction.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/sign-transaction.ts @@ -1,3 +1,4 @@ +import { GLOBAL_OVERWRITE_IPFS_CODE_BY_NETWORK } from '@lit-protocol/constants'; import { AccessControlConditions, ILitNodeClient, @@ -10,7 +11,6 @@ import { SerializedTransaction, StoredKeyData, } from '../types'; -import { GLOBAL_OVERWRITE_IPFS_CODE_BY_NETWORK } from '@lit-protocol/constants'; interface SignTransactionWithLitActionParams { litNodeClient: ILitNodeClient; diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/types.ts b/packages/wrapped-keys/src/lib/lit-actions-client/types.ts index 85510087fd..bda28ecd57 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/types.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/types.ts @@ -6,12 +6,18 @@ export type LitActionType = | 'generateEncryptedKey' | 'exportPrivateKey'; +export type LitActionTypeCommon = 'batchGenerateEncryptedKeys'; + export type LitCidRepositoryEntry = Readonly>; export type LitCidRepository = Readonly< Record >; +export type LitCidRepositoryCommon = Readonly< + Record +>; + /** * A type that represents an entry in a Lit Action Code repository. * @@ -36,3 +42,8 @@ export type LitActionCodeRepository = Readonly< export type LitActionCodeRepositoryInput = Partial< Record >; + +export type LitActionCodeRepositoryCommon = Record; +export type LitActionCodeRepositoryCommonInput = Partial< + Record +>; diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/utils.ts b/packages/wrapped-keys/src/lib/lit-actions-client/utils.ts index 14d505d855..abd7301b97 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/utils.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/utils.ts @@ -1,8 +1,14 @@ import { ExecuteJsResponse } from '@lit-protocol/types'; -import { litActionCodeRepository } from './code-repository'; -import { LIT_ACTION_CID_REPOSITORY } from './constants'; -import { LitActionType } from './types'; +import { + litActionCodeRepository, + litActionCodeRepositoryCommon, +} from './code-repository'; +import { + LIT_ACTION_CID_REPOSITORY, + LIT_ACTION_CID_REPOSITORY_COMMON, +} from './constants'; +import { LitActionType, LitActionTypeCommon } from './types'; import { Network } from '../types'; /** @@ -77,6 +83,7 @@ function assertNetworkIsValid(network: any): asserts network is Network { /** * Fetch the Lit action code or its IPFS CID for a given network and action type. + * @private * * @param {Network} network The network to get the code or CID for. * @param {LitActionType} actionType The type of action to get the code or CID for. @@ -94,3 +101,33 @@ export function getLitActionCodeOrCid( } return { litActionIpfsCid: getLitActionCid(network, actionType) }; } + +/** + * Fetch the Lit action code or its IPFS CID for a given network and action type. + * @private + * @param {LitActionTypeCommon} actionType The type of action to get the code or CID for. + * @returns {{ litActionCode?: string, litActionIpfsCid?: string }} The Lit action code or its IPFS CID. + */ +export function getLitActionCodeOrCidCommon(actionType: LitActionTypeCommon): { + litActionCode?: string; + litActionIpfsCid?: string; +} { + // Default state is that litActionCode will be falsy, unless someone has injected to it using `setLitActionsCode(); + const litActionCode = getLitActionCodeCommon(actionType); + + if (litActionCode) { + return { litActionCode }; + } + return { litActionIpfsCid: getLitActionCidCommon(actionType) }; +} + +export function getLitActionCidCommon(actionType: LitActionTypeCommon) { + return LIT_ACTION_CID_REPOSITORY_COMMON[actionType]; +} + +export function getLitActionCodeCommon( + actionType: LitActionTypeCommon +): string { + // No fuzzy validation needed here, because `setLitActionsCodeCommon()` validates its input + return litActionCodeRepositoryCommon[actionType]; +} diff --git a/packages/wrapped-keys/src/lib/types.ts b/packages/wrapped-keys/src/lib/types.ts index abfb640998..ab03175543 100644 --- a/packages/wrapped-keys/src/lib/types.ts +++ b/packages/wrapped-keys/src/lib/types.ts @@ -134,6 +134,34 @@ export interface ExportPrivateKeyResult { id: string; } +/** @typedef GeneratePrivateKeyAction + * @extends ApiParamsSupportedNetworks + */ +export type GeneratePrivateKeyAction = ApiParamsSupportedNetworks & { + generateKeyParams: { memo: string }; + signMessageParams?: { messageToSign: string | Uint8Array }; +}; + +/** @typedef BatchGeneratePrivateKeysParams + * @extends BaseApiParams + */ +export type BatchGeneratePrivateKeysParams = BaseApiParams & { + actions: GeneratePrivateKeyAction[]; +}; + +/** @typedef GeneratePrivateKeyAction + * @extends ApiParamsSupportedNetworks + */ +export interface BatchGeneratePrivateKeysActionResult { + generateEncryptedPrivateKey: GeneratePrivateKeyResult & { memo: string }; + signMessage?: { signature: string }; +} + +export interface BatchGeneratePrivateKeysResult { + pkpAddress: string; + results: BatchGeneratePrivateKeysActionResult[]; +} + /** @typedef GeneratePrivateKeyParams * @extends BaseApiParams * @property {Network} network The network for which the private key needs to be generated; keys are generated differently for different networks