# arl:claim-position-fee — Ammora SDK 0.3.8 Claim the fees a concentrated (ARL) position has earned. npm run arl:claim-position-fee -- [recipient] The token id is the one `arl:get-user-positions` prints, which is the position manager's NFT. The optional recipient receives only fees; NFT ownership, locked principal and future fee rights remain with the owner. Omitting it pays the owner, including when an authorized fee delegate is the connected caller. ── Two things called "position 1" ──────────────────────────────────────────── A concentrated pool keeps its own positions, numbered per pool, and the position manager wraps them as ERC-721 tokens numbered per manager. Both accept a `tokenId` and both answer `ownerOf`, so passing an id from one to the other does not fail loudly. It reports a different position, or refuses because that id belongs to someone else entirely. Every example here works in the manager's numbering, because that is what a wallet actually holds and what the listing script can enumerate. The managed calls also check the caller's permission on the token, so a delegate can claim where the owner allowed it. ─────────────────────────────────────────────────────────────────────────────── [Read in Docs](https://docs.ammora.org/examples?command=arl%3Aclaim-position-fee) · [Detailed guide](https://docs.ammora.org/guides/src-arl-readme) · [Local setup](https://docs.ammora.org/guides/example-setup) ## Context and execution GIWA Sepolia 91342. Public SDK 0.3.8, Math 0.3.0. Source revision: `a9b3fd798fa51495e8defec99a25de59e4d70672`. Install the SDK in your app, or prepare the example project using the local setup guide for its npm commands. Review the entry source and guide for required arguments, roles and deployment selection. `AMMORA_SEND=0` does not turn send-only commands into previews. Never put CLI keys in a browser or resend an uncertain transaction. Command: `npm run arl:claim-position-fee -- ` (`tsx src/arl/claim-position-fee.ts`). Omit `--` and arguments when the source accepts none. ## Entry file: `src/arl/claim-position-fee.ts` ```typescript /** * Claim the fees a concentrated (ARL) position has earned. * * npm run arl:claim-position-fee -- [recipient] * * The token id is the one `arl:get-user-positions` prints, which is the position * manager's NFT. The optional recipient receives only fees; NFT ownership, locked * principal and future fee rights remain with the owner. Omitting it pays the owner, * including when an authorized fee delegate is the connected caller. * * ── Two things called "position 1" ──────────────────────────────────────────── * A concentrated pool keeps its own positions, numbered per pool, and the position * manager wraps them as ERC-721 tokens numbered per manager. Both accept a * `tokenId` and both answer `ownerOf`, so passing an id from one to the other does * not fail loudly. It reports a different position, or refuses because that id * belongs to someone else entirely. * * Every example here works in the manager's numbering, because that is what a * wallet actually holds and what the listing script can enumerate. The managed * calls also check the caller's permission on the token, so a delegate can claim * where the owner allowed it. * ─────────────────────────────────────────────────────────────────────────────── */ import { DammV2Client, ammConcentratedPositionManagerV3Abi, resolveAmmConcentratedPositionFeeClaimReceipt } from "@ammora-protocol/sdk"; import { zeroAddress } from "viem"; import { clientOptions, address, deployment, reader, signer } from "../shared/env.js"; import { amount, asPool, main, positionOwner, requireContract, send, tokenInfo } from "../shared/run.js"; main(async () => { const [poolArgument, tokenIdArgument, recipientArgument, ...extra] = process.argv.slice(2); const pool = address("ARL_POOL", poolArgument); if (!tokenIdArgument || !/^\d+$/.test(tokenIdArgument) || extra.length) { throw new Error("pass the position token id: npm run arl:claim-position-fee -- [recipient]"); } const tokenId = BigInt(tokenIdArgument); const requestedRecipient = recipientArgument === undefined ? undefined : address("LP_FEE_RECIPIENT", recipientArgument); if (requestedRecipient === zeroAddress) throw new Error("LP fee recipient must not be the zero address"); const { wallet, account } = signer(); const client = await reader(); const contracts = deployment().contracts.launch; // The SDK proves the pool came from the reviewed factory before planning anything that // moves value, so an address from a link or a search result is refused here instead of // after an approval has been signed. await requireContract(client, "pool", pool); const sdk = new DammV2Client( client, pool, "concentrated", contracts.concentratedFactory, contracts.concentratedPositionManager, clientOptions(), ); const state = await asPool(pool, "a concentrated pool", () => sdk.getPoolState()); const [base, quote] = await Promise.all([ tokenInfo(client, state.baseToken), tokenInfo(client, state.quoteToken), ]); const owner = await positionOwner(client, ammConcentratedPositionManagerV3Abi, contracts.concentratedPositionManager, tokenId); console.log(`pool ${pool}`); console.log(`position ${tokenId} owned by ${owner}`); // Default to the NFT owner even when a fee delegate is connected. The SDK // checks ClaimFee vs ClaimFeeToOwner and rejects unauthorized redirection. const recipient = requestedRecipient ?? owner; console.log(`caller ${account.address}`); const transaction = await sdk.planClaimManagedConcentratedFees( tokenId, recipient, account.address, ); // Simulating returns exactly what the claim would pay, without sending it. Reading the // position's stored counters instead would be behind: claiming accrues first, so they // lag by whatever the position has earned since it was last touched. const { result } = await client.simulateContract({ abi: ammConcentratedPositionManagerV3Abi, account, address: contracts.concentratedPositionManager, functionName: "claimPositionFees", args: [tokenId, recipient], }); const [claimableBase, claimableQuote] = result as readonly [bigint, bigint]; console.log(`claimable ${amount(claimableBase, base.decimals, base.symbol)}`); console.log(` ${amount(claimableQuote, quote.decimals, quote.symbol)}`); if (claimableBase === 0n && claimableQuote === 0n) { console.log("\nnothing owed, no transaction sent"); return; } console.log(`\nclaiming fees to ${recipient}`); const hash = await send("claimPositionFees", transaction, wallet, account, client); const receipt = await client.getTransactionReceipt({ hash }); const paid = resolveAmmConcentratedPositionFeeClaimReceipt({ chainId: deployment().chain.id, receipt, expectedTransactionHash: hash, positionManager: contracts.concentratedPositionManager, tokenId, recipient, }); console.log(`\nclaimed ${amount(paid.baseAmount, base.decimals, base.symbol)}` + ` and ${amount(paid.quoteAmount, quote.decimals, quote.symbol)} from the confirmed receipt`); }); ``` ## Supporting files Keep relative paths intact. Local imports are listed recursively; JSON templates are choices to review, not approved inputs. Runtime-selected files and wallet/environment setup still follow the detailed guide. - [src/shared/env.ts](https://docs.ammora.org/examples/source/src/shared/env.ts.txt) - [src/shared/input.ts](https://docs.ammora.org/examples/source/src/shared/input.ts.txt) - [src/shared/retry.ts](https://docs.ammora.org/examples/source/src/shared/retry.ts.txt) - [src/shared/rpc.ts](https://docs.ammora.org/examples/source/src/shared/rpc.ts.txt) - [src/shared/run.ts](https://docs.ammora.org/examples/source/src/shared/run.ts.txt) - [package.json](https://docs.ammora.org/examples/source/package.json.txt) - [package-lock.json](https://docs.ammora.org/examples/source/package-lock.json.txt) - [tsconfig.json](https://docs.ammora.org/examples/source/tsconfig.json.txt) - [.env.giwa.example](https://docs.ammora.org/examples/source/file.env.giwa.example.txt) - [.nvmrc](https://docs.ammora.org/examples/source/file.nvmrc.txt) ## AI implementation checklist Use the actual SDK exports and parameter types in the linked files. Start with reads and unsigned plans where supported. Use a connected wallet for browser signing. Preserve raw integer units, exact approvals, caller authority, deadlines and receipt reconciliation. Report local checks separately from live-chain verification.