Skip to main content

Signing a Transaction

This guide covers the signTransactionWithEncryptedKey function from the Wrapped Keys SDK. For an overview of what a Wrapped Key is and what can be done with it, please go here.

Using the signTransactionWithEncryptedKey function, you can sign a transaction using a Wrapped Key. The Wrapped Keys SDK will look up the corresponding encryption metadata (ciphertext and dataToEncryptHash) for your PKP in Lit's private DynamoDB instance. If found, it well then use your provided PKP Session Signatures to authorize decryption of the private key, and will sign your provided message, returning the signed message. If the broadcast setting is enabled, then the signed transaction will also be broadcasted to the specified chain.

Below we will walk through an implementation of signTransactionWithEncryptedKey. The full code implementation can be found here.

Overview of How it Works

  1. The Wrapped Keys SDK will use the provided Wrapped Key ID and PKP Session Signatures to fetch the encryption metadata for a specific Wrapped Key
  2. Using the PKP Session Signatures, the SDK will make a request to the Lit network to execute the Sign Transaction Lit Action
  3. The Lit Action will verify the required transaction parameters were provided in order to sign the transaction
  4. The Lit Action will check the Access Control Conditions the plaintext private key was encrypted with to verify the PKP is authorized to decrypt the private key
  5. If authorized, the Wrapped Key will be decrypted within a Lit node's TEE. If not authorized, an error will be returned
  6. The Lit Action will use the decrypted Wrapped Key and the provided SignTransactionWithEncryptedKeyParams to sign the transaction
  7. If the broadcast parameter was set to true, the Lit Action will then broadcast the signed transaction to the specified network, returning the transaction hash. Otherwise, the signed transaction will be returned

Prerequisites

Before continuing with this guide, you should have an understanding of:

signTransactionWithEncryptedKey's Interface

Source code

/**
* Signs a transaction inside the Lit Action using the previously persisted wrapped key associated with the current LIT PK.
* This method fetches the encrypted key from the wrapped keys service, then executes a Lit Action that decrypts the key inside the LIT action and uses
* the decrypted key to sign the provided transaction
* Optionally, if you pass `broadcast: true`, the LIT action will also submit the signed transaction to the associated RPC endpoint on your behalf
*/
async function signTransactionWithEncryptedKey(
params: {
pkpSessionSigs: SessionSigsMap;
litNodeClient: ILitNodeClient;
network: 'evm';
id: string;
broadcast: boolean;
unsignedTransaction: EthereumLitTransaction;
}
): Promise<string>

EthereumLitTransaction has the following interface:

note

chain must be one of the supported EVM networks.

/**  EthereumLitTransaction must be provided to the `SignTransaction` endpoint when `network` is `evm`.
*
* @typedef EthereumLitTransaction
*
* @property { string } toAddress The address the transaction is 'to'
* @property { string } value The value of the transaction to be sent
* @property { number } chainId The chain ID of the target chain that the transaction will be executed on
* @property { string } [gasPrice] The exact gas price that you are willing to pay to execute the transaction
* @property { string } [gasLimit] The maximum gas price that you are willing to pay to execute the transaction
* @property { string } [dataHex] Data in hex format to be included in the transaction
*
*/
interface EthereumLitTransaction {
chain: string;
toAddress: string;
value: string;
chainId: number;
gasPrice?: string;
gasLimit?: number;
dataHex?: string;
}

Parameters

pkpSessionSigs

When a Wrapped Key is generated, it's encrypted with the following Access Control Conditions:

[
{
contractAddress: '',
standardContractType: '',
chain: CHAIN_ETHEREUM,
method: '',
parameters: [':userAddress'],
returnValueTest: {
comparator: '=',
value: pkpAddress,
},
},
];

where pkpAddress is the addressed derived from the pkpSessionSigs. This restricts the decryption of the Wrapped Key to only those whom can generate valid Authentication Signatures from the PKP which generated the Wrapped Key.

A valid pkpSessionSigs object can be obtained using the getPkpSessionSigs helper method available on an instance of LitNodeClient. We dive deeper into obtaining a pkpSessionSigs using getPkpSessionSigs in the Generating PKP Session Signatures section of this guide.

litNodeClient

