> For the complete documentation index, see [llms.txt](https://docs.aqua.network/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.aqua.network/developers/concentrated-liquidity/managing-positions-code-examples.md).

# Managing positions: code examples

This page walks through the full lifecycle of a concentrated liquidity position using the Stellar SDK. Make sure you're familiar with [Prerequisites & basics](/developers/code-examples/prerequisites-and-basics.md) — the RPC endpoints, contract addresses and data conversion helpers used below come from there.

All position management calls go **directly to the pool contract**. You can find the pool address on the pool page of the Aquarius website or programmatically — see [Get pools info](/developers/code-examples/get-pools-info.md). Concentrated pools report `pool_type: "concentrated"` both on-chain (`get_info`) and in the [backend API](/developers/code-examples/get-pools-info.md#using-backend-api) pool listing — filter the list by that field on your side.

The [complete runnable scripts](#complete-code-examples) are at the bottom of this page.

Helper functions used in the snippets below (`scval_map`, `simulate`) are defined once in the [complete code](#complete-code-examples) at the end of this page.

### Step 1. Read the pool state

Everything starts with two read-only values: the **current tick** (from `get_slot0`) and the **tick spacing**. Read-only calls are executed through transaction simulation — no signature required.

{% tabs %}
{% tab title="Python" %}

```python
# Current price state: {"sqrt_price_x96": U256, "tick": i32}
slot0 = scval_map(simulate("get_slot0"))
current_tick = slot0["tick"].i32.int32

# Tick spacing: 20 (0.1% pool), 60 (0.3%) or 200 (1.0%)
tick_spacing = simulate("get_tick_spacing").i32.int32

# Human-readable price of token0 in token1 units
current_price = 1.0001 ** current_tick
print(f"tick={current_tick} spacing={tick_spacing} price={current_price:.7f}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
// Current price state: { sqrt_price_x96: BigInt, tick: number }
const slot0 = await simulate('get_slot0');
const currentTick = slot0.tick;

// Tick spacing: 20 (0.1% pool), 60 (0.3%) or 200 (1.0%)
const tickSpacing = await simulate('get_tick_spacing');

// Human-readable price of token0 in token1 units
const currentPrice = Math.pow(1.0001, currentTick);
console.log(`tick=${currentTick} spacing=${tickSpacing} price=${currentPrice}`);
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The price is expressed as **token1 per token0 in raw (stroop) units**, where token0/token1 are the pool tokens sorted by contract ID (`get_tokens` returns them in order). Since Stellar classic assets have 7 decimals on both sides, the raw ratio matches the display price.
{% endhint %}

### Step 2. Convert your price range to ticks

Position boundaries are ticks: `price(tick) = 1.0001^tick`, and both boundaries must be multiples of the tick spacing. The helper below picks the nearest valid ticks for a target price range — here, ±5% around the current price.

{% tabs %}
{% tab title="Python" %}

```python
import math

def price_to_tick(price: float, tick_spacing: int) -> int:
   """Convert a price to the nearest valid (spacing-aligned) tick below it."""
   tick = math.floor(math.log(price) / math.log(1.0001))
   return tick - tick % tick_spacing  # Python's % keeps this aligned-down for negatives too

# Range: 5% below to 5% above the current price
tick_lower = price_to_tick(current_price * 0.95, tick_spacing)
tick_upper = price_to_tick(current_price * 1.05, tick_spacing) + tick_spacing
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
function priceToTick(price, tickSpacing) {
    /** Convert a price to the nearest valid (spacing-aligned) tick below it. */
    const tick = Math.floor(Math.log(price) / Math.log(1.0001));
    // Align down to the spacing grid (handles negative ticks correctly)
    return tick - ((tick % tickSpacing) + tickSpacing) % tickSpacing;
}

// Range: 5% below to 5% above the current price
const tickLower = priceToTick(currentPrice * 0.95, tickSpacing);
const tickUpper = priceToTick(currentPrice * 1.05, tickSpacing) + tickSpacing;
```

{% endtab %}
{% endtabs %}

### Step 3. Estimate and open the position

`estimate_deposit_position` computes how much of each token the deposit will actually consume and how much liquidity you'll receive. Use the estimated liquidity to set a `min_liquidity` slippage guard, then call `deposit_position`.

The `desired_amounts` you pass are maximums: the contract pulls what the current price requires and refunds the rest in the same transaction. If the position range lies entirely above or below the current price, only one of the two tokens is needed — pass `0` for the other.

{% tabs %}
{% tab title="Python" %}

```python
# 10 XLM and 10 USDC (raw 7-decimals amounts), in sorted token order
desired_amounts = [100_0000000, 100_0000000]

est = simulate("estimate_deposit_position", [
   scval.to_int32(tick_lower),
   scval.to_int32(tick_upper),
   scval.to_vec([scval.to_uint128(a) for a in desired_amounts]),
])
est_amounts = [u128_to_int(v.u128) for v in est.vec.sc_vec[0].vec.sc_vec]
est_liquidity = u128_to_int(est.vec.sc_vec[1].u128)
print(f"will pull {est_amounts}, mint liquidity {est_liquidity}")

# Allow 1% slippage on minted liquidity
min_liquidity = est_liquidity * 99 // 100

result = invoke("deposit_position", [
   scval.to_address(keypair.public_key),
   scval.to_int32(tick_lower),
   scval.to_int32(tick_upper),
   scval.to_vec([scval.to_uint128(a) for a in desired_amounts]),
   scval.to_uint128(min_liquidity),
])
deposited = [u128_to_int(v.u128) for v in result.vec.sc_vec[0].vec.sc_vec]
liquidity = u128_to_int(result.vec.sc_vec[1].u128)
print(f"deposited {deposited}, received liquidity {liquidity}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
// 10 XLM and 10 USDC (raw 7-decimals amounts), in sorted token order
const desiredAmounts = [1000000000n, 1000000000n];

const [estAmounts, estLiquidity] = await simulate('estimate_deposit_position', [
    nativeToScVal(tickLower, { type: 'i32' }),
    nativeToScVal(tickUpper, { type: 'i32' }),
    xdr.ScVal.scvVec(desiredAmounts.map(a => nativeToScVal(a, { type: 'u128' }))),
]);
console.log(`will pull ${estAmounts}, mint liquidity ${estLiquidity}`);

// Allow 1% slippage on minted liquidity
const minLiquidity = estLiquidity * 99n / 100n;

const [deposited, liquidity] = await invoke('deposit_position', [
    new Address(keypair.publicKey()).toScVal(),
    nativeToScVal(tickLower, { type: 'i32' }),
    nativeToScVal(tickUpper, { type: 'i32' }),
    xdr.ScVal.scvVec(desiredAmounts.map(a => nativeToScVal(a, { type: 'u128' }))),
    nativeToScVal(minLiquidity, { type: 'u128' }),
]);
console.log(`deposited ${deposited}, received liquidity ${liquidity}`);
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Depositing into the same `(tick_lower, tick_upper)` range again adds to the existing position. One account can hold up to 20 positions per pool. The first deposit into an empty pool sets the initial pool price from the ratio of the amounts and must include both tokens.
{% endhint %}

### Step 4. List your positions and check if they are in range

There are no position IDs — a position is identified by your address plus the tick range. `get_user_position_snapshot` lists all your ranges in the pool; a position keeps earning swap fees and AQUA rewards only while `tick_lower <= current_tick < tick_upper`. Keep in mind that the reward weight is [checkpoint-based](/developers/concentrated-liquidity.md#aqua-rewards) — it updates only when you deposit, withdraw or claim, so perform an action after the price crosses your range boundary to re-sync it.

The per-position calls used below (`get_position`, `get_position_fees`, `claim_position_fees`) **fail with `PositionNotFound`** if you pass a range you have no position in (for example, one you have already fully withdrawn) — take the ranges from the snapshot rather than hardcoding them.

{% tabs %}
{% tab title="Python" %}

```python
snapshot = scval_map(simulate("get_user_position_snapshot",
                              [scval.to_address(keypair.public_key)]))

current_tick = scval_map(simulate("get_slot0"))["tick"].i32.int32

for entry in snapshot["ranges"].vec.sc_vec:
   rng = scval_map(entry)
   lower = rng["tick_lower"].i32.int32
   upper = rng["tick_upper"].i32.int32
   in_range = lower <= current_tick < upper
   print(f"[{lower}, {upper}] -> prices [{1.0001 ** lower:.7f}, {1.0001 ** upper:.7f}]"
         f" {'IN RANGE' if in_range else 'OUT OF RANGE'}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const snapshot = await simulate('get_user_position_snapshot',
    [new Address(keypair.publicKey()).toScVal()]);

const { tick: nowTick } = await simulate('get_slot0');

for (const { tick_lower, tick_upper } of snapshot.ranges) {
    const inRange = tick_lower <= nowTick && nowTick < tick_upper;
    console.log(
        `[${tick_lower}, ${tick_upper}] -> prices ` +
        `[${Math.pow(1.0001, tick_lower)}, ${Math.pow(1.0001, tick_upper)}] ` +
        (inRange ? 'IN RANGE' : 'OUT OF RANGE')
    );
}
```

{% endtab %}
{% endtabs %}

### Step 5. Check and claim swap fees

Fees accrue per position in both pool tokens. `get_position_fees` (read-only) shows the pending amounts; `claim_position_fees` transfers them to you. `get_all_position_fees` / `claim_all_position_fees` do the same across all your positions at once.

{% tabs %}
{% tab title="Python" %}

```python
fees = simulate("get_position_fees", [
   scval.to_address(keypair.public_key),
   scval.to_int32(tick_lower),
   scval.to_int32(tick_upper),
])
pending = [u128_to_int(v.u128) for v in fees.vec.sc_vec]
print(f"pending fees: token0={pending[0]}, token1={pending[1]}")

if any(pending):
   claimed = invoke("claim_position_fees", [
       scval.to_address(keypair.public_key),
       scval.to_int32(tick_lower),
       scval.to_int32(tick_upper),
   ])
   print("claimed:", [u128_to_int(v.u128) for v in claimed.vec.sc_vec])
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const pending = await simulate('get_position_fees', [
    new Address(keypair.publicKey()).toScVal(),
    nativeToScVal(tickLower, { type: 'i32' }),
    nativeToScVal(tickUpper, { type: 'i32' }),
]);
console.log(`pending fees: token0=${pending[0]}, token1=${pending[1]}`);

if (pending.some(amount => amount > 0n)) {
    const claimed = await invoke('claim_position_fees', [
        new Address(keypair.publicKey()).toScVal(),
        nativeToScVal(tickLower, { type: 'i32' }),
        nativeToScVal(tickUpper, { type: 'i32' }),
    ]);
    console.log('claimed:', claimed);
}
```

{% endtab %}
{% endtabs %}

### Step 6. Claim AQUA rewards

If the pool has active [AMM rewards](/voting-and-rewards/aquarius-amm-rewards.md), in-range positions accrue AQUA. Check with `get_user_reward` and collect with `claim`.

{% tabs %}
{% tab title="Python" %}

```python
reward = u128_to_int(
   simulate("get_user_reward", [scval.to_address(keypair.public_key)]).u128
)
print(f"pending AQUA reward: {reward / 1e7}")

if reward:
   claimed = invoke("claim", [scval.to_address(keypair.public_key)])
   print(f"claimed: {u128_to_int(claimed.u128) / 1e7} AQUA")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const reward = await simulate('get_user_reward',
    [new Address(keypair.publicKey()).toScVal()]);
console.log(`pending AQUA reward: ${Number(reward) / 1e7}`);

if (reward > 0n) {
    const claimed = await invoke('claim', [new Address(keypair.publicKey()).toScVal()]);
    console.log(`claimed: ${Number(claimed) / 1e7} AQUA`);
}
```

{% endtab %}
{% endtabs %}

### Step 7. Withdraw the position

Read the position's liquidity with `get_position`, estimate the payout, then call `withdraw_position`. The payout includes the position's **entire accrued swap fee balance on top of the principal — even when you withdraw only part of the liquidity**; withdrawing the full liquidity closes the position. Use `min_amounts` as slippage guards (they are checked against the total payout, principal + fees).

To rebalance an out-of-range position, withdraw it and open a new one around the current price (Steps 1–3).

{% tabs %}
{% tab title="Python" %}

```python
position = scval_map(simulate("get_position", [
   scval.to_address(keypair.public_key),
   scval.to_int32(tick_lower),
   scval.to_int32(tick_upper),
]))
position_liquidity = u128_to_int(position["liquidity"].u128)

est = simulate("estimate_withdraw_position", [
   scval.to_address(keypair.public_key),
   scval.to_int32(tick_lower),
   scval.to_int32(tick_upper),
   scval.to_uint128(position_liquidity),
])
est_amounts = [u128_to_int(v.u128) for v in est.vec.sc_vec]

# Allow 1% slippage
min_amounts = [a * 99 // 100 for a in est_amounts]

withdrawn = invoke("withdraw_position", [
   scval.to_address(keypair.public_key),
   scval.to_int32(tick_lower),
   scval.to_int32(tick_upper),
   scval.to_uint128(position_liquidity),
   scval.to_vec([scval.to_uint128(a) for a in min_amounts]),
])
print("withdrawn:", [u128_to_int(v.u128) for v in withdrawn.vec.sc_vec])
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const position = await simulate('get_position', [
    new Address(keypair.publicKey()).toScVal(),
    nativeToScVal(tickLower, { type: 'i32' }),
    nativeToScVal(tickUpper, { type: 'i32' }),
]);
const positionLiquidity = position.liquidity;

const estAmounts = await simulate('estimate_withdraw_position', [
    new Address(keypair.publicKey()).toScVal(),
    nativeToScVal(tickLower, { type: 'i32' }),
    nativeToScVal(tickUpper, { type: 'i32' }),
    nativeToScVal(positionLiquidity, { type: 'u128' }),
]);

// Allow 1% slippage
const minAmounts = estAmounts.map(amount => amount * 99n / 100n);

const withdrawn = await invoke('withdraw_position', [
    new Address(keypair.publicKey()).toScVal(),
    nativeToScVal(tickLower, { type: 'i32' }),
    nativeToScVal(tickUpper, { type: 'i32' }),
    nativeToScVal(positionLiquidity, { type: 'u128' }),
    xdr.ScVal.scvVec(minAmounts.map(amount => nativeToScVal(amount, { type: 'u128' }))),
]);
console.log('withdrawn:', withdrawn);
```

{% endtab %}
{% endtabs %}

## Complete code examples

The scripts below implement the shared `simulate`/`invoke` helpers used in the steps above and run through the whole lifecycle. Provide the secret key of a funded account and the address of a concentrated pool.

<details>

<summary>Copy the full code Python</summary>

```python
# concentrated_position.py

import math
import time

from stellar_sdk import Address, Keypair, Network, scval, SorobanServer, TransactionBuilder, xdr
from stellar_sdk.soroban_rpc import GetTransactionStatus
from stellar_sdk.xdr import UInt128Parts

# ==========================
# Configuration Variables
# ==========================

SOROBAN_SERVER_RPC = "https://mainnet.sorobanrpc.com"
NETWORK_PASSPHRASE = Network.PUBLIC_NETWORK_PASSPHRASE

# User's secret key (ensure this is kept secure)
USER_SECRET_KEY = "S..."
# Address of the concentrated liquidity pool contract
POOL_ADDRESS = "C..."

server = SorobanServer(SOROBAN_SERVER_RPC)
keypair = Keypair.from_secret(USER_SECRET_KEY)


# ==========================
# Utility Functions
# ==========================

def u128_to_int(value: UInt128Parts) -> int:
   """Converts UInt128Parts from Stellar's XDR to a Python integer."""
   return (value.hi.uint64 << 64) + value.lo.uint64


def scval_map(scv: xdr.SCVal) -> dict:
   """Converts an ScVal map (contract struct) to a Python dict of ScVals."""
   return {entry.key.sym.sc_symbol.decode(): entry.val for entry in scv.map.sc_map}


def price_to_tick(price: float, tick_spacing: int) -> int:
   """Converts a price to the nearest valid (spacing-aligned) tick below it."""
   tick = math.floor(math.log(price) / math.log(1.0001))
   return tick - tick % tick_spacing


def build_tx(function_name: str, parameters: list):
   return (
       TransactionBuilder(
           source_account=server.load_account(keypair.public_key),
           network_passphrase=NETWORK_PASSPHRASE,
           base_fee=1000000,
       )
       .set_timeout(3600)
       .append_invoke_contract_function_op(
           contract_id=POOL_ADDRESS,
           function_name=function_name,
           parameters=parameters,
       )
       .build()
   )


def simulate(function_name: str, parameters: list = None) -> xdr.SCVal:
   """Read-only contract call executed through simulation. No signature needed."""
   simulation = server.simulate_transaction(build_tx(function_name, parameters or []))
   if simulation.error:
       raise RuntimeError(f"{function_name} simulation failed: {simulation.error}")
   return xdr.SCVal.from_xdr(simulation.results[0].xdr)


def invoke(function_name: str, parameters: list) -> xdr.SCVal:
   """State-changing contract call: prepare, sign, send and wait for the result."""
   prepared_tx = server.prepare_transaction(build_tx(function_name, parameters))
   prepared_tx.sign(keypair)
   send_response = server.send_transaction(prepared_tx)

   # Poll until the transaction is included in a ledger
   while True:
       tx_response = server.get_transaction(send_response.hash)
       if tx_response.status != GetTransactionStatus.NOT_FOUND:
           break
       time.sleep(2)

   if tx_response.status != GetTransactionStatus.SUCCESS:
       raise RuntimeError(f"{function_name} failed: {tx_response.result_xdr}")

   transaction_meta = xdr.TransactionMeta.from_xdr(tx_response.result_meta_xdr)
   return (transaction_meta.v4 if transaction_meta.v == 4 else transaction_meta.v3).soroban_meta.return_value


# ==========================
# Position Lifecycle
# ==========================

def run():
   # Step 1. Read the pool state
   slot0 = scval_map(simulate("get_slot0"))
   current_tick = slot0["tick"].i32.int32
   tick_spacing = simulate("get_tick_spacing").i32.int32
   current_price = 1.0001 ** current_tick
   print(f"tick={current_tick} spacing={tick_spacing} price={current_price:.7f}")

   # Step 2. Convert the target price range (±5%) to ticks
   tick_lower = price_to_tick(current_price * 0.95, tick_spacing)
   tick_upper = price_to_tick(current_price * 1.05, tick_spacing) + tick_spacing
   print(f"range: [{tick_lower}, {tick_upper}]")

   # Step 3. Estimate and open the position
   desired_amounts = [100_0000000, 100_0000000]  # raw amounts, sorted token order
   est = simulate("estimate_deposit_position", [
       scval.to_int32(tick_lower),
       scval.to_int32(tick_upper),
       scval.to_vec([scval.to_uint128(a) for a in desired_amounts]),
   ])
   est_liquidity = u128_to_int(est.vec.sc_vec[1].u128)

   result = invoke("deposit_position", [
       scval.to_address(keypair.public_key),
       scval.to_int32(tick_lower),
       scval.to_int32(tick_upper),
       scval.to_vec([scval.to_uint128(a) for a in desired_amounts]),
       scval.to_uint128(est_liquidity * 99 // 100),  # 1% slippage guard
   ])
   deposited = [u128_to_int(v.u128) for v in result.vec.sc_vec[0].vec.sc_vec]
   liquidity = u128_to_int(result.vec.sc_vec[1].u128)
   print(f"deposited {deposited}, received liquidity {liquidity}")

   # Step 4. List positions and check range status
   snapshot = scval_map(simulate("get_user_position_snapshot",
                                 [scval.to_address(keypair.public_key)]))
   for entry in snapshot["ranges"].vec.sc_vec:
       rng = scval_map(entry)
       lower = rng["tick_lower"].i32.int32
       upper = rng["tick_upper"].i32.int32
       status = "IN RANGE" if lower <= current_tick < upper else "OUT OF RANGE"
       print(f"position [{lower}, {upper}]: {status}")

   # Step 5. Check pending swap fees (claim with "claim_position_fees" when non-zero)
   fees = simulate("get_position_fees", [
       scval.to_address(keypair.public_key),
       scval.to_int32(tick_lower),
       scval.to_int32(tick_upper),
   ])
   print("pending fees:", [u128_to_int(v.u128) for v in fees.vec.sc_vec])

   # Step 6. Check pending AQUA rewards (claim with "claim" when non-zero)
   reward = u128_to_int(
       simulate("get_user_reward", [scval.to_address(keypair.public_key)]).u128
   )
   print(f"pending AQUA reward: {reward / 1e7}")

   # Step 7. Withdraw the whole position (principal + accrued fees)
   est = simulate("estimate_withdraw_position", [
       scval.to_address(keypair.public_key),
       scval.to_int32(tick_lower),
       scval.to_int32(tick_upper),
       scval.to_uint128(liquidity),
   ])
   min_amounts = [u128_to_int(v.u128) * 99 // 100 for v in est.vec.sc_vec]

   withdrawn = invoke("withdraw_position", [
       scval.to_address(keypair.public_key),
       scval.to_int32(tick_lower),
       scval.to_int32(tick_upper),
       scval.to_uint128(liquidity),
       scval.to_vec([scval.to_uint128(a) for a in min_amounts]),
   ])
   print("withdrawn:", [u128_to_int(v.u128) for v in withdrawn.vec.sc_vec])


if __name__ == "__main__":
   run()
```

</details>

<details>

<summary>Copy the full code JavaScript</summary>

```javascript
// concentrated_position.js
const StellarSdk = require('@stellar/stellar-sdk');
const {
    Address,
    Contract,
    Keypair,
    Networks,
    TransactionBuilder,
    TimeoutInfinite,
    BASE_FEE,
    nativeToScVal,
    scValToNative,
    rpc,
    xdr,
} = StellarSdk;

// ==========================
// Configuration Variables
// ==========================

const sorobanServerUrl = 'https://mainnet.sorobanrpc.com';

// User's secret key (ensure this is kept secure)
const userSecretKey = 'S...';
// Address of the concentrated liquidity pool contract
const poolAddress = 'C...';

const server = new rpc.Server(sorobanServerUrl);
const keypair = Keypair.fromSecret(userSecretKey);

// ==========================
// Utility Functions
// ==========================

function priceToTick(price, tickSpacing) {
    /** Converts a price to the nearest valid (spacing-aligned) tick below it. */
    const tick = Math.floor(Math.log(price) / Math.log(1.0001));
    return tick - ((tick % tickSpacing) + tickSpacing) % tickSpacing;
}

async function buildTx(functionName, parameters) {
    const account = await server.getAccount(keypair.publicKey());
    const contract = new Contract(poolAddress);
    return new TransactionBuilder(account, {
        fee: BASE_FEE,
        networkPassphrase: Networks.PUBLIC,
    })
        .addOperation(contract.call(functionName, ...parameters))
        .setTimeout(TimeoutInfinite)
        .build();
}

async function simulate(functionName, parameters = []) {
    /** Read-only contract call executed through simulation. No signature needed. */
    const tx = await buildTx(functionName, parameters);
    const result = await server.simulateTransaction(tx);
    if (!result.result) {
        throw new Error(`${functionName} simulation failed`);
    }
    return scValToNative(result.result.retval);
}

async function invoke(functionName, parameters) {
    /** State-changing contract call: prepare, sign, send and wait for the result. */
    const tx = await buildTx(functionName, parameters);
    const preparedTx = await server.prepareTransaction(tx);
    preparedTx.sign(keypair);

    const sendResponse = await server.sendTransaction(preparedTx);

    // Poll until the transaction is included in a ledger
    let txResponse = await server.getTransaction(sendResponse.hash);
    while (txResponse.status === 'NOT_FOUND') {
        await new Promise(resolve => setTimeout(resolve, 2000));
        txResponse = await server.getTransaction(sendResponse.hash);
    }

    if (txResponse.status !== 'SUCCESS') {
        throw new Error(`${functionName} failed: ${txResponse.status}`);
    }
    return scValToNative(txResponse.returnValue);
}

// ==========================
// Position Lifecycle
// ==========================

async function run() {
    // Step 1. Read the pool state
    const slot0 = await simulate('get_slot0');
    const currentTick = slot0.tick;
    const tickSpacing = await simulate('get_tick_spacing');
    const currentPrice = Math.pow(1.0001, currentTick);
    console.log(`tick=${currentTick} spacing=${tickSpacing} price=${currentPrice}`);

    // Step 2. Convert the target price range (±5%) to ticks
    const tickLower = priceToTick(currentPrice * 0.95, tickSpacing);
    const tickUpper = priceToTick(currentPrice * 1.05, tickSpacing) + tickSpacing;
    console.log(`range: [${tickLower}, ${tickUpper}]`);

    const user = new Address(keypair.publicKey()).toScVal();
    const i32 = value => nativeToScVal(value, { type: 'i32' });
    const u128 = value => nativeToScVal(value, { type: 'u128' });
    const u128vec = values => xdr.ScVal.scvVec(values.map(u128));

    // Step 3. Estimate and open the position
    const desiredAmounts = [1000000000n, 1000000000n]; // raw amounts, sorted token order
    const [, estLiquidity] = await simulate('estimate_deposit_position',
        [i32(tickLower), i32(tickUpper), u128vec(desiredAmounts)]);

    const [deposited, liquidity] = await invoke('deposit_position', [
        user,
        i32(tickLower),
        i32(tickUpper),
        u128vec(desiredAmounts),
        u128(estLiquidity * 99n / 100n), // 1% slippage guard
    ]);
    console.log(`deposited ${deposited}, received liquidity ${liquidity}`);

    // Step 4. List positions and check range status
    const snapshot = await simulate('get_user_position_snapshot', [user]);
    for (const { tick_lower, tick_upper } of snapshot.ranges) {
        const inRange = tick_lower <= currentTick && currentTick < tick_upper;
        console.log(`position [${tick_lower}, ${tick_upper}]: ${inRange ? 'IN RANGE' : 'OUT OF RANGE'}`);
    }

    // Step 5. Check pending swap fees (claim with "claim_position_fees" when non-zero)
    const pendingFees = await simulate('get_position_fees',
        [user, i32(tickLower), i32(tickUpper)]);
    console.log(`pending fees: ${pendingFees}`);

    // Step 6. Check pending AQUA rewards (claim with "claim" when non-zero)
    const reward = await simulate('get_user_reward', [user]);
    console.log(`pending AQUA reward: ${Number(reward) / 1e7}`);

    // Step 7. Withdraw the whole position (principal + accrued fees)
    const estAmounts = await simulate('estimate_withdraw_position',
        [user, i32(tickLower), i32(tickUpper), u128(liquidity)]);
    const minAmounts = estAmounts.map(amount => amount * 99n / 100n);

    const withdrawn = await invoke('withdraw_position',
        [user, i32(tickLower), i32(tickUpper), u128(liquidity), u128vec(minAmounts)]);
    console.log(`withdrawn: ${withdrawn}`);
}

run();
```

</details>
