Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LIT-3995 - (wrapped-keys) - Add signTransaction functionality to supported actions for batchGeneratePrivateKeys #702

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
40 changes: 38 additions & 2 deletions local-tests/tests/wrapped-keys/testBatchGeneratePrivateKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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({
Expand All @@ -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,
Expand Down Expand Up @@ -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}`);
}
Comment on lines +153 to +155
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we verify that the signed tx is from the the generated wallet?


// 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
62 changes: 55 additions & 7 deletions local-tests/tests/wrapped-keys/util.ts
Original file line number Diff line number Diff line change
@@ -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: '',
Expand Down Expand Up @@ -118,3 +126,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',
},
};
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MaximusHaximus we don't wanna allow fetching nonce, gas, etc or anything from the RPC. It should all be provided by the user either as a serialized tx or as params and we could construct the transaction inside the LA but we should not fetch anything from the RPC we can remove this fetching altogether

Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export async function signTransactionEthereumKey({
privateKey: string;
validatedTx: ValidatedTransaction;
unsignedTransaction: UnsignedTransaction;
}) {
}): Promise<string> {
const wallet = new ethers.Wallet(privateKey);

validatedTx.from = wallet.address;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
};
Expand All @@ -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;
Expand All @@ -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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't allow broadcast in the batchGenerate function as it uses RPC which can take arbitrary amounts of time?

privateKey: ethereumKey.privateKey,
validatedTx: getValidatedUnsignedTx(unsignedTransaction),
})
: Promise.resolve(),
]);

return {
network,
Expand All @@ -54,35 +90,47 @@ async function processEthereumAction({
...(messageSignature
? { signMessage: { signature: messageSignature } }
: {}),
...(transactionSignature
? { signTransaction: { signature: transactionSignature } }
: {}),
};
}

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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't allow broadcast?

unsignedTransaction,
privateKey: solanaKey.privateKey,
})
: Promise.resolve(),
]);

return {
network,
Expand All @@ -93,6 +141,9 @@ async function processSolanaAction({
...(messageSignature
? { signMessage: { signature: messageSignature } }
: {}),
...(transactionSignature
? { signTransaction: { signature: transactionSignature } }
: {}),
};
}

Expand Down
Loading
Loading