This is an instance of the LitNodeClient that is connected to a Lit network.

network

This parameter dictates what transaction signing Lit Action is used to sign unsignedTransaction. It must be one of the supported Wrapped Keys Networks which currently consists of:

id

This is a unique identifier (UUID v4) generated by Lit for the Wrapped Key.

Because a PKP can have multiple Wrapped Keys attached to it, this ID is used to identify which Wrapped Key to use when calling other Wrapped Key methods such as signMessageWithEncryptedKey and signTransactionWithEncryptedKey.

broadcast

When this parameter is set to true, after signing the transaction, the Wrapped Key Lit Action will broadcast the signed transaction to a network.

Which network the transaction is broadcasted to is determined one of two ways:

  1. If the network parameter is set to evm, then the chain property from the EthereumLitTransaction object will be used to lookup the corresponding RPC URL to use for broadcasting the signed transaction.
  2. If the network parameter is set to solana, then the chain property from the SerializedTransaction object will be used to create a Connection instance, connected to the specified network.

unsignedTransaction

This parameter is the unsigned transaction that the Wrapped Key will sign. Depending on the network parameter, this object will be one of two options:

If the network parameter is set to evm, then unsignedTransaction will implement the EthereumLitTransaction interface:

/**  EthereumLitTransaction must be provided to the `SignTransaction` endpoint when `network` is `evm`.
*
* @typedef EthereumLitTransaction
*
* @property { string } toAddress The address the transaction is 'to'
* @property { string } value The value of the transaction to be sent
* @property { number } chainId The chain ID of the target chain that the transaction will be executed on
* @property { string } [gasPrice] The exact gas price that you are willing to pay to execute the transaction
* @property { string } [gasLimit] The maximum gas price that you are willing to pay to execute the transaction
* @property { string } [dataHex] Data in hex format to be included in the transaction
*
*/
interface EthereumLitTransaction {
chain: string;
toAddress: string;
value: string;
chainId: number;
gasPrice?: string;
gasLimit?: number;
dataHex?: string;
}

Parameters

chain
note

chain must be one of the supported EVM networks.

This parameters determines what chain will be used to the following:

  • Get the latest nonce for the address associated with the Wrapped Key.
  • Get the current gasPrice for the chain.
  • Get the estimated gasLimit for unsignedTransaction on the chain.
  • When broadcast is set to true, it will be the chain that the signed transaction is broadcasted to.
toAddress

This parameter is the EVM based address used as the to property of the transaction, and will be the recipient of the transaction's data and value.

value

This parameter is the amount of the native token on the chain that will be transferred to toAddress. Within the Wrapped Keys Lit Action, value will be parsed using Ethers.js' parseEther, so this value should be given as the number of tokens expressed in full units, not in Wei (or whatever the smallest domination is for the chain the transaction is being signed for).

For example, "1" should be used to transfer a whole token, ".5" for half a token, and ".01" for a hundredth of a token.

chainId

This parameter is the EIP-155 chain id that will be used in the transaction object that is signed by the Wrapped Key.

You can check ChainList for your chain's chainId.

gasPrice

This parameter will set the gasPrice of the transaction in wei. If this parameter is omitted, the Wrapped Keys Lit Action will fetch the current gasPrice for chain for you.

gasLimit

This parameter will set the gasLimit for the transaction. If this parameter is omitted, the Wrapped Keys Lit Action will attempt to estimate the gasLimit on the specified chain for you. Gas estimation is done using Ethers.js' estimateGas function.

There is the possibility that ethers fails to estimate the gas for your transaction, even when it's a valid transaction, and you will receive an error along the lines of Error: When estimating gas-.... In this case, you can try manually setting the gasLimit to circumvent ethers trying to estimate it.

dataHex

This parameter will set the data property for the transaction. Data should be UTF-8 bytes represented as a hexadecimal string. You can use ethers.js' hexlify and toUtf8Bytes (or similar) methods to convert a UTF-8 string.

For example:

import { ethers } from 'ethers';

const dataHex = ethers.utils.hexlify(
ethers.utils.toUtf8Bytes('The answer to the Universe is 42.')
);

Return Value

Depending on what network and broadcast is set to, what signTransactionWithEncryptedKey returns differs:

