import {
clusterApiUrl,
ComputeBudgetProgram,
Connection,
Keypair,
MessageV0,
PACKET_DATA_SIZE,
PublicKey,
TransactionInstruction,
VersionedTransaction
} from "@solana/web3.js";
import {
constants,
findAssociatedTokenAddress,
helpers,
spl,
txs,
programs
} from "@debridge-finance/solana-utils";
import { ChainId, Solana } from "@debridge-finance/dln-client";
import bs58 from "bs58";
import { getEnvConfig } from "../../utils";
interface IOrderFromApi {
orderId: { stringValue: string; bytesArrayValue: string };
affiliateFee: { beneficiarySrc: { stringValue: string } };
giveOfferWithMetadata: { tokenAddress: { stringValue: string } };
}
// Query unlocked orders with affiliate fees
async function getUnlockedOrders({
chainIds,
beneficiaryAddress,
ref,
}: {
chainIds: number[];
beneficiaryAddress: PublicKey;
ref?: string;
}): Promise<IOrderFromApi[]> {
const MAX = 100;
let n = 0;
let lastOrders = [];
lastOrders.length = MAX;
let allOrders: IOrderFromApi[] = [];
try {
for (; ;) {
const response = await fetch("https://stats-api.dln.trade/api/Orders/filteredList", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
giveChainIds: chainIds,
takeChainIds: [],
orderStates: ["ClaimedUnlock"], // Only orders ready for withdrawal
filter: beneficiaryAddress.toString(),
referralCode: ref,
skip: n * MAX,
take: MAX,
}),
}).then((r) => r.json());
n += 1;
lastOrders = response.orders;
allOrders = [...allOrders, ...response.orders];
if (!lastOrders.length) break;
}
} catch (e) {
console.error(e);
}
// Filter to only orders where you're the beneficiary
allOrders = allOrders.filter(
(order) => order.affiliateFee.beneficiarySrc.stringValue === beneficiaryAddress.toString(),
);
return allOrders;
}
// Build withdrawal instruction for a single order
function buildWithdrawAffiliateFeeIx(
client: Solana.DlnClient,
orderId: Buffer,
beneficiary: PublicKey,
tokenMint: PublicKey,
tokenProgram?: PublicKey
) {
const discriminator = [143, 79, 158, 208, 125, 51, 86, 85];
tokenProgram = tokenProgram ?? new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
const ix = new TransactionInstruction({
keys: [
{ isSigner: true, isWritable: true, pubkey: beneficiary },
{ isSigner: false, isWritable: true, pubkey: findAssociatedTokenAddress(beneficiary, tokenMint, tokenProgram)[0] },
{ isSigner: false, isWritable: true, pubkey: client.source.accountsResolver.getGiveOrderStateAccount(orderId)[0] },
{ isSigner: false, isWritable: true, pubkey: client.source.accountsResolver.getGiveOrderWalletAddress(orderId)[0] },
{ isSigner: false, isWritable: false, pubkey: tokenMint },
{ isSigner: false, isWritable: false, pubkey: tokenProgram },
],
programId: client.source.program.programId,
data: Buffer.concat([Uint8Array.from(discriminator), Uint8Array.from(orderId)]),
});
return ix;
}
// Get withdrawal instructions for all orders with available fees
async function getWithdrawAffiliateFeeInstructions(
client: Solana.DlnClient,
orders: IOrderFromApi[]
): Promise<{ instructions: TransactionInstruction[], orderIds: string[] }> {
const orderIds: string[] = [];
const instructions: TransactionInstruction[] = [];
const chunks: PublicKey[][] = [];
// Get order wallet addresses
const wallets = orders.map(
({ orderId }) =>
client.source.accountsResolver.getGiveOrderWalletAddress(
Buffer.from(JSON.parse(orderId.bytesArrayValue))
)[0],
);
// Chunk wallets for batch querying (1000 per request)
for (let i = 0; i < wallets.length; i += 1000) {
chunks.push(wallets.slice(i, i + 1000));
}
// Get account info for all order wallets
const accounts = (
await Promise.all(
chunks.map((chunk) => client.connection.getMultipleAccountsInfo(chunk))
)
).flat();
// Build instructions only for orders with available fees
for (const [i, order] of orders.entries()) {
const rawAccount = accounts[i]!;
const account = spl.parseSplAccount(rawAccount.data);
// Only create instruction if there are fees to withdraw
if (account?.amount) {
instructions.push(
buildWithdrawAffiliateFeeIx(
client,
Buffer.from(JSON.parse(order.orderId.bytesArrayValue)),
new PublicKey(order.affiliateFee.beneficiarySrc.stringValue),
new PublicKey(order.giveOfferWithMetadata.tokenAddress.stringValue),
rawAccount.owner
)
);
orderIds.push(order.orderId.stringValue);
}
}
return { instructions, orderIds };
}
// Split instructions into optimal transaction sizes
function splitInstructions(
payer: PublicKey,
data: { instructions: TransactionInstruction[], orderIds: string[] }
): { ixPacks: TransactionInstruction[][], orderIdsPacks: string[][] } {
const { instructions, orderIds } = data;
const defaultArgs = {
payerKey: payer,
recentBlockhash: constants.FAKE_BLOCKHASH,
};
// Base instructions for compute budget
const baseInstructions = [
ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 30_000 }),
];
const ixPacks = [];
const orderIdsPacks = [];
let subIxPack = [...baseInstructions];
let subOrderIdsPacks: string[] = [];
const compileTransaction = (instructions: TransactionInstruction[]) =>
new VersionedTransaction(
MessageV0.compile({
instructions,
...defaultArgs,
}),
);
const checkSize = (instructions: TransactionInstruction[]) =>
txs.getTransactionSize(compileTransaction(instructions));
// Pack instructions into transactions that fit in packet size
for (const [i, instruction] of instructions.entries()) {
const size = checkSize([...subIxPack, instruction]);
if (size && size <= PACKET_DATA_SIZE) {
subIxPack.push(instruction);
subOrderIdsPacks.push(orderIds[i]);
} else {
// Current pack is full, start a new one
ixPacks.push(subIxPack);
orderIdsPacks.push(subOrderIdsPacks);
subIxPack = [...baseInstructions, instruction];
subOrderIdsPacks = [orderIds[i]];
}
}
if (subIxPack.length > baseInstructions.length) {
ixPacks.push(subIxPack);
orderIdsPacks.push(subOrderIdsPacks);
}
return { ixPacks, orderIdsPacks };
}
async function main() {
const { solPrivateKey, solRpcUrl } = getEnvConfig();
// Your Solana public key (affiliate fee recipient)
const beneficiaryPubkey = "862oLANNqhdXyUCwLJPBqUHrScrqNR4yoGWGTxjZftKs";
try {
Keypair.fromSecretKey(bs58.decode(solPrivateKey));
new PublicKey(beneficiaryPubkey);
} catch (err) {
console.error("Invalid keys provided");
console.error(err.message);
process.exit();
}
// Set up Solana connection and DLN client
const connection = new Connection(solRpcUrl ?? clusterApiUrl("mainnet-beta"));
const client = new Solana.DlnClient(
connection,
programs.dlnSrc,
programs.dlnDst,
programs.deBridge,
programs.settings,
);
const keypair = Keypair.fromSecretKey(bs58.decode(solPrivateKey));
const wallet = new helpers.Wallet(keypair);
// Get all unlocked orders with your affiliate address
const orders = await getUnlockedOrders({
chainIds: [ChainId.Solana],
beneficiaryAddress: new PublicKey(beneficiaryPubkey),
});
console.log(`Unclaimed orders: ${orders.length}`);
if (orders.length === 0) {
console.log("No orders available for withdrawal");
return;
}
// Build withdrawal instructions
const ordersData = await getWithdrawAffiliateFeeInstructions(client, orders);
if (ordersData.instructions.length === 0) {
console.log("No fees available to withdraw");
return;
}
// Split into optimized transaction batches
const { ixPacks, orderIdsPacks } = splitInstructions(keypair.publicKey, ordersData);
// Create versioned transactions
const txs = ixPacks.map(
(instructions) =>
new VersionedTransaction(
MessageV0.compile({
instructions,
payerKey: keypair.publicKey,
recentBlockhash: constants.FAKE_BLOCKHASH,
}),
),
);
console.log(`Total instructions: ${ordersData.instructions.length}, total transactions: ${txs.length}`);
console.log('Withdrawal started...');
// Execute each transaction
for (const [i, tx] of txs.entries()) {
const [id] = await helpers.sendAll(connection, wallet, tx, {
blockhashCommitment: "finalized",
simulationCommtiment: "confirmed",
});
console.log("-------------------------------");
console.log("Orders batch:", orderIdsPacks[i]);
console.log(`Transaction: ${id}`);
console.log(`View on Solscan: https://solscan.io/tx/${id}`);
// Wait between transactions to avoid rate limits
await helpers.sleep(5000);
}
console.log('Withdrawal complete! ✅');
}
main().catch(console.error);