diff --git a/local-tests/tests/wrapped-keys/testBatchGeneratePrivateKeys.ts b/local-tests/tests/wrapped-keys/testBatchGeneratePrivateKeys.ts index b2c40701bf..3afad0168d 100644 --- a/local-tests/tests/wrapped-keys/testBatchGeneratePrivateKeys.ts +++ b/local-tests/tests/wrapped-keys/testBatchGeneratePrivateKeys.ts @@ -6,6 +6,8 @@ import nacl from 'tweetnacl'; import bs58 from 'bs58'; import { ethers } from 'ethers'; import { BatchGeneratePrivateKeysActionResult } from '../../../packages/wrapped-keys/src/lib/types'; +import { getBaseTransactionForNetwork, getSolanaTransaction } from './util'; +import { Keypair } from '@solana/web3.js'; const { batchGeneratePrivateKeys } = api; @@ -81,6 +83,13 @@ export const testBatchGeneratePrivateKeys = async ( new Date(Date.now() + 1000 * 60 * 10).toISOString() ); // 10 mins expiry + const solanaKeypair = Keypair.generate(); + + const { + solanaTransaction, + unsignedTransaction: solanaUnsignedTransaction, + } = await getSolanaTransaction({ solanaKeypair }); + const solanaMessageToSign = 'This is a test solana message'; const evmMessageToSign = 'This is a test evm message'; const { results } = await batchGeneratePrivateKeys({ @@ -90,11 +99,20 @@ export const testBatchGeneratePrivateKeys = async ( network: 'evm', signMessageParams: { messageToSign: evmMessageToSign }, generateKeyParams: { memo: 'Test evm key' }, + signTransactionParams: { + unsignedTransaction: getBaseTransactionForNetwork({ + network: devEnv.litNodeClient.config.litNetwork, + toAddress: alice.wallet.address, + }), + }, }, { network: 'solana', signMessageParams: { messageToSign: solanaMessageToSign }, generateKeyParams: { memo: 'Test solana key' }, + // signTransactionParams: { + // unsignedTransaction: solanaUnsignedTransaction, + // }, }, ], litNodeClient: devEnv.litNodeClient, @@ -122,13 +140,31 @@ export const testBatchGeneratePrivateKeys = async ( throw new Error('Missing message signature in response'); } - console.log('solana verify sig'); + console.log('solana verify message sig'); await verifySolanaSignature(results[1], solanaMessageToSign); - console.log('evm verify sig'); + console.log('evm verify message sig'); await verifyEvmSignature(results[0], evmMessageToSign); console.log('results', results); + const signedEthTx = results[0].signTransaction.signature; + + // Test eth signed tx: + if (!ethers.utils.isHexString(signedEthTx)) { + throw new Error(`signedTx isn't hex: ${signedEthTx}`); + } + + // test solana signed tx: + // + // const signatureBuffer = Buffer.from(ethers.utils.base58.decode(signedTx)); + // solanaTransaction.addSignature(solanaKeypair.publicKey, signatureBuffer); + // + // if (!solanaTransaction.verifySignatures()) { + // throw new Error( + // `Signature: ${signedTx} doesn't validate for the Solana transaction.` + // ); + // } + log('✅ testBatchGenerateEncryptedKeys'); } catch (err) { console.log(err.message, err, err.stack); diff --git a/local-tests/tests/wrapped-keys/testSignTransactionWithSolanaEncryptedKey.ts b/local-tests/tests/wrapped-keys/testSignTransactionWithSolanaEncryptedKey.ts index 138d2c5b53..648a8ca8b1 100644 --- a/local-tests/tests/wrapped-keys/testSignTransactionWithSolanaEncryptedKey.ts +++ b/local-tests/tests/wrapped-keys/testSignTransactionWithSolanaEncryptedKey.ts @@ -12,6 +12,7 @@ import { } from '@solana/web3.js'; import { getPkpSessionSigs } from 'local-tests/setup/session-sigs/get-pkp-session-sigs'; import { ethers } from 'ethers'; +import { getSolanaTransaction } from './util'; const { importPrivateKey, signTransactionWithEncryptedKey } = api; @@ -57,11 +58,6 @@ export const testSignTransactionWithSolanaEncryptedKey = async ( memo: 'Test key', }); - const solanaConnection = new Connection( - clusterApiUrl('devnet'), - 'confirmed' - ); - // Request Solana Airdrop // const balance = await solanaConnection.getBalance(solanaKeypair.publicKey); // console.log("balance- ", balance); // Should be 0, in fact if we get the balance right after the Air Drop it will also be 0 unless we wait. We're skipping the balance confirmation @@ -81,30 +77,8 @@ export const testSignTransactionWithSolanaEncryptedKey = async ( new Date(Date.now() + 1000 * 60 * 10).toISOString() ); // 10 mins expiry - const solanaTransaction = new Transaction(); - solanaTransaction.add( - SystemProgram.transfer({ - fromPubkey: solanaKeypair.publicKey, - toPubkey: new PublicKey(solanaKeypair.publicKey), - lamports: LAMPORTS_PER_SOL / 100, // Transfer 0.01 SOL - }) - ); - solanaTransaction.feePayer = solanaKeypair.publicKey; - - const { blockhash } = await solanaConnection.getLatestBlockhash(); - solanaTransaction.recentBlockhash = blockhash; - - const serializedTransaction = solanaTransaction - .serialize({ - requireAllSignatures: false, // should be false as we're not signing the message - verifySignatures: false, // should be false as we're not signing the message - }) - .toString('base64'); - - const unsignedTransaction: SerializedTransaction = { - serializedTransaction, - chain: 'devnet', - }; + const { unsignedTransaction, solanaTransaction } = + await getSolanaTransaction({ solanaKeypair }); const signedTx = await signTransactionWithEncryptedKey({ pkpSessionSigs: pkpSessionSigsSigning, diff --git a/local-tests/tests/wrapped-keys/util.ts b/local-tests/tests/wrapped-keys/util.ts index f03f78d420..928579538f 100644 --- a/local-tests/tests/wrapped-keys/util.ts +++ b/local-tests/tests/wrapped-keys/util.ts @@ -1,17 +1,25 @@ import { LIT_NETWORKS_KEYS } from '@lit-protocol/types'; import { LIT_CHAINS } from '@lit-protocol/constants'; import { ethers } from 'ethers'; -import { config } from '@lit-protocol/wrapped-keys'; -import { - litActionRepositoryCommon, - litActionRepository, -} from '@lit-protocol/wrapped-keys-lit-actions'; - import type { + EthereumLitTransaction, LitActionCodeRepository, LitActionCodeRepositoryCommon, - EthereumLitTransaction, } from '@lit-protocol/wrapped-keys'; +import { config, SerializedTransaction } from '@lit-protocol/wrapped-keys'; +import { + litActionRepository, + litActionRepositoryCommon, +} from '@lit-protocol/wrapped-keys-lit-actions'; +import { + clusterApiUrl, + Connection, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + Transaction, +} from '@solana/web3.js'; const emptyLitActionRepositoryCommon: LitActionCodeRepositoryCommon = { batchGenerateEncryptedKeys: '', @@ -104,3 +112,43 @@ export function getBaseTransactionForNetwork({ ), }; } + +export async function getSolanaTransaction({ + solanaKeypair, +}: { + solanaKeypair: Keypair; +}): Promise<{ + unsignedTransaction: SerializedTransaction; + solanaTransaction: Transaction; +}> { + const solanaConnection = new Connection(clusterApiUrl('devnet'), 'confirmed'); + + const solanaTransaction = new Transaction(); + + solanaTransaction.add( + SystemProgram.transfer({ + fromPubkey: solanaKeypair.publicKey, + toPubkey: new PublicKey(solanaKeypair.publicKey), + lamports: LAMPORTS_PER_SOL / 100, // Transfer 0.01 SOL + }) + ); + solanaTransaction.feePayer = solanaKeypair.publicKey; + + const { blockhash } = await solanaConnection.getLatestBlockhash(); + solanaTransaction.recentBlockhash = blockhash; + + const serializedTransaction = solanaTransaction + .serialize({ + requireAllSignatures: false, // should be false as we're not signing the message + verifySignatures: false, // should be false as we're not signing the message + }) + .toString('base64'); + + return { + solanaTransaction, + unsignedTransaction: { + serializedTransaction, + chain: 'devnet', + }, + }; +} diff --git a/packages/wrapped-keys-lit-actions/src/lib/internal/ethereum/signTransaction.ts b/packages/wrapped-keys-lit-actions/src/lib/internal/ethereum/signTransaction.ts index 3c007b58d8..2d3b9dd6c8 100644 --- a/packages/wrapped-keys-lit-actions/src/lib/internal/ethereum/signTransaction.ts +++ b/packages/wrapped-keys-lit-actions/src/lib/internal/ethereum/signTransaction.ts @@ -163,7 +163,7 @@ export async function signTransactionEthereumKey({ privateKey: string; validatedTx: ValidatedTransaction; unsignedTransaction: UnsignedTransaction; -}) { +}): Promise { const wallet = new ethers.Wallet(privateKey); validatedTx.from = wallet.address; diff --git a/packages/wrapped-keys-lit-actions/src/lib/raw-action-functions/common/batchGenerateEncryptedKeys.ts b/packages/wrapped-keys-lit-actions/src/lib/raw-action-functions/common/batchGenerateEncryptedKeys.ts index 26bbf16ae2..dd817a7cee 100644 --- a/packages/wrapped-keys-lit-actions/src/lib/raw-action-functions/common/batchGenerateEncryptedKeys.ts +++ b/packages/wrapped-keys-lit-actions/src/lib/raw-action-functions/common/batchGenerateEncryptedKeys.ts @@ -1,11 +1,18 @@ import { encryptPrivateKey } from '../../internal/common/encryptKey'; import { generateEthereumPrivateKey } from '../../internal/ethereum/generatePrivateKey'; import { signMessageEthereumKey } from '../../internal/ethereum/signMessage'; +import { + getValidatedUnsignedTx, + signTransactionEthereumKey, +} from '../../internal/ethereum/signTransaction'; import { generateSolanaPrivateKey } from '../../internal/solana/generatePrivateKey'; import { signMessageSolanaKey } from '../../internal/solana/signMessage'; +import { signTransactionSolanaKey } from '../../internal/solana/signTransaction'; -interface Action { - network: 'evm' | 'solana'; +import type { UnsignedTransaction as UnsignedTransactionEthereum } from '../../internal/ethereum/signTransaction'; +import type { UnsignedTransaction as UnsignedTransactionSolana } from '../../internal/solana/signTransaction'; + +interface BaseAction { generateKeyParams: { memo: string; }; @@ -14,6 +21,24 @@ interface Action { }; } +interface ActionSolana extends BaseAction { + network: 'solana'; + signTransactionParams?: { + unsignedTransaction: UnsignedTransactionSolana; + broadcast: boolean; + }; +} + +interface ActionEthereum extends BaseAction { + network: 'evm'; + signTransactionParams?: { + unsignedTransaction: UnsignedTransactionEthereum; + broadcast: boolean; + }; +} + +type Action = ActionSolana | ActionEthereum; + export interface BatchGenerateEncryptedKeysParams { actions: Action[]; accessControlConditions: string; @@ -23,27 +48,38 @@ async function processEthereumAction({ action, accessControlConditions, }: { - action: Action; + action: ActionEthereum; accessControlConditions: string; }) { const { network, generateKeyParams } = action; const messageToSign = action.signMessageParams?.messageToSign; + const unsignedTransaction = action.signTransactionParams?.unsignedTransaction; 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(), - ]); + const [generatedPrivateKey, messageSignature, transactionSignature] = + await Promise.all([ + encryptPrivateKey({ + accessControlConditions, + publicKey: ethereumKey.publicKey, + privateKey: ethereumKey.privateKey, + }), + messageToSign + ? signMessageEthereumKey({ + messageToSign: messageToSign, + privateKey: ethereumKey.privateKey, + }) + : Promise.resolve(), + + unsignedTransaction + ? signTransactionEthereumKey({ + unsignedTransaction, + broadcast: action.signTransactionParams?.broadcast || false, + privateKey: ethereumKey.privateKey, + validatedTx: getValidatedUnsignedTx(unsignedTransaction), + }) + : Promise.resolve(), + ]); return { network, @@ -54,6 +90,9 @@ async function processEthereumAction({ ...(messageSignature ? { signMessage: { signature: messageSignature } } : {}), + ...(transactionSignature + ? { signTransaction: { signature: transactionSignature } } + : {}), }; } @@ -61,28 +100,37 @@ async function processSolanaAction({ action, accessControlConditions, }: { - action: Action; + action: ActionSolana; accessControlConditions: string; }) { const { network, generateKeyParams } = action; const messageToSign = action.signMessageParams?.messageToSign; + const unsignedTransaction = action.signTransactionParams?.unsignedTransaction; 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(), - ]); + const [generatedPrivateKey, messageSignature, transactionSignature] = + await Promise.all([ + encryptPrivateKey({ + accessControlConditions, + publicKey: solanaKey.publicKey, + privateKey: solanaKey.privateKey, + }), + messageToSign + ? signMessageSolanaKey({ + messageToSign: messageToSign, + privateKey: solanaKey.privateKey, + }) + : Promise.resolve(), + unsignedTransaction + ? signTransactionSolanaKey({ + broadcast: action.signTransactionParams?.broadcast || false, + unsignedTransaction, + privateKey: solanaKey.privateKey, + }) + : Promise.resolve(), + ]); return { network, @@ -93,6 +141,9 @@ async function processSolanaAction({ ...(messageSignature ? { signMessage: { signature: messageSignature } } : {}), + ...(transactionSignature + ? { signTransaction: { signature: transactionSignature } } + : {}), }; } 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 index 1d738ca92b..e5a4ca2f1e 100644 --- a/packages/wrapped-keys/src/lib/api/batch-generate-private-keys.ts +++ b/packages/wrapped-keys/src/lib/api/batch-generate-private-keys.ts @@ -62,10 +62,16 @@ export async function batchGeneratePrivateKeys( } = actionResult; const id = ids[ndx]; // Result of writes is in same order as provided - const signature = actionResult.signMessage?.signature; + const messageSignature = actionResult.signMessage?.signature; + const transactionSignature = actionResult.signTransaction?.signature; return { - ...(signature ? { signMessage: { signature } } : {}), + ...(messageSignature + ? { signMessage: { signature: messageSignature } } + : {}), + ...(transactionSignature + ? { signTransaction: { signature: transactionSignature } } + : {}), generateEncryptedPrivateKey: { memo: memo, id, 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 index 4a041da3ad..dc61367341 100644 --- 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 @@ -21,6 +21,7 @@ interface GeneratePrivateKeyLitActionResult { interface BatchGeneratePrivateKeysWithLitActionResult { network: Network; signMessage?: { signature: string }; + signTransaction?: { signature: string }; generateEncryptedPrivateKey: GeneratePrivateKeyLitActionResult; } diff --git a/packages/wrapped-keys/src/lib/types.ts b/packages/wrapped-keys/src/lib/types.ts index b76f2cff02..21f5304cb9 100644 --- a/packages/wrapped-keys/src/lib/types.ts +++ b/packages/wrapped-keys/src/lib/types.ts @@ -159,14 +159,33 @@ export interface ExportPrivateKeyResult { id: string; } -/** @typedef GeneratePrivateKeyAction - * @extends ApiParamsSupportedNetworks - */ -export type GeneratePrivateKeyAction = ApiParamsSupportedNetworks & { +export type GeneratePrivateKeyActionBase = ApiParamsSupportedNetworks & { generateKeyParams: { memo: string }; signMessageParams?: { messageToSign: string | Uint8Array }; }; +export type GeneratePrivateKeyActionSolana = GeneratePrivateKeyActionBase & { + network: Extract; + signTransactionParams?: { + unsignedTransaction: SerializedTransaction; + broadcast?: boolean; + }; +}; + +export type GeneratePrivateKeyActionEthereum = GeneratePrivateKeyActionBase & { + network: Extract; + signTransactionParams?: { + unsignedTransaction: EthereumLitTransaction; + broadcast?: boolean; + }; +}; + +/** @typedef GeneratePrivateKeyAction + */ +export type GeneratePrivateKeyAction = + | GeneratePrivateKeyActionSolana + | GeneratePrivateKeyActionEthereum; + /** @typedef BatchGeneratePrivateKeysParams * @extends BaseApiParams */ @@ -180,6 +199,7 @@ export type BatchGeneratePrivateKeysParams = BaseApiParams & { export interface BatchGeneratePrivateKeysActionResult { generateEncryptedPrivateKey: GeneratePrivateKeyResult & { memo: string }; signMessage?: { signature: string }; + signTransaction?: { signature: string }; } export interface BatchGeneratePrivateKeysResult {