> ## Documentation Index
> Fetch the complete documentation index at: https://build.flashnet.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Executing Transfers

> Hand off a locked position

Call `transferPosition` on the `FlashnetClient` with a pool ID, the recipient's public key, and shape-specific options. The sender must already hold an active lock on the position.

## Signature

```typescript theme={null}
await client.transferPosition(
  poolId: string,
  newOwnerPublicKey: string,
  opts?: {
    tickLower?: number;             // V3 only
    tickUpper?: number;             // V3 only
    lpTokensToTransfer?: string;    // V2 only; positive integer string
  }
): Promise<TransferPositionResponse>;
```

Pick one shape per call. Supplying both V2 (`lpTokensToTransfer`) and V3 (`tickLower`/`tickUpper`) options together is rejected before the message is signed.

## Transfer a V2 position

```typescript theme={null}
// Transfer 5_000 LP shares to a new owner
const response = await client.transferPosition(
  poolId,
  recipientPublicKey,
  { lpTokensToTransfer: "5000" }
);

console.log("Accepted:", response.accepted);
console.log("Confirmation:", response.confirmation);
```

`lpTokensToTransfer` is a decimal string of atomic LP units. The validator rejects:

* Non-digit characters (no sign, no decimal point, no scientific notation, no whitespace).
* Leading zeros (`"01"`, `"007"`).
* Zero or negative values.

The sender's residual after the call is `lp_shares[sender] - lpTokensToTransfer`. If the residual is zero, the server deletes the sender's lock row in the same transaction. Otherwise the sender's lock stays at its original expiry.

## Transfer a V3 position

```typescript theme={null}
// Transfer a concentrated position to a new owner
const response = await client.transferPosition(
  poolId,
  recipientPublicKey,
  { tickLower: -69080, tickUpper: -66850 }
);
```

Both `tickLower` and `tickUpper` are required. The pair must match an existing position the caller owns. The transfer moves the whole `(pool_id, sender, tickLower, tickUpper)` row to the recipient and rewrites the matching `lp_locks` row.

## Locking the recipient on V2

A V2 transfer carries the sender's lock to the recipient only when the recipient has no prior `lp_shares` in the pool. To deliver locked shares to a recipient who already holds an unlocked position, the recipient must lock first:

```typescript theme={null}
// Recipient locks their position at the desired expiry
await recipientClient.lockPosition(poolId, "1793836800");

// Sender then transfers
await senderClient.transferPosition(
  poolId,
  recipientPublicKey,
  { lpTokensToTransfer: "5000" }
);
```

After the transfer, the recipient's combined balance (`existing + transferred`) sits under the recipient's existing lock terms. The sender's lock metadata is not propagated.

## Verify the result

The transfer commits two pieces of state. Read both to confirm:

```typescript theme={null}
import type { LpLockInfo } from "@flashnet/sdk";

// V2: confirm shares moved
const senderPosition = await client.getLpPosition(poolId, senderPublicKey);
const recipientPosition = await client.getLpPosition(poolId, recipientPublicKey);

console.log("Sender residual:", senderPosition.lpTokensOwned);
console.log("Recipient balance:", recipientPosition.lpTokensOwned);

// Confirm lock-follow
const locks: LpLockInfo[] = await client.getPositionLocks(poolId);
const recipientLock = locks.find(l => l.ownerPublicKey === recipientPublicKey);
console.log("Recipient lock expiry:", recipientLock?.lockUntilTimestamp);
```

For a V3 transfer, query `getLpPosition` with the recipient's key and the original tick range; the row should now exist under the recipient. The sender's row at the same range no longer exists.

## Error handling

```typescript theme={null}
import { isFlashnetError } from "@flashnet/sdk";

try {
  await client.transferPosition(
    poolId,
    recipientPublicKey,
    { lpTokensToTransfer: "5000" }
  );
} catch (error) {
  if (isFlashnetError(error)) {
    console.error(error.errorCode, error.userMessage);
  }
  throw error;
}
```

Common rejection paths:

* **No active lock.** The sender holds no `lp_locks` row for the source position, or the lock has expired. Set or extend a lock first.
* **Insufficient lp\_shares (V2).** The requested amount is greater than `lp_shares[sender]`. Read the sender's balance and adjust.
* **Position not found (V3).** The `(tickLower, tickUpper)` pair does not match a position the sender owns.
* **Self-transfer.** `newOwnerPublicKey` equals the caller's key. Rejected client-side before signing.
* **Mixed shape.** Both `lpTokensToTransfer` and a tick pair were supplied. Pick one.

## Concurrency

Two parallel transfers from the same sender on the same pool serialize through the per-pool batch executor. The first commit wins; the second sees the new state and is rejected with `insufficient lp_shares` (V2) or `position does not exist` (V3). No race lets the sender double-spend the same balance.

## Limitations

* V3 transfer moves the entire position at one tick range. Partial V3 transfer requires `decreaseLiquidity` followed by an off-chain delivery.
* The intent is signed once and cannot be revoked. A typo in `newOwnerPublicKey` is recoverable only if the recipient cooperates.
* There is no on-chain record of a transfer. State lives in Flashnet alongside the rest of the pool state.

## Next steps

* [Position transfer overview](/products/flashnet-amm/transfer-overview)
* [Position locks overview](/products/flashnet-amm/lock-overview)
