Affiliate fee

Affiliate fee can be specified by anyone who integrates with DLN API or uses a deSwap widget. The fee is received by the beneficiary at the moment when the market maker unlocks liquidity from the fulfilled trade (usually a few hours after the trade is fulfilled).

Withdrawal

EVM chains

For cross-chain transfers initiated from EVM chains, the affiliate fee is transferred to the beneficiary address automatically in the same transaction where the market maker unlocks liquidity from the fulfilled trade.

Solana

For trades initiated from Solana, the affiliate fee is not transferred automatically and should be claimed by the beneficiary by interacting with the DLN program on Solana. withdrawAffiliateFee the method should be called. Here is how it can be done using DLN Client:

import { Solana } from "@debridge-finance/dln-client"
import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js";

function findAssociatedTokenAddress(wallet: PublicKey, tokenMint: PublicKey): [PublicKey, number] {
    return PublicKey.findProgramAddressSync([wallet.toBytes(), new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").toBytes(), tokenMint.toBytes()], new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"));
}

const solanaClient = new Solana.DlnClient(
    new Connection(clusterApiUrl("mainnet-beta")), // better use your own RPC
    new PublicKey("src5qyZHqTqecJV4aY6Cb6zDZLMDzrDKKezs22MPHr4"), 
    new PublicKey("dst5MGcFPoBeREFAA5E3tU5ij8m5uVYwkzkSAbsLbNo"), 
    new PublicKey("DEbrdGj3HsRsAzx6uH4MKyREKxVAfBydijLUF3ygsFfh"), 
    new PublicKey("DeSetTwWhjZq6Pz9Kfdo1KoS5NqtsM6G8ERbX4SSCSft"),
)
type Order = {
    orderId: string;
    beneficiary: PublicKey;
    giveToken: PublicKey;
}
// load order in expected format
const order: Order; 
// order could also be loaded from chain by order creation tx hash
// const order = solanaClient.getOrderFromTransaction({ giveChain: ChainId.Solana, txHash: "create tx hash" }, {});

// build withdraw tx
const tx = await solanaClient.source.withdrawAffiliateFee(order.orderId, order.beneficiary, findAssociatedTokenAddress(order.beneficiary, order.giveToken)[0]);
// send withdraw tx

Withdrawal batch of Solana affiliate fees

You can withdraw fees by using CLI ready script below:

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";

const privateKey = process.argv[2];
const beneficiaryPubkey = process.argv[3];
const rpc = process.argv[4];

try {
    Keypair.fromSecretKey(bs58.decode(privateKey));
    new PublicKey(beneficiaryPubkey);
} catch (err) {
    console.error("Format: withdrawAffiliateFees <base58 PrivateKey> <string PubKey> [rpc]");
    console.error(err.message);
    process.exit();
}

interface IOrderFromApi {
    orderId: { stringValue: string; bytesArrayValue: string };
    affiliateFee: { beneficiarySrc: { stringValue: string } };
    giveOfferWithMetadata: { tokenAddress: { stringValue: string } };
}

async function getUnlockedOrders({
    chainIds,
    beneficiaryAddress,
    ref,
}: { chainIds: number[]; beneficiaryAddress: PublicKey; ref?: string; }): Promise<IOrderFromApi[]> {
    const MAX = 100; // Max 100 records per request
    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"],
                    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);
    }

    allOrders = allOrders.filter(
        (order) => order.affiliateFee.beneficiarySrc.stringValue === beneficiaryAddress.toString(),
    );

    return allOrders;
}

async function getWithdrawAffiliateFeeInstructions(client: Solana.DlnClient, orders: IOrderFromApi[]): Promise<{ instructions: TransactionInstruction[], orderIds: string[] }> {
    const orderIds: string[] = [];
    const instructions: TransactionInstruction[] = [];
    const chunks: PublicKey[][] = [];

    const wallets = orders.map(
        ({ orderId }) =>
            client.source.accountsResolver.getGiveOrderWalletAddress(Buffer.from(JSON.parse(orderId.bytesArrayValue)))[0],
    );

    for (let i = 0; i < wallets.length; i += 1000) {
        chunks.push(wallets.slice(i, i + 1000)); // 1000 wallets per request
    }

    const accounts = (
        await Promise.all(chunks.map((chunk) => client.connection.getMultipleAccountsInfo(chunk)))
    ).flat();

    for (const [i, order] of orders.entries()) {
        const account = spl.parseSplAccount(accounts[i]!.data);

        if (account?.amount) {
            const tx = await client.source.withdrawAffiliateFee(
                order.orderId.stringValue,
                new PublicKey(order.affiliateFee.beneficiarySrc.stringValue),
                findAssociatedTokenAddress(
                    new PublicKey(order.affiliateFee.beneficiarySrc.stringValue),
                    new PublicKey(order.giveOfferWithMetadata.tokenAddress.stringValue),
                )[0],
            );

            instructions.push(tx.instructions[0]);
            orderIds.push(order.orderId.stringValue);
        }
    }

    return { instructions, orderIds };
}

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,
    };

    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));

    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 {
            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() {
    // initialization
    const connection = new Connection(rpc ?? clusterApiUrl("mainnet-beta"));
    const client = new Solana.DlnClient(
        connection,
        programs.dlnSrc,
        programs.dlnDst,
        programs.deBridge,
        programs.settings,
    );
    const keypair = Keypair.fromSecretKey(bs58.decode(privateKey));
    const wallet = new helpers.Wallet(keypair);

    // get unclaimed orders
    const orders = await getUnlockedOrders({
        chainIds: [ChainId.Solana],
        beneficiaryAddress: new PublicKey(beneficiaryPubkey),
    });

    console.log(`Unclaimed orders: ${orders.length}`);

    // create claim instructions for orders
    const ordersData = await getWithdrawAffiliateFeeInstructions(client, orders);
    // split instructions by max transaction size
    const { ixPacks, orderIdsPacks } = splitInstructions(keypair.publicKey, ordersData);

    // create transactions from instructions packs
    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...');

    // send transactions
    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(`Tx: ${id}`);
        await helpers.sleep(5000);
    }

    console.log('Done');
}

main().catch(console.error);

This script gets all transactions with unclaimed fees by beneficiary address, generates claim instructions and packs them into a minimum number of transactions.

How to use:

  1. Install following npm packages: @solana/web3.js, @debridge-finance/solana-utils, @debridge-finance/dln-client, bs58.

  2. Create typescript file and copy script above (e.g. withdrawAffiliateFees.ts).

  3. Run script. You need to pass PrivateKey (base58) for transaction signature as first argument and beneficiary address PubKey (string) as second argument. Optionally you can pass Solana RPC URL as third argument.

    ts-node withdrawAffiliateFees <base58 PrivateKey> <string PubKey> [rpc]

Last updated