If network is set to evm and broadcast is set to false, then the return value of signTransactionWithEncryptedKey will be Promise<string> where the string is the signed transaction.

If broadcast is set to true, then the signed transaction will be broadcasted to the chain, and the return value of signTransactionWithEncryptedKey will be Promise<string> where the string is the transaction hash.

Example Implementation

Now that we know what the signTransactionWithEncryptedKey function does, it's parameters, and it's return values, let's now dig into a complete implementation.

The full code implementation can be found here.

Installing the Required Dependencies

npm install \
@lit-protocol/auth-helpers \
@lit-protocol/constants \
@lit-protocol/lit-auth-client \
@lit-protocol/lit-node-client \
@lit-protocol/wrapped-keys \
ethers@v5

Instantiating an Ethers Signer

The ETHEREUM_PRIVATE_KEY environment variable is required. The corresponding Ethereum address needs to have ownership of the PKP we will be using to generate the pkpSessionSigs.

import * as ethers from 'ethers';
import { LIT_RPC } from "@lit-protocol/constants";

const ethersSigner = new ethers.Wallet(
process.env.ETHEREUM_PRIVATE_KEY,
new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE)
);

Instantiating a LitNodeClient

Here we are instantiating an instance of LitNodeClient and connecting it to the datil-dev Lit network.

import { LitNodeClient } from "@lit-protocol/lit-node-client";
import { LIT_NETWORK } from "@lit-protocol/constants";

const litNodeClient = new LitNodeClient({
litNetwork: LIT_NETWORK.DatilDev,
debug: false,
});
await litNodeClient.connect();

Generating PKP Session Signatures

The LIT_PKP_PUBLIC_KEY environment variable is required. This PKP should be owned by the corresponding Ethereum address for the ETHEREUM_PRIVATE_KEY environment variable.

The PKP's Ethereum address will be used for the Access Control Conditions used to encrypt the generated private key, and by default, will be the only entity able to authorize decryption of the private key.

note

The expiration used for the Auth Method must be 10 minutes or less to be valid.

note

The Auth Method used in this example implementation is signing a Sign in With Ethereum (EIP-4361) message using an Externally Owned Account (EOA), but any Auth Method can be used to authenticate with Lit to get PKP Session Signatures.

import { EthWalletProvider } from "@lit-protocol/lit-auth-client";
import { LIT_ABILITY } from "@lit-protocol/constants";
import {
LitActionResource,
LitPKPResource,
} from "@lit-protocol/auth-helpers";

const pkpSessionSigs = await litNodeClient.getPkpSessionSigs({
pkpPublicKey: process.env.LIT_PKP_PUBLIC_KEY,
authMethods: [
await EthWalletProvider.authenticate({
signer: ethersSigner,
litNodeClient,
expiration: new Date(Date.now() + 1000 * 60 * 10).toISOString(), // 10 minutes
}),
],
resourceAbilityRequests: [
{
resource: new LitActionResource("*"),
ability: LIT_ABILITY.LitActionExecution,
},
],
expiration: new Date(Date.now() + 1000 * 60 * 10).toISOString(), // 10 minutes
});

Signing a Transaction With A Wrapped Key

Now that we know what the signTransactionWithEncryptedKey function does, it's parameters, and it's return values, let's now dig into a complete implementation.

The full code implementation can be found here.

import { api } from "@lit-protocol/wrapped-keys";

const { signTransactionWithEncryptedKey } = api;

const transactionHash = await signTransactionWithEncryptedKey({
pkpSessionSigs,
network: 'evm',
id: process.env.WRAPPED_KEY_ID,
unsignedTransaction: {
chain: "ethereum",
toAddress: process.env.ETHEREUM_TRANSACTION_RECIPIENT
value: "4.2" // This will be 4.2 ether
chainId: 1,
dataHex: ethers.utils.hexlify(
ethers.utils.toUtf8Bytes('The answer to the Universe is 42.')
)
},
broadcast: true,
litNodeClient,
});

Summary

The full code implementation can be found here.

After executing the example implementation above, you will have a signed transaction using the Wrapped Key that's associated with PKP derived from the provided pkpSessionSigs. If broadcast was set to true, then the signed transaction was also broadcasted to the chain.