Add api to create a lock

This commit is contained in:
AdityaSalunkhe21 committed 2025-02-06 11:18:22 +05:30
1 parent fa168e5c61
commit 303d238d6c
12 files changed
+7471 -13

No files matched your search

+1
View File
@@ -88,6 +88,7 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
// Extract the image URL from the response
const imageUrl = result.data?.images?.[0]?.url
console.log(imageUrl);
if (!imageUrl) {
console.error('No image URL in response:', result)
+172
View File
@@ -0,0 +1,172 @@
import { NextRequest, NextResponse } from "next/server";
import { Connection, Keypair, ParsedInstruction, ParsedTransactionWithMeta, PartiallyDecodedInstruction, PublicKey } from "@solana/web3.js";
import { createLock } from "../../../utils/create-lock";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import assert from "assert";
import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes";
import { BN, min } from "bn.js";
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL);
const connection = new Connection(process.env.NEXT_PUBLIC_SOLANA_RPC_URL);
const MTM_MINT_ADDRESS = process.env.NEXT_PUBLIC_MTM_TOKEN_MINT;
function isParsedInstruction(
instruction: ParsedInstruction | PartiallyDecodedInstruction
): instruction is ParsedInstruction {
return (instruction as ParsedInstruction).parsed !== undefined;
}
async function extractMTMTransferDetails(
connection: Connection,
signature: string,
mtmMintAddress: string // Mint address of the MTM token
) {
try {
// Fetch the transaction details using the signature
const transaction: ParsedTransactionWithMeta | null = await connection.getParsedTransaction(signature, {
maxSupportedTransactionVersion: 0, // Ensure compatibility with legacy transactions
});
if (!transaction) {
throw new Error('Transaction not found');
}
// Extract the "from", "to", and "amount" for the MTM token transfer
let fromAddress: string | null = null;
let toAddress: string | null = null;
let tokenAmount: number | null = null;
// Function to process instructions
const processInstructions = (instructions: ParsedInstruction[]) => {
instructions.forEach((instruction: ParsedInstruction) => {
if (instruction.programId.equals(new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'))) {
// Check if the instruction is a transfer instruction
if (instruction.parsed.type === 'transfer' || instruction.parsed.type === 'transferChecked') {
const parsedInfo = instruction.parsed.info;
const mint = parsedInfo.mint;
// Check if the mint address matches the provided MTM mint address
if (mint === mtmMintAddress) {
fromAddress = parsedInfo.source;
toAddress = parsedInfo.destination;
tokenAmount = parsedInfo.amount;
}
}
}
});
};
// Filter out PartiallyDecodedInstruction and process only ParsedInstruction
const parsedInstructions = transaction.transaction.message.instructions.filter(isParsedInstruction);
// Process top-level instructions
processInstructions(parsedInstructions);
// Process inner instructions (if any)
if (transaction.meta?.innerInstructions) {
transaction.meta.innerInstructions.forEach((inner) => {
const innerParsedInstructions = inner.instructions.filter(isParsedInstruction);
processInstructions(innerParsedInstructions);
});
}
if (!fromAddress || !toAddress || !tokenAmount) {
throw new Error('No matching MTM token transfer found in the transaction');
}
return { fromAddress, toAddress, tokenAmount };
} catch (error) {
console.error('Error extracting MTM transfer details:', error);
throw error;
}
}
export async function GET(req: NextRequest) {
try {
// Get signature from URL params
const { searchParams } = new URL(req.url);
const signature = searchParams.get('signature') || '4HBtnoNUuMGpmbhD9cPiJtbxkhux31pfZs3HYud5eopAU69RaC4UbJsYdj83eafFxV6eH8pSaRgqELrwyjrWp7yz';
let amount = new BN(0);
if (!signature) {
return NextResponse.json(
{ error: "Transaction signature is required" },
{ status: 400 }
);
}
if (!MTM_MINT_ADDRESS) {
return NextResponse.json(
{ error: "MTM_MINT_ADDRESS environment variable is not set" },
{ status: 500 }
);
}
// Extract transaction details
// const result = await extractMTMTransferDetails(
// connection,
// signature,
// MTM_MINT_ADDRESS
// );
// if (!result) {
// return NextResponse.json(
// { error: "Failed to extract transaction details" },
// { status: 500 }
// );
// }
// console.log({ result });
// // Validate extracted values
// if (!result.fromAddress || result.tokenAmount <= 0) {
// return NextResponse.json(
// { error: "Invalid transaction details extracted" },
// { status: 400 }
// );
// }
assert(process.env.USER_PRIVATE_KEY, 'USER_PRIVATE_KEY is required');
assert(process.env.CLIFF_TIME, 'CLIFF_TIME is required');
const USER_PRIVATE_KEY = process.env.USER_PRIVATE_KEY;
const CLIFF_TIME = process.env.CLIFF_TIME;
const duration = new BN(CLIFF_TIME).add(new BN(Math.floor(Date.now() / 1000)));
const tokenLockerKeypair = Keypair.fromSecretKey(bs58.decode(USER_PRIVATE_KEY));
const recipientPublicKey = new PublicKey('Bnnq8n3rRKZe8NJAYn4vBkxp1v8Bnc6zXpUPpDeujCu');
amount = amount.add(new BN(1000000));
// Call createLock function with extracted values
let escrow;
try {
escrow = await createLock(tokenLockerKeypair, recipientPublicKey, duration, amount);
} catch (error) {
console.error('Error creating lock:', error);
return NextResponse.json(
{ error: "Failed to create lock" },
{ status: 500 }
);
}
console.log({ escrow });
// Return successful response
return NextResponse.json({
success: true,
data: {
sender: recipientPublicKey,
amount: amount,
}
});
} catch (error) {
console.error('API route error:', error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
+254
View File
@@ -0,0 +1,254 @@
/**
* Methods from jup-lock:
* - createLockerProgram
* - deriveEscrow
* - createVestingPlanV2
* Reference: https://github.com/jup-ag/jup-lock/blob/main/tests/locker_utils/index.ts
*/
import assert from 'assert';
import 'dotenv/config';
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
createAssociatedTokenAccountInstruction,
getAssociatedTokenAddressSync,
TOKEN_2022_PROGRAM_ID,
TOKEN_PROGRAM_ID,
} from '@solana/spl-token';
import {
AnchorProvider,
BN,
Program,
Wallet,
web3,
workspace,
} from '@coral-xyz/anchor';
import { AccountMeta, Connection, SendTransactionError } from '@solana/web3.js';
// TODO: Generate type file from IDL json
import { Locker } from '../../target/types/locker';
import { TokenExtensionUtil } from './token-2022/token-extensions';
import {
OptionRemainingAccountsInfoData,
RemainingAccountsBuilder,
RemainingAccountsType,
} from './token-2022/remaining-accounts';
assert(process.env.RPC_ENDPOINT);
const connection = new Connection(process.env.RPC_ENDPOINT);
const ESCROW_USE_SPL_TOKEN = 0;
const MEMO_PROGRAM = new web3.PublicKey(
"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
);
export function createLockerProgram(wallet: Wallet): Program<Locker> {
const provider = new AnchorProvider(connection, wallet, {
maxRetries: 3,
});
provider.opts.commitment = 'confirmed';
return workspace.Locker as Program<Locker>;
}
export function deriveEscrow(base: web3.PublicKey, programId: web3.PublicKey) {
return web3.PublicKey.findProgramAddressSync(
[Buffer.from('escrow'), base.toBuffer()],
programId
);
}
export interface CreateVestingPlanParams {
ownerKeypair: web3.Keypair;
tokenMint: web3.PublicKey;
isAssertion: boolean;
vestingStartTime: BN;
cliffTime: BN;
frequency: BN;
cliffUnlockAmount: BN;
amountPerPeriod: BN;
numberOfPeriod: BN;
recipient: web3.PublicKey;
updateRecipientMode: number;
cancelMode: number;
tokenProgram?: web3.PublicKey;
}
// V2 instructions
export async function createVestingPlanV2(params: CreateVestingPlanParams) {
let {
tokenMint,
ownerKeypair,
vestingStartTime,
cliffTime,
frequency,
cliffUnlockAmount,
amountPerPeriod,
numberOfPeriod,
recipient,
updateRecipientMode,
cancelMode,
tokenProgram,
} = params;
const program = createLockerProgram(new Wallet(ownerKeypair));
const baseKP = web3.Keypair.generate();
let [escrow] = deriveEscrow(baseKP.publicKey, program.programId);
const senderToken = getAssociatedTokenAddressSync(
tokenMint,
ownerKeypair.publicKey,
false,
tokenProgram,
ASSOCIATED_TOKEN_PROGRAM_ID
);
const escrowToken = getAssociatedTokenAddressSync(
tokenMint,
escrow,
true,
tokenProgram,
ASSOCIATED_TOKEN_PROGRAM_ID
);
let remainingAccountsInfo = null;
let remainingAccounts: AccountMeta[] = [];
if (tokenProgram == TOKEN_2022_PROGRAM_ID) {
let inputTransferHookAccounts =
await TokenExtensionUtil.getExtraAccountMetasForTransferHook(
program.provider.connection,
tokenMint,
senderToken,
escrowToken,
ownerKeypair.publicKey,
TOKEN_2022_PROGRAM_ID
);
[remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder()
.addSlice(
RemainingAccountsType.TransferHookEscrow,
inputTransferHookAccounts
)
.build() as [OptionRemainingAccountsInfoData, AccountMeta[]];
}
assert(tokenProgram);
try {
const transaction = await program.methods
.createVestingEscrowV2(
{
vestingStartTime,
cliffTime,
frequency,
cliffUnlockAmount,
amountPerPeriod,
numberOfPeriod,
updateRecipientMode,
cancelMode,
},
remainingAccountsInfo
)
.accounts({
base: baseKP.publicKey,
senderToken,
escrowToken,
recipient,
tokenMint,
sender: ownerKeypair.publicKey,
tokenProgram,
systemProgram: web3.SystemProgram.programId,
escrow,
// TODO: Fix type error for escrowToken
} as any)
.remainingAccounts(remainingAccounts ? remainingAccounts : [])
.preInstructions([
createAssociatedTokenAccountInstruction(
ownerKeypair.publicKey,
escrowToken,
escrow,
tokenMint,
tokenProgram,
ASSOCIATED_TOKEN_PROGRAM_ID
),
])
.signers([baseKP, ownerKeypair])
.rpc();
return escrow;
} catch (error) {
if (error instanceof SendTransactionError) {
console.error('Transaction failed:', error.message);
console.error('Logs:', error.logs);
}
throw error;
}
}
export interface ClaimTokenParamsV2 {
isAssertion: boolean;
escrow: web3.PublicKey;
recipient: web3.Keypair;
maxAmount: BN;
recipientToken: web3.PublicKey;
tokenProgram: web3.PublicKey;
}
export async function claimTokenV2(params: ClaimTokenParamsV2) {
let { isAssertion, escrow, recipient, maxAmount, recipientToken } = params;
const program = createLockerProgram(new Wallet(recipient));
const escrowState = await program.account.vestingEscrow.fetch(escrow);
const tokenProgram =
escrowState.tokenProgramFlag == ESCROW_USE_SPL_TOKEN
? TOKEN_PROGRAM_ID
: TOKEN_2022_PROGRAM_ID;
const escrowToken = getAssociatedTokenAddressSync(
escrowState.tokenMint,
escrow,
true,
tokenProgram,
ASSOCIATED_TOKEN_PROGRAM_ID
);
let remainingAccountsInfo = null;
let remainingAccounts: AccountMeta[] | undefined = [];
if (tokenProgram == TOKEN_2022_PROGRAM_ID) {
let claimTransferHookAccounts =
await TokenExtensionUtil.getExtraAccountMetasForTransferHook(
program.provider.connection,
escrowState.tokenMint,
escrowToken,
recipientToken,
escrow,
TOKEN_2022_PROGRAM_ID
);
[remainingAccountsInfo, remainingAccounts] = new RemainingAccountsBuilder()
.addSlice(
RemainingAccountsType.TransferHookEscrow,
claimTransferHookAccounts
)
.build();
}
const tx = await program.methods
.claimV2(maxAmount, remainingAccountsInfo)
.accounts({
tokenProgram,
tokenMint: escrowState.tokenMint,
memoProgram: MEMO_PROGRAM,
escrow,
escrowToken,
recipient: recipient.publicKey,
recipientToken,
} as any)
.remainingAccounts(remainingAccounts ? remainingAccounts : [])
.signers([recipient])
.rpc();
}
@@ -0,0 +1,50 @@
// Reference: https://github.com/jup-ag/jup-lock/blob/main/tests/locker_utils/index.ts
import { AccountMeta } from '@solana/web3.js';
export enum RemainingAccountsType {
TransferHookEscrow = 'transferHookEscrow',
}
type RemainingAccountsAnchorType = { transferHookEscrow: {} };
export type RemainingAccountsSliceData = {
accountsType: RemainingAccountsAnchorType;
length: number;
};
export type RemainingAccountsInfoData = {
slices: RemainingAccountsSliceData[];
};
// Option<RemainingAccountsInfoData> on Rust
// null is treated as None in Rust. undefined doesn't work.
export type OptionRemainingAccountsInfoData = RemainingAccountsInfoData | null;
export class RemainingAccountsBuilder {
private remainingAccounts: AccountMeta[] = [];
private slices: RemainingAccountsSliceData[] = [];
constructor() {}
addSlice(
accountsType: RemainingAccountsType,
accounts?: AccountMeta[]
): this {
if (!accounts || accounts.length === 0) return this;
this.slices.push({
accountsType: { [accountsType]: {} } as RemainingAccountsAnchorType,
length: accounts.length,
});
this.remainingAccounts.push(...accounts);
return this;
}
build(): [OptionRemainingAccountsInfoData, AccountMeta[] | undefined] {
return this.slices.length === 0
? [null, undefined]
: [{ slices: this.slices }, this.remainingAccounts];
}
}
@@ -0,0 +1,57 @@
// Reference: https://github.com/jup-ag/jup-lock/blob/main/tests/locker_utils/index.ts
import {
AccountMeta,
Connection,
PublicKey,
TransactionInstruction,
} from '@solana/web3.js';
import {
addExtraAccountsToInstruction,
getMint,
getTransferHook,
TOKEN_2022_PROGRAM_ID,
} from '@solana/spl-token';
export class TokenExtensionUtil {
public static async getExtraAccountMetasForTransferHook(
connection: Connection,
tokenMint: PublicKey,
source: PublicKey,
destination: PublicKey,
owner: PublicKey,
tokenProgram: PublicKey
): Promise<AccountMeta[] | undefined> {
let mint = await getMint(connection, tokenMint, 'confirmed', tokenProgram);
const transferHook = getTransferHook(mint);
if (!transferHook) return undefined;
const instruction = new TransactionInstruction({
programId: TOKEN_2022_PROGRAM_ID,
keys: [
{ pubkey: source, isSigner: false, isWritable: false },
{
pubkey: tokenMint,
isSigner: false,
isWritable: false,
},
{ pubkey: destination, isSigner: false, isWritable: false },
{ pubkey: owner, isSigner: false, isWritable: false },
{ pubkey: owner, isSigner: false, isWritable: false },
],
});
// Note:
await addExtraAccountsToInstruction(
connection,
instruction,
tokenMint,
'confirmed',
transferHook.programId,
);
const extraAccountMetas = instruction.keys.slice(5);
return extraAccountMetas.length > 0 ? extraAccountMetas : undefined;
}
}
+95
View File
@@ -0,0 +1,95 @@
import assert from 'assert';
import BN from 'bn.js';
import 'dotenv/config';
import bs58 from 'bs58';
import * as anchor from "@coral-xyz/anchor";
import {
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { Connection, PublicKey } from "@solana/web3.js";
import { createVestingPlanV2 } from '../locker-utils';
assert(process.env.RPC_ENDPOINT);
assert(process.env.USER_PRIVATE_KEY);
//assert(process.env.RECIPIENT_PUBLIC_KEY);
assert(process.env.MTM_MINT);
const RPC_ENDPOINT= process.env.RPC_ENDPOINT;
const MTM_MINT = process.env.MTM_MINT;
const USER_PRIVATE_KEY = process.env.USER_PRIVATE_KEY;
const userKP = anchor.web3.Keypair.fromSecretKey(bs58.decode(USER_PRIVATE_KEY));
const connection = new Connection(RPC_ENDPOINT);
const token = new PublicKey(MTM_MINT);
const provider = new anchor.AnchorProvider(
connection,
new anchor.Wallet(userKP),
// Commitment level required for simulating transaction
{ preflightCommitment: 'processed' }
);
anchor.setProvider(provider);
export async function getMTMBalance (senderKeypair: anchor.web3.Keypair): Promise<BN> {
const mintPublicKey = new PublicKey(MTM_MINT);
const publicKey = senderKeypair.publicKey;
const tokenAccounts = await connection.getTokenAccountsByOwner(publicKey, { mint: mintPublicKey });
let balance = new BN(0);
for (const tokenAccount of tokenAccounts.value) {
const accountInfo = await connection.getParsedAccountInfo(
tokenAccount.pubkey,
"confirmed"
);
if (!accountInfo.value || Buffer.isBuffer(accountInfo.value.data)) {
console.warn(
`Token account ${tokenAccount.pubkey.toBase58()} data is not parsed.`
);
continue;
}
const tokenAmount =
(accountInfo.value.data as any).parsed?.info?.tokenAmount?.amount || "0";
balance = balance.add(new BN(tokenAmount));
}
return balance;
};
export async function createLock(tokenLockerKeypair: anchor.web3.Keypair, recipientPubKey: anchor.web3.PublicKey, duration: BN, balance: BN): Promise<anchor.web3.PublicKey | void> {
//const balance = await getMTMBalance(tokenLockerKeypair);
if (balance.eq(new BN(0))) {
console.log('No balance available to create lock, skipping...');
return;
}
console.log('Creating a lock...');
const escrow = await createVestingPlanV2({
ownerKeypair: tokenLockerKeypair,
vestingStartTime: new BN(Math.floor(Date.now() / 1000) - 60), // Start immediately
tokenMint: token,
isAssertion: true,
cliffTime: duration,
frequency: new BN(1), // Not needed since full unlock happens at cliff
cliffUnlockAmount: balance, // The entire amount should be released at cliff
amountPerPeriod: new BN(0), // No tokens should be released before cliff
numberOfPeriod: new BN(1), // Only release tokens once
recipient: recipientPubKey,
updateRecipientMode: 0,
cancelMode: 1,
tokenProgram: TOKEN_PROGRAM_ID,
});
if (escrow) {
console.log('Lock created successfully: ', escrow.toString());
}
return escrow;
}
+32 -11
View File
@@ -1,28 +1,49 @@
import { PinataSDK } from 'pinata-web3';
import PinataClient from "@pinata/sdk"
import 'dotenv/config';
import assert from 'assert';
import { FluxGenerationResult } from '../services/fluxService';
import axios from 'axios';
import FormData from 'form-data';
assert(process.env.PINATA_JWT, "PINATA_JWT is required");
assert(process.env.PINATA_GATEWAY, "PINATA_GATEWAY is required");
assert(process.env.PINATA_API_KEY, "PINATA_API_KEY is required");
assert(process.env.PINATA_SECRET_KEY, "PINATA_SECRET_KEY is required");
assert(process.env.PINATA_JWT, "PINATA_SECRET_KEY is required");
assert(process.env.PINATA_GATEWAY);
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT,
pinataGateway: process.env.PINATA_GATEWAY,
const pinata = new PinataClient({
pinataApiKey: process.env.PINATA_API_KEY,
pinataSecretApiKey: process.env.PINATA_SECRET_KEY,
pinataJWTKey: process.env.PINATA_JWT
});
export async function uploadToPinata(imageUrl: string): Promise<FluxGenerationResult> {
try {
const upload = await pinata.upload.url(imageUrl);
const response = await axios.get(imageUrl, {
responseType: 'arraybuffer'
});
const publicURL = await pinata.gateways.convert(upload.IpfsHash);
// Create form data
const formData = new FormData();
formData.append('file', Buffer.from(response.data), {
filename: `image-${Date.now()}.jpg`,
contentType: 'image/jpeg',
});
// Use the correct Pinata method for file upload
const result = await pinata.pinFileToIPFS(formData);
if (!result.IpfsHash) {
throw new Error('IPFS hash not received from Pinata');
}
const publicURL = `${process.env.PINATA_GATEWAY}/ipfs/${result.IpfsHash}`;
return { imageUrl: publicURL };
} catch (error) {
console.error('Error uploading to Pinata:', error)
console.error('Error uploading to Pinata:', error);
return {
error: error instanceof Error ? error.message : 'Upload failed'
};
}
}
}