delegate-framework
Version:
A TypeScript framework for building robust, production-ready blockchain workflows with comprehensive error handling, logging, and testing. Maintained by delegate.fun
209 lines • 10.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Hopper = void 0;
const web3_js_1 = require("@solana/web3.js");
const spl_token_1 = require("@solana/spl-token");
const base_delegate_1 = require("./base-delegate");
const constants_1 = require("./constants");
const bs58_1 = __importDefault(require("bs58"));
class Hopper extends base_delegate_1.BaseDelegate {
constructor(connection, signerKeypair, heliusClient, feeTakerKeypair) {
super(connection, signerKeypair, feeTakerKeypair);
this.heliusClient = heliusClient;
}
async executeDelegate(delegateOptions) {
const requestId = this.generateRequestId();
try {
this.logOperation('hopper_execution_started', { requestId });
this.validateOptions(delegateOptions);
const { hopDestination, numOfHops, numTokens, tokenType, tokenAddress } = delegateOptions;
const hopDestinationPubkey = new web3_js_1.PublicKey(hopDestination);
const hopMap = [];
const signatures = [];
this.logOperation('hopper_setup', {
requestId,
numOfHops,
tokenType,
destination: hopDestination
});
if (tokenType === 'sol') {
// Native SOL hopper logic
const result = await this.executeSolHopping(numOfHops, numTokens, hopDestinationPubkey, hopMap, requestId);
signatures.push(...result.signatures);
}
else {
// Token hopper logic
if (!tokenAddress) {
throw new Error("tokenAddress is required when tokenType is 'token'");
}
const result = await this.executeTokenHopping(numOfHops, numTokens, tokenAddress, hopDestinationPubkey, hopMap, requestId);
signatures.push(...result.signatures);
}
this.logOperation('hopper_execution_completed', {
requestId,
signatures,
totalHops: numOfHops
});
return {
success: true,
signatures,
hopMap,
finalDestination: hopDestination,
totalHops: numOfHops
};
}
catch (error) {
await this.handleError(error instanceof Error ? error : new Error(String(error)), { requestId });
throw error;
}
}
validateOptions(delegateOptions) {
this.validateRequiredField(delegateOptions.hopDestination, 'hopDestination');
this.validatePublicKey(delegateOptions.hopDestination, 'hopDestination');
this.validateNumberField(delegateOptions.numOfHops, 'numOfHops', 1, 100);
this.validateNumberField(delegateOptions.numTokens, 'numTokens', 0);
if (delegateOptions.tokenType !== 'sol' && delegateOptions.tokenType !== 'token') {
throw new Error("tokenType must be either 'sol' or 'token'");
}
if (delegateOptions.tokenType === 'token') {
this.validateRequiredField(delegateOptions.tokenAddress, 'tokenAddress');
this.validatePublicKey(delegateOptions.tokenAddress, 'tokenAddress');
}
}
async executeSolHopping(numOfHops, numTokens, hopDestination, hopMap, requestId) {
const signatures = [];
const rentAndMinBalance = constants_1.TOKEN_ACCOUNT_RENT + 50000; // Rent + minimum balance
const costBuffer = Math.max(rentAndMinBalance, 50000); // Ensure we always leave at least rent
const initialBalance = numTokens * web3_js_1.LAMPORTS_PER_SOL;
let lastHopKeypair = this.signerKeypair;
for (let i = 0; i < numOfHops; i++) {
this.logOperation('sol_hop_progress', {
requestId,
currentHop: i + 1,
totalHops: numOfHops
});
const senderBalance = i === 0 ? initialBalance : await this.heliusClient.getBalance(lastHopKeypair.publicKey);
const newHopKeypair = web3_js_1.Keypair.generate();
hopMap.push({
publicKey: newHopKeypair.publicKey.toBase58(),
privateKey: bs58_1.default.encode(newHopKeypair.secretKey)
});
const transaction = new web3_js_1.Transaction();
transaction.add(web3_js_1.SystemProgram.transfer({
fromPubkey: lastHopKeypair.publicKey,
toPubkey: (i === numOfHops - 1) ? hopDestination : newHopKeypair.publicKey,
lamports: senderBalance - costBuffer
}));
const signature = await this.executeTransaction(transaction, lastHopKeypair, requestId);
signatures.push(signature);
lastHopKeypair = newHopKeypair;
}
return { signatures };
}
async executeTokenHopping(numOfHops, numTokens, tokenAddress, hopDestination, hopMap, requestId) {
const signatures = [];
const mint = new web3_js_1.PublicKey(tokenAddress);
const decimals = await this.getTokenDecimals(tokenAddress);
const amountToTransfer = Math.floor(this.calculateAmountToTransfer(numTokens, 1, decimals));
if (decimals === -1) {
throw new Error("Failed to get token decimals");
}
let lastHopKeypair = this.signerKeypair;
let lastHopTokenAccount = null;
// Get or create initial token account
lastHopTokenAccount = await this.retryOperation(async () => {
return await (0, spl_token_1.getOrCreateAssociatedTokenAccount)(this.connection, lastHopKeypair, mint, lastHopKeypair.publicKey, true);
}, 5);
if (!lastHopTokenAccount) {
throw new Error("Failed to get last hop token account");
}
for (let i = 0; i < numOfHops; i++) {
this.logOperation('token_hop_progress', {
requestId,
currentHop: i + 1,
totalHops: numOfHops
});
const initialBalance = await this.getTokenBalance(lastHopTokenAccount.address.toBase58());
if (initialBalance < amountToTransfer) {
throw new Error(`Insufficient token balance. Required: ${amountToTransfer}, Available: ${initialBalance}`);
}
const newHopKeypair = web3_js_1.Keypair.generate();
hopMap.push({
publicKey: newHopKeypair.publicKey.toBase58(),
privateKey: bs58_1.default.encode(newHopKeypair.secretKey)
});
const newHopAddress = (i === numOfHops - 1) ? hopDestination : newHopKeypair.publicKey;
const newHopTokenAccount = await this.retryOperation(async () => {
return await (0, spl_token_1.getOrCreateAssociatedTokenAccount)(this.connection, lastHopKeypair, mint, newHopAddress, true);
}, 5);
if (!newHopTokenAccount) {
throw new Error("Failed to get new hop token account");
}
// Transfer tokens
const tokenTransaction = new web3_js_1.Transaction();
tokenTransaction.add((0, spl_token_1.createTransferCheckedInstruction)(lastHopTokenAccount.address, mint, newHopTokenAccount.address, lastHopKeypair.publicKey, amountToTransfer, decimals));
const tokenSignature = await this.executeTransaction(tokenTransaction, lastHopKeypair, requestId);
signatures.push(tokenSignature);
// Verify transfer
const newBalance = await this.getTokenBalance(newHopTokenAccount.address.toBase58());
if (newBalance < amountToTransfer) {
throw new Error("Token transfer may have failed - balance not updated");
}
// Transfer SOL for fees
const solTransaction = new web3_js_1.Transaction();
const senderBalance = await this.heliusClient.getBalance(lastHopKeypair.publicKey);
const costBuffer = constants_1.TOKEN_ACCOUNT_RENT * 1.5;
const solToSend = (i === 0 && senderBalance > 0.01 * web3_js_1.LAMPORTS_PER_SOL)
? (numOfHops * 0.01 * web3_js_1.LAMPORTS_PER_SOL) - costBuffer
: senderBalance - costBuffer;
solTransaction.add(web3_js_1.SystemProgram.transfer({
fromPubkey: lastHopKeypair.publicKey,
toPubkey: (i === numOfHops - 1) ? new web3_js_1.PublicKey(constants_1.FEE_WALLET_ADDRESS) : newHopAddress,
lamports: solToSend
}));
const solSignature = await this.executeTransaction(solTransaction, lastHopKeypair, requestId);
signatures.push(solSignature);
lastHopKeypair = newHopKeypair;
lastHopTokenAccount = newHopTokenAccount;
}
return { signatures };
}
async executeTransaction(transaction, signer, requestId) {
transaction.feePayer = signer.publicKey;
const blockhash = await this.retryOperation(async () => {
return (await this.connection.getLatestBlockhash()).blockhash;
}, 3);
transaction.recentBlockhash = blockhash;
transaction.sign(signer);
const signature = await this.retryOperation(async () => {
return await this.connection.sendTransaction(transaction, [signer], {
skipPreflight: false,
maxRetries: 3,
});
}, 3);
await this.retryOperation(async () => {
await this.connection.confirmTransaction(signature, 'confirmed');
}, 3);
this.logOperation('transaction_confirmed', { requestId, signature });
return signature;
}
async getTokenBalance(tokenAccountAddress) {
const balanceInfo = await this.heliusClient.getTokenAccountBalance(new web3_js_1.PublicKey(tokenAccountAddress));
return parseFloat(balanceInfo.value.uiAmountString || '0');
}
async getTokenDecimals(tokenAddress) {
const mint = new web3_js_1.PublicKey(tokenAddress);
const mintInfo = await this.retryOperation(async () => {
return await (0, spl_token_1.getMint)(this.connection, mint);
}, 3);
return mintInfo.decimals;
}
calculateAmountToTransfer(numTokens, numRecipients, decimals) {
return Math.floor((numTokens / numRecipients) * Math.pow(10, decimals));
}
}
exports.Hopper = Hopper;
//# sourceMappingURL=hopper.js.map