# Vault Operations

> Always follow the **API-driven** pattern: obtain router metadata dynamically, construct contract instances with `dexFactory`/`routerFactory`, and avoid hardcoding addresses. This keeps the withdrawal flow compatible with future router updates.

## Mainnet workflow

1. Discover the vaults owed to the operator (e.g., via `stonApiClient.getWalletVaultsFee`).
2. Fetch router metadata for each vault using `getRouter(routerAddress)`.
3. Build the router contracts dynamically with `routerFactory` and request the vault instance.
4. Generate the withdrawal transaction parameters.

```typescript
import { Client, routerFactory } from "@ston-fi/sdk";
import { StonApiClient } from "@ston-fi/api";

const tonClient = new Client({
  endpoint: "https://toncenter.com/api/v2/jsonRPC",
  apiKey: process.env.TON_API_KEY,
});

const apiClient = new StonApiClient();

const userWalletAddress = "<your wallet address>";

// Retrieve pending vault fees for the wallet
const vaults = await apiClient.getWalletVaultsFee({
  walletAddress: userWalletAddress,
});

const messages = await Promise.all(
  vaults.map(async ({ routerAddress, assetAddress }) => {
    const routerMetadata = await apiClient.getRouter(routerAddress);
    const router = tonClient.open(routerFactory(routerMetadata));

    if (!("getVault" in router)) {
      throw new Error(
        `Vault contract is not supported on router version ${routerMetadata.majorVersion}.${routerMetadata.minorVersion}`,
      );
    }

    const tokenMinter =
      assetAddress === "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c"
        ? routerMetadata.ptonMasterAddress
        : assetAddress;

    const vault = tonClient.open(
      await router.getVault({
        tokenMinter,
        user: userWalletAddress,
      }),
    );

    const txParams = await vault.getWithdrawFeeTxParams({
      queryId: 12345,
    });

    return {
      address: txParams.to.toString(),
      amount: txParams.value.toString(),
      payload: txParams.body?.toBoc().toString("base64"),
    };
  }),
);
```

Send the generated messages using your preferred wallet integration (see the [transaction sending guide](https://docs.ston.fi/developer-section/common/transaction-sending)).

> **TON fees**: TON does not have a jetton minter address. When `assetAddress` equals the canonical TON address (`EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c`), pass `routerMetadata.ptonMasterAddress` as the `tokenMinter` so the SDK resolves the correct proxy contract.

## Testnet withdrawal (manual setup)

Use testnet only when you must. Because `api.ston.fi` targets mainnet, you’ll have to hardcode contract addresses, mint or source the testnet jettons (e.g., TesREED), and deploy any supporting contracts yourself before attempting fee withdrawals.

```typescript
import { TonClient } from "@ton/ton";
import { DEX } from "@ston-fi/sdk";

const client = new TonClient({
  endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC",
});

const router = client.open(
  DEX.v2_1.Router.CPI.create("kQALh-JBBIKK7gr0o4AVf9JZnEsFndqO0qTCyT-D-yBsWk0v"), // CPI Router v2.1.0 (testnet)
);

const vault = client.open(
  await router.getVault({
    tokenMinter: "kQDLvsZol3juZyOAVG8tWsJntOxeEZWEaWCbbSjYakQpuYN5", // TesREED jetton (testnet)
    user: "<your testnet wallet>",
  }),
);

const txParams = await vault.getWithdrawFeeTxParams({
  queryId: 12345,
});
```

This manual approach is strictly **for testing**. Switch back to the API-driven workflow for mainnet deployments. For a comprehensive overview of referral fees, see the [Omniston referral fees guide](https://docs.ston.fi/omniston/referral-fees#referral-fees-with-dex-v2) (while Omniston-focused, the linked section details the DEX V2 referral process).
