@lit-protocol/vincent-scaffold-sdk
Version:
Shared build configuration and utilities for Vincent abilities and policies
189 lines (165 loc) • 5.88 kB
text/typescript
import { alchemy } from "@account-kit/infra";
import { LitActionsSmartSigner } from "./litActionSmartSigner";
import { createModularAccountV2Client } from "@account-kit/smart-contracts";
import { getAlchemyChainConfig } from "../../../la-helpers/getAlchemyChainConfig";
/**
* Handler function for making contract calls
* This function handles the preparation, signing, and sending of contract transactions
*
* @param pkpPublicKey - The PKP public key for transaction signing
* @param pkpEthAddress - The ethereum address derived from PKP
* @param abi - The ABI of the contract function
* @param contractAddress - The contract address
* @param functionName - The name of the function to call
* @param args - The arguments to pass to the function
* @param overrides - Optional transaction overrides (value)
* @param chainId - Optional chain ID (defaults to yellowstoneConfig.id)
* @param eip7702AlchemyApiKey - The Alchemy API key for gas sponsorship
* @param eip7702AlchemyPolicyId - The Alchemy policy ID for gas sponsorship
* @returns The UserOperation hash. You must use the alchemy smartAccountClient.waitForUserOperationTransaction() to convert the userOp into a txHash.
*/
export const sponsoredGasContractCall = async ({
pkpPublicKey,
abi,
contractAddress,
functionName,
args,
overrides = {},
chainId,
eip7702AlchemyApiKey,
eip7702AlchemyPolicyId,
}: {
pkpPublicKey: string;
abi: any[];
contractAddress: string;
functionName: string;
args: any[];
overrides?: {
value?: string | number | bigint;
gasLimit?: number;
};
chainId?: number;
eip7702AlchemyApiKey?: string;
eip7702AlchemyPolicyId?: string;
}) => {
// Step 1: Encode function data using ethers Interface
const iface = new ethers.utils.Interface(abi);
const encodedData = iface.encodeFunctionData(functionName, args);
console.log("Encoded data:", encodedData);
if (!eip7702AlchemyApiKey || !eip7702AlchemyPolicyId) {
throw new Error(
"EIP7702 Alchemy API key and policy ID are required when using Alchemy for gas sponsorship"
);
}
if (!chainId) {
throw new Error(
"Chain ID is required when using Alchemy for gas sponsorship"
);
}
// Convert value override if exists to BigNumber
const txValue = overrides.value ? BigInt(overrides.value.toString()) : 0n;
// Create LitActionsSmartSigner for EIP-7702
const litSigner = new LitActionsSmartSigner({
pkpPublicKey,
chainId,
});
// Get the Alchemy chain configuration
const alchemyChain = getAlchemyChainConfig(chainId);
// Create the Smart Account Client with EIP-7702 mode
const smartAccountClient = await createModularAccountV2Client({
mode: "7702" as const,
transport: alchemy({ apiKey: eip7702AlchemyApiKey }),
chain: alchemyChain,
signer: litSigner,
policyId: eip7702AlchemyPolicyId,
});
console.log("Smart account client created");
// Prepare the user operation
const userOperation = {
target: contractAddress as `0x${string}`,
value: txValue,
data: encodedData as `0x${string}`,
};
console.log("User operation prepared", userOperation);
// Build the user operation
const uoStructResponse = await Lit.Actions.runOnce(
{
waitForResponse: true,
name: "buildUserOperation",
},
async () => {
try {
const uoStruct = await smartAccountClient.buildUserOperation({
uo: userOperation,
account: smartAccountClient.account,
});
// Properly serialize BigInt with a "type" tag
return JSON.stringify(uoStruct, (_, v) =>
typeof v === "bigint" ? { type: "BigInt", value: v.toString() } : v
);
} catch (e: any) {
console.log("Failed to build user operation, error below");
console.log(e);
console.log(e.stack);
return "";
}
}
);
if (uoStructResponse === "") {
throw new Error("Failed to build user operation");
}
// Custom reviver to convert {type: "BigInt", value: "..."} back to BigInt
const uoStruct = JSON.parse(uoStructResponse, (_, v) => {
if (
v &&
typeof v === "object" &&
v.type === "BigInt" &&
typeof v.value === "string"
) {
return BigInt(v.value);
}
return v;
});
console.log("User operation built, starting signing...", uoStruct);
// sign the actual user operation with the PKP.
// this must be done outside a runOnce call, because all the nodes must initiate a signature for it to be valid
const signedUserOperation = await smartAccountClient.signUserOperation({
account: smartAccountClient.account,
uoStruct,
});
console.log("User operation signed", signedUserOperation);
// getting the entry point from the smart account client so we can send the user operation
const entryPoint = smartAccountClient.account.getEntryPoint();
// console.log("Entry point", entryPoint);
// send the user operation with EIP-7702 delegation in a runOnce
// so that we don't submit it more than once
const uoHash = await Lit.Actions.runOnce(
{
waitForResponse: true,
name: "sendWithAlchemy",
},
async () => {
try {
// Send the user operation with EIP-7702 delegation
const userOpResult = await smartAccountClient.sendRawUserOperation(
signedUserOperation,
entryPoint.address
);
console.log(
`[@lit-protocol/vincent-ability-morpho/executeOperationWithGasSponsorship] User operation sent`,
{ userOpHash: userOpResult }
);
return userOpResult;
} catch (e: any) {
console.log("Failed to send user operation, error below");
console.log(e);
console.log(e.stack);
return "";
}
}
);
if (uoHash === "") {
throw new Error("Failed to send user operation");
}
return uoHash;
};